Как посчитать ранг в SQL

Карьерник — квиз-тренажёр в Telegram с 1500+ вопросами для собесов аналитика. SQL, Python, A/B, метрики. Бесплатно.

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

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

Пример для salary = [100, 200, 200, 400]:

salary ROW_NUMBER RANK DENSE_RANK
400 1 1 1
200 2 2 2
200 3 2 2
100 4 4 3

1. Ранжирование всех строк

SELECT
    name,
    salary,
    ROW_NUMBER() OVER (ORDER BY salary DESC) AS rn,
    RANK()       OVER (ORDER BY salary DESC) AS rnk,
    DENSE_RANK() OVER (ORDER BY salary DESC) AS dense_rnk
FROM employees;

2. Ранжирование по группам (PARTITION BY)

Ранг по отделу:

SELECT
    department,
    name,
    salary,
    RANK() OVER (PARTITION BY department ORDER BY salary DESC) AS dept_rank
FROM employees;

У каждого отдела свой ранг.

3. Топ-N в каждой группе

Топ-3 самых высокооплачиваемых в каждом отделе:

SELECT * FROM (
    SELECT
        department,
        name,
        salary,
        DENSE_RANK() OVER (PARTITION BY department ORDER BY salary DESC) AS rnk
    FROM employees
) t
WHERE rnk <= 3;

Важно: DENSE_RANK, не ROW_NUMBER — чтобы не пропустить связки.

4. Топ-3 (ровно 3 строки), игнорируя связки

SELECT * FROM (
    SELECT
        department,
        name,
        salary,
        ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC) AS rn
    FROM employees
) t
WHERE rn <= 3;

ROW_NUMBER выдаст ровно 3 строки (любые, при связке — произвольные).

5. Пользователи с N-м по счёту заказом

Найти всех, у кого 5-й заказ:

SELECT * FROM (
    SELECT
        *,
        ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at) AS order_num
    FROM orders
) t
WHERE order_num = 5;

6. Вторая максимальная зарплата

SELECT salary
FROM (
    SELECT salary, DENSE_RANK() OVER (ORDER BY salary DESC) AS rnk
    FROM employees
) t
WHERE rnk = 2;

Если есть несколько на 2-м месте — вернутся все.

7. Перцентильный ранг

SELECT
    name,
    salary,
    PERCENT_RANK() OVER (ORDER BY salary) AS percent_rank,
    CUME_DIST() OVER (ORDER BY salary) AS cumulative_distribution,
    NTILE(10) OVER (ORDER BY salary) AS decile
FROM employees;
  • PERCENT_RANK — ранг в долях (0 до 1)
  • CUME_DIST — доля строк с меньшим или равным значением
  • NTILE(n) — номер из n групп

8. Ранг с фильтром

Ранжировать только оплаченные заказы:

SELECT
    user_id,
    total,
    RANK() OVER (PARTITION BY user_id ORDER BY total DESC) AS rnk
FROM orders
WHERE status = 'paid';

9. Топ N % клиентов

Выделить топ-10% клиентов по выручке:

SELECT * FROM (
    SELECT
        user_id,
        SUM(total) AS total_spent,
        NTILE(10) OVER (ORDER BY SUM(total) DESC) AS decile
    FROM orders
    GROUP BY user_id
) t
WHERE decile = 1;

10. Последний заказ каждого пользователя

SELECT * FROM (
    SELECT
        *,
        ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC) AS rn
    FROM orders
) t
WHERE rn = 1;

В Postgres также работает DISTINCT ON:

SELECT DISTINCT ON (user_id) *
FROM orders
ORDER BY user_id, created_at DESC;

11. Пять самых активных пользователей

SELECT * FROM (
    SELECT
        user_id,
        COUNT(*) AS events_cnt,
        RANK() OVER (ORDER BY COUNT(*) DESC) AS activity_rank
    FROM events
    GROUP BY user_id
) t
WHERE activity_rank <= 5;

12. Медаль: золото / серебро / бронза

SELECT
    name,
    score,
    CASE DENSE_RANK() OVER (ORDER BY score DESC)
        WHEN 1 THEN 'gold'
        WHEN 2 THEN 'silver'
        WHEN 3 THEN 'bronze'
    END AS medal
FROM athletes
WHERE DENSE_RANK() OVER (ORDER BY score DESC) <= 3;

(в реальности оконка в WHERE не работает — обернуть в подзапрос).

Частые ошибки

Ошибка 1. RANK для «топ-1»

Если два пользователя равны, WHERE rnk = 1 вернёт обоих. Для уникального топа — ROW_NUMBER.

Ошибка 2. Забыть ORDER BY в OVER

-- не работает как ожидается
ROW_NUMBER() OVER (PARTITION BY user_id)

-- правильно
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at)

Ошибка 3. Использовать в WHERE

Оконные функции нельзя в WHERE — только в подзапросе / CTE.

Ошибка 4. DENSE_RANK vs RANK для топ-N

DENSE_RANK корректно работает при связках. RANK пропускает номера.

В каких СУБД работает

Во всех современных: PostgreSQL, MySQL 8+, SQL Server, Oracle, BigQuery, Snowflake, ClickHouse.

В MySQL 5.7 — нет оконных функций, нужны переменные (@rank := @rank + 1).

Связанные темы

FAQ

Какая функция для топ-N?

DENSE_RANK — корректная. ROW_NUMBER — если связки не важны и нужно ровно N.

Как вернуть топ-1 одну строку?

ROW_NUMBER. Если нужен детерминистический ответ при связках, добавьте ID в ORDER BY: ORDER BY salary DESC, id.

Что такое PERCENT_RANK?

Ранг в виде доли (0-1). Полезно для определения, где пользователь среди всех.

NTILE или PERCENT_RANK?

NTILE — для деления на N групп (децили, квартили). PERCENT_RANK — для точной позиции.


Тренируйте SQL — откройте тренажёр с 1500+ вопросами для собесов.