КПКейсПрактика
Все материалы
Бизнеспрактикумсредний

RANK и DENSE_RANK в SQL: топ-N внутри каждой группы

Как строить рейтинг в SQL с RANK, DENSE_RANK и ROW_NUMBER: топ товаров в категории, ничьи и выбор правильной оконной функции.

КПКейсПрактикаЗапланировано · 7 августа 2026 г.17 мин

ORDER BY LIMIT возвращает топ по всей таблице, но бизнесу часто нужен top-3 внутри каждой категории или страны. После GROUP BY появляется агрегатная строка, а рейтинг по группам требует оконной функции и явного решения, что делать при одинаковом результате. В этой статье отвечаем на вопрос: Как получить первые три товара в каждой категории и правильно обработать ничьи?

Коротко

RANK — когда ничьи должны сохранить место. DENSE_RANK — когда нужны последовательные места. ROW_NUMBER — когда нужно ровно N строк и допустим tie-breaker. Всегда ранжируй уже агрегированный слой.

  • Определи зерно результата до написания оператора.
  • Разделяй фильтрацию строк, групп и рассчитанных показателей.
  • Проверь NULL, дубли, границы периода и стабильность сортировки.
  • Сверь запрос на маленьком примере перед публикацией метрики.

Какую задачу решает этот оператор

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

Если два товара делят первое место, RANK <= 3 может вернуть четыре строки: 1, 1, 3, 3. Это правильно, если нужен топ с учётом ничьих. Если интерфейс должен показать ровно три товара, нужен ROW_NUMBER и стабильный product_id как tie-breaker.

Рабочий пример для аналитика

Сначала агрегируй факт до product_id + category. Затем добавь окно PARTITION BY category ORDER BY metric DESC. Внешним запросом фильтруй rank <= N. Не фильтруй оконную функцию в WHERE того же уровня.

Пример ниже намеренно оставляет явными период, ключи связи и уровень агрегации. Если заменить их на «как принято в базе», запрос может начать считать другую сущность. Сначала адаптируй названия колонок под свою схему, затем проверь результат на нескольких известных строках.

Топ-3 товаров внутри каждой категории
WITH product_revenue AS (
  SELECT category, product_id, SUM(amount) AS revenue
  FROM orders WHERE status = 'paid' GROUP BY category, product_id
), ranked AS (
  SELECT *, DENSE_RANK() OVER (PARTITION BY category ORDER BY revenue DESC) AS revenue_rank
  FROM product_revenue
)
SELECT * FROM ranked WHERE revenue_rank <= 3
ORDER BY category, revenue_rank, product_id;

Как проверить результат

Первый тест — арифметический: совпадает ли число строк с ожидаемым зерном, а сумма подгрупп с общей суммой? Второй — поведенческий: что происходит на пустой группе, NULL, повторном событии и границе периода? Третий — сравнительный: можно ли получить тот же результат независимым способом на небольшом срезе?

Не ограничивайся тем, что запрос выполнился. Рабочий SQL должен быть объяснимым: другой аналитик понимает источник, фильтр, связь и причину выбранного оператора. Если результат меняется после добавления строки, зафиксируй, какое бизнес-правило это объясняет.

Для воспроизводимости сохрани рядом с запросом дату проверки, ожидаемый результат на контрольной выборке и версию определения метрики. Если SQL выполняется в BI, проверь, какие параметры подставляет интерфейс и не меняется ли часовая зона. Перед передачей запроса другому человеку убери неявные зависимости от сортировки, текущей даты и локальных настроек.

Оконная функция под задачу
ФункцияНичьиРазмер top-N
RANKсохраняет, пропускает местаможет быть больше N
DENSE_RANKсохраняет, без пропусковможет быть больше N
ROW_NUMBERразрывает tie-breakerровно N
LAGсравнение с предыдущимне рейтинг

Зерно данных и границы расчёта

До запуска запроса выпиши, что означает одна строка в каждой таблице. В orders это может быть заказ, в order_items — позиция заказа, а в events — отдельное событие. Оператор работает поверх этого зерна: если соединить таблицы разных уровней без промежуточной агрегации, сумма и количество начнут отвечать уже на другой вопрос.

Зафиксируй период, часовую зону, статус записи и правило для повторов. Для событий важно отделить повторную отправку от двух реальных действий пользователя. Для денежных показателей нужно понять, входят ли скидки, возвраты и отмены. Эти условия должны быть видны в запросе или в названном CTE, а не жить только в голове автора.

Проверка на маленьком наборе

Сложный запрос стоит сначала проверить на пяти-десяти строках, где ответ можно посчитать вручную. Добавь один NULL, один дубль, одну строку ровно на границе периода и одну запись без связанной строки. Такой набор быстрее показывает ошибку в логике, чем просмотр тысячи строк в BI.

После ручной проверки сравни контрольные суммы и количество уникальных ключей до и после каждого важного шага. Если JOIN изменил число строк, это не обязательно ошибка, но причина должна быть понятна. Если оператор удаляет строки, запиши, какие именно условия это делают и почему они соответствуют бизнес-вопросу.

От запроса к рабочему решению

SQL-результат становится полезным только после интерпретации. Назови, какое изменение команда увидит, какой сегмент его объясняет и какое действие можно проверить дальше. Не выдавай техническую конструкцию за причинный вывод: оператор помогает посчитать срез, но сам по себе не доказывает, почему метрика изменилась.

Для дашборда оставь понятные подписи, период обновления, фильтры и ссылку на определение метрики. Для ревью сохрани запрос, допущения и известные ограничения. Если материал пойдёт в SQL-тренажёр, добавь ожидаемое зерно и несколько тестовых случаев, чтобы пользователь учился объяснять запрос, а не только получать правильный результат.

Как читать чужой SQL-запрос

Начинай не с отдельных операторов, а с внешнего результата. Найди SELECT, посмотри, какие поля группируются, затем поднимись к FROM и JOIN. У каждого источника подпиши примерное зерно и ожидаемую кардинальность связи. После этого проверь WHERE: он может убрать пользователей до расчёта, а может незаметно исключить строки, которые нужны для знаменателя.

Если в запросе есть CTE, читай их сверху вниз и фиксируй, что изменилось на каждом шаге: только названия, число строк, зерно или бизнес-смысл. Если есть окно, проверь PARTITION BY и ORDER BY. Если есть отрицательное условие, отдельно протестируй NULL. Такой порядок чтения помогает делать ревью по логике, а не спорить о стиле форматирования.

Для сравнения двух решений собери маленькую таблицу: результат, зерно, обработанные строки, поведение на NULL, границы периода и стоимость. Иногда два запроса дают одинаковые цифры на текущем срезе, но один будет ломаться после появления повторного события или новой категории. Хорошее решение выдерживает не только текущий пример, но и заранее названные крайние случаи.

Что чаще всего ломается

Рейтинг до агрегации ранжирует заказы, а не товары. RANK и DENSE_RANK не гарантируют ровно N строк. ROW_NUMBER может скрыть честную ничью. Сортировка только по метрике делает результат нестабильным.

Отдельно проверь изменение зерна после JOIN или разворачивания массива. Большая часть странных цифр появляется не из-за сложного синтаксиса, а потому что одна строка стала описывать два разных факта. Полезный приём — временно вывести ключ и посчитать COUNT(*) по нему до и после спорного шага.

Когда выбрать другой подход

RANK — когда ничьи должны сохранить место. DENSE_RANK — когда нужны последовательные места. ROW_NUMBER — когда нужно ровно N строк и допустим tie-breaker. Всегда ранжируй уже агрегированный слой.

Оператор не обязан быть единственным решением. Сравни альтернативу по трём критериям: совпадает ли смысл, видно ли зерно и можно ли проверить стоимость выполнения. Более короткий запрос не всегда лучше, если он прячет важное бизнес-правило или делает дубли незаметными.

Три варианта ранжирования

На одинаковой метрике функции дают разную форму выдачи.

Строк в top-3

Мини-практика

Построй top-3 товаров по выручке внутри категории в трёх версиях. Создай ничью и сравни количество строк. Затем добавь маржу как вторичную сортировку и обсуди, меняет ли это бизнес-смысл рейтинга.

После решения напиши короткое объяснение в формате: «одна строка результата — …; в числителе — …; знаменатель — …; период — …; потенциальное ограничение — …». Такой шаблон хорошо переносится в SQL-тренажёр, ревью и рабочий отчёт.

  • Сначала реши, что является единицей результата.
  • Проверь граничные значения и пустые наборы.
  • Сравни результат с ручным примером.
  • Сохрани запрос рядом с определением метрики.
  • Назови, что запрос не умеет доказать.

Вывод

RANK — когда ничьи должны сохранить место. DENSE_RANK — когда нужны последовательные места. ROW_NUMBER — когда нужно ровно N строк и допустим tie-breaker. Всегда ранжируй уже агрегированный слой.

Главный навык здесь — не выучить оператор, а увидеть уровень данных, на котором он работает. Когда вопрос, зерно и период зафиксированы, SQL становится воспроизводимым способом принять решение, а не набором случайных конструкций.

Продолжить чтение
Вся библиотека