Оконные функции SQL: синтаксис и примеры для аналитика

Аналитика 12 мин чтения

Оконная функция SQL — это функция, которая считает значение по набору связанных строк (окну) и записывает результат в каждую строку, сохраняя их все в выдаче. Окно задаёт конструкция OVER: PARTITION BY делит строки на группы, ORDER BY задаёт порядок, ROWS или RANGE ограничивают рамку. Так одним запросом считают нарастающий итог, ранги, долю в группе и рост к прошлому месяцу.

3.25.0версия SQLite, где появились оконные функции (2018)
8.4версия PostgreSQL, где появились оконные функции
3группы функций: ранжирующие, агрегатные и смещения

Главное за 30 секунд

  • OVER — признак оконной функции. Без OVER SUM и COUNT работают как обычные агрегаты.
  • PARTITION BY — делит строки на группы. GROUP BY сворачивает группу в одну строку, окно оставляет все строки.
  • ORDER BY внутри OVER — включает нарастающий расчёт и нужен для ранжирования, LAG и LEAD.
  • ROWS и RANGE — задают рамку. По умолчанию рамка идёт от начала группы до текущей строки, поэтому LAST_VALUE без рамки возвращает текущее значение.
  • WHERE выполняется раньше оконных функций. Фильтр по рангу пишут во внешнем запросе или CTE.

Как работает оконная функция в SQL?

Оконная функция считает значение по строкам окна и возвращает его рядом с исходными колонками. Число строк в результате не меняется.

Общий синтаксис:

функция(аргументы) OVER (
    PARTITION BY колонка_группы
    ORDER BY колонка_порядка
    ROWS BETWEEN начало AND конец
)
PARTITION BY
Делит строки на группы: по клиенту, региону, менеджеру. Без него окно — вся таблица.
ORDER BY
Сортирует строки внутри группы. От порядка зависят ранги, LAG, LEAD и нарастающий итог.
ROWS
Рамка в физических строках: «две предыдущие и текущая».
RANGE
Рамка по значению ORDER BY: строки с одинаковым значением попадают в рамку вместе.

Порядок выполнения запроса: FROM, WHERE, GROUP BY, HAVING, потом оконные функции, потом ORDER BY и LIMIT. Если одно окно повторяется, его объявляют один раз через WINDOW w AS (...) и пишут OVER w.

На какой таблице решаем задачи?

Все примеры ниже идут на учебной таблице sales: 10 продаж за январь–апрель 2026 года, четыре клиента, четыре менеджера, два региона.

SELECT * FROM sales ORDER BY id
idsale_dateclientmanagerregionamount
12026-01-14АльфаИвановаМосква120000
22026-01-22БетаПетровСПб80000
32026-02-03ГаммаСмирновМосква95000
42026-02-17АльфаИвановаМосква60000
52026-03-02БетаКозловаСПб150000
62026-03-11ДельтаСмирновМосква60000
72026-03-25ГаммаПетровСПб40000
82026-04-08АльфаСмирновМосква110000
92026-04-19ДельтаКозловаСПб90000
102026-04-28БетаПетровСПб55000

Каждый запрос и результат мы прогнали в SQLite 3.51 и DuckDB 1.4: выдача совпала. Запросы с DATE_TRUNC выполнятся и в PostgreSQL; для SQLite мы заменили его на strftime('%Y-%m-01', sale_date).

Чем оконная функция отличается от GROUP BY?

GROUP BY возвращает одну строку на группу: Москва 445 000, СПб 415 000. Оконная функция добавляет тот же итог к каждой исходной строке.

SELECT id, region, amount,
       SUM(amount) OVER (PARTITION BY region) AS region_total
FROM sales
ORDER BY region, id
idregionamountregion_total
1Москва120000445000
3Москва95000445000
4Москва60000445000
6Москва60000445000
8Москва110000445000
2СПб80000415000
5СПб150000415000
7СПб40000415000
9СПб90000415000
10СПб55000415000
ПараметрGROUP BYОконная функция
Строк в результатеПо одной на группуСтолько же, сколько на входе
Доступ к исходным колонкамТолько колонки группировки и агрегатыЛюбые колонки строки
Порядок внутри группыНе учитываетсяORDER BY внутри OVER
Ранги, LAG, LEADНетЕсть
Фильтр по результатуHAVINGВнешний запрос, CTE или QUALIFY

Долю строки в группе через GROUP BY считают в два шага: агрегат в подзапросе и JOIN к таблице. Окно делает это в одном SELECT. Такие расчёты встречаются в задачах по юнит-экономике и при разборе финансовых KPI.

Какие бывают оконные функции SQL?

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

ГруппаФункцияЧто возвращает
РанжирующиеROW_NUMBER()Номер строки по порядку, без повторов
РанжирующиеRANK()Ранг; при равенстве одинаковый ранг и пропуск следующих номеров: 1, 2, 2, 4
РанжирующиеDENSE_RANK()Ранг без пропусков: 1, 2, 2, 3
РанжирующиеNTILE(n)Номер корзины, если разбить строки на n равных частей
АгрегатныеSUM, AVG, COUNT, MIN, MAXАгрегат по рамке окна: итог группы, нарастающий итог, скользящее среднее
СмещенияLAG(x, n)Значение x из строки на n позиций раньше
СмещенияLEAD(x, n)Значение x из строки на n позиций позже
СмещенияFIRST_VALUE(x)Первое значение в рамке
СмещенияLAST_VALUE(x)Последнее значение в рамке
СмещенияNTH_VALUE(x, n)n-е значение в рамке

Разница ранжирующих функций видна на продажах 4 и 6: у обеих сумма 60 000.

SELECT id, amount,
       ROW_NUMBER() OVER (ORDER BY amount DESC, id) AS row_num,
       RANK()       OVER (ORDER BY amount DESC) AS rnk,
       DENSE_RANK() OVER (ORDER BY amount DESC) AS dense_rnk
FROM sales
WHERE amount <= 95000
ORDER BY amount DESC, id
idamountrow_numrnkdense_rnk
395000111
990000222
280000333
460000444
660000544
1055000665
740000776

ROW_NUMBER дал продажам 4 и 6 номера 4 и 5. RANK дал обеим 4 и перешёл к 6. DENSE_RANK дал обеим 4 и перешёл к 5. Колонку id в ORDER BY добавили, чтобы порядок равных сумм был одинаковым при каждом запуске.

Задачи по SQL с оконными функциями: примеры с решениями

Под каждым запросом — его результат на таблице sales.

Задача 1. Нарастающий итог выручки по месяцам

Сначала CTE сворачивает продажи до месяцев, потом SUM с ORDER BY копит сумму.

WITH m AS (
    SELECT DATE_TRUNC('month', sale_date) AS month,
           SUM(amount) AS revenue
    FROM sales
    GROUP BY 1
)
SELECT month, revenue,
       SUM(revenue) OVER (ORDER BY month) AS running_total
FROM m
ORDER BY month
monthrevenuerunning_total
2026-01-01200000200000
2026-02-01155000355000
2026-03-01250000605000
2026-04-01255000860000

Задача 2. Доля продажи в выручке клиента

Окно по клиенту без ORDER BY отдаёт итог клиента в каждую строку. Множитель 100.0 переводит деление в дробное.

SELECT client, sale_date, amount,
       SUM(amount) OVER (PARTITION BY client) AS client_total,
       ROUND(100.0 * amount / SUM(amount) OVER (PARTITION BY client), 1) AS share_pct
FROM sales
ORDER BY client, sale_date
clientsale_dateamountclient_totalshare_pct
Альфа2026-01-1412000029000041.4
Альфа2026-02-176000029000020.7
Альфа2026-04-0811000029000037.9
Бета2026-01-228000028500028.1
Бета2026-03-0215000028500052.6
Бета2026-04-285500028500019.3
Гамма2026-02-039500013500070.4
Гамма2026-03-254000013500029.6
Дельта2026-03-116000015000040.0
Дельта2026-04-199000015000060.0

Задача 3. Ранг менеджеров по продажам в регионе

GROUP BY считает сумму менеджера, RANK раздаёт места внутри региона.

WITH t AS (
    SELECT region, manager, SUM(amount) AS total
    FROM sales
    GROUP BY region, manager
)
SELECT region, manager, total,
       RANK() OVER (PARTITION BY region ORDER BY total DESC) AS place
FROM t
ORDER BY region, place
regionmanagertotalplace
МоскваСмирнов2650001
МоскваИванова1800002
СПбКозлова2400001
СПбПетров1750002

Решите ещё 150 задач на SQL

В модуле SQL программы SF Education 27 уроков, 150 задач в тренажёре и 2 бизнес-кейса. Оконные функции, подзапросы и CTE разбирают отдельными темами, первые 48 часов доступа бесплатны.

Задача 4. Рост выручки к предыдущему месяцу через LAG

LAG(revenue) берёт выручку прошлого месяца. У января прошлого месяца нет, поэтому там NULL.

WITH m AS (
    SELECT DATE_TRUNC('month', sale_date) AS month,
           SUM(amount) AS revenue
    FROM sales
    GROUP BY 1
)
SELECT month, revenue,
       LAG(revenue) OVER (ORDER BY month) AS prev_revenue,
       ROUND(100.0 * (revenue - LAG(revenue) OVER (ORDER BY month))
             / LAG(revenue) OVER (ORDER BY month), 1) AS growth_pct
FROM m
ORDER BY month
monthrevenueprev_revenuegrowth_pct
2026-01-01200000NULLNULL
2026-02-01155000200000-22.5
2026-03-0125000015500061.3
2026-04-012550002500002.0

Нулевой делитель защищают через NULLIF(..., 0).

Задача 5. Скользящее среднее за 3 месяца

Рамка ROWS BETWEEN 2 PRECEDING AND CURRENT ROW берёт текущий месяц и два предыдущих.

WITH m AS (
    SELECT DATE_TRUNC('month', sale_date) AS month,
           SUM(amount) AS revenue
    FROM sales
    GROUP BY 1
)
SELECT month, revenue,
       CAST(ROUND(AVG(revenue) OVER w, 0) AS INTEGER) AS ma_3m,
       COUNT(*) OVER w AS months_in_window
FROM m
WINDOW w AS (ORDER BY month ROWS BETWEEN 2 PRECEDING AND CURRENT ROW)
ORDER BY month
monthrevenuema_3mmonths_in_window
2026-01-012000002000001
2026-02-011550001775002
2026-03-012500002016673
2026-04-012550002200003

В январе и феврале в окне один и два месяца. Колонка months_in_window показывает это, неполные окна отбрасывают условием months_in_window = 3 во внешнем запросе.

Задача 6. Первая покупка клиента

ROW_NUMBER нумерует покупки клиента по дате, внешний запрос оставляет номер 1.

WITH t AS (
    SELECT client, sale_date, manager, amount,
           ROW_NUMBER() OVER (PARTITION BY client ORDER BY sale_date) AS rn
    FROM sales
)
SELECT client, sale_date, manager, amount
FROM t
WHERE rn = 1
ORDER BY client
clientsale_datemanageramount
Альфа2026-01-14Иванова120000
Бета2026-01-22Петров80000
Гамма2026-02-03Смирнов95000
Дельта2026-03-11Смирнов60000

Если дата первой покупки нужна рядом с каждой продажей, используйте FIRST_VALUE(sale_date) OVER (PARTITION BY client ORDER BY sale_date). По ней считают когорты.

Такие задачи дают на собеседованиях, SQL входит в навыки бизнес-аналитика.

Чем отличаются оконные функции в PostgreSQL, MySQL и ClickHouse?

Синтаксис OVER одинаков во всех трёх СУБД. Различаются функции дат и фильтр по окну.

  • PostgreSQL. Оконные функции есть с версии 8.4. Месяц получают через DATE_TRUNC('month', sale_date). Деление целого на целое отбрасывает дробную часть: 5 / 2 даёт 2.
  • MySQL 8.0+. DATE_TRUNC в справочнике функций дат нет, месяц получают через DATE_FORMAT(sale_date, '%Y-%m-01'). QUALIFY в документации MySQL 8.0 не описан, фильтр по окну пишут через CTE.
  • ClickHouse. Поддерживает ROWS и RANGE, по умолчанию берёт рамку RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW. Есть QUALIFY: он фильтрует строки после расчёта оконных функций. Помимо LAG и LEAD есть lagInFrame и leadInFrame, которые учитывают рамку окна.
  • DuckDB. Мы проверили QUALIFY: он заменяет CTE из задачи 6.

Какие ошибки чаще всего делают в оконных функциях?

Три ошибки дают неверный результат или падение запроса.

LAST_VALUE без рамки окна

С ORDER BY рамка по умолчанию заканчивается на текущей строке. LAST_VALUE возвращает значение текущей строки.

SELECT client, sale_date, amount,
       LAST_VALUE(amount) OVER (PARTITION BY client ORDER BY sale_date) AS last_amount
FROM sales
WHERE client = 'Альфа'
ORDER BY sale_date
clientsale_dateamountlast_amount
Альфа2026-01-14120000120000
Альфа2026-02-176000060000
Альфа2026-04-08110000110000

Рамку до конца группы задают явно:

SELECT client, sale_date, amount,
       LAST_VALUE(amount) OVER (
           PARTITION BY client ORDER BY sale_date
           ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
       ) AS last_amount
FROM sales
WHERE client = 'Альфа'
ORDER BY sale_date
clientsale_dateamountlast_amount
Альфа2026-01-14120000110000
Альфа2026-02-1760000110000
Альфа2026-04-08110000110000

То же правило действует для NTH_VALUE.

Фильтр по оконной функции в WHERE

WHERE выполняется раньше оконных функций, поэтому такой запрос падает:

SELECT client, sale_date
FROM sales
WHERE ROW_NUMBER() OVER (PARTITION BY client ORDER BY sale_date) = 1

DuckDB отвечает «Binder Error: WHERE clause cannot contain window functions!», SQLite — «misuse of window function ROW_NUMBER()». Фильтр выносят в CTE, как в задаче 6, или в QUALIFY.

Нарастающий итог по колонке с повторами

Без явной рамки окно работает как RANGE: строки с одинаковым значением ORDER BY считаются вместе. ROWS считает строки по одной.

SELECT id, amount,
       SUM(amount) OVER (ORDER BY amount
           ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS sum_rows,
       SUM(amount) OVER (ORDER BY amount
           RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS sum_range
FROM sales
WHERE amount <= 80000
ORDER BY amount, id
idamountsum_rowssum_range
7400004000040000
10550009500095000
460000155000215000
660000215000215000
280000295000295000

У продажи 4 sum_range сразу включает и продажу 6 с той же суммой. Для построчного итога пишите ROWS и добавляйте в ORDER BY уникальную колонку.

Частые вопросы

Можно ли использовать оконную функцию в WHERE?

Нельзя: WHERE выполняется до расчёта окон, и СУБД вернёт ошибку. Посчитайте окно в CTE или подзапросе и отфильтруйте во внешнем запросе. В ClickHouse и DuckDB есть QUALIFY для такого фильтра.

Чем ROW_NUMBER отличается от RANK и DENSE_RANK?

ROW_NUMBER нумерует строки подряд и при равенстве даёт разные номера. RANK даёт равным строкам один ранг и пропускает следующие номера: 1, 2, 2, 4. DENSE_RANK пропусков не делает: 1, 2, 2, 3.

Какая рамка окна стоит по умолчанию?

Если в OVER есть ORDER BY, рамка — RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW, от начала группы до текущей строки с её дубликатами. Без ORDER BY в рамку входит вся группа. Это описано в документации SQLite и ClickHouse.

Можно ли совместить оконную функцию и GROUP BY в одном запросе?

Можно. Окно считается после группировки и работает с её результатом. Запрос SELECT region, SUM(amount), SUM(SUM(amount)) OVER () FROM sales GROUP BY region вернёт итог региона и общий итог 860 000 в каждой строке.

Поддерживает ли MySQL оконные функции?

Да, в MySQL 8.0 и новее. В справочнике MySQL 8.0 есть раздел Window Functions с ROW_NUMBER, RANK, LAG, LEAD и рамками ROWS и RANGE. Фильтр по результату окна пишут через CTE.

Другие материалы по аналитике собраны в разделе Аналитика.

Пишите аналитические запросы на SQL без выгрузок в Excel

Модуль SQL длится 1 месяц: 13 часов теории, 21 час практики, 150 задач и разбор кейсов с интервью на позицию аналитика. Выпускники получают удостоверение со сведениями в ФРДО, доступ к курсу остаётся навсегда.

Что почитать дальше
Александр Вальцев
Автор статьи Александр Вальцев Сооснователь и CEO SF Education, эксперт по корпоративным финансам
Корп. финансы Финмоделирование Инвестиции EdTech Аналитика
10+ лет в EdTech
100 000+ студентов
×150 рост за 5 лет
1000+ часов контента
  • Сооснователь и CEO SF Education — ТОП-10 по качеству образования (РБК), ТОП-100 перспективных стартапов (HolonIQ)
  • 2 года в корпоративных финансах: private equity в Канаде, investment banking в России
  • Автор курсов и вебинаров по финансовому моделированию, лектор в вузах, организатор кейс-чемпионатов в Казахстане и Узбекистане
Образование: University of Toronto (BCom in Finance), CFA Level 1