sqltransformsdata-modelingdbt-alternativeanalytics

Construindo uma Camada Curada de Dados com SQL Transforms (Sem dbt)

Michael San Martim · 2026-04-27

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.sql

Cada arquivo usa o padrão CTAS (CREATE TABLE AS SELECT):

-- Creates a table in the curated layer
CREATE OR REPLACE TABLE curated.clean_users AS
SELECT ...

Execute todos os transforms:

Terminal window
dataspoc-lens transform run

O 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ários
  • raw.orders — transações de pedidos
  • raw.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 AS
SELECT
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_cohort
FROM raw.users
WHERE 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 AS
SELECT
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_day
FROM raw.orders
WHERE 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 AS
SELECT
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_purchase
FROM curated.fct_orders o
JOIN curated.dim_customers c ON o.user_id = c.user_id
JOIN raw.products p ON o.product_id = p.product_id
WHERE 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 AS
SELECT
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_rolling
FROM curated.order_details
GROUP BY order_date
ORDER 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 AS
SELECT
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_category
FROM curated.order_details
GROUP BY 1, 2, 3, 4, 5;

Executando os Transforms

Terminal window
# Run all transforms in order
dataspoc-lens transform run
# Run a specific transform
dataspoc-lens transform run --file 04_daily_revenue.sql
# Dry run: show the SQL without executing
dataspoc-lens transform run --dry-run

Saí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

Terminal window
# Revenue trends
dataspoc-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 customers
dataspoc-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 AI
dataspoc-lens ask "Which product categories are growing fastest this quarter?"

Lens Transforms vs. dbt: Quando Usar Cada Um

AspectoLens Transformsdbt
Tempo de setup0 (crie arquivos .sql)Horas (projeto, profiles, packages)
Gerenciamento de dependênciasOrdenação numéricaDAG via ref()
TestesVerificações SQL manuaisTestes integrados
DocumentaçãoComentários no SQLYAML + docs auto-gerados
Modelos incrementaisFull refreshMaterialização incremental
CI/CDExecute em qualquer scriptdbt Cloud, GitHub Actions
Curva de aprendizadoSabe SQL = prontoSQL + 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:

Terminal window
# Run every hour: extract then transform
0 * * * * dataspoc-pipe run postgres-production && dataspoc-lens transform run

Ou em um shell script com tratamento de erros:

#!/bin/bash
# etl.sh - Extract and transform
set -e
echo "[$(date)] Starting ETL..."
# Extract
dataspoc-pipe run postgres-production
echo "[$(date)] Extraction complete"
# Transform
dataspoc-lens transform run
echo "[$(date)] Transforms complete"
# Refresh cache for fast queries
dataspoc-lens cache refresh-stale
echo "[$(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.

Recomendados