← все задачи

SQL · задача 2 из 10

Топ-3 товара в каждой категории

Средний 15 минут оконные функцииPARTITION BYROW_NUMBER

Условие

Таблица products (id, category_id, name, sales). Верните по три самых продаваемых товара в каждой категории.

Что требуется

  • Ровно три товара на категорию (или все, если их меньше)
  • Порядок внутри категории — по убыванию продаж
  • Одним запросом, без циклов на стороне приложения

Пример

category  name     sales
Ноутбуки  A        100
Ноутбуки  B         90
Ноутбуки  C         80
Ноутбуки  D         70   <- не попадает
Телефоны  E         50

Сначала уточните

Вопросы до кода — половина оценки. Молча начать печатать хуже, чем задать два вопроса.

  • Что делать при равных продажах на границе: взять всех или ровно три?
  • Нужны ли категории, в которых товаров нет вовсе?
  • Какая версия СУБД — есть ли оконные функции (MySQL до 8.0 их не знает)?
Показать решение Скрыть решение

Решение

WITH ranked AS (
    SELECT
        p.id,
        p.category_id,
        p.name,
        p.sales,
        ROW_NUMBER() OVER (
            PARTITION BY p.category_id
            ORDER BY p.sales DESC, p.id
        ) AS position
    FROM products p
)
SELECT category_id, name, sales
FROM ranked
WHERE position <= 3
ORDER BY category_id, position;

Почему так

Почему нельзя просто GROUP BY

  • GROUP BY схлопывает группу в одну строку: он даёт максимум продаж, но не сам товар и тем более не три товара
  • Оконная функция считает по группе, но не схлопывает строки — каждая строка остаётся и получает свой номер
  • Это ключевое различие, которое и проверяют задачей

Почему фильтр в отдельном уровне

  • В WHERE оконную функцию использовать нельзя: WHERE отрабатывает до окон
  • Поэтому нумерацию делают в CTE или подзапросе, а фильтр — уровнем выше
  • Порядок выполнения FROM → WHERE → GROUP BY → окна → SELECT → ORDER BY стоит уметь проговорить

Почему в ORDER BY окна есть p.id

  • При равных продажах порядок между строками не определён, и результат может меняться от запуска к запуску
  • Уникальное поле вторым ключом делает нумерацию детерминированной
  • Если по условию нужны все при ничьей — вместо ROW_NUMBER берут RANK

Что спросят дальше

  • Спросят, как это сделать без оконных функций: коррелированный подзапрос со счётчиком «сколько товаров лучше этого»
  • Спросят про производительность: индекс (category_id, sales DESC) позволяет брать верхушку каждой группы без полной сортировки
  • Спросят про LATERAL JOIN — в Postgres это часто самый быстрый способ взять топ-N по группе

Следующая задача

Найти и удалить дубликаты — Данные приехали дважды: нужно найти повторы и удалить лишние, оставив по одной записи.