Construindo uma Camada Curada de Dados com SQL Transforms (Sem dbt)
dbt é o padrão para transformação de dados baseada em SQL. Também é um projeto de 50 arquivos com profiles, sources, models, tests, macros, packages e um arquivo YAML para cada model. Para uma equipe com 5 tabelas que precisa de um star schema, isso é exagero.
O DataSpoc Lens tem uma abordagem mais simples: arquivos SQL numerados que rodam em ordem. Cada arquivo cria uma tabela a partir de uma consulta. Sem YAML, sem Jinja, sem grafo de dependências para depurar. Apenas SQL.
O Problema com dbt para Equipes Pequenas
Para construir um star schema a partir de 3 tabelas fonte no dbt, você precisa de:
my_dbt_project/ dbt_project.yml profiles.yml models/ staging/ stg_users.sql stg_users.yml stg_orders.sql stg_orders.yml stg_products.sql stg_products.yml marts/ dim_customers.sql dim_customers.yml fct_orders.sql fct_orders.yml mart_revenue.sql mart_revenue.yml tests/ ... macros/ ...São 12+ arquivos antes de escrever uma única consulta de negócio. O poder do dbt vem do gerenciamento de dependências, testes e documentação — tudo valioso em escala. Mas para uma startup com um analista, é uma semana de setup antes de entregar qualquer valor.
Lens Transforms: Arquivos SQL Numerados
No DataSpoc Lens, transforms são arquivos .sql em um diretório. São numerados para definir a ordem de execução:
transforms/ 01_clean_users.sql 02_clean_orders.sql 03_join_order_details.sql 04_daily_revenue.sql 05_customer_dashboard.sqlCada arquivo usa o padrão CTAS (CREATE TABLE AS SELECT):
-- Creates a table in the curated layerCREATE OR REPLACE TABLE curated.clean_users ASSELECT ...Execute todos os transforms:
dataspoc-lens transform runO Lens executa em ordem numérica: 01, 02, 03, 04, 05. Cada transform pode referenciar tabelas criadas por transforms anteriores.
Construindo um Star Schema: 5 Transforms
Vamos construir uma camada analítica completa a partir de dados brutos. Começamos com três tabelas fonte do Pipe:
raw.users— contas de usuáriosraw.orders— transações de pedidosraw.products— catálogo de produtos
Transform 1: Limpar Usuários
-- transforms/01_clean_users.sql-- Clean and standardize user data
CREATE OR REPLACE TABLE curated.dim_customers ASSELECT user_id, LOWER(TRIM(email)) AS email, COALESCE(name, 'Unknown') AS name, CASE WHEN plan IN ('free', 'starter') THEN 'free' WHEN plan IN ('pro', 'professional') THEN 'pro' WHEN plan IN ('business', 'enterprise') THEN 'enterprise' ELSE 'unknown' END AS plan_tier, created_at::DATE AS signup_date, DATE_DIFF('day', created_at, CURRENT_DATE) AS days_since_signup, CASE WHEN DATE_DIFF('day', created_at, CURRENT_DATE) <= 30 THEN 'new' WHEN DATE_DIFF('day', created_at, CURRENT_DATE) <= 365 THEN 'active' ELSE 'veteran' END AS customer_cohortFROM raw.usersWHERE email IS NOT NULL AND email LIKE '%@%.%';Isso lida com a realidade confusa dos dados brutos: nomes de plano inconsistentes, nomes ausentes, emails inválidos.
Transform 2: Limpar Pedidos
-- transforms/02_clean_orders.sql-- Standardize orders, remove test data, add computed fields
CREATE OR REPLACE TABLE curated.fct_orders ASSELECT order_id, user_id, product_id, amount, quantity, amount * quantity AS line_total, discount_pct, amount * quantity * (1 - COALESCE(discount_pct, 0) / 100.0) AS net_revenue, status, order_date, DATE_TRUNC('month', order_date) AS order_month, DATE_TRUNC('quarter', order_date) AS order_quarter, DAYOFWEEK(order_date) AS day_of_week, CASE WHEN HOUR(created_at) BETWEEN 6 AND 11 THEN 'morning' WHEN HOUR(created_at) BETWEEN 12 AND 17 THEN 'afternoon' WHEN HOUR(created_at) BETWEEN 18 AND 22 THEN 'evening' ELSE 'night' END AS time_of_dayFROM raw.ordersWHERE status != 'test' AND amount > 0 AND order_date >= DATE '2024-01-01';Transform 3: Join de Detalhes do Pedido
-- transforms/03_join_order_details.sql-- Denormalized order fact with customer and product dimensions
CREATE OR REPLACE TABLE curated.order_details ASSELECT o.order_id, o.order_date, o.order_month, o.net_revenue, o.status, o.time_of_day,
-- Customer dimensions c.name AS customer_name, c.email AS customer_email, c.plan_tier, c.customer_cohort, c.signup_date,
-- Product dimensions p.name AS product_name, p.category AS product_category, p.sku,
-- Calculated DATE_DIFF('day', c.signup_date, o.order_date) AS days_to_purchaseFROM curated.fct_orders oJOIN curated.dim_customers c ON o.user_id = c.user_idJOIN raw.products p ON o.product_id = p.product_idWHERE o.status = 'completed';Transform 4: Agregação de Receita Diária
-- transforms/04_daily_revenue.sql-- Daily revenue metrics for dashboards
CREATE OR REPLACE TABLE gold.daily_revenue ASSELECT order_date, COUNT(DISTINCT order_id) AS total_orders, COUNT(DISTINCT customer_email) AS unique_customers, SUM(net_revenue) AS revenue, ROUND(AVG(net_revenue), 2) AS avg_order_value, ROUND(MEDIAN(net_revenue), 2) AS median_order_value,
-- By plan tier SUM(CASE WHEN plan_tier = 'free' THEN net_revenue ELSE 0 END) AS free_revenue, SUM(CASE WHEN plan_tier = 'pro' THEN net_revenue ELSE 0 END) AS pro_revenue, SUM(CASE WHEN plan_tier = 'enterprise' THEN net_revenue ELSE 0 END) AS enterprise_revenue,
-- By category SUM(CASE WHEN product_category = 'software' THEN net_revenue ELSE 0 END) AS software_revenue, SUM(CASE WHEN product_category = 'services' THEN net_revenue ELSE 0 END) AS services_revenue,
-- Running totals (filled in by window) SUM(SUM(net_revenue)) OVER ( ORDER BY order_date ROWS BETWEEN 6 PRECEDING AND CURRENT ROW ) AS revenue_7d_rollingFROM curated.order_detailsGROUP BY order_dateORDER BY order_date;Transform 5: Visão do Dashboard de Clientes
-- transforms/05_customer_dashboard.sql-- Per-customer summary for the analytics dashboard
CREATE OR REPLACE TABLE gold.customer_dashboard ASSELECT customer_email, customer_name, plan_tier, customer_cohort, signup_date,
COUNT(*) AS total_orders, SUM(net_revenue) AS lifetime_value, ROUND(AVG(net_revenue), 2) AS avg_order_value, MIN(order_date) AS first_order, MAX(order_date) AS last_order, DATE_DIFF('day', MAX(order_date), CURRENT_DATE) AS days_since_last_order,
CASE WHEN DATE_DIFF('day', MAX(order_date), CURRENT_DATE) <= 30 THEN 'active' WHEN DATE_DIFF('day', MAX(order_date), CURRENT_DATE) <= 90 THEN 'at_risk' ELSE 'churned' END AS activity_status,
-- Most purchased category MODE(product_category) AS favorite_categoryFROM curated.order_detailsGROUP BY 1, 2, 3, 4, 5;Executando os Transforms
# Run all transforms in orderdataspoc-lens transform run
# Run a specific transformdataspoc-lens transform run --file 04_daily_revenue.sql
# Dry run: show the SQL without executingdataspoc-lens transform run --dry-runSaída:
[10:15:01] Running 5 transforms...[10:15:01] 01_clean_users.sql → curated.dim_customers (12,340 rows)[10:15:02] 02_clean_orders.sql → curated.fct_orders (87,650 rows)[10:15:03] 03_join_order_details.sql → curated.order_details (85,200 rows)[10:15:04] 04_daily_revenue.sql → gold.daily_revenue (480 rows)[10:15:04] 05_customer_dashboard.sql → gold.customer_dashboard (11,890 rows)[10:15:04] All transforms complete (3.2s)Consulte os Resultados
# Revenue trendsdataspoc-lens query " SELECT order_date, revenue, revenue_7d_rolling FROM gold.daily_revenue WHERE order_date >= DATE '2026-04-01' ORDER BY order_date"
# At-risk customersdataspoc-lens query " SELECT customer_name, lifetime_value, days_since_last_order FROM gold.customer_dashboard WHERE activity_status = 'at_risk' ORDER BY lifetime_value DESC LIMIT 20"
# Or use AIdataspoc-lens ask "Which product categories are growing fastest this quarter?"Lens Transforms vs. dbt: Quando Usar Cada Um
| Aspecto | Lens Transforms | dbt |
|---|---|---|
| Tempo de setup | 0 (crie arquivos .sql) | Horas (projeto, profiles, packages) |
| Gerenciamento de dependências | Ordenação numérica | DAG via ref() |
| Testes | Verificações SQL manuais | Testes integrados |
| Documentação | Comentários no SQL | YAML + docs auto-gerados |
| Modelos incrementais | Full refresh | Materialização incremental |
| CI/CD | Execute em qualquer script | dbt Cloud, GitHub Actions |
| Curva de aprendizado | Sabe SQL = pronto | SQL + Jinja + YAML + conceitos dbt |
Quando Lens Transforms São Melhores
- Você tem menos de 20 transforms
- Uma ou duas pessoas mantêm o pipeline
- Dependências são simples e lineares
- Você quer começar a produzir valor hoje, não na próxima semana
- Você não precisa de materialização incremental (seus dados cabem na memória)
Quando dbt É Melhor
- Você tem 100+ models com dependências complexas
- Múltiplas equipes contribuem transforms
- Você precisa de modelos incrementais (bilhões de linhas)
- Você precisa de testes e documentação integrados
- Você tem uma equipe dedicada de analytics engineering
Agendando Transforms
Combine transforms com extração do Pipe em um cron job:
# Run every hour: extract then transform0 * * * * dataspoc-pipe run postgres-production && dataspoc-lens transform runOu em um shell script com tratamento de erros:
#!/bin/bash# etl.sh - Extract and transform
set -e
echo "[$(date)] Starting ETL..."
# Extractdataspoc-pipe run postgres-productionecho "[$(date)] Extraction complete"
# Transformdataspoc-lens transform runecho "[$(date)] Transforms complete"
# Refresh cache for fast queriesdataspoc-lens cache refresh-staleecho "[$(date)] Cache refreshed"
echo "[$(date)] ETL complete"Cinco arquivos SQL. Sem Jinja. Sem YAML. Sem grafo de dependências. Apenas transforms numerados que constroem uma camada analítica limpa a partir de dados brutos. Quando suas necessidades superarem isso, migre para dbt. Até lá, entregue valor hoje.