Learning Platform
Глоссарий Troubleshooting
Урок 10.02 · 22 мин
Средний
DocumentationDoc blocksGlossaryDRYYAML quoting

Doc blocks: переиспользование descriptions через doc(‘name’)

В прошлом уроке мы написали production-grade description для одной модели. Но в реальных проектах одни и те же термины встречаются десятки раз: customer_id, revenue, churn, lifetime_value. Каждый раз дублировать описание — recipe для расхождений. Через 6 месяцев customer_id в одной модели описан как «PK от raw.customers», в другой — «уникальный идентификатор клиента», в третьей — TODO.

Решение — doc blocks: переиспользуемые блоки документации в .md файлах + {{ doc('name') }} из YAML. Junior-уровень синтаксис уже разбирался в dbt-i; здесь — про CI gates, cross-project Mesh, {% raw %} escaping для метасимволов, и orphaned doc detection.

Business Glossary: governance-основа для doc blocks
NOTE

Базовый синтаксис {% docs name %}...{% enddocs %} + ссылка {{ doc('name') }} и YAML quoting trap (внешние ≠ внутренним) разбирались в dbt-i/14/02. Здесь — middle: CI gate check_doc_blocks.py, cross-project, escaping, orphaned detection.


Recap в одном примере

{# models/_glossary.md #}
{% docs revenue %}
**Revenue** — SUM(order_total) FILTER (status = 'completed'). NUMERIC(12, 2). USD.
Refunds не вычитаются — см. `net_revenue`.
{% enddocs %}
# models/_models.yml
- name: customer_metrics
  columns:
    - name: revenue
      description: '{{ doc("revenue") }}'  # внешние ≠ внутренним кавычкам

YAML quoting: '{{ doc("name") }}' или "{{ doc('name') }}" или block scalar |. Без кавычек YAML парсит {{ как Flow-map start и падает.


Где лежат doc blocks

dbt по умолчанию ищет doc blocks в:

# dbt_project.yml
docs-paths: ["docs"]

или (более распространённое) — в самих models/ рядом с моделями:

# dbt_project.yml
# по умолчанию dbt видит docs во всех source-paths
# поэтому .md файлы в models/ автоматически подхватываются

Типичная организация:

models/
  _glossary.md              ← общие термины
  marts/
    _marts__docs.md         ← marts-specific docs
    finance/
      _finance__docs.md     ← finance-specific docs
      revenue_metrics.sql
      _finance__models.yml

Convention: _<context>__docs.md или _glossary.md. Префикс _ группирует все YAML/MD в IDE.


Полный пример glossary

models/_glossary.md:

{% docs customer_id %}
**customer_id** — primary key dimension `dim_customers`. Originated from
`raw.customers.id` (Postgres app DB). Уникальный для каждого клиента.

Используется как FK в:
- `fct_orders.customer_id`
- `customer_metrics.customer_id`
- `customer_segments.customer_id`

**Тип**: BIGINT. **Constraints**: PK, NOT NULL.
{% enddocs %}

{% docs revenue %}
**Revenue** — суммарная сумма заказов клиента (status = 'completed') за указанный
период. Формула: `SUM(order_total) FILTER (WHERE status = 'completed')`.

**Валюта**: USD. **Тип**: NUMERIC(12, 2). **Edge case**: при 0 заказов -> 0.

**Refunds** — не вычитаются. Для net revenue использовать `net_revenue` колонку.
{% enddocs %}

{% docs net_revenue %}
**Net revenue** = revenue − refunds. Формула: `SUM(order_total - COALESCE(refund_amount, 0))`.

Отрицательный, если refunds > orders за период. Используется для accurate finance reporting.

**Валюта**: USD. **Тип**: NUMERIC(12, 2).
{% enddocs %}

{% docs ltv %}
**Lifetime value (LTV)** — суммарная revenue от клиента за всю историю (от первого
заказа до сегодня). Не учитывает refunds (см. `net_ltv` для net version).

**Формула**: `SUM(o.order_total) FILTER (WHERE o.status = 'completed')`.

**Использование**: classification сегментов, ML feature, marketing budgeting.

**Тип**: NUMERIC(12, 2).
{% enddocs %}

{% docs churn %}
**Churn** — событие потери клиента. В нашем определении: клиент не сделал заказа
за последние 90 дней.

**Формула**: `last_order_date < CURRENT_DATE - INTERVAL '90 day'`.

**Альтернативные определения** (для разных контекстов):
- **Soft churn**: 30-89 дней — клиент в риске
- **Hard churn**: 90+ дней — отток
- **Revenue churn**: monthly recurring revenue лост, не клиент (для SaaS-моделей)

В этом проекте используется **hard churn** (90 day).
{% enddocs %}

{% docs status_categories %}
Возможные значения колонки `status` в `fct_orders`:
- `completed` — заказ доставлен, оплачен
- `pending` — заказ создан, не оплачен
- `cancelled` — отменён клиентом до доставки
- `refunded` — возвращён после доставки

Изменения в этом списке требуют:
1. Обновление test `accepted_values` в `_models.yml`
2. Обновление CASE-логики в `customer_metrics` и downstream marts
3. Уведомление data-консумеров через #data-channel
{% enddocs %}

И YAML, использующий эти doc blocks:

version: 2

models:
  - name: customer_metrics
    description: |
      **Grain**: один клиент.
      **Бизнес**: KPI для customer success.
      **Обновление**: ежедневно 06:00 UTC.
      **Owner**: data-team@.
    columns:
      - name: customer_id
        description: '{{ doc("customer_id") }}'
        data_tests: [unique, not_null]

      - name: revenue
        description: '{{ doc("revenue") }}'

      - name: ltv
        description: '{{ doc("ltv") }}'

      - name: is_churned
        description: |
          Boolean flag: TRUE если клиент в hard churn.

          **Определение churn**: {{ doc("churn") }}

  - name: fct_orders
    columns:
      - name: customer_id
        description: '{{ doc("customer_id") }}'   # тот же блок что в customer_metrics
      - name: status
        description: '{{ doc("status_categories") }}'
        data_tests:
          - accepted_values:
              values: ['completed', 'pending', 'cancelled', 'refunded']

Когда поменяется определение revenue (например, добавим refunds в формулу), правишь один doc block в _glossary.md. Все YAML, ссылающиеся через {{ doc("revenue") }}, подхватят изменение.


CI gate: orphaned blocks и broken refs

Без CI doc blocks дрейфуют: typo в {{ doc('revneue') }} -> dbt parse даёт warning, но не падает, description становится строкой "Doc block 'revneue' not found". Через полгода — 20 broken refs, никто не знает.

Пример CI script scripts/check_doc_blocks.py:

#!/usr/bin/env python3
import json, re, sys, glob, pathlib

# 1) Собираем все объявленные doc blocks из .md в models/
declared = set()
for md in glob.glob('models/**/*.md', recursive=True):
    text = pathlib.Path(md).read_text()
    declared.update(re.findall(r'{%\s*docs\s+(\w+)\s*%}', text))

# 2) Собираем все ссылки из YAML / .md
referenced = set()
for path in glob.glob('models/**/*.yml', recursive=True) + glob.glob('models/**/*.md', recursive=True):
    text = pathlib.Path(path).read_text()
    referenced.update(re.findall(r'''doc\(\s*['"]([^'"]+)['"]\s*\)''', text))

# 3) Проверки
broken = referenced - declared        # ссылки на несуществующие
orphans = declared - referenced       # объявлены, но никто не ссылается

errors = []
if broken:
    errors.append(f"Broken doc refs: {sorted(broken)}")
if orphans:
    errors.append(f"Orphaned doc blocks: {sorted(orphans)}")

if errors:
    print('\n'.join(errors), file=sys.stderr)
    sys.exit(1)
print(f"OK: {len(declared)} doc blocks, {len(referenced)} references, all resolved")

Запуск в CI:

# .github/workflows/dbt-ci.yml
- name: Check doc blocks
  run: python scripts/check_doc_blocks.py

Падает при typo, при orphaned blocks (объявили, не используете — кладбище в _glossary.md), при broken refs. Один из самых high-leverage gates в documentation pipeline.


Когда выносить термин в doc block

Эвристика: если описание используется в 3+ местах, или это бизнес-критичный термин — выноси в doc block.

Выносить в doc block?

Простой тест: если ты пишешь одно и то же описание в 3-й раз — оторви в doc block. Если описание зависит от контекста модели — оставь inline.


Doc blocks и model descriptions

Можно ссылаться на doc block и в model description:

{% docs customer_metrics_overview %}
**customer_metrics** — KPI для customer success.

**Grain**: один клиент.

**Метрики**:
- `revenue` — {{ doc("revenue") }}
- `ltv` — {{ doc("ltv") }}
- `is_churned` — {{ doc("churn") }}

**Обновление**: ежедневно 06:00 UTC.

**Owner**: [email protected].
{% enddocs %}

YAML:

- name: customer_metrics
  description: '{{ doc("customer_metrics_overview") }}'

Это позволяет писать большое описание модели в .md файле (где markdown render комфортнее, чем в YAML), а YAML оставлять компактным.

NOTE

Doc blocks могут ссылаться на другие doc blocks через nested {{ doc(...) }}. Это удобно для composition: один блок «customer_metrics_overview» включает другие блоки «revenue», «ltv», «churn».


doc(‘name’, ‘project_name’) для cross-project

В dbt Mesh (multi-project setup) или при импорте package с glossary можно ссылаться на doc block из другого dbt-проекта вторым аргументом:

description: '{{ doc("revenue", "shared_finance_project") }}'

Production-пример: организация с 3-4 dbt-проектами (analytics, finance, ml) — все ссылаются на единый shared_glossary package:

# packages.yml каждого проекта
packages:
  - git: "https://github.com/myorg/dbt-shared-glossary.git"
    revision: v1.4.0
# dbt_packages/shared_glossary/models/_glossary.md
{% docs revenue %}
**Revenue** — canonical definition, governed by finance-team.
...
{% enddocs %}
# analytics проект — ссылается на canonical
- name: revenue
  description: '{{ doc("revenue", "shared_glossary") }}'

Преимущества:

  • Один источник правды для всей организации.
  • Версионирование через git tag — v1.4.0 -> v1.5.0 с подтверждением через PR.
  • При изменении definition revenue — один PR в shared_glossary, downstream проекты подхватывают через dbt deps.

Trade-off: добавляется dependency на shared package. Pin версии в packages.yml обязателен — иначе breaking change в glossary ломает все downstream.

Подробнее dbt Mesh топология — в dbt-iii.


Антипаттерны doc blocks

  1. Doc block для уникальной колонки: {% docs customer_metrics_unique_field %} — используется только в одной модели. Оверкилл. Inline проще.

  2. Очень длинный doc block: 2000 слов. Никто не читает. Лимит: 500 слов или таблицы. Если больше — оторви в отдельный .md файл и ссылайся через external link.

  3. doc block без проверки на presence: ссылаешься на {{ doc('revneue') }} (typo) — dbt парсит, генерит warning, но не падает. Description становится строкой "Doc block 'revneue' not found". CI должен ловить.

  4. doc block с markdown ошибками: # Heading в _docs.md рендерится в dbt docs UI неожиданно — стиль не совпадает. Используй **bold** для emphasis, и ## Heading для разделов в docs UI.

  5. Doc block с SQL кодом: doc block с SQL внутри — это полезно для context. Но SQL должен быть в markdown code blocks, иначе ломает rendering. Также — следить за {{ }} внутри SQL: Jinja попытается резолвить.

    Если в doc block нужен пример SQL с {{ ref(...) }}, escape через raw block:

    {% raw %}
    
        SELECT * FROM {{ ref('stg_orders') }}
    
    {% endraw %}
  6. Дубли: два разных doc blocks с похожим именем revenue_v1, revenue_v2 без deprecation. Решение: один canonical блок + DEPRECATED note в старом.


{% raw %} escaping для метасимволов в descriptions

В doc block может понадобиться показать код с Jinja-синтаксисом — пример {{ ref(...) }}, {{ source(...) }}, {% if %}. Без escape Jinja попытается резолвить эти теги при dbt docs generate и упадёт (или выдаст 'ref' is undefined).

Решение — {% raw %}...{% endraw %} блок:

{% docs incremental_pattern %}
**Incremental model pattern**:

{% raw %}
    {{ config(materialized='incremental', unique_key='event_id') }}

    SELECT * FROM {{ source('events', 'raw') }}

    {% if is_incremental() %}
      WHERE created_at > (SELECT MAX(created_at) FROM {{ this }})
    {% endif %}
{% endraw %}

Также `{% raw %}{{ var('lookback_days') }}{% endraw %}` берётся из dbt_project.yml.
{% enddocs %}

Без {% raw %} пример с {{ source('events', 'raw') }} попытается резолвить source — упадёт.

Где ещё нужен {% raw %}:

  • doc blocks с примерами SQL для macros (типа {{ safe_divide(...) }}).
  • doc blocks с {% if target.name == 'prod' %} snippets.
  • любой {{ }} или {% %} в тексте, который должен показываться как литерал, а не резолвиться.

Без понимания этого паттерна 30% production doc blocks с код-примерами просто сломаны.


Попробуй сам

В твоём dbt-проекте:

  1. Identify термины: на 3-5 mart-моделях посмотри column descriptions. Какие колонки повторяются (customer_id, revenue, …)?

  2. Создать models/_glossary.md:

    {% docs customer_id %}
    ...
    {% enddocs %}
    
    {% docs revenue %}
    ...
    {% enddocs %}
  3. Обновить YAML: заменить inline descriptions на {{ doc("name") }} для 5-10 повторяющихся колонок.

  4. Прогон:

    dbt parse
    dbt docs generate
    dbt docs serve
  5. Проверка в UI: открыть в браузере, найти колонку customer_id — должна показывать содержимое doc block.

  6. Тест на typo: сделать {{ doc("custmer_id") }} (typo) — dbt parse должен warning. CI должен fail.


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

  1. Doc block — переиспользуемый блок документации в .md файле, объявляется через {% docs name %}...{% enddocs %}, ссылается из YAML через {{ doc('name') }}.
  2. Лежат в models/_glossary.md или models/<dir>/_<context>__docs.md. По умолчанию dbt видит все .md в models/.
  3. YAML quoting: внешние кавычки отличаются от внутренних. Convention: '{{ doc("name") }}'. Block scalar (|) для multi-line.
  4. Когда выносить: используется в 3+ местах ИЛИ это бизнес-критичный термин (revenue, churn, customer_id). НЕ выносить для уникальных или контекст-зависимых колонок.
  5. Можно ссылаться на doc block в model description через тот же синтаксис. Удобно для длинных описаний модели — пишутся в .md, в YAML только ссылка.
  6. Doc blocks могут ссылаться друг на друга (nested doc(‘name’)). Это позволяет composition: «customer_metrics_overview» включает «revenue», «ltv».
  7. Антипаттерны: doc block для уникальной колонки, без проверки typo (broken ref), markdown errors, SQL с unescaped Jinja (нужен {% raw %}).
  8. CI gate: grep _glossary.md на orphaned blocks, grep YAML на broken refs. Без CI doc blocks дрейфуют.
Проверка знанийKnowledge check
Senior пишет: description: {{ doc('revenue') }} (без кавычек). dbt parse падает с YAML error. Какие правильные варианты quoting?
ОтветAnswer
YAML парсит {{ как Flow-map syntax, если кавычек нет. Правильные варианты: **1. Single quote снаружи + double inside**: description: '{{ doc("revenue") }}' **2. Double quote снаружи + single inside**: description: "{{ doc('revenue') }}" **3. Block scalar (для multi-line)**: description: | Этот column — {{ doc("revenue") }} за последние 30 дней. Используется для monthly dashboard. **Что НЕ работает**: # FAIL: одинарные внутри одинарных description: '{{ doc('revenue') }}' # FAIL: двойные внутри двойных description: "{{ doc("revenue") }}" # FAIL: YAML видит {{ как Flow-map start description: {{ doc('revenue') }} **Правило**: внешние кавычки **отличаются от внутренних**. De facto convention в dbt: одинарные снаружи + двойные внутри. **Block scalar (|)** полезен: - Multi-line description - Смесь doc block + custom text - Markdown formatting (lists, tables) более читаемо в YAML description: | **Revenue за 30 дней.** Базовая формула: {{ doc("revenue") }} Filter: order_date не меньше CURRENT_DATE - 30 day
Проверка знанийKnowledge check
Команда хочет рефакторнуть: 'revenue' появляется в 5 моделях с слегка разными формулировками. Стратегия выноса в doc block?
ОтветAnswer
Это идеальный кейс для doc block. **5 моделей** = выше threshold (3+) для extraction. **Шаг 1 — Audit**: grep -rn "revenue" models/ --include="*.yml" | head -30 Собираем все варианты formulation revenue (разные модели): - "Сумма всех заказов" - "Выручка за период" - "SUM(order_total) на completed" - "Revenue, USD" - ... **Шаг 2 — Canonical definition**: Обсудить с командой / маркетингом — какое **правильное** определение revenue для этого проекта. Возможно, что 5 моделей имеют действительно разные семантики — тогда не один doc block, а несколько: - revenue_per_order (для fct_orders) - revenue_lifetime (для customer_metrics) - revenue_monthly (для time-series) Если suspicion — обсудить с stakeholders, не делать silently. **Шаг 3 — Write doc block в models/_glossary.md**: {% docs revenue %} **Revenue** — суммарная сумма заказов клиента (status = 'completed') за указанный период. **Формула**: SUM(order_total) FILTER (WHERE status = 'completed') **Валюта**: USD. **Тип**: NUMERIC(12, 2). **Edge cases**: - 0 заказов -> revenue = 0 (COALESCE, не NULL) - Refunds: не вычитаются. Для net version -> net_revenue. {% enddocs %} **Шаг 4 — Replace в 5 YAML**: # 5 раз: - name: revenue description: '{{ doc("revenue") }}' **Шаг 5 — Verify**: dbt parse # should pass dbt docs generate dbt docs serve # check в UI, что revenue показывает doc block content **Шаг 6 — Future maintenance**: - CI gate (grep on broken refs) - При изменении формулы — править в _glossary.md (один источник правды) - Quarterly review: не разошёлся ли definition с SQL **ROI**: 5 inline descriptions -> 1 doc block + 5 references. При изменении revenue formula — изменение в одном месте, не в пяти.

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

Результат: 0 из 0
Прикладной
Вопрос 1 из 5. В YAML: `description: {{ doc('revenue') }}`. dbt parse падает с YAML error. Какие правильные варианты quoting?

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

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

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

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