Слои проекта: staging / intermediate / marts
Когда в проекте 3 модели — структура очевидна. Когда 300 — без структуры всё превращается в граф «каждый ссылается на каждого». Кто-то делает JOIN сразу в staging, кто-то — фильтр в marts, кто-то — агрегацию в середине. Через год невозможно понять, что значит «это поле» и кто на него полагается.
Стандарт dbt-сообщества — три слоя: staging, intermediate, marts. Это не закон, а convention. Большинство production-проектов следуют ему, документация dbt Labs (guide на dbt docs) описывает именно эту структуру. Знание этой схемы — это базовый словарь для любого dbt-разработчика.
Три слоя — общий обзор
Главная идея: данные текут слева направо. Каждый слой делает свою работу и передаёт результат следующему. Обратный путь (marts ссылается на marts) допустим, но редок. Skip-уровни (marts напрямую на source без staging) — антипаттерн.
Слой staging
models/staging/<source>/stg_<source>__<table>.sql — это 1:1 копия source-таблицы с минимальными преобразованиями:
- Переименование колонок в snake_case, осмысленные имена (
cust_id->customer_id). - Приведение типов (
updated_at AS DATE). - Базовая чистка — trim, lower, replace символов.
- Удаление лишних колонок —
internal_admin_notes,legacy_field_2018.
Что запрещено в staging:
- JOIN-ы. Каждый staging-файл = одна source-таблица.
- Агрегации. Никаких GROUP BY.
- Бизнес-логика. Никаких CASE WHEN tier=‘vip’ THEN … END.
- Фильтры по бизнес-правилам. WHERE deleted = false — ок (это техническая чистка). WHERE amount > 100 — нет (это business filter, место в marts).
Пример. Source app.customers:
-- app.customers (в Postgres / DuckDB)
id INT
fname VARCHAR
lname VARCHAR
email_addr VARCHAR
created_dt TIMESTAMP
deleted_flag BOOLEAN
Staging-модель models/staging/jaffle/stg_jaffle__customers.sql:
WITH source AS (
SELECT * FROM {{ source('jaffle', 'customers') }}
)
SELECT
id AS customer_id,
fname AS first_name,
lname AS last_name,
TRIM(LOWER(email_addr)) AS email,
CAST(created_dt AS DATE) AS created_date,
deleted_flag AS is_deleted
FROM source
WHERE deleted_flag = false -- техническая чистка ок
Что важно:
- Имя файла:
stg_<source>__<table>. Двойное подчёркивание__отделяет source от таблицы. Это convention из dbt Labs. - Префикс source (
stg_jaffle__) — на случай если у вас два source с таблицейcustomers. Например,stg_shopify__customersиstg_jaffle__customers. - Колонки переименованы в snake_case.
- Никакого JOIN, агрегации, бизнес-логики.
Слой intermediate
models/intermediate/<domain>/int_<purpose>.sql — опциональный слой. Не каждый проект его использует. Назначение:
- Декомпозиция сложных marts. Если mart-модель на 300 строк SQL с 8 JOIN-ами — её сложно читать и тестировать. Разбейте на 2-3 intermediate-модели.
- Pre-aggregations. Заранее посчитанные суммы / count’ы, которые используются в нескольких marts.
- Pivot / unpivot. Преобразование вертикальной таблицы в широкую (или обратно).
- Объединение разных source. Когда несколько staging-таблиц нужно слить в одну логическую сущность.
Пример. Заказ может иметь несколько items с разными типами оплаты:
-- models/intermediate/finance/int_order_items_pivoted.sql
WITH order_items AS (
SELECT * FROM {{ ref('stg_jaffle__order_items') }}
),
pivoted AS (
SELECT
order_id,
SUM(CASE WHEN item_type = 'food' THEN amount END) AS food_amount,
SUM(CASE WHEN item_type = 'drink' THEN amount END) AS drink_amount,
SUM(CASE WHEN item_type = 'dessert' THEN amount END) AS dessert_amount,
SUM(amount) AS total_amount
FROM order_items
GROUP BY order_id
)
SELECT * FROM pivoted
Эту intermediate-модель можно теперь использовать в marts/orders и marts/revenue_daily, не повторяя pivot-логику.
Что запрещено в intermediate:
- Прямые ссылки на BI-tools. Intermediate — не для UI. Это инструмент моделирования, его не показывают в Tableau / Looker.
- Конечные бизнес-определения. «Активный клиент» как формула — это в marts, не в intermediate.
Не каждый проект нужен intermediate. Если у вас 10 моделей — спокойно обойдётесь без него. Если 100+ моделей и mart-модели всё чаще на 200+ строк — добавляйте.
Слой marts
models/marts/<domain>/<entity>.sql — финальные таблицы для BI / analytics. Это то, что видят аналитики, что показывается в дашбордах, что используется в exposures.
Бизнес-понятия:
customers— обогащённый dimension клиентов.orders— fact-таблица заказов.revenue_daily— агрегат по дням.customer_metrics— wide-таблица метрик на клиента.
Имена файлов:
- Без префикса для простых mart’ов:
customers.sql,orders.sql. - С префиксом
fct_/dim_для классической Kimball-стилизации:fct_orders.sql,dim_customers.sql. - Не используйте
stg_илиint_— это другие слои.
Что разрешено в marts:
- JOIN-ы любой сложности. JOIN staging + intermediate + другие marts.
- Бизнес-логика.
CASE WHEN, бизнес-формулы. - Агрегации. GROUP BY на любом уровне.
- Window functions. ROW_NUMBER, LAG, running totals.
Пример models/marts/orders/fct_orders.sql:
WITH orders AS (
SELECT * FROM {{ ref('stg_jaffle__orders') }}
),
order_items AS (
SELECT * FROM {{ ref('int_order_items_pivoted') }}
),
customers AS (
SELECT * FROM {{ ref('stg_jaffle__customers') }}
)
SELECT
o.order_id,
o.customer_id,
o.order_date,
c.first_name || ' ' || c.last_name AS customer_name,
o.status,
oi.food_amount,
oi.drink_amount,
oi.dessert_amount,
oi.total_amount,
CASE
WHEN oi.total_amount > 100 THEN 'large'
WHEN oi.total_amount > 50 THEN 'medium'
ELSE 'small'
END AS order_size_category
FROM orders o
LEFT JOIN order_items oi ON o.order_id = oi.order_id
LEFT JOIN customers c ON o.customer_id = c.customer_id
Видно: marts — это где собирается всё. Staging — кирпичики, intermediate — стены, marts — комнаты.
Какие зависимости разрешены
Это самое важное правило:
Правила:
- staging -> только source / seed. Никаких ref на другие модели.
- intermediate -> только staging / intermediate. Не ссылается на marts.
- marts -> может всё (включая другие marts).
Антипаттерны (что НЕ делать):
marts -> sourceнапрямую, без staging. Если потом схема source поменяется — придётся править много mart-файлов. Через staging — одно место.staging -> JOIN другой staging. Это уже intermediate-задача.intermediate -> marts. Это циклическая зависимость через слой.
Структура директорий в реальном проекте
models/
staging/
jaffle/
_jaffle__sources.yml ← декларация source
_jaffle__models.yml ← descriptions + tests staging-моделей
stg_jaffle__customers.sql
stg_jaffle__orders.sql
stg_jaffle__order_items.sql
stg_jaffle__payments.sql
shopify/
_shopify__sources.yml
_shopify__models.yml
stg_shopify__products.sql
stg_shopify__inventory.sql
intermediate/
finance/
_int_finance__models.yml
int_payments_joined.sql
int_order_items_pivoted.sql
marketing/
_int_marketing__models.yml
int_campaign_costs_pivoted.sql
marts/
core/
_core__models.yml
fct_orders.sql
dim_customers.sql
finance/
_finance__models.yml
revenue_daily.sql
revenue_by_segment.sql
marketing/
_marketing__models.yml
campaign_attribution.sql
Видно, что внутри staging/, intermediate/, marts/ ещё есть поддиректории по доменам: jaffle, shopify, finance, marketing. Это не обязательно, но помогает в больших проектах.
В dbt_project.yml каждый слой обычно получает разную материализацию:
models:
jaffle_shop:
staging:
+materialized: view # staging — view, быстро пересчитывается
intermediate:
+materialized: ephemeral # intermediate — CTE, не материализуется в БД
marts:
+materialized: table # marts — table, для performance в BI
Это идиоматический паттерн — но не догма. Можно делать staging table’ами, marts view’ами и т.д., в зависимости от размеров данных и SLA.
Почему именно три слоя
Может показаться: «зачем мне staging, я же могу написать FROM source(...) сразу в mart?». Можно. Но через год вы поймёте, зачем staging.
Главная ценность staging — это точка изоляции от внешних изменений. Если Fivetran переименовал колонку cust_id -> customer_id в source, вы правите один файл stg_*.sql, а не 50 mart-файлов.
Аналогично intermediate — точка изоляции от внутренней сложности. Если бизнес-формула «активный клиент» меняется, вы правите её в одном int_active_customers.sql, а не в 10 marts.
Три слоя — это принцип single responsibility:
- staging отвечает за чистку,
- intermediate за вычислительные блоки,
- marts за бизнес-сущности.
Каждый слой — это контракт. Контракты позволяют менять имплементацию, не ломая зависимости.
Попробуй сам
Откройте свой dbt-проект. Создайте структуру:
models/
staging/
jaffle/
_jaffle__sources.yml
stg_jaffle__customers.sql
stg_jaffle__orders.sql
intermediate/
int_customer_orders.sql
marts/
dim_customers.sql
- В
stg_jaffle__customers.sqlсделайте чистку source-таблицы (переименование, типизация). - В
stg_jaffle__orders.sqlто же для заказов. - В
int_customer_orders.sqlсделайте JOIN customers + orders + GROUP BY (customer_id, COUNT orders, SUM amount). - В
dim_customers.sqlсоберите финальный dimension: customer + count + sum.
Запустите dbt run. Откройте dbt docs serve — посмотрите DAG. Должен быть чистый поток слева направо: source -> stg_* -> int_* -> dim_*.
Бонус: попробуйте сделать dim_customers ссылающимся напрямую на source без staging. Запустите. Работает? Да. Но представьте, что у вас 50 таких dim, и Fivetran переименовал колонку. Сколько мест править?
Ключевые выводы
- Стандартная архитектура dbt — три слоя: staging, intermediate, marts. Это не закон, а сильная конвенция.
- staging — 1:1 с source. Переименование, типизация, чистка. Никаких JOIN/агрегаций/бизнес-логики.
- intermediate — опциональный слой. Декомпозиция сложных marts, pre-aggregations, pivot. Не для BI напрямую.
- marts — финальные таблицы для BI. Бизнес-сущности (customers, orders, revenue). Может ссылаться на всё.
- Зависимости текут слева направо: source -> staging -> intermediate -> marts. Skip-уровни (marts -> source) — антипаттерн.
- Имена файлов:
stg_<source>__<table>,int_<purpose>,<entity>илиfct_/dim_<entity>. - Материализации по слоям в
dbt_project.yml: staging view, intermediate ephemeral, marts table — типичный паттерн. - Главная ценность слоёв — single responsibility и изоляция изменений: переименование колонки в source -> правишь staging, не 50 marts.