Бортовой журнал
Бортовой журнал

Когда работаешь с данными, часто хочется получить не просто одну сумму или среднее значение, а увидеть, как отдельные строки связаны между собой: кто первый в списке, какая продажа следующая, сколько процентов клиентов сделали заказ дороже среднего. Решить такие задачи можно, конечно, вложенными запросами — но это долго и неудобно.

Есть способ проще: оконные функции. Они позволяют строить гибкие расчёты прямо в SQL, не теряя сами данные из виду. В этой статье смотрим, как работают оконные функции, какие они бывают и как использовать их на практике.

Что такое оконные функции 

Оконные функции в SQL — это способ делать расчеты по набору строк, не склеивая их в одну, как это происходит при использовании GROUP BY. Они работают «поверх» данных: вычисляют сумму, среднее, порядковый номер строки, процент от общего и другие штуки — и все это без потери самих строк. Каждая строка остается на месте, просто к ней добавляется результат вычисления. Например, можно узнать, сколько заказов сделал клиент до текущего заказа, или какое среднее значение у зарплат в отделе, не сгруппировав все строки в одну.

Главная особенность оконных функций в том, что они считают результат не по всей таблице сразу, а по отдельным «окнам» — группам строк, которые можно настроить через PARTITION BY и отсортировать с помощью ORDER BY. За все это отвечает оператор OVER. Именно он определяет границы окна: что попадает в расчет, а что нет.

Для чего нужны оконные функции

Оконные функции нужны тогда, когда обычные агрегаты вроде SUM() или AVG() уже не справляются. Они незаменимы, если нужно:

  • посчитать сумму или среднее значение для группы строк, но показать их рядом с каждой строкой;
  • пронумеровать строки в нужном порядке (например, присвоить каждой покупке номер по дате);
  • сравнить значения между разными строками (например, найти разницу между текущей продажей и предыдущей);
  • построить скользящие итоги, ранжирование, кумулятивные суммы или скользящее среднее;
  • проанализировать временные ряды, например, рассчитывать скользящее среднее цен акций за неделю или месяц;
  • отранжировать данные по разным признакам: по сумме покупок, дате создания заказа, алфавиту;
  • разделить данные на группы и найти статистику для каждой группы;
  • отфильтровать строки по условиям, основанным на результатах расчетов внутри окна — например, оставить только те записи, где продажи выше среднего за неделю.

Без оконных функций все это тоже можно сделать, но запросы будут громоздкими. С ними же код получается короче, понятнее и чаще всего быстрее выполняется.

Синтаксис оконных функций 

Оконные функции в SQL всегда строятся по одному принципу: сначала указывается сама функция (например, SUM, AVG, ROW_NUMBER), потом через OVER() задаются условия, по которым эта функция будет работать.

Общий шаблон записи выглядит так:

<название функции>(аргументы) OVER ([PARTITION BY ...] [ORDER BY ...] [frame])

Подробнее:

  • <название функции> — имя оконной функции, например ROW_NUMBER(), RANK(), SUM() и другие;
  • (аргументы) — это могут быть столбцы или выражения, на основе которых будет идти расчет;
  • OVER() — самое важное. Указывает, что это не обычная агрегация, а расчет в пределах окна;
  • PARTITION BY — разбивает строки на группы. В каждой группе функция будет работать отдельно;
  • ORDER BY — задает порядок строк внутри группы, что особенно важно для нумерации (ROW_NUMBER, RANK);
  • frame (ROWS или RANGE) — уточняет, какие именно строки относительно текущей участвуют в расчетах.

Элементы PARTITION BY, ORDER BY и frame внутри OVER() можно использовать вместе или по отдельности. Все они необязательные, но иногда без них результат будет не тот, который нужен.

PARTITION BY

PARTITION BY нужен, чтобы разбить таблицу на независимые части — партиции. Оконная функция будет работать отдельно внутри каждой такой группы.

Если вы хотите посчитать сумму продаж в каждом отделе, сохраняя все строки, функция будет выглядеть так:

SELECT

  department,

  sale_date,

  sales,

  SUM(sales) OVER (PARTITION BY department) AS total_sales

FROM sport_sales;

Здесь каждая строка отдела получит сумму всех его продаж, а сами строки никуда не исчезнут.

ORDER BY

ORDER BY внутри OVER() определяет порядок строк для расчётов. Без него функции, которые зависят от порядка (ROW_NUMBER, RANK, LEAD, LAG), не будут работать как надо.

Например, пронумеровать покупки по дате продажи можно следующим образом:

SELECT

  sale_date,

  department,

  sales,

  ROW_NUMBER() OVER (ORDER BY sale_date) AS row_num

FROM sport_sales;

Если же добавить несколько полей в ORDER BY, сначала будет сортировка по первому полю, потом по второму и так далее.

ROWS и RANGE

Часто нужно контролировать, сколько строк участвует в расчетах относительно текущей. Для этого служат ROWSи RANGE. ROWS работает с физическим положением строк. RANGE работает по значениям в столбце сортировки.

Например, если вы хотите посчитать сумму продаж для текущей строки плюс строки до и после, то пропишите:

SELECT

  sale_date,

  department,

  sales,

  SUM(sales) OVER (ORDER BY sale_date ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING) AS moving_sum

FROM sport_sales;

FILTER (WHERE ...)

Иногда хочется посчитать функцию только для части строк. Для этого есть конструкция FILTER (WHERE ...) прямо в оконной функции.

Посчитать сумму только тех продаж, которые больше 100, можно так:

SELECT

  sale_date,

  department,

  sales,

  SUM(sales) FILTER (WHERE sales > 100) OVER (PARTITION BY department) AS big_sales_sum

FROM sport_sales;

Виды оконных функций 

Чтобы выбрать нужную функцию и использовать ее правильно, стоит понимать, какие вообще бывают оконные функции и в чем между ними разница:

Агрегатные функции

Агрегатные оконные функции выполняют вычисления над набором строк внутри окна, но при этом не объединяют строки в одну. Вместо этого они аккуратно добавляют результат в каждую строку. То есть, данные остаются на своих местах, а рядом появляется дополнительная информация — например, сумма всех продаж по дате или средняя зарплата по отделу.

К основным агрегатным функциям относятся:

  • SUM(column_name) — считает сумму значений в окне;
  • AVG(column_name) — вычисляет среднее значение;
  • MAX(column_name) — находит максимальное значение;
  • MIN(column_name) — находит минимальное значение;
  • COUNT(column_name) — считает количество строк.

Все эти функции позволяют проводить аналитику, не разрушая исходные данные.

Допустим, у нас есть таблица заказов, и мы хотим посчитать среднюю, суммарную, максимальную и минимальную стоимость заказа на каждую дату, а заодно узнать, сколько заказов было в день. Причем нам важно сохранить все заказы в результате.

Вот как будет выглядеть запрос:

SELECT

   order_id,

   order_date,

   category,

   amount,

   AVG(amount) OVER (PARTITION BY order_date) AS avg_amount,

   SUM(amount) OVER (PARTITION BY order_date) AS sum_amount,

   MAX(amount) OVER (PARTITION BY order_date) AS max_amount,

   MIN(amount) OVER (PARTITION BY order_date) AS min_amount,

   COUNT(amount) OVER (PARTITION BY order_date) AS count_amount

FROM orders;

А вот что мы получим:

order_id   

order_date      

category      

amount   

avg_amount   

sum_amount   

max_amount   

min_amount   

count_amount   

10

2025-03-01

Electronics

1500

1166.7

3500

1500

900

3

11

2025-03-01

Furniture

900

1166.7

3500

1500

900

3

12

2025-03-01

Electronics

1100

1166.7

3500

1500

900

3

13

2025-03-02

Furniture

800

875.0

1750

950

800

2

14

2025-03-02

Appliances

950

875.0

1750

950

800

2

15

2025-03-03

Electronics

1200

1400.0

2800

1600

1200

2

16

2025-03-03

Appliances

1600

1400.0

2800

1600

1200

2

Теперь для каждой строки видно, сколько в среднем заказывали в этот день, какая была суммарная сумма, сколько было заказов и какие минимальные и максимальные заказы были сделаны.

Функции ранжирования

Функции ранжирования в SQL помогают определить место каждой строки среди соседей внутри группы данных. Они особенно полезны, когда нужно выстроить строки в порядке убывания или возрастания какого-то показателя — например, зарплаты, количества продаж или даты.

Основные функции ранжирования:

  • RANK();
  • DENSE_RANK();
  • ROW_NUMBER().

Разберем каждую из них подробно.

RANK()

RANK() присваивает каждой строке ранг внутри группы, определённой через PARTITION BY. Если у нескольких строк есть одинаковые значения для сортировки, они получают одинаковый ранг. При этом следующий ранг пропускается на количество строк с одинаковым значением.

То есть, если два сотрудника получают одинаковую зарплату, оба будут иметь один и тот же ранг, а следующая запись получит ранг с пропуском. Например, если два человека делят первое место, следующий будет на третьем месте.

Запрос будет выглядеть так:

SELECT

   employee_id,

   department,

   salary,

   RANK() OVER (PARTITION BY department ORDER BY salary DESC) AS salary_rank

FROM employees;

Пояснения:

  • PARTITION BY department — делит сотрудников по отделам;
  • ORDER BY salary DESC — сортирует по зарплате в порядке убывания;
  • RANK() — присваивает ранг в каждом отделе по размеру зарплаты.

Пример результата:

employee_id   

department   

salary   

    salary_rank  

1

Sales

90000

1

2

Sales

85000

2

3

Sales

85000

2

4

Sales

70000

4

У второго и третьего сотрудников одинаковая зарплата, поэтому у них ранг 2. Следующий сотрудник получает ранг 4, а не 3 — потому что учитывается пропуск.

DENSE_RANK()

DENSE_RANK() работает похожим образом на RANK(), но без пропуска рангов. Если у нескольких строк одинаковое значение, следующий ранг будет просто увеличен на 1.

Пример:

SELECT

   employee_id,

   department,

   salary,

   DENSE_RANK() OVER (PARTITION BY department ORDER BY salary DESC) AS dense_salary_rank

FROM employees;

Пояснения:

  • PARTITION BY department — делит сотрудников по отделам;
  • ORDER BY salary DESC — сортирует по убыванию зарплаты;
  • DENSE_RANK() — присваивает ранги, без пропусков.

Результат:

employee_id   

department   

salary   

    dense_salary_rank   

1

Sales

90000

1

2

Sales

85000

2

3

Sales

85000

2

4

Sales

70000

3

Получается, что у сотрудников с зарплатой 85000 одинаковый ранг — 2. Следующий сотрудник получает ранг 3, без пропуска.

ROW_NUMBER()

ROW_NUMBER() присваивает каждой строке уникальный порядковый номер внутри группы. Даже если значения одинаковые, номера будут разными.

Пример:

SELECT

   employee_id,

   department,

   salary,

   ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC) AS row_num

FROM employees;

Пояснения:

  • PARTITION BY department — разбивает сотрудников по отделам;
  • ORDER BY salary DESC — сортирует их по убыванию зарплаты внутри каждого отдела;
  • ROW_NUMBER() — нумерует строки подряд.

Пример результата:

employee_id   

department   

salary   

    row_num   

1

Sales

90000

1

2

Sales

85000

2

3

Sales

85000

3

4

Sales

70000

4

Даже если зарплата одинаковая (85000 у двух сотрудников), у них разные порядковые номера — 2 и 3. Номера идут строго по порядку, без пропусков.

Функции смещения

Функции смещения в SQL помогают заглядывать в соседние строки относительно текущей. Это удобно, когда нужно сравнить текущее значение с предыдущим или следующим, вытащить первое или последнее значение в группе и вообще анализировать изменения во времени или в последовательностях данных.

К функциям смещения относятся:

  • LEAD();
  • LAG();
  • FIRST_VALUE();
  • LAST_VALUE().

Поговорим о каждой подробнее. 

LEAD()

Функция LEAD() позволяет посмотреть вперед на заданное количество строк и достать оттуда значение. По сути, она отвечает на вопрос: «Что будет следующим?». Например, с ее помощью можно сравнить текущую продажу со следующей или рассчитать разницу между днями.

Пример:

SELECT

   order_id,

   order_date,

   category,

   amount,

   LEAD(amount, 1) OVER (PARTITION BY category ORDER BY order_date) AS next_amount

FROM orders;

Пояснения:

  • PARTITION BY category — делит заказы по категориям товаров;
  • ORDER BY order_date — сортирует заказы по дате;
  • LEAD(amount, 1) — берет значение amount из следующей строки.

Пример результата:

order_id   

order_date      

;   category   

;   amount   

next_amount   

31

2024-05-01

Electronics

2500

2700

33

2024-05-02

Electronics

2700

NULL

32

2024-05-01

Furniture

1800

2000

34

2024-05-02

Furniture

2000

NULL

У заказа на 2500 в категории Electronics следующее значение — 2700. У последнего заказа в каждой категории следующего значения нет, поэтому NULL.

LAG()

Функция LAG() делает все наоборот — она позволяет заглянуть назад. Иными словами, отвечает на вопрос: «Что было перед этим?». Удобный вариант, когда нужно, например, посмотреть, сколько клиент тратил в прошлый раз, или сравнить продажи с предыдущим днём.

Пример:

SELECT

   order_id,

   order_date,

   category,

   amount,

   LAG(amount, 1) OVER (PARTITION BY category ORDER BY order_date) AS previous_amount

FROM orders;

Пояснения:

  • PARTITION BY category — делит заказы по категориям товаров;
  • ORDER BY order_date — сортирует заказы по дате;
  • LAG(amount, 1) — берет значение amount из предыдущей строки.

Результат:

order_id   

order_date      

;   category   

;   amount   

previous_amount   

31

2024-05-01

Electronics

2500

NULL

33

2024-05-02

Electronics

2700

2500

32

2024-05-01

Furniture

1800

NULL

34

2024-05-02

Furniture

2000

1800

У первой строки в каждой категории нет предыдущего значения — поэтому NULL. У остальных строк отображается значение из предыдущего заказа.

FIRST_VALUE()

Функция FIRST_VALUE() вытаскивает первое значение из окна строк, отсортированного по заданному порядку. Она отвечает на вопрос: «С чего всё начиналось?». Чаще всего ее используют, чтобы показать, с чего начался период.

Пример:

SELECT 

  order_id, 

  order_date, 

  category, 

  amount,

  FIRST_VALUE(amount) OVER (PARTITION BY category ORDER BY order_date) AS first_amount

FROM orders;

Пояснения:

  • PARTITION BY category — делит строки по категориям товаров;
  • ORDER BY order_date — сортирует по дате заказа;
  • FIRST_VALUE(amount) — для каждой строки берет первое значение amount в рамках своей группы.

Результат:

order_id   

order_date      

;   category   

;   amount   

first_amount   

31

2024-05-01

Electronics

2500

2500

33

2024-05-02

Electronics

2700

2500

32

2024-05-01

Furniture

1800

1800

34

2024-05-02

Furniture

2000

1800

В категории Electronics первой была покупка на 2500 — и это значение подтягивается ко всем строкам этой группы. То же самое применимо и для Furniture. 

LAST_VALUE()

А вот LAST_VALUE() вытаскивает последнее значение в окне. Она отвечает на вопрос: «Чем все закончилось?».

Когда нужно показать последнюю покупку клиента, последнее изменение статуса заказа или финальное значение в группе, используется именно эта функция.

Важно! Без правильной настройки окна LAST_VALUE() может вернуть не настоящее последнее значение, а текущее значение строки. Поэтому лучше всегда задавать полный фрейм.

Пример с фреймом:

SELECT

   order_id,

   order_date,

   category,

   amount,

   LAST_VALUE(amount) OVER (

     PARTITION BY category 

     ORDER BY order_date 

     ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING

   ) AS last_amount

FROM orders;

Пояснения:

  • PARTITION BY category — разбивает данные по категориям товаров, чтобы работать отдельно с каждой группой;
  • ORDER BY order_date — сортирует заказы внутри каждой категории по дате;
  • LAST_VALUE(amount) — берет значение из последней строки в отсортированном порядке для каждой группы;
  • ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING — указывает, что нужно видеть все окно целиком: от первой до последней строки. 

Результат:

order_id   

order_date      

;   category   

;   amount   

last_amount   

31

2024-05-01

Electronics

2500

2700

33

2024-05-02

Electronics

2700

2700

32

2024-05-01

Furniture

1800

2000

34

2024-05-02

Furniture

2000

2000

Теперь в каждой строке видно, каким было последнее значение для категории.

Функции аналитики

Функции аналитики в SQL нужны тогда, когда простой суммы или среднего значения уже мало. Они позволяют понять, как каждая строка соотносится с другими строками в пределах своей группы: какое место занимает, на каком процентиле находится и насколько «высоко» расположена по сравнению с остальными.

Главная особенность аналитических функций в том, что они смотрят сразу на всю группу строк, а не только на текущую. Они строят распределения, ранжируют строки и помогают быстро делать выводы по данным без сложных подзапросов.

К основным аналитическим оконным функциям относятся:

  • CUME_DIST();
  • PERCENT_RANK();
  • PERCENTILE_CONT();
  • PERCENTILE_DISC().

Посмотрим на каждую подробнее.

CUME_DIST()

Функция CUME_DIST() показывает, у какой доли строк в группе значение меньше или равное текущему. Результат выражается в виде числа от 0 до 1. По сути, это способ понять, насколько «высоко» находится строка в своем наборе данных.

Пример:

SELECT 

  product_id,

  category,

  price,

  CUME_DIST() OVER (PARTITION BY category ORDER BY price) AS cum_dist

FROM products;

Пояснения:

  • PARTITION BY category — делит товары по категориям;
  • ORDER BY price — сортирует их по цене в порядке возрастания;
  • CUME_DIST() — для каждой строки рассчитывает долю товаров с меньшей или равной ценой.

Результат:

product_id   

;   category   

;   price   

cum_dist   

101

Electronics

500

0.33

102

Electronics

700

0.67

103

Electronics

900

1.00

201

Furniture

300

0.5

202

Furniture

600

1.0

Таким образом, у продукта с ценой 700 в категории Electronics 67% товаров стоят меньше или столько же.

PERCENT_RANK()

PERCENT_RANK() — еще один способ оценить положение строки в группе. Он показывает ранг строки среди всех остальных как долю между 0 и 1, но при этом самая первая строка получает 0, а самая последняя — 1.

Пример:

SELECT 

  product_id,

  category,

  price,

  PERCENT_RANK() OVER (PARTITION BY category ORDER BY price) AS percent_rank

FROM products;

Пояснения:

  • PARTITION BY category — делит товары на группы по категориям;
  • ORDER BY price — сортирует товары в порядке возрастания цены;
  • PERCENT_RANK() — рассчитывает относительное положение каждой строки среди всех в группе.

Результат:

product_id   

category      

price   

percent_rank   

101

Electronics

500

0.0

102

Electronics

700

0.5

103

Electronics

900

1.0

201

Furniture

300

0.0

202

Furniture

600

1.0

Первая строка в группе всегда имеет percent_rank = 0. Последняя — 1. Остальные строки получают значение, рассчитанное по формуле: (Ранг строки – 1)  / (Общее количество строк – 1). 

PERCENTILE_CONT()

Функция PERCENTILE_CONT() нужна для вычисления непрерывного процентиля. Она ищет значение в данных, которое соответствует заданному проценту. Если точного значения нет, SQL аккуратно интерполирует его между двумя ближайшими.

Проще говоря: PERCENTILE_CONT() может возвращать значения между строками.

Например, найдем медианную цену товара в каждой категории (0.5 — это медиана, или 50-й процентиль):

SELECT 

  category,

  PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY price) OVER (PARTITION BY category) AS median_price

FROM products;

Пояснения:

  • PARTITION BY category — делит данные на группы по категориям товаров;
  • ORDER BY price — сортирует товары по цене;
  • PERCENTILE_CONT(0.5) — ищет медиану (50-й процентиль) в каждой группе.

Результат:

category      

median_price   

Electronics

700

Furniture

450

Для Electronics медиана — 700 (оно есть в данных). Для Furniture медиана — 450, хотя такого значения в таблице нет. SQL рассчитал медиану как среднее между ближайшими ценами (300 и 600).

PERCENTILE_DISC()

PERCENTILE_DISC() работает схожим образом, но возвращает реальное значение из набора данных, а не усредненное или вычисленное. Никакой интерполяции — просто ближайший элемент.

Пример:

SELECT 

  category,

  PERCENTILE_DISC(0.5) WITHIN GROUP (ORDER BY price) OVER (PARTITION BY category) AS median_price_disc

FROM products;

Пояснения:

  • PARTITION BY category — делит данные на группы по категориям товаров;
  • ORDER BY price — сортирует товары по цене;
  • PERCENTILE_DISC(0.5) — ищет медиану (50-й процентиль) в каждой группе, возвращая реальное значение из списка.

Результат:

category      

median_price_disc   

Electronics

700

Furniture

300

Здесь в категории Furniture медианой становится товар за 300, а не усредненное значение.

Заключение

Оконные функции — это один из тех инструментов SQL, который меняет подход к работе с данными. Вместо громоздких вложенных запросов и лишних группировок можно быстро посчитать ранги, суммы, медианы, доли и многое другое, причем без потери самих строк.

Они помогают строить отчеты, анализировать последовательности, сравнивать значения между собой — и делают код не только короче, но и понятнее. Самое важное — научиться правильно задавать окно: выбрать, по чему делить данные, как их упорядочить и какие строки брать в расчет.