Learning Platform
Глоссарий Troubleshooting
Урок 02.03 · 22 мин
Средний
YAML_models.yml_sources.yml_exposures.ymlconfiguration

YAML-организация в большом проекте

В dbt I вы клали все описания моделей в один файл schema.yml рядом с моделями. Это работает до 10-15 моделей. На 100+ моделях такой подход разваливается: один YAML на 5000 строк превращается в merge-conflict зону, никто не находит нужное описание за разумное время, IDE начинает тормозить на больших файлах.

Production-подход — дробление YAML по domain, с префиксом _ и подчинением структуре каталогов models. В этом уроке разберём четыре типа YAML-файлов и как их организовать.

Четыре типа YAML

В dbt-проекте middle-уровня есть четыре основных типа YAML-описаний:

ФайлСодержитГде живёт
_<source>__sources.ymlsources, freshness, source-testsв staging/<source>/
_<group>__models.ymlmodel descriptions, columns, tests, contractsв каждой папке models/
_<domain>__exposures.ymlexposures (downstream consumers)в marts/<domain>/
_macros.ymlmacros 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

Несколько важных моментов:

  1. freshness на уровне source — общий threshold для всех таблиц источника. Можно override на уровне tables[].freshness для специфической таблицы.
  2. loaded_at_field — колонка, по которой dbt определяет «свежесть» источника. Fivetran кладёт _fivetran_synced, Airbyte — _airbyte_extracted_at, custom Python jobs — обычно created_at.
  3. tests на source-колонках — проверка primary keys и NOT NULL прямо на source, до staging. Это спасает от багов синхронизации (Fivetran иногда дублирует строки).
  4. 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

Несколько ключевых моментов:

  1. config внутри YAML — конфигурация модели может жить как внутри _models.yml (config: блок), так и в SQL-файле ({{ config() }} блок). Production-tone: для table/incremental/материализаций и tags — в YAML, для специфичных-Jinja вещей — в SQL.

  2. data_tests вместо tests — в dbt 1.8+ ключ называется data_tests. Старый tests ещё работает, но deprecated. Используйте data_tests в новых проектах.

  3. dbt-expectations — пакет с расширенным набором тестов (мы его подробно разберём в модуле 07). Здесь показано как добавлять third-party тест в YAML.

  4. description обязательно на каждой модели и каждой ключевой колонке. На второстепенных колонках (updated_at, _dbt_loaded_at) можно опускать, но primary keys, foreign keys, business-critical поля — обязательно.

_exposures.yml — downstream consumers

Data Lineage и Impact Analysis в governance

exposure — это публичный контракт между 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:

  1. DAG lineage в дашбордах dbt docs — на странице модели вы видите, какие BI-дашборды от неё зависят. Это критично для impact analysis: «если я переименую колонку, что сломается?».

  2. state:modified+ через CI — Slim CI (модуль 13) выполняет только модели, которые поменялись + их downstream. Если exposure ссылается на модель, она автоматически попадает в downstream set, и CI тестирует её при изменении upstream.

  3. dbt source freshness — exposures позволяют запустить dbt source freshness --select +exposure:finance_revenue_dashboard и проверить, что все upstream sources свежие для конкретного дашборда.

  4. maturity и owner — документация для команды: что high maturity (надёжный, мониторится), что low (experimental, не критично).

TIP

В небольшой команде exposures кажутся overhead’ом. В большой — это единственный способ понять, что сломается при изменении модели. На 200 моделях без exposures каждый rename — лотерея.

_macros.yml — описание macros

dbt-i: первое знакомство с exposures

Macros — это переиспользуемые куски 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).

Попробуй сам

В своём проекте:

  1. Раздробите schema.yml (если он один на весь проект) на _<group>__models.yml в каждой папке.
  2. Создайте _<source>__sources.yml для каждого источника в staging/<source>/.
  3. Опишите 2-3 главные mart-модели с полным description, columns, data_tests.
  4. Создайте _<domain>__exposures.yml хотя бы для одного дашборда / ML pipeline / reverse-ETL job.
  5. Запустите dbt parse и dbt build — убедитесь, что всё разбирается.
Проверка знанийKnowledge check
Команда новая — 3 человека, 30 моделей. Senior предлагает один schema.yml на весь проект ('нам же мало моделей, не усложняйте'). Какие три аргумента в пользу дробления уже сейчас, а не 'потом, когда станет много'?
ОтветAnswer
Первое — merge conflicts начинаются на 3 человеках, не на 30. Когда два человека одновременно меняют разные модели в одном YAML, git mergeline получает конфликт. На 5 PR в день в команде из 3 человек это уже регулярная боль, не одноразовая. Второе — рефакторинг 'потом' дороже, чем правильная структура сейчас. Раздробить один schema.yml на 5 файлов когда там уже 150 моделей с тестами, relationships, descriptions — это 4 часа работы и риск ошибки. Сделать правильно с самого начала — 0 дополнительных часов. Третье — структура models/ и структура YAML должны зеркалить друг друга. Если SQL разбит по доменам (finance/marketing/product), а YAML один — нарушается ментальная модель: 'я ищу описание fct_orders, оно где-то в большом YAML'. С зеркальной структурой — 'оно в _finance__models.yml рядом с fct_orders.sql', находится за 2 секунды. Это не масштабирование, это базовая гигиена проекта, которую дешевле заложить сразу. Senior, скорее всего, не работал в dbt-команде из 5+ человек, иначе аргумент про conflicts он бы знал.

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

Результат: 0 из 0
Прикладной
Вопрос 1 из 6. Команда из 5 человек ведёт dbt-проект с одним schema.yml на 200+ моделей. Какая проблема становится главной?

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

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

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

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