Ранжирующие функции и top-N на группу
ROW_NUMBER, RANK и DENSE_RANK различаются только обработкой ничьих. ROW_NUMBER — рабочая лошадка для top-N на группу и дедупликации: ранжируй в подзапросе, затем фильтруй rn <= N — паттерн, заменяющий дюжину коррелированных подзапросов.
«Топ-3 продавца на регион». Джун тянется к GROUP BY и LIMIT и обнаруживает, что LIMIT применяется ко всему результату, а не на группу — нет LIMIT 3 PER region. Он пробует коррелированный подзапрос, считающий «сколько в этом регионе обошли меня», что работает и бежит за O(n²), плавясь на первой реальной таблице. Однострочный сеньорский ответ — ROW_NUMBER() OVER (PARTITION BY region ORDER BY total DESC) в подзапросе, затем WHERE rn <= 3. Ранжирующие функции — это как делать top-N на группу, дедупликацию и пагинацию без квадратичной боли.
Три ранжирующих функции, одно различие: ничьи
Все три назначают позицию внутри упорядоченной партиции. Они согласны во всём, кроме того, что происходит при ничьей двух строк по значению ORDER BY:
ROW_NUMBER()— строгие 1, 2, 3, 4… Ничьих нет; если две строки равны, он всё равно даёт им разные номера (порядок между ними произволен, если не добавить колонку-тайбрейкер). Используй, когда нужна уникальная последовательность: top-N, дедупликация, пагинация.RANK()— ничьи получают одинаковый ранг, затем следующий ранг пропускается. Две строки в ничьей за 1-е место обе1, а следующая —3(2 исчезает). Это «олимпийское» ранжирование: два золота, без серебра.DENSE_RANK()— ничьи получают одинаковый ранг, но следующий ранг не пропускается. Две1, затем2. Используй для «сколько различных уровней есть», например различных ценовых тиров.
Все три дают одинаковый результат при отсутствии ничьих — различие важно только в граничном случае, и именно тогда требование обычно неоднозначно. Когда встречаешь ранжирующую функцию в запросе, спроси себя: «что должно произойти, если две строки делят первое место?» — ответ и выбирает функцию.
SELECT region, salesperson, total,
ROW_NUMBER() OVER (PARTITION BY region ORDER BY total DESC) AS rn,
RANK() OVER (PARTITION BY region ORDER BY total DESC) AS rnk,
DENSE_RANK() OVER (PARTITION BY region ORDER BY total DESC) AS drnk
FROM region_sales;Top-N на группу: канонический паттерн
Нельзя фильтровать по оконной функции в WHERE того же SELECT — оконные функции вычисляются после WHERE, в более поздней логической фазе. Поэтому паттерн всегда двухуровневый: посчитай ранг в подзапросе (или CTE), затем отфильтруй ранг во внешнем запросе.
SELECT region, salesperson, total
FROM (
SELECT region, salesperson, total,
ROW_NUMBER() OVER (PARTITION BY region ORDER BY total DESC) AS rn
FROM region_sales
) ranked
WHERE rn <= 3
ORDER BY region, rn;Это ответ на «топ-3 на регион», «5 последних заказов на клиента», «самый высокооплачиваемый сотрудник в каждом отделе». Выбор ранжирующей функции тут важен: ROW_NUMBER даёт ровно N строк на группу (ничьи разрешены произвольно); RANK может вернуть больше N, если есть ничья на границе (трое в ничьей за 3-е место все имеют ранг 3 и все проходят rnk <= 3). Для «ровно топ-3» используй ROW_NUMBER с детерминированным тайбрейкером; для «все, кто достиг топ-3-результата» используй RANK.
Дедупликация: оставить одну строку на ключ
Та же механика дедуплицирует. Чтобы схлопнуть дублирующиеся строки до самой новой на ключ, ранжируй по свежести и оставь rn = 1:
-- оставить самое свежее событие на пользователя, отбросить более старые дубли
SELECT user_id, ts, payload
FROM (
SELECT user_id, ts, payload,
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY ts DESC) AS rn
FROM events
) e
WHERE rn = 1;PARTITION BY user_id делает каждого пользователя группой; ORDER BY ts DESC ставит самое новое первым; rn = 1 — это самая новая строка. Это чище и куда быстрее, чем гимнастика с DISTINCT ON или self-join, и обобщается: rn <= k оставляет k самых новых.
NTILE(n) — двоюродный брат для разбиения на корзины: он делит упорядоченную партицию на n примерно равных корзин и метит каждую строку 1…n — основа квартилей и перцентильных полос. PERCENT_RANK() и CUME_DIST() дают относительное положение строки дробью в [0, 1] — «эта продажа в 92-м перцентиле своего региона».
▸Частая ошибка
Баг номер один в ранжировании: фильтровать оконную функцию напрямую, WHERE ROW_NUMBER() OVER (...) <= 3. Postgres это отвергает — window functions are not allowed in WHERE — потому что окна вычисляются после WHERE в логическом конвейере (FROM → WHERE → GROUP BY → окно → SELECT → ORDER BY). Окна ещё не существует, когда работает WHERE. Нужно материализовать ранг на уровень ниже (подзапрос или CTE) и фильтровать его во внешнем запросе. По той же причине нельзя положить алиас из SELECT в WHERE: фаза, что его создаёт, ещё не отработала.
Две строки в ничьей за верхний результат в регионе. Какая функция даёт им РАЗНЫЕ номера, а какая — ОДИНАКОВЫЙ ранг, но затем пропускает следующий номер?
Почему нельзя написать WHERE ROW_NUMBER() OVER (PARTITION BY region ORDER BY total DESC) <= 3 напрямую?
Расставь шаги паттерна «топ-3 продавца на регион»:
- 1 Внутренний запрос: SELECT region, salesperson, total FROM region_sales
- 2 Добавь ROW_NUMBER() OVER (PARTITION BY region ORDER BY total DESC) AS rn
- 3 Оберни как подзапрос (или CTE), чтобы rn стал реальной колонкой
- 4 Внешний запрос фильтрует WHERE rn <= 3
- 5 ORDER BY region, rn, чтобы подать топ-3 на регион
- 01Чем различаются ROW_NUMBER, RANK и DENSE_RANK при ничьей двух строк?
- 02Напиши форму паттерна top-N на группу и скажи, почему нужны два уровня запроса.
- 03Нужно ровно по одной строке на user_id, оставив самую свежую. Какая функция и фильтр?
Ранжирующие функции назначают позицию внутри упорядоченной партиции и различаются лишь политикой ничьих: ROW_NUMBER строго различен (1, 2, 3, 4), RANK повторяет ничью и затем пропускает (1, 1, 3), DENSE_RANK повторяет без пропуска (1, 1, 2). Это различие и есть весь выбор — бери ту, чьё поведение на ничьих совпадает с твоим требованием. Доминирующее применение — ROW_NUMBER для top-N на группу и дедупликации: поскольку оконные функции вычисляются после WHERE, считаешь ранг в подзапросе или CTE и затем фильтруешь rn <= N (топ N) или rn = 1 (оставить новейшее) во внешнем запросе — паттерн, заменяющий O(n²) коррелированные подзапросы одним линейным проходом. RANK — верный выбор, когда каждая строка в ничьей на отсечке должна проходить; NTILE разбивает партицию на равные срезы, а PERCENT_RANK/CUME_DIST выражают относительное положение строки дробью. Теперь, когда тянешься к LIMIT 3 на группу и замечаешь, что это не работает, — стоп: пиши ROW_NUMBER() OVER (PARTITION BY группа ORDER BY метрика DESC) во внутреннем запросе, затем фильтруй rn <= 3 снаружи. Следующий урок переходит от позиции к накоплению: накопительные итоги, скользящие средние, дельты LAG/LEAD и ловушка фрейма LAST_VALUE.
Практика
Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.