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:
- Читает дельту (5M строк).
- Для каждой строки в дельте — ищет matching в target.
- Если 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.
Эту ошибку легко не заметить: 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 должен:
- Совпадать с partition column warehouse-таблицы (BigQuery
partition by, Snowflakecluster by, etc.). Тогда predicate активирует partition pruning. - Использовать колонку, по которой данные приходят — обычно
created_at,updated_at,event_timestamp. - Иметь окно достаточно широкое для 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 дней”) — это компромисс:
Узкое окно — быстрый run, но риск пропуска late-data. Широкое окно — медленнее, но безопаснее.
Production-обычай — 7 дней для большинства случаев. Это покрывает Fivetran-задержки (обычно до 24-48 часов) и редкие late-arriving события с запасом.
Проверка, что predicate работает
После добавления incremental_predicates:
- Запустите
dbt compile --select my_model— посмотрите generated SQL. - Скопируйте MERGE-statement в warehouse query editor.
- Запустите
EXPLAIN(Snowflake:EXPLAIN USING TABULAR, BigQuery: dry run в UI). - Проверьте, что
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 — 80, или $2400 в месяц.
После: добавили incremental_predicates=["DBT_INTERNAL_DEST.event_date > current_date - 7"]. Каждый dbt run — 3 минуты. Cost — 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, или вообще другая архитектура).
Попробуй сам
- Выберите вашу самую большую incremental-модель. Замерьте текущее время run.
- Добавьте
incremental_predicatesс date window 7 дней по правильной колонке. - Используйте
DBT_INTERNAL_DEST.prefix явно. - Запустите
dbt run, замерьте новое время. - На warehouse запустите EXPLAIN скомпилированного MERGE — убедитесь, что partition pruning активен.
Ожидаемый результат — 5-20x ускорение на BigQuery/Snowflake с partitioning, 2-5x на DuckDB.