PostgreSQL raramente fica lento por causa de “banco de dados ruim” — na maioria das vezes é configuração padrão que nunca foi ajustada para a carga real do servidor, ou índice que deveria existir e ninguém criou. Já resolvi mais de um chamado de “sistema travando” que era simplesmente shared_buffers configurado com o valor de fábrica, que é ridiculamente baixo para qualquer servidor de produção. Vou passar pelos ajustes que realmente fazem diferença, na ordem que costumo verificar quando pego um banco lento pela primeira vez.
Primeiro: os parâmetros que ninguém mexe depois da instalação
A instalação padrão do PostgreSQL vem configurada para rodar em qualquer máquina, inclusive uma bem fraca — o que significa que ela está longe do ideal para um servidor de produção. Três parâmetros resolvem a maior parte dos problemas de performance básica: shared_buffers, que define quanta memória o PostgreSQL reserva para cache próprio (regra prática: 25% da RAM disponível, não mais que isso); work_mem, que controla a memória usada em operações de ordenação e hash — se está baixo, o PostgreSQL começa a usar disco para operações que caberiam na memória, o que derruba a performance de consultas com ORDER BY ou GROUP BY pesado; e effective_cache_size, que não reserva memória de verdade, só avisa o otimizador de consultas quanto de cache do sistema operacional está disponível, ajudando ele a escolher planos de execução melhores.
Índice não é sempre a solução (às vezes é o problema)
Índice B-Tree resolve a maioria das buscas por igualdade e intervalo, e deveria ser o padrão em colunas usadas em WHERE com frequência. Para colunas com array ou JSONB, índice GIN é o caminho certo — sem ele, buscar dentro de um campo JSONB faz o PostgreSQL varrer a tabela inteira linha por linha. Só que aqui vai o aviso que a maioria ignora: cada índice criado é mais um índice para atualizar em todo INSERT, UPDATE e DELETE. Já vi tabela com oito índices numa coluna que quase nunca era filtrada — a escrita ficava lenta e ninguém sabia por quê, porque o problema não aparece na leitura, aparece na escrita.
EXPLAIN ANALYZE é o primeiro passo, não o último
Antes de qualquer ajuste, rode a consulta com EXPLAIN ANALYZE na frente — por exemplo, EXPLAIN ANALYZE SELECT * FROM clientes WHERE idade > 30; — e leia o plano de execução gerado. O que você está procurando é um “Seq Scan” (varredura sequencial da tabela inteira) onde deveria existir um “Index Scan”. Se aparecer Seq Scan numa tabela grande com filtro que deveria usar índice, ou o índice não existe, ou as estatísticas da tabela estão desatualizadas e o otimizador não confia nele. A segunda causa é mais comum do que parece, e a solução é rodar um ANALYZE manual na tabela para atualizar as estatísticas.
Particionamento: só quando a tabela realmente cresceu demais
Particionar tabela é solução para tabela de dezenas de milhões de linhas, não para “tabela que está ficando grande”. Range Partitioning funciona bem para dados com componente temporal claro (logs por mês, pedidos por trimestre); List Partitioning serve quando você tem categorias bem definidas para separar (por região, por tipo de cliente). O erro comum é particionar cedo demais — para tabelas de até alguns milhões de linhas bem indexadas, particionamento costuma adicionar complexidade sem ganho real de performance.
Autovacuum: o processo que ninguém nota até parar de funcionar
O autovacuum limpa registros mortos (deixados por UPDATE e DELETE, que no PostgreSQL não apagam a linha antiga na hora) e mantém as estatísticas atualizadas para o otimizador. Quando ele está mal configurado ou desabilitado numa tabela com muita escrita, o banco vai inchando silenciosamente até um dia a performance despencar sem motivo aparente — e aí o diagnóstico é bem mais difícil do que teria sido prevenir. Vale ajustar autovacuum_vacuum_cost_limit (limite de custo por ciclo de limpeza) e autovacuum_naptime (intervalo entre execuções) conforme o volume de escrita real do seu banco, não deixar no padrão genérico.
Conexões e cache: onde o pgbouncer entra
PostgreSQL não lida bem com um volume alto de conexões simultâneas abertas diretamente — cada conexão consome memória e recurso do processo, mesmo ociosa. O pgbouncer resolve isso atuando como pool de conexões: a aplicação conecta no pgbouncer, que gerencia um número menor de conexões reais com o banco por trás. Isso costuma resolver problemas de “banco lento sob carga” que na verdade eram excesso de conexões simultâneas, não lentidão de consulta.
Se você também cuida de monitoramento da infraestrutura por trás do banco, vale conferir nosso guia de como usar Grafana para monitoramento de sistemas e o de como monitorar servidores em tempo real com Prometheus e Grafana — ter visibilidade de CPU, memória e I/O do servidor ajuda a identificar se o gargalo é mesmo no PostgreSQL ou na máquina como um todo.
Perguntas frequentes
Qual ajuste dá mais retorno com menos esforço? Corrigir shared_buffers e work_mem, na maioria dos casos. São os dois parâmetros que mais frequentemente estão longe do ideal logo depois de uma instalação padrão.
Preciso reiniciar o PostgreSQL depois de mudar esses parâmetros? Depende do parâmetro. shared_buffers exige reinício do serviço; work_mem pode ser ajustado por sessão sem reiniciar nada, o que é útil para testar antes de aplicar globalmente.
Como saber se meu banco precisa de particionamento? Se as consultas mais lentas continuam lentas mesmo com índice correto e estatísticas atualizadas, e a tabela passa de dezenas de milhões de linhas, aí sim vale considerar. Antes disso, geralmente índice e configuração de memória resolvem.
No fim das contas, otimizar PostgreSQL é menos sobre truque mágico e mais sobre revisar sistematicamente configuração, índice, plano de consulta e manutenção automática — nessa ordem. A maioria dos “bancos lentos” que já vi melhorou drasticamente só ajustando os três primeiros parâmetros e criando um índice que deveria existir desde o início.


