Vij*_*Dev 887 mysql sql group-by greatest-n-per-group
有一个表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
Run Code Online (Sandbox Code Playgroud)
如果我运行查询select * from messages group by name,我会得到如下结果:
1 A A_data_1
4 B B_data_1
6 C C_data_1
Run Code Online (Sandbox Code Playgroud)
什么查询将返回以下结果?
3 A A_data_3
5 B B_data_2
6 C C_data_1
Run Code Online (Sandbox Code Playgroud)
也就是说,应返回每组中的最后一条记录.
目前,这是我使用的查询:
SELECT
*
FROM (SELECT
*
FROM messages
ORDER BY id DESC) AS x
GROUP BY name
Run Code Online (Sandbox Code Playgroud)
但这看起来非常低效.还有其他方法可以达到相同的效果吗?
Bil*_*win 908
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;
Run Code Online (Sandbox Code Playgroud)
以下是我在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;
Run Code Online (Sandbox Code Playgroud)
关于性能,根据数据的性质,一种解决方案或另一种解决方案可能更好.因此,您应该测试两个查询并使用在给定数据库时性能更好的查询.
例如,我有一个StackOverflow August数据转储的副本.我会用它来进行基准测试.表中有1,114,357行Posts.这是在我的Macbook Pro 2.40GHz 上的MySQL 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)
Run Code Online (Sandbox Code Playgroud)
即使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)
Run Code Online (Sandbox Code Playgroud)
现在用产生同样的查询结果我的技术有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)
Run Code Online (Sandbox Code Playgroud)
该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)
Run Code Online (Sandbox Code Playgroud)
这是我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;
Run Code Online (Sandbox Code Playgroud)
new*_*ver 141
UPD:2017-03-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是一个小组,我正在寻找request_id每个人的最后一个test_id.
Bill的解决方案已经在我的dell e4310上运行了几个小时,我不知道它什么时候会完成,即使它在覆盖索引上运行(因此using index在EXPLAIN中).
我有几个基于相同想法的其他解决方案:
(group_id, item_value)对是每个中的最后一个值group_id,group_id如果我们按降序顺序遍历索引,则每个值都是第一个;MySQL使用索引的3种方法是理解一些细节的好文章.
解决方案1
这个速度非常快,我的18M +行需要大约0.8秒:
SELECT test_id, MAX(request_id), request_id
FROM testresults
GROUP BY test_id DESC;
Run Code Online (Sandbox Code Playgroud)
如果要将顺序更改为ASC,请将其放在子查询中,仅返回id并将其用作子查询以连接到其余列:
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;
Run Code Online (Sandbox Code Playgroud)
这个数据大约需要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
Run Code Online (Sandbox Code Playgroud)
它也以降序返回测试.由于它执行完整的索引扫描,因此速度要慢得多,但它可以让您了解如何为每个组输出N max行.
查询的缺点是查询缓存无法缓存其结果.
Eri*_*ric 94
使用子查询返回正确的分组,因为你已经到了一半.
试试这个:
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
Run Code Online (Sandbox Code Playgroud)
如果不是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
Run Code Online (Sandbox Code Playgroud)
这样,您可以避免子查询中的相关子查询和/或排序,这些子查询往往非常慢/效率低.
JYe*_*ton 49
我得到了一个不同的解决方案,即获取每个组中最后一篇文章的ID,然后使用第一个查询的结果作为WHERE x IN构造的参数从messages表中进行选择:
SELECT id, name, other_columns
FROM messages
WHERE id IN (
SELECT MAX(id)
FROM messages
GROUP BY name
);
Run Code Online (Sandbox Code Playgroud)
与其他一些解决方案相比,我不知道它的表现如何,但它对于我的桌子有300多万行非常出色.(4秒执行,1200+结果)
这应该适用于MySQL和SQL Server.
Vip*_*pin 29
解决方案通过子查询小提琴链接
select * from messages where id in
(select max(id) from messages group by Name)
Run Code Online (Sandbox Code Playgroud)
解决方案通过连接条件小提琴链接
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
Run Code Online (Sandbox Code Playgroud)
这篇文章的原因是只提供小提琴链接.其他答案中已提供相同的SQL.
@Vijay Dev,您好,如果您的表消息包含自动递增主键的Id,那么要根据主键获取最新记录,您的查询应如下所示:
SELECT m1.* FROM messages m1 INNER JOIN (SELECT max(Id) as lastmsgId FROM messages GROUP BY Name) m2 ON m1.Id=m2.lastmsgId
Run Code Online (Sandbox Code Playgroud)
小智 7
具有相当速度的方法如下.
SELECT *
FROM messages a
WHERE Id = (SELECT MAX(Id) FROM messages WHERE a.Name = Name)
Run Code Online (Sandbox Code Playgroud)
结果
Id Name Other_Columns
3 A A_data_3
5 B B_data_2
6 C C_data_1
Run Code Online (Sandbox Code Playgroud)
小智 7
如果您需要分组查询中文本列的最新或最旧记录,并且您不想使用子查询,则可以执行以下操作...
前任。您有一个电影列表,需要获取该系列和最新电影的数量
| ID | 系列 | 姓名 |
|---|---|---|
| 1 | 星球大战 | 新希望 |
| 2 | 星球大战 | 帝国反击战 |
| 3 | 星球大战 | 绝地归来 |
SELECT COUNT(id), series, SUBSTRING(MAX(CONCAT(id, name)), LENGTH(id) + 1),
FROM Movies
GROUP BY series
Run Code Online (Sandbox Code Playgroud)
这返回...
| ID | 系列 | 姓名 |
|---|---|---|
| 3 | 星球大战 | 绝地归来 |
MAX 将返回具有最高值的行,因此通过将 id 与名称连接,您现在将获得最新的记录,然后只需去掉 id 即可获得最终结果。
比使用子查询更有效。
所以对于给定的例子:
SELECT MAX(Id), Name, SUBSTRING(MAX(CONCAT(Id, Other_Columns)), LENGTH(Id) + 1),
FROM messages
GROUP BY Name
Run Code Online (Sandbox Code Playgroud)
快乐编码,“愿原力与你同在”:)
小智 6
我还没有使用大型数据库进行测试,但我认为这可能比连接表更快:
SELECT *, Max(Id) FROM messages GROUP BY Name
Run Code Online (Sandbox Code Playgroud)
SELECT
column1,
column2
FROM
table_name
WHERE id IN
(SELECT
MAX(id)
FROM
table_name
GROUP BY column1)
ORDER BY column1 ;
Run Code Online (Sandbox Code Playgroud)
小智 6
您也可以从这里观看风景。
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);
Run Code Online (Sandbox Code Playgroud)
第二个解决方案
SELECT * FROM (SELECT * FROM Demo_User ORDER BY ID DESC) AS T GROUP BY NAME ;
Run Code Online (Sandbox Code Playgroud)
小智 6
这是我的解决方案:
SELECT
DISTINCT NAME,
MAX(MESSAGES) OVER(PARTITION BY NAME) MESSAGES
FROM MESSAGE;
Run Code Online (Sandbox Code Playgroud)
这是两个建议.首先,如果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;
Run Code Online (Sandbox Code Playgroud)
我假设"最后"你的意思是最后一个顺序.如果不是,请相应地更改ROW_NUMBER()窗口的ORDER BY子句.如果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
)
Run Code Online (Sandbox Code Playgroud)
换句话说,选择没有具有相同名称的later-Id消息的消息.
这是使用GROUP_CONCATorder by 获取最后一个相关记录的另一种方法,并SUBSTRING_INDEX从列表中选择一个记录
SELECT
`Id`,
`Name`,
SUBSTRING_INDEX(
GROUP_CONCAT(
`Other_Columns`
ORDER BY `Id` DESC
SEPARATOR '||'
),
'||',
1
) Other_Columns
FROM
messages
GROUP BY `Name`
Run Code Online (Sandbox Code Playgroud)
上面的查询将对Other_Columns同一组中的所有内容进行Name分组,并且使用ORDER BY id DESC将Other_Columns在我使用的情况下使用提供的分隔符按降序加入特定组中的所有内容||,使用SUBSTRING_INDEX此列表将选择第一个
显然,有许多不同的方法来获得相同的结果,您的问题似乎是在MySQL中获得每个组中最后一个结果的有效方法是什么。如果您要处理大量数据,并且假设您将InnoDB与MySQL的最新版本(例如5.7.21和8.0.4-rc)一起使用,则可能没有有效的方法。
有时我们需要对具有超过6000万行的表执行此操作。
对于这些示例,我将仅使用大约150万行的数据,其中查询将需要查找数据中所有组的结果。在我们的实际情况下,我们经常需要从大约2,000个组中返回数据(假设不需要检查很多数据)。
我将使用以下表格:
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));
Run Code Online (Sandbox Code Playgroud)
温度表填充了约150万个随机记录以及100个不同的组。selected_group填充了这100个组(在我们的示例中,所有组通常小于20%)。
由于此数据是随机的,这意味着多行可以具有相同的recordedTimestamps。我们想要的是按组ID的顺序获取所有选定组的列表,每个组的最后一个记录的时间戳,如果同一组具有多个匹配行,则该行的最后一个匹配ID。
如果假设MySQL具有last()函数,该函数从特殊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;
Run Code Online (Sandbox Code Playgroud)
在这种情况下,它只需要检查几百行,因为它不使用任何普通的GROUP BY函数。这将在0秒内执行,因此非常高效。请注意,通常在MySQL中,我们会在GROUP BY子句之后看到ORDER BY子句,但是此ORDER BY子句用于确定last()函数的ORDER,如果它在GROUP BY之后,则它将对GROUPS进行排序。如果不存在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
);
Run Code Online (Sandbox Code Playgroud)
这检查了3,009,254行,在5.7.21上花了〜0.859秒,在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;
Run Code Online (Sandbox Code Playgroud)
这检查了1,505,331行,在5.7.21上花费了约1.25秒,在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;
Run Code Online (Sandbox Code Playgroud)
这检查了3,009,685行,在5.7.21上花了〜1.95秒,在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
)
);
Run Code Online (Sandbox Code Playgroud)
这检查了6,137,810行,在5.7.21上花费了约2.2秒,在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;
Run Code Online (Sandbox Code Playgroud)
这检查了6,017,808行,并在8.0.4-rc上花费了约4.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;
Run Code Online (Sandbox Code Playgroud)
这检查了6,017,908行,并在8.0.4-rc上花费了约17.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;
Run Code Online (Sandbox Code Playgroud)
这是永远的,所以我不得不杀死它。
我们将研究如何使用MySQL获取分组依据中的最后一条记录。例如,如果您有此结果集。
id category_id post_title1 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 Group By键盘。
select * from posts group by category_id
但是我们从这个查询中得到的结果是。
id category_id post_title1 1 Title 1
4 2 Title 4
6 3 Title 6
group by将始终返回结果集中该组中的第一条记录。
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_title3 1 Title 3
5 2 Title 5
6 3 Title 6
**
你好,这个查询可能有帮助:
**
SELECT
*
FROM
message
WHERE
`Id` IN (
SELECT
MAX(`Id`)
FROM
message
GROUP BY
`Name`
)
ORDER BY
`Id` DESC
Run Code Online (Sandbox Code Playgroud)
| 归档时间: |
|
| 查看次数: |
580086 次 |
| 最近记录: |