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


140

以下是最简单的示例,尽管任何解决方案都应能够扩展到需要许多n个顶级结果的地方:

给定下面的表格,其中包含“人员”,“组”和“年龄”列,您将如何获得每个组中年龄最大的2个人?(组内的关系不应产生更多结果,而应按字母顺序给出前2个)

+ -------- + ------- + ----- +
| 人 集团| 年龄|
+ -------- + ------- + ----- +
| 鲍勃| 1 | 32 |
| 吉尔| 1 | 34 |
| 肖恩| 1 | 42 |
| 杰克 2 | 29 |
| 保罗| 2 | 36 |
| 劳拉| 2 | 39 |
+ -------- + ------- + ----- +

所需的结果集:

+ -------- + ------- + ----- +
| 肖恩| 1 | 42 |
| 吉尔| 1 | 34 |
| 劳拉| 2 | 39 |
| 保罗| 2 | 36 |
+ -------- + ------- + ----- +

注意:这个问题建立在先前的问题上- 获取每组分组的SQL结果的最大值的记录 -用于从每组中获取一个顶行,并且从@Bohemian那里得到了一个MySQL特有的答案:

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

我希望能够以此为基础,尽管我不知道如何做到。



2
检查这个例子。这是非常接近你的要求:stackoverflow.com/questions/1537606/...
萨瓦斯VEDOVA

在GROUP BY中使用LIMIT获得每个组N个结果?stackoverflow.com/questions/2129693/...
Edye陈

Answers:


88

这是使用的一种方法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;

观看演示


52
如果他有1000多个小组,这会不会有点吓人?
查尔斯·森林

1
@CharlesForest是的,这就是为什么,我说过,您必须为两个以上的组指定它。它将变得丑陋。
Taryn

1
@CharlesForest我想我找到了一个更好的解决方案,请参见我的编辑
Taryn

1
任何阅读此内容的人的注释:版本是变量,几乎是正确的。但是,MySQL不保证表达式中的求值顺序SELECT(实际上,有时会无序地求值)。解决方案的关键是将所有变量赋值放在一个表达式中。这是一个示例: stackoverflow.com/questions/38535020/…
Gordon Linoff '16

1
@GordonLinoff更新了我的答案,谢谢指出。我也花了很长时间来更新它。
Taryn

63

在其他数据库中,您可以使用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. 它按照问题中的描述处理联系(按名称的字母顺序)。

因此,我将其保留在此处,以防它可以帮助某人。


1
马克-这对我们来说很好。感谢您为恭维@bluefeet提供了另一个很好的选择-非常感谢。
Yarin 2012年

+1。这对我有用。真的很干净而且很关键。您能解释一下这是如何工作的吗?这背后的逻辑是什么?
Aditya Hajare

3
不错的解决方案,但似乎在我的环境中不起作用(MySQL 5.6),因为在选择后应用了order by子句,因此它不会返回顶部结果,请参阅我的替代解决方案来解决此问题
Laurent PELE

运行此程序时,我可以删除JOIN (SELECT @prev := NULL, @rn := 0) AS vars。我的想法是声明空变量,但对于MySql似乎是多余的。
乔·乔

1
在MySQL 5.7中,这对我来说非常有用,但是如果有人可以解释它的工作原理,那就太好了
George B

41

试试这个:

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

演示


6
用最简单的解决方案从无到有的喘不过气来!这比Ludo / Bill Karwin的优雅吗?我可以评论一下吗
Yarin 2012年

嗯,不确定它是否更优雅。但是从投票的角度来看,我猜bluefeet可能有更好的解决方案。
snuffn 2012年

2
这有问题。如果组中并列第二名,则仅返回一个最佳结果。观看修改后的演示
Yarin 2012年

2
如果需要,这不是问题。您可以设置的顺序a.person
艾伯托·里尔

不,在我的情况下不起作用,演示也不起作用
Choix

31

如何使用自我加入:

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上使用。

另一件事:在上文中,为方便起见,我将group列替换为列groupname

编辑

在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 2012年

2
这有问题。如果小组中
并列

1
@ Ludo-最初的要求是每个小组返回准确的n个结果,并且任何联系都按字母顺序解决
Yarin 2012年

包含联系的编辑对我不起作用。我得到了ERROR 1242 (21000): Subquery returns more than 1 row,大概是因为GROUP BY。当我SELECT MIN单独执行子查询时,它将生成三行:34, 39, 112看来第二个值应该是36,而不是
39。– verbamour

12

当您有很多行时,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

3
LIMIT 9999999将任何带有的表添加到表中ORDER BY。这可以防止ORDER BY被忽略。
瑞克·詹姆斯

我在包含数千行的表上运行了类似的查询,并且花了60秒返回一个结果,所以...感谢您的发帖,这对我来说是一个开始。(ETA:下降到5秒。好!)
Evan,

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小提琴:http ://sqlfiddle.com/#!2/cdbb6/15


5
伙计,其他人找到了更简单的解决方案……我仅仅花了15分钟的时间,就为自己提出如此复杂的解决方案而感到无比自豪。糟透了。
Travesty3,2012年

我必须找到一个比当前版本少1的内部版本号-这给了我这样做的答案:max(internal_version - 1)-减轻压力了:)
Jamie Strauss

8

如果其他答案不够快,请尝试以下代码

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 | ...

看了您的网站-我在哪里可以得到城市人口的数据源?TIA和RG。
Vérace

maxmind.com/en/worldcities- 我发现它非常适合尝试lat / lng搜索,查询,分区等。它足够大,很有趣,但可读性足以识别答案。加拿大子集对于此类问题非常有用。(省份少于美国城市。)
里克·詹姆斯

2

我想分享这一点,因为我花了很长时间寻找在我正在研究的Java程序中实现此目标的简单方法。这并不能完全提供您要查找的输出,但可以提供接近的输出。mysql中调用的函数在GROUP_CONCAT()指定每个组要返回多少结果时效果很好。使用LIMIT或使用任何其他奇特的方法尝试执行此操作COUNT对我来说都不起作用。因此,如果您愿意接受修改后的输出,那么这是一个很好的解决方案。假设我有一个名为“学生”的表格,其中包含学生证,他们的性别和gpa。可以说,我希望每个性别的最高GPA达到5。然后我可以这样写查询

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变量并以其他方式对其进行排序。因此,如果我有学生的年龄,则可以将“ gpa desc”替换为“ age desc”,它将起作用!您还可以将变量添加到group by语句,以在输出中获取更多列。因此,这只是我发现的一种非常灵活的方法,如果您只列出结果,可以很好地工作。


0

SQL Server row_numer()中的一项强大功能可以轻松获得结果,如下所示

select Person,[group],age
from
(
select * ,row_number() over(partition by [group] order by age desc) rn
from mytable
) t
where rn <= 2

有了8.0和10.2作为GA,这个答案就变得合理了。
瑞克·詹姆斯

@RickJames“成为GA”是什么意思?窗口函数(dev.mysql.com/doc/refman/8.0/en/window-functions.html)很好地解决了我的问题。
iedmrc '18 -10-1

1
@iedmrc-“ GA”表示“通用”。它在技术上代表“准备黄金时间”或“已发布”。他们正在开发版本,并将专注于他们错过的错误。该链接讨论了MySQL 8.0的实现,该实现可能与MariaDB 10.2的实现不同。
瑞克·詹姆斯

-1

MySQL上,对这个问题有一个非常好的答案-如何获取每个组的前N行

根据引用链接中的解决方案,您的查询将类似于:

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;

这里ntop nyour_table是你的表的名称。

我认为参考资料中的解释很明确。为了快速参考,我将在此处复制并粘贴它:

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

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

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

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

为了正确排名,我们需要按国家/地区,人口DESC排序


好吧,这是Marc Byers,Rick James和我的解决方案所使用的原理。
Laurent PELE

很难说哪个职位(堆栈溢出或SQLlines)是第
洛朗贝利

@LaurentPELE-我的发表于2015年2月。我在SQLlines上没有看到时间戳或名称。MySQL博客已经存在了很长时间,以至于有些博客已经过时了,应该将其删除-人们引用错误信息。
瑞克·詹姆斯
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.