Saya sedang melakukan pembaruan di mana saya membutuhkan kesetaraan yang tepat pada suatu tstzrange
variabel. ~ 1M baris dimodifikasi, dan kueri membutuhkan waktu ~ 13 menit. Hasil EXPLAIN ANALYZE
dapat dilihat di sini , dan hasil aktual sangat berbeda dari yang diperkirakan oleh perencana kueri. Masalahnya adalah bahwa pemindaian indeks pada t_range
mengharapkan satu baris akan dikembalikan.
Ini tampaknya terkait dengan fakta bahwa statistik pada berbagai jenis disimpan secara berbeda dari yang jenis lainnya. Melihat pg_stats
tampilan untuk kolom, n_distinct
adalah -1 dan bidang lainnya (misalnya most_common_vals
, most_common_freqs
) kosong.
Namun, harus ada statistik yang disimpan di t_range
suatu 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_range
agak 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_range
atas 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).
Hasil keseluruhan jauh lebih cepat (dengan beberapa faktor). Saya menduga ini karena penghapusan indeks, konsisten dengan jawaban Erwin .
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.
(a = b)
dua "berisi" kondisi:(a @> b AND b @> a)
? Apakah rencananya berubah?(lower(t_range),upper(t_range))
sejak Anda memeriksa kesetaraan.Jawaban:
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_timing
dan menjalankan kueri denganEXPLAIN (ANALYZE, BUFFERS)
Paket yang berbeda menampilkan baris yang akan diperbarui dalam pesanan yang berbeda. Satu dalam
trip_id
urutan, 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_id
ke 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.
sumber
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.Saya tidak yakin mengapa selektivitas predikat kesetaraan secara radikal diestimasi secara berlebihan oleh indeks GiST pada
tstzrange
kolom. Meskipun itu tetap menarik, tampaknya tidak relevan dengan kasus khusus Anda.Karena Anda
UPDATE
memodifikasi 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 AndaUPDATE
.Cukup simpan Pertanyaan sederhana Anda 1 . Solusi radikal sederhana adalah dengan menjatuhkan indeks sebelum
UPDATE
. Jika Anda memerlukannya untuk tujuan lain, buat kembali setelahUPDATE
. Ini masih akan lebih cepat daripada mempertahankan indeks selama besarUPDATE
.Untuk
UPDATE
sepertiga dari semua baris, mungkin akan membayar untuk menjatuhkan semua indeks lainnya juga - dan membuat kembali setelahUPDATE
. Satu-satunya downside: Anda memerlukan hak istimewa tambahan dan kunci eksklusif di atas meja (hanya sebentar untuk digunakanCREATE 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))
, karenatstzrange
bukan 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:
Ini bukan kasus untuk
tstzrange
, di mana inklusivitas batas atas dan bawah perlu dipertimbangkan untuk kesetaraan. Kemungkinan indeks btree harus ada pada:Dan kueri harus menggunakan ekspresi yang sama dalam
WHERE
klausa.Orang mungkin tergoda untuk hanya mengindeks seluruh nilai yang dilemparkan ke
text
:- tetapi ungkapan ini bukan(cast(t_range AS text))
IMMUTABLE
karena representasi teks daritimestamptz
nilai tergantung padatimezone
pengaturan saat ini . Anda perlu memasukkan langkah-langkah tambahan ke dalamIMMUTABLE
fungsi wrapper yang menghasilkan bentuk kanonik, dan membuat indeks fungsional ...Langkah-langkah tambahan / gagasan alternatif
Jika
shape_dist_traveled
sudah dapat memiliki nilai yang sama dengantt.shape_dist_traveled
lebih dari beberapa baris Anda yang diperbarui (dan Anda tidak bergantung pada efek samping dariUPDATE
pemicu seperti Anda ...), Anda dapat membuat kueri Anda lebih cepat dengan mengecualikan pembaruan kosong:Tentu saja, semua saran umum untuk pengoptimalan kinerja berlaku. Postgres Wiki adalah titik awal yang baik.
VACUUM FULL
akan menjadi racun bagi Anda, karena beberapa tupel mati (atau ruang yang disediakan olehFILLFACTOR
) bermanfaat untukUPDATE
kinerja.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:
sumber