Slowly Changing Dimensions: Type 1/2/3/6 без боли
Когда что выбирать, что считается «изменением», как влияет на запросы и размер БД, и как мигрировать без переписывания витрин.
Спроектируй стратегию SCD для измерения {{dimension}}. Частота изменений: {{change_rate}}. Зачем история: {{history_need}}.
Шаг 1. Классифицируй каждый атрибут
Не «у измерения SCD-тип X». У каждого атрибута свой тип.
| Атрибут | Меняется | Нужна история? | Тип | | name | редко | нет | 1 | | segment | месячно | да, для отчётов | 2 | | email | редко | только «текущий + предыдущий» | 3 | | address | средне | да + «текущий» рядом | 6 |
Шаг 2. Выбери тип
Type 1 — overwrite. UPDATE на месте. История теряется.
- Когда: исправление опечатки, атрибут не интересен исторически (имя клиента).
- Минус: отчёты задним числом «переписывают» прошлое.
Type 2 — versioned rows. Новая строка при изменении.
- Структура:
surrogate_key, natural_key, attr..., valid_from, valid_to, is_current. - Запросы по «как было на дату»:
JOIN fact ON dim.surrogate_key = fact.dim_key(исторический FK) илиWHERE event_date BETWEEN valid_from AND valid_to. - Минус: измерение растёт. Для 10M customers с месячным изменением — 120M строк за год.
Type 3 — prior value column. segment_current, segment_previous, segment_changed_at.
- Когда: интересна только последняя смена.
- Минус: не работает для трёх и более состояний.
Type 6 — гибрид (1+2+3). Type 2 строки + Type 1 «вечнотекущая» колонка + Type 3 «previous».
- Запросы «как сейчас» и «как было тогда» в одной таблице.
- По умолчанию для важных бизнес-измерений.
Шаг 3. Сурогатные ключи
Type 2 ОБЯЗАТЕЛЬНО на сурогатных ключах. Факт ссылается на сурогатный, не на natural:
fact_orders.customer_key = 12345(конкретная версия)- НЕ
fact_orders.customer_id = 'C-9821'(вечный)
Natural key сохраняй в dim для join'ов «дай мне ВСЕ версии этого клиента».
Шаг 4. Эффект на запросы
«Сколько заказов от Premium-клиентов в Q3 2025?»
- Type 1: соврёт — кто сейчас Premium, тот всегда был.
- Type 2: правда — JOIN по surrogate, segment === 'Premium' на момент заказа.
Это решение принимай ДО построения витрин. Перевод Type 1 → Type 2 — болезненная переделка.
Шаг 5. Мониторинг
- Размер:
COUNT(*) GROUP BY natural_key— нет ли клиента с 500 версий из-за бага в источнике (false changes). - Целостность:
SUM(CASE WHEN is_current THEN 1 ELSE 0 END) GROUP BY natural_key— должно быть ровно 1. - Дыры:
valid_to - LEAD(valid_from)— нет ли пропусков во времени.
Anti-patterns
- ❌ Type 2 на всём измерении «на всякий случай» — таблица раздуется на 10× без бизнес-ценности
- ❌ Type 1 для атрибутов, по которым считают историю (segment, plan_tier) — отчёты задним числом сломаются
- ❌ Сравнивать строки по hash всей строки для детекции изменения — поле
updated_atисточника поменяется без реального изменения, получишь дубликаты - ❌
valid_to = NULLдля текущей записи — индексы и JOIN'ы ломаются; используй9999-12-31 - ❌ Факт ссылается на natural_key — теряешь смысл Type 2, JOIN всегда возвращает «как сейчас»
Дизайн undo / redo: что отменяемо, как, до куда
Scope (per-action / per-session), timeline UI, keyboard, что НЕ должно быть undoable. Без «undo есть, но не для всего».
Dimensional modeling по Кимбаллу: факты и измерения
Звезда vs снежинка, типы факт-таблиц, conformed/role-playing/junk-измерения, выбор грануляции и где ломаются новички.
3NF vs star vs One Big Table: когда что выбирать
Trade-offs read perf / write complexity / storage для OLTP, OLAP и аналитики. Как мигрировать между моделями без переписывания клиентов.