skip to content

In a dbt snapshot, when do you choose the check strategy over timestamp?

level: middleimportance: must knowfreq 70%

answer

  1. it turns on trusting the source's timestamp
  2. one strategy diffs values, the other reads a clock
  3. what dates the validity window under each
  4. 'all' is available and usually wrong
  5. a partly-maintained updated_at is the worst case

basics

~20 s

Use dbt's check strategy when the source has no trustworthy updated_at column: it compares the values in check_cols instead. Prefer timestamp whenever a reliable modification timestamp exists, because it dates history by when the change happened rather than when dbt ran.

solid answer

~40 s

The `timestamp` strategy points at an `updated_at` column and treats any row whose timestamp moved as a new version; `dbt_valid_from` then carries the real business time of the change. It is cheaper and more accurate, but it is only as good as the source column — if the application updates a row without touching `updated_at`, the change is invisible forever. The `check` strategy takes `check_cols` (an explicit list, or `'all'`) and compares those column values between the source and the snapshot's current row; any difference creates a new version. It needs no timestamp, but it dates versions with the snapshot run time, so history reflects when dbt looked, not when the value changed. Use `timestamp` when a dependable `updated_at` exists, `check` with a tight column list when it does not.

code

sql · 13 lines
sql
-- timestamp: trusts a source column that moves on every write
{{ config(
    unique_key='id',
    strategy='timestamp',
    updated_at='updated_at'
) }}

-- check: compares stored values, no source timestamp needed
{{ config(
    unique_key='id',
    strategy='check',
    check_cols=['status', 'is_cancelled', 'assigned_to']
) }}

go deeper

for a junior

Know that the two strategies are timestamp and check, that timestamp needs an updated_at column and check needs a check_cols list, and that both produce the same Type 2 output shape.

for a middle

Explain the mechanics and the tradeoff: what each strategy compares, what dates dbt_valid_from under each, and why check_cols='all' is rarely the right default on a wide table.

for a senior

Show that you interrogate the source contract before choosing — proving whether updated_at is maintained on every write path — and that you know a missed change is unrecoverable rather than fixable on the next run.

for a principal

Frame this as a data-contract question with the source team: what guarantees the upstream system makes about modification timestamps, and whether the right answer is a change feed rather than periodic value diffing.

## Two ways to answer "did this row change?" Every snapshot run has to decide, for each `unique_key`, whether the source row still matches the open version in the history table. dbt offers two mechanisms, chosen with the `strategy` config. ## The timestamp strategy ```sql {{ config( unique_key='id', strategy='timestamp', updated_at='updated_at' ) }} ``` dbt compares the source row's `updated_at` against the `dbt_updated_at` of the open snapshot row. If it is later, the open row is closed at that timestamp and a new version is inserted with `dbt_valid_from` set to the source `updated_at`. This is the preferred strategy for three reasons: 1. **The history carries business time.** The window boundaries say when the value actually changed in the source system, not when your scheduler happened to fire. 2. **It is cheap.** One column comparison, no wide value diffing. 3. **It is stable under column additions.** Adding a column to the select does not by itself create versions. The catch is total dependence on the source. If the application updates `status` with a trigger that does not maintain `updated_at`, or a bulk backfill script writes rows directly, the change is silently invisible — the snapshot will never record it, and no later run can recover it. A NULL `updated_at` is also a problem: dbt has nothing to place that row on the timeline with. Before choosing `timestamp`, verify with the source owners that the column is maintained on **every** write path, and consider a test that fails if `updated_at` is NULL or in the future. ## The check strategy ```sql {{ config( unique_key='id', strategy='check', check_cols=['status', 'is_cancelled', 'assigned_to'] ) }} ``` dbt compares the listed columns value by value against the open snapshot row. Any difference closes the old version and inserts a new one. Two consequences follow directly: - **Timestamps come from the run, not the data.** `dbt_valid_from` on the new version is the snapshot execution time. If the source changed at 09:12 and the snapshot ran at 02:00 the next morning, the history says the change happened at 02:00. For most reporting that is acceptable; for anything that must reconcile against source-system timestamps, it is not. - **Only listed columns are watched.** A change to a column outside `check_cols` produces no new version, and the current row simply keeps showing the *old* value of that column until something in `check_cols` moves. This surprises people: an untracked column is not merely un-versioned, its updates are invisible until an unrelated tracked change flushes them through. `check_cols='all'` watches every column in the select. It sounds safe and is usually a mistake on wide tables: a volatile technical column — a re-scored field, a last-seen timestamp, a row hash from the loader — will spawn a new version on nearly every run, and the history table explodes while telling you nothing. Comparing many columns also costs more per run. Pick the columns whose history the business will actually query. ## Choosing between them Start from the source contract, not from convenience: - A reliable, always-maintained modification timestamp → `timestamp`. - A timestamp that exists but is only maintained on some write paths → `check` on the columns you care about, because a partially-maintained timestamp is worse than none: it looks trustworthy and silently drops changes. - No timestamp at all → `check`, with an explicit, short column list. A useful sanity check when someone claims the timestamp is reliable: compare a `check`-style column comparison for one day's data against what the `timestamp` strategy would have caught, and see whether the counts agree. ## Changing strategy later Switching strategy on a live snapshot does not restate what is already stored — old versions keep the boundaries they were written with, and you get a discontinuity at the switch point. Document it, or accept that the mixed semantics live in the table forever. Adding a column to `check_cols` behaves similarly: the new column starts being watched from the next run, and its earlier changes were never captured and cannot be recovered.

  • What is the risk of setting check_cols='all' on a wide dbt snapshot?
    Every column becomes a change trigger, including volatile technical ones like loader hashes or last-seen timestamps. Those churn on nearly every run, so the snapshot inserts a new version each time and the table grows without adding business meaning. Comparison cost also scales with width. List only the columns whose history someone will actually query.
  • Under dbt's check strategy, what does dbt_valid_from represent?
    The time the snapshot ran, not the time the value changed in the source. With no trustworthy source timestamp, dbt has nothing else to date the version with. Consumers must understand that validity windows are quantized to the snapshot schedule, and that reconciling them against source-system audit timestamps will show offsets up to one run interval.
  • You picked the timestamp strategy and later discover the source updates rows without touching updated_at. What now?
    Those changes were never captured and cannot be reconstructed from the source, so the gap is permanent. Going forward, switch to the check strategy on the affected columns, or get the source owner to maintain the timestamp on every write path. Document the discontinuity, because windows before and after the switch mean different things.

saying these in an interview costs you the question

  • Claims check strategy records when the change actually happened
  • Defaults to check_cols='all' because it is safer
  • Assumes any updated_at column is trustworthy
  • Thinks columns outside check_cols still get versioned
  • Believes switching strategy restates existing history

context