检索每个组中的最后一条记录 - MySQL

19 浏览
0 Comments

检索每个组中的最后一条记录 - MySQL

有一张表messages,其中含有以下数据:

Id   Name   Other_Columns
-------------------------
1    A       A_data_1
2    A       A_data_2
3    A       A_data_3
4    B       B_data_1
5    B       B_data_2
6    C       C_data_1

如果我运行一个查询select * from messages group by name,我将得到以下结果:

1    A       A_data_1
4    B       B_data_1
6    C       C_data_1

什么查询语句会返回以下结果?

3    A       A_data_3
5    B       B_data_2
6    C       C_data_1

也就是说,每个分组中的最后一条记录应被返回。

目前,我使用的是以下查询语句:

SELECT
  *
FROM (SELECT
  *
FROM messages
ORDER BY id DESC) AS x
GROUP BY name

但这看起来非常低效。还有其他方法可以实现同样的结果吗?

admin 更改状态以发布 2023年5月21日
0
0 Comments

更新:2017年3月31日,MySQL版本5.7.5将ONLY_FULL_GROUP_BY开关默认启用(因此,非确定性GROUP BY查询将被禁用)。此外,他们更新了GROUP BY实现,即使禁用该开关,解决方案也可能无法按预期工作。需要进行检查。

Bill Karwin的解决方案适用于分组中的条目计数较小的情况,但是当分组较大时,查询的性能变差,因为该解决方案需要约n * n / 2 + n / 2个仅为IS NULL的比较。

我在一个包含18684446行和1182个组的InnoDB表格上进行了测试。该表格包含功能测试的测试结果,并具有(test_id,request_id)作为主键。因此,test_id是一个组,我正在搜索每个test_id的最后request_id。

Bill的解决方案已经在我的dell e4310上运行了几个小时,尽管它在覆盖索引上操作(因此在EXPLAIN中使用索引)我也不知道它要何时结束。

我有几个其他解决方案都基于同样的思路:

- 如果底层索引是BTREE索引(通常是这种情况),最大的(group_id,item_value)对是每个group_id中的最后一个值,如果我们以降序遍历索引,则是每个group_id的第一个值。
- 如果我们读取由索引涵盖的值,则按索引的顺序读取值。
- 每个索引都隐式包含附加到该索引的主键列(也就是主键在覆盖索引中)。在下面的解决方案中,我直接操作主键,在您的情况下,您只需将主键列添加到结果中即可。
- 在许多情况下,以子查询形式按所需顺序收集所需的行ID,并将子查询的结果与ID上的连接。由于MySQL在子查询结果中的每一行都将需要基于主键进行单个获取,因此子查询将首先放在连接中,并且行将按子查询中ID的顺序输出(如果我们省略连接的显式ORDER BY)。

“MySQL使用索引的三种方法”是一篇很棒的文章,可以了解一些细节。

解决方案1

这个方案非常快速,对于我18M+行的数据,花费约0.8秒

SELECT test_id, MAX(request_id) AS request_id
FROM testresults
GROUP BY test_id DESC;

如果你想将顺序改为升序,将它放在一个子查询中,仅返回id并将其作为子查询来连接其余的列:

SELECT test_id, request_id
FROM (
    SELECT test_id, MAX(request_id) AS request_id
    FROM testresults
    GROUP BY test_id DESC) as ids
ORDER BY test_id;

这个方案在我的数据中花费约1.2秒。

解决方案2

这里是另一种解决方案,对于我的表格,需要约19秒:

SELECT test_id, request_id
FROM testresults, (SELECT @group:=NULL) as init
WHERE IF(IFNULL(@group, -1)=@group:=test_id, 0, 1)
ORDER BY test_id DESC, request_id DESC

它也按降序返回测试。它要慢得多,因为它进行了完全索引扫描,但它可以为您提供关于如何输出每个组的N个最大行的想法。

查询的缺点是它的结果无法由查询缓存进行缓存。

0
0 Comments

MySQL 8.0现在支持窗口函数,就像几乎所有流行的SQL实现一样。使用这种标准语法,我们可以写出最大项-n-per-group查询:

WITH ranked_messages AS (
  SELECT m.*, ROW_NUMBER() OVER (PARTITION BY name ORDER BY id DESC) AS rn
  FROM messages AS m
)
SELECT * FROM ranked_messages WHERE rn = 1;

MySQL手册中说明了寻找组内最大行的此方法和其他方法。

以下是我在2009年为这个问题撰写的原始答案:


我这样写解决方案:

SELECT m1.*
FROM messages m1 LEFT JOIN messages m2
 ON (m1.name = m2.name AND m1.id < m2.id)
WHERE m2.id IS NULL;

关于性能,一个解决方案或另一个解决方案可能更好,这取决于您的数据性质。因此,您应该测试两个查询并使用在数据库中表现更好的一个。

例如,我有一个2018年8月份的StackOverflow数据转储文件。我将用它来做基准测试。在Posts表中有1,114,357行。这在我的Macbook Pro 2.40GHz上运行MySQL 5.0.75。

我将编写一个查询来查找给定用户ID(我的)的最新帖子。

首先使用包含子查询中的GROUP BY的技术,由@Eric 展示

SELECT p1.postid
FROM Posts p1
INNER JOIN (SELECT pi.owneruserid, MAX(pi.postid) AS maxpostid
            FROM Posts pi GROUP BY pi.owneruserid) p2
  ON (p1.postid = p2.maxpostid)
WHERE p1.owneruserid = 20860;
1 row in set (1 min 17.89 sec)

即使是EXPLAIN分析也需要16秒以上:

+----+-------------+------------+--------+----------------------------+-------------+---------+--------------+---------+-------------+
| id | select_type | table      | type   | possible_keys              | key         | key_len | ref          | rows    | Extra       |
+----+-------------+------------+--------+----------------------------+-------------+---------+--------------+---------+-------------+
|  1 | PRIMARY     |  | ALL    | NULL                       | NULL        | NULL    | NULL         |   76756 |             | 
|  1 | PRIMARY     | p1         | eq_ref | PRIMARY,PostId,OwnerUserId | PRIMARY     | 8       | p2.maxpostid |       1 | Using where | 
|  2 | DERIVED     | pi         | index  | NULL                       | OwnerUserId | 8       | NULL         | 1151268 | Using index | 
+----+-------------+------------+--------+----------------------------+-------------+---------+--------------+---------+-------------+
3 rows in set (16.09 sec)

现在使用 我提出的方法LEFT JOIN 产生相同的查询结果:

SELECT p1.postid
FROM Posts p1 LEFT JOIN posts p2
  ON (p1.owneruserid = p2.owneruserid AND p1.postid < p2.postid)
WHERE p2.postid IS NULL AND p1.owneruserid = 20860;
1 row in set (0.28 sec)

EXPLAIN 分析显示两个表都可以使用它们的索引:

+----+-------------+-------+------+----------------------------+-------------+---------+-------+------+--------------------------------------+
| id | select_type | table | type | possible_keys              | key         | key_len | ref   | rows | Extra                                |
+----+-------------+-------+------+----------------------------+-------------+---------+-------+------+--------------------------------------+
|  1 | SIMPLE      | p1    | ref  | OwnerUserId                | OwnerUserId | 8       | const | 1384 | Using index                          | 
|  1 | SIMPLE      | p2    | ref  | PRIMARY,PostId,OwnerUserId | OwnerUserId | 8       | const | 1384 | Using where; Using index; Not exists | 
+----+-------------+-------+------+----------------------------+-------------+---------+-------+------+--------------------------------------+
2 rows in set (0.00 sec)


这是我的 Posts 表的DDL:

CREATE TABLE `posts` (
  `PostId` bigint(20) unsigned NOT NULL auto_increment,
  `PostTypeId` bigint(20) unsigned NOT NULL,
  `AcceptedAnswerId` bigint(20) unsigned default NULL,
  `ParentId` bigint(20) unsigned default NULL,
  `CreationDate` datetime NOT NULL,
  `Score` int(11) NOT NULL default '0',
  `ViewCount` int(11) NOT NULL default '0',
  `Body` text NOT NULL,
  `OwnerUserId` bigint(20) unsigned NOT NULL,
  `OwnerDisplayName` varchar(40) default NULL,
  `LastEditorUserId` bigint(20) unsigned default NULL,
  `LastEditDate` datetime default NULL,
  `LastActivityDate` datetime default NULL,
  `Title` varchar(250) NOT NULL default '',
  `Tags` varchar(150) NOT NULL default '',
  `AnswerCount` int(11) NOT NULL default '0',
  `CommentCount` int(11) NOT NULL default '0',
  `FavoriteCount` int(11) NOT NULL default '0',
  `ClosedDate` datetime default NULL,
  PRIMARY KEY  (`PostId`),
  UNIQUE KEY `PostId` (`PostId`),
  KEY `PostTypeId` (`PostTypeId`),
  KEY `AcceptedAnswerId` (`AcceptedAnswerId`),
  KEY `OwnerUserId` (`OwnerUserId`),
  KEY `LastEditorUserId` (`LastEditorUserId`),
  KEY `ParentId` (`ParentId`),
  CONSTRAINT `posts_ibfk_1` FOREIGN KEY (`PostTypeId`) REFERENCES `posttypes` (`PostTypeId`)
) ENGINE=InnoDB;


注:如果您想使用不同版本的MySQL、不同的数据集或不同的表设计进行另一个基准测试,请自行进行。我已经展示了上面的技巧。Stack Overflow在这里是为了向您展示如何进行软件开发工作,而不是为您做所有的工作。

0