Инкрементальные модели: только новые данные
Когда таблица слишком велика, чтобы пересчитывать целиком, — dbt дописывает только новое.
Инкрементальная модель — материализация, при которой dbt при первом запуске строит таблицу целиком, а при последующих обрабатывает и дописывает только новые/изменённые строки.
Проблема масштаба
Таблица событий в миллиард строк растёт каждый день. Пересчитывать её целиком при каждом dbt run — это часы работы и большие счета за вычисления, при том что вчерашние данные не изменились. Логично трогать только то, что появилось со вчера. Это и делает инкрементальная материализация.
Анатомия инкрементальной модели
-- models/marts/events_daily.sql
{{ config(
materialized='incremental',
unique_key='event_id'
) }}
select
event_id,
user_id,
event_type,
created_at
from {{ source('raw', 'events') }}
{% if is_incremental() %}
-- этот блок только при инкрементальном запуске
where created_at > (select max(created_at) from {{ this }})
{% endif %}
Разберём ключевые элементы:
materialized='incremental' | выбирает инкрементальную стратегию |
unique_key='event_id' | как опознать дубль для обновления, а не задвоения |
is_incremental() | true только если таблица уже существует (не первый запуск) |
{{ this }} | ссылка на саму строящуюся таблицу (её текущее состояние) |
Как идёт первый и последующий запуски
Первый run (таблицы ещё нет):
is_incremental() = false --> блок WHERE пропущен
--> обрабатываются ВСЕ события, строится полная таблица
Каждый следующий run (таблица есть):
is_incremental() = true --> WHERE created_at > max(created_at)
--> берутся только новые события, дописываются в таблицу
Зачем unique_key
Без ключа dbt просто дописывал бы строки (insert). Если событие могло прийти повторно или обновиться, получились бы дубли. unique_key говорит dbt: при совпадении ключа — заменить старую строку новой (merge/upsert), а не плодить копии. Это защищает от задвоений при пересекающихся загрузках.
Как работает под капотом
На первом запуске dbt делает обычный create table as select. На последующих он выполняет ваш SELECT с фильтром по is_incremental() во временную таблицу, а затем сливает её в основную: если задан unique_key — через merge (обновить совпавшие, вставить новые), если нет — простым insert. Полный пересчёт всё равно возможен командой dbt run --full-refresh — она дропает таблицу и строит с нуля (нужно, например, после смены логики модели).
# Обычный инкрементальный запуск (только новое)
dbt run --select events_daily
# Полная перестройка с нуля
dbt run --select events_daily --full-refresh
Частые ошибки
- Забыть
is_incremental(). Тогда даже при инкрементальной материализации dbt каждый раз будет читать весь источник — выгода пропадёт. - Не задать
unique_keyтам, где данные могут обновляться. Получите дубли вместо обновлений. - Неверный фильтр свежести. Слишком узкий
WHEREпропустит «опоздавшие» события; добавляют запас (lookback) на несколько часов. - Менять логику и забыть
--full-refresh. Старые строки останутся посчитанными по-старому.
Итоги
- Инкрементальная модель строит таблицу целиком один раз, потом дописывает только новое — экономия времени и денег на больших таблицах.
is_incremental()включает фильтр свежести,unique_keyзащищает от дублей через merge.--full-refreshперестраивает таблицу с нуля при смене логики.