Learning Platform
Глоссарий Troubleshooting
Урок 14.03 · 17 мин
Начальный
junk-dimensioncardinalityfact-table-designkimball

Junk dimensions: схлопывание флагов низкой кардинальности

При проектировании fact-таблицы заказов всегда всплывает горстка мелких признаков: оплачен заказ или нет, был ли применён промокод, канал заказа (сайт/приложение/телефон), тип доставки (курьер/самовывоз/почта). Каждый признак — это два-три-четыре возможных значения. Куда их девать?

Соблазн первый: положить каждый признак прямо в fact-таблицу отдельным столбцом. Соблазн второй: завести под каждый отдельную dimension — dim_payment_status, dim_order_channel и так далее. Оба решения плохи. Junk dimension — третий путь: собрать все эти мелкие признаки в одну компактную dimension.


Кардинальность: ключевое понятие

Кардинальность атрибута — это число различных значений, которые он принимает. У атрибута payment_status кардинальность 2 (оплачен/не оплачен). У order_channel — 3. У customer_name кардинальность огромна — миллионы. Именно низкая кардинальность делает атрибут кандидатом в junk dimension.

Почему отдельная dimension под каждый низкокардинальный флаг — плохо? Dimension с двумя строками — это вырожденная таблица: накладные расходы на саму таблицу, на её ключ, на JOIN несоразмерны двум строкам полезных данных. Десяток таких флагов даёт десяток крошечных dimensions и десяток foreign keys в fact-таблице. Fact-таблица распухает по ширине, число JOIN в каждом аналитическом запросе растёт.

Почему держать флаги прямо в fact-таблице — тоже плохо? Fact-таблица должна быть узкой: foreign keys плюс measures, и ничего лишнего. Текстовые описательные столбцы ('Курьер', 'Самовывоз') раздувают каждую строку fact-таблицы, которых миллиарды. Описательные атрибуты по природе принадлежат dimensions, а не fact-таблице.

Разберём цену хранения флагов в fact-таблице количественно. Пусть в fct_orders миллиард строк, и мы добавили четыре текстовых флага — в среднем по 10 байт каждый. Это 40 байт на строку, умноженные на миллиард, — около 40 ГБ дополнительного объёма, который читается при каждом полном скане fact-таблицы. Если же эти флаги вынесены в junk dimension, в fact-таблице остаётся один order_flags_key — целое число шириной в пару байт. Разница в объёме fact-таблицы — десятки гигабайт, а fact-таблицу сканируют постоянно. Узость fact-таблицы — это не эстетика, а прямая экономия I/O на самой большой таблице warehouse.

Есть и второй аргумент, помимо объёма. Текстовые флаги в fact-таблице повторяются: значение 'Курьер' физически хранится в каждой из сотен миллионов строк с курьерской доставкой. В junk dimension оно хранится ровно один раз — в той строке, что описывает соответствующую комбинацию. Junk dimension убирает повтор описательных значений; fact-таблица ссылается на описание, а не носит его копию в каждой строке.

Три подхода к мелким флагам
Флаги в factКаждый флаг — отдельный столбец прямо в fact-таблице. Fact распухает текстовыми описаниями, нарушается принцип узкой fact-таблицы.
Dimension на флагПод каждый флаг своя dimension с 2-3 строками. Десяток вырожденных таблиц, десяток лишних JOIN.
Junk dimensionВсе флаги в одной dimension. Один FK в fact-таблице, один JOIN, компактно.

Как устроена junk dimension

Junk dimension — это одна таблица, в которой каждый столбец соответствует одному флагу, а каждая строка — одной встречающейся комбинации значений всех флагов. У неё свой surrogate key, на который fact-таблица ссылается одним-единственным foreign key.

Сначала DDL:

CREATE TABLE dim_order_flags (
    order_flags_key  SMALLINT PRIMARY KEY,  -- surrogate key
    payment_status   VARCHAR(12),   -- 'Оплачен' | 'Не оплачен'
    order_channel    VARCHAR(12),   -- 'Сайт' | 'Приложение' | 'Телефон'
    delivery_type    VARCHAR(12),   -- 'Курьер' | 'Самовывоз' | 'Почта'
    promo_applied    VARCHAR(3)     -- 'Да' | 'Нет'
);

Теперь содержимое. Таблица хранит наблюдаемые комбинации — декартово произведение всех значений (или его часть, если какие-то сочетания невозможны):

 order_flags_key | payment_status | order_channel | delivery_type | promo_applied
-----------------+----------------+---------------+---------------+--------------
       1         | Оплачен        | Сайт          | Курьер        | Да
       2         | Оплачен        | Сайт          | Курьер        | Нет
       3         | Оплачен        | Сайт          | Самовывоз     | Нет
       4         | Не оплачен     | Телефон       | Почта         | Нет
       5         | Оплачен        | Приложение    | Курьер        | Да
      ...        | ...            | ...           | ...           | ...

Полное декартово произведение здесь: 2 * 3 * 3 * 2 = 36 комбинаций. Тридцать шесть строк — это весь объём таблицы. Fact-таблица на миллиард заказов ссылается на эти 36 строк одним столбцом order_flags_key.

Junk dimension: один FK вместо четырёх
fct_ordersFact-таблица заказов. Вместо четырёх столбцов-флагов — один foreign key order_flags_key.
один JOIN
dim_order_flags36 строк — все комбинации payment_status, order_channel, delivery_type, promo_applied. Один surrogate key на комбинацию.

Загрузка fact-таблицы с junk dimension

При загрузке заказа надо найти строку junk dimension, соответствующую его комбинации флагов, и взять её ключ. Если такой комбинации ещё нет — добавить.

-- Шаг 1: добавить недостающие комбинации (если появилась новая)
INSERT INTO dim_order_flags
    (order_flags_key, payment_status, order_channel, delivery_type, promo_applied)
SELECT
    (SELECT COALESCE(MAX(order_flags_key), 0) FROM dim_order_flags)
        + ROW_NUMBER() OVER (ORDER BY s.payment_status),
    s.payment_status, s.order_channel, s.delivery_type, s.promo_applied
FROM (
    SELECT DISTINCT payment_status, order_channel, delivery_type, promo_applied
    FROM staging_orders
) s
LEFT JOIN dim_order_flags d
       ON d.payment_status = s.payment_status
      AND d.order_channel  = s.order_channel
      AND d.delivery_type  = s.delivery_type
      AND d.promo_applied  = s.promo_applied
WHERE d.order_flags_key IS NULL;

-- Шаг 2: загрузить fct_orders, подставив order_flags_key через JOIN
INSERT INTO fct_orders (order_key, customer_key, date_key, order_flags_key, order_amount)
SELECT s.order_key, s.customer_key, s.date_key, d.order_flags_key, s.order_amount
FROM staging_orders s
JOIN dim_order_flags d
       ON d.payment_status = s.payment_status
      AND d.order_channel  = s.order_channel
      AND d.delivery_type  = s.delivery_type
      AND d.promo_applied  = s.promo_applied;

Шаг 1 находит комбинации флагов, которых ещё нет в dimension (LEFT JOIN ... WHERE ключ IS NULL), и добавляет их. Шаг 2 присоединяет junk dimension по всем четырём флагам и кладёт в fact-таблицу только её ключ. После загрузки fact-таблица не содержит ни одного текстового флага — только узкий order_flags_key.

Ключевой приём здесь — LEFT JOIN ... WHERE d.order_flags_key IS NULL. Это стандартный способ найти «то, чего ещё нет». LEFT JOIN сохраняет все строки из левого набора (различные комбинации флагов в staging) и подставляет NULL в столбцы правой таблицы там, где совпадения не нашлось. Условие IS NULL оставляет ровно те комбинации, для которых строки в dim_order_flags пока не существует. Их и нужно вставить. Этот паттерн — «найти отсутствующие через LEFT JOIN и фильтр IS NULL» — пригодится вам при загрузке почти любой dimension, не только junk.

Шаг 2 заслуживает отдельного внимания. JOIN идёт по всем четырём флагам сразу — это обязательно. Junk dimension уникальна по комбинации, и чтобы найти ровно одну нужную строку, надо сопоставить все составляющие комбинации. Если присоединять по части флагов, JOIN вернёт несколько строк junk dimension, и fact-таблица получит неоднозначный ключ — а то и размножится. Полное совпадение по всем флагам гарантирует, что каждой строке staging соответствует ровно одна строка junk dimension.

TIP

Junk dimension можно заполнить заранее полным декартовым произведением всех значений всех флагов. Тогда шаг 1 не нужен совсем — все комбинации уже есть. Это работает, когда число комбинаций невелико (десятки-сотни) и набор значений каждого флага известен. Если флагов много и часть комбинаций нереальна — заполняйте по факту, как в примере выше.

Загрузка junk dimension через dbt — паттерн DISTINCT + LEFT JOIN

Когда junk dimension оправдана, а когда нет

Junk dimension — инструмент для атрибутов, у которых одновременно: низкая кардинальность (единицы-десятки значений), описательный характер (флаги, индикаторы, мелкие категории), отсутствие собственных атрибутов (у payment_status нет «своих» свойств, которые надо было бы хранить).

Где junk dimension НЕ подходит:

  • Высокая кардинальность. Если объединить четыре флага по 2-3 значения с атрибутом на 1000 значений, декартово произведение взорвётся до десятков тысяч строк — это уже не junk dimension. Высококардинальный атрибут заслуживает своей dimension.
  • У атрибута есть собственные свойства. Если у «канала заказа» появляются свои атрибуты (стоимость канала, ответственная команда), это полноценная dim_channel, а не флаг в мусорной dimension.
  • Взрыв комбинаций. Десять флагов по три значения дают теоретически 3^10 ≈ 59 000 комбинаций. Если реально встречается лишь малая часть — junk dimension всё ещё годится (храним только встречающиеся строки). Если встречается большинство — пересмотрите дизайн.

Различие между «теоретическим» и «реальным» числом комбинаций здесь принципиально. Декартово произведение даёт верхнюю границу — все мыслимые сочетания флагов. Но в реальных данных многие сочетания не встречаются никогда: например, комбинация «не оплачен + самовывоз + промокод применён» может быть бизнес-невозможной. Поэтому на практике junk dimension часто заполняют не полным произведением, а по факту — добавляя комбинацию лишь тогда, когда она впервые встретилась в данных (как в шаге 1 загрузки выше). Тогда таблица содержит ровно столько строк, сколько реальных сочетаний есть, и остаётся компактной даже при формально большом декартовом произведении.

Признак атрибутаJunk dimensionСобственная dimension
Кардинальность 2-10[x]
Кардинальность сотни-тысячи[x]
Нет собственных атрибутов[x]
Есть собственные атрибуты[x]
Описательный флаг/индикатор[x]

Полезно понимать и обратную сторону решения. Junk dimension не обязана быть одна на fact-таблицу. Если флагов много и они логически распадаются на несвязанные группы — скажем, отдельно «обстоятельства оплаты» и отдельно «обстоятельства доставки», — иногда заводят две junk dimensions вместо одной огромной. Это компромисс: две junk dimensions дают два FK вместо одного, зато каждая остаётся компактной и осмысленной, а декартово произведение не перемножает несвязанные между собой группы флагов. Решение «одна большая или несколько junk dimensions» принимают по числу комбинаций и по тому, насколько группы флагов независимы друг от друга.

WARNING

Слово “junk” (мусор) в названии вводит в заблуждение. Это не «свалка для плохих данных» — это аккуратный инженерный приём. Сами флаги полезны для анализа: по junk dimension отлично делается срез «выручка по каналам заказа и типам доставки». «Мусорным» называют не данные, а тот факт, что эти атрибуты не тянули на отдельные dimensions и их собрали вместе.

Стоит увидеть, как junk dimension связана с другими приёмами этого модуля. Junk dimension — это, по сути, та же идея, что и conformed dimension и role-playing dimension: бороться за то, чтобы fact-таблица оставалась узкой и опрятной. Conformed dimension убирает дублирование справочников между fact-таблицами; role-playing убирает дублирование dimension при нескольких ролях; junk dimension убирает россыпь мелких FK и текстовых столбцов внутри fact-таблицы. Все три служат одной цели — fact-таблица должна нести foreign keys и measures, и ничего лишнего, а описательная нагрузка должна жить в dimensions.

И ещё одно практическое наблюдение. Junk dimension особенно уместна в transaction fact-таблицах — тех, где строка соответствует одному событию (заказ, продажа, обращение). У событий обычно есть набор «обстоятельств» — флагов, описывающих, как именно произошло событие. Эти обстоятельства редко имеют собственную структуру и редко высококардинальны — идеальные кандидаты в junk dimension. Когда вы проектируете transaction fact-таблицу и видите, что к ней «прилипает» горстка флагов, первая мысль должна быть: собрать их в junk dimension, а не размазывать по fact-таблице.


Попробуй сам

Возьмите fact-таблицу обращений в поддержку fct_support_tickets. У обращения есть мелкие признаки: приоритет (низкий/средний/высокий), решено ли с первого контакта (да/нет), канал обращения (чат/почта/телефон), был ли эскалирован (да/нет).

  1. Посчитайте кардинальность каждого признака и общее число комбинаций (декартово произведение).
  2. Напишите CREATE TABLE dim_ticket_flags с surrogate key и четырьмя столбцами.
  3. Прикиньте: сколько строк будет в junk dimension и сколько foreign keys эта dimension убирает из fct_support_tickets.
  4. Добавьте к набору признак «оператор» (50 разных операторов). Должен ли он войти в junk dimension? Обоснуйте ответ через кардинальность.

Проверьте себя: после введения junk dimension сколько описательных текстовых столбцов осталось в самой fact-таблице? Должно быть ноль.


Проверка знанийKnowledge check
Что такое junk dimension, какую проблему она решает и по какому признаку атрибута решают, включать его в junk dimension или выделять в собственную dimension?
ОтветAnswer
Junk dimension — это одна dimension-таблица, которая объединяет несколько разрозненных атрибутов низкой кардинальности (флаги, индикаторы, мелкие категории) — например, payment_status, order_channel, delivery_type, promo_applied. Каждый столбец таблицы соответствует одному флагу, каждая строка — одной встречающейся комбинации значений всех флагов; у таблицы свой surrogate key, на который fact-таблица ссылается единственным foreign key. Она решает проблему двух плохих альтернатив: держать флаги прямо в fact-таблице (раздувает узкую по замыслу fact-таблицу текстовыми описаниями) или заводить отдельную dimension под каждый флаг (россыпь вырожденных таблиц с 2-3 строками и десяток лишних JOIN). Junk dimension даёт один FK и один JOIN. Главный критерий включения атрибута — кардинальность: junk dimension подходит для атрибутов с единицами-десятками различных значений и без собственных атрибутов. Атрибут с высокой кардинальностью (сотни-тысячи значений) включать нельзя — декартово произведение взорвётся; ему нужна собственная dimension. Также собственная dimension нужна, если у атрибута есть свои свойства, которые надо хранить. То есть junk dimension — для описательных низкокардинальных флагов без собственной структуры.

Проверьте понимание

Результат: 0 из 0
Концептуальный
Вопрос 1 из 4. Что представляет собой одна строка junk dimension?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 5