Monitor PostgreSQL Vacuum Progress with pg_stat_progress_vacuum
Tracking Active Vacuum and Autovacuum Workers in PostgreSQL
PostgreSQL vacuum is the maintenance process that removes dead tuples left behind by updates and deletes, reclaims storage, and prevents transaction ID wraparound. On active databases, autovacuum runs continuously in the background, but knowing whether it is actually making progress — and on which tables — requires querying the pg_stat_progress_vacuum view.
Introduced in PostgreSQL 9.6, pg_stat_progress_vacuum exposes one row per active vacuum worker, including both manual VACUUM commands and autovacuum background workers. The view shows the current phase, block counts scanned and vacuumed, and dead tuple statistics. This is the primary tool for answering the operational question: is vacuum keeping up, and if not, where is it stuck?
Sample Code
1SELECT
2 p.pid,
3 p.datname,
4 p.relid::regclass AS table_name,
5 p.phase,
6 p.heap_blks_total,
7 p.heap_blks_scanned,
8 p.heap_blks_vacuumed,
9 CASE p.heap_blks_total
10 WHEN 0 THEN 0
11 ELSE ROUND(
12 100.0 * p.heap_blks_vacuumed
13 / p.heap_blks_total, 1
14 )
15 END AS pct_vacuumed,
16 p.index_vacuum_count,
17 p.num_dead_tuples,
18 p.max_dead_tuples,
19 NOW() - a.xact_start AS elapsed
20FROM pg_stat_progress_vacuum p
21JOIN pg_stat_activity a ON a.pid = p.pid
22ORDER BY elapsed DESC NULLS LAST;
Notes: Requires PostgreSQL 9.6 or later. Superuser or pg_monitor role required to see all workers; non-superusers see only their own sessions. Returns no rows when no vacuum is currently running.
Code Breakdown
pg_stat_progress_vacuum— one row per active VACUUM worker; reset when the worker finishesp.relid::regclass— casts the relation OID to a human-readableschema.tablenamep.phase— current vacuum phase:initializing,scanning heap,vacuuming indexes,vacuuming heap,cleaning up indexes,truncating heap, orperforming final cleanupp.heap_blks_total / scanned / vacuumed— 8 KB blocks: total in the table, how many have been scanned, how many have been vacuumedpct_vacuumed— derived completion percentage based on blocks vacuumed vs. totalp.index_vacuum_count— number of completed index vacuum passes; large tables with many indexes may require multiple passesp.num_dead_tuples— dead tuples accumulated in the current passp.max_dead_tuples— ceiling before PostgreSQL must flush dead tuples and start an index pass (controlled bymaintenance_work_mem)NOW() - a.xact_start— time the vacuum worker has been running
Key Points
- If
pct_vacuumedis not advancing over repeated samples, the vacuum worker may be blocked by an idle-in-transaction session holding an old snapshot num_dead_tuplesapproachingmax_dead_tuplesrepeatedly meansmaintenance_work_memis too small; the worker must make extra index passes- A table stuck in
vacuuming indexesfor a long time usually indicates a large or bloated index that needs attention index_vacuum_count > 1on every run signals thatmaintenance_work_memshould be increased so the worker can process all dead tuples in a single heap pass- No rows in the view during known bloat growth means autovacuum is not keeping up — check
autovacuum_vacuum_scale_factorand worker count settings
Insights and Explanations
To find which sessions are blocking vacuum from advancing, check for idle-in-transaction connections holding old transaction snapshots:
1SELECT
2 pid,
3 datname,
4 usename,
5 state,
6 NOW() - xact_start AS xact_age,
7 LEFT(query, 80) AS last_query
8FROM pg_stat_activity
9WHERE state = 'idle in transaction'
10ORDER BY xact_age DESC NULLS LAST;
To check tables that autovacuum is skipping or deferring, query pg_stat_user_tables for last vacuum timestamps and dead tuple counts:
1SELECT
2 schemaname,
3 relname,
4 n_dead_tup,
5 last_autovacuum,
6 last_vacuum
7FROM pg_stat_user_tables
8WHERE n_dead_tup > 1000
9ORDER BY n_dead_tup DESC
10LIMIT 20;
If a specific table is accumulating dead tuples faster than autovacuum can clear them, run a manual VACUUM VERBOSE <table> during a low-traffic window to force a full pass and observe the output.
Additional Considerations
- Permissions: superuser or
pg_monitorrole (PostgreSQL 10+) required to see all vacuum workers - Version compatibility:
pg_stat_progress_vacuumavailable from PostgreSQL 9.6; themax_dead_tuplescolumn was added in PostgreSQL 13 maintenance_work_mem: controlsmax_dead_tuples; increasing this reduces index passes and speeds large vacuums; set per session withSET maintenance_work_mem = '1GB'before a manual VACUUM- Autovacuum vs. manual: both appear in this view; distinguish by querying
a.queryfrompg_stat_activity— autovacuum workers showautovacuum: VACUUM <table>
References
Posts in this series
- How Many Connections Can Your PostgreSQL Database Handle?
- PostgreSQL Backend Connections via pg_stat_database
- Identifying Blocking PostgreSQL Queries using pg_stat_activity
- List PostgreSQL Databases by Size with Access Check
- Assess PostgreSQL Database Sizes Quickly and Easily
- Unveiling Your PostgreSQL Server - A Diagnostic Powerhouse
- Keep Your PostgreSQL Database Clean, Identify Idle Connections
- Query the PostgreSQL Configuration
- PostgreSQL Recovery Monitoring: Essential SQL Insights
- Restart All PostgreSQL Sequences with ALTER SEQUENCE
- Monitor Running Queries in PostgreSQL using pg_stat_activity
- Monitor PostgreSQL Active Sessions with pg_stat_activity
- PostgreSQL Error Handling Settings via pg_settings
- PostgreSQL File Location Settings Query via pg_settings
- PostgreSQL Lock Management Settings via pg_settings
- PostgreSQL Logging Configuration Query via pg_settings
- Monitor PostgreSQL Memory Settings with pg_settings
- PostgreSQL Table Row Count Estimates with SQL
- List PostgreSQL Tables by Size with SQL
- PostgreSQL WAL Settings Query Guide
- PostgreSQL SSL Settings Query Guide
- PostgreSQL Statistics Settings Query Guide
- PostgreSQL Resource Settings Query Guide
- PostgreSQL Replication Settings Query Guide
- PostgreSQL Query Planning Settings Query Guide
- PostgreSQL Preset Options Settings Query Guide
- PostgreSQL Miscellaneous Settings Query Guide
- Count PostgreSQL Sessions by State with SQL
- Kill Idle PostgreSQL Sessions with SQL
- Grant SELECT on All Tables in PostgreSQL
- Identify Insert-Only Tables in PostgreSQL
- Detect Soft Delete Patterns in PostgreSQL
- List PostgreSQL Object Comments with SQL
- List Foreign Key Constraints in PostgreSQL
- List PostgreSQL Enum Types and Their Values with SQL
- List All Views in a PostgreSQL Database with SQL
- Find PostgreSQL Tables Without a Primary Key
- List PostgreSQL Partitioned Tables with SQL
- List All Schemas in Your PostgreSQL Database
- PostgreSQL Database Statistics with pg_stat_database
- List PostgreSQL Roles and Their Privileges
- Scrubbing Email PII in PostgreSQL for GDPR Compliance
- List Installed Extensions in PostgreSQL
- List Collations in Your PostgreSQL Database
- PostgreSQL Replica Identity for Logical Replication
- Monitor PostgreSQL Vacuum Progress with pg_stat_progress_vacuum
- Monitor PostgreSQL Wait Events Using pg_stat_activity