Как с помощью SQL найти топ-3 товара по продажам в каждой категории?

«Как с помощью SQL найти топ-3 товара по продажам в каждой категории?» — вопрос из категории Аналитика и метрики, который задают на 33% собеседований Data Инженер. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Для решения этой задачи идеально подходят оконные функции (window functions) SQL, в частности ROW_NUMBER(), RANK() или DENSE_RANK(). Вот оптимальный запрос:

WITH sales_ranking AS (
    SELECT 
        p.category_id,
        c.category_name,
        p.product_id,
        p.product_name,
        SUM(oi.quantity * oi.unit_price) AS total_sales, -- Сумма продаж
        ROW_NUMBER() OVER ( 
            PARTITION BY p.category_id 
            ORDER BY SUM(oi.quantity * oi.unit_price) DESC 
        ) AS sales_rank
    FROM products p
    JOIN order_items oi ON p.product_id = oi.product_id
    JOIN categories c ON p.category_id = c.category_id
    GROUP BY p.category_id, c.category_name, p.product_id, p.product_name
)
SELECT 
    category_name,
    product_name,
    total_sales,
    sales_rank
FROM sales_ranking
WHERE sales_rank <= 3
ORDER BY category_name, sales_rank;

Как это работает:

  1. CTE sales_ranking: Сначала мы агрегируем данные, чтобы для каждого товара получить общую сумму продаж (total_sales).
  2. Оконная функция ROW_NUMBER():
    • PARTITION BY p.category_id — разбивает все строки на группы ("окна") по категориям. Ранжирование происходит внутри каждой категории отдельно.
    • ORDER BY ... DESC — сортирует товары внутри каждой категории по убыванию продаж.
    • Функция присваивает порядковый номер (sales_rank) начиная с 1 для самого продаваемого товара в категории.
  3. Финальный SELECT: Выбираем только те строки, где ранг не превышает 3.

Выбор между ROW_NUMBER, RANK и DENSE_RANK:

  • ROW_NUMBER(): Всегда дает уникальные последовательные номера (1,2,3...). Если два товара имеют одинаковые продажи, один получит ранг 3, а другой — 4, и в топ-3 попадет только один из них.
  • RANK(): При равенстве продаж присваивает одинаковый ранг, но следующий ранг пропускается (1,2,2,4...). В топ-3 (rank <=3) попадут три позиции, но товары с рангом 2 займут две из них.
  • DENSE_RANK(): При равенстве продаж присваивает одинаковый ранг, но следующий ранг не пропускается (1,2,2,3...). Это гарантирует, что в топ-3 по рангу попадут товары с тремя разными значениями ранга.

Выбор зависит от бизнес-требований: если нужно строго три записи — ROW_NUMBER, если важно показать все лидеры, даже с равными продажами — RANK или DENSE_RANK.