Top N на группу с несколькими столами

Основываясь на моих исследованиях, это очень распространенная проблема, которая обычно имеет довольно простое решение. Моя задача - изменить несколько запросов, чтобы получить все результаты, чтобы получить 3 лучших за группу. Сначала это было хорошо, и я использовал несколько рекомендаций и ответов с этого сайта для достижения этой цели (наиболее просматриваемые продукты). Тем не менее, я столкнулся с трудностями с моей последней "Лучшей продаваемой продукцией" из-за нескольких объединений.

В принципе, мне нужно получить все продукты по порядку по # наивысшим продажам на продукт, в котором максимальные продукты для каждого поставщика 3. У меня есть несколько таблиц, которые соединяются для создания исходного запроса, и каждый раз, когда я пытаюсь использовать переменные для генерации ранжирования, он приводит к недействительным результатам. Следующее должно помочь лучше понять проблему (я сократил ненужные поля для краткости):

Таблица продуктов

productid | vendorid | approved | active | deleted

Таблица поставщиков

vendorid | approved | active | deleted

Таблица заказов

orderid | `status` | deleted

Таблица позиций заказов

orderitemid | orderid | productid | price

Теперь мой первоначальный запрос для получения всех результатов выглядит следующим образом:

SELECT COUNT(oi.price) AS `NumSales`, 
       p.productid, 
       p.vendorid
FROM products p
INNER JOIN vendors v ON (p.vendorid = v.vendorid)
INNER JOIN orders_items oi ON (p.productid = oi.productid)
INNER JOIN orders o ON (oi.orderid = o.orderid)
WHERE (p.Approved = 1 AND p.Active = 1 AND p.Deleted = 0)
AND (v.Approved = 1 AND v.Active = 1 AND v.Deleted = 0)
AND o.`Status` = 'SETTLED'
AND o.Deleted = 0
GROUP BY oi.productid
ORDER BY COUNT(oi.price) DESC
LIMIT 100;

Наконец, (и здесь, где я в тупике), я пытаюсь изменить вышеприведенное утверждение таким образом, что я получил только 3-й продукт (по # продано) для каждого поставщика. Я бы добавил, что у меня есть до сих пор, но я смущен, чтобы сделать это, и этот вопрос уже является стеной текста. Я пробовал переменные, но получаю недопустимые результаты. Любая помощь будет принята с благодарностью.

Ответ 1

Даже если вы укажете LIMIT 100, для этого типа запроса потребуется полное сканирование и таблица, которая будет создана, тогда каждая проверенная запись и строка пронумерованы до окончательной фильтрации для 100, которые вы хотите отобразить.

select
    vendorid, productid, NumSales
from
(
    select
        vendorid, productid, NumSales,
        @r := IF(@g=vendorid,@r+1,1) RowNum,
        @g := vendorid
    from (select @g:=null) initvars
    CROSS JOIN 
    (
        SELECT COUNT(oi.price) AS NumSales, 
               p.productid, 
               p.vendorid
        FROM products p
        INNER JOIN vendors v ON (p.vendorid = v.vendorid)
        INNER JOIN orders_items oi ON (p.productid = oi.productid)
        INNER JOIN orders o ON (oi.orderid = o.orderid)
        WHERE (p.Approved = 1 AND p.Active = 1 AND p.Deleted = 0)
        AND (v.Approved = 1 AND v.Active = 1 AND v.Deleted = 0)
        AND o.`Status` = 'SETTLED'
        AND o.Deleted = 0
        GROUP BY p.vendorid, p.productid
        ORDER BY p.vendorid, NumSales DESC
    ) T
) U
WHERE RowNum <= 3
ORDER BY NumSales DESC
LIMIT 100;

Подход здесь

  • Группируйте, чтобы получить NumSales
  • Использовать переменные для номера строк в продажах для каждого поставщика/продукта
  • Отфильтруйте нумерованный набор данных, чтобы разрешить максимум 3 на одного поставщика
  • Закажите оставшиеся номера NumSales DESC и верните только 100

Ответ 2

Мне нравится это элегантное решение, однако, когда я запускаю адаптированный, но похожий запрос на моей машине dev, я получаю возвращаемый результат без детерминированного результата. Я считаю, что это связано с тем, как оптимизатор MySql имеет дело с назначением и чтением пользовательских переменных внутри одного и того же оператора.

Из документы:

Как правило, вы никогда не должны присваивать значение переменной пользователя и читать значение в пределах одного и того же оператора. Вы можете получить ожидаемые результаты, но это не гарантируется. Порядок оценки выражений с пользовательскими переменными равен undefined и может изменяться на основе элементов, содержащихся в данном операторе; кроме того, этот порядок не гарантируется одинаковым между версиями MySQL Server.

Просто добавьте эту заметку здесь, если кто-то столкнется с этим странным поведением.

Ответ 3

Ответ, данный @RichardTheKiwi, отлично поработал и получил мне 99% пути! Я использую MySQL и получаю только первую строку каждой группы, отмеченную номером строки, а остальные строки остаются NULL. Это привело к тому, что запрос возвращал только верхний хит для каждой группы, а не первые три строки. Чтобы исправить это, мне пришлось инициализировать @r в подзапросе initvars. Я изменил,

from (select @g:=null) initvars

to

from (select @g:=null, @r:=null) initvars

Вы также можете инициализировать @r до 0, и он будет работать одинаково. И для тех, кто менее знаком с этим типом синтаксиса, дополнительный раздел читает каждую отсортированную группу, и если строка имеет тот же vendorid, что и предыдущая строка, которая отслеживается с помощью переменной @g, она увеличивает номер строки, который хранится в переменной @r. Когда этот процесс достигнет следующей группы с новым vendorid, оператор IF больше не будет считаться истинным, а переменная @r (и, следовательно, RowNum) будет reset до 1.