← Voltar ao blog
PostgreSQL 23 de setembro de 2026 11 min de leitura

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.

VACUUM e bloat no PostgreSQL: o inimigo silencioso

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.

Tabela representada como blocos inchados de tuplas mortas sendo limpos por um processo de vacuum
Bloat não derruba o banco de uma vez. Ele vai comendo a performance por meses.

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

FerramentaBloqueia escrita?Quando usar
VACUUM FULLSim, lock exclusivoSó com janela de manutenção. Precisa de espaço em disco igual à tabela
REINDEX CONCURRENTLYNãoÍndice inchado, caso mais comum. Nativo desde o PG 12
pg_repackPraticamente nãoTabela 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.