From 33d4116a6b2ea4eef7d00ada279a329854514068 Mon Sep 17 00:00:00 2001
From: "Sami Imseih (AWS)"
 <simseih@dev-dsk-simseih-1e-fba9b514.us-east-1.amazon.com>
Date: Thu, 27 Jan 2022 01:41:20 +0000
Subject: [PATCH 1/1] Expose progress for the "vacuuming indexes/cleanup" phase
 of a VACUUM operation.

Author: Sami Imseih, based on suggestions by Nathan Bossart, Peter Geoghegan and Masahiko Sawada
Reviewed by: Nathan Bossart, Justin Pryzby
Discussion: https://www.postgresql.org/message-id/5478DFCD-2333-401A-B2F0-0D186AB09228%40amazon.com
---
 doc/src/sgml/monitoring.sgml          | 131 ++++++++++++++++++++++++++
 src/backend/access/gin/ginvacuum.c    |   4 +
 src/backend/access/gist/gistvacuum.c  |   3 +
 src/backend/access/hash/hash.c        |   1 +
 src/backend/access/heap/vacuumlazy.c  |  11 +++
 src/backend/access/nbtree/nbtree.c    |   1 +
 src/backend/access/spgist/spgvacuum.c |   4 +
 src/backend/catalog/system_views.sql  |  70 ++++++++++----
 src/backend/commands/vacuumparallel.c |  56 ++++++++---
 src/include/commands/progress.h       |   6 ++
 src/test/regress/expected/rules.out   |  63 ++++++++++---
 11 files changed, 309 insertions(+), 41 deletions(-)

diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 62f2a3332b..b2b33acb66 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -379,6 +379,15 @@ postgres   27093  0.0  0.0  30096  2752 ?        Ss   11:34   0:00 postgres: ser
       </entry>
      </row>
 
+     <row>
+      <entry><structname>pg_stat_progress_vacuum_index</structname><indexterm><primary>pg_stat_progress_vacuum_index</primary></indexterm></entry>
+      <entry>One row for each backend (including autovacuum worker processes) running
+       the <literal>vacuuming indexes</literal> or <literal>cleaning up indexes</literal> phase of a 
+       <command>VACUUM</command>, showing current progress.
+       See <xref linkend='vacuum-progress-reporting'/>.
+      </entry>
+     </row>
+
      <row>
       <entry><structname>pg_stat_progress_cluster</structname><indexterm><primary>pg_stat_progress_cluster</primary></indexterm></entry>
       <entry>One row for each backend running
@@ -6269,6 +6278,128 @@ SELECT pg_stat_get_backend_pid(s.backendid) AS pid,
        Number of dead tuples collected since the last index vacuum cycle.
       </para></entry>
      </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>index_total</structfield> <type>bigint</type>
+      </para>
+      <para>
+       The total number of indexes that will be processed by the
+       <literal>vacuuming indexes</literal> or <literal>cleaning up indexes</literal> phase
+       of vacuum.
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>index_complete_count</structfield> <type>bigint</type>
+      </para>
+      <para>
+       The total number of indexes processed by the
+       <literal>vacuuming indexes</literal> or <literal>cleaning up indexes</literal> of the vacuum.
+       For vacuums with mutiple index vacuum cycles, this value will be set to <literal>0</literal>
+       at the start of each cycle.
+      </para></entry>
+     </row>
+    </tbody>
+   </tgroup>
+  </table>
+
+  <indexterm>
+   <primary>pg_stat_progress_vacuum_index</primary>
+  </indexterm>
+
+  <para>
+   Whenever <command>VACUUM</command> is running, the
+   <structname>pg_stat_progress_vacuum_index</structname> view will contain
+   one row for each backend (including autovacuum worker processes) that is
+   currently performing the <literal>vacuuming indexes</literal> or 
+   <literal>cleaning up indexes</literal> phase of the vacuum.
+  </para>
+
+  <table id="pg-stat-progress-vacuum-index-view" xreflabel="pg_stat_progress_vacuum_index">
+   <title><structname>pg_stat_progress_vacuum_index</structname> View</title>
+   <tgroup cols="1">
+    <thead>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       Column Type
+      </para>
+      <para>
+       Description
+      </para></entry>
+     </row>
+    </thead>
+
+    <tbody>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>pid</structfield> <type>integer</type>
+      </para>
+      <para>
+       Process ID of backend.
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>datid</structfield> <type>oid</type>
+      </para>
+      <para>
+       OID of the database to which this backend is connected.
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>datname</structfield> <type>name</type>
+      </para>
+      <para>
+       Name of the database to which this backend is connected.
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>indexrelid</structfield> <type>oid</type>
+      </para>
+      <para>
+       OID of the index being processed in the ongoing phase of the vacuum.
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>phase</structfield> <type>text</type>
+      </para>
+      <para>
+       Current processing phase of a vacuum. Only the
+       <literal>vacuuming indexes</literal> or <literal>cleaning up indexes</literal>
+       phase will be listed in this view. See <xref linkend="vacuum-phases"/>.
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>leader_pid</structfield> <type>integer</type>
+      </para>
+      <para>
+       Process ID of the parallel vacuum leader, if this process is a
+       parallel vacuum worker.  <literal>NULL</literal> if this process is a
+       parallel vacuum leader or does not participate in parallel vacuum.
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>tuples_removed</structfield> <type>oid</type>
+      </para>
+      <para>
+       The number of index tuples removed by the <literal>vacuuming indexes</literal> phase.
+       This field is <literal>0</literal> during the <literal>cleaning up indexes</literal> 
+       phase.
+      </para></entry>
+     </row>
     </tbody>
    </tgroup>
   </table>
diff --git a/src/backend/access/gin/ginvacuum.c b/src/backend/access/gin/ginvacuum.c
index b4fa5f6bf8..73660719b5 100644
--- a/src/backend/access/gin/ginvacuum.c
+++ b/src/backend/access/gin/ginvacuum.c
@@ -17,8 +17,10 @@
 #include "access/gin_private.h"
 #include "access/ginxlog.h"
 #include "access/xloginsert.h"
+#include "commands/progress.h"
 #include "commands/vacuum.h"
 #include "miscadmin.h"
+#include "pgstat.h"
 #include "postmaster/autovacuum.h"
 #include "storage/indexfsm.h"
 #include "storage/lmgr.h"
@@ -60,6 +62,8 @@ ginVacuumItemPointers(GinVacuumState *gvs, ItemPointerData *items,
 		if (gvs->callback(items + i, gvs->callback_state))
 		{
 			gvs->result->tuples_removed += 1;
+			pgstat_progress_update_param(PROGRESS_VACUUM_TUPLES_REMOVED, gvs->result->tuples_removed);
+
 			if (!tmpitems)
 			{
 				/*
diff --git a/src/backend/access/gist/gistvacuum.c b/src/backend/access/gist/gistvacuum.c
index aac4afab8f..8a0f23388b 100644
--- a/src/backend/access/gist/gistvacuum.c
+++ b/src/backend/access/gist/gistvacuum.c
@@ -17,9 +17,11 @@
 #include "access/genam.h"
 #include "access/gist_private.h"
 #include "access/transam.h"
+#include "commands/progress.h"
 #include "commands/vacuum.h"
 #include "lib/integerset.h"
 #include "miscadmin.h"
+#include "pgstat.h"
 #include "storage/indexfsm.h"
 #include "storage/lmgr.h"
 #include "utils/memutils.h"
@@ -375,6 +377,7 @@ restart:
 			END_CRIT_SECTION();
 
 			vstate->stats->tuples_removed += ntodelete;
+			pgstat_progress_update_param(PROGRESS_VACUUM_TUPLES_REMOVED, vstate->stats->tuples_removed);
 			/* must recompute maxoff */
 			maxoff = PageGetMaxOffsetNumber(page);
 		}
diff --git a/src/backend/access/hash/hash.c b/src/backend/access/hash/hash.c
index d48c8a4549..84341aa7e7 100644
--- a/src/backend/access/hash/hash.c
+++ b/src/backend/access/hash/hash.c
@@ -631,6 +631,7 @@ loop_top:
 	stats->estimated_count = false;
 	stats->num_index_tuples = num_index_tuples;
 	stats->tuples_removed += tuples_removed;
+	pgstat_progress_update_param(PROGRESS_VACUUM_TUPLES_REMOVED, stats->tuples_removed);
 	/* hashvacuumcleanup will fill in num_pages */
 
 	return stats;
diff --git a/src/backend/access/heap/vacuumlazy.c b/src/backend/access/heap/vacuumlazy.c
index 1749cc2a47..432229adea 100644
--- a/src/backend/access/heap/vacuumlazy.c
+++ b/src/backend/access/heap/vacuumlazy.c
@@ -340,6 +340,7 @@ heap_vacuum_rel(Relation rel, VacuumParams *params,
 
 	pgstat_progress_start_command(PROGRESS_COMMAND_VACUUM,
 								  RelationGetRelid(rel));
+	pgstat_progress_update_param(PROGRESS_VACUUM_LEADER_PID, MyProcPid);
 
 	vacuum_set_xid_limits(rel,
 						  params->freeze_min_age,
@@ -455,6 +456,9 @@ heap_vacuum_rel(Relation rel, VacuumParams *params,
 	vacrel->FreezeLimit = FreezeLimit;
 	vacrel->MultiXactCutoff = MultiXactCutoff;
 
+	/* Advertise the total indexes to vacuum */
+	pgstat_progress_update_param(PROGRESS_VACUUM_TOTAL_INDEXES, vacrel->nindexes);
+
 	/*
 	 * Call lazy_scan_heap to perform all required heap pruning, index
 	 * vacuuming, and heap vacuuming (plus related processing)
@@ -2010,9 +2014,13 @@ lazy_vacuum_all_indexes(LVRelState *vacrel)
 			Relation	indrel = vacrel->indrels[idx];
 			IndexBulkDeleteResult *istat = vacrel->indstats[idx];
 
+			pgstat_progress_update_param(PROGRESS_VACUUM_INDEXRELID, RelationGetRelid(indrel));
 			vacrel->indstats[idx] =
 				lazy_vacuum_one_index(indrel, istat, vacrel->old_live_tuples,
 									  vacrel);
+			pgstat_progress_update_param(PROGRESS_VACUUM_NUM_INDEX_COMPLETED, idx + 1);
+			pgstat_progress_update_param(PROGRESS_VACUUM_INDEXRELID, 0);
+			pgstat_progress_update_param(PROGRESS_VACUUM_TUPLES_REMOVED, 0);
 
 			if (lazy_check_wraparound_failsafe(vacrel))
 			{
@@ -2419,9 +2427,12 @@ lazy_cleanup_all_indexes(LVRelState *vacrel)
 			Relation	indrel = vacrel->indrels[idx];
 			IndexBulkDeleteResult *istat = vacrel->indstats[idx];
 
+			pgstat_progress_update_param(PROGRESS_VACUUM_INDEXRELID, RelationGetRelid(indrel));
 			vacrel->indstats[idx] =
 				lazy_cleanup_one_index(indrel, istat, reltuples,
 									   estimated_count, vacrel);
+			pgstat_progress_update_param(PROGRESS_VACUUM_NUM_INDEX_COMPLETED, idx + 1);
+			pgstat_progress_update_param(PROGRESS_VACUUM_INDEXRELID, 0);
 		}
 	}
 	else
diff --git a/src/backend/access/nbtree/nbtree.c b/src/backend/access/nbtree/nbtree.c
index 13024af2fa..7455d73d13 100644
--- a/src/backend/access/nbtree/nbtree.c
+++ b/src/backend/access/nbtree/nbtree.c
@@ -1272,6 +1272,7 @@ backtrack:
 								nupdatable);
 
 			stats->tuples_removed += nhtidsdead;
+			pgstat_progress_update_param(PROGRESS_VACUUM_TUPLES_REMOVED, stats->tuples_removed);
 			/* must recompute maxoff */
 			maxoff = PageGetMaxOffsetNumber(page);
 
diff --git a/src/backend/access/spgist/spgvacuum.c b/src/backend/access/spgist/spgvacuum.c
index 0049630532..db73f8ef59 100644
--- a/src/backend/access/spgist/spgvacuum.c
+++ b/src/backend/access/spgist/spgvacuum.c
@@ -21,8 +21,10 @@
 #include "access/transam.h"
 #include "access/xloginsert.h"
 #include "catalog/storage_xlog.h"
+#include "commands/progress.h"
 #include "commands/vacuum.h"
 #include "miscadmin.h"
+#include "pgstat.h"
 #include "storage/bufmgr.h"
 #include "storage/indexfsm.h"
 #include "storage/lmgr.h"
@@ -160,6 +162,7 @@ vacuumLeafPage(spgBulkDeleteState *bds, Relation index, Buffer buffer,
 				bds->stats->tuples_removed += 1;
 				deletable[i] = true;
 				nDeletable++;
+				pgstat_progress_update_param(PROGRESS_VACUUM_TUPLES_REMOVED, bds->stats->tuples_removed);
 			}
 			else
 			{
@@ -430,6 +433,7 @@ vacuumLeafRoot(spgBulkDeleteState *bds, Relation index, Buffer buffer)
 				bds->stats->tuples_removed += 1;
 				toDelete[xlrec.nDelete] = i;
 				xlrec.nDelete++;
+				pgstat_progress_update_param(PROGRESS_VACUUM_TUPLES_REMOVED, bds->stats->tuples_removed);
 			}
 			else
 			{
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index 3cb69b1f87..159605cb3f 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -1113,22 +1113,60 @@ CREATE VIEW pg_stat_progress_analyze AS
         LEFT JOIN pg_database D ON S.datid = D.oid;
 
 CREATE VIEW pg_stat_progress_vacuum AS
-    SELECT
-        S.pid AS pid, S.datid AS datid, D.datname AS datname,
-        S.relid AS relid,
-        CASE S.param1 WHEN 0 THEN 'initializing'
-                      WHEN 1 THEN 'scanning heap'
-                      WHEN 2 THEN 'vacuuming indexes'
-                      WHEN 3 THEN 'vacuuming heap'
-                      WHEN 4 THEN 'cleaning up indexes'
-                      WHEN 5 THEN 'truncating heap'
-                      WHEN 6 THEN 'performing final cleanup'
-                      END AS phase,
-        S.param2 AS heap_blks_total, S.param3 AS heap_blks_scanned,
-        S.param4 AS heap_blks_vacuumed, S.param5 AS index_vacuum_count,
-        S.param6 AS max_dead_tuples, S.param7 AS num_dead_tuples
-    FROM pg_stat_get_progress_info('VACUUM') AS S
-        LEFT JOIN pg_database D ON S.datid = D.oid;
+        SELECT
+            S.pid,
+            S.datid,
+            S.datname,
+            S.relid,
+            S.phase,
+            S.heap_blks_total,
+            S.heap_blks_scanned,
+            S.heap_blks_vacuumed,
+            S.index_vacuum_count,
+            S.max_dead_tuples,
+            S.num_dead_tuples,
+            S.index_total,
+            S.index_complete_count
+        FROM (SELECT
+                  S.pid AS pid, S.datid AS datid, D.datname AS datname,
+                  S.relid AS relid, S.param12 as leader_pid,
+                  CASE S.param1 WHEN 0 THEN 'initializing'
+                                WHEN 1 THEN 'scanning heap'
+                                WHEN 2 THEN 'vacuuming indexes'
+                                WHEN 3 THEN 'vacuuming heap'
+                                WHEN 4 THEN 'cleaning up indexes'
+                                WHEN 5 THEN 'truncating heap'
+                                WHEN 6 THEN 'performing final cleanup'
+                                END AS phase,
+                  S.param2 AS heap_blks_total, S.param3 AS heap_blks_scanned,
+                  S.param4 AS heap_blks_vacuumed, S.param5 AS index_vacuum_count,
+                  S.param6 AS max_dead_tuples, S.param7 AS num_dead_tuples,
+                  S.param8 AS index_total,
+                  CASE WHEN S.param1 IN (2,4) AND S.param9 > 0
+                                              AND S.param10 > 0
+                                              AND S.param10 > (COUNT(*) OVER (PARTITION BY S.param12) - 1)
+                                              THEN S.param8 - (COUNT(*) OVER (PARTITION BY S.param12) - 1)
+                       ELSE SUM(S.param9) OVER(PARTITION BY S.param12)
+                       END AS index_complete_count
+              FROM pg_stat_get_progress_info('VACUUM') AS S
+                  LEFT JOIN pg_database D ON S.datid = D.oid) AS S
+        WHERE S.pid = S.leader_pid;
+
+CREATE VIEW pg_stat_progress_vacuum_index AS
+        SELECT
+            S.pid AS pid, S.datid AS datid,
+            D.datname AS datname,
+            S.param11 AS indexrelid,
+            CASE S.param1 WHEN 2 THEN 'vacuuming indexes'
+                          WHEN 4 THEN 'cleaning up indexes'
+                          END AS phase,
+            CASE WHEN S.param12 = s.pid THEN NULL
+                 ELSE S.param12
+                 END AS leader_pid,
+            S.param13 AS tuples_removed
+        FROM pg_stat_get_progress_info('VACUUM') AS S
+            LEFT JOIN pg_database D ON S.datid =D.oid
+        WHERE S.param1 IN (2, 4) AND S.param11 NOT IN (0);
 
 CREATE VIEW pg_stat_progress_cluster AS
     SELECT
diff --git a/src/backend/commands/vacuumparallel.c b/src/backend/commands/vacuumparallel.c
index 974a29e7a9..eb4becf900 100644
--- a/src/backend/commands/vacuumparallel.c
+++ b/src/backend/commands/vacuumparallel.c
@@ -29,6 +29,7 @@
 #include "access/amapi.h"
 #include "access/table.h"
 #include "catalog/index.h"
+#include "commands/progress.h"
 #include "commands/vacuum.h"
 #include "optimizer/paths.h"
 #include "pgstat.h"
@@ -101,6 +102,12 @@ typedef struct PVShared
 
 	/* Counter for vacuuming and cleanup */
 	pg_atomic_uint32 idx;
+
+	/* Leader PID */
+	int leader_pid;
+
+	/* Number of planned parallel workers */
+	int parallel_workers;
 } PVShared;
 
 /* Status used during parallel index vacuum or cleanup */
@@ -204,10 +211,10 @@ static int	parallel_vacuum_compute_workers(Relation *indrels, int nindexes, int
 											bool *will_parallel_vacuum);
 static void parallel_vacuum_process_all_indexes(ParallelVacuumState *pvs, int num_index_scans,
 												bool vacuum);
-static void parallel_vacuum_process_safe_indexes(ParallelVacuumState *pvs);
-static void parallel_vacuum_process_unsafe_indexes(ParallelVacuumState *pvs);
-static void parallel_vacuum_process_one_index(ParallelVacuumState *pvs, Relation indrel,
-											  PVIndStats *indstats);
+static void parallel_vacuum_process_safe_indexes(ParallelVacuumState *pvs, int *idx_vacuumed);
+static void parallel_vacuum_process_unsafe_indexes(ParallelVacuumState *pvs, int *idx_vacuumed);
+static void	parallel_vacuum_process_one_index(ParallelVacuumState *pvs, Relation indrel,
+											  PVIndStats *indstats, int *idx_vacuumed);
 static bool parallel_vacuum_index_is_parallel_safe(Relation indrel, int num_index_scans,
 												   bool vacuum);
 static void parallel_vacuum_error_callback(void *arg);
@@ -259,6 +266,8 @@ parallel_vacuum_init(Relation rel, Relation *indrels, int nindexes,
 		return NULL;
 	}
 
+	pgstat_progress_update_param(PROGRESS_VACUUM_WORKERS_PLANNED, parallel_workers);
+
 	pvs = (ParallelVacuumState *) palloc0(sizeof(ParallelVacuumState));
 	pvs->indrels = indrels;
 	pvs->nindexes = nindexes;
@@ -357,6 +366,8 @@ parallel_vacuum_init(Relation rel, Relation *indrels, int nindexes,
 		(nindexes_mwm > 0) ?
 		maintenance_work_mem / Min(parallel_workers, nindexes_mwm) :
 		maintenance_work_mem;
+	shared->parallel_workers = parallel_workers;
+	shared->leader_pid = MyProcPid;
 
 	pg_atomic_init_u32(&(shared->cost_balance), 0);
 	pg_atomic_init_u32(&(shared->active_nworkers), 0);
@@ -565,6 +576,7 @@ parallel_vacuum_process_all_indexes(ParallelVacuumState *pvs, int num_index_scan
 {
 	int			nworkers;
 	PVIndVacStatus new_status;
+	int idx_vacuumed = 0;
 
 	Assert(!IsParallelWorker());
 
@@ -671,13 +683,13 @@ parallel_vacuum_process_all_indexes(ParallelVacuumState *pvs, int num_index_scan
 	}
 
 	/* Vacuum the indexes that can be processed by only leader process */
-	parallel_vacuum_process_unsafe_indexes(pvs);
+	parallel_vacuum_process_unsafe_indexes(pvs, &idx_vacuumed);
 
 	/*
 	 * Join as a parallel worker.  The leader vacuums alone processes all
 	 * parallel-safe indexes in the case where no workers are launched.
 	 */
-	parallel_vacuum_process_safe_indexes(pvs);
+	parallel_vacuum_process_safe_indexes(pvs, &idx_vacuumed);
 
 	/*
 	 * Next, accumulate buffer and WAL usage.  (This must wait for the workers
@@ -723,7 +735,7 @@ parallel_vacuum_process_all_indexes(ParallelVacuumState *pvs, int num_index_scan
  * vacuum worker processes to vacuum the indexes in parallel.
  */
 static void
-parallel_vacuum_process_safe_indexes(ParallelVacuumState *pvs)
+parallel_vacuum_process_safe_indexes(ParallelVacuumState *pvs, int *idx_vacuumed)
 {
 	/*
 	 * Increment the active worker count if we are able to launch any worker.
@@ -755,7 +767,8 @@ parallel_vacuum_process_safe_indexes(ParallelVacuumState *pvs)
 			continue;
 
 		/* Do vacuum or cleanup of the index */
-		parallel_vacuum_process_one_index(pvs, pvs->indrels[idx], indstats);
+		parallel_vacuum_process_one_index(pvs, pvs->indrels[idx], indstats, idx_vacuumed);
+
 	}
 
 	/*
@@ -777,7 +790,7 @@ parallel_vacuum_process_safe_indexes(ParallelVacuumState *pvs)
  * enforced by parallel_vacuum_compute_workers().
  */
 static void
-parallel_vacuum_process_unsafe_indexes(ParallelVacuumState *pvs)
+parallel_vacuum_process_unsafe_indexes(ParallelVacuumState *pvs, int *idx_vacuumed)
 {
 	Assert(!IsParallelWorker());
 
@@ -796,7 +809,8 @@ parallel_vacuum_process_unsafe_indexes(ParallelVacuumState *pvs)
 			continue;
 
 		/* Do vacuum or cleanup of the index */
-		parallel_vacuum_process_one_index(pvs, pvs->indrels[i], indstats);
+		parallel_vacuum_process_one_index(pvs, pvs->indrels[i], indstats, idx_vacuumed);
+
 	}
 
 	/*
@@ -805,6 +819,7 @@ parallel_vacuum_process_unsafe_indexes(ParallelVacuumState *pvs)
 	 */
 	if (VacuumActiveNWorkers)
 		pg_atomic_sub_fetch_u32(VacuumActiveNWorkers, 1);
+
 }
 
 /*
@@ -815,7 +830,7 @@ parallel_vacuum_process_unsafe_indexes(ParallelVacuumState *pvs)
  */
 static void
 parallel_vacuum_process_one_index(ParallelVacuumState *pvs, Relation indrel,
-								  PVIndStats *indstats)
+								  PVIndStats *indstats, int *idx_vacuumed)
 {
 	IndexBulkDeleteResult *istat = NULL;
 	IndexBulkDeleteResult *istat_res;
@@ -840,13 +855,19 @@ parallel_vacuum_process_one_index(ParallelVacuumState *pvs, Relation indrel,
 	pvs->indname = pstrdup(RelationGetRelationName(indrel));
 	pvs->status = indstats->status;
 
+	pgstat_progress_update_param(PROGRESS_VACUUM_INDEXRELID, RelationGetRelid(indrel));
+
 	switch (indstats->status)
 	{
 		case PARALLEL_INDVAC_STATUS_NEED_BULKDELETE:
+			pgstat_progress_update_param(PROGRESS_VACUUM_PHASE, PROGRESS_VACUUM_PHASE_VACUUM_INDEX);
 			istat_res = vac_bulkdel_one_index(&ivinfo, istat, pvs->dead_items);
+			(*idx_vacuumed)++;
 			break;
 		case PARALLEL_INDVAC_STATUS_NEED_CLEANUP:
+			pgstat_progress_update_param(PROGRESS_VACUUM_PHASE, PROGRESS_VACUUM_PHASE_INDEX_CLEANUP);
 			istat_res = vac_cleanup_one_index(&ivinfo, istat);
+			(*idx_vacuumed)++;
 			break;
 		default:
 			elog(ERROR, "unexpected parallel vacuum index status %d for index \"%s\"",
@@ -885,6 +906,10 @@ parallel_vacuum_process_one_index(ParallelVacuumState *pvs, Relation indrel,
 	pvs->status = PARALLEL_INDVAC_STATUS_COMPLETED;
 	pfree(pvs->indname);
 	pvs->indname = NULL;
+
+	pgstat_progress_update_param(PROGRESS_VACUUM_NUM_INDEX_COMPLETED, *idx_vacuumed);
+	pgstat_progress_update_param(PROGRESS_VACUUM_INDEXRELID, 0);
+	pgstat_progress_update_param(PROGRESS_VACUUM_TUPLES_REMOVED, 0);
 }
 
 /*
@@ -943,6 +968,7 @@ parallel_vacuum_main(dsm_segment *seg, shm_toc *toc)
 	int			nindexes;
 	char	   *sharedquery;
 	ErrorContextCallback errcallback;
+	int			idx_vacuumed = 0;
 
 	/*
 	 * A parallel vacuum worker must have only PROC_IN_VACUUM flag since we
@@ -965,6 +991,9 @@ parallel_vacuum_main(dsm_segment *seg, shm_toc *toc)
 	 * workers.
 	 */
 	rel = table_open(shared->relid, ShareUpdateExclusiveLock);
+	pgstat_progress_start_command(PROGRESS_COMMAND_VACUUM, RelationGetRelid(rel));
+	pgstat_progress_update_param(PROGRESS_VACUUM_LEADER_PID, shared->leader_pid);
+	pgstat_progress_update_param(PROGRESS_VACUUM_WORKERS_PLANNED, shared->parallel_workers);
 
 	/*
 	 * Open all indexes. indrels are sorted in order by OID, which should be
@@ -973,6 +1002,8 @@ parallel_vacuum_main(dsm_segment *seg, shm_toc *toc)
 	vac_open_indexes(rel, RowExclusiveLock, &nindexes, &indrels);
 	Assert(nindexes > 0);
 
+	pgstat_progress_update_param(PROGRESS_VACUUM_TOTAL_INDEXES, nindexes);
+
 	if (shared->maintenance_work_mem_worker > 0)
 		maintenance_work_mem = shared->maintenance_work_mem_worker;
 
@@ -1022,7 +1053,7 @@ parallel_vacuum_main(dsm_segment *seg, shm_toc *toc)
 	InstrStartParallelQuery();
 
 	/* Process indexes to perform vacuum/cleanup */
-	parallel_vacuum_process_safe_indexes(&pvs);
+	parallel_vacuum_process_safe_indexes(&pvs, &idx_vacuumed);
 
 	/* Report buffer/WAL usage during parallel execution */
 	buffer_usage = shm_toc_lookup(toc, PARALLEL_VACUUM_KEY_BUFFER_USAGE, false);
@@ -1036,6 +1067,7 @@ parallel_vacuum_main(dsm_segment *seg, shm_toc *toc)
 	vac_close_indexes(nindexes, indrels, RowExclusiveLock);
 	table_close(rel, ShareUpdateExclusiveLock);
 	FreeAccessStrategy(pvs.bstrategy);
+	pgstat_progress_end_command();
 }
 
 /*
diff --git a/src/include/commands/progress.h b/src/include/commands/progress.h
index a28938caf4..83e74e44b3 100644
--- a/src/include/commands/progress.h
+++ b/src/include/commands/progress.h
@@ -25,6 +25,12 @@
 #define PROGRESS_VACUUM_NUM_INDEX_VACUUMS		4
 #define PROGRESS_VACUUM_MAX_DEAD_TUPLES			5
 #define PROGRESS_VACUUM_NUM_DEAD_TUPLES			6
+#define PROGRESS_VACUUM_TOTAL_INDEXES			7
+#define PROGRESS_VACUUM_NUM_INDEX_COMPLETED		8
+#define PROGRESS_VACUUM_WORKERS_PLANNED			9
+#define PROGRESS_VACUUM_INDEXRELID				10
+#define PROGRESS_VACUUM_LEADER_PID				11
+#define PROGRESS_VACUUM_TUPLES_REMOVED          12
 
 /* 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 d652f7b5fb..ca2e31cd90 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -2007,26 +2007,63 @@ pg_stat_progress_create_index| SELECT s.pid,
      LEFT JOIN pg_database d ON ((s.datid = d.oid)));
 pg_stat_progress_vacuum| SELECT s.pid,
     s.datid,
-    d.datname,
+    s.datname,
     s.relid,
+    s.phase,
+    s.heap_blks_total,
+    s.heap_blks_scanned,
+    s.heap_blks_vacuumed,
+    s.index_vacuum_count,
+    s.max_dead_tuples,
+    s.num_dead_tuples,
+    s.index_total,
+    s.index_complete_count
+   FROM ( SELECT s_1.pid,
+            s_1.datid,
+            d.datname,
+            s_1.relid,
+            s_1.param12 AS leader_pid,
+                CASE s_1.param1
+                    WHEN 0 THEN 'initializing'::text
+                    WHEN 1 THEN 'scanning heap'::text
+                    WHEN 2 THEN 'vacuuming indexes'::text
+                    WHEN 3 THEN 'vacuuming heap'::text
+                    WHEN 4 THEN 'cleaning up indexes'::text
+                    WHEN 5 THEN 'truncating heap'::text
+                    WHEN 6 THEN 'performing final cleanup'::text
+                    ELSE NULL::text
+                END AS phase,
+            s_1.param2 AS heap_blks_total,
+            s_1.param3 AS heap_blks_scanned,
+            s_1.param4 AS heap_blks_vacuumed,
+            s_1.param5 AS index_vacuum_count,
+            s_1.param6 AS max_dead_tuples,
+            s_1.param7 AS num_dead_tuples,
+            s_1.param8 AS index_total,
+                CASE
+                    WHEN ((s_1.param1 = ANY (ARRAY[(2)::bigint, (4)::bigint])) AND (s_1.param9 > 0) AND (s_1.param10 > 0) AND (s_1.param10 > (count(*) OVER (PARTITION BY s_1.param12) - 1))) THEN ((s_1.param8 - (count(*) OVER (PARTITION BY s_1.param12) - 1)))::numeric
+                    ELSE sum(s_1.param9) OVER (PARTITION BY s_1.param12)
+                END AS index_complete_count
+           FROM (pg_stat_get_progress_info('VACUUM'::text) s_1(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_1.datid = d.oid)))) s
+  WHERE (s.pid = s.leader_pid);
+pg_stat_progress_vacuum_index| SELECT s.pid,
+    s.datid,
+    d.datname,
+    s.param11 AS indexrelid,
         CASE s.param1
-            WHEN 0 THEN 'initializing'::text
-            WHEN 1 THEN 'scanning heap'::text
             WHEN 2 THEN 'vacuuming indexes'::text
-            WHEN 3 THEN 'vacuuming heap'::text
             WHEN 4 THEN 'cleaning up indexes'::text
-            WHEN 5 THEN 'truncating heap'::text
-            WHEN 6 THEN 'performing final cleanup'::text
             ELSE NULL::text
         END AS phase,
-    s.param2 AS heap_blks_total,
-    s.param3 AS heap_blks_scanned,
-    s.param4 AS heap_blks_vacuumed,
-    s.param5 AS index_vacuum_count,
-    s.param6 AS max_dead_tuples,
-    s.param7 AS num_dead_tuples
+        CASE
+            WHEN (s.param12 = s.pid) THEN NULL::bigint
+            ELSE s.param12
+        END AS leader_pid,
+    s.param13 AS tuples_removed
    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_database d ON ((s.datid = d.oid)))
+  WHERE ((s.param1 = ANY (ARRAY[(2)::bigint, (4)::bigint])) AND (s.param11 <> 0));
 pg_stat_replication| SELECT s.pid,
     s.usesysid,
     u.rolname AS usename,
-- 
2.32.0

