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

时间:2012-08-24 17:00:44

标签: mysql sql greatest-n-per-group mysql-variables

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

根据下面的表格,对于人员,小组和年龄栏,您如何获得每组中最老的2个人?(组内的关系不应该产生更多结果,但是给予按字母顺序排列的前2个)

+--------+-------+-----+
| Person | Group | Age |
+--------+-------+-----+
| Bob    | 1     | 32  |
| Jill   | 1     | 34  |
| Shawn  | 1     | 42  |
| Jake   | 2     | 29  |
| Paul   | 2     | 36  |
| Laura  | 2     | 39  |
+--------+-------+-----+

期望的结果集:

+--------+-------+-----+
| Shawn  | 1     | 42  |
| Jill   | 1     | 34  |
| Laura  | 2     | 39  |
| Paul   | 2     | 36  |
+--------+-------+-----+

注意:此问题建立在前一个问题的基础上 - Get records with max value for each group of grouped SQL results - 从每个组中获取一个顶行,并从@Bohemian收到一个特定于MySQL的答案:< / p>

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

我希望能够建立起来,但我不知道如何。

10 个答案:

答案 0 :(得分:82)

以下是使用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 :(得分:55)

在其他数据库中,您可以使用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. 它处理问题中描述的关系(按名称的字母顺序排列)。
  3. 所以我会留在这里,以防它可以帮助别人。

答案 2 :(得分:34)

试试这个:

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

答案 3 :(得分: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对Select top 10 records for each category

的答案给了我很大的启发

另外,我正在使用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;

给了我:

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

答案 4 :(得分: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 :(得分:8)

Snuffin解决方案似乎很慢执行,当你&#39;已经得到了很多没有按行和马克·拜尔斯/里克·詹姆斯和Bluefeet解决方案&#39;在我的环境在(的MySQL 5.6)笔的工作,因为为了通过执行后应用选择,所以这里是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;

我在一个有5百万行的表上尝试了类似的查询,它在不到3秒的时间内返回结果

答案 6 :(得分:5)

如果其他答案不够快,请尝试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 | ...

答案 7 :(得分:2)

我想分享这个,因为我花了很长时间在我正在研究的java程序中寻找一种简单的方法来实现它。这并没有给出你正在寻找的输出,但它已经接近了。 mysql中的函数GROUP_CONCAT()非常适合指定每组返回多少结果。使用LIMIT或任何其他尝试使用COUNT执行此操作的方式对我来说都不起作用。因此,如果您愿意接受修改后的输出,那么这是一个很好的解决方案。可以说我有一张叫做“学生”的桌子。与学生ID,他们的性别和gpa。让我们说我想为每个性别排名前5加法。然后我可以写这样的查询

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

请注意参数&#39; 5&#39;告诉它要连接到每一行的条目数

输出看起来像

+--------+----------------+
| Male   | 4,4,4,4,3.9    |
| Female | 4,4,3.9,3.9,3.8|
+--------+----------------+

您还可以更改ORDER BY变量,并以不同的方式对其进行排序。因此,如果我有学生的年龄,我可以更换“gpa desc&#39;与年龄desc&#39;它会工作!您还可以将变量添加到group by语句以在输出中获取更多列。所以这只是我发现的一种非常灵活的方式,如果只需列出结果就可以了。

答案 8 :(得分:0)

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

答案 9 :(得分:-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;

其中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国家,人口DESC