Modelling correctness · rule staging-depends-on-staging
dbt staging models: how to set up the staging layer
A dbt staging model tidies one source table and passes it on. The staging layer is the only place that calls source(), so everything downstream builds on clean, consistently named data.
A Metacenta review checks this under the rule Staging models read sources, not each other. Everything below applies whether or not you ever commission one.
How to organise and name staging models
dbt Labs' guide gives each source system its own folder, and names each file stg_[source]__[entity]s.sql. The double underscore separates the source from the entity. Don't group these folders by loader or business area.
models/staging/
├── shop/
│ ├── _shop__sources.yml
│ ├── stg_shop__customers.sql
│ └── stg_shop__orders.sql
└── stripe/
├── _stripe__sources.yml
└── stg_stripe__payments.sql
Each source table gets one staging model.
What belongs in a staging model
- Renaming columns
- Casting types
- Basic calculations, such as cents to pounds
- Categorising values with conditional logic
Leave out joins and aggregations. A join belongs in an intermediate model, and an aggregation changes the grain and drops detail you may need later.
select
id as order_id,
customer as customer_id,
total_cents / 100.0 as order_total,
created_at as ordered_at
from {{ source('shop', 'orders') }}
How to materialise staging models
dbt Labs recommends views. Downstream models then always read current data, and you don't store tables nobody queries directly. Set it once for the folder.
# dbt_project.yml
models:
my_project:
staging:
+materialized: view
When staging needs a join
Sometimes a clean table needs a join, such as applying a separate table of deletions. dbt Labs' answer is a base/ folder under the source's staging folder: base models read the raw tables, and a staging model joins them.
This rule treats a base/ folder under staging/ as staging, so it flags that join. Record it in the model's description as a deliberate choice. See dbt staging models that read marts for the related rule.
What this rule checks
This rule flags staging models that read another staging model. We place a model in a layer from its path: a staging, intermediate or marts folder. If no model's path places it in a layer, the rule declines.
Why it matters
Staging stops being the one place a raw table is tidied. Reading a staged model no longer tells you what it contains; you have to follow the chain back.
How to fix it
Point each cleaning step at raw data, not at another cleaning step. Move the joining or reshaping logic into an intermediate model, and leave each staging model reading only its source.
Before:
-- models/staging/shop/stg_shop__orders_enriched.sql
select o.*, c.region
from {{ ref('stg_shop__orders') }} as o
join {{ ref('stg_shop__customers') }} as c using (customer_id)
After:
-- models/intermediate/int_orders_with_region.sql
select o.*, c.region
from {{ ref('stg_shop__orders') }} as o
join {{ ref('stg_shop__customers') }} as c using (customer_id)
When it is fine to leave
Some teams deliberately build a second staging model over the first, such as deduplicating after a rename. That is a defensible style, which is why we rate this low severity at medium confidence. Say so in the model's description.
What we need to check it
manifest.json alone. Layers come from folder names; marts folders are marts, mart, presentation or reporting. A project that uses no such folders is not judged here.
Published rules it corresponds to
dbt_project_evaluator, rule fct_staging_dependent_on_staging. Ours checks the same condition and threshold.
This means our check corresponds to their rule. It does not mean the publisher reviewed or endorses it.