在GROUP BY中使用LIMIT获得每个组N个结果?


385

以下查询:

SELECT
year, id, rate
FROM h
WHERE year BETWEEN 2000 AND 2009
AND id IN (SELECT rid FROM table2)
GROUP BY id, year
ORDER BY id, rate DESC

产量:

year    id  rate
2006    p01 8
2003    p01 7.4
2008    p01 6.8
2001    p01 5.9
2007    p01 5.3
2009    p01 4.4
2002    p01 3.9
2004    p01 3.5
2005    p01 2.1
2000    p01 0.8
2001    p02 12.5
2004    p02 12.4
2002    p02 12.2
2003    p02 10.3
2000    p02 8.7
2006    p02 4.6
2007    p02 3.3

我想要的只是每个ID的前5个结果:

2006    p01 8
2003    p01 7.4
2008    p01 6.8
2001    p01 5.9
2007    p01 5.3
2001    p02 12.5
2004    p02 12.4
2002    p02 12.2
2003    p02 10.3
2000    p02 8.7

有没有一种方法可以使用在GROUP BY中起作用的某种LIMIT之类的修饰符?


10
这可以在MySQL中完成,但并不像添加LIMIT子句那样简单。这是一篇详细解释该问题的文章:如何在SQL中选择每个组的第一行/最少行/最大行这是一篇很好的文章-他为“每组前N个”问题介绍了一种优雅而幼稚的解决方案,然后逐步对此有所改善。
danben 2010年

SELECT * FROM(选择年份,编号,比率,从2000年到2009年之间的h年以及ID IN(选择从表2脱离)GROUP ID,年份OR编号,比率DESC)限制5
Mixcoatl

Answers:


115

您可以使用GROUP_CONCAT聚合函数将所有年份归入一列,并按以下项分组id和排序rate

SELECT   id, GROUP_CONCAT(year ORDER BY rate DESC) grouped_year
FROM     yourtable
GROUP BY id

结果:

-----------------------------------------------------------
|  ID | GROUPED_YEAR                                      |
-----------------------------------------------------------
| p01 | 2006,2003,2008,2001,2007,2009,2002,2004,2005,2000 |
| p02 | 2001,2004,2002,2003,2000,2006,2007                |
-----------------------------------------------------------

然后,您可以使用FIND_IN_SET,它返回第一个参数在第二个参数内的位置,例如。

SELECT FIND_IN_SET('2006', '2006,2003,2008,2001,2007,2009,2002,2004,2005,2000');
1

SELECT FIND_IN_SET('2009', '2006,2003,2008,2001,2007,2009,2002,2004,2005,2000');
6

使用和的组合,GROUP_CONCAT并按FIND_IN_SETfind_in_set返回的位置进行过滤,然后可以使用此查询,该查询仅返回每个id的前5年:

SELECT
  yourtable.*
FROM
  yourtable INNER JOIN (
    SELECT
      id,
      GROUP_CONCAT(year ORDER BY rate DESC) grouped_year
    FROM
      yourtable
    GROUP BY id) group_max
  ON yourtable.id = group_max.id
     AND FIND_IN_SET(year, grouped_year) BETWEEN 1 AND 5
ORDER BY
  yourtable.id, yourtable.year DESC;

请看这里的小提琴。

请注意,如果可以有多个行具有相同的费率,则应考虑在费率列而非年份列中使用GROUP_CONCAT(DISTINCT rate ORDER BY rate)。

GROUP_CONCAT返回的字符串的最大长度是有限的,因此,如果您需要为每个组选择一些记录,则此方法很好用。


3
那是很漂亮的表现,比较简单,很好的解释;非常感谢。到最后一点,在可以计算合理的最大长度SET SESSION group_concat_max_len = <maximum length>;的情况下,可以使用OP。在OP的情况下,非问题(因为默认值为1024),但是例如,group_concat_max_len应该至少为25:4(最大年份字符串的长度)+1(分隔符)乘以5(前5年)。字符串将被截断而不是引发错误,因此请注意警告,例如1054 rows in set, 789 warnings (0.31 sec)
蒂莫西·约翰斯

如果我想获取确切的2行而不是1到5行,那我应该使用FIND_IN_SET()。我尝试FIND_IN_SET() =2但未如预期那样显示结果。
Amogh '18年

如果大小等于或大于5,则FIND_IN_SET BETWEEN 1和5将占据GROUP_CONCAT设置的前5个位置。因此FIND_IN_SET = 2将仅占据GROUP_CONCAT中第二个位置的数据。获得2行,您可以在第1和2nd位置尝试1和2之间,假设set有2行。
jDub9

对于大型数据集,此解决方案的性能比Salman的性能好得多。无论如何,我都对这两种聪明的解决方案表示赞赏。谢谢!!
tiomno

105

所述原始查询使用的用户变量和ORDER BY上派生表; 不能保证两个怪癖的行为。修改后的答案如下。

在MySQL 5.x中,您可以在分区上使用穷人排行榜,以获得所需的结果。只是外部将表与其自身连接起来,对于每一行,计算比它的行数。在上述情况下,较少的行就是较高的比率:

SELECT t.id, t.rate, t.year, COUNT(l.rate) AS rank
FROM t
LEFT JOIN t AS l ON t.id = l.id AND t.rate < l.rate
GROUP BY t.id, t.rate, t.year
HAVING COUNT(l.rate) < 5
ORDER BY t.id, t.rate DESC, t.year

演示和结果

| id  | rate | year | rank |
|-----|------|------|------|
| p01 |  8.0 | 2006 | 0    |
| p01 |  7.4 | 2003 | 1    |
| p01 |  6.8 | 2008 | 2    |
| p01 |  5.9 | 2001 | 3    |
| p01 |  5.3 | 2007 | 4    |
| p02 | 12.5 | 2001 | 0    |
| p02 | 12.4 | 2004 | 1    |
| p02 | 12.2 | 2002 | 2    |
| p02 | 10.3 | 2003 | 3    |
| p02 |  8.7 | 2000 | 4    |

请注意,如果费率有联系,例如:

100, 90, 90, 80, 80, 80, 70, 60, 50, 40, ...

上面的查询将返回6行:

100, 90, 90, 80, 80, 80

更改为HAVING COUNT(DISTINCT l.rate) < 5以获取8行:

100, 90, 90, 80, 80, 80, 70, 60

或更改为ON t.id = l.id AND (t.rate < l.rate OR (t.rate = l.rate AND t.pri_key > l.pri_key))获得5行:

 100, 90, 90, 80, 80

在MySQL 8或更高版本只使用RANKDENSE_RANKROW_NUMBER功能:

SELECT *
FROM (
    SELECT *, RANK() OVER (PARTITION BY id ORDER BY rate DESC) AS rnk
    FROM t
) AS x
WHERE rnk <= 5

7
我认为值得一提的是关键部分是ORDER BY id,因为id值的任何更改都将重新开始计入排名。
ruuter 2015年

我为什么要运行两次以得到答复WHERE rank <=5?我第一次没有从每个ID中获得5行,但是在此之后,我能够按照您所说的进行获取。
布伦诺·里尔(Brenno Leal)

@BrennoLeal我认为您忘记了该SET语句(请参阅第一个查询)。有必要。
Salman A

3
在较新的版本中,ORDER BY派生表中的可以并且经常会被忽略。这挫败了目标。在这里可以找到有效的分组方式。
瑞克·詹姆斯

1
+1您的答案重写非常有效,因为现代MySQL / MariaDB版本遵循ANSI / ISO SQL 1992/1999/2003标准,而实际上从未真正允许ORDER BY在这样的交付/子查询中使用它。.这就是为什么现代的MySQL / MariaDB版本ORDER BY无需使用in子查询LIMIT,我相信ANSI / ISO SQL Standards 2008/2011/2016 ORDER BYFETCH FIRST n ROWS ONLY
-Raymond Nijland

21

对我来说

SUBSTRING_INDEX(group_concat(col_name order by desired_col_order_name), ',', N) 

完美地工作。没有复杂的查询。


例如:每组获得前1名

SELECT 
    *
FROM
    yourtable
WHERE
    id IN (SELECT 
            SUBSTRING_INDEX(GROUP_CONCAT(id
                            ORDER BY rate DESC),
                        ',',
                        1) id
        FROM
            yourtable
        GROUP BY year)
ORDER BY rate DESC;

您的解决方案运行完美,但是我也想从子查询中检索年份和其他列,我们该怎么做?
曼恩

9

不,您不能任意限制子查询(您可以在较新的MySQL中有限地执行子查询,但不能在每个组中获得5个结果)。

这是一个按组最大类型的查询,在SQL中这并非易事。有多种解决方法,在某些情况下可能更有效,但是对于top-n而言,通常您需要看一下Bill对先前类似问题的回答。

与该问题的大多数解决方案一样,如果存在多行具有相同rate值的行,则它可以返回多于五行的内容,因此您可能仍需要大量的后处理才能对此进行检查。


9

这需要一系列子查询来对值进行排序,限制它们,然后在分组时执行求和

@Rnk:=0;
@N:=2;
select
  c.id,
  sum(c.val)
from (
select
  b.id,
  b.bal
from (
select   
  if(@last_id=id,@Rnk+1,1) as Rnk,
  a.id,
  a.val,
  @last_id=id,
from (   
select 
  id,
  val 
from list
order by id,val desc) as a) as b
where b.rnk < @N) as c
group by c.id;

9

尝试这个:

SELECT h.year, h.id, h.rate 
FROM (SELECT h.year, h.id, h.rate, IF(@lastid = (@lastid:=h.id), @index:=@index+1, @index:=0) indx 
      FROM (SELECT h.year, h.id, h.rate 
            FROM h
            WHERE h.year BETWEEN 2000 AND 2009 AND id IN (SELECT rid FROM table2)
            GROUP BY id, h.year
            ORDER BY id, rate DESC
            ) h, (SELECT @lastid:='', @index:=0) AS a
    ) h 
WHERE h.indx <= 5;

1
未知列a.type在字段列表
ANU

5
SELECT year, id, rate
FROM (SELECT
  year, id, rate, row_number() over (partition by id order by rate DESC)
  FROM h
  WHERE year BETWEEN 2000 AND 2009
  AND id IN (SELECT rid FROM table2)
  GROUP BY id, year
  ORDER BY id, rate DESC) as subquery
WHERE row_number <= 5

子查询与您的查询几乎相同。唯一的变化是增加

row_number() over (partition by id order by rate DESC)

8
很好,但是MySQL没有窗口函数(如ROW_NUMBER())。
ypercubeᵀᴹ

3
从MySQL 8.0开始,row_number()可以使用
erickg

4

构建虚拟列(如Oracle中的RowID)

表:

`
CREATE TABLE `stack` 
(`year` int(11) DEFAULT NULL,
`id` varchar(10) DEFAULT NULL,
`rate` float DEFAULT NULL) 
ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
`

数据:

insert into stack values(2006,'p01',8);
insert into stack values(2001,'p01',5.9);
insert into stack values(2007,'p01',5.3);
insert into stack values(2009,'p01',4.4);
insert into stack values(2001,'p02',12.5);
insert into stack values(2004,'p02',12.4);
insert into stack values(2005,'p01',2.1);
insert into stack values(2000,'p01',0.8);
insert into stack values(2002,'p02',12.2);
insert into stack values(2002,'p01',3.9);
insert into stack values(2004,'p01',3.5);
insert into stack values(2003,'p02',10.3);
insert into stack values(2000,'p02',8.7);
insert into stack values(2006,'p02',4.6);
insert into stack values(2007,'p02',3.3);
insert into stack values(2003,'p01',7.4);
insert into stack values(2008,'p01',6.8);

像这样的SQL:

select t3.year,t3.id,t3.rate 
from (select t1.*, (select count(*) from stack t2 where t1.rate<=t2.rate and t1.id=t2.id) as rownum from stack t1) t3 
where rownum <=3 order by id,rate DESC;

如果删除t3中的where子句,则显示如下:

在此处输入图片说明

GET“ TOP N Record”->在where子句(t3的where子句)中添加“ rownum <= 3”;

选择“年份”->在where子句(t3的where子句)中添加“ BETWEEN 2000 AND 2009”;


如果您重复相同ID的费率,则该行将不起作用,因为rowNum计数会增加;每行不会得到3,而您可以得到0、1或2。您能想到对此的任何解决方案吗?
starvator

@starvator将“ t1.rate <= t2.rate”更改为“ t1.rate <t2.rate”,如果最佳速率在相同的id中具有相同的值,则它们都具有相同的行数,但不会增加更高;类似于“ id p01中的速率8”,如果重复使用“ t1.rate <t2.rate”,则“ id p01中的速率8”都具有相同的rownum 0;如果使用“ t1.rate <= t2.rate”,则行数为2;
王文安

3

做了一些工作,但是我认为我的解决方案值得分享,因为它看起来既优雅又快速。

SELECT h.year, h.id, h.rate 
  FROM (
    SELECT id, 
      SUBSTRING_INDEX(GROUP_CONCAT(CONCAT(id, '-', year) ORDER BY rate DESC), ',' , 5) AS l
      FROM h
      WHERE year BETWEEN 2000 AND 2009
      GROUP BY id
      ORDER BY id
  ) AS h_temp
    LEFT JOIN h ON h.id = h_temp.id 
      AND SUBSTRING_INDEX(h_temp.l, CONCAT(h.id, '-', h.year), 1) != h_temp.l

请注意,此示例是出于问题的目的而指定的,并且可以很容易地出于其他类似目的而进行修改。


2

以下帖子:sql:选择每个组的前N条记录描述了无需子查询即可实现此目的的复杂方法。

它对以下提供的其他解决方案进行了改进:

  • 在一个查询中完成所有操作
  • 能够正确利用索引
  • 避免众所周知的子查询会在MySQL中产生错误的执行计划

但是,它并不漂亮。如果在MySQL中启用了窗口函数(又称分析函数),则可以实现一个好的解决方案,但事实并非如此。该文章中使用的技巧利用了GROUP_CONCAT,有时将其称为“ MySQL的穷人窗口函数”。


1

对于那些像我这样查询超时的人。我做了以下内容以使用限制和特定组的其他任何内容。

DELIMITER $$
CREATE PROCEDURE count_limit200()
BEGIN
    DECLARE a INT Default 0;
    DECLARE stop_loop INT Default 0;
    DECLARE domain_val VARCHAR(250);
    DECLARE domain_list CURSOR FOR SELECT DISTINCT domain FROM db.one;

    OPEN domain_list;

    SELECT COUNT(DISTINCT(domain)) INTO stop_loop 
    FROM db.one;
    -- BEGIN LOOP
    loop_thru_domains: LOOP
        FETCH domain_list INTO domain_val;
        SET a=a+1;

        INSERT INTO db.two(book,artist,title,title_count,last_updated) 
        SELECT * FROM 
        (
            SELECT book,artist,title,COUNT(ObjectKey) AS titleCount, NOW() 
            FROM db.one 
            WHERE book = domain_val
            GROUP BY artist,title
            ORDER BY book,titleCount DESC
            LIMIT 200
        ) a ON DUPLICATE KEY UPDATE title_count = titleCount, last_updated = NOW();

        IF a = stop_loop THEN
            LEAVE loop_thru_domain;
        END IF;
    END LOOP loop_thru_domain;
END $$

它遍历域列表,然后每个域仅插入200个限制


1

尝试这个:

SET @num := 0, @type := '';
SELECT `year`, `id`, `rate`,
    @num := if(@type = `id`, @num + 1, 1) AS `row_number`,
    @type := `id` AS `dummy`
FROM (
    SELECT *
    FROM `h`
    WHERE (
        `year` BETWEEN '2000' AND '2009'
        AND `id` IN (SELECT `rid` FROM `table2`) AS `temp_rid`
    )
    ORDER BY `id`
) AS `temph`
GROUP BY `year`, `id`, `rate`
HAVING `row_number`<='5'
ORDER BY `id`, `rate DESC;

0

请尝试以下存储过程。我已经核实了。我得到了适当的结果,但是没有使用groupby

CREATE DEFINER=`ks_root`@`%` PROCEDURE `first_five_record_per_id`()
BEGIN
DECLARE query_string text;
DECLARE datasource1 varchar(24);
DECLARE done INT DEFAULT 0;
DECLARE tenants varchar(50);
DECLARE cur1 CURSOR FOR SELECT rid FROM demo1;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = 1;

    SET @query_string='';

      OPEN cur1;
      read_loop: LOOP

      FETCH cur1 INTO tenants ;

      IF done THEN
        LEAVE read_loop;
      END IF;

      SET @datasource1 = tenants;
      SET @query_string = concat(@query_string,'(select * from demo  where `id` = ''',@datasource1,''' order by rate desc LIMIT 5) UNION ALL ');

       END LOOP; 
      close cur1;

    SET @query_string  = TRIM(TRAILING 'UNION ALL' FROM TRIM(@query_string));  
  select @query_string;
PREPARE stmt FROM @query_string;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;

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