La ricerca del trigramma diventa molto più lenta man mano che la stringa di ricerca aumenta


16

In un database Postgres 9.1, ho una tabella table1con ~ 1,5 M righe e una colonna label(nomi semplificati per il bene di questa domanda).

C'è un trigramma-indice attivo su lower(unaccent(label))( unaccent()è stato reso immutabile per consentirne l'uso nell'indice).

La seguente query è abbastanza veloce:

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

Time: 394,295 ms

Ma la seguente query è più lenta:

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

Time: 1405,749 ms

E l'aggiunta di più parole è ancora più lenta, anche se la ricerca è più rigorosa.

Ho provato un semplice trucco per eseguire una sottoquery per la prima parola e poi una query con la stringa di ricerca completa, ma (purtroppo) il pianificatore di query ha visto attraverso le mie macchinazioni:

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%'));
Scansione heap bitmap su tabella1 (costo = 16216.01..16220.04 righe = 1 larghezza = 212) (tempo effettivo = 1824.017..1824.019 righe = 1 loop = 1)
  Ricontrolla Cond: ((lower (unaccent ((label) :: text)) ~~ '% someword%' :: text) AND (lower (unaccent ((label) :: text)) ~~ '% someword e alcuni altri %'::testo))
  -> Scansione indice bitmap su table1_label_hun_gin_trgm (costo = 0,00..16216,01 righe = 1 larghezza = 0) (tempo effettivo = 1823.900..1823.900 righe = 1 loop = 1)
        Index Cond: ((lower (unaccent ((label) :: text)) ~~ '% someword%' :: text) AND (lower (unaccent ((label) :: text)) ~~ '% someword e alcuni altri %'::testo))
Durata totale: 1824.064 ms

Il mio ultimo problema è che la stringa di ricerca proviene da un'interfaccia web che può inviare stringhe piuttosto lunghe e quindi essere piuttosto lenta e può anche costituire un vettore DOS.

Quindi le mie domande sono:

  • Come velocizzare la query?
  • C'è un modo per dividerlo in sottoquery in modo che sia più veloce?
  • Forse una versione successiva di Postgres è migliore? (Ho provato 9.4 e non sembra più veloce: sempre lo stesso effetto. Forse una versione successiva?)
  • Forse è necessaria una diversa strategia di indicizzazione?

1
Va detto che unaccent()è fornito anche da un modulo aggiuntivo e Postgres non supporta gli indici sulla funzione di default poiché non lo è IMMUTABLE. Devi aver modificato qualcosa e dovresti menzionare ciò che hai fatto esattamente nella tua domanda. Il mio consiglio permanente: stackoverflow.com/a/11007216/939860 . Inoltre, gli indici trigram supportano la corrispondenza senza distinzione tra maiuscole e minuscole. È possibile semplificare a: WHERE f_unaccent(label) ILIKE f_unaccent('%someword%')- con un indice corrispondente. Dettagli: stackoverflow.com/a/28636000/939860 .
Erwin Brandstetter,

Ho semplicemente dichiarato unaccentimmutabile. Ho aggiunto questo alla domanda.
P.Péter,

Tenere presente che l'hacking viene sovrascritto quando si aggiorna il unaccentmodulo. Uno dei motivi per cui suggerisco invece un wrapper di funzioni.
Erwin Brandstetter,

Risposte:


34

In PostgreSQL 9.6 ci sarà una nuova versione di pg_trgm, 1.2, che sarà molto meglio al riguardo. Con un piccolo sforzo, puoi anche far funzionare questa nuova versione con PostgreSQL 9.4 (devi applicare la patch, compilare tu stesso il modulo di estensione e installarlo).

Quello che fa la versione più vecchia è cercare ciascun trigramma nella query e prendere l'unione di essi, quindi applicare un filtro. Quello che farà la nuova versione è scegliere il trigramma più raro nella query e cercarne solo uno, quindi filtrare il resto in seguito.

I macchinari per farlo non esistono in 9.1. In 9.4 quel macchinario fu aggiunto, ma pg_trgm non era adattato per usarlo in quel momento.

Avresti ancora un potenziale problema DOS, poiché la persona maliziosa può creare una query che ha solo trigrammi comuni. come "% and%" o anche "% a%"


Se non riesci ad eseguire l'aggiornamento a pg_trgm 1.2, un altro modo per ingannare il planner sarebbe:

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

Concatenando la stringa vuota da etichettare, induci il planner a pensare che non possa usare l'indice su quella parte della clausola where. Quindi utilizza l'indice solo su% someword% e applica un filtro solo a quelle righe.


Inoltre, se sei sempre alla ricerca di intere parole, puoi utilizzare una funzione per tokenizzare la stringa in un array di parole e utilizzare un normale indice GIN incorporato (non pg_trgm) su quella funzione di ritorno dell'array.


13
Vale la pena ricordare che sei stato tu a scrivere la patch. E i test preliminari sulle prestazioni sono impressionanti. Questo merita davvero più voti (anche per la spiegazione e la soluzione alternativa con la versione corrente).
Erwin Brandstetter,

Sarei più interessato almeno a un riferimento al macchinario che hai usato per implementare la patch che non era presente in 9.1. Ma sono d'accordo con la risposta di Erwin.
Evan Carroll,

3

Ho trovato un modo per truffare il planner delle query, è un trucco abbastanza semplice:

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 produzione:

Scansione heap bitmap su tabella1 (costo = 6749.11..7332.71 righe = 1 larghezza = 212) (tempo effettivo = 256.607..256.609 righe = 1 loop = 1)
  Ricontrolla Cond: (inferiore (non accento ((label_hun) :: testo)) ~~ '% someword%' :: testo)
  Filtro: (inferiore (inferiore (unaccent ((etichetta) :: testo))) ~~ '% someword e qualche altro%' :: text)
  -> Scansione indice bitmap su table1_label_hun_gin_trgm (costo = 0.00..6749.11 righe = 147 larghezza = 0) (tempo effettivo = 256.499..256.499 righe = 1 loop = 1)
        Indice cond: (inferiore (unaccent ((etichetta) :: testo)) ~~ '% someword%' :: testo)
Durata totale: 256.653 ms

Quindi, poiché non esiste un indice per lower(lower(unaccent(label))), ciò creerebbe una scansione sequenziale, quindi verrà trasformato in un semplice filtro. Inoltre, un semplice AND farà lo stesso:

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%'))

Naturalmente, si tratta di un'euristica che potrebbe non funzionare bene, se la parte ritagliata utilizzata nella scansione dell'indice è molto comune. Ma nel nostro database, non c'è davvero tanta ripetizione, se uso circa 10-15 caratteri.

Sono rimaste due piccole domande:

  • Perché Postgres non può capire che qualcosa del genere sarebbe utile?
  • Cosa fa postgres nell'intervallo di tempo 0..256.499 (vedi analisi dell'output)?

1
Nell'intervallo di tempo compreso tra 0 e 256,499 sta creando la bitmap. A 256.499 produce il suo primo output, che è la bitmap. Che è anche il suo ultimo output, poiché produce solo un singolo output - una singola bitmap completata.
jjanes,
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.