Learning Platform
Глоссарий Troubleshooting
Урок 16.04 · 20 мин
Начальный
DocumentationExposuresLineageDashboardMLImpact analysis

Exposures: декларация downstream-консьюмеров

DAG-граф в dbt отлично показывает, что внутри проекта: source -> staging -> intermediate -> marts. Но что после marts? Какие дашборды читают fct_orders? Какие ML-модели зависят от customer_metrics? Какие реактивные приложения dump-ят revenue_daily?

Без формальной декларации эти зависимости — в голове у людей. Когда вы делаете breaking change в fct_orders — нет способа узнать, какие 15 дашбордов сломаются. Аналитики обнаруживают это через дни, когда дашборды показывают пустоту.

Exposures — это механизм dbt декларировать downstream-консьюмеров: дашборды, ML-модели, приложения. Они появляются в DAG как узлы за marts, и dbt build --select +my_dashboard запускает все upstream-зависимости.


Что такое exposure

Exposure — это YAML-декларация конкретного консьюмера данных:

version: 2

exposures:
  - name: monthly_revenue_dashboard
    type: dashboard
    maturity: high
    description: "Executive dashboard с месячной выручкой по сегментам"
    url: https://tableau.company.com/dashboards/monthly-revenue
    owner:
      name: Alice Johnson
      email: [email protected]
    depends_on:
      - ref('fct_orders')
      - ref('dim_customers')
      - ref('revenue_daily')

Что декларируется:

ПолеЗначение
nameУникальное имя exposure’а в проекте.
typeТип консьюмера: dashboard, notebook, analysis, ml, application.
maturityЗрелость: high, medium, low. Для prioritization в impact analysis.
descriptionЧто это, для кого, где живёт.
urlСсылка на actual ресурс (Tableau / Looker / Notion / GitHub).
ownerКто отвечает (для эскалаций «сломалось»).
depends_onСписок моделей / sources, которые этот exposure читает.

После запуска dbt parse exposure появляется в манифесте, в lineage graph, и доступен через node selection (dbt run --select +monthly_revenue_dashboard).


Зачем декларировать exposures

Зачем нужны exposures

Без exposures — все downstream-консьюмеры invisible для dbt. Это создаёт organizational blind spot: data team не знает, кто на них полагается.


Типы exposures

dbt поддерживает пять типов:

ТипКогда использоватьПример
dashboardBI-дашборд: Tableau, Looker, Metabase, Mode, Superset.”Executive Revenue Dashboard”
notebookJupyter / Hex notebook, analytical exploration.”Customer Cohort Analysis Notebook”
analysisOne-off аналитический документ (Notion, GDoc).”Q4 2025 Retention Deep-Dive”
mlML-модель или ML feature store.”Churn Prediction Model v3”
applicationЛюбое приложение или сервис, читающее данные из warehouse.”Internal Admin Tool”, “Customer Email Pipeline”

Это labelling для UI и для filtering: dbt list --select exposure:* --resource-type exposure покажет всё. Логика обработки одинаковая для всех типов.


Полный пример: monthly_revenue dashboard

# models/_exposures.yml
version: 2

exposures:
  - name: monthly_revenue_dashboard
    type: dashboard
    maturity: high
    description: |
      **Executive Revenue Dashboard** — месячная выручка по сегментам и регионам.

      Аудитория: VP Sales, CFO, CEO. Открывается на еженедельном meeting.

      Логика:
      - Total monthly revenue trend (12 месяцев).
      - Revenue by customer segment (VIP, Premium, Standard).
      - Revenue by region (Americas, EMEA, APAC).
      - Growth rate YoY.

      Refreshed: ежечасно.
    url: https://tableau.company.com/dashboards/exec-revenue-monthly

    owner:
      name: Alice Johnson
      email: [email protected]

    depends_on:
      - ref('fct_orders')
      - ref('dim_customers')
      - ref('revenue_daily')
      - source('jaffle', 'customers')   # экспозижн может зависеть от source напрямую (редко)

    tags: ['exec', 'finance']
    meta:
      sla_hours: 1                       # custom metadata
      data_freshness_required: hourly

После dbt parse это сохраняется в манифест. В dbt docs serve появляется узел monthly_revenue_dashboard в lineage graph с ссылкой на Tableau.


Lineage с exposures

В dbt docs lineage теперь полный:

source(jaffle.customers)

stg_jaffle__customers

dim_customers ──────────┐

source(jaffle.orders) -> stg_jaffle__orders -> fct_orders ─┬-> revenue_daily ─┐
                                                          │                  ↓
                                                          └──────────────-> [exposure: monthly_revenue_dashboard]

Видно весь поток: от source через staging и marts до конкретного дашборда. Аналитик / data engineer сразу видит структуру.


Использование в CI: +exposure

Самая полезная фича — селектор node selection:

dbt build --select +monthly_revenue_dashboard

Это запустит:

  1. Все sources, которые читает dashboard (через зависимости моделей).
  2. Все staging-модели, на которые опираются marts.
  3. Все intermediate (если есть).
  4. Финальные marts (fct_orders, dim_customers, revenue_daily).
  5. Все тесты на этих моделях.

Если что-то падает — dbt build падает. Дашборд не получит обновлённых данных. Это правильно — лучше остановить чем сломать.

Аналогично:

dbt build --select +exposure:monthly_revenue_dashboard    # full syntax
dbt build --select exposure:monthly_revenue_dashboard+    # сам exposure + downstream (но у exposure нет downstream)
dbt run --select +tag:exec                                 # все модели для exec-tagged exposures

В scheduled CI типично делают per-exposure runs:

# В cron / Airflow DAG / dbt Cloud Job:
dbt build --select +monthly_revenue_dashboard
dbt build --select +customer_retention_dashboard
dbt build --select +churn_prediction_model

Каждый run обновляет только данные, нужные для этого консьюмера. Не пересчитываем 200 моделей, если дашборд читает 5.


Impact analysis: что сломается

dbt list --select fct_orders+ --resource-type exposure

Покажет все exposures, зависящие от fct_orders (через любую длину пути).

Это идеальный pre-PR check:

«Я меняю fct_orders. Какие дашборды могут сломаться?»

dbt отвечает: «monthly_revenue_dashboard, customer_cohort_analysis, churn_model_v3». Идёте к их owner’ам, предупреждаете, делаете breaking change координировано.

Без exposures — этой команды нет. Вы сделали изменение, через неделю VP Sales увидел пустой дашборд, написал в Slack «у нас всё сломалось», вы разбираете полдня.


Maturity: high / medium / low

Поле maturity — это labelling приоритета. dbt не использует его автоматически, но это metadata для team-процессов:

MaturityЧто значит
highProduction-критичный. Используется executive team / customer-facing. SLA жёсткий.
mediumOperational. Используется внутри одной команды. SLA лояльный.
lowExploratory. One-off analysis, опытная notebook. Может ломаться без drama.

В команде договариваются: «PR, которые ломают high-exposures, требуют RFC + owner sign-off. low — fix forward».


owner: контакты и accountability

Поле owner — это кто отвечает, если exposure ломается:

owner:
  name: Alice Johnson
  email: [email protected]

В dbt docs UI это рендерится с mailto: ссылкой. Если что-то ломается — pipeline failure нотификации могут авто-ссылаться на owner.

Расширения (custom meta):

owner:
  name: Alice Johnson
  email: [email protected]
  slack: alice.j
  team: finance-analytics

Эти fields — custom; dbt их сохранит в манифест, но интерпретирует только name и email. Остальное — для ваших скриптов.


Где живут exposures в проекте

Стандартный путь — models/ со специальным YAML файлом:

models/
  _exposures.yml             ← все exposures в проекте
  staging/
    ...
  marts/
    ...

В больших проектах разбивают по доменам:

models/
  marts/
    finance/
      _finance__exposures.yml    ← finance dashboards и ML
      revenue_daily.sql
    marketing/
      _marketing__exposures.yml
      campaign_attribution.sql

Что удобнее — зависит от команды. В курсе — один _exposures.yml в models/.


Что НЕ есть exposure

Чтобы не путать:

Это exposureЭто НЕ exposure
Tableau dashboard, читающий fct_ordersdbt model fct_orders (это model, не exposure)
ML training pipeline на features_userML feature itself (это model, exposure — это pipeline)
Slack бот, ежедневно посылающий revenuePostgres-таблица, в которую он SELECT (это source/model)
Customer-facing API, читающий из martAPI endpoint (это application, EXPOSURE как-раз)

Exposure — это downstream, outside dbt-проекта. Если ресурс в dbt (model, seed, snapshot) — это не exposure.


Антипаттерны exposures

Антипаттерны exposures

Попробуй сам

В вашем dbt-проекте создайте models/_exposures.yml:

version: 2

exposures:
  - name: jaffle_overview_dashboard
    type: dashboard
    maturity: high
    description: "Главный дашборд Jaffle Shop с метриками заказов и клиентов"
    url: https://example.com/dashboards/jaffle-overview
    owner:
      name: Your Name
      email: [email protected]
    depends_on:
      - ref('fct_orders')
      - ref('dim_customers')

(Конечно fct_orders и dim_customers должны существовать.)

Запустите:

dbt parse
dbt docs generate
dbt docs serve

В UI откройте Lineage Graph. Найдите jaffle_overview_dashboard — увидите узел розового цвета.

Используйте селекторы:

dbt list --select +jaffle_overview_dashboard        # все upstream + сам exposure
dbt list --select fct_orders+ --resource-type exposure  # exposures зависящие от fct_orders
dbt build --select +jaffle_overview_dashboard       # запустит все upstream

Бонус: добавьте ещё 2 exposures (например, notebook тип для analytical notebook и ml тип для prediction model). Проверьте lineage — теперь видно полную картину downstream.


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

  1. Exposure — YAML-декларация downstream-консьюмера данных: дашборд, notebook, ML-модель, приложение.
  2. Зачем: impact analysis, документация, CI-runs per consumer, ownership.
  3. Типы: dashboard, notebook, analysis, ml, application. Это labelling.
  4. Maturity (high/medium/low) — для прioritization при breaking changes.
  5. owner: name + email — обязательно. Дополнительные fields (slack, team) — custom meta.
  6. depends_on — список ref() / source(), которые exposure читает. Появляется в lineage graph.
  7. CI workflow: dbt build --select +my_exposure запускает все upstream-модели до consumer’а.
  8. Impact analysis: dbt list --select model+ --resource-type exposure показывает, какие consumers зависят.
  9. Без exposures — downstream invisible, organizational blind spot.
Exposures как контракт с downstream-консьюмерами
Проверка знанийKnowledge check
Команда не использует exposures. PM просит изменить колонку amount -> amount_usd в fct_orders. Как понять, что сломается?
ОтветAnswer
Без exposures — **только догадки и расследование**:\n\n1. **Grep по репозиториям** BI-инструментов на amount, fct_orders. Найдёте hardcoded SQL queries в Tableau/Looker datasources.\n2. **Логи warehouse** на SELECT с amount FROM fct_orders за последние 30 дней. Кто запрашивает?\n3. **Slack threads** на 6 месяцев назад — кто упоминал fct_orders?\n4. **Спрос у тимлидов**: «Чьи дашборды/Notebooks читают fct_orders?». Они напомнят про 3-4 из 10.\n\nЭто **часы работы, и всё равно пропустите половину**. После change через пару дней приходят люди: «у нас всё сломалось».\n\nКак исправить процесс:\n\n1. **Сделать exposure inventory.** Опросить команды, собрать список их dashboards/notebooks/ML, читающих dbt-models.\n2. **Задекларировать все** в `models/_exposures.yml` с depends_on.\n3. **Запустить `dbt parse && dbt docs generate`** — увидеть полную картину.\n4. **С этой минуты**: `dbt list --select fct_orders+ --resource-type exposure` мгновенно отвечает на вопрос «что сломается». Все changes — координируются с owner'ами.\n\nЭто инвестиция на день-два, окупается каждым breaking change в будущем.
Проверка знанийKnowledge check
exposure `revenue_dashboard` зависит от `fct_orders`. Команда запустила `dbt build --select +revenue_dashboard`. Один из тестов на `stg_jaffle__orders` упал. Что произойдёт?
ОтветAnswer
`dbt build` останавливается на упавшем тесте. Это **правильное поведение** — лучше не дать дашборду обновиться с грязными данными, чем показывать пользователям неверную информацию.\n\nЧто конкретно:\n\n1. dbt запускает по DAG: source -> stg_jaffle__orders -> `fct_orders` -> exposure (но exposure — это только marker, не выполняемый узел).\n2. После материализации `stg_jaffle__orders` запускаются тесты на ней (`not_null`, `unique`).\n3. Тест упал — `dbt build` помечает узел как ERROR.\n4. **Downstream-узлы (fct_orders) пропускаются** — статус SKIPPED. Они не материализуются.\n5. Process exits с non-zero exit code -> CI fails -> alert.\n\nЧто делать:\n\n1. **Investigate**: какой тест упал, какое значение в данных нарушает.\n2. **Fix at source** (если можно): backend выправляет данные.\n3. **Soft warning** (если known issue): `severity: warn` на тест — это даст warning, но build продолжится.\n4. **`dbt build --select +revenue_dashboard --warn-error`** в production: жёсткое поведение, ничего не дойдёт до consumer'а пока не fix.\n\nЭто **fail-fast** model: пускай дашборд показывает вчерашние данные, а не сегодняшние неверные.

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

Результат: 0 из 0
Аналитический
Вопрос 1 из 5. PM просит изменить колонку amount -> amount_usd в fct_orders. Без exposures как понять, что сломается?

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

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

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

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