对分组结果获取每个组的前n条记录

190
以下是最简单的示例,但任何解决方案都应该能够扩展到需要多少个n个顶部结果:
给定如下表格,包含人员、组和年龄列,你将如何获取每组中最年长的2个人?(组内的并列不应产生更多结果,而是按字母顺序给出前2个)
所需结果集:
+--------+-------+-----+ | Shawn | 1 | 42 | | Jill | 1 | 34 | | Laura | 2 | 39 | | Paul | 2 | 36 | +--------+-------+-----+

注意:这个问题是在上一个问题的基础上构建的- 获取每个分组的最大值记录的SQL查询结果 - 用于从每个分组获取单个顶部行,并得到了@Bohemian提供的出色的MySQL特定答案:

select * 
from (select * from mytable order by `Group`, Age desc, Person) x
group by `Group`

我很想能够在此基础上进行构建,但我不知道如何做。

这篇关于编程问题的文章可能对你有所帮助:http://stackoverflow.com/questions/11158917/get-n-per-group-mysql 和 https://dev59.com/HHI95IYBdhLWcg3w1Bvo - Charles Forest
2
检查这个示例。它非常接近您所要求的内容:https://dev59.com/B0nSa4cB1Zd3GeqPQLXO - Savas Vedova
在GROUP BY中使用LIMIT获取每个分组的N条结果? - Edye Chan
12个回答

108

以下是一种方法,使用UNION ALL (参见带演示的SQL Fiddle)。如果您有超过两个组,则需要指定group编号并为每个group添加查询:

(
  select *
  from mytable 
  where `group` = 1
  order by age desc
  LIMIT 2
)
UNION ALL
(
  select *
  from mytable 
  where `group` = 2
  order by age desc
  LIMIT 2
)

有多种方法可以实现此操作,请参考本文以确定最适合您情况的路线:http://www.xaprb.com/blog/2006/12/07/how-to-select-the-firstleastmax-row-per-group-in-sql/ 编辑:
这个方法也可能适用于您,它为每个记录生成一个行号。使用上面链接中的示例,这将仅返回行号小于或等于2的记录:
select person, `group`, age
from 
(
   select person, `group`, age,
      (@num:=if(@group = `group`, @num +1, if(@group := `group`, 1, 1))) row_number 
  from test t
  CROSS JOIN (select @num:=0, @group:=null) c
  order by `Group`, Age desc, person
) as x 
where x.row_number <= 2;

See Demo


76
如果他有1000多个组,那岂不是有点可怕? - Charles Forest
1
@CharlesForest 是的,这就是为什么我说你必须为超过两个组指定它。否则会变得很丑陋。 - Taryn
1
@CharlesForest,我想我找到了一个更好的解决方案,请看我的编辑。 - Taryn
2
给任何阅读此文的人一个提示:变量的版本已经接近正确。但是,MySQL不能保证在SELECT中表达式的计算顺序(实际上,有时会按照不同的顺序计算)。解决方案的关键是将所有变量赋值放在单个表达式中;这里是一个示例链接:http://stackoverflow.com/questions/38535020/getting-the-latest-n-records-for-each-group/38535249#38535249。 - Gordon Linoff
1
@GordonLinoff 我更新了我的回答,感谢你指出来。我更新它的时间也太长了。 - Taryn
显示剩余7条评论

83

在其他数据库中,您可以使用ROW_NUMBER来实现此操作。MySQL不支持ROW_NUMBER,但是您可以使用变量来模拟它:

SELECT
    person,
    groupname,
    age
FROM
(
    SELECT
        person,
        groupname,
        age,
        @rn := IF(@prev = groupname, @rn + 1, 1) AS rn,
        @prev := groupname
    FROM mytable
    JOIN (SELECT @prev := NULL, @rn := 0) AS vars
    ORDER BY groupname, age DESC, person
) AS T1
WHERE rn <= 2

在线演示地址: sqlfiddle


编辑:我刚刚注意到bluefeet发了一个非常相似的答案,给他+1。然而,这个答案有两个小优点:

  1. 这是一个单一的查询。变量初始化在SELECT语句内部。
  2. 它按照问题中描述的方式处理平局(按名称字母顺序排列)。

因此,如果可以帮助某些人,我会把它留在这里。


2
马克-这对我们来说很有效。感谢您提供另一个很好的选择来补充@bluefeet的工作-非常感激。 - Yarin
+1。这对我很有用。非常干净和简洁的答案。您能否解释一下这是如何工作的?背后的逻辑是什么? - Aditya Hajare
3
不错的解决方案,但在我的环境中(MySQL 5.6)似乎不起作用,因为order by子句应用于select之后,所以它不能返回顶部结果,请看我提供的替代方案来解决这个问题。 - Laurent PELE
在运行这个程序时,我成功删除了 JOIN (SELECT @prev := NULL, @rn := 0) AS vars。我理解这是为了声明空变量,但对于 MySql 来说似乎是多余的。 - Joseph Cho
1
这在MySQL 5.7中对我非常有效,但如果有人能解释一下它是如何工作的,那就太棒了。 - George B
显示剩余4条评论

56

试试这个:

SELECT a.person, a.group, a.age FROM person AS a WHERE 
(SELECT COUNT(*) FROM person AS b 
WHERE b.group = a.group AND b.age >= a.age) <= 2 
ORDER BY a.group ASC, a.age DESC

演示


7
突然出现的Snuffin提供了最简单的解决方案!这个方法比Ludo/Bill Karwin更优雅吗?可以给我一些评论吗? - Yarin
嗯,不确定是否更优雅。但从投票结果来看,我猜bluefeet可能有更好的解决方案。 - snuffn
3
有一个问题。如果小组中有两个团队获得第二名,只会返回一个最高结果。请查看修改后的demo - Yarin
4
如果这是需要的,那么这不是问题。您可以设置 a.person 的顺序。 - Alberto Leal
不,它在我的情况下不起作用,演示也是如此。 - Choix
1
这个算法不具有良好的可扩展性 -- 它的时间复杂度为O(n*n)。 - Rick James

35

使用自连接如何:

CREATE TABLE mytable (person, groupname, age);
INSERT INTO mytable VALUES('Bob',1,32);
INSERT INTO mytable VALUES('Jill',1,34);
INSERT INTO mytable VALUES('Shawn',1,42);
INSERT INTO mytable VALUES('Jake',2,29);
INSERT INTO mytable VALUES('Paul',2,36);
INSERT INTO mytable VALUES('Laura',2,39);

SELECT a.* FROM mytable AS a
  LEFT JOIN mytable AS a2 
    ON a.groupname = a2.groupname AND a.age <= a2.age
GROUP BY a.person
HAVING COUNT(*) <= 2
ORDER BY a.groupname, a.age DESC;

给我:

a.person    a.groupname  a.age     
----------  -----------  ----------
Shawn       1            42        
Jill        1            34        
Laura       2            39        
Paul        2            36      

我受到了Bill Karwin的回答的强烈启发,其回答地址为选择每个类别前10条记录

此外,我正在使用SQLite,但这也适用于MySQL。

还有一件事:在上面的代码中,为了方便,我用一个groupname列替换了group列。

编辑:

针对OP关于丢失平局结果的评论,我在snuffin的答案基础上进行了改进以显示所有的平局。这意味着如果最后几个是平局,则可能会返回超过2行,如下所示:

.headers on
.mode column

CREATE TABLE foo (person, groupname, age);
INSERT INTO foo VALUES('Paul',2,36);
INSERT INTO foo VALUES('Laura',2,39);
INSERT INTO foo VALUES('Joe',2,36);
INSERT INTO foo VALUES('Bob',1,32);
INSERT INTO foo VALUES('Jill',1,34);
INSERT INTO foo VALUES('Shawn',1,42);
INSERT INTO foo VALUES('Jake',2,29);
INSERT INTO foo VALUES('James',2,15);
INSERT INTO foo VALUES('Fred',1,12);
INSERT INTO foo VALUES('Chuck',3,112);


SELECT a.person, a.groupname, a.age 
FROM foo AS a 
WHERE a.age >= (SELECT MIN(b.age)
                FROM foo AS b 
                WHERE (SELECT COUNT(*)
                       FROM foo AS c
                       WHERE c.groupname = b.groupname AND c.age >= b.age) <= 2
                GROUP BY b.groupname)
ORDER BY a.groupname ASC, a.age DESC;

给我:

person      groupname   age       
----------  ----------  ----------
Shawn       1           42        
Jill        1           34        
Laura       2           39        
Paul        2           36        
Joe         2           36        
Chuck       3           112      

@Ludo- 刚刚看到了Bill Karwin的回答 - 感谢您在这里应用它。 - Yarin
你认为Snuffin的回答怎么样?我正在尝试比较这两个。 - Yarin
2
这里有一个问题。如果在该组内有第二名并列的情况,只返回一个最佳结果- 请见demo - Yarin
1
@Ludo- 最初的要求是每个组返回确切的n个结果,任何平局都按字母顺序解决。 - Yarin
编辑以包括关联对我无效。 我得到了 ERROR 1242 (21000): Subquery returns more than 1 row,可能是因为 GROUP BY。 当我单独执行 SELECT MIN 子查询时,它会生成三行:34、39、112,其中第二个值应该是36而不是39。 - verbamour
与之前的答案类似,很好但在我的情况下不起作用:缺少记录或第二个订单项排序错误。 - Choix

19

如果你有很多行,那么Snuffin解决方案执行起来似乎非常慢,而Mark Byers / Rick James和Bluefeet的解决方案在我的环境(MySQL 5.6)上不起作用,因为order by是在select执行后应用的,所以这里是Marc Byers / Rick James解决方案的一个变体,以解决这个问题(带有额外的嵌套选择):

select person, groupname, age
from
(
    select person, groupname, age,
    (@rn:=if(@prev = groupname, @rn +1, 1)) as rownumb,
    @prev:= groupname 
    from 
    (
        select person, groupname, age
        from persons 
        order by groupname ,  age desc, person
    )   as sortedlist
    JOIN (select @prev:=NULL, @rn :=0) as vars
) as groupedlist 
where rownumb<=2
order by groupname ,  age desc, person;

我在一个拥有500万行数据的表上尝试了类似的查询,并且在不到3秒钟内返回了结果。


3
这是在我的环境中唯一有效的查询。谢谢! - herrherr
5
在带有 ORDER BY 的派生表中添加 LIMIT 9999999。这 可能 防止忽略 ORDER BY - Rick James
我在一个包含几千行的表上运行了类似的查询,它花费了60秒才返回一个结果,所以...感谢这篇文章,对我来说是一个开始。(更新:现在只需要5秒钟。很好!) - Evan
1
这是与订单完美配合的查询。下面的答案效果不佳。谢谢。 - emmanuel sio
1
哇,真的很好,我能够通过这种方法显著提高性能!顺便说一下,在Spring JPA Repository代码中使用nativeQuery时,我不得不使用双反斜杠\\:=转义所有变量赋值。 - RAM237

12
如果其他答案不够快,请尝试使用此代码
SELECT
        province, n, city, population
    FROM
      ( SELECT  @prev := '', @n := 0 ) init
    JOIN
      ( SELECT  @n := if(province != @prev, 1, @n + 1) AS n,
                @prev := province,
                province, city, population
            FROM  Canada
            ORDER BY
                province   ASC,
                population DESC
      ) x
    WHERE  n <= 3
    ORDER BY  province, n;

输出:

+---------------------------+------+------------------+------------+
| province                  | n    | city             | population |
+---------------------------+------+------------------+------------+
| Alberta                   |    1 | Calgary          |     968475 |
| Alberta                   |    2 | Edmonton         |     822319 |
| Alberta                   |    3 | Red Deer         |      73595 |
| British Columbia          |    1 | Vancouver        |    1837970 |
| British Columbia          |    2 | Victoria         |     289625 |
| British Columbia          |    3 | Abbotsford       |     151685 |
| Manitoba                  |    1 | ...

看了你的网站 - 城市人口数据源在哪里获取?谢谢和问候。 - Vérace
https://www.maxmind.com/en/worldcities 这个网站非常方便,可以用来进行经纬度搜索,查询,分区等实验。它的数据量足够大,但又足够易读,可以轻松找到答案。加拿大子集对于这种问题非常有用。(比美国城市少一些省份。) - Rick James
@RickJames 谢谢,这个一次就成功了。我已经花了三个小时尝试做这件事情,但是失败惨重。 - dimButTries
@dimButTries - 这是一个棘手的代码。我看到了很多表现不佳的帖子。因此,我着手寻找(或创建)最佳解决方案。 - Rick James

10

看看这个:

SELECT
  p.Person,
  p.`Group`,
  p.Age
FROM
  people p
  INNER JOIN
  (
    SELECT MAX(Age) AS Age, `Group` FROM people GROUP BY `Group`
    UNION
    SELECT MAX(p3.Age) AS Age, p3.`Group` FROM people p3 INNER JOIN (SELECT MAX(Age) AS Age, `Group` FROM people GROUP BY `Group`) p4 ON p3.Age < p4.Age AND p3.`Group` = p4.`Group` GROUP BY `Group`
  ) p2 ON p.Age = p2.Age AND p.`Group` = p2.`Group`
ORDER BY
  `Group`,
  Age DESC,
  Person;

SQL Fiddle:http://sqlfiddle.com/#!2/cdbb6/15


5
其他人发现了更简单的解决方案......而我花了15分钟时间,却为自己想出了一个非常复杂的解决方案感到无比自豪。真糟糕。 - Travesty3
我必须找到一个内部版本号,比当前版本号小1,这让我得出了以下解决方案:max(internal_version - 1),所以不用太担心 :) - Jamie S
这个版本可以返回三个项目,假设第二个和第三个项目具有相同的年龄值。我该如何解决这个问题? - NINSIIMA WILBER
@NINSIIMAWILBER 你可能需要查看其他可能的解决方案,使用 ROW_NUMBER 或类似的东西。看起来原始答案来自 Bill Karwin,已经有一个更近期(5年前)的更新,可能适用于你。 - Travesty3

7
WITH cte_window AS (
SELECT movie_name,director_id,release_date,
ROW_NUMBER() OVER( PARTITION BY director_id ORDER BY release_date DESC) r
FROM movies
)   
SELECT * FROM cte_window WHERE r <= <n>;

上述查询将返回每个导演最新的n部电影。

比被接受的答案更好,谢谢 Hiren! - Surfoo

2

我想分享一下,因为我花了很长时间寻找一种简单的方法来在我正在开发的java程序中实现这个。 这并不完全给出你想要的输出结果,但它接近。 MySQL中名为GROUP_CONCAT()的函数非常适合指定每个组返回多少个结果。 使用LIMIT或任何其他尝试使用COUNT来实现此目的的高级方法对我无效。因此,如果您愿意接受修改后的输出,则是一个很好的解决方案。 假设我有一个名为“student”的表,其中包括学生ID、性别和GPA。 假设我想为每个性别获取前5个gpas。 那么我可以像这样编写查询:

SELECT sex, SUBSTRING_INDEX(GROUP_CONCAT(cast(gpa AS char ) ORDER BY gpa desc), ',',5) 
AS subcategories FROM student GROUP BY sex;

请注意,参数“5”告诉它将多少条目连接成每行。
输出结果可能类似于:
+--------+----------------+
| Male   | 4,4,4,4,3.9    |
| Female | 4,4,3.9,3.9,3.8|
+--------+----------------+

您还可以更改ORDER BY变量,以不同的方式对其进行排序。因此,如果我有学生的年龄,我可以用'age desc'替换'gpa desc',它也能起作用!您还可以将变量添加到group by语句中,以在输出中获得更多列。因此,这只是我发现的一种相当灵活的方法,如果您只想列出结果,那么它效果很好。


1

MySQL - How To Get Top N Rows per Each Group中有一个非常好的解决方案,针对此问题。

根据参考链接中的解决方案,您的查询将如下所示:

SELECT Person, Group, Age
   FROM
     (SELECT Person, Group, Age, 
                  @group_rank := IF(@group = Group, @group_rank + 1, 1) AS group_rank,
                  @current_group := Group 
       FROM `your_table`
       ORDER BY Group, Age DESC
     ) ranked
   WHERE group_rank <= `n`
   ORDER BY Group, Age DESC;

其中n是前n个,your_table是表名。

我认为参考文献中的解释非常清晰。为了快速参考,我将在此处复制并粘贴:

目前MySQL不支持ROW_NUMBER()函数,该函数可以在组内分配序列号,但作为解决方法,我们可以使用MySQL会话变量。

这些变量不需要声明,并且可以在查询中用于计算和存储中间结果。

@current_country := country 这段代码对每一行执行,并将country列的值存储到@current_country变量中。

@country_rank := IF(@current_country = country, @country_rank + 1, 1) 在此代码中,如果@current_country相同,则增加等级,否则将其设置为1。对于第一行,@current_country为NULL,因此等级也设置为1。

为了正确排名,我们需要按国家、人口降序排序。


这是Marc Byers、Rick James和我解决方案所使用的原则。 - Laurent PELE
很难说哪篇文章(Stack Overflow还是SQLlines)是第一篇。 - Laurent PELE
@LaurentPELE - 我的发表于2015年2月。我在SQLlines上看不到时间戳或名称。MySQL博客已经存在了很长一段时间,其中一些已经过时,应该被移除--人们正在引用错误信息。 - Rick James

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接