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

Mysql 获取每组分组结果的前n条记录,mysql,sql,greatest-n-per-group,mysql-variables,Mysql,Sql,Greatest N Per Group,Mysql Variables,以下是最简单的示例,尽管任何解决方案都应该能够扩展到需要多少n个top结果: 下面的表格中有“人”、“组”和“年龄”列,您如何得到每组中最年长的两个人?组内的联系不应该产生更多的结果,而是按字母顺序给出前2个结果 +--------+-------+-----+ | Person | Group | Age | +--------+-------+-----+ | Bob | 1 | 32 | | Jill | 1 | 34 | | Shawn | 1 |

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

下面的表格中有“人”、“组”和“年龄”列,您如何得到每组中最年长的两个人?组内的联系不应该产生更多的结果,而是按字母顺序给出前2个结果

+--------+-------+-----+ | Person | Group | Age | +--------+-------+-----+ | Bob | 1 | 32 | | Jill | 1 | 34 | | Shawn | 1 | 42 | | Jake | 2 | 29 | | Paul | 2 | 36 | | Laura | 2 | 39 | +--------+-------+-----+
我很希望能够在此基础上进行构建,但我不知道如何实现。

这里有一种方法可以做到这一点,使用UNION ALL-see。这适用于两个组,如果您有两个以上的组,则需要指定组号并为每个组添加查询:

有多种方法可以做到这一点,请参阅本文以确定适合您情况的最佳路线:

编辑:

这可能也适用于您,它会为每条记录生成一个行号。使用上面链接中的示例,这将仅返回行号小于或等于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;

请参见

如何使用自连接:

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;
比尔·卡温给我的回答让我深受鼓舞

另外,我正在使用SQLite,但这应该可以在MySQL上使用

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

编辑:

继OP关于缺少平局结果的评论之后,我增加了snuffin的答案以显示所有平局。这意味着如果最后一行是tie,则可以返回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;
试试这个:

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
看看这个:

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 FIDLE:

在其他数据库中,您可以使用行号执行此操作。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
在线查看它的工作情况:

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

这是一个单一的查询。变量在SELECT语句中初始化。 它按名称的字母顺序处理问题中描述的领带。
因此,我将把它留在这里,以防它能帮助别人。

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

在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

Snuffin解决方案在有大量行的情况下执行起来相当慢,Mark Byers/Rick James和BlueFoots解决方案在我的环境MySQL 5.6上不起作用,因为order by是在执行select之后应用的,因此下面是Marc Byers/Rick James解决方案的一个变体,它通过一个额外的重叠select解决了这个问题:

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秒钟的时间内返回结果。我想与大家分享这一点,因为我花了很长时间在我正在开发的java程序中寻找一种简单的方法来实现它。这并没有给出你想要的结果,但已经很接近了。mysql中名为GROUP_CONCAT的函数在指定每个组中返回多少结果方面非常有效。使用限制或任何其他花哨的方法来尝试对COUNT执行此操作对我都不起作用。因此,如果您愿意接受修改后的输出,这是一个很好的解决方案。假设我有一个名为“学生”的表格,上面有学生ID、性别和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|
+--------+----------------+

您还可以按变量更改顺序,并以不同的方式对其进行排序。因此,如果我有学生的年龄,我可以用“年龄描述”替换“gpa描述”,它会起作用!您还可以向GROUPBY语句添加变量,以在输出中获得更多列。因此,我发现这是一种非常灵活的方法,如果您对只列出结果感到满意,那么这种方法非常有效

对于这个问题,目前有一个非常好的答案

根据引用链接中的解决方案,您的查询如下:

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,而您的_表是表的名称

我认为参考资料中的解释非常清楚。为了快速参考,我将复制并粘贴到此处:

目前MySQL不支持可以分配的行数函数 组中的序列号,但作为一种解决方法,我们可以使用MySQL 会话变量

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

@current_country:=针对每行和每行执行此代码的国家/地区 将country列的值存储到@current\u country变量

@国家排名:=IF@current_country=国家,国家排名+1,1 在此代码中,如果@current_country相同,则我们增加等级, 否则将其设置为1。对于第一行,@current_country为空,因此 等级也设置为1

为了获得正确的排名,我们需要按国家、人口、数量和数量进行排序

也许能帮你查一下这个例子


乐。这与您的问题非常接近:使用GROUP BY中的LIMIT来获得每组N个结果?如果他有1000多个群组,这不是有点吓人吗?@CharlesForest是的,是的,这就是为什么我说你必须为两个以上的群组指定它。这会变得很难看。@CharlesForest我想我找到了一个更好的解决方案,请参阅我的editA注释,以供阅读:版本是变量接近正确。但是,MySQL不保证SELECT中表达式的求值顺序,事实上,有时会无序求值。解决方案的关键是将所有变量赋值放在一个表达式中;这里有一个例子:.@GordonLinoff更新了我的答案,谢谢你指出。我花了太长时间才更新它。@Ludo-刚刚看到-谢谢你在这里应用它你觉得Snuffin的答案怎么样?我在试着比较这两者,这有个问题。如果组中第二名的结果是并列的,则只返回一个排名靠前的结果-请参见@Ludo-最初的要求是每个组返回精确的n个结果,所有的结果都按字母顺序进行解析。编辑以包含这些结果对我来说不起作用。我得到了错误1242 21000:子查询返回的行数超过1行,可能是因为GROUP BY。当我单独执行SELECT MIN子查询时,它会生成三行:34、39、112,第二行的值应该是36,而不是39。伙计,其他人找到了更简单的解决方案……我花了大约15分钟的时间在这个问题上,并且为自己提出了如此复杂的解决方案而感到无比自豪。那太糟糕了。我必须找到一个比当前版本少1的内部版本号-这给了我这样做的答案:maxinternal_版本-1-所以压力更小:snuffin用最简单的解决方案不知从哪里冒出来!这比卢多的更优雅吗?我能得到一些评论吗嗯,不确定它是否更优雅。但从投票结果来看,我想蓝脚可能有更好的解决方案。这有一个问题。如果组内第二名的成绩持平,则只返回一个排名靠前的结果。看,如果需要的话,这不是问题。您可以设置a.person的顺序。不,它在我的情况下不起作用,演示工作标记也不起作用-这对我们很有效。感谢您为“蓝脚”恭维提供了另一个很好的选择,非常感谢。这对我有用。回答得很清楚,切中要害。你能解释一下这到底是怎么回事吗?这背后的逻辑是什么?不错的解决方案,但它似乎在我的环境MySQL 5.6中不起作用,因为order by子句是在select之后应用的,因此它不会返回顶级结果,请参阅我的替代解决方案以解决此问题。运行此解决方案时,我可以删除JOIN select@prev:=NULL,@rn:=0作为VAR。我的想法是声明空变量,但这似乎与MySql无关。这在MySql 5.7中对我来说非常有用,但如果有人能解释它在你的网站上是如何工作的,那就太棒了——我从哪里可以获得城市人口的数据源?TIA和rgs我发现它可以方便地用于测试、查询、分区等。它足够大,可以让人感兴趣,但可读性也足以识别答案。加拿大子集对于这类问题很方便。省份比美国城市少。@RickJames谢谢你,这是第一次成功,我花了3个小时试图做到这一点,但失败得很惨。@Dimbutries-这是一个棘手的代码。我看到很多帖子都表现不佳。因此,我开始寻找或创建最佳解决方案。这是在我的环境中唯一有效的查询。谢谢将LIMIT 999999添加到具有ORDER BY的任何派生表中。这可以防止忽略顺序。我在一个包含几千行的表上运行了一个类似的查询,返回一个结果花了60秒,所以。。。谢谢你的帖子,这对我来说是个开始。预计到达时间:降到5秒。好的这是一个与订单完美配合的查询。下面的答案并不适用。谢天谢地,这是Marc Byers、Rick James和我的解决方案所使用的原理。很难说哪个是堆栈后溢出还是SQLlinesfirst@LaurentPELE-2015年2月发布了我的信息。我在SQLlines上看不到时间戳或名称。MySQL博客已经存在了足够长的时间,其中一些已经过时,应该删除——人们引用了错误的信息。随着8.0和10.2成为GA,这个答案变得合理了。@RickJames“成为GA”意味着什么?窗口函数很好地解决了我的问题。@iedmrc-GA表示一般可用。它是为黄金时段做好准备或发布的技术演讲。他们正在开发这个版本,并将重点关注他们错过的bug。该链接讨论了MySQL 8.0的实现,它可能不同于MariaDB 10.2的实现。
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 | ...
select Person,[group],age
from
(
select * ,row_number() over(partition by [group] order by age desc) rn
from mytable
) t
where rn <= 2
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;
SELECT sex, SUBSTRING_INDEX(GROUP_CONCAT(cast(gpa AS char ) ORDER BY gpa desc), ',',5) 
AS subcategories FROM student GROUP BY sex;
+--------+----------------+
| Male   | 4,4,4,4,3.9    |
| Female | 4,4,3.9,3.9,3.8|
+--------+----------------+
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;
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