Índice na chave primária não usado na junção simples

16

Eu tenho as seguintes definições de tabela e índice:

CREATE TABLE munkalap (
    munkalap_id serial PRIMARY KEY,
    ...
);

CREATE TABLE munkalap_lepes (
    munkalap_lepes_id serial PRIMARY KEY,
    munkalap_id integer REFERENCES munkalap (munkalap_id),
    ...
);

CREATE INDEX idx_munkalap_lepes_munkalap_id ON munkalap_lepes (munkalap_id);

Por que nenhum dos índices em munkalap_id é usado na consulta a seguir?

EXPLAIN ANALYZE SELECT ml.* FROM munkalap m JOIN munkalap_lepes ml USING (munkalap_id);

QUERY PLAN
Hash Join  (cost=119.17..2050.88 rows=38046 width=214) (actual time=0.824..18.011 rows=38046 loops=1)
  Hash Cond: (ml.munkalap_id = m.munkalap_id)
  ->  Seq Scan on munkalap_lepes ml  (cost=0.00..1313.46 rows=38046 width=214) (actual time=0.005..4.574 rows=38046 loops=1)
  ->  Hash  (cost=78.52..78.52 rows=3252 width=4) (actual time=0.810..0.810 rows=3253 loops=1)
        Buckets: 1024  Batches: 1  Memory Usage: 115kB
        ->  Seq Scan on munkalap m  (cost=0.00..78.52 rows=3252 width=4) (actual time=0.003..0.398 rows=3253 loops=1)
Total runtime: 19.786 ms

É o mesmo, mesmo se eu adicionar um filtro:

EXPLAIN ANALYZE SELECT ml.* FROM munkalap m JOIN munkalap_lepes ml USING (munkalap_id) WHERE NOT lezarva;

QUERY PLAN
Hash Join  (cost=79.60..1545.79 rows=1006 width=214) (actual time=0.616..10.824 rows=964 loops=1)
  Hash Cond: (ml.munkalap_id = m.munkalap_id)
  ->  Seq Scan on munkalap_lepes ml  (cost=0.00..1313.46 rows=38046 width=214) (actual time=0.007..5.061 rows=38046 loops=1)
  ->  Hash  (cost=78.52..78.52 rows=86 width=4) (actual time=0.587..0.587 rows=87 loops=1)
        Buckets: 1024  Batches: 1  Memory Usage: 4kB
        ->  Seq Scan on munkalap m  (cost=0.00..78.52 rows=86 width=4) (actual time=0.014..0.560 rows=87 loops=1)
              Filter: (NOT lezarva)
Total runtime: 10.911 ms
dezso
fonte

Respostas:

21

Muitas pessoas ouviram orientações de que "as varreduras seqüenciais são ruins" e procuram eliminá-las de seus planos, mas não é tão simples. Se uma consulta cobrir todas as linhas de uma tabela, uma varredura seqüencial é a maneira mais rápida de obter essas linhas. É por isso que sua consulta de junção original usou a verificação seq, porque todas as linhas nas duas tabelas eram necessárias.

Ao planejar uma consulta, o planejador do Postgres estima os custos de várias operações (IO computacional, seqüencial e aleatória) em diferentes esquemas possíveis e escolhe o plano estimado como tendo o menor custo. Ao executar E / S a partir de armazenamento rotativo (discos), o E / S aleatório geralmente é substancialmente mais lento que o E / S sequencial, a configuração padrão da página para random_page_cost e seq_page_cost estima uma diferença de custo de 4: 1.

Essas considerações entram em jogo quando se considera um método de junção ou filtro que usa um índice versus um que varre sequencialmente uma tabela. Ao usar um índice, o plano pode encontrar uma linha rapidamente por meio do índice e, em seguida, é necessário contabilizar uma leitura aleatória do bloco para resolver os dados da linha. No caso de sua segunda consulta, que adicionou um predicado de filtragem WHERE NOT lezarva, é possível ver como isso afetou as estimativas de planejamento nos resultados EXPLAIN ANALYZE. O planejador estima 1006 linhas resultantes da junção (o que corresponde bastante ao conjunto de resultados real de 964). Como a tabela maior munkalap_lepes contém cerca de 38K linhas, o planejador vê que a junção terá que acessar cerca de 1006/38046 ou 1/38 das linhas da tabela. Ele também sabe que a largura média das linhas é 214 bytes e um bloco é 8K, então há cerca de 38 linhas / bloco.

Com essas estatísticas, o planejador considera provável que a associação precise ler todos ou a maioria dos blocos de dados da tabela. Como as pesquisas de índice também não são gratuitas e o cálculo para varrer um bloco que avalia uma condição de filtro é muito barato em relação ao IO, o planejador optou por varrer sequencialmente a tabela e evitar sobrecarga de índice e leituras aleatórias enquanto calcula a varredura seq será mais rápido.

No mundo real, os dados geralmente estão disponíveis na memória através do cache da página do SO e, portanto, nem todo bloco lido requer E / S. Pode ser bastante difícil prever a eficácia de um cache para uma determinada consulta, mas o planejador de página usa algumas heurísticas simples. O valor de configuração effective_cache_size informa as estimativas dos planejadores sobre a probabilidade de incorrer em custos reais de E / S. Um valor maior fará com que ele estime um custo mais baixo para IO aleatório e, portanto, incline-o para um método controlado por índice em uma varredura seqüencial.

dbenhur
fonte
Obrigado, esta é até agora a melhor descrição (e mais concisa) que li. Esclareceu alguns pontos-chave.
dezso
11
Excelente explicação. O cálculo das linhas / página de dados está um pouco desligado. Você deve levar em consideração o cabeçalho da página (24 bytes) + 4 bytes para cada ponteiro de item por linha + o cabeçalho da linha HeapTupleHeader(23 bytes por linha) + máscara de bit nula + alinhamento de acordo com MAXALIGN. Por fim, uma quantidade desconhecida de preenchimento devido ao alinhamento dos dados, dependendo dos tipos de dados das colunas e sua sequência. No total, não há mais de 33 linhas em uma página de 8 kb neste caso. (Não tendo BRINDE em conta.)
Erwin Brandstetter
11
@ErwinBrandstetter Obrigado por preencher cálculos de tamanho de linha mais precisos. Eu sempre assumi que a saída estimada da largura da linha por explicação incluiria considerações por linha, como o cabeçalho e a máscara de bit NULL, mas não a sobrecarga no nível da página.
dbenhur
11
@ dbenhur: Você pode executar rapidamente EXPLAIN ANALYZE SELECT foo from baruma tabela fictícia básica para verificar. Além disso, o espaço real no disco depende do alinhamento dos dados, o que seria difícil de considerar quando apenas algumas linhas são recuperadas. A largura da linha em EXPLAINrepresenta o requisito básico de espaço para o conjunto recuperado de colunas.
Erwin Brandstetter
5

Você está recuperando todas as linhas de ambas as tabelas, portanto, não há benefício real usando uma varredura de índice. Uma varredura de índice só faz sentido se você estiver selecionando apenas algumas linhas de uma tabela (geralmente menos de 10% a 15%)

um cavalo sem nome
fonte
Sim, você está certo :) Tentei esclarecer a situação com um caso mais específico, veja a última consulta.
Dezso
@dezso: A mesma coisa. Se você tiver um índice (lezarva, munkalap_id)e for seletivo o suficiente, poderá ser usado. O NOTtorna isso menos provável.
usar o seguinte código
Eu adicionei um índice parcial com base na sua sugestão e ela é usada, então metade do problema está resolvido. Mas eu não esperaria que o índice na chave estrangeira sendo inútil dado que eu quiser se juntar contra apenas 87 valores em comparação com o original 3252.
Dezso
11
@dezso As linhas têm 214 bytes de largura, então você terá um pouco menos de 40 linhas por bloco de dados de 8K. A seletividade do índice também é de 1/40 (1006/38046). Portanto, Pg calcula que ler todos os blocos seqüencialmente é mais barato que a leitura provável de aproximadamente o mesmo número de blocos aleatoriamente ao usar o índice. Esses tradeoffs estimados podem ser influenciados pelos valores de configuração effective_cache_size e random_page_cost.
dbenhur
@ dbenhur: você poderia fazer o seu comentário uma resposta adequada?
Dez20