SQLROW_NUMBERwindowtutorial

Что такое ROW_NUMBER в SQL? Нумерация строк для начинающих

ROW_NUMBER — это «дай каждой строке свой номер по порядку». Простыми словами: первая оконная функция, которую стоит изучить. Нумерация по убыванию, нумерация внутри групп через PARTITION BY, классические задачи top-N в каждой группе и удаление дубликатов. С таблицами и частыми ошибками.

4 мин чтенияСправочникSQL · ROW_NUMBER · window · tutorial

ROW_NUMBER — это оконная функция, которая нумерует строки по порядку. Первая получает 1, вторая — 2, и так далее. Никаких ничьих, никаких пропусков — у каждой строки свой уникальный номер.

Это самая простая из оконных функций, и обычно с неё начинают изучение «окон». Поэтому если ты когда-нибудь слышал, что «оконные функции — это сложно», начни с ROW_NUMBER. На нём всё становится понятно.

Зачем нужен ROW_NUMBER

Главные сценарии:

  • Топ-N в каждой группе: «последние 3 заказа каждого клиента», «топ-1 продажа по каждой категории».
  • Удаление дубликатов: «оставить только самую свежую строку для каждого email».
  • Пагинация со стабильной сортировкой.

До оконных функций такие задачи решались через коррелированные подзапросы или хитрые self-join — длинно и медленно. С ROW_NUMBER это пара строк.

Базовый синтаксис

ROW_NUMBER() OVER (ORDER BY column [DESC])

ROW_NUMBER() — функция (без аргументов). OVER (...) — обязательно, говорит «это оконная функция». ORDER BY — в каком порядке нумеровать.

SELECT
  name,
  amount,
  ROW_NUMBER() OVER (ORDER BY amount DESC) AS rn
FROM orders;

ROW_NUMBER() OVER (ORDER BY amount DESC) — пронумеруй строки по убыванию суммы. Самый дорогой получит rn = 1, следующий — rn = 2.

Пример с таблицей

orders:

id customer_id amount
1 1 100
2 2 250
3 1 80
4 2 500
5 1 200

Запрос с глобальной нумерацией по убыванию суммы:

SELECT
  id, customer_id, amount,
  ROW_NUMBER() OVER (ORDER BY amount DESC) AS rn
FROM orders;

Результат:

id customer_id amount rn
4 2 500 1
2 2 250 2
5 1 200 3
1 1 100 4
3 1 80 5

PARTITION BY — нумерация внутри групп

Теперь самое важное. ROW_NUMBER умеет нумеровать внутри групп:

SELECT
  id, customer_id, amount,
  ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY amount DESC) AS rn
FROM orders;

PARTITION BY customer_id — раздели строки по клиенту. ORDER BY amount DESC — внутри каждого клиента отсортируй по убыванию. ROW_NUMBER — пронумеруй с 1 заново в каждой группе.

Результат:

id customer_id amount rn
4 2 500 1
2 2 250 2
5 1 200 1
1 1 100 2
3 1 80 3

У каждого клиента свой rn = 1 — самый дорогой заказ именно у этого клиента.

Top-N в каждой группе

Это самый частый use case. «Топ-3 заказа каждого клиента»:

WITH ranked AS (
  SELECT *, ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY amount DESC) AS rn
  FROM orders
)
SELECT * FROM ranked WHERE rn <= 3;

Без оконных функций такое решалось бы через LATERAL JOIN или хитрый коррелированный подзапрос — длинно, медленно. С ROW_NUMBER — пять строк.

Удаление дубликатов (dedup)

Сценарий: в таблице users несколько строк с одним email (наследие старых импортов), хочется оставить только самую свежую:

WITH ranked AS (
  SELECT id,
         ROW_NUMBER() OVER (PARTITION BY email ORDER BY created_at DESC) AS rn
  FROM users
)
DELETE FROM users
WHERE id IN (SELECT id FROM ranked WHERE rn > 1);

PARTITION BY email — все строки с одним email вместе. ORDER BY created_at DESC — самая свежая первая. rn = 1 — самая свежая. rn > 1 — дубли. Удаляем дубли.

Это канонический паттерн для очистки данных после миграций.

ROW_NUMBER в WHERE — оборачивай в подзапрос

Важно: ROW_NUMBER нельзя использовать прямо в WHERE того же запроса:

-- НЕ РАБОТАЕТ
SELECT *, ROW_NUMBER() OVER (ORDER BY id) AS rn
FROM users
WHERE rn = 1;
-- ERROR: column "rn" does not exist

Причина — оконные функции вычисляются после WHERE, FROM, GROUP BY. Чтобы фильтровать по rn — оборачивай в подзапрос или CTE:

SELECT * FROM (
  SELECT *, ROW_NUMBER() OVER (ORDER BY id) AS rn FROM users
) t
WHERE rn = 1;

То же правило для всех оконных функций.

ROW_NUMBER vs RANK vs DENSE_RANK

Все три нумеруют, но по-разному:

  • ROW_NUMBER: 1, 2, 3, 4, 5 — даже если у строк одинаковый ORDER BY ключ.
  • RANK: 1, 2, 2, 4, 5 — при ничьей одинаковый ранг и пропуск.
  • DENSE_RANK: 1, 2, 2, 3, 4 — при ничьей одинаковый ранг без пропуска.

ROW_NUMBER нужен, когда точно один номер на строку. RANK/DENSE_RANK — когда обработка ничьих важна. Подробнее в отдельной статье.

Частые ошибки новичков

1. ROW_NUMBER в WHERE без обёртки. Уже разобрали — оборачивай в подзапрос или CTE.

2. ROW_NUMBER без ORDER BY. Технически работает (Postgres не упадёт), но порядок строк недетерминированный. Каждый запуск может дать разные rn. Всегда указывай ORDER BY.

3. Неуникальные ORDER BY. Если у двух строк одинаковый created_at, и ты сортируешь только по created_at — какой строке достанется rn = 1, не определено. Добавь tie-breaker: ORDER BY created_at DESC, id.

4. Забыли PARTITION BY, нумеруют глобально. Хочешь топ-3 на клиента, написал ROW_NUMBER() OVER (ORDER BY created_at DESC) без PARTITION BY — пронумеровал ВСЕ заказы целиком, получил «топ-3 за всю историю», не «топ-3 на клиента».

5. ROW_NUMBER ≠ постоянный id. rn пересчитывается каждый раз. Не используй для генерации стабильных идентификаторов в БД.

6. Перфоманс на больших партициях для топ-1. Если в партиции миллионы строк, а тебе нужен только rn=1 — DISTINCT ON (key) или LIMIT 1 через подзапрос будут быстрее, чем нумеровать всю партицию и потом фильтровать.

Мини-резюме

  • ROW_NUMBER() OVER (ORDER BY ...) — уникальный номер для каждой строки.
  • PARTITION BY — нумерация заново внутри каждой группы. Основа всех «top-N per group» паттернов.
  • WHERE rn = N — только в обёртке (подзапрос или CTE).
  • Всегда указывай ORDER BY внутри OVER. Если ничьи возможны — добавь tie-breaker (например, id).
  • Канонический паттерн dedup: PARTITION BY ключ ORDER BY свежесть DESC, оставить rn = 1.
  • Если ничьи нужно обрабатывать как «равный ранг» — это уже RANK/DENSE_RANK, не ROW_NUMBER.

Закрепи на практике

Решай задачи в SQL-тренажёре с мгновенной проверкой и подсказками.

Открыть тренажёр