Learning Platform
Глоссарий Troubleshooting
Урок 15.01 · 25 мин
Начальный
Project structureStagingIntermediateMartsLayersArchitecture

Слои проекта: staging / intermediate / marts

Когда в проекте 3 модели — структура очевидна. Когда 300 — без структуры всё превращается в граф «каждый ссылается на каждого». Кто-то делает JOIN сразу в staging, кто-то — фильтр в marts, кто-то — агрегацию в середине. Через год невозможно понять, что значит «это поле» и кто на него полагается.

Стандарт dbt-сообщества — три слоя: staging, intermediate, marts. Это не закон, а convention. Большинство production-проектов следуют ему, документация dbt Labs (guide на dbt docs) описывает именно эту структуру. Знание этой схемы — это базовый словарь для любого dbt-разработчика.


Три слоя — общий обзор

Слои dbt-проекта

Главная идея: данные текут слева направо. Каждый слой делает свою работу и передаёт результат следующему. Обратный путь (marts ссылается на marts) допустим, но редок. Skip-уровни (marts напрямую на source без staging) — антипаттерн.


Слой staging

models/staging/<source>/stg_<source>__<table>.sql — это 1:1 копия source-таблицы с минимальными преобразованиями:

  1. Переименование колонок в snake_case, осмысленные имена (cust_id -> customer_id).
  2. Приведение типов (updated_at AS DATE).
  3. Базовая чистка — trim, lower, replace символов.
  4. Удаление лишних колонок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опциональный слой. Не каждый проект его использует. Назначение:

  1. Декомпозиция сложных marts. Если mart-модель на 300 строк SQL с 8 JOIN-ами — её сложно читать и тестировать. Разбейте на 2-3 intermediate-модели.
  2. Pre-aggregations. Заранее посчитанные суммы / count’ы, которые используются в нескольких marts.
  3. Pivot / unpivot. Преобразование вертикальной таблицы в широкую (или обратно).
  4. Объединение разных 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 — комнаты.


Какие зависимости разрешены

Это самое важное правило:

Допустимые зависимости между слоями

Правила:

  1. staging -> только source / seed. Никаких ref на другие модели.
  2. intermediate -> только staging / intermediate. Не ссылается на marts.
  3. 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.

TIP

Главная ценность 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
  1. В stg_jaffle__customers.sql сделайте чистку source-таблицы (переименование, типизация).
  2. В stg_jaffle__orders.sql то же для заказов.
  3. В int_customer_orders.sql сделайте JOIN customers + orders + GROUP BY (customer_id, COUNT orders, SUM amount).
  4. В dim_customers.sql соберите финальный dimension: customer + count + sum.

Запустите dbt run. Откройте dbt docs serve — посмотрите DAG. Должен быть чистый поток слева направо: source -> stg_* -> int_* -> dim_*.

Бонус: попробуйте сделать dim_customers ссылающимся напрямую на source без staging. Запустите. Работает? Да. Но представьте, что у вас 50 таких dim, и Fivetran переименовал колонку. Сколько мест править?


Ключевые выводы

  1. Стандартная архитектура dbt — три слоя: staging, intermediate, marts. Это не закон, а сильная конвенция.
  2. staging — 1:1 с source. Переименование, типизация, чистка. Никаких JOIN/агрегаций/бизнес-логики.
  3. intermediate — опциональный слой. Декомпозиция сложных marts, pre-aggregations, pivot. Не для BI напрямую.
  4. marts — финальные таблицы для BI. Бизнес-сущности (customers, orders, revenue). Может ссылаться на всё.
  5. Зависимости текут слева направо: source -> staging -> intermediate -> marts. Skip-уровни (marts -> source) — антипаттерн.
  6. Имена файлов: stg_<source>__<table>, int_<purpose>, <entity> или fct_/dim_<entity>.
  7. Материализации по слоям в dbt_project.yml: staging view, intermediate ephemeral, marts table — типичный паттерн.
  8. Главная ценность слоёв — single responsibility и изоляция изменений: переименование колонки в source -> правишь staging, не 50 marts.
Medallion architecture: bronze, silver, gold — теория за staging/marts Архитектура DWH: слои и Medallion
Проверка знанийKnowledge check
Junior написал mart `fct_revenue_by_country.sql`, который делает JOIN напрямую с `source('app', 'customers')` и `source('app', 'orders')`. Менеджер просит добавить staging-слой. Что улучшится в проекте после этого?
ОтветAnswer
После добавления staging-слоя (`stg_app__customers.sql` и `stg_app__orders.sql`):\n\n1. **Изоляция от source-схемы.** Если кто-то переименует колонку в `app.customers` (например, `cust_id` -> `customer_id`), исправление будет в **одном** stg-файле. Сейчас — везде, где есть JOIN с source.\n\n2. **Стандартизация имён.** Source часто содержит неаккуратные имена (`fname`, `bday_dt`). Staging переименует в snake_case (`first_name`, `birth_date`). Mart использует чистые имена.\n\n3. **Типизация в одном месте.** Если `created_at` приходит как VARCHAR, кастуем в TIMESTAMP в staging — все downstream получают правильный тип.\n\n4. **Тестирование source-данных.** В staging-YAML навешиваются тесты `not_null`, `unique` на ID. Это первая линия защиты от грязных данных.\n\n5. **DAG читабельнее.** В `dbt docs serve` видна структура: что приходит из source, что чистится, что материализуется.\n\n6. **Soft-delete / фильтры в одном месте.** Условие `WHERE deleted = false` пишется в staging. Все downstream работают только с активными записями.
Проверка знанийKnowledge check
Какое из следующих утверждений — антипаттерн dbt project structure? (1) `stg_jaffle__customers.sql` делает JOIN на `stg_jaffle__addresses.sql`. (2) `int_active_customers.sql` ссылается на `stg_jaffle__customers.sql`. (3) `fct_orders.sql` ссылается на `int_order_items_pivoted.sql`. (4) `dim_customers.sql` ссылается на `fct_orders.sql` через ref().
ОтветAnswer
Антипаттерн — **(1) staging делает JOIN на другой staging**. Это нарушает базовый принцип staging-слоя: `stg_*.sql` должен быть **1:1 с одной source-таблицей**. JOIN — это intermediate-задача (`int_*`).\n\nОстальные допустимы:\n- (2) intermediate -> staging — корректный поток, intermediate всегда строится на staging.\n- (3) mart -> intermediate — корректно, intermediate именно для этого создан.\n- (4) mart -> другой mart — допустимо. Например, `dim_customers` может использовать агрегаты из `fct_orders` (например, last_order_date). Главное — нет циклов.

Проверьте понимание

Результат: 0 из 0
Прикладной
Вопрос 1 из 5. В staging-слое допустима ли модель `stg_jaffle__customers_with_orders.sql`, которая JOIN-ит customers и orders из jaffle source?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 4