Раздел 26 · Data Engineering

Моделирование данных: star schema, SCD, антипаттерны

middle-senior~70 мин

без входаВойди, чтобы отмечать прогресс, вести заметки и решать задачи в редакторе. войти

Перед этим уроком

Моделирование данных: star schema, SCD, антипаттерны

В уроках 01-07 мы говорили про движки и форматы: ClickHouse, lakehouse, Parquet, Kafka, Flink. Этот урок про схему: как разложить данные так, чтобы аналитические запросы были осмысленными, быстрыми и не разваливались при эволюции бизнеса.

Аудитория: ты разрабатываешь pipeline-ы и таблицы для аналитики, заходил в OLTP-схемы и понимаешь нормальные формы. На работе встречается вопрос “почему отчёт за прошлый квартал теперь показывает другие числа” или “как показать, какой sales rep был attached к клиенту в момент сделки, если он с тех пор сменился”. Урок отвечает.

Не цель: научить писать DDL с нуля или быть Kimball-сертифицированным архитектором. Цель: дать рабочий каркас (star schema + SCD), показать, как это легализуется в современных движках (ClickHouse, Snowflake, Iceberg), и обозначить главные грабли.

0. Зачем отдельная дисциплина для аналитики

OLTP-схема (Postgres под приложение), это нормализация: каждый факт в одном месте, минимум дублирования, защита целостности через FK. Это правильно для приложения: write-патт сравнительно простой, dataset работает в строковом представлении.

OLAP-схема (DWH, lakehouse), это денормализация: один большой “factful” датасет с дублированием атрибутов. Это правильно для аналитики:

  • Колоночное хранилище (см. урок 01) дешёвое в storage, дублирование не страшно.
  • JOIN на 100M+ строк с несколькими измерениями, дорогой; денормализация снижает потребность в JOIN.
  • Бизнес-вопросы быстро меняются (“посчитай по сегменту, который я только что выдумал”), и схема должна это переживать.

Различие 3NF (OLTP) и dimensional (OLAP), фундаментальное. Один и тот же набор сущностей моделируется по-разному в зависимости от того, кто будет читать.

Row vs column layout: что физически читает диск
Layout
Запрос
прочитано с диска0B
из них полезных0B
оверхед0%

Виджет напоминает, что аналитический движок не считывает строки целиком, а только нужные колонки. Это позволяет dimensional-схеме иметь “wide tables” (десятки колонок per row), и они не делают запрос медленнее.

1. Kimball dimensional modeling

В 1996 году Ralph Kimball описал dimensional modeling: данные раскладываются на факты и измерения, а схема собирается в виде “звезды”.

1.1 Fact vs dimension

  • Fact, измеримое событие или транзакция бизнеса. Каждая строка fact-таблицы, один такой случай: продажа, клик, авторизация, начисление зарплаты.
  • Dimension, описательный контекст факта. “Кто” (customer), “что” (product), “когда” (date), “где” (location), “как” (channel).

Пример: OrderPlaced(order_id, customer_id, product_id, date, store_id, quantity, revenue).

  • Факт: одна строка с количеством и выручкой.
  • Измерения: customer, product, date, store.

1.2 Star schema

                           ┌──────────────┐
                           │ dim_date     │
                           │  date_key PK │
                           │  year        │
                           │  quarter     │
                           │  month       │
                           │  day         │
                           │  weekday     │
                           └──────┬───────┘
                                  │
                                  │ FK
                                  │
┌──────────────┐         ┌────────▼────────┐         ┌──────────────┐
│ dim_customer │◄────────┤  fact_orders    ├────────►│ dim_product  │
│  cust_key PK │ FK      │  order_id PK    │ FK      │  prod_key PK │
│  cust_id     │         │  cust_key       │         │  prod_id     │
│  segment     │         │  prod_key       │         │  category    │
│  region      │         │  date_key       │         │  brand       │
│  ...         │         │  store_key      │         │  unit_cost   │
└──────────────┘         │  quantity       │         └──────────────┘
                         │  revenue        │
                         │  discount       │
                         └────────┬────────┘
                                  │ FK
                                  │
                           ┌──────▼───────┐
                           │ dim_store    │
                           │  store_key PK│
                           │  store_id    │
                           │  city        │
                           │  region      │
                           └──────────────┘

В центре fact-таблица; вокруг, измерения; FK от факта в каждое измерение. Запросы ходят как:

SELECT d.year, d.quarter,
       p.category,
       SUM(f.revenue) AS revenue,
       AVG(f.discount) AS avg_discount
FROM fact_orders f
JOIN dim_date d ON f.date_key = d.date_key
JOIN dim_product p ON f.prod_key = p.prod_key
WHERE c.segment = 'enterprise'
GROUP BY 1, 2, 3;

Звезда читается просто, потому что JOIN-ы плоские (все в один шаг от факта). На колоночном движке это особенно эффективно, можно скипать колонки и партиции по факту, dim-таблицы маленькие и кэшируются.

1.3 Snowflake schema

Если измерения сами имеют иерархию (dim_product -> dim_brand -> dim_manufacturer), её можно раскрутить в нормализованные таблицы:

fact_orders -> dim_product -> dim_brand -> dim_manufacturer

Это snowflake schema (одноимённое DWH унаследовало название). Преимущества: меньше дублирования, гибче эволюция. Минусы: больше JOIN-ов на каждый запрос.

В современной практике star schema, дефолт; snowflake применяется точечно для очень больших измерений с глубокой иерархией. Маленькие измерения с 2-3 уровнями оставляют денормализованными.

1.4 Galaxy / fact constellation

Множественные facts, разделяющие dimensions:

dim_customer ──┬── fact_orders
               └── fact_returns
               └── fact_payments

Один customer dimension, три факта. Это типичная ситуация в зрелом DWH, где разные процессы бизнеса раскладываются на свои факты, но используют общие справочники.

2. Fact tables: типы

Три канонических типа.

2.1 Transaction fact

Одна строка, одно событие бизнеса. OrderPlaced, PageView, LoginAttempt. Не модифицируется после записи. Это самый частый тип, и он ложится на event sourcing один-в-один.

CREATE TABLE fact_orders (
    order_id BIGINT,
    cust_key BIGINT,
    prod_key BIGINT,
    date_key INT,
    store_key INT,
    quantity INT,
    revenue DECIMAL(12, 2),
    discount DECIMAL(12, 2)
)
PRIMARY KEY (order_id);

Grain (зерно факта): “одна строка = один заказ”. Это первый и самый важный design-вопрос: зафиксировать grain до моделирования. Если в команде разные люди понимают разное под “продажа” (тикет vs итоговая транзакция vs payment), таблица превращается в неконсистентность.

2.2 Periodic snapshot fact

Одна строка, состояние процесса на конец периода (день, неделя, месяц). Например, остаток на счету каждого клиента на конец дня:

CREATE TABLE fact_account_daily (
    account_key BIGINT,
    date_key INT,
    balance DECIMAL(15, 2),
    open_orders INT,
    blocked_orders INT
);

Grain: “одна строка = один аккаунт за один день”. Storage растёт линейно (N клиентов × M дней), но даёт мгновенные ответы на исторические вопросы (“какой был баланс 01.05?”).

Альтернатива, считать из transaction fact в каждом запросе. Дешевле в storage, дороже в compute. Snapshot, классический trade-off “заранее посчитанный”.

2.3 Accumulating snapshot fact

Одна строка, состояние одного бизнес-процесса с обновляющимися датами. Идеально для процессов с фиксированными вехами:

CREATE TABLE fact_order_lifecycle (
    order_id BIGINT,
    cust_key BIGINT,
    placed_date_key INT,
    paid_date_key INT,        -- NULL до оплаты
    shipped_date_key INT,     -- NULL до отгрузки
    delivered_date_key INT,   -- NULL до доставки
    cancelled_date_key INT,   -- NULL если не отменён
    revenue DECIMAL(12, 2)
);

Grain: “одна строка = один заказ во всех его ветках”. При смене состояния (заказ оплачен) обновляется дата соответствующей вехи. Это mutable fact, что в OLAP-движках требует осторожности (см. ниже).

Anti-pattern: путать accumulating snapshot с transaction fact. Если заказ имеет много жизненных стадий, и тебе нужно “сколько заказов сейчас на каждой стадии”, это accumulating snapshot. Если тебе нужно “сколько было событий ‘orderPlaced’ и ‘orderShipped’ раздельно”, это два transaction fact-а.

урок из курса

Урок из курса «Data Engineering». Курс покупается один раз и остаётся навсегда: 500 ₽.

Оплата пока не подключена: премиум и курсы открываются промокодом.