From 249378140e1016436401da4f117cb5efeab9351e Mon Sep 17 00:00:00 2001
From: Bharath Rupireddy <bharath.rupireddyforpostgres@gmail.com>
Date: Wed, 2 Sep 2026 23:27:23 +0000
Subject: [PATCH v5 2/2] Report index block progress in
 pg_stat_progress_vacuum.

Previously, pg_stat_progress_vacuum reported which index a backend
was vacuuming but not how far it had gotten through that index. This
matters most for B-tree, the common index type: for a large B-tree,
at the scale of hundreds of GBs to TBs, per-index progress is what
lets one estimate when the index phase, and together with
heap_blks_* the whole vacuum, will finish.

This commit reuses the block counters that CREATE INDEX progress
reporting added in commit ab0dfc961 (PROGRESS_SCAN_BLOCKS_TOTAL and
PROGRESS_SCAN_BLOCKS_DONE), exposing them as new index_blks_total
and index_blks_done columns. B-tree's bulk-delete scan already knows
how to report them, so this commit only turns that reporting on in
the serial and parallel index-vacuum paths. Like current_index_relid,
the counters are reported per participating backend, so each worker's
row shows progress for the index it is scanning.

These counters are kept separate from heap_blks_*. The heap block
counters must be retained across a multi-pass index vacuum, which
happens when the dead-TID store fills, and in the serial case the
leader does the index vacuuming itself, so reusing the heap counters
for index blocks would corrupt heap progress that is still needed.

No caller sets IndexVacuumInfo.report_progress to false anymore;
removing it is parked for a follow-up commit.

Author: Bharath Rupireddy <bharath.rupireddyforpostgres@gmail.com>
Reviewed-by: Michael Paquier <michael@paquier.xyz>
Reviewed-by: Sami Imseih <samimseih@gmail.com>
Discussion: https://postgr.es/m/CALj2ACX6gyBmQfaqbCsycDmaSPbq1=iPJw1OqUT+aLqaKjW8dQ@mail.gmail.com
---
 doc/src/sgml/monitoring.sgml          | 28 +++++++++++++++++++++--
 src/backend/access/heap/vacuumlazy.c  | 32 ++++++++++++++++++++-------
 src/backend/catalog/system_views.sql  |  4 +++-
 src/backend/commands/vacuumparallel.c | 16 ++++++++++----
 src/test/regress/expected/rules.out   |  4 +++-
 5 files changed, 68 insertions(+), 16 deletions(-)

diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index e39f9637775..d7f6a83edfd 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -7929,6 +7929,29 @@ FROM pg_stat_get_backend_idset() AS backendid;
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>index_blks_total</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Total number of blocks to scan for the B-tree index identified by
+       <structfield>current_index_relid</structfield>. It is reset to 0 once
+       the index has been processed.
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>index_blks_done</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Number of blocks scanned so far for the B-tree index identified by
+       <structfield>current_index_relid</structfield>. Together with
+       <structfield>index_blks_total</structfield> this gives per-index vacuum
+       progress, which is useful for estimating completion of large indexes.
+      </para></entry>
+     </row>
+
     </tbody>
    </tgroup>
   </table>
@@ -7939,8 +7962,9 @@ FROM pg_stat_get_backend_idset() AS backendid;
     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
+    <structfield>phase</structfield>, <structfield>current_index_relid</structfield>,
+    <structfield>index_blks_total</structfield> and
+    <structfield>index_blks_done</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
diff --git a/src/backend/access/heap/vacuumlazy.c b/src/backend/access/heap/vacuumlazy.c
index 8bc3de84581..459fff8df1d 100644
--- a/src/backend/access/heap/vacuumlazy.c
+++ b/src/backend/access/heap/vacuumlazy.c
@@ -3038,11 +3038,17 @@ lazy_vacuum_one_index(Relation indrel, IndexBulkDeleteResult *istat,
 {
 	IndexVacuumInfo ivinfo;
 	LVSavedErrInfo saved_err_info;
+	const int	reset_index[] = {
+		PROGRESS_VACUUM_CURRENT_INDEX_RELID,
+		PROGRESS_SCAN_BLOCKS_TOTAL,
+		PROGRESS_SCAN_BLOCKS_DONE
+	};
+	const int64 reset_val[] = {(int64) InvalidOid, 0, 0};
 
 	ivinfo.index = indrel;
 	ivinfo.heaprel = vacrel->rel;
 	ivinfo.analyze_only = false;
-	ivinfo.report_progress = false;
+	ivinfo.report_progress = true;
 	ivinfo.estimated_count = true;
 	ivinfo.message_level = DEBUG2;
 	ivinfo.num_heap_tuples = reltuples;
@@ -3073,9 +3079,11 @@ 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);
+	/*
+	 * Reset the current index progress parameters to avoid reporting stale
+	 * values.
+	 */
+	pgstat_progress_update_multi_param(3, reset_index, reset_val);
 
 	return istat;
 }
@@ -3096,11 +3104,17 @@ lazy_cleanup_one_index(Relation indrel, IndexBulkDeleteResult *istat,
 {
 	IndexVacuumInfo ivinfo;
 	LVSavedErrInfo saved_err_info;
+	const int	reset_index[] = {
+		PROGRESS_VACUUM_CURRENT_INDEX_RELID,
+		PROGRESS_SCAN_BLOCKS_TOTAL,
+		PROGRESS_SCAN_BLOCKS_DONE
+	};
+	const int64 reset_val[] = {(int64) InvalidOid, 0, 0};
 
 	ivinfo.index = indrel;
 	ivinfo.heaprel = vacrel->rel;
 	ivinfo.analyze_only = false;
-	ivinfo.report_progress = false;
+	ivinfo.report_progress = true;
 	ivinfo.estimated_count = estimated_count;
 	ivinfo.message_level = DEBUG2;
 
@@ -3130,9 +3144,11 @@ 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);
+	/*
+	 * Reset the current index progress parameters to avoid reporting stale
+	 * values.
+	 */
+	pgstat_progress_update_multi_param(3, reset_index, reset_val);
 
 	return istat;
 }
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index 24d2ae8bb44..f29e2767154 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -1354,7 +1354,9 @@ CREATE VIEW pg_stat_progress_vacuum AS
                        WHEN 2 THEN 'autovacuum'
                        WHEN 3 THEN 'autovacuum_wraparound'
                        ELSE NULL END AS started_by,
-        CAST(S.param14 AS oid) AS current_index_relid
+        CAST(S.param14 AS oid) AS current_index_relid,
+        S.param16 AS index_blks_total,
+        S.param17 AS index_blks_done
     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 d6a56f63a0f..be87b1937ad 100644
--- a/src/backend/commands/vacuumparallel.c
+++ b/src/backend/commands/vacuumparallel.c
@@ -1081,6 +1081,12 @@ parallel_vacuum_process_one_index(ParallelVacuumState *pvs, Relation indrel,
 		PROGRESS_VACUUM_CURRENT_INDEX_RELID
 	};
 	int64		progress_val[2];
+	const int	reset_index[] = {
+		PROGRESS_VACUUM_CURRENT_INDEX_RELID,
+		PROGRESS_SCAN_BLOCKS_TOTAL,
+		PROGRESS_SCAN_BLOCKS_DONE
+	};
+	const int64 reset_val[] = {(int64) InvalidOid, 0, 0};
 
 	/*
 	 * Update the pointer to the corresponding bulk-deletion result if someone
@@ -1092,7 +1098,7 @@ parallel_vacuum_process_one_index(ParallelVacuumState *pvs, Relation indrel,
 	ivinfo.index = indrel;
 	ivinfo.heaprel = pvs->heaprel;
 	ivinfo.analyze_only = false;
-	ivinfo.report_progress = false;
+	ivinfo.report_progress = true;
 	ivinfo.message_level = DEBUG2;
 	ivinfo.estimated_count = pvs->shared->estimated_count;
 	ivinfo.num_heap_tuples = pvs->shared->reltuples;
@@ -1127,9 +1133,11 @@ 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);
+	/*
+	 * Reset the current index progress parameters to avoid reporting stale
+	 * values.
+	 */
+	pgstat_progress_update_multi_param(3, reset_index, reset_val);
 
 	/*
 	 * Copy the index bulk-deletion result returned from ambulkdelete and
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index cf9b8c823c8..475604289de 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -2214,7 +2214,9 @@ pg_stat_progress_vacuum| SELECT s.pid,
             WHEN 3 THEN 'autovacuum_wraparound'::text
             ELSE NULL::text
         END AS started_by,
-    (s.param14)::oid AS current_index_relid
+    (s.param14)::oid AS current_index_relid,
+    s.param16 AS index_blks_total,
+    s.param17 AS index_blks_done
    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

