Field guide · 5~8 min
Build & test locally
dbt failures come in three kinds, and they announce themselves differently. Knowing which kind you are looking at is most of the work of fixing it.
Build the smallest thing that answers your question
Every build is a loop: change something, build, read the result. The tighter that loop, the faster you work — so the habit worth forming is to select only what the current question needs, and widen the selection only when the question widens.
| Command | The question it answers |
|---|---|
dbt build -s my_model | Does my model build, and does its data pass its tests? |
dbt build -s +my_model | Same, after first rebuilding everything upstream of it |
dbt build -s my_model+ | Did my change break anything that reads this model? |
.\build_changed.ps1 | Do all the models my branch touches still build and pass? |
The three kinds of failure
Compilation errors happen before Snowflake is involved at all. dbt could not turn your files into SQL — a misspelled ref() naming a model that does not exist, broken Jinja, a YAML file whose indentation went wrong. The message names the file and usually the line, and nothing was executed anywhere. These failures are usually quick to fix. They are the cheapest failures you will ever have, which is why the editor's live checking (which runs the same compilation as you type) is worth trusting.
Database errors mean the SQL compiled, reached Snowflake, and Snowflake rejected it: a column that does not exist on the upstream table, a type that will not cast, a syntax slip. The message in dbt's output is Snowflake's own, quoted back to you. When it does not seem to match what you wrote, remember that Snowflake never saw what you wrote — it saw the compiled SQL, after every ref() and macro expanded. Open the rendered version in target/compiled/ and the mismatch is usually obvious there.
Test failures are different in kind, and the difference matters: your SQL ran successfully, and then the data broke a rule you had asserted about it. Nothing is necessarily wrong with your code. A test failure means one of three things — your code produced wrong rows, your assumption about the data was wrong, or the source has a genuine quality problem — and deciding which is investigation, not debugging.
Reading the output
A failing build prints a lot, and most of it is consequence rather than cause. dbt builds in dependency order, so when one node fails, everything downstream of it is skipped — and each skip prints its own line. Resist reading the wall of red from the bottom:
2 of 6 ERROR creating sql view model stg_reference_opening_hours [ERROR in 1.2s]
3 of 6 SKIP relation int_site_capacity ......................... [SKIP]
4 of 6 SKIP test not_null_int_site_capacity_site_code ........... [SKIP]
...
Database Error in model stg_reference_opening_hours
invalid identifier 'OPENS_AT'One thing failed here, not three. The skips exist because of the error above them; fix the first failing node and they resolve themselves. Find the first ERROR or FAIL line, read the detailed message dbt prints for it at the end of the run, and ignore everything downstream until that is fixed.
When a test fails, look at the rows
A FAIL line prints a count — Got 14 results, configured to fail if != 0 — but the count is not the information. The rows are. Two ways to see them:
dbt show -s my_model --limit 20 # eyeball the model's outputOr run the test itself: every test compiles to a query that selects exactly the violating rows, and the compiled query is sitting in target/compiled/. Paste it into Snowflake and you are looking at the 14 offenders directly — which usually settles the question of which kind of failure this is:
- The grain test failed: look at a pair of duplicated rows side by side. If they differ in some column, your grain sentence missed a dimension of the data — the table is “one row per site per day per something else”. If they are identical, either a join in your model fanned out, or the source itself has duplicates.
not_nullfailed: look at the null rows before deciding anything. Sometimes null is legitimate and the test was your misunderstanding — drop the test and write what null means in the column description instead. Sometimes the rows are junk from the feed, which is worth a message to the team, not a silent filter.