Зачем вообще window functions
Если бы меня спросили, какая одна фича в SQL отделяет аналитика-новичка от уверенного middle — я бы без раздумий назвал оконные функции. Это инструмент, которым в реальной работе пользуются ежедневно: «процент от общего», «номер заказа клиента подряд», «running total выручки», «топ-3 продукта в каждой категории», «дельта между этим и предыдущим месяцем». Всё это — задачи окна.
GROUP BY с агрегатами справляется только с половиной таких задач, и при этом обязательно «схлопывает» строки. Окно же делает агрегат, не теряя строк — для каждой исходной строки рядом возникает дополнительная колонка с агрегатом по «соседям». Именно этим оно и ценно.
Где GROUP BY ломается
Возьмём типовой запрос аналитика: «какую долю от общей выручки за день дал каждый отдельный заказ».
С GROUP BY мы можем посчитать
Слева: GROUP BY схлопывает 3 заказа дня в 1 строку. Справа: окно сохраняет все 3 строки и добавляет агрегат как ещё одну колонку.
Раньше, до появления window functions в стандарте SQL:2003, такие задачи решали через self-join или коррелированный подзапрос — оба варианта медленные и трудночитаемые. Сегодня это однострочник.
Каждый платёж и его доля в общей выручке за день — без GROUP BY:
Обрати внимание: SUM(...) OVER (PARTITION BY ...) — это полноценная агрегация, но она применяется к окну вокруг каждой строки, а сама строка остаётся в результате. Это и есть главная идея.
Анатомия func() OVER (...)
Любая оконная функция состоит из двух частей.
- Функция — то, что считаем:
SUM,AVG,COUNT,ROW_NUMBER,RANK,LAG,LEAD,FIRST_VALUE, и так далее. Это может быть и обычный агрегат, и одна из специальных «оконных» функций (ROW_NUMBERи компания не существуют вне окна — безOVERони синтаксически невалидны). OVER (...)— определение окна. Внутри скобок может быть три части, в строгом порядке:PARTITION BY— на какие группы разбить таблицу.ORDER BY— как упорядочить строки внутри группы.frame— какое подмножество строк внутри группы реально участвует в подсчёте (ROWS BETWEEN ... AND ...).
Три необязательных компонента OVER в строгом порядке. Любой может отсутствовать — но порядок при их совместном использовании фиксирован.
Пустое OVER () — тоже валидно. Это окно, в которое попадают все строки результата. Используется, например, для «доли от общего по всей выборке».
Пустое OVER () = окно по всем строкам. Считаем долю каждого платежа в общей выручке:
Сравни с тем, что было бы без окна. Если бы мы писали через подзапрос — пришлось бы сначала считать SELECT SUM(amount_cents) FROM payments отдельным запросом, потом подставлять. С окном — одна строка кода и один проход по данным.
Окно vs группа: интуиция
Чтобы навсегда запомнить разницу, держи в голове такую картинку.
- GROUP BY превращает таблицу в новую таблицу с меньшим числом строк. На выходе — одна строка на каждое уникальное значение группы.
- Window functions оставляют таблицу того же размера, но добавляют новые колонки с агрегатами по «соседям» (с теми, кто попадает в окно текущей строки).
Это разные семантические операции, не «два способа сделать одно и то же». Когда тебе нужна сводка — используешь GROUP BY. Когда тебе нужны детали плюс контекст — окно.
Часто эти операции комбинируют: сначала GROUP BY, потом окно поверх агрегированного результата. Этот шаблон мы разберём в уроке 6.
Running total — классическая задача окна
Ещё один кейс, который без окна писать больно: накопительная сумма по времени. Каждая строка должна показывать сумму всех предыдущих платежей плюс свой.
Running total: накопительная выручка по времени, без GROUP BY:
Здесь нет PARTITION BY — значит, окно одно на всю выборку. Есть ORDER BY paid_at — значит, для каждой строки в окно попадают все строки от начала выборки и до текущей включительно (это дефолтное поведение frame, о котором будет урок 4).
Попробуй переписать тот же запрос без window functions — увидишь, как страшен self-join: каждой строке нужно «доставить» массив всех предыдущих, потом просуммировать. Это O(N^2) запрос там, где окно делает за O(N log N) или быстрее.
Где окно может появиться в запросе
Window functions можно использовать только в SELECT и в ORDER BY. В WHERE, GROUP BY, HAVING — нельзя. Это не каприз, а следствие порядка выполнения запроса: окно вычисляется после агрегации и фильтрации. К моменту, когда WHERE отсеивает строки, окно ещё не существует.
Чтобы отфильтровать по результату окна — нужно обернуть запрос в подзапрос или CTE.
Окно нельзя в WHERE — оборачиваем в подзапрос. Найдём платежи, которые дали >5% дневной выручки:
Та же логика, что и с агрегатами в HAVING: «фильтровать после агрегации» в SQL всегда требует либо HAVING (для GROUP BY), либо обёртки в подзапрос (для окон).
Нюанс: что считается «окном» по умолчанию
Когда у тебя есть OVER (ORDER BY ...) без явного ROWS/RANGE — frame по умолчанию равен RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW. Это значит: «все строки от начала окна до текущей включительно, по логике ORDER BY». Поэтому running total в примере выше работает «сам собой».
Если убрать ORDER BY — frame по умолчанию становится «вся партиция целиком». Поэтому SUM(x) OVER () без ORDER BY даёт сумму всей выборки, а SUM(x) OVER (ORDER BY date) — нарастающую сумму.
Разница между RANGE и ROWS для frame будет в уроке 4 — там много неочевидного и важного.
Чек-лист
- Window functions считают агрегаты по соседям строки, не схлопывая саму строку. Это главное отличие от GROUP BY.
- Анатомия:
func() OVER (PARTITION BY ... ORDER BY ... frame). Любая часть OVER может отсутствовать. OVER ()— окно по всем строкам. Используется для «доли от общего».OVER (ORDER BY ...)— окно от начала и до текущей строки включительно. Базовый инструмент для running total.- Окно работает только в
SELECTиORDER BY. Чтобы фильтровать по результату окна — оборачивай в подзапрос или CTE. - Окно вычисляется после WHERE, GROUP BY и HAVING — но до финального ORDER BY и LIMIT.