In dbt, when does a model use source() instead of ref()?
answer
- ask who builds the table
- one dbt owns, one it borrows
- raw landing tables versus dbt outputs
- declared in YAML versus built by dbt
- seeds are dbt's, so ref them
basics
~20 sUse source() for raw tables dbt reads but never builds, declared in a YAML sources block. Use ref() for anything dbt builds itself: models, seeds and snapshots. Both create DAG edges instead of hardcoded table names.
solid answer
~50 sBoth functions compile to a fully-qualified relation and both create an edge in dbt's DAG — the difference is who owns the table. `source('jaffle_shop', 'orders')` resolves a table that landed in the warehouse from outside dbt (an EL tool, a `COPY` job, a replication stream) and must first be declared under a `sources:` block in a YAML file, where `database`, `schema` and `identifier` tell dbt where it physically lives. `ref('stg_orders')` resolves a node dbt itself builds — a model, a seed or a snapshot — and is what makes dbt order the run correctly. The payoff is that no table name is hardcoded: dbt swaps schemas between dev and prod, lineage shows the raw table as the DAG's root, and renaming a raw table is a one-line YAML change. A seed is referenced with `ref()`, not `source()`, because dbt loads it.
code
yaml · 9 linesversion: 2
sources:
- name: jaffle_shop
database: raw
schema: jaffle_shop
tables:
- name: orders
- name: customersgo deeper
Be ready to state the rule in one line and apply it: source() for raw tables dbt does not build, ref() for models, seeds and snapshots that it does. Know that a source must be declared in YAML first.
Explain what each call compiles to and why the graph depends on it: dbt derives run order from these functions, so a hardcoded table name removes an edge dbt cannot see. Cover how the source name maps to database, schema and table.
Show the convention you enforce — one source() call per raw table inside a staging model — and what it buys during an upstream schema change. Be able to trace blast radius with source-based selection.
Own the policy: how the boundary between ingested and dbt-owned objects is declared, reviewed and audited across many projects, and how that boundary keeps lineage trustworthy when several teams publish into the same warehouse.
## The line the two functions draw dbt builds a directed acyclic graph of everything in your project by parsing the Jinja function calls inside your SQL files. Two of those calls declare upstream dependencies, and choosing between them is really a question about ownership: does dbt create this table, or does dbt merely read it? - `ref('some_model')` — dbt creates it. Models, seeds and snapshots are all dbt-built nodes and all reached with `ref()`. - `source('some_source', 'some_table')` — dbt does not create it. Something else landed the data: Fivetran, Airbyte, a `COPY` command, a CDC stream, a hand-run load script. ## What source() needs before you can call it A source has to be declared before it can be referenced. In any YAML file under your models path you add a `sources:` block: ```yaml version: 2 sources: - name: jaffle_shop database: raw schema: jaffle_shop tables: - name: orders - name: customers ``` Now `{{ source('jaffle_shop', 'orders') }}` compiles to `raw.jaffle_shop.orders`. The first argument is the source name, the second is the table name. Calling `source()` with a pair that is not declared is a compilation error, not a runtime one — dbt catches it before touching the warehouse. ## What ref() needs Nothing beyond the file existing. `ref('stg_orders')` looks up the node named `stg_orders` — a `stg_orders.sql` model file, a `stg_orders.csv` seed, or a snapshot of that name — and compiles to whatever relation dbt built for it in the current target. In development that is typically your personal schema; in production it is the deployment schema. You never write the schema yourself. ## Why the indirection is worth it Three things fall out of using the functions instead of literal table names. **Ordering.** dbt does not run your files alphabetically or in the order you list them. It reads the `ref()` and `source()` calls, builds the graph, and runs each node only after its parents. Hardcode a name and dbt has no idea the dependency exists, so it will happily build a model before the table it reads. **Environment portability.** The same SQL runs against a dev schema, a CI schema and production without edits, because both functions resolve through configuration rather than literals. **Lineage and impact analysis.** Sources appear in the generated documentation and the DAG as the graph's roots. You can answer "which models break if this raw table changes?" with a selector like `dbt build --select source:jaffle_shop+`. Hardcoded names are invisible to all of that. ## Sources are declarations, not loads A common misreading is that declaring a source makes dbt fetch or create the data. It does not. dbt is a transformation tool; it issues `SELECT`-shaped SQL against tables that already exist. A source block is a promise that the table is there, plus metadata dbt can use — descriptions, tests, and freshness thresholds checked by the separate `dbt source freshness` command. ## Seeds are dbt's, so they use ref() Seeds trip people up because they feel like input data, which pattern-matches to "source". But a seed is a CSV in your repo that dbt loads with `dbt seed`; dbt creates that table, so it is a `ref()`. The rule stays clean: if `dbt build` would create the object, use `ref()`. ## The convention that follows Most teams enforce that `source()` is called exactly once per raw table, inside a thin staging model that renames and casts columns. Everything downstream uses `ref()` on that staging model. That gives you a single place to absorb a raw-schema change, and it makes a grep for `source(` a complete inventory of what the project consumes from outside. ## Failure modes to recognise Hardcoding `raw.jaffle_shop.orders` in a model compiles fine and returns rows in production, which is exactly why it survives review. It silently drops the node from the lineage graph, breaks in dev where `raw` may not be readable, and evades every `source:` selector. Calling `source()` on a table another dbt model builds is the mirror error: it splits one node into two, so dbt no longer knows the model must run first.
- What actually breaks if a model hardcodes raw.jaffle_shop.orders instead of calling source()?The dependency vanishes from dbt's graph, so dbt may build that model before its upstream is ready and will not skip it when an upstream test fails. The name is also environment-specific, so the model breaks in dev or CI where that database is not readable. And `--select source:jaffle_shop+` no longer finds it, so impact analysis silently under-reports.
- Can a source table be tested and documented the same way a model is?Yes. Under a source's `tables:` entry you can add a `description`, column descriptions, and generic tests such as `unique` and `not_null`. Those become test nodes you can run with `dbt test --select source:jaffle_shop`, and they show up in the generated docs. Testing at the source edge catches upstream breakage before it propagates into models.
- Why do most projects call source() only inside staging models?It gives you exactly one file per raw table to change when the upstream schema moves, keeps casting and renaming in one predictable layer, and makes the set of `source()` calls a complete, greppable inventory of external dependencies. Downstream models then only ever see cleaned, dbt-owned relations.
A source is a citation to a book you did not write; a ref is a pointer to another chapter of your own manuscript. Both belong in the bibliography, but only one is yours to edit.
saying these in an interview costs you the question
- Says source() is for anything upstream, including other dbt models
- Thinks seeds are referenced with source() because they are input data
- Believes declaring a source makes dbt load or create the table
- Hardcodes raw schema names and calls the YAML declaration optional paperwork
- Cannot say where a source must be declared before it is used