Performance
Autovacuum, bloat e wraparound: o que vigiar no PostgreSQL
Tabela que cresce sem ganhar linhas é bloat, e bloat quase sempre é autovacuum impedido. Como diagnosticar antes de virar parada emergencial.
Se uma tabela do PostgreSQL ocupa muito mais espaço do que as linhas vivas justificam, o autovacuum está sendo impedido de limpar — e o motivo raramente está na configuração do autovacuum. Está numa transação aberta, num slot de replicação parado ou numa tabela grande demais para os parâmetros padrão. O sintoma é espaço em disco e queries lentas; a causa é outra coisa.
Esse é o efeito colateral do modelo MVCC. Um UPDATE no PostgreSQL não altera a linha: ele grava uma nova versão e marca a anterior como morta. Um DELETE só marca. Quem recolhe as versões mortas é o VACUUM, e enquanto ele não passa, elas continuam ocupando páginas que toda leitura sequencial precisa atravessar.
Como o autovacuum decide agir
O autovacuum não roda por horário. Ele compara o número de tuplas mortas com um limiar calculado por tabela:
limiar = autovacuum_vacuum_threshold + autovacuum_vacuum_scale_factor × linhas da tabela
Os padrões são 50 tuplas para o limiar base e 20% para o fator de escala. Em uma tabela de mil linhas, isso significa vacuum a cada ~250 alterações — frequente. Em uma tabela de cem milhões de linhas, significa esperar vinte milhões de tuplas mortas antes de qualquer limpeza.
É por isso que o bloat aparece justamente nas tabelas maiores e mais movimentadas: o padrão foi desenhado para o caso pequeno. A correção é por tabela, não global:
ALTER TABLE pedidos SET ( autovacuum_vacuum_scale_factor = 0.01, autovacuum_vacuum_threshold = 1000);
Existe um caminho paralelo para tabelas que quase só recebem INSERT, governado por autovacuum_vacuum_insert_threshold (padrão 1000) e seu fator de escala. Sem ele, uma tabela append-only nunca acumularia tuplas mortas suficientes para ser visitada — e ficaria sem receber o congelamento que o item sobre wraparound explica adiante.
Quando o autovacuum roda e mesmo assim não limpa
Este é o caso que confunde. pg_stat_user_tables mostra last_autovacuum recente, o processo aparece em pg_stat_activity, e ainda assim n_dead_tup continua subindo.
O VACUUM só pode remover uma versão de linha que nenhuma transação ainda possa enxergar. Se existe no sistema algo segurando um snapshot antigo, todas as versões posteriores a ele ficam intocáveis — em toda a instância, não só na tabela em questão. Os três suspeitos:
Transação aberta e ociosa. O clássico idle in transaction: a aplicação abriu a transação, não fez COMMIT e foi cuidar de outra coisa.
SELECT pid, state, xact_start, now() - xact_start AS duracao, queryFROM pg_stat_activityWHERE state IN ('idle in transaction', 'active') AND xact_start IS NOT NULLORDER BY xact_start;
idle_in_transaction_session_timeout encerra essas sessões automaticamente e evita que o problema volte.
Slot de replicação parado. Um slot cujo consumidor sumiu — réplica desligada, ferramenta de CDC removida sem limpar o slot — segura o xmin indefinidamente e ainda impede a reciclagem de WAL, com o risco adicional de encher o disco.
SELECT slot_name, active, wal_status, pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS wal_retidoFROM pg_replication_slots;
Standby com hot_standby_feedback = on. Consultas longas na réplica seguram versões no primário. É o comportamento contratado — evita conflito de replicação — mas precisa ser considerado no diagnóstico.
Bloat já instalado: cuidado com a "solução"
VACUUM comum devolve espaço para reuso interno da própria tabela, mas não devolve disco ao sistema operacional. Para isso existe o VACUUM FULL — e ele não é uma operação online. Ele toma lock ACCESS EXCLUSIVE, bloqueando inclusive leituras, e reescreve a tabela inteira em um novo arquivo, o que exige espaço livre próximo ao tamanho do objeto durante a execução. Em tabela grande e sistema em operação, isso é uma parada.
Quando a janela não existe, pg_repack faz a reorganização com lock exclusivo apenas em trechos curtos. Não é nativo, precisa ser instalado e testado antes, e essa avaliação é melhor feita fora do incidente.
Wraparound: o limite que não se negocia
O PostgreSQL identifica transações com um contador circular de 32 bits. Para que versões antigas continuem visíveis depois que o contador dá a volta, o VACUUM marca linhas suficientemente antigas como congeladas. Se o congelamento não acontece, a instância caminha para o wraparound.
Quando a idade de uma tabela ultrapassa autovacuum_freeze_max_age, o autovacuum inicia uma passagem anti-wraparound que não pode ser adiada e não respeita a janela de manutenção. Ela aparece em pg_stat_activity como autovacuum: VACUUM ... (to prevent wraparound), consome I/O e não deve ser cancelada — cancelar apenas empurra o problema para a próxima tentativa, mais perto do limite.
Se o congelamento continuar não acontecendo, o PostgreSQL passa a recusar novas transações de escrita para proteger os dados. A recuperação exige VACUUM com o banco em modo restrito. É uma indisponibilidade evitável com monitoramento simples:
SELECT c.relname, age(c.relfrozenxid) AS idade_xid, pg_size_pretty(pg_total_relation_size(c.oid)) AS tamanhoFROM pg_class AS cJOIN pg_namespace AS n ON n.oid = c.relnamespaceWHERE c.relkind IN ('r', 'm') AND n.nspname NOT IN ('pg_catalog', 'information_schema')ORDER BY age(c.relfrozenxid) DESCLIMIT 20;
Acompanhar essa idade ao longo do tempo mostra a tendência com semanas de antecedência. É exatamente o tipo de indicador com poder preditivo que discutimos em quais métricas realmente antecipam um incidente.
Um roteiro curto de verificação
Antes de mexer em parâmetros, responda nesta ordem: existe transação ou slot segurando xmin? As tabelas grandes têm configuração própria de autovacuum ou herdaram os 20%? A idade de transação das maiores tabelas está estável ou subindo? O autovacuum consegue terminar, ou é cancelado por locks e recomeça do zero?
Ajustar autovacuum_max_workers sem passar por essas perguntas costuma só distribuir o mesmo problema entre mais processos.
Próximo passo
Bloat e wraparound são problemas de tendência: previsíveis com antecedência, caros quando viram emergência.
A H1 Data faz um diagnóstico gratuito do ambiente PostgreSQL com leitura de bloat por tabela, efetividade do autovacuum, idade de transação e riscos de replicação, com prioridade do que tratar primeiro.