persist_docs: descriptions в warehouse как column comments
В прошлых уроках мы написали descriptions в YAML и подключили doc blocks. Эти description видны в dbt docs UI. Но что про data consumers, которые не пользуются dbt docs? Например:
- BI tool (Tableau / Looker / Mode) подключается к warehouse напрямую — видит таблицы и колонки, но не видит документации dbt.
- Аналитик пишет ad-hoc SQL через DBeaver / DataGrip / Snowflake worksheet — открывает каталог таблиц, видит имя колонки
customer_revenue_30d— что это значит? - Data scientist подключается через Python (snowflake-connector / sqlalchemy) — SELECT * -> колонки без context.
Решение — persist_docs: dbt сохраняет описания в metadata warehouse как COMMENT ON TABLE / COMMENT ON COLUMN. После dbt run descriptions появляются в каталоге warehouse, и любой инструмент, читающий metadata, их подхватит.
Этот урок — про конфигурацию persist_docs, варианты на разных warehouse, и подводные камни (length limits, special chars).
Концепт: comment в warehouse
В большинстве warehouses (Snowflake, BigQuery, Postgres, DuckDB) есть SQL syntax для добавления comments к objects:
COMMENT ON TABLE customer_metrics IS 'KPI клиентов: revenue, ltv, churn';
COMMENT ON COLUMN customer_metrics.revenue IS 'Sum завершённых заказов (USD)';
COMMENT ON COLUMN customer_metrics.is_churned IS 'TRUE если last_order > 90 days ago';
Эти comments хранятся в information schema warehouse:
-- Snowflake
SELECT column_name, comment
FROM information_schema.columns
WHERE table_name = 'CUSTOMER_METRICS';
-- BigQuery
SELECT column_name, description
FROM `project.dataset.INFORMATION_SCHEMA.COLUMNS`
WHERE table_name = 'customer_metrics';
-- DuckDB
SELECT column_name, comment
FROM information_schema.columns
WHERE table_name = 'customer_metrics';
BI-tool / IDE подключается к warehouse, читает information schema, показывает description рядом с column name. Аналитик видит контекст без открытия dbt docs.
Включение persist_docs
В dbt_project.yml (или per-model через config):
models:
+persist_docs:
relation: true # сохранять description на уровне TABLE
columns: true # сохранять description на уровне COLUMN
После этого при dbt run dbt дополнительно запускает COMMENT ON ... для каждой материализованной модели и каждой описанной колонки.
Можно настраивать per-model:
- name: customer_metrics
config:
persist_docs:
relation: true
columns: true
Или per-folder:
# dbt_project.yml
models:
my_project:
marts:
+persist_docs:
relation: true
columns: true
staging:
+persist_docs:
relation: true
columns: false # на staging — только table-level, колонки тривиальные
Это полезный pattern: на marts (consumer-facing) — full persist, на staging — только relation level. Меньше нагрузки на metadata, фокус на consumer-relevant.
Когда persist_docs нужен / не нужен
Каталог данных — куда попадает metadata из persist_docsПоддержка по warehouses
Snowflake
Поддержка: полная — relation + columns на tables, views, snapshots, seeds.
+persist_docs:
relation: true
columns: true
Snowflake принимает up to 256 characters для column comments (без значимых length issues для большинства описаний). Для table-level — больший лимит.
Снимок:
DESCRIBE TABLE analytics.customer_metrics;
-- видит column descriptions
SHOW COLUMNS IN TABLE analytics.customer_metrics;
-- видит column descriptions
BigQuery
Поддержка: полная — relation + columns. Через OPTIONS(description = '...') syntax.
+persist_docs:
relation: true
columns: true
BigQuery принимает до 1024 characters для column descriptions. Markdown rendered не в BigQuery UI — plain text only.
Redshift
Поддержка: частичная:
- relation: yes (
COMMENT ON TABLE) - columns: yes (
COMMENT ON COLUMN) - materialized views: support only since recent versions
Postgres
Поддержка: полная. Standard SQL syntax.
COMMENT ON TABLE customer_metrics IS '...';
COMMENT ON COLUMN customer_metrics.revenue IS '...';
DuckDB
Поддержка: partial / evolving.
DuckDB 1.0+ поддерживает COMMENT ON TABLE и COMMENT ON COLUMN. dbt-duckdb 1.10+ implements persist_docs.
COMMENT ON TABLE analytics.customer_metrics IS '...';
COMMENT ON COLUMN analytics.customer_metrics.revenue IS '...';
-- Чтение
SELECT column_name, comment
FROM information_schema.columns
WHERE table_name = 'customer_metrics';
Ограничения DuckDB:
- Views — comment не sticky на view-level в некоторых версиях.
- При
:memory:режиме comments исчезают вместе с процессом.
В большинстве middle-проектов dbt-duckdb persist_docs работает. На production DuckDB рекомендую тестировать на your specific version.
Полный пример: customer_metrics с persist_docs
YAML:
# models/marts/_marts__models.yml
version: 2
models:
- name: customer_metrics
config:
persist_docs:
relation: true
columns: true
description: |
KPI клиентов: revenue, ltv, churn, segment.
Grain: один клиент. Update daily 06:00 UTC.
Owner: [email protected].
columns:
- name: customer_id
description: 'PK. FK на dim_customers. From raw.customers.id.'
data_tests: [unique, not_null]
- name: revenue
description: 'Lifetime revenue (USD). SUM(order_total) FILTER completed.'
- name: ltv
description: 'Same as revenue. Alias kept for backwards compat.'
- name: is_churned
description: 'TRUE if last_order_date < CURRENT_DATE - 90 day.'
- name: segment
description: 'One of: VIP, Premium, Standard, Inactive.'
После dbt run:
-- В DuckDB / Snowflake / Postgres
SELECT column_name, comment
FROM information_schema.columns
WHERE table_name = 'customer_metrics';
-- Результат:
-- customer_id | PK. FK на dim_customers. From raw.customers.id.
-- revenue | Lifetime revenue (USD). SUM(order_total) FILTER completed.
-- ltv | Same as revenue. Alias kept for backwards compat.
-- is_churned | TRUE if last_order_date < CURRENT_DATE - 90 day.
-- segment | One of: VIP, Premium, Standard, Inactive.
В Tableau при подключении к warehouse — descriptions появляются в каталоге колонок. В DataGrip / DBeaver — в schema browser. В DataHub / Atlan — автоматически захватывается.
Gotchas: что идёт не так
1. Markdown в descriptions ломает rendering
В dbt docs description с **bold** рендерится bold. В warehouse — это raw text **bold**, asterisks видны. Аналитик в DBeaver видит:
**Revenue** — суммарная сумма за период. Формула: `SUM(order_total)`.
Markdown не render. Asterisks и backticks выглядят как мусор.
Решение — два разных descriptions:
- В YAML — markdown для dbt docs UI
- Через
meta.persist_doc_overrideили manually plain-text version
Или один tone: plain-text короткие descriptions, markdown минимизирован. Это compromise.
# Хорошо для both dbt docs + warehouse comments
description: 'Lifetime revenue (USD). SUM(order_total) FILTER WHERE status = completed.'
# Плохо для warehouse (markdown will leak)
description: |
**Lifetime revenue (USD)**.
Формула:
SUM(order_total) FILTER WHERE status = ‘completed’
Production-grade подход: краткие plain-text descriptions для basic info + длинные markdown в doc blocks. Persist использует description (короткое), doc block выдаёт длинное в dbt docs.
2. Length limits
Snowflake: 256 chars on column. Если description длинный — truncate silently.
Workaround — короткая plain-text version:
- name: very_complex_metric
description: |
{% if env_var('DBT_DOCS_LONG', '0') == '1' %}
Long markdown description with formulas, tables, edge cases...
{% else %}
Short plain-text version up to 256 chars for warehouse persist.
{% endif %}
Это hack — обычно команды просто пишут короткое description и длинное в doc block.
3. Special characters
Кавычки / apostrophes в descriptions могут break SQL escape:
description: "Customer's lifetime value" # apostrophe в строке
dbt автоматически escapes для COMMENT statement. Но если descriptions содержат не-printable / control characters — могут быть issues. Solution — sanitize при написании.
4. Views vs tables
COMMENT ON VIEW поддерживается не во всех warehouses. На DuckDB views support inconsistent. Если модель материализована как view, persist_docs может не работать.
Workaround — +materialized: table (если data size позволяет) или +materialized: incremental.
5. persist_docs не работает для seeds в некоторых warehouses
Snowflake/BigQuery: yes. DuckDB: partial. Если seeds — простые lookup tables, обычно не критично.
6. Сохранение при column rename
Если ты переименовал колонку (revenue -> lifetime_revenue) и сделал dbt run, warehouse имеет:
- Старая колонка
revenueвсё ещё имеет старый comment (until next full refresh). - Новая колонка
lifetime_revenueимеет новый comment.
При --full-refresh или incremental rebuild — старый comment стирается. Но на view-only refresh может задержаться.
CI: проверка persist_docs
# scripts/check_persist_docs.py
import duckdb # или snowflake-connector / sqlalchemy
con = duckdb.connect('./analytics.duckdb')
# Проверка, что critical mart models имеют comments
critical_marts = ['customer_metrics', 'revenue_daily', 'churn_predictions']
for model in critical_marts:
result = con.execute(f"""
SELECT COUNT(*) FROM information_schema.columns
WHERE table_name = '{model}'
AND (comment IS NULL OR comment = '')
""").fetchone()
missing = result[0]
if missing > 0:
print(f"ERROR: {model} has {missing} columns without comment")
exit(1)
print("All critical marts have full column comments")
Этот CI gate ловит регрессии: PR добавил колонку, забыл description — на dbt run persist_docs пытается сохранить пустую строку — CI catches.
Антипаттерны
-
persist_docs без plain-text version: markdown leaks в warehouse, аналитики видят asterisks. Решение — короткие plain-text для persist + длинные markdown в doc blocks для dbt docs.
-
persist_docs на больших ephemeral / view-only моделей: COMMENT не sticky, на каждом run перезаписывается. Лишняя нагрузка на metadata.
-
persist_docs на тривиальных staging: 100 моделей в staging × 20 колонок × COMMENT statement = 2000 extra DDL per run. Удваивает runtime. Решение —
persist_docsтолько на marts. -
Заполнить comments через scripts, не через dbt: команды иногда пишут вручную
COMMENT ON ...после dbt run. Это дрейфует: на next dbt run dbt перепишет comment из YAML, кастомный пропадёт. Единый source of truth — YAML. -
No verification что comments persisted: написали description в YAML, не проверили в warehouse. После рефакторинга materialization (view -> table) comments могут пропасть. Solution — periodic check.
Попробуй сам
В твоём dbt-проекте:
-
Включить persist_docs для marts:
models: my_project: marts: +persist_docs: relation: true columns: true -
dbt run на одной mart-модели.
-
Проверить в DuckDB:
SELECT column_name, comment FROM information_schema.columns WHERE table_name = 'my_mart_model'; -
Открыть в DataGrip / DBeaver — должны видеть column descriptions в schema browser.
-
Изменить description в YAML, прогнать
dbt runснова — comment в warehouse обновляется. -
Попробовать markdown в description (например
**bold**) — увидеть, что в warehouse asterisks показываются как text. Решить — оставить markdown или сократить до plain text.
Ключевые выводы
- persist_docs сохраняет descriptions из YAML в warehouse metadata через
COMMENT ON TABLE/COLUMN. Послеdbt runBI tools / DataGrip / DataHub видят descriptions. - Конфигурация:
relation: true/false,columns: true/false. Можно per-folder, per-model. - Включай для consumer-facing marts, выключай для staging (overhead, тривиальные descriptions).
- Snowflake/BigQuery/Postgres — full support. DuckDB — partial (views не везде). Redshift — partial.
- Gotcha: markdown leaks в warehouse. Лучшая практика — короткие plain-text descriptions для persist, длинные markdown в doc blocks для dbt docs UI.
- Length limits: Snowflake 256 chars on column. Длинные descriptions truncate. Solution — short plain text + long markdown.
- CI verification: Python script проверяет, что critical mart колонки имеют comments в information_schema.
- Антипаттерны: markdown без plain version, persist на views/ephemeral, manual comments через scripts (дрейфует от dbt YAML), без verification.