La búsqueda de trigrama se vuelve mucho más lenta a medida que la cadena de búsqueda se alarga


16

En una base de datos Postgres 9.1, tengo una tabla table1con ~ 1.5M filas y una columna label(nombres simplificados por el bien de esta pregunta).

Hay un funcional trigrama-índice en lower(unaccent(label))( unaccent()se ha hecho inmutable para permitir su uso en el índice).

La siguiente consulta es bastante rápida:

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

Time: 394,295 ms

Pero la siguiente consulta es más lenta:

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

Time: 1405,749 ms

Y añadiendo más palabras es aún más lenta, a pesar de que la búsqueda es más estricta.

Intenté un simple truco para ejecutar una subconsulta para la primera palabra y luego una consulta con la cadena de búsqueda completa, pero (tristemente) el planificador de consultas vio a través de mis maquinaciones:

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 en la tabla1 (costo = 16216.01..16220.04 filas = 1 ancho = 212) (tiempo real = 1824.017..1824.019 filas = 1 bucles = 1)
  Vuelva a verificar Cond: ((lower (unaccent ((label) :: text)) ~~ '% someword%' :: text) AND (lower (unaccent ((label) :: text)) ~~ '% someword y algo más %'::texto))
  -> Escaneo de índice de mapa de bits en table1_label_hun_gin_trgm (costo = 0.00..16216.01 filas = 1 ancho = 0) (tiempo real = 1823.900..1823.900 filas = 1 bucles = 1)
        Índice Cond: ((lower (unaccent ((label) :: text)) ~~ '% someword%' :: text) AND (lower (unaccent ((label) :: text)) ~~ '% someword y algo más %'::texto))
Tiempo de ejecución total: 1824.064 ms

Mi problema principal es que la cadena de búsqueda proviene de una interfaz web que puede enviar cadenas bastante largo y por lo tanto ser muy lento y también puede constituir un vector de DOS.

Entonces mis preguntas son:

  • ¿Cómo acelerar la consulta?
  • ¿Hay una manera de dividirlo en sub consultas por lo que es más rápido?
  • ¿Quizás una versión posterior de Postgres es mejor? (Intenté 9.4 y no parece más rápido: sigue siendo el mismo efecto. ¿Quizás una versión posterior?)
  • ¿Quizás se necesita una estrategia de indexación diferente?

1
Cabe mencionar que unaccent()también es proporcionado por un módulo adicional y Postgres no admite índices en la función de forma predeterminada, ya que no lo es IMMUTABLE. Debe haber alterado algo y debe mencionar lo que hizo exactamente en su pregunta. Mi consejo permanente: stackoverflow.com/a/11007216/939860 . Además, los índices de trigram admiten compatibilidad sin distinción entre mayúsculas y minúsculas. Se puede simplificar a: WHERE f_unaccent(label) ILIKE f_unaccent('%someword%')- con un índice coincidente. Detalles: stackoverflow.com/a/28636000/939860 .
Erwin Brandstetter

Simplemente me declaró unaccentinmutable. Agregué esto a la pregunta.
P.Péter

Tenga en cuenta que el truco se sobrescribe cuando actualiza el unaccentmódulo. Una de las razones por las que sugiero un envoltorio de funciones en su lugar.
Erwin Brandstetter

Respuestas:


34

En PostgreSQL 9.6 habrá una nueva versión de pg_trgm, 1.2, que será mucho mejor al respecto. Con un poco de esfuerzo, también puede hacer que esta nueva versión funcione en PostgreSQL 9.4 (debe aplicar el parche, compilar el módulo de extensión usted mismo e instalarlo).

Lo que hace la versión más antigua es buscar cada trigrama en la consulta y tomar la unión de ellos, y luego aplicar un filtro. Lo que hará la nueva versión es elegir el trigrama más raro en la consulta y buscar solo ese, y luego filtrar el resto más tarde.

La maquinaria para hacer esto no existe en 9.1. En 9.4 esa maquinaria fue agregada, pero pg_trgm no estaba adaptada para usarla en ese momento.

Aún tendría un posible problema de DOS, ya que la persona malintencionada puede elaborar una consulta que solo tenga trigramas comunes. como '% y%', o incluso '% a%'


Si no puede actualizar a pg_trgm 1.2, entonces otra forma de engañar al planificador sería:

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

Al concatenar la cadena vacía para etiquetar, engaña al planificador para que piense que no puede usar el índice en esa parte de la cláusula where. Por lo tanto, utiliza el índice solo en% someword% y aplica un filtro solo a esas filas.


Además, si siempre está buscando palabras enteras, podría usar una función para simbolizar la cadena en una matriz de palabras, y usar un índice GIN incorporado regular (no pg_trgm) en esa función de retorno de matriz.


13
Vale la pena mencionar que fuiste tú quien escribió el parche. Y las pruebas preliminares de rendimiento son impresionantes. Esto realmente merece más upvotes (también para la explicación y solución con la versión actual).
Erwin Brandstetter

Yo estaría más interesado en por lo menos una referencia a la maquinaria que utilizó para implementar el parche que no estaba en el punto 9.1. Pero, estoy de acuerdo con la respuesta de Erwin bad ass.
Evan Carroll

3

He encontrado una forma de estafa el planificador de consulta, es un truco muy 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 salida:

Bitmap Heap Escanear en la tabla1 (coste = 6749.11..7332.71 filas = 1 width = 212) (tiempo real = 256.607..256.609 filas = 1 bucles = 1)
  Vuelva a verificar Cond: (lower (unaccent ((label_hun) :: text)) ~~ '% someword%' :: text)
  Filtro: (lower (lower (unaccent ((label) :: text))) ~~ '% someword y algo más%' :: text)
  -> Escaneo de índice de mapa de bits en table1_label_hun_gin_trgm (costo = 0.00..6749.11 filas = 147 ancho = 0) (tiempo real = 256.499..256.499 filas = 1 bucles = 1)
        Índice Cond: (lower (unaccent ((label) :: text)) ~~ '% someword%' :: text)
Tiempo de ejecución total: 256.653 ms

Entonces, como no hay índice lower(lower(unaccent(label))), esto crearía un escaneo secuencial, por lo que se convertirá en un filtro simple. Lo que es más, un simple y también hará lo mismo:

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

Por supuesto, esta es una heurística que puede no funcionar bien, si la parte recortada utilizada en la exploración del índice es muy común. Sin embargo, en nuestra base de datos, no hay realmente mucho la repetición, si utilizo unos 10-15 caracteres.

Quedan dos preguntas pequeñas:

  • ¿Por qué los postgres no pueden darse cuenta de que algo como esto sería beneficioso?
  • ¿Qué hace Postgres en el rango de tiempo 0..256.499 (ver salida de análisis)?

1
En el rango de tiempo entre 0 y 256.499 está construyendo el mapa de bits. A 256.499 produce su primera salida, que es el mapa de bits. Que también es su última salida, ya que solo produce una única salida: un único mapa de bits completado.
jjanes
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.