Learning Platform
Глоссарий Troubleshooting
Урок 13.02 · 17 мин
Начальный
periodic-snapshotfact-tablekimballsemi-additive

Periodic snapshot: одна строка на сущность за период

Прошлый урок разобрал transaction fact table — строку на каждое событие. Этот тип отлично отвечает на вопрос «какие события происходили». Но есть класс вопросов, на который он отвечает плохо: вопросы про состояние. «Каков остаток на складе на конец каждого дня?» «Какой баланс счёта на конец месяца?» «Сколько активных подписок было на конец недели?»

Для таких вопросов Кимбалл вводит второй тип — periodic snapshot fact table (таблица фактов периодических снимков). Этот урок объясняет, что это за тип, в чём его зерно, чем он отличается от транзакционного и почему его measures ведут себя особым образом при суммировании.

Зерно: сущность за регулярный период

Зерно periodic snapshot формулируется так: одна строка — это состояние одной сущности за один регулярный интервал времени.

Ключевые слова — «состояние», «регулярный интервал». Транзакционная таблица записывала событие; периодический снимок записывает не событие, а снимок состояния — каким было положение дел на конец периода. И делает это регулярно: каждый день, каждую неделю, каждый месяц — по расписанию, независимо от того, происходили события или нет.

Классический пример — ежедневный снимок остатков на складе:

-- Periodic snapshot: grain = остаток одного товара на одном складе на конец одного дня
CREATE TABLE fct_inventory_daily (
    -- foreign keys: какой день, какой товар, какой склад
    date_key       INT NOT NULL REFERENCES dim_date(date_key),
    product_key    INT NOT NULL REFERENCES dim_product(product_key),
    warehouse_key  INT NOT NULL REFERENCES dim_warehouse(warehouse_key),
    -- measures: состояние на конец дня
    quantity_on_hand   INT           NOT NULL,   -- остаток штук
    inventory_value    NUMERIC(14,2) NOT NULL,   -- стоимость остатка
    units_received     INT           NOT NULL,   -- поступило за день
    units_shipped      INT           NOT NULL    -- отгружено за день
);

Каждая строка fct_inventory_daily говорит: «на конец такого-то дня на таком-то складе было столько-то штук такого-то товара на такую-то сумму». Снимок снимается каждый день — для каждой пары «товар-склад» появляется по одной строке за каждый день, всегда.

Periodic snapshot: регулярный снимок состояния
Конец периодаКаждый день в полночь (или конец недели, месяца) — момент снятия снимка
снимаем состояние
Снимок состоянияКаким было положение дел: остаток, баланс, число активных — на этот момент
одна строка на сущность
Строка за периодОдна строка на каждую сущность за каждый интервал — всегда, регулярно

Плотная, а не разреженная

Главное отличие periodic snapshot от transaction fact table — плотность. Транзакционная таблица разрежена: строка только там, где было событие. Периодический снимок плотный (dense): строка есть для каждой сущности за каждый период, всегда — независимо от того, происходило ли что-нибудь.

Разберём на примере. Товар лежит на складе неделю, и за эту неделю с ним ничего не происходило — ни поступлений, ни отгрузок. В транзакционной таблице движений за эту неделю не было бы ни одной строки. В периодическом снимке за эту неделю будет семь строк — по одной на каждый день, и в каждой quantity_on_hand показывает остаток, units_received = 0, units_shipped = 0. Состояние «лежит и ничего не происходит» — это тоже состояние, и снимок его фиксирует.

Отсюда и название «плотная»: таблица заполнена сплошь, для каждого сочетания «сущность-период» строка есть гарантированно. Это даёт предсказуемость размера: число строк periodic snapshot равно числу сущностей умножить на число периодов. 50 000 товаров на 10 складах за 365 дней — это 50000 x 10 x 365 примерно 182 млн строк в год, и это число известно заранее, оно не зависит от активности.

Плотность — это и сила, и цена. Сила: на вопрос про состояние снимок отвечает мгновенно и единообразно. «Остаток товара X на складе Y на дату Z» — это одна строка по трём ключам, прямой доступ. «Лежит и не двигается» в снимке видно так же явно, как и активное движение. Цена: снимок хранит строки даже там, где ничего не менялось, — место расходуется на «состояние покоя».

Разреженная transaction против плотной snapshot
Transaction factСтрока только когда произошло событие; ничего не произошло — строки нет
неделя без движений
Ноль строкЗа неделю без поступлений и отгрузок в транзакционной таблице нет ни одной строки
Periodic snapshotСтрока на каждую сущность за каждый период, всегда, независимо от активности
неделя без движений
7 строкЗа ту же неделю — 7 строк, по одной на день, состояние покоя тоже зафиксировано

Semi-additive measures: остаток нельзя складывать по времени

У periodic snapshot есть особенность, которую обязательно понимать: его measures состояния — semi-additive (полуаддитивные). Это значит, что их можно суммировать по одним разрезам, но нельзя суммировать по времени.

Разберём на остатке. quantity_on_hand — остаток на конец дня. Можно ли его суммировать?

По разрезу «склад» — можно. «Сколько всего штук товара X на конец 4 июля по всем складам» — это SUM(quantity_on_hand) по складам за один день. Корректно: складываем остатки разных мест на один момент.

По разрезу «время» — нельзя. «Сколько штук товара X было за неделю» через SUM(quantity_on_hand) по семи дням — бессмысленно. Если на складе всю неделю лежало 100 штук, такая сумма даст 700 — но 700 штук не существует, это одни и те же 100 штук, посчитанные семь раз. Остаток — это не поток, который накапливается, а уровень, который держится. Складывать уровни за разные моменты времени нельзя.

Что с остатком по времени делать корректно: брать значение на конкретный момент (остаток на конец недели — это строка последнего дня) или усреднять (AVG(quantity_on_hand) — средний остаток за неделю). Сумма по времени — запрещена; последнее значение или среднее — допустимы.

-- ПРАВИЛЬНО: суммируем остаток по складам на ОДИН день
SELECT SUM(quantity_on_hand) AS total_on_hand
FROM fct_inventory_daily
WHERE product_key = 4471 AND date_key = 20250704;
-- total_on_hand | 100   (остатки всех складов на этот день — корректно)

-- НЕПРАВИЛЬНО: суммируем остаток по дням недели
SELECT SUM(quantity_on_hand) AS wrong
FROM fct_inventory_daily
WHERE product_key = 4471 AND warehouse_key = 7
  AND date_key BETWEEN 20250701 AND 20250707;
-- wrong | 700   (БЕССМЫСЛЕННО: те же 100 штук посчитаны 7 раз)

-- ПРАВИЛЬНО для времени: средний остаток за неделю
SELECT AVG(quantity_on_hand) AS avg_on_hand
FROM fct_inventory_daily
WHERE product_key = 4471 AND warehouse_key = 7
  AND date_key BETWEEN 20250701 AND 20250707;
-- avg_on_hand | 100   (средний остаток — корректно)

Заметьте при этом: units_received и units_shipped в той же таблице — это потоки за день, и они полностью аддитивны, в том числе по времени. «Сколько поступило за неделю» — это честная SUM(units_received) по семи дням. В одной снимок-таблице могут жить и semi-additive measures состояния (остаток), и additive measures потоков (поступило, отгружено). Подробная классификация additive / semi-additive / non-additive — отдельный урок этого модуля; здесь главное запомнить: measures состояния в periodic snapshot не складываются по времени.

WARNING

Складывание остатков, балансов и других уровней по времени — одна из самых частых ошибок в аналитике. SUM по такому measure через несколько дат не падает, а тихо возвращает завышенное в разы число. Признак semi-additive measure — он отвечает на вопрос «сколько ЕСТЬ сейчас», а не «сколько произошло». «Есть» по времени не складывается; «произошло» складывается.

Физика хранения плотной snapshot-таблицы в колоночном движке

Transaction и periodic snapshot — дополняют друг друга

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

Тот же склад можно (и часто нужно) моделировать обоими типами сразу. fct_inventory_movements — транзакционная: строка на каждое движение (поступление, отгрузка), отвечает на «какие движения товара были, по какой причине, кем». fct_inventory_daily — периодический снимок: строка на остаток каждого товара на конец каждого дня, отвечает на «каков был остаток на дату».

Можно ли посчитать остаток из одной транзакционной таблицы? Теоретически да — просуммировать все поступления минус все отгрузки от начала времён до нужной даты. Но это дорого (скан всей истории движений на каждый запрос остатка) и неудобно. Периодический снимок предвычисляет это состояние и хранит готовым — остаток на любую дату достаётся одной строкой. Снимок — это, по сути, материализованное, регулярно обновляемое состояние, чтобы не пересчитывать его из потока событий каждый раз.

СвойствоTransaction fact tablePeriodic snapshot fact table
Зерноодно событиесостояние сущности за период
Что записываетсобытие произошлоснимок положения дел
Плотностьразрежена (строка на событие)плотная (строка на сущность за период)
Размерзависит от числа событийсущности умножить на периоды, известен заранее
Когда появляется строкакогда случилось событиекаждый период, по расписанию
Отвечает на вопроскакие события происходиликаким было состояние на момент
Measures состоянияsemi-additive (не складываются по времени)

Где periodic snapshot — правильный выбор

Periodic snapshot подходит, когда бизнес-вопрос про состояние на регулярные моменты времени, а не про отдельные события:

  • Остатки на складе на конец каждого дня.
  • Баланс счёта на конец каждого дня или месяца.
  • Число активных подписок / пользователей на конец каждой недели.
  • Стоимость портфеля на конец каждого торгового дня.
  • Заполненность отеля на каждую ночь.

Общее: измеряется уровень, который существует и держится во времени, и его снимают регулярно. Если же вопрос про дискретные события («какие платежи прошли») — это transaction fact table. Если про прохождение процесса через вехи — accumulating snapshot, следующий урок.

Попробуй сам

Возьмите банковское приложение. Нужна аналитика по счетам клиентов.

Спроектируйте periodic snapshot fct_account_balance_daily. Сформулируйте зерно одним предложением. Выпишите foreign keys (день, счёт, клиент, тип счёта) и measures. Среди measures намеренно сделайте и состояние (баланс на конец дня), и потоки (сумма поступлений за день, сумма списаний за день).

Затем разберитесь с аддитивностью. Для каждого вашего measure ответьте: складывается ли он по разрезу «клиент» (баланс всех счетов одного клиента), складывается ли по разрезу «время» (за месяц). Напишите два SQL-запроса: один правильный (суммирует баланс по счетам клиента на один день) и один неправильный (суммирует баланс по дням месяца) — и посчитайте, во сколько раз и почему завышен результат неправильного. Наконец, объясните: почему банку нужны и periodic snapshot балансов, и transaction fact table отдельных операций — на какой вопрос отвечает каждая таблица?


Проверка знанийKnowledge check
Что такое periodic snapshot fact table, чем её плотность отличается от разреженности transaction fact table, и почему её measures состояния нельзя суммировать по времени?
ОтветAnswer
Periodic snapshot fact table (таблица фактов периодических снимков) — второй тип fact-таблицы по Кимбаллу. Её зерно: одна строка — это состояние одной сущности за один регулярный интервал времени (остаток товара на складе на конец каждого дня, баланс счёта на конец месяца). В отличие от transaction fact table, которая записывает событие, периодический снимок записывает снимок состояния — каким было положение дел на конец периода — и делает это регулярно по расписанию, независимо от того, происходили события или нет. Plотность: transaction fact table разрежена — строка есть только там, где произошло событие; periodic snapshot плотная — строка есть для каждой сущности за каждый период всегда, даже если ничего не менялось (товар неделю лежал без движений — в снимке за эту неделю семь строк состояния покоя, в транзакционной таблице ноль строк). Поэтому размер снимка предсказуем: число сущностей умножить на число периодов. Measures состояния в periodic snapshot — semi-additive (полуаддитивные): их можно суммировать по разрезам вроде склада (остаток товара по всем складам на один день — корректно), но нельзя суммировать по времени. Складывать остаток за семь дней бессмысленно: если на складе всю неделю лежало 100 штук, такая сумма даст 700, но 700 штук не существует — это одни и те же 100 штук, посчитанные семь раз. Остаток — это уровень, который держится, а не поток, который накапливается; уровни за разные моменты времени не складывают. По времени для measure состояния корректны только последнее значение или среднее. При этом потоки в той же таблице (поступило, отгрузилось за день) полностью аддитивны, в том числе по времени. Periodic snapshot и transaction fact table дополняют друг друга: первая отвечает на вопрос про состояние, вторая — про события.

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

Результат: 0 из 0
Концептуальный
Вопрос 1 из 4. Чем плотность periodic snapshot отличается от разреженности transaction fact table?

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

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

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

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