Ранжирование: 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)