Symptoms
- Queries slow down over days or weeks, then speed up again after a manual
VACUUM. - The table's size on disk keeps growing even though its row count stays flat.
- Log lines like
WARNING: database "x" must be vacuumed within N transactions.
Which tables have the most dead rows?
SELECT relname,
n_live_tup,
n_dead_tup,
round(100.0 * n_dead_tup / nullif(n_live_tup + n_dead_tup, 0), 1) AS dead_pct,
last_autovacuum,
last_autoanalyze,
pg_size_pretty(pg_total_relation_size(relid)) AS total_size
FROM pg_stat_user_tables
ORDER BY n_dead_tup DESC
LIMIT 15;
If dead_pct is above about 20% and last_autovacuum is old or NULL, autovacuum isn't keeping up on that table.
What's blocking vacuum?
Vacuum can't remove rows that an open transaction might still see. Look for long-running and idle-in-transaction sessions:
SELECT pid, usename, application_name, state,
now() - xact_start AS xact_age,
left(query, 80) AS query
FROM pg_stat_activity
WHERE xact_start IS NOT NULL
ORDER BY xact_start
LIMIT 10;
Other things that hold back the xmin horizon:
SELECT slot_name, active, xmin, catalog_xmin FROM pg_replication_slots; -- abandoned slots
SELECT gid, prepared FROM pg_prepared_xacts; -- forgotten 2PC
Also check hot_standby_feedback = on on replicas, because long queries on a replica hold back vacuum on the primary.
Caution
An inactive replication slot prevents vacuum from cleaning up and also retains WAL until the disk fills. Drop slots you no longer use:
SELECT pg_drop_replication_slot('name');
Transaction ID wraparound risk
SELECT datname, age(datfrozenxid) AS xid_age,
round(100.0 * age(datfrozenxid) / 2000000000, 1) AS pct_to_wraparound
FROM pg_database ORDER BY xid_age DESC;
Above 50%, investigate. Above 75%, run VACUUM (FREEZE, VERBOSE) on the oldest tables during a quiet period.
Tune autovacuum for each table
The default autovacuum_vacuum_scale_factor of 0.2 means a 100 M-row table waits for 20 M dead rows before autovacuum runs. Lower it on large, busy tables:
ALTER TABLE orders SET (
autovacuum_vacuum_scale_factor = 0.02,
autovacuum_analyze_scale_factor = 0.01,
autovacuum_vacuum_cost_limit = 2000
);
Server-level settings to review:
| Parameter | Typical change |
|---|---|
autovacuum_max_workers | 3 → 5–8 on servers with many busy tables |
autovacuum_vacuum_cost_limit | Raise it, so vacuum isn't throttled on fast storage |
maintenance_work_mem / autovacuum_work_mem | 1–2 GB, for fewer index passes |
idle_in_transaction_session_timeout | Set it, for example 10min, to kill forgotten sessions |
Reclaiming space
VACUUM makes space reusable but rarely shrinks the files on disk. To give space back to the OS:
VACUUM FULL: rewrites the table under an ACCESS EXCLUSIVE lock, so it needs downtime.pg_repack: rewrites the table online with only brief locks. It's available on most managed services as an extension.
pg_repack -h $HOST -U $USER -d appdb -t public.orders --no-superuser-check