グループごとに最大/最小<何でも>のレコードを取得する


88

どうやってするか?

この質問の以前のタイトルは「ランクを使用した複雑なクエリでランクを使用(@Rank:= @Rank + 1)-機能しますか?」でしたが、ランクを使用したソリューションを探していましたが、Billが投稿したソリューションがはるかに良い。

元の質問:

定義された順序を指定して、各グループから最後のレコードを取得するクエリを作成しようとしています。

SET @Rank=0;

select s.*
from (select GroupId, max(Rank) AS MaxRank
      from (select GroupId, @Rank := @Rank + 1 AS Rank 
            from Table
            order by OrderField
            ) as t
      group by GroupId) as t 
  join (
      select *, @Rank := @Rank + 1 AS Rank
      from Table
      order by OrderField
      ) as s 
  on t.GroupId = s.GroupId and t.MaxRank = s.Rank
order by OrderField

@Rank := @Rank + 1は通常、ランクに使用されますが、2つのサブクエリで使用すると疑わしいように見えますが、初期化は1回だけです。このように機能しますか?

次に、複数回評価される1つのサブクエリで動作しますか?where(またはhaving)句のサブクエリのように(上記を記述する別の方法):

SET @Rank=0;

select Table.*, @Rank := @Rank + 1 AS Rank
from Table
having Rank = (select max(Rank) AS MaxRank
              from (select GroupId, @Rank := @Rank + 1 AS Rank 
                    from Table as t0
                    order by OrderField
                    ) as t
              where t.GroupId = table.GroupId
             )
order by OrderField

前もって感謝します!


2
より高度な質問はここにstackoverflow.com/questions/9841093/...
TMS

回答:


174

OrderFieldグループごとに最高の行を取得したいですか?私はこのようにします:

SELECT t1.*
FROM `Table` AS t1
LEFT OUTER JOIN `Table` AS t2
  ON t1.GroupId = t2.GroupId AND t1.OrderField < t2.OrderField
WHERE t2.GroupId IS NULL
ORDER BY t1.OrderField; // not needed! (note by Tomas)

Tomasによる編集:同じグループ内に同じOrderFieldを持つレコードがさらにあり、それらの1つだけが必要な場合は、条件を拡張することができます。

SELECT t1.*
FROM `Table` AS t1
LEFT OUTER JOIN `Table` AS t2
  ON t1.GroupId = t2.GroupId 
        AND (t1.OrderField < t2.OrderField 
         OR (t1.OrderField = t2.OrderField AND t1.Id < t2.Id))
WHERE t2.GroupId IS NULL

編集の終わり。)

つまり、同じ以上t1の行をt2持つ他の行が存在しない行を返しGroupIdますOrderFieldt2.*がNULLの場合、これは左外部結合でそのような一致が見つからなかったため、グループ内でのt1最大値を持っていることを意味していますOrderField

ランクなし、サブクエリなし。に複合インデックスがある場合、これは高速に実行され、「インデックスの使用」でt2へのアクセスが最適化されます(GroupId, OrderField)


パフォーマンスについては、各グループの最後のレコードの取得に対する私の回答を参照してください。スタックオーバーフローデータダンプを使用して、サブクエリメソッドと結合メソッドを試しました。違いは顕著です。私のテストでは、joinメソッドの実行が278倍速くなりました。

最良の結果を得るには、適切なインデックスがあることが重要です。

@Rank変数を使用するメソッドに関しては、クエリが最初のテーブルを処理した後に@Rankの値がゼロにリセットされないため、作成したとおりに機能しません。例を示します。

グループごとに最大であることがわかっている行を除いて、nullの追加フィールドを使用して、いくつかのダミーデータを挿入しました。

select * from `Table`;

+---------+------------+------+
| GroupId | OrderField | foo  |
+---------+------------+------+
|      10 |         10 | NULL |
|      10 |         20 | NULL |
|      10 |         30 | foo  |
|      20 |         40 | NULL |
|      20 |         50 | NULL |
|      20 |         60 | foo  |
+---------+------------+------+

最初のグループのランクが3に、2番目のグループのランクが6に増加し、内部クエリがこれらを正しく返すことを示すことができます。

select GroupId, max(Rank) AS MaxRank
from (
  select GroupId, @Rank := @Rank + 1 AS Rank
  from `Table`
  order by OrderField) as t
group by GroupId

+---------+---------+
| GroupId | MaxRank |
+---------+---------+
|      10 |       3 |
|      20 |       6 |
+---------+---------+

次に、結合条件なしでクエリを実行して、すべての行のデカルト積を強制し、すべての列もフェッチします。

select s.*, t.*
from (select GroupId, max(Rank) AS MaxRank
      from (select GroupId, @Rank := @Rank + 1 AS Rank 
            from `Table`
            order by OrderField
            ) as t
      group by GroupId) as t 
  join (
      select *, @Rank := @Rank + 1 AS Rank
      from `Table`
      order by OrderField
      ) as s 
  -- on t.GroupId = s.GroupId and t.MaxRank = s.Rank
order by OrderField;

+---------+---------+---------+------------+------+------+
| GroupId | MaxRank | GroupId | OrderField | foo  | Rank |
+---------+---------+---------+------------+------+------+
|      10 |       3 |      10 |         10 | NULL |    7 |
|      20 |       6 |      10 |         10 | NULL |    7 |
|      10 |       3 |      10 |         20 | NULL |    8 |
|      20 |       6 |      10 |         20 | NULL |    8 |
|      20 |       6 |      10 |         30 | foo  |    9 |
|      10 |       3 |      10 |         30 | foo  |    9 |
|      10 |       3 |      20 |         40 | NULL |   10 |
|      20 |       6 |      20 |         40 | NULL |   10 |
|      10 |       3 |      20 |         50 | NULL |   11 |
|      20 |       6 |      20 |         50 | NULL |   11 |
|      20 |       6 |      20 |         60 | foo  |   12 |
|      10 |       3 |      20 |         60 | foo  |   12 |
+---------+---------+---------+------------+------+------+

上記から、グループごとの最大ランクが正しいことがわかりますが、@ Rankは2番目の派生テーブルを処理するにつれて、7以上に増加し続けます。したがって、2番目の派生テーブルのランクは、最初の派生テーブルのランクとまったく重複しません。

2つのテーブルを処理する間に@Rankを強制的にゼロにリセットする別の派生テーブルを追加する必要があります(オプティマイザーがテーブルを評価する順序を変更しないことを希望するか、そうでなければSTRAIGHT_JOINを使用してそれを防ぎます)。

select s.*
from (select GroupId, max(Rank) AS MaxRank
      from (select GroupId, @Rank := @Rank + 1 AS Rank 
            from `Table`
            order by OrderField
            ) as t
      group by GroupId) as t 
  join (select @Rank := 0) r -- RESET @Rank TO ZERO HERE
  join (
      select *, @Rank := @Rank + 1 AS Rank
      from `Table`
      order by OrderField
      ) as s 
  on t.GroupId = s.GroupId and t.MaxRank = s.Rank
order by OrderField;

+---------+------------+------+------+
| GroupId | OrderField | foo  | Rank |
+---------+------------+------+------+
|      10 |         30 | foo  |    3 |
|      20 |         60 | foo  |    6 |
+---------+------------+------+------+

しかし、このクエリの最適化はひどいものです。インデックスを使用できず、2つの一時テーブルを作成し、それらを困難な方法でソートし、一時テーブルを結合するときにもインデックスを使用できないため、結合バッファーを使用します。これはからの出力例EXPLAINです:

+----+-------------+------------+--------+---------------+------+---------+------+------+---------------------------------+
| id | select_type | table      | type   | possible_keys | key  | key_len | ref  | rows | Extra                           |
+----+-------------+------------+--------+---------------+------+---------+------+------+---------------------------------+
|  1 | PRIMARY     | <derived4> | system | NULL          | NULL | NULL    | NULL |    1 | Using temporary; Using filesort |
|  1 | PRIMARY     | <derived2> | ALL    | NULL          | NULL | NULL    | NULL |    2 |                                 |
|  1 | PRIMARY     | <derived5> | ALL    | NULL          | NULL | NULL    | NULL |    6 | Using where; Using join buffer  |
|  5 | DERIVED     | Table      | ALL    | NULL          | NULL | NULL    | NULL |    6 | Using filesort                  |
|  4 | DERIVED     | NULL       | NULL   | NULL          | NULL | NULL    | NULL | NULL | No tables used                  |
|  2 | DERIVED     | <derived3> | ALL    | NULL          | NULL | NULL    | NULL |    6 | Using temporary; Using filesort |
|  3 | DERIVED     | Table      | ALL    | NULL          | NULL | NULL    | NULL |    6 | Using filesort                  |
+----+-------------+------------+--------+---------------+------+---------+------+------+---------------------------------+

一方、左外部結合を使用する私のソリューションは、はるかに最適化されます。一時テーブルもレポートも使用しません"Using index"。つまり、データに触れることなく、インデックスのみを使用して結合を解決できます。

+----+-------------+-------+------+---------------+---------+---------+-----------------+------+--------------------------+
| id | select_type | table | type | possible_keys | key     | key_len | ref             | rows | Extra                    |
+----+-------------+-------+------+---------------+---------+---------+-----------------+------+--------------------------+
|  1 | SIMPLE      | t1    | ALL  | NULL          | NULL    | NULL    | NULL            |    6 | Using filesort           |
|  1 | SIMPLE      | t2    | ref  | GroupId       | GroupId | 5       | test.t1.GroupId |    1 | Using where; Using index |
+----+-------------+-------+------+---------------+---------+---------+-----------------+------+--------------------------+

ブログで「joinはSQLを遅くする」と主張する人を読むでしょうが、それはナンセンスです。不十分な最適化はSQLを遅くします。


これは(OPについても)非常に役立つかもしれませんが、残念なことに、2つの質問のどちらにも答えません。
Andriy M

Billに感謝します。これはランクを回避する方法としては良い考えですが、...参加が遅くなることはありませんか?結合(where句の制限なし)は、私のクエリよりもはるかに大きなサイズになります。とにかく、アイデアをありがとう!しかし、元の質問、つまりランクがこのように機能するかどうかについても興味深いでしょう。
TMS 2012年

すばらしい回答をありがとう、ビル。ただし、@Rank1およびを使用した場合はどうなり@Rank2ますか?それで問題は解決しますか?それはあなたの解決策よりも速いでしょうか?
TMS 2012年

とを使用@Rank1@Rank2ても違いはありません。
Bill Karwin、2012年

2
その素晴らしい解決策をありがとう。私はその問題で長い間苦労していました。他のフィールド(「foo」など)にフィルターを追加したい人は、... AND t1.foo = t2.foo後で正しい結果を得るためにそれらを結合条件に追加する必要がありますWHERE ... AND foo='bar'
ownking
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.