Что такое оконные функции (Window Functions)?
Представьте школьный класс:
🟢 Junior Level
Оконные функции (Window Functions) — это специальные аналитические функции SQL, которые выполняют вычисления над набором связанных строк (окном), но при этом сохраняют исходную детализацию каждой строки, в отличие от GROUP BY, который схлопывает строки группы в одну.
graph TD
subgraph GROUP BY: Схлопывание строк
A1["Строка 1"] & A2["Строка 2"] & A3["Строка 3"] --> G["1 агрегированная строка"]
end
subgraph Window Functions: Сохранение строк
B1["Строка 1"] --> W1["Строка 1 + Оконный расчёт"]
B2["Строка 2"] --> W2["Строка 2 + Оконный расчёт"]
B3["Строка 3"] --> W3["Строка 3 + Оконный расчёт"]
end
Наглядная бытовая аналогия
Представьте школьный класс:
- GROUP BY: Учитель собирает дневники учеников и выдаёт директору одну записку: «Средний балл по классу — 4.2». Все индивидуальные оценки учеников потеряны.
- Оконная функция: Учитель возвращает дневник каждому ученику, но рядом с его личной оценкой ставит печать: «Средний балл по твоему классу — 4.2». Каждый ученик видит и свою оценку, и показатель своего класса.
Базовый пример
-- Для каждого сотрудника вывести его зарплату, а также среднюю зарплату его отдела:
SELECT
name,
department,
salary,
AVG(salary) OVER (PARTITION BY department) AS dept_avg_salary
FROM employees;
-- Результат:
-- name | department | salary | dept_avg_salary
-- Иван | IT | 150000 | 140000
-- Алексей | IT | 130000 | 140000
-- Ольга | HR | 90000 | 85000
-- Дарья | HR | 80000 | 85000
Основные категории оконных функций
- Агрегатные:
SUM() OVER(...),AVG() OVER(...),COUNT() OVER(...),MIN() / MAX() OVER(...). - Ранжирующие:
ROW_NUMBER(),RANK(),DENSE_RANK(),NTILE(). - Функции смещения (навигационные):
LAG()(предыдущая строка),LEAD()(следующая строка),FIRST_VALUE(),LAST_VALUE().
🟡 Middle Level
Анатомия оконного выражения (OVER Clause)
Любая оконная функция определяется конструкцией OVER (...), состоящей из трёх опциональных элементов:
FUNCTION() OVER (
PARTITION BY column_name -- 1. Разделение на партиции (окна)
ORDER BY sort_column -- 2. Сортировка строк внутри окна
ROWS BETWEEN ... AND ... -- 3. Рамка окна (Window Frame)
)
PARTITION BY— логически разделяет набор данных на независимые группы. ЕслиPARTITION BYопущен, окном является вся результирующая выборка.ORDER BY— задаёт порядок обхода строк внутри окна (критически важен для ранжирования и накопительных итогов).ROWS / RANGE / GROUPS— определяет границы подмножества строк (фрейма) относительно текущей строки.
Сравнение GROUP BY и Window Functions
| Характеристика | GROUP BY |
Window Functions |
|---|---|---|
| Результирующее число строк | Уменьшается (одна строка на группу) | Не изменяется (строка-в-строку) |
| Доступ к детальным данным | Теряется (только агрегаты и ключи группировки) | Сохраняется для каждого исходного поля |
| Место в конвейере SQL | До HAVING и SELECT |
Строго в момент формирования SELECT |
| Сочетаемость в одном запросе | Нельзя смешать детальные строки с группой | Можно комбинировать с GROUP BY (окна над агрегатами) |
Режимы фреймов: ROWS vs RANGE vs GROUPS
Если указан ORDER BY, но фрейм опущен, по умолчанию применяется:
RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW.
-- 1. ROWS: считает физические строки (наиболее производительный)
SELECT date, amount,
AVG(amount) OVER (
ORDER BY date
ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
) AS moving_avg_7_rows
FROM sales;
-- 2. RANGE: считает логические значения (учитывает дубликаты и интервалы)
SELECT date, amount,
SUM(amount) OVER (
ORDER BY date
RANGE BETWEEN INTERVAL '7 days' PRECEDING AND CURRENT ROW
) AS weekly_revenue
FROM sales;
[!WARNING] Разница между
ROWSи дефолтнымRANGE: при наличии повторяющихся значений вORDER BY(например, несколько заказов совершены в одну секунду),RANGEобъединит их все в один шаг и посчитает агрегат сразу для всей группы совпадений (peer rows), аROWSбудет строго идти поштучно.ROWSпрактически всегда работает быстрееRANGE.
Навигационные функции LAG() и LEAD()
Позволяют анализировать временные ряды без тяжелых SELF JOIN:
-- Расчет помесячного прироста выручки (Growth MoM)
SELECT
report_month,
revenue,
LAG(revenue, 1) OVER (ORDER BY report_month) AS prev_month_revenue,
revenue - LAG(revenue, 1) OVER (ORDER BY report_month) AS growth_amount
FROM monthly_financials;
Поддержка в Java, JPA и Hibernate
Исторически оконные функции были больным местом в Java-разработке:
- До Hibernate 6: Стандартный JPQL/HQL не поддерживал оконные функции. Разработчикам приходилось использовать либо
createNativeQuery(), либо создавать в базе SQL View, либо переходить на jOOQ. - В Hibernate 6+ (Spring Boot 3+): Оконные функции стали полноправной частью HQL/JPQL:
// ✅ Валидный HQL в Spring Boot 3 / Hibernate 6:
@Query("""
SELECT new com.example.dto.EmployeeRankDto(
e.id,
e.name,
e.salary,
DENSE_RANK() OVER (PARTITION BY e.department.id ORDER BY e.salary DESC)
)
FROM Employee e
""")
List<EmployeeRankDto> findEmployeeSalariesRanked();
🔴 Senior Level
Физический конвейер выполнения в PostgreSQL: Узел WindowAgg
Оконные функции вычисляются в плане запроса специальным исполнительным узлом WindowAgg:
QUERY PLAN:
WindowAgg (cost=125000.00..145000.00 rows=1000000 width=32) (actual time=820.10..1120.30 rows=1000000 loops=1)
-> Sort (cost=125000.00..127500.00 rows=1000000 width=24) (actual time=610.15..720.40 rows=1000000 loops=1)
Sort Key: department_id, salary DESC
Sort Method: quicksort Memory: 78000kB
-> Seq Scan on employees (...)
- СУБД считывает строки после этапов
WHERE,GROUP BYиHAVING. - Строки сортируются узлом
Sortпо ключам(PARTITION BY, ORDER BY). - Узел
WindowAggпоследовательно сканирует упорядоченный поток, удерживая в памяти активный фрейм строк и инкрементально обновляя агрегаты.
Индексация для исключения узла Sort
Сортировка миллионов строк для оконной функции — дорогая операция, требующая work_mem. Её можно полностью устранить, создав B-tree индекс, порядок колонок в котором в точности совпадает с PARTITION BY + ORDER BY:
-- Составной индекс под окно (department, created_at)
CREATE INDEX idx_orders_dept_date ON orders(department_id, created_at);
-- Запрос:
SELECT
department_id,
created_at,
amount,
SUM(amount) OVER (PARTITION BY department_id ORDER BY created_at) AS running_total
FROM orders;
-- План:
-- WindowAgg (cost=0.43..8500.00 rows=200000 width=20)
-- -> Index Scan using idx_orders_dept_date on orders
-- (Узел Sort ПОЛНОСТЬЮ ОТСУТСТВУЕТ!)
Проблема множественных окон и оптимизация через WINDOW clause
Если в одном запросе используются разные PARTITION BY или разные ORDER BY, PostgreSQL вынужден выполнять несколько независимых сортировок:
-- ❌ КАТАСТРОФА ДЛЯ ПРОИЗВОДИТЕЛЬНОСТИ: 3 разные сортировки
SELECT
AVG(amount) OVER (PARTITION BY user_id) AS u_avg,
AVG(amount) OVER (PARTITION BY store_id) AS s_avg,
AVG(amount) OVER (PARTITION BY region_id ORDER BY date) AS r_avg
FROM orders;
Оптимизация: Переиспользование окон через именованное предложение WINDOW:
SELECT
user_id,
amount,
AVG(amount) OVER w AS avg_spent,
SUM(amount) OVER w AS total_spent,
COUNT(*) OVER w AS orders_count
FROM orders
WINDOW w AS (PARTITION BY user_id ORDER BY created_at ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW);
Incremental Sort (PostgreSQL 13+)
Если таблица уже частично отсортирована по префиксу ключа (например, B-tree индекс есть только по department_id), начиная с PostgreSQL 13 оптимизатор применяет Incremental Sort:
- PostgreSQL не сортирует весь массив заново.
- Сортировка по второму полю (
created_at) выполняется небольшими порциями строго внутри каждого значенияdepartment_id. - Это снижает расход
work_memи в 5–10 раз сокращает задержку до получения первых строк (first rows latency).
Исключение строк из расчета через EXCLUDE (PostgreSQL 11+)
Конструкция EXCLUDE позволяет тонко управлять содержимым фрейма:
EXCLUDE CURRENT ROW— исключает из расчёта саму текущую строку (идеально для поиска аномалий: сравнение зарплаты сотрудника со средней зарплатой всех остальных коллег отдела).EXCLUDE GROUP— исключает текущую строку и все строки с одинаковым значением ключа сортировки.EXCLUDE TIES— исключает дубликаты ключа сортировки, но оставляет саму текущую строку.
SELECT name, department_id, salary,
AVG(salary) OVER (
PARTITION BY department_id
ORDER BY salary
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
EXCLUDE CURRENT ROW
) AS peers_average_salary
FROM employees;
🎯 Шпаргалка для интервью
Обязательно знать (Первые 30 секунд ответа)
- Что это: Оконные функции производят агрегатные и ранжирующие вычисления над набором связанных строк (
OVER), не схлопывая результат и сохраняя каждую исходную строку. - Синтаксис:
FUNCTION() OVER (PARTITION BY ... ORDER BY ... ROWS/RANGE BETWEEN ...). - Типы функций:
- Агрегаты:
SUM(),AVG(),COUNT(),MIN(),MAX(). - Ранжирование:
ROW_NUMBER()(сквозной номер),RANK()(с пропусками при равенстве),DENSE_RANK()(без пропусков). - Навигация:
LAG()(предыдущая),LEAD()(следующая).
- Агрегаты:
- Порядок выполнения в SQL: Оконные функции выполняются после
WHERE,GROUP BYиHAVING, но доDISTINCT,ORDER BYиLIMIT. - Фильтрация: Фильтровать строки по значению оконной функции в том же запросе через
WHEREилиHAVINGнельзя — требуется обёртка в CTE или подзапрос.
Частые уточняющие вопросы на засыпку
-
«Почему в WHERE нельзя отфильтровать ROW_NUMBER() = 1?» Ответ: Фаза
WHEREвыполняется на шаге 2 логического пайплайна (фильтрация строк таблицы), а узелWindowAggрассчитывается на шаге 5 (в момент формирования проекцииSELECT). Чтобы отфильтровать топ-1 запись, оконную функцию вычисляют в CTE или подзапросе, а фильтр накладывают во внешнемWHERE. -
«В чём разница между ROWS и RANGE при расчете накопительного итога?» Ответ:
ROWSработает с физическими смещениями строк, аRANGE— с логическими диапазонами значений. Если в колонке сортировки есть дубликаты (одинаковые даты),RANGEприбавит к накопительному итогу сразу все строки с совпадающей датой, аROWSбудет инкрементировать итог по одной строке. Кроме того,ROWSсущественно быстрее, так как не требует проверок равенства значений. -
«Поддерживает ли Hibernate оконные функции в запросах HQL?» Ответ: До версии Hibernate 6 оконные функции не поддерживались в HQL, требуя Native SQL или views. В Hibernate 6+ (Spring Boot 3+) оконные функции стали стандартной частью синтаксиса HQL/JPQL и могут мапиться напрямую в Java Records и DTO.
-
«Как составить идеальный индекс под оконную функцию?» Ответ: B-tree составной индекс по полям
(PARTITION BY columns, ORDER BY columns). При таком индексе СУБД считывает строки уже в идеальном порядке, и планировщик исключает ресурсоёмкую операциюSortперед узломWindowAgg.
Красные флаги (НЕ говорить на интервью)
- ❌ «Оконные функции заменяют GROUP BY во всех случаях» — если вам нужна только сводная строка на категорию без детализации,
GROUP BYработает быстрее и потребляет меньше памяти. - ❌ «Можно вложить одну оконную функцию внутрь другой (e.g. SUM(ROW_NUMBER()) OVER())» — вложенные оконные функции строго запрещены стандартом SQL (требуется CTE).
- ❌ «Оконные функции можно отфильтровать в секции HAVING» —
HAVINGфильтрует группыGROUP BY, а оконные функции рассчитываются позже. - ❌ «COUNT() OVER() ничего не стоит при пагинации»* — для подсчета общего числа строк
COUNT(*) OVER()заставит СУБД материализовать и прочитать весь набор данных, что разрушает оптимизацию пагинации сLIMIT.
Связанные темы
- Что делает ROW_NUMBER() → детальный разбор сквозной нумерации строк
- Что делает RANK() и DENSE_RANK() → алгоритмы ранжирования с дубликатами
- Что делает GROUP BY → сравнение агрегации со схлопыванием строк
- В чём разница между WHERE и HAVING → порядок исполнения фаз SQL-запроса