Что такое window functions в PostgreSQL?

MiddlePostgreSQL · Backend·Обновлено 14 июля 2026
Коротко
Window functions (оконные функции) выполняют вычисления над набором строк, связанных с текущей строкой, не сворачивая результат в одну строку — в отличие от агрегатных функций с GROUP BY.

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

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

Синтаксис

ФУНКЦИЯ() OVER (
  PARTITION BY столбец   -- разбивка на разделы (аналог GROUP BY)
  ORDER BY столбец       -- порядок внутри раздела
  ROWS/RANGE BETWEEN ... -- границы фрейма
)

Категории оконных функций

Ранжирующие — присваивают номер/ранг строке внутри раздела:

  • ROW_NUMBER() — уникальный номер строки
  • RANK() — ранг с пропусками при одинаковых значениях
  • DENSE_RANK() — ранг без пропусков
  • NTILE(n) — разбивает на n равных групп

Смещения — доступ к другим строкам без JOIN:

  • LAG(expr, offset) — значение из предыдущей строки
  • LEAD(expr, offset) — значение из следующей строки
  • FIRST_VALUE(expr) / LAST_VALUE(expr) — первое/последнее значение в окне
  • NTH_VALUE(expr, n) — n-е значение в окне

Агрегатные как оконныеSUM, AVG, COUNT, MIN, MAX с OVER()

Примеры

-- Ранжирование сотрудников по зарплате внутри отдела
SELECT
  name,
  department,
  salary,
  RANK() OVER (PARTITION BY department ORDER BY salary DESC) AS rank_in_dept
FROM employees;

-- Нарастающий итог продаж по дате
SELECT
  sale_date,
  amount,
  SUM(amount) OVER (ORDER BY sale_date
    ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS running_total
FROM sales;

-- Сравнение текущей строки с предыдущей (рост/падение)
SELECT
  sale_date,
  amount,
  LAG(amount, 1) OVER (ORDER BY sale_date) AS prev_amount,
  amount - LAG(amount, 1) OVER (ORDER BY sale_date) AS delta
FROM sales;

Фрейм окна (frame clause)

По умолчанию при наличии ORDER BY фрейм — RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW. Можно задать явно:

-- Скользящее среднее за 3 строки
SELECT
  sale_date,
  amount,
  AVG(amount) OVER (
    ORDER BY sale_date
    ROWS BETWEEN 2 PRECEDING AND CURRENT ROW
  ) AS moving_avg_3
FROM sales;

Порядок выполнения

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

Производительность

Для больших таблиц индекс по столбцам в PARTITION BY и ORDER BY значительно ускоряет выполнение. Несколько оконных функций с одинаковым OVER(...) PostgreSQL вычисляет за один проход.

Что хочет услышать интервьюер

Кандидат понимает принципиальное отличие от GROUP BY: строки не схлопываются, каждая строка сохраняет свои данные и получает вычисленное значение

Знает основные категории: ранжирующие (ROW_NUMBER, RANK, DENSE_RANK), смещения (LAG, LEAD) и агрегатные в режиме окна

Умеет объяснить и применить PARTITION BY, ORDER BY и frame clause (ROWS/RANGE BETWEEN)

Понимает порядок выполнения оконных функций в запросе и почему нельзя фильтровать по ним в WHERE напрямую

Может привести практический кейс: нарастающий итог, скользящее среднее, ранжирование внутри группы, сравнение с предыдущей строкой

Пример: Ранжирование сотрудников по зарплате внутри отдела

SELECT
  name,
  department,
  salary,
  ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC) AS row_num,
  RANK()       OVER (PARTITION BY department ORDER BY salary DESC) AS rank,
  DENSE_RANK() OVER (PARTITION BY department ORDER BY salary DESC) AS dense_rank
FROM employees;

Пример: Нарастающий итог и скользящее среднее продаж

SELECT
  sale_date,
  amount,
  -- Нарастающий итог с начала периода
  SUM(amount) OVER (
    ORDER BY sale_date
    ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
  ) AS running_total,
  -- Скользящее среднее за текущую и 2 предыдущие строки
  ROUND(
    AVG(amount) OVER (
      ORDER BY sale_date
      ROWS BETWEEN 2 PRECEDING AND CURRENT ROW
    ), 2
  ) AS moving_avg_3
FROM sales
ORDER BY sale_date;

Пример: LAG/LEAD: сравнение с соседними строками

SELECT
  sale_date,
  amount,
  LAG(amount, 1, 0) OVER (ORDER BY sale_date) AS prev_amount,
  LEAD(amount, 1)   OVER (ORDER BY sale_date) AS next_amount,
  -- Абсолютное изменение относительно предыдущего дня
  amount - LAG(amount, 1, 0) OVER (ORDER BY sale_date) AS delta
FROM sales
ORDER BY sale_date;

Пример: Фильтрация по оконной функции через CTE

-- Нельзя напрямую: WHERE rank = 1 вызовет ошибку
-- Правильный способ — обернуть в CTE или подзапрос
WITH ranked AS (
  SELECT
    *,
    DENSE_RANK() OVER (PARTITION BY department ORDER BY salary DESC) AS dr
  FROM employees
)
SELECT name, department, salary
FROM ranked
WHERE dr = 1; -- выводим только лучших по зарплате в каждом отделе

Типичные ошибки

Путают RANK() и DENSE_RANK(): не знают, что RANK оставляет пропуски при одинаковых значениях, а DENSE_RANK — нет

Не указывают ORDER BY внутри OVER(), когда он необходим для корректного фрейма, и получают неожиданный результат нарастающего итога

Пытаются фильтровать по оконной функции в WHERE вместо обёртки в CTE или подзапрос

Не понимают разницу между ROWS BETWEEN и RANGE BETWEEN, особенно при наличии дублирующихся значений ORDER BY

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

Лучшие курсы по теме

изображение курса

Docker и Ansible

Антон Ларичев
AI-тренажерыAI-тренажеры
Гарантия
Бонусы
иконка звёздочки рейтинга4.7
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

Node.js с нуля

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.8
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

Nest.js с нуля

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.6
3 999 ₽ 6 990 ₽
Подробнее