Permintaan lambat pada tabel besar dengan GROUP BY dan ORDER BY

14

Saya punya meja dengan 7,2 juta tupel yang terlihat seperti ini:

                               table public.methods
 column |          type         |                      attributes
--------+-----------------------+----------------------------------------------------
 id     | integer               | not null DEFAULT nextval('methodkey'::regclass)
 hash   | character varying(32) | not null
 string | character varying     | not null
 method | character varying     | not null
 file   | character varying     | not null
 type   | character varying     | not null
Indexes:
    "methods_pkey" PRIMARY KEY, btree (id)
    "methodhash" btree (hash)

Sekarang saya ingin memilih beberapa nilai tetapi permintaannya sangat lambat:

db=# explain 
    select hash, string, count(method) 
    from methods 
    where hash not in 
          (select hash from nostring) 
    group by hash, string 
    order by count(method) desc;
                                            QUERY PLAN
----------------------------------------------------------------------------------------
 Sort  (cost=160245190041.10..160245190962.07 rows=368391 width=182)
   Sort Key: (count(methods.method))
   ->  GroupAggregate  (cost=160245017241.77..160245057764.73 rows=368391 width=182)
       ->  Sort  (cost=160245017241.77..160245026451.53 rows=3683905 width=182)
             Sort Key: methods.hash, methods.string
             ->  Seq Scan on methods  (cost=0.00..160243305942.27 rows=3683905 width=182)
                   Filter: (NOT (SubPlan 1))
                   SubPlan 1
                   ->  Materialize  (cost=0.00..41071.54 rows=970636 width=33)
                     ->  Seq Scan on nostring  (cost=0.00..28634.36 rows=970636 width=33)

The hashkolom adalah hash md5 dari stringdan memiliki indeks. Jadi saya pikir masalah saya adalah bahwa seluruh tabel diurutkan berdasarkan id dan bukan oleh hash, jadi butuh beberapa saat untuk mengurutkannya terlebih dahulu lalu mengelompokkannya?

Tabel nostringhanya berisi daftar hash yang tidak ingin saya miliki. Tapi saya butuh kedua tabel untuk memiliki semua nilai. Jadi ini bukan opsi untuk menghapus ini.

info tambahan: tidak ada kolom yang bisa nol (perbaiki dalam definisi tabel) dan saya menggunakan postgresql 9.2.

reox
sumber
1
Selalu sediakan versi PostgreSQL yang Anda gunakan. Berapa persentase NULLnilai dalam kolom method? Apakah ada duplikat aktif string?
Erwin Brandstetter

Jawaban:

18

Jawaban LEFT JOINin @ dezso seharusnya bagus. Akan tetapi, suatu indeks tidak akan berguna (per se), karena kueri harus tetap membaca seluruh tabel - kecuali hanya scan indeks-saja di Postgres 9.2+ dan kondisi yang menguntungkan, lihat di bawah.

SELECT m.hash, m.string, count(m.method) AS method_ct
FROM   methods m
LEFT   JOIN nostring n USING (hash)
WHERE  n.hash IS NULL
GROUP  BY m.hash, m.string 
ORDER  BY count(m.method) DESC;

Jalankan EXPLAIN ANALYZEpada kueri. Beberapa kali tidak termasuk efek pencairan dan kebisingan. Bandingkan hasil terbaik.

Buat indeks multi-kolom yang cocok dengan kueri Anda:

CREATE INDEX methods_cluster_idx ON methods (hash, string, method);

Tunggu? Setelah saya katakan indeks tidak akan membantu? Kita membutuhkannya CLUSTERdi meja:

CLUSTER methods USING methods_cluster_idx;
ANALYZE methods;

Jalankan kembali EXPLAIN ANALYZE. Lebih cepat? Harus.

CLUSTERadalah operasi satu kali untuk menulis ulang seluruh tabel dalam urutan indeks yang digunakan. Ini juga efektif a VACUUM FULL. Jika Anda ingin memastikan, Anda akan menjalankan pre-test VACUUM FULLsendirian untuk melihat apa yang dapat dikaitkan dengan itu.

Jika tabel Anda melihat banyak operasi penulisan, efeknya akan menurun seiring waktu. Jadwalkan CLUSTERdi luar jam untuk mengembalikan efek. Penyesuaian yang baik tergantung dari kasus penggunaan yang tepat. Manual tentang CLUSTER.

CLUSTERadalah alat yang agak kasar, membutuhkan kunci eksklusif di atas meja. Jika Anda tidak mampu membelinya, pertimbangkan pg_repackyang dapat melakukan hal yang sama tanpa kunci eksklusif. Lebih banyak di jawaban nanti:


Jika persentase NULLnilai dalam kolom methodtinggi (lebih dari ~ 20 persen, tergantung pada ukuran baris aktual), sebagian indeks akan membantu:

CREATE INDEX methods_foo_idx ON methods (hash, string)
WHERE method IS NOT NULL;

(Pembaruan Anda nanti menunjukkan kolom Anda menjadi NOT NULL, jadi tidak berlaku.)

Jika Anda menjalankan PostgreSQL 9.2 atau yang lebih baru (seperti yang dikomentari @deszo ) indeks yang disajikan mungkin berguna tanpa CLUSTERjika perencana dapat menggunakan scan hanya indeks . Hanya berlaku di bawah kondisi yang menguntungkan: Tidak ada operasi penulisan yang akan memengaruhi peta visibilitas karena kolom terakhir VACUUMdan semua dalam kueri harus dicakup oleh indeks. Pada dasarnya tabel read-only dapat menggunakan ini kapan saja, sementara tabel yang banyak ditulis terbatas. Lebih detail di Postgres Wiki.

Indeks parsial yang disebutkan di atas bisa lebih berguna dalam kasus itu.

Jika , di sisi lain, tidak ada NULL nilai dalam kolom method, Anda harus
1.) mendefinisikannya NOT NULLdan
2.) menggunakannya count(*)sebagai gantinya count(method), itu sedikit lebih cepat dan melakukan hal yang sama tanpa adanya NULLnilai.

Jika Anda harus sering memanggil kueri ini dan tabel ini hanya baca, buat a MATERIALIZED VIEW.


Titik halus eksotis: Tabel Anda dinamai nostring, namun tampaknya mengandung hash. Dengan mengecualikan hash alih-alih string, ada kemungkinan Anda mengecualikan lebih banyak string daripada yang dimaksudkan. Sangat tidak mungkin, tetapi mungkin.

Erwin Brandstetter
sumber
dengan cluster yang jauh lebih cepat. masih perlu sekitar 5 menit untuk permintaan tapi itu jauh lebih baik daripada menjalankannya sepanjang malam: D
reox
@reox: Karena Anda menjalankan v9.2: Apakah Anda menguji dengan indeks saja, sebelum pengelompokan? Akan menarik jika melihat perbedaan. (Anda tidak dapat mereproduksi perbedaan setelah pengelompokan.) Juga (dan ini akan murah), apakah EXPLAIN menunjukkan pemindaian indeks atau pemindaian tabel penuh sekarang?
Erwin Brandstetter
5

Selamat datang di DBA.SE!

Anda dapat mencoba untuk mengurutkan kembali kueri Anda seperti ini:

SELECT m.hash, string, count(method) 
FROM 
    methods m
    LEFT JOIN nostring n ON m.hash = n.hash
WHERE n.hash IS NULL
GROUP BY hash, string 
ORDER BY count(method) DESC;

atau kemungkinan lain:

SELECT m.hash, string, count(method) 
FROM 
    methods m
WHERE NOT EXISTS (SELECT hash FROM nostring WHERE hash = m.hash)
GROUP BY hash, string 
ORDER BY count(method) DESC;

NOT IN adalah wastafel umum untuk kinerja karena sulit untuk menggunakan indeks dengannya.

Ini dapat lebih ditingkatkan dengan indeks. Indeks pada nostring.hashterlihat bermanfaat. Tetapi pertama-tama: apa yang Anda dapatkan sekarang? (Akan lebih baik untuk melihat output EXPLAIN ANALYZEkarena biayanya sendiri tidak memberi tahu waktu operasi berlangsung.)

dezso
sumber
sebuah indeks dibuat pada nostring.hash allready, tetapi saya pikir postgres tidak menggunakannya karena terlalu banyak tuple ... ketika saya explcit menonaktifkan urutan pemindaian, ia menggunakan indeks. jika saya menggunakan kiri bergabung saya mendapatkan biaya 32 juta, jadi jalannya lebih baik ... tapi saya mencoba untuk mengoptimalkannya lagi ...
reox
3
Biaya hanya untuk perencana untuk dapat menyusun rencana yang cukup baik. Waktu aktual biasanya berkorelasi dengannya, tetapi tidak harus. Jadi, jika Anda ingin memastikan, gunakan EXPLAIN ANALYZE.
dezso
1

Karena hash adalah md5, Anda mungkin dapat mencoba mengubahnya dalam angka: Anda dapat menyimpannya sebagai angka, atau hanya membuat indeks fungsional yang menghitung angka itu dalam fungsi yang tidak dapat diubah.

Orang lain sudah membuat fungsi pl / pgsql yang mengubah (bagian dari) nilai md5 dari teks ke string. Lihat /programming/9809381/hashing-a-string-to-a-numeric-value-in-postgressql untuk contoh

Saya percaya bahwa Anda benar-benar menghabiskan banyak waktu dalam perbandingan string saat memindai indeks. Jika Anda berhasil menyimpan nilai itu sebagai angka, maka itu harus benar-benar lebih cepat.

eppesuig
sumber
1
Saya ragu bahwa konversi ini akan mempercepat. Semua pertanyaan di sini menggunakan persamaan untuk perbandingan. Menghitung representasi numerik dan kemudian memeriksa kesetaraan tidak menjanjikan keuntungan besar bagi saya.
dezso
2
Saya pikir saya akan menyimpan md5 sebagai bytea daripada angka untuk efisiensi ruang: sqlfiddle.com/#!12/d41d8/252
Jack mengatakan coba topanswers.xyz
Juga, selamat datang di dba.se!
Jack bilang coba topanswers.xyz
@JackDouglas: Komentar menarik! 16 byte per md5 bukan 32 cukup sedikit untuk tabel besar.
Erwin Brandstetter
0

Saya sering mengalami masalah ini, dan menemukan trik 2 bagian sederhana.

  1. Buat indeks substring pada nilai hash: (7 biasanya panjang yang baik)

    create index methods_idx_hash_substring ON methods(substring(hash,1,7))

  2. Suruh pencarian Anda / gabung menyertakan kecocokan substring, sehingga perencana kueri disarankan untuk menggunakan indeks:

    tua: WHERE hash = :kwarg

    baru: WHERE (hash = :kwarg) AND (substring(hash,1,7) = substring(:kwarg,1,7))

Anda juga harus memiliki indeks mentah hash.

hasilnya (biasanya) adalah perencana yang akan berkonsultasi dengan indeks substring terlebih dahulu dan menyingkirkan sebagian besar baris. kemudian cocok dengan hash 32 karakter penuh dengan indeks (atau tabel) yang sesuai. pendekatan ini telah menurunkan 800ms pertanyaan menjadi 4 untuk saya.

Jonathan Vanasco
sumber