Инкрементальные модели: только новые данные

Когда таблица слишком велика, чтобы пересчитывать целиком, — 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 перестраивает таблицу с нуля при смене логики.
Проверьте себя
1. Что делает инкрементальная модель при последующих (не первых) запусках?
AПересчитывает всю таблицу заново
BОбрабатывает и дописывает только новые/изменённые строки
CУдаляет таблицу
DСоздаёт новую базу
2. Зачем в инкрементальной модели нужен unique_key?
AДля ускорения SELECT
BЧтобы при совпадении ключа обновлять строку (merge), а не плодить дубли
CЧтобы зашифровать данные
DЭто обязательное поле любой модели
3. Когда понадобится dbt run --full-refresh для инкрементальной модели?
AНикогда
BПосле изменения логики модели, чтобы перестроить таблицу с нуля
CПри каждом запуске
DТолько в dev