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

user_id

order_id

amount

1

101

1000

1

102

600

2

103

2000

3

104

500

3

105

700

Как это сделать (без знания оконных функций):

Код: 

SELECT 
    user_id,
    AVG(amount) as avg_check
FROM orders
GROUP BY user_id;SELECT     event_date,    COUNT(DISTINCT user_id) as dauFROM user_eventsWHERE event_date = '2024-01-15' -- конкретный деньGROUP BY event_date;

Результат: 

user_id

avg_check

1

800

2

2000

3

600

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

Как это сделать с оконной функцией

Код: 

SELECT 
    user_id,
    order_id,
    amount,
    AVG(amount) OVER (PARTITION BY user_id) as avg_check
FROM orders;

Результат:

user_id

order_id

amount

avg_check

1

101

1000

800

1

102

600

800

2

103

2000

2000

3

104

500

600

3

105

700

600

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


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

Итак сформулируем, что же такое вообще оконная функция:

Оконная функция, это функция, которая:1 - смотрит на группу строк (окно);2 - вычисляет что-то для этой группы, но не схлопывает строки в одну – каждая строка остаётся на месте;3 - к каждой строке дописывает результат вычисления.

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

Сравнительная таблица оконной функции и агрегации (подзапроса): 

Обычная агрегация(подзапрос)

Оконная функция

GROUP BY + SUM()

SUM() OVER (PARTITION BY ...)

Схлопывает все строки в одну

Сохраняет все строки и информацию в них

Теряется детализация

Детализация сохраняется, результат к каждой строке в новом столбце

Получаем итог по группе

Получаем итог по группе и сохраняем всю детализацию

Подробнее о составляющих оконных функций

Любая оконная функция состоит из: 

<функция> OVER (
[PARTITION BY столбец1, столбец2, ...]  -- как GROUP BY, но без схлопывания
[ORDER BY столбец1, столбец2, ...]       -- задаем порядок строк в окне

)

Обязательные составляющие: ФУНКЦИЯ() OVER ()
Не обязательные составляющие: PARTITION BY, ORDER BY

OVER() – это ключевое слово, которое как бы считает функцию для каждой группы строк, указанных в партиции внутри скобок и приписывает каждой строке значение этой функции. Без OVER() функция работает как обычная, она просто схлопывает строки и группирует данные, а с OVER() функция становится оконной, она сохраняет все исходные данные, и добавляет новые. 

PARTITION BY – это правило, которое разбивает таблицу на те самые группы (окна), в этом правиле мы и указываем по какому полю группу создавать. 

Например, у тебя есть коробка с яблоками, грушами и апельсинами: PARTITION BY fruit_type (разбиваем фрукты по типам):  яблоки – это одно окно, груши – другое окно, апельсины – третье окно. Внутри каждого окна можем что-то посчитать отдельно, например, общее количество. 

Рассмотри партиции на примере таблицы с заказами:

AVG(amount) OVER (PARTITION BY user_id)

Что тут происходит по этапам:

1 - PARTITION BY user_id – тут разбиваем таблицу на группы по пользователям, получается три окна:
Окно 1: все заказы user_id = 1 (два заказа: 1000 и 600)
Окно 2: все заказы user_id = 2 (один заказ: 2000)
Окно 3: все заказы user_id = 3 (два заказа: 500 и 700)
2 - Далее внутри каждого окна считаем AVG(amount)
3 - Результат записываем в каждую строку этого окна. 

Если PARTITION BY не указать и написать просто AVG(amount) OVER ()

Тогда всё таблица будет как одно большое окно и средний чек будет один для всех строк:

user_id

order_id

amount

avg_check

1

101

1000

960

1

102

600

960

2

103

2000

960

3

104

500

960

3

105

700

960

Среднее по всем заказам: (1000+600+2000+500+700)/5 = 960

ORDER BY внутри OVER() – это правило, которое задает в каком порядке рассматривать строки внутри окна, то есть не сортирует финальный результат (для этого есть отдельный ORDER BY в конце запроса), а именно задаёт порядок вычислений внутри каждой группы.

ORDER BY внутри OVER() → как считаемORDER BY в конце запроса → как показываем

Это важно для нумерации  ROW_NUMBER() - тут функция нумерует строки в том порядке, который ты укажешь и накопительных сумм - тут сумма нарастает от первой строки к последней.

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

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

Функция

Как обрабатывает одинаковые значения

ROW_NUMBER()

Каждой строке даёт уникальный номер, даже если значения одинаковые

RANK()

Одинаковым значениям даёт одинаковый номер, но пропускает следующий номер

DENSE_RANK()

Одинаковым значениям даёт одинаковый номер, не пропускает следующий

Разберем на примере данных.
У нас есть результаты соревнований и три участника показали одинаковое время:

Имя

Время

Место (ROW_NUMBER)

Место (RANK)

Место (DENSE_RANK)

Анна

10.2

1

1

1

Иван

10.2

2

1

1

Пётр

10.2

3

1

1

Ольга

10.5

4

4

2

Сергей

10.7

5

5

3

ROW_NUMBER: тут каждому дали уникальное место (спорно, ведь время одинаковое). Особенности расчета в том, что каждая строка получает уникальный номер, даже если значения одинаковые, то номера всё равно разные и  идут подряд: 1, 2, 3, 4, 5...

Когда использовать: когда нужно пронумеровать строки в определённом порядке, без повторок и выбрать четко первую, вторую, третью строку или еще когда нужно удалить дубликаты (оставить только одну).

RANK: все с одинаковым временем получили 1-е место, но следующее место  сразу 4-е (честно, но с пропуском). Особенности расчета в том, что тут все одинаковые значения получают одинаковый ранг, но после группы одинаковых значений пропускается следующий номер. Когда использовать: когда нужно честное спортивное ранжирование (как в олимпиаде)

DENSE_RANK: все с одинаковым временем получили 1-е место, следующее место – 2-е (без пропусков). Особенности этой функции в том, что одинаковые значения получают одинаковый ранг и после группы одинаковых значений не пропускается следующий номер. Использовать лучше тогда, когда нужна непрерывная нумерация уровней, например, если нужно создание категорий (топ 3, топ 5) без разрывов.


Агрегатные функции как оконные (без схлопывания строк)

Агрегатные функции (SUM, COUNT, AVG, MIN, MAX) в обычном режиме схлопывают строки и из группы строк получается одна, а в оконном режиме они сохраняют каждую строку, просто дописывают к ней результат выбранной агрегации.

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

Ученик

Предмет

Оценка

Анна

Математика

5

Анна

Физика

4

Иван

Математика

3

Иван

Физика

5

При использовании обычной агрегации (GROUP BY),  не сохраняется исходных данных и считается только средний балл каждого ученика: 

Анна: 4.5
Иван: 4.0

А при использовании оконной агрегации (AVG() OVER) к каждой оценке приписывается средний балл ученика, но все оценки остаются на месте:

Анна, Математика, 5       средний 4.5
Анна, Физика, 4               средний 4.5
Иван, Математика, 3       средний 4.0
Иван, Физика, 5               средний 4.0

SUM() – сумма по окну

Код: 

SELECT user_id, order_id, amount,
SUM(amount) OVER (PARTITION BY user_id) as user_total,
SUM(amount) OVER (ORDER BY order_id) as running_total
FROM orders;

Результат: 

user_id

order_id

amount

user_total

running_total

1

101

1000

1600

1000

1

102

600

1600

1600

2

103

2000

2000

3600

COUNT() — количество строк в окне

Код: 

SELECT 
user_id, 
order_id,
amount, 
COUNT(*) OVER (PARTITION BY user_id) as user_orders_count, 
COUNT(*) OVER () as total_orders_count
FROM orders;

Результат: 

user_id

order_id

amount

user_orders_count

total_orders_count

1

101

1000

2

3

1

102

600

2

3

2

103

2000

1

3

AVG() – среднее по окну

Код: 

SELECT 
user_id,
order_id,
amount,
AVG(amount) OVER (PARTITION BY user_id) as user_avg,AVG(amount) OVER () as total_avg
FROM orders;

Результат: 

user_id

order_id

amount

user_avg

total_avg

1

101

1000

800

1200

1

102

600

800

1200

2

103

2000

2000

1200

MIN() / MAX() — минимум и максимум по окну

Код:

SELECT 
 user_id,
order_id,
amount,
MIN(amount) OVER (PARTITION BY user_id) as user_min,
 MAX(amount) OVER (PARTITION BY user_id) as user_max
FROM orders;

Результат:

user_id

order_id

amount

user_min

user_max

1

101

1000

600

1000

1

102

600

600

1000

2

103

2000

2000

2000

Итак, оконные функции – это важный функционал, который позволяет выполнять вычисления по группам строк без потери детализации, но помимо функций ранжирования и агрегатных еще существуют сдвиговые  (LAG, LEAD) для сравнения с соседними строками, FIRST_VALUE и LAST_VALUE для поиска первого и последнего значения в окне и продвинутая конструкция фреймы окон (ROWS BETWEEN…AND… ) для скользящих расчётов, изучите их для продвинутого использо��ания всех возможностей оконных функций в работе.

?Еще больше про будни и задачи аналитика данных в бигтехе в моем тг канале ?Таня и Данные?

?Cтатьи для старта карьеры: 
  - Базовый минимум для старта в аналитике 
  - Как стать аналитиком с нуля (и не потратить на это много денег)
  - Обзор книг 
SQL в 2026 для аналитика (с чего начать, где учиться и что реально нужно знать)
Что реально нужно знать в Python начинающему аналитику

Cтатьи-разборы задач на SQL:
https://habr.com/ru/articles/1005262/
https://habr.com/ru/articles/1005284/

Комментарии (4)


  1. urbanizzz
    25.03.2026 02:11

    SUM(amount) OVER (ORDER BY order_id) as running_total

    Получается, что при указании ORDER BY без указания PARTITION BY, окно будет считаться от начала и до текущей записи, правильно?

    Это будет работать для всех агрегирующих функций?

    А как изменится поведение при указании партиции и порядка одновременно? Окно будет считаться по партиции от начала до текущей записи?


    1. Akina
      25.03.2026 02:11

      По умолчанию при отсутствии ORDER BY окно определено как RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING. Т.е. все записи раздела (партиции), а при отсутствии PARTITION BY - все записи набора.

      А в присутствии ORDER BY окно определено как RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW. Аналогично - по записям раздела (партиции), а при отсутствии PARTITION BY - по записям набора.

      Обратите внимание - именно RANGE, a не ROWS. То есть при наличии дубликатов по выражению сортировки для всех таких записей агрегат будет посчитан по всем таким записям. Для, например, подсчёта суммы с накоплением, для получения корректного результата необходимо явно указывать фрейм ROWS.

      with cte (val) as (
          select 1 union all
          select 2 union all
          select 2 union all
          select 3     
      )
      select val, 
             sum(val) over (order by val) summ1, 
             sum(val) over (order by val
                            ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) summ2
      from cte;
      
      val  summ1 summ2
      ----|-----|-----
       1	|  1  |  1
       2	|  5  |  3
       2	|  5  |  5
       3	|  8  |  8

      Само собой, если указывается фрейм (ROWS/RANGE/GROUPS), то используются указанные границы, а не дефолтные.


    1. astentx
      25.03.2026 02:11

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

      Далее, без указания дополнительных параметров используются параметры окна по умолчанию: range between unbounded preceding and unbounded following - все строки партиции. При добавлении order by изменяется верхняя граница: unbounded following заменяется на current row.

      При добавлении последующих модификаторов [ range | rows | groups ] between [ current row | [ unbounded | N ] preceding ]... границы задаются явно.

      На русском это все описано, например, в документации по T-SQL. Перевод местами кривой, но в целом неплохо, в непонятном месте можно переключить на английский. А в целом описание с деталями мне больше всего нравится у Oracle.


  1. Akina
    25.03.2026 02:11

    Как это сделать (без знания оконных функций):

    Вроде обещали сделать - но НЕ СДЕЛАЛИ. Структура выходных данных построенного запроса не соответствует требуемой.

    мы тут теряем детализацию, у нас нет информации о каждом конкретном заказе, тут видим только средние по пользователям

    Причина такой утраты - неправильно написанный вами запрос. Хотя задача элементарно решается подзапросом - коррелированным в списке вывода или обычным в источнике данных.

    сформулируем, что же такое вообще оконная функция

    Крайне неудачная формулировка. Вообще оконная функция - это функция, возвращающая агрегированные данные, но не изменяющая уровня агрегации самого запроса.

    Сравнительная таблица оконной функции и агрегации (подзапроса)

    А можно поинтересоваться, почему выбрана именно агрегатная функция MAX()?

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

    Не обязательные составляющие: PARTITION BY, ORDER BY

    А куда вы дели ROWS/RANGE/GROUPS?

    И в этой фразе в соответствии с правилами русского языка "не" пишется слитно.

    тут разбиваем таблицу на группы по пользователям, получается три окна

    Что? Окно?? Это вы откуда вытащили? Есть partition, есть frame, но ничего, что можно перевести как "окно", тут нет. Кроме ключевого слова WINDOW для отдельного описания определения разделов и фреймов в тексте запроса, если поддержано синтаксисом диалекта. Также окном называют выделенный набор записей, использованный для расчёта значения оконной функции, но именно для отдельной записи, а не вообще.

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

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

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

    Аналогичный вопрос.