Оконные функции
Оконные функции выполняют вычисления по набору строк, связанных с текущей строкой, не уменьшая количество строк в результирующем наборе. В отличие от агрегатных функций, которые возвращают одно значение для каждой группы строк, оконные функции вычисляют отдельное значение для каждой строки на основе строк, входящих в рамку окна этой строки. Чаще всего они применяются в SELECT-запросах для аналитических задач, таких как расчет скользящих средних, накопительных итогов или ранжирования.
Обзор оконных выражений
Расположение в запросе SELECT
Оконные выражения могут использоваться только в списке SELECT:
SELECT <function_name>( [...] ) OVER ( () | <window_specification> | <window_name> )
FROM <table_name>
[ WINDOW <window_name> AS (<window_specification>) ];
Оконное выражение применяет оконную функцию к набору строк, называемому рамкой окна (window frame), которая определяется выражением OVER.
Рамка определяет, какие строки участвуют в вычислении.
После выражения OVER можно указать одно из следующих выражений:
-
()— пустые круглые скобки, чтобы передать все строки из результата запроса в оконную функцию. -
<window_specification>— описание окна прямо в запросе. -
<window_name>— имя, указанное в описании окна с помощью выраженияWINDOW.
Чтобы отфильтровать строки по значениям оконных функций, сначала вычислите их в CTE или подзапросе.
Синтаксис описания окна
Описание окна определяет, как оконная функция группирует, упорядочивает и выбирает строки для вычислений. Базовый синтаксис выглядит следующим образом:
<function_name>( [expression [, ...]] ) OVER ( () | <window_specification> | <window_name> )
Вы можете отфильтровать строки до применения оконного вычисления, используя следующий синтаксис:
<function_name>( [expression [, ...]] ) [ FILTER ( WHERE <filter_clause> ) ] OVER ( () | <window_specification> | <window_name> )
Только агрегатные оконные функции могут использовать выражение FILTER.
Узнайте больше в разделе Функции, поддерживаемые оконными выражениями.
<window_specification> выглядит следующим образом:
[PARTITION BY <partition_column> [, ...]]
[ORDER BY <order_column> [ASC | DESC] [, ...]]
[ <frame_clause> ]
Описание окна задается следующими параметрами:
-
PARTITION BY— делит результат запроса на разделы (подмножества строк), к каждому из которых оконная функция применяется отдельно. Если выражение не указано, все строки рассматриваются как один раздел. -
ORDER BY— определяет порядок строк внутри каждого раздела. Этот порядок используется для определения последовательности выполнения вычислений. Функции ранжирования (например,RANK()илиROW_NUMBER()) требуют наличияORDER BYдля вычисления значений ранга. Для OLAP-агрегацийORDER BYнеобходим для использования рамок окна (<frame_clause>). -
<frame_clause>— определяет тип и границы рамки окна, указывая, какие строки из раздела будут включены в вычисления функции. Узнайте больше в разделе Рамка окна: RANGE и ROWS.ПРИМЕЧАНИЕВыражение
ORDER BYвнутри описания окна работает независимо от верхнеуровневого выраженияORDER BYв запросе.
Рамка окна: RANGE и ROWS
Рамка окна (window frame) — это подмножество строк внутри раздела окна (window partition), определяемое относительно текущей строки.
Если рамка указана, оконная функция обрабатывает только строки в этом скользящем окне, а не весь раздел.
Рамка может быть задана на основе значений (RANGE) или на основе количества строк (ROWS):
-
RANGE— определяет рамку на основе значений сортировки, заданных в выраженииORDER BY. Это означает, что строки с одинаковыми значениями в столбцахORDER BYзанимают одну и ту же позицию в сортировке и включаются в рамку вместе. -
ROWS— определяет рамку по числу строк до или после текущей строки.
Чтобы задать тип и границы рамки окна, используйте одну из следующих форм <frame_clause>:
{ RANGE | ROWS } <frame_start>
{ RANGE | ROWS } BETWEEN <frame_start> AND <frame_end>
Значения <frame_start> и <frame_end> определяют границы рамки и могут принимать следующие значения:
UNBOUNDED PRECEDING
<value> PRECEDING
CURRENT ROW
<value> FOLLOWING
UNBOUNDED FOLLOWING
В следующей таблице описаны граничные значения, поддерживаемые выражением RANGE.
| Значение | Применяется к | Описание |
|---|---|---|
UNBOUNDED PRECEDING |
Начало рамки |
Рамка начинается с первой строки раздела |
<value> PRECEDING |
Начало или конец рамки |
Рамка начинается или заканчивается на строках, у которых значение столбца |
CURRENT ROW |
Начало или конец рамки |
|
<value> FOLLOWING |
Начало или конец рамки |
Рамка начинается или заканчивается на строках, у которых значение столбца |
UNBOUNDED FOLLOWING |
Конец рамки |
Рамка заканчивается на последней строке раздела |
В данной таблице описаны граничные значения, поддерживаемые выражением ROWS.
| Значение | Применяется к | Описание |
|---|---|---|
UNBOUNDED PRECEDING |
Начало рамки |
Рамка начинается с первой строки раздела |
<value> PRECEDING |
Начало или конец рамки |
Рамка включает |
CURRENT ROW |
Начало или конец рамки |
Рамка начинается или заканчивается на текущей строке |
<value> FOLLOWING |
Начало или конец рамки |
Рамка включает |
UNBOUNDED FOLLOWING |
Конец рамки |
Рамка заканчивается на последней строке раздела |
Функции, поддерживаемые оконными выражениями
Оконные выражения поддерживают две основные категории функций:
-
Агрегатные функции
Агрегатные функции, такие как
SUM(),COUNT(),AVG(),MIN()иMAX(), могут использоваться как оконные функции. Это также относится к любым пользовательским агрегатным функциям. Однако агрегатные функции сортирующего типа (ordered-set) и гипотезирующего типа (hypothetical-set) нельзя использовать как оконные функции. Агрегатные функции ведут себя как оконные только если за ними следует выражениеOVER; в противном случае они работают как обычные агрегаты. -
Оконные функции общего назначения
Эти специализированные функции работают на множестве строк, связанных с текущей строкой, как определено в выражении
OVER. Они включают в себя:-
Функции ранжирования, например,
RANK(),DENSE_RANK()илиROW_NUMBER(). -
Функции распределения, например,
CUME_DIST()илиPERCENT_RANK(). -
Функции доступа к значениям, например,
LAG()илиLEAD().
-
Оконные функции общего назначения
В следующих таблицах перечислены встроенные оконные функции общего назначения, сгруппированные по типам.
| Функция | Возвращаемый тип | Описание |
|---|---|---|
RANK() |
bigint |
Вычисляет ранг строки в упорядоченной группе значений. Строки с одинаковыми значениями по критерию ранжирования получают один и тот же ранг. Для вычисления следующего ранга к текущему добавляется количество строк с одинаковым значением. В этом случае ранги могут иметь пропуски |
DENSE_RANK() |
bigint |
Вычисляет ранг строки в упорядоченной группе строк без пропусков значений ранга. Строки с равными значениями получают одинаковый ранг |
ROW_NUMBER() |
bigint |
Присваивает уникальный номер каждой строке, к которой применяется функция |
NTILE(expr) |
bigint |
Разбивает упорядоченный набор данных на несколько частей (как задано в |
| Функция | Возвращаемый тип | Описание |
|---|---|---|
CUME_DIST() |
double precision |
Вычисляет кумулятивное распределение значения в группе. Строки с одинаковыми значениями всегда получают одно и то же значение кумулятивного распределения |
PERCENT_RANK() |
double precision |
Вычисляет относительный ранг текущей строки как |
| Функция | Возвращаемый тип | Описание |
|---|---|---|
LAG(expr [,offset] [,default]) |
такой же, как тип |
Позволяет обращаться к нескольким строкам одной и той же таблицы без использования самосоединений.
Если существует упорядоченный набор строк и известна текущая позиция курсора, функция
|
LEAD(expr [,offset] [,default]) |
такой же, как тип |
Позволяет обращаться к нескольким строкам одной и той же таблицы без использования самосоединений.
Если существует упорядоченный набор строк и известна текущая позиция курсора, функция
|
FIRST_VALUE(expr) |
такой же, как тип |
Возвращает первое значение в упорядоченном наборе значений |
LAST_VALUE(expr) |
такой же, как тип |
Возвращает последнее значение в упорядоченном наборе значений |
Примеры использования
Обзор оконных функций
Чтобы применить агрегатную функцию к набору строк, используйте выражение GROUP BY.
В следующем примере показано, как получить общую сумму продаж для каждого региона:
SELECT region,
SUM(quantity * price) AS sales_amount_per_region
FROM sales
GROUP BY region
ORDER BY region;
Функция SUM() уменьшает число строк, возвращаемых SQL-запросом:
region | sales_amount_per_region --------+------------------------- East | 4540.00 West | 795.00 (2 rows)
Подобно агрегатным функциям, оконные функции выполняются на наборе строк. Однако в отличие от агрегатных функций, они не сокращают количество строк, возвращаемых запросом. Этот SQL-запрос вычисляет общую сумму продаж для каждого региона и выводит результат рядом с соответствующими строками продаж:
SELECT id,
region,
sale_date,
quantity * price AS sales_amount,
SUM(quantity * price) OVER (PARTITION BY region) AS sales_amount_per_region
FROM sales
ORDER BY region, sale_date;
Результат выглядит так:
id | region | sale_date | sales_amount | sales_amount_per_region ----+--------+---------------------+--------------+------------------------- 1 | East | 2025-01-01 09:15:00 | 1200.00 | 4540.00 2 | East | 2025-01-01 14:45:00 | 60.00 | 4540.00 3 | East | 2025-01-02 10:20:00 | 180.00 | 4540.00 4 | East | 2025-01-02 13:40:00 | 85.00 | 4540.00 6 | East | 2025-01-03 09:50:00 | 1250.00 | 4540.00 7 | East | 2025-01-03 14:10:00 | 65.00 | 4540.00 9 | East | 2025-01-04 11:05:00 | 1700.00 | 4540.00 5 | West | 2025-01-02 16:30:00 | 220.00 | 795.00 8 | West | 2025-01-03 15:55:00 | 105.00 | 795.00 10 | West | 2025-01-04 17:25:00 | 120.00 | 795.00 11 | West | 2025-01-05 10:10:00 | 150.00 | 795.00 12 | West | 2025-01-05 15:00:00 | 200.00 | 795.00 (12 rows)
Этот SELECT-запрос вычисляет накопительный итог продаж для каждого региона:
SELECT id,
region,
sale_date,
quantity * price AS sales_amount,
SUM(quantity * price) OVER (
PARTITION BY region
ORDER BY sale_date) AS running_sales_amount_per_region
FROM sales
ORDER BY region, sale_date;
Ключевое отличие — использование ORDER BY sale_date внутри оконной функции, что обеспечивает накопление суммы в хронологическом порядке продаж внутри региона.
Результаты видны в столбце running_sales_amount_per_region:
id | region | sale_date | sales_amount | running_sales_amount_per_region ----+--------+---------------------+--------------+--------------------------------- 1 | East | 2025-01-01 09:15:00 | 1200.00 | 1200.00 2 | East | 2025-01-01 14:45:00 | 60.00 | 1260.00 3 | East | 2025-01-02 10:20:00 | 180.00 | 1440.00 4 | East | 2025-01-02 13:40:00 | 85.00 | 1525.00 6 | East | 2025-01-03 09:50:00 | 1250.00 | 2775.00 7 | East | 2025-01-03 14:10:00 | 65.00 | 2840.00 9 | East | 2025-01-04 11:05:00 | 1700.00 | 4540.00 5 | West | 2025-01-02 16:30:00 | 220.00 | 220.00 8 | West | 2025-01-03 15:55:00 | 105.00 | 325.00 10 | West | 2025-01-04 17:25:00 | 120.00 | 445.00 11 | West | 2025-01-05 10:10:00 | 150.00 | 595.00 12 | West | 2025-01-05 15:00:00 | 200.00 | 795.00 (12 rows)
Выражение WINDOW
Этот запрос использует именованное окно, описанное в отдельном выражении WINDOW:
SELECT id,
region,
sale_date,
quantity * price AS sales_amount,
SUM(quantity * price) OVER
regional_window AS running_sales_amount_per_region
FROM sales
WINDOW regional_window AS (
PARTITION BY region
ORDER BY sale_date
)
ORDER BY region, sale_date;
Результат выглядит так:
id | region | sale_date | sales_amount | running_sales_amount_per_region ----+--------+---------------------+--------------+--------------------------------- 1 | East | 2025-01-01 09:15:00 | 1200.00 | 1200.00 2 | East | 2025-01-01 14:45:00 | 60.00 | 1260.00 3 | East | 2025-01-02 10:20:00 | 180.00 | 1440.00 4 | East | 2025-01-02 13:40:00 | 85.00 | 1525.00 6 | East | 2025-01-03 09:50:00 | 1250.00 | 2775.00 7 | East | 2025-01-03 14:10:00 | 65.00 | 2840.00 9 | East | 2025-01-04 11:05:00 | 1700.00 | 4540.00 5 | West | 2025-01-02 16:30:00 | 220.00 | 220.00 8 | West | 2025-01-03 15:55:00 | 105.00 | 325.00 10 | West | 2025-01-04 17:25:00 | 120.00 | 445.00 11 | West | 2025-01-05 10:10:00 | 150.00 | 595.00 12 | West | 2025-01-05 15:00:00 | 200.00 | 795.00 (12 rows)
RANGE и ROWS
Этот запрос неявно использует рамку по умолчанию в режиме RANGE, которая включает все строки раздела до текущей строки:
SELECT id,
region,
sale_date,
quantity * price AS sales_amount,
SUM(quantity * price) OVER (
PARTITION BY region
ORDER BY sale_date) AS running_sales_amount_per_region
FROM sales
ORDER BY region, sale_date;
В данном запросе рамка по умолчанию указана явно с помощью RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW:
SELECT id,
region,
sale_date,
quantity * price AS sales_amount,
SUM(quantity * price) OVER (
PARTITION BY region
ORDER BY sale_date
RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS running_sales_amount_per_region
FROM sales
ORDER BY region, sale_date;
Чтобы задать рамку на основе времени, запрос с RANGE берет строки за последние 24 часа до текущей строки в том же разделе:
SELECT id,
region,
sale_date,
quantity * price AS sales_amount,
SUM(quantity * price) OVER (
PARTITION BY region
ORDER BY sale_date
RANGE BETWEEN INTERVAL '24 hours' PRECEDING AND CURRENT ROW) AS sum_last_24_hours
FROM sales
ORDER BY region, sale_date;
Результат выглядит так:
id | region | sale_date | sales_amount | sum_last_24_hours ----+--------+---------------------+--------------+------------------- 1 | East | 2025-01-01 09:15:00 | 1200.00 | 1200.00 2 | East | 2025-01-01 14:45:00 | 60.00 | 1260.00 3 | East | 2025-01-02 10:20:00 | 180.00 | 240.00 4 | East | 2025-01-02 13:40:00 | 85.00 | 325.00 6 | East | 2025-01-03 09:50:00 | 1250.00 | 1515.00 7 | East | 2025-01-03 14:10:00 | 65.00 | 1315.00 9 | East | 2025-01-04 11:05:00 | 1700.00 | 1765.00 5 | West | 2025-01-02 16:30:00 | 220.00 | 220.00 8 | West | 2025-01-03 15:55:00 | 105.00 | 325.00 10 | West | 2025-01-04 17:25:00 | 120.00 | 120.00 11 | West | 2025-01-05 10:10:00 | 150.00 | 270.00 12 | West | 2025-01-05 15:00:00 | 200.00 | 470.00 (12 rows)
Этот фрагмент показывает, как вычисляются суммы в 24-часовой рамке для строк с ID 3 и 12, указывая, какие строки попадают в рамку, а какие выходят за ее пределы:
1 | East | 2025-01-01 09:15:00 | 1200.00 | -- вне 24-часовой рамки 2 | East | 2025-01-01 14:45:00 | 60.00 | -- внутри 24-часовой рамки 3 | East | 2025-01-02 10:20:00 | 180.00 | 240.00 -- текущая строка ... 8 | West | 2025-01-03 15:55:00 | 105.00 | -- вне 24-часовой рамки 10 | West | 2025-01-04 17:25:00 | 120.00 | -- внутри 24-часовой рамки 11 | West | 2025-01-05 10:10:00 | 150.00 | -- внутри 24-часовой рамки 12 | West | 2025-01-05 15:00:00 | 200.00 | 470.00 -- текущая строка
Этот запрос вычисляет накопительную сумму продаж для текущей строки и двух предыдущих строк в каждом регионе, используя ROWS для указания рамки окна:
SELECT id,
region,
sale_date,
quantity * price AS sales_amount,
SUM(quantity * price) OVER (
PARTITION BY region
ORDER BY sale_date
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW) AS sum_last_3_sales
FROM sales
ORDER BY region, sale_date;
Результат выглядит следующим образом:
id | region | sale_date | sales_amount | sum_last_3_sales ----+--------+---------------------+--------------+------------------ 1 | East | 2025-01-01 09:15:00 | 1200.00 | 1200.00 2 | East | 2025-01-01 14:45:00 | 60.00 | 1260.00 3 | East | 2025-01-02 10:20:00 | 180.00 | 1440.00 4 | East | 2025-01-02 13:40:00 | 85.00 | 325.00 6 | East | 2025-01-03 09:50:00 | 1250.00 | 1515.00 7 | East | 2025-01-03 14:10:00 | 65.00 | 1400.00 9 | East | 2025-01-04 11:05:00 | 1700.00 | 3015.00 5 | West | 2025-01-02 16:30:00 | 220.00 | 220.00 8 | West | 2025-01-03 15:55:00 | 105.00 | 325.00 10 | West | 2025-01-04 17:25:00 | 120.00 | 445.00 11 | West | 2025-01-05 10:10:00 | 150.00 | 375.00 12 | West | 2025-01-05 15:00:00 | 200.00 | 470.00 (12 rows)
Этот фрагмент показывает, как вычисляется накопительный итог по текущей строке и двум предыдущим для строк с ID 4 и 12, указывая, какие строки входят в рамку, а какие — нет:
1 | East | 2025-01-01 09:15:00 | 1200.00 | -- вне 3-строчной рамки 2 | East | 2025-01-01 14:45:00 | 60.00 | -- внутри 3-строчной рамки 3 | East | 2025-01-02 10:20:00 | 180.00 | -- внутри 3-строчной рамки 4 | East | 2025-01-02 13:40:00 | 85.00 | 325.00 -- текущая строка ... 8 | West | 2025-01-03 15:55:00 | 105.00 | -- вне 3-строчной рамки 10 | West | 2025-01-04 17:25:00 | 120.00 | -- внутри 3-строчной рамки 11 | West | 2025-01-05 10:10:00 | 150.00 | -- внутри 3-строчной рамки 12 | West | 2025-01-05 15:00:00 | 200.00 | 470.00 -- текущая строка
Скользящее среднее
В этом примере вычисляется скользящее среднее на основе трех последних продаж в каждом регионе:
SELECT id,
region,
sale_date,
quantity * price AS sales_amount,
AVG(quantity * price) OVER (
PARTITION BY region
ORDER BY sale_date
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW) AS moving_avg_last_3_sales
FROM sales
ORDER BY region, sale_date;
Результат выглядит следующим образом:
id | region | sale_date | sales_amount | moving_avg_last_3_sales ----+--------+---------------------+--------------+------------------------- 1 | East | 2025-01-01 09:15:00 | 1200.00 | 1200.0000000000000000 2 | East | 2025-01-01 14:45:00 | 60.00 | 630.0000000000000000 3 | East | 2025-01-02 10:20:00 | 180.00 | 480.0000000000000000 4 | East | 2025-01-02 13:40:00 | 85.00 | 108.3333333333333333 6 | East | 2025-01-03 09:50:00 | 1250.00 | 505.0000000000000000 7 | East | 2025-01-03 14:10:00 | 65.00 | 466.6666666666666667 9 | East | 2025-01-04 11:05:00 | 1700.00 | 1005.0000000000000000 5 | West | 2025-01-02 16:30:00 | 220.00 | 220.0000000000000000 8 | West | 2025-01-03 15:55:00 | 105.00 | 162.5000000000000000 10 | West | 2025-01-04 17:25:00 | 120.00 | 148.3333333333333333 11 | West | 2025-01-05 10:10:00 | 150.00 | 125.0000000000000000 12 | West | 2025-01-05 15:00:00 | 200.00 | 156.6666666666666667 (12 rows)
Функции ранжирования
Функции ROW_NUMBER() и RANK() присваивают каждой строке уникальное целое число в соответствии с ее порядком в результирующем наборе.
В этом примере каждой продаже в регионе присваивается последовательный номер с помощью функции ROW_NUMBER():
SELECT id,
region,
sale_date,
ROW_NUMBER() OVER (
PARTITION BY region
ORDER BY sale_date) AS row_num
FROM sales
ORDER BY region, sale_date;
Результат должен выглядеть так:
id | region | sale_date | row_num ----+--------+---------------------+--------- 1 | East | 2025-01-01 09:15:00 | 1 2 | East | 2025-01-01 14:45:00 | 2 3 | East | 2025-01-02 10:20:00 | 3 4 | East | 2025-01-02 13:40:00 | 4 6 | East | 2025-01-03 09:50:00 | 5 7 | East | 2025-01-03 14:10:00 | 6 9 | East | 2025-01-04 11:05:00 | 7 5 | West | 2025-01-02 16:30:00 | 1 8 | West | 2025-01-03 15:55:00 | 2 10 | West | 2025-01-04 17:25:00 | 3 11 | West | 2025-01-05 10:10:00 | 4 12 | West | 2025-01-05 15:00:00 | 5 (12 rows)
Этот запрос ранжирует строки по сумме продаж в пределах каждого региона с использованием функции RANK():
SELECT id,
region,
quantity * price AS sales_amount,
RANK() OVER (
PARTITION BY region
ORDER BY quantity * price DESC) AS rank_by_sales_amount
FROM sales
ORDER BY region, rank_by_sales_amount;
Результат следующий:
id | region | sales_amount | rank_by_sales_amount ----+--------+--------------+---------------------- 9 | East | 1700.00 | 1 6 | East | 1250.00 | 2 1 | East | 1200.00 | 3 3 | East | 180.00 | 4 4 | East | 85.00 | 5 7 | East | 65.00 | 6 2 | East | 60.00 | 7 5 | West | 220.00 | 1 12 | West | 200.00 | 2 11 | West | 150.00 | 3 10 | West | 120.00 | 4 8 | West | 105.00 | 5 (12 rows)
В этом примере сравнивается поведение функций RANK() и DENSE_RANK():
-
RANK()оставляет пропуски в последовательности рангов после совпадающих значений. -
DENSE_RANK()присваивает последовательные ранги без пропусков.
Следующие запросы присваивают ранги на основе значения региона:
SELECT id,
region,
quantity * price AS sales_amount,
RANK() OVER (
ORDER BY region DESC
) AS rank_by_region
FROM sales
ORDER BY region, rank_by_region;
SELECT id,
region,
quantity * price AS sales_amount,
DENSE_RANK() OVER (
ORDER BY region DESC
) AS dense_rank_by_region
FROM sales
ORDER BY region, dense_rank_by_region;
Результаты показывают, что RANK() присваивает одинаковый порядковый номер строкам с совпадающими значениями, но пропускает следующий доступный порядковый номер (1 и 6), тогда как DENSE_RANK() продолжает последовательность без пропусков (1 и 2):
id | region | sales_amount | rank_by_region ----+--------+--------------+---------------- 2 | East | 60.00 | 6 9 | East | 1700.00 | 6 6 | East | 1250.00 | 6 4 | East | 85.00 | 6 3 | East | 180.00 | 6 1 | East | 1200.00 | 6 7 | East | 65.00 | 6 12 | West | 200.00 | 1 5 | West | 220.00 | 1 10 | West | 120.00 | 1 11 | West | 150.00 | 1 8 | West | 105.00 | 1 (12 rows)
id | region | sales_amount | dense_rank_by_region ----+--------+--------------+---------------------- 2 | East | 60.00 | 2 9 | East | 1700.00 | 2 1 | East | 1200.00 | 2 4 | East | 85.00 | 2 3 | East | 180.00 | 2 6 | East | 1250.00 | 2 7 | East | 65.00 | 2 5 | West | 220.00 | 1 10 | West | 120.00 | 1 11 | West | 150.00 | 1 12 | West | 200.00 | 1 8 | West | 105.00 | 1 (12 rows)
Функции доступа к значениям
Этот запрос возвращает дату первой продажи в каждом регионе с помощью функции FIRST_VALUE():
SELECT id,
region,
sale_date,
FIRST_VALUE(sale_date) OVER (
PARTITION BY region
ORDER BY sale_date) AS first_sale_date
FROM sales
ORDER BY region, sale_date;
Результат выглядит так:
id | region | sale_date | first_sale_date ----+--------+---------------------+--------------------- 1 | East | 2025-01-01 09:15:00 | 2025-01-01 09:15:00 2 | East | 2025-01-01 14:45:00 | 2025-01-01 09:15:00 3 | East | 2025-01-02 10:20:00 | 2025-01-01 09:15:00 4 | East | 2025-01-02 13:40:00 | 2025-01-01 09:15:00 6 | East | 2025-01-03 09:50:00 | 2025-01-01 09:15:00 7 | East | 2025-01-03 14:10:00 | 2025-01-01 09:15:00 9 | East | 2025-01-04 11:05:00 | 2025-01-01 09:15:00 5 | West | 2025-01-02 16:30:00 | 2025-01-02 16:30:00 8 | West | 2025-01-03 15:55:00 | 2025-01-02 16:30:00 10 | West | 2025-01-04 17:25:00 | 2025-01-02 16:30:00 11 | West | 2025-01-05 10:10:00 | 2025-01-02 16:30:00 12 | West | 2025-01-05 15:00:00 | 2025-01-02 16:30:00 (12 rows)
Этот запрос возвращает дату последней продажи в каждом регионе с помощью функции LAST_VALUE():
SELECT id,
region,
sale_date,
LAST_VALUE(sale_date) OVER (
PARTITION BY region
ORDER BY sale_date
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS last_sale_date
FROM sales
ORDER BY region, sale_date;
Обратите внимание, что выражение ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING указано для того, чтобы функция LAST_VALUE() учитывала все строки раздела, поскольку окно по умолчанию — RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW.
Результат следующий:
id | region | sale_date | last_sale_date ----+--------+---------------------+--------------------- 1 | East | 2025-01-01 09:15:00 | 2025-01-04 11:05:00 2 | East | 2025-01-01 14:45:00 | 2025-01-04 11:05:00 3 | East | 2025-01-02 10:20:00 | 2025-01-04 11:05:00 4 | East | 2025-01-02 13:40:00 | 2025-01-04 11:05:00 6 | East | 2025-01-03 09:50:00 | 2025-01-04 11:05:00 7 | East | 2025-01-03 14:10:00 | 2025-01-04 11:05:00 9 | East | 2025-01-04 11:05:00 | 2025-01-04 11:05:00 5 | West | 2025-01-02 16:30:00 | 2025-01-05 15:00:00 8 | West | 2025-01-03 15:55:00 | 2025-01-05 15:00:00 10 | West | 2025-01-04 17:25:00 | 2025-01-05 15:00:00 11 | West | 2025-01-05 10:10:00 | 2025-01-05 15:00:00 12 | West | 2025-01-05 15:00:00 | 2025-01-05 15:00:00 (12 rows)
Функция LAG() возвращает значение из предыдущей строки в том же разделе:
SELECT id,
region,
sale_date,
quantity * price AS sales_amount,
LAG(quantity * price) OVER (
PARTITION BY region
ORDER BY sale_date) AS prev_sales_amount
FROM sales
ORDER BY region, sale_date;
Вывод выглядит так:
id | region | sale_date | sales_amount | prev_sales_amount ----+--------+---------------------+--------------+------------------- 1 | East | 2025-01-01 09:15:00 | 1200.00 | 2 | East | 2025-01-01 14:45:00 | 60.00 | 1200.00 3 | East | 2025-01-02 10:20:00 | 180.00 | 60.00 4 | East | 2025-01-02 13:40:00 | 85.00 | 180.00 6 | East | 2025-01-03 09:50:00 | 1250.00 | 85.00 7 | East | 2025-01-03 14:10:00 | 65.00 | 1250.00 9 | East | 2025-01-04 11:05:00 | 1700.00 | 65.00 5 | West | 2025-01-02 16:30:00 | 220.00 | 8 | West | 2025-01-03 15:55:00 | 105.00 | 220.00 10 | West | 2025-01-04 17:25:00 | 120.00 | 105.00 11 | West | 2025-01-05 10:10:00 | 150.00 | 120.00 12 | West | 2025-01-05 15:00:00 | 200.00 | 150.00 (12 rows)
Функция LEAD() возвращает значение из следующей строки в том же разделе:
SELECT id,
region,
sale_date,
quantity * price AS sales_amount,
LEAD(quantity * price) OVER (
PARTITION BY region
ORDER BY sale_date) AS next_sales_amount
FROM sales
ORDER BY region, sale_date;
Вывод должен выглядеть следующим образом:
id | region | sale_date | sales_amount | next_sales_amount ----+--------+---------------------+--------------+------------------- 1 | East | 2025-01-01 09:15:00 | 1200.00 | 60.00 2 | East | 2025-01-01 14:45:00 | 60.00 | 180.00 3 | East | 2025-01-02 10:20:00 | 180.00 | 85.00 4 | East | 2025-01-02 13:40:00 | 85.00 | 1250.00 6 | East | 2025-01-03 09:50:00 | 1250.00 | 65.00 7 | East | 2025-01-03 14:10:00 | 65.00 | 1700.00 9 | East | 2025-01-04 11:05:00 | 1700.00 | 5 | West | 2025-01-02 16:30:00 | 220.00 | 105.00 8 | West | 2025-01-03 15:55:00 | 105.00 | 120.00 10 | West | 2025-01-04 17:25:00 | 120.00 | 150.00 11 | West | 2025-01-05 10:10:00 | 150.00 | 200.00 12 | West | 2025-01-05 15:00:00 | 200.00 | (12 rows)
Оконные функции и CTE
В этом примере оконная функция применяется внутри CTE для поиска продаж, сумма которых превышает среднее значение по региону:
WITH sales_with_avg AS (SELECT id,
quantity * price AS sales_amount,
AVG(quantity * price) OVER (PARTITION BY region) AS avg_sales_amount_per_region
FROM sales)
SELECT id,
sales_amount,
avg_sales_amount_per_region
FROM sales_with_avg
WHERE sales_amount > avg_sales_amount_per_region
ORDER BY id;
Результат выглядит так:
id | sales_amount | avg_sales_amount_per_region ----+--------------+----------------------------- 1 | 1200.00 | 648.5714285714285714 5 | 220.00 | 159.0000000000000000 6 | 1250.00 | 648.5714285714285714 9 | 1700.00 | 648.5714285714285714 12 | 200.00 | 159.0000000000000000 (5 rows)