0009-Extended-vacuum-statistics-timing-metrics-for-tables.patch

text/plain

Filename: 0009-Extended-vacuum-statistics-timing-metrics-for-tables.patch
Type: text/plain
Part: 8
Message: Re: Vacuum statistics

Patch

Same data as JSON: GET /api/v1/attachments/:id/patch the parsed metadata as JSON — format, series position, per-file stats; never the diff bytes. API reference →
Format: format-patch
Series: patch 0009
Subject: Extended vacuum statistics: timing metrics for tables, indexes and database
File+
doc/src/sgml/system-views.sgml 97 0
src/backend/access/heap/vacuumlazy.c 37 5
src/backend/catalog/system_views.sql 18 0
src/backend/commands/vacuumparallel.c 1 0
src/backend/utils/activity/pgstat_vacuum.c 5 0
src/backend/utils/adt/pgstatfuncs.c 19 3
src/include/catalog/pg_proc.dat 9 9
src/include/pgstat.h 6 0
src/test/regress/expected/rules.out 15 3
src/test/regress/expected/vacuum_stats.out 58 18
src/test/regress/sql/vacuum_stats.sql 39 8
From f995078939b0180ef6e59603297b35db7031fbfb Mon Sep 17 00:00:00 2001
From: Alena Rybakina <alenka.rybakina@gmail.com>
Date: Wed, 17 Jun 2026 21:54:17 +0300
Subject: [PATCH 9/9] Extended vacuum statistics: timing metrics for tables,
 indexes and database

Expose the timing counters in pg_stat_vacuum_tables, pg_stat_vacuum_indexes
and pg_stat_vacuum_database, with documentation and regression coverage:

  blk_read_time   time spent reading blocks (with track_io_timing)
  blk_write_time  time spent writing blocks (with track_io_timing)
  delay_time      time spent in vacuum cost-based delay
  total_time      total wall-clock time of the vacuum

total_time is always positive; the regression test also exercises the
positive delay_time path with a dedicated cost-delayed vacuum.
---
 doc/src/sgml/system-views.sgml             | 97 ++++++++++++++++++++++
 src/backend/access/heap/vacuumlazy.c       | 42 ++++++++--
 src/backend/catalog/system_views.sql       | 18 ++++
 src/backend/commands/vacuumparallel.c      |  1 +
 src/backend/utils/activity/pgstat_vacuum.c |  5 ++
 src/backend/utils/adt/pgstatfuncs.c        | 22 ++++-
 src/include/catalog/pg_proc.dat            | 18 ++--
 src/include/pgstat.h                       |  6 ++
 src/test/regress/expected/rules.out        | 18 +++-
 src/test/regress/expected/vacuum_stats.out | 76 +++++++++++++----
 src/test/regress/sql/vacuum_stats.sql      | 47 +++++++++--
 11 files changed, 304 insertions(+), 46 deletions(-)

diff --git a/doc/src/sgml/system-views.sgml b/doc/src/sgml/system-views.sgml
index 2a93896d5d..e15f4952cc 100644
--- a/doc/src/sgml/system-views.sgml
+++ b/doc/src/sgml/system-views.sgml
@@ -5964,6 +5964,38 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx
        Number of this relation's blocks found in shared buffers by the vacuum.
       </para></entry>
      </row>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>blk_read_time</structfield> <type>double precision</type>
+      </para>
+      <para>
+       Time spent reading blocks by the vacuum, in milliseconds (when <varname>track_io_timing</varname> is enabled).
+      </para></entry>
+     </row>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>blk_write_time</structfield> <type>double precision</type>
+      </para>
+      <para>
+       Time spent writing (flushing) blocks by the vacuum, in milliseconds; remains zero if no flushes occurred.
+      </para></entry>
+     </row>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>delay_time</structfield> <type>double precision</type>
+      </para>
+      <para>
+       Total time the vacuum spent sleeping in vacuum delay points, in milliseconds.
+      </para></entry>
+     </row>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>total_time</structfield> <type>double precision</type>
+      </para>
+      <para>
+       Total wall-clock time of the vacuum, in milliseconds, including time spent waiting for I/O and locks.
+      </para></entry>
+     </row>
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>wraparound_failsafe</structfield> <type>integer</type>
@@ -6135,6 +6167,38 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx
        Number of block hits within this index during the vacuum.
       </para></entry>
      </row>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>blk_read_time</structfield> <type>double precision</type>
+      </para>
+      <para>
+       Time spent reading index blocks, in milliseconds (if <varname>track_io_timing</varname> is enabled).
+      </para></entry>
+     </row>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>blk_write_time</structfield> <type>double precision</type>
+      </para>
+      <para>
+       Time spent writing index blocks, in milliseconds (if <varname>track_io_timing</varname> is enabled).
+      </para></entry>
+     </row>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>delay_time</structfield> <type>double precision</type>
+      </para>
+      <para>
+       Time spent in vacuum cost-based delay while processing this index, in milliseconds.
+      </para></entry>
+     </row>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>total_time</structfield> <type>double precision</type>
+      </para>
+      <para>
+       Total time spent vacuuming this index, in milliseconds.
+      </para></entry>
+     </row>
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>wal_records</structfield> <type>bigint</type>
@@ -6257,6 +6321,38 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx
        Number of vacuum operations in this database that engaged the wraparound failsafe mechanism.
       </para></entry>
      </row>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>blk_read_time</structfield> <type>double precision</type>
+      </para>
+      <para>
+       Time spent reading blocks by vacuum operations in this database, in milliseconds (if <varname>track_io_timing</varname> is enabled).
+      </para></entry>
+     </row>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>blk_write_time</structfield> <type>double precision</type>
+      </para>
+      <para>
+       Time spent writing blocks by vacuum operations in this database, in milliseconds (if <varname>track_io_timing</varname> is enabled).
+      </para></entry>
+     </row>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>delay_time</structfield> <type>double precision</type>
+      </para>
+      <para>
+       Time spent in vacuum cost-based delay by vacuum operations in this database, in milliseconds.
+      </para></entry>
+     </row>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>total_time</structfield> <type>double precision</type>
+      </para>
+      <para>
+       Total time spent by vacuum operations in this database, in milliseconds.
+      </para></entry>
+     </row>
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>wal_records</structfield> <type>bigint</type>
@@ -6281,6 +6377,7 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx
        Total amount of WAL generated by vacuum operations in this database, in bytes.
       </para></entry>
      </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>interrupts_count</structfield> <type>integer</type>
diff --git a/src/backend/access/heap/vacuumlazy.c b/src/backend/access/heap/vacuumlazy.c
index 683bfcf9e8..6ae2ccb2de 100644
--- a/src/backend/access/heap/vacuumlazy.c
+++ b/src/backend/access/heap/vacuumlazy.c
@@ -512,13 +512,19 @@ static void restore_vacuum_error_info(LVRelState *vacrel,
 static void
 extvac_stats_start(Relation rel, LVExtStatCounters * counters)
 {
+	TimestampTz starttime;
+
 	if (!pgstat_track_vacuum_statistics)
 		return;
 
 	memset(counters, 0, sizeof(LVExtStatCounters));
 
+	starttime = GetCurrentTimestamp();
+
+	counters->starttime = starttime;
 	counters->walusage = pgWalUsage;
 	counters->bufusage = pgBufferUsage;
+	counters->VacuumDelayTime = VacuumDelayTime;
 	counters->blocks_fetched = 0;
 	counters->blocks_hit = 0;
 
@@ -546,6 +552,9 @@ extvac_stats_end(Relation rel, LVExtStatCounters * counters,
 {
 	WalUsage	walusage;
 	BufferUsage bufusage;
+	TimestampTz endtime;
+	long		secs;
+	int			usecs;
 
 	if (!pgstat_track_vacuum_statistics)
 		return;
@@ -559,6 +568,9 @@ extvac_stats_end(Relation rel, LVExtStatCounters * counters,
 	memset(&bufusage, 0, sizeof(BufferUsage));
 	BufferUsageAccumDiff(&bufusage, &pgBufferUsage, &counters->bufusage);
 
+	endtime = GetCurrentTimestamp();
+	TimestampDifference(counters->starttime, endtime, &secs, &usecs);
+
 	/*
 	 * Fill additional statistics on a vacuum processing operation.
 	 */
@@ -566,10 +578,19 @@ extvac_stats_end(Relation rel, LVExtStatCounters * counters,
 	report->total_blks_hit += bufusage.local_blks_hit + bufusage.shared_blks_hit;
 	report->total_blks_dirtied += bufusage.local_blks_dirtied + bufusage.shared_blks_dirtied;
 	report->total_blks_written += bufusage.shared_blks_written;
+
 	report->wal_records += walusage.wal_records;
 	report->wal_fpi += walusage.wal_fpi;
 	report->wal_bytes += walusage.wal_bytes;
 
+	report->blk_read_time += INSTR_TIME_GET_MILLISEC(bufusage.local_blk_read_time);
+	report->blk_read_time += INSTR_TIME_GET_MILLISEC(bufusage.shared_blk_read_time);
+	report->blk_write_time += INSTR_TIME_GET_MILLISEC(bufusage.local_blk_write_time);
+	report->blk_write_time += INSTR_TIME_GET_MILLISEC(bufusage.shared_blk_write_time);
+	report->delay_time += VacuumDelayTime - counters->VacuumDelayTime;
+
+	report->total_time += secs * 1000. + usecs / 1000.;
+
 	if (!rel->pgstat_info || !pgstat_track_counts)
 
 		/*
@@ -669,6 +690,8 @@ accumulate_heap_vacuum_statistics(LVRelState *vacrel, PgStat_VacuumRelationCount
 	extVacStats->table.missed_dead_pages = vacrel->missed_dead_pages;
 	extVacStats->common.wraparound_failsafe_count = vacrel->wraparound_failsafe;
 
+	extVacStats->common.blk_read_time -= vacrel->extVacReportIdx.common.blk_read_time;
+	extVacStats->common.blk_write_time -= vacrel->extVacReportIdx.common.blk_write_time;
 	extVacStats->common.total_blks_dirtied -= vacrel->extVacReportIdx.common.total_blks_dirtied;
 	extVacStats->common.total_blks_hit -= vacrel->extVacReportIdx.common.total_blks_hit;
 	extVacStats->common.total_blks_read -= vacrel->extVacReportIdx.common.total_blks_read;
@@ -676,12 +699,17 @@ accumulate_heap_vacuum_statistics(LVRelState *vacrel, PgStat_VacuumRelationCount
 	extVacStats->common.wal_bytes -= vacrel->extVacReportIdx.common.wal_bytes;
 	extVacStats->common.wal_fpi -= vacrel->extVacReportIdx.common.wal_fpi;
 	extVacStats->common.wal_records -= vacrel->extVacReportIdx.common.wal_records;
+
+	extVacStats->common.total_time -= vacrel->extVacReportIdx.common.total_time;
+	extVacStats->common.delay_time -= vacrel->extVacReportIdx.common.delay_time;
 }
 
 static void
 accumulate_idxs_vacuum_statistics(LVRelState *vacrel, PgStat_VacuumRelationCounts * extVacIdxStats)
 {
 	/* Fill heap-specific extended stats fields */
+	vacrel->extVacReportIdx.common.blk_read_time += extVacIdxStats->common.blk_read_time;
+	vacrel->extVacReportIdx.common.blk_write_time += extVacIdxStats->common.blk_write_time;
 	vacrel->extVacReportIdx.common.total_blks_dirtied += extVacIdxStats->common.total_blks_dirtied;
 	vacrel->extVacReportIdx.common.total_blks_hit += extVacIdxStats->common.total_blks_hit;
 	vacrel->extVacReportIdx.common.total_blks_read += extVacIdxStats->common.total_blks_read;
@@ -689,6 +717,8 @@ accumulate_idxs_vacuum_statistics(LVRelState *vacrel, PgStat_VacuumRelationCount
 	vacrel->extVacReportIdx.common.wal_bytes += extVacIdxStats->common.wal_bytes;
 	vacrel->extVacReportIdx.common.wal_fpi += extVacIdxStats->common.wal_fpi;
 	vacrel->extVacReportIdx.common.wal_records += extVacIdxStats->common.wal_records;
+	vacrel->extVacReportIdx.common.delay_time += extVacIdxStats->common.delay_time;
+	vacrel->extVacReportIdx.common.total_time += extVacIdxStats->common.total_time;
 }
 
 /*
@@ -716,6 +746,10 @@ extvac_accumulate_idx_report(PgStat_VacuumRelationCounts * dst,
 	dst->common.wal_records += src->common.wal_records;
 	dst->common.wal_fpi += src->common.wal_fpi;
 	dst->common.wal_bytes += src->common.wal_bytes;
+	dst->common.blk_read_time += src->common.blk_read_time;
+	dst->common.blk_write_time += src->common.blk_write_time;
+	dst->common.delay_time += src->common.delay_time;
+	dst->common.total_time += src->common.total_time;
 	dst->common.tuples_deleted += src->common.tuples_deleted;
 
 	dst->index.pages_deleted += src->index.pages_deleted;
@@ -904,7 +938,6 @@ heap_vacuum_rel(Relation rel, const VacuumParams *params,
 	Size		dead_items_max_bytes = 0;
 	LVExtStatCounters extVacCounters;
 	PgStat_VacuumRelationCounts extVacReport;
-	TimestampTz starttime;
 
 	/* Initialize vacuum statistics */
 	memset(&extVacReport, 0, sizeof(PgStat_VacuumRelationCounts));
@@ -922,7 +955,6 @@ heap_vacuum_rel(Relation rel, const VacuumParams *params,
 		}
 	}
 
-	starttime = GetCurrentTimestamp();
 	extvac_stats_start(rel, &extVacCounters);
 
 	pgstat_progress_start_command(PROGRESS_COMMAND_VACUUM,
@@ -1279,7 +1311,7 @@ heap_vacuum_rel(Relation rel, const VacuumParams *params,
 						 Max(vacrel->new_live_tuples, 0),
 						 vacrel->recently_dead_tuples +
 						 vacrel->missed_dead_tuples,
-						 starttime);
+						 extVacCounters.starttime);
 	pgstat_progress_end_command();
 
 	if (instrument)
@@ -1287,7 +1319,7 @@ heap_vacuum_rel(Relation rel, const VacuumParams *params,
 		TimestampTz endtime = GetCurrentTimestamp();
 
 		if (verbose || params->log_vacuum_min_duration == 0 ||
-			TimestampDifferenceExceeds(starttime, endtime,
+			TimestampDifferenceExceeds(extVacCounters.starttime, endtime,
 									   params->log_vacuum_min_duration))
 		{
 			long		secs_dur;
@@ -1303,7 +1335,7 @@ heap_vacuum_rel(Relation rel, const VacuumParams *params,
 			int64		total_blks_read;
 			int64		total_blks_dirtied;
 
-			TimestampDifference(starttime, endtime, &secs_dur, &usecs_dur);
+			TimestampDifference(extVacCounters.starttime, endtime, &secs_dur, &usecs_dur);
 			memset(&walusage, 0, sizeof(WalUsage));
 			WalUsageAccumDiff(&walusage, &pgWalUsage, &startwalusage);
 			memset(&bufferusage, 0, sizeof(BufferUsage));
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index d0a58f221c..11029edae3 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -1582,6 +1582,10 @@ CREATE VIEW pg_stat_vacuum_tables AS
         S.total_blks_written AS total_blks_written,
         S.rel_blks_read AS rel_blks_read,
         S.rel_blks_hit AS rel_blks_hit,
+        S.blk_read_time AS blk_read_time,
+        S.blk_write_time AS blk_write_time,
+        S.delay_time AS delay_time,
+        S.total_time AS total_time,
         S.wraparound_failsafe AS wraparound_failsafe,
         S.wal_records AS wal_records,
         S.wal_fpi AS wal_fpi,
@@ -1607,8 +1611,15 @@ CREATE VIEW pg_stat_vacuum_indexes AS
             S.total_blks_hit AS total_blks_hit,
             S.total_blks_dirtied AS total_blks_dirtied,
             S.total_blks_written AS total_blks_written,
+
             S.rel_blks_read AS rel_blks_read,
             S.rel_blks_hit AS rel_blks_hit,
+
+            S.blk_read_time AS blk_read_time,
+            S.blk_write_time AS blk_write_time,
+            S.delay_time AS delay_time,
+            S.total_time AS total_time,
+
             S.wal_records AS wal_records,
             S.wal_fpi AS wal_fpi,
             S.wal_bytes AS wal_bytes
@@ -1631,7 +1642,14 @@ CREATE VIEW pg_stat_vacuum_database AS
             S.db_blks_hit AS db_blks_hit,
             S.total_blks_dirtied AS total_blks_dirtied,
             S.total_blks_written AS total_blks_written,
+
             S.wraparound_failsafe AS wraparound_failsafe,
+
+            S.blk_read_time AS blk_read_time,
+            S.blk_write_time AS blk_write_time,
+            S.delay_time AS delay_time,
+            S.total_time AS total_time,
+
             S.wal_records AS wal_records,
             S.wal_fpi AS wal_fpi,
             S.wal_bytes AS wal_bytes,
diff --git a/src/backend/commands/vacuumparallel.c b/src/backend/commands/vacuumparallel.c
index ea13859485..563a1db912 100644
--- a/src/backend/commands/vacuumparallel.c
+++ b/src/backend/commands/vacuumparallel.c
@@ -1355,6 +1355,7 @@ parallel_vacuum_main(dsm_segment *seg, shm_toc *toc)
 		VacuumUpdateCosts();
 
 	VacuumCostBalance = 0;
+	VacuumDelayTime = 0;
 	VacuumCostBalanceLocal = 0;
 	VacuumSharedCostBalance = &(shared->cost_balance);
 	VacuumActiveNWorkers = &(shared->active_nworkers);
diff --git a/src/backend/utils/activity/pgstat_vacuum.c b/src/backend/utils/activity/pgstat_vacuum.c
index b1c747f255..c5ea644060 100644
--- a/src/backend/utils/activity/pgstat_vacuum.c
+++ b/src/backend/utils/activity/pgstat_vacuum.c
@@ -50,6 +50,11 @@ pgstat_accumulate_common(PgStat_CommonCounts *dst, const PgStat_CommonCounts *sr
 	ACCUMULATE_FIELD(wal_fpi);
 	ACCUMULATE_FIELD(wal_bytes);
 
+	ACCUMULATE_FIELD(blk_read_time);
+	ACCUMULATE_FIELD(blk_write_time);
+	ACCUMULATE_FIELD(delay_time);
+	ACCUMULATE_FIELD(total_time);
+
 	ACCUMULATE_FIELD(tuples_deleted);
 	ACCUMULATE_FIELD(interrupts_count);
 }
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 7473ae1eb6..2d13604bd0 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -2374,7 +2374,7 @@ pg_stat_have_stats(PG_FUNCTION_ARGS)
 Datum
 pg_stat_get_vacuum_tables(PG_FUNCTION_ARGS)
 {
-#define PG_STAT_GET_VACUUM_TABLES_STATS_COLS 21
+#define PG_STAT_GET_VACUUM_TABLES_STATS_COLS 25
 
 	Oid			relid = PG_GETARG_OID(0);
 	PgStat_VacuumRelationCounts *extvacuum;
@@ -2417,6 +2417,10 @@ pg_stat_get_vacuum_tables(PG_FUNCTION_ARGS)
 	values[i++] = Int64GetDatum(extvacuum->common.total_blks_written);
 	values[i++] = Int64GetDatum(extvacuum->common.blks_fetched - extvacuum->common.blks_hit);
 	values[i++] = Int64GetDatum(extvacuum->common.blks_hit);
+	values[i++] = Float8GetDatum(extvacuum->common.blk_read_time);
+	values[i++] = Float8GetDatum(extvacuum->common.blk_write_time);
+	values[i++] = Float8GetDatum(extvacuum->common.delay_time);
+	values[i++] = Float8GetDatum(extvacuum->common.total_time);
 	values[i++] = Int32GetDatum(extvacuum->common.wraparound_failsafe_count);
 	values[i++] = Int64GetDatum(extvacuum->common.wal_records);
 	values[i++] = Int64GetDatum(extvacuum->common.wal_fpi);
@@ -2438,7 +2442,7 @@ pg_stat_get_vacuum_tables(PG_FUNCTION_ARGS)
 Datum
 pg_stat_get_vacuum_indexes(PG_FUNCTION_ARGS)
 {
-#define PG_STAT_GET_VACUUM_INDEX_STATS_COLS 12
+#define PG_STAT_GET_VACUUM_INDEX_STATS_COLS 16
 
 	Oid			relid = PG_GETARG_OID(0);
 	PgStat_VacuumRelationCounts *extvacuum;
@@ -2475,6 +2479,11 @@ pg_stat_get_vacuum_indexes(PG_FUNCTION_ARGS)
 	values[i++] = Int64GetDatum(extvacuum->common.blks_fetched - extvacuum->common.blks_hit);
 	values[i++] = Int64GetDatum(extvacuum->common.blks_hit);
 
+	values[i++] = Float8GetDatum(extvacuum->common.blk_read_time);
+	values[i++] = Float8GetDatum(extvacuum->common.blk_write_time);
+	values[i++] = Float8GetDatum(extvacuum->common.delay_time);
+	values[i++] = Float8GetDatum(extvacuum->common.total_time);
+
 	values[i++] = Int64GetDatum(extvacuum->common.wal_records);
 	values[i++] = Int64GetDatum(extvacuum->common.wal_fpi);
 	snprintf(buf, sizeof buf, UINT64_FORMAT, extvacuum->common.wal_bytes);
@@ -2482,6 +2491,7 @@ pg_stat_get_vacuum_indexes(PG_FUNCTION_ARGS)
 									  CStringGetDatum(buf),
 									  ObjectIdGetDatum(0),
 									  Int32GetDatum(-1));
+
 	Assert(i == PG_STAT_GET_VACUUM_INDEX_STATS_COLS);
 
 	/* Returns the record as Datum */
@@ -2494,7 +2504,7 @@ pg_stat_get_vacuum_indexes(PG_FUNCTION_ARGS)
 Datum
 pg_stat_get_vacuum_database(PG_FUNCTION_ARGS)
 {
-#define PG_STAT_GET_VACUUM_DATABASE_STATS_COLS 11
+#define PG_STAT_GET_VACUUM_DATABASE_STATS_COLS 15
 
 	Oid			dbid = PG_GETARG_OID(0);
 	PgStat_VacuumDBCounts *extvacuum;
@@ -2525,6 +2535,11 @@ pg_stat_get_vacuum_database(PG_FUNCTION_ARGS)
 
 	values[i++] = Int32GetDatum(extvacuum->common.wraparound_failsafe_count);
 
+	values[i++] = Float8GetDatum(extvacuum->common.blk_read_time);
+	values[i++] = Float8GetDatum(extvacuum->common.blk_write_time);
+	values[i++] = Float8GetDatum(extvacuum->common.delay_time);
+	values[i++] = Float8GetDatum(extvacuum->common.total_time);
+
 	values[i++] = Int64GetDatum(extvacuum->common.wal_records);
 	values[i++] = Int64GetDatum(extvacuum->common.wal_fpi);
 	snprintf(buf, sizeof buf, UINT64_FORMAT, extvacuum->common.wal_bytes);
@@ -2533,6 +2548,7 @@ pg_stat_get_vacuum_database(PG_FUNCTION_ARGS)
 									  ObjectIdGetDatum(0),
 									  Int32GetDatum(-1));
 	values[i++] = Int32GetDatum(extvacuum->common.interrupts_count);
+
 	Assert(i == PG_STAT_GET_VACUUM_DATABASE_STATS_COLS);
 
 	/* Returns the record as Datum */
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 743ce9dfd6..e02d386944 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12643,9 +12643,9 @@
   proname => 'pg_stat_get_vacuum_tables', prorows => 1000, provolatile => 's', prorettype => 'record', proisstrict => 'f',
   proretset => 't',
   proargtypes => 'oid',
-  proallargtypes => '{oid,oid,int8,int8,int8,int8,int8,int8,int8,int8,int8,int8,int8,int8,int8,int8,int8,int8,int4,int8,int8,numeric}',
-  proargmodes => '{i,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o}',
-  proargnames => '{reloid,relid,pages_scanned,pages_removed,tuples_deleted,tuples_frozen,recently_dead_tuples,missed_dead_pages,missed_dead_tuples,vm_new_frozen_pages,vm_new_visible_pages,vm_new_visible_frozen_pages,total_blks_read,total_blks_hit,total_blks_dirtied,total_blks_written,rel_blks_read,rel_blks_hit,wraparound_failsafe,wal_records,wal_fpi,wal_bytes}',
+  proallargtypes => '{oid,oid,int8,int8,int8,int8,int8,int8,int8,int8,int8,int8,int8,int8,int8,int8,int8,int8,float8,float8,float8,float8,int4,int8,int8,numeric}',
+  proargmodes => '{i,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o}',
+  proargnames => '{reloid,relid,pages_scanned,pages_removed,tuples_deleted,tuples_frozen,recently_dead_tuples,missed_dead_pages,missed_dead_tuples,vm_new_frozen_pages,vm_new_visible_pages,vm_new_visible_frozen_pages,total_blks_read,total_blks_hit,total_blks_dirtied,total_blks_written,rel_blks_read,rel_blks_hit,blk_read_time,blk_write_time,delay_time,total_time,wraparound_failsafe,wal_records,wal_fpi,wal_bytes}',
   prosrc => 'pg_stat_get_vacuum_tables' }
 
 # oid8 related functions
@@ -12718,17 +12718,17 @@
   proname => 'pg_stat_get_vacuum_indexes', prorows => 1000, provolatile => 's', prorettype => 'record', proisstrict => 'f',
   proretset => 't',
   proargtypes => 'oid',
-  proallargtypes => '{oid,oid,int8,int8,int8,int8,int8,int8,int8,int8,int8,int8,numeric}',
-  proargmodes => '{i,o,o,o,o,o,o,o,o,o,o,o,o}',
-  proargnames => '{reloid,relid,pages_deleted,tuples_deleted,total_blks_read,total_blks_hit,total_blks_dirtied,total_blks_written,rel_blks_read,rel_blks_hit,wal_records,wal_fpi,wal_bytes}',
+  proallargtypes => '{oid,oid,int8,int8,int8,int8,int8,int8,int8,int8,float8,float8,float8,float8,int8,int8,numeric}',
+  proargmodes => '{i,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o}',
+  proargnames => '{reloid,relid,pages_deleted,tuples_deleted,total_blks_read,total_blks_hit,total_blks_dirtied,total_blks_written,rel_blks_read,rel_blks_hit,blk_read_time,blk_write_time,delay_time,total_time,wal_records,wal_fpi,wal_bytes}',
   prosrc => 'pg_stat_get_vacuum_indexes' },
 { oid => '8005',
   descr => 'pg_stat_get_vacuum_database returns vacuum stats values for database',
   proname => 'pg_stat_get_vacuum_database', prorows => 1000, provolatile => 's', prorettype => 'record', proisstrict => 'f',
   proretset => 't',
   proargtypes => 'oid',
-  proallargtypes => '{oid,oid,int4,int8,int8,int8,int8,int4,int8,int8,numeric,int4}',
-  proargmodes => '{i,o,o,o,o,o,o,o,o,o,o,o}',
-  proargnames => '{dbid,dboid,errors,db_blks_read,db_blks_hit,total_blks_dirtied,total_blks_written,wraparound_failsafe,wal_records,wal_fpi,wal_bytes,interrupts_count}',
+  proallargtypes => '{oid,oid,int4,int8,int8,int8,int8,int4,float8,float8,float8,float8,int8,int8,numeric,int4}',
+  proargmodes => '{i,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o}',
+  proargnames => '{dbid,dboid,errors,db_blks_read,db_blks_hit,total_blks_dirtied,total_blks_written,wraparound_failsafe,blk_read_time,blk_write_time,delay_time,total_time,wal_records,wal_fpi,wal_bytes,interrupts_count}',
   prosrc => 'pg_stat_get_vacuum_database' },
 ]
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 18fa9ac5ba..6a9e87cf0b 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -190,6 +190,12 @@ typedef struct PgStat_CommonCounts
 	int64		wal_fpi;
 	uint64		wal_bytes;
 
+	/* Time */
+	double		blk_read_time;
+	double		blk_write_time;
+	double		delay_time;
+	double		total_time;
+
 	/* tuples */
 	int64		tuples_deleted;
 
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index c650cfd664..942361dd42 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -2429,12 +2429,16 @@ pg_stat_vacuum_database| SELECT d.oid AS dboid,
     s.total_blks_dirtied,
     s.total_blks_written,
     s.wraparound_failsafe,
+    s.blk_read_time,
+    s.blk_write_time,
+    s.delay_time,
+    s.total_time,
     s.wal_records,
     s.wal_fpi,
     s.wal_bytes,
     s.interrupts_count
    FROM pg_database d,
-    LATERAL pg_stat_get_vacuum_database(d.oid) s(dboid, errors, db_blks_read, db_blks_hit, total_blks_dirtied, total_blks_written, wraparound_failsafe, wal_records, wal_fpi, wal_bytes, interrupts_count);
+    LATERAL pg_stat_get_vacuum_database(d.oid) s(dboid, errors, db_blks_read, db_blks_hit, total_blks_dirtied, total_blks_written, wraparound_failsafe, blk_read_time, blk_write_time, delay_time, total_time, wal_records, wal_fpi, wal_bytes, interrupts_count);
 pg_stat_vacuum_indexes| SELECT c.oid AS relid,
     i.oid AS indexrelid,
     n.nspname AS schemaname,
@@ -2448,6 +2452,10 @@ pg_stat_vacuum_indexes| SELECT c.oid AS relid,
     s.total_blks_written,
     s.rel_blks_read,
     s.rel_blks_hit,
+    s.blk_read_time,
+    s.blk_write_time,
+    s.delay_time,
+    s.total_time,
     s.wal_records,
     s.wal_fpi,
     s.wal_bytes
@@ -2455,7 +2463,7 @@ pg_stat_vacuum_indexes| SELECT c.oid AS relid,
      JOIN pg_index x ON ((c.oid = x.indrelid)))
      JOIN pg_class i ON ((i.oid = x.indexrelid)))
      LEFT JOIN pg_namespace n ON ((n.oid = c.relnamespace))),
-    LATERAL pg_stat_get_vacuum_indexes(i.oid) s(relid, pages_deleted, tuples_deleted, total_blks_read, total_blks_hit, total_blks_dirtied, total_blks_written, rel_blks_read, rel_blks_hit, wal_records, wal_fpi, wal_bytes)
+    LATERAL pg_stat_get_vacuum_indexes(i.oid) s(relid, pages_deleted, tuples_deleted, total_blks_read, total_blks_hit, total_blks_dirtied, total_blks_written, rel_blks_read, rel_blks_hit, blk_read_time, blk_write_time, delay_time, total_time, wal_records, wal_fpi, wal_bytes)
   WHERE (c.relkind = ANY (ARRAY['r'::"char", 't'::"char", 'm'::"char"]));
 pg_stat_vacuum_tables| SELECT n.nspname AS schemaname,
     c.relname,
@@ -2476,13 +2484,17 @@ pg_stat_vacuum_tables| SELECT n.nspname AS schemaname,
     s.total_blks_written,
     s.rel_blks_read,
     s.rel_blks_hit,
+    s.blk_read_time,
+    s.blk_write_time,
+    s.delay_time,
+    s.total_time,
     s.wraparound_failsafe,
     s.wal_records,
     s.wal_fpi,
     s.wal_bytes
    FROM (pg_class c
      JOIN pg_namespace n ON ((n.oid = c.relnamespace))),
-    LATERAL pg_stat_get_vacuum_tables(c.oid) s(relid, pages_scanned, pages_removed, tuples_deleted, tuples_frozen, recently_dead_tuples, missed_dead_pages, missed_dead_tuples, vm_new_frozen_pages, vm_new_visible_pages, vm_new_visible_frozen_pages, total_blks_read, total_blks_hit, total_blks_dirtied, total_blks_written, rel_blks_read, rel_blks_hit, wraparound_failsafe, wal_records, wal_fpi, wal_bytes)
+    LATERAL pg_stat_get_vacuum_tables(c.oid) s(relid, pages_scanned, pages_removed, tuples_deleted, tuples_frozen, recently_dead_tuples, missed_dead_pages, missed_dead_tuples, vm_new_frozen_pages, vm_new_visible_pages, vm_new_visible_frozen_pages, total_blks_read, total_blks_hit, total_blks_dirtied, total_blks_written, rel_blks_read, rel_blks_hit, blk_read_time, blk_write_time, delay_time, total_time, wraparound_failsafe, wal_records, wal_fpi, wal_bytes)
   WHERE (c.relkind = ANY (ARRAY['r'::"char", 't'::"char", 'm'::"char"]));
 pg_stat_wal| SELECT wal_records,
     wal_fpi,
diff --git a/src/test/regress/expected/vacuum_stats.out b/src/test/regress/expected/vacuum_stats.out
index 3d823ef7b6..bd3c1512d8 100644
--- a/src/test/regress/expected/vacuum_stats.out
+++ b/src/test/regress/expected/vacuum_stats.out
@@ -148,6 +148,49 @@ SELECT rel_blks_read >= 0 AS rel_blks_read,
  t             | t
 (1 row)
 
+-- timing metrics and failsafe.  The vacuum always takes some wall-clock time
+-- (total_time > 0) and does not engage the wraparound failsafe under normal
+-- conditions (wraparound_failsafe = 0).  blk_read_time/blk_write_time are only
+-- non-zero when track_io_timing is enabled, and delay_time only when a vacuum
+-- cost delay is configured, so those are merely checked for being
+-- non-negative; the positive delay_time path is exercised separately below.
+SELECT blk_read_time >= 0 AS blk_read_time,
+       blk_write_time >= 0 AS blk_write_time,
+       delay_time >= 0 AS delay_time,
+       total_time > 0 AS total_time,
+       wraparound_failsafe = 0 AS wraparound_failsafe
+  FROM pg_stat_vacuum_tables WHERE relname = 'vacstat_t';
+ blk_read_time | blk_write_time | delay_time | total_time | wraparound_failsafe 
+---------------+----------------+------------+------------+---------------------
+ t             | t              | t          | t          | t
+(1 row)
+
+-- delay path: with a vacuum cost delay configured, a vacuum that accrues cost
+-- sleeps, so delay_time advances.
+CREATE TABLE vacstat_delay (id int PRIMARY KEY, v text)
+  WITH (autovacuum_enabled = off, fillfactor = 10);
+INSERT INTO vacstat_delay SELECT g, repeat('x', 100) FROM generate_series(1, 3000) g;
+DELETE FROM vacstat_delay WHERE id % 2 = 0;
+SET vacuum_cost_delay = '1ms';
+SET vacuum_cost_limit = 1;
+VACUUM vacstat_delay;
+SELECT pg_stat_force_next_flush();
+ pg_stat_force_next_flush 
+--------------------------
+ 
+(1 row)
+
+RESET vacuum_cost_delay;
+RESET vacuum_cost_limit;
+SELECT delay_time > 0 AS delay_time,
+       total_time > 0 AS total_time
+  FROM pg_stat_vacuum_tables WHERE relname = 'vacstat_delay';
+ delay_time | total_time 
+------------+------------
+ t          | t
+(1 row)
+
+DROP TABLE vacstat_delay;
 -- WAL metrics.  A vacuum that removes tuples always emits WAL
 -- (wal_records > 0, wal_bytes > 0).  wal_fpi depends on whether a checkpoint
 -- happened recently, so it is only checked for being non-negative here; the
@@ -186,17 +229,6 @@ SELECT wal_records > 0 AS wal_records,
 (1 row)
 
 DROP TABLE vacstat_fpi;
--- wraparound failsafe.  A normal vacuum does not engage the wraparound
--- failsafe (wraparound_failsafe = 0).  The positive path requires reaching the
--- failsafe XID age and is covered by a separate TAP test under
--- src/test/modules/xid_wraparound.
-SELECT wraparound_failsafe = 0 AS wraparound_failsafe
-  FROM pg_stat_vacuum_tables WHERE relname = 'vacstat_t';
- wraparound_failsafe 
----------------------
- t
-(1 row)
-
 -- per-index view: the primary key index is processed by the same VACUUM.
 -- No btree leaf empties out (interleaved deletions), so pages_deleted = 0,
 -- while every index entry for a removed heap tuple is deleted.  The index is
@@ -211,13 +243,17 @@ SELECT indexrelname,
        total_blks_written >= 0 AS total_blks_written,
        rel_blks_read >= 0 AS rel_blks_read,
        rel_blks_hit > 0 AS rel_blks_hit,
+       blk_read_time >= 0 AS blk_read_time,
+       blk_write_time >= 0 AS blk_write_time,
+       delay_time >= 0 AS delay_time,
+       total_time > 0 AS total_time,
        wal_records > 0 AS wal_records,
        wal_fpi >= 0 AS wal_fpi,
        wal_bytes > 0 AS wal_bytes
   FROM pg_stat_vacuum_indexes WHERE relname = 'vacstat_t' ORDER BY indexrelname;
-  indexrelname  | pages_deleted | tuples_deleted | total_blks_read | total_blks_hit | total_blks_dirtied | total_blks_written | rel_blks_read | rel_blks_hit | wal_records | wal_fpi | wal_bytes 
-----------------+---------------+----------------+-----------------+----------------+--------------------+--------------------+---------------+--------------+-------------+---------+-----------
- vacstat_t_pkey | t             | t              | t               | t              | t                  | t                  | t             | t            | t           | t       | t
+  indexrelname  | pages_deleted | tuples_deleted | total_blks_read | total_blks_hit | total_blks_dirtied | total_blks_written | rel_blks_read | rel_blks_hit | blk_read_time | blk_write_time | delay_time | total_time | wal_records | wal_fpi | wal_bytes 
+----------------+---------------+----------------+-----------------+----------------+--------------------+--------------------+---------------+--------------+---------------+----------------+------------+------------+-------------+---------+-----------
+ vacstat_t_pkey | t             | t              | t               | t              | t                  | t                  | t             | t            | t             | t              | t          | t          | t           | t       | t
 (1 row)
 
 -- index page-deletion path: deleting a contiguous key range empties whole
@@ -253,20 +289,24 @@ SELECT indexrelname,
 DROP TABLE vacstat_idxdel;
 -- per-database aggregate view: no vacuum errors occurred in this database, and
 -- the vacuums in this database touched pages through the buffer cache
--- (db_blks_hit > 0) and emit WAL (wal_records > 0).
+-- (db_blks_hit > 0).
 SELECT errors = 0 AS errors,
        db_blks_read >= 0 AS db_blks_read,
        db_blks_hit > 0 AS db_blks_hit,
        total_blks_dirtied >= 0 AS total_blks_dirtied,
        total_blks_written >= 0 AS total_blks_written,
        wraparound_failsafe = 0 AS wraparound_failsafe,
+       blk_read_time >= 0 AS blk_read_time,
+       blk_write_time >= 0 AS blk_write_time,
+       delay_time >= 0 AS delay_time,
+       total_time > 0 AS total_time,
        wal_records > 0 AS wal_records,
        wal_fpi >= 0 AS wal_fpi,
        wal_bytes > 0 AS wal_bytes
   FROM pg_stat_vacuum_database WHERE dbname = current_database();
- errors | db_blks_read | db_blks_hit | total_blks_dirtied | total_blks_written | wraparound_failsafe | wal_records | wal_fpi | wal_bytes 
---------+--------------+-------------+--------------------+--------------------+---------------------+-------------+---------+-----------
- t      | t            | t           | t                  | t                  | t                   | t           | t       | t
+ errors | db_blks_read | db_blks_hit | total_blks_dirtied | total_blks_written | wraparound_failsafe | blk_read_time | blk_write_time | delay_time | total_time | wal_records | wal_fpi | wal_bytes 
+--------+--------------+-------------+--------------------+--------------------+---------------------+---------------+----------------+------------+------------+-------------+---------+-----------
+ t      | t            | t           | t                  | t                  | t                   | t             | t              | t          | t          | t           | t       | t
 (1 row)
 
 -- parallel index vacuum: index statistics must be captured for indexes
diff --git a/src/test/regress/sql/vacuum_stats.sql b/src/test/regress/sql/vacuum_stats.sql
index 40f7d0985d..baf5acdafd 100644
--- a/src/test/regress/sql/vacuum_stats.sql
+++ b/src/test/regress/sql/vacuum_stats.sql
@@ -98,6 +98,36 @@ SELECT rel_blks_read >= 0 AS rel_blks_read,
        rel_blks_hit > 0 AS rel_blks_hit
   FROM pg_stat_vacuum_tables WHERE relname = 'vacstat_t';
 
+-- timing metrics and failsafe.  The vacuum always takes some wall-clock time
+-- (total_time > 0) and does not engage the wraparound failsafe under normal
+-- conditions (wraparound_failsafe = 0).  blk_read_time/blk_write_time are only
+-- non-zero when track_io_timing is enabled, and delay_time only when a vacuum
+-- cost delay is configured, so those are merely checked for being
+-- non-negative; the positive delay_time path is exercised separately below.
+SELECT blk_read_time >= 0 AS blk_read_time,
+       blk_write_time >= 0 AS blk_write_time,
+       delay_time >= 0 AS delay_time,
+       total_time > 0 AS total_time,
+       wraparound_failsafe = 0 AS wraparound_failsafe
+  FROM pg_stat_vacuum_tables WHERE relname = 'vacstat_t';
+
+-- delay path: with a vacuum cost delay configured, a vacuum that accrues cost
+-- sleeps, so delay_time advances.
+CREATE TABLE vacstat_delay (id int PRIMARY KEY, v text)
+  WITH (autovacuum_enabled = off, fillfactor = 10);
+INSERT INTO vacstat_delay SELECT g, repeat('x', 100) FROM generate_series(1, 3000) g;
+DELETE FROM vacstat_delay WHERE id % 2 = 0;
+SET vacuum_cost_delay = '1ms';
+SET vacuum_cost_limit = 1;
+VACUUM vacstat_delay;
+SELECT pg_stat_force_next_flush();
+RESET vacuum_cost_delay;
+RESET vacuum_cost_limit;
+SELECT delay_time > 0 AS delay_time,
+       total_time > 0 AS total_time
+  FROM pg_stat_vacuum_tables WHERE relname = 'vacstat_delay';
+DROP TABLE vacstat_delay;
+
 -- WAL metrics.  A vacuum that removes tuples always emits WAL
 -- (wal_records > 0, wal_bytes > 0).  wal_fpi depends on whether a checkpoint
 -- happened recently, so it is only checked for being non-negative here; the
@@ -123,13 +153,6 @@ SELECT wal_records > 0 AS wal_records,
   FROM pg_stat_vacuum_tables WHERE relname = 'vacstat_fpi';
 DROP TABLE vacstat_fpi;
 
--- wraparound failsafe.  A normal vacuum does not engage the wraparound
--- failsafe (wraparound_failsafe = 0).  The positive path requires reaching the
--- failsafe XID age and is covered by a separate TAP test under
--- src/test/modules/xid_wraparound.
-SELECT wraparound_failsafe = 0 AS wraparound_failsafe
-  FROM pg_stat_vacuum_tables WHERE relname = 'vacstat_t';
-
 -- per-index view: the primary key index is processed by the same VACUUM.
 -- No btree leaf empties out (interleaved deletions), so pages_deleted = 0,
 -- while every index entry for a removed heap tuple is deleted.  The index is
@@ -144,6 +167,10 @@ SELECT indexrelname,
        total_blks_written >= 0 AS total_blks_written,
        rel_blks_read >= 0 AS rel_blks_read,
        rel_blks_hit > 0 AS rel_blks_hit,
+       blk_read_time >= 0 AS blk_read_time,
+       blk_write_time >= 0 AS blk_write_time,
+       delay_time >= 0 AS delay_time,
+       total_time > 0 AS total_time,
        wal_records > 0 AS wal_records,
        wal_fpi >= 0 AS wal_fpi,
        wal_bytes > 0 AS wal_bytes
@@ -168,13 +195,17 @@ DROP TABLE vacstat_idxdel;
 
 -- per-database aggregate view: no vacuum errors occurred in this database, and
 -- the vacuums in this database touched pages through the buffer cache
--- (db_blks_hit > 0) and emit WAL (wal_records > 0).
+-- (db_blks_hit > 0).
 SELECT errors = 0 AS errors,
        db_blks_read >= 0 AS db_blks_read,
        db_blks_hit > 0 AS db_blks_hit,
        total_blks_dirtied >= 0 AS total_blks_dirtied,
        total_blks_written >= 0 AS total_blks_written,
        wraparound_failsafe = 0 AS wraparound_failsafe,
+       blk_read_time >= 0 AS blk_read_time,
+       blk_write_time >= 0 AS blk_write_time,
+       delay_time >= 0 AS delay_time,
+       total_time > 0 AS total_time,
        wal_records > 0 AS wal_records,
        wal_fpi >= 0 AS wal_fpi,
        wal_bytes > 0 AS wal_bytes
-- 
2.39.5 (Apple Git-154)