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

Сравните оконный агрегат с GROUP BY: как получить общую сумму по набору, сохранив в результате каждую исходную строку?

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

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

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

SELECT order_id, customer_id, amount, SUM(amount) OVER () AS total_amount, SUM(amount) OVER (PARTITION BY customer_id) AS customer_total FROM orders;

SUM(amount) OVER () считает сумму по всему набору строк, а PARTITION BY customer_id — отдельную сумму для каждого клиента. Обе функции возвращают значение в каждой строке, поэтому заказ остаётся доступен вместе с рассчитанными итогами.

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

Обычные агрегаты и GROUP BY хорошо решают задачу свёртки данных: из множества строк получается одна строка на группу. Однако аналитические отчёты часто требуют одновременно видеть детальную запись и показатель её группы или всего набора.

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

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

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

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

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

Оконная функция состоит из агрегата и описания окна. Пустое OVER () означает одно окно для всего входного набора. PARTITION BY делит строки на независимые разделы, но не удаляет строки из результата.

Важно отличать группировку от разбиения окна:

  • GROUP BY меняет зерно результата: обычно одна строка соответствует одной группе;
  • PARTITION BY задаёт области расчёта, сохраняя одну результирующую строку на исходную строку;
  • оконный агрегат может использовать уже отфильтрованные строки, поэтому условие WHERE влияет на его знаменатель и итог;
  • если оконное вычисление должно учитывать данные до фильтрации, исходный набор обычно выносят во вложенный запрос или CTE, а фильтруют внешний результат.

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

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

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

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

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

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

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

  1. Что изменится, если добавить PARTITION BY к оконной сумме?

    Без PARTITION BY агрегат рассчитывается по всему набору строк, который поступил в оконное вычисление. С PARTITION BY customer_id этот набор логически разделяется по клиентам, и каждая строка получает итог только своего клиента.

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

  2. Почему фильтр перед оконным вычислением может изменить общую сумму?

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

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

  3. Можно ли применить оконный агрегат к результату, уже сгруппированному через GROUP BY?

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

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