检索每组中的最后一条记录 - MySQL
-
19-09-2019 - |
题
这里有张桌子 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
但这看起来效率极低。还有其他方法可以达到相同的结果吗?
解决方案
MySQL 8.0 现在支持窗口函数,就像几乎所有流行的 SQL 实现一样。使用这个标准语法,我们可以编写每组最大n个查询:
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;
以下是我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;
关于性能,一种解决方案可能会更好,具体取决于数据的性质。因此,您应该测试这两个查询,并根据您的数据库使用性能更好的查询。
例如,我有一份副本 StackOverflow 8 月数据转储. 。我将用它来进行基准测试。有 1,114,357 行 Posts
桌子。这是运行在 MySQL 我的 Macbook Pro 2.40GHz 上为 5.0.75。
我将编写一个查询来查找给定用户 ID(我的)的最新帖子。
首先使用该技术 显示 由 @Eric 与 GROUP BY
在子查询中:
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 | <derived2> | 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)
这是我的 DDL Posts
桌子:
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;
其他提示
更新:2017-03-31, 版本 5.7.5 MySQL 的 ONLY_FULL_GROUP_BY 开关默认启用(因此,非确定性 GROUP BY 查询被禁用)。此外,他们更新了 GROUP BY 实现,即使禁用了开关,该解决方案也可能无法按预期工作。需要检查一下。
当组内的项目数相当小时,Bill Karwin 的上述解决方案工作得很好,但是当组相当大时,查询的性能会变得很差,因为该解决方案需要大约 n*n/2 + n/2
仅有的 IS NULL
比较。
我在 InnoDB 表上进行了测试 18684446
行与 1182
组。该表包含功能测试的测试结果,并具有 (test_id, request_id)
作为主键。因此, test_id
是一个团体,我正在寻找最后一个 request_id
对于每个 test_id
.
比尔的解决方案已经在我的 Dell e4310 上运行了几个小时,我不知道它什么时候会完成,即使它在覆盖索引上运行(因此 using index
在解释中)。
我有几个基于相同想法的其他解决方案:
- 如果基础索引是 BTREE 索引(通常是这种情况),则最大的
(group_id, item_value)
对是每个中的最后一个值group_id
, ,这是每个的第一个group_id
如果我们按降序遍历索引; - 如果我们读取索引覆盖的值,则按照索引的顺序读取值;
- 每个索引隐式包含附加到该索引的主键列(即主键位于覆盖索引中)。在下面的解决方案中,我直接对主键进行操作,在您的情况下,您只需要在结果中添加主键列。
- 在许多情况下,在子查询中按所需的顺序收集所需的行 id 并将子查询的结果连接到 id 上要便宜得多。由于对于子查询结果中的每一行,MySQL 将需要基于主键进行一次获取,因此子查询将首先放在连接中,并且行将按照子查询中 id 的顺序输出(如果我们省略显式 ORDER BY用于加入)
MySQL 使用索引的 3 种方式 是一篇很棒的文章,可以帮助您了解一些细节。
解决方案1
这个速度快得令人难以置信,在我的 18M+ 行上大约需要 0.8 秒:
SELECT test_id, MAX(request_id), request_id
FROM testresults
GROUP BY test_id DESC;
如果要将顺序更改为 ASC,请将其放入子查询中,仅返回 ids 并将其用作子查询来连接其余列:
SELECT test_id, request_id
FROM (
SELECT test_id, MAX(request_id), 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 个最大行。
查询的缺点是查询缓存无法缓存其结果。
使用您的子查询返回正确的分组,因为你已经完成了一半。
尝试这种情况:
select
a.*
from
messages a
inner join
(select name, max(id) as maxid from messages group by name) as b on
a.id = b.maxid
如果它不id
你想的最多:
select
a.*
from
messages a
inner join
(select name, max(other_col) as other_col
from messages group by name) as b on
a.name = b.name
and a.other_col = b.other_col
这这样,可避免相关子查询和/或排序在您的子查询,这往往是非常缓慢的/低效的。
我到达一个不同的解决方案,这是获得的ID对于每个组内的最后的讯息,然后利用来自所述第一查询作为参数用于WHERE x IN
构建体的结果从所述消息表中选择:
SELECT id, name, other_columns
FROM messages
WHERE id IN (
SELECT MAX(id)
FROM messages
GROUP BY name
);
我不知道该如何执行相比,一些其他的解决方案,但它壮观工作了我的表3+万行。 (4第二执行与1200+结果)
这应该在MySQL和SQL Server的工作都。的
由子查询拨弄链路解决方案
select * from messages where id in
(select max(id) from messages group by Name)
解决方案通过加入
条件拨弄链路select m1.* from messages m1
left outer join messages m2
on ( m1.id<m2.id and m1.name=m2.name )
where m2.id is null
原因此篇是给仅拨弄链接。 相同的SQL是在其他的答案已经提供。
我尚未与大型数据库进行测试,但我认为这可能是比连接表快:
SELECT *, Max(Id) FROM messages GROUP BY Name
具有相当的速度的一种方法是如下:
SELECT *
FROM messages a
WHERE Id = (SELECT MAX(Id) FROM messages WHERE a.Name = Name)
<强>结果强>
Id Name Other_Columns
3 A A_data_3
5 B B_data_2
6 C C_data_1
下面是我的解决方案:
SELECT
DISTINCT NAME,
MAX(MESSAGES) OVER(PARTITION BY NAME) MESSAGES
FROM MESSAGE;
下面是两个建议。首先,如果MySQL支持ROW_NUMBER(),这是非常简单的:
WITH Ranked AS (
SELECT Id, Name, OtherColumns,
ROW_NUMBER() OVER (
PARTITION BY Name
ORDER BY Id DESC
) AS rk
FROM messages
)
SELECT Id, Name, OtherColumns
FROM messages
WHERE rk = 1;
我的“最后一个”你的意思是最后的编号顺序假设。如果不是这样,相应地更改ORDER BY的ROW_NUMBER()窗口的条款。如果ROW_NUMBER()是不可用的,这是另一种解决方案:
其次,如果没有,这是经常进行的好方法:
SELECT
Id, Name, OtherColumns
FROM messages
WHERE NOT EXISTS (
SELECT * FROM messages as M2
WHERE M2.Name = messages.Name
AND M2.Id > messages.Id
)
换句话说,选择信息在没有后-ID具有相同名称的消息。
这是另一种获取最后相关记录的方法 GROUP_CONCAT
与顺序和 SUBSTRING_INDEX
从列表中选择一条记录
SELECT
`Id`,
`Name`,
SUBSTRING_INDEX(
GROUP_CONCAT(
`Other_Columns`
ORDER BY `Id` DESC
SEPARATOR '||'
),
'||',
1
) Other_Columns
FROM
messages
GROUP BY `Name`
上面的查询将对所有 Other_Columns
那些在同一个 Name
分组并使用 ORDER BY id DESC
将加入所有 Other_Columns
在一个特定的组中,按降序排列,在我的例子中,我使用了提供的分隔符 ||
,使用 SUBSTRING_INDEX
在此列表中将选择第一个
小提琴演示
SELECT
column1,
column2
FROM
table_name
WHERE id IN
(SELECT
MAX(id)
FROM
table_name
GROUP BY column1)
ORDER BY column1 ;
显然有很多得到相同结果不同的方式,你的问题似乎是什么让每个组中的最后结果在MySQL的有效方式。如果您正在使用大量数据的工作,假设你正使用InnoDB与MySQL的甚至的最新版本(如21年5月7日和8.0.4-RC),则有可能不这样做的一个有效的方法。
我们有时需要甚至超过60万行与表做到这点。
在这些例子中,我将使用数据只有大约150万行,其中查询将需要找到数据中的所有组的结果。在我们的实际情况下,我们会经常需要返回回约2000组数据(假设不要求检查非常多的数据)。
我将使用以下表格:
CREATE TABLE temperature(
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
groupID INT UNSIGNED NOT NULL,
recordedTimestamp TIMESTAMP NOT NULL,
recordedValue INT NOT NULL,
INDEX groupIndex(groupID, recordedTimestamp),
PRIMARY KEY (id)
);
CREATE TEMPORARY TABLE selected_group(id INT UNSIGNED NOT NULL, PRIMARY KEY(id));
在温度表被填充有大约150万随机记录,并用100个不同的组。 所述selected_group被填充有那些100个组(在我们的情况下,这通常是对于所有组的小于20%)。
由于这数据是随机它意味着多个行可以具有相同的recordedTimestamps。我们需要的是获得组ID的顺序与上recordedTimestamp每个组中的所有选定的组的列表,如果同组有不止一个匹配行一样,则这些行的最后匹配的ID。
如果假设的MySQL有一个最后的()函数返回值从最后一排特殊的ORDER BY子句那么我们就可以简单地做:
SELECT
last(t1.id) AS id,
t1.groupID,
last(t1.recordedTimestamp) AS recordedTimestamp,
last(t1.recordedValue) AS recordedValue
FROM selected_group g
INNER JOIN temperature t1 ON t1.groupID = g.id
ORDER BY t1.recordedTimestamp, t1.id
GROUP BY t1.groupID;
这将只需要在此情况下检查几个100行,因为它不使用任何正常组BY功能。这将执行0秒,并因此是高度有效的。 需要注意的是,通常在MySQL中,我们将看到一个ORDER BY子句GROUP BY子句但这个ORDER BY子句来确定最后()函数的ORDER以下,如果是该组之后那么这将是订购组。如果没有GROUP BY子句存在,则最后的值将是相同的所有返回的行。
但MySQL没有这个让我们看看它拥有的,证明这些都不是有效的不同的想法。
示例1 强>
SELECT t1.id, t1.groupID, t1.recordedTimestamp, t1.recordedValue
FROM selected_group g
INNER JOIN temperature t1 ON t1.id = (
SELECT t2.id
FROM temperature t2
WHERE t2.groupID = g.id
ORDER BY t2.recordedTimestamp DESC, t2.id DESC
LIMIT 1
);
此检查3009254行和把〜0.859秒上21年5月7日和8.0.4-RC稍长
示例2 强>
SELECT t1.id, t1.groupID, t1.recordedTimestamp, t1.recordedValue
FROM temperature t1
INNER JOIN (
SELECT max(t2.id) AS id
FROM temperature t2
INNER JOIN (
SELECT t3.groupID, max(t3.recordedTimestamp) AS recordedTimestamp
FROM selected_group g
INNER JOIN temperature t3 ON t3.groupID = g.id
GROUP BY t3.groupID
) t4 ON t4.groupID = t2.groupID AND t4.recordedTimestamp = t2.recordedTimestamp
GROUP BY t2.groupID
) t5 ON t5.id = t1.id;
此检查1505331行和把〜1.25秒上21年5月7日和8.0.4-RC稍长
示例3 强>
SELECT t1.id, t1.groupID, t1.recordedTimestamp, t1.recordedValue
FROM temperature t1
WHERE t1.id IN (
SELECT max(t2.id) AS id
FROM temperature t2
INNER JOIN (
SELECT t3.groupID, max(t3.recordedTimestamp) AS recordedTimestamp
FROM selected_group g
INNER JOIN temperature t3 ON t3.groupID = g.id
GROUP BY t3.groupID
) t4 ON t4.groupID = t2.groupID AND t4.recordedTimestamp = t2.recordedTimestamp
GROUP BY t2.groupID
)
ORDER BY t1.groupID;
此检查3009685行和把〜1.95秒上21年5月7日和8.0.4-RC稍长
示例4 强>
SELECT t1.id, t1.groupID, t1.recordedTimestamp, t1.recordedValue
FROM selected_group g
INNER JOIN temperature t1 ON t1.id = (
SELECT max(t2.id)
FROM temperature t2
WHERE t2.groupID = g.id AND t2.recordedTimestamp = (
SELECT max(t3.recordedTimestamp)
FROM temperature t3
WHERE t3.groupID = g.id
)
);
此检查6137810行和把〜2.2秒上21年5月7日和8.0.4-RC稍长
示例5 强>
SELECT t1.id, t1.groupID, t1.recordedTimestamp, t1.recordedValue
FROM (
SELECT
t2.id,
t2.groupID,
t2.recordedTimestamp,
t2.recordedValue,
row_number() OVER (
PARTITION BY t2.groupID ORDER BY t2.recordedTimestamp DESC, t2.id DESC
) AS rowNumber
FROM selected_group g
INNER JOIN temperature t2 ON t2.groupID = g.id
) t1 WHERE t1.rowNumber = 1;
此检查6017808行和把〜上8.0.4-RC4.2秒
示例6 强>
SELECT t1.id, t1.groupID, t1.recordedTimestamp, t1.recordedValue
FROM (
SELECT
last_value(t2.id) OVER w AS id,
t2.groupID,
last_value(t2.recordedTimestamp) OVER w AS recordedTimestamp,
last_value(t2.recordedValue) OVER w AS recordedValue
FROM selected_group g
INNER JOIN temperature t2 ON t2.groupID = g.id
WINDOW w AS (
PARTITION BY t2.groupID
ORDER BY t2.recordedTimestamp, t2.id
RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
)
) t1
GROUP BY t1.groupID;
此检查6017908行和把〜上8.0.4-RC17.5秒
示例7 强>
SELECT t1.id, t1.groupID, t1.recordedTimestamp, t1.recordedValue
FROM selected_group g
INNER JOIN temperature t1 ON t1.groupID = g.id
LEFT JOIN temperature t2
ON t2.groupID = g.id
AND (
t2.recordedTimestamp > t1.recordedTimestamp
OR (t2.recordedTimestamp = t1.recordedTimestamp AND t2.id > t1.id)
)
WHERE t2.id IS NULL
ORDER BY t1.groupID;
这一个正在采取永远所以我不得不杀死它。
尝试这种情况:
SELECT jos_categories.title AS name,
joined .catid,
joined .title,
joined .introtext
FROM jos_categories
INNER JOIN (SELECT *
FROM (SELECT `title`,
catid,
`created`,
introtext
FROM `jos_content`
WHERE `sectionid` = 6
ORDER BY `id` DESC) AS yes
GROUP BY `yes`.`catid` DESC
ORDER BY `yes`.`created` DESC) AS joined
ON( joined.catid = jos_categories.id )
喜@Vijay开发,如果你的表的消息包含的标识这是自动递增的主键,然后获取主键查询应阅读下面的最新记录基础:
SELECT m1.* FROM messages m1 INNER JOIN (SELECT max(Id) as lastmsgId FROM messages GROUP BY Name) m2 ON m1.Id=m2.lastmsgId
可以从这里也采取图。
http://sqlfiddle.com/#!9/ef42b/9
<强>第一溶液强>
SELECT d1.ID,Name,City FROM Demo_User d1
INNER JOIN
(SELECT MAX(ID) AS ID FROM Demo_User GROUP By NAME) AS P ON (d1.ID=P.ID);
<强>第二溶液强>
SELECT * FROM (SELECT * FROM Demo_User ORDER BY ID DESC) AS T GROUP BY NAME ;
我们将看看如何可以在获得最后一个记录的记录了一组通过使用MySQL。例如,如果你有这样的结果集的职位。
id category_id post_title
1 1 Title 1
2 1 Title 2
3 1 Title 3
4 2 Title 4
5 2 Title 5
6 3 Title 6
我希望能够获得每个类别中的最后一个职位是哪个标题3,标题5标题6.要你将使用MySQL集团通过键盘类别获得职位。
select * from posts group by category_id
但结果我们从这个查询得到的回复是。
id category_id post_title
1 1 Title 1
4 2 Title 4
6 3 Title 6
通过将总是在组中返回的第一个记录上的结果集的组。
SELECT id, category_id, post_title
FROM posts
WHERE id IN (
SELECT MAX(id)
FROM posts
GROUP BY category_id
);
这将返回具有最高ID的职位各组
id category_id post_title
3 1 Title 3
5 2 Title 5
6 3 Title 6
有没有我们可以使用此方法在一个表中删除重复什么办法?结果集是基本的唯一记录的集合,因此,如果我们能在结果集中删除所有记录的时候,我们将有效地没有重复?我试过,但MySQL的给了一个1093的错误。
DELETE FROM messages WHERE id NOT IN
(SELECT m1.id
FROM messages m1 LEFT JOIN messages m2
ON (m1.name = m2.name AND m1.id < m2.id)
WHERE m2.id IS NULL)
时有没有办法也许将输出保存到临时变量然后从NOT IN(临时变量)删除? @Bill感谢一个非常有用的解决方案。
编辑:想我找到了解决方案:
DROP TABLE IF EXISTS UniqueIDs;
CREATE Temporary table UniqueIDs (id Int(11));
INSERT INTO UniqueIDs
(SELECT T1.ID FROM Table T1 LEFT JOIN Table T2 ON
(T1.Field1 = T2.Field1 AND T1.Field2 = T2.Field2 #Comparison Fields
AND T1.ID < T2.ID)
WHERE T2.ID IS NULL);
DELETE FROM Table WHERE id NOT IN (SELECT ID FROM UniqueIDs);
在下面的查询将正常工作,按你的问题。
SELECT M1.*
FROM MESSAGES M1,
(
SELECT SUBSTR(Others_data,1,2),MAX(Others_data) AS Max_Others_data
FROM MESSAGES
GROUP BY 1
) M2
WHERE M1.Others_data = M2.Max_Others_data
ORDER BY Others_data;
如果要为每个Name
最后一行,那么也可以把按降序排列的行数由Name
和顺序由Id
每个行组。
<强> QUERY 强>
SELECT t1.Id,
t1.Name,
t1.Other_Columns
FROM
(
SELECT Id,
Name,
Other_Columns,
(
CASE Name WHEN @curA
THEN @curRow := @curRow + 1
ELSE @curRow := 1 AND @curA := Name END
) + 1 AS rn
FROM messages t,
(SELECT @curRow := 0, @curA := '') r
ORDER BY Name,Id DESC
)t1
WHERE t1.rn = 1
ORDER BY t1.Id;
SQL小提琴
如果性能是真的你的关心,你可以引入名为类型BIT的IsLastInGroup
表中的新列。
其设置为true在其上最后的列和每行的插入/更新维护它/删除。写操作会比较慢,但是你将受益于阅读。这取决于你的使用情况,我建议只如果你正在阅读为重点。
所以,你的查询将是这样的:
SELECT * FROM Messages WHERE IsLastInGroup = 1
SELECT * FROM table_name WHERE primary_key IN (SELECT MAX(primary_key) FROM table_name GROUP BY column_name )
select * from messages group by name desc
这样如何:
SELECT DISTINCT ON (name) *
FROM messages
ORDER BY name, id DESC;
我有类似的问题(PostgreSQL的强硬),并在1M记录表。这种解决方案需要1.7S VS由一个与LEFT产生44S JOIN。 在我而言,我不得不对过滤NULL值,你的名称的字段的corrispondant,0.2秒造成甚至更好的性能。
您通过计算可以分组,也得到类似组的最后一个项目:
SELECT
user,
COUNT(user) AS count,
MAX(id) as last
FROM request
GROUP BY user