Guia Visual

Modelos Incrementais

Um modelo incremental processa apenas as linhas que mudaram desde a última execução, em vez de reconstruir a tabela inteira do zero a cada vez.

O problema de reconstruir tudo

Por padrão, um modelo table no dbt se reconstrói completamente a cada execução: drop, re-select, re-insert de todas as linhas. Para uma dimensão de 10.000 linhas isso é tranquilo. Para uma tabela de eventos com dois bilhões de linhas, é uma query lenta e cara que você roda repetidamente para recalcular linhas que não mudaram desde ontem.

Um modelo incremental quebra esse padrão. Na primeira execução ele constrói a tabela inteira. Em todas as execuções seguintes, processa apenas as linhas novas ou alteradas e as incorpora na tabela que já existe.

Comece com a materialização table ou view a menos que tenha uma razão concreta para mudar. Rebuilds completos são mais simples, mais fáceis de debugar e garantem correção. Use incremental quando uma tabela é genuinamente grande demais e lenta demais para reconstruir.

Como funciona

Duas peças fazem um modelo incremental funcionar: uma config que marca o modelo como incremental, e um bloco is_incremental() que filtra apenas as linhas novas.

{{ config(materialized='incremental', unique_key='order_id') }}

select * from {{ source('shop', 'orders') }}

{% if is_incremental() %}
  where updated_at > (select max(updated_at) from {{ this }})
{% endif %}

is_incremental() retorna true somente quando as três condições se cumprem: a tabela de destino já existe, o modelo está materializado como incremental e --full-refresh não foi passado. Quando é true, a cláusula where está ativa e apenas as linhas novas chegam à lógica incremental.

{{ this }} é uma variável Jinja do dbt que resolve para o nome completo da tabela de destino do próprio modelo, por exemplo analytics.fct_orders. O dbt a substitui em tempo de compilação, então o sub-select sempre escaneia a tabela de destino ativa, e não uma source.

As cinco estratégias

A incremental_strategy diz ao dbt o que fazer com as linhas que passam pelo filtro is_incremental(). Escolha com base no seu warehouse, no formato dos dados e nos requisitos de correção.

Full refresh e primeira execução

Quando is_incremental() é false: na primeira execução de um modelo, ou quando você passa --full-refresh, o dbt ignora toda a lógica incremental e reconstrói a tabela inteira do zero. Cada widget de estratégia abaixo tem um toggle --full-refresh para revisitar isso a qualquer momento.

select * from {{ source('shop', 'orders') }}
{% if is_incremental() %}
  where updated_at > (select max(updated_at) from {{ this }})
{% endif %}

fct_orders: antes

linhas desta execução

fct_orders: depois

append

Insere cada linha recebida sem verificar duplicatas.

{{ config(
  materialized='incremental',
  incremental_strategy='append'
) }}

Rápido e barato. Funciona para tabelas de eventos onde o filtro is_incremental() garante linhas sem sobreposição, ou onde os modelos downstream deduplicam. Se o modelo rodar novamente e o filtro não for restrito o suficiente, você terá duplicatas; avance pelo widget abaixo e veja o pedido 3 ser inserido duas vezes.

select * from {{ source('shop', 'orders') }}
{% if is_incremental() %}
  where updated_at > (select max(updated_at) from {{ this }})
{% endif %}

fct_orders: antes

linhas desta execução

fct_orders: depois

merge

Compara as linhas recebidas com a tabela existente pela unique_key. Linhas que batem são atualizadas no lugar; linhas que não batem são inseridas. Estratégia padrão no Snowflake, BigQuery, Postgres e Redshift quando unique_key está configurado.

{{ config(
  materialized='incremental',
  unique_key='order_id'
) }}

A estratégia mais amplamente útil. Avance pelo widget abaixo, depois desative unique_key configured para ver o que acontece sem ele; o pedido 3 é duplicado em vez de atualizado, o erro clássico de incremental.

select * from {{ source('shop', 'orders') }}
{% if is_incremental() %}
  where updated_at > (select max(updated_at) from {{ this }})
{% endif %}

fct_orders: antes

linhas desta execução

fct_orders: depois

delete+insert

Apaga cada linha existente cuja chave aparece no lote recebido e depois insere o lote completo. Produz a mesma tabela final que o merge, mas usa DELETE + INSERT em vez de um MERGE statement. O widget mostra os dois SQL statements e a tag “replaced” no resultado; mesmo estado final que o merge, mecânica diferente.

{{ config(
  materialized='incremental',
  unique_key='order_id',
  incremental_strategy='delete+insert'
) }}
select * from {{ source('shop', 'orders') }}
{% if is_incremental() %}
  where updated_at > (select max(updated_at) from {{ this }})
{% endif %}

fct_orders: antes

linhas desta execução

fct_orders: depois

insert_overwrite

Substitui partições inteiras em vez de linhas individuais. Sem unique_key; a granularidade é a partição, não a linha. Suportado no Snowflake, Databricks, Spark, Redshift e Postgres. Não suportado no BigQuery.

{{ config(
  materialized='incremental',
  incremental_strategy='insert_overwrite',
  partition_by={'field': 'created_date', 'data_type': 'date'}
) }}

O risco principal: se o lote de uma partição não contiver todas as linhas que existiam antes, as linhas ausentes são perdidas definitivamente. No exemplo abaixo, e6 e e7 existiam na partição de Jan 15 mas não estavam no lote; elas são perdidas permanentemente após o overwrite.

fct_events: antes

Jan 13
e1 click
e2 view
Jan 14
e3 click
e4 purchase
Jan 15 partição desta execução
e5 click
e6 view
e7 purchase

lote chegando, só 15 de jan

e5 scroll atualizado
e8 purchase novo

e6 e e7 não estão neste lote.

fct_events: depois

Jan 13
e1 click
e2 view
Jan 14
e3 click
e4 purchase
Jan 15 partição substituída
e5 scroll novo
e8 purchase novo
⚠ e6 (view) e e7 (purchase) estavam em 15 de jan mas não estavam no lote; perdidos permanentemente

microbatch

Divide a execução em uma query delimitada por período de tempo (hora, dia, mês). Cada batch é independente, então o dbt pode rodá-los em paralelo e dar retry apenas nos que falharam. Feito para tabelas grandes de série temporal onde uma única query incremental é lenta demais ou propensa a erros demais.

{{ config(
  materialized='incremental',
  incremental_strategy='microbatch',
  event_time='created_at',
  batch_size='day'
) }}

Suportado no Snowflake, Databricks, Spark, Trino e Athena. Ainda não disponível no Postgres, BigQuery ou Redshift.

Em vez de uma query com um filtro is_incremental(), o dbt divide a execução em uma query delimitada por período, aqui por dia. Cada batch é independente, então o dbt pode rodá-los em paralelo e fazer retry só dos que falharem.

{{ config(
    materialized='incremental',
    incremental_strategy='microbatch',
    event_time='created_at',
    batch_size='day',
    begin='2024-01-01',
) }}
select * from {{ ref('page_views') }}
Jun 9
Jun 10
Jun 11
Jun 12
Jun 13
Jun 14
Jun 15hoje
  • já carregado
  • batches desta execução
  • batch com falha

Config avançado

on_schema_change

Controla o que acontece quando você adiciona, remove ou altera colunas no SQL do modelo. O padrão ignore descarta silenciosamente as novas colunas; é uma fonte frequente de schema drift que só aparece mais tarde.

No widget abaixo, o SQL do modelo muda de [order_id, amount, legacy_field] para [order_id, amount, status]. Escolha cada opção para ver como a tabela de destino fica após a próxima execução incremental.

Tabela destino (atual)
order_id amount legacy_field
Colunas do novo SQL do model
order_id amount status

merge_update_columns / merge_exclude_columns

Restringe quais colunas são atualizadas durante um merge. Útil quando algumas colunas são definidas uma vez na inserção e nunca devem ser sobrescritas; por exemplo, created_at.

{{ config(
  materialized='incremental',
  unique_key='order_id',
  merge_update_columns=['status', 'updated_at']
) }}

incremental_predicates

Limita o scan da tabela de destino existente durante o merge, reduzindo o custo em tabelas grandes. Use DBT_INTERNAL_DEST para referenciar o destino e DBT_INTERNAL_SOURCE para o novo lote.

{{ config(
  materialized='incremental',
  unique_key='order_id',
  incremental_predicates=["DBT_INTERNAL_DEST.created_date >= dateadd('day', -7, current_date)"]
) }}

Isso diz ao dbt: procure linhas correspondentes apenas nos últimos 7 dias da tabela de destino, não em todo o histórico. Certifique-se de que o filtro is_incremental() produz linhas apenas dentro da janela do predicate; caso contrário, as linhas existentes fora dela não serão atualizadas.


Use o widget abaixo para comparar as três estratégias por linha lado a lado. Todos os controles são ao vivo.

is_incremental() gates a where filter; the incremental_strategy decides how the rows it lets through land in the table. Flip the switches to see what each run writes to fct_orders.

select * from {{ source('shop', 'orders') }}
{% if is_incremental() %}
  where updated_at > (select max(updated_at) from {{ this }})
{% endif %}

dbt run -s fct_orders

fct_orders: before

rows this run processes

fct_orders: after

Quando usar este padrão

Use um modelo incremental quando uma tabela é grande e majoritariamente append (event logs, clickstream, orders) e um rebuild completo ficou lento ou caro demais. Para tabelas pequenas, um modelo table simples é mais direto e as partes móveis extras não valem a pena.