Pencarian trigram menjadi jauh lebih lambat karena string pencarian menjadi lebih panjang

17

Dalam database Postgres 9.1, saya memiliki tabel table1dengan ~ 1,5 juta baris dan kolom label(nama yang disederhanakan untuk pertanyaan ini).

Ada trigram-indeks fungsional pada lower(unaccent(label))( unaccent()telah dibuat tidak dapat diubah untuk memungkinkan penggunaannya dalam indeks).

Permintaan berikut ini cukup cepat:

SELECT count(*) FROM table1
WHERE (lower(unaccent(label)) like lower(unaccent('%someword%')));
 count 
-------
     1
(1 row)

Time: 394,295 ms

Tetapi kueri berikut lebih lambat:

SELECT count(*) FROM table1
WHERE (lower(unaccent(label)) like lower(unaccent('%someword and some more%')));
 count 
-------
     1
(1 row)

Time: 1405,749 ms

Dan menambahkan lebih banyak kata bahkan lebih lambat, meskipun pencariannya lebih ketat.

Saya mencoba trik sederhana untuk menjalankan subquery untuk kata pertama dan kemudian kueri dengan string pencarian lengkap, tetapi (sayangnya) perencana kueri melihat melalui intrik saya:

EXPLAIN ANALYZE
SELECT * FROM (
   SELECT id, title, label from table1
   WHERE lower(unaccent(label)) like lower(unaccent('%someword%'))
   ) t1
WHERE lower(unaccent(label)) like lower(unaccent('%someword and some more%'));
Bitmap Heap Scan pada table1 (biaya = 16216.01..16220.04 baris = 1 lebar = 212) (waktu aktual = 1824.017..1824.019 baris = 1 loop = 1)
  Periksa kembali Cond: ((lebih rendah (unaccent ((label) :: text)) ~~ '% anyord%' :: text) AND (lebih rendah (unaccent ((label) :: text)) ~~ '% sortord dan beberapa lagi %'::teks))
  -> Pemindaian Indeks Bitmap pada table1_label_hun_gin_trgm (biaya = 0,00..16216,01 baris = 1 lebar = 0) (waktu aktual = 1823.900..1823.900 baris = 1 putaran = 1)
        Indeks Cond: ((lebih rendah (unaccent ((label) :: text)) ~~ '% anyord%' :: text) AND (lebih rendah (unaccent ((label) :: text)) ~~ '% anyord dan beberapa lagi %'::teks))
Total runtime: 1824.064 ms

Masalah utama saya adalah bahwa string pencarian berasal dari antarmuka web yang dapat mengirim string yang cukup panjang dan karenanya sangat lambat dan juga merupakan vektor DOS.

Jadi pertanyaan saya adalah:

  • Bagaimana cara mempercepat kueri?
  • Apakah ada cara untuk memecahnya menjadi subqueries sehingga lebih cepat?
  • Mungkin versi Postgres yang lebih baru lebih baik? (Saya mencoba 9,4 dan sepertinya tidak lebih cepat: masih efek yang sama. Mungkin versi yang lebih baru?)
  • Mungkin diperlukan strategi pengindeksan yang berbeda?
P.Péter
sumber
1
Harus disebutkan bahwa unaccent()ini juga disediakan oleh modul tambahan dan Postgres tidak mendukung indeks pada fungsi secara default karena tidak IMMUTABLE. Anda harus mengubah sesuatu dan Anda harus menyebutkan apa yang Anda lakukan persis dalam pertanyaan Anda. Saran berdiri saya: stackoverflow.com/a/11007216/939860 . Juga, indeks trigram mendukung pencocokan case-sensitive di luar kotak. Anda dapat menyederhanakan untuk: WHERE f_unaccent(label) ILIKE f_unaccent('%someword%')- dengan indeks yang cocok. Detail: stackoverflow.com/a/28636000/939860 .
Erwin Brandstetter
Saya hanya menyatakan unaccentabadi. Saya menambahkan ini ke pertanyaan.
P.Péter
Ketahuilah bahwa peretasan ditimpa saat Anda memperbarui unaccentmodul. Salah satu alasan mengapa saya menyarankan pembungkus fungsi sebagai gantinya.
Erwin Brandstetter

Jawaban:

35

Di PostgreSQL 9.6 akan ada versi baru pg_trgm, 1.2, yang akan jauh lebih baik tentang ini. Dengan sedikit usaha, Anda juga bisa membuat versi baru ini berfungsi di bawah PostgreSQL 9.4 (Anda harus menerapkan tambalan, dan mengkompilasi modul ekstensi sendiri dan menginstalnya).

Apa yang dilakukan versi tertua adalah mencari setiap trigram dalam kueri dan mengambil gabungannya, lalu menerapkan filter. Apa yang versi baru akan lakukan adalah memilih trigram yang paling langka dalam kueri dan mencari yang itu, dan kemudian menyaring sisanya.

Mesin untuk melakukan ini tidak ada dalam 9.1. Di 9.4 bahwa mesin ditambahkan, tetapi pg_trgm tidak diadaptasi untuk menggunakannya saat itu.

Anda masih memiliki potensi masalah DOS, karena orang jahat dapat membuat kueri yang hanya memiliki trigram umum. seperti '% dan%', atau bahkan '% a%'


Jika Anda tidak dapat memutakhirkan ke pg_trgm 1.2, maka cara lain untuk mengelabui perencana adalah:

WHERE (lower(unaccent(label)) like lower(unaccent('%someword%'))) 
AND   (lower(unaccent(label||'')) like 
      lower(unaccent('%someword and some more%')));

Dengan menggabungkan string kosong ke label, Anda menipu perencana untuk berpikir itu tidak dapat menggunakan indeks pada bagian mana klausa. Jadi ia menggunakan indeks pada% anyord%, dan menerapkan filter untuk hanya baris-baris itu.


Juga, jika Anda selalu mencari seluruh kata, Anda bisa menggunakan fungsi untuk tokenize string ke dalam array kata, dan menggunakan indeks GIN built-in biasa (bukan pg_trgm) pada fungsi yang mengembalikan array.

jjanes
sumber
13
Layak disebutkan bahwa kaulah yang menulis tambalan itu. Dan tes kinerja awal sangat mengesankan. Ini benar - benar layak mendapatkan lebih banyak upvotes (juga untuk penjelasan dan solusi dengan versi saat ini).
Erwin Brandstetter
Saya akan lebih tertarik pada setidaknya referensi ke mesin yang Anda gunakan untuk mengimplementasikan tambalan yang tidak ada di 9.1. Tapi, aku setuju dengan jawaban pantat buruk Erwin.
Evan Carroll
4

Saya telah menemukan cara untuk menipu perencana kueri, ini adalah hack yang cukup sederhana:

SELECT *
FROM (
   select id, title, label
   from   table1
   where  lower(unaccent(label)) like lower(unaccent('%someword%'))
   ) t1
WHERE lower(lower(unaccent(label))) like lower(unaccent('%someword and more%'))

EXPLAIN keluaran:

Bitmap Heap Scan pada table1 (biaya = 6749.11..7332.71 baris = 1 lebar = 212) (waktu aktual = 256.607..256.609 baris = 1 loop = 1)
  Periksa kembali Cond: (lebih rendah (unaccent ((label_hun) :: text)) ~~ '% anyord%' :: text)
  Saring: (lebih rendah (lebih rendah (tanpa tanda baca (label) :: teks))) ~~ '% sesuatu dan lebih banyak lagi%' :: teks)
  -> Pemindaian Indeks Bitmap pada table1_label_hun_gin_trgm (biaya = 0,00..6749,11 baris = 147 lebar = 0) (waktu aktual = 256,499..256,499 baris = 1 putaran = 1)
        Indeks Cond: (lebih rendah (unaccent ((label) :: text)) ~~ '% anyord%' :: text)
Total runtime: 256.653 ms

Jadi, karena tidak ada indeks untuk lower(lower(unaccent(label))), ini akan membuat pemindaian berurutan, sehingga akan berubah menjadi filter sederhana. Terlebih lagi, sederhana DAN juga akan melakukan hal yang sama:

SELECT id, title, label
FROM table1
WHERE lower(unaccent(label)) like lower(unaccent('%someword%'))
AND   lower(lower(unaccent(label))) like lower(unaccent('%someword and more%'))

Tentu saja, ini adalah heuristik yang mungkin tidak berfungsi dengan baik, jika bagian cut-out yang digunakan dalam pemindaian indeks sangat umum. Tetapi dalam database kami, tidak ada pengulangan yang terlalu banyak, jika saya menggunakan sekitar 10-15 karakter.

Ada dua pertanyaan kecil yang tersisa:

  • Mengapa postgres tidak tahu bahwa sesuatu seperti ini akan bermanfaat?
  • Apa yang dilakukan postgres dalam rentang waktu 0..256.499 (lihat menganalisis keluaran)?
P.Péter
sumber
1
Dalam rentang waktu antara 0 dan 256.499 itu sedang membangun bitmap. Pada 256.499 ini menghasilkan output pertama, yaitu bitmap. Yang juga merupakan output terakhirnya, karena hanya menghasilkan output tunggal - bitmap lengkap.
jjanes