Snapshots are dbt's built-in Type 2 SCD tool.
The snapshot block
-- snapshots/customers_snapshot.sql
{% snapshot customers_snapshot %}
{{
config(
target_schema='snapshots',
unique_key='customer_id',
strategy='timestamp',
updated_at='updated_at',
)
}}
SELECT id AS customer_id, email, country, segment, updated_at
FROM {{ source('shopify', 'customers') }}
{% endsnapshot %}
Run dbt snapshot. First run creates the table with dbt_valid_from, dbt_valid_to, dbt_scd_id. Subsequent runs detect changes, expire old rows, insert new versions.
Two strategies
timestamp
Uses source's updated_at. Row is "changed" if updated_at is newer.
strategy='timestamp', updated_at='updated_at',
check
Compares specific columns row-by-row.
strategy='check', check_cols=['email', 'country', 'segment'],
Use when source has no reliable updated_at.
Columns dbt adds
dbt_scd_id— surrogate key for this version.dbt_valid_from— when this version became active.dbt_valid_to— when superseded (NULL = current).dbt_updated_at— source updated_at value.
Querying
Current state:
SELECT * FROM {{ ref('customers_snapshot') }} WHERE dbt_valid_to IS NULL
State at a specific time:
SELECT * FROM {{ ref('customers_snapshot') }}
WHERE customer_id = 7
AND dbt_valid_from <= '2026-04-01'
AND (dbt_valid_to > '2026-04-01' OR dbt_valid_to IS NULL)
Joining facts to snapshots
The point — facts join to the version active at fact time:
SELECT o.order_id, c.country, c.segment
FROM {{ ref('fct_orders') }} o
LEFT JOIN {{ ref('customers_snapshot') }} c
ON c.customer_id = o.customer_id
AND o.order_placed_at >= c.dbt_valid_from
AND (o.order_placed_at < c.dbt_valid_to OR c.dbt_valid_to IS NULL)
Where to put snapshots
Separate snapshots/ folder. Separate schema (snapshots or dbt_snapshots). Snapshots accumulate history forever and shouldn't be rebuilt.
Common mistakes
- Skipping snapshots, using
tablefor dims — lose history. - Running infrequently for volatile data — miss intermediate states.
- Including high-velocity columns in check_cols — every login generates a row.
- Querying
WHERE dbt_valid_to IS NULLeverywhere — defeats Type 2.
Takeaway
Snapshots = dbt's Type 2 SCD. Pick strategy, point at source, schedule. Join on time-range conditions for history-correct queries.