VACUUM e Autovacuum no PostgreSQL – Entendendo bloat na prática
Muita gente que começa a trabalhar com PostgreSQL escuta falar de VACUUM o tempo todo.
Mas no começo parece algo meio abstrato.
Tabela cresce, autovacuum roda, dead tuples aparecem… e às vezes fica difícil entender
o que realmente está acontecendo por trás.
Então resolvi montar um laboratório simples para demonstrar na prática como o bloat
aparece e como o VACUUM atua dentro do PostgreSQL.
A ideia aqui é simular um cenário bem comum no dia a dia:
tabelas recebendo UPDATE e DELETE, gerando versões antigas de linhas e acumulando
dead tuples.
Ambiente do laboratório
PostgreSQL 17
Ambiente standalone
Tabela de teste com carga artificial de dados
1 – Criando um banco de laboratório
CREATE DATABASE lab_vacuum;Conectando nele:
psql -U postgres -d lab_vacuum ou \c xxx2 – Criando uma tabela de teste
CREATE TABLE teste_bloat (
id serial PRIMARY KEY,
nome text,
status text,
criado_em timestamp default now()
);3 – Inserindo uma massa inicial de dados
INSERT INTO teste_bloat (nome, status)
SELECT
md5(random()::text),
'ativo'
FROM generate_series(1,200000);Aqui criamos uma tabela com um volume razoável de dados para facilitar a simulação.
4 – Verificando o tamanho inicial da tabela
SELECT
relname,
pg_size_pretty(pg_total_relation_size(relid)) AS total_size
FROM pg_catalog.pg_statio_user_tables
WHERE relname = 'teste_bloat';5 – Conferindo estatísticas da tabela
SELECT
schemaname,
relname,
n_live_tup,
n_dead_tup,
last_vacuum,
last_autovacuum,
vacuum_count,
autovacuum_count
FROM pg_stat_user_tables
WHERE relname = 'teste_bloat';Nesse momento o esperado é:
n_live_tup alto
n_dead_tup próximo de zero
schemaname | relname | n_live_tup | n_dead_tup | last_vacuum | last_autovacuum | vacuum_count | autovacuum_count
------------+-------------+------------+------------+-------------+-------------------------------+--------------+------------------
public | teste_bloat | 200000 | 0 | | 2026-03-16 21:29:22.468759-03 | 0 | 1
6 – Gerando bloat com UPDATE
UPDATE teste_bloat
SET status = 'inativo';Aqui acontece algo importante no PostgreSQL.
O UPDATE não sobrescreve a linha existente.
Ele cria uma nova versão da linha e marca a anterior como morta.
Isso acontece por causa do MVCC (Multi Version Concurrency Control).
schemaname | relname | n_live_tup | n_dead_tup | last_vacuum | last_autovacuum | vacuum_count | autovacuum_count
------------+-------------+------------+------------+-------------+-------------------------------+--------------+------------------
public | teste_bloat | 200000 | 200000 | | 2026-03-16 21:29:22.468759-03 | 0 | 17 – Gerando ainda mais bloat
UPDATE teste_bloat
SET nome = md5(random()::text);Agora aumentamos ainda mais o número de versões antigas das linhas.
8 – Conferindo as dead tuples
SELECT
schemaname,
relname,
n_live_tup,
n_dead_tup,
ROUND(
(n_dead_tup::numeric / NULLIF(n_live_tup + n_dead_tup,0))*100,
2
) AS pct_dead
FROM pg_stat_user_tables
WHERE relname = 'teste_bloat';Aqui já conseguimos observar o acúmulo de dead tuples na tabela.
schemaname | relname | n_live_tup | n_dead_tup | last_vacuum | last_autovacuum | vacuum_count | autovacuum_count
------------+-------------+------------+------------+-------------+-------------------------------+--------------+------------------
public | teste_bloat | 200000 | 400000 | | 2026-03-16 21:29:22.468759-03 | 0 | 9 – Conferindo novamente o tamanho da tabela
SELECT
relname,
pg_size_pretty(pg_total_relation_size(relid)) AS total_size
FROM pg_catalog.pg_statio_user_tables
WHERE relname = 'teste_bloat';
relname | total_size
-------------+------------
teste_bloat | 58 MBMesmo sem inserir novos dados úteis, a tabela pode crescer por causa das versões antigas.
10 – Gerando mais bloat com DELETE
DELETE FROM teste_bloat
WHERE id <= 50000;Deletes também geram linhas mortas, que precisam ser limpas posteriormente.
schemaname | relname | n_live_tup | n_dead_tup | last_vacuum | last_autovacuum | vacuum_count | autovacuum_count
------------+-------------+------------+------------+-------------+-------------------------------+--------------+------------------
public | teste_bloat | 150000 | 50000 | | 2026-03-16 21:30:52.773007-03 | 0 | 211 – Executando VACUUM ANALYZE
VACUUM ANALYZE teste_bloat;O que acontece aqui:
VACUUM remove as linhas mortas internamente.
ANALYZE atualiza as estatísticas usadas pelo otimizador de consultas.
12 – Validando após o VACUUM
SELECT
schemaname,
relname,
n_live_tup,
n_dead_tup,
last_vacuum,
last_autovacuum,
vacuum_count,
autovacuum_count
FROM pg_stat_user_tables
WHERE relname = 'teste_bloat';
schemaname | relname | n_live_tup | n_dead_tup | last_vacuum | last_autovacuum | vacuum_count | autovacuum_count
------------+-------------+------------+------------+-------------------------------+-------------------------------+--------------+------------------
public | teste_bloat | 150000 | 0 | 2026-03-16 21:32:51.372299-03 | 2026-03-16 21:30:52.773007-03 | 1 | 2
(1 row)Agora devemos ver uma redução significativa nas dead tuples.
13 – Um detalhe importante
Mesmo após o VACUUM, o tamanho da tabela pode não diminuir conforme vemos.
lab_vacuum=# SELECT
relname,
pg_size_pretty(pg_total_relation_size(relid)) AS total_size
FROM pg_catalog.pg_statio_user_tables
WHERE relname = 'teste_bloat';
relname | total_size
-------------+------------
teste_bloat | 58 MBIsso acontece porque o VACUUM libera o espaço internamente
para reutilização, mas não devolve o espaço ao sistema operacional.
14 – Quando o tamanho realmente diminui
Para reduzir fisicamente o tamanho da tabela, é possível usar:
VACUUM FULL teste_bloat;
lab_vacuum=# VACUUM FULL teste_bloat;
VACUUM
lab_vacuum=# SELECT
relname,
pg_size_pretty(pg_total_relation_size(relid)) AS total_size
FROM pg_catalog.pg_statio_user_tables
WHERE relname = 'teste_bloat';
relname | total_size
-------------+------------
teste_bloat | 15 MBMas aqui existe um ponto importante:
VACUUM FULL reescreve a tabela inteira
pega lock exclusivo
não é recomendado executar em horário crítico de produção
Segue incrementado:
Um ponto importante: O Autovacuum
No laboratório acima executamos o VACUUM manualmente apenas para demonstrar o funcionamento, mas na prática quem faz esse trabalho no dia a dia é o Autovacuum.
O autovacuum é um processo automático do PostgreSQL responsável por:
- remover dead tuples
- atualizar estatísticas das tabelas
- evitar crescimento desnecessário das tabelas (bloat)
- prevenir problemas de transaction ID wraparound
Ou seja, ele roda em background mantendo o banco saudável sem intervenção do DBA.
Podemos verificar se ele já executou em uma tabela com a view pg_stat_user_tables:
SELECT
schemaname,
relname,
n_live_tup,
n_dead_tup,
last_autovacuum,
last_autoanalyze,
autovacuum_count,
autoanalyze_count
FROM pg_stat_user_tables
ORDER BY last_autovacuum DESC;Por que precisamos ter cuidado com o Autovacuum
Apesar de ser essencial, o autovacuum também precisa ser bem ajustado. Se estiver mal configurado ou atrasado, alguns problemas podem aparecer:
- acúmulo de dead tuples
- crescimento excessivo das tabelas (bloat)
- aumento de I/O e CPU
- piora no desempenho das consultas
Por outro lado, se configurado de forma muito agressiva, o autovacuum pode competir por recursos com queries da aplicação.
Por isso é importante acompanhar parâmetros como:
| Parâmetro | Descrição |
|---|---|
autovacuum_vacuum_scale_factor | % de dead tuples para disparar o vacuum |
autovacuum_analyze_scale_factor | % de mudanças para disparar o analyze |
autovacuum_max_workers | número máximo de workers paralelos |
autovacuum_naptime | intervalo entre verificações |
autovacuum_vacuum_cost_delay | throttle para não sobrecarregar I/O |
Ajuste por tabela
Em ambientes com tabelas muito grandes ou com muita escrita, é comum ajustar o autovacuum por tabela:
ALTER TABLE tabela_critica SET( autovacuum_vacuum_scale_factor = 0.01, autovacuum_analyze_scale_factor = 0.005);Assim o processo de limpeza acontece mais cedo e evita acúmulo de bloat.
Para monitorar tabelas que estão acumulando dead tuples e o autovacuum não está dando conta, use:
SELECT schemaname, relname, n_dead_tup, last_autovacuum, ROUND(n_dead_tup::numeric / NULLIF(n_live_tup, 0) * 100, 2) AS pct_dead FROM pg_stat_user_tables WHERE n_dead_tup 1000 ORDER BY pct_dead DESC;Tabelas com pct_dead alto são candidatas a ajuste fino do autovacuum ou a um VACUUM ANALYZE manual pontual.
Ao trabalhar com PostgreSQL é fundamental entender que operações como UPDATE e DELETE não removem imediatamente os dados antigos.
Por causa do MVCC, o banco mantém versões anteriores das linhas para garantir consistência de leitura, e essas versões acabam se tornando dead tuples.
É aí que entram o VACUUM e o Autovacuum.
O VACUUM é responsável por limpar essas linhas mortas e liberar espaço interno para reutilização, enquanto o autovacuum faz esse trabalho automaticamente em segundo plano, mantendo o banco saudável na maior parte do tempo sem intervenção manual.
Quando esse processo funciona bem, o PostgreSQL consegue manter tabelas e índices sob controle mesmo em ambientes com muita escrita.
Por outro lado, quando o autovacuum está mal ajustado ou atrasado, começam a aparecer sintomas conhecidos no dia a dia do DBA: crescimento inesperado de tabelas, aumento de I/O, consultas mais lentas e casos de bloat.
DBA Yan Spinelli
