Learning Platform
Глоссарий Troubleshooting
Урок 20.04 · 21 мин
Продвинутый
capstonescd2date-dimensiondimensional-modeling

Капстоун: SCD2 и date dimension

В прошлом уроке мы построили star schema проката: fact-таблица fct_rental и четыре dimensions. Но мы оставили два пробела. Первый: dimensions «застыли» — мы не подумали, что атрибуты клиента или станции меняются во времени. Второй: dim_date мы лишь упомянули. Этот урок закрывает оба — реализуем SCD Type 2 и детально построим date dimension.

Оба пробела — про время. SCD2 отвечает на вопрос «как сохранить историю изменений измерения». Date dimension — на вопрос «как правильно моделировать само время как измерение». Без них star schema из прошлого урока работает, но даёт неверные ответы на исторические вопросы.


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

Конкретный сценарий. Клиент Анна зарегистрировалась в Москве, в марте совершила 5 аренд. В апреле она переехала в Казань и обновила город в профиле. В мае — ещё 3 аренды.

Аналитик спрашивает: «сколько выручки принесли клиенты из Москвы». Если в dim_customer город просто перезаписан на «Казань» (это SCD Type 1 из модуля про SCD), то все аренды Анны — и мартовские московские, и майские казанские — посчитаются как «Казань». Мартовская выручка задним числом «переехала» в Казань. Отчёт стал неверным.

Корень проблемы: fact-строка должна быть связана с тем состоянием dimension, которое было актуально в момент события. Мартовская аренда — с «московской» Анной, майская — с «казанской». SCD Type 1 это состояние не хранит. Нужен SCD Type 2.


Решение: SCD Type 2

Из модуля про Slowly Changing Dimensions: SCD Type 2 при изменении атрибута вставляет новую строку с новым surrogate key, не трогая старую. Каждая версия сущности — отдельная строка. Для этого dimension получает служебные столбцы: effective date, end date, current flag.

Перепроектируем dim_customer под SCD2:

CREATE TABLE dim_customer (
    customer_key     INTEGER PRIMARY KEY,   -- surrogate key: УНИКАЛЕН НА ВЕРСИЮ
    customer_id      INTEGER,               -- durable key: один на сущность
    full_name        VARCHAR(150),
    city             VARCHAR(100),          -- меняющийся атрибут
    -- служебные столбцы SCD2:
    effective_date   DATE    NOT NULL,      -- с какого дня версия актуальна
    end_date         DATE    NOT NULL,      -- по какой день (9999-12-31 у текущей)
    is_current       BOOLEAN NOT NULL       -- флаг текущей версии
);

Ключевое различие двух идентификаторов (из модуля про ключи и SCD):

  • customer_keysurrogate key, уникален на каждую версию. У московской Анны один key, у казанской — другой.
  • customer_iddurable key, один на сущность на всю её жизнь. У обеих версий Анны он одинаков. Durable key позволяет собрать все версии одного клиента.

Посмотрим, что лежит в dim_customer после переезда Анны:

customer_key | customer_id | full_name | city   | effective_date | end_date   | is_current
501          | 1001        | Анна      | Москва | 2026-01-10     | 2026-04-15 | false
502          | 1001        | Анна      | Казань | 2026-04-15     | 9999-12-31 | true

Две строки на одного клиента. У старой версии end_date проставлен датой переезда и is_current = false. У новой версии effective_date = дата переезда, end_date = заглушка 9999-12-31, is_current = true. История сохранена полностью.

SCD2: новая версия не перезаписывает старую
customer_key 501: МоскваВерсия Анны до переезда: effective 2026-01-10, end 2026-04-15, is_current false
переезд: вставляется новая строка
customer_key 502: КазаньВерсия Анны после переезда: effective 2026-04-15, end 9999-12-31, is_current true

Как fact-таблица связывается с версией

Теперь главное — как fct_rental ссылается на правильную версию. Здесь и работает SCD2.

При загрузке аренды в fact-таблицу ETL ищет в dim_customer версию клиента, актуальную на дату аренды. Правило поиска: найти строку, где customer_id совпадает И дата аренды попадает в интервал [effective_date, end_date).

-- При загрузке fct_rental подбираем версию dimension на дату аренды
INSERT INTO fct_rental (customer_key, date_key, rental_amount, ...)
SELECT
    dc.customer_key,        -- key ИМЕННО ТОЙ версии, что действовала
    ...
FROM staging_rentals r
JOIN dim_customer dc
  ON dc.customer_id = r.customer_id
 AND r.rental_date >= dc.effective_date    -- аренда попадает
 AND r.rental_date <  dc.end_date          -- в интервал версии
...

Результат: мартовская аренда Анны получит customer_key = 501 (Москва), майская — customer_key = 502 (Казань). Каждая fact-строка навсегда «приклеена» к тому состоянию клиента, которое было в момент события. Теперь запрос «выручка клиентов из Москвы» через JOIN dim_customer WHERE city = 'Москва' посчитает только мартовские аренды — верно.

Запрос аналитикаКак отвечается через SCD2
Выручка по городу «как было»JOIN по customer_key — каждая аренда с историчным городом
Текущий город всех клиентовФильтр is_current = true
Все версии конкретного клиентаJOIN по customer_id (durable key)
TIP

Какие именно атрибуты вести по SCD2 — проектное решение. Не каждый атрибут требует истории. Город клиента — да (от него зависят отчёты «как было»). Опечатка в имени, которую просто исправили, — обычно Type 1 (перезапись), истории не нужно. В капстоуне разумно вести по SCD2 город клиента и, например, переименования станций; технические поля можно оставить Type 1. SCD2 на каждый атрибут раздувает dimension без пользы.

dbt snapshot автоматизирует SCD2 — механика для dim_customer проката

Date dimension: время как полноценное измерение

Второй пробел — dim_date. Возникает вопрос: зачем вообще отдельная таблица дат, если в fact-таблице уже есть started_at? Почему не фильтровать прямо по нему?

Потому что у даты для аналитики есть десятки атрибутов, которых нет в самом timestamp: день недели, номер месяца, название месяца, квартал, год, признак выходного, признак праздника, номер недели, фискальный период. Date dimension (из модуля про dimension-таблицы) — это таблица со строкой на каждый календарный день и всеми этими атрибутами заранее вычисленными.

CREATE TABLE dim_date (
    date_key        INTEGER PRIMARY KEY,   -- "умный" ключ YYYYMMDD: 20260520
    full_date       DATE    NOT NULL,
    day_of_week     VARCHAR(15),           -- 'среда'
    day_number      INTEGER,               -- 20
    month_number    INTEGER,               -- 5
    month_name      VARCHAR(15),           -- 'май'
    quarter         INTEGER,               -- 2
    year            INTEGER,               -- 2026
    is_weekend      BOOLEAN,               -- false
    is_holiday      BOOLEAN                -- false
);

-- Строка для 20 мая 2026:
-- 20260520 | 2026-05-20 | среда | 20 | 5 | май | 2 | 2026 | false | false

Две детали заслуживают внимания.

«Умный» ключ. date_key — это YYYYMMDD целым числом: 20 мая 2026 -> 20260520. Из модуля про ключи вы помните правило «surrogate key без смысла». Date dimension — известное исключение из этого правила: ключ-дата осмысленный, потому что даты не меняются и не появляются заново, и читаемый ключ удобен (по date_key сразу видно дату, можно сортировать и фильтровать диапазоном без JOIN).

Заполнение заранее. dim_date не наполняется по мере поступления данных — её генерируют наперёд, сразу на годы вперёд (например, 2020-2035). Это разовая операция: дни известны заранее, ждать их «прихода» незачем.

Зачем всё это? Date dimension превращает сложные календарные запросы в простые. «Выручка по будням против выходных» без dim_date — это вычисление дня недели из timestamp в каждом запросе. С dim_date — это JOIN dim_date ... GROUP BY is_weekend. «Выручка по кварталам», «сравнение год к году» — всё становится тривиальным GROUP BY по готовому атрибуту.

Date dimension: одна строка на день, атрибуты вычислены заранее
fct_rentalFact-таблица аренд: FK date_key ссылается на dim_date
JOIN по date_key
dim_dateСтрока на каждый день: день недели, месяц, квартал, год, выходной, праздник — всё готово

С SCD2 и date dimension star schema проката завершена и корректна: она хранит историю изменений измерений и даёт удобный аналитический доступ ко времени. В финальном уроке мы пройдём по всей модели с критическим взглядом — поищем анти-паттерны, обсудим документацию и code review, и научимся защищать проектное решение.


Попробуй сам

Продолжите кейс онлайн-библиотеки.

  1. У читателя есть «категория» (студент, преподаватель, обычный), которая может меняться. Перепроектируйте dim_reader под SCD2: добавьте effective_date, end_date, is_current, разделите surrogate key и durable key.
  2. Смоделируйте: читатель был «студент», стал «преподаватель». Покажите две строки в dim_reader после изменения.
  3. Напишите условие JOIN, по которому fact-таблица выдач подберёт версию читателя, актуальную на дату выдачи.
  4. Спроектируйте dim_date для библиотеки. Какой «умный» ключ возьмёте? Какие атрибуты будут полезны для отчётов «выдачи по месяцам» и «просрочки по дням недели»?
  5. Решите, какие атрибуты читателя вести по SCD2, а какие — по Type 1. Обоснуйте.

Проверка знанийKnowledge check
Зачем в star schema проката нужны SCD Type 2 и date dimension, и как каждый из них устроен?
ОтветAnswer
SCD Type 2 нужен, потому что атрибуты измерений меняются во времени, а fact-строка должна быть связана с тем состоянием dimension, которое было актуально в момент события. Если клиент переехал из Москвы в Казань и город просто перезаписан (SCD Type 1), то все его аренды — и старые московские, и новые казанские — посчитаются как Казань, и исторический отчёт станет неверным. SCD Type 2 при изменении атрибута вставляет новую строку с новым surrogate key, не трогая старую: каждая версия сущности — отдельная строка. Dimension получает служебные столбцы effective_date (с какого дня версия актуальна), end_date (по какой день, у текущей версии заглушка 9999-12-31) и is_current. Различают два идентификатора: surrogate key уникален на каждую версию, durable key один на сущность на всю жизнь и позволяет собрать все её версии. При загрузке fact-таблицы ETL подбирает версию dimension, актуальную на дату события: ищет строку, где durable key совпадает и дата события попадает в интервал [effective_date, end_date). Так каждая fact-строка навсегда приклеена к историчному состоянию измерения. Date dimension нужна, потому что у даты для аналитики десятки атрибутов, которых нет в самом timestamp: день недели, месяц, квартал, год, признак выходного и праздника, номер недели, фискальные периоды. Date dimension — таблица со строкой на каждый календарный день и всеми этими атрибутами, вычисленными заранее. У неё две особенности: умный ключ формата YYYYMMDD (известное исключение из правила "surrogate key без смысла", потому что даты не меняются и читаемый ключ удобен) и заполнение наперёд на годы вперёд разовой операцией. Date dimension превращает сложные календарные запросы в простой GROUP BY по готовому атрибуту.

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

Результат: 0 из 0
Аналитический
Вопрос 1 из 4. Клиент переехал из Москвы в Казань. Почему SCD Type 1 (перезапись города) даёт неверный отчёт 'выручка клиентов из Москвы'?

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

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

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

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