Por que o CTE é muito pior do que subconsultas em linha

11

Estou tentando entender melhor como o planejador de consultas funciona no postgresql.

Eu tenho 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

Ele roda em menos de 10 ms no meu banco de dados, com cerca de 500 mil entradas na tabela de usuários.

Então, pensei que, para evitar as sub-seleções duplicadas, eu poderia reescrever a consulta como um CTE, assim:

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;

No entanto, essa consulta reescrita é executada em cerca de 1 segundo! Por que isso acontece? Vejo no explica que ele não usa o índice de geometria, mas algo pode ser feito para isso? Obrigado!

Outra maneira de escrever a consulta é:

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;

No entanto, isso também será tão lento quanto o CTE.

Se, por outro lado, extraio os parâmetros me e os insiro estaticamente, a consulta é rápida novamente:

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

Explicar a primeira 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)

Explique a 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
fonte
3
Eu escrevi sobre isso recentemente; consulte blog.2ndquadrant.com/postgresql-ctes-are-optimization-fences . Embora atualmente haja alguns problemas de DNS que possam limitar a acessibilidade desse site. Tente uma subconsulta em FROMvez do termo CTE para obter melhores resultados.
Craig Ringer
e se você usar (select id, latest_location from users where id = 2)como o cte? Talvez seja o * que está causando esse problema
Cha cha
Eu teria pensado que você iria olhar para os usuários mais próximos do sexo oposto :)
cha
@cha Não faz diferença na velocidade para selecionar apenas sexo e localização no cte. (No meu caso eu quero tirar a média de usuários semelhantes, só que eu simplificado a consulta para a pergunta)
viblo
@ CraigRinger Eu não acho que é a cerca de otimização. Eu também tentei sua sugestão e também foi lenta. Por outro lado, se eu extrair os parâmetros manualmente, é rápido (e é uma opção real no meu caso, o resultado final é uma função).
viblo

Respostas:

11

Tente o seguinte:

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;

Quando olho para o plano rápido, eis o que salta para mim (em negrito):

 Limite (custo = 5,69 a 20,11 linhas = 50 de largura = 36) (tempo real = 0,512 a 8,114 linhas = 50 loops = 1)
   InitPlan 1 ( retorna $ 0 )
     -> Varredura de índice usando users_pkey nos usuários users_1 (custo = 0,42..2,64 linhas = 1 largura = 32) (tempo real = 0,032..0,033 linhas = 1 loops = 1)
           Cond do índice: (id = 2)
   InitPlan 2 ( retorna $ 1 )
     -> Varredura de índice usando users_pkey nos usuários users_2 (custo = 0,42..2,64 linhas = 1 largura = 4) (tempo real = 0,009..0,010 linhas = 1 loops = 1)
           Cond do índice: (id = 2)
   -> Varredura de índice usando users_latest_location_gix nos usuários (custo = 0,41..70796,51 linhas = 245470 largura = 36) (tempo real = 0,509..8.100 linhas = 50 loops = 1)
         Ordenar por: (latest_location   $ 0 )
         Filtro: (sexo = R $ 1 )
         Linhas removidas pelo filtro: 20
 Tempo de execução total: 8.211 ms
(12 linhas)

Na versão lenta, o planejador de consultas está avaliando o operador de igualdade gendere o operador de geometria latest_locationno contexto de uma junção , onde o valor de mepode variar com cada linha (mesmo que tenha estimado corretamente apenas 1 linha). Na versão rápida, os valores de gendere latest_locationsão tratados como escalares porque são emitidos por subconsultas em linha, o que indica ao planejador de consultas que ele possui apenas um valor para cada um. Esse é o mesmo motivo pelo qual você obtém o plano rápido ao colar os valores literais.

Noah Yetter
fonte
Eu acho que você pode remover meda fromcláusula agora.
Jarius Hebzo 01/08/1918