Skip to content
PostgreSQL plug-in

Vacuum Advisor

View Markdown

If a table keeps growing while autovacuum is nominally running, the answer is usually one of three things: the table’s own storage parameters moved its trigger point, cleanup is running but not often enough for the churn, or something is pinning the transaction horizon so no vacuum anywhere can remove the dead rows. Vacuum Advisor answers all three from PostgreSQL’s own statistics catalogs, names the table or the holder, and hands you the exact statement to run. It needs no extension for anything except the bloat estimate, and it never runs any of the SQL it recommends.

Every command on this page is advisory. The ALTER TABLE tuning statements, SELECT pg_terminate_backend(…), SELECT pg_drop_replication_slot(…) and ROLLBACK PREPARED all appear as text you copy and run yourself, in your own tooling, after you have decided they are safe. The plug-in executes none of them.

Prerequisites for this page

  • The core page is catalog-native. It reads pg_stat_user_tables, pg_class, pg_settings, pg_stat_activity, pg_replication_slots and pg_prepared_xacts under the monitoring role, with no extension required.
  • The Avoidable-Growth / Bloat Estimate section needs the pgstattuple extension installed on the monitored database. See Optional extensions. Without it that one section is empty and nothing else on the page changes.
  • No extra grant is needed for the estimate: pgstattuple’s approximate function requires pg_stat_scan_tables, which pg_monitor already includes. A table the role somehow cannot read is skipped.
  • The Autovacuum runs · 24h KPI is read through an Enterprise Manager job, so Preferred Credentials must be set for the target. It also needs two daily snapshots in the agent-local store before it can compute a delta.

Where to find it: on a PostgreSQL Database target, target navigation tree ▸ database name ▸ Vacuum Advisor. The same entry appears in the target menu. Two live watch pages sit under Realtime ▸ Vacuum xmin Horizon and Realtime ▸ Vacuums in Progress. The same findings alert through the target’s All Metrics.

In this page: The Vacuum Advisor page · Acting on a recommendation · Autovacuum frequency detection · Table bloat estimate · xmin horizon root cause · Wraparound monitoring · Vacuums in Progress

The page collects once when it loads and does not auto-refresh. The detections move slowly and re-polling would only re-run the heavier catalog queries. Reload the page when you want fresh numbers.

The Vacuum Advisor page with the XID Consumption panel, Vacuum Health KPIs and the Detections band

Top band: cluster XID position on the left, health KPIs and the xmin root-cause card on the right.

Read it top to bottom. The bands narrow from cluster-wide risk to the individual table.

One live line, built from the databases metric:

Cluster-wide max XID age: 148,332,190 — oldest database: template0 · ~6.9% of the 2B wraparound limit

Wraparound is cluster-scoped, so this deliberately reports the cluster’s oldest database rather than the one you are looking at. That is very often a template database, and it is normal. The panel notes underneath that snapshots are daily and alerting is active.

Two KPIs and a card.

KPIWhat it reports
Dead-tuple bloatDead tuples as a share of all rows, across the tables that have vacuum findings. Catalog-native.
Autovacuum runs · 24hHow many autovacuum runs happened across the database in the last day, computed by deltaing snapshots of PostgreSQL’s lifetime autovacuum_count.

The xmin-horizon root-cause card sits below them and has two states. When cleanup is pinned its left edge turns red and it reads Horizon blocked by <holder type> <holder> (PID <n>). Suggested action: <command>. When nothing is holding the horizon its left edge turns green and it reads “No session, replication slot or prepared transaction is currently holding the xmin horizon back.”

Autovacuum runs · 24h shows ”—” until it has the history it needs, with the tooltip “Accumulating: needs two daily snapshots to compute a delta”. Once it has them, the tooltip names the two snapshots the delta was computed over and how many tables were autovacuumed. A busy database reporting zero runs is itself a finding.

Four counters summarizing what the tables below contain.

KPIWhat it counts
Tables past autovacuum triggerTables whose real dead-tuple count is past their own recomputed trigger point.
Highest dead-tuple ratioThe worst dead-to-live ratio in the set.
Bloat findingsRows in the bloat estimate table. Zero when pgstattuple is absent.
xmin horizon blockedYes or No.
SectionWhat it carriesColumns
Tables · Vacuum RecommendationsThe slim per-table verdict list. Start here.Table, Dead Ratio, Last Autovacuum, Severity, Recommendation
xmin Horizon · Holder DetailEvery entity pinning the horizon, oldest first.Is Horizon, Holder Type, Holder, PID, Database, State, xmin Age, Wraparound Severity, Suggested Action
Per-Table Vacuum Recommendations (Full Detail)The evidence behind each verdict.Severity, Database, Schema, Table, Dead Tuples, Est. Live Rows, Trigger Point, Dead Ratio, Last Autovacuum, Autovacuum Count, Eff. Scale Factor, Reloptions, Recommendation
Avoidable-Growth / Bloat EstimateFree space and dead-tuple bloat per table, from pgstattuple.Severity, Database, Schema, Table, Table Size, Free %, Free Space, Dead Tuples, Dead Tuple %, Live Tuples

Two behaviors explain most “why isn’t my table here?” questions:

  • A table appears in the recommendation set only when it has dead tuples or carries per-table storage parameters. A clean, unconfigured table is not listed. A healthy table with notable reloptions is listed for visibility, with a comment-only recommendation.
  • An empty xmin Horizon · Holder Detail table is the healthy state, not a collection failure.

An amber informational banner appears at the top of the page, as on every plug-in page, when the collection throttle is active on the agent host. See Collection throttle.

The Recommendation cell opened into the copy dialog with the ALTER TABLE statement

Clicking a Recommendation cell opens the statement in a copy dialog. Nothing is applied.

  1. Find the table in Tables · Vacuum Recommendations, sorted worst first.
  2. Read the matching row in Per-Table Vacuum Recommendations (Full Detail) for the evidence: dead tuples against the trigger point, when autovacuum last ran, and any reloptions in effect.
  3. Click the Recommendation cell. The tuning SQL opens in a copy dialog.
  4. Copy it, review it, and run it in your own tooling.
  5. The finding clears on its own at the next collection once dead tuples fall back under the trigger point. There is nothing to close manually.

The same dialog is used in both recommendation tables, so it does not matter which one you click from.

This is the detection behind the Vacuum Advisor (Frequency) metric. For every user table it recomputes the exact dead-tuple count at which autovacuum should fire:

autovacuum_vacuum_threshold + autovacuum_vacuum_scale_factor × reltuples

using the table’s effective settings, so a per-table reloptions override beats the cluster-wide GUC. That distinction is the usual answer to “why isn’t autovacuum touching this table?”, and the Reloptions and Eff. Scale Factor columns exist to make it visible.

SeverityCondition
HIGHDead tuples past the trigger point and the last autovacuum is missing or more than a day old. Cleanup is demonstrably not keeping up.
MEDIUMDead tuples past the trigger point.
LOWEvery other listed table, including those above half their trigger point.

Each row also carries a plain-language evidence sentence, for example: “Table has 412908 dead tuples, past its autovacuum trigger point of 105220 (last autovacuum: 2026-08-19 03:14); autovacuum frequency appears insufficient for this table’s churn”. When autovacuum has been switched off on the table, the sentence ends with ” - NOTE: autovacuum is DISABLED on this table via reloptions”.

A table past its trigger point gets a statement of this shape:

ALTER TABLE public.orders SET (autovacuum_vacuum_scale_factor=0.02);

The proposed factor is the lower of two candidates: a value that would have fired at half the dead tuples the table has now, and half the table’s current effective factor. It therefore always tightens the trigger and never loosens it, with a floor of 0.01. When autovacuum has been disabled on the table through reloptions (the most common reason a past-trigger table was never vacuumed), the same statement also sets autovacuum_enabled=true:

ALTER TABLE public.orders SET (autovacuum_enabled=true, autovacuum_vacuum_scale_factor=0.02);

Tables that are not past their trigger get a comment instead of a statement: -- autovacuum is keeping up (no tuning needed), or -- per-table storage parameters in effect: <reloptions> when the table carries overrides worth seeing.

MetricInternal nameCollectedDefault WarningDefault CriticalOccurrencesClears when
Vacuum Advisor (Frequency)vacuum_advisorEvery 30 minutesSeverity = HIGHNot set1Dead tuples fall back under the trigger point at the next collection

Wraparound carries its own Critical-bearing metrics, which is why this one ships with Warning only. The alert message names the table, its dead-tuple count, its trigger point and the recommendation SQL, so the ticket already contains the fix. Retune the threshold per target from the metric’s own threshold settings, or fleet-wide with the shipped monitoring templates. The collection schedule is editable per target in the same place.

Finding detail persists to the agent-local store on every collection, under the Vacuum Advisor row on the Retention Policies page. Alert state lives in the Enterprise Manager repository as usual.

The frequency detection and the bloat estimate are cause and consequence. The frequency detection tells you cleanup is falling behind, which is something to act on early. The bloat estimate tells you a table has already grown avoidably, and that is a reclaim to plan.

This section and the Table Bloat Estimate metric need the pgstattuple extension on the monitored database. Extensions in PostgreSQL are per-database, so install it in each database you want estimated. The plug-in detects it automatically through the catalog and never installs it. Without it the metric emits zero rows and the page section is simply empty. That is a healthy, expected state, not an error.

The estimate uses pgstattuple’s fast approximate function, which samples only the pages PostgreSQL has not already marked as fully visible, so it never reads a whole table. It covers ordinary permanent tables in user schemas of at least 256 KB. A table outside those bounds is skipped and logged. A table the role cannot read leaves that whole database’s estimate empty for that collection, a known limitation in this release. That is unusual with the standard role, because pg_monitor already includes the pg_stat_scan_tables membership the function needs, so if a database’s estimate stays empty with pgstattuple installed, check that the monitoring role still holds pg_monitor.

SeverityCondition
HIGHFree space ≥ 40%, or dead tuples ≥ 20%.
MEDIUMFree space ≥ 20%, or dead tuples ≥ 10%.
LOWEverything else in the set.

Each row carries its own evidence sentence, for example: “Table is ~46.2% free space (912 MB) with 2204118 dead tuples (~21.7%); a VACUUM (FULL) or pg_repack would reclaim the avoidable growth”.

Work a flagged table in that order: check the vacuum recommendation tables first so the bloat stops growing, then reclaim the space in your own tooling with a VACUUM, or with a rewrite or repack for severe cases.

MetricInternal nameCollectedDefault WarningDefault CriticalOccurrencesClears when
Table Bloat Estimatetable_bloatEvery 30 minutesSeverity = HIGHNot set1The estimate drops back under the line at the next collection

Detail history persists to the agent-local store under the Table Bloat row on the Retention Policies page.

Autovacuum cannot remove a dead row that might still be visible to some open snapshot. The oldest such snapshot is the cluster’s xmin horizon, and while something holds it back, no amount of vacuum tuning helps: cleanup runs and reclaims nothing. This is the case where Tables past autovacuum trigger climbs while autovacuum is plainly running.

The plug-in identifies exactly what is holding the horizon and gives you the release command for it. It never runs that command. Three kinds of holder are detected; replication slots are reported differently depending on whether the slot is active, so the table has four rows:

Holder TypeRead fromHolder looks likeSuggested Action
backendpg_stat_activity.backend_xminbackend pid 41207Names the user and application, and the timestamp it has been holding xmin since, then SELECT pg_terminate_backend(41207);
replication_slot (active)pg_replication_slotsslot standby_2Names the slot and its active PID and tells you to check the standby or subscriber, then SELECT pg_drop_replication_slot('standby_2'); if it is obsolete
replication_slot (inactive)pg_replication_slotsslot standby_2Says the inactive slot is pinning the horizon, then SELECT pg_drop_replication_slot('standby_2'); if it is obsolete
prepared_transactionpg_prepared_xactsprepared txn_9f31Names the owner, then ROLLBACK PREPARED 'txn_9f31'; if the transaction is abandoned

The collector excludes its own session, so it never reports itself as the blocker.

The Realtime Vacuum xmin Horizon page listing holders with Is Horizon, xmin Age and Suggested Action

Realtime ▸ Vacuum xmin Horizon. The row with Is Horizon = 1 is the blocker; the rest are queued behind it.

  1. Open Realtime ▸ Vacuum xmin Horizon and set Auto Refresh to No Refresh, 15, 30 or 60 seconds. The page re-queries current state on each tick.
  2. Find the row where Is Horizon is 1. Only that row is the problem. Many rows on a busy system is normal.
  3. Copy its Suggested Action, review it, and run it in your own session if it is safe to do so. The page has no action buttons by design.
  4. Watch the row disappear on the next refresh, then look at what becomes the new Is Horizon = 1. Blockers are often layered.

The xmin Horizon holder table with the blocking session first and its release command in Suggested Action

The holder list, oldest first: Is Horizon = 1 marks the blocker, and Suggested Action carries its release command. After a release, the next holder moves to the top.

xmin Age growing between refreshes means the situation is getting worse in real time. A holder sitting at xmin age 0 blocks nothing, is not counted as blocked by the KPI or the root-cause card, and is typically a transient in-flight collection. The detail table still lists it.

MetricInternal nameCollectedDefault WarningDefault CriticalOccurrencesClears when
Vacuum xmin Horizon (Root Cause)xmin_horizonEvery 30 minutesWraparound Severity ≥ 1Wraparound Severity ≥ 22The holder is gone at the next collection

Wraparound Severity is a band off the holder’s XID age: 1 at 1 billion XIDs, 2 at 1.5 billion. The two-occurrence rule is deliberate debouncing. A legitimate long batch job that pins the horizon for a single collection does not page anyone, while a holder that is still there on the next collection alerts within the hour. The alert message names the holder and its release command.

This metric is current-state only. The history of XID age lives on the databases and tables data described next.

Transaction-ID age and multixact age are tracked continuously against the wraparound limit, at both database and table grain. Multixact wraparound is tracked and alerted separately from XID wraparound at both levels, because a database can be perfectly healthy on one and in trouble on the other.

Normally you do nothing here: these thresholds ship enabled. The XID Consumption panel gives you the live cluster position, and when an age alert arrives, the rest of the Vacuum Advisor page tells you which tables are holding cleanup back and whether the horizon is pinned.

MetricInternal nameCollectedDefault WarningDefault CriticalOccurrencesClears when
Databases · Transaction Unfrozen Agedatabases / transaction_ageEvery 30 minutes1,000,000,0001,500,000,0002Age returns under the warning line
Databases · Multixact Unfrozen Agedatabases / multixact_ageEvery 30 minutes1,000,000,0001,500,000,0002Age returns under the warning line
Tables · Table Unfrozen XID Agetables / table_xid_ageEvery 30 minutes1,000,000,0001,500,000,0002Age returns under the warning line
Tables · Table Unfrozen Multixact Agetables / table_multixact_ageEvery 30 minutes1,000,000,0001,500,000,0002Age returns under the warning line

The table-level rows are the safety net. Their alert messages say directly that the table is holding back the database’s datfrozenxid or datminmxid and to check autovacuum, so a table past its trigger with stale cleanup raises an alert even while autovacuum is nominally running.

Tuning is not the response to one of these alerts. The ALTER TABLE … autovacuum_vacuum_scale_factor recommendation tightens when autovacuum fires next time; it does nothing to the age already accumulated, because only a freeze advances a table’s relfrozenxid.

Check the xmin horizon first. If a session, replication slot or prepared transaction is pinning the horizon, a freeze cannot advance past it and reclaims nothing, so release the holder before you go any further. Once the horizon is clear, run a freeze against the named table in your own tooling:

VACUUM (FREEZE) public.orders;

As everywhere else on this page, the plug-in never runs the freeze for you.

Age history accumulates in the agent-local store from day one with no configuration. Every collection stages full-resolution rows, and the daily condense keeps each day’s maximum XID and multixact ages, so trend depth builds on its own without touching the Retention Policies page.

All four thresholds are retunable per target or through the shipped monitoring templates, and alerts route through the standard Enterprise Manager notification framework to whatever connector you have bound.

A live view of what pg_stat_progress_vacuum currently reports, joined to session state. It needs no extension and no setup.

The Realtime Vacuums in Progress page showing a running vacuum advancing through its phases

Realtime ▸ Vacuums in Progress, with the Auto Refresh control above the table.

Open Realtime ▸ Vacuums in Progress, set Auto Refresh to No Refresh, 15, 30 or 60 seconds, and watch running vacuums advance phase by phase. It is the page to use while confirming a manual VACUUM you kicked off, or checking whether autovacuum is actively working a table the advisor flagged.

ColumnWhat it shows
PIDThe process running the vacuum.
Database NameThe database the vacuum is running in.
PhaseThe current vacuum phase as PostgreSQL reports it.
Heap Blocks TotalBlocks in the heap being vacuumed.
Heap Blocks ScannedBlocks scanned so far.
Heap Blocks VacuumedBlocks vacuumed so far.
Percent VacuumedCompletion against the total.
StateThe backend’s session state.
Backend TypeDistinguishes an autovacuum worker from a client backend running a manual VACUUM.
Query Start TimeWhen the vacuum began.

An empty table means no vacuum is running right now.