Filter concepts by levelShowing all levels.

PostgreSQL · Vacuuming

VACUUM, ANALYZE and Autovacuum

VACUUM vs VACUUM FULL vs ANALYZE, why autovacuum is essential infrastructure, bloat as a moving balance, how one open transaction can block cleanup database-wide, stale statistics, and the threshold formula behind it all

  • VACUUM reuses, ANALYZE informstwo independent concerns, often run together
  • Per-table, not scheduledautovacuum reacts to each table's own activity
  • Bloat is a moving balancecreated by writes, reclaimed by VACUUM
  • One session, database-wide blocka stuck transaction holds back cleanup everywhere

1.VACUUM, VACUUM FULL and ANALYZE

VACUUM reclaims dead tuple space for reuse. VACUUM FULL rewrites the whole table to reclaim space AND shrink the file, at the cost of an exclusive lock. ANALYZE is a separate job entirely: it collects statistics about a table's data distribution so the query planner can make good decisions — it does not touch dead tuples at all. VACUUM ANALYZE runs both together in one pass.

Key features

  • VACUUM: reclaims dead tuple space for reuse within the table, does not shrink the file, requires no exclusive lock
  • VACUUM FULL: rewrites the table into a new, compact file, does shrink it and returns space to the OS, requires an ACCESS EXCLUSIVE lock
  • ANALYZE: collects statistics (row count estimate, most-common values, histograms) the query planner uses — completely separate concern from dead tuples
  • VACUUM ANALYZE (or just letting autovacuum handle both) runs both operations together in a single table scan
  • Stale statistics from a missing ANALYZE can cause bad query plans even on a table with zero bloat — the two problems are independent

VACUUM vs VACUUM FULL vs ANALYZE

VACUUM vs VACUUM FULL vs ANALYZE
CommandWhat it addressesLock required
VACUUMdead tuple space (reuse, not shrink)none blocking ordinary access
VACUUM FULLdead tuple space (reuse AND shrink)ACCESS EXCLUSIVE
ANALYZEplanner statistics — unrelated to dead tuplesnone blocking ordinary access
Two unrelated jobs that happen to run together

VACUUM / VACUUM FULL

  • Reclaims dead tuple space
  • VACUUM: reuse only, no lock; FULL: reuse + shrink, ACCESS EXCLUSIVE lock
  • Never touches planner statistics

ANALYZE

  • Refreshes row-count and value-distribution statistics
  • The query planner reads these to choose a plan
  • Never touches dead tuples
  • VACUUM / VACUUM FULL
    • Reclaims dead tuple space
    • VACUUM: reuse only, no lock; FULL: reuse + shrink, ACCESS EXCLUSIVE lock
    • Never touches planner statistics
  • ANALYZE
    • Refreshes row-count and value-distribution statistics
    • The query planner reads these to choose a plan
    • Never touches dead tuples

Command

sql
VACUUM accounts;               -- reclaim dead tuple space for reuse
ANALYZE accounts;              -- refresh planner statistics
VACUUM ANALYZE accounts;       -- both, in one pass
VACUUM FULL accounts;          -- reclaim AND shrink -- rare, blocking

VACUUM reclaims dead tuple space for reuse (not shrink). VACUUM FULL reclaims AND shrinks, at the cost of an ACCESS EXCLUSIVE lock. ANALYZE refreshes planner statistics — a completely separate concern from dead tuples. A table can need one badly without needing the other at all.

2.Autovacuum Workers and Why Autovacuum Is Essential

Autovacuum is a background process that automatically runs VACUUM (and ANALYZE) on tables once they accumulate enough dead tuples or data changes, using one or more worker processes so a busy database does not need a human to remember to run VACUUM manually. It is essential because without it, dead tuples and stale statistics would accumulate indefinitely on any actively-written table, degrading performance and eventually risking transaction ID wraparound.

Key features

  • Autovacuum runs as one or more background worker processes, launched by the autovacuum launcher process on a periodic check (autovacuum_naptime, default 1 minute)
  • autovacuum_max_workers caps how many tables can be vacuumed concurrently — a large database with many tables needing attention at once can exhaust this pool
  • Autovacuum decides per-table, using each table's own accumulated dead-tuple/change count against a threshold, not a global fixed schedule
  • Without autovacuum, dead tuples accumulate indefinitely on any actively-written table, and transaction ID wraparound protection is the ultimate, database-halting consequence of statistics/vacuum falling far enough behind
  • Autovacuum is enabled by default and disabling it globally is strongly discouraged — it is considered essential infrastructure, not an optional tuning feature

Manual VACUUM vs autovacuum

Manual VACUUM vs autovacuum
PropertyManual VACUUM onlyAutovacuum
Triggera human remembers to run itper-table thresholds, checked automatically
Adapts to per-table write patternsno — same schedule for every tableyes — busier tables vacuum more often
Risk if forgottenunbounded bloat, stale stats, wraparound risklow — runs automatically as needed
A self-regulating loop, not a human-remembered schedule

launcher checks

every autovacuum_naptime (default 1 min)

per-table threshold

compares each table's own dead-tuple/change count

worker runs VACUUM/ANALYZE

only on tables that crossed their threshold

  1. launcher checks — every autovacuum_naptime (default 1 min)
  2. per-table threshold — compares each table's own dead-tuple/change count
  3. worker runs VACUUM/ANALYZE — only on tables that crossed their threshold

Command

sql
SHOW autovacuum;              -- on (the default)
SHOW autovacuum_max_workers;  -- 3 (the default)
SHOW autovacuum_naptime;      -- 1min (the default check interval)

-- see autovacuum activity per table
SELECT relname, last_autovacuum, last_autoanalyze
  FROM pg_stat_user_tables;

Autovacuum is a background process (one or more workers, launched periodically) that decides per-table, from each table's own accumulated dead-tuple/change count, when to run VACUUM/ANALYZE automatically — turning vacuuming from a manual scheduling problem into a self-regulating one. Never disable it globally; tune its aggressiveness per-table instead.

3.Routine Vacuum vs Table-Rewriting VACUUM FULL

The documented operational goal is running routine VACUUM (via autovacuum, ordinarily) often enough that a table never accumulates enough bloat to need VACUUM FULL at all — routine vacuuming is meant to be the steady-state norm, and VACUUM FULL is meant to be a rare, deliberate recovery operation for a table that has already bloated far beyond what routine vacuuming alone can fix.

Key features

  • The recommended strategy: moderately-frequent routine VACUUM (usually via autovacuum) rather than infrequent VACUUM FULL runs
  • VACUUM FULL is appropriate for recovery — a table that has already bloated significantly, often because autovacuum was disabled, misconfigured, or blocked by a long-running transaction for an extended period
  • Needing VACUUM FULL repeatedly on the same table is a signal to investigate why routine vacuuming is not keeping up, not a sign to schedule VACUUM FULL as routine maintenance itself
  • pg_stat_user_tables' n_dead_tup relative to n_live_tup, tracked over time, is the practical signal for whether routine vacuuming is keeping pace
  • A table with a stable, low dead-tuple ratio needs no VACUUM FULL at all, regardless of how much total write activity it has seen historically

When each is the appropriate response

When each is the appropriate response
SituationAppropriate response
Ordinary, ongoing write activityroutine VACUUM (autovacuum) — the steady-state norm
A table that bloated after autovacuum was disabled/blocked for a long timeVACUUM FULL — deliberate, one-time recovery
VACUUM FULL "needed" every week on the same tableinvestigate why routine vacuuming is falling behind — do not just keep running VACUUM FULL

Command

sql
-- the routine, steady-state signal to check:
SELECT relname, n_live_tup, n_dead_tup,
       round(100.0 * n_dead_tup / GREATEST(n_live_tup, 1), 1) AS dead_pct
  FROM pg_stat_user_tables
 ORDER BY dead_pct DESC
 LIMIT 10;

The goal is routine VACUUM (via autovacuum) running often enough that VACUUM FULL is never actually needed. VACUUM FULL is a rare, deliberate recovery operation, not routine maintenance — needing it repeatedly on the same table is a signal that routine vacuuming has fallen behind for a fixable reason, worth diagnosing rather than working around.

4.Table and Index Bloat

Bloat is disk space consumed by dead tuples (or, for an index, dead index entries) that VACUUM has not yet reclaimed — it makes tables and their indexes physically larger than the live data alone would require, slowing down scans that now have to skip over more dead space to find live rows, and wasting cache memory on pages that hold no useful data.

Key features

  • Table bloat: dead tuples not yet reclaimed by VACUUM, inflating the table's physical size beyond its live row count
  • Index bloat: a parallel problem on indexes — dead index entries pointing at now-dead row versions also need reclaiming
  • Bloat degrades performance even for queries that never touch the dead rows directly, since scans and cache usage both pay for the extra physical size
  • Bloat is a moving balance (write activity creates it, VACUUM reclaims it), not a one-time condition — the dead-tuple ratio over time is the practical signal to watch
  • High bloat despite regular autovacuum activity usually points to a specific cause: a blocking long-running transaction, undersized thresholds for a hot table, or a workload creating dead tuples faster than autovacuum can keep up

What bloat costs

What bloat costs
CostWhy it happens
Slower sequential/index scansmore physical pages to read for the same live data
Wasted cache (shared_buffers) spacecache holds dead-tuple pages instead of useful data
Larger backups and higher storage costthe table file itself is physically larger
Index bloat specificallyslower index scans, larger index files, same root cause
Bloat is a moving balance, not a fixed property
normallyif vacuumingfalls behind

Write activity

UPDATE / DELETE create dead tuples

Dead tuples accumulate

inflate physical size beyond live data

VACUUM reclaims

shrinks the backlog, if it keeps pace

Long transaction blocks it

reclaiming stalls — bloat grows unbounded

  • Write activity — UPDATE / DELETE create dead tuples
    • leads to Dead tuples accumulate
  • Dead tuples accumulate — inflate physical size beyond live data
    • leads to VACUUM reclaims (normally)
    • on error, leads to Long transaction blocks it (if vacuuming falls behind)
  • VACUUM reclaims — shrinks the backlog, if it keeps pace
  • Long transaction blocks it — reclaiming stalls — bloat grows unbounded

Command

sql
-- the practical bloat signal: dead tuples relative to live ones
SELECT relname, n_live_tup, n_dead_tup,
       round(100.0 * n_dead_tup / GREATEST(n_live_tup + n_dead_tup, 1), 1) AS dead_pct
  FROM pg_stat_user_tables
 ORDER BY n_dead_tup DESC
 LIMIT 10;

Bloat is the moving balance of dead tuples (table) or dead entries (index) not yet reclaimed by VACUUM — it degrades every access pattern by inflating physical size and wasting cache. n_dead_tup relative to n_live_tup in pg_stat_user_tables is the practical signal; check it before assuming a slow query needs a schema or index fix.

5.How Long-Running Transactions Can Prevent Cleanup

VACUUM can only remove a dead tuple once it is certain no open transaction's snapshot could still legitimately need to see it — so a single long-running or idle-in-transaction session, anywhere in the database, holds back the point past which VACUUM can safely reclaim ANY dead tuple, on ANY table, not just tables that session has touched.

Key features

  • VACUUM cannot remove a dead tuple if any open transaction's snapshot could still need it — this check is conservative and database-wide, not scoped to tables the long transaction has touched
  • pg_stat_activity's backend_xmin column shows the oldest row version a given session's snapshot might still need
  • age(backend_xmin) growing large for any session is the direct, queryable signal of this problem
  • An idle-in-transaction session (one that opened a transaction and then stopped doing anything) is the most common real-world cause — often an application bug, a forgotten interactive psql session, or a connection pool holding a transaction open
  • The fix is always the same: commit, roll back, or terminate the offending session — there is no way to "vacuum around" an open transaction's snapshot
Why the block is database-wide, not table-scoped
conservativeassumptionappliesdatabase-wide

One idle-in-transaction session

never touched table X

VACUUM cannot know its future queries

might query X next

VACUUM on table X is held back

even though the session never used it

  • One idle-in-transaction session — never touched table X
    • leads to VACUUM cannot know its future queries (conservative assumption)
  • VACUUM cannot know its future queries — might query X next
    • leads to VACUUM on table X is held back (applies database-wide)
  • VACUUM on table X is held back — even though the session never used it

Command

sql
-- find the session(s) holding back the vacuum horizon
SELECT pid, state, age(backend_xmin) AS xmin_age, now() - xact_start AS duration
  FROM pg_stat_activity
 WHERE backend_xmin IS NOT NULL
 ORDER BY xmin_age DESC
 LIMIT 10;

VACUUM cannot reclaim a dead tuple if ANY open transaction's snapshot could still need it — this is a conservative, database-wide check, so one idle-in-transaction session can block cleanup on tables it never even touched. Diagnose via pg_stat_activity's backend_xmin age across the WHOLE database, not just activity on the bloated table itself.

6.Statistics Collection and Stale Statistics

ANALYZE samples a table's rows and records what it finds — an estimated row count, the most common values per column, and how values are distributed — into pg_statistic, which the query planner reads when deciding how to execute a query. When those statistics no longer reflect reality (after a large bulk load, a big DELETE, or a data distribution that has genuinely shifted), the planner's cost estimates go wrong and it can choose a bad plan even though nothing about the schema or indexes changed.

Key features

  • ANALYZE stores its findings in pg_statistic (and the more readable pg_stats view) — estimated row count, most-common values, histogram boundaries, correlation
  • The planner uses these stored statistics, not a live read of the data, to estimate how many rows a WHERE clause will match and choose a plan accordingly
  • A large bulk INSERT, a big DELETE, or a substantial shift in a column's value distribution can all leave existing statistics badly stale until the next ANALYZE
  • autovacuum triggers ANALYZE automatically based on how many rows have changed since the last one (the analyze threshold/scale factor), similar to how it triggers VACUUM
  • Stale statistics are a common, underappreciated cause of "this query used to be fast" reports — nothing about the query or schema changed, only the gap between reality and what the planner believes

What ANALYZE records vs what it is used for

What ANALYZE records vs what it is used for
Recorded (pg_stats)Used by the planner to estimate
Estimated row countoverall table size for cost calculations
Most common values + their frequencieshow selective an equality filter (WHERE x = value) really is
Histogram of value rangeshow selective a range filter (WHERE x > value) really is
Correlation (physical vs logical ordering)whether an index scan or sequential scan is cheaper
Statistics drift out of sync with a bulk load
  1. T0

    1,000 rows, evenly distributed

    planner statistics match reality

  2. T1

    bulk load: +990,000 rows, all "shipped"

    pg_statistic is NOT updated by the INSERT itself

  3. T2

    query plan degrades

    planner still assumes the old, now-wrong distribution

  4. T3

    ANALYZE runs

    statistics refreshed — plan recovers

  1. T0: 1,000 rows, evenly distributed — planner statistics match reality
  2. T1: bulk load: +990,000 rows, all "shipped" — pg_statistic is NOT updated by the INSERT itself
  3. T2: query plan degrades — planner still assumes the old, now-wrong distribution
  4. T3: ANALYZE runs — statistics refreshed — plan recovers

Command

sql
SELECT attname, n_distinct, most_common_vals, last_analyze
  FROM pg_stats JOIN pg_stat_user_tables USING (relname)
 WHERE tablename = 'orders' AND attname = 'status';

ANALYZE orders;  -- refresh the statistics explicitly

ANALYZE records what the planner believes about the data (row counts, most-common values, histograms) into pg_statistic — a snapshot, not a live view. A large bulk load, big DELETE, or distribution shift can leave that snapshot badly stale until the next ANALYZE, producing a query plan that looks wrong for no reason visible in the schema or indexes. Check last_analyze before assuming a slow query needs an indexing fix.

7.Autovacuum Thresholds and Scale Factors

Autovacuum decides when to vacuum a table using a formula: threshold = base threshold + scale factor × table size (in rows). The base threshold (default 50) is a small fixed floor so tiny tables still get vacuumed promptly; the scale factor (default 0.1, meaning 10%) makes the real trigger point scale with table size, so a huge table does not get vacuumed constantly over proportionally tiny changes.

Key features

  • Vacuum threshold formula: autovacuum_vacuum_threshold + autovacuum_vacuum_scale_factor × reltuples (defaults: 50 + 0.1 × row count)
  • Analyze threshold formula: the same shape, with its own separate parameters (autovacuum_analyze_threshold, default 50; autovacuum_analyze_scale_factor, default 0.1)
  • A table is also always vacuumed once its relfrozenxid age exceeds autovacuum_freeze_max_age, regardless of the dead-tuple threshold — the wraparound-prevention trigger
  • Both threshold and scale factor can be overridden per table via storage parameters, which is the standard way to tune a specific hot table more aggressively
  • A very large table with the default 0.1 scale factor can accumulate a large ABSOLUTE number of dead tuples before triggering — often the reason to lower the scale factor for specific large, high-churn tables

The formula, worked out for two table sizes

The formula, worked out for two table sizes
Table sizeThreshold (50 + 0.1 × rows)Dead tuples needed to trigger
1,000 rows50 + 0.1 × 1,000150
10,000,000 rows50 + 0.1 × 10,000,000~1,000,050

Command

sql
SHOW autovacuum_vacuum_threshold;      -- 50
SHOW autovacuum_vacuum_scale_factor;   -- 0.1

-- tune a specific hot, large table more aggressively
ALTER TABLE orders SET (
  autovacuum_vacuum_scale_factor = 0.02,
  autovacuum_vacuum_threshold = 1000
);

Autovacuum's trigger point is threshold = base threshold + scale factor × table size, applied separately to vacuum and analyze — a small base floor keeps tiny tables responsive, while the size-proportional scale factor keeps huge tables from vacuuming constantly. Tune scale factor/threshold PER TABLE for an exceptional hot table, never globally for one table's needs.

Remember

  • VACUUM reclaims dead tuple space for reuse; ANALYZE refreshes planner statistics — independent concerns, commonly run together as VACUUM ANALYZE.
  • Autovacuum decides per-table, from each table's own accumulated changes — never disable it globally; tune thresholds per table instead.
  • The goal is routine VACUUM keeping pace so VACUUM FULL is never actually needed — needing it repeatedly signals a fixable root cause.
  • One idle-in-transaction session can block VACUUM database-wide, on tables it never touched — diagnose via backend_xmin age across ALL sessions.
  • Stale statistics (not the schema or indexes) are a common cause of a query plan degrading after a large data change — check last_analyze first.

Pro tip

  • threshold = base threshold + scale factor × table size — a small floor for tiny tables, a proportional cadence for huge ones.
  • pg_stat_user_tables' n_dead_tup / n_live_tup ratio is the practical, at-a-glance bloat signal.
  • Tune autovacuum_vacuum_scale_factor per table for a specific hot table — never lower it globally just to fix one table.

Next up

  • Storage and Table Internals
  • Index Fundamentals
  • B-Tree Indexes

Sources

  • Routine Vacuuming — postgresql.org/docs/17/routine-vacuuming.html
  • Every example executed on PostgreSQL 17.11 · checked 2026-08-24

Spotted a mistake on this page? Tell us.