dbt: структура проекта и слои на собеседовании Data Engineer

Проверь себя · 1/3разбор после ответа
Нужно получить количество заказов по паре (user_id, status) из таблицы orders. Какой запрос верный?

Почему структуру dbt спрашивают

dbt позволяет написать одну гигантскую модель с десятью джойнами и агрегациями — и она даже заработает. Проблема в том, что через полгода в таком проекте невозможно ничего найти, переиспользовать или протестировать. Поэтому на собесе Data Engineer вопрос про структуру проекта проверяет не знание dbt как инструмента, а инженерную зрелость: умеете ли вы разбивать трансформацию на слои, где у каждого своя ответственность.

Стандарт, который продвигает dbt Labs и который ждут почти на любом собесе, — три слоя: staging, intermediate и marts. Данные текут снизу вверх: сырьё чистится, обогащается и превращается в витрины для бизнеса. Разберём каждый слой и зачем он нужен.

Слоёная структура

Типичная раскладка каталога models/ в dbt-проекте:

models/
  staging/      — лёгкая очистка сырья, 1:1 с источниками
  intermediate/ — переиспользуемые джойны и логика
  marts/        — витрины для бизнеса

Идея в разделении ответственности: staging приводит источники к порядку, intermediate готовит переиспользуемые заготовки, marts отдаёт готовые витрины в BI и ML. Каждый слой опирается только на предыдущий, поэтому DAG остаётся читаемым, а изменение источника не приходится править в двадцати местах.

Staging-модели

Именование: stg_<source>__<table>.sql (двойное подчёркивание отделяет источник от таблицы).

Назначение — лёгкая очистка данных прямо из источника, без бизнес-логики. Здесь делают ровно три вещи:

  • Переименовывают колонки к единому стилю (например, всё в snake_case, iduser_id).
  • Приводят типы (CAST строки к дате, текста к числу).
  • Ставят лёгкие фильтры вроде отсева удалённых записей (deleted = false).
SELECT
  id AS user_id,
  email,
  CAST(created_at AS TIMESTAMP) AS created_at
FROM {{ source('app', 'users') }}
WHERE deleted = FALSE

Правило: одна staging-модель на одну таблицу источника, никаких джойнов и агрегаций. Staging — это тонкий слой-адаптер между сырьём и остальным проектом; обычно его материализуют как view, чтобы не хранить лишние копии. Как только источник переименовал колонку — правку делаем в одном месте, и весь проект дальше её не замечает.

Intermediate

Именование: int_<purpose>.sql.

Назначение — переиспользуемые джойны и промежуточная логика, которую не хочется отдавать напрямую в BI. Это «рабочие заготовки» между staging и marts.

-- int_user_with_orders.sql
SELECT
  u.user_id,
  u.email,
  COUNT(o.id) AS orders_count
FROM {{ ref('stg_app__users') }} u
LEFT JOIN {{ ref('stg_app__orders') }} o ON u.user_id = o.user_id
GROUP BY 1, 2

Смысл слоя — DRY. Если один и тот же джойн «пользователи + заказы» нужен в трёх разных витринах, его выносят в intermediate и переиспользуют через ref(), а не копируют трижды. Intermediate обычно не показывают аналитикам напрямую и часто материализуют как ephemeral или view — это внутренняя кухня трансформации.

Marts

Именование: fct_<grain>.sql, dim_<entity>.sql или mart_<subject>__<grain>.sql.

Назначение — витрины для бизнеса. Именно на marts смотрят дашборды, отчёты и ML-фичи. Здесь собирается финальная бизнес-логика: агрегации, метрики, готовые к употреблению таблицы фактов и измерений.

-- fct_orders_daily.sql
SELECT
  DATE,
  country,
  SUM(revenue) AS revenue,
  COUNT(*) AS orders_count
FROM {{ ref('int_orders_enriched') }}
GROUP BY 1, 2

Marts почти всегда материализуют как таблицы (а не view): по ним постоянно ходят дашборды, и пересчитывать логику на каждый запрос дорого. Организуют marts обычно по бизнес-доменам — финансы, маркетинг, продукт, — чтобы каждая команда работала со своим набором витрин.

Готовишься к собесу Data Engineer?
Spark, Airflow, ClickHouse, SQL для DE — вопросы с разборами в Telegram
Тренировать DE в Telegram

Соглашения по именованию

Предсказуемые имена — это не эстетика, а навигация: по названию модели сразу понятно, что это и на каком она слое.

sources:      raw_app.users
staging:      stg_app__users
intermediate: int_users_enriched
marts:
  - dim_*  — измерения (сущности: пользователи, товары)
  - fct_*  — факты (события: заказы, платежи)
  - mart_* — широкие витрины под конкретный отчёт

Единая схема именования помогает и людям, и инструментам: новый человек в команде за минуту понимает граф зависимостей, а автогенерация документации и поиск по проекту работают предсказуемо. На собесе умение внятно объяснить эту схему ценится выше, чем знание экзотических фич dbt.

Как это спрашивают на собесе

Как звучат вопросы вживую и что за ними стоит:

  • «Опишите структуру своего dbt-проекта.» Ждут три слоя и внятное объяснение ответственности каждого, а не пересказ документации.
  • «Куда положить джойн, который нужен в трёх витринах?» В intermediate — это прямая проверка на понимание DRY.
  • «Чем fct отличается от dim?» fct — таблица фактов (события, метрики, много строк), dim — измерение (сущности, атрибуты). Классика Кимбалла, её ждут почти всегда.
  • «Как материализуете слои?» staging и intermediate — обычно view/ephemeral, marts — table. Проверяют, понимаете ли вы trade-off между стоимостью хранения и скоростью чтения.
  • «Зачем вообще staging, если можно джойнить из source напрямую?» Чтобы изолировать проект от изменений источника и не переписывать логику по всему проекту при переименовании колонки.

Частые ошибки

  • Джойны и агрегации в staging. Staging должен быть 1:1 с источником. Как только туда заезжает бизнес-логика, слой перестаёт быть переиспользуемым адаптером.
  • Отсутствие intermediate. Если из staging сразу лепить marts, одинаковые джойны расползаются по витринам копипастом. Первое же изменение логики придётся вносить в пяти местах.
  • Ссылки на source в обход ref(). Прямой доступ к сырью из marts ломает граф зависимостей и lineage — dbt перестаёт видеть порядок построения.
  • Хаотичное именование. orders_final_v2, test_new — и через месяц проект нечитаем. Единая схема stg_/int_/fct_/dim_ экономит часы навигации.
  • Всё материализуется как table. Тяжёлые таблицы на каждом слое раздувают хранилище и время прогона. staging и intermediate почти всегда должны быть view.

Связанные темы

FAQ

Зачем разбивать проект на три слоя, а не писать одну модель?

Слои разделяют ответственность: staging чистит источник, intermediate переиспользует логику, marts отдаёт витрины бизнесу. Это делает проект читаемым, тестируемым и устойчивым к изменениям — при переименовании колонки в источнике правка нужна в одном месте, а не по всему проекту.

Чем отличаются fct- и dim-таблицы?

fct (fact) — таблица фактов: события и метрики, обычно много строк (заказы, платежи, клики). dim (dimension) — измерение: сущности и их атрибуты (пользователи, товары, даты). Это терминология размерного моделирования Кимбалла, marts традиционно строят именно в ней.

Как материализовать слои?

staging и intermediate чаще всего делают view или ephemeral — они лёгкие и не хранят данные лишний раз. marts материализуют как table, потому что по витринам постоянно ходят дашборды и пересчитывать логику на каждый запрос дорого. Тяжёлые marts нередко делают incremental.

Где ставить тесты на качество данных?

И на staging (поймать проблемы источника раньше), и на marts (гарантировать контракт для BI). На staging обычно unique/not_null/relationships, на marts — проверки бизнес-логики и корректности агрегаций.

Это официальная информация?

Нет. Статья основана на best practices dbt Labs и опыте кандидатов. Конкретные соглашения и раскладка слоёв зависят от команды и проекта.


Тренируйте Data Engineering — откройте тренажёр с 1500+ вопросами для собесов.