Как работает функция ROW_NUMBER() в SQL и зачем нужны OVER и PARTITION BY?
Увидел в чужом запросе конструкцию ROW_NUMBER() OVER (PARTITION BY ... ORDER BY ...) и не до конца понял, как она работает. Что делает ROW_NUMBER, зачем нужны OVER и PARTITION BY? Можно пример, например, как выбрать самую свежую запись по каждому пользователю.
2 ответа
ROW_NUMBER() — это оконная функция, которая присваивает каждой строке результата уникальный порядковый номер 1, 2, 3, ... в рамках заданного окна и порядка сортировки.
Разберём синтаксис OVER (PARTITION BY ... ORDER BY ...):
OVER— обязательная часть, которая объявляет, что функция оконная, и задаёт «окно» (набор строк), над которым она работает.PARTITION BY— разбивает строки на группы (партиции). Нумерация начинается заново с1в каждой группе. ЕслиPARTITION BYне указан — всё считается одной большой группой.ORDER BY(внутриOVER) — задаёт порядок, в котором назначаются номера. ДляROW_NUMBER()он обязателен по смыслу: без него порядок номеров недетерминирован.
Простой пример — сквозная нумерация сотрудников по зарплате:
SELECT
name,
department,
salary,
ROW_NUMBER() OVER (ORDER BY salary DESC) AS rn
FROM employees;
С PARTITION BY нумерация рестартует внутри каждого отдела:
SELECT
name,
department,
salary,
ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC) AS rn
FROM employees;
Теперь у каждого отдела свой rn = 1 у самого высокооплачиваемого.
Классическая задача — взять самую свежую запись по каждому пользователю. Нумеруем заказы внутри партиции пользователя по дате убывания и берём первый:
WITH ranked AS (
SELECT
o.*,
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC) AS rn
FROM orders o
)
SELECT *
FROM ranked
WHERE rn = 1;
Важный момент: оконные функции нельзя использовать прямо в WHERE той же выборки (они вычисляются позже фильтрации), поэтому оборачиваем в CTE или подзапрос, а уже снаружи фильтруем WHERE rn = 1.
Главное, что нужно усвоить про ROW_NUMBER(): в отличие от GROUP BY, оконная функция не схлопывает строки. Все исходные строки остаются на месте, к ним просто добавляется столбец с номером. Это и делает приём «пронумеровать и взять первую» таким удобным для дедупликации и выбора топ-N по группе.
Ещё пара практических заметок:
- Если в
ORDER BYвнутри окна есть ничьи (одинаковые значения),ROW_NUMBER()всё равно даст разные номера — порядок между равными строками формально не определён. Если нужна стабильность, добавьте уникальный тай-брейкер, напримерORDER BY created_at DESC, id DESC. ROW_NUMBER()отлично подходит для пагинации в связке сOFFSET/FETCHили фильтром по диапазону номеров.