Skip to main content
Most “the data is wrong” findings are not data problems. They are comparison-window problems: two systems read at slightly different moments, or a target that has not finished catching up. This page is how you rule that out. Reach for these in order:

Template variables

A variable is resolved once per run and interpolated into both sides, so the two queries are guaranteed to use the same boundary.
Variables also interpolate into a table dataset’s where:, so you do not have to switch to raw SQL to use them.

What you can write inside {{ }}

Offsets subtract from now, today or yesterday, in units of d (days), h (hours), m (minutes) or w (weeks). Only subtraction is supported — a comparison window that reaches into the future is not a thing you want. Override any variable for a single run:
The resolved values are recorded in the audit log, which is what makes a re-check compare the same window as the original run.
NOW(), CURRENT_DATE and GETDATE() in a query are a bug, not a shortcut. Each side evaluates them at its own moment, on its own clock. Any row written between the two evaluations appears on one side and not the other, and you get a mismatch that has nothing to do with your data — reliably, on every run, for a different row each time.check-config warns when it finds one. Set strict_time_references: true at suite level to make it an error, which is worth doing in CI.

Windows

A window: restricts a reconciliation to a recent period without you writing the predicate:
  • lookback is required: "14d", "2h", "1w".
  • strategy: sliding (the default) measures back from the run’s reference time, so each run compares a moving fortnight. fixed anchors the window to a period boundary, so consecutive runs inside the same period compare exactly the same rows.
Use a window when history is immutable and re-comparing it every hour is a waste of warehouse spend. Use fixed when you want two runs to be comparable to each other, not just each correct in isolation.

Cutoffs for a lagging target

This is the important one, and the one people reach for last when they should reach for it first. If the target trails the source — a CDC stream, a batch load, a read replica — then rows written recently are on the source and not yet on the target. They are not missing. They are in flight. A reconciliation with no cutoff reports them as a difference, every time, and the difference moves as the lag moves. A cutoff: derives a watermark from the data itself, then filters both sides to at-or-below it. Rows in flight are excluded from the comparison rather than reported as errors.
The difference it makes, on the same pair of tables — a primary with 500 orders and a replica that has caught up to 470 of them:
Same data, same query, two different conclusions. The second one is the true one: the replica is not missing anything, it is simply thirty rows behind, and the comparison now says so by agreeing about the 415 rows both sides have definitely seen.
Notice the cutoff is stricter than the lag — 415 rows, not 470. truncate: HOUR snaps the watermark down to an hour boundary and offset: "-5m" pulls it back further. Both are deliberate margin, because a pipeline that writes slightly out of order can land a row below a watermark it has already passed.

Three levels of control

One column, both sides:
Each side’s watermark is MAX(created_at) over its own dataset; the lower of the two becomes the cutoff.

How a cutoff resolves

1

Derive a watermark per configured side

MAX (or MIN) of the named column, or the result of your custom query.
2

Combine the two into one value

combine: min by default.
3

Truncate to a boundary

Optional. Skipped when the watermark is not a timestamp.
4

Apply the offset

Optional, after truncation. Negative buys margin.
5

Build the WHERE clause for each side

Against the derived column, or whatever apply: names.
Every one of those intermediate values — both watermarks, the combined cutoff, and the final WHERE clause per side — is recorded in the audit log. A run’s comparison window is always recoverable after the fact.
combine: min is the default and almost always the right answer. The lower of the two watermarks is the point both sides have certainly reached.max, source and target each keep rows that one side has and the other may not — which is a mismatch you created. They exist for cases where you know the asymmetry is real; if you are not sure, you want min.
The watermark does not have to be a timestamp. A monotonically increasing sequence or id works exactly the same way, in which case truncate and offset are ignored rather than applied.

Time travel

as_of compares against a point-in-time snapshot rather than the current state, on platforms that support it — Snowflake, BigQuery and Databricks:
This is the tool for “was it right yesterday, before the deploy?” and for pinning both sides to the same instant when one of them is being written to continuously.
A re-check replays the original run’s queries, which for an as_of reconciliation means it re-compares the same frozen snapshot and can therefore never turn green. Pass --reresolve to re-derive the queries against current data.

Choosing between them

Use a cutoff when

The two systems are the same data at different times. You want to compare what both sides have, and ignore what is still on its way.

Use a window when

History is settled and you only care about recent data — usually for cost, not correctness.

Use variables when

You need an explicit, reproducible boundary in your own SQL, identical on both sides.

Use `as_of` when

You need a specific historical state, not “now”.
They compose. A cutoff on top of a window is a normal thing to write: compare the last fortnight, up to the point the target has caught up to.

Next

Running locally

Overriding variables per run, and reading what a run resolved them to.

Investigating results

Confirming a difference is real, and re-checking after a fix.