Оконные функции ранжирования часто выглядят почти одинаково, поэтому на практике их регулярно путают. Самая частая ошибка: разработчик ожидает поведение DENSE_RANK(), а пишет RANK(), или наоборот.
В этой статье разберем, чем отличаются ROW_NUMBER(), RANK() и DENSE_RANK(), на каких задачах их использовать и как быстро запомнить разницу.
В чем идея каждой функции
ROW_NUMBER()просто нумерует строки по порядку.RANK()присваивает одинаковый ранг одинаковым значениям, но оставляет пропуски.DENSE_RANK()тоже присваивает одинаковый ранг одинаковым значениям, но без пропусков.
Если совсем коротко:
ROW_NUMBER()->1, 2, 3, 4RANK()->1, 2, 2, 4DENSE_RANK()->1, 2, 2, 3
Пример на одной таблице
Пусть у нас есть продажи менеджеров:
SELECT 'Анна' AS manager, 150 AS revenue
UNION ALL
SELECT 'Борис', 120
UNION ALL
SELECT 'Вика', 120
UNION ALL
SELECT 'Глеб', 90
Теперь применим все три функции сразу:
SELECT
manager,
revenue,
ROW_NUMBER() OVER (ORDER BY revenue DESC) AS row_num,
RANK() OVER (ORDER BY revenue DESC) AS rank_num,
DENSE_RANK() OVER (ORDER BY revenue DESC) AS dense_rank_num
FROM sales
Результат будет таким:
manager revenue row_num rank_num dense_rank_num
Анна 150 1 1 1
Борис 120 2 2 2
Вика 120 3 2 2
Глеб 90 4 4 3
Вот здесь и видно главное:
ROW_NUMBER()не смотрит, что значения равны. Для него каждая строка отдельная.RANK()видит одинаковые значения, но после них делает пропуск.DENSE_RANK()видит одинаковые значения и не делает пропуск.
Когда использовать ROW_NUMBER()
ROW_NUMBER() нужен, когда тебе важно выбрать ровно одну строку из группы.
Например, выбрать последнюю запись по клиенту:
SELECT *
FROM (
SELECT
client_id,
event_time,
status,
ROW_NUMBER() OVER (
PARTITION BY client_id
ORDER BY event_time DESC
) AS rn
FROM client_events
) t
WHERE rn = 1
Это один из самых частых паттернов в аналитике:
- последнее состояние клиента
- последняя транзакция
- первый заказ
- самая свежая версия записи
Почему тут не RANK() и не DENSE_RANK()? Потому что тебе нужна одна строка, а не несколько. Если две записи совпадут по времени, RANK() вернет обе как 1, а ROW_NUMBER() все равно оставит одну первой.
Когда использовать RANK()
RANK() подходит, когда ты строишь честный рейтинг и хочешь сохранить идею “одинаковые значения делят место”.
Например, рейтинг сотрудников по выручке:
SELECT
employee_name,
revenue,
RANK() OVER (ORDER BY revenue DESC) AS revenue_rank
FROM employees
Если два сотрудника делят второе место, следующий будет уже на четвертом. Это логично для соревнований и лидербордов.
Полезно использовать:
- в отчетах с местами в рейтинге
- в спортивной логике
- в задачах, где важен именно “порядковый номер места”
Когда использовать DENSE_RANK()
DENSE_RANK() нужен, когда ты хочешь ранжирование по уровням, а не по местам.
Например, выделить топ уникальных уровней зарплат:
SELECT
employee_name,
salary,
DENSE_RANK() OVER (ORDER BY salary DESC) AS salary_group
FROM employees
Тогда одинаковые зарплаты попадают в одну группу, а следующая группа идет без дырки.
Это удобно, когда ты:
- делишь значения на уровни
- выбираешь топ-3 уникальных значения
- строишь сегменты по рангу
Например, выбрать сотрудников с тремя самыми высокими уникальными зарплатами:
SELECT *
FROM (
SELECT
employee_name,
salary,
DENSE_RANK() OVER (ORDER BY salary DESC) AS salary_group
FROM employees
) t
WHERE salary_group <= 3
Частая ошибка №1: забыть PARTITION BY
Если ты хочешь ранжировать внутри категории, региона, отдела или клиента, почти всегда нужен PARTITION BY.
SELECT
category,
product_name,
revenue,
ROW_NUMBER() OVER (
PARTITION BY category
ORDER BY revenue DESC
) AS rn
FROM products
Без PARTITION BY ранжирование будет по всей таблице сразу.
Частая ошибка №2: не продумать ORDER BY
Все три функции зависят от сортировки.
ROW_NUMBER() OVER (ORDER BY created_at DESC)
Если сортировка выбрана не та, ты получишь “правильный синтаксис, но неправильную бизнес-логику”.
Особенно опасно, когда в ORDER BY есть повторы. Тогда стоит добавить второй столбец:
ROW_NUMBER() OVER (
ORDER BY created_at DESC, id DESC
)
Так результат будет стабильнее.
Как быстро запомнить разницу
Запоминалка может быть такой:
ROW_NUMBER()-> каждая строка уникальнаRANK()-> одинаковые значения делят место, следующее место с пропускомDENSE_RANK()-> одинаковые значения делят место, но без пропуска
Еще проще:
- нужен один победитель ->
ROW_NUMBER() - нужен спортивный рейтинг ->
RANK() - нужны плотные группы ->
DENSE_RANK()
Итог
Хотя эти функции выглядят очень похоже, бизнес-смысл у них разный:
ROW_NUMBER()выбирает конкретную строкуRANK()строит рейтинг с пропускамиDENSE_RANK()строит плотный рейтинг без пропусков
Если на собеседовании спрашивают разницу между ROW_NUMBER, RANK и DENSE_RANK, почти всегда хотят услышать не только формулировку, но и пример: 1,2,3, 1,2,2,4, 1,2,2,3.
Именно это лучше всего показывает, что ты действительно понимаешь тему, а не просто запомнил определение.