В отчёте требуется показать долю каждой группы в общем итоге без потери строк групп; какой порядок вычислен...

В отчёте требуется показать долю каждой группы в общем итоге без потери строк групп; какой порядок вычислений обеспечит корректный знаменатель?

Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

Сначала нужно получить по одной строке на каждую группу с помощью GROUP BY, затем применить оконную функцию к этим уже агрегированным строкам. Знаменатель доли рассчитывается как сумма групповых итогов оконным агрегатом, поэтому он повторяется в каждой строке, не схлопывая результат.

Исторический контекст

GROUP BY предназначен для свёртки множества исходных строк в набор групповых итогов. Такой результат удобен для отчётов, но после агрегации исходный контекст строки теряется.

Оконные функции появились как средство аналитических расчётов поверх результата, где важно одновременно видеть значение строки и общий, накопительный или соседний контекст. Это позволяет вычислять доли, ранги и отклонения без дополнительного схлопывания строк.

Постановка проблемы

Пусть отчёт должен содержать категорию, её сумму и долю этой суммы в общем объёме. Если сначала вычислять оконный итог на уровне исходных продаж, а затем смешивать его с групповой агрегацией, можно получить разные уровни детализации и некорректное сопоставление числителя со знаменателем.

Особенно опасно применять фильтр к группам после расчёта долей без явного решения, что считать общим итогом: все группы или только прошедшие фильтр. От этого меняется знаменатель и, следовательно, смысл процентов.

Подробное решение

Сначала формируется набор group_totals: одна строка на группу и её агрегированное значение. Затем оконная функция SUM(...) OVER () суммирует столбец групповых итогов, сохраняя каждую строку результата.

WITH group_totals AS ( SELECT category, SUM(amount) AS group_total FROM sales GROUP BY category ) SELECT category, group_total, group_total / NULLIF(SUM(group_total) OVER (), 0) AS share FROM group_totals;

Внутренний запрос задаёт гранулярность результата — одну строку на category. Оконная сумма не объединяет эти строки, а вычисляет общий итог поверх них; поэтому каждая категория получает собственную сумму и единый знаменатель.

NULLIF защищает от деления на ноль, если общий итог равен нулю. В конкретной СУБД также нужно учитывать типы данных: при целочисленном делении для процентной доли может потребоваться привести числитель или знаменатель к десятичному типу.

Если перед оконным расчётом используется HAVING, в окно попадут только оставшиеся группы. Поэтому доли будут рассчитаны относительно отфильтрованного набора, а не обязательно относительно всех исходных данных. Для доли от общего объёма до фильтрации нужны отдельные этапы или отдельный оконный расчёт.

Ситуация из практики

В отчёте о продажах нужно показать вклад каждого региона в оборот. Вариант с коррелированным подзапросом может пересчитывать общий оборот для каждой строки, что усложняет план выполнения и делает намерение менее очевидным.

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

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

Что кандидаты часто упускают

  1. Что произойдёт, если применить оконную сумму к исходным продажам до группировки?

    Оконная сумма на исходном уровне посчитает общий оборот по строкам продаж, но не создаст корректный набор региональных итогов. Для доли региона всё равно потребуется связать этот общий результат с агрегатом региона; смешивание уровней детализации без такого шага может привести к повторению или неверному суммированию значений.

  2. Почему нельзя заменить оконную сумму обычной агрегатной функцией без OVER?

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

  3. Как изменится доля, если перед оконным расчётом отфильтровать группы через HAVING?

    Оконная функция работает с результатом предыдущих логических этапов, поэтому исключённые HAVING группы не войдут в её окно. Например, доли групп с оборотом выше порога будут суммироваться до ста процентов только в том случае, если знаменатель также рассчитывается по отфильтрованным группам. Если требуется доля от полного оборота, полный знаменатель нужно вычислить отдельно до фильтрации или вынести фильтр на внешний уровень.