In dbt, organizing your data models into distinct layers—Staging, Intermediate, and Mart—is a fundamental best practice for building robust, maintainable, and scalable data pipelines. This layered architecture promotes separation of concerns, making your transformations easier to understand, debug, and reuse. Each layer serves a specific purpose, guiding raw data through progressive stages of cleaning, aggregation, and business logic application until it's ready for consumption.
Your journey begins with the Staging Layer (conventionally prefixed stg_). These models are the first transformation step from your raw source data. Their primary role is minimal cleaning and standardization: casting data types, renaming columns for consistency, basic filtering (e.g., removing soft-deleted records), and handling nulls. Staging models should typically represent a one-to-one mapping with your source tables, avoiding complex joins or aggregations. They act as a reliable, consistent foundation for all downstream transformations, abstracting away the idiosyncrasies of your source systems.
Next, the Intermediate Layer (often prefixed int_) is where reusable, complex business logic or transformations reside. These models typically join multiple staging models or perform aggregations that will be used by several final data marts. By centralizing these calculations, you avoid duplication and ensure consistency across your reports. Finally, the Mart Layer is your consumption-ready data. This layer (often prefixed fct_ for facts, dim_ for dimensions, or agg_ for aggregates) contains models tailored to specific business use cases, dashboards, or analytical tools. They leverage intermediate models and provide the final, user-friendly data structures required by analysts and business users.
Key Takeaways
- Layered architecture (Staging, Intermediate, Mart) enhances dbt project clarity and maintainability.
- Staging models (
stg_): First step, minimal cleaning, 1:1 with source data. - Intermediate models (
int_): Complex, reusable business logic, joins across staging models. - Mart models (
fct_,dim_,agg_): Final, consumption-ready data for specific business needs. - Consistent naming conventions are crucial for navigation and understanding dependencies.
Code Example
-- models/staging/stg_raw_products.sql
SELECT
id AS product_id,
name AS product_name,
price AS unit_price_usd
FROM {{ source('ecommerce', 'products') }}
WHERE is_available = TRUE
-- models/intermediate/int_products_with_tax.sql
SELECT
product_id,
product_name,
unit_price_usd,
unit_price_usd * 1.05 AS price_with_tax_usd -- Example reusable logic
FROM {{ ref('stg_raw_products') }}
-- models/marts/fct_product_catalog.sql
SELECT
product_id,
product_name,
price_with_tax_usd AS retail_price_usd,
'Active' AS product_status_category
FROM {{ ref('int_products_with_tax') }}How this code works
This code demonstrates dbt's layered approach to data transformation, organizing raw product data into progressively refined staging, intermediate, and mart models. The overall job is to transform raw product details into a clean, enriched, and analytics-ready format. The first model, models/staging/stg_raw_products.sql, serves as the staging layer. It connects directly to the raw ecommerce.products table using {{ source('ecommerce', 'products') }}. This layer focuses on initial cleaning and standardization, such as renaming columns like id AS product_id, and filtering out unavailable items using WHERE is_available = TRUE to provide a consistent base.
Moving into the intermediate layer, models/intermediate/int_products_with_tax.sql builds upon the staged data, referencing it with {{ ref('stg_raw_products') }}. This model introduces reusable business logic, such as calculating price_with_tax_usd by multiplying unit_price_usd by 1.05. Finally, models/marts/fct_product_catalog.sql creates a mart layer, consuming data from the intermediate model via {{ ref('int_products_with_tax') }}. Mart models are tailored for specific analytical use cases, here renaming price_with_tax_usd AS retail_price_usd and adding a fixed 'Active' AS product_status_category. A subtle but critical dbt feature is how {{ ref() }} automatically manages the build order: dbt understands that stg_raw_products must run before int_products_with_tax, and int_products_with_tax before fct_product_catalog, ensuring all transformations execute in the correct sequence.