35

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

例如:

宠物用品

img1
img2
img3
img4

宠物食品

img1
img2
img3
img4

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

从类别中选择 id

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

从 category_id = :category_id 的项目中选择图像
ORDER BY date_listed DESC LIMIT 4

我想弄清楚的是我是否可以只使用 1 个查询并获取所有这些数据。我有 33 个类别,所以我认为这可能有助于减少对数据库的调用次数。

有谁知道这是否可能?或者,如果 33 个电话不是什么大不了的事,我应该用简单的方法来做。

4

8 回答 8

92

这是每组最大 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) 与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;
于 2009-09-18T06:31:37.553 回答
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;

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

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

此逻辑假设没有重复的日期列出值(对于给定的 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
于 2009-09-18T04:32:49.150 回答
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 的行。
于 2012-08-24T23:15:07.313 回答
0

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

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
于 2009-09-18T04:12:29.513 回答
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
于 2009-09-18T10:43:53.590 回答
0

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

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

于 2018-10-01T10:57:57.050 回答
-1

不是很漂亮但是:

SELECT image 
FROM item 
WHERE date_listed IN (SELECT date_listed 
                      FROM item 
                      ORDER BY date_listed DESC LIMIT 4)
于 2009-09-18T04:06:02.677 回答
-2

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

这个这个线程供参考

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

于 2009-09-18T04:08:18.607 回答