Оконные функции SQL: первый нормальный пример

Оконные функции в SQL звучат так, будто ты случайно открыл главу не для своего уровня. OVER, PARTITION BY, ROW_NUMBER, накопительные суммы — выглядит как аналитическая магия

На деле идея довольно человеческая. Иногда нам нужно посчитать что-то "рядом со строкой", но не схлопывать строки в одну, как это делает GROUP BY

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

Что получится в конце

Мы напишем запрос:

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-05790026100
Анна2026-01-121500026100
Анна2026-02-03320026100
Игорь2026-01-08450014300
Игорь2026-01-20980014300
Мария2026-02-0112001200

Строки остались. Просто к каждой строке добавилась сумма по группе клиента

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-0579001
Анна2026-01-12150002
Анна2026-02-0332003
Игорь2026-01-0845001
Игорь2026-01-2098002
Мария2026-02-0112001

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-0579007900
Анна2026-01-121500022900
Анна2026-02-03320026100

Это уже похоже на аналитику: мы видим, как накопилась выручка по каждому клиенту во времени

Собираем полезный отчет

Сделаем отчет, где есть номер заказа, сумма заказа, накопительная сумма и общая сумма по клиенту:

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

Мини-задания

  1. Пронумеруй заказы каждого клиента от дорогого к дешевому.
  2. Посчитай общую сумму заказов клиента рядом с каждой строкой.
  3. Сделай накопительную сумму по всем заказам без PARTITION BY.
  4. Найди первый заказ каждого клиента.
  5. Найди самый дорогой заказ каждого клиента.

Возможное решение пятого задания:

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

Если вы собираете тему по шагам, рядом лучше открыть:

Оцените статью
0 0 голоса
Рейтинг статьи
Подписаться
Уведомить о
guest

0 комментариев
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии
0
Оставьте комментарий! Напишите, что думаете по поводу статьи.x