Оконные функции

Рейтинг: 40.9% · 8 голосов
Подробный курс по PostgreSQL: SQL по демобазе, типы данных и JSONB, индексы, транзакции и MVCC, VACUUM, EXPLAIN и планировщик, роли и привилегии, репликация, бэкап, настройка под нагрузку и под 1С. Актуально на 2026.
Ответить
Аватара пользователя
Egor_DBA
Сообщения: 47
Зарегистрирован: 11 май 2026, 05:31

Оконные функции

Сообщение Egor_DBA »

Оглавление курса (47)
  1. Что такое PostgreSQL: история, философия, где применяется
  2. Урок 1. Что нового в PostgreSQL 15 и далее (15 -> 16 -> 17)
  3. Установка и первый запуск: Linux, Windows, Docker
  4. psql детально: метакоманды и работа в консоли
  5. Демобаза Авиаперевозки Postgres Pro: структура и развёртывание
  6. SELECT: проекция, фильтрация, сортировка
  7. Транзакции и ACID
  8. Уровни изоляции транзакций: Read Committed, Repeatable Read, Serializable
  9. Соединения таблиц (JOIN)
  10. Агрегация: GROUP BY, HAVING, GROUPING SETS
  11. Подзапросы и CTE (WITH), рекурсия
  12. Оконные функции (вы здесь)
  13. Изменение данных: INSERT/UPDATE/DELETE, UPSERT, MERGE
  14. Числовые и булевы типы, NULL и трёхзначная логика
  15. Строки, текст, дата и время, таймзоны
  16. Массивы, диапазоны, enum, составные типы
  17. JSON и JSONB: операторы и индексация
  18. DDL: CREATE TABLE, схемы, ALTER
  19. Ограничения целостности
  20. Представления и материализованные представления
  21. Последовательности, IDENTITY, генерируемые столбцы
  22. Индексы: B-tree и когда индекс не используется
  23. Типы индексов: Hash, GiST, SP-GiST, GIN, BRIN
  24. Продвинутые индексы: частичные, по выражению, покрывающие
  25. Обслуживание индексов и таблиц: bloat, REINDEX, CONCURRENTLY
  26. Полнотекстовый поиск
  27. Роли и пользователи
  28. Привилегии: GRANT/REVOKE
  29. Row-Level Security и безопасность данных
  30. Подключение приложения: pg_hba.conf, SSL, пулы соединений
  31. Серверное программирование: функции и процедуры
  32. Триггеры и события
  33. Архитектура PostgreSQL: процессы и память
  34. MVCC: версии строк и видимость
  35. VACUUM, autovacuum, freeze и wraparound
  36. WAL, контрольные точки и долговечность
  37. Блокировки и взаимоблокировки
  38. Планировщик запросов
  39. EXPLAIN и EXPLAIN ANALYZE: чтение планов
  40. Конфигурация сервера: ключевые параметры
  41. Настройка под нагрузку и под 1С
  42. Параллельные запросы и партиционирование
  43. Резервное копирование и восстановление
  44. Репликация: потоковая и hot standby
  45. Логическая репликация и кластерные решения
  46. Внешние данные, расширения и сертификация
  47. Разбор планов запросов на explain.tensor.ru: глубокое чтение EXPLAIN ANALYZE
Урок 11. Оконные функции

Бывает так: нужно посчитать сумму по группе, но при этом не схлопывать строки в одну. Например, показать каждый рейс и рядом - его долю в выручке дня. Обычный GROUP BY тут не подходит, он сворачивает детали. Эту задачу в postgresql решают оконные функции (window functions). В этом уроке разберём, как работает OVER, чем PARTITION BY отличается от GROUP BY, как ранжировать строки через row_number, rank и dense_rank, заглядывать в соседние строки через lag и lead, считать нарастающие итоги и управлять рамкой окна (ROWS, RANGE, GROUPS). По дороге соберём топ рейсов и накопительную выручку на демобазе Авиаперевозки.

Изображение

Как это работает

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

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

Порядок выполнения важен для понимания. Оконные функции вычисляются почти в самом конце обработки запроса - после WHERE, GROUP BY и HAVING, но до ORDER BY всего запроса и до LIMIT. Поэтому в WHERE нельзя сослаться на результат оконной функции напрямую: его ещё не существует. Спасает обёртка - подзапрос или CTE.

Функции делятся на несколько семейств. Ранжирующие: row_number дает сплошную нумерацию 1,2,3 без учёта равенств; rank при равных значениях ставит одинаковый ранг и потом делает пропуск (1,1,3); dense_rank тоже даёт равным одинаковый ранг, но без пропусков (1,1,2); ntile(n) делит секцию на n примерно равных корзин. Смещающие: lag смотрит на предыдущую строку, lead - на следующую, что удобно для расчёта дельты к прошлому периоду. И обычные агрегаты - sum, avg, count, max - тоже работают как оконные, если добавить OVER.

Отдельно про рамку, потому что это главный источник сюрпризов. Если в OVER есть ORDER BY, но рамка не указана явно, PostgreSQL по умолчанию берёт RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW. Это значит сумму от начала секции до текущей строки - то, что нужно для нарастающего итога. ROWS считает строки физически, RANGE - по значению ORDER BY (строки с одинаковым ключом попадают в рамку вместе), GROUPS (появился в PG 11) шагает целыми группами равных значений.

SQL и примеры

Топ-10 рейсов по выручке. Считаем сумму билетов по каждому рейсу и нумеруем по убыванию.

Код: Выделить всё

SELECT f.flight_no,
       f.scheduled_departure::date AS dep_date,
       sum(tf.amount) AS revenue,
       row_number() OVER (ORDER BY sum(tf.amount) DESC) AS rn
FROM flights f
JOIN ticket_flights tf ON tf.flight_id = f.flight_id
GROUP BY f.flight_id, f.flight_no
ORDER BY revenue DESC
LIMIT 10;
Здесь sum работает как обычный агрегат с GROUP BY, а row_number навешивается поверх уже сгруппированного результата - окно и группировка сочетаются.

Доля рейса в выручке своего дня. PARTITION BY режет по дате вылета, оконный sum даёт итог по дню, не схлопывая строки.

Код: Выделить всё

SELECT flight_no, dep_date, revenue,
       round(100.0 * revenue
             / sum(revenue) OVER (PARTITION BY dep_date), 2) AS pct_of_day
FROM (
    SELECT f.flight_id, f.flight_no,
           f.scheduled_departure::date AS dep_date,
           sum(tf.amount) AS revenue
    FROM flights f
    JOIN ticket_flights tf ON tf.flight_id = f.flight_id
    GROUP BY f.flight_id, f.flight_no, dep_date
) s
ORDER BY dep_date, pct_of_day DESC;
Нарастающий итог выручки по дням. ORDER BY внутри окна включает рамку по умолчанию, и sum копит от начала к текущему дню.

Код: Выделить всё

SELECT day,
       day_revenue,
       sum(day_revenue) OVER (ORDER BY day) AS running_total
FROM (
    SELECT b.book_date::date AS day,
           sum(b.total_amount) AS day_revenue
    FROM bookings b
    GROUP BY b.book_date::date
) d
ORDER BY day;
Сравнение с предыдущим днём через lag - получаем дельту.

Код: Выделить всё

SELECT day, day_revenue,
       lag(day_revenue) OVER (ORDER BY day) AS prev_day,
       day_revenue - lag(day_revenue, 1, 0) OVER (ORDER BY day) AS delta
FROM (
    SELECT book_date::date AS day, sum(total_amount) AS day_revenue
    FROM bookings GROUP BY book_date::date
) d
ORDER BY day;
Третий аргумент lag (тут 0) - значение по умолчанию, когда предыдущей строки нет. Удобно, чтобы в первой строке не было NULL.

Ранжирование внутри секции. Самые дорогие билеты в каждом классе обслуживания, тремя разными функциями для наглядности.

Код: Выделить всё

SELECT fare_conditions, amount,
       row_number() OVER w AS rn,
       rank()       OVER w AS rnk,
       dense_rank() OVER w AS drnk
FROM ticket_flights
WINDOW w AS (PARTITION BY fare_conditions ORDER BY amount DESC)
ORDER BY fare_conditions, amount DESC
LIMIT 30;
Здесь использован отдельный блок WINDOW: окно описано один раз под именем w и переиспользуется тремя функциями. Это чище, чем копировать OVER (...) повторно.

Частые грабли
  • Ссылка на оконную функцию в WHERE или HAVING. Их там ещё нет - оборачивайте запрос в подзапрос или CTE и фильтруйте снаружи.
  • Путают row_number и rank. Для дедупликации (оставить по одной строке на ключ) нужен именно row_number: rank при дублях даст несколько строк с рангом 1.
  • Забывают ORDER BY в окне для нарастающего итога. Без него sum посчитает итог по всей секции одинаково для всех строк, накопления не будет.
  • Молчаливая рамка по умолчанию RANGE при равных значениях ORDER BY суммирует все строки с одинаковым ключом сразу - итог скачет. Если нужна построчная логика, пишите ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW явно.
  • Думают, что DISTINCT уберёт дубли до окна. Оконные функции считаются раньше DISTINCT, поэтому результат может удивить.
  • NULL в lag и lead на краях секции. Если не задать значение по умолчанию третьим аргументом, получите NULL и сломаете арифметику.
Мини-лаба
  • Подключитесь к демобазе: psql -d demo (или своё имя), затем SET search_path = bookings;
  • Постройте топ-5 аэропортов вылета по числу рейсов: сгруппируйте flights по departure_airport, добавьте row_number() OVER (ORDER BY count(*) DESC).
  • Для таблицы ticket_flights выведите fare_conditions, amount и ntile(4) OVER (PARTITION BY fare_conditions ORDER BY amount) - разбейте билеты каждого класса на 4 корзины по цене.
  • Посчитайте нарастающий итог бронирований по дням из bookings с sum(...) OVER (ORDER BY book_date::date).
  • Добавьте к предыдущему шагу lag, чтобы вывести разницу выручки с прошлым днём и значение по умолчанию 0 для первого дня.
  • Переделайте один из запросов на блок WINDOW name AS (...) и убедитесь, что результат не изменился.
  • Сравните в одном SELECT rank() и dense_rank() по amount внутри класса и найдите место, где их значения расходятся.
Контрольные вопросы
  • Чем оконная функция принципиально отличается от агрегата с GROUP BY?
  • Почему нельзя отфильтровать строки по результату row_number прямо в WHERE и как это обойти?
  • В чём разница между row_number, rank и dense_rank на данных с повторами?
  • Какая рамка действует по умолчанию, если в OVER указан ORDER BY, и когда стоит заменить RANGE на ROWS?
  • Для чего нужен третий аргумент функций lag и lead?
  • Что делает блок WINDOW и какую проблему он решает при нескольких оконных функциях?
👍6 ❤️1 🔥 😄 🤔1
Аватара пользователя
arch13
Сообщения: 1
Зарегистрирован: 16 май 2026, 15:57

Re: Оконные функции

Сообщение arch13 »

А можно без подзапроса оставить только топ-3 в каждой секции? У меня row_number в WHERE ругается на несуществующий столбец, обернул в CTE - заработало.
👍1 ❤️ 🔥 😄 🤔
Аватара пользователя
toxicdruid
Сообщения: 1
Зарегистрирован: 13 май 2026, 03:19

Re: Оконные функции

Сообщение toxicdruid »

Поймал тот самый прикол с RANGE: нарастающий итог по сумме скакал, потому что были одинаковые amount. Поменял на ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW и стало ровно.
👍 ❤️ 🔥 😄 🤔
Ответить
← Предыдущая глава
Подзапросы и CTE (WITH), рекурсия
Следующая глава →
Изменение данных: INSERT/UPDATE/DELETE, UPSERT, MERGE

Все главы курса «PostgreSQL: от первого запроса до продакшена»

Поделиться темой: ✈ Telegram VK
Похожие запросы: оконные функции postgresql примеры over partition by

Вернуться в «PostgreSQL: от первого запроса до продакшена»

Кто сейчас на конференции

Сейчас этот форум просматривают: нет зарегистрированных пользователей и 2 гостя