SQL仅选择列上具有最大值的行

Maj*_*our 1142 mysql sql aggregate-functions greatest-n-per-group groupwise-maximum

我有这个文件表(这里是简化版):

+------+-------+--------------------------------------+
| id   | rev   | content                              |
+------+-------+--------------------------------------+
| 1    | 1     | ...                                  |
| 2    | 1     | ...                                  |
| 1    | 2     | ...                                  |
| 1    | 3     | ...                                  |
+------+-------+--------------------------------------+
Run Code Online (Sandbox Code Playgroud)

如何为每个id选择一行并且只选择最大转速?
使用上面的数据,结果应该包含两行:[1, 3, ...][2, 1, ..].我正在使用MySQL.

目前,我在while循环中使用检查来检测并覆盖结果集中的旧转速.但这是实现结果的唯一方法吗?是不是有SQL解决方案?

更新
作为答案提示,有一个SQL的解决方案,并且这里sqlfiddle演示.

更新2
我注意到在添加上述sqlfiddle之后,问题被投票的速率超过了答案的upvote率.那不是故意的!小提琴是基于答案,特别是接受的答案.

Adr*_*iro 1777

乍一看...

您只需要一个GROUP BY具有MAX聚合函数的子句:

SELECT id, MAX(rev)
FROM YourTable
GROUP BY id
Run Code Online (Sandbox Code Playgroud)

从来没有这么简单,是吗?

我刚刚注意到你也需要这个content专栏.

这是SQL中一个非常常见的问题:在每个组标识符的列中查找具有一些最大值的行的整个数据.在我的职业生涯中,我听到了很多.实际上,这是我在当前工作的技术面试中回答的问题之一.

实际上,StackOverflow社区创建一个标记只是为了处理这样的问题:.

基本上,您有两种方法可以解决该问题:

加入简单的group-identifier, max-value-in-group子查询

在这种方法中,您首先group-identifier, max-value-in-group在子查询中找到(上面已经解决过).然后将表连接到子查询,group-identifier并且两者都相等max-value-in-group:

SELECT a.id, a.rev, a.contents
FROM YourTable a
INNER JOIN (
    SELECT id, MAX(rev) rev
    FROM YourTable
    GROUP BY id
) b ON a.id = b.id AND a.rev = b.rev
Run Code Online (Sandbox Code Playgroud)

Left加入self,调整连接条件和过滤器

在这种方法中,你自己加入了表.当然,平等在于group-identifier.然后,2个聪明的举动:

  1. 第二个连接条件是左侧值小于右侧值
  2. 当你执行第1步时,实际具有最大值的行将NULL在右侧(它是a LEFT JOIN,记住吗?).然后,我们过滤连接结果,仅显示右侧所在的行NULL.

所以你最终得到:

SELECT a.*
FROM YourTable a
LEFT OUTER JOIN YourTable b
    ON a.id = b.id AND a.rev < b.rev
WHERE b.id IS NULL;
Run Code Online (Sandbox Code Playgroud)

结论

两种方法都带来了完全相同的结果.

如果有两行max-value-in-groupfor group-identifier,则两行都将在结果中.

这两种方法都是SQL ANSI兼容的,因此,无论其"风味"如何,它都可以与您喜欢的RDBMS一起使用.

这两种方法都具有性能友好性,但您的里程可能会有所不同(RDBMS,DB结构,索引等).所以,当你选择一种方法而不是另一种方法时,基准.并确保你选择对你最有意义的一个.

  • @mk3009hppw:比较双精度数是否相等是_完全_确定性的,尽管认为它在某种程度上不是确定性的想法是一种常见的误解。人们通常的意思(如果他们不只是鹦鹉学舌从其他地方听到的东西)是不精确的浮点计算(可能像 0.1 + 0.2 一样简单)可能不会准确地返回“预期”结果 (0.3),因为舍入,或者[比较具有不同精度的数字类型](https://randomascii.wordpress.com/2012/06/26/doubles-are-not-floats-so-dont-compare-them/)可能会出现意外的行为。但这些都没有在这里发生。 (12认同)
  • 我知道MySQL允许你将非聚合字段添加到"按分组"查询,但我觉得有点无意义.尝试通过id`从YourTable组运行这个`select id,max(rev),rev,你明白我的意思.花点时间尝试理解它 (9认同)
  • 这是一个非常糟糕的主意,因为您想要最大化的字段可能是双精度数,并且比较双精度数是否相等是不确定的。我认为只有 O(n^2) 算法在这里有效。 (9认同)
  • 另一种方法是使用窗口函数。它们似乎提供了更好的性能。我会做类似的事情:`SELECT DISTINCT id, MAX(rev) OVER (PARTITION BY id), FIRST_VALUE(content) OVER (PARTITION BY id ORDER BY rev DESC) FROM YourTable` (8认同)
  • 我怎么能让它每组只返回一行呢?这些答案不会返回每组中比较值等于最大值的每一行吗?例如,假设OP的数据集中有第二行,id = 1,rev = 3.它不会返回id = 1,rev = 3的两行吗? (7认同)
  • @JasonMcCarrell我很高兴这个答案对你有所帮助!我明白你的观点,这就是为什么我称之为`group_identifier`,它可能是一列或多列.在您的情况下,`group_identifier`是名称和年龄的组合 (3认同)
  • 为什么第一个解决方案有效?对于由单行而不是所有行整体组成的每个组,不会运行`max`函数. (3认同)
  • @RobertChrist随意断开与第一个版本的关系,只需在初始`SELECT`之后添加`DISTINCT ON(yt.id)`.这使我的查询需要两倍的时间.所以,我不打破平局,因为在我的情况下,联系实际上是不可能的. (2认同)
  • 我很欣赏第二个解决方案,因为Doctrine DQL不支持JOIN语句中的子查询(比如第一个解决方案),所以这是一个非常有用的工作! (2认同)

Kev*_*ton 233

我的偏好是使用尽可能少的代码......

你可以IN 尝试这样做:

SELECT * 
FROM t1 WHERE (id,rev) IN 
( SELECT id, MAX(rev)
  FROM t1
  GROUP BY id
)
Run Code Online (Sandbox Code Playgroud)

在我看来它不那么复杂......更容易阅读和维护.

  • 好奇 - 我们可以在哪个数据库引擎中使用这种类型的WHERE子句?SQL Server不支持此功能. (25认同)
  • 适用于PostgreSQL. (21认同)
  • oracle&mysql(不确定其他数据库对不起) (19认同)
  • 确认在DB2中工作 (11认同)
  • 不适用于SQLite. (11认同)
  • 我刚刚用 SQLite 3.27.2 尝试过,它似乎工作得很好 (5认同)
  • 并且提供的答案是有效的ANSI \ ISO SQL !!! (2认同)
  • 更新:Percona 5.6.22以合理的方式优化它 - Order(N),其中N是表中的行数.MySQL 5.5.43执行_terrible_ job - Order(N*N).MariaDB 10.0.28执行_superior_作业 - 没有表扫描! (2认同)
  • 也适用于spark sql.:) (2认同)
  • 可悲的是,这在Presto中还不起作用:( (2认同)

top*_*hef 77

我很惊讶,没有答案提供SQL窗口功能解决方案:

SELECT a.id, a.rev, a.contents
  FROM (SELECT id, rev, contents,
               ROW_NUMBER() OVER (PARTITION BY id ORDER BY rev DESC) rank
          FROM YourTable) a
 WHERE a.rank = 1 
Run Code Online (Sandbox Code Playgroud)

在SQL标准ANSI/ISO标准SQL:2003中添加,后来使用ANSI/ISO标准SQL:2008进行了扩展,现在所有主要供应商都可以使用窗口(或窗口)功能.有更多类型的排名函数可用于处理平局问题:RANK, DENSE_RANK, PERSENT_RANK.

  • 这可能在MariaDB 10.2和MySQL 8.0.2中有效,但之前没有. (8认同)
  • 直觉是棘手的事情.我发现它比其他答案更直观,因为它构建了回答问题的显式数据结构.但是,再一次,直觉是偏见的另一面...... (4认同)
  • 与相关查询(性能杀手)或其他聚合函数相比,这是更有效的方法。现在应该将其标记为已接受的答案。 (4认同)
  • 最后,我开始想知道为什么不在这里.这比本页面上的绝大多数"旧帽子"答案更"直观",并且几乎在所有情况下都更有效,因为它只需要一次传递数据.现在大多数数据库都支持这些标准窗口函数(MySQL迟到了,但是从v8开始). (3认同)
  • 由于简单,窗口函数的方法应该是首选。 (3认同)
  • 我认为您不能使用“rank”作为代码第 3 行的字段名称。至少在 mysql 8.0.29 中不是这样。`rank` 是一个保留字,你必须使用其他的东西,比如 `ranked_order` 或任何你想要的。 (2认同)

Vaj*_*ecz 75

另一种解决方案是使用相关子查询:

select yt.id, yt.rev, yt.contents
    from YourTable yt
    where rev = 
        (select max(rev) from YourTable st where yt.id=st.id)
Run Code Online (Sandbox Code Playgroud)

索引(id,rev)使子查询几乎成为一个简单的查找...

以下是与@AdrianCarneiro的答案(子查询,leftjoin)中的解决方案的比较,基于使用InnoDB表的MySQL测量,约1百万条记录,组大小为:1-3.

对于全表扫描,子查询/ leftjoin /相关时序彼此相关为6/8/9,当涉及直接查找或batch(id in (1,2,3))时,子查询比其他子查询慢得多(由于重新运行子查询).但是我无法区分leftjoin和相关解决方案的速度.

最后一点,由于leftjoin在组中创建了n*(n + 1)/ 2个连接,其性能可能会受到组大小的严重影响......

  • 我认为如果rev不是唯一的,这是行不通的。 (3认同)

Dav*_*ter 46

我无法保证性能,但这是一个受Microsoft Excel限制的伎俩.它有一些很好的功能

好东西

  • 它应该强制只返回一个"最大记录",即使有一个平局(有时是有用的)
  • 它不需要加入

APPROACH

它有点难看,需要您了解rev列的有效值范围.让我们假设我们知道rev列是一个介于0.00和999之间的数字,包括小数,但是小数点右边只有两位数(例如34.17将是一个有效值).

事情的要点是,您可以通过字符串连接/打包主要比较字段以及所需数据来创建单个合成列.通过这种方式,您可以强制SQL的MAX()聚合函数返回所有数据(因为它已经打包到一个列中).然后你必须解压缩数据.

以下是用SQL编写的上述示例的外观

SELECT id, 
       CAST(SUBSTRING(max(packed_col) FROM 2 FOR 6) AS float) as max_rev,
       SUBSTRING(max(packed_col) FROM 11) AS content_for_max_rev 
FROM  (SELECT id, 
       CAST(1000 + rev + .001 as CHAR) || '---' || CAST(content AS char) AS packed_col
       FROM yourtable
      ) 
GROUP BY id
Run Code Online (Sandbox Code Playgroud)

例如,无论转速值如何,都通过强制转速列为许多已知字符长度来开始打包

  • 3.2成为1003.201
  • 57变为1057.001
  • 923.88成为1923.881

如果你做得对,两个数字的字符串比较应该产生与两个数字的数字比较相同的"max",并且很容易使用substring函数转换回原始数字(它可以以一种形式或另一种形式提供)到处).


Hol*_*ger 31

我认为这是最简单的解决方案:

SELECT *
FROM
    (SELECT *
    FROM Employee
    ORDER BY Salary DESC)
AS employeesub
GROUP BY employeesub.Salary;
Run Code Online (Sandbox Code Playgroud)
  • SELECT*:返回所有字段.
  • FROM Employee:搜索表.
  • (SELECT*...)子查询:返回所有人,按工资排序.
  • GROUP BY employeesub.Salary ::强制每个员工的排序最高的Salary行作为返回的结果.

如果您碰巧只需要一行,那就更容易了:

SELECT *
FROM Employee
ORDER BY Employee.Salary DESC
LIMIT 1
Run Code Online (Sandbox Code Playgroud)

我也认为最容易分解,理解和修改其他目的:

  • ORDER BY Employee.Salary DESC:按薪水排序结果,首先是最高工资.
  • 限制1:只返回一个结果.

理解这种方法,解决任何这些类似问题变得微不足道:让员工获得最低工资(将DESC更改为ASC),获得前十名收入员工(将LIMIT 1更改为LIMIT 10),通过另一个领域进行排序(更改ORDER BY Employee.Salary to ORDER BY Employee.Commission)等.


Mar*_*c B 21

像这样的东西?

SELECT yourtable.id, rev, content
FROM yourtable
INNER JOIN (
    SELECT id, max(rev) as maxrev FROM yourtable
    WHERE yourtable
    GROUP BY id
) AS child ON (yourtable.id = child.id) AND (yourtable.rev = maxrev)
Run Code Online (Sandbox Code Playgroud)

  • "你在哪里"做什么? (14认同)

Rei*_*ica 8

我喜欢使用NOT EXIST基于解决方案来解决这个问题:

SELECT 
  id, 
  rev
  -- you can select other columns here
FROM YourTable t
WHERE NOT EXISTS (
   SELECT * FROM YourTable t WHERE t.id = id AND rev > t.rev
)
Run Code Online (Sandbox Code Playgroud)

  • 似乎是最简单最独立的方法。与“in”类似,但没有分组和函数。对于我的用例来说似乎也很快。简单性非常重要,尤其是在使用 ORM 的情况下,这只是可以与其他条件链接的另一个条件,它不会像使用联接的情况那样使查询的结构复杂化。 (2认同)

小智 7

SELECT *
FROM Employee
where Employee.Salary in (select max(salary) from Employee group by Employe_id)
ORDER BY Employee.Salary
Run Code Online (Sandbox Code Playgroud)


sch*_*ebe 7

完成这项工作的另一种方法是MAX()在OVER PARTITION子句中使用解析函数

SELECT t.*
  FROM
    (
    SELECT id
          ,rev
          ,contents
          ,MAX(rev) OVER (PARTITION BY id) as max_rev
      FROM YourTable
    ) t
  WHERE t.rev = t.max_rev 
Run Code Online (Sandbox Code Playgroud)

ROW_NUMBER()这篇文章中已经记录的另一个OVER PARTITION解决方案是

SELECT t.*
  FROM
    (
    SELECT id
          ,rev
          ,contents
          ,ROW_NUMBER() OVER (PARTITION BY id ORDER BY rev DESC) rank
      FROM YourTable
    ) t
  WHERE t.rank = 1 
Run Code Online (Sandbox Code Playgroud)

此2 SELECT在Oracle 10g上运行良好。

MAX()解决方案一定比该ROW_NUMBER()解决方案运行得更快,因为MAX()复杂度是最小的,O(n)ROW_NUMBER()复杂度是最小的O(n.log(n)),它n表示表中的记录数!


Yur*_*nyy 6

由于这是关于这个问题的最受欢迎的问题,我将在此重新发布另一个答案:

看起来有更简单的方法(但仅限于MySQL):

select *
from (select * from mytable order by id, rev desc ) x
group by id
Run Code Online (Sandbox Code Playgroud)

这个问题中用户波西米亚人提供信用答案,以便为这个问题提供如此简洁优雅的答案.

编辑:虽然这个解决方案适用于很多人,但从长远来看可能不稳定,因为MySQL不保证GROUP BY语句将为不在GROUP BY列表中的列返回有意义的值.因此,使用此解决方案需要您自担风险

  • 除了它是错误的,因为不能保证内部查询的顺序意味着任何东西,GROUP BY也始终保证采取第一个遇到的行.至少在MySQL中,我会假设所有其他人.事实上,我假设MySQL只是忽略整个ORDER BY.任何未来版本或配置更改都可能会破坏此查询. (7认同)
  • @Jannes 关于 GROUP BY 不能保证采用第一个遇到的行 - 你是完全正确的 - 发现这个问题 http://bugs.mysql.com/bug.php?id=71942 要求提供这样的保证。现在更新我的答案 (2认同)

小智 6

我想,你想要这个吗?

select * from docs where (id, rev) IN (select id, max(rev) as rev from docs group by id order by id)  
Run Code Online (Sandbox Code Playgroud)

SQL小提琴: 检查这里


Kyl*_*Mit 5

不是mySQL,但是对于其他人发现此问题并使用SQL的方法,另一种解决问题的方法是Cross Apply在MS SQL中使用

WITH DocIds AS (SELECT DISTINCT id FROM docs)

SELECT d2.id, d2.rev, d2.content
FROM DocIds d1
CROSS APPLY (
  SELECT Top 1 * FROM docs d
  WHERE d.id = d1.id
  ORDER BY rev DESC
) d2
Run Code Online (Sandbox Code Playgroud)

这是SqlFiddle中的一个示例


Jan*_*nes 5

我几乎没有看到的第三个解决方案是MySQL特定的,看起来像这样:

SELECT id, MAX(rev) AS rev
 , 0+SUBSTRING_INDEX(GROUP_CONCAT(numeric_content ORDER BY rev DESC), ',', 1) AS numeric_content
FROM t1
GROUP BY id
Run Code Online (Sandbox Code Playgroud)

是的它看起来很糟糕(转换为字符串和返回等)但根据我的经验,它通常比其他解决方案更快.也许这仅仅是针对我的用例,但我在具有数百万条记录和许多独特ID的表格上使用过它.也许是因为MySQL在优化其他解决方案方面非常糟糕(至少在我提出这个解决方案的5.0天内).

一个重要的事情是GROUP_CONCAT具有它可以构建的字符串的最大长度.您可能希望通过设置group_concat_max_len变量来提高此限制.请记住,如果您有大量行,这将是对缩放的限制.

无论如何,如果您的内容字段已经是文本,则上述内容不会直接起作用.在这种情况下,您可能希望使用不同的分隔符,例如\ 0.你也会group_concat_max_len更快地遇到极限.