skip to content

Sources and Seeds

Declaring the raw tables your project reads, with freshness checks, and loading small reference CSVs as seeds. Interviewers ask about source() versus ref() because it draws the line between what dbt owns and what it merely consumes.

on this pageshow

questions

7

In dbt, when does a model use source() instead of ref()?

level: juniorimportance: must knowfreq 78%

answer

  1. ask who builds the table
  2. one dbt owns, one it borrows
  3. raw landing tables versus dbt outputs
  4. declared in YAML versus built by dbt
  5. seeds are dbt's, so ref them

basics

~20 s

Use 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 s

Both 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 lines
yaml
version: 2

sources:
  - name: jaffle_shop
    database: raw
    schema: jaffle_shop
    tables:
      - name: orders
      - name: customers

go deeper

for a junior

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.

for a middle

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.

for a senior

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.

for a principal

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

context

open as a page

In dbt, what is a seed and what data belongs in one?

level: juniorimportance: must knowfreq 62%

basics

~20 s

A seed is a CSV file in the project's seeds directory that dbt loads into the warehouse as a table when you run dbt seed. It suits small, static, version-controlled reference data, not raw source data.

open as a page

In dbt, how does the source freshness check decide a raw table is stale?

level: middleimportance: should knowfreq 55%

basics

~20 s

It takes the maximum value of the source table's loaded_at_field, measures the lag to the current time, and compares that lag with the warn_after and error_after thresholds in the source YAML, returning pass, warn or error per table.

open as a page

In a dbt source definition, what do database, schema and identifier control?

level: middleimportance: should knowfreq 48%

basics

~20 s

They form the physical relation dbt compiles a source() call into. The source name defaults to the schema and the table name defaults to the identifier, so you set schema or identifier only when the warehouse names differ from the names you want in code.

open as a page

How do you stop dbt models from building on a stale source table?

level: seniorimportance: should knowfreq 40%

basics

~20 s

A dbt run never checks freshness, so you must gate it: run dbt source freshness as a separate orchestrator step that fails the pipeline on error, or put a recency test on the source so dbt build skips every downstream model when it fails.

open as a page

When should reference data live as a dbt seed rather than a pipeline-loaded table?

level: principalimportance: should knowfreq 36%

basics

~20 s

Seed it when the analytics team is the system of record and the data is small, slow-moving and non-sensitive, so a pull request is the right change process. Ingest it when an upstream system already owns the data or it changes often.

open as a page

A dbt seed of ZIP codes loads as integers and loses leading zeros — how do you fix it?

level: middleimportance: nice to knowfreq 34%

basics

~20 s

dbt infers seed column types from the CSV contents, so an all-digit column becomes numeric. Pin it with a column_types config naming the column as a string type, then run dbt seed with full refresh so the table is dropped and recreated.

open as a page