获取每个分组的最大值记录的SQL结果

351

如何获取包含每个分组最大值的行?

我看到了一些过于复杂的变体,但没有一个很好的答案。我试图提供最简单的例子:

给定下面这样的表格,具有人员、分组和年龄列,如何获取每个组中最年长的人? (在一个组内的平局应该给出字母顺序排名靠前的结果)

Person | Group | Age
---
Bob  | 1     | 32  
Jill | 1     | 34  
Shawn| 1     | 42  
Jake | 2     | 29  
Paul | 2     | 36  
Laura| 2     | 39  

期望的结果集:

Shawn | 1     | 42    
Laura | 2     | 39  

24
注意:该被接受的答案是在2012年撰写的,并且在当时有效。然而,基于评论中的多个原因,它现在已不再适用。 - Rick James
@RickJames - 在您的页面上找到了解决方案:http://mysql.rjweb.org/doc.php/groupwise_max#using_variables。使用MySQL 8+的“窗口函数”。谢谢! - kJamesy
@kJamesy - 是的,但这是直接指向用于该用途的“窗口函数”的指针:http://mysql.rjweb.org/doc.php/groupwise_max#using_windowing_functions_ - Rick James
19个回答

431

正确的解决方案是:

SELECT o.*
FROM `Persons` o                    # 'o' from 'oldest person in group'
  LEFT JOIN `Persons` b             # 'b' from 'bigger age'
      ON o.Group = b.Group AND o.Age < b.Age
WHERE b.Age is NULL                 # bigger age not found

工作原理:

它将 o 的每一行与 b 中在列 Group 中具有相同值且在列 Age 中具有较大值的所有行匹配。在列 Age 中,如果来自 o 的任何行没有其组中最大值,则它将与 b 中的一行或多行匹配。

使用 LEFT JOIN 将组中最年长的人(包括那些独处在他们的组中的人)与 b 中的一个由 NULL 填充的行(“组中没有最大年龄”)匹配。
使用 INNER JOIN 会使这些行不匹配并被忽略。

WHERE 子句仅保留从 b 中提取的字段中具有 NULL 的行。它们是每个组中最年长的人。

进一步阅读

该解决方案以及许多其他解决方案都在书籍《SQL反模式卷1:避免数据库编程的陷阱》中解释。


64
顺便提一下,如果 o.Age = b.Age,对于同一组可能会返回两行或更多行,例如,如果第二组的 Paul 和 Laura 一样都是39岁。但是,如果我们不想要这种情况,可以使用以下语句进行限制:ON o.Group = b.Group AND (o.Age < b.Age or (o.Age = b.Age and o.id < b.id)) - Todor
13
太不可思议了!对于20M条记录,它比“朴素”算法(使用max()子查询进行连接)快了50倍。 - user2706534
6
与 @Todor 的评论完美配合。我想补充一点,如果还有其他查询条件,它们必须添加在 FROM 和 LEFT JOIN 中。例如:FROM (SELECT * FROM Person WHERE Age != 32) o LEFT JOIN (SELECT * FROM Person WHERE Age != 32) b - 如果你想排除年龄为 32 的人。 - Alain Zelink
7
这个解决方案起初是有效的,但在尝试处理10,000多行共享相同ID时,它开始在慢查询日志中被报告为慢查询。在索引列上进行了JOIN操作。这是一个罕见的情况,但认为值得一提。 - chaseisabelle
4
正确,可以。速度快,不行。它的时间复杂度为O(N^2)。 - Rick James
显示剩余6条评论

146

在mysql中有一种超级简单的方法来做到这一点:

select * 
from (select * from mytable order by `Group`, age desc, Person) x
group by `Group`

这个方法有效是因为在mysql中,你可以不对非group-by列进行聚合,这种情况下mysql会返回第一行。解决方法是首先按照每个组所需的行排序,然后按照你想要值的列进行分组。
你可以避免复杂的子查询尝试找到max()等,并且还可以避免在存在多个最大值时返回多个行(其他答案会这样做)。
注意:这是一个仅适用于mysql的解决方案。我知道的所有其他数据库都会抛出SQL语法错误,提示“未聚合的列未在group by子句中列出”或类似的消息。因为这个解决方案使用了未记录的行为,更谨慎的人可能希望包含一个测试来断言它在将来的MySQL版本中仍然工作。
版本5.7更新:
自从版本5.7以来,sql-mode设置默认包括ONLY_FULL_GROUP_BY,因此为了使其正常工作,您必须选择此选项(编辑服务器的选项文件以删除此设置)。

92
"mysql只返回第一行" - 这可能是它的工作方式,但并不保证。文档指出:“服务器可以自由选择每个组中的任何值,因此除非它们相同,否则所选择的值是不确定的。” 服务器不会选择行,而是选择在SELECT子句中出现且未使用聚合函数计算的每个列或表达式的值(不一定来自同一行)。 - axiac
18
这种行为在MySQL 5.7.5上发生了改变,默认情况下,它会拒绝此查询,因为SELECT子句中的列与GROUP BY列之间不存在函数依赖关系。如果它被配置为接受此查询(ONLY_FULL_GROUP_BY被禁用),则会像以前的版本一样工作(即这些列的值是不确定的)。 - axiac
31
我很惊讶这个答案获得了这么多赞,但它是错误的和糟糕的。这个查询不能保证可行。尽管使用了 order by 子句,子查询中的数据是一个无序集合。MySQL 现在可能真的会对记录进行排序并保持该顺序,但如果在将来的某个版本中停止排序,则不会违反任何规则。然后 GROUP BY 将缩减为一条记录,但所有字段都将从记录中任意选择。MySQL 目前可能只选取第一行,但在未来的版本中也可能选取任何其他行甚至来自不同行的值。 - Thorsten Kettner
13
好的,我们在这方面意见不同。我不使用仅仅偶然能够运行且依赖于一些测试的未记录功能。你知道,你只是幸运地得到了完整的第一条记录,而文档清楚地说明你可能会得到任何不确定的值,但你仍然使用它。任何简单的会话或数据库设置都可能随时改变这种情况。我认为这样做太冒险了。 - Thorsten Kettner
9
根据文档,这个答案似乎是错误的。服务器可以从每个组中选择任何值...此外,通过添加ORDER BY子句无法影响从每个组中选择的值。结果集排序发生在值被选择之后,而ORDER BY不会影响服务器选择每个组内的哪个值。 - Tgr
显示剩余23条评论

85

你可以对子查询中检索的 MAX(Group)Age 进行联接查询。这种方法在大多数关系型数据库管理系统上都是可移植的。

SELECT t1.*
FROM yourTable t1
INNER JOIN
(
    SELECT `Group`, MAX(Age) AS max_age
    FROM yourTable
    GROUP BY `Group`
) t2
    ON t1.`Group` = t2.`Group` AND t1.Age = t2.max_age;

Michael,谢谢你的回复-但是你对于Bohemian评论中提到的返回多行的问题有答案吗? - Yarin
3
如果有两行的Group = 2, Age = 20,子查询将返回其中一行,但是联结中的 ON 条件将匹配 _这两行_,因此会返回 2 行相同的组/年龄,尽管其他列的值不同,而不是一行。 - Michael Berkowski
那么我们是不是在说,除非我们采用Bohemians的MySQL-only路线,否则无法将结果限制为每个组只有一个? - Yarin
选择一个或多个组(当多个组具有完全相同的值时)是否都比其他组的所有值都大? - Mohammad Afrashteh
@MohammadAfrashteh 这是一组太复杂的要求,无法在评论中描述(并且与原始帖子相距太远)。您应该将其作为自己的完整问题发布,附带示例输入行和预期查询输出的样本。 - Michael Berkowski
显示剩余5条评论

33
在PostgreSQL中,您可以使用DISTINCT ON子句:DISTINCT ON
SELECT DISTINCT ON ("group") * FROM "mytable" ORDER BY "group", "age" DESC;

2
@IgorKulagin - 在Postgres中无法工作- 错误信息:列"mytable.id"必须出现在GROUP BY子句中或用于聚合函数 - Yarin
25
在许多情况下,MySQL查询可能只是偶然起作用的。 "SELECT *" 可能会返回与所属 MAX(age) 不对应的信息。这个答案是错误的。SQLite 也可能存在同样的情况。 - Albert Hendriks
2
但这适用于需要选择分组列和最大列的情况。这不符合上述要求,因为它将产生('Bob',1,42),但预期结果是('Shawn',1,42)。 - Ram Babu
1
适用于PostgreSQL。 - Karol Gasienica
2
这是错误的答案,因为MySQL“随机”选择不属于GROUP或AGE的列中的值。只有当您仅需要这些列时才可以使用。 - erdomester
显示剩余7条评论

8

不确定MySQL是否有row_number函数。如果有,您可以使用它来获得所需的结果。在SQL Server上,您可以执行类似以下的操作:

CREATE TABLE p
(
 person NVARCHAR(10),
 gp INT,
 age INT
);
GO
INSERT  INTO p
VALUES  ('Bob', 1, 32);
INSERT  INTO p
VALUES  ('Jill', 1, 34);
INSERT  INTO p
VALUES  ('Shawn', 1, 42);
INSERT  INTO p
VALUES  ('Jake', 2, 29);
INSERT  INTO p
VALUES  ('Paul', 2, 36);
INSERT  INTO p
VALUES  ('Laura', 2, 39);
GO

SELECT  t.person, t.gp, t.age
FROM    (
         SELECT *,
                ROW_NUMBER() OVER (PARTITION BY gp ORDER BY age DESC) row
         FROM   p
        ) t
WHERE   t.row = 1;

2
它确实可以,自8.0版本以来就可以了。 - Ilja Everilä

6

改进axiac的解决方案以避免在每个组中选择多行,同时允许使用索引

SELECT o.*
FROM `Persons` o 
  LEFT JOIN `Persons` b 
      ON o.Group = b.Group AND o.Age < b.Age
  LEFT JOIN `Persons` c 
      ON o.Group = c.Group AND o.Age = c.Age and o.id < c.id
WHERE b.Age is NULL and c.id is null

4
使用排名方法。
SELECT @rn :=  CASE WHEN @prev_grp <> groupa THEN 1 ELSE @rn+1 END AS rn,  
   @prev_grp :=groupa,
   person,age,groupa  
FROM   users,(SELECT @rn := 0) r        
HAVING rn=1
ORDER  BY groupa,age DESC,person

这个 SQL 可以解释如下:
  1. 从 users 表和 (select @rn := 0) r 中选择所有列,并按照 groupa、age 和 person 的降序排序。

  2. @prev_grp 为 null

  3. @rn := CASE WHEN @prev_grp <> groupa THEN 1 ELSE @rn+1 END,这是一个三元运算表达式
    类似于这样,如果 prev_grp != groupa,则 rn = 1,否则 rn = rn + 1

  4. 通过 having rn=1 过滤出所需的行


sel - 需要解释一下 - 我从未见过 := - 这是什么? - Yarin
1
":=" 是赋值运算符。您可以在http://dev.mysql.com/doc/refman/5.0/en/user-variables.html上阅读更多相关信息。" - sel
我需要深入研究一下 - 我认为答案过于复杂化了我们的情况,但感谢您教给我新的东西。 - Yarin

3

我不会使用“Group”作为列名,因为它是保留字。然而,以下SQL语句可以正常工作。

SELECT a.Person, a.Group, a.Age FROM [TABLE_NAME] a
INNER JOIN 
(
  SELECT `Group`, MAX(Age) AS oldest FROM [TABLE_NAME] 
  GROUP BY `Group`
) b ON a.Group = b.Group AND a.Age = b.oldest

谢谢,尽管这会在年龄相同时返回多个记录。 - Yarin
@Yarin,如何确定哪个是正确的最老者?多个答案似乎都是正确的,否则请使用限制和排序。 - Duncan

3

如果您只需要检索一列,我的解决方案才能起作用,然而对于我来说,在性能方面这是找到的最佳解决方案(它仅使用一个单一查询!):

SELECT SUBSTRING_INDEX(GROUP_CONCAT(column_x ORDER BY column_y),',',1) AS xyz,
   column_z
FROM table_name
GROUP BY column_z;

它使用GROUP_CONCAT来创建一个有序的连接列表,然后我使用substring只获取第一个。

可以确认,在group_concat内部使用相同的键进行排序,可以获得多个列,但需要为每个列编写单独的group_concat/index/substring。 - Rasika
奖金在于您可以在group_concat内添加多个列进行排序,从而轻松解决并保证每个组中仅有一条记录。对于简单高效的解决方案,做得很好! - Rasika

2

axiac的解决方案最终对我最有效。但是,我还有一个额外的复杂性:从两列中推导出一个计算得出的“最大值”。

让我们使用同样的例子:我想要每个组中最年长的人。如果有人一样老,就选身高最高的人。

我必须执行左连接两次才能实现这种行为:

SELECT o1.* WHERE
    (SELECT o.*
    FROM `Persons` o
    LEFT JOIN `Persons` b
    ON o.Group = b.Group AND o.Age < b.Age
    WHERE b.Age is NULL) o1
LEFT JOIN
    (SELECT o.*
    FROM `Persons` o
    LEFT JOIN `Persons` b
    ON o.Group = b.Group AND o.Age < b.Age
    WHERE b.Age is NULL) o2
ON o1.Group = o2.Group AND o1.Height < o2.Height 
WHERE o2.Height is NULL;

希望这可以帮到你!虽然我猜应该有更好的方法来完成这个操作...

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接