14.1. Uso do comando EXPLAIN #

14.1.1. EXPLAIN básico
14.1.2. EXPLAIN ANALYZE
14.1.3. Advertências

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).

14.1.1. 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 = constante. (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 t2.) 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 (subplan_name).colN, que se refere à N-é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.

14.1.2. 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.

14.1.3. Advertências #

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.