Dev Notes

Tecnologia, infraestrutura e desenvolvimento de software.

← Todos os posts

Por que seu banco de dados está lento (e como descobrir)

Banco de dados lento é um dos problemas mais comuns e mais mal diagnosticados no desenvolvimento de software. A reação instintiva é adicionar cache, trocar de banco ou jogar mais hardware no problema. Essas soluções às vezes funcionam por acidente, mas raramente resolvem a causa raiz.

A causa raiz, na imensa maioria dos casos, é uma query que faz mais trabalho do que precisaria. E descobrir isso exige uma habilidade específica: saber ler o plano de execução de uma query.

O que é um plano de execução

Quando você envia uma query para o banco de dados, ele não a executa diretamente. Primeiro, o otimizador analisa a query, os dados disponíveis, as estatísticas das tabelas e os índices existentes, e decide qual é a forma mais eficiente de obter o resultado. Esse planejamento é o plano de execução.

O plano de execução é uma árvore de operações. Cada nó da árvore representa uma operação: um scan de tabela, uma busca por índice, um join, uma ordenação. Ler esse plano é como ver o rascunho mental que o banco fez antes de responder sua query.

No PostgreSQL, você acessa o plano com EXPLAIN. Com EXPLAIN ANALYZE, o banco executa a query de verdade e mostra os números reais ao lado das estimativas. Essa diferença é importante: às vezes o otimizador faz uma escolha ruim porque as estatísticas estão desatualizadas.

Os sinais de problema mais comuns

Sequential scan em tabelas grandes. Seq Scan significa que o banco está lendo a tabela inteira para encontrar as linhas que você quer. Em tabelas pequenas isso é normal — às vezes é até mais rápido que usar um índice. Em tabelas com milhões de linhas, é quase sempre um sinal de que um índice está faltando ou não está sendo usado.

Nested loop com muitas iterações. Nested loop é uma estratégia de join onde para cada linha de uma tabela, o banco busca as correspondentes na outra. Isso é eficiente quando uma das tabelas é pequena. Quando ambas são grandes, o número de operações cresce quadraticamente e o resultado é uma query que nunca termina.

Sort sem índice. Se uma query tem ORDER BY em uma coluna sem índice e o resultado tem muitas linhas, o banco precisa carregar tudo em memória e ordenar. Isso é lento e consome memória. Um índice na coluna de ordenação resolve.

Estimativas muito erradas. Quando o plano mostra "rows=1" mas o ANALYZE mostra "rows=50000", o otimizador tomou decisões com informações erradas. Rodar ANALYZE na tabela atualiza as estatísticas e geralmente muda o plano para melhor.

Como identificar as queries problemáticas

Antes de otimizar qualquer coisa, você precisa saber o que otimizar. Otimizar uma query que roda uma vez por dia não tem o mesmo impacto que otimizar uma que roda mil vezes por minuto.

No PostgreSQL, a extensão pg_stat_statements registra estatísticas de todas as queries executadas: quantas vezes rodou, tempo total, tempo médio, linhas retornadas. Com ela ativa, uma consulta simples mostra as queries que mais consomem tempo total — que é a métrica mais relevante para o desempenho geral do sistema.

Ferramentas de APM (Application Performance Monitoring) como Datadog, New Relic ou o próprio pganalyze fazem isso de forma visual e com alertas. Mas mesmo sem essas ferramentas, o slow query log do banco — que registra queries que passam de um threshold de tempo — já é um ponto de partida muito útil.

Índices: quando ajudam e quando não ajudam

A solução mais comum para queries lentas é criar um índice. E frequentemente é a solução certa. Mas índices têm custo: eles ocupam espaço em disco e tornam inserções, atualizações e deleções mais lentas, porque o banco precisa manter o índice atualizado.

Um índice em uma coluna com poucos valores distintos (como uma coluna booleana ou um campo de status com três opções) raramente ajuda. O banco muitas vezes prefere o seq scan porque o índice levaria a muitas linhas de qualquer forma.

Índices compostos — em múltiplas colunas — podem ser muito eficientes para queries que filtram por várias condições juntas, mas só funcionam se a query respeita a ordem das colunas no índice. Um índice em (a, b) ajuda queries que filtram por a ou por a e b juntos. Não ajuda queries que filtram só por b.

O problema do N+1

O problema do N+1 não aparece no plano de execução de uma única query — porque cada query individual pode ser rápida. O problema está no padrão: sua aplicação faz uma query para buscar N registros, e depois faz mais N queries para buscar dados relacionados de cada um.

Com 10 registros isso é imperceptível. Com 1.000 são 1.001 queries por request. A solução é usar joins ou carregar os dados relacionados em uma única query adicional — o que ORMs modernos chamam de eager loading.

Identificar N+1 requer observar os logs de queries durante uma operação real. Se você vê a mesma query se repetindo dezenas de vezes com parâmetros diferentes, é quase certamente N+1.

Quando o problema não é a query

Às vezes a query está bem, mas o banco ainda está lento. Nesse caso, o problema pode ser outra coisa: lock contention (queries esperando outras liberarem locks), conexões esgotadas (a aplicação espera por uma conexão disponível no pool), ou simplesmente volume — queries corretas ficam lentas quando a tabela cresce para um tamanho que não foi considerado no design original.

Para lock contention, pg_locks e pg_stat_activity mostram quem está esperando por quem. Para volume, a solução costuma ser particionamento de tabela ou arquivamento de dados históricos.

Conclusão

Banco de dados lento é um sintoma, não um problema. O problema é quase sempre uma operação que faz mais trabalho do que o necessário — uma tabela inteira sendo lida quando só precisava de algumas linhas, um join mal planejado, estatísticas desatualizadas enganando o otimizador.

A habilidade de ler um plano de execução, identificar as queries mais custosas e entender por que o banco fez as escolhas que fez é o que separa uma investigação eficiente de um processo de tentativa e erro. Não é difícil de aprender — e resolve a grande maioria dos problemas de desempenho sem precisar trocar de tecnologia.