Оконные функции ранжирования часто выглядят почти одинаково, поэтому на практике их регулярно путают. Самая частая ошибка: разработчик ожидает поведение DENSE_RANK(), а пишет RANK(), или наоборот.

В этой статье разберем, чем отличаются ROW_NUMBER(), RANK() и DENSE_RANK(), на каких задачах их использовать и как быстро запомнить разницу.

В чем идея каждой функции

  • ROW_NUMBER() просто нумерует строки по порядку.
  • RANK() присваивает одинаковый ранг одинаковым значениям, но оставляет пропуски.
  • DENSE_RANK() тоже присваивает одинаковый ранг одинаковым значениям, но без пропусков.

Если совсем коротко:

  • ROW_NUMBER() -> 1, 2, 3, 4
  • RANK() -> 1, 2, 2, 4
  • DENSE_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.

Именно это лучше всего показывает, что ты действительно понимаешь тему, а не просто запомнил определение.