Estudo Aprofundado
dbt
Do SELECT mais simples aos data contracts e ao Semantic Layer:
como o dbt transformou SQL em engenharia de software — modelos versionados, testados, documentados e orquestrados —
e por que virou a ferramenta de transformação mais demandada do mercado de engenharia de dados.
Cada tópico traz teoria, exemplo prático, um caso de uso real e link para a documentação oficial.
pip install dbt-core dbt-<adapter> (ex.: dbt-snowflake, dbt-databricks, dbt-postgres, dbt-duckdb)dbt init, que gera a estrutura de pastas e o profiles.yml de conexãodbt compile para ver o SQL puro gerado a partir do Jinja antes de rodar de verdadedbt-duckdb com arquivos locais — o mesmo raciocínio de produção, em escala de notebookFundamentos
O que o dbt resolve e como um projeto é organizado
dbt (data build tool) não extrai nem carrega dados — ele assume que o dado bruto já está no warehouse (ELT) e cuida só da transformação: SQL organizado em arquivos .sql, versionados no Git, com dependências resolvidas automaticamente, testes de dados e documentação gerada.
Antes do dbt, transformação em SQL geralmente vivia em scripts soltos, procedures ou notebooks sem controle de versão nem teste — dbt trouxe para o SQL analítico práticas que engenharia de software já tinha havia décadas.
SELECT — o dbt não escreve o dado, ele compila o Jinja em SQL puro e executa um CREATE TABLE AS ou CREATE VIEW AS no warehouse. Isso significa que o dbt nunca move dados entre sistemas: tudo acontece dentro do mesmo warehouse/lakehouse onde os dados já residem — daí ele ser a peça "T" clássica de uma stack ELT (Airbyte/Fivetran fazem o EL, dbt faz o T).
dbt_project.yml declara o nome do projeto e as configurações padrão de cada pasta de models. profiles.yml guarda as credenciais de conexão com o warehouse — fica fora do projeto (normalmente em ~/.dbt/) e nunca é versionado, porque tem segredos.
dbt_project.yml (o quê) e profiles.yml (onde/como conectar) é o que permite o mesmo projeto rodar em dev, CI e produção sem alterar uma linha de código — só o target ativo muda. Cada profile pode ter múltiplos targets (ex.: dev e prod), cada um apontando para um schema/warehouse diferente.# dbt_project.yml name: 'loja_analytics' version: '1.0.0' profile: 'loja_analytics' model-paths: ["models"] seed-paths: ["seeds"] snapshot-paths: ["snapshots"] models: loja_analytics: staging: +materialized: view marts: +materialized: table
dbt init gera a estrutura inicial de pastas do zerodbt debug testa a conexão do profiles.yml com o warehouse
Cada arquivo .sql dentro de models/ é um model: o nome do arquivo vira o nome da view/tabela, e o conteúdo é apenas um SELECT — sem CREATE TABLE, sem INSERT. O dbt cuida da DDL/DML por trás.
dbt run executa todos os models do projeto na ordem certa; dbt run --select stg_pedidos roda só um model específico.-- models/staging/stg_pedidos.sql select id as pedido_id, cliente_id, valor_total, criado_em as data_pedido from {{ source('vendas', 'pedidos') }}
view recalcula a query a cada leitura (zero armazenamento extra, mais lento); table materializa fisicamente o resultado (mais rápido de ler, ocupa espaço e leva tempo de build); ephemeral nem chega a existir como objeto — é injetado como CTE dentro de quem o referencia.
SELECT — o dbt gera SQL diferente por trás de cada uma: para table, um CREATE TABLE AS SELECT (ou DROP + recriação); para view, um CREATE VIEW AS; para ephemeral, nenhum DDL — o SQL é literalmente colado como CTE no model consumidor no momento da compilação. Trocar a materialização não muda o resultado lógico, só a característica de custo/performance de leitura vs. escrita.ephemeral não pode ser consultado fora do dbt (não existe no warehouse) e, encadeado em excesso, gera CTEs profundas difíceis de debugar — use para models pequenos e intermediários, não para camadas inteiras.view — barata, sempre atualizadatable — performance de leitura para BIModelagem e o DAG
ref(), source() e as camadas de um projeto maduro
source() aponta para uma tabela bruta já existente no warehouse (fora do controle do dbt); ref() aponta para outro model dbt. Nunca escreva o nome do schema/tabela na mão — sempre ref()/source().
{{ ref('stg_pedidos') }}, o dbt não insere o nome literal — ele resolve o nome completo e qualificado da tabela/view naquele ambiente (dev pode usar um schema com seu prefixo, prod usa o schema de produção). É justamente por parsear cada chamada de ref()/source() no projeto inteiro que o dbt constrói o DAG (grafo de dependências) automaticamente e decide sozinho a ordem de execução — nenhuma orquestração manual é necessária dentro do próprio dbt.dbt docs generate + dbt docs serve mostra esse DAG visualmente — ótimo para revisar o impacto de uma mudança antes de fazer merge.-- models/marts/fct_pedidos.sql select p.pedido_id, p.cliente_id, c.nome as nome_cliente, p.valor_total from {{ ref('stg_pedidos') }} p left join {{ ref('stg_clientes') }} c on p.cliente_id = c.cliente_id
staging (stg_*) faz limpeza 1:1 com a fonte — renomeia colunas, converte tipos, nada de joins. intermediate (int_*) combina/agrega lógica reutilizável. marts (fct_*/dim_*) entrega o modelo final consumido pelo BI.
Todo {{ ... }} ou {% ... %} num model é Jinja, o motor de templates Python que o dbt usa para gerar SQL dinamicamente — config() no topo de um arquivo sobrescreve as configurações do dbt_project.yml só para aquele model.
dbt compile é a ferramenta de debug nº1: mostra exatamente o SQL que será enviado.{{ config(materialized='table', tags=['diario']) }}
select
regiao,
{% for ano in [2024, 2025, 2026] %}
sum(case when ano_pedido = {{ ano }} then valor end) as receita_{{ ano }}{% if not loop.last %},{% endif %}
{% endfor %}
from {{ ref('fct_pedidos') }}
group by 1
Um macro é uma função Jinja reutilizável escrita em macros/*.sql — encapsula lógica SQL repetida (uma fórmula de negócio, uma conversão de fuso horário) num único lugar chamado de vários models.
-- macros/centavos_para_reais.sql {% macro centavos_para_reais(coluna) %} ( {{ coluna }} / 100.0 ) {% endmacro %} -- uso em um model select {{ centavos_para_reais('valor_em_centavos') }} as valor_reais from {{ ref('stg_pedidos') }}
Testes e Documentação
Confiança versionada junto com o código
Testes vivem em arquivos .yml ao lado dos models, não em código Python separado. Os 4 testes nativos cobrem a maioria dos casos: chave única, coluna obrigatória, integridade referencial e domínio de valores.
SELECT que deve retornar zero linhas para passar — unique vira um GROUP BY ... HAVING COUNT(*) > 1, por exemplo. É por isso que testes dbt rodam inteiramente dentro do warehouse (dbt test só dispara SQL) e escalam junto com o volume de dados, sem precisar extrair nada para fora.# models/marts/_marts.yml models: - name: fct_pedidos columns: - name: pedido_id tests: [unique, not_null] - name: cliente_id tests: - relationships: to: ref('dim_clientes') field: cliente_id - name: status tests: - accepted_values: values: ['pago', 'cancelado', 'pendente']
Um singular test é um arquivo .sql em tests/ com uma query livre que também precisa retornar zero linhas — para regras de negócio que não cabem nos 4 testes genéricos. Pacotes como dbt-utils e dbt_expectations adicionam dezenas de testes genéricos prontos (faixas de valor, distribuição, formato de string).
not_null nem unique — vira um singular test com uma condição livre.-- tests/assert_entrega_apos_pedido.sql select * from {{ ref('fct_pedidos') }} where data_entrega < data_pedido
Descrições de models e colunas ficam no mesmo .yml dos testes. dbt docs generate lê o projeto inteiro e monta um site estático navegável, com o DAG completo — quem depende de quem, até a fonte bruta.
Um contract declara explicitamente nome, tipo e constraints (not null, primary key) de cada coluna. Se o SELECT do model produzir algo diferente do declarado, o build falha antes de gravar no warehouse.
ALTER silencioso de tipo (um INT que virou BIGINT na fonte) só é percebido quando algo consumidor quebra rio abaixo — às vezes um dashboard de produção. O contract move essa verificação para o momento do dbt build, tratando a interface do model como um contrato de API entre times, não como um detalhe implícito de implementação.models: - name: fct_pedidos config: contract: enforced: true columns: - name: pedido_id data_type: bigint constraints: - type: not_null - name: valor_total data_type: decimal(18,2)
Incrementalidade
Quando reprocessar tudo todo dia não escala mais
Um model incremental só reprocessa linhas novas/alteradas depois do primeiro build completo. A macro is_incremental() retorna true apenas em runs subsequentes, permitindo um WHERE de filtro que não existe na primeira execução.
incremental_strategy) define como as linhas novas se combinam com a tabela existente: append só insere (risco de duplicar se reprocessar a mesma janela); merge faz upsert por chave (a mais usada, exige suporte a MERGE no warehouse); insert_overwrite substitui partições inteiras (rápido em warehouses particionados como BigQuery); microbatch (mais recente) processa a incremental em lotes de tempo previsíveis, com reprocessamento automático de lotes com falha.dbt run --full-refresh para reconstruir a tabela do zero com a nova regra.{{ config(
materialized='incremental',
unique_key='pedido_id',
incremental_strategy='merge'
) }}
select *
from {{ source('vendas', 'pedidos') }}
{% if is_incremental() %}
where atualizado_em > (select max(atualizado_em) from {{ this }})
{% endif %}
A fonte só guarda o estado atual de uma linha (ex.: endereço do cliente hoje) — se ela mudar, o valor antigo se perde para sempre. snapshots resolvem isso: a cada run, comparam o estado atual da fonte com o snapshot anterior e inserem uma nova versão da linha quando algo muda, preservando a antiga com data de validade.
dbt_valid_from/dbt_valid_to, e a versão vigente tem dbt_valid_to IS NULL. A estratégia timestamp detecta mudança por uma coluna de updated_at; a check compara colunas específicas linha a linha quando a fonte não tem timestamp confiável.-- snapshots/clientes_snapshot.sql {% snapshot clientes_snapshot %} {{ config( target_schema='snapshots', unique_key='cliente_id', strategy='timestamp', updated_at='atualizado_em' ) }} select * from {{ source('vendas', 'clientes') }} {% endsnapshot %}
Um seed é um CSV pequeno em seeds/ que o dbt carrega como tabela via dbt seed — útil para listas fixas (mapeamento de UF para região, categorias de produto) que não existem em nenhum sistema de origem.
Recursos avançados
Packages, hooks, métricas e controle de ambiente
Um package dbt é um projeto dbt inteiro (macros, models, testes) publicado no dbt Hub, instalado via packages.yml e dbt deps. dbt-utils (funções genéricas), dbt_expectations (testes ao estilo Great Expectations) e codegen (gera YAML de sources/models automaticamente) são os mais usados do mercado.
# packages.yml packages: - package: dbt-labs/dbt_utils version: [">=1.0.0", "<2.0.0"] - package: calogica/dbt_expectations version: [">=0.10.0", "<0.11.0"]
pre-hook/post-hook rodam SQL antes/depois de um model específico ser materializado (ex.: conceder permissões numa tabela recém-criada). on-run-start/on-run-end rodam uma vez por invocação do dbt inteira. dbt run-operation executa um macro isoladamente, sem nenhum model — útil para scripts de manutenção.
GRANT SELECT automático para o role de BI — um post-hook genérico aplicado a toda a pasta marts resolve isso sem intervenção manual em cada model novo.# dbt_project.yml models: loja_analytics: marts: +post-hook: "grant select on {{ this }} to role bi_reader"
O Semantic Layer permite definir uma métrica (ex.: "receita líquida") uma única vez, em cima de um model, com o motor MetricFlow — e consultá-la de forma consistente a partir de SQL, Python, Power BI, Tableau ou qualquer ferramenta integrada, sem reescrever a fórmula em cada uma.
# models/marts/_metrics.yml metrics: - name: receita_liquida label: "Receita Líquida" type: simple type_params: measure: valor_total filter: "{{ Dimension('status') }} != 'cancelado'"
--vars injeta valores em tempo de execução (ex.: uma data de corte) sem editar código. --select state:modified+ compara o projeto atual com um manifest.json de produção salvo e roda só os models alterados (e seus downstream) — a base do "Slim CI".
--defer combinado com state:modified permite rodar um PR de CI construindo apenas os models tocados, enquanto ref() para models não tocados resolve para o objeto já existente em produção em vez de tentar reconstruí-lo num schema temporário vazio. Isso reduz o tempo e o custo de CI de "rodar o projeto inteiro" para "rodar só o que mudou", mesmo em projetos com milhares de models.Produção & Orquestração
Onde o dbt entra numa stack real, e onde não entra
dbt Core é a CLI open-source: você orquestra e agenda por fora (cron, Airflow). dbt Cloud (evoluindo para o motor dbt Fusion) adiciona IDE web, agendamento nativo, ambientes gerenciados, Slim CI pronto e o Semantic Layer hospedado — sem infraestrutura própria para manter.
O dbt não é um agendador — algo precisa disparar dbt build periodicamente. Airflow tem o cosmos (Astronomer) e operadores nativos que expandem o DAG do dbt em tasks individuais dentro do próprio Airflow; Dagster trata cada model dbt como um asset de primeira classe, herdando o DAG do dbt automaticamente.
dbt-databricks) e pode ser agendado direto como uma task de Databricks Workflows/Lakeflow Jobs — dispensando um orquestrador externo quando o resto do pipeline já vive na mesma plataforma. Fora do Databricks, expandir o dbt dentro de um DAG do Airflow (em vez de um único node "rodar dbt") dá granularidade de retry e alerta por model, não só por pipeline inteiro.
Um pipeline maduro roda dbt build (models + testes juntos, na ordem certa) a cada PR usando Slim CI, e falha o merge se um teste quebrar. O pacote elementary lê os metadados de execução do próprio dbt (run_results.json) e monta dashboards de observabilidade — falhas de teste, freshness, anomalias — sem ferramenta paga.
stg_pedidos. O CI roda automaticamente só esse model e seus downstream (via state:modified+), os testes de fct_pedidos quebram, e o merge é bloqueado antes que o erro chegue à produção.O dbt é a escolha natural sempre que a transformação acontece dentro de um warehouse/lakehouse com SQL — que é o caso da maioria absoluta dos pipelines analíticos modernos.
dbt → BIdbt rodando via SQL Warehouse do Databricks ou Trino