The essentials

Quick reference

One focused task per row. Jump to the related section for complete, working examples.

UseSyntaxExamples
Vacuum one tableVACUUM public.orders;View examples
Vacuum and refresh statisticsVACUUM (ANALYZE, VERBOSE) public.orders;View examples
Inspect table maintenance healthSELECT 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 statisticsSELECT relname, n_mod_since_analyze, last_autoanalyze FROM pg_stat_user_tables ORDER BY n_mod_since_analyze DESC;View examples
Monitor active vacuum workSELECT pid, relid::regclass, phase, heap_blks_scanned, heap_blks_total FROM pg_stat_progress_vacuum;View examples
Identify autovacuum sessionsSELECT pid, backend_type, query_start, wait_event_type, wait_event FROM pg_stat_activity WHERE backend_type = 'autovacuum worker';View examples
Inspect autovacuum settingsSELECT name, setting, unit FROM pg_settings WHERE name LIKE 'autovacuum%' ORDER BY name;View examples
Tune a high-churn tableALTER TABLE public.orders SET (autovacuum_vacuum_scale_factor = 0.01, autovacuum_vacuum_threshold = 5000);View examples
Inspect table overridesSELECT relname, reloptions FROM pg_class WHERE oid = 'public.orders'::regclass;View examples
Rank database XID ageSELECT datname, age(datfrozenxid) AS xid_age FROM pg_database ORDER BY xid_age DESC;View examples
Rank table XID ageSELECT 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 horizonsSELECT 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 transactionsSELECT gid, prepared, owner, database, age(transaction) FROM pg_prepared_xacts ORDER BY age(transaction) DESC;View examples
Inspect replication slot horizonsSELECT slot_name, slot_type, active, xmin, catalog_xmin FROM pg_replication_slots ORDER BY slot_name;View examples
Measure HOT update useSELECT relname, n_tup_upd, n_tup_hot_upd FROM pg_stat_user_tables ORDER BY n_tup_upd DESC;View examples
Reserve heap page spaceCREATE TABLE account_state (account_id bigint PRIMARY KEY, status text NOT NULL) WITH (fillfactor = 80);View examples
Compare table and total sizeSELECT pg_relation_size('public.orders'), pg_total_relation_size('public.orders');View examples
Rank total relation sizesSELECT 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

01

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.

Perform targeted maintenance with diagnostic output
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;
Back to quick reference ↑
02

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.

Rank tables that may be falling behind
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;
Back to quick reference ↑
03

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.

Read phase, scan progress, and wait state together
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;
Back to quick reference ↑
04

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.

Inspect effective settings, then scope a reviewed override
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;
Back to quick reference ↑
05

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.

Review database and table XID ages against configuration
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;
Back to quick reference ↑
06

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.

Inventory session, prepared-transaction, and slot horizons
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;
Back to quick reference ↑
07

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.

Design page headroom and observe update behavior
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';
Back to quick reference ↑
08

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.

Compare heap, indexes, and total footprint without rewriting
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;
Back to quick reference ↑

Sources and further reading

References

Authoritative documentation used to verify and expand this cheat sheet.

  1. PostgreSQL Global Development GroupPostgreSQL: Routine Vacuumingpostgresql.org
  2. PostgreSQL Global Development GroupPostgreSQL: VACUUMpostgresql.org
  3. PostgreSQL Global Development GroupPostgreSQL: Vacuuming Configurationpostgresql.org
  4. PostgreSQL Global Development GroupPostgreSQL: Progress Reportingpostgresql.org
  5. PostgreSQL Global Development GroupPostgreSQL: Cumulative Statistics Systempostgresql.org
  6. 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.

Share feedback