Оконные функции: ROW_NUMBER, RANK, DENSE_RANK

ROW_NUMBER, RANK, DENSE_RANK — три способа пронумеровать строки и в чём их разница.

Оконная функция считает значение по «окну» строк, не схлопывая их в одну (в отличие от агрегата). OVER (...) задаёт окно.

Чем окно отличается от GROUP BY

GROUP BY сворачивает группу в одну строку. Оконная функция возвращает значение для каждой строки, но считает его по группе (окну). Поэтому рядом с зарплатой сотрудника можно показать его ранг в отделе — строки остаются на месте.

Три функции ранжирования

Все три нумеруют строки в порядке ORDER BY внутри окна, но по-разному ведут себя при ничьей:

  • ROW_NUMBER() — сплошная нумерация 1,2,3,4 — даже при равных значениях номера разные;
  • RANK() — при ничьей одинаковый ранг, но дальше номер «прыгает» (1,1,3);
  • DENSE_RANK() — при ничьей одинаковый ранг, без пропусков (1,1,2).
CREATE TABLE emp (id INTEGER PRIMARY KEY, name TEXT, salary INTEGER);
INSERT INTO emp (name, salary) VALUES
    ('Аня',120000), ('Борис',100000), ('Вера',100000), ('Гена',90000);

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

Вывод:

Аня|120000|1|1|1
Борис|100000|2|2|2
Вера|100000|3|2|2
Гена|90000|4|4|3

Борис и Вера получают по 100000 — ничья. Смотрите, что вышло: ROW_NUMBER дал им 2 и 3 (разные), RANK — обоим 2, а следующему (Гена) 4 (тройка пропущена), DENSE_RANK — обоим 2, а Гене 3 (без пропуска). Это и есть классический вопрос «в чём разница между RANK и DENSE_RANK».

PARTITION BY — окна по группам

Добавив PARTITION BY, мы нумеруем строки внутри каждой группы отдельно — нумерация сбрасывается на каждой новой группе. Найдём ранг зарплаты внутри отдела:

CREATE TABLE emp (id INTEGER PRIMARY KEY, name TEXT, dept TEXT, salary INTEGER);
INSERT INTO emp (name, dept, salary) VALUES
    ('Аня','IT',120000), ('Борис','IT',95000),
    ('Вера','HR',80000),  ('Гена','HR',90000);

SELECT dept, name, salary,
       ROW_NUMBER() OVER (PARTITION BY dept ORDER BY salary DESC) AS rang_v_otdele
FROM emp
ORDER BY dept, rang_v_otdele;

Вывод:

HR|Гена|90000|1
HR|Вера|80000|2
IT|Аня|120000|1
IT|Борис|95000|2

В каждом отделе своя нумерация с единицы: в IT первая Аня, в HR первый Гена. Это фундамент для задачи «топ-N по группам», к которой мы вернёмся.

Итог

  • Оконная функция считает по окну, но не схлопывает строки — в отличие от GROUP BY.
  • ROW_NUMBER — уникальные номера; RANK — с пропусками при ничьей; DENSE_RANK — без пропусков.
  • PARTITION BY запускает отдельную нумерацию внутри каждой группы.
Проверьте себя
1. Чем оконная функция отличается от агрегата с GROUP BY?
AНичем
BОконная функция не схлопывает строки — возвращает значение для каждой
CОконная работает только с числами
DGROUP BY быстрее всегда
2. Чем RANK отличается от DENSE_RANK при ничьей?
AНичем
BRANK пропускает следующий номер после ничьей, DENSE_RANK — нет
CDENSE_RANK даёт уникальные номера
DRANK не работает с ORDER BY
3. Что делает PARTITION BY dept?
AСортирует по dept
BЗапускает отдельную нумерацию/расчёт внутри каждого отдела
CУдаляет дубликаты отделов
DГруппирует строки в одну
Поддержать проект