Dans une base de données Postgres 9.1, j'ai une table table1
avec 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?
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 casIMMUTABLE
. 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 .unaccent
immuable. J'ai ajouté ceci à la question.unaccent
module. L'une des raisons pour lesquelles je suggère un wrapper de fonction à la place.Réponses:
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:
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.
la source
J'ai trouvé un moyen d'arnaquer le planificateur de requêtes, c'est un hack assez simple:
EXPLAIN
production: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: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:
la source