Por que seu banco de dados fica lento muito antes de você perceber

Tem vídeo desse conteúdo no canal Veja o passo a passo completo em vídeo.
Assistir no YouTube →
Por que seu banco de dados fica lento muito antes de você perceber

Banco de dados lento quase nunca é um evento. É um processo. Ele não acorda um dia decidindo travar — ele vai ficando devagar, requisição por requisição, até que num pico de uso alguém abre um chamado dizendo que “o sistema está pesado”. A essa altura, o problema já estava lá havia meses.

A boa notícia é que os culpados são quase sempre os mesmos, e dá para enxergá-los muito antes do incêndio. Vamos aos dois mais comuns.

Culpado número um: a query sem índice

Com mil registros na tabela, uma busca sem índice é instantânea. O banco varre tudo e nem sua. Com um milhão de registros, essa mesma busca vira uma varredura completa a cada chamada — o famoso full table scan. E o pior: em desenvolvimento, com pouca massa de dados, você nunca vê o problema. Ele só aparece em produção, quando a tabela cresceu.

A ferramenta para enxergar isso é o EXPLAIN. Coloque ele na frente da sua query e o banco te conta o plano que pretende executar:

EXPLAIN SELECT * FROM pedidos WHERE cliente_id = 42;

-- Sem índice em cliente_id:
--   Seq Scan on pedidos  (cost=0.00..18334.00 rows=8 ...)
-- Com índice:
--   Index Scan using idx_pedidos_cliente on pedidos ...

“Seq Scan” numa tabela grande é um sinal amarelo. Significa que o banco está lendo linha por linha para encontrar o que você pediu. Criar um índice na coluna que você filtra transforma essa varredura num acesso direto:

CREATE INDEX idx_pedidos_cliente ON pedidos (cliente_id);

Um detalhe que vale ouro: índice não é de graça. Cada índice deixa as escritas um pouco mais lentas, porque o banco precisa mantê-lo atualizado a cada INSERT e UPDATE. Então a regra não é “indexe tudo” — é “indexe o que você realmente filtra e ordena com frequência”.

Culpado número dois: o problema N+1

Esse é traiçoeiro porque nasce de um código que parece limpo. Você lista os pedidos e, para cada um, mostra o nome do cliente. Em muitos ORMs, isso vira exatamente isto por baixo dos panos:

-- 1 query para pegar os pedidos:
SELECT * FROM pedidos LIMIT 100;

-- + 1 query para CADA pedido, buscando o cliente:
SELECT * FROM clientes WHERE id = 1;
SELECT * FROM clientes WHERE id = 2;
SELECT * FROM clientes WHERE id = 3;
-- ... mais 97 vezes

Cem pedidos, 101 idas ao banco. Cada uma custa uma viagem de rede. No seu ambiente local, com o banco na mesma máquina, isso é imperceptível. Em produção, com o banco a alguns milissegundos de distância, essas viagens somam e o endpoint que parecia rápido começa a arrastar.

A solução é buscar tudo de uma vez, seja com um JOIN, seja com o carregamento antecipado que a maioria dos ORMs oferece (o famoso eager loading):

SELECT p.*, c.nome
FROM pedidos p
JOIN clientes c ON c.id = p.cliente_id
LIMIT 100;
-- Uma query. Uma viagem.

Como ver antes do usuário reclamar

A parte mais importante deste texto não é o índice nem o JOIN. É criar o hábito de olhar. A maioria dos times descobre esses problemas tarde porque simplesmente nunca observa as queries que a aplicação dispara.

  • Ligue o log de queries lentas do banco. No PostgreSQL e no MySQL, você define um limite (digamos, 200 ms) e tudo que passar disso vai para o log. É o seu detector de fumaça.
  • Rode a aplicação localmente com um contador de queries por requisição. Se abrir uma tela dispara 300 queries, você achou um N+1 sem precisar esperar produção.
  • Teste com volume realista. Popular a tabela com um milhão de linhas fake antes de subir revela em segundos o que levaria meses para aparecer com dados de brincadeira.

Performance de banco raramente é sobre otimização heroica. É sobre não deixar o problema óbvio crescer no escuro.

Um passo além: o índice composto

Quando você filtra por mais de uma coluna ao mesmo tempo — ou filtra por uma e ordena por outra —, um índice em cada coluna separada nem sempre resolve. O banco costuma se sair muito melhor com um índice composto, que cobre as colunas na ordem certa:

-- Busca por cliente E ordena por data:
SELECT * FROM pedidos
WHERE cliente_id = 42
ORDER BY criado_em DESC;

CREATE INDEX idx_pedidos_cliente_data
  ON pedidos (cliente_id, criado_em DESC);

A ordem das colunas dentro do índice importa: coloque primeiro a coluna que você usa para filtrar por igualdade, depois a que usa para ordenar ou filtrar por faixa. Errar essa ordem faz o banco ignorar o índice em silêncio — e você jura que indexou, mas o EXPLAIN continua mostrando aquela varredura sequencial teimosa.

O ponto que fica

Seu banco provavelmente já está fazendo trabalho demais em algum endpoint que você usa todo dia — só que ainda dentro de uma margem que ninguém percebe. A diferença entre um time que apaga incêndio e um que dorme tranquilo não é conhecimento avançado de banco. É o hábito de rodar um EXPLAIN na query nova, olhar o log de lentidão de vez em quando e testar com dados de verdade antes de o usuário testar por você.

Leia também

Prefere ver em vídeo?

Esse artigo virou um vídeo completo no canal, com diagramas animados e demo ao vivo.

Assistir no YouTube →