Learning Platform
Глоссарий Troubleshooting
Урок 08.01 · 20 мин
Начальный
incrementalis_incremental()first runfull refresh

В прошлом модуле мы понимали, что table-материализация при каждом dbt run пересоздаёт таблицу с нуля. Это работает на малых данных, но становится медленным, когда таблица растёт. Если у тебя 100M строк events, пересчитывать всё каждые 15 минут — это часы compute и большие счета за warehouse.

Решение — incremental materialization. dbt при первом run создаёт таблицу полностью, при следующих runs — обрабатывает только новые/изменённые строки (дельту). Это самая важная и самая сложная materialization, которую должен освоить junior’у.

Главная идея incremental

dbt запоминает, что в таблице уже есть данные, и при следующем run обрабатывает только то, чего ещё нет. Конкретный механизм зависит от стратегии (append/merge/delete+insert — следующий урок), но базовая логика общая:

Incremental vs table: разница в работе dbt run

Table — каждый run пересчитывает всё. Incremental — первый run строит полностью, последующие добавляют только дельту. На больших таблицах разница в десятки и сотни раз по времени.

Tablerun 1: SELECT 100M, CREATE 100M
Tablerun 2: SELECT 100M, REPLACE 100M
Tablerun 3: SELECT 100M, REPLACE 100M
Incrementalrun 1: SELECT 100M, CREATE 100M
Incrementalrun 2: SELECT 1M (только новые), INSERT 1M
Incrementalrun 3: SELECT 1M (только новые), INSERT 1M

На таблице 100M строк, где каждый час прибавляется 1M:

  • table: каждый dbt run — 100M+. На 10 запусках в день = 1B обработанных строк.
  • incremental: первый run — 100M, дальше по 1M. На 10 запусках в день = 109M обработанных строк. На порядок дешевле.

Минимальный пример incremental-модели

TIP

Здесь и в следующих уроках встретится Jinja-синтаксис: {% if ... %} для условий и {{ ... }} для подстановки значений. Если впервые видишь — это шаблонизатор, который выполняется перед SQL и подменяет код. Полностью разбираем в модуле 10. Сейчас просто принимай как магию.

Простейшая incremental модель для таблицы orders с колонкой updated_at:

{{ config(
    materialized='incremental',
    unique_key='order_id'
) }}

SELECT *
FROM {{ source('jaffle_shop', 'raw_orders') }}

{% if is_incremental() %}
WHERE updated_at > (SELECT MAX(updated_at) FROM {{ this }})
{% endif %}

Разберём по строкам:

  • materialized='incremental' — говорит dbt использовать incremental-материализацию.
  • unique_key='order_id' — колонка, по которой dbt различает уникальные записи. Нужна для большинства стратегий (merge, delete+insert).
  • SELECT * FROM source(...) — основной запрос, который выполняется при первом run.
  • {'{% if is_incremental() %}'} — Jinja-условие, которое истинно только при инкрементальном run. То, что внутри блока, добавляется к запросу только когда таблица уже существует и это не —full-refresh.
  • WHERE updated_at > (SELECT MAX(updated_at) FROM {'{{'}'} this {'}}'} — фильтр, который оставляет только новые строки. {'{{'} this {'}}'} — ссылка на саму таблицу, которая уже существует в warehouse.

Что такое is_incremental()

is_incremental() — это macro, которая возвращает True, если выполняются четыре условия одновременно:

  1. Это инкрементальная модель (materialized=‘incremental’);
  2. Таблица уже существует в warehouse;
  3. Это не первый run (нет --full-refresh флага);
  4. Используется не unit test.

Если хотя бы одно условие False — is_incremental() вернёт False, и блок {'{% if is_incremental() %}'} пропустится.

Жизненный цикл incremental-модели

Первый run или --full-refresh: is_incremental()=False, выполняется полный SELECT с CREATE TABLE. Последующие run: is_incremental()=True, выполняется только дельта-фильтр с INSERT/MERGE.

dbt run первый разтаблицы нет
Falseis_incremental() возвращает False. Блок {% if is_incremental() %} пропускается. Выполняется CREATE TABLE AS полный SELECT.
CREATE TABLEполный пересчёт
dbt run второй разтаблица есть
Trueis_incremental() = True. Блок выполняется. Фильтр оставляет только новые строки. INSERT/MERGE.
INSERT новых строкинкремент
dbt run --full-refreshтаблица есть, но игнорим
Falseis_incremental() = False (из-за --full-refresh). Блок пропускается. DROP + CREATE полный.
DROP + CREATE TABLEполный пересчёт

--full-refresh: спасательный круг

Иногда нужно пересчитать incremental-таблицу с нуля. Причины:

  • Изменилась логика модели (например, новый расчёт или новая колонка).
  • Заметили баг — таблица содержит некорректные строки.
  • Нужно “перезагрузить” историю после изменения raw-данных.

Команда:

dbt run --select fact_orders --full-refresh

При запуске с --full-refresh dbt игнорирует существующую таблицу: делает DROP, выполняет полный SELECT без WHERE фильтра, создаёт новую.

WARNING

—full-refresh на больших таблицах — операция дорогая (тот же время, что dbt run на таблице с нуля). Не используй его в обычном расписании. Это инструмент для разработки и аварийного восстановления, не для регулярных runs.

Жизненный цикл: что ожидать

Пошаговый сценарий типичной incremental-модели:

Шаг 1: первый раз. Запускаешь dbt run --select fact_orders. dbt видит, что таблицы нет -> is_incremental()=False -> {'{% if is_incremental() %}'} блок пропускается -> выполняется полный SELECT -> CREATE TABLE. Таблица создаётся со всеми историческими данными.

Шаг 2: следующий run через час. Запускаешь dbt run --select fact_orders. dbt видит, что таблица есть -> is_incremental()=True -> блок выполняется -> WHERE фильтр оставляет только строки с updated_at > max(updated_at в таблице) -> результат пишется через INSERT/MERGE/etc.

Шаг 3: разработка. Меняешь SQL модели — добавляешь новую колонку. Запускаешь dbt run. Получаешь ошибку: “Schema mismatch: column X exists in model but not in target table”. Решение: dbt run --full-refresh, чтобы пересоздать с правильной схемой.

Шаг 4: восстановление. Загрузчик сломался и записал плохие данные. Ты исправил raw. Чтобы убрать плохие данные из incremental-таблицы — dbt run --full-refresh. Полный пересчёт со свежей raw.

Параметр on_schema_change

Когда меняешь схему модели (добавляешь колонку), incremental может конфликтовать с существующей таблицей. Параметр on_schema_change управляет поведением:

{{ config(
    materialized='incremental',
    unique_key='order_id',
    on_schema_change='fail'
) }}

Варианты:

  • 'fail' (default) — упасть с ошибкой при изменении схемы. Безопасно: ничего не сломается.
  • 'ignore' — игнорировать новые/удалённые колонки. Старые данные остаются как есть.
  • 'append_new_columns' — добавить новые колонки в существующую таблицу. Старые строки получат NULL в новых колонках.
  • 'sync_all_columns' — полностью синхронизировать схему (добавить и удалить).

В большинстве случаев 'fail' — правильный default. Если хочешь добавить колонку, делай это явно через --full-refresh или вручную меняй DDL.

TIP

Schema change — частая проблема в первые недели разработки incremental. Если планируешь часто менять SQL — на стадии разработки оставь materialized=‘table’. Когда логика стабилизируется и таблица растёт — переключи на incremental.

Что нельзя использовать в incremental

Несколько ловушек:

1. Оконные функции по всей истории. Если в SELECT написано ROW_NUMBER() OVER (ORDER BY created_at), при инкрементальном run dbt видит только новые строки, нумерация не учитывает всю историю. Решение: материализовать как table или специальная логика с lookup.

2. ORDER BY без LIMIT. Бесполезно в incremental — порядок не сохраняется при INSERT.

3. DISTINCT на исторических данных. Дедуп не работает между уже залитыми и новыми строками. Используй unique_key для дедупа.

4. Self-JOIN на полной истории. Аналогично окнам — JOIN происходит только на новых строках.

Если модель требует “посмотреть на всю историю” в одном запросе — она не подходит для incremental в чистом виде. Можно реализовать через lookup back window (читать историю + дельту), но это уже advanced.

Что в скомпилированном SQL

Сравним два варианта.

Первый run (target/run при первом запуске):

CREATE TABLE jaffle_shop.main.fact_orders AS (
    SELECT *
    FROM jaffle_shop.main.raw_orders
);

Никакого WHERE — потому что {'{% if is_incremental() %}'} оказался False, блок выпал.

Второй run (target/run после первого):

-- Стратегия по умолчанию для DuckDB — delete+insert; ниже упрощённо
DELETE FROM jaffle_shop.main.fact_orders
WHERE order_id IN (
    SELECT order_id FROM (
        SELECT *
        FROM jaffle_shop.main.raw_orders
        WHERE updated_at > (SELECT MAX(updated_at) FROM jaffle_shop.main.fact_orders)
    )
);

INSERT INTO jaffle_shop.main.fact_orders (
    SELECT *
    FROM jaffle_shop.main.raw_orders
    WHERE updated_at > (SELECT MAX(updated_at) FROM jaffle_shop.main.fact_orders)
);

Видишь WHERE с фильтром — это и есть рендер {'{% if is_incremental() %}'} блока. Конкретная стратегия (delete+insert vs merge) — в следующем уроке.

Команды для отладки

Посмотреть, сколько строк обработала incremental-модель:

dbt run --select fact_orders

Output:

14:23:01  1 of 1 START sql incremental model main.fact_orders ..... [RUN]
14:23:02  1 of 1 OK created sql incremental model main.fact_orders [INSERT 1245 in 0.45s]

“INSERT 1245” — это число новых строк. Сравни с “100M” при первом run — разница порядков. Если показывает INSERT 0 — нет новых строк, ничего не сделалось.

Чтобы пересчитать всё с нуля:

dbt run --select fact_orders --full-refresh

Output будет похож на первый run.

Попробуй сам

В своём проекте создай models/marts/fact_orders.sql:

{{ config(
    materialized='incremental',
    unique_key='order_id'
) }}

SELECT
    order_id,
    customer_id,
    amount,
    created_at,
    updated_at
FROM {{ source('jaffle_shop', 'raw_orders') }}

{% if is_incremental() %}
WHERE updated_at > (SELECT MAX(updated_at) FROM {{ this }})
{% endif %}

Запусти первый раз:

dbt run --select fact_orders

Должно появиться “INSERT N”, где N = число всех orders.

Запусти ещё раз без изменений в raw_orders:

dbt run --select fact_orders

Должно быть “INSERT 0” — ничего нового нет.

Добавь в raw_orders новую строку (например через INSERT INTO raw_orders VALUES (...) напрямую в DuckDB) и запусти dbt run снова. Увидишь “INSERT 1” — обработана только новая строка.

Затем:

dbt run --select fact_orders --full-refresh

Это будет полный пересчёт — обработаются все строки заново.

Что мы поняли

Incremental materialization обрабатывает только новые/изменённые строки, не пересчитывая всю таблицу. Ключевая Jinja-конструкция — {'{% if is_incremental() %}'}, истинная только при инкрементальном run. --full-refresh пересоздаёт таблицу с нуля — для разработки и восстановления. on_schema_change контролирует поведение при изменении схемы модели.

В следующем уроке углубимся в unique_key — параметр, который определяет, как dbt решает, что обновить, а что добавить.

Incremental models: deep dive в стратегии и production-философию Batch-обработка: окна, расписание, идемпотентность
Проверка знанийKnowledge check
Ты добавил в модель incremental новую колонку user_segment, и dbt run падает с ошибкой 'Schema mismatch'. Что делать?
ОтветAnswer
Параметр on_schema_change='fail' (default) не позволяет автоматически менять схему существующей таблицы. Варианты: (1) dbt run --full-refresh — пересоздаст таблицу с новой схемой, потеряя историю и потратив время на полный пересчёт; (2) on_schema_change='append_new_columns' — добавит колонку автоматически, в старых строках будет NULL; (3) on_schema_change='sync_all_columns' — синхронизирует обе стороны. Для важных таблиц лучше явный --full-refresh с пониманием, что произойдёт.

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

Результат: 0 из 0
Концептуальный
Вопрос 1 из 6. Когда is_incremental() возвращает True?

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

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

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

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