sql >> Base de Datos >  >> RDS >> PostgreSQL

¿Por qué un ligero cambio en el término de búsqueda ralentiza tanto la consulta?

¿Por qué?

La razón es esto:

Consulta rápida:

->  Hash Left Join  (cost=1378.60..2467.48 rows=15 width=79) (actual time=41.759..85.037 rows=1129 loops=1)
      ...
      Filter: (unaccent(((((COALESCE(p.abrev, ''::character varying))::text || ' ('::text) || (COALESCE(p.prenome, ''::character varying))::text) || ')'::text)) ~~* (...)

Consulta lenta:

->  Hash Left Join  (cost=1378.60..2467.48 rows=1 width=79) (actual time=35.084..80.209 rows=1129 loops=1)
      ...
      Filter: (unaccent(((((COALESCE(p.abrev, ''::character varying))::text || ' ('::text) || (COALESCE(p.prenome, ''::character varying))::text) || ')'::text)) ~~* unacc (...)

Extender el patrón de búsqueda por otro carácter hace que Postgres asuma aún menos aciertos. (Por lo general, esta es una estimación razonable). Obviamente, Postgres no tiene estadísticas lo suficientemente precisas (ninguna, en realidad, siga leyendo) para esperar la misma cantidad de visitas que realmente obtiene.

Esto provoca un cambio a un plan de consulta diferente, que es aún menos óptimo para el real número de resultados rows=1129 .

Solución

Suponiendo que el Postgres 9.5 actual ya que no ha sido declarado.

Una forma de mejorar la situación es crear un índice de expresión en la expresión del predicado. Esto hace que Postgres recopile estadísticas para la expresión real, lo que puede ayudar a la consulta incluso si el índice en sí no se usa para la consulta . Sin el índice, no hay estadísticas para la expresión en absoluto. Y si se hace bien, el índice se puede usar para la consulta, eso es incluso mucho mejor. Pero hay múltiples problemas con su expresión actual:

unaccent(TEXT(coalesce(p.abrev,'')||' ('||coalesce(p.prenome,'')||')')) ilike unaccent('%vicen%')

Considere esta consulta actualizada, basada en algunas suposiciones sobre sus definiciones de tablas no reveladas:

SELECT e.id
     , (SELECT count(*) FROM imgitem
        WHERE tabid = e.id AND tab = 'esp') AS imgs -- count(*) is faster
     , e.ano, e.mes, e.dia
     , e.ano::text || to_char(e.mes2, 'FM"-"00')
                   || to_char(e.dia,  'FM"-"00') AS data    
     , pl.pltag, e.inpa, e.det, d.ano anodet
     , format('%s (%s)', p.abrev, p.prenome) AS determinador
     , d.tax
     , coalesce(v.val,v.valf)   || ' ' || vu.unit  AS altura
     , coalesce(v1.val,v1.valf) || ' ' || vu1.unit AS dap
     , d.fam, tf.nome família, d.gen, tg.nome AS gênero, d.sp
     , ts.nome AS espécie, d.inf, e.loc, l.nome localidade, e.lat, e.lon
FROM      pess    p                        -- reorder!
JOIN      det     d   ON d.detby   = p.id  -- INNER JOIN !
LEFT JOIN tax     tf  ON tf.oldfam = d.fam
LEFT JOIN tax     tg  ON tg.oldgen = d.gen
LEFT JOIN tax     ts  ON ts.oldsp  = d.sp
LEFT JOIN tax     ti  ON ti.oldinf = d.inf  -- unused, see @joop's comment
LEFT JOIN esp     e   ON e.det     = d.id
LEFT JOIN loc     l   ON l.id      = e.loc
LEFT JOIN var     v   ON v.esp     = e.id AND v.key  = 265
LEFT JOIN varunit vu  ON vu.id     = v.unit
LEFT JOIN var     v1  ON v1.esp    = e.id AND v1.key = 264
LEFT JOIN varunit vu1 ON vu1.id    = v1.unit
LEFT JOIN pl          ON pl.id     = e.pl
WHERE f_unaccent(p.abrev)   ILIKE f_unaccent('%' || 'vicenti' || '%') OR
      f_unaccent(p.prenome) ILIKE f_unaccent('%' || 'vicenti' || '%');

Puntos principales

¿Por qué f_unaccent() ? Porque unaccent() no se puede indexar. Lee esto:

Utilicé la función descrita allí para permitir el siguiente (¡recomendado!) Trigrama funcional de varias columnas GIN índice :

CREATE INDEX pess_unaccent_nome_trgm_idx ON pess
USING gin (f_unaccent(pess) gin_trgm_ops, f_unaccent(prenome) gin_trgm_ops);

Si no está familiarizado con los índices de trigramas, lea esto primero:

Y posiblemente:

Asegúrese de ejecutar la última versión de Postgres (actualmente 9.5). Ha habido mejoras sustanciales en los índices GIN. Y le interesarán las mejoras en pg_trgm 1.2, cuyo lanzamiento está programado para el próximo Postgres 9.6:

Declaraciones preparadas son una forma común de ejecutar consultas con parámetros (especialmente con texto de la entrada del usuario). Postgres tiene que encontrar un plan que funcione mejor para cualquier parámetro dado. Agregar comodines como constantes al término de búsqueda como este:

f_unaccent(p.abrev) ILIKE f_unaccent('%' || 'vicenti' || '%')

('vicenti' sería reemplazado con un parámetro). Entonces, Postgres sabe que estamos tratando con un patrón que no está anclado ni a la izquierda ni a la derecha, lo que permitiría diferentes estrategias. Respuesta relacionada con más detalles:

O tal vez vuelva a planificar la consulta para cada término de búsqueda (posiblemente usando SQL dinámico en una función). Pero asegúrese de que el tiempo de planificación no consuma ninguna posible ganancia de rendimiento.

El WHERE condición en columnas en pess contradice LEFT JOIN . Postgres se ve obligado a convertir eso en INNER JOIN . Lo que es peor, la unión llega tarde en el árbol de unión. Y dado que Postgres no puede reordenar sus uniones (ver más abajo), eso puede resultar muy costoso. Mover la tabla al primero posición en el FROM cláusula para eliminar filas antes de tiempo. Siguiendo LEFT JOIN s no elimina ninguna fila por definición. Pero con tantas tablas, es importante mover uniones que podrían multiplicar filas hasta el final.

Estás uniendo 13 mesas, 12 de ellas con LEFT JOIN lo que deja 12! posibles combinaciones - o 11! * 2! si tomamos el LEFT JOIN en cuenta que es realmente un INNER JOIN . Eso es demasiado muchos para que Postgres evalúe todas las permutaciones posibles para el mejor plan de consulta. Lee sobre join_collapse_limit :

La configuración predeterminada para join_collapse_limit es 8 , lo que significa que Postgres no intentará reordenar las tablas en su FROM y el orden de las tablas es relevante .

Una forma de solucionar esto sería dividir la parte crítica del rendimiento en un CET me gusta -tanto?noredirect=1#comment60339150_36343860">@joop comentó . No establezca join_collapse_limit mucho más alto o los tiempos para la planificación de consultas que involucran muchas tablas unidas se deteriorarán.

Acerca de su fecha concatenada llamado data :

cast(cast(e.ano as varchar(4))||'-'||right('0'||cast(e.mes as varchar(2)),2)||'-'|| right('0'||cast(e.dia as varchar(2)),2) as varchar(10)) as data

Asumiendo construye a partir de tres columnas numéricas para el año, el mes y el día, que se definen como NOT NULL , usa esto en su lugar:

e.ano::text || to_char(e.mes2, 'FM"-"00')
            || to_char(e.dia,  'FM"-"00') AS data

Sobre la FM modificador de patrón de plantilla:

Pero realmente, debe almacenar la fecha como tipo de datos date para empezar.

También simplificado:

format('%s (%s)', p.abrev, p.prenome) AS determinador

No hará que la consulta sea más rápida, pero es mucho más limpia. Consulte format() .

Lo primero es lo último, todos los consejos habituales para la optimización del rendimiento aplica:

Si hace todo esto bien, debería ver consultas mucho más rápidas para todos patrones.