Bagaimana cara menangani rencana kueri buruk yang disebabkan oleh kesetaraan tepat pada jenis rentang?

28

Saya sedang melakukan pembaruan di mana saya membutuhkan kesetaraan yang tepat pada suatu tstzrangevariabel. ~ 1M baris dimodifikasi, dan kueri membutuhkan waktu ~ 13 menit. Hasil EXPLAIN ANALYZEdapat dilihat di sini , dan hasil aktual sangat berbeda dari yang diperkirakan oleh perencana kueri. Masalahnya adalah bahwa pemindaian indeks pada t_rangemengharapkan satu baris akan dikembalikan.

Ini tampaknya terkait dengan fakta bahwa statistik pada berbagai jenis disimpan secara berbeda dari yang jenis lainnya. Melihat pg_statstampilan untuk kolom, n_distinctadalah -1 dan bidang lainnya (misalnya most_common_vals, most_common_freqs) kosong.

Namun, harus ada statistik yang disimpan di t_rangesuatu tempat. Pembaruan yang sangat mirip di mana saya menggunakan 'dalam' pada t_range alih-alih kesetaraan yang tepat membutuhkan waktu sekitar 4 menit untuk melakukan, dan menggunakan rencana kueri yang sangat berbeda (lihat di sini ). Rencana kueri kedua masuk akal bagi saya karena setiap baris di tabel temp dan sebagian besar dari tabel sejarah akan digunakan. Lebih penting lagi, perencana kueri memprediksi jumlah baris yang kira-kira benar untuk filter aktif t_range.

Distribusi t_rangeagak tidak biasa. Saya menggunakan tabel ini untuk menyimpan status historis tabel lain, dan perubahan ke tabel lainnya terjadi sekaligus dalam dump besar, jadi tidak ada banyak nilai yang berbeda t_range. Berikut adalah jumlah yang sesuai dengan masing-masing nilai unik t_range:

                              t_range                              |  count  
-------------------------------------------------------------------+---------
 ["2014-06-12 20:58:21.447478+00","2014-06-27 07:00:00+00")        |  994676
 ["2014-06-12 20:58:21.447478+00","2014-08-01 01:22:14.621887+00") |   36791
 ["2014-06-27 07:00:00+00","2014-08-01 07:00:01+00")               | 1000403
 ["2014-06-27 07:00:00+00",infinity)                               |   36791
 ["2014-08-01 07:00:01+00",infinity)                               |  999753

Hitungan untuk perbedaan di t_rangeatas selesai, jadi kardinalitasnya adalah ~ 3M (yang ~ 1M akan dipengaruhi oleh salah satu permintaan pembaruan).

Mengapa kueri 1 berkinerja jauh lebih buruk daripada kueri 2? Dalam kasus saya, kueri 2 adalah pengganti yang baik, tetapi jika kesetaraan rentang yang tepat benar-benar diperlukan, bagaimana saya bisa membuat Postgres menggunakan rencana kueri yang lebih cerdas?

Definisi tabel dengan indeks (menjatuhkan kolom yang tidak relevan):

       Column        |   Type    |                                  Modifiers                                   
---------------------+-----------+------------------------------------------------------------------------------
 history_id          | integer   | not null default nextval('gtfs_stop_times_history_history_id_seq'::regclass)
 t_range             | tstzrange | not null
 trip_id             | text      | not null
 stop_sequence       | integer   | not null
 shape_dist_traveled | real      | 
Indexes:
    "gtfs_stop_times_history_pkey" PRIMARY KEY, btree (history_id)
    "gtfs_stop_times_history_t_range" gist (t_range)
    "gtfs_stop_times_history_trip_id" btree (trip_id)

Pertanyaan 1:

UPDATE gtfs_stop_times_history sth
SET shape_dist_traveled = tt.shape_dist_traveled
FROM gtfs_stop_times_temp tt
WHERE sth.trip_id = tt.trip_id
AND sth.stop_sequence = tt.stop_sequence
AND sth.t_range = '["2014-08-01 07:00:01+00",infinity)'::tstzrange;

Pertanyaan 2:

UPDATE gtfs_stop_times_history sth
SET shape_dist_traveled = tt.shape_dist_traveled
FROM gtfs_stop_times_temp tt
WHERE sth.trip_id = tt.trip_id
AND sth.stop_sequence = tt.stop_sequence
AND '2014-08-01 07:00:01+00'::timestamptz <@ sth.t_range;

Pembaruan Q1 999753 baris dan pembaruan Q2 999753 + 36791 = 1036544 (yaitu, tabel temp sedemikian rupa sehingga setiap baris yang cocok dengan kondisi rentang waktu diperbarui).

Saya mencoba pertanyaan ini sebagai tanggapan terhadap komentar @ ypercube :

Kueri 3:

UPDATE gtfs_stop_times_history sth
SET shape_dist_traveled = tt.shape_dist_traveled
FROM gtfs_stop_times_temp tt
WHERE sth.trip_id = tt.trip_id
AND sth.stop_sequence = tt.stop_sequence
AND sth.t_range <@ '["2014-08-01 07:00:01+00",infinity)'::tstzrange
AND '["2014-08-01 07:00:01+00",infinity)'::tstzrange <@ sth.t_range;

Rencana kueri dan hasil (lihat di sini ) adalah antara antara dua kasus sebelumnya (~ 6 menit).

2016/02/05 EDIT

Tidak lagi memiliki akses ke data setelah 1,5 tahun, saya membuat tabel uji dengan struktur yang sama (tanpa indeks) dan kardinalitas serupa. jawaban jjanes mengusulkan bahwa penyebabnya mungkin adalah urutan tabel sementara yang digunakan untuk pembaruan. Saya tidak dapat menguji hipotesis secara langsung karena saya tidak memiliki akses ke track_io_timing(menggunakan Amazon RDS).

  1. Hasil keseluruhan jauh lebih cepat (dengan beberapa faktor). Saya menduga ini karena penghapusan indeks, konsisten dengan jawaban Erwin .

  2. Dalam kasus uji ini, kueri 1 dan 2 pada dasarnya mengambil jumlah waktu yang sama, karena keduanya menggunakan gabungan gabung. Artinya, saya tidak dapat memicu apa pun yang menyebabkan Postgres memilih hash join, jadi saya tidak memiliki kejelasan tentang mengapa Postgres memilih hash join yang berkinerja buruk.

abeboparebop
sumber
1
Bagaimana jika Anda dikonversi kondisi kesetaraan (a = b)dua "berisi" kondisi: (a @> b AND b @> a)? Apakah rencananya berubah?
ypercubeᵀᴹ
@ ypercube: paket berubah secara substansial, meskipun masih belum cukup optimal - lihat hasil edit saya # 2.
abeboparebop
1
Gagasan lain adalah menambahkan indeks btree reguler pada (lower(t_range),upper(t_range))sejak Anda memeriksa kesetaraan.
ypercubeᵀᴹ

Jawaban:

9

Perbedaan terbesar dalam waktu dalam rencana eksekusi Anda adalah di simpul atas, UPDATE itu sendiri. Ini menunjukkan bahwa sebagian besar waktu Anda akan ke IO selama pembaruan. Anda dapat memverifikasi ini dengan menghidupkan track_io_timingdan menjalankan kueri denganEXPLAIN (ANALYZE, BUFFERS)

Paket yang berbeda menampilkan baris yang akan diperbarui dalam pesanan yang berbeda. Satu dalam trip_idurutan, dan yang lain dalam urutan mana pun mereka secara fisik hadir dalam tabel temp.

Tabel yang sedang diperbarui tampaknya memiliki tatanan fisik yang berkorelasi dengan kolom trip_id, dan memperbarui baris dalam tatanan ini mengarah ke pola IO yang efisien dengan bacaan baca-depan / berurutan. Sementara tatanan fisik tabel temp tampaknya menyebabkan banyak pembacaan acak.

Jika Anda bisa menambahkan order by trip_idke pernyataan yang membuat tabel temp, itu bisa menyelesaikan masalah untuk Anda.

PostgreSQL tidak memperhitungkan efek pemesanan IO saat merencanakan operasi UPDATE. (Tidak seperti operasi SELECT, di mana ia memperhitungkannya). Jika PostgreSQL lebih pintar, ia akan menyadari bahwa satu paket menghasilkan urutan yang lebih efisien, atau akan menyisipkan simpul pengurutan eksplisit antara pembaruan dan simpul turunannya sehingga pembaruan tersebut akan mendapatkan baris yang diumpankan dalam urutan ctid.

Anda benar bahwa PostgreSQL melakukan pekerjaan yang buruk memperkirakan selektivitas dari kesetaraan bergabung pada rentang. Namun, ini hanya berhubungan dengan masalah mendasar Anda. Kueri yang lebih efisien pada bagian tertentu dari pembaruan Anda mungkin tanpa sengaja terjadi untuk memberi makan baris ke pembaruan yang tepat dalam urutan yang lebih baik, tetapi jika demikian, itu sebagian besar karena kurang beruntung.

jjanes
sumber
Sayangnya saya tidak dapat memodifikasi track_io_timing, dan (karena sudah satu setengah tahun!) Saya tidak lagi memiliki akses ke data asli. Namun, saya menguji teori Anda dengan membuat tabel dengan skema yang sama dan ukuran yang sama (jutaan baris), dan menjalankan dua pembaruan berbeda - satu di mana tabel pembaruan temp diurutkan seperti tabel asli, dan yang lain di mana ia diurutkan secara acak. Sayangnya, kedua pembaruan membutuhkan waktu yang kira-kira sama, yang menyiratkan bahwa pemesanan tabel pembaruan tidak memengaruhi kueri ini.
abeboparebop
7

Saya tidak yakin mengapa selektivitas predikat kesetaraan secara radikal diestimasi secara berlebihan oleh indeks GiST pada tstzrangekolom. Meskipun itu tetap menarik, tampaknya tidak relevan dengan kasus khusus Anda.

Karena Anda UPDATEmemodifikasi sepertiga (!) Dari semua baris 3M yang ada, indeks tidak akan membantu sama sekali . Sebaliknya, secara bertahap memperbarui indeks selain tabel akan menambah biaya besar untuk Anda UPDATE.

Cukup simpan Pertanyaan sederhana Anda 1 . Solusi radikal sederhana adalah dengan menjatuhkan indeks sebelum UPDATE. Jika Anda memerlukannya untuk tujuan lain, buat kembali setelah UPDATE. Ini masih akan lebih cepat daripada mempertahankan indeks selama besar UPDATE.

Untuk UPDATEsepertiga dari semua baris, mungkin akan membayar untuk menjatuhkan semua indeks lainnya juga - dan membuat kembali setelah UPDATE. Satu-satunya downside: Anda memerlukan hak istimewa tambahan dan kunci eksklusif di atas meja (hanya sebentar untuk digunakan CREATE INDEX CONCURRENTLY).

Ide @ ypercube untuk menggunakan btree bukan indeks GiST tampaknya bagus di prinsip. Tetapi tidak untuk sepertiga dari semua baris (di mana tidak ada indeks yang baik untuk memulai dengan), dan tidak pada adil (lower(t_range),upper(t_range)), karena tstzrangebukan jenis rentang diskrit.

Kebanyakan tipe rentang diskrit memiliki bentuk kanonik, yang membuat konsep "kesetaraan" lebih sederhana: batas bawah dan atas dari nilai dalam bentuk kanonik menentukannya. Dokumentasi:

Jenis rentang diskrit harus memiliki fungsi kanonikisasi yang menyadari ukuran langkah yang diinginkan untuk jenis elemen. Fungsi kanonikisasi dibebankan dengan mengkonversi nilai ekivalen dari tipe rentang untuk memiliki representasi yang identik, khususnya secara konsisten inklusif atau batas eksklusif. Jika fungsi kanonikisasi tidak ditentukan, maka rentang dengan pemformatan berbeda akan selalu diperlakukan sebagai tidak setara, meskipun mereka mungkin mewakili set nilai yang sama dalam kenyataan.

Jenis rentang bawaan int4range,, int8rangedan daterangesemua menggunakan bentuk kanonik yang mencakup batas bawah dan mengecualikan batas atas; itu adalah [),. Namun, tipe rentang yang ditentukan pengguna dapat menggunakan konvensi lain.

Ini bukan kasus untuk tstzrange, di mana inklusivitas batas atas dan bawah perlu dipertimbangkan untuk kesetaraan. Kemungkinan indeks btree harus ada pada:

(lower(t_range), upper(t_range), lower_inc(t_range), upper_inc(t_range))

Dan kueri harus menggunakan ekspresi yang sama dalam WHEREklausa.

Orang mungkin tergoda untuk hanya mengindeks seluruh nilai yang dilemparkan ke text: (cast(t_range AS text))- tetapi ungkapan ini bukan IMMUTABLEkarena representasi teks dari timestamptznilai tergantung pada timezonepengaturan saat ini . Anda perlu memasukkan langkah-langkah tambahan ke dalam IMMUTABLEfungsi wrapper yang menghasilkan bentuk kanonik, dan membuat indeks fungsional ...

Langkah-langkah tambahan / gagasan alternatif

Jika shape_dist_traveledsudah dapat memiliki nilai yang sama dengan tt.shape_dist_traveledlebih dari beberapa baris Anda yang diperbarui (dan Anda tidak bergantung pada efek samping dari UPDATEpemicu seperti Anda ...), Anda dapat membuat kueri Anda lebih cepat dengan mengecualikan pembaruan kosong:

WHERE ...
AND   shape_dist_traveled IS DISTINCT FROM tt.shape_dist_traveled;

Tentu saja, semua saran umum untuk pengoptimalan kinerja berlaku. Postgres Wiki adalah titik awal yang baik.

VACUUM FULLakan menjadi racun bagi Anda, karena beberapa tupel mati (atau ruang yang disediakan oleh FILLFACTOR) bermanfaat untuk UPDATEkinerja.

Dengan banyak baris yang diperbarui, dan jika Anda mampu membelinya (tanpa akses bersamaan atau dependensi lainnya), mungkin bahkan lebih cepat untuk menulis tabel yang sama sekali baru daripada memperbarui di tempat. Instruksi dalam jawaban terkait ini:

Erwin Brandstetter
sumber