Оконные функции в SQL для аналитиков данных¶
1. Введение в оконные функции¶
Что такое оконные функции?¶
Оконные функции в SQL — это функции, которые работают с набором строк, называемым "окном". В отличие от агрегатных функций, которые агрегируют все строки в группу, оконные функции позволяют выполнять вычисления, не изменяя исходные строки. Это позволяет, например, вычислить накопленные суммы или ранжировать строки, не теряя при этом информации о каждой строке.
Отличие от агрегатных функций*¶
Агрегатные функции (например, SUM(), AVG(), COUNT()) сгруппируют строки и вернут одну строку для каждой группы.
Пример с агрегатной функцией:
Результат:
| product_id | total_sales |
|---|---|
| 1 | 500 |
| 2 | 300 |
| 3 | 700 |
В этом примере мы видим, как агрегатная функция SUM(amount) суммирует значения по каждому product_id, но все остальные данные, например, количество заказов для каждого товара, теряются.
Оконная функция сохраняет все строки, но добавляет вычисления по окну.
Запрос с оконной функцией:
Результат:
| product_id | amount | total_sales |
|---|---|---|
| 1 | 100 | 500 |
| 1 | 200 | 500 |
| 1 | 200 | 500 |
| 2 | 100 | 300 |
| 2 | 200 | 300 |
| 3 | 300 | 700 |
| 3 | 400 | 700 |
Теперь, в отличие от агрегатной функции, мы видим для каждой строки значение total_sales для соответствующего product_id, не теряя данные о каждом заказе.
2. Основные элементы оконных функций¶
Оконные функции обычно состоят из двух важных частей: PARTITION BY и ORDER BY.
PARTITION BYделит набор данных на группы, как в случае сGROUP BY, но строки внутри каждой группы сохраняются.ORDER BYопределяет порядок строк в окне для вычислений, что важно для таких функций, как ранжирование или подсчёт накопленных значений.ROWS/RANGE– определяет рамки окна (например, текущая строка и две предыдущие).
Пример с PARTITION BY и ORDER BY:
SELECT
order_id,
order_date,
total_amount,
SUM(total_amount) OVER () AS cumulative_sales
FROM orders;
Результат:
| order_id | order_date | total_amount | cumulative_sales |
|---|---|---|---|
| 101 | 2025-03-01 | 150 | 150 |
| 102 | 2025-03-01 | 200 | 350 |
| 103 | 2025-03-02 | 100 | 100 |
| 104 | 2025-03-02 | 300 | 400 |
| 105 | 2025-03-03 | 250 | 250 |
Здесь PARTITION BY order_date группирует строки по дате заказа, а ORDER BY order_date обеспечивает правильный порядок для вычисления накопленных продаж.
3. Использование ROWS BETWEEN¶
Когда нужно подсчитывать, например, скользящие суммы или средние значения для определённого диапазона строк, можно использовать ROWS BETWEEN. Эта конструкция позволяет определять диапазон строк, на основе которых будет вычисляться функция.
Пример с ROWS BETWEEN для подсчёта скользящей суммы:
SELECT
sale_date,
amount,
SUM(amount) OVER (ORDER BY sale_date ROWS BETWEEN 2 PRECEDING AND CURRENT ROW) AS moving_sum
FROM sales;
Результат:
| sale_date | amount | moving_sum |
|---|---|---|
| 2025-03-01 | 100 | 100 |
| 2025-03-02 | 150 | 250 |
| 2025-03-03 | 200 | 450 |
| 2025-03-04 | 50 | 400 |
| 2025-03-05 | 300 | 550 |
Здесь для каждой строки считается сумма amount для текущего дня и двух предыдущих. Это создаёт скользящую сумму, что полезно для анализа трендов.
4. Агрегатные оконные функции¶
Можно использовать оконные функции, такие как SUM(), AVG() и COUNT(), чтобы вычислять агрегатные показатели, но при этом не терять индивидуальные строки.
Пример запроса с SUM(), AVG() и COUNT():
SELECT
order_date,
total_amount,
COUNT(*) OVER (PARTITION BY order_date) AS orders_count,
AVG(total_amount) OVER (PARTITION BY order_date) AS avg_amount,
SUM(total_amount) OVER (PARTITION BY order_date) AS total_sales
FROM orders;
Результат:
| order_date | total_amount | orders_count | avg_amount | total_sales |
|---|---|---|---|---|
| 2025-03-01 | 150 | 2 | 175 | 350 |
| 2025-03-01 | 200 | 2 | 175 | 350 |
| 2025-03-02 | 100 | 2 | 200 | 400 |
| 2025-03-02 | 300 | 2 | 200 | 400 |
| 2025-03-03 | 250 | 1 | 250 | 250 |
Здесь для каждой строки рассчитываются: количество заказов, среднее значение и суммарные продажи для каждой даты, но данные о каждом заказе сохраняются.
5. Функции ранжирования¶
Оконные функции также могут использоваться для ранжирования строк. Например, можно присваивать каждому заказу его позицию в списке по величине суммы или по дате.
Пример с ROW_NUMBER():
SELECT
customer_id,
order_id,
total_amount,
ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY total_amount DESC) AS order_rank
FROM orders;
Результат:
| customer_id | order_id | total_amount | order_rank |
|---|---|---|---|
| 1 | 101 | 150 | 2 |
| 1 | 102 | 200 | 1 |
| 2 | 103 | 100 | 2 |
| 2 | 104 | 300 | 1 |
| 3 | 105 | 250 | 1 |
В этом примере каждый заказ получает уникальный номер в пределах каждого клиента, начиная с самого большого по сумме.
6. RANK() и DENSE_RANK()¶
Функции RANK() и DENSE_RANK() присваивают ранги, но ведут себя немного по-разному, если есть одинаковые значения.
Пример с RANK() и DENSE_RANK():
SELECT
customer_id,
order_id,
total_amount,
RANK() OVER (PARTITION BY customer_id ORDER BY total_amount DESC) AS order_rank,
DENSE_RANK() OVER (PARTITION BY customer_id ORDER BY total_amount DESC) AS dense_order_rank
FROM orders;
Результат:
| customer_id | order_id | total_amount | order_rank | dense_order_rank |
|---|---|---|---|---|
| 1 | 101 | 500 | 1 | 1 |
| 1 | 102 | 500 | 1 | 1 |
| 1 | 103 | 400 | 3 | 2 |
| 2 | 104 | 300 | 1 | 1 |
| 2 | 105 | 200 | 2 | 2 |
| 2 | 106 | 200 | 2 | 2 |
RANK(): Если два заказа имеют одинаковую сумму, то в следующем ранге будет пропущено одно место (например, два заказа с одинаковым значением получат 1-й ранг, а следующий — 3-й).
DENSE_RANK: В случае одинаковых значений ранки не пропускаются (например, два заказа с одинаковой суммой получат 1-й ранг, следующий будет 2-й).
7. LAG() и LEAD()¶
Функции LAG() и LEAD() позволяют получить значения из предыдущих или следующих строк, что полезно для вычислений разниц, сравнений или анализа изменений.
Пример с LAG():
SELECT
order_id,
order_date,
total_amount,
LAG(total_amount, 1, 0) OVER (ORDER BY order_date) AS prev_order_amount,
total_amount - LAG(total_amount, 1, 0) OVER (ORDER BY order_date) AS diff_from_prev
FROM orders;
Результат:
| order_id | order_date | total_amount | prev_order_amount | diff_from_prev |
|---|---|---|---|---|
| 101 | 2025-03-01 | 150 | 0 | 150 |
| 102 | 2025-03-01 | 200 | 150 | 50 |
| 103 | 2025-03-02 | 100 | 200 | -100 |
| 104 | 2025-03-02 | 300 | 100 | 200 |
| 105 | 2025-03-03 | 250 | 300 | -50 |
Здесь мы видим разницу между суммой текущего заказа и предыдущего.
8. FIRST_VALUE() и LAST_VALUE()¶
Функции FIRST_VALUE() и LAST_VALUE() позволяют получить первое или последнее значение в окне, что полезно для анализа первой или последней строки в наборе данных.
Пример с FIRST_VALUE():
SELECT
customer_id,
order_id,
order_date,
FIRST_VALUE(order_date) OVER (PARTITION BY customer_id ORDER BY order_date) AS first_order_date
FROM orders;
Результат:
| customer_id | order_id | order_date | first_order_date |
|---|---|---|---|
| 1 | 101 | 2025-03-01 | 2025-03-01 |
| 1 | 102 | 2025-03-02 | 2025-03-01 |
| 2 | 103 | 2025-03-02 | 2025-03-02 |
| 2 | 104 | 2025-03-03 | 2025-03-02 |
| 3 | 105 | 2025-03-04 | 2025-03-04 |
Здесь для каждого покупателя отображается дата его первого заказа.
Заключение и разбор ошибок¶
Типичные ошибки при использовании оконных функций¶
- Отсутствие ORDER BY в оконных функциях – может привести к неожиданным результатам.
- Неправильное использование ROWS BETWEEN – нужно точно задавать границы окна. Например,
ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWINGохватит одну строку до и одну после текущей, аROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROWвозьмёт все предыдущие строки до текущей. - Использование оконных функций в WHERE – они применяются только в
SELECT,ORDER BY,HAVING.