web-dev-qa-db-ja.com

グループごとにN個の結果を得るためにGROUP BY内でLIMITを使用する

次のクエリ

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を使用してこれを行う方法はありますか?

341
Wells

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 を使うことができます。これは、2番目の引数の内側の最初の引数の位置を返します。

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_CONCATFIND_IN_SETを組み合わせて使用​​し、find_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レートORDER BYレート)をレート列に使用することを検討してください。

GROUP_CONCATによって返される文字列の最大長は制限されているため、すべてのグループに対していくつかのレコードを選択する必要がある場合、これはうまく機能します。

99
fthiella

オリジナルのクエリ は派生テーブルでユーザー変数と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

8行を取得するには、HAVING COUNT(DISTINCT l.rate) < 5に変更します。

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

5行にするには、ON t.id = l.id AND (t.rate < l.rate OR (t.rate = l.rate AND t.pri_key > l.pri_key))に変更します。

 100, 90, 90, 80, 80

MySQL 8以降では、 RANKDENSE_RANKまたはROW_NUMBER 関数を使用するだけです。

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

私にとっては

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;
16
Vishal Kumar

いいえ、サブクエリを任意に制限することはできません(新しいMySQLでは限られた範囲でしかできませんが、グループごとに5つの結果を得ることはできません)。

これはgroupwise-maximum型のクエリで、SQLでは簡単ではありません。 さまざまな方法 で対処する方法がありますが、場合によってはより効率的になることがありますが、一般的にはtop-nの場合は 以前の同様の質問に対するBillの答え

この問題に対するほとんどの解決策と同じように、同じrate値を持つ行が複数ある場合、5行以上を返すことがあるので、それを確認するためにはまだ大量の後処理が必要になるかもしれません。

9
bobince

これには、値をランク付けし、それらを制限してから、グループ化しながら合計を実行するための一連の副照会が必要です。

@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;
9
Saharsh Shah

仮想列を構築する(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節を削除すると、次のようになります。

enter image description here

GET "TOP N Record" - > where句(t3のwhere句)に "rownum <= 3"を追加します。

"the year"を選択 - > where句(t3のwhere句)に "BETWEEN 2000 AND 2009"を追加します。

4
Wang Wen'an

ある程度の作業は必要でしたが、私の解決策は、非常に高速であると同時にエレガントでもあるため、共有するものになると思います。

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

この例は質問の目的のために指定されており、他の同様の目的のために非常に簡単に修正できることに注意してください。

3
John
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)
2
Ricky Moreno

次の投稿: sql:グループごとの上位Nレコードの選択 では、副照会なしでこれを達成するための複雑な方法について説明しています。

それはここで提供される他の解決策を改良します:

  • 1回のクエリですべてを実行する
  • インデックスを適切に利用できるようになる
  • 悪名高い実行計画をMySQLで生成することで知られていることで知られている、副問い合わせの回避

しかしそれはきれいではありません。良い解決策はMySQLで有効にされたウィンドウ関数(別名分析関数)で達成可能でしょう - しかし、そうではありません。この記事で使用されているトリックはGROUP_CONCATを利用しています。これは「貧乏人のMySQL用ウィンドウ関数」とも呼ばれます。

2
Shlomi Noach

クエリがタイムアウトした私のような人のために。私は以下を制限と他の何かを特定のグループによって使用するために作りました。

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
Dev-Ria

これを試して:

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;
1
MLF

下記のストアドプロシージャをお試しください。確認済みです。 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
0
Himanshu Patel