Tempi di query lenti per ricerche di somiglianza con indici pg_trgm


9

Abbiamo aggiunto due indici pg_trgm a una tabella, per consentire la ricerca fuzzy per indirizzo e-mail o nome, poiché dobbiamo trovare utenti per nome o indirizzi e-mail che sono stati erroneamente registrati durante la registrazione (ad esempio "@ gmail.con"). ANALYZEè stato eseguito dopo la creazione dell'indice.

Tuttavia, fare una ricerca classificata su uno di questi indici è molto lento nella stragrande maggioranza dei casi. ad esempio, con un timeout maggiore, una query potrebbe tornare in 60 secondi, in occasioni molto rare fino a 15 secondi, ma di solito le query scadranno.

pg_trgm.similarity_thresholdè il valore predefinito di 0.3, ma aumentarlo fino a 0.8che non sembra fare la differenza.

Questa particolare tabella ha oltre 25 milioni di righe ed è costantemente interrogata, aggiornata e inserita (il tempo medio per ciascuna è inferiore a 2 ms). L'impostazione è PostgreSQL 9.6.6 in esecuzione su un'istanza RDS db.m4.large con memoria SSD per scopi generici e parametri predefiniti più o meno. L'estensione pg_trgm è la versione 1.3.

Interrogazioni:

  • SELECT *
    FROM users
    WHERE email % 'chris@example.com'
    ORDER BY email <-> 'chris@example.com' LIMIT 10;
  • SELECT *
    FROM users
    WHERE (first_name || ' ' || last_name) % 'chris orr'
    ORDER BY (first_name || ' ' || last_name) <-> 'chris orr' LIMIT 10;

Queste query non devono essere eseguite molto spesso (dozzine di volte al giorno), ma dovrebbero essere basate sullo stato corrente della tabella e idealmente restituire entro circa 10 secondi.


Schema:

=> \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

(Mi rendo conto che probabilmente dovremmo anche aggiungere unaccent()al users_search_name_idxe la query nome ...)


spiega:

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

È più probabile che la ricerca e-mail scada rispetto alla ricerca per nome, ma presumibilmente perché gli indirizzi e-mail sono così simili (ad esempio molti indirizzi @ gmail.com).

EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM users WHERE email % 'chris@example.com' ORDER BY email <-> 'chris@example.com' 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 % 'chris@example.com'::text)
        Order By: ((email)::text <-> 'chris@example.com'::text)
        Buffers: shared hit=83 read=428918
Planning time: 0.100 ms
Execution time: 62181.186 ms

Quale potrebbe essere una ragione per i tempi di query lenti? Qualcosa a che fare con il numero di buffer letti? Non sono riuscito a trovare molte informazioni sull'ottimizzazione di questo particolare tipo di query e le query sono comunque molto simili a quelle contenute nella documentazione di pg_trgm.

È qualcosa che potremmo ottimizzare, o implementare meglio in Postgres, o cercare qualcosa come Elasticsearch potrebbe adattarsi meglio a questo particolare caso d'uso?


1
La tua versione è pg_trgmalmeno 1.3? Puoi controllare con "\ dx" in psql.
jjanes,

Sei stato in grado di riprodurre qualsiasi query top-n classificata utilizzando l' <->operatore che utilizza un indice?
Colin 't Hart,

Supponendo che le impostazioni siano predefinite, giocherei con la soglia di somiglianza. In questo modo puoi ottenere risultati più piccoli, quindi forse il costo complessivo può diminuire ...
Michał Zaborowski

@jjanes Grazie per il puntatore. Sì, la versione è 1.3.
Christopher Orr,

1
@ MichałZaborowski Come menzionato nella domanda, l'ho provato, ma sfortunatamente non ho riscontrato alcun miglioramento.
Christopher Orr,

Risposte:


1

Potresti essere in grado di ottenere prestazioni migliori con gin_trgm_opsanziché gist_trgm_ops. La cosa migliore è piuttosto imprevedibile, è sensibile alla distribuzione di modelli di testo e lunghezze nei dati e nei termini della query. Devi solo provarlo e vedere come funziona per te. Una cosa è che il metodo GIN sarà abbastanza sensibile pg_trgm.similarity_threshold, a differenza del metodo GiST. Dipenderà anche da quale versione di pg_trgm hai. Se hai iniziato con una versione precedente di PostgreSQL ma l' pg_upgradehai aggiornata , potresti non avere l'ultima versione. Il planner non è in grado di prevedere quale tipo di indice sia superiore a quello che possiamo fare. Quindi, per provarlo, non puoi semplicemente creare entrambi, devi lasciar cadere l'altro, per forzare il pianificatore a usare quello che desideri.

Nel caso specifico della colonna e-mail, potresti essere meglio dividerli in nome utente e dominio, quindi eseguire una query per un nome utente simile con dominio esatto e viceversa. Quindi l'estrema prevalenza dei principali provider di e-mail cloud ha meno probabilità di inquinare gli indici con trigrammi che aggiungono poche informazioni.

Infine, qual è il caso d'uso per questo? Sapere perché è necessario eseguire queste query potrebbe portare a suggerimenti migliori. In particolare, perché dovresti fare una ricerca di somiglianza sulle e-mail, una volta che sono state verificate come consegnabili e che vanno alla persona corretta? Forse potresti creare un indice parziale solo sul sottoinsieme di email che non sono ancora state verificate?


Grazie per le informazioni. Proverò invece un indice GIN e giocherò con la soglia. Inoltre, sì, questo è un ottimo punto per avere un indice parziale per indirizzi non verificati. Tuttavia, anche per gli indirizzi di posta elettronica verificati, possono essere necessarie corrispondenze fuzzy (ad esempio persone che dimenticano i punti negli indirizzi @ gmail.com), ma probabilmente è il caso di avere una tabella separata con colonne di parte locale e dominio normalizzate, come accennato.
Christopher Orr,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.