如何选择每个类别最新的四个项目?
Posted
技术标签:
【中文标题】如何选择每个类别最新的四个项目?【英文标题】:How to SELECT the newest four items per category? 【发布时间】:2010-11-29 09:01:36 【问题描述】:我有一个项目数据库。每个项目都使用类别表中的类别 ID 进行分类。我正在尝试创建一个列出每个类别的页面,并且在每个类别下我想显示该类别中的 4 个最新项目。
例如:
宠物用品
img1
img2
img3
img4
宠物食品
img1
img2
img3
img4
我知道我可以通过像这样查询每个类别的数据库来轻松解决这个问题:
从类别中选择 id
然后遍历该数据并为每个类别查询数据库以获取最新项目:
SELECT image FROM item where category_id = :category_id ORDER BY date_listed DESC LIMIT 4
我想弄清楚我是否可以只使用 1 个查询并获取所有这些数据。我有 33 个类别,所以我认为这可能有助于减少对数据库的调用次数。
有人知道这是否可能吗?或者,如果 33 次通话没什么大不了的,我应该用简单的方法来做。
【问题讨论】:
您的类别有多“静态”?它是一个不时变化的列表还是不变的? 类别是非常静态的(很少会改变)。除非我添加一个我认为不会发生或非常罕见的类别,否则它们永远不会真正改变 @justinl:如果它们是静态的,最好使用简单的 UNION 语句。例如,请参阅我的答案。 @justinl 建议的问题标题:“mysql,A JOIN B:如何限制来自 B 的 N 行,对于来自 A 的每个 PK?” 您可以使用此处解释的窗口功能***.com/a/38854846/2723942 【参考方案1】:这是每组最大 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;
我假设item
表的主键是item_id
,并且它是一个单调递增的伪键。也就是说,item_id
中较大的值对应于 item
中较新的行。
它的工作原理如下:对于每个项目,都有一些其他较新的项目。例如,有三个项目比第四个最新项目新。有零个项目比最新项目更新。因此,我们希望将每个项目 (i1
) 与较新且与 i1
具有相同类别的项目集 (i2
) 进行比较。如果这些较新项目的数量少于四个,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;
【讨论】:
仅供参考:如果您想限制其他表列,您必须在 ON 括号中执行此操作,并在 GROUP BY 上方使用 WHERE 例如:ON (i2.active = TRUE) WHERE i1 .active = TRUE @drake,你说得对。但是为了找到每个组的前 1 个,还有另一种更有效的查询样式,因为它可以在完全不使用 GROUP BY 的情况下完成任务。例如,请参阅我在 ***.com/questions/121387/… 中的回答 @drake,根据我的经验,任何差异都非常微小。您可以自己进行基准测试以确定。通常,出于逻辑原因,您应该使用 COUNT(column) - 当您希望计数跳过列为 NULL 的行时。而 COUNT(*) 计算所有行,无论该列是否为空。 @Davos: dev.mysql.com/doc/refman/8.0/en/… @RaymondNijland,是的,MySQL 的 AUTO_INCREMENT 是一个单调递增的伪键。其他 SQL 实现使用 SEQUENCE、IDENTITY 等术语。【参考方案2】:此解决方案改编自 another SO solution,感谢 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 个项目)。还添加了不相关的过滤器子句,以防止显示“已售出”的条目或没有图像的条目(添加要求)
此逻辑假设没有重复的日期列出值(对于给定的 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 键并使用 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
【讨论】:
现在我得到:#1054 - 'order 子句'中的未知列 'date_listed' 如果我从 ORDER 子句中删除 date_listed 它确实有效,但它似乎没有遍历不同的类别,但是而是一遍又一遍地列出相同的类别 好的,我知道了 date_listed(我只是像使用 category_id 一样将它添加到 JOIN 的子查询中)。但是返回结果的每一行都显示了相同的 categoryName、ID 和图像路径 哈哈它是如此接近。但返回的行都来自同一类别(即使我有六个不同类别的项目)。 其实我感觉很糟糕,我让你走上了这条赛道,但设计存在缺陷。基本上,子查询从自连接表示的笛卡尔积中产生 [对我们来说是正确但不利的] 一大堆行。另一个问题,一旦解决了这个问题,我们就可以解决,就像现在写的那样,图像表中不可能有任何两条记录具有相同的日期和相同的 category_id... 不用担心我的时间。这有点像一个挑战,加上一个很好的自我检查,当像这样“简单”的东西最终在我脸上炸开时......我再给它 30 分钟......【参考方案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 的行。【讨论】:
幸运的是 MySQL 8.0 将支持windowed functions【参考方案4】:根据您的类别的恒定程度,以下是最简单的路线
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
【讨论】:
谢谢大卫。那么这种将所有查询组合成 1 个大查询的方法是否比执行 33 个单独的查询(每个类别 1 个)更有效? 是的,如果只是因为您可能将 33 个单独的查询作为来自数据库的单独请求执行,那么它可以是。其中一些时间用于简单地将数据往返于数据库服务器之间。我还将 UNION 修改为 UNION ALL,它不会检查和删除重复项。在任何情况下你可能都没有。 谢谢。你是对的,我不会有任何重复,因为所有项目都有一个 PK。此外,似乎我可以通过查询所有类别 ID 来构建查询,然后通过迭代这些结果并将它们组合成一个字符串并将该字符串用作新查询来构建一个查询。 如果你想这样做。我说为什么要麻烦,特别是如果您告诉我类别更改不经常发生。如果是这种情况,请复制并粘贴。当类别发生变化时,您可以返回此查询并进行适当的修改。它不会是自动的,但它会起作用。 我刚刚意识到在您的查询中我不明白如何加入类别。例如。这些 SELECT 语句如何知道 Category 是什么?因为类别 ID 和名称在另一个表中。【参考方案5】:下面的代码显示了一种在循环中执行此操作的方法 它确实需要大量编辑,但我希望它有所帮助。
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
【讨论】:
【参考方案6】:最近我遇到了类似的情况,我尝试了一个对我有用的查询,它独立于数据库
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
【讨论】:
【参考方案7】:不是很漂亮但是:
SELECT image
FROM item
WHERE date_listed IN (SELECT date_listed
FROM item
ORDER BY date_listed DESC LIMIT 4)
【讨论】:
这需要为每个类别调用,对吧?有没有办法将它全部分组到 1 个查询中? 哎呀,不知道你不能在子查询中做 LIMIT 另一个问题:多个图像可能具有相同的 date_listed,您最终可能会得到不正确的数据 你可以在子查询中做一个限制,它只需要限制为 1。【参考方案8】:好的,在谷歌搜索后快速回答是否至少在 mysql 上是不可能的
这是reference的这个帖子
如果您害怕让服务器崩溃并且希望代码执行得更好,也许您应该缓存该查询的结果
【讨论】:
以上是关于如何选择每个类别最新的四个项目?的主要内容,如果未能解决你的问题,请参考以下文章