Temps de requête lents pour les recherches de similarité avec les indices pg_trgm

9

Nous avons ajouté deux indices pg_trgm à une table, pour permettre une recherche floue par adresse e-mail ou par nom, car nous devons trouver les utilisateurs par nom ou par adresses e-mail qui ont été mal orthographiées lors de l'inscription (par exemple, "@ gmail.con"). ANALYZEa été exécuté après la création de l'index.

Cependant, la recherche classée sur l'un ou l'autre de ces indices est très lente dans la grande majorité des cas. c'est-à-dire qu'avec un délai d'attente plus long, une requête peut revenir en 60 secondes, en de très rares occasions aussi rapidement que 15 secondes, mais généralement les requêtes expirent.

pg_trgm.similarity_thresholdest la valeur par défaut de 0.3, mais cela 0.8ne semble pas faire de différence.

Ce tableau particulier compte plus de 25 millions de lignes et est constamment interrogé, mis à jour et inséré dans (le temps moyen pour chacun est inférieur à 2 ms). La configuration est PostgreSQL 9.6.6 s'exécutant sur une instance RDS db.m4.large avec un stockage SSD à usage général et des paramètres par défaut plus ou moins. L'extension pg_trgm est la version 1.3.

Requêtes:

  • 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;
    

Ces requêtes n'ont pas besoin d'être exécutées très souvent (des dizaines de fois par jour), mais elles doivent être basées sur l'état actuel de la table et idéalement renvoyer dans un délai d'environ 10 secondes.


Schéma:

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

(Je suis conscient que nous devrions probablement aussi ajouter unaccent()à users_search_name_idxet la requête de nom ...)


Explique:

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

La recherche par e-mail est plus susceptible de se terminer que la recherche par nom, mais c'est probablement parce que les adresses e-mail sont très similaires (par exemple, beaucoup d'adresses @ 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

Quelle pourrait être la raison de la lenteur des requêtes? Quelque chose à voir avec le nombre de tampons lus? Je n'ai pas pu trouver beaucoup d'informations sur l'optimisation de ce type particulier de requête, et les requêtes sont très similaires à celles de la documentation pg_trgm de toute façon.

Est-ce quelque chose que nous pourrions optimiser, ou mieux implémenter dans Postgres, ou envisager quelque chose comme Elasticsearch serait-il mieux adapté à ce cas d'utilisation particulier?

Christopher Orr
la source
1
Votre version est-elle d' pg_trgmau moins 1.3? Vous pouvez vérifier avec "\ dx" dans psql.
jjanes
Avez-vous été en mesure de reproduire une requête top-n classée à l'aide de l' <->opérateur qui utilise un index?
Colin 't Hart
En supposant que les paramètres sont par défaut, je jouerais avec un seuil de similitude. De cette façon, vous pouvez obtenir un résultat plus petit, donc peut-être que le coût global peut baisser ...
Michał Zaborowski
@jjanes Merci pour le pointeur. Oui, la version est 1.3.
Christopher Orr
1
@ MichałZaborowski Comme mentionné dans la question, j'ai essayé cela, mais malheureusement je n'ai vu aucune amélioration.
Christopher Orr

Réponses:

1

Vous pourrez peut-être obtenir de meilleures performances avec gin_trgm_opsplutôt qu'avec gist_trgm_ops. Ce qui est mieux est assez imprévisible, il est sensible à la distribution des motifs et des longueurs de texte dans vos données et dans vos termes de requête. Vous devez à peu près l'essayer et voir comment cela fonctionne pour vous. Une chose est que la méthode GIN sera assez sensible pg_trgm.similarity_threshold, contrairement à la méthode GiST. Cela dépendra également de la version de pg_trgm dont vous disposez. Si vous avez commencé avec une ancienne version de PostgreSQL mais que pg_upgradevous l'avez mise à jour avec , vous pourriez ne pas avoir la dernière version. Le planificateur ne fait pas mieux pour prédire quel type d'index est supérieur à ce que nous pouvons faire. Donc, pour le tester, vous ne pouvez pas simplement créer les deux, vous devez supprimer l'autre, pour forcer le planificateur à utiliser celui que vous voulez.

Dans le cas spécifique de la colonne e-mail, il serait préférable de les diviser en nom d'utilisateur et domaine, puis de rechercher un nom d'utilisateur similaire avec le domaine exact et vice versa. Ensuite, la prévalence extrême des principaux fournisseurs de messagerie cloud est moins susceptible de polluer les index avec des trigrammes qui ajoutent peu d'informations.

Enfin quel est le cas d'utilisation pour cela? Savoir pourquoi vous devez exécuter ces requêtes pourrait conduire à de meilleures suggestions. En particulier, pourquoi auriez-vous besoin de faire une recherche de similitude sur les e-mails, une fois qu'ils ont été vérifiés comme étant livrables et envoyés à la bonne personne? Peut-être pourriez-vous construire un index partiel uniquement sur le sous-ensemble d'e-mails qui n'ont pas encore été vérifiés?

jjanes
la source
Merci pour l'info. Je vais essayer un index GIN à la place et jouer avec le seuil. De plus, oui, c'est un bon point d'avoir un index partiel pour les adresses non vérifiées. Cependant, même pour les adresses e-mail vérifiées, des correspondances floues peuvent être nécessaires (par exemple, des personnes oubliant les points dans les adresses @ gmail.com), mais c'est probablement un cas pour avoir un tableau séparé avec des colonnes de partie locale et de domaine normalisées, comme vous le mentionnez.
Christopher Orr