Skip to content
DWKD

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.