How to Build a dbt Staging-to-Marts Project
A dbt staging-to-marts project is the standard analytics engineering pattern: a staging layer cleans raw tables, and a marts layer joins and aggregates them into reporting models. Here's how to build one from scratch.
The staging-marts pattern
A mature dbt project has a two-layer model hierarchy:
- Staging (
models/staging/) - one model per raw source table. It selects, casts types, renames columns, and filters nothing. This is the single clean copy of the raw data that every downstream model uses. - Marts (
models/marts/) - business-question models that join and aggregate staging models into the tables analysts actually query.
The split matters because it decouples "what the source looks like" from "what the business wants to know."
Project scaffolding
A minimal dbt_project.yml sets the project name, profile, and materializations per layer:
name: shop
version: 1.0.0
config-version: 2
profile: shop
model-paths: ["models"]
target-path: target
models:
shop:
staging:
+materialized: view # staging models are cheap views
marts:
+materialized: table # mart models are materialized tables
Declare sources
models/sources.yml tells dbt where your raw data lives. Using {{ source() }}
instead of a hard-coded schema name means you can swap schemas (dev vs prod) without
touching the model SQL:
version: 2
sources:
- name: raw
schema: public
tables:
- name: orders
- name: customers
The staging model
models/staging/stg_orders.sql selects from the source reference and normalizes
column names and types - nothing else:
SELECT
id AS order_id,
customer_id,
total_amount AS amount,
created_at
FROM {{ source('raw', 'orders') }}
The mart model
models/marts/customer_orders.sql uses {{ ref('stg_orders') }} to reference the
staging model. dbt's DAG resolver guarantees staging runs first:
WITH orders AS (
SELECT * FROM {{ ref('stg_orders') }}
),
customers AS (
SELECT id AS customer_id, name AS customer_name
FROM {{ source('raw', 'customers') }}
)
SELECT
c.customer_id,
c.customer_name,
COUNT(o.order_id) AS total_orders,
COALESCE(SUM(o.amount), 0) AS total_spend
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.customer_id
GROUP BY c.customer_id, c.customer_name
Adding tests
A schema.yml next to the mart file attaches column-level tests. dbt build
runs models AND tests in a single command:
version: 2
models:
- name: customer_orders
columns:
- name: customer_id
tests:
- not_null
- unique
Run the full project with:
cd dbt_project
dbt build --profiles-dir ..
dbt build compiles the Jinja SQL, materializes staging views, materializes the
mart table, and runs every schema test - one command for the full lineage.
Want to try it hands-on? HeyDevJob gives you this exact setup in a live cloud workspace in your browser - edit it, run it, and see it work. Free, nothing to install.
Try it in a workspace →What you'll practice
- Declaring raw sources in sources.yml and referencing them with {{ source() }}
- Building a staging model that normalizes a raw table and a mart that joins + aggregates it
- Attaching not_null and unique tests via schema.yml and running the full lineage with dbt build
FAQ
What is the dbt staging-to-marts pattern?
Staging models (one per raw source table) clean and normalize raw data. Mart models join and aggregate staging models into business-facing reporting tables. The split keeps source changes isolated from business logic.
What is the difference between {{ source() }} and {{ ref() }} in dbt?
{{ source('raw', 'orders') }} references a raw table declared in sources.yml - outside dbt's control. {{ ref('stg_orders') }} references another dbt model and tells dbt to build that model first, wiring the DAG.
How do I run models and tests together in dbt?
Use dbt build - it runs dbt run (materializes models) and dbt test (runs schema tests) in dependency order in one command. dbt run alone skips tests.
What is the difference between staging and marts in dbt?
Staging models clean and standardize raw source data one-to-one (rename columns, cast types); marts join and aggregate staging models into business-ready tables. Staging is plumbing; marts are the product.
What are marts in dbt?
Marts are the final, business-facing dbt models - joined and aggregated tables (often star-schema facts and dimensions) that analysts query directly, built on top of staging models.
Keep learning
Learn it by doing. Open this in a live cloud workspace, make the change yourself, and keep a record of the work you can share.
Open the workspace →