Como usar explain analyze no PostgreSQL para entender planos de execução
1. Introdução ao EXPLAIN e EXPLAIN ANALYZE
O PostgreSQL oferece duas ferramentas fundamentais para entender como suas consultas são executadas: EXPLAIN e EXPLAIN ANALYZE. A diferença crucial entre elas está no fato de que EXPLAIN mostra apenas o plano estimado pelo otimizador, enquanto EXPLAIN ANALYZE executa a consulta de fato e apresenta métricas reais de desempenho.
Um plano de execução é a estratégia que o PostgreSQL escolhe para resolver uma consulta SQL. Ele determina a ordem de acesso às tabelas, quais índices utilizar, como realizar junções e como agregar dados. Compreender esse plano é essencial para identificar gargalos de performance e otimizar consultas lentas.
As variações mais comuns incluem:
- EXPLAIN (ANALYZE, BUFFERS) — adiciona informações sobre uso de cache
- EXPLAIN (ANALYZE, FORMAT JSON) — saída estruturada para ferramentas de visualização
- EXPLAIN (ANALYZE, COSTS OFF) — foco apenas nos tempos reais
2. Lendo a Saída do EXPLAIN ANALYZE
A saída do EXPLAIN ANALYZE possui estrutura hierárquica, onde cada linha representa um nó do plano. Vamos analisar um exemplo prático:
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM orders WHERE customer_id = 42;
Seq Scan on orders (cost=0.00..1843.00 rows=1 width=36) (actual time=0.023..12.450 rows=5 loops=1)
Filter: (customer_id = 42)
Rows Removed by Filter: 99995
Buffers: shared hit=100 read=50
Planning Time: 0.085 ms
Execution Time: 12.523 ms
Componentes essenciais:
- cost: estimativa do otimizador (custo inicial..custo total)
- rows: número estimado de linhas retornadas
- actual time: tempo real (início..fim) em milissegundos
- loops: quantas vezes o nó foi executado
- Buffers: páginas lidas do cache (hit) ou do disco (read)
- Rows Removed by Filter: linhas descartadas por condições WHERE
O gargalo principal geralmente está no nó com maior tempo total (actual time * loops). No exemplo acima, o Sequential Scan varreu 100.000 linhas para encontrar apenas 5, indicando ausência de índice adequado.
3. Operadores Comuns e Seus Significados
Sequential Scan vs. Index Scan vs. Index Only Scan
-- Sequential Scan (varredura completa da tabela)
Seq Scan on products (cost=0.00..2350.00 rows=50000 width=40)
-- Index Scan (acesso via índice, busca na tabela)
Index Scan using idx_products_category on products (cost=0.29..8.31 rows=1 width=40)
Index Cond: (category_id = 5)
-- Index Only Scan (apenas índice, sem acessar tabela)
Index Only Scan using idx_products_sku on products (cost=0.29..4.30 rows=1 width=20)
Join Operators
-- Nested Loop (para poucas linhas na tabela externa)
Nested Loop (cost=0.30..15.20 rows=10 width=76)
-> Seq Scan on customers (cost=0.00..5.00 rows=1 width=40)
-> Index Scan using idx_orders_customer on orders (cost=0.29..10.20 rows=5 width=36)
-- Hash Join (para tabelas grandes sem índice)
Hash Join (cost=150.00..450.00 rows=1000 width=76)
Hash Cond: (c.id = o.customer_id)
-> Seq Scan on customers c (cost=0.00..100.00 rows=5000 width=40)
-> Hash (cost=100.00..100.00 rows=5000 width=36)
-- Merge Join (para dados ordenados)
Merge Join (cost=200.00..500.00 rows=1000 width=76)
Merge Cond: (c.id = o.customer_id)
4. Analisando Métricas de Performance
Tempo de execução vs. tempo de planejamento
O Planning Time representa o tempo que o otimizador gastou para gerar o plano. O Execution Time é o tempo real de execução. Se o planning time for muito alto, considere usar prepared statements.
Buffers e cache
Buffers: shared hit=100 read=50 dirtied=5
- shared hit: páginas encontradas no cache (bom)
- shared read: páginas lidas do disco (evitar)
- shared dirtied: páginas modificadas durante a consulta
Uma consulta ideal deve ter predominantemente "shared hit", indicando que os dados estão em memória.
Rows Removed by Filter
Este indicador mostra quantas linhas foram descartadas por condições WHERE. Valores altos em relação ao retorno indicam baixa seletividade do índice ou falta de índice apropriado.
5. Estratégias para Otimizar com Base no Plano
Quando adicionar um índice
-- Plano problemático
Seq Scan on orders (cost=0.00..1843.00 rows=5 width=36)
Filter: (status = 'pending' AND created_at > '2024-01-01')
-- Solução: criar índice composto
CREATE INDEX idx_orders_status_created ON orders (status, created_at);
Ajustando consultas
Subqueries podem ser transformadas em JOINs para melhor performance:
-- Subquery ineficiente
SELECT * FROM customers WHERE id IN (SELECT customer_id FROM orders WHERE total > 1000);
-- Reescrita com JOIN
SELECT DISTINCT c.* FROM customers c JOIN orders o ON c.id = o.customer_id WHERE o.total > 1000;
Forçando planos alternativos
Em casos de diagnóstico, é possível desabilitar operadores específicos:
-- Forçar uso de Index Scan em vez de Sequential Scan
SET enable_seqscan = OFF;
EXPLAIN ANALYZE SELECT * FROM orders WHERE customer_id = 42;
6. Casos Práticos de Diagnóstico
Caso 1: Nested Loop ineficiente
Nested Loop (cost=0.30..5000.00 rows=1000 width=76) (actual time=0.050..150.000 rows=1000 loops=1)
-> Seq Scan on customers (cost=0.00..100.00 rows=5000 width=40) (actual time=0.020..5.000 rows=5000 loops=1)
-> Index Scan using idx_orders_customer on orders (cost=0.29..0.98 rows=1 width=36) (actual time=0.030..0.030 rows=1 loops=5000)
O problema: 5000 loops no Index Scan. Solução: adicionar índice em orders.customer_id e considerar Hash Join.
Caso 2: Hash Join com spill to disk
Hash Join (cost=500.00..2000.00 rows=50000 width=76) (actual time=100.000..500.000 rows=50000 loops=1)
Hash Cond: (c.id = o.customer_id)
-> Seq Scan on customers (cost=0.00..100.00 rows=5000 width=40)
-> Hash (cost=300.00..300.00 rows=50000 width=36) (actual time=50.000..50.000 rows=50000 loops=1)
Buckets: 4096 Batches: 4 Memory Usage: 2048kB
O indicador "Batches: 4" mostra que a hash table não coube na memória e houve escrita em disco. Solução: aumentar work_mem.
Caso 3: Index Scan não sendo utilizado
Seq Scan on products (cost=0.00..500.00 rows=1000 width=40) (actual time=0.050..50.000 rows=1000 loops=1)
Filter: (price > 100 AND category = 'eletrônicos')
Verifique se o índice existe e se as estatísticas estão atualizadas:
ANALYZE products;
EXPLAIN ANALYZE SELECT * FROM products WHERE price > 100 AND category = 'eletrônicos';
7. Ferramentas e Boas Práticas para Uso Contínuo
Monitoramento com pg_stat_statements
-- Habilitar extensão
CREATE EXTENSION pg_stat_statements;
-- Consultar queries mais lentas
SELECT query, mean_exec_time, calls, rows
FROM pg_stat_statements
ORDER BY mean_exec_time DESC
LIMIT 10;
auto_explain para logging automático
-- No postgresql.conf
shared_preload_libraries = 'auto_explain'
auto_explain.log_min_duration = '500ms' -- loga queries acima de 500ms
auto_explain.log_analyze = on
auto_explain.log_buffers = on
Visualização gráfica
- pgAdmin: ferramenta nativa com visualização visual de planos
- explain.dalibo.com: analisador online que coloriza e explica cada nó
Checklist para análise sistemática
- Execute
EXPLAIN (ANALYZE, BUFFERS) - Identifique o nó com maior tempo total
- Verifique
Rows Removed by Filtervs.rowsretornadas - Analise
Buffers: muitosreadindicam falta de cache - Confira se índices estão sendo utilizados
- Avalie se o tipo de join é adequado ao volume de dados
- Verifique estatísticas com
ANALYZEse necessário
Referências
- Documentação oficial do PostgreSQL: EXPLAIN — Documentação completa sobre a sintaxe e parâmetros do comando EXPLAIN
- Documentação oficial do PostgreSQL: Usando EXPLAIN — Guia oficial sobre interpretação de planos de execução
- PostgreSQL Tutorial: EXPLAIN ANALYZE Explained — Tutorial prático com exemplos de uso do EXPLAIN ANALYZE
- Use the Index, Luke: PostgreSQL Execution Plans — Guia avançado sobre leitura e otimização de planos de execução
- pgMustard: How to Read PostgreSQL EXPLAIN — Ferramenta e guia visual para interpretação de planos de execução
- Cybertec: PostgreSQL EXPLAIN ANALYZE - A Complete Guide — Artigo técnico detalhado com casos reais de otimização
- explain.dalibo.com — Visualizador online gratuito de planos de execução PostgreSQL