Tiempos de consulta lentos para búsquedas de similitud con índices pg_trgm


9

Agregamos dos índices pg_trgm a una tabla, para permitir la búsqueda difusa por dirección de correo electrónico o nombre, ya que necesitamos encontrar usuarios por nombre o direcciones de correo electrónico que hayan sido mal escritas durante el registro (por ejemplo, "@ gmail.con"). ANALYZEse ejecutó después de la creación del índice.

Sin embargo, hacer una búsqueda ordenada en cualquiera de estos índices es muy lento en la gran mayoría de los casos. es decir, con un tiempo de espera incrementado, una consulta puede regresar en 60 segundos, en muy raras ocasiones tan rápido como 15 segundos, pero generalmente las consultas agotarán el tiempo de espera.

pg_trgm.similarity_thresholdes el valor predeterminado de 0.3, pero aumentar esto a 0.8no parece hacer la diferencia.

Esta tabla en particular tiene más de 25 millones de filas, y se consulta, actualiza e inserta constantemente (el tiempo medio para cada una es inferior a 2 ms). La configuración es PostgreSQL 9.6.6 ejecutándose en una instancia RDS db.m4.large con almacenamiento SSD de propósito general y parámetros predeterminados más o menos. La extensión pg_trgm es la versión 1.3.

Consultas:

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

Estas consultas no necesitan ejecutarse con mucha frecuencia (docenas de veces al día), pero deben basarse en el estado actual de la tabla, y lo ideal es que regresen en unos 10 segundos.


Esquema:

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

(Soy consciente de que probablemente deberíamos también añadir unaccent()a users_search_name_idxy la consulta de nombre ...)


Explica:

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

Es más probable que se agote el tiempo de espera de la búsqueda de correo electrónico que la búsqueda de nombre, pero probablemente se deba a que las direcciones de correo electrónico son muy similares (por ejemplo, muchas direcciones de @ 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

¿Cuál podría ser una razón para los tiempos de consulta lentos? ¿Algo relacionado con la cantidad de buffers que se leen? No pude encontrar mucha información sobre cómo optimizar este tipo particular de consulta, y las consultas son muy similares a las de la documentación de pg_trgm de todos modos.

¿Es esto algo que podríamos optimizar o implementar mejor en Postgres, o buscar algo como Elasticsearch sería mejor para este caso de uso en particular?


1
¿Es su versión de pg_trgmal menos 1.3? Puede consultar con "\ dx" en psql.
jjanes

¿Ha podido reproducir alguna consulta top-n clasificada usando el <->operador que usa un índice?
Colin 't Hart

Suponiendo que la configuración es predeterminada, jugaría con un umbral de similitud. De esa manera puede obtener un resultado más pequeño, por lo que tal vez el costo total pueda bajar ...
Michał Zaborowski

@jjanes Gracias por el puntero. Sí, la versión es 1.3.
Christopher Orr

1
@ MichałZaborowski Como se mencionó en la pregunta, intenté eso, pero desafortunadamente no vi ninguna mejora.
Christopher Orr

Respuestas:


1

Es posible que pueda obtener un mejor rendimiento en gin_trgm_opslugar de gist_trgm_ops. Lo que es mejor es bastante impredecible, es sensible a la distribución de patrones de texto y longitudes en sus datos y en sus términos de consulta. Simplemente tienes que probarlo y ver cómo funciona para ti. Una cosa es que el método GIN será bastante sensible pg_trgm.similarity_threshold, a diferencia del método GiST. También dependerá de la versión de pg_trgm que tenga. Si comenzó con una versión anterior de PostgreSQL pero la actualizó pg_upgrade, es posible que no tenga la última versión. El planificador no puede predecir mejor qué tipo de índice es superior al que podemos hacer. Entonces, para probarlo, no puedes simplemente crear ambos, tienes que soltar el otro, para forzar al planificador a usar el que quieras.

En el caso específico de la columna de correo electrónico, puede ser mejor dividirlos en nombre de usuario y dominio, y luego buscar un nombre de usuario similar con un dominio exacto y viceversa. Entonces, la prevalencia extrema de los principales proveedores de correo electrónico en la nube es menos probable que contamine los índices con trigramas que agregan poca información.

Finalmente, ¿cuál es el caso de uso para esto? Saber por qué necesita ejecutar estas consultas podría conducir a mejores sugerencias. En particular, ¿por qué necesitaría hacer una búsqueda de similitud en los correos electrónicos, una vez que se haya verificado que se pueden entregar e ir a la persona correcta? ¿Quizás podría crear un índice parcial solo en el subconjunto de correos electrónicos que aún no se han verificado?


Gracias por la info. Probaré un índice GIN y jugaré con el umbral. Además, sí, ese es un gran punto sobre tener un índice parcial para direcciones no verificadas. Sin embargo, incluso para las direcciones de correo electrónico verificadas, es posible que se necesiten coincidencias difusas (por ejemplo, personas que olvidan los puntos en las direcciones de @ gmail.com), pero ese es probablemente el caso de tener una tabla separada con columnas de dominio y partes locales normalizadas, como usted menciona.
Christopher Orr
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.