Snapshots: что такое SCD2 и зачем нужны
Большинство таблиц в source-системах хранят только текущее состояние. У клиента изменился адрес — старый адрес перетёрся. У товара поменялась цена — старая цена потеряна. Если вам нужно отвечать на вопросы вида «по какой цене был продан этот товар год назад?» или «в каком городе жил клиент в момент покупки?», current-state данных не хватает.
Snapshot в dbt — это механизм, который отслеживает изменения в source-таблице и хранит историю. Концептуально это SCD2 (Slowly Changing Dimension Type 2) — паттерн из теории data warehousing.
Этот урок — про концепт. Следующий — про конкретные стратегии (timestamp/check) и YAML-конфигурацию.
SCD: типы slowly changing dimensions
Slowly Changing Dimension — это измерение, которое медленно меняется во времени. Адрес клиента, цена товара, статус сотрудника. В отличие от facts (транзакции, события), измерения не пишутся каждую секунду, а обновляются раз в неделю или месяц.
В классической теории Кимбала есть шесть типов SCD (Type 0..6). На практике используются три:
Type 1 — это то, что делает большинство source-систем. Никакой истории.
Type 2 — то, что делает dbt snapshot. Каждая версия записи хранится как отдельная строка с дополнительными колонками dbt_valid_from / dbt_valid_to. Это стандарт data warehousing для истории.
Type 3 — добавление колонок previous_X, previous_previous_X. Редко.
Конкретный пример: customers.address
В source-таблице app.customers лежит:
| customer_id | name | address | updated_at |
|---|---|---|---|
| 1 | Alice | Moscow, RU | 2026-01-15 10:00:00 |
| 2 | Bob | London, GB | 2026-02-20 11:00:00 |
Сегодня — 2026-03-15. Алиса переехала. Source-система делает UPDATE:
| customer_id | name | address | updated_at |
|---|---|---|---|
| 1 | Alice | Yerevan, AM | 2026-03-15 09:00:00 |
| 2 | Bob | London, GB | 2026-02-20 11:00:00 |
Старый адрес Алисы — потерян навсегда. Запросы вида «в каком городе жила Алиса в феврале?» не ответятся.
После dbt snapshot таблица snapshots.customers_snapshot выглядит так:
| customer_id | name | address | updated_at | dbt_scd_id | dbt_updated_at | dbt_valid_from | dbt_valid_to |
|---|---|---|---|---|---|---|---|
| 1 | Alice | Moscow, RU | 2026-01-15 10:00:00 | a8e3… | 2026-01-15 10:00:00 | 2026-01-15 10:00:00 | 2026-03-15 09:00:00 |
| 1 | Alice | Yerevan, AM | 2026-03-15 09:00:00 | b7f2… | 2026-03-15 09:00:00 | 2026-03-15 09:00:00 | NULL |
| 2 | Bob | London, GB | 2026-02-20 11:00:00 | c9d1… | 2026-02-20 11:00:00 | 2026-02-20 11:00:00 | NULL |
Что произошло:
- У Алисы две строки: старая Moscow и новая Yerevan.
- В старой строке
dbt_valid_to = 2026-03-15 09:00:00— момент, когда зафиксировалось изменение. - В новой
dbt_valid_to = NULL— это текущее значение. - У Боба одна строка с
dbt_valid_to = NULL, потому что Боб не менялся. dbt_scd_id— уникальный хеш версии записи (для дедупликации).
Теперь запросы про историю работают:
-- В каком городе жила Алиса 1 февраля 2026?
SELECT address
FROM customers_snapshot
WHERE customer_id = 1
AND '2026-02-01' BETWEEN dbt_valid_from AND COALESCE(dbt_valid_to, '9999-01-01');
-- -> Moscow, RU
Зачем это нужно в реальности
Истории клиента / продукта нужны везде, где есть аналитика на момент:
- Финансовая отчётность. «Сколько мы выручили в первом квартале по тарифным планам, действовавшим тогда?» Если план поменялся в апреле — нужна цена на момент транзакции, а не текущая.
- Атрибуция продаж. «Какой sales rep вёл этого клиента в момент сделки?» Reps меняются; снапшот хранит, кто отвечал за клиента когда.
- Compliance / audit. «Кто был VIP-клиентом на день X?» Регуляторы требуют воспроизводимости.
- A/B-эксперименты. «В каком сегменте был клиент на старте эксперимента?» Сегмент мог поменяться к концу эксперимента.
Без snapshots — вы реконструируете эти данные из логов изменений (если они есть) или просто врёте: «вот текущая цена, забейте на то, что в момент сделки она была другой».
Как работает dbt snapshot — high-level
dbt snapshot — отдельная команда, не dbt run. Что она делает:
Ключевые моменты:
- Snapshot нужен unique_key — без него dbt не может сказать «эта строка та же, что вчера, только изменилась». unique_key обычно = primary key source-таблицы.
- Snapshot хранится отдельно — это не view над source, это физическая таблица, которая аккумулирует историю.
- Snapshot никогда не сжимается — старые строки лежат вечно. Это feature, не bug.
- Snapshot прогоняется по расписанию — типично раз в час / раз в день. Чем чаще — тем точнее история, но тем больше I/O.
Куда складываются snapshots
В стандартном dbt-проекте — директория snapshots/:
jaffle_shop/
dbt_project.yml
models/
seeds/
snapshots/
customers_snapshot.yml ← конфиг snapshot 1.9+ синтаксис
products_snapshot.yml
tests/
macros/
Путь настраивается в dbt_project.yml:
snapshot-paths: ["snapshots"]
В версиях dbt менее 1.9 snapshots определялись в .sql файлах с Jinja-блоком {% snapshot %} ... {% endsnapshot %}. Этот синтаксис ещё поддерживается, но deprecated. Новый рекомендуемый формат — YAML (с 1.9+).
Об этом — следующий урок.
Snapshot — это нода в DAG
Как и seed, snapshot — это полноценный узел в графе зависимостей:
-- models/marts/customers_history.sql
SELECT
customer_id,
name,
address,
dbt_valid_from,
dbt_valid_to
FROM {{ ref('customers_snapshot') }}
dbt build запустит snapshots в правильной последовательности по DAG. dbt run — не запустит snapshots, нужна команда dbt snapshot или dbt build.
Snapshot работает с source или с model, но не с другим snapshot. На source/model можно ссылаться через source() / ref(). Если попытаться построить snapshot поверх другого snapshot — теряется грань между «активной строкой» и «исторической», и логика SCD2 ломается.
Когда snapshot, когда — нет
DuckDB-специфика snapshots
В курсе используется DuckDB. Несколько важных моментов:
- Стратегии timestamp и check работают. Поддержка полная.
- hard_deletes НЕ работает в DuckDB. Это feature dbt 1.9+, которая отмечает удалённые строки. На DuckDB не реализована.
- Snapshots на external материализации НЕ работают. Если source — это
read_parquet(...)без физической таблицы, snapshot упадёт. - Concurrency: DuckDB — single-writer на файл. Если snapshot выполняется, параллельные модели на эту таблицу подождут.
Эти ограничения важны на проде с DuckDB (есть проекты, которые так делают). В Snowflake/BigQuery/Postgres работает всё.
Что НЕ делает snapshot
Самое частое заблуждение junior’ов: «snapshot — это бекап». Нет.
- Snapshot не сохраняет deleted rows (без hard_deletes config, которого нет в DuckDB). Если строка пропала из source — dbt просто не обновит её dbt_valid_to. Она будет «зависшая активная» с устаревшими данными.
- Snapshot не работает на partial source. Если source-запрос отфильтровал часть строк (WHERE), dbt подумает что эти строки удалены, и закроет их в snapshot. Урок: snapshot-source должен быть полным dataset’ом.
- Snapshot не делает audit log. История — только для конфигурированных колонок. Если изменилась колонка вне check_cols (для check-стратегии) — изменение пропустится.
Попробуй сам
Откройте свой dbt-проект. Создайте файл snapshots/customers_snapshot.yml (на следующем уроке конкретный синтаксис будет; сейчас просто продумайте дизайн):
- Какая source-таблица? Скорее всего
stg_jaffle__customers(staging). - Какой unique_key?
customer_id. - Что отслеживать? Все колонки или конкретные? (
address,email,tier) - Как часто запускать? (
dbt snapshotраз в день в cron’е) - Как использовать в моделях?
{{ ref('customers_snapshot') }}— затем JOIN-ы с фактами по customer_id и периоду действия.
Подумайте о двух кейсах:
- Аналитик хочет revenue per customer tier за квартал. Нужна история tier на момент каждой транзакции. JOIN orders ON customer_id AND order_date BETWEEN dbt_valid_from AND dbt_valid_to.
- Compliance просит знать, кто был VIP-клиентом на 2026-04-01. Тот же snapshot, фильтр
WHERE '2026-04-01' BETWEEN dbt_valid_from AND COALESCE(dbt_valid_to, '9999-12-31') AND tier = 'VIP'.
Это упражнение на mental model: snapshot — это таблица с периодами действия, и JOIN-ы по периодам — обычный SQL.
Ключевые выводы
- SCD2 — паттерн data warehousing для отслеживания истории dimension-данных. Каждая версия записи — отдельная строка с
valid_from/valid_to. dbt snapshotреализует SCD2. После запуска в snapshot-таблице две строки на каждое изменение: старая сdbt_valid_to = время_измененияи новая сdbt_valid_to = NULL.- Snapshot нужен unique_key и стратегия определения изменений (timestamp или check).
- Snapshot хранится физически в warehouse, не view. Никогда не сжимается, растёт со временем.
- Используется для slowly changing dimensions: customers, products, employees. Не для transaction-facts (они append-only сами по себе).
dbt snapshot— отдельная команда.dbt runне запускает snapshots.dbt buildзапускает в порядке DAG.- В DuckDB: timestamp/check стратегии работают, hard_deletes не реализован, snapshots на external материализации не работают.