2009-09-18 4 views
25

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

Для примера:

зоотоваров

img1 
img2 
img3 
img4 

Pet Food

img1 
img2 
img3 
img4 

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

SELECT id FROM category

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

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

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

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

+1

Я должен копать свою книгу SQL ^^ – RageZ

+0

Как «статические» являются вашими категориями? Это список, который меняется время от времени или постоянный? –

+0

ха-ха. рыть копать! : D – justinl

ответ

69

Это проблема величайшая-н-за группу, и это очень распространенный 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; 

Я предполагаю, что первичный ключ таблицы itemitem_id, и что это монотонно возрастающая pseudokey. То есть большее значение в 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. Теперь мы можем решить такие проблемы, как другое RDBMS сделать:

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; 
+0

Привет, Билл. Спасибо, что отлично работает! Что-то, о чем я не упоминал в вопросе, заключается в том, что я хочу только вернуть результаты с изображением, если изображение пустым, тогда я не хочу, чтобы он возвращался. Как я могу поместить предложение WHERE там, чтобы оно не возвращало изображение, если item.image = '' – justinl

+0

А я просто понял это. Я поставил «WHERE image <>» над строкой GROUP BY – justinl

+1

FYI: Если вы хотите ограничить другие столбцы таблицы, вы должны сделать это в скобках ON и использовать WHERE чуть выше GROUP BY , например: ON (i2.active = TRUE) WHERE i1.active = TRUE – justinl

0

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

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

Не работает mysql не поддерживает LIMIT в подзапросе – RageZ

+0

Это нужно будет вызывать для каждой категории, правильно? Есть ли способ сгруппировать все это в один запрос? – justinl

+0

oops, не знал, что вы не можете сделать LIMIT в подзапросе – tster

-1

ОК после того, как прибегая к помощи быстрого ответа бы это не представляется возможным, по крайней мере на MySQL

это эта нить для reference

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

0

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

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

Спасибо David. Таким образом, этот способ объединения всех запросов в один большой запрос более эффективен, чем выполнение 33 отдельных запросов (по 1 для каждой категории)? – justinl

+0

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

+0

Спасибо. Вы правы, что у меня не будет дубликатов, потому что у всех предметов есть ПК. Также кажется, что я могу просто построить запрос, запросив все идентификаторы категорий, а затем построив запрос, итерации по этим результатам и объединения их в строку и использование этой строки в качестве нового запроса. – justinl

5

Это решение является адаптацией от another SO solution, спасибо RageZ для размещения этой связанной/аналогичный вопрос.

ПРИМЕЧАНИЕ

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

Преимущество моего решения в том, что оно возвращает одну запись для категории_ид (информация из таблицы элементов «свернута»). Основным недостатком моего решения является отсутствие читаемости и растущая сложность, так как количество желаемых строк растет (скажем, 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 и т. Д. Соответственно (а также для null случаях, когда для данного идентификатора категории имеется менее 4 элементов). Кроме того, были добавлены несвязанные предложения фильтра, чтобы запретить показ записей, которые «проданы» или записи, которые не имеют изображения (добавленные требования)

В этой логике делается предположение, что нет нумерованных значений даты (для данной категории_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 
+0

Теперь я получаю: # 1054 - Неизвестный столбец 'date_listed' in 'order clause' Если я удалю date_listed из предложения ORDER, он действительно работает, но, похоже, он не перебирает разные категории, а вместо этого просто перечисляет та же категория снова и снова – justinl

+0

Хорошо, я получил date_listed понял (я просто добавил его в подзапрос JOIN, как мы сделали с category_id). Но каждая строка возвращаемого результата показывает ту же категориюName, ID и путь изображения – justinl

+0

haha, это так близко. но возвращаемые строки относятся к одной категории (хотя у меня есть полдюжины элементов в разных категориях). – justinl

0

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

 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 
3

В других базах данных вы можете сделать это, используя функцию 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

К счастью, MySQL 8.0 будет поддерживать [оконные функции] (https: // dev.mysql.com/doc/refman/8.0/en/window-functions-usage.html) – lad2025

Смежные вопросы