Indeks pada kunci utama tidak digunakan dalam gabung sederhana

16

Saya memiliki definisi tabel dan indeks berikut:

CREATE TABLE munkalap (
    munkalap_id serial PRIMARY KEY,
    ...
);

CREATE TABLE munkalap_lepes (
    munkalap_lepes_id serial PRIMARY KEY,
    munkalap_id integer REFERENCES munkalap (munkalap_id),
    ...
);

CREATE INDEX idx_munkalap_lepes_munkalap_id ON munkalap_lepes (munkalap_id);

Mengapa tidak ada indeks pada munkalap_id yang digunakan dalam kueri berikut?

EXPLAIN ANALYZE SELECT ml.* FROM munkalap m JOIN munkalap_lepes ml USING (munkalap_id);

QUERY PLAN
Hash Join  (cost=119.17..2050.88 rows=38046 width=214) (actual time=0.824..18.011 rows=38046 loops=1)
  Hash Cond: (ml.munkalap_id = m.munkalap_id)
  ->  Seq Scan on munkalap_lepes ml  (cost=0.00..1313.46 rows=38046 width=214) (actual time=0.005..4.574 rows=38046 loops=1)
  ->  Hash  (cost=78.52..78.52 rows=3252 width=4) (actual time=0.810..0.810 rows=3253 loops=1)
        Buckets: 1024  Batches: 1  Memory Usage: 115kB
        ->  Seq Scan on munkalap m  (cost=0.00..78.52 rows=3252 width=4) (actual time=0.003..0.398 rows=3253 loops=1)
Total runtime: 19.786 ms

Itu sama bahkan jika saya menambahkan filter:

EXPLAIN ANALYZE SELECT ml.* FROM munkalap m JOIN munkalap_lepes ml USING (munkalap_id) WHERE NOT lezarva;

QUERY PLAN
Hash Join  (cost=79.60..1545.79 rows=1006 width=214) (actual time=0.616..10.824 rows=964 loops=1)
  Hash Cond: (ml.munkalap_id = m.munkalap_id)
  ->  Seq Scan on munkalap_lepes ml  (cost=0.00..1313.46 rows=38046 width=214) (actual time=0.007..5.061 rows=38046 loops=1)
  ->  Hash  (cost=78.52..78.52 rows=86 width=4) (actual time=0.587..0.587 rows=87 loops=1)
        Buckets: 1024  Batches: 1  Memory Usage: 4kB
        ->  Seq Scan on munkalap m  (cost=0.00..78.52 rows=86 width=4) (actual time=0.014..0.560 rows=87 loops=1)
              Filter: (NOT lezarva)
Total runtime: 10.911 ms
dezso
sumber

Jawaban:

21

Banyak orang telah mendengar panduan bahwa "pemindaian berurutan itu buruk" dan berusaha untuk menghilangkannya dari rencana mereka, tetapi itu tidak sesederhana itu. Jika kueri akan mencakup setiap baris dalam tabel, pemindaian berurutan adalah cara tercepat untuk mendapatkan baris-baris itu. Inilah sebabnya mengapa permintaan bergabung asli Anda menggunakan pemindaian seq, karena semua baris di kedua tabel diperlukan.

Saat merencanakan kueri, perencana Postgres memperkirakan biaya berbagai operasi (perhitungan, sekuensial, dan IO acak) di bawah skema yang berbeda, dan memilih rencana yang diperkirakan memiliki biaya terendah. Ketika melakukan IO dari penyimpanan berputar (disk), IO acak biasanya jauh lebih lambat dari IO berurutan, konfigurasi pg default untuk random_page_cost dan seq_page_cost memperkirakan perbedaan biaya 4: 1.

Pertimbangan ini mulai berlaku saat mempertimbangkan metode join atau filter yang menggunakan indeks vs yang secara berurutan memindai tabel. Saat menggunakan indeks, paket dapat menemukan baris dengan cepat melalui indeks, kemudian harus menghitung pembacaan blok acak untuk menyelesaikan data baris. Dalam kasus kueri kedua Anda yang menambahkan predikat pemfilteran WHERE NOT lezarva, Anda bisa melihat bagaimana ini memengaruhi estimasi perencanaan dalam hasil EXPLAIN ANALYZE. Perencana memperkirakan 1006 baris yang dihasilkan dari gabungan (yang sangat cocok dengan hasil aktual set 964). Mengingat bahwa tabel yang lebih besar munkalap_lepes berisi sekitar 38K baris, perencana melihat bahwa gabungan tersebut harus mengakses sekitar 1006/38046 atau 1/38 dari baris dalam tabel. Ia juga tahu lebar baris rata-rata adalah 214 byte dan satu blok adalah 8 ribu, jadi ada sekitar 38 baris / blok.

Dengan statistik ini, perencana mempertimbangkan kemungkinan bahwa join harus membaca semua atau sebagian besar blok data tabel. Karena pencarian indeks juga tidak gratis, dan perhitungan untuk memindai blok mengevaluasi kondisi filter sangat murah dibandingkan dengan IO, perencana telah memilih untuk secara berurutan memindai tabel dan menghindari overhead indeks dan pembacaan acak saat menghitung pemindaian seq akan lebih cepat.

Di dunia nyata, data sering tersedia dalam memori melalui cache halaman OS, sehingga tidak setiap blok yang dibaca membutuhkan IO. Mungkin sangat sulit untuk memprediksi seberapa efektif cache untuk query yang diberikan, tetapi perencana Pg memang menggunakan beberapa heuristik sederhana. Nilai konfigurasi effective_cache_size menginformasikan perkiraan perencana tentang kemungkinan menimbulkan biaya IO aktual. Nilai yang lebih besar akan menyebabkannya memperkirakan biaya yang lebih rendah untuk IO acak dan dengan demikian dapat membiaskannya ke arah metode yang digerakkan indeks melalui pemindaian berurutan.

dbenhur
sumber
Terima kasih, sejauh ini deskripsi terbaik (dan paling ringkas) yang pernah saya baca. Mengklarifikasi beberapa poin utama.
dezso
1
Penjelasan yang bagus. Namun perhitungan halaman baris / data sedikit tidak aktif. Anda harus memperhitungkan header halaman (24 byte) + 4 byte untuk setiap penunjuk item per baris + header baris HeapTupleHeader(23 byte per baris) + NULL bitmask + alignment sesuai dengan MAXALIGN. Akhirnya, jumlah padding yang tidak diketahui karena penyelarasan data tergantung pada tipe data kolom dan urutannya. Secara keseluruhan tidak ada lebih dari 33 baris pada halaman 8 kb dalam hal ini. (Tidak memperhitungkan TOAST.)
Erwin Brandstetter
1
@ ErwinBrandstetter Terima kasih telah mengisi perhitungan ukuran baris yang lebih rumit. Saya selalu mengasumsikan output estimasi lebar baris dengan menjelaskan akan mencakup pertimbangan per-baris seperti header dan NULL-bitmask, tetapi bukan overhead level halaman.
dbenhur
1
@dbenhur: Anda dapat menjalankan quick EXPLAIN ANALYZE SELECT foo from bardengan tabel dummy dasar untuk memverifikasi. Juga, ruang disk sebenarnya tergantung pada penyelarasan data, yang akan sulit untuk diperhitungkan ketika hanya beberapa baris yang diambil. Lebar baris dalam EXPLAINmewakili persyaratan ruang dasar untuk set kolom yang diambil.
Erwin Brandstetter
5

Anda mengambil semua baris dari kedua tabel, sehingga tidak ada manfaat nyata dengan menggunakan pemindaian indeks. Pemindaian indeks hanya masuk akal jika Anda memilih hanya beberapa baris dari sebuah tabel (biasanya kurang dari 10% -15%)

seekor kuda tanpa nama
sumber
Yap, Anda benar :) Saya mencoba mengklarifikasi situasi dengan kasus yang lebih spesifik, lihat permintaan terakhir.
dezso
@dezso: Hal yang sama. Jika Anda memiliki indeks aktif (lezarva, munkalap_id)dan cukup selektif, maka dapat digunakan. Itu NOTmembuat itu kurang mungkin.
ypercubeᵀᴹ
Saya menambahkan indeks parsial berdasarkan saran Anda dan itu digunakan, jadi setengah dari masalah diselesaikan. Tapi saya tidak akan berharap indeks pada kunci asing menjadi tidak berguna mengingat bahwa saya ingin BERGABUNG dengan hanya 87 nilai dibandingkan dengan 3252. asli
dezso
1
@dezso Baris rata-rata lebar 214 byte sehingga Anda akan memiliki sedikit di bawah 40 baris per blok data 8K. Selektivitas indeks juga sekitar 1/40 (1006/38046). Jadi, angka Pg yang membaca semua blok secara berurutan lebih murah daripada kemungkinan membaca tentang jumlah blok yang sama secara acak saat menggunakan indeks. Perkiraan tradeoff ini dapat dipengaruhi oleh nilai konfigurasi efektif_cache_size dan random_page_cost.
dbenhur
@dbenhur: dapatkah Anda membuat komentar sebagai jawaban yang tepat?
dezso