Оконные функции в SQL: что это и зачем они нужны
Рассказываем, как производить агрегатные вычисления и не терять исходные строки.
Иллюстрация: Оля Ежак для Skillbox Media
Оконные функции в SQL (от англ. window function) — это особый класс функций, позволяющий производить вычисления по определённым группам строк в базе данных. При этом они не объединяют строки в одну, а возвращают столько же, сколько было на входе.
Эти функции удобно использовать для отчётов, анализа данных, финансового моделирования и других задач, где нужно видеть результаты в контексте исходных данных. С их помощью можно выполнять различные вычисления для набора строк: подсчитывать среднее число, сумму и многие другое.
Всё самое важное об оконных функциях в SQL:
ЭКСПЕРТ
Елена Грачёва
Разработчик-фрилансер. Занимается iOS-разработкой с 2011 года. Вела курс по Swift в Астраханском государственном университете. Программный директор и эксперт по мобильной разработке в Skillbox.
С 2016 года также занималась управлением проектами в мобильной разработке.
Таблица для примеров
Создадим таблицу sport_sales, содержащую продажи магазина спортивных товаров по отделам. Включим в неё следующие поля:
- id — уникальный идентификатор каждой записи;
- sale_date — дата продаж по каждому отделу;
- department — наименование отдела;
- sales — количество продаж по каждому отделу за день.
Таблицу можно создать с помощью следующего запроса:
А для заполнения таблицы данными используем следующий код:
В результате получим вот такую таблицу базы данных, будем использовать её:
id | sale_date | department | sales |
---|---|---|---|
1 | 2024-02-01 | Shoes | 6 |
2 | 2024-02-01 | Clothing | 5 |
3 | 2024-02-01 | Equipment | 10 |
4 | 2024-02-02 | Clothing | 6 |
5 | 2024-02-02 | Equipment | 4 |
6 | 2024-02-02 | Shoes | 4 |
7 | 2024-02-03 | Shoes | 8 |
8 | 2024-02-03 | Equipment | 10 |
9 | 2024-02-03 | Clothing | 4 |
Синтаксис оконных функций
🛠️ Технические детали
Синтаксис оконных функций может различаться в разных реализациях SQL. Некоторые функции могут называться иначе или их может не быть вовсе. Запросы в этой статье показаны на примере PL\SQL — диалекта, который используется в базах данных под управлением Oracle DB.
В общем виде синтаксис оконных функций выглядит так:
Детально разберём структуру:
- <window_function_name> — имя оконной функции, например ROW_NUMBER(), RANK(), DENSE_RANK() и другие.
- (arguments) — аргументы функции, например имя столбца, по которому происходит расчёт.
- OVER() — ключевое слово, которое определяет, как оконная функция будет применятся к набору данных.
- [Partitioning] — определяет критерий, по которому строки делятся на подгруппы. Это необязательный компонент.
- [Ordering] — указывает порядок строк в каждой подгруппе, что важно для таких функций, как RANK(), ROW_NUMBER() и других. Этот элемент тоже необязателен.
- [Frame] — задаёт «фрейм» строк относительно текущей. Как правило, используется с ключевыми словами ROWS или RANGE.
Рассмотрим подробнее эти компоненты.
PARTITION BY
Параметр, который позволяет разделить данные на группы, внутри которых будет применяться оконная функция. PARTITION BY действует аналогично GROUP BY в агрегатных функциях, но в оконных функциях результат возвращается для каждой строки входных данных.
Например, рассмотрим, как работает запрос с группировкой GROUP BY, считающий суммарные продажи за период по каждому отделу:
В результате получим:
department | sum_sales |
---|---|
Clothing | 15 |
Equipment | 24 |
Shoes | 18 |
Этот запрос выводит в столбце sum_sales сумму продаж по каждому отделу.
Теперь используем оконную функцию:
Результат будет таким:
id | sale_date | department | sales | sum_sales |
---|---|---|---|---|
2 | 2024-02-01 | Clothing | 5 | 15 |
4 | 2024-02-02 | Clothing | 6 | 15 |
9 | 2024-02-03 | Clothing | 4 | 15 |
3 | 2024-02-01 | Equipment | 10 | 24 |
5 | 2024-02-02 | Equipment | 4 | 24 |
8 | 2024-02-03 | Equipment | 10 | 24 |
1 | 2024-02-01 | Shoes | 6 | 18 |
6 | 2024-02-02 | Shoes | 4 | 18 |
7 | 2024-02-03 | Shoes | 8 | 18 |
Этот запрос выводит продажи каждого отдела в столбце sales и суммарные продажи в столбце sum_sales. Все строки из исходной таблицы сохраняются, причём они сгруппированы по отделам.
ORDER BY
Ключевое слово ORDER BY определяет, как данные будут упорядочены при применении оконной функции.
Для определения порядка строк используются ключевые слова ASC и DESC:
- ASC — сортировка по возрастанию. Это значение по умолчанию. Упорядочивание от наименьшего значения к наибольшему.
- DESC — сортировка по убыванию. Упорядочивание от наибольшего значения к наименьшему.
ORDER BY может включать несколько колонок. Например, ORDER BY sale_date ASC, sale DESC сначала упорядочивает данные отделов по датам в порядке возрастания, а затем по продажам в порядке убывания.
Составим запрос с использованием функции RANK(), которая присваивает ранг каждой строке в зависимости от значения в столбце sale. Отделу с наибольшими продажами за день присваивается ранг 1, с наименьшими — ранг 3. Значения столбца sale_date сгруппируем по возрастанию, а sales — по убыванию:
Вот какую таблицу получим в итоге:
id | sale_date | department | sales | rank |
---|---|---|---|---|
3 | 2024-02-01 | Equipment | 10 | 1 |
1 | 2024-02-01 | Shoes | 6 | 2 |
2 | 2024-02-01 | Clothing | 5 | 3 |
4 | 2024-02-02 | Clothing | 6 | 1 |
5 | 2024-02-02 | Equipment | 4 | 2 |
6 | 2024-02-02 | Shoes | 4 | 2 |
8 | 2024-02-03 | Equipment | 10 | 1 |
7 | 2024-02-03 | Shoes | 8 | 2 |
9 | 2024-02-03 | Clothing | 4 | 3 |
ROWS или RANGE
Ключевые слова ROWS и RANGE определяют, какие строки окна будут учитываться при выполнении расчётов. Они устанавливают «фрейм» окна — набор строк относительно текущей строки, который будет использоваться для вычислений.
ROWS задаёт границы окна в пределах определённого количества строк до или после текущей строки. Например, ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING указывает, что окно включает строку перед текущей, текущую и строку после. На их основании будет вычисляться функция.
Напишем запрос, который суммирует продажи в текущей, предыдущей и последующей строках и размещает полученные суммы в столбце sum_sales:
В результате получим:
id | sale_date | department | sales | sum_sales |
---|---|---|---|---|
1 | 2024-02-01 | Shoes | 6 | 11 |
2 | 2024-02-01 | Clothing | 5 | 21 |
3 | 2024-02-01 | Equipment | 10 | 21 |
4 | 2024-02-02 | Clothing | 6 | 20 |
5 | 2024-02-02 | Equipment | 4 | 14 |
6 | 2024-02-02 | Shoes | 4 | 16 |
7 | 2024-02-03 | Shoes | 8 | 22 |
8 | 2024-02-03 | Equipment | 10 | 22 |
9 | 2024-02-03 | Clothing | 4 | 14 |
В таблице выше данные сгруппированы по датам, и в столбце sum_sales считаются так:
- Когда текущая — первая строка, суммируются продажи из первой и второй строки.
- Когда текущая — вторая строка, суммируются продажи из первой, второй и третьей строки и так далее.
RANGE устанавливает границы окна на основе значений заданного столбца, а не на физическом расположении строк, как это делает ROWS. Он группирует вместе строки с одинаковыми или близкими значениями в указанном столбце сортировки.
То есть, когда вы используете RANGE вместе с ORDER BY, SQL обрабатывает окно для каждой строки, включая в него всё с соответствующими или сопоставимыми значениями столбца, указанного в ORDER BY. Это значит, что, если есть несколько строк с одинаковыми значениями в сортируемом столбце (например, одинаковые даты или отделы), они все будут включены в окно.
Например, рассчитаем кумулятивную (накапливающуюся) сумму продаж по датам. Для этого напишем следующий запрос:
В этом примере для каждой даты будут суммироваться все значения sale начиная от самой первой записи до текущей даты, включая все строки с датами, равными дате текущей.
В результате получим:
id | sale_date | department | sales | cumulative_sales |
---|---|---|---|---|
1 | 2024-02-01 | Shoes | 6 | 21 |
2 | 2024-02-01 | Clothing | 5 | 21 |
3 | 2024-02-01 | Equipment | 10 | 21 |
4 | 2024-02-02 | Clothing | 6 | 35 |
5 | 2024-02-02 | Equipment | 4 | 35 |
6 | 2024-02-02 | Shoes | 4 | 35 |
7 | 2024-02-03 | Shoes | 8 | 57 |
8 | 2024-02-03 | Equipment | 10 | 57 |
9 | 2024-02-03 | Clothing | 4 | 57 |
RANGE может быть полезен, когда нужно производить вычисления на группах данных, которые логически связаны (например, записи с теми же датами или близкими ценами).
Классы оконных функций
В SQL есть множество оконных функций, которые упрощают работу с данными. С их помощью можно быстро проводить сложные вычисления по группам строк, связанных с текущей. Основные оконные функции можно разделить на три большие группы.
Агрегатные функции
Агрегатные функции — позволяют выполнять суммирование, подсчёт, нахождение максимумов и минимумов и средних значений. Они производят вычисления над набором строк в окне и возвращают одно результирующее значение:
- SUM(column_name) — возвращает сумму выбранных значений;
- AVG(column_name) — вычисляет среднее значение;
- MAX(column_name) и MIN(column_name) — возвращают максимальное и минимальное значение соответственно;
- COUNT(column_name) — находит количество значений.
В качестве примера применим агрегатные функции к продажам в отделах для каждой даты так, чтобы каждая строка в окне сохранила исходные значения:
В результате получим следующую таблицу:
id | sale_ date | department | sale | avg_s | sum_s | max_s | min_s | count_s |
---|---|---|---|---|---|---|---|---|
1 | 2024-02-01 | Shoes | 6 | 7.0 | 21 | 10 | 5 | 3 |
2 | 2024-02-01 | Clothing | 5 | 7.0 | 21 | 10 | 5 | 3 |
3 | 2024-02-01 | Equipment | 10 | 7.0 | 21 | 10 | 5 | 3 |
4 | 2024-02-02 | Clothing | 6 | 4.7 | 14 | 6 | 4 | 3 |
5 | 2024-02-02 | Equipment | 4 | 4.7 | 14 | 6 | 4 | 3 |
6 | 2024-02-02 | Shoes | 4 | 4.7 | 14 | 6 | 4 | 3 |
7 | 2024-02-03 | Shoes | 8 | 7.3 | 22 | 10 | 4 | 3 |
8 | 2024-02-03 | Equipment | 10 | 7.3 | 22 | 10 | 4 | 3 |
9 | 2024-02-03 | Clothing | 4 | 7.3 | 22 | 10 | 4 | 3 |
Важно отметить, что агрегатные функции чувствительны к NULL. К примеру, AVG() и SUM() игнорируют такие значения, а COUNT() — считает все строки, даже с NULL.
Функции ранжирования
Функции ранжирования предоставляют способы оценки позиции каждой строки среди своих соседей в определённом порядке. Они полезны, когда необходимо определить ранги в наборе данных.
Основные функции ранжирования:
Функция RANK(). Присваивает ранг каждой строке в разделе окна. Если строки одинаковы, они получают одинаковый ранг, но следующий ранг будет увеличен на количество строк с одинаковым рангом.
В примере ниже SQL-запрос присваивает строкам ранги в порядке возрастания числа продаж. Если количество продаж одинаковое (id 5 и 6), то будет присвоен ранг 1, а следующей по порядку строке (id 4) — ранг 3:
Так выглядит таблица с результатом запроса:
id | sale_date | department | sales | rank_s |
---|---|---|---|---|
2 | 2024-02-01 | Clothing | 5 | 1 |
1 | 2024-02-01 | Shoes | 6 | 2 |
3 | 2024-02-01 | Equipment | 10 | 3 |
5 | 2024-02-02 | Equipment | 4 | 1 |
6 | 2024-02-02 | Shoes | 4 | 1 |
4 | 2024-02-02 | Clothing | 6 | 3 |
9 | 2024-02-03 | Clothing | 4 | 1 |
7 | 2024-02-03 | Shoes | 8 | 2 |
8 | 2024-02-03 | Equipment | 10 | 3 |
Функция DENSE_RANK(). Похожа на RANK(), но работает «плотнее». Это значит, что следующий ранг увеличивается на 1, независимо от количества строк с одинаковым рангом.
Напишем простой запрос с использованием функции DENSE_RANK():
В этом примере SQL-запрос также присваивает строкам ранг в порядке возрастания количества продаж. Если количество продаж одинаково (id 5 и 6), то будет присвоен ранг 1, а следующей по порядку строке (id 4) — ранг 2. Вот как это отражается в таблице:
id | sale_date | department | sales | dn_rank_s |
---|---|---|---|---|
2 | 2024-02-01 | Clothing | 5 | 1 |
1 | 2024-02-01 | Shoes | 6 | 2 |
3 | 2024-02-01 | Equipment | 10 | 3 |
5 | 2024-02-02 | Equipment | 4 | 1 |
6 | 2024-02-02 | Shoes | 4 | 1 |
4 | 2024-02-02 | Clothing | 6 | 2 |
9 | 2024-02-03 | Clothing | 4 | 1 |
7 | 2024-02-03 | Shoes | 8 | 2 |
8 | 2024-02-03 | Equipment | 10 | 3 |
Функция ROW_NUMBER(). Присваивает уникальный порядковый номер каждой строке в разделе окна.
SQL-запрос выглядит следующим образом:
В результате все данные рассортированы по датам продаж и отмечены уникальными номерами:
id | sale_date | department | sales | row_n |
---|---|---|---|---|
2 | 2024-02-01 | Clothing | 5 | 1 |
3 | 2024-02-01 | Equipment | 10 | 2 |
1 | 2024-02-01 | Shoes | 6 | 3 |
4 | 2024-02-02 | Clothing | 6 | 1 |
5 | 2024-02-02 | Equipment | 4 | 2 |
6 | 2024-02-02 | Shoes | 4 | 3 |
9 | 2024-02-03 | Clothing | 4 | 1 |
8 | 2024-02-03 | Equipment | 10 | 2 |
7 | 2024-02-03 | Shoes | 8 | 3 |
Функции смещения
Функции смещения позволяют выполнять операции над текущей строкой, в зависимости от других строк в окне. Они бывают полезны, когда нужно анализировать последовательности или временные ряды данных.
Основные функции смещения:
Функция LEAD(). Позволяет заглянуть вперёд на определённое количество строк от текущей и получить значение из столбца в этих строках.
К примеру, запрос может выглядеть следующим образом:
В результате для каждого отдела запрос возвращает продажи текущего дня (sales) и следующего (next_day_s):
id | sale_date | department | sales | next_day_s |
---|---|---|---|---|
2 | 2024-02-01 | Clothing | 5 | 6 |
4 | 2024-02-02 | Clothing | 6 | 4 |
9 | 2024-02-03 | Clothing | 4 | None |
3 | 2024-02-01 | Equipment | 10 | 4 |
5 | 2024-02-02 | Equipment | 4 | 10 |
8 | 2024-02-03 | Equipment | 10 | None |
1 | 2024-02-01 | Shoes | 6 | 4 |
6 | 2024-02-02 | Shoes | 4 | 8 |
7 | 2024-02-03 | Shoes | 8 | None |
Функция LAG(). Похожа на LEAD(), но, вместо того чтобы «заглядывать вперёд», эта функция «смотрит назад» на определённое количество строк.
Здесь в каждой строке окна столбец sales покажет продажи текущего, а столбец last_day_s — продажи предыдущего дня для каждого отдела:
id | sale_date | department | sales | last_day_s |
---|---|---|---|---|
2 | 2024-02-01 | Clothing | 5 | None |
4 | 2024-02-02 | Clothing | 6 | 5 |
9 | 2024-02-03 | Clothing | 4 | 6 |
3 | 2024-02-01 | Equipment | 10 | None |
5 | 2024-02-02 | Equipment | 4 | 10 |
8 | 2024-02-03 | Equipment | 10 | 4 |
1 | 2024-02-01 | Shoes | 6 | None |
6 | 2024-02-02 | Shoes | 4 | 6 |
7 | 2024-02-03 | Shoes | 8 | 4 |
Функции FIRST_VALUE() и LAST_VALUE(). Эти функции возвращают первое и последнее значение столбца в окне соответственно.
С помощью вот такого запроса можно вывести продажи текущего дня и продажи за первое и третье число для каждого отдела:
Результат работы запроса выглядит так:
id | sale_date | department | sales | first_sale | last_sale |
---|---|---|---|---|---|
2 | 2024-02-01 | Clothing | 5 | 5 | 4 |
4 | 2024-02-02 | Clothing | 6 | 5 | 4 |
9 | 2024-02-03 | Clothing | 4 | 5 | 4 |
3 | 2024-02-01 | Equipment | 10 | 10 | 10 |
5 | 2024-02-02 | Equipment | 4 | 10 | 10 |
8 | 2024-02-03 | Equipment | 10 | 10 | 10 |
1 | 2024-02-01 | Shoes | 6 | 6 | 8 |
6 | 2024-02-02 | Shoes | 4 | 6 | 8 |
7 | 2024-02-03 | Shoes | 8 | 6 | 8 |
В заключение
Оконные функции делают SQL-запросы более гибкими и мощными. Они помогают избежать потерь информации, которая содержится в каждой строке исходных данных. А ещё выполняют различные расчёты с таблицами — например, ранжирование, суммирование или выведение статистики.
Оконные функции оперируют в контексте «окна» — подмножества таблицы и добавляют новую информацию в отдельные столбцы. Они позволяют избежать дополнительных запросов, что упрощает работу с базами данных и делает SQL-запросы более читаемыми.
Больше интересного про код — в нашем телеграм-канале. Подписывайтесь!