Consulta ao PostgreSQL muito lenta quando a subconsulta foi adicionada

10

Eu tenho uma consulta relativamente simples em uma tabela com linhas de 1,5 milhão:

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

EXPLAIN ANALYZE resultado:

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

Até aí tudo bem, rápido e usa os índices disponíveis.
Agora, se eu modificar uma consulta um pouco, o resultado será:

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

A EXPLAIN ANALYZEsaída é:

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

Não é tão rápido, e usando o seq scan ...

Obviamente, a consulta original executada pelo aplicativo é um pouco mais complexa e ainda mais lenta, e é claro que o original gerado por hibernação não é (SELECT 9762715), mas a lentidão existe mesmo para isso (SELECT 9762715)! A consulta é gerada pelo hibernate, por isso é um grande desafio alterá-los e alguns recursos não estão disponíveis (por exemplo, UNIONnão estão disponíveis, o que seria rápido).

As questões

  1. Por que o índice não pode ser usado no segundo caso? Como eles poderiam ser usados?
  2. Posso melhorar o desempenho da consulta de outra maneira?

Pensamentos adicionais

Parece que poderíamos usar o primeiro caso executando manualmente um SELECT e colocando a lista resultante na consulta. Mesmo com 5000 números na lista IN (), é quatro vezes mais rápido que a segunda solução. No entanto, parece errado (também, pode ser 100 vezes mais rápido :)). É completamente incompreensível o motivo pelo qual o planejador de consultas usa um método completamente diferente para essas duas consultas. Por isso, gostaria de encontrar uma solução melhor para esse problema.

P.Péter
fonte
De alguma forma, você pode reescrever seu código para que o hibernate gere um em JOINvez de IN ()? Além disso, publicationfoi analisado recentemente?
Dezso
Sim, eu fiz o VACUUM ANALYZE e o VACUUM FULL. Não houve alteração no desempenho. Quanto ao segundo, o AFAIR tentamos isso e não afetou significativamente o desempenho da consulta.
P.Péter 7/09/15
11
Se o Hibernate falha ao gerar uma consulta adequada, por que você não usa apenas SQL bruto? É como insistir no Google tradutor enquanto você já sabe como expressá-lo em inglês. Quanto à sua pergunta: realmente depende da consulta real oculta por trás (SELECT 9762715).
Erwin Brandstetter
Como mencionei abaixo, é lento, mesmo que a consulta interna seja (SELECT 9762715) . Para a pergunta de hibernação: isso pode ser feito, mas requer uma reescrita séria de código, pois temos consultas de critérios de hibernação definidas pelo usuário que são traduzidas on-the-fly. Então, basicamente estaríamos modificando o hibernate, o que é uma tarefa enorme, com muitos efeitos colaterais possíveis.
P.Péter 7/09/15

Respostas:

6

O núcleo do problema se torna óbvio aqui:

Varredura Seq na publicação (custo = 0,01..349652,84 linhas = 744661 largura = 8) (tempo real = 2735,888..2841,393 linhas = 1 loops = 1)

O Postgres estima retornar 744661 linhas, enquanto, na verdade, acaba sendo uma única linha. Se o Postgres não souber melhor o que esperar da consulta, ele não poderá planejar melhor. Precisamos ver a consulta real oculta por trás (SELECT 9762715)- e provavelmente também conhecer a definição da tabela, restrições, cardinalidades e distribuição de dados. Obviamente, o Postgres não é capaz de prever quantas poucas linhas serão retornadas por ele. Pode haver maneiras de reescrever a consulta, dependendo do que é .

Se você sabe que a subconsulta nunca pode retornar mais do que nlinhas, basta informar ao Postgres usando:

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

Se n for pequeno o suficiente, o Postgres mudará para verificações de índice (bitmap). No entanto , isso só funciona para o caso simples. Para de trabalhar ao adicionar uma ORcondição: o planejador de consultas não pode lidar com isso no momento.

Eu raramente uso IN (SELECT ...)para começar. Normalmente, há uma maneira melhor de implementar o mesmo, geralmente com uma EXISTSsemi-junção. Às vezes com um ( LEFT) JOIN( LATERAL) ...

A solução óbvia seria usar UNION, mas você descartou isso. Não posso dizer mais sem conhecer a subconsulta real e outros detalhes relevantes.

Erwin Brandstetter
fonte
2
Não há nenhuma consulta oculta por trás (SELECT 9762715) ! Se eu executar essa consulta exata que você vê acima. Obviamente, a consulta de hibernação original é um pouco mais complicada, mas (acho que) consegui identificar onde o planejador de consultas se desvia, então apresentei essa parte da consulta. No entanto, as explicações e consultas acima são verbatim ctrl-cv.
P.Péter 7/09/2015
Quanto à segunda parte, o limite interno não funciona: EXPLAIN ANALYZE SELECT mtid FROM publication WHERE mtid IN (SELECT 9762715 LIMIT 1) OR last_modifier=21321 LIMIT 5000;também faz uma varredura seqüencial e também é executada por cerca de 3 segundos ...
P.Péter
@ P.Péter: Funciona para mim no meu teste local com uma subconsulta real no Postgres 9.4. Se o que você mostra é sua consulta real, você já tem a sua solução: Use a primeira consulta na sua pergunta com uma constante em vez de uma subconsulta.
Erwin Brandstetter
Bem, eu também tentou uma subconsulta em uma nova tabela de teste: 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;. E o efeito ainda estava lá para as mesmas consultas test: qualquer subconsulta resultou em uma verificação seq ... Eu tentei as versões 9.1 e 9.4. O efeito é o mesmo.
P.Péter 7/09/15
11
@ P.Péter: Fiz o teste novamente e percebi que havia testado sem a ORcondição. O truque LIMITsó funciona para o caso mais simples.
Erwin Brandstetter
6

Meu colega encontrou uma maneira de alterar a consulta para que ele precise de uma reescrita simples e faça o que precisa, ou seja, fazer a subseleção em uma etapa e depois realizar as operações adicionais no resultado:

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

A explicação analisar agora é:

 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 criar um analisador simples que encontre e reescreva todos os subselecionados dessa maneira e adicione-o a um gancho de hibernação para manipular a consulta nativa.

P.Péter
fonte
Isso soa engraçado. Não é mais fácil remover todos os SELECTs, como você fez na sua primeira consulta na pergunta?
Dez14
Obviamente, eu poderia fazer uma abordagem em duas etapas: fazer um SELECTseparadamente e, em seguida, fazer a seleção externa com uma lista estática após o IN. No entanto, isso é significativamente mais lento (de 5 a 10 vezes, se a subconsulta tiver mais do que alguns resultados), pois você possui viagens de ida e volta extras à rede, além de que o postgres formata muitos resultados e, em seguida, java analisa esses resultados (e executa o mesmo novamente para trás). A solução acima faz o mesmo semântica, deixando o processo dentro do postgres. Em suma, atualmente, este parece ser o caminho mais rápido, com a menor modificação no nosso caso.
P.Péter 14/09/15
Ah entendo. O que eu não sabia é que você pode obter muitos IDs de cada vez.
Dezso
1

Responda a uma segunda pergunta: Sim, você pode adicionar ORDER BY à sua subconsulta, o que terá um impacto positivo. Mas é semelhante à solução "EXISTS (subconsulta)" no desempenho. Há uma diferença significativa mesmo com a subconsulta resultando em duas linhas.

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