В дашборде бюджет завышается в месяцах с несколькими дневными продажами. Какой дефект модели объясняет результат?
WITH sales AS (
SELECT '2025-01' AS month, 100 AS amount
UNION ALL SELECT '2025-01', 150
), budget AS (
SELECT '2025-01' AS month, 200 AS amount
)
SELECT s.month, SUM(s.amount) AS sales, SUM(b.amount) AS budget
FROM sales s
JOIN budget b ON b.month = s.month
GROUP BY s.month;
Причина — соединение таблиц с разным уровнем детализации: продажи хранятся по дням, а бюджет — по месяцам. При соединении по месяцу одна строка бюджета присоединяется к каждой дневной строке, поэтому SUM(b.amount) считает один и тот же бюджет несколько раз.
Нужно хранить факты продаж и бюджета отдельно и связывать их через общие измерения, либо предварительно агрегировать продажи до месячного уровня перед сравнением.
В аналитических хранилищах разделение данных на факты и измерения появилось как способ избежать неоднозначных расчётов и сделать отчётность предсказуемой. Факт обычно описывает события на определённом уровне детализации: например, одну продажу или продажи за день.
Разные бизнес-процессы имеют разные уровни детализации. Продажи могут фиксироваться ежедневно, а бюджет — ежемесячно или ежегодно. BI-модель должна сохранять это различие, а не скрывать его неявным соединением таблиц.
В примере две строки продаж относятся к одному месяцу, а бюджет представлен одной строкой. После соединения бюджет 200 появляется в результате дважды: по одному разу для каждой строки продаж. Итоговый SUM(b.amount) становится равен 400, хотя фактический бюджет равен 200.
Такая ошибка опасна тем, что визуально отчёт может выглядеть правдоподобно. Завышенный бюджет искажает отклонение от плана, проценты выполнения и управленческие выводы; при увеличении детализации продаж ошибка обычно становится больше.
Главный принцип — перед агрегацией убедиться, что показатель считается на корректном зерне факта. Если бюджет задан на уровне месяца, его нельзя суммировать после присоединения к строкам, имеющим более мелкое зерно, если бюджет не был предварительно распределён по этим строкам.
Безопасный вариант — сначала агрегировать продажи до месяца, а затем соединить результат с месячным бюджетом:
В более устойчивой BI-модели продажи и бюджет обычно остаются отдельными таблицами фактов. Они связываются с общими измерениями, например календарём, организацией и продуктом, но не соединяются напрямую строка-к-строке. Мера бюджета должна учитывать её исходное зерно и не выполнять обычное суммирование поверх случайно размноженных строк.
Простое SUM(DISTINCT budget) не является универсальным исправлением: одинаковые суммы разных бюджетных записей могут ошибочно схлопнуться. Также нельзя бездумно распределять месячный бюджет по дням — для этого нужна согласованная бизнес-логика, например равномерное распределение или профиль сезонности.
В компании сравнивали дневную выручку с месячным планом. Первоначально месячный план присоединили к ежедневным продажам, после чего показатель выполнения плана в BI-системе стал существенно ниже ожидаемого. Проверка исходных таблиц показала, что сумма продаж была верной, а план дублировался на каждой дневной строке.
Рассматривались три варианта. Уникальная агрегация бюджета была бы быстрой, но могла скрыть реальные дубли и сломаться при совпадающих значениях. Распределение месячного плана по дням позволяло строить дневной график, но требовало согласованного правила распределения и меняло смысл показателя. Выбранным решением стало отдельное сравнение месячных агрегатов для KPI и отдельная таблица распределённого дневного плана для графиков, где такой прогноз действительно требовался.
После этого месячный KPI перестал зависеть от количества дней и транзакций, а дневная визуализация получила явно задокументированную методику распределения бюджета.
1. Можно ли просто использовать MAX(budget) вместо SUM(budget) после соединения?
Только если в каждой группе действительно существует ровно одно логическое значение бюджета и это правило гарантировано моделью. MAX может замаскировать размножение строк, но не устраняет ошибочное соединение; при наличии нескольких бюджетных записей он даст неверный результат.
2. Почему фильтр по продукту может сделать месячный бюджет неполным?
Если бюджет задан только на уровне месяца и не содержит продукта, фильтр продукта нельзя автоматически применять к нему как к обычному факту. Такой фильтр либо не должен влиять на бюджет, либо бюджет должен быть спланирован на уровне продукта. Иначе BI-система создаёт видимость детализации, которой нет в исходных данных.
3. Как проверить, что мера не зависит от случайного числа строк?
Нужно сравнить результат с контрольным расчётом на исходном зерне и проверить его при изменении детализации: например, при переходе от месяца к дню и при добавлении транзакционного измерения. Если бюджет меняется только потому, что появилось больше строк продаж, мера нарушает зерно данных или использует некорректную связь.