La recherche de trigrammes devient beaucoup plus lente à mesure que la chaîne de recherche s'allonge


16

Dans une base de données Postgres 9.1, j'ai une table table1avec environ 1,5 million de lignes et une colonne label(noms simplifiés pour cette question).

Il existe un trigram-index fonctionnel lower(unaccent(label))( unaccent()a été rendu immuable pour permettre son utilisation dans l'index).

La requête suivante est assez rapide:

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

Time: 394,295 ms

Mais la requête suivante est plus lente:

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

Time: 1405,749 ms

Et ajouter plus de mots est encore plus lent, même si la recherche est plus stricte.

J'ai essayé une astuce simple pour exécuter une sous-requête pour le premier mot, puis une requête avec la chaîne de recherche complète, mais (malheureusement) le planificateur de requêtes a vu à travers mes machinations:

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 sur table1 (coût = 16216.01..16220.04 lignes = 1 largeur = 212) (temps réel = 1824.017..1824.019 lignes = 1 boucles = 1)
  Vérifiez à nouveau Cond: ((lower (unaccent ((label) :: text)) ~~ '% someword%' :: text) AND (lower (unaccent ((label) :: text)) ~~ '% someword et quelques autres %'::texte))
  -> Bitmap Index Scan sur table1_label_hun_gin_trgm (coût = 0,00..16216,01 lignes = 1 largeur = 0) (temps réel = 1823,900..1823,900 lignes = 1 boucles = 1)
        Index Cond: ((lower (unaccent ((label) :: text)) ~~ '% someword%' :: text) AND (lower (unaccent ((label) :: text)) ~~ '% someword et quelques autres %'::texte))
Durée d'exécution totale: 1824,064 ms

Mon problème ultime est que la chaîne de recherche provient d'une interface Web qui peut envoyer des chaînes assez longues et donc être assez lente et peut également constituer un vecteur DOS.

Mes questions sont donc:

  • Comment accélérer la requête?
  • Existe-t-il un moyen de le diviser en sous-requêtes afin qu'il soit plus rapide?
  • Peut-être qu'une version ultérieure de Postgres est meilleure? (J'ai essayé 9.4 et ça ne semble pas plus rapide: toujours le même effet. Peut-être une version plus récente?)
  • Peut-être qu'une stratégie d'indexation différente est nécessaire?

1
Il faut mentionner qu'il unaccent()est également fourni par un module supplémentaire et Postgres ne prend pas en charge les index sur la fonction par défaut car ce n'est pas le cas IMMUTABLE. Vous devez avoir modifié quelque chose et vous devez mentionner exactement ce que vous avez fait dans votre question. Mon conseil permanent: stackoverflow.com/a/11007216/939860 . De plus, les index trigrammes prennent en charge la correspondance insensible à la casse prête à l'emploi. Vous pouvez simplifier pour: WHERE f_unaccent(label) ILIKE f_unaccent('%someword%')- avec un index correspondant. Détails: stackoverflow.com/a/28636000/939860 .
Erwin Brandstetter

J'ai simplement déclaré unaccentimmuable. J'ai ajouté ceci à la question.
P.Péter

Sachez que le hack est écrasé lorsque vous mettez à jour le unaccentmodule. L'une des raisons pour lesquelles je suggère un wrapper de fonction à la place.
Erwin Brandstetter

Réponses:


34

Dans PostgreSQL 9.6, il y aura une nouvelle version de pg_trgm, 1.2, qui sera bien meilleure à ce sujet. Avec un petit effort, vous pouvez également faire fonctionner cette nouvelle version sous PostgreSQL 9.4 (vous devez appliquer le patch, compiler le module d'extension vous-même et l'installer).

Ce que fait la version la plus ancienne, c'est rechercher chaque trigramme dans la requête et prendre leur union, puis appliquer un filtre. La nouvelle version va choisir le trigramme le plus rare dans la requête et rechercher juste celui-là, puis filtrer le reste plus tard.

Le mécanisme pour le faire n'existe pas en 9.1. En 9.4, cette machinerie a été ajoutée, mais pg_trgm n'était pas adapté pour l'utiliser à ce moment-là.

Vous auriez toujours un problème DOS potentiel, car la personne malveillante peut créer une requête qui n'a que des trigrammes communs. comme '% et%', ou même '% a%'


Si vous ne pouvez pas passer à pg_trgm 1.2, une autre façon de tromper le planificateur serait:

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

En concaténant la chaîne vide à étiqueter, vous incitez le planificateur à penser qu'il ne peut pas utiliser l'index sur cette partie de la clause where. Il utilise donc l'index uniquement sur le% someword% et applique un filtre à ces lignes uniquement.


De plus, si vous recherchez toujours des mots entiers, vous pouvez utiliser une fonction pour convertir la chaîne en jetons dans un tableau de mots, et utiliser un index GIN standard intégré (pas pg_trgm) sur cette fonction de retour de tableau.


13
Il convient de mentionner que c'est vous qui avez écrit le patch. Et les tests de performances préliminaires sont impressionnants. Cela mérite vraiment plus de votes positifs (également pour l'explication et la solution de contournement avec la version actuelle).
Erwin Brandstetter du

Je serais plus intéressé par au moins une référence à la machinerie que vous avez utilisée pour implémenter le correctif qui n'était pas là en 9.1. Mais, je suis d'accord avec la réponse Erwin Bad Ass.
Evan Carroll

3

J'ai trouvé un moyen d'arnaquer le planificateur de requêtes, c'est un hack assez simple:

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

Bitmap Heap Scan sur table1 (coût = 6749.11..7332.71 lignes = 1 largeur = 212) (temps réel = 256.607..256.609 lignes = 1 boucles = 1)
  Vérifiez à nouveau Cond: (lower (unaccent ((label_hun) :: text)) ~~ '% someword%' :: text)
  Filtre: (inférieur (inférieur (non accentué ((étiquette) :: texte))) ~~ '% un mot et encore plus%' :: texte)
  -> Bitmap Index Scan sur table1_label_hun_gin_trgm (coût = 0,00..6749,11 lignes = 147 largeur = 0) (temps réel = 256,499..256,499 lignes = 1 boucles = 1)
        Index Cond: (inférieur (non accentué ((label) :: text)) ~~ '% someword%' :: text)
Durée d'exécution totale: 256,653 ms

Donc, comme il n'y a pas d'index pour lower(lower(unaccent(label))), cela créerait un balayage séquentiel, donc il deviendrait un simple filtre. De plus, un simple ET fera de même:

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

Bien sûr, il s'agit d'une heuristique qui peut ne pas fonctionner correctement si la partie découpée utilisée dans l'analyse d'index est très courante. Mais dans notre base de données, il n'y a pas vraiment autant de répétitions, si j'utilise environ 10-15 caractères.

Il reste deux petites questions:

  • Pourquoi les postgres ne peuvent-ils pas comprendre que quelque chose comme ça serait bénéfique?
  • Que font les postgres dans la plage de temps 0..256.499 (voir analyser la sortie)?

1
Dans l'intervalle de temps compris entre 0 et 256,499, il construit le bitmap. À 256,499, il produit sa première sortie, qui est le bitmap. Ce qui est également sa dernière sortie, car elle ne produit qu'une seule sortie - une seule bitmap terminée.
jjanes
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.