Прямой ответ
dbt (data build tool) — «T» из ELT как инженерный проект: вы пишете SELECT-запросы (модели) в .sql-файлах с git-дисциплиной (версии, ревью, тесты, CI), а dbt превращает их в таблицы/вьюхи в складе, строит DAG зависимостей сам (из {{ ref('модель') }} — не из имён таблиц — порядок выполнения/пересборка — автоматически) и тестирует данные декларативно; ценность против «скрипты в cron»: воспроизводимость (dbt build — весь граф с нуля — ETL-контекст — Python-скрипты с сырыми INSERT — хрупки и нечитаемы), тестируемость (not_null/unique/accepted_values/relationships — YAML рядом с моделью — «в orders.user_id есть несуществующие пользователи» — тест красный до дашборда), документация (генерируется из кода + описаний — lineage-граф — «откуда эта цифра» — кровь данных вместо устной традиции); materializations (как модель материализуется): view (дёшево/свежо — запрос на лету — медленное чтение), table (CREATE TABLE AS — быстрое чтение, полная пересборка — мелкие справочники), incremental (дозагрузка новых/изменённых строк — {{ this }} + unique_key — событийные таблицы/миллионы строк — стратегия merge/append/delete+insert — «пересобирать 500M строк каждый час» — убийство склада — ClickHouse-специфика —ReplacingMergeTree-соседство), ephemeral (CTE-подстановка — промежуточные без хранения — лёгкие промежуточные); структура проекта (конвенция важнее изобретательства): staging (1:1 обёртки над сырыми источниками — переименование/типы/кастинги — единственное место, знающее имена сырых таблиц — sources: YAML — freshness-проверка «сырьё протухло») → intermediate (промежуточная логика — ephemeral/view) → marts (бизнес-сущности для дашбордов — table/incremental — витрины); тесты двух видов: generic (unique/not_null/relationships/accepted_values — YAML — 90% нужд) и singular (свой SQL — «сумма позиций = сумма заказа» — assert-запрос: строки есть — тест провален — бизнес-инварианты — самые ценные); seed-файлы (CSV в репо — справочники/маппинги — версионируются с кодом — «таблица в Excel у аналитика» умирает); CI-практика: dbt build --select state:modified+ (только изменённое и зависимое — deferral/slim CI — полный граф на каждый PR — разорительно — state-сравнение с прод-манифестом — паттерн slim CI); dbt docs generate/serve — документация+lineage — культура документации — «доки пишутся сами» — отговорка «нет времени на документацию данных» снята; границы: dbt — трансформации (SQL в складе), не оркестрация (cron/Airflow запускает dbt build — dbt не знает «когда» и «что если upstream-файл не пришёл» — связка dbt+Airflow — стандарт) и не загрузка (Extract/Load — Airbyte/Fivetran/скрипты — dbt начинается, когда данные уже в складе — ELT-граница).
Практика: инкрементальные модели, снапшоты, environments
Incremental-дисциплина (где dbt-проекты ломаются): паттерн модели — WHERE event_time > (SELECT max(event_time) FROM {{ this }}) (дозагрузка — первый прогон — полная, далее — хвост) + unique_key (dedup при повторах — merge-стратегия — «событие прислали дважды» — вебхук-дубли — склад не исключение); подводные камни: backfill исторических данных (инкрементал «не увидит» прошлое — --full-refresh точечно или delete+insert окно — «подгрузили старый файл — дашборд не изменился» — частый тикет); late-arriving data (событие за вчера пришло сегодня — окно дозагрузки с запасом (3 дня) + merge по ключу — «строго по max(event_time)» — тихие потери); партиционирование (postgres-партиции/ClickHouse ORDER BY ключ — инкрементал+партиции — связка производительности); snapshots (SCD-2 из коробки — dbt snapshot — история изменений строки (valid_from/valid_to — «какая цена была 1 марта» — аналитика по состоянию на дату — вручную писать SCD-2 — боль, снапшот — 20 строк конфига — медленно меняющиеся измерения); environments (dev/prod — один код, разные схемы (target в profiles.yml — dev_jane.orders против prod.orders — разработчик не трогает прод — 12-factor-конфигурация — секреты подключения — env/secrets); dbt run-operation/hooks (pre/post-hook — GRANT'ы после сборки — права аналитиков — «забыли выдать права на новую витрину» — hook решает системно); packages (dbt_utils — date_spine/surrogate_key — не изобретать — пакеты в packages.yml — версии pin'им — зависимости — supply chain и здесь); метрики/semantic layer (metrics в YAML — «выручка» определена один раз — дашборды из одного определения — «у маркетинга выручка не сходится с финансами» — лечится semantic-слоем — PowerBI/SQL-выбор — BI-инструменты поверх одного источника правды).
Практический сценарий: от 200 SQL-скриптов в cron к dbt за квартал
Контекст: e-commerce аналитика — 200+ SQL-скриптов (cron, ручной порядок, INSERT INTO ... SELECT), симптомы: «цифра на дашборде поехала» (скрипт №47 отработал до №12 — порядок зависимостей жил в голове сеньора), «восстановить витрину» — полдня ручных прогонов, новый аналитик онбордится месяц («какие таблицы живые — никто не знает»). Миграция (квартал, по слоям): (1) инвентаризация — cron-скрипты → список моделей, порядок зависимостей восстановлен grep'ом по именам таблиц (неделя — «карта» впервые появилась); (2) каркас — dbt-проект (Postgres-схема analytics — склад на том же Postgres — отдельная БД/схема — staging/intermediate/marts — конвенция), sources+freshness на сырьё (orders_raw/users_raw — проверка «сырьё старше 26 часов» — алерт — мониторинг — «файл не приехал» видно сразу, не через дашборд); (3) перенос волнами (по доменам: сначала marts продаж — 30 моделей — staging-слой переименовал/кастовал сырьё — марты читают staging через ref() — DAG собрался сам; потом маркетинг, финансы — «старый cron выключен — новый dbt build в Airflow» — параллельный прогон 2 недели — сверка цифр — расхождений ноль — доверие команды — критично для миграции); (4) тесты как принятие (каждая модель переезжает только с not_null на ключи + unique + relationships на FK — «в orders.user_id 3% висящих» — тесты нашли в первую неделю — данные чинили на входе — «склад мусорный» — впервые измерено); singular-тест «итоги позиций = итог заказа» — поймал баг старого скрипта, живший год; (5) инкрементал на событийные (events — 400M строк — полная пересборка 2 часа → incremental merge — 8 минут — планы запросов — unique_key event_id — дубли вебхуков схлопываются); (6) slim CI (GitHub Actions — dbt build --select state:modified+ — PR меняет модель — пересборка+тесты только затронутого — 4 минуты против 40 полного графа — CI-кэширование); (7) docs+lineage в общий доступ («откуда цифра» — клик по графу — онбординг новичка — месяц → неделя — измерено); итоги квартала: 200 скриптов → 140 моделей (часть умерла — «а этим дашбордом никто не пользовался» — lineage-граф показал сирот — очистка), инциденты «цифра поехала» — 8/мес → 1/мес (тесты+freshness), восстановление витрины — dbt build --select +модель — минуты; главный вывод — dbt не «ещё один инструмент», а перенос инженерной культуры (git/тесты/ревью/CI) на территорию SQL — техдолг аналитики лечится теми же методами, что код.
FAQ
dbt против ETL-скриптов на Python (pandas/airflow-задачи) — когда что? dbt — когда трансформация выражается SQL-ом внутри склада (агрегации/джойны/витрины — 80% аналитических задач — декларативно/тестируемо/документируемо); Python — когда нужна программная логика (API-загрузки, ML-фичи, сложные парсинги — ETL на Python — dbt не для «скачать и распарсить»); связка-стандарт: Python делает EL (доставил сырьё в склад), dbt делает T (превратил в витрины) — оркестрирует Airflow; «pandas-трансформации в памяти против SQL в складе» — на больших объёмах склад победит (pandas-производительность — память ноутбука против MPP — выбор инструментов); критерий: данные помещаются и живут в складе — dbt; живут в файлах/потоках вне склада — Python.
Нужен ли «настоящий» склад (Snowflake/BigQuery) или dbt работает на Postgres? dbt-адаптеры есть для Postgres/ClickHouse/SQLite — «склад» — это роль, не продукт (аналитическая БД с колоночными/партиционными возможностями — ClickHouse — open source склад); критерии масштаба: до ~сотен ГБ и десятков параллельных аналитиков — Postgres-схема (или отдельная реплика — read-реплики — аналитика не мешает проду) — dbt работает прекрасно; выше — колоночные MPP (ClickHouse self-hosted — бесплатно, Snowflake/BigQuery — managed с ценой за удобство); миграционный путь dbt-проекта между адаптерами — SQL-диалекты правятся (пакеты-макросы абстрагируют часть — не всё — «переезд Postgres→ClickHouse» — недели, не дни — JOIN-семантика/типы); вывод: начинать на Postgres — нормально и дёшево — dbt-дисциплина переносима, склад меняется под объём.
Как тестировать dbt-модели в CI без дорогого полного прогона?
Slim CI: dbt parse (валидация SQL/YAML — секунды) → dbt build --select state:modified+ --defer --state prod_manifest/ (пересборка только изменённых моделей и их потомков, остальное — deferral на прод-объекты — минуты против часов — манифест прода артефакт CI); изоляция — отдельная CI-схема/BД (testcontainers-Postgres — паттерн — ephemeral-окружение — мусор не копится); данные для тестов — seeds + синтетические fixtures (реальные данные в CI — приватность/комплаенс); контрактные проверки — dbt-тесты и есть тесты данных (unique/not_null/singular — гоняются на каждом PR — тест-пирамида данных); частота полного графа — ночной пайплайн (регрессия «состояние всего» — днём — только diff — CI-скорость).
Что такое source freshness и зачем оно?
dbt source freshness — проверка «сырьё обновлено недавно» (loaded_at колонка + порог в sources.yml — «orders_raw старше 26 часов» — warning/error); зачем: витрины строятся из сырья — сырьё протухло — витрины молча врут (данные вчерашние, дашборд «свежий» — худший класс бага — доверие к цифрам); интеграция: freshness перед build в Airflow (протухло — алерт Alertmanager + build не запускается/помечается — «не строим на гнилье»); загрузка без loaded_at — добавляем на EL-этапе (техническая колонка — ETL-практика — копейки); это «healthcheck входных данных» — healthcheck-культура, перенесённая на данные — дешёвая страховка от класса инцидентов «цифры не те».
Источники: документация dbt (introduction, materializations, data tests) — ссылки в шапке.