Conformed dimensions и шина (bus matrix)
Представьте крупную компанию: продажи, доставка, поддержка, возвраты — у каждого процесса есть своя fact-таблица. Аналитик задаёт вопрос: «дайте выручку и количество обращений в поддержку по одним и тем же регионам за один и тот же месяц». Если у fct_sales своя таблица регионов, а у fct_support — своя, ответить честно нельзя: в одной таблице регион называется Сев-Запад, в другой Северо-Западный ФО, коды разные, границы регионов обновлялись в разное время. Числа из двух отчётов невозможно положить рядом.
Conformed dimension решает ровно эту проблему. Это dimension с идентичным смыслом и идентичным содержимым, переиспользуемая несколькими fact-таблицами. Одна dim_region, одна dim_date, одна dim_product — на весь warehouse. Тогда «регион» в отчёте по продажам и «регион» в отчёте по поддержке — это буквально одни и те же строки с одними и теми же ключами. Числа становятся сравнимыми.
Что значит «conformed» технически
Dimension считается conformed между двумя fact-таблицами, если выполняется одно из двух условий. Первое: dimension физически одна и та же таблица, и обе fact-таблицы ссылаются на неё своими foreign keys. Второе, более слабое: dimensions разные таблицы, но одна является строгим подмножеством другой по строкам и атрибутам, и общие атрибуты имеют одинаковые имена и значения. Второй случай называют conformed rollup — например, dim_month есть агрегированная версия dim_date, и месяц в обеих таблицах означает одно и то же.
Главное требование — согласованность ключей и значений. Если в dim_product товар с product_key = 4071 это «Кофе молотый 250г», то этот же ключ обязан означать тот же товар во всех fact-таблицах. Тогда JOIN по product_key корректен независимо от того, к какой fact-таблице мы присоединяем dimension.
Здесь важно понять, почему conformed dimension должна быть именно одной физической таблицей или строгим подмножеством, а не «двумя похожими справочниками». Похожесть — это не согласованность. Два справочника товаров, заведённых разными командами, почти наверняка разойдутся: один обновит категорию товара в марте, другой — в мае; один напишет «Кофе», другой «Кофе молотый»; один присвоит товару ключ 4071, другой 9203. Каждое такое расхождение делает JOIN по этим dimensions либо неполным, либо ошибочным. Единственный способ гарантировать, что «товар» в одной fact-таблице и «товар» в другой — это буквально одно и то же, — иметь под ними одну общую таблицу с одним набором ключей. Conformed dimension — это не «договорённость о схожем содержимом», а физическое единство справочника.
Из этого следует и практическое правило обновления. Раз conformed dimension одна, у неё должен быть один владелец — одна команда или один ETL-процесс, отвечающий за её наполнение и изменения. Если каждая команда, использующая dimension, начнёт вносить в неё свои правки независимо, согласованность снова разрушится. Conformed dimension живёт по принципу «один источник изменений — много потребителей».
Эффект: можно посчитать «чистые продажи = продажи минус возвраты по каждой категории товаров» и быть уверенным, что категория товара в обоих слагаемых определена одинаково.
Drill-across: зачем это всё нужно
Операция, ради которой существуют conformed dimensions, называется drill-across — анализ, объединяющий measures из нескольких fact-таблиц по общим dimensions. Drill-across нельзя делать одним JOIN-ом fact-таблиц напрямую: у них разный grain, и соединение по dimension-ключам размножит строки (декартово раздувание по пересекающимся ключам).
Правильный приём — двухфазный. Сначала каждую fact-таблицу агрегируют отдельно до общего набора dimensions. Потом два уже агрегированных результата соединяют по этим dimensions.
-- Drill-across: выручка и сумма возвратов по категории и месяцу
WITH sales AS (
SELECT p.category, d.year_month,
SUM(s.sale_amount) AS revenue
FROM fct_sales s
JOIN dim_product p ON p.product_key = s.product_key
JOIN dim_date d ON d.date_key = s.date_key
GROUP BY p.category, d.year_month
),
returns AS (
SELECT p.category, d.year_month,
SUM(r.return_amount) AS returned
FROM fct_returns r
JOIN dim_product p ON p.product_key = r.product_key
JOIN dim_date d ON d.date_key = r.date_key
GROUP BY p.category, d.year_month
)
SELECT s.category, s.year_month,
s.revenue,
COALESCE(r.returned, 0) AS returned,
s.revenue - COALESCE(r.returned, 0) AS net_revenue
FROM sales s
LEFT JOIN returns r
ON r.category = s.category
AND r.year_month = s.year_month
ORDER BY s.category, s.year_month;
category | year_month | revenue | returned | net_revenue
------------+------------+----------+----------+------------
Кофе | 2026-01 | 412300.0 | 18400.0 | 393900.0
Кофе | 2026-02 | 388100.0 | 12050.0 | 376050.0
Чай | 2026-01 | 207600.0 | 4300.0 | 203300.0
Чай | 2026-02 | 221900.0 | 6800.0 | 215100.0
Этот запрос корректен только потому, что dim_product и dim_date — conformed. category и year_month берутся из одних и тех же dimension-таблиц, поэтому строки sales и returns соединяются точно. Если бы у возвратов была отдельная таблица товаров со своими категориями, JOIN по category либо потерял бы строки (несовпадение названий), либо склеил неправильные.
Разберём, почему именно две фазы. В первой фазе каждая fact-таблица сворачивается до общего «зерна анализа» — здесь это пара (категория, месяц). После свёртки sales содержит ровно одну строку на каждую пару, и returns — тоже одну. Во второй фазе мы соединяем два набора, в каждом из которых ключ соединения уникален. Соединение «один к одному» по (категория, месяц) не может ничего размножить — это ключевое свойство, дающее корректность. Если же соединять fact-таблицы напрямую, до свёртки, то одной строке продаж соответствовало бы много строк возвратов с тем же товаром и датой (и наоборот), и SUM посчитал бы каждую сумму многократно. Именно поэтому drill-across — это всегда «сначала агрегировать каждую таблицу по отдельности, потом соединить агрегаты».
LEFT JOIN во второй фазе выбран намеренно: товар мог продаваться, но ни разу не возвращаться. При INNER JOIN такие категории выпали бы из отчёта целиком — мы потеряли бы продажи товаров с нулевыми возвратами. LEFT JOIN сохраняет все строки продаж, а COALESCE(r.returned, 0) подставляет ноль там, где возвратов не было. Выбор типа соединения в drill-across — это содержательное решение, а не формальность: оно определяет, какие строки попадут в итог.
Запомните правило: fact-таблицы между собой напрямую НЕ соединяют. Объединение разных бизнес-процессов всегда идёт через conformed dimensions и двухфазную агрегацию. Это и есть drill-across.
Bus matrix: карта enterprise DWH
Когда conformed dimensions нужно спланировать на весь warehouse заранее, Kimball предлагает инструмент — bus matrix (матрица шины). Это таблица: строки — бизнес-процессы (будущие fact-таблицы), столбцы — conformed dimensions. На пересечении стоит отметка, если данная dimension применима к данному процессу.
Название «шина» (bus) — из электроники: общая шина, к которой подключаются разные устройства. Здесь общая шина — это набор conformed dimensions, а «устройства» — fact-таблицы, которые к ним подключаются.
| Бизнес-процесс | dim_date | dim_product | dim_customer | dim_store | dim_employee |
|---|---|---|---|---|---|
| Продажи | [x] | [x] | [x] | [x] | [x] |
| Возвраты | [x] | [x] | [x] | [x] | [x] |
| Поставки | [x] | [x] | [x] | ||
| Обращения в поддержку | [x] | [x] | [x] | [x] | |
| Маркетинг-кампании | [x] | [x] |
Читать матрицу можно двумя способами. По строке — какие dimensions нужны конкретной fact-таблице (продажам нужны все пять). По столбцу — сколько процессов разделяют одну dimension. dim_date стоит в каждой строке: это самая «conformed» dimension в любом warehouse, её строит и переиспользует весь проект. dim_product встречается почти везде — значит, спроектировать её надо особенно тщательно, цена ошибки умножается на число процессов.
Почему bus matrix экономит проект
Bus matrix — это не документация ради документации, а способ избежать самой дорогой ошибки в warehouse: несогласованных dimensions. Без него каждая команда строит свою fact-таблицу со своими справочниками, и через год выясняется, что десять разных таблиц «клиентов» нельзя согласовать.
Bus matrix даёт три практических выигрыша. Первый — порядок работ: dimensions с большим числом отметок в столбце строят первыми, потому что от них зависит больше всего. Второй — параллельность: разные команды могут одновременно делать fct_sales и fct_support, договорившись о наборе общих dimensions заранее; интеграция произойдёт автоматически, потому что ключи совпадут. Третий — incremental-доставка: warehouse не нужно строить целиком, можно выпустить одну fact-таблицу, потом вторую — и они всё равно состыкуются через шину.
Стоит подчеркнуть, как bus matrix меняет сам характер работы над warehouse. Без неё построение хранилища — это монолитный проект: пока не готово всё, не работает ничего, а интеграцию приходится «доделывать» в конце, когда выясняется, что справочники не сходятся. С bus matrix построение становится потоком независимых задач. Каждая fact-таблица — отдельная поставка, и поскольку набор conformed dimensions зафиксирован заранее как контракт, новая fact-таблица гарантированно встраивается в уже существующее хранилище без переделок. Это превращает рискованный большой проект в управляемую серию мелких.
Полезно также понимать, что bus matrix — живой документ, а не разовый артефакт. По мере роста компании появляются новые бизнес-процессы (новые строки) и иногда новые conformed dimensions (новые столбцы). Матрицу дополняют, но уже введённые conformed dimensions стараются не менять — на них завязаны все существующие fact-таблицы. Поэтому первые conformed dimensions проектируют с запасом: лучше сразу заложить в dim_customer атрибуты, которые понадобятся будущим процессам, чем потом перестраивать dimension, от которой зависит десяток fact-таблиц.
Самая частая ошибка джуниора — начать строить fact-таблицы, не нарисовав bus matrix. Через несколько месяцев в warehouse оказывается три dim_customer с разными ключами, и drill-across между процессами становится невозможен без болезненной миграции. Матрица рисуется до первой fact-таблицы.
Conformed dimension — это контракт. Подписав его (внеся dimension в шину), команда обязуется: ключи стабильны, значения согласованы, изменения координируются. Цена — дисциплина. Выигрыш — warehouse, где любые два процесса можно честно сравнить.
Попробуй сам
Возьмите три бизнес-процесса знакомой вам области — например, для интернет-магазина это «оформление заказа», «доставка», «отзыв на товар». Для каждого:
- Выпишите, какая одна строка fact-таблицы (grain) представляет этот процесс.
- Перечислите dimensions, которые описывают эту строку (по кому, по чему, когда, где).
- Сведите всё в bus matrix: процессы по строкам, dimensions по столбцам, отметки на пересечениях.
- Найдите dimension с наибольшим числом отметок в столбце — это кандидат строить первым. Найдите dimension, которая встречается лишь раз — подумайте, не станет ли она conformed позже, когда добавится четвёртый процесс.
Проверьте себя: если две fact-таблицы делят dimension, смогли бы вы написать drill-across запрос по ней? Если нет — dimension ещё не conformed.