O VACUUM FULL resolve um problema de armazenamento criando temporariamente outro: o PostgreSQL reescreve a relação em um novo arquivo e mantém o antigo até a operação terminar com sucesso. Quando o volume da tabela está quase cheio, o comando pode falhar antes de devolver um único gigabyte ao sistema operacional. A saída prática é registrar um volume auxiliar como tablespace temporário.
Um agrado da nossa equipe: transformamos uma técnica útil de tablespace em um runbook de produção, incluindo os pontos fáceis de esquecer: índices, TOAST, crescimento de WAL, locks, partições e a volta para o volume original.
O que “sem espaço em disco” realmente significa
Este procedimento não faz uma reescrita acontecer sem armazenamento. Ele significa “sem espaço livre suficiente no volume onde a tabela está agora”. Você ainda precisa de um disco anexado, volume SAN/NFS com durabilidade e latência adequadas ou outro filesystem local com capacidade temporária. A documentação do PostgreSQL confirma que o VACUUM FULL obtém lock ACCESS EXCLUSIVE e usa espaço adicional porque grava uma nova cópia antes de liberar a antiga.
O VACUUM (ANALYZE) comum deve continuar sendo a manutenção padrão: ele recupera o espaço das dead tuples para reutilização dentro da mesma tabela e normalmente permite leituras e escritas concorrentes. Use FULL apenas quando for necessário devolver espaço relevante ao sistema operacional e houver janela de manutenção.
Pré-flight: meça antes de mover
Troque public.orders pela relação que será reescrita. Esta consulta separa o tamanho de heap/TOAST dos índices e mostra o tablespace atual:
SELECT
c.oid::regclass AS relation,
COALESCE(t.spcname, 'pg_default') AS tablespace,
pg_size_pretty(pg_table_size(c.oid)) AS table_and_toast,
pg_size_pretty(pg_indexes_size(c.oid)) AS indexes,
pg_size_pretty(pg_total_relation_size(c.oid)) AS total
FROM pg_class c
LEFT JOIN pg_tablespace t ON t.oid = c.reltablespace
WHERE c.oid = 'public.orders'::regclass;
Para um orçamento conservador, reserve no volume auxiliar pelo menos 2 × pg_total_relation_size mais 20%. A primeira cópia nasce quando os objetos migram para o tablespace; uma segunda pode coexistir enquanto o VACUUM FULL reescreve a tabela e recria seus índices. Tabelas com muito bloat podem exigir menos, mas um runbook de produção não deve depender do melhor caso.
- Mantenha espaço livre no volume original do PostgreSQL para o
pg_wal. Movimentações e reescritas podem gerar bastante WAL. - Confirme que réplicas e archive conseguem consumir o pico; um replication slot inativo pode reter WAL e lotar o disco original.
- Estime a janela e ensaie a sequência completa em uma restauração antes da produção.
- Tenha backup recuperável e verificado. Mover para tablespace é manutenção, não estratégia de backup.
Confira dead tuples e bloqueadores
SELECT schemaname, relname, n_live_tup, n_dead_tup,
last_autovacuum, last_vacuum
FROM pg_stat_user_tables
WHERE relid = 'public.orders'::regclass;
SELECT pid, usename, state, xact_start, wait_event_type, wait_event,
left(query, 120) AS query
FROM pg_stat_activity
WHERE xact_start IS NOT NULL
ORDER BY xact_start;
Tanto ALTER TABLE ... SET TABLESPACE quanto VACUUM FULL precisam de locks fortes. Um lock_timeout curto impede a operação de esperar escondida atrás do tráfego, mas a correção real é uma janela controlada, drenando novas sessões da aplicação.
1. Prepare o filesystem auxiliar
Como root, monte o volume extra e crie uma pasta vazia pertencente à conta de sistema que executa o PostgreSQL:
install -d -o postgres -g postgres -m 700 /mnt/pg_vacuum_tmp/ts_vacuum
df -hT /mnt/pg_vacuum_tmp
findmnt /mnt/pg_vacuum_tmp
Em distribuições com SELinux, rotule o volume antes de o PostgreSQL acessá-lo:
semanage fcontext -a -t postgresql_db_t "/mnt/pg_vacuum_tmp(/.*)?"
restorecon -Rv /mnt/pg_vacuum_tmp
Não crie um tablespace dentro do PGDATA e não aponte dois clusters PostgreSQL para a mesma pasta. O caminho deve ser absoluto, vazio, durável durante toda a operação e pertencer ao usuário de sistema do PostgreSQL.
2. Registre o tablespace temporário
Execute como superusuário do PostgreSQL. CREATE TABLESPACE não pode rodar dentro de bloco de transação:
CREATE TABLESPACE ts_vacuum_tmp
LOCATION '/mnt/pg_vacuum_tmp/ts_vacuum';
SELECT spcname, pg_tablespace_location(oid)
FROM pg_tablespace
WHERE spcname = 'ts_vacuum_tmp';
3. Leve os índices e a tabela para o disco auxiliar
A movimentação da tabela não leva seus índices. No psql, a sequência abaixo move primeiro todos os índices válidos e depois heap e TOAST. Execute no banco que contém a tabela:
et ON_ERROR_STOP on
SET lock_timeout = '10s';
SELECT format(
'ALTER INDEX %s SET TABLESPACE ts_vacuum_tmp;',
indexrelid::regclass
)
FROM pg_index
WHERE indrelid = 'public.orders'::regclass
AND indisvalid
gexec
ALTER TABLE public.orders SET TABLESPACE ts_vacuum_tmp;
Em tabela particionada, mover a tabela pai não move as partições. Inventarie e mova explicitamente cada partição folha. Verifique também views materializadas ou índices administrados separadamente da relação alvo.
4. Rode o VACUUM FULL no disco auxiliar
Execute fora de uma transação explícita. O vacuumdb deixa banco e tabela bem claros:
vacuumdb --dbname=app_production --table='public.orders' --full --analyze --verbose
Monitore três destinos: o mount auxiliar, o volume original de dados/WAL e o archive. Em outra sessão, acompanhe o progresso nas versões suportadas:
SELECT pid, datname, relid::regclass AS relation,
phase, heap_blks_scanned, heap_blks_total
FROM pg_stat_progress_cluster;
O VACUUM FULL usa a mesma view de progresso de reescrita do CLUSTER. Resultado vazio depois do término é normal.
5. Valide antes de voltar
SELECT
c.oid::regclass AS relation,
COALESCE(t.spcname, 'pg_default') AS tablespace,
pg_size_pretty(pg_table_size(c.oid)) AS table_and_toast,
pg_size_pretty(pg_indexes_size(c.oid)) AS indexes,
pg_size_pretty(pg_total_relation_size(c.oid)) AS total
FROM pg_class c
LEFT JOIN pg_tablespace t ON t.oid = c.reltablespace
WHERE c.oid = 'public.orders'::regclass;
SELECT count(*) FROM public.orders;
Rode smoke tests da aplicação e confirme que as réplicas alcançaram o primário. Se a relação compactada ainda não couber com margem no volume original, pare aqui e mantenha-a em um tablespace permanente e monitorado, em vez de forçar o retorno.
6. Devolva os objetos compactados e limpe
et ON_ERROR_STOP on
SET lock_timeout = '10s';
ALTER TABLE public.orders SET TABLESPACE pg_default;
SELECT format(
'ALTER INDEX %s SET TABLESPACE pg_default;',
indexrelid::regclass
)
FROM pg_index
WHERE indrelid = 'public.orders'::regclass
AND indisvalid
gexec
DROP TABLESPACE ts_vacuum_tmp;
O DROP TABLESPACE só termina se nenhum objeto restar ali. Depois do sucesso, desmonte e desanexe o volume temporário pelo procedimento do sistema operacional ou da nuvem. Nunca apague a pasta enquanto o PostgreSQL ainda tiver o tablespace registrado.
Plano de falha e alternativas mais seguras
| Situação | Melhor resposta |
|---|---|
| Não existe janela de manutenção | Avalie pg_repack; ele reduz a fase de lock exclusivo, mas ainda precisa de disco extra e testes cuidadosos. |
| O espaço só precisa ser reutilizado pela tabela | Use VACUUM (ANALYZE) comum; não pague o custo de uma reescrita completa. |
| A tabela é naturalmente temporal | Particione, arquive partições antigas e remova-as, em vez de reescrever repetidamente um heap gigante. |
| O volume de WAL já está crítico | Corrija archive/slots ou aumente a margem de WAL antes de qualquer reescrita. |
| A volta falha | Mantenha a relação no tablespace auxiliar, restaure o serviço e investigue a capacidade. Não remova o tablespace. |
Monitore a causa, não apenas a limpeza
Uma reescrita bem-sucedida trata o bloat acumulado; ela não explica por que ele apareceu. O PG Monitoring acompanha crescimento de dead tuples, cadência do autovacuum, transações longas, tamanho das tabelas, retenção por replication slots e previsão de disco continuamente. Essa evidência mostra se a correção permanente é tuning de autovacuum, higiene de transações, particionamento ou capacidade — e evita outro VACUUM FULL emergencial.
Referências: documentação do VACUUM, CREATE TABLESPACE, ALTER TABLE e a técnica original de tablespace que inspirou este runbook ampliado.