Índice, EXPLAIN e a query lenta
- postgres
- banco-de-dados
- performance
Publicado em por Gabriel Dias · 5 min de leitura · Engenharia
Quase toda query lenta que eu já investiguei caía em uma de três categorias: falta índice, o índice existe mas não pode ser usado, ou o índice é usado e ainda assim o banco vai à tabela buscar o resto.
As três têm diagnóstico e correção diferentes, e as três aparecem no EXPLAIN.
O que é um índice, por dentro
Um índice de banco relacional é uma B+tree. Cada nó ocupa uma página de disco e guarda centenas de chaves com ponteiros. Com um fator de ramificação de umas trezentas, uma árvore de três níveis endereça vinte e sete milhões de registros.
Três acessos a disco no pior caso — e os dois primeiros níveis quase sempre estão em memória, então na prática é um.
A variante "mais" significa que os dados ficam só nas folhas, e as folhas são ligadas entre si numa lista. Isso deixa os nós internos mais enxutos e transforma varredura por faixa em caminhar numa lista encadeada.
É por isso que índice de banco é árvore e não hash: hash resolve igualdade em tempo constante e não resolve faixa nem ordenação. E faixa e ordenação são a maior parte das suas queries.
O custo do índice
Índice acelera leitura e desacelera escrita, porque toda inserção, atualização e remoção precisa atualizar todas as árvores relevantes. Índice também ocupa disco e memória.
Consequência: índice não usado é puro custo. E quase todo banco de produção tem vários.
No Postgres, a consulta é direta:
SELECT relname, indexrelname, idx_scan
FROM pg_stat_user_indexes
WHERE idx_scan = 0
ORDER BY relname;Índices com zero varreduras desde o último reset das estatísticas são candidatos a remoção. Confira antes se não são de unicidade — esses têm outra função.
A ordem das colunas decide tudo
Índice composto segue o princípio do dicionário telefônico: ordenado por sobrenome e depois por nome.
Você acha todos os "Silva" rapidamente. Você acha "Silva, João" rapidamente. Mas se você só sabe o nome "João", o dicionário não ajuda — teria que ler tudo.
Índice em (a, b) serve para:
- filtro em
a - filtro em
aeb
Não serve para filtro apenas em b.
A regra prática para montar índice composto: coloque primeiro as colunas usadas com igualdade, depois a coluna usada com faixa, e por último as usadas só para ordenar.
O que impede o índice de ser usado
Existem padrões que anulam o índice mesmo com ele criado. Os mais frequentes:
Função na coluna. WHERE upper(email) = 'X' não usa o índice em email. Solução: criar índice
na expressão, ou normalizar na escrita.
LIKE com curinga no início. WHERE nome LIKE '%silva' não pode usar B-tree, porque a árvore é
ordenada pelo começo da string. Para isso existe índice de trigrama ou busca textual.
Comparação entre tipos diferentes. Uma coluna varchar comparada com número força conversão e
derruba o índice.
Baixa seletividade. Se a condição retorna metade da tabela, varrer a tabela é mais rápido que pular de índice para tabela milhares de vezes. O banco sabe disso e escolhe o Seq Scan de propósito. Nesse caso, o Seq Scan não é o problema.
Index only scan: a otimização subestimada
Quando o banco usa um índice, ele normalmente encontra o ponteiro e vai à tabela buscar as colunas que faltam. São dois acessos.
Se o índice contiver todas as colunas de que a query precisa, o banco responde sem tocar na tabela. Isso é um index only scan, e é um acesso em vez de dois.
No Postgres, você consegue isso com INCLUDE:
CREATE INDEX idx_pedidos_cliente
ON pedidos (cliente_id, criado_em)
INCLUDE (status, valor);As colunas do INCLUDE não participam da ordenação — só ficam armazenadas na folha. É a diferença
entre pagar um acesso e pagar dois, em toda execução.
Na minha experiência, é a mudança de índice com melhor relação entre esforço e ganho que existe, e a menos aplicada.
Como ler um EXPLAIN ANALYZE
Você quer olhar três coisas, nessa ordem.
Primeira: o tipo de acesso. Seq Scan numa tabela grande com filtro seletivo é sinal de índice faltando. Index Scan é bom. Index Only Scan é ótimo. Bitmap Heap Scan aparece quando muitas linhas casam e o banco decide ordenar os acessos ao disco — normalmente razoável.
Segunda: estimativa versus realidade. O plano mostra quantas linhas o banco esperava e quantas
vieram. Se ele esperava dez e vieram cem mil, as estatísticas estão desatualizadas, e o plano
escolhido foi ruim por falta de informação. Rode ANALYZE na tabela.
Terceira: onde está o tempo. Cada nó do plano mostra o tempo acumulado. Procure o nó que consome a maior fatia e trabalhe nele. Não otimize o resto.
E adicione BUFFERS ao EXPLAIN: ele mostra quantas páginas foram lidas do cache e quantas do
disco. Uma query que lê muito do disco é candidata a caber melhor em memória ou a precisar de um
índice mais enxuto.
O roteiro completo
Quando uma query estiver lenta:
- Rode
EXPLAIN (ANALYZE, BUFFERS). - Se for Seq Scan com filtro seletivo → crie o índice, respeitando a ordem das colunas.
- Se for Index Scan mas com muitos acessos à tabela → considere
INCLUDEpara virar Index Only Scan. - Se a estimativa estiver muito errada → rode
ANALYZEe considere aumentar o alvo de estatística naquela coluna. - Se nada disso resolver → o problema pode não ser a query. Pode ser lock, pode ser I/O saturado, pode ser plano ruim por parâmetro. Aí você sobe um nível e vai para observabilidade.
Um e-mail por semana, sem enrolação
O que eu aprendi construindo software e IA na semana — em texto curto, direto, sem newsletter de 3 mil palavras. Cancele quando quiser.