Como uma coluna sem índice matou nosso banco de dados
Um único índice ausente transformou uma API de 8 ms em um pesadelo de 8 segundos.
Realizamos um teste de carga antes de um lançamento importante. O desenvolvimento local funcionou bem com 100 registros. Então, simulamos 500 usuários contra 1,5 milhão de linhas em dados com o tamanho de produção.
Os resultados foram ruins:
- Os tempos de resposta da API saltaram de 45 ms para 8.000 ms.
- A CPU do banco de dados atingiu 100 %.
- Pools de conexão esgotados, causando erros de timeout.
Verificamos o log de consultas lentas (slow-query log) e encontramos um endpoint que buscava o histórico de pedidos por ID de usuário e status.
Executar EXPLAIN ANALYZE no PostgreSQL mostrou um Sequential Scan. O mecanismo leu todas as 1,5 milhão de linhas para cada requisição porque não havia um índice em user_id.
Com 100 requisições simultâneas, ele escaneou 150 milhões de linhas de uma só vez.
A correção levou cinco minutos.
Em vez de um índice simples em user_id, criamos um índice composto em (user_id, status, created_at DESC). Isso permitiu que o banco de dados:
- Filtrasse por
user_id. - Filtrasse por
status. - Retornasse as linhas mais recentes imediatamente.
- Pulasse etapas extras de ordenação.
Usamos CREATE INDEX CONCURRENTLY para que a tabela permanecesse desbloqueada durante a operação.
Após a correção:
- O tempo de consulta caiu de 8.150 ms para 0,14 ms.
- A latência da API caiu de 8 segundos para 12 ms.
- O uso de CPU caiu de 100 % para menos de 8 %.
Lições aprendidas:
- Testes locais podem ser enganosos; 100 linhas não representam um milhão.
- Indexe suas chaves estrangeiras — a maioria dos ORMs pula isso.
- Execute
EXPLAIN ANALYZEantes de comprar mais hardware. - Projete índices com base nas consultas que você realmente executa.
Não escale servidores primeiro. Escale consultas.
