В первом уроке мы пробежались по dbt build и dbt retry. Теперь разберёмся, что внутри этих команд особенно полезно и какие подводные камни ждут, когда проект становится большим.
dbt build vs dbt run + dbt test
Это главный паттерн production-CI. Различие тонкое, но критическое.
dbt run + dbt test:
dbt run # все модели бегут, материализуются
dbt test # потом все тесты бегут
Проблема: если тест на stg_orders упал — customers (downstream) уже посчитался и записался в warehouse по грязным данным. Мониторинг видит зелёный run, BI-отчёты показывают неверные цифры. Тесты — это просто SELECT-запросы, они не «откатывают» материализацию.
dbt build:
dbt build # порядок: seed -> run -> test -> snapshot, интерливед
Внутри: для каждой модели выполняется блок «материализовать -> запустить её тесты -> если что-то упало, пометить downstream как SKIP». Это и есть short-circuit upstream failures.
stg_orders материализуется OK, но её тест not_null падает. customers (downstream) пропускается. revenue_daily, который зависит от customers, тоже пропускается каскадом.
Результат: stg_orders материализована (это уже не откатить), но customers и revenue_daily остались со вчерашними данными. BI видит вчерашние числа — но не неверные. Лучше остановка, чем грязь.
В выводе это выглядит так:
$ dbt build
14:35:01 1 of 23 START seed file main.country_codes ........ [PASS in 0.12s]
14:35:01 3 of 23 START sql view model main.stg_jaffle__orders ........ [OK in 0.09s]
14:35:01 6 of 23 START test not_null_stg_jaffle__orders_order_id ........ [FAIL in 0.05s]
14:35:01 7 of 23 SKIP relation main.customers ........................... [SKIP]
14:35:01 8 of 23 SKIP relation main.orders .............................. [SKIP]
14:35:01 9 of 23 SKIP relation main.revenue_daily ....................... [SKIP]
14:35:01 Done. PASS=20 WARN=0 ERROR=1 SKIP=3 TOTAL=23
PASS=20 ERROR=1 SKIP=3 — упал один, и три каскадом скипнулись. CI должен зафейлить пайплайн при ERROR > 0.
Поведение при WARN
Не все тесты одинаково жёсткие. Можно настроить тесту severity: warn — тогда провал даст WARN, не ERROR, и downstream продолжит работать:
models:
- name: stg_orders
columns:
- name: customer_id
data_tests:
- relationships:
to: ref('stg_customers')
field: customer_id
config:
severity: warn # не блокировать на orphans
Запуск:
$ dbt build --select +stg_orders
... 1 of 5 WARN relationships_stg_orders_customer_id ... [WARN in 0.08s]
... downstream продолжается ...
Семантика: «хорошо бы это починить, но не пожар». Используется для мягких бизнес-правил. Хард-проверки (PK, NOT NULL) оставляй error.
dbt retry: продолжить с места падения
Сценарий: ты запустил dbt build в 9 утра на 300 моделей. На 150-й упала сетевая ошибка. К 9:45 заметил. Запускать всё заново — это ещё 45 минут.
dbt retry смотрит в target/run_results.json (артефакт последнего запуска), находит все узлы со статусом error или skipped, и запускает только их + их downstream:
$ dbt retry
14:55:01 Retrying 151 nodes (1 error + 150 skipped from previous run)
14:55:01 1 of 151 START sql view model main.stg_orders ........ [OK in 0.08s]
... 150 more ...
14:55:02 Done. PASS=151 WARN=0 ERROR=0 SKIP=0 TOTAL=151
Что важно:
dbt retryне пересчитывает уже успешные узлы — экономия времени линейная.- Для DAG-стороны: dbt смотрит, какие узлы не были запущены (SKIP), и запускает их в правильном топологическом порядке.
- Если предыдущий запуск был
dbt build,dbt retryтоже сделаетbuild(тесты + материализация). --targetи другие флаги используются те же, что были в исходном запуске (читаются изrun_results.json).
dbt retry особенно ценен при:
- Flaky warehouse connections (Snowflake/BigQuery бывает «отвалился сетевой пул»).
- Падение по out-of-memory на одной тяжёлой модели — фиксишь конфиг и retry, не пересчитывая остальное.
- Партиционные джобы, где пересчёт всего месяца занимает часы.
--full-refresh: переброс инкрементальных
Инкрементальные модели по умолчанию делают INSERT/MERGE новых строк, не пересчитывая историю. Это быстро, но если ты:
- изменил логику модели (новая колонка, новая агрегация, изменился фильтр)
- обнаружил баг и хочешь переcчитать историю
- сменил схему
unique_key
— нужно сказать dbt «пересчитай ВСЁ заново», то есть DROP TABLE + полный CREATE TABLE AS SELECT:
dbt run --select customers_incremental --full-refresh
С этим флагом инкрементальная модель ведёт себя как materialized='table': полная пересборка с нуля.
$ dbt run --select customers_incremental --full-refresh
14:58:11 1 of 1 START sql incremental model main.customers_incremental ... [RUN]
14:58:11 1 of 1 OK created sql incremental model main.customers_incremental ... [OK with 100000 rows in 12.4s]
Без флага модель работала бы инкрементом: «вставить только новые с last_updated_at > max(существующий)».
--full-refresh запускается на ВСЕ инкрементальные модели в селектора. Если запустить dbt run --full-refresh без --select — пересоберутся все инкрементальные. На большом проекте это может занять часы. Всегда сужай селектор: --select tag:hourly --full-refresh.
Конкретные модели можно защитить от --full-refresh через {{ config(full_refresh=false) }} — тогда даже с глобальным флагом эта модель сделает инкремент. Используется для исторических таблиц, которые слишком дорого пересобирать.
--sample (1.10 новинка)
В dbt 1.10 появился флаг --sample для time-based sampling. Это работает только с моделями, у которых задан event_time в {{ config() }}:
{{ config(
materialized='table',
event_time='order_date'
) }}
select * from {{ source('raw', 'orders') }}
Запуск с сэмплом за последние 7 дней:
dbt run --select orders --sample='7 days'
dbt добавит фильтр WHERE order_date >= current_date - interval '7 days' к запросу, не материализуя всю историю. Это не инкрементальная модель и не --full-refresh — это просто более узкий datalimit для локальной разработки.
Зачем: на проде в таблице 5 миллиардов строк. На локалке хочется быстро прогнать модель, чтобы проверить, что SQL правильный. С --sample='7 days' — это занимает секунды вместо часов.
$ dbt run --select revenue_daily --sample='30 days'
15:00:01 Running with dbt=1.10.2
15:00:01 Sampling: event_time filtered to last 30 days
15:00:01 1 of 1 START sql table model main.revenue_daily ... [OK in 4.2s]
Это не альтернатива инкрементальным моделям — после sampling в таблице будут только последние 30 дней, остальные данные потеряны. Используется только в локальной разработке. В CI/проде не запускают.
Combined: build + retry на больших проектах
Реальный production-сценарий:
Ночной 6:00 джоб: dbt build на 500 моделей. Длится 3 часа. В 7:30 упала modeлm int_payments_joined — OOM в DuckDB.
Утром 9:00: фиксишь конфиг этой модели (увеличил memory_limit, упростил join). Запускаешь:
dbt retry
dbt пересоберёт только int_payments_joined плюс 200 downstream. Это занимает 30 минут (вместо 3 часов с нуля). К 10:00 пайплайн зелёный.
Альтернативный путь: dbt build --select int_payments_joined+. Эффект тот же, но retry удобнее — не надо помнить, что упало.
Особенности dbt build для разных типов узлов
dbt build запускает:
- Seeds первыми — они не имеют зависимостей.
- Models — в порядке топологической сортировки DAG.
- Snapshots — после моделей, от которых они зависят.
- Tests — сразу после каждой модели, на которую они навешены. То есть generic-тест на
stg_ordersзапустится сразу послеstg_orders.sql, а не в конце.
Singular-тесты (файлы в tests/) запускаются после моделей, от которых они зависят через ref().
Тесты не идут после всех моделей — они идут сразу после своей модели. Это даёт short-circuit downstream немедленно при провале.
Если на шаге 3 тест провалился — шаги 4-5-6 пропускаются (SKIP). Это spaceous tests видны сразу после своей модели, не в конце.
Когда лучше НЕ использовать dbt build
Несколько сценариев, где dbt build не подходит:
-
Локальная разработка, где хочется быстрых итераций. Тесты замедляют. Используй
dbt run --select my_model, тесты запускай отдельно когда модель уже работает. -
Только тесты на свежей prod-копии.
dbt test --select state:modified+ --state prod— проверяет PR-изменения против актуальной prod-данных без пересборки. -
Только материализация без тестов (например, hourly-инкремент, тесты делаются раз в день):
dbt run --select tag:hourly. -
Compile-only для CI smoke check:
dbt compile— проверить, что Jinja валидна и SQL генерируется. Это самая быстрая проверка PR.
Попробуй сам
-
В Jaffle Shop добавь broken-тест в
_models.yml:models: - name: stg_orders columns: - name: order_id data_tests: - accepted_values: values: ['NEVER']Запусти
dbt build. Посмотри, какие модели стали SKIP. -
Запусти
dbt retry— что произошло? -
Убери broken-тест, запусти
dbt buildещё раз. Теперь добавьseverity: warnв существующий тест на staging — посмотри, как меняется поведение downstream. -
Создай простую инкрементальную модель:
`{{ config(materialized='incremental', unique_key='order_id') }}` select * from `{{ ref('stg_orders') }}` `{% if is_incremental() %}` where updated_at > (select max(updated_at) from `{{ this }}`) `{% endif %}`Запусти дважды без
--full-refresh, посмотри, что во второй раз только инкремент. Затем с--full-refresh— полная пересборка.
Чек-лист
- dbt build — production CI default. Тесты interleaved, при провале — SKIP downstream.
- dbt run + dbt test — НЕ используй в проде. Грязные данные в marts при провале теста.
- severity: warn на тесте — WARN не блокирует downstream.
- dbt retry — продолжить с места падения, не пересчитывая успешное.
- —full-refresh — пересобрать инкрементальные с нуля. Сужай через —select.
{{ config(full_refresh=false) }}— защита модели от глобального full-refresh.- —sample=‘7 days’ — time-based sampling для локалки (1.10+).
- В CI после изменения кода:
dbt build --select "state:modified+" --state ./prod-manifest. - В CI после фейла: подними артефакты, посмотри
target/run_results.json,dbt retryлокально для отладки.