Как выбрать новейшие четыре элемента в категории? - PullRequest
31 голосов
/ 18 сентября 2009

У меня есть база товаров. Каждый элемент классифицируется с помощью идентификатора категории из таблицы категорий. Я пытаюсь создать страницу, в которой перечислены все категории, а под каждой категорией я хочу показать 4 новых элемента в этой категории.

Например:

Зоотовары

img1
img2
img3
img4

Корм ​​для домашних животных

img1
img2
img3
img4

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

SELECT id FROM category

Затем перебираем эти данные и запрашиваем базу данных для каждой категории, чтобы получить новейшие предметы:

SELECT image FROM item where category_id = :category_id 
ORDER BY date_listed DESC LIMIT 4

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

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

Ответы [ 8 ]

80 голосов
/ 18 сентября 2009

Это самая большая проблема для каждой группы, и это очень распространенный вопрос SQL.

Вот как я решаю это с внешними объединениями:

SELECT i1.*
FROM item i1
LEFT OUTER JOIN item i2
  ON (i1.category_id = i2.category_id AND i1.item_id < i2.item_id)
GROUP BY i1.item_id
HAVING COUNT(*) < 4
ORDER BY category_id, date_listed;

Я предполагаю, что первичный ключ таблицы item равен item_id, и это монотонно увеличивающийся псевдоключ. То есть, большее значение в item_id соответствует более новой строке в item.

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

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


Другое решение, которое работает, но использует функцию пользовательских переменных MySQL:

SELECT *
FROM (
    SELECT i.*, @r := IF(@g = category_id, @r+1, 1) AS rownum, @g := category_id
    FROM (@g:=null, @r:=0) AS _init
    CROSS JOIN item i
    ORDER BY i.category_id, i.date_listed
) AS t
WHERE t.rownum <= 3;

В MySQL 8.0.3 появилась поддержка стандартных оконных функций SQL. Теперь мы можем решить такую ​​проблему, как это делают другие СУБД:

WITH numbered_item AS (
  SELECT *, ROW_NUMBER() OVER (PARTITION BY category_id ORDER BY item_id) AS rownum
  FROM item
)
SELECT * FROM numbered_item WHERE rownum <= 4;
5 голосов
/ 18 сентября 2009

Это решение является адаптацией от другого решения SO , спасибо RageZ за поиск этого связанного / похожего вопроса.

Примечание

Это решение кажется удовлетворительным для случая использования Джастина. В зависимости от вашего варианта использования вы можете проверить решения Билла Карвина или Дэвида Андреса в этой публикации. Решение Билла имеет мой голос! Посмотрите, почему, когда я поставил оба запроса рядом друг с другом; -)

Преимущество моего решения состоит в том, что оно возвращает одну запись для category_id (информация из таблицы элементов "свернута"). Основным недостатком моего решения является недостаточная читаемость и растущая сложность по мере увеличения количества желаемых строк (скажем, 6 строк на категорию, а не 6). Кроме того, это может быть немного медленнее по мере роста количества строк в таблице элементов. (Несмотря на это, все решения будут работать лучше при меньшем количестве подходящих строк в таблице элементов, и поэтому рекомендуется периодически удалять или перемещать более старые элементы и / или устанавливать флаг, чтобы помочь SQL отфильтровывать строки раньше)

Первая попытка (не сработала !!!) ...

Проблема с этим подходом заключалась в том, что подзапрос [по праву, но плохо для нас] выдает очень много строк, основанных на декартовых произведениях, определяемых самосоединениями ...

SELECT id, CategoryName(?), tblFourImages.*
FROM category
JOIN (
    SELECT i1.category_id, i1.image as Image1, i2.image AS Image2, i3.image AS Image3, i4.image AS Image4
    FROM item AS i1
    LEFT JOIN item AS i2 ON i1.category_id = i2.category_id AND i1.date_listed > i2.date_listed
    LEFT JOIN item AS i3 ON i2.category_id = i3.category_id AND i2.date_listed > i3.date_listed
    LEFT JOIN item AS i4 ON i3.category_id = i4.category_id AND i3.date_listed > i4.date_listed
) AS tblFourImages ON tblFourImages.category_id = category.id
--WHERE  here_some_addtional l criteria if needed
ORDER BY id ASC;

Вторая попытка. (работает нормально!)

В подзапросе добавлено предложение WHERE, в котором указанная дата должна быть самой последней, второй последней, самой поздней третей и т. Д. Для i1, i2, i3 и т. Д. Соответственно (и также допускается нулевые случаи, когда их меньше, чем 4 предмета для данной категории). Также были добавлены несвязанные условия фильтра для предотвращения показа «проданных» записей или записей без изображения (добавлены требования)

Эта логика предполагает, что в списке отсутствуют повторяющиеся значения даты (для данного category_id). Такие случаи в противном случае создали бы повторяющиеся строки. По сути, это использование указанной даты с использованием монотонно увеличенного первичного ключа, как определено / требуется в решении Билла.

SELECT id, CategoryName, tblFourImages.*
FROM category
JOIN (
    SELECT i1.category_id, i1.image as Image1, i2.image AS Image2, i3.image AS Image3, i4.image AS Image4, i4.date_listed
    FROM item AS i1
    LEFT JOIN item AS i2 ON i1.category_id = i2.category_id AND i1.date_listed > i2.date_listed AND i2.sold = FALSE AND i2.image IS NOT NULL
          AND i1.sold = FALSE AND i1.image IS NOT NULL
    LEFT JOIN item AS i3 ON i2.category_id = i3.category_id AND i2.date_listed > i3.date_listed AND i3.sold = FALSE AND i3.image IS NOT NULL
    LEFT JOIN item AS i4 ON i3.category_id = i4.category_id AND i3.date_listed > i4.date_listed AND i4.sold = FALSE AND i4.image IS NOT NULL
    WHERE NOT EXISTS (SELECT * FROM item WHERE category_id = i1.category_id AND date_listed > i1.date_listed)
      AND (i2.image IS NULL OR (NOT EXISTS (SELECT * FROM item WHERE category_id = i1.category_id AND date_listed > i2.date_listed AND date_listed <> i1.date_listed)))
      AND (i3.image IS NULL OR (NOT EXISTS (SELECT * FROM item WHERE category_id = i1.category_id AND date_listed > i3.date_listed AND date_listed <> i1.date_listed AND date_listed <> i2.date_listed)))
      AND (i4.image IS NULL OR (NOT EXISTS (SELECT * FROM item WHERE category_id = i1.category_id AND date_listed > i4.date_listed AND date_listed <> i1.date_listed AND date_listed <> i2.date_listed AND date_listed <> i3.date_listed)))
) AS tblFourImages ON tblFourImages.category_id = category.id
--WHERE  --
ORDER BY id ASC;

Теперь ... сравните следующее, где я ввожу ключ item_id и использую решение Билла, чтобы предоставить их список для "внешнего" запроса. Вы можете понять, почему подход Билла лучше ...

SELECT id, CategoryName, image, date_listed, item_id
FROM item I
LEFT OUTER JOIN category C ON C.id = I.category_id
WHERE I.item_id IN 
(
SELECT i1.item_id
FROM item i1
LEFT OUTER JOIN item i2
  ON (i1.category_id = i2.category_id AND i1.item_id < i2.item_id
      AND i1.sold = 'N' AND i2.sold = 'N'
      AND i1.image <> '' AND i2.image <> ''
      )
GROUP BY i1.item_id
HAVING COUNT(*) < 4
)
ORDER BY category_id, item_id DESC
3 голосов
/ 25 августа 2012

В других базах данных вы можете сделать это, используя функцию ROW_NUMBER.

SELECT
    category_id, image, date_listed
FROM
(
    SELECT
        category_id, image, date_listed,
        ROW_NUMBER() OVER (PARTITION BY category_id
                           ORDER BY date_listed DESC) AS rn
    FROM item
) AS T1
WHERE rn <= 4

К сожалению, MySQL не поддерживает функцию ROW_NUMBER, но вы можете эмулировать ее с помощью переменных:

SELECT
    category_id, image, date_listed
FROM
(
    SELECT
        category_id, image, date_listed,
        @rn := IF(@prev = category_id, @rn + 1, 1) AS rn,
        @prev := category_id
    FROM item
    JOIN (SELECT @prev := NULL, @rn = 0) AS vars
    ORDER BY category_id, date_listed DESC
) AS T1
WHERE rn <= 4

Посмотрите, как работает онлайн: sqlfiddle

Работает следующим образом:

  • По умолчанию @prev имеет значение NULL, а @rn равно 0.
  • Для каждой строки, которую мы видим, проверьте, совпадает ли category_id с предыдущей строкой.
    • Если да, увеличить номер строки.
    • В противном случае начать новую категорию и сбросить номер строки обратно в 1.
  • Когда подзапрос завершается, последний шаг заключается в фильтрации, так что сохраняются только строки с номером строки, меньшим или равным 4.
0 голосов
/ 01 октября 2018

Недавно я столкнулся с подобной ситуацией, я попробовал работающий для меня запрос, который не зависит от базы данных

SELECT i.* FROM Item AS i JOIN Category c ON i.category_id=c.id WHERE
(SELECT count(*) FROM Item i1 WHERE 
i1.category_id=i.category_id AND 
i1.date_listed>=i.date_listed) <=3 
ORDER BY category_id,date_listed DESC;

Это эквивалентно выполнению 2 для циклов и проверке, являются ли элементы новее, чем это, меньше 3

0 голосов
/ 18 сентября 2009

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

        declare @RowId int
 declare @CategoryId int
        declare @CategoryName varchar(MAX)

 create table PART (RowId int, CategoryId int, CategoryName varchar)
 create table  NEWESTFOUR(RowId int, CategoryId int, CategoryName varchar, Image image)
        select RowId = ROW_NUMBER(),CategoryId,CategoryName into PART from [Category Table]


        set @PartId = 0
 set @CategoryId = 0 
 while @Part_Id <= --count
 begin
   set @PartId = @PartId + 1
          SELECT @CategoryId = category_id, @CategoryName = category_name from PART where PartId = @Part_Id
          SELECT RowId = @PartId, image,CategoryId = @category_id, CategoryName = @category_name   FROM item into NEWESTFOUR where category_id = :category_id 
ORDER BY date_listed DESC LIMIT 4

 end
 select * from NEWESTFOUR
 drop table NEWESTFOUR
        drop table PART
0 голосов
/ 18 сентября 2009

В зависимости от того, насколько постоянны ваши категории, следующий простейший маршрут

SELECT C.CategoryName, R.Image, R.date_listed
FROM
(
    SELECT CategoryId, Image, date_listed
    FROM 
    (
      SELECT CategoryId, Image, date_listed
      FROM item
      WHERE Category = 'Pet Supplies'
      ORDER BY date_listed DESC LIMIT 4
    ) T

    UNION ALL

    SELECT CategoryId, Image, date_listed
    FROM
    (        
      SELECT CategoryId, Image, date_listed
      FROM item
      WHERE Category = 'Pet Food'
      ORDER BY date_listed DESC LIMIT 4
    ) T
) RecentItemImages R
INNER JOIN Categories C ON C.CategoryId = R.CategoryId
ORDER BY C.CategoryName, R.Image, R.date_listed
0 голосов
/ 18 сентября 2009

хорошо, после поиска в Google быстрый ответ, если это невозможно, по крайней мере, на MySQL

это эта тема для ссылки

возможно, вам следует кэшировать результат этого запроса, если вы боитесь заставить сервер упасть и хотите, чтобы код работал лучше

0 голосов
/ 18 сентября 2009

не очень красиво, но:

SELECT image 
FROM item 
WHERE date_listed IN (SELECT date_listed 
                      FROM item 
                      ORDER BY date_listed DESC LIMIT 4)
...