Ответ
Для решения этой задачи идеально подходят оконные функции (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;
Как это работает:
- CTE
sales_ranking: Сначала мы агрегируем данные, чтобы для каждого товара получить общую сумму продаж (total_sales). - Оконная функция
ROW_NUMBER():PARTITION BY p.category_id— разбивает все строки на группы ("окна") по категориям. Ранжирование происходит внутри каждой категории отдельно.ORDER BY ... DESC— сортирует товары внутри каждой категории по убыванию продаж.- Функция присваивает порядковый номер (
sales_rank) начиная с 1 для самого продаваемого товара в категории.
- Финальный 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.