dbt Incremental Models

Incremental models process only new or changed data rather than rebuilding the entire table on every run. For large tables with millions of rows, this difference means a run that would take hours completes in minutes. Incremental models are the most important performance tool in dbt for handling big datasets.

The Core Idea

Full table refresh (materialized='table'):
  Every run: delete all rows, insert all rows from scratch
  Run 1:  insert 10,000,000 rows  (10 minutes)
  Run 2:  insert 10,001,000 rows  (10 minutes)
  Run 3:  insert 10,002,000 rows  (10 minutes)

Incremental model (materialized='incremental'):
  Run 1:  insert 10,000,000 rows  (10 minutes) ← full load first time
  Run 2:  insert 1,000 new rows   (2 seconds)
  Run 3:  insert 1,000 new rows   (2 seconds)

Basic Incremental Model Syntax

-- models/fct_events.sql
{{ config(materialized='incremental') }}

select
    event_id,
    user_id,
    event_type,
    event_timestamp,
    properties
from {{ source('analytics', 'raw_events') }}

{% if is_incremental() %}
  -- Only load rows newer than the latest row already in this table
  where event_timestamp > (select max(event_timestamp) from {{ this }})
{% endif %}

Key Parts Explained

materialized='incremental' — Tells dbt to use incremental logic.

{% if is_incremental() %} — This block runs only on incremental runs (not the first full load).

{{ this }} — Refers to the current state of the incremental table already in the warehouse.

First Run vs Subsequent Runs

First run:
  is_incremental() = False
  WHERE clause is skipped
  All rows from the source load into a new table
  Result: table with 10,000,000 rows

Second run (next day, 1,000 new events arrived):
  is_incremental() = True
  WHERE clause: event_timestamp > '2025-06-30 23:59:59'
  Only 1,000 new rows are selected
  dbt inserts these 1,000 rows into the existing table
  Result: table now has 10,001,000 rows

The unique_key Configuration

By default, incremental models only append new rows. If your source data can update existing records (orders changing status, customers updating their profile), you need a unique_key to handle updates:

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

select
    order_id,
    status,
    updated_at,
    amount_dollars
from {{ source('ecommerce', 'orders') }}

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

With unique_key='order_id': if a new row arrives with the same order_id as an existing row, dbt replaces the existing row rather than appending a duplicate.

When to Filter Incrementally

Pick the best column for your WHERE clause:

Column Type           Use Case
-----------           --------
event_timestamp       Event streams (each event has a creation time)
updated_at            Records that can change (orders, customers)
load_date             Batch-loaded data with a daily partition column
_fivetran_synced      Fivetran-loaded data (Fivetran adds this column)

Full Refresh Override

When you change the model's logic significantly or add new columns, rebuild the entire table from scratch:

dbt run --select fct_events --full-refresh

The --full-refresh flag forces is_incremental() to return False, causing dbt to drop and recreate the table from scratch.

Late-Arriving Data

Some source systems send data late. An event that occurred three days ago might arrive in the raw table today. A simple max timestamp filter would miss these late rows.

-- Handle late-arriving data with a lookback window
{% if is_incremental() %}
  where event_timestamp > (
    select dateadd(day, -3, max(event_timestamp))  -- 3-day lookback
    from {{ this }}
  )
{% endif %}

This re-processes the last three days on every run to catch any late-arriving events.

Incremental Model Checklist

[ ] Set materialized='incremental' in config block
[ ] Add is_incremental() conditional WHERE clause
[ ] Choose the right timestamp column for filtering
[ ] Set unique_key if records can be updated (not just inserted)
[ ] Test with --full-refresh after changing model logic or adding columns
[ ] Consider late-arriving data and add a lookback window if needed

Leave a Comment

Your email address will not be published. Required fields are marked *