467

以下查询:

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 的修饰符来做到这一点?

4

13 回答 13

147

您可以使用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

使用 and 的组合,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)rate不是在year列上使用。

返回的字符串的最大长度GROUP_CONCAT是有限的,因此如果您需要为每个组选择几条记录,这很有效。

于 2013-03-23T09:47:24.877 回答
134

原始查询使用用户变量和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 或更高版本中,只需使用RANK,DENSE_RANKROW_NUMBER函数:

SELECT *
FROM (
    SELECT *, RANK() OVER (PARTITION BY id ORDER BY rate DESC) AS rnk
    FROM t
) AS x
WHERE rnk <= 5
于 2015-05-15T21:49:31.187 回答
22

对我来说像

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;
于 2013-10-04T15:35:28.027 回答
13

不,您不能任意限制子查询(在较新的 MySQL 中您可以在有限的范围内这样做,但每组不能有 5 个结果)。

这是一个 groupwise-maximum 类型的查询,这在 SQL 中并不简单。有多种方法可以解决在某些情况下可能更有效的问题,但对于一般情况下的 top-n,您需要查看Bill 对先前类似问题的回答。

与此问题的大多数解决方案一样,如果有多行具有相同的rate值,它可以返回超过五行,因此您可能仍需要大量的后处理来检查它。

于 2010-01-25T01:37:08.147 回答
10

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

@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;
于 2012-11-02T00:11:19.733 回答
10
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)
于 2012-11-29T00:04:32.177 回答
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;
于 2013-01-05T09:37:40.427 回答
5

构建虚拟列(如Oracle中的RowID)</p>

桌子:

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" --> 添加rownum <=3inwhere子句(t3 的 where 子句);

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

于 2016-05-09T10:16:02.497 回答
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

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

于 2016-10-25T13:20:14.150 回答
2

以下帖子:sql:选择每组前 N 条记录描述了在没有子查询的情况下实现此目的的复杂方法。

它通过以下方式改进了此处提供的其他解决方案:

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

然而它并不漂亮。如果在 MySQL 中启用了窗口函数(又名分析函数),则可以实现一个好的解决方案——但它们不是。上述帖子中使用的技巧利用了 GROUP_CONCAT,它有时被描述为“穷人的 MySQL 窗口函数”。

于 2012-07-06T17:10:04.957 回答
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 个限制

于 2012-12-17T17:14:52.773 回答
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;
于 2014-12-24T08:07:44.157 回答
1

请尝试以下存储过程。我已经验证过了。我得到了正确的结果,但没有使用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
于 2016-07-15T12:39:33.477 回答