TL;DR — Resumo Rápido
Guia completa de VACUUM e autovacuum no PostgreSQL: workers, limiares, custo de atraso, monitoramento de bloat, prevencao de wraparound e pg_repack.
O modelo de controle de concorrência multiversão (MVCC) do PostgreSQL mantém versões antigas de linhas vivas para servir leitores concorrentes. Sem limpeza regular, essas tuplas mortas se acumulam, inflam o tamanho das tabelas, tornam os scans sequenciais mais lentos e eventualmente ameaçam a disponibilidade do banco de dados através do wraparound do Transaction ID. Este guia cobre todos os controles que um DBA pode usar: desde os parâmetros globais do autovacuum até substituições por tabela, desde consultas de monitoramento de bloat até reorganização online com pg_repack.
Pré-requisitos
Antes de começar, certifique-se de ter:
- PostgreSQL 13 ou posterior (a maioria dos exemplos funciona desde a versão 12+; algumas sintaxes requerem a 14+).
- Acesso ao psql ou pgAdmin com o papel superuser ou pg_monitor.
- Extensão pg_repack instalada se você planeja reorganização online (abordada mais adiante).
- Familiaridade com
postgresql.confe a capacidade de recarregar a configuração.
MVCC e Tuplas Mortas
O PostgreSQL nunca modifica uma linha no lugar. Um UPDATE escreve uma nova versão da tupla e marca a antiga como morta. Um DELETE também marca a linha como morta sem removê-la. Esse design permite que os leitores vejam um snapshot consistente sem bloquear, mas o trade-off é o acúmulo de tuplas mortas no disco.
As tuplas mortas consomem espaço em disco, inflam o tamanho físico da tabela e tornam os scans sequenciais mais lentos porque o PostgreSQL deve ler e pular cada página mesmo que a maioria das suas linhas esteja morta. A view pg_stat_user_tables rastreia a contagem:
SELECT
relname,
n_live_tup,
n_dead_tup,
round(n_dead_tup::numeric / nullif(n_live_tup + n_dead_tup, 0) * 100, 1) AS dead_pct,
last_autovacuum,
last_autoanalyze
FROM pg_stat_user_tables
ORDER BY n_dead_tup DESC
LIMIT 20;
Uma tabela com dead_pct acima de 10–15% é candidata a atenção imediata.
VACUUM, VACUUM FULL e VACUUM ANALYZE
O PostgreSQL inclui três variantes de VACUUM com comportamentos muito diferentes:
| Comando | Bloqueia a tabela? | Devolve espaço ao SO? | Atualiza estatísticas? | Seguro em produção? |
|---|---|---|---|---|
VACUUM | Não (ShareUpdateExclusiveLock) | Não — marca espaço reutilizável | Não | Sim |
VACUUM ANALYZE | Não | Não | Sim | Sim |
VACUUM FULL | Sim (AccessExclusiveLock) | Sim | Não | Com cautela |
VACUUM marca as tuplas mortas como disponíveis para reutilização. O arquivo físico não é reduzido — as páginas liberadas são adicionadas ao Free Space Map (FSM) para futuras inserções. Este é o comando principal que você deve usar.
VACUUM FULL reescreve a tabela inteira em um novo arquivo heap, depois descarta o antigo. Devolve espaço em disco ao sistema operacional e remove o bloat de índices reconstruindo todos os índices, mas adquire um bloqueio exclusivo durante toda a sua execução. Em uma tabela de 100 GB, isso pode significar 30 minutos ou mais de indisponibilidade total. Nunca execute VACUUM FULL em uma tabela de produção ativa sem agendar uma janela de manutenção — ou melhor, use pg_repack.
VACUUM ANALYZE combina a limpeza de tuplas mortas com uma atualização de estatísticas, que o planejador de consultas usa para escolher planos de execução ótimos.
Configuração do Autovacuum
O autovacuum executa workers em segundo plano que chamam VACUUM e ANALYZE automaticamente. Os parâmetros principais estão em postgresql.conf:
# Número de workers concorrentes de autovacuum (padrão: 3)
autovacuum_max_workers = 5
# Frequência com que o lançador do autovacuum verifica as tabelas (padrão: 1min)
autovacuum_naptime = 30s
# Mínimo de tuplas mortas antes de ativar VACUUM (padrão: 50)
autovacuum_vacuum_threshold = 50
# Fração de linhas vivas que podem estar mortas antes de ativar VACUUM (padrão: 0.2 = 20%)
autovacuum_vacuum_scale_factor = 0.05
# Mínimo de mudanças de linhas antes de ativar ANALYZE (padrão: 50)
autovacuum_analyze_threshold = 50
# Fração de linhas alteradas antes de ativar ANALYZE (padrão: 0.1 = 10%)
autovacuum_analyze_scale_factor = 0.02
# Limitação de E/S: atraso entre operações de buffer do vacuum em milissegundos (padrão: 2)
autovacuum_vacuum_cost_delay = 2ms
# Máximo de unidades de custo gastas antes de um atraso (padrão: -1 = usa vacuum_cost_limit = 200)
autovacuum_vacuum_cost_limit = 400
Entendendo a Fórmula de Ativação
O autovacuum é ativado em uma tabela quando:
n_dead_tup > autovacuum_vacuum_threshold + autovacuum_vacuum_scale_factor * n_live_tup
Para uma tabela de 10 milhões de linhas com os valores padrão:
limiar = 50 + 0.20 × 10,000,000 = 2,000,050 tuplas mortas
Isso significa que 20% da tabela deve estar morta antes que o autovacuum seja ativado. Para uma tabela de alta escrita, isso é tarde demais. Reduzir autovacuum_vacuum_scale_factor para 0.01 ou mesmo 0.005 é apropriado para tabelas grandes com atualizações frequentes.
Após alterar postgresql.conf, recarregue a configuração (não é necessário reiniciar):
SELECT pg_reload_conf();
Monitorando o Bloat
As views pg_stat_user_tables e pg_stat_activity são suas principais ferramentas de monitoramento.
Consulta de Painel de Tuplas Mortas
SELECT
schemaname,
relname,
n_dead_tup,
n_live_tup,
last_autovacuum,
last_autoanalyze,
autovacuum_count,
pg_size_pretty(pg_total_relation_size(schemaname || '.' || relname)) AS total_size
FROM pg_stat_user_tables
WHERE n_dead_tup > 1000
ORDER BY n_dead_tup DESC;
Encontrar Workers de Autovacuum em Execução
SELECT
pid,
now() - pg_stat_activity.query_start AS duration,
query,
state
FROM pg_stat_activity
WHERE query LIKE 'autovacuum:%'
ORDER BY duration DESC;
Substituições de Autovacuum por Tabela
Os parâmetros globais afetam todas as tabelas. Tabelas grandes com atualizações frequentes precisam de limiares mais apertados. Aplique parâmetros de armazenamento por tabela com ALTER TABLE:
-- Ativar vacuum quando 1% das linhas estiverem mortas (em vez do padrão de 20%)
ALTER TABLE orders SET (
autovacuum_vacuum_scale_factor = 0.01,
autovacuum_vacuum_threshold = 100,
autovacuum_analyze_scale_factor = 0.005,
autovacuum_vacuum_cost_delay = 0
);
-- Verificar as substituições
SELECT relname, reloptions
FROM pg_class
WHERE relname = 'orders';
Esta é a mudança mais impactante que você pode fazer para uma tabela ativa. O fator de escala global permanece conservador para tabelas pequenas, enquanto a tabela ativa recebe vacuum agressivo.
Prevenção do Wraparound de Transaction ID
O PostgreSQL usa IDs de transação (XIDs) de 32 bits. Após aproximadamente 2,1 bilhões de transações, o contador reinicia. As tuplas mais antigas que o horizonte de wraparound podem se tornar visíveis novamente ou invisíveis, causando corrupção de dados.
Verificando a Idade do XID
SELECT
relname,
age(relfrozenxid) AS xid_age,
pg_size_pretty(pg_total_relation_size(oid)) AS size
FROM pg_class
WHERE relkind = 'r'
ORDER BY age(relfrozenxid) DESC
LIMIT 20;
Quando xid_age excede autovacuum_freeze_max_age (padrão: 200 milhões), o autovacuum forçará um freeze vacuum nessa tabela. Monitore no nível do banco de dados:
SELECT datname, age(datfrozenxid) AS db_xid_age
FROM pg_database
ORDER BY age(datfrozenxid) DESC;
Configure um alerta quando a idade XID de qualquer banco de dados exceder 1,5 bilhão.
pg_repack: Reorganização Online de Tabelas
Quando VACUUM não consegue recuperar espaço físico e VACUUM FULL é inaceitável pelo bloqueio, pg_repack é a solução. Ele reconstrói a tabela usando uma tabela sombra e triggers, depois as troca atomicamente — tudo enquanto a tabela original permanece totalmente disponível.
Instalação do pg_repack
# Ubuntu/Debian
sudo apt install postgresql-15-repack
-- No banco de dados alvo
CREATE EXTENSION pg_repack;
Reorganizando uma Tabela Online
# Reorganizar uma única tabela com bloat
pg_repack -d mydb -t orders
# Reorganizar todas as tabelas em um banco de dados
pg_repack -d mydb
# Simulação (mostrar o que aconteceria)
pg_repack -d mydb -t orders --dry-run
Comparação pg_repack vs VACUUM FULL:
| Aspecto | pg_repack | VACUUM FULL |
|---|---|---|
| Bloqueio durante a reconstrução | Nenhum | AccessExclusiveLock (duração completa) |
| Espaço em disco necessário | 2× tamanho da tabela temporariamente | 2× tamanho da tabela temporariamente |
| Reconstrução de índices | Sim (concorrente) | Sim (bloqueante) |
| Requer chave primária | Sim | Não |
| Seguro em produção | Sim | Apenas em janela de manutenção |
Bloat de Índices e REINDEX CONCURRENTLY
Os índices acumulam bloat independentemente da tabela. Reconstrua índices sem downtime:
-- Reconstruir um único índice de forma concorrente
REINDEX INDEX CONCURRENTLY orders_created_at_idx;
-- Reconstruir todos os índices de uma tabela de forma concorrente (PostgreSQL 14+)
REINDEX TABLE CONCURRENTLY orders;
Interação com PgBouncer e Autovacuum
O PgBouncer no modo de pooling de transações pode fazer conexões de aplicações ociosas manterem transações abertas que fixam o xmin mais antigo. Configure idle_in_transaction_session_timeout = '5min' no PostgreSQL para encerrar automaticamente transações obsoletas.
SELECT slot_name, xmin, catalog_xmin, age(xmin) FROM pg_replication_slots;
Verifique também os slots de replicação, pois podem fixar o xmin indefinidamente.
Erros Comuns
1. Definir autovacuum = off globalmente. Nunca faça isso em um banco de dados de produção. Sem autovacuum, o desligamento por wraparound é inevitável.
2. Executar VACUUM FULL em horários de pico. O bloqueio exclusivo bloqueia tudo. Agende em janelas de manutenção ou use pg_repack.
3. Ignorar transações de longa duração. Se uma transação longa mantiver um xmin antigo, VACUUM não pode remover tuplas mortas independentemente das configurações.
4. Configurar autovacuum_max_workers muito alto. Cada worker consome conexões e E/S. Ajuste cost_delay e cost_limit primeiro.
Resumo
O desempenho saudável do PostgreSQL depende do VACUUM acompanhar o ritmo da sua carga de escrita:
- Reduza
autovacuum_vacuum_scale_factorpara tabelas grandes com muitas escritas — o padrão de 20% é excessivamente permissivo. - Monitore
n_dead_tupelast_autovacuumcontinuamente viapg_stat_user_tables. - Verifique
xid_ageregularmente; alerte acima de 1,5 bilhão para prevenir emergências de wraparound. - Use substituições por tabela (
ALTER TABLE SET) em vez de relaxar as configurações globais para todas as tabelas. - Substitua VACUUM FULL por pg_repack para reorganização online sem downtime.
- Reconstrua índices com
REINDEX CONCURRENTLYquando o bloat de índices afetar o desempenho das consultas.