VACUUM e bloat no PostgreSQL: o inimigo silencioso
MVCC cobra o preço em tuplas mortas. Em tabelas de alta escrita o autovacuum não acompanha, o bloat degrada tudo devagar e o wraparound espera na esquina. Como medir, ajustar e corrigir — com SQL.

Todo DBA que vem do Oracle ou do SQL Server leva o mesmo susto com o PostgreSQL: aqui, um UPDATE não atualiza a linha. Ele escreve uma versão nova e marca a antiga como morta. É o MVCC fazendo o trabalho dele — leitor nunca bloqueia escritor, escritor nunca bloqueia leitor. Lindo. E caro.
O preço é que alguém precisa recolher esse lixo. Esse alguém é o VACUUM. E quando o volume cresce — 10 TB, 100 TB, 300 TB —, as suposições que funcionavam no seu banco de 50 GB simplesmente param de valer.
Primeiro: meça, não adivinhe
Bloat é chute na maioria dos blogs. Comece com o que o Postgres já sabe:
SELECT relname AS tabela,
n_live_tup AS vivos,
n_dead_tup AS mortos,
round(100.0 * n_dead_tup / NULLIF(n_live_tup + n_dead_tup, 0), 1) AS pct_morto,
last_autovacuum,
last_autoanalyze,
pg_size_pretty(pg_total_relation_size(relid)) AS tamanho
FROM pg_stat_user_tables
WHERE n_dead_tup > 10000
ORDER BY n_dead_tup DESC
LIMIT 20;
Para a medição séria, use a extensão oficial:
CREATE EXTENSION IF NOT EXISTS pgstattuple;
SELECT * FROM pgstattuple('public.pedidos');
SELECT * FROM pgstatindex('public.idx_pedidos_cliente');
Regra de bolso que uso: até 20% de espaço livre é saudável. Acima de 40%, tem algo errado no autovacuum. Acima de 60%, o índice está fazendo você ler disco à toa em toda query.
Por que o autovacuum não dá conta
O padrão do autovacuum é conservador — foi desenhado para não atrapalhar. Ele dispara quando as tuplas mortas passam de autovacuum_vacuum_threshold mais 20% do número de linhas. Traduzindo: numa tabela de 500 milhões de linhas, ele só acorda com 100 milhões de tuplas mortas. A essa altura o estrago está feito.
A correção não é global, é por tabela. Tabela grande e quente merece tratamento próprio:
ALTER TABLE pedidos SET (
autovacuum_vacuum_scale_factor = 0.01, -- 1% em vez de 20%
autovacuum_vacuum_threshold = 5000,
autovacuum_analyze_scale_factor = 0.005,
autovacuum_vacuum_cost_delay = 2, -- ms; menor = mais agressivo
autovacuum_vacuum_cost_limit = 2000
);
-- Mais workers, se a máquina aguenta
ALTER SYSTEM SET autovacuum_max_workers = 6;
ALTER SYSTEM SET autovacuum_naptime = '15s';
ALTER SYSTEM SET maintenance_work_mem = '2GB';
SELECT pg_reload_conf();
Detalhe que muita gente erra: autovacuum_max_workers exige restart, e o cost_limit é dividido entre os workers ativos. Subir worker sem subir limite deixa cada um mais lento.
Os três bloqueadores do VACUUM
Às vezes o autovacuum roda e o bloat não cai. Nesses casos, sempre é um destes três segurando o horizonte de limpeza:
-- 1. Transacao longa aberta (o classico BEGIN esquecido no cliente)
SELECT pid, state, now() - xact_start AS idade, left(query, 60)
FROM pg_stat_activity
WHERE xact_start IS NOT NULL
ORDER BY xact_start
LIMIT 10;
-- 2. Slot de replicacao parado
SELECT slot_name, active, xmin, catalog_xmin FROM pg_replication_slots;
-- 3. Prepared transaction esquecida (two-phase commit orfa)
SELECT * FROM pg_prepared_xacts;
Enquanto qualquer um desses segurar um xmin antigo, o VACUUM pode rodar o dia inteiro que não vai liberar nada. Já perdi uma madrugada nisso antes de aprender a olhar aqui primeiro. E sim: hot_standby_feedback = on na réplica entra nessa lista — ela empurra o xmin dela para o primário, o que resolve conflito de query e cria bloat. A escolha está explicada em alta disponibilidade e replicação.
Wraparound: o problema que para o banco
O contador de transaction ID tem 32 bits. Se a idade de uma tabela chegar perto de 2 bilhões sem VACUUM de freeze, o PostgreSQL entra em modo de proteção e recusa novas escritas. Não é lentidão: é banco parado. Monitore como se fosse alarme de incêndio:
SELECT relname,
age(relfrozenxid) AS idade_xid,
round(100.0 * age(relfrozenxid) / 2000000000, 1) AS pct_do_limite
FROM pg_class
WHERE relkind = 'r'
ORDER BY age(relfrozenxid) DESC
LIMIT 10;
Acima de 50% do limite, investigue. Acima de 80%, agende janela. O PostgreSQL 17 melhorou bastante a memória de trabalho do vacuum, com menos passadas em índices grandes, mas melhoria não substitui monitoramento.
Como tirar o bloat que já existe
| Ferramenta | Bloqueia escrita? | Quando usar |
|---|---|---|
VACUUM FULL | Sim, lock exclusivo | Só com janela de manutenção. Precisa de espaço em disco igual à tabela |
REINDEX CONCURRENTLY | Não | Índice inchado, caso mais comum. Nativo desde o PG 12 |
pg_repack | Praticamente não | Tabela inchada em produção 24x7 |
| Particionamento | - | Prevenção: DROP de partição é instantâneo e VACUUM de partição é barato |
-- O que resolve 80% dos casos, sem downtime
REINDEX INDEX CONCURRENTLY idx_pedidos_cliente;
REINDEX TABLE CONCURRENTLY pedidos;
A virada de chave em escala grande
Acima de alguns terabytes, a resposta deixa de ser "ajustar o autovacuum" e passa a ser arquitetura. Tabela de log de 300 TB não se limpa: se particiona por tempo e se descarta partição. Um DROP de partição custa milissegundos; um DELETE de 2 bilhões de linhas gera 2 bilhões de tuplas mortas e uma semana de VACUUM.
CREATE TABLE eventos (
id bigserial,
criado_em timestamptz NOT NULL,
payload jsonb
) PARTITION BY RANGE (criado_em);
CREATE TABLE eventos_2026_09 PARTITION OF eventos
FOR VALUES FROM ('2026-09-01') TO ('2026-10-01');
-- Expurgo instantaneo, sem bloat
DROP TABLE eventos_2026_03;
Continue pelo guia definitivo de PostgreSQL em produção. Bloat também envenena estatística, o que leva direto ao query planner imprevisível. E se o seu problema é volume em outro banco, veja MySQL com grandes volumes de dados.
Sobre o autor: Alexandre Almeida atua com bancos de dados há mais de 25 anos, foi instrutor oficial de MySQL e Oracle para mais de 1.000 alunos e escreveu Aprendendo MySQL: Prática, Teoria e Laboratórios. Exemplos validados em PostgreSQL 16 e 17.
Fontes: PostgreSQL Documentation — Routine Vacuuming e Preventing Transaction ID Wraparound Failures; documentação das extensões pgstattuple e pg_repack; notas de release do PostgreSQL 17.