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.

Data Engineerdbtanalytics-engineeringsql

The staging-marts pattern

A mature dbt project has a two-layer model hierarchy:

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

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

Make an ETL Pipeline IdempotentData projectSpeed Up a Slow SQL Report QueryData projectMake a CSV Importer Resilient to Bad DataData projectData roadmapStep by step to hiredData interview questionsSTAR answersAll Data projectsProjects hub

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 →