Learning Platform
Глоссарий Troubleshooting
Урок 08.03 · 19 мин
Начальный
appenddelete+insertmergeincremental_strategyDuckDB 1.4

В предыдущих уроках мы изучили концепцию incremental и роль unique_key. Теперь — детали реализации: какой именно DDL/DML dbt отправляет в warehouse при инкрементальном run.

Это управляется параметром incremental_strategy. На DuckDB доступны три варианта: append, delete+insert, merge. Каждая со своими гарантиями, ограничениями и performance.

append: самая простая, без updates

Стратегия append просто INSERT’ит новые строки. Не делает никакого дедупа. Это значит, если в источнике уже есть строка с тем же ключом, что и в таблице, она запишется как дубликат.

{{ config(
    materialized='incremental',
    incremental_strategy='append'
) }}

SELECT *
FROM {{ source('analytics', 'page_views') }}

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

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

INSERT INTO target.page_views
SELECT * FROM source.page_views
WHERE event_time > (SELECT MAX(event_time) FROM target.page_views);

Никаких DELETE, MERGE — только INSERT. unique_key игнорируется в этой стратегии.

Когда append лучший выбор:

  • Append-only данные (event logs, page views, transactions без updates).
  • Каждая строка в источнике уникальна и никогда не меняется.
  • Производительность критична — INSERT самая дешёвая операция.

Когда append плохой выбор:

  • Источник может обновлять строки (orders с changing status).
  • В дельте могут быть повторы из-за late-arriving events.

delete+insert: дедупликация через DELETE

Стратегия delete+insert работает в два шага:

  1. DELETE FROM target WHERE unique_key IN (новые ключи) — удаляет существующие строки с этими ключами.
  2. INSERT INTO target (новые данные) — вставляет новые строки.

В результате старые версии удалены, новые вставлены — эффект upsert.

{{ config(
    materialized='incremental',
    incremental_strategy='delete+insert',
    unique_key='order_id'
) }}

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

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

Скомпилируется в (упрощённо):

-- Шаг 1
DELETE FROM target.orders
WHERE order_id IN (
    SELECT order_id FROM source.orders
    WHERE updated_at > (SELECT MAX(updated_at) FROM target.orders)
);

-- Шаг 2
INSERT INTO target.orders
SELECT * FROM source.orders
WHERE updated_at > (SELECT MAX(updated_at) FROM target.orders);

Когда delete+insert хороша:

  • Нужен upsert (обновление + добавление).
  • Warehouse не поддерживает MERGE (DuckDB до 1.4).
  • Простая логика, легко дебагать.

Минусы:

  • Две операции вместо одной — не атомарно, между DELETE и INSERT короткое окно с потерянными строками.
  • На большом числе ключей DELETE WHERE col IN (…) может быть медленным.
NOTE

delete+insert — стратегия по умолчанию на dbt-duckdb для incremental с unique_key (до версии где появился merge). На DuckDB она работает быстро для большинства задач. На некоторых warehouse есть свои дефолты — например, BigQuery предпочитает merge.

merge: атомарный upsert (DuckDB 1.4+)

Стратегия merge использует SQL-конструкцию MERGE — атомарную операцию upsert, появившуюся в DuckDB 1.4.

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

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

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

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

MERGE INTO target.orders AS target
USING (
    SELECT * FROM source.orders
    WHERE updated_at > (SELECT MAX(updated_at) FROM target.orders)
) AS source
ON target.order_id = source.order_id
WHEN MATCHED THEN UPDATE SET ...
WHEN NOT MATCHED THEN INSERT ...;

Одна операция — атомарная. Преимущества:

  • Атомарность: либо всё применилось, либо ничего. Без промежуточного состояния.
  • Часто быстрее delete+insert на больших данных.
  • Понятная семантика SQL.

Минусы:

  • Требует DuckDB 1.4+. На более старых — ошибка компиляции.
  • На очень больших таблицах может потребовать настройки memory.
WARNING

Если используешь dbt-duckdb с merge и при run получаешь “Parser Error: Unknown statement type MERGE” — у тебя DuckDB старее 1.4. Обнови duckdb (pip install -U duckdb) или используй стратегию delete+insert.

Сравнение стратегий

Append vs delete+insert vs merge

Append — самый быстрый, но без updates. delete+insert — upsert через два шага, не атомарный. merge — атомарный upsert, нужен DuckDB 1.4+. Выбор зависит от формы данных и версии DuckDB.

appendINSERT only
speed: fastestneeds unique_key: no
best for: event logs
delete+insertDELETE then INSERT
speed: mediumneeds unique_key: yes
best for: upsert pre-1.4
mergeatomic MERGE
speed: fast on large dataneeds unique_key: yes
best for: upsert on DuckDB 1.4+

Когда какую стратегию

append — выбирай, когда:

  • Данные append-only (events, transactions).
  • Не нужно обновлять существующие строки.
  • Хочешь максимальной производительности.

delete+insert — выбирай, когда:

  • Нужен upsert, и DuckDB менее 1.4.
  • Хочешь явно видеть DELETE+INSERT в логах для дебага.
  • Объёмы небольшие, не критично.

merge — выбирай, когда:

  • Нужен upsert, и DuckDB 1.4+ доступен.
  • Хочешь атомарность.
  • Большие объёмы, важна производительность.

В сомнениях: на свежем DuckDB используй merge. Это стандарт индустрии (Snowflake/BigQuery тоже предпочитают merge).

—full-refresh: общий для всех стратегий

dbt run --full-refresh обходит любую стратегию: dbt делает DROP + CREATE TABLE AS SELECT с полным запросом без {'{% if is_incremental() %}'} блока. После full-refresh структура таблицы возможно поменялась — этот режим часто используется при изменении SQL модели.

dbt run --select fact_orders --full-refresh

Подходит как для append, так и для merge — стратегия применяется только к инкрементальным runs.

Конфигурация incremental_strategy

Можно задать в самой модели:

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

Или в dbt_project.yml как default для всех incremental в папке:

models:
  jaffle_shop:
    marts:
      +materialized: incremental
      +incremental_strategy: merge

config в SQL переопределяет YAML, как и для других параметров.

Что произойдёт, если стратегия не указана

Если incremental_strategy не указан:

  • С unique_key — dbt берёт default для адаптера (на dbt-duckdb обычно delete+insert, на новых версиях возможно merge).
  • Без unique_key — dbt берёт append.

Лучше явно указывать стратегию, чтобы не зависеть от defaults адаптера.

Дополнительные параметры стратегий

merge_exclude_columns (для merge) — список колонок, которые НЕ обновлять при MATCHED:

{{ config(
    materialized='incremental',
    incremental_strategy='merge',
    unique_key='order_id',
    merge_exclude_columns=['inserted_at']
) }}

Полезно для аудит-колонок: inserted_at должна сохранять время первого insert, а не каждого update.

merge_update_columns (для merge) — наоборот, явный список колонок для UPDATE:

{{ config(
    materialized='incremental',
    incremental_strategy='merge',
    unique_key='order_id',
    merge_update_columns=['status', 'updated_at']
) }}

Используется реже, но полезно для строгого контроля.

DuckDB-специфика: концентрированный список

  1. append всегда работает — даже на самых старых DuckDB.
  2. delete+insert работает с любой версии, активно использовался до 1.4.
  3. merge требует DuckDB 1.4+ — pip install -U duckdb обновит.
  4. microbatch (1.9+) НЕ ПОДДЕРЖИВАЕТ unique_key в dbt-duckdb. Это ограничение адаптера, на других warehouse поддерживается. Об этом — в следующем уроке.
  5. Single writer per file: incremental на одной DuckDB-файле работает корректно, конкуррентный dbt run не нужен.

Пример: переход append -> merge

Допустим, у тебя fact_orders на стратегии append. Поняв, что данные могут обновляться, решил перейти на merge:

-- Было
{{ config(
    materialized='incremental',
    incremental_strategy='append'
) }}

-- Стало
{{ config(
    materialized='incremental',
    incremental_strategy='merge',
    unique_key='order_id'
) }}

После изменения нужен --full-refresh, чтобы:

  1. Убрать накопленные дубли из таблицы.
  2. Пересчитать с merge-стратегией с нуля.
dbt run --select fact_orders --full-refresh

После этого следующие runs будут уже инкрементальными с merge-стратегией.

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

Посмотреть, какая стратегия применена:

dbt list --select fact_orders --output json | jq '.[] | {name, config: .config | {materialized, incremental_strategy, unique_key}}'

Output:

{
  "name": "fact_orders",
  "config": {
    "materialized": "incremental",
    "incremental_strategy": "merge",
    "unique_key": "order_id"
  }
}

Скомпилированный SQL в target/run/<project>/models/.../fact_orders.sql покажет финальный DDL — DELETE/INSERT или MERGE.

Попробуй сам

Создай incremental-модель с разными стратегиями. Создай models/marts/test_append.sql:

{{ config(
    materialized='incremental',
    incremental_strategy='append'
) }}

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

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

Запусти несколько раз с разными настройками. Проверь:

SELECT COUNT(*), COUNT(DISTINCT order_id) FROM test_append;

Если COUNT > COUNT DISTINCT — это дубли из-за append.

Создай models/marts/test_merge.sql с теми же данными, но incremental_strategy='merge' и unique_key='order_id'. Запусти несколько раз. Должны быть равны COUNT и DISTINCT.

Сравни target/run/…/test_append.sql и target/run/…/test_merge.sql — увидишь разный финальный SQL.

Что мы поняли

Incremental strategies определяют способ применения дельты: append (только INSERT, без дедупа), delete+insert (DELETE matching + INSERT all, не атомарный, default для DuckDB до 1.4), merge (атомарный MERGE, требует DuckDB 1.4+). Стратегия выбирается через incremental_strategy параметр или в dbt_project.yml. На свежем DuckDB merge — рекомендуемый дефолт для upsert’ов. —full-refresh обходит любую стратегию.

В следующем уроке — microbatch, специальная стратегия для time-based incremental, появилась в dbt 1.9.

delete+insert vs merge: deep dive на уровне сгенерированного SQL Data-modifying CTE: INSERT/UPDATE/DELETE внутри WITH
Проверка знанийKnowledge check
Ты используешь incremental_strategy='merge' с unique_key='order_id' на DuckDB 1.3. dbt run падает с 'Parser Error: Unknown statement type MERGE'. Какие два решения?
ОтветAnswer
(1) Обновить DuckDB до 1.4+: pip install -U duckdb (или указать в requirements.txt). MERGE появился именно в 1.4. (2) Переключить стратегию на delete+insert — она работает с любой версии DuckDB, эффект upsert тот же, просто не атомарный и через две операции. На production-проекте предпочтительнее обновить DuckDB; для legacy/staging — delete+insert как костыль.

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

Результат: 0 из 0
Концептуальный
Вопрос 1 из 6. В чём ключевое отличие стратегии append от delete+insert?

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

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

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

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