Оконные функции SQL: агрегация
Это фрагмент моего курса Оконные функции SQL с пошаговым изложением, наглядными иллюстрациями и практическими упражнениями.
На предыдущих уроках мы разобрали оконные функции ранжирования и смещения.
Агрегация — это когда мы считаем суммарные или средние показатели (агрегаты). Например, среднюю зарплату по каждому региону или количество золотых медалей у каждой страны в зачете Олимпийских игр.
Мы будем агрегировать данные по сотрудникам из таблицы employees
:
┌────┬──────────┬────────┬────────────┬────────┐
│ id │ name │ city │ department │ salary │
├────┼──────────┼────────┼────────────┼────────┤
│ 11 │ Дарья │ Самара │ hr │ 70 │
│ 12 │ Борис │ Самара │ hr │ 78 │
│ 21 │ Елена │ Самара │ it │ 84 │
│ 22 │ Ксения │ Москва │ it │ 90 │
│ 23 │ Леонид │ Самара │ it │ 104 │
│ 24 │ Марина │ Москва │ it │ 104 │
│ 25 │ Иван │ Москва │ it │ 120 │
│ 31 │ Вероника │ Москва │ sales │ 96 │
│ 32 │ Григорий │ Самара │ sales │ 96 │
│ 33 │ Анна │ Москва │ sales │ 100 │
└────┴──────────┴────────┴────────────┴────────┘
Содержание:
Агрегат по секции
У каждого департамента есть фонд оплаты труда — денежная сумма, которая ежемесячно уходит на выплату зарплат сотрудникам. Посмотрим, какой процент от этого фонда составляет зарплата каждого сотрудника:
Столбец fund
показывает фонд оплаты труда отдела, а perc
— долю зарплаты сотрудника от этого фонда. Видно, что в HR и продажах все более-менее ровно, а у айтишников есть заметный разброс зарплат.
Как перейти от «было» к «стало»?
Отсортируем таблицу по департаментам:
select
name, department, salary,
null as fund, null as perc
from employees
order by department, salary, id;
Теперь пройдем от первой строчки до последней. На каждом шаге будем считать:
fund
— сумму зарплат по департаменту в целом;perc
— долю зарплаты сотрудника от этой суммы.
и так далее...
Одной гифкой:
Окно состоит из секций по департаментам. При этом порядок записей в секции неважен: мы считаем сумму значений salary
, а она не зависит от порядка.
window w as (
partition by department
)
Для расчета fund
подойдет обычная функция sum()
— ее можно использовать поверх окна. А perc
посчитаем как salary / fund
:
select
name, department, salary,
sum(salary) over w as fund,
round(salary * 100.0 / sum(salary) over w) as perc
from employees
window w as (partition by department)
order by department, salary, id;
Функция sum()
работает без неожиданностей — считает сумму значений по всей секции, которой принадлежит текущая строка.
✎ Задачка: Фонд оплаты труда по городу (+ еще одна)
Только практика превращает абстрактные знания в навыки. Поэтому я рекомендую не просто читать эту статью, а проходить курс или книгу — в них достаточно упражнений, чтобы уверенно освоить «окошки».
Если вас пока устраивает одна теория — продолжим.
Фильтрация и порядок выполнения
Вернемся к запросу, который считал фонд оплаты труда по департаменту:
select
name, department, salary,
sum(salary) over w as fund
from employees
window w as (partition by department)
order by department, salary, id;
Допустим, мы хотим оставить в отчете только самарских сотрудников. Добавим фильтр:
select
name, salary,
sum(salary) over w as fund
from employees
where city = 'Самара'
window w as (partition by department)
order by department, salary, id;
Фильтр сработал. Вот только значения fund
отличаются от ожидаемых:
┌──────────┬────────┬──────┐
│ name │ salary │ fund │
├──────────┼────────┼──────┤
│ Дарья │ 70 │ 148 │
│ Борис │ 78 │ 148 │
│ Елена │ 84 │ 502 │
│ Леонид │ 104 │ 502 │
│ Григорий │ 96 │ 292 │
└──────────┴────────┴──────┘
┌──────────┬────────┬──────┐
│ name │ salary │ fund │
├──────────┼────────┼──────┤
│ Дарья │ 70 │ 148 │
│ Борис │ 78 │ 148 │
│ Елена │ 84 │ 188 │
│ Леонид │ 104 │ 188 │
│ Григорий │ 96 │ 96 │
└──────────┴────────┴──────┘
Все дело в порядке выполнения операций. Вот в какой последовательности действует движок, когда выполняет запрос:
- Взять нужные таблицы (
from
) и соединить их при необходимости (join
). - Отфильтровать строки (
where
). - Сгруппировать строки (
group by
). - Отфильтровать результат группировки (
having
). - Взять конкретные столбцы из результата (
select
). - Рассчитать значения оконных функций (
function() over window
). - Отсортировать то, что получилось (
order by
).
Таким образом, окна отрабатывают предпоследним шагом, уже после фильтрации и группировки результатов. Поэтому в нашем запросе fund
отражает не сумму всех зарплат по департаменту, а сумму только по самарским сотрудникам.
Решение — использовать подзапрос с окном и фильтровать его в основном запросе:
with emp as (
select
name, city, salary,
sum(salary) over w as fund
from employees
window w as (partition by department)
order by department, salary, id
)
select name, salary, fund
from emp where city = 'Самара';
Описание окна
До сих пор мы описывали окно в блоке window
и ссылались на него в выражении over
:
select
name, department, salary,
count(*) over w as emp_count,
sum(salary) over w as fund
from employees
window w as (partition by department)
order by department, salary, id;
Это не единственный способ. SQL разрешает вообще не использовать window
и описывать окно прямо внутри over
:
select
name, department, salary,
count(*) over (partition by department) as emp_count,
sum(salary) over (partition by department) as fund
from employees
order by department, salary, id;
Мне больше нравится вариант с window
— его легче читать, и можно явно переиспользовать окно. Но в документации часто встречается определение окна внутри over
, поэтому не удивляйтесь, когда увидите его.
Кстати, определение окна может быть пустым:
select
name, department, salary,
count(*) over () as emp_count,
sum(salary) over () as fund
from employees
order by department, salary, id;
Такое окно включает все строки, так что emp_count
покажет общее количество сотрудников, а fund
— общий фонд оплаты труда по всем записям employees
.
✎ Задачка: Порядок выполнения (+ еще одна)
Только практика превращает абстрактные знания в навыки. Поэтому я рекомендую не просто читать эту статью, а проходить курс или книгу — в них достаточно упражнений, чтобы уверенно освоить «окошки».
Если вас пока устраивает одна теория — продолжим.
Функции агрегации
Оконные функции агрегации:
Функция | Описание |
---|---|
min(value) | минимальное value среди строк секции окна |
max(value) | максимальное value |
count(value) | количество value , не равных null |
avg(value) | среднее значение по всем value |
sum(value) | сумма значений value |
group_concat(value, separator) | соединение значений value через разделитель separator (SQLite и MySQL) |
string_agg(value, separator) | аналог group_concat() в PostgreSQL и MS SQL |
Так держать
Мы разобрались, как считать фиксированные агрегаты в окнах. На следующем уроке займемся скользящими агрегатами!
Записаться на курс или купить книгу (скоро)
★ Подписывайтесь на новые заметки.