# Идемпотентность в dbt

Идемпотентность — одна из основополагающих целей любого проекта dbt. Она означает, что ваши преобразования можно безопасно запускать повторно и они всегда будут давать один и тот же результат независимо от количества предыдущих запусков.

Почему это важно с самого начала

Если вы только начинаете работать с dbt, стоит разобраться в идемпотентности до создания инкрементальных моделей. Легко случайно написать логику, которая корректно работает при первом запуске, но приводит к ошибкам при повторных.

## Что означает идемпотентность в dbt

Модель dbt идемпотентна, если она даёт одинаковые результаты независимо от того, запускаете вы её один раз или сто раз. Итоговое состояние данных должно быть одинаковым независимо от количества запусков модели.

Это объясняется тем, что каждая модель — чистая функция от вашего кода и текущего состояния базы данных, а не от количества предыдущих запусков. При одинаковых входных данных она всегда выдаёт одинаковый результат.

Благодаря этому преобразования dbt:

* Безопасно запускать повторно: если выполнение прервалось на полпути, повторный запуск не повредит данные.
* Удобно использовать для дозагрузки исторических данных: их можно повторно обрабатывать, не опасаясь двойного подсчёта или расхождений.
* Предсказуемы в CI: один и тот же код всегда создаёт одни и те же данные, что делает тестирование надёжным.

## Материализации и идемпотентность

Большинство материализаций dbt идемпотентны по умолчанию:

| Материализация      | Как обеспечивается идемпотентность                                                                                               |
| ------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| `table`             | Таблица пересоздаётся с нуля при каждом запуске (`create or replace`)                                                            |
| `view`              | Определение представления заменяется при каждом запуске                                                                          |
| `incremental`       | Требует продуманной настройки.                                                                                                   |
| `materialized_view` | Определение материализованного представления создаётся или заменяется при каждом запуске; обновлением данных управляет хранилище |

## Идемпотентность и инкрементальные модели

Инкрементальные модели обрабатывают только новые или изменённые строки, поэтому зависят от текущего состояния целевой таблицы. Если не соблюдать осторожность, повторный запуск инкрементальной модели может привести к появлению дубликатов строк или несогласованным результатам.

Самая распространённая ошибка — добавлять строки без дедупликации или надёжного уникального ключа. Сравните эти два примера:

**Неидемпотентная инкрементальная модель:**

В этом примере используется инкрементальная фильтрация, но не задан `unique_key`, поэтому строки на границе выборки могут добавляться повторно при каждом следующем запуске, что может привести к появлению дубликатов.

```sql
-- ❌ Not idempotent: re-runs can duplicate rows 
{{ config(materialized='incremental') }}

select * from {{ source('events', 'raw_events') }}

{% if is_incremental() %}
  where event_at >= (select max(event_at) from {{ this }})
{% endif %}
```

**Идемпотентная инкрементальная модель:**

В этом примере добавлен `unique_key`, поэтому совпадающие строки обновляются или заменяются, а не добавляются как дубликаты.

```sql
-- ✅ Idempotent when event_id is unique
{{ config(
  materialized='incremental',
  unique_key='event_id'
) }}

select * from {{ source('events', 'raw_events') }}

{% if is_incremental() %}
  where event_at >= (select max(event_at) from {{ this }})
{% endif %}
```

Если задан `unique_key`, dbt обновляет существующие строки и вставляет новые вместо добавления дубликатов. В зависимости от адаптера и [инкрементальной стратегии](https://docs.getdbt.tech/docs/build/incremental-strategy.md) dbt использует для этого `merge` или `delete+insert`.

Для больших наборов данных временных рядов также можно использовать [микропакетные инкрементальные модели](https://docs.getdbt.tech/docs/build/incremental-microbatch.md). Микропакетные модели обрабатывают данные пакетами на основе столбца `event_time` и могут обеспечивать более устойчивую работу при очень больших объёмах инкрементальной обработки.

### Распространённые риски

| Риск                                                  | Почему нарушается идемпотентность                                                                                       | Решение                                                                                  |
| ----------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
| Использование `current_timestamp()` в модели          | При каждом запуске получаются разные значения                                                                           | Используйте столбец из исходных данных в качестве временной метки                        |
| Добавление строк без `unique_key`                     | Без `unique_key` большинство адаптеров и стратегий добавляют строки и могут создавать дубликаты при повторных запусках. | Добавьте `unique_key` в конфигурацию инкрементальной модели                              |
| Генерация суррогатных ключей со случайными значениями | При разных запусках для одной и той же строки создаются разные ключи                                                    | Используйте детерминированное хеширование (например, `dbt_utils.generate_surrogate_key`) |
| Жёсткая привязка логики к «сегодняшней дате»          | Результаты меняются в зависимости от времени запуска модели, а не от данных                                             | Фильтруйте по временным меткам исходных данных                                           |

## Полное обновление как страховка

Если инкрементальная модель оказывается в некорректном состоянии (например, из-за изменения схемы или ошибки в логике), вы всегда можете выполнить `dbt run --full-refresh`, чтобы удалить таблицу и создать её заново с нуля. Это запасной механизм, позволяющий восстановить инкрементальные модели.

Рассматривайте `--full-refresh` как подтверждение того, что базовая логика по-прежнему идемпотентна. Даже если инкрементальная обработка оптимизирована, полный результат всегда должен оставаться воспроизводимым.

## Связанная документация

Эти документы описывают возможности dbt, для которых идемпотентность наиболее важна:

* [Выбор узлов по состоянию и запуски без учёта состояния](https://docs.getdbt.tech/reference/node-selection/state-selection.md)
* [Инкрементальные модели](https://docs.getdbt.tech/docs/build/incremental-models.md)
* [Микропакетные инкрементальные модели](https://docs.getdbt.tech/docs/build/incremental-microbatch.md)
