La consulta PostgreSQL es muy lenta cuando se agrega la subconsulta

10

Tengo una consulta relativamente simple en una tabla con 1.5M filas:

SELECT mtid FROM publication
WHERE mtid IN (9762715) OR last_modifier=21321
LIMIT 5000;

EXPLAIN ANALYZE salida:

Limit  (cost=8.84..12.86 rows=1 width=8) (actual time=0.985..0.986 rows=1 loops=1)
  ->  Bitmap Heap Scan on publication  (cost=8.84..12.86 rows=1 width=8) (actual time=0.984..0.985 rows=1 loops=1)
        Recheck Cond: ((mtid = 9762715) OR (last_modifier = 21321))
        ->  BitmapOr  (cost=8.84..8.84 rows=1 width=0) (actual time=0.971..0.971 rows=0 loops=1)
              ->  Bitmap Index Scan on publication_pkey  (cost=0.00..4.42 rows=1 width=0) (actual time=0.295..0.295 rows=1 loops=1)
                    Index Cond: (mtid = 9762715)
              ->  Bitmap Index Scan on publication_last_modifier_btree  (cost=0.00..4.42 rows=1 width=0) (actual time=0.674..0.674 rows=0 loops=1)
                    Index Cond: (last_modifier = 21321)
Total runtime: 1.027 ms

Hasta ahora todo bien, rápido y utiliza los índices disponibles.
Ahora, si modifico una consulta solo un poco, el resultado será:

SELECT mtid FROM publication
WHERE mtid IN (SELECT 9762715) OR last_modifier=21321
LIMIT 5000;

El EXPLAIN ANALYZEresultado es:

Limit  (cost=0.01..2347.74 rows=5000 width=8) (actual time=2735.891..2841.398 rows=1 loops=1)
  ->  Seq Scan on publication  (cost=0.01..349652.84 rows=744661 width=8) (actual time=2735.888..2841.393 rows=1 loops=1)
        Filter: ((hashed SubPlan 1) OR (last_modifier = 21321))
        SubPlan 1
          ->  Result  (cost=0.00..0.01 rows=1 width=0) (actual time=0.001..0.001 rows=1 loops=1)
Total runtime: 2841.442 ms

No tan rápido, y usando el escaneo seq ...

Por supuesto, la consulta original ejecutada por la aplicación es un poco más compleja e incluso más lenta, y por supuesto, el original generado por hibernación no lo es (SELECT 9762715), ¡pero la lentitud está ahí incluso para eso (SELECT 9762715)! La consulta es generada por hibernate, por lo que es un gran desafío cambiarlas, y algunas características no están disponibles (por ejemplo, UNIONno está disponible, lo que sería rápido).

Las preguntas

  1. ¿Por qué no se puede usar el índice en el segundo caso? ¿Cómo podrían ser utilizados?
  2. ¿Puedo mejorar el rendimiento de las consultas de otra manera?

Pensamientos adicionales

Parece que podríamos usar el primer caso haciendo un SELECT manualmente y luego colocando la lista resultante en la consulta. Incluso con 5000 números en la lista IN () es cuatro veces más rápido que la segunda solución. Sin embargo, parece INCORRECTO (también, podría ser 100 veces más rápido :)). Es completamente incomprensible por qué el planificador de consultas utiliza un método completamente diferente para estas dos consultas, por lo que me gustaría encontrar una mejor solución para este problema.

P.Péter
fuente
¿Puedes de alguna manera reescribir tu código para que hibernate genere un en JOINlugar del IN ()? Además, ¿ha publicationsido analizado recientemente?
dezso
Sí, hice tanto ANÁLISIS DE VACÍO como VACÍO COMPLETO. No hubo cambios en el rendimiento. En cuanto a la segunda, AFAIR lo intentamos y no afectó significativamente el rendimiento de la consulta.
P.Péter
1
Si Hibernate no puede generar una consulta adecuada, ¿por qué no usa SQL sin formato? Es como insistir en el traductor de Google mientras ya sabes mejor cómo expresarlo en inglés. En cuanto a su pregunta: realmente depende de la consulta real oculta detrás (SELECT 9762715).
Erwin Brandstetter
Como mencioné a continuación, es lento incluso si la consulta interna es (SELECT 9762715) . A la pregunta de hibernación: podría hacerse, pero requiere una reescritura de código seria, ya que tenemos consultas de criterios de hibernación definidas por el usuario que se traducen sobre la marcha. Así que esencialmente estaríamos modificando la hibernación, que es una gran empresa con muchos posibles efectos secundarios.
P.Péter

Respuestas:

6

El núcleo del problema se vuelve obvio aquí:

Seq Scan en la publicación (costo = 0.01..349652.84 filas = 744661 ancho = 8) (tiempo real = 2735.888..2841.393 filas = 1 bucles = 1)

Postgres estima devolver 744661 filas mientras que, de hecho, resulta ser una sola fila. Si Postgres no sabe mejor qué esperar de la consulta, no puede planificar mejor. Necesitaríamos ver la consulta real oculta detrás (SELECT 9762715), y probablemente también conocer la definición de tabla, restricciones, cardinalidades y distribución de datos. Obviamente, Postgres no es capaz de predecir qué tan pocas filas será devuelto por el mismo. Puede haber formas de reescribir la consulta, dependiendo de lo que sea .

Si sabe que la subconsulta nunca puede devolver más que nfilas, puede decirle a Postgres usando:

SELECT mtid
FROM   publication
WHERE  mtid IN (SELECT ... LIMIT n) --  OR last_modifier=21321
LIMIT  5000;

Si n es lo suficientemente pequeño, Postgres cambiará a escaneos de índice (mapa de bits). Sin embargo , eso solo funciona para el caso simple. Deja de funcionar al agregar una ORcondición: el planificador de consultas no puede hacer frente a eso actualmente.

Raramente uso IN (SELECT ...)para empezar. Por lo general, hay una mejor manera de implementar lo mismo, a menudo con una EXISTSsemiunión. A veces con un ( LEFT) JOIN( LATERAL) ...

La solución obvia sería usar UNION, pero usted lo descartó. No puedo decir más sin conocer la subconsulta real y otros detalles relevantes.

Erwin Brandstetter
fuente
2
¡No hay ninguna consulta oculta detrás (SELECT 9762715) ! Si ejecuto esa consulta exacta que ves arriba. Por supuesto, la consulta original de hibernación es un poco más complicada, pero (creo que) logré señalar dónde se extravía el planificador de consultas, así que presenté esa parte de la consulta. Sin embargo, lo anterior explica y las consultas son textualmente ctrl-cv.
P.Péter
En cuanto a la segunda parte, el límite interno no funciona: EXPLAIN ANALYZE SELECT mtid FROM publication WHERE mtid IN (SELECT 9762715 LIMIT 1) OR last_modifier=21321 LIMIT 5000;también realiza una exploración secuencial y también se ejecuta durante unos 3 segundos ...
P.Péter
@ P.Péter: Funciona para mí en mi prueba local con una subconsulta real en Postgres 9.4. Si lo que muestra es su consulta real, entonces ya tiene su solución: use la primera consulta en su pregunta con una constante en lugar de una subconsulta.
Erwin Brandstetter
Bueno, también probé una subconsulta en una nueva tabla de prueba: CREATE TABLE test (mtid bigint NOT NULL, last_modifier bigint, CONSTRAINT test_property_pkey PRIMARY KEY (mtid)); CREATE INDEX test_last_modifier_btree ON test USING btree (last_modifier); INSERT INTO test (mtid, last_modifier) SELECT mtid, last_modifier FROM publication;. Y el efecto todavía estaba ahí para las mismas consultas test: cualquier subconsulta resultó en un escaneo de secuencia ... Probé tanto 9.1 como 9.4. El efecto es el mismo.
P.Péter
1
@ P.Péter: volví a realizar la prueba y me di cuenta de que lo había hecho sin la ORcondición. El truco con LIMITsolo funciona para el caso más simple.
Erwin Brandstetter
6

Mi colega ha encontrado una manera de cambiar la consulta para que necesite una simple reescritura y haga lo que debe hacer, es decir, hacer la subselección en un paso y luego realizar las operaciones adicionales sobre el resultado:

SELECT mtid FROM publication 
WHERE 
  mtid = ANY( (SELECT ARRAY(SELECT 9762715))::bigint[] )
  OR last_modifier=21321
LIMIT 5000;

El explicar analizar ahora es:

 Limit  (cost=92.58..9442.38 rows=2478 width=8) (actual time=0.071..0.074 rows=1 loops=1)
   InitPlan 2 (returns $1)
     ->  Result  (cost=0.01..0.02 rows=1 width=0) (actual time=0.010..0.011 rows=1 loops=1)
           InitPlan 1 (returns $0)
             ->  Result  (cost=0.00..0.01 rows=1 width=0) (actual time=0.001..0.002 rows=1 loops=1)
   ->  Bitmap Heap Scan on publication  (cost=92.56..9442.36 rows=2478 width=8) (actual time=0.069..0.070 rows=1 loops=1)
         Recheck Cond: ((mtid = ANY (($1)::bigint[])) OR (last_modifier = 21321))
         Heap Blocks: exact=1
         ->  BitmapOr  (cost=92.56..92.56 rows=2478 width=0) (actual time=0.060..0.060 rows=0 loops=1)
               ->  Bitmap Index Scan on publication_pkey  (cost=0.00..44.38 rows=10 width=0) (actual time=0.046..0.046 rows=1 loops=1)
                     Index Cond: (mtid = ANY (($1)::bigint[]))
               ->  Bitmap Index Scan on publication_last_modifier_btree  (cost=0.00..46.94 rows=2468 width=0) (actual time=0.011..0.011 rows=0 loops=1)
                     Index Cond: (last_modifier = 21321)
 Planning time: 0.704 ms
 Execution time: 0.153 ms

Parece que podemos crear un analizador simple que encuentre y reescriba todas las subselecciones de esta manera, y agregarlo a un enlace de hibernación para manipular la consulta nativa.

P.Péter
fuente
Eso suena divertido. ¿No es más fácil eliminar todos los SELECTs, como lo hizo en su primera consulta en la pregunta?
dezso
Por supuesto, podría hacer un enfoque de dos pasos: hacer uno por SELECTseparado y luego hacer la selección externa con una lista estática después del IN. Sin embargo, eso es significativamente más lento (5-10 veces si la subconsulta tiene más de unos pocos resultados), ya que tiene viajes de ida y vuelta a la red adicionales además de que tiene formato de postgres muchos resultados y luego Java analiza esos resultados (y luego hace lo mismo otra vez al revés). La solución anterior hace lo mismo semánticamente, mientras deja el proceso dentro de postgres. Con todo, actualmente esta parece ser la forma más rápida con la modificación más pequeña en nuestro caso.
P.Péter
Ah, ya veo. Lo que no sabía es que puede obtener muchas identificaciones a la vez.
dezso
1

Responda a una segunda pregunta: Sí, puede agregar ORDER BY a su subconsulta, lo que tendrá un impacto positivo. Pero es similar a la solución "EXISTE (subconsulta)" en rendimiento. Hay una diferencia significativa incluso con subconsultas que resultan en dos filas.

SELECT mtid FROM publication
WHERE mtid IN (SELECT #column# ORDER BY #column#) OR last_modifier=21321
LIMIT 5000;
iki
fuente