What a snapshot does
Each time you run dbt snapshot, dbt compares the current source rows with the snapshot table:
- New rows are inserted with
dbt_valid_fromset anddbt_valid_toNULL. - Changed rows get the old version closed (
dbt_valid_toset) and a new version inserted. - Unchanged rows are left alone.
The result is full history of a mutable source table, such as customers, products or account status.
Warning
Snapshot history can't be rebuilt from the source. If the snapshot table is dropped or corrupted, the history is gone. Back it up, and never run it with
--full-refreshthinking it behaves like a model.
1. Configure a snapshot (dbt 1.9+ YAML)
# snapshots/customers_snapshot.yml
snapshots:
- name: customers_snapshot
relation: source('crm', 'customers')
config:
schema: snapshots
unique_key: customer_id
strategy: timestamp
updated_at: updated_at
hard_deletes: invalidate
Older versions use a SQL file with a {% snapshot %} block:
{% snapshot customers_snapshot %}
{{ config(target_schema='snapshots', unique_key='customer_id',
strategy='timestamp', updated_at='updated_at', invalidate_hard_deletes=True) }}
select * from {{ source('crm', 'customers') }}
{% endsnapshot %}
Run it:
dbt snapshot --select customers_snapshot
2. Choose the strategy
| Strategy | Use when | Watch out |
|---|---|---|
timestamp | Source has a reliable updated_at that changes on every update | If some updates don't touch updated_at, those changes are missed |
check | No reliable timestamp | Set check_cols to the columns you care about; all compares every column and costs more |
config:
strategy: check
check_cols: [email, segment, status]
3. Unique key must be truly unique
If two source rows share the unique_key, the snapshot produces duplicate current rows or fails during the merge. Test the source first:
sources:
- name: crm
tables:
- name: customers
columns:
- name: customer_id
tests: [unique, not_null]
For composite keys, build a surrogate in the snapshot select, or pass a list of columns to unique_key where your dbt version supports it.
4. Hard deletes
By default, rows deleted from the source stay "current" in the snapshot for ever. Options (dbt 1.9+):
hard_deletes | Behaviour |
|---|---|
ignore (default) | Deleted rows stay open |
invalidate | Closes the current version (dbt_valid_to set) |
new_record | Inserts a new row flagged as deleted (dbt_is_deleted) |
5. Using the history
Current state:
select * from {{ ref('customers_snapshot') }} where dbt_valid_to is null
As at a date:
select * from {{ ref('customers_snapshot') }}
where dbt_valid_from <= '2026-06-30'
and (dbt_valid_to > '2026-06-30' or dbt_valid_to is null)
Build a dimension model on top of the snapshot that renames the columns to valid_from / valid_to / is_current, rather than having reports query the snapshot directly.
dbt 1.9 also lets you set dbt_valid_to_current (for example '9999-12-31') so current rows get a far-future date instead of NULL.
6. Schedule
- Run snapshots before models in each job:
dbt snapshot && dbt build. - Run them often enough to catch changes. A change that happens and reverts between two runs is never recorded.
- If the source is CDC or append-only with full history already, you may not need a snapshot. Build SCD2 from the change log instead.
Common problems
| Problem | Cause | Fix |
|---|---|---|
| Duplicate current rows | Non-unique key in source | Fix the key, deduplicate in the snapshot select, then repair the table |
| Every run creates new versions for all rows | check strategy with columns that change every time (load timestamps), or a non-deterministic select | Exclude volatile columns from check_cols |
| Changes not captured | updated_at not maintained by the source | Use check strategy |
| Snapshot table wiped | Someone dropped it or ran a destructive operation | Restore from warehouse time travel or backup |
| Type changes in source break the run | Column type changed | Alter the snapshot table column, then rerun |
Fix a duplicate current row directly in the warehouse (example for one key), after taking a backup:
create table snapshots.customers_snapshot_bak as select * from snapshots.customers_snapshot;
update snapshots.customers_snapshot
set dbt_valid_to = current_timestamp
where customer_id = 1042 and dbt_valid_to is null
and dbt_scd_id <> (select max(dbt_scd_id) from snapshots.customers_snapshot
where customer_id = 1042 and dbt_valid_to is null);
Checklist
- Strategy matches how the source records changes
- Unique and not-null tests on the source key
- Hard-delete behaviour chosen
- Snapshots run before models, frequently enough
- Snapshot tables backed up or covered by time travel
- Dimension model built on top for reporting