In dbt, why should a model use ref('stg_orders') instead of a hard-coded table name?
answer
- a string tells dbt nothing
- the call is an edge, not just a name
- order, environment, lineage
- who builds first, and in whose schema
basics
~20 sBecause ref() does three things a literal name cannot: it declares a dependency so dbt builds the upstream model first, it resolves to the right database and schema for the environment you are running in, and it feeds lineage, docs and selection.
solid answer
~40 s`ref()` is how dbt learns that one model depends on another. At parse time each `ref('stg_orders')` becomes an edge in the project's DAG, so dbt topologically sorts the graph and builds `stg_orders` before anything that reads it — you never order models by hand. At compile time the same call resolves to the fully qualified relation for the **current target**, so the identical file reads `dev_alice.stg_orders` when you run in development and the production relation on the scheduled run. And because the edges are known, the lineage graph, the generated docs and selection syntax such as `dbt build --select stg_orders+` are all correct for free. A hard-coded `analytics.stg_orders` gives you none of that: no ordering guarantee, no environment switching, and an invisible dependency.
code
sql · 4 lines-- Hard-coded: dbt sees no dependency at all
select *
from analytics.stg_orders
where ordered_at >= '2024-01-01'go deeper
Memorise the three payoffs — build order, correct schema per environment, and lineage — and always write ref() rather than a table name. Know that raw tables get source() instead.
Explain the mechanism: each ref() is parsed into a DAG edge and resolved at compile time against the active target's database and schema. Be able to say what selection operators do with those edges.
Talk about the failure mode you have seen: an invisible dependency that makes downstream data intermittently stale under multiple threads, and CI slim builds that skip a model because no edge exists.
Frame it as the contract that makes the project graph trustworthy. Every enforcement mechanism — impact analysis, CI selection, lineage for consumers — is only as complete as the ref() discipline, so it belongs in review standards and automated checks.
## The function, in one line `ref()` is a Jinja function that takes the *name of a dbt model* and returns the relation that model builds. Writing `from {{ ref('stg_orders') }}` in a model is the only supported way for one model to read another. It replaces what you would otherwise type — a literal `database.schema.table` — and in exchange dbt gets to know something a string literal can never tell it: that these two models are connected. ## Benefit 1: build order comes from the graph When dbt parses a project it reads every model file, extracts every `ref()` call, and assembles a **directed acyclic graph** of nodes and edges. Each `ref()` is one edge pointing from the referenced model to the referencing one. dbt then topologically sorts that graph and executes models in dependency order, running independent branches concurrently up to the `threads` setting in your profile. This is why a dbt project has no scheduler file, no ordering list and no numbered filenames. You do not tell dbt that staging runs before marts; the `ref()` calls in the mart models already said so. Add a new model that refs three others and it slots into the correct position automatically. Remove a reference and the edge disappears. The corollary is severe: a dependency expressed as a literal table name is invisible to that sort. dbt has no reason to build the upstream model first, so with multiple threads a downstream model can read the previous run's data — or a half-built relation — and the failure is intermittent, which is the worst kind. ## Benefit 2: the same code runs in every environment `ref()` does not return a fixed string. It resolves through the project's manifest to the referenced model's `database`, `schema` and identifier **as configured for the target you are currently running against**. That target comes from `profiles.yml`: your development target usually points at a personal schema, CI at a throwaway one, production at the shared one. So one file, unchanged and committed once, reads `dbt_alice.stg_orders` on your laptop and the production relation on the nightly run. Hard-code `analytics.stg_orders` and every environment reads production. Your development run silently tests against production data, your CI build validates nothing you changed, and the moment someone renames a schema, every literal has to be found and edited by hand. ## Benefit 3: lineage, docs and selection Because the edges are recorded, dbt can answer graph questions: - **Documentation and lineage.** `dbt docs generate` renders the DAG; a consumer can see which raw source a mart column ultimately came from. Only `ref()` and `source()` edges appear. - **Selection.** `dbt build --select stg_orders+` builds `stg_orders` and everything downstream of it — that operator is defined purely in terms of graph edges. Likewise `--select +fct_orders` builds a model and its ancestors, and a CI job that runs `state:modified+` rebuilds changed models plus their descendants. A missing edge means a model that *should* have been rebuilt is skipped. - **Impact analysis.** Before you change or drop a model, the graph tells you who reads it. ## Reading data dbt did not build `ref()` only addresses dbt models. Tables that arrive from outside the project — raw loaded data — are declared in YAML as **sources** and addressed with `source('jaffle_shop', 'orders')`. That call creates the same kind of graph edge, anchoring lineage at the raw table and enabling freshness checks. The rule of thumb is simple: never type a raw table name into a model either; declare it as a source once and reference it. ## The two-argument form `ref()` also accepts two arguments — `ref('package_name', 'model_name')` — which disambiguates when an installed package contains a model with the same name as one of yours. Later dbt versions use the same two-argument shape to reference a model published by a different dbt project. ## What a good answer sounds like Interviewers ask this constantly because it separates "I have read the tutorial" from "I have run this in anger." The weak answer is "it is best practice" or "it makes the SQL portable." The strong answer names the three concrete mechanisms — **build order, environment resolution, and graph metadata** — and then names the failure mode of not using it: a dependency dbt cannot see, which produces stale downstream data non-deterministically and lies in the lineage graph.
- How would you reference a raw table that dbt does not build?Declare it as a source in a YAML file — a source name, a table name, and the database and schema it lives in — then reference it with `source('jaffle_shop', 'orders')`. That creates a real graph edge, so lineage reaches back to the raw data and freshness checks become possible, without any literal table name in the model SQL.
- Does ref() paste the upstream model's SQL into your query?Normally no: it resolves to the upstream relation's fully qualified name, and your query reads the built table or view. The exception is an ephemeral model, which dbt never builds — a `ref()` to it inlines its compiled SQL as a common table expression in the referencing model instead.
- If ref() resolves at compile time, what happens when you rename a model file?Every `ref()` to the old name fails at parse time with a "depends on a node named ... which was not found" error, before any SQL runs. That is the desirable outcome — a rename breaks loudly and immediately, whereas a hard-coded name would keep pointing at a stale relation until someone noticed wrong numbers.
saying these in an interview costs you the question
- Says ref() is just a style preference or shorthand
- Claims ref() copies the upstream model's SQL inline
- Thinks you still have to order models manually somewhere
- Believes a hard-coded name works fine as long as the schema exists
- Cannot name what ref() gives beyond "portability"