检索每个组中的最后一条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/2IS NULL比较。

我在一个有1182组的18684446行的InnoDB表上做了testing。 该表包含functiontesting的testing结果,并具有(test_id, request_id)作为主键。 因此, test_id是一个组,我正在为每个test_idsearch最后一个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秒