Learning Platform
Глоссарий Troubleshooting
Урок 09.03 · 22 мин
Начальный
scdscd-type-2historydimensional-modelingkimball

Проблема: атрибуты меняются

В OLTP-системе клиент может изменить адрес, email, segment. Простой UPDATE:

UPDATE customers SET country = 'CA' WHERE id = 42;  -- был US

В DWH этого недостаточно. Возникают аналитические вопросы:

  • Сколько мы продали клиентам в US на момент Q1 2025?
  • Если клиент изменил country с US на CA — куда отнести его прошлогодние заказы?
  • Что было raw-атрибутом клиента, когда он размещал заказ 12345?

Если мы просто UPDATE-нем dim_customer — мы перепишем историю. Прошлогодний отчёт “продажи в US” станет неверным, потому что country клиента уже стал CA.

Эта проблема — Slowly Changing Dimensions (SCD). Кимбалл определил несколько типов SCD, каждый — стратегия обработки изменений в dimension над временем.

SCD Type 0 и Type 1: крайние случаи

Type 0: Original — никогда не меняется

Атрибут заморожен. Используется для immutable значений: created_date, original_signup_country. Самый простой тип, используется редко.

Type 1: Overwrite

При изменении атрибута просто перезаписываем в DWH. История не сохраняется.

-- Источник: customer 42 сменил email
UPDATE dim_customer
SET email = '[email protected]'
WHERE customer_id = 42;

Подходит для:

  • Опечаток и corrections (исправление неправильно введённого имени).
  • Атрибутов без аналитического значения (phone number).
  • Когда history не важна.

Не подходит для атрибутов, по которым делается аналитика во времени (country, segment, salary band).

SCD Type 2: Add row — сохраняем историю

Самый используемый тип. При изменении атрибута создаётся новая строка в dim. Старая помечается как expired, новая — current.

CREATE TABLE dim_customer (
  customer_key BIGINT PRIMARY KEY,        -- surrogate, разный для каждой версии
  customer_id BIGINT,                      -- natural key, одинаковый для всех версий
  name TEXT,
  email TEXT,
  country TEXT,
  segment TEXT,
  -- SCD Type 2 metadata
  valid_from DATE,                         -- когда строка стала актуальной
  valid_to DATE,                           -- когда стала не актуальной (NULL = current)
  is_current BOOLEAN                       -- shortcut для WHERE is_current
);

Жизненный цикл строк

День 1: customer 42 регистрируется, country = US.

customer_key | customer_id | country | valid_from | valid_to   | is_current
1            | 42          | US      | 2025-01-01 | NULL       | TRUE

День 100: customer 42 переехал в CA.

customer_key | customer_id | country | valid_from | valid_to   | is_current
1            | 42          | US      | 2025-01-01 | 2025-04-11 | FALSE
2            | 42          | CA      | 2025-04-11 | NULL       | TRUE

Старая строка получила valid_to = дата изменения, is_current = FALSE. Создана новая с новым surrogate customer_key. valid_to = NULL означает “актуальная сейчас”. Обрати внимание: valid_to старой строки совпадает с valid_from новой — это полуоткрытый интервал, поэтому point-in-time join valid_from <= order_date AND order_date < valid_to не теряет граничный день и не даёт перекрытий.

Связь с fact-таблицами

Это где surrogate keys критичны. Fact записи указывают на конкретную версию dimension.

Заказ 12345 от 2025-02-15 (когда customer был US): customer_key = 1. Заказ 67890 от 2025-06-01 (когда customer стал CA): customer_key = 2.

fact_orders:
order_id | customer_key | order_date  | amount
12345    | 1            | 2025-02-15  | 100
67890    | 2            | 2025-06-01  | 200

Теперь запрос “продажи в US в 2025” корректно возвращает заказ 12345 (в момент заказа клиент был US-resident), а заказ 67890 идёт в CA. История сохранена точно.

SCD Type 2: история через rows

Каждое изменение — новая строка с новым surrogate key

Day 1: customer 42 регистрируется. Один row в dim_customer. valid_to = NULL, is_current = TRUE. Fact записи в этот момент ссылаются на customer_key = 1
Day 100: customer 42 переехал. Старая строка получает valid_to = дата изменения (= valid_from новой строки, полуоткрытый интервал без зазора), is_current = FALSE. Новая строка с customer_key=2, country=CA, valid_to=NULL
Fact tables: старые orders ссылаются на customer_key=1 (US-version), новые — на customer_key=2 (CA-version). История точно сохранена, аналитика 'продажи в US' за 2025 Q1 корректна

Реализация: dbt snapshot

В практике SCD Type 2 автоматизирует dbt через snapshot:

{% snapshot dim_customer_snapshot %}
{{
  config(
    target_schema='snapshots',
    unique_key='customer_id',
    strategy='check',
    check_cols=['country', 'segment', 'email']
  )
}}
SELECT customer_id, name, email, country, segment
FROM {{ source('postgres', 'customers') }}
{% endsnapshot %}

При каждом run dbt сравнивает source с предыдущим snapshot. Если check_cols изменились — expires old row (dbt_valid_to = NOW()) и inserts new row (dbt_valid_from = NOW()). Создаёт metadata колонки: dbt_valid_from, dbt_valid_to, dbt_scd_id. Это стандарт Modern Data Stack — большинство production dbt-проектов используют snapshots для SCD.

SCD Type 3: Previous column

Хранится предыдущее значение в отдельной колонке. Только last change запоминается, более глубокая история теряется.

CREATE TABLE dim_customer (
  customer_key BIGINT PRIMARY KEY,
  customer_id BIGINT,
  country TEXT,                    -- current
  previous_country TEXT,           -- one step back
  country_change_date DATE
);

После переезда US -> CA:

customer_key | country | previous_country | country_change_date
1            | CA      | US               | 2025-04-11

Подходит когда нужно простое сравнение “before/after” без полной истории. Используется редко — Type 2 в большинстве случаев лучше.

SCD Type 6: Hybrid (кратко)

Type 6 совмещает Type 1, Type 2 и Type 3 в одной dim:

  • Type 2 строки для исторической accuracy.
  • Type 1 атрибут с current value (для быстрых current-запросов без поиска active row).
  • Type 3 атрибут с previous value (для before/after сравнений).

Это даёт максимальную гибкость, но требует поддержки трёх типов одновременно. Используется в зрелых DWH для критичных dimensions.

Какой type когда использовать

Decision matrix для SCD

Какой type подходит для каждого атрибута

Type 1 для: corrections, опечатки, технические атрибуты без аналитики во времени. История не нужна
Type 2 для: country, segment, salary band — атрибуты по которым делается аналитика во времени. Самый частый выбор, стандарт
Type 3 для: простое before/after сравнение последнего изменения. Редко в современной практике
Type 6 для: критичные dimensions где нужно одновременно история (Type 2), current state (Type 1), и previous (Type 3). Сложно, для зрелых DWH
АтрибутTypeПочему
Имя клиента (опечатка)Type 1Correction, история не нужна
CountryType 2Используется в historical analytics
SegmentType 2Tracking customer journey
EmailType 1Не аналитический атрибут
Signup dateType 0Immutable
Salary bandType 2Compensation analysis
PhoneType 1Не для аналитики
Subscription tierType 2Critical for revenue analytics
Address (точный)Type 1Для current shipping, не для history
Country (для tax)Type 2Critical for tax history

Производительность и storage

SCD Type 2 раздувает dim_customer. Если customer обновляется в среднем 2 раза в год, и компания работает 10 лет — для каждого customer примерно 20 строк. На 1M customers — это 20M строк dim. Не катастрофа, но dim становится больше fact. Современные columnar DWH справляются.

Оптимизации:

  1. Не делать Type 2 на атрибутах, которые часто меняются (выносить в mini-dim).
  2. Использовать смысловые change detection — не каждое CDC event = SCD изменение.
  3. Архивировать старые versions, если глубокая история не нужна.

Попробуй сам

  1. Реализуй SCD Type 2 руками в DuckDB. Создай table customers, симулируй 3-4 изменения country/segment. Напиши SQL для update dim_customer с правильной экспирацией.
  2. Используй dbt snapshot для того же — посмотри, как dbt автоматически creates valid_from/valid_to.
  3. Создай fact_orders с FK к dim_customer (через customer_key). Сделай запрос “продажи по country” за период, когда customer был US, и убедись что результаты корректны после change.
  4. Спроектируй: какие атрибуты dim_employee в HR DWH — Type 1, Type 2, Type 0? (salary, department, manager, hire_date, name, address).
Проверка знанийKnowledge check
Команда строит dim_customer с атрибутами: name, email, country, segment, age_band, signup_date, address, phone. Определи, какой SCD type подходит каждому и аргументируй.
ОтветAnswer
Анализ по атрибутам. name — Type 1 (изменения как исправление опечатки, история не нужна). email — Type 1 (не аналитический атрибут). country — Type 2 (критично для tax, marketing, historical analytics; нужно знать 'в момент заказа клиент был в US'). segment — Type 2 (customer journey tracking, retention analysis требует истории). age_band — Type 2 (если меняется при дне рождения; альтернативно Type 1, если считать на лету из birth_date). signup_date — Type 0 (immutable). address — обычно Type 1 (точный адрес для current shipping; для analytics используется country с Type 2). phone — Type 1 (не для аналитики).
dbt snapshots: автоматический SCD Type 2 без ручного SQL

Дизайн dimensional моделей разбираем в будущем dwh-design-course.

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

Результат: 0 из 0
Прикладной
Вопрос 1 из 5. Customer переехал из US в CA. В DWH делаем простой UPDATE dim_customer SET country = 'CA' WHERE customer_id = 42. Почему это плохо для аналитики?

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

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

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

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