Learning Platform
Урок 09.06 · 19 мин
Средний
Window vs GROUP BYAggregation inside windowQuery semanticsLogical execution order

К этому моменту мы умеем считать почти всё, что может понадобиться в окне. Самое время остановиться и упорядочить главное: в каких ситуациях GROUP BY, а в каких окно — и почему. Это не просто стиль — это вопрос корректности.

Логическая последовательность выполнения запроса

Чтобы понять, что во что вкладывается, держи в голове логический порядок выполнения SQL-запроса:

Логический порядок выполнения SELECT

Стандарт SQL определяет именно такой порядок (физически PostgreSQL может оптимизировать — но семантика именно эта). Окна — после агрегации, до финального ORDER BY.

1FROM + JOIN — собираем исходные строки
2WHERE — фильтруем строки
3GROUP BY — группируем
4HAVING — фильтруем группы
5SELECT (вычисления, window functions)Окно вычисляется здесь — уже после агрегации. Поэтому в окне можно использовать агрегаты GROUP BY.
6DISTINCT
7ORDER BY
8LIMIT / OFFSET

Из этого вытекает всё остальное:

  • В 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 работает; в окне такого нет. Сколько уникальных методов оплаты у каждого клиента:

PostgreSQL

В окне это будет ошибкой: COUNT(DISTINCT p.method) OVER (PARTITION BY o.customer_id) PostgreSQL не примет.

Использование агрегата в HAVING

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

HAVING — фильтрация групп. Клиенты с более чем 2 платежами:

PostgreSQL

Чем окно умеет то, чего не умеет 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 нумерации:

PostgreSQL

Можно ли вкладывать агрегат в окно

Можно — это легальная и часто используемая конструкция.

Агрегат внутри окна — нормально. Считаем долю клиента в выручке (а сначала агрегируем по клиенту):

PostgreSQL

Что здесь происходит:

  1. GROUP BY группирует по customer_id, считает SUM(amount_cents) для каждого клиента.
  2. После агрегации применяется окно: внешний SUM(SUM(amount_cents)) OVER () — это «сумма агрегатов по всем группам», то есть общая выручка.
  3. Деление даёт долю клиента в общей выручке.

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 + дельта к прошлому дню:

PostgreSQL

Это типичный дашбордовый запрос — почти любой отчёт строится в две стадии: «свести» (GROUP BY) и «оконтурить контекст» (окно). Иногда то же самое можно написать и без CTE — agg-функции внутри окна как мы видели выше — но CTE делает запрос читаемее.

Эвристика выбора

Кратко, как решать «GROUP BY или окно»:

  • Если в выводе ты ждёшь одну строку на группу — GROUP BY.
  • Если в выводе ты ждёшь все строки + контекст группы — окно.
  • Если нужны скользящие/нарастающие/позиционные агрегаты — окно (GROUP BY этого не умеет).
  • Если нужен COUNT(DISTINCT x) в группе — GROUP BY (окно DISTINCT не умеет).
  • Если нужно отфильтровать по результату — GROUP BY + HAVING, или окно + обёртка в подзапрос.
  • Часто оптимально скомбинировать: GROUP BY → CTE → окно.
Проверка знанийKnowledge check
Запрос: SELECT department, AVG(salary) OVER () FROM employees GROUP BY department; Что вернёт? Сколько строк? Что в колонке AVG?
ОтветAnswer
GROUP BY department группирует по отделам — вернётся столько строк, сколько уникальных отделов. AVG(salary) — это агрегат GROUP BY, но без него в SELECT нельзя. На самом деле этот запрос упадёт с ошибкой: salary не агрегирован и не в GROUP BY. Если переписать как SELECT department, AVG(AVG(salary)) OVER () FROM employees GROUP BY department — то это уже корректно: внутренний AVG — агрегат по отделу, внешний AVG — окно поверх группы по всем отделам. Получится «средняя средняя зарплата по отделам». Урок: окно над агрегатом ОК, агрегат без GROUP BY и без обёртки — нет.
GROUP BY vs оконные функции в dbt-моделях

Чек-лист

  • Логический порядок: 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, дельта, доля).

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

Результат: 0 из 0
Концептуальный
Вопрос 1 из 4. В каком месте логического порядка выполнения SQL-запроса вычисляются window functions?

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

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

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

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