The essentials
Quick reference
One focused task per row. Jump to the related section for complete, working examples.
| Use | Syntax | Examples |
|---|---|---|
| Vacuum one table | VACUUM public.orders; | View examples |
| Vacuum and refresh statistics | VACUUM (ANALYZE, VERBOSE) public.orders; | View examples |
| Inspect table maintenance health | SELECT relname, n_live_tup, n_dead_tup, last_autovacuum
FROM pg_stat_user_tables
ORDER BY n_dead_tup DESC; | View examples |
| Find stale planner statistics | SELECT relname, n_mod_since_analyze, last_autoanalyze
FROM pg_stat_user_tables
ORDER BY n_mod_since_analyze DESC; | View examples |
| Monitor active vacuum work | SELECT pid,
relid::regclass,
phase,
heap_blks_scanned,
heap_blks_total
FROM pg_stat_progress_vacuum; | View examples |
| Identify autovacuum sessions | SELECT pid,
backend_type,
query_start,
wait_event_type,
wait_event
FROM pg_stat_activity
WHERE backend_type = 'autovacuum worker'; | View examples |
| Inspect autovacuum settings | SELECT name, setting, unit
FROM pg_settings
WHERE name LIKE 'autovacuum%'
ORDER BY name; | View examples |
| Tune a high-churn table | ALTER TABLE public.orders
SET (autovacuum_vacuum_scale_factor = 0.01, autovacuum_vacuum_threshold = 5000); | View examples |
| Inspect table overrides | SELECT relname, reloptions
FROM pg_class
WHERE oid = 'public.orders'::regclass; | View examples |
| Rank database XID age | SELECT datname, age(datfrozenxid) AS xid_age
FROM pg_database
ORDER BY xid_age DESC; | View examples |
| Rank table XID age | SELECT oid::regclass, age(relfrozenxid) AS xid_age
FROM pg_class
WHERE relkind IN ('r','m')
ORDER BY xid_age DESC; | View examples |
| Find old transaction horizons | SELECT pid, xact_start, backend_xmin, age(backend_xmin)
FROM pg_stat_activity
WHERE backend_xmin IS NOT NULL
ORDER BY age(backend_xmin) DESC; | View examples |
| Find old prepared transactions | SELECT gid, prepared, owner, database, age(transaction)
FROM pg_prepared_xacts
ORDER BY age(transaction) DESC; | View examples |
| Inspect replication slot horizons | SELECT slot_name, slot_type, active, xmin, catalog_xmin
FROM pg_replication_slots
ORDER BY slot_name; | View examples |
| Measure HOT update use | SELECT relname, n_tup_upd, n_tup_hot_upd
FROM pg_stat_user_tables
ORDER BY n_tup_upd DESC; | View examples |
| Reserve heap page space | CREATE TABLE account_state (account_id bigint PRIMARY KEY, status text NOT NULL)
WITH (fillfactor = 80); | View examples |
| Compare table and total size | SELECT pg_relation_size('public.orders'),
pg_total_relation_size('public.orders'); | View examples |
| Rank total relation sizes | SELECT relid::regclass,
pg_total_relation_size(relid) AS bytes
FROM pg_stat_user_tables
ORDER BY bytes DESC; | View examples |
VACUUM is part of PostgreSQL's concurrency model, not optional housekeeping. Healthy systems let autovacuum reclaim reusable space, maintain visibility information, and freeze old row versions continuously. Production work starts with trends and cleanup horizons: measure which tables fall behind, tune exceptional tables deliberately, and reserve rewriting operations for diagnosed problems and rehearsed maintenance windows.
Step by step
Detailed examples
Use standard VACUUM as continuous maintenance
MVCC keeps old row versions until no transaction can see them. Standard VACUUM removes reusable dead versions from tables and indexes, updates the visibility map, and freezes sufficiently old transaction IDs while ordinary reads and writes continue. It usually does not return space to the operating system; its goal is stable reuse. ANALYZE is related but separate planner-statistics work, and the combined command is useful after unusual data changes when autovacuum cannot catch up quickly enough.
VACUUM (ANALYZE, VERBOSE) public.orders;
-- For a read-only preview of command defaults and options:
SELECT name, setting
FROM pg_settings
WHERE name IN (
'vacuum_cost_delay',
'vacuum_cost_limit',
'maintenance_work_mem'
)
ORDER BY name; Trend maintenance signals instead of trusting one snapshot
pg_stat_user_tables provides estimates and cumulative counters, not an exact bloat measurement. Track dead rows relative to workload and table size, time since vacuum or analyze, insert volume since vacuum, and whether counts keep rising across collection intervals. Statistics can reset, and a low dead-tuple estimate does not prove compact storage. Pair these signals with latency, I/O, relation-size growth, logs, and workload events.
SELECT schemaname,
relname,
n_live_tup,
n_dead_tup,
n_ins_since_vacuum,
n_mod_since_analyze,
last_autovacuum,
last_autoanalyze,
autovacuum_count
FROM pg_stat_user_tables
ORDER BY n_dead_tup DESC, n_mod_since_analyze DESC; Interpret vacuum phase and progress counters
pg_stat_progress_vacuum contains one row for every active standard vacuum, including autovacuum workers. The heap counters describe the heap-scan phase; they are not a universal percent complete because index cleanup, heap cleanup, truncation, repeated index cycles, and final work are separate phases. VACUUM FULL is a table rewrite and appears in pg_stat_progress_cluster instead. Join activity data when a worker looks stalled so a wait can be distinguished from cost-based throttling or normal phase work.
SELECT p.pid,
p.relid::regclass AS relation,
p.phase,
p.heap_blks_scanned,
p.heap_blks_total,
p.index_vacuum_count,
a.wait_event_type,
a.wait_event
FROM pg_stat_progress_vacuum AS p
JOIN pg_stat_activity AS a USING (pid)
ORDER BY p.pid; Tune exceptional tables from an explicit budget
For updates and deletes, autovacuum's trigger is based on a fixed threshold plus a scale factor times the estimated table size, capped by autovacuum_vacuum_max_threshold in current PostgreSQL. On a very large, high-churn table, a lower per-table scale factor can start cleanup sooner without making every table equally aggressive. Worker count, work memory, cost delay, I/O capacity, and the number of busy databases still limit throughput. Measure backlog and runtime before and after each change, and keep anti-wraparound protection enabled.
SELECT name, setting, unit, source
FROM pg_settings
WHERE name IN (
'autovacuum',
'autovacuum_max_workers',
'autovacuum_naptime',
'autovacuum_vacuum_threshold',
'autovacuum_vacuum_scale_factor',
'autovacuum_vacuum_max_threshold'
)
ORDER BY name;
ALTER TABLE public.orders SET (
autovacuum_vacuum_scale_factor = 0.01,
autovacuum_vacuum_threshold = 5000
);
SELECT relname, reloptions
FROM pg_class
WHERE oid = 'public.orders'::regclass; Monitor freezing as a data-safety requirement
PostgreSQL's finite transaction ID space requires every table to be vacuumed and old row versions frozen before wraparound. age(datfrozenxid) gives the database-level lower bound, while age(relfrozenxid) locates old relations within the connected database. Anti-wraparound autovacuum can run even when ordinary autovacuum is disabled and should not be canceled casually. Alert with substantial headroom below autovacuum_freeze_max_age and investigate why work is delayed rather than relying on emergency VACUUM FREEZE.
SELECT datname, age(datfrozenxid) AS xid_age
FROM pg_database
ORDER BY xid_age DESC;
SELECT n.nspname AS schema_name,
c.relname,
age(c.relfrozenxid) AS xid_age
FROM pg_class AS c
JOIN pg_namespace AS n ON n.oid = c.relnamespace
WHERE c.relkind IN ('r', 'm')
ORDER BY xid_age DESC
LIMIT 25;
SHOW autovacuum_freeze_max_age; Find horizons that prevent dead-row removal
A vacuum can scan successfully yet leave row versions that remain visible to an old snapshot or reserved by replication. Long-running or idle-in-transaction sessions, prepared transactions, and physical or logical replication slots can hold cleanup horizons. Inventory them before intervening, confirm ownership and downstream dependencies, and use application-specific resolution procedures; terminating a session, resolving a prepared transaction, or dropping a slot changes external state and can cause outages or replica rebuilds.
SELECT pid, usename, application_name, state,
xact_start, backend_xmin, age(backend_xmin) AS xmin_age
FROM pg_stat_activity
WHERE backend_xmin IS NOT NULL
ORDER BY xmin_age DESC;
SELECT gid, prepared, owner, database,
age(transaction) AS xid_age
FROM pg_prepared_xacts
ORDER BY xid_age DESC;
SELECT slot_name, slot_type, active, xmin, catalog_xmin,
restart_lsn, confirmed_flush_lsn
FROM pg_replication_slots
ORDER BY slot_name; Reduce new bloat with HOT-friendly design
A HOT update can avoid creating successor entries in indexes when indexed columns are unchanged and PostgreSQL can place the new row version on the same heap page. Avoid indexing frequently changed columns without a demonstrated query need. For newly designed high-churn tables, a lower fillfactor can reserve page space for updates, trading a larger initial heap for fewer new-page updates and less index churn. Validate with n_tup_hot_upd and n_tup_upd trends; changing fillfactor alone does not rewrite existing pages.
CREATE TABLE account_state (
account_id bigint PRIMARY KEY,
status text NOT NULL,
balance numeric(14,2) NOT NULL,
updated_at timestamptz NOT NULL
) WITH (fillfactor = 80);
SELECT relname,
n_tup_upd,
n_tup_hot_upd,
n_tup_newpage_upd
FROM pg_stat_user_tables
WHERE relname = 'account_state'; Separate reusable space from rewrite-worthy bloat
Standard VACUUM normally makes dead space reusable inside the relation rather than shrinking its file, so a large table can be healthy when that space is reused by continuing writes. Relation-size growth plus workload and vacuum trends is evidence for investigation, not proof of bloat. VACUUM FULL compacts by rewriting the table, requires ACCESS EXCLUSIVE, needs extra temporary disk, and blocks concurrent table use; it is not routine maintenance. Confirm that space must be returned to the operating system and rehearse the lock, disk, WAL, and recovery impact before any rewrite.
SELECT c.oid::regclass AS relation,
pg_size_pretty(pg_relation_size(c.oid)) AS heap_size,
pg_size_pretty(pg_indexes_size(c.oid)) AS index_size,
pg_size_pretty(pg_total_relation_size(c.oid)) AS total_size,
s.n_live_tup,
s.n_dead_tup,
s.last_autovacuum
FROM pg_class AS c
JOIN pg_stat_user_tables AS s ON s.relid = c.oid
WHERE c.oid = 'public.orders'::regclass; Sources and further reading
References
Authoritative documentation used to verify and expand this cheat sheet.
- PostgreSQL Global Development GroupPostgreSQL: Routine Vacuumingpostgresql.org
- PostgreSQL Global Development GroupPostgreSQL: VACUUMpostgresql.org
- PostgreSQL Global Development GroupPostgreSQL: Vacuuming Configurationpostgresql.org
- PostgreSQL Global Development GroupPostgreSQL: Progress Reportingpostgresql.org
- PostgreSQL Global Development GroupPostgreSQL: Cumulative Statistics Systempostgresql.org
- PostgreSQL Global Development GroupPostgreSQL: Heap-Only Tuplespostgresql.org
Help us improve
Found a typo or missing example?
Tell us what would make this cheat sheet clearer, more complete, or more useful.



