Learning Platform
Глоссарий Troubleshooting
Урок 18.04 · 22 мин
Начальный
dbt builddbt retry--full-refresh--sampleShort-circuitRecovery

В первом уроке мы пробежались по 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.

dbt build: short-circuit на провале теста

stg_orders материализуется OK, но её тест not_null падает. customers (downstream) пропускается. revenue_daily, который зависит от customers, тоже пропускается каскадом.

stg_ordersOK материализовалась
test not_nullFAIL — order_id NULL
customersSKIP (upstream fail)
revenue_dailySKIP (cascade)

Результат: 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(существующий)».

WARNING

--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().

dbt build временной порядок

Тесты не идут после всех моделей — они идут сразу после своей модели. Это даёт short-circuit downstream немедленно при провале.

Если на шаге 3 тест провалился — шаги 4-5-6 пропускаются (SKIP). Это spaceous tests видны сразу после своей модели, не в конце.


Когда лучше НЕ использовать dbt build

Несколько сценариев, где dbt build не подходит:

  1. Локальная разработка, где хочется быстрых итераций. Тесты замедляют. Используй dbt run --select my_model, тесты запускай отдельно когда модель уже работает.

  2. Только тесты на свежей prod-копии. dbt test --select state:modified+ --state prod — проверяет PR-изменения против актуальной prod-данных без пересборки.

  3. Только материализация без тестов (например, hourly-инкремент, тесты делаются раз в день): dbt run --select tag:hourly.

  4. Compile-only для CI smoke check: dbt compile — проверить, что Jinja валидна и SQL генерируется. Это самая быстрая проверка PR.


Попробуй сам

  1. В Jaffle Shop добавь broken-тест в _models.yml:

    models:
      - name: stg_orders
        columns:
          - name: order_id
            data_tests:
              - accepted_values:
                  values: ['NEVER']

    Запусти dbt build. Посмотри, какие модели стали SKIP.

  2. Запусти dbt retry — что произошло?

  3. Убери broken-тест, запусти dbt build ещё раз. Теперь добавь severity: warn в существующий тест на staging — посмотри, как меняется поведение downstream.

  4. Создай простую инкрементальную модель:

    `{{ 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 локально для отладки.
Slim CI: state:modified+ в GitHub Actions Идемпотентность pipeline-ов
Проверка знанийKnowledge check
У вас инкрементальная модель orders_fact, которая считает revenue по сложной логике (10 джойнов). На проде она 200M строк, пересборка с нуля занимает 3 часа. Аналитик нашёл баг: фильтр на статусе исключал слишком много. Исправил SQL. Что нужно сделать, чтобы починить исторические данные, и почему просто dbt run --select orders_fact не сработает?
ОтветAnswer
Просто dbt run --select orders_fact выполнит инкрементальный путь — обновит только последние записи по фильтру is_incremental(). Исторические данные останутся посчитанными по старой (неправильной) логике. Чтобы пересчитать с нуля, нужен флаг --full-refresh: dbt run --select orders_fact --full-refresh. Это сделает DROP TABLE + CREATE TABLE AS SELECT, применит исправленную логику ко всей истории. На 200M это 3 часа — можно запустить ночью. Если хочется проверить SQL до полной пересборки, в 1.10 есть --sample='30 days' — пересобрать только последний месяц, убедиться, что бизнес-логика правильная, потом запустить --full-refresh без --sample. Также если потеряли данные критичны — снапшот таблицы до пересборки (CREATE TABLE orders_fact_backup AS ...) спасёт если новый код тоже окажется неверным.

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

Результат: 0 из 0
Прикладной
Вопрос 1 из 6. Внутри dbt build тест на staging-модели stg_orders провалился. Что произошло с downstream-моделями customers и revenue_daily?

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

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

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

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