How to Implement dbt SCD2 Snapshots (Slowly Changing Dimensions)

dbt snapshots implement Slowly Changing Dimension Type 2 by automatically versioning rows when watched columns change - no hand-written surrogate key logic. You define a snapshot block, run it on a schedule, and dbt handles the valid_from/to bookkeeping.

Data Engineerdbtsnapshotsscd2

What SCD2 means and why it matters

A Slowly Changing Dimension Type 2 (SCD2) preserves the full history of a row by keeping every version rather than overwriting in place. When alice@old.com changes to alice@new.com, you end up with two rows:

id email dbt_valid_from dbt_valid_to
1 alice@old.com 2024-01-01 2024-06-01
1 alice@new.com 2024-06-01 NULL

dbt_valid_to = NULL means "current." Without SCD2, historical reports retroactively pick up today's dimension values - every time a customer changes their email, your old revenue reports silently show the new address.

Create the snapshot block

Snapshots live in dbt_project/snapshots/. Create customers_snapshot.sql:

{% snapshot customers_snapshot %}

  {{ config(
      target_schema='dbt_demo',
      unique_key='id',
      strategy='check',
      check_cols=['name', 'email']
  ) }}

  SELECT id, name, email
  FROM {{ source('raw', 'customers') }}

{% endsnapshot %}

Key settings: - target_schema - the schema dbt writes the snapshot table into - unique_key - the natural key used to match existing rows - strategy: check - dbt compares the listed check_cols on every run; a change in any of them closes the old row and inserts a new one - strategy: timestamp - the alternative when you have a reliable updated_at column; cheaper than comparing N columns

Run the snapshot across a simulated change

cd dbt_project

# First run: captures all rows with dbt_valid_to = NULL
dbt snapshot --profiles-dir ..

# Simulate a dimension change
psql -h localhost -U postgres -d app -c \
  "UPDATE customers SET email='alice@new-domain.com' WHERE id=1"

# Second run: dbt detects the change, closes the old row, inserts the new version
dbt snapshot --profiles-dir ..

# Inspect the result
psql -h localhost -U postgres -d app -c \
  "SELECT id, email, dbt_valid_from, dbt_valid_to
   FROM dbt_demo.customers_snapshot
   ORDER BY id, dbt_valid_from"

After the second run you see two rows for id=1 - one with dbt_valid_to set (closed, historical) and one with dbt_valid_to = NULL (current).

The four metadata columns dbt adds

dbt appends these to every snapshot table automatically:

Production patterns

Want to try it hands-on? HeyDevJob gives you this exact setup in a live cloud workspace in your browser - edit it, run it, and see it work. Free, nothing to install.

Try it in a workspace →

What you'll practice

FAQ

What is the difference between dbt snapshot check and timestamp strategy?

The check strategy compares specific columns (check_cols) on every run and closes a row if any watched column changes - good when there is no updated_at column. The timestamp strategy uses an updated_at column to detect changes and is more efficient because dbt only reprocesses rows newer than the last run.

How do I query historical dimension values with dbt snapshots?

Join your fact table to the snapshot with WHERE fact.event_at BETWEEN snapshot.dbt_valid_from AND COALESCE(snapshot.dbt_valid_to, NOW()). This returns the dimension row that was current at the time of each fact event rather than today's value.

When should I use SCD2 instead of just keeping the latest row?

Use SCD2 whenever you need accurate historical reporting - for example, revenue by the customer segment they belonged to at purchase time, not their current segment. If all your reports are real-time dashboards with no historical comparison, a simple overwrite (SCD1) is simpler.

Keep learning

Make an ETL Pipeline IdempotentData projectSpeed Up a Slow SQL Report QueryData projectMake a CSV Importer Resilient to Bad DataData projectData roadmapStep by step to hiredData interview questionsSTAR answersAll Data projectsProjects hub

Learn it by doing. Open this in a live cloud workspace, make the change yourself, and keep a record of the work you can share.

Open the workspace →