Metacenta

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.