A dbt model selects from analytics.stg_orders by full table name instead of ref() — what breaks?
answer
- the run succeeds; the numbers do not
- dbt cannot order what it cannot see
- dev quietly reads prod
- the safety net has a hole exactly here
basics
~20 sThe dependency becomes invisible: dbt may build the model before or alongside its upstream, so results are intermittently stale. Development and CI runs read production data, lineage and docs are wrong, and selection or slim CI silently skips the model.
solid answer
~50 sFour failures, and the nastiest is intermittent. First, **ordering**: with no DAG edge, dbt is free to schedule the model at any point, so with several threads it can read yesterday's data or a relation mid-rebuild — the run succeeds and the numbers are wrong. Second, **environments**: the literal points at one schema, so your dev run and CI both read production while writing to a scratch schema, meaning CI validates nothing you changed. Third, **lineage**: the docs graph and any downstream impact analysis omit the edge, so someone refactoring `stg_orders` has no idea this model exists. Fourth, **selection**: `--select stg_orders+` and `state:modified+` never include it, so the model is skipped by exactly the CI job meant to catch the breakage. Fix it by replacing the literal with `ref()`, then add a CI check that greps model SQL for schema-qualified names.
code
sql · 4 lines-- models/marts/fct_orders.sql
-- no edge: dbt may run this before stg_orders is rebuilt
select order_id, customer_id, order_total
from analytics.stg_ordersgo deeper
Recognise the anti-pattern on sight and replace the literal with ref(), or with source() if the table is raw. Know that the run will not error — the data just goes wrong.
Explain why the missing edge removes the ordering guarantee and why the same file then reads production from every environment. Be able to show the fix and verify it in the compiled SQL.
Lead with the intermittent staleness under concurrent execution, then connect it to lineage that lies and CI selection that skips the model. Describe how you would audit an existing project and add a check that blocks new occurrences.
Treat reference completeness as the invariant every downstream guarantee rests on — impact analysis, slim CI, deprecation policy — and enforce it automatically rather than by review, because a single literal quietly degrades all of them.
## Why this is a senior question Everyone can recite "use `ref()`." The interview question is what actually goes wrong, in what order, and how you would find it in a project that already has the problem. The answer is interesting because the failures are staggered: one is intermittent, one is silent, one only bites during a refactor, and one disables the very safety net that would have caught the others. ## Failure 1 — non-deterministic build order dbt derives execution order from `ref()` and `source()` edges and runs independent branches concurrently, up to the configured thread count. A model whose only reference to its input is a literal string has **no incoming edge**, so from dbt's point of view it is a root node with no prerequisites. It may be scheduled first. The consequence is not a crash. The relation exists — it holds yesterday's data, or, on a warehouse where a rebuild is not atomic, a partially populated state. The run reports success and the mart is quietly stale. Change the thread count, add an unrelated model, and the ordering shifts; the symptom appears and disappears. Teams often chase this for weeks because a rerun "fixes" it. ## Failure 2 — every environment reads production `ref()` resolves per target; a literal does not. So a developer running against a personal schema builds *their* model from *production* `stg_orders`. That is bad in three directions: - Local changes to `stg_orders` are never exercised downstream, so a breaking change looks fine. - CI builds into a throwaway schema but reads production inputs, meaning the pull request is not actually tested against the version of the code it changes. - If anyone ever grants write-shaped access, a dev run can touch a production relation. The schema also becomes a deployment-time landmine: renaming or moving a schema requires finding every literal by hand, and a missed one fails at run time rather than at parse time. ## Failure 3 — lineage and documentation lie The generated documentation renders the graph from the manifest, which is assembled from references. A missing edge means the docs show a model with no parents and show `stg_orders` with one fewer child. Consumers doing impact analysis before a change — "who reads this column?" — get a confidently wrong answer. This is the failure that turns a routine refactor into an outage, because the person deleting a column checked the lineage graph and it told them nobody was downstream. ## Failure 4 — the safety nets are disabled Selection operators traverse the same graph: - `dbt build --select stg_orders+` after changing staging will **not** include this model, so its tests do not run. - A slim CI job using `state:modified+` will not rebuild it when its upstream changes. - A blast-radius query before a deprecation will not list it. So the exact automation you built to catch the first three failures skips the one model that has them. ## Finding the problem in an existing project - **Grep the model files** for schema-qualified patterns and for known schema names; hard-coded references are textually obvious once you look. - **Read the docs DAG** for models with no parents that clearly should have some — a mart with zero ancestors is a strong signal. - **Compare the compiled SQL** in `target/compiled/` against the graph: a relation appearing in the SQL that is not among the node's dependencies is the smoking gun. - **Add a CI lint step** that fails the build when a model file contains a schema-qualified identifier, with an allowlist for the rare legitimate case. Project-linting packages exist that codify checks of this kind; a ten-line grep in CI covers most of it. ## Fixing it safely Replacing the literal with `ref()` (or `source()` if the table is raw and outside the project) is a one-line change, but it changes the graph, so re-run with `--select +the_model` to confirm the ancestry now materialises, and check the compiled SQL resolves to the expected schema in dev. Where the referenced table genuinely is external — a table another team owns, not built by this project — the correct fix is a **source** declaration, not a literal: you get the edge, the lineage anchor and freshness checking, and the schema stays configurable in YAML. ## What a weak answer sounds like "It's not best practice" or "it won't be portable." Neither names a failure. The strong answer leads with the intermittent staleness — because that is the one that costs a business real money before anyone notices — and then explains why the tooling that should have caught it was blind by construction.
- How would you detect hard-coded references across a large existing dbt project?Grep model files for schema-qualified identifiers and known schema names, then cross-check the docs graph for models with implausibly few parents. A stronger check compares each node's compiled SQL against its recorded dependencies: any relation in the SQL that is not a dependency is a hard-coded reference. Wire that as a failing CI step so new ones cannot land.
- The referenced table is genuinely built by another team, not by this project. What is the right fix?Declare it as a source in YAML and reference it with `source()`. That gives a real graph edge, anchors lineage at the external table, makes the database and schema configurable per environment, and enables freshness checks so you learn when the upstream team's load is late. A literal name achieves none of that.
- Why does raising the thread count sometimes make this bug appear for the first time?Because with one thread dbt still executes nodes in a topological order that often happens to put staging models first, masking the missing edge. More threads means more concurrency and a different interleaving, so the unconstrained node can start before its de facto input finishes. The bug was always there; concurrency just exposed it.
saying these in an interview costs you the question
- Says it only matters for code style or portability
- Expects a loud error rather than silently stale data
- Thinks dbt infers the dependency from the table name in the SQL
- Believes running single-threaded makes it safe
- Proposes ordering models manually instead of adding the edge