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

问题描述 投票:1042回答:27

这个问题在这里已有答案:

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

+------+-------+--------------------------------------+
| id   | rev   | content                              |
+------+-------+--------------------------------------+
| 1    | 1     | ...                                  |
| 2    | 1     | ...                                  |
| 1    | 2     | ...                                  |
| 1    | 3     | ...                                  |
+------+-------+--------------------------------------+

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

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

更新 正如答案所示,有一个SQL解决方案,和here a sqlfiddle demo

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

mysql sql aggregate-functions greatest-n-per-group groupwise-maximum
27个回答
1670
投票

乍一看...

你需要的只是一个带有GROUP BY聚合函数的MAX子句:

SELECT id, MAX(rev)
FROM YourTable
GROUP BY id

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

我刚刚注意到你也需要content专栏。

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

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

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

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

在这种方法中,您首先在子查询中找到group-identifier, max-value-in-group(已在上面解决)。然后你将表连接到group-identifiermax-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

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

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

  1. 第二个连接条件是左侧值小于右侧值
  2. 当你执行第1步时,实际具有最大值的行将在右侧具有NULL(它是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;

结论

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

如果max-value-in-group有两行group-identifier,那么两行都将在结果中。

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

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


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

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

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

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


5
投票

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

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

Here's an example in SqlFiddle


4
投票

如果select语句中有许多字段,并且您希望通过优化代码获得所有这些字段的最新值:

select * from
(select * from table_name
order by id,rev desc) temp
group by id 

4
投票

我想,你想要这个吗?

select * from docs where (id, rev) IN (select id, max(rev) as rev from docs group by id order by id)  

SQL小提琴:Check here


4
投票
SELECT *
FROM Employee
where Employee.Salary in (select max(salary) from Employee group by Employe_id)
ORDER BY Employee.Salary

4
投票

另一种方法是在OVER PARTITION子句中使用MAX()解析函数

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 

本文中已经记录的其他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 

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

MAX()解决方案运行肯定更快ROW_NUMBER()解决方案,因为MAX()复杂性是O(n)ROW_NUMBER()复杂性是最小O(n.log(n)),其中n代表表中的记录数!


3
投票

此解决方案只能从YourTable中进行一次选择,因此速度更快。根据sqlfiddle.com上的测试,它仅适用于MySQL和SQLite(适用于SQLite删除DESC)。也许它可以调整为适用于我不熟悉的其他语言。

SELECT *
FROM ( SELECT *
       FROM ( SELECT 1 as id, 1 as rev, 'content1' as content
              UNION
              SELECT 2, 1, 'content2'
              UNION
              SELECT 1, 2, 'content3'
              UNION
              SELECT 1, 3, 'content4'
            ) as YourTable
       ORDER BY id, rev DESC
   ) as YourTable
GROUP BY id

3
投票

我会用这个:

select t.*
from test as t
join
   (select max(rev) as rev
    from test
    group by id) as o
on o.rev = t.rev

子查询SELECT可能不太有效,但在JOIN子句中似乎是可用的。我不是优化查询的专家,但我已经尝试过MySQL,PostgreSQL,FireBird,它确实非常好用。

您可以在多个连接和WHERE子句中使用此模式。这是我的工作示例(解决与你的问题相同的表“坚固”):

select *
from platnosci as p
join firmy as f
on p.id_rel_firmy = f.id_rel
join (select max(id_obj) as id_obj
      from firmy
      group by id_rel) as o
on o.id_obj = f.id_obj and p.od > '2014-03-01'

在具有数万条记录的表格上询问它,并且在真正不太强大的机器上花费少于0.01秒。

我不会使用IN子句(因为它在上面的某处提到)。 IN用于使用简短的constans列表,而不是在子查询上构建的查询过滤器。这是因为IN中的子查询是针对每个扫描记录执行的,这可能使查询花费非常少的时间。


3
投票

这个怎么样:

SELECT all_fields.*  
FROM (SELECT id, MAX(rev) FROM yourtable GROUP BY id) AS max_recs  
LEFT OUTER JOIN yourtable AS all_fields 
ON max_recs.id = all_fields.id

2
投票

这是一个很好的方法

使用以下代码:

with temp as  ( 
select count(field1) as summ , field1
from table_name
group by field1 )
select * from temp where summ = (select max(summ) from temp)

216
投票

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

你可以使用IN来做到这一点:

SELECT * 
FROM t1 WHERE (id,rev) IN 
( SELECT id, MAX(rev)
  FROM t1
  GROUP BY id
)

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


2
投票

我喜欢通过某些列对记录进行排名来做到这一点。在这种情况下,按rev分组的id排名。那些rev较高的人排名较低。所以最高的rev排名为1。

select id, rev, content
from
 (select
    @rowNum := if(@prevValue = id, @rowNum+1, 1) as row_num,
    id, rev, content,
    @prevValue := id
  from
   (select id, rev, content from YOURTABLE order by id asc, rev desc) TEMP,
   (select @rowNum := 1 from DUAL) X,
   (select @prevValue := -1 from DUAL) Y) TEMP
where row_num = 1;

不确定引入变量是否会使整个事情变得更慢。但至少我不是两次询问YOURTABLE


2
投票

以相反的顺序对rev字段进行排序,然后按id分组,这给出了每个分组的第一行,即具有最高rev值的分组。

SELECT * FROM (SELECT * FROM table1 ORDER BY id, rev DESC) X GROUP BY X.id;

使用以下数据在http://sqlfiddle.com/中进行测试

CREATE TABLE table1
    (`id` int, `rev` int, `content` varchar(11));

INSERT INTO table1
    (`id`, `rev`, `content`)
VALUES
    (1, 1, 'One-One'),
    (1, 2, 'One-Two'),
    (2, 1, 'Two-One'),
    (2, 2, 'Two-Two'),
    (3, 2, 'Three-Two'),
    (3, 1, 'Three-One'),
    (3, 3, 'Three-Three')
;

这在MySql 5.5和5.6中给出了以下结果

id  rev content
1   2   One-Two
2   2   Two-Two
3   3   Three-Two

2
投票

这是另一种解决方案,希望它能帮到某人

Select a.id , a.rev, a.content from Table1 a
inner join 
(SELECT id, max(rev) rev FROM Table1 GROUP BY id) x on x.id =a.id and x.rev =a.rev

2
投票

这些答案都没有对我有用。

这对我有用。

with score as (select max(score_up) from history)
select history.* from score, history where history.score_up = score.max

2
投票

这是另一种仅使用具有该字段最大值的字段检索记录的解决方案。这适用于SQL400,这是我工作的平台。在此示例中,将通过以下SQL语句检索字段FIELD5中具有最大值的记录。

SELECT A.KEYFIELD1, A.KEYFIELD2, A.FIELD3, A.FIELD4, A.FIELD5
  FROM MYFILE A
 WHERE RRN(A) IN
   (SELECT RRN(B) 
      FROM MYFILE B
     WHERE B.KEYFIELD1 = A.KEYFIELD1 AND B.KEYFIELD2 = A.KEYFIELD2
     ORDER BY B.FIELD5 DESC
     FETCH FIRST ROW ONLY)

1
投票

Explanation

这不是纯SQL。这将使用SQLAlchemy ORM。

我来到这里寻找SQLAlchemy的帮助,所以我将使用python / SQLAlchemy版本复制Adrian Carneiro的答案,特别是外部连接部分。

此查询回答了以下问题:

“你能否把这组记录中的记录(基于相同的id)返回给我,这些记录具有最高的版本号”。

这允许我复制记录,更新记录,增加其版本号,并以这样的方式获得旧版本的副本,以便我可以显示随时间的变化。

Code

MyTableAlias = aliased(MyTable)
newest_records = appdb.session.query(MyTable).select_from(join(
    MyTable, 
    MyTableAlias, 
    onclause=and_(
        MyTable.id == MyTableAlias.id,
        MyTable.version_int < MyTableAlias.version_int
    ),
    isouter=True
    )
).filter(
    MyTableAlias.id  == None,
).all()

在PostgreSQL数据库上测试。


0
投票

我用下面的方法解决了我自己的问题。我首先创建了一个临时表,并为每个唯一ID插入了最大rev值。

CREATE TABLE #temp1
(
    id varchar(20)
    , rev int
)
INSERT INTO #temp1
SELECT a.id, MAX(a.rev) as rev
FROM 
    (
        SELECT id, content, SUM(rev) as rev
        FROM YourTable
        GROUP BY id, content
    ) as a 
GROUP BY a.id
ORDER BY a.id

然后我将这些最大值(#temp1)加入到所有可能的id / content组合中。通过这样做,我自然地过滤掉了非最大id /内容组合,并且剩下每个最大转速值。

SELECT a.id, a.rev, content
FROM #temp1 as a
LEFT JOIN
    (
        SELECT id, content, SUM(rev) as rev
        FROM YourTable
        GROUP BY id, content
    ) as b on a.id = b.id and a.rev = b.rev
GROUP BY a.id, a.rev, b.content
ORDER BY a.id

0
投票

revid组合成maxRevId的一个MAX()值然后将其拆分回原始值时,可以在没有连接的情况下进行选择:

SELECT maxRevId & ((1 << 32) - 1) as id, maxRevId >> 32 AS rev
FROM (SELECT MAX(((rev << 32) | id)) AS maxRevId
      FROM YourTable
      GROUP BY id) x;

当存在复杂的连接而不是单个表时,这尤其快。使用传统方法,复杂连接将完成两次。

revidINT UNSIGNED(32位)并且组合值适合BIGINT UNSIGNED(64位)时,上述组合很简单。当idrev大于32位值或由多列组成时,您需要将该值合并到例如一个二进制值,适用于MAX()的填充。


71
投票

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

select yt.id, yt.rev, yt.contents
    from YourTable yt
    where rev = 
        (select max(rev) from YourTable st where yt.id=st.id)

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

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

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

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


65
投票

我很惊讶,没有答案提供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 

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


45
投票

我无法保证性能,但这是一个受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

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

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

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


28
投票

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

SELECT *
FROM
    (SELECT *
    FROM Employee
    ORDER BY Salary DESC)
AS employeesub
GROUP BY employeesub.Salary;
  • SELECT *:归还所有领域。
  • FROM Employee:搜索表。
  • (SELECT *...)子查询:返回所有人,按工资排序。
  • GROUP BY employeesub.Salary:强制每个员工的排序最高的Salary行作为返回的结果。

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

SELECT *
FROM Employee
ORDER BY Employee.Salary DESC
LIMIT 1

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

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

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


19
投票

像这样的东西?

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)

7
投票

我喜欢使用基于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
)

这将选择组中具有最大值的所有记录,并允许您选择其他列。


6
投票

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

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

select *
from (select * from mytable order by id, rev desc ) x
group by id

请在this question中回答用户Bohemian的回答,为这个问题提供如此简洁优雅的答案。

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

© www.soinside.com 2019 - 2024. All rights reserved.