グループ化された結果の各グループの上位nレコードを取得する


140

次の例は最も単純な例ですが、どのようなソリューションでもn個の上位結果が必要な場合にスケーリングできるはずです。

以下のようなテーブルにperson、group、ageの列がある場合、各グループで最も年長の2人をどのように取得しますか?(グループ内のタイはより多くの結果をもたらすべきではありませんが、アルファベット順で最初の2つを与えます)

+ -------- + ------- + ----- +
| 人| グループ| 年齢|
+ -------- + ------- + ----- +
| ボブ| 1 | 32 |
| ジル| 1 | 34 |
| ショーン| 1 | 42 |
| ジェイク| 2 | 29 |
| ポール| 2 | 36 |
| ローラ| 2 | 39 |
+ -------- + ------- + ----- +

望ましい結果セット:

+ -------- + ------- + ----- +
| ショーン| 1 | 42 |
| ジル| 1 | 34 |
| ローラ| 2 | 39 |
| ポール| 2 | 36 |
+ -------- + ------- + ----- +

注:この質問は、前の1つに基づいており、グループ化されたSQL結果の各グループの最大値を持つレコードを取得します -各グループから1つの上位行を取得し、@ BohemianからMySQL固有の素晴らしい回答を受け取りました。

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

どうすればよいかわかりませんが、これを構築できるようになりたいと思います。



2
この例を確認してください。それはあなたが求めるものにはかなり近いです:stackoverflow.com/questions/1537606/...
サバスVedova

GROUP BY内でLIMITを使用して、グループごとにN個の結果を取得しますか?stackoverflow.com/questions/2129693/...
Edyeチャン

回答:


88

これを使用する方法の1つを次に示しますUNION ALL(「デモ付きのSQL Fiddle」を参照)。これは2つのグループで機能し、3つ以上のグループがある場合は、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;

デモを見る


52
もし彼が1000以上のグループを持っているとしたら、それはこれを少し恐ろしくするのではないでしょうか?
Charles Forest

1
@CharlesForestはい、そうです。それが、3つ以上のグループに対して指定する必要があると私が述べた理由です。醜くなります。
タリン

1
@CharlesForest私はより良い解決策を見つけたと思います。私の編集を参照してください
Taryn

1
これを読んでいる人への注意:バージョンは変数であり、ほぼ正しいです。ただし、MySQLはの式の評価順序を保証しませんSELECT(実際に、順序どおりに評価されないこともあります)。ソリューションの鍵は、すべての変数の割り当てを1つの式に入れることです。ここに例があります: stackoverflow.com/questions/38535020/…
ゴードンリノフ2016

1
@GordonLinoff指摘してくれてありがとう。また、更新に時間がかかりすぎました。
タリン

63

他のデータベースでは、を使用してこれを行うことができます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が非常によく似た答えを投稿したことに気づきました。ただし、この回答には2つの小さな利点があります。

  1. それは単一のクエリです。変数はSELECTステートメント内で初期化されます。
  2. 質問(名前のアルファベット順)で説明されているように、ネクタイを処理します。

誰かを助けることができるように、ここに置いておきます。


1
Mark-これは私たちにとってはうまく機能しています。@bluefeetのお世辞に代わる別の良い代替手段を提供してくれてありがとう。
Yarin

+1。これでうまくいきました。本当にきれいで、正解です。これがどのように機能するのか正確に説明できますか?この背後にあるロジックは何ですか?
Aditya Hajare 2017

3
素晴らしいソリューションですが、私の環境(MySQL 5.6)では、selectの後にorder by句が適用され、上位の結果が返されないため、機能していないようです。この問題を修正するための代替ソリューションを参照してください
Laurent PELE

これを実行中に、私は削除することができましたJOIN (SELECT @prev := NULL, @rn := 0) AS vars。空の変数を宣言するという考えはわかりますが、MySqlには無関係のようです。
Joseph Cho

1
これはMySQL 5.7では私にとってはうまく機能しますが、誰かがそれがどのように機能するかを説明できればすばらしいでしょう
George B

41

これを試して:

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

デモ


6
最も簡単な解決策でどこからともなく出るスナッフィン!これはLudo / Bill Karwinよりもエレガントですか?コメントを
お願いし

うーん、もっとエレガントかどうかはわかりません。しかし、投票から判断すると、bluefeetがより良い解決策になると思います。
2012

2
これには問題があります。グループ内で2位に同点がある場合、上位の結果が1つだけ返されます。変更されたデモを
Yarin

2
必要であれば問題ありません。の順序を設定できa.personます。
Alberto Leal 2017

いいえ、私の場合は機能しません。デモも機能しません
Choix

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      

各カテゴリのトップ10レコード選択するという Bill Karwinからの回答に強く刺激を受けました

また、私はSQLiteを使用していますが、これはMySQLで動作するはずです。

もう1つ:上記では、便宜上groupgroupname列を列に置き換えました。

編集

欠落しているタイの結果に関するOPのコメントのフォローアップとして、私はすべてのタイを表示するためにスナッフィンの回答を増やしました。つまり、最後の行が同数の場合、以下に示すように、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      

Ludo-ただ、@その見たビルKarwinから答えを -ここでそれを適用するためのおかげで
Yarin

スナフィンの答えをどう思いますか?私は2つを比較しようとしています
Yarin

2
これには問題があります。グループ内で2位が同点の場合、上位の結果が1つだけ返されます
Yarin

1
@ Ludo-元の要件は、各グループが正確なn個の結果を返し、すべての関係がアルファベット順に解決されることでした
Yarin

同点を含めるための編集は、私には機能しません。私が取得ERROR 1242 (21000): Subquery returns more than 1 rowおそらくの、GROUP BY。私が実行した場合SELECT MINのみサブクエリを、それが三列を生成します。34, 39, 112そして、第二の値が36、ではない39でなければなりませんが表示されます
verbamour

12

多数の行があり、Mark Byers / Rick JamesおよびBluefeetソリューションが私の環境(MySQL 5.6)で機能しない場合、selectの実行後にorder byが適用されるため、Snuffinソリューションの実行は非常に遅いようです。この問題を修正するための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;

500万行のテーブルで同様のクエリを実行したところ、3秒未満で結果が返されました。


3
これは私の環境で動作している唯一のクエリです。ありがとう!
2008年

3
を使用LIMIT 9999999して派生テーブルに追加しますORDER BY。これにより、が無視されるのを防ぐことができORDER BYます。
リックジェームズ

数千行を含むテーブルで同様のクエリを実行したところ、1つの結果が返されるまでに60秒かかりました。そのため、投稿ありがとうございます。(ETA:
エヴァン

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
男、他の人ははるかに簡単な解決策を見つけました...私はこれに15分ほど費やしただけで、そのような複雑な解決策を考え出したことも信じられないほど誇りに思っていました。それは最悪です。
Travesty3 2012

私は現在よりも1少ない内部バージョン番号を見つける必要がありました-これは私にこれを行うための答えを与えました:max(internal_version - 1)-それほどストレスが少ない:)
ジェイミー・シュトラウス

8

他の答えが十分に速くない場合このコードを試してください:

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

あなたのサイトを見ました-都市の人口のデータソースはどこで入手できますか?TIAとrgs。
Vérace

maxmind.com/en/worldcities-lat / lng検索、クエリ、パーティショニングなど を試すのに便利です。興味深いほど大きく、答えを認識するのに十分なほど読みやすくなっています。カナダのサブセットはこの種の質問に便利です。(米国の都市より州が少ない。)
リック・ジェームズ

2

これを共有したかったのは、私が取り組んでいるJavaプログラムにこれを実装する簡単な方法を探すのに長い時間を費やしたからです。これはあなたが探している出力を完全に与えるわけではありませんが、それに近いものです。mysqlで呼び出された関数はGROUP_CONCAT()、各グループで返す結果の数を指定するのに非常によく機能しました。を使用LIMITしてこれを実行しようとする他の派手な方法を使用してもCOUNTうまくいきませんでした。したがって、変更された出力を受け入れる用意がある場合は、これは優れたソリューションです。学生ID、性別、GPAを含む「学生」というテーブルがあるとします。性別ごとに5 gpaを上位にしたいとしましょう。それから私はこのようなクエリを書くことができます

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

パラメータ「5」は、各行に連結するエントリの数を示します。

そして、出力は次のようになります

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

ORDER BY変数を変更して、別の方法でそれらを並べ替えることもできます。したがって、学生の年齢があった場合、「gpa desc」を「age desc」に置き換えることができ、それは機能します!group byステートメントに変数を追加して、出力により多くの列を取得することもできます。したがって、これは私が見つけた方法であり、かなり柔軟性があり、結果を一覧表示するだけで大​​丈夫であればうまく機能します。


0

SQL Server row_numer()では、次のように簡単に結果を取得できる強力な関数です。

select Person,[group],age
from
(
select * ,row_number() over(partition by [group] order by age desc) rn
from mytable
) t
where rn <= 2

8.0と10.2がGAであることから、この答えは合理的になりつつあります。
リックジェームズ

@RickJames「GAであること」とはどういう意味ですか?ウィンドウ関数(dev.mysql.com/doc/refman/8.0/en/window-functions.html)は私の問題を非常によく解決しました。
iedmrc

1
@iedmrc-「GA」は「一般提供」を意味します。それは「プライムタイムの準備ができている」、または「リリースされた」のテックスピークです。彼らはバージョンの開発を終えており、見逃したバグに焦点を合わせます。そのリンクでは、MySQL 8.0の実装について説明しています。これは、MariaDB 10.2の実装とは異なる場合があります。
リックジェームズ

-1

MySQLでこの問題に対する本当に良い答えがあります-各グループごとに上位N行を取得する方法

参照リンクのソリューションに基づくと、クエリは次のようになります。

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 nそしてyour_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なので、ランクはまた、1に設定します。

正しいランキングを得るには、ORDER BY国、人口DESCが必要です


まあ、それはマークバイヤーズ、リックジェームスと私のソリューションによって使用される原則です。
Laurent PELE

どの投稿(Stack OverflowまたはSQLlines)が最初であるかを言うのは難しい
Laurent PELE

@LaurentPELE-Mineは2015年2月に投稿されました。SQLlinesにタイムスタンプや名前は表示されません。MySQLブログは古くなっており、古くなっているものもあり、削除する必要があります。人々は誤った情報を引用しています。
リックジェームズ
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.