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

Posted

技术标签:

【中文标题】获取每组分组结果的前 n 条记录【英文标题】:Get top n records for each group of grouped results 【发布时间】:2021-09-01 17:25:16 【问题描述】:

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

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

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

想要的结果集:

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

注意:这个问题建立在前一个问题的基础上 - Get records with max value for each group of grouped SQL results - 用于从每个组中获取一个顶行,并且从@Bohemian 获得了一个很好的 mysql 特定答案:

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

希望能够以此为基础,虽然我不知道如何。

【问题讨论】:

***.com/questions/11158917/get-n-per-group-mysql 和 ***.com/questions/2129693/… 可能会对您有所帮助 检查这个例子。它非常接近您的要求:***.com/questions/1537606/… 在 GROUP BY 中使用 LIMIT 以获得每组 N 个结果? ***.com/questions/2129693/… 【参考方案1】:

这是执行此操作的一种方法,使用UNION ALL(请参阅SQL Fiddle with Demo)。这适用于两个组,如果您有两个以上的组,那么您需要指定 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;

见Demo

【讨论】:

如果他有 1 000 多个组,那会不会有点吓人? @CharlesForest 是的,它会,这就是为什么我说你必须为两个以上的组指定它。它会变得丑陋。 @CharlesForest 我想我找到了更好的解决方案,请参阅我的编辑 给任何阅读本文的人的注意事项:版本是变量接近正确。但是,MySQL 不保证 SELECT 中表达式的求值顺序(事实上,有时会乱序求值)。解决方案的关键是将所有变量赋值放在一个表达式中;这是一个例子:***.com/questions/38535020/…. @GordonLinoff 更新了我的答案,感谢您指出。我也花了太长时间来更新它。【参考方案2】:

在其他数据库中,您可以使用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 给他。然而,这个答案有两个小优点:

    这是一个单一的查询。变量在 SELECT 语句中初始化。 它处理问题中描述的关系(按名称按字母顺序排列)。

所以我会把它留在这里以防它可以帮助某人。

【讨论】:

Mark- 这对我们很有效。感谢您提供另一个不错的选择来赞美 @bluefeet's- 非常感谢。 +1。这对我有用。真的很干净,很中肯的答案。你能解释一下这是如何工作的吗?这背后的逻辑是什么? 不错的解决方案,但它似乎在我的环境(MySQL 5.6)中不起作用,因为在 select 之后应用了 order by 子句,因此它不会返回最佳结果,请参阅我的替代解决方案来解决这个问题问题 运行时我可以删除JOIN (SELECT @prev := NULL, @rn := 0) AS vars。我的想法是声明空变量,但对于 MySql 来说似乎无关紧要。 这在 MySQL 5.7 中非常适合我,但如果有人能解释它是如何工作的就太好了【参考方案3】:

试试这个:

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

DEMO

【讨论】:

snuffin 用最简单的解决方案冒出来!这比 Ludo 的/Bill Karwin's 更优雅吗?我可以得到一些评论 嗯,不知道是不是更优雅。但从投票来看,我猜 bluefeet 可能有更好的解决方案。 这个有问题。如果组内第二名并列,则只返回一个排名靠前的结果。见修改demo 如果需要,这不是问题。可以设置a.person的顺序。 不,在我的情况下它不起作用,DEMO 也不起作用【参考方案4】:

如何使用自连接:

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 对Select top 10 records for each category 的回答让我深受鼓舞

另外,我正在使用 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- 刚刚看到 answer from Bill Karwin - 感谢您在这里应用它 你觉得Snuffin的回答怎么样?我正在尝试比较两者 这个有问题。如果组内第二名并列,则只返回一个***结果 - 见demo @Ludo- 最初的要求是每个组返回确切的 n 个结果,任何关系都按字母顺序解决 包含关系的编辑对我不起作用。我得到ERROR 1242 (21000): Subquery returns more than 1 row,大概是因为GROUP BY。当我单独执行 SELECT MIN 子查询时,它会生成三行:34, 39, 112 并且看起来第二个值应该是 36,而不是 39。【参考方案5】:

当您有大量行并且 Mark Byers/Rick James 和 Bluefeet 解决方案在我的环境(MySQL 5.6)上不起作用时,Snuffin 解决方案似乎执行起来很慢,因为 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 秒的时间内返回结果

【讨论】:

这是在我的环境中运行的唯一查询。谢谢! LIMIT 9999999 添加到任何带有ORDER BY 的派生表中。这可能防止ORDER BY被忽略。 我在包含几千行的表上运行了一个类似的查询,返回一个结果需要 60 秒,所以...感谢您的帖子,这对我来说是一个开始。 (预计到达时间:减少到 5 秒。好!) 这是与订单完美配合的查询。下面的答案效果不佳。谢谢 哇,真的很好,我能够使用这种方法显着提高性能!顺便说一句,必须在 nativeQuery 的 spring jpa 存储库代码中使用双反斜杠 \\:= 转义所有变量分配。【参考方案6】:

看看这个:

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

【讨论】:

伙计,其他人找到了更简单的解决方案……我只花了大约 15 分钟的时间,并且为自己提出了如此复杂的解决方案而感到无比自豪。太糟糕了。 我必须找到一个比当前版本小 1 的内部版本号 - 这给了我这样做的答案:max(internal_version - 1) - 所以压力更小:)【参考方案7】:

如果其他答案不够快,请尝试this code:

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 和 rgs。 maxmind.com/en/worldcities -- 我发现它对于尝试lat/lng searches、查询、分区等很方便。它足够大,很有趣,但可读性足以识别答案。对于这类问题,加拿大子集很方便。 (省份少于美国城市。) @RickJames 谢谢,这是第一次成功,我花了 3 个小时试图做到这一点,但惨遭失败。 @dimButTries - 这是一个棘手的代码。我看到很多帖子表现不佳。所以我开始寻找(或创造)最佳解决方案。【参考方案8】:

在 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 -“GA”表示“普遍可用”。它是“准备好迎接黄金时间”或“发布”的技术术语。他们正在开发版本,并将专注于他们错过的错误。该链接讨论了 MySQL 8.0 的实现,这可能与 MariaDB 10.2 的实现不同。【参考方案9】:

我想分享这个,因为我花了很长时间寻找一种在我正在开发的 java 程序中实现它的简单方法。这并不能完全给出您正在寻找的输出,但它很接近。 mysql 中名为GROUP_CONCAT() 的函数在指定每个组中返回多少个结果方面非常有效。使用LIMIT 或任何其他尝试对COUNT 执行此操作的奇特方式对我不起作用。因此,如果您愿意接受修改后的输出,这是一个很好的解决方案。假设我有一张名为“学生”的表格,其中包含学生 ID、性别和 gpa。可以说我想为每个性别获得前 5 个 gpa。然后我可以这样写查询

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 语句以在输出中获取更多列。所以这只是我发现的一种非常灵活的方式,如果你只列出结果就可以了。

【讨论】:

【参考方案10】:

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;

其中ntop nyour_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,因此 rank 也设置为 1。

为了正确的排名,我们需要 ORDER BY country, population DESC

【讨论】:

嗯,这是 Marc Byers、Rick James 和我的解决方案使用的原理。 很难说哪个帖子(堆栈溢出或 SQLlines)是第一个 @LaurentPELE - 我的发布于 2015 年 2 月。我在 SQLlines 上看不到时间戳或名称。 MySQL 博客已经存在了很长时间,以至于其中一些已经过时,应该被删除——人们引用了错误信息。【参考方案11】:
SELECT
p1.Person,
p1.`GROUP`,
p1.Age  
   FROM
person AS p1 
 WHERE
(
SELECT
    COUNT( DISTINCT ( p2.age ) ) 
FROM
    person AS p2 
WHERE
    p2.`GROUP` = p1.`GROUP` 
    AND p2.Age >= p1.Age 
) < 2 
ORDER BY
p1.`GROUP` ASC,
p1.age DESC

reference leetcode

【讨论】:

【参考方案12】:
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 部电影。

【讨论】:

以上是关于获取每组分组结果的前 n 条记录的主要内容,如果未能解决你的问题,请参考以下文章

Oracle分组查询取每组排序后的前N条记录

MySQL取每组的前N条记录

mysql使用GROUP BY分组实现取前N条记录的方法

MySQL分组查询后如何获取每组的前N条数据,你会吗?

mysql分组后,取每组的前3条数据(并且有顺序)

分组取前N记录(转)