Production-grade descriptions: что писать и как поддерживать живыми
В dbt I мы научились писать description: "..." в YAML и видеть его в dbt docs serve. Но что отличает production-grade documentation от формальной отписки «таблица customer’ов»? Какие descriptions реально помогают аналитикам, а какие — формальная галочка для PR-review?
Этот урок — про производственный tone для descriptions: какую информацию обязательно включать, как поддерживать descriptions живыми (не устаревающими), и как организовать docs review в команде.
Что отличает плохое description от хорошего
Возьмём модель customer_segments в реальном проекте. Junior пишет:
- name: customer_segments
description: "Сегменты клиентов"
Это бесполезно. Аналитик, открывший docs, узнал только то, что и так понятно из названия. На сколько сегментов? По какому правилу? Как часто обновляется? Что грешат, если segment = NULL?
Senior пишет так:
- name: customer_segments
description: |
**Grain**: один клиент (customer_id уникален).
**Бизнес-смысл**: классификация клиентов по lifetime revenue и
активности за последние 12 месяцев. Используется для:
- Email-кампаний (segment = 'VIP' -> высокий бюджет)
- Churn-risk scoring (segment = 'Inactive' -> priority outreach)
- Annual planning (CLV прогноз per segment)
**Правила классификации**:
| Segment | Условие |
|------------|------------------------------------------------------|
| VIP | ltv >= $5000 AND orders_last_12m >= 12 |
| Premium | ltv >= $1000 AND orders_last_12m >= 6 |
| Standard | ltv >= $100 |
| Inactive | ltv < $100 OR orders_last_12m = 0 |
**Обновление**: ежедневно через scheduled job (cron 06:00 UTC).
**Owner**: [email protected] (Slack: #data-help)
**Связанные модели**:
- upstream: `customer_lifetime_value`, `customer_orders_summary`
- downstream: `email_campaigns_targeting`, `churn_predictions`
**История**: добавлено в v1 проекта 2025-09. Сегментация была обсуждена с
маркетингом 2025-08-15 (см. Notion doc «Customer Segmentation Logic v2»).
Это — production-grade. Шесть категорий информации, которые каждый production description должен содержать:
Дополнительно полезно — история (когда добавлено, ссылка на discussion / PR / Notion). Это контекст для будущего читателя: «зачем мы вообще это сделали?»
Описания колонок: тот же стандарт
Колонки тоже заслуживают descriptions. Особенно derived/computed:
- name: customer_segments
columns:
- name: customer_id
description: |
FK на `dim_customers.customer_id`. Уникальный идентификатор клиента
из source-системы (Postgres app DB -> raw.customers -> stg_customers).
data_tests:
- unique
- not_null
- relationships:
to: ref('dim_customers')
field: customer_id
- name: ltv
description: |
Lifetime value: суммарная выручка от клиента за всю историю
(от первого заказа до dbt_valid_to NULL в customers_snapshot).
**Формула**: `SUM(order_total) - SUM(refund_amount)` по всем completed orders.
**Edge case**: если у клиента 0 завершённых заказов — ltv = 0
(через COALESCE, не NULL).
**Тип**: NUMERIC(12, 2), валюта USD.
- name: segment
description: |
Сегмент клиента. Одно из значений: 'VIP', 'Premium', 'Standard', 'Inactive'.
См. полную таблицу правил в description модели.
data_tests:
- not_null
- accepted_values:
values: ['VIP', 'Premium', 'Standard', 'Inactive']
- name: orders_last_12m
description: |
Количество завершённых заказов за последние 12 месяцев от текущей даты.
**Формула**: `COUNT(o.order_id) WHERE o.order_date >= CURRENT_DATE - INTERVAL '12 month' AND o.status = 'completed'`
**Использование**: входит в формулу segment. Отдельно может быть полезно
для активности dashboard.
Что включаем в column description:
- FK назначение (если внешний ключ).
- Формулу (для derived/computed колонок). Иначе аналитик догадывается, что значит revenue.
- Edge cases (что происходит при 0 / NULL / отрицательных).
- Тип данных (если важно для downstream — особенно для DECIMAL precision).
- Валюта/единицы (USD vs EUR, hours vs minutes).
Для тривиальных колонок (first_name TEXT) можно короче, но всё равно явно — «имя клиента из source app.customers».
Naming convention для documentation
Согласованные имена помогают навигации:
| Элемент | Convention | Пример |
|---|---|---|
| Schema YAML | _<context>.yml или _<resource>.yml | _models.yml, _sources.yml |
| Schema YAML per directory | _<dir>__models.yml | _marts__finance__models.yml |
| Sources file | _sources.yml | один на проект или per source |
| Exposures file | _exposures.yml | один на проект |
| Doc blocks | _docs.md или _<topic>__docs.md | _metrics__docs.md |
| Glossary | _glossary.md | повторяющиеся термины |
Этот convention помогает в IDE: открыл папку — сразу видно YAML и docs.
Альтернатива (старый стиль) — один большой schema.yml в корне models/. На малых проектах ок, на проекте 100+ моделей становится непригодным.
Pro tip: для критических моделей делать models/marts/finance/_models.yml рядом с SQL, а для всех тривиальных staging — models/staging/_stg__models.yml один. Гранулярность зависит от reorganization frequency.
Как поддерживать docs живыми
Бизнес-глоссарий — единый источник терминовГлавная проблема: docs устаревают. Кто-то добавил колонку, забыл обновить YAML — описания нет. Кто-то сделал рефакторинг — формулы в description не пересмотрены — теперь врёт.
Лекарства — процесс, не tools.
1. PR-чеклист
В CONTRIBUTING.md или GitHub PR template:
## Documentation checklist
- [ ] Каждая новая модель имеет description (grain, business meaning, owner)
- [ ] Каждая новая колонка имеет description (формула если derived, edge cases)
- [ ] Обновлены descriptions для изменённых колонок
- [ ] Если новая FK — добавлен `relationships` test
- [ ] Если новый сегмент/категория — обновлён `accepted_values` test
- [ ] Если новый mart — добавлен exposure (если consumer existed)
Reviewer проверяет каждый пункт. Без чеклиста — фокус только на SQL, docs дрейфуют.
2. CI gate: dbt docs generate без warning
В CI добавь job, который проверяет:
- name: Check docs completeness
run: |
dbt docs generate --target ci
python scripts/check_descriptions.py
Скрипт check_descriptions.py парсит target/manifest.json и фейлит, если:
- Модель в
models/marts/без description. - Колонка
customer_id/customer_email/revenue(бизнес-критичные) без description. - Колонка с типом DECIMAL/NUMERIC без указания precision.
# scripts/check_descriptions.py (упрощённо)
import json, sys
with open('target/manifest.json') as f:
manifest = json.load(f)
errors = []
for node_id, node in manifest['nodes'].items():
if node['resource_type'] == 'model' and node['fqn'][1] == 'marts':
if not node.get('description'):
errors.append(f"Mart model {node['name']} missing description")
for col_name, col in node['columns'].items():
if not col.get('description'):
errors.append(f"Column {node['name']}.{col_name} missing description")
if errors:
print('\n'.join(errors))
sys.exit(1)
Это strict gate: PR без описания не мержится.
3. Glossary через doc blocks
Если у тебя есть термины, повторяющиеся в 5+ моделях (например, revenue, customer_id, churn), оторви их в glossary через doc blocks. Об этом — следующий урок.
Это убирает дублирование: один источник правды для определения revenue, все модели ссылаются. Изменил один раз — обновилось везде.
4. Документация как часть code review
Не разделять «PR с кодом» и «PR с doc». Каждый код-PR содержит docs. Иначе docs накапливаются как технический долг и никогда не пишутся.
Senior должен отказывать в merge при отсутствии docs на новой модели. Это болезненно поначалу, но устанавливает культуру.
Документация для legacy-проекта
Что делать, если ты пришёл в проект с 200 моделей и 80% без descriptions?
Шаг 1 — Аудит:
# Список моделей без descriptions
dbt parse
python -c "
import json
m = json.load(open('target/manifest.json'))
no_desc = [n['name'] for n in m['nodes'].values()
if n['resource_type'] == 'model' and not n.get('description')]
print(f'{len(no_desc)} models without description')
for name in no_desc[:20]:
print(f' - {name}')
"
Шаг 2 — Приоритизация:
- Marts (consumer-facing, аналитики). Сначала.
- Staging (важно для onboarding новых dev’ов).
- Intermediate (часто эфемерные, можно последними).
Шаг 3 — Написание:
- Не делать за неделю. Цель: 80% покрытия за квартал.
- Каждый sprint — 5-10 моделей.
- Привлечь knowledgeable analysts для проверки бизнес-смысла (они знают лучше).
Шаг 4 — CI gate:
- После 80% coverage — включить strict gate.
- Без description — PR не мержится.
- Это фиксирует coverage от регрессий.
Шаг 5 — Maintain:
- Каждый PR обновляет docs.
- Quarterly review: проверить, что formulas в description не разошлись с SQL.
- Glossary через doc blocks (следующий урок).
Антипаттерны descriptions
-
Description дублирует имя:
name: customer_id, description: "ID клиента". Полезной информации ноль. Лучше:description: "FK на dim_customers, originated from raw.customers.id". -
Description устарел: модель пересмотрена, но описание не обновлено. Хуже отсутствия — врёт. Решение: PR-чеклист + quarterly review.
-
Description содержит SQL:
description: "SELECT ... FROM ...". Логика SQL и так видна в.sqlфайле, dublication не нужно. В description — бизнес-описание, не SQL. -
Description “TODO”:
description: "TODO add description". Не лучше, чем пусто, но создаёт false sense of progress. CI gate должен это ловить (grep TODO). -
Слишком длинно: 1000 слов в description. Никто не читает. Лимит: 100-300 слов на model description. Если нужно больше — оторви в
_<model>__docs.mdчерез doc block. -
Markdown vs plain:
description: "**Grain**: ..."рендерится в dbt docs как HTML. Используй markdown — таблицы, lists, bold. Plain text — нечитаемо. -
No owner: при сломанных данных аналитик не знает, кому писать. Owner обязательно — почта, Slack channel, или GitHub team.
Попробуй сам
Открой свой dbt-проект. Возьми критическую mart-модель (revenue / churn / customer_metrics). Сейчас description, вероятно, такой:
- name: customer_metrics
description: "Метрики клиента"
Перепиши его, используя 6 категорий:
- Grain:
один клиент (customer_id уникален) - Бизнес-смысл:
KPI для customer success: LTV, days_since_last_order, NPS - Формулы: таблица с расчётом каждой метрики
- Обновление:
daily 06:00 UTC через Airflow DAGcustomer_metrics_daily“ - Owner:
data-team@..., Slack #data-help - Связи: upstream / downstream
Покажи команде. Сравните old vs new. Это production-grade documentation.
Ключевые выводы
- Production description содержит 6 категорий: grain, бизнес-смысл, формулы/правила, обновление, owner, связи. Минимум — 5 из 6.
- Описания колонок обязательны для derived/computed/FK/categorical. Включают формулу, edge cases, валюту/единицы.
- Naming convention:
_models.yml,_sources.yml,_docs.md. Один schema per directory для больших проектов. - Поддержка живыми: PR-чеклист, CI gate (грэп по manifest), code review с фокусом на docs, quarterly stale review.
- Legacy проект: audit -> prioritize (marts first) -> write 5-10/sprint -> CI gate after 80% coverage -> maintain.
- Антипаттерны: дублирование имени, устаревший, SQL вместо бизнеса, TODO без content, no owner, plain text вместо markdown.
- Description — часть кода. Без него PR не мержится. Это культура, не tooling.