Оконные функции (Window Functions)¶
Оконные функции выполняют вычисления над набором строк, связанных с текущей строкой, но в отличие от агрегатных функций, они не группируют строки в один результат.
📊 Синтаксис оконных функций¶
функция_окна(аргументы) OVER (
[PARTITION BY столбец]
[ORDER BY столбец]
[ROWS/RANGE между_строки]
)
- PARTITION BY — разбивает результат на группы (как GROUP BY, но без агрегации)
- ORDER BY — сортирует строки внутри окна
- ROWS/RANGE — определяет диапазон строк для вычисления
🔢 Нумерация и ранжирование¶
ROW_NUMBER()¶
Присваивает уникальный порядковый номер каждой строке.
Результат:
| name | salary | dept_id | rank |
|---|---|---|---|
| Директор | 150000 | 10 | 1 |
| Менеджер | 120000 | 20 | 2 |
| Аналитик | 90000 | 20 | 3 |
RANK() и DENSE_RANK()¶
- RANK() — присваивает одинаковый ранг равным значениям, следующий ранг пропускается
- DENSE_RANK() — присваивает одинаковый ранг равным значениям, следующий ранг не пропускается
SELECT
name,
salary,
RANK() OVER (ORDER BY salary DESC) AS rank,
DENSE_RANK() OVER (ORDER BY salary DESC) AS dense_rank
FROM employees;
Пример с одинаковыми зарплатами:
| name | salary | rank | dense_rank |
|---|---|---|---|
| A | 100000 | 1 | 1 |
| B | 100000 | 1 | 1 |
| C | 90000 | 3 | 2 |
| D | 80000 | 4 | 3 |
NTILE(n)¶
Разбивает строки на n групп примерно одинакового размера.
-- Разделить сотрудников на 4 квартеля по зарплате
SELECT
name,
salary,
NTILE(4) OVER (ORDER BY salary DESC) AS quartile
FROM employees;
📈 Агрегатные оконные функции¶
Агрегатные функции можно использовать как оконные.
Накопительный итог (Running Total)¶
Скользящее среднее¶
SELECT
order_date,
amount,
AVG(amount) OVER (
ORDER BY order_date
ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
) AS moving_avg_7days
FROM orders;
Агрегаты с PARTITION BY¶
SELECT
name,
salary,
dept_id,
AVG(salary) OVER (PARTITION BY dept_id) AS avg_dept_salary,
salary - AVG(salary) OVER (PARTITION BY dept_id) AS diff_from_avg
FROM employees;
🔀 Функции смещения¶
LAG() и LEAD()¶
Доступ к предыдущим и следующим строкам.
-- Сравнение с предыдущим месяцем
SELECT
month,
sales,
LAG(sales, 1) OVER (ORDER BY month) AS prev_month_sales,
sales - LAG(sales, 1) OVER (ORDER BY month) AS change
FROM monthly_sales;
-- Прогноз на следующий период
SELECT
month,
sales,
LEAD(sales, 1) OVER (ORDER BY month) AS next_month_forecast
FROM monthly_sales;
FIRST_VALUE() и LAST_VALUE()¶
Получение первого и последнего значения в окне.
SELECT
name,
hire_date,
dept_id,
FIRST_VALUE(name) OVER (
PARTITION BY dept_id
ORDER BY hire_date
) AS first_hire_in_dept,
LAST_VALUE(name) OVER (
PARTITION BY dept_id
ORDER BY hire_date
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
) AS last_hire_in_dept
FROM employees;
💡 Практические примеры¶
Пример 1: Топ-3 сотрудника в каждом департаменте¶
SELECT name, dept_id, salary, rank_in_dept
FROM (
SELECT
name,
dept_id,
salary,
DENSE_RANK() OVER (
PARTITION BY dept_id
ORDER BY salary DESC
) AS rank_in_dept
FROM employees
) AS ranked
WHERE rank_in_dept <= 3;
Пример 2: Анализ изменений зарплаты¶
SELECT
name,
year,
salary,
LAG(salary) OVER (PARTITION BY name ORDER BY year) AS prev_salary,
salary - LAG(salary) OVER (PARTITION BY name ORDER BY year) AS change,
ROUND(
100.0 * (salary - LAG(salary) OVER (PARTITION BY name ORDER BY year))
/ LAG(salary) OVER (PARTITION BY name ORDER BY year),
2
) AS change_percent
FROM employee_salaries
ORDER BY name, year;
Пример 3: Накопительные продажи по месяцам¶
SELECT
DATE_TRUNC('month', order_date) AS month,
SUM(total_amount) AS monthly_sales,
SUM(SUM(total_amount)) OVER (ORDER BY DATE_TRUNC('month', order_date)) AS running_total,
AVG(SUM(total_amount)) OVER (
ORDER BY DATE_TRUNC('month', order_date)
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW
) AS three_month_avg
FROM orders
GROUP BY DATE_TRUNC('month', order_date)
ORDER BY month;
Пример 4: Выявление аномалий¶
-- Найти заказы, превышающие средний более чем на 2 стандартных отклонения
SELECT order_id, customer_id, total_amount, avg_amount, std_dev, z_score
FROM (
SELECT
order_id,
customer_id,
total_amount,
AVG(total_amount) OVER () AS avg_amount,
STDDEV(total_amount) OVER () AS std_dev,
(total_amount - AVG(total_amount) OVER ())
/ NULLIF(STDDEV(total_amount) OVER (), 0) AS z_score
FROM orders
) AS stats
WHERE ABS(z_score) > 2;
Пример 5: Сравнение с лучшим показателем¶
SELECT
salesperson,
region,
sales,
MAX(sales) OVER (PARTITION BY region) AS best_in_region,
ROUND(
100.0 * sales / MAX(sales) OVER (PARTITION BY region),
1
) AS percent_of_best
FROM sales_data;
⚠️ Частые ошибки¶
| Ошибка | Проблема | Решение |
|---|---|---|
| Забытый ORDER BY в LAST_VALUE | Возвращает только до текущей строки | Добавьте ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING |
| Неправильное понимание RANGE vs ROWS | Разные результаты | Используйте ROWS для подсчёта строк, RANGE для значений |
| Производительность на больших данных | Медленное выполнение | Индексируйте столбцы в PARTITION BY и ORDER BY |
| Путаница с NULL | Неожиданные результаты | Используйте COALESCE для обработки NULL |
🎯 Окно по умолчанию¶
Если не указано иначе, окно включает: - Все строки партиции (без PARTITION BY — все строки результата) - От первой строки до текущей (при ORDER BY) - Или все строки (без ORDER BY)
-- Эквивалентны:
SUM(salary) OVER ()
SUM(salary) OVER (ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING)
📊 Сравнение функций ранжирования¶
| Функция | Одинаковые значения | Следующий ранг | Пример |
|---|---|---|---|
| ROW_NUMBER | Уникальные номера | +1 | 1, 2, 3, 4 |
| RANK | Одинаковый ранг | Пропуск | 1, 1, 3, 4 |
| DENSE_RANK | Одинаковый ранг | Без пропуска | 1, 1, 2, 3 |
| NTILE(n) | Распределение по группам | - | 1, 1, 2, 2 (для n=2) |
➡️ Что дальше?¶
Изучите CTE для создания читаемых сложных запросов!