public inbox for [email protected]
help / color / mirror / Atom feedFrom: Alena Rybakina <[email protected]>
To: pgsql-hackers <[email protected]>
Cc: Amit Kapila <[email protected]>
Cc: Jim Nasby <[email protected]>
Cc: Bertrand Drouvot <[email protected]>
Cc: Kirill Reshke <[email protected]>
Cc: Masahiko Sawada <[email protected]>
Cc: Melanie Plageman <[email protected]>
Cc: jian he <[email protected]>
Cc: Sami Imseih <[email protected]>
Cc: vignesh C <[email protected]>
Cc: Alexander Korotkov <[email protected]>
Cc: Ilia Evdokimov <[email protected]>
Cc: Andrey Borodin <[email protected]>
Cc: Andrei Zubkov <[email protected]>
Cc: Andrei Lepikhov <[email protected]>
Subject: Re: Vacuum statistics
Date: Tue, 28 Apr 2026 05:16:29 +0300
Message-ID: <[email protected]> (raw)
In-Reply-To: <[email protected]>
References: <[email protected]>
<[email protected]>
<[email protected]>
<[email protected]>
<[email protected]>
<[email protected]>
<[email protected]>
<[email protected]>
<[email protected]>
<[email protected]>
<[email protected]>
<[email protected]>
<[email protected]>
<[email protected]>
<[email protected]>
<[email protected]>
<[email protected]>
<[email protected]>
<[email protected]>
Hi, all!
I have updated the core patch that implements the machinery for
collecting extended vacuum statistics (I didn't touch the first patch
that is ready for commit, only patches that are related to extension),
and rebased the ext_vacuum_statistics extension on top of it. The split
is intentional: the core only gathers metrics and hands them out, while
the actual storage and SQL-level access to the statistics live entirely
in the extension. If the extension is not loaded, the overhead is
essentially zero - we only fill a small struct on the stack and do a
NULL check on the hook.
What was updated in the core
The core gains the machinery and the hook through which the extension
receives metrics after each vacuum.
The hook. A new hook has been added in pgstat - set_report_vacuum_hook.
It is fired once per vacuumed table and once per vacuumed index, plus
when forming the per-database aggregate. The extension registers its
handler in _PG_init and by default the hook is NULL, so without an
extension the core behaves exactly as before.
The set of statistics is the same as before. Common to tables, indexes
and the database - hits and misses in shared buffers, number of dirtied
and written pages, WAL volume, buffer read and write times, sleep time
spent in delay points, total wall-clock vacuum time (including I/O and
lock waits), counter of emergency anti-wraparound vacuums, number of
interrupts and removed tuples. Tables additionally report frozen tuples,
pages marked all-frozen / all-visible in the visibility map, number of
scanned and removed pages, number of index passes, etc. Indexes report
freed pages.
The least obvious part of the implementation is subtracting index
statistics from the table statistics. This is the bit worth
highlighting. The thing is that indexes are vacuumed before the heap,
and the buffer and WAL statistics that we capture at the heap level by
the end of the heap vacuum already include everything that was spent on
the indexes. If we simply expose the diff of pgBufferUsage/pgWalUsage
between start and end, the table ends up with double-counted pages/WAL:
once in its own report, and a second time inside the reports of its
indexes. This is especially noticeable with parallel index vacuum:
workers accumulate their usage in the leader only after they finish, so
without subtraction the heap report would receive the combined cost of
all workers as a "bonus".
To handle this, as each index finishes vacuuming, its counters are
accumulated into the state of the current operation, and at the moment
the heap report is built these sums are subtracted out. As a result, the
extension receives clean numbers: "this is what was actually spent on
the table itself", and separately "this is what was actually spent on
each index". The behaviour is idempotent for both serial and parallel
vacuum.
The ext_vacuum_statistics extension
The extension registers the hook handler and stores the received data
through the pgstat custom statistics infrastructure. That is, vacuum
counters are kept not in the extension's own files, but together with
the regular cumulative statistics - they survive a restart and are reset
together with pg_stat_reset_*. Access is provided through three views:
one for tables, one for indexes, and one with the per-database aggregate.
Filtering
This is where the main flexibility lives - the extension does not force
"collect everything", but lets you choose both what to track and which
metrics to keep.
By object type. You can limit collection to databases only (without
per-table detail), to tables only, or collect both. Among tables, you
can additionally filter system / user / all.
By an explicit list. An alternative to "by type" is a whitelist: you
turn the corresponding mode on, and the extension starts collecting
statistics only for the databases and tables that were explicitly
registered via add_track_database / add_track_relation (with matching
remove_* for removal). When the lists are off, the type filter is in
effect; when they are on, only the list applies. This is convenient when
you are interested in monitoring specific "hot" tables and do not want
to spend memory on statistics for everything else.
This list is persisted to disk, and there is one more non-trivial part
here. List changes are concurrent - multiple sessions may call
add_track_* simultaneously, plus there is an object-access hook that
cleans the entry on DROP. To avoid ending up with a torn file, access to
the list is serialized via a dedicated LWLock tranche (requested from a
shmem_request_hook), and the file itself is written atomically: first
into a temporary file, then fflush + pg_fsync + durable_rename. All I/O
return codes are checked; on error the temporary file is removed and the
real one is left untouched; PG_TRY/PG_CATCH guarantees cleanup on
ereport(ERROR). Reading the list takes the same lock in shared mode, so
a concurrent write cannot tear the load.
By metric category. There is also a GUC that takes a list and turns on
the categories of interest - buffers, WAL, general counters, timings (or
all). Unwanted categories are simply skipped on the hook handler side
and never make it into the pgstat entry, which reduces the overhead of
the handler itself. This is useful when, for example, only timings are
needed - in that case the extension does not waste time copying the
buffer and WAL fields.
Privileges. The add_track_* / remove_track_* functions require superuser
or pg_read_all_stats. At the SQL level, EXECUTE is revoked from PUBLIC
and granted only to pg_read_all_stats, so a regular user has no access
to mutating the list. The views are unrestricted, like regular statistics.
What is in the patches
0002-Machinery-for-grabbing-extended-vacuum-statistics.patch - the
machinery in the core plus the hook.
0003-ext_vacuum_statistics-...patch - the extension itself, filtering,
views, tests.
-----------
Best regards,
Alena Rybakina
Yandex Cloud
From 19f5a39f7e97d3fc2d18415ba2c51ffcd3b32f49 Mon Sep 17 00:00:00 2001
From: Alena Rybakina <[email protected]>
Date: Mon, 30 Mar 2026 09:07:24 +0300
Subject: [PATCH 1/3] Track table VM stability.
Add rev_all_visible_pages and rev_all_frozen_pages counters to
pg_stat_all_tables tracking the number of times the all-visible and
all-frozen bits are cleared in the visibility map. These bits are cleared by
backend processes during regular DML operations. Hence, the counters are placed
in table statistic entry.
A high rev_all_visible_pages rate relative to DML volume indicates
that modifications are scattered across previously-clean pages rather
than concentrated on already-dirty ones, causing index-only scans to
fall back to heap fetches. A high rev_all_frozen_pages rate indicates
that vacuum's freezing work is being frequently undone by concurrent
DML.
Authors: Alena Rybakina <[email protected]>,
Andrei Lepikhov <[email protected]>,
Andrei Zubkov <[email protected]>
Reviewed-by: Dilip Kumar <[email protected]>,
Masahiko Sawada <[email protected]>,
Ilia Evdokimov <[email protected]>,
Jian He <[email protected]>,
Kirill Reshke <[email protected]>,
Alexander Korotkov <[email protected]>,
Jim Nasby <[email protected]>,
Sami Imseih <[email protected]>,
Karina Litskevich <[email protected]>,
Andrey Borodin <[email protected]>
---
doc/src/sgml/monitoring.sgml | 32 +++
src/backend/access/heap/visibilitymap.c | 10 +
src/backend/catalog/system_views.sql | 4 +-
src/backend/utils/activity/pgstat_relation.c | 2 +
src/backend/utils/adt/pgstatfuncs.c | 6 +
src/include/catalog/pg_proc.dat | 10 +
src/include/pgstat.h | 17 +-
.../expected/vacuum-extending-freeze.out | 185 ++++++++++++++++++
src/test/isolation/isolation_schedule | 1 +
.../specs/vacuum-extending-freeze.spec | 117 +++++++++++
src/test/regress/expected/rules.out | 12 +-
11 files changed, 391 insertions(+), 5 deletions(-)
create mode 100644 src/test/isolation/expected/vacuum-extending-freeze.out
create mode 100644 src/test/isolation/specs/vacuum-extending-freeze.spec
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 08d5b824552..3467abf6d8a 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -4377,6 +4377,38 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>visible_page_marks_cleared</structfield> <type>bigint</type>
+ </para>
+ <para>
+ Number of times the all-visible mark in the
+ <link linkend="storage-vm">visibility map</link> was cleared for
+ pages of this table. The all-visible mark of a heap page is
+ cleared whenever a backend process modifies a page that was
+ previously marked all-visible by vacuum activity (whether manual
+ <command>VACUUM</command> or autovacuum). The page must then be
+ processed again by vacuum on a subsequent run. A high rate of
+ change in this counter means that vacuum has to repeatedly
+ re-process pages of this table.
+ </para></entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>frozen_page_marks_cleared</structfield> <type>bigint</type>
+ </para>
+ <para>
+ Number of times the all-frozen mark in the
+ <link linkend="storage-vm">visibility map</link> was cleared for
+ pages of this table. The all-frozen mark of a heap page is cleared
+ whenever a backend process modifies a page that was previously
+ marked all-frozen by vacuum activity (manual <command>VACUUM</command>
+ or autovacuum). The page must then be processed again by vacuum on
+ the next freeze run for this table.
+ </para></entry>
+ </row>
+
<row>
<entry role="catalog_table_entry"><para role="column_definition">
<structfield>last_vacuum</structfield> <type>timestamp with time zone</type>
diff --git a/src/backend/access/heap/visibilitymap.c b/src/backend/access/heap/visibilitymap.c
index 4fd470702aa..f055ec3819c 100644
--- a/src/backend/access/heap/visibilitymap.c
+++ b/src/backend/access/heap/visibilitymap.c
@@ -102,6 +102,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"
@@ -173,6 +174,15 @@ visibilitymap_clear(Relation rel, BlockNumber heapBlk, Buffer vmbuf, uint8 flags
if (map[mapByte] & mask)
{
+ /*
+ * Track how often all-visible or all-frozen bits are cleared in the
+ * visibility map.
+ */
+ if (map[mapByte] & ((flags & VISIBILITYMAP_ALL_VISIBLE) << mapOffset))
+ pgstat_count_visible_page_marks_cleared(rel);
+ if (map[mapByte] & ((flags & VISIBILITYMAP_ALL_FROZEN) << mapOffset))
+ pgstat_count_frozen_page_marks_cleared(rel);
+
map[mapByte] &= ~mask;
MarkBufferDirty(vmbuf);
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index 73a1c1c4670..71e993c8783 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -747,7 +747,9 @@ CREATE VIEW pg_stat_all_tables AS
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_visible_page_marks_cleared(C.oid) AS visible_page_marks_cleared,
+ pg_stat_get_frozen_page_marks_cleared(C.oid) AS frozen_page_marks_cleared
FROM pg_class C LEFT JOIN
pg_index I ON C.oid = I.indrelid
LEFT JOIN pg_namespace N ON (N.oid = C.relnamespace)
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index b2ca28f83ba..92e1f60a080 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -881,6 +881,8 @@ 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->visible_page_marks_cleared += lstats->counts.visible_page_marks_cleared;
+ tabentry->frozen_page_marks_cleared += lstats->counts.frozen_page_marks_cleared;
/* Clamp live_tuples in case of negative delta_live_tuples */
tabentry->live_tuples = Max(tabentry->live_tuples, 0);
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 1408de387ea..b6f064338fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -108,6 +108,12 @@ PG_STAT_GET_RELENTRY_INT64(tuples_updated)
/* pg_stat_get_vacuum_count */
PG_STAT_GET_RELENTRY_INT64(vacuum_count)
+/* pg_stat_get_visible_page_marks_cleared */
+PG_STAT_GET_RELENTRY_INT64(visible_page_marks_cleared)
+
+/* pg_stat_get_frozen_page_marks_cleared */
+PG_STAT_GET_RELENTRY_INT64(frozen_page_marks_cleared)
+
#define PG_STAT_GET_RELENTRY_FLOAT8(stat) \
Datum \
CppConcat(pg_stat_get_,stat)(PG_FUNCTION_ARGS) \
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..f8241268017 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12769,4 +12769,14 @@
proname => 'hashoid8extended', prorettype => 'int8',
proargtypes => 'oid8 int8', prosrc => 'hashoid8extended' },
+{ oid => '8002',
+ descr => 'statistics: number of times the all-visible marks in the visibility map were cleared for pages of this table',
+ proname => 'pg_stat_get_visible_page_marks_cleared', provolatile => 's',
+ proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+ prosrc => 'pg_stat_get_visible_page_marks_cleared' },
+{ oid => '8003',
+ descr => 'statistics: number of times the all-frozen marks in the visibility map were cleared for pages of this table',
+ proname => 'pg_stat_get_frozen_page_marks_cleared', provolatile => 's',
+ proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+ prosrc => 'pg_stat_get_frozen_page_marks_cleared' },
]
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index dfa2e837638..7db36cf8add 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -160,6 +160,8 @@ typedef struct PgStat_TableCounts
PgStat_Counter blocks_fetched;
PgStat_Counter blocks_hit;
+ PgStat_Counter visible_page_marks_cleared;
+ PgStat_Counter frozen_page_marks_cleared;
} PgStat_TableCounts;
/* ----------
@@ -218,7 +220,7 @@ typedef struct PgStat_TableXactStatus
* ------------------------------------------------------------
*/
-#define PGSTAT_FILE_FORMAT_ID 0x01A5BCBC
+#define PGSTAT_FILE_FORMAT_ID 0x01A5BCBD
typedef struct PgStat_ArchiverStats
{
@@ -469,6 +471,8 @@ typedef struct PgStat_StatTabEntry
PgStat_Counter blocks_fetched;
PgStat_Counter blocks_hit;
+ PgStat_Counter visible_page_marks_cleared;
+ PgStat_Counter frozen_page_marks_cleared;
TimestampTz last_vacuum_time; /* user initiated vacuum */
PgStat_Counter vacuum_count;
@@ -749,6 +753,17 @@ extern void pgstat_report_analyze(Relation rel,
if (pgstat_should_count_relation(rel)) \
(rel)->pgstat_info->counts.blocks_hit++; \
} while (0)
+/* count revocations of all-visible and all-frozen marks in visibility map */
+#define pgstat_count_visible_page_marks_cleared(rel) \
+ do { \
+ if (pgstat_should_count_relation(rel)) \
+ (rel)->pgstat_info->counts.visible_page_marks_cleared++; \
+ } while (0)
+#define pgstat_count_frozen_page_marks_cleared(rel) \
+ do { \
+ if (pgstat_should_count_relation(rel)) \
+ (rel)->pgstat_info->counts.frozen_page_marks_cleared++; \
+ } 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-freeze.out b/src/test/isolation/expected/vacuum-extending-freeze.out
new file mode 100644
index 00000000000..994a8df56df
--- /dev/null
+++ b/src/test/isolation/expected/vacuum-extending-freeze.out
@@ -0,0 +1,185 @@
+Parsed test spec with 2 sessions
+
+starting permutation: s2_vacuum_freeze s1_get_set_vm_flags_stats s1_update_table s1_get_cleared_vm_flags_stats s2_vacuum_freeze s1_get_set_vm_flags_stats s2_vacuum_freeze s1_select_from_index s2_delete_from_table s1_get_cleared_vm_flags_stats s2_vacuum_freeze s1_get_set_vm_flags_stats s1_commit s1_get_cleared_vm_flags_stats
+pg_stat_force_next_flush
+------------------------
+
+(1 row)
+
+step s2_vacuum_freeze:
+ VACUUM FREEZE vestat;
+
+step s1_get_set_vm_flags_stats:
+ SELECT pg_stat_force_next_flush();
+
+ SELECT c.relallfrozen > frozen_flag_count as relallfrozen, c.relallvisible > all_visibile_flag_count as relallvisible
+ FROM pg_class c, stats_state
+ WHERE c.relname = 'vestat';
+
+ UPDATE stats_state
+ SET frozen_flag_count = c.relallfrozen,
+ all_visibile_flag_count = c.relallvisible
+ FROM pg_class c
+ WHERE c.relname = 'vestat';
+
+pg_stat_force_next_flush
+------------------------
+
+(1 row)
+
+relallfrozen|relallvisible
+------------+-------------
+t |t
+(1 row)
+
+step s1_update_table:
+ UPDATE vestat SET x = x + 1001 where x >= 2500;
+ SELECT pg_stat_force_next_flush();
+
+pg_stat_force_next_flush
+------------------------
+
+(1 row)
+
+step s1_get_cleared_vm_flags_stats:
+ SELECT pg_stat_force_next_flush();
+
+ SELECT v.visible_page_marks_cleared > cleared_all_visibile_flag_count as visible_page_marks_cleared,
+ v.frozen_page_marks_cleared > cleared_frozen_flag_count as frozen_page_marks_cleared
+ FROM pg_stat_all_tables v, stats_state
+ WHERE v.relname = 'vestat';
+
+ UPDATE stats_state
+ SET cleared_all_visibile_flag_count = v.visible_page_marks_cleared,
+ cleared_frozen_flag_count = v.frozen_page_marks_cleared
+ FROM pg_stat_all_tables v
+ WHERE v.relname = 'vestat';
+
+pg_stat_force_next_flush
+------------------------
+
+(1 row)
+
+visible_page_marks_cleared|frozen_page_marks_cleared
+--------------------------+-------------------------
+t |t
+(1 row)
+
+step s2_vacuum_freeze:
+ VACUUM FREEZE vestat;
+
+step s1_get_set_vm_flags_stats:
+ SELECT pg_stat_force_next_flush();
+
+ SELECT c.relallfrozen > frozen_flag_count as relallfrozen, c.relallvisible > all_visibile_flag_count as relallvisible
+ FROM pg_class c, stats_state
+ WHERE c.relname = 'vestat';
+
+ UPDATE stats_state
+ SET frozen_flag_count = c.relallfrozen,
+ all_visibile_flag_count = c.relallvisible
+ FROM pg_class c
+ WHERE c.relname = 'vestat';
+
+pg_stat_force_next_flush
+------------------------
+
+(1 row)
+
+relallfrozen|relallvisible
+------------+-------------
+t |t
+(1 row)
+
+step s2_vacuum_freeze:
+ VACUUM FREEZE vestat;
+
+step s1_select_from_index:
+ BEGIN;
+ SELECT count(x) FROM vestat WHERE x > 2000;
+
+count
+-----
+ 3000
+(1 row)
+
+step s2_delete_from_table:
+ DELETE FROM vestat WHERE x > 4930;
+
+step s1_get_cleared_vm_flags_stats:
+ SELECT pg_stat_force_next_flush();
+
+ SELECT v.visible_page_marks_cleared > cleared_all_visibile_flag_count as visible_page_marks_cleared,
+ v.frozen_page_marks_cleared > cleared_frozen_flag_count as frozen_page_marks_cleared
+ FROM pg_stat_all_tables v, stats_state
+ WHERE v.relname = 'vestat';
+
+ UPDATE stats_state
+ SET cleared_all_visibile_flag_count = v.visible_page_marks_cleared,
+ cleared_frozen_flag_count = v.frozen_page_marks_cleared
+ FROM pg_stat_all_tables v
+ WHERE v.relname = 'vestat';
+
+pg_stat_force_next_flush
+------------------------
+
+(1 row)
+
+visible_page_marks_cleared|frozen_page_marks_cleared
+--------------------------+-------------------------
+f |f
+(1 row)
+
+step s2_vacuum_freeze:
+ VACUUM FREEZE vestat;
+
+step s1_get_set_vm_flags_stats:
+ SELECT pg_stat_force_next_flush();
+
+ SELECT c.relallfrozen > frozen_flag_count as relallfrozen, c.relallvisible > all_visibile_flag_count as relallvisible
+ FROM pg_class c, stats_state
+ WHERE c.relname = 'vestat';
+
+ UPDATE stats_state
+ SET frozen_flag_count = c.relallfrozen,
+ all_visibile_flag_count = c.relallvisible
+ FROM pg_class c
+ WHERE c.relname = 'vestat';
+
+pg_stat_force_next_flush
+------------------------
+
+(1 row)
+
+relallfrozen|relallvisible
+------------+-------------
+f |f
+(1 row)
+
+step s1_commit:
+ COMMIT;
+
+step s1_get_cleared_vm_flags_stats:
+ SELECT pg_stat_force_next_flush();
+
+ SELECT v.visible_page_marks_cleared > cleared_all_visibile_flag_count as visible_page_marks_cleared,
+ v.frozen_page_marks_cleared > cleared_frozen_flag_count as frozen_page_marks_cleared
+ FROM pg_stat_all_tables v, stats_state
+ WHERE v.relname = 'vestat';
+
+ UPDATE stats_state
+ SET cleared_all_visibile_flag_count = v.visible_page_marks_cleared,
+ cleared_frozen_flag_count = v.frozen_page_marks_cleared
+ FROM pg_stat_all_tables v
+ WHERE v.relname = 'vestat';
+
+pg_stat_force_next_flush
+------------------------
+
+(1 row)
+
+visible_page_marks_cleared|frozen_page_marks_cleared
+--------------------------+-------------------------
+t |t
+(1 row)
+
diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule
index 1578ba191c8..91ffc57ebd4 100644
--- a/src/test/isolation/isolation_schedule
+++ b/src/test/isolation/isolation_schedule
@@ -126,3 +126,4 @@ test: serializable-parallel-3
test: matview-write-skew
test: lock-nowait
test: for-portion-of
+test: vacuum-extending-freeze
diff --git a/src/test/isolation/specs/vacuum-extending-freeze.spec b/src/test/isolation/specs/vacuum-extending-freeze.spec
new file mode 100644
index 00000000000..17c204e2326
--- /dev/null
+++ b/src/test/isolation/specs/vacuum-extending-freeze.spec
@@ -0,0 +1,117 @@
+# In short, this test validates the correctness and stability of cumulative
+# vacuum statistics accounting around freezing, visibility, and revision
+# tracking across VACUUM and backend operations.
+# In addition, the test provides a scenario where one process holds a
+# transaction open while another process deletes tuples. We expect that
+# a backend clears the all-frozen and all-visible flags, which were set
+# by VACUUM earlier, only after the committing transaction makes the
+# deletions visible.
+
+setup
+{
+ CREATE TABLE vestat (x int, y int)
+ WITH (autovacuum_enabled = off, fillfactor = 70);
+
+ INSERT INTO vestat
+ SELECT i, i FROM generate_series(1, 5000) AS g(i);
+
+ CREATE INDEX vestat_idx ON vestat (x);
+
+ CREATE TABLE stats_state (frozen_flag_count int, all_visibile_flag_count int,
+ cleared_frozen_flag_count int, cleared_all_visibile_flag_count int);
+ INSERT INTO stats_state VALUES (0,0,0,0);
+ ANALYZE vestat;
+
+ -- Ensure stats are flushed before starting the scenario
+ SELECT pg_stat_force_next_flush();
+}
+
+teardown
+{
+ DROP TABLE IF EXISTS vestat;
+ RESET vacuum_freeze_min_age;
+ RESET vacuum_freeze_table_age;
+
+}
+
+session s1
+
+step s1_get_set_vm_flags_stats
+{
+ SELECT pg_stat_force_next_flush();
+
+ SELECT c.relallfrozen > frozen_flag_count as relallfrozen, c.relallvisible > all_visibile_flag_count as relallvisible
+ FROM pg_class c, stats_state
+ WHERE c.relname = 'vestat';
+
+ UPDATE stats_state
+ SET frozen_flag_count = c.relallfrozen,
+ all_visibile_flag_count = c.relallvisible
+ FROM pg_class c
+ WHERE c.relname = 'vestat';
+}
+
+step s1_get_cleared_vm_flags_stats
+{
+ SELECT pg_stat_force_next_flush();
+
+ SELECT v.visible_page_marks_cleared > cleared_all_visibile_flag_count as visible_page_marks_cleared,
+ v.frozen_page_marks_cleared > cleared_frozen_flag_count as frozen_page_marks_cleared
+ FROM pg_stat_all_tables v, stats_state
+ WHERE v.relname = 'vestat';
+
+ UPDATE stats_state
+ SET cleared_all_visibile_flag_count = v.visible_page_marks_cleared,
+ cleared_frozen_flag_count = v.frozen_page_marks_cleared
+ FROM pg_stat_all_tables v
+ WHERE v.relname = 'vestat';
+}
+
+step s1_select_from_index
+{
+ BEGIN;
+ SELECT count(x) FROM vestat WHERE x > 2000;
+}
+
+step s1_commit
+{
+ COMMIT;
+}
+
+session s2
+setup
+{
+ -- Configure aggressive freezing vacuum behavior
+ SET vacuum_freeze_min_age = 0;
+ SET vacuum_freeze_table_age = 0;
+}
+step s2_delete_from_table
+{
+ DELETE FROM vestat WHERE x > 4930;
+}
+step s2_vacuum_freeze
+{
+ VACUUM FREEZE vestat;
+}
+
+step s1_update_table
+{
+ UPDATE vestat SET x = x + 1001 where x >= 2500;
+ SELECT pg_stat_force_next_flush();
+}
+
+permutation
+ s2_vacuum_freeze
+ s1_get_set_vm_flags_stats
+ s1_update_table
+ s1_get_cleared_vm_flags_stats
+ s2_vacuum_freeze
+ s1_get_set_vm_flags_stats
+ s2_vacuum_freeze
+ s1_select_from_index
+ s2_delete_from_table
+ s1_get_cleared_vm_flags_stats
+ s2_vacuum_freeze
+ s1_get_set_vm_flags_stats
+ s1_commit
+ s1_get_cleared_vm_flags_stats
\ No newline at end of file
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index a65a5bf0c4f..096e4f763f3 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1846,7 +1846,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_visible_page_marks_cleared(c.oid) AS visible_page_marks_cleared,
+ pg_stat_get_frozen_page_marks_cleared(c.oid) AS frozen_page_marks_cleared
FROM ((pg_class c
LEFT JOIN pg_index i ON ((c.oid = i.indrelid)))
LEFT JOIN pg_namespace n ON ((n.oid = c.relnamespace)))
@@ -2357,7 +2359,9 @@ pg_stat_sys_tables| SELECT relid,
total_autovacuum_time,
total_analyze_time,
total_autoanalyze_time,
- stats_reset
+ stats_reset,
+ visible_page_marks_cleared,
+ frozen_page_marks_cleared
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,
@@ -2412,7 +2416,9 @@ pg_stat_user_tables| SELECT relid,
total_autovacuum_time,
total_analyze_time,
total_autoanalyze_time,
- stats_reset
+ stats_reset,
+ visible_page_marks_cleared,
+ frozen_page_marks_cleared
FROM pg_stat_all_tables
WHERE ((schemaname <> ALL (ARRAY['pg_catalog'::name, 'information_schema'::name])) AND (schemaname !~ '^pg_toast'::text));
pg_stat_wal| SELECT wal_records,
--
2.39.5 (Apple Git-154)
From 3a5e0bd82578d1fea63d6bda229dc4d0b224684e Mon Sep 17 00:00:00 2001
From: Alena Rybakina <[email protected]>
Date: Mon, 2 Mar 2026 23:09:32 +0300
Subject: [PATCH 2/3] Machinery for grabbing extended vacuum statistics.
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Add infrastructure inside lazy vacuum to gather extended per-vacuum
metrics and expose them to extensions via a new hook. Core itself
does not persist these metrics — that is the job of an extension
(see ext_vacuum_statistics).
Statistics are gathered separately for tables and indexes according
to vacuum phases. The ExtVacReport union and type field distinguish
PGSTAT_EXTVAC_TABLE vs PGSTAT_EXTVAC_INDEX. Heap vacuum stats are
sent to the cumulative statistics system after vacuum has processed
the indexes. Database vacuum statistics aggregate per-table and
per-index statistics within the database.
Common for tables, indexes, and database: total_blks_hit, total_blks_read
and total_blks_dirtied are the number of hit, miss and dirtied pages
in shared buffers during a vacuum operation. total_blks_dirtied counts
only pages dirtied by this vacuum. blk_read_time and blk_write_time
track access and flush time for buffer pages; blk_write_time can stay
zero if no flushes occurred. total_time is wall-clock time from start
to finish, including idle time (I/O and lock waits). delay_time is
total vacuum sleep time in vacuum delay points.
Both table and index report tuples_deleted (tuples removed by the vacuum),
pages_removed (pages by which relation storage was reduced) and
pages_deleted (freed pages; file size may remain unchanged). These are
independent of WAL and buffer stats and are not summed at the database
level.
Table only: pages_frozen (pages marked all-frozen in the visibility map),
pages_all_visible (pages marked all-visible in the visibility map),
wraparound_failsafe_count (number of urgent anti-wraparound vacuums).
Table and database share wraparound_failsafe (count of urgent anti-wraparound
cleanups). Database only: errors (number of error-level errors caught
during vacuum).
set_report_vacuum_hook (set_report_vacuum_hook_type) -- called
once per vacuumed relation/index with a PgStat_VacuumRelationCounts
payload tagged by ExtVacReportType (PGSTAT_EXTVAC_TABLE / _INDEX /
_DB / _INVALID).
Authors: Alena Rybakina <[email protected]>,
Andrei Lepikhov <[email protected]>,
Andrei Zubkov <[email protected]>
Reviewed-by: Dilip Kumar <[email protected]>,
Masahiko Sawada <[email protected]>,
Ilia Evdokimov <[email protected]>,
jian he <[email protected]>,
Kirill Reshke <[email protected]>,
Alexander Korotkov <[email protected]>,
Jim Nasby <[email protected]>,
Sami Imseih <[email protected]>,
Karina Litskevich <[email protected]>
---
src/backend/access/heap/vacuumlazy.c | 234 ++++++++++++++++++-
src/backend/commands/vacuum.c | 4 +
src/backend/commands/vacuumparallel.c | 12 +
src/backend/utils/activity/pgstat_relation.c | 24 ++
src/include/commands/vacuum.h | 29 +++
src/include/pgstat.h | 69 ++++++
6 files changed, 367 insertions(+), 5 deletions(-)
diff --git a/src/backend/access/heap/vacuumlazy.c b/src/backend/access/heap/vacuumlazy.c
index 39395aed0d5..e4d4c93d641 100644
--- a/src/backend/access/heap/vacuumlazy.c
+++ b/src/backend/access/heap/vacuumlazy.c
@@ -283,6 +283,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 */
@@ -410,6 +412,15 @@ typedef struct LVRelState
* been permanently disabled.
*/
BlockNumber eager_scan_remaining_fails;
+
+ int32 wraparound_failsafe_count; /* # of emergency vacuums for
+ * anti-wraparound */
+
+ /*
+ * We need to accumulate index statistics for later subtraction from heap
+ * stats.
+ */
+ PgStat_VacuumRelationCounts extVacReportIdx;
} LVRelState;
@@ -485,6 +496,166 @@ static void restore_vacuum_error_info(LVRelState *vacrel,
const LVSavedErrInfo *saved_vacrel);
+/* Extended vacuum statistics functions */
+
+/*
+ * extvac_stats_start - Save cut-off values before start of relation processing.
+ */
+static void
+extvac_stats_start(Relation rel, LVExtStatCounters * counters)
+{
+ memset(counters, 0, sizeof(LVExtStatCounters));
+ counters->starttime = GetCurrentTimestamp();
+ counters->walusage = pgWalUsage;
+ counters->bufusage = pgBufferUsage;
+ counters->VacuumDelayTime = VacuumDelayTime;
+ counters->blocks_fetched = 0;
+ counters->blocks_hit = 0;
+
+ if (rel->pgstat_info && pgstat_track_counts)
+ {
+ counters->blocks_fetched = rel->pgstat_info->counts.blocks_fetched;
+ counters->blocks_hit = rel->pgstat_info->counts.blocks_hit;
+ }
+}
+
+/*
+ * extvac_stats_end - Finish extended vacuum statistic gathering and form report.
+ */
+static void
+extvac_stats_end(Relation rel, LVExtStatCounters * counters,
+ PgStat_CommonCounts * report)
+{
+ WalUsage walusage;
+ BufferUsage bufusage;
+ TimestampTz endtime;
+ long secs;
+ int usecs;
+
+ memset(report, 0, sizeof(PgStat_CommonCounts));
+ 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);
+
+ 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) +
+ INSTR_TIME_GET_MILLISEC(bufusage.shared_blk_read_time);
+ report->blk_write_time = INSTR_TIME_GET_MILLISEC(bufusage.local_blk_write_time) +
+ INSTR_TIME_GET_MILLISEC(bufusage.shared_blk_write_time);
+ report->delay_time = VacuumDelayTime - counters->VacuumDelayTime;
+ report->total_time = secs * 1000.0 + usecs / 1000.0;
+
+ if (rel->pgstat_info && pgstat_track_counts)
+ {
+ report->blks_fetched = rel->pgstat_info->counts.blocks_fetched - counters->blocks_fetched;
+ report->blks_hit = rel->pgstat_info->counts.blocks_hit - counters->blocks_hit;
+ }
+}
+
+/*
+ * extvac_stats_start_idx - Start extended vacuum statistic gathering for index.
+ */
+void
+extvac_stats_start_idx(Relation rel, IndexBulkDeleteResult *stats,
+ LVExtStatCountersIdx * counters)
+{
+ extvac_stats_start(rel, &counters->common);
+ counters->pages_deleted = 0;
+ counters->tuples_removed = 0;
+
+ if (stats != NULL)
+ {
+ counters->tuples_removed = stats->tuples_removed;
+ counters->pages_deleted = stats->pages_deleted;
+ }
+}
+
+
+/*
+ * extvac_stats_end_idx - Finish extended vacuum statistic gathering for index.
+ */
+void
+extvac_stats_end_idx(Relation rel, IndexBulkDeleteResult *stats,
+ LVExtStatCountersIdx * counters, PgStat_VacuumRelationCounts * report)
+{
+ memset(report, 0, sizeof(PgStat_VacuumRelationCounts));
+ extvac_stats_end(rel, &counters->common, &report->common);
+ report->type = PGSTAT_EXTVAC_INDEX;
+
+ if (stats != NULL)
+ {
+ report->common.tuples_deleted = stats->tuples_removed - counters->tuples_removed;
+ report->index.pages_deleted = stats->pages_deleted - counters->pages_deleted;
+ }
+}
+
+/*
+ * Accumulate index stats into vacrel for later subtraction from heap stats.
+ * It needs to prevent double-counting of stats for heaps that
+ * include indexes because indexes are vacuumed before the heap.
+ * We need to be careful with buffer usage and wal usage during parallel vacuum
+ * because they are accumulated summarly for all indexes at once by leader after
+ * all workers have finished.
+ */
+static void
+accumulate_idxs_vacuum_statistics(LVRelState *vacrel,
+ PgStat_VacuumRelationCounts * extVacIdxStats)
+{
+ 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;
+}
+
+/* Build heap-specific extended stats */
+static void
+accumulate_heap_vacuum_statistics(LVRelState *vacrel, PgStat_VacuumRelationCounts * extVacStats)
+{
+ 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->new_all_frozen_pages;
+ extVacStats->table.vm_new_visible_pages = vacrel->new_all_visible_pages;
+ extVacStats->table.vm_new_visible_frozen_pages = vacrel->new_all_visible_all_frozen_pages;
+ 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.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->common.wraparound_failsafe_count = vacrel->wraparound_failsafe_count;
+
+ /* Hook is invoked from pgstat_report_vacuum() when extstats is passed */
+
+ /* Subtract index stats from heap to avoid double-counting */
+ 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->common.total_time -= vacrel->extVacReportIdx.common.total_time;
+ extVacStats->common.delay_time -= vacrel->extVacReportIdx.common.delay_time;
+}
/*
* Helper to set up the eager scanning state for vacuuming a single relation.
@@ -643,7 +814,10 @@ heap_vacuum_rel(Relation rel, const VacuumParams *params,
ErrorContextCallback errcallback;
char **indnames = NULL;
Size dead_items_max_bytes = 0;
+ LVExtStatCounters extVacCounters;
+ PgStat_VacuumRelationCounts extVacReport;
+ memset(&extVacReport, 0, sizeof(extVacReport));
verbose = (params->options & VACOPT_VERBOSE) != 0;
instrument = (verbose || (AmAutoVacuumWorkerProcess() &&
params->log_vacuum_min_duration >= 0));
@@ -660,6 +834,9 @@ heap_vacuum_rel(Relation rel, const VacuumParams *params,
/* Used for instrumentation and stats report */
starttime = GetCurrentTimestamp();
+ if (set_report_vacuum_hook)
+ extvac_stats_start(rel, &extVacCounters);
+
pgstat_progress_start_command(PROGRESS_COMMAND_VACUUM,
RelationGetRelid(rel));
if (AmAutoVacuumWorkerProcess())
@@ -687,7 +864,9 @@ 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;
+ memset(&vacrel->extVacReportIdx, 0, sizeof(vacrel->extVacReportIdx));
vacrel->phase = VACUUM_ERRCB_PHASE_UNKNOWN;
vacrel->verbose = verbose;
errcallback.callback = vacuum_error_callback;
@@ -803,6 +982,9 @@ heap_vacuum_rel(Relation rel, const VacuumParams *params,
vacrel->rel_pages = orig_rel_pages = RelationGetNumberOfBlocks(rel);
vacrel->vistest = GlobalVisTestFor(rel);
+ /* Initialize wraparound failsafe count for extended vacuum stats */
+ vacrel->wraparound_failsafe_count = 0;
+
/* Initialize state used to track oldest extant XID/MXID */
vacrel->NewRelfrozenXid = vacrel->cutoffs.OldestXmin;
vacrel->NewRelminMxid = vacrel->cutoffs.OldestMxact;
@@ -985,11 +1167,26 @@ heap_vacuum_rel(Relation rel, const VacuumParams *params,
* soon in cases where the failsafe prevented significant amounts of heap
* vacuuming.
*/
- pgstat_report_vacuum(rel,
- Max(vacrel->new_live_tuples, 0),
- vacrel->recently_dead_tuples +
- vacrel->missed_dead_tuples,
- starttime);
+ if (set_report_vacuum_hook)
+ {
+ extvac_stats_end(rel, &extVacCounters, &extVacReport.common);
+ accumulate_heap_vacuum_statistics(vacrel, &extVacReport);
+
+ pgstat_report_vacuum_ext(rel,
+ Max(vacrel->new_live_tuples, 0),
+ vacrel->recently_dead_tuples +
+ vacrel->missed_dead_tuples,
+ starttime,
+ &extVacReport);
+ }
+ else
+ pgstat_report_vacuum_ext(rel,
+ Max(vacrel->new_live_tuples, 0),
+ vacrel->recently_dead_tuples +
+ vacrel->missed_dead_tuples,
+ starttime,
+ NULL);
+
pgstat_progress_end_command();
if (instrument)
@@ -2903,6 +3100,7 @@ lazy_check_wraparound_failsafe(LVRelState *vacrel)
int64 progress_val[3] = {0, 0, PROGRESS_VACUUM_MODE_FAILSAFE};
VacuumFailsafeActive = true;
+ vacrel->wraparound_failsafe_count++;
/*
* Abandon use of a buffer access strategy to allow use of all of
@@ -3015,7 +3213,11 @@ lazy_vacuum_one_index(Relation indrel, IndexBulkDeleteResult *istat,
{
IndexVacuumInfo ivinfo;
LVSavedErrInfo saved_err_info;
+ LVExtStatCountersIdx extVacCounters;
+ PgStat_VacuumRelationCounts extVacReport;
+ if (set_report_vacuum_hook)
+ extvac_stats_start_idx(indrel, istat, &extVacCounters);
ivinfo.index = indrel;
ivinfo.heaprel = vacrel->rel;
ivinfo.analyze_only = false;
@@ -3033,6 +3235,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);
@@ -3041,6 +3244,14 @@ lazy_vacuum_one_index(Relation indrel, IndexBulkDeleteResult *istat,
istat = vac_bulkdel_one_index(&ivinfo, istat, vacrel->dead_items,
vacrel->dead_items_info);
+ if (set_report_vacuum_hook)
+ {
+ memset(&extVacReport, 0, sizeof(extVacReport));
+ extvac_stats_end_idx(indrel, istat, &extVacCounters, &extVacReport);
+ pgstat_report_vacuum_ext(indrel, -1, -1, 0, &extVacReport);
+ accumulate_idxs_vacuum_statistics(vacrel, &extVacReport);
+ }
+
/* Revert to the previous phase information for error traceback */
restore_vacuum_error_info(vacrel, &saved_err_info);
pfree(vacrel->indname);
@@ -3065,7 +3276,11 @@ lazy_cleanup_one_index(Relation indrel, IndexBulkDeleteResult *istat,
{
IndexVacuumInfo ivinfo;
LVSavedErrInfo saved_err_info;
+ LVExtStatCountersIdx extVacCounters;
+ PgStat_VacuumRelationCounts extVacReport;
+ if (set_report_vacuum_hook)
+ extvac_stats_start_idx(indrel, istat, &extVacCounters);
ivinfo.index = indrel;
ivinfo.heaprel = vacrel->rel;
ivinfo.analyze_only = false;
@@ -3084,12 +3299,21 @@ 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 (set_report_vacuum_hook)
+ {
+ memset(&extVacReport, 0, sizeof(extVacReport));
+ extvac_stats_end_idx(indrel, istat, &extVacCounters, &extVacReport);
+ pgstat_report_vacuum_ext(indrel, -1, -1, 0, &extVacReport);
+ accumulate_idxs_vacuum_statistics(vacrel, &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/commands/vacuum.c b/src/backend/commands/vacuum.c
index 99d0db82ed7..a7fb73173f5 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -118,6 +118,9 @@ pg_atomic_uint32 *VacuumSharedCostBalance = NULL;
pg_atomic_uint32 *VacuumActiveNWorkers = NULL;
int VacuumCostBalanceLocal = 0;
+/* Cumulative storage to report total vacuum delay time (msec). */
+double VacuumDelayTime = 0;
+
/* non-export function prototypes */
static List *expand_vacuum_rel(VacuumRelation *vrel,
MemoryContext vac_context, int options);
@@ -2561,6 +2564,7 @@ vacuum_delay_point(bool is_analyze)
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 41cefcfde54..200f12a2d1b 100644
--- a/src/backend/commands/vacuumparallel.c
+++ b/src/backend/commands/vacuumparallel.c
@@ -1076,6 +1076,8 @@ parallel_vacuum_process_one_index(ParallelVacuumState *pvs, Relation indrel,
IndexBulkDeleteResult *istat = NULL;
IndexBulkDeleteResult *istat_res;
IndexVacuumInfo ivinfo;
+ LVExtStatCountersIdx extVacCounters;
+ PgStat_VacuumRelationCounts extVacReport;
/*
* Update the pointer to the corresponding bulk-deletion result if someone
@@ -1084,6 +1086,8 @@ parallel_vacuum_process_one_index(ParallelVacuumState *pvs, Relation indrel,
if (indstats->istat_updated)
istat = &(indstats->istat);
+ if (set_report_vacuum_hook)
+ extvac_stats_start_idx(indrel, istat, &extVacCounters);
ivinfo.index = indrel;
ivinfo.heaprel = pvs->heaprel;
ivinfo.analyze_only = false;
@@ -1112,6 +1116,13 @@ parallel_vacuum_process_one_index(ParallelVacuumState *pvs, Relation indrel,
RelationGetRelationName(indrel));
}
+ if (set_report_vacuum_hook)
+ {
+ memset(&extVacReport, 0, sizeof(extVacReport));
+ extvac_stats_end_idx(indrel, istat_res, &extVacCounters, &extVacReport);
+ pgstat_report_vacuum_ext(indrel, -1, -1, 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
@@ -1276,6 +1287,7 @@ parallel_vacuum_main(dsm_segment *seg, shm_toc *toc)
VacuumUpdateCosts();
VacuumCostBalance = 0;
+ VacuumDelayTime = 0;
VacuumCostBalanceLocal = 0;
VacuumSharedCostBalance = &(shared->cost_balance);
VacuumActiveNWorkers = &(shared->active_nworkers);
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index 92e1f60a080..226d7aa06d5 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -272,6 +272,30 @@ pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
}
+/*
+ * Hook for extensions to receive extended vacuum statistics.
+ * NULL when no extension has registered.
+ */
+set_report_vacuum_hook_type set_report_vacuum_hook = NULL;
+
+/*
+ * Report extended vacuum statistics to extensions via set_report_vacuum_hook.
+ * When livetuples/deadtuples/starttime are provided (heap case), also calls
+ * pgstat_report_vacuum. For indexes, pass -1, -1, 0 to skip pgstat_report_vacuum.
+ */
+void
+pgstat_report_vacuum_ext(Relation rel, PgStat_Counter livetuples,
+ PgStat_Counter deadtuples, TimestampTz starttime,
+ PgStat_VacuumRelationCounts * extstats)
+{
+ pgstat_report_vacuum(rel, livetuples, deadtuples, starttime);
+
+ if (extstats != NULL && set_report_vacuum_hook)
+ (*set_report_vacuum_hook) (RelationGetRelid(rel),
+ rel->rd_rel->relisshared,
+ extstats);
+}
+
/*
* Report that the table was just analyzed and flush IO statistics.
*
diff --git a/src/include/commands/vacuum.h b/src/include/commands/vacuum.h
index 956d9cea36d..a925f7da992 100644
--- a/src/include/commands/vacuum.h
+++ b/src/include/commands/vacuum.h
@@ -21,9 +21,11 @@
#include "catalog/pg_class.h"
#include "catalog/pg_statistic.h"
#include "catalog/pg_type.h"
+#include "executor/instrument.h"
#include "parser/parse_node.h"
#include "storage/buf.h"
#include "utils/relcache.h"
+#include "pgstat.h"
/*
* Flags for amparallelvacuumoptions to control the participation of bulkdelete
@@ -354,6 +356,33 @@ extern PGDLLIMPORT pg_atomic_uint32 *VacuumSharedCostBalance;
extern PGDLLIMPORT pg_atomic_uint32 *VacuumActiveNWorkers;
extern PGDLLIMPORT int VacuumCostBalanceLocal;
+/* Cumulative storage to report total vacuum delay time (msec). */
+extern PGDLLIMPORT double VacuumDelayTime;
+
+/* Counters for extended vacuum statistics gathering */
+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;
+
+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);
+
extern PGDLLIMPORT bool VacuumFailsafeActive;
extern PGDLLIMPORT double vacuum_cost_delay;
extern PGDLLIMPORT int vacuum_cost_limit;
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 7db36cf8add..8d934973dc1 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -93,6 +93,64 @@ typedef struct PgStat_FunctionCounts
/*
* Working state needed to accumulate per-function-call timing statistics.
*/
+/*
+ * Extended vacuum statistics - passed to extensions via set_report_vacuum_hook.
+ * Type of entry: table (heap), index, or database aggregate.
+ */
+typedef enum ExtVacReportType
+{
+ PGSTAT_EXTVAC_INVALID = 0,
+ PGSTAT_EXTVAC_TABLE = 1,
+ PGSTAT_EXTVAC_INDEX = 2,
+ PGSTAT_EXTVAC_DB = 3,
+} ExtVacReportType;
+
+typedef struct PgStat_CommonCounts
+{
+ int64 total_blks_read;
+ int64 total_blks_hit;
+ int64 total_blks_dirtied;
+ int64 total_blks_written;
+ int64 blks_fetched;
+ int64 blks_hit;
+ int64 wal_records;
+ int64 wal_fpi;
+ uint64 wal_bytes;
+ double blk_read_time;
+ double blk_write_time;
+ double delay_time;
+ double total_time;
+ int32 wraparound_failsafe_count;
+ int32 interrupts_count;
+ int64 tuples_deleted;
+} PgStat_CommonCounts;
+
+typedef struct PgStat_VacuumRelationCounts
+{
+ PgStat_CommonCounts common;
+ ExtVacReportType type;
+ union
+ {
+ struct
+ {
+ int64 tuples_frozen;
+ int64 recently_dead_tuples;
+ int64 missed_dead_tuples;
+ int64 pages_scanned;
+ int64 pages_removed;
+ int64 vm_new_frozen_pages;
+ int64 vm_new_visible_pages;
+ int64 vm_new_visible_frozen_pages;
+ int64 missed_dead_pages;
+ int64 index_vacuum_count;
+ } table;
+ struct
+ {
+ int64 pages_deleted;
+ } index;
+ };
+} PgStat_VacuumRelationCounts;
+
typedef struct PgStat_FunctionCallUsage
{
/* Link to function's hashtable entry (must still be there at exit!) */
@@ -703,6 +761,17 @@ extern void pgstat_unlink_relation(Relation rel);
extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
PgStat_Counter deadtuples,
TimestampTz starttime);
+
+extern void pgstat_report_vacuum_ext(Relation rel,
+ PgStat_Counter livetuples,
+ PgStat_Counter deadtuples,
+ TimestampTz starttime,
+ PgStat_VacuumRelationCounts * extstats);
+
+/* Hook for extensions to receive extended vacuum statistics */
+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;
extern void pgstat_report_analyze(Relation rel,
PgStat_Counter livetuples, PgStat_Counter deadtuples,
bool resetcounter, TimestampTz starttime);
--
2.39.5 (Apple Git-154)
From cf8285d7557582d6995d58ca62599e7e47b20b1b Mon Sep 17 00:00:00 2001
From: Alena Rybakina <[email protected]>
Date: Tue, 28 Apr 2026 03:43:29 +0300
Subject: [PATCH 3/3] ext_vacuum_statistics: extension for extended vacuum
statistics
Introduce a new extension that collects extended per-vacuum
metrics via set_report_vacuum_hook and stores them through pgstat's
custom statistics infrastructure.
Tracking scope is controlled by GUCs:
* vacuum_statistics.enabled -- master switch
* vacuum_statistics.object_types -- databases / relations / all
* vacuum_statistics.track_relations -- system / user / all
* vacuum_statistics.track_{databases,relations}_from_list
-- restrict tracking to objects registered via
add_track_database() / add_track_relation();
removal via remove_track_*() and OAT_DROP hook
* vacuum_statistics.collect -- buffers / wal /
general / timing / all, consulted by ACCUM_IF() to skip
unwanted categories at run time
add_track_* / remove_track_* require superuser or pg_read_all_stats.
---
contrib/Makefile | 1 +
contrib/ext_vacuum_statistics/Makefile | 24 +
contrib/ext_vacuum_statistics/README.md | 165 ++
.../expected/ext_vacuum_statistics.out | 52 +
.../vacuum-extending-in-repetable-read.out | 52 +
.../ext_vacuum_statistics--1.0.sql | 272 ++++
.../ext_vacuum_statistics.conf | 2 +
.../ext_vacuum_statistics.control | 5 +
contrib/ext_vacuum_statistics/meson.build | 41 +
.../vacuum-extending-in-repetable-read.spec | 59 +
.../t/052_vacuum_extending_basic_test.pl | 780 +++++++++
.../t/053_vacuum_extending_freeze_test.pl | 285 ++++
.../t/054_vacuum_extending_gucs_test.pl | 279 ++++
.../ext_vacuum_statistics/vacuum_statistics.c | 1387 +++++++++++++++++
contrib/meson.build | 1 +
doc/src/sgml/contrib.sgml | 1 +
doc/src/sgml/extvacuumstatistics.sgml | 502 ++++++
doc/src/sgml/filelist.sgml | 1 +
18 files changed, 3909 insertions(+)
create mode 100644 contrib/ext_vacuum_statistics/Makefile
create mode 100644 contrib/ext_vacuum_statistics/README.md
create mode 100644 contrib/ext_vacuum_statistics/expected/ext_vacuum_statistics.out
create mode 100644 contrib/ext_vacuum_statistics/expected/vacuum-extending-in-repetable-read.out
create mode 100644 contrib/ext_vacuum_statistics/ext_vacuum_statistics--1.0.sql
create mode 100644 contrib/ext_vacuum_statistics/ext_vacuum_statistics.conf
create mode 100644 contrib/ext_vacuum_statistics/ext_vacuum_statistics.control
create mode 100644 contrib/ext_vacuum_statistics/meson.build
create mode 100644 contrib/ext_vacuum_statistics/specs/vacuum-extending-in-repetable-read.spec
create mode 100644 contrib/ext_vacuum_statistics/t/052_vacuum_extending_basic_test.pl
create mode 100644 contrib/ext_vacuum_statistics/t/053_vacuum_extending_freeze_test.pl
create mode 100644 contrib/ext_vacuum_statistics/t/054_vacuum_extending_gucs_test.pl
create mode 100644 contrib/ext_vacuum_statistics/vacuum_statistics.c
create mode 100644 doc/src/sgml/extvacuumstatistics.sgml
diff --git a/contrib/Makefile b/contrib/Makefile
index 7d91fe77db3..3140f2bf844 100644
--- a/contrib/Makefile
+++ b/contrib/Makefile
@@ -19,6 +19,7 @@ SUBDIRS = \
dict_int \
dict_xsyn \
earthdistance \
+ ext_vacuum_statistics \
file_fdw \
fuzzystrmatch \
hstore \
diff --git a/contrib/ext_vacuum_statistics/Makefile b/contrib/ext_vacuum_statistics/Makefile
new file mode 100644
index 00000000000..ed80bdf28d0
--- /dev/null
+++ b/contrib/ext_vacuum_statistics/Makefile
@@ -0,0 +1,24 @@
+# contrib/ext_vacuum_statistics/Makefile
+
+EXTENSION = ext_vacuum_statistics
+MODULE_big = ext_vacuum_statistics
+OBJS = vacuum_statistics.o
+DATA = ext_vacuum_statistics--1.0.sql
+PGFILEDESC = "ext_vacuum_statistics - convenience views for extended vacuum statistics"
+
+ISOLATION = vacuum-extending-in-repetable-read
+ISOLATION_OPTS = --temp-config=$(top_srcdir)/contrib/ext_vacuum_statistics/ext_vacuum_statistics.conf
+TAP_TESTS = 1
+
+NO_INSTALLCHECK = 1
+
+ifdef USE_PGXS
+PG_CONFIG = pg_config
+PGXS := $(shell $(PG_CONFIG) --pgxs)
+include $(PGXS)
+else
+subdir = contrib/ext_vacuum_statistics
+top_builddir = ../..
+include $(top_builddir)/src/Makefile.global
+include $(top_srcdir)/contrib/contrib-global.mk
+endif
diff --git a/contrib/ext_vacuum_statistics/README.md b/contrib/ext_vacuum_statistics/README.md
new file mode 100644
index 00000000000..51697eab023
--- /dev/null
+++ b/contrib/ext_vacuum_statistics/README.md
@@ -0,0 +1,165 @@
+# ext_vacuum_statistics
+
+Extended vacuum statistics extension for PostgreSQL. It collects and exposes detailed per-table, per-index, and per-database vacuum statistics (buffer I/O, WAL, general, timing) via convenient views in the `ext_vacuum_statistics` schema.
+
+## Installation
+
+```
+./configure tmp_install="$(pwd)/my/inst"
+make clean && make && make install
+cd contrib/ext_vacuum_statistics
+make && make install
+```
+
+It is essential that the extension is listed in `shared_preload_libraries` because it registers a vacuum hook at server startup.
+
+In your `postgresql.conf`:
+
+```
+shared_preload_libraries = 'ext_vacuum_statistics'
+```
+
+Restart PostgreSQL.
+
+In your database:
+
+```sql
+CREATE EXTENSION ext_vacuum_statistics;
+```
+
+## Usage
+
+Query vacuum statistics via the provided views:
+
+```sql
+-- Per-table heap vacuum statistics
+SELECT * FROM ext_vacuum_statistics.pg_stats_vacuum_tables;
+
+-- Per-index vacuum statistics
+SELECT * FROM ext_vacuum_statistics.pg_stats_vacuum_indexes;
+
+-- Per-database aggregate vacuum statistics
+SELECT * FROM ext_vacuum_statistics.pg_stats_vacuum_database;
+```
+
+Example output:
+
+```
+ relname | total_blks_read | total_blks_hit | wal_records | tuples_deleted | pages_removed
+-----------+-----------------+----------------+-------------+----------------+---------------
+ mytable | 120 | 340 | 15 | 500 | 10
+```
+
+Reset statistics when needed:
+
+```sql
+SELECT ext_vacuum_statistics.vacuum_statistics_reset();
+```
+
+## Configuration (GUCs)
+
+| GUC | Default | Description |
+|-----|---------|-------------|
+| `vacuum_statistics.enabled` | on | Enable extended vacuum statistics collection |
+| `vacuum_statistics.object_types` | all | Object types for statistics: `all`, `databases`, `relations` |
+| `vacuum_statistics.track_relations` | all | When tracking relations: `all`, `system`, `user` |
+| `vacuum_statistics.track_databases_from_list` | off | If on, track only databases added via add_track_database |
+| `vacuum_statistics.track_relations_from_list` | off | If on, track only relations added via add_track_relation |
+
+## Memory usage
+
+Each tracked object (table, index, or database) uses approximately **232 bytes** of shared memory on Linux x86_64 (e.g. Ubuntu): common stats (buffers, WAL, timing) ~144 bytes; type + union ~88 bytes (union holds table-specific or index-specific fields, allocated size is the same for both).
+
+The exact size depends on the platform. Call `ext_vacuum_statistics.shared_memory_size()` to get the total shared memory used by the extension. The GUCs provided by the extension allow controlling the amount of memory used: `vacuum_statistics.object_types` to track only databases or relations, `vacuum_statistics.track_relations` to restrict to user or system tables/indexes, and `track_*_from_list` to track only selected databases and relations.
+
+Example: a database with 1000 tables and 2000 indexes, all tracked, uses about **700 KB** on Ubuntu (3001 entries × 232 bytes). Per-database entries add one entry per tracked database.
+
+## Advanced tuning
+
+### Track only database-level stats
+
+```sql
+SET vacuum_statistics.object_types = 'databases';
+```
+
+Statistics are accumulated per database; per-relation views remain empty.
+
+### Track only user or system tables
+
+```sql
+SET vacuum_statistics.object_types = 'relations';
+SET vacuum_statistics.track_relations = 'user'; -- skip system catalogs
+-- or
+SET vacuum_statistics.track_relations = 'system'; -- only system catalogs
+```
+
+### Filter by database or relation OIDs
+
+Add OIDs via functions (persisted to `pg_stat/ext_vacuum_statistics_track.oid`) and enable filtering:
+
+```sql
+-- Add databases and relations to track
+SELECT ext_vacuum_statistics.add_track_database(16384);
+SELECT ext_vacuum_statistics.add_track_relation(16384, 16385); -- dboid, reloid
+SELECT ext_vacuum_statistics.add_track_relation(0, 16386); -- rel 16386 in any db
+
+-- Enable list-based filtering (off = track all)
+SET vacuum_statistics.track_databases_from_list = on;
+SET vacuum_statistics.track_relations_from_list = on;
+```
+
+Remove OIDs when no longer needed:
+
+```sql
+SELECT ext_vacuum_statistics.remove_track_database(16384);
+SELECT ext_vacuum_statistics.remove_track_relation(16384, 16385);
+```
+
+Inspect the current tracking configuration:
+
+```sql
+SELECT * FROM ext_vacuum_statistics.track_list();
+```
+
+Returns `track_kind`, `dboid`, `reloid`. When `dboid` or `reloid` is NULL, statistics are collected for all.
+
+## Recipes
+
+**Reduce overhead by tracking only databases:**
+
+```sql
+SET vacuum_statistics.object_types = 'databases';
+```
+
+**Track only a specific table in a specific database:**
+
+```sql
+SELECT ext_vacuum_statistics.add_track_database(
+ (SELECT oid FROM pg_database WHERE datname = current_database())
+);
+SELECT ext_vacuum_statistics.add_track_relation(
+ (SELECT oid FROM pg_database WHERE datname = current_database()),
+ 'mytable'::regclass
+);
+SET vacuum_statistics.track_databases_from_list = on;
+SET vacuum_statistics.track_relations_from_list = on;
+```
+
+**Disable statistics collection temporarily:**
+
+```sql
+SET vacuum_statistics.enabled = off;
+```
+
+## Views
+
+| View | Description |
+|------|-------------|
+| `ext_vacuum_statistics.pg_stats_vacuum_tables` | Per-table heap vacuum stats (pages scanned, tuples deleted, WAL, timing, etc.) |
+| `ext_vacuum_statistics.pg_stats_vacuum_indexes` | Per-index vacuum stats |
+| `ext_vacuum_statistics.pg_stats_vacuum_database` | Per-database aggregate vacuum stats |
+
+## Limitations
+
+- Must be loaded via `shared_preload_libraries`; it cannot be loaded on demand.
+- Tracking configuration (`add_track_*`, `remove_track_*`) is stored in a file and shared across all databases in the cluster.
diff --git a/contrib/ext_vacuum_statistics/expected/ext_vacuum_statistics.out b/contrib/ext_vacuum_statistics/expected/ext_vacuum_statistics.out
new file mode 100644
index 00000000000..89c9594dea8
--- /dev/null
+++ b/contrib/ext_vacuum_statistics/expected/ext_vacuum_statistics.out
@@ -0,0 +1,52 @@
+-- ext_vacuum_statistics regression test
+
+-- Create extension
+CREATE EXTENSION ext_vacuum_statistics;
+
+-- Verify schema and views exist
+SELECT nspname FROM pg_namespace WHERE nspname = 'ext_vacuum_statistics';
+ nspname
+------------------
+ ext_vacuum_statistics
+(1 row)
+
+-- Views should be queryable (may return empty if no vacuum has run)
+SELECT COUNT(*) >= 0 FROM ext_vacuum_statistics.pg_stats_vacuum_tables;
+ ?column?
+----------
+ t
+(1 row)
+
+SELECT COUNT(*) >= 0 FROM ext_vacuum_statistics.pg_stats_vacuum_indexes;
+ ?column?
+----------
+ t
+(1 row)
+
+SELECT COUNT(*) >= 0 FROM ext_vacuum_statistics.pg_stats_vacuum_database;
+ ?column?
+----------
+ t
+(1 row)
+
+-- Verify views have expected columns
+SELECT COUNT(*) AS tables_cols FROM information_schema.columns
+WHERE table_schema = 'ext_vacuum_statistics' AND table_name = 'tables';
+ tables_cols
+-------------
+ 28
+(1 row)
+
+SELECT COUNT(*) AS indexes_cols FROM information_schema.columns
+WHERE table_schema = 'ext_vacuum_statistics' AND table_name = 'indexes';
+ indexes_cols
+--------------
+ 20
+(1 row)
+
+SELECT COUNT(*) AS database_cols FROM information_schema.columns
+WHERE table_schema = 'ext_vacuum_statistics' AND table_name = 'database';
+ database_cols
+---------------
+ 15
+(1 row)
diff --git a/contrib/ext_vacuum_statistics/expected/vacuum-extending-in-repetable-read.out b/contrib/ext_vacuum_statistics/expected/vacuum-extending-in-repetable-read.out
new file mode 100644
index 00000000000..6b381f9d232
--- /dev/null
+++ b/contrib/ext_vacuum_statistics/expected/vacuum-extending-in-repetable-read.out
@@ -0,0 +1,52 @@
+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 ext_vacuum_statistics.pg_stats_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
+-------+--------------+--------------------+------------------+-----------------+-------------
+(0 rows)
+
+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 ext_vacuum_statistics.pg_stats_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 ext_vacuum_statistics.pg_stats_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/contrib/ext_vacuum_statistics/ext_vacuum_statistics--1.0.sql b/contrib/ext_vacuum_statistics/ext_vacuum_statistics--1.0.sql
new file mode 100644
index 00000000000..aa3a9ec9699
--- /dev/null
+++ b/contrib/ext_vacuum_statistics/ext_vacuum_statistics--1.0.sql
@@ -0,0 +1,272 @@
+/*-------------------------------------------------------------------------
+ *
+ * ext_vacuum_statistics--1.0.sql
+ * Extended vacuum statistics via hook and custom storage
+ *
+ * This extension collects extended vacuum statistics via set_report_vacuum_hook
+ * and stores them in shared memory.
+ *
+ *-------------------------------------------------------------------------
+ */
+
+\echo Use "CREATE EXTENSION ext_vacuum_statistics" to load this file. \quit
+
+CREATE SCHEMA IF NOT EXISTS ext_vacuum_statistics;
+
+COMMENT ON SCHEMA ext_vacuum_statistics IS
+ 'Extended vacuum statistics (heap, index, database)';
+
+-- Reset functions
+CREATE OR REPLACE FUNCTION ext_vacuum_statistics.extvac_reset_entry(
+ dboid oid,
+ relid oid,
+ type int4
+)
+RETURNS boolean
+AS 'MODULE_PATHNAME', 'extvac_reset_entry'
+LANGUAGE C STRICT PARALLEL SAFE;
+
+CREATE OR REPLACE FUNCTION ext_vacuum_statistics.extvac_reset_db_entry(dboid oid)
+RETURNS bigint
+AS 'MODULE_PATHNAME', 'extvac_reset_db_entry'
+LANGUAGE C STRICT PARALLEL SAFE;
+
+CREATE OR REPLACE FUNCTION ext_vacuum_statistics.vacuum_statistics_reset()
+RETURNS bigint
+AS 'MODULE_PATHNAME', 'vacuum_statistics_reset'
+LANGUAGE C STRICT PARALLEL SAFE;
+
+CREATE OR REPLACE FUNCTION ext_vacuum_statistics.shared_memory_size()
+RETURNS bigint
+AS 'MODULE_PATHNAME', 'extvac_shared_memory_size'
+LANGUAGE C STRICT PARALLEL SAFE;
+
+COMMENT ON FUNCTION ext_vacuum_statistics.shared_memory_size() IS
+ 'Total shared memory in bytes used by the extension for vacuum statistics.';
+
+-- Add/remove OIDs for tracking
+CREATE OR REPLACE FUNCTION ext_vacuum_statistics.add_track_database(dboid oid)
+RETURNS boolean
+AS 'MODULE_PATHNAME', 'evs_add_track_database'
+LANGUAGE C STRICT;
+
+CREATE OR REPLACE FUNCTION ext_vacuum_statistics.remove_track_database(dboid oid)
+RETURNS boolean
+AS 'MODULE_PATHNAME', 'evs_remove_track_database'
+LANGUAGE C STRICT;
+
+CREATE OR REPLACE FUNCTION ext_vacuum_statistics.add_track_relation(dboid oid, reloid oid)
+RETURNS boolean
+AS 'MODULE_PATHNAME', 'evs_add_track_relation'
+LANGUAGE C STRICT;
+
+CREATE OR REPLACE FUNCTION ext_vacuum_statistics.remove_track_relation(dboid oid, reloid oid)
+RETURNS boolean
+AS 'MODULE_PATHNAME', 'evs_remove_track_relation'
+LANGUAGE C STRICT;
+
+CREATE OR REPLACE FUNCTION ext_vacuum_statistics.track_list()
+RETURNS TABLE(track_kind text, dboid oid, reloid oid)
+AS 'MODULE_PATHNAME', 'evs_track_list'
+LANGUAGE C STRICT;
+
+COMMENT ON FUNCTION ext_vacuum_statistics.track_list() IS
+ 'List of database and relation OIDs for which vacuum statistics are collected.';
+
+-- Track-list mutation requires superuser or pg_read_all_stats; hide the
+-- functions from PUBLIC so the error is also produced for ordinary users
+-- before the C-level privilege check runs.
+REVOKE ALL ON FUNCTION ext_vacuum_statistics.add_track_database(oid) FROM PUBLIC;
+REVOKE ALL ON FUNCTION ext_vacuum_statistics.remove_track_database(oid) FROM PUBLIC;
+REVOKE ALL ON FUNCTION ext_vacuum_statistics.add_track_relation(oid, oid) FROM PUBLIC;
+REVOKE ALL ON FUNCTION ext_vacuum_statistics.remove_track_relation(oid, oid) FROM PUBLIC;
+GRANT EXECUTE ON FUNCTION ext_vacuum_statistics.add_track_database(oid) TO pg_read_all_stats;
+GRANT EXECUTE ON FUNCTION ext_vacuum_statistics.remove_track_database(oid) TO pg_read_all_stats;
+GRANT EXECUTE ON FUNCTION ext_vacuum_statistics.add_track_relation(oid, oid) TO pg_read_all_stats;
+GRANT EXECUTE ON FUNCTION ext_vacuum_statistics.remove_track_relation(oid, oid) TO pg_read_all_stats;
+
+-- Internal C function to fetch table vacuum stats
+CREATE OR REPLACE FUNCTION ext_vacuum_statistics.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 STABLE;
+
+-- Internal C function to fetch index vacuum stats
+CREATE OR REPLACE FUNCTION ext_vacuum_statistics.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 STABLE;
+
+-- Internal C function to fetch database vacuum stats
+CREATE OR REPLACE FUNCTION ext_vacuum_statistics.pg_stats_get_vacuum_database(
+ IN dboid oid,
+ OUT dbid 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 interrupts_count integer
+)
+RETURNS SETOF record
+AS 'MODULE_PATHNAME', 'pg_stats_get_vacuum_database'
+LANGUAGE C STRICT STABLE;
+
+-- View: vacuum statistics per table (heap)
+CREATE VIEW ext_vacuum_statistics.pg_stats_vacuum_tables AS
+SELECT
+ rel.oid AS relid,
+ ns.nspname AS schema,
+ rel.relname AS relname,
+ db.datname AS dbname,
+ 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_database db,
+ pg_class rel,
+ pg_namespace ns,
+ LATERAL ext_vacuum_statistics.pg_stats_get_vacuum_tables(db.oid, rel.oid) stats
+WHERE db.datname = current_database()
+ AND rel.relkind = 'r'
+ AND rel.relnamespace = ns.oid
+ AND rel.oid = stats.relid;
+
+COMMENT ON VIEW ext_vacuum_statistics.pg_stats_vacuum_tables IS
+ 'Extended vacuum statistics per table (heap)';
+
+-- View: vacuum statistics per index
+CREATE VIEW ext_vacuum_statistics.pg_stats_vacuum_indexes AS
+SELECT
+ rel.oid AS indexrelid,
+ ns.nspname AS schema,
+ rel.relname AS indexrelname,
+ db.datname AS dbname,
+ 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_database db,
+ pg_class rel,
+ pg_namespace ns,
+ LATERAL ext_vacuum_statistics.pg_stats_get_vacuum_indexes(db.oid, rel.oid) stats
+WHERE db.datname = current_database()
+ AND rel.relkind = 'i'
+ AND rel.relnamespace = ns.oid
+ AND rel.oid = stats.relid;
+
+COMMENT ON VIEW ext_vacuum_statistics.pg_stats_vacuum_indexes IS
+ 'Extended vacuum statistics per index';
+
+-- View: vacuum statistics per database (aggregate)
+CREATE VIEW ext_vacuum_statistics.pg_stats_vacuum_database AS
+SELECT
+ db.oid AS dboid,
+ db.datname AS dbname,
+ stats.total_blks_read AS db_blks_read,
+ stats.total_blks_hit AS db_blks_hit,
+ stats.total_blks_dirtied AS db_blks_dirtied,
+ stats.total_blks_written AS db_blks_written,
+ stats.wal_records AS db_wal_records,
+ stats.wal_fpi AS db_wal_fpi,
+ stats.wal_bytes AS db_wal_bytes,
+ stats.blk_read_time AS db_blk_read_time,
+ stats.blk_write_time AS db_blk_write_time,
+ stats.delay_time AS db_delay_time,
+ stats.total_time AS db_total_time,
+ stats.wraparound_failsafe_count AS db_wraparound_failsafe_count,
+ stats.interrupts_count
+FROM pg_database db
+LEFT JOIN LATERAL ext_vacuum_statistics.pg_stats_get_vacuum_database(db.oid) stats ON db.oid = stats.dbid;
+
+COMMENT ON VIEW ext_vacuum_statistics.pg_stats_vacuum_database IS
+ 'Extended vacuum statistics per database (aggregate)';
diff --git a/contrib/ext_vacuum_statistics/ext_vacuum_statistics.conf b/contrib/ext_vacuum_statistics/ext_vacuum_statistics.conf
new file mode 100644
index 00000000000..9b711487623
--- /dev/null
+++ b/contrib/ext_vacuum_statistics/ext_vacuum_statistics.conf
@@ -0,0 +1,2 @@
+# Config for ext_vacuum_statistics regression tests
+shared_preload_libraries = 'ext_vacuum_statistics'
diff --git a/contrib/ext_vacuum_statistics/ext_vacuum_statistics.control b/contrib/ext_vacuum_statistics/ext_vacuum_statistics.control
new file mode 100644
index 00000000000..518350a64b7
--- /dev/null
+++ b/contrib/ext_vacuum_statistics/ext_vacuum_statistics.control
@@ -0,0 +1,5 @@
+# ext_vacuum_statistics extension
+comment = 'Extended vacuum statistics via hook (requires shared_preload_libraries)'
+default_version = '1.0'
+relocatable = true
+module_pathname = '$libdir/ext_vacuum_statistics'
diff --git a/contrib/ext_vacuum_statistics/meson.build b/contrib/ext_vacuum_statistics/meson.build
new file mode 100644
index 00000000000..72338baa500
--- /dev/null
+++ b/contrib/ext_vacuum_statistics/meson.build
@@ -0,0 +1,41 @@
+# Copyright (c) 2022-2026, PostgreSQL Global Development Group
+#
+# ext_vacuum_statistics - extended vacuum statistics via hook
+# Requires shared_preload_libraries = 'ext_vacuum_statistics'
+
+ext_vacuum_statistics_sources = files(
+ 'vacuum_statistics.c',
+)
+
+ext_vacuum_statistics = shared_module('ext_vacuum_statistics',
+ ext_vacuum_statistics_sources,
+ kwargs: contrib_mod_args + {
+ 'dependencies': contrib_mod_args['dependencies'],
+ },
+)
+contrib_targets += ext_vacuum_statistics
+
+install_data(
+ 'ext_vacuum_statistics.control',
+ 'ext_vacuum_statistics--1.0.sql',
+ kwargs: contrib_data_args,
+)
+
+tests += {
+ 'name': 'ext_vacuum_statistics',
+ 'sd': meson.current_source_dir(),
+ 'bd': meson.current_build_dir(),
+ 'isolation': {
+ 'specs': [
+ 'vacuum-extending-in-repetable-read',
+ ],
+ 'regress_args': ['--temp-config', files('ext_vacuum_statistics.conf')],
+ 'runningcheck': false,
+ },
+ 'tap': {
+ 'tests': [
+ 't/052_vacuum_extending_basic_test.pl',
+ 't/053_vacuum_extending_freeze_test.pl',
+ ],
+ },
+}
diff --git a/contrib/ext_vacuum_statistics/specs/vacuum-extending-in-repetable-read.spec b/contrib/ext_vacuum_statistics/specs/vacuum-extending-in-repetable-read.spec
new file mode 100644
index 00000000000..4891e248cca
--- /dev/null
+++ b/contrib/ext_vacuum_statistics/specs/vacuum-extending-in-repetable-read.spec
@@ -0,0 +1,59 @@
+# Test for checking recently_dead_tuples, tuples_deleted and frozen tuples in ext_vacuum_statistics.pg_stats_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);
+ CREATE EXTENSION ext_vacuum_statistics;
+ SET track_io_timing = on;
+}
+
+teardown
+{
+ DROP EXTENSION ext_vacuum_statistics CASCADE;
+ DROP TABLE test_vacuum_stat_isolation CASCADE;
+ RESET track_io_timing;
+}
+
+session s1
+setup {
+ SET track_io_timing = on;
+}
+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
+setup {
+ SET track_io_timing = 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; }
+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 ext_vacuum_statistics.pg_stats_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
diff --git a/contrib/ext_vacuum_statistics/t/052_vacuum_extending_basic_test.pl b/contrib/ext_vacuum_statistics/t/052_vacuum_extending_basic_test.pl
new file mode 100644
index 00000000000..9463d5145f4
--- /dev/null
+++ b/contrib/ext_vacuum_statistics/t/052_vacuum_extending_basic_test.pl
@@ -0,0 +1,780 @@
+# 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 heap tables, indexes, and databases using:
+#
+# • ext_vacuum_statistics.pg_stats_vacuum_tables
+# • ext_vacuum_statistics.pg_stats_vacuum_indexes
+# • ext_vacuum_statistics.pg_stats_vacuum_database
+#
+# 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: preload extension and logging level
+$node->append_conf('postgresql.conf', q{
+ shared_preload_libraries = 'ext_vacuum_statistics'
+ 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;
+ CREATE EXTENSION ext_vacuum_statistics;
+});
+# 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 EXTENSION ext_vacuum_statistics;
+ 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;"
+);
+
+#------------------------------------------------------------------------------
+# 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 ext_vacuum_statistics.pg_stats_vacuum_tables and ext_vacuum_statistics.pg_stats_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.
+#------------------------------------------------------------------------------
+
+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) {
+
+ my $result_query = $node->safe_psql(
+ $dbname,
+ "VACUUM vestat;
+ SELECT
+ (SELECT (tuples_deleted > $tab_tuples_deleted AND wal_records > $tab_wal_records)
+ FROM ext_vacuum_statistics.pg_stats_vacuum_tables
+ WHERE relname = 'vestat')
+ AND
+ (SELECT (tuples_deleted > $idx_tuples_deleted AND wal_records > $idx_wal_records)
+ FROM ext_vacuum_statistics.pg_stats_vacuum_indexes
+ WHERE indexrelname = 'vestat_pkey');"
+ );
+
+ return 1 if ($result_query eq 't');
+
+ sleep($interval);
+ }
+
+ return 0;
+}
+
+#------------------------------------------------------------------------------
+# Variables to hold vacuum-stat snapshots for later comparisons
+#------------------------------------------------------------------------------
+
+my $vm_new_visible_frozen_pages = 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 $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 $vm_new_visible_frozen_pages_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;
+
+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 and its
+# primary index, 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_visible_frozen_pages, tuples_deleted, pages_scanned, pages_removed, wal_records, wal_bytes, wal_fpi
+ FROM ext_vacuum_statistics.pg_stats_vacuum_tables
+ WHERE relname = 'vestat';"
+ );
+
+ $base_statistics =~ s/\s*\|\s*/ /g; # transform " | " into space
+ ($vm_new_visible_frozen_pages, $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 ext_vacuum_statistics.pg_stats_vacuum_indexes
+ WHERE indexrelname = '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;
+}
+
+#------------------------------------------------------------------------------
+# 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 {
+ $vm_new_visible_frozen_pages_prev = $vm_new_visible_frozen_pages;
+ $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;
+
+ $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;
+}
+
+#------------------------------------------------------------------------------
+# 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" .
+ " vm_new_visible_frozen_pages = $vm_new_visible_frozen_pages_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" .
+ " vm_new_visible_frozen_pages = $vm_new_visible_frozen_pages\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" .
+ "Index statistics:\n" .
+ " Before test:\n" .
+ " tuples_deleted = $index_tuples_deleted_prev\n" .
+ " pages_deleted = $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_deleted = $index_pages_deleted\n" .
+ " wal_records = $index_wal_records\n" .
+ " wal_bytes = $index_wal_bytes\n" .
+ " wal_fpi = $index_wal_fpi\n"
+ );
+};
+
+sub fetch_error_base_db_vacuum_statistics {
+ my (%args) = @_;
+
+ # Validate presence of required args (allow 0 as valid numeric baseline)
+ die "database name required"
+ unless exists $args{database_name} && defined $args{database_name};
+ my $database_name = $args{database_name};
+
+ # fetch actual base database vacuum statistics
+ my $base_statistics = $node->safe_psql(
+ $database_name,
+ "SELECT db_blks_hit, db_blks_dirtied,
+ db_blks_written, db_wal_records,
+ db_wal_fpi, db_wal_bytes
+ FROM ext_vacuum_statistics.pg_stats_vacuum_database, pg_database
+ WHERE pg_database.datname = '$dbname'
+ AND pg_database.oid = ext_vacuum_statistics.pg_stats_vacuum_database.dboid;"
+ );
+ $base_statistics =~ s/\s*\|\s*/ /g; # transform " | " in space
+ my ($db_blks_hit, $total_blks_dirtied, $total_blks_written,
+ $wal_records, $wal_fpi, $wal_bytes) = split /\s+/, $base_statistics;
+
+ diag(
+ "BASE STATS MISMATCH FOR DATABASE $dbname:\n" .
+ " db_blks_hit = $db_blks_hit\n" .
+ " total_blks_dirtied = $total_blks_dirtied\n" .
+ " total_blks_written = $total_blks_written\n" .
+ " wal_records = $wal_records\n" .
+ " wal_fpi = $wal_fpi\n" .
+ " wal_bytes = $wal_bytes\n"
+ );
+}
+
+
+#------------------------------------------------------------------------------
+# Test 1: Delete half the rows, run VACUUM, and wait for stats to advance
+#------------------------------------------------------------------------------
+subtest 'Test 1: Delete half the rows, run VACUUM' => 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,
+ 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 ext_vacuum_statistics update after $timeout seconds after vacuuming half-deleted table";
+
+fetch_vacuum_stats();
+
+ok($vm_new_visible_frozen_pages == $vm_new_visible_frozen_pages_prev, 'table vm_new_visible_frozen_pages 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');
+
+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();
+
+#------------------------------------------------------------------------------
+# Test 2: Delete all rows, run VACUUM, and wait for stats to advance
+#------------------------------------------------------------------------------
+subtest 'Test 2: Delete all rows, run VACUUM' => sub
+{
+save_vacuum_stats();
+
+$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,
+ 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)')
+ or diag "Timeout waiting for ext_vacuum_statistics update after $timeout seconds after vacuuming all-deleted table";
+
+fetch_vacuum_stats();
+
+ok($vm_new_visible_frozen_pages > $vm_new_visible_frozen_pages_prev, 'table vm_new_visible_frozen_pages has increased');
+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 > 0, '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 stay the same');
+
+} or print_vacuum_stats_on_error();
+
+#------------------------------------------------------------------------------
+# 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
+{
+save_vacuum_stats();
+
+$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;"
+);
+
+fetch_vacuum_stats();
+
+ok($vm_new_visible_frozen_pages == $vm_new_visible_frozen_pages_prev, 'table vm_new_visible_frozen_pages 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');
+
+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();
+
+#------------------------------------------------------------------------------
+# 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
+{
+
+save_vacuum_stats();
+
+$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_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 updating tuples in the table (tuples_deleted and wal_records advanced)')
+ or diag "Timeout waiting for ext_vacuum_statistics update after $timeout seconds";
+
+fetch_vacuum_stats();
+
+ok($vm_new_visible_frozen_pages == $vm_new_visible_frozen_pages_prev, 'table vm_new_visible_frozen_pages 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');
+
+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();
+
+#------------------------------------------------------------------------------
+# Test 5: Update table, trancate and vacuuming
+#------------------------------------------------------------------------------
+subtest 'Test 5: Update table, trancate and vacuuming' => sub
+{
+
+save_vacuum_stats();
+
+$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_wal_records => $wal_records_prev,
+);
+
+ok($updated, 'vacuum stats updated after updating tuples and trancation in the table (wal_records advanced)')
+ or diag "Timeout waiting for ext_vacuum_statistics update after $timeout seconds";
+
+fetch_vacuum_stats();
+
+ok($vm_new_visible_frozen_pages == $vm_new_visible_frozen_pages_prev, 'table vm_new_visible_frozen_pages 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');
+
+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();
+
+#------------------------------------------------------------------------------
+# Test 6: Delete all tuples from table, trancate, and vacuuming
+#------------------------------------------------------------------------------
+subtest 'Test 6: Delete all tuples from table, trancate, and vacuuming' => sub
+{
+
+save_vacuum_stats();
+
+$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_wal_records => $wal_records,
+);
+
+ok($updated, 'vacuum stats updated after deleting all tuples and trancation in the table (wal_records advanced)')
+ or diag "Timeout waiting for ext_vacuum_statistics update after $timeout seconds";
+
+fetch_vacuum_stats();
+
+ok($vm_new_visible_frozen_pages == $vm_new_visible_frozen_pages_prev, 'table vm_new_visible_frozen_pages 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');
+
+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();
+
+my $dboid = $node->safe_psql(
+ $dbname,
+ "SELECT oid FROM pg_database WHERE datname = current_database();"
+);
+
+#-------------------------------------------------------------------------------------------------------
+# Test 7: Check if we return single vacuum statistics for particular relation from the current database
+#-------------------------------------------------------------------------------------------------------
+subtest 'Test 7: Check if we return vacuum statistics from the current database' => sub
+{
+save_vacuum_stats();
+
+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 relation in the current database
+$base_stats = $node->safe_psql(
+ $dbname,
+ "SELECT count(*) FROM ext_vacuum_statistics.pg_stats_get_vacuum_tables((SELECT oid FROM pg_database WHERE datname = current_database()), $reloid);"
+);
+is($base_stats, 1, '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(*) FROM ext_vacuum_statistics.pg_stats_get_vacuum_indexes((SELECT oid FROM pg_database WHERE datname = current_database()), $reloid);"
+);
+is($base_stats, 1, '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(*) FROM ext_vacuum_statistics.pg_stats_get_vacuum_tables((SELECT oid FROM pg_database WHERE datname = current_database()), 1);"
+);
+is($base_stats, 0, 'table vacuum stats return no rows, as expected');
+
+$base_stats = $node->safe_psql(
+ $dbname,
+ "SELECT count(*) FROM ext_vacuum_statistics.pg_stats_get_vacuum_indexes((SELECT oid FROM pg_database WHERE datname = current_database()), 1);"
+);
+is($base_stats, 0, 'index vacuum stats return no rows, as expected');
+
+# Check if we can get vacuum statistics of all relations in the current database
+$base_stats = $node->safe_psql(
+ $dbname,
+ "SELECT count(*) > 0 FROM ext_vacuum_statistics.pg_stats_vacuum_tables;"
+);
+ok($base_stats eq 't', 'vacuum stats per all heap objects available');
+
+$base_stats = $node->safe_psql(
+ $dbname,
+ "SELECT count(*) > 0 FROM ext_vacuum_statistics.pg_stats_vacuum_indexes;"
+);
+ok($base_stats eq 't', 'vacuum stats per all index objects available');
+};
+
+#------------------------------------------------------------------------------
+# Test 8: Check relation-level vacuum statistics from another database
+#------------------------------------------------------------------------------
+subtest 'Test 8: Check relation-level vacuum statistics from another database' => sub
+{
+$base_stats = $node->safe_psql(
+ 'postgres',
+ "SELECT count(*)
+ FROM ext_vacuum_statistics.pg_stats_vacuum_indexes
+ WHERE indexrelname = 'vestat_pkey';"
+);
+is($base_stats, 0, 'check the printing index vacuum extended statistics from another database are not available');
+
+$base_stats = $node->safe_psql(
+ 'postgres',
+ "SELECT count(*)
+ FROM ext_vacuum_statistics.pg_stats_vacuum_tables
+ WHERE relname = 'vestat';"
+);
+is($base_stats, 0, 'check the printing heap vacuum extended statistics from another database are not available');
+
+# Check that relations from another database are not visible in the view when querying from postgres
+$base_stats = $node->safe_psql(
+ 'postgres',
+ "SELECT count(*) FROM ext_vacuum_statistics.pg_stats_vacuum_tables WHERE relname = 'vestat';"
+);
+is($base_stats, 0, 'vacuum stats per all tables objects from another database are not available as expected');
+
+$base_stats = $node->safe_psql(
+ 'postgres',
+ "SELECT count(*) FROM ext_vacuum_statistics.pg_stats_vacuum_indexes WHERE indexrelname = 'vestat_pkey';"
+);
+is($base_stats, 0, 'vacuum stats per all index objects from another database are not available as expected');
+};
+
+#--------------------------------------------------------------------------------------
+# Test 9: Check database-level vacuum statistics from the current and another database
+#--------------------------------------------------------------------------------------
+subtest 'Test 9: Check database-level vacuum statistics from the current and another database' => sub
+{
+my $db_blk_hit = 0;
+my $total_blks_dirtied = 0;
+my $total_blks_written = 0;
+my $wal_records = 0;
+my $wal_fpi = 0;
+my $wal_bytes = 0;
+$base_stats = $node->safe_psql(
+ $dbname,
+ "SELECT db_blks_hit, db_blks_dirtied,
+ db_blks_written, db_wal_records,
+ db_wal_fpi, db_wal_bytes
+ FROM ext_vacuum_statistics.pg_stats_vacuum_database, pg_database
+ WHERE pg_database.datname = '$dbname'
+ AND pg_database.oid = ext_vacuum_statistics.pg_stats_vacuum_database.dboid;"
+);
+$base_stats =~ s/\s*\|\s*/ /g; # transform " | " into space
+ ($db_blk_hit, $total_blks_dirtied, $total_blks_written, $wal_records, $wal_fpi, $wal_bytes)
+ = split /\s+/, $base_stats;
+
+ok($db_blk_hit > 0, 'db_blks_hit is more than 0');
+ok($total_blks_dirtied > 0, 'total_blks_dirtied is more than 0');
+ok($total_blks_written > 0, 'total_blks_written is more than 0');
+ok($wal_records > 0, 'wal_records is more than 0');
+ok($wal_fpi > 0, 'wal_fpi is more than 0');
+ok($wal_bytes > 0, 'wal_bytes is more than 0');
+
+$base_stats = $node->safe_psql(
+ 'postgres',
+ "SELECT count(*) = 1
+ FROM ext_vacuum_statistics.pg_stats_vacuum_database, pg_database
+ WHERE pg_database.datname = '$dbname'
+ AND pg_database.oid = ext_vacuum_statistics.pg_stats_vacuum_database.dboid;"
+);
+ok($base_stats eq 't', 'check database-level vacuum stats from another database are available');
+};
+
+#------------------------------------------------------------------------------
+# Test 10: Cleanup checks: ensure functions return empty sets for OID = 0
+#------------------------------------------------------------------------------
+subtest 'Test 10: Cleanup checks: ensure functions return empty sets for OID = 0' => sub
+{
+my $dboid = $node->safe_psql(
+ $dbname,
+ "SELECT oid FROM pg_database WHERE datname = current_database();"
+);
+
+# Vacuum statistics for invalid relation OID return empty
+$base_stats = $node->safe_psql(
+ $dbname,
+ q{
+ SELECT COUNT(*)
+ FROM ext_vacuum_statistics.pg_stats_get_vacuum_tables((SELECT oid FROM pg_database WHERE datname = current_database()), 0);
+ }
+);
+is($base_stats, 0, 'vacuum stats per heap from invalid relation OID return empty as expected');
+
+$base_stats = $node->safe_psql(
+ $dbname,
+ q{
+ SELECT COUNT(*)
+ FROM ext_vacuum_statistics.pg_stats_get_vacuum_indexes((SELECT oid FROM pg_database WHERE datname = current_database()), 0);
+ }
+);
+is($base_stats, 0, 'vacuum stats per index from invalid relation OID return empty as expected');
+
+$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(*)
+ FROM ext_vacuum_statistics.pg_stats_vacuum_tables WHERE relid = 0;
+ }
+);
+is($base_stats, 0, 'ext_vacuum_statistics.pg_stats_vacuum_tables correctly returns no rows for OID = 0');
+
+$base_stats = $node->safe_psql(
+ $dbname,
+ q{
+ SELECT COUNT(*)
+ FROM ext_vacuum_statistics.pg_stats_vacuum_indexes WHERE indexrelid = 0;
+ }
+);
+is($base_stats, 0, 'ext_vacuum_statistics.pg_stats_vacuum_indexes correctly returns no rows for OID = 0');
+
+my $reloid = $node->safe_psql(
+ $dbname,
+ q{
+ SELECT oid FROM pg_class WHERE relname = 'pg_shdepend';
+ }
+);
+
+$node->safe_psql($dbname, "VACUUM pg_shdepend;");
+
+# Check if we can get vacuum statistics for cluster relations (shared catalogs)
+$base_stats = $node->safe_psql(
+ $dbname,
+ qq{
+ SELECT count(*) > 0
+ FROM ext_vacuum_statistics.pg_stats_get_vacuum_tables((SELECT oid FROM pg_database WHERE datname = current_database()), $reloid);
+ }
+);
+
+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(*) > 0
+ FROM ext_vacuum_statistics.pg_stats_get_vacuum_indexes((SELECT oid FROM pg_database WHERE datname = current_database()), $indoid);
+ }
+);
+
+is($base_stats, 't', 'vacuum stats for common index objects available');
+
+$node->safe_psql('postgres',
+ "DROP DATABASE $dbname;
+ VACUUM;"
+);
+
+$base_stats = $node->safe_psql(
+ 'postgres',
+ q{
+ SELECT count(*) = 0
+ FROM ext_vacuum_statistics.pg_stats_get_vacuum_database(0);
+ }
+);
+is($base_stats, 't', 'vacuum stats from database with invalid database OID return empty, as expected');
+};
+
+$node->stop;
+
+done_testing();
diff --git a/contrib/ext_vacuum_statistics/t/053_vacuum_extending_freeze_test.pl b/contrib/ext_vacuum_statistics/t/053_vacuum_extending_freeze_test.pl
new file mode 100644
index 00000000000..4f8f025c63e
--- /dev/null
+++ b/contrib/ext_vacuum_statistics/t/053_vacuum_extending_freeze_test.pl
@@ -0,0 +1,285 @@
+# Copyright (c) 2025 PostgreSQL Global Development Group
+#
+# Test cumulative vacuum stats using ext_vacuum_statistics extension (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: preload extension and aggressive freezing behavior
+$node->append_conf('postgresql.conf', q{
+ shared_preload_libraries = 'ext_vacuum_statistics'
+ 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
+ track_functions = 'all'
+});
+
+$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';
+
+# Create extension
+$node->safe_psql($dbname, q{
+ CREATE EXTENSION ext_vacuum_statistics;
+});
+
+#------------------------------------------------------------------------------
+# 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 ext_vacuum_statistics.pg_stats_vacuum_tables until the named columns exceed the
+# provided baseline values or until timeout.
+#
+# tab_all_frozen_pages_count => 0 # baseline numeric
+# tab_all_visible_pages_count => 0 # baseline numeric
+# run_vacuum => 0 # if true, run vacuum before polling
+#
+# Returns: 1 if the condition is met before timeout, 0 otherwise.
+#------------------------------------------------------------------------------
+sub wait_for_vacuum_stats {
+ my (%args) = @_;
+
+ my $tab_all_frozen_pages_count = $args{tab_all_frozen_pages_count} || 0;
+ my $tab_all_visible_pages_count = $args{tab_all_visible_pages_count} || 0;
+ my $run_vacuum = $args{run_vacuum} ? 1 : 0;
+ my $result_query;
+
+ my $start = time();
+ my $sql;
+
+ # Run VACUUM once if requested, before polling
+ if ($run_vacuum) {
+ $node->safe_psql($dbname, 'VACUUM (FREEZE, VERBOSE) vestat');
+ }
+
+ while ((time() - $start) < $timeout) {
+
+ if ($run_vacuum) {
+ $sql = "
+ SELECT (vm_new_visible_frozen_pages > $tab_all_frozen_pages_count)
+ FROM ext_vacuum_statistics.pg_stats_vacuum_tables
+ WHERE relname = 'vestat'";
+ }
+ else {
+ $sql = "
+ SELECT (pg_stat_get_frozen_page_marks_cleared(c.oid) > $tab_all_frozen_pages_count AND
+ pg_stat_get_visible_page_marks_cleared(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');
+
+ sleep($interval);
+ }
+
+ return 0;
+}
+
+#------------------------------------------------------------------------------
+# Variables to hold vacuum statistics snapshots for comparisons
+#------------------------------------------------------------------------------
+
+my $vm_new_visible_frozen_pages = 0;
+
+my $rev_all_frozen_pages = 0;
+my $rev_all_visible_pages = 0;
+
+my $vm_new_visible_frozen_pages_prev = 0;
+
+my $rev_all_frozen_pages_prev = 0;
+my $rev_all_visible_pages_prev = 0;
+
+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 {
+ $vm_new_visible_frozen_pages = $node->safe_psql(
+ $dbname,
+ "SELECT vt.vm_new_visible_frozen_pages
+ FROM ext_vacuum_statistics.pg_stats_vacuum_tables vt
+ WHERE vt.relname = 'vestat';"
+ );
+
+ $rev_all_frozen_pages = $node->safe_psql(
+ $dbname,
+ "SELECT pg_stat_get_frozen_page_marks_cleared(c.oid)
+ FROM pg_class c
+ WHERE c.relname = 'vestat';"
+ );
+
+ $rev_all_visible_pages = $node->safe_psql(
+ $dbname,
+ "SELECT pg_stat_get_visible_page_marks_cleared(c.oid)
+ FROM pg_class c
+ WHERE c.relname = 'vestat';"
+ );
+}
+
+#------------------------------------------------------------------------------
+# save_vacuum_stats
+#------------------------------------------------------------------------------
+sub save_vacuum_stats {
+ $vm_new_visible_frozen_pages_prev = $vm_new_visible_frozen_pages;
+ $rev_all_frozen_pages_prev = $rev_all_frozen_pages;
+ $rev_all_visible_pages_prev = $rev_all_visible_pages;
+}
+
+#------------------------------------------------------------------------------
+# print_vacuum_stats_on_error
+#------------------------------------------------------------------------------
+sub print_vacuum_stats_on_error {
+ diag(
+ "Statistics in the failed test\n" .
+ "Table statistics:\n" .
+ " Before test:\n" .
+ " vm_new_visible_frozen_pages = $vm_new_visible_frozen_pages_prev\n" .
+ " rev_all_frozen_pages = $rev_all_frozen_pages_prev\n" .
+ " rev_all_visible_pages = $rev_all_visible_pages_prev\n" .
+ " After test:\n" .
+ " vm_new_visible_frozen_pages = $vm_new_visible_frozen_pages\n" .
+ " rev_all_frozen_pages = $rev_all_frozen_pages\n" .
+ " rev_all_visible_pages = $rev_all_visible_pages\n"
+ );
+};
+
+#------------------------------------------------------------------------------
+# Test 1: Create test table, populate it and run an initial vacuum to force freezing
+#------------------------------------------------------------------------------
+
+subtest 'Test 1: Create test table, populate it and run an initial vacuum to force freezing' => sub
+{
+$node->safe_psql($dbname, q{
+ CREATE TABLE vestat (x int)
+ WITH (autovacuum_enabled = off, fillfactor = 10);
+ INSERT INTO vestat SELECT x FROM generate_series(1, 1000) AS g(x);
+ ANALYZE vestat;
+ VACUUM (FREEZE, VERBOSE) vestat;
+});
+
+$updated = wait_for_vacuum_stats(
+ 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_visible_frozen_pages advanced)')
+ or diag "Timeout waiting for ext_vacuum_statistics to update after $timeout seconds during vacuum";
+
+fetch_vacuum_stats();
+
+ok($vm_new_visible_frozen_pages > $vm_new_visible_frozen_pages_prev, 'table vm_new_visible_frozen_pages has increased');
+ok($rev_all_frozen_pages == $rev_all_frozen_pages_prev, 'table rev_all_frozen_pages stay the same');
+ok($rev_all_visible_pages == $rev_all_visible_pages_prev, 'table rev_all_visible_pages stay the same');
+} or print_vacuum_stats_on_error();
+
+#------------------------------------------------------------------------------
+# Test 2: Trigger backend updates
+# Backend activity should reset per-page visibility/freeze marks and increment revision counters
+#------------------------------------------------------------------------------
+subtest 'Test 2: Trigger backend updates' => sub
+{
+save_vacuum_stats();
+
+$node->safe_psql($dbname, q{
+ UPDATE vestat SET x = x + 1001;
+});
+
+$updated = wait_for_vacuum_stats(
+ 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 vacuum stats update after $timeout seconds";
+
+fetch_vacuum_stats();
+
+ok($vm_new_visible_frozen_pages == $vm_new_visible_frozen_pages_prev, 'table vm_new_visible_frozen_pages stay the same');
+ok($rev_all_frozen_pages > $rev_all_frozen_pages_prev, 'table rev_all_frozen_pages has increased');
+ok($rev_all_visible_pages > $rev_all_visible_pages_prev, 'table rev_all_visible_pages has increased');
+} or print_vacuum_stats_on_error();
+
+#------------------------------------------------------------------------------
+# Test 3: Force another vacuum after backend modifications - vacuum should restore freeze/visibility
+#------------------------------------------------------------------------------
+subtest 'Test 3: Force another vacuum after backend modifications - vacuum should restore freeze/visibility' => sub
+{
+save_vacuum_stats();
+
+$node->safe_psql($dbname, q{ VACUUM vestat; });
+
+$updated = wait_for_vacuum_stats(
+ tab_all_frozen_pages_count => $vm_new_visible_frozen_pages,
+ tab_all_visible_pages_count => 0,
+ run_vacuum => 1,
+);
+
+ok($updated,
+ 'vacuum stats updated after vacuuming the all-updated table (vm_new_visible_frozen_pages advanced)')
+ or diag "Timeout waiting for ext_vacuum_statistics to update after $timeout seconds during vacuum";
+
+fetch_vacuum_stats();
+
+ok($vm_new_visible_frozen_pages > $vm_new_visible_frozen_pages_prev, 'table vm_new_visible_frozen_pages has increased');
+ok($rev_all_frozen_pages == $rev_all_frozen_pages_prev, 'table rev_all_frozen_pages stay the same');
+ok($rev_all_visible_pages == $rev_all_visible_pages_prev, 'table rev_all_visible_pages stay the same');
+} or print_vacuum_stats_on_error();
+
+#------------------------------------------------------------------------------
+# Cleanup
+#------------------------------------------------------------------------------
+
+$node->safe_psql('postgres', q{
+ DROP DATABASE statistic_vacuum_database_regression;
+});
+
+$node->stop;
+done_testing();
diff --git a/contrib/ext_vacuum_statistics/t/054_vacuum_extending_gucs_test.pl b/contrib/ext_vacuum_statistics/t/054_vacuum_extending_gucs_test.pl
new file mode 100644
index 00000000000..a195249842b
--- /dev/null
+++ b/contrib/ext_vacuum_statistics/t/054_vacuum_extending_gucs_test.pl
@@ -0,0 +1,279 @@
+# Copyright (c) 2025 PostgreSQL Global Development Group
+#
+# Test GUC parameters for ext_vacuum_statistics extension:
+# vacuum_statistics.enabled
+# vacuum_statistics.object_types (all, databases, relations)
+# vacuum_statistics.track_relations (all, system, user)
+# vacuum_statistics.track_databases_from_list, add/remove_track_database
+# add/remove_track_database, add/remove_track_relation, track_*_from_list
+
+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_gucs');
+$node->init;
+
+$node->append_conf('postgresql.conf', q{
+ shared_preload_libraries = 'ext_vacuum_statistics'
+ log_min_messages = notice
+});
+
+$node->start;
+
+#------------------------------------------------------------------------------
+# Database creation and initialization
+#------------------------------------------------------------------------------
+
+$node->safe_psql('postgres', q{
+ CREATE DATABASE statistic_vacuum_gucs;
+});
+
+my $dbname = 'statistic_vacuum_gucs';
+
+$node->safe_psql($dbname, q{
+ CREATE EXTENSION ext_vacuum_statistics;
+ CREATE TABLE guc_test (x int PRIMARY KEY)
+ WITH (autovacuum_enabled = off);
+ INSERT INTO guc_test SELECT x FROM generate_series(1, 100) AS g(x);
+ ANALYZE guc_test;
+});
+
+# Get OIDs for filtering tests
+my $dboid = $node->safe_psql($dbname, q{SELECT oid FROM pg_database WHERE datname = current_database()});
+my $reloid = $node->safe_psql($dbname, q{SELECT oid FROM pg_class WHERE relname = 'guc_test'});
+
+#------------------------------------------------------------------------------
+# Reset stats and run vacuum (all in one session so GUCs persist)
+#------------------------------------------------------------------------------
+
+sub reset_and_vacuum {
+ my ($db, $table, $opts) = @_;
+ $table ||= 'guc_test';
+ my $gucs = $opts && $opts->{gucs} ? $opts->{gucs} : [];
+ my $modify = $opts && $opts->{modify};
+ my $extra = $opts && $opts->{extra_vacuum} ? $opts->{extra_vacuum} : [];
+ $extra = [$extra] unless ref $extra eq 'ARRAY';
+ my $sql = join("\n", (map { "SET $_;" } @$gucs),
+ "SELECT ext_vacuum_statistics.vacuum_statistics_reset();",
+ $modify ? (
+ "TRUNCATE $table;",
+ "INSERT INTO $table SELECT x FROM generate_series(1, 100) AS g(x);",
+ "DELETE FROM $table;",
+ ) : (),
+ "VACUUM $table;",
+ (map { "VACUUM $_;" } @$extra),
+ # Make pending stats visible to subsequent sessions without sleeping.
+ "SELECT pg_stat_force_next_flush();");
+ $node->safe_psql($db, $sql);
+}
+
+#------------------------------------------------------------------------------
+# Test 1: vacuum_statistics.enabled
+#------------------------------------------------------------------------------
+subtest 'vacuum_statistics.enabled' => sub {
+ reset_and_vacuum($dbname);
+
+ # Default: enabled - should have stats
+ my $count = $node->safe_psql($dbname,
+ "SELECT COUNT(*) FROM ext_vacuum_statistics.pg_stats_vacuum_tables WHERE relname = 'guc_test'");
+ ok($count > 0, 'stats collected when enabled');
+
+ # Disable, reset and vacuum in same session. Assert not only that the
+ # row count is zero, but that the specific counters remain zero: a stray
+ # row with zero counters would otherwise pass a bare COUNT(*)=0 check.
+ reset_and_vacuum($dbname, 'guc_test', { gucs => ['vacuum_statistics.enabled = off'] });
+
+ $count = $node->safe_psql($dbname,
+ "SELECT COUNT(*) FROM ext_vacuum_statistics.pg_stats_vacuum_tables WHERE relname = 'guc_test'");
+ is($count, 0, 'no rows when disabled');
+
+ my $sums = $node->safe_psql($dbname, q{
+ SELECT COALESCE(SUM(total_blks_read), 0)
+ + COALESCE(SUM(total_blks_dirtied), 0)
+ + COALESCE(SUM(pages_scanned), 0)
+ FROM ext_vacuum_statistics.pg_stats_vacuum_tables
+ WHERE relname = 'guc_test'
+ });
+ is($sums, '0', 'no counters accumulated when disabled');
+};
+
+#------------------------------------------------------------------------------
+# Test 2: vacuum_statistics.object_types (databases only, relations only)
+#------------------------------------------------------------------------------
+subtest 'vacuum_statistics.object_types' => sub {
+ # track only db stats, no relation stats
+ reset_and_vacuum($dbname, 'guc_test', {
+ gucs => ["vacuum_statistics.object_types = 'databases'"],
+ modify => 1,
+ });
+ my $db_has_dbs = $node->safe_psql($dbname,
+ "SELECT COALESCE(SUM(db_blks_hit), 0) FROM ext_vacuum_statistics.pg_stats_vacuum_database WHERE dboid = $dboid");
+ my $rel_dbs = $node->safe_psql($dbname,
+ "SELECT COUNT(*) FROM ext_vacuum_statistics.pg_stats_vacuum_tables WHERE relname = 'guc_test'");
+ is($rel_dbs, 0, 'track=databases: no relation stats');
+ ok($db_has_dbs > 0, 'track=databases: database stats collected');
+
+ # track only relation stats, no db stats
+ reset_and_vacuum($dbname, 'guc_test', {
+ gucs => ["vacuum_statistics.object_types = 'relations'"],
+ modify => 1,
+ });
+ my $db_has_rels = $node->safe_psql($dbname,
+ "SELECT COALESCE(SUM(db_blks_hit), 0) > 0 FROM ext_vacuum_statistics.pg_stats_vacuum_database WHERE dboid = $dboid");
+ my $rel_rels = $node->safe_psql($dbname,
+ "SELECT COUNT(*) FROM ext_vacuum_statistics.pg_stats_vacuum_tables WHERE relname = 'guc_test'");
+ ok($rel_rels > 0, 'track=relations: relation stats collected');
+ is($db_has_rels, 'f', 'track=relations: no database stats');
+};
+
+#------------------------------------------------------------------------------
+# Test 3: vacuum_statistics.track_relations (system, user)
+#------------------------------------------------------------------------------
+subtest 'vacuum_statistics.track_relations' => sub {
+ # track_relations - only user tables
+ reset_and_vacuum($dbname, 'guc_test', {
+ gucs => [
+ "vacuum_statistics.object_types = 'relations'",
+ "vacuum_statistics.track_relations = 'user'",
+ ],
+ extra_vacuum => ['pg_class'],
+ });
+
+ my $user_rel = $node->safe_psql($dbname,
+ "SELECT COUNT(*) FROM ext_vacuum_statistics.pg_stats_vacuum_tables WHERE relname = 'guc_test'");
+ my $sys_rel = $node->safe_psql($dbname,
+ "SELECT COUNT(*) FROM ext_vacuum_statistics.pg_stats_vacuum_tables WHERE relname = 'pg_class'");
+ ok($user_rel > 0, 'track_relations=user: user table stats collected');
+ is($sys_rel, 0, 'track_relations=user: system table stats not collected');
+
+ # track_relations - only system tables
+ reset_and_vacuum($dbname, 'guc_test', {
+ gucs => [
+ "vacuum_statistics.object_types = 'relations'",
+ "vacuum_statistics.track_relations = 'system'",
+ ],
+ extra_vacuum => ['pg_class'],
+ });
+
+ $user_rel = $node->safe_psql($dbname,
+ "SELECT COUNT(*) FROM ext_vacuum_statistics.pg_stats_vacuum_tables WHERE relname = 'guc_test'");
+ $sys_rel = $node->safe_psql($dbname,
+ "SELECT COUNT(*) FROM ext_vacuum_statistics.pg_stats_vacuum_tables WHERE relname = 'pg_class'");
+ is($user_rel, 0, 'track_relations=system: user table stats not collected');
+ ok($sys_rel > 0, 'track_relations=system: system table stats collected');
+};
+
+#------------------------------------------------------------------------------
+# Test 4: track_databases (via add/remove_track_database)
+#------------------------------------------------------------------------------
+subtest 'track_databases (add/remove)' => sub {
+ $node->safe_psql($dbname, "SELECT ext_vacuum_statistics.remove_track_database($dboid)");
+ $node->safe_psql($dbname, "SELECT ext_vacuum_statistics.add_track_database($dboid)");
+ reset_and_vacuum($dbname, 'guc_test', { gucs => ["vacuum_statistics.track_databases_from_list = on"], modify => 1 });
+
+ my $rel_count = $node->safe_psql($dbname,
+ "SELECT COUNT(*) FROM ext_vacuum_statistics.pg_stats_vacuum_tables WHERE relname = 'guc_test'");
+ ok($rel_count > 0, 'db in list: stats collected');
+
+ $node->safe_psql($dbname, "SELECT ext_vacuum_statistics.remove_track_database($dboid)");
+ reset_and_vacuum($dbname, 'guc_test', { gucs => ["vacuum_statistics.track_databases_from_list = on"], modify => 1 });
+
+ $rel_count = $node->safe_psql($dbname,
+ "SELECT COUNT(*) FROM ext_vacuum_statistics.pg_stats_vacuum_tables WHERE relname = 'guc_test'");
+ is($rel_count, 0, 'db removed from list: no stats');
+};
+
+#------------------------------------------------------------------------------
+# Test 5: track_relations (via add/remove_track_relation)
+#------------------------------------------------------------------------------
+subtest 'track_relations (add/remove)' => sub {
+ $node->safe_psql($dbname, "SELECT ext_vacuum_statistics.remove_track_relation($dboid, $reloid)");
+ $node->safe_psql($dbname, "SELECT ext_vacuum_statistics.add_track_relation($dboid, $reloid)");
+ reset_and_vacuum($dbname, 'guc_test', { gucs => ["vacuum_statistics.track_relations_from_list = on"], modify => 1 });
+
+ my $rel_count = $node->safe_psql($dbname,
+ "SELECT COUNT(*) FROM ext_vacuum_statistics.pg_stats_vacuum_tables WHERE relname = 'guc_test'");
+ ok($rel_count > 0, 'table in list: stats collected');
+
+ $node->safe_psql($dbname, "SELECT ext_vacuum_statistics.remove_track_relation($dboid, $reloid)");
+ reset_and_vacuum($dbname, 'guc_test', { gucs => ["vacuum_statistics.track_relations_from_list = on"], modify => 1 });
+
+ $rel_count = $node->safe_psql($dbname,
+ "SELECT COUNT(*) FROM ext_vacuum_statistics.pg_stats_vacuum_tables WHERE relname = 'guc_test'");
+ is($rel_count, 0, 'table removed from list: no stats');
+};
+
+#------------------------------------------------------------------------------
+# Test 6: vacuum_statistics.collect - per-category gating
+#
+# With collect='wal' only wal_* counters must advance; buffer, timing, and
+# general categories must stay at zero. With collect='buffers' the inverse
+# holds. Unknown tokens must be rejected by the check-hook.
+#------------------------------------------------------------------------------
+subtest 'vacuum_statistics.collect' => sub {
+ # wal-only: WAL counters should accumulate, buffers/timing/general should not.
+ reset_and_vacuum($dbname, 'guc_test', {
+ gucs => ["vacuum_statistics.collect = 'wal'"],
+ modify => 1,
+ });
+
+ my $wal = $node->safe_psql($dbname, q{
+ SELECT COALESCE(SUM(wal_records), 0) > 0
+ FROM ext_vacuum_statistics.pg_stats_vacuum_tables
+ WHERE relname = 'guc_test'
+ });
+ is($wal, 't', "collect='wal': wal_records accumulated");
+
+ my $other = $node->safe_psql($dbname, q{
+ SELECT COALESCE(SUM(total_blks_read), 0)
+ + COALESCE(SUM(total_blks_hit), 0)
+ + COALESCE(SUM(total_time), 0)
+ + COALESCE(SUM(tuples_deleted), 0)
+ + COALESCE(SUM(pages_scanned), 0)
+ FROM ext_vacuum_statistics.pg_stats_vacuum_tables
+ WHERE relname = 'guc_test'
+ });
+ is($other, '0',
+ "collect='wal': buffer/timing/general counters not accumulated");
+
+ # buffers-only: buffer counters should advance, WAL should not.
+ reset_and_vacuum($dbname, 'guc_test', {
+ gucs => ["vacuum_statistics.collect = 'buffers'"],
+ modify => 1,
+ });
+
+ my $buf = $node->safe_psql($dbname, q{
+ SELECT COALESCE(SUM(total_blks_read), 0)
+ + COALESCE(SUM(total_blks_hit), 0) > 0
+ FROM ext_vacuum_statistics.pg_stats_vacuum_tables
+ WHERE relname = 'guc_test'
+ });
+ is($buf, 't', "collect='buffers': buffer counters accumulated");
+
+ my $wal_off = $node->safe_psql($dbname, q{
+ SELECT COALESCE(SUM(wal_records), 0)
+ FROM ext_vacuum_statistics.pg_stats_vacuum_tables
+ WHERE relname = 'guc_test'
+ });
+ is($wal_off, '0',
+ "collect='buffers': WAL counters not accumulated");
+
+ # Unknown category must be rejected by the check-hook.
+ my ($ret, $stdout, $stderr) = $node->psql($dbname,
+ "SET vacuum_statistics.collect = 'nope'");
+ isnt($ret, 0, "collect='nope': rejected by check-hook");
+ like($stderr, qr/Unrecognized category "nope"/,
+ "collect='nope': errdetail names the offending token");
+};
+
+$node->stop;
+
+done_testing();
diff --git a/contrib/ext_vacuum_statistics/vacuum_statistics.c b/contrib/ext_vacuum_statistics/vacuum_statistics.c
new file mode 100644
index 00000000000..75d1bd2cf06
--- /dev/null
+++ b/contrib/ext_vacuum_statistics/vacuum_statistics.c
@@ -0,0 +1,1387 @@
+/*
+ * ext_vacuum_statistics - Extended vacuum statistics for PostgreSQL
+ *
+ * This module collects detailed vacuum statistics (I/O, WAL, timing, etc.)
+ * at relation and database level by hooking into the vacuum reporting path.
+ * Statistics are stored via pgstat custom statistics. Management of statistics
+ * storage and output functions are implemented in this module.
+ */
+#include "postgres.h"
+
+#include "access/transam.h"
+#include "catalog/catalog.h"
+#include "catalog/objectaccess.h"
+#include "catalog/pg_authid.h"
+#include "catalog/pg_class.h"
+#include "catalog/pg_database.h"
+#include "fmgr.h"
+#include "funcapi.h"
+#include "miscadmin.h"
+#include "pgstat.h"
+#include "storage/fd.h"
+#include "storage/ipc.h"
+#include "storage/lwlock.h"
+#include "utils/acl.h"
+#include "utils/builtins.h"
+#include "utils/fmgrprotos.h"
+#include "utils/guc.h"
+#include "utils/hsearch.h"
+#include "utils/lsyscache.h"
+#include "utils/pgstat_kind.h"
+#include "utils/pgstat_internal.h"
+#include "utils/tuplestore.h"
+
+#ifdef PG_MODULE_MAGIC
+PG_MODULE_MAGIC;
+#endif
+
+/* Two kinds: relations (tables/indexes) and database aggregates */
+#define PGSTAT_KIND_EXTVAC_RELATION 24
+#define PGSTAT_KIND_EXTVAC_DB 25
+
+#define SJ_NODENAME "vacuum_statistics"
+#define EVS_TRACK_FILENAME "pg_stat/ext_vacuum_statistics_track.oid"
+
+/* Bit flags for evs_track (object_types): 'all', 'databases', 'relations' */
+#define EVS_TRACK_RELATIONS 0x01
+#define EVS_TRACK_DATABASES 0x02
+
+/* Bit flags for evs_track_relations: 'all', 'system', 'user' */
+#define EVS_FILTER_SYSTEM 0x01
+#define EVS_FILTER_USER 0x02
+
+/*
+ * Bit flags for evs_collect_mask. Each category groups counters that can be
+ * accumulated (or skipped) together, letting users reduce overhead at run
+ * time by turning off categories they don't need.
+ */
+#define EVS_COLLECT_BUFFERS 0x1 /* blks_*, blk_*_time */
+#define EVS_COLLECT_WAL 0x2 /* wal_records, wal_fpi, wal_bytes */
+#define EVS_COLLECT_GENERAL 0x4 /* tuples_deleted, pages_*, vm_*,
+ * wraparound_failsafe_count,
+ * interrupts_count */
+#define EVS_COLLECT_TIMING 0x8 /* delay_time, total_time */
+#define EVS_COLLECT_ALL (EVS_COLLECT_BUFFERS | EVS_COLLECT_WAL | \
+ EVS_COLLECT_GENERAL | EVS_COLLECT_TIMING)
+
+/* GUCs */
+static bool evs_enabled = true;
+static char *evs_track = "all"; /* 'all', 'databases', 'relations' */
+static char *evs_track_relations = "all"; /* 'all', 'system', 'user' */
+static int evs_track_bits = EVS_TRACK_RELATIONS | EVS_TRACK_DATABASES;
+static int evs_track_relations_bits = EVS_FILTER_SYSTEM | EVS_FILTER_USER;
+static bool evs_track_databases_from_list = false; /* if true, track only
+ * databases in list */
+static bool evs_track_relations_from_list = false; /* if true, track only
+ * relations in list */
+static char *evs_collect = "all"; /* categories to collect */
+static int evs_collect_mask = EVS_COLLECT_ALL;
+
+/* Hook */
+static set_report_vacuum_hook_type prev_report_vacuum_hook = NULL;
+static object_access_hook_type prev_object_access_hook = NULL;
+static shmem_request_hook_type prev_shmem_request_hook = NULL;
+
+/* Forward declarations */
+static void pgstat_report_vacuum_extstats(Oid tableoid, bool shared,
+ PgStat_VacuumRelationCounts * params);
+static bool evs_oid_in_list(HTAB *hash, Oid oid);
+static void evs_track_hash_ensure_init(void);
+static void evs_track_save_file(void);
+static void evs_track_load_file(void);
+static void evs_drop_access_hook(ObjectAccessType access, Oid classId,
+ Oid objectId, int subId, void *arg);
+static void evs_shmem_request(void);
+
+/* Hash tables for track_databases and track_relations_list (backend-local) */
+static HTAB *evs_track_databases_hash = NULL;
+static HTAB *evs_track_relations_hash = NULL;
+static bool evs_track_hash_initialized = false;
+
+/*
+ * Named LWLock tranche protecting the on-disk track file and serializing
+ * backend-local reloads/saves across concurrent backends.
+ */
+#define EVS_TRACK_TRANCHE_NAME "ext_vacuum_statistics_track"
+static LWLock *evs_track_lock = NULL;
+
+static inline LWLock *
+evs_get_track_lock(void)
+{
+ if (evs_track_lock == NULL)
+ evs_track_lock = &GetNamedLWLockTranche(EVS_TRACK_TRANCHE_NAME)->lock;
+ return evs_track_lock;
+}
+
+/*
+ * objid encoding for relations: (relid << 2) | (type & 3)
+ */
+#define EXTVAC_OBJID(relid, type) (((uint64) (relid)) << 2 | ((type) & 3))
+
+/* Key for relation tracking: (dboid, reloid).
+ * InvalidOid for dboid means it is a cluster object.
+ */
+typedef struct
+{
+ Oid dboid;
+ Oid reloid;
+} EvsTrackRelKey;
+
+/* Shared memory entry for vacuum stats; one per relation or database. */
+typedef struct PgStatShared_ExtVacEntry
+{
+ PgStatShared_Common header;
+ PgStat_VacuumRelationCounts stats;
+} PgStatShared_ExtVacEntry;
+
+/* PgStat kind for per-relation vacuum statistics (tables/indexes) */
+static const PgStat_KindInfo extvac_relation_kind_info = {
+ .name = "ext_vacuum_statistics_relation",
+ .fixed_amount = false,
+ .accessed_across_databases = true,
+ .write_to_file = true,
+ .track_entry_count = true,
+ .shared_size = sizeof(PgStatShared_ExtVacEntry),
+ .shared_data_off = offsetof(PgStatShared_ExtVacEntry, stats),
+ .shared_data_len = sizeof(PgStat_VacuumRelationCounts),
+ .pending_size = 0,
+ .flush_pending_cb = NULL,
+};
+
+/* PgStat kind for per-database aggregated vacuum statistics */
+static const PgStat_KindInfo extvac_db_kind_info = {
+ .name = "ext_vacuum_statistics_db",
+ .fixed_amount = false,
+ .accessed_across_databases = true,
+ .write_to_file = true,
+ .track_entry_count = true,
+ .shared_size = sizeof(PgStatShared_ExtVacEntry),
+ .shared_data_off = offsetof(PgStatShared_ExtVacEntry, stats),
+ .shared_data_len = sizeof(PgStat_VacuumRelationCounts),
+ .pending_size = 0,
+ .flush_pending_cb = NULL,
+};
+
+/*
+ * Accumulate a single counter only if its category is enabled in
+ * evs_collect_mask. Parentheses around every argument: the macro is invoked
+ * from expression contexts and with expressions as the destination pointer.
+ */
+#define ACCUM_IF(dst, src, field, cat) \
+ do { \
+ if ((evs_collect_mask) & (cat)) \
+ ((dst))->field += ((src))->field; \
+ } while (0)
+
+static inline void
+pgstat_accumulate_common(PgStat_CommonCounts * dst, const PgStat_CommonCounts * src)
+{
+ ACCUM_IF(dst, src, total_blks_read, EVS_COLLECT_BUFFERS);
+ ACCUM_IF(dst, src, total_blks_hit, EVS_COLLECT_BUFFERS);
+ ACCUM_IF(dst, src, total_blks_dirtied, EVS_COLLECT_BUFFERS);
+ ACCUM_IF(dst, src, total_blks_written, EVS_COLLECT_BUFFERS);
+ ACCUM_IF(dst, src, blks_fetched, EVS_COLLECT_BUFFERS);
+ ACCUM_IF(dst, src, blks_hit, EVS_COLLECT_BUFFERS);
+ ACCUM_IF(dst, src, blk_read_time, EVS_COLLECT_BUFFERS);
+ ACCUM_IF(dst, src, blk_write_time, EVS_COLLECT_BUFFERS);
+ ACCUM_IF(dst, src, delay_time, EVS_COLLECT_TIMING);
+ ACCUM_IF(dst, src, total_time, EVS_COLLECT_TIMING);
+ ACCUM_IF(dst, src, wal_records, EVS_COLLECT_WAL);
+ ACCUM_IF(dst, src, wal_fpi, EVS_COLLECT_WAL);
+ ACCUM_IF(dst, src, wal_bytes, EVS_COLLECT_WAL);
+ ACCUM_IF(dst, src, wraparound_failsafe_count, EVS_COLLECT_GENERAL);
+ ACCUM_IF(dst, src, interrupts_count, EVS_COLLECT_GENERAL);
+ ACCUM_IF(dst, src, tuples_deleted, EVS_COLLECT_GENERAL);
+}
+
+static inline void
+pgstat_accumulate_extvac_stats(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);
+ Assert(src->type == dst->type);
+
+ pgstat_accumulate_common(&dst->common, &src->common);
+
+ if (dst->type == PGSTAT_EXTVAC_TABLE &&
+ (evs_collect_mask & EVS_COLLECT_GENERAL) != 0)
+ {
+ dst->table.pages_scanned += src->table.pages_scanned;
+ dst->table.pages_removed += src->table.pages_removed;
+ dst->table.tuples_frozen += src->table.tuples_frozen;
+ dst->table.recently_dead_tuples += src->table.recently_dead_tuples;
+ 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->table.missed_dead_pages += src->table.missed_dead_pages;
+ dst->table.missed_dead_tuples += src->table.missed_dead_tuples;
+ dst->table.index_vacuum_count += src->table.index_vacuum_count;
+ }
+ else if (dst->type == PGSTAT_EXTVAC_INDEX &&
+ (evs_collect_mask & EVS_COLLECT_GENERAL) != 0)
+ {
+ dst->index.pages_deleted += src->index.pages_deleted;
+ }
+}
+
+/*
+ * GUC check hooks: validate the string and compute the bitmask into *extra.
+ * Rejecting unknown values here prevents silent fall-through to "all".
+ */
+static bool
+evs_track_check_hook(char **newval, void **extra, GucSource source)
+{
+ int *bits;
+
+ if (*newval == NULL)
+ return false;
+
+ bits = (int *) guc_malloc(LOG, sizeof(int));
+ if (!bits)
+ return false;
+
+ if (strcmp(*newval, "all") == 0)
+ *bits = EVS_TRACK_RELATIONS | EVS_TRACK_DATABASES;
+ else if (strcmp(*newval, "databases") == 0)
+ *bits = EVS_TRACK_DATABASES;
+ else if (strcmp(*newval, "relations") == 0)
+ *bits = EVS_TRACK_RELATIONS;
+ else
+ {
+ guc_free(bits);
+ GUC_check_errdetail("Allowed values are \"all\", \"databases\", \"relations\".");
+ return false;
+ }
+ *extra = bits;
+ return true;
+}
+
+static void
+evs_track_assign_hook(const char *newval, void *extra)
+{
+ evs_track_bits = *((int *) extra);
+}
+
+static bool
+evs_track_relations_check_hook(char **newval, void **extra, GucSource source)
+{
+ int *bits;
+
+ if (*newval == NULL)
+ return false;
+
+ bits = (int *) guc_malloc(LOG, sizeof(int));
+ if (!bits)
+ return false;
+
+ if (strcmp(*newval, "all") == 0)
+ *bits = EVS_FILTER_SYSTEM | EVS_FILTER_USER;
+ else if (strcmp(*newval, "system") == 0)
+ *bits = EVS_FILTER_SYSTEM;
+ else if (strcmp(*newval, "user") == 0)
+ *bits = EVS_FILTER_USER;
+ else
+ {
+ guc_free(bits);
+ GUC_check_errdetail("Allowed values are \"all\", \"system\", \"user\".");
+ return false;
+ }
+ *extra = bits;
+ return true;
+}
+
+static void
+evs_track_relations_assign_hook(const char *newval, void *extra)
+{
+ evs_track_relations_bits = *((int *) extra);
+}
+
+/*
+ * Check hook for vacuum_statistics.collect.
+ *
+ * Accepts a comma- or whitespace-separated list of category names
+ * (buffers, wal, general, timing) or the shorthand "all". Computes the
+ * matching bitmask once and stashes it in *extra; the assign hook just
+ * copies it into evs_collect_mask. Unknown tokens are rejected so the
+ * setting cannot silently collapse to the "all" default.
+ */
+static bool
+evs_collect_check_hook(char **newval, void **extra, GucSource source)
+{
+ int *mask;
+ char *copy;
+ char *p;
+ char *tok;
+ int accum = 0;
+ bool saw_all = false;
+
+ if (*newval == NULL)
+ return false;
+
+ mask = (int *) guc_malloc(LOG, sizeof(int));
+ if (!mask)
+ return false;
+
+ /* Empty string means "all", matching the default behavior. */
+ if ((*newval)[0] == '\0')
+ {
+ *mask = EVS_COLLECT_ALL;
+ *extra = mask;
+ return true;
+ }
+
+ copy = pstrdup(*newval);
+ for (p = copy; (tok = strtok(p, " \t,")) != NULL; p = NULL)
+ {
+ if (pg_strcasecmp(tok, "all") == 0)
+ saw_all = true;
+ else if (pg_strcasecmp(tok, "buffers") == 0)
+ accum |= EVS_COLLECT_BUFFERS;
+ else if (pg_strcasecmp(tok, "wal") == 0)
+ accum |= EVS_COLLECT_WAL;
+ else if (pg_strcasecmp(tok, "general") == 0)
+ accum |= EVS_COLLECT_GENERAL;
+ else if (pg_strcasecmp(tok, "timing") == 0)
+ accum |= EVS_COLLECT_TIMING;
+ else
+ {
+ /*
+ * GUC_check_errdetail formats the message immediately, but tok
+ * points into copy; emit the detail first, then free the
+ * scratch buffer so the formatted string is already stashed in
+ * GUC_check_errdetail_string.
+ */
+ GUC_check_errdetail("Unrecognized category \"%s\" in vacuum_statistics.collect; "
+ "allowed values are \"all\", \"buffers\", \"wal\", \"general\", \"timing\".",
+ tok);
+ pfree(copy);
+ guc_free(mask);
+ return false;
+ }
+ }
+ pfree(copy);
+
+ *mask = saw_all ? EVS_COLLECT_ALL : accum;
+ if (*mask == 0)
+ *mask = EVS_COLLECT_ALL;
+ *extra = mask;
+ return true;
+}
+
+static void
+evs_collect_assign_hook(const char *newval, void *extra)
+{
+ evs_collect_mask = *((int *) extra);
+}
+
+void
+_PG_init(void)
+{
+ if (!process_shared_preload_libraries_in_progress)
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("ext_vacuum_statistics module could be loaded only on startup."),
+ errdetail("Add 'ext_vacuum_statistics' into the shared_preload_libraries list.")));
+
+ DefineCustomBoolVariable("vacuum_statistics.enabled",
+ "Enable extended vacuum statistics collection.",
+ NULL, &evs_enabled, true,
+ PGC_SUSET, 0, NULL, NULL, NULL);
+
+ DefineCustomStringVariable("vacuum_statistics.object_types",
+ "Object types for statistics: 'all', 'databases', 'relations'.",
+ NULL, &evs_track, "all",
+ PGC_SUSET, 0,
+ evs_track_check_hook,
+ evs_track_assign_hook, NULL);
+
+ DefineCustomStringVariable("vacuum_statistics.track_relations",
+ "When tracking relations: 'all', 'system', 'user'.",
+ NULL, &evs_track_relations, "all",
+ PGC_SUSET, 0,
+ evs_track_relations_check_hook,
+ evs_track_relations_assign_hook, NULL);
+
+ DefineCustomBoolVariable("vacuum_statistics.track_databases_from_list",
+ "If true, track only databases added via add_track_database.",
+ NULL, &evs_track_databases_from_list, false,
+ PGC_SUSET, 0, NULL, NULL, NULL);
+
+ DefineCustomBoolVariable("vacuum_statistics.track_relations_from_list",
+ "If true, track only relations added via add_track_relation.",
+ NULL, &evs_track_relations_from_list, false,
+ PGC_SUSET, 0, NULL, NULL, NULL);
+
+ DefineCustomStringVariable("vacuum_statistics.collect",
+ "Statistics categories to collect.",
+ "Comma- or whitespace-separated list of: "
+ "\"buffers\", \"wal\", \"general\", \"timing\"; "
+ "or \"all\" for every category (default).",
+ &evs_collect, "all",
+ PGC_SUSET, 0,
+ evs_collect_check_hook,
+ evs_collect_assign_hook, NULL);
+
+ MarkGUCPrefixReserved(SJ_NODENAME);
+
+ pgstat_register_kind(PGSTAT_KIND_EXTVAC_RELATION, &extvac_relation_kind_info);
+ pgstat_register_kind(PGSTAT_KIND_EXTVAC_DB, &extvac_db_kind_info);
+
+ prev_shmem_request_hook = shmem_request_hook;
+ shmem_request_hook = evs_shmem_request;
+
+ 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 = evs_drop_access_hook;
+}
+
+static void
+evs_shmem_request(void)
+{
+ if (prev_shmem_request_hook)
+ prev_shmem_request_hook();
+
+ RequestNamedLWLockTranche(EVS_TRACK_TRANCHE_NAME, 1);
+}
+
+/*
+ * Object access hook: remove dropped objects from track lists.
+ */
+static void
+evs_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)
+ {
+ if (classId == RelationRelationId && subId == 0)
+ {
+ char relkind = get_rel_relkind(objectId);
+ EvsTrackRelKey key;
+ bool found;
+
+ if (relkind == RELKIND_RELATION || relkind == RELKIND_INDEX)
+ {
+ LWLock *lock = evs_get_track_lock();
+
+ LWLockAcquire(lock, LW_EXCLUSIVE);
+ evs_track_hash_ensure_init();
+ key.dboid = MyDatabaseId;
+ key.reloid = objectId;
+ hash_search(evs_track_relations_hash, &key, HASH_REMOVE, &found);
+ key.dboid = InvalidOid;
+ hash_search(evs_track_relations_hash, &key, HASH_REMOVE, &found);
+ evs_track_save_file();
+ LWLockRelease(lock);
+ }
+ }
+
+ if (classId == DatabaseRelationId && objectId != InvalidOid)
+ {
+ LWLock *lock = evs_get_track_lock();
+ bool found;
+
+ LWLockAcquire(lock, LW_EXCLUSIVE);
+ evs_track_hash_ensure_init();
+ hash_search(evs_track_databases_hash, &objectId, HASH_REMOVE, &found);
+ evs_track_save_file();
+ LWLockRelease(lock);
+ }
+ }
+}
+
+/*
+ * Storage of track lists in a separate file.
+ *
+ * Stores the lists of database OIDs and (dboid, reloid) pairs used for
+ * selective tracking when track_databases_from_list or track_relations_from_list
+ * is enabled.
+ * Data stores in pg_stat/ext_vacuum_statistics_track.oid
+ */
+/*
+ * Initialize the backend-local tracking hashes and load their contents
+ * from the on-disk file.
+ *
+ * The hashes are per-backend, so no lock is needed to protect them from
+ * other processes; however, another backend may be concurrently rewriting
+ * the track file, so we take a shared lock for the file read.
+ */
+static void
+evs_track_hash_ensure_init(void)
+{
+ HASHCTL ctl;
+ LWLock *lock;
+ bool need_load;
+
+ if (evs_track_hash_initialized)
+ return;
+
+ lock = evs_get_track_lock();
+
+ if (evs_track_databases_hash == NULL)
+ {
+ memset(&ctl, 0, sizeof(ctl));
+ ctl.keysize = sizeof(Oid);
+ ctl.entrysize = sizeof(Oid);
+ ctl.hcxt = TopMemoryContext;
+ evs_track_databases_hash =
+ hash_create("ext_vacuum_statistics track databases",
+ 64, &ctl, HASH_ELEM | HASH_BLOBS);
+ }
+
+ if (evs_track_relations_hash == NULL)
+ {
+ memset(&ctl, 0, sizeof(ctl));
+ ctl.keysize = sizeof(EvsTrackRelKey);
+ ctl.entrysize = sizeof(EvsTrackRelKey);
+ ctl.hcxt = TopMemoryContext;
+ evs_track_relations_hash =
+ hash_create("ext_vacuum_statistics track relations",
+ 64, &ctl, HASH_ELEM | HASH_BLOBS);
+ }
+
+ need_load = !LWLockHeldByMe(lock);
+ if (need_load)
+ LWLockAcquire(lock, LW_SHARED);
+ PG_TRY();
+ {
+ evs_track_load_file();
+ evs_track_hash_initialized = true;
+ }
+ PG_FINALLY();
+ {
+ if (need_load)
+ LWLockRelease(lock);
+ }
+ PG_END_TRY();
+}
+
+/*
+ * Load track lists from disk into the backend-local hashes.
+ *
+ * Caller must hold evs_track_lock at least in shared mode, since the file
+ * may be concurrently rewritten by another backend.
+ */
+static void
+evs_track_load_file(void)
+{
+ char path[MAXPGPATH];
+ FILE *fp;
+ char buf[MAXPGPATH];
+ bool in_relations = false;
+ Oid oid;
+ EvsTrackRelKey key;
+ bool found;
+
+ if (!DataDir || DataDir[0] == '\0' ||
+ !evs_track_databases_hash || !evs_track_relations_hash)
+ return;
+
+ snprintf(path, sizeof(path), "%s/%s", DataDir, EVS_TRACK_FILENAME);
+ fp = AllocateFile(path, "r");
+ if (!fp)
+ {
+ if (errno != ENOENT)
+ ereport(LOG,
+ (errcode_for_file_access(),
+ errmsg("could not open track file \"%s\": %m", path)));
+ return;
+ }
+
+ PG_TRY();
+ {
+ while (fgets(buf, sizeof(buf), fp))
+ {
+ size_t len = strlen(buf);
+
+ /* Reject unterminated lines (longer than buffer) as corruption. */
+ if (len > 0 && buf[len - 1] != '\n' && !feof(fp))
+ ereport(ERROR,
+ (errcode(ERRCODE_DATA_CORRUPTED),
+ errmsg("line too long in track file \"%s\"", path)));
+
+ if (strncmp(buf, "[databases]", 11) == 0)
+ {
+ in_relations = false;
+ continue;
+ }
+ if (strncmp(buf, "[relations]", 11) == 0)
+ {
+ in_relations = true;
+ continue;
+ }
+ if (in_relations)
+ {
+ if (sscanf(buf, "%u %u", &key.dboid, &key.reloid) == 2)
+ hash_search(evs_track_relations_hash, &key, HASH_ENTER, &found);
+ else if (sscanf(buf, "%u", &oid) == 1)
+ {
+ key.dboid = InvalidOid;
+ key.reloid = oid;
+ hash_search(evs_track_relations_hash, &key, HASH_ENTER, &found);
+ }
+ }
+ else if (sscanf(buf, "%u", &oid) == 1)
+ hash_search(evs_track_databases_hash, &oid, HASH_ENTER, &found);
+ }
+
+ if (ferror(fp))
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not read track file \"%s\": %m", path)));
+ }
+ PG_FINALLY();
+ {
+ FreeFile(fp);
+ }
+ PG_END_TRY();
+}
+
+/*
+ * Atomically rewrite the track file. Caller must hold evs_track_lock
+ * in exclusive mode.
+ */
+static void
+evs_track_save_file(void)
+{
+ char path[MAXPGPATH];
+ char tmppath[MAXPGPATH];
+ FILE *fp;
+ HASH_SEQ_STATUS status;
+ Oid *entry;
+ EvsTrackRelKey *rel_entry;
+ bool failed = false;
+
+ if (!DataDir || DataDir[0] == '\0' ||
+ !evs_track_databases_hash || !evs_track_relations_hash)
+ return;
+
+ snprintf(path, sizeof(path), "%s/%s", DataDir, EVS_TRACK_FILENAME);
+ snprintf(tmppath, sizeof(tmppath), "%s.tmp", path);
+
+ fp = AllocateFile(tmppath, PG_BINARY_W);
+ if (!fp)
+ {
+ ereport(LOG,
+ (errcode_for_file_access(),
+ errmsg("could not create track file \"%s\": %m", tmppath)));
+ return;
+ }
+
+ PG_TRY();
+ {
+ if (fputs("[databases]\n", fp) == EOF)
+ failed = true;
+
+ if (!failed)
+ {
+ hash_seq_init(&status, evs_track_databases_hash);
+ while ((entry = (Oid *) hash_seq_search(&status)) != NULL)
+ {
+ if (fprintf(fp, "%u\n", *entry) < 0)
+ {
+ hash_seq_term(&status);
+ failed = true;
+ break;
+ }
+ }
+ }
+
+ if (!failed && fputs("[relations]\n", fp) == EOF)
+ failed = true;
+
+ if (!failed)
+ {
+ hash_seq_init(&status, evs_track_relations_hash);
+ while ((rel_entry = (EvsTrackRelKey *) hash_seq_search(&status)) != NULL)
+ {
+ int rc;
+
+ if (OidIsValid(rel_entry->dboid))
+ rc = fprintf(fp, "%u %u\n", rel_entry->dboid, rel_entry->reloid);
+ else
+ rc = fprintf(fp, "0 %u\n", rel_entry->reloid);
+ if (rc < 0)
+ {
+ hash_seq_term(&status);
+ failed = true;
+ break;
+ }
+ }
+ }
+
+ if (!failed && fflush(fp) != 0)
+ failed = true;
+
+ if (!failed)
+ {
+ int fd = fileno(fp);
+
+ if (fd >= 0 && pg_fsync(fd) != 0)
+ ereport(LOG,
+ (errcode_for_file_access(),
+ errmsg("could not fsync track file \"%s\": %m",
+ tmppath)));
+ }
+ }
+ PG_CATCH();
+ {
+ FreeFile(fp);
+ (void) unlink(tmppath);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+
+ if (FreeFile(fp) != 0)
+ {
+ ereport(LOG,
+ (errcode_for_file_access(),
+ errmsg("could not close track file \"%s\": %m", tmppath)));
+ failed = true;
+ }
+
+ if (failed)
+ {
+ ereport(LOG,
+ (errcode_for_file_access(),
+ errmsg("could not write track file \"%s\": %m", tmppath)));
+ if (unlink(tmppath) != 0 && errno != ENOENT)
+ ereport(LOG,
+ (errcode_for_file_access(),
+ errmsg("could not unlink \"%s\": %m", tmppath)));
+ return;
+ }
+
+ if (durable_rename(tmppath, path, LOG) != 0)
+ {
+ if (unlink(tmppath) != 0 && errno != ENOENT)
+ ereport(LOG,
+ (errcode_for_file_access(),
+ errmsg("could not unlink \"%s\": %m", tmppath)));
+ }
+}
+
+/*
+ * Check if OID is in the given hash
+ */
+static bool
+evs_oid_in_list(HTAB *hash, Oid oid)
+{
+ if (!hash)
+ return false;
+ if (hash_get_num_entries(hash) == 0)
+ return false;
+ return hash_search(hash, &oid, HASH_FIND, NULL) != NULL;
+}
+
+/*
+ * Check if (dboid, relid) is in track_relations list.
+ */
+static bool
+evs_rel_in_list(Oid dboid, Oid relid)
+{
+ EvsTrackRelKey key;
+
+ if (!evs_track_relations_hash)
+ return false;
+ if (hash_get_num_entries(evs_track_relations_hash) == 0)
+ return false;
+ key.dboid = dboid;
+ key.reloid = relid;
+ if (hash_search(evs_track_relations_hash, &key, HASH_FIND, NULL) != NULL)
+ return true;
+ key.dboid = InvalidOid;
+ return hash_search(evs_track_relations_hash, &key, HASH_FIND, NULL) != NULL;
+}
+
+/*
+ * Decide whether to track statistics for relations.
+ * Relation is tracked if it is in the track list or a special filter is enabled.
+ */
+static bool
+evs_should_track_relation_statistics(Oid dboid, Oid relid)
+{
+ evs_track_hash_ensure_init();
+
+ if (evs_track_databases_from_list &&
+ !evs_oid_in_list(evs_track_databases_hash, dboid))
+ return false;
+ if (evs_track_relations_from_list &&
+ !(evs_rel_in_list(dboid, relid) || evs_rel_in_list(InvalidOid, relid)))
+ return false;
+
+ if ((evs_track_bits & EVS_TRACK_RELATIONS) == 0)
+ return false; /* database-only mode */
+ if (evs_track_relations_bits == EVS_FILTER_SYSTEM)
+ return IsCatalogRelationOid(relid);
+ if (evs_track_relations_bits == EVS_FILTER_USER)
+ return !IsCatalogRelationOid(relid);
+ return true;
+}
+
+/*
+ * Decide whether to track statistics for databases.
+ * Database statistics is tracked if it is in the track list or a special filter is enabled.
+ */
+static bool
+evs_should_track_database_statistics(Oid dboid)
+{
+ evs_track_hash_ensure_init();
+
+ if (evs_track_databases_from_list &&
+ !evs_oid_in_list(evs_track_databases_hash, dboid))
+ return false;
+ if ((evs_track_bits & EVS_TRACK_DATABASES) == 0)
+ return false; /* relations-only mode */
+ if (evs_track_bits == EVS_TRACK_DATABASES)
+ return true; /* databases-only, accumulate to db */
+ return true;
+}
+
+
+/* Accumulate common counts for database-level stats. */
+static inline void
+pgstat_accumulate_common_for_db(PgStat_CommonCounts * dst,
+ const PgStat_CommonCounts * src)
+{
+ pgstat_accumulate_common(dst, src);
+}
+
+/*
+ * Store incoming vacuum stats into pgstat custom statistics.
+ * store_relation: create/update per-relation entry
+ * store_db: accumulate into database-level entry (dboid, objid=0).
+ * Uses pgstat_get_entry_ref_locked and pgstat_accumulate_* for atomic updates.
+ */
+static void
+extvac_store(Oid dboid, Oid relid, int type,
+ PgStat_VacuumRelationCounts * params,
+ bool store_relation, bool store_db)
+{
+ PgStat_EntryRef *entry_ref;
+ PgStatShared_ExtVacEntry *shared;
+ uint64 objid;
+
+ if (!evs_enabled)
+ return;
+
+ if (store_relation)
+ {
+ objid = EXTVAC_OBJID(relid, type);
+ entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_EXTVAC_RELATION, dboid, objid, false);
+ if (entry_ref)
+ {
+ shared = (PgStatShared_ExtVacEntry *) entry_ref->shared_stats;
+ if (shared->stats.type == PGSTAT_EXTVAC_INVALID)
+ {
+ memset(&shared->stats, 0, sizeof(shared->stats));
+ shared->stats.type = params->type;
+ }
+ pgstat_accumulate_extvac_stats(&shared->stats, params);
+ pgstat_unlock_entry(entry_ref);
+ }
+ }
+
+ if (store_db)
+ {
+ entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_EXTVAC_DB, dboid, InvalidOid, false);
+ if (entry_ref)
+ {
+ shared = (PgStatShared_ExtVacEntry *) entry_ref->shared_stats;
+ if (shared->stats.type == PGSTAT_EXTVAC_INVALID)
+ {
+ memset(&shared->stats, 0, sizeof(shared->stats));
+ shared->stats.type = PGSTAT_EXTVAC_DB;
+ }
+ pgstat_accumulate_common_for_db(&shared->stats.common, ¶ms->common);
+ pgstat_unlock_entry(entry_ref);
+ }
+ }
+}
+
+/*
+ * Vacuum report hook: called when vacuum finishes. Filters by track settings,
+ * stores stats per-relation and/or per-database, then chains to previous hook.
+ */
+static void
+pgstat_report_vacuum_extstats(Oid tableoid, bool shared,
+ PgStat_VacuumRelationCounts * params)
+{
+ Oid dboid = shared ? InvalidOid : MyDatabaseId;
+ bool store_relation;
+ bool store_db;
+
+ if (evs_enabled)
+ {
+ store_relation = evs_should_track_relation_statistics(dboid, tableoid);
+ store_db = evs_should_track_database_statistics(dboid);
+
+ if (store_relation || store_db)
+ extvac_store(dboid, tableoid, params->type, params, store_relation, store_db);
+ }
+ if (prev_report_vacuum_hook)
+ prev_report_vacuum_hook(tableoid, shared, params);
+}
+
+/* Reset statistics for a single relation entry. */
+static bool
+extvac_reset_by_relid(Oid dboid, Oid relid, int type)
+{
+ uint64 objid = EXTVAC_OBJID(relid, type);
+
+ pgstat_reset_entry(PGSTAT_KIND_EXTVAC_RELATION, dboid, objid, 0);
+ return true;
+}
+
+/* Callback for pgstat_reset_matching_entries: match relation entries for given db */
+static bool
+match_extvac_relations_for_db(PgStatShared_HashEntry *entry, Datum match_data)
+{
+ return entry->key.kind == PGSTAT_KIND_EXTVAC_RELATION &&
+ entry->key.dboid == DatumGetObjectId(match_data);
+}
+
+/*
+ * Reset statistics for a database (aggregate entry) and all its relations.
+ */
+static int64
+extvac_database_reset(Oid dboid)
+{
+ pgstat_reset_matching_entries(match_extvac_relations_for_db,
+ ObjectIdGetDatum(dboid), 0);
+ pgstat_reset_entry(PGSTAT_KIND_EXTVAC_DB, dboid, 0, 0);
+ return 1;
+}
+
+/* Reset all vacuum statistics (both relation and database entries). */
+static int64
+extvac_stat_reset(void)
+{
+ pgstat_reset_of_kind(PGSTAT_KIND_EXTVAC_RELATION);
+ pgstat_reset_of_kind(PGSTAT_KIND_EXTVAC_DB);
+ return 0; /* count not available */
+}
+
+PG_FUNCTION_INFO_V1(vacuum_statistics_reset);
+PG_FUNCTION_INFO_V1(extvac_shared_memory_size);
+PG_FUNCTION_INFO_V1(extvac_reset_entry);
+PG_FUNCTION_INFO_V1(extvac_reset_db_entry);
+
+Datum
+vacuum_statistics_reset(PG_FUNCTION_ARGS)
+{
+ PG_RETURN_INT64(extvac_stat_reset());
+}
+
+Datum
+extvac_reset_entry(PG_FUNCTION_ARGS)
+{
+ Oid dboid = PG_GETARG_OID(0);
+ Oid relid = PG_GETARG_OID(1);
+ int type = PG_GETARG_INT32(2);
+
+ PG_RETURN_BOOL(extvac_reset_by_relid(dboid, relid, type));
+}
+
+Datum
+extvac_reset_db_entry(PG_FUNCTION_ARGS)
+{
+ Oid dboid = PG_GETARG_OID(0);
+
+ PG_RETURN_INT64(extvac_database_reset(dboid));
+}
+
+/*
+ * Return total shared memory in bytes used by the extension for vacuum stats.
+ * Used for monitoring and capacity planning: memory grows with the number of
+ * tracked relations and databases.
+ */
+Datum
+extvac_shared_memory_size(PG_FUNCTION_ARGS)
+{
+ uint64 rel_count;
+ uint64 db_count;
+ uint64 total;
+ size_t entry_size = sizeof(PgStatShared_ExtVacEntry);
+
+ rel_count = pgstat_get_entry_count(PGSTAT_KIND_EXTVAC_RELATION);
+ db_count = pgstat_get_entry_count(PGSTAT_KIND_EXTVAC_DB);
+ total = rel_count + db_count;
+
+ PG_RETURN_INT64((int64) (total * entry_size));
+}
+
+/*
+ * Track list management: add/remove database or relation OIDs.
+ * Changes are persisted to pg_stat/ext_vacuum_statistics_track.oid.
+ */
+
+PG_FUNCTION_INFO_V1(evs_add_track_database);
+PG_FUNCTION_INFO_V1(evs_remove_track_database);
+PG_FUNCTION_INFO_V1(evs_add_track_relation);
+PG_FUNCTION_INFO_V1(evs_remove_track_relation);
+
+/*
+ * Mutating track-list entry points: require server-wide privilege, since
+ * the underlying lists steer tracking for every backend.
+ */
+static void
+evs_require_track_privilege(const char *funcname)
+{
+ if (!superuser() && !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("permission denied for function %s", funcname),
+ errhint("Only superusers and members of pg_read_all_stats "
+ "may change the vacuum statistics track list.")));
+}
+
+Datum
+evs_add_track_database(PG_FUNCTION_ARGS)
+{
+ Oid oid = PG_GETARG_OID(0);
+ bool found;
+ LWLock *lock;
+
+ evs_require_track_privilege("add_track_database");
+ lock = evs_get_track_lock();
+ LWLockAcquire(lock, LW_EXCLUSIVE);
+ evs_track_hash_ensure_init();
+ hash_search(evs_track_databases_hash, &oid, HASH_ENTER, &found);
+ evs_track_save_file();
+ LWLockRelease(lock);
+ PG_RETURN_BOOL(!found); /* true if newly added */
+}
+
+Datum
+evs_remove_track_database(PG_FUNCTION_ARGS)
+{
+ Oid oid = PG_GETARG_OID(0);
+ bool found;
+ LWLock *lock;
+
+ evs_require_track_privilege("remove_track_database");
+ lock = evs_get_track_lock();
+ LWLockAcquire(lock, LW_EXCLUSIVE);
+ evs_track_hash_ensure_init();
+ hash_search(evs_track_databases_hash, &oid, HASH_REMOVE, &found);
+ evs_track_save_file();
+ LWLockRelease(lock);
+ PG_RETURN_BOOL(found);
+}
+
+Datum
+evs_add_track_relation(PG_FUNCTION_ARGS)
+{
+ EvsTrackRelKey key;
+ bool found;
+ LWLock *lock;
+
+ evs_require_track_privilege("add_track_relation");
+ key.dboid = PG_GETARG_OID(0);
+ key.reloid = PG_GETARG_OID(1);
+ lock = evs_get_track_lock();
+ LWLockAcquire(lock, LW_EXCLUSIVE);
+ evs_track_hash_ensure_init();
+ hash_search(evs_track_relations_hash, &key, HASH_ENTER, &found);
+ evs_track_save_file();
+ LWLockRelease(lock);
+ PG_RETURN_BOOL(!found); /* true if newly added */
+}
+
+Datum
+evs_remove_track_relation(PG_FUNCTION_ARGS)
+{
+ EvsTrackRelKey key;
+ bool found;
+ LWLock *lock;
+
+ evs_require_track_privilege("remove_track_relation");
+ key.dboid = PG_GETARG_OID(0);
+ key.reloid = PG_GETARG_OID(1);
+ lock = evs_get_track_lock();
+ LWLockAcquire(lock, LW_EXCLUSIVE);
+ evs_track_hash_ensure_init();
+ hash_search(evs_track_relations_hash, &key, HASH_REMOVE, &found);
+ evs_track_save_file();
+ LWLockRelease(lock);
+ PG_RETURN_BOOL(found);
+}
+
+/*
+ * Returns the list of database and relation OIDs for which statistics
+ * are collected.
+ */
+PG_FUNCTION_INFO_V1(evs_track_list);
+
+Datum
+evs_track_list(PG_FUNCTION_ARGS)
+{
+ ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
+ TupleDesc tupdesc;
+ Tuplestorestate *tupstore;
+ MemoryContext per_query_ctx;
+ MemoryContext oldcontext;
+ Datum values[3];
+ bool nulls[3] = {false, false, false};
+ HASH_SEQ_STATUS status;
+ Oid *entry;
+ EvsTrackRelKey *rel_entry;
+
+ if (!rsinfo || !IsA(rsinfo, ReturnSetInfo))
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("ext_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("ext_vacuum_statistics: materialize mode required")));
+
+ evs_track_hash_ensure_init();
+
+ per_query_ctx = rsinfo->econtext->ecxt_per_query_memory;
+ oldcontext = MemoryContextSwitchTo(per_query_ctx);
+
+ if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
+ elog(ERROR, "ext_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;
+
+ /* Databases */
+ if (hash_get_num_entries(evs_track_databases_hash) == 0)
+ {
+ values[0] = CStringGetTextDatum("database");
+ nulls[1] = true;
+ nulls[2] = true;
+ tuplestore_putvalues(tupstore, tupdesc, values, nulls);
+ nulls[1] = false;
+ nulls[2] = false;
+ }
+ else
+ {
+ hash_seq_init(&status, evs_track_databases_hash);
+ while ((entry = (Oid *) hash_seq_search(&status)) != NULL)
+ {
+ values[0] = CStringGetTextDatum("database");
+ values[1] = ObjectIdGetDatum(*entry);
+ nulls[2] = true;
+ tuplestore_putvalues(tupstore, tupdesc, values, nulls);
+ nulls[2] = false;
+ }
+ }
+
+ /* Relations */
+ if (hash_get_num_entries(evs_track_relations_hash) == 0)
+ {
+ values[0] = CStringGetTextDatum("relation");
+ nulls[1] = true;
+ nulls[2] = true;
+ tuplestore_putvalues(tupstore, tupdesc, values, nulls);
+ nulls[1] = false;
+ nulls[2] = false;
+ }
+ else
+ {
+ hash_seq_init(&status, evs_track_relations_hash);
+ while ((rel_entry = (EvsTrackRelKey *) hash_seq_search(&status)) != NULL)
+ {
+ values[0] = CStringGetTextDatum("relation");
+ values[1] = ObjectIdGetDatum(rel_entry->dboid);
+ values[2] = ObjectIdGetDatum(rel_entry->reloid);
+ tuplestore_putvalues(tupstore, tupdesc, values, nulls);
+ }
+ }
+
+ MemoryContextSwitchTo(oldcontext);
+
+ return (Datum) 0;
+}
+
+/*
+ * Output vacuum statistics (tables, indexes, or per-database aggregates).
+ */
+#define EXTVAC_COMMON_STAT_COLS 12
+
+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);
+ 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);
+ 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->common.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->common.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);
+}
+
+static Datum
+pg_stats_vacuum(FunctionCallInfo fcinfo, int type)
+{
+ ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
+ MemoryContext per_query_ctx;
+ MemoryContext oldcontext;
+ Tuplestorestate *tupstore;
+ TupleDesc tupdesc;
+ Oid dbid = PG_GETARG_OID(0);
+
+ if (rsinfo == NULL || !IsA(rsinfo, ReturnSetInfo))
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("ext_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("ext_vacuum_statistics: materialize mode required")));
+
+ per_query_ctx = rsinfo->econtext->ecxt_per_query_memory;
+ oldcontext = MemoryContextSwitchTo(per_query_ctx);
+
+ if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
+ elog(ERROR, "ext_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);
+ PgStat_VacuumRelationCounts *stats;
+
+ if (!OidIsValid(relid))
+ return (Datum) 0;
+
+ stats = (PgStat_VacuumRelationCounts *)
+ pgstat_fetch_entry(PGSTAT_KIND_EXTVAC_RELATION, dbid,
+ EXTVAC_OBJID(relid, type), NULL);
+
+ if (!stats)
+ stats = (PgStat_VacuumRelationCounts *)
+ pgstat_fetch_entry(PGSTAT_KIND_EXTVAC_RELATION, InvalidOid,
+ EXTVAC_OBJID(relid, type), NULL);
+
+ if (stats && stats->type == type)
+ tuplestore_put_for_relation(relid, tupstore, tupdesc, stats);
+ }
+ else if (type == PGSTAT_EXTVAC_DB)
+ {
+ if (OidIsValid(dbid))
+ {
+#define EXTVAC_DB_STAT_COLS 14
+ Datum values[EXTVAC_DB_STAT_COLS];
+ bool nulls[EXTVAC_DB_STAT_COLS];
+ int i = 0;
+ PgStat_VacuumRelationCounts *stats;
+
+ stats = (PgStat_VacuumRelationCounts *)
+ pgstat_fetch_entry(PGSTAT_KIND_EXTVAC_DB, dbid,
+ InvalidOid, NULL);
+ if (stats && stats->type == PGSTAT_EXTVAC_DB)
+ {
+ memset(nulls, 0, sizeof(nulls));
+ values[i++] = ObjectIdGetDatum(dbid);
+ tuplestore_put_common(&stats->common, values, nulls, &i);
+ values[i++] = Int32GetDatum(stats->common.interrupts_count);
+ Assert(i == EXTVAC_DB_STAT_COLS);
+ tuplestore_putvalues(tupstore, tupdesc, values, nulls);
+ }
+ }
+ /* invalid dbid: return empty set */
+ }
+ else
+ elog(PANIC, "ext_vacuum_statistics: invalid type %d", type);
+
+ 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);
+
+Datum
+pg_stats_get_vacuum_tables(PG_FUNCTION_ARGS)
+{
+ return pg_stats_vacuum(fcinfo, PGSTAT_EXTVAC_TABLE);
+}
+
+Datum
+pg_stats_get_vacuum_indexes(PG_FUNCTION_ARGS)
+{
+ return pg_stats_vacuum(fcinfo, PGSTAT_EXTVAC_INDEX);
+}
+
+Datum
+pg_stats_get_vacuum_database(PG_FUNCTION_ARGS)
+{
+ return pg_stats_vacuum(fcinfo, PGSTAT_EXTVAC_DB);
+}
diff --git a/contrib/meson.build b/contrib/meson.build
index ebb7f83d8c5..d7dc0fd07f0 100644
--- a/contrib/meson.build
+++ b/contrib/meson.build
@@ -26,6 +26,7 @@ subdir('cube')
subdir('dblink')
subdir('dict_int')
subdir('dict_xsyn')
+subdir('ext_vacuum_statistics')
subdir('earthdistance')
subdir('file_fdw')
subdir('fuzzystrmatch')
diff --git a/doc/src/sgml/contrib.sgml b/doc/src/sgml/contrib.sgml
index b9b03654aad..2a38f9042bb 100644
--- a/doc/src/sgml/contrib.sgml
+++ b/doc/src/sgml/contrib.sgml
@@ -141,6 +141,7 @@ CREATE EXTENSION <replaceable>extension_name</replaceable>;
&dict-int;
&dict-xsyn;
&earthdistance;
+ &extvacuumstatistics;
&file-fdw;
&fuzzystrmatch;
&hstore;
diff --git a/doc/src/sgml/extvacuumstatistics.sgml b/doc/src/sgml/extvacuumstatistics.sgml
new file mode 100644
index 00000000000..75eb4691c4d
--- /dev/null
+++ b/doc/src/sgml/extvacuumstatistics.sgml
@@ -0,0 +1,502 @@
+<!-- doc/src/sgml/extvacuumstatistics.sgml -->
+
+<sect1 id="extvacuumstatistics" xreflabel="ext_vacuum_statistics">
+ <title>ext_vacuum_statistics — extended vacuum statistics</title>
+
+ <indexterm zone="extvacuumstatistics">
+ <primary>ext_vacuum_statistics</primary>
+ </indexterm>
+
+ <para>
+ The <filename>ext_vacuum_statistics</filename> module provides
+ extended per-table, per-index, and per-database vacuum statistics
+ (buffer I/O, WAL, general, timing) via views in the
+ <literal>ext_vacuum_statistics</literal> schema.
+ </para>
+
+ <para>
+ The module must be loaded by adding <literal>ext_vacuum_statistics</literal> to
+ <xref linkend="guc-shared-preload-libraries"/> in
+ <filename>postgresql.conf</filename>, because it registers a vacuum hook at
+ server startup. This means that a server restart is needed to add or remove
+ the module. After installation, run
+ <command>CREATE EXTENSION ext_vacuum_statistics</command> in each database
+ where you want to use it.
+ </para>
+
+ <para>
+ When active, the module provides views
+ <structname>ext_vacuum_statistics.pg_stats_vacuum_tables</structname>,
+ <structname>ext_vacuum_statistics.pg_stats_vacuum_indexes</structname>, and
+ <structname>ext_vacuum_statistics.pg_stats_vacuum_database</structname>,
+ plus functions to reset statistics and manage tracking.
+ </para>
+
+ <para>
+ Each tracked object (one table, one index, or one database) uses
+ approximately 232 bytes of shared memory on Linux x86_64 (e.g. Ubuntu):
+ common stats (buffers, WAL, timing) plus header and LWLock ~144 bytes;
+ type + union ~88 bytes (the union holds table-specific or index-specific
+ fields; the allocated size is the same for both). The exact size depends on the platform. Call
+ <function>ext_vacuum_statistics.shared_memory_size()</function> to get
+ the total shared memory used by the extension. The extension's GUCs allow controlling memory by limiting
+ which objects are tracked:
+ <varname>vacuum_statistics.object_types</varname>,
+ <varname>vacuum_statistics.track_relations</varname>, and
+ <varname>track_*_from_list</varname>.
+ Example: a database with 1000 tables and 2000 indexes uses about 700 KB
+ on Ubuntu ((1000 + 2000 + 1) × 232 bytes).
+ </para>
+
+ <sect2 id="extvacuumstatistics-pg-stats-vacuum-tables">
+ <title>The <structname>ext_vacuum_statistics.pg_stats_vacuum_tables</structname> View</title>
+
+ <indexterm zone="extvacuumstatistics">
+ <secondary>pg_stats_vacuum_tables</secondary>
+ </indexterm>
+
+ <para>
+ The view <structname>ext_vacuum_statistics.pg_stats_vacuum_tables</structname>
+ contains one row for each table in the current database (including TOAST
+ tables), showing statistics about vacuuming that specific table. The columns
+ are shown in <xref linkend="extvacuumstatistics-pg-stats-vacuum-tables-columns"/>.
+ </para>
+
+ <table id="extvacuumstatistics-pg-stats-vacuum-tables-columns">
+ <title><structname>ext_vacuum_statistics.pg_stats_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>dbname</structfield> <type>name</type>
+ </para>
+ <para>
+ Name of the database containing 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
+ </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
+ </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
+ </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
+ </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
+ </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 blocks by vacuum operations, in milliseconds
+ </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 blocks by vacuum operations, in milliseconds
+ </para></entry>
+ </row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>delay_time</structfield> <type>float8</type>
+ </para>
+ <para>
+ Time spent in vacuum delay points, 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>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>wraparound_failsafe_count</structfield> <type>int4</type>
+ </para>
+ <para>
+ Number of times vacuum was run to prevent a wraparound problem
+ </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 found in the buffer cache by vacuum
+ </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>pages_scanned</structfield> <type>int8</type>
+ </para>
+ <para>
+ Number of pages examined by vacuum operations
+ </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 physical storage by vacuum operations
+ </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 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 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 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_frozen</structfield> <type>int8</type>
+ </para>
+ <para>
+ Number of tuples 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 left due to visibility in transactions
+ </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>missed_dead_pages</structfield> <type>int8</type>
+ </para>
+ <para>
+ Number of pages that had at least one missed dead tuple
+ </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 tuples that could not be pruned due to failure to acquire a cleanup lock
+ </para></entry>
+ </row>
+ </tbody>
+ </tgroup>
+ </table>
+ </sect2>
+
+ <sect2 id="extvacuumstatistics-pg-stats-vacuum-indexes">
+ <title>The <structname>ext_vacuum_statistics.pg_stats_vacuum_indexes</structname> View</title>
+
+ <indexterm zone="extvacuumstatistics">
+ <secondary>pg_stats_vacuum_indexes</secondary>
+ </indexterm>
+
+ <para>
+ The view <structname>ext_vacuum_statistics.pg_stats_vacuum_indexes</structname>
+ contains one row for each index in the current database, showing statistics
+ about vacuuming that specific index. Columns include
+ <structfield>indexrelid</structfield>, <structfield>schema</structfield>,
+ <structfield>indexrelname</structfield>, <structfield>dbname</structfield>,
+ buffer I/O (<structfield>total_blks_read</structfield>,
+ <structfield>total_blks_hit</structfield>, etc.), WAL
+ (<structfield>wal_records</structfield>, <structfield>wal_fpi</structfield>,
+ <structfield>wal_bytes</structfield>), timing
+ (<structfield>blk_read_time</structfield>, <structfield>blk_write_time</structfield>,
+ <structfield>delay_time</structfield>, <structfield>total_time</structfield>),
+ and <structfield>tuples_deleted</structfield>, <structfield>pages_deleted</structfield>.
+ </para>
+ </sect2>
+
+ <sect2 id="extvacuumstatistics-pg-stats-vacuum-database">
+ <title>The <structname>ext_vacuum_statistics.pg_stats_vacuum_database</structname> View</title>
+
+ <indexterm zone="extvacuumstatistics">
+ <secondary>pg_stats_vacuum_database</secondary>
+ </indexterm>
+
+ <para>
+ The view <structname>ext_vacuum_statistics.pg_stats_vacuum_database</structname>
+ contains one row for each database in the cluster, showing aggregate vacuum
+ statistics for that database. Columns include
+ <structfield>dboid</structfield>, <structfield>dbname</structfield>,
+ <structfield>db_blks_read</structfield>, <structfield>db_blks_hit</structfield>,
+ <structfield>db_blks_dirtied</structfield>, <structfield>db_blks_written</structfield>,
+ WAL stats (<structfield>db_wal_records</structfield>,
+ <structfield>db_wal_fpi</structfield>, <structfield>db_wal_bytes</structfield>),
+ timing (<structfield>db_blk_read_time</structfield>,
+ <structfield>db_blk_write_time</structfield>, <structfield>db_delay_time</structfield>,
+ <structfield>db_total_time</structfield>),
+ <structfield>db_wraparound_failsafe_count</structfield>, and
+ <structfield>interrupts_count</structfield>.
+ </para>
+ </sect2>
+
+ <sect2 id="extvacuumstatistics-functions">
+ <title>Functions</title>
+
+ <variablelist>
+ <varlistentry>
+ <term>
+ <function>ext_vacuum_statistics.shared_memory_size()</function>
+ <returnvalue>bigint</returnvalue>
+ </term>
+ <listitem>
+ <para>
+ Returns the total shared memory in bytes used by the extension for
+ vacuum statistics (relations plus databases).
+ </para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term>
+ <function>ext_vacuum_statistics.vacuum_statistics_reset()</function>
+ <returnvalue>bigint</returnvalue>
+ </term>
+ <listitem>
+ <para>
+ Resets all vacuum statistics. Returns the number of entries reset.
+ </para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term>
+ <function>ext_vacuum_statistics.add_track_database(dboid oid)</function>
+ <returnvalue>boolean</returnvalue>
+ </term>
+ <listitem>
+ <para>
+ Adds a database OID to the tracking list (persisted to
+ <filename>pg_stat/ext_vacuum_statistics_track.oid</filename>).
+ Returns true if newly added.
+ </para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term>
+ <function>ext_vacuum_statistics.remove_track_database(dboid oid)</function>
+ <returnvalue>boolean</returnvalue>
+ </term>
+ <listitem>
+ <para>
+ Removes a database OID from the tracking list. Returns true if found and removed.
+ </para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term>
+ <function>ext_vacuum_statistics.add_track_relation(dboid oid, reloid oid)</function>
+ <returnvalue>boolean</returnvalue>
+ </term>
+ <listitem>
+ <para>
+ Adds a (database, relation) OID pair to the tracking list. Returns true if newly added.
+ </para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term>
+ <function>ext_vacuum_statistics.remove_track_relation(dboid oid, reloid oid)</function>
+ <returnvalue>boolean</returnvalue>
+ </term>
+ <listitem>
+ <para>
+ Removes a (database, relation) pair from the tracking list. Returns true if found and removed.
+ </para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term>
+ <function>ext_vacuum_statistics.track_list()</function>
+ <returnvalue>TABLE(track_kind text, dboid oid, reloid oid)</returnvalue>
+ </term>
+ <listitem>
+ <para>
+ Returns the list of database and relation OIDs for which vacuum statistics
+ are collected. When <structfield>dboid</structfield> or
+ <structfield>reloid</structfield> is NULL, statistics are collected for all.
+ </para>
+ </listitem>
+ </varlistentry>
+ </variablelist>
+ </sect2>
+
+ <sect2 id="extvacuumstatistics-configuration">
+ <title>Configuration Parameters</title>
+
+ <variablelist>
+ <varlistentry>
+ <term><varname>vacuum_statistics.enabled</varname> (<type>boolean</type>)</term>
+ <listitem>
+ <para>
+ Enables extended vacuum statistics collection. Default: <literal>on</literal>.
+ </para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><varname>vacuum_statistics.object_types</varname> (<type>string</type>)</term>
+ <listitem>
+ <para>
+ Object types for statistics: <literal>all</literal>, <literal>databases</literal>, or
+ <literal>relations</literal>. Default: <literal>all</literal>.
+ </para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><varname>vacuum_statistics.track_relations</varname> (<type>string</type>)</term>
+ <listitem>
+ <para>
+ When tracking relations: <literal>all</literal>, <literal>system</literal>, or
+ <literal>user</literal>. Default: <literal>all</literal>.
+ </para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><varname>vacuum_statistics.track_databases_from_list</varname> (<type>boolean</type>)</term>
+ <listitem>
+ <para>
+ If on, track only databases added via <function>add_track_database</function>.
+ Default: <literal>off</literal>.
+ </para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><varname>vacuum_statistics.track_relations_from_list</varname> (<type>boolean</type>)</term>
+ <listitem>
+ <para>
+ If on, track only relations added via <function>add_track_relation</function>.
+ Default: <literal>off</literal>.
+ </para>
+ </listitem>
+ </varlistentry>
+ </variablelist>
+ </sect2>
+</sect1>
diff --git a/doc/src/sgml/filelist.sgml b/doc/src/sgml/filelist.sgml
index 25a85082759..85d721467c0 100644
--- a/doc/src/sgml/filelist.sgml
+++ b/doc/src/sgml/filelist.sgml
@@ -133,6 +133,7 @@
<!ENTITY dict-xsyn SYSTEM "dict-xsyn.sgml">
<!ENTITY dummy-seclabel SYSTEM "dummy-seclabel.sgml">
<!ENTITY earthdistance SYSTEM "earthdistance.sgml">
+<!ENTITY extvacuumstatistics SYSTEM "extvacuumstatistics.sgml">
<!ENTITY file-fdw SYSTEM "file-fdw.sgml">
<!ENTITY fuzzystrmatch SYSTEM "fuzzystrmatch.sgml">
<!ENTITY hstore SYSTEM "hstore.sgml">
--
2.39.5 (Apple Git-154)
Attachments:
[text/plain] v38-0001-Track-table-VM-stability.patch (21.7K, 3-v38-0001-Track-table-VM-stability.patch)
download | inline diff:
From 19f5a39f7e97d3fc2d18415ba2c51ffcd3b32f49 Mon Sep 17 00:00:00 2001
From: Alena Rybakina <[email protected]>
Date: Mon, 30 Mar 2026 09:07:24 +0300
Subject: [PATCH 1/3] Track table VM stability.
Add rev_all_visible_pages and rev_all_frozen_pages counters to
pg_stat_all_tables tracking the number of times the all-visible and
all-frozen bits are cleared in the visibility map. These bits are cleared by
backend processes during regular DML operations. Hence, the counters are placed
in table statistic entry.
A high rev_all_visible_pages rate relative to DML volume indicates
that modifications are scattered across previously-clean pages rather
than concentrated on already-dirty ones, causing index-only scans to
fall back to heap fetches. A high rev_all_frozen_pages rate indicates
that vacuum's freezing work is being frequently undone by concurrent
DML.
Authors: Alena Rybakina <[email protected]>,
Andrei Lepikhov <[email protected]>,
Andrei Zubkov <[email protected]>
Reviewed-by: Dilip Kumar <[email protected]>,
Masahiko Sawada <[email protected]>,
Ilia Evdokimov <[email protected]>,
Jian He <[email protected]>,
Kirill Reshke <[email protected]>,
Alexander Korotkov <[email protected]>,
Jim Nasby <[email protected]>,
Sami Imseih <[email protected]>,
Karina Litskevich <[email protected]>,
Andrey Borodin <[email protected]>
---
doc/src/sgml/monitoring.sgml | 32 +++
src/backend/access/heap/visibilitymap.c | 10 +
src/backend/catalog/system_views.sql | 4 +-
src/backend/utils/activity/pgstat_relation.c | 2 +
src/backend/utils/adt/pgstatfuncs.c | 6 +
src/include/catalog/pg_proc.dat | 10 +
src/include/pgstat.h | 17 +-
.../expected/vacuum-extending-freeze.out | 185 ++++++++++++++++++
src/test/isolation/isolation_schedule | 1 +
.../specs/vacuum-extending-freeze.spec | 117 +++++++++++
src/test/regress/expected/rules.out | 12 +-
11 files changed, 391 insertions(+), 5 deletions(-)
create mode 100644 src/test/isolation/expected/vacuum-extending-freeze.out
create mode 100644 src/test/isolation/specs/vacuum-extending-freeze.spec
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 08d5b824552..3467abf6d8a 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -4377,6 +4377,38 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>visible_page_marks_cleared</structfield> <type>bigint</type>
+ </para>
+ <para>
+ Number of times the all-visible mark in the
+ <link linkend="storage-vm">visibility map</link> was cleared for
+ pages of this table. The all-visible mark of a heap page is
+ cleared whenever a backend process modifies a page that was
+ previously marked all-visible by vacuum activity (whether manual
+ <command>VACUUM</command> or autovacuum). The page must then be
+ processed again by vacuum on a subsequent run. A high rate of
+ change in this counter means that vacuum has to repeatedly
+ re-process pages of this table.
+ </para></entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>frozen_page_marks_cleared</structfield> <type>bigint</type>
+ </para>
+ <para>
+ Number of times the all-frozen mark in the
+ <link linkend="storage-vm">visibility map</link> was cleared for
+ pages of this table. The all-frozen mark of a heap page is cleared
+ whenever a backend process modifies a page that was previously
+ marked all-frozen by vacuum activity (manual <command>VACUUM</command>
+ or autovacuum). The page must then be processed again by vacuum on
+ the next freeze run for this table.
+ </para></entry>
+ </row>
+
<row>
<entry role="catalog_table_entry"><para role="column_definition">
<structfield>last_vacuum</structfield> <type>timestamp with time zone</type>
diff --git a/src/backend/access/heap/visibilitymap.c b/src/backend/access/heap/visibilitymap.c
index 4fd470702aa..f055ec3819c 100644
--- a/src/backend/access/heap/visibilitymap.c
+++ b/src/backend/access/heap/visibilitymap.c
@@ -102,6 +102,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"
@@ -173,6 +174,15 @@ visibilitymap_clear(Relation rel, BlockNumber heapBlk, Buffer vmbuf, uint8 flags
if (map[mapByte] & mask)
{
+ /*
+ * Track how often all-visible or all-frozen bits are cleared in the
+ * visibility map.
+ */
+ if (map[mapByte] & ((flags & VISIBILITYMAP_ALL_VISIBLE) << mapOffset))
+ pgstat_count_visible_page_marks_cleared(rel);
+ if (map[mapByte] & ((flags & VISIBILITYMAP_ALL_FROZEN) << mapOffset))
+ pgstat_count_frozen_page_marks_cleared(rel);
+
map[mapByte] &= ~mask;
MarkBufferDirty(vmbuf);
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index 73a1c1c4670..71e993c8783 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -747,7 +747,9 @@ CREATE VIEW pg_stat_all_tables AS
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_visible_page_marks_cleared(C.oid) AS visible_page_marks_cleared,
+ pg_stat_get_frozen_page_marks_cleared(C.oid) AS frozen_page_marks_cleared
FROM pg_class C LEFT JOIN
pg_index I ON C.oid = I.indrelid
LEFT JOIN pg_namespace N ON (N.oid = C.relnamespace)
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index b2ca28f83ba..92e1f60a080 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -881,6 +881,8 @@ 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->visible_page_marks_cleared += lstats->counts.visible_page_marks_cleared;
+ tabentry->frozen_page_marks_cleared += lstats->counts.frozen_page_marks_cleared;
/* Clamp live_tuples in case of negative delta_live_tuples */
tabentry->live_tuples = Max(tabentry->live_tuples, 0);
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 1408de387ea..b6f064338fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -108,6 +108,12 @@ PG_STAT_GET_RELENTRY_INT64(tuples_updated)
/* pg_stat_get_vacuum_count */
PG_STAT_GET_RELENTRY_INT64(vacuum_count)
+/* pg_stat_get_visible_page_marks_cleared */
+PG_STAT_GET_RELENTRY_INT64(visible_page_marks_cleared)
+
+/* pg_stat_get_frozen_page_marks_cleared */
+PG_STAT_GET_RELENTRY_INT64(frozen_page_marks_cleared)
+
#define PG_STAT_GET_RELENTRY_FLOAT8(stat) \
Datum \
CppConcat(pg_stat_get_,stat)(PG_FUNCTION_ARGS) \
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..f8241268017 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12769,4 +12769,14 @@
proname => 'hashoid8extended', prorettype => 'int8',
proargtypes => 'oid8 int8', prosrc => 'hashoid8extended' },
+{ oid => '8002',
+ descr => 'statistics: number of times the all-visible marks in the visibility map were cleared for pages of this table',
+ proname => 'pg_stat_get_visible_page_marks_cleared', provolatile => 's',
+ proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+ prosrc => 'pg_stat_get_visible_page_marks_cleared' },
+{ oid => '8003',
+ descr => 'statistics: number of times the all-frozen marks in the visibility map were cleared for pages of this table',
+ proname => 'pg_stat_get_frozen_page_marks_cleared', provolatile => 's',
+ proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+ prosrc => 'pg_stat_get_frozen_page_marks_cleared' },
]
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index dfa2e837638..7db36cf8add 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -160,6 +160,8 @@ typedef struct PgStat_TableCounts
PgStat_Counter blocks_fetched;
PgStat_Counter blocks_hit;
+ PgStat_Counter visible_page_marks_cleared;
+ PgStat_Counter frozen_page_marks_cleared;
} PgStat_TableCounts;
/* ----------
@@ -218,7 +220,7 @@ typedef struct PgStat_TableXactStatus
* ------------------------------------------------------------
*/
-#define PGSTAT_FILE_FORMAT_ID 0x01A5BCBC
+#define PGSTAT_FILE_FORMAT_ID 0x01A5BCBD
typedef struct PgStat_ArchiverStats
{
@@ -469,6 +471,8 @@ typedef struct PgStat_StatTabEntry
PgStat_Counter blocks_fetched;
PgStat_Counter blocks_hit;
+ PgStat_Counter visible_page_marks_cleared;
+ PgStat_Counter frozen_page_marks_cleared;
TimestampTz last_vacuum_time; /* user initiated vacuum */
PgStat_Counter vacuum_count;
@@ -749,6 +753,17 @@ extern void pgstat_report_analyze(Relation rel,
if (pgstat_should_count_relation(rel)) \
(rel)->pgstat_info->counts.blocks_hit++; \
} while (0)
+/* count revocations of all-visible and all-frozen marks in visibility map */
+#define pgstat_count_visible_page_marks_cleared(rel) \
+ do { \
+ if (pgstat_should_count_relation(rel)) \
+ (rel)->pgstat_info->counts.visible_page_marks_cleared++; \
+ } while (0)
+#define pgstat_count_frozen_page_marks_cleared(rel) \
+ do { \
+ if (pgstat_should_count_relation(rel)) \
+ (rel)->pgstat_info->counts.frozen_page_marks_cleared++; \
+ } 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-freeze.out b/src/test/isolation/expected/vacuum-extending-freeze.out
new file mode 100644
index 00000000000..994a8df56df
--- /dev/null
+++ b/src/test/isolation/expected/vacuum-extending-freeze.out
@@ -0,0 +1,185 @@
+Parsed test spec with 2 sessions
+
+starting permutation: s2_vacuum_freeze s1_get_set_vm_flags_stats s1_update_table s1_get_cleared_vm_flags_stats s2_vacuum_freeze s1_get_set_vm_flags_stats s2_vacuum_freeze s1_select_from_index s2_delete_from_table s1_get_cleared_vm_flags_stats s2_vacuum_freeze s1_get_set_vm_flags_stats s1_commit s1_get_cleared_vm_flags_stats
+pg_stat_force_next_flush
+------------------------
+
+(1 row)
+
+step s2_vacuum_freeze:
+ VACUUM FREEZE vestat;
+
+step s1_get_set_vm_flags_stats:
+ SELECT pg_stat_force_next_flush();
+
+ SELECT c.relallfrozen > frozen_flag_count as relallfrozen, c.relallvisible > all_visibile_flag_count as relallvisible
+ FROM pg_class c, stats_state
+ WHERE c.relname = 'vestat';
+
+ UPDATE stats_state
+ SET frozen_flag_count = c.relallfrozen,
+ all_visibile_flag_count = c.relallvisible
+ FROM pg_class c
+ WHERE c.relname = 'vestat';
+
+pg_stat_force_next_flush
+------------------------
+
+(1 row)
+
+relallfrozen|relallvisible
+------------+-------------
+t |t
+(1 row)
+
+step s1_update_table:
+ UPDATE vestat SET x = x + 1001 where x >= 2500;
+ SELECT pg_stat_force_next_flush();
+
+pg_stat_force_next_flush
+------------------------
+
+(1 row)
+
+step s1_get_cleared_vm_flags_stats:
+ SELECT pg_stat_force_next_flush();
+
+ SELECT v.visible_page_marks_cleared > cleared_all_visibile_flag_count as visible_page_marks_cleared,
+ v.frozen_page_marks_cleared > cleared_frozen_flag_count as frozen_page_marks_cleared
+ FROM pg_stat_all_tables v, stats_state
+ WHERE v.relname = 'vestat';
+
+ UPDATE stats_state
+ SET cleared_all_visibile_flag_count = v.visible_page_marks_cleared,
+ cleared_frozen_flag_count = v.frozen_page_marks_cleared
+ FROM pg_stat_all_tables v
+ WHERE v.relname = 'vestat';
+
+pg_stat_force_next_flush
+------------------------
+
+(1 row)
+
+visible_page_marks_cleared|frozen_page_marks_cleared
+--------------------------+-------------------------
+t |t
+(1 row)
+
+step s2_vacuum_freeze:
+ VACUUM FREEZE vestat;
+
+step s1_get_set_vm_flags_stats:
+ SELECT pg_stat_force_next_flush();
+
+ SELECT c.relallfrozen > frozen_flag_count as relallfrozen, c.relallvisible > all_visibile_flag_count as relallvisible
+ FROM pg_class c, stats_state
+ WHERE c.relname = 'vestat';
+
+ UPDATE stats_state
+ SET frozen_flag_count = c.relallfrozen,
+ all_visibile_flag_count = c.relallvisible
+ FROM pg_class c
+ WHERE c.relname = 'vestat';
+
+pg_stat_force_next_flush
+------------------------
+
+(1 row)
+
+relallfrozen|relallvisible
+------------+-------------
+t |t
+(1 row)
+
+step s2_vacuum_freeze:
+ VACUUM FREEZE vestat;
+
+step s1_select_from_index:
+ BEGIN;
+ SELECT count(x) FROM vestat WHERE x > 2000;
+
+count
+-----
+ 3000
+(1 row)
+
+step s2_delete_from_table:
+ DELETE FROM vestat WHERE x > 4930;
+
+step s1_get_cleared_vm_flags_stats:
+ SELECT pg_stat_force_next_flush();
+
+ SELECT v.visible_page_marks_cleared > cleared_all_visibile_flag_count as visible_page_marks_cleared,
+ v.frozen_page_marks_cleared > cleared_frozen_flag_count as frozen_page_marks_cleared
+ FROM pg_stat_all_tables v, stats_state
+ WHERE v.relname = 'vestat';
+
+ UPDATE stats_state
+ SET cleared_all_visibile_flag_count = v.visible_page_marks_cleared,
+ cleared_frozen_flag_count = v.frozen_page_marks_cleared
+ FROM pg_stat_all_tables v
+ WHERE v.relname = 'vestat';
+
+pg_stat_force_next_flush
+------------------------
+
+(1 row)
+
+visible_page_marks_cleared|frozen_page_marks_cleared
+--------------------------+-------------------------
+f |f
+(1 row)
+
+step s2_vacuum_freeze:
+ VACUUM FREEZE vestat;
+
+step s1_get_set_vm_flags_stats:
+ SELECT pg_stat_force_next_flush();
+
+ SELECT c.relallfrozen > frozen_flag_count as relallfrozen, c.relallvisible > all_visibile_flag_count as relallvisible
+ FROM pg_class c, stats_state
+ WHERE c.relname = 'vestat';
+
+ UPDATE stats_state
+ SET frozen_flag_count = c.relallfrozen,
+ all_visibile_flag_count = c.relallvisible
+ FROM pg_class c
+ WHERE c.relname = 'vestat';
+
+pg_stat_force_next_flush
+------------------------
+
+(1 row)
+
+relallfrozen|relallvisible
+------------+-------------
+f |f
+(1 row)
+
+step s1_commit:
+ COMMIT;
+
+step s1_get_cleared_vm_flags_stats:
+ SELECT pg_stat_force_next_flush();
+
+ SELECT v.visible_page_marks_cleared > cleared_all_visibile_flag_count as visible_page_marks_cleared,
+ v.frozen_page_marks_cleared > cleared_frozen_flag_count as frozen_page_marks_cleared
+ FROM pg_stat_all_tables v, stats_state
+ WHERE v.relname = 'vestat';
+
+ UPDATE stats_state
+ SET cleared_all_visibile_flag_count = v.visible_page_marks_cleared,
+ cleared_frozen_flag_count = v.frozen_page_marks_cleared
+ FROM pg_stat_all_tables v
+ WHERE v.relname = 'vestat';
+
+pg_stat_force_next_flush
+------------------------
+
+(1 row)
+
+visible_page_marks_cleared|frozen_page_marks_cleared
+--------------------------+-------------------------
+t |t
+(1 row)
+
diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule
index 1578ba191c8..91ffc57ebd4 100644
--- a/src/test/isolation/isolation_schedule
+++ b/src/test/isolation/isolation_schedule
@@ -126,3 +126,4 @@ test: serializable-parallel-3
test: matview-write-skew
test: lock-nowait
test: for-portion-of
+test: vacuum-extending-freeze
diff --git a/src/test/isolation/specs/vacuum-extending-freeze.spec b/src/test/isolation/specs/vacuum-extending-freeze.spec
new file mode 100644
index 00000000000..17c204e2326
--- /dev/null
+++ b/src/test/isolation/specs/vacuum-extending-freeze.spec
@@ -0,0 +1,117 @@
+# In short, this test validates the correctness and stability of cumulative
+# vacuum statistics accounting around freezing, visibility, and revision
+# tracking across VACUUM and backend operations.
+# In addition, the test provides a scenario where one process holds a
+# transaction open while another process deletes tuples. We expect that
+# a backend clears the all-frozen and all-visible flags, which were set
+# by VACUUM earlier, only after the committing transaction makes the
+# deletions visible.
+
+setup
+{
+ CREATE TABLE vestat (x int, y int)
+ WITH (autovacuum_enabled = off, fillfactor = 70);
+
+ INSERT INTO vestat
+ SELECT i, i FROM generate_series(1, 5000) AS g(i);
+
+ CREATE INDEX vestat_idx ON vestat (x);
+
+ CREATE TABLE stats_state (frozen_flag_count int, all_visibile_flag_count int,
+ cleared_frozen_flag_count int, cleared_all_visibile_flag_count int);
+ INSERT INTO stats_state VALUES (0,0,0,0);
+ ANALYZE vestat;
+
+ -- Ensure stats are flushed before starting the scenario
+ SELECT pg_stat_force_next_flush();
+}
+
+teardown
+{
+ DROP TABLE IF EXISTS vestat;
+ RESET vacuum_freeze_min_age;
+ RESET vacuum_freeze_table_age;
+
+}
+
+session s1
+
+step s1_get_set_vm_flags_stats
+{
+ SELECT pg_stat_force_next_flush();
+
+ SELECT c.relallfrozen > frozen_flag_count as relallfrozen, c.relallvisible > all_visibile_flag_count as relallvisible
+ FROM pg_class c, stats_state
+ WHERE c.relname = 'vestat';
+
+ UPDATE stats_state
+ SET frozen_flag_count = c.relallfrozen,
+ all_visibile_flag_count = c.relallvisible
+ FROM pg_class c
+ WHERE c.relname = 'vestat';
+}
+
+step s1_get_cleared_vm_flags_stats
+{
+ SELECT pg_stat_force_next_flush();
+
+ SELECT v.visible_page_marks_cleared > cleared_all_visibile_flag_count as visible_page_marks_cleared,
+ v.frozen_page_marks_cleared > cleared_frozen_flag_count as frozen_page_marks_cleared
+ FROM pg_stat_all_tables v, stats_state
+ WHERE v.relname = 'vestat';
+
+ UPDATE stats_state
+ SET cleared_all_visibile_flag_count = v.visible_page_marks_cleared,
+ cleared_frozen_flag_count = v.frozen_page_marks_cleared
+ FROM pg_stat_all_tables v
+ WHERE v.relname = 'vestat';
+}
+
+step s1_select_from_index
+{
+ BEGIN;
+ SELECT count(x) FROM vestat WHERE x > 2000;
+}
+
+step s1_commit
+{
+ COMMIT;
+}
+
+session s2
+setup
+{
+ -- Configure aggressive freezing vacuum behavior
+ SET vacuum_freeze_min_age = 0;
+ SET vacuum_freeze_table_age = 0;
+}
+step s2_delete_from_table
+{
+ DELETE FROM vestat WHERE x > 4930;
+}
+step s2_vacuum_freeze
+{
+ VACUUM FREEZE vestat;
+}
+
+step s1_update_table
+{
+ UPDATE vestat SET x = x + 1001 where x >= 2500;
+ SELECT pg_stat_force_next_flush();
+}
+
+permutation
+ s2_vacuum_freeze
+ s1_get_set_vm_flags_stats
+ s1_update_table
+ s1_get_cleared_vm_flags_stats
+ s2_vacuum_freeze
+ s1_get_set_vm_flags_stats
+ s2_vacuum_freeze
+ s1_select_from_index
+ s2_delete_from_table
+ s1_get_cleared_vm_flags_stats
+ s2_vacuum_freeze
+ s1_get_set_vm_flags_stats
+ s1_commit
+ s1_get_cleared_vm_flags_stats
\ No newline at end of file
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index a65a5bf0c4f..096e4f763f3 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1846,7 +1846,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_visible_page_marks_cleared(c.oid) AS visible_page_marks_cleared,
+ pg_stat_get_frozen_page_marks_cleared(c.oid) AS frozen_page_marks_cleared
FROM ((pg_class c
LEFT JOIN pg_index i ON ((c.oid = i.indrelid)))
LEFT JOIN pg_namespace n ON ((n.oid = c.relnamespace)))
@@ -2357,7 +2359,9 @@ pg_stat_sys_tables| SELECT relid,
total_autovacuum_time,
total_analyze_time,
total_autoanalyze_time,
- stats_reset
+ stats_reset,
+ visible_page_marks_cleared,
+ frozen_page_marks_cleared
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,
@@ -2412,7 +2416,9 @@ pg_stat_user_tables| SELECT relid,
total_autovacuum_time,
total_analyze_time,
total_autoanalyze_time,
- stats_reset
+ stats_reset,
+ visible_page_marks_cleared,
+ frozen_page_marks_cleared
FROM pg_stat_all_tables
WHERE ((schemaname <> ALL (ARRAY['pg_catalog'::name, 'information_schema'::name])) AND (schemaname !~ '^pg_toast'::text));
pg_stat_wal| SELECT wal_records,
--
2.39.5 (Apple Git-154)
[text/plain] v38-0002-Machinery-for-grabbing-extended-vacuum-statistics.patch (25.0K, 4-v38-0002-Machinery-for-grabbing-extended-vacuum-statistics.patch)
download | inline diff:
From 3a5e0bd82578d1fea63d6bda229dc4d0b224684e Mon Sep 17 00:00:00 2001
From: Alena Rybakina <[email protected]>
Date: Mon, 2 Mar 2026 23:09:32 +0300
Subject: [PATCH 2/3] Machinery for grabbing extended vacuum statistics.
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Add infrastructure inside lazy vacuum to gather extended per-vacuum
metrics and expose them to extensions via a new hook. Core itself
does not persist these metrics — that is the job of an extension
(see ext_vacuum_statistics).
Statistics are gathered separately for tables and indexes according
to vacuum phases. The ExtVacReport union and type field distinguish
PGSTAT_EXTVAC_TABLE vs PGSTAT_EXTVAC_INDEX. Heap vacuum stats are
sent to the cumulative statistics system after vacuum has processed
the indexes. Database vacuum statistics aggregate per-table and
per-index statistics within the database.
Common for tables, indexes, and database: total_blks_hit, total_blks_read
and total_blks_dirtied are the number of hit, miss and dirtied pages
in shared buffers during a vacuum operation. total_blks_dirtied counts
only pages dirtied by this vacuum. blk_read_time and blk_write_time
track access and flush time for buffer pages; blk_write_time can stay
zero if no flushes occurred. total_time is wall-clock time from start
to finish, including idle time (I/O and lock waits). delay_time is
total vacuum sleep time in vacuum delay points.
Both table and index report tuples_deleted (tuples removed by the vacuum),
pages_removed (pages by which relation storage was reduced) and
pages_deleted (freed pages; file size may remain unchanged). These are
independent of WAL and buffer stats and are not summed at the database
level.
Table only: pages_frozen (pages marked all-frozen in the visibility map),
pages_all_visible (pages marked all-visible in the visibility map),
wraparound_failsafe_count (number of urgent anti-wraparound vacuums).
Table and database share wraparound_failsafe (count of urgent anti-wraparound
cleanups). Database only: errors (number of error-level errors caught
during vacuum).
set_report_vacuum_hook (set_report_vacuum_hook_type) -- called
once per vacuumed relation/index with a PgStat_VacuumRelationCounts
payload tagged by ExtVacReportType (PGSTAT_EXTVAC_TABLE / _INDEX /
_DB / _INVALID).
Authors: Alena Rybakina <[email protected]>,
Andrei Lepikhov <[email protected]>,
Andrei Zubkov <[email protected]>
Reviewed-by: Dilip Kumar <[email protected]>,
Masahiko Sawada <[email protected]>,
Ilia Evdokimov <[email protected]>,
jian he <[email protected]>,
Kirill Reshke <[email protected]>,
Alexander Korotkov <[email protected]>,
Jim Nasby <[email protected]>,
Sami Imseih <[email protected]>,
Karina Litskevich <[email protected]>
---
src/backend/access/heap/vacuumlazy.c | 234 ++++++++++++++++++-
src/backend/commands/vacuum.c | 4 +
src/backend/commands/vacuumparallel.c | 12 +
src/backend/utils/activity/pgstat_relation.c | 24 ++
src/include/commands/vacuum.h | 29 +++
src/include/pgstat.h | 69 ++++++
6 files changed, 367 insertions(+), 5 deletions(-)
diff --git a/src/backend/access/heap/vacuumlazy.c b/src/backend/access/heap/vacuumlazy.c
index 39395aed0d5..e4d4c93d641 100644
--- a/src/backend/access/heap/vacuumlazy.c
+++ b/src/backend/access/heap/vacuumlazy.c
@@ -283,6 +283,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 */
@@ -410,6 +412,15 @@ typedef struct LVRelState
* been permanently disabled.
*/
BlockNumber eager_scan_remaining_fails;
+
+ int32 wraparound_failsafe_count; /* # of emergency vacuums for
+ * anti-wraparound */
+
+ /*
+ * We need to accumulate index statistics for later subtraction from heap
+ * stats.
+ */
+ PgStat_VacuumRelationCounts extVacReportIdx;
} LVRelState;
@@ -485,6 +496,166 @@ static void restore_vacuum_error_info(LVRelState *vacrel,
const LVSavedErrInfo *saved_vacrel);
+/* Extended vacuum statistics functions */
+
+/*
+ * extvac_stats_start - Save cut-off values before start of relation processing.
+ */
+static void
+extvac_stats_start(Relation rel, LVExtStatCounters * counters)
+{
+ memset(counters, 0, sizeof(LVExtStatCounters));
+ counters->starttime = GetCurrentTimestamp();
+ counters->walusage = pgWalUsage;
+ counters->bufusage = pgBufferUsage;
+ counters->VacuumDelayTime = VacuumDelayTime;
+ counters->blocks_fetched = 0;
+ counters->blocks_hit = 0;
+
+ if (rel->pgstat_info && pgstat_track_counts)
+ {
+ counters->blocks_fetched = rel->pgstat_info->counts.blocks_fetched;
+ counters->blocks_hit = rel->pgstat_info->counts.blocks_hit;
+ }
+}
+
+/*
+ * extvac_stats_end - Finish extended vacuum statistic gathering and form report.
+ */
+static void
+extvac_stats_end(Relation rel, LVExtStatCounters * counters,
+ PgStat_CommonCounts * report)
+{
+ WalUsage walusage;
+ BufferUsage bufusage;
+ TimestampTz endtime;
+ long secs;
+ int usecs;
+
+ memset(report, 0, sizeof(PgStat_CommonCounts));
+ 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);
+
+ 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) +
+ INSTR_TIME_GET_MILLISEC(bufusage.shared_blk_read_time);
+ report->blk_write_time = INSTR_TIME_GET_MILLISEC(bufusage.local_blk_write_time) +
+ INSTR_TIME_GET_MILLISEC(bufusage.shared_blk_write_time);
+ report->delay_time = VacuumDelayTime - counters->VacuumDelayTime;
+ report->total_time = secs * 1000.0 + usecs / 1000.0;
+
+ if (rel->pgstat_info && pgstat_track_counts)
+ {
+ report->blks_fetched = rel->pgstat_info->counts.blocks_fetched - counters->blocks_fetched;
+ report->blks_hit = rel->pgstat_info->counts.blocks_hit - counters->blocks_hit;
+ }
+}
+
+/*
+ * extvac_stats_start_idx - Start extended vacuum statistic gathering for index.
+ */
+void
+extvac_stats_start_idx(Relation rel, IndexBulkDeleteResult *stats,
+ LVExtStatCountersIdx * counters)
+{
+ extvac_stats_start(rel, &counters->common);
+ counters->pages_deleted = 0;
+ counters->tuples_removed = 0;
+
+ if (stats != NULL)
+ {
+ counters->tuples_removed = stats->tuples_removed;
+ counters->pages_deleted = stats->pages_deleted;
+ }
+}
+
+
+/*
+ * extvac_stats_end_idx - Finish extended vacuum statistic gathering for index.
+ */
+void
+extvac_stats_end_idx(Relation rel, IndexBulkDeleteResult *stats,
+ LVExtStatCountersIdx * counters, PgStat_VacuumRelationCounts * report)
+{
+ memset(report, 0, sizeof(PgStat_VacuumRelationCounts));
+ extvac_stats_end(rel, &counters->common, &report->common);
+ report->type = PGSTAT_EXTVAC_INDEX;
+
+ if (stats != NULL)
+ {
+ report->common.tuples_deleted = stats->tuples_removed - counters->tuples_removed;
+ report->index.pages_deleted = stats->pages_deleted - counters->pages_deleted;
+ }
+}
+
+/*
+ * Accumulate index stats into vacrel for later subtraction from heap stats.
+ * It needs to prevent double-counting of stats for heaps that
+ * include indexes because indexes are vacuumed before the heap.
+ * We need to be careful with buffer usage and wal usage during parallel vacuum
+ * because they are accumulated summarly for all indexes at once by leader after
+ * all workers have finished.
+ */
+static void
+accumulate_idxs_vacuum_statistics(LVRelState *vacrel,
+ PgStat_VacuumRelationCounts * extVacIdxStats)
+{
+ 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;
+}
+
+/* Build heap-specific extended stats */
+static void
+accumulate_heap_vacuum_statistics(LVRelState *vacrel, PgStat_VacuumRelationCounts * extVacStats)
+{
+ 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->new_all_frozen_pages;
+ extVacStats->table.vm_new_visible_pages = vacrel->new_all_visible_pages;
+ extVacStats->table.vm_new_visible_frozen_pages = vacrel->new_all_visible_all_frozen_pages;
+ 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.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->common.wraparound_failsafe_count = vacrel->wraparound_failsafe_count;
+
+ /* Hook is invoked from pgstat_report_vacuum() when extstats is passed */
+
+ /* Subtract index stats from heap to avoid double-counting */
+ 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->common.total_time -= vacrel->extVacReportIdx.common.total_time;
+ extVacStats->common.delay_time -= vacrel->extVacReportIdx.common.delay_time;
+}
/*
* Helper to set up the eager scanning state for vacuuming a single relation.
@@ -643,7 +814,10 @@ heap_vacuum_rel(Relation rel, const VacuumParams *params,
ErrorContextCallback errcallback;
char **indnames = NULL;
Size dead_items_max_bytes = 0;
+ LVExtStatCounters extVacCounters;
+ PgStat_VacuumRelationCounts extVacReport;
+ memset(&extVacReport, 0, sizeof(extVacReport));
verbose = (params->options & VACOPT_VERBOSE) != 0;
instrument = (verbose || (AmAutoVacuumWorkerProcess() &&
params->log_vacuum_min_duration >= 0));
@@ -660,6 +834,9 @@ heap_vacuum_rel(Relation rel, const VacuumParams *params,
/* Used for instrumentation and stats report */
starttime = GetCurrentTimestamp();
+ if (set_report_vacuum_hook)
+ extvac_stats_start(rel, &extVacCounters);
+
pgstat_progress_start_command(PROGRESS_COMMAND_VACUUM,
RelationGetRelid(rel));
if (AmAutoVacuumWorkerProcess())
@@ -687,7 +864,9 @@ 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;
+ memset(&vacrel->extVacReportIdx, 0, sizeof(vacrel->extVacReportIdx));
vacrel->phase = VACUUM_ERRCB_PHASE_UNKNOWN;
vacrel->verbose = verbose;
errcallback.callback = vacuum_error_callback;
@@ -803,6 +982,9 @@ heap_vacuum_rel(Relation rel, const VacuumParams *params,
vacrel->rel_pages = orig_rel_pages = RelationGetNumberOfBlocks(rel);
vacrel->vistest = GlobalVisTestFor(rel);
+ /* Initialize wraparound failsafe count for extended vacuum stats */
+ vacrel->wraparound_failsafe_count = 0;
+
/* Initialize state used to track oldest extant XID/MXID */
vacrel->NewRelfrozenXid = vacrel->cutoffs.OldestXmin;
vacrel->NewRelminMxid = vacrel->cutoffs.OldestMxact;
@@ -985,11 +1167,26 @@ heap_vacuum_rel(Relation rel, const VacuumParams *params,
* soon in cases where the failsafe prevented significant amounts of heap
* vacuuming.
*/
- pgstat_report_vacuum(rel,
- Max(vacrel->new_live_tuples, 0),
- vacrel->recently_dead_tuples +
- vacrel->missed_dead_tuples,
- starttime);
+ if (set_report_vacuum_hook)
+ {
+ extvac_stats_end(rel, &extVacCounters, &extVacReport.common);
+ accumulate_heap_vacuum_statistics(vacrel, &extVacReport);
+
+ pgstat_report_vacuum_ext(rel,
+ Max(vacrel->new_live_tuples, 0),
+ vacrel->recently_dead_tuples +
+ vacrel->missed_dead_tuples,
+ starttime,
+ &extVacReport);
+ }
+ else
+ pgstat_report_vacuum_ext(rel,
+ Max(vacrel->new_live_tuples, 0),
+ vacrel->recently_dead_tuples +
+ vacrel->missed_dead_tuples,
+ starttime,
+ NULL);
+
pgstat_progress_end_command();
if (instrument)
@@ -2903,6 +3100,7 @@ lazy_check_wraparound_failsafe(LVRelState *vacrel)
int64 progress_val[3] = {0, 0, PROGRESS_VACUUM_MODE_FAILSAFE};
VacuumFailsafeActive = true;
+ vacrel->wraparound_failsafe_count++;
/*
* Abandon use of a buffer access strategy to allow use of all of
@@ -3015,7 +3213,11 @@ lazy_vacuum_one_index(Relation indrel, IndexBulkDeleteResult *istat,
{
IndexVacuumInfo ivinfo;
LVSavedErrInfo saved_err_info;
+ LVExtStatCountersIdx extVacCounters;
+ PgStat_VacuumRelationCounts extVacReport;
+ if (set_report_vacuum_hook)
+ extvac_stats_start_idx(indrel, istat, &extVacCounters);
ivinfo.index = indrel;
ivinfo.heaprel = vacrel->rel;
ivinfo.analyze_only = false;
@@ -3033,6 +3235,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);
@@ -3041,6 +3244,14 @@ lazy_vacuum_one_index(Relation indrel, IndexBulkDeleteResult *istat,
istat = vac_bulkdel_one_index(&ivinfo, istat, vacrel->dead_items,
vacrel->dead_items_info);
+ if (set_report_vacuum_hook)
+ {
+ memset(&extVacReport, 0, sizeof(extVacReport));
+ extvac_stats_end_idx(indrel, istat, &extVacCounters, &extVacReport);
+ pgstat_report_vacuum_ext(indrel, -1, -1, 0, &extVacReport);
+ accumulate_idxs_vacuum_statistics(vacrel, &extVacReport);
+ }
+
/* Revert to the previous phase information for error traceback */
restore_vacuum_error_info(vacrel, &saved_err_info);
pfree(vacrel->indname);
@@ -3065,7 +3276,11 @@ lazy_cleanup_one_index(Relation indrel, IndexBulkDeleteResult *istat,
{
IndexVacuumInfo ivinfo;
LVSavedErrInfo saved_err_info;
+ LVExtStatCountersIdx extVacCounters;
+ PgStat_VacuumRelationCounts extVacReport;
+ if (set_report_vacuum_hook)
+ extvac_stats_start_idx(indrel, istat, &extVacCounters);
ivinfo.index = indrel;
ivinfo.heaprel = vacrel->rel;
ivinfo.analyze_only = false;
@@ -3084,12 +3299,21 @@ 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 (set_report_vacuum_hook)
+ {
+ memset(&extVacReport, 0, sizeof(extVacReport));
+ extvac_stats_end_idx(indrel, istat, &extVacCounters, &extVacReport);
+ pgstat_report_vacuum_ext(indrel, -1, -1, 0, &extVacReport);
+ accumulate_idxs_vacuum_statistics(vacrel, &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/commands/vacuum.c b/src/backend/commands/vacuum.c
index 99d0db82ed7..a7fb73173f5 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -118,6 +118,9 @@ pg_atomic_uint32 *VacuumSharedCostBalance = NULL;
pg_atomic_uint32 *VacuumActiveNWorkers = NULL;
int VacuumCostBalanceLocal = 0;
+/* Cumulative storage to report total vacuum delay time (msec). */
+double VacuumDelayTime = 0;
+
/* non-export function prototypes */
static List *expand_vacuum_rel(VacuumRelation *vrel,
MemoryContext vac_context, int options);
@@ -2561,6 +2564,7 @@ vacuum_delay_point(bool is_analyze)
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 41cefcfde54..200f12a2d1b 100644
--- a/src/backend/commands/vacuumparallel.c
+++ b/src/backend/commands/vacuumparallel.c
@@ -1076,6 +1076,8 @@ parallel_vacuum_process_one_index(ParallelVacuumState *pvs, Relation indrel,
IndexBulkDeleteResult *istat = NULL;
IndexBulkDeleteResult *istat_res;
IndexVacuumInfo ivinfo;
+ LVExtStatCountersIdx extVacCounters;
+ PgStat_VacuumRelationCounts extVacReport;
/*
* Update the pointer to the corresponding bulk-deletion result if someone
@@ -1084,6 +1086,8 @@ parallel_vacuum_process_one_index(ParallelVacuumState *pvs, Relation indrel,
if (indstats->istat_updated)
istat = &(indstats->istat);
+ if (set_report_vacuum_hook)
+ extvac_stats_start_idx(indrel, istat, &extVacCounters);
ivinfo.index = indrel;
ivinfo.heaprel = pvs->heaprel;
ivinfo.analyze_only = false;
@@ -1112,6 +1116,13 @@ parallel_vacuum_process_one_index(ParallelVacuumState *pvs, Relation indrel,
RelationGetRelationName(indrel));
}
+ if (set_report_vacuum_hook)
+ {
+ memset(&extVacReport, 0, sizeof(extVacReport));
+ extvac_stats_end_idx(indrel, istat_res, &extVacCounters, &extVacReport);
+ pgstat_report_vacuum_ext(indrel, -1, -1, 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
@@ -1276,6 +1287,7 @@ parallel_vacuum_main(dsm_segment *seg, shm_toc *toc)
VacuumUpdateCosts();
VacuumCostBalance = 0;
+ VacuumDelayTime = 0;
VacuumCostBalanceLocal = 0;
VacuumSharedCostBalance = &(shared->cost_balance);
VacuumActiveNWorkers = &(shared->active_nworkers);
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index 92e1f60a080..226d7aa06d5 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -272,6 +272,30 @@ pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
}
+/*
+ * Hook for extensions to receive extended vacuum statistics.
+ * NULL when no extension has registered.
+ */
+set_report_vacuum_hook_type set_report_vacuum_hook = NULL;
+
+/*
+ * Report extended vacuum statistics to extensions via set_report_vacuum_hook.
+ * When livetuples/deadtuples/starttime are provided (heap case), also calls
+ * pgstat_report_vacuum. For indexes, pass -1, -1, 0 to skip pgstat_report_vacuum.
+ */
+void
+pgstat_report_vacuum_ext(Relation rel, PgStat_Counter livetuples,
+ PgStat_Counter deadtuples, TimestampTz starttime,
+ PgStat_VacuumRelationCounts * extstats)
+{
+ pgstat_report_vacuum(rel, livetuples, deadtuples, starttime);
+
+ if (extstats != NULL && set_report_vacuum_hook)
+ (*set_report_vacuum_hook) (RelationGetRelid(rel),
+ rel->rd_rel->relisshared,
+ extstats);
+}
+
/*
* Report that the table was just analyzed and flush IO statistics.
*
diff --git a/src/include/commands/vacuum.h b/src/include/commands/vacuum.h
index 956d9cea36d..a925f7da992 100644
--- a/src/include/commands/vacuum.h
+++ b/src/include/commands/vacuum.h
@@ -21,9 +21,11 @@
#include "catalog/pg_class.h"
#include "catalog/pg_statistic.h"
#include "catalog/pg_type.h"
+#include "executor/instrument.h"
#include "parser/parse_node.h"
#include "storage/buf.h"
#include "utils/relcache.h"
+#include "pgstat.h"
/*
* Flags for amparallelvacuumoptions to control the participation of bulkdelete
@@ -354,6 +356,33 @@ extern PGDLLIMPORT pg_atomic_uint32 *VacuumSharedCostBalance;
extern PGDLLIMPORT pg_atomic_uint32 *VacuumActiveNWorkers;
extern PGDLLIMPORT int VacuumCostBalanceLocal;
+/* Cumulative storage to report total vacuum delay time (msec). */
+extern PGDLLIMPORT double VacuumDelayTime;
+
+/* Counters for extended vacuum statistics gathering */
+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;
+
+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);
+
extern PGDLLIMPORT bool VacuumFailsafeActive;
extern PGDLLIMPORT double vacuum_cost_delay;
extern PGDLLIMPORT int vacuum_cost_limit;
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 7db36cf8add..8d934973dc1 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -93,6 +93,64 @@ typedef struct PgStat_FunctionCounts
/*
* Working state needed to accumulate per-function-call timing statistics.
*/
+/*
+ * Extended vacuum statistics - passed to extensions via set_report_vacuum_hook.
+ * Type of entry: table (heap), index, or database aggregate.
+ */
+typedef enum ExtVacReportType
+{
+ PGSTAT_EXTVAC_INVALID = 0,
+ PGSTAT_EXTVAC_TABLE = 1,
+ PGSTAT_EXTVAC_INDEX = 2,
+ PGSTAT_EXTVAC_DB = 3,
+} ExtVacReportType;
+
+typedef struct PgStat_CommonCounts
+{
+ int64 total_blks_read;
+ int64 total_blks_hit;
+ int64 total_blks_dirtied;
+ int64 total_blks_written;
+ int64 blks_fetched;
+ int64 blks_hit;
+ int64 wal_records;
+ int64 wal_fpi;
+ uint64 wal_bytes;
+ double blk_read_time;
+ double blk_write_time;
+ double delay_time;
+ double total_time;
+ int32 wraparound_failsafe_count;
+ int32 interrupts_count;
+ int64 tuples_deleted;
+} PgStat_CommonCounts;
+
+typedef struct PgStat_VacuumRelationCounts
+{
+ PgStat_CommonCounts common;
+ ExtVacReportType type;
+ union
+ {
+ struct
+ {
+ int64 tuples_frozen;
+ int64 recently_dead_tuples;
+ int64 missed_dead_tuples;
+ int64 pages_scanned;
+ int64 pages_removed;
+ int64 vm_new_frozen_pages;
+ int64 vm_new_visible_pages;
+ int64 vm_new_visible_frozen_pages;
+ int64 missed_dead_pages;
+ int64 index_vacuum_count;
+ } table;
+ struct
+ {
+ int64 pages_deleted;
+ } index;
+ };
+} PgStat_VacuumRelationCounts;
+
typedef struct PgStat_FunctionCallUsage
{
/* Link to function's hashtable entry (must still be there at exit!) */
@@ -703,6 +761,17 @@ extern void pgstat_unlink_relation(Relation rel);
extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
PgStat_Counter deadtuples,
TimestampTz starttime);
+
+extern void pgstat_report_vacuum_ext(Relation rel,
+ PgStat_Counter livetuples,
+ PgStat_Counter deadtuples,
+ TimestampTz starttime,
+ PgStat_VacuumRelationCounts * extstats);
+
+/* Hook for extensions to receive extended vacuum statistics */
+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;
extern void pgstat_report_analyze(Relation rel,
PgStat_Counter livetuples, PgStat_Counter deadtuples,
bool resetcounter, TimestampTz starttime);
--
2.39.5 (Apple Git-154)
[text/plain] v38-0003-ext_vacuum_statistics-extension-for-extended-vacuum-.patch (145.2K, 5-v38-0003-ext_vacuum_statistics-extension-for-extended-vacuum-.patch)
download | inline diff:
From cf8285d7557582d6995d58ca62599e7e47b20b1b Mon Sep 17 00:00:00 2001
From: Alena Rybakina <[email protected]>
Date: Tue, 28 Apr 2026 03:43:29 +0300
Subject: [PATCH 3/3] ext_vacuum_statistics: extension for extended vacuum
statistics
Introduce a new extension that collects extended per-vacuum
metrics via set_report_vacuum_hook and stores them through pgstat's
custom statistics infrastructure.
Tracking scope is controlled by GUCs:
* vacuum_statistics.enabled -- master switch
* vacuum_statistics.object_types -- databases / relations / all
* vacuum_statistics.track_relations -- system / user / all
* vacuum_statistics.track_{databases,relations}_from_list
-- restrict tracking to objects registered via
add_track_database() / add_track_relation();
removal via remove_track_*() and OAT_DROP hook
* vacuum_statistics.collect -- buffers / wal /
general / timing / all, consulted by ACCUM_IF() to skip
unwanted categories at run time
add_track_* / remove_track_* require superuser or pg_read_all_stats.
---
contrib/Makefile | 1 +
contrib/ext_vacuum_statistics/Makefile | 24 +
contrib/ext_vacuum_statistics/README.md | 165 ++
.../expected/ext_vacuum_statistics.out | 52 +
.../vacuum-extending-in-repetable-read.out | 52 +
.../ext_vacuum_statistics--1.0.sql | 272 ++++
.../ext_vacuum_statistics.conf | 2 +
.../ext_vacuum_statistics.control | 5 +
contrib/ext_vacuum_statistics/meson.build | 41 +
.../vacuum-extending-in-repetable-read.spec | 59 +
.../t/052_vacuum_extending_basic_test.pl | 780 +++++++++
.../t/053_vacuum_extending_freeze_test.pl | 285 ++++
.../t/054_vacuum_extending_gucs_test.pl | 279 ++++
.../ext_vacuum_statistics/vacuum_statistics.c | 1387 +++++++++++++++++
contrib/meson.build | 1 +
doc/src/sgml/contrib.sgml | 1 +
doc/src/sgml/extvacuumstatistics.sgml | 502 ++++++
doc/src/sgml/filelist.sgml | 1 +
18 files changed, 3909 insertions(+)
create mode 100644 contrib/ext_vacuum_statistics/Makefile
create mode 100644 contrib/ext_vacuum_statistics/README.md
create mode 100644 contrib/ext_vacuum_statistics/expected/ext_vacuum_statistics.out
create mode 100644 contrib/ext_vacuum_statistics/expected/vacuum-extending-in-repetable-read.out
create mode 100644 contrib/ext_vacuum_statistics/ext_vacuum_statistics--1.0.sql
create mode 100644 contrib/ext_vacuum_statistics/ext_vacuum_statistics.conf
create mode 100644 contrib/ext_vacuum_statistics/ext_vacuum_statistics.control
create mode 100644 contrib/ext_vacuum_statistics/meson.build
create mode 100644 contrib/ext_vacuum_statistics/specs/vacuum-extending-in-repetable-read.spec
create mode 100644 contrib/ext_vacuum_statistics/t/052_vacuum_extending_basic_test.pl
create mode 100644 contrib/ext_vacuum_statistics/t/053_vacuum_extending_freeze_test.pl
create mode 100644 contrib/ext_vacuum_statistics/t/054_vacuum_extending_gucs_test.pl
create mode 100644 contrib/ext_vacuum_statistics/vacuum_statistics.c
create mode 100644 doc/src/sgml/extvacuumstatistics.sgml
diff --git a/contrib/Makefile b/contrib/Makefile
index 7d91fe77db3..3140f2bf844 100644
--- a/contrib/Makefile
+++ b/contrib/Makefile
@@ -19,6 +19,7 @@ SUBDIRS = \
dict_int \
dict_xsyn \
earthdistance \
+ ext_vacuum_statistics \
file_fdw \
fuzzystrmatch \
hstore \
diff --git a/contrib/ext_vacuum_statistics/Makefile b/contrib/ext_vacuum_statistics/Makefile
new file mode 100644
index 00000000000..ed80bdf28d0
--- /dev/null
+++ b/contrib/ext_vacuum_statistics/Makefile
@@ -0,0 +1,24 @@
+# contrib/ext_vacuum_statistics/Makefile
+
+EXTENSION = ext_vacuum_statistics
+MODULE_big = ext_vacuum_statistics
+OBJS = vacuum_statistics.o
+DATA = ext_vacuum_statistics--1.0.sql
+PGFILEDESC = "ext_vacuum_statistics - convenience views for extended vacuum statistics"
+
+ISOLATION = vacuum-extending-in-repetable-read
+ISOLATION_OPTS = --temp-config=$(top_srcdir)/contrib/ext_vacuum_statistics/ext_vacuum_statistics.conf
+TAP_TESTS = 1
+
+NO_INSTALLCHECK = 1
+
+ifdef USE_PGXS
+PG_CONFIG = pg_config
+PGXS := $(shell $(PG_CONFIG) --pgxs)
+include $(PGXS)
+else
+subdir = contrib/ext_vacuum_statistics
+top_builddir = ../..
+include $(top_builddir)/src/Makefile.global
+include $(top_srcdir)/contrib/contrib-global.mk
+endif
diff --git a/contrib/ext_vacuum_statistics/README.md b/contrib/ext_vacuum_statistics/README.md
new file mode 100644
index 00000000000..51697eab023
--- /dev/null
+++ b/contrib/ext_vacuum_statistics/README.md
@@ -0,0 +1,165 @@
+# ext_vacuum_statistics
+
+Extended vacuum statistics extension for PostgreSQL. It collects and exposes detailed per-table, per-index, and per-database vacuum statistics (buffer I/O, WAL, general, timing) via convenient views in the `ext_vacuum_statistics` schema.
+
+## Installation
+
+```
+./configure tmp_install="$(pwd)/my/inst"
+make clean && make && make install
+cd contrib/ext_vacuum_statistics
+make && make install
+```
+
+It is essential that the extension is listed in `shared_preload_libraries` because it registers a vacuum hook at server startup.
+
+In your `postgresql.conf`:
+
+```
+shared_preload_libraries = 'ext_vacuum_statistics'
+```
+
+Restart PostgreSQL.
+
+In your database:
+
+```sql
+CREATE EXTENSION ext_vacuum_statistics;
+```
+
+## Usage
+
+Query vacuum statistics via the provided views:
+
+```sql
+-- Per-table heap vacuum statistics
+SELECT * FROM ext_vacuum_statistics.pg_stats_vacuum_tables;
+
+-- Per-index vacuum statistics
+SELECT * FROM ext_vacuum_statistics.pg_stats_vacuum_indexes;
+
+-- Per-database aggregate vacuum statistics
+SELECT * FROM ext_vacuum_statistics.pg_stats_vacuum_database;
+```
+
+Example output:
+
+```
+ relname | total_blks_read | total_blks_hit | wal_records | tuples_deleted | pages_removed
+-----------+-----------------+----------------+-------------+----------------+---------------
+ mytable | 120 | 340 | 15 | 500 | 10
+```
+
+Reset statistics when needed:
+
+```sql
+SELECT ext_vacuum_statistics.vacuum_statistics_reset();
+```
+
+## Configuration (GUCs)
+
+| GUC | Default | Description |
+|-----|---------|-------------|
+| `vacuum_statistics.enabled` | on | Enable extended vacuum statistics collection |
+| `vacuum_statistics.object_types` | all | Object types for statistics: `all`, `databases`, `relations` |
+| `vacuum_statistics.track_relations` | all | When tracking relations: `all`, `system`, `user` |
+| `vacuum_statistics.track_databases_from_list` | off | If on, track only databases added via add_track_database |
+| `vacuum_statistics.track_relations_from_list` | off | If on, track only relations added via add_track_relation |
+
+## Memory usage
+
+Each tracked object (table, index, or database) uses approximately **232 bytes** of shared memory on Linux x86_64 (e.g. Ubuntu): common stats (buffers, WAL, timing) ~144 bytes; type + union ~88 bytes (union holds table-specific or index-specific fields, allocated size is the same for both).
+
+The exact size depends on the platform. Call `ext_vacuum_statistics.shared_memory_size()` to get the total shared memory used by the extension. The GUCs provided by the extension allow controlling the amount of memory used: `vacuum_statistics.object_types` to track only databases or relations, `vacuum_statistics.track_relations` to restrict to user or system tables/indexes, and `track_*_from_list` to track only selected databases and relations.
+
+Example: a database with 1000 tables and 2000 indexes, all tracked, uses about **700 KB** on Ubuntu (3001 entries × 232 bytes). Per-database entries add one entry per tracked database.
+
+## Advanced tuning
+
+### Track only database-level stats
+
+```sql
+SET vacuum_statistics.object_types = 'databases';
+```
+
+Statistics are accumulated per database; per-relation views remain empty.
+
+### Track only user or system tables
+
+```sql
+SET vacuum_statistics.object_types = 'relations';
+SET vacuum_statistics.track_relations = 'user'; -- skip system catalogs
+-- or
+SET vacuum_statistics.track_relations = 'system'; -- only system catalogs
+```
+
+### Filter by database or relation OIDs
+
+Add OIDs via functions (persisted to `pg_stat/ext_vacuum_statistics_track.oid`) and enable filtering:
+
+```sql
+-- Add databases and relations to track
+SELECT ext_vacuum_statistics.add_track_database(16384);
+SELECT ext_vacuum_statistics.add_track_relation(16384, 16385); -- dboid, reloid
+SELECT ext_vacuum_statistics.add_track_relation(0, 16386); -- rel 16386 in any db
+
+-- Enable list-based filtering (off = track all)
+SET vacuum_statistics.track_databases_from_list = on;
+SET vacuum_statistics.track_relations_from_list = on;
+```
+
+Remove OIDs when no longer needed:
+
+```sql
+SELECT ext_vacuum_statistics.remove_track_database(16384);
+SELECT ext_vacuum_statistics.remove_track_relation(16384, 16385);
+```
+
+Inspect the current tracking configuration:
+
+```sql
+SELECT * FROM ext_vacuum_statistics.track_list();
+```
+
+Returns `track_kind`, `dboid`, `reloid`. When `dboid` or `reloid` is NULL, statistics are collected for all.
+
+## Recipes
+
+**Reduce overhead by tracking only databases:**
+
+```sql
+SET vacuum_statistics.object_types = 'databases';
+```
+
+**Track only a specific table in a specific database:**
+
+```sql
+SELECT ext_vacuum_statistics.add_track_database(
+ (SELECT oid FROM pg_database WHERE datname = current_database())
+);
+SELECT ext_vacuum_statistics.add_track_relation(
+ (SELECT oid FROM pg_database WHERE datname = current_database()),
+ 'mytable'::regclass
+);
+SET vacuum_statistics.track_databases_from_list = on;
+SET vacuum_statistics.track_relations_from_list = on;
+```
+
+**Disable statistics collection temporarily:**
+
+```sql
+SET vacuum_statistics.enabled = off;
+```
+
+## Views
+
+| View | Description |
+|------|-------------|
+| `ext_vacuum_statistics.pg_stats_vacuum_tables` | Per-table heap vacuum stats (pages scanned, tuples deleted, WAL, timing, etc.) |
+| `ext_vacuum_statistics.pg_stats_vacuum_indexes` | Per-index vacuum stats |
+| `ext_vacuum_statistics.pg_stats_vacuum_database` | Per-database aggregate vacuum stats |
+
+## Limitations
+
+- Must be loaded via `shared_preload_libraries`; it cannot be loaded on demand.
+- Tracking configuration (`add_track_*`, `remove_track_*`) is stored in a file and shared across all databases in the cluster.
diff --git a/contrib/ext_vacuum_statistics/expected/ext_vacuum_statistics.out b/contrib/ext_vacuum_statistics/expected/ext_vacuum_statistics.out
new file mode 100644
index 00000000000..89c9594dea8
--- /dev/null
+++ b/contrib/ext_vacuum_statistics/expected/ext_vacuum_statistics.out
@@ -0,0 +1,52 @@
+-- ext_vacuum_statistics regression test
+
+-- Create extension
+CREATE EXTENSION ext_vacuum_statistics;
+
+-- Verify schema and views exist
+SELECT nspname FROM pg_namespace WHERE nspname = 'ext_vacuum_statistics';
+ nspname
+------------------
+ ext_vacuum_statistics
+(1 row)
+
+-- Views should be queryable (may return empty if no vacuum has run)
+SELECT COUNT(*) >= 0 FROM ext_vacuum_statistics.pg_stats_vacuum_tables;
+ ?column?
+----------
+ t
+(1 row)
+
+SELECT COUNT(*) >= 0 FROM ext_vacuum_statistics.pg_stats_vacuum_indexes;
+ ?column?
+----------
+ t
+(1 row)
+
+SELECT COUNT(*) >= 0 FROM ext_vacuum_statistics.pg_stats_vacuum_database;
+ ?column?
+----------
+ t
+(1 row)
+
+-- Verify views have expected columns
+SELECT COUNT(*) AS tables_cols FROM information_schema.columns
+WHERE table_schema = 'ext_vacuum_statistics' AND table_name = 'tables';
+ tables_cols
+-------------
+ 28
+(1 row)
+
+SELECT COUNT(*) AS indexes_cols FROM information_schema.columns
+WHERE table_schema = 'ext_vacuum_statistics' AND table_name = 'indexes';
+ indexes_cols
+--------------
+ 20
+(1 row)
+
+SELECT COUNT(*) AS database_cols FROM information_schema.columns
+WHERE table_schema = 'ext_vacuum_statistics' AND table_name = 'database';
+ database_cols
+---------------
+ 15
+(1 row)
diff --git a/contrib/ext_vacuum_statistics/expected/vacuum-extending-in-repetable-read.out b/contrib/ext_vacuum_statistics/expected/vacuum-extending-in-repetable-read.out
new file mode 100644
index 00000000000..6b381f9d232
--- /dev/null
+++ b/contrib/ext_vacuum_statistics/expected/vacuum-extending-in-repetable-read.out
@@ -0,0 +1,52 @@
+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 ext_vacuum_statistics.pg_stats_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
+-------+--------------+--------------------+------------------+-----------------+-------------
+(0 rows)
+
+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 ext_vacuum_statistics.pg_stats_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 ext_vacuum_statistics.pg_stats_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/contrib/ext_vacuum_statistics/ext_vacuum_statistics--1.0.sql b/contrib/ext_vacuum_statistics/ext_vacuum_statistics--1.0.sql
new file mode 100644
index 00000000000..aa3a9ec9699
--- /dev/null
+++ b/contrib/ext_vacuum_statistics/ext_vacuum_statistics--1.0.sql
@@ -0,0 +1,272 @@
+/*-------------------------------------------------------------------------
+ *
+ * ext_vacuum_statistics--1.0.sql
+ * Extended vacuum statistics via hook and custom storage
+ *
+ * This extension collects extended vacuum statistics via set_report_vacuum_hook
+ * and stores them in shared memory.
+ *
+ *-------------------------------------------------------------------------
+ */
+
+\echo Use "CREATE EXTENSION ext_vacuum_statistics" to load this file. \quit
+
+CREATE SCHEMA IF NOT EXISTS ext_vacuum_statistics;
+
+COMMENT ON SCHEMA ext_vacuum_statistics IS
+ 'Extended vacuum statistics (heap, index, database)';
+
+-- Reset functions
+CREATE OR REPLACE FUNCTION ext_vacuum_statistics.extvac_reset_entry(
+ dboid oid,
+ relid oid,
+ type int4
+)
+RETURNS boolean
+AS 'MODULE_PATHNAME', 'extvac_reset_entry'
+LANGUAGE C STRICT PARALLEL SAFE;
+
+CREATE OR REPLACE FUNCTION ext_vacuum_statistics.extvac_reset_db_entry(dboid oid)
+RETURNS bigint
+AS 'MODULE_PATHNAME', 'extvac_reset_db_entry'
+LANGUAGE C STRICT PARALLEL SAFE;
+
+CREATE OR REPLACE FUNCTION ext_vacuum_statistics.vacuum_statistics_reset()
+RETURNS bigint
+AS 'MODULE_PATHNAME', 'vacuum_statistics_reset'
+LANGUAGE C STRICT PARALLEL SAFE;
+
+CREATE OR REPLACE FUNCTION ext_vacuum_statistics.shared_memory_size()
+RETURNS bigint
+AS 'MODULE_PATHNAME', 'extvac_shared_memory_size'
+LANGUAGE C STRICT PARALLEL SAFE;
+
+COMMENT ON FUNCTION ext_vacuum_statistics.shared_memory_size() IS
+ 'Total shared memory in bytes used by the extension for vacuum statistics.';
+
+-- Add/remove OIDs for tracking
+CREATE OR REPLACE FUNCTION ext_vacuum_statistics.add_track_database(dboid oid)
+RETURNS boolean
+AS 'MODULE_PATHNAME', 'evs_add_track_database'
+LANGUAGE C STRICT;
+
+CREATE OR REPLACE FUNCTION ext_vacuum_statistics.remove_track_database(dboid oid)
+RETURNS boolean
+AS 'MODULE_PATHNAME', 'evs_remove_track_database'
+LANGUAGE C STRICT;
+
+CREATE OR REPLACE FUNCTION ext_vacuum_statistics.add_track_relation(dboid oid, reloid oid)
+RETURNS boolean
+AS 'MODULE_PATHNAME', 'evs_add_track_relation'
+LANGUAGE C STRICT;
+
+CREATE OR REPLACE FUNCTION ext_vacuum_statistics.remove_track_relation(dboid oid, reloid oid)
+RETURNS boolean
+AS 'MODULE_PATHNAME', 'evs_remove_track_relation'
+LANGUAGE C STRICT;
+
+CREATE OR REPLACE FUNCTION ext_vacuum_statistics.track_list()
+RETURNS TABLE(track_kind text, dboid oid, reloid oid)
+AS 'MODULE_PATHNAME', 'evs_track_list'
+LANGUAGE C STRICT;
+
+COMMENT ON FUNCTION ext_vacuum_statistics.track_list() IS
+ 'List of database and relation OIDs for which vacuum statistics are collected.';
+
+-- Track-list mutation requires superuser or pg_read_all_stats; hide the
+-- functions from PUBLIC so the error is also produced for ordinary users
+-- before the C-level privilege check runs.
+REVOKE ALL ON FUNCTION ext_vacuum_statistics.add_track_database(oid) FROM PUBLIC;
+REVOKE ALL ON FUNCTION ext_vacuum_statistics.remove_track_database(oid) FROM PUBLIC;
+REVOKE ALL ON FUNCTION ext_vacuum_statistics.add_track_relation(oid, oid) FROM PUBLIC;
+REVOKE ALL ON FUNCTION ext_vacuum_statistics.remove_track_relation(oid, oid) FROM PUBLIC;
+GRANT EXECUTE ON FUNCTION ext_vacuum_statistics.add_track_database(oid) TO pg_read_all_stats;
+GRANT EXECUTE ON FUNCTION ext_vacuum_statistics.remove_track_database(oid) TO pg_read_all_stats;
+GRANT EXECUTE ON FUNCTION ext_vacuum_statistics.add_track_relation(oid, oid) TO pg_read_all_stats;
+GRANT EXECUTE ON FUNCTION ext_vacuum_statistics.remove_track_relation(oid, oid) TO pg_read_all_stats;
+
+-- Internal C function to fetch table vacuum stats
+CREATE OR REPLACE FUNCTION ext_vacuum_statistics.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 STABLE;
+
+-- Internal C function to fetch index vacuum stats
+CREATE OR REPLACE FUNCTION ext_vacuum_statistics.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 STABLE;
+
+-- Internal C function to fetch database vacuum stats
+CREATE OR REPLACE FUNCTION ext_vacuum_statistics.pg_stats_get_vacuum_database(
+ IN dboid oid,
+ OUT dbid 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 interrupts_count integer
+)
+RETURNS SETOF record
+AS 'MODULE_PATHNAME', 'pg_stats_get_vacuum_database'
+LANGUAGE C STRICT STABLE;
+
+-- View: vacuum statistics per table (heap)
+CREATE VIEW ext_vacuum_statistics.pg_stats_vacuum_tables AS
+SELECT
+ rel.oid AS relid,
+ ns.nspname AS schema,
+ rel.relname AS relname,
+ db.datname AS dbname,
+ 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_database db,
+ pg_class rel,
+ pg_namespace ns,
+ LATERAL ext_vacuum_statistics.pg_stats_get_vacuum_tables(db.oid, rel.oid) stats
+WHERE db.datname = current_database()
+ AND rel.relkind = 'r'
+ AND rel.relnamespace = ns.oid
+ AND rel.oid = stats.relid;
+
+COMMENT ON VIEW ext_vacuum_statistics.pg_stats_vacuum_tables IS
+ 'Extended vacuum statistics per table (heap)';
+
+-- View: vacuum statistics per index
+CREATE VIEW ext_vacuum_statistics.pg_stats_vacuum_indexes AS
+SELECT
+ rel.oid AS indexrelid,
+ ns.nspname AS schema,
+ rel.relname AS indexrelname,
+ db.datname AS dbname,
+ 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_database db,
+ pg_class rel,
+ pg_namespace ns,
+ LATERAL ext_vacuum_statistics.pg_stats_get_vacuum_indexes(db.oid, rel.oid) stats
+WHERE db.datname = current_database()
+ AND rel.relkind = 'i'
+ AND rel.relnamespace = ns.oid
+ AND rel.oid = stats.relid;
+
+COMMENT ON VIEW ext_vacuum_statistics.pg_stats_vacuum_indexes IS
+ 'Extended vacuum statistics per index';
+
+-- View: vacuum statistics per database (aggregate)
+CREATE VIEW ext_vacuum_statistics.pg_stats_vacuum_database AS
+SELECT
+ db.oid AS dboid,
+ db.datname AS dbname,
+ stats.total_blks_read AS db_blks_read,
+ stats.total_blks_hit AS db_blks_hit,
+ stats.total_blks_dirtied AS db_blks_dirtied,
+ stats.total_blks_written AS db_blks_written,
+ stats.wal_records AS db_wal_records,
+ stats.wal_fpi AS db_wal_fpi,
+ stats.wal_bytes AS db_wal_bytes,
+ stats.blk_read_time AS db_blk_read_time,
+ stats.blk_write_time AS db_blk_write_time,
+ stats.delay_time AS db_delay_time,
+ stats.total_time AS db_total_time,
+ stats.wraparound_failsafe_count AS db_wraparound_failsafe_count,
+ stats.interrupts_count
+FROM pg_database db
+LEFT JOIN LATERAL ext_vacuum_statistics.pg_stats_get_vacuum_database(db.oid) stats ON db.oid = stats.dbid;
+
+COMMENT ON VIEW ext_vacuum_statistics.pg_stats_vacuum_database IS
+ 'Extended vacuum statistics per database (aggregate)';
diff --git a/contrib/ext_vacuum_statistics/ext_vacuum_statistics.conf b/contrib/ext_vacuum_statistics/ext_vacuum_statistics.conf
new file mode 100644
index 00000000000..9b711487623
--- /dev/null
+++ b/contrib/ext_vacuum_statistics/ext_vacuum_statistics.conf
@@ -0,0 +1,2 @@
+# Config for ext_vacuum_statistics regression tests
+shared_preload_libraries = 'ext_vacuum_statistics'
diff --git a/contrib/ext_vacuum_statistics/ext_vacuum_statistics.control b/contrib/ext_vacuum_statistics/ext_vacuum_statistics.control
new file mode 100644
index 00000000000..518350a64b7
--- /dev/null
+++ b/contrib/ext_vacuum_statistics/ext_vacuum_statistics.control
@@ -0,0 +1,5 @@
+# ext_vacuum_statistics extension
+comment = 'Extended vacuum statistics via hook (requires shared_preload_libraries)'
+default_version = '1.0'
+relocatable = true
+module_pathname = '$libdir/ext_vacuum_statistics'
diff --git a/contrib/ext_vacuum_statistics/meson.build b/contrib/ext_vacuum_statistics/meson.build
new file mode 100644
index 00000000000..72338baa500
--- /dev/null
+++ b/contrib/ext_vacuum_statistics/meson.build
@@ -0,0 +1,41 @@
+# Copyright (c) 2022-2026, PostgreSQL Global Development Group
+#
+# ext_vacuum_statistics - extended vacuum statistics via hook
+# Requires shared_preload_libraries = 'ext_vacuum_statistics'
+
+ext_vacuum_statistics_sources = files(
+ 'vacuum_statistics.c',
+)
+
+ext_vacuum_statistics = shared_module('ext_vacuum_statistics',
+ ext_vacuum_statistics_sources,
+ kwargs: contrib_mod_args + {
+ 'dependencies': contrib_mod_args['dependencies'],
+ },
+)
+contrib_targets += ext_vacuum_statistics
+
+install_data(
+ 'ext_vacuum_statistics.control',
+ 'ext_vacuum_statistics--1.0.sql',
+ kwargs: contrib_data_args,
+)
+
+tests += {
+ 'name': 'ext_vacuum_statistics',
+ 'sd': meson.current_source_dir(),
+ 'bd': meson.current_build_dir(),
+ 'isolation': {
+ 'specs': [
+ 'vacuum-extending-in-repetable-read',
+ ],
+ 'regress_args': ['--temp-config', files('ext_vacuum_statistics.conf')],
+ 'runningcheck': false,
+ },
+ 'tap': {
+ 'tests': [
+ 't/052_vacuum_extending_basic_test.pl',
+ 't/053_vacuum_extending_freeze_test.pl',
+ ],
+ },
+}
diff --git a/contrib/ext_vacuum_statistics/specs/vacuum-extending-in-repetable-read.spec b/contrib/ext_vacuum_statistics/specs/vacuum-extending-in-repetable-read.spec
new file mode 100644
index 00000000000..4891e248cca
--- /dev/null
+++ b/contrib/ext_vacuum_statistics/specs/vacuum-extending-in-repetable-read.spec
@@ -0,0 +1,59 @@
+# Test for checking recently_dead_tuples, tuples_deleted and frozen tuples in ext_vacuum_statistics.pg_stats_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);
+ CREATE EXTENSION ext_vacuum_statistics;
+ SET track_io_timing = on;
+}
+
+teardown
+{
+ DROP EXTENSION ext_vacuum_statistics CASCADE;
+ DROP TABLE test_vacuum_stat_isolation CASCADE;
+ RESET track_io_timing;
+}
+
+session s1
+setup {
+ SET track_io_timing = on;
+}
+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
+setup {
+ SET track_io_timing = 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; }
+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 ext_vacuum_statistics.pg_stats_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
diff --git a/contrib/ext_vacuum_statistics/t/052_vacuum_extending_basic_test.pl b/contrib/ext_vacuum_statistics/t/052_vacuum_extending_basic_test.pl
new file mode 100644
index 00000000000..9463d5145f4
--- /dev/null
+++ b/contrib/ext_vacuum_statistics/t/052_vacuum_extending_basic_test.pl
@@ -0,0 +1,780 @@
+# 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 heap tables, indexes, and databases using:
+#
+# • ext_vacuum_statistics.pg_stats_vacuum_tables
+# • ext_vacuum_statistics.pg_stats_vacuum_indexes
+# • ext_vacuum_statistics.pg_stats_vacuum_database
+#
+# 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: preload extension and logging level
+$node->append_conf('postgresql.conf', q{
+ shared_preload_libraries = 'ext_vacuum_statistics'
+ 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;
+ CREATE EXTENSION ext_vacuum_statistics;
+});
+# 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 EXTENSION ext_vacuum_statistics;
+ 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;"
+);
+
+#------------------------------------------------------------------------------
+# 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 ext_vacuum_statistics.pg_stats_vacuum_tables and ext_vacuum_statistics.pg_stats_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.
+#------------------------------------------------------------------------------
+
+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) {
+
+ my $result_query = $node->safe_psql(
+ $dbname,
+ "VACUUM vestat;
+ SELECT
+ (SELECT (tuples_deleted > $tab_tuples_deleted AND wal_records > $tab_wal_records)
+ FROM ext_vacuum_statistics.pg_stats_vacuum_tables
+ WHERE relname = 'vestat')
+ AND
+ (SELECT (tuples_deleted > $idx_tuples_deleted AND wal_records > $idx_wal_records)
+ FROM ext_vacuum_statistics.pg_stats_vacuum_indexes
+ WHERE indexrelname = 'vestat_pkey');"
+ );
+
+ return 1 if ($result_query eq 't');
+
+ sleep($interval);
+ }
+
+ return 0;
+}
+
+#------------------------------------------------------------------------------
+# Variables to hold vacuum-stat snapshots for later comparisons
+#------------------------------------------------------------------------------
+
+my $vm_new_visible_frozen_pages = 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 $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 $vm_new_visible_frozen_pages_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;
+
+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 and its
+# primary index, 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_visible_frozen_pages, tuples_deleted, pages_scanned, pages_removed, wal_records, wal_bytes, wal_fpi
+ FROM ext_vacuum_statistics.pg_stats_vacuum_tables
+ WHERE relname = 'vestat';"
+ );
+
+ $base_statistics =~ s/\s*\|\s*/ /g; # transform " | " into space
+ ($vm_new_visible_frozen_pages, $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 ext_vacuum_statistics.pg_stats_vacuum_indexes
+ WHERE indexrelname = '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;
+}
+
+#------------------------------------------------------------------------------
+# 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 {
+ $vm_new_visible_frozen_pages_prev = $vm_new_visible_frozen_pages;
+ $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;
+
+ $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;
+}
+
+#------------------------------------------------------------------------------
+# 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" .
+ " vm_new_visible_frozen_pages = $vm_new_visible_frozen_pages_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" .
+ " vm_new_visible_frozen_pages = $vm_new_visible_frozen_pages\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" .
+ "Index statistics:\n" .
+ " Before test:\n" .
+ " tuples_deleted = $index_tuples_deleted_prev\n" .
+ " pages_deleted = $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_deleted = $index_pages_deleted\n" .
+ " wal_records = $index_wal_records\n" .
+ " wal_bytes = $index_wal_bytes\n" .
+ " wal_fpi = $index_wal_fpi\n"
+ );
+};
+
+sub fetch_error_base_db_vacuum_statistics {
+ my (%args) = @_;
+
+ # Validate presence of required args (allow 0 as valid numeric baseline)
+ die "database name required"
+ unless exists $args{database_name} && defined $args{database_name};
+ my $database_name = $args{database_name};
+
+ # fetch actual base database vacuum statistics
+ my $base_statistics = $node->safe_psql(
+ $database_name,
+ "SELECT db_blks_hit, db_blks_dirtied,
+ db_blks_written, db_wal_records,
+ db_wal_fpi, db_wal_bytes
+ FROM ext_vacuum_statistics.pg_stats_vacuum_database, pg_database
+ WHERE pg_database.datname = '$dbname'
+ AND pg_database.oid = ext_vacuum_statistics.pg_stats_vacuum_database.dboid;"
+ );
+ $base_statistics =~ s/\s*\|\s*/ /g; # transform " | " in space
+ my ($db_blks_hit, $total_blks_dirtied, $total_blks_written,
+ $wal_records, $wal_fpi, $wal_bytes) = split /\s+/, $base_statistics;
+
+ diag(
+ "BASE STATS MISMATCH FOR DATABASE $dbname:\n" .
+ " db_blks_hit = $db_blks_hit\n" .
+ " total_blks_dirtied = $total_blks_dirtied\n" .
+ " total_blks_written = $total_blks_written\n" .
+ " wal_records = $wal_records\n" .
+ " wal_fpi = $wal_fpi\n" .
+ " wal_bytes = $wal_bytes\n"
+ );
+}
+
+
+#------------------------------------------------------------------------------
+# Test 1: Delete half the rows, run VACUUM, and wait for stats to advance
+#------------------------------------------------------------------------------
+subtest 'Test 1: Delete half the rows, run VACUUM' => 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,
+ 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 ext_vacuum_statistics update after $timeout seconds after vacuuming half-deleted table";
+
+fetch_vacuum_stats();
+
+ok($vm_new_visible_frozen_pages == $vm_new_visible_frozen_pages_prev, 'table vm_new_visible_frozen_pages 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');
+
+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();
+
+#------------------------------------------------------------------------------
+# Test 2: Delete all rows, run VACUUM, and wait for stats to advance
+#------------------------------------------------------------------------------
+subtest 'Test 2: Delete all rows, run VACUUM' => sub
+{
+save_vacuum_stats();
+
+$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,
+ 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)')
+ or diag "Timeout waiting for ext_vacuum_statistics update after $timeout seconds after vacuuming all-deleted table";
+
+fetch_vacuum_stats();
+
+ok($vm_new_visible_frozen_pages > $vm_new_visible_frozen_pages_prev, 'table vm_new_visible_frozen_pages has increased');
+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 > 0, '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 stay the same');
+
+} or print_vacuum_stats_on_error();
+
+#------------------------------------------------------------------------------
+# 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
+{
+save_vacuum_stats();
+
+$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;"
+);
+
+fetch_vacuum_stats();
+
+ok($vm_new_visible_frozen_pages == $vm_new_visible_frozen_pages_prev, 'table vm_new_visible_frozen_pages 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');
+
+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();
+
+#------------------------------------------------------------------------------
+# 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
+{
+
+save_vacuum_stats();
+
+$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_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 updating tuples in the table (tuples_deleted and wal_records advanced)')
+ or diag "Timeout waiting for ext_vacuum_statistics update after $timeout seconds";
+
+fetch_vacuum_stats();
+
+ok($vm_new_visible_frozen_pages == $vm_new_visible_frozen_pages_prev, 'table vm_new_visible_frozen_pages 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');
+
+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();
+
+#------------------------------------------------------------------------------
+# Test 5: Update table, trancate and vacuuming
+#------------------------------------------------------------------------------
+subtest 'Test 5: Update table, trancate and vacuuming' => sub
+{
+
+save_vacuum_stats();
+
+$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_wal_records => $wal_records_prev,
+);
+
+ok($updated, 'vacuum stats updated after updating tuples and trancation in the table (wal_records advanced)')
+ or diag "Timeout waiting for ext_vacuum_statistics update after $timeout seconds";
+
+fetch_vacuum_stats();
+
+ok($vm_new_visible_frozen_pages == $vm_new_visible_frozen_pages_prev, 'table vm_new_visible_frozen_pages 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');
+
+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();
+
+#------------------------------------------------------------------------------
+# Test 6: Delete all tuples from table, trancate, and vacuuming
+#------------------------------------------------------------------------------
+subtest 'Test 6: Delete all tuples from table, trancate, and vacuuming' => sub
+{
+
+save_vacuum_stats();
+
+$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_wal_records => $wal_records,
+);
+
+ok($updated, 'vacuum stats updated after deleting all tuples and trancation in the table (wal_records advanced)')
+ or diag "Timeout waiting for ext_vacuum_statistics update after $timeout seconds";
+
+fetch_vacuum_stats();
+
+ok($vm_new_visible_frozen_pages == $vm_new_visible_frozen_pages_prev, 'table vm_new_visible_frozen_pages 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');
+
+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();
+
+my $dboid = $node->safe_psql(
+ $dbname,
+ "SELECT oid FROM pg_database WHERE datname = current_database();"
+);
+
+#-------------------------------------------------------------------------------------------------------
+# Test 7: Check if we return single vacuum statistics for particular relation from the current database
+#-------------------------------------------------------------------------------------------------------
+subtest 'Test 7: Check if we return vacuum statistics from the current database' => sub
+{
+save_vacuum_stats();
+
+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 relation in the current database
+$base_stats = $node->safe_psql(
+ $dbname,
+ "SELECT count(*) FROM ext_vacuum_statistics.pg_stats_get_vacuum_tables((SELECT oid FROM pg_database WHERE datname = current_database()), $reloid);"
+);
+is($base_stats, 1, '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(*) FROM ext_vacuum_statistics.pg_stats_get_vacuum_indexes((SELECT oid FROM pg_database WHERE datname = current_database()), $reloid);"
+);
+is($base_stats, 1, '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(*) FROM ext_vacuum_statistics.pg_stats_get_vacuum_tables((SELECT oid FROM pg_database WHERE datname = current_database()), 1);"
+);
+is($base_stats, 0, 'table vacuum stats return no rows, as expected');
+
+$base_stats = $node->safe_psql(
+ $dbname,
+ "SELECT count(*) FROM ext_vacuum_statistics.pg_stats_get_vacuum_indexes((SELECT oid FROM pg_database WHERE datname = current_database()), 1);"
+);
+is($base_stats, 0, 'index vacuum stats return no rows, as expected');
+
+# Check if we can get vacuum statistics of all relations in the current database
+$base_stats = $node->safe_psql(
+ $dbname,
+ "SELECT count(*) > 0 FROM ext_vacuum_statistics.pg_stats_vacuum_tables;"
+);
+ok($base_stats eq 't', 'vacuum stats per all heap objects available');
+
+$base_stats = $node->safe_psql(
+ $dbname,
+ "SELECT count(*) > 0 FROM ext_vacuum_statistics.pg_stats_vacuum_indexes;"
+);
+ok($base_stats eq 't', 'vacuum stats per all index objects available');
+};
+
+#------------------------------------------------------------------------------
+# Test 8: Check relation-level vacuum statistics from another database
+#------------------------------------------------------------------------------
+subtest 'Test 8: Check relation-level vacuum statistics from another database' => sub
+{
+$base_stats = $node->safe_psql(
+ 'postgres',
+ "SELECT count(*)
+ FROM ext_vacuum_statistics.pg_stats_vacuum_indexes
+ WHERE indexrelname = 'vestat_pkey';"
+);
+is($base_stats, 0, 'check the printing index vacuum extended statistics from another database are not available');
+
+$base_stats = $node->safe_psql(
+ 'postgres',
+ "SELECT count(*)
+ FROM ext_vacuum_statistics.pg_stats_vacuum_tables
+ WHERE relname = 'vestat';"
+);
+is($base_stats, 0, 'check the printing heap vacuum extended statistics from another database are not available');
+
+# Check that relations from another database are not visible in the view when querying from postgres
+$base_stats = $node->safe_psql(
+ 'postgres',
+ "SELECT count(*) FROM ext_vacuum_statistics.pg_stats_vacuum_tables WHERE relname = 'vestat';"
+);
+is($base_stats, 0, 'vacuum stats per all tables objects from another database are not available as expected');
+
+$base_stats = $node->safe_psql(
+ 'postgres',
+ "SELECT count(*) FROM ext_vacuum_statistics.pg_stats_vacuum_indexes WHERE indexrelname = 'vestat_pkey';"
+);
+is($base_stats, 0, 'vacuum stats per all index objects from another database are not available as expected');
+};
+
+#--------------------------------------------------------------------------------------
+# Test 9: Check database-level vacuum statistics from the current and another database
+#--------------------------------------------------------------------------------------
+subtest 'Test 9: Check database-level vacuum statistics from the current and another database' => sub
+{
+my $db_blk_hit = 0;
+my $total_blks_dirtied = 0;
+my $total_blks_written = 0;
+my $wal_records = 0;
+my $wal_fpi = 0;
+my $wal_bytes = 0;
+$base_stats = $node->safe_psql(
+ $dbname,
+ "SELECT db_blks_hit, db_blks_dirtied,
+ db_blks_written, db_wal_records,
+ db_wal_fpi, db_wal_bytes
+ FROM ext_vacuum_statistics.pg_stats_vacuum_database, pg_database
+ WHERE pg_database.datname = '$dbname'
+ AND pg_database.oid = ext_vacuum_statistics.pg_stats_vacuum_database.dboid;"
+);
+$base_stats =~ s/\s*\|\s*/ /g; # transform " | " into space
+ ($db_blk_hit, $total_blks_dirtied, $total_blks_written, $wal_records, $wal_fpi, $wal_bytes)
+ = split /\s+/, $base_stats;
+
+ok($db_blk_hit > 0, 'db_blks_hit is more than 0');
+ok($total_blks_dirtied > 0, 'total_blks_dirtied is more than 0');
+ok($total_blks_written > 0, 'total_blks_written is more than 0');
+ok($wal_records > 0, 'wal_records is more than 0');
+ok($wal_fpi > 0, 'wal_fpi is more than 0');
+ok($wal_bytes > 0, 'wal_bytes is more than 0');
+
+$base_stats = $node->safe_psql(
+ 'postgres',
+ "SELECT count(*) = 1
+ FROM ext_vacuum_statistics.pg_stats_vacuum_database, pg_database
+ WHERE pg_database.datname = '$dbname'
+ AND pg_database.oid = ext_vacuum_statistics.pg_stats_vacuum_database.dboid;"
+);
+ok($base_stats eq 't', 'check database-level vacuum stats from another database are available');
+};
+
+#------------------------------------------------------------------------------
+# Test 10: Cleanup checks: ensure functions return empty sets for OID = 0
+#------------------------------------------------------------------------------
+subtest 'Test 10: Cleanup checks: ensure functions return empty sets for OID = 0' => sub
+{
+my $dboid = $node->safe_psql(
+ $dbname,
+ "SELECT oid FROM pg_database WHERE datname = current_database();"
+);
+
+# Vacuum statistics for invalid relation OID return empty
+$base_stats = $node->safe_psql(
+ $dbname,
+ q{
+ SELECT COUNT(*)
+ FROM ext_vacuum_statistics.pg_stats_get_vacuum_tables((SELECT oid FROM pg_database WHERE datname = current_database()), 0);
+ }
+);
+is($base_stats, 0, 'vacuum stats per heap from invalid relation OID return empty as expected');
+
+$base_stats = $node->safe_psql(
+ $dbname,
+ q{
+ SELECT COUNT(*)
+ FROM ext_vacuum_statistics.pg_stats_get_vacuum_indexes((SELECT oid FROM pg_database WHERE datname = current_database()), 0);
+ }
+);
+is($base_stats, 0, 'vacuum stats per index from invalid relation OID return empty as expected');
+
+$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(*)
+ FROM ext_vacuum_statistics.pg_stats_vacuum_tables WHERE relid = 0;
+ }
+);
+is($base_stats, 0, 'ext_vacuum_statistics.pg_stats_vacuum_tables correctly returns no rows for OID = 0');
+
+$base_stats = $node->safe_psql(
+ $dbname,
+ q{
+ SELECT COUNT(*)
+ FROM ext_vacuum_statistics.pg_stats_vacuum_indexes WHERE indexrelid = 0;
+ }
+);
+is($base_stats, 0, 'ext_vacuum_statistics.pg_stats_vacuum_indexes correctly returns no rows for OID = 0');
+
+my $reloid = $node->safe_psql(
+ $dbname,
+ q{
+ SELECT oid FROM pg_class WHERE relname = 'pg_shdepend';
+ }
+);
+
+$node->safe_psql($dbname, "VACUUM pg_shdepend;");
+
+# Check if we can get vacuum statistics for cluster relations (shared catalogs)
+$base_stats = $node->safe_psql(
+ $dbname,
+ qq{
+ SELECT count(*) > 0
+ FROM ext_vacuum_statistics.pg_stats_get_vacuum_tables((SELECT oid FROM pg_database WHERE datname = current_database()), $reloid);
+ }
+);
+
+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(*) > 0
+ FROM ext_vacuum_statistics.pg_stats_get_vacuum_indexes((SELECT oid FROM pg_database WHERE datname = current_database()), $indoid);
+ }
+);
+
+is($base_stats, 't', 'vacuum stats for common index objects available');
+
+$node->safe_psql('postgres',
+ "DROP DATABASE $dbname;
+ VACUUM;"
+);
+
+$base_stats = $node->safe_psql(
+ 'postgres',
+ q{
+ SELECT count(*) = 0
+ FROM ext_vacuum_statistics.pg_stats_get_vacuum_database(0);
+ }
+);
+is($base_stats, 't', 'vacuum stats from database with invalid database OID return empty, as expected');
+};
+
+$node->stop;
+
+done_testing();
diff --git a/contrib/ext_vacuum_statistics/t/053_vacuum_extending_freeze_test.pl b/contrib/ext_vacuum_statistics/t/053_vacuum_extending_freeze_test.pl
new file mode 100644
index 00000000000..4f8f025c63e
--- /dev/null
+++ b/contrib/ext_vacuum_statistics/t/053_vacuum_extending_freeze_test.pl
@@ -0,0 +1,285 @@
+# Copyright (c) 2025 PostgreSQL Global Development Group
+#
+# Test cumulative vacuum stats using ext_vacuum_statistics extension (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: preload extension and aggressive freezing behavior
+$node->append_conf('postgresql.conf', q{
+ shared_preload_libraries = 'ext_vacuum_statistics'
+ 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
+ track_functions = 'all'
+});
+
+$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';
+
+# Create extension
+$node->safe_psql($dbname, q{
+ CREATE EXTENSION ext_vacuum_statistics;
+});
+
+#------------------------------------------------------------------------------
+# 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 ext_vacuum_statistics.pg_stats_vacuum_tables until the named columns exceed the
+# provided baseline values or until timeout.
+#
+# tab_all_frozen_pages_count => 0 # baseline numeric
+# tab_all_visible_pages_count => 0 # baseline numeric
+# run_vacuum => 0 # if true, run vacuum before polling
+#
+# Returns: 1 if the condition is met before timeout, 0 otherwise.
+#------------------------------------------------------------------------------
+sub wait_for_vacuum_stats {
+ my (%args) = @_;
+
+ my $tab_all_frozen_pages_count = $args{tab_all_frozen_pages_count} || 0;
+ my $tab_all_visible_pages_count = $args{tab_all_visible_pages_count} || 0;
+ my $run_vacuum = $args{run_vacuum} ? 1 : 0;
+ my $result_query;
+
+ my $start = time();
+ my $sql;
+
+ # Run VACUUM once if requested, before polling
+ if ($run_vacuum) {
+ $node->safe_psql($dbname, 'VACUUM (FREEZE, VERBOSE) vestat');
+ }
+
+ while ((time() - $start) < $timeout) {
+
+ if ($run_vacuum) {
+ $sql = "
+ SELECT (vm_new_visible_frozen_pages > $tab_all_frozen_pages_count)
+ FROM ext_vacuum_statistics.pg_stats_vacuum_tables
+ WHERE relname = 'vestat'";
+ }
+ else {
+ $sql = "
+ SELECT (pg_stat_get_frozen_page_marks_cleared(c.oid) > $tab_all_frozen_pages_count AND
+ pg_stat_get_visible_page_marks_cleared(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');
+
+ sleep($interval);
+ }
+
+ return 0;
+}
+
+#------------------------------------------------------------------------------
+# Variables to hold vacuum statistics snapshots for comparisons
+#------------------------------------------------------------------------------
+
+my $vm_new_visible_frozen_pages = 0;
+
+my $rev_all_frozen_pages = 0;
+my $rev_all_visible_pages = 0;
+
+my $vm_new_visible_frozen_pages_prev = 0;
+
+my $rev_all_frozen_pages_prev = 0;
+my $rev_all_visible_pages_prev = 0;
+
+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 {
+ $vm_new_visible_frozen_pages = $node->safe_psql(
+ $dbname,
+ "SELECT vt.vm_new_visible_frozen_pages
+ FROM ext_vacuum_statistics.pg_stats_vacuum_tables vt
+ WHERE vt.relname = 'vestat';"
+ );
+
+ $rev_all_frozen_pages = $node->safe_psql(
+ $dbname,
+ "SELECT pg_stat_get_frozen_page_marks_cleared(c.oid)
+ FROM pg_class c
+ WHERE c.relname = 'vestat';"
+ );
+
+ $rev_all_visible_pages = $node->safe_psql(
+ $dbname,
+ "SELECT pg_stat_get_visible_page_marks_cleared(c.oid)
+ FROM pg_class c
+ WHERE c.relname = 'vestat';"
+ );
+}
+
+#------------------------------------------------------------------------------
+# save_vacuum_stats
+#------------------------------------------------------------------------------
+sub save_vacuum_stats {
+ $vm_new_visible_frozen_pages_prev = $vm_new_visible_frozen_pages;
+ $rev_all_frozen_pages_prev = $rev_all_frozen_pages;
+ $rev_all_visible_pages_prev = $rev_all_visible_pages;
+}
+
+#------------------------------------------------------------------------------
+# print_vacuum_stats_on_error
+#------------------------------------------------------------------------------
+sub print_vacuum_stats_on_error {
+ diag(
+ "Statistics in the failed test\n" .
+ "Table statistics:\n" .
+ " Before test:\n" .
+ " vm_new_visible_frozen_pages = $vm_new_visible_frozen_pages_prev\n" .
+ " rev_all_frozen_pages = $rev_all_frozen_pages_prev\n" .
+ " rev_all_visible_pages = $rev_all_visible_pages_prev\n" .
+ " After test:\n" .
+ " vm_new_visible_frozen_pages = $vm_new_visible_frozen_pages\n" .
+ " rev_all_frozen_pages = $rev_all_frozen_pages\n" .
+ " rev_all_visible_pages = $rev_all_visible_pages\n"
+ );
+};
+
+#------------------------------------------------------------------------------
+# Test 1: Create test table, populate it and run an initial vacuum to force freezing
+#------------------------------------------------------------------------------
+
+subtest 'Test 1: Create test table, populate it and run an initial vacuum to force freezing' => sub
+{
+$node->safe_psql($dbname, q{
+ CREATE TABLE vestat (x int)
+ WITH (autovacuum_enabled = off, fillfactor = 10);
+ INSERT INTO vestat SELECT x FROM generate_series(1, 1000) AS g(x);
+ ANALYZE vestat;
+ VACUUM (FREEZE, VERBOSE) vestat;
+});
+
+$updated = wait_for_vacuum_stats(
+ 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_visible_frozen_pages advanced)')
+ or diag "Timeout waiting for ext_vacuum_statistics to update after $timeout seconds during vacuum";
+
+fetch_vacuum_stats();
+
+ok($vm_new_visible_frozen_pages > $vm_new_visible_frozen_pages_prev, 'table vm_new_visible_frozen_pages has increased');
+ok($rev_all_frozen_pages == $rev_all_frozen_pages_prev, 'table rev_all_frozen_pages stay the same');
+ok($rev_all_visible_pages == $rev_all_visible_pages_prev, 'table rev_all_visible_pages stay the same');
+} or print_vacuum_stats_on_error();
+
+#------------------------------------------------------------------------------
+# Test 2: Trigger backend updates
+# Backend activity should reset per-page visibility/freeze marks and increment revision counters
+#------------------------------------------------------------------------------
+subtest 'Test 2: Trigger backend updates' => sub
+{
+save_vacuum_stats();
+
+$node->safe_psql($dbname, q{
+ UPDATE vestat SET x = x + 1001;
+});
+
+$updated = wait_for_vacuum_stats(
+ 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 vacuum stats update after $timeout seconds";
+
+fetch_vacuum_stats();
+
+ok($vm_new_visible_frozen_pages == $vm_new_visible_frozen_pages_prev, 'table vm_new_visible_frozen_pages stay the same');
+ok($rev_all_frozen_pages > $rev_all_frozen_pages_prev, 'table rev_all_frozen_pages has increased');
+ok($rev_all_visible_pages > $rev_all_visible_pages_prev, 'table rev_all_visible_pages has increased');
+} or print_vacuum_stats_on_error();
+
+#------------------------------------------------------------------------------
+# Test 3: Force another vacuum after backend modifications - vacuum should restore freeze/visibility
+#------------------------------------------------------------------------------
+subtest 'Test 3: Force another vacuum after backend modifications - vacuum should restore freeze/visibility' => sub
+{
+save_vacuum_stats();
+
+$node->safe_psql($dbname, q{ VACUUM vestat; });
+
+$updated = wait_for_vacuum_stats(
+ tab_all_frozen_pages_count => $vm_new_visible_frozen_pages,
+ tab_all_visible_pages_count => 0,
+ run_vacuum => 1,
+);
+
+ok($updated,
+ 'vacuum stats updated after vacuuming the all-updated table (vm_new_visible_frozen_pages advanced)')
+ or diag "Timeout waiting for ext_vacuum_statistics to update after $timeout seconds during vacuum";
+
+fetch_vacuum_stats();
+
+ok($vm_new_visible_frozen_pages > $vm_new_visible_frozen_pages_prev, 'table vm_new_visible_frozen_pages has increased');
+ok($rev_all_frozen_pages == $rev_all_frozen_pages_prev, 'table rev_all_frozen_pages stay the same');
+ok($rev_all_visible_pages == $rev_all_visible_pages_prev, 'table rev_all_visible_pages stay the same');
+} or print_vacuum_stats_on_error();
+
+#------------------------------------------------------------------------------
+# Cleanup
+#------------------------------------------------------------------------------
+
+$node->safe_psql('postgres', q{
+ DROP DATABASE statistic_vacuum_database_regression;
+});
+
+$node->stop;
+done_testing();
diff --git a/contrib/ext_vacuum_statistics/t/054_vacuum_extending_gucs_test.pl b/contrib/ext_vacuum_statistics/t/054_vacuum_extending_gucs_test.pl
new file mode 100644
index 00000000000..a195249842b
--- /dev/null
+++ b/contrib/ext_vacuum_statistics/t/054_vacuum_extending_gucs_test.pl
@@ -0,0 +1,279 @@
+# Copyright (c) 2025 PostgreSQL Global Development Group
+#
+# Test GUC parameters for ext_vacuum_statistics extension:
+# vacuum_statistics.enabled
+# vacuum_statistics.object_types (all, databases, relations)
+# vacuum_statistics.track_relations (all, system, user)
+# vacuum_statistics.track_databases_from_list, add/remove_track_database
+# add/remove_track_database, add/remove_track_relation, track_*_from_list
+
+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_gucs');
+$node->init;
+
+$node->append_conf('postgresql.conf', q{
+ shared_preload_libraries = 'ext_vacuum_statistics'
+ log_min_messages = notice
+});
+
+$node->start;
+
+#------------------------------------------------------------------------------
+# Database creation and initialization
+#------------------------------------------------------------------------------
+
+$node->safe_psql('postgres', q{
+ CREATE DATABASE statistic_vacuum_gucs;
+});
+
+my $dbname = 'statistic_vacuum_gucs';
+
+$node->safe_psql($dbname, q{
+ CREATE EXTENSION ext_vacuum_statistics;
+ CREATE TABLE guc_test (x int PRIMARY KEY)
+ WITH (autovacuum_enabled = off);
+ INSERT INTO guc_test SELECT x FROM generate_series(1, 100) AS g(x);
+ ANALYZE guc_test;
+});
+
+# Get OIDs for filtering tests
+my $dboid = $node->safe_psql($dbname, q{SELECT oid FROM pg_database WHERE datname = current_database()});
+my $reloid = $node->safe_psql($dbname, q{SELECT oid FROM pg_class WHERE relname = 'guc_test'});
+
+#------------------------------------------------------------------------------
+# Reset stats and run vacuum (all in one session so GUCs persist)
+#------------------------------------------------------------------------------
+
+sub reset_and_vacuum {
+ my ($db, $table, $opts) = @_;
+ $table ||= 'guc_test';
+ my $gucs = $opts && $opts->{gucs} ? $opts->{gucs} : [];
+ my $modify = $opts && $opts->{modify};
+ my $extra = $opts && $opts->{extra_vacuum} ? $opts->{extra_vacuum} : [];
+ $extra = [$extra] unless ref $extra eq 'ARRAY';
+ my $sql = join("\n", (map { "SET $_;" } @$gucs),
+ "SELECT ext_vacuum_statistics.vacuum_statistics_reset();",
+ $modify ? (
+ "TRUNCATE $table;",
+ "INSERT INTO $table SELECT x FROM generate_series(1, 100) AS g(x);",
+ "DELETE FROM $table;",
+ ) : (),
+ "VACUUM $table;",
+ (map { "VACUUM $_;" } @$extra),
+ # Make pending stats visible to subsequent sessions without sleeping.
+ "SELECT pg_stat_force_next_flush();");
+ $node->safe_psql($db, $sql);
+}
+
+#------------------------------------------------------------------------------
+# Test 1: vacuum_statistics.enabled
+#------------------------------------------------------------------------------
+subtest 'vacuum_statistics.enabled' => sub {
+ reset_and_vacuum($dbname);
+
+ # Default: enabled - should have stats
+ my $count = $node->safe_psql($dbname,
+ "SELECT COUNT(*) FROM ext_vacuum_statistics.pg_stats_vacuum_tables WHERE relname = 'guc_test'");
+ ok($count > 0, 'stats collected when enabled');
+
+ # Disable, reset and vacuum in same session. Assert not only that the
+ # row count is zero, but that the specific counters remain zero: a stray
+ # row with zero counters would otherwise pass a bare COUNT(*)=0 check.
+ reset_and_vacuum($dbname, 'guc_test', { gucs => ['vacuum_statistics.enabled = off'] });
+
+ $count = $node->safe_psql($dbname,
+ "SELECT COUNT(*) FROM ext_vacuum_statistics.pg_stats_vacuum_tables WHERE relname = 'guc_test'");
+ is($count, 0, 'no rows when disabled');
+
+ my $sums = $node->safe_psql($dbname, q{
+ SELECT COALESCE(SUM(total_blks_read), 0)
+ + COALESCE(SUM(total_blks_dirtied), 0)
+ + COALESCE(SUM(pages_scanned), 0)
+ FROM ext_vacuum_statistics.pg_stats_vacuum_tables
+ WHERE relname = 'guc_test'
+ });
+ is($sums, '0', 'no counters accumulated when disabled');
+};
+
+#------------------------------------------------------------------------------
+# Test 2: vacuum_statistics.object_types (databases only, relations only)
+#------------------------------------------------------------------------------
+subtest 'vacuum_statistics.object_types' => sub {
+ # track only db stats, no relation stats
+ reset_and_vacuum($dbname, 'guc_test', {
+ gucs => ["vacuum_statistics.object_types = 'databases'"],
+ modify => 1,
+ });
+ my $db_has_dbs = $node->safe_psql($dbname,
+ "SELECT COALESCE(SUM(db_blks_hit), 0) FROM ext_vacuum_statistics.pg_stats_vacuum_database WHERE dboid = $dboid");
+ my $rel_dbs = $node->safe_psql($dbname,
+ "SELECT COUNT(*) FROM ext_vacuum_statistics.pg_stats_vacuum_tables WHERE relname = 'guc_test'");
+ is($rel_dbs, 0, 'track=databases: no relation stats');
+ ok($db_has_dbs > 0, 'track=databases: database stats collected');
+
+ # track only relation stats, no db stats
+ reset_and_vacuum($dbname, 'guc_test', {
+ gucs => ["vacuum_statistics.object_types = 'relations'"],
+ modify => 1,
+ });
+ my $db_has_rels = $node->safe_psql($dbname,
+ "SELECT COALESCE(SUM(db_blks_hit), 0) > 0 FROM ext_vacuum_statistics.pg_stats_vacuum_database WHERE dboid = $dboid");
+ my $rel_rels = $node->safe_psql($dbname,
+ "SELECT COUNT(*) FROM ext_vacuum_statistics.pg_stats_vacuum_tables WHERE relname = 'guc_test'");
+ ok($rel_rels > 0, 'track=relations: relation stats collected');
+ is($db_has_rels, 'f', 'track=relations: no database stats');
+};
+
+#------------------------------------------------------------------------------
+# Test 3: vacuum_statistics.track_relations (system, user)
+#------------------------------------------------------------------------------
+subtest 'vacuum_statistics.track_relations' => sub {
+ # track_relations - only user tables
+ reset_and_vacuum($dbname, 'guc_test', {
+ gucs => [
+ "vacuum_statistics.object_types = 'relations'",
+ "vacuum_statistics.track_relations = 'user'",
+ ],
+ extra_vacuum => ['pg_class'],
+ });
+
+ my $user_rel = $node->safe_psql($dbname,
+ "SELECT COUNT(*) FROM ext_vacuum_statistics.pg_stats_vacuum_tables WHERE relname = 'guc_test'");
+ my $sys_rel = $node->safe_psql($dbname,
+ "SELECT COUNT(*) FROM ext_vacuum_statistics.pg_stats_vacuum_tables WHERE relname = 'pg_class'");
+ ok($user_rel > 0, 'track_relations=user: user table stats collected');
+ is($sys_rel, 0, 'track_relations=user: system table stats not collected');
+
+ # track_relations - only system tables
+ reset_and_vacuum($dbname, 'guc_test', {
+ gucs => [
+ "vacuum_statistics.object_types = 'relations'",
+ "vacuum_statistics.track_relations = 'system'",
+ ],
+ extra_vacuum => ['pg_class'],
+ });
+
+ $user_rel = $node->safe_psql($dbname,
+ "SELECT COUNT(*) FROM ext_vacuum_statistics.pg_stats_vacuum_tables WHERE relname = 'guc_test'");
+ $sys_rel = $node->safe_psql($dbname,
+ "SELECT COUNT(*) FROM ext_vacuum_statistics.pg_stats_vacuum_tables WHERE relname = 'pg_class'");
+ is($user_rel, 0, 'track_relations=system: user table stats not collected');
+ ok($sys_rel > 0, 'track_relations=system: system table stats collected');
+};
+
+#------------------------------------------------------------------------------
+# Test 4: track_databases (via add/remove_track_database)
+#------------------------------------------------------------------------------
+subtest 'track_databases (add/remove)' => sub {
+ $node->safe_psql($dbname, "SELECT ext_vacuum_statistics.remove_track_database($dboid)");
+ $node->safe_psql($dbname, "SELECT ext_vacuum_statistics.add_track_database($dboid)");
+ reset_and_vacuum($dbname, 'guc_test', { gucs => ["vacuum_statistics.track_databases_from_list = on"], modify => 1 });
+
+ my $rel_count = $node->safe_psql($dbname,
+ "SELECT COUNT(*) FROM ext_vacuum_statistics.pg_stats_vacuum_tables WHERE relname = 'guc_test'");
+ ok($rel_count > 0, 'db in list: stats collected');
+
+ $node->safe_psql($dbname, "SELECT ext_vacuum_statistics.remove_track_database($dboid)");
+ reset_and_vacuum($dbname, 'guc_test', { gucs => ["vacuum_statistics.track_databases_from_list = on"], modify => 1 });
+
+ $rel_count = $node->safe_psql($dbname,
+ "SELECT COUNT(*) FROM ext_vacuum_statistics.pg_stats_vacuum_tables WHERE relname = 'guc_test'");
+ is($rel_count, 0, 'db removed from list: no stats');
+};
+
+#------------------------------------------------------------------------------
+# Test 5: track_relations (via add/remove_track_relation)
+#------------------------------------------------------------------------------
+subtest 'track_relations (add/remove)' => sub {
+ $node->safe_psql($dbname, "SELECT ext_vacuum_statistics.remove_track_relation($dboid, $reloid)");
+ $node->safe_psql($dbname, "SELECT ext_vacuum_statistics.add_track_relation($dboid, $reloid)");
+ reset_and_vacuum($dbname, 'guc_test', { gucs => ["vacuum_statistics.track_relations_from_list = on"], modify => 1 });
+
+ my $rel_count = $node->safe_psql($dbname,
+ "SELECT COUNT(*) FROM ext_vacuum_statistics.pg_stats_vacuum_tables WHERE relname = 'guc_test'");
+ ok($rel_count > 0, 'table in list: stats collected');
+
+ $node->safe_psql($dbname, "SELECT ext_vacuum_statistics.remove_track_relation($dboid, $reloid)");
+ reset_and_vacuum($dbname, 'guc_test', { gucs => ["vacuum_statistics.track_relations_from_list = on"], modify => 1 });
+
+ $rel_count = $node->safe_psql($dbname,
+ "SELECT COUNT(*) FROM ext_vacuum_statistics.pg_stats_vacuum_tables WHERE relname = 'guc_test'");
+ is($rel_count, 0, 'table removed from list: no stats');
+};
+
+#------------------------------------------------------------------------------
+# Test 6: vacuum_statistics.collect - per-category gating
+#
+# With collect='wal' only wal_* counters must advance; buffer, timing, and
+# general categories must stay at zero. With collect='buffers' the inverse
+# holds. Unknown tokens must be rejected by the check-hook.
+#------------------------------------------------------------------------------
+subtest 'vacuum_statistics.collect' => sub {
+ # wal-only: WAL counters should accumulate, buffers/timing/general should not.
+ reset_and_vacuum($dbname, 'guc_test', {
+ gucs => ["vacuum_statistics.collect = 'wal'"],
+ modify => 1,
+ });
+
+ my $wal = $node->safe_psql($dbname, q{
+ SELECT COALESCE(SUM(wal_records), 0) > 0
+ FROM ext_vacuum_statistics.pg_stats_vacuum_tables
+ WHERE relname = 'guc_test'
+ });
+ is($wal, 't', "collect='wal': wal_records accumulated");
+
+ my $other = $node->safe_psql($dbname, q{
+ SELECT COALESCE(SUM(total_blks_read), 0)
+ + COALESCE(SUM(total_blks_hit), 0)
+ + COALESCE(SUM(total_time), 0)
+ + COALESCE(SUM(tuples_deleted), 0)
+ + COALESCE(SUM(pages_scanned), 0)
+ FROM ext_vacuum_statistics.pg_stats_vacuum_tables
+ WHERE relname = 'guc_test'
+ });
+ is($other, '0',
+ "collect='wal': buffer/timing/general counters not accumulated");
+
+ # buffers-only: buffer counters should advance, WAL should not.
+ reset_and_vacuum($dbname, 'guc_test', {
+ gucs => ["vacuum_statistics.collect = 'buffers'"],
+ modify => 1,
+ });
+
+ my $buf = $node->safe_psql($dbname, q{
+ SELECT COALESCE(SUM(total_blks_read), 0)
+ + COALESCE(SUM(total_blks_hit), 0) > 0
+ FROM ext_vacuum_statistics.pg_stats_vacuum_tables
+ WHERE relname = 'guc_test'
+ });
+ is($buf, 't', "collect='buffers': buffer counters accumulated");
+
+ my $wal_off = $node->safe_psql($dbname, q{
+ SELECT COALESCE(SUM(wal_records), 0)
+ FROM ext_vacuum_statistics.pg_stats_vacuum_tables
+ WHERE relname = 'guc_test'
+ });
+ is($wal_off, '0',
+ "collect='buffers': WAL counters not accumulated");
+
+ # Unknown category must be rejected by the check-hook.
+ my ($ret, $stdout, $stderr) = $node->psql($dbname,
+ "SET vacuum_statistics.collect = 'nope'");
+ isnt($ret, 0, "collect='nope': rejected by check-hook");
+ like($stderr, qr/Unrecognized category "nope"/,
+ "collect='nope': errdetail names the offending token");
+};
+
+$node->stop;
+
+done_testing();
diff --git a/contrib/ext_vacuum_statistics/vacuum_statistics.c b/contrib/ext_vacuum_statistics/vacuum_statistics.c
new file mode 100644
index 00000000000..75d1bd2cf06
--- /dev/null
+++ b/contrib/ext_vacuum_statistics/vacuum_statistics.c
@@ -0,0 +1,1387 @@
+/*
+ * ext_vacuum_statistics - Extended vacuum statistics for PostgreSQL
+ *
+ * This module collects detailed vacuum statistics (I/O, WAL, timing, etc.)
+ * at relation and database level by hooking into the vacuum reporting path.
+ * Statistics are stored via pgstat custom statistics. Management of statistics
+ * storage and output functions are implemented in this module.
+ */
+#include "postgres.h"
+
+#include "access/transam.h"
+#include "catalog/catalog.h"
+#include "catalog/objectaccess.h"
+#include "catalog/pg_authid.h"
+#include "catalog/pg_class.h"
+#include "catalog/pg_database.h"
+#include "fmgr.h"
+#include "funcapi.h"
+#include "miscadmin.h"
+#include "pgstat.h"
+#include "storage/fd.h"
+#include "storage/ipc.h"
+#include "storage/lwlock.h"
+#include "utils/acl.h"
+#include "utils/builtins.h"
+#include "utils/fmgrprotos.h"
+#include "utils/guc.h"
+#include "utils/hsearch.h"
+#include "utils/lsyscache.h"
+#include "utils/pgstat_kind.h"
+#include "utils/pgstat_internal.h"
+#include "utils/tuplestore.h"
+
+#ifdef PG_MODULE_MAGIC
+PG_MODULE_MAGIC;
+#endif
+
+/* Two kinds: relations (tables/indexes) and database aggregates */
+#define PGSTAT_KIND_EXTVAC_RELATION 24
+#define PGSTAT_KIND_EXTVAC_DB 25
+
+#define SJ_NODENAME "vacuum_statistics"
+#define EVS_TRACK_FILENAME "pg_stat/ext_vacuum_statistics_track.oid"
+
+/* Bit flags for evs_track (object_types): 'all', 'databases', 'relations' */
+#define EVS_TRACK_RELATIONS 0x01
+#define EVS_TRACK_DATABASES 0x02
+
+/* Bit flags for evs_track_relations: 'all', 'system', 'user' */
+#define EVS_FILTER_SYSTEM 0x01
+#define EVS_FILTER_USER 0x02
+
+/*
+ * Bit flags for evs_collect_mask. Each category groups counters that can be
+ * accumulated (or skipped) together, letting users reduce overhead at run
+ * time by turning off categories they don't need.
+ */
+#define EVS_COLLECT_BUFFERS 0x1 /* blks_*, blk_*_time */
+#define EVS_COLLECT_WAL 0x2 /* wal_records, wal_fpi, wal_bytes */
+#define EVS_COLLECT_GENERAL 0x4 /* tuples_deleted, pages_*, vm_*,
+ * wraparound_failsafe_count,
+ * interrupts_count */
+#define EVS_COLLECT_TIMING 0x8 /* delay_time, total_time */
+#define EVS_COLLECT_ALL (EVS_COLLECT_BUFFERS | EVS_COLLECT_WAL | \
+ EVS_COLLECT_GENERAL | EVS_COLLECT_TIMING)
+
+/* GUCs */
+static bool evs_enabled = true;
+static char *evs_track = "all"; /* 'all', 'databases', 'relations' */
+static char *evs_track_relations = "all"; /* 'all', 'system', 'user' */
+static int evs_track_bits = EVS_TRACK_RELATIONS | EVS_TRACK_DATABASES;
+static int evs_track_relations_bits = EVS_FILTER_SYSTEM | EVS_FILTER_USER;
+static bool evs_track_databases_from_list = false; /* if true, track only
+ * databases in list */
+static bool evs_track_relations_from_list = false; /* if true, track only
+ * relations in list */
+static char *evs_collect = "all"; /* categories to collect */
+static int evs_collect_mask = EVS_COLLECT_ALL;
+
+/* Hook */
+static set_report_vacuum_hook_type prev_report_vacuum_hook = NULL;
+static object_access_hook_type prev_object_access_hook = NULL;
+static shmem_request_hook_type prev_shmem_request_hook = NULL;
+
+/* Forward declarations */
+static void pgstat_report_vacuum_extstats(Oid tableoid, bool shared,
+ PgStat_VacuumRelationCounts * params);
+static bool evs_oid_in_list(HTAB *hash, Oid oid);
+static void evs_track_hash_ensure_init(void);
+static void evs_track_save_file(void);
+static void evs_track_load_file(void);
+static void evs_drop_access_hook(ObjectAccessType access, Oid classId,
+ Oid objectId, int subId, void *arg);
+static void evs_shmem_request(void);
+
+/* Hash tables for track_databases and track_relations_list (backend-local) */
+static HTAB *evs_track_databases_hash = NULL;
+static HTAB *evs_track_relations_hash = NULL;
+static bool evs_track_hash_initialized = false;
+
+/*
+ * Named LWLock tranche protecting the on-disk track file and serializing
+ * backend-local reloads/saves across concurrent backends.
+ */
+#define EVS_TRACK_TRANCHE_NAME "ext_vacuum_statistics_track"
+static LWLock *evs_track_lock = NULL;
+
+static inline LWLock *
+evs_get_track_lock(void)
+{
+ if (evs_track_lock == NULL)
+ evs_track_lock = &GetNamedLWLockTranche(EVS_TRACK_TRANCHE_NAME)->lock;
+ return evs_track_lock;
+}
+
+/*
+ * objid encoding for relations: (relid << 2) | (type & 3)
+ */
+#define EXTVAC_OBJID(relid, type) (((uint64) (relid)) << 2 | ((type) & 3))
+
+/* Key for relation tracking: (dboid, reloid).
+ * InvalidOid for dboid means it is a cluster object.
+ */
+typedef struct
+{
+ Oid dboid;
+ Oid reloid;
+} EvsTrackRelKey;
+
+/* Shared memory entry for vacuum stats; one per relation or database. */
+typedef struct PgStatShared_ExtVacEntry
+{
+ PgStatShared_Common header;
+ PgStat_VacuumRelationCounts stats;
+} PgStatShared_ExtVacEntry;
+
+/* PgStat kind for per-relation vacuum statistics (tables/indexes) */
+static const PgStat_KindInfo extvac_relation_kind_info = {
+ .name = "ext_vacuum_statistics_relation",
+ .fixed_amount = false,
+ .accessed_across_databases = true,
+ .write_to_file = true,
+ .track_entry_count = true,
+ .shared_size = sizeof(PgStatShared_ExtVacEntry),
+ .shared_data_off = offsetof(PgStatShared_ExtVacEntry, stats),
+ .shared_data_len = sizeof(PgStat_VacuumRelationCounts),
+ .pending_size = 0,
+ .flush_pending_cb = NULL,
+};
+
+/* PgStat kind for per-database aggregated vacuum statistics */
+static const PgStat_KindInfo extvac_db_kind_info = {
+ .name = "ext_vacuum_statistics_db",
+ .fixed_amount = false,
+ .accessed_across_databases = true,
+ .write_to_file = true,
+ .track_entry_count = true,
+ .shared_size = sizeof(PgStatShared_ExtVacEntry),
+ .shared_data_off = offsetof(PgStatShared_ExtVacEntry, stats),
+ .shared_data_len = sizeof(PgStat_VacuumRelationCounts),
+ .pending_size = 0,
+ .flush_pending_cb = NULL,
+};
+
+/*
+ * Accumulate a single counter only if its category is enabled in
+ * evs_collect_mask. Parentheses around every argument: the macro is invoked
+ * from expression contexts and with expressions as the destination pointer.
+ */
+#define ACCUM_IF(dst, src, field, cat) \
+ do { \
+ if ((evs_collect_mask) & (cat)) \
+ ((dst))->field += ((src))->field; \
+ } while (0)
+
+static inline void
+pgstat_accumulate_common(PgStat_CommonCounts * dst, const PgStat_CommonCounts * src)
+{
+ ACCUM_IF(dst, src, total_blks_read, EVS_COLLECT_BUFFERS);
+ ACCUM_IF(dst, src, total_blks_hit, EVS_COLLECT_BUFFERS);
+ ACCUM_IF(dst, src, total_blks_dirtied, EVS_COLLECT_BUFFERS);
+ ACCUM_IF(dst, src, total_blks_written, EVS_COLLECT_BUFFERS);
+ ACCUM_IF(dst, src, blks_fetched, EVS_COLLECT_BUFFERS);
+ ACCUM_IF(dst, src, blks_hit, EVS_COLLECT_BUFFERS);
+ ACCUM_IF(dst, src, blk_read_time, EVS_COLLECT_BUFFERS);
+ ACCUM_IF(dst, src, blk_write_time, EVS_COLLECT_BUFFERS);
+ ACCUM_IF(dst, src, delay_time, EVS_COLLECT_TIMING);
+ ACCUM_IF(dst, src, total_time, EVS_COLLECT_TIMING);
+ ACCUM_IF(dst, src, wal_records, EVS_COLLECT_WAL);
+ ACCUM_IF(dst, src, wal_fpi, EVS_COLLECT_WAL);
+ ACCUM_IF(dst, src, wal_bytes, EVS_COLLECT_WAL);
+ ACCUM_IF(dst, src, wraparound_failsafe_count, EVS_COLLECT_GENERAL);
+ ACCUM_IF(dst, src, interrupts_count, EVS_COLLECT_GENERAL);
+ ACCUM_IF(dst, src, tuples_deleted, EVS_COLLECT_GENERAL);
+}
+
+static inline void
+pgstat_accumulate_extvac_stats(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);
+ Assert(src->type == dst->type);
+
+ pgstat_accumulate_common(&dst->common, &src->common);
+
+ if (dst->type == PGSTAT_EXTVAC_TABLE &&
+ (evs_collect_mask & EVS_COLLECT_GENERAL) != 0)
+ {
+ dst->table.pages_scanned += src->table.pages_scanned;
+ dst->table.pages_removed += src->table.pages_removed;
+ dst->table.tuples_frozen += src->table.tuples_frozen;
+ dst->table.recently_dead_tuples += src->table.recently_dead_tuples;
+ 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->table.missed_dead_pages += src->table.missed_dead_pages;
+ dst->table.missed_dead_tuples += src->table.missed_dead_tuples;
+ dst->table.index_vacuum_count += src->table.index_vacuum_count;
+ }
+ else if (dst->type == PGSTAT_EXTVAC_INDEX &&
+ (evs_collect_mask & EVS_COLLECT_GENERAL) != 0)
+ {
+ dst->index.pages_deleted += src->index.pages_deleted;
+ }
+}
+
+/*
+ * GUC check hooks: validate the string and compute the bitmask into *extra.
+ * Rejecting unknown values here prevents silent fall-through to "all".
+ */
+static bool
+evs_track_check_hook(char **newval, void **extra, GucSource source)
+{
+ int *bits;
+
+ if (*newval == NULL)
+ return false;
+
+ bits = (int *) guc_malloc(LOG, sizeof(int));
+ if (!bits)
+ return false;
+
+ if (strcmp(*newval, "all") == 0)
+ *bits = EVS_TRACK_RELATIONS | EVS_TRACK_DATABASES;
+ else if (strcmp(*newval, "databases") == 0)
+ *bits = EVS_TRACK_DATABASES;
+ else if (strcmp(*newval, "relations") == 0)
+ *bits = EVS_TRACK_RELATIONS;
+ else
+ {
+ guc_free(bits);
+ GUC_check_errdetail("Allowed values are \"all\", \"databases\", \"relations\".");
+ return false;
+ }
+ *extra = bits;
+ return true;
+}
+
+static void
+evs_track_assign_hook(const char *newval, void *extra)
+{
+ evs_track_bits = *((int *) extra);
+}
+
+static bool
+evs_track_relations_check_hook(char **newval, void **extra, GucSource source)
+{
+ int *bits;
+
+ if (*newval == NULL)
+ return false;
+
+ bits = (int *) guc_malloc(LOG, sizeof(int));
+ if (!bits)
+ return false;
+
+ if (strcmp(*newval, "all") == 0)
+ *bits = EVS_FILTER_SYSTEM | EVS_FILTER_USER;
+ else if (strcmp(*newval, "system") == 0)
+ *bits = EVS_FILTER_SYSTEM;
+ else if (strcmp(*newval, "user") == 0)
+ *bits = EVS_FILTER_USER;
+ else
+ {
+ guc_free(bits);
+ GUC_check_errdetail("Allowed values are \"all\", \"system\", \"user\".");
+ return false;
+ }
+ *extra = bits;
+ return true;
+}
+
+static void
+evs_track_relations_assign_hook(const char *newval, void *extra)
+{
+ evs_track_relations_bits = *((int *) extra);
+}
+
+/*
+ * Check hook for vacuum_statistics.collect.
+ *
+ * Accepts a comma- or whitespace-separated list of category names
+ * (buffers, wal, general, timing) or the shorthand "all". Computes the
+ * matching bitmask once and stashes it in *extra; the assign hook just
+ * copies it into evs_collect_mask. Unknown tokens are rejected so the
+ * setting cannot silently collapse to the "all" default.
+ */
+static bool
+evs_collect_check_hook(char **newval, void **extra, GucSource source)
+{
+ int *mask;
+ char *copy;
+ char *p;
+ char *tok;
+ int accum = 0;
+ bool saw_all = false;
+
+ if (*newval == NULL)
+ return false;
+
+ mask = (int *) guc_malloc(LOG, sizeof(int));
+ if (!mask)
+ return false;
+
+ /* Empty string means "all", matching the default behavior. */
+ if ((*newval)[0] == '\0')
+ {
+ *mask = EVS_COLLECT_ALL;
+ *extra = mask;
+ return true;
+ }
+
+ copy = pstrdup(*newval);
+ for (p = copy; (tok = strtok(p, " \t,")) != NULL; p = NULL)
+ {
+ if (pg_strcasecmp(tok, "all") == 0)
+ saw_all = true;
+ else if (pg_strcasecmp(tok, "buffers") == 0)
+ accum |= EVS_COLLECT_BUFFERS;
+ else if (pg_strcasecmp(tok, "wal") == 0)
+ accum |= EVS_COLLECT_WAL;
+ else if (pg_strcasecmp(tok, "general") == 0)
+ accum |= EVS_COLLECT_GENERAL;
+ else if (pg_strcasecmp(tok, "timing") == 0)
+ accum |= EVS_COLLECT_TIMING;
+ else
+ {
+ /*
+ * GUC_check_errdetail formats the message immediately, but tok
+ * points into copy; emit the detail first, then free the
+ * scratch buffer so the formatted string is already stashed in
+ * GUC_check_errdetail_string.
+ */
+ GUC_check_errdetail("Unrecognized category \"%s\" in vacuum_statistics.collect; "
+ "allowed values are \"all\", \"buffers\", \"wal\", \"general\", \"timing\".",
+ tok);
+ pfree(copy);
+ guc_free(mask);
+ return false;
+ }
+ }
+ pfree(copy);
+
+ *mask = saw_all ? EVS_COLLECT_ALL : accum;
+ if (*mask == 0)
+ *mask = EVS_COLLECT_ALL;
+ *extra = mask;
+ return true;
+}
+
+static void
+evs_collect_assign_hook(const char *newval, void *extra)
+{
+ evs_collect_mask = *((int *) extra);
+}
+
+void
+_PG_init(void)
+{
+ if (!process_shared_preload_libraries_in_progress)
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("ext_vacuum_statistics module could be loaded only on startup."),
+ errdetail("Add 'ext_vacuum_statistics' into the shared_preload_libraries list.")));
+
+ DefineCustomBoolVariable("vacuum_statistics.enabled",
+ "Enable extended vacuum statistics collection.",
+ NULL, &evs_enabled, true,
+ PGC_SUSET, 0, NULL, NULL, NULL);
+
+ DefineCustomStringVariable("vacuum_statistics.object_types",
+ "Object types for statistics: 'all', 'databases', 'relations'.",
+ NULL, &evs_track, "all",
+ PGC_SUSET, 0,
+ evs_track_check_hook,
+ evs_track_assign_hook, NULL);
+
+ DefineCustomStringVariable("vacuum_statistics.track_relations",
+ "When tracking relations: 'all', 'system', 'user'.",
+ NULL, &evs_track_relations, "all",
+ PGC_SUSET, 0,
+ evs_track_relations_check_hook,
+ evs_track_relations_assign_hook, NULL);
+
+ DefineCustomBoolVariable("vacuum_statistics.track_databases_from_list",
+ "If true, track only databases added via add_track_database.",
+ NULL, &evs_track_databases_from_list, false,
+ PGC_SUSET, 0, NULL, NULL, NULL);
+
+ DefineCustomBoolVariable("vacuum_statistics.track_relations_from_list",
+ "If true, track only relations added via add_track_relation.",
+ NULL, &evs_track_relations_from_list, false,
+ PGC_SUSET, 0, NULL, NULL, NULL);
+
+ DefineCustomStringVariable("vacuum_statistics.collect",
+ "Statistics categories to collect.",
+ "Comma- or whitespace-separated list of: "
+ "\"buffers\", \"wal\", \"general\", \"timing\"; "
+ "or \"all\" for every category (default).",
+ &evs_collect, "all",
+ PGC_SUSET, 0,
+ evs_collect_check_hook,
+ evs_collect_assign_hook, NULL);
+
+ MarkGUCPrefixReserved(SJ_NODENAME);
+
+ pgstat_register_kind(PGSTAT_KIND_EXTVAC_RELATION, &extvac_relation_kind_info);
+ pgstat_register_kind(PGSTAT_KIND_EXTVAC_DB, &extvac_db_kind_info);
+
+ prev_shmem_request_hook = shmem_request_hook;
+ shmem_request_hook = evs_shmem_request;
+
+ 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 = evs_drop_access_hook;
+}
+
+static void
+evs_shmem_request(void)
+{
+ if (prev_shmem_request_hook)
+ prev_shmem_request_hook();
+
+ RequestNamedLWLockTranche(EVS_TRACK_TRANCHE_NAME, 1);
+}
+
+/*
+ * Object access hook: remove dropped objects from track lists.
+ */
+static void
+evs_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)
+ {
+ if (classId == RelationRelationId && subId == 0)
+ {
+ char relkind = get_rel_relkind(objectId);
+ EvsTrackRelKey key;
+ bool found;
+
+ if (relkind == RELKIND_RELATION || relkind == RELKIND_INDEX)
+ {
+ LWLock *lock = evs_get_track_lock();
+
+ LWLockAcquire(lock, LW_EXCLUSIVE);
+ evs_track_hash_ensure_init();
+ key.dboid = MyDatabaseId;
+ key.reloid = objectId;
+ hash_search(evs_track_relations_hash, &key, HASH_REMOVE, &found);
+ key.dboid = InvalidOid;
+ hash_search(evs_track_relations_hash, &key, HASH_REMOVE, &found);
+ evs_track_save_file();
+ LWLockRelease(lock);
+ }
+ }
+
+ if (classId == DatabaseRelationId && objectId != InvalidOid)
+ {
+ LWLock *lock = evs_get_track_lock();
+ bool found;
+
+ LWLockAcquire(lock, LW_EXCLUSIVE);
+ evs_track_hash_ensure_init();
+ hash_search(evs_track_databases_hash, &objectId, HASH_REMOVE, &found);
+ evs_track_save_file();
+ LWLockRelease(lock);
+ }
+ }
+}
+
+/*
+ * Storage of track lists in a separate file.
+ *
+ * Stores the lists of database OIDs and (dboid, reloid) pairs used for
+ * selective tracking when track_databases_from_list or track_relations_from_list
+ * is enabled.
+ * Data stores in pg_stat/ext_vacuum_statistics_track.oid
+ */
+/*
+ * Initialize the backend-local tracking hashes and load their contents
+ * from the on-disk file.
+ *
+ * The hashes are per-backend, so no lock is needed to protect them from
+ * other processes; however, another backend may be concurrently rewriting
+ * the track file, so we take a shared lock for the file read.
+ */
+static void
+evs_track_hash_ensure_init(void)
+{
+ HASHCTL ctl;
+ LWLock *lock;
+ bool need_load;
+
+ if (evs_track_hash_initialized)
+ return;
+
+ lock = evs_get_track_lock();
+
+ if (evs_track_databases_hash == NULL)
+ {
+ memset(&ctl, 0, sizeof(ctl));
+ ctl.keysize = sizeof(Oid);
+ ctl.entrysize = sizeof(Oid);
+ ctl.hcxt = TopMemoryContext;
+ evs_track_databases_hash =
+ hash_create("ext_vacuum_statistics track databases",
+ 64, &ctl, HASH_ELEM | HASH_BLOBS);
+ }
+
+ if (evs_track_relations_hash == NULL)
+ {
+ memset(&ctl, 0, sizeof(ctl));
+ ctl.keysize = sizeof(EvsTrackRelKey);
+ ctl.entrysize = sizeof(EvsTrackRelKey);
+ ctl.hcxt = TopMemoryContext;
+ evs_track_relations_hash =
+ hash_create("ext_vacuum_statistics track relations",
+ 64, &ctl, HASH_ELEM | HASH_BLOBS);
+ }
+
+ need_load = !LWLockHeldByMe(lock);
+ if (need_load)
+ LWLockAcquire(lock, LW_SHARED);
+ PG_TRY();
+ {
+ evs_track_load_file();
+ evs_track_hash_initialized = true;
+ }
+ PG_FINALLY();
+ {
+ if (need_load)
+ LWLockRelease(lock);
+ }
+ PG_END_TRY();
+}
+
+/*
+ * Load track lists from disk into the backend-local hashes.
+ *
+ * Caller must hold evs_track_lock at least in shared mode, since the file
+ * may be concurrently rewritten by another backend.
+ */
+static void
+evs_track_load_file(void)
+{
+ char path[MAXPGPATH];
+ FILE *fp;
+ char buf[MAXPGPATH];
+ bool in_relations = false;
+ Oid oid;
+ EvsTrackRelKey key;
+ bool found;
+
+ if (!DataDir || DataDir[0] == '\0' ||
+ !evs_track_databases_hash || !evs_track_relations_hash)
+ return;
+
+ snprintf(path, sizeof(path), "%s/%s", DataDir, EVS_TRACK_FILENAME);
+ fp = AllocateFile(path, "r");
+ if (!fp)
+ {
+ if (errno != ENOENT)
+ ereport(LOG,
+ (errcode_for_file_access(),
+ errmsg("could not open track file \"%s\": %m", path)));
+ return;
+ }
+
+ PG_TRY();
+ {
+ while (fgets(buf, sizeof(buf), fp))
+ {
+ size_t len = strlen(buf);
+
+ /* Reject unterminated lines (longer than buffer) as corruption. */
+ if (len > 0 && buf[len - 1] != '\n' && !feof(fp))
+ ereport(ERROR,
+ (errcode(ERRCODE_DATA_CORRUPTED),
+ errmsg("line too long in track file \"%s\"", path)));
+
+ if (strncmp(buf, "[databases]", 11) == 0)
+ {
+ in_relations = false;
+ continue;
+ }
+ if (strncmp(buf, "[relations]", 11) == 0)
+ {
+ in_relations = true;
+ continue;
+ }
+ if (in_relations)
+ {
+ if (sscanf(buf, "%u %u", &key.dboid, &key.reloid) == 2)
+ hash_search(evs_track_relations_hash, &key, HASH_ENTER, &found);
+ else if (sscanf(buf, "%u", &oid) == 1)
+ {
+ key.dboid = InvalidOid;
+ key.reloid = oid;
+ hash_search(evs_track_relations_hash, &key, HASH_ENTER, &found);
+ }
+ }
+ else if (sscanf(buf, "%u", &oid) == 1)
+ hash_search(evs_track_databases_hash, &oid, HASH_ENTER, &found);
+ }
+
+ if (ferror(fp))
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not read track file \"%s\": %m", path)));
+ }
+ PG_FINALLY();
+ {
+ FreeFile(fp);
+ }
+ PG_END_TRY();
+}
+
+/*
+ * Atomically rewrite the track file. Caller must hold evs_track_lock
+ * in exclusive mode.
+ */
+static void
+evs_track_save_file(void)
+{
+ char path[MAXPGPATH];
+ char tmppath[MAXPGPATH];
+ FILE *fp;
+ HASH_SEQ_STATUS status;
+ Oid *entry;
+ EvsTrackRelKey *rel_entry;
+ bool failed = false;
+
+ if (!DataDir || DataDir[0] == '\0' ||
+ !evs_track_databases_hash || !evs_track_relations_hash)
+ return;
+
+ snprintf(path, sizeof(path), "%s/%s", DataDir, EVS_TRACK_FILENAME);
+ snprintf(tmppath, sizeof(tmppath), "%s.tmp", path);
+
+ fp = AllocateFile(tmppath, PG_BINARY_W);
+ if (!fp)
+ {
+ ereport(LOG,
+ (errcode_for_file_access(),
+ errmsg("could not create track file \"%s\": %m", tmppath)));
+ return;
+ }
+
+ PG_TRY();
+ {
+ if (fputs("[databases]\n", fp) == EOF)
+ failed = true;
+
+ if (!failed)
+ {
+ hash_seq_init(&status, evs_track_databases_hash);
+ while ((entry = (Oid *) hash_seq_search(&status)) != NULL)
+ {
+ if (fprintf(fp, "%u\n", *entry) < 0)
+ {
+ hash_seq_term(&status);
+ failed = true;
+ break;
+ }
+ }
+ }
+
+ if (!failed && fputs("[relations]\n", fp) == EOF)
+ failed = true;
+
+ if (!failed)
+ {
+ hash_seq_init(&status, evs_track_relations_hash);
+ while ((rel_entry = (EvsTrackRelKey *) hash_seq_search(&status)) != NULL)
+ {
+ int rc;
+
+ if (OidIsValid(rel_entry->dboid))
+ rc = fprintf(fp, "%u %u\n", rel_entry->dboid, rel_entry->reloid);
+ else
+ rc = fprintf(fp, "0 %u\n", rel_entry->reloid);
+ if (rc < 0)
+ {
+ hash_seq_term(&status);
+ failed = true;
+ break;
+ }
+ }
+ }
+
+ if (!failed && fflush(fp) != 0)
+ failed = true;
+
+ if (!failed)
+ {
+ int fd = fileno(fp);
+
+ if (fd >= 0 && pg_fsync(fd) != 0)
+ ereport(LOG,
+ (errcode_for_file_access(),
+ errmsg("could not fsync track file \"%s\": %m",
+ tmppath)));
+ }
+ }
+ PG_CATCH();
+ {
+ FreeFile(fp);
+ (void) unlink(tmppath);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+
+ if (FreeFile(fp) != 0)
+ {
+ ereport(LOG,
+ (errcode_for_file_access(),
+ errmsg("could not close track file \"%s\": %m", tmppath)));
+ failed = true;
+ }
+
+ if (failed)
+ {
+ ereport(LOG,
+ (errcode_for_file_access(),
+ errmsg("could not write track file \"%s\": %m", tmppath)));
+ if (unlink(tmppath) != 0 && errno != ENOENT)
+ ereport(LOG,
+ (errcode_for_file_access(),
+ errmsg("could not unlink \"%s\": %m", tmppath)));
+ return;
+ }
+
+ if (durable_rename(tmppath, path, LOG) != 0)
+ {
+ if (unlink(tmppath) != 0 && errno != ENOENT)
+ ereport(LOG,
+ (errcode_for_file_access(),
+ errmsg("could not unlink \"%s\": %m", tmppath)));
+ }
+}
+
+/*
+ * Check if OID is in the given hash
+ */
+static bool
+evs_oid_in_list(HTAB *hash, Oid oid)
+{
+ if (!hash)
+ return false;
+ if (hash_get_num_entries(hash) == 0)
+ return false;
+ return hash_search(hash, &oid, HASH_FIND, NULL) != NULL;
+}
+
+/*
+ * Check if (dboid, relid) is in track_relations list.
+ */
+static bool
+evs_rel_in_list(Oid dboid, Oid relid)
+{
+ EvsTrackRelKey key;
+
+ if (!evs_track_relations_hash)
+ return false;
+ if (hash_get_num_entries(evs_track_relations_hash) == 0)
+ return false;
+ key.dboid = dboid;
+ key.reloid = relid;
+ if (hash_search(evs_track_relations_hash, &key, HASH_FIND, NULL) != NULL)
+ return true;
+ key.dboid = InvalidOid;
+ return hash_search(evs_track_relations_hash, &key, HASH_FIND, NULL) != NULL;
+}
+
+/*
+ * Decide whether to track statistics for relations.
+ * Relation is tracked if it is in the track list or a special filter is enabled.
+ */
+static bool
+evs_should_track_relation_statistics(Oid dboid, Oid relid)
+{
+ evs_track_hash_ensure_init();
+
+ if (evs_track_databases_from_list &&
+ !evs_oid_in_list(evs_track_databases_hash, dboid))
+ return false;
+ if (evs_track_relations_from_list &&
+ !(evs_rel_in_list(dboid, relid) || evs_rel_in_list(InvalidOid, relid)))
+ return false;
+
+ if ((evs_track_bits & EVS_TRACK_RELATIONS) == 0)
+ return false; /* database-only mode */
+ if (evs_track_relations_bits == EVS_FILTER_SYSTEM)
+ return IsCatalogRelationOid(relid);
+ if (evs_track_relations_bits == EVS_FILTER_USER)
+ return !IsCatalogRelationOid(relid);
+ return true;
+}
+
+/*
+ * Decide whether to track statistics for databases.
+ * Database statistics is tracked if it is in the track list or a special filter is enabled.
+ */
+static bool
+evs_should_track_database_statistics(Oid dboid)
+{
+ evs_track_hash_ensure_init();
+
+ if (evs_track_databases_from_list &&
+ !evs_oid_in_list(evs_track_databases_hash, dboid))
+ return false;
+ if ((evs_track_bits & EVS_TRACK_DATABASES) == 0)
+ return false; /* relations-only mode */
+ if (evs_track_bits == EVS_TRACK_DATABASES)
+ return true; /* databases-only, accumulate to db */
+ return true;
+}
+
+
+/* Accumulate common counts for database-level stats. */
+static inline void
+pgstat_accumulate_common_for_db(PgStat_CommonCounts * dst,
+ const PgStat_CommonCounts * src)
+{
+ pgstat_accumulate_common(dst, src);
+}
+
+/*
+ * Store incoming vacuum stats into pgstat custom statistics.
+ * store_relation: create/update per-relation entry
+ * store_db: accumulate into database-level entry (dboid, objid=0).
+ * Uses pgstat_get_entry_ref_locked and pgstat_accumulate_* for atomic updates.
+ */
+static void
+extvac_store(Oid dboid, Oid relid, int type,
+ PgStat_VacuumRelationCounts * params,
+ bool store_relation, bool store_db)
+{
+ PgStat_EntryRef *entry_ref;
+ PgStatShared_ExtVacEntry *shared;
+ uint64 objid;
+
+ if (!evs_enabled)
+ return;
+
+ if (store_relation)
+ {
+ objid = EXTVAC_OBJID(relid, type);
+ entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_EXTVAC_RELATION, dboid, objid, false);
+ if (entry_ref)
+ {
+ shared = (PgStatShared_ExtVacEntry *) entry_ref->shared_stats;
+ if (shared->stats.type == PGSTAT_EXTVAC_INVALID)
+ {
+ memset(&shared->stats, 0, sizeof(shared->stats));
+ shared->stats.type = params->type;
+ }
+ pgstat_accumulate_extvac_stats(&shared->stats, params);
+ pgstat_unlock_entry(entry_ref);
+ }
+ }
+
+ if (store_db)
+ {
+ entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_EXTVAC_DB, dboid, InvalidOid, false);
+ if (entry_ref)
+ {
+ shared = (PgStatShared_ExtVacEntry *) entry_ref->shared_stats;
+ if (shared->stats.type == PGSTAT_EXTVAC_INVALID)
+ {
+ memset(&shared->stats, 0, sizeof(shared->stats));
+ shared->stats.type = PGSTAT_EXTVAC_DB;
+ }
+ pgstat_accumulate_common_for_db(&shared->stats.common, ¶ms->common);
+ pgstat_unlock_entry(entry_ref);
+ }
+ }
+}
+
+/*
+ * Vacuum report hook: called when vacuum finishes. Filters by track settings,
+ * stores stats per-relation and/or per-database, then chains to previous hook.
+ */
+static void
+pgstat_report_vacuum_extstats(Oid tableoid, bool shared,
+ PgStat_VacuumRelationCounts * params)
+{
+ Oid dboid = shared ? InvalidOid : MyDatabaseId;
+ bool store_relation;
+ bool store_db;
+
+ if (evs_enabled)
+ {
+ store_relation = evs_should_track_relation_statistics(dboid, tableoid);
+ store_db = evs_should_track_database_statistics(dboid);
+
+ if (store_relation || store_db)
+ extvac_store(dboid, tableoid, params->type, params, store_relation, store_db);
+ }
+ if (prev_report_vacuum_hook)
+ prev_report_vacuum_hook(tableoid, shared, params);
+}
+
+/* Reset statistics for a single relation entry. */
+static bool
+extvac_reset_by_relid(Oid dboid, Oid relid, int type)
+{
+ uint64 objid = EXTVAC_OBJID(relid, type);
+
+ pgstat_reset_entry(PGSTAT_KIND_EXTVAC_RELATION, dboid, objid, 0);
+ return true;
+}
+
+/* Callback for pgstat_reset_matching_entries: match relation entries for given db */
+static bool
+match_extvac_relations_for_db(PgStatShared_HashEntry *entry, Datum match_data)
+{
+ return entry->key.kind == PGSTAT_KIND_EXTVAC_RELATION &&
+ entry->key.dboid == DatumGetObjectId(match_data);
+}
+
+/*
+ * Reset statistics for a database (aggregate entry) and all its relations.
+ */
+static int64
+extvac_database_reset(Oid dboid)
+{
+ pgstat_reset_matching_entries(match_extvac_relations_for_db,
+ ObjectIdGetDatum(dboid), 0);
+ pgstat_reset_entry(PGSTAT_KIND_EXTVAC_DB, dboid, 0, 0);
+ return 1;
+}
+
+/* Reset all vacuum statistics (both relation and database entries). */
+static int64
+extvac_stat_reset(void)
+{
+ pgstat_reset_of_kind(PGSTAT_KIND_EXTVAC_RELATION);
+ pgstat_reset_of_kind(PGSTAT_KIND_EXTVAC_DB);
+ return 0; /* count not available */
+}
+
+PG_FUNCTION_INFO_V1(vacuum_statistics_reset);
+PG_FUNCTION_INFO_V1(extvac_shared_memory_size);
+PG_FUNCTION_INFO_V1(extvac_reset_entry);
+PG_FUNCTION_INFO_V1(extvac_reset_db_entry);
+
+Datum
+vacuum_statistics_reset(PG_FUNCTION_ARGS)
+{
+ PG_RETURN_INT64(extvac_stat_reset());
+}
+
+Datum
+extvac_reset_entry(PG_FUNCTION_ARGS)
+{
+ Oid dboid = PG_GETARG_OID(0);
+ Oid relid = PG_GETARG_OID(1);
+ int type = PG_GETARG_INT32(2);
+
+ PG_RETURN_BOOL(extvac_reset_by_relid(dboid, relid, type));
+}
+
+Datum
+extvac_reset_db_entry(PG_FUNCTION_ARGS)
+{
+ Oid dboid = PG_GETARG_OID(0);
+
+ PG_RETURN_INT64(extvac_database_reset(dboid));
+}
+
+/*
+ * Return total shared memory in bytes used by the extension for vacuum stats.
+ * Used for monitoring and capacity planning: memory grows with the number of
+ * tracked relations and databases.
+ */
+Datum
+extvac_shared_memory_size(PG_FUNCTION_ARGS)
+{
+ uint64 rel_count;
+ uint64 db_count;
+ uint64 total;
+ size_t entry_size = sizeof(PgStatShared_ExtVacEntry);
+
+ rel_count = pgstat_get_entry_count(PGSTAT_KIND_EXTVAC_RELATION);
+ db_count = pgstat_get_entry_count(PGSTAT_KIND_EXTVAC_DB);
+ total = rel_count + db_count;
+
+ PG_RETURN_INT64((int64) (total * entry_size));
+}
+
+/*
+ * Track list management: add/remove database or relation OIDs.
+ * Changes are persisted to pg_stat/ext_vacuum_statistics_track.oid.
+ */
+
+PG_FUNCTION_INFO_V1(evs_add_track_database);
+PG_FUNCTION_INFO_V1(evs_remove_track_database);
+PG_FUNCTION_INFO_V1(evs_add_track_relation);
+PG_FUNCTION_INFO_V1(evs_remove_track_relation);
+
+/*
+ * Mutating track-list entry points: require server-wide privilege, since
+ * the underlying lists steer tracking for every backend.
+ */
+static void
+evs_require_track_privilege(const char *funcname)
+{
+ if (!superuser() && !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("permission denied for function %s", funcname),
+ errhint("Only superusers and members of pg_read_all_stats "
+ "may change the vacuum statistics track list.")));
+}
+
+Datum
+evs_add_track_database(PG_FUNCTION_ARGS)
+{
+ Oid oid = PG_GETARG_OID(0);
+ bool found;
+ LWLock *lock;
+
+ evs_require_track_privilege("add_track_database");
+ lock = evs_get_track_lock();
+ LWLockAcquire(lock, LW_EXCLUSIVE);
+ evs_track_hash_ensure_init();
+ hash_search(evs_track_databases_hash, &oid, HASH_ENTER, &found);
+ evs_track_save_file();
+ LWLockRelease(lock);
+ PG_RETURN_BOOL(!found); /* true if newly added */
+}
+
+Datum
+evs_remove_track_database(PG_FUNCTION_ARGS)
+{
+ Oid oid = PG_GETARG_OID(0);
+ bool found;
+ LWLock *lock;
+
+ evs_require_track_privilege("remove_track_database");
+ lock = evs_get_track_lock();
+ LWLockAcquire(lock, LW_EXCLUSIVE);
+ evs_track_hash_ensure_init();
+ hash_search(evs_track_databases_hash, &oid, HASH_REMOVE, &found);
+ evs_track_save_file();
+ LWLockRelease(lock);
+ PG_RETURN_BOOL(found);
+}
+
+Datum
+evs_add_track_relation(PG_FUNCTION_ARGS)
+{
+ EvsTrackRelKey key;
+ bool found;
+ LWLock *lock;
+
+ evs_require_track_privilege("add_track_relation");
+ key.dboid = PG_GETARG_OID(0);
+ key.reloid = PG_GETARG_OID(1);
+ lock = evs_get_track_lock();
+ LWLockAcquire(lock, LW_EXCLUSIVE);
+ evs_track_hash_ensure_init();
+ hash_search(evs_track_relations_hash, &key, HASH_ENTER, &found);
+ evs_track_save_file();
+ LWLockRelease(lock);
+ PG_RETURN_BOOL(!found); /* true if newly added */
+}
+
+Datum
+evs_remove_track_relation(PG_FUNCTION_ARGS)
+{
+ EvsTrackRelKey key;
+ bool found;
+ LWLock *lock;
+
+ evs_require_track_privilege("remove_track_relation");
+ key.dboid = PG_GETARG_OID(0);
+ key.reloid = PG_GETARG_OID(1);
+ lock = evs_get_track_lock();
+ LWLockAcquire(lock, LW_EXCLUSIVE);
+ evs_track_hash_ensure_init();
+ hash_search(evs_track_relations_hash, &key, HASH_REMOVE, &found);
+ evs_track_save_file();
+ LWLockRelease(lock);
+ PG_RETURN_BOOL(found);
+}
+
+/*
+ * Returns the list of database and relation OIDs for which statistics
+ * are collected.
+ */
+PG_FUNCTION_INFO_V1(evs_track_list);
+
+Datum
+evs_track_list(PG_FUNCTION_ARGS)
+{
+ ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
+ TupleDesc tupdesc;
+ Tuplestorestate *tupstore;
+ MemoryContext per_query_ctx;
+ MemoryContext oldcontext;
+ Datum values[3];
+ bool nulls[3] = {false, false, false};
+ HASH_SEQ_STATUS status;
+ Oid *entry;
+ EvsTrackRelKey *rel_entry;
+
+ if (!rsinfo || !IsA(rsinfo, ReturnSetInfo))
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("ext_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("ext_vacuum_statistics: materialize mode required")));
+
+ evs_track_hash_ensure_init();
+
+ per_query_ctx = rsinfo->econtext->ecxt_per_query_memory;
+ oldcontext = MemoryContextSwitchTo(per_query_ctx);
+
+ if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
+ elog(ERROR, "ext_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;
+
+ /* Databases */
+ if (hash_get_num_entries(evs_track_databases_hash) == 0)
+ {
+ values[0] = CStringGetTextDatum("database");
+ nulls[1] = true;
+ nulls[2] = true;
+ tuplestore_putvalues(tupstore, tupdesc, values, nulls);
+ nulls[1] = false;
+ nulls[2] = false;
+ }
+ else
+ {
+ hash_seq_init(&status, evs_track_databases_hash);
+ while ((entry = (Oid *) hash_seq_search(&status)) != NULL)
+ {
+ values[0] = CStringGetTextDatum("database");
+ values[1] = ObjectIdGetDatum(*entry);
+ nulls[2] = true;
+ tuplestore_putvalues(tupstore, tupdesc, values, nulls);
+ nulls[2] = false;
+ }
+ }
+
+ /* Relations */
+ if (hash_get_num_entries(evs_track_relations_hash) == 0)
+ {
+ values[0] = CStringGetTextDatum("relation");
+ nulls[1] = true;
+ nulls[2] = true;
+ tuplestore_putvalues(tupstore, tupdesc, values, nulls);
+ nulls[1] = false;
+ nulls[2] = false;
+ }
+ else
+ {
+ hash_seq_init(&status, evs_track_relations_hash);
+ while ((rel_entry = (EvsTrackRelKey *) hash_seq_search(&status)) != NULL)
+ {
+ values[0] = CStringGetTextDatum("relation");
+ values[1] = ObjectIdGetDatum(rel_entry->dboid);
+ values[2] = ObjectIdGetDatum(rel_entry->reloid);
+ tuplestore_putvalues(tupstore, tupdesc, values, nulls);
+ }
+ }
+
+ MemoryContextSwitchTo(oldcontext);
+
+ return (Datum) 0;
+}
+
+/*
+ * Output vacuum statistics (tables, indexes, or per-database aggregates).
+ */
+#define EXTVAC_COMMON_STAT_COLS 12
+
+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);
+ 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);
+ 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->common.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->common.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);
+}
+
+static Datum
+pg_stats_vacuum(FunctionCallInfo fcinfo, int type)
+{
+ ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
+ MemoryContext per_query_ctx;
+ MemoryContext oldcontext;
+ Tuplestorestate *tupstore;
+ TupleDesc tupdesc;
+ Oid dbid = PG_GETARG_OID(0);
+
+ if (rsinfo == NULL || !IsA(rsinfo, ReturnSetInfo))
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("ext_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("ext_vacuum_statistics: materialize mode required")));
+
+ per_query_ctx = rsinfo->econtext->ecxt_per_query_memory;
+ oldcontext = MemoryContextSwitchTo(per_query_ctx);
+
+ if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
+ elog(ERROR, "ext_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);
+ PgStat_VacuumRelationCounts *stats;
+
+ if (!OidIsValid(relid))
+ return (Datum) 0;
+
+ stats = (PgStat_VacuumRelationCounts *)
+ pgstat_fetch_entry(PGSTAT_KIND_EXTVAC_RELATION, dbid,
+ EXTVAC_OBJID(relid, type), NULL);
+
+ if (!stats)
+ stats = (PgStat_VacuumRelationCounts *)
+ pgstat_fetch_entry(PGSTAT_KIND_EXTVAC_RELATION, InvalidOid,
+ EXTVAC_OBJID(relid, type), NULL);
+
+ if (stats && stats->type == type)
+ tuplestore_put_for_relation(relid, tupstore, tupdesc, stats);
+ }
+ else if (type == PGSTAT_EXTVAC_DB)
+ {
+ if (OidIsValid(dbid))
+ {
+#define EXTVAC_DB_STAT_COLS 14
+ Datum values[EXTVAC_DB_STAT_COLS];
+ bool nulls[EXTVAC_DB_STAT_COLS];
+ int i = 0;
+ PgStat_VacuumRelationCounts *stats;
+
+ stats = (PgStat_VacuumRelationCounts *)
+ pgstat_fetch_entry(PGSTAT_KIND_EXTVAC_DB, dbid,
+ InvalidOid, NULL);
+ if (stats && stats->type == PGSTAT_EXTVAC_DB)
+ {
+ memset(nulls, 0, sizeof(nulls));
+ values[i++] = ObjectIdGetDatum(dbid);
+ tuplestore_put_common(&stats->common, values, nulls, &i);
+ values[i++] = Int32GetDatum(stats->common.interrupts_count);
+ Assert(i == EXTVAC_DB_STAT_COLS);
+ tuplestore_putvalues(tupstore, tupdesc, values, nulls);
+ }
+ }
+ /* invalid dbid: return empty set */
+ }
+ else
+ elog(PANIC, "ext_vacuum_statistics: invalid type %d", type);
+
+ 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);
+
+Datum
+pg_stats_get_vacuum_tables(PG_FUNCTION_ARGS)
+{
+ return pg_stats_vacuum(fcinfo, PGSTAT_EXTVAC_TABLE);
+}
+
+Datum
+pg_stats_get_vacuum_indexes(PG_FUNCTION_ARGS)
+{
+ return pg_stats_vacuum(fcinfo, PGSTAT_EXTVAC_INDEX);
+}
+
+Datum
+pg_stats_get_vacuum_database(PG_FUNCTION_ARGS)
+{
+ return pg_stats_vacuum(fcinfo, PGSTAT_EXTVAC_DB);
+}
diff --git a/contrib/meson.build b/contrib/meson.build
index ebb7f83d8c5..d7dc0fd07f0 100644
--- a/contrib/meson.build
+++ b/contrib/meson.build
@@ -26,6 +26,7 @@ subdir('cube')
subdir('dblink')
subdir('dict_int')
subdir('dict_xsyn')
+subdir('ext_vacuum_statistics')
subdir('earthdistance')
subdir('file_fdw')
subdir('fuzzystrmatch')
diff --git a/doc/src/sgml/contrib.sgml b/doc/src/sgml/contrib.sgml
index b9b03654aad..2a38f9042bb 100644
--- a/doc/src/sgml/contrib.sgml
+++ b/doc/src/sgml/contrib.sgml
@@ -141,6 +141,7 @@ CREATE EXTENSION <replaceable>extension_name</replaceable>;
&dict-int;
&dict-xsyn;
&earthdistance;
+ &extvacuumstatistics;
&file-fdw;
&fuzzystrmatch;
&hstore;
diff --git a/doc/src/sgml/extvacuumstatistics.sgml b/doc/src/sgml/extvacuumstatistics.sgml
new file mode 100644
index 00000000000..75eb4691c4d
--- /dev/null
+++ b/doc/src/sgml/extvacuumstatistics.sgml
@@ -0,0 +1,502 @@
+<!-- doc/src/sgml/extvacuumstatistics.sgml -->
+
+<sect1 id="extvacuumstatistics" xreflabel="ext_vacuum_statistics">
+ <title>ext_vacuum_statistics — extended vacuum statistics</title>
+
+ <indexterm zone="extvacuumstatistics">
+ <primary>ext_vacuum_statistics</primary>
+ </indexterm>
+
+ <para>
+ The <filename>ext_vacuum_statistics</filename> module provides
+ extended per-table, per-index, and per-database vacuum statistics
+ (buffer I/O, WAL, general, timing) via views in the
+ <literal>ext_vacuum_statistics</literal> schema.
+ </para>
+
+ <para>
+ The module must be loaded by adding <literal>ext_vacuum_statistics</literal> to
+ <xref linkend="guc-shared-preload-libraries"/> in
+ <filename>postgresql.conf</filename>, because it registers a vacuum hook at
+ server startup. This means that a server restart is needed to add or remove
+ the module. After installation, run
+ <command>CREATE EXTENSION ext_vacuum_statistics</command> in each database
+ where you want to use it.
+ </para>
+
+ <para>
+ When active, the module provides views
+ <structname>ext_vacuum_statistics.pg_stats_vacuum_tables</structname>,
+ <structname>ext_vacuum_statistics.pg_stats_vacuum_indexes</structname>, and
+ <structname>ext_vacuum_statistics.pg_stats_vacuum_database</structname>,
+ plus functions to reset statistics and manage tracking.
+ </para>
+
+ <para>
+ Each tracked object (one table, one index, or one database) uses
+ approximately 232 bytes of shared memory on Linux x86_64 (e.g. Ubuntu):
+ common stats (buffers, WAL, timing) plus header and LWLock ~144 bytes;
+ type + union ~88 bytes (the union holds table-specific or index-specific
+ fields; the allocated size is the same for both). The exact size depends on the platform. Call
+ <function>ext_vacuum_statistics.shared_memory_size()</function> to get
+ the total shared memory used by the extension. The extension's GUCs allow controlling memory by limiting
+ which objects are tracked:
+ <varname>vacuum_statistics.object_types</varname>,
+ <varname>vacuum_statistics.track_relations</varname>, and
+ <varname>track_*_from_list</varname>.
+ Example: a database with 1000 tables and 2000 indexes uses about 700 KB
+ on Ubuntu ((1000 + 2000 + 1) × 232 bytes).
+ </para>
+
+ <sect2 id="extvacuumstatistics-pg-stats-vacuum-tables">
+ <title>The <structname>ext_vacuum_statistics.pg_stats_vacuum_tables</structname> View</title>
+
+ <indexterm zone="extvacuumstatistics">
+ <secondary>pg_stats_vacuum_tables</secondary>
+ </indexterm>
+
+ <para>
+ The view <structname>ext_vacuum_statistics.pg_stats_vacuum_tables</structname>
+ contains one row for each table in the current database (including TOAST
+ tables), showing statistics about vacuuming that specific table. The columns
+ are shown in <xref linkend="extvacuumstatistics-pg-stats-vacuum-tables-columns"/>.
+ </para>
+
+ <table id="extvacuumstatistics-pg-stats-vacuum-tables-columns">
+ <title><structname>ext_vacuum_statistics.pg_stats_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>dbname</structfield> <type>name</type>
+ </para>
+ <para>
+ Name of the database containing 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
+ </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
+ </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
+ </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
+ </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
+ </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 blocks by vacuum operations, in milliseconds
+ </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 blocks by vacuum operations, in milliseconds
+ </para></entry>
+ </row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>delay_time</structfield> <type>float8</type>
+ </para>
+ <para>
+ Time spent in vacuum delay points, 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>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>wraparound_failsafe_count</structfield> <type>int4</type>
+ </para>
+ <para>
+ Number of times vacuum was run to prevent a wraparound problem
+ </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 found in the buffer cache by vacuum
+ </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>pages_scanned</structfield> <type>int8</type>
+ </para>
+ <para>
+ Number of pages examined by vacuum operations
+ </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 physical storage by vacuum operations
+ </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 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 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 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_frozen</structfield> <type>int8</type>
+ </para>
+ <para>
+ Number of tuples 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 left due to visibility in transactions
+ </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>missed_dead_pages</structfield> <type>int8</type>
+ </para>
+ <para>
+ Number of pages that had at least one missed dead tuple
+ </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 tuples that could not be pruned due to failure to acquire a cleanup lock
+ </para></entry>
+ </row>
+ </tbody>
+ </tgroup>
+ </table>
+ </sect2>
+
+ <sect2 id="extvacuumstatistics-pg-stats-vacuum-indexes">
+ <title>The <structname>ext_vacuum_statistics.pg_stats_vacuum_indexes</structname> View</title>
+
+ <indexterm zone="extvacuumstatistics">
+ <secondary>pg_stats_vacuum_indexes</secondary>
+ </indexterm>
+
+ <para>
+ The view <structname>ext_vacuum_statistics.pg_stats_vacuum_indexes</structname>
+ contains one row for each index in the current database, showing statistics
+ about vacuuming that specific index. Columns include
+ <structfield>indexrelid</structfield>, <structfield>schema</structfield>,
+ <structfield>indexrelname</structfield>, <structfield>dbname</structfield>,
+ buffer I/O (<structfield>total_blks_read</structfield>,
+ <structfield>total_blks_hit</structfield>, etc.), WAL
+ (<structfield>wal_records</structfield>, <structfield>wal_fpi</structfield>,
+ <structfield>wal_bytes</structfield>), timing
+ (<structfield>blk_read_time</structfield>, <structfield>blk_write_time</structfield>,
+ <structfield>delay_time</structfield>, <structfield>total_time</structfield>),
+ and <structfield>tuples_deleted</structfield>, <structfield>pages_deleted</structfield>.
+ </para>
+ </sect2>
+
+ <sect2 id="extvacuumstatistics-pg-stats-vacuum-database">
+ <title>The <structname>ext_vacuum_statistics.pg_stats_vacuum_database</structname> View</title>
+
+ <indexterm zone="extvacuumstatistics">
+ <secondary>pg_stats_vacuum_database</secondary>
+ </indexterm>
+
+ <para>
+ The view <structname>ext_vacuum_statistics.pg_stats_vacuum_database</structname>
+ contains one row for each database in the cluster, showing aggregate vacuum
+ statistics for that database. Columns include
+ <structfield>dboid</structfield>, <structfield>dbname</structfield>,
+ <structfield>db_blks_read</structfield>, <structfield>db_blks_hit</structfield>,
+ <structfield>db_blks_dirtied</structfield>, <structfield>db_blks_written</structfield>,
+ WAL stats (<structfield>db_wal_records</structfield>,
+ <structfield>db_wal_fpi</structfield>, <structfield>db_wal_bytes</structfield>),
+ timing (<structfield>db_blk_read_time</structfield>,
+ <structfield>db_blk_write_time</structfield>, <structfield>db_delay_time</structfield>,
+ <structfield>db_total_time</structfield>),
+ <structfield>db_wraparound_failsafe_count</structfield>, and
+ <structfield>interrupts_count</structfield>.
+ </para>
+ </sect2>
+
+ <sect2 id="extvacuumstatistics-functions">
+ <title>Functions</title>
+
+ <variablelist>
+ <varlistentry>
+ <term>
+ <function>ext_vacuum_statistics.shared_memory_size()</function>
+ <returnvalue>bigint</returnvalue>
+ </term>
+ <listitem>
+ <para>
+ Returns the total shared memory in bytes used by the extension for
+ vacuum statistics (relations plus databases).
+ </para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term>
+ <function>ext_vacuum_statistics.vacuum_statistics_reset()</function>
+ <returnvalue>bigint</returnvalue>
+ </term>
+ <listitem>
+ <para>
+ Resets all vacuum statistics. Returns the number of entries reset.
+ </para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term>
+ <function>ext_vacuum_statistics.add_track_database(dboid oid)</function>
+ <returnvalue>boolean</returnvalue>
+ </term>
+ <listitem>
+ <para>
+ Adds a database OID to the tracking list (persisted to
+ <filename>pg_stat/ext_vacuum_statistics_track.oid</filename>).
+ Returns true if newly added.
+ </para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term>
+ <function>ext_vacuum_statistics.remove_track_database(dboid oid)</function>
+ <returnvalue>boolean</returnvalue>
+ </term>
+ <listitem>
+ <para>
+ Removes a database OID from the tracking list. Returns true if found and removed.
+ </para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term>
+ <function>ext_vacuum_statistics.add_track_relation(dboid oid, reloid oid)</function>
+ <returnvalue>boolean</returnvalue>
+ </term>
+ <listitem>
+ <para>
+ Adds a (database, relation) OID pair to the tracking list. Returns true if newly added.
+ </para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term>
+ <function>ext_vacuum_statistics.remove_track_relation(dboid oid, reloid oid)</function>
+ <returnvalue>boolean</returnvalue>
+ </term>
+ <listitem>
+ <para>
+ Removes a (database, relation) pair from the tracking list. Returns true if found and removed.
+ </para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term>
+ <function>ext_vacuum_statistics.track_list()</function>
+ <returnvalue>TABLE(track_kind text, dboid oid, reloid oid)</returnvalue>
+ </term>
+ <listitem>
+ <para>
+ Returns the list of database and relation OIDs for which vacuum statistics
+ are collected. When <structfield>dboid</structfield> or
+ <structfield>reloid</structfield> is NULL, statistics are collected for all.
+ </para>
+ </listitem>
+ </varlistentry>
+ </variablelist>
+ </sect2>
+
+ <sect2 id="extvacuumstatistics-configuration">
+ <title>Configuration Parameters</title>
+
+ <variablelist>
+ <varlistentry>
+ <term><varname>vacuum_statistics.enabled</varname> (<type>boolean</type>)</term>
+ <listitem>
+ <para>
+ Enables extended vacuum statistics collection. Default: <literal>on</literal>.
+ </para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><varname>vacuum_statistics.object_types</varname> (<type>string</type>)</term>
+ <listitem>
+ <para>
+ Object types for statistics: <literal>all</literal>, <literal>databases</literal>, or
+ <literal>relations</literal>. Default: <literal>all</literal>.
+ </para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><varname>vacuum_statistics.track_relations</varname> (<type>string</type>)</term>
+ <listitem>
+ <para>
+ When tracking relations: <literal>all</literal>, <literal>system</literal>, or
+ <literal>user</literal>. Default: <literal>all</literal>.
+ </para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><varname>vacuum_statistics.track_databases_from_list</varname> (<type>boolean</type>)</term>
+ <listitem>
+ <para>
+ If on, track only databases added via <function>add_track_database</function>.
+ Default: <literal>off</literal>.
+ </para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><varname>vacuum_statistics.track_relations_from_list</varname> (<type>boolean</type>)</term>
+ <listitem>
+ <para>
+ If on, track only relations added via <function>add_track_relation</function>.
+ Default: <literal>off</literal>.
+ </para>
+ </listitem>
+ </varlistentry>
+ </variablelist>
+ </sect2>
+</sect1>
diff --git a/doc/src/sgml/filelist.sgml b/doc/src/sgml/filelist.sgml
index 25a85082759..85d721467c0 100644
--- a/doc/src/sgml/filelist.sgml
+++ b/doc/src/sgml/filelist.sgml
@@ -133,6 +133,7 @@
<!ENTITY dict-xsyn SYSTEM "dict-xsyn.sgml">
<!ENTITY dummy-seclabel SYSTEM "dummy-seclabel.sgml">
<!ENTITY earthdistance SYSTEM "earthdistance.sgml">
+<!ENTITY extvacuumstatistics SYSTEM "extvacuumstatistics.sgml">
<!ENTITY file-fdw SYSTEM "file-fdw.sgml">
<!ENTITY fuzzystrmatch SYSTEM "fuzzystrmatch.sgml">
<!ENTITY hstore SYSTEM "hstore.sgml">
--
2.39.5 (Apple Git-154)
view thread (75+ messages) latest in thread
reply
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Reply to all the recipients using the --to and --cc options:
reply via email
To: [email protected]
Cc: [email protected], [email protected], [email protected], [email protected], [email protected], [email protected], [email protected], [email protected], [email protected], [email protected], [email protected], [email protected], [email protected], [email protected], [email protected]
Subject: Re: Vacuum statistics
In-Reply-To: <[email protected]>
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
This inbox is served by agora; see mirroring instructions
for how to clone and mirror all data and code used for this inbox