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

How to SELECT the newest four items per category?

提问人:justinl 提问时间:9/18/2009 最后编辑:gung - Reinstate Monicajustinl 更新时间:10/1/2018 访问量:18751

问:

我有一个项目数据库。每个项目都使用类别表中的类别 ID 进行分类。我正在尝试创建一个列出每个类别的页面,在每个类别下方,我想显示该类别中的 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 个电话没什么大不了的,我应该用简单的方法去做。

SQL MySQL 每个组最大 n

评论

0赞 David Andres 9/18/2009
您的类别有多“静态”?它是一个时不时变化的列表还是恒定的?
0赞 justinl 9/18/2009
类别是非常静态的(很少会改变)。除非我添加一个我认为不会发生或非常罕见的类别,否则它们永远不会真正改变
0赞 David Andres 9/18/2009
@justinl:如果它们是静态的,最好使用简单的 UNION 语句。有关示例,请参阅我的答案。
0赞 mjv 9/18/2009
@justinl建议的问题标题:“MySql,A JOIN B:如何限制从 B 开始的 N 行,用于来自 A 的每个 PK?
0赞 Paramvir Singh Karwal 2/1/2022
您可以使用此处介绍的窗口化功能 stackoverflow.com/a/38854846/2723942

答:

-1赞 tster 9/18/2009 #1

不是很漂亮,但是:

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

评论

0赞 justinl 9/18/2009
这需要为每个类别调用,对吧?有没有办法将它们全部分组到 1 个查询中?
0赞 tster 9/18/2009
哎呀,不知道你不能在子查询中做 LIMIT
1赞 Steve McLeod 9/18/2009
另一个问题是:多个图像可能具有相同的date_listed,您最终可能会得到不正确的数据
0赞 Jage 3/20/2010
你可以在子查询中做一个限制,它只需要限制 1 即可。
-2赞 RageZ 9/18/2009 #2

好的,在谷歌搜索后,快速回答是不可能的,至少在MySQL上是不可能的

这个这个线程供参考

也许你应该缓存该查询的结果,如果你害怕使服务器崩溃,并且你希望代码表现得更好

0赞 David Andres 9/18/2009 #3

根据类别的恒定程度,以下是最简单的路线

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赞 justinl 9/18/2009
谢谢大卫。那么,这种将所有查询合并为 1 个大查询的方法是否比执行 33 个单独的查询(每个类别 1 个)更有效?
0赞 David Andres 9/18/2009
是的,可以,如果只是因为您可能正在将 33 个单独的查询作为来自数据库的单独请求进行。其中一些时间只是简单地在数据库服务器之间来回穿梭数据。我还将 UNION 修改为 UNION ALL,它不会检查和删除重复项。无论如何,你可能都不会有。
0赞 justinl 9/18/2009
谢谢。你是对的,我不会有任何重复,因为所有物品都有 PK。此外,似乎我可以通过查询所有类别 ID 来构建查询,然后通过遍历这些结果并将它们组合成一个字符串并使用该字符串作为新查询来构建查询。
0赞 David Andres 9/18/2009
如果这是你想做的。我说何必呢,特别是如果你告诉我类别变化并不经常发生。如果是这种情况,请复制并粘贴。当类别更改时,您可以返回到此查询并进行适当的修改。它不会是自动的,但它会起作用。
0赞 justinl 9/18/2009
我刚刚意识到我不明白在您的查询中如何加入类别。例如,这些 SELECT 语句如何知道 Category 是什么?因为类别 ID 和名称在另一个表中。
5赞 10 revsmjv #4

此解决方案是从另一个 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;

第二次尝试。(工作正常!

为子查询添加 WHERE 子句,强制列出的日期分别为 i1、i2、i3 等的最晚、第二最新、最晚等(并且还允许给定类别 ID 的项少于 4 个时出现 null 情况)。此外,还添加了不相关的过滤子句,以防止显示“已售出”的条目或没有图像的条目(增加了要求)

此逻辑假定没有重复的日期列表值(对于给定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;

现在。。。比较以下内容,我引入了一个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

评论

0赞 justinl 9/18/2009
现在我得到: #1054 - “order 子句”中的未知列“date_listed” 如果我从 ORDER 子句中删除date_listed,它确实有效,但它似乎没有遍历不同的类别,而只是一遍又一遍地列出相同的类别
0赞 justinl 9/18/2009
好的,我弄清楚了date_listed(我只是把它添加到JOIN的子查询中,就像我们对category_id所做的那样)。但返回结果的每一行都显示相同的 categoryName、ID 和图像路径
0赞 justinl 9/18/2009
哈哈,好近。但是返回的行都来自同一类别(即使我在不同的类别中有六个项目)。
0赞 mjv 9/18/2009
实际上,我感觉很糟糕,我让你走上了这条赛道,但设计有一个缺陷。基本上,子查询从笛卡尔积中产生一大堆行,这些行由自连接表示。另一个附带问题,一旦这个问题得到解决,我们就可以解决,正如现在所写的那样,图像表中不可能有任何两条具有相同日期相同category_id的记录......
0赞 mjv 9/18/2009
不用担心我的时间。这有点像一个挑战,加上一个很好的自我检查,当像这样“简单”的东西最终在我的脸上爆炸时......我再给它 30 分钟......
98赞 Bill Karwin 9/18/2009 #5

这是每组最大的 n 个问题,也是一个非常常见的 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_iditem_iditem

它是这样工作的:对于每个项目,还有一些其他较新的项目。例如,有三个项目比第四个最新项目更新。没有比最新项目更新的项目。因此,我们要将每个项目 () 与较新且具有相同类别的项目 () 集进行比较。如果这些新项目的数量少于四个,则是我们包括的项目之一。否则,不要包含它。i1i2i1i1

此解决方案的优点在于,无论您有多少个类别,它都可以工作,并且如果您更改类别,它将继续工作。即使某些类别中的项目数量少于 4 个,它也有效。


另一个有效但依赖于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;

评论

1赞 justinl 9/29/2009
仅供参考:如果要对其他表列进行约束,则必须在ON括号中进行约束,并在GROUP BY上方使用WHERE,例如:ON (i2.active = TRUE) WHERE i1.active = TRUE
1赞 Bill Karwin 7/8/2015
@drake,你是对的。但是,为了查找每个组的前 1 名,还有另一种更有效的查询样式,因为它可以在不使用 GROUP BY 的情况下完成任务。例如,请参阅我在 stackoverflow.com/questions/121387/ 中的答案......
1赞 Bill Karwin 7/9/2015
@drake,根据我的经验,任何差异都非常小。您可以自己进行基准测试以确保。通常,出于逻辑原因,应使用 COUNT(column) - 当您希望计数跳过列为 NULL 的行时。而 COUNT(*) 对所有行进行计数,无论该列是否为 null。
1赞 Bill Karwin 1/16/2018
@Davos: dev.mysql.com/doc/refman/8.0/en/...
1赞 Bill Karwin 5/14/2019
@RaymondNijland,是的,MySQL的AUTO_INCREMENT是一个单调递增的伪键。其他 SQL 实现使用 SEQUENCE、IDENTITY 等术语。
0赞 Ali YILDIRIM 9/18/2009 #6

下面的代码显示了一种在循环中执行此操作的方法 它肯定需要大量的编辑,但我希望它有所帮助。

        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赞 Mark Byers 8/25/2012 #7

在其他数据库中,您可以使用该函数执行此操作。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

其工作原理如下:

  • Intially @prev设置为 NULL,@rn设置为 0。
  • 对于我们看到的每一行,检查category_id是否与上一行相同。
    • 如果是,则递增行号。
    • 否则,请启动一个新类别并将行号重置回 1。
  • 子查询完成后,最后一步是进行筛选,以便仅保留行号小于或等于 4 的行。

评论

0赞 Lukasz Szozda 12/1/2017
幸运的是,MySQL 8.0将支持窗口函数
0赞 rakesh 10/1/2018 #8

最近我遇到了类似的情况,我尝试了一个对我有用的查询,它独立于数据库

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 个 for 循环并检查比这更新的项目是否小于 3