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 ANALYZE antes de comprar mais hardware.
  • Projete índices com base nas consultas que você realmente executa.

Não escale servidores primeiro. Escale consultas.

Fonte: https://dev.to/mia_keller_ffd2584c046ecb/how-an-unindexed-column-silently-killed-our-database-under-load-and-the-5-minute-fix-m32