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:
    dbt test --select source:*
    See our full guide: dbt Tests Guide.
  • 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_field or freshness for 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 downstream select statements.
  • For giant raw inputs, push partition/pruning predicates into your staging select and 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:staging to build staging plus tests in one go.
  • dbt run --select +ref:int_orders_enriched to 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.

Next steps

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.