1494

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

ID 内容
1 1 ...
2 1 ...
1 2 ...
1 3 ...

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

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

4

27 回答 27

2338

第一眼看去...

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

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

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

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

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

实际上,Stack Overflow 社区创建了一个标签来处理此类问题非常普遍:

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

加入简单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

Left Joining with 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;

结论

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

如果您有两行max-value-in-groupfor group-identifier,则这两行都将出现在两种方法的结果中。

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

这两种方法也对性能友好,但是您的里程可能会有所不同(RDBMS、数据库结构、索引等)。因此,当您选择一种方法而不是另一种方法时,基准测试. 并确保您选择对您最有意义的那个。

于 2011-10-12T19:43:53.237 回答
350

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

你可以IN 试试这个:

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

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

于 2011-10-12T19:47:41.397 回答
156

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

于 2016-08-09T15:29:17.410 回答
100

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

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 的答案(子查询,leftjoin)中的解决方案的比较,基于 MySQL 测量,InnoDB 表有约 100 万条记录,组大小为:1-3。

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

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

于 2014-01-23T14:16:11.100 回答
46

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

好东西

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

方法

它有点难看,需要您对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

包装首先强制rev列是一个已知字符长度的数字,而不考虑rev的值,例如

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

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

于 2013-06-30T06:02:30.600 回答
43

唯一标识符?是的!唯一标识符!

开发 MySQL 数据库的最佳方法之一是拥有每个id AUTOINCREMENT数据库(来源 MySQL.com)。这可以带来多种优势,这里不一一赘述。该问题的问题在于其示例具有重复的 ID。这无视了唯一标识符的这些巨大优势,同时也让那些已经熟悉这一点的人感到困惑。

最干净的解决方案

DB小提琴

较新版本的 MySQLONLY_FULL_GROUP_BY默认情况下启用,这里的许多解决方案在这种情况下测试会失败

即便如此,我们也可以简单地选择DISTINCT someuniquefieldMAX( whateverotherfieldtoselect )(*somethirdfield)等,而不必担心理解结果或查询是如何工作的:

SELECT DISTINCT t1.id, MAX(t1.rev), MAX(t2.content)
FROM Table1 AS t1
JOIN Table1 AS t2 ON t2.id = t1.id AND t2.rev = (
    SELECT MAX(rev) FROM Table1 t3 WHERE t3.id = t1.id
)
GROUP BY t1.id;
  • SELECT DISTINCT Table1.id, max(Table1.rev), max(Table2.content): return DISTINCTsomefield, MAX()some otherfield,最后一个MAX()是多余的,因为我知道它只是一行,但它是查询所必需的。
  • FROM Employee: 表搜索。
  • JOIN Table1 AS Table2 ON Table2.rev = Table1.rev: 在第一个表上加入第二个表,因为,我们需要获取 max(table1.rev) 的注释。
  • GROUP BY Table1.id: 强制将每个员工的薪水排在最前面的行作为返回结果。

请注意,由于在 OP 的问题中“内容”是“...”,因此无法测试它是否有效。所以,我把它改成了“..a”,“..b”,所以,我们现在实际上可以看到结果是正确的:

id  max(Table1.rev) max(Table2.content)
1   3   ..d
2   1   ..b

为什么是干净的? DISTINCT(), MAX(), 等等,都很好地利用了 MySQL 索引。这会更快。或者,如果您有索引,并且将其与查看所有行的查询进行比较,它会快得多。

原始解决方案

ONLY_FULL_GROUP_BY禁用后,我们仍然可以使用,GROUP BY但是我们只在 Salary 上使用它,而不是在 id 上:

SELECT *
FROM
    (SELECT *
    FROM Employee
    ORDER BY Salary DESC)
AS employeesub
GROUP BY employeesub.Salary;
  • SELECT *:返回所有字段。
  • FROM Employee: 表搜索。
  • (SELECT *...)子查询:返回所有人,按薪水排序。
  • GROUP BY employeesub.Salary: 强制将每个员工的薪水排在最前面的行作为返回结果。

唯一行解决方案

请注意关系数据库的定义:“表中的每一行都有自己的唯一键。” 这意味着,在问题的示例中, id必须是唯一的,在这种情况下,我们可以这样做:

SELECT *
FROM Employee
WHERE Employee.id = 12345
ORDER BY Employee.Salary DESC
LIMIT 1

希望这是一个解决问题的解决方案,并帮助每个人更好地了解数据库中正在发生的事情。

于 2016-09-14T00:28:36.640 回答
23

像这样的东西?

SELECT yourtable.id, rev, content
FROM yourtable
INNER JOIN (
    SELECT id, max(rev) as maxrev
    FROM yourtable
    GROUP BY id
) AS child ON (yourtable.id = child.id) AND (yourtable.rev = maxrev)
于 2011-10-12T19:48:45.670 回答
18

完成这项工作的另一种方法是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 

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表示表中的记录数!

于 2018-02-20T09:07:22.600 回答
14

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

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

于 2014-09-05T21:58:28.763 回答
7
SELECT *
FROM Employee
where Employee.Salary in (select max(salary) from Employee group by Employe_id)
ORDER BY Employee.Salary
于 2017-07-30T18:12:46.063 回答
7

我想,你想要这个?

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

SQL Fiddle: 检查这里

于 2018-12-29T11:00:18.020 回答
6

我几乎从未见过提到的第三种解决方案是特定于 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更快地达到极限。

于 2014-10-10T11:57:00.803 回答
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

这是 SqlFiddle 中的一个示例

于 2014-05-30T13:47:53.067 回答
5

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

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

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

请感谢用户 Bohemian这个问题中的回答,因为它为这个问题提供了如此简洁和优雅的答案。

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

于 2014-07-03T14:33:34.277 回答
4

我会用这个:

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 子句中使用此模式。这是我的工作示例(解决与表“firmy”相同的问题):

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 用于与短的常量列表一起使用,而不是作为基于子查询的查询过滤器。这是因为 IN 中的子查询是针对每个扫描的记录执行的,这会使查询花费很长时间。

于 2015-03-04T18:12:10.567 回答
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
于 2013-07-14T16:09:41.223 回答
3

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

select * from
(select * from table_name
order by id,rev desc) temp
group by id 
于 2015-09-04T05:33:22.053 回答
2

此解决方案仅从 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
于 2014-01-29T07:49:11.443 回答
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)
于 2015-01-07T11:36:08.023 回答
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两次。

于 2015-07-16T18:52:31.847 回答
2

以相反的顺序对 rev 字段进行排序,然后按 id 分组,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
于 2015-12-11T03:14:18.257 回答
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
于 2017-06-20T10:10:35.127 回答
2

这些答案都不适合我。

这对我有用。

with score as (select max(score_up) from history)
select history.* from score, history where history.score_up = score.max
于 2017-07-13T18:19:20.983 回答
2

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

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)
于 2017-10-16T23:48:32.827 回答
1

解释

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

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

此查询回答以下问题:

“你能把这组记录(基于相同的id)中版本号最高的记录还给我吗”。

这允许我复制记录、更新它、增加它的版本号,并拥有旧版本的副本,以便我可以显示随时间的变化。

代码

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 数据库上测试。

于 2019-02-22T15:18:26.237 回答
0

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

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/content 组合,并留下每个唯一的最大转速值。

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
于 2018-01-05T10:51:51.500 回答
0

当您将revand组合id为一个maxRevIdMAX(),然后将其拆分回原始值时,您可以在没有连接的情况下进行选择:

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 位)时的位函数。当id&rev大于 32 位值或由多个列组成时,您需要将该值组合成例如具有适当填充的二进制值MAX()

于 2018-09-17T09:08:22.873 回答