Dapatkan rekaman dengan <whatever> tertinggi / terkecil per grup

88

Bagaimana cara melakukannya?

Judul sebelumnya dari pertanyaan ini adalah " menggunakan peringkat (@Rank: = @Rank + 1) dalam kueri kompleks dengan subkueri - apakah akan berhasil? " Karena saya sedang mencari solusi menggunakan peringkat, tetapi sekarang saya melihat bahwa solusi yang diposting oleh Bill adalah jauh lebih baik.

Pertanyaan asli:

Saya mencoba membuat kueri yang akan mengambil catatan terakhir dari setiap grup dengan beberapa urutan yang ditentukan:

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

Ekspresi @Rank := @Rank + 1biasanya digunakan untuk peringkat, tetapi bagi saya terlihat mencurigakan saat digunakan dalam 2 subkueri, tetapi hanya diinisialisasi sekali. Apakah akan bekerja seperti ini?

Dan kedua, apakah ini akan bekerja dengan satu subkueri yang dievaluasi beberapa kali? Seperti subquery di mana (atau memiliki) klausa (cara lain untuk menulis di atas):

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

Terima kasih sebelumnya!

TMS
sumber
2
pertanyaan lebih lanjut di sini stackoverflow.com/questions/9841093/…
TMS

Jawaban:

174

Jadi Anda ingin mendapatkan baris dengan jumlah tertinggi OrderFieldper grup? Saya akan melakukannya dengan cara ini:

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)

( EDIT oleh Tomas: Jika ada lebih banyak record dengan OrderField yang sama dalam grup yang sama dan Anda memerlukan tepat salah satunya, Anda mungkin ingin memperpanjang kondisi:

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

akhir pengeditan.)

Dengan kata lain, kembalikan baris t1yang tidak t2ada baris lain yang sama GroupIddan lebih besar OrderField. Ketika t2.*NULL, itu berarti left outer join tidak menemukan kecocokan tersebut, dan karena itu t1memiliki nilai terbesar OrderFielddalam grup.

Tidak ada peringkat, tidak ada subkueri. Ini akan berjalan cepat dan mengoptimalkan akses ke t2 dengan "Menggunakan indeks" jika Anda mengaktifkan indeks gabungan (GroupId, OrderField).


Mengenai kinerja, lihat jawaban saya untuk Mengambil rekor terakhir di setiap grup . Saya mencoba metode subquery dan metode bergabung menggunakan dump data Stack Overflow. Perbedaannya luar biasa: metode bergabung berjalan 278 kali lebih cepat dalam pengujian saya.

Penting bagi Anda untuk memiliki indeks yang tepat untuk mendapatkan hasil terbaik!

Mengenai metode Anda yang menggunakan variabel @Rank, ini tidak akan berfungsi seperti yang Anda tulis, karena nilai @Rank tidak akan disetel ulang ke nol setelah kueri memproses tabel pertama. Saya akan tunjukkan sebuah contoh.

Saya memasukkan beberapa data dummy, dengan bidang tambahan yang nol kecuali pada baris yang kami tahu adalah yang terbesar per grup:

select * from `Table`;

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

Kami dapat menunjukkan bahwa peringkat meningkat menjadi tiga untuk grup pertama dan enam untuk grup kedua, dan kueri bagian dalam mengembalikan ini dengan benar:

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

Sekarang jalankan kueri tanpa kondisi gabungan, untuk memaksa produk Kartesius dari semua baris, dan kami juga mengambil semua kolom:

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

Kita dapat melihat dari penjelasan di atas bahwa peringkat maksimal per grup sudah benar, tetapi @Rank terus meningkat saat memproses tabel turunan kedua, menjadi 7 dan lebih tinggi. Jadi peringkat dari tabel turunan kedua tidak akan pernah tumpang tindih dengan peringkat dari tabel turunan pertama sama sekali.

Anda harus menambahkan tabel turunan lain untuk memaksa @Rank disetel ulang ke nol di antara pemrosesan dua tabel (dan berharap pengoptimal tidak mengubah urutan evaluasi tabel, atau gunakan STRAIGHT_JOIN untuk mencegahnya):

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

Tetapi pengoptimalan kueri ini sangat buruk. Itu tidak dapat menggunakan indeks apa pun, itu membuat dua tabel sementara, mengurutkannya dengan cara yang sulit, dan bahkan menggunakan buffer gabungan karena tidak dapat menggunakan indeks saat menggabungkan tabel temp juga. Ini adalah contoh keluaran dari 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                  |
+----+-------------+------------+--------+---------------+------+---------+------+------+---------------------------------+

Sedangkan solusi saya menggunakan gabungan luar kiri mengoptimalkan jauh lebih baik. Ini tidak menggunakan tabel temp dan bahkan laporan "Using index"yang berarti dapat menyelesaikan gabungan hanya menggunakan indeks, tanpa menyentuh datanya.

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

Anda mungkin akan membaca orang yang membuat klaim di blog mereka bahwa "bergabung membuat SQL menjadi lambat," tapi itu tidak masuk akal. Pengoptimalan yang buruk membuat SQL menjadi lambat.

Bill Karwin
sumber
Ini mungkin terbukti cukup berguna (untuk OP juga), tapi, sayangnya, tidak menjawab satupun dari dua pertanyaan yang diajukan.
Andriy M
Terima kasih Bill, itu ide yang bagus bagaimana menghindari peringkat, tapi ... bukankah penggabungannya akan lambat? Gabungan (tanpa batasan klausa where) akan memiliki ukuran yang jauh lebih besar daripada di kueri saya. Terima kasih atas idenya! Tetapi saya juga akan tertarik dengan pertanyaan awal, yaitu apakah peringkat akan bekerja seperti ini.
TMS
Terima kasih atas jawaban yang sangat bagus, Bill. Namun, bagaimana jika saya menggunakan @Rank1dan @Rank2, satu untuk setiap subkueri? Akankah itu menyelesaikan masalah? Apakah itu lebih cepat dari solusi Anda?
TMS
Menggunakan @Rank1dan tidak @Rank2akan membuat perbedaan.
Bill Karwin
2
Terima kasih untuk solusi hebat itu. Saya berjuang lama dengan masalah itu. Untuk orang-orang yang ingin menambahkan filter untuk bidang lain misalnya "foo" Anda perlu menambahkannya ke kondisi ... AND t1.foo = t2.fooWHERE ... AND foo='bar'
penggabungan