Ранжирование: ROW_NUMBER, RANK, DENSE_RANK

Нумерация и ранжирование в SQL: ROW_NUMBER, RANK, DENSE_RANK — как присвоить порядковый номер или место каждой строке.

Функции ранжирования присваивают каждой строке номер или место внутри окна. Они незаменимы для задач «топ-N», поиска первых/последних, дубликатов.

ROW_NUMBER — сквозной номер строки

ROW_NUMBER() нумерует строки без пропусков и без учёта одинаковых значений:

-- Нумерация сотрудников по убыванию зарплаты
SELECT
  emp_name,
  salary,
  ROW_NUMBER() OVER (ORDER BY salary DESC) AS rn
FROM employees;

Результат:

emp_name     | salary | rn
-------------+--------+---
Sarah Connor | 8000   | 1
Rick Deckard | 7200   | 2
Tony Montana | 6500   | 3
Martin Blank | 5600   | 4
Ethan Hunt   | 5000   | 5

RANK — ранг с пропусками

RANK() присваивает одинаковый ранг строкам с одинаковым значением, следующий ранг — с пропуском (1, 2, 2, 4, ...):

-- Ранжируем сотрудников по зарплате
-- (добавим искусственного дублёра Tony Montana с той же зарплатой
--  через подзапрос, чтобы показать поведение RANK)
SELECT
  emp_name,
  salary,
  RANK()       OVER (ORDER BY salary DESC) AS rank_pos,
  DENSE_RANK() OVER (ORDER BY salary DESC) AS dense_pos
FROM employees;

Результат:

emp_name     | salary | rank_pos | dense_pos
-------------+--------+----------+----------
Sarah Connor | 8000   | 1        | 1
Rick Deckard | 7200   | 2        | 2
Tony Montana | 6500   | 3        | 3
Martin Blank | 5600   | 4        | 4
Ethan Hunt   | 5000   | 5        | 5

В нашей БД все зарплаты уникальны, поэтому RANK и DENSE_RANK совпадают. Разница проявляется при одинаковых значениях: RANK даёт 1,2,2,4; DENSE_RANK — 1,2,2,3 (без пропуска).

Ранжирование внутри отдела (PARTITION BY)

-- Для каждого сотрудника — его место по зарплате внутри отдела
SELECT
  e.emp_name,
  d.dept_name,
  e.salary,
  ROW_NUMBER() OVER (
    PARTITION BY e.dept_id
    ORDER BY e.salary DESC
  ) AS rank_in_dept
FROM employees AS e
JOIN departments AS d ON d.dept_id = e.dept_id
ORDER BY d.dept_name, rank_in_dept;

Результат:

emp_name     | dept_name       | salary | rank_in_dept
-------------+-----------------+--------+-------------
Tony Montana | Administration  | 6500   | 1
Rick Deckard | Finance         | 7200   | 1
Ethan Hunt   | Human Resources | 5000   | 1
Sarah Connor | Sales           | 8000   | 1

Топ-1 в каждом отделе через подзапрос

Распространённый паттерн: нумеруем строки через ROW_NUMBER, затем фильтруем только первые:

-- Самый высокооплачиваемый сотрудник в каждом отделе
SELECT emp_name, dept_id, salary
FROM (
  SELECT
    emp_name, dept_id, salary,
    ROW_NUMBER() OVER (
      PARTITION BY dept_id
      ORDER BY salary DESC
    ) AS rn
  FROM employees
  WHERE dept_id IS NOT NULL
) AS ranked
WHERE rn = 1
ORDER BY salary DESC;

Результат:

emp_name     | dept_id | salary
-------------+---------+-------
Sarah Connor | 5       | 8000
Rick Deckard | 3       | 7200
Tony Montana | 1       | 6500
Ethan Hunt   | 4       | 5000

Сравнение функций

ФункцияОдинаковые значенияСледующий номер
ROW_NUMBERразные (произвольно)без пропуска
RANKодинаковый рангс пропуском
DENSE_RANKодинаковый рангбез пропуска

Коротко

  • ROW_NUMBER() — уникальный сквозной номер каждой строке; одинаковых не бывает.
  • RANK() — одинаковый ранг при совпадении значений, следующий ранг с пропуском.
  • DENSE_RANK() — то же, но без пропуска в нумерации.
  • Паттерн «топ-N на группу»: ROW_NUMBER() OVER (PARTITION BY ... ORDER BY ...) + фильтр по номеру в подзапросе.
Проверьте себя
1. Две строки имеют одинаковое значение. Каков их ранг при RANK() и каков следующий ранг после них?
AРазные ранги, следующий ранг идёт подряд
BОдинаковый ранг, следующий ранг с пропуском
CОдинаковый ранг, следующий ранг без пропуска
DОшибка — ранги не могут повторяться
2. В чём разница между RANK() и DENSE_RANK()?
AНет разницы
BDENSE_RANK не делает пропуска в нумерации при совпадающих значениях
CRANK работает только с числами
DDENSE_RANK всегда даёт уникальные номера
3. Как найти топ-1 сотрудника по зарплате в каждом отделе?
ASELECT MAX(salary) FROM employees GROUP BY dept_id
BSELECT * FROM (SELECT *, ROW_NUMBER() OVER (PARTITION BY dept_id ORDER BY salary DESC) AS rn FROM employees) WHERE rn = 1
CSELECT emp_name FROM employees ORDER BY salary DESC LIMIT 1
DSELECT emp_name, dept_id WHERE salary = MAX(salary)
Поддержать проект