关于mysql:SQL仅选择列上具有最大值的行

SQL select only rows with max value on a column

本问题已经有最佳答案,请猛点这里访问。

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

1
2
3
4
5
6
7
8
+------+-------+--------------------------------------+
| id   | rev   | content                              |
+------+-------+--------------------------------------+
| 1    | 1     | ...                                  |
| 2    | 1     | ...                                  |
| 1    | 2     | ...                                  |
| 1    | 3     | ...                                  |
+------+-------+--------------------------------------+

如何为每个ID选择一行,并且只选择最大的Rev?根据以上数据,结果应该包含两行:[1, 3, ...][2, 1, ..]。我在使用MySQL。

目前,我在while循环中使用checks来检测和重写结果集中的旧版本。但是,这是实现结果的唯一方法吗?没有SQL解决方案吗?

更新如答案所示,有一个SQL解决方案,这里是一个sqlfiddle演示。

更新2我注意到在添加了上述sqlfiddle之后,问题的上票率已经超过了答案的上票率。这不是我的本意!小提琴是以答案为基础的,尤其是公认的答案。


乍一看…

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

1
2
3
SELECT id, MAX(rev)
FROM YourTable
GROUP BY id

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

我刚注意到你也需要content列。

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

实际上,stackoverflow社区创建了一个标签来处理这样的问题是非常常见的:每组最多N个。

基本上,您有两种方法来解决这个问题:

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

在这种方法中,首先在子查询中找到group-identifier, max-value-in-group(上面已经解决了)。然后,您将表与group-identifiermax-value-in-group上相等的子查询联接:

1
2
3
4
5
6
7
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

使用自身左连接,调整连接条件和过滤器

在这种方法中,您离开了与表本身的联接。当然,平等存在于group-identifier中。然后,两个聪明的动作:

  • 第二个联接条件的左侧值小于右侧值
  • 在执行步骤1时,实际具有最大值的行的右侧将有NULL(它是LEFT JOIN),记得吗?然后,我们过滤合并结果,只显示右侧为NULL的行。
  • 所以你最终会得到:

    1
    2
    3
    4
    5
    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结构、索引等)。所以当你选择一种方法而不是另一种方法时,基准测试。确保你选择对你来说最有意义的那个。


    我的首选是使用尽可能少的代码…

    你可以用IN来做。试试这个:

    1
    2
    3
    4
    5
    6
    SELECT *
    FROM t1 WHERE (id,rev) IN
    ( SELECT id, MAX(rev)
      FROM t1
      GROUP BY id
    )

    在我看来,这并不复杂…易于阅读和维护。


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

    1
    2
    3
    4
    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's answer(subquery,leftjoin)中的解决方案的比较,基于mysql测量,innodb表约为100万条记录,组大小为:1-3。

    对于全表扫描,子查询/leftjoin/相关计时以6/8/9相互关联,而对于直接查找或批处理(id in (1,2,3)),子查询比其他查询慢得多(由于重新运行子查询)。然而,我无法在速度上区分leftjoin和相关的解决方案。

    最后一点要注意的是,当leftjoin在组中创建n*(n+1)/2个联接时,其性能会受到组大小的严重影响…


    我惊讶于没有人提供SQL窗口函数解决方案:

    1
    2
    3
    4
    5
    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


    我不能保证它的性能,但这里有一个技巧是受Microsoft Excel的限制而启发的。它有一些好的特点

    好东西

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

    途径

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

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

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

    1
    2
    3
    4
    5
    6
    7
    8
    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

    包装开始时,强制rev列为已知字符长度的数字,而不考虑rev的值,例如

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

    如果你做得对,两个数字的字符串比较应该产生与两个数字的数字比较相同的"max",并且使用substring函数(在一种形式或其他几乎所有地方都可用)很容易转换回原始数字。


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

    1
    2
    3
    4
    5
    6
    7
    SELECT *
    FROM
        (SELECT *
        FROM Employee
        ORDER BY Salary DESC)
    AS employeesub
    GROUP BY employeesub.Salary;
    • SELECT *返回所有字段。
    • FROM Employee:已搜索的表。
    • (SELECT *...)子查询:返回所有人员,按薪资排序。
    • GROUP BY employeesub.Salary:强制每个员工的最高排序工资行作为返回结果。

    如果你恰好只需要一行,那就更容易了:

    1
    2
    3
    4
    SELECT *
    FROM Employee
    ORDER BY Employee.Salary DESC
    LIMIT 1

    我还认为最容易分解、理解和修改到其他目的:

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

    理解这种方法,解决这些类似的问题就变得微不足道了:让工资最低的员工(将DESC改为ASC、让收入最高的10名员工(将LIMIT 1改为LIMIT 10、通过另一个字段进行排序(将ORDER BY Employee.Salary改为ORDER BY Employee.Commission等)。


    像这样?

    1
    2
    3
    4
    5
    6
    7
    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)


    既然这是关于这个问题最流行的问题,我也会在这里发布另一个答案:

    看起来有更简单的方法可以做到这一点(但仅在MySQL中):

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

    请相信用户波西米亚在这个问题的答案,提供这样一个简洁和优雅的回答这个问题。

    Edit: though this solution works for many people it may not be stable in the long run, since MySQL doesn't guarantee that GROUP BY statement will return meaningful values for columns not in GROUP BY list. So use this solution at your own risk!


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

    1
    2
    3
    4
    5
    6
    7
    8
    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
    )

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


    不是mysql,但是对于发现这个问题并使用sql的其他人来说,解决最大的n-per-group问题的另一种方法是在ms-sql中使用Cross Apply

    1
    2
    3
    4
    5
    6
    7
    8
    9
    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

    下面是sqlfiddle中的一个示例


    第三种解决方案是MySQL特有的,看起来像这样:

    1
    2
    3
    4
    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天内)。

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

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


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

    1
    2
    3
    4
    select * from
    (select * from table_name
    order by id,rev desc) temp
    group by id


    我想,你想要这个?

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

    SQL小提琴:这里检查


    这个怎么样?

    1
    2
    3
    4
    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


    此解决方案只允许您选择一个选项,因此速度更快。根据sqlfiddle.com上的测试,它只适用于mysql和sqlite(对于sqlite remove desc)。也许它可以调整到我不熟悉的其他语言上。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    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


    我会用这个:

    1
    2
    3
    4
    5
    6
    7
    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可能不太有效,但in join子句似乎可用。我不是优化查询的专家,但我在MySQL、PostgreSQL、Firebird上都做过尝试,而且效果很好。

    您可以在多个联接和WHERE子句中使用此模式。这是我的工作示例(用表格"firmy"解决与您的问题相同的问题):

    1
    2
    3
    4
    5
    6
    7
    8
    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子句(正如上面提到的)。在中,它被赋予与常量的短列表一起使用,而不是作为建立在子查询上的查询过滤器。这是因为在中对每个扫描的记录执行子查询,这会使查询占用很长的时间。


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

    另一种方法是在over-partition子句中使用MAX()分析函数。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    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()过度分区解决方案是

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    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个选项在Oracle10g上很好地工作。

    max()解决方案比ROW_NUMBER()解决方案运行得更快,因为MAX()的复杂性是O(n),而ROW_NUMBER()的复杂性是O(n.log(n)),其中n表示表中的记录数!


    这是个很好的方法

    使用以下代码:

    1
    2
    3
    4
    5
    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)


    将rev字段按相反的顺序排序,然后按ID分组,ID给出每个分组的第一行,即rev值最高的一行。

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

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    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')
    ;

    这在MySQL5.5和5.6中给出了以下结果

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


    这是另一个解决办法,希望它能帮助别人

    1
    2
    3
    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

    这些答案对我都不起作用。

    这就是我的工作。

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

    这是另一个只检索具有该字段最大值的字段的记录的解决方案。这适用于我工作的平台sql400。在本例中,字段field5中最大值的记录将由以下SQL语句检索。

    1
    2
    3
    4
    5
    6
    7
    8
    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)

    我喜欢通过按列对记录进行排序来完成这项工作。在这种情况下,按id分组,对rev值进行排序。rev越高,排名就越低。所以最高的rev将排名为1。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    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


    解释

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

    我来这里是为了寻求sqlacalchemy的帮助,所以我将使用python/sqlacalchemy版本复制adrian carneiro的答案,特别是外部连接部分。

    此查询回答以下问题:

    "您能把这组记录中版本号最高的记录(基于相同的ID)返回给我吗?"

    这允许我复制记录,更新它,增加它的版本号,并以一种我可以显示随时间变化的方式复制旧版本。

    代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    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数据库上测试。


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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    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/内容组合中。通过这样做,我自然地过滤掉了非最大ID/内容组合,并且只剩下了每个组合的最大rev值。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    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

    当您将revid组合为MAX()的一个maxRevId值,然后将其拆分为原始值时,可以不使用联接进行选择:

    1
    2
    3
    4
    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()添加适当的填充。