检索每个组中的最后一条logging
有一个表格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
也就是说,每个组中的最后一个logging应该被返回。
目前,这是我使用的查询:
select * from (select * from messages ORDER BY id DESC) AS x GROUP BY name
但是这看起来非常低效。 任何其他方式来达到相同的结果?
我这样写解决scheme:
SELECT m1.* FROM messages m1 LEFT JOIN messages m2 ON (m1.name = m2.name AND m1.id < m2.id) WHERE m2.id IS NULL;
关于性能,根据数据的性质,一种解决scheme或另一种解决scheme可能会更好。 所以你应该testing两个查询,并使用性能更好的数据库。
例如,我有一个StackOverflow八月数据转储的副本。 我会用它作为基准。 在Posts
表中有1,114,357行。 这是在我的Macbook Pro 2.40GHz的MySQL 5.0.75上运行。
我将编写一个查询来查找给定用户ID(我的)的最新post。
首先在子查询中使用@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)
即使是解释分析也需要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)
这是我的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;
UPD:2017-03-31,MySQL的5.7.5版本默认启用了ONLY_FULL_GROUP_BY开关(因此,非确定性GROUP BY查询被禁用)。 而且,他们更新了GROUP BY实现,即使使用禁用的开关,解决scheme也可能无法按预期工作。 一个需要检查。
上面的Bill Karwin的解决scheme在组内的项数很小时工作正常,但是当组比较大时,查询性能变差,因为解决scheme仅需要大约n*n/2 + n/2
个IS NULL
比较。
我在一个有1182
组的18684446
行的InnoDB表上做了testing。 该表包含functiontesting的testing结果,并具有(test_id, request_id)
作为主键。 因此, test_id
是一个组,我正在为每个test_id
search最后一个request_id
。
比尔的解决scheme已经在我的戴尔e4310上运行了好几个小时,我不知道什么时候它会完成,即使它在一个覆盖索引上运行(因此在EXPLAIN中using index
)。
我有一些基于相同想法的其他解决scheme:
- 如果底层索引是BTREE索引(通常是这种情况),那么最大
(group_id, item_value)
对是每个group_id
的最后一个值,如果我们按降序浏览索引,那么这是每个group_id
第一个值; - 如果我们读取索引所涵盖的值,则按照索引的顺序读取值;
- 每个索引都隐式地包含附加到该索引的主键列(即主键位于coverage索引中)。 在下面的解决scheme中,我直接操作主键,在这种情况下,您只需要在结果中添加主键列。
- 在许多情况下,在子查询中以所需的顺序收集所需的行标识并在ID上join子查询的结果要便宜得多。 由于对于子查询中的每一行结果,MySQL将需要基于主键的单次获取,子查询将首先放入连接,并且这些行将以子查询中ID的顺序输出(如果我们省略显式ORDER BY为join)
MySQL使用索引的3种方式是理解一些细节的好文章。
解决scheme1
这个速度非常快,我的18M +行大概需要0.8秒。
SELECT test_id, MAX(request_id), request_id FROM testresults GROUP BY test_id DESC;
如果要将订单更改为ASC,请将其置于子查询中,仅返回id并将其作为子查询join其余列:
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秒。
解决scheme2
这是另一个解决scheme,我的表需要大约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
它也以降序的方式返回testing。 由于它进行了完整的索引扫描,速度要慢得多,但是在这里给你一个想法,如何为每个组输出N最大行。
查询的缺点是它的结果不能被查询cachingcaching。
使用你的子查询返回正确的分组,因为你在那里。
尝试这个:
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
如果不是你想要的最大的:
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
这样,您就避免了子查询中的相关子查询和/或sorting,这往往是非常缓慢/低效的。
我得到了一个不同的解决scheme,即获取每个组中最后一篇文章的ID,然后使用第一个查询的结果作为WHERE x IN
构造的参数从消息表中select:
SELECT id, name, other_columns FROM messages WHERE id IN ( SELECT MAX(id) FROM messages GROUP BY name );
我不知道如何执行相比其他解决scheme,但它为我的表超过300万行的壮观工作。 (4次执行1200+结果)
这应该在MySQL和SQL Server上都能工作。
解决scheme通过子查询提琴链接
select * from messages where id in (select max(id) from messages group by Name)
解决scheme通过连接条件小提琴链接
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。
我还没有testing大数据库,但我认为这可能比连接表更快:
SELECT *, Max(Id) FROM messages GROUP BY Name
这里有两个build议。 首先,如果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;
我假设你的意思是最后一个“最后”的Id顺序。 如果不是,则相应地更改ROW_NUMBER()窗口的ORDER BY子句。 如果ROW_NUMBER()不可用,这是另一个解决scheme:
其次,如果没有,这往往是一个好的方法来进行:
SELECT Id, Name, OtherColumns FROM messages WHERE NOT EXISTS ( SELECT * FROM messages as M2 WHERE M2.Name = messages.Name AND M2.Id > messages.Id )
换句话说,select不存在具有相同名称的稍后Id消息的消息。
SELECT column1, column2 FROM table_name WHERE id IN (SELECT MAX(id) FROM table_name GROUP BY column1) ORDER BY column1 ;
这是我的解决scheme:
SELECT DISTINCT NAME, MAX(MESSAGES) OVER(PARTITION BY NAME) MESSAGES FROM MESSAGE;
尝试这个:
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 )
这是另一种使用GROUP_CONCAT
获取最后一个相关logging的方法,通过和SUBSTRING_INDEX
从列表中select一个logging
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
将join所有Other_Columns
在一个特定的组中的降序与提供的分隔符在我的情况下,我已经使用||
,在这个列表上使用SUBSTRING_INDEX
将会select第一个
小提琴演示
您也可以从这里观看。
http://sqlfiddle.com/#!9/ef42b/9
第一个解决scheme
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);
第二个解决scheme
SELECT * FROM (SELECT * FROM Demo_User ORDER BY ID DESC) AS T GROUP BY NAME ;
有什么办法可以使用这种方法来删除表中的重复项? 结果集基本上是唯一logging的集合,所以如果我们可以删除不在结果集中的所有logging,那么我们实际上将没有重复logging? 我试过这个,但是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)
有没有办法可能将输出保存到一个临时variables,然后从NOT IN(临时variables)删除? @ Bill感谢一个非常有用的解决scheme。
编辑:认为我find了解决办法:
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;
嗨@Vijay开发如果您的表消息包含Id是自动递增主键然后获取主键上的最新logging基础您的查询应该阅读如下:
SELECT m1.* FROM messages m1 INNER JOIN (SELECT max(Id) as lastmsgId FROM messages GROUP BY Name) m2 ON m1.Id=m2.lastmsgId
如果您想为每个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小提琴
select * from messages group by name desc
这个怎么样:
SELECT DISTINCT ON (name) * FROM messages ORDER BY name, id DESC;
我有类似的问题(在Postgresql艰难)和1Mlogging表。 这个解决scheme需要1.7秒,而使用LEFT JOIN生成44秒。 在我的情况下,我不得不筛选名称字段的对应值为空值,导致更好的performance0.2秒