Моделирование измерений организует данные вокруг процесса, который считают, и описаний, по которым его группируют. На трёх позициях чека сравним звезду, снежинку и созвездие.
Что хранит факт
Выберем зерно: одна строка — одна позиция в чеке. Тогда сумма строки зависит от количества и цены именно этой позиции. Номер чека повторится у двух позиций чека 701 — это нормально; ключом факта служит line_id или пара (receipt_id, line_no).
| line_id | receipt_id | date_key | product_key | store_key | qty | amount_rub |
|---|---|---|---|---|---|---|
| 1 | 701 | 20260901 | 11 | 5 | 2 | 300 |
| 2 | 701 | 20260901 | 12 | 5 | 1 | 250 |
| 3 | 702 | 20260902 | 11 | 5 | 1 | 150 |
Это таблица фактов fact_sale_line. Ключи задают контекст, меры — количество и сумму. product_key служит ссылкой на описание товара, qty и amount_rub участвуют в расчётах.
Измерения расшифровывают ключи: какой товар купили, в каком магазине, в какой день. Дата продажи и дата возврата могут ссылаться на один календарь, но играют разные роли. Описания нужны для фильтрации и группировки, а не для арифметики по их кодам.
Звезда: короткий путь к описанию
В звезде факт находится в центре, а каждое измерение присоединяется к нему напрямую. Например, dim_product сразу содержит название товара, категорию и отдел:
| product_key | sku | товар | категория | отдел |
|---|---|---|---|---|
| 11 | TEA-01 | Чай | Напитки | Продукты |
| 12 | COF-01 | Кофе | Напитки | Продукты |
dim_store с ключом 5 содержит магазин «Центральный», город Казань. dim_date расшифровывает date_key в календарную дату, неделю и месяц. Теперь запрос «сумма по категориям» соединяет факт с dim_product. Для наших строк категория «Напитки» даст 300 + 250 + 150 = 700 ₽.
WITH fact_sale_line(product_key, amount_rub) AS (
VALUES (11, 300), (12, 250), (11, 150)
), dim_product(product_key, category_name) AS (
VALUES (11, 'Напитки'), (12, 'Напитки')
)
SELECT p.category_name, SUM(f.amount_rub) AS sales_rub
FROM fact_sale_line AS f
JOIN dim_product AS p ON p.product_key = f.product_key
GROUP BY p.category_name;Для каждого product_key должна находиться ровно одна подходящая строка измерения. Если у чая две исторические версии описания и факт соединяют только по sku, строки продажи размножатся и сумма будет неверной. Историю измерений разберём в SCD; пока достаточно понимать, зачем факт ссылается на конкретный ключ версии.
Плюс звезды — ясный интерфейс для аналитика: атрибуты товара собраны в одном месте, путь запроса короткий. Цена — название категории «Напитки» повторяется у многих товаров. Это сознательная денормализация для чтения. Она требует управляемого обновления описаний, зато пользователю не нужно всякий раз восстанавливать цепочку товар → категория → отдел.
Снежинка: иерархия вынесена отдельно
В снежинке часть измерения нормализована. dim_product хранит товар и category_key, dim_category — название категории и department_key, dim_department — отдел. Факт продажи остаётся тем же: одна позиция чека и те же 700 ₽. Меняется только путь к описанию.
| Таблица | Пример строки |
|---|---|
| dim_product | 11, TEA-01, «Чай», category_key=3 |
| dim_category | 3, «Напитки», department_key=2 |
| dim_department | 2, «Продукты» |
Снежинка добавляет JOIN с категорией. Отдельная таблица полезна, когда категория имеет собственные атрибуты, права или используется несколькими измерениями. В BI такой путь сложнее; стоимость чтения и сопровождения сравнивают для конкретных запросов.
Запрос по тем же строкам добавляет одно соединение:
WITH fact_sale_line(product_key, amount_rub) AS (
VALUES (11, 300), (12, 250), (11, 150)
), dim_product(product_key, category_key) AS (
VALUES (11, 3), (12, 3)
), dim_category(category_key, category_name) AS (
VALUES (3, 'Напитки')
)
SELECT c.category_name, SUM(f.amount_rub) AS sales_rub
FROM fact_sale_line AS f
JOIN dim_product AS p ON p.product_key = f.product_key
JOIN dim_category AS c ON c.category_key = p.category_key
GROUP BY c.category_name;Оба запроса возвращают «Напитки — 700 ₽». Каждая строка факта должна находить одно описание товара и одну категорию.
Для исторического отчёта версии описаний нужны в обеих структурах. В снежинке согласуют версии всей цепочки товар → категория → отдел.
Созвездие: несколько процессов на общих измерениях
Продажа и возврат — разные процессы. В fact_sale_line одна строка означает позицию чека продажи; в fact_return_line — позицию возврата с датой возврата и суммой. Обе таблицы могут использовать согласованное измерение товара и магазина. Несколько фактов вокруг общих измерений образуют созвездие.
Допустим, по чаю были две строки продаж: 300 ₽ и 150 ₽, а затем один возврат на 150 ₽. Если соединить каждый факт продажи с возвратом только по product_key, строка возврата повторится дважды и получится возврат 300 ₽. Правильнее сначала агрегировать продажи и возвраты каждый на своём зерне до сравнимого разреза — например, товар и месяц, — а потом сопоставить полученные итоги. Сумма продаж чая 450 ₽, сумма возврата 150 ₽. Если отчёт называется «чистые продажи», нужно отдельно решить, относить ли возврат к месяцу продажи или к месяцу фактического возврата.
Общее измерение не означает одинаковую роль даты или одинаковую частоту строк. Согласованный справочник товара позволяет сравнить процессы; он не разрешает прямой JOIN двух больших фактов без определения зерна результата.
Когда какую форму выбрать
| Вопрос | Звезда | Снежинка |
|---|---|---|
| Где категория товара? | В строке dim_product | В отдельной dim_category |
| Сколько переходов до неё? | Факт → товар | Факт → товар → категория |
| Где поддерживать общее описание? | В затронутых строках измерения | В отдельной таблице категории |
| Что с историей? | Нужна версия измерения или другое явное правило | Нужно согласовать версии всей цепочки |
| Что обычно проще пользователю? | Искать разрезы в одном измерении | Разбираться в нескольких связанных таблицах |
Звезда обычно даёт короткий путь к атрибутам для регулярной витрины. Снежинку выбирают для управляемой общей иерархии. Созвездие объединяет несколько процессов через согласованные измерения и может содержать обе формы.