0008-Extended-vacuum-statistics-WAL-metrics-for-tables-in.patch

text/plain

Filename: 0008-Extended-vacuum-statistics-WAL-metrics-for-tables-in.patch
Type: text/plain
Part: 7
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 0008
Subject: Extended vacuum statistics: WAL metrics for tables, indexes and database
File+
doc/src/sgml/system-views.sgml 72 0
src/backend/access/heap/vacuumlazy.c 18 1
src/backend/catalog/system_views.sql 14 3
src/backend/utils/activity/pgstat_vacuum.c 4 0
src/backend/utils/adt/pgstatfuncs.c 29 3
src/include/catalog/pg_proc.dat 9 9
src/include/pgstat.h 5 0
src/test/regress/expected/rules.out 15 6
src/test/regress/expected/vacuum_stats.out 46 8
src/test/regress/sql/vacuum_stats.sql 32 2
From 868f4512b0f73f8bb1869e7be33ef0d6035332b3 Mon Sep 17 00:00:00 2001
From: Alena Rybakina <alenka.rybakina@gmail.com>
Date: Tue, 16 Jun 2026 10:53:49 +0300
Subject: [PATCH 8/8] Extended vacuum statistics: WAL metrics for tables,
 indexes and database

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

  wal_records  number of WAL records generated by the vacuum
  wal_fpi      number of WAL full page images generated by the vacuum
  wal_bytes    total amount of WAL generated by the vacuum, in bytes

A vacuum that removes tuples always emits WAL, so wal_records and wal_bytes
are positive.  wal_fpi depends on checkpoint timing; the regression test
exercises its positive path with a dedicated vacuum run right after a
CHECKPOINT.

This completes the extended vacuum statistics views: every metric category is
now reported for tables, indexes and the database aggregate.
---
 doc/src/sgml/system-views.sgml             | 72 ++++++++++++++++++++++
 src/backend/access/heap/vacuumlazy.c       | 19 +++++-
 src/backend/catalog/system_views.sql       | 17 ++++-
 src/backend/utils/activity/pgstat_vacuum.c |  4 ++
 src/backend/utils/adt/pgstatfuncs.c        | 32 +++++++++-
 src/include/catalog/pg_proc.dat            | 18 +++---
 src/include/pgstat.h                       |  5 ++
 src/test/regress/expected/rules.out        | 21 +++++--
 src/test/regress/expected/vacuum_stats.out | 54 +++++++++++++---
 src/test/regress/sql/vacuum_stats.sql      | 34 +++++++++-
 10 files changed, 244 insertions(+), 32 deletions(-)

diff --git a/doc/src/sgml/system-views.sgml b/doc/src/sgml/system-views.sgml
index 0443cbad40..50fcc3aa7d 100644
--- a/doc/src/sgml/system-views.sgml
+++ b/doc/src/sgml/system-views.sgml
@@ -6004,6 +6004,30 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx
        Number of times the failsafe mechanism was triggered to prevent transaction ID wraparound during the vacuum.
       </para></entry>
      </row>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>wal_records</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Total number of WAL records generated by the vacuum.
+      </para></entry>
+     </row>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>wal_fpi</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Total number of WAL full page images generated by the vacuum.
+      </para></entry>
+     </row>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>wal_bytes</structfield> <type>numeric</type>
+      </para>
+      <para>
+       Total amount of WAL generated by the vacuum, in bytes.
+      </para></entry>
+     </row>
     </tbody>
    </tgroup>
   </table>
@@ -6175,6 +6199,30 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx
        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>
+      </para>
+      <para>
+       Number of WAL records generated while vacuuming this index.
+      </para></entry>
+     </row>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>wal_fpi</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Number of WAL full page images generated while vacuuming this index.
+      </para></entry>
+     </row>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>wal_bytes</structfield> <type>numeric</type>
+      </para>
+      <para>
+       Total amount of WAL generated while vacuuming this index, in bytes.
+      </para></entry>
+     </row>
     </tbody>
    </tgroup>
   </table>
@@ -6305,6 +6353,30 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx
        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>
+      </para>
+      <para>
+       Number of WAL records generated by vacuum operations in this database.
+      </para></entry>
+     </row>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>wal_fpi</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Number of WAL full page images generated by vacuum operations in this database.
+      </para></entry>
+     </row>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>wal_bytes</structfield> <type>numeric</type>
+      </para>
+      <para>
+       Total amount of WAL generated by vacuum operations in this database, in bytes.
+      </para></entry>
+     </row>
     </tbody>
    </tgroup>
   </table>
diff --git a/src/backend/access/heap/vacuumlazy.c b/src/backend/access/heap/vacuumlazy.c
index 997eabf1ec..8c2e595a13 100644
--- a/src/backend/access/heap/vacuumlazy.c
+++ b/src/backend/access/heap/vacuumlazy.c
@@ -551,6 +551,7 @@ static void
 extvac_stats_end(Relation rel, LVExtStatCounters * counters,
 				 PgStat_CommonCounts * report)
 {
+	WalUsage	walusage;
 	BufferUsage bufusage;
 	TimestampTz endtime;
 	long		secs;
@@ -561,7 +562,10 @@ extvac_stats_end(Relation rel, LVExtStatCounters * counters,
 
 	memset(report, 0, sizeof(PgStat_CommonCounts));
 
-	/* Calculate diffs of global stat parameters on buffer usage. */
+	/* Calculate diffs of global stat parameters on WAL and buffer usage. */
+	memset(&walusage, 0, sizeof(WalUsage));
+	WalUsageAccumDiff(&walusage, &pgWalUsage, &counters->walusage);
+
 	memset(&bufusage, 0, sizeof(BufferUsage));
 	BufferUsageAccumDiff(&bufusage, &pgBufferUsage, &counters->bufusage);
 
@@ -576,6 +580,10 @@ extvac_stats_end(Relation rel, LVExtStatCounters * counters,
 	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);
@@ -689,6 +697,9 @@ accumulate_heap_vacuum_statistics(LVRelState *vacrel, PgStat_VacuumRelationCount
 	extVacStats->common.total_blks_hit -= vacrel->extVacReportIdx.common.total_blks_hit;
 	extVacStats->common.total_blks_read -= vacrel->extVacReportIdx.common.total_blks_read;
 	extVacStats->common.total_blks_written -= vacrel->extVacReportIdx.common.total_blks_written;
+	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;
@@ -704,6 +715,9 @@ accumulate_idxs_vacuum_statistics(LVRelState *vacrel, PgStat_VacuumRelationCount
 	vacrel->extVacReportIdx.common.total_blks_hit += extVacIdxStats->common.total_blks_hit;
 	vacrel->extVacReportIdx.common.total_blks_read += extVacIdxStats->common.total_blks_read;
 	vacrel->extVacReportIdx.common.total_blks_written += extVacIdxStats->common.total_blks_written;
+	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;
 }
@@ -730,6 +744,9 @@ extvac_accumulate_idx_report(PgStat_VacuumRelationCounts * dst,
 	dst->common.total_blks_written += src->common.total_blks_written;
 	dst->common.blks_fetched += src->common.blks_fetched;
 	dst->common.blks_hit += src->common.blks_hit;
+	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;
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index 11135733c2..31c97120b6 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -1586,7 +1586,10 @@ CREATE VIEW pg_stat_vacuum_tables AS
         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.wraparound_failsafe AS wraparound_failsafe,
+        S.wal_records AS wal_records,
+        S.wal_fpi AS wal_fpi,
+        S.wal_bytes AS wal_bytes
 
     FROM pg_class C JOIN
             pg_namespace N ON N.oid = C.relnamespace,
@@ -1615,7 +1618,11 @@ CREATE VIEW pg_stat_vacuum_indexes AS
             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.total_time AS total_time,
+
+            S.wal_records AS wal_records,
+            S.wal_fpi AS wal_fpi,
+            S.wal_bytes AS wal_bytes
     FROM
             pg_class C JOIN
             pg_index X ON C.oid = X.indrelid JOIN
@@ -1641,7 +1648,11 @@ CREATE VIEW pg_stat_vacuum_database AS
             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.total_time AS total_time,
+
+            S.wal_records AS wal_records,
+            S.wal_fpi AS wal_fpi,
+            S.wal_bytes AS wal_bytes
     FROM
             pg_database D,
             LATERAL pg_stat_get_vacuum_database(D.oid) S;
diff --git a/src/backend/utils/activity/pgstat_vacuum.c b/src/backend/utils/activity/pgstat_vacuum.c
index d0e2eea258..c0a6bf9e97 100644
--- a/src/backend/utils/activity/pgstat_vacuum.c
+++ b/src/backend/utils/activity/pgstat_vacuum.c
@@ -46,6 +46,10 @@ pgstat_accumulate_common(PgStat_CommonCounts *dst, const PgStat_CommonCounts *sr
 	ACCUMULATE_FIELD(blks_fetched);
 	ACCUMULATE_FIELD(blks_hit);
 
+	ACCUMULATE_FIELD(wal_records);
+	ACCUMULATE_FIELD(wal_fpi);
+	ACCUMULATE_FIELD(wal_bytes);
+
 	ACCUMULATE_FIELD(blk_read_time);
 	ACCUMULATE_FIELD(blk_write_time);
 	ACCUMULATE_FIELD(delay_time);
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 75ebff6d68..a03b7ef17b 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -2374,13 +2374,14 @@ pg_stat_have_stats(PG_FUNCTION_ARGS)
 Datum
 pg_stat_get_vacuum_tables(PG_FUNCTION_ARGS)
 {
-#define PG_STAT_GET_VACUUM_TABLES_STATS_COLS 22
+#define PG_STAT_GET_VACUUM_TABLES_STATS_COLS 25
 
 	Oid			relid = PG_GETARG_OID(0);
 	PgStat_VacuumRelationCounts *extvacuum;
 	TupleDesc	tupdesc;
 	Datum		values[PG_STAT_GET_VACUUM_TABLES_STATS_COLS] = {0};
 	bool		nulls[PG_STAT_GET_VACUUM_TABLES_STATS_COLS] = {0};
+	char		buf[256];
 	int			i = 0;
 
 	/* Build a tuple descriptor for our result type */
@@ -2421,6 +2422,13 @@ pg_stat_get_vacuum_tables(PG_FUNCTION_ARGS)
 	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);
+	snprintf(buf, sizeof buf, UINT64_FORMAT, extvacuum->common.wal_bytes);
+	values[i++] = DirectFunctionCall3(numeric_in,
+									  CStringGetDatum(buf),
+									  ObjectIdGetDatum(0),
+									  Int32GetDatum(-1));
 
 	Assert(i == PG_STAT_GET_VACUUM_TABLES_STATS_COLS);
 
@@ -2434,13 +2442,14 @@ 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 13
+#define PG_STAT_GET_VACUUM_INDEX_STATS_COLS 16
 
 	Oid			relid = PG_GETARG_OID(0);
 	PgStat_VacuumRelationCounts *extvacuum;
 	TupleDesc	tupdesc;
 	Datum		values[PG_STAT_GET_VACUUM_INDEX_STATS_COLS] = {0};
 	bool		nulls[PG_STAT_GET_VACUUM_INDEX_STATS_COLS] = {0};
+	char		buf[256];
 	int			i = 0;
 
 	if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
@@ -2475,6 +2484,14 @@ pg_stat_get_vacuum_indexes(PG_FUNCTION_ARGS)
 	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);
+	values[i++] = DirectFunctionCall3(numeric_in,
+									  CStringGetDatum(buf),
+									  ObjectIdGetDatum(0),
+									  Int32GetDatum(-1));
+
 	Assert(i == PG_STAT_GET_VACUUM_INDEX_STATS_COLS);
 
 	/* Returns the record as Datum */
@@ -2487,13 +2504,14 @@ 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 14
 
 	Oid			dbid = PG_GETARG_OID(0);
 	PgStat_VacuumDBCounts *extvacuum;
 	TupleDesc	tupdesc;
 	Datum		values[PG_STAT_GET_VACUUM_DATABASE_STATS_COLS] = {0};
 	bool		nulls[PG_STAT_GET_VACUUM_DATABASE_STATS_COLS] = {0};
+	char		buf[256];
 	int			i = 0;
 
 	if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
@@ -2522,6 +2540,14 @@ pg_stat_get_vacuum_database(PG_FUNCTION_ARGS)
 	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);
+	values[i++] = DirectFunctionCall3(numeric_in,
+									  CStringGetDatum(buf),
+									  ObjectIdGetDatum(0),
+									  Int32GetDatum(-1));
+
 	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 75d6e2b329..4e64f42042 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,float8,float8,float8,float8,int4}',
-  proargmodes => '{i,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}',
+  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,float8,float8,float8,float8}',
-  proargmodes => '{i,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}',
+  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,float8,float8,float8,float8}',
-  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,blk_read_time,blk_write_time,delay_time,total_time}',
+  proallargtypes => '{oid,oid,int4,int8,int8,int8,int8,int4,float8,float8,float8,float8,int8,int8,numeric}',
+  proargmodes => '{i,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}',
   prosrc => 'pg_stat_get_vacuum_database' },
 ]
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index bfc2995389..88f2b027af 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -185,6 +185,11 @@ typedef struct PgStat_CommonCounts
 	int64		blks_fetched;
 	int64		blks_hit;
 
+	/* WAL */
+	int64		wal_records;
+	int64		wal_fpi;
+	uint64		wal_bytes;
+
 	/* Time */
 	double		blk_read_time;
 	double		blk_write_time;
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 952392f66d..d0d2135c06 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -2432,9 +2432,12 @@ pg_stat_vacuum_database| SELECT d.oid AS dboid,
     s.blk_read_time,
     s.blk_write_time,
     s.delay_time,
-    s.total_time
+    s.total_time,
+    s.wal_records,
+    s.wal_fpi,
+    s.wal_bytes
    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, blk_read_time, blk_write_time, delay_time, total_time);
+    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);
 pg_stat_vacuum_indexes| SELECT c.oid AS relid,
     i.oid AS indexrelid,
     n.nspname AS schemaname,
@@ -2451,12 +2454,15 @@ pg_stat_vacuum_indexes| SELECT c.oid AS relid,
     s.blk_read_time,
     s.blk_write_time,
     s.delay_time,
-    s.total_time
+    s.total_time,
+    s.wal_records,
+    s.wal_fpi,
+    s.wal_bytes
    FROM (((pg_class c
      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, blk_read_time, blk_write_time, delay_time, total_time)
+    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,
@@ -2481,10 +2487,13 @@ pg_stat_vacuum_tables| SELECT n.nspname AS schemaname,
     s.blk_write_time,
     s.delay_time,
     s.total_time,
-    s.wraparound_failsafe
+    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, blk_read_time, blk_write_time, delay_time, total_time, wraparound_failsafe)
+    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 b5bae99946..30fadd1e6a 100644
--- a/src/test/regress/expected/vacuum_stats.out
+++ b/src/test/regress/expected/vacuum_stats.out
@@ -161,6 +161,38 @@ SELECT delay_time > 0 AS delay_time,
 (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
+-- positive wal_fpi path is exercised separately below.
+SELECT wal_records > 0 AS wal_records,
+       wal_fpi >= 0 AS wal_fpi,
+       wal_bytes > 0 AS wal_bytes
+  FROM pg_stat_vacuum_tables WHERE relname = 'vacstat_t';
+ wal_records | wal_fpi | wal_bytes 
+-------------+---------+-----------
+ t           | t       | t
+(1 row)
+
+-- WAL full-page-image path: a CHECKPOINT immediately before the vacuum forces
+-- the first modification of each page to emit a full page image, so wal_fpi
+-- advances.
+CREATE TABLE vacstat_fpi (id int PRIMARY KEY, v text)
+  WITH (autovacuum_enabled = off);
+INSERT INTO vacstat_fpi SELECT g, repeat('x', 20) FROM generate_series(1, 1000) g;
+DELETE FROM vacstat_fpi WHERE id % 2 = 0;
+CHECKPOINT;
+VACUUM vacstat_fpi;
+SELECT wal_records > 0 AS wal_records,
+       wal_fpi > 0 AS wal_fpi,
+       wal_bytes > 0 AS wal_bytes
+  FROM pg_stat_vacuum_tables WHERE relname = 'vacstat_fpi';
+ wal_records | wal_fpi | wal_bytes 
+-------------+---------+-----------
+ t           | t       | t
+(1 row)
+
+DROP TABLE vacstat_fpi;
 -- 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
@@ -178,11 +210,14 @@ SELECT indexrelname,
        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
+       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 | blk_read_time | blk_write_time | delay_time | total_time 
-----------------+---------------+----------------+-----------------+----------------+--------------------+--------------------+---------------+--------------+---------------+----------------+------------+------------
- vacstat_t_pkey | t             | 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
@@ -216,10 +251,13 @@ SELECT errors = 0 AS errors,
        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
+       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 | blk_read_time | blk_write_time | delay_time | total_time 
---------+--------------+-------------+--------------------+--------------------+---------------------+---------------+----------------+------------+------------
- t      | 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)
 
diff --git a/src/test/regress/sql/vacuum_stats.sql b/src/test/regress/sql/vacuum_stats.sql
index ee517d6f44..2a366ebdf7 100644
--- a/src/test/regress/sql/vacuum_stats.sql
+++ b/src/test/regress/sql/vacuum_stats.sql
@@ -123,6 +123,30 @@ SELECT delay_time > 0 AS delay_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
+-- positive wal_fpi path is exercised separately below.
+SELECT wal_records > 0 AS wal_records,
+       wal_fpi >= 0 AS wal_fpi,
+       wal_bytes > 0 AS wal_bytes
+  FROM pg_stat_vacuum_tables WHERE relname = 'vacstat_t';
+
+-- WAL full-page-image path: a CHECKPOINT immediately before the vacuum forces
+-- the first modification of each page to emit a full page image, so wal_fpi
+-- advances.
+CREATE TABLE vacstat_fpi (id int PRIMARY KEY, v text)
+  WITH (autovacuum_enabled = off);
+INSERT INTO vacstat_fpi SELECT g, repeat('x', 20) FROM generate_series(1, 1000) g;
+DELETE FROM vacstat_fpi WHERE id % 2 = 0;
+CHECKPOINT;
+VACUUM vacstat_fpi;
+SELECT wal_records > 0 AS wal_records,
+       wal_fpi > 0 AS wal_fpi,
+       wal_bytes > 0 AS wal_bytes
+  FROM pg_stat_vacuum_tables WHERE relname = 'vacstat_fpi';
+DROP TABLE vacstat_fpi;
+
 -- 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
@@ -140,7 +164,10 @@ SELECT indexrelname,
        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
+       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;
 
 -- index page-deletion path: deleting a contiguous key range empties whole
@@ -170,5 +197,8 @@ SELECT errors = 0 AS errors,
        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
+       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();
-- 
2.39.5 (Apple Git-154)