From 40dd523966ec6032acd73382063875fc3b31a46f Mon Sep 17 00:00:00 2001
From: Bharath Rupireddy <bharath.rupireddyforpostgres@gmail.com>
Date: Wed, 2 Sep 2026 23:24:10 +0000
Subject: [PATCH v5 1/2] Report the index being vacuumed in
 pg_stat_progress_vacuum.

Previously, pg_stat_progress_vacuum showed the total and processed
index counts, but not which index a backend was actually working on.
On a table with many indexes of different access methods, this made
it hard to tell which index a slow or stuck vacuum was spending its
time on.

This commit adds a current_index_relid column reporting the OID of
the index a backend is currently vacuuming or cleaning up. It is set
before each index is processed and reset once that index is done, so
it does not report a stale value after the phase moves on.

During parallel index vacuum the leader also participates, and each
participant processes a different set of indexes. That per-index
progress cannot be collapsed into a single leader row without losing
the detail that matters, so each participant, the leader and the
workers alike, now reports its own row and shows the index it is
processing. A worker row carries the columns that come from its own
backend state, pid, datid, datname, relid, phase and
current_index_relid; the remaining columns track command-level heap
progress that only the leader maintains and read as zero on worker
rows. Because a table can be vacuumed by only one VACUUM at a time,
the rows sharing a datid and relid make up a single vacuum, so they
can be grouped that way to see the leader together with all of its
workers. If the exact leader-to-worker mapping is wanted, it can be
recovered from leader_pid in pg_stat_activity.

Author: Bharath Rupireddy <bharath.rupireddyforpostgres@gmail.com>
Reviewed-by: Michael Paquier <michael@paquier.xyz>
Reviewed-by: Sami Imseih <samimseih@gmail.com>
Reviewed-by: Satyanarayana Narlapuram <satyanarlapuram@gmail.com>
Discussion: https://postgr.es/m/CALj2ACUgwSchK6jQ2CdKLBWUADTOE_zKdTff2Zg3E6hOuXKv-w@mail.gmail.com
---
 doc/src/sgml/monitoring.sgml          | 47 +++++++++++++++++++++++++--
 src/backend/access/heap/vacuumlazy.c  | 16 +++++++++
 src/backend/catalog/system_views.sql  |  3 +-
 src/backend/commands/vacuumparallel.c | 25 ++++++++++++++
 src/include/commands/progress.h       |  1 +
 src/test/regress/expected/rules.out   |  3 +-
 6 files changed, 91 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index b403fb990a7..e39f9637775 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -7651,8 +7651,11 @@ FROM pg_stat_get_backend_idset() AS backendid;
   <para>
    Whenever <command>VACUUM</command> is running, the
    <structname>pg_stat_progress_vacuum</structname> view will contain
-   one row for each backend (including autovacuum worker processes) that is
-   currently vacuuming.  The tables below describe the information
+   one row for each backend (including autovacuum worker processes and
+   parallel workers launched for
+   <link linkend="sql-vacuum">parallel vacuum</link>) that is currently
+   vacuuming; see the note following the view for the columns reported on
+   parallel worker rows. The tables below describe the information
    that will be reported and provide information about how to interpret it.
    Progress for <command>VACUUM FULL</command> commands is reported via
    <structname>pg_stat_progress_repack</structname>, and is also visible via
@@ -7910,10 +7913,50 @@ FROM pg_stat_get_backend_idset() AS backendid;
        </itemizedlist>
       </para></entry>
      </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>current_index_relid</structfield> <type>oid</type>
+      </para>
+      <para>
+       If <command>VACUUM</command> is currently processing an index, this
+       column shows the OID of the index being vacuumed. The value is set
+       when the phase is <literal>vacuuming indexes</literal> or
+       <literal>cleaning up indexes</literal> and is reset to 0 when all
+       indexes have been processed. During parallel index vacuum, each
+       parallel worker row shows the index that particular worker is
+       processing.
+      </para></entry>
+     </row>
+
     </tbody>
    </tgroup>
   </table>
 
+  <note>
+   <para>
+    During a parallel vacuum, each participating backend (the leader and each
+    parallel worker) reports its own row. On a worker row the meaningful
+    columns are <structfield>pid</structfield>, <structfield>datid</structfield>,
+    <structfield>datname</structfield>, <structfield>relid</structfield>,
+    <structfield>phase</structfield> and
+    <structfield>current_index_relid</structfield>. The remaining columns track
+    command-level heap progress that only the leader maintains; they read as
+    zero on worker rows. Because a table can be vacuumed by only one
+    <command>VACUUM</command> at a time, the rows sharing a given
+    <structfield>datid</structfield> and <structfield>relid</structfield>
+    together make up a single vacuum, so they can be grouped that way to see
+    the leader and all of its workers. The leader and worker process IDs can be
+    correlated with <structfield>leader_pid</structfield> in
+    <link linkend="monitoring-pg-stat-activity-view"><structname>pg_stat_activity</structname></link>
+    if needed. Because the worker rows are separate backends, a role that does
+    not have privileges of the <literal>pg_read_all_stats</literal> role and
+    does not own those backends sees only their <structfield>pid</structfield>,
+    with the remaining columns <literal>NULL</literal>, just as in
+    <structname>pg_stat_activity</structname>.
+   </para>
+  </note>
+
   <table id="vacuum-phases">
    <title>VACUUM Phases</title>
    <tgroup cols="2">
diff --git a/src/backend/access/heap/vacuumlazy.c b/src/backend/access/heap/vacuumlazy.c
index 063ef2208de..8bc3de84581 100644
--- a/src/backend/access/heap/vacuumlazy.c
+++ b/src/backend/access/heap/vacuumlazy.c
@@ -3048,6 +3048,10 @@ lazy_vacuum_one_index(Relation indrel, IndexBulkDeleteResult *istat,
 	ivinfo.num_heap_tuples = reltuples;
 	ivinfo.strategy = vacrel->bstrategy;
 
+	/* Report which index we're currently processing */
+	pgstat_progress_update_param(PROGRESS_VACUUM_CURRENT_INDEX_RELID,
+								 (int64) RelationGetRelid(indrel));
+
 	/*
 	 * Update error traceback information.
 	 *
@@ -3069,6 +3073,10 @@ lazy_vacuum_one_index(Relation indrel, IndexBulkDeleteResult *istat,
 	pfree(vacrel->indname);
 	vacrel->indname = NULL;
 
+	/* Reset the current index relid to avoid reporting a stale value */
+	pgstat_progress_update_param(PROGRESS_VACUUM_CURRENT_INDEX_RELID,
+								 (int64) InvalidOid);
+
 	return istat;
 }
 
@@ -3099,6 +3107,10 @@ lazy_cleanup_one_index(Relation indrel, IndexBulkDeleteResult *istat,
 	ivinfo.num_heap_tuples = reltuples;
 	ivinfo.strategy = vacrel->bstrategy;
 
+	/* Report which index we're currently processing */
+	pgstat_progress_update_param(PROGRESS_VACUUM_CURRENT_INDEX_RELID,
+								 (int64) RelationGetRelid(indrel));
+
 	/*
 	 * Update error traceback information.
 	 *
@@ -3118,6 +3130,10 @@ lazy_cleanup_one_index(Relation indrel, IndexBulkDeleteResult *istat,
 	pfree(vacrel->indname);
 	vacrel->indname = NULL;
 
+	/* Reset the current index relid to avoid reporting a stale value */
+	pgstat_progress_update_param(PROGRESS_VACUUM_CURRENT_INDEX_RELID,
+								 (int64) InvalidOid);
+
 	return istat;
 }
 
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index 8612d99a890..24d2ae8bb44 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -1353,7 +1353,8 @@ CREATE VIEW pg_stat_progress_vacuum AS
         CASE S.param13 WHEN 1 THEN 'manual'
                        WHEN 2 THEN 'autovacuum'
                        WHEN 3 THEN 'autovacuum_wraparound'
-                       ELSE NULL END AS started_by
+                       ELSE NULL END AS started_by,
+        CAST(S.param14 AS oid) AS current_index_relid
     FROM pg_stat_get_progress_info('VACUUM') AS S
         LEFT JOIN pg_database D ON S.datid = D.oid;
 
diff --git a/src/backend/commands/vacuumparallel.c b/src/backend/commands/vacuumparallel.c
index 767d162e578..d6a56f63a0f 100644
--- a/src/backend/commands/vacuumparallel.c
+++ b/src/backend/commands/vacuumparallel.c
@@ -1076,6 +1076,11 @@ parallel_vacuum_process_one_index(ParallelVacuumState *pvs, Relation indrel,
 	IndexBulkDeleteResult *istat = NULL;
 	IndexBulkDeleteResult *istat_res;
 	IndexVacuumInfo ivinfo;
+	const int	progress_index[] = {
+		PROGRESS_VACUUM_PHASE,
+		PROGRESS_VACUUM_CURRENT_INDEX_RELID
+	};
+	int64		progress_val[2];
 
 	/*
 	 * Update the pointer to the corresponding bulk-deletion result if someone
@@ -1097,6 +1102,16 @@ parallel_vacuum_process_one_index(ParallelVacuumState *pvs, Relation indrel,
 	pvs->indname = pstrdup(RelationGetRelationName(indrel));
 	pvs->status = indstats->status;
 
+	/*
+	 * Report the phase and the index we're about to process before we start,
+	 * so that it is visible for the whole duration of the index scan.
+	 */
+	progress_val[0] = (indstats->status == PARALLEL_INDVAC_STATUS_NEED_BULKDELETE)
+		? PROGRESS_VACUUM_PHASE_VACUUM_INDEX
+		: PROGRESS_VACUUM_PHASE_INDEX_CLEANUP;
+	progress_val[1] = (int64) RelationGetRelid(indrel);
+	pgstat_progress_update_multi_param(2, progress_index, progress_val);
+
 	switch (indstats->status)
 	{
 		case PARALLEL_INDVAC_STATUS_NEED_BULKDELETE:
@@ -1112,6 +1127,10 @@ parallel_vacuum_process_one_index(ParallelVacuumState *pvs, Relation indrel,
 				 RelationGetRelationName(indrel));
 	}
 
+	/* Reset the current index relid to avoid reporting a stale value */
+	pgstat_progress_update_param(PROGRESS_VACUUM_CURRENT_INDEX_RELID,
+								 (int64) InvalidOid);
+
 	/*
 	 * Copy the index bulk-deletion result returned from ambulkdelete and
 	 * amvacuumcleanup to the DSM segment if it's the first cycle because they
@@ -1315,6 +1334,9 @@ parallel_vacuum_main(dsm_segment *seg, shm_toc *toc)
 	/* Prepare to track buffer usage during parallel execution */
 	InstrStartParallelQuery();
 
+	/* Register this worker for vacuum progress reporting */
+	pgstat_progress_start_command(PROGRESS_COMMAND_VACUUM, shared->relid);
+
 	/* Process indexes to perform vacuum/cleanup */
 	parallel_vacuum_process_safe_indexes(&pvs);
 
@@ -1334,6 +1356,9 @@ parallel_vacuum_main(dsm_segment *seg, shm_toc *toc)
 	/* Pop the error context stack */
 	error_context_stack = errcallback.previous;
 
+	/* Unregister this worker from vacuum progress reporting */
+	pgstat_progress_end_command();
+
 	vac_close_indexes(nindexes, indrels, RowExclusiveLock);
 	table_close(rel, ShareUpdateExclusiveLock);
 	FreeAccessStrategy(pvs.bstrategy);
diff --git a/src/include/commands/progress.h b/src/include/commands/progress.h
index 2a12920c75f..bee311955b6 100644
--- a/src/include/commands/progress.h
+++ b/src/include/commands/progress.h
@@ -31,6 +31,7 @@
 #define PROGRESS_VACUUM_DELAY_TIME				10
 #define PROGRESS_VACUUM_MODE					11
 #define PROGRESS_VACUUM_STARTED_BY				12
+#define PROGRESS_VACUUM_CURRENT_INDEX_RELID		13
 
 /* Phases of vacuum (as advertised via PROGRESS_VACUUM_PHASE) */
 #define PROGRESS_VACUUM_PHASE_SCAN_HEAP			1
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 1a29d46213e..cf9b8c823c8 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -2213,7 +2213,8 @@ pg_stat_progress_vacuum| SELECT s.pid,
             WHEN 2 THEN 'autovacuum'::text
             WHEN 3 THEN 'autovacuum_wraparound'::text
             ELSE NULL::text
-        END AS started_by
+        END AS started_by,
+    (s.param14)::oid AS current_index_relid
    FROM (pg_stat_get_progress_info('VACUUM'::text) s(pid, datid, relid, param1, param2, param3, param4, param5, param6, param7, param8, param9, param10, param11, param12, param13, param14, param15, param16, param17, param18, param19, param20)
      LEFT JOIN pg_database d ON ((s.datid = d.oid)));
 pg_stat_recovery| SELECT promote_triggered,
-- 
2.47.3

