如何选择每个类别的最新四个项目?

jus*_*inl 31 mysql sql greatest-n-per-group

我有一个项目数据库.每个项目都使用类别表中的类别ID进行分类.我正在尝试创建一个列出每个类别的页面,在每个类别下面我想要显示该类别中的4个最新项目.

例如:

宠物用品

img1
img2
img3
img4
Run Code Online (Sandbox Code Playgroud)

宠物食品

img1
img2
img3
img4
Run Code Online (Sandbox Code Playgroud)

我知道我可以通过查询每个类别的数据库轻松解决这个问题,如下所示:

SELECT id FROM category

然后迭代该数据并查询每个类别的数据库以获取最新的项目:

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

我想弄清楚的是,如果我可以使用1个查询并获取所有数据.我有33个类别,所以我想也许这有助于减少对数据库的调用次数.

任何人都知道这是否可行?或者,如果33次通话不是那么大,我应该这么简单.

Bil*_*win 80

这是每组最大的问题,这是一个非常常见的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;
Run Code Online (Sandbox Code Playgroud)

我假设item表的主键是item_id,并且它是一个单调增加的假.也就是说,更大的值item_id对应于更新的行item.

以下是它的工作原理:对于每个项目,还有一些更新的其他项目.例如,有三个项目比第四个最新项目更新.没有比最新项目更新的项目.因此,我们希望将每个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;
Run Code Online (Sandbox Code Playgroud)

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;
Run Code Online (Sandbox Code Playgroud)


mjv*_*mjv 5

这个解决方案是来自另一个SO解决方案的改编,感谢RageZ找到这个相关/类似的问题.

注意

对Justin的用例来说,这个解决方案似乎令人满意.根据您的使用情况,您可能需要在此帖子中查看Bill Karwin或David Andres的解决方案.比尔的解决方案有我的投票!看看为什么,因为我把两个查询放在一起;-)

我的解决方案的好处是它每个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;
Run Code Online (Sandbox Code Playgroud)

第二次尝试. (工作正常!)

为子查询添加了一个WHERE子句,强制列出的日期分别是i1,i2,i3等的最新,第二,最新,等等(并且当允许少于4个项目时允许空案例)给定的类别ID).还添加了不相关的过滤器子句,以防止显示"已售出"的条目或没有图像的条目(添加的要求)

该逻辑假设没有重复的日期列出值(对于给定的category_id).否则,这种情况会产生重复的行. 实际上,所列出日期的使用是Bill解决方案中定义/要求的单调递增主键的使用.

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;
Run Code Online (Sandbox Code Playgroud)

现在......比较以下我介绍item_id键的地方,并使用Bill的解决方案将这些列表提供给"外部"查询.你可以看出为什么比尔的方法更好......

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
Run Code Online (Sandbox Code Playgroud)