Saída do EXPLAIN SQL Não Precisa Parecer Texto Estranho
EXPLAIN output parece que o banco de dados está te provocando. Aqui está como realmente ler planos de consulta no PostgreSQL — números de custo, tipos de nó e o que corrigir.
Você executa EXPLAIN em uma consulta lenta. O banco de dados retorna algo assim:
Hash Join (cost=145.00..578.00 rows=3200 width=96)
Hash Cond: (o.customer_id = c.id)
-> Seq Scan on orders (cost=0.00..248.00 rows=16000 width=64)
-> Hash (cost=132.50..132.50 rows=1000 width=32)
-> Seq Scan on customers (cost=0.00..132.50 rows=1000 width=32)
Filter: ((country)::text = 'US'::text)
Rows Removed by Filter: 9000
Você olha para ele por dez segundos. Você adiciona um índice aleatoriamente. Você reinicia a aplicação e espera. Isso não é uma estratégia de depuração — é superstição com mais etapas.
EXPLAIN está realmente dizendo algo específico e ação. Aqui está como lê-lo.
EXPLAIN versus EXPLAIN ANALYZE: Qual usar?
Simples EXPLAIN mostra o que o planejador intende fazer. Ele nunca executa a consulta — apenas mostra o plano com custos estimados. Rápido, mas as estimativas podem estar erradas.
EXPLAIN ANALYZE executa realmente a consulta e adiciona os tempos reais. Você obtém tanto o plano quanto os dados reais:
-- PostgreSQL: run the query and show real timings
EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM orders
JOIN customers ON orders.customer_id = customers.id
WHERE customers.country = 'US';
-- MySQL equivalent
EXPLAIN ANALYZE SELECT * FROM orders
JOIN customers ON orders.customer_id = customers.id
WHERE customers.country = 'US';
O BUFFERS opção é exclusiva do PostgreSQL e adiciona contagens de acertos e erros no buffer — útil para diagnosticar problemas de I/O, mas ignore por enquanto.
Uma advertência: EXPLAIN ANALYZE executa a consulta de fato. Em um DELETE ou UPDATE, envolva em uma transação e desfaça:
BEGIN;
EXPLAIN ANALYZE DELETE FROM orders WHERE status = 'pending';
ROLLBACK;
Os números de custo: o que eles significam e o que não significam
Cada nó no plano mostra (cost=X..Y rows=N width=W). As pessoas veem esses números e assumem que são milissegundos. Eles não são.
- custo=X..Y — X é o custo inicial (trabalho feito antes da primeira linha ser retornada), Y é o custo total para processar todas as linhas. A unidade é arbitrária "custo de página" — aproximadamente proporcional às leituras de páginas no disco, mas calibrada pelos constantes de custo do PostgreSQL. Um custo de 1,0 representa uma leitura sequencial de uma página.
- linhas=N — o número estimado de linhas pelo planejador. Pode estar totalmente errado se as estatísticas das tabelas estiverem desatualizadas.
- largura=W — tamanho médio da linha em bytes. Linhas mais largas atrasam junções e ordenações.
Quando você executa EXPLAIN ANALYZE, cada nó também recebe (actual time=X..Y rows=N loops=L). Esses são milissegundos. Uma grande diferença entre o estimado e o real é o sinal mais confiável de que o planejador tomou uma decisão errada.
Um passo a passo anotado do EXPLAIN ANALYZE
Aqui está a mesma consulta do início, desta vez com ANALYZE e com cada linha explicada:
Hash Join (cost=145.00..578.00 rows=3200 width=96)
(actual time=2.451..12.879 rows=3168 loops=1)
-- Top-level node. Joins orders and customers via hashing.
-- cost estimate: 145..578 | actual: 2.4ms startup, 12.9ms total
-- rows estimate: 3200 | actual: 3168 — pretty close here
Hash Cond: (o.customer_id = c.id)
-- This is the join key. The planner built a hash table on customers.id
-- then probed it with every row from orders.
-> Seq Scan on orders (cost=0.00..248.00 rows=16000 width=64)
(actual time=0.019..3.125 rows=16000 loops=1)
-- Full table scan on orders. 16,000 rows scanned.
-- No filter here — we pull all orders and join them below.
-- This is expected: we need all orders, so no index would help here.
-> Hash (cost=132.50..132.50 rows=1000 width=32)
(actual time=2.412..2.413 rows=1000 loops=1)
Buckets: 1024 Batches: 1 Memory Usage: 64kB
-- Builds the in-memory hash table from the customers result.
-- 1 batch = fits in work_mem. Multiple batches = spilling to disk (bad).
-> Seq Scan on customers (cost=0.00..132.50 rows=1000 width=32)
(actual time=0.012..1.345 rows=1000 loops=1)
Filter: ((country)::text = 'US'::text)
Rows Removed by Filter: 9000
-- Scanned all 10,000 customer rows. Kept 1,000 where country='US'.
-- *** This is the expensive part. An index on customers.country
-- would let PostgreSQL skip the 9,000 discarded rows entirely. ***
Planning Time: 0.187 ms
Execution Time: 13.451 ms
O item de ação surge imediatamente: Rows Removed by Filter: 9000 em uma varredura completa da tabela é o padrão mais comum para "você precisa de um índice aqui".
Os tipos de nó que você realmente encontrará
Seq Scan
Lê todas as linhas da tabela, na ordem do heap. As pessoas veem isso e querem imediatamente adicionar um índice. Mas um Seq Scan não é sempre errado — se seu filtro corresponder a 20%+ das linhas, o planejador decide corretamente que pesquisas aleatórias por índice seriam mais lentas. Um Seq Scan em uma tabela de 500 linhas é aceitável. Um Seq Scan em uma tabela de 10 milhões de linhas com um filtro altamente seletivo é um problema.
Index Scan
Usa um índice B-tree para localizar as linhas correspondentes, depois recupera cada linha do heap. A leitura do heap é a parte mais cara — cada linha é uma leitura aleatória. Ótimo quando o filtro é seletivo (ou seja, você está recuperando uma pequena fração de linhas).
Index Only Scan
A consulta pode ser respondida inteiramente a partir do índice sem tocar o heap. Isso exige que todas as colunas na cláusula SELECT e WHERE sejam cobertas pelo índice. O tipo mais rápido de varredura — se você vê isso, o design do índice está desempenhando seu papel.
Bitmap Heap Scan
Um ponto intermediário. O planejador constrói um mapa de quais páginas do heap contêm linhas correspondentes (via Bitmap Index Scan), depois recupera apenas essas páginas na ordem correta, reduzindo leituras aleatórias. Comum quando um Index Scan teria muitas leituras aleatórias, mas um Seq Scan varreduraria muitas linhas desnecessárias.
Hash Join
Constrói uma tabela hash a partir do input menor, depois procura nela com cada linha do input maior. Ótimo para grandes junções sem uma ordem útil de classificação. Preste atenção ao Batches: > 1 no nó Hash — isso significa que a tabela hash foi salva no disco porque excedeu work_mem. Aumentar work_mem para aquela sessão geralmente resolve isso.
Nested Loop
Para cada linha do lado externo, procura no lado interno (geralmente via índice). Excelente quando um lado é pequeno — O(n) em vez de O(n log n). Terrível quando ambos os lados são grandes, porque executa a varredura interna uma vez por cada linha do lado externo. Se você vê um Nested Loop com loops=50000, essa varredura interna está sendo executada 50.000 vezes.
Merge Join
Ambos os inputs chegam ordenados pela chave de junção; o planejador os percorre em paralelo. Eficiente, mas exige a ordenação inicial. Você verá isso quando ambos os lados já têm um índice na chave de junção, ou quando o planejador decide que uma etapa de ordenação é mais barata do que hashing.
Por que o planejador escolhe Seq Scan em vez de Index Scan
Isso confunde as pessoas mais. Você tem um índice. A consulta é lenta. O EXPLAIN mostra uma Seq Scan mesmo assim.
O planejador usa estatísticas de coluna — armazenadas em pg_statistic, atualizadas por ANALYZE — para estimar quantas linhas um filtro retornará. Se ele estima que 30% da tabela corresponderá, um Seq Scan é realmente mais barato do que 300.000 pesquisas aleatórias por índice. As leituras aleatórias são caras.
O limiar onde o planejador prefere um Seq Scan é aproximadamente 10–20% de seletividade, dependendo do seu hardware e constantes de custo. Você pode verificar a estimativa do planejador contra a realidade:
-- Check the planner's selectivity estimate
EXPLAIN SELECT * FROM orders WHERE status = 'pending';
-- Compare with actual count
SELECT COUNT(*) FROM orders WHERE status = 'pending';
SELECT COUNT(*) FROM orders;
Se o planejador estimou 8.000 linhas, mas há apenas 80, é um problema de estatísticas desatualizadas. Execute ANALYZE orders; e verifique novamente o plano. Isso corrige planos ruins mais frequentemente do que adicionar índices.
Ler o EXPLAIN no formato JSON
O PostgreSQL pode exibir o plano no formato JSON, o que é mais fácil de interpretar programaticamente ou explorar em uma visão em árvore:
EXPLAIN (ANALYZE, FORMAT JSON) SELECT * FROM orders
JOIN customers ON orders.customer_id = customers.id
WHERE customers.country = 'US';
A saída JSON é densa. Se você colar em IO Tools’ JSON Formatter, ela se torna uma árvore navegável — útil quando lidando com planos profundamente aninhados de consultas complexas com múltiplas subconsultas ou CTEs.
Um fluxo prático de depuração
Quando uma consulta é lenta, siga esta sequência em vez de adivinhar:
- Execute
EXPLAIN (ANALYZE, BUFFERS)na consulta exata com parâmetros reais de produção (não$1placeholders — use os valores reais). - Encontre o nó mais caro. Procure pelo maior
actual time, não pelo maior custo estimado. Eles podem ser diferentes. - Compare estimativas e valores reais de linhas nessa etapa. Uma discrepância de 10x ou mais significa que o planejador estava trabalhando com informações ruins. Execute
ANALYZE <table>;primeiro. - Procure por Seq Scans em grandes tabelas com filtros seletivos.
Rows Removed by Filter: <large number>diretamente abaixo de um Seq Scan é seu candidato ao índice. - Verifique nós Hash para
Batches > 1. Se presente, a junção foi salva no disco. Aumentework_mempara aquela sessão e reteste. - Verifique Nested Loop com contagens altas. Uma contagem de loop em milhares significa que a varredura interna está sendo sobrecarregada. Um índice na coluna de junção da tabela interna geralmente resolve isso.
Antes de escrever novos índices, também execute suas consultas pelo IO Tools’ SQL Formatter — consultas legíveis são mais fáceis de analisar, e às vezes uma reescrita elimina o problema inteiramente.
Entender o plano é o primeiro passo, não o índice. Uma vez que você conseguir ler a saída do EXPLAIN, você passará a gastar menos tempo adivinhando e mais tempo fazendo mudanças direcionadas que realmente funcionam.
Instale nossas extensões
Adicione ferramentas de IO ao seu navegador favorito para acesso instantâneo e pesquisa mais rápida
恵 O placar chegou!
Placar é uma forma divertida de acompanhar seus jogos, todos os dados são armazenados em seu navegador. Mais recursos serão lançados em breve!
Ferramentas essenciais
Ver tudo Novas chegadas
Ver tudoAtualizar: Nosso ferramenta mais recente foi adicionado em 8 de junho de 2026
