The PostgreSQL administration field guideField notes · Runbooks · Free certification
practitioner20 minute lesson

Vacuum, bloat, and transaction ID age

Understand row-version cleanup, freezing, vacuum capacity, and why file size does not shrink after routine vacuum.

Progress stays on this device

Mental model

Updates and deletes create row versions that remain until no transaction can need them. Standard vacuum marks that space reusable inside the table, updates visibility information, and freezes sufficiently old transaction IDs. It normally does not return the file to the operating system. Vacuum health is therefore about keeping cleanup and freezing ahead of change, not forcing files to shrink after every delete.

What healthy usually looks like

  • Dead-row estimates stabilise relative to change rate and table size.
  • Autovacuum runs frequently enough that table XID age remains well below emergency thresholds.
  • Long transactions, abandoned replication slots, and prepared transactions do not pin old horizons indefinitely.

Ask the database a bounded question

Diagnostic query

Inspect dead rows, vacuum history, and XID age

Prioritise tables where dead rows or old transaction IDs suggest maintenance is falling behind.

SELECT
  table_stats.schemaname,
  table_stats.relname,
  table_stats.n_live_tup,
  table_stats.n_dead_tup,
  table_stats.last_autovacuum,
  table_stats.autovacuum_count,
  age(table_class.relfrozenxid) AS transaction_id_age
FROM pg_stat_user_tables AS table_stats
JOIN pg_class AS table_class
  ON table_class.oid = table_stats.relid
ORDER BY transaction_id_age DESC, table_stats.n_dead_tup DESC
LIMIT 30;
low observation cost

Read the output

  1. Compare vacuum work with the table's change rate. A busy table may need lower scale factors or more vacuum capacity.
  2. Check old `backend_xmin`, prepared transactions, and inactive replication slots when cleanup cannot advance.
  3. Use standard vacuum for routine work; `VACUUM FULL` rewrites and takes an `ACCESS EXCLUSIVE` lock.

Common traps

  • Using `VACUUM FULL` as routine maintenance because a relation file did not shrink.
  • Raising autovacuum thresholds globally without measuring the tables that generate the workload.
  • Watching dead tuples while ignoring transaction ID age and old snapshot holders.

Check your reasoning

Why might a table file remain the same size after a successful standard VACUUM?

Related runbooks

Sources