Uma consulta lenta costuma parecer um problema localizado: ela demora, recebe timeout e a aplicação segue atendendo o restante. Só que algumas queries conseguem pressionar a memória até afetar outras conexões, matar processos e, dependendo da proteção oferecida pelo ambiente, empurrar o PostgreSQL inteiro para uma recuperação após crash.
O ponto mais traiçoeiro é que work_mem não funciona como um teto de memória por consulta. A documentação do PostgreSQL define o parâmetro como um limite base por operação de sort ou hash antes que a execução comece a usar arquivos temporários. Uma mesma query pode ter várias dessas operações, executadas por vários processos, enquanto outras sessões fazem o mesmo.
Quatro megabytes podem virar muito mais
O valor padrão de work_mem é 4 MB. Isso não significa que uma consulta receberá no máximo 4 MB. Um plano pode conter um sort para ORDER BY, outro para um merge join, uma agregação por hash e uma tabela de hash para o join. Cada nó elegível tem seu próprio orçamento.
Operações de hash ainda recebem outro multiplicador. O limite usado por elas é calculado a partir de work_mem * hash_mem_multiplier, cujo padrão atual é 2. Com as configurações padrão, uma única hash table pode chegar a aproximadamente 8 MB antes de derramar parte do trabalho para o disco.
SHOW work_mem;
SHOW hash_mem_multiplier;
EXPLAIN (ANALYZE, BUFFERS)
SELECT customer_id, sum(total)
FROM orders
GROUP BY customer_id
ORDER BY sum(total) DESC;
O EXPLAIN ANALYZE ajuda a encontrar Sort Method, Memory Usage, quantidade de batches e uso de disco. Ele deve ser usado com cuidado em produção porque executa a consulta de verdade. Para queries que alteram dados, a inspeção exige ainda mais cautela e uma estratégia explícita de transação.
Paralelismo multiplica o orçamento
Quando o planner escolhe um plano paralelo, os nós abaixo de Gather ou Gather Merge são executados por workers. O processo líder também pode participar do trabalho. Como cada worker é um processo separado, limites como work_mem são aplicados individualmente.
A documentação chama atenção para esse efeito: uma consulta com quatro workers pode consumir até cinco vezes os recursos de uma execução sem paralelismo, contando o líder. Isso não torna a execução paralela ruim. Ela pode reduzir bastante a latência, mas precisa entrar na conta de capacidade do servidor.
Agora coloque concorrência nessa equação. Dez conexões executando planos com múltiplos sorts, hashes e workers não compartilham um único balde de work_mem. É por isso que aumentar o parâmetro globalmente para eliminar arquivos temporários pode trocar um gargalo de I/O por picos perigosos de RAM.
Nem toda estrutura consegue escapar para o disco
Muitas operações conseguem dividir dados em lotes e continuar no disco quando a memória fica curta. Outras estruturas internas precisam manter o estado durante toda a consulta. Um teste publicado pela ClickHouse em 28 de setembro explorou um desses casos usando uma CTE recursiva com UNION.
Para remover duplicatas entre iterações, a execução mantém uma tabela com valores já visitados. No cenário construído para o benchmark, essa estrutura crescia até cerca de 1 GiB por consulta e não obedecia a um mecanismo útil de spill baseado em work_mem. Com várias conexões concorrentes, o teste conseguiu diferenciar ambientes que encerravam consultas, derrubavam sessões ou deixavam o sistema operacional matar processos por falta de memória.
O resultado merece contexto: o artigo foi produzido por um fornecedor de PostgreSQL gerenciado e seu próprio produto venceu a comparação. A metodologia e a explicação do executor são úteis, mas os números não devem virar um ranking universal. Configuração da instância, supervisores, limites do sistema operacional, armazenamento e escolhas do provedor mudam bastante o comportamento.
Por que um processo morto pode reiniciar o cluster
O PostgreSQL usa processos separados para atender conexões. Se um backend termina de forma anormal, o postmaster precisa assumir que a memória compartilhada pode ter ficado inconsistente. O caminho seguro é encerrar os demais processos e iniciar a recuperação. Para a aplicação, uma query problemática deixa de ser apenas uma requisição com erro e passa a derrubar conexões saudáveis.
Serviços gerenciados tentam evitar esse estágio com estratégias diferentes. Alguns encerram a consulta, outros matam a sessão antes que o kernel intervenha, e há ambientes que colocam limites mais conservadores para preservar o cluster. Essa camada de proteção é uma característica operacional importante, mas raramente aparece na primeira tela de comparação entre provedores.
Como reduzir o raio de explosão
Não existe um número mágico para work_mem. O valor adequado depende dos planos, da concorrência e da memória disponível. Ainda assim, algumas práticas deixam o risco mais visível:
- Dimensione pela concorrência: considere operações por plano, workers paralelos e número de sessões simultâneas, não apenas uma query isolada.
- Evite aumentar o valor global por reflexo: uma configuração maior pode ser útil em uma sessão de relatório e perigosa como padrão de toda a aplicação.
- Observe spills e planos: arquivos temporários, sorts externos, batches de hash e mudanças de plano conforme os dados crescem são sinais úteis.
- Coloque limites no trabalho:
statement_timeoutcontém duração, enquanto pool de conexões e limites do ambiente ajudam a controlar concorrência e memória. Nenhum deles substitui os demais. - Teste com volume parecido com produção: um plano barato com milhares de linhas pode mudar de forma quando a tabela chega a milhões.
- Entenda a proteção do provedor: pergunte o que acontece quando uma query excede a RAM e quanto tempo o serviço leva para se recuperar.
Também vale separar perfis de carga. Endpoints transacionais, relatórios, jobs e consultas administrativas não precisam usar o mesmo pool nem as mesmas configurações. Permitir que uma tela de análise dispute memória sem controle com o tráfego principal é uma decisão arquitetural, mesmo quando aconteceu por falta de decisão.
A lição prática não é desconfiar de toda CTE recursiva ou desligar o paralelismo. É parar de tratar work_mem como a memória máxima de uma query. Quando o banco está sob concorrência, a unidade real de consumo aparece espalhada pelo plano, pelos workers e pelas conexões. A configuração só fica segura quando essa multiplicação entra na conta.
Faça a conta por operação concorrente
Uma estimativa conservadora começa com conexões ativas × operações de memória por plano × work_mem e depois considera workers paralelos. Não use max_connections × work_mem como previsão exata, mas use o exercício para perceber se uma configuração global cabe no pior pico plausível.
40 consultas concorrentes
× 3 nós de sort/hash
× 2 workers paralelos
× 64 MB de work_mem
= 15 GB de orçamento potencial
O valor real depende do plano e alguns nós não atingem o limite, mas a conta impede tratar 64 MB como custo da conexão inteira. Compare o resultado com RAM disponível depois de shared_buffers, sistema operacional e outros processos.
Ajuste localmente e deixe evidência
Para uma consulta conhecida, prefira SET LOCAL work_mem dentro da transação, ou uma role específica, em vez de elevar o cluster inteiro. Ative log_temp_files com um limiar útil, acompanhe pg_stat_statements e compare EXPLAIN (ANALYZE, BUFFERS) com cuidado em ambiente seguro.
Memória maior não corrige uma consulta que faz trabalho desnecessário. Um N+1 pode multiplicar operações pequenas; uma paginação por offset profundo obriga o banco a percorrer linhas que serão descartadas. Primeiro reduza o trabalho, depois dê memória ao que restou.
