Subject: Re: Vacuum statistics

From: Alena Rybakina <[email-masked]>
Date: Mon, 13 Jan 2025 14:19:46 +0300

Hi, all! I updated the patches and I solved the problems with parallel 
vacuum.

On 19.12.2024 13:37, Alena Rybakina wrote:
>
> Hi! I updated patch.
>
> On 02.12.2024 23:12, Alena Rybakina wrote:
>> On 02.12.2024 11:27, Alexander Korotkov wrote:
>>> Hi, Alena!
>>>
>>> On Wed, Nov 13, 2024 at 6:21 PM Alena Rybakina
>>> <[email-masked]>  wrote:
>>>> Updated 0001-v13 attached, as well as the diff between v12 and v13.
>>>>
>>>> Thank you)
>>>>
>>>> And I agree with your changes. And included them in patches.
>>> Thank you for the updated patchset.  Some points from me.
>>>
>>> * I've read the previous discussion on how important to keep all these
>>> fields regarding vacuum statistics including points by Andrei and Jim.
>>> It still worrying me that statistics volume is going to burst in about
>>> 3 times, but I don't have a particular proposal on how to make more
>>> granular approach.  I wonder if you could propose something.
>
> I propose to solve this with a guc (track_vacuum_statistics) that will 
> allow us to disable statistics collection or enable it back when 
> needed. This statistics is useful if you collect it over a certain 
> period of time and watch the dynamics of the change, so I think a hook 
> will not hurt here.
>
> I also added new statistics vm_new_visible_frozen_pages due to the 
> dc6acfd commit and renamed some of my statistics from frozen_pages to 
> vm_new_frozen_pages and all-visible_pages to vm_new_visible_pages. I 
> also added statistics missed_tupes, missed_pages. Both are necessary 
> to take into account how many tuples were not cleared by vacuum due to 
> failure to acquire a cleanup lock on a heap page. The second 
> statistics missed_pages will allow you to track whether this is one 
> particular page or not.
>
> This information can make it clear that perhaps the data is broken 
> somewhere or there is an error in the operation of the database, for 
> example.
>
I changed the purpose of the guc. By default, vacuum database statistics 
are always collected, and the track_vacuum_statistics hook enables the 
ability to collect extended statistics for a given database's relations.
This is done to achieve a balance between the allocated memory for 
storing statistics and having the necessary monitoring at hand to track 
the state of vacuum operation.
In addition to the fact that there are far fewer databases in a system 
than relations, vacuum statistics for a database contain fewer 
statistics than relations, but
they are enough to indicate that something may be wrong in the system 
and prompt the administrator to enable extended monitoring for relations.

By default, the guc is disabled.

> I also noticed that my stats for indexes were not being collected 
> while parallel vacuum was running. I fixed it by adding some extra 
> code that basically captured changes to the 
> parallel_vacuum_process_all_indexes function. I used a script like 
> this to check if everything was correct.
>
> pgbench -d postgres -i -s 10
>
>
> my/inst/bin/pg_basebackup -D ~/backup
>
>
> #psql
>
> --check parallel vacuum statistics
>
> create index accounts_idx1 on pgbench_accounts(bid);
> create index accounts_idx2 on pgbench_accounts(aid, bid);
>
> delete from pgbench_accounts where aid >5;
> set max_parallel_maintenance_workers = 8;
> VACUUM (PARALLEL 3) pgbench_accounts;
>
>
> create index accounts_idx1 on pgbench_accounts(bid);
> create index accounts_idx2 on pgbench_accounts(aid, bid);
>
> delete from pgbench_accounts where aid >5;
> set max_parallel_maintenance_workers = 8;
> VACUUM (PARALLEL 3) pgbench_accounts;
>
>
> pg_ctl -D ../postgres_data11 -l logfile stop
>
> rm -rf ../postgres_data/*
>
> cp -r ~/backup/* ~/postgres_data/
>
> pg_ctl -D ../postgres_data11 -l logfile start
>
>
> --check vacuum statistics processed by postmaster only
>
> create index accounts_idx1 on pgbench_accounts(bid);
> create index accounts_idx2 on pgbench_accounts(aid, bid);
>
> delete from pgbench_accounts where aid >5;
> set max_parallel_maintenance_workers = 8;
> VACUUM (PARALLEL 0) pgbench_accounts;
>
> I noticed that rel_blks_read and rel_blks_hit are too small compared 
> to the vacuum statistics when the vacuum is not parallel. I suspect 
> that this problem is related to the fact that the relationship 
> statistics have not reached that time. You can see that they are 
> calculated in my patch like this:
>
> report->blks_fetched =
>     rel->pgstat_info->counts.blocks_fetched - counters->blocks_fetched;
>   report->blks_hit =
>     rel->pgstat_info->counts.blocks_hit - counters->blocks_hit;
>
>
> The second thing that bothered me was that some table stats differ in 
> the fields total_blks_read, rel_blks_read, pages_removed. If with the 
> buffer this could be related to the fact that in a single run we rely 
> on the stats of the global buffer and shaft statistics and this could 
> explain why there are more of them, then with pages_removed I have no 
> explanation yet as to what could have happened. I am still studying this.
>
> When you have time, take a look at the patches, I will be glad to 
> receive any feedback.
>
I fixed this problem. Now the statistics of parallel and non-parallel 
vacuums are almost equal. See files output_vacuum_0_workers and 
output_vacuum_3_workers. The results were obtained from the test that I 
described above.
In fact, we need to add tracking statistics in the 
parallel_vacuum_one_index. But I noticed another problem that I have 
already fixed.

Vacuum statistics for indexes were accumulated in heap vacuum statistics 
because of the complexity of vacuum operations,
namely, vacuum statistics for index cleaning were included in heap 
relationship vacuum statistics.
Vacuum switches from cleaning the table to its indexes and back during 
its operation, and we need to take this into account.
So, before cleaning indexes, we now save the collected vacuum statistics 
for the heap, but we send it only after the processing is complete.

> To view statistics:
>
> select vt.relname, total_blks_read AS total_blks_read,
>   total_blks_hit AS total_blks_hit,
>   total_blks_dirtied AS total_blks_dirtied,
>   total_blks_written AS total_blks_written,
>
>   rel_blks_read AS rel_blks_read,
>   rel_blks_hit AS rel_blks_hit,
>
>   pages_deleted AS pages_deleted,
>   tuples_deleted AS tuples_deleted,
>
>   wal_records AS wal_records,
>   wal_fpi AS wal_fpi,
>   wal_bytes AS wal_bytes,
>
>   blk_read_time AS blk_read_time,
>   blk_write_time AS blk_write_time,
>
>   delay_time AS delay_time,
>   total_time AS total_time
>   FROM pg_stat_get_vacuum_indexes vt, pg_class c
> WHERE (vt.relname='accounts_idx1' or vt.relname='accounts_idx2' or 
> vt.relname = 'pgbench_accounts_pkey') AND vt.relid = c.oid;
>
>
> select stats.relname,stats.total_blks_read AS total_blks_read,
>   stats.total_blks_hit AS total_blks_hit,
>   stats.total_blks_dirtied AS total_blks_dirtied,
>   stats.total_blks_written AS total_blks_written,
>
>   stats.rel_blks_read AS rel_blks_read,
>   stats.rel_blks_hit AS rel_blks_hit,
>
>   stats.pages_scanned AS pages_scanned,
>   stats.pages_removed AS pages_removed,
>   stats.pages_frozen AS pages_frozen,
>   stats.pages_all_visible AS pages_all_visible,
>   stats.tuples_deleted AS tuples_deleted,
>   stats.tuples_frozen AS tuples_frozen,
>   stats.dead_tuples AS dead_tuples,
>
>   stats.index_vacuum_count AS index_vacuum_count,
>   stats.wal_records AS wal_records,
>   stats.wal_fpi AS wal_fpi,
>   stats.wal_bytes AS wal_bytes,
>
>   stats.blk_read_time AS blk_read_time,
>   stats.blk_write_time AS blk_write_time,
>
>   stats.delay_time AS delay_time,
>   stats.total_time AS total_time from pg_stat_vacuum_tables stats, 
> pg_stat_all_tables  WHERE stats.relname = 'pgbench_accounts' and 
> stats.relid = pg_stat_all_tables.relid;
>
>
Just in case, I'll write that during the test I used simpler queries:

select * from pg_stat_vacuum_tables where relname like '%accounts%';

select * from pg_stat_vacuum_indexes where relname like '%accounts_%';

-- 
Regards,
Alena Rybakina
Postgres Professional
From 270446c07b03d9891e3867657c95fe2721b55e82 Mon Sep 17 00:00:00 2001
From: Alena Rybakina <[email-masked]>
Date: Fri, 10 Jan 2025 09:57:13 +0300
Subject: [PATCH 1/4] Machinery for grabbing an extended vacuum statistics on
 heap relations.

Value of total_blks_hit, total_blks_read, total_blks_dirtied are number of
hitted, missed and dirtied pages in shared buffers during a vacuum operation
respectively.

total_blks_dirtied means 'dirtied only by this action'. So, if this page was
dirty before the vacuum operation, it doesn't count this page as 'dirtied'.

The tuples_deleted parameter is the number of tuples cleaned up by the vacuum
operation.

The delay_time value means total vacuum sleep time in vacuum delay point.
The pages_removed value is the number of pages by which the physical data
storage of the relation was reduced.
The value of pages_deleted parameter is the number of freed pages in the table
(file size may not have changed).

Tracking of IO during an (auto)vacuum operation.
Introduced variables blk_read_time and blk_write_time tracks only access to
buffer pages and flushing them to disk. Reading operation is trivial, but
writing measurement technique is not obvious.
So, during a vacuum writing time can be zero incremented because no any flushing
operations were performed.

System time and user time are parameters that describes how much time a vacuum
operation has spent in executing of code in user space and kernel space
accordingly. Also, accumulate total time of a vacuum that is a diff between
timestamps in start and finish points in the vacuum code.
Remember about idle time, when vacuum waited for IO and locks, so total time
isn't equal a sum of user and system time, but no less.

pages_frozen is a number of pages that are marked as frozen in vm during vacuum.
This parameter is incremented if page is marked as all-frozen.
pages_all_visible is a number of pages that are marked as all-visible in vm during
vacuum.

wraparound_failsafe_count is a number of times when the vacuum starts urgent cleanup
to prevent wraparound problem which is critical for the database.

Authors: Alena Rybakina <[email-masked]>,
	 Andrei Lepikhov <[email-masked]>,
	 Andrei Zubkov <[email-masked]>
Reviewed-by: Dilip Kumar <[email-masked]>, Masahiko Sawada <[email-masked]>,
	     Ilia Evdokimov <[email-masked]>, jian he <[email-masked]>,
	     Kirill Reshke <[email-masked]>, Alexander Korotkov <[email-masked]>,
	     Jim Nasby <[email-masked]>, Sami Imseih <[email-masked]>
---
 src/backend/access/heap/vacuumlazy.c          | 150 +++++++++++-
 src/backend/access/heap/visibilitymap.c       |  10 +
 src/backend/catalog/system_views.sql          |  52 +++-
 src/backend/commands/vacuum.c                 |   4 +
 src/backend/commands/vacuumparallel.c         |   1 +
 src/backend/utils/activity/pgstat.c           |  12 +-
 src/backend/utils/activity/pgstat_relation.c  |  47 +++-
 src/backend/utils/adt/pgstatfuncs.c           | 147 ++++++++++++
 src/backend/utils/error/elog.c                |  13 +
 src/backend/utils/misc/guc_tables.c           |   9 +
 src/include/catalog/pg_proc.dat               |  18 ++
 src/include/commands/vacuum.h                 |   1 +
 src/include/pgstat.h                          |  82 ++++++-
 src/include/utils/elog.h                      |   1 +
 src/include/utils/pgstat_internal.h           |   1 -
 .../vacuum-extending-in-repetable-read.out    |  53 +++++
 src/test/isolation/isolation_schedule         |   1 +
 .../vacuum-extending-in-repetable-read.spec   |  53 +++++
 src/test/regress/expected/rules.out           |  44 +++-
 .../expected/vacuum_tables_statistics.out     | 225 ++++++++++++++++++
 src/test/regress/parallel_schedule            |   5 +
 .../regress/sql/vacuum_tables_statistics.sql  | 180 ++++++++++++++
 22 files changed, 1091 insertions(+), 18 deletions(-)
 create mode 100644 src/test/isolation/expected/vacuum-extending-in-repetable-read.out
 create mode 100644 src/test/isolation/specs/vacuum-extending-in-repetable-read.spec
 create mode 100644 src/test/regress/expected/vacuum_tables_statistics.out
 create mode 100644 src/test/regress/sql/vacuum_tables_statistics.sql

diff --git a/src/backend/access/heap/vacuumlazy.c b/src/backend/access/heap/vacuumlazy.c
index 09fab08b8e1..03179f8ecaa 100644
--- a/src/backend/access/heap/vacuumlazy.c
+++ b/src/backend/access/heap/vacuumlazy.c
@@ -165,6 +165,7 @@ typedef struct LVRelState
 	/* Error reporting state */
 	char	   *dbname;
 	char	   *relnamespace;
+	Oid			reloid;
 	char	   *relname;
 	char	   *indname;		/* Current index name */
 	BlockNumber blkno;			/* used only for heap operations */
@@ -229,6 +230,8 @@ typedef struct LVRelState
 	BlockNumber next_unskippable_block; /* next unskippable block */
 	bool		next_unskippable_allvis;	/* its visibility status */
 	Buffer		next_unskippable_vmbuffer;	/* buffer containing its VM bit */
+
+	int32		wraparound_failsafe_count; /* the number of times to prevent workaround problem */
 } LVRelState;
 
 /* Struct for saving and restoring vacuum error information. */
@@ -239,6 +242,18 @@ typedef struct LVSavedErrInfo
 	VacErrPhase phase;
 } LVSavedErrInfo;
 
+/*
+ * Counters and usage data for extended stats tracking.
+ */
+typedef struct LVExtStatCounters
+{
+	TimestampTz starttime;
+	WalUsage	walusage;
+	BufferUsage bufusage;
+	double		VacuumDelayTime;
+	PgStat_Counter blocks_fetched;
+	PgStat_Counter blocks_hit;
+} LVExtStatCounters;
 
 /* non-export function prototypes */
 static void lazy_scan_heap(LVRelState *vacrel);
@@ -292,6 +307,106 @@ static void update_vacuum_error_info(LVRelState *vacrel,
 static void restore_vacuum_error_info(LVRelState *vacrel,
 									  const LVSavedErrInfo *saved_vacrel);
 
+/* ----------
+ * extvac_stats_start() -
+ *
+ * Save cut-off values of extended vacuum counters before start of a relation
+ * processing.
+ * ----------
+ */
+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;
+
+	if (!rel->pgstat_info || !pgstat_track_counts)
+		/*
+		 * if something goes wrong or user doesn't want to track a database
+		 * activity - just suppress it.
+		 */
+		return;
+
+	counters->blocks_fetched = rel->pgstat_info->counts.blocks_fetched;
+	counters->blocks_hit = rel->pgstat_info->counts.blocks_hit;
+}
+
+/* ----------
+ * extvac_stats_end() -
+ *
+ *	Called to finish an extended vacuum statistic gathering and form a report.
+ * ----------
+ */
+static void
+extvac_stats_end(Relation rel, LVExtStatCounters *counters,
+				  ExtVacReport *report)
+{
+	WalUsage	walusage;
+	BufferUsage	bufusage;
+	TimestampTz endtime;
+	long		secs;
+	int			usecs;
+
+	if(!pgstat_track_vacuum_statistics)
+		return;
+
+	/* 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);
+
+	endtime = GetCurrentTimestamp();
+	TimestampDifference(counters->starttime, endtime, &secs, &usecs);
+
+	memset(report, 0, sizeof(ExtVacReport));
+
+	/*
+	 * Fill additional statistics on a vacuum processing operation.
+	 */
+	report->total_blks_read = bufusage.local_blks_read + bufusage.shared_blks_read;
+	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)
+		/*
+		 * if something goes wrong or an user doesn't want to track a database
+		 * activity - just suppress it.
+		 */
+		return;
+
+	report->blks_fetched =
+		rel->pgstat_info->counts.blocks_fetched - counters->blocks_fetched;
+	report->blks_hit =
+		rel->pgstat_info->counts.blocks_hit - counters->blocks_hit;
+}
 
 /*
  *	heap_vacuum_rel() -- perform VACUUM for one heap relation
@@ -324,7 +439,14 @@ heap_vacuum_rel(Relation rel, VacuumParams *params,
 	WalUsage	startwalusage = pgWalUsage;
 	BufferUsage startbufferusage = pgBufferUsage;
 	ErrorContextCallback errcallback;
+	LVExtStatCounters extVacCounters;
+	ExtVacReport extVacReport;
 	char	  **indnames = NULL;
+	ExtVacReport allzero;
+
+	/* Initialize vacuum statistics */
+	memset(&allzero, 0, sizeof(ExtVacReport));
+	extVacReport = allzero;
 
 	verbose = (params->options & VACOPT_VERBOSE) != 0;
 	instrument = (verbose || (AmAutoVacuumWorkerProcess() &&
@@ -342,7 +464,7 @@ heap_vacuum_rel(Relation rel, VacuumParams *params,
 
 	pgstat_progress_start_command(PROGRESS_COMMAND_VACUUM,
 								  RelationGetRelid(rel));
-
+	extvac_stats_start(rel, &extVacCounters);
 	/*
 	 * Setup error traceback support for ereport() first.  The idea is to set
 	 * up an error context callback to display additional information on any
@@ -359,6 +481,7 @@ heap_vacuum_rel(Relation rel, VacuumParams *params,
 	vacrel->dbname = get_database_name(MyDatabaseId);
 	vacrel->relnamespace = get_namespace_name(RelationGetNamespace(rel));
 	vacrel->relname = pstrdup(RelationGetRelationName(rel));
+	vacrel->reloid = RelationGetRelid(rel);
 	vacrel->indname = NULL;
 	vacrel->phase = VACUUM_ERRCB_PHASE_UNKNOWN;
 	vacrel->verbose = verbose;
@@ -446,6 +569,7 @@ heap_vacuum_rel(Relation rel, VacuumParams *params,
 	vacrel->vm_new_visible_pages = 0;
 	vacrel->vm_new_visible_frozen_pages = 0;
 	vacrel->vm_new_frozen_pages = 0;
+	vacrel->wraparound_failsafe_count = 0;
 
 	/*
 	 * Get cutoffs that determine which deleted tuples are considered DEAD,
@@ -591,6 +715,26 @@ heap_vacuum_rel(Relation rel, VacuumParams *params,
 						vacrel->NewRelfrozenXid, vacrel->NewRelminMxid,
 						&frozenxid_updated, &minmulti_updated, false);
 
+	/* Make generic extended vacuum stats report */
+	extvac_stats_end(rel, &extVacCounters, &extVacReport);
+
+	if(pgstat_track_vacuum_statistics)
+	{
+		/* Fill heap-specific extended stats fields */
+		extVacReport.pages_scanned = vacrel->scanned_pages;
+		extVacReport.pages_removed = vacrel->removed_pages;
+		extVacReport.vm_new_frozen_pages = vacrel->vm_new_frozen_pages;
+		extVacReport.vm_new_visible_pages = vacrel->vm_new_visible_pages;
+		extVacReport.vm_new_visible_frozen_pages = vacrel->vm_new_visible_frozen_pages;
+		extVacReport.tuples_deleted = vacrel->tuples_deleted;
+		extVacReport.tuples_frozen = vacrel->tuples_frozen;
+		extVacReport.recently_dead_tuples = vacrel->recently_dead_tuples;
+		extVacReport.missed_dead_tuples = vacrel->missed_dead_tuples;
+		extVacReport.missed_dead_pages = vacrel->missed_dead_pages;
+		extVacReport.index_vacuum_count = vacrel->num_index_scans;
+		extVacReport.wraparound_failsafe_count = vacrel->wraparound_failsafe_count;
+	}
+
 	/*
 	 * Report results to the cumulative stats system, too.
 	 *
@@ -605,7 +749,8 @@ heap_vacuum_rel(Relation rel, VacuumParams *params,
 						 rel->rd_rel->relisshared,
 						 Max(vacrel->new_live_tuples, 0),
 						 vacrel->recently_dead_tuples +
-						 vacrel->missed_dead_tuples);
+						 vacrel->missed_dead_tuples,
+						 &extVacReport);
 	pgstat_progress_end_command();
 
 	if (instrument)
@@ -2418,6 +2563,7 @@ lazy_check_wraparound_failsafe(LVRelState *vacrel)
 		int64		progress_val[2] = {0, 0};
 
 		VacuumFailsafeActive = true;
+		vacrel->wraparound_failsafe_count ++;
 
 		/*
 		 * Abandon use of a buffer access strategy to allow use of all of
diff --git a/src/backend/access/heap/visibilitymap.c b/src/backend/access/heap/visibilitymap.c
index 745a04ef26e..07623a045fa 100644
--- a/src/backend/access/heap/visibilitymap.c
+++ b/src/backend/access/heap/visibilitymap.c
@@ -91,6 +91,7 @@
 #include "access/xloginsert.h"
 #include "access/xlogutils.h"
 #include "miscadmin.h"
+#include "pgstat.h"
 #include "port/pg_bitutils.h"
 #include "storage/bufmgr.h"
 #include "storage/smgr.h"
@@ -160,6 +161,15 @@ visibilitymap_clear(Relation rel, BlockNumber heapBlk, Buffer vmbuf, uint8 flags
 
 	if (map[mapByte] & mask)
 	{
+		/*
+		 * As part of vacuum stats, track how often all-visible or all-frozen
+		 * bits are cleared.
+		 */
+		if (map[mapByte] >> mapOffset & flags & VISIBILITYMAP_ALL_VISIBLE)
+			pgstat_count_vm_rev_all_visible(rel);
+		if (map[mapByte] >> mapOffset & flags & VISIBILITYMAP_ALL_FROZEN)
+			pgstat_count_vm_rev_all_frozen(rel);
+
 		map[mapByte] &= ~mask;
 
 		MarkBufferDirty(vmbuf);
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index 7a595c84db9..43ac27ed5b4 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -691,7 +691,9 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
-            pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count
+            pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_rev_all_frozen_pages(C.oid) AS rev_all_frozen_pages,
+            pg_stat_get_rev_all_visible_pages(C.oid) AS rev_all_visible_pages
     FROM pg_class C LEFT JOIN
          pg_index I ON C.oid = I.indrelid
          LEFT JOIN pg_namespace N ON (N.oid = C.relnamespace)
@@ -1381,3 +1383,51 @@ CREATE VIEW pg_stat_subscription_stats AS
 
 CREATE VIEW pg_wait_events AS
     SELECT * FROM pg_get_wait_events();
+--
+-- Show extended cumulative statistics on a vacuum operation over all tables and
+-- databases of the instance.
+-- Use Invalid Oid "0" as an input relation id to get stat on each table in a
+-- database.
+--
+
+CREATE VIEW pg_stat_vacuum_tables AS
+SELECT
+  ns.nspname AS schemaname,
+  rel.relname AS relname,
+  stats.relid as relid,
+
+  stats.total_blks_read AS total_blks_read,
+  stats.total_blks_hit AS total_blks_hit,
+  stats.total_blks_dirtied AS total_blks_dirtied,
+  stats.total_blks_written AS total_blks_written,
+
+  stats.rel_blks_read AS rel_blks_read,
+  stats.rel_blks_hit AS rel_blks_hit,
+
+  stats.pages_scanned AS pages_scanned,
+  stats.pages_removed AS pages_removed,
+  stats.vm_new_frozen_pages AS vm_new_frozen_pages,
+  stats.vm_new_visible_pages AS vm_new_visible_pages,
+  stats.vm_new_visible_frozen_pages AS vm_new_visible_frozen_pages,
+  stats.missed_dead_pages AS missed_dead_pages,
+  stats.tuples_deleted AS tuples_deleted,
+  stats.tuples_frozen AS tuples_frozen,
+  stats.recently_dead_tuples AS recently_dead_tuples,
+  stats.missed_dead_tuples AS missed_dead_tuples,
+
+  stats.wraparound_failsafe AS wraparound_failsafe,
+  stats.index_vacuum_count AS index_vacuum_count,
+  stats.wal_records AS wal_records,
+  stats.wal_fpi AS wal_fpi,
+  stats.wal_bytes AS wal_bytes,
+
+  stats.blk_read_time AS blk_read_time,
+  stats.blk_write_time AS blk_write_time,
+
+  stats.delay_time AS delay_time,
+  stats.total_time AS total_time
+
+FROM pg_class rel
+  JOIN pg_namespace ns ON ns.oid = rel.relnamespace,
+  LATERAL pg_stat_get_vacuum_tables(rel.oid) stats
+WHERE rel.relkind = 'r';
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index e6745e6145c..1ea277b2c16 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -102,6 +102,9 @@ pg_atomic_uint32 *VacuumSharedCostBalance = NULL;
 pg_atomic_uint32 *VacuumActiveNWorkers = NULL;
 int			VacuumCostBalanceLocal = 0;
 
+/* Cumulative storage to report total vacuum delay time. */
+double VacuumDelayTime = 0; /* msec. */
+
 /* non-export function prototypes */
 static List *expand_vacuum_rel(VacuumRelation *vrel,
 							   MemoryContext vac_context, int options);
@@ -2418,6 +2421,7 @@ vacuum_delay_point(void)
 			exit(1);
 
 		VacuumCostBalance = 0;
+		VacuumDelayTime += msec;
 
 		/*
 		 * Balance and update limit values for autovacuum workers. We must do
diff --git a/src/backend/commands/vacuumparallel.c b/src/backend/commands/vacuumparallel.c
index 0d92e694d6a..672f8f4bfe8 100644
--- a/src/backend/commands/vacuumparallel.c
+++ b/src/backend/commands/vacuumparallel.c
@@ -1047,6 +1047,7 @@ parallel_vacuum_main(dsm_segment *seg, shm_toc *toc)
 	/* Set cost-based vacuum delay */
 	VacuumUpdateCosts();
 	VacuumCostBalance = 0;
+	VacuumDelayTime = 0;
 	VacuumCostBalanceLocal = 0;
 	VacuumSharedCostBalance = &(shared->cost_balance);
 	VacuumActiveNWorkers = &(shared->active_nworkers);
diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index 34520535d54..d21b9302c29 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -191,7 +191,7 @@ static void pgstat_reset_after_failure(void);
 static bool pgstat_flush_pending_entries(bool nowait);
 
 static void pgstat_prep_snapshot(void);
-static void pgstat_build_snapshot(void);
+static void pgstat_build_snapshot(PgStat_Kind statKind);
 static void pgstat_build_snapshot_fixed(PgStat_Kind kind);
 
 static inline bool pgstat_is_kind_valid(PgStat_Kind kind);
@@ -204,7 +204,7 @@ static inline bool pgstat_is_kind_valid(PgStat_Kind kind);
 
 bool		pgstat_track_counts = false;
 int			pgstat_fetch_consistency = PGSTAT_FETCH_CONSISTENCY_CACHE;
-
+bool		pgstat_track_vacuum_statistics = true;
 
 /* ----------
  * state shared with pgstat_*.c
@@ -261,7 +261,6 @@ static bool pgstat_is_initialized = false;
 static bool pgstat_is_shutdown = false;
 #endif
 
-
 /*
  * The different kinds of built-in statistics.
  *
@@ -911,7 +910,6 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 		pgstat_reset_entries_of_kind(kind, ts);
 }
 
-
 /* ------------------------------------------------------------
  * Fetching of stats
  * ------------------------------------------------------------
@@ -980,7 +978,7 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
 
 	/* if we need to build a full snapshot, do so */
 	if (pgstat_fetch_consistency == PGSTAT_FETCH_CONSISTENCY_SNAPSHOT)
-		pgstat_build_snapshot();
+		pgstat_build_snapshot(PGSTAT_KIND_INVALID);
 
 	/* if caching is desired, look up in cache */
 	if (pgstat_fetch_consistency > PGSTAT_FETCH_CONSISTENCY_NONE)
@@ -1096,7 +1094,7 @@ pgstat_snapshot_fixed(PgStat_Kind kind)
 		pgstat_clear_snapshot();
 
 	if (pgstat_fetch_consistency == PGSTAT_FETCH_CONSISTENCY_SNAPSHOT)
-		pgstat_build_snapshot();
+		pgstat_build_snapshot(PGSTAT_KIND_INVALID);
 	else
 		pgstat_build_snapshot_fixed(kind);
 
@@ -1147,7 +1145,7 @@ pgstat_prep_snapshot(void)
 }
 
 static void
-pgstat_build_snapshot(void)
+pgstat_build_snapshot(PgStat_Kind statKind)
 {
 	dshash_seq_status hstat;
 	PgStatShared_HashEntry *p;
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index 09247ba0971..458bd4ece49 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -47,6 +47,8 @@ static void add_tabstat_xact_level(PgStat_TableStatus *pgstat_info, int nest_lev
 static void ensure_tabstat_xact_level(PgStat_TableStatus *pgstat_info);
 static void save_truncdrop_counters(PgStat_TableXactStatus *trans, bool is_drop);
 static void restore_truncdrop_counters(PgStat_TableXactStatus *trans);
+static void pgstat_accumulate_extvac_stats(ExtVacReport *dst, ExtVacReport *src,
+							   bool accumulate_reltype_specific_info);

 /*
@@ -208,7 +210,8 @@ pgstat_drop_relation(Relation rel)
  */
 void
 pgstat_report_vacuum(Oid tableoid, bool shared,
-					 PgStat_Counter livetuples, PgStat_Counter deadtuples)
+					 PgStat_Counter livetuples, PgStat_Counter deadtuples,
+					 ExtVacReport *params)
 {
 	PgStat_EntryRef *entry_ref;
 	PgStatShared_Relation *shtabentry;
@@ -232,6 +235,8 @@ pgstat_report_vacuum(Oid tableoid, bool shared,
 	tabentry->live_tuples = livetuples;
 	tabentry->dead_tuples = deadtuples;
 
+	pgstat_accumulate_extvac_stats(&tabentry->vacuum_ext, params, true);
+
 	/*
 	 * It is quite possible that a non-aggressive VACUUM ended up skipping
 	 * various pages, however, we'll zero the insert counter here regardless.
@@ -859,6 +864,9 @@ pgstat_relation_flush_cb(PgStat_EntryRef *entry_ref, bool nowait)
 	tabentry->blocks_fetched += lstats->counts.blocks_fetched;
 	tabentry->blocks_hit += lstats->counts.blocks_hit;
 
+	tabentry->rev_all_frozen_pages += lstats->counts.rev_all_frozen_pages;
+	tabentry->rev_all_visible_pages += lstats->counts.rev_all_visible_pages;
+
 	/* Clamp live_tuples in case of negative delta_live_tuples */
 	tabentry->live_tuples = Max(tabentry->live_tuples, 0);
 	/* Likewise for dead_tuples */
@@ -982,3 +990,40 @@ restore_truncdrop_counters(PgStat_TableXactStatus *trans)
 		trans->tuples_deleted = trans->deleted_pre_truncdrop;
 	}
 }
+
+static void
+pgstat_accumulate_extvac_stats(ExtVacReport *dst, ExtVacReport *src,
+							   bool accumulate_reltype_specific_info)
+{
+	dst->total_blks_read += src->total_blks_read;
+	dst->total_blks_hit += src->total_blks_hit;
+	dst->total_blks_dirtied += src->total_blks_dirtied;
+	dst->total_blks_written += src->total_blks_written;
+	dst->wal_bytes += src->wal_bytes;
+	dst->wal_fpi += src->wal_fpi;
+	dst->wal_records += src->wal_records;
+	dst->blk_read_time += src->blk_read_time;
+	dst->blk_write_time += src->blk_write_time;
+	dst->delay_time += src->delay_time;
+	dst->total_time += src->total_time;
+
+	if (!accumulate_reltype_specific_info)
+		return;
+
+	dst->blks_fetched += src->blks_fetched;
+	dst->blks_hit += src->blks_hit;
+
+	dst->pages_scanned += src->pages_scanned;
+	dst->pages_removed += src->pages_removed;
+	dst->vm_new_frozen_pages += src->vm_new_frozen_pages;
+	dst->vm_new_visible_pages += src->vm_new_visible_pages;
+	dst->vm_new_visible_frozen_pages += src->vm_new_visible_frozen_pages;
+	dst->tuples_deleted += src->tuples_deleted;
+	dst->tuples_frozen += src->tuples_frozen;
+	dst->recently_dead_tuples += src->recently_dead_tuples;
+	dst->index_vacuum_count += src->index_vacuum_count;
+	dst->wraparound_failsafe_count += src->wraparound_failsafe_count;
+	dst->missed_dead_pages += src->missed_dead_pages;
+	dst->missed_dead_tuples += src->missed_dead_tuples;
+
+}
\ No newline at end of file
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 5f8d20a406d..da31d9d3e29 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -106,6 +106,12 @@ PG_STAT_GET_RELENTRY_INT64(tuples_updated)
 /* pg_stat_get_vacuum_count */
 PG_STAT_GET_RELENTRY_INT64(vacuum_count)
 
+/* pg_stat_get_rev_frozen_pages */
+PG_STAT_GET_RELENTRY_INT64(rev_all_frozen_pages)
+
+/* pg_stat_get_rev_all_visible_pages */
+PG_STAT_GET_RELENTRY_INT64(rev_all_visible_pages)
+
 #define PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat)					\
 Datum															\
 CppConcat(pg_stat_get_,stat)(PG_FUNCTION_ARGS)					\
@@ -2172,3 +2178,144 @@ pg_stat_have_stats(PG_FUNCTION_ARGS)
 
 	PG_RETURN_BOOL(pgstat_have_entry(kind, dboid, objid));
 }
+
+
+/*
+ * Get the vacuum statistics for the heap tables.
+ */
+Datum
+pg_stat_get_vacuum_tables(PG_FUNCTION_ARGS)
+{
+	#define PG_STAT_GET_VACUUM_TABLES_STATS_COLS 26
+
+	Oid						relid = PG_GETARG_OID(0);
+	PgStat_StatTabEntry     *tabentry;
+	ExtVacReport 			*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;
+	ExtVacReport allzero;
+
+	/* Initialise attributes information in the tuple descriptor */
+	tupdesc = CreateTemplateTupleDesc(PG_STAT_GET_VACUUM_TABLES_STATS_COLS);
+
+	TupleDescInitEntry(tupdesc, (AttrNumber) ++i, "relid",
+					   INT4OID, -1, 0);
+	TupleDescInitEntry(tupdesc, (AttrNumber) ++i, "total_blks_read",
+					   INT8OID, -1, 0);
+	TupleDescInitEntry(tupdesc, (AttrNumber) ++i, "total_blks_hit",
+					   INT8OID, -1, 0);
+	TupleDescInitEntry(tupdesc, (AttrNumber) ++i, "total_blks_dirtied",
+					   INT8OID, -1, 0);
+	TupleDescInitEntry(tupdesc, (AttrNumber) ++i, "total_blks_written",
+					   INT8OID, -1, 0);
+	TupleDescInitEntry(tupdesc, (AttrNumber) ++i, "rel_blks_read",
+					   INT8OID, -1, 0);
+	TupleDescInitEntry(tupdesc, (AttrNumber) ++i, "rel_blks_hit",
+					   INT8OID, -1, 0);
+	TupleDescInitEntry(tupdesc, (AttrNumber) ++i, "pages_scanned",
+					   INT8OID, -1, 0);
+	TupleDescInitEntry(tupdesc, (AttrNumber) ++i, "pages_removed",
+					   INT8OID, -1, 0);
+	TupleDescInitEntry(tupdesc, (AttrNumber) ++i, "vm_new_frozen_pages",
+					   INT8OID, -1, 0);
+	TupleDescInitEntry(tupdesc, (AttrNumber) ++i, "vm_new_visible_pages",
+					   INT8OID, -1, 0);
+	TupleDescInitEntry(tupdesc, (AttrNumber) ++i, "vm_new_visible_frozen_pages",
+					   INT8OID, -1, 0);
+	TupleDescInitEntry(tupdesc, (AttrNumber) ++i, "missed_dead_pages",
+					   INT8OID, -1, 0);
+	TupleDescInitEntry(tupdesc, (AttrNumber) ++i, "tuples_deleted",
+					   INT8OID, -1, 0);
+	TupleDescInitEntry(tupdesc, (AttrNumber) ++i, "tuples_frozen",
+					   INT8OID, -1, 0);
+	TupleDescInitEntry(tupdesc, (AttrNumber) ++i, "recently_dead_tuples",
+					   INT8OID, -1, 0);
+	TupleDescInitEntry(tupdesc, (AttrNumber) ++i, "missed_dead_tuples",
+					   INT8OID, -1, 0);
+	TupleDescInitEntry(tupdesc, (AttrNumber) ++i, "wraparound_failsafe_count",
+					   INT4OID, -1, 0);
+	TupleDescInitEntry(tupdesc, (AttrNumber) ++i, "index_vacuum_count",
+					   INT8OID, -1, 0);
+	TupleDescInitEntry(tupdesc, (AttrNumber) ++i, "wal_records",
+					   INT8OID, -1, 0);
+	TupleDescInitEntry(tupdesc, (AttrNumber) ++i, "wal_fpi",
+					   INT8OID, -1, 0);
+	TupleDescInitEntry(tupdesc, (AttrNumber) ++i, "wal_bytes",
+					   NUMERICOID, -1, 0);
+
+	TupleDescInitEntry(tupdesc, (AttrNumber) ++i, "blk_read_time",
+					   FLOAT8OID, -1, 0);
+	TupleDescInitEntry(tupdesc, (AttrNumber) ++i, "blk_write_time",
+					   FLOAT8OID, -1, 0);
+
+	TupleDescInitEntry(tupdesc, (AttrNumber) ++i, "delay_time",
+					   FLOAT8OID, -1, 0);
+	TupleDescInitEntry(tupdesc, (AttrNumber) ++i, "total_time",
+					   FLOAT8OID, -1, 0);
+
+	Assert(i == PG_STAT_GET_VACUUM_TABLES_STATS_COLS);
+
+	BlessTupleDesc(tupdesc);
+
+	tabentry = pgstat_fetch_stat_tabentry(relid);
+
+	if (tabentry == NULL)
+	{
+		/* If the subscription is not found, initialise its stats */
+		memset(&allzero, 0, sizeof(ExtVacReport));
+		extvacuum = &allzero;
+	}
+	else
+	{
+		extvacuum = &(tabentry->vacuum_ext);
+	}
+
+	i = 0;
+
+	values[i++] = ObjectIdGetDatum(relid);
+
+	values[i++] = Int64GetDatum(extvacuum->total_blks_read);
+	values[i++] = Int64GetDatum(extvacuum->total_blks_hit);
+	values[i++] = Int64GetDatum(extvacuum->total_blks_dirtied);
+	values[i++] = Int64GetDatum(extvacuum->total_blks_written);
+
+	values[i++] = Int64GetDatum(extvacuum->blks_fetched -
+									extvacuum->blks_hit);
+	values[i++] = Int64GetDatum(extvacuum->blks_hit);
+
+	values[i++] = Int64GetDatum(extvacuum->pages_scanned);
+	values[i++] = Int64GetDatum(extvacuum->pages_removed);
+	values[i++] = Int64GetDatum(extvacuum->vm_new_frozen_pages);
+	values[i++] = Int64GetDatum(extvacuum->vm_new_visible_pages);
+	values[i++] = Int64GetDatum(extvacuum->vm_new_visible_frozen_pages);
+	values[i++] = Int64GetDatum(extvacuum->missed_dead_pages);
+	values[i++] = Int64GetDatum(extvacuum->tuples_deleted);
+	values[i++] = Int64GetDatum(extvacuum->tuples_frozen);
+	values[i++] = Int64GetDatum(extvacuum->recently_dead_tuples);
+	values[i++] = Int64GetDatum(extvacuum->missed_dead_tuples);
+	values[i++] = Int32GetDatum(extvacuum->wraparound_failsafe_count);
+	values[i++] = Int64GetDatum(extvacuum->index_vacuum_count);
+
+	values[i++] = Int64GetDatum(extvacuum->wal_records);
+	values[i++] = Int64GetDatum(extvacuum->wal_fpi);
+
+	/* Convert to numeric, like pg_stat_statements */
+	snprintf(buf, sizeof buf, UINT64_FORMAT, extvacuum->wal_bytes);
+	values[i++] = DirectFunctionCall3(numeric_in,
+									  CStringGetDatum(buf),
+									  ObjectIdGetDatum(0),
+									  Int32GetDatum(-1));
+
+	values[i++] = Float8GetDatum(extvacuum->blk_read_time);
+	values[i++] = Float8GetDatum(extvacuum->blk_write_time);
+	values[i++] = Float8GetDatum(extvacuum->delay_time);
+	values[i++] = Float8GetDatum(extvacuum->total_time);
+
+	Assert(i == PG_STAT_GET_VACUUM_TABLES_STATS_COLS);
+
+	/* Returns the record as Datum */
+	PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
\ No newline at end of file
diff --git a/src/backend/utils/error/elog.c b/src/backend/utils/error/elog.c
index 860bbd40d42..4da8d3f87fd 100644
--- a/src/backend/utils/error/elog.c
+++ b/src/backend/utils/error/elog.c
@@ -1619,6 +1619,19 @@ getinternalerrposition(void)
 	return edata->internalpos;
 }
 
+/*
+ * Return elevel of errors
+ */
+int
+geterrelevel(void)
+{
+	ErrorData  *edata = &errordata[errordata_stack_depth];
+
+	/* we don't bother incrementing recursion_depth */
+	CHECK_STACK_DEPTH();
+
+	return edata->elevel;
+}
 
 /*
  * Functions to allow construction of error message strings separately from
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index c9d8cd796a8..f2d31e174b4 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -1478,6 +1478,15 @@ struct config_bool ConfigureNamesBool[] =
 		false,
 		NULL, NULL, NULL
 	},
+	{
+		{"track_vacuum_statistics", PGC_SUSET, STATS_CUMULATIVE,
+			gettext_noop("Collects vacuum statistics for table relations."),
+			NULL
+		},
+		&pgstat_track_vacuum_statistics,
+		true,
+		NULL, NULL, NULL
+	},
 	{
 		{"track_wal_io_timing", PGC_SUSET, STATS_CUMULATIVE,
 			gettext_noop("Collects timing statistics for WAL I/O activity."),
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index b37e8a6f882..0d9584a9fec 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12429,4 +12429,22 @@
   proargtypes => 'int2',
   prosrc => 'gist_stratnum_identity' },
 
+{ oid => '8001',
+  descr => 'pg_stat_get_vacuum_tables returns vacuum stats values for table',
+  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,int8,numeric,float8,float8,float8,float8}',
+  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,o}',
+  proargnames => '{reloid,relid,total_blks_read,total_blks_hit,total_blks_dirtied,total_blks_written,rel_blks_read,rel_blks_hit,pages_scanned,pages_removed,vm_new_frozen_pages,vm_new_visible_pages,vm_new_visible_frozen_pages,missed_dead_pages,tuples_deleted,tuples_frozen,recently_dead_tuples,missed_dead_tuples,wraparound_failsafe,index_vacuum_count,wal_records,wal_fpi,wal_bytes,blk_read_time,blk_write_time,delay_time,total_time}',
+  prosrc => 'pg_stat_get_vacuum_tables' },
+
+  { oid => '8002', descr => 'statistics: number of times the all-visible pages in the visibility map was removed for pages of table',
+  proname => 'pg_stat_get_rev_all_visible_pages', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_rev_all_visible_pages' },
+  { oid => '8003', descr => 'statistics: number of times the all-frozen pages in the visibility map was removed for pages of table',
+  proname => 'pg_stat_get_rev_all_frozen_pages', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_rev_all_frozen_pages' },
 ]
diff --git a/src/include/commands/vacuum.h b/src/include/commands/vacuum.h
index 12d0b61950d..94d599767df 100644
--- a/src/include/commands/vacuum.h
+++ b/src/include/commands/vacuum.h
@@ -308,6 +308,7 @@ extern PGDLLIMPORT int vacuum_multixact_failsafe_age;
 extern PGDLLIMPORT pg_atomic_uint32 *VacuumSharedCostBalance;
 extern PGDLLIMPORT pg_atomic_uint32 *VacuumActiveNWorkers;
 extern PGDLLIMPORT int VacuumCostBalanceLocal;
+extern PGDLLIMPORT double VacuumDelayTime;
 
 extern PGDLLIMPORT bool VacuumFailsafeActive;
 extern PGDLLIMPORT double vacuum_cost_delay;
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 6475889c58c..0266993dbf2 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -167,6 +167,53 @@ typedef struct PgStat_BackendSubEntry
 	PgStat_Counter conflict_count[CONFLICT_NUM_TYPES];
 } PgStat_BackendSubEntry;
 
+/* ----------
+ *
+ * ExtVacReport
+ *
+ * Additional statistics of vacuum processing over a heap relation.
+ * pages_removed is the amount by which the physically shrank,
+ * if any (ie the change in its total size on disk)
+ * pages_deleted refer to free space within the index file
+ * ----------
+ */
+typedef struct ExtVacReport
+{
+	/* number of blocks missed, hit, dirtied and written during a vacuum of specific relation */
+	int64		total_blks_read;
+	int64		total_blks_hit;
+	int64		total_blks_dirtied;
+	int64		total_blks_written;
+
+	/* blocks missed and hit for just the heap during a vacuum of specific relation */
+	int64		blks_fetched;
+	int64		blks_hit;
+
+	/* Vacuum WAL usage stats */
+	int64		wal_records;	/* wal usage: number of WAL records */
+	int64		wal_fpi;		/* wal usage: number of WAL full page images produced */
+	uint64		wal_bytes;		/* wal usage: size of WAL records produced */
+
+	/* Time stats. */
+	double		blk_read_time;	/* time spent reading pages, in msec */
+	double		blk_write_time; /* time spent writing pages, in msec */
+	double		delay_time;		/* how long vacuum slept in vacuum delay point, in msec */
+	double		total_time;		/* total time of a vacuum operation, in msec */
+
+	int64		pages_scanned;		/* heap pages examined (not skipped by VM) */
+	int64		pages_removed;		/* heap pages removed by vacuum "truncation" */
+	int64		vm_new_frozen_pages;		/* pages marked in VM as frozen */
+	int64		vm_new_visible_pages;	/* pages marked in VM as all-visible */
+	int64		vm_new_visible_frozen_pages;	/* pages marked in VM as all-visible and frozen */
+	int64		missed_dead_tuples;		/* tuples not pruned by vacuum due to failure to get a cleanup lock */
+	int64		missed_dead_pages;		/* pages with missed dead tuples */
+	int64		tuples_deleted;		/* tuples deleted by vacuum */
+	int64		tuples_frozen;		/* tuples frozen up by vacuum */
+	int64		recently_dead_tuples;	/* deleted tuples that are still visible to some transaction */
+	int64		index_vacuum_count;	/* the number of index vacuumings */
+	int32		wraparound_failsafe_count;	/* the number of times to prevent workaround problem */
+} ExtVacReport;
+
 /* ----------
  * PgStat_TableCounts			The actual per-table counts kept by a backend
  *
@@ -209,6 +256,16 @@ typedef struct PgStat_TableCounts
 
 	PgStat_Counter blocks_fetched;
 	PgStat_Counter blocks_hit;
+
+	PgStat_Counter rev_all_visible_pages;
+	PgStat_Counter rev_all_frozen_pages;
+
+	/*
+	 * Additional cumulative stat on vacuum operations.
+	 * Use an expensive structure as an abstraction for different types of
+	 * relations.
+	 */
+	ExtVacReport	vacuum_ext;
 } PgStat_TableCounts;
 
 /* ----------
@@ -267,7 +324,7 @@ typedef struct PgStat_TableXactStatus
  * ------------------------------------------------------------
  */
 
-#define PGSTAT_FILE_FORMAT_ID	0x01A5BCB0
+#define PGSTAT_FILE_FORMAT_ID	0x01A5BCB1
 
 typedef struct PgStat_ArchiverStats
 {
@@ -429,6 +486,8 @@ typedef struct PgStat_StatDBEntry
 	PgStat_Counter parallel_workers_launched;
 
 	TimestampTz stat_reset_timestamp;
+
+	ExtVacReport vacuum_ext;		/* extended vacuum statistics */
 } PgStat_StatDBEntry;
 
 typedef struct PgStat_StatFuncEntry
@@ -502,6 +561,11 @@ typedef struct PgStat_StatTabEntry
 	PgStat_Counter analyze_count;
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
+
+	PgStat_Counter rev_all_visible_pages;
+	PgStat_Counter rev_all_frozen_pages;
+
+	ExtVacReport vacuum_ext;
 } PgStat_StatTabEntry;
 
 typedef struct PgStat_WalStats
@@ -676,7 +740,8 @@ extern void pgstat_assoc_relation(Relation rel);
 extern void pgstat_unlink_relation(Relation rel);
 
 extern void pgstat_report_vacuum(Oid tableoid, bool shared,
-								 PgStat_Counter livetuples, PgStat_Counter deadtuples);
+								 PgStat_Counter livetuples, PgStat_Counter deadtuples,
+								 ExtVacReport *params);
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter);
@@ -727,6 +792,17 @@ extern void pgstat_report_analyze(Relation rel,
 		if (pgstat_should_count_relation(rel))						\
 			(rel)->pgstat_info->counts.blocks_hit++;				\
 	} while (0)
+/* accumulate unfrozen all-visible and all-frozen pages */
+#define pgstat_count_vm_rev_all_visible(rel)						\
+	do {															\
+		if (pgstat_should_count_relation(rel))						\
+			(rel)->pgstat_info->counts.rev_all_visible_pages++;	\
+	} while (0)
+#define pgstat_count_vm_rev_all_frozen(rel)						\
+	do {															\
+		if (pgstat_should_count_relation(rel))						\
+			(rel)->pgstat_info->counts.rev_all_frozen_pages++;	\
+	} while (0)
 
 extern void pgstat_count_heap_insert(Relation rel, PgStat_Counter n);
 extern void pgstat_count_heap_update(Relation rel, bool hot, bool newpage);
@@ -744,7 +820,6 @@ extern PgStat_StatTabEntry *pgstat_fetch_stat_tabentry_ext(bool shared,
 														   Oid reloid);
 extern PgStat_TableStatus *find_tabstat_entry(Oid rel_id);
 
-
 /*
  * Functions in pgstat_replslot.c
  */
@@ -815,6 +890,7 @@ extern PgStat_WalStats *pgstat_fetch_stat_wal(void);
 extern PGDLLIMPORT bool pgstat_track_counts;
 extern PGDLLIMPORT int pgstat_track_functions;
 extern PGDLLIMPORT int pgstat_fetch_consistency;
+extern PGDLLIMPORT bool pgstat_track_vacuum_statistics;

 /*
diff --git a/src/include/utils/elog.h b/src/include/utils/elog.h
index 7161f5c6ad6..9e080747a92 100644
--- a/src/include/utils/elog.h
+++ b/src/include/utils/elog.h
@@ -230,6 +230,7 @@ extern int	geterrlevel(void);
 extern int	geterrposition(void);
 extern int	getinternalerrposition(void);
 
+extern int	geterrelevel(void);
 
 /*----------
  * Old-style error reporting API: to be used in this way:
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index 4bb8e5c53ab..4f836e7fca0 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -600,7 +600,6 @@ extern PgStat_EntryRef *pgstat_fetch_pending_entry(PgStat_Kind kind,
 extern void *pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid);
 extern void pgstat_snapshot_fixed(PgStat_Kind kind);
 
-
 /*
  * Functions in pgstat_archiver.c
  */
diff --git a/src/test/isolation/expected/vacuum-extending-in-repetable-read.out b/src/test/isolation/expected/vacuum-extending-in-repetable-read.out
new file mode 100644
index 00000000000..87f7e40b4a6
--- /dev/null
+++ b/src/test/isolation/expected/vacuum-extending-in-repetable-read.out
@@ -0,0 +1,53 @@
+unused step name: s2_delete
+Parsed test spec with 2 sessions
+
+starting permutation: s2_insert s2_print_vacuum_stats_table s1_begin_repeatable_read s2_update s2_insert_interrupt s2_vacuum s2_print_vacuum_stats_table s1_commit s2_checkpoint s2_vacuum s2_print_vacuum_stats_table
+step s2_insert: INSERT INTO test_vacuum_stat_isolation(id, ival) SELECT ival, ival%10 FROM generate_series(1,1000) As ival;
+step s2_print_vacuum_stats_table: 
+    SELECT
+    vt.relname, vt.tuples_deleted, vt.recently_dead_tuples, vt.missed_dead_tuples, vt.missed_dead_pages, vt.tuples_frozen
+    FROM pg_stat_vacuum_tables vt, pg_class c
+    WHERE vt.relname = 'test_vacuum_stat_isolation' AND vt.relid = c.oid;
+
+relname                   |tuples_deleted|recently_dead_tuples|missed_dead_tuples|missed_dead_pages|tuples_frozen
+--------------------------+--------------+--------------------+------------------+-----------------+-------------
+test_vacuum_stat_isolation|             0|                   0|                 0|                0|            0
+(1 row)
+
+step s1_begin_repeatable_read: 
+  BEGIN transaction ISOLATION LEVEL REPEATABLE READ;
+  select count(ival) from test_vacuum_stat_isolation where id>900;
+
+count
+-----
+  100
+(1 row)
+
+step s2_update: UPDATE test_vacuum_stat_isolation SET ival = ival + 2 where id > 900;
+step s2_insert_interrupt: INSERT INTO test_vacuum_stat_isolation values (1,1);
+step s2_vacuum: VACUUM test_vacuum_stat_isolation;
+step s2_print_vacuum_stats_table: 
+    SELECT
+    vt.relname, vt.tuples_deleted, vt.recently_dead_tuples, vt.missed_dead_tuples, vt.missed_dead_pages, vt.tuples_frozen
+    FROM pg_stat_vacuum_tables vt, pg_class c
+    WHERE vt.relname = 'test_vacuum_stat_isolation' AND vt.relid = c.oid;
+
+relname                   |tuples_deleted|recently_dead_tuples|missed_dead_tuples|missed_dead_pages|tuples_frozen
+--------------------------+--------------+--------------------+------------------+-----------------+-------------
+test_vacuum_stat_isolation|             0|                 100|                 0|                0|            0
+(1 row)
+
+step s1_commit: COMMIT;
+step s2_checkpoint: CHECKPOINT;
+step s2_vacuum: VACUUM test_vacuum_stat_isolation;
+step s2_print_vacuum_stats_table: 
+    SELECT
+    vt.relname, vt.tuples_deleted, vt.recently_dead_tuples, vt.missed_dead_tuples, vt.missed_dead_pages, vt.tuples_frozen
+    FROM pg_stat_vacuum_tables vt, pg_class c
+    WHERE vt.relname = 'test_vacuum_stat_isolation' AND vt.relid = c.oid;
+
+relname                   |tuples_deleted|recently_dead_tuples|missed_dead_tuples|missed_dead_pages|tuples_frozen
+--------------------------+--------------+--------------------+------------------+-----------------+-------------
+test_vacuum_stat_isolation|           100|                 100|                 0|                0|          101
+(1 row)
+
diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule
index 143109aa4da..e93dd4f626c 100644
--- a/src/test/isolation/isolation_schedule
+++ b/src/test/isolation/isolation_schedule
@@ -95,6 +95,7 @@ test: timeouts
 test: vacuum-concurrent-drop
 test: vacuum-conflict
 test: vacuum-skip-locked
+test: vacuum-extending-in-repetable-read
 test: stats
 test: horizons
 test: predicate-hash
diff --git a/src/test/isolation/specs/vacuum-extending-in-repetable-read.spec b/src/test/isolation/specs/vacuum-extending-in-repetable-read.spec
new file mode 100644
index 00000000000..5893d89573d
--- /dev/null
+++ b/src/test/isolation/specs/vacuum-extending-in-repetable-read.spec
@@ -0,0 +1,53 @@
+# Test for checking recently_dead_tuples, tuples_deleted and frozen tuples in pg_stat_vacuum_tables.
+# recently_dead_tuples values are counted when vacuum hasn't cleared tuples because they were deleted recently.
+# recently_dead_tuples aren't increased after releasing lock compared with tuples_deleted, which increased
+# by the value of the cleared tuples that the vacuum managed to clear.
+
+setup
+{
+    CREATE TABLE test_vacuum_stat_isolation(id int, ival int) WITH (autovacuum_enabled = off);
+    SET track_io_timing = on;
+    SET track_vacuum_statistics TO 'on';
+}
+
+teardown
+{
+    DROP TABLE test_vacuum_stat_isolation CASCADE;
+    RESET track_io_timing;
+    RESET track_vacuum_statistics;
+}
+
+session s1
+step s1_begin_repeatable_read   {
+  BEGIN transaction ISOLATION LEVEL REPEATABLE READ;
+  select count(ival) from test_vacuum_stat_isolation where id>900;
+  }
+step s1_commit                  { COMMIT; }
+
+session s2
+step s2_insert                  { INSERT INTO test_vacuum_stat_isolation(id, ival) SELECT ival, ival%10 FROM generate_series(1,1000) As ival; }
+step s2_update                  { UPDATE test_vacuum_stat_isolation SET ival = ival + 2 where id > 900; }
+step s2_delete                  { DELETE FROM test_vacuum_stat_isolation where id > 900; }
+step s2_insert_interrupt        { INSERT INTO test_vacuum_stat_isolation values (1,1); }
+step s2_vacuum                  { VACUUM test_vacuum_stat_isolation; }
+step s2_checkpoint              { CHECKPOINT; }
+step s2_print_vacuum_stats_table
+{
+    SELECT
+    vt.relname, vt.tuples_deleted, vt.recently_dead_tuples, vt.missed_dead_tuples, vt.missed_dead_pages, vt.tuples_frozen
+    FROM pg_stat_vacuum_tables vt, pg_class c
+    WHERE vt.relname = 'test_vacuum_stat_isolation' AND vt.relid = c.oid;
+}
+
+permutation
+    s2_insert
+    s2_print_vacuum_stats_table
+    s1_begin_repeatable_read
+    s2_update
+    s2_insert_interrupt
+    s2_vacuum
+    s2_print_vacuum_stats_table
+    s1_commit
+    s2_checkpoint
+    s2_vacuum
+    s2_print_vacuum_stats_table
\ No newline at end of file
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 3014d047fef..b49f7936f93 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1804,7 +1804,9 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
-    pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count
+    pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_rev_all_frozen_pages(c.oid) AS rev_all_frozen_pages,
+    pg_stat_get_rev_all_visible_pages(c.oid) AS rev_all_visible_pages
    FROM ((pg_class c
      LEFT JOIN pg_index i ON ((c.oid = i.indrelid)))
      LEFT JOIN pg_namespace n ON ((n.oid = c.relnamespace)))
@@ -2188,7 +2190,9 @@ pg_stat_sys_tables| SELECT relid,
     vacuum_count,
     autovacuum_count,
     analyze_count,
-    autoanalyze_count
+    autoanalyze_count,
+    rev_all_frozen_pages,
+    rev_all_visible_pages
    FROM pg_stat_all_tables
   WHERE ((schemaname = ANY (ARRAY['pg_catalog'::name, 'information_schema'::name])) OR (schemaname ~ '^pg_toast'::text));
 pg_stat_user_functions| SELECT p.oid AS funcid,
@@ -2236,9 +2240,43 @@ pg_stat_user_tables| SELECT relid,
     vacuum_count,
     autovacuum_count,
     analyze_count,
-    autoanalyze_count
+    autoanalyze_count,
+    rev_all_frozen_pages,
+    rev_all_visible_pages
    FROM pg_stat_all_tables
   WHERE ((schemaname <> ALL (ARRAY['pg_catalog'::name, 'information_schema'::name])) AND (schemaname !~ '^pg_toast'::text));
+pg_stat_vacuum_tables| SELECT ns.nspname AS schemaname,
+    rel.relname,
+    stats.relid,
+    stats.total_blks_read,
+    stats.total_blks_hit,
+    stats.total_blks_dirtied,
+    stats.total_blks_written,
+    stats.rel_blks_read,
+    stats.rel_blks_hit,
+    stats.pages_scanned,
+    stats.pages_removed,
+    stats.vm_new_frozen_pages,
+    stats.vm_new_visible_pages,
+    stats.vm_new_visible_frozen_pages,
+    stats.missed_dead_pages,
+    stats.tuples_deleted,
+    stats.tuples_frozen,
+    stats.recently_dead_tuples,
+    stats.missed_dead_tuples,
+    stats.wraparound_failsafe,
+    stats.index_vacuum_count,
+    stats.wal_records,
+    stats.wal_fpi,
+    stats.wal_bytes,
+    stats.blk_read_time,
+    stats.blk_write_time,
+    stats.delay_time,
+    stats.total_time
+   FROM (pg_class rel
+     JOIN pg_namespace ns ON ((ns.oid = rel.relnamespace))),
+    LATERAL pg_stat_get_vacuum_tables(rel.oid) stats(relid, total_blks_read, total_blks_hit, total_blks_dirtied, total_blks_written, rel_blks_read, rel_blks_hit, pages_scanned, pages_removed, vm_new_frozen_pages, vm_new_visible_pages, vm_new_visible_frozen_pages, missed_dead_pages, tuples_deleted, tuples_frozen, recently_dead_tuples, missed_dead_tuples, wraparound_failsafe, index_vacuum_count, wal_records, wal_fpi, wal_bytes, blk_read_time, blk_write_time, delay_time, total_time)
+  WHERE (rel.relkind = 'r'::"char");
 pg_stat_wal| SELECT wal_records,
     wal_fpi,
     wal_bytes,
diff --git a/src/test/regress/expected/vacuum_tables_statistics.out b/src/test/regress/expected/vacuum_tables_statistics.out
new file mode 100644
index 00000000000..0c05a812dd1
--- /dev/null
+++ b/src/test/regress/expected/vacuum_tables_statistics.out
@@ -0,0 +1,225 @@
+--
+-- Test cumulative vacuum stats system
+--
+-- Check the wall statistics collected during vacuum operation:
+-- number of frozen and visible pages set by vacuum;
+-- number of frozen and visible pages removed by backend.
+-- Statistic wal_fpi is not displayed in this test because its behavior is unstable.
+--
+-- conditio sine qua non
+SHOW track_counts;  -- must be on
+ track_counts 
+--------------
+ on
+(1 row)
+
+\set sample_size 10000
+-- not enabled by default, but we want to test it...
+SET track_functions TO 'all';
+-- Test that vacuum statistics will be empty when parameter is off.
+SET track_vacuum_statistics TO 'off';
+CREATE TABLE vestat (x int) WITH (autovacuum_enabled = off, fillfactor = 10);
+INSERT INTO vestat SELECT x FROM generate_series(1,:sample_size) as x;
+ANALYZE vestat;
+DELETE FROM vestat WHERE x % 2 = 0;
+VACUUM (PARALLEL 0, BUFFER_USAGE_LIMIT 128) vestat;
+-- Must be empty.
+SELECT relname,total_blks_read, total_blks_hit, total_blks_dirtied, total_blks_written,rel_blks_read, rel_blks_hit,
+pages_scanned, pages_removed, vm_new_frozen_pages, vm_new_visible_pages, vm_new_visible_frozen_pages, missed_dead_pages,
+tuples_deleted, tuples_frozen, recently_dead_tuples, missed_dead_tuples, index_vacuum_count,
+wal_records, wal_fpi, wal_bytes, blk_read_time, blk_write_time,delay_time, total_time
+FROM pg_stat_vacuum_tables vt
+WHERE vt.relname = 'vestat';
+ relname | total_blks_read | total_blks_hit | total_blks_dirtied | total_blks_written | rel_blks_read | rel_blks_hit | pages_scanned | pages_removed | vm_new_frozen_pages | vm_new_visible_pages | vm_new_visible_frozen_pages | missed_dead_pages | tuples_deleted | tuples_frozen | recently_dead_tuples | missed_dead_tuples | index_vacuum_count | wal_records | wal_fpi | wal_bytes | blk_read_time | blk_write_time | delay_time | total_time 
+---------+-----------------+----------------+--------------------+--------------------+---------------+--------------+---------------+---------------+---------------------+----------------------+-----------------------------+-------------------+----------------+---------------+----------------------+--------------------+--------------------+-------------+---------+-----------+---------------+----------------+------------+------------
+ vestat  |               0 |              0 |                  0 |                  0 |             0 |            0 |             0 |             0 |                   0 |                    0 |                           0 |                 0 |              0 |             0 |                    0 |                  0 |                  0 |           0 |       0 |         0 |             0 |              0 |          0 |          0
+(1 row)
+
+RESET track_vacuum_statistics;
+DROP TABLE vestat CASCADE;
+SHOW track_vacuum_statistics;  -- must be on
+ track_vacuum_statistics 
+-------------------------
+ on
+(1 row)
+
+-- ensure pending stats are flushed
+SELECT pg_stat_force_next_flush();
+ pg_stat_force_next_flush 
+--------------------------
+ 
+(1 row)
+
+--SET stats_fetch_consistency = snapshot;
+CREATE TABLE vestat (x int) WITH (autovacuum_enabled = off, fillfactor = 10);
+INSERT INTO vestat SELECT x FROM generate_series(1,:sample_size) as x;
+ANALYZE vestat;
+SELECT oid AS roid from pg_class where relname = 'vestat' \gset
+DELETE FROM vestat WHERE x % 2 = 0;
+-- Before the first vacuum execution extended stats view is empty.
+SELECT vt.relname,vm_new_frozen_pages,tuples_deleted,relpages,pages_scanned,pages_removed
+FROM pg_stat_vacuum_tables vt, pg_class c
+WHERE vt.relname = 'vestat' AND vt.relid = c.oid;
+ relname | vm_new_frozen_pages | tuples_deleted | relpages | pages_scanned | pages_removed 
+---------+---------------------+----------------+----------+---------------+---------------
+ vestat  |                   0 |              0 |      455 |             0 |             0
+(1 row)
+
+SELECT relpages AS rp
+FROM pg_class c
+WHERE relname = 'vestat' \gset
+VACUUM (PARALLEL 0, BUFFER_USAGE_LIMIT 128, INDEX_CLEANUP OFF) vestat;
+-- it is necessary to check the wal statistics
+CHECKPOINT;
+-- The table and index extended vacuum statistics should show us that
+-- vacuum frozed pages and clean up pages, but pages_removed stayed the same
+-- because of not full table have cleaned up
+SELECT vt.relname,vm_new_frozen_pages > 0 AS vm_new_frozen_pages,tuples_deleted > 0 AS tuples_deleted,relpages-:rp = 0 AS relpages,pages_scanned > 0 AS pages_scanned,pages_removed = 0 AS pages_removed
+FROM pg_stat_vacuum_tables vt, pg_class c
+WHERE vt.relname = 'vestat' AND vt.relid = c.oid;
+ relname | vm_new_frozen_pages | tuples_deleted | relpages | pages_scanned | pages_removed 
+---------+---------------------+----------------+----------+---------------+---------------
+ vestat  | f                   | t              | t        | t             | t
+(1 row)
+
+SELECT vm_new_frozen_pages AS fp,tuples_deleted AS td,relpages AS rp, pages_scanned AS ps, pages_removed AS pr
+FROM pg_stat_vacuum_tables vt, pg_class c
+WHERE vt.relname = 'vestat' AND vt.relid = c.oid \gset
+-- Store WAL advances into variables
+SELECT wal_records AS hwr,wal_bytes AS hwb,wal_fpi AS hfpi FROM pg_stat_vacuum_tables WHERE relname = 'vestat' \gset
+-- Look into WAL records deltas.
+SELECT wal_records > 0 AS dWR, wal_bytes > 0 AS dWB
+FROM pg_stat_vacuum_tables WHERE relname = 'vestat';
+ dwr | dwb 
+-----+-----
+ t   | t
+(1 row)
+
+DELETE FROM vestat;;
+VACUUM (PARALLEL 0, BUFFER_USAGE_LIMIT 128, INDEX_CLEANUP OFF) vestat;
+-- it is necessary to check the wal statistics
+CHECKPOINT;
+-- pages_removed must be increased
+SELECT vt.relname,vm_new_frozen_pages-:fp > 0 AS vm_new_frozen_pages,tuples_deleted-:td > 0 AS tuples_deleted,relpages -:rp = 0 AS relpages,pages_scanned-:ps > 0 AS pages_scanned,pages_removed-:pr > 0 AS pages_removed
+FROM pg_stat_vacuum_tables vt, pg_class c
+WHERE vt.relname = 'vestat' AND vt.relid = c.oid;
+ relname | vm_new_frozen_pages | tuples_deleted | relpages | pages_scanned | pages_removed 
+---------+---------------------+----------------+----------+---------------+---------------
+ vestat  | f                   | t              | f        | t             | t
+(1 row)
+
+SELECT vm_new_frozen_pages AS fp,tuples_deleted AS td,relpages AS rp, pages_scanned AS ps, pages_removed AS pr
+FROM pg_stat_vacuum_tables vt, pg_class c
+WHERE vt.relname = 'vestat' AND vt.relid = c.oid \gset
+-- Store WAL advances into variables
+SELECT wal_records-:hwr AS dwr, wal_bytes-:hwb AS dwb, wal_fpi-:hfpi AS dfpi
+FROM pg_stat_vacuum_tables WHERE relname = 'vestat' \gset
+-- WAL advance should be detected.
+SELECT :dwr > 0 AS dWR, :dwb > 0 AS dWB;
+ dwr | dwb 
+-----+-----
+ t   | t
+(1 row)
+
+-- Store WAL advances into variables
+SELECT wal_records AS hwr,wal_bytes AS hwb,wal_fpi AS hfpi FROM pg_stat_vacuum_tables WHERE relname = 'vestat' \gset
+INSERT INTO vestat SELECT x FROM generate_series(1,:sample_size) as x;
+DELETE FROM vestat WHERE x % 2 = 0;
+-- VACUUM FULL doesn't report to stat collector. So, no any advancements of statistics
+-- are detected here.
+VACUUM FULL vestat;
+-- It is necessary to check the wal statistics
+CHECKPOINT;
+-- Store WAL advances into variables
+SELECT wal_records-:hwr AS dwr2, wal_bytes-:hwb AS dwb2, wal_fpi-:hfpi AS dfpi2
+FROM pg_stat_vacuum_tables WHERE relname = 'vestat' \gset
+-- WAL and other statistics advance should not be detected.
+SELECT :dwr2=0 AS dWR, :dfpi2=0 AS dFPI, :dwb2=0 AS dWB;
+ dwr | dfpi | dwb 
+-----+------+-----
+ t   | t    | t
+(1 row)
+
+SELECT vt.relname,vm_new_frozen_pages-:fp = 0 AS vm_new_frozen_pages,tuples_deleted-:td = 0 AS tuples_deleted,relpages -:rp < 0 AS relpages,pages_scanned-:ps = 0 AS pages_scanned,pages_removed-:pr = 0 AS pages_removed
+FROM pg_stat_vacuum_tables vt, pg_class c
+WHERE vt.relname = 'vestat' AND vt.relid = c.oid;
+ relname | vm_new_frozen_pages | tuples_deleted | relpages | pages_scanned | pages_removed 
+---------+---------------------+----------------+----------+---------------+---------------
+ vestat  | t                   | t              | f        | t             | t
+(1 row)
+
+SELECT vm_new_frozen_pages AS fp,tuples_deleted AS td,relpages AS rp, pages_scanned AS ps,pages_removed AS pr
+FROM pg_stat_vacuum_tables vt, pg_class c
+WHERE vt.relname = 'vestat' AND vt.relid = c.oid \gset
+-- Store WAL advances into variables
+SELECT wal_records AS hwr,wal_bytes AS hwb,wal_fpi AS hfpi FROM pg_stat_vacuum_tables WHERE relname = 'vestat' \gset
+DELETE FROM vestat;
+TRUNCATE vestat;
+VACUUM (PARALLEL 0, BUFFER_USAGE_LIMIT 128, INDEX_CLEANUP OFF) vestat;
+-- it is necessary to check the wal statistics
+CHECKPOINT;
+-- Store WAL advances into variables after removing all tuples from the table
+SELECT wal_records-:hwr AS dwr3, wal_bytes-:hwb AS dwb3, wal_fpi-:hfpi AS dfpi3
+FROM pg_stat_vacuum_tables WHERE relname = 'vestat' \gset
+--There are nothing changed
+SELECT :dwr3>0 AS dWR, :dfpi3=0 AS dFPI, :dwb3>0 AS dWB;
+ dwr | dfpi | dwb 
+-----+------+-----
+ t   | t    | t
+(1 row)
+
+--
+-- Now, the table and index is compressed into zero number of pages. Check it
+-- in vacuum extended statistics.
+-- The vm_new_frozen_pages, pages_scanned values shouldn't be changed
+--
+SELECT vt.relname,vm_new_frozen_pages-:fp = 0 AS vm_new_frozen_pages,tuples_deleted-:td = 0 AS tuples_deleted,relpages -:rp = 0 AS relpages,pages_scanned-:ps = 0 AS pages_scanned,pages_removed-:pr = 0 AS pages_removed
+FROM pg_stat_vacuum_tables vt, pg_class c
+WHERE vt.relname = 'vestat' AND vt.relid = c.oid;
+ relname | vm_new_frozen_pages | tuples_deleted | relpages | pages_scanned | pages_removed 
+---------+---------------------+----------------+----------+---------------+---------------
+ vestat  | t                   | t              | f        | t             | t
+(1 row)
+
+INSERT INTO vestat SELECT x FROM generate_series(1,:sample_size) as x;
+ANALYZE vestat;
+-- must be empty
+SELECT vm_new_frozen_pages, vm_new_visible_pages, rev_all_frozen_pages,rev_all_visible_pages,vm_new_visible_frozen_pages
+FROM pg_stat_vacuum_tables, pg_stat_all_tables WHERE pg_stat_vacuum_tables.relname = 'vestat' and pg_stat_vacuum_tables.relid = pg_stat_all_tables.relid;
+ vm_new_frozen_pages | vm_new_visible_pages | rev_all_frozen_pages | rev_all_visible_pages | vm_new_visible_frozen_pages 
+---------------------+----------------------+----------------------+-----------------------+-----------------------------
+                   0 |                  910 |                    0 |                     0 |                         455
+(1 row)
+
+VACUUM (PARALLEL 0, BUFFER_USAGE_LIMIT 128) vestat;
+-- backend defreezed pages
+SELECT vm_new_frozen_pages > 0 AS vm_new_frozen_pages,vm_new_visible_pages > 0 AS vm_new_visible_pages,vm_new_visible_frozen_pages > 0 AS vm_new_visible_frozen_pages,rev_all_frozen_pages = 0 AS rev_all_frozen_pages,rev_all_visible_pages = 0 AS rev_all_visible_pages
+FROM pg_stat_vacuum_tables, pg_stat_all_tables WHERE pg_stat_vacuum_tables.relname = 'vestat' and pg_stat_vacuum_tables.relid = pg_stat_all_tables.relid;
+ vm_new_frozen_pages | vm_new_visible_pages | vm_new_visible_frozen_pages | rev_all_frozen_pages | rev_all_visible_pages 
+---------------------+----------------------+-----------------------------+----------------------+-----------------------
+ f                   | t                    | t                           | t                    | t
+(1 row)
+
+SELECT vm_new_frozen_pages AS pf, vm_new_visible_pages AS pv,vm_new_visible_frozen_pages AS pvf, rev_all_frozen_pages AS hafp,rev_all_visible_pages AS havp
+FROM pg_stat_vacuum_tables, pg_stat_all_tables WHERE pg_stat_vacuum_tables.relname = 'vestat' and pg_stat_vacuum_tables.relid = pg_stat_all_tables.relid \gset
+UPDATE vestat SET x = x + 1001;
+VACUUM (PARALLEL 0, BUFFER_USAGE_LIMIT 128) vestat;
+SELECT vm_new_frozen_pages > :pf AS vm_new_frozen_pages,vm_new_visible_pages > :pv AS vm_new_visible_pages,vm_new_visible_frozen_pages > :pvf AS vm_new_visible_frozen_pages,rev_all_frozen_pages > :hafp AS rev_all_frozen_pages,rev_all_visible_pages > :havp AS rev_all_visible_pages
+FROM pg_stat_vacuum_tables, pg_stat_all_tables WHERE pg_stat_vacuum_tables.relname = 'vestat' and pg_stat_vacuum_tables.relid = pg_stat_all_tables.relid;
+ vm_new_frozen_pages | vm_new_visible_pages | vm_new_visible_frozen_pages | rev_all_frozen_pages | rev_all_visible_pages 
+---------------------+----------------------+-----------------------------+----------------------+-----------------------
+ f                   | t                    | f                           | f                    | f
+(1 row)
+
+SELECT vm_new_frozen_pages AS pf, vm_new_visible_pages AS pv, vm_new_visible_frozen_pages AS pvf, rev_all_frozen_pages AS hafp,rev_all_visible_pages AS havp
+FROM pg_stat_vacuum_tables, pg_stat_all_tables WHERE pg_stat_vacuum_tables.relname = 'vestat' and pg_stat_vacuum_tables.relid = pg_stat_all_tables.relid \gset
+VACUUM (PARALLEL 0, BUFFER_USAGE_LIMIT 128) vestat;
+-- vacuum freezed pages
+SELECT vm_new_frozen_pages = :pf AS vm_new_frozen_pages,vm_new_visible_pages = :pv AS vm_new_visible_pages,vm_new_visible_frozen_pages = :pvf AS vm_new_visible_frozen_pages, rev_all_frozen_pages = :hafp AS rev_all_frozen_pages,rev_all_visible_pages = :havp AS rev_all_visible_pages
+FROM pg_stat_vacuum_tables, pg_stat_all_tables WHERE pg_stat_vacuum_tables.relname = 'vestat' and pg_stat_vacuum_tables.relid = pg_stat_all_tables.relid;
+ vm_new_frozen_pages | vm_new_visible_pages | vm_new_visible_frozen_pages | rev_all_frozen_pages | rev_all_visible_pages 
+---------------------+----------------------+-----------------------------+----------------------+-----------------------
+ t                   | t                    | t                           | t                    | t
+(1 row)
+
+DROP TABLE vestat CASCADE;
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 1edd9e45ebb..d11f6b7ef4b 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -136,3 +136,8 @@ test: fast_default
 # run tablespace test at the end because it drops the tablespace created during
 # setup that other tests may use.
 test: tablespace
+
+# ----------
+# Check vacuum statistics
+# ----------
+test: vacuum_tables_statistics
\ No newline at end of file
diff --git a/src/test/regress/sql/vacuum_tables_statistics.sql b/src/test/regress/sql/vacuum_tables_statistics.sql
new file mode 100644
index 00000000000..8ad69108ca1
--- /dev/null
+++ b/src/test/regress/sql/vacuum_tables_statistics.sql
@@ -0,0 +1,180 @@
+--
+-- Test cumulative vacuum stats system
+--
+-- Check the wall statistics collected during vacuum operation:
+-- number of frozen and visible pages set by vacuum;
+-- number of frozen and visible pages removed by backend.
+-- Statistic wal_fpi is not displayed in this test because its behavior is unstable.
+--
+
+-- conditio sine qua non
+SHOW track_counts;  -- must be on
+\set sample_size 10000
+
+-- not enabled by default, but we want to test it...
+SET track_functions TO 'all';
+
+-- Test that vacuum statistics will be empty when parameter is off.
+SET track_vacuum_statistics TO 'off';
+
+CREATE TABLE vestat (x int) WITH (autovacuum_enabled = off, fillfactor = 10);
+INSERT INTO vestat SELECT x FROM generate_series(1,:sample_size) as x;
+ANALYZE vestat;
+
+DELETE FROM vestat WHERE x % 2 = 0;
+
+VACUUM (PARALLEL 0, BUFFER_USAGE_LIMIT 128) vestat;
+
+-- Must be empty.
+SELECT relname,total_blks_read, total_blks_hit, total_blks_dirtied, total_blks_written,rel_blks_read, rel_blks_hit,
+pages_scanned, pages_removed, vm_new_frozen_pages, vm_new_visible_pages, vm_new_visible_frozen_pages, missed_dead_pages,
+tuples_deleted, tuples_frozen, recently_dead_tuples, missed_dead_tuples, index_vacuum_count,
+wal_records, wal_fpi, wal_bytes, blk_read_time, blk_write_time,delay_time, total_time
+FROM pg_stat_vacuum_tables vt
+WHERE vt.relname = 'vestat';
+
+RESET track_vacuum_statistics;
+DROP TABLE vestat CASCADE;
+
+SHOW track_vacuum_statistics;  -- must be on
+
+-- ensure pending stats are flushed
+SELECT pg_stat_force_next_flush();
+
+--SET stats_fetch_consistency = snapshot;
+CREATE TABLE vestat (x int) WITH (autovacuum_enabled = off, fillfactor = 10);
+INSERT INTO vestat SELECT x FROM generate_series(1,:sample_size) as x;
+ANALYZE vestat;
+
+SELECT oid AS roid from pg_class where relname = 'vestat' \gset
+
+DELETE FROM vestat WHERE x % 2 = 0;
+-- Before the first vacuum execution extended stats view is empty.
+SELECT vt.relname,vm_new_frozen_pages,tuples_deleted,relpages,pages_scanned,pages_removed
+FROM pg_stat_vacuum_tables vt, pg_class c
+WHERE vt.relname = 'vestat' AND vt.relid = c.oid;
+SELECT relpages AS rp
+FROM pg_class c
+WHERE relname = 'vestat' \gset
+
+VACUUM (PARALLEL 0, BUFFER_USAGE_LIMIT 128, INDEX_CLEANUP OFF) vestat;
+-- it is necessary to check the wal statistics
+CHECKPOINT;
+
+-- The table and index extended vacuum statistics should show us that
+-- vacuum frozed pages and clean up pages, but pages_removed stayed the same
+-- because of not full table have cleaned up
+SELECT vt.relname,vm_new_frozen_pages > 0 AS vm_new_frozen_pages,tuples_deleted > 0 AS tuples_deleted,relpages-:rp = 0 AS relpages,pages_scanned > 0 AS pages_scanned,pages_removed = 0 AS pages_removed
+FROM pg_stat_vacuum_tables vt, pg_class c
+WHERE vt.relname = 'vestat' AND vt.relid = c.oid;
+SELECT vm_new_frozen_pages AS fp,tuples_deleted AS td,relpages AS rp, pages_scanned AS ps, pages_removed AS pr
+FROM pg_stat_vacuum_tables vt, pg_class c
+WHERE vt.relname = 'vestat' AND vt.relid = c.oid \gset
+
+-- Store WAL advances into variables
+SELECT wal_records AS hwr,wal_bytes AS hwb,wal_fpi AS hfpi FROM pg_stat_vacuum_tables WHERE relname = 'vestat' \gset
+
+-- Look into WAL records deltas.
+SELECT wal_records > 0 AS dWR, wal_bytes > 0 AS dWB
+FROM pg_stat_vacuum_tables WHERE relname = 'vestat';
+
+DELETE FROM vestat;;
+VACUUM (PARALLEL 0, BUFFER_USAGE_LIMIT 128, INDEX_CLEANUP OFF) vestat;
+-- it is necessary to check the wal statistics
+CHECKPOINT;
+
+-- pages_removed must be increased
+SELECT vt.relname,vm_new_frozen_pages-:fp > 0 AS vm_new_frozen_pages,tuples_deleted-:td > 0 AS tuples_deleted,relpages -:rp = 0 AS relpages,pages_scanned-:ps > 0 AS pages_scanned,pages_removed-:pr > 0 AS pages_removed
+FROM pg_stat_vacuum_tables vt, pg_class c
+WHERE vt.relname = 'vestat' AND vt.relid = c.oid;
+SELECT vm_new_frozen_pages AS fp,tuples_deleted AS td,relpages AS rp, pages_scanned AS ps, pages_removed AS pr
+FROM pg_stat_vacuum_tables vt, pg_class c
+WHERE vt.relname = 'vestat' AND vt.relid = c.oid \gset
+
+-- Store WAL advances into variables
+SELECT wal_records-:hwr AS dwr, wal_bytes-:hwb AS dwb, wal_fpi-:hfpi AS dfpi
+FROM pg_stat_vacuum_tables WHERE relname = 'vestat' \gset
+
+-- WAL advance should be detected.
+SELECT :dwr > 0 AS dWR, :dwb > 0 AS dWB;
+
+-- Store WAL advances into variables
+SELECT wal_records AS hwr,wal_bytes AS hwb,wal_fpi AS hfpi FROM pg_stat_vacuum_tables WHERE relname = 'vestat' \gset
+
+INSERT INTO vestat SELECT x FROM generate_series(1,:sample_size) as x;
+DELETE FROM vestat WHERE x % 2 = 0;
+-- VACUUM FULL doesn't report to stat collector. So, no any advancements of statistics
+-- are detected here.
+VACUUM FULL vestat;
+-- It is necessary to check the wal statistics
+CHECKPOINT;
+
+-- Store WAL advances into variables
+SELECT wal_records-:hwr AS dwr2, wal_bytes-:hwb AS dwb2, wal_fpi-:hfpi AS dfpi2
+FROM pg_stat_vacuum_tables WHERE relname = 'vestat' \gset
+
+-- WAL and other statistics advance should not be detected.
+SELECT :dwr2=0 AS dWR, :dfpi2=0 AS dFPI, :dwb2=0 AS dWB;
+
+SELECT vt.relname,vm_new_frozen_pages-:fp = 0 AS vm_new_frozen_pages,tuples_deleted-:td = 0 AS tuples_deleted,relpages -:rp < 0 AS relpages,pages_scanned-:ps = 0 AS pages_scanned,pages_removed-:pr = 0 AS pages_removed
+FROM pg_stat_vacuum_tables vt, pg_class c
+WHERE vt.relname = 'vestat' AND vt.relid = c.oid;
+SELECT vm_new_frozen_pages AS fp,tuples_deleted AS td,relpages AS rp, pages_scanned AS ps,pages_removed AS pr
+FROM pg_stat_vacuum_tables vt, pg_class c
+WHERE vt.relname = 'vestat' AND vt.relid = c.oid \gset
+
+-- Store WAL advances into variables
+SELECT wal_records AS hwr,wal_bytes AS hwb,wal_fpi AS hfpi FROM pg_stat_vacuum_tables WHERE relname = 'vestat' \gset
+
+DELETE FROM vestat;
+TRUNCATE vestat;
+VACUUM (PARALLEL 0, BUFFER_USAGE_LIMIT 128, INDEX_CLEANUP OFF) vestat;
+-- it is necessary to check the wal statistics
+CHECKPOINT;
+
+-- Store WAL advances into variables after removing all tuples from the table
+SELECT wal_records-:hwr AS dwr3, wal_bytes-:hwb AS dwb3, wal_fpi-:hfpi AS dfpi3
+FROM pg_stat_vacuum_tables WHERE relname = 'vestat' \gset
+
+--There are nothing changed
+SELECT :dwr3>0 AS dWR, :dfpi3=0 AS dFPI, :dwb3>0 AS dWB;
+
+--
+-- Now, the table and index is compressed into zero number of pages. Check it
+-- in vacuum extended statistics.
+-- The vm_new_frozen_pages, pages_scanned values shouldn't be changed
+--
+SELECT vt.relname,vm_new_frozen_pages-:fp = 0 AS vm_new_frozen_pages,tuples_deleted-:td = 0 AS tuples_deleted,relpages -:rp = 0 AS relpages,pages_scanned-:ps = 0 AS pages_scanned,pages_removed-:pr = 0 AS pages_removed
+FROM pg_stat_vacuum_tables vt, pg_class c
+WHERE vt.relname = 'vestat' AND vt.relid = c.oid;
+
+INSERT INTO vestat SELECT x FROM generate_series(1,:sample_size) as x;
+ANALYZE vestat;
+
+-- must be empty
+SELECT vm_new_frozen_pages, vm_new_visible_pages, rev_all_frozen_pages,rev_all_visible_pages,vm_new_visible_frozen_pages
+FROM pg_stat_vacuum_tables, pg_stat_all_tables WHERE pg_stat_vacuum_tables.relname = 'vestat' and pg_stat_vacuum_tables.relid = pg_stat_all_tables.relid;
+
+VACUUM (PARALLEL 0, BUFFER_USAGE_LIMIT 128) vestat;
+
+-- backend defreezed pages
+SELECT vm_new_frozen_pages > 0 AS vm_new_frozen_pages,vm_new_visible_pages > 0 AS vm_new_visible_pages,vm_new_visible_frozen_pages > 0 AS vm_new_visible_frozen_pages,rev_all_frozen_pages = 0 AS rev_all_frozen_pages,rev_all_visible_pages = 0 AS rev_all_visible_pages
+FROM pg_stat_vacuum_tables, pg_stat_all_tables WHERE pg_stat_vacuum_tables.relname = 'vestat' and pg_stat_vacuum_tables.relid = pg_stat_all_tables.relid;
+SELECT vm_new_frozen_pages AS pf, vm_new_visible_pages AS pv,vm_new_visible_frozen_pages AS pvf, rev_all_frozen_pages AS hafp,rev_all_visible_pages AS havp
+FROM pg_stat_vacuum_tables, pg_stat_all_tables WHERE pg_stat_vacuum_tables.relname = 'vestat' and pg_stat_vacuum_tables.relid = pg_stat_all_tables.relid \gset
+
+UPDATE vestat SET x = x + 1001;
+VACUUM (PARALLEL 0, BUFFER_USAGE_LIMIT 128) vestat;
+
+SELECT vm_new_frozen_pages > :pf AS vm_new_frozen_pages,vm_new_visible_pages > :pv AS vm_new_visible_pages,vm_new_visible_frozen_pages > :pvf AS vm_new_visible_frozen_pages,rev_all_frozen_pages > :hafp AS rev_all_frozen_pages,rev_all_visible_pages > :havp AS rev_all_visible_pages
+FROM pg_stat_vacuum_tables, pg_stat_all_tables WHERE pg_stat_vacuum_tables.relname = 'vestat' and pg_stat_vacuum_tables.relid = pg_stat_all_tables.relid;
+SELECT vm_new_frozen_pages AS pf, vm_new_visible_pages AS pv, vm_new_visible_frozen_pages AS pvf, rev_all_frozen_pages AS hafp,rev_all_visible_pages AS havp
+FROM pg_stat_vacuum_tables, pg_stat_all_tables WHERE pg_stat_vacuum_tables.relname = 'vestat' and pg_stat_vacuum_tables.relid = pg_stat_all_tables.relid \gset
+
+VACUUM (PARALLEL 0, BUFFER_USAGE_LIMIT 128) vestat;
+
+-- vacuum freezed pages
+SELECT vm_new_frozen_pages = :pf AS vm_new_frozen_pages,vm_new_visible_pages = :pv AS vm_new_visible_pages,vm_new_visible_frozen_pages = :pvf AS vm_new_visible_frozen_pages, rev_all_frozen_pages = :hafp AS rev_all_frozen_pages,rev_all_visible_pages = :havp AS rev_all_visible_pages
+FROM pg_stat_vacuum_tables, pg_stat_all_tables WHERE pg_stat_vacuum_tables.relname = 'vestat' and pg_stat_vacuum_tables.relid = pg_stat_all_tables.relid;
+
+DROP TABLE vestat CASCADE;
-- 
2.34.1

From 01e0f1f6f32a2fe0b1589ab86acaa090354a09f9 Mon Sep 17 00:00:00 2001
From: Alena Rybakina <[email-masked]>
Date: Mon, 13 Jan 2025 10:32:07 +0300
Subject: [PATCH 2/4] Machinery for grabbing an extended vacuum statistics on
 index relations.

They are gathered separatelly from table statistics.

As for tables, we gather vacuum shared buffers statistics for index relations like
value of total_blks_hit, total_blks_read, total_blks_dirtied, wal statistics, io time
during flushing buffer pages to disk, delay and total time.

Due to the fact that such statistics are common as for tables, as for indexes we
set them in the union ExtVacReport structure. We only added some determination 'type'
field to highlight what kind belong to these statistics: PGSTAT_EXTVAC_TABLE or
PGSTAT_EXTVAC_INDEX. Generally, PGSTAT_EXTVAC_INVALID type leads to wrong code process.

Some statistics belong only one type of both tables or indexes. So, we added substructures
sych table and index inside ExtVacReport structure.

Therefore, we gather only for tables such statistics like number of scanned, removed pages,
their charecteristics according VM (all-visible and frozen). In addition, for tables we
gather number frozen, deleted and recently dead tuples and how many times vacuum processed
indexes for tables.

Controversally for indexes we gather number of deleted pages and deleted tuples only.

As for tables, deleted pages and deleted tuples reflect the overall performance of the vacuum
for the index relationship.

Since the vacuum cleans up references to tuple indexes before cleaning up table tuples,
which adds some complexity to the vacuum process, namely the vacuum switches from cleaning up
a table to its indexes and back during its operation, we need to save the vacuum statistics
collected for the heap before it starts cleaning up the indexes.
That's why it's necessary to track the vacuum statistics for the heap several times during
the vacuum procedure. To avoid sending the statistics to the Cumulative Statistics System
several times, we save these statistics in the LVRelState structure and only after vacuum
finishes cleaning up the heap, it sends them to the Cumulative Statistics System.

Authors: Alena Rybakina <[email-masked]>,
   Andrei Lepikhov <[email-masked]>,
   Andrei Zubkov <[email-masked]>
Reviewed-by: Dilip Kumar <[email-masked]>, Masahiko Sawada <[email-masked]>,
       Ilia Evdokimov <[email-masked]>, jian he <[email-masked]>,
       Kirill Reshke <[email-masked]>, Alexander Korotkov <[email-masked]>,
       Jim Nasby <[email-masked]>, Sami Imseih <[email-masked]>
---
 src/backend/access/heap/vacuumlazy.c          | 247 ++++++++++++++----
 src/backend/catalog/system_views.sql          |  32 +++
 src/backend/commands/vacuumparallel.c         |  14 +
 src/backend/utils/activity/pgstat.c           |   4 +
 src/backend/utils/activity/pgstat_relation.c  |  48 ++--
 src/backend/utils/adt/pgstatfuncs.c           | 133 +++++++++-
 src/backend/utils/misc/guc_tables.c           |   2 +-
 src/include/catalog/pg_proc.dat               |   9 +
 src/include/commands/vacuum.h                 |  25 ++
 src/include/pgstat.h                          |  58 +++-
 .../vacuum-extending-in-repetable-read.out    |   4 +-
 src/test/regress/expected/rules.out           |  22 ++
 .../expected/vacuum_index_statistics.out      | 183 +++++++++++++
 .../expected/vacuum_tables_statistics.out     |  40 ++-
 src/test/regress/parallel_schedule            |   1 +
 .../regress/sql/vacuum_index_statistics.sql   | 151 +++++++++++
 .../regress/sql/vacuum_tables_statistics.sql  |  12 +
 17 files changed, 884 insertions(+), 101 deletions(-)
 create mode 100644 src/test/regress/expected/vacuum_index_statistics.out
 create mode 100644 src/test/regress/sql/vacuum_index_statistics.sql

diff --git a/src/backend/access/heap/vacuumlazy.c b/src/backend/access/heap/vacuumlazy.c
index 03179f8ecaa..e7d2ccc668e 100644
--- a/src/backend/access/heap/vacuumlazy.c
+++ b/src/backend/access/heap/vacuumlazy.c
@@ -166,6 +166,7 @@ typedef struct LVRelState
 	char	   *dbname;
 	char	   *relnamespace;
 	Oid			reloid;
+	Oid			indoid;
 	char	   *relname;
 	char	   *indname;		/* Current index name */
 	BlockNumber blkno;			/* used only for heap operations */
@@ -232,6 +233,8 @@ typedef struct LVRelState
 	Buffer		next_unskippable_vmbuffer;	/* buffer containing its VM bit */
 
 	int32		wraparound_failsafe_count; /* the number of times to prevent workaround problem */
+
+	ExtVacReport extVacReport;
 } LVRelState;
 
 /* Struct for saving and restoring vacuum error information. */
@@ -242,19 +245,6 @@ typedef struct LVSavedErrInfo
 	VacErrPhase phase;
 } LVSavedErrInfo;
 
-/*
- * Counters and usage data for extended stats tracking.
- */
-typedef struct LVExtStatCounters
-{
-	TimestampTz starttime;
-	WalUsage	walusage;
-	BufferUsage bufusage;
-	double		VacuumDelayTime;
-	PgStat_Counter blocks_fetched;
-	PgStat_Counter blocks_hit;
-} LVExtStatCounters;
-
 /* non-export function prototypes */
 static void lazy_scan_heap(LVRelState *vacrel);
 static bool heap_vac_scan_next_block(LVRelState *vacrel, BlockNumber *blkno,
@@ -373,27 +363,25 @@ extvac_stats_end(Relation rel, LVExtStatCounters *counters,
 	endtime = GetCurrentTimestamp();
 	TimestampDifference(counters->starttime, endtime, &secs, &usecs);
 
-	memset(report, 0, sizeof(ExtVacReport));
-
 	/*
 	 * Fill additional statistics on a vacuum processing operation.
 	 */
-	report->total_blks_read = bufusage.local_blks_read + bufusage.shared_blks_read;
-	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->total_blks_read += bufusage.local_blks_read + bufusage.shared_blks_read;
+	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->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.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->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.;
+	report->total_time += secs * 1000. + usecs / 1000.;
 
 	if (!rel->pgstat_info || !pgstat_track_counts)
 		/*
@@ -402,12 +390,97 @@ extvac_stats_end(Relation rel, LVExtStatCounters *counters,
 		 */
 		return;
 
-	report->blks_fetched =
+	report->blks_fetched +=
 		rel->pgstat_info->counts.blocks_fetched - counters->blocks_fetched;
-	report->blks_hit =
+	report->blks_hit +=
 		rel->pgstat_info->counts.blocks_hit - counters->blocks_hit;
 }
 
+void
+extvac_stats_start_idx(Relation rel, IndexBulkDeleteResult *stats,
+					   LVExtStatCountersIdx *counters)
+{
+	if(!pgstat_track_vacuum_statistics)
+		return;
+
+	extvac_stats_start(rel, &counters->common);
+	counters->pages_deleted = counters->tuples_removed = 0;
+
+	if (stats != NULL)
+	{
+		/*
+		 * XXX: Why do we need this code here? If it is needed, I feel lack of
+		 * comments, describing the reason.
+		 */
+		counters->tuples_removed = stats->tuples_removed;
+		counters->pages_deleted = stats->pages_deleted;
+	}
+}
+
+void
+extvac_stats_end_idx(Relation rel, IndexBulkDeleteResult *stats,
+					 LVExtStatCountersIdx *counters, ExtVacReport *report)
+{
+	memset(report, 0, sizeof(ExtVacReport));
+
+	extvac_stats_end(rel, &counters->common, report);
+	report->type = PGSTAT_EXTVAC_INDEX;
+
+	if (stats != NULL)
+	{
+		/*
+		 * if something goes wrong or an user doesn't want to track a database
+		 * activity - just suppress it.
+		 */
+
+		/* Fill index-specific extended stats fields */
+		report->tuples_deleted =
+							stats->tuples_removed - counters->tuples_removed;
+		report->index.pages_deleted =
+							stats->pages_deleted - counters->pages_deleted;
+	}
+}
+
+/* Accumulate vacuum statistics for heap.
+ *
+  * Because of complexity of vacuum processing: it switch procesing between
+  * the heap relation to index relations and visa versa, we need to store
+  * gathered statistics information for heap relations several times before
+  * the vacuum starts processing the indexes again.
+  *
+  * It is necessary to gather correct statistics information for heap and indexes
+  * otherwice the index statistics information would be added to his parent heap
+  * statistics information and it would be difficult to analyze it later.
+  *
+  * We can't subtract union vacuum statistics information for index from the heap relations
+  * because of total and delay time time statistics collecting during parallel vacuum
+  * procudure.
+*/
+static void
+accumulate_heap_vacuum_statistics(LVExtStatCounters *extVacCounters, LVRelState *vacrel)
+{
+	if (!pgstat_track_vacuum_statistics)
+		return;
+
+	extvac_stats_end(vacrel->rel, extVacCounters, &(vacrel->extVacReport));
+
+	/* Fill heap-specific extended stats fields */
+	vacrel->extVacReport.type = PGSTAT_EXTVAC_TABLE;
+	vacrel->extVacReport.table.pages_scanned += vacrel->scanned_pages;
+	vacrel->extVacReport.table.pages_removed += vacrel->removed_pages;
+	vacrel->extVacReport.table.vm_new_frozen_pages += vacrel->vm_new_frozen_pages;
+	vacrel->extVacReport.table.vm_new_visible_pages += vacrel->vm_new_visible_pages;
+	vacrel->extVacReport.table.vm_new_visible_frozen_pages += vacrel->vm_new_visible_frozen_pages;
+	vacrel->extVacReport.tuples_deleted += vacrel->tuples_deleted;
+	vacrel->extVacReport.table.tuples_frozen += vacrel->tuples_frozen;
+	vacrel->extVacReport.table.recently_dead_tuples += vacrel->recently_dead_tuples;
+	vacrel->extVacReport.table.recently_dead_tuples += vacrel->recently_dead_tuples;
+	vacrel->extVacReport.table.missed_dead_tuples += vacrel->missed_dead_tuples;
+	vacrel->extVacReport.table.missed_dead_pages += vacrel->missed_dead_pages;
+	vacrel->extVacReport.table.index_vacuum_count += vacrel->num_index_scans;
+	vacrel->extVacReport.table.wraparound_failsafe_count += vacrel->wraparound_failsafe_count;
+}
+
 /*
  *	heap_vacuum_rel() -- perform VACUUM for one heap relation
  *
@@ -440,13 +513,7 @@ heap_vacuum_rel(Relation rel, VacuumParams *params,
 	BufferUsage startbufferusage = pgBufferUsage;
 	ErrorContextCallback errcallback;
 	LVExtStatCounters extVacCounters;
-	ExtVacReport extVacReport;
 	char	  **indnames = NULL;
-	ExtVacReport allzero;
-
-	/* Initialize vacuum statistics */
-	memset(&allzero, 0, sizeof(ExtVacReport));
-	extVacReport = allzero;
 
 	verbose = (params->options & VACOPT_VERBOSE) != 0;
 	instrument = (verbose || (AmAutoVacuumWorkerProcess() &&
@@ -464,7 +531,6 @@ heap_vacuum_rel(Relation rel, VacuumParams *params,
 
 	pgstat_progress_start_command(PROGRESS_COMMAND_VACUUM,
 								  RelationGetRelid(rel));
-	extvac_stats_start(rel, &extVacCounters);
 	/*
 	 * Setup error traceback support for ereport() first.  The idea is to set
 	 * up an error context callback to display additional information on any
@@ -637,6 +703,8 @@ heap_vacuum_rel(Relation rel, VacuumParams *params,
 	 */
 	lazy_scan_heap(vacrel);
 
+	extvac_stats_start(rel, &extVacCounters);
+
 	/*
 	 * Free resources managed by dead_items_alloc.  This ends parallel mode in
 	 * passing when necessary.
@@ -715,26 +783,6 @@ heap_vacuum_rel(Relation rel, VacuumParams *params,
 						vacrel->NewRelfrozenXid, vacrel->NewRelminMxid,
 						&frozenxid_updated, &minmulti_updated, false);
 
-	/* Make generic extended vacuum stats report */
-	extvac_stats_end(rel, &extVacCounters, &extVacReport);
-
-	if(pgstat_track_vacuum_statistics)
-	{
-		/* Fill heap-specific extended stats fields */
-		extVacReport.pages_scanned = vacrel->scanned_pages;
-		extVacReport.pages_removed = vacrel->removed_pages;
-		extVacReport.vm_new_frozen_pages = vacrel->vm_new_frozen_pages;
-		extVacReport.vm_new_visible_pages = vacrel->vm_new_visible_pages;
-		extVacReport.vm_new_visible_frozen_pages = vacrel->vm_new_visible_frozen_pages;
-		extVacReport.tuples_deleted = vacrel->tuples_deleted;
-		extVacReport.tuples_frozen = vacrel->tuples_frozen;
-		extVacReport.recently_dead_tuples = vacrel->recently_dead_tuples;
-		extVacReport.missed_dead_tuples = vacrel->missed_dead_tuples;
-		extVacReport.missed_dead_pages = vacrel->missed_dead_pages;
-		extVacReport.index_vacuum_count = vacrel->num_index_scans;
-		extVacReport.wraparound_failsafe_count = vacrel->wraparound_failsafe_count;
-	}
-
 	/*
 	 * Report results to the cumulative stats system, too.
 	 *
@@ -744,13 +792,34 @@ heap_vacuum_rel(Relation rel, VacuumParams *params,
 	 * It seems like a good idea to err on the side of not vacuuming again too
 	 * soon in cases where the failsafe prevented significant amounts of heap
 	 * vacuuming.
+	 *
+	 * We are ready to send vacuum statistics information for heap relations.
 	 */
-	pgstat_report_vacuum(RelationGetRelid(rel),
+	if(pgstat_track_vacuum_statistics)
+	{
+		/* Make generic extended vacuum stats report and
+		 * fill heap-specific extended stats fields.
+		 */
+		accumulate_heap_vacuum_statistics(&extVacCounters, vacrel);
+
+		pgstat_report_vacuum(RelationGetRelid(rel),
 						 rel->rd_rel->relisshared,
 						 Max(vacrel->new_live_tuples, 0),
 						 vacrel->recently_dead_tuples +
 						 vacrel->missed_dead_tuples,
-						 &extVacReport);
+						 &(vacrel->extVacReport));
+
+	}
+	else
+	{
+		pgstat_report_vacuum(RelationGetRelid(rel),
+							 rel->rd_rel->relisshared,
+							 Max(vacrel->new_live_tuples, 0),
+							 vacrel->recently_dead_tuples +
+							 vacrel->missed_dead_tuples,
+							 NULL);
+	}
+
 	pgstat_progress_end_command();
 
 	if (instrument)
@@ -1007,6 +1076,7 @@ lazy_scan_heap(LVRelState *vacrel)
 		PROGRESS_VACUUM_MAX_DEAD_TUPLE_BYTES
 	};
 	int64		initprog_val[3];
+	LVExtStatCounters extVacCounters;
 
 	/* Report that we're scanning the heap, advertising total # of blocks */
 	initprog_val[0] = PROGRESS_VACUUM_PHASE_SCAN_HEAP;
@@ -1020,6 +1090,11 @@ lazy_scan_heap(LVRelState *vacrel)
 	vacrel->next_unskippable_allvis = false;
 	vacrel->next_unskippable_vmbuffer = InvalidBuffer;
 
+	/* Set the current state of buffer, wal and time related statistics and
+	 * set other statistics to zero.
+	*/
+	extvac_stats_start(vacrel->rel, &extVacCounters);
+
 	while (heap_vac_scan_next_block(vacrel, &blkno, &all_visible_according_to_vm))
 	{
 		Buffer		buf;
@@ -1070,8 +1145,17 @@ lazy_scan_heap(LVRelState *vacrel)
 
 			/* Perform a round of index and heap vacuuming */
 			vacrel->consider_bypass_optimization = false;
+
+			/* Before starting to process the indexes save the current heap statistics */
+			accumulate_heap_vacuum_statistics(&extVacCounters, vacrel);
+
 			lazy_vacuum(vacrel);
 
+			/* Now we again we need to switch on heap relation, so
+			 * let's save the current cumullative statistics and reset others.
+			*/
+			extvac_stats_start(vacrel->rel, &extVacCounters);
+
 			/*
 			 * Vacuum the Free Space Map to make newly-freed space visible on
 			 * upper-level FSM pages.  Note we have not yet processed blkno.
@@ -1220,6 +1304,8 @@ lazy_scan_heap(LVRelState *vacrel)
 		Max(vacrel->new_live_tuples, 0) + vacrel->recently_dead_tuples +
 		vacrel->missed_dead_tuples;
 
+	/* Before starting to process the indexes save the current heap statistics */
+	accumulate_heap_vacuum_statistics(&extVacCounters, vacrel);
 	/*
 	 * Do index vacuuming (call each index's ambulkdelete routine), then do
 	 * related heap vacuuming
@@ -1227,6 +1313,11 @@ lazy_scan_heap(LVRelState *vacrel)
 	if (vacrel->dead_items_info->num_items > 0)
 		lazy_vacuum(vacrel);
 
+	/* Due to the fact that the vacuum will process FSM after having processed
+	 * the indexes we need to take account statistics for heap too.
+	 */
+	extvac_stats_start(vacrel->rel, &extVacCounters);
+
 	/*
 	 * Vacuum the remainder of the Free Space Map.  We must do this whether or
 	 * not there were indexes, and whether or not we bypassed index vacuuming.
@@ -1237,6 +1328,9 @@ lazy_scan_heap(LVRelState *vacrel)
 	/* report all blocks vacuumed */
 	pgstat_progress_update_param(PROGRESS_VACUUM_HEAP_BLKS_VACUUMED, blkno);
 
+	/* Before starting to process the indexes save the current heap statistics */
+	accumulate_heap_vacuum_statistics(&extVacCounters, vacrel);
+
 	/* Do final index cleanup (call each index's amvacuumcleanup routine) */
 	if (vacrel->nindexes > 0 && vacrel->do_index_cleanup)
 		lazy_cleanup_all_indexes(vacrel);
@@ -2093,6 +2187,7 @@ static void
 lazy_vacuum(LVRelState *vacrel)
 {
 	bool		bypass;
+	LVExtStatCounters extVacCounters;
 
 	/* Should not end up here with no indexes */
 	Assert(vacrel->nindexes > 0);
@@ -2105,6 +2200,9 @@ lazy_vacuum(LVRelState *vacrel)
 		return;
 	}
 
+	/* Set initial statistics values to gather vacuum statistics for the heap */
+	extvac_stats_start(vacrel->rel, &extVacCounters);
+
 	/*
 	 * Consider bypassing index vacuuming (and heap vacuuming) entirely.
 	 *
@@ -2161,6 +2259,9 @@ lazy_vacuum(LVRelState *vacrel)
 				  (TidStoreMemoryUsage(vacrel->dead_items) < (32L * 1024L * 1024L)));
 	}
 
+	/* Before starting to process the indexes save the current heap statistics */
+	accumulate_heap_vacuum_statistics(&extVacCounters, vacrel);
+
 	if (bypass)
 	{
 		/*
@@ -2675,6 +2776,11 @@ lazy_vacuum_one_index(Relation indrel, IndexBulkDeleteResult *istat,
 {
 	IndexVacuumInfo ivinfo;
 	LVSavedErrInfo saved_err_info;
+	LVExtStatCountersIdx extVacCounters;
+	ExtVacReport extVacReport;
+
+	/* Set initial statistics values to gather vacuum statistics for the index */
+	extvac_stats_start_idx(indrel, istat, &extVacCounters);
 
 	ivinfo.index = indrel;
 	ivinfo.heaprel = vacrel->rel;
@@ -2693,6 +2799,7 @@ lazy_vacuum_one_index(Relation indrel, IndexBulkDeleteResult *istat,
 	 */
 	Assert(vacrel->indname == NULL);
 	vacrel->indname = pstrdup(RelationGetRelationName(indrel));
+	vacrel->indoid = RelationGetRelid(indrel);
 	update_vacuum_error_info(vacrel, &saved_err_info,
 							 VACUUM_ERRCB_PHASE_VACUUM_INDEX,
 							 InvalidBlockNumber, InvalidOffsetNumber);
@@ -2701,6 +2808,15 @@ lazy_vacuum_one_index(Relation indrel, IndexBulkDeleteResult *istat,
 	istat = vac_bulkdel_one_index(&ivinfo, istat, vacrel->dead_items,
 								  vacrel->dead_items_info);
 
+	if(pgstat_track_vacuum_statistics)
+	{
+		/* Make extended vacuum stats report for index */
+		extvac_stats_end_idx(indrel, istat, &extVacCounters, &extVacReport);
+		pgstat_report_vacuum(RelationGetRelid(indrel),
+								indrel->rd_rel->relisshared,
+								0, 0, &extVacReport);
+	}
+
 	/* Revert to the previous phase information for error traceback */
 	restore_vacuum_error_info(vacrel, &saved_err_info);
 	pfree(vacrel->indname);
@@ -2725,6 +2841,11 @@ lazy_cleanup_one_index(Relation indrel, IndexBulkDeleteResult *istat,
 {
 	IndexVacuumInfo ivinfo;
 	LVSavedErrInfo saved_err_info;
+	LVExtStatCountersIdx extVacCounters;
+	ExtVacReport extVacReport;
+
+	/* Set initial statistics values to gather vacuum statistics for the index */
+	extvac_stats_start_idx(indrel, istat, &extVacCounters);
 
 	ivinfo.index = indrel;
 	ivinfo.heaprel = vacrel->rel;
@@ -2744,12 +2865,22 @@ lazy_cleanup_one_index(Relation indrel, IndexBulkDeleteResult *istat,
 	 */
 	Assert(vacrel->indname == NULL);
 	vacrel->indname = pstrdup(RelationGetRelationName(indrel));
+	vacrel->indoid = RelationGetRelid(indrel);
 	update_vacuum_error_info(vacrel, &saved_err_info,
 							 VACUUM_ERRCB_PHASE_INDEX_CLEANUP,
 							 InvalidBlockNumber, InvalidOffsetNumber);
 
 	istat = vac_cleanup_one_index(&ivinfo, istat);
 
+	if(pgstat_track_vacuum_statistics)
+	{
+		/* Make extended vacuum stats report for index */
+		extvac_stats_end_idx(indrel, istat, &extVacCounters, &extVacReport);
+		pgstat_report_vacuum(RelationGetRelid(indrel),
+								indrel->rd_rel->relisshared,
+								0, 0, &extVacReport);
+	}
+
 	/* Revert to the previous phase information for error traceback */
 	restore_vacuum_error_info(vacrel, &saved_err_info);
 	pfree(vacrel->indname);
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index 43ac27ed5b4..09e93b21e82 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -1431,3 +1431,35 @@ FROM pg_class rel
   JOIN pg_namespace ns ON ns.oid = rel.relnamespace,
   LATERAL pg_stat_get_vacuum_tables(rel.oid) stats
 WHERE rel.relkind = 'r';
+
+CREATE VIEW pg_stat_vacuum_indexes AS
+SELECT
+  rel.oid as relid,
+  ns.nspname AS schemaname,
+  rel.relname AS relname,
+
+  total_blks_read AS total_blks_read,
+  total_blks_hit AS total_blks_hit,
+  total_blks_dirtied AS total_blks_dirtied,
+  total_blks_written AS total_blks_written,
+
+  rel_blks_read AS rel_blks_read,
+  rel_blks_hit AS rel_blks_hit,
+
+  pages_deleted AS pages_deleted,
+  tuples_deleted AS tuples_deleted,
+
+  wal_records AS wal_records,
+  wal_fpi AS wal_fpi,
+  wal_bytes AS wal_bytes,
+
+  blk_read_time AS blk_read_time,
+  blk_write_time AS blk_write_time,
+
+  delay_time AS delay_time,
+  total_time AS total_time
+FROM
+  pg_class rel
+  JOIN pg_namespace ns ON ns.oid = rel.relnamespace,
+  LATERAL pg_stat_get_vacuum_indexes(rel.oid) stats
+WHERE rel.relkind = 'i';
\ No newline at end of file
diff --git a/src/backend/commands/vacuumparallel.c b/src/backend/commands/vacuumparallel.c
index 672f8f4bfe8..d41711a80d5 100644
--- a/src/backend/commands/vacuumparallel.c
+++ b/src/backend/commands/vacuumparallel.c
@@ -868,6 +868,8 @@ parallel_vacuum_process_one_index(ParallelVacuumState *pvs, Relation indrel,
 	IndexBulkDeleteResult *istat = NULL;
 	IndexBulkDeleteResult *istat_res;
 	IndexVacuumInfo ivinfo;
+	LVExtStatCountersIdx extVacCounters;
+	ExtVacReport extVacReport;
 
 	/*
 	 * Update the pointer to the corresponding bulk-deletion result if someone
@@ -876,6 +878,9 @@ parallel_vacuum_process_one_index(ParallelVacuumState *pvs, Relation indrel,
 	if (indstats->istat_updated)
 		istat = &(indstats->istat);
 
+	/* Set initial statistics values to gather vacuum statistics for the index */
+	extvac_stats_start_idx(indrel, &(indstats->istat), &extVacCounters);
+
 	ivinfo.index = indrel;
 	ivinfo.heaprel = pvs->heaprel;
 	ivinfo.analyze_only = false;
@@ -904,6 +909,15 @@ parallel_vacuum_process_one_index(ParallelVacuumState *pvs, Relation indrel,
 				 RelationGetRelationName(indrel));
 	}
 
+	if(istat_res != NULL && pgstat_track_vacuum_statistics)
+	{
+		/* Make extended vacuum stats report for index */
+		extvac_stats_end_idx(indrel, istat_res, &extVacCounters, &extVacReport);
+		pgstat_report_vacuum(RelationGetRelid(indrel),
+								indrel->rd_rel->relisshared,
+								0, 0, &extVacReport);
+	}
+
 	/*
 	 * Copy the index bulk-deletion result returned from ambulkdelete and
 	 * amvacuumcleanup to the DSM segment if it's the first cycle because they
diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index d21b9302c29..dc81bb12c86 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -1190,6 +1190,10 @@ pgstat_build_snapshot(PgStat_Kind statKind)
 		if (p->dropped)
 			continue;
 
+		if (statKind != PGSTAT_KIND_INVALID && statKind != p->key.kind)
+			/* Load stat of specific type, if defined */
+			continue;
+
 		Assert(pg_atomic_read_u32(&p->refcount) > 0);
 
 		stats_data = dsa_get_address(pgStatLocal.dsa, p->body);
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index 458bd4ece49..b18ae610b84 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -995,6 +995,9 @@ static void
 pgstat_accumulate_extvac_stats(ExtVacReport *dst, ExtVacReport *src,
 							   bool accumulate_reltype_specific_info)
 {
+	if(!pgstat_track_vacuum_statistics)
+		return;
+
 	dst->total_blks_read += src->total_blks_read;
 	dst->total_blks_hit += src->total_blks_hit;
 	dst->total_blks_dirtied += src->total_blks_dirtied;
@@ -1010,20 +1013,35 @@ pgstat_accumulate_extvac_stats(ExtVacReport *dst, ExtVacReport *src,
 	if (!accumulate_reltype_specific_info)
 		return;
 
-	dst->blks_fetched += src->blks_fetched;
-	dst->blks_hit += src->blks_hit;
-
-	dst->pages_scanned += src->pages_scanned;
-	dst->pages_removed += src->pages_removed;
-	dst->vm_new_frozen_pages += src->vm_new_frozen_pages;
-	dst->vm_new_visible_pages += src->vm_new_visible_pages;
-	dst->vm_new_visible_frozen_pages += src->vm_new_visible_frozen_pages;
-	dst->tuples_deleted += src->tuples_deleted;
-	dst->tuples_frozen += src->tuples_frozen;
-	dst->recently_dead_tuples += src->recently_dead_tuples;
-	dst->index_vacuum_count += src->index_vacuum_count;
-	dst->wraparound_failsafe_count += src->wraparound_failsafe_count;
-	dst->missed_dead_pages += src->missed_dead_pages;
-	dst->missed_dead_tuples += src->missed_dead_tuples;
+	if (dst->type == PGSTAT_EXTVAC_INVALID)
+		dst->type = src->type;
+
+	Assert(src->type == PGSTAT_EXTVAC_INVALID || src->type == dst->type);
+
+	if (dst->type == src->type)
+	{
+		dst->blks_fetched += src->blks_fetched;
+		dst->blks_hit += src->blks_hit;
 
+		if (dst->type == PGSTAT_EXTVAC_TABLE)
+		{
+			dst->table.pages_scanned += src->table.pages_scanned;
+			dst->table.pages_removed += src->table.pages_removed;
+			dst->table.vm_new_frozen_pages += src->table.vm_new_frozen_pages;
+			dst->table.vm_new_visible_pages += src->table.vm_new_visible_pages;
+			dst->table.vm_new_visible_frozen_pages += src->table.vm_new_visible_frozen_pages;
+			dst->tuples_deleted += src->tuples_deleted;
+			dst->table.tuples_frozen += src->table.tuples_frozen;
+			dst->table.recently_dead_tuples += src->table.recently_dead_tuples;
+			dst->table.index_vacuum_count += src->table.index_vacuum_count;
+			dst->table.missed_dead_pages += src->table.missed_dead_pages;
+			dst->table.missed_dead_tuples += src->table.missed_dead_tuples;
+			dst->table.wraparound_failsafe_count += src->table.wraparound_failsafe_count;
+		}
+		else if (dst->type == PGSTAT_EXTVAC_INDEX)
+		{
+			dst->index.pages_deleted += src->index.pages_deleted;
+			dst->tuples_deleted += src->tuples_deleted;
+		}
+	}
 }
\ No newline at end of file
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index da31d9d3e29..3105a9de390 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -2286,18 +2286,19 @@ pg_stat_get_vacuum_tables(PG_FUNCTION_ARGS)
 									extvacuum->blks_hit);
 	values[i++] = Int64GetDatum(extvacuum->blks_hit);
 
-	values[i++] = Int64GetDatum(extvacuum->pages_scanned);
-	values[i++] = Int64GetDatum(extvacuum->pages_removed);
-	values[i++] = Int64GetDatum(extvacuum->vm_new_frozen_pages);
-	values[i++] = Int64GetDatum(extvacuum->vm_new_visible_pages);
-	values[i++] = Int64GetDatum(extvacuum->vm_new_visible_frozen_pages);
-	values[i++] = Int64GetDatum(extvacuum->missed_dead_pages);
+	values[i++] = Int64GetDatum(extvacuum->table.pages_scanned);
+	values[i++] = Int64GetDatum(extvacuum->table.pages_removed);
+	values[i++] = Int64GetDatum(extvacuum->table.vm_new_frozen_pages);
+	values[i++] = Int64GetDatum(extvacuum->table.vm_new_visible_pages);
+	values[i++] = Int64GetDatum(extvacuum->table.vm_new_visible_frozen_pages);
+	values[i++] = Int64GetDatum(extvacuum->table.missed_dead_pages);
 	values[i++] = Int64GetDatum(extvacuum->tuples_deleted);
-	values[i++] = Int64GetDatum(extvacuum->tuples_frozen);
-	values[i++] = Int64GetDatum(extvacuum->recently_dead_tuples);
-	values[i++] = Int64GetDatum(extvacuum->missed_dead_tuples);
-	values[i++] = Int32GetDatum(extvacuum->wraparound_failsafe_count);
-	values[i++] = Int64GetDatum(extvacuum->index_vacuum_count);
+	values[i++] = Int64GetDatum(extvacuum->table.tuples_frozen);
+	values[i++] = Int64GetDatum(extvacuum->table.recently_dead_tuples);
+	values[i++] = Int64GetDatum(extvacuum->table.missed_dead_tuples);
+
+	values[i++] = Int32GetDatum(extvacuum->table.wraparound_failsafe_count);
+	values[i++] = Int64GetDatum(extvacuum->table.index_vacuum_count);
 
 	values[i++] = Int64GetDatum(extvacuum->wal_records);
 	values[i++] = Int64GetDatum(extvacuum->wal_fpi);
@@ -2316,6 +2317,116 @@ pg_stat_get_vacuum_tables(PG_FUNCTION_ARGS)
 
 	Assert(i == PG_STAT_GET_VACUUM_TABLES_STATS_COLS);
 
+	/* Returns the record as Datum */
+	PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
+/*
+ * Get the vacuum statistics for the heap tables.
+ */
+Datum
+pg_stat_get_vacuum_indexes(PG_FUNCTION_ARGS)
+{
+	#define PG_STAT_GET_VACUUM_INDEX_STATS_COLS	16
+
+	Oid						relid = PG_GETARG_OID(0);
+	PgStat_StatTabEntry     *tabentry;
+	ExtVacReport 			*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;
+	ExtVacReport allzero;
+
+	/* Initialise attributes information in the tuple descriptor */
+	tupdesc = CreateTemplateTupleDesc(PG_STAT_GET_VACUUM_INDEX_STATS_COLS);
+
+	TupleDescInitEntry(tupdesc, (AttrNumber) ++i, "relid",
+					   INT4OID, -1, 0);
+	TupleDescInitEntry(tupdesc, (AttrNumber) ++i, "total_ blks_read",
+					   INT8OID, -1, 0);
+	TupleDescInitEntry(tupdesc, (AttrNumber) ++i, "total_blks_hit",
+					   INT8OID, -1, 0);
+	TupleDescInitEntry(tupdesc, (AttrNumber) ++i, "total_blks_dirtied",
+					   INT8OID, -1, 0);
+	TupleDescInitEntry(tupdesc, (AttrNumber) ++i, "total_blks_written",
+					   INT8OID, -1, 0);
+	TupleDescInitEntry(tupdesc, (AttrNumber) ++i, "rel_blks_read",
+					   INT8OID, -1, 0);
+	TupleDescInitEntry(tupdesc, (AttrNumber) ++i, "rel_blks_hit",
+					   INT8OID, -1, 0);
+	TupleDescInitEntry(tupdesc, (AttrNumber) ++i, "pages_deleted",
+					   INT8OID, -1, 0);
+	TupleDescInitEntry(tupdesc, (AttrNumber) ++i, "tuples_deleted",
+					   INT8OID, -1, 0);
+	TupleDescInitEntry(tupdesc, (AttrNumber) ++i, "wal_records",
+					   INT8OID, -1, 0);
+	TupleDescInitEntry(tupdesc, (AttrNumber) ++i, "wal_fpi",
+					   INT8OID, -1, 0);
+	TupleDescInitEntry(tupdesc, (AttrNumber) ++i, "wal_bytes",
+					   NUMERICOID, -1, 0);
+
+	TupleDescInitEntry(tupdesc, (AttrNumber) ++i, "blk_read_time",
+					   FLOAT8OID, -1, 0);
+	TupleDescInitEntry(tupdesc, (AttrNumber) ++i, "blk_write_time",
+					   FLOAT8OID, -1, 0);
+
+	TupleDescInitEntry(tupdesc, (AttrNumber) ++i, "delay_time",
+					   FLOAT8OID, -1, 0);
+	TupleDescInitEntry(tupdesc, (AttrNumber) ++i, "total_time",
+					   FLOAT8OID, -1, 0);
+
+	Assert(i == PG_STAT_GET_VACUUM_INDEX_STATS_COLS);
+
+	BlessTupleDesc(tupdesc);
+
+	tabentry = pgstat_fetch_stat_tabentry(relid);
+
+	if (tabentry == NULL)
+	{
+		/* If the subscription is not found, initialise its stats */
+		memset(&allzero, 0, sizeof(ExtVacReport));
+		extvacuum = &allzero;
+	}
+	else
+	{
+		extvacuum = &(tabentry->vacuum_ext);
+	}
+
+	i = 0;
+
+	values[i++] = ObjectIdGetDatum(relid);
+
+	values[i++] = Int64GetDatum(extvacuum->total_blks_read);
+	values[i++] = Int64GetDatum(extvacuum->total_blks_hit);
+	values[i++] = Int64GetDatum(extvacuum->total_blks_dirtied);
+	values[i++] = Int64GetDatum(extvacuum->total_blks_written);
+
+	values[i++] = Int64GetDatum(extvacuum->blks_fetched -
+									extvacuum->blks_hit);
+	values[i++] = Int64GetDatum(extvacuum->blks_hit);
+
+	values[i++] = Int64GetDatum(extvacuum->index.pages_deleted);
+	values[i++] = Int64GetDatum(extvacuum->tuples_deleted);
+
+	values[i++] = Int64GetDatum(extvacuum->wal_records);
+	values[i++] = Int64GetDatum(extvacuum->wal_fpi);
+
+	/* Convert to numeric, like pg_stat_statements */
+	snprintf(buf, sizeof buf, UINT64_FORMAT, extvacuum->wal_bytes);
+	values[i++] = DirectFunctionCall3(numeric_in,
+									  CStringGetDatum(buf),
+									  ObjectIdGetDatum(0),
+									  Int32GetDatum(-1));
+
+	values[i++] = Float8GetDatum(extvacuum->blk_read_time);
+	values[i++] = Float8GetDatum(extvacuum->blk_write_time);
+	values[i++] = Float8GetDatum(extvacuum->delay_time);
+	values[i++] = Float8GetDatum(extvacuum->total_time);
+
+	Assert(i == PG_STAT_GET_VACUUM_INDEX_STATS_COLS);
+
 	/* Returns the record as Datum */
 	PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
 }
\ No newline at end of file
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index f2d31e174b4..2a96cf51a36 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -1480,7 +1480,7 @@ struct config_bool ConfigureNamesBool[] =
 	},
 	{
 		{"track_vacuum_statistics", PGC_SUSET, STATS_CUMULATIVE,
-			gettext_noop("Collects vacuum statistics for table relations."),
+			gettext_noop("Collects vacuum statistics for relations."),
 			NULL
 		},
 		&pgstat_track_vacuum_statistics,
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0d9584a9fec..d7f06a69487 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12447,4 +12447,13 @@
   proname => 'pg_stat_get_rev_all_frozen_pages', provolatile => 's',
   proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_rev_all_frozen_pages' },
+{ oid => '8004',
+  descr => 'pg_stat_get_vacuum_indexes return stats values',
+  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,float8,float8,float8,float8}',
+  proargmodes => '{i,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o}',
+  proargnames => '{reloid,relid,total_blks_read,total_blks_hit,total_blks_dirtied,total_blks_written,rel_blks_read,rel_blks_hit,pages_deleted,tuples_deleted,wal_records,wal_fpi,wal_bytes,blk_read_time,blk_write_time,delay_time,total_time}',
+  prosrc => 'pg_stat_get_vacuum_indexes' }
 ]
diff --git a/src/include/commands/vacuum.h b/src/include/commands/vacuum.h
index 94d599767df..4d4a3d7b02d 100644
--- a/src/include/commands/vacuum.h
+++ b/src/include/commands/vacuum.h
@@ -25,6 +25,7 @@
 #include "storage/buf.h"
 #include "storage/lock.h"
 #include "utils/relcache.h"
+#include "pgstat.h"
 
 /*
  * Flags for amparallelvacuumoptions to control the participation of bulkdelete
@@ -288,6 +289,26 @@ typedef struct VacDeadItemsInfo
 	int64		num_items;		/* current # of entries */
 } VacDeadItemsInfo;
 
+/*
+ * Counters and usage data for extended stats tracking.
+ */
+typedef struct LVExtStatCounters
+{
+	TimestampTz starttime;
+	WalUsage	walusage;
+	BufferUsage bufusage;
+	double		VacuumDelayTime;
+	PgStat_Counter blocks_fetched;
+	PgStat_Counter blocks_hit;
+} LVExtStatCounters;
+
+typedef struct LVExtStatCountersIdx
+{
+	LVExtStatCounters common;
+	int64		pages_deleted;
+	int64		tuples_removed;
+} LVExtStatCountersIdx;
+
 /* GUC parameters */
 extern PGDLLIMPORT int default_statistics_target;	/* PGDLLIMPORT for PostGIS */
 extern PGDLLIMPORT int vacuum_freeze_min_age;
@@ -386,4 +407,8 @@ extern double anl_random_fract(void);
 extern double anl_init_selection_state(int n);
 extern double anl_get_next_S(double t, int n, double *stateptr);
 
+extern void extvac_stats_start_idx(Relation rel, IndexBulkDeleteResult *stats,
+					   LVExtStatCountersIdx *counters);
+extern void extvac_stats_end_idx(Relation rel, IndexBulkDeleteResult *stats,
+					 LVExtStatCountersIdx *counters, ExtVacReport *report);
 #endif							/* VACUUM_H */
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 0266993dbf2..572e34e0bb0 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -167,11 +167,19 @@ typedef struct PgStat_BackendSubEntry
 	PgStat_Counter conflict_count[CONFLICT_NUM_TYPES];
 } PgStat_BackendSubEntry;
 
+/* Type of ExtVacReport */
+typedef enum ExtVacReportType
+{
+	PGSTAT_EXTVAC_INVALID = 0,
+	PGSTAT_EXTVAC_TABLE = 1,
+	PGSTAT_EXTVAC_INDEX = 2
+} ExtVacReportType;
+
 /* ----------
  *
  * ExtVacReport
  *
- * Additional statistics of vacuum processing over a heap relation.
+ * Additional statistics of vacuum processing over a relation.
  * pages_removed is the amount by which the physically shrank,
  * if any (ie the change in its total size on disk)
  * pages_deleted refer to free space within the index file
@@ -200,18 +208,44 @@ typedef struct ExtVacReport
 	double		delay_time;		/* how long vacuum slept in vacuum delay point, in msec */
 	double		total_time;		/* total time of a vacuum operation, in msec */
 
-	int64		pages_scanned;		/* heap pages examined (not skipped by VM) */
-	int64		pages_removed;		/* heap pages removed by vacuum "truncation" */
-	int64		vm_new_frozen_pages;		/* pages marked in VM as frozen */
-	int64		vm_new_visible_pages;	/* pages marked in VM as all-visible */
-	int64		vm_new_visible_frozen_pages;	/* pages marked in VM as all-visible and frozen */
-	int64		missed_dead_tuples;		/* tuples not pruned by vacuum due to failure to get a cleanup lock */
-	int64		missed_dead_pages;		/* pages with missed dead tuples */
 	int64		tuples_deleted;		/* tuples deleted by vacuum */
-	int64		tuples_frozen;		/* tuples frozen up by vacuum */
-	int64		recently_dead_tuples;	/* deleted tuples that are still visible to some transaction */
-	int64		index_vacuum_count;	/* the number of index vacuumings */
-	int32		wraparound_failsafe_count;	/* the number of times to prevent workaround problem */
+
+	ExtVacReportType type;		/* heap, index, etc. */
+
+	/* ----------
+	 *
+	 * There are separate metrics of statistic for tables and indexes,
+	 * which collect during vacuum.
+	 * The union operator allows to combine these statistics
+	 * so that each metric is assigned to a specific class of collected statistics.
+	 * Such a combined structure was called per_type_stats.
+	 * The name of the structure itself is not used anywhere,
+	 * it exists only for understanding the code.
+	 * ----------
+	*/
+	union
+	{
+		struct
+		{
+			int64		pages_scanned;		/* heap pages examined (not skipped by VM) */
+			int64		pages_removed;		/* heap pages removed by vacuum "truncation" */
+			int64		pages_frozen;		/* pages marked in VM as frozen */
+			int64		pages_all_visible;	/* pages marked in VM as all-visible */
+			int64		tuples_frozen;		/* tuples frozen up by vacuum */
+			int64		recently_dead_tuples;	/* deleted tuples that are still visible to some transaction */
+			int64		vm_new_frozen_pages;		/* pages marked in VM as frozen */
+			int64		vm_new_visible_pages;	/* pages marked in VM as all-visible */
+			int64		vm_new_visible_frozen_pages;	/* pages marked in VM as all-visible and frozen */
+			int64		missed_dead_tuples;		/* tuples not pruned by vacuum due to failure to get a cleanup lock */
+			int64		missed_dead_pages;		/* pages with missed dead tuples */
+			int64		index_vacuum_count;	/* number of index vacuumings */
+			int32		wraparound_failsafe_count;	/* the number of times to prevent workaround problem */
+		}			table;
+		struct
+		{
+			int64		pages_deleted;		/* number of pages deleted by vacuum */
+		}			index;
+	} /* per_type_stats */;
 } ExtVacReport;
 
 /* ----------
diff --git a/src/test/isolation/expected/vacuum-extending-in-repetable-read.out b/src/test/isolation/expected/vacuum-extending-in-repetable-read.out
index 87f7e40b4a6..6d960423912 100644
--- a/src/test/isolation/expected/vacuum-extending-in-repetable-read.out
+++ b/src/test/isolation/expected/vacuum-extending-in-repetable-read.out
@@ -34,7 +34,7 @@ step s2_print_vacuum_stats_table:
 
 relname                   |tuples_deleted|recently_dead_tuples|missed_dead_tuples|missed_dead_pages|tuples_frozen
 --------------------------+--------------+--------------------+------------------+-----------------+-------------
-test_vacuum_stat_isolation|             0|                 100|                 0|                0|            0
+test_vacuum_stat_isolation|             0|                 600|                 0|                0|            0
 (1 row)
 
 step s1_commit: COMMIT;
@@ -48,6 +48,6 @@ step s2_print_vacuum_stats_table:
 
 relname                   |tuples_deleted|recently_dead_tuples|missed_dead_tuples|missed_dead_pages|tuples_frozen
 --------------------------+--------------+--------------------+------------------+-----------------+-------------
-test_vacuum_stat_isolation|           100|                 100|                 0|                0|          101
+test_vacuum_stat_isolation|           300|                 600|                 0|                0|          303
 (1 row)
 
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index b49f7936f93..247e22a82a5 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -2245,6 +2245,28 @@ pg_stat_user_tables| SELECT relid,
     rev_all_visible_pages
    FROM pg_stat_all_tables
   WHERE ((schemaname <> ALL (ARRAY['pg_catalog'::name, 'information_schema'::name])) AND (schemaname !~ '^pg_toast'::text));
+pg_stat_vacuum_indexes| SELECT rel.oid AS relid,
+    ns.nspname AS schemaname,
+    rel.relname,
+    stats.total_blks_read,
+    stats.total_blks_hit,
+    stats.total_blks_dirtied,
+    stats.total_blks_written,
+    stats.rel_blks_read,
+    stats.rel_blks_hit,
+    stats.pages_deleted,
+    stats.tuples_deleted,
+    stats.wal_records,
+    stats.wal_fpi,
+    stats.wal_bytes,
+    stats.blk_read_time,
+    stats.blk_write_time,
+    stats.delay_time,
+    stats.total_time
+   FROM (pg_class rel
+     JOIN pg_namespace ns ON ((ns.oid = rel.relnamespace))),
+    LATERAL pg_stat_get_vacuum_indexes(rel.oid) stats(relid, total_blks_read, total_blks_hit, total_blks_dirtied, total_blks_written, rel_blks_read, rel_blks_hit, pages_deleted, tuples_deleted, wal_records, wal_fpi, wal_bytes, blk_read_time, blk_write_time, delay_time, total_time)
+  WHERE (rel.relkind = 'i'::"char");
 pg_stat_vacuum_tables| SELECT ns.nspname AS schemaname,
     rel.relname,
     stats.relid,
diff --git a/src/test/regress/expected/vacuum_index_statistics.out b/src/test/regress/expected/vacuum_index_statistics.out
new file mode 100644
index 00000000000..e00a0fc683c
--- /dev/null
+++ b/src/test/regress/expected/vacuum_index_statistics.out
@@ -0,0 +1,183 @@
+--
+-- Test cumulative vacuum stats system
+--
+-- Check the wall statistics collected during vacuum operation:
+-- number of frozen and visible pages set by vacuum;
+-- number of frozen and visible pages removed by backend.
+-- Statistic wal_fpi is not displayed in this test because its behavior is unstable.
+--
+-- conditio sine qua non
+SHOW track_counts;  -- must be on
+ track_counts 
+--------------
+ on
+(1 row)
+
+\set sample_size 10000
+-- not enabled by default, but we want to test it...
+SET track_functions TO 'all';
+-- Test that vacuum statistics will be empty when parameter is off.
+SET track_vacuum_statistics TO 'off';
+CREATE TABLE vestat (x int) WITH (autovacuum_enabled = off, fillfactor = 10);
+INSERT INTO vestat SELECT x FROM generate_series(1,:sample_size) as x;
+ANALYZE vestat;
+DELETE FROM vestat WHERE x % 2 = 0;
+VACUUM (PARALLEL 0, BUFFER_USAGE_LIMIT 128) vestat;
+-- Must be empty.
+SELECT *
+FROM pg_stat_vacuum_indexes vt
+WHERE vt.relname = 'vestat';
+ relid | schemaname | relname | total_blks_read | total_blks_hit | total_blks_dirtied | total_blks_written | rel_blks_read | rel_blks_hit | pages_deleted | tuples_deleted | wal_records | wal_fpi | wal_bytes | blk_read_time | blk_write_time | delay_time | total_time 
+-------+------------+---------+-----------------+----------------+--------------------+--------------------+---------------+--------------+---------------+----------------+-------------+---------+-----------+---------------+----------------+------------+------------
+(0 rows)
+
+RESET track_vacuum_statistics;
+DROP TABLE vestat CASCADE;
+SHOW track_vacuum_statistics;  -- must be on
+ track_vacuum_statistics 
+-------------------------
+ on
+(1 row)
+
+-- ensure pending stats are flushed
+SELECT pg_stat_force_next_flush();
+ pg_stat_force_next_flush 
+--------------------------
+ 
+(1 row)
+
+\set sample_size 10000
+SET vacuum_freeze_min_age = 0;
+SET vacuum_freeze_table_age = 0;
+--SET stats_fetch_consistency = snapshot;
+CREATE TABLE vestat (x int primary key) WITH (autovacuum_enabled = off, fillfactor = 10);
+INSERT INTO vestat SELECT x FROM generate_series(1,:sample_size) as x;
+ANALYZE vestat;
+SELECT oid AS ioid from pg_class where relname = 'vestat_pkey' \gset
+DELETE FROM vestat WHERE x % 2 = 0;
+-- Before the first vacuum execution extended stats view is empty.
+SELECT vt.relname,relpages,pages_deleted,tuples_deleted
+FROM pg_stat_vacuum_indexes vt, pg_class c
+WHERE vt.relname = 'vestat_pkey' AND vt.relid = c.oid;
+   relname   | relpages | pages_deleted | tuples_deleted 
+-------------+----------+---------------+----------------
+ vestat_pkey |       30 |             0 |              0
+(1 row)
+
+SELECT relpages AS irp
+FROM pg_class c
+WHERE relname = 'vestat_pkey' \gset
+VACUUM (PARALLEL 0, BUFFER_USAGE_LIMIT 128, INDEX_CLEANUP ON) vestat;
+-- it is necessary to check the wal statistics
+CHECKPOINT;
+-- The table and index extended vacuum statistics should show us that
+-- vacuum frozed pages and clean up pages, but pages_removed stayed the same
+-- because of not full table have cleaned up
+SELECT vt.relname,relpages-:irp = 0 AS relpages,pages_deleted = 0 AS pages_deleted,tuples_deleted > 0 AS tuples_deleted
+FROM pg_stat_vacuum_indexes vt, pg_class c
+WHERE vt.relname = 'vestat_pkey' AND vt.relid = c.oid;
+   relname   | relpages | pages_deleted | tuples_deleted 
+-------------+----------+---------------+----------------
+ vestat_pkey | t        | t             | t
+(1 row)
+
+SELECT vt.relname,relpages AS irp,pages_deleted AS ipd,tuples_deleted AS itd
+FROM pg_stat_vacuum_indexes vt, pg_class c
+WHERE vt.relname = 'vestat_pkey' AND vt.relid = c.oid \gset
+-- Store WAL advances into variables
+SELECT wal_records AS iwr,wal_bytes AS iwb,wal_fpi AS ifpi FROM pg_stat_vacuum_indexes WHERE relname = 'vestat_pkey' \gset
+-- Look into WAL records deltas.
+SELECT wal_records > 0 AS diWR, wal_bytes > 0 AS diWB
+FROM pg_stat_vacuum_indexes WHERE relname = 'vestat_pkey';
+ diwr | diwb 
+------+------
+ t    | t
+(1 row)
+
+DELETE FROM vestat;;
+VACUUM (PARALLEL 0, BUFFER_USAGE_LIMIT 128, INDEX_CLEANUP ON) vestat;
+-- it is necessary to check the wal statistics
+CHECKPOINT;
+-- pages_removed must be increased
+SELECT vt.relname,relpages-:irp = 0 AS relpages,pages_deleted-:ipd > 0 AS pages_deleted,tuples_deleted-:itd > 0 AS tuples_deleted
+FROM pg_stat_vacuum_indexes vt, pg_class c
+WHERE vt.relname = 'vestat_pkey' AND vt.relid = c.oid;
+   relname   | relpages | pages_deleted | tuples_deleted 
+-------------+----------+---------------+----------------
+ vestat_pkey | t        | t             | t
+(1 row)
+
+SELECT vt.relname,relpages AS irp,pages_deleted AS ipd,tuples_deleted AS itd
+FROM pg_stat_vacuum_indexes vt, pg_class c
+WHERE vt.relname = 'vestat_pkey' AND vt.relid = c.oid \gset
+-- Store WAL advances into variables
+SELECT wal_records-:iwr AS diwr, wal_bytes-:iwb AS diwb, wal_fpi-:ifpi AS difpi
+FROM pg_stat_vacuum_indexes WHERE relname = 'vestat_pkey' \gset
+-- WAL advance should be detected.
+SELECT :diwr > 0 AS diWR, :diwb > 0 AS diWB;
+ diwr | diwb 
+------+------
+ t    | t
+(1 row)
+
+-- Store WAL advances into variables
+SELECT wal_records AS iwr,wal_bytes AS iwb,wal_fpi AS ifpi FROM pg_stat_vacuum_indexes WHERE relname = 'vestat_pkey' \gset
+INSERT INTO vestat SELECT x FROM generate_series(1,:sample_size) as x;
+DELETE FROM vestat WHERE x % 2 = 0;
+-- VACUUM FULL doesn't report to stat collector. So, no any advancements of statistics
+-- are detected here.
+VACUUM FULL vestat;
+-- It is necessary to check the wal statistics
+CHECKPOINT;
+-- Store WAL advances into variables
+SELECT wal_records-:iwr AS diwr2, wal_bytes-:iwb AS diwb2, wal_fpi-:ifpi AS difpi2
+FROM pg_stat_vacuum_indexes WHERE relname = 'vestat_pkey' \gset
+-- WAL and other statistics advance should not be detected.
+SELECT :diwr2=0 AS diWR, :difpi2=0 AS iFPI, :diwb2=0 AS diWB;
+ diwr | ifpi | diwb 
+------+------+------
+ t    | t    | t
+(1 row)
+
+SELECT vt.relname,relpages-:irp < 0 AS relpages,pages_deleted-:ipd = 0 AS pages_deleted,tuples_deleted-:itd = 0 AS tuples_deleted
+FROM pg_stat_vacuum_indexes vt, pg_class c
+WHERE vt.relname = 'vestat_pkey' AND vt.relid = c.oid;
+   relname   | relpages | pages_deleted | tuples_deleted 
+-------------+----------+---------------+----------------
+ vestat_pkey | t        | t             | t
+(1 row)
+
+SELECT vt.relname,relpages AS irp,pages_deleted AS ipd,tuples_deleted AS itd
+FROM pg_stat_vacuum_indexes vt, pg_class c
+WHERE vt.relname = 'vestat_pkey' AND vt.relid = c.oid \gset
+-- Store WAL advances into variables
+SELECT wal_records AS iwr,wal_bytes AS iwb,wal_fpi AS ifpi FROM pg_stat_vacuum_indexes WHERE relname = 'vestat_pkey' \gset
+DELETE FROM vestat;
+TRUNCATE vestat;
+VACUUM (PARALLEL 0, BUFFER_USAGE_LIMIT 128, INDEX_CLEANUP ON) vestat;
+-- it is necessary to check the wal statistics
+CHECKPOINT;
+-- Store WAL advances into variables after removing all tuples from the table
+SELECT wal_records-:iwr AS diwr3, wal_bytes-:iwb AS diwb3, wal_fpi-:ifpi AS difpi3
+FROM pg_stat_vacuum_indexes WHERE relname = 'vestat_pkey' \gset
+--There are nothing changed
+SELECT :diwr3=0 AS diWR, :difpi3=0 AS iFPI, :diwb3=0 AS diWB;
+ diwr | ifpi | diwb 
+------+------+------
+ t    | t    | t
+(1 row)
+
+--
+-- Now, the table and index is compressed into zero number of pages. Check it
+-- in vacuum extended statistics.
+-- The pages_frozen, pages_scanned values shouldn't be changed
+--
+SELECT vt.relname,relpages-:irp = 0 AS relpages,pages_deleted-:ipd = 0 AS pages_deleted,tuples_deleted-:itd = 0 AS tuples_deleted
+FROM pg_stat_vacuum_indexes vt, pg_class c
+WHERE vt.relname = 'vestat_pkey' AND vt.relid = c.oid;
+   relname   | relpages | pages_deleted | tuples_deleted 
+-------------+----------+---------------+----------------
+ vestat_pkey | f        | t             | t
+(1 row)
+
+DROP TABLE vestat;
diff --git a/src/test/regress/expected/vacuum_tables_statistics.out b/src/test/regress/expected/vacuum_tables_statistics.out
index 0c05a812dd1..119c7abea5f 100644
--- a/src/test/regress/expected/vacuum_tables_statistics.out
+++ b/src/test/regress/expected/vacuum_tables_statistics.out
@@ -181,17 +181,39 @@ WHERE vt.relname = 'vestat' AND vt.relid = c.oid;
  vestat  | t                   | t              | f        | t             | t
 (1 row)
 
+DROP TABLE vestat;
+SELECT pg_stat_force_next_flush();
+ pg_stat_force_next_flush 
+--------------------------
+ 
+(1 row)
+
+SET vacuum_freeze_min_age = 0;
+SET vacuum_freeze_table_age = 0;
+CREATE TABLE vestat (x int) WITH (autovacuum_enabled = off, fillfactor = 10);
 INSERT INTO vestat SELECT x FROM generate_series(1,:sample_size) as x;
 ANALYZE vestat;
+SELECT pg_stat_force_next_flush();
+ pg_stat_force_next_flush 
+--------------------------
+ 
+(1 row)
+
 -- must be empty
 SELECT vm_new_frozen_pages, vm_new_visible_pages, rev_all_frozen_pages,rev_all_visible_pages,vm_new_visible_frozen_pages
 FROM pg_stat_vacuum_tables, pg_stat_all_tables WHERE pg_stat_vacuum_tables.relname = 'vestat' and pg_stat_vacuum_tables.relid = pg_stat_all_tables.relid;
  vm_new_frozen_pages | vm_new_visible_pages | rev_all_frozen_pages | rev_all_visible_pages | vm_new_visible_frozen_pages 
 ---------------------+----------------------+----------------------+-----------------------+-----------------------------
-                   0 |                  910 |                    0 |                     0 |                         455
+                   0 |                    0 |                    0 |                     0 |                           0
 (1 row)
 
 VACUUM (PARALLEL 0, BUFFER_USAGE_LIMIT 128) vestat;
+SELECT pg_stat_force_next_flush();
+ pg_stat_force_next_flush 
+--------------------------
+ 
+(1 row)
+
 -- backend defreezed pages
 SELECT vm_new_frozen_pages > 0 AS vm_new_frozen_pages,vm_new_visible_pages > 0 AS vm_new_visible_pages,vm_new_visible_frozen_pages > 0 AS vm_new_visible_frozen_pages,rev_all_frozen_pages = 0 AS rev_all_frozen_pages,rev_all_visible_pages = 0 AS rev_all_visible_pages
 FROM pg_stat_vacuum_tables, pg_stat_all_tables WHERE pg_stat_vacuum_tables.relname = 'vestat' and pg_stat_vacuum_tables.relid = pg_stat_all_tables.relid;
@@ -204,16 +226,28 @@ SELECT vm_new_frozen_pages AS pf, vm_new_visible_pages AS pv,vm_new_visible_froz
 FROM pg_stat_vacuum_tables, pg_stat_all_tables WHERE pg_stat_vacuum_tables.relname = 'vestat' and pg_stat_vacuum_tables.relid = pg_stat_all_tables.relid \gset
 UPDATE vestat SET x = x + 1001;
 VACUUM (PARALLEL 0, BUFFER_USAGE_LIMIT 128) vestat;
+SELECT pg_stat_force_next_flush();
+ pg_stat_force_next_flush 
+--------------------------
+ 
+(1 row)
+
 SELECT vm_new_frozen_pages > :pf AS vm_new_frozen_pages,vm_new_visible_pages > :pv AS vm_new_visible_pages,vm_new_visible_frozen_pages > :pvf AS vm_new_visible_frozen_pages,rev_all_frozen_pages > :hafp AS rev_all_frozen_pages,rev_all_visible_pages > :havp AS rev_all_visible_pages
 FROM pg_stat_vacuum_tables, pg_stat_all_tables WHERE pg_stat_vacuum_tables.relname = 'vestat' and pg_stat_vacuum_tables.relid = pg_stat_all_tables.relid;
  vm_new_frozen_pages | vm_new_visible_pages | vm_new_visible_frozen_pages | rev_all_frozen_pages | rev_all_visible_pages 
 ---------------------+----------------------+-----------------------------+----------------------+-----------------------
- f                   | t                    | f                           | f                    | f
+ f                   | t                    | t                           | t                    | t
 (1 row)
 
 SELECT vm_new_frozen_pages AS pf, vm_new_visible_pages AS pv, vm_new_visible_frozen_pages AS pvf, rev_all_frozen_pages AS hafp,rev_all_visible_pages AS havp
 FROM pg_stat_vacuum_tables, pg_stat_all_tables WHERE pg_stat_vacuum_tables.relname = 'vestat' and pg_stat_vacuum_tables.relid = pg_stat_all_tables.relid \gset
 VACUUM (PARALLEL 0, BUFFER_USAGE_LIMIT 128) vestat;
+SELECT pg_stat_force_next_flush();
+ pg_stat_force_next_flush 
+--------------------------
+ 
+(1 row)
+
 -- vacuum freezed pages
 SELECT vm_new_frozen_pages = :pf AS vm_new_frozen_pages,vm_new_visible_pages = :pv AS vm_new_visible_pages,vm_new_visible_frozen_pages = :pvf AS vm_new_visible_frozen_pages, rev_all_frozen_pages = :hafp AS rev_all_frozen_pages,rev_all_visible_pages = :havp AS rev_all_visible_pages
 FROM pg_stat_vacuum_tables, pg_stat_all_tables WHERE pg_stat_vacuum_tables.relname = 'vestat' and pg_stat_vacuum_tables.relid = pg_stat_all_tables.relid;
@@ -222,4 +256,6 @@ FROM pg_stat_vacuum_tables, pg_stat_all_tables WHERE pg_stat_vacuum_tables.relna
  t                   | t                    | t                           | t                    | t
 (1 row)
 
+RESET vacuum_freeze_min_age;
+RESET vacuum_freeze_table_age;
 DROP TABLE vestat CASCADE;
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index d11f6b7ef4b..977a87a5b1f 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -140,4 +140,5 @@ test: tablespace
 # ----------
 # Check vacuum statistics
 # ----------
+test: vacuum_index_statistics
 test: vacuum_tables_statistics
\ No newline at end of file
diff --git a/src/test/regress/sql/vacuum_index_statistics.sql b/src/test/regress/sql/vacuum_index_statistics.sql
new file mode 100644
index 00000000000..ae146e1d23f
--- /dev/null
+++ b/src/test/regress/sql/vacuum_index_statistics.sql
@@ -0,0 +1,151 @@
+--
+-- Test cumulative vacuum stats system
+--
+-- Check the wall statistics collected during vacuum operation:
+-- number of frozen and visible pages set by vacuum;
+-- number of frozen and visible pages removed by backend.
+-- Statistic wal_fpi is not displayed in this test because its behavior is unstable.
+--
+-- conditio sine qua non
+SHOW track_counts;  -- must be on
+
+\set sample_size 10000
+
+-- not enabled by default, but we want to test it...
+SET track_functions TO 'all';
+
+-- Test that vacuum statistics will be empty when parameter is off.
+SET track_vacuum_statistics TO 'off';
+
+CREATE TABLE vestat (x int) WITH (autovacuum_enabled = off, fillfactor = 10);
+INSERT INTO vestat SELECT x FROM generate_series(1,:sample_size) as x;
+ANALYZE vestat;
+
+DELETE FROM vestat WHERE x % 2 = 0;
+
+VACUUM (PARALLEL 0, BUFFER_USAGE_LIMIT 128) vestat;
+
+-- Must be empty.
+SELECT *
+FROM pg_stat_vacuum_indexes vt
+WHERE vt.relname = 'vestat';
+
+RESET track_vacuum_statistics;
+DROP TABLE vestat CASCADE;
+
+SHOW track_vacuum_statistics;  -- must be on
+
+-- ensure pending stats are flushed
+SELECT pg_stat_force_next_flush();
+
+\set sample_size 10000
+SET vacuum_freeze_min_age = 0;
+SET vacuum_freeze_table_age = 0;
+--SET stats_fetch_consistency = snapshot;
+CREATE TABLE vestat (x int primary key) WITH (autovacuum_enabled = off, fillfactor = 10);
+INSERT INTO vestat SELECT x FROM generate_series(1,:sample_size) as x;
+ANALYZE vestat;
+
+SELECT oid AS ioid from pg_class where relname = 'vestat_pkey' \gset
+
+DELETE FROM vestat WHERE x % 2 = 0;
+-- Before the first vacuum execution extended stats view is empty.
+SELECT vt.relname,relpages,pages_deleted,tuples_deleted
+FROM pg_stat_vacuum_indexes vt, pg_class c
+WHERE vt.relname = 'vestat_pkey' AND vt.relid = c.oid;
+SELECT relpages AS irp
+FROM pg_class c
+WHERE relname = 'vestat_pkey' \gset
+
+VACUUM (PARALLEL 0, BUFFER_USAGE_LIMIT 128, INDEX_CLEANUP ON) vestat;
+-- it is necessary to check the wal statistics
+CHECKPOINT;
+
+-- The table and index extended vacuum statistics should show us that
+-- vacuum frozed pages and clean up pages, but pages_removed stayed the same
+-- because of not full table have cleaned up
+SELECT vt.relname,relpages-:irp = 0 AS relpages,pages_deleted = 0 AS pages_deleted,tuples_deleted > 0 AS tuples_deleted
+FROM pg_stat_vacuum_indexes vt, pg_class c
+WHERE vt.relname = 'vestat_pkey' AND vt.relid = c.oid;
+SELECT vt.relname,relpages AS irp,pages_deleted AS ipd,tuples_deleted AS itd
+FROM pg_stat_vacuum_indexes vt, pg_class c
+WHERE vt.relname = 'vestat_pkey' AND vt.relid = c.oid \gset
+
+-- Store WAL advances into variables
+SELECT wal_records AS iwr,wal_bytes AS iwb,wal_fpi AS ifpi FROM pg_stat_vacuum_indexes WHERE relname = 'vestat_pkey' \gset
+
+-- Look into WAL records deltas.
+SELECT wal_records > 0 AS diWR, wal_bytes > 0 AS diWB
+FROM pg_stat_vacuum_indexes WHERE relname = 'vestat_pkey';
+
+DELETE FROM vestat;;
+VACUUM (PARALLEL 0, BUFFER_USAGE_LIMIT 128, INDEX_CLEANUP ON) vestat;
+-- it is necessary to check the wal statistics
+CHECKPOINT;
+
+-- pages_removed must be increased
+SELECT vt.relname,relpages-:irp = 0 AS relpages,pages_deleted-:ipd > 0 AS pages_deleted,tuples_deleted-:itd > 0 AS tuples_deleted
+FROM pg_stat_vacuum_indexes vt, pg_class c
+WHERE vt.relname = 'vestat_pkey' AND vt.relid = c.oid;
+SELECT vt.relname,relpages AS irp,pages_deleted AS ipd,tuples_deleted AS itd
+FROM pg_stat_vacuum_indexes vt, pg_class c
+WHERE vt.relname = 'vestat_pkey' AND vt.relid = c.oid \gset
+
+-- Store WAL advances into variables
+SELECT wal_records-:iwr AS diwr, wal_bytes-:iwb AS diwb, wal_fpi-:ifpi AS difpi
+FROM pg_stat_vacuum_indexes WHERE relname = 'vestat_pkey' \gset
+
+-- WAL advance should be detected.
+SELECT :diwr > 0 AS diWR, :diwb > 0 AS diWB;
+
+-- Store WAL advances into variables
+SELECT wal_records AS iwr,wal_bytes AS iwb,wal_fpi AS ifpi FROM pg_stat_vacuum_indexes WHERE relname = 'vestat_pkey' \gset
+
+INSERT INTO vestat SELECT x FROM generate_series(1,:sample_size) as x;
+DELETE FROM vestat WHERE x % 2 = 0;
+-- VACUUM FULL doesn't report to stat collector. So, no any advancements of statistics
+-- are detected here.
+VACUUM FULL vestat;
+-- It is necessary to check the wal statistics
+CHECKPOINT;
+
+-- Store WAL advances into variables
+SELECT wal_records-:iwr AS diwr2, wal_bytes-:iwb AS diwb2, wal_fpi-:ifpi AS difpi2
+FROM pg_stat_vacuum_indexes WHERE relname = 'vestat_pkey' \gset
+
+-- WAL and other statistics advance should not be detected.
+SELECT :diwr2=0 AS diWR, :difpi2=0 AS iFPI, :diwb2=0 AS diWB;
+
+SELECT vt.relname,relpages-:irp < 0 AS relpages,pages_deleted-:ipd = 0 AS pages_deleted,tuples_deleted-:itd = 0 AS tuples_deleted
+FROM pg_stat_vacuum_indexes vt, pg_class c
+WHERE vt.relname = 'vestat_pkey' AND vt.relid = c.oid;
+SELECT vt.relname,relpages AS irp,pages_deleted AS ipd,tuples_deleted AS itd
+FROM pg_stat_vacuum_indexes vt, pg_class c
+WHERE vt.relname = 'vestat_pkey' AND vt.relid = c.oid \gset
+
+-- Store WAL advances into variables
+SELECT wal_records AS iwr,wal_bytes AS iwb,wal_fpi AS ifpi FROM pg_stat_vacuum_indexes WHERE relname = 'vestat_pkey' \gset
+
+DELETE FROM vestat;
+TRUNCATE vestat;
+VACUUM (PARALLEL 0, BUFFER_USAGE_LIMIT 128, INDEX_CLEANUP ON) vestat;
+-- it is necessary to check the wal statistics
+CHECKPOINT;
+
+-- Store WAL advances into variables after removing all tuples from the table
+SELECT wal_records-:iwr AS diwr3, wal_bytes-:iwb AS diwb3, wal_fpi-:ifpi AS difpi3
+FROM pg_stat_vacuum_indexes WHERE relname = 'vestat_pkey' \gset
+
+--There are nothing changed
+SELECT :diwr3=0 AS diWR, :difpi3=0 AS iFPI, :diwb3=0 AS diWB;
+
+--
+-- Now, the table and index is compressed into zero number of pages. Check it
+-- in vacuum extended statistics.
+-- The pages_frozen, pages_scanned values shouldn't be changed
+--
+SELECT vt.relname,relpages-:irp = 0 AS relpages,pages_deleted-:ipd = 0 AS pages_deleted,tuples_deleted-:itd = 0 AS tuples_deleted
+FROM pg_stat_vacuum_indexes vt, pg_class c
+WHERE vt.relname = 'vestat_pkey' AND vt.relid = c.oid;
+
+DROP TABLE vestat;
diff --git a/src/test/regress/sql/vacuum_tables_statistics.sql b/src/test/regress/sql/vacuum_tables_statistics.sql
index 8ad69108ca1..dfd7af70027 100644
--- a/src/test/regress/sql/vacuum_tables_statistics.sql
+++ b/src/test/regress/sql/vacuum_tables_statistics.sql
@@ -148,14 +148,22 @@ SELECT vt.relname,vm_new_frozen_pages-:fp = 0 AS vm_new_frozen_pages,tuples_dele
 FROM pg_stat_vacuum_tables vt, pg_class c
 WHERE vt.relname = 'vestat' AND vt.relid = c.oid;
 
+DROP TABLE vestat;
+SELECT pg_stat_force_next_flush();
+SET vacuum_freeze_min_age = 0;
+SET vacuum_freeze_table_age = 0;
+CREATE TABLE vestat (x int) WITH (autovacuum_enabled = off, fillfactor = 10);
+
 INSERT INTO vestat SELECT x FROM generate_series(1,:sample_size) as x;
 ANALYZE vestat;
+SELECT pg_stat_force_next_flush();
 
 -- must be empty
 SELECT vm_new_frozen_pages, vm_new_visible_pages, rev_all_frozen_pages,rev_all_visible_pages,vm_new_visible_frozen_pages
 FROM pg_stat_vacuum_tables, pg_stat_all_tables WHERE pg_stat_vacuum_tables.relname = 'vestat' and pg_stat_vacuum_tables.relid = pg_stat_all_tables.relid;
 
 VACUUM (PARALLEL 0, BUFFER_USAGE_LIMIT 128) vestat;
+SELECT pg_stat_force_next_flush();
 
 -- backend defreezed pages
 SELECT vm_new_frozen_pages > 0 AS vm_new_frozen_pages,vm_new_visible_pages > 0 AS vm_new_visible_pages,vm_new_visible_frozen_pages > 0 AS vm_new_visible_frozen_pages,rev_all_frozen_pages = 0 AS rev_all_frozen_pages,rev_all_visible_pages = 0 AS rev_all_visible_pages
@@ -165,6 +173,7 @@ FROM pg_stat_vacuum_tables, pg_stat_all_tables WHERE pg_stat_vacuum_tables.relna
 
 UPDATE vestat SET x = x + 1001;
 VACUUM (PARALLEL 0, BUFFER_USAGE_LIMIT 128) vestat;
+SELECT pg_stat_force_next_flush();
 
 SELECT vm_new_frozen_pages > :pf AS vm_new_frozen_pages,vm_new_visible_pages > :pv AS vm_new_visible_pages,vm_new_visible_frozen_pages > :pvf AS vm_new_visible_frozen_pages,rev_all_frozen_pages > :hafp AS rev_all_frozen_pages,rev_all_visible_pages > :havp AS rev_all_visible_pages
 FROM pg_stat_vacuum_tables, pg_stat_all_tables WHERE pg_stat_vacuum_tables.relname = 'vestat' and pg_stat_vacuum_tables.relid = pg_stat_all_tables.relid;
@@ -172,9 +181,12 @@ SELECT vm_new_frozen_pages AS pf, vm_new_visible_pages AS pv, vm_new_visible_fro
 FROM pg_stat_vacuum_tables, pg_stat_all_tables WHERE pg_stat_vacuum_tables.relname = 'vestat' and pg_stat_vacuum_tables.relid = pg_stat_all_tables.relid \gset
 
 VACUUM (PARALLEL 0, BUFFER_USAGE_LIMIT 128) vestat;
+SELECT pg_stat_force_next_flush();
 
 -- vacuum freezed pages
 SELECT vm_new_frozen_pages = :pf AS vm_new_frozen_pages,vm_new_visible_pages = :pv AS vm_new_visible_pages,vm_new_visible_frozen_pages = :pvf AS vm_new_visible_frozen_pages, rev_all_frozen_pages = :hafp AS rev_all_frozen_pages,rev_all_visible_pages = :havp AS rev_all_visible_pages
 FROM pg_stat_vacuum_tables, pg_stat_all_tables WHERE pg_stat_vacuum_tables.relname = 'vestat' and pg_stat_vacuum_tables.relid = pg_stat_all_tables.relid;
 
+RESET vacuum_freeze_min_age;
+RESET vacuum_freeze_table_age;
 DROP TABLE vestat CASCADE;
-- 
2.34.1

From 143d926f778a70912d4e0a063d74c9b7bd5ff8d5 Mon Sep 17 00:00:00 2001
From: Alena Rybakina <[email-masked]>
Date: Fri, 10 Jan 2025 10:50:00 +0300
Subject: [PATCH 3/4] Machinery for grabbing an extended vacuum statistics on
 databases.

Database vacuum statistics information is the collected general
vacuum statistics indexes and tables owned by the databases, which
they belong to.

By default, vacuum database statistics are always collected, and
the track_vacuum_statistics guc enables the ability to collect
extended statistics for a given database's relations. This is done
to achieve a balance between the allocated memory for storing statistics
and having the necessary monitoring at hand to track the state of
vacuum operation.
In addition to the fact that there are far fewer databases in a system
than relations, vacuum statistics for a database contain fewer statistics
than relations, but they are enough to indicate that something may be
wrong in the system and prompt the administrator to enable extended
monitoring for relations.

So, buffer, wal, statistics of I/O time of read and writen blocks
statistics will be observed because they are collected for both
tables, indexes. In addition, we show the number of errors caught
during operation of the vacuum only for the error level.

wraparound_failsafe_count is a number of times when the vacuum starts
urgent cleanup to prevent wraparound problem which is critical for
the database.

Authors: Alena Rybakina <[email-masked]>,
   Andrei Lepikhov <[email-masked]>,
   Andrei Zubkov <[email-masked]>
Reviewed-by: Dilip Kumar <[email-masked]>, Masahiko Sawada <[email-masked]>,
       Ilia Evdokimov <[email-masked]>, jian he <[email-masked]>,
       Kirill Reshke <[email-masked]>, Alexander Korotkov <[email-masked]>,
       Jim Nasby <[email-masked]>, Sami Imseih <[email-masked]>
---
 src/backend/access/heap/vacuumlazy.c          |  85 ++++++---------
 src/backend/catalog/system_views.sql          |  27 ++++-
 src/backend/commands/vacuumparallel.c         |   2 +-
 src/backend/utils/activity/pgstat.c           |   2 +-
 src/backend/utils/activity/pgstat_database.c  |   1 +
 src/backend/utils/activity/pgstat_relation.c  |  52 ++++++++-
 src/backend/utils/adt/pgstatfuncs.c           | 100 +++++++++++++++++-
 src/backend/utils/misc/guc_tables.c           |   2 +-
 src/include/catalog/pg_proc.dat               |  13 ++-
 src/include/pgstat.h                          |   5 +-
 .../vacuum-extending-in-repetable-read.spec   |   6 ++
 src/test/regress/expected/rules.out           |  17 +++
 .../expected/vacuum_index_statistics.out      |  16 +--
 ...ut => vacuum_tables_and_db_statistics.out} |  87 +++++++++++++--
 src/test/regress/parallel_schedule            |   2 +-
 .../regress/sql/vacuum_index_statistics.sql   |   6 +-
 ...ql => vacuum_tables_and_db_statistics.sql} |  66 +++++++++++-
 17 files changed, 399 insertions(+), 90 deletions(-)
 rename src/test/regress/expected/{vacuum_tables_statistics.out => vacuum_tables_and_db_statistics.out} (83%)
 rename src/test/regress/sql/{vacuum_tables_statistics.sql => vacuum_tables_and_db_statistics.sql} (82%)

diff --git a/src/backend/access/heap/vacuumlazy.c b/src/backend/access/heap/vacuumlazy.c
index e7d2ccc668e..1f42c75b631 100644
--- a/src/backend/access/heap/vacuumlazy.c
+++ b/src/backend/access/heap/vacuumlazy.c
@@ -309,9 +309,6 @@ extvac_stats_start(Relation rel, LVExtStatCounters *counters)
 {
 	TimestampTz	starttime;
 
-	if(!pgstat_track_vacuum_statistics)
-		return;
-
 	memset(counters, 0, sizeof(LVExtStatCounters));
 
 	starttime = GetCurrentTimestamp();
@@ -350,9 +347,6 @@ extvac_stats_end(Relation rel, LVExtStatCounters *counters,
 	long		secs;
 	int			usecs;
 
-	if(!pgstat_track_vacuum_statistics)
-		return;
-
 	/* Calculate diffs of global stat parameters on WAL and buffer usage. */
 	memset(&walusage, 0, sizeof(WalUsage));
 	WalUsageAccumDiff(&walusage, &pgWalUsage, &counters->walusage);
@@ -400,9 +394,6 @@ void
 extvac_stats_start_idx(Relation rel, IndexBulkDeleteResult *stats,
 					   LVExtStatCountersIdx *counters)
 {
-	if(!pgstat_track_vacuum_statistics)
-		return;
-
 	extvac_stats_start(rel, &counters->common);
 	counters->pages_deleted = counters->tuples_removed = 0;
 
@@ -459,9 +450,6 @@ extvac_stats_end_idx(Relation rel, IndexBulkDeleteResult *stats,
 static void
 accumulate_heap_vacuum_statistics(LVExtStatCounters *extVacCounters, LVRelState *vacrel)
 {
-	if (!pgstat_track_vacuum_statistics)
-		return;
-
 	extvac_stats_end(vacrel->rel, extVacCounters, &(vacrel->extVacReport));
 
 	/* Fill heap-specific extended stats fields */
@@ -478,7 +466,7 @@ accumulate_heap_vacuum_statistics(LVExtStatCounters *extVacCounters, LVRelState
 	vacrel->extVacReport.table.missed_dead_tuples += vacrel->missed_dead_tuples;
 	vacrel->extVacReport.table.missed_dead_pages += vacrel->missed_dead_pages;
 	vacrel->extVacReport.table.index_vacuum_count += vacrel->num_index_scans;
-	vacrel->extVacReport.table.wraparound_failsafe_count += vacrel->wraparound_failsafe_count;
+	vacrel->extVacReport.wraparound_failsafe_count += vacrel->wraparound_failsafe_count;
 }
 
 /*
@@ -795,30 +783,14 @@ heap_vacuum_rel(Relation rel, VacuumParams *params,
 	 *
 	 * We are ready to send vacuum statistics information for heap relations.
 	 */
-	if(pgstat_track_vacuum_statistics)
-	{
-		/* Make generic extended vacuum stats report and
-		 * fill heap-specific extended stats fields.
-		 */
-		accumulate_heap_vacuum_statistics(&extVacCounters, vacrel);
+	accumulate_heap_vacuum_statistics(&extVacCounters, vacrel);
 
-		pgstat_report_vacuum(RelationGetRelid(rel),
-						 rel->rd_rel->relisshared,
-						 Max(vacrel->new_live_tuples, 0),
-						 vacrel->recently_dead_tuples +
-						 vacrel->missed_dead_tuples,
-						 &(vacrel->extVacReport));
-
-	}
-	else
-	{
-		pgstat_report_vacuum(RelationGetRelid(rel),
-							 rel->rd_rel->relisshared,
-							 Max(vacrel->new_live_tuples, 0),
-							 vacrel->recently_dead_tuples +
-							 vacrel->missed_dead_tuples,
-							 NULL);
-	}
+	pgstat_report_vacuum(RelationGetRelid(rel),
+						rel->rd_rel->relisshared,
+						Max(vacrel->new_live_tuples, 0),
+						vacrel->recently_dead_tuples +
+						vacrel->missed_dead_tuples,
+						&(vacrel->extVacReport));
 
 	pgstat_progress_end_command();
 
@@ -2808,14 +2780,11 @@ lazy_vacuum_one_index(Relation indrel, IndexBulkDeleteResult *istat,
 	istat = vac_bulkdel_one_index(&ivinfo, istat, vacrel->dead_items,
 								  vacrel->dead_items_info);
 
-	if(pgstat_track_vacuum_statistics)
-	{
-		/* Make extended vacuum stats report for index */
-		extvac_stats_end_idx(indrel, istat, &extVacCounters, &extVacReport);
-		pgstat_report_vacuum(RelationGetRelid(indrel),
-								indrel->rd_rel->relisshared,
-								0, 0, &extVacReport);
-	}
+	/* Make extended vacuum stats report for index */
+	extvac_stats_end_idx(indrel, istat, &extVacCounters, &extVacReport);
+	pgstat_report_vacuum(RelationGetRelid(indrel),
+							indrel->rd_rel->relisshared,
+							0, 0, &extVacReport);
 
 	/* Revert to the previous phase information for error traceback */
 	restore_vacuum_error_info(vacrel, &saved_err_info);
@@ -2872,14 +2841,11 @@ lazy_cleanup_one_index(Relation indrel, IndexBulkDeleteResult *istat,
 
 	istat = vac_cleanup_one_index(&ivinfo, istat);
 
-	if(pgstat_track_vacuum_statistics)
-	{
-		/* Make extended vacuum stats report for index */
-		extvac_stats_end_idx(indrel, istat, &extVacCounters, &extVacReport);
-		pgstat_report_vacuum(RelationGetRelid(indrel),
-								indrel->rd_rel->relisshared,
-								0, 0, &extVacReport);
-	}
+	/* Make extended vacuum stats report for index */
+	extvac_stats_end_idx(indrel, istat, &extVacCounters, &extVacReport);
+	pgstat_report_vacuum(RelationGetRelid(indrel),
+							indrel->rd_rel->relisshared,
+							0, 0, &extVacReport);
 
 	/* Revert to the previous phase information for error traceback */
 	restore_vacuum_error_info(vacrel, &saved_err_info);
@@ -3493,6 +3459,9 @@ vacuum_error_callback(void *arg)
 	switch (errinfo->phase)
 	{
 		case VACUUM_ERRCB_PHASE_SCAN_HEAP:
+			if(geterrelevel() == ERROR)
+					pgstat_report_vacuum_error(errinfo->reloid, PGSTAT_EXTVAC_TABLE);
+
 			if (BlockNumberIsValid(errinfo->blkno))
 			{
 				if (OffsetNumberIsValid(errinfo->offnum))
@@ -3508,6 +3477,9 @@ vacuum_error_callback(void *arg)
 			break;
 
 		case VACUUM_ERRCB_PHASE_VACUUM_HEAP:
+			if(geterrelevel() == ERROR)
+				pgstat_report_vacuum_error(errinfo->reloid, PGSTAT_EXTVAC_TABLE);
+
 			if (BlockNumberIsValid(errinfo->blkno))
 			{
 				if (OffsetNumberIsValid(errinfo->offnum))
@@ -3523,16 +3495,25 @@ vacuum_error_callback(void *arg)
 			break;
 
 		case VACUUM_ERRCB_PHASE_VACUUM_INDEX:
+			if(geterrelevel() == ERROR)
+				pgstat_report_vacuum_error(errinfo->indoid, PGSTAT_EXTVAC_INDEX);
+
 			errcontext("while vacuuming index \"%s\" of relation \"%s.%s\"",
 					   errinfo->indname, errinfo->relnamespace, errinfo->relname);
 			break;
 
 		case VACUUM_ERRCB_PHASE_INDEX_CLEANUP:
+			if(geterrelevel() == ERROR)
+				pgstat_report_vacuum_error(errinfo->indoid, PGSTAT_EXTVAC_INDEX);
+
 			errcontext("while cleaning up index \"%s\" of relation \"%s.%s\"",
 					   errinfo->indname, errinfo->relnamespace, errinfo->relname);
 			break;
 
 		case VACUUM_ERRCB_PHASE_TRUNCATE:
+			if(geterrelevel() == ERROR)
+				pgstat_report_vacuum_error(errinfo->reloid, PGSTAT_EXTVAC_TABLE);
+
 			if (BlockNumberIsValid(errinfo->blkno))
 				errcontext("while truncating relation \"%s.%s\" to %u blocks",
 						   errinfo->relnamespace, errinfo->relname, errinfo->blkno);
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index 09e93b21e82..9ece851f2c4 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -1462,4 +1462,29 @@ FROM
   pg_class rel
   JOIN pg_namespace ns ON ns.oid = rel.relnamespace,
   LATERAL pg_stat_get_vacuum_indexes(rel.oid) stats
-WHERE rel.relkind = 'i';
\ No newline at end of file
+WHERE rel.relkind = 'i';
+
+CREATE VIEW pg_stat_vacuum_database AS
+SELECT
+  db.oid as dboid,
+  db.datname AS dbname,
+
+  stats.db_blks_read AS db_blks_read,
+  stats.db_blks_hit AS db_blks_hit,
+  stats.total_blks_dirtied AS total_blks_dirtied,
+  stats.total_blks_written AS total_blks_written,
+
+  stats.wal_records AS wal_records,
+  stats.wal_fpi AS wal_fpi,
+  stats.wal_bytes AS wal_bytes,
+
+  stats.blk_read_time AS blk_read_time,
+  stats.blk_write_time AS blk_write_time,
+
+  stats.delay_time AS delay_time,
+  stats.total_time AS total_time,
+  stats.wraparound_failsafe AS wraparound_failsafe,
+  stats.errors AS errors
+FROM
+  pg_database db,
+  LATERAL pg_stat_get_vacuum_database(db.oid) stats;
\ No newline at end of file
diff --git a/src/backend/commands/vacuumparallel.c b/src/backend/commands/vacuumparallel.c
index d41711a80d5..8f288342c98 100644
--- a/src/backend/commands/vacuumparallel.c
+++ b/src/backend/commands/vacuumparallel.c
@@ -909,7 +909,7 @@ parallel_vacuum_process_one_index(ParallelVacuumState *pvs, Relation indrel,
 				 RelationGetRelationName(indrel));
 	}
 
-	if(istat_res != NULL && pgstat_track_vacuum_statistics)
+	if(istat_res != NULL)
 	{
 		/* Make extended vacuum stats report for index */
 		extvac_stats_end_idx(indrel, istat_res, &extVacCounters, &extVacReport);
diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index dc81bb12c86..c30543c5642 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -204,7 +204,7 @@ static inline bool pgstat_is_kind_valid(PgStat_Kind kind);
 
 bool		pgstat_track_counts = false;
 int			pgstat_fetch_consistency = PGSTAT_FETCH_CONSISTENCY_CACHE;
-bool		pgstat_track_vacuum_statistics = true;
+bool		pgstat_track_vacuum_statistics = false;
 
 /* ----------
  * state shared with pgstat_*.c
diff --git a/src/backend/utils/activity/pgstat_database.c b/src/backend/utils/activity/pgstat_database.c
index 05a8ccfdb75..d5c1e2a2cf5 100644
--- a/src/backend/utils/activity/pgstat_database.c
+++ b/src/backend/utils/activity/pgstat_database.c
@@ -449,6 +449,7 @@ pgstat_database_flush_cb(PgStat_EntryRef *entry_ref, bool nowait)
 	pgstat_unlock_entry(entry_ref);
 
 	memset(pendingent, 0, sizeof(*pendingent));
+	memset(&(pendingent)->vacuum_ext, 0, sizeof(ExtVacReport));
 
 	return true;
 }
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index b18ae610b84..2671269c01a 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -205,6 +205,38 @@ pgstat_drop_relation(Relation rel)
 	}
 }
 
+/* ---------
+ * pgstat_report_vacuum_error() -
+ *
+ *	Tell the collector about an (auto)vacuum interruption.
+ * ---------
+ */
+void
+pgstat_report_vacuum_error(Oid tableoid, ExtVacReportType m_type)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	Oid			dboid =  MyDatabaseId;
+	PgStat_StatDBEntry *dbentry;	/* pending database entry */
+
+	if (!pgstat_track_counts)
+		return;
+
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											dboid, tableoid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	tabentry->vacuum_ext.type = m_type;
+	pgstat_unlock_entry(entry_ref);
+
+	dbentry = pgstat_prep_database_pending(dboid);
+	dbentry->vacuum_ext.errors++;
+	dbentry->vacuum_ext.type = m_type;
+}
+
 /*
  * Report that the table was just vacuumed and flush IO statistics.
  */
@@ -216,6 +248,7 @@ pgstat_report_vacuum(Oid tableoid, bool shared,
 	PgStat_EntryRef *entry_ref;
 	PgStatShared_Relation *shtabentry;
 	PgStat_StatTabEntry *tabentry;
+	PgStatShared_Database *dbentry;
 	Oid			dboid = (shared ? InvalidOid : MyDatabaseId);
 	TimestampTz ts;
 
@@ -235,7 +268,8 @@ pgstat_report_vacuum(Oid tableoid, bool shared,
 	tabentry->live_tuples = livetuples;
 	tabentry->dead_tuples = deadtuples;
 
-	pgstat_accumulate_extvac_stats(&tabentry->vacuum_ext, params, true);
+	if(pgstat_track_vacuum_statistics)
+		pgstat_accumulate_extvac_stats(&tabentry->vacuum_ext, params, true);
 
 	/*
 	 * It is quite possible that a non-aggressive VACUUM ended up skipping
@@ -270,6 +304,16 @@ pgstat_report_vacuum(Oid tableoid, bool shared,
 	 */
 	pgstat_flush_io(false);
 	pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+
+	if (dboid != InvalidOid)
+	{
+		entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_DATABASE,
+											dboid, InvalidOid, false);
+		dbentry = (PgStatShared_Database *) entry_ref->shared_stats;
+
+		pgstat_accumulate_extvac_stats(&dbentry->stats.vacuum_ext, params, false);
+		pgstat_unlock_entry(entry_ref);
+	}
 }
 
 /*
@@ -995,9 +1039,6 @@ static void
 pgstat_accumulate_extvac_stats(ExtVacReport *dst, ExtVacReport *src,
 							   bool accumulate_reltype_specific_info)
 {
-	if(!pgstat_track_vacuum_statistics)
-		return;
-
 	dst->total_blks_read += src->total_blks_read;
 	dst->total_blks_hit += src->total_blks_hit;
 	dst->total_blks_dirtied += src->total_blks_dirtied;
@@ -1009,6 +1050,8 @@ pgstat_accumulate_extvac_stats(ExtVacReport *dst, ExtVacReport *src,
 	dst->blk_write_time += src->blk_write_time;
 	dst->delay_time += src->delay_time;
 	dst->total_time += src->total_time;
+	dst->wraparound_failsafe_count += src->wraparound_failsafe_count;
+	dst->errors += src->errors;
 
 	if (!accumulate_reltype_specific_info)
 		return;
@@ -1036,7 +1079,6 @@ pgstat_accumulate_extvac_stats(ExtVacReport *dst, ExtVacReport *src,
 			dst->table.index_vacuum_count += src->table.index_vacuum_count;
 			dst->table.missed_dead_pages += src->table.missed_dead_pages;
 			dst->table.missed_dead_tuples += src->table.missed_dead_tuples;
-			dst->table.wraparound_failsafe_count += src->table.wraparound_failsafe_count;
 		}
 		else if (dst->type == PGSTAT_EXTVAC_INDEX)
 		{
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 3105a9de390..de279767db2 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -2297,7 +2297,7 @@ pg_stat_get_vacuum_tables(PG_FUNCTION_ARGS)
 	values[i++] = Int64GetDatum(extvacuum->table.recently_dead_tuples);
 	values[i++] = Int64GetDatum(extvacuum->table.missed_dead_tuples);
 
-	values[i++] = Int32GetDatum(extvacuum->table.wraparound_failsafe_count);
+	values[i++] = Int32GetDatum(extvacuum->wraparound_failsafe_count);
 	values[i++] = Int64GetDatum(extvacuum->table.index_vacuum_count);
 
 	values[i++] = Int64GetDatum(extvacuum->wal_records);
@@ -2427,6 +2427,104 @@ pg_stat_get_vacuum_indexes(PG_FUNCTION_ARGS)
 
 	Assert(i == PG_STAT_GET_VACUUM_INDEX_STATS_COLS);
 
+	/* Returns the record as Datum */
+	PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
+Datum
+pg_stat_get_vacuum_database(PG_FUNCTION_ARGS)
+{
+	#define PG_STAT_GET_VACUUM_DATABASE_STATS_COLS	14
+
+	Oid						 dbid = PG_GETARG_OID(0);
+	PgStat_StatDBEntry 		*dbentry;
+	ExtVacReport 			*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;
+	ExtVacReport allzero;
+
+	/* Initialise attributes information in the tuple descriptor */
+	tupdesc = CreateTemplateTupleDesc(PG_STAT_GET_VACUUM_DATABASE_STATS_COLS);
+
+	TupleDescInitEntry(tupdesc, (AttrNumber) ++i, "dbid",
+					   INT4OID, -1, 0);
+	TupleDescInitEntry(tupdesc, (AttrNumber) ++i, "total_ blks_read",
+					   INT8OID, -1, 0);
+	TupleDescInitEntry(tupdesc, (AttrNumber) ++i, "total_blks_hit",
+					   INT8OID, -1, 0);
+	TupleDescInitEntry(tupdesc, (AttrNumber) ++i, "total_blks_dirtied",
+					   INT8OID, -1, 0);
+	TupleDescInitEntry(tupdesc, (AttrNumber) ++i, "total_blks_written",
+					   INT8OID, -1, 0);
+	TupleDescInitEntry(tupdesc, (AttrNumber) ++i, "wal_records",
+					   INT8OID, -1, 0);
+	TupleDescInitEntry(tupdesc, (AttrNumber) ++i, "wal_fpi",
+					   INT8OID, -1, 0);
+	TupleDescInitEntry(tupdesc, (AttrNumber) ++i, "wal_bytes",
+					   NUMERICOID, -1, 0);
+
+	TupleDescInitEntry(tupdesc, (AttrNumber) ++i, "blk_read_time",
+					   FLOAT8OID, -1, 0);
+	TupleDescInitEntry(tupdesc, (AttrNumber) ++i, "blk_write_time",
+					   FLOAT8OID, -1, 0);
+
+	TupleDescInitEntry(tupdesc, (AttrNumber) ++i, "delay_time",
+					   FLOAT8OID, -1, 0);
+	TupleDescInitEntry(tupdesc, (AttrNumber) ++i, "total_time",
+					   FLOAT8OID, -1, 0);
+	TupleDescInitEntry(tupdesc, (AttrNumber) ++i, "wraparound_failsafe_count",
+					   INT4OID, -1, 0);
+	TupleDescInitEntry(tupdesc, (AttrNumber) ++i, "errors",
+					   INT4OID, -1, 0);
+
+	Assert(i == PG_STAT_GET_VACUUM_DATABASE_STATS_COLS);
+
+	BlessTupleDesc(tupdesc);
+
+	dbentry = pgstat_fetch_stat_dbentry(dbid);
+
+	if (dbentry == NULL)
+	{
+		/* If the subscription is not found, initialise its stats */
+		memset(&allzero, 0, sizeof(ExtVacReport));
+		extvacuum = &allzero;
+	}
+	else
+	{
+		extvacuum = &(dbentry->vacuum_ext);
+	}
+
+	i = 0;
+
+	values[i++] = ObjectIdGetDatum(dbid);
+
+	values[i++] = Int64GetDatum(extvacuum->total_blks_read);
+	values[i++] = Int64GetDatum(extvacuum->total_blks_hit);
+	values[i++] = Int64GetDatum(extvacuum->total_blks_dirtied);
+	values[i++] = Int64GetDatum(extvacuum->total_blks_written);
+
+	values[i++] = Int64GetDatum(extvacuum->wal_records);
+	values[i++] = Int64GetDatum(extvacuum->wal_fpi);
+
+	/* Convert to numeric, like pg_stat_statements */
+	snprintf(buf, sizeof buf, UINT64_FORMAT, extvacuum->wal_bytes);
+	values[i++] = DirectFunctionCall3(numeric_in,
+									  CStringGetDatum(buf),
+									  ObjectIdGetDatum(0),
+									  Int32GetDatum(-1));
+
+	values[i++] = Float8GetDatum(extvacuum->blk_read_time);
+	values[i++] = Float8GetDatum(extvacuum->blk_write_time);
+	values[i++] = Float8GetDatum(extvacuum->delay_time);
+	values[i++] = Float8GetDatum(extvacuum->total_time);
+	values[i++] = Int32GetDatum(extvacuum->wraparound_failsafe_count);
+	values[i++] = Int32GetDatum(extvacuum->errors);
+
+	Assert(i == PG_STAT_GET_VACUUM_DATABASE_STATS_COLS);
+
 	/* Returns the record as Datum */
 	PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
 }
\ No newline at end of file
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 2a96cf51a36..ed973703763 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -1484,7 +1484,7 @@ struct config_bool ConfigureNamesBool[] =
 			NULL
 		},
 		&pgstat_track_vacuum_statistics,
-		true,
+		false,
 		NULL, NULL, NULL
 	},
 	{
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index d7f06a69487..7261f7bbeaa 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12448,12 +12448,21 @@
   proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_rev_all_frozen_pages' },
 { oid => '8004',
-  descr => 'pg_stat_get_vacuum_indexes return stats values',
+  descr => 'pg_stat_get_vacuum_indexes returns vacuum stats values for index',
   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,float8,float8,float8,float8}',
   proargmodes => '{i,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o}',
   proargnames => '{reloid,relid,total_blks_read,total_blks_hit,total_blks_dirtied,total_blks_written,rel_blks_read,rel_blks_hit,pages_deleted,tuples_deleted,wal_records,wal_fpi,wal_bytes,blk_read_time,blk_write_time,delay_time,total_time}',
-  prosrc => 'pg_stat_get_vacuum_indexes' }
+  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,int8,int8,int8,int8,int8,int8,numeric,float8,float8,float8,float8,int4,int4}',
+  proargmodes => '{i,o,o,o,o,o,o,o,o,o,o,o,o,o,o}',
+  proargnames => '{dbid,dboid,db_blks_read,db_blks_hit,total_blks_dirtied,total_blks_written,wal_records,wal_fpi,wal_bytes,blk_read_time,blk_write_time,delay_time,total_time,wraparound_failsafe,errors}',
+  prosrc => 'pg_stat_get_vacuum_database' },
 ]
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 572e34e0bb0..857a5c8f861 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -210,6 +210,9 @@ typedef struct ExtVacReport
 
 	int64		tuples_deleted;		/* tuples deleted by vacuum */
 
+	int32		errors;
+	int32		wraparound_failsafe_count;	/* the number of times to prevent wraparound problem */
+
 	ExtVacReportType type;		/* heap, index, etc. */
 
 	/* ----------
@@ -239,7 +242,6 @@ typedef struct ExtVacReport
 			int64		missed_dead_tuples;		/* tuples not pruned by vacuum due to failure to get a cleanup lock */
 			int64		missed_dead_pages;		/* pages with missed dead tuples */
 			int64		index_vacuum_count;	/* number of index vacuumings */
-			int32		wraparound_failsafe_count;	/* the number of times to prevent workaround problem */
 		}			table;
 		struct
 		{
@@ -779,6 +781,7 @@ extern void pgstat_report_vacuum(Oid tableoid, bool shared,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter);
+extern void pgstat_report_vacuum_error(Oid tableoid, ExtVacReportType m_type);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/isolation/specs/vacuum-extending-in-repetable-read.spec b/src/test/isolation/specs/vacuum-extending-in-repetable-read.spec
index 5893d89573d..cfec3159580 100644
--- a/src/test/isolation/specs/vacuum-extending-in-repetable-read.spec
+++ b/src/test/isolation/specs/vacuum-extending-in-repetable-read.spec
@@ -18,6 +18,9 @@ teardown
 }
 
 session s1
+setup		{
+    SET track_vacuum_statistics TO 'on';
+    }
 step s1_begin_repeatable_read   {
   BEGIN transaction ISOLATION LEVEL REPEATABLE READ;
   select count(ival) from test_vacuum_stat_isolation where id>900;
@@ -25,6 +28,9 @@ step s1_begin_repeatable_read   {
 step s1_commit                  { COMMIT; }
 
 session s2
+setup		{
+    SET track_vacuum_statistics TO 'on';
+    }
 step s2_insert                  { INSERT INTO test_vacuum_stat_isolation(id, ival) SELECT ival, ival%10 FROM generate_series(1,1000) As ival; }
 step s2_update                  { UPDATE test_vacuum_stat_isolation SET ival = ival + 2 where id > 900; }
 step s2_delete                  { DELETE FROM test_vacuum_stat_isolation where id > 900; }
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 247e22a82a5..285dfd3840e 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -2245,6 +2245,23 @@ pg_stat_user_tables| SELECT relid,
     rev_all_visible_pages
    FROM pg_stat_all_tables
   WHERE ((schemaname <> ALL (ARRAY['pg_catalog'::name, 'information_schema'::name])) AND (schemaname !~ '^pg_toast'::text));
+pg_stat_vacuum_database| SELECT db.oid AS dboid,
+    db.datname AS dbname,
+    stats.db_blks_read,
+    stats.db_blks_hit,
+    stats.total_blks_dirtied,
+    stats.total_blks_written,
+    stats.wal_records,
+    stats.wal_fpi,
+    stats.wal_bytes,
+    stats.blk_read_time,
+    stats.blk_write_time,
+    stats.delay_time,
+    stats.total_time,
+    stats.wraparound_failsafe,
+    stats.errors
+   FROM pg_database db,
+    LATERAL pg_stat_get_vacuum_database(db.oid) stats(dboid, db_blks_read, db_blks_hit, total_blks_dirtied, total_blks_written, wal_records, wal_fpi, wal_bytes, blk_read_time, blk_write_time, delay_time, total_time, wraparound_failsafe, errors);
 pg_stat_vacuum_indexes| SELECT rel.oid AS relid,
     ns.nspname AS schemaname,
     rel.relname,
diff --git a/src/test/regress/expected/vacuum_index_statistics.out b/src/test/regress/expected/vacuum_index_statistics.out
index e00a0fc683c..9e5d33342c9 100644
--- a/src/test/regress/expected/vacuum_index_statistics.out
+++ b/src/test/regress/expected/vacuum_index_statistics.out
@@ -16,8 +16,12 @@ SHOW track_counts;  -- must be on
 \set sample_size 10000
 -- not enabled by default, but we want to test it...
 SET track_functions TO 'all';
--- Test that vacuum statistics will be empty when parameter is off.
-SET track_vacuum_statistics TO 'off';
+SHOW track_vacuum_statistics;  -- must be off
+ track_vacuum_statistics 
+-------------------------
+ off
+(1 row)
+
 CREATE TABLE vestat (x int) WITH (autovacuum_enabled = off, fillfactor = 10);
 INSERT INTO vestat SELECT x FROM generate_series(1,:sample_size) as x;
 ANALYZE vestat;
@@ -33,12 +37,7 @@ WHERE vt.relname = 'vestat';
 
 RESET track_vacuum_statistics;
 DROP TABLE vestat CASCADE;
-SHOW track_vacuum_statistics;  -- must be on
- track_vacuum_statistics 
--------------------------
- on
-(1 row)
-
+SET track_vacuum_statistics TO 'on';
 -- ensure pending stats are flushed
 SELECT pg_stat_force_next_flush();
  pg_stat_force_next_flush 
@@ -181,3 +180,4 @@ WHERE vt.relname = 'vestat_pkey' AND vt.relid = c.oid;
 (1 row)
 
 DROP TABLE vestat;
+RESET track_vacuum_statistics;
diff --git a/src/test/regress/expected/vacuum_tables_statistics.out b/src/test/regress/expected/vacuum_tables_and_db_statistics.out
similarity index 83%
rename from src/test/regress/expected/vacuum_tables_statistics.out
rename to src/test/regress/expected/vacuum_tables_and_db_statistics.out
index 119c7abea5f..d7f0bb32252 100644
--- a/src/test/regress/expected/vacuum_tables_statistics.out
+++ b/src/test/regress/expected/vacuum_tables_and_db_statistics.out
@@ -6,7 +6,6 @@
 -- number of frozen and visible pages removed by backend.
 -- Statistic wal_fpi is not displayed in this test because its behavior is unstable.
 --
--- conditio sine qua non
 SHOW track_counts;  -- must be on
  track_counts 
 --------------
@@ -16,8 +15,12 @@ SHOW track_counts;  -- must be on
 \set sample_size 10000
 -- not enabled by default, but we want to test it...
 SET track_functions TO 'all';
--- Test that vacuum statistics will be empty when parameter is off.
-SET track_vacuum_statistics TO 'off';
+SHOW track_vacuum_statistics;  -- must be off
+ track_vacuum_statistics 
+-------------------------
+ off
+(1 row)
+
 CREATE TABLE vestat (x int) WITH (autovacuum_enabled = off, fillfactor = 10);
 INSERT INTO vestat SELECT x FROM generate_series(1,:sample_size) as x;
 ANALYZE vestat;
@@ -37,12 +40,12 @@ WHERE vt.relname = 'vestat';
 
 RESET track_vacuum_statistics;
 DROP TABLE vestat CASCADE;
-SHOW track_vacuum_statistics;  -- must be on
- track_vacuum_statistics 
--------------------------
- on
-(1 row)
-
+CREATE DATABASE regression_statistic_vacuum_db;
+CREATE DATABASE regression_statistic_vacuum_db1;
+\c regression_statistic_vacuum_db;
+SET track_vacuum_statistics TO on;
+-- not enabled by default, but we want to test it...
+SET track_functions TO 'all';
 -- ensure pending stats are flushed
 SELECT pg_stat_force_next_flush();
  pg_stat_force_next_flush 
@@ -259,3 +262,69 @@ FROM pg_stat_vacuum_tables, pg_stat_all_tables WHERE pg_stat_vacuum_tables.relna
 RESET vacuum_freeze_min_age;
 RESET vacuum_freeze_table_age;
 DROP TABLE vestat CASCADE;
+-- Now check vacuum statistics for current database
+SELECT dbname,
+       db_blks_hit > 0 AS db_blks_hit,
+       total_blks_dirtied > 0 AS total_blks_dirtied,
+       total_blks_written > 0 AS total_blks_written,
+       wal_records > 0 AS wal_records,
+       wal_fpi > 0 AS wal_fpi,
+       wal_bytes > 0 AS wal_bytes,
+       total_time > 0 AS total_time
+FROM
+pg_stat_vacuum_database
+WHERE dbname = current_database();
+             dbname             | db_blks_hit | total_blks_dirtied | total_blks_written | wal_records | wal_fpi | wal_bytes | total_time 
+--------------------------------+-------------+--------------------+--------------------+-------------+---------+-----------+------------
+ regression_statistic_vacuum_db | t           | t                  | t                  | t           | t       | t         | t
+(1 row)
+
+-- ensure pending stats are flushed
+SELECT pg_stat_force_next_flush();
+ pg_stat_force_next_flush 
+--------------------------
+ 
+(1 row)
+
+CREATE TABLE vestat (x int) WITH (autovacuum_enabled = off, fillfactor = 10);
+INSERT INTO vestat SELECT x FROM generate_series(1,:sample_size) as x;
+ANALYZE vestat;
+UPDATE vestat SET x = 10001;
+VACUUM (PARALLEL 0, BUFFER_USAGE_LIMIT 128) vestat;
+\c regression_statistic_vacuum_db1;
+SET track_vacuum_statistics TO on;
+-- Now check vacuum statistics for postgres database from another database
+SELECT dbname,
+       db_blks_hit > 0 AS db_blks_hit,
+       total_blks_dirtied > 0 AS total_blks_dirtied,
+       total_blks_written > 0 AS total_blks_written,
+       wal_records > 0 AS wal_records,
+       wal_fpi > 0 AS wal_fpi,
+       wal_bytes > 0 AS wal_bytes,
+       total_time > 0 AS total_time
+FROM
+pg_stat_vacuum_database
+WHERE dbname = 'regression_statistic_vacuum_db';
+             dbname             | db_blks_hit | total_blks_dirtied | total_blks_written | wal_records | wal_fpi | wal_bytes | total_time 
+--------------------------------+-------------+--------------------+--------------------+-------------+---------+-----------+------------
+ regression_statistic_vacuum_db | t           | t                  | t                  | t           | t       | t         | t
+(1 row)
+
+\c regression_statistic_vacuum_db
+SET track_vacuum_statistics TO on;
+DROP TABLE vestat CASCADE;
+\c regression_statistic_vacuum_db1;
+SET track_vacuum_statistics TO on;
+SELECT count(*)
+FROM pg_database d
+CROSS JOIN pg_stat_get_vacuum_tables(0)
+WHERE oid = 0; -- must be 0
+ count 
+-------
+     0
+(1 row)
+
+\c postgres
+DROP DATABASE regression_statistic_vacuum_db1;
+DROP DATABASE regression_statistic_vacuum_db;
+RESET track_vacuum_statistics;
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 977a87a5b1f..19c76b96830 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -141,4 +141,4 @@ test: tablespace
 # Check vacuum statistics
 # ----------
 test: vacuum_index_statistics
-test: vacuum_tables_statistics
\ No newline at end of file
+test: vacuum_tables_and_db_statistics
\ No newline at end of file
diff --git a/src/test/regress/sql/vacuum_index_statistics.sql b/src/test/regress/sql/vacuum_index_statistics.sql
index ae146e1d23f..9b7e645187d 100644
--- a/src/test/regress/sql/vacuum_index_statistics.sql
+++ b/src/test/regress/sql/vacuum_index_statistics.sql
@@ -14,8 +14,7 @@ SHOW track_counts;  -- must be on
 -- not enabled by default, but we want to test it...
 SET track_functions TO 'all';
 
--- Test that vacuum statistics will be empty when parameter is off.
-SET track_vacuum_statistics TO 'off';
+SHOW track_vacuum_statistics;  -- must be off
 
 CREATE TABLE vestat (x int) WITH (autovacuum_enabled = off, fillfactor = 10);
 INSERT INTO vestat SELECT x FROM generate_series(1,:sample_size) as x;
@@ -33,7 +32,7 @@ WHERE vt.relname = 'vestat';
 RESET track_vacuum_statistics;
 DROP TABLE vestat CASCADE;
 
-SHOW track_vacuum_statistics;  -- must be on
+SET track_vacuum_statistics TO 'on';
 
 -- ensure pending stats are flushed
 SELECT pg_stat_force_next_flush();
@@ -149,3 +148,4 @@ FROM pg_stat_vacuum_indexes vt, pg_class c
 WHERE vt.relname = 'vestat_pkey' AND vt.relid = c.oid;
 
 DROP TABLE vestat;
+RESET track_vacuum_statistics;
diff --git a/src/test/regress/sql/vacuum_tables_statistics.sql b/src/test/regress/sql/vacuum_tables_and_db_statistics.sql
similarity index 82%
rename from src/test/regress/sql/vacuum_tables_statistics.sql
rename to src/test/regress/sql/vacuum_tables_and_db_statistics.sql
index dfd7af70027..06d2d683251 100644
--- a/src/test/regress/sql/vacuum_tables_statistics.sql
+++ b/src/test/regress/sql/vacuum_tables_and_db_statistics.sql
@@ -7,15 +7,13 @@
 -- Statistic wal_fpi is not displayed in this test because its behavior is unstable.
 --
 
--- conditio sine qua non
 SHOW track_counts;  -- must be on
 \set sample_size 10000
 
 -- not enabled by default, but we want to test it...
 SET track_functions TO 'all';
 
--- Test that vacuum statistics will be empty when parameter is off.
-SET track_vacuum_statistics TO 'off';
+SHOW track_vacuum_statistics;  -- must be off
 
 CREATE TABLE vestat (x int) WITH (autovacuum_enabled = off, fillfactor = 10);
 INSERT INTO vestat SELECT x FROM generate_series(1,:sample_size) as x;
@@ -36,7 +34,13 @@ WHERE vt.relname = 'vestat';
 RESET track_vacuum_statistics;
 DROP TABLE vestat CASCADE;
 
-SHOW track_vacuum_statistics;  -- must be on
+CREATE DATABASE regression_statistic_vacuum_db;
+CREATE DATABASE regression_statistic_vacuum_db1;
+\c regression_statistic_vacuum_db;
+SET track_vacuum_statistics TO on;
+
+-- not enabled by default, but we want to test it...
+SET track_functions TO 'all';
 
 -- ensure pending stats are flushed
 SELECT pg_stat_force_next_flush();
@@ -190,3 +194,57 @@ FROM pg_stat_vacuum_tables, pg_stat_all_tables WHERE pg_stat_vacuum_tables.relna
 RESET vacuum_freeze_min_age;
 RESET vacuum_freeze_table_age;
 DROP TABLE vestat CASCADE;
+-- Now check vacuum statistics for current database
+SELECT dbname,
+       db_blks_hit > 0 AS db_blks_hit,
+       total_blks_dirtied > 0 AS total_blks_dirtied,
+       total_blks_written > 0 AS total_blks_written,
+       wal_records > 0 AS wal_records,
+       wal_fpi > 0 AS wal_fpi,
+       wal_bytes > 0 AS wal_bytes,
+       total_time > 0 AS total_time
+FROM
+pg_stat_vacuum_database
+WHERE dbname = current_database();
+
+-- ensure pending stats are flushed
+SELECT pg_stat_force_next_flush();
+
+CREATE TABLE vestat (x int) WITH (autovacuum_enabled = off, fillfactor = 10);
+INSERT INTO vestat SELECT x FROM generate_series(1,:sample_size) as x;
+ANALYZE vestat;
+UPDATE vestat SET x = 10001;
+VACUUM (PARALLEL 0, BUFFER_USAGE_LIMIT 128) vestat;
+
+\c regression_statistic_vacuum_db1;
+SET track_vacuum_statistics TO on;
+
+-- Now check vacuum statistics for postgres database from another database
+SELECT dbname,
+       db_blks_hit > 0 AS db_blks_hit,
+       total_blks_dirtied > 0 AS total_blks_dirtied,
+       total_blks_written > 0 AS total_blks_written,
+       wal_records > 0 AS wal_records,
+       wal_fpi > 0 AS wal_fpi,
+       wal_bytes > 0 AS wal_bytes,
+       total_time > 0 AS total_time
+FROM
+pg_stat_vacuum_database
+WHERE dbname = 'regression_statistic_vacuum_db';
+
+\c regression_statistic_vacuum_db
+SET track_vacuum_statistics TO on;
+
+DROP TABLE vestat CASCADE;
+
+\c regression_statistic_vacuum_db1;
+SET track_vacuum_statistics TO on;
+SELECT count(*)
+FROM pg_database d
+CROSS JOIN pg_stat_get_vacuum_tables(0)
+WHERE oid = 0; -- must be 0
+
+\c postgres
+DROP DATABASE regression_statistic_vacuum_db1;
+DROP DATABASE regression_statistic_vacuum_db;
+RESET track_vacuum_statistics;
-- 
2.34.1

From a69869b450bb4bf3023a58e9c49e4c1b852693dc Mon Sep 17 00:00:00 2001
From: Alena Rybakina <[email-masked]>
Date: Thu, 19 Dec 2024 12:57:49 +0300
Subject: [PATCH 4/4] Add documentation about the system views that are used in
 the machinery of vacuum statistics.

---
 doc/src/sgml/system-views.sgml | 755 +++++++++++++++++++++++++++++++++
 1 file changed, 755 insertions(+)

diff --git a/doc/src/sgml/system-views.sgml b/doc/src/sgml/system-views.sgml
index a586156614d..d59f1944cef 100644
--- a/doc/src/sgml/system-views.sgml
+++ b/doc/src/sgml/system-views.sgml
@@ -5066,4 +5066,759 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx
   </table>
  </sect1>
 
+<sect1 id="view-pg-stat-vacuum-database">
+  <title><structname>pg_stat_vacuum_database</structname></title>
+
+  <indexterm zone="view-pg-stat-vacuum-database">
+   <primary>pg_stat_vacuum_database</primary>
+  </indexterm>
+
+  <para>
+   The view <structname>pg_stat_vacuum_database</structname> will contain
+   one row for each database in the current cluster, showing statistics about
+   vacuuming that database.
+  </para>
+
+  <table>
+   <title><structname>pg_stat_vacuum_database</structname> Columns</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>dbid</structfield> <type>oid</type>
+      </para>
+      <para>
+       OID of a database
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>total_blks_read</structfield> <type>int8</type>
+      </para>
+      <para>
+        Number of database blocks read by vacuum operations
+        performed on this database
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>total_blks_hit</structfield> <type>int8</type>
+      </para>
+      <para>
+        Number of times database blocks were found in the
+        buffer cache by vacuum operations
+        performed on this database
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>total_blks_dirtied</structfield> <type>int8</type>
+      </para>
+      <para>
+        Number of database blocks dirtied by vacuum operations
+        performed on this database
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>total_blks_written</structfield> <type>int8</type>
+      </para>
+      <para>
+        Number of database blocks written by vacuum operations
+        performed on this database
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>wal_records</structfield> <type>int8</type>
+      </para>
+      <para>
+        Total number of WAL records generated by vacuum operations
+        performed on this database
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>wal_fpi</structfield> <type>int8</type>
+      </para>
+      <para>
+        Total number of WAL full page images generated by vacuum operations
+        performed on 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 bytes generated by vacuum operations
+        performed on this database
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>blk_read_time</structfield> <type>float8</type>
+      </para>
+      <para>
+        Time spent reading database blocks by vacuum operations performed on
+        this database, in milliseconds (if <xref linkend="guc-track-io-timing"/> is enabled,
+        otherwise zero)
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>blk_write_time</structfield> <type>float8</type>
+      </para>
+      <para>
+        Time spent writing database blocks by vacuum operations performed on
+        this database, in milliseconds (if <xref linkend="guc-track-io-timing"/> is enabled,
+        otherwise zero)
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>delay_time</structfield> <type>float8</type>
+      </para>
+      <para>
+        Time spent sleeping in a vacuum delay point by vacuum operations performed on
+        this database, in milliseconds (see <xref linkend="runtime-config-resource-vacuum-cost"/>
+        for details)
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>system_time</structfield> <type>float8</type>
+      </para>
+      <para>
+        System CPU time of vacuuming this database, in milliseconds
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>user_time</structfield> <type>float8</type>
+      </para>
+      <para>
+        User CPU time of vacuuming this database, in milliseconds
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>total_time</structfield> <type>float8</type>
+      </para>
+      <para>
+        Total time of vacuuming this database, in milliseconds
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>wraparound_failsafe_count</structfield> <type>int4</type>
+      </para>
+      <para>
+        Number of times the vacuum was run to prevent a wraparound problem.
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>errors</structfield> <type>int4</type>
+      </para>
+      <para>
+        Number of times vacuum operations performed on this database
+        were interrupted on any errors
+      </para></entry>
+     </row>
+    </tbody>
+   </tgroup>
+  </table>
+ </sect1>
+
+  <sect1 id="view-pg-stat-vacuum-indexes">
+  <title><structname>pg_stat_vacuum_indexes</structname></title>
+
+  <indexterm zone="view-pg-stat-vacuum-indexes">
+   <primary>pg_stat_vacuum_indexes</primary>
+  </indexterm>
+
+  <para>
+   The view <structname>pg_stat_vacuum_indexes</structname> will contain
+   one row for each index in the current database (including TOAST
+   table indexes), showing statistics about vacuuming that specific index.
+  </para>
+
+  <table>
+   <title><structname>pg_stat_vacuum_indexes</structname> Columns</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>relid</structfield> <type>oid</type>
+      </para>
+      <para>
+       OID of an index
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>schema</structfield> <type>name</type>
+      </para>
+      <para>
+        Name of the schema this index is in
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>relname</structfield> <type>name</type>
+      </para>
+      <para>
+       Name of this index
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>total_blks_read</structfield> <type>int8</type>
+      </para>
+      <para>
+        Number of database blocks read by vacuum operations
+        performed on this index
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>total_blks_hit</structfield> <type>int8</type>
+      </para>
+      <para>
+        Number of times database blocks were found in the
+        buffer cache by vacuum operations
+        performed on this index
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>total_blks_dirtied</structfield> <type>int8</type>
+      </para>
+      <para>
+        Number of database blocks dirtied by vacuum operations
+        performed on this index
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>total_blks_written</structfield> <type>int8</type>
+      </para>
+      <para>
+        Number of database blocks written by vacuum operations
+        performed on this index
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>rel_blks_read</structfield> <type>int8</type>
+      </para>
+      <para>
+        Number of blocks vacuum operations read from this
+        index
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>rel_blks_hit</structfield> <type>int8</type>
+      </para>
+      <para>
+        Number of times blocks of this index were already found
+        in the buffer cache by vacuum operations, so that a read was not necessary
+        (this only includes hits in the
+        project; buffer cache, not the operating system's file system cache)
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>pages_deleted</structfield> <type>int8</type>
+      </para>
+      <para>
+        Number of pages deleted by vacuum operations
+        performed on this index
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>tuples_deleted</structfield> <type>int8</type>
+      </para>
+      <para>
+        Number of dead tuples vacuum operations deleted from this index
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>wal_records</structfield> <type>int8</type>
+      </para>
+      <para>
+        Total number of WAL records generated by vacuum operations
+        performed on this index
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>wal_fpi</structfield> <type>int8</type>
+      </para>
+      <para>
+        Total number of WAL full page images generated by vacuum operations
+        performed on 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 bytes generated by vacuum operations
+        performed on this index
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>blk_read_time</structfield> <type>int8</type>
+      </para>
+      <para>
+        Time spent reading database blocks by vacuum operations performed on
+        this index, in milliseconds (if <xref linkend="guc-track-io-timing"/> is enabled,
+        otherwise zero)
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>blk_write_time</structfield> <type>int8</type>
+      </para>
+      <para>
+        Time spent writing database blocks by vacuum operations performed on
+        this index, in milliseconds (if <xref linkend="guc-track-io-timing"/> is enabled,
+        otherwise zero)
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>delay_time</structfield> <type>float8</type>
+      </para>
+      <para>
+        Time spent sleeping in a vacuum delay point by vacuum operations performed on
+        this index, in milliseconds (see <xref linkend="runtime-config-resource-vacuum-cost"/>
+        for details)
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>system_time</structfield> <type>float8</type>
+      </para>
+      <para>
+        System CPU time of vacuuming this index, in milliseconds
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>user_time</structfield> <type>float8</type>
+      </para>
+      <para>
+        User CPU time of vacuuming this index, in milliseconds
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>total_time</structfield> <type>float8</type>
+      </para>
+      <para>
+        Total time of vacuuming this index, in milliseconds
+      </para></entry>
+     </row>
+
+    </tbody>
+   </tgroup>
+  </table>
+ </sect1>
+
+ <sect1 id="view-pg-stat-vacuum-tables">
+  <title><structname>pg_stat_vacuum_tables</structname></title>
+
+  <indexterm zone="view-pg-stat-vacuum-tables">
+   <primary>pg_stat_vacuum_tables</primary>
+  </indexterm>
+
+  <para>
+   The view <structname>pg_stat_vacuum_tables</structname> will contain
+   one row for each table in the current database (including TOAST
+   tables), showing statistics about vacuuming that specific table.
+  </para>
+
+  <table>
+   <title><structname>pg_stat_vacuum_tables</structname> Columns</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>relid</structfield> <type>oid</type>
+      </para>
+      <para>
+       OID of a table
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>schema</structfield> <type>name</type>
+      </para>
+      <para>
+        Name of the schema this table is in
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>relname</structfield> <type>name</type>
+      </para>
+      <para>
+       Name of this table
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>total_blks_read</structfield> <type>int8</type>
+      </para>
+      <para>
+        Number of database blocks read by vacuum operations
+        performed on this table
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>total_blks_hit</structfield> <type>int8</type>
+      </para>
+      <para>
+        Number of times database blocks were found in the
+        buffer cache by vacuum operations
+        performed on this table
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>total_blks_dirtied</structfield> <type>int8</type>
+      </para>
+      <para>
+        Number of blocks written directly by vacuum or auto vacuum.
+        Blocks that are dirtied by a vacuum process can be written out by another process.
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>total_blks_written</structfield> <type>int8</type>
+      </para>
+      <para>
+        Number of database blocks written by vacuum operations
+        performed on this table
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>rel_blks_read</structfield> <type>int8</type>
+      </para>
+      <para>
+        Number of blocks vacuum operations read from this
+        table
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>rel_blks_hit</structfield> <type>int8</type>
+      </para>
+      <para>
+        Number of times blocks of this table were already found
+        in the buffer cache by vacuum operations, so that a read was not necessary
+        (this only includes hits in the
+        project; buffer cache, not the operating system's file system cache)
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>pages_scanned</structfield> <type>int8</type>
+      </para>
+      <para>
+        Number of pages examined by vacuum operations
+        performed on this table
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>pages_removed</structfield> <type>int8</type>
+      </para>
+      <para>
+        Number of pages removed from the physical storage by vacuum operations
+        performed on this table
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>vm_new_frozen_pages</structfield> <type>int8</type>
+      </para>
+      <para>
+        Number of the number of pages newly set all-frozen by vacuum
+        in the visibility map.
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>vm_new_visible_pages</structfield> <type>int8</type>
+      </para>
+      <para>
+        Number of the number of pages newly set all-visible by vacuum
+        in the visibility map.
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>vm_new_visible_frozen_pages</structfield> <type>int8</type>
+      </para>
+      <para>
+        Number of the number of pages newly set all-visible and all-frozen
+        by vacuum in the visibility map.
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>tuples_deleted</structfield> <type>int8</type>
+      </para>
+      <para>
+        Number of dead tuples vacuum operations deleted from this table
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>tuples_frozen</structfield> <type>int8</type>
+      </para>
+      <para>
+        Number of tuples of this table that vacuum operations marked as
+        frozen
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>recently_dead_tuples</structfield> <type>int8</type>
+      </para>
+      <para>
+        Number of dead tuples vacuum operations left in this table due
+        to their visibility in transactions
+      </para></entry>
+     </row>
+
+    <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>missed_dead_tuples</structfield> <type>int8</type>
+      </para>
+      <para>
+        Number of fully DEAD (not just RECENTLY_DEAD) tuples  that could not be
+        pruned due to failure to acquire a cleanup lock on a heap page.
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>index_vacuum_count</structfield> <type>int8</type>
+      </para>
+      <para>
+        Number of times indexes on this table were vacuumed
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>wraparound_failsafe_count</structfield> <type>int4</type>
+      </para>
+      <para>
+        Number of times the vacuum was run to prevent a wraparound problem.
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>missed_dead_pages</structfield> <type>int8</type>
+      </para>
+      <para>
+        Number of pages that had at least one missed_dead_tuples.
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>wal_records</structfield> <type>int8</type>
+      </para>
+      <para>
+        Total number of WAL records generated by vacuum operations
+        performed on this table
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>wal_fpi</structfield> <type>int8</type>
+      </para>
+      <para>
+        Total number of WAL full page images generated by vacuum operations
+        performed on this table
+      </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 bytes generated by vacuum operations
+        performed on this table
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>blk_read_time</structfield> <type>int8</type>
+      </para>
+      <para>
+        Time spent reading database blocks by vacuum operations performed on
+        this table, in milliseconds (if <xref linkend="guc-track-io-timing"/> is enabled,
+        otherwise zero)
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>blk_write_time</structfield> <type>int8</type>
+      </para>
+      <para>
+        Time spent writing database blocks by vacuum operations performed on
+        this table, in milliseconds (if <xref linkend="guc-track-io-timing"/> is enabled,
+        otherwise zero)
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>delay_time</structfield> <type>float8</type>
+      </para>
+      <para>
+        Time spent sleeping in a vacuum delay point by vacuum operations performed on
+        this table, in milliseconds (see <xref linkend="runtime-config-resource-vacuum-cost"/>
+        for details)
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>system_time</structfield> <type>float8</type>
+      </para>
+      <para>
+        System CPU time of vacuuming this table, in milliseconds
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>user_time</structfield> <type>float8</type>
+      </para>
+      <para>
+        User CPU time of vacuuming this table, in milliseconds
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>total_time</structfield> <type>float8</type>
+      </para>
+      <para>
+        Total time of vacuuming this table, in milliseconds
+      </para></entry>
+     </row>
+
+    </tbody>
+   </tgroup>
+  </table>
+  <para>Columns <structfield>total_*</structfield>, <structfield>wal_*</structfield>
+    and <structfield>blk_*</structfield> include data on vacuuming indexes on this table, while columns
+    <structfield>system_time</structfield> and <structfield>user_time</structfield> only include data
+    on vacuuming the heap.</para>
+ </sect1>
 </chapter>
-- 
2.34.1

---

From: Alena Rybakina <[email-masked]>
Date: Mon, 13 Jan 2025 16:26:57 +0300

I noticed that the cfbot is bad, the reason seems to be related to the 
lack of a parameter in src/backend/utils/misc/postgresql.conf.sample. I 
added it, it should help.

On 13.01.2025 14:19, Alena Rybakina wrote:
>> I propose to solve this with a guc (track_vacuum_statistics) that 
>> will allow us to disable statistics collection or enable it back when 
>> period of time and watch the dynamics of the change, so I think a 
>> hook will not hurt here.
>>
>> to take into account how many tuples were not cleared by vacuum due 
>> to failure to acquire a cleanup lock on a heap page. The second 
> I changed the purpose of the guc. By default, vacuum database 
> statistics are always collected, and the track_vacuum_statistics hook 
> enables the ability to collect extended statistics for a given 
> database's relations.
> storing statistics and having the necessary monitoring at hand to 
> track the state of vacuum operation.
> output_vacuum_3_workers. The results were obtained from the test that 
> I described above.
> Vacuum statistics for indexes were accumulated in heap vacuum 
> statistics because of the complexity of vacuum operations,
> So, before cleaning indexes, we now save the collected vacuum 
> statistics for the heap, but we send it only after the processing is 
> complete.
>
>> To view statistics:
>>

-- 
Regards,
Alena Rybakina
Postgres Professional
From bdf712ead5b8cc88ac32f92e1801f3d5a04e9aa9 Mon Sep 17 00:00:00 2001
From: Alena Rybakina <[email-masked]>
Date: Fri, 10 Jan 2025 09:57:13 +0300
Subject: [PATCH 1/4] Machinery for grabbing an extended vacuum statistics on
 heap relations.

 src/backend/utils/misc/postgresql.conf.sample |   1 +
 23 files changed, 1092 insertions(+), 18 deletions(-)

 
+/*

+/* ----------
 
 /*

 
 	/*

 	if (instrument)

 		/*
 
 	if (map[mapByte] & mask)
 	{
+		/*
 
 		MarkBufferDirty(vmbuf);

 
 		VacuumCostBalance = 0;
 
 		/*

 
 
 /* ----------
 
-
 /*
 
-

 

 static void

 /*

 

+/*
 
 /*
  * Functions to allow construction of error message strings separately from
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index c9d8cd796a8..f2d31e174b4 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -1478,6 +1478,15 @@ struct config_bool ConfigureNamesBool[] =
 		false,
 		NULL, NULL, NULL
 	},
+	{
+		{"track_vacuum_statistics", PGC_SUSET, STATS_CUMULATIVE,
+			gettext_noop("Collects vacuum statistics for table relations."),
+			NULL
+		},
+		&pgstat_track_vacuum_statistics,
+		true,
+		NULL, NULL, NULL
+	},
 	{
 		{"track_wal_io_timing", PGC_SUSET, STATS_CUMULATIVE,
 			gettext_noop("Collects timing statistics for WAL I/O activity."),
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index b2bc43383db..522e56f1a83 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -644,6 +644,7 @@
 #track_wal_io_timing = off
 #track_functions = none			# none, pl, all
 #stats_fetch_consistency = cache	# cache, none, snapshot
+#track_vacuum_statistics = off

 # - Monitoring -
 
+{ oid => '8001',

+/* ----------
+ *
+ * ExtVacReport
+ *

 /* ----------

 

 

-
 /*

 /*

 /*----------
 
-
 /*

From 691f7479ccd2bd316776af4502364b16e9ad0eb3 Mon Sep 17 00:00:00 2001
From: Alena Rybakina <[email-masked]>
Date: Mon, 13 Jan 2025 10:32:07 +0300
Subject: [PATCH 2/4] Machinery for grabbing an extended vacuum statistics on
 index relations.

 

-/*

 

 
-	report->blks_fetched =
 
+void

 

 	if (instrument)

 

 

 

 	ivinfo.index = indrel;

 	ivinfo.index = indrel;

 
 	/*

 

 

 

 /*
 
+/*

 
 
 /* ----------

 step s1_commit: COMMIT;

 
+DROP TABLE vestat;

 
 
+DROP TABLE vestat;
 
 -- must be empty

 

 
 -- vacuum freezed pages

From 78f8f814bb9aab9d048631c2a957e6095ee0259b Mon Sep 17 00:00:00 2001
From: Alena Rybakina <[email-masked]>
Date: Fri, 10 Jan 2025 10:50:00 +0300
Subject: [PATCH 3/4] Machinery for grabbing an extended vacuum statistics on
 databases.

 

 

 /*

 

 

 

 

 
 /* ----------

 	return true;
 }
 
+/* ---------

 
 	/*
 
 /*

 

 
 
+	int32		errors;
 
 	/* ----------
 
 /*
 
 session s1
+setup		{
 
 session s2
+setup		{

 DROP TABLE vestat;

 

 
 DROP TABLE vestat;

 

 

From 7dfbb0198b960c360b509993c01c7c33ec6bf0b8 Mon Sep 17 00:00:00 2001

---

---

From: Alexander Korotkov <[email-masked]>
Date: Sun, 2 Feb 2025 22:43:19 +0200

On Mon, Jan 13, 2025 at 3:26 PM Alena Rybakina
<[email-masked]> wrote:
> I noticed that the cfbot is bad, the reason seems to be related to the lack of a parameter in src/backend/utils/misc/postgresql.conf.sample. I added it, it should help.

The patch doesn't apply cleanly.  Please rebase.

I see you introduced new GUC variable pgstat_track_vacuum_statistics,
which should address the increased size of statistics.  However, I
don't see how it could affect the size of PgStat_StatTabEntry struct.
It seems that when pgstat_track_vacuum_statistics == 0, extended
vacuum statistics is not collected but the size of hash table entries
is the same.  Also, should pgstat_track_vacuum_statistics also affect
per database statistics?

The name of 0001 is "... on heap relations".  Should we say "on table
relations", because new machinery should work with alternative table
AMs as well.

There are deletions of empty lines in
src/include/utils/pgstat_internal.h and src/include/pgstat.h.  Please,
remote them as it's not purpose of this patchset.

------
Regards,
Alexander Korotkov
Supabase

---

From: Alena Rybakina <[email-masked]>
Date: Tue, 4 Feb 2025 18:22:52 +0300

Hi! Thank you for your review!

> The patch doesn't apply cleanly.  Please rebase.
I rebased them.
> is the same.

Yes, hash table entries will be the same but vacuum_ext structure stored 
in PgStat_StatTabEntry will not be filled with statistics, although
vacuum_ext structure stored in PgStat_StatDBEntry will be fill be.

>   Also, should pgstat_track_vacuum_statistics also affect
> per database statistics?

According to my original idea, I thought that we could collect extended 
statistics on relationships depending on whether the hook is enabled, 
and always on databases. This will help us to constantly collect 
statistics on the vacuum and notice when something is wrong and at the 
same time not very expensive: there are much fewer databases compared to 
the same relationships and there are much fewer statistics there. You 
can introduce an additional hook that disables all collection of vacuum 
statistics. This patch can be seen here for the 17th version of the 
patch [0], I have not yet started adding it, since we did not come to an 
agreement.

At the moment, I have made a patch for vacuum statistics for databases 
that disables all vacuum statistics, the hook is disabled by default.

[0] 
https://www.postgresql.org/message-id/attachment/170462/v17-0003-Machinery-for-grabbing-an-extended-vacuum-statistics.patch

> relations", because new machinery should work with alternative table
Agree. Fixed
> remote them as it's not purpose of this patchset.
fixed

-- 
Regards,
Alena Rybakina
Postgres Professional
From 280329ec62f08403e8df5c941fb4c2458fd47e72 Mon Sep 17 00:00:00 2001
From: Alena Rybakina <[email-masked]>
Date: Tue, 4 Feb 2025 16:34:18 +0300
Subject: [PATCH 1/4] Machinery for grabbing an extended vacuum statistics on
 table relations.

Authors: Alena Rybakina <[email-masked]>,
	 Andrei Lepikhov <[email-masked]>,
	 Andrei Zubkov <[email-masked]>
Reviewed-by: Dilip Kumar <[email-masked]>, Masahiko Sawada <[email-masked]>,
	     Ilia Evdokimov <[email-masked]>, jian he <[email-masked]>,
	     Kirill Reshke <[email-masked]>, Alexander Korotkov <[email-masked]>,
	     Jim Nasby <[email-masked]>, Sami Imseih <[email-masked]>
---
 src/backend/access/heap/vacuumlazy.c          | 150 +++++++++++-
 src/backend/access/heap/visibilitymap.c       |  10 +
 src/backend/catalog/system_views.sql          |  52 +++-
 src/backend/commands/vacuum.c                 |   4 +
 src/backend/commands/vacuumparallel.c         |   1 +
 src/backend/utils/activity/pgstat.c           |  12 +-
 src/backend/utils/activity/pgstat_relation.c  |  46 +++-
 src/backend/utils/adt/pgstatfuncs.c           | 147 ++++++++++++
 src/backend/utils/error/elog.c                |  13 +
 src/backend/utils/misc/guc_tables.c           |   9 +
 src/backend/utils/misc/postgresql.conf.sample |   1 +
 src/include/catalog/pg_proc.dat               |  18 ++
 src/include/commands/vacuum.h                 |   1 +
 src/include/pgstat.h                          |  80 +++++-
 src/include/utils/elog.h                      |   1 +
 .../vacuum-extending-in-repetable-read.out    |  53 ++++
 src/test/isolation/isolation_schedule         |   1 +
 .../vacuum-extending-in-repetable-read.spec   |  53 ++++
 src/test/regress/expected/rules.out           |  44 +++-
 .../expected/vacuum_tables_statistics.out     | 227 ++++++++++++++++++
 src/test/regress/parallel_schedule            |   5 +
 .../regress/sql/vacuum_tables_statistics.sql  | 183 ++++++++++++++
 22 files changed, 1095 insertions(+), 16 deletions(-)
 create mode 100644 src/test/isolation/expected/vacuum-extending-in-repetable-read.out
 create mode 100644 src/test/isolation/specs/vacuum-extending-in-repetable-read.spec
 create mode 100644 src/test/regress/expected/vacuum_tables_statistics.out
 create mode 100644 src/test/regress/sql/vacuum_tables_statistics.sql

diff --git a/src/backend/access/heap/vacuumlazy.c b/src/backend/access/heap/vacuumlazy.c
index 075af385cd1..6f1c52a7b66 100644
--- a/src/backend/access/heap/vacuumlazy.c
+++ b/src/backend/access/heap/vacuumlazy.c
@@ -219,6 +219,7 @@ typedef struct LVRelState
 	/* Error reporting state */
 	char	   *dbname;
 	char	   *relnamespace;
+	Oid			reloid;
 	char	   *relname;
 	char	   *indname;		/* Current index name */
 	BlockNumber blkno;			/* used only for heap operations */
@@ -283,6 +284,8 @@ typedef struct LVRelState
 	BlockNumber next_unskippable_block; /* next unskippable block */
 	bool		next_unskippable_allvis;	/* its visibility status */
 	Buffer		next_unskippable_vmbuffer;	/* buffer containing its VM bit */
+
+	int32		wraparound_failsafe_count; /* the number of times to prevent workaround problem */
 } LVRelState;
 
 /* Struct for saving and restoring vacuum error information. */
@@ -293,6 +296,18 @@ typedef struct LVSavedErrInfo
 	VacErrPhase phase;
 } LVSavedErrInfo;
 
+/*
 
@@ -346,6 +361,106 @@ static void update_vacuum_error_info(LVRelState *vacrel,
 
+/* ----------
 
 /*
@@ -378,7 +493,14 @@ heap_vacuum_rel(Relation rel, VacuumParams *params,
 
 	verbose = (params->options & VACOPT_VERBOSE) != 0;
 	instrument = (verbose || (AmAutoVacuumWorkerProcess() &&
@@ -398,7 +520,7 @@ heap_vacuum_rel(Relation rel, VacuumParams *params,
 
@@ -415,6 +537,7 @@ heap_vacuum_rel(Relation rel, VacuumParams *params,
@@ -502,6 +625,7 @@ heap_vacuum_rel(Relation rel, VacuumParams *params,
 
 	/*
 	 * Get cutoffs that determine which deleted tuples are considered DEAD,
@@ -647,6 +771,26 @@ heap_vacuum_rel(Relation rel, VacuumParams *params,
 						vacrel->NewRelfrozenXid, vacrel->NewRelminMxid,
 						&frozenxid_updated, &minmulti_updated, false);
 
@@ -662,7 +806,8 @@ heap_vacuum_rel(Relation rel, VacuumParams *params,
-						 starttime);
+						 starttime,
 
 	if (instrument)
@@ -2475,6 +2620,7 @@ lazy_check_wraparound_failsafe(LVRelState *vacrel)
 		int64		progress_val[2] = {0, 0};

 		/*
 
 	if (map[mapByte] & mask)
 	{
+		/*
 
 		MarkBufferDirty(vmbuf);
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index cddc3ea9b53..3ec4f2caaf4 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -700,7 +700,9 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
-            pg_stat_get_total_autoanalyze_time(C.oid) AS total_autoanalyze_time
+            pg_stat_get_total_autoanalyze_time(C.oid) AS total_autoanalyze_time,
+            pg_stat_get_rev_all_frozen_pages(C.oid) AS rev_all_frozen_pages,
+            pg_stat_get_rev_all_visible_pages(C.oid) AS rev_all_visible_pages
     FROM pg_class C LEFT JOIN
          pg_index I ON C.oid = I.indrelid
          LEFT JOIN pg_namespace N ON (N.oid = C.relnamespace)
@@ -1392,3 +1394,51 @@ CREATE VIEW pg_stat_subscription_stats AS

 
 		VacuumCostBalance = 0;
 
 		/*
index dc3322c256b..b91f3b6cc28 100644
@@ -1054,6 +1054,7 @@ parallel_vacuum_main(dsm_segment *seg, shm_toc *toc)
index 3168b825e25..826a5685b2a 100644

 
 
 /* ----------
 
-
 /*
  * The different kinds of built-in statistics.
  *
@@ -901,7 +900,6 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 		pgstat_reset_entries_of_kind(kind, ts);
 }
 
-
 /* ------------------------------------------------------------
  * Fetching of stats
  * ------------------------------------------------------------
@@ -970,7 +968,7 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid)

 	/* if caching is desired, look up in cache */
 	if (pgstat_fetch_consistency > PGSTAT_FETCH_CONSISTENCY_NONE)
@@ -1086,7 +1084,7 @@ pgstat_snapshot_fixed(PgStat_Kind kind)
 		pgstat_clear_snapshot();

@@ -1137,7 +1135,7 @@ pgstat_prep_snapshot(void)
 }
 
 static void
index d64595a165c..0272dd1f393 100644

 /*
@@ -209,7 +211,7 @@ pgstat_drop_relation(Relation rel)
 void
 pgstat_report_vacuum(Oid tableoid, bool shared,
 					 PgStat_Counter livetuples, PgStat_Counter deadtuples,
-					 TimestampTz starttime)
+					 TimestampTz starttime, ExtVacReport *params)
 {
 	PgStat_EntryRef *entry_ref;
 	PgStatShared_Relation *shtabentry;
@@ -235,6 +237,8 @@ pgstat_report_vacuum(Oid tableoid, bool shared,
 	tabentry->live_tuples = livetuples;
 	tabentry->dead_tuples = deadtuples;
 
@@ -872,6 +876,9 @@ pgstat_relation_flush_cb(PgStat_EntryRef *entry_ref, bool nowait)
 
@@ -995,3 +1002,40 @@ restore_truncdrop_counters(PgStat_TableXactStatus *trans)
index e9096a88492..e112762ed2f 100644
 
+/* pg_stat_get_rev_frozen_pages */
+PG_STAT_GET_RELENTRY_INT64(rev_all_frozen_pages)
+
+/* pg_stat_get_rev_all_visible_pages */
+PG_STAT_GET_RELENTRY_INT64(rev_all_visible_pages)
+
 #define PG_STAT_GET_RELENTRY_FLOAT8(stat)						\
 Datum															\
 CppConcat(pg_stat_get_,stat)(PG_FUNCTION_ARGS)					\
@@ -2244,3 +2250,144 @@ pg_stat_have_stats(PG_FUNCTION_ARGS)

+/*
 
 /*
index 71448bb4fdd..3c16ef4e869 100644
@@ -1479,6 +1479,15 @@ struct config_bool ConfigureNamesBool[] =
index 079efa1baa7..0c6e19980d6 100644
@@ -636,6 +636,7 @@

 # - Monitoring -
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 5b8c2ad2a54..5967d29be09 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12454,4 +12454,22 @@
   proargtypes => 'int4',
   prosrc => 'gist_stratnum_common' },
 
+{ oid => '8001',
 
 extern PGDLLIMPORT bool VacuumFailsafeActive;
 extern PGDLLIMPORT double vacuum_cost_delay;
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index dd823d3f56e..f63f227e345 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -112,6 +112,53 @@ typedef struct PgStat_BackendSubEntry
 	PgStat_Counter conflict_count[CONFLICT_NUM_TYPES];
 } PgStat_BackendSubEntry;
 
+/* ----------
+ *
+ * ExtVacReport
+ *
@@ -154,6 +201,16 @@ typedef struct PgStat_TableCounts

 /* ----------
@@ -212,7 +269,7 @@ typedef struct PgStat_TableXactStatus
  * ------------------------------------------------------------
  */
 
-#define PGSTAT_FILE_FORMAT_ID	0x01A5BCB3
+#define PGSTAT_FILE_FORMAT_ID	0x01A5BCB4
 
 typedef struct PgStat_ArchiverStats
 {
@@ -394,6 +451,8 @@ typedef struct PgStat_StatDBEntry
 	PgStat_Counter parallel_workers_launched;

 typedef struct PgStat_StatFuncEntry
@@ -472,6 +531,11 @@ typedef struct PgStat_StatTabEntry
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
 	PgStat_Counter total_autoanalyze_time;
+
+	PgStat_Counter rev_all_visible_pages;
+	PgStat_Counter rev_all_frozen_pages;
+
+	ExtVacReport vacuum_ext;
 } PgStat_StatTabEntry;
 
 typedef struct PgStat_WalStats
@@ -657,7 +721,7 @@ extern void pgstat_unlink_relation(Relation rel);
 
 extern void pgstat_report_vacuum(Oid tableoid, bool shared,
 								 PgStat_Counter livetuples, PgStat_Counter deadtuples,
-								 TimestampTz starttime);
+								 TimestampTz starttime, ExtVacReport *params);
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
@@ -708,6 +772,17 @@ extern void pgstat_report_analyze(Relation rel,
 		if (pgstat_should_count_relation(rel))						\
 			(rel)->pgstat_info->counts.blocks_hit++;				\
 	} while (0)
+/* accumulate unfrozen all-visible and all-frozen pages */
+#define pgstat_count_vm_rev_all_visible(rel)						\
+	do {															\
+		if (pgstat_should_count_relation(rel))						\
+			(rel)->pgstat_info->counts.rev_all_visible_pages++;	\
+	} while (0)
+#define pgstat_count_vm_rev_all_frozen(rel)						\
+	do {															\
+		if (pgstat_should_count_relation(rel))						\
+			(rel)->pgstat_info->counts.rev_all_frozen_pages++;	\
+	} while (0)
 
@@ -796,6 +871,7 @@ extern PgStat_WalStats *pgstat_fetch_stat_wal(void);

 /*

 /*----------
index 3361f6a69c9..ee933159edf 100644
@@ -1808,7 +1808,9 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
-    pg_stat_get_total_autoanalyze_time(c.oid) AS total_autoanalyze_time
+    pg_stat_get_total_autoanalyze_time(c.oid) AS total_autoanalyze_time,
@@ -2198,7 +2200,9 @@ pg_stat_sys_tables| SELECT relid,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-    total_autoanalyze_time
+    total_autoanalyze_time,
@@ -2250,9 +2254,43 @@ pg_stat_user_tables| SELECT relid,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-    total_autoanalyze_time
+    total_autoanalyze_time,
index 00000000000..b5ea9c9ab1e
@@ -0,0 +1,227 @@
+ f                   | t                    | f                           | t                    | t
index 00000000000..5bc34bec64b

From 40666c3f43e00d4356e74a98b5745a782e18ceb4 Mon Sep 17 00:00:00 2001
From: Alena Rybakina <[email-masked]>
Date: Tue, 4 Feb 2025 17:34:15 +0300
Subject: [PATCH 2/4] Machinery for grabbing an extended vacuum statistics on 
 index relations.

Authors: Alena Rybakina <[email-masked]>,
   Andrei Lepikhov <[email-masked]>,
   Andrei Zubkov <[email-masked]>
Reviewed-by: Dilip Kumar <[email-masked]>, Masahiko Sawada <[email-masked]>,
       Ilia Evdokimov <[email-masked]>, jian he <[email-masked]>,
       Kirill Reshke <[email-masked]>, Alexander Korotkov <[email-masked]>,
       Jim Nasby <[email-masked]>, Sami Imseih <[email-masked]>
---
 src/backend/access/heap/vacuumlazy.c          | 292 ++++++++++++++----
 src/backend/catalog/system_views.sql          |  32 ++
 src/backend/commands/vacuumparallel.c         |  14 +
 src/backend/utils/activity/pgstat.c           |   4 +
 src/backend/utils/activity/pgstat_relation.c  |  48 ++-
 src/backend/utils/adt/pgstatfuncs.c           | 133 +++++++-
 src/backend/utils/misc/guc_tables.c           |   2 +-
 src/include/catalog/pg_proc.dat               |   9 +
 src/include/commands/vacuum.h                 |  25 ++
 src/include/pgstat.h                          |  58 +++-
 .../vacuum-extending-in-repetable-read.out    |   4 +-
 src/test/regress/expected/rules.out           |  22 ++
 .../expected/vacuum_index_statistics.out      | 183 +++++++++++
 src/test/regress/parallel_schedule            |   1 +
 .../regress/sql/vacuum_index_statistics.sql   | 151 +++++++++
 15 files changed, 873 insertions(+), 105 deletions(-)
 create mode 100644 src/test/regress/expected/vacuum_index_statistics.out
 create mode 100644 src/test/regress/sql/vacuum_index_statistics.sql

diff --git a/src/backend/access/heap/vacuumlazy.c b/src/backend/access/heap/vacuumlazy.c
index 6f1c52a7b66..991fb831726 100644
--- a/src/backend/access/heap/vacuumlazy.c
+++ b/src/backend/access/heap/vacuumlazy.c
@@ -220,6 +220,7 @@ typedef struct LVRelState
 	char	   *dbname;
 	char	   *relnamespace;
 	Oid			reloid;
+	Oid			indoid;
 	char	   *relname;
 	char	   *indname;		/* Current index name */
 	BlockNumber blkno;			/* used only for heap operations */
@@ -286,6 +287,8 @@ typedef struct LVRelState
 	Buffer		next_unskippable_vmbuffer;	/* buffer containing its VM bit */

 /* Struct for saving and restoring vacuum error information. */
@@ -296,19 +299,6 @@ typedef struct LVSavedErrInfo
 	VacErrPhase phase;
 } LVSavedErrInfo;
 
-/*
@@ -427,27 +417,25 @@ extvac_stats_end(Relation rel, LVExtStatCounters *counters,

 

 	if (!rel->pgstat_info || !pgstat_track_counts)
 		/*
@@ -456,12 +444,96 @@ extvac_stats_end(Relation rel, LVExtStatCounters *counters,
 		 */
 		return;
 
-	report->blks_fetched =
 
+void
+	/* Set initial values for common heap and index statistics*/
@@ -494,13 +566,7 @@ heap_vacuum_rel(Relation rel, VacuumParams *params,
 
 	verbose = (params->options & VACOPT_VERBOSE) != 0;
 	instrument = (verbose || (AmAutoVacuumWorkerProcess() &&
@@ -520,7 +586,6 @@ heap_vacuum_rel(Relation rel, VacuumParams *params,
 
@@ -693,6 +758,8 @@ heap_vacuum_rel(Relation rel, VacuumParams *params,
 	 */
 	lazy_scan_heap(vacrel);
 
@@ -771,26 +838,6 @@ heap_vacuum_rel(Relation rel, VacuumParams *params,
 
@@ -800,14 +847,37 @@ heap_vacuum_rel(Relation rel, VacuumParams *params,
+		extvac_stats_end(vacrel->rel, &extVacCounters, &(vacrel->extVacReport));
 						 starttime,
+							 starttime,
 
 	if (instrument)
@@ -1064,6 +1134,7 @@ lazy_scan_heap(LVRelState *vacrel)
 		PROGRESS_VACUUM_MAX_DEAD_TUPLE_BYTES
 	};
 	int64		initprog_val[3];
+	LVExtStatCounters extVacCounters;
 
@@ -1077,6 +1148,13 @@ lazy_scan_heap(LVRelState *vacrel)
 
+	/*
+	 * Due to the fact that vacuum heap processing needs their index vacuuming
+	 * we need to track them separately and accumulate heap vacuum statistics
+	 * separately. So last processes are related to only heap vacuuming process.
+	 */
+	extvac_stats_start(vacrel->rel, &extVacCounters);
+
 	while (heap_vac_scan_next_block(vacrel, &blkno, &all_visible_according_to_vm))
 	{
 		Buffer		buf;
@@ -1127,8 +1205,26 @@ lazy_scan_heap(LVRelState *vacrel)
 
 			/* Perform a round of index and heap vacuuming */
 			vacrel->consider_bypass_optimization = false;
+
+			/*
+			 * Lazy vacuum stage includes index vacuuming and cleaning up stage, so
+			 * we prefer tracking them separately.
+			 * Before starting to process the indexes save the current heap statistics
+			*/
+			extvac_stats_end(vacrel->rel, &extVacCounters, &(vacrel->extVacReport));
+			accumulate_heap_vacuum_statistics(&extVacCounters, vacrel);
+
 			lazy_vacuum(vacrel);
 
+			/*
+			 * After completion lazy vacuum, we start again tracking vacuum statistics for
+			 * heap-related objects like FSM, VM, provide heap prunning.
+			 * It seems dangerously that we have start tracking but there are no end, but
+			 * it is safe. The end tracking is located before lazy vacuum stage in the same
+			 * loop or after it.
+ 			*/
+			extvac_stats_start(vacrel->rel, &extVacCounters);
+
 			/*
 			 * Vacuum the Free Space Map to make newly-freed space visible on
 			 * upper-level FSM pages.  Note we have not yet processed blkno.
@@ -1277,6 +1373,12 @@ lazy_scan_heap(LVRelState *vacrel)
 		Max(vacrel->new_live_tuples, 0) + vacrel->recently_dead_tuples +
 		vacrel->missed_dead_tuples;
 
+	/*
+	 * Vacuum can process lazy vacuum again and we save heap statistics now
+	 * just in case in tend to avoid collecting vacuum index statistics again.
+	 */
+	extvac_stats_end(vacrel->rel, &extVacCounters, &(vacrel->extVacReport));
+	accumulate_heap_vacuum_statistics(&extVacCounters, vacrel);
 	/*
 	 * Do index vacuuming (call each index's ambulkdelete routine), then do
 	 * related heap vacuuming
@@ -1284,6 +1386,12 @@ lazy_scan_heap(LVRelState *vacrel)
 	if (vacrel->dead_items_info->num_items > 0)
 		lazy_vacuum(vacrel);
 
+	/*
+	 * We need to take into account heap vacuum statistics during process of
+	 * FSM.
+ 	 */
+	extvac_stats_start(vacrel->rel, &extVacCounters);
+
 	/*
 	 * Vacuum the remainder of the Free Space Map.  We must do this whether or
 	 * not there were indexes, and whether or not we bypassed index vacuuming.
@@ -1294,6 +1402,10 @@ lazy_scan_heap(LVRelState *vacrel)
 	/* report all blocks vacuumed */
 	pgstat_progress_update_param(PROGRESS_VACUUM_HEAP_BLKS_VACUUMED, blkno);
 
+	/* Before starting final index clan up stage save heap statistics */
+	extvac_stats_end(vacrel->rel, &extVacCounters, &(vacrel->extVacReport));
+	accumulate_heap_vacuum_statistics(&extVacCounters, vacrel);
+
 	/* Do final index cleanup (call each index's amvacuumcleanup routine) */
 	if (vacrel->nindexes > 0 && vacrel->do_index_cleanup)
 		lazy_cleanup_all_indexes(vacrel);
@@ -2150,6 +2262,7 @@ static void
 lazy_vacuum(LVRelState *vacrel)
 {
 	bool		bypass;
+	LVExtStatCounters extVacCounters;
 
 	/* Should not end up here with no indexes */
 	Assert(vacrel->nindexes > 0);
@@ -2162,6 +2275,9 @@ lazy_vacuum(LVRelState *vacrel)
 		return;
 	}
 
+	/* Set initial statistics values to gather vacuum statistics for the heap */
+	extvac_stats_start(vacrel->rel, &extVacCounters);
+
 	/*
 	 * Consider bypassing index vacuuming (and heap vacuuming) entirely.
 	 *
@@ -2218,6 +2334,14 @@ lazy_vacuum(LVRelState *vacrel)
 				  TidStoreMemoryUsage(vacrel->dead_items) < 32 * 1024 * 1024);
 	}
 
+	/*
+	 * Vacuum is likely to vacuum indexes again, so save vacuum statistics for
+	 * heap relations now.
+	 * The vacuum process below doesn't contain any useful statistics information
+	 * for heap if indexes won't be processed, but we will track them separately.
+	 */
+	extvac_stats_end(vacrel->rel, &extVacCounters, &(vacrel->extVacReport));
+
 	if (bypass)
 	{
 		/*
@@ -2234,11 +2358,21 @@ lazy_vacuum(LVRelState *vacrel)
 	}
 	else if (lazy_vacuum_all_indexes(vacrel))
 	{
-		/*
-		 * We successfully completed a round of index vacuuming.  Do related
-		 * heap vacuuming now.
-		 */
-		lazy_vacuum_heap_rel(vacrel);
+		/* Now the vacuum is going to process heap relation, so
+		 * we need to set intial statistic values for tracking.
+		*/
+
+		/* Set initial statistics values to gather vacuum statistics for the heap */
+		extvac_stats_start(vacrel->rel, &extVacCounters);
+
+ 		/*
+ 		 * We successfully completed a round of index vacuuming.  Do related
+ 		 * heap vacuuming now.
+ 		 */
+ 		lazy_vacuum_heap_rel(vacrel);
+
+		extvac_stats_end(vacrel->rel, &extVacCounters, &(vacrel->extVacReport));
+		accumulate_heap_vacuum_statistics(&extVacCounters, vacrel);
 	}
 	else
 	{
@@ -2732,6 +2866,11 @@ lazy_vacuum_one_index(Relation indrel, IndexBulkDeleteResult *istat,
 {
 	IndexVacuumInfo ivinfo;
 	LVSavedErrInfo saved_err_info;
+	LVExtStatCountersIdx extVacCounters;
+	ExtVacReport extVacReport;
+
+	/* Set initial statistics values to gather vacuum statistics for the index */
+	extvac_stats_start_idx(indrel, istat, &extVacCounters);
 
 	ivinfo.index = indrel;
@@ -2750,6 +2889,7 @@ lazy_vacuum_one_index(Relation indrel, IndexBulkDeleteResult *istat,
@@ -2758,6 +2898,15 @@ lazy_vacuum_one_index(Relation indrel, IndexBulkDeleteResult *istat,
 
+								0, 0, 0, &extVacReport);
@@ -2782,6 +2931,11 @@ lazy_cleanup_one_index(Relation indrel, IndexBulkDeleteResult *istat,
 
 	ivinfo.index = indrel;
@@ -2801,12 +2955,22 @@ lazy_cleanup_one_index(Relation indrel, IndexBulkDeleteResult *istat,

+								0, 0, 0, &extVacReport);
index 3ec4f2caaf4..da9079afa21 100644
@@ -1442,3 +1442,35 @@ FROM pg_class rel
index b91f3b6cc28..145538c53f9 100644
 
 	/*

+	if(pgstat_track_vacuum_statistics)
+	{
+		/* Make extended vacuum stats report for index */
+		extvac_stats_end_idx(indrel, istat_res, &extVacCounters, &extVacReport);
+		pgstat_report_vacuum(RelationGetRelid(indrel),
+								indrel->rd_rel->relisshared,
+								0, 0, 0, &extVacReport);
+	}
+
 	/*
 	 * Copy the index bulk-deletion result returned from ambulkdelete and
 	 * amvacuumcleanup to the DSM segment if it's the first cycle because they
diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index 826a5685b2a..363cbf2bb04 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -1180,6 +1180,10 @@ pgstat_build_snapshot(PgStat_Kind statKind)
 		if (p->dropped)
 			continue;

 		stats_data = dsa_get_address(pgStatLocal.dsa, p->body);
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index 0272dd1f393..cd4ffb50bca 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -1007,6 +1007,9 @@ static void
 pgstat_accumulate_extvac_stats(ExtVacReport *dst, ExtVacReport *src,
 							   bool accumulate_reltype_specific_info)
 {
+	if(!pgstat_track_vacuum_statistics)
+		return;
+
 	dst->total_blks_read += src->total_blks_read;
 	dst->total_blks_hit += src->total_blks_hit;
 	dst->total_blks_dirtied += src->total_blks_dirtied;
@@ -1022,20 +1025,35 @@ pgstat_accumulate_extvac_stats(ExtVacReport *dst, ExtVacReport *src,
 	if (!accumulate_reltype_specific_info)
 		return;

index e112762ed2f..80e867d773f 100644
@@ -2358,18 +2358,19 @@ pg_stat_get_vacuum_tables(PG_FUNCTION_ARGS)

 	values[i++] = Int64GetDatum(extvacuum->wal_records);
 	values[i++] = Int64GetDatum(extvacuum->wal_fpi);
@@ -2388,6 +2389,116 @@ pg_stat_get_vacuum_tables(PG_FUNCTION_ARGS)

index 3c16ef4e869..3f7750ad6a4 100644
@@ -1481,7 +1481,7 @@ struct config_bool ConfigureNamesBool[] =
index 5967d29be09..c983e069eef 100644
@@ -12472,4 +12472,13 @@
 
 /*
 
+/*
 
index f63f227e345..a305836f237 100644
@@ -112,11 +112,19 @@ typedef struct PgStat_BackendSubEntry
 
@@ -145,18 +153,44 @@ typedef struct ExtVacReport

 /* ----------

 step s1_commit: COMMIT;

index ee933159edf..88b0e13ed58 100644
@@ -2259,6 +2259,28 @@ pg_stat_user_tables| SELECT relid,

From e75bebff6b2d725578aa4a7d5cc2009cd2ee7e42 Mon Sep 17 00:00:00 2001
From: Alena Rybakina <[email-masked]>
Date: Tue, 4 Feb 2025 17:57:44 +0300
Subject: [PATCH 3/4] Machinery for grabbing an extended vacuum statistics on 
 databases.

Authors: Alena Rybakina <[email-masked]>,
   Andrei Lepikhov <[email-masked]>,
   Andrei Zubkov <[email-masked]>
Reviewed-by: Dilip Kumar <[email-masked]>, Masahiko Sawada <[email-masked]>,
       Ilia Evdokimov <[email-masked]>, jian he <[email-masked]>,
       Kirill Reshke <[email-masked]>, Alexander Korotkov <[email-masked]>,
       Jim Nasby <[email-masked]>, Sami Imseih <[email-masked]>
---
 src/backend/access/heap/vacuumlazy.c          |  17 ++-
 src/backend/catalog/system_views.sql          |  27 ++++-
 src/backend/utils/activity/pgstat.c           |   2 +-
 src/backend/utils/activity/pgstat_database.c  |   1 +
 src/backend/utils/activity/pgstat_relation.c  |  46 +++++++-
 src/backend/utils/adt/pgstatfuncs.c           | 100 +++++++++++++++++-
 src/backend/utils/misc/guc_tables.c           |   2 +-
 src/include/catalog/pg_proc.dat               |  13 ++-
 src/include/pgstat.h                          |   5 +-
 .../vacuum-extending-in-repetable-read.spec   |   6 ++
 src/test/regress/expected/rules.out           |  17 +++
 .../expected/vacuum_index_statistics.out      |  16 +--
 ...ut => vacuum_tables_and_db_statistics.out} |  87 +++++++++++++--
 src/test/regress/parallel_schedule            |   2 +-
 .../regress/sql/vacuum_index_statistics.sql   |   6 +-
 ...ql => vacuum_tables_and_db_statistics.sql} |  69 +++++++++++-
 16 files changed, 381 insertions(+), 35 deletions(-)
 rename src/test/regress/expected/{vacuum_tables_statistics.out => vacuum_tables_and_db_statistics.out} (82%)
 rename src/test/regress/sql/{vacuum_tables_statistics.sql => vacuum_tables_and_db_statistics.sql} (81%)

index 991fb831726..7b99dc5b3b2 100644
@@ -531,7 +531,7 @@ accumulate_heap_vacuum_statistics(LVExtStatCounters *extVacCounters, LVRelState
 
 /*
@@ -3583,6 +3583,9 @@ vacuum_error_callback(void *arg)
 	switch (errinfo->phase)
 	{
 		case VACUUM_ERRCB_PHASE_SCAN_HEAP:
+			if(geterrelevel() == ERROR)
+					pgstat_report_vacuum_error(errinfo->reloid, PGSTAT_EXTVAC_TABLE);
+
 			if (BlockNumberIsValid(errinfo->blkno))
 			{
 				if (OffsetNumberIsValid(errinfo->offnum))
@@ -3598,6 +3601,9 @@ vacuum_error_callback(void *arg)
 			break;
 
@@ -3613,16 +3619,25 @@ vacuum_error_callback(void *arg)
 			break;

 
index da9079afa21..a72d01102bb 100644
@@ -1473,4 +1473,29 @@ FROM
index 363cbf2bb04..4cc1d09d96f 100644

 /* ----------

 	return true;
 }
index cd4ffb50bca..5d36d5a2140 100644
 
+/* ---------
 	PgStat_Counter elapsedtime;
@@ -274,6 +307,16 @@ pgstat_report_vacuum(Oid tableoid, bool shared,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 
 /*
@@ -1021,6 +1064,8 @@ pgstat_accumulate_extvac_stats(ExtVacReport *dst, ExtVacReport *src,
 
 	if (!accumulate_reltype_specific_info)
 		return;
@@ -1048,7 +1093,6 @@ pgstat_accumulate_extvac_stats(ExtVacReport *dst, ExtVacReport *src,
 			dst->table.index_vacuum_count += src->table.index_vacuum_count;
 			dst->table.missed_dead_pages += src->table.missed_dead_pages;
 			dst->table.missed_dead_tuples += src->table.missed_dead_tuples;
-			dst->table.wraparound_failsafe_count += src->table.wraparound_failsafe_count;
 		}
 		else if (dst->type == PGSTAT_EXTVAC_INDEX)
 		{
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 80e867d773f..59eb528b20c 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -2369,7 +2369,7 @@ pg_stat_get_vacuum_tables(PG_FUNCTION_ARGS)
 	values[i++] = Int64GetDatum(extvacuum->table.recently_dead_tuples);
 	values[i++] = Int64GetDatum(extvacuum->table.missed_dead_tuples);

 	values[i++] = Int64GetDatum(extvacuum->wal_records);
@@ -2499,6 +2499,104 @@ pg_stat_get_vacuum_indexes(PG_FUNCTION_ARGS)

index 3f7750ad6a4..7315be9b7bc 100644
@@ -1485,7 +1485,7 @@ struct config_bool ConfigureNamesBool[] =
index c983e069eef..df2981abd82 100644
@@ -12473,12 +12473,21 @@
index a305836f237..dcf8cb763a4 100644
@@ -155,6 +155,9 @@ typedef struct ExtVacReport

+	int32		errors;
 
 	/* ----------
@@ -184,7 +187,6 @@ typedef struct ExtVacReport
 			int64		missed_dead_tuples;		/* tuples not pruned by vacuum due to failure to get a cleanup lock */
 			int64		missed_dead_pages;		/* pages with missed dead tuples */
 			int64		index_vacuum_count;	/* number of index vacuumings */
-			int32		wraparound_failsafe_count;	/* the number of times to prevent workaround problem */
 		}			table;
 		struct
 		{
@@ -759,6 +761,7 @@ extern void pgstat_report_vacuum(Oid tableoid, bool shared,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_vacuum_error(Oid tableoid, ExtVacReportType m_type);
 
 /*
 
 session s1
+setup		{
 
 session s2
+setup		{
index 88b0e13ed58..1741753b52d 100644
@@ -2259,6 +2259,23 @@ pg_stat_user_tables| SELECT relid,

 DROP TABLE vestat;
index b5ea9c9ab1e..0300e7b6276 100644
 
@@ -225,3 +228,69 @@ FROM pg_stat_vacuum_tables, pg_stat_all_tables WHERE pg_stat_vacuum_tables.relna
 (1 row)

 

 
 DROP TABLE vestat;
similarity index 81%
index 5bc34bec64b..ca7dbde9387 100644

 

 
@@ -180,4 +184,59 @@ VACUUM (PARALLEL 0, BUFFER_USAGE_LIMIT 128) vestat;
 
-DROP TABLE vestat CASCADE;

From 18c423acda8886b25c85d27c443cf4c93e8f5891 Mon Sep 17 00:00:00 2001

---

diff --git a/doc/src/sgml/system-views.sgml b/doc/src/sgml/system-views.sgml
index 8e2b0a7927b..a52547b2d5b 100644
--- a/doc/src/sgml/system-views.sgml
+++ b/doc/src/sgml/system-views.sgml
@@ -5068,4 +5068,759 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx
   </table>
  </sect1>

---

From: Alena Rybakina <[email-masked]>
Date: Tue, 4 Feb 2025 18:49:43 +0300

On 04.02.2025 18:22, Alena Rybakina wrote:
> According to my original idea, I thought that we could collect 
> extended statistics on relationships depending on whether the hook is 
> enabled, and always on databases. This will help us to constantly 
> collect statistics on the vacuum and notice when something is wrong 
> and at the same time not very expensive: there are much fewer 
> databases compared to the same relationships and there are much fewer 
> statistics there. You can introduce an additional hook that disables 
> all collection of vacuum statistics. This patch can be seen here for 
> the 17th version of the patch [0], I have not yet started adding it, 
> since we did not come to an agreement.
I added this version as 
"v19-0003.2-extended-vacuum-statistics.patch.no-cbot"

-- 
Regards,
Alena Rybakina
Postgres Professional
From af474098037c8292d9c82c711c0fe78aa1580395 Mon Sep 17 00:00:00 2001
From: Alena Rybakina <[email-masked]>
Date: Tue, 4 Feb 2025 18:39:35 +0300
Subject: [PATCH] Machinery for grabbing an extended vacuum statistics on 
 databases.

 src/backend/access/heap/vacuumlazy.c          |  75 +++++--------
 ...ut => vacuum_tables_and_db_statistics.out} |  89 ++++++++++++++--
 src/test/regress/regression.diffs             |  12 +++
 17 files changed, 408 insertions(+), 86 deletions(-)
 create mode 100644 src/test/regress/regression.diffs

diff --git a/src/backend/access/heap/vacuumlazy.c b/src/backend/access/heap/vacuumlazy.c
index 991fb831726..f1b3be70785 100644
--- a/src/backend/access/heap/vacuumlazy.c
+++ b/src/backend/access/heap/vacuumlazy.c
@@ -363,9 +363,6 @@ extvac_stats_start(Relation rel, LVExtStatCounters *counters)
 {
 	TimestampTz	starttime;

 	starttime = GetCurrentTimestamp();
@@ -404,9 +401,6 @@ extvac_stats_end(Relation rel, LVExtStatCounters *counters,
 	long		secs;
 	int			usecs;
 
@@ -454,9 +448,6 @@ void
 	/* Set initial values for common heap and index statistics*/
@@ -531,7 +522,7 @@ accumulate_heap_vacuum_statistics(LVExtStatCounters *extVacCounters, LVRelState
 
 /*
@@ -850,15 +841,8 @@ heap_vacuum_rel(Relation rel, VacuumParams *params,
 	 *
 	 * We are ready to send vacuum statistics information for heap relations.
 	 */
-	if(pgstat_track_vacuum_statistics)
-	{
-		/* Make generic extended vacuum stats report and
-		 * fill heap-specific extended stats fields.
-		 */
-		extvac_stats_end(vacrel->rel, &extVacCounters, &(vacrel->extVacReport));
-		accumulate_heap_vacuum_statistics(&extVacCounters, vacrel);
-
-		pgstat_report_vacuum(RelationGetRelid(rel),
+	accumulate_heap_vacuum_statistics(&extVacCounters, vacrel);
+	pgstat_report_vacuum(RelationGetRelid(rel),
 						 rel->rd_rel->relisshared,
 						 Max(vacrel->new_live_tuples, 0),
 						 vacrel->recently_dead_tuples +
@@ -866,18 +850,6 @@ heap_vacuum_rel(Relation rel, VacuumParams *params,
 						 starttime,
 						 &(vacrel->extVacReport));
 
-	}
-	else
-	{
-							 starttime,
 
 	if (instrument)
@@ -2898,14 +2870,11 @@ lazy_vacuum_one_index(Relation indrel, IndexBulkDeleteResult *istat,
 	istat = vac_bulkdel_one_index(&ivinfo, istat, vacrel->dead_items,
 								  vacrel->dead_items_info);
 
-								0, 0, 0, &extVacReport);
 
 	/* Revert to the previous phase information for error traceback */
 	restore_vacuum_error_info(vacrel, &saved_err_info);
@@ -2962,14 +2931,11 @@ lazy_cleanup_one_index(Relation indrel, IndexBulkDeleteResult *istat,

-								0, 0, 0, &extVacReport);
 
 	/* Revert to the previous phase information for error traceback */
 	restore_vacuum_error_info(vacrel, &saved_err_info);
@@ -3583,6 +3549,9 @@ vacuum_error_callback(void *arg)
 	switch (errinfo->phase)
 	{
 		case VACUUM_ERRCB_PHASE_SCAN_HEAP:
+			if(geterrelevel() == ERROR)
+					pgstat_report_vacuum_error(errinfo->reloid, PGSTAT_EXTVAC_TABLE);
+
 			if (BlockNumberIsValid(errinfo->blkno))
 			{
 				if (OffsetNumberIsValid(errinfo->offnum))
@@ -3598,6 +3567,9 @@ vacuum_error_callback(void *arg)
 			break;
 
@@ -3613,16 +3585,25 @@ vacuum_error_callback(void *arg)
 			break;

 

 /* ----------

 	return true;
 }
index cd4ffb50bca..6d45db6d3d2 100644
 
+/* ---------
@@ -237,7 +270,8 @@ pgstat_report_vacuum(Oid tableoid, bool shared,

 	/*
@@ -274,6 +308,16 @@ pgstat_report_vacuum(Oid tableoid, bool shared,
 
 /*
@@ -1007,9 +1051,6 @@ static void
@@ -1021,6 +1062,8 @@ pgstat_accumulate_extvac_stats(ExtVacReport *dst, ExtVacReport *src,
 
@@ -1048,7 +1091,6 @@ pgstat_accumulate_extvac_stats(ExtVacReport *dst, ExtVacReport *src,

 

 
+	int32		errors;
 
 	/* ----------
 
 /*
 
 session s1
+setup		{
 
 session s2
+setup		{

 DROP TABLE vestat;
index b5ea9c9ab1e..f79ae58fb38 100644
 
@@ -165,7 +168,7 @@ FROM pg_stat_vacuum_tables WHERE relname = 'vestat' \gset
 SELECT :dwr3>0 AS dWR, :dfpi3=0 AS dFPI, :dwb3>0 AS dWB;
  dwr | dfpi | dwb 
 -----+------+-----
- t   | t    | t
+ f   | t    | f
 (1 row)

diff --git a/src/test/regress/regression.diffs b/src/test/regress/regression.diffs
index 00000000000..d4c24f41b40
+++ b/src/test/regress/regression.diffs
@@ -0,0 +1,12 @@
+diff -U3 /home/alena/postgrespro__copy61/src/test/regress/expected/vacuum_tables_and_db_statistics.out /home/alena/postgrespro__copy61/src/test/regress/results/vacuum_tables_and_db_statistics.out
+--- /home/alena/postgrespro__copy61/src/test/regress/expected/vacuum_tables_and_db_statistics.out	2025-02-04 18:03:12.645127559 +0300
++++ /home/alena/postgrespro__copy61/src/test/regress/results/vacuum_tables_and_db_statistics.out	2025-02-04 18:36:10.238349991 +0300
+@@ -168,7 +168,7 @@
+ SELECT :dwr3>0 AS dWR, :dfpi3=0 AS dFPI, :dwb3>0 AS dWB;
+ -----+------+-----
+- t   | t    | t
++ f   | t    | f
+ (1 row)
+ --

 

 DROP TABLE vestat;

---

From: Alexander Korotkov <[email-masked]>
Date: Wed, 5 Feb 2025 08:59:40 +0200

> vacuum_ext structure stored in PgStat_StatDBEntry will be fill be.

What is the point for disabling pgstat_track_vacuum_statistics then?
I don't see it saves any valuable resources.  The original point by
Masahiko Sawada was growth of data structures in times [1] (and
corresponding memory consumption especially with large number of
tables).  Now, disabling pgstat_track_vacuum_statistics only saves
some cycles of pgstat_accumulate_extvac_stats(), and that seems
insignificant.

I see that we use hash tables with static element size.  So, we can't
save space by dynamically changing entries size on the base of GUC.
But could we move vacuum statistics to separate hash tables?  When GUC
is disabled, new hash tables could be just empty.

Links
1. https://www.postgresql.org/message-id/CAD21AoD66b3u28n%3D73kudgMp5wiGiyYUN9LuC9z2ka6YTru5Gw%40mail.gmail.com

------
Regards,
Alexander Korotkov
Supabase

---

From: Alena Rybakina <[email-masked]>
Date: Wed, 5 Feb 2025 15:03:24 +0300

On 05.02.2025 09:59, Alexander Korotkov wrote:
> 1.https://www.postgresql.org/message-id/CAD21AoD66b3u28n%3D73kudgMp5wiGiyYUN9LuC9z2ka6YTru5Gw%40mail.gmail.com
>
I understand what you're talking about. I'm looking at the 
pgstat_assoc_relation function and I think that's where I need to decide 
whether we need to allocate memory in the hash table for vacuum 
statistics for them or not.
The same thing happens there depending on the installed 
pgstat_track_counts guc and pgstat_enabled value consequently. Like here:

Specifically, there is an example that for partitions, for example, 
statistics are not accumulated and the condition used like that, like here:

if(!pgstat_track_counts)
{
if(rel->pgstat_info)
pgstat_unlink_relation(rel);
/* We're not counting at all */
rel->pgstat_enabled= false;
rel->pgstat_info= NULL;
return;
}
I think I can try yo add an external parameter in the relation like 
ext_vacuum_pgstat_info and determine its values depending on the guc's 
pgstat_track_vacuum_statisticsvalue.

-- 
Regards,
Alena Rybakina
Postgres Professional

---

From: Alena Rybakina <[email-masked]>
Date: Mon, 17 Feb 2025 17:46:16 +0300

On 04.02.2025 18:22, Alena Rybakina wrote:
>>> I noticed that the cfbot is bad, the reason seems to be related to 
>>> the lack of a parameter in 
>>> src/backend/utils/misc/postgresql.conf.sample. I added it, it should 
>>> help.
The patch needed a rebase again. There is nothing new since version 18, 
only a rebase.

-- 
Regards,
Alena Rybakina
Postgres Professional
From fc069a3a6319b5bf40d2f0f1efceae1c9b7a68a8 Mon Sep 17 00:00:00 2001
From: Alexander Korotkov <[email-masked]>
Date: Thu, 13 Feb 2025 00:56:03 +0200
Subject: [PATCH 1/4] Implement Self-Join Elimination
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

The Self-Join Elimination (SJE) feature removes an inner join of a plain
table to itself in the query tree if it is proven that the join can be
replaced with a scan without impacting the query result.  Self-join and
inner relation get replaced with the outer in query, equivalence classes,
and planner info structures.  Also, the inner restrictlist moves to the
outer one with the removal of duplicated clauses.  Thus, this optimization
reduces the length of the range table list (this especially makes sense for
partitioned relations), reduces the number of restriction clauses and,
in turn, selectivity estimations, and potentially improves total planner
prediction for the query.

This feature is dedicated to avoiding redundancy, which can appear after
pull-up transformations or the creation of an EquivalenceClass-derived clause
like the below.

  SELECT * FROM t1 WHERE x IN (SELECT t3.x FROM t1 t3);
  SELECT * FROM t1 WHERE EXISTS (SELECT t3.x FROM t1 t3 WHERE t3.x = t1.x);
  SELECT * FROM t1,t2, t1 t3 WHERE t1.x = t2.x AND t2.x = t3.x;

In the future, we could also reduce redundancy caused by subquery pull-up
after unnecessary outer join removal in cases like the one below.

  SELECT * FROM t1 WHERE x IN
    (SELECT t3.x FROM t1 t3 LEFT JOIN t2 ON t2.x = t1.x);

Also, it can drastically help to join partitioned tables, removing entries
even before their expansion.

The SJE proof is based on innerrel_is_unique() machinery.

We can remove a self-join when for each outer row:

 1. At most, one inner row matches the join clause;
 2. Each matched inner row must be (physically) the same as the outer one;
 3. Inner and outer rows have the same row mark.

In this patch, we use the next approach to identify a self-join:

 1. Collect all merge-joinable join quals which look like a.x = b.x;
 2. Add to the list above the baseretrictinfo of the inner table;
 3. Check innerrel_is_unique() for the qual list.  If it returns false, skip
    this pair of joining tables;
 4. Check uniqueness, proved by the baserestrictinfo clauses. To prove the
    possibility of self-join elimination, the inner and outer clauses must
    match exactly.

The relation replacement procedure is not trivial and is partly combined
with the one used to remove useless left joins.  Tests covering this feature
were added to join.sql.  Some of the existing regression tests changed due
to self-join removal logic.

Discussion: https://postgr.es/m/flat/64486b0b-0404-e39e-322d-0801154901f3%40postgrespro.ru
Author: Andrey Lepikhov <[email-masked]>
Author: Alexander Kuzmenkov <[email-masked]>
Co-authored-by: Alexander Korotkov <[email-masked]>
Co-authored-by: Alena Rybakina <[email-masked]>
Reviewed-by: Tom Lane <[email-masked]>
Reviewed-by: Robert Haas <[email-masked]>
Reviewed-by: Andres Freund <[email-masked]>
Reviewed-by: Simon Riggs <[email-masked]>
Reviewed-by: Jonathan S. Katz <[email-masked]>
Reviewed-by: David Rowley <[email-masked]>
Reviewed-by: Thomas Munro <[email-masked]>
Reviewed-by: Konstantin Knizhnik <[email-masked]>
Reviewed-by: Heikki Linnakangas <[email-masked]>
Reviewed-by: Hywel Carver <[email-masked]>
Reviewed-by: Laurenz Albe <[email-masked]>
Reviewed-by: Ronan Dunklau <[email-masked]>
Reviewed-by: vignesh C <[email-masked]>
Reviewed-by: Zhihong Yu <[email-masked]>
Reviewed-by: Greg Stark <[email-masked]>
Reviewed-by: Jaime Casanova <[email-masked]>
Reviewed-by: Michał Kłeczek <[email-masked]>
Reviewed-by: Alena Rybakina <[email-masked]>
Reviewed-by: Alexander Korotkov <[email-masked]>
---
 doc/src/sgml/config.sgml                  |   16 +
 src/backend/optimizer/path/equivclass.c   |    3 +-
 src/backend/optimizer/path/indxpath.c     |   39 +
 src/backend/optimizer/plan/analyzejoins.c | 1240 +++++++++++++++++++--
 src/backend/optimizer/plan/planmain.c     |    5 +
 src/backend/optimizer/prep/prepunion.c    |    9 +-
 src/backend/rewrite/rewriteManip.c        |  126 ++-
 src/backend/utils/misc/guc_tables.c       |   10 +
 src/include/nodes/pathnodes.h             |   40 +-
 src/include/optimizer/optimizer.h         |    2 +
 src/include/optimizer/paths.h             |    3 +
 src/include/optimizer/planmain.h          |    6 +
 src/include/rewrite/rewriteManip.h        |    4 +
 src/test/regress/expected/equivclass.out  |   30 +
 src/test/regress/expected/join.out        | 1083 ++++++++++++++++++
 src/test/regress/expected/sysviews.out    |    3 +-
 src/test/regress/sql/equivclass.sql       |   16 +
 src/test/regress/sql/join.sql             |  494 ++++++++
 src/tools/pgindent/typedefs.list          |    2 +
 19 files changed, 2983 insertions(+), 148 deletions(-)

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 60829b79d83..336630ce417 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -5544,6 +5544,22 @@ ANY <replaceable class="parameter">num_sync</replaceable> ( <replaceable class="
       </listitem>
      </varlistentry>
 
+     <varlistentry id="guc-enable_self_join_elimination" xreflabel="enable_self_join_elimination">
+      <term><varname>enable_self_join_elimination</varname> (<type>boolean</type>)
+      <indexterm>
+       <primary><varname>enable_self_join_elimination</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        Enables or disables the query planner's optimization which analyses
+        the query tree and replaces self joins with semantically equivalent
+        single scans.  Takes into consideration only plain tables.
+        The default is <literal>on</literal>.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="guc-enable-seqscan" xreflabel="enable_seqscan">
       <term><varname>enable_seqscan</varname> (<type>boolean</type>)
       <indexterm>
diff --git a/src/backend/optimizer/path/equivclass.c b/src/backend/optimizer/path/equivclass.c
index 7cafaca33c5..0f9ecf5ee8b 100644
--- a/src/backend/optimizer/path/equivclass.c
+++ b/src/backend/optimizer/path/equivclass.c
@@ -852,7 +852,8 @@ find_computable_ec_member(PlannerInfo *root,
 	exprvars = pull_var_clause((Node *) exprs,
 							   PVC_INCLUDE_AGGREGATES |
 							   PVC_INCLUDE_WINDOWFUNCS |
-							   PVC_INCLUDE_PLACEHOLDERS);
+							   PVC_INCLUDE_PLACEHOLDERS |
+							   PVC_INCLUDE_CONVERTROWTYPES);
 
 	foreach(lc, ec->ec_members)
 	{
diff --git a/src/backend/optimizer/path/indxpath.c b/src/backend/optimizer/path/indxpath.c
index 6e2051efc65..a43ca16d683 100644
--- a/src/backend/optimizer/path/indxpath.c
+++ b/src/backend/optimizer/path/indxpath.c
@@ -4162,6 +4162,22 @@ bool
 relation_has_unique_index_for(PlannerInfo *root, RelOptInfo *rel,
 							  List *restrictlist,
 							  List *exprlist, List *oprlist)
+{
+	return relation_has_unique_index_ext(root, rel, restrictlist,
+										 exprlist, oprlist, NULL);
+}
+
+/*
+ * relation_has_unique_index_ext
+ *	  Same as relation_has_unique_index_for(), but supports extra_clauses
+ *	  parameter.  If extra_clauses isn't NULL, return baserestrictinfo clauses
+ *	  which were used to derive uniqueness.
+ */
+bool
+relation_has_unique_index_ext(PlannerInfo *root, RelOptInfo *rel,
+							  List *restrictlist,
+							  List *exprlist, List *oprlist,
+							  List **extra_clauses)
 {
 	ListCell   *ic;
 
@@ -4217,6 +4233,7 @@ relation_has_unique_index_for(PlannerInfo *root, RelOptInfo *rel,
 	{
 		IndexOptInfo *ind = (IndexOptInfo *) lfirst(ic);
 		int			c;
+		List	   *exprs = NIL;
 
 		/*
 		 * If the index is not unique, or not immediately enforced, or if it's
@@ -4268,6 +4285,24 @@ relation_has_unique_index_for(PlannerInfo *root, RelOptInfo *rel,
 				if (match_index_to_operand(rexpr, c, ind))
 				{
 					matched = true; /* column is unique */
+
+					if (bms_membership(rinfo->clause_relids) == BMS_SINGLETON)
+					{
+						MemoryContext oldMemCtx =
+							MemoryContextSwitchTo(root->planner_cxt);
+
+						/*
+						 * Add filter clause into a list allowing caller to
+						 * know if uniqueness have made not only by join
+						 * clauses.
+						 */
+						Assert(bms_is_empty(rinfo->left_relids) ||
+							   bms_is_empty(rinfo->right_relids));
+						if (extra_clauses)
+							exprs = lappend(exprs, rinfo);
+						MemoryContextSwitchTo(oldMemCtx);
+					}
+
 					break;
 				}
 			}
@@ -4310,7 +4345,11 @@ relation_has_unique_index_for(PlannerInfo *root, RelOptInfo *rel,
 
 		/* Matched all key columns of this index? */
 		if (c == ind->nkeycolumns)
+		{
+			if (extra_clauses)
+				*extra_clauses = exprs;
 			return true;
+		}
 	}
 
 	return false;
diff --git a/src/backend/optimizer/plan/analyzejoins.c b/src/backend/optimizer/plan/analyzejoins.c
index b33fc671775..3aa04d0d4e1 100644
--- a/src/backend/optimizer/plan/analyzejoins.c
+++ b/src/backend/optimizer/plan/analyzejoins.c
@@ -22,6 +22,7 @@
  */
 #include "postgres.h"
 
+#include "catalog/pg_class.h"
 #include "nodes/nodeFuncs.h"
 #include "optimizer/joininfo.h"
 #include "optimizer/optimizer.h"
@@ -30,27 +31,48 @@
 #include "optimizer/placeholder.h"
 #include "optimizer/planmain.h"
 #include "optimizer/restrictinfo.h"
+#include "rewrite/rewriteManip.h"
 #include "utils/lsyscache.h"
 
+/*
+ * Utility structure.  A sorting procedure is needed to simplify the search
+ * of SJE-candidate baserels referencing the same database relation.  Having
+ * collected all baserels from the query jointree, the planner sorts them
+ * according to the reloid value, groups them with the next pass and attempts
+ * to remove self-joins.
+ *
+ * Preliminary sorting prevents quadratic behavior that can be harmful in the
+ * case of numerous joins.
+ */
+typedef struct
+{
+	int			relid;
+	Oid			reloid;
+} SelfJoinCandidate;
+
+bool		enable_self_join_elimination;
+
 /* local functions */
 static bool join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo);
-static void remove_rel_from_query(PlannerInfo *root, int relid,
-								  SpecialJoinInfo *sjinfo);
+static void remove_leftjoinrel_from_query(PlannerInfo *root, int relid,
+										  SpecialJoinInfo *sjinfo);
 static void remove_rel_from_restrictinfo(RestrictInfo *rinfo,
 										 int relid, int ojrelid);
 static void remove_rel_from_eclass(EquivalenceClass *ec,
-								   int relid, int ojrelid);
+								   int relid, int ojrelid, int subst);
 static List *remove_rel_from_joinlist(List *joinlist, int relid, int *nremoved);
 static bool rel_supports_distinctness(PlannerInfo *root, RelOptInfo *rel);
 static bool rel_is_distinct_for(PlannerInfo *root, RelOptInfo *rel,
-								List *clause_list);
+								List *clause_list, List **extra_clauses);
 static Oid	distinct_col_search(int colno, List *colnos, List *opids);
 static bool is_innerrel_unique_for(PlannerInfo *root,
 								   Relids joinrelids,
 								   Relids outerrelids,
 								   RelOptInfo *innerrel,
 								   JoinType jointype,
-								   List *restrictlist);
+								   List *restrictlist,
+								   List **extra_clauses);
+static int	self_join_candidates_cmp(const void *a, const void *b);

 /*
@@ -88,7 +110,7 @@ restart:
 		 */
 		innerrelid = bms_singleton_member(sjinfo->min_righthand);
 
-		remove_rel_from_query(root, innerrelid, sjinfo);
+		remove_leftjoinrel_from_query(root, innerrelid, sjinfo);
 
 		/* We verify that exactly one reference gets removed from joinlist */
 		nremoved = 0;
@@ -276,7 +298,7 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 	 * Now that we have the relevant equality join clauses, try to prove the
 	 * innerrel distinct.
 	 */
-	if (rel_is_distinct_for(root, innerrel, clause_list))
+	if (rel_is_distinct_for(root, innerrel, clause_list, NULL))
 		return true;
 
 	/*
@@ -288,36 +310,31 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)

 /*
- * Remove the target relid and references to the target join from the
+ * Remove the target rel->relid and references to the target join from the
  * planner's data structures, having determined that there is no need
- * to include them in the query.
+ * to include them in the query. Optionally replace them with subst if subst
+ * is non-negative.
  *
- * We are not terribly thorough here.  We only bother to update parts of
- * the planner's data structures that will actually be consulted later.
+ * This function updates only parts needed for both left-join removal and
+ * self-join removal.
  */
 static void
-remove_rel_from_query(PlannerInfo *root, int relid, SpecialJoinInfo *sjinfo)
+remove_rel_from_query(PlannerInfo *root, RelOptInfo *rel,
+					  int subst, SpecialJoinInfo *sjinfo,
+					  Relids joinrelids)
 {
-	RelOptInfo *rel = find_base_rel(root, relid);
-	int			ojrelid = sjinfo->ojrelid;
-	Relids		joinrelids;
-	Relids		join_plus_commute;
-	List	   *joininfos;
+	int			relid = rel->relid;
+	int			ojrelid = (sjinfo != NULL) ? sjinfo->ojrelid : -1;
 	Index		rti;
 	ListCell   *l;
 
-	/* Compute the relid set for the join we are considering */
-	joinrelids = bms_union(sjinfo->min_lefthand, sjinfo->min_righthand);
-	Assert(ojrelid != 0);
-	joinrelids = bms_add_member(joinrelids, ojrelid);
-
 	/*
 	 * Update all_baserels and related relid sets.
 	 */
-	root->all_baserels = bms_del_member(root->all_baserels, relid);
-	root->outer_join_rels = bms_del_member(root->outer_join_rels, ojrelid);
-	root->all_query_rels = bms_del_member(root->all_query_rels, relid);
-	root->all_query_rels = bms_del_member(root->all_query_rels, ojrelid);
+	root->all_baserels = adjust_relid_set(root->all_baserels, relid, subst);
+	root->outer_join_rels = adjust_relid_set(root->outer_join_rels, ojrelid, subst);
+	root->all_query_rels = adjust_relid_set(root->all_query_rels, relid, subst);
+	root->all_query_rels = adjust_relid_set(root->all_query_rels, ojrelid, subst);
 
 	/*
 	 * Likewise remove references from SpecialJoinInfo data structures.
@@ -341,20 +358,33 @@ remove_rel_from_query(PlannerInfo *root, int relid, SpecialJoinInfo *sjinfo)
 		sjinf->min_righthand = bms_copy(sjinf->min_righthand);
 		sjinf->syn_lefthand = bms_copy(sjinf->syn_lefthand);
 		sjinf->syn_righthand = bms_copy(sjinf->syn_righthand);
-		/* Now remove relid and ojrelid bits from the sets: */
-		sjinf->min_lefthand = bms_del_member(sjinf->min_lefthand, relid);
-		sjinf->min_righthand = bms_del_member(sjinf->min_righthand, relid);
-		sjinf->syn_lefthand = bms_del_member(sjinf->syn_lefthand, relid);
-		sjinf->syn_righthand = bms_del_member(sjinf->syn_righthand, relid);
-		sjinf->min_lefthand = bms_del_member(sjinf->min_lefthand, ojrelid);
-		sjinf->min_righthand = bms_del_member(sjinf->min_righthand, ojrelid);
-		sjinf->syn_lefthand = bms_del_member(sjinf->syn_lefthand, ojrelid);
-		sjinf->syn_righthand = bms_del_member(sjinf->syn_righthand, ojrelid);
-		/* relid cannot appear in these fields, but ojrelid can: */
-		sjinf->commute_above_l = bms_del_member(sjinf->commute_above_l, ojrelid);
-		sjinf->commute_above_r = bms_del_member(sjinf->commute_above_r, ojrelid);
-		sjinf->commute_below_l = bms_del_member(sjinf->commute_below_l, ojrelid);
-		sjinf->commute_below_r = bms_del_member(sjinf->commute_below_r, ojrelid);
+		/* Now remove relid from the sets: */
+		sjinf->min_lefthand = adjust_relid_set(sjinf->min_lefthand, relid, subst);
+		sjinf->min_righthand = adjust_relid_set(sjinf->min_righthand, relid, subst);
+		sjinf->syn_lefthand = adjust_relid_set(sjinf->syn_lefthand, relid, subst);
+		sjinf->syn_righthand = adjust_relid_set(sjinf->syn_righthand, relid, subst);
+
+		if (sjinfo != NULL)
+		{
+			Assert(subst <= 0 && ojrelid > 0);
+
+			/* Remove ojrelid bits from the sets: */
+			sjinf->min_lefthand = bms_del_member(sjinf->min_lefthand, ojrelid);
+			sjinf->min_righthand = bms_del_member(sjinf->min_righthand, ojrelid);
+			sjinf->syn_lefthand = bms_del_member(sjinf->syn_lefthand, ojrelid);
+			sjinf->syn_righthand = bms_del_member(sjinf->syn_righthand, ojrelid);
+			/* relid cannot appear in these fields, but ojrelid can: */
+			sjinf->commute_above_l = bms_del_member(sjinf->commute_above_l, ojrelid);
+			sjinf->commute_above_r = bms_del_member(sjinf->commute_above_r, ojrelid);
+			sjinf->commute_below_l = bms_del_member(sjinf->commute_below_l, ojrelid);
+			sjinf->commute_below_r = bms_del_member(sjinf->commute_below_r, ojrelid);
+		}
+		else
+		{
+			Assert(subst > 0 && ojrelid == -1);
+
+			ChangeVarNodes((Node *) sjinf->semi_rhs_exprs, relid, subst, 0);
+		}
 	}
 
 	/*
@@ -375,10 +405,10 @@ remove_rel_from_query(PlannerInfo *root, int relid, SpecialJoinInfo *sjinfo)
 	{
 		PlaceHolderInfo *phinfo = (PlaceHolderInfo *) lfirst(l);
 
-		Assert(!bms_is_member(relid, phinfo->ph_lateral));
+		Assert(sjinfo == NULL || !bms_is_member(relid, phinfo->ph_lateral));
 		if (bms_is_subset(phinfo->ph_needed, joinrelids) &&
 			bms_is_member(relid, phinfo->ph_eval_at) &&
-			!bms_is_member(ojrelid, phinfo->ph_eval_at))
+			(sjinfo == NULL || !bms_is_member(ojrelid, phinfo->ph_eval_at)))
 		{
 			root->placeholder_list = foreach_delete_current(root->placeholder_list,
 															l);
@@ -388,21 +418,112 @@ remove_rel_from_query(PlannerInfo *root, int relid, SpecialJoinInfo *sjinfo)
 		{
 			PlaceHolderVar *phv = phinfo->ph_var;
 
-			phinfo->ph_eval_at = bms_del_member(phinfo->ph_eval_at, relid);
-			phinfo->ph_eval_at = bms_del_member(phinfo->ph_eval_at, ojrelid);
+			phinfo->ph_eval_at = adjust_relid_set(phinfo->ph_eval_at, relid, subst);
+			phinfo->ph_eval_at = adjust_relid_set(phinfo->ph_eval_at, ojrelid, subst);
 			Assert(!bms_is_empty(phinfo->ph_eval_at));	/* checked previously */
 			/* Reduce ph_needed to contain only "relation 0"; see below */
 			if (bms_is_member(0, phinfo->ph_needed))
 				phinfo->ph_needed = bms_make_singleton(0);
 			else
 				phinfo->ph_needed = NULL;
-			phv->phrels = bms_del_member(phv->phrels, relid);
-			phv->phrels = bms_del_member(phv->phrels, ojrelid);
+
+			phinfo->ph_lateral = adjust_relid_set(phinfo->ph_lateral, relid, subst);
+
+			/*
+			 * ph_lateral might contain rels mentioned in ph_eval_at after the
+			 * replacement, remove them.
+			 */
+			phinfo->ph_lateral = bms_difference(phinfo->ph_lateral, phinfo->ph_eval_at);
+			/* ph_lateral might or might not be empty */
+
+			phv->phrels = adjust_relid_set(phv->phrels, relid, subst);
+			phv->phrels = adjust_relid_set(phv->phrels, ojrelid, subst);
 			Assert(!bms_is_empty(phv->phrels));
+
+			ChangeVarNodes((Node *) phv->phexpr, relid, subst, 0);
+
 			Assert(phv->phnullingrels == NULL); /* no need to adjust */
 		}
 	}
 
+	/*
+	 * Likewise remove references from EquivalenceClasses.
+	 */
+	foreach(l, root->eq_classes)
+	{
+		EquivalenceClass *ec = (EquivalenceClass *) lfirst(l);
+
+		if (bms_is_member(relid, ec->ec_relids) ||
+			(sjinfo == NULL || bms_is_member(ojrelid, ec->ec_relids)))
+			remove_rel_from_eclass(ec, relid, ojrelid, subst);
+	}
+
+	/*
+	 * Finally, we must recompute per-Var attr_needed and per-PlaceHolderVar
+	 * ph_needed relid sets.  These have to be known accurately, else we may
+	 * fail to remove other now-removable outer joins.  And our removal of the
+	 * join clause(s) for this outer join may mean that Vars that were
+	 * formerly needed no longer are.  So we have to do this honestly by
+	 * repeating the construction of those relid sets.  We can cheat to one
+	 * small extent: we can avoid re-examining the targetlist and HAVING qual
+	 * by preserving "relation 0" bits from the existing relid sets.  This is
+	 * safe because we'd never remove such references.
+	 *
+	 * So, start by removing all other bits from attr_needed sets and
+	 * lateral_vars lists.  (We already did this above for ph_needed.)
+	 */
+	for (rti = 1; rti < root->simple_rel_array_size; rti++)
+	{
+		RelOptInfo *otherrel = root->simple_rel_array[rti];
+		int			attroff;
+
+		/* there may be empty slots corresponding to non-baserel RTEs */
+		if (otherrel == NULL)
+			continue;
+
+		Assert(otherrel->relid == rti); /* sanity check on array */
+
+		for (attroff = otherrel->max_attr - otherrel->min_attr;
+			 attroff >= 0;
+			 attroff--)
+		{
+			if (bms_is_member(0, otherrel->attr_needed[attroff]))
+				otherrel->attr_needed[attroff] = bms_make_singleton(0);
+			else
+				otherrel->attr_needed[attroff] = NULL;
+		}
+
+		if (subst > 0)
+			ChangeVarNodes((Node *) otherrel->lateral_vars, relid, subst, 0);
+	}
+}
+
+/*
+ * Remove the target relid and references to the target join from the
+ * planner's data structures, having determined that there is no need
+ * to include them in the query.
+ *
+ * We are not terribly thorough here.  We only bother to update parts of
+ * the planner's data structures that will actually be consulted later.
+ */
+static void
+remove_leftjoinrel_from_query(PlannerInfo *root, int relid,
+							  SpecialJoinInfo *sjinfo)
+{
+	RelOptInfo *rel = find_base_rel(root, relid);
+	int			ojrelid = sjinfo->ojrelid;
+	Relids		joinrelids;
+	Relids		join_plus_commute;
+	List	   *joininfos;
+	ListCell   *l;
+
+	/* Compute the relid set for the join we are considering */
+	joinrelids = bms_union(sjinfo->min_lefthand, sjinfo->min_righthand);
+	Assert(ojrelid != 0);
+	joinrelids = bms_add_member(joinrelids, ojrelid);
+
+	remove_rel_from_query(root, rel, -1, sjinfo, joinrelids);
+
 	/*
 	 * Remove any joinquals referencing the rel from the joininfo lists.
 	 *
@@ -465,18 +586,6 @@ remove_rel_from_query(PlannerInfo *root, int relid, SpecialJoinInfo *sjinfo)
 		}
 	}
 
-	/*
-	 * Likewise remove references from EquivalenceClasses.
-	 */
-	foreach(l, root->eq_classes)
-	{
-		EquivalenceClass *ec = (EquivalenceClass *) lfirst(l);
-
-		if (bms_is_member(relid, ec->ec_relids) ||
-			bms_is_member(ojrelid, ec->ec_relids))
-			remove_rel_from_eclass(ec, relid, ojrelid);
-	}
-
 	/*
 	 * There may be references to the rel in root->fkey_list, but if so,
 	 * match_foreign_keys_to_quals() will get rid of them.
@@ -492,42 +601,6 @@ remove_rel_from_query(PlannerInfo *root, int relid, SpecialJoinInfo *sjinfo)
 	/* And nuke the RelOptInfo, just in case there's another access path */
 	pfree(rel);
 
-	/*
-	 * Finally, we must recompute per-Var attr_needed and per-PlaceHolderVar
-	 * ph_needed relid sets.  These have to be known accurately, else we may
-	 * fail to remove other now-removable outer joins.  And our removal of the
-	 * join clause(s) for this outer join may mean that Vars that were
-	 * formerly needed no longer are.  So we have to do this honestly by
-	 * repeating the construction of those relid sets.  We can cheat to one
-	 * small extent: we can avoid re-examining the targetlist and HAVING qual
-	 * by preserving "relation 0" bits from the existing relid sets.  This is
-	 * safe because we'd never remove such references.
-	 *
-	 * So, start by removing all other bits from attr_needed sets.  (We
-	 * already did this above for ph_needed.)
-	 */
-	for (rti = 1; rti < root->simple_rel_array_size; rti++)
-	{
-		RelOptInfo *otherrel = root->simple_rel_array[rti];
-		int			attroff;
-
-		/* there may be empty slots corresponding to non-baserel RTEs */
-		if (otherrel == NULL)
-			continue;
-
-		Assert(otherrel->relid == rti); /* sanity check on array */
-
-		for (attroff = otherrel->max_attr - otherrel->min_attr;
-			 attroff >= 0;
-			 attroff--)
-		{
-			if (bms_is_member(0, otherrel->attr_needed[attroff]))
-				otherrel->attr_needed[attroff] = bms_make_singleton(0);
-			else
-				otherrel->attr_needed[attroff] = NULL;
-		}
-	}
-
 	/*
 	 * Now repeat construction of attr_needed bits coming from all other
 	 * sources.
@@ -607,13 +680,13 @@ remove_rel_from_restrictinfo(RestrictInfo *rinfo, int relid, int ojrelid)
  * level(s).
  */
 static void
-remove_rel_from_eclass(EquivalenceClass *ec, int relid, int ojrelid)
+remove_rel_from_eclass(EquivalenceClass *ec, int relid, int ojrelid, int subst)
 {
 	ListCell   *lc;
 
 	/* Fix up the EC's overall relids */
-	ec->ec_relids = bms_del_member(ec->ec_relids, relid);
-	ec->ec_relids = bms_del_member(ec->ec_relids, ojrelid);
+	ec->ec_relids = adjust_relid_set(ec->ec_relids, relid, subst);
+	ec->ec_relids = adjust_relid_set(ec->ec_relids, ojrelid, subst);
 
 	/*
 	 * Fix up the member expressions.  Any non-const member that ends with
@@ -625,11 +698,11 @@ remove_rel_from_eclass(EquivalenceClass *ec, int relid, int ojrelid)
 		EquivalenceMember *cur_em = (EquivalenceMember *) lfirst(lc);
 
 		if (bms_is_member(relid, cur_em->em_relids) ||
-			bms_is_member(ojrelid, cur_em->em_relids))
+			(ojrelid != -1 && bms_is_member(ojrelid, cur_em->em_relids)))
 		{
 			Assert(!cur_em->em_is_const);
-			cur_em->em_relids = bms_del_member(cur_em->em_relids, relid);
-			cur_em->em_relids = bms_del_member(cur_em->em_relids, ojrelid);
+			cur_em->em_relids = adjust_relid_set(cur_em->em_relids, relid, subst);
+			cur_em->em_relids = adjust_relid_set(cur_em->em_relids, ojrelid, subst);
 			if (bms_is_empty(cur_em->em_relids))
 				ec->ec_members = foreach_delete_current(ec->ec_members, lc);
 		}
@@ -640,7 +713,10 @@ remove_rel_from_eclass(EquivalenceClass *ec, int relid, int ojrelid)
 	{
 		RestrictInfo *rinfo = (RestrictInfo *) lfirst(lc);
 
-		remove_rel_from_restrictinfo(rinfo, relid, ojrelid);
+		if (ojrelid == -1)
+			ChangeVarNodes((Node *) rinfo, relid, subst, 0);
+		else
+			remove_rel_from_restrictinfo(rinfo, relid, ojrelid);
 	}
 
 	/*
@@ -844,9 +920,15 @@ rel_supports_distinctness(PlannerInfo *root, RelOptInfo *rel)
  * Note that the passed-in clause_list may be destructively modified!  This
  * is OK for current uses, because the clause_list is built by the caller for
  * the sole purpose of passing to this function.
+ *
+ * (*extra_clauses) to be set to the right sides of baserestrictinfo clauses,
+ * looking like "x = const" if distinctness is derived from such clauses, not
+ * joininfo clauses.  Pass NULL to the extra_clauses if this value is not
+ * needed.
  */
 static bool
-rel_is_distinct_for(PlannerInfo *root, RelOptInfo *rel, List *clause_list)
+rel_is_distinct_for(PlannerInfo *root, RelOptInfo *rel, List *clause_list,
+					List **extra_clauses)
 {
 	/*
 	 * We could skip a couple of tests here if we assume all callers checked
@@ -859,10 +941,11 @@ rel_is_distinct_for(PlannerInfo *root, RelOptInfo *rel, List *clause_list)
 	{
 		/*
 		 * Examine the indexes to see if we have a matching unique index.
-		 * relation_has_unique_index_for automatically adds any usable
+		 * relation_has_unique_index_ext automatically adds any usable
 		 * restriction clauses for the rel, so we needn't do that here.
 		 */
-		if (relation_has_unique_index_for(root, rel, clause_list, NIL, NIL))
+		if (relation_has_unique_index_ext(root, rel, clause_list, NIL, NIL,
+										  extra_clauses))
 			return true;
 	}
 	else if (rel->rtekind == RTE_SUBQUERY)
@@ -1176,9 +1259,35 @@ innerrel_is_unique(PlannerInfo *root,
 				   JoinType jointype,
 				   List *restrictlist,
 				   bool force_cache)
+{
+	return innerrel_is_unique_ext(root, joinrelids, outerrelids, innerrel,
+								  jointype, restrictlist, force_cache, NULL);
+}
+
+/*
+ * innerrel_is_unique_ext
+ *	  Do the same as innerrel_is_unique(), but also set to (*extra_clauses)
+ *	  additional clauses from a baserestrictinfo list used to prove the
+ *	  uniqueness.
+ *
+ * A non-NULL extra_clauses indicates that we're checking for self-join and
+ * correspondingly dealing with filtered clauses.
+ */
+bool
+innerrel_is_unique_ext(PlannerInfo *root,
+					   Relids joinrelids,
+					   Relids outerrelids,
+					   RelOptInfo *innerrel,
+					   JoinType jointype,
+					   List *restrictlist,
+					   bool force_cache,
+					   List **extra_clauses)
 {
 	MemoryContext old_context;
 	ListCell   *lc;
+	UniqueRelInfo *uniqueRelInfo;
+	List	   *outer_exprs = NIL;
+	bool		self_join = (extra_clauses != NULL);
 
 	/* Certainly can't prove uniqueness when there are no joinclauses */
 	if (restrictlist == NIL)
@@ -1193,17 +1302,28 @@ innerrel_is_unique(PlannerInfo *root,
 
 	/*
 	 * Query the cache to see if we've managed to prove that innerrel is
-	 * unique for any subset of this outerrel.  We don't need an exact match,
-	 * as extra outerrels can't make the innerrel any less unique (or more
-	 * formally, the restrictlist for a join to a superset outerrel must be a
-	 * superset of the conditions we successfully used before).
+	 * unique for any subset of this outerrel.  For non-self-join search, we
+	 * don't need an exact match, as extra outerrels can't make the innerrel
+	 * any less unique (or more formally, the restrictlist for a join to a
+	 * superset outerrel must be a superset of the conditions we successfully
+	 * used before). For self-join search, we require an exact match of
+	 * outerrels because we need extra clauses to be valid for our case. Also,
+	 * for self-join checking we've filtered the clauses list.  Thus, we can
+	 * match only the result cached for a self-join search for another
+	 * self-join check.
 	 */
 	foreach(lc, innerrel->unique_for_rels)
 	{
-		Relids		unique_for_rels = (Relids) lfirst(lc);
+		uniqueRelInfo = (UniqueRelInfo *) lfirst(lc);
 
-		if (bms_is_subset(unique_for_rels, outerrelids))
+		if ((!self_join && bms_is_subset(uniqueRelInfo->outerrelids, outerrelids)) ||
+			(self_join && bms_equal(uniqueRelInfo->outerrelids, outerrelids) &&
+			 uniqueRelInfo->self_join))
+		{
+			if (extra_clauses)
+				*extra_clauses = uniqueRelInfo->extra_clauses;
 			return true;		/* Success! */
+		}
 	}
 
 	/*
@@ -1220,7 +1340,8 @@ innerrel_is_unique(PlannerInfo *root,
 
 	/* No cached information, so try to make the proof. */
 	if (is_innerrel_unique_for(root, joinrelids, outerrelids, innerrel,
-							   jointype, restrictlist))
+							   jointype, restrictlist,
+							   self_join ? &outer_exprs : NULL))
 	{
 		/*
 		 * Cache the positive result for future probes, being sure to keep it
@@ -1233,10 +1354,16 @@ innerrel_is_unique(PlannerInfo *root,
 		 * supersets of them anyway.
 		 */
 		old_context = MemoryContextSwitchTo(root->planner_cxt);
+		uniqueRelInfo = makeNode(UniqueRelInfo);
+		uniqueRelInfo->outerrelids = bms_copy(outerrelids);
+		uniqueRelInfo->self_join = self_join;
+		uniqueRelInfo->extra_clauses = outer_exprs;
 		innerrel->unique_for_rels = lappend(innerrel->unique_for_rels,
-											bms_copy(outerrelids));
+											uniqueRelInfo);
 		MemoryContextSwitchTo(old_context);
 
+		if (extra_clauses)
+			*extra_clauses = outer_exprs;
 		return true;			/* Success! */
 	}
 	else
@@ -1282,7 +1409,8 @@ is_innerrel_unique_for(PlannerInfo *root,
 					   Relids outerrelids,
 					   RelOptInfo *innerrel,
 					   JoinType jointype,
-					   List *restrictlist)
+					   List *restrictlist,
+					   List **extra_clauses)
 {
 	List	   *clause_list = NIL;
 	ListCell   *lc;
@@ -1312,17 +1440,895 @@ is_innerrel_unique_for(PlannerInfo *root,
 			continue;			/* not mergejoinable */
 
 		/*
-		 * Check if clause has the form "outer op inner" or "inner op outer",
-		 * and if so mark which side is inner.
+		 * Check if the clause has the form "outer op inner" or "inner op
+		 * outer", and if so mark which side is inner.
 		 */
 		if (!clause_sides_match_join(restrictinfo, outerrelids,
 									 innerrel->relids))
 			continue;			/* no good for these input relations */
 
-		/* OK, add to list */
+		/* OK, add to the list */
 		clause_list = lappend(clause_list, restrictinfo);
 	}
 
 	/* Let rel_is_distinct_for() do the hard work */
-	return rel_is_distinct_for(root, innerrel, clause_list);
+	return rel_is_distinct_for(root, innerrel, clause_list, extra_clauses);
+}
+
+/*
+ * Update EC members to point to the remaining relation instead of the removed
+ * one, removing duplicates.
+ *
+ * Restriction clauses for base relations are already distributed to
+ * the respective baserestrictinfo lists (see
+ * generate_implied_equalities_for_column). The above code has already processed
+ * this list and updated these clauses to reference the remaining
+ * relation, so that we can skip them here based on their relids.
+ *
+ * Likewise, we have already processed the join clauses that join the
+ * removed relation to the remaining one.
+ *
+ * Finally, there might be join clauses tying the removed relation to
+ * some third relation.  We can't just delete the source clauses and
+ * regenerate them from the EC because the corresponding equality
+ * operators might be missing (see the handling of ec_broken).
+ * Therefore, we will update the references in the source clauses.
+ *
+ * Derived clauses can be generated again, so it is simpler just to
+ * delete them.
+ */
+static void
+update_eclasses(EquivalenceClass *ec, int from, int to)
+{
+	List	   *new_members = NIL;
+	List	   *new_sources = NIL;
+
+	foreach_node(EquivalenceMember, em, ec->ec_members)
+	{
+		bool		is_redundant = false;
+
+		if (!bms_is_member(from, em->em_relids))
+		{
+			new_members = lappend(new_members, em);
+			continue;
+		}
+
+		em->em_relids = adjust_relid_set(em->em_relids, from, to);
+		em->em_jdomain->jd_relids = adjust_relid_set(em->em_jdomain->jd_relids, from, to);
+
+		/* We only process inner joins */
+		ChangeVarNodes((Node *) em->em_expr, from, to, 0);
+
+		foreach_node(EquivalenceMember, other, new_members)
+		{
+			if (!equal(em->em_relids, other->em_relids))
+				continue;
+
+			if (equal(em->em_expr, other->em_expr))
+			{
+				is_redundant = true;
+				break;
+			}
+		}
+
+		if (!is_redundant)
+			new_members = lappend(new_members, em);
+	}
+
+	list_free(ec->ec_members);
+	ec->ec_members = new_members;
+
+	list_free(ec->ec_derives);
+	ec->ec_derives = NULL;
+
+	/* Update EC source expressions */
+	foreach_node(RestrictInfo, rinfo, ec->ec_sources)
+	{
+		bool		is_redundant = false;
+
+		if (!bms_is_member(from, rinfo->required_relids))
+		{
+			new_sources = lappend(new_sources, rinfo);
+			continue;
+		}
+
+		ChangeVarNodes((Node *) rinfo, from, to, 0);
+
+		/*
+		 * After switching the clause to the remaining relation, check it for
+		 * redundancy with existing ones. We don't have to check for
+		 * redundancy with derived clauses, because we've just deleted them.
+		 */
+		foreach_node(RestrictInfo, other, new_sources)
+		{
+			if (!equal(rinfo->clause_relids, other->clause_relids))
+				continue;
+
+			if (equal(rinfo->clause, other->clause))
+			{
+				is_redundant = true;
+				break;
+			}
+		}
+
+		if (!is_redundant)
+			new_sources = lappend(new_sources, rinfo);
+	}
+
+	list_free(ec->ec_sources);
+	ec->ec_sources = new_sources;
+	ec->ec_relids = adjust_relid_set(ec->ec_relids, from, to);
+}
+
+/*
+ * "Logically" compares two RestrictInfo's ignoring the 'rinfo_serial' field,
+ * which makes almost every RestrictInfo unique.  This type of comparison is
+ * useful when removing duplicates while moving RestrictInfo's from removed
+ * relation to remaining relation during self-join elimination.
+ *
+ * XXX: In the future, we might remove the 'rinfo_serial' field completely and
+ * get rid of this function.
+ */
+static bool
+restrict_infos_logically_equal(RestrictInfo *a, RestrictInfo *b)
+{
+	int			saved_rinfo_serial = a->rinfo_serial;
+	bool		result;
+
+	a->rinfo_serial = b->rinfo_serial;
+	result = equal(a, b);
+	a->rinfo_serial = saved_rinfo_serial;
+
+	return result;
+}
+
+/*
+ * This function adds all non-redundant clauses to the keeping relation
+ * during self-join elimination.  That is a contradictory operation. On the
+ * one hand, we reduce the length of the `restrict` lists, which can
+ * impact planning or executing time.  Additionally, we improve the
+ * accuracy of cardinality estimation.  On the other hand, it is one more
+ * place that can make planning time much longer in specific cases.  It
+ * would have been better to avoid calling the equal() function here, but
+ * it's the only way to detect duplicated inequality expressions.
+ *
+ * (*keep_rinfo_list) is given by pointer because it might be altered by
+ * distribute_restrictinfo_to_rels().
+ */
+static void
+add_non_redundant_clauses(PlannerInfo *root,
+						  List *rinfo_candidates,
+						  List **keep_rinfo_list,
+						  Index removed_relid)
+{
+	foreach_node(RestrictInfo, rinfo, rinfo_candidates)
+	{
+		bool		is_redundant = false;
+
+		Assert(!bms_is_member(removed_relid, rinfo->required_relids));
+
+		foreach_node(RestrictInfo, src, (*keep_rinfo_list))
+		{
+			if (!bms_equal(src->clause_relids, rinfo->clause_relids))
+				/* Can't compare trivially different clauses */
+				continue;
+
+			if (src == rinfo ||
+				(rinfo->parent_ec != NULL &&
+				 src->parent_ec == rinfo->parent_ec) ||
+				restrict_infos_logically_equal(rinfo, src))
+			{
+				is_redundant = true;
+				break;
+			}
+		}
+		if (!is_redundant)
+			distribute_restrictinfo_to_rels(root, rinfo);
+	}
+}
+
+/*
+ * Remove a relation after we have proven that it participates only in an
+ * unneeded unique self-join.
+ *
+ * Replace any links in planner info structures.
+ *
+ * Transfer join and restriction clauses from the removed relation to the
+ * remaining one. We change the Vars of the clause to point to the
+ * remaining relation instead of the removed one. The clauses that require
+ * a subset of joinrelids become restriction clauses of the remaining
+ * relation, and others remain join clauses. We append them to
+ * baserestrictinfo and joininfo, respectively, trying not to introduce
+ * duplicates.
+ *
+ * We also have to process the 'joinclauses' list here, because it
+ * contains EC-derived join clauses which must become filter clauses. It
+ * is not enough to just correct the ECs because the EC-derived
+ * restrictions are generated before join removal (see
+ * generate_base_implied_equalities).
+ *
+ * NOTE: Remember to keep the code in sync with PlannerInfo to be sure all
+ * cached relids and relid bitmapsets can be correctly cleaned during the
+ * self-join elimination procedure.
+ */
+static void
+remove_self_join_rel(PlannerInfo *root, PlanRowMark *kmark, PlanRowMark *rmark,
+					 RelOptInfo *toKeep, RelOptInfo *toRemove,
+					 List *restrictlist)
+{
+	List	   *joininfos;
+	ListCell   *lc;
+	int			i;
+	List	   *jinfo_candidates = NIL;
+	List	   *binfo_candidates = NIL;
+
+	Assert(toKeep->relid > 0);
+	Assert(toRemove->relid > 0);
+
+	/*
+	 * Replace the index of the removing table with the keeping one. The
+	 * technique of removing/distributing restrictinfo is used here to attach
+	 * just appeared (for keeping relation) join clauses and avoid adding
+	 * duplicates of those that already exist in the joininfo list.
+	 */
+	joininfos = list_copy(toRemove->joininfo);
+	foreach_node(RestrictInfo, rinfo, joininfos)
+	{
+		remove_join_clause_from_rels(root, rinfo, rinfo->required_relids);
+		ChangeVarNodes((Node *) rinfo, toRemove->relid, toKeep->relid, 0);
+
+		if (bms_membership(rinfo->required_relids) == BMS_MULTIPLE)
+			jinfo_candidates = lappend(jinfo_candidates, rinfo);
+		else
+			binfo_candidates = lappend(binfo_candidates, rinfo);
+	}
+
+	/*
+	 * Concatenate restrictlist to the list of base restrictions of the
+	 * removing table just to simplify the replacement procedure: all of them
+	 * weren't connected to any keeping relations and need to be added to some
+	 * rels.
+	 */
+	toRemove->baserestrictinfo = list_concat(toRemove->baserestrictinfo,
+											 restrictlist);
+	foreach_node(RestrictInfo, rinfo, toRemove->baserestrictinfo)
+	{
+		ChangeVarNodes((Node *) rinfo, toRemove->relid, toKeep->relid, 0);
+
+		if (bms_membership(rinfo->required_relids) == BMS_MULTIPLE)
+			jinfo_candidates = lappend(jinfo_candidates, rinfo);
+		else
+			binfo_candidates = lappend(binfo_candidates, rinfo);
+	}
+
+	/*
+	 * Now, add all non-redundant clauses to the keeping relation.
+	 */
+	add_non_redundant_clauses(root, binfo_candidates,
+							  &toKeep->baserestrictinfo, toRemove->relid);
+	add_non_redundant_clauses(root, jinfo_candidates,
+							  &toKeep->joininfo, toRemove->relid);
+
+	list_free(binfo_candidates);
+	list_free(jinfo_candidates);
+
+	/*
+	 * Arrange equivalence classes, mentioned removing a table, with the
+	 * keeping one: varno of removing table should be replaced in members and
+	 * sources lists. Also, remove duplicated elements if this replacement
+	 * procedure created them.
+	 */
+	i = -1;
+	while ((i = bms_next_member(toRemove->eclass_indexes, i)) >= 0)
+	{
+		EquivalenceClass *ec = (EquivalenceClass *) list_nth(root->eq_classes, i);
+
+		update_eclasses(ec, toRemove->relid, toKeep->relid);
+		toKeep->eclass_indexes = bms_add_member(toKeep->eclass_indexes, i);
+	}
+
+	/*
+	 * Transfer the targetlist and attr_needed flags.
+	 */
+
+	foreach(lc, toRemove->reltarget->exprs)
+	{
+		Node	   *node = lfirst(lc);
+
+		ChangeVarNodes(node, toRemove->relid, toKeep->relid, 0);
+		if (!list_member(toKeep->reltarget->exprs, node))
+			toKeep->reltarget->exprs = lappend(toKeep->reltarget->exprs, node);
+	}
+
+	for (i = toKeep->min_attr; i <= toKeep->max_attr; i++)
+	{
+		int			attno = i - toKeep->min_attr;
+
+		toRemove->attr_needed[attno] = adjust_relid_set(toRemove->attr_needed[attno],
+														toRemove->relid, toKeep->relid);
+		toKeep->attr_needed[attno] = bms_add_members(toKeep->attr_needed[attno],
+													 toRemove->attr_needed[attno]);
+	}
+
+	/*
+	 * If the removed relation has a row mark, transfer it to the remaining
+	 * one.
+	 *
+	 * If both rels have row marks, just keep the one corresponding to the
+	 * remaining relation because we verified earlier that they have the same
+	 * strength.
+	 */
+	if (rmark)
+	{
+		if (kmark)
+		{
+			Assert(kmark->markType == rmark->markType);
+
+			root->rowMarks = list_delete_ptr(root->rowMarks, rmark);
+		}
+		else
+		{
+			/* Shouldn't have inheritance children here. */
+			Assert(rmark->rti == rmark->prti);
+
+			rmark->rti = rmark->prti = toKeep->relid;
+		}
+	}
+
+	/*
+	 * Replace varno in all the query structures, except nodes RangeTblRef
+	 * otherwise later remove_rel_from_joinlist will yield errors.
+	 */
+	ChangeVarNodesExtended((Node *) root->parse, toRemove->relid, toKeep->relid, 0, false);
+
+	/* Replace links in the planner info */
+	remove_rel_from_query(root, toRemove, toKeep->relid, NULL, NULL);
+
+	/* At last, replace varno in root targetlist and HAVING clause */
+	ChangeVarNodes((Node *) root->processed_tlist, toRemove->relid, toKeep->relid, 0);
+	ChangeVarNodes((Node *) root->processed_groupClause, toRemove->relid, toKeep->relid, 0);
+
+	adjust_relid_set(root->all_result_relids, toRemove->relid, toKeep->relid);
+	adjust_relid_set(root->leaf_result_relids, toRemove->relid, toKeep->relid);
+
+	/*
+	 * There may be references to the rel in root->fkey_list, but if so,
+	 * match_foreign_keys_to_quals() will get rid of them.
+	 */
+
+	/*
+	 * Finally, remove the rel from the baserel array to prevent it from being
+	 * referenced again.  (We can't do this earlier because
+	 * remove_join_clause_from_rels will touch it.)
+	 */
+	root->simple_rel_array[toRemove->relid] = NULL;
+
+	/* And nuke the RelOptInfo, just in case there's another access path. */
+	pfree(toRemove);
+
+	/*
+	 * Now repeat construction of attr_needed bits coming from all other
+	 * sources.
+	 */
+	rebuild_placeholder_attr_needed(root);
+	rebuild_joinclause_attr_needed(root);
+	rebuild_eclass_attr_needed(root);
+	rebuild_lateral_attr_needed(root);
+}
+
+/*
+ * split_selfjoin_quals
+ *		Processes 'joinquals' by building two lists: one containing the quals
+ *		where the columns/exprs are on either side of the join match and
+ *		another one containing the remaining quals.
+ *
+ * 'joinquals' must only contain quals for a RTE_RELATION being joined to
+ * itself.
+ */
+static void
+split_selfjoin_quals(PlannerInfo *root, List *joinquals, List **selfjoinquals,
+					 List **otherjoinquals, int from, int to)
+{
+	List	   *sjoinquals = NIL;
+	List	   *ojoinquals = NIL;
+
+	foreach_node(RestrictInfo, rinfo, joinquals)
+	{
+		OpExpr	   *expr;
+		Node	   *leftexpr;
+		Node	   *rightexpr;
+
+		/* In general, clause looks like F(arg1) = G(arg2) */
+		if (!rinfo->mergeopfamilies ||
+			bms_num_members(rinfo->clause_relids) != 2 ||
+			bms_membership(rinfo->left_relids) != BMS_SINGLETON ||
+			bms_membership(rinfo->right_relids) != BMS_SINGLETON)
+		{
+			ojoinquals = lappend(ojoinquals, rinfo);
+			continue;
+		}
+
+		expr = (OpExpr *) rinfo->clause;
+
+		if (!IsA(expr, OpExpr) || list_length(expr->args) != 2)
+		{
+			ojoinquals = lappend(ojoinquals, rinfo);
+			continue;
+		}
+
+		leftexpr = get_leftop(rinfo->clause);
+		rightexpr = copyObject(get_rightop(rinfo->clause));
+
+		if (leftexpr && IsA(leftexpr, RelabelType))
+			leftexpr = (Node *) ((RelabelType *) leftexpr)->arg;
+		if (rightexpr && IsA(rightexpr, RelabelType))
+			rightexpr = (Node *) ((RelabelType *) rightexpr)->arg;
+
+		/*
+		 * Quite an expensive operation, narrowing the use case. For example,
+		 * when we have cast of the same var to different (but compatible)
+		 * types.
+		 */
+		ChangeVarNodes(rightexpr, bms_singleton_member(rinfo->right_relids),
+					   bms_singleton_member(rinfo->left_relids), 0);
+
+		if (equal(leftexpr, rightexpr))
+			sjoinquals = lappend(sjoinquals, rinfo);
+		else
+			ojoinquals = lappend(ojoinquals, rinfo);
+	}
+
+	*selfjoinquals = sjoinquals;
+	*otherjoinquals = ojoinquals;
+}
+
+/*
+ * Check for a case when uniqueness is at least partly derived from a
+ * baserestrictinfo clause. In this case, we have a chance to return only
+ * one row (if such clauses on both sides of SJ are equal) or nothing (if they
+ * are different).
+ */
+static bool
+match_unique_clauses(PlannerInfo *root, RelOptInfo *outer, List *uclauses,
+					 Index relid)
+{
+	foreach_node(RestrictInfo, rinfo, uclauses)
+	{
+		Expr	   *clause;
+		Node	   *iclause;
+		Node	   *c1;
+		bool		matched = false;
+
+		Assert(outer->relid > 0 && relid > 0);
+
+		/* Only filters like f(R.x1,...,R.xN) == expr we should consider. */
+		Assert(bms_is_empty(rinfo->left_relids) ^
+			   bms_is_empty(rinfo->right_relids));
+
+		clause = (Expr *) copyObject(rinfo->clause);
+		ChangeVarNodes((Node *) clause, relid, outer->relid, 0);
+
+		iclause = bms_is_empty(rinfo->left_relids) ? get_rightop(clause) :
+			get_leftop(clause);
+		c1 = bms_is_empty(rinfo->left_relids) ? get_leftop(clause) :
+			get_rightop(clause);
+
+		/*
+		 * Compare these left and right sides with the corresponding sides of
+		 * the outer's filters. If no one is detected - return immediately.
+		 */
+		foreach_node(RestrictInfo, orinfo, outer->baserestrictinfo)
+		{
+			Node	   *oclause;
+			Node	   *c2;
+
+			if (orinfo->mergeopfamilies == NIL)
+				/* Don't consider clauses that aren't similar to 'F(X)=G(Y)' */
+				continue;
+
+			Assert(is_opclause(orinfo->clause));
+
+			oclause = bms_is_empty(orinfo->left_relids) ?
+				get_rightop(orinfo->clause) : get_leftop(orinfo->clause);
+			c2 = (bms_is_empty(orinfo->left_relids) ?
+				  get_leftop(orinfo->clause) : get_rightop(orinfo->clause));
+
+			if (equal(iclause, oclause) && equal(c1, c2))
+			{
+				matched = true;
+				break;
+			}
+		}
+
+		if (!matched)
+			return false;
+	}
+
+	return true;
+}
+
+/*
+ * Find and remove unique self-joins in a group of base relations that have
+ * the same Oid.
+ *
+ * Returns a set of relids that were removed.
+ */
+static Relids
+remove_self_joins_one_group(PlannerInfo *root, Relids relids)
+{
+	Relids		result = NULL;
+	int			k;				/* Index of kept relation */
+	int			r = -1;			/* Index of removed relation */
+
+	while ((r = bms_next_member(relids, r)) > 0)
+	{
+		RelOptInfo *inner = root->simple_rel_array[r];
+
+		k = r;
+
+		while ((k = bms_next_member(relids, k)) > 0)
+		{
+			Relids		joinrelids = NULL;
+			RelOptInfo *outer = root->simple_rel_array[k];
+			List	   *restrictlist;
+			List	   *selfjoinquals;
+			List	   *otherjoinquals;
+			ListCell   *lc;
+			bool		jinfo_check = true;
+			PlanRowMark *omark = NULL;
+			PlanRowMark *imark = NULL;
+			List	   *uclauses = NIL;
+
+			/* A sanity check: the relations have the same Oid. */
+			Assert(root->simple_rte_array[k]->relid ==
+				   root->simple_rte_array[r]->relid);
+
+			/*
+			 * It is impossible to eliminate the join of two relations if they
+			 * belong to different rules of order. Otherwise, the planner
+			 * can't find any variants of the correct query plan.
+			 */
+			foreach(lc, root->join_info_list)
+			{
+				SpecialJoinInfo *info = (SpecialJoinInfo *) lfirst(lc);
+
+				if ((bms_is_member(k, info->syn_lefthand) ^
+					 bms_is_member(r, info->syn_lefthand)) ||
+					(bms_is_member(k, info->syn_righthand) ^
+					 bms_is_member(r, info->syn_righthand)))
+				{
+					jinfo_check = false;
+					break;
+				}
+			}
+			if (!jinfo_check)
+				continue;
+
+			/*
+			 * Check Row Marks equivalence. We can't remove the join if the
+			 * relations have row marks of different strength (e.g., one is
+			 * locked FOR UPDATE, and another just has ROW_MARK_REFERENCE for
+			 * EvalPlanQual rechecking).
+			 */
+			foreach(lc, root->rowMarks)
+			{
+				PlanRowMark *rowMark = (PlanRowMark *) lfirst(lc);
+
+				if (rowMark->rti == k)
+				{
+					Assert(imark == NULL);
+					imark = rowMark;
+				}
+				else if (rowMark->rti == r)
+				{
+					Assert(omark == NULL);
+					omark = rowMark;
+				}
+
+				if (omark && imark)
+					break;
+			}
+			if (omark && imark && omark->markType != imark->markType)
+				continue;
+
+			/*
+			 * We only deal with base rels here, so their relids bitset
+			 * contains only one member -- their relid.
+			 */
+			joinrelids = bms_add_member(joinrelids, r);
+			joinrelids = bms_add_member(joinrelids, k);
+
+			/*
+			 * PHVs should not impose any constraints on removing self-joins.
+			 */
+
+			/*
+			 * At this stage, joininfo lists of inner and outer can contain
+			 * only clauses required for a superior outer join that can't
+			 * influence this optimization. So, we can avoid to call the
+			 * build_joinrel_restrictlist() routine.
+			 */
+			restrictlist = generate_join_implied_equalities(root, joinrelids,
+															inner->relids,
+															outer, NULL);
+			if (restrictlist == NIL)
+				continue;
+
+			/*
+			 * Process restrictlist to separate the self-join quals from the
+			 * other quals. e.g., "x = x" goes to selfjoinquals and "a = b" to
+			 * otherjoinquals.
+			 */
+			split_selfjoin_quals(root, restrictlist, &selfjoinquals,
+								 &otherjoinquals, inner->relid, outer->relid);
+
+			Assert(list_length(restrictlist) ==
+				   (list_length(selfjoinquals) + list_length(otherjoinquals)));
+
+			/*
+			 * To enable SJE for the only degenerate case without any self
+			 * join clauses at all, add baserestrictinfo to this list. The
+			 * degenerate case works only if both sides have the same clause.
+			 * So doesn't matter which side to add.
+			 */
+			selfjoinquals = list_concat(selfjoinquals, outer->baserestrictinfo);
+
+			/*
+			 * Determine if the inner table can duplicate outer rows.  We must
+			 * bypass the unique rel cache here since we're possibly using a
+			 * subset of join quals. We can use 'force_cache' == true when all
+			 * join quals are self-join quals.  Otherwise, we could end up
+			 * putting false negatives in the cache.
+			 */
+			if (!innerrel_is_unique_ext(root, joinrelids, inner->relids,
+										outer, JOIN_INNER, selfjoinquals,
+										list_length(otherjoinquals) == 0,
+										&uclauses))
+				continue;
+
+			/*
+			 * 'uclauses' is the copy of outer->baserestrictinfo that are
+			 * associated with an index.  We proved by matching selfjoinquals
+			 * to a unique index that the outer relation has at most one
+			 * matching row for each inner row.  Sometimes that is not enough.
+			 * e.g. "WHERE s1.b = s2.b AND s1.a = 1 AND s2.a = 2" when the
+			 * unique index is (a,b).  Having non-empty uclauses, we must
+			 * validate that the inner baserestrictinfo contains the same
+			 * expressions, or we won't match the same row on each side of the
+			 * join.
+			 */
+			if (!match_unique_clauses(root, inner, uclauses, outer->relid))
+				continue;
+
+			/*
+			 * We can remove either relation, so remove the inner one in order
+			 * to simplify this loop.
+			 */
+			remove_self_join_rel(root, omark, imark, outer, inner, restrictlist);
+
+			result = bms_add_member(result, r);
+
+			/* We have removed the outer relation, try the next one. */
+			break;
+		}
+	}
+
+	return result;
+}
+
+/*
+ * Gather indexes of base relations from the joinlist and try to eliminate self
+ * joins.
+ */
+static Relids
+remove_self_joins_recurse(PlannerInfo *root, List *joinlist, Relids toRemove)
+{
+	ListCell   *jl;
+	Relids		relids = NULL;
+	SelfJoinCandidate *candidates = NULL;
+	int			i;
+	int			j;
+	int			numRels;
+
+	/* Collect indexes of base relations of the join tree */
+	foreach(jl, joinlist)
+	{
+		Node	   *jlnode = (Node *) lfirst(jl);
+
+		if (IsA(jlnode, RangeTblRef))
+		{
+			int			varno = ((RangeTblRef *) jlnode)->rtindex;
+			RangeTblEntry *rte = root->simple_rte_array[varno];
+
+			/*
+			 * We only consider ordinary relations as candidates to be
+			 * removed, and these relations should not have TABLESAMPLE
+			 * clauses specified.  Removing a relation with TABLESAMPLE clause
+			 * could potentially change the syntax of the query. Because of
+			 * UPDATE/DELETE EPQ mechanism, currently Query->resultRelation or
+			 * Query->mergeTargetRelation associated rel cannot be eliminated.
+			 */
+			if (rte->rtekind == RTE_RELATION &&
+				rte->relkind == RELKIND_RELATION &&
+				rte->tablesample == NULL &&
+				varno != root->parse->resultRelation &&
+				varno != root->parse->mergeTargetRelation)
+			{
+				Assert(!bms_is_member(varno, relids));
+				relids = bms_add_member(relids, varno);
+			}
+		}
+		else if (IsA(jlnode, List))
+		{
+			/* Recursively go inside the sub-joinlist */
+			toRemove = remove_self_joins_recurse(root, (List *) jlnode,
+												 toRemove);
+		}
+		else
+			elog(ERROR, "unrecognized joinlist node type: %d",
+				 (int) nodeTag(jlnode));
+	}
+
+	numRels = bms_num_members(relids);
+
+	/* Need at least two relations for the join */
+	if (numRels < 2)
+		return toRemove;
+
+	/*
+	 * In order to find relations with the same oid we first build an array of
+	 * candidates and then sort it by oid.
+	 */
+	candidates = (SelfJoinCandidate *) palloc(sizeof(SelfJoinCandidate) *
+											  numRels);
+	i = -1;
+	j = 0;
+	while ((i = bms_next_member(relids, i)) >= 0)
+	{
+		candidates[j].relid = i;
+		candidates[j].reloid = root->simple_rte_array[i]->relid;
+		j++;
+	}
+
+	qsort(candidates, numRels, sizeof(SelfJoinCandidate),
+		  self_join_candidates_cmp);
+
+	/*
+	 * Iteratively form a group of relation indexes with the same oid and
+	 * launch the routine that detects self-joins in this group and removes
+	 * excessive range table entries.
+	 *
+	 * At the end of the iteration, exclude the group from the overall relids
+	 * list. So each next iteration of the cycle will involve less and less
+	 * value of relids.
+	 */
+	i = 0;
+	for (j = 1; j < numRels + 1; j++)
+	{
+		if (j == numRels || candidates[j].reloid != candidates[i].reloid)
+		{
+			if (j - i >= 2)
+			{
+				/* Create a group of relation indexes with the same oid */
+				Relids		group = NULL;
+				Relids		removed;
+
+				while (i < j)
+				{
+					group = bms_add_member(group, candidates[i].relid);
+					i++;
+				}
+				relids = bms_del_members(relids, group);
+
+				/*
+				 * Try to remove self-joins from a group of identical entries.
+				 * Make the next attempt iteratively - if something is deleted
+				 * from a group, changes in clauses and equivalence classes
+				 * can give us a chance to find more candidates.
+				 */
+				do
+				{
+					Assert(!bms_overlap(group, toRemove));
+					removed = remove_self_joins_one_group(root, group);
+					toRemove = bms_add_members(toRemove, removed);
+					group = bms_del_members(group, removed);
+				} while (!bms_is_empty(removed) &&
+						 bms_membership(group) == BMS_MULTIPLE);
+				bms_free(removed);
+				bms_free(group);
+			}
+			else
+			{
+				/* Single relation, just remove it from the set */
+				relids = bms_del_member(relids, candidates[i].relid);
+				i = j;
+			}
+		}
+	}
+
+	Assert(bms_is_empty(relids));
+
+	return toRemove;
+}
+
+/*
+ * Compare self-join candidates by their oids.
+ */
+static int
+self_join_candidates_cmp(const void *a, const void *b)
+{
+	const SelfJoinCandidate *ca = (const SelfJoinCandidate *) a;
+	const SelfJoinCandidate *cb = (const SelfJoinCandidate *) b;
+
+	if (ca->reloid != cb->reloid)
+		return (ca->reloid < cb->reloid ? -1 : 1);
+	else
+		return 0;
+}
+
+/*
+ * Find and remove useless self joins.
+ *
+ * Search for joins where a relation is joined to itself. If the join clause
+ * for each tuple from one side of the join is proven to match the same
+ * physical row (or nothing) on the other side, that self-join can be
+ * eliminated from the query.  Suitable join clauses are assumed to be in the
+ * form of X = X, and can be replaced with NOT NULL clauses.
+ *
+ * For the sake of simplicity, we don't apply this optimization to special
+ * joins. Here is a list of what we could do in some particular cases:
+ * 'a a1 semi join a a2': is reduced to inner by reduce_unique_semijoins,
+ * and then removed normally.
+ * 'a a1 anti join a a2': could simplify to a scan with 'outer quals AND
+ * (IS NULL on join columns OR NOT inner quals)'.
+ * 'a a1 left join a a2': could simplify to a scan like inner but without
+ * NOT NULL conditions on join columns.
+ * 'a a1 left join (a a2 join b)': can't simplify this, because join to b
+ * can both remove rows and introduce duplicates.
+ *
+ * To search for removable joins, we order all the relations on their Oid,
+ * go over each set with the same Oid, and consider each pair of relations
+ * in this set.
+ *
+ * To remove the join, we mark one of the participating relations as dead
+ * and rewrite all references to it to point to the remaining relation.
+ * This includes modifying RestrictInfos, EquivalenceClasses, and
+ * EquivalenceMembers. We also have to modify the row marks. The join clauses
+ * of the removed relation become either restriction or join clauses, based on
+ * whether they reference any relations not participating in the removed join.
+ *
+ * 'joinlist' is the top-level joinlist of the query. If it has any
+ * references to the removed relations, we update them to point to the
+ * remaining ones.
+ */
+List *
+remove_useless_self_joins(PlannerInfo *root, List *joinlist)
+{
+	Relids		toRemove = NULL;
+	int			relid = -1;
+
+	if (!enable_self_join_elimination || joinlist == NIL ||
+		(list_length(joinlist) == 1 && !IsA(linitial(joinlist), List)))
+		return joinlist;
+
+	/*
+	 * Merge pairs of relations participated in self-join. Remove unnecessary
+	 * range table entries.
+	 */
+	toRemove = remove_self_joins_recurse(root, joinlist, toRemove);
+
+	if (unlikely(toRemove != NULL))
+	{
+		/* At the end, remove orphaned relation links */
+		while ((relid = bms_next_member(toRemove, relid)) >= 0)
+		{
+			int			nremoved = 0;
+
+			joinlist = remove_rel_from_joinlist(joinlist, relid, &nremoved);
+			if (nremoved != 1)
+				elog(ERROR, "failed to find relation %d in joinlist", relid);
+		}
+	}
+
+	return joinlist;
 }
diff --git a/src/backend/optimizer/plan/planmain.c b/src/backend/optimizer/plan/planmain.c
index ade23fd9d56..5467e094ca7 100644
--- a/src/backend/optimizer/plan/planmain.c
+++ b/src/backend/optimizer/plan/planmain.c
@@ -233,6 +233,11 @@ query_planner(PlannerInfo *root,
 	 */
 	reduce_unique_semijoins(root);
 
+	/*
+	 * Remove self joins on a unique column.
+	 */
+	joinlist = remove_useless_self_joins(root, joinlist);
+
 	/*
 	 * Now distribute "placeholders" to base rels as needed.  This has to be
 	 * done after join removal because removal could change whether a
diff --git a/src/backend/optimizer/prep/prepunion.c b/src/backend/optimizer/prep/prepunion.c
index 7c27dc24e21..eab44da65b8 100644
--- a/src/backend/optimizer/prep/prepunion.c
+++ b/src/backend/optimizer/prep/prepunion.c
@@ -639,14 +639,17 @@ build_setop_child_paths(PlannerInfo *root, RelOptInfo *rel,
 	 * otherwise do statistical estimation.
 	 *
 	 * XXX you don't really want to know about this: we do the estimation
-	 * using the subquery's original targetlist expressions, not the
+	 * using the subroot->parse's original targetlist expressions, not the
 	 * subroot->processed_tlist which might seem more appropriate.  The reason
 	 * is that if the subquery is itself a setop, it may return a
 	 * processed_tlist containing "varno 0" Vars generated by
 	 * generate_append_tlist, and those would confuse estimate_num_groups
 	 * mightily.  We ought to get rid of the "varno 0" hack, but that requires
 	 * a redesign of the parsetree representation of setops, so that there can
-	 * be an RTE corresponding to each setop's output.
+	 * be an RTE corresponding to each setop's output. Note, we use this not
+	 * subquery's targetlist but subroot->parse's targetlist, because it was
+	 * revised by self-join removal.  subquery's targetlist might contain the
+	 * references to the removed relids.
 	 */
 	if (pNumGroups)
 	{
@@ -659,7 +662,7 @@ build_setop_child_paths(PlannerInfo *root, RelOptInfo *rel,
 			*pNumGroups = rel->cheapest_total_path->rows;
 		else
 			*pNumGroups = estimate_num_groups(subroot,
-											  get_tlist_exprs(subquery->targetList, false),
+											  get_tlist_exprs(subroot->parse->targetList, false),
 											  rel->cheapest_total_path->rows,
 											  NULL,
 											  NULL);
diff --git a/src/backend/rewrite/rewriteManip.c b/src/backend/rewrite/rewriteManip.c
index a115b217c91..9433548d279 100644
--- a/src/backend/rewrite/rewriteManip.c
+++ b/src/backend/rewrite/rewriteManip.c
@@ -64,7 +64,6 @@ static bool locate_windowfunc_walker(Node *node,
 									 locate_windowfunc_context *context);
 static bool checkExprHasSubLink_walker(Node *node, void *context);
 static Relids offset_relid_set(Relids relids, int offset);
-static Relids adjust_relid_set(Relids relids, int oldrelid, int newrelid);
 static Node *add_nulling_relids_mutator(Node *node,
 										add_nulling_relids_context *context);
 static Node *remove_nulling_relids_mutator(Node *node,
@@ -543,6 +542,8 @@ offset_relid_set(Relids relids, int offset)
  * (identified by sublevels_up and rt_index), and change their varno fields
  * to 'new_index'.  The varnosyn fields are changed too.  Also, adjust other
  * nodes that contain rangetable indexes, such as RangeTblRef and JoinExpr.
+ * Specifying 'change_RangeTblRef' to false allows skipping RangeTblRef.
+ * See ChangeVarNodesExtended for details.
  *
  * NOTE: although this has the form of a walker, we cheat and modify the
  * nodes in-place.  The given expression tree should have been copied
@@ -554,6 +555,7 @@ typedef struct
 	int			rt_index;
 	int			new_index;
 	int			sublevels_up;
+	bool		change_RangeTblRef;
 } ChangeVarNodes_context;
 
 static bool
@@ -586,7 +588,7 @@ ChangeVarNodes_walker(Node *node, ChangeVarNodes_context *context)
 			cexpr->cvarno = context->new_index;
 		return false;
 	}
-	if (IsA(node, RangeTblRef))
+	if (IsA(node, RangeTblRef) && context->change_RangeTblRef)
 	{
 		RangeTblRef *rtr = (RangeTblRef *) node;
 
@@ -633,6 +635,75 @@ ChangeVarNodes_walker(Node *node, ChangeVarNodes_context *context)
 		}
 		return false;
 	}
+	if (IsA(node, RestrictInfo))
+	{
+		RestrictInfo *rinfo = (RestrictInfo *) node;
+		int			relid = -1;
+		bool		is_req_equal =
+			(rinfo->required_relids == rinfo->clause_relids);
+		bool		clause_relids_is_multiple =
+			(bms_membership(rinfo->clause_relids) == BMS_MULTIPLE);
+
+		if (bms_is_member(context->rt_index, rinfo->clause_relids))
+		{
+			expression_tree_walker((Node *) rinfo->clause, ChangeVarNodes_walker, (void *) context);
+			expression_tree_walker((Node *) rinfo->orclause, ChangeVarNodes_walker, (void *) context);
+
+			rinfo->clause_relids =
+				adjust_relid_set(rinfo->clause_relids, context->rt_index, context->new_index);
+			rinfo->num_base_rels = bms_num_members(rinfo->clause_relids);
+			rinfo->left_relids =
+				adjust_relid_set(rinfo->left_relids, context->rt_index, context->new_index);
+			rinfo->right_relids =
+				adjust_relid_set(rinfo->right_relids, context->rt_index, context->new_index);
+		}
+
+		if (is_req_equal)
+			rinfo->required_relids = rinfo->clause_relids;
+		else
+			rinfo->required_relids =
+				adjust_relid_set(rinfo->required_relids, context->rt_index, context->new_index);
+
+		rinfo->outer_relids =
+			adjust_relid_set(rinfo->outer_relids, context->rt_index, context->new_index);
+		rinfo->incompatible_relids =
+			adjust_relid_set(rinfo->incompatible_relids, context->rt_index, context->new_index);
+
+		if (rinfo->mergeopfamilies &&
+			bms_get_singleton_member(rinfo->clause_relids, &relid) &&
+			clause_relids_is_multiple &&
+			relid == context->new_index && IsA(rinfo->clause, OpExpr))
+		{
+			Expr	   *leftOp;
+			Expr	   *rightOp;
+
+			leftOp = (Expr *) get_leftop(rinfo->clause);
+			rightOp = (Expr *) get_rightop(rinfo->clause);
+
+			/*
+			 * For self-join elimination, changing varnos could transform
+			 * "t1.a = t2.a" into "t1.a = t1.a".  That is always true as long
+			 * as "t1.a" is not null.  We use qual() to check for such a case,
+			 * and then we replace the qual for a check for not null
+			 * (NullTest).
+			 */
+			if (leftOp != NULL && equal(leftOp, rightOp))
+			{
+				NullTest   *ntest = makeNode(NullTest);
+
+				ntest->arg = leftOp;
+				ntest->nulltesttype = IS_NOT_NULL;
+				ntest->argisrow = false;
+				ntest->location = -1;
+				rinfo->clause = (Expr *) ntest;
+				rinfo->mergeopfamilies = NIL;
+				rinfo->left_em = NULL;
+				rinfo->right_em = NULL;
+			}
+			Assert(rinfo->orclause == NULL);
+		}
+		return false;
+	}
 	if (IsA(node, AppendRelInfo))
 	{
 		AppendRelInfo *appinfo = (AppendRelInfo *) node;
@@ -665,32 +736,32 @@ ChangeVarNodes_walker(Node *node, ChangeVarNodes_context *context)
 	return expression_tree_walker(node, ChangeVarNodes_walker, context);
 }
 
+/*
+ * ChangeVarNodesExtended - similar to ChangeVarNodes, but has additional
+ *							'change_RangeTblRef' param
+ *
+ * ChangeVarNodes changes a given node and all of its underlying nodes.
+ * However, self-join elimination (SJE) needs to skip the RangeTblRef node
+ * type.  During SJE's last step, remove_rel_from_joinlist() removes
+ * remaining RangeTblRefs with target relid.  If ChangeVarNodes() replaces
+ * the target relid before, remove_rel_from_joinlist() fails to identify
+ * the nodes to delete.
+ */
 void
-ChangeVarNodes(Node *node, int rt_index, int new_index, int sublevels_up)
+ChangeVarNodesExtended(Node *node, int rt_index, int new_index,
+					   int sublevels_up, bool change_RangeTblRef)
 {
 	ChangeVarNodes_context context;
 
 	context.rt_index = rt_index;
 	context.new_index = new_index;
 	context.sublevels_up = sublevels_up;
+	context.change_RangeTblRef = change_RangeTblRef;
 
-	/*
-	 * Must be prepared to start with a Query or a bare expression tree; if
-	 * it's a Query, go straight to query_tree_walker to make sure that
-	 * sublevels_up doesn't get incremented prematurely.
-	 */
 	if (node && IsA(node, Query))
 	{
 		Query	   *qry = (Query *) node;
 
-		/*
-		 * If we are starting at a Query, and sublevels_up is zero, then we
-		 * must also fix rangetable indexes in the Query itself --- namely
-		 * resultRelation, mergeTargetRelation, exclRelIndex  and rowMarks
-		 * entries.  sublevels_up cannot be zero when recursing into a
-		 * subquery, so there's no need to have the same logic inside
-		 * ChangeVarNodes_walker.
-		 */
 		if (sublevels_up == 0)
 		{
 			ListCell   *l;
@@ -701,7 +772,6 @@ ChangeVarNodes(Node *node, int rt_index, int new_index, int sublevels_up)
 			if (qry->mergeTargetRelation == rt_index)
 				qry->mergeTargetRelation = new_index;
 
-			/* this is unlikely to ever be used, but ... */
 			if (qry->onConflict && qry->onConflict->exclRelIndex == rt_index)
 				qry->onConflict->exclRelIndex = new_index;
 
@@ -719,15 +789,22 @@ ChangeVarNodes(Node *node, int rt_index, int new_index, int sublevels_up)
 		ChangeVarNodes_walker(node, &context);
 }
 
+void
+ChangeVarNodes(Node *node, int rt_index, int new_index, int sublevels_up)
+{
+	ChangeVarNodesExtended(node, rt_index, new_index, sublevels_up, true);
+}
+
 /*
- * Substitute newrelid for oldrelid in a Relid set
+ * adjust_relid_set - substitute newrelid for oldrelid in a Relid set
  *
- * Note: some extensions may pass a special varno such as INDEX_VAR for
- * oldrelid.  bms_is_member won't like that, but we should tolerate it.
- * (Perhaps newrelid could also be a special varno, but there had better
- * not be a reason to inject that into a nullingrels or phrels set.)
+ * Attempt to remove oldrelid from a Relid set (as long as it's not a special
+ * varno).  If oldrelid was found and removed, insert newrelid into a Relid
+ * set (as long as it's not a special varno).  Therefore, when oldrelid is
+ * a special varno, this function does nothing.  When newrelid is a special
+ * varno, this function behaves as delete.
  */
-static Relids
+Relids
 adjust_relid_set(Relids relids, int oldrelid, int newrelid)
 {
 	if (!IS_SPECIAL_VARNO(oldrelid) && bms_is_member(oldrelid, relids))
@@ -736,7 +813,8 @@ adjust_relid_set(Relids relids, int oldrelid, int newrelid)
 		relids = bms_copy(relids);
 		/* Remove old, add new */
 		relids = bms_del_member(relids, oldrelid);
-		relids = bms_add_member(relids, newrelid);
+		if (!IS_SPECIAL_VARNO(newrelid))
+			relids = bms_add_member(relids, newrelid);
 	}
 	return relids;
 }
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 42728189322..cce73314609 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -988,6 +988,16 @@ struct config_bool ConfigureNamesBool[] =
 		true,
 		NULL, NULL, NULL
 	},
+	{
+		{"enable_self_join_elimination", PGC_USERSET, QUERY_TUNING_METHOD,
+			gettext_noop("Enable removal of unique self-joins."),
+			NULL,
+			GUC_EXPLAIN | GUC_NOT_IN_SAMPLE
+		},
+		&enable_self_join_elimination,
+		true,
+		NULL, NULL, NULL
+	},
 	{
 		{"enable_group_by_reordering", PGC_USERSET, QUERY_TUNING_METHOD,
 			gettext_noop("Enables reordering of GROUP BY keys."),
diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h
index 00c700cc3e7..fbf05322c75 100644
--- a/src/include/nodes/pathnodes.h
+++ b/src/include/nodes/pathnodes.h
@@ -201,6 +201,11 @@ typedef struct PlannerGlobal
  * Not all fields are printed.  (In some cases, there is no print support for
  * the field type; in others, doing so would lead to infinite recursion or
  * bloat dump output more than seems useful.)
+ *
+ * NOTE: When adding new entries containing relids and relid bitmapsets,
+ * remember to check that they will be correctly processed by
+ * the remove_self_join_rel function - relid of removing relation will be
+ * correctly replaced with the keeping one.
  *----------
  */
 #ifndef HAVE_PLANNERINFO_TYPEDEF
@@ -753,7 +758,7 @@ typedef struct PartitionSchemeData *PartitionScheme;
  * populate these fields, for base rels; but someday they might be used for
  * join rels too:
  *
- *		unique_for_rels - list of Relid sets, each one being a set of other
+ *		unique_for_rels - list of UniqueRelInfo, each one being a set of other
  *					rels for which this one has been proven unique
  *		non_unique_for_rels - list of Relid sets, each one being a set of
  *					other rels for which we have tried and failed to prove
@@ -992,7 +997,7 @@ typedef struct RelOptInfo
 	/*
 	 * cache space for remembering if we have proven this relation unique
 	 */
-	/* known unique for these other relid set(s) */
+	/* known unique for these other relid set(s) given in UniqueRelInfo(s) */
 	List	   *unique_for_rels;
 	/* known not unique for these set(s) */
 	List	   *non_unique_for_rels;
@@ -3463,4 +3468,35 @@ typedef struct AggTransInfo
 	bool		initValueIsNull;
 } AggTransInfo;
 
+/*
+ * UniqueRelInfo caches a fact that a relation is unique when being joined
+ * to other relation(s).
+ */
+typedef struct UniqueRelInfo
+{
+	pg_node_attr(no_copy_equal, no_read, no_query_jumble)
+
+	NodeTag		type;
+
+	/*
+	 * The relation in consideration is unique when being joined with this set
+	 * of other relation(s).
+	 */
+	Relids		outerrelids;
+
+	/*
+	 * The relation in consideration is unique when considering only clauses
+	 * suitable for self-join (passed split_selfjoin_quals()).
+	 */
+	bool		self_join;
+
+	/*
+	 * Additional clauses from a baserestrictinfo list that were used to prove
+	 * the uniqueness.   We cache it for the self-join checking procedure: a
+	 * self-join can be removed if the outer relation contains strictly the
+	 * same set of clauses.
+	 */
+	List	   *extra_clauses;
+} UniqueRelInfo;
+
 #endif							/* PATHNODES_H */
diff --git a/src/include/optimizer/optimizer.h b/src/include/optimizer/optimizer.h
index bcf8ed645c2..78e05d88c8e 100644
--- a/src/include/optimizer/optimizer.h
+++ b/src/include/optimizer/optimizer.h
@@ -192,6 +192,8 @@ extern SortGroupClause *get_sortgroupref_clause_noerr(Index sortref,
 											 * output list */
 #define PVC_RECURSE_PLACEHOLDERS	0x0020	/* recurse into PlaceHolderVar
 											 * arguments */
+#define PVC_INCLUDE_CONVERTROWTYPES	0x0040	/* include ConvertRowtypeExprs in
+											 * output list */
 
 extern Bitmapset *pull_varnos(PlannerInfo *root, Node *node);
 extern Bitmapset *pull_varnos_of_level(PlannerInfo *root, Node *node, int levelsup);
diff --git a/src/include/optimizer/paths.h b/src/include/optimizer/paths.h
index 46955d128f0..bc5dfd7db41 100644
--- a/src/include/optimizer/paths.h
+++ b/src/include/optimizer/paths.h
@@ -72,6 +72,9 @@ extern void create_index_paths(PlannerInfo *root, RelOptInfo *rel);
 extern bool relation_has_unique_index_for(PlannerInfo *root, RelOptInfo *rel,
 										  List *restrictlist,
 										  List *exprlist, List *oprlist);
+extern bool relation_has_unique_index_ext(PlannerInfo *root, RelOptInfo *rel,
+										  List *restrictlist, List *exprlist,
+										  List *oprlist, List **extra_clauses);
 extern bool indexcol_is_bool_constant_for_query(PlannerInfo *root,
 												IndexOptInfo *index,
 												int indexcol);
diff --git a/src/include/optimizer/planmain.h b/src/include/optimizer/planmain.h
index fee3378bbe3..5a930199611 100644
--- a/src/include/optimizer/planmain.h
+++ b/src/include/optimizer/planmain.h
@@ -20,6 +20,7 @@
 /* GUC parameters */
 #define DEFAULT_CURSOR_TUPLE_FRACTION 0.1
 extern PGDLLIMPORT double cursor_tuple_fraction;
+extern PGDLLIMPORT bool enable_self_join_elimination;
 
 /* query_planner callback to compute query_pathkeys */
 typedef void (*query_pathkeys_callback) (PlannerInfo *root, void *extra);
@@ -113,6 +114,11 @@ extern bool query_is_distinct_for(Query *query, List *colnos, List *opids);
 extern bool innerrel_is_unique(PlannerInfo *root,
 							   Relids joinrelids, Relids outerrelids, RelOptInfo *innerrel,
 							   JoinType jointype, List *restrictlist, bool force_cache);
+extern bool innerrel_is_unique_ext(PlannerInfo *root, Relids joinrelids,
+								   Relids outerrelids, RelOptInfo *innerrel,
+								   JoinType jointype, List *restrictlist,
+								   bool force_cache, List **uclauses);
+extern List *remove_useless_self_joins(PlannerInfo *root, List *jointree);
 
 /*
  * prototypes for plan/setrefs.c
diff --git a/src/include/rewrite/rewriteManip.h b/src/include/rewrite/rewriteManip.h
index 512823033b9..5ec475c63e9 100644
--- a/src/include/rewrite/rewriteManip.h
+++ b/src/include/rewrite/rewriteManip.h
@@ -15,6 +15,7 @@
 #define REWRITEMANIP_H
 
 #include "nodes/parsenodes.h"
+#include "nodes/pathnodes.h"
 
 struct AttrMap;					/* avoid including attmap.h here */
 
@@ -41,11 +42,14 @@ typedef enum ReplaceVarsNoMatchOption
 } ReplaceVarsNoMatchOption;

+extern Relids adjust_relid_set(Relids relids, int oldrelid, int newrelid);
 extern void CombineRangeTables(List **dst_rtable, List **dst_perminfos,
 							   List *src_rtable, List *src_perminfos);
 extern void OffsetVarNodes(Node *node, int offset, int sublevels_up);
 extern void ChangeVarNodes(Node *node, int rt_index, int new_index,
 						   int sublevels_up);
+extern void ChangeVarNodesExtended(Node *node, int rt_index, int new_index,
+								   int sublevels_up, bool change_RangeTblRef);
 extern void IncrementVarSublevelsUp(Node *node, int delta_sublevels_up,
 									int min_sublevels_up);
 extern void IncrementVarSublevelsUp_rtable(List *rtable,
diff --git a/src/test/regress/expected/equivclass.out b/src/test/regress/expected/equivclass.out
index 56227505009..ad8ab294ff6 100644
--- a/src/test/regress/expected/equivclass.out
+++ b/src/test/regress/expected/equivclass.out
@@ -434,6 +434,36 @@ explain (costs off)
    Filter: ((unique1 IS NOT NULL) AND (unique2 IS NOT NULL))
 (2 rows)
 
+-- Test that broken ECs are processed correctly during self join removal.
+-- Disable merge joins so that we don't get an error about missing commutator.
+-- Test both orientations of the join clause, because only one of them breaks
+-- the EC.
+set enable_mergejoin to off;
+explain (costs off)
+  select * from ec0 m join ec0 n on m.ff = n.ff
+  join ec1 p on m.ff + n.ff = p.f1;
+              QUERY PLAN               
+---------------------------------------
+ Nested Loop
+   Join Filter: ((n.ff + n.ff) = p.f1)
+   ->  Seq Scan on ec0 n
+   ->  Materialize
+         ->  Seq Scan on ec1 p
+(5 rows)
+
+explain (costs off)
+  select * from ec0 m join ec0 n on m.ff = n.ff
+  join ec1 p on p.f1::int8 = (m.ff + n.ff)::int8alias1;
+                          QUERY PLAN                           
+---------------------------------------------------------------
+ Nested Loop
+   Join Filter: ((p.f1)::bigint = ((n.ff + n.ff))::int8alias1)
+   ->  Seq Scan on ec0 n
+   ->  Materialize
+         ->  Seq Scan on ec1 p
+(5 rows)
+
+reset enable_mergejoin;
 -- this could be converted, but isn't at present
 explain (costs off)
   select * from tenk1 where unique1 = unique1 or unique2 = unique2;
diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out
index 3ffc066b1f8..a57bb18c24f 100644
--- a/src/test/regress/expected/join.out
+++ b/src/test/regress/expected/join.out
@@ -5966,6 +5966,27 @@ select c.id, ss.a from c
          ->  Seq Scan on c
 (7 rows)
 
+-- check the case when the placeholder relates to an outer join and its
+-- inner in the press field but actually uses only the outer side of the join
+explain (costs off)
+SELECT q.val FROM b LEFT JOIN (
+  SELECT (q1.z IS NOT NULL) AS val
+  FROM b LEFT JOIN (
+    SELECT (t1.b_id IS NOT NULL) AS z FROM a t1 LEFT JOIN a t2 USING (id)
+    ) AS q1
+  ON true
+) AS q ON true;
+                QUERY PLAN                
+------------------------------------------
+ Nested Loop Left Join
+   ->  Seq Scan on b
+   ->  Materialize
+         ->  Nested Loop Left Join
+               ->  Seq Scan on b b_1
+               ->  Materialize
+                     ->  Seq Scan on a t1
+(7 rows)
+
 CREATE TEMP TABLE parted_b (id int PRIMARY KEY) partition by range(id);
 CREATE TEMP TABLE parted_b1 partition of parted_b for values from (0) to (10);
 -- test join removals on a partitioned table
@@ -6377,6 +6398,1068 @@ select * from
 ----+----+----+----
 (0 rows)
 
+--
+-- test that semi- or inner self-joins on a unique column are removed
+--
+-- enable only nestloop to get more predictable plans
+set enable_hashjoin to off;
+set enable_mergejoin to off;
+create table sj (a int unique, b int, c int unique);
+insert into sj values (1, null, 2), (null, 2, null), (2, 1, 1);
+analyze sj;
+-- Trivial self-join case.
+explain (costs off)
+select p.* from sj p, sj q where q.a = p.a and q.b = q.a - 1;
+                  QUERY PLAN                   
+-----------------------------------------------
+ Seq Scan on sj q
+   Filter: ((a IS NOT NULL) AND (b = (a - 1)))
+(2 rows)
+
+select p.* from sj p, sj q where q.a = p.a and q.b = q.a - 1;
+ a | b | c 
+---+---+---
+ 2 | 1 | 1
+(1 row)
+
+-- Self-join removal performs after a subquery pull-up process and could remove
+-- such kind of self-join too. Check this option.
+explain (costs off)
+select * from sj p
+where exists (select * from sj q
+              where q.a = p.a and q.b < 10);
+                QUERY PLAN                
+------------------------------------------
+ Seq Scan on sj q
+   Filter: ((a IS NOT NULL) AND (b < 10))
+(2 rows)
+
+select * from sj p
+where exists (select * from sj q
+              where q.a = p.a and q.b < 10);
+ a | b | c 
+---+---+---
+ 2 | 1 | 1
+(1 row)
+
+-- Don't remove self-join for the case of equality of two different unique columns.
+explain (costs off)
+select * from sj t1, sj t2 where t1.a = t2.c and t1.b is not null;
+              QUERY PLAN               
+---------------------------------------
+ Nested Loop
+   Join Filter: (t1.a = t2.c)
+   ->  Seq Scan on sj t2
+   ->  Materialize
+         ->  Seq Scan on sj t1
+               Filter: (b IS NOT NULL)
+(6 rows)
+
+-- Ensure that relations with TABLESAMPLE clauses are not considered as
+-- candidates to be removed
+explain (costs off)
+select * from sj t1
+    join lateral
+      (select * from sj tablesample system(t1.b)) s
+    on t1.a = s.a;
+              QUERY PLAN               
+---------------------------------------
+ Nested Loop
+   ->  Seq Scan on sj t1
+   ->  Memoize
+         Cache Key: t1.a, t1.b
+         Cache Mode: binary
+         ->  Sample Scan on sj
+               Sampling: system (t1.b)
+               Filter: (t1.a = a)
+(8 rows)
+
+-- Ensure that SJE does not form a self-referential lateral dependency
+explain (costs off)
+select * from sj t1
+    left join lateral
+      (select t1.a as t1a, * from sj t2) s
+    on true
+where t1.a = s.a;
+        QUERY PLAN         
+---------------------------
+ Seq Scan on sj t2
+   Filter: (a IS NOT NULL)
+(2 rows)
+
+-- Degenerated case.
+explain (costs off)
+select * from
+  (select a as x from sj where false) as q1,
+  (select a as y from sj where false) as q2
+where q1.x = q2.y;
+        QUERY PLAN        
+--------------------------
+ Result
+   One-Time Filter: false
+(2 rows)
+
+-- We can't use a cross-EC generated self join qual because of current logic of
+-- the generate_join_implied_equalities routine.
+explain (costs off)
+select * from sj t1, sj t2 where t1.a = t1.b and t1.b = t2.b and t2.b = t2.a;
+          QUERY PLAN          
+------------------------------
+ Nested Loop
+   Join Filter: (t1.a = t2.b)
+   ->  Seq Scan on sj t1
+         Filter: (a = b)
+   ->  Seq Scan on sj t2
+         Filter: (b = a)
+(6 rows)
+
+explain (costs off)
+select * from sj t1, sj t2, sj t3
+where t1.a = t1.b and t1.b = t2.b and t2.b = t2.a and
+      t1.b = t3.b and t3.b = t3.a;
+             QUERY PLAN             
+------------------------------------
+ Nested Loop
+   Join Filter: (t1.a = t3.b)
+   ->  Nested Loop
+         Join Filter: (t1.a = t2.b)
+         ->  Seq Scan on sj t1
+               Filter: (a = b)
+         ->  Seq Scan on sj t2
+               Filter: (b = a)
+   ->  Seq Scan on sj t3
+         Filter: (b = a)
+(10 rows)
+
+-- Double self-join removal.
+-- Use a condition on "b + 1", not on "b", for the second join, so that
+-- the equivalence class is different from the first one, and we can
+-- test the non-ec code path.
+explain (costs off)
+select *
+from  sj t1
+      join sj t2 on t1.a = t2.a and t1.b = t2.b
+	  join sj t3 on t2.a = t3.a and t2.b + 1 = t3.b + 1;
+                                QUERY PLAN                                 
+---------------------------------------------------------------------------
+ Seq Scan on sj t3
+   Filter: ((a IS NOT NULL) AND (b IS NOT NULL) AND ((b + 1) IS NOT NULL))
+(2 rows)
+
+-- subselect that references the removed relation
+explain (costs off)
+select t1.a, (select a from sj where a = t2.a and a = t1.a)
+from sj t1, sj t2
+where t1.a = t2.a;
+                QUERY PLAN                
+------------------------------------------
+ Seq Scan on sj t2
+   Filter: (a IS NOT NULL)
+   SubPlan 1
+     ->  Result
+           One-Time Filter: (t2.a = t2.a)
+           ->  Seq Scan on sj
+                 Filter: (a = t2.a)
+(7 rows)
+
+-- self-join under outer join
+explain (costs off)
+select * from sj x join sj y on x.a = y.a
+left join int8_tbl z on x.a = z.q1;
+             QUERY PLAN             
+------------------------------------
+ Nested Loop Left Join
+   Join Filter: (y.a = z.q1)
+   ->  Seq Scan on sj y
+         Filter: (a IS NOT NULL)
+   ->  Materialize
+         ->  Seq Scan on int8_tbl z
+(6 rows)
+
+explain (costs off)
+select * from sj x join sj y on x.a = y.a
+left join int8_tbl z on y.a = z.q1;
+             QUERY PLAN             
+------------------------------------
+ Nested Loop Left Join
+   Join Filter: (y.a = z.q1)
+   ->  Seq Scan on sj y
+         Filter: (a IS NOT NULL)
+   ->  Materialize
+         ->  Seq Scan on int8_tbl z
+(6 rows)
+
+explain (costs off)
+select * from (
+  select t1.*, t2.a as ax from sj t1 join sj t2
+  on (t1.a = t2.a and t1.c * t1.c = t2.c + 2 and t2.b is null)
+) as q1
+left join
+  (select t3.* from sj t3, sj t4 where t3.c = t4.c) as q2
+on q1.ax = q2.a;
+                                QUERY PLAN                                 
+---------------------------------------------------------------------------
+ Nested Loop Left Join
+   Join Filter: (t2.a = t4.a)
+   ->  Seq Scan on sj t2
+         Filter: ((b IS NULL) AND (a IS NOT NULL) AND ((c * c) = (c + 2)))
+   ->  Seq Scan on sj t4
+         Filter: (c IS NOT NULL)
+(6 rows)
+
+-- Test that placeholders are updated correctly after join removal
+explain (costs off)
+select * from (values (1)) x
+left join (select coalesce(y.q1, 1) from int8_tbl y
+	right join sj j1 inner join sj j2 on j1.a = j2.a
+	on true) z
+on true;
+                QUERY PLAN                
+------------------------------------------
+ Nested Loop Left Join
+   ->  Result
+   ->  Nested Loop Left Join
+         ->  Seq Scan on sj j2
+               Filter: (a IS NOT NULL)
+         ->  Materialize
+               ->  Seq Scan on int8_tbl y
+(7 rows)
+
+-- Test that references to the removed rel in lateral subqueries are replaced
+-- correctly after join removal
+explain (verbose, costs off)
+select t3.a from sj t1
+	join sj t2 on t1.a = t2.a
+	join lateral (select t1.a offset 0) t3 on true;
+             QUERY PLAN             
+------------------------------------
+ Nested Loop
+   Output: (t2.a)
+   ->  Seq Scan on public.sj t2
+         Output: t2.a, t2.b, t2.c
+         Filter: (t2.a IS NOT NULL)
+   ->  Result
+         Output: t2.a
+(7 rows)
+
+explain (verbose, costs off)
+select t3.a from sj t1
+	join sj t2 on t1.a = t2.a
+	join lateral (select * from (select t1.a offset 0) offset 0) t3 on true;
+             QUERY PLAN             
+------------------------------------
+ Nested Loop
+   Output: (t2.a)
+   ->  Seq Scan on public.sj t2
+         Output: t2.a, t2.b, t2.c
+         Filter: (t2.a IS NOT NULL)
+   ->  Result
+         Output: t2.a
+(7 rows)
+
+explain (verbose, costs off)
+select t4.a from sj t1
+	join sj t2 on t1.a = t2.a
+	join lateral (select t3.a from sj t3, (select t1.a) offset 0) t4 on true;
+             QUERY PLAN             
+------------------------------------
+ Nested Loop
+   Output: t3.a
+   ->  Seq Scan on public.sj t2
+         Output: t2.a, t2.b, t2.c
+         Filter: (t2.a IS NOT NULL)
+   ->  Seq Scan on public.sj t3
+         Output: t3.a
+(7 rows)
+
+-- Check updating of semi_rhs_exprs links from upper-level semi join to
+-- the removing relation
+explain (verbose, costs off)
+select t1.a from sj t1 where t1.b in (
+  select t2.b from sj t2 join sj t3 on t2.c=t3.c);
+                QUERY PLAN                
+------------------------------------------
+ Nested Loop Semi Join
+   Output: t1.a
+   Join Filter: (t1.b = t3.b)
+   ->  Seq Scan on public.sj t1
+         Output: t1.a, t1.b, t1.c
+   ->  Materialize
+         Output: t3.c, t3.b
+         ->  Seq Scan on public.sj t3
+               Output: t3.c, t3.b
+               Filter: (t3.c IS NOT NULL)
+(10 rows)
+
+--
+-- SJE corner case: uniqueness of an inner is [partially] derived from
+-- baserestrictinfo clauses.
+-- XXX: We really should allow SJE for these corner cases?
+--
+INSERT INTO sj VALUES (3, 1, 3);
+-- Don't remove SJ
+EXPLAIN (COSTS OFF)
+SELECT * FROM sj j1, sj j2 WHERE j1.b = j2.b AND j1.a = 2 AND j2.a = 3;
+          QUERY PLAN          
+------------------------------
+ Nested Loop
+   Join Filter: (j1.b = j2.b)
+   ->  Seq Scan on sj j1
+         Filter: (a = 2)
+   ->  Seq Scan on sj j2
+         Filter: (a = 3)
+(6 rows)
+
+-- Return one row
+SELECT * FROM sj j1, sj j2 WHERE j1.b = j2.b AND j1.a = 2 AND j2.a = 3;
+ a | b | c | a | b | c 
+---+---+---+---+---+---
+ 2 | 1 | 1 | 3 | 1 | 3
+(1 row)
+
+-- Remove SJ, define uniqueness by a constant
+EXPLAIN (COSTS OFF)
+SELECT * FROM sj j1, sj j2 WHERE j1.b = j2.b AND j1.a = 2 AND j2.a = 2;
+               QUERY PLAN                
+-----------------------------------------
+ Seq Scan on sj j2
+   Filter: ((b IS NOT NULL) AND (a = 2))
+(2 rows)
+
+-- Return one row
+SELECT * FROM sj j1, sj j2 WHERE j1.b = j2.b AND j1.a = 2 AND j2.a = 2;
+ a | b | c | a | b | c 
+---+---+---+---+---+---
+ 2 | 1 | 1 | 2 | 1 | 1
+(1 row)
+
+-- Remove SJ, define uniqueness by a constant expression
+EXPLAIN (COSTS OFF)
+SELECT * FROM sj j1, sj j2
+WHERE j1.b = j2.b
+  AND j1.a = (EXTRACT(DOW FROM current_timestamp(0))/15 + 3)::int
+  AND (EXTRACT(DOW FROM current_timestamp(0))/15 + 3)::int = j2.a;
+                                                         QUERY PLAN                                                         
+----------------------------------------------------------------------------------------------------------------------------
+ Seq Scan on sj j2
+   Filter: ((b IS NOT NULL) AND (a = (((EXTRACT(dow FROM CURRENT_TIMESTAMP(0)) / '15'::numeric) + '3'::numeric))::integer))
+(2 rows)
+
+-- Return one row
+SELECT * FROM sj j1, sj j2
+WHERE j1.b = j2.b
+  AND j1.a = (EXTRACT(DOW FROM current_timestamp(0))/15 + 3)::int
+  AND (EXTRACT(DOW FROM current_timestamp(0))/15 + 3)::int = j2.a;
+ a | b | c | a | b | c 
+---+---+---+---+---+---
+ 3 | 1 | 3 | 3 | 1 | 3
+(1 row)
+
+-- Remove SJ
+EXPLAIN (COSTS OFF)
+SELECT * FROM sj j1, sj j2 WHERE j1.b = j2.b AND j1.a = 1 AND j2.a = 1;
+               QUERY PLAN                
+-----------------------------------------
+ Seq Scan on sj j2
+   Filter: ((b IS NOT NULL) AND (a = 1))
+(2 rows)
+
+-- Return no rows
+SELECT * FROM sj j1, sj j2 WHERE j1.b = j2.b AND j1.a = 1 AND j2.a = 1;
+ a | b | c | a | b | c 
+---+---+---+---+---+---
+(0 rows)
+
+-- Shuffle a clause. Remove SJ
+EXPLAIN (COSTS OFF)
+SELECT * FROM sj j1, sj j2 WHERE j1.b = j2.b AND 1 = j1.a AND j2.a = 1;
+               QUERY PLAN                
+-----------------------------------------
+ Seq Scan on sj j2
+   Filter: ((b IS NOT NULL) AND (a = 1))
+(2 rows)
+
+-- Return no rows
+SELECT * FROM sj j1, sj j2 WHERE j1.b = j2.b AND 1 = j1.a AND j2.a = 1;
+ a | b | c | a | b | c 
+---+---+---+---+---+---
+(0 rows)
+
+-- SJE Corner case: a 'a.x=a.x' clause, have replaced with 'a.x IS NOT NULL'
+-- after SJ elimination it shouldn't be a mergejoinable clause.
+EXPLAIN (COSTS OFF)
+SELECT t4.*
+FROM (SELECT t1.*, t2.a AS a1 FROM sj t1, sj t2 WHERE t1.b = t2.b) AS t3
+JOIN sj t4 ON (t4.a = t3.a) WHERE t3.a1 = 42;
+           QUERY PLAN            
+---------------------------------
+ Nested Loop
+   Join Filter: (t1.b = t2.b)
+   ->  Seq Scan on sj t2
+         Filter: (a = 42)
+   ->  Seq Scan on sj t1
+         Filter: (a IS NOT NULL)
+(6 rows)
+
+SELECT t4.*
+FROM (SELECT t1.*, t2.a AS a1 FROM sj t1, sj t2 WHERE t1.b = t2.b) AS t3
+JOIN sj t4 ON (t4.a = t3.a) WHERE t3.a1 = 42;
+ a | b | c 
+---+---+---
+(0 rows)
+
+-- Functional index
+CREATE UNIQUE INDEX sj_fn_idx ON sj((a * a));
+-- Remove SJ
+EXPLAIN (COSTS OFF)
+SELECT * FROM sj j1, sj j2
+	WHERE j1.b = j2.b AND j1.a*j1.a = 1 AND j2.a*j2.a = 1;
+                  QUERY PLAN                   
+-----------------------------------------------
+ Seq Scan on sj j2
+   Filter: ((b IS NOT NULL) AND ((a * a) = 1))
+(2 rows)
+
+-- Don't remove SJ
+EXPLAIN (COSTS OFF)
+SELECT * FROM sj j1, sj j2
+	WHERE j1.b = j2.b AND j1.a*j1.a = 1 AND j2.a*j2.a = 2;
+          QUERY PLAN           
+-------------------------------
+ Nested Loop
+   Join Filter: (j1.b = j2.b)
+   ->  Seq Scan on sj j1
+         Filter: ((a * a) = 1)
+   ->  Seq Scan on sj j2
+         Filter: ((a * a) = 2)
+(6 rows)
+
+-- Restriction contains expressions in both sides, Remove SJ.
+EXPLAIN (COSTS OFF)
+SELECT * FROM sj j1, sj j2
+WHERE j1.b = j2.b
+  AND (j1.a*j1.a) = (EXTRACT(DOW FROM current_timestamp(0))/15 + 3)::int
+  AND (EXTRACT(DOW FROM current_timestamp(0))/15 + 3)::int = (j2.a*j2.a);
+                                                            QUERY PLAN                                                            
+----------------------------------------------------------------------------------------------------------------------------------
+ Seq Scan on sj j2
+   Filter: ((b IS NOT NULL) AND ((a * a) = (((EXTRACT(dow FROM CURRENT_TIMESTAMP(0)) / '15'::numeric) + '3'::numeric))::integer))
+(2 rows)
+
+-- Empty set of rows should be returned
+SELECT * FROM sj j1, sj j2
+WHERE j1.b = j2.b
+  AND (j1.a*j1.a) = (EXTRACT(DOW FROM current_timestamp(0))/15 + 3)::int
+  AND (EXTRACT(DOW FROM current_timestamp(0))/15 + 3)::int = (j2.a*j2.a);
+ a | b | c | a | b | c 
+---+---+---+---+---+---
+(0 rows)
+
+-- Restriction contains volatile function - disable SJE feature.
+EXPLAIN (COSTS OFF)
+SELECT * FROM sj j1, sj j2
+WHERE j1.b = j2.b
+  AND (j1.a*j1.c/3) = (random()/3 + 3)::int
+  AND (random()/3 + 3)::int = (j2.a*j2.c/3);
+                                                QUERY PLAN                                                 
+-----------------------------------------------------------------------------------------------------------
+ Nested Loop
+   Join Filter: (j1.b = j2.b)
+   ->  Seq Scan on sj j1
+         Filter: (((a * c) / 3) = (((random() / '3'::double precision) + '3'::double precision))::integer)
+   ->  Seq Scan on sj j2
+         Filter: ((((random() / '3'::double precision) + '3'::double precision))::integer = ((a * c) / 3))
+(6 rows)
+
+-- Return one row
+SELECT * FROM sj j1, sj j2
+WHERE j1.b = j2.b
+  AND (j1.a*j1.c/3) = (random()/3 + 3)::int
+  AND (random()/3 + 3)::int = (j2.a*j2.c/3);
+ a | b | c | a | b | c 
+---+---+---+---+---+---
+ 3 | 1 | 3 | 3 | 1 | 3
+(1 row)
+
+-- Multiple filters
+CREATE UNIQUE INDEX sj_temp_idx1 ON sj(a,b,c);
+-- Remove SJ
+EXPLAIN (COSTS OFF)
+SELECT * FROM sj j1, sj j2
+	WHERE j1.b = j2.b AND j1.a = 2 AND j1.c = 3 AND j2.a = 2 AND 3 = j2.c;
+                     QUERY PLAN                      
+-----------------------------------------------------
+ Seq Scan on sj j2
+   Filter: ((b IS NOT NULL) AND (a = 2) AND (c = 3))
+(2 rows)
+
+-- Don't remove SJ
+EXPLAIN (COSTS OFF)
+	SELECT * FROM sj j1, sj j2
+	WHERE j1.b = j2.b AND 2 = j1.a AND j1.c = 3 AND j2.a = 1 AND 3 = j2.c;
+              QUERY PLAN               
+---------------------------------------
+ Nested Loop
+   Join Filter: (j1.b = j2.b)
+   ->  Seq Scan on sj j1
+         Filter: ((2 = a) AND (c = 3))
+   ->  Seq Scan on sj j2
+         Filter: ((c = 3) AND (a = 1))
+(6 rows)
+
+CREATE UNIQUE INDEX sj_temp_idx ON sj(a,b);
+-- Don't remove SJ
+EXPLAIN (COSTS OFF)
+SELECT * FROM sj j1, sj j2 WHERE j1.b = j2.b AND j1.a = 2;
+          QUERY PLAN          
+------------------------------
+ Nested Loop
+   Join Filter: (j1.b = j2.b)
+   ->  Seq Scan on sj j1
+         Filter: (a = 2)
+   ->  Seq Scan on sj j2
+(5 rows)
+
+-- Don't remove SJ
+EXPLAIN (COSTS OFF)
+SELECT * FROM sj j1, sj j2 WHERE j1.b = j2.b AND 2 = j2.a;
+          QUERY PLAN          
+------------------------------
+ Nested Loop
+   Join Filter: (j1.b = j2.b)
+   ->  Seq Scan on sj j2
+         Filter: (2 = a)
+   ->  Seq Scan on sj j1
+(5 rows)
+
+-- Don't remove SJ
+EXPLAIN (COSTS OFF)
+SELECT * FROM sj j1, sj j2 WHERE j1.b = j2.b AND (j1.a = 1 OR j2.a = 1);
+                          QUERY PLAN                           
+---------------------------------------------------------------
+ Nested Loop
+   Join Filter: ((j1.b = j2.b) AND ((j1.a = 1) OR (j2.a = 1)))
+   ->  Seq Scan on sj j1
+   ->  Materialize
+         ->  Seq Scan on sj j2
+(5 rows)
+
+DROP INDEX sj_fn_idx, sj_temp_idx1, sj_temp_idx;
+-- Test that OR predicated are updated correctly after join removal
+CREATE TABLE tab_with_flag ( id INT PRIMARY KEY, is_flag SMALLINT);
+CREATE INDEX idx_test_is_flag ON tab_with_flag (is_flag);
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) FROM tab_with_flag
+WHERE
+	(is_flag IS NULL OR is_flag = 0)
+	AND id IN (SELECT id FROM tab_with_flag WHERE id IN (2, 3));
+                        QUERY PLAN                         
+-----------------------------------------------------------
+ Aggregate
+   ->  Bitmap Heap Scan on tab_with_flag
+         Recheck Cond: (id = ANY ('{2,3}'::integer[]))
+         Filter: ((is_flag IS NULL) OR (is_flag = 0))
+         ->  Bitmap Index Scan on tab_with_flag_pkey
+               Index Cond: (id = ANY ('{2,3}'::integer[]))
+(6 rows)
+
+DROP TABLE tab_with_flag;
+-- HAVING clause
+explain (costs off)
+select p.b from sj p join sj q on p.a = q.a group by p.b having sum(p.a) = 1;
+           QUERY PLAN            
+---------------------------------
+ HashAggregate
+   Group Key: q.b
+   Filter: (sum(q.a) = 1)
+   ->  Seq Scan on sj q
+         Filter: (a IS NOT NULL)
+(5 rows)
+
+-- update lateral references and range table entry reference
+explain (verbose, costs off)
+select 1 from (select x.* from sj x, sj y where x.a = y.a) q,
+  lateral generate_series(1, q.a) gs(i);
+                      QUERY PLAN                      
+------------------------------------------------------
+ Nested Loop
+   Output: 1
+   ->  Seq Scan on public.sj y
+         Output: y.a, y.b, y.c
+         Filter: (y.a IS NOT NULL)
+   ->  Function Scan on pg_catalog.generate_series gs
+         Output: gs.i
+         Function Call: generate_series(1, y.a)
+(8 rows)
+
+explain (verbose, costs off)
+select 1 from (select y.* from sj x, sj y where x.a = y.a) q,
+  lateral generate_series(1, q.a) gs(i);
+                      QUERY PLAN                      
+------------------------------------------------------
+ Nested Loop
+   Output: 1
+   ->  Seq Scan on public.sj y
+         Output: y.a, y.b, y.c
+         Filter: (y.a IS NOT NULL)
+   ->  Function Scan on pg_catalog.generate_series gs
+         Output: gs.i
+         Function Call: generate_series(1, y.a)
+(8 rows)
+
+-- Test that a non-EC-derived join clause is processed correctly. Use an
+-- outer join so that we can't form an EC.
+explain (costs off) select * from sj p join sj q on p.a = q.a
+  left join sj r on p.a + q.a = r.a;
+             QUERY PLAN             
+------------------------------------
+ Nested Loop Left Join
+   Join Filter: ((q.a + q.a) = r.a)
+   ->  Seq Scan on sj q
+         Filter: (a IS NOT NULL)
+   ->  Materialize
+         ->  Seq Scan on sj r
+(6 rows)
+
+-- FIXME this constant false filter doesn't look good. Should we merge
+-- equivalence classes?
+explain (costs off)
+select * from sj p, sj q where p.a = q.a and p.b = 1 and q.b = 2;
+                     QUERY PLAN                      
+-----------------------------------------------------
+ Seq Scan on sj q
+   Filter: ((a IS NOT NULL) AND (b = 2) AND (b = 1))
+(2 rows)
+
+-- Check that attr_needed is updated correctly after self-join removal. In this
+-- test, the join of j1 with j2 is removed. k1.b is required at either j1 or j2.
+-- If this info is lost, join targetlist for (k1, k2) will not contain k1.b.
+-- Use index scan for k1 so that we don't get 'b' from physical tlist used for
+-- seqscan. Also disable reordering of joins because this test depends on a
+-- particular join tree.
+create table sk (a int, b int);
+create index on sk(a);
+set join_collapse_limit to 1;
+set enable_seqscan to off;
+explain (costs off) select 1 from
+	(sk k1 join sk k2 on k1.a = k2.a)
+	join (sj j1 join sj j2 on j1.a = j2.a) on j1.b = k1.b;
+                     QUERY PLAN                      
+-----------------------------------------------------
+ Nested Loop
+   Join Filter: (k1.b = j2.b)
+   ->  Nested Loop
+         ->  Index Scan using sk_a_idx on sk k1
+         ->  Index Only Scan using sk_a_idx on sk k2
+               Index Cond: (a = k1.a)
+   ->  Materialize
+         ->  Index Scan using sj_a_key on sj j2
+               Index Cond: (a IS NOT NULL)
+(9 rows)
+
+explain (costs off) select 1 from
+	(sk k1 join sk k2 on k1.a = k2.a)
+	join (sj j1 join sj j2 on j1.a = j2.a) on j2.b = k1.b;
+                     QUERY PLAN                      
+-----------------------------------------------------
+ Nested Loop
+   Join Filter: (k1.b = j2.b)
+   ->  Nested Loop
+         ->  Index Scan using sk_a_idx on sk k1
+         ->  Index Only Scan using sk_a_idx on sk k2
+               Index Cond: (a = k1.a)
+   ->  Materialize
+         ->  Index Scan using sj_a_key on sj j2
+               Index Cond: (a IS NOT NULL)
+(9 rows)
+
+reset join_collapse_limit;
+reset enable_seqscan;
+-- Check that clauses from the join filter list is not lost on the self-join removal
+CREATE TABLE emp1 (id SERIAL PRIMARY KEY NOT NULL, code int);
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT * FROM emp1 e1, emp1 e2 WHERE e1.id = e2.id AND e2.code <> e1.code;
+                QUERY PLAN                
+------------------------------------------
+ Seq Scan on public.emp1 e2
+   Output: e2.id, e2.code, e2.id, e2.code
+   Filter: (e2.code <> e2.code)
+(3 rows)
+
+-- Shuffle self-joined relations. Only in the case of iterative deletion
+-- attempts explains of these queries will be identical.
+CREATE UNIQUE INDEX ON emp1((id*id));
+EXPLAIN (COSTS OFF)
+SELECT count(*) FROM emp1 c1, emp1 c2, emp1 c3
+WHERE c1.id=c2.id AND c1.id*c2.id=c3.id*c3.id;
+               QUERY PLAN                
+-----------------------------------------
+ Aggregate
+   ->  Seq Scan on emp1 c3
+         Filter: ((id * id) IS NOT NULL)
+(3 rows)
+
+EXPLAIN (COSTS OFF)
+SELECT count(*) FROM emp1 c1, emp1 c2, emp1 c3
+WHERE c1.id=c3.id AND c1.id*c3.id=c2.id*c2.id;
+               QUERY PLAN                
+-----------------------------------------
+ Aggregate
+   ->  Seq Scan on emp1 c3
+         Filter: ((id * id) IS NOT NULL)
+(3 rows)
+
+EXPLAIN (COSTS OFF)
+SELECT count(*) FROM emp1 c1, emp1 c2, emp1 c3
+WHERE c3.id=c2.id AND c3.id*c2.id=c1.id*c1.id;
+               QUERY PLAN                
+-----------------------------------------
+ Aggregate
+   ->  Seq Scan on emp1 c3
+         Filter: ((id * id) IS NOT NULL)
+(3 rows)
+
+-- Check the usage of a parse tree by the set operations (bug #18170)
+EXPLAIN (COSTS OFF)
+SELECT c1.code FROM emp1 c1 LEFT JOIN emp1 c2 ON c1.id = c2.id
+WHERE c2.id IS NOT NULL
+EXCEPT ALL
+SELECT c3.code FROM emp1 c3;
+        QUERY PLAN         
+---------------------------
+ HashSetOp Except All
+   ->  Seq Scan on emp1 c2
+   ->  Seq Scan on emp1 c3
+(3 rows)
+
+-- Check that SJE removes references from PHVs correctly
+explain (costs off)
+select * from emp1 t1 left join
+    (select coalesce(t3.code, 1) from emp1 t2
+        left join (emp1 t3 join emp1 t4 on t3.id = t4.id)
+        on true)
+on true;
+                 QUERY PLAN                  
+---------------------------------------------
+ Nested Loop Left Join
+   ->  Seq Scan on emp1 t1
+   ->  Materialize
+         ->  Nested Loop Left Join
+               ->  Seq Scan on emp1 t2
+               ->  Materialize
+                     ->  Seq Scan on emp1 t4
+(7 rows)
+
+-- Check that SJE removes the whole PHVs correctly
+explain (verbose, costs off)
+select 1 from emp1 t1 left join
+    ((select 1 as x, * from emp1 t2) s1 inner join
+        (select * from emp1 t3) s2 on s1.id = s2.id)
+    on true
+where s1.x = 1;
+               QUERY PLAN               
+----------------------------------------
+ Nested Loop
+   Output: 1
+   ->  Seq Scan on public.emp1 t1
+         Output: t1.id, t1.code
+   ->  Materialize
+         Output: t3.id
+         ->  Seq Scan on public.emp1 t3
+               Output: t3.id
+               Filter: (1 = 1)
+(9 rows)
+
+-- Check that PHVs do not impose any constraints on removing self joins
+explain (verbose, costs off)
+select * from emp1 t1 join emp1 t2 on t1.id = t2.id left join
+    lateral (select t1.id as t1id, * from generate_series(1,1) t3) s on true;
+                        QUERY PLAN                        
+----------------------------------------------------------
+ Nested Loop Left Join
+   Output: t2.id, t2.code, t2.id, t2.code, (t2.id), t3.t3
+   ->  Seq Scan on public.emp1 t2
+         Output: t2.id, t2.code
+   ->  Function Scan on pg_catalog.generate_series t3
+         Output: t3.t3, t2.id
+         Function Call: generate_series(1, 1)
+(7 rows)
+
+explain (verbose, costs off)
+select * from generate_series(1,10) t1(id) left join
+    lateral (select t1.id as t1id, t2.id from emp1 t2 join emp1 t3 on t2.id = t3.id)
+on true;
+                      QUERY PLAN                      
+------------------------------------------------------
+ Nested Loop Left Join
+   Output: t1.id, (t1.id), t3.id
+   ->  Function Scan on pg_catalog.generate_series t1
+         Output: t1.id
+         Function Call: generate_series(1, 10)
+   ->  Seq Scan on public.emp1 t3
+         Output: t3.id, t1.id
+(7 rows)
+
+-- Check that SJE replaces join clauses involving the removed rel correctly
+explain (costs off)
+select * from emp1 t1
+   inner join emp1 t2 on t1.id = t2.id
+    left join emp1 t3 on t1.id > 1 and t1.id < 2;
+                  QUERY PLAN                  
+----------------------------------------------
+ Nested Loop Left Join
+   Join Filter: ((t2.id > 1) AND (t2.id < 2))
+   ->  Seq Scan on emp1 t2
+   ->  Materialize
+         ->  Seq Scan on emp1 t3
+(5 rows)
+
+-- Check that SJE doesn't replace the target relation
+EXPLAIN (COSTS OFF)
+WITH t1 AS (SELECT * FROM emp1)
+UPDATE emp1 SET code = t1.code + 1 FROM t1
+WHERE t1.id = emp1.id RETURNING emp1.id, emp1.code, t1.code;
+                      QUERY PLAN                       
+-------------------------------------------------------
+ Update on emp1
+   ->  Nested Loop
+         ->  Seq Scan on emp1
+         ->  Index Scan using emp1_pkey on emp1 emp1_1
+               Index Cond: (id = emp1.id)
+(5 rows)
+
+INSERT INTO emp1 VALUES (1, 1), (2, 1);
+WITH t1 AS (SELECT * FROM emp1)
+UPDATE emp1 SET code = t1.code + 1 FROM t1
+WHERE t1.id = emp1.id RETURNING emp1.id, emp1.code, t1.code;
+ id | code | code 
+----+------+------
+  1 |    2 |    1
+  2 |    2 |    1
+(2 rows)
+
+TRUNCATE emp1;
+EXPLAIN (COSTS OFF)
+UPDATE sj sq SET b = 1 FROM sj as sz WHERE sq.a = sz.a;
+             QUERY PLAN              
+-------------------------------------
+ Update on sj sq
+   ->  Nested Loop
+         Join Filter: (sq.a = sz.a)
+         ->  Seq Scan on sj sq
+         ->  Materialize
+               ->  Seq Scan on sj sz
+(6 rows)
+
+CREATE RULE sj_del_rule AS ON DELETE TO sj
+  DO INSTEAD
+    UPDATE sj SET a = 1 WHERE a = old.a;
+EXPLAIN (COSTS OFF) DELETE FROM sj;
+              QUERY PLAN              
+--------------------------------------
+ Update on sj sj_1
+   ->  Nested Loop
+         Join Filter: (sj.a = sj_1.a)
+         ->  Seq Scan on sj sj_1
+         ->  Materialize
+               ->  Seq Scan on sj
+(6 rows)
+
+DROP RULE sj_del_rule ON sj CASCADE;
+-- Check that SJE does not mistakenly omit qual clauses (bug #18187)
+insert into emp1 values (1, 1);
+explain (costs off)
+select 1 from emp1 full join
+    (select * from emp1 t1 join
+        emp1 t2 join emp1 t3 on t2.id = t3.id
+        on true
+    where false) s on true
+where false;
+        QUERY PLAN        
+--------------------------
+ Result
+   One-Time Filter: false
+(2 rows)
+
+select 1 from emp1 full join
+    (select * from emp1 t1 join
+        emp1 t2 join emp1 t3 on t2.id = t3.id
+        on true
+    where false) s on true
+where false;
+ ?column? 
+----------
+(0 rows)
+
+-- Check that SJE does not mistakenly re-use knowledge of relation uniqueness
+-- made with different set of quals
+insert into emp1 values (2, 1);
+explain (costs off)
+select * from emp1 t1 where exists (select * from emp1 t2
+                                    where t2.id = t1.code and t2.code > 0);
+                 QUERY PLAN                  
+---------------------------------------------
+ Nested Loop
+   ->  Seq Scan on emp1 t1
+   ->  Index Scan using emp1_pkey on emp1 t2
+         Index Cond: (id = t1.code)
+         Filter: (code > 0)
+(5 rows)
+
+select * from emp1 t1 where exists (select * from emp1 t2
+                                    where t2.id = t1.code and t2.code > 0);
+ id | code 
+----+------
+  1 |    1
+  2 |    1
+(2 rows)
+
+-- We can remove the join even if we find the join can't duplicate rows and
+-- the base quals of each side are different.  In the following case we end up
+-- moving quals over to s1 to make it so it can't match any rows.
+create table sl(a int, b int, c int);
+create unique index on sl(a, b);
+vacuum analyze sl;
+-- Both sides are unique, but base quals are different
+explain (costs off)
+select * from sl t1, sl t2 where t1.a = t2.a and t1.b = 1 and t2.b = 2;
+          QUERY PLAN          
+------------------------------
+ Nested Loop
+   Join Filter: (t1.a = t2.a)
+   ->  Seq Scan on sl t1
+         Filter: (b = 1)
+   ->  Seq Scan on sl t2
+         Filter: (b = 2)
+(6 rows)
+
+-- Check NullTest in baserestrictinfo list
+explain (costs off)
+select * from sl t1, sl t2
+where t1.a = t2.a and t1.b = 1 and t2.b = 2
+  and t1.c IS NOT NULL and t2.c IS NOT NULL
+  and t2.b IS NOT NULL and t1.b IS NOT NULL
+  and t1.a IS NOT NULL and t2.a IS NOT NULL;
+                                      QUERY PLAN                                       
+---------------------------------------------------------------------------------------
+ Nested Loop
+   Join Filter: (t1.a = t2.a)
+   ->  Seq Scan on sl t1
+         Filter: ((c IS NOT NULL) AND (b IS NOT NULL) AND (a IS NOT NULL) AND (b = 1))
+   ->  Seq Scan on sl t2
+         Filter: ((c IS NOT NULL) AND (b IS NOT NULL) AND (a IS NOT NULL) AND (b = 2))
+(6 rows)
+
+explain (verbose, costs off)
+select * from sl t1, sl t2
+where t1.b = t2.b and t2.a = 3 and t1.a = 3
+  and t1.c IS NOT NULL and t2.c IS NOT NULL
+  and t2.b IS NOT NULL and t1.b IS NOT NULL
+  and t1.a IS NOT NULL and t2.a IS NOT NULL;
+                                         QUERY PLAN                                          
+---------------------------------------------------------------------------------------------
+ Seq Scan on public.sl t2
+   Output: t2.a, t2.b, t2.c, t2.a, t2.b, t2.c
+   Filter: ((t2.c IS NOT NULL) AND (t2.b IS NOT NULL) AND (t2.a IS NOT NULL) AND (t2.a = 3))
+(3 rows)
+
+-- Join qual isn't mergejoinable, but inner is unique.
+EXPLAIN (COSTS OFF)
+SELECT n2.a FROM sj n1, sj n2 WHERE n1.a <> n2.a AND n2.a = 1;
+          QUERY PLAN           
+-------------------------------
+ Nested Loop
+   Join Filter: (n1.a <> n2.a)
+   ->  Seq Scan on sj n2
+         Filter: (a = 1)
+   ->  Seq Scan on sj n1
+(5 rows)
+
+EXPLAIN (COSTS OFF)
+SELECT * FROM
+(SELECT n2.a FROM sj n1, sj n2 WHERE n1.a <> n2.a) q0, sl
+WHERE q0.a = 1;
+          QUERY PLAN           
+-------------------------------
+ Nested Loop
+   Join Filter: (n1.a <> n2.a)
+   ->  Nested Loop
+         ->  Seq Scan on sl
+         ->  Seq Scan on sj n2
+               Filter: (a = 1)
+   ->  Seq Scan on sj n1
+(7 rows)
+
+-- Check optimization disabling if it will violate special join conditions.
+-- Two identical joined relations satisfies self join removal conditions but
+-- stay in different special join infos.
+CREATE TABLE sj_t1 (id serial, a int);
+CREATE TABLE sj_t2 (id serial, a int);
+CREATE TABLE sj_t3 (id serial, a int);
+CREATE TABLE sj_t4 (id serial, a int);
+CREATE UNIQUE INDEX ON sj_t3 USING btree (a,id);
+CREATE UNIQUE INDEX ON sj_t2 USING btree (id);
+EXPLAIN (COSTS OFF)
+SELECT * FROM sj_t1
+JOIN (
+	SELECT sj_t2.id AS id FROM sj_t2
+	WHERE EXISTS
+		(
+		SELECT TRUE FROM sj_t3,sj_t4 WHERE sj_t3.a = 1 AND sj_t3.id = sj_t2.id
+		)
+	) t2t3t4
+ON sj_t1.id = t2t3t4.id
+JOIN (
+	SELECT sj_t2.id AS id FROM sj_t2
+	WHERE EXISTS
+		(
+		SELECT TRUE FROM sj_t3,sj_t4 WHERE sj_t3.a = 1 AND sj_t3.id = sj_t2.id
+		)
+	) _t2t3t4
+ON sj_t1.id = _t2t3t4.id;
+                                     QUERY PLAN                                      
+-------------------------------------------------------------------------------------
+ Nested Loop
+   Join Filter: (sj_t3.id = sj_t1.id)
+   ->  Nested Loop
+         Join Filter: (sj_t2.id = sj_t3.id)
+         ->  Nested Loop Semi Join
+               ->  Nested Loop
+                     ->  HashAggregate
+                           Group Key: sj_t3.id
+                           ->  Nested Loop
+                                 ->  Seq Scan on sj_t4
+                                 ->  Materialize
+                                       ->  Bitmap Heap Scan on sj_t3
+                                             Recheck Cond: (a = 1)
+                                             ->  Bitmap Index Scan on sj_t3_a_id_idx
+                                                   Index Cond: (a = 1)
+                     ->  Index Only Scan using sj_t2_id_idx on sj_t2 sj_t2_1
+                           Index Cond: (id = sj_t3.id)
+               ->  Nested Loop
+                     ->  Index Only Scan using sj_t3_a_id_idx on sj_t3 sj_t3_1
+                           Index Cond: ((a = 1) AND (id = sj_t3.id))
+                     ->  Seq Scan on sj_t4 sj_t4_1
+         ->  Index Only Scan using sj_t2_id_idx on sj_t2
+               Index Cond: (id = sj_t2_1.id)
+   ->  Seq Scan on sj_t1
+(24 rows)
+
+--
+-- Test RowMarks-related code
+--
+-- Both sides have explicit LockRows marks
+EXPLAIN (COSTS OFF)
+SELECT a1.a FROM sj a1,sj a2 WHERE (a1.a=a2.a) FOR UPDATE;
+           QUERY PLAN            
+---------------------------------
+ LockRows
+   ->  Seq Scan on sj a2
+         Filter: (a IS NOT NULL)
+(3 rows)
+
+reset enable_hashjoin;
+reset enable_mergejoin;
 --
 -- Test hints given on incorrect column references are useful
 --
diff --git a/src/test/regress/expected/sysviews.out b/src/test/regress/expected/sysviews.out
index 352abc0bd42..83228cfca29 100644
--- a/src/test/regress/expected/sysviews.out
+++ b/src/test/regress/expected/sysviews.out
@@ -168,10 +168,11 @@ select name, setting from pg_settings where name like 'enable%';
  enable_partitionwise_aggregate | off
  enable_partitionwise_join      | off
  enable_presorted_aggregate     | on
+ enable_self_join_elimination   | on
  enable_seqscan                 | on
  enable_sort                    | on
  enable_tidscan                 | on
-(23 rows)
+(24 rows)
 
 -- There are always wait event descriptions for various types.  InjectionPoint
 -- may be present or absent, depending on history since last postmaster start.
diff --git a/src/test/regress/sql/equivclass.sql b/src/test/regress/sql/equivclass.sql
index 28ed7910d01..7fc2159349b 100644
--- a/src/test/regress/sql/equivclass.sql
+++ b/src/test/regress/sql/equivclass.sql
@@ -259,6 +259,22 @@ drop user regress_user_ectest;
 explain (costs off)
   select * from tenk1 where unique1 = unique1 and unique2 = unique2;
 
+-- Test that broken ECs are processed correctly during self join removal.
+-- Disable merge joins so that we don't get an error about missing commutator.
+-- Test both orientations of the join clause, because only one of them breaks
+-- the EC.
+set enable_mergejoin to off;
+
+explain (costs off)
+  select * from ec0 m join ec0 n on m.ff = n.ff
+  join ec1 p on m.ff + n.ff = p.f1;
+
+explain (costs off)
+  select * from ec0 m join ec0 n on m.ff = n.ff
+  join ec1 p on p.f1::int8 = (m.ff + n.ff)::int8alias1;
+
+reset enable_mergejoin;
+
 -- this could be converted, but isn't at present
 explain (costs off)
   select * from tenk1 where unique1 = unique1 or unique2 = unique2;
diff --git a/src/test/regress/sql/join.sql b/src/test/regress/sql/join.sql
index c7349eab933..c29d13b9fed 100644
--- a/src/test/regress/sql/join.sql
+++ b/src/test/regress/sql/join.sql
@@ -2175,6 +2175,17 @@ select c.id, ss.a from c
   left join (select d.a from onerow, d left join b on d.a = b.id) ss
   on c.id = ss.a;
 
+-- check the case when the placeholder relates to an outer join and its
+-- inner in the press field but actually uses only the outer side of the join
+explain (costs off)
+SELECT q.val FROM b LEFT JOIN (
+  SELECT (q1.z IS NOT NULL) AS val
+  FROM b LEFT JOIN (
+    SELECT (t1.b_id IS NOT NULL) AS z FROM a t1 LEFT JOIN a t2 USING (id)
+    ) AS q1
+  ON true
+) AS q ON true;
+
 CREATE TEMP TABLE parted_b (id int PRIMARY KEY) partition by range(id);
 CREATE TEMP TABLE parted_b1 partition of parted_b for values from (0) to (10);
 
@@ -2409,6 +2420,489 @@ select * from
 select * from
   int8_tbl x join (int4_tbl x cross join int4_tbl y(ff)) j on q1 = f1; -- ok
 
+--
+-- test that semi- or inner self-joins on a unique column are removed
+--
+
+-- enable only nestloop to get more predictable plans
+set enable_hashjoin to off;
+set enable_mergejoin to off;
+
+create table sj (a int unique, b int, c int unique);
+insert into sj values (1, null, 2), (null, 2, null), (2, 1, 1);
+analyze sj;
+
+-- Trivial self-join case.
+explain (costs off)
+select p.* from sj p, sj q where q.a = p.a and q.b = q.a - 1;
+select p.* from sj p, sj q where q.a = p.a and q.b = q.a - 1;
+
+-- Self-join removal performs after a subquery pull-up process and could remove
+-- such kind of self-join too. Check this option.
+explain (costs off)
+select * from sj p
+where exists (select * from sj q
+              where q.a = p.a and q.b < 10);
+select * from sj p
+where exists (select * from sj q
+              where q.a = p.a and q.b < 10);
+
+-- Don't remove self-join for the case of equality of two different unique columns.
+explain (costs off)
+select * from sj t1, sj t2 where t1.a = t2.c and t1.b is not null;
+
+-- Ensure that relations with TABLESAMPLE clauses are not considered as
+-- candidates to be removed
+explain (costs off)
+select * from sj t1
+    join lateral
+      (select * from sj tablesample system(t1.b)) s
+    on t1.a = s.a;
+
+-- Ensure that SJE does not form a self-referential lateral dependency
+explain (costs off)
+select * from sj t1
+    left join lateral
+      (select t1.a as t1a, * from sj t2) s
+    on true
+where t1.a = s.a;
+
+-- Degenerated case.
+explain (costs off)
+select * from
+  (select a as x from sj where false) as q1,
+  (select a as y from sj where false) as q2
+where q1.x = q2.y;
+
+-- We can't use a cross-EC generated self join qual because of current logic of
+-- the generate_join_implied_equalities routine.
+explain (costs off)
+select * from sj t1, sj t2 where t1.a = t1.b and t1.b = t2.b and t2.b = t2.a;
+explain (costs off)
+select * from sj t1, sj t2, sj t3
+where t1.a = t1.b and t1.b = t2.b and t2.b = t2.a and
+      t1.b = t3.b and t3.b = t3.a;
+
+-- Double self-join removal.
+-- Use a condition on "b + 1", not on "b", for the second join, so that
+-- the equivalence class is different from the first one, and we can
+-- test the non-ec code path.
+explain (costs off)
+select *
+from  sj t1
+      join sj t2 on t1.a = t2.a and t1.b = t2.b
+	  join sj t3 on t2.a = t3.a and t2.b + 1 = t3.b + 1;
+
+-- subselect that references the removed relation
+explain (costs off)
+select t1.a, (select a from sj where a = t2.a and a = t1.a)
+from sj t1, sj t2
+where t1.a = t2.a;
+
+-- self-join under outer join
+explain (costs off)
+select * from sj x join sj y on x.a = y.a
+left join int8_tbl z on x.a = z.q1;
+
+explain (costs off)
+select * from sj x join sj y on x.a = y.a
+left join int8_tbl z on y.a = z.q1;
+
+explain (costs off)
+select * from (
+  select t1.*, t2.a as ax from sj t1 join sj t2
+  on (t1.a = t2.a and t1.c * t1.c = t2.c + 2 and t2.b is null)
+) as q1
+left join
+  (select t3.* from sj t3, sj t4 where t3.c = t4.c) as q2
+on q1.ax = q2.a;
+
+-- Test that placeholders are updated correctly after join removal
+explain (costs off)
+select * from (values (1)) x
+left join (select coalesce(y.q1, 1) from int8_tbl y
+	right join sj j1 inner join sj j2 on j1.a = j2.a
+	on true) z
+on true;
+
+-- Test that references to the removed rel in lateral subqueries are replaced
+-- correctly after join removal
+explain (verbose, costs off)
+select t3.a from sj t1
+	join sj t2 on t1.a = t2.a
+	join lateral (select t1.a offset 0) t3 on true;
+
+explain (verbose, costs off)
+select t3.a from sj t1
+	join sj t2 on t1.a = t2.a
+	join lateral (select * from (select t1.a offset 0) offset 0) t3 on true;
+
+explain (verbose, costs off)
+select t4.a from sj t1
+	join sj t2 on t1.a = t2.a
+	join lateral (select t3.a from sj t3, (select t1.a) offset 0) t4 on true;
+
+-- Check updating of semi_rhs_exprs links from upper-level semi join to
+-- the removing relation
+explain (verbose, costs off)
+select t1.a from sj t1 where t1.b in (
+  select t2.b from sj t2 join sj t3 on t2.c=t3.c);
+
+--
+-- SJE corner case: uniqueness of an inner is [partially] derived from
+-- baserestrictinfo clauses.
+-- XXX: We really should allow SJE for these corner cases?
+--
+
+INSERT INTO sj VALUES (3, 1, 3);
+
+-- Don't remove SJ
+EXPLAIN (COSTS OFF)
+SELECT * FROM sj j1, sj j2 WHERE j1.b = j2.b AND j1.a = 2 AND j2.a = 3;
+-- Return one row
+SELECT * FROM sj j1, sj j2 WHERE j1.b = j2.b AND j1.a = 2 AND j2.a = 3;
+
+-- Remove SJ, define uniqueness by a constant
+EXPLAIN (COSTS OFF)
+SELECT * FROM sj j1, sj j2 WHERE j1.b = j2.b AND j1.a = 2 AND j2.a = 2;
+-- Return one row
+SELECT * FROM sj j1, sj j2 WHERE j1.b = j2.b AND j1.a = 2 AND j2.a = 2;
+
+-- Remove SJ, define uniqueness by a constant expression
+EXPLAIN (COSTS OFF)
+SELECT * FROM sj j1, sj j2
+WHERE j1.b = j2.b
+  AND j1.a = (EXTRACT(DOW FROM current_timestamp(0))/15 + 3)::int
+  AND (EXTRACT(DOW FROM current_timestamp(0))/15 + 3)::int = j2.a;
+-- Return one row
+SELECT * FROM sj j1, sj j2
+WHERE j1.b = j2.b
+  AND j1.a = (EXTRACT(DOW FROM current_timestamp(0))/15 + 3)::int
+  AND (EXTRACT(DOW FROM current_timestamp(0))/15 + 3)::int = j2.a;
+
+-- Remove SJ
+EXPLAIN (COSTS OFF)
+SELECT * FROM sj j1, sj j2 WHERE j1.b = j2.b AND j1.a = 1 AND j2.a = 1;
+-- Return no rows
+SELECT * FROM sj j1, sj j2 WHERE j1.b = j2.b AND j1.a = 1 AND j2.a = 1;
+
+-- Shuffle a clause. Remove SJ
+EXPLAIN (COSTS OFF)
+SELECT * FROM sj j1, sj j2 WHERE j1.b = j2.b AND 1 = j1.a AND j2.a = 1;
+-- Return no rows
+SELECT * FROM sj j1, sj j2 WHERE j1.b = j2.b AND 1 = j1.a AND j2.a = 1;
+
+-- SJE Corner case: a 'a.x=a.x' clause, have replaced with 'a.x IS NOT NULL'
+-- after SJ elimination it shouldn't be a mergejoinable clause.
+EXPLAIN (COSTS OFF)
+SELECT t4.*
+FROM (SELECT t1.*, t2.a AS a1 FROM sj t1, sj t2 WHERE t1.b = t2.b) AS t3
+JOIN sj t4 ON (t4.a = t3.a) WHERE t3.a1 = 42;
+SELECT t4.*
+FROM (SELECT t1.*, t2.a AS a1 FROM sj t1, sj t2 WHERE t1.b = t2.b) AS t3
+JOIN sj t4 ON (t4.a = t3.a) WHERE t3.a1 = 42;
+
+-- Functional index
+CREATE UNIQUE INDEX sj_fn_idx ON sj((a * a));
+
+-- Remove SJ
+EXPLAIN (COSTS OFF)
+SELECT * FROM sj j1, sj j2
+	WHERE j1.b = j2.b AND j1.a*j1.a = 1 AND j2.a*j2.a = 1;
+-- Don't remove SJ
+EXPLAIN (COSTS OFF)
+SELECT * FROM sj j1, sj j2
+	WHERE j1.b = j2.b AND j1.a*j1.a = 1 AND j2.a*j2.a = 2;
+
+-- Restriction contains expressions in both sides, Remove SJ.
+EXPLAIN (COSTS OFF)
+SELECT * FROM sj j1, sj j2
+WHERE j1.b = j2.b
+  AND (j1.a*j1.a) = (EXTRACT(DOW FROM current_timestamp(0))/15 + 3)::int
+  AND (EXTRACT(DOW FROM current_timestamp(0))/15 + 3)::int = (j2.a*j2.a);
+-- Empty set of rows should be returned
+SELECT * FROM sj j1, sj j2
+WHERE j1.b = j2.b
+  AND (j1.a*j1.a) = (EXTRACT(DOW FROM current_timestamp(0))/15 + 3)::int
+  AND (EXTRACT(DOW FROM current_timestamp(0))/15 + 3)::int = (j2.a*j2.a);
+
+-- Restriction contains volatile function - disable SJE feature.
+EXPLAIN (COSTS OFF)
+SELECT * FROM sj j1, sj j2
+WHERE j1.b = j2.b
+  AND (j1.a*j1.c/3) = (random()/3 + 3)::int
+  AND (random()/3 + 3)::int = (j2.a*j2.c/3);
+-- Return one row
+SELECT * FROM sj j1, sj j2
+WHERE j1.b = j2.b
+  AND (j1.a*j1.c/3) = (random()/3 + 3)::int
+  AND (random()/3 + 3)::int = (j2.a*j2.c/3);
+
+-- Multiple filters
+CREATE UNIQUE INDEX sj_temp_idx1 ON sj(a,b,c);
+
+-- Remove SJ
+EXPLAIN (COSTS OFF)
+SELECT * FROM sj j1, sj j2
+	WHERE j1.b = j2.b AND j1.a = 2 AND j1.c = 3 AND j2.a = 2 AND 3 = j2.c;
+
+-- Don't remove SJ
+EXPLAIN (COSTS OFF)
+	SELECT * FROM sj j1, sj j2
+	WHERE j1.b = j2.b AND 2 = j1.a AND j1.c = 3 AND j2.a = 1 AND 3 = j2.c;
+
+CREATE UNIQUE INDEX sj_temp_idx ON sj(a,b);
+
+-- Don't remove SJ
+EXPLAIN (COSTS OFF)
+SELECT * FROM sj j1, sj j2 WHERE j1.b = j2.b AND j1.a = 2;
+
+-- Don't remove SJ
+EXPLAIN (COSTS OFF)
+SELECT * FROM sj j1, sj j2 WHERE j1.b = j2.b AND 2 = j2.a;
+
+-- Don't remove SJ
+EXPLAIN (COSTS OFF)
+SELECT * FROM sj j1, sj j2 WHERE j1.b = j2.b AND (j1.a = 1 OR j2.a = 1);
+
+DROP INDEX sj_fn_idx, sj_temp_idx1, sj_temp_idx;
+
+-- Test that OR predicated are updated correctly after join removal
+CREATE TABLE tab_with_flag ( id INT PRIMARY KEY, is_flag SMALLINT);
+CREATE INDEX idx_test_is_flag ON tab_with_flag (is_flag);
+
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) FROM tab_with_flag
+WHERE
+	(is_flag IS NULL OR is_flag = 0)
+	AND id IN (SELECT id FROM tab_with_flag WHERE id IN (2, 3));
+DROP TABLE tab_with_flag;
+
+-- HAVING clause
+explain (costs off)
+select p.b from sj p join sj q on p.a = q.a group by p.b having sum(p.a) = 1;
+
+-- update lateral references and range table entry reference
+explain (verbose, costs off)
+select 1 from (select x.* from sj x, sj y where x.a = y.a) q,
+  lateral generate_series(1, q.a) gs(i);
+
+explain (verbose, costs off)
+select 1 from (select y.* from sj x, sj y where x.a = y.a) q,
+  lateral generate_series(1, q.a) gs(i);
+
+-- Test that a non-EC-derived join clause is processed correctly. Use an
+-- outer join so that we can't form an EC.
+explain (costs off) select * from sj p join sj q on p.a = q.a
+  left join sj r on p.a + q.a = r.a;
+
+-- FIXME this constant false filter doesn't look good. Should we merge
+-- equivalence classes?
+explain (costs off)
+select * from sj p, sj q where p.a = q.a and p.b = 1 and q.b = 2;
+
+-- Check that attr_needed is updated correctly after self-join removal. In this
+-- test, the join of j1 with j2 is removed. k1.b is required at either j1 or j2.
+-- If this info is lost, join targetlist for (k1, k2) will not contain k1.b.
+-- Use index scan for k1 so that we don't get 'b' from physical tlist used for
+-- seqscan. Also disable reordering of joins because this test depends on a
+-- particular join tree.
+create table sk (a int, b int);
+create index on sk(a);
+set join_collapse_limit to 1;
+set enable_seqscan to off;
+explain (costs off) select 1 from
+	(sk k1 join sk k2 on k1.a = k2.a)
+	join (sj j1 join sj j2 on j1.a = j2.a) on j1.b = k1.b;
+explain (costs off) select 1 from
+	(sk k1 join sk k2 on k1.a = k2.a)
+	join (sj j1 join sj j2 on j1.a = j2.a) on j2.b = k1.b;
+reset join_collapse_limit;
+reset enable_seqscan;
+
+-- Check that clauses from the join filter list is not lost on the self-join removal
+CREATE TABLE emp1 (id SERIAL PRIMARY KEY NOT NULL, code int);
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT * FROM emp1 e1, emp1 e2 WHERE e1.id = e2.id AND e2.code <> e1.code;
+
+-- Shuffle self-joined relations. Only in the case of iterative deletion
+-- attempts explains of these queries will be identical.
+CREATE UNIQUE INDEX ON emp1((id*id));
+
+EXPLAIN (COSTS OFF)
+SELECT count(*) FROM emp1 c1, emp1 c2, emp1 c3
+WHERE c1.id=c2.id AND c1.id*c2.id=c3.id*c3.id;
+
+EXPLAIN (COSTS OFF)
+SELECT count(*) FROM emp1 c1, emp1 c2, emp1 c3
+WHERE c1.id=c3.id AND c1.id*c3.id=c2.id*c2.id;
+
+EXPLAIN (COSTS OFF)
+SELECT count(*) FROM emp1 c1, emp1 c2, emp1 c3
+WHERE c3.id=c2.id AND c3.id*c2.id=c1.id*c1.id;
+
+-- Check the usage of a parse tree by the set operations (bug #18170)
+EXPLAIN (COSTS OFF)
+SELECT c1.code FROM emp1 c1 LEFT JOIN emp1 c2 ON c1.id = c2.id
+WHERE c2.id IS NOT NULL
+EXCEPT ALL
+SELECT c3.code FROM emp1 c3;
+
+-- Check that SJE removes references from PHVs correctly
+explain (costs off)
+select * from emp1 t1 left join
+    (select coalesce(t3.code, 1) from emp1 t2
+        left join (emp1 t3 join emp1 t4 on t3.id = t4.id)
+        on true)
+on true;
+
+-- Check that SJE removes the whole PHVs correctly
+explain (verbose, costs off)
+select 1 from emp1 t1 left join
+    ((select 1 as x, * from emp1 t2) s1 inner join
+        (select * from emp1 t3) s2 on s1.id = s2.id)
+    on true
+where s1.x = 1;
+
+-- Check that PHVs do not impose any constraints on removing self joins
+explain (verbose, costs off)
+select * from emp1 t1 join emp1 t2 on t1.id = t2.id left join
+    lateral (select t1.id as t1id, * from generate_series(1,1) t3) s on true;
+
+explain (verbose, costs off)
+select * from generate_series(1,10) t1(id) left join
+    lateral (select t1.id as t1id, t2.id from emp1 t2 join emp1 t3 on t2.id = t3.id)
+on true;
+
+-- Check that SJE replaces join clauses involving the removed rel correctly
+explain (costs off)
+select * from emp1 t1
+   inner join emp1 t2 on t1.id = t2.id
+    left join emp1 t3 on t1.id > 1 and t1.id < 2;
+
+-- Check that SJE doesn't replace the target relation
+EXPLAIN (COSTS OFF)
+WITH t1 AS (SELECT * FROM emp1)
+UPDATE emp1 SET code = t1.code + 1 FROM t1
+WHERE t1.id = emp1.id RETURNING emp1.id, emp1.code, t1.code;
+
+INSERT INTO emp1 VALUES (1, 1), (2, 1);
+
+WITH t1 AS (SELECT * FROM emp1)
+UPDATE emp1 SET code = t1.code + 1 FROM t1
+WHERE t1.id = emp1.id RETURNING emp1.id, emp1.code, t1.code;
+
+TRUNCATE emp1;
+
+EXPLAIN (COSTS OFF)
+UPDATE sj sq SET b = 1 FROM sj as sz WHERE sq.a = sz.a;
+
+CREATE RULE sj_del_rule AS ON DELETE TO sj
+  DO INSTEAD
+    UPDATE sj SET a = 1 WHERE a = old.a;
+EXPLAIN (COSTS OFF) DELETE FROM sj;
+DROP RULE sj_del_rule ON sj CASCADE;
+
+-- Check that SJE does not mistakenly omit qual clauses (bug #18187)
+insert into emp1 values (1, 1);
+explain (costs off)
+select 1 from emp1 full join
+    (select * from emp1 t1 join
+        emp1 t2 join emp1 t3 on t2.id = t3.id
+        on true
+    where false) s on true
+where false;
+select 1 from emp1 full join
+    (select * from emp1 t1 join
+        emp1 t2 join emp1 t3 on t2.id = t3.id
+        on true
+    where false) s on true
+where false;
+
+-- Check that SJE does not mistakenly re-use knowledge of relation uniqueness
+-- made with different set of quals
+insert into emp1 values (2, 1);
+explain (costs off)
+select * from emp1 t1 where exists (select * from emp1 t2
+                                    where t2.id = t1.code and t2.code > 0);
+select * from emp1 t1 where exists (select * from emp1 t2
+                                    where t2.id = t1.code and t2.code > 0);
+
+-- We can remove the join even if we find the join can't duplicate rows and
+-- the base quals of each side are different.  In the following case we end up
+-- moving quals over to s1 to make it so it can't match any rows.
+create table sl(a int, b int, c int);
+create unique index on sl(a, b);
+vacuum analyze sl;
+
+-- Both sides are unique, but base quals are different
+explain (costs off)
+select * from sl t1, sl t2 where t1.a = t2.a and t1.b = 1 and t2.b = 2;
+
+-- Check NullTest in baserestrictinfo list
+explain (costs off)
+select * from sl t1, sl t2
+where t1.a = t2.a and t1.b = 1 and t2.b = 2
+  and t1.c IS NOT NULL and t2.c IS NOT NULL
+  and t2.b IS NOT NULL and t1.b IS NOT NULL
+  and t1.a IS NOT NULL and t2.a IS NOT NULL;
+explain (verbose, costs off)
+select * from sl t1, sl t2
+where t1.b = t2.b and t2.a = 3 and t1.a = 3
+  and t1.c IS NOT NULL and t2.c IS NOT NULL
+  and t2.b IS NOT NULL and t1.b IS NOT NULL
+  and t1.a IS NOT NULL and t2.a IS NOT NULL;
+
+-- Join qual isn't mergejoinable, but inner is unique.
+EXPLAIN (COSTS OFF)
+SELECT n2.a FROM sj n1, sj n2 WHERE n1.a <> n2.a AND n2.a = 1;
+
+EXPLAIN (COSTS OFF)
+SELECT * FROM
+(SELECT n2.a FROM sj n1, sj n2 WHERE n1.a <> n2.a) q0, sl
+WHERE q0.a = 1;
+
+-- Check optimization disabling if it will violate special join conditions.
+-- Two identical joined relations satisfies self join removal conditions but
+-- stay in different special join infos.
+CREATE TABLE sj_t1 (id serial, a int);
+CREATE TABLE sj_t2 (id serial, a int);
+CREATE TABLE sj_t3 (id serial, a int);
+CREATE TABLE sj_t4 (id serial, a int);
+
+CREATE UNIQUE INDEX ON sj_t3 USING btree (a,id);
+CREATE UNIQUE INDEX ON sj_t2 USING btree (id);
+
+EXPLAIN (COSTS OFF)
+SELECT * FROM sj_t1
+JOIN (
+	SELECT sj_t2.id AS id FROM sj_t2
+	WHERE EXISTS
+		(
+		SELECT TRUE FROM sj_t3,sj_t4 WHERE sj_t3.a = 1 AND sj_t3.id = sj_t2.id
+		)
+	) t2t3t4
+ON sj_t1.id = t2t3t4.id
+JOIN (
+	SELECT sj_t2.id AS id FROM sj_t2
+	WHERE EXISTS
+		(
+		SELECT TRUE FROM sj_t3,sj_t4 WHERE sj_t3.a = 1 AND sj_t3.id = sj_t2.id
+		)
+	) _t2t3t4
+ON sj_t1.id = _t2t3t4.id;
+
+--
+-- Test RowMarks-related code
+--
+
+-- Both sides have explicit LockRows marks
+EXPLAIN (COSTS OFF)
+SELECT a1.a FROM sj a1,sj a2 WHERE (a1.a=a2.a) FOR UPDATE;
+
+reset enable_hashjoin;
+reset enable_mergejoin;
+
 --
 -- Test hints given on incorrect column references are useful
 --
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index b6c170ac249..bce4214503d 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2593,6 +2593,7 @@ SeenRelsEntry
 SelectLimit
 SelectStmt
 Selectivity
+SelfJoinCandidate
 SemTPadded
 SemiAntiJoinFactors
 SeqScan
@@ -4056,6 +4057,7 @@ unicodeStyleColumnFormat
 unicodeStyleFormat
 unicodeStyleRowFormat
 unicode_linestyle
+UniqueRelInfo
 unit_conversion
 unlogged_relation_entry
 utf_local_conversion_func
-- 
2.34.1

From 40ab9437e0efa6b35005c497d53dcc219acb7038 Mon Sep 17 00:00:00 2001
From: Alena Rybakina <[email-masked]>
Date: Mon, 17 Feb 2025 17:13:55 +0300
Subject: [PATCH 2/4] Machinery for grabbing an extended vacuum statistics on 
 table relations.

 src/backend/access/heap/vacuumlazy.c          | 149 +++++++++++-
 22 files changed, 1094 insertions(+), 16 deletions(-)

diff --git a/src/backend/access/heap/vacuumlazy.c b/src/backend/access/heap/vacuumlazy.c
index 911f68b413d..839bd41be62 100644
--- a/src/backend/access/heap/vacuumlazy.c
+++ b/src/backend/access/heap/vacuumlazy.c
@@ -290,6 +290,7 @@ typedef struct LVRelState
 	/* Error reporting state */
 	char	   *dbname;
 	char	   *relnamespace;
+	Oid			reloid;
 	char	   *relname;
 	char	   *indname;		/* Current index name */
 	BlockNumber blkno;			/* used only for heap operations */
@@ -408,6 +409,8 @@ typedef struct LVRelState
 	 * been permanently disabled.
 	 */
 	BlockNumber eager_scan_remaining_fails;
+
+	int32		wraparound_failsafe_count; /* the number of times to prevent workaround problem */
 } LVRelState;

@@ -419,6 +422,18 @@ typedef struct LVSavedErrInfo
 	VacErrPhase phase;
 } LVSavedErrInfo;
 
+/*
 
@@ -475,7 +490,106 @@ static void update_vacuum_error_info(LVRelState *vacrel,
 
+/* ----------

 /*
  * Helper to set up the eager scanning state for vacuuming a single relation.
@@ -631,7 +745,14 @@ heap_vacuum_rel(Relation rel, VacuumParams *params,
 
 	verbose = (params->options & VACOPT_VERBOSE) != 0;
 	instrument = (verbose || (AmAutoVacuumWorkerProcess() &&
@@ -651,7 +772,7 @@ heap_vacuum_rel(Relation rel, VacuumParams *params,
 
@@ -668,6 +789,7 @@ heap_vacuum_rel(Relation rel, VacuumParams *params,
@@ -756,6 +878,7 @@ heap_vacuum_rel(Relation rel, VacuumParams *params,
 	vacrel->rel_pages = orig_rel_pages = RelationGetNumberOfBlocks(rel);
 
 	/*
@@ -914,6 +1037,26 @@ heap_vacuum_rel(Relation rel, VacuumParams *params,
 						vacrel->NewRelfrozenXid, vacrel->NewRelminMxid,
 						&frozenxid_updated, &minmulti_updated, false);
 
@@ -929,7 +1072,8 @@ heap_vacuum_rel(Relation rel, VacuumParams *params,
 
 	if (instrument)
@@ -2944,6 +3088,7 @@ lazy_check_wraparound_failsafe(LVRelState *vacrel)
 		int64		progress_val[2] = {0, 0};

 		/*
 
 	if (map[mapByte] & mask)
 	{
+		/*
 
 		MarkBufferDirty(vmbuf);
index eff0990957e..7526c8973ee 100644
@@ -1394,3 +1396,51 @@ CREATE VIEW pg_stat_subscription_stats AS
 
index 0239d9bae65..aea2cf307da 100644
@@ -114,6 +114,9 @@ pg_atomic_uint32 *VacuumSharedCostBalance = NULL;
 
@@ -2497,6 +2500,7 @@ vacuum_delay_point(bool is_analyze)
 			exit(1);
 
 		VacuumCostBalance = 0;
 
 		/*
index 2b9d548cdeb..7924c526cb0 100644

 
 
 /* ----------
 
-
 /*
 
-

 

 static void

 /*

 

+/*
 
 /*
index cce73314609..6aa98f99317 100644
@@ -1499,6 +1499,15 @@ struct config_bool ConfigureNamesBool[] =
index d472987ed46..248ef6c9a36 100644
@@ -640,6 +640,7 @@

 # - Monitoring -
index 9e803d610d7..3cc2eff3437 100644
@@ -12464,4 +12464,22 @@
 
+{ oid => '8001',
index 1571a66c6bf..a5fe622d932 100644
@@ -326,6 +326,7 @@ extern PGDLLIMPORT double vacuum_max_eager_freeze_failure_rate;
 
index 53f2a8458e6..d369284101a 100644
 
+/* ----------
+ *
+ * ExtVacReport
+ *

 /* ----------

 

 typedef struct PgStat_WalStats
@@ -656,7 +720,7 @@ extern void pgstat_unlink_relation(Relation rel);
 
@@ -707,6 +771,17 @@ extern void pgstat_report_analyze(Relation rel,
 
@@ -795,6 +870,7 @@ extern PgStat_WalStats *pgstat_fetch_stat_wal(void);

 /*

 /*----------
index 5baba8d39ff..7cc94fbf275 100644
@@ -2200,7 +2202,9 @@ pg_stat_sys_tables| SELECT relid,
@@ -2252,9 +2256,43 @@ pg_stat_user_tables| SELECT relid,
index e63ee2cf2bb..6493e9b4681 100644

From eed2ae2928f4082e9678e94d0adb4aa1386f9e66 Mon Sep 17 00:00:00 2001
From: Alena Rybakina <[email-masked]>
Date: Mon, 17 Feb 2025 17:28:55 +0300
Subject: [PATCH 3/4] Machinery for grabbing an extended vacuum statistics on 
 databases.

 src/backend/access/heap/vacuumlazy.c          | 290 ++++++++++++++----
 15 files changed, 871 insertions(+), 105 deletions(-)

diff --git a/src/backend/access/heap/vacuumlazy.c b/src/backend/access/heap/vacuumlazy.c
index 839bd41be62..a0c68ad2c8d 100644
--- a/src/backend/access/heap/vacuumlazy.c
+++ b/src/backend/access/heap/vacuumlazy.c
@@ -411,6 +411,8 @@ typedef struct LVRelState
 	BlockNumber eager_scan_remaining_fails;

@@ -422,19 +424,6 @@ typedef struct LVSavedErrInfo
 	VacErrPhase phase;
 } LVSavedErrInfo;
 
-/*
 static void heap_vacuum_eager_scan_setup(LVRelState *vacrel,
@@ -556,27 +545,25 @@ extvac_stats_end(Relation rel, LVExtStatCounters *counters,

 

 	if (!rel->pgstat_info || !pgstat_track_counts)
 		/*
@@ -585,12 +572,97 @@ extvac_stats_end(Relation rel, LVExtStatCounters *counters,
 		 */
 		return;
 
-	report->blks_fetched =
 
+void
  * Helper to set up the eager scanning state for vacuuming a single relation.
  * Initializes the eager scan management related members of the LVRelState.
@@ -746,13 +818,7 @@ heap_vacuum_rel(Relation rel, VacuumParams *params,
 
 	verbose = (params->options & VACOPT_VERBOSE) != 0;
 	instrument = (verbose || (AmAutoVacuumWorkerProcess() &&
@@ -772,7 +838,6 @@ heap_vacuum_rel(Relation rel, VacuumParams *params,
 
@@ -959,6 +1024,8 @@ heap_vacuum_rel(Relation rel, VacuumParams *params,
 	 */
 	lazy_scan_heap(vacrel);
 
@@ -1037,26 +1104,6 @@ heap_vacuum_rel(Relation rel, VacuumParams *params,
 
@@ -1066,14 +1113,37 @@ heap_vacuum_rel(Relation rel, VacuumParams *params,
 
 	if (instrument)
@@ -1346,6 +1416,7 @@ lazy_scan_heap(LVRelState *vacrel)
 		PROGRESS_VACUUM_MAX_DEAD_TUPLE_BYTES
 	};
 	int64		initprog_val[3];
+	LVExtStatCounters extVacCounters;
 
 	/* Report that we're scanning the heap, advertising total # of blocks */
 	initprog_val[0] = PROGRESS_VACUUM_PHASE_SCAN_HEAP;
@@ -1369,6 +1440,13 @@ lazy_scan_heap(LVRelState *vacrel)
 										vacrel,
 										sizeof(uint8));
 
+	/*
 	while (true)
 	{
 		Buffer		buf;
@@ -1415,8 +1493,25 @@ lazy_scan_heap(LVRelState *vacrel)

+			/*
+			 * After completion lazy vacuum, we start again tracking vacuum statistics for
+			 * heap-related objects like FSM, VM, provide heap prunning.
+			 * It seems dangerously that we have start tracking but there are no end, but
+			 * it is safe. The end tracking is located before lazy vacuum stage in the same
+			 * loop or after it.
+ 			*/
+			extvac_stats_start(vacrel->rel, &extVacCounters);
+
 			/*
 			 * Vacuum the Free Space Map to make newly-freed space visible on
 			 * upper-level FSM pages. Note that blkno is the previously
@@ -1639,6 +1734,13 @@ lazy_scan_heap(LVRelState *vacrel)
 
 	read_stream_end(stream);
 
+	/*
@@ -1646,6 +1748,12 @@ lazy_scan_heap(LVRelState *vacrel)
 
+	/*
+	 * We need to take into account heap vacuum statistics during process of
+	 * FSM.
+ 	 */
+	extvac_stats_start(vacrel->rel, &extVacCounters);
+
 	/*
 	 * Vacuum the remainder of the Free Space Map.  We must do this whether or
 	 * not there were indexes, and whether or not we bypassed index vacuuming.
@@ -1658,6 +1766,10 @@ lazy_scan_heap(LVRelState *vacrel)
 	/* report all blocks vacuumed */
 	pgstat_progress_update_param(PROGRESS_VACUUM_HEAP_BLKS_VACUUMED, rel_pages);
 
@@ -2575,6 +2687,7 @@ static void
 
 	/* Should not end up here with no indexes */
 	Assert(vacrel->nindexes > 0);
@@ -2587,6 +2700,9 @@ lazy_vacuum(LVRelState *vacrel)
 		return;
 	}
 
@@ -2643,6 +2759,14 @@ lazy_vacuum(LVRelState *vacrel)
 
+	/*
@@ -2659,11 +2783,21 @@ lazy_vacuum(LVRelState *vacrel)
@@ -3200,6 +3334,11 @@ lazy_vacuum_one_index(Relation indrel, IndexBulkDeleteResult *istat,
 
 	ivinfo.index = indrel;
 	ivinfo.heaprel = vacrel->rel;
@@ -3226,6 +3365,15 @@ lazy_vacuum_one_index(Relation indrel, IndexBulkDeleteResult *istat,
 	istat = vac_bulkdel_one_index(&ivinfo, istat, vacrel->dead_items,
 								  vacrel->dead_items_info);
 
@@ -3250,6 +3398,11 @@ lazy_cleanup_one_index(Relation indrel, IndexBulkDeleteResult *istat,
 
 	ivinfo.index = indrel;
 	ivinfo.heaprel = vacrel->rel;
@@ -3275,6 +3428,15 @@ lazy_cleanup_one_index(Relation indrel, IndexBulkDeleteResult *istat,

index 7526c8973ee..d8c7c179094 100644
@@ -1444,3 +1444,35 @@ FROM pg_class rel
index 7924c526cb0..000388a565f 100644
 
 	/*

 

 

 
 
index 6aa98f99317..43edb9169d2 100644
@@ -1501,7 +1501,7 @@ struct config_bool ConfigureNamesBool[] =
index 3cc2eff3437..c6805c79ca1 100644
@@ -12482,4 +12482,13 @@
index a5fe622d932..95a60534c12 100644
 
 /*
  * Flags for amparallelvacuumoptions to control the participation of bulkdelete
@@ -295,6 +296,26 @@ typedef struct VacDeadItemsInfo
 	int64		num_items;		/* current # of entries */
 } VacDeadItemsInfo;
 
+/*
@@ -406,4 +427,8 @@ extern double anl_random_fract(void);
 
index d369284101a..115cb736300 100644

 
 /* ----------

 step s1_commit: COMMIT;

index 7cc94fbf275..709e1d6a22b 100644
@@ -2261,6 +2261,28 @@ pg_stat_user_tables| SELECT relid,
index 6493e9b4681..bdb73ccab6b 100644

From ac4783ee2451a57c9127eecdb3186e4dcf09b074 Mon Sep 17 00:00:00 2001
From: Alena Rybakina <[email-masked]>
Date: Mon, 17 Feb 2025 17:29:48 +0300
Subject: [PATCH 4/4] Add documentation about the system views that are used in
 the machinery of vacuum statistics.

---

diff --git a/doc/src/sgml/system-views.sgml b/doc/src/sgml/system-views.sgml
index be81c2b51d2..b05cb04a235 100644
--- a/doc/src/sgml/system-views.sgml
+++ b/doc/src/sgml/system-views.sgml
@@ -5069,4 +5069,759 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx
   </table>
  </sect1>

---

From: Alena Rybakina <[email-masked]>
Date: Thu, 27 Feb 2025 21:00:29 +0300

Hi!
On 17.02.2025 17:46, Alena Rybakina wrote:
>>>> src/backend/utils/misc/postgresql.conf.sample. I added it, it 
>>>> should help.
> The patch needed a rebase again. There is nothing new since version 
> 18, only a rebase.

The patch needed a new rebase.

Sorry, but due to feeling unwell I picked up a patch from another thread 
and squashed the patch for vacuum statistics for indexes and heaps in 
the previous version. Now I fixed everything together with the rebase.

-- 
Regards,
Alena Rybakina
Postgres Professional
From 4a87db57b73e47f41056c0e59cef5d5862c56e99 Mon Sep 17 00:00:00 2001
From: Alena Rybakina <[email-masked]>
Date: Thu, 27 Feb 2025 20:01:38 +0300
Subject: [PATCH 1/4] Machinery for grabbing an extended vacuum statistics on 
 table relations.

index 1af18a78a2b..fbe7cc25458 100644
+	int32		wraparound_failsafe_count; /* number of emergency vacuums to prevent anti-wraparound shutdown */
 } LVRelState;

+/*
 
@@ -475,6 +490,106 @@ static void update_vacuum_error_info(LVRelState *vacrel,
 
+/* ----------

 /*
@@ -631,7 +746,14 @@ heap_vacuum_rel(Relation rel, VacuumParams *params,
 
 	verbose = (params->options & VACOPT_VERBOSE) != 0;
 	instrument = (verbose || (AmAutoVacuumWorkerProcess() &&
@@ -651,7 +773,7 @@ heap_vacuum_rel(Relation rel, VacuumParams *params,
 
@@ -668,6 +790,7 @@ heap_vacuum_rel(Relation rel, VacuumParams *params,
@@ -757,6 +880,7 @@ heap_vacuum_rel(Relation rel, VacuumParams *params,
 
 	/*
 	 * Get cutoffs that determine which deleted tuples are considered DEAD,
@@ -914,6 +1038,26 @@ heap_vacuum_rel(Relation rel, VacuumParams *params,
 						vacrel->NewRelfrozenXid, vacrel->NewRelminMxid,
 						&frozenxid_updated, &minmulti_updated, false);
 
@@ -929,7 +1073,8 @@ heap_vacuum_rel(Relation rel, VacuumParams *params,
 
 	if (instrument)
@@ -2944,6 +3089,7 @@ lazy_check_wraparound_failsafe(LVRelState *vacrel)
 		int64		progress_val[2] = {0, 0};

 		/*
 
 	if (map[mapByte] & mask)
 	{
+		/*
 
 		MarkBufferDirty(vmbuf);
index a4d2cfdcaf5..2e42f6d7108 100644
@@ -1390,3 +1392,51 @@ CREATE VIEW pg_stat_subscription_stats AS

 
 		VacuumCostBalance = 0;
 
 		/*

 
 
 /* ----------
 
-
 /*
 
-

 

 static void

 /*

index efb6d0032af..81332bc0ba2 100644
 
@@ -2248,3 +2254,144 @@ pg_stat_have_stats(PG_FUNCTION_ARGS)

+/*
 
 /*
index ad25cbb39c5..7bed539e6d6 100644
@@ -1501,6 +1501,15 @@ struct config_bool ConfigureNamesBool[] =
index 5362ff80519..5faa8d4f995 100644

 # - Monitoring -
index cd9422d0bac..29dcd8bac55 100644
@@ -12450,4 +12450,22 @@
 
+{ oid => '8001',
 
index 67656264b62..84e61a27b17 100644
 
+/* ----------
+ *
+ * ExtVacReport
+ *
+	int32		wraparound_failsafe_count;	/* number of emergency vacuums to prevent anti-wraparound shutdown */

 /* ----------
 
-#define PGSTAT_FILE_FORMAT_ID	0x01A5BCB5
+#define PGSTAT_FILE_FORMAT_ID	0x01A5BCB6

 
 
 /* ------
@@ -654,7 +718,7 @@ extern void pgstat_unlink_relation(Relation rel);
 
@@ -705,6 +769,17 @@ extern void pgstat_report_analyze(Relation rel,
 
@@ -793,6 +868,7 @@ extern PgStat_WalStats *pgstat_fetch_stat_wal(void);

 /*
index a0244bff1fc..512890896fe 100644

 /*----------
index 62f69ac20b2..38f93294fbf 100644
index 37b6d21e1f9..fd3bbf85418 100644

From 1f43ece50d3e0b3404365129a3010601bf3676eb Mon Sep 17 00:00:00 2001
From: Alena Rybakina <[email-masked]>
Date: Thu, 27 Feb 2025 20:42:05 +0300
Subject: [PATCH 2/4] Machinery for grabbing an extended vacuum statistics on 
 index relations.

diff --git a/src/backend/access/heap/vacuumlazy.c b/src/backend/access/heap/vacuumlazy.c
index fbe7cc25458..3941f5e247f 100644
--- a/src/backend/access/heap/vacuumlazy.c
+++ b/src/backend/access/heap/vacuumlazy.c
@@ -291,6 +291,7 @@ typedef struct LVRelState
 	char	   *dbname;
 	char	   *relnamespace;
 	Oid			reloid;
+	Oid			indoid;
 	char	   *relname;
 	char	   *indname;		/* Current index name */
 	BlockNumber blkno;			/* used only for heap operations */
@@ -411,6 +412,8 @@ typedef struct LVRelState
 	BlockNumber eager_scan_remaining_fails;
 
 	int32		wraparound_failsafe_count; /* number of emergency vacuums to prevent anti-wraparound shutdown */
+
+	ExtVacReport extVacReport;
 } LVRelState;

@@ -422,19 +425,6 @@ typedef struct LVSavedErrInfo
 	VacErrPhase phase;
 } LVSavedErrInfo;
 
-/*
@@ -556,27 +546,25 @@ extvac_stats_end(Relation rel, LVExtStatCounters *counters,

 

 	if (!rel->pgstat_info || !pgstat_track_counts)
 		/*
@@ -585,12 +573,96 @@ extvac_stats_end(Relation rel, LVExtStatCounters *counters,
 		 */
 		return;
 
-	report->blks_fetched =
 
+void
 
 /*
@@ -747,13 +819,7 @@ heap_vacuum_rel(Relation rel, VacuumParams *params,
 
 	verbose = (params->options & VACOPT_VERBOSE) != 0;
 	instrument = (verbose || (AmAutoVacuumWorkerProcess() &&
@@ -773,7 +839,6 @@ heap_vacuum_rel(Relation rel, VacuumParams *params,
 
@@ -960,6 +1025,8 @@ heap_vacuum_rel(Relation rel, VacuumParams *params,
 	 */
 	lazy_scan_heap(vacrel);
 
@@ -1038,26 +1105,6 @@ heap_vacuum_rel(Relation rel, VacuumParams *params,
 
@@ -1067,14 +1114,37 @@ heap_vacuum_rel(Relation rel, VacuumParams *params,
 
 	if (instrument)
 
 	/* Report that we're scanning the heap, advertising total # of blocks */
 	initprog_val[0] = PROGRESS_VACUUM_PHASE_SCAN_HEAP;
@@ -1360,6 +1431,13 @@ lazy_scan_heap(LVRelState *vacrel)
 	vacrel->next_unskippable_eager_scanned = false;
 	vacrel->next_unskippable_vmbuffer = InvalidBuffer;
 
+	/*
+	 * Due to the fact that vacuum heap processing needs their index vacuuming
+	 * we need to track them separately and accumulate heap vacuum statistics
+	 * separately. So last processes are related to only heap vacuuming process.
+	 */
+	extvac_stats_start(vacrel->rel, &extVacCounters);
+
 	/* Set up the read stream for vacuum's first pass through the heap */
 	stream = read_stream_begin_relation(READ_STREAM_MAINTENANCE,
 										vacrel->bstrategy,
@@ -1416,8 +1494,26 @@ lazy_scan_heap(LVRelState *vacrel)

+			/*
@@ -1640,6 +1736,12 @@ lazy_scan_heap(LVRelState *vacrel)
 
 	read_stream_end(stream);
 
+	/*
@@ -1647,6 +1749,12 @@ lazy_scan_heap(LVRelState *vacrel)
 
+	/*
@@ -1659,6 +1767,10 @@ lazy_scan_heap(LVRelState *vacrel)
 
@@ -2576,6 +2688,7 @@ static void
 
 	/* Should not end up here with no indexes */
 	Assert(vacrel->nindexes > 0);
@@ -2588,6 +2701,9 @@ lazy_vacuum(LVRelState *vacrel)
 		return;
 	}
 
@@ -2644,6 +2760,14 @@ lazy_vacuum(LVRelState *vacrel)
 
+	/*
@@ -2660,11 +2784,21 @@ lazy_vacuum(LVRelState *vacrel)
@@ -3201,6 +3335,11 @@ lazy_vacuum_one_index(Relation indrel, IndexBulkDeleteResult *istat,
 
 	ivinfo.index = indrel;
@@ -3219,6 +3358,7 @@ lazy_vacuum_one_index(Relation indrel, IndexBulkDeleteResult *istat,
@@ -3227,6 +3367,15 @@ lazy_vacuum_one_index(Relation indrel, IndexBulkDeleteResult *istat,
 
@@ -3251,6 +3400,11 @@ lazy_cleanup_one_index(Relation indrel, IndexBulkDeleteResult *istat,
 
 	ivinfo.index = indrel;
@@ -3270,12 +3424,22 @@ lazy_cleanup_one_index(Relation indrel, IndexBulkDeleteResult *istat,

index 2e42f6d7108..7217473c37b 100644
@@ -1440,3 +1440,35 @@ FROM pg_class rel
 
 	/*

 

 
index 81332bc0ba2..d4d804cc95b 100644
@@ -2362,18 +2362,19 @@ pg_stat_get_vacuum_tables(PG_FUNCTION_ARGS)

 	values[i++] = Int64GetDatum(extvacuum->wal_records);
 	values[i++] = Int64GetDatum(extvacuum->wal_fpi);
@@ -2392,6 +2393,116 @@ pg_stat_get_vacuum_tables(PG_FUNCTION_ARGS)

index 7bed539e6d6..4bdd6399bdf 100644
@@ -1503,7 +1503,7 @@ struct config_bool ConfigureNamesBool[] =
index 29dcd8bac55..e1bd5b809ae 100644
@@ -12468,4 +12468,13 @@
 
 /*
 
+/*
 
index 84e61a27b17..347d868766e 100644

-	int32		wraparound_failsafe_count;	/* number of emergency vacuums to prevent anti-wraparound shutdown */
+			int32		wraparound_failsafe_count;	/* number of emergency vacuums to prevent anti-wraparound shutdown */
 
 /* ----------

 step s1_commit: COMMIT;

index 38f93294fbf..b7e351616a5 100644
index fd3bbf85418..56331aa837e 100644

From e7614312950a658a7d8d73ac4a1e527b8e4546ce Mon Sep 17 00:00:00 2001
From: Alena Rybakina <[email-masked]>
Date: Tue, 4 Feb 2025 17:57:44 +0300
Subject: [PATCH 3/4] Machinery for grabbing an extended vacuum statistics on
 databases.

index 3941f5e247f..e6be44cc5c9 100644
@@ -660,7 +660,7 @@ accumulate_heap_vacuum_statistics(LVExtStatCounters *extVacCounters, LVRelState

@@ -4052,6 +4052,9 @@ vacuum_error_callback(void *arg)
 	switch (errinfo->phase)
 	{
 		case VACUUM_ERRCB_PHASE_SCAN_HEAP:
+			if(geterrelevel() == ERROR)
+					pgstat_report_vacuum_error(errinfo->reloid, PGSTAT_EXTVAC_TABLE);
+
 			if (BlockNumberIsValid(errinfo->blkno))
 			{
 				if (OffsetNumberIsValid(errinfo->offnum))
@@ -4067,6 +4070,9 @@ vacuum_error_callback(void *arg)
 			break;
 
@@ -4082,16 +4088,25 @@ vacuum_error_callback(void *arg)
 			break;

 
index 7217473c37b..79a5f1b5742 100644
@@ -1471,4 +1471,29 @@ FROM

 /* ----------

 	return true;
 }
 
+/* ---------
 
 /*
 
index d4d804cc95b..04929c8c090 100644
@@ -2373,7 +2373,7 @@ pg_stat_get_vacuum_tables(PG_FUNCTION_ARGS)

 	values[i++] = Int64GetDatum(extvacuum->wal_records);
@@ -2503,6 +2503,104 @@ pg_stat_get_vacuum_indexes(PG_FUNCTION_ARGS)

index 4bdd6399bdf..d2cd09fd412 100644
@@ -1507,7 +1507,7 @@ struct config_bool ConfigureNamesBool[] =
index e1bd5b809ae..4d55a4beb53 100644
@@ -12469,12 +12469,21 @@
index 347d868766e..feb17b70f8e 100644

+	int32		errors;
 
 	/* ----------
-			int32		wraparound_failsafe_count;	/* number of emergency vacuums to prevent anti-wraparound shutdown */
@@ -756,6 +758,7 @@ extern void pgstat_report_vacuum(Oid tableoid, bool shared,
 
 /*
 
 session s1
+setup		{
 
 session s2
+setup		{
index b7e351616a5..956db8471eb 100644
@@ -2261,6 +2261,23 @@ pg_stat_user_tables| SELECT relid,

 DROP TABLE vestat;

index 56331aa837e..d228b0c5315 100644

 

 DROP TABLE vestat;

 

 

From f8e4a0885491d6f8eddf94a890fe21784a8e8d34 Mon Sep 17 00:00:00 2001

---

diff --git a/doc/src/sgml/system-views.sgml b/doc/src/sgml/system-views.sgml
index 3f5a306247e..4ad130c6f34 100644
--- a/doc/src/sgml/system-views.sgml
+++ b/doc/src/sgml/system-views.sgml
@@ -5074,4 +5074,759 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx
   </table>
  </sect1>

---

From: Ilia Evdokimov <[email-masked]>
Date: Mon, 10 Mar 2025 12:13:08 +0300

Hi,

After commit eaf5027 we should add information about wal_buffers_full.

Any thoughts?

--
Best regards,
Ilia Evdokimov,
Tantor Labs LLC.

---

From: Kirill Reshke <[email-masked]>
Date: Mon, 10 Mar 2025 18:33:15 +0500

> the previous version. Now I fixed everything together with the rebase.

Hi!
CI fails on this one[0]

Is it a flap or a real problem caused by v20?

```

 SELECT relpages AS irp
diff -U3 /tmp/cirrus-ci-build/src/test/regress/expected/vacuum_tables_and_db_statistics.out
/tmp/cirrus-ci-build/build-32/testrun/recovery/027_stream_regress/data/results/vacuum_tables_and_db_statistics.out
--- /tmp/cirrus-ci-build/src/test/regress/expected/vacuum_tables_and_db_statistics.out
2025-03-10 09:36:10.274799176 +0000
+++ /tmp/cirrus-ci-build/build-32/testrun/recovery/027_stream_regress/data/results/vacuum_tables_and_db_statistics.out
2025-03-10 09:49:35.796596462 +0000
@@ -65,7 +65,7 @@
 WHERE vt.relname = 'vestat' AND vt.relid = c.oid;
  relname | vm_new_frozen_pages | tuples_deleted | relpages |
pages_scanned | pages_removed
 ---------+---------------------+----------------+----------+---------------+---------------
- vestat  |                   0 |              0 |      455 |
   0 |             0
+ vestat  |                   0 |              0 |      417 |
   0 |             0
 (1 row)

 SELECT relpages AS rp
=== EOF ===

```

[0] https://api.cirrus-ci.com/v1/artifact/task/5336493629112320/testrun/build-32/testrun/recovery/027_stream_regress/log/regress_log_027_stream_regress

-- 
Best regards,
Kirill Reshke

---

From: Alena Rybakina <[email-masked]>
Date: Wed, 12 Mar 2025 22:36:39 +0300

Hi!

On 10.03.2025 16:33, Kirill Reshke wrote:
> On Thu, 27 Feb 2025 at 23:00, Alena Rybakina<[email-masked]>  wrote:
>> Hi!
> [0]https://api.cirrus-ci.com/v1/artifact/task/5336493629112320/testrun/build-32/testrun/recovery/027_stream_regress/log/regress_log_027_stream_regress
Thank you for your help, I'll fix it soon.

-- 
Regards,
Alena Rybakina
Postgres Professional

---

From: Alena Rybakina <[email-masked]>
Date: Wed, 12 Mar 2025 22:41:33 +0300

Hi!

> After commit eaf5027 we should add information about wal_buffers_full.
I think I can add it. To be honest, I haven't gotten to know this 
statistic yet, haven't had time to get to know this commit yet. How will 
this statistic help us analyze the work of the vacuum for relations?

-- 
Regards,
Alena Rybakina
Postgres Professional

---

From: Jim Nasby <[email-masked]>
Date: Wed, 12 Mar 2025 17:15:53 -0500

> this statistic help us analyze the work of the vacuum for relations?

The usecase I can see here is that we don't want autovac creating so much
WAL traffic that it starts forcing other backends to have to write WAL out.
But tracking how many times autovac writes WAL buffers won't help with that
(though we also don't want any WAL buffers written by autovac to be counted
in the system-wide wal_buffers_full: autovac is a BG process and it's fine
if it's spending time writing WAL out). I think the same is true of a
manual vacuum as well.

What would be helpful would be a way to determine if autovac was causing
enough traffic to force other backends to write WAL. Offhand I'm not sure
how practical that actually is though.

BTW, there's also an argument to be made that autovac should throttle
itself if we're close to running out of available WAL buffers...

---

From: Bertrand Drouvot <[email-masked]>
Date: Thu, 13 Mar 2025 06:42:57 +0000

Hi,

> But tracking how many times autovac writes WAL buffers won't help with that

Right, because the one that increments the wal_buffers_full metric could "just"
be a victim (i.e the one that happens to trigger the WAL buffers disk flush,
even though other backends contributed most of the buffer usage).

> in the system-wide wal_buffers_full:

why? Or do you mean that it would be good to have 2 kinds of metrics: one
generated by "maintenance" activity and one by "regular" backends?

> how practical that actually is though.

a051e71e28a could help to see how much WAL has by written by the autovac workers.

> itself if we're close to running out of available WAL buffers...

hmm, yeah I think that's an interesting idea OTOH that would mean to "delegate"
the WAL buffers flush to another backend.

Regards,

-- 
Bertrand Drouvot
PostgreSQL Contributors Team
RDS Open Source Databases
Amazon Web Services: https://aws.amazon.com

---

From: vignesh C <[email-masked]>
Date: Mon, 17 Mar 2025 12:12:48 +0530

> The patch needed a new rebase.

I noticed that the CI failure reported at [1], Ilia's comment from
[2], changed the status to Waiting on Author, please address them and
update it to Needs review.
[1] - https://www.postgresql.org/message-id/CALdSSPiw_-0_L3YV%3DQn7oopPqY2XVrXwDSGLdSXS69QvMdXisQ%40mail.gmail.com
[2] - https://www.postgresql.org/message-id/47a7b784-5218-43f2-96e3-65f9a729c5a5%40tantorlabs.com

Regards,
Vignesh

---

From: Alena Rybakina <[email-masked]>
Date: Tue, 18 Mar 2025 08:57:40 +0300

On 17.03.2025 09:42, vignesh C wrote:
> On Thu, 27 Feb 2025 at 23:30, Alena Rybakina<[email-masked]>  wrote:
>> Hi!
> [1] -https://www.postgresql.org/message-id/CALdSSPiw_-0_L3YV%3DQn7oopPqY2XVrXwDSGLdSXS69QvMdXisQ%40mail.gmail.com
> [2] -https://www.postgresql.org/message-id/47a7b784-5218-43f2-96e3-65f9a729c5a5%40tantorlabs.com
Okay, thank you!

-- 
Regards,
Alena Rybakina
Postgres Professional

---

From: Alena Rybakina <[email-masked]>
Date: Fri, 21 Mar 2025 22:42:17 +0300

> the WAL buffers flush to another backend.

I will add it and fix the tests but later and I'll explain why.

I'm working on this issue [0] and try have already created new 
statistics in Statistics Collector to store database and relation vacuum 
statistics: PGSTAT_KIND_VACUUM_DB and PGSTAT_KIND_VACUUM_RELATION.

Vacuum statistics are saved there instead of relation's and database's 
statistic structure, but for some reason it is not possible to find them 
in the hash table when building a snapshot and display them accordingly.
I have not yet figured out where the error is.

Without solving this problem, committing vacuum statistics is not yet 
possible. An alternative way for us was to refuse some statistics for 
now for relations,
but we could not agree on which statistics should not be displayed yet 
and for now we are only adding them :).

I understand why this is important to display more vacuum information 
about vacuum statistics - it will allow us to better understand the 
problems of incorrect vacuum settings or, for example, notice a bug in 
its operation.

In order to reduce the memory consumption for storing them for those who 
are not going to use them, I just realized that we need to create a 
separate space for storing the statistics
I mentioned above (PGSTAT_KIND_VACUUM_DB and 
PGSTAT_KIND_VACUUM_RELATION), there is no other way to do this and I am 
still trying to complete this functionality.

I doubt that I will have time for this by code freeze date and even if I 
do, I will hardly have time for a normal review. There's really a lot 
more to learn related to the stat collector, so
I'm postponing it to the next commitfest.

Sorry. I'll fix the tests as soon as I finish this part, since they'll 
most likely either break the same way or in some new way.

Tomorrow or the day after tomorrow I will send a diff patch with what I 
have already managed to demonstrate the problem, since I need to bring 
the code to a normal form.
Maybe someone who worked with the stat collector will suddenly tell me 
where and what I have implemented incorrectly.

-- 
Regards,
Alena Rybakina
Postgres Professional

---

From: Alena Rybakina <[email-masked]>
Date: Fri, 21 Mar 2025 22:46:41 +0300

Sorry, I forgot to provide a link to the problem [0], actually. So I 
provided it below.

[0] 
https://www.postgresql.org/message-id/CAPpHfduoJEuoixPTTg2tjhnXqrdobuMaQGxriqxJ9TjN1uxOuA%40mail.gmail.com

On 21.03.2025 22:42, Alena Rybakina wrote:
> statistics in Statistics Collector to store database and relation 
> vacuum statistics: PGSTAT_KIND_VACUUM_DB and PGSTAT_KIND_VACUUM_RELATION.
>
> statistic structure, but for some reason it is not possible to find 
> them in the hash table when building a snapshot and display them 
> accordingly.
> In order to reduce the memory consumption for storing them for those 
> who are not going to use them, I just realized that we need to create 
> a separate space for storing the statistics
> PGSTAT_KIND_VACUUM_RELATION), there is no other way to do this and I 
> am still trying to complete this functionality.
>
> I doubt that I will have time for this by code freeze date and even if 
> I do, I will hardly have time for a normal review. There's really a 
> lot more to learn related to the stat collector, so
> Tomorrow or the day after tomorrow I will send a diff patch with what 
> I have already managed to demonstrate the problem, since I need to 
> bring the code to a normal form.

-- 
Regards,
Alena Rybakina
Postgres Professional

---

From: Jim Nasby <[email-masked]>
Date: Mon, 24 Mar 2025 18:02:31 -0500

On Fri, Mar 21, 2025 at 2:42 PM Alena Rybakina <[email-masked]>
wrote:

>  See below...

> I still don't think that helps (see below)

> Maybe it does, maybe it doesn't... but now I think you're getting why I'm
complaining about the proposed WAL flush metrics: who *flushes* WAL tells
you absolutely nothing about who generated the WAL. Not only that, but
flushing WAL isn't necessarily even bad: a user backend can't COMMIT
without flushing some amount of WAL (ignoring async-commit of course). That
really casts the whole idea of having stats on who's flushing how much WAL
in a new light: you can NOT use any such metric without a bunch of other
context; including who else was flushing how much WAL, whether WAL had to
absolutely be flushed anyway (ie, at bare minimum a COMMIT must flush
enough WAL to cover the commit record), and even where all the WAL is
coming from in the first place.

Though now that I think about it... if we're reporting how much WAL is
being generated by vacuum, then *maybe* it's helpful to also report how
much WAL is being flushed by vacuum. My emphasis on *maybe* is because it's
fine if autovac is writing more than it flushes, so long as the remainder
is being flushed by the checkpointer and not user backends... but you could
also determine that just by looking at how much WAL backends are flushing.

Basically, I'm leaning towards it would be best to rethink the whole
purpose of reporting WAL flush metrics before we further muddy the waters
by adding vacuum stats about it. At minimum we should have a metric that
shows how much WAL backends flushed because they *had* to due to
synchronous commit settings (which does affect more than just COMMIT).

> I'm working on this issue [0] and try have already created new statistics
> in Statistics Collector to store database and relation vacuum statistics:
> PGSTAT_KIND_VACUUM_DB and PGSTAT_KIND_VACUUM_RELATION.
>
> statistic structure, but for some reason it is not possible to find them in
> the hash table when building a snapshot and display them accordingly.
> possible. An alternative way for us was to refuse some statistics for now
> for relations,
> but we could not agree on which statistics should not be displayed yet and
> for now we are only adding them :).
>
> problems of incorrect vacuum settings or, for example, notice a bug in its
> operation.
>
> I mentioned above (PGSTAT_KIND_VACUUM_DB and PGSTAT_KIND_VACUUM_RELATION),
> there is no other way to do this and I am still trying to complete this
> functionality.
>
> do, I will hardly have time for a normal review. There's really a lot more
> to learn related to the stat collector, so
> have already managed to demonstrate the problem, since I need to bring the
> code to a normal form.

---

From: Alena Rybakina <[email-masked]>
Date: Tue, 25 Mar 2025 09:12:57 +0300

Hi! I rebased the patches again - PGSTAT_FILE_FORMAT_ID needed to be fixed.

> where and what I have implemented incorrectly.
I attached also diff version that contains what I was talking about. The 
test case:

create table t (x int);

insert into t select id from generate_series(1,1000) id;

delete from t where id > 900;

vacuum;

select * from pg_stat_vacuum_tables where relname = 't';

-- 
Regards,
Alena Rybakina
Postgres Professional
From 0c0aee02a36c33e68e7a80d31160f6f83cbb6eb0 Mon Sep 17 00:00:00 2001

---

From a737c545f3d01ab39430deb43a1925cf31cbd863 Mon Sep 17 00:00:00 2001
From: Alena Rybakina <[email-masked]>
Date: Tue, 4 Feb 2025 17:57:44 +0300
Subject: [PATCH 3/4] Machinery for grabbing an extended vacuum statistics on
 databases.

index f5aad61afa6..d9b07837831 100644

@@ -4065,6 +4065,9 @@ vacuum_error_callback(void *arg)
 	switch (errinfo->phase)
 	{
 		case VACUUM_ERRCB_PHASE_SCAN_HEAP:
+			if(geterrelevel() == ERROR)
+					pgstat_report_vacuum_error(errinfo->reloid, PGSTAT_EXTVAC_TABLE);
+
 			if (BlockNumberIsValid(errinfo->blkno))
 			{
 				if (OffsetNumberIsValid(errinfo->offnum))
@@ -4080,6 +4083,9 @@ vacuum_error_callback(void *arg)
 			break;
 
@@ -4095,16 +4101,25 @@ vacuum_error_callback(void *arg)
 			break;

 
index c69c60de49b..a895c8789e8 100644
@@ -1472,4 +1472,29 @@ FROM
index e0552f840a0..09fa0fbee57 100644
@@ -203,7 +203,7 @@ static inline bool pgstat_is_kind_valid(PgStat_Kind kind);

 /* ----------

 	return true;
 }
 
+/* ---------
 
 /*
 
index 15fa3de0871..c2acdcf0e0e 100644
@@ -2383,7 +2383,7 @@ pg_stat_get_vacuum_tables(PG_FUNCTION_ARGS)

 	values[i++] = Int64GetDatum(extvacuum->wal_records);
@@ -2513,6 +2513,104 @@ pg_stat_get_vacuum_indexes(PG_FUNCTION_ARGS)

index 191894aacab..259067a4030 100644
@@ -1502,7 +1502,7 @@ struct config_bool ConfigureNamesBool[] =
index 369806befb6..474e0f86b73 100644
@@ -12498,12 +12498,21 @@
index 30e7e5537f5..66e6e721563 100644
@@ -154,6 +154,9 @@ typedef struct ExtVacReport

+	int32		errors;
 
 	/* ----------
@@ -183,7 +186,6 @@ typedef struct ExtVacReport
@@ -762,6 +764,7 @@ extern void pgstat_report_vacuum(Oid tableoid, bool shared,
 
 /*
 
 session s1
+setup		{
 
 session s2
+setup		{
index 640f799b7c1..c5df58ebd72 100644
@@ -2262,6 +2262,23 @@ pg_stat_user_tables| SELECT relid,

 DROP TABLE vestat;

index 3498ce06635..37fa1c67a49 100644

 

 DROP TABLE vestat;

 

 

From 0abab1585a59bb8ed862022093bb898fbbbb8790 Mon Sep 17 00:00:00 2001
From: Alena Rybakina <[email-masked]>
Date: Thu, 27 Feb 2025 20:42:05 +0300
Subject: [PATCH 2/4] Machinery for grabbing an extended vacuum statistics on
 index relations.

index 592b018c300..f5aad61afa6 100644

 
-/*

 

 
-	report->blks_fetched =
 
+void
 
 /*
@@ -748,13 +820,7 @@ heap_vacuum_rel(Relation rel, VacuumParams *params,
 
 	verbose = (params->options & VACOPT_VERBOSE) != 0;
 	instrument = (verbose || (AmAutoVacuumWorkerProcess() &&
@@ -774,7 +840,6 @@ heap_vacuum_rel(Relation rel, VacuumParams *params,
 
@@ -961,6 +1026,8 @@ heap_vacuum_rel(Relation rel, VacuumParams *params,
 	 */
 	lazy_scan_heap(vacrel);
 
@@ -1048,26 +1115,6 @@ heap_vacuum_rel(Relation rel, VacuumParams *params,
 
@@ -1077,14 +1124,37 @@ heap_vacuum_rel(Relation rel, VacuumParams *params,
 
 	if (instrument)
@@ -1356,6 +1426,7 @@ lazy_scan_heap(LVRelState *vacrel)
 		PROGRESS_VACUUM_MAX_DEAD_TUPLE_BYTES
 	};
 	int64		initprog_val[3];
+	LVExtStatCounters extVacCounters;
 
@@ -1370,6 +1441,13 @@ lazy_scan_heap(LVRelState *vacrel)
 
+	/*
@@ -1429,8 +1507,26 @@ lazy_scan_heap(LVRelState *vacrel)

+			/*
@@ -1653,6 +1749,12 @@ lazy_scan_heap(LVRelState *vacrel)
 
 	read_stream_end(stream);
 
+	/*
@@ -1660,6 +1762,12 @@ lazy_scan_heap(LVRelState *vacrel)
 
+	/*
@@ -1672,6 +1780,10 @@ lazy_scan_heap(LVRelState *vacrel)
 
@@ -2589,6 +2701,7 @@ static void
 
 	/* Should not end up here with no indexes */
 	Assert(vacrel->nindexes > 0);
@@ -2601,6 +2714,9 @@ lazy_vacuum(LVRelState *vacrel)
 		return;
 	}
 
@@ -2657,6 +2773,14 @@ lazy_vacuum(LVRelState *vacrel)
 
+	/*
@@ -2673,11 +2797,21 @@ lazy_vacuum(LVRelState *vacrel)
@@ -3214,6 +3348,11 @@ lazy_vacuum_one_index(Relation indrel, IndexBulkDeleteResult *istat,
 
 	ivinfo.index = indrel;
@@ -3232,6 +3371,7 @@ lazy_vacuum_one_index(Relation indrel, IndexBulkDeleteResult *istat,
@@ -3240,6 +3380,15 @@ lazy_vacuum_one_index(Relation indrel, IndexBulkDeleteResult *istat,
 
@@ -3264,6 +3413,11 @@ lazy_cleanup_one_index(Relation indrel, IndexBulkDeleteResult *istat,
 
 	ivinfo.index = indrel;
@@ -3283,12 +3437,22 @@ lazy_cleanup_one_index(Relation indrel, IndexBulkDeleteResult *istat,

index 603bf97e042..c69c60de49b 100644
@@ -1441,3 +1441,35 @@ FROM pg_class rel
 
 	/*

index 521a802fe2d..e0552f840a0 100644
@@ -1179,6 +1179,10 @@ pgstat_build_snapshot(PgStat_Kind statKind)
 		if (p->dropped)
 			continue;

 
 
index 416f3c51b0c..15fa3de0871 100644
@@ -2372,18 +2372,19 @@ pg_stat_get_vacuum_tables(PG_FUNCTION_ARGS)

 	values[i++] = Int64GetDatum(extvacuum->wal_records);
 	values[i++] = Int64GetDatum(extvacuum->wal_fpi);
@@ -2402,6 +2403,116 @@ pg_stat_get_vacuum_tables(PG_FUNCTION_ARGS)

index 40767e44601..191894aacab 100644
@@ -1498,7 +1498,7 @@ struct config_bool ConfigureNamesBool[] =
index 6bac3cbc3eb..369806befb6 100644
@@ -12497,4 +12497,13 @@
index 6d1b2991ce5..fb134f3402e 100644
 
 /*
 
+/*
@@ -408,4 +429,8 @@ extern double anl_random_fract(void);
 
index 3522223d9ca..30e7e5537f5 100644
@@ -111,11 +111,19 @@ typedef struct PgStat_BackendSubEntry
 
@@ -144,18 +152,44 @@ typedef struct ExtVacReport

 /* ----------

 step s1_commit: COMMIT;

index f4bba9cc30c..640f799b7c1 100644
@@ -2262,6 +2262,28 @@ pg_stat_user_tables| SELECT relid,
index 99bcc46efcc..3498ce06635 100644

From 5973c92e91f1a409067a864e6964dfa6e922cb8b Mon Sep 17 00:00:00 2001
From: Alena Rybakina <[email-masked]>
Date: Thu, 27 Feb 2025 20:01:38 +0300
Subject: [PATCH 1/4] Machinery for grabbing an extended vacuum statistics on
 table relations.

index 2cbcf5e5db2..592b018c300 100644

+/*

+/* ----------

 /*
@@ -632,7 +747,14 @@ heap_vacuum_rel(Relation rel, VacuumParams *params,
 
 	verbose = (params->options & VACOPT_VERBOSE) != 0;
 	instrument = (verbose || (AmAutoVacuumWorkerProcess() &&
@@ -652,7 +774,7 @@ heap_vacuum_rel(Relation rel, VacuumParams *params,
 
@@ -669,6 +791,7 @@ heap_vacuum_rel(Relation rel, VacuumParams *params,
@@ -758,6 +881,7 @@ heap_vacuum_rel(Relation rel, VacuumParams *params,
 
 	/*
 	 * Get cutoffs that determine which deleted tuples are considered DEAD,
@@ -924,6 +1048,26 @@ heap_vacuum_rel(Relation rel, VacuumParams *params,
 						vacrel->NewRelfrozenXid, vacrel->NewRelminMxid,
 						&frozenxid_updated, &minmulti_updated, false);
 
@@ -939,7 +1083,8 @@ heap_vacuum_rel(Relation rel, VacuumParams *params,
 
 	if (instrument)
@@ -2957,6 +3102,7 @@ lazy_check_wraparound_failsafe(LVRelState *vacrel)
 		int64		progress_val[2] = {0, 0};

 		/*
 
 	if (map[mapByte] & mask)
 	{
+		/*
 
 		MarkBufferDirty(vmbuf);
index 31d269b7ee0..603bf97e042 100644
@@ -1391,3 +1393,51 @@ CREATE VIEW pg_stat_subscription_stats AS
 
index f0a7b87808d..1ef67299d48 100644
@@ -115,6 +115,9 @@ pg_atomic_uint32 *VacuumSharedCostBalance = NULL;
 
@@ -2512,6 +2515,7 @@ vacuum_delay_point(bool is_analyze)
 			exit(1);
 
 		VacuumCostBalance = 0;
 
 		/*
index 40063085073..521a802fe2d 100644
@@ -190,7 +190,7 @@ static void pgstat_reset_after_failure(void);

 static inline bool pgstat_is_kind_valid(PgStat_Kind kind);
@@ -203,7 +203,7 @@ static inline bool pgstat_is_kind_valid(PgStat_Kind kind);

 /* ----------
  * state shared with pgstat_*.c
@@ -260,7 +260,6 @@ static bool pgstat_is_initialized = false;
 static bool pgstat_is_shutdown = false;
 #endif
 
-
 /*
  * The different kinds of built-in statistics.
  *
@@ -900,7 +899,6 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 		pgstat_reset_entries_of_kind(kind, ts);
 }
 
-
 /* ------------------------------------------------------------
  * Fetching of stats
  * ------------------------------------------------------------
@@ -969,7 +967,7 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid)

 	/* if caching is desired, look up in cache */
 	if (pgstat_fetch_consistency > PGSTAT_FETCH_CONSISTENCY_NONE)
@@ -1085,7 +1083,7 @@ pgstat_snapshot_fixed(PgStat_Kind kind)
 		pgstat_clear_snapshot();

@@ -1136,7 +1134,7 @@ pgstat_prep_snapshot(void)
 }
 
 static void

 /*

index 97af7c6554f..416f3c51b0c 100644
 
@@ -2258,3 +2264,144 @@ pg_stat_have_stats(PG_FUNCTION_ARGS)

+/*
 
 /*
index 989825d3a9c..40767e44601 100644
@@ -1496,6 +1496,15 @@ struct config_bool ConfigureNamesBool[] =
index 0b9e3066bde..25a8f931983 100644
@@ -658,6 +658,7 @@

 # - Monitoring -
index 0d29ef50ff2..6bac3cbc3eb 100644
@@ -12479,4 +12479,22 @@
 
+{ oid => '8001',
index bc37a80dc74..6d1b2991ce5 100644
@@ -327,6 +327,7 @@ extern PGDLLIMPORT double vacuum_max_eager_freeze_failure_rate;
 
 extern PGDLLIMPORT bool VacuumFailsafeActive;
 extern PGDLLIMPORT double vacuum_cost_delay;
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 5bfe19e66be..3522223d9ca 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -111,6 +111,53 @@ typedef struct PgStat_BackendSubEntry
 	PgStat_Counter conflict_count[CONFLICT_NUM_TYPES];
 } PgStat_BackendSubEntry;
 
+/* ----------
+ *
+ * ExtVacReport
+ *
@@ -153,6 +200,16 @@ typedef struct PgStat_TableCounts

 /* ----------
@@ -211,7 +268,7 @@ typedef struct PgStat_TableXactStatus
  * ------------------------------------------------------------
  */
 
-#define PGSTAT_FILE_FORMAT_ID	0x01A5BCB7
+#define PGSTAT_FILE_FORMAT_ID	0x01A5BCB8
 
 typedef struct PgStat_ArchiverStats
 {
@@ -375,6 +432,8 @@ typedef struct PgStat_StatDBEntry
 	PgStat_Counter parallel_workers_launched;

@@ -453,6 +512,11 @@ typedef struct PgStat_StatTabEntry
 
 /* ------
@@ -660,7 +724,7 @@ extern void pgstat_unlink_relation(Relation rel);
 
@@ -711,6 +775,17 @@ extern void pgstat_report_analyze(Relation rel,
 
@@ -799,6 +874,7 @@ extern PgStat_WalStats *pgstat_fetch_stat_wal(void);

 /*
index 855c147325b..b403ffcc090 100644

 /*----------
index 47478969135..f4bba9cc30c 100644
@@ -2201,7 +2203,9 @@ pg_stat_sys_tables| SELECT relid,
@@ -2253,9 +2257,43 @@ pg_stat_user_tables| SELECT relid,
index 0a35f2f8f6a..99bcc46efcc 100644

diff --git a/src/backend/access/heap/vacuumlazy.c b/src/backend/access/heap/vacuumlazy.c
index d9b07837831..c620d088204 100644
--- a/src/backend/access/heap/vacuumlazy.c
+++ b/src/backend/access/heap/vacuumlazy.c
@@ -413,7 +413,7 @@ typedef struct LVRelState

-	ExtVacReport extVacReport;
+	PgStat_VacuumRelationCounts extVacReport;
 } LVRelState;

@@ -525,7 +525,7 @@ extvac_stats_start(Relation rel, LVExtStatCounters *counters)
  */
 static void
 extvac_stats_end(Relation rel, LVExtStatCounters *counters,
-				  ExtVacReport *report)
+				 PgStat_VacuumRelationCounts *report)
 {
 	WalUsage	walusage;
 	BufferUsage	bufusage;
@@ -603,9 +603,9 @@ extvac_stats_start_idx(Relation rel, IndexBulkDeleteResult *stats,
 
 void
 extvac_stats_end_idx(Relation rel, IndexBulkDeleteResult *stats,
-					 LVExtStatCountersIdx *counters, ExtVacReport *report)
+					 LVExtStatCountersIdx *counters, PgStat_VacuumRelationCounts *report)
 {
-	memset(report, 0, sizeof(ExtVacReport));
+	memset(report, 0, sizeof(PgStat_VacuumRelationCounts));
 
 	extvac_stats_end(rel, &counters->common, report);
 	report->type = PGSTAT_EXTVAC_INDEX;
@@ -1127,6 +1127,14 @@ heap_vacuum_rel(Relation rel, VacuumParams *params,
 	 *
 	 * We are ready to send vacuum statistics information for heap relations.
 	 */
+
+	pgstat_report_vacuum(RelationGetRelid(rel),
+						 rel->rd_rel->relisshared,
+						 Max(vacrel->new_live_tuples, 0),
+						 vacrel->recently_dead_tuples +
+						 vacrel->missed_dead_tuples,
+						 starttime);
+
 	if(pgstat_track_vacuum_statistics)
 	{
 		/* Make generic extended vacuum stats report and
@@ -1135,25 +1143,10 @@ heap_vacuum_rel(Relation rel, VacuumParams *params,
 		extvac_stats_end(vacrel->rel, &extVacCounters, &(vacrel->extVacReport));
 		accumulate_heap_vacuum_statistics(&extVacCounters, vacrel);
 
+		pgstat_report_tab_vacuum_extstats(vacrel->reloid, true,
 
 	}
-	else
-	{

@@ -3349,7 +3342,7 @@ lazy_vacuum_one_index(Relation indrel, IndexBulkDeleteResult *istat,
 	IndexVacuumInfo ivinfo;
 	LVSavedErrInfo saved_err_info;
 	LVExtStatCountersIdx extVacCounters;
-	ExtVacReport extVacReport;
+	PgStat_VacuumRelationCounts extVacReport;
 
 	/* Set initial statistics values to gather vacuum statistics for the index */
 	extvac_stats_start_idx(indrel, istat, &extVacCounters);
@@ -3384,9 +3377,8 @@ lazy_vacuum_one_index(Relation indrel, IndexBulkDeleteResult *istat,
 	{
 		/* Make extended vacuum stats report for index */
 		extvac_stats_end_idx(indrel, istat, &extVacCounters, &extVacReport);
-		pgstat_report_vacuum(RelationGetRelid(indrel),
-								indrel->rd_rel->relisshared,
-								0, 0, 0, &extVacReport);
+		pgstat_report_tab_vacuum_extstats(vacrel->indoid, true,
+										  &extVacReport);
 	}
 
 	/* Revert to the previous phase information for error traceback */
@@ -3414,7 +3406,7 @@ lazy_cleanup_one_index(Relation indrel, IndexBulkDeleteResult *istat,
 	IndexVacuumInfo ivinfo;
 	LVSavedErrInfo saved_err_info;
 	LVExtStatCountersIdx extVacCounters;
-	ExtVacReport extVacReport;
+	PgStat_VacuumRelationCounts extVacReport;
 
 	/* Set initial statistics values to gather vacuum statistics for the index */
 	extvac_stats_start_idx(indrel, istat, &extVacCounters);
@@ -3448,9 +3440,8 @@ lazy_cleanup_one_index(Relation indrel, IndexBulkDeleteResult *istat,
 	{
 		/* Make extended vacuum stats report for index */
 		extvac_stats_end_idx(indrel, istat, &extVacCounters, &extVacReport);
-		pgstat_report_vacuum(RelationGetRelid(indrel),
-								indrel->rd_rel->relisshared,
-								0, 0, 0, &extVacReport);
+		pgstat_report_tab_vacuum_extstats(vacrel->indoid, true,
+										  &extVacReport);
 	}
 
 	/* Revert to the previous phase information for error traceback */
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index bd3554c0bfd..75c2e122bf2 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -1873,6 +1873,7 @@ heap_drop_with_catalog(Oid relid)
 
 	/* ensure that stats are dropped if transaction commits */
 	pgstat_drop_relation(rel);
+	pgstat_vacuum_relation_delete_pending_cb(RelationGetRelid(rel));
 
 	/*
 	 * Close relcache entry, but *keep* AccessExclusiveLock on the relation
diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c
index 739a92bdcc1..e4fa754aab4 100644
--- a/src/backend/catalog/index.c
+++ b/src/backend/catalog/index.c
@@ -2327,6 +2327,7 @@ index_drop(Oid indexId, bool concurrent, bool concurrent_lock_mode)
 
 	/* ensure that stats are dropped if transaction commits */
 	pgstat_drop_relation(userIndexRelation);
+	pgstat_vacuum_relation_delete_pending_cb(RelationGetRelid(userIndexRelation));
 
 	/*
 	 * Close and flush the index's relcache entry, to ensure relcache doesn't
diff --git a/src/backend/commands/dbcommands.c b/src/backend/commands/dbcommands.c
index 5fbbcdaabb1..c4b910cd928 100644
--- a/src/backend/commands/dbcommands.c
+++ b/src/backend/commands/dbcommands.c
@@ -1789,6 +1789,7 @@ dropdb(const char *dbname, bool missing_ok, bool force)
 	 * Tell the cumulative stats system to forget it immediately, too.
 	 */
 	pgstat_drop_database(db_id);
+	pgstat_drop_vacuum_database(db_id);
 
 	/*
 	 * Except for the deletion of the catalog row, subsequent actions are not
diff --git a/src/backend/commands/vacuumparallel.c b/src/backend/commands/vacuumparallel.c
index 000388a565f..c2abed144c4 100644
--- a/src/backend/commands/vacuumparallel.c
+++ b/src/backend/commands/vacuumparallel.c
@@ -869,7 +869,7 @@ parallel_vacuum_process_one_index(ParallelVacuumState *pvs, Relation indrel,
 	IndexBulkDeleteResult *istat_res;
 	IndexVacuumInfo ivinfo;
 	LVExtStatCountersIdx extVacCounters;
-	ExtVacReport extVacReport;
+	PgStat_VacuumRelationCounts extVacReport;
 
 	/*
 	 * Update the pointer to the corresponding bulk-deletion result if someone
@@ -913,9 +913,8 @@ parallel_vacuum_process_one_index(ParallelVacuumState *pvs, Relation indrel,
 	{
 		/* Make extended vacuum stats report for index */
 		extvac_stats_end_idx(indrel, istat_res, &extVacCounters, &extVacReport);
-		pgstat_report_vacuum(RelationGetRelid(indrel),
-								indrel->rd_rel->relisshared,
-								0, 0, 0, &extVacReport);
+		pgstat_report_tab_vacuum_extstats(RelationGetRelid(indrel), true,
+										  &extVacReport);
 	}
 
 	/*
diff --git a/src/backend/utils/activity/Makefile b/src/backend/utils/activity/Makefile
index 9c2443e1ecd..183f7514d2d 100644
--- a/src/backend/utils/activity/Makefile
+++ b/src/backend/utils/activity/Makefile
@@ -27,6 +27,7 @@ OBJS = \
 	pgstat_function.o \
 	pgstat_io.o \
 	pgstat_relation.o \
+	pgstat_vacuum.o \
 	pgstat_replslot.o \
 	pgstat_shmem.o \
 	pgstat_slru.o \
diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index 09fa0fbee57..8a5f355e9bc 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -478,6 +478,34 @@ static const PgStat_KindInfo pgstat_kind_builtin_infos[PGSTAT_KIND_BUILTIN_SIZE]
 		.reset_all_cb = pgstat_wal_reset_all_cb,
 		.snapshot_cb = pgstat_wal_snapshot_cb,
 	},
+	[PGSTAT_KIND_VACUUM_DB] = {
+		.name = "vacuum statistics",
+
+		.fixed_amount = false,
+		.write_to_file = true,
+		/* so pg_stat_database entries can be seen in all databases */
+		.accessed_across_databases = true,
+
+		.shared_size = sizeof(PgStatShared_VacuumDB),
+		.shared_data_off = offsetof(PgStatShared_VacuumDB, stats),
+		.shared_data_len = sizeof(((PgStatShared_VacuumDB *) 0)->stats),
+		.pending_size = sizeof(PgStat_VacuumDBCounts),
+
+		.flush_pending_cb = pgstat_vacuum_db_flush_cb,
+	},
+	[PGSTAT_KIND_VACUUM_RELATION] = {
+		.name = "vacuum statistics",
+
+		.fixed_amount = false,
+		.write_to_file = true,
+
+		.shared_size = sizeof(PgStatShared_VacuumRelation),
+		.shared_data_off = offsetof(PgStatShared_VacuumRelation, stats),
+		.shared_data_len = sizeof(((PgStatShared_VacuumRelation *) 0)->stats),
+		.pending_size = sizeof(PgStat_RelationVacuumPending),
+
+		.flush_pending_cb = pgstat_vacuum_relation_flush_cb
+	},
 };
 
 /*
diff --git a/src/backend/utils/activity/pgstat_database.c b/src/backend/utils/activity/pgstat_database.c
index d5c1e2a2cf5..344f0a24683 100644
--- a/src/backend/utils/activity/pgstat_database.c
+++ b/src/backend/utils/activity/pgstat_database.c
@@ -46,6 +46,15 @@ pgstat_drop_database(Oid databaseid)
 	pgstat_drop_transactional(PGSTAT_KIND_DATABASE, databaseid, InvalidOid);
 }
 
+/*
+ * Remove entry for the database being dropped.
+ */
+void
+pgstat_drop_vacuum_database(Oid databaseid)
+{
+	pgstat_drop_transactional(PGSTAT_KIND_VACUUM_DB, databaseid, InvalidOid);
+}
+
 /*
  * Called from autovacuum.c to report startup of an autovacuum process.
  * We are called before InitPostgres is done, so can't rely on MyDatabaseId;
@@ -449,7 +458,6 @@ pgstat_database_flush_cb(PgStat_EntryRef *entry_ref, bool nowait)
 	pgstat_unlock_entry(entry_ref);
 
 	memset(pendingent, 0, sizeof(*pendingent));
-	memset(&(pendingent)->vacuum_ext, 0, sizeof(ExtVacReport));
 
 	return true;
 }
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index 5d36d5a2140..db6dba70331 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -47,8 +47,6 @@ static void add_tabstat_xact_level(PgStat_TableStatus *pgstat_info, int nest_lev
 static void ensure_tabstat_xact_level(PgStat_TableStatus *pgstat_info);
 static void save_truncdrop_counters(PgStat_TableXactStatus *trans, bool is_drop);
 static void restore_truncdrop_counters(PgStat_TableXactStatus *trans);
-static void pgstat_accumulate_extvac_stats(ExtVacReport *dst, ExtVacReport *src,
-							   bool accumulate_reltype_specific_info);

 /*
@@ -205,50 +203,17 @@ pgstat_drop_relation(Relation rel)
 	}
 }
 
-/* ---------
- * pgstat_report_vacuum_error() -
- *
- *	Tell the collector about an (auto)vacuum interruption.
- * ---------
- */
-void
-pgstat_report_vacuum_error(Oid tableoid, ExtVacReportType m_type)
-{
-	PgStat_EntryRef *entry_ref;
-	PgStatShared_Relation *shtabentry;
-	PgStat_StatTabEntry *tabentry;
-	Oid			dboid =  MyDatabaseId;
-	PgStat_StatDBEntry *dbentry;	/* pending database entry */
-
-	if (!pgstat_track_counts)
-		return;
-
-	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
-											dboid, tableoid, false);
-
-	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
-	tabentry = &shtabentry->stats;
-
-	tabentry->vacuum_ext.type = m_type;
-	pgstat_unlock_entry(entry_ref);
-
-	dbentry = pgstat_prep_database_pending(dboid);
-	dbentry->vacuum_ext.errors++;
-	dbentry->vacuum_ext.type = m_type;
-}
-
 /*
  * Report that the table was just vacuumed and flush IO statistics.
  */
 void
 pgstat_report_vacuum(Oid tableoid, bool shared,
 					 PgStat_Counter livetuples, PgStat_Counter deadtuples,
-					 TimestampTz starttime, ExtVacReport *params)
+					 TimestampTz starttime)
 {
 	PgStat_EntryRef *entry_ref;
 	PgStatShared_Relation *shtabentry;
 	PgStat_StatTabEntry *tabentry;
-	PgStatShared_Database *dbentry;
 	Oid			dboid = (shared ? InvalidOid : MyDatabaseId);
 	TimestampTz ts;
 	PgStat_Counter elapsedtime;
@@ -270,8 +235,6 @@ pgstat_report_vacuum(Oid tableoid, bool shared,
 	tabentry->live_tuples = livetuples;
 	tabentry->dead_tuples = deadtuples;
 
-	pgstat_accumulate_extvac_stats(&tabentry->vacuum_ext, params, true);
-
 	/*
 	 * It is quite possible that a non-aggressive VACUUM ended up skipping
 	 * various pages, however, we'll zero the insert counter here regardless.
@@ -307,16 +270,6 @@ pgstat_report_vacuum(Oid tableoid, bool shared,
 	 */
 	pgstat_flush_io(false);
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
-
-	if (dboid != InvalidOid)
-	{
-		entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_DATABASE,
-											dboid, InvalidOid, false);
-		dbentry = (PgStatShared_Database *) entry_ref->shared_stats;
-
-		pgstat_accumulate_extvac_stats(&dbentry->stats.vacuum_ext, params, false);
-		pgstat_unlock_entry(entry_ref);
-	}
 }
 
 /*
@@ -942,6 +895,12 @@ pgstat_relation_flush_cb(PgStat_EntryRef *entry_ref, bool nowait)
 	return true;
 }
 
+void
+pgstat_vacuum_relation_delete_pending_cb(Oid relid)
+{
+	pgstat_drop_transactional(PGSTAT_KIND_VACUUM_RELATION, relid, InvalidOid);
+}
+
 void
 pgstat_relation_delete_pending_cb(PgStat_EntryRef *entry_ref)
 {
@@ -1044,60 +1003,4 @@ restore_truncdrop_counters(PgStat_TableXactStatus *trans)
 		trans->tuples_updated = trans->updated_pre_truncdrop;
 		trans->tuples_deleted = trans->deleted_pre_truncdrop;
 	}
-}
-
-static void
-pgstat_accumulate_extvac_stats(ExtVacReport *dst, ExtVacReport *src,
-							   bool accumulate_reltype_specific_info)
-{
-	if(!pgstat_track_vacuum_statistics)
-		return;
-
-	dst->total_blks_read += src->total_blks_read;
-	dst->total_blks_hit += src->total_blks_hit;
-	dst->total_blks_dirtied += src->total_blks_dirtied;
-	dst->total_blks_written += src->total_blks_written;
-	dst->wal_bytes += src->wal_bytes;
-	dst->wal_fpi += src->wal_fpi;
-	dst->wal_records += src->wal_records;
-	dst->blk_read_time += src->blk_read_time;
-	dst->blk_write_time += src->blk_write_time;
-	dst->delay_time += src->delay_time;
-	dst->total_time += src->total_time;
-	dst->wraparound_failsafe_count += src->wraparound_failsafe_count;
-	dst->errors += src->errors;
-
-	if (!accumulate_reltype_specific_info)
-		return;
-
-	if (dst->type == PGSTAT_EXTVAC_INVALID)
-		dst->type = src->type;
-
-	Assert(src->type == PGSTAT_EXTVAC_INVALID || src->type == dst->type);
-
-	if (dst->type == src->type)
-	{
-		dst->blks_fetched += src->blks_fetched;
-		dst->blks_hit += src->blks_hit;
-
-		if (dst->type == PGSTAT_EXTVAC_TABLE)
-		{
-			dst->table.pages_scanned += src->table.pages_scanned;
-			dst->table.pages_removed += src->table.pages_removed;
-			dst->table.vm_new_frozen_pages += src->table.vm_new_frozen_pages;
-			dst->table.vm_new_visible_pages += src->table.vm_new_visible_pages;
-			dst->table.vm_new_visible_frozen_pages += src->table.vm_new_visible_frozen_pages;
-			dst->tuples_deleted += src->tuples_deleted;
-			dst->table.tuples_frozen += src->table.tuples_frozen;
-			dst->table.recently_dead_tuples += src->table.recently_dead_tuples;
-			dst->table.index_vacuum_count += src->table.index_vacuum_count;
-			dst->table.missed_dead_pages += src->table.missed_dead_pages;
-			dst->table.missed_dead_tuples += src->table.missed_dead_tuples;
-		}
-		else if (dst->type == PGSTAT_EXTVAC_INDEX)
-		{
-			dst->index.pages_deleted += src->index.pages_deleted;
-			dst->tuples_deleted += src->tuples_deleted;
-		}
-	}
 }
\ No newline at end of file
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index c2acdcf0e0e..3605ec98317 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -2275,14 +2275,14 @@ pg_stat_get_vacuum_tables(PG_FUNCTION_ARGS)
 	#define PG_STAT_GET_VACUUM_TABLES_STATS_COLS 26
 
 	Oid						relid = PG_GETARG_OID(0);
-	PgStat_StatTabEntry     *tabentry;
-	ExtVacReport 			*extvacuum;
+	PgStat_VacuumRelationCounts 			*extvacuum;
+	PgStat_RelationVacuumPending *pending;
 	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;
-	ExtVacReport allzero;
+	PgStat_VacuumRelationCounts allzero;
 
 	/* Initialise attributes information in the tuple descriptor */
 	tupdesc = CreateTemplateTupleDesc(PG_STAT_GET_VACUUM_TABLES_STATS_COLS);
@@ -2346,18 +2346,16 @@ pg_stat_get_vacuum_tables(PG_FUNCTION_ARGS)
 
 	BlessTupleDesc(tupdesc);
 
-	tabentry = pgstat_fetch_stat_tabentry(relid);
+	pending = find_vacuum_relation_entry(relid);
 
-	if (tabentry == NULL)
+	if (pending == NULL)
 	{
 		/* If the subscription is not found, initialise its stats */
-		memset(&allzero, 0, sizeof(ExtVacReport));
+		memset(&allzero, 0, sizeof(PgStat_VacuumRelationCounts));
 		extvacuum = &allzero;
 	}
 	else
-	{
-		extvacuum = &(tabentry->vacuum_ext);
-	}
+		extvacuum = &(pending->counts);
 
 	i = 0;
 
@@ -2416,14 +2414,14 @@ pg_stat_get_vacuum_indexes(PG_FUNCTION_ARGS)
 	#define PG_STAT_GET_VACUUM_INDEX_STATS_COLS	16
 
 	Oid						relid = PG_GETARG_OID(0);
-	PgStat_StatTabEntry     *tabentry;
-	ExtVacReport 			*extvacuum;
+	PgStat_VacuumRelationCounts 			*extvacuum;
+	PgStat_RelationVacuumPending *pending;
 	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;
-	ExtVacReport allzero;
+	PgStat_VacuumRelationCounts allzero;
 
 	/* Initialise attributes information in the tuple descriptor */
 	tupdesc = CreateTemplateTupleDesc(PG_STAT_GET_VACUUM_INDEX_STATS_COLS);
@@ -2467,18 +2465,16 @@ pg_stat_get_vacuum_indexes(PG_FUNCTION_ARGS)
 
 	BlessTupleDesc(tupdesc);
 
-	tabentry = pgstat_fetch_stat_tabentry(relid);
+	pending = find_vacuum_relation_entry(relid);
 
-	if (tabentry == NULL)
+	if (pending == NULL)
 	{
 		/* If the subscription is not found, initialise its stats */
-		memset(&allzero, 0, sizeof(ExtVacReport));
+		memset(&allzero, 0, sizeof(PgStat_VacuumRelationCounts));
 		extvacuum = &allzero;
 	}
-	else
-	{
-		extvacuum = &(tabentry->vacuum_ext);
-	}
+
+	extvacuum = &(pending->counts);
 
 	i = 0;
 
@@ -2523,14 +2519,14 @@ pg_stat_get_vacuum_database(PG_FUNCTION_ARGS)
 	#define PG_STAT_GET_VACUUM_DATABASE_STATS_COLS	14
 
 	Oid						 dbid = PG_GETARG_OID(0);
-	PgStat_StatDBEntry 		*dbentry;
-	ExtVacReport 			*extvacuum;
+	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;
-	ExtVacReport allzero;
+
+	PG_RETURN_NULL();
 
 	/* Initialise attributes information in the tuple descriptor */
 	tupdesc = CreateTemplateTupleDesc(PG_STAT_GET_VACUUM_DATABASE_STATS_COLS);
@@ -2570,18 +2566,7 @@ pg_stat_get_vacuum_database(PG_FUNCTION_ARGS)
 
 	BlessTupleDesc(tupdesc);
 
-	dbentry = pgstat_fetch_stat_dbentry(dbid);
-
-	if (dbentry == NULL)
-	{
-		/* If the subscription is not found, initialise its stats */
-		memset(&allzero, 0, sizeof(ExtVacReport));
-		extvacuum = &allzero;
-	}
-	else
-	{
-		extvacuum = &(dbentry->vacuum_ext);
-	}
+	extvacuum = pgstat_prep_vacuum_database_pending(dbid);
 
 	i = 0;
 
diff --git a/src/include/commands/vacuum.h b/src/include/commands/vacuum.h
index fb134f3402e..f895151ca09 100644
--- a/src/include/commands/vacuum.h
+++ b/src/include/commands/vacuum.h
@@ -432,5 +432,5 @@ extern double anl_get_next_S(double t, int n, double *stateptr);
 extern void extvac_stats_start_idx(Relation rel, IndexBulkDeleteResult *stats,
 					   LVExtStatCountersIdx *counters);
 extern void extvac_stats_end_idx(Relation rel, IndexBulkDeleteResult *stats,
-					 LVExtStatCountersIdx *counters, ExtVacReport *report);
+					 LVExtStatCountersIdx *counters, PgStat_VacuumRelationCounts *report);
 #endif							/* VACUUM_H */
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 66e6e721563..1760b35b5eb 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -119,9 +119,56 @@ typedef enum ExtVacReportType
 	PGSTAT_EXTVAC_INDEX = 2
 } ExtVacReportType;
 
+/* ----------
+ * PgStat_TableCounts			The actual per-table counts kept by a backend
+ *
+ * This struct should contain only actual event counters, because we make use
+ * of pg_memory_is_all_zeros() to detect whether there are any stats updates
+ * to apply.
+ *
+ * It is a component of PgStat_TableStatus (within-backend state).
+ *
+ * Note: for a table, tuples_returned is the number of tuples successfully
+ * fetched by heap_getnext, while tuples_fetched is the number of tuples
+ * successfully fetched by heap_fetch under the control of bitmap indexscans.
+ * For an index, tuples_returned is the number of index entries returned by
+ * the index AM, while tuples_fetched is the number of tuples successfully
+ * fetched by heap_fetch under the control of simple indexscans for this index.
+ *
+ * tuples_inserted/updated/deleted/hot_updated/newpage_updated count attempted
+ * actions, regardless of whether the transaction committed.  delta_live_tuples,
+ * delta_dead_tuples, and changed_tuples are set depending on commit or abort.
+ * Note that delta_live_tuples and delta_dead_tuples can be negative!
+ * ----------
+ */
+typedef struct PgStat_TableCounts
+{
+	PgStat_Counter numscans;
+
+	PgStat_Counter tuples_returned;
+	PgStat_Counter tuples_fetched;
+
+	PgStat_Counter tuples_inserted;
+	PgStat_Counter tuples_updated;
+	PgStat_Counter tuples_deleted;
+	PgStat_Counter tuples_hot_updated;
+	PgStat_Counter tuples_newpage_updated;
+	bool		truncdropped;
+
+	PgStat_Counter delta_live_tuples;
+	PgStat_Counter delta_dead_tuples;
+	PgStat_Counter changed_tuples;
+
+	PgStat_Counter blocks_fetched;
+	PgStat_Counter blocks_hit;
+
+	PgStat_Counter rev_all_visible_pages;
+	PgStat_Counter rev_all_frozen_pages;
+} PgStat_TableCounts;
+
 /* ----------
  *
- * ExtVacReport
+ * PgStat_VacuumRelationCounts
  *
  * Additional statistics of vacuum processing over a relation.
  * pages_removed is the amount by which the physically shrank,
@@ -129,7 +176,7 @@ typedef enum ExtVacReportType
  * pages_deleted refer to free space within the index file
  * ----------
  */
-typedef struct ExtVacReport
+typedef struct PgStat_VacuumRelationCounts
 {
 	/* number of blocks missed, hit, dirtied and written during a vacuum of specific relation */
 	int64		total_blks_read;
@@ -154,7 +201,6 @@ typedef struct ExtVacReport

-	int32		errors;
 	int32		wraparound_failsafe_count;	/* the number of times to prevent wraparound problem */
 
 	ExtVacReportType type;		/* heap, index, etc. */
@@ -192,61 +238,44 @@ typedef struct ExtVacReport
 			int64		pages_deleted;		/* number of pages deleted by vacuum */
 		}			index;
 	} /* per_type_stats */;
-} ExtVacReport;
+} PgStat_VacuumRelationCounts;
 
-/* ----------
- * PgStat_TableCounts			The actual per-table counts kept by a backend
- *
- * This struct should contain only actual event counters, because we make use
- * of pg_memory_is_all_zeros() to detect whether there are any stats updates
- * to apply.
- *
- * It is a component of PgStat_TableStatus (within-backend state).
- *
- * Note: for a table, tuples_returned is the number of tuples successfully
- * fetched by heap_getnext, while tuples_fetched is the number of tuples
- * successfully fetched by heap_fetch under the control of bitmap indexscans.
- * For an index, tuples_returned is the number of index entries returned by
- * the index AM, while tuples_fetched is the number of tuples successfully
- * fetched by heap_fetch under the control of simple indexscans for this index.
- *
- * tuples_inserted/updated/deleted/hot_updated/newpage_updated count attempted
- * actions, regardless of whether the transaction committed.  delta_live_tuples,
- * delta_dead_tuples, and changed_tuples are set depending on commit or abort.
- * Note that delta_live_tuples and delta_dead_tuples can be negative!
- * ----------
- */
-typedef struct PgStat_TableCounts
+typedef struct PgStat_VacuumRelationStatus
 {
-	PgStat_Counter numscans;
+	Oid			id;				/* table's OID */
+	bool		shared;			/* is it a shared catalog? */
+	PgStat_VacuumRelationCounts counts;	/* event counts to be sent */
+} PgStat_VacuumRelationStatus;
 
-	PgStat_Counter tuples_returned;
-	PgStat_Counter tuples_fetched;
+typedef struct PgStat_VacuumDBCounts
+{
+	Oid			dbjid;
+	/* number of blocks missed, hit, dirtied and written during a vacuum of specific relation */
+	int64		total_blks_read;
+	int64		total_blks_hit;
+	int64		total_blks_dirtied;
+	int64		total_blks_written;
 
-	PgStat_Counter tuples_inserted;
-	PgStat_Counter tuples_updated;
-	PgStat_Counter tuples_deleted;
-	PgStat_Counter tuples_hot_updated;
-	PgStat_Counter tuples_newpage_updated;
-	bool		truncdropped;
+	/* blocks missed and hit for just the heap during a vacuum of specific relation */
+	int64		blks_fetched;
+	int64		blks_hit;
 
-	PgStat_Counter delta_live_tuples;
-	PgStat_Counter delta_dead_tuples;
-	PgStat_Counter changed_tuples;
+	/* Vacuum WAL usage stats */
+	int64		wal_records;	/* wal usage: number of WAL records */
+	int64		wal_fpi;		/* wal usage: number of WAL full page images produced */
+	uint64		wal_bytes;		/* wal usage: size of WAL records produced */

-	PgStat_Counter rev_all_visible_pages;
-	PgStat_Counter rev_all_frozen_pages;
+	int64		tuples_deleted;		/* tuples deleted by vacuum */
 
-	/*
-	 * Additional cumulative stat on vacuum operations.
-	 * Use an expensive structure as an abstraction for different types of
-	 * relations.
-	 */
-	ExtVacReport	vacuum_ext;
-} PgStat_TableCounts;
+	int32		errors;
+	int32		wraparound_failsafe_count;	/* the number of times to prevent wraparound problem */
+} PgStat_VacuumDBCounts;
 
 /* ----------
  * PgStat_TableStatus			Per-table status within a backend
@@ -272,6 +301,12 @@ typedef struct PgStat_TableStatus
 	Relation	relation;		/* rel that is using this entry */
 } PgStat_TableStatus;
 
+typedef struct PgStat_RelationVacuumPending
+{
+	Oid			id;				/* table's OID */
+	PgStat_VacuumRelationCounts counts;	/* event counts to be sent */
+} PgStat_RelationVacuumPending;
+
 /* ----------
  * PgStat_TableXactStatus		Per-table, per-subtransaction status
  * ----------
@@ -468,8 +503,6 @@ typedef struct PgStat_StatDBEntry
 	PgStat_Counter parallel_workers_launched;
 
 	TimestampTz stat_reset_timestamp;
-
-	ExtVacReport vacuum_ext;		/* extended vacuum statistics */
 } PgStat_StatDBEntry;
 
 typedef struct PgStat_StatFuncEntry
@@ -551,8 +584,6 @@ typedef struct PgStat_StatTabEntry
 
 	PgStat_Counter rev_all_visible_pages;
 	PgStat_Counter rev_all_frozen_pages;
-
-	ExtVacReport vacuum_ext;
 } PgStat_StatTabEntry;
 
 /* ------
@@ -760,7 +791,7 @@ extern void pgstat_unlink_relation(Relation rel);
 
 extern void pgstat_report_vacuum(Oid tableoid, bool shared,
 								 PgStat_Counter livetuples, PgStat_Counter deadtuples,
-								 TimestampTz starttime, ExtVacReport *params);
+								 TimestampTz starttime);
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
@@ -895,6 +926,15 @@ extern int	pgstat_get_transactional_drops(bool isCommit, struct xl_xact_stats_it
 extern void pgstat_execute_transactional_drops(int ndrops, struct xl_xact_stats_item *items, bool is_redo);

+extern void pgstat_drop_vacuum_database(Oid databaseid);
+extern void pgstat_vacuum_relation_delete_pending_cb(Oid relid);
+extern void
+pgstat_report_tab_vacuum_extstats(Oid tableoid, bool shared,
+								  PgStat_VacuumRelationCounts *params);
+extern PgStat_RelationVacuumPending * find_vacuum_relation_entry(Oid relid);
+extern PgStat_VacuumDBCounts *pgstat_prep_vacuum_database_pending(Oid dboid);
+extern PgStat_VacuumRelationCounts *pgstat_fetch_stat_vacuum_tabentry(Oid relid);
+
 /*
  * Functions in pgstat_wal.c
  */
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index d5557e6e998..140adbcdbd6 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -439,6 +439,18 @@ typedef struct PgStatShared_Relation
 	PgStat_StatTabEntry stats;
 } PgStatShared_Relation;
 
+typedef struct PgStatShared_VacuumDB
+{
+	PgStatShared_Common header;
+	PgStat_VacuumDBCounts stats;
+} PgStatShared_VacuumDB;
+
+typedef struct PgStatShared_VacuumRelation
+{
+	PgStatShared_Common header;
+	PgStat_VacuumRelationCounts stats;
+} PgStatShared_VacuumRelation;
+
 typedef struct PgStatShared_Function
 {
 	PgStatShared_Common header;
@@ -607,6 +619,9 @@ extern PgStat_EntryRef *pgstat_fetch_pending_entry(PgStat_Kind kind,
 extern void *pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid);
 extern void pgstat_snapshot_fixed(PgStat_Kind kind);
 
+bool pgstat_vacuum_db_flush_cb(PgStat_EntryRef *entry_ref, bool nowait);
+extern bool pgstat_vacuum_relation_flush_cb(PgStat_EntryRef *entry_ref, bool nowait);
+
 
 /*
  * Functions in pgstat_archiver.c
diff --git a/src/include/utils/pgstat_kind.h b/src/include/utils/pgstat_kind.h
index f44169fd5a3..454661f9d6a 100644
--- a/src/include/utils/pgstat_kind.h
+++ b/src/include/utils/pgstat_kind.h
@@ -38,9 +38,11 @@
 #define PGSTAT_KIND_IO	10
 #define PGSTAT_KIND_SLRU	11
 #define PGSTAT_KIND_WAL	12
+#define PGSTAT_KIND_VACUUM_DB	13
+#define PGSTAT_KIND_VACUUM_RELATION	14
 
 #define PGSTAT_KIND_BUILTIN_MIN PGSTAT_KIND_DATABASE
-#define PGSTAT_KIND_BUILTIN_MAX PGSTAT_KIND_WAL
+#define PGSTAT_KIND_BUILTIN_MAX PGSTAT_KIND_VACUUM_RELATION
 #define PGSTAT_KIND_BUILTIN_SIZE (PGSTAT_KIND_BUILTIN_MAX + 1)
 
 /* Custom stats kinds */

---

From: Alena Rybakina <[email-masked]>
Date: Fri, 9 May 2025 15:03:55 +0300

Hi, all!

> 1.https://www.postgresql.org/message-id/CAD21AoD66b3u28n%3D73kudgMp5wiGiyYUN9LuC9z2ka6YTru5Gw%40mail.gmail.com
I did a rebase and finished the part with storing statistics separately 
from the relation statistics - now it is possible to disable the 
collection of statistics for relationsh using gucs and
this allows us to solve the problem with the memory consumed.

For now I have formatted all this as a diff file. The diff file must be 
applied after all patches have been applied. While looking at it I 
noticed that the code requires significant code refactoring, so I will 
do that.

-- 
Regards,
Alena Rybakina
Postgres Professional
From 0990e725306d9488291d022a37bc17eb66d1256c Mon Sep 17 00:00:00 2001
From: Alena Rybakina <[email-masked]>
Date: Thu, 8 May 2025 21:14:23 +0300
Subject: [PATCH 1/4] Machinery for grabbing an extended vacuum statistics on 
 table relations.

index f28326bad09..28f222afe60 100644

+/*

+/* ----------

 /*

 
 	/*

 	if (instrument)
@@ -2972,6 +3117,7 @@ lazy_check_wraparound_failsafe(LVRelState *vacrel)
 		int64		progress_val[2] = {0, 0};

 		/*
 
 	if (map[mapByte] & mask)
 	{
+		/*
 
 		MarkBufferDirty(vmbuf);
index 15efb02badb..0205b9bb58c 100644
@@ -713,7 +713,9 @@ CREATE VIEW pg_stat_all_tables AS
@@ -1412,3 +1414,51 @@ REVOKE ALL ON pg_aios FROM PUBLIC;
 GRANT SELECT ON pg_aios TO pg_read_all_stats;
 REVOKE EXECUTE ON FUNCTION pg_get_aios() FROM PUBLIC;
 GRANT EXECUTE ON FUNCTION pg_get_aios() TO pg_read_all_stats;
index 33a33bf6b1c..ffb7e1eef4c 100644
 
@@ -2514,6 +2517,7 @@ vacuum_delay_point(bool is_analyze)
 			exit(1);
 
 		VacuumCostBalance = 0;
 
 		/*
index 8b57845e870..23cb62e36a7 100644

 
 
 /* ----------
 
-
 /*
  * The different kinds of built-in statistics.
  *
@@ -897,7 +896,6 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 		pgstat_reset_entries_of_kind(kind, ts);
 }
 
-
 /* ------------------------------------------------------------
  * Fetching of stats
  * ------------------------------------------------------------
@@ -966,7 +964,7 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid)

 	/* if caching is desired, look up in cache */
 	if (pgstat_fetch_consistency > PGSTAT_FETCH_CONSISTENCY_NONE)
@@ -1082,7 +1080,7 @@ pgstat_snapshot_fixed(PgStat_Kind kind)
 		pgstat_clear_snapshot();

@@ -1133,7 +1131,7 @@ pgstat_prep_snapshot(void)
 }
 
 static void
index 28587e2916b..ee0385cd809 100644

 /*
 
@@ -881,6 +885,9 @@ pgstat_relation_flush_cb(PgStat_EntryRef *entry_ref, bool nowait)
 
@@ -1004,3 +1011,40 @@ restore_truncdrop_counters(PgStat_TableXactStatus *trans)

index 47af743990f..8c9e8fb18e1 100644
@@ -1624,6 +1624,19 @@ getinternalerrposition(void)
 
+/*
 
 /*
index 2f8cbd86759..115f0c51cc2 100644
@@ -1508,6 +1508,15 @@ struct config_bool ConfigureNamesBool[] =
index 34826d01380..b4971a9dffe 100644
@@ -665,6 +665,7 @@

 # - Monitoring -
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 62beb71da28..e2aaaf6cd59 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12566,4 +12566,22 @@
   proargnames => '{pid,io_id,io_generation,state,operation,off,length,target,handle_data_len,raw_result,result,target_desc,f_sync,f_localmem,f_buffered}',
   prosrc => 'pg_get_aios' },
 
+{ oid => '8001',
 
index 378f2f2c2ba..6c88d57aef7 100644
 
+/* ----------
+ *
+ * ExtVacReport
+ *

 /* ----------

 

 /* ------

 /*
diff --git a/src/include/utils/elog.h b/src/include/utils/elog.h
index 5eac0e16970..6a30a4db47d 100644
--- a/src/include/utils/elog.h
+++ b/src/include/utils/elog.h
@@ -230,6 +230,7 @@ extern int	geterrcode(void);
 extern int	geterrposition(void);
 extern int	getinternalerrposition(void);

 /*----------
index e3c669a29c7..6faee3ad2c3 100644
@@ -96,6 +96,7 @@ test: timeouts
index 6cf828ca8d0..10a482e2db4 100644
@@ -1829,7 +1829,9 @@ pg_stat_all_tables| SELECT c.oid AS relid,
@@ -2222,7 +2224,9 @@ pg_stat_sys_tables| SELECT relid,
@@ -2274,9 +2278,43 @@ pg_stat_user_tables| SELECT relid,
index a424be2a6bf..ee0343c2729 100644
@@ -140,3 +140,8 @@ test: fast_default

From 49e728d788435ae909ecf05374506918e1e525f2 Mon Sep 17 00:00:00 2001
From: Alena Rybakina <[email-masked]>
Date: Thu, 8 May 2025 21:20:26 +0300
Subject: [PATCH 2/4] Machinery for grabbing an extended vacuum statistics on 
 index relations.

index 28f222afe60..f42c700f6bb 100644

 
-/*

 

 
-	report->blks_fetched =
 
+void
 
 /*

 

 	if (instrument)

+	/*
+	 * Due to the fact that vacuum heap processing needs their index vacuuming
+	 * we need to track them separately and accumulate heap vacuum statistics
+	 * separately. So last processes are related to only heap vacuuming process.
+	 */
+	extvac_stats_start(vacrel->rel, &extVacCounters);
+
 	/*
 	 * Set up the read stream for vacuum's first pass through the heap.
 	 *
@@ -1434,8 +1512,26 @@ lazy_scan_heap(LVRelState *vacrel)

+			/*
@@ -1658,6 +1754,12 @@ lazy_scan_heap(LVRelState *vacrel)
 
 	read_stream_end(stream);
 
+	/*
@@ -1665,6 +1767,12 @@ lazy_scan_heap(LVRelState *vacrel)
 
+	/*
@@ -1677,6 +1785,10 @@ lazy_scan_heap(LVRelState *vacrel)
 
@@ -2594,6 +2706,7 @@ static void
 
 	/* Should not end up here with no indexes */
 	Assert(vacrel->nindexes > 0);
@@ -2606,6 +2719,9 @@ lazy_vacuum(LVRelState *vacrel)
 		return;
 	}
 
@@ -2662,6 +2778,14 @@ lazy_vacuum(LVRelState *vacrel)
 
+	/*
@@ -2678,11 +2802,21 @@ lazy_vacuum(LVRelState *vacrel)
@@ -3229,6 +3363,11 @@ lazy_vacuum_one_index(Relation indrel, IndexBulkDeleteResult *istat,
 
 	ivinfo.index = indrel;
@@ -3247,6 +3386,7 @@ lazy_vacuum_one_index(Relation indrel, IndexBulkDeleteResult *istat,
@@ -3255,6 +3395,15 @@ lazy_vacuum_one_index(Relation indrel, IndexBulkDeleteResult *istat,
 
@@ -3279,6 +3428,11 @@ lazy_cleanup_one_index(Relation indrel, IndexBulkDeleteResult *istat,
 
 	ivinfo.index = indrel;
@@ -3298,12 +3452,22 @@ lazy_cleanup_one_index(Relation indrel, IndexBulkDeleteResult *istat,

index 0205b9bb58c..b4187e5ad54 100644
@@ -1462,3 +1462,35 @@ FROM pg_class rel
 
 	/*

index 23cb62e36a7..f5f75aa4264 100644
@@ -1176,6 +1176,10 @@ pgstat_build_snapshot(PgStat_Kind statKind)
 		if (p->dropped)
 			continue;

 		stats_data = dsa_get_address(pgStatLocal.dsa, p->body);
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index ee0385cd809..9ee03509490 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -1016,6 +1016,9 @@ static void
 pgstat_accumulate_extvac_stats(ExtVacReport *dst, ExtVacReport *src,
 							   bool accumulate_reltype_specific_info)
 {
+	if(!pgstat_track_vacuum_statistics)
+		return;
+
 	dst->total_blks_read += src->total_blks_read;
 	dst->total_blks_hit += src->total_blks_hit;
 	dst->total_blks_dirtied += src->total_blks_dirtied;
@@ -1031,20 +1034,35 @@ pgstat_accumulate_extvac_stats(ExtVacReport *dst, ExtVacReport *src,
 	if (!accumulate_reltype_specific_info)
 		return;

 

 
index 115f0c51cc2..42f4cac5e0e 100644
@@ -1510,7 +1510,7 @@ struct config_bool ConfigureNamesBool[] =
index e2aaaf6cd59..558b313d0db 100644
@@ -12584,4 +12584,13 @@
 
 /*
 
+/*
 
index 6c88d57aef7..4def2c60d1d 100644

 
 /* ----------

 step s1_commit: COMMIT;

index 10a482e2db4..4e5e5ca54da 100644
@@ -2283,6 +2283,28 @@ pg_stat_user_tables| SELECT relid,
index ee0343c2729..0197830b5cd 100644
@@ -144,4 +144,5 @@ test: tablespace

From e19e46e20339b477fbcd4538f0b985386f9b7dfa Mon Sep 17 00:00:00 2001
From: Alena Rybakina <[email-masked]>
Date: Tue, 4 Feb 2025 17:57:44 +0300
Subject: [PATCH 3/4] Machinery for grabbing an extended vacuum statistics on
 databases.

index f42c700f6bb..1caf3ac3b36 100644

@@ -4080,6 +4080,9 @@ vacuum_error_callback(void *arg)
 	switch (errinfo->phase)
 	{
 		case VACUUM_ERRCB_PHASE_SCAN_HEAP:
+			if(geterrelevel() == ERROR)
+					pgstat_report_vacuum_error(errinfo->reloid, PGSTAT_EXTVAC_TABLE);
+
 			if (BlockNumberIsValid(errinfo->blkno))
 			{
 				if (OffsetNumberIsValid(errinfo->offnum))
@@ -4095,6 +4098,9 @@ vacuum_error_callback(void *arg)
 			break;
 
@@ -4110,16 +4116,25 @@ vacuum_error_callback(void *arg)
 			break;

 
index b4187e5ad54..0f8346d7b3c 100644
@@ -1493,4 +1493,29 @@ FROM
index f5f75aa4264..85557736a3a 100644

 /* ----------
  * state shared with pgstat_*.c
diff --git a/src/backend/utils/activity/pgstat_database.c b/src/backend/utils/activity/pgstat_database.c
index b31f20d41bc..65207d30378 100644
--- a/src/backend/utils/activity/pgstat_database.c
+++ b/src/backend/utils/activity/pgstat_database.c
@@ -485,6 +485,7 @@ pgstat_database_flush_cb(PgStat_EntryRef *entry_ref, bool nowait)
 	pgstat_unlock_entry(entry_ref);

 	return true;
 }
index 9ee03509490..1695680ea62 100644
 
+/* ---------
 
 /*
@@ -1030,6 +1073,8 @@ pgstat_accumulate_extvac_stats(ExtVacReport *dst, ExtVacReport *src,
 
@@ -1057,7 +1102,6 @@ pgstat_accumulate_extvac_stats(ExtVacReport *dst, ExtVacReport *src,

 
 
index 42f4cac5e0e..a24dec63f3a 100644
@@ -1514,7 +1514,7 @@ struct config_bool ConfigureNamesBool[] =
index 558b313d0db..4710bf997c4 100644
@@ -12585,12 +12585,21 @@
index 4def2c60d1d..f8158aa353c 100644

+	int32		errors;
 
 	/* ----------
 
 /*
 
 session s1
+setup		{
 
 session s2
+setup		{
index 4e5e5ca54da..f63f25f94d8 100644
@@ -2283,6 +2283,23 @@ pg_stat_user_tables| SELECT relid,

 DROP TABLE vestat;

index 0197830b5cd..fa2489716cc 100644
@@ -145,4 +145,4 @@ test: tablespace

 

 DROP TABLE vestat;

 

 

From d6ff4cdfc22e2721ff7d1c1e91e340976420e29e Mon Sep 17 00:00:00 2001

---

diff --git a/doc/src/sgml/system-views.sgml b/doc/src/sgml/system-views.sgml
index b58c52ea50f..7e5acd7c52e 100644
--- a/doc/src/sgml/system-views.sgml
+++ b/doc/src/sgml/system-views.sgml
@@ -5474,4 +5474,759 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx
   </table>
  </sect1>

index 1caf3ac3b36..8a328638fd3 100644

 
 void
 
@@ -1127,33 +1127,18 @@ heap_vacuum_rel(Relation rel, VacuumParams *params,

-	}
-	else
-	{
-		pgstat_report_vacuum(RelationGetRelid(rel),
-							 rel->rd_rel->relisshared,
-							 Max(vacrel->new_live_tuples, 0),
-							 vacrel->recently_dead_tuples +
-							 vacrel->missed_dead_tuples,
-							 starttime,
-							 NULL);
-	}
+	/* Make generic extended vacuum stats report and fill heap-specific extended stats fields */
+	extvac_stats_end(vacrel->rel, &extVacCounters, &(vacrel->extVacReport));
+	accumulate_heap_vacuum_statistics(&extVacCounters, vacrel);
+	pgstat_report_tab_vacuum_extstats(vacrel->reloid, rel->rd_rel->relisshared, &(vacrel->extVacReport));

@@ -3364,7 +3349,7 @@ lazy_vacuum_one_index(Relation indrel, IndexBulkDeleteResult *istat,
 	IndexVacuumInfo ivinfo;
 
@@ -3395,14 +3380,10 @@ lazy_vacuum_one_index(Relation indrel, IndexBulkDeleteResult *istat,
 
+	pgstat_report_tab_vacuum_extstats(vacrel->indoid, indrel->rd_rel->relisshared,
+										&extVacReport);
 
@@ -3429,7 +3410,7 @@ lazy_cleanup_one_index(Relation indrel, IndexBulkDeleteResult *istat,
 
 	/* Set initial statistics values to gather vacuum statistics for the index */
 	extvac_stats_start_idx(indrel, istat, &extVacCounters);
@@ -3459,14 +3440,10 @@ lazy_cleanup_one_index(Relation indrel, IndexBulkDeleteResult *istat,

+	pgstat_report_tab_vacuum_extstats(vacrel->indoid, indrel->rd_rel->relisshared,
+										&extVacReport);
 
 	/* Revert to the previous phase information for error traceback */
 	restore_vacuum_error_info(vacrel, &saved_err_info);
@@ -4081,7 +4058,7 @@ vacuum_error_callback(void *arg)
 	{
 		case VACUUM_ERRCB_PHASE_SCAN_HEAP:
 			if(geterrelevel() == ERROR)
-					pgstat_report_vacuum_error(errinfo->reloid, PGSTAT_EXTVAC_TABLE);
+					pgstat_report_vacuum_error();
 
 			if (BlockNumberIsValid(errinfo->blkno))
 			{
@@ -4099,7 +4076,7 @@ vacuum_error_callback(void *arg)
 
 		case VACUUM_ERRCB_PHASE_VACUUM_HEAP:
 			if(geterrelevel() == ERROR)
-				pgstat_report_vacuum_error(errinfo->reloid, PGSTAT_EXTVAC_TABLE);
+				pgstat_report_vacuum_error();
 
 			if (BlockNumberIsValid(errinfo->blkno))
 			{
@@ -4117,7 +4094,7 @@ vacuum_error_callback(void *arg)
 
 		case VACUUM_ERRCB_PHASE_VACUUM_INDEX:
 			if(geterrelevel() == ERROR)
-				pgstat_report_vacuum_error(errinfo->indoid, PGSTAT_EXTVAC_INDEX);
+				pgstat_report_vacuum_error();
 
 			errcontext("while vacuuming index \"%s\" of relation \"%s.%s\"",
 					   errinfo->indname, errinfo->relnamespace, errinfo->relname);
@@ -4125,7 +4102,7 @@ vacuum_error_callback(void *arg)
 
 		case VACUUM_ERRCB_PHASE_INDEX_CLEANUP:
 			if(geterrelevel() == ERROR)
-				pgstat_report_vacuum_error(errinfo->indoid, PGSTAT_EXTVAC_INDEX);
+				pgstat_report_vacuum_error();
 
 			errcontext("while cleaning up index \"%s\" of relation \"%s.%s\"",
 					   errinfo->indname, errinfo->relnamespace, errinfo->relname);
@@ -4133,7 +4110,7 @@ vacuum_error_callback(void *arg)
 
 		case VACUUM_ERRCB_PHASE_TRUNCATE:
 			if(geterrelevel() == ERROR)
-				pgstat_report_vacuum_error(errinfo->reloid, PGSTAT_EXTVAC_TABLE);
+				pgstat_report_vacuum_error();
 
index fbaed5359ad..72c8e339c45 100644

 	/*

 	/*
 
 	/*
index 000388a565f..9401e46d755 100644
 
 	/*
 	 * Update the pointer to the corresponding bulk-deletion result if someone
@@ -909,14 +909,10 @@ parallel_vacuum_process_one_index(ParallelVacuumState *pvs, Relation indrel,
 				 RelationGetRelationName(indrel));
 	}
 
-	if(pgstat_track_vacuum_statistics)
-	{
-		/* Make extended vacuum stats report for index */
-		extvac_stats_end_idx(indrel, istat_res, &extVacCounters, &extVacReport);
-		pgstat_report_vacuum(RelationGetRelid(indrel),
-								indrel->rd_rel->relisshared,
-								0, 0, 0, &extVacReport);
-	}
+	/* Make extended vacuum stats report for index */
+	extvac_stats_end_idx(indrel, istat_res, &extVacCounters, &extVacReport);
+	pgstat_report_tab_vacuum_extstats(RelationGetRelid(indrel), indrel->rd_rel->relisshared,
+										&extVacReport);
 
 	/*
index 85557736a3a..ca764a3a214 100644
 
 /*
index 65207d30378..80e6c7c229a 100644
 
+/*
@@ -485,7 +494,6 @@ pgstat_database_flush_cb(PgStat_EntryRef *entry_ref, bool nowait)

 	return true;
 }
index 1695680ea62..acc8f0b8a52 100644

 /*
 
-/* ---------

 /*
@@ -951,6 +904,12 @@ pgstat_relation_flush_cb(PgStat_EntryRef *entry_ref, bool nowait)
 	return true;
 }
 
+void
@@ -1053,60 +1012,4 @@ restore_truncdrop_counters(PgStat_TableXactStatus *trans)
index c2acdcf0e0e..423a256c83a 100644
@@ -2275,89 +2275,29 @@ pg_stat_get_vacuum_tables(PG_FUNCTION_ARGS)
 
+	PgStat_VacuumRelationCounts *pending;
 
-	/* Initialise attributes information in the tuple descriptor */
-	tupdesc = CreateTemplateTupleDesc(PG_STAT_GET_VACUUM_TABLES_STATS_COLS);
-
-	TupleDescInitEntry(tupdesc, (AttrNumber) ++i, "relid",
-					   INT4OID, -1, 0);
-	TupleDescInitEntry(tupdesc, (AttrNumber) ++i, "total_blks_read",
-					   INT8OID, -1, 0);
-	TupleDescInitEntry(tupdesc, (AttrNumber) ++i, "total_blks_hit",
-					   INT8OID, -1, 0);
-	TupleDescInitEntry(tupdesc, (AttrNumber) ++i, "total_blks_dirtied",
-					   INT8OID, -1, 0);
-	TupleDescInitEntry(tupdesc, (AttrNumber) ++i, "total_blks_written",
-					   INT8OID, -1, 0);
-	TupleDescInitEntry(tupdesc, (AttrNumber) ++i, "rel_blks_read",
-					   INT8OID, -1, 0);
-	TupleDescInitEntry(tupdesc, (AttrNumber) ++i, "rel_blks_hit",
-					   INT8OID, -1, 0);
-	TupleDescInitEntry(tupdesc, (AttrNumber) ++i, "pages_scanned",
-					   INT8OID, -1, 0);
-	TupleDescInitEntry(tupdesc, (AttrNumber) ++i, "pages_removed",
-					   INT8OID, -1, 0);
-	TupleDescInitEntry(tupdesc, (AttrNumber) ++i, "vm_new_frozen_pages",
-					   INT8OID, -1, 0);
-	TupleDescInitEntry(tupdesc, (AttrNumber) ++i, "vm_new_visible_pages",
-					   INT8OID, -1, 0);
-	TupleDescInitEntry(tupdesc, (AttrNumber) ++i, "vm_new_visible_frozen_pages",
-					   INT8OID, -1, 0);
-	TupleDescInitEntry(tupdesc, (AttrNumber) ++i, "missed_dead_pages",
-					   INT8OID, -1, 0);
-	TupleDescInitEntry(tupdesc, (AttrNumber) ++i, "tuples_deleted",
-					   INT8OID, -1, 0);
-	TupleDescInitEntry(tupdesc, (AttrNumber) ++i, "tuples_frozen",
-					   INT8OID, -1, 0);
-	TupleDescInitEntry(tupdesc, (AttrNumber) ++i, "recently_dead_tuples",
-					   INT8OID, -1, 0);
-	TupleDescInitEntry(tupdesc, (AttrNumber) ++i, "missed_dead_tuples",
-					   INT8OID, -1, 0);
-	TupleDescInitEntry(tupdesc, (AttrNumber) ++i, "wraparound_failsafe_count",
-					   INT4OID, -1, 0);
-	TupleDescInitEntry(tupdesc, (AttrNumber) ++i, "index_vacuum_count",
-					   INT8OID, -1, 0);
-	TupleDescInitEntry(tupdesc, (AttrNumber) ++i, "wal_records",
-					   INT8OID, -1, 0);
-	TupleDescInitEntry(tupdesc, (AttrNumber) ++i, "wal_fpi",
-					   INT8OID, -1, 0);
-	TupleDescInitEntry(tupdesc, (AttrNumber) ++i, "wal_bytes",
-					   NUMERICOID, -1, 0);
+	/* Build a tuple descriptor for our result type */
+	if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
+		elog(ERROR, "return type must be a row type");
 
-	TupleDescInitEntry(tupdesc, (AttrNumber) ++i, "blk_read_time",
-					   FLOAT8OID, -1, 0);
-	TupleDescInitEntry(tupdesc, (AttrNumber) ++i, "blk_write_time",
-					   FLOAT8OID, -1, 0);
-
-	TupleDescInitEntry(tupdesc, (AttrNumber) ++i, "delay_time",
-					   FLOAT8OID, -1, 0);
-	TupleDescInitEntry(tupdesc, (AttrNumber) ++i, "total_time",
-					   FLOAT8OID, -1, 0);
-
-	Assert(i == PG_STAT_GET_VACUUM_TABLES_STATS_COLS);
-
-	BlessTupleDesc(tupdesc);
+	pending = pgstat_fetch_stat_vacuum_tabentry(relid, MyDatabaseId);
 
+		extvacuum = pending;
 
 	i = 0;
 
@@ -2416,69 +2356,29 @@ pg_stat_get_vacuum_indexes(PG_FUNCTION_ARGS)
 	#define PG_STAT_GET_VACUUM_INDEX_STATS_COLS	16
 
 	Oid						relid = PG_GETARG_OID(0);
-	PgStat_StatTabEntry     *tabentry;
-	ExtVacReport 			*extvacuum;
+	PgStat_VacuumRelationCounts 			*extvacuum;
+	PgStat_VacuumRelationCounts *pending;
 	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;
-	ExtVacReport allzero;
-
-	/* Initialise attributes information in the tuple descriptor */
-	tupdesc = CreateTemplateTupleDesc(PG_STAT_GET_VACUUM_INDEX_STATS_COLS);
-
-	TupleDescInitEntry(tupdesc, (AttrNumber) ++i, "relid",
-					   INT4OID, -1, 0);
-	TupleDescInitEntry(tupdesc, (AttrNumber) ++i, "total_ blks_read",
-					   INT8OID, -1, 0);
-	TupleDescInitEntry(tupdesc, (AttrNumber) ++i, "total_blks_hit",
-					   INT8OID, -1, 0);
-	TupleDescInitEntry(tupdesc, (AttrNumber) ++i, "total_blks_dirtied",
-					   INT8OID, -1, 0);
-	TupleDescInitEntry(tupdesc, (AttrNumber) ++i, "total_blks_written",
-					   INT8OID, -1, 0);
-	TupleDescInitEntry(tupdesc, (AttrNumber) ++i, "rel_blks_read",
-					   INT8OID, -1, 0);
-	TupleDescInitEntry(tupdesc, (AttrNumber) ++i, "rel_blks_hit",
-					   INT8OID, -1, 0);
-	TupleDescInitEntry(tupdesc, (AttrNumber) ++i, "pages_deleted",
-					   INT8OID, -1, 0);
-	TupleDescInitEntry(tupdesc, (AttrNumber) ++i, "tuples_deleted",
-					   INT8OID, -1, 0);
-	TupleDescInitEntry(tupdesc, (AttrNumber) ++i, "wal_records",
-					   INT8OID, -1, 0);
-	TupleDescInitEntry(tupdesc, (AttrNumber) ++i, "wal_fpi",
-					   INT8OID, -1, 0);
-	TupleDescInitEntry(tupdesc, (AttrNumber) ++i, "wal_bytes",
-					   NUMERICOID, -1, 0);
+	PgStat_VacuumRelationCounts allzero;
 
-	TupleDescInitEntry(tupdesc, (AttrNumber) ++i, "blk_read_time",
-					   FLOAT8OID, -1, 0);
-	TupleDescInitEntry(tupdesc, (AttrNumber) ++i, "blk_write_time",
-					   FLOAT8OID, -1, 0);
+	/* Build a tuple descriptor for our result type */
+	if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
+		elog(ERROR, "return type must be a row type");
 
-	TupleDescInitEntry(tupdesc, (AttrNumber) ++i, "delay_time",
-					   FLOAT8OID, -1, 0);
-	TupleDescInitEntry(tupdesc, (AttrNumber) ++i, "total_time",
-					   FLOAT8OID, -1, 0);
+	pending = pgstat_fetch_stat_vacuum_tabentry(relid, MyDatabaseId);
 
-	Assert(i == PG_STAT_GET_VACUUM_INDEX_STATS_COLS);
-
-	BlessTupleDesc(tupdesc);
-
-	tabentry = pgstat_fetch_stat_tabentry(relid);
-
-	if (tabentry == NULL)
+	if (pending == NULL)
 	{
 		/* If the subscription is not found, initialise its stats */
-		memset(&allzero, 0, sizeof(ExtVacReport));
+		memset(&allzero, 0, sizeof(PgStat_VacuumRelationCounts));
 		extvacuum = &allzero;
 	}
 	else
-	{
-		extvacuum = &(tabentry->vacuum_ext);
-	}
+		extvacuum = pending;
 
 	i = 0;
 
@@ -2523,67 +2423,29 @@ pg_stat_get_vacuum_database(PG_FUNCTION_ARGS)
 	#define PG_STAT_GET_VACUUM_DATABASE_STATS_COLS	14
 
 	Oid						 dbid = PG_GETARG_OID(0);
-	PgStat_StatDBEntry 		*dbentry;
-	ExtVacReport 			*extvacuum;
+	PgStat_VacuumDBCounts	*extvacuum;
+	PgStat_VacuumDBCounts	*pending;
 	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;
-	ExtVacReport allzero;
-
-	/* Initialise attributes information in the tuple descriptor */
-	tupdesc = CreateTemplateTupleDesc(PG_STAT_GET_VACUUM_DATABASE_STATS_COLS);
-
-	TupleDescInitEntry(tupdesc, (AttrNumber) ++i, "dbid",
-					   INT4OID, -1, 0);
-	TupleDescInitEntry(tupdesc, (AttrNumber) ++i, "total_ blks_read",
-					   INT8OID, -1, 0);
-	TupleDescInitEntry(tupdesc, (AttrNumber) ++i, "total_blks_hit",
-					   INT8OID, -1, 0);
-	TupleDescInitEntry(tupdesc, (AttrNumber) ++i, "total_blks_dirtied",
-					   INT8OID, -1, 0);
-	TupleDescInitEntry(tupdesc, (AttrNumber) ++i, "total_blks_written",
-					   INT8OID, -1, 0);
-	TupleDescInitEntry(tupdesc, (AttrNumber) ++i, "wal_records",
-					   INT8OID, -1, 0);
-	TupleDescInitEntry(tupdesc, (AttrNumber) ++i, "wal_fpi",
-					   INT8OID, -1, 0);
-	TupleDescInitEntry(tupdesc, (AttrNumber) ++i, "wal_bytes",
-					   NUMERICOID, -1, 0);
+	PgStat_VacuumDBCounts allzero;
 
-	TupleDescInitEntry(tupdesc, (AttrNumber) ++i, "blk_read_time",
-					   FLOAT8OID, -1, 0);
-	TupleDescInitEntry(tupdesc, (AttrNumber) ++i, "blk_write_time",
-					   FLOAT8OID, -1, 0);
+	/* Build a tuple descriptor for our result type */
+	if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
+		elog(ERROR, "return type must be a row type");
 
-	TupleDescInitEntry(tupdesc, (AttrNumber) ++i, "delay_time",
-					   FLOAT8OID, -1, 0);
-	TupleDescInitEntry(tupdesc, (AttrNumber) ++i, "total_time",
-					   FLOAT8OID, -1, 0);
-	TupleDescInitEntry(tupdesc, (AttrNumber) ++i, "wraparound_failsafe_count",
-					   INT4OID, -1, 0);
-	TupleDescInitEntry(tupdesc, (AttrNumber) ++i, "errors",
-					   INT4OID, -1, 0);
+	pending = pgstat_fetch_stat_vacuum_dbentry(dbid);
 
-	Assert(i == PG_STAT_GET_VACUUM_DATABASE_STATS_COLS);
-
-	BlessTupleDesc(tupdesc);
-
-	dbentry = pgstat_fetch_stat_dbentry(dbid);
-
-	if (dbentry == NULL)
+	if (pending == NULL)
 	{
 		/* If the subscription is not found, initialise its stats */
-		memset(&allzero, 0, sizeof(ExtVacReport));
+		memset(&allzero, 0, sizeof(PgStat_VacuumRelationCounts));
 		extvacuum = &allzero;
 	}
 	else
-	{
-		extvacuum = &(dbentry->vacuum_ext);
-	}
-
-	i = 0;
+		extvacuum = pending;
 
 	values[i++] = ObjectIdGetDatum(dbid);
 
diff --git a/src/include/commands/vacuum.h b/src/include/commands/vacuum.h
index fb134f3402e..f895151ca09 100644
--- a/src/include/commands/vacuum.h
+++ b/src/include/commands/vacuum.h
@@ -432,5 +432,5 @@ extern double anl_get_next_S(double t, int n, double *stateptr);
 extern void extvac_stats_start_idx(Relation rel, IndexBulkDeleteResult *stats,
 					   LVExtStatCountersIdx *counters);
 extern void extvac_stats_end_idx(Relation rel, IndexBulkDeleteResult *stats,
-					 LVExtStatCountersIdx *counters, ExtVacReport *report);
+					 LVExtStatCountersIdx *counters, PgStat_VacuumRelationCounts *report);
 #endif							/* VACUUM_H */
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index f8158aa353c..f57a96d3aa2 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -116,12 +116,60 @@ typedef enum ExtVacReportType
 {
 	PGSTAT_EXTVAC_INVALID = 0,
 	PGSTAT_EXTVAC_TABLE = 1,
-	PGSTAT_EXTVAC_INDEX = 2
+	PGSTAT_EXTVAC_INDEX = 2,
+	PGSTAT_EXTVAC_DB = 3,
 } ExtVacReportType;
 
 /* ----------
@@ -129,7 +177,7 @@ typedef enum ExtVacReportType
@@ -154,7 +202,6 @@ typedef struct ExtVacReport

-	int32		errors;
 
 	ExtVacReportType type;		/* heap, index, etc. */
@@ -192,61 +239,44 @@ typedef struct ExtVacReport
 			int64		pages_deleted;		/* number of pages deleted by vacuum */
 		}			index;
 	} /* per_type_stats */;
-} ExtVacReport;
+} PgStat_VacuumRelationCounts;
 
-/* ----------

 

 
-	/*
 
 /* ----------
  * PgStat_TableStatus			Per-table status within a backend
@@ -272,6 +302,12 @@ typedef struct PgStat_TableStatus
 	Relation	relation;		/* rel that is using this entry */
 } PgStat_TableStatus;
 
@@ -468,8 +504,6 @@ typedef struct PgStat_StatDBEntry

 typedef struct PgStat_StatFuncEntry
@@ -551,8 +585,6 @@ typedef struct PgStat_StatTabEntry

 /* ------
@@ -760,11 +792,11 @@ extern void pgstat_unlink_relation(Relation rel);
 
 extern void pgstat_report_vacuum(Oid tableoid, bool shared,
 								 PgStat_Counter livetuples, PgStat_Counter deadtuples,
-								 TimestampTz starttime, ExtVacReport *params);
+								 TimestampTz starttime);
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
-extern void pgstat_report_vacuum_error(Oid tableoid, ExtVacReportType m_type);
+extern void pgstat_report_vacuum_error();
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
@@ -895,6 +927,15 @@ extern int	pgstat_get_transactional_drops(bool isCommit, struct xl_xact_stats_it
 extern void pgstat_execute_transactional_drops(int ndrops, struct xl_xact_stats_item *items, bool is_redo);

+extern PgStat_VacuumRelationCounts *pgstat_fetch_stat_vacuum_tabentry(Oid relid, Oid dbid);
+PgStat_VacuumDBCounts *pgstat_fetch_stat_vacuum_dbentry(Oid dbid);

 
 /*

 /* Custom stats kinds */

---

From: Amit Kapila <[email-masked]>
Date: Mon, 12 May 2025 18:00:03 +0530

On Fri, May 9, 2025 at 5:34 PM Alena Rybakina <[email-masked]> wrote:
>
> I did a rebase and finished the part with storing statistics separately from the relation statistics - now it is possible to disable the collection of statistics for relationsh using gucs and

I think this patch is trying to collect data similar to what we do for
pg_stat_statements for SQL statements. So, can't we follow a similar
idea such that these additional statistics will be collected once some
external module like pg_stat_statements is enabled? That module should
be responsible for accumulating and resetting the data, so we won't
have this memory consumption issue.

BTW, how will these new statistics be used to autotune a vacuum? And
do we need all the statistics proposed by this patch?

-- 
With Regards,
Amit Kapila.

---

From: Alena Rybakina <[email-masked]>
Date: Tue, 13 May 2025 05:49:02 -0400

Hi!

> have this memory consumption issue.
The idea is good, it will require one hook for the pgstat_report_vacuum 
function, the extvac_stats_start and extvac_stats_end functions can be 
run if the extension is loaded, so as not to add more hooks.
But I see a problem here with tracking deleted objects for which 
statistics are no longer needed. There are two solutions to this and I 
don't like both of them, to be honest.
The first way is to add a background process that will go through the 
table with saved statistics and check whether the relation or the 
database are relevant now or not and if not, then
delete the vacuum statistics information for it. This may be 
resource-intensive. The second way is to add hooks for deleting the 
database and relationships (functions dropdb, index_drop, 
heap_drop_with_catalog).
> BTW, how will these new statistics be used to autotune a vacuum?
yes, but they are collected on demand - by guc.
> And
Regarding this issue, it was discussed here and so far we have come to 
the conclusion that statistics are needed for a deep understanding of 
the work of vacuum statistics [0] [1] [2].

[0] 
https://www.postgresql.org/message-id/0B6CBF4C-CC2A-4200-9126-CE3A390D938B%40upgrade.com

[1] 
https://www.postgresql.org/message-id/6732acf8ce0f31025b535ae1a64568750924a887.camel%40moonset.ru

[2] 
https://www.postgresql.org/message-id/5AA8FFD5-6DE2-4A31-8E00-AE98F738F5D1%40upgrade.com

-- 
Regards,
Alena Rybakina
Postgres Professional

---

From: Amit Kapila <[email-masked]>
Date: Wed, 14 May 2025 14:25:05 +0530

> database and relationships (functions dropdb, index_drop,

How does pg_stat_io manages this? I mean how it removes objects that
are dropped? Does some background task removes it?

> the work of vacuum statistics [0] [1] [2].

I haven't gone through the emails, but my opinion is to break the
number of stats into some important subset of stats first and then
keep enhancing it. Right now, the patch struggles with two concerns:
one is what the design should be to capture the required stats, and
the second is convincing ourselves whether we need all the stats it is
trying to expose. Breaking into a smaller subset of stats could
alleviate the second concern.

-- 
With Regards,
Amit Kapila.

---

From: Alexander Korotkov <[email-masked]>
Date: Mon, 2 Jun 2025 19:25:28 +0300

> run if the extension is loaded, so as not to add more hooks.

+1
Nice idea of a hook.  Given the volume of the patch, it might be a
good idea to keep this as an extension.

> database and relationships (functions dropdb, index_drop,

Can we workaround this with object_access_hook?

------
Regards,
Alexander Korotkov
Supabase

---

From: Alena Rybakina <[email-masked]>
Date: Mon, 2 Jun 2025 19:50:42 +0300

> good idea to keep this as an extension.
Okay, I'll realize it and apply the patch)
> Can we workaround this with object_access_hook?

I think this could fix the problem. For the OAT-DROP access type, we can 
call a function to reset the vacuum statistics for relations that are 
about to be dropped.

At the moment, I don’t see any limitations to using this approach.

-- 
Regards,
Alena Rybakina
Postgres Professional

---

From: Alena Rybakina <[email-masked]>
Date: Mon, 2 Jun 2025 22:56:41 +0300

> good idea to keep this as an extension.

Today, I finalized the vacuum statistics separation approach and 
refactored the vacuum statistics structures (patch 4).

I also reworked the table statistics to avoid mixing index statistics in 
parallel vacuum mode (patch 2).

The new approach excludes buffer usage and WAL statistics for indexes 
from the table’s statistics.
For timing, if vacuuming is sequential, the total time spent on all 
indexes is subtracted from the table’s total vacuum time by adding up 
the individual index vacuum times. If vacuuming is parallel, the total 
index vacuum time is subtracted as a whole.

static void
accumulate_idxs_vacuum_statistics(LVRelState *vacrel, ExtVacReport 
*extVacIdxStats)
{
     if (!pgstat_track_vacuum_statistics)
         return;

     /* Fill heap-specific extended stats fields */
     vacrel->extVacReportIdx.blk_read_time += extVacIdxStats->blk_read_time;
     vacrel->extVacReportIdx.blk_write_time += 
extVacIdxStats->blk_write_time;
     vacrel->extVacReportIdx.total_blks_dirtied += 
extVacIdxStats->total_blks_dirtied;
     vacrel->extVacReportIdx.total_blks_hit += 
extVacIdxStats->total_blks_hit;
     vacrel->extVacReportIdx.total_blks_read += 
extVacIdxStats->total_blks_read;
     vacrel->extVacReportIdx.total_blks_written += 
extVacIdxStats->total_blks_written;
     vacrel->extVacReportIdx.wal_bytes += extVacIdxStats->wal_bytes;
     vacrel->extVacReportIdx.wal_fpi += extVacIdxStats->wal_fpi;
     vacrel->extVacReportIdx.wal_records += extVacIdxStats->wal_records;
     vacrel->extVacReportIdx.delay_time += extVacIdxStats->delay_time;

     vacrel->extVacReportIdx.total_time += extVacIdxStats->total_time;

}

if (ParallelVacuumIsActive(vacrel))
{
     LVExtStatCounters counters;
     ExtVacReport extVacReport;

     memset(&extVacReport, 0, sizeof(ExtVacReport));

     extvac_stats_start(vacrel->rel, &counters);

     /* Outsource everything to parallel variant */
     parallel_vacuum_bulkdel_all_indexes(vacrel->pvs, old_live_tuples,
vacrel->num_index_scans);

     extvac_stats_end(vacrel->rel, &counters, &extVacReport);
     accumulate_idxs_vacuum_statistics(vacrel, &extVacReport);
}

Currently, database statistics work incorrectly — I'm investigating the 
issue.

In parallel, I'm starting work on the extension.

-- 
Regards,
Alena Rybakina
Postgres Professional
From 6c52152b3d96d4b7dc2de2692b116af20be67dca Mon Sep 17 00:00:00 2001
From: Alena Rybakina <[email-masked]>
Date: Thu, 8 May 2025 21:14:23 +0300
Subject: [PATCH 1/5] Machinery for grabbing an extended vacuum statistics on
 table relations.

index 708674d8fcf..ee27e70a798 100644

+/*

+/* ----------

 /*

 
 	/*

 	if (instrument)
@@ -2977,6 +3122,7 @@ lazy_check_wraparound_failsafe(LVRelState *vacrel)
 		int64		progress_val[2] = {0, 0};

 		/*
 
 	if (map[mapByte] & mask)
 	{
+		/*
 
 		MarkBufferDirty(vmbuf);
index 08f780a2e63..47d27314b55 100644
@@ -708,7 +708,9 @@ CREATE VIEW pg_stat_all_tables AS
@@ -1407,3 +1409,51 @@ REVOKE ALL ON pg_aios FROM PUBLIC;

 		VacuumCostBalance = 0;
 
 		/*
index 0feea1d30ec..2b55d9b7c0e 100644

 
 
 /* ----------
 
-
 /*
 
-

 

 static void

 /*

index e980109f245..a5610199893 100644

 
+/*
 
 /*
index 87ce76b18f4..e971b390281 100644
@@ -661,6 +661,7 @@

 # - Monitoring -
index 37a484147a8..c04d3880241 100644
@@ -12556,4 +12556,22 @@
 
+{ oid => '8001',

+/* ----------
+ *
+ * ExtVacReport
+ *

 /* ----------

 

 /* ------

 /*

 /*----------

From 4e19e818679ae56608a0bc087fdc463b3d2183fb Mon Sep 17 00:00:00 2001
From: Alena Rybakina <[email-masked]>
Date: Mon, 2 Jun 2025 19:35:02 +0300
Subject: [PATCH 2/5] Machinery for grabbing an extended vacuum statistics on
 index relations.

Authors: Alena Rybakina <[email-masked]>,
   Andrei Lepikhov <[email-masked]>,
   Andrei Zubkov <[email-masked]>
Reviewed-by: Dilip Kumar <[email-masked]>, Masahiko Sawada <[email-masked]>,
       Ilia Evdokimov <[email-masked]>, jian he <[email-masked]>,
       Kirill Reshke <[email-masked]>, Alexander Korotkov <[email-masked]>,
       Jim Nasby <[email-masked]>, Sami Imseih <[email-masked]>
---
 src/backend/access/heap/vacuumlazy.c          | 270 ++++++++++++++----
 src/backend/catalog/system_views.sql          |  32 +++
 src/backend/commands/vacuumparallel.c         |  14 +
 src/backend/utils/activity/pgstat.c           |   4 +
 src/backend/utils/activity/pgstat_relation.c  |  48 +++-
 src/backend/utils/adt/pgstatfuncs.c           | 133 ++++++++-
 src/backend/utils/misc/guc_tables.c           |   2 +-
 src/include/catalog/pg_proc.dat               |   9 +
 src/include/commands/vacuum.h                 |  25 ++
 src/include/pgstat.h                          |  58 +++-
 .../vacuum-extending-in-repetable-read.out    |   4 +-
 src/test/regress/expected/rules.out           |  22 ++
 .../expected/vacuum_index_statistics.out      | 183 ++++++++++++
 src/test/regress/parallel_schedule            |   1 +
 .../regress/sql/vacuum_index_statistics.sql   | 151 ++++++++++
 15 files changed, 864 insertions(+), 92 deletions(-)
 create mode 100644 src/test/regress/expected/vacuum_index_statistics.out
 create mode 100644 src/test/regress/sql/vacuum_index_statistics.sql

index ee27e70a798..0888be2afea 100644
 
 	int32		wraparound_failsafe_count; /* number of emergency vacuums to prevent anti-wraparound shutdown */
+
+	ExtVacReport extVacReportIdx;
 } LVRelState;

-/*

 

 	if (!rel->pgstat_info || !pgstat_track_counts)
 		/*
@@ -585,12 +573,131 @@ extvac_stats_end(Relation rel, LVExtStatCounters *counters,
 		 */
 		return;
 
-	report->blks_fetched =
 
+void
+extvac_stats_start_idx(Relation rel, IndexBulkDeleteResult *stats,
+					   LVExtStatCountersIdx *counters)
+{
+	if(!pgstat_track_vacuum_statistics)
+		return;
+
+	/* Set initial values for common heap and index statistics*/
+	extvac_stats_start(rel, &counters->common);
+	counters->pages_deleted = counters->tuples_removed = 0;
+
+	if (stats != NULL)
+	{
+		/*
+		 * XXX: Why do we need this code here? If it is needed, I feel lack of
+		 * comments, describing the reason.
+		 */
+		counters->tuples_removed = stats->tuples_removed;
+		counters->pages_deleted = stats->pages_deleted;
+	}
+}
+
+void
+extvac_stats_end_idx(Relation rel, IndexBulkDeleteResult *stats,
+					 LVExtStatCountersIdx *counters, ExtVacReport *report)
+{
+	memset(report, 0, sizeof(ExtVacReport));
+
+	extvac_stats_end(rel, &counters->common, report);
+	report->type = PGSTAT_EXTVAC_INDEX;
+
+	if (stats != NULL)
+	{
+		/*
+		 * if something goes wrong or an user doesn't want to track a database
+		 * activity - just suppress it.
+		 */
+
+		/* Fill index-specific extended stats fields */
+		report->tuples_deleted =
+							stats->tuples_removed - counters->tuples_removed;
+		report->index.pages_deleted =
+							stats->pages_deleted - counters->pages_deleted;
+	}
+}
+
+/* Accumulate vacuum statistics for heap.
+ *
+  * Because of complexity of vacuum processing: it switch procesing between
+  * the heap relation to index relations and visa versa, we need to store
+  * gathered statistics information for heap relations several times before
+  * the vacuum starts processing the indexes again.
+  *
+  * It is necessary to gather correct statistics information for heap and indexes
+  * otherwice the index statistics information would be added to his parent heap
+  * statistics information and it would be difficult to analyze it later.
+  *
+  * We can't subtract union vacuum statistics information for index from the heap relations
+  * because of total and delay time time statistics collecting during parallel vacuum
+  * procudure.
+*/
+static void
+accumulate_heap_vacuum_statistics(LVRelState *vacrel, ExtVacReport *extVacStats)
+{
+	if (!pgstat_track_vacuum_statistics)
+		return;
+
+	/* Fill heap-specific extended stats fields */
+	extVacStats->type = PGSTAT_EXTVAC_TABLE;
+	extVacStats->table.pages_scanned = vacrel->scanned_pages;
+	extVacStats->table.pages_removed = vacrel->removed_pages;
+	extVacStats->table.vm_new_frozen_pages = vacrel->vm_new_frozen_pages;
+	extVacStats->table.vm_new_visible_pages = vacrel->vm_new_visible_pages;
+	extVacStats->table.vm_new_visible_frozen_pages = vacrel->vm_new_visible_frozen_pages;
+	extVacStats->tuples_deleted = vacrel->tuples_deleted;
+	extVacStats->table.tuples_frozen = vacrel->tuples_frozen;
+	extVacStats->table.recently_dead_tuples = vacrel->recently_dead_tuples;
+	extVacStats->table.recently_dead_tuples = vacrel->recently_dead_tuples;
+	extVacStats->table.missed_dead_tuples = vacrel->missed_dead_tuples;
+	extVacStats->table.missed_dead_pages = vacrel->missed_dead_pages;
+	extVacStats->table.index_vacuum_count = vacrel->num_index_scans;
+	extVacStats->table.wraparound_failsafe_count = vacrel->wraparound_failsafe_count;
+
+	extVacStats->blk_read_time -= vacrel->extVacReportIdx.blk_read_time;
+	extVacStats->blk_write_time -= vacrel->extVacReportIdx.blk_write_time;
+	extVacStats->total_blks_dirtied -= vacrel->extVacReportIdx.total_blks_dirtied;
+	extVacStats->total_blks_hit -= vacrel->extVacReportIdx.total_blks_hit;
+	extVacStats->total_blks_read -= vacrel->extVacReportIdx.total_blks_read;
+	extVacStats->total_blks_written -= vacrel->extVacReportIdx.total_blks_written;
+	extVacStats->wal_bytes -= vacrel->extVacReportIdx.wal_bytes;
+	extVacStats->wal_fpi -= vacrel->extVacReportIdx.wal_fpi;
+	extVacStats->wal_records -= vacrel->extVacReportIdx.wal_records;
+
+	extVacStats->total_time -= vacrel->extVacReportIdx.total_time;
+	extVacStats->delay_time -= vacrel->extVacReportIdx.delay_time;
+
+}
+
+static void
+accumulate_idxs_vacuum_statistics(LVRelState *vacrel, ExtVacReport *extVacIdxStats)
+{
+	if (!pgstat_track_vacuum_statistics)
+		return;
+
+	/* Fill heap-specific extended stats fields */
+	vacrel->extVacReportIdx.blk_read_time += extVacIdxStats->blk_read_time;
+	vacrel->extVacReportIdx.blk_write_time += extVacIdxStats->blk_write_time;
+	vacrel->extVacReportIdx.total_blks_dirtied += extVacIdxStats->total_blks_dirtied;
+	vacrel->extVacReportIdx.total_blks_hit += extVacIdxStats->total_blks_hit;
+	vacrel->extVacReportIdx.total_blks_read += extVacIdxStats->total_blks_read;
+	vacrel->extVacReportIdx.total_blks_written += extVacIdxStats->total_blks_written;
+	vacrel->extVacReportIdx.wal_bytes += extVacIdxStats->wal_bytes;
+	vacrel->extVacReportIdx.wal_fpi += extVacIdxStats->wal_fpi;
+	vacrel->extVacReportIdx.wal_records += extVacIdxStats->wal_records;
+	vacrel->extVacReportIdx.delay_time += extVacIdxStats->delay_time;
+
+	vacrel->extVacReportIdx.total_time += extVacIdxStats->total_time;
+}
+
 
 /*
  * Helper to set up the eager scanning state for vacuuming a single relation.
@@ -750,11 +857,9 @@ heap_vacuum_rel(Relation rel, VacuumParams *params,
 	LVExtStatCounters extVacCounters;
 	ExtVacReport extVacReport;
 	char	  **indnames = NULL;
-	ExtVacReport allzero;
 
 	/* Initialize vacuum statistics */
-	memset(&allzero, 0, sizeof(ExtVacReport));
-	extVacReport = allzero;
+	memset(&extVacReport, 0, sizeof(ExtVacReport));
 
 	verbose = (params->options & VACOPT_VERBOSE) != 0;
 	instrument = (verbose || (AmAutoVacuumWorkerProcess() &&
@@ -800,6 +905,8 @@ heap_vacuum_rel(Relation rel, VacuumParams *params,
 	errcallback.previous = error_context_stack;
 	error_context_stack = &errcallback;
 
+	memset(&vacrel->extVacReportIdx, 0, sizeof(ExtVacReport));
+
 	/* Set up high level stuff about rel and its indexes */
 	vacrel->rel = rel;
 	vac_open_indexes(vacrel->rel, RowExclusiveLock, &vacrel->nindexes,
@@ -1051,23 +1158,6 @@ heap_vacuum_rel(Relation rel, VacuumParams *params,
 	/* Make generic extended vacuum stats report */
 	extvac_stats_end(rel, &extVacCounters, &extVacReport);
 
@@ -1078,13 +1168,34 @@ heap_vacuum_rel(Relation rel, VacuumParams *params,
+		extvac_stats_end(vacrel->rel, &extVacCounters, &extVacReport);
+		accumulate_heap_vacuum_statistics(vacrel, &extVacReport);
 						 &extVacReport);
 
 	if (instrument)
@@ -2781,10 +2892,20 @@ lazy_vacuum_all_indexes(LVRelState *vacrel)
 	}
 	else
 	{
+		LVExtStatCounters counters;
+		ExtVacReport extVacReport;
+
+		memset(&extVacReport, 0, sizeof(ExtVacReport));
+
+		extvac_stats_start(vacrel->rel, &counters);
+
 		/* Outsource everything to parallel variant */
 		parallel_vacuum_bulkdel_all_indexes(vacrel->pvs, old_live_tuples,
 											vacrel->num_index_scans);
 
+		extvac_stats_end(vacrel->rel, &counters, &extVacReport);
+		accumulate_idxs_vacuum_statistics(vacrel, &extVacReport);
+
 		/*
 		 * Do a postcheck to consider applying wraparound failsafe now.  Note
 		 * that parallel VACUUM only gets the precheck and this postcheck.
@@ -3205,10 +3326,20 @@ lazy_cleanup_all_indexes(LVRelState *vacrel)
 	}
 	else
 	{
+		LVExtStatCounters counters;
+		ExtVacReport extVacReport;
+
+		memset(&extVacReport, 0, sizeof(ExtVacReport));
+
+		extvac_stats_start(vacrel->rel, &counters);
+
 		/* Outsource everything to parallel variant */
 		parallel_vacuum_cleanup_all_indexes(vacrel->pvs, reltuples,
 											vacrel->num_index_scans,
 											estimated_count);
+
+		extvac_stats_end(vacrel->rel, &counters, &extVacReport);
+		accumulate_idxs_vacuum_statistics(vacrel, &extVacReport);
 	}
 
 	/* Reset the progress counters */
@@ -3234,6 +3365,11 @@ lazy_vacuum_one_index(Relation indrel, IndexBulkDeleteResult *istat,
 {
 	IndexVacuumInfo ivinfo;
 	LVSavedErrInfo saved_err_info;
+	LVExtStatCountersIdx extVacCounters;
+	ExtVacReport extVacReport;
+
+	/* Set initial statistics values to gather vacuum statistics for the index */
+	extvac_stats_start_idx(indrel, istat, &extVacCounters);
 
 	ivinfo.index = indrel;
@@ -3252,6 +3388,7 @@ lazy_vacuum_one_index(Relation indrel, IndexBulkDeleteResult *istat,
@@ -3260,6 +3397,19 @@ lazy_vacuum_one_index(Relation indrel, IndexBulkDeleteResult *istat,
 
+		if (!ParallelVacuumIsActive(vacrel))
+			accumulate_idxs_vacuum_statistics(vacrel, &extVacReport);
@@ -3284,6 +3434,11 @@ lazy_cleanup_one_index(Relation indrel, IndexBulkDeleteResult *istat,
 
 	ivinfo.index = indrel;
@@ -3303,12 +3458,25 @@ lazy_cleanup_one_index(Relation indrel, IndexBulkDeleteResult *istat,

+		if (!ParallelVacuumIsActive(vacrel))
+			accumulate_idxs_vacuum_statistics(vacrel, &extVacReport);
index 47d27314b55..83d55e78606 100644
@@ -1457,3 +1457,35 @@ FROM pg_class rel
index 2b55d9b7c0e..65de45a4447 100644
 
 	/*

 

 
index a5610199893..482929b75e9 100644

 
 
index c04d3880241..8c77ae96100 100644
@@ -12574,4 +12574,13 @@
 
 /*
 
+/*

 
 
 /* ----------

 step s1_commit: COMMIT;

From 2978fa59fc553b1a711731ae83159e4f44241dee Mon Sep 17 00:00:00 2001
From: Alena Rybakina <[email-masked]>
Date: Tue, 4 Feb 2025 17:57:44 +0300
Subject: [PATCH 3/5] Machinery for grabbing an extended vacuum statistics on
 databases.

diff --git a/src/backend/access/heap/vacuumlazy.c b/src/backend/access/heap/vacuumlazy.c
index 0888be2afea..3d72f74b05e 100644
--- a/src/backend/access/heap/vacuumlazy.c
+++ b/src/backend/access/heap/vacuumlazy.c
@@ -660,7 +660,7 @@ accumulate_heap_vacuum_statistics(LVRelState *vacrel, ExtVacReport *extVacStats)
 	extVacStats->table.missed_dead_tuples = vacrel->missed_dead_tuples;
 	extVacStats->table.missed_dead_pages = vacrel->missed_dead_pages;
 	extVacStats->table.index_vacuum_count = vacrel->num_index_scans;
-	extVacStats->table.wraparound_failsafe_count = vacrel->wraparound_failsafe_count;
+	extVacStats->wraparound_failsafe_count = vacrel->wraparound_failsafe_count;
 
 	extVacStats->blk_read_time -= vacrel->extVacReportIdx.blk_read_time;
 	extVacStats->blk_write_time -= vacrel->extVacReportIdx.blk_write_time;
@@ -4089,6 +4089,9 @@ vacuum_error_callback(void *arg)
 	switch (errinfo->phase)
 	{
 		case VACUUM_ERRCB_PHASE_SCAN_HEAP:
+			if(geterrelevel() == ERROR)
+					pgstat_report_vacuum_error(errinfo->reloid, PGSTAT_EXTVAC_TABLE);
+
 			if (BlockNumberIsValid(errinfo->blkno))
 			{
 				if (OffsetNumberIsValid(errinfo->offnum))
@@ -4104,6 +4107,9 @@ vacuum_error_callback(void *arg)
 			break;
 
@@ -4119,16 +4125,25 @@ vacuum_error_callback(void *arg)
 			break;

 
index 83d55e78606..0ae31b87989 100644
@@ -1488,4 +1488,29 @@ FROM

 /* ----------

 	return true;
 }
 
+/* ---------
 
 /*
 
index 482929b75e9..a2ece2c36cf 100644

 
 
index 8c77ae96100..4e1e29eec7e 100644
@@ -12575,12 +12575,21 @@

+	int32		errors;
 
 	/* ----------
 
 /*
 
 session s1
+setup		{
 
 session s2
+setup		{

 DROP TABLE vestat;

 

 
 
 DROP TABLE vestat;

 

 

From e2da3e08e08f3c1edc12160a237f15516e506270 Mon Sep 17 00:00:00 2001
From: Alena Rybakina <[email-masked]>
Date: Mon, 2 Jun 2025 22:24:38 +0300
Subject: [PATCH 4/5] Vacuum statistics have been separated from regular
 relation and database statistics to reduce memory usage. Dedicated
 PGSTAT_KIND_VACUUM_RELATION and PGSTAT_KIND_VACUUM_DB entries were added to
 the stats collector to efficiently allocate memory for vacuum-specific
 metrics, which require significantly more space per relation.

---
 src/backend/access/heap/vacuumlazy.c         | 168 +++++------
 src/backend/catalog/heap.c                   |   1 +
 src/backend/catalog/index.c                  |   1 +
 src/backend/commands/dbcommands.c            |   1 +
 src/backend/commands/vacuumparallel.c        |  14 +-
 src/backend/utils/activity/Makefile          |   1 +
 src/backend/utils/activity/pgstat.c          |  28 ++
 src/backend/utils/activity/pgstat_database.c |  10 +-
 src/backend/utils/activity/pgstat_relation.c | 111 +-------
 src/backend/utils/activity/pgstat_vacuum.c   | 224 +++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 285 +++++--------------
 src/include/commands/vacuum.h                |   2 +-
 src/include/pgstat.h                         | 200 +++++++------
 src/include/utils/pgstat_internal.h          |  15 +
 src/include/utils/pgstat_kind.h              |   4 +-
 15 files changed, 569 insertions(+), 496 deletions(-)
 create mode 100644 src/backend/utils/activity/pgstat_vacuum.c

index 3d72f74b05e..24b6b64c3fc 100644

-	ExtVacReport extVacReportIdx;
+	PgStat_VacuumRelationCounts extVacReportIdx;
 } LVRelState;

@@ -525,7 +525,7 @@ extvac_stats_start(Relation rel, LVExtStatCounters *counters)
  */
 static void
 extvac_stats_end(Relation rel, LVExtStatCounters *counters,
-				  ExtVacReport *report)
+				 PgStat_VacuumRelationCounts *report)
 {
 	WalUsage	walusage;
 	BufferUsage	bufusage;
@@ -549,22 +549,22 @@ extvac_stats_end(Relation rel, LVExtStatCounters *counters,
 	/*
 	 * Fill additional statistics on a vacuum processing operation.
 	 */
-	report->total_blks_read += bufusage.local_blks_read + bufusage.shared_blks_read;
-	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->common.total_blks_read += bufusage.local_blks_read + bufusage.shared_blks_read;
+	report->common.total_blks_hit += bufusage.local_blks_hit + bufusage.shared_blks_hit;
+	report->common.total_blks_dirtied += bufusage.local_blks_dirtied + bufusage.shared_blks_dirtied;
+	report->common.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->common.wal_records += walusage.wal_records;
+	report->common.wal_fpi += walusage.wal_fpi;
+	report->common.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->common.blk_read_time += INSTR_TIME_GET_MILLISEC(bufusage.local_blk_read_time);
+	report->common.blk_read_time += INSTR_TIME_GET_MILLISEC(bufusage.shared_blk_read_time);
+	report->common.blk_write_time += INSTR_TIME_GET_MILLISEC(bufusage.local_blk_write_time);
+	report->common.blk_write_time += INSTR_TIME_GET_MILLISEC(bufusage.shared_blk_write_time);
+	report->common.delay_time += VacuumDelayTime - counters->VacuumDelayTime;
 
-	report->total_time += secs * 1000. + usecs / 1000.;
+	report->common.total_time += secs * 1000. + usecs / 1000.;
 
 	if (!rel->pgstat_info || !pgstat_track_counts)
 		/*
@@ -573,9 +573,9 @@ extvac_stats_end(Relation rel, LVExtStatCounters *counters,
 		 */
 		return;
 
-	report->blks_fetched +=
+	report->common.blks_fetched +=
 		rel->pgstat_info->counts.blocks_fetched - counters->blocks_fetched;
-	report->blks_hit +=
+	report->common.blks_hit +=
 		rel->pgstat_info->counts.blocks_hit - counters->blocks_hit;
 }

 void
 
 	extvac_stats_end(rel, &counters->common, report);
 	report->type = PGSTAT_EXTVAC_INDEX;
@@ -618,7 +618,7 @@ extvac_stats_end_idx(Relation rel, IndexBulkDeleteResult *stats,
 		 */
 
 		/* Fill index-specific extended stats fields */
-		report->tuples_deleted =
+		report->common.tuples_deleted =
 							stats->tuples_removed - counters->tuples_removed;
 		report->index.pages_deleted =
 							stats->pages_deleted - counters->pages_deleted;
@@ -641,7 +641,7 @@ extvac_stats_end_idx(Relation rel, IndexBulkDeleteResult *stats,
   * procudure.
 */
 static void
-accumulate_heap_vacuum_statistics(LVRelState *vacrel, ExtVacReport *extVacStats)
+accumulate_heap_vacuum_statistics(LVRelState *vacrel, PgStat_VacuumRelationCounts *extVacStats)
 {
 	if (!pgstat_track_vacuum_statistics)
 		return;
@@ -653,49 +653,49 @@ accumulate_heap_vacuum_statistics(LVRelState *vacrel, ExtVacReport *extVacStats)
 	extVacStats->table.vm_new_frozen_pages = vacrel->vm_new_frozen_pages;
 	extVacStats->table.vm_new_visible_pages = vacrel->vm_new_visible_pages;
 	extVacStats->table.vm_new_visible_frozen_pages = vacrel->vm_new_visible_frozen_pages;
-	extVacStats->tuples_deleted = vacrel->tuples_deleted;
+	extVacStats->common.tuples_deleted = vacrel->tuples_deleted;
 	extVacStats->table.tuples_frozen = vacrel->tuples_frozen;
 	extVacStats->table.recently_dead_tuples = vacrel->recently_dead_tuples;
 	extVacStats->table.recently_dead_tuples = vacrel->recently_dead_tuples;
 	extVacStats->table.missed_dead_tuples = vacrel->missed_dead_tuples;
 	extVacStats->table.missed_dead_pages = vacrel->missed_dead_pages;
 	extVacStats->table.index_vacuum_count = vacrel->num_index_scans;
-	extVacStats->wraparound_failsafe_count = vacrel->wraparound_failsafe_count;
+	extVacStats->common.wraparound_failsafe_count = vacrel->wraparound_failsafe_count;
 
-	extVacStats->blk_read_time -= vacrel->extVacReportIdx.blk_read_time;
-	extVacStats->blk_write_time -= vacrel->extVacReportIdx.blk_write_time;
-	extVacStats->total_blks_dirtied -= vacrel->extVacReportIdx.total_blks_dirtied;
-	extVacStats->total_blks_hit -= vacrel->extVacReportIdx.total_blks_hit;
-	extVacStats->total_blks_read -= vacrel->extVacReportIdx.total_blks_read;
-	extVacStats->total_blks_written -= vacrel->extVacReportIdx.total_blks_written;
-	extVacStats->wal_bytes -= vacrel->extVacReportIdx.wal_bytes;
-	extVacStats->wal_fpi -= vacrel->extVacReportIdx.wal_fpi;
-	extVacStats->wal_records -= vacrel->extVacReportIdx.wal_records;
+	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;
+	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->total_time -= vacrel->extVacReportIdx.total_time;
-	extVacStats->delay_time -= vacrel->extVacReportIdx.delay_time;
+	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, ExtVacReport *extVacIdxStats)
+accumulate_idxs_vacuum_statistics(LVRelState *vacrel, PgStat_VacuumRelationCounts *extVacIdxStats)
 {
 	if (!pgstat_track_vacuum_statistics)
 		return;
 
 	/* Fill heap-specific extended stats fields */
-	vacrel->extVacReportIdx.blk_read_time += extVacIdxStats->blk_read_time;
-	vacrel->extVacReportIdx.blk_write_time += extVacIdxStats->blk_write_time;
-	vacrel->extVacReportIdx.total_blks_dirtied += extVacIdxStats->total_blks_dirtied;
-	vacrel->extVacReportIdx.total_blks_hit += extVacIdxStats->total_blks_hit;
-	vacrel->extVacReportIdx.total_blks_read += extVacIdxStats->total_blks_read;
-	vacrel->extVacReportIdx.total_blks_written += extVacIdxStats->total_blks_written;
-	vacrel->extVacReportIdx.wal_bytes += extVacIdxStats->wal_bytes;
-	vacrel->extVacReportIdx.wal_fpi += extVacIdxStats->wal_fpi;
-	vacrel->extVacReportIdx.wal_records += extVacIdxStats->wal_records;
-	vacrel->extVacReportIdx.delay_time += extVacIdxStats->delay_time;
-
-	vacrel->extVacReportIdx.total_time += extVacIdxStats->total_time;
+	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;
+	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;
 }

@@ -855,11 +855,11 @@ heap_vacuum_rel(Relation rel, VacuumParams *params,
 
 	/* Initialize vacuum statistics */
-	memset(&extVacReport, 0, sizeof(ExtVacReport));
+	memset(&extVacReport, 0, sizeof(PgStat_VacuumRelationCounts));
 
 	verbose = (params->options & VACOPT_VERBOSE) != 0;
 	instrument = (verbose || (AmAutoVacuumWorkerProcess() &&
@@ -905,7 +905,7 @@ heap_vacuum_rel(Relation rel, VacuumParams *params,
 	errcallback.previous = error_context_stack;
 	error_context_stack = &errcallback;
 
-	memset(&vacrel->extVacReportIdx, 0, sizeof(ExtVacReport));
+	memset(&vacrel->extVacReportIdx, 0, sizeof(PgStat_VacuumRelationCounts));
 
 	/* Set up high level stuff about rel and its indexes */
 	vacrel->rel = rel;
@@ -1176,25 +1176,16 @@ heap_vacuum_rel(Relation rel, VacuumParams *params,
 		extvac_stats_end(vacrel->rel, &extVacCounters, &extVacReport);
 		accumulate_heap_vacuum_statistics(vacrel, &extVacReport);
 
+		pgstat_report_tab_vacuum_extstats(vacrel->reloid, rel->rd_rel->relisshared, &extVacReport);
 
 	}
-	else
-	{

@@ -2893,9 +2884,9 @@ lazy_vacuum_all_indexes(LVRelState *vacrel)
 	else
 	{
 		LVExtStatCounters counters;
-		ExtVacReport extVacReport;
+		PgStat_VacuumRelationCounts extVacReport;
 
-		memset(&extVacReport, 0, sizeof(ExtVacReport));
+		memset(&extVacReport, 0, sizeof(PgStat_VacuumRelationCounts));
 
 		extvac_stats_start(vacrel->rel, &counters);
 
@@ -3327,9 +3318,9 @@ lazy_cleanup_all_indexes(LVRelState *vacrel)
 	else
 	{
 		LVExtStatCounters counters;
-		ExtVacReport extVacReport;
+		PgStat_VacuumRelationCounts extVacReport;
 
-		memset(&extVacReport, 0, sizeof(ExtVacReport));
+		memset(&extVacReport, 0, sizeof(PgStat_VacuumRelationCounts));
 
 		extvac_stats_start(vacrel->rel, &counters);
 
@@ -3366,7 +3357,7 @@ lazy_vacuum_one_index(Relation indrel, IndexBulkDeleteResult *istat,
 	IndexVacuumInfo ivinfo;
 
 	/* Set initial statistics values to gather vacuum statistics for the index */
 	extvac_stats_start_idx(indrel, istat, &extVacCounters);
@@ -3405,9 +3396,7 @@ lazy_vacuum_one_index(Relation indrel, IndexBulkDeleteResult *istat,
 		if (!ParallelVacuumIsActive(vacrel))
 			accumulate_idxs_vacuum_statistics(vacrel, &extVacReport);
 
-		pgstat_report_vacuum(RelationGetRelid(indrel),
-								indrel->rd_rel->relisshared,
-								0, 0, 0, &extVacReport);
+		pgstat_report_tab_vacuum_extstats(vacrel->indoid, indrel->rd_rel->relisshared, &extVacReport);
 	}
 
@@ -3435,7 +3424,7 @@ lazy_cleanup_one_index(Relation indrel, IndexBulkDeleteResult *istat,
 
 	/* Set initial statistics values to gather vacuum statistics for the index */
 	extvac_stats_start_idx(indrel, istat, &extVacCounters);
@@ -3472,9 +3461,7 @@ lazy_cleanup_one_index(Relation indrel, IndexBulkDeleteResult *istat,
 		if (!ParallelVacuumIsActive(vacrel))
 			accumulate_idxs_vacuum_statistics(vacrel, &extVacReport);
 
-		pgstat_report_vacuum(RelationGetRelid(indrel),
-								indrel->rd_rel->relisshared,
-								0, 0, 0, &extVacReport);
+		pgstat_report_tab_vacuum_extstats(vacrel->indoid, indrel->rd_rel->relisshared, &extVacReport);
 	}
 
 	/* Revert to the previous phase information for error traceback */
@@ -4075,6 +4062,27 @@ update_relstats_all_indexes(LVRelState *vacrel)
 	}
 }
 
+/* ---------
+ * pgstat_report_vacuum_error() -
+ *
+ *	Tell the collector about an (auto)vacuum interruption.
+ * ---------
+ */
+static void
+pgstat_report_vacuum_error()
+{
+	PgStat_VacuumDBCounts *vacuum_dbentry;
+
+	if(!pgstat_track_vacuum_statistics)
+		return;
+
+	vacuum_dbentry = pgstat_fetch_stat_vacuum_dbentry(MyDatabaseId);
+
+    if(vacuum_dbentry == NULL)
+    return;
+	vacuum_dbentry->errors++;
+}
+
 /*
  * Error context callback for errors occurring during vacuum.  The error
  * context messages for index phases should match the messages set in parallel
@@ -4090,7 +4098,7 @@ vacuum_error_callback(void *arg)
 	{
 		case VACUUM_ERRCB_PHASE_SCAN_HEAP:
 			if(geterrelevel() == ERROR)
-					pgstat_report_vacuum_error(errinfo->reloid, PGSTAT_EXTVAC_TABLE);
+					pgstat_report_vacuum_error();
 
 			if (BlockNumberIsValid(errinfo->blkno))
 			{
@@ -4108,7 +4116,7 @@ vacuum_error_callback(void *arg)

 			if (BlockNumberIsValid(errinfo->blkno))
 			{
@@ -4126,7 +4134,7 @@ vacuum_error_callback(void *arg)

 			errcontext("while vacuuming index \"%s\" of relation \"%s.%s\"",
 					   errinfo->indname, errinfo->relnamespace, errinfo->relname);
@@ -4134,7 +4142,7 @@ vacuum_error_callback(void *arg)

 			errcontext("while cleaning up index \"%s\" of relation \"%s.%s\"",
 					   errinfo->indname, errinfo->relnamespace, errinfo->relname);
@@ -4142,7 +4150,7 @@ vacuum_error_callback(void *arg)

 
 
 	/*

 	/*
 
 	/*
index 65de45a4447..b67326eafcc 100644
 
 	/*

 	/*
 
 /*
 
+/*

 	return true;
 }

 /*
 
-/* ---------

 /*
 
+void
+pgstat_vacuum_relation_delete_pending_cb(Oid relid)
+{
+	pgstat_drop_transactional(PGSTAT_KIND_VACUUM_RELATION, relid, InvalidOid);
+}
+
 void
 pgstat_relation_delete_pending_cb(PgStat_EntryRef *entry_ref)
 {
@@ -1053,60 +1012,4 @@ restore_truncdrop_counters(PgStat_TableXactStatus *trans)
 		trans->tuples_updated = trans->updated_pre_truncdrop;
 		trans->tuples_deleted = trans->deleted_pre_truncdrop;
 	}
-}
-
-static void
-pgstat_accumulate_extvac_stats(ExtVacReport *dst, ExtVacReport *src,
-							   bool accumulate_reltype_specific_info)
-{
-	if(!pgstat_track_vacuum_statistics)
-		return;
-
-	dst->total_blks_read += src->total_blks_read;
-	dst->total_blks_hit += src->total_blks_hit;
-	dst->total_blks_dirtied += src->total_blks_dirtied;
-	dst->total_blks_written += src->total_blks_written;
-	dst->wal_bytes += src->wal_bytes;
-	dst->wal_fpi += src->wal_fpi;
-	dst->wal_records += src->wal_records;
-	dst->blk_read_time += src->blk_read_time;
-	dst->blk_write_time += src->blk_write_time;
-	dst->delay_time += src->delay_time;
-	dst->total_time += src->total_time;
-	dst->wraparound_failsafe_count += src->wraparound_failsafe_count;
-	dst->errors += src->errors;
-
-	if (!accumulate_reltype_specific_info)
-		return;
-
-	if (dst->type == PGSTAT_EXTVAC_INVALID)
-		dst->type = src->type;
-
-	Assert(src->type == PGSTAT_EXTVAC_INVALID || src->type == dst->type);
-
-	if (dst->type == src->type)
-	{
-		dst->blks_fetched += src->blks_fetched;
-		dst->blks_hit += src->blks_hit;
-
-		if (dst->type == PGSTAT_EXTVAC_TABLE)
-		{
-			dst->table.pages_scanned += src->table.pages_scanned;
-			dst->table.pages_removed += src->table.pages_removed;
-			dst->table.vm_new_frozen_pages += src->table.vm_new_frozen_pages;
-			dst->table.vm_new_visible_pages += src->table.vm_new_visible_pages;
-			dst->table.vm_new_visible_frozen_pages += src->table.vm_new_visible_frozen_pages;
-			dst->tuples_deleted += src->tuples_deleted;
-			dst->table.tuples_frozen += src->table.tuples_frozen;
-			dst->table.recently_dead_tuples += src->table.recently_dead_tuples;
-			dst->table.index_vacuum_count += src->table.index_vacuum_count;
-			dst->table.missed_dead_pages += src->table.missed_dead_pages;
-			dst->table.missed_dead_tuples += src->table.missed_dead_tuples;
-		}
-		else if (dst->type == PGSTAT_EXTVAC_INDEX)
-		{
-			dst->index.pages_deleted += src->index.pages_deleted;
-			dst->tuples_deleted += src->tuples_deleted;
-		}
-	}
 }
\ No newline at end of file
diff --git a/src/backend/utils/activity/pgstat_vacuum.c b/src/backend/utils/activity/pgstat_vacuum.c
new file mode 100644
index 00000000000..7e1db137a18
--- /dev/null
+++ b/src/backend/utils/activity/pgstat_vacuum.c
@@ -0,0 +1,224 @@
+#include "postgres.h"
+
+#include "pgstat.h"
+#include "utils/pgstat_internal.h"
+#include "utils/memutils.h"
+
+/* ----------
+ * GUC parameters
+ * ----------
+ */
+bool		pgstat_track_vacuum_statistics_for_relations = false;
+
+#define ACCUMULATE_FIELD(field) dst->field += src->field;
+
+#define ACCUMULATE_SUBFIELD(substruct, field) \
+    (dst->substruct.field += src->substruct.field)
+
+static void
+pgstat_accumulate_common(PgStat_CommonCounts *dst, const PgStat_CommonCounts *src)
+{
+	dst->total_blks_read += src->total_blks_read;
+	dst->total_blks_hit += src->total_blks_hit;
+	dst->total_blks_dirtied += src->total_blks_dirtied;
+	dst->total_blks_written += src->total_blks_written;
+
+	dst->blks_fetched += src->blks_fetched;
+	dst->blks_hit += src->blks_hit;
+
+	dst->wal_records += src->wal_records;
+	dst->wal_fpi += src->wal_fpi;
+	dst->wal_bytes += src->wal_bytes;
+
+	dst->blk_read_time += src->blk_read_time;
+	dst->blk_write_time += src->blk_write_time;
+	dst->delay_time += src->delay_time;
+	dst->total_time += src->total_time;
+
+	dst->tuples_deleted += src->tuples_deleted;
+	dst->wraparound_failsafe_count += src->wraparound_failsafe_count;
+}
+
+static void
+pgstat_accumulate_extvac_stats_relations(PgStat_VacuumRelationCounts *dst, PgStat_VacuumRelationCounts *src)
+{
+    if(!pgstat_track_vacuum_statistics)
+		return;
+
+    if (dst->type == PGSTAT_EXTVAC_INVALID)
+        dst->type = src->type;
+
+    Assert(src->type != PGSTAT_EXTVAC_INVALID && src->type != PGSTAT_EXTVAC_DB && src->type == dst->type);
+
+    pgstat_accumulate_common(&dst->common, &src->common);
+
+    dst->common.blks_fetched += src->common.blks_fetched;
+    dst->common.blks_hit += src->common.blks_hit;
+
+    if (dst->type == PGSTAT_EXTVAC_TABLE)
+    {
+        ACCUMULATE_SUBFIELD(table, pages_scanned);
+        ACCUMULATE_SUBFIELD(table, pages_removed);
+        ACCUMULATE_SUBFIELD(table, vm_new_frozen_pages);
+        ACCUMULATE_SUBFIELD(table, vm_new_visible_pages);
+        ACCUMULATE_SUBFIELD(table, vm_new_visible_frozen_pages);
+        dst->common.tuples_deleted += src->common.tuples_deleted;
+        ACCUMULATE_SUBFIELD(table, tuples_frozen);
+        ACCUMULATE_SUBFIELD(table, recently_dead_tuples);
+        ACCUMULATE_SUBFIELD(table, index_vacuum_count);
+        ACCUMULATE_SUBFIELD(table, missed_dead_pages);
+        ACCUMULATE_SUBFIELD(table, missed_dead_tuples);
+    }
+    else if (dst->type == PGSTAT_EXTVAC_INDEX)
+    {
+        ACCUMULATE_SUBFIELD(index, pages_deleted);
+        dst->common.tuples_deleted += src->common.tuples_deleted;
+    }
+}
+
+static void
+pgstat_accumulate_extvac_stats_db(PgStat_VacuumDBCounts *dst, PgStat_VacuumDBCounts *src)
+{
+    if(!pgstat_track_vacuum_statistics)
+		return;
+
+    pgstat_accumulate_common((PgStat_CommonCounts *) dst, (PgStat_CommonCounts *) src);
+    dst->errors += src->errors;
+}
+
+static void
+pgstat_increment_tab_extvac_stats_to_db(PgStat_VacuumDBCounts *dst, PgStat_VacuumRelationCounts *src)
+{
+    if(!pgstat_track_vacuum_statistics)
+		return;
+
+    pgstat_accumulate_common((PgStat_CommonCounts *) dst, (PgStat_CommonCounts *) src);
+}
+
+/*
+ * Report that the table was just vacuumed and flush statistics.
+ */
+void
+pgstat_report_tab_vacuum_extstats(Oid tableoid, bool shared,
+								  PgStat_VacuumRelationCounts *params)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_VacuumRelation *shtabentry;
+	PgStatShared_VacuumDB *shdbentry;
+	Oid			dboid = (shared ? InvalidOid : MyDatabaseId);
+
+	if(!pgstat_track_vacuum_statistics)
+		return;
+
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_VACUUM_RELATION,
+											dboid, tableoid, false);
+	shtabentry = (PgStatShared_VacuumRelation *) entry_ref->shared_stats;
+	pgstat_accumulate_extvac_stats_relations(&shtabentry->stats, params);
+
+	pgstat_unlock_entry(entry_ref);
+
+
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_VACUUM_DB,
+											dboid, InvalidOid, false);
+
+	shdbentry = (PgStatShared_VacuumDB *) entry_ref->shared_stats;
+
+	pgstat_increment_tab_extvac_stats_to_db(&shdbentry->stats, params);
+
+	pgstat_unlock_entry(entry_ref);
+}
+
+/*
+ * Flush out pending stats for the entry
+ *
+ * If nowait is true, this function returns false if lock could not
+ * immediately acquired, otherwise true is returned.
+ */
+bool
+pgstat_vacuum_relation_flush_cb(PgStat_EntryRef *entry_ref, bool nowait)
+{
+	PgStatShared_VacuumRelation *shtabstats;
+	PgStat_RelationVacuumPending *pendingent;	/* table entry of shared stats */
+
+	pendingent = (PgStat_RelationVacuumPending *) entry_ref->pending;
+	shtabstats = (PgStatShared_VacuumRelation *) entry_ref->shared_stats;
+
+	/*
+	 * Ignore entries that didn't accumulate any actual counts.
+	 */
+	if (pg_memory_is_all_zeros(&pendingent,
+							   sizeof(struct PgStat_RelationVacuumPending)))
+		return true;
+
+	if (!pgstat_lock_entry(entry_ref, nowait))
+	{
+        return false;
+    }
+
+	pgstat_accumulate_extvac_stats_relations(&(shtabstats->stats), &(pendingent->counts));
+
+	pgstat_unlock_entry(entry_ref);
+
+	return true;
+}
+
+/*
+ * Support function for the SQL-callable pgstat* functions. Returns
+ * the vacuum collected statistics for one relation or NULL.
+ */
+PgStat_VacuumRelationCounts *
+pgstat_fetch_stat_vacuum_tabentry(Oid relid, Oid dbid)
+{
+	return (PgStat_VacuumRelationCounts *)
+		pgstat_fetch_entry(PGSTAT_KIND_VACUUM_RELATION, dbid, relid);
+}
+
+PgStat_VacuumDBCounts *
+pgstat_fetch_stat_vacuum_dbentry(Oid dbid)
+{
+	return (PgStat_VacuumDBCounts *)
+		pgstat_fetch_entry(PGSTAT_KIND_VACUUM_DB, dbid, InvalidOid);
+}
+
+bool
+pgstat_vacuum_db_flush_cb(PgStat_EntryRef *entry_ref, bool nowait)
+{
+	PgStatShared_VacuumDB *sharedent;
+	PgStat_VacuumDBCounts *pendingent;
+
+	pendingent = (PgStat_VacuumDBCounts *) entry_ref->pending;
+	sharedent = (PgStatShared_VacuumDB *) entry_ref->shared_stats;
+
+	if (!pgstat_lock_entry(entry_ref, nowait))
+		return false;
+
+	/* The entry was successfully flushed, add the same to database stats */
+	pgstat_accumulate_extvac_stats_db(&(sharedent->stats), pendingent);
+
+	pgstat_unlock_entry(entry_ref);
+
+	return true;
+}
+
+/*
+ * Find or create a local PgStat_VacuumDBCounts entry for dboid.
+ */
+PgStat_VacuumDBCounts *
+pgstat_prep_vacuum_database_pending(Oid dboid)
+{
+	PgStat_EntryRef *entry_ref;
+
+	/*
+	 * This should not report stats on database objects before having
+	 * connected to a database.
+	 */
+	Assert(!OidIsValid(dboid) || OidIsValid(MyDatabaseId));
+
+	entry_ref = pgstat_prep_pending_entry(PGSTAT_KIND_VACUUM_DB, dboid, InvalidOid,
+										  NULL);
+
+    if(entry_ref == NULL)
+        return NULL;
+
+    return entry_ref->pending;
+}
\ No newline at end of file
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index a2ece2c36cf..8603d4dd576 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -2265,7 +2265,6 @@ pg_stat_have_stats(PG_FUNCTION_ARGS)
 	PG_RETURN_BOOL(pgstat_have_entry(kind, dboid, objid));
 }
 
-
 /*
  * Get the vacuum statistics for the heap tables.
  */
@@ -2275,102 +2274,42 @@ pg_stat_get_vacuum_tables(PG_FUNCTION_ARGS)
 	#define PG_STAT_GET_VACUUM_TABLES_STATS_COLS 26

 

 	i = 0;
 
 	values[i++] = ObjectIdGetDatum(relid);
 
-	values[i++] = Int64GetDatum(extvacuum->total_blks_read);
-	values[i++] = Int64GetDatum(extvacuum->total_blks_hit);
-	values[i++] = Int64GetDatum(extvacuum->total_blks_dirtied);
-	values[i++] = Int64GetDatum(extvacuum->total_blks_written);
+	values[i++] = Int64GetDatum(extvacuum->common.total_blks_read);
+	values[i++] = Int64GetDatum(extvacuum->common.total_blks_hit);
+	values[i++] = Int64GetDatum(extvacuum->common.total_blks_dirtied);
+	values[i++] = Int64GetDatum(extvacuum->common.total_blks_written);
 
-	values[i++] = Int64GetDatum(extvacuum->blks_fetched -
-									extvacuum->blks_hit);
-	values[i++] = Int64GetDatum(extvacuum->blks_hit);
+	values[i++] = Int64GetDatum(extvacuum->common.blks_fetched -
+									extvacuum->common.blks_hit);
+	values[i++] = Int64GetDatum(extvacuum->common.blks_hit);
 
 	values[i++] = Int64GetDatum(extvacuum->table.pages_scanned);
 	values[i++] = Int64GetDatum(extvacuum->table.pages_removed);
@@ -2378,28 +2317,28 @@ pg_stat_get_vacuum_tables(PG_FUNCTION_ARGS)
 	values[i++] = Int64GetDatum(extvacuum->table.vm_new_visible_pages);
 	values[i++] = Int64GetDatum(extvacuum->table.vm_new_visible_frozen_pages);
 	values[i++] = Int64GetDatum(extvacuum->table.missed_dead_pages);
-	values[i++] = Int64GetDatum(extvacuum->tuples_deleted);
+	values[i++] = Int64GetDatum(extvacuum->common.tuples_deleted);
 	values[i++] = Int64GetDatum(extvacuum->table.tuples_frozen);
 	values[i++] = Int64GetDatum(extvacuum->table.recently_dead_tuples);
 	values[i++] = Int64GetDatum(extvacuum->table.missed_dead_tuples);
 
-	values[i++] = Int32GetDatum(extvacuum->wraparound_failsafe_count);
+	values[i++] = Int32GetDatum(extvacuum->common.wraparound_failsafe_count);
 	values[i++] = Int64GetDatum(extvacuum->table.index_vacuum_count);
 
-	values[i++] = Int64GetDatum(extvacuum->wal_records);
-	values[i++] = Int64GetDatum(extvacuum->wal_fpi);
+	values[i++] = Int64GetDatum(extvacuum->common.wal_records);
+	values[i++] = Int64GetDatum(extvacuum->common.wal_fpi);
 
 	/* Convert to numeric, like pg_stat_statements */
-	snprintf(buf, sizeof buf, UINT64_FORMAT, extvacuum->wal_bytes);
+	snprintf(buf, sizeof buf, UINT64_FORMAT, extvacuum->common.wal_bytes);
 	values[i++] = DirectFunctionCall3(numeric_in,
 									  CStringGetDatum(buf),
 									  ObjectIdGetDatum(0),
 									  Int32GetDatum(-1));
 
-	values[i++] = Float8GetDatum(extvacuum->blk_read_time);
-	values[i++] = Float8GetDatum(extvacuum->blk_write_time);
-	values[i++] = Float8GetDatum(extvacuum->delay_time);
-	values[i++] = Float8GetDatum(extvacuum->total_time);
+	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);

@@ -2416,100 +2355,60 @@ pg_stat_get_vacuum_indexes(PG_FUNCTION_ARGS)
 	#define PG_STAT_GET_VACUUM_INDEX_STATS_COLS	16

 

 	i = 0;
 
 	values[i++] = ObjectIdGetDatum(relid);
 
-	values[i++] = Int64GetDatum(extvacuum->total_blks_read);
-	values[i++] = Int64GetDatum(extvacuum->total_blks_hit);
-	values[i++] = Int64GetDatum(extvacuum->total_blks_dirtied);
-	values[i++] = Int64GetDatum(extvacuum->total_blks_written);
+	values[i++] = Int64GetDatum(extvacuum->common.total_blks_read);
+	values[i++] = Int64GetDatum(extvacuum->common.total_blks_hit);
+	values[i++] = Int64GetDatum(extvacuum->common.total_blks_dirtied);
+	values[i++] = Int64GetDatum(extvacuum->common.total_blks_written);
 
-	values[i++] = Int64GetDatum(extvacuum->blks_fetched -
-									extvacuum->blks_hit);
-	values[i++] = Int64GetDatum(extvacuum->blks_hit);
+	values[i++] = Int64GetDatum(extvacuum->common.blks_fetched -
+									extvacuum->common.blks_hit);
+	values[i++] = Int64GetDatum(extvacuum->common.blks_hit);
 
 	values[i++] = Int64GetDatum(extvacuum->index.pages_deleted);
-	values[i++] = Int64GetDatum(extvacuum->tuples_deleted);
+	values[i++] = Int64GetDatum(extvacuum->common.tuples_deleted);
 
-	values[i++] = Int64GetDatum(extvacuum->wal_records);
-	values[i++] = Int64GetDatum(extvacuum->wal_fpi);
+	values[i++] = Int64GetDatum(extvacuum->common.wal_records);
+	values[i++] = Int64GetDatum(extvacuum->common.wal_fpi);
 
 	/* Convert to numeric, like pg_stat_statements */
-	snprintf(buf, sizeof buf, UINT64_FORMAT, extvacuum->wal_bytes);
+	snprintf(buf, sizeof buf, UINT64_FORMAT, extvacuum->common.wal_bytes);
 	values[i++] = DirectFunctionCall3(numeric_in,
 									  CStringGetDatum(buf),
 									  ObjectIdGetDatum(0),
 									  Int32GetDatum(-1));
 
-	values[i++] = Float8GetDatum(extvacuum->blk_read_time);
-	values[i++] = Float8GetDatum(extvacuum->blk_write_time);
-	values[i++] = Float8GetDatum(extvacuum->delay_time);
-	values[i++] = Float8GetDatum(extvacuum->total_time);
+	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);

@@ -2523,90 +2422,52 @@ pg_stat_get_vacuum_database(PG_FUNCTION_ARGS)
 	#define PG_STAT_GET_VACUUM_DATABASE_STATS_COLS	14

 

 
-	values[i++] = Int64GetDatum(extvacuum->total_blks_read);
-	values[i++] = Int64GetDatum(extvacuum->total_blks_hit);
-	values[i++] = Int64GetDatum(extvacuum->total_blks_dirtied);
-	values[i++] = Int64GetDatum(extvacuum->total_blks_written);
+	values[i++] = Int64GetDatum(extvacuum->common.total_blks_read);
+	values[i++] = Int64GetDatum(extvacuum->common.total_blks_hit);
+	values[i++] = Int64GetDatum(extvacuum->common.total_blks_dirtied);
+	values[i++] = Int64GetDatum(extvacuum->common.total_blks_written);
 
-	values[i++] = Int64GetDatum(extvacuum->wal_records);
-	values[i++] = Int64GetDatum(extvacuum->wal_fpi);
+	values[i++] = Int64GetDatum(extvacuum->common.wal_records);
+	values[i++] = Int64GetDatum(extvacuum->common.wal_fpi);
 
 	/* Convert to numeric, like pg_stat_statements */
-	snprintf(buf, sizeof buf, UINT64_FORMAT, extvacuum->wal_bytes);
+	snprintf(buf, sizeof buf, UINT64_FORMAT, extvacuum->common.wal_bytes);
 	values[i++] = DirectFunctionCall3(numeric_in,
 									  CStringGetDatum(buf),
 									  ObjectIdGetDatum(0),
 									  Int32GetDatum(-1));
 
-	values[i++] = Float8GetDatum(extvacuum->blk_read_time);
-	values[i++] = Float8GetDatum(extvacuum->blk_write_time);
-	values[i++] = Float8GetDatum(extvacuum->delay_time);
-	values[i++] = Float8GetDatum(extvacuum->total_time);
-	values[i++] = Int32GetDatum(extvacuum->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++] = Int32GetDatum(extvacuum->common.wraparound_failsafe_count);
 	values[i++] = Int32GetDatum(extvacuum->errors);
 
 	Assert(i == PG_STAT_GET_VACUUM_DATABASE_STATS_COLS);
index f8158aa353c..a12590948fb 100644
@@ -116,46 +116,100 @@ typedef enum ExtVacReportType
 
 /* ----------
+ * PgStat_TableCounts			The actual per-table counts kept by a backend
  *
- * ExtVacReport
+ * This struct should contain only actual event counters, because we make use
+ * of pg_memory_is_all_zeros() to detect whether there are any stats updates
+ * to apply.
  *
- * Additional statistics of vacuum processing over a relation.
- * pages_removed is the amount by which the physically shrank,
- * if any (ie the change in its total size on disk)
- * pages_deleted refer to free space within the index file
+ * It is a component of PgStat_TableStatus (within-backend state).
+ *
+ * Note: for a table, tuples_returned is the number of tuples successfully
+ * fetched by heap_getnext, while tuples_fetched is the number of tuples
+ * successfully fetched by heap_fetch under the control of bitmap indexscans.
+ * For an index, tuples_returned is the number of index entries returned by
+ * the index AM, while tuples_fetched is the number of tuples successfully
+ * fetched by heap_fetch under the control of simple indexscans for this index.
+ *
+ * tuples_inserted/updated/deleted/hot_updated/newpage_updated count attempted
+ * actions, regardless of whether the transaction committed.  delta_live_tuples,
+ * delta_dead_tuples, and changed_tuples are set depending on commit or abort.
+ * Note that delta_live_tuples and delta_dead_tuples can be negative!
  * ----------
  */
-typedef struct ExtVacReport
+typedef struct PgStat_TableCounts
 {
-	/* number of blocks missed, hit, dirtied and written during a vacuum of specific relation */
-	int64		total_blks_read;
-	int64		total_blks_hit;
-	int64		total_blks_dirtied;
-	int64		total_blks_written;
+	PgStat_Counter numscans;
 
-	/* blocks missed and hit for just the heap during a vacuum of specific relation */
-	int64		blks_fetched;
-	int64		blks_hit;
+	PgStat_Counter tuples_returned;
+	PgStat_Counter tuples_fetched;
 
-	/* Vacuum WAL usage stats */
-	int64		wal_records;	/* wal usage: number of WAL records */
-	int64		wal_fpi;		/* wal usage: number of WAL full page images produced */
-	uint64		wal_bytes;		/* wal usage: size of WAL records produced */
+	PgStat_Counter tuples_inserted;
+	PgStat_Counter tuples_updated;
+	PgStat_Counter tuples_deleted;
+	PgStat_Counter tuples_hot_updated;
+	PgStat_Counter tuples_newpage_updated;
+	bool		truncdropped;
+
+	PgStat_Counter delta_live_tuples;
+	PgStat_Counter delta_dead_tuples;
+	PgStat_Counter changed_tuples;
+
+	PgStat_Counter blocks_fetched;
+	PgStat_Counter blocks_hit;
 
-	/* Time stats. */
-	double		blk_read_time;	/* time spent reading pages, in msec */
-	double		blk_write_time; /* time spent writing pages, in msec */
-	double		delay_time;		/* how long vacuum slept in vacuum delay point, in msec */
-	double		total_time;		/* total time of a vacuum operation, in msec */
+	PgStat_Counter rev_all_visible_pages;
+	PgStat_Counter rev_all_frozen_pages;
+} PgStat_TableCounts;
 
-	int64		tuples_deleted;		/* tuples deleted by vacuum */
+typedef struct PgStat_CommonCounts
+{
+	/* blocks */
+	int64 total_blks_read;
+	int64 total_blks_hit;
+	int64 total_blks_dirtied;
+	int64 total_blks_written;
+
+	/* heap blocks */
+	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;
+	double delay_time;
+	double total_time;
+
+	/* tuples */
+	int64 tuples_deleted;
+
+	/* failsafe */
+	int32 wraparound_failsafe_count;
+} PgStat_CommonCounts;
 
-	int32		errors;
-	int32		wraparound_failsafe_count;	/* the number of times to prevent wraparound problem */
+/* ----------
+ *
+ * PgStat_VacuumRelationCounts
+ *
+ * Additional statistics of vacuum processing over a relation.
+ * pages_removed is the amount by which the physically shrank,
+ * if any (ie the change in its total size on disk)
+ * pages_deleted refer to free space within the index file
+ * ----------
+ */
+typedef struct PgStat_VacuumRelationCounts
+{
+	PgStat_CommonCounts common;

@@ -174,16 +228,16 @@ typedef struct ExtVacReport
 	{
 		struct
 		{
+			int64		tuples_frozen;		/* tuples frozen up by vacuum */
+			int64		recently_dead_tuples;	/* deleted tuples that are still visible to some transaction */
+			int64		missed_dead_tuples;		/* tuples not pruned by vacuum due to failure to get a cleanup lock */
 			int64		pages_scanned;		/* heap pages examined (not skipped by VM) */
 			int64		pages_removed;		/* heap pages removed by vacuum "truncation" */
 			int64		pages_frozen;		/* pages marked in VM as frozen */
 			int64		pages_all_visible;	/* pages marked in VM as all-visible */
-			int64		tuples_frozen;		/* tuples frozen up by vacuum */
-			int64		recently_dead_tuples;	/* deleted tuples that are still visible to some transaction */
 			int64		vm_new_frozen_pages;		/* pages marked in VM as frozen */
 			int64		vm_new_visible_pages;	/* pages marked in VM as all-visible */
 			int64		vm_new_visible_frozen_pages;	/* pages marked in VM as all-visible and frozen */
-			int64		missed_dead_tuples;		/* tuples not pruned by vacuum due to failure to get a cleanup lock */
 			int64		missed_dead_pages;		/* pages with missed dead tuples */
 			int64		index_vacuum_count;	/* number of index vacuumings */
 		}			table;
@@ -192,61 +246,21 @@ typedef struct ExtVacReport
 			int64		pages_deleted;		/* number of pages deleted by vacuum */
 		}			index;
 	} /* per_type_stats */;
-} ExtVacReport;
+} PgStat_VacuumRelationCounts;
 
-/* ----------
 
-	/*
+	PgStat_CommonCounts common;
 
 /* ----------
  * PgStat_TableStatus			Per-table status within a backend
@@ -272,6 +286,12 @@ typedef struct PgStat_TableStatus
 	Relation	relation;		/* rel that is using this entry */
 } PgStat_TableStatus;
 
@@ -468,8 +488,6 @@ typedef struct PgStat_StatDBEntry

 typedef struct PgStat_StatFuncEntry
@@ -551,8 +569,6 @@ typedef struct PgStat_StatTabEntry

 /* ------
@@ -760,11 +776,10 @@ extern void pgstat_unlink_relation(Relation rel);

 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
@@ -895,6 +910,15 @@ extern int	pgstat_get_transactional_drops(bool isCommit, struct xl_xact_stats_it
 extern void pgstat_execute_transactional_drops(int ndrops, struct xl_xact_stats_item *items, bool is_redo);

 
 
 /*

 /* Custom stats kinds */
-- 
2.34.1

From 66ddd445242f0181edb903ed7ace60e75ca70890 Mon Sep 17 00:00:00 2001
From: Alena Rybakina <[email-masked]>
Date: Thu, 19 Dec 2024 12:57:49 +0300
Subject: [PATCH 5/5] Add documentation about the system views that are used in
 the machinery of vacuum statistics.

---

---

From: Alena Rybakina <[email-masked]>
Date: Tue, 3 Jun 2025 15:27:54 +0300

On 02.06.2025 22:56, Alena Rybakina wrote:
> Currently, database statistics work incorrectly — I'm investigating 
> the issue.
I fixed it and attached the patches.
> In parallel, I'm starting work on the extension.
I started working on this and will attach the first version soon.

 
+/*

+/* ----------

 /*

 
 	/*

 	if (instrument)

 		/*
 
 	if (map[mapByte] & mask)
 	{
+		/*
 
 		MarkBufferDirty(vmbuf);

 		VacuumCostBalance = 0;
 
 		/*

 
 
 /* ----------
 
-
 /*
 
-

 

 static void

 /*

 

+/*
 
 /*

 # - Monitoring -
 
+{ oid => '8001',

+/* ----------
+ *
+ * ExtVacReport
+ *

 /* ----------

 

 /* ------

 /*

 /*----------

 

-/*

 

 
-	report->blks_fetched =
 
+void
 
 /*

 

 	if (instrument)

 
 	ivinfo.index = indrel;

 	ivinfo.index = indrel;

 
 	/*

 

 

 

 /*
 
+/*

 
 
 /* ----------

 step s1_commit: COMMIT;

 

 

 
 /* ----------

 	return true;
 }
 
+/* ---------
 
 /*

 

 
 
+	int32		errors;
 
 	/* ----------
 
 /*
 
 session s1
+setup		{
 
 session s2
+setup		{

 DROP TABLE vestat;

 

 
 
 DROP TABLE vestat;

 

 

From 52221c338a7a7b842437e3bff0c385192233d3e5 Mon Sep 17 00:00:00 2001

---
 src/backend/access/heap/vacuumlazy.c          | 194 ++++++------
 src/backend/catalog/heap.c                    |   1 +
 src/backend/catalog/index.c                   |   1 +
 src/backend/catalog/system_views.sql          | 178 +++++------
 src/backend/commands/dbcommands.c             |   1 +
 src/backend/commands/vacuumparallel.c         |  14 +-
 src/backend/utils/activity/Makefile           |   1 +
 src/backend/utils/activity/pgstat.c           |  28 ++
 src/backend/utils/activity/pgstat_database.c  |  10 +-
 src/backend/utils/activity/pgstat_relation.c  | 111 +------
 src/backend/utils/activity/pgstat_vacuum.c    | 215 +++++++++++++
 src/backend/utils/adt/pgstatfuncs.c           | 285 +++++-------------
 src/include/commands/vacuum.h                 |   2 +-
 src/include/pgstat.h                          | 200 ++++++------
 src/include/utils/pgstat_internal.h           |  15 +
 src/include/utils/pgstat_kind.h               |   4 +-
 src/test/regress/expected/rules.out           | 146 ++++-----
 .../expected/vacuum_index_statistics.out      |  82 ++---
 .../regress/sql/vacuum_index_statistics.sql   |  48 +--
 19 files changed, 805 insertions(+), 731 deletions(-)
 create mode 100644 src/backend/utils/activity/pgstat_vacuum.c

index 3d72f74b05e..0c828fb90dd 100644

@@ -525,7 +525,7 @@ extvac_stats_start(Relation rel, LVExtStatCounters *counters)
  */
 static void
 extvac_stats_end(Relation rel, LVExtStatCounters *counters,
-				  ExtVacReport *report)
+				 PgStat_CommonCounts *report)
 {
 	WalUsage	walusage;
 	BufferUsage	bufusage;
@@ -586,6 +586,8 @@ extvac_stats_start_idx(Relation rel, IndexBulkDeleteResult *stats,
 	if(!pgstat_track_vacuum_statistics)
 		return;
 
+	memset(&counters->common, 0, sizeof(LVExtStatCounters));
+
 	/* Set initial values for common heap and index statistics*/
 	extvac_stats_start(rel, &counters->common);
 	counters->pages_deleted = counters->tuples_removed = 0;
@@ -603,11 +605,13 @@ extvac_stats_start_idx(Relation rel, IndexBulkDeleteResult *stats,
 
 void
+	extvac_stats_end(rel, &counters->common, &report->common);
 
-	extvac_stats_end(rel, &counters->common, report);
 	report->type = PGSTAT_EXTVAC_INDEX;
 
 	if (stats != NULL)
@@ -618,7 +622,7 @@ extvac_stats_end_idx(Relation rel, IndexBulkDeleteResult *stats,
 		 */
 
@@ -641,7 +645,7 @@ extvac_stats_end_idx(Relation rel, IndexBulkDeleteResult *stats,
@@ -653,49 +657,49 @@ accumulate_heap_vacuum_statistics(LVRelState *vacrel, ExtVacReport *extVacStats)

 
 }
 
 static void

@@ -855,11 +859,11 @@ heap_vacuum_rel(Relation rel, VacuumParams *params,

@@ -905,7 +909,8 @@ heap_vacuum_rel(Relation rel, VacuumParams *params,
 
-	memset(&vacrel->extVacReportIdx, 0, sizeof(ExtVacReport));
+	memset(&vacrel->extVacReportIdx, 0, sizeof(PgStat_VacuumRelationCounts));
+	memset(&extVacReport.common, 0, sizeof(PgStat_CommonCounts));
 
 	/* Set up high level stuff about rel and its indexes */
 	vacrel->rel = rel;
@@ -1156,7 +1161,7 @@ heap_vacuum_rel(Relation rel, VacuumParams *params,
 						&frozenxid_updated, &minmulti_updated, false);
 
 	/* Make generic extended vacuum stats report */
-	extvac_stats_end(rel, &extVacCounters, &extVacReport);
+	extvac_stats_end(rel, &extVacCounters, &extVacReport.common);
 
 	/*
 	 * Report results to the cumulative stats system, too.
@@ -1168,33 +1173,20 @@ heap_vacuum_rel(Relation rel, VacuumParams *params,
 	 * soon in cases where the failsafe prevented significant amounts of heap
 	 * vacuuming.
 	 */
-	if(pgstat_track_vacuum_statistics)
-	{
-		/* Make generic extended vacuum stats report and
-		 * fill heap-specific extended stats fields.
-		 */
-		extvac_stats_end(vacrel->rel, &extVacCounters, &extVacReport);
-		accumulate_heap_vacuum_statistics(vacrel, &extVacReport);

-	}
-	else
-	{
+	pgstat_report_vacuum_extstats(vacrel->reloid, rel->rd_rel->relisshared, &extVacReport);

@@ -2893,9 +2885,9 @@ lazy_vacuum_all_indexes(LVRelState *vacrel)
 	else
 	{
 		LVExtStatCounters counters;
-		ExtVacReport extVacReport;
+		PgStat_VacuumRelationCounts extVacReport;
 
-		memset(&extVacReport, 0, sizeof(ExtVacReport));
+		memset(&extVacReport.common, 0, sizeof(PgStat_CommonCounts));

@@ -2903,7 +2895,7 @@ lazy_vacuum_all_indexes(LVRelState *vacrel)
 		parallel_vacuum_bulkdel_all_indexes(vacrel->pvs, old_live_tuples,
 											vacrel->num_index_scans);
 
-		extvac_stats_end(vacrel->rel, &counters, &extVacReport);
+		extvac_stats_end(vacrel->rel, &counters, &extVacReport.common);
 		accumulate_idxs_vacuum_statistics(vacrel, &extVacReport);
 
 		/*
@@ -3327,9 +3319,9 @@ lazy_cleanup_all_indexes(LVRelState *vacrel)
 	else
 	{
 		LVExtStatCounters counters;
-		ExtVacReport extVacReport;
+		PgStat_VacuumRelationCounts extVacReport;
 
-		memset(&extVacReport, 0, sizeof(ExtVacReport));
+		memset(&extVacReport.common, 0, sizeof(PgStat_CommonCounts));

@@ -3338,7 +3330,7 @@ lazy_cleanup_all_indexes(LVRelState *vacrel)
 											vacrel->num_index_scans,
 											estimated_count);
 
-		extvac_stats_end(vacrel->rel, &counters, &extVacReport);
+		extvac_stats_end(vacrel->rel, &counters, &extVacReport.common);
 		accumulate_idxs_vacuum_statistics(vacrel, &extVacReport);
 	}
 
@@ -3366,7 +3358,10 @@ lazy_vacuum_one_index(Relation indrel, IndexBulkDeleteResult *istat,
 	IndexVacuumInfo ivinfo;
 	LVSavedErrInfo saved_err_info;
 	LVExtStatCountersIdx extVacCounters;
-	ExtVacReport extVacReport;
+	PgStat_VacuumRelationCounts extVacReport;
+
+	memset(&extVacReport, 0, sizeof(PgStat_VacuumRelationCounts));
+	memset(&extVacReport.common, 0, sizeof(PgStat_CommonCounts));
 
@@ -3397,18 +3392,13 @@ lazy_vacuum_one_index(Relation indrel, IndexBulkDeleteResult *istat,

-		if (!ParallelVacuumIsActive(vacrel))
-			accumulate_idxs_vacuum_statistics(vacrel, &extVacReport);
+	if (!ParallelVacuumIsActive(vacrel))
+		accumulate_idxs_vacuum_statistics(vacrel, &extVacReport);
 
-		pgstat_report_vacuum(RelationGetRelid(indrel),
-								indrel->rd_rel->relisshared,
-								0, 0, 0, &extVacReport);
-	}
+	pgstat_report_vacuum_extstats(vacrel->indoid, indrel->rd_rel->relisshared, &extVacReport);
 
 	/* Revert to the previous phase information for error traceback */
 	restore_vacuum_error_info(vacrel, &saved_err_info);
@@ -3435,7 +3425,10 @@ lazy_cleanup_one_index(Relation indrel, IndexBulkDeleteResult *istat,
 	IndexVacuumInfo ivinfo;
 	LVSavedErrInfo saved_err_info;
 	LVExtStatCountersIdx extVacCounters;
-	ExtVacReport extVacReport;
+	PgStat_VacuumRelationCounts extVacReport;
+
+	memset(&extVacReport, 0, sizeof(PgStat_VacuumRelationCounts));
+	memset(&extVacReport.common, 0, sizeof(PgStat_CommonCounts));
 
 	/* Set initial statistics values to gather vacuum statistics for the index */
 	extvac_stats_start_idx(indrel, istat, &extVacCounters);
@@ -3465,17 +3458,13 @@ lazy_cleanup_one_index(Relation indrel, IndexBulkDeleteResult *istat,

-	if(pgstat_track_vacuum_statistics)
-	{
-		/* Make extended vacuum stats report for index */
-		extvac_stats_end_idx(indrel, istat, &extVacCounters, &extVacReport);
-		if (!ParallelVacuumIsActive(vacrel))
-			accumulate_idxs_vacuum_statistics(vacrel, &extVacReport);
-
-		pgstat_report_vacuum(RelationGetRelid(indrel),
-								indrel->rd_rel->relisshared,
-								0, 0, 0, &extVacReport);
-	}
+	/* Make extended vacuum stats report for index */
+	extvac_stats_end_idx(indrel, istat, &extVacCounters, &extVacReport);
+
+	if (!ParallelVacuumIsActive(vacrel))
+		accumulate_idxs_vacuum_statistics(vacrel, &extVacReport);
+
+	pgstat_report_vacuum_extstats(vacrel->indoid, indrel->rd_rel->relisshared, &extVacReport);
 
 	/* Revert to the previous phase information for error traceback */
 	restore_vacuum_error_info(vacrel, &saved_err_info);
@@ -4075,6 +4064,27 @@ update_relstats_all_indexes(LVRelState *vacrel)
 	}
 }
 
+/* ---------
@@ -4090,7 +4100,7 @@ vacuum_error_callback(void *arg)
 
 			if (BlockNumberIsValid(errinfo->blkno))
 			{
@@ -4108,7 +4118,7 @@ vacuum_error_callback(void *arg)

 			if (BlockNumberIsValid(errinfo->blkno))
 			{
@@ -4126,7 +4136,7 @@ vacuum_error_callback(void *arg)

 			errcontext("while vacuuming index \"%s\" of relation \"%s.%s\"",
 					   errinfo->indname, errinfo->relnamespace, errinfo->relname);
@@ -4134,7 +4144,7 @@ vacuum_error_callback(void *arg)

 			errcontext("while cleaning up index \"%s\" of relation \"%s.%s\"",
 					   errinfo->indname, errinfo->relnamespace, errinfo->relname);
@@ -4142,7 +4152,7 @@ vacuum_error_callback(void *arg)

 
 
 	/*

 	/*
 	 * Close and flush the index's relcache entry, to ensure relcache doesn't
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index 0ae31b87989..954e2c6cd7b 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -1417,100 +1417,104 @@ GRANT EXECUTE ON FUNCTION pg_get_aios() TO pg_read_all_stats;
 --
 
 CREATE VIEW pg_stat_vacuum_tables AS
-SELECT
-  ns.nspname AS schemaname,
-  rel.relname AS relname,
-  stats.relid as relid,
-
-  stats.total_blks_read AS total_blks_read,
-  stats.total_blks_hit AS total_blks_hit,
-  stats.total_blks_dirtied AS total_blks_dirtied,
-  stats.total_blks_written AS total_blks_written,
-
-  stats.rel_blks_read AS rel_blks_read,
-  stats.rel_blks_hit AS rel_blks_hit,
-
-  stats.pages_scanned AS pages_scanned,
-  stats.pages_removed AS pages_removed,
-  stats.vm_new_frozen_pages AS vm_new_frozen_pages,
-  stats.vm_new_visible_pages AS vm_new_visible_pages,
-  stats.vm_new_visible_frozen_pages AS vm_new_visible_frozen_pages,
-  stats.missed_dead_pages AS missed_dead_pages,
-  stats.tuples_deleted AS tuples_deleted,
-  stats.tuples_frozen AS tuples_frozen,
-  stats.recently_dead_tuples AS recently_dead_tuples,
-  stats.missed_dead_tuples AS missed_dead_tuples,
-
-  stats.wraparound_failsafe AS wraparound_failsafe,
-  stats.index_vacuum_count AS index_vacuum_count,
-  stats.wal_records AS wal_records,
-  stats.wal_fpi AS wal_fpi,
-  stats.wal_bytes AS wal_bytes,
-
-  stats.blk_read_time AS blk_read_time,
-  stats.blk_write_time AS blk_write_time,
-
-  stats.delay_time AS delay_time,
-  stats.total_time AS total_time
-
-FROM pg_class rel
-  JOIN pg_namespace ns ON ns.oid = rel.relnamespace,
-  LATERAL pg_stat_get_vacuum_tables(rel.oid) stats
-WHERE rel.relkind = 'r';
+    SELECT
+        N.nspname AS schemaname,
+        C.relname AS relname,
+        S.relid as relid,
+
+        S.total_blks_read AS total_blks_read,
+        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.pages_scanned AS pages_scanned,
+        S.pages_removed AS pages_removed,
+        S.vm_new_frozen_pages AS vm_new_frozen_pages,
+        S.vm_new_visible_pages AS vm_new_visible_pages,
+        S.vm_new_visible_frozen_pages AS vm_new_visible_frozen_pages,
+        S.missed_dead_pages AS missed_dead_pages,
+        S.tuples_deleted AS tuples_deleted,
+        S.tuples_frozen AS tuples_frozen,
+        S.recently_dead_tuples AS recently_dead_tuples,
+        S.missed_dead_tuples AS missed_dead_tuples,
+
+        S.wraparound_failsafe AS wraparound_failsafe,
+        S.index_vacuum_count AS index_vacuum_count,
+        S.wal_records AS wal_records,
+        S.wal_fpi AS wal_fpi,
+        S.wal_bytes AS wal_bytes,
+
+        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
+
+    FROM pg_class C JOIN
+            pg_namespace N ON N.oid = C.relnamespace,
+            LATERAL pg_stat_get_vacuum_tables(C.oid) S
+    WHERE C.relkind IN ('r', 't', 'm');
 
 CREATE VIEW pg_stat_vacuum_indexes AS
-SELECT
-  rel.oid as relid,
-  ns.nspname AS schemaname,
-  rel.relname AS relname,
+    SELECT
+            C.oid AS relid,
+            I.oid AS indexrelid,
+            N.nspname AS schemaname,
+            C.relname AS relname,
+            I.relname AS indexrelname,
 
-  total_blks_read AS total_blks_read,
-  total_blks_hit AS total_blks_hit,
-  total_blks_dirtied AS total_blks_dirtied,
-  total_blks_written AS total_blks_written,
+            S.total_blks_read AS total_blks_read,
+            S.total_blks_hit AS total_blks_hit,
+            S.total_blks_dirtied AS total_blks_dirtied,
+            S.total_blks_written AS total_blks_written,
 
-  rel_blks_read AS rel_blks_read,
-  rel_blks_hit AS rel_blks_hit,
+            S.rel_blks_read AS rel_blks_read,
+            S.rel_blks_hit AS rel_blks_hit,
 
-  pages_deleted AS pages_deleted,
-  tuples_deleted AS tuples_deleted,
+            S.pages_deleted AS pages_deleted,
+            S.tuples_deleted AS tuples_deleted,
 
-  wal_records AS wal_records,
-  wal_fpi AS wal_fpi,
-  wal_bytes AS wal_bytes,
+            S.wal_records AS wal_records,
+            S.wal_fpi AS wal_fpi,
+            S.wal_bytes AS wal_bytes,
 
-  blk_read_time AS blk_read_time,
-  blk_write_time AS blk_write_time,
+            S.blk_read_time AS blk_read_time,
+            S.blk_write_time AS blk_write_time,
 
-  delay_time AS delay_time,
-  total_time AS total_time
-FROM
-  pg_class rel
-  JOIN pg_namespace ns ON ns.oid = rel.relnamespace,
-  LATERAL pg_stat_get_vacuum_indexes(rel.oid) stats
-WHERE rel.relkind = 'i';
+            S.delay_time AS delay_time,
+            S.total_time AS total_time
+    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
+    WHERE C.relkind IN ('r', 't', 'm');
 
 CREATE VIEW pg_stat_vacuum_database AS
-SELECT
-  db.oid as dboid,
-  db.datname AS dbname,
-
-  stats.db_blks_read AS db_blks_read,
-  stats.db_blks_hit AS db_blks_hit,
-  stats.total_blks_dirtied AS total_blks_dirtied,
-  stats.total_blks_written AS total_blks_written,
-
-  stats.wal_records AS wal_records,
-  stats.wal_fpi AS wal_fpi,
-  stats.wal_bytes AS wal_bytes,
-
-  stats.blk_read_time AS blk_read_time,
-  stats.blk_write_time AS blk_write_time,
-
-  stats.delay_time AS delay_time,
-  stats.total_time AS total_time,
-  stats.wraparound_failsafe AS wraparound_failsafe,
-  stats.errors AS errors
-FROM
-  pg_database db,
-  LATERAL pg_stat_get_vacuum_database(db.oid) stats;
\ No newline at end of file
+    SELECT
+            D.oid as dboid,
+            D.datname AS dbname,
+
+            S.db_blks_read AS db_blks_read,
+            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.wal_records AS wal_records,
+            S.wal_fpi AS wal_fpi,
+            S.wal_bytes AS wal_bytes,
+
+            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.errors AS errors
+    FROM
+            pg_database D,
+            LATERAL pg_stat_get_vacuum_database(D.oid) S;
\ No newline at end of file
diff --git a/src/backend/commands/dbcommands.c b/src/backend/commands/dbcommands.c
index 5fbbcdaabb1..c4b910cd928 100644
--- a/src/backend/commands/dbcommands.c
+++ b/src/backend/commands/dbcommands.c
@@ -1789,6 +1789,7 @@ dropdb(const char *dbname, bool missing_ok, bool force)
 	 * Tell the cumulative stats system to forget it immediately, too.
 	 */
 	pgstat_drop_database(db_id);
+	pgstat_drop_vacuum_database(db_id);
 
 	/*
index 65de45a4447..3c37d1f07ce 100644
 
 	/*
 
+	pgstat_report_vacuum_extstats(RelationGetRelid(indrel), indrel->rd_rel->relisshared,
+										&extVacReport);
 
 	/*
 
 /*
 
+/*

 	return true;
 }

 /*
 
-/* ---------

 /*
 
+void
index 00000000000..e11f19e46b2
@@ -0,0 +1,215 @@
+	ACCUMULATE_FIELD(total_blks_read);
+	ACCUMULATE_FIELD(total_blks_hit);
+	ACCUMULATE_FIELD(total_blks_dirtied);
+	ACCUMULATE_FIELD(total_blks_written);
+	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);
+	ACCUMULATE_FIELD(total_time);
+	ACCUMULATE_FIELD(tuples_deleted);
+	ACCUMULATE_FIELD(wraparound_failsafe_count);
+    ACCUMULATE_SUBFIELD(common, blks_fetched);
+    ACCUMULATE_SUBFIELD(common, blks_hit);
+        ACCUMULATE_SUBFIELD(common, tuples_deleted);
+        ACCUMULATE_SUBFIELD(common, tuples_deleted);
+pgstat_report_vacuum_extstats(Oid tableoid, bool shared,
+	pgstat_accumulate_common(&shdbentry->stats.common, &params->common);
 
-
 /*

 

 	i = 0;

 

 

 

 

 
 	i = 0;

 

 

 

 

 

 
 
index f8158aa353c..3a8a43b3813 100644
 
 /* ----------

 
-	/* Time stats. */

-	int32		errors;

 
-/* ----------
 
-	/*
 
 /* ----------

 

 /* ------

 /*

+pgstat_report_vacuum_extstats(Oid tableoid, bool shared,

 
 /*

 /* Custom stats kinds */
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index f63f25f94d8..767175e2a66 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -2283,77 +2283,81 @@ pg_stat_user_tables| SELECT relid,
     rev_all_visible_pages
    FROM pg_stat_all_tables
   WHERE ((schemaname <> ALL (ARRAY['pg_catalog'::name, 'information_schema'::name])) AND (schemaname !~ '^pg_toast'::text));
-pg_stat_vacuum_database| SELECT db.oid AS dboid,
-    db.datname AS dbname,
-    stats.db_blks_read,
-    stats.db_blks_hit,
-    stats.total_blks_dirtied,
-    stats.total_blks_written,
-    stats.wal_records,
-    stats.wal_fpi,
-    stats.wal_bytes,
-    stats.blk_read_time,
-    stats.blk_write_time,
-    stats.delay_time,
-    stats.total_time,
-    stats.wraparound_failsafe,
-    stats.errors
-   FROM pg_database db,
-    LATERAL pg_stat_get_vacuum_database(db.oid) stats(dboid, db_blks_read, db_blks_hit, total_blks_dirtied, total_blks_written, wal_records, wal_fpi, wal_bytes, blk_read_time, blk_write_time, delay_time, total_time, wraparound_failsafe, errors);
-pg_stat_vacuum_indexes| SELECT rel.oid AS relid,
-    ns.nspname AS schemaname,
-    rel.relname,
-    stats.total_blks_read,
-    stats.total_blks_hit,
-    stats.total_blks_dirtied,
-    stats.total_blks_written,
-    stats.rel_blks_read,
-    stats.rel_blks_hit,
-    stats.pages_deleted,
-    stats.tuples_deleted,
-    stats.wal_records,
-    stats.wal_fpi,
-    stats.wal_bytes,
-    stats.blk_read_time,
-    stats.blk_write_time,
-    stats.delay_time,
-    stats.total_time
-   FROM (pg_class rel
-     JOIN pg_namespace ns ON ((ns.oid = rel.relnamespace))),
-    LATERAL pg_stat_get_vacuum_indexes(rel.oid) stats(relid, total_blks_read, total_blks_hit, total_blks_dirtied, total_blks_written, rel_blks_read, rel_blks_hit, pages_deleted, tuples_deleted, wal_records, wal_fpi, wal_bytes, blk_read_time, blk_write_time, delay_time, total_time)
-  WHERE (rel.relkind = 'i'::"char");
-pg_stat_vacuum_tables| SELECT ns.nspname AS schemaname,
-    rel.relname,
-    stats.relid,
-    stats.total_blks_read,
-    stats.total_blks_hit,
-    stats.total_blks_dirtied,
-    stats.total_blks_written,
-    stats.rel_blks_read,
-    stats.rel_blks_hit,
-    stats.pages_scanned,
-    stats.pages_removed,
-    stats.vm_new_frozen_pages,
-    stats.vm_new_visible_pages,
-    stats.vm_new_visible_frozen_pages,
-    stats.missed_dead_pages,
-    stats.tuples_deleted,
-    stats.tuples_frozen,
-    stats.recently_dead_tuples,
-    stats.missed_dead_tuples,
-    stats.wraparound_failsafe,
-    stats.index_vacuum_count,
-    stats.wal_records,
-    stats.wal_fpi,
-    stats.wal_bytes,
-    stats.blk_read_time,
-    stats.blk_write_time,
-    stats.delay_time,
-    stats.total_time
-   FROM (pg_class rel
-     JOIN pg_namespace ns ON ((ns.oid = rel.relnamespace))),
-    LATERAL pg_stat_get_vacuum_tables(rel.oid) stats(relid, total_blks_read, total_blks_hit, total_blks_dirtied, total_blks_written, rel_blks_read, rel_blks_hit, pages_scanned, pages_removed, vm_new_frozen_pages, vm_new_visible_pages, vm_new_visible_frozen_pages, missed_dead_pages, tuples_deleted, tuples_frozen, recently_dead_tuples, missed_dead_tuples, wraparound_failsafe, index_vacuum_count, wal_records, wal_fpi, wal_bytes, blk_read_time, blk_write_time, delay_time, total_time)
-  WHERE (rel.relkind = 'r'::"char");
+pg_stat_vacuum_database| SELECT d.oid AS dboid,
+    d.datname AS dbname,
+    s.db_blks_read,
+    s.db_blks_hit,
+    s.total_blks_dirtied,
+    s.total_blks_written,
+    s.wal_records,
+    s.wal_fpi,
+    s.wal_bytes,
+    s.blk_read_time,
+    s.blk_write_time,
+    s.delay_time,
+    s.total_time,
+    s.wraparound_failsafe,
+    s.errors
+   FROM pg_database d,
+    LATERAL pg_stat_get_vacuum_database(d.oid) s(dboid, db_blks_read, db_blks_hit, total_blks_dirtied, total_blks_written, wal_records, wal_fpi, wal_bytes, blk_read_time, blk_write_time, delay_time, total_time, wraparound_failsafe, errors);
+pg_stat_vacuum_indexes| SELECT c.oid AS relid,
+    i.oid AS indexrelid,
+    n.nspname AS schemaname,
+    c.relname,
+    i.relname AS indexrelname,
+    s.total_blks_read,
+    s.total_blks_hit,
+    s.total_blks_dirtied,
+    s.total_blks_written,
+    s.rel_blks_read,
+    s.rel_blks_hit,
+    s.pages_deleted,
+    s.tuples_deleted,
+    s.wal_records,
+    s.wal_fpi,
+    s.wal_bytes,
+    s.blk_read_time,
+    s.blk_write_time,
+    s.delay_time,
+    s.total_time
+   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, total_blks_read, total_blks_hit, total_blks_dirtied, total_blks_written, rel_blks_read, rel_blks_hit, pages_deleted, tuples_deleted, wal_records, wal_fpi, wal_bytes, blk_read_time, blk_write_time, delay_time, total_time)
+  WHERE (c.relkind = ANY (ARRAY['r'::"char", 't'::"char", 'm'::"char"]));
+pg_stat_vacuum_tables| SELECT n.nspname AS schemaname,
+    c.relname,
+    s.relid,
+    s.total_blks_read,
+    s.total_blks_hit,
+    s.total_blks_dirtied,
+    s.total_blks_written,
+    s.rel_blks_read,
+    s.rel_blks_hit,
+    s.pages_scanned,
+    s.pages_removed,
+    s.vm_new_frozen_pages,
+    s.vm_new_visible_pages,
+    s.vm_new_visible_frozen_pages,
+    s.missed_dead_pages,
+    s.tuples_deleted,
+    s.tuples_frozen,
+    s.recently_dead_tuples,
+    s.missed_dead_tuples,
+    s.wraparound_failsafe,
+    s.index_vacuum_count,
+    s.wal_records,
+    s.wal_fpi,
+    s.wal_bytes,
+    s.blk_read_time,
+    s.blk_write_time,
+    s.delay_time,
+    s.total_time
+   FROM (pg_class c
+     JOIN pg_namespace n ON ((n.oid = c.relnamespace))),
+    LATERAL pg_stat_get_vacuum_tables(c.oid) s(relid, total_blks_read, total_blks_hit, total_blks_dirtied, total_blks_written, rel_blks_read, rel_blks_hit, pages_scanned, pages_removed, vm_new_frozen_pages, vm_new_visible_pages, vm_new_visible_frozen_pages, missed_dead_pages, tuples_deleted, tuples_frozen, recently_dead_tuples, missed_dead_tuples, wraparound_failsafe, index_vacuum_count, wal_records, wal_fpi, wal_bytes, blk_read_time, blk_write_time, delay_time, total_time)
+  WHERE (c.relkind = ANY (ARRAY['r'::"char", 't'::"char", 'm'::"char"]));
 pg_stat_wal| SELECT wal_records,
     wal_fpi,
     wal_bytes,
diff --git a/src/test/regress/expected/vacuum_index_statistics.out b/src/test/regress/expected/vacuum_index_statistics.out
index 9e5d33342c9..4654a536ad6 100644
--- a/src/test/regress/expected/vacuum_index_statistics.out
+++ b/src/test/regress/expected/vacuum_index_statistics.out
@@ -30,9 +30,9 @@ VACUUM (PARALLEL 0, BUFFER_USAGE_LIMIT 128) vestat;
 -- Must be empty.
 SELECT *
 FROM pg_stat_vacuum_indexes vt
-WHERE vt.relname = 'vestat';
- relid | schemaname | relname | total_blks_read | total_blks_hit | total_blks_dirtied | total_blks_written | rel_blks_read | rel_blks_hit | pages_deleted | tuples_deleted | wal_records | wal_fpi | wal_bytes | blk_read_time | blk_write_time | delay_time | total_time 
--------+------------+---------+-----------------+----------------+--------------------+--------------------+---------------+--------------+---------------+----------------+-------------+---------+-----------+---------------+----------------+------------+------------
+WHERE vt.indexrelname = 'vestat';
+ relid | indexrelid | schemaname | relname | indexrelname | total_blks_read | total_blks_hit | total_blks_dirtied | total_blks_written | rel_blks_read | rel_blks_hit | pages_deleted | tuples_deleted | wal_records | wal_fpi | wal_bytes | blk_read_time | blk_write_time | delay_time | total_time 
+-------+------------+------------+---------+--------------+-----------------+----------------+--------------------+--------------------+---------------+--------------+---------------+----------------+-------------+---------+-----------+---------------+----------------+------------+------------
 (0 rows)
 
 RESET track_vacuum_statistics;
@@ -55,12 +55,12 @@ ANALYZE vestat;
 SELECT oid AS ioid from pg_class where relname = 'vestat_pkey' \gset
 DELETE FROM vestat WHERE x % 2 = 0;
 -- Before the first vacuum execution extended stats view is empty.
-SELECT vt.relname,relpages,pages_deleted,tuples_deleted
+SELECT vt.indexrelname,relpages,pages_deleted,tuples_deleted
 FROM pg_stat_vacuum_indexes vt, pg_class c
-WHERE vt.relname = 'vestat_pkey' AND vt.relid = c.oid;
-   relname   | relpages | pages_deleted | tuples_deleted 
--------------+----------+---------------+----------------
- vestat_pkey |       30 |             0 |              0
+WHERE vt.indexrelname = 'vestat_pkey' AND vt.indexrelid = c.oid;
+ indexrelname | relpages | pages_deleted | tuples_deleted 
+--------------+----------+---------------+----------------
+ vestat_pkey  |       30 |             0 |              0
 (1 row)
 
 SELECT relpages AS irp
@@ -72,22 +72,22 @@ CHECKPOINT;
 -- The table and index extended vacuum statistics should show us that
 -- vacuum frozed pages and clean up pages, but pages_removed stayed the same
 -- because of not full table have cleaned up
-SELECT vt.relname,relpages-:irp = 0 AS relpages,pages_deleted = 0 AS pages_deleted,tuples_deleted > 0 AS tuples_deleted
+SELECT vt.indexrelname,relpages-:irp = 0 AS relpages,pages_deleted = 0 AS pages_deleted,tuples_deleted > 0 AS tuples_deleted
 FROM pg_stat_vacuum_indexes vt, pg_class c
-WHERE vt.relname = 'vestat_pkey' AND vt.relid = c.oid;
-   relname   | relpages | pages_deleted | tuples_deleted 
--------------+----------+---------------+----------------
- vestat_pkey | t        | t             | t
+WHERE vt.indexrelname = 'vestat_pkey' AND vt.indexrelid = c.oid;
+ indexrelname | relpages | pages_deleted | tuples_deleted 
+--------------+----------+---------------+----------------
+ vestat_pkey  | t        | t             | t
 (1 row)
 
-SELECT vt.relname,relpages AS irp,pages_deleted AS ipd,tuples_deleted AS itd
+SELECT vt.indexrelname,relpages AS irp,pages_deleted AS ipd,tuples_deleted AS itd
 FROM pg_stat_vacuum_indexes vt, pg_class c
-WHERE vt.relname = 'vestat_pkey' AND vt.relid = c.oid \gset
+WHERE vt.indexrelname = 'vestat_pkey' AND vt.indexrelid = c.oid \gset
 -- Store WAL advances into variables
-SELECT wal_records AS iwr,wal_bytes AS iwb,wal_fpi AS ifpi FROM pg_stat_vacuum_indexes WHERE relname = 'vestat_pkey' \gset
+SELECT wal_records AS iwr,wal_bytes AS iwb,wal_fpi AS ifpi FROM pg_stat_vacuum_indexes WHERE indexrelname = 'vestat_pkey' \gset
 -- Look into WAL records deltas.
 SELECT wal_records > 0 AS diWR, wal_bytes > 0 AS diWB
-FROM pg_stat_vacuum_indexes WHERE relname = 'vestat_pkey';
+FROM pg_stat_vacuum_indexes WHERE indexrelname = 'vestat_pkey';
  diwr | diwb 
 ------+------
  t    | t
@@ -98,20 +98,20 @@ VACUUM (PARALLEL 0, BUFFER_USAGE_LIMIT 128, INDEX_CLEANUP ON) vestat;
 -- it is necessary to check the wal statistics
 CHECKPOINT;
 -- pages_removed must be increased
-SELECT vt.relname,relpages-:irp = 0 AS relpages,pages_deleted-:ipd > 0 AS pages_deleted,tuples_deleted-:itd > 0 AS tuples_deleted
+SELECT vt.indexrelname,relpages-:irp = 0 AS relpages,pages_deleted-:ipd > 0 AS pages_deleted,tuples_deleted-:itd > 0 AS tuples_deleted
 FROM pg_stat_vacuum_indexes vt, pg_class c
-WHERE vt.relname = 'vestat_pkey' AND vt.relid = c.oid;
-   relname   | relpages | pages_deleted | tuples_deleted 
--------------+----------+---------------+----------------
- vestat_pkey | t        | t             | t
+WHERE vt.indexrelname = 'vestat_pkey' AND vt.indexrelid = c.oid;
+ indexrelname | relpages | pages_deleted | tuples_deleted 
+--------------+----------+---------------+----------------
+ vestat_pkey  | t        | t             | t
 (1 row)
 
-SELECT vt.relname,relpages AS irp,pages_deleted AS ipd,tuples_deleted AS itd
+SELECT vt.indexrelname,relpages AS irp,pages_deleted AS ipd,tuples_deleted AS itd
 FROM pg_stat_vacuum_indexes vt, pg_class c
-WHERE vt.relname = 'vestat_pkey' AND vt.relid = c.oid \gset
+WHERE vt.indexrelname = 'vestat_pkey' AND vt.indexrelid = c.oid \gset
 -- Store WAL advances into variables
 SELECT wal_records-:iwr AS diwr, wal_bytes-:iwb AS diwb, wal_fpi-:ifpi AS difpi
-FROM pg_stat_vacuum_indexes WHERE relname = 'vestat_pkey' \gset
+FROM pg_stat_vacuum_indexes WHERE indexrelname = 'vestat_pkey' \gset
 -- WAL advance should be detected.
 SELECT :diwr > 0 AS diWR, :diwb > 0 AS diWB;
  diwr | diwb 
@@ -120,7 +120,7 @@ SELECT :diwr > 0 AS diWR, :diwb > 0 AS diWB;
 (1 row)
 
 -- Store WAL advances into variables
-SELECT wal_records AS iwr,wal_bytes AS iwb,wal_fpi AS ifpi FROM pg_stat_vacuum_indexes WHERE relname = 'vestat_pkey' \gset
+SELECT wal_records AS iwr,wal_bytes AS iwb,wal_fpi AS ifpi FROM pg_stat_vacuum_indexes WHERE indexrelname = 'vestat_pkey' \gset
 INSERT INTO vestat SELECT x FROM generate_series(1,:sample_size) as x;
 DELETE FROM vestat WHERE x % 2 = 0;
 -- VACUUM FULL doesn't report to stat collector. So, no any advancements of statistics
@@ -130,7 +130,7 @@ VACUUM FULL vestat;
 CHECKPOINT;
 -- Store WAL advances into variables
 SELECT wal_records-:iwr AS diwr2, wal_bytes-:iwb AS diwb2, wal_fpi-:ifpi AS difpi2
-FROM pg_stat_vacuum_indexes WHERE relname = 'vestat_pkey' \gset
+FROM pg_stat_vacuum_indexes WHERE indexrelname = 'vestat_pkey' \gset
 -- WAL and other statistics advance should not be detected.
 SELECT :diwr2=0 AS diWR, :difpi2=0 AS iFPI, :diwb2=0 AS diWB;
  diwr | ifpi | diwb 
@@ -138,19 +138,19 @@ SELECT :diwr2=0 AS diWR, :difpi2=0 AS iFPI, :diwb2=0 AS diWB;
  t    | t    | t
 (1 row)
 
-SELECT vt.relname,relpages-:irp < 0 AS relpages,pages_deleted-:ipd = 0 AS pages_deleted,tuples_deleted-:itd = 0 AS tuples_deleted
+SELECT vt.indexrelname,relpages-:irp < 0 AS relpages,pages_deleted-:ipd = 0 AS pages_deleted,tuples_deleted-:itd = 0 AS tuples_deleted
 FROM pg_stat_vacuum_indexes vt, pg_class c
-WHERE vt.relname = 'vestat_pkey' AND vt.relid = c.oid;
-   relname   | relpages | pages_deleted | tuples_deleted 
--------------+----------+---------------+----------------
- vestat_pkey | t        | t             | t
+WHERE vt.indexrelname = 'vestat_pkey' AND vt.indexrelid = c.oid;
+ indexrelname | relpages | pages_deleted | tuples_deleted 
+--------------+----------+---------------+----------------
+ vestat_pkey  | t        | t             | t
 (1 row)
 
-SELECT vt.relname,relpages AS irp,pages_deleted AS ipd,tuples_deleted AS itd
+SELECT vt.indexrelname,relpages AS irp,pages_deleted AS ipd,tuples_deleted AS itd
 FROM pg_stat_vacuum_indexes vt, pg_class c
-WHERE vt.relname = 'vestat_pkey' AND vt.relid = c.oid \gset
+WHERE vt.indexrelname = 'vestat_pkey' AND vt.indexrelid = c.oid \gset
 -- Store WAL advances into variables
-SELECT wal_records AS iwr,wal_bytes AS iwb,wal_fpi AS ifpi FROM pg_stat_vacuum_indexes WHERE relname = 'vestat_pkey' \gset
+SELECT wal_records AS iwr,wal_bytes AS iwb,wal_fpi AS ifpi FROM pg_stat_vacuum_indexes WHERE indexrelname = 'vestat_pkey' \gset
 DELETE FROM vestat;
 TRUNCATE vestat;
 VACUUM (PARALLEL 0, BUFFER_USAGE_LIMIT 128, INDEX_CLEANUP ON) vestat;
@@ -158,7 +158,7 @@ VACUUM (PARALLEL 0, BUFFER_USAGE_LIMIT 128, INDEX_CLEANUP ON) vestat;
 CHECKPOINT;
 -- Store WAL advances into variables after removing all tuples from the table
 SELECT wal_records-:iwr AS diwr3, wal_bytes-:iwb AS diwb3, wal_fpi-:ifpi AS difpi3
-FROM pg_stat_vacuum_indexes WHERE relname = 'vestat_pkey' \gset
+FROM pg_stat_vacuum_indexes WHERE indexrelname = 'vestat_pkey' \gset
 --There are nothing changed
 SELECT :diwr3=0 AS diWR, :difpi3=0 AS iFPI, :diwb3=0 AS diWB;
  diwr | ifpi | diwb 
@@ -171,12 +171,12 @@ SELECT :diwr3=0 AS diWR, :difpi3=0 AS iFPI, :diwb3=0 AS diWB;
 -- in vacuum extended statistics.
 -- The pages_frozen, pages_scanned values shouldn't be changed
 --
-SELECT vt.relname,relpages-:irp = 0 AS relpages,pages_deleted-:ipd = 0 AS pages_deleted,tuples_deleted-:itd = 0 AS tuples_deleted
+SELECT vt.indexrelname,relpages-:irp = 0 AS relpages,pages_deleted-:ipd = 0 AS pages_deleted,tuples_deleted-:itd = 0 AS tuples_deleted
 FROM pg_stat_vacuum_indexes vt, pg_class c
-WHERE vt.relname = 'vestat_pkey' AND vt.relid = c.oid;
-   relname   | relpages | pages_deleted | tuples_deleted 
--------------+----------+---------------+----------------
- vestat_pkey | f        | t             | t
+WHERE vt.indexrelname = 'vestat_pkey' AND vt.indexrelid = c.oid;
+ indexrelname | relpages | pages_deleted | tuples_deleted 
+--------------+----------+---------------+----------------
+ vestat_pkey  | f        | t             | t
 (1 row)
 
 DROP TABLE vestat;
diff --git a/src/test/regress/sql/vacuum_index_statistics.sql b/src/test/regress/sql/vacuum_index_statistics.sql
index 9b7e645187d..57e5420b9b6 100644
--- a/src/test/regress/sql/vacuum_index_statistics.sql
+++ b/src/test/regress/sql/vacuum_index_statistics.sql
@@ -27,7 +27,7 @@ VACUUM (PARALLEL 0, BUFFER_USAGE_LIMIT 128) vestat;
 -- Must be empty.
 SELECT *
 FROM pg_stat_vacuum_indexes vt
-WHERE vt.relname = 'vestat';
+WHERE vt.indexrelname = 'vestat';
 
 RESET track_vacuum_statistics;
 DROP TABLE vestat CASCADE;
@@ -49,9 +49,9 @@ SELECT oid AS ioid from pg_class where relname = 'vestat_pkey' \gset
 
 DELETE FROM vestat WHERE x % 2 = 0;
 -- Before the first vacuum execution extended stats view is empty.
-SELECT vt.relname,relpages,pages_deleted,tuples_deleted
+SELECT vt.indexrelname,relpages,pages_deleted,tuples_deleted
 FROM pg_stat_vacuum_indexes vt, pg_class c
-WHERE vt.relname = 'vestat_pkey' AND vt.relid = c.oid;
+WHERE vt.indexrelname = 'vestat_pkey' AND vt.indexrelid = c.oid;
 SELECT relpages AS irp
 FROM pg_class c
 WHERE relname = 'vestat_pkey' \gset
@@ -63,19 +63,19 @@ CHECKPOINT;
 -- The table and index extended vacuum statistics should show us that
 -- vacuum frozed pages and clean up pages, but pages_removed stayed the same
 -- because of not full table have cleaned up
-SELECT vt.relname,relpages-:irp = 0 AS relpages,pages_deleted = 0 AS pages_deleted,tuples_deleted > 0 AS tuples_deleted
+SELECT vt.indexrelname,relpages-:irp = 0 AS relpages,pages_deleted = 0 AS pages_deleted,tuples_deleted > 0 AS tuples_deleted
 FROM pg_stat_vacuum_indexes vt, pg_class c
-WHERE vt.relname = 'vestat_pkey' AND vt.relid = c.oid;
-SELECT vt.relname,relpages AS irp,pages_deleted AS ipd,tuples_deleted AS itd
+WHERE vt.indexrelname = 'vestat_pkey' AND vt.indexrelid = c.oid;
+SELECT vt.indexrelname,relpages AS irp,pages_deleted AS ipd,tuples_deleted AS itd
 FROM pg_stat_vacuum_indexes vt, pg_class c
-WHERE vt.relname = 'vestat_pkey' AND vt.relid = c.oid \gset
+WHERE vt.indexrelname = 'vestat_pkey' AND vt.indexrelid = c.oid \gset
 
 -- Store WAL advances into variables
-SELECT wal_records AS iwr,wal_bytes AS iwb,wal_fpi AS ifpi FROM pg_stat_vacuum_indexes WHERE relname = 'vestat_pkey' \gset
+SELECT wal_records AS iwr,wal_bytes AS iwb,wal_fpi AS ifpi FROM pg_stat_vacuum_indexes WHERE indexrelname = 'vestat_pkey' \gset
 
 -- Look into WAL records deltas.
 SELECT wal_records > 0 AS diWR, wal_bytes > 0 AS diWB
-FROM pg_stat_vacuum_indexes WHERE relname = 'vestat_pkey';
+FROM pg_stat_vacuum_indexes WHERE indexrelname = 'vestat_pkey';
 
 DELETE FROM vestat;;
 VACUUM (PARALLEL 0, BUFFER_USAGE_LIMIT 128, INDEX_CLEANUP ON) vestat;
@@ -83,22 +83,22 @@ VACUUM (PARALLEL 0, BUFFER_USAGE_LIMIT 128, INDEX_CLEANUP ON) vestat;
 CHECKPOINT;
 
 -- pages_removed must be increased
-SELECT vt.relname,relpages-:irp = 0 AS relpages,pages_deleted-:ipd > 0 AS pages_deleted,tuples_deleted-:itd > 0 AS tuples_deleted
+SELECT vt.indexrelname,relpages-:irp = 0 AS relpages,pages_deleted-:ipd > 0 AS pages_deleted,tuples_deleted-:itd > 0 AS tuples_deleted
 FROM pg_stat_vacuum_indexes vt, pg_class c
-WHERE vt.relname = 'vestat_pkey' AND vt.relid = c.oid;
-SELECT vt.relname,relpages AS irp,pages_deleted AS ipd,tuples_deleted AS itd
+WHERE vt.indexrelname = 'vestat_pkey' AND vt.indexrelid = c.oid;
+SELECT vt.indexrelname,relpages AS irp,pages_deleted AS ipd,tuples_deleted AS itd
 FROM pg_stat_vacuum_indexes vt, pg_class c
-WHERE vt.relname = 'vestat_pkey' AND vt.relid = c.oid \gset
+WHERE vt.indexrelname = 'vestat_pkey' AND vt.indexrelid = c.oid \gset
 
 -- Store WAL advances into variables
 SELECT wal_records-:iwr AS diwr, wal_bytes-:iwb AS diwb, wal_fpi-:ifpi AS difpi
-FROM pg_stat_vacuum_indexes WHERE relname = 'vestat_pkey' \gset
+FROM pg_stat_vacuum_indexes WHERE indexrelname = 'vestat_pkey' \gset
 
 -- WAL advance should be detected.
 SELECT :diwr > 0 AS diWR, :diwb > 0 AS diWB;
 
 -- Store WAL advances into variables
-SELECT wal_records AS iwr,wal_bytes AS iwb,wal_fpi AS ifpi FROM pg_stat_vacuum_indexes WHERE relname = 'vestat_pkey' \gset
+SELECT wal_records AS iwr,wal_bytes AS iwb,wal_fpi AS ifpi FROM pg_stat_vacuum_indexes WHERE indexrelname = 'vestat_pkey' \gset
 
 INSERT INTO vestat SELECT x FROM generate_series(1,:sample_size) as x;
 DELETE FROM vestat WHERE x % 2 = 0;
@@ -110,20 +110,20 @@ CHECKPOINT;
 
 -- Store WAL advances into variables
 SELECT wal_records-:iwr AS diwr2, wal_bytes-:iwb AS diwb2, wal_fpi-:ifpi AS difpi2
-FROM pg_stat_vacuum_indexes WHERE relname = 'vestat_pkey' \gset
+FROM pg_stat_vacuum_indexes WHERE indexrelname = 'vestat_pkey' \gset
 
 -- WAL and other statistics advance should not be detected.
 SELECT :diwr2=0 AS diWR, :difpi2=0 AS iFPI, :diwb2=0 AS diWB;
 
-SELECT vt.relname,relpages-:irp < 0 AS relpages,pages_deleted-:ipd = 0 AS pages_deleted,tuples_deleted-:itd = 0 AS tuples_deleted
+SELECT vt.indexrelname,relpages-:irp < 0 AS relpages,pages_deleted-:ipd = 0 AS pages_deleted,tuples_deleted-:itd = 0 AS tuples_deleted
 FROM pg_stat_vacuum_indexes vt, pg_class c
-WHERE vt.relname = 'vestat_pkey' AND vt.relid = c.oid;
-SELECT vt.relname,relpages AS irp,pages_deleted AS ipd,tuples_deleted AS itd
+WHERE vt.indexrelname = 'vestat_pkey' AND vt.indexrelid = c.oid;
+SELECT vt.indexrelname,relpages AS irp,pages_deleted AS ipd,tuples_deleted AS itd
 FROM pg_stat_vacuum_indexes vt, pg_class c
-WHERE vt.relname = 'vestat_pkey' AND vt.relid = c.oid \gset
+WHERE vt.indexrelname = 'vestat_pkey' AND vt.indexrelid = c.oid \gset
 
 -- Store WAL advances into variables
-SELECT wal_records AS iwr,wal_bytes AS iwb,wal_fpi AS ifpi FROM pg_stat_vacuum_indexes WHERE relname = 'vestat_pkey' \gset
+SELECT wal_records AS iwr,wal_bytes AS iwb,wal_fpi AS ifpi FROM pg_stat_vacuum_indexes WHERE indexrelname = 'vestat_pkey' \gset
 
 DELETE FROM vestat;
 TRUNCATE vestat;
@@ -133,7 +133,7 @@ CHECKPOINT;
 
 -- Store WAL advances into variables after removing all tuples from the table
 SELECT wal_records-:iwr AS diwr3, wal_bytes-:iwb AS diwb3, wal_fpi-:ifpi AS difpi3
-FROM pg_stat_vacuum_indexes WHERE relname = 'vestat_pkey' \gset
+FROM pg_stat_vacuum_indexes WHERE indexrelname = 'vestat_pkey' \gset
 
 --There are nothing changed
 SELECT :diwr3=0 AS diWR, :difpi3=0 AS iFPI, :diwb3=0 AS diWB;
@@ -143,9 +143,9 @@ SELECT :diwr3=0 AS diWR, :difpi3=0 AS iFPI, :diwb3=0 AS diWB;
 -- in vacuum extended statistics.
 -- The pages_frozen, pages_scanned values shouldn't be changed
 --
-SELECT vt.relname,relpages-:irp = 0 AS relpages,pages_deleted-:ipd = 0 AS pages_deleted,tuples_deleted-:itd = 0 AS tuples_deleted
+SELECT vt.indexrelname,relpages-:irp = 0 AS relpages,pages_deleted-:ipd = 0 AS pages_deleted,tuples_deleted-:itd = 0 AS tuples_deleted
 FROM pg_stat_vacuum_indexes vt, pg_class c
-WHERE vt.relname = 'vestat_pkey' AND vt.relid = c.oid;
+WHERE vt.indexrelname = 'vestat_pkey' AND vt.indexrelid = c.oid;
 
 DROP TABLE vestat;

From 5f07a5bbe804b858e7e15e8bb64c96ccc787b184 Mon Sep 17 00:00:00 2001

---

---

From: Alena Rybakina <[email-masked]>
Date: Mon, 1 Sep 2025 22:13:18 +0300

I've rebased the patches to the current HEAD.

-- 
Regards,
Alena Rybakina
Postgres Professional

From bf8e64e8996f718465b0ee8093d2e5efed6064a8 Mon Sep 17 00:00:00 2001
From: Alena Rybakina <[email-masked]>
Date: Mon, 1 Sep 2025 21:40:24 +0300
Subject: [PATCH 1/5] Machinery for grabbing an extended vacuum statistics on 
 table relations.

 22 files changed, 1096 insertions(+), 16 deletions(-)

diff --git a/src/backend/access/heap/vacuumlazy.c b/src/backend/access/heap/vacuumlazy.c
index 932701d8420..980101e9b97 100644
--- a/src/backend/access/heap/vacuumlazy.c
+++ b/src/backend/access/heap/vacuumlazy.c
@@ -289,6 +289,7 @@ typedef struct LVRelState
 	/* Error reporting state */
 	char	   *dbname;
 	char	   *relnamespace;
+	Oid			reloid;
 	char	   *relname;
 	char	   *indname;		/* Current index name */
 	BlockNumber blkno;			/* used only for heap operations */
@@ -407,6 +408,8 @@ typedef struct LVRelState
 	 * been permanently disabled.
 	 */
 	BlockNumber eager_scan_remaining_fails;
+
+	int32		wraparound_failsafe_count; /* number of emergency vacuums to prevent anti-wraparound shutdown */
 } LVRelState;

@@ -418,6 +421,18 @@ typedef struct LVSavedErrInfo
 	VacErrPhase phase;
 } LVSavedErrInfo;
 
+/*
 
@@ -474,6 +489,106 @@ static void update_vacuum_error_info(LVRelState *vacrel,
 
+/* ----------

 /*
@@ -632,6 +747,13 @@ heap_vacuum_rel(Relation rel, const VacuumParams params,
 
 	verbose = (params.options & VACOPT_VERBOSE) != 0;
 	instrument = (verbose || (AmAutoVacuumWorkerProcess() &&
@@ -651,7 +773,7 @@ heap_vacuum_rel(Relation rel, const VacuumParams params,
 
 	pgstat_progress_start_command(PROGRESS_COMMAND_VACUUM,
 								  RelationGetRelid(rel));
-
+	extvac_stats_start(rel, &extVacCounters);
 	/*
 	 * Setup error traceback support for ereport() first.  The idea is to set
 	 * up an error context callback to display additional information on any
@@ -668,6 +790,7 @@ heap_vacuum_rel(Relation rel, const VacuumParams params,
 	vacrel->dbname = get_database_name(MyDatabaseId);
 	vacrel->relnamespace = get_namespace_name(RelationGetNamespace(rel));
 	vacrel->relname = pstrdup(RelationGetRelationName(rel));
+	vacrel->reloid = RelationGetRelid(rel);
 	vacrel->indname = NULL;
 	vacrel->phase = VACUUM_ERRCB_PHASE_UNKNOWN;
 	vacrel->verbose = verbose;
@@ -776,6 +899,7 @@ heap_vacuum_rel(Relation rel, const VacuumParams params,
 	vacrel->aggressive = vacuum_get_cutoffs(rel, params, &vacrel->cutoffs);
 	vacrel->rel_pages = orig_rel_pages = RelationGetNumberOfBlocks(rel);
 	vacrel->vistest = GlobalVisTestFor(rel);
+	vacrel->wraparound_failsafe_count = 0;
 
 	/* Initialize state used to track oldest extant XID/MXID */
 	vacrel->NewRelfrozenXid = vacrel->cutoffs.OldestXmin;
@@ -924,6 +1048,26 @@ heap_vacuum_rel(Relation rel, const VacuumParams params,
 						vacrel->NewRelfrozenXid, vacrel->NewRelminMxid,
 						&frozenxid_updated, &minmulti_updated, false);
 
@@ -939,7 +1083,8 @@ heap_vacuum_rel(Relation rel, const VacuumParams params,
 
 	if (instrument)
@@ -2961,6 +3106,7 @@ lazy_check_wraparound_failsafe(LVRelState *vacrel)
 		int64		progress_val[2] = {0, 0};

 		/*
index 953ad4a4843..a21e77cd551 100644
 
 	if (map[mapByte] & mask)
 	{
+		/*
 
 		MarkBufferDirty(vmbuf);
index 1b3c5a55882..6480c3fc92a 100644
@@ -716,7 +716,9 @@ CREATE VIEW pg_stat_all_tables AS
@@ -1420,3 +1422,51 @@ REVOKE ALL ON pg_aios FROM PUBLIC;
index 733ef40ae7c..d8776ff1901 100644
@@ -116,6 +116,9 @@ pg_atomic_uint32 *VacuumSharedCostBalance = NULL;
 
@@ -2533,6 +2536,7 @@ vacuum_delay_point(bool is_analyze)
 			exit(1);
 
 		VacuumCostBalance = 0;
 
 		/*
index ffb5b8cce34..a19d4a770b8 100644

 
 
 /* ----------
  * state shared with pgstat_*.c
@@ -265,7 +265,6 @@ static bool pgstat_is_initialized = false;
 static bool pgstat_is_shutdown = false;
 #endif
 
-
 /*
  * The different kinds of built-in statistics.
  *
@@ -883,7 +882,6 @@ pgstat_reset_of_kind(PgStat_Kind kind)
 		pgstat_reset_entries_of_kind(kind, ts);
 }
 
-
 /* ------------------------------------------------------------
  * Fetching of stats
  * ------------------------------------------------------------
@@ -952,7 +950,7 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid)

 	/* if caching is desired, look up in cache */
 	if (pgstat_fetch_consistency > PGSTAT_FETCH_CONSISTENCY_NONE)
@@ -1068,7 +1066,7 @@ pgstat_snapshot_fixed(PgStat_Kind kind)
 		pgstat_clear_snapshot();

@@ -1119,7 +1117,7 @@ pgstat_prep_snapshot(void)
 }
 
 static void
index 69df741cbf6..e023926ff05 100644

 /*

index c756c2bebaa..ee461ea378d 100644
 
@@ -2260,3 +2266,144 @@ pg_stat_have_stats(PG_FUNCTION_ARGS)
 
index b7b9692f8c8..f0ecf86e514 100644
@@ -1627,6 +1627,19 @@ getinternalerrposition(void)
 
+/*
 
 /*
  * Functions to allow construction of error message strings separately from
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index f137129209f..5af593aa1a7 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -1568,6 +1568,16 @@ struct config_bool ConfigureNamesBool[] =
 		NULL, NULL, NULL
 	},
 
+	{
index a9d8293474a..05713b4b65c 100644

 # - Monitoring -
index 118d6da1ace..687d51c2a6a 100644
@@ -12576,4 +12576,22 @@
 
+{ oid => '8001',
index 14eeccbd718..4d05e1a0fac 100644
 
index 202bd2d5ace..4fd114e9a5f 100644
 
+/* ----------
+ *
+ * ExtVacReport
+ *

 /* ----------

 

 /* ------

 /*
index 675f4f5f469..356dadd6b0a 100644

 /*----------
index 9f1e997d81b..815dcee23b2 100644
@@ -97,6 +97,7 @@ test: timeouts
index 35e8aad7701..349e7deba01 100644
@@ -1833,7 +1833,9 @@ pg_stat_all_tables| SELECT c.oid AS relid,
@@ -2232,7 +2234,9 @@ pg_stat_sys_tables| SELECT relid,
@@ -2284,9 +2288,43 @@ pg_stat_user_tables| SELECT relid,
index fbffc67ae60..cd779ab8eca 100644

From c420c88f6c6aa326de47954cf552389efa7c3c37 Mon Sep 17 00:00:00 2001
From: Alena Rybakina <[email-masked]>
Date: Mon, 1 Sep 2025 21:42:25 +0300
Subject: [PATCH 2/5] Machinery for grabbing an extended vacuum statistics on 
 index relations.

 src/backend/access/heap/vacuumlazy.c          | 268 ++++++++++++++----
 15 files changed, 864 insertions(+), 90 deletions(-)

diff --git a/src/backend/access/heap/vacuumlazy.c b/src/backend/access/heap/vacuumlazy.c
index 980101e9b97..e206aeecc9c 100644
--- a/src/backend/access/heap/vacuumlazy.c
+++ b/src/backend/access/heap/vacuumlazy.c
@@ -290,6 +290,7 @@ typedef struct LVRelState
 	char	   *dbname;
 	char	   *relnamespace;
 	Oid			reloid;
+	Oid			indoid;
 	char	   *relname;
 	char	   *indname;		/* Current index name */
 	BlockNumber blkno;			/* used only for heap operations */
@@ -410,6 +411,8 @@ typedef struct LVRelState
 	BlockNumber eager_scan_remaining_fails;

@@ -421,19 +424,6 @@ typedef struct LVSavedErrInfo
 	VacErrPhase phase;
 } LVSavedErrInfo;
 
-/*
@@ -555,27 +545,25 @@ extvac_stats_end(Relation rel, LVExtStatCounters *counters,

 

 	if (!rel->pgstat_info || !pgstat_track_counts)
 		/*
@@ -584,12 +572,131 @@ extvac_stats_end(Relation rel, LVExtStatCounters *counters,
 		 */
 		return;
 
-	report->blks_fetched =
 
+void
 
 /*
  * Helper to set up the eager scanning state for vacuuming a single relation.
@@ -752,7 +859,7 @@ heap_vacuum_rel(Relation rel, const VacuumParams params,
 	ExtVacReport allzero;
 
 	extVacReport = allzero;
 
 	verbose = (params.options & VACOPT_VERBOSE) != 0;
@@ -799,6 +906,8 @@ heap_vacuum_rel(Relation rel, const VacuumParams params,
 	errcallback.previous = error_context_stack;
 	error_context_stack = &errcallback;
 
@@ -1051,23 +1160,6 @@ heap_vacuum_rel(Relation rel, const VacuumParams params,
 
@@ -1078,13 +1170,34 @@ heap_vacuum_rel(Relation rel, const VacuumParams params,
 
 	if (instrument)
@@ -2776,10 +2889,20 @@ lazy_vacuum_all_indexes(LVRelState *vacrel)
 	}
 	else
 	{
 
@@ -3189,10 +3312,20 @@ lazy_cleanup_all_indexes(LVRelState *vacrel)
 
@@ -3218,6 +3351,11 @@ lazy_vacuum_one_index(Relation indrel, IndexBulkDeleteResult *istat,
 
 	ivinfo.index = indrel;
@@ -3236,6 +3374,7 @@ lazy_vacuum_one_index(Relation indrel, IndexBulkDeleteResult *istat,
@@ -3244,6 +3383,19 @@ lazy_vacuum_one_index(Relation indrel, IndexBulkDeleteResult *istat,
 
@@ -3268,6 +3420,11 @@ lazy_cleanup_one_index(Relation indrel, IndexBulkDeleteResult *istat,
 
 	ivinfo.index = indrel;
@@ -3287,12 +3444,25 @@ lazy_cleanup_one_index(Relation indrel, IndexBulkDeleteResult *istat,

index 6480c3fc92a..eb63be1833a 100644
@@ -1470,3 +1470,35 @@ FROM pg_class rel
 
 	/*

index a19d4a770b8..2b7058e5c01 100644
@@ -1162,6 +1162,10 @@ pgstat_build_snapshot(PgStat_Kind statKind)
 		if (p->dropped)
 			continue;

index e023926ff05..c6194584b35 100644

index ee461ea378d..defe1990e11 100644
@@ -2374,18 +2374,19 @@ pg_stat_get_vacuum_tables(PG_FUNCTION_ARGS)

 	values[i++] = Int64GetDatum(extvacuum->wal_records);
 	values[i++] = Int64GetDatum(extvacuum->wal_fpi);
@@ -2404,6 +2405,116 @@ pg_stat_get_vacuum_tables(PG_FUNCTION_ARGS)

index 5af593aa1a7..ac6427cf1e9 100644
@@ -1570,7 +1570,7 @@ struct config_bool ConfigureNamesBool[] =
 
 	{
index 687d51c2a6a..2fbd001178e 100644
@@ -12594,4 +12594,13 @@
index 4d05e1a0fac..dcc542750b8 100644
 
 /*
 
+/*
 
index 4fd114e9a5f..aec25ad2262 100644

 
 /* ----------

 step s1_commit: COMMIT;

index 349e7deba01..fdd5341bdfc 100644
@@ -2293,6 +2293,28 @@ pg_stat_user_tables| SELECT relid,
index cd779ab8eca..4eb03353104 100644

From 789ed32efc0ac06792d4e6cbe6759af7ad8aeb3c Mon Sep 17 00:00:00 2001
From: Alena Rybakina <[email-masked]>
Date: Mon, 1 Sep 2025 21:43:33 +0300
Subject: [PATCH 3/5] Machinery for grabbing an extended vacuum statistics on 
 databases.

index e206aeecc9c..3c3e23cd943 100644
@@ -659,7 +659,7 @@ accumulate_heap_vacuum_statistics(LVRelState *vacrel, ExtVacReport *extVacStats)
 
 	extVacStats->blk_read_time -= vacrel->extVacReportIdx.blk_read_time;
 	extVacStats->blk_write_time -= vacrel->extVacReportIdx.blk_write_time;
@@ -4075,6 +4075,9 @@ vacuum_error_callback(void *arg)
 	switch (errinfo->phase)
 	{
 		case VACUUM_ERRCB_PHASE_SCAN_HEAP:
+			if(geterrelevel() == ERROR)
+					pgstat_report_vacuum_error(errinfo->reloid, PGSTAT_EXTVAC_TABLE);
+
 			if (BlockNumberIsValid(errinfo->blkno))
 			{
 				if (OffsetNumberIsValid(errinfo->offnum))
@@ -4090,6 +4093,9 @@ vacuum_error_callback(void *arg)
 			break;
 
@@ -4105,16 +4111,25 @@ vacuum_error_callback(void *arg)
 			break;

 
index eb63be1833a..e5370211d74 100644
@@ -1501,4 +1501,29 @@ FROM
index 2b7058e5c01..614daf60372 100644

 /* ----------

 	return true;
 }
index c6194584b35..bf7ab345be0 100644
 
+/* ---------
 
 /*
 
index defe1990e11..1c39ada2c3e 100644
@@ -2385,7 +2385,7 @@ pg_stat_get_vacuum_tables(PG_FUNCTION_ARGS)

 	values[i++] = Int64GetDatum(extvacuum->wal_records);
@@ -2515,6 +2515,104 @@ pg_stat_get_vacuum_indexes(PG_FUNCTION_ARGS)

index ac6427cf1e9..6f16db4bee9 100644
@@ -1574,7 +1574,7 @@ struct config_bool ConfigureNamesBool[] =
 
index 2fbd001178e..9781d4f59da 100644
@@ -12595,12 +12595,21 @@
index aec25ad2262..50876f5446f 100644

+	int32		errors;
 
 	/* ----------
 
 /*
 
 session s1
+setup		{
 
 session s2
+setup		{
index fdd5341bdfc..14f19e5bcbe 100644
@@ -2293,6 +2293,23 @@ pg_stat_user_tables| SELECT relid,

 DROP TABLE vestat;

index 4eb03353104..798692cf21a 100644

 

 DROP TABLE vestat;

 

 

From c7acebdd3a7b21b615c087aef9c6acd30e49c4a4 Mon Sep 17 00:00:00 2001
From: Alena Rybakina <[email-masked]>
Date: Mon, 1 Sep 2025 21:44:59 +0300
Subject: [PATCH 4/5] Vacuum statistics have been separated from regular
 relation and database statistics to reduce memory usage. Dedicated
 PGSTAT_KIND_VACUUM_RELATION and PGSTAT_KIND_VACUUM_DB entries were added to
 the stats collector to efficiently allocate memory for vacuum-specific
 metrics, which require significantly more space per relation.

---
 src/backend/access/heap/vacuumlazy.c          | 196 ++++++------
 19 files changed, 805 insertions(+), 733 deletions(-)

diff --git a/src/backend/access/heap/vacuumlazy.c b/src/backend/access/heap/vacuumlazy.c
index 3c3e23cd943..606a03827c7 100644
--- a/src/backend/access/heap/vacuumlazy.c
+++ b/src/backend/access/heap/vacuumlazy.c
@@ -412,7 +412,7 @@ typedef struct LVRelState

@@ -524,7 +524,7 @@ extvac_stats_start(Relation rel, LVExtStatCounters *counters)
  */
 static void
 extvac_stats_end(Relation rel, LVExtStatCounters *counters,
-				  ExtVacReport *report)
+				 PgStat_CommonCounts *report)
 {
 	WalUsage	walusage;
 	BufferUsage	bufusage;
@@ -585,6 +585,8 @@ extvac_stats_start_idx(Relation rel, IndexBulkDeleteResult *stats,
 	if(!pgstat_track_vacuum_statistics)
 		return;
 
@@ -602,11 +604,13 @@ extvac_stats_start_idx(Relation rel, IndexBulkDeleteResult *stats,
 
 void

 	if (stats != NULL)
@@ -617,7 +621,7 @@ extvac_stats_end_idx(Relation rel, IndexBulkDeleteResult *stats,
 		 */
 
@@ -640,7 +644,7 @@ extvac_stats_end_idx(Relation rel, IndexBulkDeleteResult *stats,
@@ -652,49 +656,49 @@ accumulate_heap_vacuum_statistics(LVRelState *vacrel, ExtVacReport *extVacStats)

 
 }
 
 static void

@@ -855,12 +859,10 @@ heap_vacuum_rel(Relation rel, const VacuumParams params,

 	verbose = (params.options & VACOPT_VERBOSE) != 0;
 	instrument = (verbose || (AmAutoVacuumWorkerProcess() &&
@@ -906,7 +908,8 @@ heap_vacuum_rel(Relation rel, const VacuumParams params,
 	errcallback.previous = error_context_stack;
 	error_context_stack = &errcallback;

 	/* Set up high level stuff about rel and its indexes */
 	vacrel->rel = rel;
@@ -1158,7 +1161,7 @@ heap_vacuum_rel(Relation rel, const VacuumParams params,
 						&frozenxid_updated, &minmulti_updated, false);

 	/*
@@ -1170,33 +1173,20 @@ heap_vacuum_rel(Relation rel, const VacuumParams params,

-	}
-	else
-	{

@@ -2890,9 +2880,9 @@ lazy_vacuum_all_indexes(LVRelState *vacrel)
 	else
 	{
 		LVExtStatCounters counters;
-		ExtVacReport extVacReport;
+		PgStat_VacuumRelationCounts extVacReport;

 
@@ -2900,7 +2890,7 @@ lazy_vacuum_all_indexes(LVRelState *vacrel)
 		parallel_vacuum_bulkdel_all_indexes(vacrel->pvs, old_live_tuples,
 											vacrel->num_index_scans);

 		/*
@@ -3313,9 +3303,9 @@ lazy_cleanup_all_indexes(LVRelState *vacrel)
 	else
 	{
 		LVExtStatCounters counters;
-		ExtVacReport extVacReport;
+		PgStat_VacuumRelationCounts extVacReport;

 
@@ -3324,7 +3314,7 @@ lazy_cleanup_all_indexes(LVRelState *vacrel)
 											vacrel->num_index_scans,
 											estimated_count);

@@ -3352,7 +3342,10 @@ lazy_vacuum_one_index(Relation indrel, IndexBulkDeleteResult *istat,
 	IndexVacuumInfo ivinfo;
 
@@ -3383,18 +3376,13 @@ lazy_vacuum_one_index(Relation indrel, IndexBulkDeleteResult *istat,

 
 
@@ -3421,7 +3409,10 @@ lazy_cleanup_one_index(Relation indrel, IndexBulkDeleteResult *istat,
 
 	/* Set initial statistics values to gather vacuum statistics for the index */
 	extvac_stats_start_idx(indrel, istat, &extVacCounters);
@@ -3451,17 +3442,13 @@ lazy_cleanup_one_index(Relation indrel, IndexBulkDeleteResult *istat,

 
 	/* Revert to the previous phase information for error traceback */
 	restore_vacuum_error_info(vacrel, &saved_err_info);
@@ -4061,6 +4048,27 @@ update_relstats_all_indexes(LVRelState *vacrel)
 	}
 }
 
+/* ---------
@@ -4076,7 +4084,7 @@ vacuum_error_callback(void *arg)
 
 			if (BlockNumberIsValid(errinfo->blkno))
 			{
@@ -4094,7 +4102,7 @@ vacuum_error_callback(void *arg)

 			if (BlockNumberIsValid(errinfo->blkno))
 			{
@@ -4112,7 +4120,7 @@ vacuum_error_callback(void *arg)

 			errcontext("while vacuuming index \"%s\" of relation \"%s.%s\"",
 					   errinfo->indname, errinfo->relnamespace, errinfo->relname);
@@ -4120,7 +4128,7 @@ vacuum_error_callback(void *arg)

 			errcontext("while cleaning up index \"%s\" of relation \"%s.%s\"",
 					   errinfo->indname, errinfo->relnamespace, errinfo->relname);
@@ -4128,7 +4136,7 @@ vacuum_error_callback(void *arg)

 			if (BlockNumberIsValid(errinfo->blkno))
 				errcontext("while truncating relation \"%s.%s\" to %u blocks",
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index fd6537567ea..f82f11490ff 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -1883,6 +1883,7 @@ heap_drop_with_catalog(Oid relid)

 	/*
index c4029a4f3d3..3bccc1097fb 100644

 	/*
 	 * Close and flush the index's relcache entry, to ensure relcache doesn't
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e5370211d74..b3f60ea546d 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -1430,100 +1430,104 @@ GRANT EXECUTE ON FUNCTION pg_get_aios() TO pg_read_all_stats;
 --

 

 

 
index 2793fd83771..25ede1d9824 100644
@@ -1815,6 +1815,7 @@ dropdb(const char *dbname, bool missing_ok, bool force)
 
 	/*
 
 	/*

 	/*
index 614daf60372..d03fc10666e 100644
@@ -479,6 +479,34 @@ static const PgStat_KindInfo pgstat_kind_builtin_infos[PGSTAT_KIND_BUILTIN_SIZE]
 
 /*
 
+/*

 	return true;
 }
index bf7ab345be0..817372f9cec 100644

 /*
 
-/* ---------

 /*
 
+void
index 1c39ada2c3e..b2df237c337 100644
@@ -2267,7 +2267,6 @@ pg_stat_have_stats(PG_FUNCTION_ARGS)
 
-
 /*
  * Get the vacuum statistics for the heap tables.
  */
@@ -2277,102 +2276,42 @@ pg_stat_get_vacuum_tables(PG_FUNCTION_ARGS)
 	#define PG_STAT_GET_VACUUM_TABLES_STATS_COLS 26

 

 	i = 0;

 
 
@@ -2380,28 +2319,28 @@ pg_stat_get_vacuum_tables(PG_FUNCTION_ARGS)

 

 
@@ -2418,100 +2357,60 @@ pg_stat_get_vacuum_indexes(PG_FUNCTION_ARGS)
 	#define PG_STAT_GET_VACUUM_INDEX_STATS_COLS	16

 

 	i = 0;

 

 

 
@@ -2525,90 +2424,52 @@ pg_stat_get_vacuum_database(PG_FUNCTION_ARGS)
 	#define PG_STAT_GET_VACUUM_DATABASE_STATS_COLS	14

 

 

 
 
index dcc542750b8..bc9df1433c2 100644
index 50876f5446f..3b9eb87221a 100644
 
 /* ----------

 
-	/* Time stats. */

-	int32		errors;

 
-/* ----------
 
-	/*
 
 /* ----------

 

 /* ------

 /*

index 6cf00008f63..5c5ab8e2ee4 100644
@@ -432,6 +432,18 @@ typedef struct PgStatShared_Relation
 
@@ -600,6 +612,9 @@ extern PgStat_EntryRef *pgstat_fetch_pending_entry(PgStat_Kind kind,

 /*
index eb5f0b3ae6d..52e884fbf8b 100644

 /* Custom stats kinds */
index 14f19e5bcbe..ef2d6545953 100644
@@ -2293,77 +2293,81 @@ pg_stat_user_tables| SELECT relid,

 SELECT relpages AS irp

 

 
 DROP TABLE vestat;

 

 DELETE FROM vestat;;

 

 

 
 
 DELETE FROM vestat;
 TRUNCATE vestat;

 
 DROP TABLE vestat;

From 1a7be3fe1e1232c800e1c20210e27c2279155d3b Mon Sep 17 00:00:00 2001

---

diff --git a/doc/src/sgml/system-views.sgml b/doc/src/sgml/system-views.sgml
index 4187191ea74..183fb0bfce3 100644
--- a/doc/src/sgml/system-views.sgml
+++ b/doc/src/sgml/system-views.sgml
@@ -5545,4 +5545,759 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx
   </table>
  </sect1>

---

From: Alena Rybakina <[email-masked]>
Date: Thu, 4 Sep 2025 18:49:20 +0300

Hi, all!

On 02.06.2025 19:50, Alena Rybakina wrote:
>>>>> I did a rebase and finished the part with storing statistics 
>>>>> separately from the relation statistics - now it is possible to 
>>>>> disable the collection of statistics for relationsh using gucs and
> I think this could fix the problem. For the OAT-DROP access type, we 
> can call a function to reset the vacuum statistics for relations that 
> are about to be dropped.
>
I’ve prepared the first working version of the extension.

I haven’t yet implemented writing the statistics to a file and reloading 
them into a hash table and shared memory at instance startup, and I also 
haven’t implemented a proper output for database-level statistics yet.

I structured the extension as follows: statistics are stored in a hash 
table keyed by a composite key - database OID, relation OID, and object 
type (index, table, or database). When VACUUM or a worker processes a 
table or index, an exclusive lock is taken to update the corresponding 
record; a shared lock is taken when reading the statistics. For 
database-level output, I plan to compute the totals by summing table and 
index statistics on demand.

To optimize that, I plan to keep entries in the hash table ordered by 
database OID. When accessing the first element by the partial key 
(database OID), I’ll scan forward and aggregate until the partitial 
database key changes.

Right now this requires adding the extension to 
`shared_preload_libraries`. I haven’t found a way to avoid that because 
of shared-memory setup, and I’m not sure it’s even possible.

I’m also unsure whether it’s better to store the statistics in the 
cumulative statistics system (as done here) or entirely inside the 
extension. Note that the code added to the core to support the extension 
executes regardless of whether the extension is enabled.
From 098e381ca88e2600cb8e8528e3f54f588d4c43a8 Mon Sep 17 00:00:00 2001
From: Alena Rybakina <[email-masked]>
Date: Thu, 4 Sep 2025 18:16:52 +0300
Subject: [PATCH] Core patch for vacuum statistics.

---
 src/backend/access/heap/vacuumlazy.c         | 309 ++++++++++++++++++-
 src/backend/access/heap/visibilitymap.c      |  10 +
 src/backend/commands/vacuum.c                |   4 +
 src/backend/commands/vacuumparallel.c        |  12 +
 src/backend/utils/activity/pgstat_relation.c |  36 ++-
 src/backend/utils/adt/pgstatfuncs.c          |   6 +
 src/backend/utils/error/elog.c               |  13 +
 src/include/catalog/pg_proc.dat              |   8 +
 src/include/commands/vacuum.h                |  26 ++
 src/include/pgstat.h                         | 119 ++++++-
 src/include/utils/elog.h                     |   1 +
 src/test/regress/expected/rules.out          |  12 +-
 12 files changed, 545 insertions(+), 11 deletions(-)

diff --git a/src/backend/access/heap/vacuumlazy.c b/src/backend/access/heap/vacuumlazy.c
index 932701d8420..330f9f90a6b 100644
--- a/src/backend/access/heap/vacuumlazy.c
+++ b/src/backend/access/heap/vacuumlazy.c
@@ -289,6 +289,8 @@ typedef struct LVRelState
 	/* Error reporting state */
 	char	   *dbname;
 	char	   *relnamespace;
+	Oid			reloid;
+	Oid			indoid;
 	char	   *relname;
 	char	   *indname;		/* Current index name */
 	BlockNumber blkno;			/* used only for heap operations */
@@ -407,6 +409,10 @@ typedef struct LVRelState
 	 * been permanently disabled.
 	 */
 	BlockNumber eager_scan_remaining_fails;
+
+	int32		wraparound_failsafe_count; /* number of emergency vacuums to prevent anti-wraparound shutdown */
+
+	PgStat_VacuumRelationCounts extVacReportIdx;
 } LVRelState;

@@ -474,6 +480,209 @@ static void update_vacuum_error_info(LVRelState *vacrel,
 static void restore_vacuum_error_info(LVRelState *vacrel,
 									  const LVSavedErrInfo *saved_vacrel);
 
+/* ----------
+		report->index.tuples_deleted =
+	extVacStats->table.tuples_deleted = vacrel->tuples_deleted;

 /*
@@ -632,6 +841,13 @@ heap_vacuum_rel(Relation rel, const VacuumParams params,
 	BufferUsage startbufferusage = pgBufferUsage;
 	ErrorContextCallback errcallback;
 	char	  **indnames = NULL;
+	LVExtStatCounters extVacCounters;
+	PgStat_VacuumRelationCounts ExtVacReport;
+	PgStat_VacuumRelationCounts allzero;
+
+	/* Initialize vacuum statistics */
+	memset(&allzero, 0, sizeof(PgStat_VacuumRelationCounts));
+	ExtVacReport = allzero;
 
 	verbose = (params.options & VACOPT_VERBOSE) != 0;
 	instrument = (verbose || (AmAutoVacuumWorkerProcess() &&
@@ -652,6 +868,7 @@ heap_vacuum_rel(Relation rel, const VacuumParams params,
 	pgstat_progress_start_command(PROGRESS_COMMAND_VACUUM,
 								  RelationGetRelid(rel));
 
@@ -668,6 +885,7 @@ heap_vacuum_rel(Relation rel, const VacuumParams params,
@@ -676,6 +894,8 @@ heap_vacuum_rel(Relation rel, const VacuumParams params,
 
@@ -776,6 +996,7 @@ heap_vacuum_rel(Relation rel, const VacuumParams params,
 
@@ -924,6 +1145,9 @@ heap_vacuum_rel(Relation rel, const VacuumParams params,
 
+	/* Make generic extended vacuum stats report */
+	extvac_stats_end(rel, &extVacCounters, &ExtVacReport);
+
 	/*
 	 * Report results to the cumulative stats system, too.
 	 *
@@ -934,12 +1158,20 @@ heap_vacuum_rel(Relation rel, const VacuumParams params,
 	 * soon in cases where the failsafe prevented significant amounts of heap
 	 * vacuuming.
 	 */
+	/* Make generic extended vacuum stats report and
+		* fill heap-specific extended stats fields.
+		*/
+	extvac_stats_end(vacrel->rel, &extVacCounters, &ExtVacReport);
+	accumulate_heap_vacuum_statistics(vacrel, &ExtVacReport);
+
 	pgstat_report_vacuum(RelationGetRelid(rel),
-						 rel->rd_rel->relisshared,
-						 Max(vacrel->new_live_tuples, 0),
-						 vacrel->recently_dead_tuples +
-						 vacrel->missed_dead_tuples,
-						 starttime);
+						rel->rd_rel->relisshared,
+						Max(vacrel->new_live_tuples, 0),
+						vacrel->recently_dead_tuples +
+						vacrel->missed_dead_tuples,
+						starttime,
+						&ExtVacReport);
+
 	pgstat_progress_end_command();
 
 	if (instrument)
@@ -2631,10 +2863,20 @@ lazy_vacuum_all_indexes(LVRelState *vacrel)
 	}
 	else
 	{
+		LVExtStatCounters counters;
+		PgStat_VacuumRelationCounts PgStat_VacuumRelationCounts;
+
+		memset(&PgStat_VacuumRelationCounts, 0, sizeof(PgStat_VacuumRelationCounts));
+
+		extvac_stats_start(vacrel->rel, &counters);
+
 		/* Outsource everything to parallel variant */
 		parallel_vacuum_bulkdel_all_indexes(vacrel->pvs, old_live_tuples,
 											vacrel->num_index_scans);
 
+		extvac_stats_end(vacrel->rel, &counters, &PgStat_VacuumRelationCounts);
+		accumulate_idxs_vacuum_statistics(vacrel, &PgStat_VacuumRelationCounts);
+
 		/*
 		 * Do a postcheck to consider applying wraparound failsafe now.  Note
 		 * that parallel VACUUM only gets the precheck and this postcheck.
@@ -2961,6 +3203,7 @@ lazy_check_wraparound_failsafe(LVRelState *vacrel)
 		int64		progress_val[2] = {0, 0};

 		/*
 		 * Abandon use of a buffer access strategy to allow use of all of
@@ -3043,10 +3286,20 @@ lazy_cleanup_all_indexes(LVRelState *vacrel)
 	}
 	else
 	{
+		LVExtStatCounters counters;
+		PgStat_VacuumRelationCounts PgStat_VacuumRelationCounts;
+
+		memset(&PgStat_VacuumRelationCounts, 0, sizeof(PgStat_VacuumRelationCounts));
+
+		extvac_stats_start(vacrel->rel, &counters);
+
 		/* Outsource everything to parallel variant */
 		parallel_vacuum_cleanup_all_indexes(vacrel->pvs, reltuples,
 											vacrel->num_index_scans,
 											estimated_count);
+
+		extvac_stats_end(vacrel->rel, &counters, &PgStat_VacuumRelationCounts);
+		accumulate_idxs_vacuum_statistics(vacrel, &PgStat_VacuumRelationCounts);
 	}
 
 	/* Reset the progress counters */
@@ -3072,6 +3325,11 @@ lazy_vacuum_one_index(Relation indrel, IndexBulkDeleteResult *istat,
 {
 	IndexVacuumInfo ivinfo;
 	LVSavedErrInfo saved_err_info;
+	LVExtStatCountersIdx extVacCounters;
+	PgStat_VacuumRelationCounts PgStat_VacuumRelationCounts;
+
+	/* Set initial statistics values to gather vacuum statistics for the index */
+	extvac_stats_start_idx(indrel, istat, &extVacCounters);
 
 	ivinfo.index = indrel;
@@ -3090,6 +3348,7 @@ lazy_vacuum_one_index(Relation indrel, IndexBulkDeleteResult *istat,
@@ -3098,6 +3357,16 @@ lazy_vacuum_one_index(Relation indrel, IndexBulkDeleteResult *istat,
 
+	/* Make extended vacuum stats report for index */
+	extvac_stats_end_idx(indrel, istat, &extVacCounters, &PgStat_VacuumRelationCounts);
+
+	if (!ParallelVacuumIsActive(vacrel))
+		accumulate_idxs_vacuum_statistics(vacrel, &PgStat_VacuumRelationCounts);
+
+	pgstat_report_vacuum(RelationGetRelid(indrel),
+							indrel->rd_rel->relisshared,
+							0, 0, 0, &PgStat_VacuumRelationCounts);
+
 	/* Revert to the previous phase information for error traceback */
 	restore_vacuum_error_info(vacrel, &saved_err_info);
 	pfree(vacrel->indname);
@@ -3122,6 +3391,11 @@ lazy_cleanup_one_index(Relation indrel, IndexBulkDeleteResult *istat,
 {
 	IndexVacuumInfo ivinfo;
 	LVSavedErrInfo saved_err_info;
+	LVExtStatCountersIdx extVacCounters;
+	PgStat_VacuumRelationCounts PgStat_VacuumRelationCounts;
+
+	/* Set initial statistics values to gather vacuum statistics for the index */
+	extvac_stats_start_idx(indrel, istat, &extVacCounters);
 
 	ivinfo.index = indrel;
@@ -3141,12 +3415,22 @@ lazy_cleanup_one_index(Relation indrel, IndexBulkDeleteResult *istat,

+	/* Make extended vacuum stats report for index */
+	extvac_stats_end_idx(indrel, istat, &extVacCounters, &PgStat_VacuumRelationCounts);
+	if (!ParallelVacuumIsActive(vacrel))
+		accumulate_idxs_vacuum_statistics(vacrel, &PgStat_VacuumRelationCounts);
+
+	pgstat_report_vacuum(RelationGetRelid(indrel),
+							indrel->rd_rel->relisshared,
+							0, 0, 0, &PgStat_VacuumRelationCounts);
+
 	/* Revert to the previous phase information for error traceback */
 	restore_vacuum_error_info(vacrel, &saved_err_info);
 	pfree(vacrel->indname);
@@ -3759,6 +4043,9 @@ vacuum_error_callback(void *arg)
 	switch (errinfo->phase)
 	{
 		case VACUUM_ERRCB_PHASE_SCAN_HEAP:
+			if(geterrelevel() == ERROR)
+					pgstat_report_vacuum_error(errinfo->reloid, errinfo->rel->rd_rel->relisshared, PGSTAT_EXTVAC_TABLE);
+
 			if (BlockNumberIsValid(errinfo->blkno))
 			{
 				if (OffsetNumberIsValid(errinfo->offnum))
@@ -3774,6 +4061,9 @@ vacuum_error_callback(void *arg)
 			break;
 
 		case VACUUM_ERRCB_PHASE_VACUUM_HEAP:
+			if(geterrelevel() == ERROR)
+				pgstat_report_vacuum_error(errinfo->reloid, errinfo->rel->rd_rel->relisshared, PGSTAT_EXTVAC_TABLE);
+
 			if (BlockNumberIsValid(errinfo->blkno))
 			{
 				if (OffsetNumberIsValid(errinfo->offnum))
@@ -3789,16 +4079,25 @@ vacuum_error_callback(void *arg)
 			break;
 
+				pgstat_report_vacuum_error(errinfo->reloid, errinfo->rel->rd_rel->relisshared, PGSTAT_EXTVAC_INDEX);
 
+				pgstat_report_vacuum_error(errinfo->reloid, errinfo->rel->rd_rel->relisshared, PGSTAT_EXTVAC_INDEX);
 
+				pgstat_report_vacuum_error(errinfo->reloid, errinfo->rel->rd_rel->relisshared, PGSTAT_EXTVAC_TABLE);
 
 	if (map[mapByte] & mask)
 	{
+		/*
 
 		MarkBufferDirty(vmbuf);

 		VacuumCostBalance = 0;
 
 		/*
index 0feea1d30ec..b5461ec661b 100644
 
 	/*
 
@@ -904,6 +909,12 @@ parallel_vacuum_process_one_index(ParallelVacuumState *pvs, Relation indrel,
 
@@ -1054,6 +1065,7 @@ parallel_vacuum_main(dsm_segment *seg, shm_toc *toc)
index 69df741cbf6..33a4009f746 100644
@@ -203,13 +203,39 @@ pgstat_drop_relation(Relation rel)
 	}
 }
 
+/* ---------
+ * pgstat_report_vacuum_error() -
+ *
+ *	Tell the collector about an (auto)vacuum interruption.
+ * ---------
+ */
+void
+pgstat_report_vacuum_error(Oid tableoid, bool shared, ExtVacReportType m_type)
+{
+	PgStat_VacuumRelationCounts params;
+
+	if (!pgstat_track_counts)
+		return;
+
+	if (set_report_vacuum_hook)
+	{
+		memset(&params, 0, sizeof(PgStat_VacuumRelationCounts));
+
+		params.common.interrupts_count++;
+
+		(*set_report_vacuum_hook) (tableoid, shared, &params);
+	}
+}
+
+set_report_vacuum_hook_type set_report_vacuum_hook = NULL;
+
 /*
  * Report that the table was just vacuumed and flush IO statistics.
  */
 void
 pgstat_report_vacuum(Oid tableoid, bool shared,
 					 PgStat_Counter livetuples, PgStat_Counter deadtuples,
-					 TimestampTz starttime)
+					 TimestampTz starttime, PgStat_VacuumRelationCounts *params)
 {
 	PgStat_EntryRef *entry_ref;
 	PgStatShared_Relation *shtabentry;
@@ -235,6 +261,11 @@ pgstat_report_vacuum(Oid tableoid, bool shared,
 	tabentry->live_tuples = livetuples;
 	tabentry->dead_tuples = deadtuples;
 
+	if (set_report_vacuum_hook)
+	{
+		(*set_report_vacuum_hook) (tableoid, shared, params);
+	}
+
 	/*
 	 * It is quite possible that a non-aggressive VACUUM ended up skipping
 	 * various pages, however, we'll zero the insert counter here regardless.
@@ -881,6 +912,9 @@ pgstat_relation_flush_cb(PgStat_EntryRef *entry_ref, bool nowait)
 	tabentry->blocks_fetched += lstats->counts.blocks_fetched;
 	tabentry->blocks_hit += lstats->counts.blocks_hit;
 
index c756c2bebaa..9482bf80721 100644

+/*
 
 /*
index 118d6da1ace..e0c7cf29b3a 100644
@@ -12576,4 +12576,12 @@
 
index 14eeccbd718..bc9df1433c2 100644
 
 /*
 
+/*
@@ -327,6 +348,7 @@ extern PGDLLIMPORT double vacuum_max_eager_freeze_failure_rate;
 
@@ -407,4 +429,8 @@ extern double anl_random_fract(void);
 
+extern void extvac_stats_start_idx(Relation rel, IndexBulkDeleteResult *stats,
+					   LVExtStatCountersIdx *counters);
+extern void extvac_stats_end_idx(Relation rel, IndexBulkDeleteResult *stats,
+					 LVExtStatCountersIdx *counters, PgStat_VacuumRelationCounts *report);
 #endif							/* VACUUM_H */
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index f402b17295c..ed6b3dc1d6f 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -100,6 +100,98 @@ typedef struct PgStat_FunctionCallUsage
 	instr_time	start;
 } PgStat_FunctionCallUsage;
 
+
+	int32 interrupts_count;
  * PgStat_BackendSubEntry	Non-flushed subscription stats.
  * ----------
@@ -153,6 +245,9 @@ typedef struct PgStat_TableCounts

 /* ----------
@@ -211,7 +306,7 @@ typedef struct PgStat_TableXactStatus
  * ------------------------------------------------------------
  */

@@ -453,6 +548,9 @@ typedef struct PgStat_StatTabEntry
 
 /* ------
@@ -660,10 +758,11 @@ extern void pgstat_unlink_relation(Relation rel);
 
 extern void pgstat_report_vacuum(Oid tableoid, bool shared,
 								 PgStat_Counter livetuples, PgStat_Counter deadtuples,
-								 TimestampTz starttime);
+								 TimestampTz starttime, PgStat_VacuumRelationCounts *params);
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_vacuum_error(Oid tableoid, bool shared, ExtVacReportType m_type);
 
 /*
@@ -711,6 +810,17 @@ extern void pgstat_report_analyze(Relation rel,
 
 extern void pgstat_count_heap_insert(Relation rel, PgStat_Counter n);
 extern void pgstat_count_heap_update(Relation rel, bool hot, bool newpage);
@@ -838,4 +948,9 @@ extern PGDLLIMPORT PgStat_Counter pgStatTransactionIdleTime;
 /* updated by the traffic cop and in errfinish() */
 extern PGDLLIMPORT SessionEndType pgStatSessionEndCause;
 
+/* Hook for plugins to get control in set_rel_pathlist() */
+typedef void (*set_report_vacuum_hook_type) (Oid tableoid, bool shared, PgStat_VacuumRelationCounts *params);
+extern PGDLLIMPORT set_report_vacuum_hook_type set_report_vacuum_hook;
+
+
 #endif							/* PGSTAT_H */
diff --git a/src/include/utils/elog.h b/src/include/utils/elog.h
index 675f4f5f469..356dadd6b0a 100644
--- a/src/include/utils/elog.h
+++ b/src/include/utils/elog.h
@@ -230,6 +230,7 @@ extern int	geterrcode(void);
 extern int	geterrposition(void);
 extern int	getinternalerrposition(void);

 /*----------
index 35e8aad7701..4731ca2121e 100644
@@ -2284,7 +2288,9 @@ pg_stat_user_tables| SELECT relid,

From 9b4dd2e845cb8fdc9c1cee09da8665da2bccfd42 Mon Sep 17 00:00:00 2001
From: Alena Rybakina <[email-masked]>
Date: Thu, 4 Sep 2025 18:24:46 +0300
Subject: [PATCH] Create vacuum extension statistics

---
 Makefile                                      |  21 +
 .../vacuum-extending-in-repetable-read.out    |  53 ++
 expected/vacuum_index_statistics.out          | 183 +++++
 expected/vacuum_tables_and_db_statistics.out  | 296 ++++++++
 spec/vacuum-extending-in-repetable-read.spec  | 173 +++++
 sql/vacuum_index_statistics.sql               | 138 ++++
 sql/vacuum_tables_and_db_statistics.sql       | 224 ++++++
 vacuum_statistics--1.0.sql                    | 191 ++++++
 vacuum_statistics.c                           | 636 ++++++++++++++++++
 vacuum_statistics.control                     |   4 +
 10 files changed, 1919 insertions(+)
 create mode 100644 Makefile
 create mode 100644 expected/vacuum-extending-in-repetable-read.out
 create mode 100644 expected/vacuum_index_statistics.out
 create mode 100644 expected/vacuum_tables_and_db_statistics.out
 create mode 100644 spec/vacuum-extending-in-repetable-read.spec
 create mode 100644 sql/vacuum_index_statistics.sql
 create mode 100644 sql/vacuum_tables_and_db_statistics.sql
 create mode 100644 vacuum_statistics--1.0.sql
 create mode 100644 vacuum_statistics.c
 create mode 100644 vacuum_statistics.control

diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..7fd875e
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,21 @@
+EXTENSION = vacuum_statistics
+EXTVERSION = 1.0
+MODULE_big = vacuum_statistics
+PGFILEDESC = "Vacuum Statistics - extension for storage statistics of vacuum workload"
+OBJS = vacuum_statistics.o
+
+DATA = vacuum_statistics--1.0.sql
+
+REGRESS = vacuum_index_statistics vacuum_tables_and_db_statistics
+ISOLATION = vacuum-extending-in-repetable-read
+
+ifdef USE_PGXS
+PG_CONFIG = pg_config
+PGXS := $(shell $(PG_CONFIG) --pgxs)
+include $(PGXS)
+else
+subdir = contrib/vacuum_statistics
+top_builddir = ../..
+include $(top_builddir)/src/Makefile.global
+include $(top_srcdir)/contrib/contrib-global.mk
+endif
\ No newline at end of file
diff --git a/expected/vacuum-extending-in-repetable-read.out b/expected/vacuum-extending-in-repetable-read.out
new file mode 100644
index 0000000..6d96042
--- /dev/null
+++ b/expected/vacuum-extending-in-repetable-read.out
@@ -0,0 +1,53 @@
+unused step name: s2_delete
+Parsed test spec with 2 sessions
+
+starting permutation: s2_insert s2_print_vacuum_stats_table s1_begin_repeatable_read s2_update s2_insert_interrupt s2_vacuum s2_print_vacuum_stats_table s1_commit s2_checkpoint s2_vacuum s2_print_vacuum_stats_table
+step s2_insert: INSERT INTO test_vacuum_stat_isolation(id, ival) SELECT ival, ival%10 FROM generate_series(1,1000) As ival;
+step s2_print_vacuum_stats_table: 
+    SELECT
+    vt.relname, vt.tuples_deleted, vt.recently_dead_tuples, vt.missed_dead_tuples, vt.missed_dead_pages, vt.tuples_frozen
+    FROM pg_stat_vacuum_tables vt, pg_class c
+    WHERE vt.relname = 'test_vacuum_stat_isolation' AND vt.relid = c.oid;
+
+relname                   |tuples_deleted|recently_dead_tuples|missed_dead_tuples|missed_dead_pages|tuples_frozen
+--------------------------+--------------+--------------------+------------------+-----------------+-------------
+test_vacuum_stat_isolation|             0|                   0|                 0|                0|            0
+(1 row)
+
+step s1_begin_repeatable_read: 
+  BEGIN transaction ISOLATION LEVEL REPEATABLE READ;
+  select count(ival) from test_vacuum_stat_isolation where id>900;
+
+count
+-----
+  100
+(1 row)
+
+step s2_update: UPDATE test_vacuum_stat_isolation SET ival = ival + 2 where id > 900;
+step s2_insert_interrupt: INSERT INTO test_vacuum_stat_isolation values (1,1);
+step s2_vacuum: VACUUM test_vacuum_stat_isolation;
+step s2_print_vacuum_stats_table: 
+    SELECT
+    vt.relname, vt.tuples_deleted, vt.recently_dead_tuples, vt.missed_dead_tuples, vt.missed_dead_pages, vt.tuples_frozen
+    FROM pg_stat_vacuum_tables vt, pg_class c
+    WHERE vt.relname = 'test_vacuum_stat_isolation' AND vt.relid = c.oid;
+
+relname                   |tuples_deleted|recently_dead_tuples|missed_dead_tuples|missed_dead_pages|tuples_frozen
+--------------------------+--------------+--------------------+------------------+-----------------+-------------
+test_vacuum_stat_isolation|             0|                 600|                 0|                0|            0
+(1 row)
+
+step s1_commit: COMMIT;
+step s2_checkpoint: CHECKPOINT;
+step s2_vacuum: VACUUM test_vacuum_stat_isolation;
+step s2_print_vacuum_stats_table: 
+    SELECT
+    vt.relname, vt.tuples_deleted, vt.recently_dead_tuples, vt.missed_dead_tuples, vt.missed_dead_pages, vt.tuples_frozen
+    FROM pg_stat_vacuum_tables vt, pg_class c
+    WHERE vt.relname = 'test_vacuum_stat_isolation' AND vt.relid = c.oid;
+
+relname                   |tuples_deleted|recently_dead_tuples|missed_dead_tuples|missed_dead_pages|tuples_frozen
+--------------------------+--------------+--------------------+------------------+-----------------+-------------
+test_vacuum_stat_isolation|           300|                 600|                 0|                0|          303
+(1 row)
+
diff --git a/expected/vacuum_index_statistics.out b/expected/vacuum_index_statistics.out
new file mode 100644
index 0000000..4654a53
--- /dev/null
+++ b/expected/vacuum_index_statistics.out
@@ -0,0 +1,183 @@
+--
+-- Test cumulative vacuum stats system
+--
+-- Check the wall statistics collected during vacuum operation:
+-- number of frozen and visible pages set by vacuum;
+-- number of frozen and visible pages removed by backend.
+-- Statistic wal_fpi is not displayed in this test because its behavior is unstable.
+--
+-- conditio sine qua non
+SHOW track_counts;  -- must be on
+ track_counts 
+--------------
+ on
+(1 row)
+
+\set sample_size 10000
+-- not enabled by default, but we want to test it...
+SET track_functions TO 'all';
+SHOW track_vacuum_statistics;  -- must be off
+ track_vacuum_statistics 
+-------------------------
+ off
+(1 row)
+
+CREATE TABLE vestat (x int) WITH (autovacuum_enabled = off, fillfactor = 10);
+INSERT INTO vestat SELECT x FROM generate_series(1,:sample_size) as x;
+ANALYZE vestat;
+DELETE FROM vestat WHERE x % 2 = 0;
+VACUUM (PARALLEL 0, BUFFER_USAGE_LIMIT 128) vestat;
+-- Must be empty.
+SELECT *
+FROM pg_stat_vacuum_indexes vt
+WHERE vt.indexrelname = 'vestat';
+ relid | indexrelid | schemaname | relname | indexrelname | total_blks_read | total_blks_hit | total_blks_dirtied | total_blks_written | rel_blks_read | rel_blks_hit | pages_deleted | tuples_deleted | wal_records | wal_fpi | wal_bytes | blk_read_time | blk_write_time | delay_time | total_time 
+-------+------------+------------+---------+--------------+-----------------+----------------+--------------------+--------------------+---------------+--------------+---------------+----------------+-------------+---------+-----------+---------------+----------------+------------+------------
+(0 rows)
+
+RESET track_vacuum_statistics;
+DROP TABLE vestat CASCADE;
+SET track_vacuum_statistics TO 'on';
+-- ensure pending stats are flushed
+SELECT pg_stat_force_next_flush();
+ pg_stat_force_next_flush 
+--------------------------
+ 
+(1 row)
+
+\set sample_size 10000
+SET vacuum_freeze_min_age = 0;
+SET vacuum_freeze_table_age = 0;
+--SET stats_fetch_consistency = snapshot;
+CREATE TABLE vestat (x int primary key) WITH (autovacuum_enabled = off, fillfactor = 10);
+INSERT INTO vestat SELECT x FROM generate_series(1,:sample_size) as x;
+ANALYZE vestat;
+SELECT oid AS ioid from pg_class where relname = 'vestat_pkey' \gset
+DELETE FROM vestat WHERE x % 2 = 0;
+-- Before the first vacuum execution extended stats view is empty.
+SELECT vt.indexrelname,relpages,pages_deleted,tuples_deleted
+FROM pg_stat_vacuum_indexes vt, pg_class c
+WHERE vt.indexrelname = 'vestat_pkey' AND vt.indexrelid = c.oid;
+ indexrelname | relpages | pages_deleted | tuples_deleted 
+--------------+----------+---------------+----------------
+ vestat_pkey  |       30 |             0 |              0
+(1 row)
+
+SELECT relpages AS irp
+FROM pg_class c
+WHERE relname = 'vestat_pkey' \gset
+VACUUM (PARALLEL 0, BUFFER_USAGE_LIMIT 128, INDEX_CLEANUP ON) vestat;
+-- it is necessary to check the wal statistics
+CHECKPOINT;
+-- The table and index extended vacuum statistics should show us that
+-- vacuum frozed pages and clean up pages, but pages_removed stayed the same
+-- because of not full table have cleaned up
+SELECT vt.indexrelname,relpages-:irp = 0 AS relpages,pages_deleted = 0 AS pages_deleted,tuples_deleted > 0 AS tuples_deleted
+FROM pg_stat_vacuum_indexes vt, pg_class c
+WHERE vt.indexrelname = 'vestat_pkey' AND vt.indexrelid = c.oid;
+ indexrelname | relpages | pages_deleted | tuples_deleted 
+--------------+----------+---------------+----------------
+ vestat_pkey  | t        | t             | t
+(1 row)
+
+SELECT vt.indexrelname,relpages AS irp,pages_deleted AS ipd,tuples_deleted AS itd
+FROM pg_stat_vacuum_indexes vt, pg_class c
+WHERE vt.indexrelname = 'vestat_pkey' AND vt.indexrelid = c.oid \gset
+-- Store WAL advances into variables
+SELECT wal_records AS iwr,wal_bytes AS iwb,wal_fpi AS ifpi FROM pg_stat_vacuum_indexes WHERE indexrelname = 'vestat_pkey' \gset
+-- Look into WAL records deltas.
+SELECT wal_records > 0 AS diWR, wal_bytes > 0 AS diWB
+FROM pg_stat_vacuum_indexes WHERE indexrelname = 'vestat_pkey';
+ diwr | diwb 
+------+------
+ t    | t
+(1 row)
+
+DELETE FROM vestat;;
+VACUUM (PARALLEL 0, BUFFER_USAGE_LIMIT 128, INDEX_CLEANUP ON) vestat;
+-- it is necessary to check the wal statistics
+CHECKPOINT;
+-- pages_removed must be increased
+SELECT vt.indexrelname,relpages-:irp = 0 AS relpages,pages_deleted-:ipd > 0 AS pages_deleted,tuples_deleted-:itd > 0 AS tuples_deleted
+FROM pg_stat_vacuum_indexes vt, pg_class c
+WHERE vt.indexrelname = 'vestat_pkey' AND vt.indexrelid = c.oid;
+ indexrelname | relpages | pages_deleted | tuples_deleted 
+--------------+----------+---------------+----------------
+ vestat_pkey  | t        | t             | t
+(1 row)
+
+SELECT vt.indexrelname,relpages AS irp,pages_deleted AS ipd,tuples_deleted AS itd
+FROM pg_stat_vacuum_indexes vt, pg_class c
+WHERE vt.indexrelname = 'vestat_pkey' AND vt.indexrelid = c.oid \gset
+-- Store WAL advances into variables
+SELECT wal_records-:iwr AS diwr, wal_bytes-:iwb AS diwb, wal_fpi-:ifpi AS difpi
+FROM pg_stat_vacuum_indexes WHERE indexrelname = 'vestat_pkey' \gset
+-- WAL advance should be detected.
+SELECT :diwr > 0 AS diWR, :diwb > 0 AS diWB;
+ diwr | diwb 
+------+------
+ t    | t
+(1 row)
+
+-- Store WAL advances into variables
+SELECT wal_records AS iwr,wal_bytes AS iwb,wal_fpi AS ifpi FROM pg_stat_vacuum_indexes WHERE indexrelname = 'vestat_pkey' \gset
+INSERT INTO vestat SELECT x FROM generate_series(1,:sample_size) as x;
+DELETE FROM vestat WHERE x % 2 = 0;
+-- VACUUM FULL doesn't report to stat collector. So, no any advancements of statistics
+-- are detected here.
+VACUUM FULL vestat;
+-- It is necessary to check the wal statistics
+CHECKPOINT;
+-- Store WAL advances into variables
+SELECT wal_records-:iwr AS diwr2, wal_bytes-:iwb AS diwb2, wal_fpi-:ifpi AS difpi2
+FROM pg_stat_vacuum_indexes WHERE indexrelname = 'vestat_pkey' \gset
+-- WAL and other statistics advance should not be detected.
+SELECT :diwr2=0 AS diWR, :difpi2=0 AS iFPI, :diwb2=0 AS diWB;
+ diwr | ifpi | diwb 
+------+------+------
+ t    | t    | t
+(1 row)
+
+SELECT vt.indexrelname,relpages-:irp < 0 AS relpages,pages_deleted-:ipd = 0 AS pages_deleted,tuples_deleted-:itd = 0 AS tuples_deleted
+FROM pg_stat_vacuum_indexes vt, pg_class c
+WHERE vt.indexrelname = 'vestat_pkey' AND vt.indexrelid = c.oid;
+ indexrelname | relpages | pages_deleted | tuples_deleted 
+--------------+----------+---------------+----------------
+ vestat_pkey  | t        | t             | t
+(1 row)
+
+SELECT vt.indexrelname,relpages AS irp,pages_deleted AS ipd,tuples_deleted AS itd
+FROM pg_stat_vacuum_indexes vt, pg_class c
+WHERE vt.indexrelname = 'vestat_pkey' AND vt.indexrelid = c.oid \gset
+-- Store WAL advances into variables
+SELECT wal_records AS iwr,wal_bytes AS iwb,wal_fpi AS ifpi FROM pg_stat_vacuum_indexes WHERE indexrelname = 'vestat_pkey' \gset
+DELETE FROM vestat;
+TRUNCATE vestat;
+VACUUM (PARALLEL 0, BUFFER_USAGE_LIMIT 128, INDEX_CLEANUP ON) vestat;
+-- it is necessary to check the wal statistics
+CHECKPOINT;
+-- Store WAL advances into variables after removing all tuples from the table
+SELECT wal_records-:iwr AS diwr3, wal_bytes-:iwb AS diwb3, wal_fpi-:ifpi AS difpi3
+FROM pg_stat_vacuum_indexes WHERE indexrelname = 'vestat_pkey' \gset
+--There are nothing changed
+SELECT :diwr3=0 AS diWR, :difpi3=0 AS iFPI, :diwb3=0 AS diWB;
+ diwr | ifpi | diwb 
+------+------+------
+ t    | t    | t
+(1 row)
+
+--
+-- Now, the table and index is compressed into zero number of pages. Check it
+-- in vacuum extended statistics.
+-- The pages_frozen, pages_scanned values shouldn't be changed
+--
+SELECT vt.indexrelname,relpages-:irp = 0 AS relpages,pages_deleted-:ipd = 0 AS pages_deleted,tuples_deleted-:itd = 0 AS tuples_deleted
+FROM pg_stat_vacuum_indexes vt, pg_class c
+WHERE vt.indexrelname = 'vestat_pkey' AND vt.indexrelid = c.oid;
+ indexrelname | relpages | pages_deleted | tuples_deleted 
+--------------+----------+---------------+----------------
+ vestat_pkey  | f        | t             | t
+(1 row)
+
+DROP TABLE vestat;
+RESET track_vacuum_statistics;
diff --git a/expected/vacuum_tables_and_db_statistics.out b/expected/vacuum_tables_and_db_statistics.out
new file mode 100644
index 0000000..0300e7b
--- /dev/null
+++ b/expected/vacuum_tables_and_db_statistics.out
@@ -0,0 +1,296 @@
+--
+-- Test cumulative vacuum stats system
+--
+-- Check the wall statistics collected during vacuum operation:
+-- number of frozen and visible pages set by vacuum;
+-- number of frozen and visible pages removed by backend.
+-- Statistic wal_fpi is not displayed in this test because its behavior is unstable.
+--
+SHOW track_counts;  -- must be on
+ track_counts 
+--------------
+ on
+(1 row)
+
+\set sample_size 10000
+-- not enabled by default, but we want to test it...
+SET track_functions TO 'all';
+SHOW track_vacuum_statistics;  -- must be off
+ track_vacuum_statistics 
+-------------------------
+ off
+(1 row)
+
+CREATE TABLE vestat (x int) WITH (autovacuum_enabled = off, fillfactor = 10);
+INSERT INTO vestat SELECT x FROM generate_series(1,:sample_size) as x;
+ANALYZE vestat;
+DELETE FROM vestat WHERE x % 2 = 0;
+VACUUM (PARALLEL 0, BUFFER_USAGE_LIMIT 128) vestat;
+-- Must be empty.
+SELECT relname,total_blks_read, total_blks_hit, total_blks_dirtied, total_blks_written,rel_blks_read, rel_blks_hit,
+pages_scanned, pages_removed, vm_new_frozen_pages, vm_new_visible_pages, vm_new_visible_frozen_pages, missed_dead_pages,
+tuples_deleted, tuples_frozen, recently_dead_tuples, missed_dead_tuples, index_vacuum_count,
+wal_records, wal_fpi, wal_bytes, blk_read_time, blk_write_time,delay_time, total_time
+FROM pg_stat_vacuum_tables vt
+WHERE vt.relname = 'vestat';
+ relname | total_blks_read | total_blks_hit | total_blks_dirtied | total_blks_written | rel_blks_read | rel_blks_hit | pages_scanned | pages_removed | vm_new_frozen_pages | vm_new_visible_pages | vm_new_visible_frozen_pages | missed_dead_pages | tuples_deleted | tuples_frozen | recently_dead_tuples | missed_dead_tuples | index_vacuum_count | wal_records | wal_fpi | wal_bytes | blk_read_time | blk_write_time | delay_time | total_time 
+---------+-----------------+----------------+--------------------+--------------------+---------------+--------------+---------------+---------------+---------------------+----------------------+-----------------------------+-------------------+----------------+---------------+----------------------+--------------------+--------------------+-------------+---------+-----------+---------------+----------------+------------+------------
+ vestat  |               0 |              0 |                  0 |                  0 |             0 |            0 |             0 |             0 |                   0 |                    0 |                           0 |                 0 |              0 |             0 |                    0 |                  0 |                  0 |           0 |       0 |         0 |             0 |              0 |          0 |          0
+(1 row)
+
+RESET track_vacuum_statistics;
+DROP TABLE vestat CASCADE;
+CREATE DATABASE regression_statistic_vacuum_db;
+CREATE DATABASE regression_statistic_vacuum_db1;
+\c regression_statistic_vacuum_db;
+SET track_vacuum_statistics TO on;
+-- not enabled by default, but we want to test it...
+SET track_functions TO 'all';
+-- ensure pending stats are flushed
+SELECT pg_stat_force_next_flush();
+ pg_stat_force_next_flush 
+--------------------------
+ 
+(1 row)
+
+--SET stats_fetch_consistency = snapshot;
+CREATE TABLE vestat (x int) WITH (autovacuum_enabled = off, fillfactor = 10);
+INSERT INTO vestat SELECT x FROM generate_series(1,:sample_size) as x;
+ANALYZE vestat;
+SELECT oid AS roid from pg_class where relname = 'vestat' \gset
+DELETE FROM vestat WHERE x % 2 = 0;
+-- Before the first vacuum execution extended stats view is empty.
+SELECT vt.relname,vm_new_frozen_pages,tuples_deleted,relpages,pages_scanned,pages_removed
+FROM pg_stat_vacuum_tables vt, pg_class c
+WHERE vt.relname = 'vestat' AND vt.relid = c.oid;
+ relname | vm_new_frozen_pages | tuples_deleted | relpages | pages_scanned | pages_removed 
+---------+---------------------+----------------+----------+---------------+---------------
+ vestat  |                   0 |              0 |      455 |             0 |             0
+(1 row)
+
+SELECT relpages AS rp
+FROM pg_class c
+WHERE relname = 'vestat' \gset
+VACUUM (PARALLEL 0, BUFFER_USAGE_LIMIT 128, INDEX_CLEANUP OFF) vestat;
+-- it is necessary to check the wal statistics
+CHECKPOINT;
+-- The table and index extended vacuum statistics should show us that
+-- vacuum frozed pages and clean up pages, but pages_removed stayed the same
+-- because of not full table have cleaned up
+SELECT vt.relname,vm_new_frozen_pages > 0 AS vm_new_frozen_pages,tuples_deleted > 0 AS tuples_deleted,relpages-:rp = 0 AS relpages,pages_scanned > 0 AS pages_scanned,pages_removed = 0 AS pages_removed
+FROM pg_stat_vacuum_tables vt, pg_class c
+WHERE vt.relname = 'vestat' AND vt.relid = c.oid;
+ relname | vm_new_frozen_pages | tuples_deleted | relpages | pages_scanned | pages_removed 
+---------+---------------------+----------------+----------+---------------+---------------
+ vestat  | f                   | t              | t        | t             | t
+(1 row)
+
+SELECT vm_new_frozen_pages AS fp,tuples_deleted AS td,relpages AS rp, pages_scanned AS ps, pages_removed AS pr
+FROM pg_stat_vacuum_tables vt, pg_class c
+WHERE vt.relname = 'vestat' AND vt.relid = c.oid \gset
+-- Store WAL advances into variables
+SELECT wal_records AS hwr,wal_bytes AS hwb,wal_fpi AS hfpi FROM pg_stat_vacuum_tables WHERE relname = 'vestat' \gset
+-- Look into WAL records deltas.
+SELECT wal_records > 0 AS dWR, wal_bytes > 0 AS dWB
+FROM pg_stat_vacuum_tables WHERE relname = 'vestat';
+ dwr | dwb 
+-----+-----
+ t   | t
+(1 row)
+
+DELETE FROM vestat;;
+VACUUM (PARALLEL 0, BUFFER_USAGE_LIMIT 128, INDEX_CLEANUP OFF) vestat;
+-- it is necessary to check the wal statistics
+CHECKPOINT;
+-- pages_removed must be increased
+SELECT vt.relname,vm_new_frozen_pages-:fp > 0 AS vm_new_frozen_pages,tuples_deleted-:td > 0 AS tuples_deleted,relpages -:rp = 0 AS relpages,pages_scanned-:ps > 0 AS pages_scanned,pages_removed-:pr > 0 AS pages_removed
+FROM pg_stat_vacuum_tables vt, pg_class c
+WHERE vt.relname = 'vestat' AND vt.relid = c.oid;
+ relname | vm_new_frozen_pages | tuples_deleted | relpages | pages_scanned | pages_removed 
+---------+---------------------+----------------+----------+---------------+---------------
+ vestat  | f                   | t              | f        | t             | t
+(1 row)
+
+SELECT vm_new_frozen_pages AS fp,tuples_deleted AS td,relpages AS rp, pages_scanned AS ps, pages_removed AS pr
+FROM pg_stat_vacuum_tables vt, pg_class c
+WHERE vt.relname = 'vestat' AND vt.relid = c.oid \gset
+-- Store WAL advances into variables
+SELECT wal_records-:hwr AS dwr, wal_bytes-:hwb AS dwb, wal_fpi-:hfpi AS dfpi
+FROM pg_stat_vacuum_tables WHERE relname = 'vestat' \gset
+-- WAL advance should be detected.
+SELECT :dwr > 0 AS dWR, :dwb > 0 AS dWB;
+ dwr | dwb 
+-----+-----
+ t   | t
+(1 row)
+
+-- Store WAL advances into variables
+SELECT wal_records AS hwr,wal_bytes AS hwb,wal_fpi AS hfpi FROM pg_stat_vacuum_tables WHERE relname = 'vestat' \gset
+INSERT INTO vestat SELECT x FROM generate_series(1,:sample_size) as x;
+DELETE FROM vestat WHERE x % 2 = 0;
+-- VACUUM FULL doesn't report to stat collector. So, no any advancements of statistics
+-- are detected here.
+VACUUM FULL vestat;
+-- It is necessary to check the wal statistics
+CHECKPOINT;
+-- Store WAL advances into variables
+SELECT wal_records-:hwr AS dwr2, wal_bytes-:hwb AS dwb2, wal_fpi-:hfpi AS dfpi2
+FROM pg_stat_vacuum_tables WHERE relname = 'vestat' \gset
+-- WAL and other statistics advance should not be detected.
+SELECT :dwr2=0 AS dWR, :dfpi2=0 AS dFPI, :dwb2=0 AS dWB;
+ dwr | dfpi | dwb 
+-----+------+-----
+ t   | t    | t
+(1 row)
+
+SELECT vt.relname,vm_new_frozen_pages-:fp = 0 AS vm_new_frozen_pages,tuples_deleted-:td = 0 AS tuples_deleted,relpages -:rp < 0 AS relpages,pages_scanned-:ps = 0 AS pages_scanned,pages_removed-:pr = 0 AS pages_removed
+FROM pg_stat_vacuum_tables vt, pg_class c
+WHERE vt.relname = 'vestat' AND vt.relid = c.oid;
+ relname | vm_new_frozen_pages | tuples_deleted | relpages | pages_scanned | pages_removed 
+---------+---------------------+----------------+----------+---------------+---------------
+ vestat  | t                   | t              | f        | t             | t
+(1 row)
+
+SELECT vm_new_frozen_pages AS fp,tuples_deleted AS td,relpages AS rp, pages_scanned AS ps,pages_removed AS pr
+FROM pg_stat_vacuum_tables vt, pg_class c
+WHERE vt.relname = 'vestat' AND vt.relid = c.oid \gset
+-- Store WAL advances into variables
+SELECT wal_records AS hwr,wal_bytes AS hwb,wal_fpi AS hfpi FROM pg_stat_vacuum_tables WHERE relname = 'vestat' \gset
+DELETE FROM vestat;
+TRUNCATE vestat;
+VACUUM (PARALLEL 0, BUFFER_USAGE_LIMIT 128, INDEX_CLEANUP OFF) vestat;
+-- it is necessary to check the wal statistics
+CHECKPOINT;
+-- Store WAL advances into variables after removing all tuples from the table
+SELECT wal_records-:hwr AS dwr3, wal_bytes-:hwb AS dwb3, wal_fpi-:hfpi AS dfpi3
+FROM pg_stat_vacuum_tables WHERE relname = 'vestat' \gset
+--There are nothing changed
+SELECT :dwr3>0 AS dWR, :dfpi3=0 AS dFPI, :dwb3>0 AS dWB;
+ dwr | dfpi | dwb 
+-----+------+-----
+ t   | t    | t
+(1 row)
+
+--
+-- Now, the table and index is compressed into zero number of pages. Check it
+-- in vacuum extended statistics.
+-- The vm_new_frozen_pages, pages_scanned values shouldn't be changed
+--
+SELECT vt.relname,vm_new_frozen_pages-:fp = 0 AS vm_new_frozen_pages,tuples_deleted-:td = 0 AS tuples_deleted,relpages -:rp = 0 AS relpages,pages_scanned-:ps = 0 AS pages_scanned,pages_removed-:pr = 0 AS pages_removed
+FROM pg_stat_vacuum_tables vt, pg_class c
+WHERE vt.relname = 'vestat' AND vt.relid = c.oid;
+ relname | vm_new_frozen_pages | tuples_deleted | relpages | pages_scanned | pages_removed 
+---------+---------------------+----------------+----------+---------------+---------------
+ vestat  | t                   | t              | f        | t             | t
+(1 row)
+
+DROP TABLE vestat CASCADE;
+CREATE TABLE vestat (x int) WITH (autovacuum_enabled = off, fillfactor = 10);
+INSERT INTO vestat SELECT x FROM generate_series(1,:sample_size) as x;
+ANALYZE vestat;
+-- must be empty
+SELECT vm_new_frozen_pages, vm_new_visible_pages, rev_all_frozen_pages,rev_all_visible_pages,vm_new_visible_frozen_pages
+FROM pg_stat_vacuum_tables, pg_stat_all_tables WHERE pg_stat_vacuum_tables.relname = 'vestat' and pg_stat_vacuum_tables.relid = pg_stat_all_tables.relid;
+ vm_new_frozen_pages | vm_new_visible_pages | rev_all_frozen_pages | rev_all_visible_pages | vm_new_visible_frozen_pages 
+---------------------+----------------------+----------------------+-----------------------+-----------------------------
+                   0 |                    0 |                    0 |                     0 |                           0
+(1 row)
+
+VACUUM (PARALLEL 0, BUFFER_USAGE_LIMIT 128) vestat;
+-- backend defreezed pages
+SELECT vm_new_frozen_pages > 0 AS vm_new_frozen_pages,vm_new_visible_pages > 0 AS vm_new_visible_pages,vm_new_visible_frozen_pages > 0 AS vm_new_visible_frozen_pages,rev_all_frozen_pages = 0 AS rev_all_frozen_pages,rev_all_visible_pages = 0 AS rev_all_visible_pages
+FROM pg_stat_vacuum_tables, pg_stat_all_tables WHERE pg_stat_vacuum_tables.relname = 'vestat' and pg_stat_vacuum_tables.relid = pg_stat_all_tables.relid;
+ vm_new_frozen_pages | vm_new_visible_pages | vm_new_visible_frozen_pages | rev_all_frozen_pages | rev_all_visible_pages 
+---------------------+----------------------+-----------------------------+----------------------+-----------------------
+ f                   | t                    | f                           | t                    | t
+(1 row)
+
+SELECT vm_new_frozen_pages AS pf, vm_new_visible_pages AS pv,vm_new_visible_frozen_pages AS pvf, rev_all_frozen_pages AS hafp,rev_all_visible_pages AS havp
+FROM pg_stat_vacuum_tables, pg_stat_all_tables WHERE pg_stat_vacuum_tables.relname = 'vestat' and pg_stat_vacuum_tables.relid = pg_stat_all_tables.relid \gset
+UPDATE vestat SET x = x + 1001;
+VACUUM (PARALLEL 0, BUFFER_USAGE_LIMIT 128) vestat;
+SELECT vm_new_frozen_pages > :pf AS vm_new_frozen_pages,vm_new_visible_pages > :pv AS vm_new_visible_pages,vm_new_visible_frozen_pages > :pvf AS vm_new_visible_frozen_pages,rev_all_frozen_pages > :hafp AS rev_all_frozen_pages,rev_all_visible_pages > :havp AS rev_all_visible_pages
+FROM pg_stat_vacuum_tables, pg_stat_all_tables WHERE pg_stat_vacuum_tables.relname = 'vestat' and pg_stat_vacuum_tables.relid = pg_stat_all_tables.relid;
+ vm_new_frozen_pages | vm_new_visible_pages | vm_new_visible_frozen_pages | rev_all_frozen_pages | rev_all_visible_pages 
+---------------------+----------------------+-----------------------------+----------------------+-----------------------
+ f                   | t                    | f                           | f                    | f
+(1 row)
+
+SELECT vm_new_frozen_pages AS pf, vm_new_visible_pages AS pv, vm_new_visible_frozen_pages AS pvf, rev_all_frozen_pages AS hafp,rev_all_visible_pages AS havp
+FROM pg_stat_vacuum_tables, pg_stat_all_tables WHERE pg_stat_vacuum_tables.relname = 'vestat' and pg_stat_vacuum_tables.relid = pg_stat_all_tables.relid \gset
+VACUUM (PARALLEL 0, BUFFER_USAGE_LIMIT 128) vestat;
+-- vacuum freezed pages
+SELECT vm_new_frozen_pages = :pf AS vm_new_frozen_pages,vm_new_visible_pages = :pv AS vm_new_visible_pages,vm_new_visible_frozen_pages = :pvf AS vm_new_visible_frozen_pages, rev_all_frozen_pages = :hafp AS rev_all_frozen_pages,rev_all_visible_pages = :havp AS rev_all_visible_pages
+FROM pg_stat_vacuum_tables, pg_stat_all_tables WHERE pg_stat_vacuum_tables.relname = 'vestat' and pg_stat_vacuum_tables.relid = pg_stat_all_tables.relid;
+ vm_new_frozen_pages | vm_new_visible_pages | vm_new_visible_frozen_pages | rev_all_frozen_pages | rev_all_visible_pages 
+---------------------+----------------------+-----------------------------+----------------------+-----------------------
+ t                   | t                    | t                           | t                    | t
+(1 row)
+
+DROP TABLE vestat CASCADE;
+-- Now check vacuum statistics for current database
+SELECT dbname,
+       db_blks_hit > 0 AS db_blks_hit,
+       total_blks_dirtied > 0 AS total_blks_dirtied,
+       total_blks_written > 0 AS total_blks_written,
+       wal_records > 0 AS wal_records,
+       wal_fpi > 0 AS wal_fpi,
+       wal_bytes > 0 AS wal_bytes,
+       total_time > 0 AS total_time
+FROM
+pg_stat_vacuum_database
+WHERE dbname = current_database();
+             dbname             | db_blks_hit | total_blks_dirtied | total_blks_written | wal_records | wal_fpi | wal_bytes | total_time 
+--------------------------------+-------------+--------------------+--------------------+-------------+---------+-----------+------------
+ regression_statistic_vacuum_db | t           | t                  | t                  | t           | t       | t         | t
+(1 row)
+
+-- ensure pending stats are flushed
+SELECT pg_stat_force_next_flush();
+ pg_stat_force_next_flush 
+--------------------------
+ 
+(1 row)
+
+CREATE TABLE vestat (x int) WITH (autovacuum_enabled = off, fillfactor = 10);
+INSERT INTO vestat SELECT x FROM generate_series(1,:sample_size) as x;
+ANALYZE vestat;
+UPDATE vestat SET x = 10001;
+VACUUM (PARALLEL 0, BUFFER_USAGE_LIMIT 128) vestat;
+\c regression_statistic_vacuum_db1;
+SET track_vacuum_statistics TO on;
+-- Now check vacuum statistics for postgres database from another database
+SELECT dbname,
+       db_blks_hit > 0 AS db_blks_hit,
+       total_blks_dirtied > 0 AS total_blks_dirtied,
+       total_blks_written > 0 AS total_blks_written,
+       wal_records > 0 AS wal_records,
+       wal_fpi > 0 AS wal_fpi,
+       wal_bytes > 0 AS wal_bytes,
+       total_time > 0 AS total_time
+FROM
+pg_stat_vacuum_database
+WHERE dbname = 'regression_statistic_vacuum_db';
+             dbname             | db_blks_hit | total_blks_dirtied | total_blks_written | wal_records | wal_fpi | wal_bytes | total_time 
+--------------------------------+-------------+--------------------+--------------------+-------------+---------+-----------+------------
+ regression_statistic_vacuum_db | t           | t                  | t                  | t           | t       | t         | t
+(1 row)
+
+\c regression_statistic_vacuum_db
+SET track_vacuum_statistics TO on;
+DROP TABLE vestat CASCADE;
+\c regression_statistic_vacuum_db1;
+SET track_vacuum_statistics TO on;
+SELECT count(*)
+FROM pg_database d
+CROSS JOIN pg_stat_get_vacuum_tables(0)
+WHERE oid = 0; -- must be 0
+ count 
+-------
+     0
+(1 row)
+
+\c postgres
+DROP DATABASE regression_statistic_vacuum_db1;
+DROP DATABASE regression_statistic_vacuum_db;
+RESET track_vacuum_statistics;
diff --git a/spec/vacuum-extending-in-repetable-read.spec b/spec/vacuum-extending-in-repetable-read.spec
new file mode 100644
index 0000000..13b6c91
--- /dev/null
+++ b/spec/vacuum-extending-in-repetable-read.spec
@@ -0,0 +1,173 @@
+# A number of tests dedicated to verification of the 'Extended Vacuum Statistics'
+# feature.
+# By default, statistics has a volatile nature. So, selection result can depend
+# on a bunch of things. Here some trivial tests are performed that should work
+# in the most cases.
+# Test for checking pages: frozen, scanned, removed, number of tuple_deleted in pgpro_stats_vacuum_tables.
+# Besides, this test check pages scanned, pages removed, tuples_deleted in pgpro_stats_vacuum_tables and
+# wal values statistic collected over vacuum operation as for tables as for indexes.
+
+setup
+{
+    CREATE TABLE vestat (x int primary key) WITH (autovacuum_enabled = off);
+
+    CREATE EXTENSION vacuum_statistics;
+
+    CREATE TABLE vacuum_wal_stats_table
+    (relid int, wal_records int, wal_fpi int, wal_bytes int);
+    insert into vacuum_wal_stats_table (relid)
+    select oid from pg_class c
+    WHERE relname = 'vestat';
+    UPDATE vacuum_wal_stats_table SET
+    wal_records = 0, wal_fpi = 0, wal_bytes = 0;
+
+    CREATE TABLE vacuum_wal_stats_index
+    (relid int, wal_records int, wal_fpi int, wal_bytes int);
+    insert into vacuum_wal_stats_index (relid)
+    select oid from pg_class c
+    WHERE relname = 'vestat_pkey';
+    UPDATE vacuum_wal_stats_index SET
+    wal_records = 0, wal_fpi = 0, wal_bytes = 0;
+
+    SET track_io_timing = on;
+    SHOW track_counts;  -- must be on
+    SET track_functions TO 'all';
+
+}
+
+teardown
+{
+    RESET vacuum_freeze_min_age;
+    RESET vacuum_freeze_table_age;
+    DROP TABLE vestat CASCADE;
+    DROP TABLE vacuum_wal_stats_index;
+    DROP TABLE vacuum_wal_stats_table;
+    DROP EXTENSION vacuum_statistics;
+}
+
+session s1
+step s1_set_agressive_vacuum    { SET vacuum_freeze_min_age = 0; }
+step s1_insert                  { INSERT INTO vestat(x) SELECT id FROM generate_series(1,770) As id; }
+step s1_update                  { UPDATE vestat SET x = x+1; }
+step s1_delete_half_table       { DELETE FROM vestat WHERE x % 2 = 0; }
+step s1_delete_full_table       { DELETE FROM vestat; }
+step s1_vacuum                  { VACUUM vestat; }
+step s1_vacuum_full             { VACUUM FULL vestat; }
+step s1_vacuum_parallel         { VACUUM (PARALLEL 2, INDEX_CLEANUP ON) vestat; }
+step s1_analyze                 { ANALYZE vestat; }
+step s1_trancate                { TRUNCATE vestat; }
+step s1_checkpoint              { CHECKPOINT; }
+step s1_print_vacuum_stats_tables
+{
+    SELECT vt.relname,
+           pages_frozen,
+           tuples_deleted,
+           pages_scanned,
+           pages_removed
+    FROM pgpro_stats_vacuum_tables vt, pg_class c
+    WHERE vt.relname = 'vestat' AND
+          vt.relid = c.oid;
+}
+
+step s1_print_vacuum_stats_indexes
+{
+    SELECT vt.relname,
+           pages_deleted,
+           tuples_deleted
+    FROM pgpro_stats_vacuum_indexes vt, pg_class c
+    WHERE vt.relname = 'vestat_pkey' AND
+          vt.relid = c.oid;
+}
+
+step s1_save_walls
+{
+    UPDATE vacuum_wal_stats_table SET
+    wal_records = dWR, wal_fpi = dFPI, wal_bytes = dWB
+    FROM (SELECT relid, wal_records AS dWR, wal_fpi AS dFPI, wal_bytes AS dWB
+    FROM pgpro_stats_vacuum_tables vt, pg_class c
+    WHERE vt.relname = 'vestat' AND
+          vt.relid = c.oid) t
+    WHERE
+          t.relid = t.relid;
+
+   UPDATE vacuum_wal_stats_index SET
+    wal_records = iWR, wal_fpi = iFPI, wal_bytes = iWB
+    FROM (SELECT relid, wal_records AS iWR, wal_fpi AS iFPI, wal_bytes AS iWB
+    FROM pgpro_stats_vacuum_indexes vt, pg_class c
+    WHERE vt.relname = 'vestat_pkey' AND
+          vt.relid = c.oid) t
+    WHERE
+          t.relid = t.relid;
+}
+
+step s1_difference
+{
+    SELECT t1.wal_records - t0.wal_records > 0 AS dWR,
+           t1.wal_fpi - t0.wal_fpi > 0 AS dFPI,
+           t1.wal_bytes - t0.wal_bytes > 0 AS dWB
+    FROM vacuum_wal_stats_table t0, pgpro_stats_vacuum_tables t1
+    WHERE t0.relid = t1.relid;
+
+    SELECT t1.wal_records - t0.wal_records > 0 AS iWR,
+           t1.wal_fpi - t0.wal_fpi > 0 AS iFPI,
+           t1.wal_bytes - t0.wal_bytes > 0 AS iWB
+    FROM vacuum_wal_stats_table t0, pgpro_stats_vacuum_tables t1
+    WHERE t0.relid = t1.relid;
+}
+
+permutation
+    s1_insert
+    s1_print_vacuum_stats_tables
+    s1_print_vacuum_stats_indexes
+    s1_set_agressive_vacuum
+    s1_analyze
+    s1_delete_half_table
+    s1_print_vacuum_stats_tables
+    s1_print_vacuum_stats_indexes
+    s1_vacuum
+    s1_print_vacuum_stats_tables
+    s1_print_vacuum_stats_indexes
+    s1_delete_full_table
+    s1_vacuum_parallel
+    s1_print_vacuum_stats_tables
+    s1_print_vacuum_stats_indexes
+    s1_insert
+    s1_update
+    s1_vacuum_full
+    s1_print_vacuum_stats_tables
+    s1_print_vacuum_stats_indexes
+    s1_delete_full_table
+    s1_trancate
+    s1_vacuum
+    s1_print_vacuum_stats_tables
+    s1_print_vacuum_stats_indexes
+
+permutation
+    s1_insert
+    s1_set_agressive_vacuum
+    s1_analyze
+    s1_delete_half_table
+    s1_checkpoint
+    s1_difference
+    s1_save_walls
+    s1_vacuum
+    s1_checkpoint
+    s1_difference
+    s1_save_walls
+    s1_delete_full_table
+    s1_vacuum_parallel
+    s1_checkpoint
+    s1_difference
+    s1_save_walls
+    s1_insert
+    s1_update
+    s1_vacuum_full
+    s1_checkpoint
+    s1_difference
+    s1_save_walls
+    s1_delete_full_table
+    s1_trancate
+    s1_vacuum
+    s1_checkpoint
+    s1_difference
+    s1_save_walls
\ No newline at end of file
diff --git a/sql/vacuum_index_statistics.sql b/sql/vacuum_index_statistics.sql
new file mode 100644
index 0000000..996ea04
--- /dev/null
+++ b/sql/vacuum_index_statistics.sql
@@ -0,0 +1,138 @@
+--
+-- Test cumulative vacuum stats system
+--
+-- Check the wall statistics collected during vacuum operation:
+-- number of frozen and visible pages set by vacuum;
+-- number of frozen and visible pages removed by backend.
+-- Statistic wal_fpi is not displayed in this test because its behavior is unstable.
+--
+-- conditio sine qua non
+SHOW track_counts;  -- must be on
+
+\set sample_size 10000
+
+-- not enabled by default, but we want to test it...
+SET track_functions TO 'all';
+
+CREATE EXTENSION vacuum_statistics;
+
+SHOW vacuum_statistics.enabled;  -- must be on
+
+CREATE TABLE vestat (x int) WITH (autovacuum_enabled = off, fillfactor = 10);
+INSERT INTO vestat SELECT x FROM generate_series(1,:sample_size) as x;
+ANALYZE vestat;
+
+DELETE FROM vestat WHERE x % 2 = 0;
+
+VACUUM (PARALLEL 0, BUFFER_USAGE_LIMIT 128) vestat;
+
+-- Must be empty.
+SELECT *
+FROM pg_stats_vacuum_indexes vt
+WHERE vt.indexrelname = 'vestat';
+
+SELECT oid AS ioid from pg_class where relname = 'vestat_pkey' \gset
+
+DELETE FROM vestat WHERE x % 2 = 0;
+-- Before the first vacuum execution extended stats view is empty.
+SELECT vt.indexrelname,relpages,pages_deleted,tuples_deleted
+FROM pg_stats_vacuum_indexes vt, pg_class c
+WHERE vt.indexrelname = 'vestat_pkey' AND vt.indexrelid = c.oid;
+SELECT relpages AS irp
+FROM pg_class c
+WHERE relname = 'vestat_pkey' \gset
+
+VACUUM (PARALLEL 0, BUFFER_USAGE_LIMIT 128, INDEX_CLEANUP ON) vestat;
+-- it is necessary to check the wal statistics
+CHECKPOINT;
+
+-- The table and index extended vacuum statistics should show us that
+-- vacuum frozed pages and clean up pages, but pages_removed stayed the same
+-- because of not full table have cleaned up
+SELECT vt.indexrelname,relpages-:irp = 0 AS relpages,pages_deleted = 0 AS pages_deleted,tuples_deleted > 0 AS tuples_deleted
+FROM pg_stats_vacuum_indexes vt, pg_class c
+WHERE vt.indexrelname = 'vestat_pkey' AND vt.indexrelid = c.oid;
+SELECT vt.indexrelname,relpages AS irp,pages_deleted AS ipd,tuples_deleted AS itd
+FROM pg_stats_vacuum_indexes vt, pg_class c
+WHERE vt.indexrelname = 'vestat_pkey' AND vt.indexrelid = c.oid \gset
+
+-- Store WAL advances into variables
+SELECT wal_records AS iwr,wal_bytes AS iwb,wal_fpi AS ifpi FROM pg_stats_vacuum_indexes WHERE indexrelname = 'vestat_pkey' \gset
+
+-- Look into WAL records deltas.
+SELECT wal_records > 0 AS diWR, wal_bytes > 0 AS diWB
+FROM pg_stats_vacuum_indexes WHERE indexrelname = 'vestat_pkey';
+
+DELETE FROM vestat;;
+VACUUM (PARALLEL 0, BUFFER_USAGE_LIMIT 128, INDEX_CLEANUP ON) vestat;
+-- it is necessary to check the wal statistics
+CHECKPOINT;
+
+-- pages_removed must be increased
+SELECT vt.indexrelname,relpages-:irp = 0 AS relpages,pages_deleted-:ipd > 0 AS pages_deleted,tuples_deleted-:itd > 0 AS tuples_deleted
+FROM pg_stats_vacuum_indexes vt, pg_class c
+WHERE vt.indexrelname = 'vestat_pkey' AND vt.indexrelid = c.oid;
+SELECT vt.indexrelname,relpages AS irp,pages_deleted AS ipd,tuples_deleted AS itd
+FROM pg_stats_vacuum_indexes vt, pg_class c
+WHERE vt.indexrelname = 'vestat_pkey' AND vt.indexrelid = c.oid \gset
+
+-- Store WAL advances into variables
+SELECT wal_records-:iwr AS diwr, wal_bytes-:iwb AS diwb, wal_fpi-:ifpi AS difpi
+FROM pg_stats_vacuum_indexes WHERE indexrelname = 'vestat_pkey' \gset
+
+-- WAL advance should be detected.
+SELECT :diwr > 0 AS diWR, :diwb > 0 AS diWB;
+
+-- Store WAL advances into variables
+SELECT wal_records AS iwr,wal_bytes AS iwb,wal_fpi AS ifpi FROM pg_stats_vacuum_indexes WHERE indexrelname = 'vestat_pkey' \gset
+
+INSERT INTO vestat SELECT x FROM generate_series(1,:sample_size) as x;
+DELETE FROM vestat WHERE x % 2 = 0;
+-- VACUUM FULL doesn't report to stat collector. So, no any advancements of statistics
+-- are detected here.
+VACUUM FULL vestat;
+-- It is necessary to check the wal statistics
+CHECKPOINT;
+
+-- Store WAL advances into variables
+SELECT wal_records-:iwr AS diwr2, wal_bytes-:iwb AS diwb2, wal_fpi-:ifpi AS difpi2
+FROM pg_stats_vacuum_indexes WHERE indexrelname = 'vestat_pkey' \gset
+
+-- WAL and other statistics advance should not be detected.
+SELECT :diwr2=0 AS diWR, :difpi2=0 AS iFPI, :diwb2=0 AS diWB;
+
+SELECT vt.indexrelname,relpages-:irp < 0 AS relpages,pages_deleted-:ipd = 0 AS pages_deleted,tuples_deleted-:itd = 0 AS tuples_deleted
+FROM pg_stats_vacuum_indexes vt, pg_class c
+WHERE vt.indexrelname = 'vestat_pkey' AND vt.indexrelid = c.oid;
+SELECT vt.indexrelname,relpages AS irp,pages_deleted AS ipd,tuples_deleted AS itd
+FROM pg_stats_vacuum_indexes vt, pg_class c
+WHERE vt.indexrelname = 'vestat_pkey' AND vt.indexrelid = c.oid \gset
+
+-- Store WAL advances into variables
+SELECT wal_records AS iwr,wal_bytes AS iwb,wal_fpi AS ifpi FROM pg_stats_vacuum_indexes WHERE indexrelname = 'vestat_pkey' \gset
+
+DELETE FROM vestat;
+TRUNCATE vestat;
+VACUUM (PARALLEL 0, BUFFER_USAGE_LIMIT 128, INDEX_CLEANUP ON) vestat;
+-- it is necessary to check the wal statistics
+CHECKPOINT;
+
+-- Store WAL advances into variables after removing all tuples from the table
+SELECT wal_records-:iwr AS diwr3, wal_bytes-:iwb AS diwb3, wal_fpi-:ifpi AS difpi3
+FROM pg_stats_vacuum_indexes WHERE indexrelname = 'vestat_pkey' \gset
+
+--There are nothing changed
+SELECT :diwr3=0 AS diWR, :difpi3=0 AS iFPI, :diwb3=0 AS diWB;
+
+--
+-- Now, the table and index is compressed into zero number of pages. Check it
+-- in vacuum extended statistics.
+-- The pages_frozen, pages_scanned values shouldn't be changed
+--
+SELECT vt.indexrelname,relpages-:irp = 0 AS relpages,pages_deleted-:ipd = 0 AS pages_deleted,tuples_deleted-:itd = 0 AS tuples_deleted
+FROM pg_stats_vacuum_indexes vt, pg_class c
+WHERE vt.indexrelname = 'vestat_pkey' AND vt.indexrelid = c.oid;
+
+DROP TABLE vestat;
+
+DROP EXTENSION vacuum_statistics;
diff --git a/sql/vacuum_tables_and_db_statistics.sql b/sql/vacuum_tables_and_db_statistics.sql
new file mode 100644
index 0000000..1e81a82
--- /dev/null
+++ b/sql/vacuum_tables_and_db_statistics.sql
@@ -0,0 +1,224 @@
+--
+-- Test cumulative vacuum stats system
+--
+-- Check the wall statistics collected during vacuum operation:
+-- number of frozen and visible pages set by vacuum;
+-- number of frozen and visible pages removed by backend.
+-- Statistic wal_fpi is not displayed in this test because its behavior is unstable.
+--
+
+SHOW track_counts;  -- must be on
+\set sample_size 10000
+
+-- not enabled by default, but we want to test it...
+SET track_functions TO 'all';
+
+CREATE EXTENSION vacuum_statistics;
+
+SHOW vacuum_statistics.enabled;  -- must be on
+
+CREATE DATABASE regression_statistic_vacuum_db;
+CREATE DATABASE regression_statistic_vacuum_db1;
+\c regression_statistic_vacuum_db;
+CREATE EXTENSION vacuum_statistics;
+
+-- not enabled by default, but we want to test it...
+SET track_functions TO 'all';
+
+-- ensure pending stats are flushed
+SELECT pg_stat_force_next_flush();
+
+--SET stats_fetch_consistency = snapshot;
+CREATE TABLE vestat (x int) WITH (autovacuum_enabled = off, fillfactor = 10);
+INSERT INTO vestat SELECT x FROM generate_series(1,:sample_size) as x;
+ANALYZE vestat;
+
+SELECT oid AS roid from pg_class where relname = 'vestat' \gset
+
+DELETE FROM vestat WHERE x % 2 = 0;
+-- Before the first vacuum execution extended stats view is empty.
+SELECT vt.relname,vm_new_frozen_pages,tuples_deleted,relpages,pages_scanned,pages_removed
+FROM pg_stats_vacuum_tables vt, pg_class c
+WHERE vt.relname = 'vestat' AND vt.relid = c.oid;
+SELECT relpages AS rp
+FROM pg_class c
+WHERE relname = 'vestat' \gset
+
+VACUUM (PARALLEL 0, BUFFER_USAGE_LIMIT 128, INDEX_CLEANUP OFF) vestat;
+-- it is necessary to check the wal statistics
+CHECKPOINT;
+
+-- The table and index extended vacuum statistics should show us that
+-- vacuum frozed pages and clean up pages, but pages_removed stayed the same
+-- because of not full table have cleaned up
+SELECT vt.relname,vm_new_frozen_pages > 0 AS vm_new_frozen_pages,tuples_deleted > 0 AS tuples_deleted,relpages-:rp = 0 AS relpages,pages_scanned > 0 AS pages_scanned,pages_removed = 0 AS pages_removed
+FROM pg_stats_vacuum_tables vt, pg_class c
+WHERE vt.relname = 'vestat' AND vt.relid = c.oid;
+SELECT vm_new_frozen_pages AS fp,tuples_deleted AS td,relpages AS rp, pages_scanned AS ps, pages_removed AS pr
+FROM pg_stats_vacuum_tables vt, pg_class c
+WHERE vt.relname = 'vestat' AND vt.relid = c.oid \gset
+
+-- Store WAL advances into variables
+SELECT wal_records AS hwr,wal_bytes AS hwb,wal_fpi AS hfpi FROM pg_stats_vacuum_tables WHERE relname = 'vestat' \gset
+
+-- Look into WAL records deltas.
+SELECT wal_records > 0 AS dWR, wal_bytes > 0 AS dWB
+FROM pg_stats_vacuum_tables WHERE relname = 'vestat';
+
+DELETE FROM vestat;;
+VACUUM (PARALLEL 0, BUFFER_USAGE_LIMIT 128, INDEX_CLEANUP OFF) vestat;
+-- it is necessary to check the wal statistics
+CHECKPOINT;
+
+-- pages_removed must be increased
+SELECT vt.relname,vm_new_frozen_pages-:fp > 0 AS vm_new_frozen_pages,tuples_deleted-:td > 0 AS tuples_deleted,relpages -:rp = 0 AS relpages,pages_scanned-:ps > 0 AS pages_scanned,pages_removed-:pr > 0 AS pages_removed
+FROM pg_stats_vacuum_tables vt, pg_class c
+WHERE vt.relname = 'vestat' AND vt.relid = c.oid;
+SELECT vm_new_frozen_pages AS fp,tuples_deleted AS td,relpages AS rp, pages_scanned AS ps, pages_removed AS pr
+FROM pg_stats_vacuum_tables vt, pg_class c
+WHERE vt.relname = 'vestat' AND vt.relid = c.oid \gset
+
+-- Store WAL advances into variables
+SELECT wal_records-:hwr AS dwr, wal_bytes-:hwb AS dwb, wal_fpi-:hfpi AS dfpi
+FROM pg_stats_vacuum_tables WHERE relname = 'vestat' \gset
+
+-- WAL advance should be detected.
+SELECT :dwr > 0 AS dWR, :dwb > 0 AS dWB;
+
+-- Store WAL advances into variables
+SELECT wal_records AS hwr,wal_bytes AS hwb,wal_fpi AS hfpi FROM pg_stats_vacuum_tables WHERE relname = 'vestat' \gset
+
+INSERT INTO vestat SELECT x FROM generate_series(1,:sample_size) as x;
+DELETE FROM vestat WHERE x % 2 = 0;
+-- VACUUM FULL doesn't report to stat collector. So, no any advancements of statistics
+-- are detected here.
+VACUUM FULL vestat;
+-- It is necessary to check the wal statistics
+CHECKPOINT;
+
+-- Store WAL advances into variables
+SELECT wal_records-:hwr AS dwr2, wal_bytes-:hwb AS dwb2, wal_fpi-:hfpi AS dfpi2
+FROM pg_stats_vacuum_tables WHERE relname = 'vestat' \gset
+
+-- WAL and other statistics advance should not be detected.
+SELECT :dwr2=0 AS dWR, :dfpi2=0 AS dFPI, :dwb2=0 AS dWB;
+
+SELECT vt.relname,vm_new_frozen_pages-:fp = 0 AS vm_new_frozen_pages,tuples_deleted-:td = 0 AS tuples_deleted,relpages -:rp < 0 AS relpages,pages_scanned-:ps = 0 AS pages_scanned,pages_removed-:pr = 0 AS pages_removed
+FROM pg_stats_vacuum_tables vt, pg_class c
+WHERE vt.relname = 'vestat' AND vt.relid = c.oid;
+SELECT vm_new_frozen_pages AS fp,tuples_deleted AS td,relpages AS rp, pages_scanned AS ps,pages_removed AS pr
+FROM pg_stats_vacuum_tables vt, pg_class c
+WHERE vt.relname = 'vestat' AND vt.relid = c.oid \gset
+
+-- Store WAL advances into variables
+SELECT wal_records AS hwr,wal_bytes AS hwb,wal_fpi AS hfpi FROM pg_stats_vacuum_tables WHERE relname = 'vestat' \gset
+
+DELETE FROM vestat;
+TRUNCATE vestat;
+VACUUM (PARALLEL 0, BUFFER_USAGE_LIMIT 128, INDEX_CLEANUP OFF) vestat;
+-- it is necessary to check the wal statistics
+CHECKPOINT;
+
+-- Store WAL advances into variables after removing all tuples from the table
+SELECT wal_records-:hwr AS dwr3, wal_bytes-:hwb AS dwb3, wal_fpi-:hfpi AS dfpi3
+FROM pg_stats_vacuum_tables WHERE relname = 'vestat' \gset
+
+--There are nothing changed
+SELECT :dwr3>0 AS dWR, :dfpi3=0 AS dFPI, :dwb3>0 AS dWB;
+
+--
+-- Now, the table and index is compressed into zero number of pages. Check it
+-- in vacuum extended statistics.
+-- The vm_new_frozen_pages, pages_scanned values shouldn't be changed
+--
+SELECT vt.relname,vm_new_frozen_pages-:fp = 0 AS vm_new_frozen_pages,tuples_deleted-:td = 0 AS tuples_deleted,relpages -:rp = 0 AS relpages,pages_scanned-:ps = 0 AS pages_scanned,pages_removed-:pr = 0 AS pages_removed
+FROM pg_stats_vacuum_tables vt, pg_class c
+WHERE vt.relname = 'vestat' AND vt.relid = c.oid;
+
+DROP TABLE vestat CASCADE;
+CREATE TABLE vestat (x int) WITH (autovacuum_enabled = off, fillfactor = 10);
+
+INSERT INTO vestat SELECT x FROM generate_series(1,:sample_size) as x;
+ANALYZE vestat;
+
+-- must be empty
+SELECT vm_new_frozen_pages, vm_new_visible_pages, rev_all_frozen_pages,rev_all_visible_pages,vm_new_visible_frozen_pages
+FROM pg_stats_vacuum_tables, pg_stat_all_tables WHERE pg_stats_vacuum_tables.relname = 'vestat' and pg_stats_vacuum_tables.relid = pg_stat_all_tables.relid;
+
+VACUUM (PARALLEL 0, BUFFER_USAGE_LIMIT 128) vestat;
+
+-- backend defreezed pages
+SELECT vm_new_frozen_pages > 0 AS vm_new_frozen_pages,vm_new_visible_pages > 0 AS vm_new_visible_pages,vm_new_visible_frozen_pages > 0 AS vm_new_visible_frozen_pages,rev_all_frozen_pages = 0 AS rev_all_frozen_pages,rev_all_visible_pages = 0 AS rev_all_visible_pages
+FROM pg_stats_vacuum_tables, pg_stat_all_tables WHERE pg_stats_vacuum_tables.relname = 'vestat' and pg_stats_vacuum_tables.relid = pg_stat_all_tables.relid;
+SELECT vm_new_frozen_pages AS pf, vm_new_visible_pages AS pv,vm_new_visible_frozen_pages AS pvf, rev_all_frozen_pages AS hafp,rev_all_visible_pages AS havp
+FROM pg_stats_vacuum_tables, pg_stat_all_tables WHERE pg_stats_vacuum_tables.relname = 'vestat' and pg_stats_vacuum_tables.relid = pg_stat_all_tables.relid \gset
+
+UPDATE vestat SET x = x + 1001;
+VACUUM (PARALLEL 0, BUFFER_USAGE_LIMIT 128) vestat;
+
+SELECT vm_new_frozen_pages > :pf AS vm_new_frozen_pages,vm_new_visible_pages > :pv AS vm_new_visible_pages,vm_new_visible_frozen_pages > :pvf AS vm_new_visible_frozen_pages,rev_all_frozen_pages > :hafp AS rev_all_frozen_pages,rev_all_visible_pages > :havp AS rev_all_visible_pages
+FROM pg_stats_vacuum_tables, pg_stat_all_tables WHERE pg_stats_vacuum_tables.relname = 'vestat' and pg_stats_vacuum_tables.relid = pg_stat_all_tables.relid;
+SELECT vm_new_frozen_pages AS pf, vm_new_visible_pages AS pv, vm_new_visible_frozen_pages AS pvf, rev_all_frozen_pages AS hafp,rev_all_visible_pages AS havp
+FROM pg_stats_vacuum_tables, pg_stat_all_tables WHERE pg_stats_vacuum_tables.relname = 'vestat' and pg_stats_vacuum_tables.relid = pg_stat_all_tables.relid \gset
+
+VACUUM (PARALLEL 0, BUFFER_USAGE_LIMIT 128) vestat;
+
+-- vacuum freezed pages
+SELECT vm_new_frozen_pages = :pf AS vm_new_frozen_pages,vm_new_visible_pages = :pv AS vm_new_visible_pages,vm_new_visible_frozen_pages = :pvf AS vm_new_visible_frozen_pages, rev_all_frozen_pages = :hafp AS rev_all_frozen_pages,rev_all_visible_pages = :havp AS rev_all_visible_pages
+FROM pg_stats_vacuum_tables, pg_stat_all_tables WHERE pg_stats_vacuum_tables.relname = 'vestat' and pg_stats_vacuum_tables.relid = pg_stat_all_tables.relid;
+
+DROP TABLE vestat CASCADE;
+
+select count(*) from pg_stats_vacuum_tables where relname = 'vestat';
+
+-- -- Now check vacuum statistics for current database
+-- SELECT dbname,
+--        db_blks_hit > 0 AS db_blks_hit,
+--        total_blks_dirtied > 0 AS total_blks_dirtied,
+--        total_blks_written > 0 AS total_blks_written,
+--        wal_records > 0 AS wal_records,
+--        wal_fpi > 0 AS wal_fpi,
+--        wal_bytes > 0 AS wal_bytes,
+--        total_time > 0 AS total_time
+-- FROM
+-- pg_stat_vacuum_database
+-- WHERE dbname = current_database();
+
+-- -- ensure pending stats are flushed
+-- SELECT pg_stat_force_next_flush();
+
+-- CREATE TABLE vestat (x int) WITH (autovacuum_enabled = off, fillfactor = 10);
+-- INSERT INTO vestat SELECT x FROM generate_series(1,:sample_size) as x;
+-- ANALYZE vestat;
+-- UPDATE vestat SET x = 10001;
+-- VACUUM (PARALLEL 0, BUFFER_USAGE_LIMIT 128) vestat;
+
+-- \c regression_statistic_vacuum_db1;
+-- CREATE EXTENSION vacuum_statistics;
+
+-- -- Now check vacuum statistics for postgres database from another database
+-- SELECT dbname,
+--        db_blks_hit > 0 AS db_blks_hit,
+--        total_blks_dirtied > 0 AS total_blks_dirtied,
+--        total_blks_written > 0 AS total_blks_written,
+--        wal_records > 0 AS wal_records,
+--        wal_fpi > 0 AS wal_fpi,
+--        wal_bytes > 0 AS wal_bytes,
+--        total_time > 0 AS total_time
+-- FROM
+-- pg_stat_vacuum_database
+-- WHERE dbname = 'regression_statistic_vacuum_db';
+
+-- \c regression_statistic_vacuum_db
+
+-- DROP TABLE vestat CASCADE;
+
+-- \c regression_statistic_vacuum_db1;
+-- SELECT count(*)
+-- FROM pg_database d
+-- CROSS JOIN pg_stat_get_vacuum_tables(0)
+-- WHERE oid = 0; -- must be 0
+
+\c postgres
+DROP DATABASE regression_statistic_vacuum_db1;
+DROP DATABASE regression_statistic_vacuum_db;
\ No newline at end of file
diff --git a/vacuum_statistics--1.0.sql b/vacuum_statistics--1.0.sql
new file mode 100644
index 0000000..bb78f3a
--- /dev/null
+++ b/vacuum_statistics--1.0.sql
@@ -0,0 +1,191 @@
+-- complain if script is sourced in psql, rather than via CREATE EXTENSION
+\echo Use "CREATE EXTENSION vacuum_statistics" to load this file. \quit
+
+-- schema: extvac
+CREATE SCHEMA IF NOT EXISTS extvac;
+
+
+-- In your extension's .sql install script
+CREATE OR REPLACE FUNCTION extvac_reset_entry(
+    dboid oid,
+    relid oid,
+    type  int4
+)
+RETURNS void
+AS 'MODULE_PATHNAME', 'extvac_reset_entry'
+LANGUAGE C
+STRICT
+PARALLEL SAFE;
+
+--
+-- Show extended cumulative statistics on a vacuum operation over all tables and
+-- databases of the instance.
+-- Use Invalid Oid "0" as an input relation id to get stat on each table in a
+-- database.
+--
+CREATE FUNCTION pg_stats_get_vacuum_tables(
+    IN  dboid oid,
+    IN  reloid oid,
+
+    OUT relid oid,
+
+    OUT total_blks_read bigint,
+    OUT total_blks_hit bigint,
+    OUT total_blks_dirtied bigint,
+    OUT total_blks_written bigint,
+
+    OUT wal_records bigint,
+    OUT wal_fpi bigint,
+    OUT wal_bytes numeric,
+
+    OUT blk_read_time double precision,
+    OUT blk_write_time double precision,
+    OUT delay_time double precision,
+    OUT total_time double precision,
+
+    OUT wraparound_failsafe_count integer,
+
+    OUT rel_blks_read bigint,
+    OUT rel_blks_hit  bigint,
+
+    OUT tuples_deleted bigint,
+    OUT pages_scanned bigint,
+    OUT pages_removed bigint,
+    OUT vm_new_frozen_pages bigint,
+    OUT vm_new_visible_pages bigint,
+    OUT vm_new_visible_frozen_pages bigint,
+    OUT tuples_frozen bigint,
+    OUT recently_dead_tuples bigint,
+    OUT index_vacuum_count bigint,
+    OUT missed_dead_pages bigint,
+    OUT missed_dead_tuples bigint
+)
+RETURNS SETOF record
+AS 'MODULE_PATHNAME', 'pg_stats_get_vacuum_tables'
+LANGUAGE C
+STRICT
+VOLATILE;
+
+GRANT EXECUTE ON FUNCTION pg_stats_get_vacuum_tables TO PUBLIC;
+
+-- Tables view
+DROP VIEW IF EXISTS pg_stats_vacuum_tables;
+
+CREATE VIEW pg_stats_vacuum_tables AS
+SELECT
+  rel.oid                         AS relid,
+  ns.nspname                      AS "schema",
+  rel.relname                     AS relname,
+
+  stats.total_blks_read,
+  stats.total_blks_hit,
+  stats.total_blks_dirtied,
+  stats.total_blks_written,
+
+  stats.wal_records,
+  stats.wal_fpi,
+  stats.wal_bytes,
+
+  stats.blk_read_time,
+  stats.blk_write_time,
+  stats.delay_time,
+  stats.total_time,
+
+  stats.wraparound_failsafe_count,
+
+  stats.rel_blks_read,
+  stats.rel_blks_hit,
+
+  stats.tuples_deleted,
+  stats.pages_scanned,
+  stats.pages_removed,
+  stats.vm_new_frozen_pages,
+  stats.vm_new_visible_pages,
+  stats.vm_new_visible_frozen_pages,
+  stats.tuples_frozen,
+  stats.recently_dead_tuples,
+  stats.index_vacuum_count,
+  stats.missed_dead_pages,
+  stats.missed_dead_tuples
+FROM pg_class rel
+JOIN pg_namespace ns ON ns.oid = rel.relnamespace
+CROSS JOIN LATERAL pg_stats_get_vacuum_tables(
+  (SELECT oid FROM pg_database WHERE datname = current_database()),
+  rel.oid
+) AS stats
+WHERE rel.relkind = 'r';
+
+
+CREATE FUNCTION pg_stats_get_vacuum_indexes(
+    IN  dboid oid,
+    IN  reloid oid,
+
+    OUT relid oid,
+
+    OUT total_blks_read bigint,
+    OUT total_blks_hit bigint,
+    OUT total_blks_dirtied bigint,
+    OUT total_blks_written bigint,
+
+    OUT wal_records bigint,
+    OUT wal_fpi bigint,
+    OUT wal_bytes numeric,
+
+    OUT blk_read_time double precision,
+    OUT blk_write_time double precision,
+    OUT delay_time double precision,
+    OUT total_time double precision,
+
+    OUT wraparound_failsafe_count integer,
+
+    OUT rel_blks_read bigint,
+    OUT rel_blks_hit  bigint,
+
+    OUT tuples_deleted bigint,
+    OUT pages_deleted bigint
+)
+RETURNS SETOF record
+AS 'MODULE_PATHNAME', 'pg_stats_get_vacuum_indexes'
+LANGUAGE C
+STRICT
+VOLATILE;
+
+GRANT EXECUTE ON FUNCTION pg_stats_get_vacuum_indexes TO PUBLIC;
+
+-- Indexes view
+DROP VIEW IF EXISTS pg_stats_vacuum_indexes;
+
+CREATE VIEW pg_stats_vacuum_indexes AS
+SELECT
+  rel.oid                         AS relid,
+  ns.nspname                      AS "schema",
+  rel.relname                     AS relname,
+
+  stats.total_blks_read,
+  stats.total_blks_hit,
+  stats.total_blks_dirtied,
+  stats.total_blks_written,
+
+  stats.wal_records,
+  stats.wal_fpi,
+  stats.wal_bytes,
+
+  stats.blk_read_time,
+  stats.blk_write_time,
+  stats.delay_time,
+  stats.total_time,
+
+  stats.wraparound_failsafe_count,
+
+  stats.rel_blks_read,
+  stats.rel_blks_hit,
+
+  stats.tuples_deleted,
+  stats.pages_deleted
+FROM pg_class rel
+JOIN pg_namespace ns ON ns.oid = rel.relnamespace
+CROSS JOIN LATERAL pg_stats_get_vacuum_indexes(
+  (SELECT oid FROM pg_database WHERE datname = current_database()),
+  rel.oid
+) AS stats
+WHERE rel.relkind = 'i';
\ No newline at end of file
diff --git a/vacuum_statistics.c b/vacuum_statistics.c
new file mode 100644
index 0000000..bba71f1
--- /dev/null
+++ b/vacuum_statistics.c
@@ -0,0 +1,636 @@
+#include "postgres.h"
+
+#include "pgstat.h"
+#include "fmgr.h"
+#include "miscadmin.h"
+#include "storage/ipc.h"
+#include "storage/lwlock.h"
+#include "storage/shmem.h"
+#include "utils/guc.h"
+#include "utils/hsearch.h"
+#include "utils/memutils.h"
+#include "common/hashfn.h"
+#include "storage/spin.h"
+#include "utils/fmgrprotos.h"
+#include "funcapi.h"
+#include "catalog/objectaccess.h"
+#include "catalog/pg_class.h"
+#include "utils/lsyscache.h"
+
+/* Public module hooks */
+void _PG_init(void);
+#ifdef PG_MODULE_MAGIC
+PG_MODULE_MAGIC;
+#endif
+
+#define SJ_NODENAME		"VacuumStatistics"
+
+/* --- GUCs --- */
+static bool evc_enabled = true;
+static int  evc_max_entries = 10000;
+
+/* --- Hook chaining --- */
+static shmem_request_hook_type prev_shmem_request_hook = NULL;
+static shmem_startup_hook_type prev_shmem_startup_hook = NULL;
+static set_report_vacuum_hook_type prev_report_vacuum_hook = NULL;
+static object_access_hook_type	prev_object_access_hook;
+
+/* --- Names --- */
+#define EVC_STATE_NAME    "extvac_shared_state"
+#define EVC_HASH_NAME     "extvac_hash"
+#define EVC_TRANCHE_NAME  "extvac_tranche"
+
+/* --- Forward declarations --- */
+static Size evc_memsize(void);
+static void evc_shmem_request(void);
+static void evc_shmem_startup(void);
+static void evc_drop_access_hook(ObjectAccessType access,
+								 Oid classId,
+								 Oid objectId,
+								 int subId,
+								 void *arg);
+
+/* ---- Key / Entry ---- */
+
+typedef struct ExtVacKey
+{
+	Oid     dboid;      /* InvalidOid for shared catalogs */
+	Oid     relid;      /* relation OID */
+	uint8   type;       /* ExtVacReportType (heap / index / …) */
+} ExtVacKey;
+
+typedef struct ExtVacEntry
+{
+	/* hash key MUST be first when using HASH_BLOBS */
+	ExtVacKey                     key;
+
+	/* stats payload */
+	PgStat_VacuumRelationCounts   stats;
+
+	/* metadata */
+	TimestampTz                  first_seen;
+	TimestampTz                  stats_reset;
+} ExtVacEntry;
+
+typedef struct ExtVacSharedState
+{
+	LWLock lock;
+	LWLock evc_lock_hash;
+
+	dsa_handle	evc_dsa_handler;
+	bool evc_changed;
+} ExtVacSharedState;
+
+static HTAB *evc_hash = NULL;
+static ExtVacSharedState *evc = NULL;
+
+static void
+pgstat_report_vacuum_extstats(Oid tableoid, bool shared,
+								  PgStat_VacuumRelationCounts *params);
+
+#define ACCUMULATE_FIELD(field) dst->field += src->field;
+
+#define ACCUMULATE_SUBFIELD(substruct, field) \
+    (dst->substruct.field += src->substruct.field)
+
+static inline void
+pgstat_accumulate_common(PgStat_CommonCounts *dst, const PgStat_CommonCounts *src)
+{
+	ACCUMULATE_FIELD(total_blks_read);
+	ACCUMULATE_FIELD(total_blks_hit);
+	ACCUMULATE_FIELD(total_blks_dirtied);
+	ACCUMULATE_FIELD(total_blks_written);
+
+	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);
+	ACCUMULATE_FIELD(total_time);
+
+	ACCUMULATE_FIELD(wraparound_failsafe_count);
+}
+
+static inline void
+pgstat_accumulate_extvac_stats_relations(PgStat_VacuumRelationCounts *dst, const PgStat_VacuumRelationCounts *src)
+{
+    if (dst->type == PGSTAT_EXTVAC_INVALID)
+        dst->type = src->type;
+
+    //Assert(src->type != PGSTAT_EXTVAC_INVALID && src->type != PGSTAT_EXTVAC_DB && src->type == dst->type);
+
+    pgstat_accumulate_common(&dst->common, &src->common);
+
+    ACCUMULATE_SUBFIELD(common, blks_fetched);
+    ACCUMULATE_SUBFIELD(common, blks_hit);
+
+    if (dst->type == PGSTAT_EXTVAC_TABLE)
+    {
+        ACCUMULATE_SUBFIELD(table, tuples_deleted);
+        ACCUMULATE_SUBFIELD(table, pages_scanned);
+        ACCUMULATE_SUBFIELD(table, pages_removed);
+        ACCUMULATE_SUBFIELD(table, vm_new_frozen_pages);
+        ACCUMULATE_SUBFIELD(table, vm_new_visible_pages);
+        ACCUMULATE_SUBFIELD(table, vm_new_visible_frozen_pages);
+        ACCUMULATE_SUBFIELD(table, tuples_frozen);
+        ACCUMULATE_SUBFIELD(table, recently_dead_tuples);
+        ACCUMULATE_SUBFIELD(table, index_vacuum_count);
+        ACCUMULATE_SUBFIELD(table, missed_dead_pages);
+        ACCUMULATE_SUBFIELD(table, missed_dead_tuples);
+    }
+    else if (dst->type == PGSTAT_EXTVAC_INDEX)
+    {
+        ACCUMULATE_SUBFIELD(table, tuples_deleted);
+        ACCUMULATE_SUBFIELD(index, pages_deleted);
+    }
+}
+
+void
+_PG_init(void)
+{
+	/*
+	 * In order to create our shared memory area, we have to be loaded via
+	 * shared_preload_libraries.  If not, fall out without hooking into any of
+	 * the main system.  (We don't throw error here because it seems useful to
+	 * allow the vacuum_statistics functions to be created even when the
+	 * module isn't active.  The functions must protect themselves against
+	 * being called then, however.)
+	 */
+	if (!process_shared_preload_libraries_in_progress)
+		return;
+
+
+	/* GUCs */
+	DefineCustomBoolVariable("vacuum_statistics.enabled",
+							 "Enable extension vacuum statistics collection.",
+							 NULL,
+							 &evc_enabled,
+							 true,
+							 PGC_SUSET, 0,
+							 NULL, NULL, NULL);
+
+	DefineCustomIntVariable("vacuum_statistics.max_entries",
+							"Maximum number of hash table entries.",
+							NULL,
+							&evc_max_entries,
+							10000,   /* default */
+							100,     /* min */
+							INT_MAX / 2, /* max */
+							PGC_POSTMASTER, 0,
+							NULL, NULL, NULL);
+
+	MarkGUCPrefixReserved(SJ_NODENAME);
+
+	/* Chain shmem hooks */
+	prev_shmem_request_hook = shmem_request_hook;
+	shmem_request_hook = evc_shmem_request;
+
+	prev_shmem_startup_hook = shmem_startup_hook;
+	shmem_startup_hook = evc_shmem_startup;
+
+	/* If you piggyback on pgstat vacuum report hook, chain it here */
+	prev_report_vacuum_hook = set_report_vacuum_hook;
+	set_report_vacuum_hook = pgstat_report_vacuum_extstats;
+
+	prev_object_access_hook	= object_access_hook;
+	object_access_hook	= evc_drop_access_hook;
+}
+
+static Size
+evc_memsize(void)
+{
+	Size sz = 0;
+
+	/* shared state header */
+	sz = MAXALIGN(sizeof(ExtVacSharedState));
+
+	/* dynahash buckets + entries */
+	sz = add_size(sz,
+		hash_estimate_size(evc_max_entries, sizeof(ExtVacEntry)));
+
+	return sz;
+}
+
+static void
+evc_shmem_request(void)
+{
+	if (prev_shmem_request_hook)
+		prev_shmem_request_hook();
+
+	/* Ask postmaster for our memory slice */
+	RequestAddinShmemSpace(evc_memsize());
+}
+
+static void
+evc_shmem_startup(void)
+{
+	HASHCTL ctl;
+	bool found;
+
+	if (prev_shmem_startup_hook)
+		prev_shmem_startup_hook();
+
+	evc = NULL;
+	evc_hash = NULL;
+
+	LWLockAcquire(AddinShmemInitLock, LW_EXCLUSIVE);
+
+	/* Shared state header */
+	evc = ShmemInitStruct(EVC_STATE_NAME,
+						  sizeof(ExtVacSharedState),
+						  &found);
+
+	/* First time only: resolve our tranche root (optional to store) */
+	if (!found)
+	{
+		evc->evc_dsa_handler = DSM_HANDLE_INVALID;
+		
+		evc->evc_changed = false;
+
+		LWLockInitialize(&evc->lock, LWLockNewTrancheId());
+		LWLockInitialize(&evc->evc_lock_hash, LWLockNewTrancheId());
+	}
+
+	/* dynahash parameters */
+	ctl.keysize = sizeof(ExtVacKey);
+	ctl.entrysize = sizeof(ExtVacEntry);
+	evc_hash = ShmemInitHash(EVC_HASH_NAME, evc_max_entries, evc_max_entries,
+							  &ctl, HASH_ELEM | HASH_BLOBS);
+
+	LWLockRelease(AddinShmemInitLock);
+
+	LWLockRegisterTranche(evc->lock.tranche, EVC_TRANCHE_NAME);
+	LWLockRegisterTranche(evc->evc_lock_hash.tranche, EVC_TRANCHE_NAME);
+
+	//if (!IsUnderPostmaster && !found)
+}
+
+static ExtVacEntry *
+evc_store(Oid dboid, Oid relid, bool shared, uint8 type,
+		  PgStat_VacuumRelationCounts *counts)
+{
+	bool		tblOverflow;
+	HASHACTION	action;
+	ExtVacEntry *e;
+	bool found = false;
+	ExtVacKey key = { .dboid = dboid, .relid = relid, .type = type };
+
+	if (!evc_enabled || evc_hash == NULL)
+		return NULL;
+
+	if (shared)
+		dboid = InvalidOid;
+
+	Assert(!LWLockHeldByMe(&evc->evc_lock_hash));
+
+	LWLockAcquire(&evc->evc_lock_hash, LW_EXCLUSIVE);
+
+	tblOverflow = hash_get_num_entries(evc_hash) < evc_max_entries ? false : true;
+
+	if (!tblOverflow)
+		action = tblOverflow ? HASH_FIND : HASH_ENTER;
+
+	e = (ExtVacEntry *) hash_search(evc_hash, &key, action, &found);
+
+	if (!found)
+	{
+		if (action == HASH_FIND)
+		{
+			/*
+			 * Hash table is full. To avoid possible problems - don't try to add
+			 * more, just exit
+			 */
+			LWLockRelease(&evc->evc_lock_hash);
+			ereport(LOG,
+				(errcode(ERRCODE_OUT_OF_MEMORY),
+				 errmsg("[Vacuum Statistics] Data storage is full. No more data can be added."),
+				 errhint("Increase value of evc_max_entries on restart of the instance")));
+			return NULL;
+		}
+
+		memset(&e->stats, 0, sizeof(e->stats));
+		e->stats.type = type;
+		e->first_seen  = GetCurrentTimestamp();
+		e->stats_reset = e->first_seen;
+	}
+	
+	pgstat_accumulate_extvac_stats_relations(&e->stats, counts);
+
+	evc->evc_changed = true;
+	LWLockRelease(&evc->evc_lock_hash);
+
+	return e;
+}
+
+static void
+pgstat_report_vacuum_extstats(Oid tableoid, bool shared,
+							  PgStat_VacuumRelationCounts *params)
+{
+	/* Call ours */
+	Oid dboid = shared ? InvalidOid : MyDatabaseId;
+	evc_store(dboid, tableoid, shared, params->type, params);
+
+	/* Chain to previous if any */
+	if (prev_report_vacuum_hook)
+		prev_report_vacuum_hook(tableoid, shared, params);
+}
+
+static void
+evc_entry_reset(ExtVacEntry *e)
+{
+    Assert(e != NULL);
+
+    LWLockAcquire(&evc->evc_lock_hash, LW_EXCLUSIVE);
+
+    /* wipe stats, but preserve type and key metadata */
+    memset(&e->stats, 0, sizeof(e->stats));
+    e->stats.type   = e->key.type;   /* relatch type */
+    e->stats_reset  = GetCurrentTimestamp();
+
+   LWLockRelease(&evc->evc_lock_hash);
+}
+
+static void
+evc_reset_by_relid(Oid dboid, Oid relid, uint8 type)
+{
+    ExtVacKey key;
+    ExtVacEntry *e;
+
+    if (!evc || !evc_hash)
+        return;
+
+    key.dboid = dboid;
+    key.relid = relid;
+    key.type  = type;
+
+    e = (ExtVacEntry *) hash_search(evc_hash, &key, HASH_FIND, NULL);
+    if (e)
+        evc_entry_reset(e);
+}
+
+PG_FUNCTION_INFO_V1(extvac_reset_entry);
+
+Datum
+extvac_reset_entry(PG_FUNCTION_ARGS)
+{
+    Oid dboid = PG_GETARG_OID(0);
+    Oid relid = PG_GETARG_OID(1);
+    int32 type = PG_GETARG_INT32(2);
+
+    evc_reset_by_relid(dboid, relid, (uint8) type);
+
+    PG_RETURN_VOID();
+}
+
+/*
+ * Object access hook
+ */
+static void
+evc_drop_access_hook(ObjectAccessType access,
+					 Oid classId,
+					 Oid objectId,
+					 int subId,
+					 void *arg)
+{
+	if (prev_object_access_hook)
+		(*prev_object_access_hook) (access, classId, objectId, subId, arg);
+
+	if (access == OAT_DROP)
+	{
+		char		relkind = get_rel_relkind(objectId);
+
+		if(classId == RelationRelationId && subId == 0)
+		{
+			if(relkind == RELKIND_RELATION)
+				evc_reset_by_relid(MyDatabaseId, objectId, PGSTAT_EXTVAC_TABLE);
+			else if (relkind == RELKIND_INDEX)
+				evc_reset_by_relid(MyDatabaseId, objectId, PGSTAT_EXTVAC_INDEX);
+		}
+	}
+}
+
+
+/* Number of output arguments (columns) of vacuum stats for various API versions */
+#define EXTVAC_COMMON_STAT_COLS 	12 /* maximum of above */
+
+static void
+tuplestore_put_common(PgStat_CommonCounts *vacuum_ext,
+                      Datum *values, bool *nulls, int *i)
+{
+    char buf[256];
+    const int base = *i;
+
+    values[(*i)++] = Int64GetDatum(vacuum_ext->total_blks_read);
+    values[(*i)++] = Int64GetDatum(vacuum_ext->total_blks_hit);
+    values[(*i)++] = Int64GetDatum(vacuum_ext->total_blks_dirtied);
+    values[(*i)++] = Int64GetDatum(vacuum_ext->total_blks_written);
+    values[(*i)++] = Int64GetDatum(vacuum_ext->wal_records);
+    values[(*i)++] = Int64GetDatum(vacuum_ext->wal_fpi);
+
+    /* Convert to numeric, like pg_stat_statements */
+    snprintf(buf, sizeof buf, UINT64_FORMAT, vacuum_ext->wal_bytes);
+    values[(*i)++] = DirectFunctionCall3(numeric_in,
+                                         CStringGetDatum(buf),
+                                         ObjectIdGetDatum(0),
+                                         Int32GetDatum(-1));
+
+    values[(*i)++] = Float8GetDatum(vacuum_ext->blk_read_time);
+    values[(*i)++] = Float8GetDatum(vacuum_ext->blk_write_time);
+    values[(*i)++] = Float8GetDatum(vacuum_ext->delay_time);
+    values[(*i)++] = Float8GetDatum(vacuum_ext->total_time);
+    values[(*i)++] = Int32GetDatum(vacuum_ext->wraparound_failsafe_count);
+
+    /* If you meant 12, fix the constant. Otherwise add the missing field. */
+    Assert((*i - base) == EXTVAC_COMMON_STAT_COLS);
+}
+
+#define EXTVAC_HEAP_STAT_COLS	26
+#define EXTVAC_IDX_STAT_COLS	17
+#define EXTVAC_MAX_STAT_COLS Max(EXTVAC_HEAP_STAT_COLS, EXTVAC_IDX_STAT_COLS)
+
+static void
+tuplestore_put_for_relation(Oid relid, Tuplestorestate *tupstore,
+			   TupleDesc tupdesc, PgStat_VacuumRelationCounts *vacuum_ext)
+{
+	Datum values[EXTVAC_MAX_STAT_COLS];
+	bool  nulls[EXTVAC_MAX_STAT_COLS];
+	int i = 0;
+	memset(nulls, 0, sizeof(nulls));
+
+	values[i++] = ObjectIdGetDatum(relid);
+
+	tuplestore_put_common(&vacuum_ext->common,
+							values, nulls, &i);
+	
+	values[i++] = Int64GetDatum(vacuum_ext->common.blks_fetched -
+								vacuum_ext->common.blks_hit);
+	values[i++] = Int64GetDatum(vacuum_ext->common.blks_hit);
+
+	if (vacuum_ext->type == PGSTAT_EXTVAC_TABLE)
+	{
+		values[i++] = Int64GetDatum(vacuum_ext->table.tuples_deleted);
+		values[i++] = Int64GetDatum(vacuum_ext->table.pages_scanned);
+		values[i++] = Int64GetDatum(vacuum_ext->table.pages_removed);
+		values[i++] = Int64GetDatum(vacuum_ext->table.vm_new_frozen_pages);
+		values[i++] = Int64GetDatum(vacuum_ext->table.vm_new_visible_pages);
+		values[i++] = Int64GetDatum(vacuum_ext->table.vm_new_visible_frozen_pages);
+		values[i++] = Int64GetDatum(vacuum_ext->table.tuples_frozen);
+		values[i++] = Int64GetDatum(vacuum_ext->table.recently_dead_tuples);
+		values[i++] = Int64GetDatum(vacuum_ext->table.index_vacuum_count);
+		values[i++] = Int64GetDatum(vacuum_ext->table.missed_dead_pages);
+		values[i++] = Int64GetDatum(vacuum_ext->table.missed_dead_tuples);
+	}
+	else if (vacuum_ext->type == PGSTAT_EXTVAC_INDEX)
+	{
+		values[i++] = Int64GetDatum(vacuum_ext->index.tuples_deleted);
+		values[i++] = Int64GetDatum(vacuum_ext->index.pages_deleted);
+	}
+
+	Assert(i == ((vacuum_ext->type == PGSTAT_EXTVAC_TABLE) ? EXTVAC_HEAP_STAT_COLS : EXTVAC_IDX_STAT_COLS));
+
+	tuplestore_putvalues(tupstore, tupdesc, values, nulls);
+}
+
+/*
+ * Get the vacuum statistics for the heap tables or indexes.
+ * See comment to pgpro_stats_statements() about SQL API.
+ */
+static Datum
+pg_stats_vacuum(FunctionCallInfo fcinfo, ExtVacReportType type)
+{
+	ReturnSetInfo		   *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
+	MemoryContext			per_query_ctx;
+	MemoryContext			oldcontext;
+	Tuplestorestate		   *tupstore;
+	TupleDesc				tupdesc;
+	Oid						dbid = PG_GETARG_OID(0);
+
+	/* Check if caller supports us returning a tuplestore */
+	if (rsinfo == NULL || !IsA(rsinfo, ReturnSetInfo))
+		ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 errmsg("vacuum statistics: set-valued function called in context that cannot accept a set")));
+	if (!(rsinfo->allowedModes & SFRM_Materialize))
+		ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 errmsg("vacuum statistics: materialize mode required, but it is not allowed in this context")));
+
+	/* Switch to long-lived context to create the returned data structures */
+	per_query_ctx = rsinfo->econtext->ecxt_per_query_memory;
+	oldcontext = MemoryContextSwitchTo(per_query_ctx);
+
+	/* Build a tuple descriptor for our result type */
+	if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
+		elog(ERROR, "vacuum statistics: return type must be a row type");
+
+	tupstore = tuplestore_begin_heap(true, false, work_mem);
+	rsinfo->returnMode = SFRM_Materialize;
+	rsinfo->setResult = tupstore;
+	rsinfo->setDesc = tupdesc;
+
+	MemoryContextSwitchTo(oldcontext);
+
+	if (type == PGSTAT_EXTVAC_INDEX || type == PGSTAT_EXTVAC_TABLE)
+	{
+		Oid	relid = PG_GETARG_OID(1);
+		ExtVacEntry *vacuum_ext;
+
+		/* Load table statistics for specified database. */
+
+		if (OidIsValid(relid))
+		{
+			ExtVacKey key;
+
+			if (!evc || !evc_hash)
+        		return (Datum) 0;
+
+			key.dboid = dbid;
+			key.relid = relid;
+			key.type  = type;
+
+			LWLockAcquire(&evc->evc_lock_hash, LW_SHARED);
+
+			vacuum_ext = (ExtVacEntry *) hash_search(evc_hash, &key, HASH_FIND, NULL);
+			
+			if (vacuum_ext == NULL || vacuum_ext->stats.type != type)
+			{	
+				/* Table don't exists or isn't an heap relation. */
+				LWLockRelease(&evc->evc_lock_hash);
+				return (Datum) 0;
+			}
+
+			LWLockRelease(&evc->evc_lock_hash);
+
+			tuplestore_put_for_relation(relid, tupstore, tupdesc, &vacuum_ext->stats);
+		}
+	}
+	// else if (type == PGSTAT_EXTVAC_DB)
+	// {
+	// 	PgStat_CommonCounts	   *vacuum_ext;
+
+	// 	vacuum_ext = fetch_dbstat_dbentry(dbid);
+
+	// 	if (vacuum_ext == NULL)
+	// 		/* Table doesn't exist or isn't a heap relation */
+	// 		PG_RETURN_NULL();
+
+	// 	Datum values[EXTVAC_COMMON_STAT_COLS];
+	// 	bool nulls[EXTVAC_COMMON_STAT_COLS];
+	// 	int i = 0;
+
+	// 	memset(nulls, 0, EXTVAC_COMMON_STAT_COLS * sizeof(bool));
+
+	// 	values[i++] = ObjectIdGetDatum(dbid);
+
+	// 	tuplestore_put_common(tupstore, tupdesc, vacuum_ext,
+	// 								&values, &nulls, &i);
+		
+	// 	tuplestore_putvalues(tupstore, tupdesc, values, nulls);
+
+	// 	return (Datum) 0;
+	// }
+
+	return (Datum) 0;
+}
+
+PG_FUNCTION_INFO_V1(pg_stats_get_vacuum_tables);
+PG_FUNCTION_INFO_V1(pg_stats_get_vacuum_indexes);
+PG_FUNCTION_INFO_V1(pg_stats_get_vacuum_database);
+
+/*
+ * Get the vacuum statistics for the heap tables.
+ */
+Datum
+pg_stats_get_vacuum_tables(PG_FUNCTION_ARGS)
+{
+	return pg_stats_vacuum(fcinfo, PGSTAT_EXTVAC_TABLE);
+
+	PG_RETURN_NULL();
+}
+
+/*
+ * Get the vacuum statistics for the indexes.
+*/
+Datum
+pg_stats_get_vacuum_indexes(PG_FUNCTION_ARGS)
+{
+	return pg_stats_vacuum(fcinfo, PGSTAT_EXTVAC_INDEX);
+
+	PG_RETURN_NULL();
+}
+
+/*
+ * Get the vacuum statistics for the databases.
+ */
+Datum
+pg_stats_get_vacuum_database(PG_FUNCTION_ARGS)
+{
+	return pg_stats_vacuum(fcinfo, PGSTAT_EXTVAC_DB);
+
+	PG_RETURN_NULL();
+}
\ No newline at end of file
diff --git a/vacuum_statistics.control b/vacuum_statistics.control
new file mode 100644
index 0000000..1c72754
--- /dev/null
+++ b/vacuum_statistics.control
@@ -0,0 +1,4 @@
+comment = 'vacuum statistics'
+default_version = '1.0'
+module_pathname = '$libdir/vacuum_statistics'
+relocatable = true
\ No newline at end of file
-- 
2.34.1

---

From: Alena Rybakina <[email-masked]>
Date: Thu, 4 Sep 2025 19:18:21 +0300

To be honest, I haven’t provided extensions for the PostgreSQL [0] to 
hackers yet, nor have I encountered this situation in general. Just in 
case, I created an open repository on GitHub with the code and added a 
description in the README.

[0] https://github.com/Alena0704/vacuum_statistics#

On 04.09.2025 18:49, Alena Rybakina wrote:
> I haven’t yet implemented writing the statistics to a file and 
> reloading them into a hash table and shared memory at instance 
> startup, and I also haven’t implemented a proper output for 
> database-level statistics yet.
>
> table keyed by a composite key - database OID, relation OID, and 
> object type (index, table, or database). When VACUUM or a worker 
> processes a table or index, an exclusive lock is taken to update the 
> corresponding record; a shared lock is taken when reading the 
> statistics. For database-level output, I plan to compute the totals by 
> summing table and index statistics on demand.
>
> `shared_preload_libraries`. I haven’t found a way to avoid that 
> because of shared-memory setup, and I’m not sure it’s even possible.
>
> extension. Note that the code added to the core to support the 
> extension executes regardless of whether the extension is enabled.

---

From: Ilia Evdokimov <[email-masked]>
Date: Mon, 15 Sep 2025 23:46:04 +0300

Hi Alena,

Thanks for the work you’ve done.

> I've rebased the patches to the current HEAD.

Right now there is a bug: when I run a simple

SELECT * FROM pg_stat_vacuum_database;

psql crashes.

The root cause is an incorrect zeroing of a local variable:
PgStat_VacuumDBCounts allzero;
- memset(&allzero, 0, sizeof(PgStat_VacuumRelationCounts));
+ memset(&allzero, 0, sizeof(PgStat_VacuumDBCounts));

--
Best regards,
Ilia Evdokimov,
Tantor Labs LLC,
https://tantorlabs.com

---

From: Bharath Rupireddy <[email-masked]>
Date: Wed, 24 Sep 2025 17:03:08 -0700

Hi,

> do we need all the statistics proposed by this patch?

Thanks for working on this. I agree with the general idea of having
minimal changes to the core. I think a simple approach would be to
have a hook in heap_vacuum_rel at the end, where vacuum stats are
prepared in a buffer for emitting LOG messages. External modules can
then handle storing, rotating, interpreting, aggregating (per
relation/per database), and exposing the stats to end-users via SQL.
The core can define a common data structure, fill it, and send it to
external modules. I haven't had a chance to read the whole thread or
review the patches; I'm sure this has been discussed.

-- 
Bharath Rupireddy

---

From: Alena Rybakina <[email-masked]>
Date: Thu, 25 Sep 2025 16:53:09 +0300

Hi! Thank you for reviewing it.

My email hasn’t been working properly recently, so I missed your letter 
— sorry about that.

Yes, I completely agree with your proposed fix and have included it in 
the updated version of the patch.

Just to remind you, this version stores vacuum statistics separately in 
the cumulative system due to the issue with disabling the gathering of 
vacuum statistics.

The patch had some conflicts with the current master branch, but I’ve 
resolved them and now it applies cleanly without conflicts.

> + memset(&allzero, 0, sizeof(PgStat_VacuumDBCounts));

From d51643eebf73cffd4cce82a04bf7db62d9007afb Mon Sep 17 00:00:00 2001
From: Alena Rybakina <[email-masked]>
Date: Thu, 25 Sep 2025 16:20:08 +0300
Subject: [PATCH 1/5] Machinery for grabbing an extended vacuum statistics on 
 table relations.

 src/backend/utils/misc/guc_parameters.dat     |   8 +-
 22 files changed, 1093 insertions(+), 17 deletions(-)

index 981d9380a92..437e5f581ae 100644

+/*

+/* ----------

 /*

 

 	if (instrument)
@@ -2967,6 +3112,7 @@ lazy_check_wraparound_failsafe(LVRelState *vacrel)
 		int64		progress_val[2] = {0, 0};

 		/*
index 7306c16f05c..a881c33b75a 100644
 
 	if (map[mapByte] & mask)
 	{
+		/*
 
 		MarkBufferDirty(vmbuf);
index c77fa0234bb..44937ca37fc 100644

 		VacuumCostBalance = 0;
 
 		/*
index 73c2ced3f4e..400fafe921b 100644

 
 
 /* ----------
 
-
 /*
 
-
 /* ------------------------------------------------------------
  * Fetching of stats
  * ------------------------------------------------------------
@@ -949,7 +947,7 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid)

 	/* if caching is desired, look up in cache */
 	if (pgstat_fetch_consistency > PGSTAT_FETCH_CONSISTENCY_NONE)
@@ -1065,7 +1063,7 @@ pgstat_snapshot_fixed(PgStat_Kind kind)
 		pgstat_clear_snapshot();

@@ -1116,7 +1114,7 @@ pgstat_prep_snapshot(void)
 }
 
 static void

 /*

 

+/*
 
 /*
  * Functions to allow construction of error message strings separately from
diff --git a/src/backend/utils/misc/guc_parameters.dat b/src/backend/utils/misc/guc_parameters.dat
index 6bc6be13d2a..a67887deffd 100644
--- a/src/backend/utils/misc/guc_parameters.dat
+++ b/src/backend/utils/misc/guc_parameters.dat
@@ -506,8 +506,14 @@
 },
 
 { name => 'track_wal_io_timing', type => 'bool', context => 'PGC_SUSET', group => 'STATS_CUMULATIVE',
+  short_desc => 'Collects vacuum statistics for vacuum activity.',
+  variable => 'pgstat_track_vacuum_statistics',
+  boot_val => 'false',
+},
+
+{ name => 'track_vacuum_statistics', type => 'bool', context => 'PGC_SUSET', group => 'STATS_CUMULATIVE',
   short_desc => 'Collects timing statistics for WAL I/O activity.',
-  variable => 'track_wal_io_timing',
+  variable => 'track_vacuum_statistics',
   boot_val => 'false',
 },
 
index c36fcb9ab61..c1f8d3f0edf 100644
@@ -662,6 +662,7 @@

 # - Monitoring -
index 01eba3b5a19..0f41f9d0658 100644
@@ -12588,4 +12588,22 @@
 
+{ oid => '8001',
 
index f402b17295c..9e37b96de92 100644
 
+/* ----------
+ *
+ * ExtVacReport
+ *

 /* ----------

 

 /* ------

 /*

 /*----------
index 5afae33d370..645909b1969 100644
@@ -99,6 +99,7 @@ test: timeouts

From 604e852929d7b9c1bf7fe3d58b8bbc0db3e12caa Mon Sep 17 00:00:00 2001
From: Alena Rybakina <[email-masked]>
Date: Thu, 25 Sep 2025 16:22:38 +0300
Subject: [PATCH 2/5] Machinery for grabbing an extended vacuum statistics on 
 index relations.

 14 files changed, 863 insertions(+), 89 deletions(-)

index 437e5f581ae..07ad337dc82 100644

 
-/*

 

 
-	report->blks_fetched =
 
+void
 
 /*

 

 	if (instrument)
@@ -2782,10 +2895,20 @@ lazy_vacuum_all_indexes(LVRelState *vacrel)
 	}
 	else
 	{
 
@@ -3195,10 +3318,20 @@ lazy_cleanup_all_indexes(LVRelState *vacrel)
 
@@ -3224,6 +3357,11 @@ lazy_vacuum_one_index(Relation indrel, IndexBulkDeleteResult *istat,
 
 	ivinfo.index = indrel;
@@ -3242,6 +3380,7 @@ lazy_vacuum_one_index(Relation indrel, IndexBulkDeleteResult *istat,
@@ -3250,6 +3389,19 @@ lazy_vacuum_one_index(Relation indrel, IndexBulkDeleteResult *istat,
 
@@ -3274,6 +3426,11 @@ lazy_cleanup_one_index(Relation indrel, IndexBulkDeleteResult *istat,
 
 	ivinfo.index = indrel;
@@ -3293,12 +3450,25 @@ lazy_cleanup_one_index(Relation indrel, IndexBulkDeleteResult *istat,

index 44937ca37fc..4bbd1499bb8 100644
 
 	/*

index 400fafe921b..fd2c5e15369 100644
@@ -1159,6 +1159,10 @@ pgstat_build_snapshot(PgStat_Kind statKind)
 		if (p->dropped)
 			continue;

 

 

index 0f41f9d0658..2a8b6b2c1b8 100644
@@ -12606,4 +12606,13 @@
 
 /*
 
+/*
 
index 9e37b96de92..1f1402d4179 100644

 
 /* ----------

 step s1_commit: COMMIT;

From 1aa51ec901cbf46e53137fa099b5474ebca5b614 Mon Sep 17 00:00:00 2001
From: Alena Rybakina <[email-masked]>
Date: Mon, 1 Sep 2025 21:43:33 +0300
Subject: [PATCH 3/5] Machinery for grabbing an extended vacuum statistics on
 databases.

 15 files changed, 380 insertions(+), 34 deletions(-)

index 07ad337dc82..aac65933e1a 100644
 
 	extVacStats->blk_read_time -= vacrel->extVacReportIdx.blk_read_time;
 	extVacStats->blk_write_time -= vacrel->extVacReportIdx.blk_write_time;
@@ -4081,6 +4081,9 @@ vacuum_error_callback(void *arg)
 	switch (errinfo->phase)
 	{
 		case VACUUM_ERRCB_PHASE_SCAN_HEAP:
+			if(geterrelevel() == ERROR)
+					pgstat_report_vacuum_error(errinfo->reloid, PGSTAT_EXTVAC_TABLE);
+
 			if (BlockNumberIsValid(errinfo->blkno))
 			{
 				if (OffsetNumberIsValid(errinfo->offnum))
@@ -4096,6 +4099,9 @@ vacuum_error_callback(void *arg)
 			break;
 
@@ -4111,16 +4117,25 @@ vacuum_error_callback(void *arg)
 			break;

 
index 4bbd1499bb8..1c5e351a672 100644
index fd2c5e15369..6cb9077a27f 100644

 /* ----------

 	return true;
 }
 
+/* ---------
 
 /*

 

index 2a8b6b2c1b8..49c3c16ce5b 100644
@@ -12607,12 +12607,21 @@
index 1f1402d4179..6952f833d45 100644

+	int32		errors;
 
 	/* ----------
 
 /*
 
 session s1
+setup		{
 
 session s2
+setup		{

 DROP TABLE vestat;

 

 
 
 DROP TABLE vestat;

 

 

From 0084f46c8b4a8bfe8d81f54747c305e48753a920 Mon Sep 17 00:00:00 2001

---

index aac65933e1a..57cd144f77a 100644

 
 
 void

 	if (stats != NULL)

 
 
 }
 
 static void

 

 

 	/*

-	}
-	else
-	{

@@ -2896,9 +2886,9 @@ lazy_vacuum_all_indexes(LVRelState *vacrel)
 	else
 	{
 		LVExtStatCounters counters;
-		ExtVacReport extVacReport;
+		PgStat_VacuumRelationCounts extVacReport;

 
@@ -2906,7 +2896,7 @@ lazy_vacuum_all_indexes(LVRelState *vacrel)
 		parallel_vacuum_bulkdel_all_indexes(vacrel->pvs, old_live_tuples,
 											vacrel->num_index_scans);

 		/*
@@ -3319,9 +3309,9 @@ lazy_cleanup_all_indexes(LVRelState *vacrel)
 	else
 	{
 		LVExtStatCounters counters;
-		ExtVacReport extVacReport;
+		PgStat_VacuumRelationCounts extVacReport;

 
@@ -3330,7 +3320,7 @@ lazy_cleanup_all_indexes(LVRelState *vacrel)
 											vacrel->num_index_scans,
 											estimated_count);

@@ -3358,7 +3348,10 @@ lazy_vacuum_one_index(Relation indrel, IndexBulkDeleteResult *istat,
 	IndexVacuumInfo ivinfo;
 
@@ -3389,18 +3382,13 @@ lazy_vacuum_one_index(Relation indrel, IndexBulkDeleteResult *istat,

 
 
@@ -3427,7 +3415,10 @@ lazy_cleanup_one_index(Relation indrel, IndexBulkDeleteResult *istat,
 
 	/* Set initial statistics values to gather vacuum statistics for the index */
 	extvac_stats_start_idx(indrel, istat, &extVacCounters);
@@ -3457,17 +3448,13 @@ lazy_cleanup_one_index(Relation indrel, IndexBulkDeleteResult *istat,

 
 	/* Revert to the previous phase information for error traceback */
 	restore_vacuum_error_info(vacrel, &saved_err_info);
@@ -4067,6 +4054,27 @@ update_relstats_all_indexes(LVRelState *vacrel)
 	}
 }
 
+/* ---------
@@ -4082,7 +4090,7 @@ vacuum_error_callback(void *arg)
 
 			if (BlockNumberIsValid(errinfo->blkno))
 			{
@@ -4100,7 +4108,7 @@ vacuum_error_callback(void *arg)

 			if (BlockNumberIsValid(errinfo->blkno))
 			{
@@ -4118,7 +4126,7 @@ vacuum_error_callback(void *arg)

 

 

 	/*
index 5d9db167e59..7def7c13b15 100644

 	/*
index 1c5e351a672..28bbf11cf97 100644

 

 

 
 
 	/*
 
 	/*

 	/*
index 6cb9077a27f..0fc16a58210 100644
 
 /*
 
+/*

 	return true;
 }

 /*
 
-/* ---------

 /*
 
+void
index 1c39ada2c3e..68a99d5db97 100644
 
-
 /*

 

 	i = 0;

 

 

 

 

 
 	i = 0;

 

 

 

 

 

 
 
index 6952f833d45..475ce581674 100644
 
 /* ----------

 
-	/* Time stats. */

-	int32		errors;

 
-/* ----------
 
-	/*
 
 /* ----------

 

 /* ------

 /*

index bf75ebcef31..628e65e4a5d 100644
@@ -447,6 +447,18 @@ typedef struct PgStatShared_Relation
 
@@ -615,6 +627,9 @@ extern PgStat_EntryRef *pgstat_fetch_pending_entry(PgStat_Kind kind,

 /*

 /* Custom stats kinds */

 SELECT relpages AS irp

 

 
 DROP TABLE vestat;

 

 DELETE FROM vestat;;

 

 

 
 
 DELETE FROM vestat;
 TRUNCATE vestat;

 
 DROP TABLE vestat;

From 4f937350e6f570e9a576abdbc7698b350f4d0288 Mon Sep 17 00:00:00 2001

---

---

From: Alena Rybakina <[email-masked]>
Date: Thu, 25 Sep 2025 18:10:12 +0300

Hi all,

I’ve prepared an extension that adds vacuum statistics [0] (master 
branch), and it’s working stably. The attached patch is a core patch 
that enables this extension to work.

Right now, I’m experimenting with a core patch. Specifically, in 
load_file I can detect whether vacuum_statistics is listed in 
shared_preload_libraries and, if so, start collecting vacuum statistics 
in the core.
However, I think it would be more reliable to simply add a dedicated 
hook for vacuum statistics collection in the core. In my view, an 
extension may be loaded but disabled for vacuum statistics collection - 
and in that case we shouldn’t gather them.

In general, I’m not entirely happy with the current organization. One 
issue that bothers me is having to scan the entire hash table to provide 
vacuum statistics for a database, with aggregation.

At the moment, the hash table uses (dboid, reloid, type) as the key.
This could be improved by introducing another hash table keyed by dboid, 
with entries containing arrays of the first table’s keys (dboid, reloid, 
type) (where dboid is either kept or omitted).
The idea is that we find the relevant array for a given database and 
then aggregate its statistics by iterating over the first table using 
those keys. I’ve started implementing this approach in the main branch 
of the same repository, but
I’m still working out the issues with dynamic memory management.

I also have an idea for effectively splitting statistics into “core” and 
“extra.”

Core statistics:
For databases (also collected for tables and indexes): delay_time, 
total_time
For tables: pages_scanned, pages_removed, tuples_deleted, 
vm_new_frozen_pages, vm_new_visible_pages
For indexes: tuples_deleted, pages_deleted

Extra statistics:
For databases (also collected for tables and indexes): total_blks_read, 
total_blks_dirtied, total_blks_written, blks_fetched, blks_hit, 
blk_read_time, blk_write_time
For tables: recently_dead_tuples, missed_dead_tuples, 
vm_new_visible_frozen_pages, missed_dead_pages, tuples_frozen

WAL statistics (separately for databases and relations):wal_records, 
wal_fpi, wal_bytes

I’ve already started drafting the first implementation, but I still need 
to carefully handle memory allocation.

Additionally, I’m considering letting users define which databases, 
schemas, or tables/relations should have vacuum statistics collected. I 
believe this could be valuable for large, high-load systems.
For example, the core statistics might show that a particular database 
is frequently vacuumed - so we could then focus on tracking only that 
one. Similarly, if certain tables are heavily updated by backends and
vacuumed often, we could target those specifically. Conceptually, this 
would act like a filter, but at this point, it’s just an idea for a 
future improvement.

This is the direction I’m planning to take with the patch. If you have 
alternative ideas about how to organize the code, I’d be glad to hear them!

> review the patches; I'm sure this has been discussed.
As for how this may help databases in practice, I think it deserves a 
separate thread once the vacuum statistics patch is pushed.

In short, such statistics are essential to understand the real impact of 
vacuum on system load.

For example:
If vacuum runs very frequently on a table or index, this might point to 
table or index bloat, or to overly aggressive configuration.
Conversely, if vacuum freezes or removes very few tuples, it may suggest 
that vacuum is not aggressive enough, or that delays are set too high.
If missed_dead_pages and missed_dead_tuples are high compared to 
tuples_deleted, that may indicate vacuum can’t obtain a INDEX CLEANUP 
LOCK or doesn’t retry due to long delays.
Statistics related to wraparound activity can also hint that autovacuum 
settings require adjustment.

It’s also possible that this system could be made more automatic in the 
future, but I haven’t fully worked out how yet. I think that discussion 
belongs in a separate thread once vacuum statistics themselves are 
committed.

[0] https://github.com/Alena0704/vacuum_statistics/tree/master

-------------

Best regards,
Alena Rybakina
Postgres Professional

From 81eff11a045d15a45f41805aea5ac36438acafa6 Mon Sep 17 00:00:00 2001
From: Alena Rybakina <[email-masked]>
Date: Thu, 4 Sep 2025 18:16:52 +0300
Subject: Core patch.

---
 src/backend/access/heap/vacuumlazy.c         | 308 ++++++++++++++++++-
 src/backend/access/heap/visibilitymap.c      |  10 +
 src/backend/catalog/system_views.sql         |   4 +-
 src/backend/commands/vacuum.c                |   4 +
 src/backend/commands/vacuumparallel.c        |  12 +
 src/backend/utils/activity/pgstat_relation.c |  36 ++-
 src/backend/utils/adt/pgstatfuncs.c          |   6 +
 src/backend/utils/error/elog.c               |  13 +
 src/include/catalog/pg_proc.dat              |   8 +
 src/include/commands/vacuum.h                |  26 ++
 src/include/pgstat.h                         | 118 ++++++-
 src/include/utils/elog.h                     |   1 +
 src/test/regress/expected/rules.out          |  12 +-
 13 files changed, 546 insertions(+), 12 deletions(-)

index 932701d8420..a56cb0222fa 100644

@@ -474,6 +480,208 @@ static void update_vacuum_error_info(LVRelState *vacrel,
 static void restore_vacuum_error_info(LVRelState *vacrel,
 									  const LVSavedErrInfo *saved_vacrel);
 
+/* ----------

 /*
@@ -632,6 +840,13 @@ heap_vacuum_rel(Relation rel, const VacuumParams params,
 
 	verbose = (params.options & VACOPT_VERBOSE) != 0;
 	instrument = (verbose || (AmAutoVacuumWorkerProcess() &&
@@ -652,6 +867,7 @@ heap_vacuum_rel(Relation rel, const VacuumParams params,
 	pgstat_progress_start_command(PROGRESS_COMMAND_VACUUM,
 								  RelationGetRelid(rel));
 
@@ -668,6 +884,7 @@ heap_vacuum_rel(Relation rel, const VacuumParams params,
@@ -676,6 +893,8 @@ heap_vacuum_rel(Relation rel, const VacuumParams params,
 
@@ -776,6 +995,7 @@ heap_vacuum_rel(Relation rel, const VacuumParams params,
 
@@ -924,6 +1144,9 @@ heap_vacuum_rel(Relation rel, const VacuumParams params,
 
@@ -934,12 +1157,20 @@ heap_vacuum_rel(Relation rel, const VacuumParams params,
 
 	if (instrument)
@@ -2631,10 +2862,20 @@ lazy_vacuum_all_indexes(LVRelState *vacrel)
 	}
 	else
 	{
 
@@ -2961,6 +3202,7 @@ lazy_check_wraparound_failsafe(LVRelState *vacrel)

 		/*
@@ -3043,10 +3285,20 @@ lazy_cleanup_all_indexes(LVRelState *vacrel)
 
@@ -3072,6 +3324,11 @@ lazy_vacuum_one_index(Relation indrel, IndexBulkDeleteResult *istat,
 
 	ivinfo.index = indrel;
@@ -3090,6 +3347,7 @@ lazy_vacuum_one_index(Relation indrel, IndexBulkDeleteResult *istat,
@@ -3098,6 +3356,16 @@ lazy_vacuum_one_index(Relation indrel, IndexBulkDeleteResult *istat,
 
@@ -3122,6 +3390,11 @@ lazy_cleanup_one_index(Relation indrel, IndexBulkDeleteResult *istat,
 
 	ivinfo.index = indrel;
@@ -3141,12 +3414,22 @@ lazy_cleanup_one_index(Relation indrel, IndexBulkDeleteResult *istat,

@@ -3759,6 +4042,9 @@ vacuum_error_callback(void *arg)
@@ -3774,6 +4060,9 @@ vacuum_error_callback(void *arg)
 			break;
 
@@ -3789,16 +4078,25 @@ vacuum_error_callback(void *arg)
 			break;

 
 
 	if (map[mapByte] & mask)
 	{
+		/*
 
 		MarkBufferDirty(vmbuf);
index c77fa0234bb..b3242dc6f5a 100644
+            pg_stat_get_rev_all_frozen_pages(C.oid) as rev_all_frozen_pages,
+            pg_stat_get_rev_all_visible_pages(C.oid) as rev_all_visible_pages

 		VacuumCostBalance = 0;
 
 		/*
 
 	/*

 
+/* ---------

 
 
+/*
 
 /*

 /*
 
+/*

index f402b17295c..15da1f27654 100644
@@ -100,6 +100,97 @@ typedef struct PgStat_FunctionCallUsage
 
+
@@ -153,6 +244,9 @@ typedef struct PgStat_TableCounts

 /* ----------
@@ -211,7 +305,7 @@ typedef struct PgStat_TableXactStatus
  * ------------------------------------------------------------
  */

@@ -453,6 +547,9 @@ typedef struct PgStat_StatTabEntry
 
 /* ------
@@ -660,10 +757,11 @@ extern void pgstat_unlink_relation(Relation rel);

 /*
@@ -711,6 +809,17 @@ extern void pgstat_report_analyze(Relation rel,
 
@@ -838,4 +947,9 @@ extern PGDLLIMPORT PgStat_Counter pgStatTransactionIdleTime;

 
 /*----------

---

From: Alena Rybakina <[email-masked]>
Date: Sun, 21 Dec 2025 02:36:42 +0300

Hi,
I’ve added some changes to one of the approaches and also did additional 
cleanup and stabilization work on the vacuum statistics tests. Specifically:

  * I moved the vacuum statistics tests into the tests tab and made them
    more stable. For slower machines, vacuum is now triggered inside the
    statistics wait function. Previously, some backends didn’t have
    enough time to release the lock, which could lead to differences
    because the vacuum hadn’t fully completed yet.
  * I also ran the backend tests and fixed a couple of minor issues
    along the way.
  * I ran pgindent to clean up and normalize the formatting.

For now, I’ve temporarily removed collecting statistics related to 
database-level errors when vacuum is forced to stop. I’m currently stuck 
on how to properly expose statistics for cluster-level objects, since 
their dbid is 0.

At the moment, only the second test still looks odd, and I haven’t fully 
figured out why yet. It seems like aggressive vacuum can no longer be 
triggered the same way as before with the current gucs, but I’m still 
investigating this.

Best regards,
Alena Rybakina
From f96d5079774fe129fff32761bba4ab9089e491bd Mon Sep 17 00:00:00 2001
From: Alena Rybakina <[email-masked]>
Date: Tue, 9 Dec 2025 09:56:34 +0300
Subject: [PATCH 1/5] Machinery for grabbing an extended vacuum statistics on
 table relations.

Authors: Alena Rybakina <[email-masked]>,
	 Andrei Lepikhov <[email-masked]>,
	 Andrei Zubkov <[email-masked]>
Reviewed-by: Dilip Kumar <[email-masked]>, Masahiko Sawada <[email-masked]>,
	     Ilia Evdokimov <[email-masked]>, jian he <[email-masked]>,
	     Kirill Reshke <[email-masked]>, Alexander Korotkov <[email-masked]>,
	     Jim Nasby <[email-masked]>, Sami Imseih <[email-masked]>,
	     Karina Litskevich <[email-masked]>
---
 src/backend/access/heap/vacuumlazy.c          | 145 ++++-
 src/backend/access/heap/visibilitymap.c       |  10 +
 src/backend/catalog/system_views.sql          |  52 +-
 src/backend/commands/vacuum.c                 |   4 +
 src/backend/commands/vacuumparallel.c         |   1 +
 src/backend/utils/activity/pgstat_relation.c  |  46 +-
 src/backend/utils/adt/pgstatfuncs.c           |  86 +++
 src/backend/utils/misc/postgresql.conf.sample |   1 +
 src/include/catalog/pg_proc.dat               |  18 +
 src/include/commands/vacuum.h                 |   1 +
 src/include/pgstat.h                          |  92 ++-
 .../vacuum-extending-in-repetable-read.out    |  53 ++
 src/test/isolation/isolation_schedule         |   1 +
 .../vacuum-extending-in-repetable-read.spec   |  53 ++
 .../t/050_vacuum_extending_basic_test.pl      | 571 ++++++++++++++++++
 .../t/051_vacuum_extending_freeze_test.pl     | 395 ++++++++++++
 src/test/regress/expected/rules.out           |  44 +-
 src/test/regress/parallel_schedule            |   2 +-
 18 files changed, 1565 insertions(+), 10 deletions(-)
 create mode 100644 src/test/isolation/expected/vacuum-extending-in-repetable-read.out
 create mode 100644 src/test/isolation/specs/vacuum-extending-in-repetable-read.spec
 create mode 100644 src/test/recovery/t/050_vacuum_extending_basic_test.pl
 create mode 100644 src/test/recovery/t/051_vacuum_extending_freeze_test.pl

diff --git a/src/backend/access/heap/vacuumlazy.c b/src/backend/access/heap/vacuumlazy.c
index 30778a15639..66e09d0a0cf 100644
--- a/src/backend/access/heap/vacuumlazy.c
+++ b/src/backend/access/heap/vacuumlazy.c
@@ -289,6 +289,7 @@ typedef struct LVRelState
 	/* Error reporting state */
 	char	   *dbname;
 	char	   *relnamespace;
+	Oid			reloid;
 	char	   *relname;
 	char	   *indname;		/* Current index name */
 	BlockNumber blkno;			/* used only for heap operations */
@@ -407,6 +408,10 @@ typedef struct LVRelState
 	 * been permanently disabled.
 	 */
 	BlockNumber eager_scan_remaining_fails;
+
+	int32		wraparound_failsafe_count;	/* number of emergency vacuums to
+											 * prevent anti-wraparound
+											 * shutdown */
 } LVRelState;

@@ -418,6 +423,18 @@ typedef struct LVSavedErrInfo
 	VacErrPhase phase;
 } LVSavedErrInfo;
 
+/*
 
@@ -487,6 +504,102 @@ static void update_vacuum_error_info(LVRelState *vacrel,
 
+/* ----------
+extvac_stats_start(Relation rel, LVExtStatCounters * counters)
+extvac_stats_end(Relation rel, LVExtStatCounters * counters,
+				 ExtVacReport * report)

 /*
@@ -645,6 +758,13 @@ heap_vacuum_rel(Relation rel, const VacuumParams params,
 
 	verbose = (params.options & VACOPT_VERBOSE) != 0;
 	instrument = (verbose || (AmAutoVacuumWorkerProcess() &&
@@ -673,6 +793,8 @@ heap_vacuum_rel(Relation rel, const VacuumParams params,
 		pgstat_progress_update_param(PROGRESS_VACUUM_STARTED_BY,
 									 PROGRESS_VACUUM_STARTED_BY_MANUAL);
 
@@ -689,6 +811,7 @@ heap_vacuum_rel(Relation rel, const VacuumParams params,
@@ -797,6 +920,7 @@ heap_vacuum_rel(Relation rel, const VacuumParams params,
 
@@ -951,6 +1075,23 @@ heap_vacuum_rel(Relation rel, const VacuumParams params,
 
@@ -965,7 +1106,8 @@ heap_vacuum_rel(Relation rel, const VacuumParams params,
 
 	if (instrument)
@@ -3019,6 +3161,7 @@ lazy_check_wraparound_failsafe(LVRelState *vacrel)
 		int64		progress_val[3] = {0, 0, PROGRESS_VACUUM_MODE_FAILSAFE};
 
 		VacuumFailsafeActive = true;
+		vacrel->wraparound_failsafe_count++;
 
 		/*
index d14588e92ae..3030242d98e 100644
@@ -92,6 +92,7 @@
@@ -161,6 +162,15 @@ visibilitymap_clear(Relation rel, BlockNumber heapBlk, Buffer vmbuf, uint8 flags
 
 	if (map[mapByte] & mask)
 	{
+		/*
 
 		MarkBufferDirty(vmbuf);
index 0a0f95f6bb9..ffb407d414f 100644
@@ -727,7 +727,9 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_total_autoanalyze_time(C.oid) AS total_autoanalyze_time,
-            pg_stat_get_stat_reset_time(C.oid) AS stats_reset
+            pg_stat_get_stat_reset_time(C.oid) AS stats_reset,
@@ -1452,3 +1454,51 @@ REVOKE ALL ON pg_aios FROM PUBLIC;
index 0528d1b6ecb..dd519447387 100644
@@ -117,6 +117,9 @@ pg_atomic_uint32 *VacuumSharedCostBalance = NULL;
 
@@ -2536,6 +2539,7 @@ vacuum_delay_point(bool is_analyze)
 			exit(1);
 
 		VacuumCostBalance = 0;
 
 		/*
index 8a37c08871a..114cd7c31d3 100644
index 55a10c299db..361713479e8 100644
+static void pgstat_accumulate_extvac_stats(ExtVacReport * dst, ExtVacReport * src,

 /*
@@ -208,7 +210,7 @@ pgstat_drop_relation(Relation rel)
  */
 void
 pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
-					 PgStat_Counter deadtuples, TimestampTz starttime)
+					 PgStat_Counter deadtuples, TimestampTz starttime, ExtVacReport * params)
 {
 	PgStat_EntryRef *entry_ref;
 	PgStatShared_Relation *shtabentry;
@@ -234,6 +236,8 @@ pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 	tabentry->live_tuples = livetuples;
 	tabentry->dead_tuples = deadtuples;
 
@@ -880,6 +884,9 @@ pgstat_relation_flush_cb(PgStat_EntryRef *entry_ref, bool nowait)
 
@@ -1009,3 +1016,40 @@ restore_truncdrop_counters(PgStat_TableXactStatus *trans)
+pgstat_accumulate_extvac_stats(ExtVacReport * dst, ExtVacReport * src,
index ef6fffe60b9..d7dfda0c1a7 100644
 
@@ -2307,3 +2313,83 @@ pg_stat_have_stats(PG_FUNCTION_ARGS)
 
+#define PG_STAT_GET_VACUUM_TABLES_STATS_COLS 26
+	if (!tabentry)
+		InitMaterializedSRF(fcinfo, 0);
index dc9e2255f8a..867638fe74b 100644
@@ -669,6 +669,7 @@

 # - Monitoring -
index fd9448ec7b9..915a5a7822f 100644
@@ -12612,4 +12612,22 @@
 
+{ oid => '8001',
index 1f3290c7fbf..6b997bc7fb1 100644
@@ -332,6 +332,7 @@ extern PGDLLIMPORT double vacuum_max_eager_freeze_failure_rate;
 
 extern PGDLLIMPORT bool VacuumFailsafeActive;
 extern PGDLLIMPORT double vacuum_cost_delay;
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 6714363144a..46d12fa3bd0 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -114,6 +114,66 @@ typedef struct PgStat_BackendSubEntry
 	PgStat_Counter conflict_count[CONFLICT_NUM_TYPES];
 } PgStat_BackendSubEntry;
 
+/* ----------
+ *
+ * ExtVacReport
+ *
+ * Additional statistics of vacuum processing over a heap relation.
+ * pages_removed is the amount by which the physically shrank,
+ * if any (ie the change in its total size on disk)
+ * pages_deleted refer to free space within the index file
+ * ----------
+ */
+typedef struct ExtVacReport
+{
+	/*
+	 * number of blocks missed, hit, dirtied and written during a vacuum of
+	 * specific relation
+	 */
+	int64		total_blks_read;
+	int64		total_blks_hit;
+	int64		total_blks_dirtied;
+	int64		total_blks_written;
+
+	/*
+	 * blocks missed and hit for just the heap during a vacuum of specific
+	 * relation
+	 */
+	int64		blks_fetched;
+	int64		blks_hit;
+
+	/* Vacuum WAL usage stats */
+	int64		wal_records;	/* wal usage: number of WAL records */
+	int64		wal_fpi;		/* wal usage: number of WAL full page images
+								 * produced */
+	uint64		wal_bytes;		/* wal usage: size of WAL records produced */
+
+	/* Time stats. */
+	double		blk_read_time;	/* time spent reading pages, in msec */
+	double		blk_write_time; /* time spent writing pages, in msec */
+	double		delay_time;		/* how long vacuum slept in vacuum delay
+								 * point, in msec */
+	double		total_time;		/* total time of a vacuum operation, in msec */
+
+	int64		pages_scanned;	/* heap pages examined (not skipped by VM) */
+	int64		pages_removed;	/* heap pages removed by vacuum "truncation" */
+	int64		vm_new_frozen_pages;	/* pages marked in VM as frozen */
+	int64		vm_new_visible_pages;	/* pages marked in VM as all-visible */
+	int64		vm_new_visible_frozen_pages;	/* pages marked in VM as
+												 * all-visible and frozen */
+	int64		missed_dead_tuples; /* tuples not pruned by vacuum due to
+									 * failure to get a cleanup lock */
+	int64		missed_dead_pages;	/* pages with missed dead tuples */
+	int64		tuples_deleted; /* tuples deleted by vacuum */
+	int64		tuples_frozen;	/* tuples frozen up by vacuum */
+	int64		recently_dead_tuples;	/* deleted tuples that are still
+										 * visible to some transaction */
+	int64		index_vacuum_count; /* the number of index vacuumings */
+	int32		wraparound_failsafe_count;	/* number of emergency vacuums to
+											 * prevent anti-wraparound
+											 * shutdown */
+}			ExtVacReport;
+
 /* ----------
  * PgStat_TableCounts			The actual per-table counts kept by a backend
  *
@@ -156,6 +216,15 @@ typedef struct PgStat_TableCounts
 
 	PgStat_Counter blocks_fetched;
 	PgStat_Counter blocks_hit;
+
+	PgStat_Counter rev_all_visible_pages;
+	PgStat_Counter rev_all_frozen_pages;
+
+	/*
+	 * Additional cumulative stat on vacuum operations. Use an expensive
+	 * structure as an abstraction for different types of relations.
+	 */
+	ExtVacReport vacuum_ext;
 } PgStat_TableCounts;
 
 /* ----------
@@ -214,7 +283,7 @@ typedef struct PgStat_TableXactStatus
  * ------------------------------------------------------------
  */
 
-#define PGSTAT_FILE_FORMAT_ID	0x01A5BCBB
+#define PGSTAT_FILE_FORMAT_ID	0x01A5BCBC
 
 typedef struct PgStat_ArchiverStats
 {
@@ -378,6 +447,8 @@ typedef struct PgStat_StatDBEntry
 	PgStat_Counter parallel_workers_launched;

 typedef struct PgStat_StatFuncEntry
@@ -461,8 +532,12 @@ typedef struct PgStat_StatTabEntry
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
 	PgStat_Counter total_autoanalyze_time;
-
 	TimestampTz stat_reset_time;
+
+	PgStat_Counter rev_all_visible_pages;
+	PgStat_Counter rev_all_frozen_pages;
+
+	ExtVacReport vacuum_ext;
 } PgStat_StatTabEntry;
 
 /* ------
@@ -671,7 +746,7 @@ extern void pgstat_unlink_relation(Relation rel);
 
 extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 								 PgStat_Counter deadtuples,
-								 TimestampTz starttime);
+								 TimestampTz starttime, ExtVacReport * params);
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
@@ -722,6 +797,17 @@ extern void pgstat_report_analyze(Relation rel,
 		if (pgstat_should_count_relation(rel))						\
 			(rel)->pgstat_info->counts.blocks_hit++;				\
 	} while (0)
+/* accumulate unfrozen all-visible and all-frozen pages */
+#define pgstat_count_vm_rev_all_visible(rel)						\
+	do {															\
+		if (pgstat_should_count_relation(rel))						\
+			(rel)->pgstat_info->counts.rev_all_visible_pages++;	\
+	} while (0)
+#define pgstat_count_vm_rev_all_frozen(rel)						\
+	do {															\
+		if (pgstat_should_count_relation(rel))						\
+			(rel)->pgstat_info->counts.rev_all_frozen_pages++;	\
+	} while (0)
 
 extern void pgstat_count_heap_insert(Relation rel, PgStat_Counter n);
 extern void pgstat_count_heap_update(Relation rel, bool hot, bool newpage);
diff --git a/src/test/isolation/expected/vacuum-extending-in-repetable-read.out b/src/test/isolation/expected/vacuum-extending-in-repetable-read.out
new file mode 100644
index 00000000000..87f7e40b4a6
--- /dev/null
+++ b/src/test/isolation/expected/vacuum-extending-in-repetable-read.out
@@ -0,0 +1,53 @@
+unused step name: s2_delete
+Parsed test spec with 2 sessions
+
+starting permutation: s2_insert s2_print_vacuum_stats_table s1_begin_repeatable_read s2_update s2_insert_interrupt s2_vacuum s2_print_vacuum_stats_table s1_commit s2_checkpoint s2_vacuum s2_print_vacuum_stats_table
+step s2_insert: INSERT INTO test_vacuum_stat_isolation(id, ival) SELECT ival, ival%10 FROM generate_series(1,1000) As ival;
+step s2_print_vacuum_stats_table: 
+    SELECT
+    vt.relname, vt.tuples_deleted, vt.recently_dead_tuples, vt.missed_dead_tuples, vt.missed_dead_pages, vt.tuples_frozen
+    FROM pg_stat_vacuum_tables vt, pg_class c
+    WHERE vt.relname = 'test_vacuum_stat_isolation' AND vt.relid = c.oid;
+
+relname                   |tuples_deleted|recently_dead_tuples|missed_dead_tuples|missed_dead_pages|tuples_frozen
+--------------------------+--------------+--------------------+------------------+-----------------+-------------
+test_vacuum_stat_isolation|             0|                   0|                 0|                0|            0
+(1 row)
+
+step s1_begin_repeatable_read: 
+  BEGIN transaction ISOLATION LEVEL REPEATABLE READ;
+  select count(ival) from test_vacuum_stat_isolation where id>900;
+
+count
+-----
+  100
+(1 row)
+
+step s2_update: UPDATE test_vacuum_stat_isolation SET ival = ival + 2 where id > 900;
+step s2_insert_interrupt: INSERT INTO test_vacuum_stat_isolation values (1,1);
+step s2_vacuum: VACUUM test_vacuum_stat_isolation;
+step s2_print_vacuum_stats_table: 
+    SELECT
+    vt.relname, vt.tuples_deleted, vt.recently_dead_tuples, vt.missed_dead_tuples, vt.missed_dead_pages, vt.tuples_frozen
+    FROM pg_stat_vacuum_tables vt, pg_class c
+    WHERE vt.relname = 'test_vacuum_stat_isolation' AND vt.relid = c.oid;
+
+relname                   |tuples_deleted|recently_dead_tuples|missed_dead_tuples|missed_dead_pages|tuples_frozen
+--------------------------+--------------+--------------------+------------------+-----------------+-------------
+test_vacuum_stat_isolation|             0|                 100|                 0|                0|            0
+(1 row)
+
+step s1_commit: COMMIT;
+step s2_checkpoint: CHECKPOINT;
+step s2_vacuum: VACUUM test_vacuum_stat_isolation;
+step s2_print_vacuum_stats_table: 
+    SELECT
+    vt.relname, vt.tuples_deleted, vt.recently_dead_tuples, vt.missed_dead_tuples, vt.missed_dead_pages, vt.tuples_frozen
+    FROM pg_stat_vacuum_tables vt, pg_class c
+    WHERE vt.relname = 'test_vacuum_stat_isolation' AND vt.relid = c.oid;
+
+relname                   |tuples_deleted|recently_dead_tuples|missed_dead_tuples|missed_dead_pages|tuples_frozen
+--------------------------+--------------+--------------------+------------------+-----------------+-------------
+test_vacuum_stat_isolation|           100|                 100|                 0|                0|          101
+(1 row)
+
diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule
index f2e067b1fbc..1c231418706 100644
--- a/src/test/isolation/isolation_schedule
+++ b/src/test/isolation/isolation_schedule
@@ -98,6 +98,7 @@ test: timeouts
 test: vacuum-concurrent-drop
 test: vacuum-conflict
 test: vacuum-skip-locked
+test: vacuum-extending-in-repetable-read
 test: stats
 test: horizons
 test: predicate-hash
diff --git a/src/test/isolation/specs/vacuum-extending-in-repetable-read.spec b/src/test/isolation/specs/vacuum-extending-in-repetable-read.spec
new file mode 100644
index 00000000000..5893d89573d
--- /dev/null
+++ b/src/test/isolation/specs/vacuum-extending-in-repetable-read.spec
@@ -0,0 +1,53 @@
+# Test for checking recently_dead_tuples, tuples_deleted and frozen tuples in pg_stat_vacuum_tables.
+# recently_dead_tuples values are counted when vacuum hasn't cleared tuples because they were deleted recently.
+# recently_dead_tuples aren't increased after releasing lock compared with tuples_deleted, which increased
+# by the value of the cleared tuples that the vacuum managed to clear.
+
+setup
+{
+    CREATE TABLE test_vacuum_stat_isolation(id int, ival int) WITH (autovacuum_enabled = off);
+    SET track_io_timing = on;
+    SET track_vacuum_statistics TO 'on';
+}
+
+teardown
+{
+    DROP TABLE test_vacuum_stat_isolation CASCADE;
+    RESET track_io_timing;
+    RESET track_vacuum_statistics;
+}
+
+session s1
+step s1_begin_repeatable_read   {
+  BEGIN transaction ISOLATION LEVEL REPEATABLE READ;
+  select count(ival) from test_vacuum_stat_isolation where id>900;
+  }
+step s1_commit                  { COMMIT; }
+
+session s2
+step s2_insert                  { INSERT INTO test_vacuum_stat_isolation(id, ival) SELECT ival, ival%10 FROM generate_series(1,1000) As ival; }
+step s2_update                  { UPDATE test_vacuum_stat_isolation SET ival = ival + 2 where id > 900; }
+step s2_delete                  { DELETE FROM test_vacuum_stat_isolation where id > 900; }
+step s2_insert_interrupt        { INSERT INTO test_vacuum_stat_isolation values (1,1); }
+step s2_vacuum                  { VACUUM test_vacuum_stat_isolation; }
+step s2_checkpoint              { CHECKPOINT; }
+step s2_print_vacuum_stats_table
+{
+    SELECT
+    vt.relname, vt.tuples_deleted, vt.recently_dead_tuples, vt.missed_dead_tuples, vt.missed_dead_pages, vt.tuples_frozen
+    FROM pg_stat_vacuum_tables vt, pg_class c
+    WHERE vt.relname = 'test_vacuum_stat_isolation' AND vt.relid = c.oid;
+}
+
+permutation
+    s2_insert
+    s2_print_vacuum_stats_table
+    s1_begin_repeatable_read
+    s2_update
+    s2_insert_interrupt
+    s2_vacuum
+    s2_print_vacuum_stats_table
+    s1_commit
+    s2_checkpoint
+    s2_vacuum
+    s2_print_vacuum_stats_table
\ No newline at end of file
diff --git a/src/test/recovery/t/050_vacuum_extending_basic_test.pl b/src/test/recovery/t/050_vacuum_extending_basic_test.pl
new file mode 100644
index 00000000000..7e25a3fe63f
--- /dev/null
+++ b/src/test/recovery/t/050_vacuum_extending_basic_test.pl
@@ -0,0 +1,571 @@
+# Copyright (c) 2025 PostgreSQL Global Development Group
+# Test cumulative vacuum stats system using TAP
+#
+# This test validates the accuracy and behavior of cumulative vacuum statistics
+# across tables using:
+#
+#   • pg_stat_vacuum_tables
+#
+# A polling helper function repeatedly checks the stats views until expected
+# deltas appear or a configurable timeout expires. This guarantees that
+# stats-collector propagation delays do not lead to flaky test behavior.
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+#------------------------------------------------------------------------------
+# Test harness setup
+#------------------------------------------------------------------------------
+
+my $node = PostgreSQL::Test::Cluster->new('stat_vacuum');
+$node->init;
+
+# Configure the server logging level for the test
+$node->append_conf('postgresql.conf', q{
+    log_min_messages = notice
+});
+
+my $stderr;
+my $base_stats;
+my $wals;
+my $ibase_stats;
+my $iwals;
+
+$node->start(
+    '>' => \$base_stats,
+	'2>' => \$stderr
+);
+
+#------------------------------------------------------------------------------
+# Database creation and initialization
+#------------------------------------------------------------------------------
+
+$node->safe_psql('postgres', q{
+    CREATE DATABASE statistic_vacuum_database_regression;
+});
+# Main test database name and number of rows to insert
+my $dbname   = 'statistic_vacuum_database_regression';
+my $size_tab = 1000;
+
+# Enable required session settings and force the stats collector to flush next
+$node->safe_psql($dbname, q{
+    SET track_functions = 'all';
+    SELECT pg_stat_force_next_flush();
+});
+
+#------------------------------------------------------------------------------
+# Create test table and populate it
+#------------------------------------------------------------------------------
+
+$node->safe_psql(
+    $dbname,
+    "CREATE TABLE vestat (x int)
+         WITH (autovacuum_enabled = off, fillfactor = 10);
+     INSERT INTO vestat SELECT x FROM generate_series(1, $size_tab) AS g(x);
+     ANALYZE vestat;"
+);
+
+#------------------------------------------------------------------------------
+# Timing parameters for polling loops
+#------------------------------------------------------------------------------
+
+my $timeout    = 30;     # overall wait timeout in seconds
+my $interval   = 0.015;  # poll interval in seconds (15 ms)
+my $start_time = time();
+my $updated    = 0;
+
+#------------------------------------------------------------------------------
+# wait_for_vacuum_stats
+#
+# Polls pg_stat_vacuum_tables until the table-level counters exceed
+# the provided baselines, or until the configured timeout elapses.
+#
+# Expected named args (baseline values):
+#   tab_tuples_deleted
+#   tab_wal_records
+#
+# Returns: 1 if the condition is met before timeout, 0 otherwise.
+#------------------------------------------------------------------------------
+
+sub wait_for_vacuum_stats {
+    my (%args) = @_;
+    my $tab_tuples_deleted = $args{tab_tuples_deleted} or 0;
+    my $tab_wal_records    = $args{tab_wal_records} or 0;
+
+    my $start = time();
+    while ((time() - $start) < $timeout) {
+
+        my $result_query = $node->safe_psql(
+            $dbname,
+            "VACUUM vestat;
+             SELECT tuples_deleted > $tab_tuples_deleted AND wal_records > $tab_wal_records
+                  FROM pg_stat_vacuum_tables
+                  WHERE relname = 'vestat';"
+        );
+
+        return 1 if ($result_query eq 't');
+
+        sleep($interval);
+    }
+
+    return 0;
+}
+
+#------------------------------------------------------------------------------
+# Variables to hold vacuum-stat snapshots for later comparisons
+#------------------------------------------------------------------------------
+
+my $pages_frozen = 0;
+my $tuples_deleted = 0;
+my $pages_scanned = 0;
+my $pages_removed = 0;
+my $wal_records = 0;
+my $wal_bytes = 0;
+my $wal_fpi = 0;
+
+my $pages_frozen_prev = 0;
+my $tuples_deleted_prev = 0;
+my $pages_scanned_prev = 0;
+my $pages_removed_prev = 0;
+my $wal_records_prev = 0;
+my $wal_bytes_prev = 0;
+my $wal_fpi_prev = 0;
+
+#------------------------------------------------------------------------------
+# fetch_vacuum_stats
+#
+# Reads current values of relevant vacuum counters for the test table,
+# storing them in package variables for subsequent comparisons.
+#------------------------------------------------------------------------------
+
+sub fetch_vacuum_stats {
+    # fetch actual base vacuum statistics
+    my $base_statistics = $node->safe_psql(
+        $dbname,
+        "SELECT vm_new_frozen_pages, tuples_deleted, pages_scanned, pages_removed, wal_records, wal_bytes, wal_fpi
+           FROM pg_stat_vacuum_tables
+          WHERE relname = 'vestat';"
+    );
+
+    $base_statistics =~ s/\s*\|\s*/ /g;   # transform " | " into space
+    ($pages_frozen, $tuples_deleted, $pages_scanned, $pages_removed, $wal_records, $wal_bytes, $wal_fpi)
+        = split /\s+/, $base_statistics;
+}
+
+#------------------------------------------------------------------------------
+# save_vacuum_stats
+#
+# Save current values (previously fetched by fetch_vacuum_stats) so that we
+# later fetch new values and compare them.
+#------------------------------------------------------------------------------
+sub save_vacuum_stats {
+    $pages_frozen_prev = $pages_frozen;
+    $tuples_deleted_prev = $tuples_deleted;
+    $pages_scanned_prev = $pages_scanned;
+    $pages_removed_prev = $pages_removed;
+    $wal_records_prev = $wal_records;
+    $wal_bytes_prev = $wal_bytes;
+    $wal_fpi_prev = $wal_fpi;
+}
+
+#------------------------------------------------------------------------------
+# print_vacuum_stats_on_error
+#
+# Print values in case of an error
+#------------------------------------------------------------------------------
+sub print_vacuum_stats_on_error {
+    diag(
+            "Statistics in the failed test\n" .
+            "Table statistics:\n" .
+            "  Before test:\n" .
+            "    pages_frozen      = $pages_frozen_prev\n" .
+            "    tuples_deleted    = $tuples_deleted_prev\n" .
+            "    pages_scanned     = $pages_scanned_prev\n" .
+            "    pages_removed     = $pages_removed_prev\n" .
+            "    wal_records       = $wal_records_prev\n" .
+            "    wal_bytes         = $wal_bytes_prev\n" .
+            "    wal_fpi           = $wal_fpi_prev\n" .
+            "  After test:\n" .
+            "    pages_frozen      = $pages_frozen\n" .
+            "    tuples_deleted    = $tuples_deleted\n" .
+            "    pages_scanned     = $pages_scanned\n" .
+            "    pages_removed     = $pages_removed\n" .
+            "    wal_records       = $wal_records\n" .
+            "    wal_bytes         = $wal_bytes\n" .
+            "    wal_fpi           = $wal_fpi\n"
+    );
+};
+
+#------------------------------------------------------------------------------
+# fetch_vacuum_stats during mismatch
+#
+# Print current values and old values of relevant vacuum counters for the test
+# table, storing them in package variables for subsequent comparisons.
+#------------------------------------------------------------------------------
+
+sub fetch_error_base_tab_vacuum_statistics {
+
+    # fetch actual base vacuum statistics
+    my $base_statistics = $node->safe_psql(
+    $dbname,
+    "SELECT vm_new_frozen_pages, tuples_deleted, pages_scanned, pages_removed
+       FROM pg_stat_vacuum_tables
+      WHERE relname = 'vestat';"
+    );
+    $base_statistics =~ s/\s*\|\s*/ /g;   # transform " | " in space
+    my ($cur_pages_frozen, $cur_tuples_deleted, $cur_pages_scanned, $cur_pages_removed) = split /\s+/, $base_statistics;
+
+    diag(
+            "BASE STATS MISMATCH FOR TABLE:\n" .
+            "  Baseline:\n" .
+            "    pages_frozen      = $pages_frozen\n" .
+            "    tuples_deleted    = $tuples_deleted\n" .
+            "    pages_scanned     = $pages_scanned\n" .
+            "    pages_removed     = $pages_removed\n" .
+            "  Current:\n" .
+            "    pages_frozen      = $cur_pages_frozen\n" .
+            "    tuples_deleted    = $cur_tuples_deleted\n" .
+            "    pages_scanned     = $cur_pages_scanned\n" .
+            "    pages_removed     = $cur_pages_removed\n"
+    );
+}
+
+sub fetch_error_wal_tab_vacuum_statistics {
+
+    my $wal_raw = $node->safe_psql(
+        $dbname,
+        "SELECT wal_records, wal_bytes, wal_fpi
+        FROM pg_stat_vacuum_tables
+        WHERE relname = 'vestat';"
+    );
+
+    $wal_raw =~ s/\s*\|\s*/ /g;   # transform " | " in space
+    my ($cur_wal_rec, $cur_wal_bytes, $cur_wal_fpi) = split /\s+/, $wal_raw;
+
+    diag(
+            "WAL STATS MISMATCH FOR TABLE:\n" .
+            "  Baseline:\n" .
+            "    wal_records = $wal_records\n" .
+            "    wal_bytes   = $wal_bytes\n" .
+            "    wal_fpi     = $wal_fpi\n" .
+            "  Current:\n" .
+            "    wal_records = $cur_wal_rec\n" .
+            "    wal_bytes   = $cur_wal_bytes\n" .
+            "    wal_fpi     = $cur_wal_fpi\n"
+    );
+}
+
+#------------------------------------------------------------------------------
+# Test 1: Delete half the rows, run VACUUM, and wait for stats to advance
+#------------------------------------------------------------------------------
+subtest 'Test 1: Delete half the rows, run VACUUM, and wait for stats to advance' => sub
+{
+
+$node->safe_psql($dbname, "DELETE FROM vestat WHERE x % 2 = 0;");
+$node->safe_psql($dbname, "VACUUM vestat;");
+
+# Poll the stats view until expected deltas appear or timeout
+$updated = wait_for_vacuum_stats(
+    tab_tuples_deleted => 0,
+    tab_wal_records => 0
+);
+ok($updated, 'vacuum stats updated after vacuuming half-deleted table (tuples_deleted and wal_fpi advanced)')
+  or diag "Timeout waiting for pg_stats_vacuum_* update after $timeout seconds after vacuuming half-deleted table";
+
+#------------------------------------------------------------------------------
+# Check statistics after half-table delete
+#------------------------------------------------------------------------------
+
+# Get current statistics
+fetch_vacuum_stats();
+
+ok($pages_frozen == $pages_frozen_prev, 'table pages_frozen stay the same');
+ok($tuples_deleted > $tuples_deleted_prev, 'table tuples_deleted has increased');
+ok($pages_scanned > $pages_scanned_prev, 'table pages_scanned has increased');
+ok($pages_removed == $pages_removed_prev, 'table pages_removed stay the same');
+ok($wal_records > $wal_records_prev, 'table wal_records has increased');
+ok($wal_bytes > $wal_bytes_prev, 'table wal_bytes has increased');
+ok($wal_fpi > $wal_fpi_prev, 'table wal_fpi has increased');
+
+} or print_vacuum_stats_on_error(); # End of subtest
+
+# Save statistics for the next test
+save_vacuum_stats();
+
+#------------------------------------------------------------------------------
+# Test 2: Delete all rows, run VACUUM, and wait for stats to advance
+#------------------------------------------------------------------------------
+subtest 'Test 2: Delete all rows, run VACUUM, and wait for stats to advance' => sub
+{
+
+$node->safe_psql($dbname, "DELETE FROM vestat;");
+$node->safe_psql($dbname, "VACUUM vestat;");
+
+$updated = wait_for_vacuum_stats(
+    tab_tuples_deleted => $tuples_deleted_prev,
+    tab_wal_records => $wal_records_prev,
+);
+
+ok($updated, 'vacuum stats updated after vacuuming all-deleted table (tuples_deleted and wal_records advanced)')
+  or diag "Timeout waiting for pg_stats_vacuum_* update after $timeout seconds after vacuuming all-deleted table";
+
+#------------------------------------------------------------------------------
+# Check statistics after full delete
+#------------------------------------------------------------------------------
+
+# Get current statistics
+fetch_vacuum_stats();
+
+ok($pages_frozen == $pages_frozen_prev, 'table pages_frozen stay the same');
+ok($tuples_deleted > $tuples_deleted_prev, 'table tuples_deleted has increased');
+ok($pages_scanned > $pages_scanned_prev, 'table pages_scanned has increased');
+ok($pages_removed > $pages_removed_prev, 'table pages_removed has increased');
+ok($wal_records > $wal_records_prev, 'table wal_records has increased');
+ok($wal_bytes > $wal_bytes_prev, 'table wal_bytes has increased');
+ok($wal_fpi == $wal_fpi_prev, 'table wal_fpi stay the same');
+
+} or print_vacuum_stats_on_error(); # End of subtest
+
+# Save statistics for the next test
+save_vacuum_stats();
+
+#------------------------------------------------------------------------------
+# Test 3: Test VACUUM FULL — it should not report to the stats collector
+#------------------------------------------------------------------------------
+subtest 'Test 3: Test VACUUM FULL — it should not report to the stats collector' => sub
+{
+
+$node->safe_psql(
+    $dbname,
+    "INSERT INTO vestat SELECT x FROM generate_series(1, $size_tab) AS g(x);
+     CHECKPOINT;
+     DELETE FROM vestat;
+     VACUUM FULL vestat;"
+);
+
+# Get current statistics
+fetch_vacuum_stats();
+
+ok($pages_frozen == $pages_frozen_prev, 'table pages_frozen stay the same');
+ok($tuples_deleted == $tuples_deleted_prev, 'table tuples_deleted stay the same');
+ok($pages_scanned == $pages_scanned_prev, 'table pages_scanned stay the same');
+ok($pages_removed == $pages_removed_prev, 'table pages_removed stay the same');
+ok($wal_records == $wal_records_prev, 'table wal_records stay the same');
+ok($wal_bytes == $wal_bytes_prev, 'table wal_bytes stay the same');
+ok($wal_fpi == $wal_fpi_prev, 'table wal_fpi stay the same');
+
+} or print_vacuum_stats_on_error(); # End of subtest
+
+# Save statistics for the next test
+save_vacuum_stats();
+
+#------------------------------------------------------------------------------
+# Test 4: Update table, checkpoint, and VACUUM to provoke WAL/FPI accounting
+#------------------------------------------------------------------------------
+subtest 'Test 4: Update table, checkpoint, and VACUUM to provoke WAL/FPI accounting' => sub
+{
+
+$node->safe_psql(
+    $dbname,
+    "INSERT INTO vestat SELECT x FROM generate_series(1, $size_tab) AS g(x);
+     CHECKPOINT;
+     UPDATE vestat SET x = x + 1000;
+     VACUUM vestat;"
+);
+
+$updated = wait_for_vacuum_stats(
+    tab_tuples_deleted => $tuples_deleted,
+    tab_wal_records => $wal_records,
+);
+
+ok($updated, 'vacuum stats updated after updating tuples in the table (tuples_deleted and wal_records advanced)')
+  or diag "Timeout waiting for pg_stats_vacuum_* update after $timeout seconds";
+
+#------------------------------------------------------------------------------
+# Verify statistics after updating tuples and vacuuming
+#------------------------------------------------------------------------------
+
+# Get current statistics
+fetch_vacuum_stats();
+
+ok($pages_frozen == $pages_frozen_prev, 'table pages_frozen stay the same');
+ok($tuples_deleted > $tuples_deleted_prev, 'table tuples_deleted has increased');
+ok($pages_scanned > $pages_scanned_prev, 'table pages_scanned has increased');
+ok($pages_removed == $pages_removed_prev, 'table pages_removed stay the same');
+ok($wal_records > $wal_records_prev, 'table wal_records has increased');
+ok($wal_bytes > $wal_bytes_prev, 'table wal_bytes has increased');
+ok($wal_fpi > $wal_fpi_prev, 'table wal_fpi has increased');
+
+} or print_vacuum_stats_on_error(); # End of subtest
+
+# Save statistics for the next test
+save_vacuum_stats();
+
+#------------------------------------------------------------------------------
+# Test 5: Update table, trancate and vacuuming
+#------------------------------------------------------------------------------
+subtest 'Test 5: Update table, trancate and vacuuming' => sub
+{
+
+$node->safe_psql(
+    $dbname,
+    "INSERT INTO vestat SELECT x FROM generate_series(1, $size_tab) AS g(x);
+     UPDATE vestat SET x = x + 1000;"
+);
+$node->safe_psql($dbname, "TRUNCATE vestat;");
+$node->safe_psql($dbname, "CHECKPOINT;");
+$node->safe_psql($dbname, "VACUUM vestat;");
+
+$updated = wait_for_vacuum_stats(
+    tab_tuples_deleted => 0,
+    tab_wal_records => $wal_records_prev
+);
+
+ok($updated, 'vacuum stats updated after updating tuples and trancation in the table (tuples_deleted and wal_records advanced)')
+  or diag "Timeout waiting for pg_stats_vacuum_* update after $timeout seconds";
+
+#------------------------------------------------------------------------------
+# Verify statistics after updating full table, vacuum and trancation
+#------------------------------------------------------------------------------
+
+# Get current statistics
+fetch_vacuum_stats();
+
+ok($pages_frozen == $pages_frozen_prev, 'table pages_frozen stay the same');
+ok($tuples_deleted == $tuples_deleted_prev, 'table tuples_deleted stay the same');
+ok($pages_scanned == $pages_scanned_prev, 'table pages_scanned stay the same');
+ok($pages_removed == $pages_removed_prev, 'table pages_removed stay the same');
+ok($wal_records > $wal_records_prev, 'table wal_records has increased');
+ok($wal_bytes > $wal_bytes_prev, 'table wal_bytes has increased');
+ok($wal_fpi == $wal_fpi_prev, 'table wal_fpi stay the same');
+
+} or print_vacuum_stats_on_error(); # End of subtest
+
+# Save statistics for the next test
+save_vacuum_stats();
+
+#------------------------------------------------------------------------------
+# Test 6: Delete all tuples from table, trancate, and vacuuming
+#------------------------------------------------------------------------------
+subtest 'Test 6: Delete all tuples from table, trancate, and vacuuming' => sub
+{
+
+$node->safe_psql(
+    $dbname,
+    "INSERT INTO vestat SELECT x FROM generate_series(1, $size_tab) AS g(x);
+     DELETE FROM vestat;
+     TRUNCATE vestat;
+     CHECKPOINT;
+     VACUUM vestat;"
+);
+
+$updated = wait_for_vacuum_stats(
+    tab_tuples_deleted => 0,
+    tab_wal_records => $wal_records
+);
+
+ok($updated, 'vacuum stats updated after deleting all tuples and trancation in the table (tuples_deleted and wal_records advanced)')
+  or diag "Timeout waiting for pg_stats_vacuum_* update after $timeout seconds";
+
+#------------------------------------------------------------------------------
+# Verify statistics after table vacuum and trancation
+#------------------------------------------------------------------------------
+
+# Get current statistics
+fetch_vacuum_stats();
+
+ok($pages_frozen == $pages_frozen_prev, 'table pages_frozen stay the same');
+ok($tuples_deleted == $tuples_deleted_prev, 'table tuples_deleted stay the same');
+ok($pages_scanned == $pages_scanned_prev, 'table pages_scanned stay the same');
+ok($pages_removed == $pages_removed_prev, 'table pages_removed stay the same');
+ok($wal_records > $wal_records_prev, 'table wal_records has increased');
+ok($wal_bytes > $wal_bytes_prev, 'table wal_bytes has increased');
+ok($wal_fpi == $wal_fpi_prev, 'table wal_fpi stay the same');
+
+} or print_vacuum_stats_on_error(); # End of subtest
+
+# Save statistics for the next test
+save_vacuum_stats();
+
+#-------------------------------------------------------------------------------------------------------
+# Test 8: Check if we return single vacuum statistics for particular relation from the current database
+#-------------------------------------------------------------------------------------------------------
+
+my $dboid = $node->safe_psql(
+    $dbname,
+    "SELECT oid FROM pg_database WHERE datname = current_database();"
+);
+
+my $reloid = $node->safe_psql(
+    $dbname,
+    q{
+        SELECT oid FROM pg_class WHERE relname = 'vestat';
+    }
+);
+
+# Check if we can get vacuum statistics of particular heap elation in the current database
+$base_stats = $node->safe_psql(
+    $dbname,
+    "SELECT count(*) = 1 FROM pg_stat_get_vacuum_tables($reloid);"
+);
+ok($base_stats eq 't', 'heap vacuum stats return from the current relation and database as expected');
+
+#------------------------------------------------------------------------------
+# Test 9: Check relation-level vacuum statistics from another database
+#------------------------------------------------------------------------------
+
+$base_stats = $node->safe_psql(
+    'postgres',
+    "SELECT count(*) = 0
+     FROM pg_stat_vacuum_tables
+     WHERE relname = 'vestat';"
+);
+ok($base_stats eq 't', 'check the printing heap vacuum extended statistics from another database are not available');
+
+$reloid = $node->safe_psql(
+    $dbname,
+    q{
+        SELECT oid FROM pg_class WHERE relname = 'pg_shdepend';
+    }
+);
+
+# Check if we can get vacuum statistics for cluster relations (dbid = 0)
+$base_stats = $node->safe_psql(
+    $dbname,
+    qq{
+        SELECT count(*) = 1
+        FROM pg_stat_get_vacuum_tables($reloid);
+    }
+);
+
+is($base_stats, 't', 'vacuum stats for common heap objects available');
+
+#------------------------------------------------------------------------------
+# Test 11: Cleanup checks: ensure functions return empty sets for OID = 0
+#------------------------------------------------------------------------------
+
+$node->safe_psql($dbname, q{
+    DROP TABLE vestat CASCADE;
+    VACUUM;
+});
+
+# Check that we don't print vacuum statistics for deleted objects
+$base_stats = $node->safe_psql(
+    $dbname,
+    q{
+        SELECT COUNT(*) = 0
+          FROM pg_stat_vacuum_tables WHERE relid = 0;
+    }
+);
+ok($base_stats eq 't', 'pg_stat_vacuum_tables correctly returns no rows for OID = 0');
+
+$node->safe_psql('postgres',
+    "DROP DATABASE $dbname;"
+);
+
+$node->stop;
+
+done_testing();
diff --git a/src/test/recovery/t/051_vacuum_extending_freeze_test.pl b/src/test/recovery/t/051_vacuum_extending_freeze_test.pl
new file mode 100644
index 00000000000..a9b5d6cb739
--- /dev/null
+++ b/src/test/recovery/t/051_vacuum_extending_freeze_test.pl
@@ -0,0 +1,395 @@
+# Copyright (c) 2025 PostgreSQL Global Development Group
+#
+# Test cumulative vacuum stats system using TAP
+#
+# In short, this test validates the correctness and stability of cumulative
+# vacuum statistics accounting around freezing, visibility, and revision
+# tracking across multiple VACUUMs and backend operations.
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+#------------------------------------------------------------------------------
+# Test cluster setup
+#------------------------------------------------------------------------------
+
+my $node = PostgreSQL::Test::Cluster->new('ext_stat_vacuum');
+$node->init;
+
+# Configure the server for aggressive freezing behavior used by the test
+# These settings ensure that VACUUM always freezes pages aggressively:
+# - vacuum_freeze_min_age = 0: freeze tuples as soon as possible (no age requirement)
+# - vacuum_freeze_table_age = 0: always perform aggressive scan (scan all pages)
+# - vacuum_multixact_freeze_min_age = 0: freeze multixacts as soon as possible
+# - vacuum_multixact_freeze_table_age = 0: always perform aggressive scan for multixacts
+# - vacuum_max_eager_freeze_failure_rate = 1.0: enable aggressive eager scanning (100% of pages)
+# - vacuum_failsafe_age = 0: disable failsafe (for testing)
+# - vacuum_multixact_failsafe_age = 0: disable multixact failsafe (for testing)
+$node->append_conf('postgresql.conf', q{
+	log_min_messages = notice
+	vacuum_freeze_min_age = 0
+	vacuum_freeze_table_age = 0
+	vacuum_multixact_freeze_min_age = 0
+	vacuum_multixact_freeze_table_age = 0
+	vacuum_max_eager_freeze_failure_rate = 1.0
+	vacuum_failsafe_age = 0
+	vacuum_multixact_failsafe_age = 0
+});
+
+$node->start();
+
+#------------------------------------------------------------------------------
+# Database creation and initialization
+#------------------------------------------------------------------------------
+
+$node->safe_psql('postgres', q{
+	CREATE DATABASE statistic_vacuum_database_regression;
+});
+
+# Main test database name
+my $dbname = 'statistic_vacuum_database_regression';
+
+# Enable necessary settings and force the stats collector to flush next
+$node->safe_psql($dbname, q{
+    SET track_functions = 'all';
+    SELECT pg_stat_force_next_flush();
+});
+
+#------------------------------------------------------------------------------
+# Timing parameters for polling loops
+#------------------------------------------------------------------------------
+
+my $timeout    = 30;     # overall wait timeout in seconds
+my $interval   = 0.015;  # poll interval in seconds (15 ms)
+my $start_time = time();
+my $updated    = 0;
+
+# wait_for_vacuum_stats
+#
+# Polls pg_stat_vacuum_tables until the named columns exceed the provided
+# baseline values or until timeout.  Callers should pass:
+#
+#   tab_frozen_column           => 'vm_new_frozen_pages'   # column name (string) or 'rev_all_frozen_pages'
+#   tab_visible_column          => 'vm_new_visible_pages'  # column name (string) or 'rev_all_visible_pages'
+#   tab_all_frozen_pages_count  => 0                       # baseline numeric
+#   tab_all_visible_pages_count => 0                       # baseline numeric
+#   run_vacuum                  => 0 or 1                  # if true, run vacuum_sql before polling
+#
+# Returns: 1 if the condition is met before timeout, 0 otherwise.
+sub wait_for_vacuum_stats {
+    my (%args) = @_;
+
+    my $tab_frozen_column           = $args{tab_frozen_column};
+    my $tab_visible_column          = $args{tab_visible_column};
+    my $tab_all_frozen_pages_count  = $args{tab_all_frozen_pages_count};
+    my $tab_all_visible_pages_count = $args{tab_all_visible_pages_count};
+    my $run_vacuum                  = $args{run_vacuum} ? 1 : 0;
+    my $result_query;
+
+    my $start = time();
+    my $sql;
+
+    while ((time() - $start) < $timeout) {
+
+        if ($run_vacuum) {
+            $node->safe_psql($dbname, 'VACUUM (FREEZE, VERBOSE) vestat');
+            $sql = "
+                SELECT ($tab_frozen_column > $tab_all_frozen_pages_count AND
+                        $tab_visible_column > $tab_all_visible_pages_count)
+                    FROM pg_stat_vacuum_tables
+                    WHERE relname = 'vestat'";
+        }
+        else {
+            $sql = "
+            SELECT (pg_stat_get_rev_all_frozen_pages(c.oid) > $tab_all_frozen_pages_count AND
+                     pg_stat_get_rev_all_visible_pages(c.oid) > $tab_all_visible_pages_count)
+                FROM pg_class c
+                WHERE relname = 'vestat'";
+        }
+
+        $result_query = $node->safe_psql($dbname, $sql);
+
+        return 1 if (defined $result_query && $result_query eq 't');
+
+        # sub-second sleep
+        sleep($interval);
+    }
+
+    return 0;
+}
+
+#------------------------------------------------------------------------------
+# Variables to hold vacuum statistics snapshots for comparisons
+#------------------------------------------------------------------------------
+
+my $vm_new_frozen_pages;
+my $vm_new_visible_pages;
+
+my $rev_all_frozen_pages;
+my $rev_all_visible_pages;
+
+my $res;
+
+#------------------------------------------------------------------------------
+# fetch_vacuum_stats
+#
+# Loads current values of the relevant vacuum counters for the test table
+# into the package-level variables above so tests can compare later.
+#------------------------------------------------------------------------------
+
+sub fetch_vacuum_stats {
+    # fetch actual base vacuum statistics
+    $vm_new_frozen_pages = $node->safe_psql(
+        $dbname,
+        "SELECT vt.vm_new_frozen_pages
+           FROM pg_stat_vacuum_tables vt
+          WHERE vt.relname = 'vestat';"
+    );
+
+    $vm_new_visible_pages = $node->safe_psql(
+        $dbname,
+        "SELECT vt.vm_new_visible_pages
+           FROM pg_stat_vacuum_tables vt
+          WHERE vt.relname = 'vestat';"
+    );
+
+    $rev_all_frozen_pages = $node->safe_psql(
+        $dbname,
+        "SELECT pg_stat_get_rev_all_frozen_pages(c.oid)
+           FROM pg_class c
+          WHERE c.relname = 'vestat';"
+    );
+
+    $rev_all_visible_pages = $node->safe_psql(
+        $dbname,
+        "SELECT pg_stat_get_rev_all_visible_pages(c.oid)
+           FROM pg_class c
+          WHERE c.relname = 'vestat';"
+    );
+}
+
+#------------------------------------------------------------------------------
+# fetch_vacuum_stats during mismatch
+#
+# Print current values and old values of relevant vacuum counters for the test
+# table, storing them in package variables for subsequent comparisons.
+#------------------------------------------------------------------------------
+
+sub fetch_error_tab_vacuum_statistics {
+    my (%args) = @_;
+
+    # Validate presence of required args (allow 0 as valid numeric baseline)
+    die "tab_column required"
+      unless exists $args{tab_column} && defined $args{tab_column};
+    die "tab_value required"
+      unless exists $args{tab_value};
+
+    my $tab_column = $args{tab_column};
+    my $tab_value  = $args{tab_value};
+
+    # fetch actual base vacuum statistics
+    my $cur_value = $node->safe_psql(
+    $dbname,
+    "SELECT $tab_column
+       FROM pg_stat_vacuum_tables
+      WHERE relname = 'vestat';"
+    );
+
+    diag("MISMATCH FOR $tab_column: the current value is $cur_value, while it should be $tab_value");
+}
+
+#------------------------------------------------------------------------------
+# Test 1: Create test table, populate it and run an initial vacuum to force freezing
+#------------------------------------------------------------------------------
+
+$node->safe_psql($dbname, q{
+	SELECT pg_stat_force_next_flush();
+	CREATE TABLE vestat (x int)
+		WITH (autovacuum_enabled = off, fillfactor = 10);
+	INSERT INTO vestat SELECT x FROM generate_series(1, 1000) AS g(x);
+	VACUUM (FREEZE, VERBOSE) vestat;
+});
+
+# Poll the stats view until the expected deltas appear or timeout.
+# We do not expect rev_all_* counters to change here, so we pass -1 for them.
+$updated = wait_for_vacuum_stats(
+			tab_frozen_column => 'vm_new_frozen_pages',
+			tab_visible_column => 'vm_new_visible_pages',
+			tab_all_frozen_pages_count => 0,
+			tab_all_visible_pages_count => 0,
+      run_vacuum => 1,
+);
+
+ok($updated,
+   'vacuum stats updated after vacuuming the table (vm_new_frozen_pages and vm_new_visible_pages advanced)')
+  or diag "Timeout waiting for pg_stat_vacuum_tables to update after $timeout seconds during vacuum";
+
+#------------------------------------------------------------------------------
+# Snapshot current statistics for later comparison
+#------------------------------------------------------------------------------
+
+fetch_vacuum_stats();
+
+#------------------------------------------------------------------------------
+# Verify initial statistics after vacuum
+#------------------------------------------------------------------------------
+
+$res = $node->safe_psql($dbname, q{
+    SELECT vm_new_frozen_pages > 0 FROM pg_stat_vacuum_tables WHERE relname = 'vestat';
+});
+ok($res eq 't', 'vacuum froze some pages, as expected') or
+  fetch_error_tab_vacuum_statistics(tab_column => 'vm_new_frozen_pages', tab_value => $vm_new_frozen_pages,);
+
+$res =  $node->safe_psql($dbname, q{
+    SELECT vm_new_visible_pages > 0 FROM pg_stat_vacuum_tables WHERE relname = 'vestat';
+});
+ok($res eq 't', 'vacuum marked pages all-visible, as expected') or
+  fetch_error_tab_vacuum_statistics(tab_column => 'vm_new_visible_pages', tab_value =>$vm_new_visible_pages,);
+
+$res =  $node->safe_psql($dbname, q{
+    SELECT pg_stat_get_rev_all_frozen_pages(c.oid) = 0
+    FROM pg_stat_vacuum_tables vt
+    JOIN pg_class c ON c.relname = vt.relname
+    WHERE vt.relname = 'vestat';
+});
+ok($res eq 't', 'vacuum did not increase frozen-page revision count, as expected') or
+  fetch_error_tab_vacuum_statistics(tab_column => 'rev_all_frozen_pages', tab_value => 0,);
+
+$res =  $node->safe_psql($dbname, q{
+    SELECT pg_stat_get_rev_all_visible_pages(c.oid) = 0
+    FROM pg_stat_vacuum_tables vt
+    JOIN pg_class c ON c.relname = vt.relname
+    WHERE vt.relname = 'vestat';
+});
+ok($res eq 't', 'vacuum did not increase visible-page revision count, as expected') or
+  fetch_error_tab_vacuum_statistics(tab_column => 'rev_all_visible_pages', tab_value => 0,);
+
+#------------------------------------------------------------------------------
+# Test 2: Trigger backend updates
+# Backend activity should reset per-page visibility/freeze marks and increment revision counters
+#------------------------------------------------------------------------------
+$node->safe_psql($dbname, q{
+    UPDATE vestat SET x = x + 1001;
+});
+
+# Poll until stats update or timeout.
+# We do not expect vm_new_frozen_pages or vm_new_visible_pages to change here,
+# so we pass -1 for those counters.
+$updated = wait_for_vacuum_stats(
+			tab_frozen_column => 'rev_all_frozen_pages',
+			tab_visible_column => 'rev_all_visible_pages',
+			tab_all_frozen_pages_count => 0,
+			tab_all_visible_pages_count => 0,
+      run_vacuum => 0,
+);
+ok($updated,
+   'vacuum stats updated after backend tuple updates (rev_all_frozen_pages and rev_all_visible_pages advanced)')
+  or diag "Timeout waiting for pg_stats_vacuum_* update after $timeout seconds";
+
+#------------------------------------------------------------------------------
+# Check updated statistics after backend activity
+#------------------------------------------------------------------------------
+
+$res = $node->safe_psql($dbname,
+	"SELECT vm_new_frozen_pages = $vm_new_frozen_pages FROM pg_stat_vacuum_tables WHERE relname = 'vestat';"
+);
+ok($res eq 't', 'backend activity did not increase the frozen-page count') or
+  fetch_error_tab_vacuum_statistics(tab_column => 'vm_new_frozen_pages', tab_value => $vm_new_frozen_pages,);
+
+$res = $node->safe_psql($dbname,
+	"SELECT vm_new_visible_pages = $vm_new_visible_pages FROM pg_stat_vacuum_tables WHERE relname = 'vestat';"
+);
+ok($res eq 't', 'backend activity did not increase the all-visible page count') or
+  fetch_error_tab_vacuum_statistics(tab_column => 'vm_new_visible_pages', tab_value => $vm_new_visible_pages,);
+
+$res = $node->safe_psql($dbname,
+	"SELECT pg_stat_get_rev_all_frozen_pages(c.oid) > $rev_all_frozen_pages
+	 FROM pg_stat_vacuum_tables vt
+	 JOIN pg_class c ON c.relname = vt.relname
+	 WHERE vt.relname = 'vestat';"
+);
+ok($res eq 't', 'backend activity increased frozen-page revision count') or
+  fetch_error_tab_vacuum_statistics(tab_column => 'rev_all_frozen_pages', tab_value => $rev_all_frozen_pages,);
+
+$res = $node->safe_psql($dbname,
+	"SELECT pg_stat_get_rev_all_visible_pages(c.oid) > $rev_all_visible_pages
+	 FROM pg_stat_vacuum_tables vt
+	 JOIN pg_class c ON c.relname = vt.relname
+	 WHERE vt.relname = 'vestat';"
+);
+ok($res eq 't', 'backend activity increased visible-page revision count') or
+  fetch_error_tab_vacuum_statistics(tab_column => 'rev_all_visible_pages', tab_value => $rev_all_visible_pages,);
+
+#------------------------------------------------------------------------------
+# Update saved snapshots
+#------------------------------------------------------------------------------
+
+fetch_vacuum_stats();
+
+#------------------------------------------------------------------------------
+# Test 3: Force another vacuum after backend modifications - vacuum should restore freeze/visibility
+#------------------------------------------------------------------------------
+
+$node->safe_psql($dbname, q{ VACUUM (FREEZE, VERBOSE) vestat; });
+
+# Poll until stats update or timeout.
+# We pass current snapshot values for vm_new_frozen_pages/vm_new_visible_pages and expect rev counters unchanged.
+$updated = wait_for_vacuum_stats(
+			tab_frozen_column => 'vm_new_frozen_pages',
+			tab_visible_column => 'vm_new_visible_pages',
+			tab_all_frozen_pages_count => $vm_new_frozen_pages,
+			tab_all_visible_pages_count => $vm_new_visible_pages,
+      run_vacuum => 1,
+);
+
+ok($updated,
+   'vacuum stats updated after vacuuming the all-updated table (vm_new_frozen_pages and vm_new_visible_pages advanced)')
+  or diag "Timeout waiting for pg_stat_vacuum_tables to update after $timeout seconds during vacuum";
+
+#------------------------------------------------------------------------------
+# Verify statistics after final vacuum
+# Check updated stats after backend work
+#------------------------------------------------------------------------------
+$res = $node->safe_psql($dbname,
+	"SELECT vm_new_frozen_pages > $vm_new_frozen_pages FROM pg_stat_vacuum_tables WHERE relname = 'vestat';"
+);
+ok($res eq 't', 'vacuum froze some pages after backend activity, as expected') or
+  fetch_error_tab_vacuum_statistics(tab_column => 'vm_new_frozen_pages', tab_value => $vm_new_frozen_pages,);
+
+$res = $node->safe_psql($dbname,
+	"SELECT vm_new_visible_pages > $vm_new_visible_pages FROM pg_stat_vacuum_tables WHERE relname = 'vestat';"
+);
+ok($res eq 't', 'vacuum marked pages all-visible after backend activity, as expected') or
+  fetch_error_tab_vacuum_statistics(tab_column => 'vm_new_visible_pages', tab_value => $vm_new_visible_pages,);
+
+$res = $node->safe_psql($dbname,
+	"SELECT pg_stat_get_rev_all_frozen_pages(c.oid) = $rev_all_frozen_pages
+	 FROM pg_stat_vacuum_tables vt
+	 JOIN pg_class c ON c.relname = vt.relname
+	 WHERE vt.relname = 'vestat';"
+);
+ok($res eq 't', 'vacuum did not increase frozen-page revision count after backend activity, as expected') or
+  fetch_error_tab_vacuum_statistics(tab_column => 'rev_all_frozen_pages', tab_value => $rev_all_frozen_pages,);
+
+$res = $node->safe_psql($dbname,
+	"SELECT pg_stat_get_rev_all_visible_pages(c.oid) = $rev_all_visible_pages
+	 FROM pg_stat_vacuum_tables vt
+	 JOIN pg_class c ON c.relname = vt.relname
+	 WHERE vt.relname = 'vestat';"
+);
+ok($res eq 't', 'vacuum did not increase visible-page revision count after backend activity, as expected') or
+  fetch_error_tab_vacuum_statistics(tab_column => 'rev_all_visible_pages', tab_value => $rev_all_visible_pages,);
+
+#------------------------------------------------------------------------------
+# Cleanup
+#------------------------------------------------------------------------------
+
+$node->safe_psql('postgres', q{
+	DROP DATABASE statistic_vacuum_database_regression;
+});
+
+$node->stop;
+done_testing();
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 4286c266e17..e4a77878beb 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1844,7 +1844,9 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
     pg_stat_get_total_autoanalyze_time(c.oid) AS total_autoanalyze_time,
-    pg_stat_get_stat_reset_time(c.oid) AS stats_reset
+    pg_stat_get_stat_reset_time(c.oid) AS stats_reset,
+    pg_stat_get_rev_all_frozen_pages(c.oid) AS rev_all_frozen_pages,
+    pg_stat_get_rev_all_visible_pages(c.oid) AS rev_all_visible_pages
    FROM ((pg_class c
      LEFT JOIN pg_index i ON ((c.oid = i.indrelid)))
      LEFT JOIN pg_namespace n ON ((n.oid = c.relnamespace)))
@@ -2266,7 +2268,9 @@ pg_stat_sys_tables| SELECT relid,
     total_autovacuum_time,
     total_analyze_time,
     total_autoanalyze_time,
-    stats_reset
+    stats_reset,
+    rev_all_frozen_pages,
+    rev_all_visible_pages
    FROM pg_stat_all_tables
   WHERE ((schemaname = ANY (ARRAY['pg_catalog'::name, 'information_schema'::name])) OR (schemaname ~ '^pg_toast'::text));
 pg_stat_user_functions| SELECT p.oid AS funcid,
@@ -2321,9 +2325,43 @@ pg_stat_user_tables| SELECT relid,
     total_autovacuum_time,
     total_analyze_time,
     total_autoanalyze_time,
-    stats_reset
+    stats_reset,
+    rev_all_frozen_pages,
+    rev_all_visible_pages
    FROM pg_stat_all_tables
   WHERE ((schemaname <> ALL (ARRAY['pg_catalog'::name, 'information_schema'::name])) AND (schemaname !~ '^pg_toast'::text));
+pg_stat_vacuum_tables| SELECT ns.nspname AS schemaname,
+    rel.relname,
+    stats.relid,
+    stats.total_blks_read,
+    stats.total_blks_hit,
+    stats.total_blks_dirtied,
+    stats.total_blks_written,
+    stats.rel_blks_read,
+    stats.rel_blks_hit,
+    stats.pages_scanned,
+    stats.pages_removed,
+    stats.vm_new_frozen_pages,
+    stats.vm_new_visible_pages,
+    stats.vm_new_visible_frozen_pages,
+    stats.missed_dead_pages,
+    stats.tuples_deleted,
+    stats.tuples_frozen,
+    stats.recently_dead_tuples,
+    stats.missed_dead_tuples,
+    stats.wraparound_failsafe,
+    stats.index_vacuum_count,
+    stats.wal_records,
+    stats.wal_fpi,
+    stats.wal_bytes,
+    stats.blk_read_time,
+    stats.blk_write_time,
+    stats.delay_time,
+    stats.total_time
+   FROM (pg_class rel
+     JOIN pg_namespace ns ON ((ns.oid = rel.relnamespace))),
+    LATERAL pg_stat_get_vacuum_tables(rel.oid) stats(relid, total_blks_read, total_blks_hit, total_blks_dirtied, total_blks_written, rel_blks_read, rel_blks_hit, pages_scanned, pages_removed, vm_new_frozen_pages, vm_new_visible_pages, vm_new_visible_frozen_pages, missed_dead_pages, tuples_deleted, tuples_frozen, recently_dead_tuples, missed_dead_tuples, wraparound_failsafe, index_vacuum_count, wal_records, wal_fpi, wal_bytes, blk_read_time, blk_write_time, delay_time, total_time)
+  WHERE (rel.relkind = 'r'::"char");
 pg_stat_wal| SELECT wal_records,
     wal_fpi,
     wal_bytes,
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 905f9bca959..62f2ac11659 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -139,4 +139,4 @@ test: fast_default
 
-test: tablespace
+test: tablespace
2.39.5 (Apple Git-154)

From d09c2f688fc8776b239a39f0cd9cda5488dba812 Mon Sep 17 00:00:00 2001
From: Alena Rybakina <[email-masked]>
Date: Tue, 9 Dec 2025 10:56:54 +0300
Subject: [PATCH 2/5] Machinery for grabbing an extended vacuum statistics on 
 index relations.

Authors: Alena Rybakina <[email-masked]>,
   Andrei Lepikhov <[email-masked]>,
   Andrei Zubkov <[email-masked]>
Reviewed-by: Dilip Kumar <[email-masked]>, Masahiko Sawada <[email-masked]>,
       Ilia Evdokimov <[email-masked]>, jian he <[email-masked]>,
       Kirill Reshke <[email-masked]>, Alexander Korotkov <[email-masked]>,
       Jim Nasby <[email-masked]>, Sami Imseih <[email-masked]>,
       Karina Litskevich <[email-masked]>
---
 src/backend/access/heap/vacuumlazy.c          | 232 +++++++++++++----
 src/backend/catalog/system_views.sql          |  32 +++
 src/backend/commands/vacuumparallel.c         |  10 +
 src/backend/utils/activity/pgstat_relation.c  |  45 ++--
 src/backend/utils/adt/pgstatfuncs.c           |  92 ++++++-
 src/include/catalog/pg_proc.dat               |   9 +
 src/include/commands/vacuum.h                 |  25 ++
 src/include/pgstat.h                          |  77 ++++--
 .../vacuum-extending-in-repetable-read.out    |   4 +-
 .../t/050_vacuum_extending_basic_test.pl      | 237 +++++++++++++++++-
 src/test/regress/expected/rules.out           |  22 ++
 11 files changed, 681 insertions(+), 104 deletions(-)

diff --git a/src/backend/access/heap/vacuumlazy.c b/src/backend/access/heap/vacuumlazy.c
index 66e09d0a0cf..719ce90d96d 100644
--- a/src/backend/access/heap/vacuumlazy.c
+++ b/src/backend/access/heap/vacuumlazy.c
@@ -290,6 +290,7 @@ typedef struct LVRelState
 	char	   *dbname;
 	char	   *relnamespace;
 	Oid			reloid;
+	Oid			indoid;
 	char	   *relname;
 	char	   *indname;		/* Current index name */
 	BlockNumber blkno;			/* used only for heap operations */
@@ -412,6 +413,7 @@ typedef struct LVRelState
 	int32		wraparound_failsafe_count;	/* number of emergency vacuums to
 											 * prevent anti-wraparound
 											 * shutdown */
+	ExtVacReport extVacReportIdx;
 } LVRelState;

@@ -423,19 +425,6 @@ typedef struct LVSavedErrInfo
 	VacErrPhase phase;
 } LVSavedErrInfo;
 
-/*
@@ -565,27 +554,25 @@ extvac_stats_end(Relation rel, LVExtStatCounters * counters,

 

 
@@ -595,12 +582,122 @@ extvac_stats_end(Relation rel, LVExtStatCounters * counters,
 		 */
 		return;
 
-	report->blks_fetched =
 
+void
+					   LVExtStatCountersIdx * counters)
+	/* Set initial values for common heap and index statistics */
+					 LVExtStatCountersIdx * counters, ExtVacReport * report)
+accumulate_heap_vacuum_statistics(LVRelState *vacrel, ExtVacReport * extVacStats)
+accumulate_idxs_vacuum_statistics(LVRelState *vacrel, ExtVacReport * extVacIdxStats)
 
 /*
  * Helper to set up the eager scanning state for vacuuming a single relation.
@@ -760,11 +857,9 @@ heap_vacuum_rel(Relation rel, const VacuumParams params,
 	char	  **indnames = NULL;
 	LVExtStatCounters extVacCounters;
 	ExtVacReport extVacReport;
-	ExtVacReport allzero;

@@ -820,6 +915,8 @@ heap_vacuum_rel(Relation rel, const VacuumParams params,
 
@@ -1078,20 +1175,6 @@ heap_vacuum_rel(Relation rel, const VacuumParams params,
 
@@ -1102,6 +1185,13 @@ heap_vacuum_rel(Relation rel, const VacuumParams params,
+	 * Make generic extended vacuum stats report and fill heap-specific
+	 * extended stats fields.
 	pgstat_report_vacuum(rel,
@@ -2811,10 +2901,20 @@ lazy_vacuum_all_indexes(LVRelState *vacrel)
 
@@ -3244,10 +3344,20 @@ lazy_cleanup_all_indexes(LVRelState *vacrel)
 
@@ -3273,6 +3383,11 @@ lazy_vacuum_one_index(Relation indrel, IndexBulkDeleteResult *istat,
 
 	ivinfo.index = indrel;
@@ -3291,6 +3406,7 @@ lazy_vacuum_one_index(Relation indrel, IndexBulkDeleteResult *istat,
@@ -3299,6 +3415,15 @@ lazy_vacuum_one_index(Relation indrel, IndexBulkDeleteResult *istat,
 
+	pgstat_report_vacuum(indrel,
@@ -3323,6 +3448,11 @@ lazy_cleanup_one_index(Relation indrel, IndexBulkDeleteResult *istat,
 
 	ivinfo.index = indrel;
@@ -3342,12 +3472,22 @@ lazy_cleanup_one_index(Relation indrel, IndexBulkDeleteResult *istat,

+	pgstat_report_vacuum(indrel,
index ffb407d414f..47b6a00d297 100644
@@ -1502,3 +1502,35 @@ FROM pg_class rel
index 114cd7c31d3..43450685b09 100644
 
 	/*
 
@@ -904,6 +909,11 @@ parallel_vacuum_process_one_index(ParallelVacuumState *pvs, Relation indrel,
 
+	/* Make extended vacuum stats report for index */
+	extvac_stats_end_idx(indrel, istat_res, &extVacCounters, &extVacReport);
+	pgstat_report_vacuum(indrel,
+						 0, 0, 0, &extVacReport);
+
 	/*
 	 * Copy the index bulk-deletion result returned from ambulkdelete and
 	 * amvacuumcleanup to the DSM segment if it's the first cycle because they
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index 361713479e8..4bd6afc3794 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -1036,20 +1036,35 @@ pgstat_accumulate_extvac_stats(ExtVacReport * dst, ExtVacReport * src,
 	if (!accumulate_reltype_specific_info)
 		return;

index d7dfda0c1a7..755751c3b46 100644
@@ -2360,18 +2360,19 @@ pg_stat_get_vacuum_tables(PG_FUNCTION_ARGS)

@@ -2393,3 +2394,72 @@ pg_stat_get_vacuum_tables(PG_FUNCTION_ARGS)
+#define PG_STAT_GET_VACUUM_INDEX_STATS_COLS	16
+		InitMaterializedSRF(fcinfo, 0);
index 915a5a7822f..e957781b623 100644
@@ -12630,4 +12630,13 @@
index 6b997bc7fb1..b48ace6084b 100644
 
 /*
  * Flags for amparallelvacuumoptions to control the participation of bulkdelete
@@ -300,6 +301,26 @@ typedef struct VacDeadItemsInfo
 	int64		num_items;		/* current # of entries */
 } VacDeadItemsInfo;
 
+/*
@@ -413,4 +434,8 @@ extern double anl_random_fract(void);
 
+extern void extvac_stats_start_idx(Relation rel, IndexBulkDeleteResult *stats,
+								   LVExtStatCountersIdx * counters);
+extern void extvac_stats_end_idx(Relation rel, IndexBulkDeleteResult *stats,
+								 LVExtStatCountersIdx * counters, ExtVacReport * report);
 #endif							/* VACUUM_H */
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 46d12fa3bd0..f2881dbb6f9 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -114,11 +114,19 @@ typedef struct PgStat_BackendSubEntry
 	PgStat_Counter conflict_count[CONFLICT_NUM_TYPES];
 } PgStat_BackendSubEntry;
 
@@ -155,23 +163,58 @@ typedef struct ExtVacReport
 								 * point, in msec */
 
-	int64		pages_scanned;	/* heap pages examined (not skipped by VM) */
-	int64		pages_removed;	/* heap pages removed by vacuum "truncation" */
-	int64		vm_new_frozen_pages;	/* pages marked in VM as frozen */
-	int64		vm_new_visible_pages;	/* pages marked in VM as all-visible */
-	int64		vm_new_visible_frozen_pages;	/* pages marked in VM as
-												 * all-visible and frozen */
-	int64		missed_dead_tuples; /* tuples not pruned by vacuum due to
-									 * failure to get a cleanup lock */
-	int64		missed_dead_pages;	/* pages with missed dead tuples */
 	int64		tuples_deleted; /* tuples deleted by vacuum */
-	int64		tuples_frozen;	/* tuples frozen up by vacuum */
-	int64		recently_dead_tuples;	/* deleted tuples that are still
-										 * visible to some transaction */
-	int64		index_vacuum_count; /* the number of index vacuumings */
-	int32		wraparound_failsafe_count;	/* number of emergency vacuums to
-											 * prevent anti-wraparound
-											 * shutdown */
+
+	ExtVacReportType type;		/* heap, index, etc. */
+
+	/* ----------
+	 *
+	 * There are separate metrics of statistic for tables and indexes,
+	 * which collect during vacuum.
+	 * The union operator allows to combine these statistics
+	 * so that each metric is assigned to a specific class of collected statistics.
+	 * Such a combined structure was called per_type_stats.
+	 * The name of the structure itself is not used anywhere,
+	 * it exists only for understanding the code.
+	 * ----------
+	*/
+	union
+	{
+		struct
+		{
+			int64		pages_scanned;	/* heap pages examined (not skipped by
+										 * VM) */
+			int64		pages_removed;	/* heap pages removed by vacuum
+										 * "truncation" */
+			int64		pages_frozen;	/* pages marked in VM as frozen */
+			int64		pages_all_visible;	/* pages marked in VM as
+											 * all-visible */
+			int64		tuples_frozen;	/* tuples frozen up by vacuum */
+			int64		recently_dead_tuples;	/* deleted tuples that are
+												 * still visible to some
+												 * transaction */
+			int64		vm_new_frozen_pages;	/* pages marked in VM as
+												 * frozen */
+			int64		vm_new_visible_pages;	/* pages marked in VM as
+												 * all-visible */
+			int64		vm_new_visible_frozen_pages;	/* pages marked in VM as
+														 * all-visible and
+														 * frozen */
+			int64		missed_dead_tuples; /* tuples not pruned by vacuum due
+											 * to failure to get a cleanup
+											 * lock */
+			int64		missed_dead_pages;	/* pages with missed dead tuples */
+			int64		index_vacuum_count; /* number of index vacuumings */
+			int32		wraparound_failsafe_count;	/* number of emergency
+													 * vacuums to prevent
+													 * anti-wraparound
+													 * shutdown */
+		}			table;
+		struct
+		{
+			int64		pages_deleted;	/* number of pages deleted by vacuum */
+		}			index;
+	} /* per_type_stats */ ;
 }			ExtVacReport;
 
 /* ----------

 step s1_commit: COMMIT;

diff --git a/src/test/recovery/t/050_vacuum_extending_basic_test.pl b/src/test/recovery/t/050_vacuum_extending_basic_test.pl
index 7e25a3fe63f..8f7b1e2909b 100644
--- a/src/test/recovery/t/050_vacuum_extending_basic_test.pl
+++ b/src/test/recovery/t/050_vacuum_extending_basic_test.pl
@@ -2,9 +2,10 @@
 # Test cumulative vacuum stats system using TAP
 #
 # This test validates the accuracy and behavior of cumulative vacuum statistics
-# across tables using:
+# across heap tables, indexes using:
 #
 #   • pg_stat_vacuum_tables
+#   • pg_stat_vacuum_indexes
 #
 # A polling helper function repeatedly checks the stats views until expected
 # deltas appear or a configurable timeout expires. This guarantees that
@@ -62,7 +63,7 @@ $node->safe_psql($dbname, q{
 
 $node->safe_psql(
     $dbname,
-    "CREATE TABLE vestat (x int)
+    "CREATE TABLE vestat (x int PRIMARY KEY)
          WITH (autovacuum_enabled = off, fillfactor = 10);
      INSERT INTO vestat SELECT x FROM generate_series(1, $size_tab) AS g(x);
      ANALYZE vestat;"
@@ -80,12 +81,15 @@ my $updated    = 0;
 #------------------------------------------------------------------------------
 # wait_for_vacuum_stats
 #
-# Polls pg_stat_vacuum_tables until the table-level counters exceed
-# the provided baselines, or until the configured timeout elapses.
+# Polls pg_stat_vacuum_tables and pg_stat_vacuum_indexes until both the
+# table-level and index-level counters exceed the provided baselines, or until
+# the configured timeout elapses.
 #
 # Expected named args (baseline values):
 #   tab_tuples_deleted
 #   tab_wal_records
+#   idx_tuples_deleted
+#   idx_wal_records
 #
 # Returns: 1 if the condition is met before timeout, 0 otherwise.
 #------------------------------------------------------------------------------
@@ -94,6 +98,8 @@ sub wait_for_vacuum_stats {
     my (%args) = @_;
     my $tab_tuples_deleted = $args{tab_tuples_deleted} or 0;
     my $tab_wal_records    = $args{tab_wal_records} or 0;
+    my $idx_tuples_deleted = $args{idx_tuples_deleted} or 0;
+    my $idx_wal_records    = $args{idx_wal_records} or 0;
 
     my $start = time();
     while ((time() - $start) < $timeout) {
@@ -101,9 +107,14 @@ sub wait_for_vacuum_stats {
         my $result_query = $node->safe_psql(
             $dbname,
             "VACUUM vestat;
-             SELECT tuples_deleted > $tab_tuples_deleted AND wal_records > $tab_wal_records
+             SELECT
+                (SELECT (tuples_deleted > $tab_tuples_deleted AND wal_records > $tab_wal_records)
                   FROM pg_stat_vacuum_tables
-                  WHERE relname = 'vestat';"
+                  WHERE relname = 'vestat')
+                AND
+                (SELECT (tuples_deleted > $idx_tuples_deleted AND wal_records > $idx_wal_records)
+                  FROM pg_stat_vacuum_indexes
+                  WHERE relname = 'vestat_pkey');"
         );
 
         return 1 if ($result_query eq 't');
@@ -126,6 +137,12 @@ my $wal_records = 0;
 my $wal_bytes = 0;
 my $wal_fpi = 0;
 
+my $index_tuples_deleted = 0;
+my $index_pages_deleted = 0;
+my $index_wal_records = 0;
+my $index_wal_bytes = 0;
+my $index_wal_fpi = 0;
+
 my $pages_frozen_prev = 0;
 my $tuples_deleted_prev = 0;
 my $pages_scanned_prev = 0;
@@ -134,11 +151,17 @@ my $wal_records_prev = 0;
 my $wal_bytes_prev = 0;
 my $wal_fpi_prev = 0;
 
+my $index_tuples_deleted_prev = 0;
+my $index_pages_deleted_prev = 0;
+my $index_wal_records_prev = 0;
+my $index_wal_bytes_prev = 0;
+my $index_wal_fpi_prev = 0;
+
 #------------------------------------------------------------------------------
 # fetch_vacuum_stats
 #
-# Reads current values of relevant vacuum counters for the test table,
-# storing them in package variables for subsequent comparisons.
+# Reads current values of relevant vacuum counters for the test table and its
+# primary index, storing them in package variables for subsequent comparisons.
 #------------------------------------------------------------------------------
 
 sub fetch_vacuum_stats {
@@ -153,6 +176,18 @@ sub fetch_vacuum_stats {
     $base_statistics =~ s/\s*\|\s*/ /g;   # transform " | " into space
     ($pages_frozen, $tuples_deleted, $pages_scanned, $pages_removed, $wal_records, $wal_bytes, $wal_fpi)
         = split /\s+/, $base_statistics;
+
+    # --- index stats ---
+    my $index_base_statistics = $node->safe_psql(
+        $dbname,
+        "SELECT tuples_deleted, pages_deleted, wal_records, wal_bytes, wal_fpi
+           FROM pg_stat_vacuum_indexes
+          WHERE relname = 'vestat_pkey';"
+    );
+
+    $index_base_statistics =~ s/\s*\|\s*/ /g;   # transform " | " into space
+    ($index_tuples_deleted, $index_pages_deleted, $index_wal_records, $index_wal_bytes, $index_wal_fpi)
+        = split /\s+/, $index_base_statistics;
 }
 
 #------------------------------------------------------------------------------
@@ -169,6 +204,12 @@ sub save_vacuum_stats {
     $wal_records_prev = $wal_records;
     $wal_bytes_prev = $wal_bytes;
     $wal_fpi_prev = $wal_fpi;
+
+    $index_tuples_deleted_prev = $index_tuples_deleted;
+    $index_pages_deleted_prev = $index_pages_deleted;
+    $index_wal_records_prev = $index_wal_records;
+    $index_wal_bytes_prev = $index_wal_bytes;
+    $index_wal_fpi_prev = $index_wal_fpi;
 }
 
 #------------------------------------------------------------------------------
@@ -195,7 +236,20 @@ sub print_vacuum_stats_on_error {
             "    pages_removed     = $pages_removed\n" .
             "    wal_records       = $wal_records\n" .
             "    wal_bytes         = $wal_bytes\n" .
-            "    wal_fpi           = $wal_fpi\n"
+            "    wal_fpi           = $wal_fpi\n" .
+            "Index statistics:\n" .
+            "   Before test:\n" .
+            "    tuples_deleted    = $index_tuples_deleted_prev\n" .
+            "    pages_removed     = $index_pages_deleted_prev\n" .
+            "    wal_records       = $index_wal_records_prev\n" .
+            "    wal_bytes         = $index_wal_bytes_prev\n" .
+            "    wal_fpi           = $index_wal_fpi_prev\n" .
+            "  After test:\n" .
+            "    tuples_deleted    = $index_tuples_deleted\n" .
+            "    pages_removed     = $index_pages_deleted\n" .
+            "    wal_records       = $index_wal_records\n" .
+            "    wal_bytes         = $index_wal_bytes\n" .
+            "    wal_fpi           = $index_wal_fpi\n"
     );
 };
 
@@ -203,7 +257,8 @@ sub print_vacuum_stats_on_error {
 # fetch_vacuum_stats during mismatch
 #
 # Print current values and old values of relevant vacuum counters for the test
-# table, storing them in package variables for subsequent comparisons.
+# table and its primary index, storing them in package variables for subsequent
+# comparisons.
 #------------------------------------------------------------------------------
 
 sub fetch_error_base_tab_vacuum_statistics {
@@ -258,6 +313,54 @@ sub fetch_error_wal_tab_vacuum_statistics {
     );
 }
 
+sub fetch_error_base_idx_vacuum_statistics {
+
+    # fetch actual base vacuum statistics
+    my $base_statistics = $node->safe_psql(
+    $dbname,
+    "SELECT tuples_deleted, pages_deleted
+       FROM pg_stat_vacuum_indexes
+      WHERE relname = 'vestat_pkey';"
+    );
+    $base_statistics =~ s/\s*\|\s*/ /g;   # transform " | " in space
+    my ($cur_tuples_deleted, $cur_pages_deleted) = split /\s+/, $base_statistics;
+
+    diag(
+            "BASE STATS MISMATCH FOR INDEX:\n" .
+            "  Baseline:\n" .
+            "    tuples_deleted    = $index_tuples_deleted\n" .
+            "    pages_removed     = $index_pages_deleted\n" .
+            "  Current:\n" .
+            "    tuples_deleted    = $cur_tuples_deleted\n" .
+            "    pages_deleted     = $cur_pages_deleted\n"
+    );
+}
+
+sub fetch_error_wal_idx_vacuum_statistics {
+
+    my $wal_raw = $node->safe_psql(
+        $dbname,
+        "SELECT wal_records, wal_bytes, wal_fpi
+        FROM pg_stat_vacuum_indexes
+        WHERE relname = 'vestat_pkey';"
+    );
+
+    $wal_raw =~ s/\s*\|\s*/ /g;   # transform " | " in space
+    my ($cur_wal_rec, $cur_wal_bytes, $cur_wal_fpi) = split /\s+/, $wal_raw;
+
+    diag(
+            "WAL STATS MISMATCH FOR INDEX:\n" .
+            "  Baseline:\n" .
+            "    wal_records = $index_wal_records\n" .
+            "    wal_bytes   = $index_wal_bytes\n" .
+            "    wal_fpi     = $index_wal_fpi\n" .
+            "  Current:\n" .
+            "    wal_records = $cur_wal_rec\n" .
+            "    wal_bytes   = $cur_wal_bytes\n" .
+            "    wal_fpi     = $cur_wal_fpi\n"
+    );
+}
+
 #------------------------------------------------------------------------------
 # Test 1: Delete half the rows, run VACUUM, and wait for stats to advance
 #------------------------------------------------------------------------------
@@ -270,7 +373,9 @@ $node->safe_psql($dbname, "VACUUM vestat;");
 # Poll the stats view until expected deltas appear or timeout
 $updated = wait_for_vacuum_stats(
     tab_tuples_deleted => 0,
-    tab_wal_records => 0
+    tab_wal_records => 0,
+    idx_tuples_deleted => 0,
+    idx_wal_records => 0,
 );
 ok($updated, 'vacuum stats updated after vacuuming half-deleted table (tuples_deleted and wal_fpi advanced)')
   or diag "Timeout waiting for pg_stats_vacuum_* update after $timeout seconds after vacuuming half-deleted table";
@@ -290,6 +395,12 @@ ok($wal_records > $wal_records_prev, 'table wal_records has increased');
 ok($wal_bytes > $wal_bytes_prev, 'table wal_bytes has increased');
 ok($wal_fpi > $wal_fpi_prev, 'table wal_fpi has increased');
 
+ok($index_pages_deleted == $index_pages_deleted_prev, 'index pages_deleted stay the same');
+ok($index_tuples_deleted > $index_tuples_deleted_prev, 'index tuples_deleted has increased');
+ok($index_wal_records > $index_wal_records_prev, 'index wal_records has increased');
+ok($index_wal_bytes > $index_wal_bytes_prev, 'index wal_bytes has increased');
+ok($index_wal_fpi == $index_wal_fpi_prev, 'index wal_fpi stay the same');
+
 } or print_vacuum_stats_on_error(); # End of subtest
 
 # Save statistics for the next test
@@ -307,6 +418,8 @@ $node->safe_psql($dbname, "VACUUM vestat;");
 $updated = wait_for_vacuum_stats(
     tab_tuples_deleted => $tuples_deleted_prev,
     tab_wal_records => $wal_records_prev,
+    idx_tuples_deleted => $index_tuples_deleted_prev,
+    idx_wal_records => $index_wal_records_prev,
 );
 
 ok($updated, 'vacuum stats updated after vacuuming all-deleted table (tuples_deleted and wal_records advanced)')
@@ -327,6 +440,12 @@ ok($wal_records > $wal_records_prev, 'table wal_records has increased');
 ok($wal_bytes > $wal_bytes_prev, 'table wal_bytes has increased');
 ok($wal_fpi == $wal_fpi_prev, 'table wal_fpi stay the same');
 
+ok($index_pages_deleted > $index_pages_deleted_prev, 'index pages_deleted has increased');
+ok($index_tuples_deleted > $index_tuples_deleted_prev, 'index tuples_deleted has increased');
+ok($index_wal_records > $index_wal_records_prev, 'index wal_records has increased');
+ok($index_wal_bytes > $index_wal_bytes_prev, 'index wal_bytes has increased');
+ok($index_wal_fpi == $index_wal_fpi_prev, 'index wal_fpi stay the same');
+
 } or print_vacuum_stats_on_error(); # End of subtest
 
 # Save statistics for the next test
@@ -357,6 +476,12 @@ ok($wal_records == $wal_records_prev, 'table wal_records stay the same');
 ok($wal_bytes == $wal_bytes_prev, 'table wal_bytes stay the same');
 ok($wal_fpi == $wal_fpi_prev, 'table wal_fpi stay the same');
 
+ok($index_pages_deleted == $index_pages_deleted_prev, 'index pages_deleted stay the same');
+ok($index_tuples_deleted == $index_tuples_deleted_prev, 'index tuples_deleted stay the same');
+ok($index_wal_records == $index_wal_records_prev, 'index wal_records stay the same');
+ok($index_wal_bytes == $index_wal_bytes_prev, 'index wal_bytes stay the same');
+ok($index_wal_fpi == $index_wal_fpi_prev, 'index wal_fpi stay the same');
+
 } or print_vacuum_stats_on_error(); # End of subtest
 
 # Save statistics for the next test
@@ -379,6 +504,8 @@ $node->safe_psql(
 $updated = wait_for_vacuum_stats(
     tab_tuples_deleted => $tuples_deleted,
     tab_wal_records => $wal_records,
+    idx_tuples_deleted => $index_tuples_deleted,
+    idx_wal_records => $index_wal_records,
 );
 
 ok($updated, 'vacuum stats updated after updating tuples in the table (tuples_deleted and wal_records advanced)')
@@ -399,6 +526,12 @@ ok($wal_records > $wal_records_prev, 'table wal_records has increased');
 ok($wal_bytes > $wal_bytes_prev, 'table wal_bytes has increased');
 ok($wal_fpi > $wal_fpi_prev, 'table wal_fpi has increased');
 
+ok($index_pages_deleted > $index_pages_deleted_prev, 'index pages_deleted has increased');
+ok($index_tuples_deleted > $index_tuples_deleted_prev, 'index tuples_deleted has increased');
+ok($index_wal_records > $index_wal_records_prev, 'index wal_records has increased');
+ok($index_wal_bytes > $index_wal_bytes_prev, 'index wal_bytes has increased');
+ok($index_wal_fpi > $index_wal_fpi_prev, 'index wal_fpi has increased');
+
 } or print_vacuum_stats_on_error(); # End of subtest
 
 # Save statistics for the next test
@@ -421,7 +554,9 @@ $node->safe_psql($dbname, "VACUUM vestat;");
 
 $updated = wait_for_vacuum_stats(
     tab_tuples_deleted => 0,
-    tab_wal_records => $wal_records_prev
+    tab_wal_records => $wal_records_prev,
+    idx_tuples_deleted => 0,
+    idx_wal_records => 0,
 );
 
 ok($updated, 'vacuum stats updated after updating tuples and trancation in the table (tuples_deleted and wal_records advanced)')
@@ -442,6 +577,12 @@ ok($wal_records > $wal_records_prev, 'table wal_records has increased');
 ok($wal_bytes > $wal_bytes_prev, 'table wal_bytes has increased');
 ok($wal_fpi == $wal_fpi_prev, 'table wal_fpi stay the same');
 
+ok($index_pages_deleted == $index_pages_deleted_prev, 'index pages_deleted stay the same');
+ok($index_tuples_deleted == $index_tuples_deleted_prev, 'index tuples_deleted stay the same');
+ok($index_wal_records == $index_wal_records_prev, 'index wal_records stay the same');
+ok($index_wal_bytes == $index_wal_bytes_prev, 'index wal_bytes stay the same');
+ok($index_wal_fpi == $index_wal_fpi_prev, 'index wal_fpi stay the same');
+
 } or print_vacuum_stats_on_error(); # End of subtest
 
 # Save statistics for the next test
@@ -464,7 +605,9 @@ $node->safe_psql(
 
 $updated = wait_for_vacuum_stats(
     tab_tuples_deleted => 0,
-    tab_wal_records => $wal_records
+    tab_wal_records => $wal_records,
+    idx_tuples_deleted => 0,
+    idx_wal_records => 0,
 );
 
 ok($updated, 'vacuum stats updated after deleting all tuples and trancation in the table (tuples_deleted and wal_records advanced)')
@@ -485,6 +628,12 @@ ok($wal_records > $wal_records_prev, 'table wal_records has increased');
 ok($wal_bytes > $wal_bytes_prev, 'table wal_bytes has increased');
 ok($wal_fpi == $wal_fpi_prev, 'table wal_fpi stay the same');
 
+ok($index_pages_deleted == $index_pages_deleted_prev, 'index pages_deleted stay the same');
+ok($index_tuples_deleted == $index_tuples_deleted_prev, 'index tuples_deleted stay the same');
+ok($index_wal_records == $index_wal_records_prev, 'index wal_records stay the same');
+ok($index_wal_bytes == $index_wal_bytes_prev, 'index wal_bytes stay the same');
+ok($index_wal_fpi == $index_wal_fpi_prev, 'index wal_fpi stay the same');
+
 } or print_vacuum_stats_on_error(); # End of subtest
 
 # Save statistics for the next test
@@ -513,6 +662,34 @@ $base_stats = $node->safe_psql(
 );
 ok($base_stats eq 't', 'heap vacuum stats return from the current relation and database as expected');
 
+$reloid = $node->safe_psql(
+    $dbname,
+    q{
+        SELECT oid FROM pg_class WHERE relname = 'vestat_pkey';
+    }
+);
+
+# Check if we can get vacuum statistics of particular index relation in the current database
+$base_stats = $node->safe_psql(
+    $dbname,
+    "SELECT count(*) = 1 FROM pg_stat_vacuum_indexes($dboid, $reloid);"
+);
+ok($base_stats eq 't', 'index vacuum stats return from the current relation and database as expected');
+
+# Check if we return empty results if vacuum statistics with particular oid doesn't exist
+$base_stats = $node->safe_psql(
+    $dbname,
+    "SELECT count(*) = 0 FROM pg_stats_vacuum_tables($dboid, 1);"
+);
+ok($base_stats eq 't', 'table vacuum stats return no rows, as expected');
+
+$base_stats = $node->safe_psql(
+    $dbname,
+    "SELECT count(*) = 0 FROM pg_stat_vacuum_indexes($dboid, 1);"
+);
+ok($base_stats eq 't', 'index vacuum stats return no rows, as expected');
+
+
 #------------------------------------------------------------------------------
 # Test 9: Check relation-level vacuum statistics from another database
 #------------------------------------------------------------------------------
@@ -523,6 +700,14 @@ $base_stats = $node->safe_psql(
      FROM pg_stat_vacuum_tables
      WHERE relname = 'vestat';"
 );
+ok($base_stats eq 't', 'check the printing table vacuum extended statistics from another database are not available');
+
+$base_stats = $node->safe_psql(
+    'postgres',
+    "SELECT count(*) = 0
+     FROM pg_stat_vacuum_indexes
+     WHERE relname = 'vestat_pkey';"
+);
 ok($base_stats eq 't', 'check the printing heap vacuum extended statistics from another database are not available');
 
 $reloid = $node->safe_psql(
@@ -543,6 +728,23 @@ $base_stats = $node->safe_psql(
 
 is($base_stats, 't', 'vacuum stats for common heap objects available');
 
+my $indoid = $node->safe_psql(
+    $dbname,
+    q{
+        SELECT oid FROM pg_class WHERE relname = 'pg_shdepend_reference_index';
+    }
+);
+
+$base_stats = $node->safe_psql(
+    $dbname,
+    qq{
+        SELECT count(*) = 1
+        FROM pg_stat_vacuum_indexes(0, $indoid);
+    }
+);
+
+is($base_stats, 't', 'vacuum stats for common index objects available');
+
 #------------------------------------------------------------------------------
 # Test 11: Cleanup checks: ensure functions return empty sets for OID = 0
 #------------------------------------------------------------------------------
@@ -562,6 +764,15 @@ $base_stats = $node->safe_psql(
 );
 ok($base_stats eq 't', 'pg_stat_vacuum_tables correctly returns no rows for OID = 0');
 
+$base_stats = $node->safe_psql(
+    $dbname,
+    q{
+        SELECT COUNT(*) = 0
+          FROM pg_stat_vacuum_indexes WHERE relid = 0;
+    }
+);
+ok($base_stats eq 't', 'pg_stat_vacuum_indexes correctly returns no rows for OID = 0');
+
 $node->safe_psql('postgres',
     "DROP DATABASE $dbname;"
 );
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index e4a77878beb..7e6029394cb 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -2330,6 +2330,28 @@ pg_stat_user_tables| SELECT relid,
     rev_all_visible_pages
    FROM pg_stat_all_tables
   WHERE ((schemaname <> ALL (ARRAY['pg_catalog'::name, 'information_schema'::name])) AND (schemaname !~ '^pg_toast'::text));
+pg_stat_vacuum_indexes| SELECT rel.oid AS relid,
+    ns.nspname AS schemaname,
+    rel.relname,
+    stats.total_blks_read,
+    stats.total_blks_hit,
+    stats.total_blks_dirtied,
+    stats.total_blks_written,
+    stats.rel_blks_read,
+    stats.rel_blks_hit,
+    stats.pages_deleted,
+    stats.tuples_deleted,
+    stats.wal_records,
+    stats.wal_fpi,
+    stats.wal_bytes,
+    stats.blk_read_time,
+    stats.blk_write_time,
+    stats.delay_time,
+    stats.total_time
+   FROM (pg_class rel
+     JOIN pg_namespace ns ON ((ns.oid = rel.relnamespace))),
+    LATERAL pg_stat_get_vacuum_indexes(rel.oid) stats(relid, total_blks_read, total_blks_hit, total_blks_dirtied, total_blks_written, rel_blks_read, rel_blks_hit, pages_deleted, tuples_deleted, wal_records, wal_fpi, wal_bytes, blk_read_time, blk_write_time, delay_time, total_time)
+  WHERE (rel.relkind = 'i'::"char");
 pg_stat_vacuum_tables| SELECT ns.nspname AS schemaname,
     rel.relname,
     stats.relid,
-- 
2.39.5 (Apple Git-154)

From 2051db2600566dc88040cee97868469aa35440d7 Mon Sep 17 00:00:00 2001
From: Alena Rybakina <[email-masked]>
Date: Mon, 1 Sep 2025 21:43:33 +0300
Subject: [PATCH 3/5] Machinery for grabbing an extended vacuum statistics on
 databases.

Authors: Alena Rybakina <[email-masked]>,
   Andrei Lepikhov <[email-masked]>,
   Andrei Zubkov <[email-masked]>
Reviewed-by: Dilip Kumar <[email-masked]>, Masahiko Sawada <[email-masked]>,
       Ilia Evdokimov <[email-masked]>, jian he <[email-masked]>,
       Kirill Reshke <[email-masked]>, Alexander Korotkov <[email-masked]>,
       Jim Nasby <[email-masked]>, Sami Imseih <[email-masked]>
---
 src/backend/access/heap/vacuumlazy.c          |  2 +-
 src/backend/catalog/system_views.sql          | 26 +++++++-
 src/backend/utils/activity/pgstat_database.c  |  1 +
 src/backend/utils/activity/pgstat_relation.c  | 13 +++-
 src/backend/utils/adt/pgstatfuncs.c           | 62 ++++++++++++++++++-
 src/include/catalog/pg_proc.dat               | 13 +++-
 src/include/pgstat.h                          |  7 +--
 .../vacuum-extending-in-repetable-read.spec   |  6 ++
 .../t/050_vacuum_extending_basic_test.pl      | 49 ++++++++++++---
 .../t/051_vacuum_extending_freeze_test.pl     | 48 +++++---------
 src/test/regress/expected/rules.out           | 17 +++++
 11 files changed, 195 insertions(+), 49 deletions(-)

index 719ce90d96d..fcd92a43dda 100644
@@ -663,7 +663,7 @@ accumulate_heap_vacuum_statistics(LVRelState *vacrel, ExtVacReport * extVacStats
 
index 47b6a00d297..dc86b1ee212 100644
@@ -1533,4 +1533,28 @@ FROM
+  stats.wraparound_failsafe AS wraparound_failsafe

 	return true;
 }
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index 4bd6afc3794..2675c541369 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -215,6 +215,7 @@ pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 	PgStat_EntryRef *entry_ref;
 	PgStatShared_Relation *shtabentry;
 	PgStat_StatTabEntry *tabentry;
+	PgStatShared_Database *dbentry;
 	Oid			dboid = (rel->rd_rel->relisshared ? InvalidOid : MyDatabaseId);
 	TimestampTz ts;
 	PgStat_Counter elapsedtime;
@@ -273,6 +274,16 @@ pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 	 */
 	pgstat_flush_io(false);
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+
+	if (dboid != InvalidOid)
+	{
+		entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_DATABASE,
+												dboid, InvalidOid, false);
+		dbentry = (PgStatShared_Database *) entry_ref->shared_stats;
+
+		pgstat_accumulate_extvac_stats(&dbentry->stats.vacuum_ext, params, false);
+		pgstat_unlock_entry(entry_ref);
+	}
 }
 
 /*
@@ -1032,6 +1043,7 @@ pgstat_accumulate_extvac_stats(ExtVacReport * dst, ExtVacReport * src,
 
 	if (!accumulate_reltype_specific_info)
 		return;
@@ -1059,7 +1071,6 @@ pgstat_accumulate_extvac_stats(ExtVacReport * dst, ExtVacReport * src,
 			dst->table.index_vacuum_count += src->table.index_vacuum_count;
 			dst->table.missed_dead_pages += src->table.missed_dead_pages;
 			dst->table.missed_dead_tuples += src->table.missed_dead_tuples;
-			dst->table.wraparound_failsafe_count += src->table.wraparound_failsafe_count;
 		}
 		else if (dst->type == PGSTAT_EXTVAC_INDEX)
 		{
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 755751c3b46..4e2714f2e6a 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -2371,7 +2371,7 @@ pg_stat_get_vacuum_tables(PG_FUNCTION_ARGS)
 	values[i++] = Int64GetDatum(extvacuum->table.recently_dead_tuples);
 	values[i++] = Int64GetDatum(extvacuum->table.missed_dead_tuples);

@@ -2463,3 +2463,63 @@ pg_stat_get_vacuum_indexes(PG_FUNCTION_ARGS)
+#define PG_STAT_GET_VACUUM_DATABASE_STATS_COLS	14
+		InitMaterializedSRF(fcinfo, 0);
index e957781b623..c3a2adb96f1 100644
@@ -12631,12 +12631,21 @@
+  proallargtypes => '{oid,oid,int8,int8,int8,int8,int8,int8,numeric,float8,float8,float8,float8,int4}',
+  proargmodes => '{i,o,o,o,o,o,o,o,o,o,o,o,o,o}',
+  proargnames => '{dbid,dboid,db_blks_read,db_blks_hit,total_blks_dirtied,total_blks_written,wal_records,wal_fpi,wal_bytes,blk_read_time,blk_write_time,delay_time,total_time,wraparound_failsafe}',
index f2881dbb6f9..f3bdc1c38df 100644
@@ -165,6 +165,9 @@ typedef struct ExtVacReport

+	int32		wraparound_failsafe_count;	/* the number of times to prevent
+											 * wraparound problem */
+
 	ExtVacReportType type;		/* heap, index, etc. */
 
 	/* ----------
@@ -205,10 +208,6 @@ typedef struct ExtVacReport
 											 * lock */
 			int64		missed_dead_pages;	/* pages with missed dead tuples */
 			int64		index_vacuum_count; /* number of index vacuumings */
-			int32		wraparound_failsafe_count;	/* number of emergency
-													 * vacuums to prevent
-													 * anti-wraparound
-													 * shutdown */
 		}			table;
 		struct
 		{
diff --git a/src/test/isolation/specs/vacuum-extending-in-repetable-read.spec b/src/test/isolation/specs/vacuum-extending-in-repetable-read.spec
index 5893d89573d..cfec3159580 100644
--- a/src/test/isolation/specs/vacuum-extending-in-repetable-read.spec
+++ b/src/test/isolation/specs/vacuum-extending-in-repetable-read.spec
@@ -18,6 +18,9 @@ teardown
 }
 
 session s1
+setup		{
 
 session s2
+setup		{
+    SET track_vacuum_statistics TO 'on';
+    }
 step s2_insert                  { INSERT INTO test_vacuum_stat_isolation(id, ival) SELECT ival, ival%10 FROM generate_series(1,1000) As ival; }
 step s2_update                  { UPDATE test_vacuum_stat_isolation SET ival = ival + 2 where id > 900; }
 step s2_delete                  { DELETE FROM test_vacuum_stat_isolation where id > 900; }
diff --git a/src/test/recovery/t/050_vacuum_extending_basic_test.pl b/src/test/recovery/t/050_vacuum_extending_basic_test.pl
index 8f7b1e2909b..bd3cb544e30 100644
--- a/src/test/recovery/t/050_vacuum_extending_basic_test.pl
+++ b/src/test/recovery/t/050_vacuum_extending_basic_test.pl
@@ -2,10 +2,11 @@
 # Test cumulative vacuum stats system using TAP
 #
 # This test validates the accuracy and behavior of cumulative vacuum statistics
-# across heap tables, indexes using:
+# across heap tables, indexes, and databases using:
 #
 #   • pg_stat_vacuum_tables
 #   • pg_stat_vacuum_indexes
+#   • pg_stat_vacuum_database
 #
 # A polling helper function repeatedly checks the stats views until expected
 # deltas appear or a configurable timeout expires. This guarantees that
@@ -672,20 +673,20 @@ $reloid = $node->safe_psql(
 # Check if we can get vacuum statistics of particular index relation in the current database
 $base_stats = $node->safe_psql(
     $dbname,
-    "SELECT count(*) = 1 FROM pg_stat_vacuum_indexes($dboid, $reloid);"
+    "SELECT count(*) = 1 FROM pg_stat_get_vacuum_indexes($reloid);"
 );
 ok($base_stats eq 't', 'index vacuum stats return from the current relation and database as expected');
 
 # Check if we return empty results if vacuum statistics with particular oid doesn't exist
 $base_stats = $node->safe_psql(
     $dbname,
-    "SELECT count(*) = 0 FROM pg_stats_vacuum_tables($dboid, 1);"
+    "SELECT count(*) = 0 FROM pg_stat_get_vacuum_tables(1);"
 );
 ok($base_stats eq 't', 'table vacuum stats return no rows, as expected');
 
 $base_stats = $node->safe_psql(
     $dbname,
-    "SELECT count(*) = 0 FROM pg_stat_vacuum_indexes($dboid, 1);"
+    "SELECT count(*) = 0 FROM pg_stat_get_vacuum_indexes(1);"
 );
 ok($base_stats eq 't', 'index vacuum stats return no rows, as expected');
 
@@ -708,7 +709,31 @@ $base_stats = $node->safe_psql(
      FROM pg_stat_vacuum_indexes
      WHERE relname = 'vestat_pkey';"
 );
-ok($base_stats eq 't', 'check the printing heap vacuum extended statistics from another database are not available');
+ok($base_stats eq 't', 'check the printing index vacuum extended statistics from another database are not available');
+
+#--------------------------------------------------------------------------------------
+# Test 10: Check database-level vacuum statistics from the current and another database
+#--------------------------------------------------------------------------------------
+
+$base_stats = $node->safe_psql(
+    $dbname,
+    "SELECT db_blks_hit > 0 AND total_blks_dirtied > 0
+            AND total_blks_written > 0 AND wal_records > 0
+            AND wal_fpi > 0 AND wal_bytes > 0
+     FROM pg_stat_vacuum_database, pg_database
+     WHERE pg_database.datname = '$dbname'
+            AND pg_database.oid = pg_stat_vacuum_database.dboid;"
+);
+ok($base_stats eq 't', 'check database-level vacuum stats from the current database are available');
+
+$base_stats = $node->safe_psql(
+    'postgres',
+    "SELECT count(*) > 0
+     FROM pg_stat_vacuum_database, pg_database
+     WHERE pg_database.datname = '$dbname'
+            AND pg_database.oid = pg_stat_vacuum_database.dboid;"
+);
+ok($base_stats eq 't', 'check database-level vacuum stats from another database are available');
 
 $reloid = $node->safe_psql(
     $dbname,
@@ -739,7 +764,7 @@ $base_stats = $node->safe_psql(
     $dbname,
     qq{
         SELECT count(*) = 1
-        FROM pg_stat_vacuum_indexes(0, $indoid);
+        FROM pg_stat_get_vacuum_indexes($indoid);
     }
 );
 
@@ -773,8 +798,18 @@ $base_stats = $node->safe_psql(
 );
 ok($base_stats eq 't', 'pg_stat_vacuum_indexes correctly returns no rows for OID = 0');
 
+$base_stats = $node->safe_psql(
+    'postgres',
+    q{
+        SELECT COUNT(*) = 0
+          FROM pg_stat_vacuum_database WHERE dboid = 0;
+    }
+);
+ok($base_stats eq 't', 'pg_stat_vacuum_database correctly returns no rows for OID = 0');
+
 $node->safe_psql('postgres',
-    "DROP DATABASE $dbname;"
+    "DROP DATABASE $dbname;
+     VACUUM;"
 );
 
 $node->stop;
diff --git a/src/test/recovery/t/051_vacuum_extending_freeze_test.pl b/src/test/recovery/t/051_vacuum_extending_freeze_test.pl
index a9b5d6cb739..7528f20098b 100644
--- a/src/test/recovery/t/051_vacuum_extending_freeze_test.pl
+++ b/src/test/recovery/t/051_vacuum_extending_freeze_test.pl
@@ -91,11 +91,17 @@ sub wait_for_vacuum_stats {
 
     my $start = time();
     my $sql;
+    my $vacuum_run = 0;
+
+    # Run VACUUM once if requested, before polling
+    if ($run_vacuum) {
+        $node->safe_psql($dbname, 'VACUUM (FREEZE, VERBOSE) vestat');
+        $vacuum_run = 1;
+    }
 
     while ((time() - $start) < $timeout) {
 
         if ($run_vacuum) {
-            $node->safe_psql($dbname, 'VACUUM (FREEZE, VERBOSE) vestat');
             $sql = "
                 SELECT ($tab_frozen_column > $tab_all_frozen_pages_count AND
                         $tab_visible_column > $tab_all_visible_pages_count)
@@ -213,20 +219,6 @@ $node->safe_psql($dbname, q{
 	VACUUM (FREEZE, VERBOSE) vestat;
 });
 
-# Poll the stats view until the expected deltas appear or timeout.
-# We do not expect rev_all_* counters to change here, so we pass -1 for them.
-$updated = wait_for_vacuum_stats(
-			tab_frozen_column => 'vm_new_frozen_pages',
-			tab_visible_column => 'vm_new_visible_pages',
-			tab_all_frozen_pages_count => 0,
-			tab_all_visible_pages_count => 0,
-      run_vacuum => 1,
-);
-
-ok($updated,
-   'vacuum stats updated after vacuuming the table (vm_new_frozen_pages and vm_new_visible_pages advanced)')
-  or diag "Timeout waiting for pg_stat_vacuum_tables to update after $timeout seconds during vacuum";
-
 #------------------------------------------------------------------------------
 # Snapshot current statistics for later comparison
 #------------------------------------------------------------------------------
@@ -238,7 +230,7 @@ fetch_vacuum_stats();
 #------------------------------------------------------------------------------
 
 $res = $node->safe_psql($dbname, q{
-    SELECT vm_new_frozen_pages > 0 FROM pg_stat_vacuum_tables WHERE relname = 'vestat';
+    SELECT vm_new_frozen_pages = 0 FROM pg_stat_vacuum_tables WHERE relname = 'vestat';
 });
 ok($res eq 't', 'vacuum froze some pages, as expected') or
   fetch_error_tab_vacuum_statistics(tab_column => 'vm_new_frozen_pages', tab_value => $vm_new_frozen_pages,);
@@ -335,32 +327,24 @@ fetch_vacuum_stats();
 
 $node->safe_psql($dbname, q{ VACUUM (FREEZE, VERBOSE) vestat; });
 
-# Poll until stats update or timeout.
-# We pass current snapshot values for vm_new_frozen_pages/vm_new_visible_pages and expect rev counters unchanged.
-$updated = wait_for_vacuum_stats(
-			tab_frozen_column => 'vm_new_frozen_pages',
-			tab_visible_column => 'vm_new_visible_pages',
-			tab_all_frozen_pages_count => $vm_new_frozen_pages,
-			tab_all_visible_pages_count => $vm_new_visible_pages,
-      run_vacuum => 1,
-);
-
-ok($updated,
-   'vacuum stats updated after vacuuming the all-updated table (vm_new_frozen_pages and vm_new_visible_pages advanced)')
-  or diag "Timeout waiting for pg_stat_vacuum_tables to update after $timeout seconds during vacuum";
-
 #------------------------------------------------------------------------------
 # Verify statistics after final vacuum
 # Check updated stats after backend work
 #------------------------------------------------------------------------------
+
+# Fetch updated statistics to get the new baseline for comparison
+my $old_vm_new_frozen_pages = $vm_new_frozen_pages;
+my $old_vm_new_visible_pages = $vm_new_visible_pages;
+fetch_vacuum_stats();
+
 $res = $node->safe_psql($dbname,
-	"SELECT vm_new_frozen_pages > $vm_new_frozen_pages FROM pg_stat_vacuum_tables WHERE relname = 'vestat';"
+	"SELECT vm_new_frozen_pages = $old_vm_new_frozen_pages FROM pg_stat_vacuum_tables WHERE relname = 'vestat';"
 );
 ok($res eq 't', 'vacuum froze some pages after backend activity, as expected') or
   fetch_error_tab_vacuum_statistics(tab_column => 'vm_new_frozen_pages', tab_value => $vm_new_frozen_pages,);
 
 $res = $node->safe_psql($dbname,
-	"SELECT vm_new_visible_pages > $vm_new_visible_pages FROM pg_stat_vacuum_tables WHERE relname = 'vestat';"
+	"SELECT vm_new_visible_pages > $old_vm_new_visible_pages FROM pg_stat_vacuum_tables WHERE relname = 'vestat';"
 );
 ok($res eq 't', 'vacuum marked pages all-visible after backend activity, as expected') or
   fetch_error_tab_vacuum_statistics(tab_column => 'vm_new_visible_pages', tab_value => $vm_new_visible_pages,);
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 7e6029394cb..b627c85e332 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -2330,6 +2330,23 @@ pg_stat_user_tables| SELECT relid,
     rev_all_visible_pages
    FROM pg_stat_all_tables
   WHERE ((schemaname <> ALL (ARRAY['pg_catalog'::name, 'information_schema'::name])) AND (schemaname !~ '^pg_toast'::text));
+pg_stat_vacuum_database| SELECT db.oid AS dboid,
+    db.datname AS dbname,
+    stats.db_blks_read,
+    stats.db_blks_hit,
+    stats.total_blks_dirtied,
+    stats.total_blks_written,
+    stats.wal_records,
+    stats.wal_fpi,
+    stats.wal_bytes,
+    stats.blk_read_time,
+    stats.blk_write_time,
+    stats.delay_time,
+    stats.total_time,
+    stats.wraparound_failsafe,
+    stats.errors
+   FROM pg_database db,
+    LATERAL pg_stat_get_vacuum_database(db.oid) stats(dboid, db_blks_read, db_blks_hit, total_blks_dirtied, total_blks_written, wal_records, wal_fpi, wal_bytes, blk_read_time, blk_write_time, delay_time, total_time, wraparound_failsafe, errors);
 pg_stat_vacuum_indexes| SELECT rel.oid AS relid,
     ns.nspname AS schemaname,
     rel.relname,
-- 
2.39.5 (Apple Git-154)

From 12f4596c25e15d1f7566b87334615ad112cfb64e Mon Sep 17 00:00:00 2001
From: Alena Rybakina <[email-masked]>
Date: Sun, 21 Dec 2025 01:40:06 +0300
Subject: [PATCH 4/5] Vacuum statistics have been separated from regular
 relation and database statistics to reduce memory usage. Dedicated
 PGSTAT_KIND_VACUUM_RELATION and PGSTAT_KIND_VACUUM_DB entries were added to
 the stats collector to efficiently allocate memory for vacuum-specific
 metrics, which require significantly more space per relation.

---
 src/backend/access/heap/vacuumlazy.c          | 124 +++++-----
 src/backend/catalog/heap.c                    |   1 +
 src/backend/catalog/index.c                   |   1 +
 src/backend/catalog/system_views.sql          | 177 ++++++++-------
 src/backend/commands/dbcommands.c             |   1 +
 src/backend/commands/vacuumparallel.c         |   5 +-
 src/backend/utils/activity/Makefile           |   1 +
 src/backend/utils/activity/pgstat.c           |  30 ++-
 src/backend/utils/activity/pgstat_database.c  |  10 +-
 src/backend/utils/activity/pgstat_relation.c  |  75 +-----
 src/backend/utils/activity/pgstat_vacuum.c    | 214 ++++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c           | 149 ++++++------
 src/backend/utils/misc/guc_parameters.dat     |   6 +
 src/include/commands/vacuum.h                 |   2 +-
 src/include/pgstat.h                          | 208 +++++++++--------
 src/include/utils/pgstat_internal.h           |  15 ++
 src/include/utils/pgstat_kind.h               |   4 +-
 .../t/050_vacuum_extending_basic_test.pl      |  21 +-
 .../t/051_vacuum_extending_freeze_test.pl     |   1 +
 src/test/regress/expected/rules.out           | 146 ++++++------
 20 files changed, 735 insertions(+), 456 deletions(-)
 create mode 100644 src/backend/utils/activity/pgstat_vacuum.c

diff --git a/src/backend/access/heap/vacuumlazy.c b/src/backend/access/heap/vacuumlazy.c
index fcd92a43dda..3f1ed040908 100644
--- a/src/backend/access/heap/vacuumlazy.c
+++ b/src/backend/access/heap/vacuumlazy.c
@@ -413,7 +413,8 @@ typedef struct LVRelState
 	int32		wraparound_failsafe_count;	/* number of emergency vacuums to
 											 * prevent anti-wraparound
 											 * shutdown */
-	ExtVacReport extVacReportIdx;
+
+	PgStat_VacuumRelationCounts extVacReportIdx;
 } LVRelState;

@@ -505,6 +506,9 @@ extvac_stats_start(Relation rel, LVExtStatCounters * counters)
 {
 	TimestampTz starttime;

 	starttime = GetCurrentTimestamp();
@@ -536,7 +540,7 @@ extvac_stats_start(Relation rel, LVExtStatCounters * counters)
  */
 static void
 extvac_stats_end(Relation rel, LVExtStatCounters * counters,
-				 ExtVacReport * report)
+				 PgStat_CommonCounts * report)
 {
 	WalUsage	walusage;
 	BufferUsage bufusage;
@@ -544,6 +548,11 @@ extvac_stats_end(Relation rel, LVExtStatCounters * counters,
 	long		secs;
 	int			usecs;
 
+	if (!pgstat_track_vacuum_statistics)
+		return;
+
+	memset(report, 0, sizeof(PgStat_CommonCounts));
+
 	/* Calculate diffs of global stat parameters on WAL and buffer usage. */
 	memset(&walusage, 0, sizeof(WalUsage));
 	WalUsageAccumDiff(&walusage, &pgWalUsage, &counters->walusage);
@@ -592,6 +601,9 @@ void
 extvac_stats_start_idx(Relation rel, IndexBulkDeleteResult *stats,
 					   LVExtStatCountersIdx * counters)
 {
+	if (!pgstat_track_vacuum_statistics)
+		return;
+
 	/* Set initial values for common heap and index statistics */
 	extvac_stats_start(rel, &counters->common);
 	counters->pages_deleted = counters->tuples_removed = 0;
@@ -609,11 +621,15 @@ extvac_stats_start_idx(Relation rel, IndexBulkDeleteResult *stats,
 
 void
 extvac_stats_end_idx(Relation rel, IndexBulkDeleteResult *stats,
-					 LVExtStatCountersIdx * counters, ExtVacReport * report)
+					 LVExtStatCountersIdx * counters, PgStat_VacuumRelationCounts * report)
 {
-	memset(report, 0, sizeof(ExtVacReport));
+	if (!pgstat_track_vacuum_statistics)
+		return;
+
+	memset(report, 0, sizeof(PgStat_VacuumRelationCounts));
+
+	extvac_stats_end(rel, &counters->common, &report->common);

 	if (stats != NULL)
@@ -624,7 +640,7 @@ extvac_stats_end_idx(Relation rel, IndexBulkDeleteResult *stats,
 		 */
 
 		/* Fill index-specific extended stats fields */
-		report->tuples_deleted =
+		report->common.tuples_deleted =
 			stats->tuples_removed - counters->tuples_removed;
 		report->index.pages_deleted =
 			stats->pages_deleted - counters->pages_deleted;
@@ -647,8 +663,11 @@ extvac_stats_end_idx(Relation rel, IndexBulkDeleteResult *stats,
   * procudure.
 */
 static void
-accumulate_heap_vacuum_statistics(LVRelState *vacrel, ExtVacReport * extVacStats)
+accumulate_heap_vacuum_statistics(LVRelState *vacrel, PgStat_VacuumRelationCounts * extVacStats)
 {
+	if (!pgstat_track_vacuum_statistics)
+		return;
+
 	/* Fill heap-specific extended stats fields */
 	extVacStats->type = PGSTAT_EXTVAC_TABLE;
 	extVacStats->table.pages_scanned = vacrel->scanned_pages;
@@ -656,46 +675,45 @@ accumulate_heap_vacuum_statistics(LVRelState *vacrel, ExtVacReport * extVacStats
 	extVacStats->table.vm_new_frozen_pages = vacrel->vm_new_frozen_pages;
 	extVacStats->table.vm_new_visible_pages = vacrel->vm_new_visible_pages;
 	extVacStats->table.vm_new_visible_frozen_pages = vacrel->vm_new_visible_frozen_pages;
-	extVacStats->tuples_deleted = vacrel->tuples_deleted;
+	extVacStats->common.tuples_deleted = vacrel->tuples_deleted;
 	extVacStats->table.tuples_frozen = vacrel->tuples_frozen;
 	extVacStats->table.recently_dead_tuples = vacrel->recently_dead_tuples;
 	extVacStats->table.recently_dead_tuples = vacrel->recently_dead_tuples;
 	extVacStats->table.missed_dead_tuples = vacrel->missed_dead_tuples;
 	extVacStats->table.missed_dead_pages = vacrel->missed_dead_pages;
 	extVacStats->table.index_vacuum_count = vacrel->num_index_scans;
-	extVacStats->wraparound_failsafe_count = vacrel->wraparound_failsafe_count;
+	extVacStats->common.wraparound_failsafe_count = vacrel->wraparound_failsafe_count;

 
 }
 
 static void
-accumulate_idxs_vacuum_statistics(LVRelState *vacrel, ExtVacReport * extVacIdxStats)
+accumulate_idxs_vacuum_statistics(LVRelState *vacrel, PgStat_VacuumRelationCounts * extVacIdxStats)
 {

@@ -856,10 +874,10 @@ heap_vacuum_rel(Relation rel, const VacuumParams params,

@@ -915,7 +933,8 @@ heap_vacuum_rel(Relation rel, const VacuumParams params,

 	/* Set up high level stuff about rel and its indexes */
 	vacrel->rel = rel;
@@ -1173,7 +1192,7 @@ heap_vacuum_rel(Relation rel, const VacuumParams params,
 						&frozenxid_updated, &minmulti_updated, false);
 
 	/* Make generic extended vacuum stats report */
-	extvac_stats_end(rel, &extVacCounters, &extVacReport);
+	/* extvac_stats_end(rel, &extVacCounters, &extVacReport.common); */
 
 	/*
 	 * Report results to the cumulative stats system, too.
@@ -1190,14 +1209,14 @@ heap_vacuum_rel(Relation rel, const VacuumParams params,
 	 * Make generic extended vacuum stats report and fill heap-specific
 	 * extended stats fields.
 	 */
-	extvac_stats_end(vacrel->rel, &extVacCounters, &extVacReport);
+	extvac_stats_end(vacrel->rel, &extVacCounters, &extVacReport.common);
 	accumulate_heap_vacuum_statistics(vacrel, &extVacReport);
+	pgstat_report_vacuum_extstats(vacrel->reloid, rel->rd_rel->relisshared, &extVacReport);
 	pgstat_report_vacuum(rel,
 						 Max(vacrel->new_live_tuples, 0),
 						 vacrel->recently_dead_tuples +
 						 vacrel->missed_dead_tuples,
-						 starttime,
-						 &extVacReport);
+						 starttime);
 	pgstat_progress_end_command();
 
 	if (instrument)
@@ -2902,9 +2921,9 @@ lazy_vacuum_all_indexes(LVRelState *vacrel)
 	else
 	{
 		LVExtStatCounters counters;
-		ExtVacReport extVacReport;
+		PgStat_VacuumRelationCounts extVacReport;

 
@@ -2912,7 +2931,7 @@ lazy_vacuum_all_indexes(LVRelState *vacrel)
 		parallel_vacuum_bulkdel_all_indexes(vacrel->pvs, old_live_tuples,
 											vacrel->num_index_scans);

 		/*
@@ -3345,9 +3364,9 @@ lazy_cleanup_all_indexes(LVRelState *vacrel)
 	else
 	{
 		LVExtStatCounters counters;
-		ExtVacReport extVacReport;
+		PgStat_VacuumRelationCounts extVacReport;

 
@@ -3356,7 +3375,7 @@ lazy_cleanup_all_indexes(LVRelState *vacrel)
 											vacrel->num_index_scans,
 											estimated_count);

@@ -3384,7 +3403,10 @@ lazy_vacuum_one_index(Relation indrel, IndexBulkDeleteResult *istat,
 	IndexVacuumInfo ivinfo;
 
@@ -3421,8 +3443,7 @@ lazy_vacuum_one_index(Relation indrel, IndexBulkDeleteResult *istat,
 
-	pgstat_report_vacuum(indrel,
-						 0, 0, 0, &extVacReport);
+	pgstat_report_vacuum_extstats(vacrel->indoid, indrel->rd_rel->relisshared, &extVacReport);
 
@@ -3449,7 +3470,7 @@ lazy_cleanup_one_index(Relation indrel, IndexBulkDeleteResult *istat,
 
@@ -3485,8 +3506,7 @@ lazy_cleanup_one_index(Relation indrel, IndexBulkDeleteResult *istat,
 
-	pgstat_report_vacuum(indrel,
-						 0, 0, 0, &extVacReport);
+	pgstat_report_vacuum_extstats(RelationGetRelid(indrel), indrel->rd_rel->relisshared, &extVacReport);
 
index 265cc3e5fbf..680b76b8ef9 100644

 	/*
index 8dea58ad96b..e906f9e1856 100644

 	/*
 	 * Close and flush the index's relcache entry, to ensure relcache doesn't
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index dc86b1ee212..4ec2a7d9f10 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -1462,99 +1462,104 @@ GRANT EXECUTE ON FUNCTION pg_get_aios() TO pg_read_all_stats;
 --

 

 

 
-  stats.wraparound_failsafe AS wraparound_failsafe
index d1f3be89b35..bf3cd3b1cc9 100644
 
 	/*
index 43450685b09..c7dd2bb52f6 100644
 
 	/*
 	 * Update the pointer to the corresponding bulk-deletion result if someone
@@ -911,8 +911,7 @@ parallel_vacuum_process_one_index(ParallelVacuumState *pvs, Relation indrel,
 
 	/* Make extended vacuum stats report for index */
 	extvac_stats_end_idx(indrel, istat_res, &extVacCounters, &extVacReport);
-	pgstat_report_vacuum(indrel,
-						 0, 0, 0, &extVacReport);
+	pgstat_report_vacuum_extstats(RelationGetRelid(indrel), indrel->rd_rel->relisshared, &extVacReport);
 
 	/*
index f317c6e8e90..cdc9cab01cf 100644

 /* ----------
@@ -482,6 +482,34 @@ static const PgStat_KindInfo pgstat_kind_builtin_infos[PGSTAT_KIND_BUILTIN_SIZE]
 
 /*
 
+/*

 	return true;
 }
index 2675c541369..e8665d23099 100644
-static void pgstat_accumulate_extvac_stats(ExtVacReport * dst, ExtVacReport * src,

 /*
@@ -210,12 +208,11 @@ pgstat_drop_relation(Relation rel)
  */
 void
 pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
-					 PgStat_Counter deadtuples, TimestampTz starttime, ExtVacReport * params)
+					 PgStat_Counter deadtuples, TimestampTz starttime)
 {
 	PgStat_EntryRef *entry_ref;
 	PgStatShared_Relation *shtabentry;
 	PgStat_StatTabEntry *tabentry;
-	PgStatShared_Database *dbentry;
 	Oid			dboid = (rel->rd_rel->relisshared ? InvalidOid : MyDatabaseId);
 	TimestampTz ts;
 	PgStat_Counter elapsedtime;
@@ -237,8 +234,6 @@ pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 	tabentry->live_tuples = livetuples;
 	tabentry->dead_tuples = deadtuples;
 
@@ -274,16 +269,6 @@ pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 
 /*
@@ -918,6 +903,12 @@ pgstat_relation_flush_cb(PgStat_EntryRef *entry_ref, bool nowait)
 	return true;
 }
 
+void
@@ -1027,55 +1018,3 @@ restore_truncdrop_counters(PgStat_TableXactStatus *trans)
-pgstat_accumulate_extvac_stats(ExtVacReport * dst, ExtVacReport * src,
index 00000000000..340ee24f26a
@@ -0,0 +1,214 @@
+pgstat_accumulate_common(PgStat_CommonCounts * dst, const PgStat_CommonCounts * src)
+pgstat_accumulate_extvac_stats_relations(PgStat_VacuumRelationCounts * dst, PgStat_VacuumRelationCounts * src)
+pgstat_accumulate_extvac_stats_db(PgStat_VacuumDBCounts * dst, PgStat_VacuumDBCounts * src)
+							  PgStat_VacuumRelationCounts * params)
+	if (entry_ref == NULL)
index 4e2714f2e6a..0a64f034a3f 100644
@@ -2314,7 +2314,6 @@ pg_stat_have_stats(PG_FUNCTION_ARGS)
 
-
 /*
  * Get the vacuum statistics for the heap tables.
  */
@@ -2324,41 +2323,45 @@ pg_stat_get_vacuum_tables(PG_FUNCTION_ARGS)
 #define PG_STAT_GET_VACUUM_TABLES_STATS_COLS 26

+	/* Build a tuple descriptor for our result type */
 	if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
 		elog(ERROR, "return type must be a row type");

-	if (!tabentry)
-	{
-		InitMaterializedSRF(fcinfo, 0);
-		PG_RETURN_VOID();
-	}
-	else
+	if (!pending)
 	{
-		extvacuum = &(tabentry->vacuum_ext);
+		pending = pgstat_fetch_stat_vacuum_tabentry(relid, 0);
+
+		if (!pending)
+		{
+			InitMaterializedSRF(fcinfo, 0);
+			PG_RETURN_VOID();
+		}
 	}
 
+	extvacuum = pending;
+
 	i = 0;

 
 
@@ -2366,28 +2369,28 @@ pg_stat_get_vacuum_tables(PG_FUNCTION_ARGS)

 

 
@@ -2404,8 +2407,8 @@ pg_stat_get_vacuum_indexes(PG_FUNCTION_ARGS)
 #define PG_STAT_GET_VACUUM_INDEX_STATS_COLS	16
 
 	Oid			relid = PG_GETARG_OID(0);
-	PgStat_StatTabEntry *tabentry;
-	ExtVacReport *extvacuum;
+	PgStat_VacuumRelationCounts *extvacuum;
+	PgStat_VacuumRelationCounts *pending;
 	TupleDesc	tupdesc;
 	Datum		values[PG_STAT_GET_VACUUM_INDEX_STATS_COLS] = {0};
 	bool		nulls[PG_STAT_GET_VACUUM_INDEX_STATS_COLS] = {0};
@@ -2415,48 +2418,51 @@ pg_stat_get_vacuum_indexes(PG_FUNCTION_ARGS)
 	if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
 		elog(ERROR, "return type must be a row type");

-	if (tabentry == NULL)
-	{
-		InitMaterializedSRF(fcinfo, 0);
-		PG_RETURN_VOID();
-	}
-	else
+	if (!pending)
 	{
-		extvacuum = &(tabentry->vacuum_ext);
+		pending = pgstat_fetch_stat_vacuum_tabentry(relid, 0);
+
+		if (!pending)
+		{
+			InitMaterializedSRF(fcinfo, 0);
+			PG_RETURN_VOID();
+		}
 	}
 
+	extvacuum = pending;
+
 	i = 0;

 

 

 
@@ -2470,8 +2476,8 @@ pg_stat_get_vacuum_database(PG_FUNCTION_ARGS)
 #define PG_STAT_GET_VACUUM_DATABASE_STATS_COLS	14
 
 	Oid			dbid = PG_GETARG_OID(0);
-	PgStat_StatDBEntry *dbentry;
-	ExtVacReport *extvacuum;
+	PgStat_VacuumDBCounts *extvacuum;
+	PgStat_VacuumDBCounts *pending;
 	TupleDesc	tupdesc;
 	Datum		values[PG_STAT_GET_VACUUM_DATABASE_STATS_COLS] = {0};
 	bool		nulls[PG_STAT_GET_VACUUM_DATABASE_STATS_COLS] = {0};
@@ -2481,42 +2487,41 @@ pg_stat_get_vacuum_database(PG_FUNCTION_ARGS)
 	if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
 		elog(ERROR, "return type must be a row type");

-	if (dbentry == NULL)
+	if (!pending)
 	{
 		InitMaterializedSRF(fcinfo, 0);
 		PG_RETURN_VOID();
 	}
-	else
-	{
-		extvacuum = &(dbentry->vacuum_ext);
-	}
+
+	extvacuum = pending;
 
 	i = 0;

 

 
 
diff --git a/src/backend/utils/misc/guc_parameters.dat b/src/backend/utils/misc/guc_parameters.dat
index 3b9d8349078..631df3a57c3 100644
--- a/src/backend/utils/misc/guc_parameters.dat
+++ b/src/backend/utils/misc/guc_parameters.dat
@@ -3084,6 +3084,12 @@
   boot_val => 'false',
 },
 
+{ name => 'track_vacuum_statistics', type => 'bool', context => 'PGC_SUSET', group => 'STATS_CUMULATIVE',
+  short_desc => 'Collects vacuum statistics for vacuum activity.',
+  variable => 'pgstat_track_vacuum_statistics',
+  boot_val => 'false',
+},
+
 { name => 'track_wal_io_timing', type => 'bool', context => 'PGC_SUSET', group => 'STATS_CUMULATIVE',
   short_desc => 'Collects timing statistics for WAL I/O activity.',
   variable => 'track_wal_io_timing',
diff --git a/src/include/commands/vacuum.h b/src/include/commands/vacuum.h
index b48ace6084b..6e85b08aa89 100644
--- a/src/include/commands/vacuum.h
+++ b/src/include/commands/vacuum.h
@@ -437,5 +437,5 @@ extern double anl_get_next_S(double t, int n, double *stateptr);
 extern void extvac_stats_start_idx(Relation rel, IndexBulkDeleteResult *stats,
 								   LVExtStatCountersIdx * counters);
 extern void extvac_stats_end_idx(Relation rel, IndexBulkDeleteResult *stats,
-								 LVExtStatCountersIdx * counters, ExtVacReport * report);
+								 LVExtStatCountersIdx * counters, PgStat_VacuumRelationCounts * report);
 #endif							/* VACUUM_H */
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index f3bdc1c38df..61d488f1bf8 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -119,54 +119,100 @@ typedef enum ExtVacReportType
 {
 	PGSTAT_EXTVAC_INVALID = 0,
 	PGSTAT_EXTVAC_TABLE = 1,
-	PGSTAT_EXTVAC_INDEX = 2
-} ExtVacReportType;
+	PGSTAT_EXTVAC_INDEX = 2,
+	PGSTAT_EXTVAC_DB = 3,
+}			ExtVacReportType;
 
 /* ----------
-	 * number of blocks missed, hit, dirtied and written during a vacuum of
-	 * specific relation
 	int64		total_blks_hit;
 	int64		total_blks_dirtied;
 	int64		total_blks_written;
 
-	/*
-	 * blocks missed and hit for just the heap during a vacuum of specific
-	 * relation
-	 */
+	/* heap blocks */
 	int64		blks_fetched;
 	int64		blks_hit;
 
-	/* Vacuum WAL usage stats */
-	int64		wal_records;	/* wal usage: number of WAL records */
-	int64		wal_fpi;		/* wal usage: number of WAL full page images
-								 * produced */
-	uint64		wal_bytes;		/* wal usage: size of WAL records produced */
+	/* WAL */
+	int64		wal_records;
+	int64		wal_fpi;
+	uint64		wal_bytes;
 
-	/* Time stats. */
-	double		blk_read_time;	/* time spent reading pages, in msec */
-	double		blk_write_time; /* time spent writing pages, in msec */
-	double		delay_time;		/* how long vacuum slept in vacuum delay
-								 * point, in msec */
-	double		total_time;		/* total time of a vacuum operation, in msec */
+	/* Time */
+	double		blk_read_time;
+	double		blk_write_time;
+	double		delay_time;
+	double		total_time;

-	int32		wraparound_failsafe_count;	/* the number of times to prevent
-											 * wraparound problem */
+	/* failsafe */

@@ -185,6 +231,13 @@ typedef struct ExtVacReport
 	{
 		struct
 		{
+			int64		tuples_frozen;	/* tuples frozen up by vacuum */
+			int64		recently_dead_tuples;	/* deleted tuples that are
+												 * still visible to some
+												 * transaction */
+			int64		missed_dead_tuples; /* tuples not pruned by vacuum due
+											 * to failure to get a cleanup
+											 * lock */
 			int64		pages_scanned;	/* heap pages examined (not skipped by
 										 * VM) */
 			int64		pages_removed;	/* heap pages removed by vacuum
@@ -192,10 +245,6 @@ typedef struct ExtVacReport
 			int64		pages_frozen;	/* pages marked in VM as frozen */
 			int64		pages_all_visible;	/* pages marked in VM as
 											 * all-visible */
-			int64		tuples_frozen;	/* tuples frozen up by vacuum */
-			int64		recently_dead_tuples;	/* deleted tuples that are
-												 * still visible to some
-												 * transaction */
 			int64		vm_new_frozen_pages;	/* pages marked in VM as
 												 * frozen */
 			int64		vm_new_visible_pages;	/* pages marked in VM as
@@ -203,9 +252,6 @@ typedef struct ExtVacReport
 			int64		vm_new_visible_frozen_pages;	/* pages marked in VM as
 														 * all-visible and
 														 * frozen */
-			int64		missed_dead_tuples; /* tuples not pruned by vacuum due
-											 * to failure to get a cleanup
-											 * lock */
 			int64		missed_dead_pages;	/* pages with missed dead tuples */
 			int64		index_vacuum_count; /* number of index vacuumings */
 		}			table;
@@ -214,60 +260,21 @@ typedef struct ExtVacReport
 			int64		pages_deleted;	/* number of pages deleted by vacuum */
 		}			index;
 	} /* per_type_stats */ ;
-}			ExtVacReport;
+}			PgStat_VacuumRelationCounts;
 
-/* ----------
 
-	/*
-	 * Additional cumulative stat on vacuum operations. Use an expensive
-	 * structure as an abstraction for different types of relations.
-	 */
-	ExtVacReport vacuum_ext;
-} PgStat_TableCounts;
+typedef struct PgStat_VacuumDBCounts
+{
+	Oid			dbjid;
+	PgStat_CommonCounts common;
+	int32		errors;
+}			PgStat_VacuumDBCounts;
 
 /* ----------
  * PgStat_TableStatus			Per-table status within a backend
@@ -293,6 +300,12 @@ typedef struct PgStat_TableStatus
 	Relation	relation;		/* rel that is using this entry */
 } PgStat_TableStatus;
 
@@ -489,8 +502,6 @@ typedef struct PgStat_StatDBEntry

 typedef struct PgStat_StatFuncEntry
@@ -578,8 +589,6 @@ typedef struct PgStat_StatTabEntry

 /* ------
@@ -788,7 +797,7 @@ extern void pgstat_unlink_relation(Relation rel);
 
 extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 								 PgStat_Counter deadtuples,
-								 TimestampTz starttime, ExtVacReport * params);
+								 TimestampTz starttime);
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
@@ -924,6 +933,16 @@ extern int	pgstat_get_transactional_drops(bool isCommit, struct xl_xact_stats_it
 extern void pgstat_execute_transactional_drops(int ndrops, struct xl_xact_stats_item *items, bool is_redo);

+extern void pgstat_drop_vacuum_database(Oid databaseid);
+extern void pgstat_vacuum_relation_delete_pending_cb(Oid relid);
+extern void
+			pgstat_report_vacuum_extstats(Oid tableoid, bool shared,
+										  PgStat_VacuumRelationCounts * params);
+extern PgStat_RelationVacuumPending * find_vacuum_relation_entry(Oid relid);
+extern PgStat_VacuumDBCounts * pgstat_prep_vacuum_database_pending(Oid dboid);
+extern PgStat_VacuumRelationCounts * pgstat_fetch_stat_vacuum_tabentry(Oid relid, Oid dbid);
+PgStat_VacuumDBCounts *pgstat_fetch_stat_vacuum_dbentry(Oid dbid);
+
 /*
  * Functions in pgstat_wal.c
  */
@@ -940,7 +959,8 @@ extern PgStat_WalStats *pgstat_fetch_stat_wal(void);
 extern PGDLLIMPORT bool pgstat_track_counts;
 extern PGDLLIMPORT int pgstat_track_functions;
 extern PGDLLIMPORT int pgstat_fetch_consistency;
-
+extern PGDLLIMPORT bool pgstat_track_vacuum_statistics;
+extern PGDLLIMPORT bool pgstat_track_vacuum_statistics_for_relations;
 
 /*
  * Variables in pgstat_bgwriter.c
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index 7dffab8dbdd..4abe70cb54e 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -500,6 +500,18 @@ typedef struct PgStatShared_Relation
 	PgStat_StatTabEntry stats;
 } PgStatShared_Relation;
 
@@ -678,6 +690,9 @@ extern PgStat_EntryRef *pgstat_fetch_pending_entry(PgStat_Kind kind,

 /*

 /* Custom stats kinds */
diff --git a/src/test/recovery/t/050_vacuum_extending_basic_test.pl b/src/test/recovery/t/050_vacuum_extending_basic_test.pl
index bd3cb544e30..e2fd541fd89 100644
--- a/src/test/recovery/t/050_vacuum_extending_basic_test.pl
+++ b/src/test/recovery/t/050_vacuum_extending_basic_test.pl
@@ -28,6 +28,7 @@ $node->init;
 # Configure the server logging level for the test
 $node->append_conf('postgresql.conf', q{
     log_min_messages = notice
+    track_vacuum_statistics = on
 });
 
 my $stderr;
@@ -64,8 +65,9 @@ $node->safe_psql($dbname, q{
 
 $node->safe_psql(
     $dbname,
-    "CREATE TABLE vestat (x int PRIMARY KEY)
+    "CREATE TABLE vestat (x int)
          WITH (autovacuum_enabled = off, fillfactor = 10);
+     create index vestat_pkey on vestat (x);
      INSERT INTO vestat SELECT x FROM generate_series(1, $size_tab) AS g(x);
      ANALYZE vestat;"
 );
@@ -115,7 +117,7 @@ sub wait_for_vacuum_stats {
                 AND
                 (SELECT (tuples_deleted > $idx_tuples_deleted AND wal_records > $idx_wal_records)
                   FROM pg_stat_vacuum_indexes
-                  WHERE relname = 'vestat_pkey');"
+                  WHERE indexrelname = 'vestat_pkey');"
         );
 
         return 1 if ($result_query eq 't');
@@ -183,7 +185,7 @@ sub fetch_vacuum_stats {
         $dbname,
         "SELECT tuples_deleted, pages_deleted, wal_records, wal_bytes, wal_fpi
            FROM pg_stat_vacuum_indexes
-          WHERE relname = 'vestat_pkey';"
+          WHERE indexrelname = 'vestat_pkey';"
     );
 
     $index_base_statistics =~ s/\s*\|\s*/ /g;   # transform " | " into space
@@ -321,7 +323,7 @@ sub fetch_error_base_idx_vacuum_statistics {
     $dbname,
     "SELECT tuples_deleted, pages_deleted
        FROM pg_stat_vacuum_indexes
-      WHERE relname = 'vestat_pkey';"
+      WHERE indexrelname = 'vestat_pkey';"
     );
     $base_statistics =~ s/\s*\|\s*/ /g;   # transform " | " in space
     my ($cur_tuples_deleted, $cur_pages_deleted) = split /\s+/, $base_statistics;
@@ -343,7 +345,7 @@ sub fetch_error_wal_idx_vacuum_statistics {
         $dbname,
         "SELECT wal_records, wal_bytes, wal_fpi
         FROM pg_stat_vacuum_indexes
-        WHERE relname = 'vestat_pkey';"
+        WHERE indexrelname = 'vestat_pkey';"
     );
 
     $wal_raw =~ s/\s*\|\s*/ /g;   # transform " | " in space
@@ -707,7 +709,7 @@ $base_stats = $node->safe_psql(
     'postgres',
     "SELECT count(*) = 0
      FROM pg_stat_vacuum_indexes
-     WHERE relname = 'vestat_pkey';"
+     WHERE indexrelname = 'vestat_pkey';"
 );
 ok($base_stats eq 't', 'check the printing index vacuum extended statistics from another database are not available');
 
@@ -742,6 +744,9 @@ $reloid = $node->safe_psql(
     }
 );
 
+# Run VACUUM on shared table to ensure stats entry is created
+$node->safe_psql($dbname, "VACUUM pg_shdepend;");
+
 # Check if we can get vacuum statistics for cluster relations (dbid = 0)
 $base_stats = $node->safe_psql(
     $dbname,
@@ -760,6 +765,10 @@ my $indoid = $node->safe_psql(
     }
 );
 
+# Run VACUUM on shared index to ensure stats entry is created
+# Note: VACUUM on the table will also vacuum its indexes
+$node->safe_psql($dbname, "VACUUM pg_shdepend;");
+
 $base_stats = $node->safe_psql(
     $dbname,
     qq{
diff --git a/src/test/recovery/t/051_vacuum_extending_freeze_test.pl b/src/test/recovery/t/051_vacuum_extending_freeze_test.pl
index 7528f20098b..2a1c506a22f 100644
--- a/src/test/recovery/t/051_vacuum_extending_freeze_test.pl
+++ b/src/test/recovery/t/051_vacuum_extending_freeze_test.pl
@@ -37,6 +37,7 @@ $node->append_conf('postgresql.conf', q{
 	vacuum_max_eager_freeze_failure_rate = 1.0
 	vacuum_failsafe_age = 0
 	vacuum_multixact_failsafe_age = 0
+  track_vacuum_statistics = on
 });
 
 $node->start();
index b627c85e332..4e8b8b8a2b1 100644
@@ -2330,77 +2330,81 @@ pg_stat_user_tables| SELECT relid,
2.39.5 (Apple Git-154)

From cfbf50c85ae9bfdfb9167dab446bf3256e33ddb1 Mon Sep 17 00:00:00 2001

---

diff --git a/doc/src/sgml/system-views.sgml b/doc/src/sgml/system-views.sgml
index 162c76b729a..50578cae90d 100644
--- a/doc/src/sgml/system-views.sgml
+++ b/doc/src/sgml/system-views.sgml
@@ -5654,4 +5654,759 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx
   </table>
  </sect1>
 
2.39.5 (Apple Git-154)