Что было user.subscription_plan 3 месяца назад? Сколько клиентов downgrade'нулись с Pro на Free в марте? Когда конкретно изменился их status?
Если users table в твоей DWH хранит только current state — ответа нет. Историю съел UPDATE.
dbt Snapshots — это механизм SCD Type 2 (Slowly Changing Dimension) для отслеживания каждого изменения строки во времени. Этот гайд — про практическое использование.
SCD Type 2 vs Type 1: быстрый flow
| Type | Что хранится | Когда |
|---|---|---|
| Type 1 | Только current state (overwrite) | Не важно прошлое: «текущая страна юзера» |
| Type 2 | Каждая версия с valid_from/valid_to | Важно прошлое: «план в момент покупки» |
| Type 3 | Previous + current value | Редко, только 1 history-уровень |
Snapshot dbt = Type 2.
Real-world пример: ARR Retention Cohort. Если cohort-анализ строится по «plan на момент signup», а users.plan переписывается → данные за прошлое исчезают.
dbt snapshot architecture
dbt snapshots создают таблицу с 4 мета-колонками:
| Column | Описание |
|---|---|
dbt_valid_from | Когда этот рекод стал актуальным |
dbt_valid_to | Когда устарел (NULL = текущий active) |
dbt_updated_at | Когда последний раз менялся |
dbt_scd_id | MD5 hash unique_key + valid_from (composite PK) |
Запрос «какие были users 2024-03-15»:
SELECT *
FROM users_snapshot
WHERE '2024-03-15' BETWEEN dbt_valid_from AND COALESCE(dbt_valid_to, '9999-12-31');
Шаг 1-3: первый snapshot через strategy='timestamp'
Если в source table есть updated_at — это самая надёжная стратегия.
Шаг 1. Создать snapshot definition
-- snapshots/users_snapshot.sql
{% snapshot users_snapshot %}
{{
config(
target_schema='snapshots',
unique_key='user_id',
strategy='timestamp',
updated_at='updated_at',
invalidate_hard_deletes=True
)
}}
SELECT
user_id,
email,
plan,
status,
country,
created_at,
updated_at
FROM {{ source('raw', 'users') }}
{% endsnapshot %}
Шаг 2. Запустить snapshot
dbt snapshot --select users_snapshot
dbt сравнивает каждую строку source vs snapshot. Если updated_at в source > dbt_updated_at в snapshot → новая версия запись.
Шаг 3. Запросить historical data
-- Текущее состояние:
SELECT * FROM snapshots.users_snapshot WHERE dbt_valid_to IS NULL;
-- Снапшот на 15 марта 2024:
SELECT *
FROM snapshots.users_snapshot
WHERE '2024-03-15' BETWEEN dbt_valid_from AND COALESCE(dbt_valid_to, '9999-12-31');
Шаг 1-4: snapshot через strategy='check' (без updated_at)
Если updated_at нет — используй check strategy. Она хеширует переданные колонки и сравнивает.
Шаг 1. Snapshot config
-- snapshots/users_check_snapshot.sql
{% snapshot users_check_snapshot %}
{{
config(
target_schema='snapshots',
unique_key='user_id',
strategy='check',
check_cols=['plan', 'status', 'country']
)
}}
SELECT
user_id,
email,
plan,
status,
country,
created_at
FROM {{ source('raw', 'users') }}
{% endsnapshot %}
Шаг 2. check_cols: какие колонки трекать
# dbt видит изменения только в этих колонках:
check_cols=['plan', 'status', 'country']
# Email и created_at могут меняться — snapshot их игнорирует
# (быстрее, меньше storage)
Шаг 3. Все колонки через 'all'
check_cols='all' -- любое изменение → новая запись
Медленнее (нужен hash всех колонок), но проще для small dimension tables.
Шаг 4. Тонкая настройка
{{
config(
target_schema='snapshots',
unique_key='user_id',
strategy='check',
check_cols=['plan', 'status'],
invalidate_hard_deletes=True, -- если row удалена в source → close в snapshot
updated_at='dbt_updated_at' -- кастомное имя для меты
)
}}
Production patterns: snapshot frequency
Cron через Airflow
# DAG: snapshot_dimensions
@dag(schedule='0 */6 * * *') # каждые 6 часов
def snapshot_dimensions():
BashOperator(
task_id='snapshot_users',
bash_command='cd /dbt && dbt snapshot --select users_snapshot'
)
Trade-off: чаще → точнее, но больше storage. Реальные benchmarks:
- Каждый час для critical: subscriptions, prices
- Раз в 6 часов для users/teams
- Раз в сутки для slowly-changing: regions, categories
Hard-delete handling
invalidate_hard_deletes=True
Если row удалена из source → dbt_valid_to = now(). Без флага snapshot будет считать row актуальной навсегда.
Но! Hard-delete check медленный (full anti-join с source). Для больших таблиц (10M+) рассмотри alternative: soft-delete через колонку deleted_at + регулярный snapshot.
Snapshot perf на 100M+ rows
dbt snapshot --select users_snapshot --threads 8
Distinguish: snapshot — это MERGE-based, не append. Performance падает с ростом snapshot table.
Рассмотри:
- Partition snapshot по
dbt_valid_from - Cluster key (Snowflake) или
ORDER BY(BigQuery) по unique_key
Querying historical data: time-travel
-- Customer journey: какой план был у юзера в каждый месяц 2024?
WITH date_spine AS (
SELECT date_month FROM (
VALUES ('2024-01-01'), ('2024-02-01'), ('2024-03-01'), ('2024-04-01'),
('2024-05-01'), ('2024-06-01'), ('2024-07-01'), ('2024-08-01')
) AS t(date_month)
)
SELECT
ds.date_month,
s.user_id,
s.plan,
s.status
FROM date_spine ds
LEFT JOIN snapshots.users_snapshot s
ON ds.date_month BETWEEN s.dbt_valid_from AND COALESCE(s.dbt_valid_to, '9999-12-31')
WHERE s.user_id = '12345'
ORDER BY ds.date_month;
Output:
date_month user_id plan status
2024-01-01 12345 Free active
2024-02-01 12345 Pro active ← upgrade!
2024-03-01 12345 Pro active
2024-04-01 12345 Free active ← downgrade :(
Теперь можно cohort-анализ: «retention Pro-юзеров cohort февраль-2024».
Common bugs
Race condition при concurrent snapshot
dbt snapshot не блокирует source. Если в source строки меняются ПОКА бежит snapshot — некоторые изменения пропадут.
Решение: или SELECT ... FOR UPDATE (PG), или snapshot на CDC-stream вместо source live, или freeze source через transaction isolation.
Schema drift
Если в source появился новый column, а в snapshot он не определён — silently ignored.
# Solution: dbt-utils star() помощник
{% snapshot users_snapshot %}
{{ config(...) }}
SELECT {{ dbt_utils.star(from=ref('source_users')) }}
FROM {{ ref('source_users') }}
{% endsnapshot %}
Повторный run уже завершённого snapshot
При повторном dbt snapshot без новых изменений → ничего не должно меняться. Но если updated_at неправильный (NULL или fake update без реальных изменений) → создаются дубликаты записи с одинаковыми колонками.
Detection:
SELECT user_id, COUNT(*) FROM users_snapshot
GROUP BY 1, plan, status, country
HAVING COUNT(*) > 1;
Performance на 100M+ rows
Каждый snapshot — это MERGE. Без правильных indices/cluster keys на target — full scan каждый раз.
Solution:
-- Snowflake
ALTER TABLE users_snapshot CLUSTER BY (user_id, dbt_valid_from);
-- PostgreSQL
CREATE INDEX idx_users_snapshot_uid_valid
ON snapshots.users_snapshot (user_id, dbt_valid_from);
FAQ
Snapshot vs Incremental: какой выбрать?
- Snapshot — для slowly changing dimensions (users, products, plans). Хранит history.
- Incremental — для fast-growing facts (events, orders). Append-only.
- Если нужна history фактов — incremental snapshot table с partition by date.
Как мигрировать existing данные в snapshot?
Если уже есть current users → запустить dbt snapshot — создаст snapshot со всеми текущими записями (valid_from = now()). Historical записи не появятся (их нет в source). Чтобы получить history → backfill из логов / event store.
Можно ли snapshot на view?
Да, если view содержит unique_key + timestamp. Но perf может быть плохой — каждый snapshot материализует view + сравнивает.
Какая partition strategy для snapshot table?
PARTITION BY dbt_valid_from (по месяцам или неделям). Это позволяет filter «active rows» (dbt_valid_to IS NULL) с partition pruning.
Как откатить плохой snapshot?
dbt не предоставляет rollback напрямую. Решение:
DELETE FROM users_snapshot WHERE dbt_valid_from = '2024-03-15'(удалить bad versions)UPDATE ... SET dbt_valid_to = NULLдля previous versions (re-open)- Запустить
dbt snapshotповторно с правильными данными
Что дальше
- 🧪 SQL-тренажёр — практика SQL для snapshot queries
- 🧠 3000+ вопросов с собесов — 50+ про dbt
- 📚 dbt практический гайд — первые шаги
- 🔄 dbt Incremental Models — append-only стратегии
- 🧪 dbt Tests — quality checks для snapshot
- ⚡ Airflow DAG patterns — оркестрация dbt snapshot
Источники
- docs.getdbt.com/docs/build/snapshots — официальная документация
- discourse.getdbt.com — community discussions
- Kimball SCD Type 2 — теория за SCD
Snapshot — это страховка от потерянной истории. Открой SQL-тренажёр и тренируй time-travel queries — это reusable навык для любых SCD-данных независимо от dbt.