Четырёхшаговый процесс дизайна
Прошлый урок показал, как выглядит star schema. Этот урок отвечает на следующий вопрос: как к ней прийти? С чего начать, столкнувшись с реальными бизнес-данными, и в каком порядке принимать решения?
Кимбалл дал на это чёткий ответ — четырёхшаговый процесс дизайна (four-step dimensional design process). Это не бюрократическая формальность, а проверенная последовательность, где каждый шаг опирается на предыдущий, и порядок строго обязателен. Перепутайте шаги — и получите модель, которая отвечает не на те вопросы или вообще не сходится. Этот урок разбирает все четыре шага по очереди на сквозном примере и объясняет, почему именно такой порядок.
Четыре шага
Вот они, в обязательном порядке:
- Выбрать бизнес-процесс (select the business process).
- Объявить grain (declare the grain).
- Выбрать dimensions (identify the dimensions).
- Выбрать facts (identify the facts).
Каждый последующий шаг имеет смысл только после предыдущего: dimensions выбирают под объявленный grain, facts — под тот же grain и выбранные dimensions. Порядок — это и есть метод.
Шаг 1: выбрать бизнес-процесс
Бизнес-процесс — это операционное событие реального мира, которое порождает данные и которые бизнес хочет анализировать. Не отдел, не отчёт, не таблица — именно событие. Примеры процессов: оформление заказа, отгрузка товара со склада, оплата счёта, регистрация пользователя, показ рекламного объявления, обращение в поддержку.
Почему дизайн начинается именно с процесса? Потому что каждый процесс становится одной fact-таблицей. «Продажи» — одна fact-таблица. «Отгрузки» — другая. Выбрать процесс — значит решить, что станет центром конкретной звезды.
Тонкость, на которой спотыкаются джуниоры: не путать процесс и отдел. «Отдел маркетинга» — не процесс. Внутри маркетинга есть процессы: показ объявления, клик, конверсия лида — каждый со своими данными и своей fact-таблицей. Если начать с отдела, получится мешанина разных событий в одной таблице. Начинайте всегда с конкретного глагола-события.
Возьмём сквозной пример на весь урок: сервис проката велосипедов. Бизнес-процесс, который мы моделируем: завершение поездки (один прокат велосипеда от взятия до возврата).
Хороший признак, что вы выбрали именно процесс, а не что-то другое: процесс описывается глаголом и происходит в конкретный момент или интервал времени. «Поездка завершилась», «заказ оформлен», «платёж проведён». Если ваш «процесс» описывается существительным-категорией («аналитика клиентов», «отчётность по складу») — это не процесс, копайте до события.
Шаг 2: объявить grain
Grain (зерно) — это ответ на вопрос: что представляет одна строка fact-таблицы? Какому атомарному факту соответствует одна запись?
Это самое важное решение во всём дизайне. Следующий урок целиком посвящён grain, поэтому здесь — суть и место в процессе. Grain объявляется одним точным предложением. Для нашего примера: одна строка fct_trips — это одна завершённая поездка на велосипеде.
Почему grain объявляют так рано — вторым шагом, до dimensions и facts? Потому что grain задаёт рамку для обоих следующих шагов. Объявленное зерно говорит, что вообще может быть dimension этой таблицы (только то, что имеет одно значение на одну поездку) и что может быть measure (только то, что измеримо на уровне одной поездки). Без объявленного grain шаги 3 и 4 повисают в воздухе — непонятно, под что выбирать.
Главное правило: все строки одной fact-таблицы обязаны быть одного grain. Нельзя в fct_trips держать вперемешку строки «одна поездка» и строки «итог за день». Смешанный grain ломает любую агрегацию: SUM начинает складывать несопоставимое. Один grain на таблицу — железно.
И второе правило, которое разовьёт следующий урок: предпочитайте самый низкий, атомарный grain — самое мелкое событие, какое есть. Одна поездка, а не «итог по станции за день». Атомарное зерно даёт максимум гибкости: из мелких строк можно собрать любой агрегат, а из заранее свёрнутого итога мелкие детали уже не достать.
Шаг 3: выбрать dimensions
Dimensions выбираются так: для объявленного grain ответьте на вопрос «как можно описать одно событие этого зерна?». Каждый осмысленный ответ — кандидат в dimension.
Для зерна «одна завершённая поездка» спросим: как описать одну поездку?
- Когда она произошла — dimension времени (
dim_date, возможноdim_time). - Какой велосипед использовали —
dim_bike(модель, тип, год выпуска). - Какой клиент ехал —
dim_rider(тип подписки, возрастная группа, город). - Откуда взяли велосипед —
dim_stationв роли станции старта. - Куда вернули —
dim_stationв роли станции финиша. - Какая погода была —
dim_weather(если данные есть; погода влияет на спрос).
Каждый из этих ответов имеет ровно одно значение на одну поездку — а значит, согласуется с grain. Это критерий проверки: dimension допустима, только если у неё одно значение на одну строку fact-таблицы. У одной поездки один велосипед, один клиент, одна станция старта — всё подходит. А вот «список всех станций, мимо которых проехал велосипед» — не dimension этой fact-таблицы: значений много на одну поездку, grain нарушен.
Каждая dimension даёт пользователю набор разрезов: dim_bike позволит резать поездки по модели и типу велосипеда, dim_rider — по типу подписки, dim_station — по району. Чем богаче dimensions описательными атрибутами, тем больше вопросов сможет задать аналитик.
Шаг 4: выбрать facts
Facts (measures) выбираются последними: для объявленного grain ответьте «что числового можно измерить у одного такого события?».
Для зерна «одна завершённая поездка»:
- trip_duration_minutes — длительность поездки в минутах.
- trip_distance_km — пройденное расстояние.
- fare_amount — сумма оплаты за поездку.
- calories_burned — оценка сожжённых калорий (если сервис её считает).
Главный критерий тот же — соответствие grain: measure должна быть измерима именно на уровне одной строки. У одной поездки есть своя длительность, своё расстояние, своя стоимость — всё подходит. А вот «средняя длительность поездки по станции» — не measure уровня поездки: это уже агрегат. Агрегаты не хранят в fact-таблице атомарного grain — их вычисляют запросом из атомарных строк. В fact кладут сырые, аддитивные числа одного события; усреднения и доли считают на лету.
Частая ошибка — класть в fact-таблицу уже посчитанные средние и проценты. Если в fct_trips положить столбец avg_duration, его нельзя корректно агрегировать: среднее средних не равно общему среднему. В fact кладите аддитивные слагаемые (длительность, расстояние, сумму), а отношения и средние пусть считает запрос: SUM(trip_duration) / COUNT(*). Подробнее про аддитивность measures — в следующем модуле.
Собираем результат
Четыре шага пройдены — складываем star schema проката велосипедов.
-- Шаг 1: процесс = завершение поездки
-- Шаг 2: grain = одна строка = одна завершённая поездка
CREATE TABLE fct_trips (
-- Шаг 3: dimensions (foreign keys)
date_key INT NOT NULL REFERENCES dim_date(date_key),
bike_key INT NOT NULL REFERENCES dim_bike(bike_key),
rider_key INT NOT NULL REFERENCES dim_rider(rider_key),
start_station_key INT NOT NULL REFERENCES dim_station(station_key),
end_station_key INT NOT NULL REFERENCES dim_station(station_key),
-- Шаг 4: facts (measures, измеримы на уровне одной поездки)
trip_duration_minutes INT NOT NULL,
trip_distance_km NUMERIC(8,2) NOT NULL,
fare_amount NUMERIC(8,2) NOT NULL
);
И запрос — «средняя длительность и выручка по типу велосипеда и месяцу»:
SELECT
b.bike_type,
d.month_name,
COUNT(*) AS trips,
AVG(f.trip_duration_minutes) AS avg_duration,
SUM(f.fare_amount) AS revenue
FROM fct_trips f
JOIN dim_bike b ON f.bike_key = b.bike_key
JOIN dim_date d ON f.date_key = d.date_key
GROUP BY b.bike_type, d.month_name
ORDER BY revenue DESC;
-- bike_type | month_name | trips | avg_duration | revenue
-- electric | July | 184320 | 23.7 | 920140.00
-- standard | July | 311208 | 18.2 | 610870.50
-- ...
Заметьте: среднее avg_duration считается запросом из атомарных строк, а не хранится. Каждая dimension даёт свой разрез. Запрос читается прозрачно — таков результат прохождения четырёх шагов по порядку.
| Шаг | Вопрос | Результат для примера |
|---|---|---|
| 1. Процесс | какое событие моделируем? | завершение поездки |
| 2. Grain | что означает одна строка? | одна завершённая поездка |
| 3. Dimensions | как описать одно событие? | дата, велосипед, клиент, станции |
| 4. Facts | что числового измерить? | длительность, расстояние, оплата |
Почему порядок нельзя менять
Стоит явно объяснить, почему именно такая последовательность и почему перестановка ломает дизайн.
Процесс перед grain. Пока не выбран процесс, нечему объявлять зерно — grain это всегда зерно конкретного процесса. «Одна строка чего?» — сначала ответьте «чего».
Grain перед dimensions и facts. Это ключевое. Grain определяет, что вообще допустимо в шагах 3 и 4. Если выбирать dimensions до объявления grain, легко взять разрез, который не имеет одного значения на строку, — и модель не сойдётся. Объявленный grain — это фильтр-критерий: dimension годится, если одно значение на строку; measure годится, если измерима на строке. Без grain нет критерия, выбор превращается в гадание.
Facts последними. Facts проверяются на соответствие grain и часто привязаны к набору dimensions. Их выбирают, когда рамка (grain) и контекст (dimensions) уже зафиксированы.
Этот порядок — не ритуал, а защита от самой частой ошибки размерного моделирования: смешанного или плохо определённого grain. Дисциплина «процесс -> grain -> dimensions -> facts» гарантирует, что зерно объявлено явно и рано, а всё остальное согласовано с ним.
Четырёхшаговый процесс на практике — Kimball-light в dbt-проекте Размерное моделирование с точки зрения DE-пайплайнаНа собеседовании по data modeling четырёхшаговый процесс — почти гарантированный вопрос. Ответ-формула: «Выбрать бизнес-процесс, объявить grain, выбрать dimensions, выбрать facts — именно в этом порядке, потому что grain задаёт рамку для dimensions и facts, и его нужно зафиксировать раньше них».
Попробуй сам
Возьмите бизнес-процесс «обращение клиента в службу поддержки» (один тикет от создания до закрытия) и пройдите все четыре шага письменно.
Шаг 1: убедитесь, что это процесс, а не отдел — сформулируйте его глаголом-событием. Шаг 2: объявите grain одним точным предложением и проверьте, что он атомарный. Шаг 3: выпишите 4-6 dimensions, и для каждой проверьте критерий «одно значение на один тикет». Шаг 4: выпишите 3-5 facts, и для каждой проверьте «измеримо на уровне одного тикета»; следите, чтобы не положить туда среднее или процент.
Затем соберите DDL для fct_support_tickets. И ответьте на главный вопрос: что бы пошло не так, если бы вы выбрали dimensions до того, как объявили grain? Приведите конкретный пример разреза, который кажется уместным, но нарушает зерно «один тикет».