VACUUM e autovacuum no PostgreSQL

O PostgreSQL nunca deleta linhas fisicamente, UPDATE e DELETE criam dead tuples que acumulam e degradam a performance ao longo do tempo. VACUUM limpa essas linhas mortas, e autovacuum faz isso automaticamente em background. Entender e tunar o autovacuum é essencial para bancos de produção saudáveis.

Por que o PostgreSQL precisa de VACUUM

MVCC, dead tuples e table bloat explicados:

sql
-- Entender o problema:
-- No PostgreSQL, UPDATE cria uma NOVA versão da linha (MVCC)
-- A versão antiga fica como "dead tuple" — ocupa espaço mas é invisível

-- Simular bloat:
CREATE TABLE teste (id serial, valor text);
INSERT INTO teste SELECT generate_series(1, 100000), 'dados iniciais';
UPDATE teste SET valor = 'atualizado';  -- cria 100k dead tuples!

-- Ver tamanho real vs tamanho útil da tabela:
SELECT
  relname AS tabela,
  pg_size_pretty(pg_relation_size(oid)) AS tamanho_total,
  pg_size_pretty(pg_relation_size(oid) - (reltuples * 100)) AS estimativa_bloat,
  n_dead_tup AS dead_tuples,
  n_live_tup AS live_tuples,
  round(100.0 * n_dead_tup / nullif(n_live_tup + n_dead_tup, 0), 1) AS pct_mortas
FROM pg_stat_user_tables
WHERE relname = 'teste';

-- Verificar quando o autovacuum rodou por último:
SELECT relname, last_vacuum, last_autovacuum, last_analyze, last_autoanalyze
FROM pg_stat_user_tables
ORDER BY n_dead_tup DESC
LIMIT 20;

Executar VACUUM manualmente

Comandos VACUUM e quando usar cada variante:

sql
-- VACUUM simples: limpa dead tuples mas NÃO devolve espaço ao SO:
VACUUM tabela_grande;

-- VACUUM ANALYZE: limpa + atualiza estatísticas do planejador:
VACUUM ANALYZE tabela_grande;

-- VACUUM FULL: limpa + reescreve a tabela compactada (devolve espaço ao SO):
-- ATENÇÃO: bloqueia a tabela! Não usar em produção sem manutenção
VACUUM FULL tabela_grande;

-- VACUUM VERBOSE: mostrar progresso detalhado:
VACUUM VERBOSE ANALYZE tabela_grande;

-- Para desfragmentar sem bloquear: usar pg_repack (extensão):
-- apt install postgresql-16-repack
-- pg_repack -d producao -t tabela_grande

-- Ver progresso do VACUUM em execução (PostgreSQL 9.6+):
SELECT phase, heap_blks_scanned, heap_blks_total,
       round(100.0 * heap_blks_scanned / nullif(heap_blks_total, 0), 1) AS pct
FROM pg_stat_progress_vacuum;

-- Executar VACUUM em todo o banco (apenas para manutenção):
-- vacuumdb -U postgres -d producao --analyze --verbose

Tunar o autovacuum para tabelas de alto tráfego

Configurar o autovacuum por tabela para APIs com muitos UPDATEs:

sql
-- Ver configuração atual do autovacuum:
SELECT name, setting, unit FROM pg_settings WHERE name LIKE '%autovacuum%';

-- Configurações globais no postgresql.conf:
autovacuum = on                          # nunca desativar!
autovacuum_max_workers = 3               # workers paralelos (default: 3)
autovacuum_naptime = 1min                # intervalo entre verificações

# Quando acionar VACUUM (por tabela):
autovacuum_vacuum_threshold = 50         # mínimo de dead tuples
autovacuum_vacuum_scale_factor = 0.2     # 20% da tabela com dead tuples

# Quando acionar ANALYZE:
autovacuum_analyze_threshold = 50
autovacuum_analyze_scale_factor = 0.1   # 10% da tabela modificada

# Throttle para não sobrecarregar I/O:
autovacuum_vacuum_cost_delay = 2ms       # pausa entre páginas (default: 2ms)
autovacuum_vacuum_cost_limit = 200       # custo por ciclo antes de pausar

-- Configuração POR TABELA (mais granular):
-- Para tabelas com muitos UPDATEs (sessions, eventos):
ALTER TABLE sessoes_usuario SET (
  autovacuum_vacuum_scale_factor = 0.01,  -- acionar com 1% de dead tuples
  autovacuum_vacuum_threshold = 100,
  autovacuum_analyze_scale_factor = 0.01,
  autovacuum_vacuum_cost_delay = 1        -- mais agressivo no I/O
);

-- Para tabelas mostly read-only (quase sem updates):
ALTER TABLE produtos_catalogo SET (
  autovacuum_vacuum_scale_factor = 0.5,   -- acionar só com 50% de dead tuples
  autovacuum_naptime = 3600               -- verificar a cada hora
);

Transaction ID Wraparound: o erro mais crítico

Detectar e prevenir o shutdown emergencial do PostgreSQL:

sql
-- O PostgreSQL usa Transaction IDs (XID) de 32 bits — máximo: 2 bilhões
-- Quando o XID se aproxima do limite: PostgreSQL PARA de aceitar writes
-- para evitar corrupção (transaction wraparound)

-- Monitorar XID age de cada banco (CRÍTICO):
SELECT
  datname,
  age(datfrozenxid) AS xid_age,
  2147483648 - age(datfrozenxid) AS transacoes_restantes,
  round(100.0 * age(datfrozenxid) / 2147483648, 1) AS pct_consumido
FROM pg_database
ORDER BY age(datfrozenxid) DESC;

-- Monitorar por tabela:
SELECT
  relname,
  age(relfrozenxid) AS xid_age,
  round(100.0 * age(relfrozenxid) / 2147483648, 1) AS pct
FROM pg_class
WHERE relkind = 'r'
ORDER BY age(relfrozenxid) DESC
LIMIT 20;

-- Alerta: se xid_age > 1.5 bilhões → executar VACUUM FREEZE urgente
-- postgresql.conf — acionar VACUUM FREEZE preventivo:
vacuum_freeze_min_age = 50000000          -- congelar XIDs com mais de 50M transações
vacuum_freeze_table_age = 150000000       -- forçar VACUUM FREEZE na tabela inteira
autovacuum_freeze_max_age = 200000000     -- VACUMM FREEZE preventivo automático

-- Se o banco estiver próximo do limite (acima de 1.9B):
-- VACUUM FREEZE producao;  (pode levar horas, mas evita shutdown)

Detectar e resolver table bloat

Identificar tabelas e índices inflados e compactá-los:

sql
-- Query para detectar bloat em tabelas:
SELECT
  tablename,
  pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename)) AS tamanho_total,
  n_dead_tup,
  n_live_tup,
  round(100.0 * n_dead_tup / nullif(n_live_tup + n_dead_tup, 0), 1) AS pct_bloat
FROM pg_stat_user_tables
WHERE n_dead_tup > 10000
ORDER BY n_dead_tup DESC;

-- Detectar bloat em índices:
SELECT
  indexname,
  pg_size_pretty(pg_relation_size(indexrelid)) AS tamanho_indice,
  idx_scan AS leituras
FROM pg_stat_user_indexes
JOIN pg_class ON pg_class.oid = indexrelid
WHERE pg_relation_size(indexrelid) > 10 * 1024 * 1024  -- > 10 MB
ORDER BY pg_relation_size(indexrelid) DESC;

-- Reconstruir índice inchado sem bloquear:
REINDEX INDEX CONCURRENTLY idx_meu_indice_grande;
-- ou todos os índices de uma tabela:
REINDEX TABLE CONCURRENTLY minha_tabela;

-- pg_repack: compactar tabela sem lock (melhor que VACUUM FULL):
-- pg_repack -d producao --table minha_tabela --no-order
-- (requer extensão: CREATE EXTENSION pg_repack;)

Não quer configurar manualmente?

Implante o VPS para APIs em menos de 5 minutos com a Runstack. Infraestrutura da OPEN Datacenter, com servidores em São Paulo.

Perguntas frequentes

Causas comuns: (1) Transação longa aberta que impede o VACUUM de remover dead tuples visíveis para ela, verifique pg_stat_activity por transações com query_start muito antigas e encerre-as; (2) Autovacuum sendo throttled por cost_delay alto, reduza autovacuum_vacuum_cost_delay para tabelas críticas; (3) Tabela com muitas partições, autovacuum trata cada partição separadamente; (4) Número de workers insuficiente, aumente autovacuum_max_workers.

Raramente em produção. Use VACUUM FULL somente: em janela de manutenção com aplicação offline, quando a tabela está > 50% bloat e precisa devolver espaço em disco urgentemente, ou como última recurso antes de uma operação crítica. Para a maioria dos casos: pg_repack é melhor, compacta sem lock exclusivo. VACUUM regular + autovacuum bem configurado previne a necessidade de VACUUM FULL.

Sim, são operações distintas. VACUUM: limpa dead tuples e congela XIDs antigos. ANALYZE: coleta estatísticas sobre a distribuição de dados nas colunas (quantos valores distintos, distribuição de frequência). O planejador de queries usa essas estatísticas para escolher o melhor plano. VACUUM ANALYZE faz ambos. Use ANALYZE após inserir ou atualizar grandes volumes de dados para o planejador ter informações atualizadas.

Nunca desative o autovacuum globalmente em produção, o banco eventualmente para por transaction ID wraparound. Se precisar pausar para uma operação de carga intensiva: desative por tabela específica: ALTER TABLE minha_tabela SET (autovacuum_enabled = false); e reative depois. Ou use autovacuum_vacuum_cost_delay alto temporariamente para reduzir o impacto no I/O durante a operação.

Monitore n_dead_tup em pg_stat_user_tables, se crescer continuamente sem cair: autovacuum não está conseguindo acompanhar. Verifique pg_stat_activity por vacuum workers ativos. Se n_dead_tup > 10% de n_live_tup persistentemente: aumente autovacuum_max_workers, reduza autovacuum_vacuum_scale_factor para a tabela, ou reduza autovacuum_vacuum_cost_delay para o worker trabalhar mais rápido.