YAML-организация в большом проекте
В dbt I вы клали все описания моделей в один файл schema.yml рядом с моделями. Это работает до 10-15 моделей. На 100+ моделях такой подход разваливается: один YAML на 5000 строк превращается в merge-conflict зону, никто не находит нужное описание за разумное время, IDE начинает тормозить на больших файлах.
Production-подход — дробление YAML по domain, с префиксом _ и подчинением структуре каталогов models. В этом уроке разберём четыре типа YAML-файлов и как их организовать.
Четыре типа YAML
В dbt-проекте middle-уровня есть четыре основных типа YAML-описаний:
| Файл | Содержит | Где живёт |
|---|---|---|
_<source>__sources.yml | sources, freshness, source-tests | в staging/<source>/ |
_<group>__models.yml | model descriptions, columns, tests, contracts | в каждой папке models/ |
_<domain>__exposures.yml | exposures (downstream consumers) | в marts/<domain>/ |
_macros.yml | macros descriptions, args, return | в macros/ |
Все они обязательно начинаются с подчёркивания и имеют префикс группы — это конвенция dbt Labs из 2024 best practices.
_sources.yml — описание source-таблиц
_<source>__sources.yml живёт в models/staging/<source>/ рядом со staging-моделями того же источника. Один файл на один источник.
Пример models/staging/stripe/_stripe__sources.yml:
version: 2
sources:
- name: stripe
description: "Raw данные из Stripe API через Fivetran"
database: raw
schema: stripe
loader: fivetran
freshness:
warn_after: {count: 12, period: hour}
error_after: {count: 24, period: hour}
loaded_at_field: _fivetran_synced
tables:
- name: charges
description: "Все транзакции (charges) в Stripe"
columns:
- name: id
description: "Stripe charge ID"
tests:
- unique
- not_null
- name: customer
description: "FK -> stripe.customers.id"
tests:
- not_null
- name: amount
description: "Сумма в центах. Делим на 100 для долларов в staging"
loaded_at_field: created
- name: customers
description: "Customers в Stripe"
columns:
- name: id
tests:
- unique
- not_null
Несколько важных моментов:
- freshness на уровне source — общий threshold для всех таблиц источника. Можно override на уровне
tables[].freshnessдля специфической таблицы. - loaded_at_field — колонка, по которой dbt определяет «свежесть» источника. Fivetran кладёт
_fivetran_synced, Airbyte —_airbyte_extracted_at, custom Python jobs — обычноcreated_at. - tests на source-колонках — проверка primary keys и NOT NULL прямо на source, до staging. Это спасает от багов синхронизации (Fivetran иногда дублирует строки).
- description обязательно — это первое, что увидит downstream-команда через
dbt docs. Пустые description — техдолг.
_models.yml — описание моделей
_<group>__models.yml — основной YAML, описывающий staging/intermediate/marts-модели. Один файл на одну папку (group).
Пример models/marts/finance/_finance__models.yml:
version: 2
models:
- name: fct_orders
description: |
Фактовая таблица заказов. Один row = один заказ.
Источник: orders из application database через Fivetran,
enriched с payments из Stripe.
config:
materialized: incremental
unique_key: order_id
tags: [finance, critical]
columns:
- name: order_id
description: "Primary key. UUID v4 формат."
data_tests:
- unique
- not_null
- name: customer_id
description: "FK -> dim_customers.customer_id"
data_tests:
- not_null
- relationships:
to: ref('dim_customers')
field: customer_id
- name: order_total_usd
description: "Общая сумма заказа в USD. Включает налог и доставку."
data_tests:
- not_null
- dbt_expectations.expect_column_values_to_be_between:
min_value: 0
max_value: 1000000
- name: order_status
description: "Статус заказа: pending, paid, shipped, delivered, cancelled"
data_tests:
- accepted_values:
values: [pending, paid, shipped, delivered, cancelled]
- name: created_at
description: "Когда заказ был создан"
data_tests:
- not_null
- name: dim_customers
description: "Customers dimension с актуальным состоянием"
config:
materialized: table
tags: [finance]
columns:
- name: customer_id
description: "Primary key. UUID v4."
data_tests:
- unique
- not_null
Несколько ключевых моментов:
-
config внутри YAML — конфигурация модели может жить как внутри
_models.yml(config:блок), так и в SQL-файле ({{ config() }}блок). Production-tone: для table/incremental/материализаций и tags — в YAML, для специфичных-Jinja вещей — в SQL. -
data_testsвместоtests— в dbt 1.8+ ключ называетсяdata_tests. Старыйtestsещё работает, но deprecated. Используйтеdata_testsв новых проектах. -
dbt-expectations — пакет с расширенным набором тестов (мы его подробно разберём в модуле 07). Здесь показано как добавлять third-party тест в YAML.
-
description обязательно на каждой модели и каждой ключевой колонке. На второстепенных колонках (
updated_at,_dbt_loaded_at) можно опускать, но primary keys, foreign keys, business-critical поля — обязательно.
_exposures.yml — downstream consumers
Data Lineage и Impact Analysis в governanceexposure — это публичный контракт между dbt-проектом и downstream-системой (BI dashboard, ML pipeline, reverse-ETL). Это новый концепт middle-уровня, в dbt I мы его не касались.
Пример models/marts/finance/_finance__exposures.yml:
version: 2
exposures:
- name: finance_revenue_dashboard
label: "Finance Revenue Dashboard"
type: dashboard
maturity: high
url: https://looker.company.com/dashboards/123
description: |
Главный dashboard финансовой команды.
Показывает MRR, ARR, churn по месяцам.
depends_on:
- ref('fct_orders')
- ref('fct_invoices')
- ref('dim_customers')
owner:
name: Finance Team
email: [email protected]
- name: revenue_ml_model
type: ml
maturity: medium
description: |
ML-модель для предсказания churn'а.
Тренируется ежедневно на fct_user_sessions.
depends_on:
- ref('fct_user_sessions')
- ref('dim_customers')
owner:
name: Data Science Team
email: [email protected]
Зачем нужны exposures:
-
DAG lineage в дашбордах dbt docs — на странице модели вы видите, какие BI-дашборды от неё зависят. Это критично для impact analysis: «если я переименую колонку, что сломается?».
-
state:modified+ через CI — Slim CI (модуль 13) выполняет только модели, которые поменялись + их downstream. Если exposure ссылается на модель, она автоматически попадает в downstream set, и CI тестирует её при изменении upstream.
-
dbt source freshness — exposures позволяют запустить
dbt source freshness --select +exposure:finance_revenue_dashboardи проверить, что все upstream sources свежие для конкретного дашборда. -
maturityиowner— документация для команды: что high maturity (надёжный, мониторится), что low (experimental, не критично).
В небольшой команде exposures кажутся overhead’ом. В большой — это единственный способ понять, что сломается при изменении модели. На 200 моделях без exposures каждый rename — лотерея.
_macros.yml — описание macros
dbt-i: первое знакомство с exposuresMacros — это переиспользуемые куски Jinja-кода (мы их подробно разберём в модуле 05). Их тоже нужно документировать.
Пример macros/_macros.yml:
version: 2
macros:
- name: cents_to_dollars
description: |
Делит integer-сумму в центах на 100, возвращает decimal в долларах.
Использовать в staging-моделях для денежных колонок из Stripe.
arguments:
- name: column_name
type: string
description: "Имя колонки с суммой в центах"
- name: decimal_places
type: integer
description: "Количество знаков после запятой. Default 2."
- name: get_tenant_id
description: |
Возвращает tenant_id для текущего target.
В dev возвращает test_tenant, в prod — берёт из env_var.
arguments: []
Macro descriptions редко становятся проблемой — но если у вас 30+ macro, без YAML никто не помнит, что какой macro делает и какие у него параметры.
Production gotchas
1. Один большой YAML на 5000 строк
Самая частая ошибка — оставить «один schema.yml на весь проект», как было в dbt I. На 100+ моделях это превращается в:
- Merge conflicts на каждом PR — два человека меняют разные модели, но один файл.
- IDE тормозит — открыть и найти модель занимает 30 секунд.
- Невозможно code review — diff на 200 строк изменений в одном YAML нечитаем.
Решение — дробить YAML по той же структуре, что и SQL-модели: один _<group>__models.yml на каждую папку. Если в папке 20 моделей — 20 моделей в одном YAML, это нормально. Если в папке 50 — пора дробить папку.
2. YAML и SQL живут раздельно
dbt не следит за тем, что каждая SQL-модель имеет запись в YAML. Можно создать fct_orders.sql, забыть про YAML, и dbt run запустится без ошибок. Это техдолг.
Решение — пакет dbt_project_evaluator (следующий урок) — проверяет, что каждая модель имеет description.
3. Тесты в YAML vs тесты в SQL
dbt поддерживает два места для тестов:
- YAML — generic tests (
unique,not_null, custom generic) в_models.yml. - SQL — singular tests в
tests/*.sql.
В YAML живут тесты-проверки колонок. В SQL — тесты-проверки business invariants (например, «sum(revenue) per day strictly positive»).
На production-уровне обычно ~80% тестов — generic в YAML, ~20% — singular в tests/. Это здоровый баланс.
4. config дублируется в YAML и SQL
Если вы пишете и в {{ config() }} блоке SQL, и в _models.yml, YAML побеждает для большинства параметров (но не для всех — read docs!). Это источник тонких багов: меняете config в SQL, ничего не меняется, потому что YAML override.
Решение — выберите одно место для конфигурации и держитесь его. Production-обычай: базовая конфигурация (materialized, unique_key, tags) — в YAML, сложная Jinja-логика (partition_by с {{ }}-условиями) — в SQL.
DuckDB-замечания
- DuckDB поддерживает все четыре типа YAML без специфики.
- Source
external_location(Parquet/CSV на S3) — это DuckDB-only расширение_sources.yml, на Snowflake/BQ не работает. - Тесты
unique/not_nullна DuckDB работают, ноrelationshipsс FK на MotherDuck не работает (FK constraints не enforced).
Попробуй сам
В своём проекте:
- Раздробите
schema.yml(если он один на весь проект) на_<group>__models.ymlв каждой папке. - Создайте
_<source>__sources.ymlдля каждого источника вstaging/<source>/. - Опишите 2-3 главные mart-модели с полным description, columns, data_tests.
- Создайте
_<domain>__exposures.ymlхотя бы для одного дашборда / ML pipeline / reverse-ETL job. - Запустите
dbt parseиdbt build— убедитесь, что всё разбирается.