Autovacuum, bloat e tuplas mortas no Postgres em operações
Postgres nunca deleta linhas in place — updates e deletes deixam tuplas mortas que autovacuum deve reclamar. Quando vacuum fica para trás, tabelas incham, sequential scans desaceleram, índices crescem e wraparound de transaction ID vira ameaça existencial.
A latência de consultas dobrou. EXPLAIN mostrou Index Scan — o mesmo plano do mês passado. A taxa de acerto do buffer cache parecia boa. A tabela tinha quatro milhões de linhas vivas e trinta e oito milhões de tuplas mortas que autovacuum não havia reclamado porque uma consulta analítica de longa duração bloqueou a limpeza por onze dias. Autovacuum, bloat e gestão de tuplas mortas no Postgres não são trivia de DBA — são operações de produção para qualquer equipe rodando cargas OLTP com updates, soft deletes ou alto churn. Vacuum é como Postgres reclama espaço, atualiza visibility maps e previne wraparound de transaction ID. Quando fica para trás, performance degrada gradualmente até degradar de repente.
O controle de concorrência multi-versão (MVCC) do Postgres mantém versões antigas de linhas visíveis para transações em andamento. Um UPDATE cria nova versão de linha e marca a antiga como morta. Um DELETE marca linhas mortas sem removê-las imediatamente. Tuplas mortas ocupam disco, enchem buffers em sequential scans e incham índices até vacuum reclamá-las. Autovacuum é o worker em background que deveria manter baixa a fração de tuplas mortas sem intervenção manual — quando está ajustado e desbloqueado.
How dead tuples accumulate and why autovacuum exists
Cada statement mutante produz tuplas mortas:
| Operation | Live rows | Dead tuples after |
|---|---|---|
| INSERT | +1 nova | 0 |
| UPDATE | +1 nova versão | 1 morta (versão antiga) |
| DELETE | 0 | 1 morta |
| Soft delete (UPDATE flag) | 1 atualizada | 1 morta (versão antiga) |
Tabelas de alto churn — sessões, notificações, filas de jobs, staging de auditoria — geram tuplas mortas mais rápido que tabelas majoritariamente append-only. Sem vacuum, o tamanho do arquivo da tabela cresce monotonicamente enquanto a contagem de linhas vivas permanece estável. Consultas escaneiam tuplas mortas mesmo quando cláusulas WHERE as filtram — checagens de visibilidade custam CPU e I/O.
Autovacuum lança workers quando tabelas excedem limiares de tuplas mortas:
autovacuum threshold = autovacuum_vacuum_threshold
+ autovacuum_vacuum_scale_factor * reltuples
O scale factor padrão de 0.2 significa que vacuum dispara após 20% das linhas vivas estimadas mortas — em tabela de dez milhões de linhas, dois milhões de tuplas mortas antes da limpeza começar. Tabelas OLTP de alto churn costumam precisar scale factors mais baixos.
Autovacuum não é manutenção opcional. É parte da higiene do write path do Postgres.
Vacuum também congela transaction IDs em linhas antigas, marcando-as seguras contra wraparound de transaction ID — modo de falha que pode forçar shutdown para proteger integridade de dados quando o cluster se aproxima do limite de dois bilhões de transações.
Bloat: table heap, indexes, and visibility maps
Heap bloat — arquivos de tabela maiores que os dados vivos exigem. Mensurável via extensões ou consultas de estimação comparando pg_relation_size com a pegada esperada de linhas vivas. Bloat causa mais páginas lidas por consulta, menor eficiência de cache e janelas de backup mais longas.
Index bloat — índices retêm ponteiros para tuplas mortas do heap até vacuum reclamar ambos. Índices inchados aumentam I/O de scan; padrões de índice composto entregam menos benefício quando páginas estão esparsas com entradas mortas.
Visibility map — quando atualizado, index-only scans pulam fetches do heap. Vacuum atrasado reduz oportunidades de index-only scan.
Bloat não se corrige sozinho sem vacuum. INSERT sozinho não reclama espaço de tuplas mortas no meio de páginas — só vacuum marca espaço reutilizável. Bloat extremo pode exigir VACUUM FULL ou reescrita estilo pg_repack — ambos com custo operacional e implicações de lock discutidas abaixo.
Autovacuum tuning for production OLTP
Configurações padrão de autovacuum servem para bancos de desenvolvimento de baixo churn. OLTP de produção com updates precisa de tuning global ou por tabela:
-- Aggressive tuning for high-churn table
ALTER TABLE notification_events SET (
autovacuum_vacuum_scale_factor = 0.02,
autovacuum_vacuum_threshold = 1000,
autovacuum_analyze_scale_factor = 0.01,
autovacuum_vacuum_cost_delay = 2
);Parâmetros-chave:
| Parameter | Purpose | Tuning direction for churn |
|---|---|---|
autovacuum_vacuum_scale_factor | % de tuplas mortas que dispara | Mais baixo (0.01–0.05) em tabelas quentes |
autovacuum_vacuum_threshold | Mínimo de tuplas mortas antes do trigger | Subir levemente em tabelas pequenas para evitar vacuum constante |
autovacuum_vacuum_cost_delay | Limita I/O durante vacuum | Menor delay = vacuum mais rápido, mais impacto em I/O |
autovacuum_max_workers | Workers de vacuum em paralelo | Aumentar em multi-core com muitas tabelas inchadas |
autovacuum_naptime | Intervalo de poll do launcher | Mais baixo (10s) quando muitas tabelas precisam de atenção |
Mudanças globais via postgresql.conf ou ALTER SYSTEM; por tabela via ALTER TABLE ... SET para outliers sem afetar todo o cluster.
Analyze roda junto com vacuum — estatísticas obsoletas causam planos ruins que parecem índices faltantes. Tabelas com churn pesado precisam de scale factors de analyze agressivos também.
Monitorar atividade de autovacuum:
SELECT relname, last_vacuum, last_autovacuum, last_analyze, last_autoanalyze,
n_live_tup, n_dead_tup,
round(n_dead_tup::numeric / NULLIF(n_live_tup + n_dead_tup, 0) * 100, 2) AS dead_pct
FROM pg_stat_user_tables
ORDER BY n_dead_tup DESC
LIMIT 20;Alertar quando dead_pct exceder limiar (comumente 10–20% sustentado) ou last_autovacuum for mais antigo que o esperado para a taxa de churn.
What blocks vacuum and how to unblock it
Autovacuum não pode reclamar tuplas mortas ainda visíveis para qualquer transação aberta. Bloqueadores:
Long-running transactions. Consulta analítica aberta por horas mantém horizonte xmin — todas as tuplas mortas desde o início da consulta permanecem. Solução: idle_in_transaction_session_timeout, timeouts de statement em roles OLTP, rotear consultas longas para réplicas.
Replication slots. Slots de replicação lógica impedem que vacuum remova tuplas que réplicas não consumiram. Slot atrasado → vacuum bloqueado → bloat cresce no publisher. Monitorar lag em bytes e idade de pg_replication_slots.
Prepared transactions. Sobras de commit em duas fases bloqueiam limpeza. Auditar pg_prepared_xacts.
Lock contention. Autovacuum espera locks de tabela retidos por DDL ou DML longo. Raro em OLTP mas comum durante migrações zero-downtime se migrações retêm locks fortes.
Consulta de diagnóstico para bloqueadores:
SELECT pid, state, query_start, xact_start, query
FROM pg_stat_activity
WHERE state != 'idle'
AND backend_xmin IS NOT NULL
ORDER BY xact_start
LIMIT 10;O xact_start mais antigo entre transações ativas define até onde vacuum pode avançar. Runbooks de operações devem incluir matar sessões idle-in-transaction descontroladas e escalar lag de replication slot antes de bloat virar latência visível ao cliente.
Manual vacuum, VACUUM FULL, and pg_repack tradeoffs
Routine VACUUM — seguro, concorrente, reclama tuplas mortas para reutilização dentro do arquivo da tabela, não devolve espaço ao SO na maioria dos casos. Executar manualmente quando autovacuum ficou para trás após recuperação de incidente:
VACUUM (VERBOSE, ANALYZE) notification_events;VACUUM FULL — reescreve tabela inteira, lock exclusivo, devolve espaço ao SO. Risco de downtime em tabelas grandes. Reservar para janelas de manutenção ou tabelas pequenas para lockear brevemente. Não é resposta rotineira a bloat.
pg_repack / pg_squeeze — reescrita online com locking mínimo. Overhead operacional de instalar extensão e agendar. Justificado quando heap bloat excede 50% e VACUUM não reduz arquivo porque páginas estão esparsas mas não vazias o suficiente para reutilização.
Prevenção supera cura: autovacuum ajustado, sem transações longas no primary, monitoramento de replication slot. Reescrita manual é recuperação cara, não estado estável.
Wraparound vacuum and the emergency tier
Transaction IDs do Postgres são de 32 bits e fazem wrap. Linhas mais antigas que autovacuum_freeze_max_age transações devem ser congeladas. Autovacuum lança vacuum anti-wraparound agressivamente quando tabelas se aproximam do limite de idade — visível em logs como autovacuum: vacuum em tabelas com alta idade de relfrozenxid.
Se vacuum anti-wraparound não consegue acompanhar — bloqueado por transações longas, throttling de I/O ou workers insuficientes — o cluster entra em aviso e eventualmente transaction ID wraparound protection shutdown para prevenir corrupção de dados. É emergência de parada total, não degradação gradual.
Monitorar:
SELECT datname, age(datfrozenxid) AS db_age,
current_setting('autovacuum_freeze_max_age')::bigint AS max_age
FROM pg_database
ORDER BY age(datfrozenxid) DESC;Alertar em 50% e 75% de autovacuum_freeze_max_age. Incidentes de wraparound remontam a sinais de lag de autovacuum ignorados semanas antes.
Operations runbook: daily signals and incident response
Integrar saúde de vacuum em monitoramento padrão de banco:
Daily dashboard
- Tabelas top por
n_dead_tupe percentual morto - Tempo desde
last_autovacuumem tabelas de maior churn - Idade da transação ativa mais antiga
- Lag de replication slot (se replicação lógica habilitada)
- Idade de
frozenxidde banco e tabela
Incident response: checar ratio de tuplas mortas e transações bloqueadoras primeiro; executar VACUUM ANALYZE manual se autovacuum estagnou; agendar pg_repack só quando bloat exceder 2× pegada de dados vivos. Preventive: autovacuum por tabela em tabelas quentes no schema time, idle_in_transaction_session_timeout em roles de app, analytics em réplicas não no primary.
How should teams manage Postgres vacuum in production?
Decisões operacionais que evitam bloat virar incidentes de latência.
When should autovacuum settings be per-table vs global?
Por tabela para outliers — filas de notificação, armazéns de sessão, tabelas de status de jobs com padrões update-heavy. Defaults globais para schema de churn moderado. Documentar ajustes por tabela em comentários de migração para que futuros engenheiros saibam por que existem.
Is dead tuple percentage or absolute count more useful for alerting?
Ambos. Alto percentual em tabelas pequenas importa menos que percentual moderado em tabelas de bilhão de linhas. Alertar sobre n_dead_tup absoluto acima de limiar E percentual acima de limiar para tabelas acima de cutoff de tamanho.
Can autovacuum be too aggressive?
Sim. cost_delay muito baixo em instâncias saturadas de I/O compete com consultas OLTP. Ajustar durante headroom de I/O observado — aumentar delay se vacuum causa picos de latência, diminuir se tuplas mortas se acumulam. Medir, não adivinhar.
When is bloat bad enough to require rewrite?
Quando tamanho da tabela excede 1.5–2× tamanho estimado de dados vivos E vacuum não reduz tamanho do arquivo E I/O de consultas correlaciona com métricas de bloat da tabela. Abaixo desse limiar, autovacuum ajustado costuma bastar.
A common argument runs the other way
A visão oposta sustenta que armazenamento SSD moderno é barato — deixar tabelas crescerem, adicionar disco quando necessário e pular complexidade de tuning de vacuum.
Custo de disco é a menor linha. Poluição do buffer cache não é barata — tabelas inchadas desalojam páginas úteis, elevando I/O de leitura em toda a carga. Bloat de índices multiplica amplificação de escrita em cada update. Janelas de backup e restore crescem com tamanho de arquivo independentemente da contagem de linhas vivas. Shutdown de proteção wraparound não se resolve provisionando mais terabytes. Tuning de vacuum é higiene operacional que preserva latência previsível — escalar armazenamento é controle compensatório que chega depois que performance já degradou.
Key takeaways
- Updates e deletes criam tuplas mortas; autovacuum as reclama e congela transaction IDs.
- Scale factor padrão de autovacuum (20%) é preguiçoso demais para OLTP de alto churn — ajustar por tabela.
- Transações longas e replication slots atrasados bloqueiam vacuum — monitorar xact mais antigo e lag de slot.
- Bloat degrada eficiência de heap e índice; index-only scans sofrem quando visibility maps atrasam.
VACUUM FULLlockea em exclusiva — prevenção via autovacuum supera reescrita de emergência.- Monitoramento de idade wraparound previne o único modo de falha que desliga Postgres por completo.
- Métricas de tuplas mortas pertencem ao mesmo dashboard que latência de consultas e lag de replicação.
Conclusion
Autovacuum é invisível quando funciona e conspícuo quando não — usualmente como crescimento inexplicável de latência em tabelas cujos planos de consulta nunca mudaram. Maturidade operacional de Postgres inclui monitoramento de tuplas mortas, tuning de autovacuum por tabela para hotspots de churn e passos de runbook para vacuum bloqueado antes de bloat exigir cirurgia de reescrita.
A primeira auditoria: dez tabelas top por contagem de tuplas mortas, transação ativa mais antiga no primary, lag de replication slot se aplicável, idade frozenxid por banco. Achados não zero em múltiplas colunas indicam dívida de vacuum acumulando mais rápido que autovacuum consegue pagar — ajustar antes de avisos de wraparound virarem páginas de outage.