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 — все downstream-консьюмеры invisible для dbt. Это создаёт organizational blind spot: data team не знает, кто на них полагается.
Типы exposures
dbt поддерживает пять типов:
| Тип | Когда использовать | Пример |
|---|---|---|
dashboard | BI-дашборд: Tableau, Looker, Metabase, Mode, Superset. | ”Executive Revenue Dashboard” |
notebook | Jupyter / Hex notebook, analytical exploration. | ”Customer Cohort Analysis Notebook” |
analysis | One-off аналитический документ (Notion, GDoc). | ”Q4 2025 Retention Deep-Dive” |
ml | ML-модель или 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
Это запустит:
- Все sources, которые читает dashboard (через зависимости моделей).
- Все staging-модели, на которые опираются marts.
- Все intermediate (если есть).
- Финальные marts (
fct_orders,dim_customers,revenue_daily). - Все тесты на этих моделях.
Если что-то падает — 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 | Что значит |
|---|---|
high | Production-критичный. Используется executive team / customer-facing. SLA жёсткий. |
medium | Operational. Используется внутри одной команды. SLA лояльный. |
low | Exploratory. 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_orders | dbt model fct_orders (это model, не exposure) |
| ML training pipeline на features_user | ML feature itself (это model, exposure — это pipeline) |
| Slack бот, ежедневно посылающий revenue | Postgres-таблица, в которую он SELECT (это source/model) |
| Customer-facing API, читающий из mart | API endpoint (это application, EXPOSURE как-раз) |
Exposure — это downstream, outside dbt-проекта. Если ресурс в dbt (model, seed, snapshot) — это не exposure.
Антипаттерны 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.
Ключевые выводы
- Exposure — YAML-декларация downstream-консьюмера данных: дашборд, notebook, ML-модель, приложение.
- Зачем: impact analysis, документация, CI-runs per consumer, ownership.
- Типы:
dashboard,notebook,analysis,ml,application. Это labelling. - Maturity (
high/medium/low) — для прioritization при breaking changes. - owner: name + email — обязательно. Дополнительные fields (slack, team) — custom meta.
- depends_on — список
ref()/source(), которые exposure читает. Появляется в lineage graph. - CI workflow:
dbt build --select +my_exposureзапускает все upstream-модели до consumer’а. - Impact analysis:
dbt list --select model+ --resource-type exposureпоказывает, какие consumers зависят. - Без exposures — downstream invisible, organizational blind spot.