From 76b329d7353e814765ab7183779e64d75cba5b7c Mon Sep 17 00:00:00 2001
From: Bharath Rupireddy <bharath.rupireddyforpostgres@gmail.com>
Date: Tue, 4 Aug 2026 20:24:51 +0000
Subject: [PATCH v3] Report indexes being vacuumed in pg_stat_progress_vacuum

Previously, during the "vacuuming indexes" and "cleaning up
indexes" phases, pg_stat_progress_vacuum reported how many
indexes had been processed but not which index was being
processed. On a table with many indexes, especially indexes of
different types, this made it hard to tell which index a slow or
stuck vacuum was working on.

This commit adds two columns to pg_stat_progress_vacuum,
index_vacuum_pids and index_vacuum_oids. Each process, the leader
and any parallel workers, reports the OID of the index it is
currently processing into its own progress array slot. The view
gathers these onto the leader's row as two arrays that are
aligned by position, so that unnest(index_vacuum_pids,
index_vacuum_oids) gives the (pid, index) pairs. For a serial
vacuum each array holds a single value, and both are null when no
index is being processed.

Because pg_stat_progress_vacuum reports one row per command, the
per-worker information is aggregated onto the leader's row rather
than shown as separate rows. This needs no new shared memory,
since the per-process progress array slots already exist.

Author: Bharath Rupireddy <bharath.rupireddyforpostgres@gmail.com>
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          | 23 +++++++++++++++++++++++
 src/backend/access/heap/vacuumlazy.c  | 16 ++++++++++++++++
 src/backend/catalog/system_views.sql  | 21 +++++++++++++++++++--
 src/backend/commands/vacuumparallel.c | 22 ++++++++++++++++++++++
 src/include/commands/progress.h       |  1 +
 src/test/regress/expected/rules.out   | 15 ++++++++++++---
 6 files changed, 93 insertions(+), 5 deletions(-)

diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 099e9b6f4e9..0913a16ea9a 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -7892,6 +7892,29 @@ FROM pg_stat_get_backend_idset() AS backendid;
        </itemizedlist>
       </para></entry>
      </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>index_vacuum_pids</structfield> <type>integer[]</type>
+      </para>
+      <para>
+       The process IDs currently vacuuming or cleaning up an index (the
+       leader process and any parallel workers). Null when no index is
+       being processed.
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>index_vacuum_oids</structfield> <type>oid[]</type>
+      </para>
+      <para>
+       The OIDs of the indexes being vacuumed or cleaned up, aligned by
+       position with <structfield>index_vacuum_pids</structfield>: the index
+       at each position is being processed by the process ID at the same
+       position. Null when no index is being processed.
+      </para></entry>
+     </row>
     </tbody>
    </tgroup>
   </table>
diff --git a/src/backend/access/heap/vacuumlazy.c b/src/backend/access/heap/vacuumlazy.c
index 39395aed0d5..3831215ba6a 100644
--- a/src/backend/access/heap/vacuumlazy.c
+++ b/src/backend/access/heap/vacuumlazy.c
@@ -3025,6 +3025,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,
+								 RelationGetRelid(indrel));
+
 	/*
 	 * Update error traceback information.
 	 *
@@ -3046,6 +3050,10 @@ lazy_vacuum_one_index(Relation indrel, IndexBulkDeleteResult *istat,
 	pfree(vacrel->indname);
 	vacrel->indname = NULL;
 
+	/* Reset the current index relid */
+	pgstat_progress_update_param(PROGRESS_VACUUM_CURRENT_INDEX_RELID,
+								 InvalidOid);
+
 	return istat;
 }
 
@@ -3076,6 +3084,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,
+								 RelationGetRelid(indrel));
+
 	/*
 	 * Update error traceback information.
 	 *
@@ -3095,6 +3107,10 @@ lazy_cleanup_one_index(Relation indrel, IndexBulkDeleteResult *istat,
 	pfree(vacrel->indname);
 	vacrel->indname = NULL;
 
+	/* Reset the current index relid */
+	pgstat_progress_update_param(PROGRESS_VACUUM_CURRENT_INDEX_RELID,
+								 InvalidOid);
+
 	return istat;
 }
 
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index 090281a03dd..8f93ec58e47 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -1353,9 +1353,26 @@ 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,
+        I.index_vacuum_pids AS index_vacuum_pids,
+        I.index_vacuum_oids AS index_vacuum_oids
     FROM pg_stat_get_progress_info('VACUUM') AS S
-        LEFT JOIN pg_database D ON S.datid = D.oid;
+        LEFT JOIN pg_database D ON S.datid = D.oid
+        LEFT JOIN pg_stat_activity A ON S.pid = A.pid,
+        LATERAL (
+            -- Aggregate the indexes being processed by this vacuum's leader
+            -- and its parallel workers (if any) into the single leader row.
+            -- Both arrays use the same ORDER BY so that they stay aligned by
+            -- position; the leader is listed first when it is itself
+            -- processing an index.
+            SELECT array_agg(W.pid ORDER BY W.pid <> S.pid, W.pid) AS index_vacuum_pids,
+                   array_agg(CAST(W.param14 AS oid) ORDER BY W.pid <> S.pid, W.pid) AS index_vacuum_oids
+            FROM pg_stat_get_progress_info('VACUUM') AS W
+                LEFT JOIN pg_stat_activity WA ON W.pid = WA.pid
+            WHERE COALESCE(WA.leader_pid, W.pid) = S.pid
+              AND W.param14 <> 0
+        ) I
+    WHERE A.leader_pid IS NULL;
 
 CREATE VIEW pg_stat_progress_repack AS
     SELECT
diff --git a/src/backend/commands/vacuumparallel.c b/src/backend/commands/vacuumparallel.c
index 767d162e578..01b11bf5803 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,13 @@ parallel_vacuum_process_one_index(ParallelVacuumState *pvs, Relation indrel,
 	pvs->indname = pstrdup(RelationGetRelationName(indrel));
 	pvs->status = indstats->status;
 
+	/* Report which index we're currently processing and the current phase */
+	progress_val[0] = (indstats->status == PARALLEL_INDVAC_STATUS_NEED_BULKDELETE)
+		? PROGRESS_VACUUM_PHASE_VACUUM_INDEX
+		: PROGRESS_VACUUM_PHASE_INDEX_CLEANUP;
+	progress_val[1] = RelationGetRelid(indrel);
+	pgstat_progress_update_multi_param(2, progress_index, progress_val);
+
 	switch (indstats->status)
 	{
 		case PARALLEL_INDVAC_STATUS_NEED_BULKDELETE:
@@ -1144,6 +1156,10 @@ parallel_vacuum_process_one_index(ParallelVacuumState *pvs, Relation indrel,
 	pfree(pvs->indname);
 	pvs->indname = NULL;
 
+	/* Reset the current index relid */
+	pgstat_progress_update_param(PROGRESS_VACUUM_CURRENT_INDEX_RELID,
+								 InvalidOid);
+
 	/*
 	 * Call the parallel variant of pgstat_progress_incr_param so workers can
 	 * report progress of index vacuum to the leader.
@@ -1315,6 +1331,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 +1353,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 6a3341356da..78a5dff0b82 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -2213,9 +2213,18 @@ 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
-   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)));
+        END AS started_by,
+    i.index_vacuum_pids,
+    i.index_vacuum_oids
+   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)))
+     LEFT JOIN pg_stat_activity a ON ((s.pid = a.pid))),
+    LATERAL ( SELECT array_agg(w.pid ORDER BY (w.pid <> s.pid), w.pid) AS index_vacuum_pids,
+            array_agg((w.param14)::oid ORDER BY (w.pid <> s.pid), w.pid) AS index_vacuum_oids
+           FROM (pg_stat_get_progress_info('VACUUM'::text) w(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_stat_activity wa ON ((w.pid = wa.pid)))
+          WHERE ((COALESCE(wa.leader_pid, w.pid) = s.pid) AND (w.param14 <> 0))) i
+  WHERE (a.leader_pid IS NULL);
 pg_stat_recovery| SELECT promote_triggered,
     last_replayed_read_lsn,
     last_replayed_end_lsn,
-- 
2.47.3

