¿Por qué el CTE es mucho peor que las subconsultas en línea?

11

Estoy tratando de entender mejor cómo funciona el planificador de consultas en postgresql.

Tengo esta consulta:

select id from users 
    where id <> 2
    and gender = (select gender from users where id = 2)
    order by latest_location::geometry <-> (select latest_location from users where id = 2) ASC
    limit 50

Se ejecuta en menos de 10 ms en mi base de datos con alrededor de 500k entradas en la tabla de usuarios.

Luego pensé que para evitar las subselecciones duplicadas podría reescribir la consulta como CTE, de esta manera:

with me as (
    select * from users where id = 2
)
select u.id, u.popularity from users u, me 
    where u.gender = me.gender
    order by  u.latest_location::geometry <-> me.latest_location::geometry ASC
    limit 50;

Sin embargo, esta consulta reescrita se ejecuta en alrededor de 1 segundo. ¿Por qué pasó esto? Veo en las explicaciones que no usa el índice de geometría, pero ¿se puede hacer algo para eso? ¡Gracias!

Otra forma de escribir la consulta es:

select u.id, u.popularity from users u, (select gender, latest_location from users where id = 2) as me 
    where u.gender = me.gender
    order by  u.latest_location::geometry <-> me.latest_location::geometry ASC
    limit 50;

Sin embargo, esto también será tan lento como el CTE.

Si, por otro lado, extraigo los parámetros me y los inserto estáticamente, la consulta vuelve a ser rápida:

select u.id, u.popularity from users u
    where u.gender = 'male'
    order by  u.latest_location::geometry <-> '0101000000A49DE61DA71C5A403D0AD7A370F54340'::geometry ASC
    limit 50;

Explicar la primera consulta (rápida)

 Limit  (cost=5.69..20.11 rows=50 width=36) (actual time=0.512..8.114 rows=50 loops=1)
   InitPlan 1 (returns $0)
     ->  Index Scan using users_pkey on users users_1  (cost=0.42..2.64 rows=1 width=32) (actual time=0.032..0.033 rows=1 loops=1)
           Index Cond: (id = 2)
   InitPlan 2 (returns $1)
     ->  Index Scan using users_pkey on users users_2  (cost=0.42..2.64 rows=1 width=4) (actual time=0.009..0.010 rows=1 loops=1)
           Index Cond: (id = 2)
   ->  Index Scan using users_latest_location_gix on users  (cost=0.41..70796.51 rows=245470 width=36) (actual time=0.509..8.100 rows=50 loops=1)
         Order By: (latest_location <-> $0)
         Filter: (gender = $1)
         Rows Removed by Filter: 20
 Total runtime: 8.211 ms
(12 rows)

Explicar la segunda consulta (lenta)

Limit  (cost=62419.82..62419.95 rows=50 width=76) (actual time=1024.963..1024.970 rows=50 loops=1)
   CTE me
     ->  Index Scan using users_pkey on users  (cost=0.42..2.64 rows=1 width=221) (actual time=0.037..0.038 rows=1 loops=1)
           Index Cond: (id = 2)
   ->  Sort  (cost=62417.18..63030.86 rows=245470 width=76) (actual time=1024.959..1024.963 rows=50 loops=1)
         Sort Key: ((u.latest_location <-> me.latest_location))
         Sort Method: top-N heapsort  Memory: 28kB
         ->  Hash Join  (cost=0.03..54262.85 rows=245470 width=76) (actual time=0.122..938.131 rows=288646 loops=1)
               Hash Cond: (u.gender = me.gender)
               ->  Seq Scan on users u  (cost=0.00..49353.41 rows=490941 width=48) (actual time=0.021..465.025 rows=490994 loops=1)
               ->  Hash  (cost=0.02..0.02 rows=1 width=36) (actual time=0.054..0.054 rows=1 loops=1)
                     Buckets: 1024  Batches: 1  Memory Usage: 1kB
                     ->  CTE Scan on me  (cost=0.00..0.02 rows=1 width=36) (actual time=0.047..0.049 rows=1 loops=1)
 Total runtime: 1025.096 ms
viblo
fuente
3
Escribí sobre esto recientemente; ver blog.2ndquadrant.com/postgresql-ctes-are-optimization-fences . Aunque actualmente hay algunos problemas de DNS que pueden limitar el alcance de ese sitio. Pruebe una subconsulta en FROMlugar del término CTE para obtener mejores resultados.
Craig Ringer
¿Qué (select id, latest_location from users where id = 2)pasa si lo usa como el CTE? Tal vez sea el * el que está causando este problema
cha
Pensé que buscarías a los usuarios más cercanos de género opuesto :)
cha
@cha No hace ninguna diferencia en la velocidad para simplemente seleccionar el género y la ubicación en el cte. (En mi caso, quiero tomar el promedio de usuarios similares, solo que simplifiqué la consulta para la pregunta)
viblo
@CraigRinger No creo que sea la valla de optimización. También probé tu sugerencia y también fue lenta. Por otro lado, si extraigo los parámetros manualmente, es rápido (y en mi caso es una opción real, el resultado final es una función de todos modos).
viblo

Respuestas:

11

Prueba esto:

with me as (
    select * from users where id = 2
)
select u.id, u.popularity from users u, me 
    where u.gender = (select gender from me)
    order by  u.latest_location::geometry <-> (select latest_location from me)::geometry ASC
    limit 50;

Cuando miro el plan rápido, esto es lo que me llama la atención (en negrita):

 Límite (costo = 5.69..20.11 filas = 50 ancho = 36) (tiempo real = 0.512..8.114 filas = 50 bucles = 1)
   InitPlan 1 ( devuelve $ 0 )
     -> Escaneo de índice usando users_pkey en usuarios users_1 (costo = 0.42..2.64 filas = 1 ancho = 32) (tiempo real = 0.032..0.033 filas = 1 bucles = 1)
           Índice Cond: (id = 2)
   InitPlan 2 ( devuelve $ 1 )
     -> Escaneo de índice usando users_pkey en usuarios users_2 (costo = 0.42..2.64 filas = 1 ancho = 4) (tiempo real = 0.009..0.010 filas = 1 bucles = 1)
           Índice Cond: (id = 2)
   -> Escaneo de índice utilizando users_latest_location_gix en usuarios (costo = 0.41..70796.51 filas = 245470 ancho = 36) (tiempo real = 0.509..8.100 filas = 50 bucles = 1)
         Ordenar por: (Latest_location   $ 0 )
         Filtro: (género = $ 1 )
         Filas eliminadas por filtro: 20
 Tiempo de ejecución total: 8.211 ms
(12 filas)

En la versión lenta, el planificador de consultas está evaluando el operador de igualdad gendery el operador de geometría latest_locationen el contexto de una unión , donde el valor de mepodría variar con cada fila (a pesar de que ha estimado correctamente solo 1 fila). En la versión rápida, los valores de gendery latest_locationse tratan como escalares porque son emitidos por subconsultas en línea, lo que le dice al planificador de consultas que solo tiene un valor de cada uno para tratar. Esta es la misma razón por la que obtiene el plan rápido cuando pega los valores literales.

Noah Yetter
fuente
Creo que puedes eliminarlo mede la fromcláusula ahora.
Jarius Hebzo