Um Guia Sistemático para Depurar Consultas Lentas no PostgreSQL
Depure consultas lentas no PostgreSQL com pg_stat_statements, EXPLAIN ANALYZE, leituras de buffer, estimativas de linhas, índices e verificação.
Um Guia Sistemático para Depurar Consultas Lentas no PostgreSQL
Consultas lentas no PostgreSQL são mais fáceis de corrigir quando você para de tratá-las como mistérios. O banco de dados geralmente pode mostrar o caminho que escolheu, quantas linhas esperava, quantas linhas realmente tocou, se leu do cache ou do disco e se esperou por outra sessão.
O erro que vejo com mais frequência é pular direto de "este endpoint está lento" para "criar um índice". Às vezes funciona. Às vezes, o índice é ignorado porque o predicado está escrito de uma forma que não pode usá-lo. Às vezes, a consulta está boa, mas bloqueada por uma transação que está aberta há vinte minutos. Uma abordagem sistemática economiza tempo porque separa a forma da consulta, as estimativas do planejador, E/S, memória e bloqueios.
Entendendo os Gargalos de Desempenho de Consultas
Antes de mergulhar nas ferramentas, é essencial reconhecer razões comuns pelas quais uma consulta PostgreSQL pode ter desempenho ruim. Esses problemas geralmente se enquadram em algumas categorias principais:
- Índices Ausentes ou Ineficientes: O banco de dados é forçado a realizar varreduras sequenciais em tabelas grandes quando um índice poderia ter fornecido acesso rápido.
- Estrutura de Consulta Subótima: Junções complexas, subconsultas desnecessárias ou uso inadequado de funções podem confundir o planejador.
- Estatísticas Desatualizadas: O PostgreSQL depende de estatísticas para construir planos de execução eficientes. Se as estatísticas estiverem desatualizadas, o planejador pode escolher um caminho ineficiente.
- Contenção de Recursos: Problemas como altos tempos de espera de E/S, bloqueios excessivos ou memória insuficiente alocada para o PostgreSQL.
Passo 1: Identificando a Consulta Lenta
Antes de corrigir uma consulta lenta, você deve identificá-la com precisão. Confiar em reclamações de usuários é ineficiente; você precisa de dados empíricos do próprio banco de dados.
Usando pg_stat_statements
O método mais eficaz para rastrear consultas que consomem muitos recursos em um ambiente de produção é usar a extensão pg_stat_statements. Este módulo rastreia estatísticas de execução para todas as consultas executadas no banco de dados.
Habilitando a Extensão (requer privilégios de superusuário e recarga de configuração):
-- 1. Certifique-se de que está listado no postgresql.conf
-- shared_preload_libraries = 'pg_stat_statements'
-- 2. Conecte-se ao banco de dados e crie a extensão
CREATE EXTENSION pg_stat_statements;
Consultando os Principais Ofensores:
Para encontrar as consultas que consomem mais tempo total, use a seguinte consulta:
SELECT
query,
calls,
total_exec_time,
mean_exec_time,
rows
FROM
pg_stat_statements
ORDER BY
total_exec_time DESC
LIMIT 10;
Em versões mais antigas do PostgreSQL, essas colunas podem ser nomeadas total_time e mean_time. Use os nomes que seu servidor expõe.
Tempo total e tempo médio respondem a perguntas diferentes. Uma consulta que tem média de 20 milissegundos, mas é executada um milhão de vezes, pode ser o maior custo do seu banco de dados. Uma consulta executada uma vez por hora por 40 segundos pode ser dolorosa para um usuário, mas menos importante para todo o sistema. Observe ambos.
Se você precisar da consulta atualmente lenta, não da historicamente cara, verifique as sessões ativas:
SELECT pid, now() - query_start AS age, wait_event_type, wait_event, query
FROM pg_stat_activity
WHERE state = 'active'
ORDER BY query_start;
Passo 2: Analisando o Plano de Execução com EXPLAIN ANALYZE
Uma vez que uma consulta lenta é isolada, o próximo passo crítico é entender como o PostgreSQL a está executando. O comando EXPLAIN mostra o plano pretendido, mas EXPLAIN ANALYZE realmente executa a consulta e relata o tempo real gasto em cada etapa.
Sintaxe e Uso
Sempre envolva sua consulta lenta com EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON) para a saída mais detalhada. A opção BUFFERS é crucial, pois mostra a atividade de E/S de disco.
EXPLAIN (ANALYZE, BUFFERS)
SELECT *
FROM large_table lt
JOIN other_table ot ON lt.id = ot.lt_id
WHERE lt.status = 'active' AND lt.created_at > NOW() - INTERVAL '1 day';
Interpretando a Saída
A saída é lida de baixo para cima e da direita para a esquerda, pois os nós mais internos são executados primeiro. As principais métricas a serem focadas incluem:
cost=: O custo estimado do planejador, não o tempo de relógio de parede. Use-o para comparar escolhas de plano, não como milissegundos.rows=: O número estimado de linhas processadas por aquele nó.actual time=: O tempo real gasto em milissegundos nesta operação específica.rows=(Real): O número real de linhas retornadas por este nó.loops=: Quantas vezes este nó foi executado (muitas vezes alto em loops aninhados).
Detectando Ineficiências:
- Varreduras Sequenciais em Tabelas Grandes: Se o acesso a uma tabela grande usa
Seq Scanem vez de umIndex ScanouBitmap Index Scan, você provavelmente precisa de um índice melhor. - Grande Discrepância entre Linhas Estimadas e Reais: Se o planejador estimou 10 linhas, mas o nó realmente processou 1.000.000 de linhas, as estatísticas estão desatualizadas ou o planejador fez uma escolha ruim.
- Alto
actual timeem Junções/Ordenações: Tempo excessivo gasto em operaçõesHash Join,Merge JoinouSortgeralmente indica memória insuficiente (work_mem) ou incapacidade de usar índices de forma eficaz.
Observe também as linhas Buffers. shared hit significa que o PostgreSQL encontrou páginas no cache. shared read significa que ele teve que ler páginas do armazenamento. Uma consulta pode ser lenta porque o plano é ruim, ou porque o plano é razoável, mas lê uma grande quantidade de dados frios do disco.
Dica: Para planos complexos, use ferramentas online como explain.depesz.com ou o visualizador de planos de explicação do pgAdmin para interpretar os resultados graficamente.
Passo 3: Lidando com Gargalos Comuns
Com base nas descobertas do seu EXPLAIN ANALYZE, aplique correções direcionadas.
Otimização de Índices
Se Seq Scan domina uma tabela grande e a consulta é seletiva, considere índices nas colunas usadas nas cláusulas WHERE, JOIN e ORDER BY. Uma varredura sequencial não é automaticamente ruim; o PostgreSQL pode escolhê-la corretamente quando a maioria das linhas é necessária.
Exemplo: Se a consulta filtra por status e depois junta em user_id:
-- Crie um índice composto para buscas e junções mais rápidas
CREATE INDEX idx_large_table_status_user_id ON large_table (status, user_id);
Para sistemas de produção, use CREATE INDEX CONCURRENTLY quando precisar evitar bloquear escritas, e lembre-se de que ele não pode ser executado dentro de um bloco de transação normal:
CREATE INDEX CONCURRENTLY idx_large_table_status_user_id
ON large_table (status, user_id);
Atualizando Estatísticas (VACUUM ANALYZE)
Se o planejador está fazendo estimativas extremamente imprecisas (incompatibilidade entre linhas estimadas e reais), force uma atualização das estatísticas da tabela.
ANALYZE VERBOSE table_name;
-- Para tabelas altamente ativas, considere limites de autovacuum/analyze por tabela mais baixos.
Ajuste de Memória
Se operações de ordenação ou hash estão sendo derramadas no disco (muitas vezes indicado por alta E/S na saída BUFFERS ou ordenação lenta), aumente a memória de trabalho disponível do PostgreSQL.
-- Aumente work_mem no nível da sessão para o teste de consulta específico
SET work_mem = '128MB';
-- Ou globalmente no postgresql.conf para melhorias de desempenho sustentadas
Aviso: Aumentar
work_memglobalmente muito alto pode esgotar a memória do sistema se muitas consultas complexas forem executadas simultaneamente. Ajuste isso cuidadosamente com base na capacidade do servidor.
Procure por detalhes de derramamento de ordenação ou hash no plano. A saída mais recente do PostgreSQL pode mostrar métodos de ordenação como external merge Disk: ..., que é um sinal claro de que a operação excedeu a memória disponível e escreveu arquivos temporários.
Reescrevendo Consultas
Às vezes, a própria estrutura é o problema. Evite predicados não SARGable (condições que impedem o uso de índice), como aplicar funções a colunas indexadas na cláusula WHERE:
Ineficiente (impede o uso de índice):
WHERE DATE(created_at) = '2023-10-01'
Eficiente (permite o uso de índice):
WHERE created_at >= '2023-10-01 00:00:00' AND created_at < '2023-10-02 00:00:00'
Outro padrão comum é selecionar muito mais colunas do que a aplicação precisa. SELECT * torna os planos mais difíceis de otimizar, aumenta o uso de memória e pode forçar leituras extras de heap quando uma varredura apenas de índice poderia funcionar. Para caminhos críticos, liste as colunas deliberadamente.
Verificações de Bloqueio
Se EXPLAIN ANALYZE é rápido na sua sessão, mas a aplicação está lenta, a consulta pode estar esperando antes mesmo de obter tempo de execução útil. Verifique esperas de bloqueio:
SELECT pid, wait_event_type, wait_event, now() - query_start AS age, query
FROM pg_stat_activity
WHERE wait_event_type = 'Lock'
ORDER BY query_start;
Em seguida, encontre o bloqueador:
SELECT blocked.pid AS blocked_pid,
blocker.pid AS blocker_pid,
blocked.query AS blocked_query,
blocker.query AS blocker_query
FROM pg_stat_activity blocked
JOIN pg_locks blocked_locks
ON blocked_locks.pid = blocked.pid AND NOT blocked_locks.granted
JOIN pg_locks blocker_locks
ON blocker_locks.locktype = blocked_locks.locktype
AND blocker_locks.database IS NOT DISTINCT FROM blocked_locks.database
AND blocker_locks.relation IS NOT DISTINCT FROM blocked_locks.relation
AND blocker_locks.transactionid IS NOT DISTINCT FROM blocked_locks.transactionid
AND blocker_locks.pid <> blocked_locks.pid
JOIN pg_stat_activity blocker
ON blocker.pid = blocker_locks.pid;
A correção pode ser no nível da aplicação: transações mais curtas, mover chamadas lentas de API externa para fora da transação, evitar SELECT ... FOR UPDATE desnecessário ou alterar a ordem em que as tabelas são atualizadas para que transações concorrentes não entrem em deadlock.
Um Pequeno Exemplo: A Consulta Lenta do Dashboard
Suponha que um dashboard execute esta consulta a cada poucos segundos:
SELECT *
FROM orders
WHERE DATE(created_at) = CURRENT_DATE
AND status = 'paid'
ORDER BY created_at DESC
LIMIT 50;
A tabela tem milhões de linhas. EXPLAIN ANALYZE mostra uma varredura sequencial, um grande número de linhas removidas pelo filtro e uma ordenação. O primeiro instinto pode ser indexar created_at, mas o predicado envolve a coluna em DATE(created_at), então um índice normal em created_at é menos útil.
Reescreva o filtro de data como um intervalo:
SELECT id, customer_id, total_cents, created_at
FROM orders
WHERE created_at >= CURRENT_DATE
AND created_at < CURRENT_DATE + INTERVAL '1 day'
AND status = 'paid'
ORDER BY created_at DESC
LIMIT 50;
Em seguida, considere um índice que corresponda ao filtro e à ordenação:
CREATE INDEX CONCURRENTLY idx_orders_paid_created_at
ON orders (created_at DESC)
WHERE status = 'paid';
Esta não é uma receita de índice universal. Funciona quando paid é um filtro comum do dashboard e quando os pedidos pagos mais recentes são o que a aplicação geralmente solicita. Se a aplicação também filtra fortemente por conta, o melhor índice pode começar com account_id. O ponto é projetar o índice em torno do padrão de acesso real, não em torno de uma única coluna mencionada na consulta.
Após a alteração, o plano deve mostrar menos linhas varridas e, idealmente, evitar uma ordenação explícita. Se o plano ainda escolher uma varredura sequencial, verifique se o intervalo de datas é muito amplo, se as estatísticas estão desatualizadas ou se os parâmetros da consulta em produção diferem do seu caso de teste.
Passo 4: Verificação e Monitoramento
Após implementar uma alteração, execute novamente EXPLAIN ANALYZE na mesma consulta com parâmetros comparáveis. O objetivo nem sempre é ver uma varredura de índice. O objetivo é ver menos trabalho: menos linhas removidas por filtros, menos buffers lidos, sem derramamento de disco, melhores estimativas de linhas ou menos tempo gasto no nó caro.
Continue monitorando pg_stat_statements para confirmar que a consulta modificada não está mais aparecendo na lista dos principais ofensores, garantindo que a correção tenha um impacto global positivo.
Observe também o custo de escrita após adicionar índices. Cada novo índice deve ser mantido durante inserções, atualizações e exclusões. Um índice de leitura perfeito para um dashboard pode ser uma má troca se ele desacelerar um caminho de ingestão de alto volume. Para tabelas importantes, verifique ambos os lados: a consulta lenta melhorou e a latência de escrita ou o inchaço da tabela pioraram depois?
Mais um hábito ajuda durante incidentes reais: teste com valores de parâmetros realistas. O PostgreSQL pode escolher planos diferentes para um cliente com dez linhas e um cliente com dez milhões de linhas. Se a aplicação usa instruções preparadas, planos genéricos também podem se comportar de forma diferente da consulta avulsa que você cola no psql. Quando o problema de produção afeta um locatário, uma conta ou um intervalo de datas, reproduza essa forma o mais próximo possível em um ambiente seguro.
Se a consulta for destrutiva ou muito cara para executar com EXPLAIN ANALYZE, comece com EXPLAIN simples, execute em staging ou envolva o teste em uma transação que você desfaz. Para UPDATE e DELETE, você ainda pode aprender muito com a parte de varredura e junção do plano antes de fazer qualquer alteração em produção.
Mantenha uma pequena nota com o plano antes e depois, o tempo e o motivo da alteração. Esse hábito previne regressões acidentais mais tarde e dá à próxima pessoa uma explicação real em vez de um nome de índice misterioso no esquema.