EXPLAIN #
O PostgreSQL elabora um
plano de consulta para cada consulta que recebe.
A escolha do plano certo para corresponder à estrutura da consulta e
às propriedades dos dados é absolutamente crítico para um bom desempenho,
portanto, o sistema inclui um planejador complexo
que tenta escolher bons planos. Pode ser usado o comando
EXPLAIN para
ver qual plano de consulta o planejador criou para qualquer consulta.
A leitura do plano é uma arte que requer alguma experiência para
dominar, mas esta seção tenta cobrir o básico.
Os exemplos nesta seção são extraídos do banco de dados de teste de
regressão após fazer um VACUUM ANALYZE,
usando os arquivos-fonte de desenvolvimento da versão 18.
Deve ser possível obter resultados semelhantes tentando os exemplos
por você mesmo, mas os custos estimados e contagens de linhas podem
variar um pouco, porque as estatísticas do ANALYZE
são amostras aleatórias em vez de exatas, e devido aos custos serem
inerentemente um pouco dependente da plataforma.
Os exemplos usam o formato de saída padrão “text” do
EXPLAIN, por ser compacto e conveniente para
leitura por humanos.
Se for desejado alimentar a saída de EXPLAIN para
um programa para análise posterior, deve ser usado um de seus
formatos de saída legíveis por máquina (XML, JSON ou YAML).
EXPLAIN básico #
A estrutura do plano de consulta é uma árvore de
nós de plano.
Os nós no nível inferior da árvore são nós de varredura:
retornam linhas brutas da tabela.
Existem diferentes tipos de nós de varredura para diferentes métodos
de acesso a tabelas: varreduras sequenciais, varreduras de índice
e varreduras de índice de bitmap.
Existem também conjuntos de linhas que não vêm de tabelas, como
cláusulas VALUES e funções que retornam conjunto
em um FROM, que possuem seus próprios tipos de
nó de varredura.
Se a consulta requerer junção, agregação, ordenação, ou outras
operações na linha bruta, então vão existir nós adicionais acima
dos nós de varredura para realizar estas operações.
Novamente, normalmente existe mais de uma forma possível de realizar
estas operações, então podem aparecer tipos de nós diferentes também.
A saída do comando EXPLAIN tem uma linha para
cada nó da árvore do plano, mostrando o tipo de nó básico mais as
estimativas de custo que o planejador fez para a execução deste nó
do plano.
Podem aparecer linhas adicionais, espaçadas da linha de resumo do nó,
para mostrar propriedades adicionais do nó.
A primeira linha (a linha de resumo para o nó mais alto) mostra o
custo total de execução estimado para o plano;
é este número que o planejador procura minimizar.
A seguir está um exemplo trivial, apenas para mostrar como a saída se parece:
EXPLAIN
SELECT *
FROM tenk1;
QUERY PLAN
--------------------------------------------------------------
Seq Scan on tenk1 (cost=0.00..445.00 rows=10000 width=244)
Como esta consulta não possui a cláusula WHERE,
ela deve varrer todas as linhas da tabela, portanto, o planejador
optou por usar um plano de varredura sequencial simples.
Os números citados entre parênteses são (da esquerda para a direita):
Custo inicial estimado. Este é o tempo gasto antes que a fase de saída possa começar, por exemplo, tempo para fazer a ordenação em um nó de ordenação.
Custo total estimado.
Isto é declarado na suposição que o nó do plano será executado
até a conclusão, ou seja, todas as linhas disponíveis serão
recuperadas. Na prática, o nó pai de um nó pode parar de ler
todas as linhas disponíveis
(veja o exemplo LIMIT abaixo).
Número estimado de saídas de linhas por este nó do plano. Novamente, supõe-se que o nó seja executado até a conclusão.
Largura média estimada das linhas geradas por este nó do plano (em bytes).
Os custos são medidos em unidades arbitrárias determinadas pelos
parâmetros de custo do planejador
(veja Constantes de custo do planejador).
A prática tradicional é medir os custos em unidades de buscas de
páginas de disco; ou seja, seq_page_cost
é convencionalmente definido como 1.0 e os demais
parâmetros de custo são definidos em relação a isto.
Os exemplos nesta seção são executados com os parâmetros de custo
padrão.
É importante entender que o custo de um nó de nível superior inclui o custo de todos os seus nós filhos. Também é importante perceber que o custo reflete apenas os aspectos que o planejador considera importantes. Em particular, o custo não considera o tempo gasto para converter os valores de saída em formato de texto ou para transmiti-los ao cliente, o que pode ser um fator importante no tempo real decorrido; mas o planejador ignora estes custos porque não pode alterá-los modificando o plano. (Acreditamos que todo plano correto produzirá o mesmo conjunto de linhas.)
O valor rows (linhas) é um pouco
complicado, porque não é o número de linhas processadas ou varridas
pelo nó do plano, mas sim o número emitido pelo nó.
Geralmente é menor que o número verificado, como resultado da
filtragem por qualquer condição da cláusula WHERE
que está sendo aplicada no nó.
Idealmente, a estimativa de linhas de nível superior serão uma
aproximação do número de linhas realmente retornadas, atualizadas
ou excluídas pela consulta.
Voltando ao nosso exemplo:
EXPLAIN
SELECT *
FROM tenk1;
QUERY PLAN
--------------------------------------------------------------
Seq Scan on tenk1 (cost=0.00..445.00 rows=10000 width=244)
Estes números são derivados de forma muito direta. Se fizermos:
SELECT relpages, reltuples FROM pg_class WHERE relname = 'tenk1';
descobriremos que tenk1 tem 345 páginas de
disco e 10.000 linhas.
O custo estimado é calculado como (páginas de disco lidas *
seq_page_cost) + (linhas varridas *
cpu_tuple_cost).
Por padrão, seq_page_cost tem o valor 1.0, e
cpu_tuple_cost tem o valor 0.01,
então o custo estimado é (345 * 1.0) + (10000 * 0.01) = 445.
Agora vamos modificar a consulta para adicionar uma condição
WHERE:
EXPLAIN
SELECT *
FROM tenk1
WHERE unique1 < 7000;
QUERY PLAN
------------------------------------------------------------
Seq Scan on tenk1 (cost=0.00..470.00 rows=7000 width=244)
Filter: (unique1 < 7000)
Note que a saída do comando EXPLAIN mostra a
cláusula WHERE sendo aplicada como uma condição
Filter (filtro) anexada ao nó do plano
Seq Scan (varredura sequencial),
significando que o nó do plano verifica a condição para cada linha
que percorre, e considera apenas as que passam na condição.
A estimativa de linhas de saída foi reduzida devido à cláusula
WHERE.
No entanto, a verificação ainda terá que visitar todas as 10.000
linhas, portanto, o custo não diminuiu; na verdade, subiu um pouco
(por 10000 * cpu_operator_cost, para ser exato)
para refletir o tempo extra de processamento gasto analisando a
condição WHERE.
O número real de linhas que esta consulta selecionaria é 7.000,
mas a estimativa para rows (linhas)
é apenas aproximada.
Se tentarmos duplicar este experimento, poderemos obter uma
estimativa ligeiramente diferente; além disso, a estimativa pode
mudar após cada comando ANALYZE, porque as
estatísticas produzidas pelo ANALYZE
são obtidas de uma amostra aleatória da tabela.
Agora, vamos tornar a condição mais restritiva:
EXPLAIN
SELECT *
FROM tenk1
WHERE unique1 < 100;
QUERY PLAN
----------------------------------------------------------------
Bitmap Heap Scan on tenk1 ↵
(cost=5.06..224.98 rows=100 width=244)
Recheck Cond: (unique1 < 100)
-> Bitmap Index Scan on tenk1_unique1 ↵
(cost=0.00..5.04 rows=100 width=0)
Index Cond: (unique1 < 100)
Aqui o planejador decidiu usar um plano de duas etapas:
o nó do plano filho visita um índice para encontrar as localizações
das linhas que correspondem à condição do índice e, em seguida,
o nó do plano superior procura estas linhas na própria tabela.
Buscar as linhas em separado é muito mais caro do que lê-las
sequencialmente, mas como nem todas as páginas da tabela precisam
ser acessadas, isto ainda sai mais econômico que a varredura sequencial.
(A razão para usar um plano com dois níveis, é que o nó do plano
superior ordena os locais das linhas identificados pelo índice em ordem
física antes de lê-los, para minimizar o custo das buscas separadas.
O bitmap mencionado nos nomes dos nós é o
mecanismo que faz a ordenação.)
Agora vamos adicionar outra condição à cláusula WHERE:
EXPLAIN
SELECT *
FROM tenk1
WHERE unique1 < 100
AND stringu1 = 'xxx';
QUERY PLAN
----------------------------------------------------------------
Bitmap Heap Scan on tenk1 ↵
(cost=5.04..225.20 rows=1 width=244)
Recheck Cond: (unique1 < 100)
Filter: (stringu1 = 'xxx'::name)
-> Bitmap Index Scan on tenk1_unique1 ↵
(cost=0.00..5.04 rows=100 width=0)
Index Cond: (unique1 < 100)
A condição adicionada stringu1 = 'xxx' reduz a
estimativa da quantidade de linhas de saída, mas não o custo, porque
ainda temos que visitar o mesmo conjunto de linhas.
Note que a cláusula stringu1 não pode ser
aplicada como condição de índice, porque este índice está apenas na
coluna unique1.
Em vez disso, esta cláusula é aplicada como um filtro nas linhas
recuperadas pelo índice.
Assim, o custo aumentou um pouco para refletir esta verificação extra.
A condição adicional stringu1 = 'xxx' reduz a
estimativa da quantidade de linhas de saída, mas não o custo, porque
ainda precisamos visitar o mesmo conjunto de linhas.
Isto ocorre porque a cláusula stringu1 não pode
ser aplicada como uma condição de índice, já que este índice está
presente apenas na coluna unique1.
Em vez disto, ela é aplicada como um filtro nas linhas recuperadas
usando o índice. Portanto, o custo aumentou ligeiramente para
refletir esta verificação extra.
Em alguns casos, o planejador vai preferir um plano de varredura de índice “simples”:
EXPLAIN
SELECT *
FROM tenk1
WHERE unique1 = 42;
QUERY PLAN
----------------------------------------------------------------
Index Scan using tenk1_unique1 on tenk1 ↵
(cost=0.29..8.30 rows=1 width=244)
Index Cond: (unique1 = 42)
Neste tipo de plano, as linhas da tabela são buscadas na ordem do
índice, o que as torna ainda mais caras para leitura, mas são tão
poucas que o custo extra para ordenar as localizações das linhas
não compensa.
Você verá este tipo de plano com mais frequência para consultas
que buscam apenas uma única linha.
Também é usado frequentemente para consultas com uma condição
ORDER BY que corresponde à ordem do índice,
porque nenhuma etapa de ordenação extra é necessária para
satisfazer o ORDER BY.
Neste exemplo, adicionar ORDER BY unique1 usaria
o mesmo plano, porque o índice já fornece implicitamente a
ordenação solicitada.
O planejador pode implementar uma cláusula ORDER BY
de várias maneiras.
O exemplo acima mostra que esta cláusula de ordenação pode ser
implementada implicitamente.
O planejador também pode adicionar uma etapa de
Sort (classificação) explícita:
EXPLAIN
SELECT *
FROM tenk1
ORDER BY unique1;
QUERY PLAN
----------------------------------------------------------------------
Sort (cost=1109.39..1134.39 rows=10000 width=244)
Sort Key: unique1
-> Seq Scan on tenk1 (cost=0.00..445.00 rows=10000 width=244)
Se uma parte do plano garante a ordenação em um prefixo das chaves de ordenação necessárias, então o planejador pode decidir usar uma etapa Incremental Sort (ordenação incremental):
EXPLAIN
SELECT * F
ROM tenk1
ORDER BY hundred, ten
LIMIT 100;
QUERY PLAN
----------------------------------------------------------------
Limit (cost=19.35..39.49 rows=100 width=244)
-> Incremental Sort ↵
(cost=19.35..2033.39 rows=10000 width=244)
Sort Key: hundred, ten
Presorted Key: hundred
-> Index Scan using tenk1_hundred on tenk1 ↵
(cost=0.29..1574.20 rows=10000 width=244)
Em comparação com as ordenações normais, a ordenação incremental
permite o retorno de tuplas antes que todo o conjunto de resultados
seja ordenado, permitindo otimizações em consultas usando
LIMIT.
Também pode reduzir o uso de memória e a probabilidade de enviar
ordenações para o disco, mas tem um custo: a sobrecarga de distribuir
o conjunto de linhas de resultado em vários lotes de ordenação.
Havendo índices distintos em várias colunas referenciadas pela
cláusula WHERE, o planejador pode optar por usar
uma combinação AND ou OR
destes índices:
EXPLAIN
SELECT *
FROM tenk1
WHERE unique1 < 100
AND unique2 > 9000;
QUERY PLAN
----------------------------------------------------------------
Bitmap Heap Scan on tenk1 ↵
(cost=25.07..60.11 rows=10 width=244)
Recheck Cond: ((unique1 < 100) AND (unique2 > 9000))
-> BitmapAnd ↵
(cost=25.07..25.07 rows=10 width=0)
-> Bitmap Index Scan on tenk1_unique1 ↵
(cost=0.00..5.04 rows=100 width=0)
Index Cond: (unique1 < 100)
-> Bitmap Index Scan on tenk1_unique2 ↵
(cost=0.00..19.78 rows=999 width=0)
Index Cond: (unique2 > 9000)
Mas isto requer visitar os dois índices, portanto, não é necessariamente uma vitória em comparação com o uso de apenas um índice e o tratamento da outra condição como um filtro. Se forem alterados os intervalos presentes na consulta, veremos que o plano muda de acordo com estes intervalos.
A seguir está um exemplo mostrando os efeitos de LIMIT:
EXPLAIN
SELECT *
FROM tenk1
WHERE unique1 < 100
AND unique2 > 9000
LIMIT 2;
QUERY PLAN
----------------------------------------------------------------
Limit (cost=0.29..14.28 rows=2 width=244)
-> Index Scan using tenk1_unique2 on tenk1 ↵
(cost=0.29..70.27 rows=10 width=244)
Index Cond: (unique2 > 9000)
Filter: (unique1 < 100)
Esta é a mesma consulta acima, com a cláusula LIMIT
adicionada para que nem todas as linhas precisem ser recuperadas
e, com isto, o planejador mudou de ideia sobre o que fazer.
Note que o custo total e a contagem de linhas do nó
Index Scan (varredura sequencial)
são mostrados como se tivessem sido executados até a conclusão.
Entretanto, espera-se que o nó Limit
(limite) pare após recuperar apenas um quinto dessas linhas, portanto,
seu custo total é apenas um quinto, e este é o custo real estimado
da consulta.
Este plano é preferível a adicionar um nó
Limit ao plano anterior, porque este
Limit não poderia evitar o custo
inicial da varredura do índice de bitmap, portanto,
o custo total seria algo acima de 25 unidades com esta abordagem.
Vamos tentar juntar duas tabelas, usando as colunas que discutimos:
EXPLAIN
SELECT *
FROM tenk1 t1, tenk2 t2
WHERE t1.unique1 < 10
AND t1.unique2 = t2.unique2;
QUERY PLAN
----------------------------------------------------------------
Nested Loop (cost=4.65..118.50 rows=10 width=488)
-> Bitmap Heap Scan on tenk1 t1 ↵
(cost=4.36..39.38 rows=10 width=244)
Recheck Cond: (unique1 < 10)
-> Bitmap Index Scan on tenk1_unique1 ↵
(cost=0.00..4.36 rows=10 width=0)
Index Cond: (unique1 < 10)
-> Index Scan using tenk2_unique2 on tenk2 t2 ↵
(cost=0.29..7.90 rows=1 width=244)
Index Cond: (unique2 = t1.unique2)
Neste plano, temos um nó de junção
Nested Loop (laço aninhado)
com duas varreduras de tabela como entradas ou filhos.
O recuo (indentação) das linhas de resumo do nó reflete a estrutura
da árvore do plano.
O primeiro filho da junção, ou “externo”, é uma
varredura de bitmap semelhante às que vimos antes.
Seu custo e contagem de linhas são os mesmos que obteríamos de
SELECT ... WHERE unique1 < 10,
porque estamos aplicando a cláusula WHERE
unique1 < 10 neste nó.
A cláusula t1.unique2 = t2.unique2 ainda não é
relevante, portanto não afeta a contagem de linhas da varredura externa.
O nó de junção de laço aninhado executará seu segundo filho, ou
“interno”, uma vez para cada linha obtida do filho externo.
Os valores das colunas da linha externa corrente podem ser conectados
à varredura interna; aqui, o valor t1.unique2 da
linha externa está disponível, então obtemos um plano e custos
semelhantes ao que vimos acima para o caso simples
SELECT ... WHERE t2.unique2 = .
(O custo estimado é, na verdade, um pouco menor do que o visto acima,
como resultado do armazenamento em cache que deve ocorrer durante as
repetidas verificações de índice em constantet2.)
Os custos do nó de laço são então definidos baseado no custo da
varredura externa, mais uma repetição da varredura interna para cada
linha externa (10 * 7.90, aqui), mais um pouco de tempo de CPU para
processamento da junção.
Neste exemplo, a contagem de linhas de saída da junção é igual ao
produto das contagens de linhas das duas varreduras, mas isto não
é verdade em todos os casos, porque pode haver cláusulas
WHERE adicionais que mencionam as duas tabelas,
portanto, podem ser aplicado apenas no ponto de junção, e não em
nenhuma varredura de entrada. A seguir está um exemplo:
EXPLAIN
SELECT *
FROM tenk1 t1, tenk2 t2
WHERE t1.unique1 < 10
AND t2.unique2 < 10
AND t1.hundred < t2.hundred;
QUERY PLAN
----------------------------------------------------------------
Nested Loop (cost=4.65..49.36 rows=33 width=488)
Join Filter: (t1.hundred < t2.hundred)
-> Bitmap Heap Scan on tenk1 t1 ↵
(cost=4.36..39.38 rows=10 width=244)
Recheck Cond: (unique1 < 10)
-> Bitmap Index Scan on tenk1_unique1 ↵
(cost=0.00..4.36 rows=10 width=0)
Index Cond: (unique1 < 10)
-> Materialize (cost=0.29..8.51 rows=10 width=244)
-> Index Scan using tenk2_unique2 on tenk2 t2 ↵
(cost=0.29..8.46 rows=10 width=244)
Index Cond: (unique2 < 10)
A condição t1.hundred < t2.hundred não pode
ser testada no índice tenk2_unique2, então é
aplicada no nó de junção.
Isto reduz a contagem de linhas de saída estimada do nó de junção,
mas não altera nenhuma varredura de entrada.
Note que aqui o planejador escolheu “materializar”
a relação interna da junção, colocando um nó de plano
Materialize (materializar) sobre ela,
significando que a varredura de índice t2 será
feita apenas uma vez, mesmo que o nó de junção de laço aninhado
precise ler estes dados dez vezes, uma vez para cada linha da
relação externa.
O nó Materialize salva os dados na
memória à medida que são lidos e, em seguida, retorna os dados
da memória em cada passagem subsequente.
Ao lidar com associações externas, podem ser vistos nós do plano de
associação com as condições Join Filter
(filtro de junção) e Filter simples
anexadas.
As condições de Join Filter vêm da
cláusula ON da junção externa, portanto, uma
linha que falhe na condição do
Join Filter ainda pode ser lançada
como uma linha estendida nula.
Mas a condição de Filter simples é
aplicada após as regras de junção externa, portanto, atua removendo
linhas incondicionalmente.
Em uma junção interna não há diferença semântica entre estes tipos
de filtros.
Se alterarmos um pouco a seletividade da consulta, podemos obter um plano de junção muito diferente:
EXPLAIN
SELECT *
FROM tenk1 t1, tenk2 t2
WHERE t1.unique1 < 100
AND t1.unique2 = t2.unique2;
QUERY PLAN
----------------------------------------------------------------
Hash Join (cost=226.23..709.73 rows=100 width=488)
Hash Cond: (t2.unique2 = t1.unique2)
-> Seq Scan on tenk2 t2 ↵
(cost=0.00..445.00 rows=10000 width=244)
-> Hash (cost=224.98..224.98 rows=100 width=244)
-> Bitmap Heap Scan on tenk1 t1 ↵
(cost=5.06..224.98 rows=100 width=244)
Recheck Cond: (unique1 < 100)
-> Bitmap Index Scan on tenk1_unique1 ↵
(cost=0.00..5.04 rows=100 width=0)
Index Cond: (unique1 < 100)
Aqui, o planejador optou por usar uma junção de hash,
onde as linhas de uma tabela são inseridas em uma tabela de
hash na memória, após a qual a outra tabela é
percorrida e a tabela de hash é testada para correspondências com
cada linha.
Novamente observe como o recuo (indentação) reflete a estrutura do plano:
a varredura de bitmap em tenk1
é a entrada para o nó Hash, que constrói a tabela
de hash.
Isto é então retornado para o nó Hash Join,
que lê as linhas de seu plano filho externo, e pesquisa a tabela de
hash para cada linha.
Outro tipo possível de junção é a junção Merge (mesclagem), mostrada aqui:
EXPLAIN
SELECT *
FROM tenk1 t1, onek t2
WHERE t1.unique1 < 100
AND t1.unique2 = t2.unique2;
QUERY PLAN
----------------------------------------------------------------
Merge Join (cost=0.56..233.49 rows=10 width=488)
Merge Cond: (t1.unique2 = t2.unique2)
-> Index Scan using tenk1_unique2 on tenk1 t1 ↵
(cost=0.29..643.28 rows=100 width=244)
Filter: (unique1 < 100)
-> Index Scan using onek_unique2 on onek t2 ↵
(cost=0.28..166.28 rows=1000 width=244)
A junção por mesclagem (intercalação) exige que os dados de entrada estejam classificados pelas chaves de junção. Neste exemplo, cada entrada é classificada usando uma varredura de índice para visitar as linhas na ordem correta; Mas também poderia ser usada uma varredura sequencial e ordenação. (Varredura sequencial e ordenação frequentemente supera uma varredura de índice para classificar muitas linhas, devido ao acesso não sequencial ao disco exigido pela varredura de índice.)
Uma maneira de ver planos diferentes é forçar o planejador a desconsiderar alguma estratégia considerada mais econômica, usando os sinalizadores de ativar/desativar descritos na Configuração do método de planejamento. (Esta é uma ferramenta grosseira, mas útil. Veja também Controle do planejador usando cláusulas JOIN explícitas.) Por exemplo, se não estivermos convencidos de que a junção por mesclagem (merge join) é o melhor tipo de junção para o exemplo anterior, poderíamos tentar
SET enable_mergejoin = off;
EXPLAIN
SELECT *
FROM tenk1 t1, onek t2
WHERE t1.unique1 < 100
AND t1.unique2 = t2.unique2;
QUERY PLAN
----------------------------------------------------------------
Hash Join (cost=226.23..344.08 rows=10 width=488)
Hash Cond: (t2.unique2 = t1.unique2)
-> Seq Scan on onek t2 ↵
(cost=0.00..114.00 rows=1000 width=244)
-> Hash (cost=224.98..224.98 rows=100 width=244)
-> Bitmap Heap Scan on tenk1 t1 ↵
(cost=5.06..224.98 rows=100 width=244)
Recheck Cond: (unique1 < 100)
-> Bitmap Index Scan on tenk1_unique1 ↵
(cost=0.00..5.04 rows=100 width=0)
Index Cond: (unique1 < 100)
que mostra que o planejador considera que a junção por
hash seria quase 50% mais cara
do que a junção por mesclagem neste caso.
Naturalmente, a próxima questão é se isto está correto.
Podemos investigar isto usando EXPLAIN ANALYZE,
conforme discutido abaixo.
Ao usar os parâmetros de ativação/desativação para desativar tipos
de nós de plano, muitos deles apenas desencorajam o uso do nó de
plano correspondente e não impedem completamente a capacidade do
planejador de usar este tipo de nó.
Isto é intencional, para que o planejador ainda mantenha a
capacidade de elaborar um plano para uma determinada consulta.
Quando o plano resultante contiver um nó desativado,
a saída do comando EXPLAIN indicará este fato.
SET enable_seqscan = off; EXPLAIN SELECT * FROM unit;
QUERY PLAN
----------------------------------------------------------
Seq Scan on unit (cost=0.00..21.30 rows=1130 width=44)
Disabled: true
Como a tabela unit não possui índices, não há
outro meio de ler os dados da tabela, portanto, a varredura
sequencial é a única opção disponível para o planejador de consultas.
Alguns planos de consulta envolvem subplanos,
que surgem de um sub-SELECT na consulta original.
Estas consultas podem, às vezes, ser transformadas em planos de
junção comuns, mas quando isto não é possível, obtemos planos como:
EXPLAIN VERBOSE
SELECT unique1
FROM tenk1 t
WHERE t.ten <
ALL (SELECT o.ten FROM onek o WHERE o.four = t.four);
QUERY PLAN
----------------------------------------------------------------
Seq Scan on public.tenk1 t ↵
(cost=0.00..586095.00 rows=5000 width=4)
Output: t.unique1
Filter: (ALL (t.ten < (SubPlan 1).col1))
SubPlan 1
-> Seq Scan on public.onek o ↵
(cost=0.00..116.50 rows=250 width=4)
Output: o.ten
Filter: (o.four = t.four)
Este exemplo bem artificial serve para mostrar alguns pontos:
os valores do plano externo podem ser passados para um subplano
(aqui t.four é passado para baixo) e os
resultados da subseleção ficam disponíveis para o plano externo.
Estes valores resultantes são mostrados pelo comando
EXPLAIN com notações como
(,
que se refere à subplan_name).colNN-ésima coluna de saída
do sub-SELECT.
No exemplo acima, o operador ALL executa o
subplano novamente para cada linha da consulta externa.
(o que causa o elevado custo estimado.).
Algumas consultas podem usar um subplano hashed
para evitar isto:
EXPLAIN
SELECT *
FROM tenk1 t
WHERE t.unique1 NOT IN
(SELECT o.unique1 FROM onek o);
QUERY PLAN
----------------------------------------------------------------
Seq Scan on tenk1 t (cost=61.77..531.77 rows=5000 width=244)
Filter: (NOT (ANY (unique1 = (hashed SubPlan 1).col1)))
SubPlan 1
-> Index Only Scan using onek_unique1 on onek o ↵
(cost=0.28..59.27 rows=1000 width=4)
(4 rows)
Aqui, o subplano é executado uma única vez e sua saída é carregada
em uma tabela de hash na memória,
que é então consultada pelo operador externo ANY.
Isto requer que o sub-SELECT não faça referência
a nenhuma variável da consulta externa,
e que o operador de comparação de ANY esteja
preparado para o hashing.
Se, além de não fazer referência a nenhuma variável da consulta
externa, a subconsulta SELECT não puder retornar
mais de uma linha, ela poderá ser implementada como um
initplan (plano de inicialização):
EXPLAIN VERBOSE
SELECT unique1
FROM tenk1 t1
WHERE t1.ten =
(SELECT (random() * 10)::integer);
QUERY PLAN
-----------------------------------------------------------------
Seq Scan on public.tenk1 t1 ↵
(cost=0.02..470.02 rows=1000 width=4)
Output: t1.unique1
Filter: (t1.ten = (InitPlan 1).col1)
InitPlan 1
-> Result (cost=0.00..0.02 rows=1 width=4)
Output: ((random() * '10'::double precision))::integer
O initplan é executado apenas uma vez por
execução do plano externo, e seus resultados são salvos para
reutilização em linhas posteriores do plano externo.
Portanto, neste exemplo, a função random()
é avaliada apenas uma vez e todos os valores de
t1.ten são comparados ao mesmo número inteiro
escolhido aleatoriamente.
Isto é bem diferente do que aconteceria sem a construção
sub-SELECT.
EXPLAIN ANALYZE #
É possível verificar a precisão das estimativas do planejador usando
a opção ANALYZE do EXPLAIN.
Com esta opção, o comando EXPLAIN executa
realmente a consulta e exibe as contagens de linhas verdadeiras
e o tempo de execução real acumulado em cada nó do plano,
com as mesmas estimativas que o
EXPLAIN simples mostra.
Por exemplo, podemos obter um resultado como este:
EXPLAIN ANALYZE
SELECT *
FROM tenk1 t1, tenk2 t2
WHERE t1.unique1 < 10
AND t1.unique2 = t2.unique2;
QUERY PLAN
----------------------------------------------------------------
Nested Loop (cost=4.65..118.50 rows=10 width=488) ↵
(actual time=0.017..0.051 rows=10.00 loops=1)
Buffers: shared hit=36 read=6
-> Bitmap Heap Scan on tenk1 t1 ↵
(cost=4.36..39.38 rows=10 width=244) ↵
(actual time=0.009..0.017 rows=10.00 loops=1)
Recheck Cond: (unique1 < 10)
Heap Blocks: exact=10
Buffers: shared hit=3 read=5 written=4
-> Bitmap Index Scan on tenk1_unique1 ↵
(cost=0.00..4.36 rows=10 width=0) ↵
(actual time=0.004..0.004 rows=10.00 loops=1)
Index Cond: (unique1 < 10)
Index Searches: 1
Buffers: shared hit=2
-> Index Scan using tenk2_unique2 on tenk2 t2 ↵
(cost=0.29..7.90 rows=1 width=244) ↵
(actual time=0.003..0.003 rows=1.00 loops=10)
Index Cond: (unique2 = t1.unique2)
Index Searches: 10
Buffers: shared hit=24 read=6
Planning:
Buffers: shared hit=15 dirtied=9
Planning Time: 0.485 ms
Execution Time: 0.073 ms
Note que os valores de actual time (tempo real) estão em milissegundos de tempo real, enquanto as estimativas de cost (custo) são expressas em unidades arbitrárias; então é improvável haver correspondência. O que é geralmente mais importante observar é se as contagens de linhas estimadas estão razoavelmente próximas da realidade. Neste exemplo, as estimativas estavam todas corretas, mas isto é bastante incomum na prática.
Em alguns planos de consulta, é possível que um nó de subplano seja
executado mais de uma vez.
Por exemplo, a varredura de índice interno será executada uma vez por
linha externa no plano de nested-loop
(laço aninhado) acima.
Nestes casos, o valor de loops informa
o número total de execuções do nó, e os valores reais de tempo e
linhas mostrados são médias por execução.
Isto é feito para tornar os números comparáveis com a forma como
as estimativas de custo são mostradas.
Multiplique pelo valor de loops para
obter o tempo total realmente gasto no nó.
No exemplo acima, gastou-se um total de 0,030 milissegundos
executando as varreduras de índice em tenk2.
Em alguns casos, EXPLAIN ANALYZE mostra
estatísticas de execução adicionais, além dos tempos de execução
do nó do plano e contagens de linhas.
Por exemplo, os nós Sort e
Hash fornecem informações extras:
EXPLAIN ANALYZE
SELECT *
FROM tenk1 t1, tenk2 t2
WHERE t1.unique1 < 100
AND t1.unique2 = t2.unique2
ORDER BY t1.fivethous;
QUERY PLAN
----------------------------------------------------------------
Sort (cost=713.05..713.30 rows=100 width=488) ↵
(actual time=2.995..3.002 rows=100.00 loops=1)
Sort Key: t1.fivethous
Sort Method: quicksort Memory: 74kB
Buffers: shared hit=440
-> Hash Join ↵
(cost=226.23..709.73 rows=100 width=488) ↵
(actual time=0.515..2.920 rows=100.00 loops=1)
Hash Cond: (t2.unique2 = t1.unique2)
Buffers: shared hit=437
-> Seq Scan on tenk2 t2 ↵
(cost=0.00..445.00 rows=10000 width=244) ↵
(actual time=0.026..1.790 rows=10000.00 loops=1)
Buffers: shared hit=345
-> Hash (cost=224.98..224.98 rows=100 width=244) ↵
(actual time=0.476..0.477 rows=100.00 loops=1)
Buckets: 1024 Batches: 1 Memory Usage: 35kB
Buffers: shared hit=92
-> Bitmap Heap Scan on tenk1 t1 ↵
(cost=5.06..224.98 rows=100 width=244) ↵
(actual time=0.030..0.450 rows=100.00 loops=1)
Recheck Cond: (unique1 < 100)
Heap Blocks: exact=90
Buffers: shared hit=92
-> Bitmap Index Scan on tenk1_unique1 ↵
(cost=0.00..5.04 rows=100 width=0) ↵
(actual time=0.013..0.013 rows=100.00 loops=1)
Index Cond: (unique1 < 100)
Index Searches: 1
Buffers: shared hit=2
Planning:
Buffers: shared hit=12
Planning Time: 0.187 ms
Execution Time: 3.036 ms
O nó Sort mostra o método de ordenação
usado (em particular, se a ordenação foi na memória ou no disco),
e a quantidade de memória ou espaço em disco necessária.
O nó Hash mostra o número de
Buckets (blocos) e
Batches (lotes) de
hash, bem como a quantidade máxima de memória
usada para a tabela de hash.
(Se o número de Batches (lotes) for
maior que um, também haverá uso de espaço em disco envolvido,
mas isto não será mostrado.)
Os nós de varredura de índice (Index Scan)
(bem como os nós de varredura de índice Bitmap
(Bitmap Index Scan) e varredura
somente de índice(Index-Only Scan))
possuem uma linha “Index Searches” que mostra o número
total de buscas realizadas em todas as
execuções/loops do nó::
EXPLAIN ANALYZE
SELECT *
FROM tenk1
WHERE thousand IN (1, 500, 700, 999);
QUERY PLAN
----------------------------------------------------------------
Bitmap Heap Scan on tenk1 ↵
(cost=9.45..73.44 rows=40 width=244) ↵
(actual time=0.012..0.028 rows=40.00 loops=1)
Recheck Cond: (thousand = ANY ('{1,500,700,999}'::integer[]))
Heap Blocks: exact=39
Buffers: shared hit=47
-> Bitmap Index Scan on tenk1_thous_tenthous ↵
(cost=0.00..9.44 rows=40 width=0) ↵
(actual time=0.009..0.009 rows=40.00 loops=1)
Index Cond: ↵
(thousand = ANY ('{1,500,700,999}'::integer[]))
Index Searches: 4
Buffers: shared hit=8
Planning Time: 0.029 ms
Execution Time: 0.034 ms
Aqui vemos um nó de varredura de índice Bitmap que precisou
de 4 pesquisas de índice separadas.
A varredura teve que pesquisar o índice a partir da página raiz
do índice tenk1_thous_tenthous uma vez para
cada valor inteiro da construção IN do predicado.
Entretanto, o número de buscas no índice geralmente não terá uma
correspondência tão simples com o predicado da consulta:
EXPLAIN ANALYZE
SELECT *
FROM tenk1
WHERE thousand IN (1, 2, 3, 4);
QUERY PLAN
----------------------------------------------------------------
Bitmap Heap Scan on tenk1 ↵
(cost=9.45..73.44 rows=40 width=244) ↵
(actual time=0.009..0.019 rows=40.00 loops=1)
Recheck Cond: (thousand = ANY ('{1,2,3,4}'::integer[]))
Heap Blocks: exact=38
Buffers: shared hit=40
-> Bitmap Index Scan on tenk1_thous_tenthous ↵
(cost=0.00..9.44 rows=40 width=0) ↵
(actual time=0.005..0.005 rows=40.00 loops=1)
Index Cond: (thousand = ANY ('{1,2,3,4}'::integer[]))
Index Searches: 1
Buffers: shared hit=2
Planning Time: 0.029 ms
Execution Time: 0.026 ms
Esta variante da nossa consulta IN realizou
apenas uma procura no índice.
O processo de procura no índice levou menos tempo (em comparação com
a consulta original), porque sua construção IN
utiliza valores que correspondem a tuplas de índice armazenadas
lado a lado, na mesma página folha do índice
tenk1_thous_tenthous.
A linha “Index Searches” também é útil com varreduras de índice B-Tree que aplicam a otimização de varredura de salto (skip scan) para percorrer o índice de forma mais eficiente:
EXPLAIN ANALYZE
SELECT four, unique1
FROM tenk1
WHERE four BETWEEN 1 AND 3 AND unique1 = 42;
QUERY PLAN
----------------------------------------------------------------
Index Only Scan using tenk1_four_unique1_idx on tenk1 ↵
(cost=0.29..6.90 rows=1 width=8) ↵
(actual time=0.006..0.007 rows=1.00 loops=1)
Index Cond: ((four >= 1) AND (four <= 3) AND (unique1 = 42))
Heap Fetches: 0
Index Searches: 3
Buffers: shared hit=7
Planning Time: 0.029 ms
Execution Time: 0.012 ms
Aqui vemos um nó de varredura somente de índice
(Index-Only Scan) usando
tenk1_four_unique1_idx, um índice
multi-colunas nas colunas four e
unique1 da tabela
tenk1.
A varredura realiza 3 buscas, cada uma lendo uma única página folha
do índice:
“four = 1 AND unique1 = 42”,
“four = 2 AND unique1 = 42” e
“four = 3 AND unique1 = 42”.
Este índice é geralmente um bom alvo para varredura de salto,
visto que, como discutido em Índices multicoluna,
sua coluna principal (a coluna four)
contém apenas 4 valores distintos, enquanto sua segunda/última coluna
(a coluna unique1) contém muitos valores
distintos.
Outro tipo de informação extra é o número de linhas removidas por uma condição de filtro:
EXPLAIN ANALYZE
SELECT *
FROM tenk1
WHERE ten < 7;
QUERY PLAN
-------------------------------------------------------------------
Seq Scan on tenk1 (cost=0.00..470.00 rows=7000 width=244) ↵
(actual time=0.030..1.995 rows=7000.00 loops=1)
Filter: (ten < 7)
Rows Removed by Filter: 3000
Buffers: shared hit=345
Planning Time: 0.102 ms
Execution Time: 2.145 ms
Estas contagens podem ser particularmente valiosas para condições de filtro aplicadas em nós de junção. A linha Rows Removed by Filter (linhas removidas pelo filtro) só aparece quando pelo menos uma linha varrida, ou um par de junção potencial no caso de um nó de junção, é rejeitado pela condição de filtro.
Um caso semelhante às condições de filtro ocorre com varreduras de índice lossy (com perdas). Por exemplo, considere esta procura por polígonos contendo um ponto específico:
EXPLAIN ANALYZE
SELECT *
FROM polygon_tbl
WHERE f1 @> polygon '(0.5,2.0)';
QUERY PLAN
----------------------------------------------------------------------
Seq Scan on polygon_tbl (cost=0.00..1.09 rows=1 width=85) ↵
(actual time=0.023..0.023 rows=0.00 loops=1)
Filter: (f1 @> '((0.5,2))'::polygon)
Rows Removed by Filter: 7
Buffers: shared hit=1
Planning Time: 0.039 ms
Execution Time: 0.033 ms
O planejador pensa (corretamente) que esta tabela de amostra é muito pequena para se preocupar com varredura de índice, então temos uma varredura sequencial simples, na qual todas as linhas foram rejeitadas pela condição de filtro. Mas se forçarmos o uso de uma varredura de índice, teremos:
SET enable_seqscan TO off;
EXPLAIN ANALYZE
SELECT *
FROM polygon_tbl
WHERE f1 @> polygon '(0.5,2.0)';
QUERY PLAN
----------------------------------------------------------------------
Index Scan using gpolygonind on polygon_tbl ↵
(cost=0.13..8.15 rows=1 width=85) ↵
(actual time=0.074..0.074 rows=0.00 loops=1)
Index Cond: (f1 @> '((0.5,2))'::polygon)
Rows Removed by Index Recheck: 1
Index Searches: 1
Buffers: shared hit=1
Planning Time: 0.039 ms
Execution Time: 0.098 ms
Aqui pode-se ver que o índice retornou uma linha candidata, rejeitada por uma nova verificação da condição do índice. Isto acontece, porque um índice GiST é lossy (com perdas) para testes de polígonos contendo: na verdade, o índice retorna as linhas com polígonos que se sobrepõem ao destino e, em seguida, temos que fazer o teste de contém exato nessas linhas.
O comando EXPLAIN tem uma opção
BUFFERS que fornece detalhes adicionais sobre
as operações de E/S realizadas durante o planejamento e a execução
da consulta em questão.
Os números de buffer exibidos mostram
a contagem de buffers não distintos
acessados, lidos, modificados e gravados para o nó em questão
e todos os seus nós filhos.
A opção ANALYZE ativa implicitamente a opção
BUFFERS.
Caso isto não seja desejado, BUFFERS pode ser
desativado explicitamente:
EXPLAIN (ANALYZE, BUFFERS OFF)
SELECT *
FROM tenk1
WHERE unique1 < 100 AND unique2 > 9000;
QUERY PLAN
----------------------------------------------------------------------
Bitmap Heap Scan on tenk1 ↵
(cost=25.07..60.11 rows=10 width=244) ↵
(actual time=0.105..0.114 rows=10.00 loops=1)
Recheck Cond: ((unique1 < 100) AND (unique2 > 9000))
Heap Blocks: exact=10
-> BitmapAnd ↵
(cost=25.07..25.07 rows=10 width=0) ↵
(actual time=0.100..0.101 rows=0.00 loops=1)
-> Bitmap Index Scan on tenk1_unique1 ↵
(cost=0.00..5.04 rows=100 width=0) ↵
(actual time=0.027..0.027 rows=100.00 loops=1)
Index Cond: (unique1 < 100)
Index Searches: 1
-> Bitmap Index Scan on tenk1_unique2 ↵
(cost=0.00..19.78 rows=999 width=0) ↵
(actual time=0.070..0.070 rows=999.00 loops=1)
Index Cond: (unique2 > 9000)
Index Searches: 1
Planning Time: 0.162 ms
Execution Time: 0.143 ms
Tenha em mente que, como EXPLAIN ANALYZE executa
realmente a consulta, quaisquer efeitos colaterais ocorrerão
normalmente, mesmo que quaisquer resultados que a consulta possa
produzir sejam descartados em favor da impressão dos dados do comando
EXPLAIN.
Se quiser analisar uma consulta de modificação de dados sem alterar
as tabelas, poderá desfazer o comando posteriormente. Por exemplo:
BEGIN; EXPLAIN ANALYZE UPDATE tenk1 SET hundred = hundred + 1 WHERE unique1 < 100;
QUERY PLAN
----------------------------------------------------------------------
Update on tenk1 ↵
(cost=5.06..225.23 rows=0 width=0) ↵
(actual time=1.634..1.635 rows=0.00 loops=1)
-> Bitmap Heap Scan on tenk1 ↵
(cost=5.06..225.23 rows=100 width=10) ↵
(actual time=0.065..0.141 rows=100.00 loops=1)
Recheck Cond: (unique1 < 100)
Heap Blocks: exact=90
Buffers: shared hit=4 read=2
-> Bitmap Index Scan on tenk1_unique1 ↵
(cost=0.00..5.04 rows=100 width=0) ↵
(actual time=0.031..0.031 rows=100.00 loops=1)
Index Cond: (unique1 < 100)
Index Searches: 1
Buffers: shared read=2
Planning Time: 0.151 ms
Execution Time: 1.856 ms
ROLLBACK;
Como visto neste exemplo, quando a consulta é um comando
INSERT, UPDATE,
DELETE ou MERGE, o trabalho
real de aplicar as alterações da tabela é feito por um nó de plano
Insert,
Update,
Delete ou
Merge de nível superior.
Os nós de plano abaixo destes nós realizam o trabalho de localizar
as linhas antigas e/ou computam os novos dados.
Então, acima, vemos o mesmo tipo de varredura de tabela de
bitmap que já vimos antes, e sua saída é
alimentada para um nó Update
que armazena as linhas atualizadas.
Vale a pena notar que, embora o nó de modificação de dados possa
levar uma quantidade considerável de tempo de execução
(aqui, está consumindo a maior parte do tempo), o planejador
atualmente não adiciona nada às estimativas de custo para
contabilizar este trabalho.
Isto se dá, porque o trabalho a ser feito é o mesmo para cada plano de
consulta correto, portanto, não afeta as decisões de planejamento.
Quando um comando UPDATE, DELETE
ou MERGE afeta uma hierarquia de herança,
a saída pode parecer assim:
EXPLAIN UPDATE gtest_parent SET f1 = CURRENT_DATE WHERE f2 = 101;
QUERY PLAN
----------------------------------------------------------------------
Update on gtest_parent (cost=0.00..3.06 rows=0 width=0)
Update on gtest_child gtest_parent_1
Update on gtest_child2 gtest_parent_2
Update on gtest_child3 gtest_parent_3
-> Append (cost=0.00..3.06 rows=3 width=14)
-> Seq Scan on gtest_child gtest_parent_1 ↵
(cost=0.00..1.01 rows=1 width=14)
Filter: (f2 = 101)
-> Seq Scan on gtest_child2 gtest_parent_2 ↵
(cost=0.00..1.01 rows=1 width=14)
Filter: (f2 = 101)
-> Seq Scan on gtest_child3 gtest_parent_3 ↵
(cost=0.00..1.01 rows=1 width=14)
Filter: (f2 = 101)
Neste exemplo, o nó Update precisa considerar três tabelas filhas, mas não a tabela particionada mencionada originalmente (uma vez que ela nunca armazena nenhum dado). Portanto, existem três subplanos de varredura de entrada, um para cada tabela. Para maior clareza, o nó Update é anotado para mostrar as tabelas de destino específicas que serão atualizadas, na mesma ordem dos subplanos correspondentes.
O Planning time mostrado no comando
EXPLAIN ANALYZE, é o tempo necessário para gerar
o plano de consulta a partir da consulta analisada e otimizá-lo.
Não inclui análise ou reescrita.
O Execution time mostrado no comando
EXPLAIN ANALYZE inclui o tempo de inicialização
e desligamento do executor, bem como o tempo para executar qualquer
gatilho que seja acionado, mas não inclui o tempo de análise,
reescrita ou planejamento.
O tempo gasto executando os gatilhos BEFORE,
caso haja algum, está incluído no tempo para o nó
Insert,
Update, ou
Delete relacionado;
mas o tempo gasto executando gatilhos AFTER não
está contado, porque os gatilhos AFTER são
acionados após a conclusão de todo o plano.
O tempo total gasto em cada gatilho (BEFORE ou
AFTER) também é mostrado em separado.
Note que os gatilhos de restrição
deferred (adiados) não serão
executados até o final da transação e, portanto, não são considerados
pelo comando EXPLAIN ANALYZE.
O tempo mostrado para o nó de nível superior não inclui o tempo
necessário para converter os dados de saída da consulta em um
formato exibível, ou para enviá-los ao cliente.
Enquanto EXPLAIN ANALYZE nunca irá enviar os
dados para o cliente; pode-se instruí-lo a converter os dados de
saída da consulta em um formato exibível e medir o tempo necessário
para isto, especificando a opção SERIALIZE.
Este tempo será mostrado separadamente e também está incluído no
Execution time total.
Existem duas maneiras significativas em que os tempos de execução
medidos pelo EXPLAIN ANALYZE podem se desviar
da execução normal da mesma consulta.
Primeiro, uma vez que nenhuma linha de saída é entregue ao cliente,
os custos de transmissão de rede não estão incluídos.
Os custos de conversão de E/S também não estão incluídos,
a menos que seja especificado SERIALIZE.
Em segundo lugar, a sobrecarga de medição adicionada pelo comando
EXPLAIN ANALYZE pode ser significativa,
principalmente em máquinas com chamadas lentas do sistema operacional
gettimeofday().
Pode ser usada a ferramenta pg_test_timing
para medir a sobrecarga de tempo no seu sistema.
Os resultados do comando EXPLAIN não devem ser
extrapolados para situações muito diferentes daquelas em que se está
testando; por exemplo, não se pode presumir que os resultados em uma
tabela do tamanho de um brinquedo se apliquem a tabelas grandes.
As estimativas de custo do planejador não são lineares, portanto,
o planejador pode escolher um plano diferente para uma tabela maior
ou menor.
Um exemplo extremo é que em uma tabela que ocupa apenas uma página
de disco, quase sempre será obtido um plano de varredura sequencial,
independentemente dos índices estarem disponíveis ou não.
O planejador percebe que será necessária uma leitura de página de
disco para processar a tabela em qualquer caso, portanto, não há
valor em gastar leituras de página adicionais para examinar um índice.
(Vimos isto acontecer no exemplo polygon_tbl acima.)
Há casos em que os valores reais e estimados não combinam bem,
mas nada está em verdade errado.
Um destes casos ocorre quando a execução do nó do plano é
interrompida pelo LIMIT ou efeito similar.
Por exemplo, na consulta com LIMIT usada anteriormente,
EXPLAIN ANALYZE
SELECT *
FROM tenk1
WHERE unique1 < 100
AND unique2 > 9000 LIMIT 2;
QUERY PLAN
----------------------------------------------------------------------
Limit (cost=0.29..14.33 rows=2 width=244) ↵
(actual time=0.051..0.071 rows=2.00 loops=1)
Buffers: shared hit=16
-> Index Scan using tenk1_unique2 on tenk1 ↵
(cost=0.29..70.50 rows=10 width=244) ↵
(actual time=0.051..0.070 rows=2.00 loops=1)
Index Cond: (unique2 > 9000)
Filter: (unique1 < 100)
Rows Removed by Filter: 287
Index Searches: 1
Buffers: shared hit=16
Planning Time: 0.077 ms
Execution Time: 0.086 ms
o custo estimado e a contagem de linhas para o nó Index Scan são mostrados como se tivessem sido executados até a conclusão. Mas, na realidade, o nó Limit parou de solicitar linhas após receber duas linhas, portanto, a contagem real de linhas é de apenas 2, e o tempo de execução é menor que a estimativa de custo sugeriria. Este não é um erro de estimativa, mas apenas uma discrepância na forma como as estimativas e os valores reais são exibidos.
As junções Merge (mesclagem) também
possuem artefatos de medição que podem confundir os incautos.
Uma junção de mesclagem para de ler uma entrada se esgotar a outra
entrada, e o próximo valor de chave em uma entrada for maior que o
último valor de chave da outra entrada;
neste caso, não pode haver mais correspondências, portanto, não há
necessidade de varrer o restante da primeira entrada.
Isto resulta em não ler tudo de um nó filho, com resultados como os
mencionados para LIMIT.
Além disso, se o filho externo (primeiro) contiver linhas com valores
de chave duplicados, será feito backup do filho interno (segundo),
e verificado novamente para a parte de suas linhas que corresponde
a este valor de chave.
O comando EXPLAIN ANALYZE conta estas emissões
repetidas das mesmas linhas internas como se fossem linhas
adicionais reais.
Quando há muitas repetições externas, a contagem real de linhas
informada para o nó do plano filho interno pode ser muito
maior do que o número de linhas que estão realmente na relação interna.
Normalmente, o comando EXPLAIN exibe cada nó de
plano criado pelo planejador.
Entretanto, há casos em que o executor pode determinar que
determinados nós não precisam ser executados, porque não podem
produzir nenhuma linha, baseado em valores de parâmetros que não
estavam disponíveis no momento do planejamento.
(No momento, isto só pode acontecer para nós filho de um nó
Append ou
MergeAppend que está analisando uma
tabela particionada.)
Quando isto acontece, estes nós de plano são omitidos da saída
do comando EXPLAIN, e uma anotação
Subplans Removed: N
aparece em seu lugar.