Диалект — PostgreSQL. OVER(), PARTITION BY, функции ранжирования, смещения, накопительные итоги.
Оконная функция — это функция, которая выполняет вычисления по набору строк, связанных с текущей строкой (это называется "окном"), но не сворачивает строки в одну.
GROUP BY:
Оконные функции:
Через GROUP BY (теряем детали):
SELECT department_id, AVG(salary) AS avg_salary
FROM employees
GROUP BY department_id;Результат:
| department_id | avg_salary |
|---|---|
| 1 | 120000 |
| 2 | 85000 |
| 3 | 72500 |
Мы видим среднюю зарплату по отделам, но не видим, кто сколько зарабатывает.
Через оконную функцию (сохраняем детали):
SELECT
name,
department_id,
salary,
AVG(salary) OVER (PARTITION BY department_id) AS dept_avg_salary
FROM employees;Результат:
| name | department_id | salary | dept_avg_salary |
|---|---|---|---|
| Анна | 1 | 120000 | 120000 |
| Мария | 1 | 130000 | 120000 |
| Павел | 1 | 110000 | 120000 |
| Иван | 2 | 80000 | 85000 |
| Елена | 2 | 90000 | 85000 |
| Сергей | 2 | 85000 | 85000 |
| Андрей | 3 | 70000 | 72500 |
| Дарья | 3 | 75000 | 72500 |
Мы видим каждого сотрудника и при этом среднюю зарплату по его отделу.
💡 Когда использовать оконные функции:
- Когда нужно добавить агрегат к каждой строке (например, "зарплата сотрудника vs средняя по отделу")
- Когда нужно ранжировать строки внутри группы
- Когда нужно сравнить строку с предыдущей/следующей
- Когда нужно посчитать накопительный итог
1. Базовый синтаксис: OVER(), PARTITION BY, ORDER BY
функция() OVER (
[PARTITION BY столбец1, столбец2, ...]
[ORDER BY столбец3, столбец4, ...]
[frame_clause] -- для агрегатных функций
)PARTITION BY работает как GROUP BY, но не сворачивает строки. Он разбивает данные на партиции (группы), и оконная функция применяется внутри каждой партиции независимо.
SELECT
name,
department_id,
salary,
COUNT(*) OVER (PARTITION BY department_id) AS dept_count
FROM employees;Результат:
| name | department_id | salary | dept_count |
|---|---|---|---|
| Анна | 1 | 120000 | 3 |
| Мария | 1 | 130000 | 3 |
| Павел | 1 | 110000 | 3 |
| Иван | 2 | 80000 | 3 |
| Елена | 2 | 90000 | 3 |
| Сергей | 2 | 85000 | 3 |
| Андрей | 3 | 70000 | 2 |
| Дарья | 3 | 75000 | 2 |
Каждая строка получает количество сотрудников в своём отделе.
ORDER BY в оконной функции определяет порядок строк внутри партиции. Это важно для функций ранжирования и для агрегатных функций с frame clause.
SELECT
name,
department_id,
salary,
ROW_NUMBER() OVER (PARTITION BY department_id ORDER BY salary DESC) AS rank_in_dept
FROM employees;Результат:
| name | department_id | salary | rank_in_dept |
|---|---|---|---|
| Мария | 1 | 130000 | 1 |
| Анна | 1 | 120000 | 2 |
| Павел | 1 | 110000 | 3 |
| Елена | 2 | 90000 | 1 |
| Сергей | 2 | 85000 | 2 |
| Иван | 2 | 80000 | 3 |
| Дарья | 3 | 75000 | 1 |
| Андрей | 3 | 70000 | 2 |
Внутри каждого отдела сотрудники отсортированы по убыванию зарплаты, и каждой строке присвоен номер.
Если не указать PARTITION BY, окно — это все строки результата.
SELECT
name,
salary,
SUM(salary) OVER () AS total_salary
FROM employees;Каждая строка получит общую сумму зарплат (760 000).
Эти функции присваивают номер каждой строке внутри партиции. Разница — в том, как они обрабатывают одинаковые значения.
Присваивает уникальный номер, даже если значения одинаковые.
SELECT
name,
salary,
ROW_NUMBER() OVER (ORDER BY salary DESC) AS row_num
FROM employees;Результат:
| name | salary | row_num |
|---|---|---|
| Мария | 130000 | 1 |
| Анна | 120000 | 2 |
| Павел | 110000 | 3 |
| Елена | 90000 | 4 |
| Сергей | 85000 | 5 |
| Иван | 80000 | 6 |
| Дарья | 75000 | 7 |
| Андрей | 70000 | 8 |
Если бы у двух сотрудников была одинаковая зарплата, они всё равно получили бы разные номера (какой именно — зависит от порядка обработки, недетерминировано).
SELECT
name,
salary,
RANK() OVER (ORDER BY salary DESC) AS rank
FROM employees;Если бы у Анны и Павла была одинаковая зарплата (120 000):
| name | salary | rank |
|---|---|---|
| Мария | 130000 | 1 |
| Анна | 120000 | 2 |
| Павел | 120000 | 2 |
| Елена | 90000 | 4 (пропустили 3) |
| ... | ... | ... |
SELECT
name,
salary,
DENSE_RANK() OVER (ORDER BY salary DESC) AS dense_rank
FROM employees;Если бы у Анны и Павла была одинаковая зарплата (120 000):
| name | salary | dense_rank |
|---|---|---|
| Мария | 130000 | 1 |
| Анна | 120000 | 2 |
| Павел | 120000 | 2 |
| Елена | 90000 | 3 (не пропустили) |
| ... | ... | ... |
💡 Когда что использовать:
ROW_NUMBER()— когда нужен уникальный номер (например, для пагинации)RANK()— когда нужно показать "место" с пропусками (как в спорте: 1, 2, 2, 4)DENSE_RANK()— когда нужно показать "место" без пропусков (1, 2, 2, 3)
WITH ranked AS (
SELECT
name,
department_id,
salary,
ROW_NUMBER() OVER (PARTITION BY department_id ORDER BY salary DESC) AS rn
FROM employees
)
SELECT name, department_id, salary
FROM ranked
WHERE rn <= 2;Результат:
| name | department_id | salary |
|---|---|---|
| Мария | 1 | 130000 |
| Анна | 1 | 120000 |
| Елена | 2 | 90000 |
| Сергей | 2 | 85000 |
| Дарья | 3 | 75000 |
| Андрей | 3 | 70000 |
Эти функции позволяют получить значение из следующей (LEAD) или предыдущей (LAG) строки в окне.
SELECT
name,
department_id,
salary,
LAG(salary) OVER (PARTITION BY department_id ORDER BY salary) AS prev_salary
FROM employees;Результат:
| name | department_id | salary | prev_salary |
|---|---|---|---|
| Павел | 1 | 110000 | NULL |
| Анна | 1 | 120000 | 110000 |
| Мария | 1 | 130000 | 120000 |
| Иван | 2 | 80000 | NULL |
| Сергей | 2 | 85000 | 80000 |
| Елена | 2 | 90000 | 85000 |
| Андрей | 3 | 70000 | NULL |
| Дарья | 3 | 75000 | 70000 |
Для первой строки в каждой партиции prev_salary = NULL, потому что предыдущей строки нет.
SELECT
name,
department_id,
salary,
LEAD(salary) OVER (PARTITION BY department_id ORDER BY salary) AS next_salary
FROM employees;Результат:
| name | department_id | salary | next_salary |
|---|---|---|---|
| Павел | 1 | 110000 | 120000 |
| Анна | 1 | 120000 | 130000 |
| Мария | 1 | 130000 | NULL |
| Иван | 2 | 80000 | 85000 |
| Сергей | 2 | 85000 | 90000 |
| Елена | 2 | 90000 | NULL |
| Андрей | 3 | 70000 | 75000 |
| Дарья | 3 | 75000 | NULL |
Можно указать, на сколько строк сместиться:
SELECT
name,
salary,
LAG(salary, 2) OVER (ORDER BY salary) AS salary_2_rows_back
FROM employees;Если нет предыдущей/следующей строки, можно указать значение по умолчанию:
SELECT
name,
department_id,
salary,
LAG(salary, 1, 0) OVER (PARTITION BY department_id ORDER BY salary) AS prev_salary
FROM employees;Теперь вместо NULL будет 0.
💡 Практическое применение:
- Посчитать разницу между текущей и предыдущей зарплатой
- Найти рост/падение показателей
- Сравнить текущий месяц с предыдущим
SELECT
name,
department_id,
salary,
LAG(salary) OVER (PARTITION BY department_id ORDER BY salary) AS prev_salary,
salary - LAG(salary) OVER (PARTITION BY department_id ORDER BY salary) AS salary_diff
FROM employees;Обычные агрегатные функции (SUM, AVG, COUNT, MIN, MAX) можно использовать как оконные. Но для них важно понимать frame clause — какую часть окна использовать.
Без ORDER BY агрегат считается по всей партиции:
SELECT
name,
department_id,
salary,
SUM(salary) OVER (PARTITION BY department_id) AS dept_total
FROM employees;Каждая строка получает сумму зарплат по своему отделу.
С ORDER BY агрегат считается накопительно — от начала партиции до текущей строки:
SELECT
name,
department_id,
salary,
SUM(salary) OVER (PARTITION BY department_id ORDER BY salary) AS running_total
FROM employees;Результат:
| name | department_id | salary | running_total |
|---|---|---|---|
| Павел | 1 | 110000 | 110000 |
| Анна | 1 | 120000 | 230000 |
| Мария | 1 | 130000 | 360000 |
| Иван | 2 | 80000 | 80000 |
| Сергей | 2 | 85000 | 165000 |
| Елена | 2 | 90000 | 255000 |
| Андрей | 3 | 70000 | 70000 |
| Дарья | 3 | 75000 | 145000 |
Для Анны: 110000 + 120000 = 230000. Для Марии: 110000 + 120000 + 130000 = 360000.
По умолчанию с ORDER BY используется frame: RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW (от начала партиции до текущей строки).
Можно указать явно:
SELECT
name,
department_id,
salary,
SUM(salary) OVER (
PARTITION BY department_id
ORDER BY salary
ROWS BETWEEN 1 PRECEDING AND CURRENT ROW
) AS sum_last_2
FROM employees;ROWS BETWEEN 1 PRECEDING AND CURRENT ROW — сумма текущей строки и одной предыдущей.
Результат:
| name | department_id | salary | sum_last_2 |
|---|---|---|---|
| Павел | 1 | 110000 | 110000 |
| Анна | 1 | 120000 | 230000 |
| Мария | 1 | 130000 | 250000 |
| Иван | 2 | 80000 | 80000 |
| Сергей | 2 | 85000 | 165000 |
| Елена | 2 | 90000 | 175000 |
| Андрей | 3 | 70000 | 70000 |
| Дарья | 3 | 75000 | 145000 |
Для Анны: 110000 + 120000 = 230000. Для Марии: 120000 + 130000 = 250000.
SELECT
name,
department_id,
salary,
AVG(salary) OVER (
PARTITION BY department_id
ORDER BY salary
ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING
) AS moving_avg
FROM employees;ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING — средняя по текущей, предыдущей и следующей строке.
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING — вся партиция (то же, что без ORDER BY)ROWS BETWEEN 3 PRECEDING AND CURRENT ROW — последние 4 строки (включая текущую)RANGE BETWEEN ... — работает со значениями, а не с позициями строк💡 ROWS vs RANGE:
ROWS— считает по количеству строкRANGE— считает по значениям (если есть дубликаты, они попадают в один frame)5. Комбинированные примеры
SELECT
name,
department_id,
salary,
RANK() OVER (PARTITION BY department_id ORDER BY salary DESC) AS rank_in_dept,
AVG(salary) OVER (PARTITION BY department_id) AS dept_avg,
salary - AVG(salary) OVER (PARTITION BY department_id) AS diff_from_avg
FROM employees;Результат:
| name | department_id | salary | rank_in_dept | dept_avg | diff_from_avg |
|---|---|---|---|---|---|
| Мария | 1 | 130000 | 1 | 120000 | 10000 |
| Анна | 1 | 120000 | 2 | 120000 | 0 |
| Павел | 1 | 110000 | 3 | 120000 | -10000 |
| Елена | 2 | 90000 | 1 | 85000 | 5000 |
| Сергей | 2 | 85000 | 2 | 85000 | 0 |
| Иван | 2 | 80000 | 3 | 85000 | -5000 |
| Дарья | 3 | 75000 | 1 | 72500 | 2500 |
| Андрей | 3 | 70000 | 2 | 72500 | -2500 |
SELECT
name,
department_id,
salary,
LAG(salary) OVER (PARTITION BY department_id ORDER BY salary) AS prev_salary,
salary - LAG(salary) OVER (PARTITION BY department_id ORDER BY salary) AS salary_growth
FROM employees
WHERE LAG(salary) OVER (PARTITION BY department_id ORDER BY salary) IS NOT NULL;⚠️ Ошибка: нельзя использовать оконные функции в
WHERE. Нужно обернуть в CTE:
WITH salary_data AS (
SELECT
name,
department_id,
salary,
LAG(salary) OVER (PARTITION BY department_id ORDER BY salary) AS prev_salary
FROM employees
)
SELECT
name,
department_id,
salary,
prev_salary,
salary - prev_salary AS salary_growth
FROM salary_data
WHERE prev_salary IS NOT NULL;SELECT
name,
salary,
SUM(salary) OVER (ORDER BY salary) AS running_total,
SUM(salary) OVER () AS grand_total
FROM employees;⚠️ Оконные функции нельзя использовать в
WHERE,GROUP BY,HAVING.
Почему? Потому что оконные функции выполняются после WHERE, GROUP BY, HAVING, но до ORDER BY.
| Этап | Клауза | Описание |
|---|---|---|
| 1 | FROM | → берём таблицу |
| 2 | WHERE | → отбрасываем лишние строки |
| 3 | GROUP BY | → группируем |
| 4 | HAVING | → фильтруем группы |
| 5 | SELECT | → вычисляем оконные функции |
| 6 | ORDER BY | → сортируем готовый результат |
| 7 | LIMIT | → обрезаем |
Если нужно отфильтровать по результату оконной функции, используй CTE или подзапрос:
-- НЕ РАБОТАЕТ:
SELECT name, salary
FROM employees
WHERE RANK() OVER (ORDER BY salary DESC) <= 3;
-- РАБОТАЕТ:
WITH ranked AS (
SELECT name, salary, RANK() OVER (ORDER BY salary DESC) AS rnk
FROM employees
)
SELECT name, salary
FROM ranked
WHERE rnk <= 3;Попробуй решить самостоятельно, не подглядывая ниже.
ROW_NUMBER()).ROW_NUMBER()).-- 1
SELECT
name,
salary,
SUM(salary) OVER () AS total_salary
FROM employees;
-- 2
SELECT
name,
salary,
AVG(salary) OVER (PARTITION BY department_id) AS dept_avg,
salary - AVG(salary) OVER (PARTITION BY department_id) AS diff
FROM employees;
-- 3
SELECT
name,
department_id,
salary,
ROW_NUMBER() OVER (PARTITION BY department_id ORDER BY salary DESC) AS rn
FROM employees;
-- 4
WITH ranked AS (
SELECT
name,
department_id,
salary,
ROW_NUMBER() OVER (PARTITION BY department_id ORDER BY salary DESC) AS rn
FROM employees
)
SELECT name, department_id, salary
FROM ranked
WHERE rn = 1;
-- 5
SELECT
name,
salary,
LAG(salary) OVER (PARTITION BY department_id ORDER BY salary) AS prev_salary
FROM employees;
-- 6
SELECT
name,
salary,
SUM(salary) OVER (ORDER BY salary) AS running_total
FROM employees;
-- 7
SELECT
name,
department_id,
salary,
AVG(salary) OVER (
PARTITION BY department_id
ORDER BY salary
ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING
) AS moving_avg
FROM employees;
-- 8
WITH with_prev AS (
SELECT
name,
department_id,
salary,
LAG(salary) OVER (PARTITION BY department_id ORDER BY salary) AS prev_salary
FROM employees
)
SELECT name, department_id, salary
FROM with_prev
WHERE salary > prev_salary;Ты готова к шестому этапу, если можешь без подсказки объяснить:
GROUP BY;PARTITION BY и ORDER BY в оконной функции;ROW_NUMBER() отличается от RANK() и DENSE_RANK();LEAD() и LAG();WHERE.