Learning Platform
Глоссарий Troubleshooting
Урок 14.04 · 25 мин
Начальный
SnapshotsStrategiesTimestampCheckYAMLunique_key

Snapshot стратегии: timestamp vs check

В предыдущем уроке вы поняли зачем нужны snapshots. Этот урок — как именно dbt определяет, что строка изменилась. Есть две стратегии, и выбор между ними — главное архитектурное решение при настройке snapshot.

Также этот урок — про новый YAML-синтаксис из dbt 1.9+, который пришёл на смену старому .sql файлу с Jinja-блоком.


Две стратегии — timestamp и check

dbt предлагает два способа определить, что строка в source-таблице изменилась:

Две стратегии snapshot
timestampСтратегия timestamp: dbt сравнивает source.updated_at с snapshot.dbt_updated_at. Если source.updated_at > snapshot.dbt_updated_at -> строка изменилась. Быстро, надёжно, требует наличия updated_at в source. Это идеальная стратегия, когда source-система пишет updated_at.
checkСтратегия check: dbt сравнивает значения в указанных check_cols. Если любая из колонок отличается от snapshot — строка изменилась. Медленнее, не требует updated_at.

Правило большого пальца:

  • Есть надёжный updated_at в source — берите timestamp.
  • Нет — берите check и явно перечисляйте колонки, которые хотите отслеживать.

Не используйте check со всеми колонками подряд — это медленно и шумно. Если у вас есть колонка last_login_at, которая меняется при каждом заходе пользователя, — она забьёт snapshot тысячами «изменений».


YAML-синтаксис 1.9+

Начиная с dbt 1.9, snapshots декларируются в YAML. Раньше использовался .sql файл с Jinja:

{# Старый синтаксис dbt < 1.9 — deprecated path #}
{% snapshot customers_snapshot %}

{{
    config(
      target_schema='snapshots',
      unique_key='customer_id',
      strategy='timestamp',
      updated_at='updated_at'
    )
}}

SELECT * FROM {{ source('app', 'customers') }}

{% endsnapshot %}

С 1.9 — это YAML в snapshots/:

# snapshots/customers_snapshot.yml — НОВЫЙ синтаксис 1.9+
snapshots:
  - name: customers_snapshot
    relation: source('app', 'customers')
    config:
      schema: snapshots
      unique_key: customer_id
      strategy: timestamp
      updated_at: updated_at

Если нужен более сложный SQL (фильтры, JOIN), всё ещё пишите .sql файл, но Jinja-блок {% snapshot %} больше не нужен:

-- snapshots/customers_snapshot.sql — для сложной SQL логики
{{
    config(
      target_schema='snapshots',
      unique_key='customer_id',
      strategy='timestamp',
      updated_at='updated_at'
    )
}}

SELECT
    customer_id,
    name,
    address,
    tier,
    updated_at
FROM {{ source('app', 'customers') }}
WHERE deleted = false

В этом курсе используем YAML вариант как рекомендуемый.

NOTE

В dbt 1.10 старый {% snapshot %} блок ещё работает с warning. В будущих версиях будет удалён. Все новые snapshots пишите в YAML.


Полный YAML snapshot — timestamp

# snapshots/customers_snapshot.yml
snapshots:
  - name: customers_snapshot
    description: "SCD2-история таблицы customers с момента запуска dbt"
    relation: source('app', 'customers')
    config:
      schema: snapshots
      unique_key: customer_id
      strategy: timestamp
      updated_at: updated_at
      dbt_valid_to_current: "to_date('9999-12-31')"

Разберём каждое поле:

ПолеЗначениеЗачем
namecustomers_snapshotИмя snapshot в DAG. Будет таблица с этим именем в warehouse.
relationsource('app', 'customers')Откуда брать данные. Может быть source(...), ref(...), или прямой SQL через .sql.
schemasnapshotsСхема целевой таблицы. Дефолт = target schema.
unique_keycustomer_idИдентификатор записи. Обязателен. Может быть строкой или списком.
strategytimestampСтратегия определения изменений.
updated_atupdated_atКолонка timestamp в source. Используется для сравнения.
dbt_valid_to_current"to_date('9999-12-31')"Что писать в dbt_valid_to для активных строк. По умолчанию NULL.

dbt_valid_to_current — полезная опция. Если оставить NULL — нужно делать COALESCE(dbt_valid_to, '9999-12-31') во всех downstream-запросах. Если поставить sentinel-дату — JOIN-ы упрощаются.


Полный YAML snapshot — check

# snapshots/products_snapshot.yml
snapshots:
  - name: products_snapshot
    description: "История изменений products: цена и категория"
    relation: source('app', 'products')
    config:
      schema: snapshots
      unique_key: product_id
      strategy: check
      check_cols:
        - price
        - category
        - is_active

Ключевое отличие — check_cols. dbt будет отслеживать только эти три колонки. Изменение name (если оно случится) не создаст новую SCD2-версию.

Можно также указать check_cols: 'all' — отслеживать все колонки. Но это создаёт проблемы:

  1. Шумно. Малейшее изменение в любой колонке создаёт новую версию.
  2. Медленнее. Большое сравнение на каждый run.
  3. Нестабильно. Если source добавит/удалит колонку — структура snapshot нарушится.

check_cols: 'all' — антипаттерн, не делайте так.

WARNING

Стратегия check не отслеживает изменения в колонках, которые не в check_cols. Если price поменялся и вы забыли его добавить — изменение пропустится. Перечисляйте все business-critical колонки.


unique_key — composite key

Если строка идентифицируется не одной колонкой, а комбинацией (например, customer_id + product_id в таблице subscriptions) — unique_key может быть списком:

snapshots:
  - name: subscriptions_snapshot
    relation: source('app', 'subscriptions')
    config:
      unique_key:
        - customer_id
        - product_id
      strategy: timestamp
      updated_at: updated_at

dbt сгенерирует MERGE с условием на оба поля. Если в source есть две строки с одинаковым customer_id, но разным product_id — snapshot их различит.

DANGER

unique_key должен быть stable и non-null. Если customer_id когда-то NULL — snapshot создаст «зависшую» строку без идентификации, и каждый run будет делать новый INSERT (думая что это новая запись). Всегда добавляйте not_null тест на колонки unique_key источника.


Дополнительные колонки в snapshot

После выполнения snapshot, помимо source-колонок, в таблице будут:

КолонкаЗначение
dbt_scd_idХеш SHA256 от unique_key + updated_at (или check_cols). Уникальный идентификатор версии записи.
dbt_updated_atTimestamp создания этой версии в snapshot. Для timestamp = source.updated_at. Для check = время snapshot.
dbt_valid_fromС какого момента эта версия действительна. = dbt_updated_at.
dbt_valid_toДо какого момента эта версия действительна. NULL (или dbt_valid_to_current) для активной строки.

В check-стратегии есть нюанс: dbt_updated_at = время выполнения snapshot, а не реальное время изменения данных. Если check запустился через 24 часа после изменения — dbt_valid_from будет на 24 часа позже фактического изменения.

Это компромисс: check не знает, когда именно строка изменилась, только что изменилась.


Полный пример use-case: jaffle_shop

В Jaffle Shop у нас есть source-таблица app.customers:

-- Что есть в source
SELECT * FROM app.customers;
customer_idnametieremailupdated_at
1Alicestandard[email protected]2026-01-15 10:00:00
2Bobpremium[email protected]2026-02-20 11:00:00

Создаём snapshot:

# snapshots/_snapshots.yml
snapshots:
  - name: customers_snapshot
    description: "История изменений customers — для атрибуции продаж по tier"
    relation: source('app', 'customers')
    config:
      schema: snapshots
      unique_key: customer_id
      strategy: timestamp
      updated_at: updated_at
      dbt_valid_to_current: "to_date('9999-12-31')"

Запускаем первый раз:

dbt snapshot

В snapshots.customers_snapshot появятся две строки (по одной на customer) с dbt_valid_to = 9999-12-31.

Через неделю Алиса апгрейднулась до premium tier. В source:

UPDATE app.customers
SET tier = 'premium', updated_at = '2026-01-22 14:00:00'
WHERE customer_id = 1;

Запускаем snapshot снова:

dbt snapshot

В snapshot теперь:

customer_idnametierupdated_atdbt_valid_fromdbt_valid_to
1Alicestandard2026-01-15 10:00:002026-01-15 10:00:002026-01-22 14:00:00
1Alicepremium2026-01-22 14:00:002026-01-22 14:00:009999-12-31
2Bobpremium2026-02-20 11:00:002026-02-20 11:00:009999-12-31

Теперь модель customers_orders может корректно атрибутировать каждую транзакцию к tier на момент сделки:

-- models/marts/orders_with_tier.sql
SELECT
    o.order_id,
    o.customer_id,
    o.order_date,
    o.amount,
    s.tier AS tier_at_order_time
FROM {{ ref('stg_jaffle__orders') }} o
LEFT JOIN {{ ref('customers_snapshot') }} s
    ON o.customer_id = s.customer_id
    AND o.order_date >= s.dbt_valid_from
    AND o.order_date < s.dbt_valid_to

Если Алиса сделала заказ 2026-01-18 (до апгрейда) — tier_at_order_time = 'standard'. Если 2026-01-25 (после) — tier_at_order_time = 'premium'. Это и есть атрибуция на момент.


invalidate_hard_deletes — что делать, если строка пропала

В dbt 1.9+ есть конфиг hard_deletes с тремя значениями:

ЗначениеПоведение
ignore (default)Если строка пропала из source — игнорируем. Snapshot оставляет последнюю версию как «активную». Дефолт.
invalidateЕсли строка пропала — закрываем версию (dbt_valid_to = now()). Активной версии больше нет.
new_recordЕсли строка пропала — добавляем новую версию с флагом dbt_is_deleted = TRUE. Старая версия закрывается.
snapshots:
  - name: customers_snapshot
    relation: source('app', 'customers')
    config:
      schema: snapshots
      unique_key: customer_id
      strategy: timestamp
      updated_at: updated_at
      hard_deletes: invalidate  # 1.9+ feature
WARNING

hard_deletes поддерживается на Postgres, Snowflake, BigQuery. В DuckDB на момент 2026 это не реализовано. На DuckDB используйте soft-delete паттерн: в source держите колонку is_deleted boolean, и trackайте её через check-стратегию или включите в timestamp-логику.


Запуск и расписание

Snapshot — отдельная команда:

dbt snapshot                                   # запустит все snapshots
dbt snapshot --select customers_snapshot       # один конкретный
dbt build --select +my_mart                    # включит snapshots в DAG до my_mart

В production snapshot обычно прогоняется по расписанию через cron / Airflow / dbt Cloud Jobs:

  • Ежедневно — типичная частота для customers, products dimension.
  • Каждый час — для быстро меняющихся источников (например, статусы заказов).
  • Каждые 15 минут — для realtime-аналитики (редко в junior-проектах).

Чем чаще запускается snapshot, тем точнее история — но больше I/O и storage.


Попробуй сам

В вашем dbt-проекте на DuckDB:

  1. Создайте source-таблицу customers_demo с колонками customer_id, tier, updated_at:
CREATE TABLE main.customers_demo AS
SELECT * FROM (VALUES
    (1, 'Alice', 'standard', TIMESTAMP '2026-01-15 10:00:00'),
    (2, 'Bob',   'premium',  TIMESTAMP '2026-02-20 11:00:00')
) AS t(customer_id, name, tier, updated_at);
  1. Опишите её как source в models/_sources.yml:
sources:
  - name: app
    schema: main
    tables:
      - name: customers_demo
  1. Создайте snapshot snapshots/customers_snapshot.yml:
snapshots:
  - name: customers_snapshot
    relation: source('app', 'customers_demo')
    config:
      schema: snapshots
      unique_key: customer_id
      strategy: timestamp
      updated_at: updated_at
  1. Запустите:
dbt snapshot
dbt show --inline "SELECT * FROM {{ ref('customers_snapshot') }}"
  1. Измените tier Алисы:
UPDATE main.customers_demo
SET tier = 'premium', updated_at = TIMESTAMP '2026-01-22 14:00:00'
WHERE customer_id = 1;
  1. Снова dbt snapshot. В таблице теперь три строки. Старая Алиса с dbt_valid_to, новая Алиса с dbt_valid_to IS NULL, Боб без изменений.

Бонус: переделайте на check-стратегию (check_cols: [tier]). Что меняется в timestamp колонок dbt_valid_from / dbt_valid_to?


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

  1. Две стратегии: timestamp (сравнение updated_at) и check (сравнение значений в check_cols). Выбор зависит от наличия надёжного updated_at в source.
  2. YAML-синтаксис 1.9+ — новый стандарт. Старый .sql с {% snapshot %} блоком — deprecated path.
  3. unique_key обязателен. Может быть строкой или списком. Должен быть not-null и stable.
  4. Колонки SCD2: dbt_scd_id, dbt_updated_at, dbt_valid_from, dbt_valid_to. Активные строки имеют dbt_valid_to = NULL или dbt_valid_to_current.
  5. check_cols: ‘all’ — антипаттерн. Перечисляйте конкретные business-critical колонки.
  6. hard_deletes конфиг (1.9+) — ignore / invalidate / new_record. В DuckDB не реализован — используйте soft-delete.
  7. Snapshot запускается отдельной командой dbt snapshot или включается в dbt build. Расписание — обычно ежедневно через cron / Airflow.
  8. Use case: атрибуция фактов к dimension-данным на момент через JOIN по order_date BETWEEN dbt_valid_from AND dbt_valid_to.
Timestamp strategy: углублённый разбор edge cases SCD Type 5, 6, 7 и bi-temporal моделирование
Проверка знанийKnowledge check
У вас source-таблица `orders` без колонки updated_at — приложение пишет только created_at, но строки потом меняются (status: pending -> paid -> shipped -> delivered). Какую стратегию snapshot выбрать и что в check_cols?
ОтветAnswer
Стратегия — **check**, потому что нет надёжного updated_at. В `check_cols` нужно включить **только бизнес-значимые колонки**, которые отражают жизненный цикл заказа:\n\n```yaml\nsnapshots:\n - name: orders_snapshot\n relation: source('app', 'orders')\n config:\n schema: snapshots\n unique_key: order_id\n strategy: check\n check_cols:\n - status\n - shipping_address\n - total_amount\n```\n\nНЕ включайте `updated_at` (она шумная), `last_viewed_by_admin` (не бизнес), `status_history_json` (часто меняется). Цель — каждая новая SCD2-строка отражает **смысловое** изменение заказа, а не технические.\n\nВажный нюанс check: `dbt_valid_from` будет = время выполнения `dbt snapshot`, а не время реального изменения. Поэтому атрибуция к моменту может «плавать» в пределах окна между запусками (если snapshot раз в час — то плавает в пределах часа).
Проверка знанийKnowledge check
Junior настроил `unique_key: email` для snapshot customers. Через две недели обнаружил, что в snapshot у клиента появилось 5 «параллельных активных» строк. Что пошло не так?
ОтветAnswer
Скорее всего, поле `email` оказалось **не stable**:\n\n1. **Клиент поменял email**. Старый email теперь не находит match в source -> dbt думает что это **новая запись**, делает INSERT. Активная строка со старым email остаётся (если `hard_deletes: ignore`).\n2. **Email иногда NULL**. NULL = NULL в SQL = FALSE, никакой match не находит -> каждый запуск snapshot создаёт «новую» строку.\n3. **Email иногда лежит в разной нормализации** ([email protected] vs [email protected]). dbt сравнивает as-is.\n\nПравильное решение: `unique_key: customer_id` (стабильный, не-null primary key из БД). Поле email отслеживается как обычная колонка SCD2 — изменилась -> новая версия с тем же customer_id, но новым email.\n\nОбщее правило: `unique_key` — это **бизнес-идентификатор сущности**, а не атрибут, который может меняться.

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

Результат: 0 из 0
Прикладной
Вопрос 1 из 5. У source таблицы `orders` нет колонки updated_at. Приложение пишет только created_at, но строки потом меняются (status: pending -> paid -> shipped -> delivered). Какую стратегию snapshot выбрать?

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

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

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

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