Learning Platform
Глоссарий Troubleshooting
Урок 05.01 · 22 мин
Средний
SnapshotsSCD2Slowly Changing DimensionsProductionCDC

SCD2 recap: зачем snapshots в production

Этот модуль — про production-grade snapshots. На уровне junior вы видели snapshot как «способ сохранить историю customers». На уровне middle вопрос ставится острее: какие гарантии даёт snapshot, где он ломается под нагрузкой, как ведут себя ключевые конфиги в dbt 1.9+ (hard_deletes, dbt_valid_to_current, snapshot_meta_column_names) и где это всё проваливается на DuckDB.

Этот первый урок — короткий recap, чтобы выровнять словарь и зафиксировать чек-лист производственных требований к snapshot. Дальше — глубже по каждой стратегии и крайним случаям.


SCD2 в одном экране

Источник пишет текущее состояние: одна строка на сущность, любое изменение перетирает прошлое. Это эквивалент SCD Type 1 — история теряется в момент UPDATE.

Snapshot превращает Type 1 в Type 2: каждая версия записи — отдельная строка с интервалом действия.

SCD1 vs SCD2 на одной таблице
app.customers (SCD1)Source app.customers хранит только текущее состояние. UPDATE перетирает старое значение. Это SCD Type 1 — история теряется.
customers_snapshot (SCD2)Snapshot customers_snapshot — physical SCD2 таблица в warehouse. Хранит все версии, каждое изменение = новая строка с dbt_valid_from / dbt_valid_to.

Ключевые SCD2-колонки, которые dbt добавляет к source:

  • dbt_scd_id — хеш версии (unique_key + updated_at / check_cols). Используется как PK snapshot-таблицы.
  • dbt_updated_at — когда эта версия зафиксирована.
  • dbt_valid_from — начало периода действия (= dbt_updated_at).
  • dbt_valid_to — конец периода. NULL (или dbt_valid_to_current) у активной версии.

В dbt 1.9+ имена этих колонок настраиваются через snapshot_meta_column_names — пригодится при миграции из старого хранилища, где привычны другие имена (valid_from_ts, is_current_flag).


Что отличает middle от junior

На junior хватало понимать «snapshot — это история». В production добавляются нелинейные требования:

  1. Идемпотентность. dbt snapshot можно перезапустить в течение одного «окна» без дублирования. Это достигается тем, что dbt сравнивает source с активными строками (dbt_valid_to IS NULL / = dbt_valid_to_current) — повторный запуск ничего не меняет, если данные не изменились.
  2. Атомарность одного run. dbt оборачивает MERGE snapshot в транзакцию warehouse. Если посередине упало — состояние snapshot остаётся целым.
  3. Late-arriving изменения. Source может опоздать: вчерашнее UPDATE пришло сегодня. Snapshot пишет «изменение зафиксировано сегодня», а не «вчера». В uri для compliance это критично — храните event_time отдельно от dbt_valid_from.
  4. Hard deletes. Соурс может удалить строку. По умолчанию hard_deletes: ignore — snapshot этого не заметит, останется зависшая активная строка. Это разбирается отдельным уроком.
  5. Schema evolution. Source добавил/удалил колонку. Snapshot должен пережить это без --full-refresh (что бы потеряло историю).
  6. Тесты на snapshot. Snapshot — это нода dbt, поэтому к нему применяется dbt test. На middle обязательны минимум unique на dbt_scd_id, not_null на unique_key + dbt_valid_from, not_null на dbt_valid_to where dbt_valid_to_current is set.

Чек-лист production snapshot

Перед тем как катить snapshot на прод, проверьте:

Production snapshot checklist

Эти восемь пунктов — то, что отличает «работающий snapshot» от «snapshot, на который можно опереться в compliance audit». Каждый из них раскроем глубже в следующих уроках.


Snapshot vs CDC vs audit log

Data Modeling: SCD Type 2 — теоретическая база dbt-i: первое знакомство с snapshots

Это три разных способа «хранить историю». На middle важно понимать границы.

ПодходЧто хранитКто ведётГранулярностьСильные стороныСлабости
dbt snapshotSCD2-версии по unique_keydbt по расписаниюНа момент запускаДекларативно, дешёво, лежит в warehouseМежду запусками изменения не видны
CDC (Debezium, logical replication)Каждое INSERT/UPDATE/DELETESource DB через WALНа каждое изменениеРеальное время, не теряет промежуточные statesСложная инфраструктура, требует stream-processing
Audit log в appЧто задумал бизнес-eventСама aplikacijaНа каждый user actionСемантика бизнес-уровняЗависит от полноты кода, лёгко забыть писать

dbt snapshot — это дешёвый snapshot текущего состояния через интервалы. Если бизнесу нужны все промежуточные pending -> confirmed -> paid -> refunded за заказ и интервал между ними — snapshot не подходит (он покажет только то, что было в момент его запуска). Тогда — CDC или внутренний event log в источнике.

Хорошо ли так делать в проекте? Часто комбинация: CDC из транзакционных систем для realtime фактов, snapshots для медленно меняющихся dimension-таблиц (customers, products, employees), где гранулярность раз в час / день достаточна.


DuckDB ограничения, которые отличают middle-проект от учебного

Большинство наших примеров работают на DuckDB, но три ограничения серьёзно влияют на дизайн:

  1. hard_deletes не реализован (на dbt-duckdb 1.10.x состояние 2026 года). Любая ваша политика удалений должна обходиться через soft-delete (колонка is_deleted) или post-hook.
  2. Snapshot на external materialization не работает. Если source — read_parquet('s3://...') без attached table, snapshot упадёт. Нужна локальная таблица или ATTACH к Postgres/Iceberg.
  3. Single-writer per file. В момент dbt snapshot ваша DuckDB-база заблокирована для других writer-процессов. Если у вас несколько проектов пишут в одну .duckdb — нужно либо MotherDuck, либо разнесённые .duckdb-файлы.

В Snowflake / BigQuery / Postgres всё это решено. Дальше в курсе мы будем явно отмечать «вот тут DuckDB ведёт себя иначе» — это полезный навык для middle, потому что в реальной работе вы редко сидите на одном warehouse всю карьеру.


Попробуй сам

Откройте репозиторий dbt-ii-labs и найдите models/staging/_sources.yml. Ответьте на четыре вопроса по бизнес-логике snapshot для каждой таблицы:

  1. Нужен ли snapshot на эту таблицу? Если это fact (orders, events) — нет, она и так append-only. Если dimension (customers, products) — скорее всего да.
  2. Какая стратегия? Если в source стабильный updated_at — timestamp. Если нет — check, и какие колонки в check_cols.
  3. Что в unique_key? Стабильный PK, не атрибут.
  4. Какая политика hard_deletes? Допустимо ли терять deleted rows? Если нет — soft-delete pattern или переход на Postgres/Snowflake.

Зафиксируйте ответы как комментарии в YAML. Эти ответы — основа всех следующих уроков модуля.


Ключевые выводы

  1. SCD2 = слайс на момент через dbt_valid_from / dbt_valid_to. Активная строка — valid_to IS NULL или = dbt_valid_to_current sentinel.
  2. Production snapshot требует не только конфига, но и schedule, тестов, документации, DR-плана.
  3. Snapshot — не CDC и не audit log: фиксирует состояние на момент запуска, между запусками изменения теряются.
  4. На DuckDB важны три ограничения: нет hard_deletes, snapshot не работает на external materialization, single-writer per file.
  5. dbt 1.9+ привнёс hard_deletes, dbt_valid_to_current, snapshot_meta_column_names, YAML-синтаксис — это базис production-уровня, который мы дальше разбираем.
Проверка знанийKnowledge check
Команда хочет хранить полную историю заказов: статусы pending -> confirmed -> paid -> refunded, время каждого перехода. snapshot с unique_key=order_id и check_cols=[status] — корректное решение?
ОтветAnswer
Не совсем. Snapshot фиксирует состояние **на момент запуска**. Если статусы меняются быстрее, чем интервал `dbt snapshot` (например, snapshot раз в час, а заказ за 5 минут прошёл pending -> paid), промежуточный pending-snapshot может **никогда не быть зафиксирован**. \n\nЧто делать:\n\n1. **Если бизнесу хватит точности «состояние на каждый час»** — snapshot работает, но статус check\\_cols = [status] плюс расписание `@hourly`. Принять, что промежуточные статусы могут теряться.\n2. **Если нужны все переходы** — это **не snapshot use case**. Правильное решение:\n - CDC через Debezium / logical replication из source DB.\n - Event log в самом приложении: каждый переход пишется в `order_status_history` (order_id, old_status, new_status, changed_at).\n - Audit log от бизнеса, который пишет каждое изменение явным INSERT.\n3. **Гибрид**: snapshot для customers / products, event log в источнике для orders.\n\nПравило: snapshot подходит для **медленно меняющихся dimensions**. Для быстро меняющихся facts со всеми переходами — CDC или event log в источнике.
Проверка знанийKnowledge check
На code review предлагают для customers_snapshot выставить dbt_valid_to_current: NULL (как раньше, до 1.9). Что ответить?
ОтветAnswer
Это рабочий, но устаревший подход. Аргументы против `NULL`:\n\n1. **NULL в BETWEEN ломает JOIN-ы**. `order_date BETWEEN s.dbt_valid_from AND NULL` всегда FALSE — активные клиенты пропадут из результата. Каждый downstream-запрос должен делать `COALESCE(dbt_valid_to, '9999-12-31')`. Один пропуск COALESCE = battle бага.\n2. **NULL ломает unique-тест на (unique_key, dbt_valid_to)**. `NULL ≠ NULL` — если есть два активных дубля (что не должно быть, но баг возможен), тест не упадёт.\n3. **NULL не выражает «активна сейчас» явно**. Junior может подумать что NULL = «неизвестно когда закрылась». 9999-12-31 — sentinel, явно отвечает «активна».\n\nПравильно:\n\n```yaml\nconfig:\n dbt_valid_to_current: "to_date('9999-12-31')"\n```\n\nЭто dbt 1.9+ feature. Если проект на старой версии — поставить post-hook, который ставит sentinel вручную, или COALESCE-обернуть в downstream views. На новых snapshots всегда сразу с sentinel.

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

Результат: 0 из 0
Концептуальный
Вопрос 1 из 6. Что отличает middle-level подход к snapshot от junior? Выберите наиболее полный ответ.

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

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

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

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