SCD типы для Data Engineer: гайд для собеса

Готовься к собесу аналитика как в Duolingo
10 минут в день — SQL, Python, A/B, метрики. 1700+ вопросов в Telegram
Открыть Карьерник в Telegram

Карьерник — Duolingo для аналитиков: 10 минут в день тренируй SQL, Python, A/B, статистику, метрики и ещё 3 темы собеса. 1500+ вопросов в Telegram-боте. Бесплатно.

Что такое SCD и зачем

SCD (Slowly Changing Dimensions) — стратегии хранения изменений в таблицах измерений Kimball-модели. Когда у клиента сменился адрес, у товара — категория, у сотрудника — отдел, есть выбор: затереть старое значение, сохранить историю, или сохранить только последнее предыдущее.

Главная боль без SCD — отчёт «выручка по регионам за прошлый год» считается по текущей привязке клиентов к региону. Клиент переехал из Москвы в Питер в декабре — его январская покупка в Москве вдруг стала питерской. Менеджер по Москве недосчитался выручки, бухгалтерия спрашивает, что произошло. Это классический баг отчётности, и решается он через SCD Type 2.

На собесе DE спросят: «Какие типы SCD знаете?» — нужно назвать минимум 0, 1, 2 и понять кейсы для каждого.

SCD Type 0: фиксация

Значение никогда не меняется. Если в исходнике обновилось — игнорируем.

Когда использовать: дата создания клиента, оригинальный канал привлечения, первая покупка. Эти атрибуты по бизнес-смыслу неизменны.

-- dim_user (Type 0 для signup_channel)
| user_id | signup_channel | created_at          |
|---------|----------------|---------------------|
| 1       | organic        | 2024-03-15 10:00:00 |

Если в проде в users поменяли signup_channel с organic на paid — DWH остаётся с organic. Это намеренно.

Реализация: на уровне ETL просто не апдейтим колонку. В dbt — игнорируем поле в snapshot/merge.

SCD Type 1: перезапись

Хранится только текущее значение. История теряется.

Когда использовать: опечатки, телефоны, email — то, где исторические значения не нужны для аналитики.

-- dim_user (Type 1 для email)
| user_id | email                |
|---------|----------------------|
| 1       | new@example.com      |  -- было old@example.com, обновили

Реализация — UPSERT по user_id:

MERGE INTO dim_user AS dst
USING staging_users AS src
ON dst.user_id = src.user_id
WHEN MATCHED THEN UPDATE SET email = src.email, phone = src.phone
WHEN NOT MATCHED THEN INSERT (user_id, email, phone) VALUES (src.user_id, src.email, src.phone);

В dbt — incremental-модель со стратегией merge и unique_key='user_id'.

Type 1 — самый дешёвый, но необратимо стирает историю. Прежде чем выбирать его — спросить аналитика: «Нам когда-нибудь понадобится знать, что email был раньше другим?»

SCD Type 2: история версий

На каждое изменение — новая строка. У строк есть valid_from / valid_to и/или is_current флаг.

Когда использовать: адрес клиента, отдел сотрудника, категория товара, тариф подписки. Всё, что влияет на отчётность по периодам.

-- dim_user (Type 2 для region)
| user_id | region | valid_from          | valid_to            | is_current |
|---------|--------|---------------------|---------------------|------------|
| 1       | MSK    | 2024-01-01 00:00:00 | 2024-12-15 14:30:00 | FALSE      |
| 1       | SPB    | 2024-12-15 14:30:00 | 9999-12-31 00:00:00 | TRUE       |

Запрос «выручка по региону на день покупки»:

SELECT
    d.region,
    SUM(o.amount) AS revenue
FROM fct_orders o
JOIN dim_user d
    ON o.user_id = d.user_id
   AND o.created_at >= d.valid_from
   AND o.created_at <  d.valid_to
GROUP BY d.region;

Реализация в чистом SQL — три шага: закрыть текущую версию (UPDATE valid_to=NOW(), is_current=false), вставить новую версию, обработать новых пользователей.

В dbt — snapshot:

{% snapshot dim_user_snapshot %}
{{ config(
    unique_key='user_id',
    strategy='check',
    check_cols=['region', 'tariff']
) }}
SELECT user_id, region, tariff, email FROM {{ source('raw', 'users') }}
{% endsnapshot %}

Surrogate key — отдельная колонка user_sk (sha256 от user_id + valid_from), нужна для джоинов в фактах, чтобы зацепить именно эту версию строки.

Готовься к собесу аналитика как в Duolingo
10 минут в день — SQL, Python, A/B, метрики. 1700+ вопросов в Telegram
Открыть Карьерник в Telegram

SCD Type 3: одно прошлое значение

Хранится текущее и одно предыдущее значение в отдельных колонках.

Когда использовать: редко. Кейс — нужно знать «нынешний и предыдущий», но не всю историю. Например, текущий и прошлый менеджер клиента.

| user_id | manager_current | manager_previous | manager_changed_at  |
|---------|-----------------|------------------|---------------------|
| 1       | Anna            | Boris            | 2025-04-01 10:00:00 |

Реализация:

UPDATE dim_user
SET manager_previous = manager_current,
    manager_current = 'Catherine',
    manager_changed_at = NOW()
WHERE user_id = 1;

Type 3 редко встречается на практике — обычно либо Type 1 (не нужна история), либо Type 2 (нужна полная). Если на собесе спросили «когда Type 3?» — честный ответ «редко, обычно для одной критичной смены при ограничении на размер таблицы».

SCD Type 6: гибрид 1+2+3

Одна таблица содержит признаки всех трёх типов: история версий (Type 2), плюс колонка с актуальным значением во всех строках (Type 1), плюс колонка с предыдущим значением (Type 3).

| user_sk | user_id | region | region_current | valid_from | valid_to | is_current |
|---------|---------|--------|----------------|------------|----------|------------|
| 1       | 1       | MSK    | SPB            | 2024-01-01 | 2024-12-15 | FALSE |
| 2       | 1       | SPB    | SPB            | 2024-12-15 | 9999-12-31 | TRUE  |

Зачем region_current во всех строках: чтобы аналитик мог одним JOIN получить историю покупок клиента с пометкой «где он сейчас». Без этого нужен self-join.

Type 6 — мощно, но сложно поддерживать: при каждом обновлении нужно update region_current во ВСЕХ исторических строках клиента. На больших dimension-ах — дорогая операция.

На собесе Type 6 спрашивают редко. Знать — плюс к экспертизе, реализовывать — только если требование явно.

Частые ошибки

Использовать Type 1, потом плакать. Год назад «история не нужна», сейчас CFO просит cohort-анализ — а данных нет. Если есть сомнения — Type 2.

Type 2 без surrogate key. Джоины фактов с dimension по user_id без учёта valid_from/valid_to дают M:N. Surrogate key + правильный JOIN решают.

Открытый valid_to как NULL. Удобнее 9999-12-31: BETWEEN-фильтры работают без OR valid_to IS NULL.

Игнорировать late-arriving facts. Факт пришёл вчера, но датирован прошлым годом — нужен правильный SCD-срез на ту дату. Если справочник уже несколько раз менялся — джоин даст не ту версию. Решение: всегда джоинить через valid_from <= fact_date < valid_to.

Triggers вместо batch update. Ручные триггеры на изменения в OLTP, чтобы пушить в DWH — фрагильно. Лучше — CDC или регулярный snapshot.

Type 2 на каждое поле. Если хранить историю по каждой колонке — таблица раздуется в десятки раз. Type 2 — только на бизнес-критичные атрибуты, остальные Type 1.

Связанные темы

FAQ

Какой тип SCD по умолчанию выбирать?

Type 1 — для технических полей (email, телефон), Type 2 — для бизнес-атрибутов, влияющих на отчётность по периодам (регион, отдел, тариф). Если не уверен — Type 2: дешевле сначала хранить историю, чем потом восстанавливать.

Как реализовать SCD Type 2 в ClickHouse?

ReplacingMergeTree с версионной колонкой и регулярным OPTIMIZE FINAL даёт что-то близкое к Type 2, но не идентично — старые версии хранятся до мержа. Для строгого SCD2 — отдельная таблица с явными valid_from/valid_to и INSERT при изменениях, либо dbt snapshot поверх MergeTree.

Что такое late-arriving dimension?

Факт пришёл, а строки в dimension под нужный момент времени ещё нет (или она ещё не имеет нужной версии). Решения: «inferred member» (заглушка с placeholder, потом обновляется), либо отложить факт до прихода dimension.

Surrogate key — это всегда автоинкремент?

Нет. Чаще — hash от business key + valid_from (детерминированно, можно пересчитать). Auto-increment плох тем, что после full-refresh ID меняются и фактовые таблицы ломаются.

Slowly vs Rapidly Changing Dimension — есть разница?

Slowly — изменения редкие (раз в месяц/год). Rapidly — частые (несколько раз в день). RCD требуют отдельных стратегий — обычно выносят быстро меняющийся атрибут в junk-dimension или mini-dimension, чтобы не раздувать основную.

Это официальная информация?

Нет. Статья основана на работах Kimball, документации dbt и опыте практики. Конкретные требования к моделированию зависят от компании и команды.


Тренируйте моделирование данных — откройте тренажёр с 1500+ вопросами для собесов.