Mengapa?
Alasan apakah ini:
Permintaan cepat:
-> Hash Left Join (cost=1378.60..2467.48 rows=15 width=79) (actual time=41.759..85.037 rows=1129 loops=1) ... Filter: (unaccent(((((COALESCE(p.abrev, ''::character varying))::text || ' ('::text) || (COALESCE(p.prenome, ''::character varying))::text) || ')'::text)) ~~* (...)
Permintaan lambat:
-> Hash Left Join (cost=1378.60..2467.48 rows=1 width=79) (actual time=35.084..80.209 rows=1129 loops=1) ... Filter: (unaccent(((((COALESCE(p.abrev, ''::character varying))::text || ' ('::text) || (COALESCE(p.prenome, ''::character varying))::text) || ')'::text)) ~~* unacc (...)
Memperluas pola pencarian dengan karakter lain menyebabkan Postgres mengasumsikan lebih sedikit hit. (Biasanya, ini adalah perkiraan yang masuk akal.) Postgres jelas tidak memiliki statistik yang cukup tepat (tidak ada, sebenarnya, terus membaca) untuk mengharapkan jumlah klik yang sama dengan yang Anda dapatkan.
Hal ini menyebabkan peralihan ke paket kueri yang berbeda, yang bahkan kurang optimal untuk aktual jumlah klik rows=1129
.
Solusi
Dengan asumsi Postgres 9.5 saat ini karena belum dideklarasikan.
Salah satu cara untuk memperbaiki situasi adalah dengan membuat indeks ekspresi pada ekspresi dalam predikat. Ini membuat Postgres mengumpulkan statistik untuk ekspresi aktual, yang dapat membantu kueri bahkan jika indeks itu sendiri tidak digunakan untuk kueri . Tanpa indeks, tidak ada statistik untuk ekspresi sama sekali. Dan jika dilakukan dengan benar, indeks dapat digunakan untuk kueri, itu bahkan jauh lebih baik. Tapi ada banyak masalah dengan ekspresi Anda saat ini:
unaccent(TEXT(coalesce(p.abrev,'')||' ('||coalesce(p.prenome,'')||')')) ilike unaccent('%vicen%')
Pertimbangkan kueri yang diperbarui ini, berdasarkan beberapa asumsi tentang definisi tabel rahasia Anda:
SELECT e.id
, (SELECT count(*) FROM imgitem
WHERE tabid = e.id AND tab = 'esp') AS imgs -- count(*) is faster
, e.ano, e.mes, e.dia
, e.ano::text || to_char(e.mes2, 'FM"-"00')
|| to_char(e.dia, 'FM"-"00') AS data
, pl.pltag, e.inpa, e.det, d.ano anodet
, format('%s (%s)', p.abrev, p.prenome) AS determinador
, d.tax
, coalesce(v.val,v.valf) || ' ' || vu.unit AS altura
, coalesce(v1.val,v1.valf) || ' ' || vu1.unit AS dap
, d.fam, tf.nome família, d.gen, tg.nome AS gênero, d.sp
, ts.nome AS espécie, d.inf, e.loc, l.nome localidade, e.lat, e.lon
FROM pess p -- reorder!
JOIN det d ON d.detby = p.id -- INNER JOIN !
LEFT JOIN tax tf ON tf.oldfam = d.fam
LEFT JOIN tax tg ON tg.oldgen = d.gen
LEFT JOIN tax ts ON ts.oldsp = d.sp
LEFT JOIN tax ti ON ti.oldinf = d.inf -- unused, see @joop's comment
LEFT JOIN esp e ON e.det = d.id
LEFT JOIN loc l ON l.id = e.loc
LEFT JOIN var v ON v.esp = e.id AND v.key = 265
LEFT JOIN varunit vu ON vu.id = v.unit
LEFT JOIN var v1 ON v1.esp = e.id AND v1.key = 264
LEFT JOIN varunit vu1 ON vu1.id = v1.unit
LEFT JOIN pl ON pl.id = e.pl
WHERE f_unaccent(p.abrev) ILIKE f_unaccent('%' || 'vicenti' || '%') OR
f_unaccent(p.prenome) ILIKE f_unaccent('%' || 'vicenti' || '%');
Poin utama
Mengapa f_unaccent()
? Karena unaccent()
tidak dapat diindeks. Baca ini:
Saya menggunakan fungsi yang diuraikan di sana untuk memungkinkan trigram fungsional multikolom berikut (disarankan!) GIN indeks :
CREATE INDEX pess_unaccent_nome_trgm_idx ON pess
USING gin (f_unaccent(pess) gin_trgm_ops, f_unaccent(prenome) gin_trgm_ops);
Jika Anda tidak terbiasa dengan indeks trigram, baca ini dulu:
Dan mungkin:
Pastikan untuk menjalankan Postgres versi terbaru (saat ini 9.5). Ada peningkatan substansial pada indeks GIN. Dan Anda akan tertarik dengan peningkatan pg_trgm 1.2, dijadwalkan akan dirilis bersama Postgres 9.6 mendatang:
Pernyataan yang disiapkan adalah cara umum untuk mengeksekusi kueri dengan parameter (terutama dengan teks dari input pengguna). Postgres harus menemukan rencana yang paling cocok untuk parameter apa pun. Tambahkan karakter pengganti sebagai konstanta ke ke istilah pencarian seperti ini:
f_unaccent(p.abrev) ILIKE f_unaccent('%' || 'vicenti' || '%')
('vicenti'
akan diganti dengan parameter.) Jadi Postgres tahu bahwa kita berurusan dengan pola yang tidak berlabuh ke kiri atau ke kanan - yang akan memungkinkan strategi yang berbeda. Jawaban terkait dengan detail lebih lanjut:
Atau mungkin merencanakan ulang kueri untuk setiap istilah pencarian (mungkin menggunakan SQL dinamis dalam suatu fungsi). Namun pastikan waktu perencanaan tidak memakan kemungkinan peningkatan kinerja.
WHERE
kondisi kolom di pess
bertentangan dengan . Postgres terpaksa mengonversinya menjadi LEFT JOIN
INNER JOIN
. Yang lebih buruk lagi, join datang terlambat di pohon join. Dan karena Postgres tidak dapat menyusun ulang gabungan Anda (lihat di bawah), itu bisa menjadi sangat mahal. Pindahkan tabel ke pertama posisi di FROM
klausa untuk menghilangkan baris lebih awal. Mengikuti LEFT JOIN
s tidak menghilangkan baris apa pun menurut definisi. Tetapi dengan banyak tabel, penting untuk memindahkan gabungan yang mungkin berlipat ganda baris sampai akhir.
Anda bergabung dengan 13 tabel, 12 di antaranya dengan LEFT JOIN
yang meninggalkan 12!
kemungkinan kombinasi - atau 11! * 2!
jika kita mengambil yang LEFT JOIN
mempertimbangkan bahwa itu benar-benar INNER JOIN
. Itu juga banyak untuk Postgres untuk mengevaluasi semua kemungkinan permutasi untuk rencana kueri terbaik. Baca tentang join_collapse_limit
:
- Contoh Kueri untuk menampilkan kesalahan estimasi Kardinalitas di PostgreSQL
- SQL INNER JOIN pada beberapa tabel sama dengan sintaks WHERE
Pengaturan default untuk join_collapse_limit
adalah 8 , yang berarti Postgres tidak akan mencoba menyusun ulang tabel di FROM
. Anda klausa dan urutan tabel relevan .
Salah satu cara untuk mengatasinya adalah dengan membagi bagian kinerja-kritis menjadi CTE
seperti @joop berkomentar
. Jangan setel join_collapse_limit
jauh lebih tinggi atau waktu untuk perencanaan kueri yang melibatkan banyak tabel yang digabungkan akan memburuk.
Tentang tanggal gabungan bernama data
:
cast(cast(e.ano as varchar(4))||'-'||right('0'||cast(e.mes as varchar(2)),2)||'-'|| right('0'||cast(e.dia as varchar(2)),2) as varchar(10)) as data
Dengan asumsi Anda membangun dari tiga kolom numerik untuk tahun, bulan dan hari, yang didefinisikan NOT NULL
, gunakan ini sebagai gantinya:
e.ano::text || to_char(e.mes2, 'FM"-"00')
|| to_char(e.dia, 'FM"-"00') AS data
Tentang FM
pengubah pola template:
Tapi sungguh, Anda harus menyimpan tanggal sebagai tipe data date
untuk memulai.
Juga disederhanakan:
format('%s (%s)', p.abrev, p.prenome) AS determinador
Tidak akan membuat kueri lebih cepat, tetapi jauh lebih bersih. Lihat format()
.
Hal pertama terakhir, semua saran biasa untuk pengoptimalan kinerja berlaku:
Jika Anda melakukan semua ini dengan benar, Anda akan melihat kueri yang jauh lebih cepat untuk semua pola.