Naming в production: migrations, deprecation, linters
Базовая деривация имён ты знаешь по dbt-i/13: stg_<source>__<entity> для staging, int_<purpose> или int_<domain>__<entity> для intermediate, fct_<entity> / dim_<entity> / bridge_<entity> / agg_<entity> для marts. Двойное подчёркивание __ разделяет source/domain от entity. Колонки <entity>_id, <...>_at, is_<...>. Этот урок не повторяет деривацию — он про то, что происходит с этими именами через год после старта, когда нужно их менять.
Когда вы стартуете dbt-проект, naming — это академическое упражнение: «правильное имя или нет». Когда проект два года в проде, naming — это операция, которая может положить 30 BI-дашбордов за один merge, если делать её неаккуратно.
Этот урок — про три вещи, которые отличают production naming от учебного: как переименовать модель без поломки consumers, как автоматически валидировать конвенции в CI, и как написать style guide, который команда из 10+ человек реально соблюдает.
Migration pattern — три фазы переименования
Самая частая ошибка junior-аналитика — открыть PR с заголовком «Rename: customer_revenue -> fct_customer_revenue» и одним коммитом, который меняет имя файла. На маленьком проекте это работает. На проекте с 5+ downstream consumers — это диверсия.
Правильный rename — трёхфазный, растянут на 4-8 недель, и каждая фаза занимает отдельный PR.
Фаза 1: Add new (week 0)
Создаёте новую модель с правильным именем. Старая остаётся. Обе работают параллельно.
# models/marts/finance/_finance__models.yml
version: 2
models:
- name: fct_customer_revenue
description: >
Revenue per customer per month. Replaces customer_revenue
(deprecated 2026-06-01, removal 2026-08-01).
config:
contract:
enforced: true
columns:
- name: customer_id
data_type: varchar
constraints:
- type: not_null
- type: primary_key
- name: revenue_usd
data_type: decimal(18,2)
constraints:
- type: not_null
- name: month_start_date
data_type: date
constraints:
- type: not_null
- name: customer_revenue
description: >
DEPRECATED 2026-06-01. Use fct_customer_revenue instead.
Will be removed on 2026-08-01.
deprecation_date: 2026-08-01
config:
tags: ["deprecated"]
Сам SQL-файл fct_customer_revenue.sql обычно сделан так:
-- fct_customer_revenue.sql
with revenue as (
select * from `{{ ref('int_finance__customer_revenue_calc') }}`
)
select * from revenue
А старая customer_revenue.sql переписывается на тонкий alias:
-- customer_revenue.sql (deprecated alias)
select * from `{{ ref('fct_customer_revenue') }}`
Это критично: старая модель теперь просто пробрасывает данные новой, источник истины — одна модель, не две. Иначе через месяц они расползутся.
Фаза 2: Deprecate (weeks 1-6)
Включаете deprecation_date на старой модели. Это встроенный механизм dbt: когда любой downstream ref’ит модель с deprecation_date ближе чем через 60 дней, dbt выводит warning при dbt parse:
WARNING: Model 'customer_revenue' is deprecated.
It will be removed on 2026-08-01.
References from: dashboards.exposure.executive_summary,
dashboards.exposure.cfo_monthly,
ml_pipeline.feature_store.user_features
Этот warning попадает в CI-логи каждого PR во всех downstream-проектах. Если у вас dbt Mesh — warning виден в child-проектах автоматически. Это встроенный механизм коммуникации, который не требует Slack-сообщений и email-рассылок.
В этой же фазе:
- Notify consumers явно. Email + Slack-message в
#data-platformсо списком dashboards, которые ref’ят старую модель. Список берётся изdbt ls --select +customer_revenue --output jsonплюс exposures. - Track usage. Если ваш warehouse даёт query logs (Snowflake
QUERY_HISTORY, BigQueryINFORMATION_SCHEMA.JOBS_BY_PROJECT) — мониторьте, кто реально читает старую модель. Если за 4 недели читателей не убавляется, deadline сдвигается. - Не ускорять deadline. Соблазн «никто не возражает, удалим раньше» — типичный путь к падению prod. Кто-то всегда возражает, просто молча.
Фаза 3: Remove (week 8)
В deadline-день: удаляете SQL-файл старой модели, удаляете YAML-запись, делаете один PR с понятным заголовком Remove deprecated: customer_revenue. Этот PR идёт в обычное CI, не требует особых проверок — все консьюмеры уже мигрированы.
Add new (week 0) -> deprecate с date (weeks 1-6) -> remove (week 8). Каждая фаза — отдельный PR. Между фазами consumers мигрируют, dbt warnings подсвечивают зависимости.
Versions — параллельный механизм для breaking changes
dbt-i: базовые конвенции именованияdeprecation_date хорош для rename. Но что если вы не переименовываете модель, а меняете её колонки? Например, fct_orders.total (без валюты) должна стать fct_orders.total_amount_usd. Тут rename файла не помогает — имя модели то же.
Здесь работают dbt model versions. Идея: одна логическая модель, две физические версии, переход постепенный.
# models/marts/finance/_finance__models.yml
models:
- name: fct_orders
latest_version: 2
config:
contract:
enforced: true
columns:
- name: order_id
- name: customer_id
- name: total_amount_usd
data_type: decimal(18,2)
- name: order_date
versions:
- v: 2
defined_in: fct_orders_v2
- v: 1
deprecation_date: 2026-08-01
defined_in: fct_orders_v1
columns:
- include: '*'
exclude: ['total_amount_usd']
- name: total
data_type: decimal(18,2)
Что происходит:
ref('fct_orders')без версии -> берётlatest_version: 2, то естьfct_orders_v2.ref('fct_orders', v=1)-> старая версия, ещё работает.ref('fct_orders', v=2)-> новая версия, явно.
Downstream-команды мигрируют по версиям, а не по rename:
-- старый dashboard.sql (постепенно обновляется)
select
order_id,
sum(total) as revenue
from `{{ ref('fct_orders', v=1) }}`
-- новый dashboard.sql после миграции
select
order_id,
sum(total_amount_usd) as revenue
from `{{ ref('fct_orders', v=2) }}`
Versions — это infrastructure, которая позволяет вам сделать breaking change без коммуникационного chaos. Каждая команда мигрирует на v=2 в своём темпе, dbt парсер видит, кто ещё на v=1, deprecation_date выводит на этой версии warning.
Используйте versions только для публичных моделей (marts с access: public). Versions имеют overhead: два физических файла, два набора тестов, два места для документации. На internal intermediate-моделях это не окупается. Эмпирическое правило: если модель ref’ит хотя бы один exposure (BI-дашборд, ML-пайплайн, Reverse ETL) — кандидат на versions при breaking change. Если только intermediate из вашего же домена — просто переименуйте, обновите ref’ы синхронно.
Linters в CI — sqlfluff и project_evaluator
dbt-iii: model versions в Mesh — миграция v1 -> v2 между проектамиКонвенции, которые человек должен помнить, через год нарушаются в 30% случаев. Конвенции, которые проверяет CI, нарушаются в 0%.
Два инструмента, которые покрывают 95% naming-проверок: sqlfluff для SQL-конвенций, dbt_project_evaluator для structure/naming на уровне dbt.
sqlfluff: SQL и naming rules
sqlfluff — линтер для SQL, понимающий Jinja и dbt. Конфигурация через .sqlfluff в корне проекта:
[sqlfluff]
templater = dbt
dialect = duckdb
exclude_rules = L016, L029
[sqlfluff:rules:capitalisation.keywords]
capitalisation_policy = lower
[sqlfluff:rules:references.consistent]
single_table_references = consistent
[sqlfluff:rules:references.qualification]
unqualified_references = false
[sqlfluff:templater:dbt]
project_dir = .
profiles_dir = ~/.dbt
Запуск в CI:
sqlfluff lint models/ --format github-annotation-native
sqlfluff fix models/ --force # автофикс safe-нарушений
Для naming-конвенций моделей sqlfluff умеет проверять регулярные выражения на имена файлов. Создайте models/.sqlfluff:
[sqlfluff:rules:references.special_chars]
# имена колонок только snake_case
allow_space_in_identifier = false
quoted_identifiers_policy = none
[sqlfluff:rules:capitalisation.identifiers]
extended_capitalisation_policy = lower
dbt_project_evaluator: structure rules
Пакет от dbt Labs, который запускается как dbt-модели в вашем проекте и проверяет:
- Все staging-модели named
stg_<source>__<entity>. - Все mart-модели в
marts/имеют префиксfct_/dim_/bridge_/agg_. - Нет ref на mart из staging (direction-rule).
- Нет staging-моделей с join на другую staging.
- Все source-таблицы используются хотя бы одной staging-моделью.
- Все модели имеют description в YAML.
- Все primary keys имеют test
unique+not_null.
packages.yml:
packages:
- package: dbt-labs/dbt_project_evaluator
version: [">=0.13.0", "<0.14.0"]
В CI:
dbt deps
dbt build --select package:dbt_project_evaluator
Если есть нарушения — модели fct_marts_or_intermediate_dependent_on_source, fct_undocumented_models и др. содержат строки с конкретными violations. CI настраивается так, чтобы dbt test --select package:dbt_project_evaluator падал на любых нарушениях.
PR проходит через два уровня: sqlfluff (SQL и naming syntax) и dbt_project_evaluator (структура и dependencies). Оба должны зелёные, чтобы PR merge'нулся.
Real anti-patterns из production
Список того, что реально встречается в проектах, доросших до middle-уровня без conventions. Каждый — реальный пример из dbt-аудитов.
final_v2_final.sql — копия final_v2.sql, потому что аналитик не доверял версии 2 и сделал «финальную финальную». Через год их три: final_v2_final, final_v2_final_real, final_v2_final_USE_THIS. Лечение — model versions с latest_version.
tmp_test.sql в models/, не в analysis/. Это staging для какой-то одноразовой проверки, забытый три месяца назад. Сейчас один dashboard ref’ит её, и аналитик боится удалить. Лечение — dbt_project_evaluator падает на моделях без description, не давая такому добраться до prod.
analytics_kpis_2024.sql — год в имени. В 2026 году будет analytics_kpis_2025.sql и analytics_kpis_2024_archive. Year-stamping — антипаттерн: модель должна быть временно-нейтральна, а год должен быть колонкой данных. Лечение — sqlfluff regex-правило ^(stg|int|fct|dim|bridge|agg)_[a-z_]+$ без цифр в имени файла.
vasily_dashboard.sql — имя аналитика в имени модели. Через 2 года Василий уйдёт, и модель никто не тронет «потому что это Васина». Лечение — model groups с owner: [email protected], никаких personal names в model paths.
fct_orders_FINAL_USE_THIS.sql — UPPERCASE-emphasis. Сигнал, что в проекте есть fct_orders_old, fct_orders_legacy, fct_orders_deprecated, и команда не уверена, какая правильная. Лечение — deprecation_date + version migration вместо «название кричит, какая правильная».
stg_stripe_charges.sql (одинарное подчёркивание вместо двойного) — точечное нарушение конвенции. Один файл из 100. Через год их 5, через два — 15. Лечение — sqlfluff regex на имя файла, падает в CI.
int_stripe_charges_dedup.sql — source в имени intermediate. Intermediate должен быть business-aware, не source-aware. stripe_charges это уже staging-слой; intermediate, который пере-дедупит charges — это staging с дополнительной логикой, должен либо быть в staging как stg_stripe__charges (если dedup универсальный), либо в intermediate как int_finance__payments_canonical (если это бизнес-логика). Лечение — code review плюс dbt_project_evaluator правило int_models_naming.
mart_orders.sql вместо fct_orders.sql — «mart» это слой, а не префикс. Каждая модель в marts/ это mart по определению, префикс должен описывать тип (fact / dim / bridge / agg), не повторять название слоя. Лечение — sqlfluff regex на имена файлов в models/marts/.
Org-wide style guide — один документ, версионированный
Все эти конвенции должны жить в одном месте: docs/dbt-style-guide.md в репозитории проекта. Не в Confluence, не в Notion, не «в голове у Васи».
Структура хорошего style guide:
- Naming derivation — формулы для каждого слоя, ссылки на dbt Labs guide.
- Anti-patterns — конкретный список того, что нельзя, с примерами.
- Migration patterns — как rename, как breaking change, как deprecate.
- CI rules — какие linters запускаются, на что они падают.
- Exceptions — где можно отступить от convention и почему. Эта секция критична: без неё guide становится догмой, и команда начинает его обходить.
Версионирование guide. Style guide живёт в git, изменения через PR. Каждое крупное изменение — добавление anti-pattern, изменение конвенции, исключение — проходит обычный review. В commit message записывается rationale изменения: почему добавили, какой incident это решает.
# git log docs/dbt-style-guide.md
2026-04-12 Add anti-pattern: year-stamping in model names
2026-03-08 Loosen rule on int_ prefix for one-off finance models
2026-02-15 Add deprecation_date workflow with 60-day notice
2026-01-03 Forbid CamelCase identifiers (incident SE-2024-009)
Команда из 10 человек смотрит на guide раз в неделю при review PR — и это работает только если он короткий (не больше 5 страниц), конкретный (примеры, не абстракции) и актуальный (последний commit не старше 3 месяцев).
Plant a check: раз в квартал run dbt_project_evaluator на чужом проекте (партнёрском, например, проекте соседней команды). Если ваш guide неверен или устарел — соседи это покажут «у вас не так как у нас, а лучше или хуже?». Это лучший способ держать guide живым.
Один шаблон на team / company
Когда внутри компании несколько dbt-проектов, style guide должен быть company-wide, не per-project. Иначе при ротации аналитика между командами он сталкивается с пятью разными «как мы тут пишем staging».
Технически это решается через shared dbt package с конвенциями:
# packages.yml в каждом проекте компании
packages:
- git: "https://github.com/mycompany/dbt-style-pack.git"
revision: "1.4.0"
В пакете лежат: .sqlfluff config, custom dbt_project_evaluator overrides, shared macros для cents-to-dollars, type-cast helpers. Style guide сам — Markdown в репозитории пакета, ссылка из README каждого проекта.
Когда команда меняет конвенцию (например, переходит на domain-based intermediate), она обновляет пакет до 1.5.0, и все проекты обновляют packages.yml. Это не мгновенно — команды мигрируют в своём темпе, — но даёт единый источник истины для всей компании.
Попробуй сам
- Откройте свой dbt-проект и найдите хотя бы один anti-pattern из списка выше. Запишите его.
- Установите
dbt_project_evaluatorчерезpackages.yml, запуститеdbt deps && dbt build --select package:dbt_project_evaluator. Посмотрите, сколько violations найдено. - Установите
sqlfluffлокально, запуститеsqlfluff lint models/. Посмотрите количество warnings. - Выберите одну модель, которую вы хотите переименовать, и запишите trёхфазный план: даты PR1, PR2, PR3, список consumers, кому пойдёт notification.
- Создайте файл
docs/dbt-style-guide.mdв проекте, даже если он на одну страницу. Запишите туда naming-конвенции, которые уже соблюдаются. Это первая версия — дальше она будет расти.