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

Yar*_*rin 212 mysql sql greatest-n-per-group

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

我在这个问题上看到了一些过于复杂的变化,没有一个有很好的答案.我试图把最简单的例子放在一起:

给出如下表格,包含人,组和年龄列,您将如何获得每组中最老的人?(组内的一个平局应该给出第一个字母结果)

Person | Group | Age
---
Bob  | 1     | 32  
Jill | 1     | 34  
Shawn| 1     | 42  
Jake | 2     | 29  
Paul | 2     | 36  
Laura| 2     | 39  
Run Code Online (Sandbox Code Playgroud)

期望的结果集:

Shawn | 1     | 42    
Laura | 2     | 39  
Run Code Online (Sandbox Code Playgroud)

axi*_*iac 268

正确的解决方案是:

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
Run Code Online (Sandbox Code Playgroud)

这个怎么运作:

它匹配每一行,o其中所有行在列中b具有相同的值,在列中Group具有更大的值Age.在列中o没有其组的最大值的任何行将Age匹配来自的一行或多行b.

LEFT JOIN使得它与组中最老的人(包括他们组中独自的人)匹配,其中一行充满了NULLs b('组中没有最大年龄').
使用INNER JOIN使这些行不匹配,它们将被忽略.

WHERE子句仅保留NULL从中提取的字段中具有s 的行b.他们是每个群体中最年长的人.

进一步阅读

" SQL反模式:避免数据库编程陷阱 "一书中解释了此解决方案和许多其他解决方案

  • 顺便说一句,如果`o.Age = b.Age`,这可以为同一组返回两行或更多行,例如,如果来自第2组的Paul像Laura一样在39.但是如果我们不想要这样的行为,我们可以这样做:`ON o.Group = b.Group AND(o.Age <b.Age or(o.Age = b.Age and o.id <b.id)) (39认同)
  • 难以置信!对于20M记录,它比"天真"算法快50倍(使用max()连接子查询) (7认同)
  • 该解决方案有效; 但是,当尝试使用共享相同ID的10,000多行时,它开始在慢查询日志中报告.正在加入索引列.一个罕见的案例,但认为值得一提. (5认同)
  • 正确,好的。快,不。它是阶数(N^2)。 (4认同)
  • 与@Todor评论完美配合.我想补充一点,如果还有其他查询条件,则必须在FROM和LEFT JOIN中添加它们.喜欢的东西:***FROM(SELECT*FROM Person WHERE Age!= 32)o LEFT JOIN(SELECT*FROM Person WHERE Age!= 32)b*** - 如果你想解雇32岁的人 (2认同)
  • 我编辑了我的书的链接,因为我刚刚出版了第二版。 (2认同)

Boh*_*ian 131

在mysql中有一个超级简单的方法:

select * 
from (select * from mytable order by `Group`, age desc, Person) x
group by `Group`
Run Code Online (Sandbox Code Playgroud)

这工作,因为在MySQL中你被允许聚集非组逐列,在这种情况下,MySQL的只是返回的第一排.解决方案是首先对数据进行排序,使得对于每个组,您想要的行是第一个,然后按您想要值的列进行分组.

您可以避免尝试查找max()等的复杂子查询,以及当多个行具有相同的最大值时返回多行的问题(如其他答案所做的那样)

注意:这是一个仅限mysql的解决方案.我知道的所有其他数据库都会抛出SQL语法错误,并显示消息"非聚合列未列在group by子句中"或类似内容.因为此解决方案使用未记录的行为,所以如果MySQL的未来版本更改此行为,则更谨慎可能需要包含测试以声明它仍然有效.

版本5.7更新:

从5.7版开始,默认情况下sql-mode包含该设置ONLY_FULL_GROUP_BY,因此要使其工作,您必须没有此选项(编辑服务器的选项文件以删除此设置).

  • *"mysql只返回第一行."* - 也许这是它的工作原理,但不保证.[documentation](http://dev.mysql.com/doc/refman/5.6/en/group-by-handling.html)说:**"服务器可以自由选择每个组中的任何值,所以除非他们是一样的,选择的价值是不确定的."**.服务器不会为`SELECT`子句中出现的每个列或表达式选择行而是选择值(不一定来自同一行),并且不使用聚合函数计算. (61认同)
  • 此行为在[MySQL 5.7.5](http://dev.mysql.com/doc/refman/5.7/en/group-by-handling.html)上更改,默认情况下,它拒绝此查询,因为`SELECT`子句在功能上不依赖于`GROUP BY`列.如果它被配置为接受它("ONLY_FULL_GROUP_BY"被禁用),它就像以前的版本一样工作(即这些列的值是不确定的). (15认同)
  • 我很惊讶这个答案得到了如此多的赞成.这是错的,而且很糟糕.此查询无法保证正常工作.尽管order by子句,子查询中的数据仍是无序集.MySQL*可能*现在真正订购记录并保留该顺序,但如果它在某个未来版本中停止这样做,它就不会破坏任何规则.然后`GROUP BY'压缩到一个记录,但所有字段将从记录中任意选择.它*可能*MySQL目前只是总是选择第一行,但它也可以选择任何其他行或甚至未来版本中***行的值. (15认同)
  • 好的,我们在这里不同意.我不使用刚刚碰巧在当前工作的未记录的功能,并依赖于一些有希望覆盖它的测试.你知道你很幸运,当前的实现为你提供了完整的第一条记录,其中文档清楚地表明你可能会得到任何不确定的值,但你仍然使用它.一些简单的会话或数据库设置可能随时更改此设置.我认为这太冒险了. (9认同)
  • 这个答案似乎错了.根据[doc](https://dev.mysql.com/doc/refman/5.7/en/group-by-handling.html),*服务器可以自由选择每个组中的任何值...此外,添加ORDER BY子句不会影响每个组中值的选择.选择值后会发生结果集排序,ORDER BY不会影响服务器选择的每个组中的值.* (3认同)
  • @Yarin在这种情况下,大多数其他RDBMS都不允许你"GROUP BY Group",因为`SELECT`列表中存在其他列. (2认同)
  • @Bohemian您的YAGNI链接通过说:“即使您完全,完全,完全确定以后要使用某个功能,也请不要立即实施它”,从而详细说明它的适用范围。它在谈论功能,而不是错误。说一个测试用例是证明它可能失败的必要条件,就像说您的房子是安全的,因为您不能使它淹没。并非每个人都处于每次需要解决某些安全问题就可以轻松进行“完整系统回归”的情况。 (2认同)
  • 在MySQL 8中,甚至可能在之前,优化器只是完全摆脱了子查询.它也是[记录](https://dev.mysql.com/doc/refman/8.0/en/derived-table-optimization.html),如果封闭查询具有,则简单地忽略派生表的ORDER BY子句GROUP BY子句. (2认同)
  • 同样的事情,MySQL 5.7:https://www.db-fiddle.com/f/6NwTqvoaTAUjofL34Rz3Ld/3.ORDER BY被忽略. (2认同)

Mic*_*ski 42

你可以加入一个拉取MAX(Group)和的子查询Age.此方法可在大多数RDBMS中移植.

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;
Run Code Online (Sandbox Code Playgroud)

  • @Yarin 如果有 2 行,例如其中 `Group = 2, Age = 20`,子查询将返回其中之一,但 join `ON` 子句将匹配 _both_,因此您将获得 2 行返回相同的组/年龄,但其他列的值不同,而不是一个。 (2认同)

Igo*_*gin 28

我对SQLite(可能是MySQL)的简单解决方案:

SELECT *, MAX(age) FROM mytable GROUP BY `Group`;
Run Code Online (Sandbox Code Playgroud)

但它在PostgreSQL和其他一些平台上不起作用.

在PostgreSQL中,您可以使用DISTINCT ON子句:

SELECT DISTINCT ON ("group") * FROM "mytable" ORDER BY "group", "age" DESC;
Run Code Online (Sandbox Code Playgroud)

  • MySQL查询可能只在很多场合偶然发生."SELECT*"可能返回与归属MAX(年龄)不对应的信息.这个答案是对的.这可能也是SQLite的情况. (11认同)
  • @IgorKulagin - 在Postgres中不起作用 - 错误消息:*列"mytable.id"必须出现在GROUP BY子句中或用于聚合函数* (2认同)
  • 但这符合我们需要选择分组列和最大列的情况.这不符合上述要求('Bob',1,42),但预期结果是('Shawn',1,42) (2认同)
  • 这是一个错误的答案,因为 mysql “随机”从非 GROUP 或 AGE 的列中选择值。仅当您只需要这些列时这才适用。 (2认同)

Joh*_*uri 7

改进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
Run Code Online (Sandbox Code Playgroud)


use*_*268 5

不确定 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;
Run Code Online (Sandbox Code Playgroud)

  • 从 8.0 开始确实如此。 (2认同)