Checklist
dbt Model Review Checklist
Correctness, then cost, then comprehensibility - plus the full-refresh diff against your own incremental table that almost nobody on a project has ever run.
A transformation review that opens with naming conventions is a review that will miss the defect. The defect is nearly always a join that quietly changed what one row means, and no style comment has ever caught one.
So the order is fixed: correctness, then cost, then comprehensibility. Finish each pass before starting the next, because a reviewer who has started renaming CTEs has stopped looking for fan-out.
Why this exists
The most common real defect in a transformation is fan-out. You join to a table you believe holds one row per key, it does not, and every sum downstream is inflated. Nothing errors. The number simply gets larger and stays plausible — which is the whole problem, because an inflated revenue figure looks like a good quarter.
The defense is structural, not attentive. Every model states its grain in one sentence: what exactly one row is. A model with no stated grain cannot be reviewed, because you cannot evaluate a join without knowing the intended output grain — you are just reading SQL and nodding.
The second class of defect lives in incremental models, where cost and correctness collide. A record that arrives late, or changes after its window has passed, needs a path back in. If it has none, you have a correctness bug presenting itself as a cost optimization, found later by somebody reconciling a month-end total.
Which brings up the test almost nobody runs: build the model with a full refresh into a scratch schema, then diff it row for row against the production incremental table. If no one on the project has ever done this, assume the two differ. It is the cheapest high-yield check on the list, and it gets skipped every time — precisely because the incremental table has never visibly misbehaved.
How to use it
Not on every model. Run the full checklist on anything something else depends on: a mart a dashboard reads, a model other models select from, an export that leaves the building. Staging gets grain, types, one uniqueness test.
Review the compiled SQL, not the file. A macro, a variable and one conditional, and the query the warehouse received is not the query you are reading.
Check that tests pass last, not first. Seeing green before you read the SQL turns you into a rubber stamp. unique and not_null describe shape; they cannot describe meaning, and the fan-out you are hunting can pass both.
Before approving anything that changes a grain, a filter or a column type, look at what depends on this model. The blast radius is not the file in front of you, and a downstream owner finding out from their own broken dashboard is how review loses consent.
Time-box it: twenty minutes on correctness, ten on cost, ten on comprehensibility. If correctness overruns, stop and send it back. Polishing a model you already know is wrong is how reviews earn a reputation for being slow and catching nothing.
Where it goes wrong
This checklist cannot tell you the model computes the right thing. It confirms the join is one-to-one and has no opinion about whether the filter excluded the wrong customers. Semantic correctness needs a reviewer who knows the domain, or a comparison against a number somebody already trusts — and that comparison belongs in the model’s documentation, so the next reviewer inherits it.
The full-refresh diff is not free. On a large model it costs real compute, and when the source has a retention window it is impossible — the raw history is gone. When it is impossible, that is the finding, not an excuse: the incremental table has quietly become the only copy of that data and it cannot be rebuilt. Put that in the review, not in your notes.
Do not run this on a model that exists so somebody could look at something once. Formal review on a throwaway teaches people to route around review — by putting the real logic in the dashboard, where nobody reviews anything at all.
And a checklist decays into ritual. Once a year, delete every item that has not caught a defect. A shorter list people read beats a complete one they scroll past.
Copy and Clear become available when the worksheet is ready.
DBT MODEL REVIEW CHECKLIST
Three passes, in order. Do not start pass 2 until pass 1 is clean.
Full version for any model something else depends on. Staging models
get the short version: grain, types, one uniqueness test.
MODEL: REVIEWER: DATE:
MATERIALIZATION: view / table / incremental / ephemeral
WHAT CHANGED:
PASS 0 -- ENTRY CONDITIONS. If either fails, send it back unread.
"one row per per "
Not stated -> it cannot be reviewed. Return it.
dbt compile --select then open target/compiled/...
PASS 1 -- CORRECTNESS (~20 min). Overruns? Stop and send it back.
GRAIN AND FAN-OUT
compound, the test is on the compound key. A unique test on one
column of a two-column grain tests a different claim entirely.
expected relationship: 1:1 / 1:many / many:1
what guarantees the right side is unique on the join key?
name the test or constraint, not the belief
select count(*), count(distinct ) from
Equal? Y / N If N you have found it. Stop here.
JOIN AND FILTER SEMANTICS
That silently converts it to an inner join. It belongs in ON.
without warning. select count(*) ... where is null
people expect to keep. Use "is distinct from" deliberately.
day boundary match what the consuming report filters on?
and is it deterministic across runs?
A non-deterministic tie-break makes the model irreproducible.
INCREMENTAL MODELS ONLY
- A source row that ARRIVES late -- lookback: days.
Chosen on what basis?
Build --full-refresh into a scratch schema, then diff row for
row against the production incremental table.
Run on ? Y / N Rows differing:
Never run on this project before -> assume they differ.
Impossible because source history is gone -> that IS the
finding: this table is now the only copy and cannot be
rebuilt. Write it in the review, not in your notes.
PASS 2 -- COST (~10 min). A cheap wrong number is still wrong.
or index column, or on something derived from it? Derived means
a full scan, every run, quietly.
every morning wants to be a table. A table nobody reads daily
wants to be a view.
must not silently appear in an export.
inherited from a model somebody copied?
- Rebuild time before: after:
PASS 3 -- COMPREHENSIBILITY (~10 min)
FROM -- a person, a document, a decision -- not what it does.
"exclude type 7" is the code. "finance excludes internal
transfers, agreed with " is the comment.
DOWNSTREAM, THEN VERDICT
- Children listed: dbt ls --select +
Who reads them:
Grain, filter or type changed? They have been told on .
VERDICT
- Approve with follow-up: by
refresh diff has never been run on an incremental model.