PostgreSQL Mastery
PostgreSQL Mastery lessons / Module 22: VACUUM

VACUUM, autovacuum and bloat

Level 5: Internals30 min

VACUUM removes dead tuples so their space can be reused, updates the visibility map for index-only scans, and freezes old rows. Autovacuum runs it automatically; tuning autovacuum for large, busy tables is one of the most important PostgreSQL operations skills.

This lesson is interactive. Run every example on a real PostgreSQL 18 server in your browser, read EXPLAIN ANALYZE plans, and take the quiz.
Open the interactive lesson
You will learn to
  • Explain what VACUUM, VACUUM FULL and ANALYZE each do
  • Read autovacuum triggers and tune them per table
  • Measure dead tuples and bloat, and know what stops VACUUM from cleaning

conceptThe concept

Plain VACUUM scans pages that may contain dead tuples, removes those no snapshot can see, removes their index entries, records free space in the free space map, and sets visibility-map bits. It runs alongside normal reads and writes. It only gives space back to the operating system when empty pages sit at the end of the table.

VACUUM FULL rewrites the table and its indexes into new compact files. It needs an ACCESS EXCLUSIVE lock for the whole duration, blocking even reads, plus room for a full copy. In production, pg_repack does the same online.

Autovacuum vacuums a table when dead tuples exceed autovacuum_vacuum_threshold + autovacuum_vacuum_scale_factor * reltuples (50 + 20%), and also after many inserts. For a 500 million row table, 20% is 100 million dead rows before it starts, so large tables get per-table settings such as autovacuum_vacuum_scale_factor = 0.01. Workers are throttled by autovacuum_vacuum_cost_limit so they do not saturate I/O.

whyWhy it matters

When autovacuum cannot keep up, tables and indexes bloat, queries read more pages, index-only scans degrade to heap fetches, and eventually transaction-ID wraparound protection forces aggressive vacuums at the worst possible time.

Use caseReal-world scenario

A nightly job updates every row of a 50 GB table. The table doubles in size every night. Batching the update, letting VACUUM run between batches and lowering the table's scale factor keeps it stable.

Try itExamples

Run and edit these in the interactive lesson.

Create bloat and clean it up
DROP TABLE IF EXISTS bloat_lab;
CREATE TABLE bloat_lab WITH (autovacuum_enabled = off) AS
  SELECT g AS id, repeat('x', 100) AS filler FROM generate_series(1, 20000) g;
SELECT pg_size_pretty(pg_relation_size('bloat_lab')) AS after_load;

UPDATE bloat_lab SET filler = repeat('y', 100);
SELECT pg_stat_force_next_flush();
SELECT n_live_tup, n_dead_tup, pg_size_pretty(pg_relation_size('bloat_lab')) AS after_update
FROM pg_stat_user_tables WHERE relname = 'bloat_lab';

VACUUM bloat_lab;
SELECT pg_stat_force_next_flush();
SELECT n_dead_tup, pg_size_pretty(pg_relation_size('bloat_lab')) AS after_vacuum
FROM pg_stat_user_tables WHERE relname = 'bloat_lab';

VACUUM FULL bloat_lab;
SELECT pg_size_pretty(pg_relation_size('bloat_lab')) AS after_vacuum_full;
DROP TABLE bloat_lab;
The UPDATE doubles the table (every row now has a dead old version). VACUUM removes the dead tuples but the file stays the same size, ready for reuse. VACUUM FULL rewrites it compactly, under an exclusive lock.
Is autovacuum keeping up?
SELECT relname, n_live_tup, n_dead_tup,
       round(100.0 * n_dead_tup / greatest(n_live_tup, 1), 1) AS dead_pct,
       last_vacuum, last_autovacuum, last_analyze, last_autoanalyze, vacuum_count, autovacuum_count
FROM pg_stat_user_tables
ORDER BY n_dead_tup DESC;
When will autovacuum trigger for each table?
SELECT c.relname,
       c.reltuples::bigint AS rows,
       current_setting('autovacuum_vacuum_threshold')::int
         + current_setting('autovacuum_vacuum_scale_factor')::float8 * c.reltuples AS dead_tuples_to_trigger
FROM pg_class c
WHERE c.relnamespace = 'public'::regnamespace AND c.relkind = 'r'
ORDER BY c.reltuples DESC;
Per-table autovacuum settings
BEGIN;
ALTER TABLE events SET (autovacuum_vacuum_scale_factor = 0.01, autovacuum_vacuum_threshold = 1000, autovacuum_analyze_scale_factor = 0.02);
SELECT relname, reloptions FROM pg_class WHERE relname = 'events';
ROLLBACK;

internalsUnder the hood

Three things stop VACUUM from removing dead tuples: a long-running or idle-in-transaction session, a replication slot (or a standby with hot_standby_feedback) whose xmin is old, and orphaned prepared transactions. All three show up as an old backend_xmin or slot xmin, and VACUUM VERBOSE reports "dead but not yet removable" tuples.

Avoid / preferDo this, not that

Avoid
VACUUM FULL orders;  -- weekly cron
Prefer
-- tune autovacuum for the table; for real bloat, rebuild online with
-- pg_repack / pg_squeeze, or REPACK (CONCURRENTLY) orders; on PostgreSQL 19

VACUUM FULL rewrites the table under an ACCESS EXCLUSIVE lock, blocking reads and writes for the duration. Routine plain VACUUM keeps space reusable without it.

Avoid
ALTER TABLE events SET (autovacuum_enabled = false);
Prefer
ALTER TABLE events SET (autovacuum_vacuum_scale_factor = 0.02, autovacuum_vacuum_cost_limit = 2000);

Disabling autovacuum leads to bloat and eventually a forced anti-wraparound vacuum at the worst moment. Make it more aggressive instead.

PitfallsCommon mistakes

  • Disabling autovacuum on a busy table because it "uses I/O". Bloat and wraparound protection will cost far more later.
  • Running VACUUM FULL in business hours. It locks the table against reads for the whole rewrite; use pg_repack.
  • Leaving the default 20% scale factor on huge tables, so autovacuum runs rarely and takes hours when it does.

LimitsWhen not to use it

Avoid VACUUM FULL and CLUSTER on busy tables unless you can afford the exclusive lock; they are for maintenance windows or for tables nobody is using. PostgreSQL 19 folds both into REPACK, whose CONCURRENTLY option holds the exclusive lock only for the final swap.

SecuritySecurity notes

Since PostgreSQL 17, the MAINTAIN privilege and the pg_maintain role let operators run VACUUM, ANALYZE and REINDEX without owning tables or being superuser.

ChallengeTry it yourself

Show each table's live and dead tuples, the dead percentage and the last autovacuum time, worst first.

Show one solution
One solution
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
FROM pg_stat_user_tables
ORDER BY dead_pct DESC NULLS LAST;

InterviewQuestions you should be able to answer

What is the difference between VACUUM and VACUUM FULL?
VACUUM marks space from dead tuples as reusable, cleans index entries, updates the free space and visibility maps and freezes old tuples, running concurrently with reads and writes; it rarely shrinks the file. VACUUM FULL rewrites the table and indexes into new files, returning space to the OS, but takes an ACCESS EXCLUSIVE lock for the duration and needs extra disk for the copy. pg_repack achieves the same rewrite with only brief locks.
Autovacuum runs but the table keeps growing. What do you check?
First, whether dead tuples are removable: look for long-running or idle-in-transaction sessions (pg_stat_activity backend_xmin), stale replication slots (pg_replication_slots xmin, catalog_xmin), standbys with hot_standby_feedback and old prepared transactions. Then whether autovacuum is keeping up: thresholds too high for the table size (lower scale_factor per table), throttling too aggressive (raise autovacuum_vacuum_cost_limit), too few workers, or it keeps getting cancelled by lock conflicts. VACUUM VERBOSE shows how many dead tuples could not be removed.

QuizCheck your understanding

4 questions with instant feedback in the interactive lesson; a wrong answer goes into your review deck.

Go deeperOfficial documentation