Learning Platform
Глоссарий Troubleshooting
Урок 03.04 · 25 мин
Средний
incremental_predicatesMERGE optimizationDBT_INTERNAL_DESTpartition pruning

incremental_predicates: оптимизация MERGE и DELETE

В прошлом уроке мы видели тонкость: dbt-генерированные DELETE и MERGE по умолчанию сканируют всю target-таблицу. На таблице 100M строк это терпимо. На 1B+ — катастрофа: dbt run, который должен занимать минуту, занимает полчаса, и при этом блокирует BI-дашборды на этом таблице.

Решение — incremental_predicates. Это параметр, который позволяет добавить дополнительный WHERE-фильтр в DELETE/MERGE. Если правильно настроить, time-to-update падает в 50-100x на больших таблицах. Это главный production knob для incremental моделей.

Зачем нужны incremental_predicates

Возьмём типичную MERGE-операцию:

MERGE INTO analytics.events AS target
USING <delta> AS source
ON target.event_id = source.event_id
WHEN MATCHED THEN UPDATE ...
WHEN NOT MATCHED THEN INSERT ...;

Что делает warehouse:

  1. Читает дельту (5M строк).
  2. Для каждой строки в дельте — ищет matching в target.
  3. Если target имеет 1B строк, warehouse сканирует через все 1B, чтобы найти matches.

Для warehouse’ов с partitioning (BigQuery, Snowflake): даже с partition pruning warehouse не знает, какие партиции имеют matching, и обычно сканирует все. Это scan через 1B строк ради 5M обновлений.

incremental_predicates говорит warehouse: «не ищи matches в всём target, ищи только в последних 7 днях». Это превращает full scan в partition scan:

{{ config(
    materialized='incremental',
    incremental_strategy='merge',
    unique_key='event_id',
    incremental_predicates=[
        "DBT_INTERNAL_DEST.event_timestamp > current_date - 7"
    ]
) }}

Что под капотом

С incremental_predicates dbt компилирует MERGE так:

MERGE INTO analytics.events AS DBT_INTERNAL_DEST
USING <delta> AS DBT_INTERNAL_SOURCE
ON DBT_INTERNAL_DEST.event_id = DBT_INTERNAL_SOURCE.event_id
   AND DBT_INTERNAL_DEST.event_timestamp > current_date - 7  -- predicate
WHEN MATCHED THEN UPDATE ...
WHEN NOT MATCHED THEN INSERT ...;

Predicate добавляется к ON-clause. Warehouse теперь сканирует только строки target за последние 7 дней — даже если в target 1B исторических строк, scan видит только 5-10M.

На BigQuery с partition by event_date это активирует partition pruning — warehouse физически не читает старые партиции. Cost экономия колоссальная.

DBT_INTERNAL_DEST — ключевой prefix

В predicate всегда префиксируйте колонки DBT_INTERNAL_DEST. — это alias, который dbt даёт target-таблице в MERGE.

-- ХОРОШО — explicit DBT_INTERNAL_DEST prefix
incremental_predicates=[
    "DBT_INTERNAL_DEST.event_timestamp > current_date - 7"
]

-- ПЛОХО — без prefix
incremental_predicates=[
    "event_timestamp > current_date - 7"
]

Без prefix’а predicate может быть ambiguous (warehouse не знает, к target или к source применять), и в худшем случае silent skip predicate — warehouse игнорирует условие, и вы получаете обычный full scan без warning’а в dbt run output.

WARNING

Эту ошибку легко не заметить: dbt run пройдёт, MERGE отработает, результат будет корректным. Но perf-выигрыш нулевой, потому что predicate не применился. Всегда EXPLAIN финальный MERGE на warehouse и проверяйте, что partition pruning активен.

incremental_predicates для delete+insert

incremental_predicates работает и со стратегией delete+insert, но логика чуть другая. dbt добавляет predicate к DELETE-statement:

{{ config(
    materialized='incremental',
    incremental_strategy='delete+insert',
    unique_key='order_id',
    incremental_predicates=[
        "DBT_INTERNAL_DEST.updated_at > current_date - 3"
    ]
) }}

Компилируется в:

DELETE FROM analytics.orders AS DBT_INTERNAL_DEST
WHERE order_id IN (SELECT order_id FROM <delta>)
  AND DBT_INTERNAL_DEST.updated_at > current_date - 3;

DELETE сканирует только последние 3 дня target. Огромная экономия на больших таблицах.

Какой predicate выбрать

Predicate должен:

  1. Совпадать с partition column warehouse-таблицы (BigQuery partition by, Snowflake cluster by, etc.). Тогда predicate активирует partition pruning.
  2. Использовать колонку, по которой данные приходят — обычно created_at, updated_at, event_timestamp.
  3. Иметь окно достаточно широкое для late-arriving data. Если данные могут опаздывать на 2 дня, predicate > current_date - 1 отсечёт половину updates.

Типичные predicates:

-- Для event-таблиц с partition by event_date:
"DBT_INTERNAL_DEST.event_timestamp > current_date - 7"

-- Для order-таблиц с updated_at:
"DBT_INTERNAL_DEST.updated_at > dateadd(day, -3, current_timestamp)"

-- Composite predicate (для multi-partition setup):
[
    "DBT_INTERNAL_DEST.event_date > current_date - 7",
    "DBT_INTERNAL_DEST.tenant_id IN (SELECT distinct tenant_id FROM " ~ ref('stg_active_tenants') ~ ")"
]

Composite predicate — это массив строк, dbt их объединяет через AND. Используется когда нужно несколько фильтров: partition + tenant + status.

Как выбрать окно

Окно (например, “последние 7 дней”) — это компромисс:

Window trade-off в incremental_predicates

Узкое окно — быстрый run, но риск пропуска late-data. Широкое окно — медленнее, но безопаснее.

1 деньОчень быстро. Подходит для real-time данных без late-arriving. Риск — пропуск любого опоздавшего event
7 днейProduction default. Покрывает типичные delays (Fivetran sometimes delays a day or two). Compute умеренный
30 днейБезопасно для месячных back-fills. Compute заметный, но всё ещё лучше full scan
365 днейОчень широко. Близко к full scan на годовой таблице. Подходит только когда late-data сильно опаздывают

Production-обычай — 7 дней для большинства случаев. Это покрывает Fivetran-задержки (обычно до 24-48 часов) и редкие late-arriving события с запасом.

Проверка, что predicate работает

После добавления incremental_predicates:

  1. Запустите dbt compile --select my_model — посмотрите generated SQL.
  2. Скопируйте MERGE-statement в warehouse query editor.
  3. Запустите EXPLAIN (Snowflake: EXPLAIN USING TABULAR, BigQuery: dry run в UI).
  4. Проверьте, что Partitions scanned или Partitions pruned показывает скан только последних 7 дней.

Если scanned partitions = total partitions — predicate не работает. Проверьте DBT_INTERNAL_DEST prefix, правильность column name, тип данных.

Real example: 10x speedup

Production-кейс с проекта 2025:

До: incremental MERGE на 1B-таблице events. Каждый dbt run — 35 минут. Cost — 8наrun.10runsвдень—8 на run. 10 runs в день — 80, или $2400 в месяц.

После: добавили incremental_predicates=["DBT_INTERNAL_DEST.event_date > current_date - 7"]. Каждый dbt run — 3 минуты. Cost — 0.50наrun.10runsвдень—0.50 на run. 10 runs в день — 5, или $150 в месяц.

Выигрыш: 16x по скорости, 16x по cost. Месячная экономия $2250. Изменение в коде — 1 строка config.

Это типичный production-результат. incremental_predicates — knob с наибольшим ROI среди всех dbt-оптимизаций.

DuckDB-специфика

  • incremental_predicates работает на DuckDB.
  • DuckDB не имеет native partitioning как BigQuery, но всё равно benefit от predicates: warehouse использует zone maps и predicate pushdown.
  • Для DuckLake (DuckDB partitioned tables, новый формат 2026) predicates активируют partition pruning.
  • Прямое сравнение: на DuckDB локально incremental_predicates даёт обычно 3-5x speedup. На Snowflake/BQ — 10-100x благодаря partition pruning.

Production gotchas

1. Predicate с wrong column

Если colonna в predicate не индексирована и не partitioned, warehouse всё равно делает full scan, просто потом фильтрует. Perf-выигрыша нет. Решение — predicate ВСЕГДА по partition/clustering column.

2. Tiny window для late-data scenarios

Predicate > current_date - 1 на таблице с Fivetran-загрузкой раз в 12 часов отсечёт строки, которые загрузились 30 часов назад. В target пропустятся updates за 6 часов окна. Через неделю downstream метрики отличаются на этот пропуск. Решение — окно >= 3 дней для Fivetran, >= 7 для Airbyte/Kafka задержек.

3. Predicate, который зависит от source данных

Соблазн — написать predicate на основе delta-данных:

-- ПЛОХО: predicate зависит от source
incremental_predicates=[
    "DBT_INTERNAL_DEST.tenant_id IN (SELECT distinct tenant_id FROM " ~ ref('stg_orders') ~ ")"
]

Это компилируется в subquery в MERGE, warehouse делает это medium-cost’ом. Лучше — статический predicate (date range), который warehouse оптимизатор гарантированно поймёт.

4. Забыли DBT_INTERNAL_DEST prefix

Уже упоминал, но повторюсь — это самая частая ошибка. Predicate тихо игнорируется, perf-выигрыш нулевой, дни уходят на debug “почему не быстрее”.

5. Predicate в config, но modeled column нет

Predicate ссылается на event_timestamp, но в SELECT-statement модели колонки event_timestamp нет (есть только created_at). MERGE упадёт на стадии runtime: column event_timestamp not found in DBT_INTERNAL_DEST. Решение — убедиться, что predicate-колонки есть в SELECT.

Когда predicate НЕ нужен

dbt-iii: performance at scale — selector-based и partition pruning
  • На таблицах меньше 100M строк — overhead настройки не окупается, обычный MERGE достаточен.
  • На full-refresh таблицах — predicate не применяется при --full-refresh.
  • Когда дельта большая (>10% таблицы) — даже с predicate scan велик, надо думать о другой стратегии (microbatch, partitioning, или вообще другая архитектура).

Попробуй сам

  1. Выберите вашу самую большую incremental-модель. Замерьте текущее время run.
  2. Добавьте incremental_predicates с date window 7 дней по правильной колонке.
  3. Используйте DBT_INTERNAL_DEST. prefix явно.
  4. Запустите dbt run, замерьте новое время.
  5. На warehouse запустите EXPLAIN скомпилированного MERGE — убедитесь, что partition pruning активен.

Ожидаемый результат — 5-20x ускорение на BigQuery/Snowflake с partitioning, 2-5x на DuckDB.

Проверка знанийKnowledge check
Команда добавила incremental_predicates на 1B-таблицу events: "event_timestamp > current_date - 7". Изменений в run time не наблюдают. Что могли упустить и как диагностировать?
ОтветAnswer
Самые частые причины 'predicate без эффекта' — пять штук. Первое — забыли DBT_INTERNAL_DEST. prefix. predicate должен быть 'DBT_INTERNAL_DEST.event_timestamp > current_date - 7', не просто 'event_timestamp > ...'. Без prefix warehouse не знает, к target или source применять, и тихо игнорирует. Это самая частая ошибка. Второе — колонка event_timestamp не partitioned/clustered на warehouse. Predicate синтаксически валидный, но warehouse делает full scan и потом фильтрует — perf-выигрыша нет. Решение — partition by event_date в warehouse-таблице. Третье — predicate column не существует в target. Если в SELECT нет event_timestamp (есть только created_at), MERGE упадёт с column not found. Если падения нет, значит predicate проигнорирован. Четвёртое — окно слишком широкое для эффекта. Если дельта 5M строк, а predicate смотрит на последние 30 дней (300M строк target), скан 60x меньше чем 1B, но всё равно медленный. Сузить окно до 7 дней. Пятое — incremental_strategy='append'. На append predicate не применяется, потому что нет MERGE/DELETE. Диагностика — запустить 'dbt compile --select model', взять generated SQL, скопировать в warehouse, сделать EXPLAIN. Если 'Partitions scanned' = total — predicate не работает. Если 'Partitions scanned' = 7/365 — работает, но дальше ищем bottleneck в другом месте (source scan, hash join, etc).

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

Результат: 0 из 0
Прикладной
Вопрос 1 из 6. Зачем нужен префикс DBT_INTERNAL_DEST в incremental_predicates?

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

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

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

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