dbt ref() vs source(): Model-to-Table References Explained
Use ref() for dbt-built models and source() for external raw tables you declare in YAML. This guide shows exact SQL/YAML, selectors, and pitfalls so your builds run in the right order.
Short answer: use ref() to point a model at another model in your dbt project; use source() to point a model at a raw table you’ve registered as a source. That’s it. Get those two decisions right and dbt will automatically build the dependency graph, generate lineage and documentation, and run things in the correct order. Below are concise rules, examples, and selectors you’ll actually use on a real warehouse, plus how to handle staging, schemas, and testing for reliable data.
ref() vs source() in dbt: the quick rules
Use this mental model:
- source(): External, raw inputs. Think ingestion or source data you don’t control in dbt.
- ref(): Internal, modeled outputs. Think dbt-built tables/views inside your schemas.
If a relation is created by dbt, reference it with ref(). If it’s created outside dbt (ingestion jobs, vendor tools, Kafka sinks), reference it with source().
What is a source in dbt?
A source is a declared external relation (often in a raw schema) that dbt doesn’t build but can test and check for freshness. You define sources in YAML, including the database and schema location and optional freshness settings. This is the backbone of sources in dbt.
version: 2
sources:
- name: app
database: analytics_prod
schema: raw
description: Raw ingestion landing area
freshness:
warn_after: {count: 24, period: hour}
error_after: {count: 48, period: hour}
loaded_at_field: _ingested_at
tables:
- name: orders
description: Orders from transactional system
- name: customers
Then, in a model SQL, use source like this:
-- models/stg_orders.sql
select
id as order_id,
customer_id,
order_created_at,
total_amount
from {{ source('app', 'orders') }}
where order_created_at >= dateadd(day, -30, current_date)
When you run dbt source freshness, dbt checks the configured loaded_at_field against those thresholds. You can add tests directly to these source tables to protect your downstream data.
What does ref do in dbt?
ref() is a Jinja function in dbt that creates references between models so dbt can compute a dag, order execution, and resolve the correct database relation names at runtime. It’s the primary way to link transformations and ensure dependency-aware builds.
-- models/int_order_revenue.sql
select
o.order_id,
o.customer_id,
o.order_created_at,
o.total_amount,
c.customer_lifetime_value
from {{ ref('stg_orders') }} as o
left join {{ ref('stg_customers') }} as c
on o.customer_id = c.customer_id
By using ref() for dbt models, you unlock materialization-agnostic SQL, easier refactoring, and correct environment-specific relation names without hardcoding schema paths. This keeps your code portable across environments and targets.
Comparison: source() vs ref()
| Aspect | source() | ref() |
|---|---|---|
| What you reference | External raw table/view (ingested outside dbt) | dbt-built model relation |
| Where it’s defined | YAML sources block | Model SQL file in your project |
| Build behavior | Not built by dbt | Built by dbt during run/build |
| Freshness | Supported via dbt source freshness |
Not applicable |
| Testing | Source-level tests supported | Model and column tests supported |
| Use cases | Landing, vendor exports, CDC sinks | Staging, intermediate, marts |
End-to-end example: orders at 40M rows
Scenario: your orders table has 40M rows in a raw schema. You want a clean stage model, then an intermediate model that joins customers and computes revenue metrics.
# sources.yml
version: 2
sources:
- name: app
database: analytics_prod
schema: raw
loaded_at_field: _ingested_at
tables:
- name: orders
- name: customers
-- models/stg_orders.sql
{{ config(materialized='incremental', unique_key='id') }}
select
id as order_id,
customer_id,
order_created_at,
cast(total_amount as numeric) as total_amount
from {{ source('app', 'orders') }}
{% if is_incremental() %}
where order_created_at > (select coalesce(max(order_created_at), '1900-01-01') from {{ this }})
{% endif %}
-- models/stg_customers.sql
select
id as customer_id,
lower(email) as email,
created_at
from {{ source('app', 'customers') }}
-- models/int_orders_enriched.sql
select
o.order_id,
o.customer_id,
o.total_amount,
c.email,
o.order_created_at
from {{ ref('stg_orders') }} o
left join {{ ref('stg_customers') }} c
on o.customer_id = c.customer_id
Notice how we source raw relations and ref modeled relations. If performance is a concern, use incremental strategies on the large staging model so repeated queries only process new data. For a beginner-friendly walkthrough of materializations, continue with our dbt tutorial for beginners and see the dbt topic hub.
Selectors you’ll actually use
- Run only models downstream of a specific raw source:
dbt run --select +source:app.orders - Test just your sources:
See our full guide: dbt Tests Guide.dbt test --select source:* - Check freshness for all sources:
dbt source freshness - List what will execute before a model:
dbt ls --select @int_orders_enriched
Freshness: include and exclude
How do I exclude a table from a freshness snapshot? Two practical options:
- Do not set
loaded_at_fieldorfreshnessfor that table in YAML. dbt will skip it. - Use selectors to include only what you want:
dbt source freshness --select source:app.orders
If you truly don’t want dbt to consider a declared raw table at all, you can set enabled: false on that table, but that disables the source reference entirely.
Environments, schemas, and how dbt resolves references
ref() and source() resolve to the right database and schema for the current target. You specify these in your profiles and environment settings; dbt swaps them at runtime, so the same SQL file works in dev, staging, and prod without edits. That’s why you shouldn’t hardcode relation paths in SQL.
- ref(): points to a built relation in your target schema (naming can vary by adapter). dbt will handle quoting and case rules for the warehouse.
- source(): points to the declared database and schema you set in YAML. You can override per-environment via variables or profile targets if your raw zones differ.
Outcome: the same code can run safely across environments, the tool knows the dependency order, and your team can review a predictable workflow. This also keeps documentation accurate as models move or you rename schemas.
Common pitfalls and how to handle them
- Hardcoding schema names in SQL. Don’t. Use source() and ref() so dbt resolves environments per schema and database automatically.
- Referencing a dbt-built relation with source(). If dbt creates it, use ref().
- Mixing staging with marts. Keep staging models thin and close to source tables; push business logic into intermediate and mart layers for clarity and testing.
- Forgetting tests on sources. Add not_null/unique tests early to catch ingestion drifts. See the dbt test guide.
- Permissions drift. Ensure the executing user can read raw schemas and write to analytics schemas.
Best practice: clear layer boundaries
My best practice: source() in staging models; ref() everywhere else. That separation makes references between models obvious and simplifies your workflow. It also produces cleaner lineage and faster reviews for your team.
FAQ
What is a source in dbt?
A declared external relation that dbt doesn’t build. You define database and schema plus optional freshness in YAML; then reference it with {{ source('name', 'table') }}. This keeps your stage close to the upstream load while enabling tests and freshness.
What does ref do in dbt?
It’s the primary way of using ref to point one model at another. The ref() function in dbt creates a dependency and lets dbt resolve the correct relation name at runtime. This ensures correct order and environment-specific schemas.
Is dbt still open source?
Yes. dbt Core remains open source; dbt Cloud is a hosted, paid platform. You can run Core locally or in CI and still benefit from the same model semantics. See our overview of metrics in the dbt Semantic Layer.
What is the difference between sources and staging in dbt?
“Source” is a declaration layer pointing to raw inputs; “staging” is your first dbt model layer that selects from source() and standardizes fields and types. Staging does minimal transformation and prepares data for downstream models.
Can’t you just materialize the data as a table in staging or intermediate to avoid repeated work?
Yes—materializations and incremental transformation patterns are great when row counts explode. Materialize staging models with incremental strategies to cut compute on large query runs. Keep core logic modular and unit-testable; then materialize where it counts.
How do I run models downstream of one source?
Use selectors that start from a source: dbt run --select +source:app.orders. That will execute all models that depend on the orders source.
How do I run data tests on just my sources?
Run dbt test --select source:*. Add table and column tests in YAML next to the source definitions.
How do I exclude a table from a freshness snapshot?
Don’t set loaded_at_field or freshness for that table, or use selectors: dbt source freshness --select source:my_src.my_table.
"?(?P<table>\w+)"? — what is this?
It’s a regex that captures an optional-quoted identifier into a named group table. Handy if you’re parsing logs or generating sql from metadata. Example: use it in a script to extract a table name from a statement when you need to script selection logic.
But, given that I had a hard time understanding what you are trying to get across (and others here as well), I am guessing dbt is a new introduced tool in your stack?
If dbt is new in your stack, start by declaring sources, then add staging models that select from them, and finally build marts via ref(). Keep changes small, add tests early, and let dbt’s dag guide the order. Our beginner tutorial is the fastest on-ramp.
Selectors, testing, and documentation in one place
Once you define sources and models, dbt can generate documentation, produce a dependency graph, and ensure your builds run in the right order. You can also push metrics later; read our overview of the dbt Semantic Layer for where metrics fit. For change tracking on slowly-evolving data, see the dbt Snapshots playbook (our dbt developer hub primer).
Advanced tips that save time
- Use
ref()everywhere for internal references between models so renames don’t break downstreamselectstatements. - For giant raw inputs, push partition/pruning predicates into your staging
selectand consider incremental materializations to reduce query cost in the warehouse. - Keep one source per upstream system, with clear description and owners, to simplify discovery and on-call tasks.
- If you must rename schemas across environments, ref() and source() will resolve to the right schema based on your setting and targets.
- When ingestion load jobs change, update YAML and tests first so your dag stays accurate.
Putting it all together: source and ref in one pattern
Here’s a compact blueprint you can copy into your dbt project to wire raw to marts:
# 1) Declare source
sources:
- name: billing
database: fin_prod
schema: raw
loaded_at_field: _loaded_at
tables:
- name: invoices
-- 2) Stage from source
-- models/stg_invoices.sql
select *
from {{ source('billing', 'invoices') }}
where invoice_date >= dateadd(month, -6, current_date)
-- 3) Intermediate logic
-- models/int_invoice_amounts.sql
select
invoice_id,
sum(line_amount) as total_amount
from {{ ref('stg_invoices') }}
group by 1
-- 4) Mart
-- models/mart_invoice_summary.sql
select * from {{ ref('int_invoice_amounts') }}
This keeps raw inputs behind source(), model-to-model relationships behind ref(), and your marts focused. It also makes your dag readable at a glance and lets dbt execute models consistently across your warehouse.
Commands you’ll reuse daily
dbt build --select tag:stagingto build staging plus tests in one go.dbt run --select +ref:int_orders_enrichedto run a model and all upstream dependencies.dbt ls --select source:*to enumerate all declared raw inputs.
Why this matters
Clean separation of source() and ref() keeps your warehouse portable across environments, makes deployment safer, and lets dbt execute models in deterministic order. It’s also critical for future-proofing as your tool stack grows. Treat source and ref as the core function pair that stabilizes your pipeline. As a tool choice, dbt helps you manage data reliably in a modern analytics platform without fragile path hardcoding.
Glossary-level reminders
- function in dbt: ref() and source() are functions that resolve relation names and set dependencies.
- dbt models: SQL files materialized as views/tables; always reference them with ref().
- dbt project: Your repo with models, macros, and YAML that define sources and tests.
- references between models: created via ref(); never hardcode schema-qualified names.
- source and ref: one for external inputs, the other for internal outputs.
- source tables: raw relations declared in YAML; test and monitor them for freshness.
One-liner guidance
source() for external raw inputs; ref() for internal modeled relations—source and ref are the backbone of a maintainable dbt build.
Before you leave: grab our free graded practice exercises at /practice.
Take this concept into practice.
Reading is fine. Doing is what gets you hired. Pick an exercise on this topic or open a portfolio project.
- dbt
dbt Semantic Layer for Analytics Engineers: MetricFlow & dbt Labs
A deep, practitioner-first guide to the dbt semantic layer: define metrics once, serve them everywhere. We cover MetricFlow, security, integrations, and real query patterns.
- dbt
dbt Snapshots: dbt snapshot Playbook (dbt developer hub)
A practitioner’s guide to dbt snapshots: configuration, timestamp vs check strategy, performance, deletes, and testing—no SCD theory rehash.
- dbt
dbt Tutorial for Beginners: Learn dbt, the Data Build Tool
A fast, practical dbt tutorial for beginners. Go from setup to your first dbt project with real SQL, ref(), tests, docs, snapshots, and incremental models.
