К этому моменту мы умеем считать почти всё, что может понадобиться в окне. Самое время остановиться и упорядочить главное: в каких ситуациях GROUP BY, а в каких окно — и почему. Это не просто стиль — это вопрос корректности.
Логическая последовательность выполнения запроса
Чтобы понять, что во что вкладывается, держи в голове логический порядок выполнения SQL-запроса:
Стандарт SQL определяет именно такой порядок (физически PostgreSQL может оптимизировать — но семантика именно эта). Окна — после агрегации, до финального ORDER BY.
Из этого вытекает всё остальное:
- В
WHEREнельзя использовать ни агрегаты, ни окна — на этапе WHERE их ещё нет. - В
HAVINGможно использовать агрегаты (они уже посчитаны), но нельзя окна (они будут позже). - В
SELECTможно всё — и агрегаты, и окна, и комбинировать их. - В
ORDER BYможно использовать окна, потому что он идёт после SELECT.
Чем GROUP BY умеет то, чего не умеет окно
Уменьшение размера выборки
GROUP BY возвращает по одной строке на группу. Окно — столько же строк, сколько было. Если задача звучит как «сколько уникальных клиентов сегодня заказали» — это GROUP BY (или COUNT(DISTINCT ...)).
Через окно ту же сводку можно получить — но с дублированием строк, потом DISTINCT. Это неэффективно и нечитаемо.
COUNT(DISTINCT x) в группе
COUNT(DISTINCT x) работает в обычных агрегатах, но в окне DISTINCT не поддерживается (есть редкие СУБД-исключения, но не PostgreSQL). Если нужен «уникальный счёт в окне» — приходится крутить CTE и хитрые приёмы (например, dense_rank плюс max).
COUNT(DISTINCT) в GROUP BY работает; в окне такого нет. Сколько уникальных методов оплаты у каждого клиента:
В окне это будет ошибкой: COUNT(DISTINCT p.method) OVER (PARTITION BY o.customer_id) PostgreSQL не примет.
Использование агрегата в HAVING
HAVING позволяет отфильтровать сами группы по их агрегатам. Окно не может — окно нужно оборачивать в подзапрос для аналогичной фильтрации.
HAVING — фильтрация групп. Клиенты с более чем 2 платежами:
Чем окно умеет то, чего не умеет GROUP BY
Сохранение всех строк
Главное и неоспоримое преимущество. Если задача требует «для каждой строки показать что-то про её группу» — это окно. GROUP BY не сможет, не теряя строк.
Скользящие агрегаты, running totals
GROUP BY вообще не умеет понятия «строки от начала до текущей». Это чисто оконная семантика — без frame и ORDER BY внутри OVER такого не выразить.
LAG, LEAD, FIRST_VALUE и прочее
GROUP BY такого не имеет в принципе. Это функции от позиции в упорядоченной последовательности — а GROUP BY уже потерял исходные строки, остались только агрегаты.
Top-N per group
GROUP BY возвращает одну строку на группу. Чтобы получить, например, топ-3 заказа каждого клиента — нужно либо коррелированный подзапрос с LIMIT (не работает в стандартном SQL), либо окно.
Можно ли вкладывать окно в окно
Нельзя. PostgreSQL запретит написать SUM(ROW_NUMBER() OVER (...)) OVER (...) напрямую. Если нужно — оборачивай во внешний подзапрос или CTE: сначала вычисли первое окно, потом второе поверх результата.
Два окна через CTE — нумеруем платежи клиента и считаем running total нумерации:
Можно ли вкладывать агрегат в окно
Можно — это легальная и часто используемая конструкция.
Агрегат внутри окна — нормально. Считаем долю клиента в выручке (а сначала агрегируем по клиенту):
Что здесь происходит:
- GROUP BY группирует по customer_id, считает
SUM(amount_cents)для каждого клиента. - После агрегации применяется окно: внешний
SUM(SUM(amount_cents)) OVER ()— это «сумма агрегатов по всем группам», то есть общая выручка. - Деление даёт долю клиента в общей выручке.
SUM(SUM(x)) OVER () выглядит дико на первый взгляд, но логически это просто: внутренний SUM — агрегат GROUP BY, внешний — окно поверх него.
Нельзя положить окно внутрь агрегата
Нельзя. SUM(ROW_NUMBER() OVER (...)) — синтаксическая ошибка. Причина — порядок выполнения: GROUP BY (3) идёт до окон (5), поэтому окно ещё не существует, когда применяется агрегат.
Это не оптимизаторное ограничение, а семантическое — нет смысла говорить «сумма ROW_NUMBER в группе», потому что ROW_NUMBER ещё не существует на момент группировки.
Решение — обычное: посчитать окно в CTE, потом агрегировать.
Комбинирование: GROUP BY → окно поверх
Один из самых полезных паттернов: сначала свести данные через GROUP BY (например, в дневные суммы), потом наложить окно — для running total, дельт, скользящих средних.
Дневной отчёт: revenue + running total + дельта к прошлому дню:
Это типичный дашбордовый запрос — почти любой отчёт строится в две стадии: «свести» (GROUP BY) и «оконтурить контекст» (окно). Иногда то же самое можно написать и без CTE — agg-функции внутри окна как мы видели выше — но CTE делает запрос читаемее.
Эвристика выбора
Кратко, как решать «GROUP BY или окно»:
- Если в выводе ты ждёшь одну строку на группу — GROUP BY.
- Если в выводе ты ждёшь все строки + контекст группы — окно.
- Если нужны скользящие/нарастающие/позиционные агрегаты — окно (GROUP BY этого не умеет).
- Если нужен
COUNT(DISTINCT x)в группе — GROUP BY (окно DISTINCT не умеет). - Если нужно отфильтровать по результату — GROUP BY + HAVING, или окно + обёртка в подзапрос.
- Часто оптимально скомбинировать: GROUP BY → CTE → окно.
Чек-лист
- Логический порядок: FROM → WHERE → GROUP BY → HAVING → SELECT (тут окна) → DISTINCT → ORDER BY → LIMIT.
- GROUP BY коллапсирует строки, окно — нет. Это семантическое различие, не стилистическое.
- В окне нет
COUNT(DISTINCT x)— а в GROUP BY есть. - Агрегат внутри окна — легально и полезно (
SUM(SUM(x)) OVER ()— доля от общего). - Окно внутри агрегата — запрещено: окна вычисляются после агрегации.
- Окно внутри окна — только через CTE/подзапрос.
- Типичный паттерн отчёта: GROUP BY → CTE → окно поверх (running total, дельта, доля).