Оконные функции в SQL звучат так, будто ты случайно открыл главу не для своего уровня. OVER, PARTITION BY, ROW_NUMBER, накопительные суммы — выглядит как аналитическая магия
На деле идея довольно человеческая. Иногда нам нужно посчитать что-то "рядом со строкой", но не схлопывать строки в одну, как это делает GROUP BY
Например, у нас есть заказы. Мы хотим видеть каждый заказ, но рядом добавить номер заказа клиента и накопительную сумму его покупок. Обычный GROUP BY тут мешает, потому что он превращает несколько строк в одну. Оконная функция оставляет строки на месте и добавляет расчет поверх них
- Что получится в конце
- Готовим таблицу заказов
- Зачем не подходит GROUP BY
- SUM OVER: сумма без схлопывания строк
- ROW_NUMBER: номер строки внутри группы
- Накопительная сумма
- Собираем полезный отчет
- RANK и DENSE_RANK
- Частые ошибки
- Путать ORDER BY запроса и ORDER BY окна
- Ждать, что PARTITION BY фильтрует строки
- Забыть про одинаковые даты
- Где это пригодится
- Мини-задания
- Ответы на эти вопросы могут быть для вас полезными
- Что такое оконные функции SQL?
- Чем оконные функции отличаются от GROUP BY?
- Что делает PARTITION BY?
- Зачем нужен ORDER BY внутри OVER?
- Оконные функции есть во всех базах?
- Что почитать дальше по SQL
Что получится в конце
Мы напишем запрос:
SELECT
customer,
order_date,
amount,
ROW_NUMBER() OVER (
PARTITION BY customer
ORDER BY order_date
) AS order_number,
SUM(amount) OVER (
PARTITION BY customer
ORDER BY order_date
) AS running_total
FROM orders
ORDER BY customer, order_date;
Он покажет каждый заказ, его номер внутри клиента и накопительную сумму
Готовим таблицу заказов
Создадим таблицу:
CREATE TABLE orders (
id INTEGER PRIMARY KEY,
customer TEXT,
order_date DATE,
amount INTEGER
);
Добавим данные:
INSERT INTO orders (id, customer, order_date, amount) VALUES
(1, 'Анна', '2026-01-05', 7900),
(2, 'Анна', '2026-01-12', 15000),
(3, 'Анна', '2026-02-03', 3200),
(4, 'Игорь', '2026-01-08', 4500),
(5, 'Игорь', '2026-01-20', 9800),
(6, 'Мария', '2026-02-01', 1200);
Обычный список:
SELECT customer, order_date, amount
FROM orders
ORDER BY customer, order_date;
Пока это просто строки
Зачем не подходит GROUP BY
Если мы хотим посчитать сумму по клиенту, можно написать:
SELECT customer, SUM(amount) AS total_amount
FROM orders
GROUP BY customer;
Результат:
| Анна | 26100 |
|---|---|
| Игорь | 14300 |
| Мария | 1200 |
Это полезно, но мы потеряли отдельные заказы. Нет дат, нет каждой покупки, нет порядка
А если нужно видеть и строку заказа, и сумму по клиенту рядом? Вот тут нужна оконная функция
SUM OVER: сумма без схлопывания строк
SELECT
customer,
order_date,
amount,
SUM(amount) OVER (
PARTITION BY customer
) AS customer_total
FROM orders
ORDER BY customer, order_date;
Результат:
| Анна | 2026-01-05 | 7900 | 26100 |
|---|---|---|---|
| Анна | 2026-01-12 | 15000 | 26100 |
| Анна | 2026-02-03 | 3200 | 26100 |
| Игорь | 2026-01-08 | 4500 | 14300 |
| Игорь | 2026-01-20 | 9800 | 14300 |
| Мария | 2026-02-01 | 1200 | 1200 |
Строки остались. Просто к каждой строке добавилась сумма по группе клиента
PARTITION BY customer означает: считай отдельно внутри каждого клиента. Можно думать об этом как о временной "коробке" строк, в которой функция делает расчет
ROW_NUMBER: номер строки внутри группы
Теперь пронумеруем заказы каждого клиента:
SELECT
customer,
order_date,
amount,
ROW_NUMBER() OVER (
PARTITION BY customer
ORDER BY order_date
) AS order_number
FROM orders
ORDER BY customer, order_date;
Результат:
| Анна | 2026-01-05 | 7900 | 1 |
|---|---|---|---|
| Анна | 2026-01-12 | 15000 | 2 |
| Анна | 2026-02-03 | 3200 | 3 |
| Игорь | 2026-01-08 | 4500 | 1 |
| Игорь | 2026-01-20 | 9800 | 2 |
| Мария | 2026-02-01 | 1200 | 1 |
ORDER BY order_date внутри OVER важен. Без него база не знает, в каком порядке нумеровать строки
Накопительная сумма
Теперь добавим накопительный итог по каждому клиенту:
SELECT
customer,
order_date,
amount,
SUM(amount) OVER (
PARTITION BY customer
ORDER BY order_date
) AS running_total
FROM orders
ORDER BY customer, order_date;
Для Анны результат будет:
| Анна | 2026-01-05 | 7900 | 7900 |
|---|---|---|---|
| Анна | 2026-01-12 | 15000 | 22900 |
| Анна | 2026-02-03 | 3200 | 26100 |
Это уже похоже на аналитику: мы видим, как накопилась выручка по каждому клиенту во времени
Собираем полезный отчет
Сделаем отчет, где есть номер заказа, сумма заказа, накопительная сумма и общая сумма по клиенту:
SELECT
customer,
order_date,
amount,
ROW_NUMBER() OVER (
PARTITION BY customer
ORDER BY order_date
) AS order_number,
SUM(amount) OVER (
PARTITION BY customer
ORDER BY order_date
) AS running_total,
SUM(amount) OVER (
PARTITION BY customer
) AS customer_total
FROM orders
ORDER BY customer, order_date;
В одном запросе мы получаем и строки, и аналитику. Без отдельного скрипта, без Excel-формул, без ручного подсчета
RANK и DENSE_RANK
Еще одна частая задача: ранжировать строки. Например, найти самые дорогие заказы внутри каждого клиента
SELECT
customer,
order_date,
amount,
RANK() OVER (
PARTITION BY customer
ORDER BY amount DESC
) AS amount_rank
FROM orders
ORDER BY customer, amount_rank;
RANK присваивает место. Если две строки имеют одинаковую сумму, они получат одинаковый ранг, а следующий ранг может быть пропущен. DENSE_RANK в похожей ситуации не делает пропусков
Для первого урока достаточно запомнить:
ROW_NUMBER— просто номер строки;RANK— место с учетом одинаковых значений;SUM OVER— расчет суммы поверх строк.
Частые ошибки
Путать ORDER BY запроса и ORDER BY окна
Внешний ORDER BY сортирует итоговый результат:
ORDER BY customer, order_date
ORDER BY внутри OVER задает порядок расчета функции:
OVER (
PARTITION BY customer
ORDER BY order_date
)
Это разные вещи. Для накопительной суммы важен именно порядок внутри окна
Ждать, что PARTITION BY фильтрует строки
PARTITION BY не фильтрует. Он только делит строки на группы для расчета. Если нужно убрать строки, используй WHERE
Забыть про одинаковые даты
Если у клиента два заказа в одну дату, порядок внутри дня может быть неочевидным. Лучше добавить второй критерий:
ORDER BY order_date, id
Так расчет становится стабильнее
Где это пригодится
Оконные функции нужны не только аналитикам. Они полезны, когда нужно:
- найти первый заказ клиента;
- пронумеровать события пользователя;
- посчитать накопительную выручку;
- сравнить строку со средней по группе;
- выбрать последнюю запись по каждому объекту;
- сделать рейтинг товаров внутри категории.
Например, первый заказ каждого клиента:
SELECT *
FROM (
SELECT
customer,
order_date,
amount,
ROW_NUMBER() OVER (
PARTITION BY customer
ORDER BY order_date
) AS rn
FROM orders
) t
WHERE rn = 1;
Мы сначала пронумеровали заказы внутри клиента, потом оставили только rn = 1
Мини-задания
- Пронумеруй заказы каждого клиента от дорогого к дешевому.
- Посчитай общую сумму заказов клиента рядом с каждой строкой.
- Сделай накопительную сумму по всем заказам без
PARTITION BY. - Найди первый заказ каждого клиента.
- Найди самый дорогой заказ каждого клиента.
Возможное решение пятого задания:
SELECT customer, order_date, amount
FROM (
SELECT
customer,
order_date,
amount,
ROW_NUMBER() OVER (
PARTITION BY customer
ORDER BY amount DESC, id
) AS rn
FROM orders
) ranked_orders
WHERE rn = 1;
Ответы на эти вопросы могут быть для вас полезными
Что такое оконные функции SQL?
Оконные функции считают значение по набору связанных строк, но не схлопывают результат в одну строку. Поэтому можно видеть и исходные строки, и расчет рядом с ними
Чем оконные функции отличаются от GROUP BY?
GROUP BY группирует строки и возвращает одну строку на группу. Оконная функция оставляет строки на месте и добавляет расчет поверх выбранного окна
Что делает PARTITION BY?
PARTITION BY делит строки на группы для расчета оконной функции. Например, PARTITION BY customer означает: считать отдельно для каждого клиента
Зачем нужен ORDER BY внутри OVER?
Он задает порядок расчета внутри окна. Без него нельзя корректно сделать номер строки, накопительную сумму или рейтинг
Оконные функции есть во всех базах?
В современных PostgreSQL, SQL Server, MySQL 8+, SQLite и многих других базах они есть, но детали синтаксиса могут отличаться. Перед переносом запроса в конкретную базу стоит проверить документацию
Что почитать дальше по SQL
Если вы собираете тему по шагам, рядом лучше открыть:
- GROUP BY и HAVING: группировка без каши — сравнить оконные функции с обычной группировкой.
- JOIN в SQL: INNER, LEFT и ошибки новичка — подготовить связанные данные для аналитических запросов.
- SQL с нуля: первый запрос, фильтр и сортировка — вернуться к базе SELECT и ORDER BY.
- INSERT, UPDATE, DELETE без потери данных — понять границу между отчетными и изменяющими запросами.



