Waktu permintaan yang lambat untuk pencarian kesamaan dengan indeks pg_trgm

9

Kami menambahkan dua indeks pg_trgm ke tabel, untuk mengaktifkan pencarian fuzzy baik dengan alamat email atau nama, karena kami perlu menemukan pengguna berdasarkan nama, atau alamat email yang salah eja saat mendaftar (mis. "@ Gmail.con"). ANALYZEdijalankan setelah pembuatan indeks.

Namun, melakukan pencarian peringkat pada salah satu indeks ini sangat lambat dalam sebagian besar kasus. yaitu dengan peningkatan batas waktu, permintaan mungkin kembali dalam 60 detik, pada kesempatan yang sangat jarang secepat 15 detik, tetapi biasanya permintaan akan habis waktu.

pg_trgm.similarity_thresholdadalah nilai default dari 0.3, tetapi menabrak ini 0.8tampaknya tidak membuat perbedaan.

Tabel khusus ini memiliki lebih dari 25 juta baris, dan terus-menerus ditanyai, diperbarui, dan dimasukkan ke dalam (waktu rata-rata untuk masing-masing adalah di bawah 2ms). Penyiapannya adalah PostgreSQL 9.6.6 yang berjalan pada instance RDS db.m4.large dengan penyimpanan SSD tujuan umum, dan parameter default lebih banyak atau kurang. Ekstensi pg_trgm adalah versi 1.3.

Pertanyaan:

  • SELECT *
    FROM users
    WHERE email % '[email protected]'
    ORDER BY email <-> '[email protected]' LIMIT 10;
  • SELECT *
    FROM users
    WHERE (first_name || ' ' || last_name) % 'chris orr'
    ORDER BY (first_name || ' ' || last_name) <-> 'chris orr' LIMIT 10;

Kueri ini tidak perlu dijalankan sangat sering (puluhan kali sehari), tetapi harus didasarkan pada kondisi tabel saat ini, dan idealnya kembali dalam waktu sekitar 10 detik.


Skema:

=> \d+ users
                                          Table "public.users"
          Column   |            Type             | Collation | Nullable | Default | Storage  
-------------------+-----------------------------+-----------+----------+---------+----------
 id                | uuid                        |           | not null |         | plain    
 email             | citext                      |           | not null |         | extended 
 email_is_verified | boolean                     |           | not null |         | plain    
 first_name        | text                        |           | not null |         | extended 
 last_name         | text                        |           | not null |         | extended 
 created_at        | timestamp without time zone |           |          | now()   | plain    
 updated_at        | timestamp without time zone |           |          | now()   | plain    
                  | boolean                     |           | not null | false   | plain    
                  | character varying(60)       |           |          |         | extended 
                  | character varying(6)        |           |          |         | extended 
                  | character varying(6)        |           |          |         | extended 
                  | boolean                     |           |          |         | plain    
Indexes:
  "users_pkey" PRIMARY KEY, btree (id)
  "users_email_key" UNIQUE, btree (email)
  "users_search_email_idx" gist (email gist_trgm_ops)
  "users_search_name_idx" gist (((first_name || ' '::text) || last_name) gist_trgm_ops)
  "users_updated_at_idx" btree (updated_at)
Triggers:
  update_users BEFORE UPDATE ON users FOR EACH ROW EXECUTE PROCEDURE update_modified_column()
Options: autovacuum_analyze_scale_factor=0.01, autovacuum_vacuum_scale_factor=0.05

(Saya sadar bahwa kita harus mungkin juga menambahkan unaccent()untuk users_search_name_idxdan query nama ...)


Menjelaskan:

EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM users WHERE (first_name || ' ' || last_name) % 'chris orr' ORDER BY (first_name || ' ' || last_name) <-> 'chris orr' LIMIT 10;:

Limit  (cost=0.42..40.28 rows=10 width=152) (actual time=58671.973..58676.193 rows=10 loops=1)
  Buffers: shared hit=66227 read=231821
  ->  Index Scan using users_search_name_idx on users  (cost=0.42..100264.13 rows=25153 width=152) (actual time=58671.970..58676.180 rows=10 loops=1)
        Index Cond: (((first_name || ' '::text) || last_name) % 'chris orr'::text)
        Order By: (((first_name || ' '::text) || last_name) <-> 'chris orr'::text"
        Buffers: shared hit=66227 read=231821
Planning time: 0.125 ms
Execution time: 58676.265 ms

Pencarian email lebih cenderung ke waktu daripada pencarian nama, tetapi itu mungkin karena alamat email sangat mirip (misalnya banyak alamat @ gmail.com).

EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM users WHERE email % '[email protected]' ORDER BY email <-> '[email protected]' LIMIT 10;:

Limit  (cost=0.42..40.43 rows=10 width=152) (actual time=58851.719..62181.128 rows=10 loops=1)
  Buffers: shared hit=83 read=428918
  ->  Index Scan using users_search_email_idx on users  (cost=0.42..100646.36 rows=25153 width=152) (actual time=58851.716..62181.113 rows=10 loops=1)
        Index Cond: ((email)::text % '[email protected]'::text)
        Order By: ((email)::text <-> '[email protected]'::text)
        Buffers: shared hit=83 read=428918
Planning time: 0.100 ms
Execution time: 62181.186 ms

Apa yang bisa menjadi alasan untuk waktu permintaan yang lambat? Ada hubungannya dengan jumlah buffer yang dibaca? Saya tidak dapat menemukan banyak informasi tentang cara mengoptimalkan jenis permintaan ini, dan pertanyaannya sangat mirip dengan yang ada dalam dokumentasi pg_trgm.

Apakah ini sesuatu yang bisa kami optimalkan, atau terapkan lebih baik di Postgres, atau ingin sesuatu seperti Elasticsearch lebih cocok untuk kasus penggunaan khusus ini?

Christopher Orr
sumber
1
Apakah versi Anda pg_trgmsetidaknya 1,3? Anda dapat memeriksa dengan "\ dx" di psql.
jjanes
Apakah Anda dapat mereproduksi kueri n teratas yang diberi peringkat menggunakan <->operator yang menggunakan indeks?
Colin 't Hart
Dengan asumsi pengaturan default saya akan bermain dengan ambang kesamaan. Dengan begitu Anda bisa mendapatkan hasil yang lebih kecil, jadi mungkin biaya keseluruhan bisa turun ...
Michał Zaborowski
@ jjanes Terima kasih atas penunjuknya. Ya, versinya adalah 1.3.
Christopher Orr
1
@ MichałZaborowski Seperti yang disebutkan dalam pertanyaan, saya mencobanya, tapi sayangnya tidak ada perbaikan.
Christopher Orr

Jawaban:

1

Anda mungkin bisa mendapatkan kinerja yang lebih baik gin_trgm_opsdaripada gist_trgm_ops. Mana yang lebih baik sangat tidak terduga, sensitif terhadap distribusi pola dan panjang teks dalam data Anda dan dalam istilah kueri Anda. Anda cukup banyak hanya harus mencobanya dan melihat cara kerjanya untuk Anda. Satu hal adalah bahwa metode GIN akan sangat sensitif pg_trgm.similarity_threshold, tidak seperti metode GiST. Ini juga akan tergantung pada versi pg_trgm apa yang Anda miliki. Jika Anda mulai dengan PostgreSQL versi lama tetapi memperbaruinya dengan pg_upgrade, Anda mungkin tidak memiliki versi terbaru. Perencana tidak lebih baik dalam memprediksi tipe indeks mana yang lebih unggul daripada yang dapat kita lakukan. Jadi untuk mengujinya, Anda tidak bisa hanya membuat keduanya, Anda harus menjatuhkan yang lain, untuk memaksa perencana untuk menggunakan yang Anda inginkan.

Dalam kasus khusus kolom email, Anda mungkin lebih baik membaginya menjadi nama pengguna dan domain, dan kemudian meminta nama pengguna yang mirip dengan domain yang tepat dan sebaliknya. Kemudian prevalensi ekstrim dari penyedia surel cloud besar kurang cenderung mencemari indeks dengan trigram yang menambahkan sedikit informasi.

Akhirnya, apa gunanya kasus ini? Mengetahui mengapa Anda perlu menjalankan kueri ini dapat menghasilkan saran yang lebih baik. Khususnya, mengapa Anda perlu melakukan pencarian kesamaan pada email, setelah mereka diverifikasi sebagai pengiriman dan pergi ke orang yang tepat? Mungkin Anda bisa membuat indeks parsial hanya pada himpunan bagian dari email yang belum diverifikasi?

jjanes
sumber
Terimakasih atas infonya. Saya akan mencoba indeks GIN sebagai gantinya, dan bermain dengan ambang batas. Juga, ya, itu poin bagus tentang memiliki indeks parsial untuk alamat yang tidak diverifikasi. Namun bahkan untuk alamat email terverifikasi masih ada kecocokan fuzzy (mis. Orang lupa titik-titik di alamat @ gmail.com), tetapi itu mungkin merupakan kasus untuk memiliki tabel terpisah dengan bagian lokal dan kolom domain yang dinormalisasi, seperti yang Anda sebutkan.
Christopher Orr