Permintaan postgreSQL sangat lambat ketika ditambahkan subquery

10

Saya memiliki kueri yang relatif sederhana di atas meja dengan baris 1.5M:

SELECT mtid FROM publication
WHERE mtid IN (9762715) OR last_modifier=21321
LIMIT 5000;

EXPLAIN ANALYZE keluaran:

Limit  (cost=8.84..12.86 rows=1 width=8) (actual time=0.985..0.986 rows=1 loops=1)
  ->  Bitmap Heap Scan on publication  (cost=8.84..12.86 rows=1 width=8) (actual time=0.984..0.985 rows=1 loops=1)
        Recheck Cond: ((mtid = 9762715) OR (last_modifier = 21321))
        ->  BitmapOr  (cost=8.84..8.84 rows=1 width=0) (actual time=0.971..0.971 rows=0 loops=1)
              ->  Bitmap Index Scan on publication_pkey  (cost=0.00..4.42 rows=1 width=0) (actual time=0.295..0.295 rows=1 loops=1)
                    Index Cond: (mtid = 9762715)
              ->  Bitmap Index Scan on publication_last_modifier_btree  (cost=0.00..4.42 rows=1 width=0) (actual time=0.674..0.674 rows=0 loops=1)
                    Index Cond: (last_modifier = 21321)
Total runtime: 1.027 ms

Sejauh ini bagus, cepat dan menggunakan indeks yang tersedia.
Sekarang, jika saya memodifikasi sedikit kueri, hasilnya adalah:

SELECT mtid FROM publication
WHERE mtid IN (SELECT 9762715) OR last_modifier=21321
LIMIT 5000;

The EXPLAIN ANALYZEoutput:

Limit  (cost=0.01..2347.74 rows=5000 width=8) (actual time=2735.891..2841.398 rows=1 loops=1)
  ->  Seq Scan on publication  (cost=0.01..349652.84 rows=744661 width=8) (actual time=2735.888..2841.393 rows=1 loops=1)
        Filter: ((hashed SubPlan 1) OR (last_modifier = 21321))
        SubPlan 1
          ->  Result  (cost=0.00..0.01 rows=1 width=0) (actual time=0.001..0.001 rows=1 loops=1)
Total runtime: 2841.442 ms

Tidak begitu cepat, dan menggunakan pemindaian seq ...

Tentu saja, kueri asli yang dijalankan oleh aplikasi sedikit lebih kompleks, dan bahkan lebih lambat, dan tentu saja orisinal yang dihasilkan hibernate tidak (SELECT 9762715), tetapi kelambatan ada bahkan untuk itu (SELECT 9762715)! Permintaan dihasilkan oleh hibernate, sehingga cukup sulit untuk mengubahnya, dan beberapa fitur tidak tersedia (mis. UNIONTidak tersedia, yang akan cepat).

Pertanyaan-pertanyaan

  1. Mengapa indeks tidak dapat digunakan dalam kasus kedua? Bagaimana mereka bisa digunakan?
  2. Bisakah saya meningkatkan kinerja permintaan dengan cara lain?

Pikiran tambahan

Tampaknya kita bisa menggunakan kasus pertama dengan melakukan SELECT secara manual, dan kemudian memasukkan daftar yang dihasilkan ke dalam kueri. Bahkan dengan 5000 angka dalam daftar IN () itu empat kali lebih cepat daripada solusi kedua. Namun, sepertinya SALAH (juga, bisa 100 kali lebih cepat :)). Ini benar-benar tidak dapat dimengerti mengapa perencana kueri menggunakan metode yang sama sekali berbeda untuk dua pertanyaan ini, jadi saya ingin mencari solusi yang lebih baik untuk masalah ini.

P.Péter
sumber
Bisakah Anda entah bagaimana menulis ulang kode Anda sehingga hibernate menghasilkan JOINbukan IN ()? Juga, publicationsudah dianalisis baru-baru ini?
dezso
Ya, saya melakukan VACUUM ANALYZE dan VACUUM FULL. Tidak ada perubahan dalam kinerja. Mengenai yang kedua, AFAIR kami mencobanya dan itu tidak mempengaruhi kinerja kueri secara signifikan.
P.Péter
1
Jika Hibernate gagal menghasilkan kueri yang tepat, mengapa Anda tidak menggunakan SQL mentah saja? Itu seperti memaksa Google menerjemahkan sementara Anda sudah lebih tahu cara mengekspresikannya dalam bahasa Inggris. Adapun pertanyaan Anda: itu benar-benar tergantung pada permintaan aktual yang tersembunyi di belakang (SELECT 9762715).
Erwin Brandstetter
Seperti yang saya sebutkan di bawah ini, lambat bahkan jika permintaan dalam adalah (SELECT 9762715) . Untuk pertanyaan hibernasi: itu bisa dilakukan, tetapi membutuhkan penulisan ulang kode serius, karena kami memiliki kueri kriteria hibernasi yang ditentukan pengguna yang diterjemahkan saat itu juga. Jadi pada dasarnya kami akan memodifikasi hibernate yang merupakan upaya besar dengan banyak efek samping yang mungkin.
P.Péter

Jawaban:

6

Inti dari masalah menjadi jelas di sini:

Pemindaian Seq pada publikasi (biaya = 0,01..349652.84 baris = 744661 lebar = 8) (waktu aktual = 2735.888..2841.393 baris = 1 loop = 1)

Postgres memperkirakan untuk mengembalikan 744661 baris, pada kenyataannya, ternyata menjadi satu baris. Jika Postgres tidak tahu apa yang diharapkan dari kueri, Postgres tidak dapat merencanakan dengan lebih baik. Kita perlu melihat permintaan aktual yang tersembunyi di belakang (SELECT 9762715)- dan mungkin juga tahu definisi tabel, batasan, kardinalitas, dan distribusi data. Jelas, Postgres tidak dapat memprediksi bagaimana beberapa baris akan dikembalikan oleh itu. Mungkin ada cara untuk menulis ulang query, tergantung pada apa yang .

Jika Anda tahu bahwa subquery tidak pernah dapat mengembalikan lebih dari nbaris, Anda bisa memberi tahu Postgres dengan menggunakan:

SELECT mtid
FROM   publication
WHERE  mtid IN (SELECT ... LIMIT n) --  OR last_modifier=21321
LIMIT  5000;

Jika n cukup kecil, Postgres akan beralih ke scan indeks (bitmap). Namun , itu hanya berfungsi untuk kasus sederhana. Berhenti berfungsi saat menambahkan ORkondisi: perencana kueri saat ini tidak dapat mengatasinya.

Saya jarang menggunakannya IN (SELECT ...)untuk memulai. Biasanya ada cara yang lebih baik untuk mengimplementasikan hal yang sama, seringkali dengan EXISTSsemi-join. Terkadang dengan ( LEFT) JOIN( LATERAL) ...

Solusi yang jelas akan digunakan UNION, tetapi Anda mengesampingkan itu. Saya tidak bisa mengatakan lebih banyak tanpa mengetahui subquery yang sebenarnya dan detail relevan lainnya.

Erwin Brandstetter
sumber
2
Tidak ada permintaan yang tersembunyi di belakang (SELECT 9762715) ! Jika saya menjalankan kueri persis yang Anda lihat di atas. Tentu saja, permintaan hibernate asli sedikit lebih rumit, tetapi saya (pikir saya) berhasil menentukan di mana perencana kueri tersesat, jadi saya menyajikan bagian dari permintaan itu. Namun, penjelasan dan kueri di atas adalah kata demi kata ctrl-cv.
P.Péter
Mengenai bagian kedua, batas dalam tidak berfungsi: EXPLAIN ANALYZE SELECT mtid FROM publication WHERE mtid IN (SELECT 9762715 LIMIT 1) OR last_modifier=21321 LIMIT 5000;juga melakukan pemindaian sekuensial dan juga berjalan sekitar 3 detik ...
P.Péter
@ P.Péter: Ini berfungsi untuk saya dalam tes lokal saya dengan subquery aktual pada Postgres 9.4. Jika yang Anda tunjukkan adalah permintaan sebenarnya, maka Anda sudah memiliki solusi: Gunakan kueri pertama dalam pertanyaan Anda dengan konstanta alih-alih subquery.
Erwin Brandstetter
Nah, saya juga mencoba subquery di atas meja pengujian baru: CREATE TABLE test (mtid bigint NOT NULL, last_modifier bigint, CONSTRAINT test_property_pkey PRIMARY KEY (mtid)); CREATE INDEX test_last_modifier_btree ON test USING btree (last_modifier); INSERT INTO test (mtid, last_modifier) SELECT mtid, last_modifier FROM publication;. Dan efeknya masih ada untuk pertanyaan yang sama test: setiap subquery menghasilkan pemindaian seq ... Saya mencoba keduanya 9.1 dan 9.4. Efeknya sama.
P.Péter
1
@ P.Péter: Saya menjalankan tes lagi dan menyadari bahwa saya telah menguji tanpa ORsyarat. Trik dengan LIMIThanya berfungsi untuk case yang lebih sederhana.
Erwin Brandstetter
6

Rekan saya telah menemukan cara untuk mengubah kueri sehingga perlu menulis ulang sederhana dan melakukan apa yang perlu dilakukan, yaitu melakukan subselect dalam satu langkah, dan kemudian melakukan operasi lebih lanjut pada hasilnya:

SELECT mtid FROM publication 
WHERE 
  mtid = ANY( (SELECT ARRAY(SELECT 9762715))::bigint[] )
  OR last_modifier=21321
LIMIT 5000;

Analisis menjelaskan sekarang adalah:

 Limit  (cost=92.58..9442.38 rows=2478 width=8) (actual time=0.071..0.074 rows=1 loops=1)
   InitPlan 2 (returns $1)
     ->  Result  (cost=0.01..0.02 rows=1 width=0) (actual time=0.010..0.011 rows=1 loops=1)
           InitPlan 1 (returns $0)
             ->  Result  (cost=0.00..0.01 rows=1 width=0) (actual time=0.001..0.002 rows=1 loops=1)
   ->  Bitmap Heap Scan on publication  (cost=92.56..9442.36 rows=2478 width=8) (actual time=0.069..0.070 rows=1 loops=1)
         Recheck Cond: ((mtid = ANY (($1)::bigint[])) OR (last_modifier = 21321))
         Heap Blocks: exact=1
         ->  BitmapOr  (cost=92.56..92.56 rows=2478 width=0) (actual time=0.060..0.060 rows=0 loops=1)
               ->  Bitmap Index Scan on publication_pkey  (cost=0.00..44.38 rows=10 width=0) (actual time=0.046..0.046 rows=1 loops=1)
                     Index Cond: (mtid = ANY (($1)::bigint[]))
               ->  Bitmap Index Scan on publication_last_modifier_btree  (cost=0.00..46.94 rows=2468 width=0) (actual time=0.011..0.011 rows=0 loops=1)
                     Index Cond: (last_modifier = 21321)
 Planning time: 0.704 ms
 Execution time: 0.153 ms

Tampaknya kita dapat membuat parser sederhana yang menemukan dan menulis ulang semua subselect dengan cara ini, dan menambahkannya ke pengait hibernasi untuk memanipulasi kueri asli.

P.Péter
sumber
Terdengar menyenangkan. Bukankah lebih mudah untuk menghapus semua saja SELECT, seperti yang Anda miliki di pertanyaan pertama dalam pertanyaan?
dezso
Tentu saja, saya bisa melakukan pendekatan dua langkah: lakukan secara SELECTterpisah, dan kemudian lakukan seleksi luar dengan daftar statis setelah IN. Namun, itu secara signifikan lebih lambat (5-10 kali jika subquery memiliki lebih dari beberapa hasil), karena Anda memiliki round-trip jaringan tambahan plus Anda memiliki postgres memformat banyak hasil dan kemudian java mem-parsing hasil tersebut (dan kemudian melakukan sama lagi mundur). Solusi di atas melakukan hal yang sama secara semantik, sambil meninggalkan proses di dalam postgres. Secara keseluruhan, saat ini ini tampaknya menjadi cara tercepat dengan modifikasi terkecil dalam kasus kami.
P.Péter
Ah, begitu. Yang saya tidak tahu adalah bahwa Anda bisa mendapatkan banyak ID sekaligus.
dezso
1

Jawab pertanyaan kedua: Ya, Anda bisa menambahkan ORDER BY ke subquery Anda, yang akan berdampak positif. Tapi itu mirip dengan solusi "EXISTS (subquery)" dalam kinerja. Ada perbedaan yang signifikan bahkan dengan subquery menghasilkan dua baris.

SELECT mtid FROM publication
WHERE mtid IN (SELECT #column# ORDER BY #column#) OR last_modifier=21321
LIMIT 5000;
iki
sumber