GROUP BY内でLIMITを使用して、グループごとにN個の結果を取得しますか?


388

次のクエリ:

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

SELECT * FROM(SELECT年、ID、料金FROM h WHERE年BETWEEN 2000から2009 AND AND IN(SELECT rid FROM table2)GROUP BY id、year ORDER BY id、rate DESC)LIMIT 5
Mixcoatl

回答:


115

GROUP_CONCAT集計関数を使用して、すべての年を1つの列にまとめ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_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レートORDER BYレート)を使用することを検討する必要があることに注意してください。

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


3
これは、見事に実行され、比較的単純で、優れた説明です。どうもありがとうございます。最後のポイントまで、合理的な最大長を計算できる場合SET SESSION group_concat_max_len = <maximum length>;は、OPの場合は問題なし(デフォルトは1024であるため)を使用できますが、例として、group_concat_max_lenは少なくとも25:4(max年の長さの文字列)+ 1(区切り文字)、5(最初の5年)の倍数。文字列はエラーをスローするのではなく切り捨てられるため、などの警告に注意してください1054 rows in set, 789 warnings (0.31 sec)
ティモシージョンズ

私は私が使うべきものではなく、1〜5よりも正確な2行をフェッチしますFIND_IN_SET()。試してみましたFIND_IN_SET() =2が、期待通りの結果が出ませんでした。
Amogh

FIND_IN_SET BETWEEN 1および5は、サイズが5以上の場合にGROUP_CONCATの最初の5桁を取得します。したがって、FIND_IN_SET = 2は、GROUP_CONCATの2番目の位置のデータのみを取得します。2つの行を取得すると、セットに2つの行があると仮定して、1番目と2番目の位置に対してBETWEEN 1と2を試すことができます。
jDub9 2018年

このソリューションは、大規模なデータセットに対して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_RANKまたはROW_NUMBER関数を使用するだけです。

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

7
idの値を変更すると、ランクのカウントが再開されるため、重要な部分はORDER BY idであることに言及する価値があると思います。
ruuter、2015

応答を取得するために2回実行する必要があるのはなぜ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ます。ORDER BYこれを組み合わせて使用​​すると、ANSI / ISO SQL標準2008/2011/2016は、deliverd /サブクエリを合法的にするFETCH 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;

あなたのソリューションは完璧に機能しましたが、サブクエリから年や他の列も取得したいのですが、どうすればいいですか?
MaNn

9

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

これは、グループワイズ最大タイプのクエリであり、SQLで行うのは簡単ではありません。いくつかのケースではより効率的な方法に対処するためにさまざまな方法がありますが、一般的にトップnの場合は、同様の前の質問に対するビルの回答を確認する必要があります。

この問題のほとんどのソリューションと同様に、同じrate値の行が複数ある場合は5行を超えることがあるので、それを確認するために大量の後処理が必要になる場合があります。


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 2017年

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"-> "rownum <= 3"をwhere句に追加します(t3のwhere句)。

「年」を選択-> where句に「BETWEEN 2000 AND 2009」を追加します(t3のwhere節)。


同じIDで繰り返すレートがある場合、rowNumカウントが高くなるため、これは機能しません。行ごとに3は得られず、0、1、または2が得られます。これに対する解決策はありますか?
スターバター2016年

@starvatorは、「t1.rate <= t2.rate」を「t1.rate <t2.rate」に変更します。最高レートが同じIDで同じ値を持っている場合、それらすべてに同じRownumがありますが、それ以上高くなりません。「rate p in id p01」のように、繰り返される場合、「t1.rate <t2.rate」を使用すると、「rate 8 in id p01」は両方とも同じ行番号0になります。「t1.rate <= t2.rate」を使用する場合、rownumは2です。
Wang Wen'an 2016

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で有効になっているウィンドウ関数(別名分析関数)があれば、良い解決策が得られますが、そうではありません。上記の投稿で使用されているトリックは、「MySQLの貧弱な人のウィンドウ関数」と呼ばれることもあるGROUP_CONCATを利用しています。


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
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.