A dor real: "está lento, mas não sei por quê"
Toda empresa que cresce, em algum momento, vive isso. Aquela consulta no sistema que era rápida começa a demorar. Primeiro, 1 segundo. Depois, 3. Quando você percebe, virou 12 segundos pra abrir o relatório de vendas do mês — e a equipe comercial começa a reclamar.
No Excel, você não tem muita escolha. Espera carregar, torce pra dar certo, talvez quebra a planilha em pedaços menores. Diagnóstico fica em "está lento" — sem entender o porquê.
No banco de dados, é diferente. Você tem uma ferramenta que mostra exatamente o caminho que o banco está fazendo, quanto tempo demora cada etapa, e onde está o gargalo real. Esse comando se chama EXPLAIN.
O que é EXPLAIN
EXPLAIN é um comando do PostgreSQL (e de praticamente todo banco relacional sério) que pede ao banco pra mostrar o plano de execução de uma consulta. Em vez de rodar a consulta e te dar o resultado, ele te mostra como ele rodaria.
Uso básico — basta colocar EXPLAIN antes do SELECT:
EXPLAIN
SELECT cliente, SUM(valor)
FROM pedidos
WHERE data >= '2026-01-01'
GROUP BY cliente;
O banco devolve algo assim:
HashAggregate (cost=1234.56..1456.78 rows=200 width=40)
Group Key: cliente
-> Seq Scan on pedidos (cost=0.00..1000.00 rows=10000 width=40)
Filter: (data >= '2026-01-01')
Parece grego. Mas em 5 minutos de leitura vira informação cristalina.
EXPLAIN vs EXPLAIN ANALYZE — a diferença que importa
| Comando | O que faz |
|---|---|
| EXPLAIN | Mostra o plano estimado. Não roda a query. |
| EXPLAIN ANALYZE | Mostra o plano + roda a query de verdade + tempo real medido. |
Na prática, você sempre quer EXPLAIN ANALYZE quando está investigando lentidão real. O EXPLAIN puro é estimativa (cost calculado por heurística). O EXPLAIN ANALYZE traz números reais (actual time).
EXPLAIN ANALYZE
SELECT cliente, SUM(valor)
FROM pedidos
WHERE data >= '2026-01-01'
GROUP BY cliente;
Cuidado: EXPLAIN ANALYZE executa a query. Em produção, evite com UPDATE/DELETE/INSERT — vai aplicar a mudança. Pra investigação segura, envolva em transação:
BEGIN;
EXPLAIN ANALYZE UPDATE pedidos SET status = 'pago' WHERE id = 123;
ROLLBACK;
O ROLLBACK desfaz tudo após a análise.
Como ler o output — as 4 informações que importam
Cada linha do plano é uma operação. Olhe de baixo pra cima (o banco executa de dentro pra fora — o nó mais interno é o primeiro).
1. Tipo de operação
| Operação | O que é | Bom ou ruim? |
|---|---|---|
| Seq Scan | Leu a tabela inteira, linha por linha | 🟡 Ok em tabela pequena, ruim em tabela grande |
| Index Scan | Usou índice pra ir direto nas linhas certas | 🟢 Geralmente ótimo |
| Bitmap Index Scan | Misto — usa índice mas com ajuste | 🟢 Bom pra filtros que retornam muitos registros |
| Index Only Scan | Resposta veio só do índice, sem ir na tabela | 🟢🟢 Excelente |
| Nested Loop | Pra cada linha da tabela A, percorre tabela B | 🟡 Ok com poucas linhas, péssimo em massa |
| Hash Join | Construiu hash em memória pra combinar tabelas | 🟢 Geralmente bom |
| Sort | Ordenou em memória ou disco | 🟡 Disco = ruim, memória = ok |
2. Cost (custo estimado)
(cost=0.00..1000.00 rows=10000 width=40)
- 0.00 = custo pra começar a devolver primeira linha
- 1000.00 = custo total pra terminar
- rows = quantas linhas o banco acha que vai processar
- width = tamanho médio em bytes de cada linha
Cost não tem unidade real. É uma escala interna do PostgreSQL. Compare cost antes e depois da otimização — você quer ver o número cair.
3. Actual time (só com EXPLAIN ANALYZE)
(actual time=2.345..1234.567 rows=10000 loops=1)
- 2.345 ms = tempo até a primeira linha sair
- 1234.567 ms = tempo total — esse é o que dói
- rows = quantas linhas DE VERDADE foram processadas
- loops = quantas vezes essa operação rodou (1 = só uma vez)
4. Diferença entre estimado e real
estimated rows=10000 ↔ actual rows=2 000 000
Se o estimado é muito diferente do real, o banco está com estatísticas desatualizadas. Solução: rode ANALYZE nome_da_tabela; pra atualizar.
Caso real — antes e depois do índice
Imagine que essa consulta está demorando 8 segundos:
SELECT * FROM despesas
WHERE vendedor_id = 42 AND data >= '2026-01-01';
Antes do índice (EXPLAIN ANALYZE)
Seq Scan on despesas (cost=0.00..18500.00 rows=300 width=80)
(actual time=12.456..8245.789 rows=287 loops=1)
Filter: ((vendedor_id = 42) AND (data >= '2026-01-01'))
Rows Removed by Filter: 765432
Planning Time: 0.234 ms
Execution Time: 8246.123 ms
Leitura:
Seq Scan= leu a tabela toda (765 mil linhas removidas pelo filtro!)Execution Time: 8.246 ms= 8 segundos- Foi buscar 287 linhas mas precisou ler 765.000 pra achar — desperdício brutal
Solução: criar índice composto
CREATE INDEX idx_despesas_vendedor_data
ON despesas (vendedor_id, data);
Depois do índice
Index Scan using idx_despesas_vendedor_data on despesas
(cost=0.42..125.50 rows=287 width=80)
(actual time=0.123..3.456 rows=287 loops=1)
Index Cond: ((vendedor_id = 42) AND (data >= '2026-01-01'))
Planning Time: 0.456 ms
Execution Time: 4.123 ms
Leitura:
Index Scan= foi direto via índiceExecution Time: 4.123 ms= 4 milissegundos- De 8 segundos pra 4 ms = 2.000x mais rápido
Esse é o tipo de ganho que muda a percepção do sistema da equipe.
Quando criar um índice — sinais óbvios
Use EXPLAIN ANALYZE e procure por:
- ✅ Sequential Scan em tabela grande (>10 mil linhas) com filtro seletivo
- ✅ Rows Removed by Filter muito alto (banco lê milhares pra achar poucos)
- ✅ Sort que aparece e demora — pode virar Index Scan ordenado
- ✅ Mesma query rodando muito num campo específico
E não crie índice quando:
- ❌ Tabela tem menos de 1.000 linhas — Seq Scan é mais rápido
- ❌ Coluna tem pouca cardinalidade (ex: campo "ativo" só com TRUE/FALSE)
- ❌ Tabela tem muito INSERT/UPDATE — todo índice deixa escrita mais lenta. Mais índices = mais lentidão pra gravar.
- ❌ Coluna usada com
LIKE '%xxx%'— índice B-tree não funciona com wildcard no início. Aí precisa de índice especial (GIN compg_trgm)
3 pegadinhas que enganam até quem já usa EXPLAIN
1. Cache mascara o problema
A primeira execução de uma query lê do disco (lenta). A segunda já lê do cache em memória (rápida). Pra medir de verdade, rode 3-4 vezes e considere as últimas — não a primeira.
2. Dataset pequeno em dev, grande em produção
Sua máquina de desenvolvimento tem 1.000 pedidos. Produção tem 5 milhões. O plano que o banco escolhe muda com o tamanho dos dados. Uma query que parece rápida no dev pode virar um Seq Scan brutal em produção.
Solução: teste sempre contra um snapshot recente da produção (ou um dataset com pelo menos a mesma ordem de grandeza).
3. ANALYZE travada
Se o EXPLAIN mostra rows=1000 mas você sabe que a tabela tem 1 milhão, as estatísticas estão velhas. Rode:
ANALYZE nome_da_tabela;
Pra coletar estatísticas atualizadas. Sem isso, o PostgreSQL pode tomar decisão errada baseada em dado defasado.
Ferramentas que ajudam — além do terminal
EXPLAIN puro é texto. Pra queries complexas, ler árvore aninhada de 30 linhas vira tortura. Use:
explain.dalibo.com
- Cola o
EXPLAIN ANALYZElá - Mostra árvore visual interativa
- Marca em vermelho os nós lentos
- Gratuito, online, não precisa cadastro
pgAdmin (GUI do PostgreSQL)
- Botão "Explain" e "Explain Analyze" direto no editor
- Mostra plano gráfico
- Bom pra investigação visual rápida
pg_stat_statements (extensão do PostgreSQL)
- Não é EXPLAIN, mas complementa
- Mostra as queries mais lentas do sistema ao longo do tempo
- Bom pra descobrir o que vale otimizar primeiro
Por que isso importa pro negócio
Quando você não tem EXPLAIN:
- "O relatório está lento" → resposta: "vou ver"
- Sem dados, palpita: "talvez precise de mais servidor"
- Compra hardware caro pra resolver um problema de 1 índice esquecido
- 3 meses depois, está lento de novo
Quando você usa EXPLAIN:
- "O relatório está lento" → roda EXPLAIN ANALYZE
- Vê que é Seq Scan em tabela de 2 milhões
- Cria índice em 30 segundos
- Tempo cai de 12s pra 80ms
- Custo: zero — só conhecimento aplicado
A diferença entre "achar que melhorou" e ter controle real do sistema está em saber ler 20 linhas de texto. É um dos melhores investimentos de tempo que um analista ou desenvolvedor pode fazer.
Resumo prático — sua próxima consulta lenta
- Identifique a query que está lenta
- Rode com
EXPLAIN ANALYZEna frente - Procure: tem Seq Scan em tabela grande?
Rows Removed by Filteralto? Sort em disco? - Identifique a coluna do filtro/join sem índice
- Crie o índice apropriado:
CREATE INDEX nome_descritivo ON tabela (coluna); - Rode EXPLAIN ANALYZE de novo
- Compare o tempo. Se caiu = sucesso. Se não caiu = índice errado, tente outro
Em 90% dos casos de "está lento", o problema é índice faltando ou mal escolhido. EXPLAIN te dá o mapa pra encontrar.
Fechamento
Dominar EXPLAIN significa sair do "funciona mais ou menos" pra ter controle real sobre seus dados. E quando você tem controle sobre dados, tem controle sobre decisões — que é onde mora o resultado real do negócio.
O Excel te ensina a fazer cálculo. O SQL te ensina a entender o cálculo. O EXPLAIN te ensina a explicar por que o cálculo demora — e como tirar o gargalo.
Próximo passo: pega uma query lenta do seu sistema hoje e roda EXPLAIN ANALYZE nela. Em 10 minutos você vai entender mais sobre performance do que muita gente entende em 10 anos.