public inbox for [email protected]
help / color / mirror / Atom feedFrom: Alena Rybakina <[email protected]>
To: Alexander Korotkov <[email protected]>
Cc: pgsql-hackers <[email protected]>
Cc: Masahiko Sawada <[email protected]>
Cc: Amit Kapila <[email protected]>
Cc: Jim Nasby <[email protected]>
Cc: Bertrand Drouvot <[email protected]>
Cc: Kirill Reshke <[email protected]>
Cc: Melanie Plageman <[email protected]>
Cc: jian he <[email protected]>
Cc: Sami Imseih <[email protected]>
Cc: vignesh C <[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: Thu, 18 Jun 2026 06:32:27 +0300
Message-ID: <[email protected]> (raw)
In-Reply-To: <CAPpHfdvsNSu3vxBQK4srQk5bp1PDKkMLfbv7HhzE3D_yW9ELvg@mail.gmail.com>
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]>
<CAPpHfdvsNSu3vxBQK4srQk5bp1PDKkMLfbv7HhzE3D_yW9ELvg@mail.gmail.com>
Hi, Alexander! Thank you for the careful review. All five points are
addressed in the attached,
reworked patch set (now split into 8 per-category patches).
On 16.06.2026 20:30, Alexander Korotkov wrote:
> Some notes about it.
> 1) PgStat_CommonCounts.interrupts_count is never incremented.
Fixed. It's now incremented from vacuum_error_callback() (when
geterrlevel() == ERROR and a relation is set) via the new
pgstat_report_vacuum_error(), which bumps interrupts_count in the
PGSTAT_KIND_VACUUM_DB shmem entry. It writes
directly to shared memory because the vacuum's transaction is aborting,
and is reported at the database level
(pg_stat_vacuum_database). New TAP test 015_vacuum_stats_interrupts.pl
cancels a running VACUUM and checks
the counter advances.
(patch 0004)
> 2) LVRelState.wraparound_failsafe_count can only be incremented once
> in lazy_check_wraparound_failsafe(). Should we replace it with a
> bool?
Done. LVRelState.wraparound_failsafe is now a bool, set to true in
lazy_check_wraparound_failsafe(). The per-relation view reports it as a
0/1 flag reflecting
the latest vacuum, and the per-database aggregate sums those flags into
a count. This metric
is covered by 005_vacuum_stats_failsafe.pl (xid_wraparound module),
which burns enough
XIDs to engage the failsafe and asserts the counters advance.
(patch 0004)
> 3) Vacuum statistics isn't counted correctly for parallel index
> vacuum. parallel_vacuum_process_one_index() doesn't call neither
> extvac_stats_start_idx(), extvac_stats_end_idx(), or
> extvac_accumulate_idx_repor(). This leads all index vacuum stats to
> go the heap stats. I also think you need a reliable test cases to
> cover issues like this.
Fixed. parallel_vacuum_process_one_index() now samples each index pass with
extvac_stats_start_idx() / extvac_stats_end_idx() and accumulates it via
extvac_accumulate_idx_report() into the index's DSM-resident totals. The
leader reports those
totals once per index in parallel_vacuum_end() and feeds them back
through idx_heap_total, so
the per-index buffer/WAL/timing usage is subtracted from the parent heap
instead of being charged to it.
For the reliable test, I added a TAP test
(src/test/modules/test_misc/t/016_vacuum_stats_parallel.pl).
It forces the parallel path and verifies it was actually taken by
checking the VACUUM (VERBOSE) output for a launched
parallel worker. It then vacuums two identical tables - one with
PARALLEL 2, one with PARALLEL 0 - and asserts that
the per-index statistics (tuples_deleted, pages_deleted, wal_records)
are identical between the two paths,
which is exactly the invariant that was broken before: the parallel path
now neither loses the index stats nor
charges them to the heap.
(machinery + parallel sampling and the parallel test in patch 0003, "WAL
metrics and the sampling machinery")
> 4) It seems that pgstat_vacuum_db_flush_cb() and
> pgstat_vacuum_relation_flush_cb() are unused as
> pgstat_report_vacuum_extstats() reports directly to shmem. What was
> your intention here?
Good catch! Fixed! pgstat_report_vacuum_extstats() now accumulates into
pending entries with
pgstat_prep_pending_entry() for both PGSTAT_KIND_VACUUM_RELATION and
PGSTAT_KIND_VACUUM_DB,
which pgstat_report_stat() then flushes through the registered
pgstat_vacuum_relation_flush_cb() /
pgstat_vacuum_db_flush_cb().
(relation pending in patch 0001; database pending in patch 0003)
> 5) Commit message of 0001 uses stale names
> rev_all_visible_pages/rev_all_frozen_pages counters for
> visible_page_marks_cleared` / `frozen_page_marks_cleared.
Fixed.
A few other changes in this version, beyond the five points above:
* I split the patch set so that each commit introduces one category of
statistics.
In particular, recently_dead_tuples now lives in the same commit as
missed_dead_tuples (patch 0003), where
it logically belongs - both are dead-tuple counters derived the same
way, so keeping them together makes
the series easier to review.
* I also changed the order in which the counters are added across the
commits. I'm still working out the right way
to verify the buffer statistics, timings and he frozen /
visibility-map counters.
Thanks again for the review.
--
-----------
Best regards,
Alena Rybakina,
Yandex Cloud
From 4a88c028674fdad4f5db26e8e8c0a608a7f2b797 Mon Sep 17 00:00:00 2001
From: Alena Rybakina <[email protected]>
Date: Mon, 30 Mar 2026 09:07:24 +0300
Subject: [PATCH 1/9] Track table VM stability.
Add visible_page_marks_cleared and frozen_page_marks_cleared 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 visible_page_marks_cleared 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 frozen_page_marks_cleared 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 08d5b82455..3467abf6d8 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 4fd470702a..f055ec3819 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 8f129baec9..b57128bb12 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 b2ca28f83b..92e1f60a08 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 6f9c9c72de..6409987d66 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 be157a5fbe..291b039859 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12693,4 +12693,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 dfa2e83763..7db36cf8ad 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 0000000000..994a8df56d
--- /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 15c33fad4c..b48758a211 100644
--- a/src/test/isolation/isolation_schedule
+++ b/src/test/isolation/isolation_schedule
@@ -127,3 +127,4 @@ test: matview-write-skew
test: lock-nowait
test: for-portion-of
test: ddl-dependency-locking
+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 0000000000..17c204e232
--- /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 a65a5bf0c4..096e4f763f 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 db60e288bea35ca4ae140be37466d7898e5d5920 Mon Sep 17 00:00:00 2001
From: Alena Rybakina <[email protected]>
Date: Tue, 16 Jun 2026 10:06:53 +0300
Subject: [PATCH 2/9] Extended vacuum statistics: core heap and tuple metrics
for tables and indexes
Expose the core per-table heap and tuple metrics of the last VACUUM through
the new pg_stat_vacuum_tables view and pg_stat_get_vacuum_tables() function,
with documentation and regression coverage:
pages_scanned heap pages examined (not skipped via the VM)
pages_removed heap pages by which storage was truncated
tuples_deleted dead tuples removed by the vacuum
tuples_frozen tuples frozen by the vacuum
The matching per-index core metrics are exposed at the same time through the
new pg_stat_vacuum_indexes view and pg_stat_get_vacuum_indexes() function:
pages_deleted index pages deleted (made reusable) by the vacuum
tuples_deleted index entries removed by the vacuum
This first commit also adds the infrastructure the rest of the series builds
on. Extended vacuum statistics are stored in the cumulative statistics system
under a dedicated stats kind, PGSTAT_KIND_VACUUM_RELATION, kept separate from
the regular relation statistics. They are collected during vacuum and
accumulated per relation, only while the track_vacuum_statistics GUC is
enabled. Indexes are tracked under the same stats kind, with their totals
reported once per index at the end of the vacuum.
---
doc/src/sgml/config.sgml | 20 ++
doc/src/sgml/system-views.sgml | 182 ++++++++++++++++++
src/backend/access/heap/vacuumlazy.c | 78 ++++++++
src/backend/catalog/heap.c | 1 +
src/backend/catalog/index.c | 1 +
src/backend/catalog/system_views.sql | 34 ++++
src/backend/commands/vacuumparallel.c | 27 +++
src/backend/utils/activity/Makefile | 1 +
src/backend/utils/activity/meson.build | 1 +
src/backend/utils/activity/pgstat.c | 15 +-
src/backend/utils/activity/pgstat_relation.c | 6 +
src/backend/utils/activity/pgstat_vacuum.c | 129 +++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 83 ++++++++
src/backend/utils/misc/guc_parameters.dat | 6 +
src/backend/utils/misc/postgresql.conf.sample | 1 +
src/include/catalog/pg_proc.dat | 19 ++
src/include/pgstat.h | 80 +++++++-
src/include/utils/pgstat_internal.h | 8 +
src/include/utils/pgstat_kind.h | 3 +-
src/test/regress/expected/rules.out | 24 +++
src/test/regress/expected/vacuum_stats.out | 114 +++++++++++
src/test/regress/parallel_schedule | 3 +
src/test/regress/sql/vacuum_stats.sql | 70 +++++++
23 files changed, 903 insertions(+), 3 deletions(-)
create mode 100644 src/backend/utils/activity/pgstat_vacuum.c
create mode 100644 src/test/regress/expected/vacuum_stats.out
create mode 100644 src/test/regress/sql/vacuum_stats.sql
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index fa566c9e55..57d7ecfd6f 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -9110,6 +9110,26 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
</listitem>
</varlistentry>
+ <varlistentry id="guc-track-vacuum-statistics" xreflabel="track_vacuum_statistics">
+ <term><varname>track_vacuum_statistics</varname> (<type>boolean</type>)
+ <indexterm>
+ <primary><varname>track_vacuum_statistics</varname> configuration parameter</primary>
+ </indexterm>
+ </term>
+ <listitem>
+ <para>
+ Enables collection of extended statistics about the work performed by
+ <command>VACUUM</command>, reported in the
+ <structname>pg_stat_vacuum_tables</structname>,
+ <structname>pg_stat_vacuum_indexes</structname> and
+ <structname>pg_stat_vacuum_database</structname> views.
+ This parameter is off by default.
+ Only superusers and users with the appropriate <literal>SET</literal>
+ privilege can change this setting.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry id="guc-track-functions" xreflabel="track_functions">
<term><varname>track_functions</varname> (<type>enum</type>)
<indexterm>
diff --git a/doc/src/sgml/system-views.sgml b/doc/src/sgml/system-views.sgml
index 2ebec6928d..19576018df 100644
--- a/doc/src/sgml/system-views.sgml
+++ b/doc/src/sgml/system-views.sgml
@@ -5782,4 +5782,186 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx
</table>
</sect1>
+ <sect1 id="view-pg-stat-vacuum-tables">
+ <title><structname>pg_stat_vacuum_tables</structname></title>
+
+ <indexterm zone="view-pg-stat-vacuum-tables">
+ <primary>pg_stat_vacuum_tables</primary>
+ </indexterm>
+
+ <para>
+ The <structname>pg_stat_vacuum_tables</structname> view will contain one row
+ for each table in the current database, showing extended statistics about
+ the activity of the most recent <command>VACUUM</command> on that table.
+ These statistics are accumulated only while
+ <varname>track_vacuum_statistics</varname> is enabled.
+ </para>
+
+ <table id="view-pg-stat-vacuum-tables-cols">
+ <title><structname>pg_stat_vacuum_tables</structname> Columns</title>
+ <tgroup cols="1">
+ <thead>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ Column Type
+ </para>
+ <para>
+ Description
+ </para></entry>
+ </row>
+ </thead>
+
+ <tbody>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>schemaname</structfield> <type>name</type>
+ </para>
+ <para>
+ Name of the schema that the 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 the table.
+ </para></entry>
+ </row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>relid</structfield> <type>oid</type>
+ </para>
+ <para>
+ OID of the table.
+ </para></entry>
+ </row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>pages_scanned</structfield> <type>bigint</type>
+ </para>
+ <para>
+ Number of heap pages examined by the vacuum (pages that were not skipped using the visibility map).
+ </para></entry>
+ </row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>pages_removed</structfield> <type>bigint</type>
+ </para>
+ <para>
+ Number of heap pages by which the table's storage was physically reduced (truncated) by the vacuum.
+ </para></entry>
+ </row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>tuples_deleted</structfield> <type>bigint</type>
+ </para>
+ <para>
+ Number of dead tuples removed by the vacuum.
+ </para></entry>
+ </row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>tuples_frozen</structfield> <type>bigint</type>
+ </para>
+ <para>
+ Number of tuples frozen by the vacuum.
+ </para></entry>
+ </row>
+ </tbody>
+ </tgroup>
+ </table>
+ </sect1>
+
+ <sect1 id="view-pg-stat-vacuum-indexes">
+ <title><structname>pg_stat_vacuum_indexes</structname></title>
+
+ <indexterm zone="view-pg-stat-vacuum-indexes">
+ <primary>pg_stat_vacuum_indexes</primary>
+ </indexterm>
+
+ <para>
+ The <structname>pg_stat_vacuum_indexes</structname> view will contain one row
+ for each index in the current database, showing extended statistics about
+ the activity of the most recent <command>VACUUM</command> on that index.
+ These statistics are accumulated only while
+ <varname>track_vacuum_statistics</varname> is enabled.
+ </para>
+
+ <table id="view-pg-stat-vacuum-indexes-cols">
+ <title><structname>pg_stat_vacuum_indexes</structname> Columns</title>
+ <tgroup cols="1">
+ <thead>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ Column Type
+ </para>
+ <para>
+ Description
+ </para></entry>
+ </row>
+ </thead>
+
+ <tbody>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>relid</structfield> <type>oid</type>
+ </para>
+ <para>
+ OID of the table the index is on.
+ </para></entry>
+ </row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>indexrelid</structfield> <type>oid</type>
+ </para>
+ <para>
+ OID of the index.
+ </para></entry>
+ </row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>schemaname</structfield> <type>name</type>
+ </para>
+ <para>
+ Name of the schema that the 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 the table.
+ </para></entry>
+ </row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>indexrelname</structfield> <type>name</type>
+ </para>
+ <para>
+ Name of the index.
+ </para></entry>
+ </row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>pages_deleted</structfield> <type>bigint</type>
+ </para>
+ <para>
+ Number of index pages deleted (made reusable) by the vacuum.
+ </para></entry>
+ </row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>tuples_deleted</structfield> <type>bigint</type>
+ </para>
+ <para>
+ Number of index entries removed by the vacuum.
+ </para></entry>
+ </row>
+ </tbody>
+ </tgroup>
+ </table>
+ </sect1>
+
</chapter>
diff --git a/src/backend/access/heap/vacuumlazy.c b/src/backend/access/heap/vacuumlazy.c
index 39395aed0d..ffcfa3d16a 100644
--- a/src/backend/access/heap/vacuumlazy.c
+++ b/src/backend/access/heap/vacuumlazy.c
@@ -283,6 +283,7 @@ typedef struct LVRelState
/* Error reporting state */
char *dbname;
char *relnamespace;
+ Oid reloid;
char *relname;
char *indname; /* Current index name */
BlockNumber blkno; /* used only for heap operations */
@@ -483,6 +484,8 @@ static void update_vacuum_error_info(LVRelState *vacrel,
OffsetNumber offnum);
static void restore_vacuum_error_info(LVRelState *vacrel,
const LVSavedErrInfo *saved_vacrel);
+static void accumulate_heap_vacuum_statistics(LVRelState *vacrel,
+ PgStat_VacuumRelationCounts *extVacStats);
@@ -609,6 +612,63 @@ heap_vacuum_eager_scan_setup(LVRelState *vacrel, const VacuumParams *params)
first_region_ratio;
}
+/*
+ * Fill the extended vacuum statistics report for a heap relation with the
+ * counters that are derived directly from the LVRelState. Buffer, WAL and
+ * timing counters require sampling resource usage around the vacuum and are
+ * gathered separately; they are not touched here.
+ */
+static void
+accumulate_heap_vacuum_statistics(LVRelState *vacrel,
+ PgStat_VacuumRelationCounts *extVacStats)
+{
+ if (!pgstat_track_vacuum_statistics)
+ return;
+
+ extVacStats->type = PGSTAT_EXTVAC_TABLE;
+ extVacStats->table.pages_scanned = vacrel->scanned_pages;
+ extVacStats->table.pages_removed = vacrel->removed_pages;
+ extVacStats->common.tuples_deleted = vacrel->tuples_deleted;
+ extVacStats->table.tuples_frozen = vacrel->tuples_frozen;
+}
+
+/*
+ * Report the per-index extended vacuum statistics, one report per index.
+ *
+ * The per-index counters (pages_deleted and the number of removed index
+ * entries) are derived directly from each index's final IndexBulkDeleteResult,
+ * which already holds the totals accumulated across all bulkdelete and cleanup
+ * passes -- so no per-pass sampling is needed here. Used by the non-parallel
+ * path only; the parallel path reports its DSM-resident results from
+ * parallel_vacuum_end().
+ */
+static void
+report_index_vacuum_extstats(LVRelState *vacrel)
+{
+ if (!pgstat_track_vacuum_statistics)
+ return;
+
+ for (int idx = 0; idx < vacrel->nindexes; idx++)
+ {
+ Relation indrel = vacrel->indrels[idx];
+ IndexBulkDeleteResult *istat = vacrel->indstats[idx];
+ PgStat_VacuumRelationCounts report;
+
+ /* Skip indexes that this vacuum did not process */
+ if (istat == NULL)
+ continue;
+
+ memset(&report, 0, sizeof(report));
+ report.type = PGSTAT_EXTVAC_INDEX;
+ report.common.tuples_deleted = istat->tuples_removed;
+ report.index.pages_deleted = istat->pages_deleted;
+
+ pgstat_report_vacuum_extstats(RelationGetRelid(indrel),
+ indrel->rd_rel->relisshared,
+ &report);
+ }
+}
+
/*
* heap_vacuum_rel() -- perform VACUUM for one heap relation
*
@@ -643,6 +703,10 @@ heap_vacuum_rel(Relation rel, const VacuumParams *params,
ErrorContextCallback errcallback;
char **indnames = NULL;
Size dead_items_max_bytes = 0;
+ PgStat_VacuumRelationCounts extVacReport;
+
+ /* Initialize the extended vacuum statistics report */
+ memset(&extVacReport, 0, sizeof(PgStat_VacuumRelationCounts));
verbose = (params->options & VACOPT_VERBOSE) != 0;
instrument = (verbose || (AmAutoVacuumWorkerProcess() &&
@@ -686,6 +750,7 @@ heap_vacuum_rel(Relation rel, const VacuumParams *params,
vacrel = palloc0_object(LVRelState);
vacrel->dbname = get_database_name(MyDatabaseId);
vacrel->relnamespace = get_namespace_name(RelationGetNamespace(rel));
+ vacrel->reloid = RelationGetRelid(rel);
vacrel->relname = pstrdup(RelationGetRelationName(rel));
vacrel->indname = NULL;
vacrel->phase = VACUUM_ERRCB_PHASE_UNKNOWN;
@@ -700,6 +765,7 @@ heap_vacuum_rel(Relation rel, const VacuumParams *params,
vac_open_indexes(vacrel->rel, RowExclusiveLock, &vacrel->nindexes,
&vacrel->indrels);
vacrel->bstrategy = bstrategy;
+
if (instrument && vacrel->nindexes > 0)
{
/* Copy index names used by instrumentation (not error reporting) */
@@ -985,6 +1051,9 @@ heap_vacuum_rel(Relation rel, const VacuumParams *params,
* soon in cases where the failsafe prevented significant amounts of heap
* vacuuming.
*/
+ accumulate_heap_vacuum_statistics(vacrel, &extVacReport);
+ pgstat_report_vacuum_extstats(vacrel->reloid, rel->rd_rel->relisshared,
+ &extVacReport);
pgstat_report_vacuum(rel,
Max(vacrel->new_live_tuples, 0),
vacrel->recently_dead_tuples +
@@ -1618,6 +1687,15 @@ lazy_scan_heap(LVRelState *vacrel)
/* Do final index cleanup (call each index's amvacuumcleanup routine) */
if (vacrel->nindexes > 0 && vacrel->do_index_cleanup)
lazy_cleanup_all_indexes(vacrel);
+
+ /*
+ * Report the per-index extended vacuum statistics accumulated over all
+ * bulkdelete and cleanup passes, exactly once per index. The parallel
+ * path reports its DSM-resident totals from parallel_vacuum_end() instead,
+ * so only do it here when index vacuuming ran in the leader.
+ */
+ if (vacrel->nindexes > 0 && !ParallelVacuumIsActive(vacrel))
+ report_index_vacuum_extstats(vacrel);
}
/*
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index 88087654de..709fe33e61 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -1903,6 +1903,7 @@ heap_drop_with_catalog(Oid relid)
/* ensure that stats are dropped if transaction commits */
pgstat_drop_relation(rel);
+ pgstat_vacuum_relation_delete_pending_cb(RelationGetRelid(rel));
/*
* Close relcache entry, but *keep* AccessExclusiveLock on the relation
diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c
index 9407c357f2..f986276677 100644
--- a/src/backend/catalog/index.c
+++ b/src/backend/catalog/index.c
@@ -2345,6 +2345,7 @@ index_drop(Oid indexId, bool concurrent, bool concurrent_lock_mode)
/* ensure that stats are dropped if transaction commits */
pgstat_drop_relation(userIndexRelation);
+ pgstat_vacuum_relation_delete_pending_cb(RelationGetRelid(userIndexRelation));
/*
* Close and flush the index's relcache entry, to ensure relcache doesn't
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index b57128bb12..04ca38e51c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -1559,3 +1559,37 @@ CREATE VIEW pg_aios AS
SELECT * FROM pg_get_aios();
REVOKE ALL ON pg_aios FROM PUBLIC;
GRANT SELECT ON pg_aios TO pg_read_all_stats;
+
+CREATE VIEW pg_stat_vacuum_tables AS
+ SELECT
+ N.nspname AS schemaname,
+ C.relname AS relname,
+ S.relid AS relid,
+
+ S.pages_scanned AS pages_scanned,
+ S.pages_removed AS pages_removed,
+ S.tuples_deleted AS tuples_deleted,
+ S.tuples_frozen AS tuples_frozen
+
+ FROM pg_class C JOIN
+ pg_namespace N ON N.oid = C.relnamespace,
+ LATERAL pg_stat_get_vacuum_tables(C.oid) S
+ WHERE C.relkind IN ('r', 't', 'm');
+
+CREATE VIEW pg_stat_vacuum_indexes AS
+ SELECT
+ C.oid AS relid,
+ I.oid AS indexrelid,
+ N.nspname AS schemaname,
+ C.relname AS relname,
+ I.relname AS indexrelname,
+
+ S.pages_deleted AS pages_deleted,
+ S.tuples_deleted AS tuples_deleted
+ FROM
+ pg_class C JOIN
+ pg_index X ON C.oid = X.indrelid JOIN
+ pg_class I ON I.oid = X.indexrelid
+ LEFT JOIN pg_namespace N ON (N.oid = C.relnamespace),
+ LATERAL pg_stat_get_vacuum_indexes(I.oid) S
+ WHERE C.relkind IN ('r', 't', 'm');
diff --git a/src/backend/commands/vacuumparallel.c b/src/backend/commands/vacuumparallel.c
index 41cefcfde5..7725c4ecc1 100644
--- a/src/backend/commands/vacuumparallel.c
+++ b/src/backend/commands/vacuumparallel.c
@@ -535,6 +535,33 @@ parallel_vacuum_end(ParallelVacuumState *pvs, IndexBulkDeleteResult **istats)
DestroyParallelContext(pvs->pcxt);
ExitParallelMode();
+ /*
+ * Report the per-index extended vacuum statistics, one report per index,
+ * derived directly from each index's final IndexBulkDeleteResult. The
+ * indexes are still open here (pvs->indrels is the leader's own array, not
+ * in the now-destroyed DSM).
+ */
+ if (pgstat_track_vacuum_statistics)
+ {
+ for (int i = 0; i < pvs->nindexes; i++)
+ {
+ Relation indrel = pvs->indrels[i];
+ PgStat_VacuumRelationCounts report;
+
+ if (istats[i] == NULL)
+ continue;
+
+ memset(&report, 0, sizeof(report));
+ report.type = PGSTAT_EXTVAC_INDEX;
+ report.common.tuples_deleted = istats[i]->tuples_removed;
+ report.index.pages_deleted = istats[i]->pages_deleted;
+
+ pgstat_report_vacuum_extstats(RelationGetRelid(indrel),
+ indrel->rd_rel->relisshared,
+ &report);
+ }
+ }
+
if (AmAutoVacuumWorkerProcess())
pv_shared_cost_params = NULL;
diff --git a/src/backend/utils/activity/Makefile b/src/backend/utils/activity/Makefile
index ca3ef89bf5..b7db9c034a 100644
--- a/src/backend/utils/activity/Makefile
+++ b/src/backend/utils/activity/Makefile
@@ -28,6 +28,7 @@ OBJS = \
pgstat_io.o \
pgstat_lock.o \
pgstat_relation.o \
+ pgstat_vacuum.o \
pgstat_replslot.o \
pgstat_shmem.o \
pgstat_slru.o \
diff --git a/src/backend/utils/activity/meson.build b/src/backend/utils/activity/meson.build
index 1aa7ece529..2a0b50d07d 100644
--- a/src/backend/utils/activity/meson.build
+++ b/src/backend/utils/activity/meson.build
@@ -17,6 +17,7 @@ backend_sources += files(
'pgstat_shmem.c',
'pgstat_slru.c',
'pgstat_subscription.c',
+ 'pgstat_vacuum.c',
'pgstat_wal.c',
'pgstat_xact.c',
)
diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index b67da88c7d..4e950672e2 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -204,7 +204,7 @@ static inline bool pgstat_is_kind_valid(PgStat_Kind kind);
bool pgstat_track_counts = false;
int pgstat_fetch_consistency = PGSTAT_FETCH_CONSISTENCY_CACHE;
-
+bool pgstat_track_vacuum_statistics = false;
/* ----------
* state shared with pgstat_*.c
@@ -500,6 +500,19 @@ static const PgStat_KindInfo pgstat_kind_builtin_infos[PGSTAT_KIND_BUILTIN_SIZE]
.reset_all_cb = pgstat_wal_reset_all_cb,
.snapshot_cb = pgstat_wal_snapshot_cb,
},
+ [PGSTAT_KIND_VACUUM_RELATION] = {
+ .name = "vacuum statistics",
+
+ .fixed_amount = false,
+ .write_to_file = true,
+
+ .shared_size = sizeof(PgStatShared_VacuumRelation),
+ .shared_data_off = offsetof(PgStatShared_VacuumRelation, stats),
+ .shared_data_len = sizeof(((PgStatShared_VacuumRelation *) 0)->stats),
+ .pending_size = sizeof(PgStat_RelationVacuumPending),
+
+ .flush_pending_cb = pgstat_vacuum_relation_flush_cb
+ },
};
/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index 92e1f60a08..141e2af607 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -904,6 +904,12 @@ pgstat_relation_flush_cb(PgStat_EntryRef *entry_ref, bool nowait)
return true;
}
+void
+pgstat_vacuum_relation_delete_pending_cb(Oid relid)
+{
+ pgstat_drop_transactional(PGSTAT_KIND_VACUUM_RELATION, relid, InvalidOid);
+}
+
void
pgstat_relation_delete_pending_cb(PgStat_EntryRef *entry_ref)
{
diff --git a/src/backend/utils/activity/pgstat_vacuum.c b/src/backend/utils/activity/pgstat_vacuum.c
new file mode 100644
index 0000000000..ac6baee516
--- /dev/null
+++ b/src/backend/utils/activity/pgstat_vacuum.c
@@ -0,0 +1,129 @@
+/* -------------------------------------------------------------------------
+ *
+ * pgstat_vacuum.c
+ * Implementation of extended vacuum statistics.
+ *
+ * This file contains the implementation of extended vacuum statistics. It is
+ * kept separate from pgstat_relation.c and pgstat_database.c to reduce the
+ * memory footprint of the regular relation and database statistics: vacuum
+ * metrics require significantly more space per relation, so they live in their
+ * own PGSTAT_KIND_VACUUM_RELATION stats kind.
+ *
+ * Copyright (c) 2001-2026, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * src/backend/utils/activity/pgstat_vacuum.c
+ * -------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "pgstat.h"
+#include "utils/memutils.h"
+#include "utils/pgstat_internal.h"
+
+#define ACCUMULATE_SUBFIELD(substruct, field) (dst->substruct.field += src->substruct.field)
+
+/*
+ * Accumulate the per-table extended vacuum counters collected so far.
+ *
+ * Only the counters derived directly from the vacuum's own bookkeeping are
+ * summed here. The buffer, WAL and timing counters (and the per-index
+ * counters) are accumulated by additional code added together with the
+ * helpers that gather them.
+ */
+static void
+pgstat_accumulate_extvac_stats_relations(PgStat_VacuumRelationCounts *dst,
+ PgStat_VacuumRelationCounts *src)
+{
+ if (!pgstat_track_vacuum_statistics)
+ return;
+
+ if (dst->type == PGSTAT_EXTVAC_INVALID)
+ dst->type = src->type;
+
+ Assert(src->type != PGSTAT_EXTVAC_INVALID &&
+ src->type != PGSTAT_EXTVAC_DB &&
+ src->type == dst->type);
+
+ ACCUMULATE_SUBFIELD(common, tuples_deleted);
+
+ if (dst->type == PGSTAT_EXTVAC_TABLE)
+ {
+ ACCUMULATE_SUBFIELD(table, pages_scanned);
+ ACCUMULATE_SUBFIELD(table, pages_removed);
+ ACCUMULATE_SUBFIELD(table, tuples_frozen);
+ }
+ else if (dst->type == PGSTAT_EXTVAC_INDEX)
+ {
+ ACCUMULATE_SUBFIELD(index, pages_deleted);
+ }
+}
+
+/*
+ * Report that the relation was just vacuumed, accumulating its extended
+ * statistics into the per-relation entry.
+ */
+void
+pgstat_report_vacuum_extstats(Oid tableoid, bool shared,
+ PgStat_VacuumRelationCounts *params)
+{
+ PgStat_EntryRef *entry_ref;
+ PgStat_RelationVacuumPending *relpending;
+ Oid dboid = (shared ? InvalidOid : MyDatabaseId);
+
+ if (!pgstat_track_vacuum_statistics)
+ return;
+
+ /*
+ * Accumulate into a pending entry instead of taking a shared-stats lock
+ * here; pgstat_report_stat() flushes it to shared memory through the
+ * registered flush callback once the vacuum's transaction completes.
+ */
+ entry_ref = pgstat_prep_pending_entry(PGSTAT_KIND_VACUUM_RELATION,
+ dboid, tableoid, NULL);
+ relpending = (PgStat_RelationVacuumPending *) entry_ref->pending;
+ pgstat_accumulate_extvac_stats_relations(&relpending->counts, params);
+}
+
+/*
+ * Flush out pending per-relation extended vacuum stats for the entry.
+ *
+ * If nowait is true, this function returns false if the lock could not be
+ * acquired immediately, otherwise true is returned.
+ */
+bool
+pgstat_vacuum_relation_flush_cb(PgStat_EntryRef *entry_ref, bool nowait)
+{
+ PgStatShared_VacuumRelation *shtabstats;
+ PgStat_RelationVacuumPending *pendingent;
+
+ pendingent = (PgStat_RelationVacuumPending *) entry_ref->pending;
+ shtabstats = (PgStatShared_VacuumRelation *) entry_ref->shared_stats;
+
+ /* Ignore entries that didn't accumulate any actual counts. */
+ if (pg_memory_is_all_zeros(pendingent,
+ sizeof(PgStat_RelationVacuumPending)))
+ return true;
+
+ if (!pgstat_lock_entry(entry_ref, nowait))
+ return false;
+
+ pgstat_accumulate_extvac_stats_relations(&shtabstats->stats,
+ &pendingent->counts);
+
+ pgstat_unlock_entry(entry_ref);
+
+ return true;
+}
+
+/*
+ * Support function for the SQL-callable pgstat* functions. Returns the vacuum
+ * collected statistics for one relation or NULL.
+ */
+PgStat_VacuumRelationCounts *
+pgstat_fetch_stat_vacuum_tabentry(Oid relid, Oid dbid)
+{
+ return (PgStat_VacuumRelationCounts *)
+ pgstat_fetch_entry(PGSTAT_KIND_VACUUM_RELATION, dbid, relid, NULL);
+}
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 6409987d66..e72f76180e 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -2367,3 +2367,86 @@ pg_stat_have_stats(PG_FUNCTION_ARGS)
PG_RETURN_BOOL(pgstat_have_entry(kind, dboid, objid));
}
+
+/*
+ * Get the extended vacuum statistics for a heap table.
+ */
+Datum
+pg_stat_get_vacuum_tables(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_GET_VACUUM_TABLES_STATS_COLS 5
+
+ Oid relid = PG_GETARG_OID(0);
+ PgStat_VacuumRelationCounts *extvacuum;
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_GET_VACUUM_TABLES_STATS_COLS] = {0};
+ bool nulls[PG_STAT_GET_VACUUM_TABLES_STATS_COLS] = {0};
+ int i = 0;
+
+ /* Build a tuple descriptor for our result type */
+ if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
+ elog(ERROR, "return type must be a row type");
+
+ extvacuum = pgstat_fetch_stat_vacuum_tabentry(relid, MyDatabaseId);
+ if (!extvacuum)
+ {
+ /* Retry as a shared relation before giving up. */
+ extvacuum = pgstat_fetch_stat_vacuum_tabentry(relid, InvalidOid);
+ if (!extvacuum)
+ {
+ InitMaterializedSRF(fcinfo, 0);
+ PG_RETURN_VOID();
+ }
+ }
+
+ values[i++] = ObjectIdGetDatum(relid);
+ values[i++] = Int64GetDatum(extvacuum->table.pages_scanned);
+ values[i++] = Int64GetDatum(extvacuum->table.pages_removed);
+ values[i++] = Int64GetDatum(extvacuum->common.tuples_deleted);
+ values[i++] = Int64GetDatum(extvacuum->table.tuples_frozen);
+
+ Assert(i == PG_STAT_GET_VACUUM_TABLES_STATS_COLS);
+
+ /* Returns the record as Datum */
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
+/*
+ * Get the extended vacuum statistics for an index.
+ */
+Datum
+pg_stat_get_vacuum_indexes(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_GET_VACUUM_INDEX_STATS_COLS 3
+
+ Oid relid = PG_GETARG_OID(0);
+ PgStat_VacuumRelationCounts *extvacuum;
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_GET_VACUUM_INDEX_STATS_COLS] = {0};
+ bool nulls[PG_STAT_GET_VACUUM_INDEX_STATS_COLS] = {0};
+ int i = 0;
+
+ if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
+ elog(ERROR, "return type must be a row type");
+
+ extvacuum = pgstat_fetch_stat_vacuum_tabentry(relid, MyDatabaseId);
+ if (!extvacuum)
+ {
+ extvacuum = pgstat_fetch_stat_vacuum_tabentry(relid, InvalidOid);
+ if (!extvacuum)
+ {
+ InitMaterializedSRF(fcinfo, 0);
+ PG_RETURN_VOID();
+ }
+ }
+
+ values[i++] = ObjectIdGetDatum(relid);
+
+ values[i++] = Int64GetDatum(extvacuum->index.pages_deleted);
+ values[i++] = Int64GetDatum(extvacuum->common.tuples_deleted);
+
+ Assert(i == PG_STAT_GET_VACUUM_INDEX_STATS_COLS);
+
+ /* Returns the record as Datum */
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
diff --git a/src/backend/utils/misc/guc_parameters.dat b/src/backend/utils/misc/guc_parameters.dat
index afaa058b04..bb8eb41394 100644
--- a/src/backend/utils/misc/guc_parameters.dat
+++ b/src/backend/utils/misc/guc_parameters.dat
@@ -3220,6 +3220,12 @@
boot_val => 'false',
},
+{ name => 'track_vacuum_statistics', type => 'bool', context => 'PGC_SUSET', group => 'STATS_CUMULATIVE',
+ short_desc => 'Collects vacuum statistics for vacuum activity.',
+ variable => 'pgstat_track_vacuum_statistics',
+ boot_val => 'false',
+},
+
{ name => 'track_wal_io_timing', type => 'bool', context => 'PGC_SUSET', group => 'STATS_CUMULATIVE',
short_desc => 'Collects timing statistics for WAL I/O activity.',
variable => 'track_wal_io_timing',
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index ac38cddaaf..4cf28f0f2e 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -702,6 +702,7 @@
#track_wal_io_timing = off
#track_functions = none # none, pl, all
#stats_fetch_consistency = cache # cache, none, snapshot
+#track_vacuum_statistics = off
# - Monitoring -
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 291b039859..9616a01466 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12638,6 +12638,16 @@
proargnames => '{pid,io_id,io_generation,state,operation,off,length,target,handle_data_len,raw_result,result,target_desc,f_sync,f_localmem,f_buffered}',
prosrc => 'pg_get_aios', proacl => '{POSTGRES=X,pg_read_all_stats=X}' },
+{ oid => '8001',
+ descr => 'pg_stat_get_vacuum_tables returns vacuum stats values for table',
+ proname => 'pg_stat_get_vacuum_tables', prorows => 1000, provolatile => 's', prorettype => 'record', proisstrict => 'f',
+ proretset => 't',
+ proargtypes => 'oid',
+ proallargtypes => '{oid,oid,int8,int8,int8,int8}',
+ proargmodes => '{i,o,o,o,o,o}',
+ proargnames => '{reloid,relid,pages_scanned,pages_removed,tuples_deleted,tuples_frozen}',
+ prosrc => 'pg_stat_get_vacuum_tables' }
+
# oid8 related functions
{ oid => '6436', descr => 'convert oid to oid8',
proname => 'oid8', prorettype => 'oid8', proargtypes => 'oid',
@@ -12703,4 +12713,13 @@
proname => 'pg_stat_get_frozen_page_marks_cleared', provolatile => 's',
proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
prosrc => 'pg_stat_get_frozen_page_marks_cleared' },
+{ oid => '8004',
+ descr => 'pg_stat_get_vacuum_indexes returns vacuum stats values for index',
+ proname => 'pg_stat_get_vacuum_indexes', prorows => 1000, provolatile => 's', prorettype => 'record', proisstrict => 'f',
+ proretset => 't',
+ proargtypes => 'oid',
+ proallargtypes => '{oid,oid,int8,int8}',
+ proargmodes => '{i,o,o,o}',
+ proargnames => '{reloid,relid,pages_deleted,tuples_deleted}',
+ prosrc => 'pg_stat_get_vacuum_indexes' },
]
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 7db36cf8ad..568624de5b 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -118,6 +118,15 @@ typedef struct PgStat_BackendSubEntry
PgStat_Counter conflict_count[CONFLICT_NUM_TYPES];
} PgStat_BackendSubEntry;
+/* Type of ExtVacReport */
+typedef enum ExtVacReportType
+{
+ PGSTAT_EXTVAC_INVALID = 0,
+ PGSTAT_EXTVAC_TABLE = 1,
+ PGSTAT_EXTVAC_INDEX = 2,
+ PGSTAT_EXTVAC_DB = 3,
+} ExtVacReportType;
+
/* ----------
* PgStat_TableCounts The actual per-table counts kept by a backend
*
@@ -164,6 +173,63 @@ typedef struct PgStat_TableCounts
PgStat_Counter frozen_page_marks_cleared;
} PgStat_TableCounts;
+typedef struct PgStat_CommonCounts
+{
+ /* tuples */
+ int64 tuples_deleted;
+} PgStat_CommonCounts;
+
+/* ----------
+ *
+ * PgStat_VacuumRelationCounts
+ *
+ * Additional statistics of vacuum processing over a relation. Counters that
+ * require sampling buffer/WAL/timing usage, and the per-index counters, are
+ * added to the common and per-type members later, together with the code that
+ * gathers them.
+ * ----------
+ */
+typedef struct PgStat_VacuumRelationCounts
+{
+ PgStat_CommonCounts common;
+
+ ExtVacReportType type; /* heap, index, etc. */
+
+ /* ----------
+ *
+ * There are separate metrics of statistic for tables and indexes,
+ * which collect during vacuum.
+ * The union operator allows to combine these statistics
+ * so that each metric is assigned to a specific class of collected statistics.
+ * Such a combined structure was called per_type_stats.
+ * The name of the structure itself is not used anywhere,
+ * it exists only for understanding the code.
+ * ----------
+ */
+ union
+ {
+ struct
+ {
+ int64 tuples_frozen; /* tuples frozen up by vacuum */
+ int64 pages_scanned; /* heap pages examined (not skipped by
+ * VM) */
+ int64 pages_removed; /* heap pages removed by vacuum
+ * "truncation" */
+ } table;
+ struct
+ {
+ int64 pages_deleted; /* number of pages deleted by vacuum */
+ } index;
+ } /* per_type_stats */ ;
+} PgStat_VacuumRelationCounts;
+
+typedef struct PgStat_VacuumRelationStatus
+{
+ Oid id; /* table's OID */
+ bool shared; /* is it a shared catalog? */
+ PgStat_VacuumRelationCounts counts; /* event counts to be sent */
+} PgStat_VacuumRelationStatus;
+
/* ----------
* PgStat_TableStatus Per-table status within a backend
*
@@ -188,6 +254,12 @@ typedef struct PgStat_TableStatus
Relation relation; /* rel that is using this entry */
} PgStat_TableStatus;
+typedef struct PgStat_RelationVacuumPending
+{
+ Oid id; /* table's OID */
+ PgStat_VacuumRelationCounts counts; /* event counts to be sent */
+} PgStat_RelationVacuumPending;
+
/* ----------
* PgStat_TableXactStatus Per-table, per-subtransaction status
* ----------
@@ -838,6 +910,12 @@ extern int pgstat_get_transactional_drops(bool isCommit, struct xl_xact_stats_it
extern void pgstat_execute_transactional_drops(int ndrops, struct xl_xact_stats_item *items, bool is_redo);
+extern void pgstat_vacuum_relation_delete_pending_cb(Oid relid);
+extern void
+ pgstat_report_vacuum_extstats(Oid tableoid, bool shared,
+ PgStat_VacuumRelationCounts * params);
+extern PgStat_VacuumRelationCounts * pgstat_fetch_stat_vacuum_tabentry(Oid relid, Oid dbid);
+
/*
* Functions in pgstat_wal.c
*/
@@ -854,7 +932,7 @@ extern PgStat_WalStats *pgstat_fetch_stat_wal(void);
extern PGDLLIMPORT bool pgstat_track_counts;
extern PGDLLIMPORT int pgstat_track_functions;
extern PGDLLIMPORT int pgstat_fetch_consistency;
-
+extern PGDLLIMPORT bool pgstat_track_vacuum_statistics;
/*
* Variables in pgstat_bgwriter.c
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index fe463faaf6..46a127af2b 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -507,6 +507,12 @@ typedef struct PgStatShared_Relation
PgStat_StatTabEntry stats;
} PgStatShared_Relation;
+typedef struct PgStatShared_VacuumRelation
+{
+ PgStatShared_Common header;
+ PgStat_VacuumRelationCounts stats;
+} PgStatShared_VacuumRelation;
+
typedef struct PgStatShared_Function
{
PgStatShared_Common header;
@@ -689,6 +695,8 @@ extern void *pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid,
bool *may_free);
extern void pgstat_snapshot_fixed(PgStat_Kind kind);
+extern bool pgstat_vacuum_relation_flush_cb(PgStat_EntryRef *entry_ref, bool nowait);
+
/*
* Functions in pgstat_archiver.c
diff --git a/src/include/utils/pgstat_kind.h b/src/include/utils/pgstat_kind.h
index 2d78a02968..f92149066c 100644
--- a/src/include/utils/pgstat_kind.h
+++ b/src/include/utils/pgstat_kind.h
@@ -39,9 +39,10 @@
#define PGSTAT_KIND_LOCK 11
#define PGSTAT_KIND_SLRU 12
#define PGSTAT_KIND_WAL 13
+#define PGSTAT_KIND_VACUUM_RELATION 14
#define PGSTAT_KIND_BUILTIN_MIN PGSTAT_KIND_DATABASE
-#define PGSTAT_KIND_BUILTIN_MAX PGSTAT_KIND_WAL
+#define PGSTAT_KIND_BUILTIN_MAX PGSTAT_KIND_VACUUM_RELATION
#define PGSTAT_KIND_BUILTIN_SIZE (PGSTAT_KIND_BUILTIN_MAX + 1)
/* Custom stats kinds */
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 096e4f763f..8f779a92ff 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -2421,6 +2421,30 @@ pg_stat_user_tables| SELECT relid,
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_vacuum_indexes| SELECT c.oid AS relid,
+ i.oid AS indexrelid,
+ n.nspname AS schemaname,
+ c.relname,
+ i.relname AS indexrelname,
+ s.pages_deleted,
+ s.tuples_deleted
+ FROM (((pg_class c
+ JOIN pg_index x ON ((c.oid = x.indrelid)))
+ JOIN pg_class i ON ((i.oid = x.indexrelid)))
+ LEFT JOIN pg_namespace n ON ((n.oid = c.relnamespace))),
+ LATERAL pg_stat_get_vacuum_indexes(i.oid) s(relid, pages_deleted, tuples_deleted)
+ WHERE (c.relkind = ANY (ARRAY['r'::"char", 't'::"char", 'm'::"char"]));
+pg_stat_vacuum_tables| SELECT n.nspname AS schemaname,
+ c.relname,
+ s.relid,
+ s.pages_scanned,
+ s.pages_removed,
+ s.tuples_deleted,
+ s.tuples_frozen
+ FROM (pg_class c
+ JOIN pg_namespace n ON ((n.oid = c.relnamespace))),
+ LATERAL pg_stat_get_vacuum_tables(c.oid) s(relid, pages_scanned, pages_removed, tuples_deleted, tuples_frozen)
+ WHERE (c.relkind = ANY (ARRAY['r'::"char", 't'::"char", 'm'::"char"]));
pg_stat_wal| SELECT wal_records,
wal_fpi,
wal_bytes,
diff --git a/src/test/regress/expected/vacuum_stats.out b/src/test/regress/expected/vacuum_stats.out
new file mode 100644
index 0000000000..cafaa9bdd9
--- /dev/null
+++ b/src/test/regress/expected/vacuum_stats.out
@@ -0,0 +1,114 @@
+--
+-- Extended vacuum statistics views (pg_stat_vacuum_tables, _indexes, _database)
+--
+SET track_vacuum_statistics = on;
+CREATE TABLE vacstat_t (id int PRIMARY KEY, v text)
+ WITH (autovacuum_enabled = off);
+INSERT INTO vacstat_t SELECT g, repeat('x', 20) FROM generate_series(1, 1000) g;
+DELETE FROM vacstat_t WHERE id % 2 = 0;
+VACUUM vacstat_t;
+SELECT pg_stat_force_next_flush();
+ pg_stat_force_next_flush
+--------------------------
+
+(1 row)
+
+-- core heap-page and tuple metrics. This VACUUM runs without concurrent
+-- activity: the surviving tuples are too fresh to be frozen (tuples_frozen = 0)
+-- and the interleaved deletions leave no trailing empty pages to truncate
+-- (pages_removed = 0).
+SELECT pages_scanned > 0 AS pages_scanned,
+ pages_removed = 0 AS pages_removed,
+ tuples_deleted = 500 AS tuples_deleted,
+ tuples_frozen = 0 AS tuples_frozen
+ FROM pg_stat_vacuum_tables WHERE relname = 'vacstat_t';
+ pages_scanned | pages_removed | tuples_deleted | tuples_frozen
+---------------+---------------+----------------+---------------
+ t | t | t | t
+(1 row)
+
+-- pages_removed path: deleting every tuple lets VACUUM truncate the now-empty
+-- trailing heap pages, so pages_removed advances.
+CREATE TABLE vacstat_trunc (id int)
+ WITH (autovacuum_enabled = off);
+INSERT INTO vacstat_trunc SELECT generate_series(1, 10000);
+DELETE FROM vacstat_trunc;
+VACUUM vacstat_trunc;
+SELECT pg_stat_force_next_flush();
+ pg_stat_force_next_flush
+--------------------------
+
+(1 row)
+
+SELECT pages_removed > 0 AS pages_removed,
+ tuples_deleted = 10000 AS tuples_deleted
+ FROM pg_stat_vacuum_tables WHERE relname = 'vacstat_trunc';
+ pages_removed | tuples_deleted
+---------------+----------------
+ t | t
+(1 row)
+
+DROP TABLE vacstat_trunc;
+-- tuples_frozen path: an aggressive VACUUM (FREEZE) freezes all live tuples,
+-- so tuples_frozen advances.
+CREATE TABLE vacstat_freeze (x int)
+ WITH (autovacuum_enabled = off);
+INSERT INTO vacstat_freeze SELECT generate_series(1, 1000);
+VACUUM (FREEZE) vacstat_freeze;
+SELECT pg_stat_force_next_flush();
+ pg_stat_force_next_flush
+--------------------------
+
+(1 row)
+
+SELECT tuples_frozen > 0 AS tuples_frozen
+ FROM pg_stat_vacuum_tables WHERE relname = 'vacstat_freeze';
+ tuples_frozen
+---------------
+ t
+(1 row)
+
+DROP TABLE vacstat_freeze;
+-- per-index view: the primary key index is processed by the same VACUUM.
+-- No btree leaf empties out (interleaved deletions), so pages_deleted = 0,
+-- while every index entry for a removed heap tuple is deleted.
+SELECT indexrelname,
+ pages_deleted = 0 AS pages_deleted,
+ tuples_deleted = 500 AS tuples_deleted
+ FROM pg_stat_vacuum_indexes WHERE relname = 'vacstat_t' ORDER BY indexrelname;
+ indexrelname | pages_deleted | tuples_deleted
+----------------+---------------+----------------
+ vacstat_t_pkey | t | t
+(1 row)
+
+-- index page-deletion path: deleting a contiguous key range empties whole
+-- btree leaf pages, which VACUUM then deletes (pages_deleted > 0), and every
+-- removed index entry is counted (tuples_deleted).
+CREATE TABLE vacstat_idxdel (id int PRIMARY KEY, v text)
+ WITH (autovacuum_enabled = off);
+INSERT INTO vacstat_idxdel SELECT g, repeat('x', 20) FROM generate_series(1, 10000) g;
+VACUUM vacstat_idxdel;
+SELECT pg_stat_force_next_flush();
+ pg_stat_force_next_flush
+--------------------------
+
+(1 row)
+
+DELETE FROM vacstat_idxdel WHERE id <= 9000;
+VACUUM vacstat_idxdel;
+SELECT pg_stat_force_next_flush();
+ pg_stat_force_next_flush
+--------------------------
+
+(1 row)
+
+SELECT indexrelname,
+ pages_deleted > 0 AS pages_deleted,
+ tuples_deleted = 9000 AS tuples_deleted
+ FROM pg_stat_vacuum_indexes WHERE relname = 'vacstat_idxdel' ORDER BY indexrelname;
+ indexrelname | pages_deleted | tuples_deleted
+---------------------+---------------+----------------
+ vacstat_idxdel_pkey | t | t
+(1 row)
+
+DROP TABLE vacstat_idxdel;
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 8fa0a6c47f..3c73207a9f 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -91,6 +91,9 @@ test: select_parallel
test: write_parallel
test: vacuum_parallel
+# extended vacuum statistics views
+test: vacuum_stats
+
# Run this alone, because concurrent DROP TABLE would make non-superuser
# "ANALYZE;" fail with "relation with OID $n does not exist".
test: maintain_every
diff --git a/src/test/regress/sql/vacuum_stats.sql b/src/test/regress/sql/vacuum_stats.sql
new file mode 100644
index 0000000000..6061f433a3
--- /dev/null
+++ b/src/test/regress/sql/vacuum_stats.sql
@@ -0,0 +1,70 @@
+--
+-- Extended vacuum statistics views (pg_stat_vacuum_tables, _indexes, _database)
+--
+SET track_vacuum_statistics = on;
+
+CREATE TABLE vacstat_t (id int PRIMARY KEY, v text)
+ WITH (autovacuum_enabled = off);
+INSERT INTO vacstat_t SELECT g, repeat('x', 20) FROM generate_series(1, 1000) g;
+DELETE FROM vacstat_t WHERE id % 2 = 0;
+VACUUM vacstat_t;
+SELECT pg_stat_force_next_flush();
+
+-- core heap-page and tuple metrics. This VACUUM runs without concurrent
+-- activity: the surviving tuples are too fresh to be frozen (tuples_frozen = 0)
+-- and the interleaved deletions leave no trailing empty pages to truncate
+-- (pages_removed = 0).
+SELECT pages_scanned > 0 AS pages_scanned,
+ pages_removed = 0 AS pages_removed,
+ tuples_deleted = 500 AS tuples_deleted,
+ tuples_frozen = 0 AS tuples_frozen
+ FROM pg_stat_vacuum_tables WHERE relname = 'vacstat_t';
+
+-- pages_removed path: deleting every tuple lets VACUUM truncate the now-empty
+-- trailing heap pages, so pages_removed advances.
+CREATE TABLE vacstat_trunc (id int)
+ WITH (autovacuum_enabled = off);
+INSERT INTO vacstat_trunc SELECT generate_series(1, 10000);
+DELETE FROM vacstat_trunc;
+VACUUM vacstat_trunc;
+SELECT pg_stat_force_next_flush();
+SELECT pages_removed > 0 AS pages_removed,
+ tuples_deleted = 10000 AS tuples_deleted
+ FROM pg_stat_vacuum_tables WHERE relname = 'vacstat_trunc';
+DROP TABLE vacstat_trunc;
+
+-- tuples_frozen path: an aggressive VACUUM (FREEZE) freezes all live tuples,
+-- so tuples_frozen advances.
+CREATE TABLE vacstat_freeze (x int)
+ WITH (autovacuum_enabled = off);
+INSERT INTO vacstat_freeze SELECT generate_series(1, 1000);
+VACUUM (FREEZE) vacstat_freeze;
+SELECT pg_stat_force_next_flush();
+SELECT tuples_frozen > 0 AS tuples_frozen
+ FROM pg_stat_vacuum_tables WHERE relname = 'vacstat_freeze';
+DROP TABLE vacstat_freeze;
+
+-- per-index view: the primary key index is processed by the same VACUUM.
+-- No btree leaf empties out (interleaved deletions), so pages_deleted = 0,
+-- while every index entry for a removed heap tuple is deleted.
+SELECT indexrelname,
+ pages_deleted = 0 AS pages_deleted,
+ tuples_deleted = 500 AS tuples_deleted
+ FROM pg_stat_vacuum_indexes WHERE relname = 'vacstat_t' ORDER BY indexrelname;
+
+-- index page-deletion path: deleting a contiguous key range empties whole
+-- btree leaf pages, which VACUUM then deletes (pages_deleted > 0), and every
+-- removed index entry is counted (tuples_deleted).
+CREATE TABLE vacstat_idxdel (id int PRIMARY KEY, v text)
+ WITH (autovacuum_enabled = off);
+INSERT INTO vacstat_idxdel SELECT g, repeat('x', 20) FROM generate_series(1, 10000) g;
+VACUUM vacstat_idxdel;
+SELECT pg_stat_force_next_flush();
+DELETE FROM vacstat_idxdel WHERE id <= 9000;
+VACUUM vacstat_idxdel;
+SELECT pg_stat_force_next_flush();
+SELECT indexrelname,
+ pages_deleted > 0 AS pages_deleted,
+ tuples_deleted = 9000 AS tuples_deleted
+ FROM pg_stat_vacuum_indexes WHERE relname = 'vacstat_idxdel' ORDER BY indexrelname;
+DROP TABLE vacstat_idxdel;
--
2.39.5 (Apple Git-154)
From ffb8a24d9772ac5bc408962750280f9d3886990f Mon Sep 17 00:00:00 2001
From: Alena Rybakina <[email protected]>
Date: Tue, 16 Jun 2026 10:41:24 +0300
Subject: [PATCH 3/9] Extended vacuum statistics: recently-dead and missed dead
tuples for tables
Expose the counters for dead tuples that VACUUM could not remove, with
documentation and regression coverage:
recently_dead_tuples dead tuples still visible to some transaction and
therefore not yet removable
missed_dead_tuples dead tuples skipped because their heap page could not
be cleanup-locked
missed_dead_pages heap pages that contained such skipped dead tuples
These non-zero paths require concurrent activity that an ordinary regression
test cannot create deterministically, so they are covered by a dedicated TAP
test, src/test/modules/test_misc/t/014_vacuum_stats.pl: a held REPEATABLE READ
snapshot keeps recently deleted tuples visible (recently_dead_tuples), and a
concurrently pinned heap page prevents cleanup (missed_dead_tuples /
missed_dead_pages).
---
doc/src/sgml/system-views.sgml | 24 ++++++
src/backend/access/heap/vacuumlazy.c | 3 +
src/backend/catalog/system_views.sql | 5 +-
src/backend/utils/activity/pgstat_vacuum.c | 3 +
src/backend/utils/adt/pgstatfuncs.c | 5 +-
src/include/catalog/pg_proc.dat | 6 +-
src/include/pgstat.h | 7 ++
src/test/modules/test_misc/meson.build | 1 +
.../modules/test_misc/t/014_vacuum_stats.pl | 84 +++++++++++++++++++
src/test/regress/expected/rules.out | 7 +-
src/test/regress/expected/vacuum_stats.out | 15 ++++
src/test/regress/sql/vacuum_stats.sql | 11 +++
12 files changed, 164 insertions(+), 7 deletions(-)
create mode 100644 src/test/modules/test_misc/t/014_vacuum_stats.pl
diff --git a/doc/src/sgml/system-views.sgml b/doc/src/sgml/system-views.sgml
index 19576018df..b96c653929 100644
--- a/doc/src/sgml/system-views.sgml
+++ b/doc/src/sgml/system-views.sgml
@@ -5868,6 +5868,30 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx
Number of tuples frozen by the vacuum.
</para></entry>
</row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>recently_dead_tuples</structfield> <type>bigint</type>
+ </para>
+ <para>
+ Number of dead tuples that are still visible to some transaction and therefore could not yet be removed.
+ </para></entry>
+ </row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>missed_dead_pages</structfield> <type>bigint</type>
+ </para>
+ <para>
+ Number of heap pages containing dead tuples that the vacuum could not remove because it failed to acquire a cleanup lock.
+ </para></entry>
+ </row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>missed_dead_tuples</structfield> <type>bigint</type>
+ </para>
+ <para>
+ Number of dead tuples that the vacuum could not remove because it failed to acquire a cleanup lock on their page.
+ </para></entry>
+ </row>
</tbody>
</tgroup>
</table>
diff --git a/src/backend/access/heap/vacuumlazy.c b/src/backend/access/heap/vacuumlazy.c
index ffcfa3d16a..7abc83dbfd 100644
--- a/src/backend/access/heap/vacuumlazy.c
+++ b/src/backend/access/heap/vacuumlazy.c
@@ -630,6 +630,9 @@ accumulate_heap_vacuum_statistics(LVRelState *vacrel,
extVacStats->table.pages_removed = vacrel->removed_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_pages = vacrel->missed_dead_pages;
+ extVacStats->table.missed_dead_tuples = vacrel->missed_dead_tuples;
}
/*
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index 04ca38e51c..6f697ab390 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -1569,7 +1569,10 @@ CREATE VIEW pg_stat_vacuum_tables AS
S.pages_scanned AS pages_scanned,
S.pages_removed AS pages_removed,
S.tuples_deleted AS tuples_deleted,
- S.tuples_frozen AS tuples_frozen
+ S.tuples_frozen AS tuples_frozen,
+ S.recently_dead_tuples AS recently_dead_tuples,
+ S.missed_dead_pages AS missed_dead_pages,
+ S.missed_dead_tuples AS missed_dead_tuples
FROM pg_class C JOIN
pg_namespace N ON N.oid = C.relnamespace,
diff --git a/src/backend/utils/activity/pgstat_vacuum.c b/src/backend/utils/activity/pgstat_vacuum.c
index ac6baee516..644a732592 100644
--- a/src/backend/utils/activity/pgstat_vacuum.c
+++ b/src/backend/utils/activity/pgstat_vacuum.c
@@ -53,6 +53,9 @@ pgstat_accumulate_extvac_stats_relations(PgStat_VacuumRelationCounts *dst,
ACCUMULATE_SUBFIELD(table, pages_scanned);
ACCUMULATE_SUBFIELD(table, pages_removed);
ACCUMULATE_SUBFIELD(table, tuples_frozen);
+ ACCUMULATE_SUBFIELD(table, recently_dead_tuples);
+ ACCUMULATE_SUBFIELD(table, missed_dead_pages);
+ ACCUMULATE_SUBFIELD(table, missed_dead_tuples);
}
else if (dst->type == PGSTAT_EXTVAC_INDEX)
{
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index e72f76180e..742f4974d5 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -2374,7 +2374,7 @@ pg_stat_have_stats(PG_FUNCTION_ARGS)
Datum
pg_stat_get_vacuum_tables(PG_FUNCTION_ARGS)
{
-#define PG_STAT_GET_VACUUM_TABLES_STATS_COLS 5
+#define PG_STAT_GET_VACUUM_TABLES_STATS_COLS 8
Oid relid = PG_GETARG_OID(0);
PgStat_VacuumRelationCounts *extvacuum;
@@ -2404,6 +2404,9 @@ pg_stat_get_vacuum_tables(PG_FUNCTION_ARGS)
values[i++] = Int64GetDatum(extvacuum->table.pages_removed);
values[i++] = Int64GetDatum(extvacuum->common.tuples_deleted);
values[i++] = Int64GetDatum(extvacuum->table.tuples_frozen);
+ values[i++] = Int64GetDatum(extvacuum->table.recently_dead_tuples);
+ values[i++] = Int64GetDatum(extvacuum->table.missed_dead_pages);
+ values[i++] = Int64GetDatum(extvacuum->table.missed_dead_tuples);
Assert(i == PG_STAT_GET_VACUUM_TABLES_STATS_COLS);
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 9616a01466..6d683413a4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12643,9 +12643,9 @@
proname => 'pg_stat_get_vacuum_tables', prorows => 1000, provolatile => 's', prorettype => 'record', proisstrict => 'f',
proretset => 't',
proargtypes => 'oid',
- proallargtypes => '{oid,oid,int8,int8,int8,int8}',
- proargmodes => '{i,o,o,o,o,o}',
- proargnames => '{reloid,relid,pages_scanned,pages_removed,tuples_deleted,tuples_frozen}',
+ proallargtypes => '{oid,oid,int8,int8,int8,int8,int8,int8,int8}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{reloid,relid,pages_scanned,pages_removed,tuples_deleted,tuples_frozen,recently_dead_tuples,missed_dead_pages,missed_dead_tuples}',
prosrc => 'pg_stat_get_vacuum_tables' }
# oid8 related functions
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 568624de5b..bdcf758441 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -211,10 +211,17 @@ typedef struct PgStat_VacuumRelationCounts
struct
{
int64 tuples_frozen; /* tuples frozen up by vacuum */
+ int64 recently_dead_tuples; /* deleted tuples that are
+ * still visible to some
+ * transaction */
int64 pages_scanned; /* heap pages examined (not skipped by
* VM) */
int64 pages_removed; /* heap pages removed by vacuum
* "truncation" */
+ int64 missed_dead_pages; /* pages with missed dead tuples */
+ int64 missed_dead_tuples; /* tuples not pruned by vacuum due
+ * to failure to get a cleanup
+ * lock */
} table;
struct
{
diff --git a/src/test/modules/test_misc/meson.build b/src/test/modules/test_misc/meson.build
index 969e90b396..267145163a 100644
--- a/src/test/modules/test_misc/meson.build
+++ b/src/test/modules/test_misc/meson.build
@@ -22,6 +22,7 @@ tests += {
't/011_lock_stats.pl',
't/012_ddlutils.pl',
't/013_temp_obj_multisession.pl',
+ 't/014_vacuum_stats.pl',
],
# The injection points are cluster-wide, so disable installcheck
'runningcheck': false,
diff --git a/src/test/modules/test_misc/t/014_vacuum_stats.pl b/src/test/modules/test_misc/t/014_vacuum_stats.pl
new file mode 100644
index 0000000000..9b64b0996c
--- /dev/null
+++ b/src/test/modules/test_misc/t/014_vacuum_stats.pl
@@ -0,0 +1,84 @@
+# Copyright (c) 2024-2026, PostgreSQL Global Development Group
+
+# Test the recently_dead_tuples and missed_dead_tuples/missed_dead_pages
+# counters of the extended vacuum statistics view pg_stat_vacuum_tables.
+#
+# These counters depend on inter-session visibility and on VACUUM's ability to
+# acquire a cleanup lock, so they cannot be exercised by an ordinary
+# single-session regression test. A dedicated TAP cluster gives us full
+# control over the removal horizon (no concurrent backends hold it back), which
+# makes the outcome deterministic -- unlike an isolation test running against a
+# shared regression cluster.
+
+use strict;
+use warnings FATAL => 'all';
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+my $node = PostgreSQL::Test::Cluster->new('main');
+$node->init;
+$node->append_conf(
+ 'postgresql.conf', qq[
+autovacuum = off
+track_vacuum_statistics = on
+]);
+$node->start;
+
+# A small table that fits on a single heap page, so the deleted tuples and the
+# page pinned by the cursor below are the same page.
+$node->safe_psql(
+ 'postgres', qq[
+CREATE TABLE vacstat_iso (id int, ival int) WITH (autovacuum_enabled = off);
+INSERT INTO vacstat_iso SELECT i, i FROM generate_series(1, 50) i;
+]);
+
+# Helper: fetch the four interesting counters for the table.
+my $stats_query = qq[
+SELECT tuples_deleted, recently_dead_tuples, missed_dead_tuples, missed_dead_pages
+ FROM pg_stat_vacuum_tables WHERE relname = 'vacstat_iso'];
+
+# This session first holds an old snapshot (so the deleted tuples stay
+# recently dead), and later pins the heap page (so VACUUM cannot get a cleanup
+# lock and the now-removable tuples are missed instead).
+my $holder = $node->background_psql('postgres', on_error_stop => 1);
+
+# 1. Hold a repeatable-read snapshot that can still see the soon-to-be-deleted
+# tuples, preventing VACUUM from removing them.
+$holder->query_safe(
+ 'BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ;'
+ . ' SELECT count(*) FROM vacstat_iso;');
+
+# 2. Delete ten tuples and vacuum. They are dead but not yet removable, so
+# they are counted as recently dead and not removed.
+$node->safe_psql('postgres', 'DELETE FROM vacstat_iso WHERE id <= 10;');
+$node->safe_psql('postgres', 'VACUUM vacstat_iso;');
+is( $node->safe_psql('postgres', $stats_query),
+ "0|10|0|0",
+ 'recently_dead_tuples counted while an old snapshot is held');
+
+# 3. Release the old snapshot, then pin the table's single heap page with a
+# cursor so VACUUM cannot acquire a cleanup lock on it.
+$holder->query_safe('COMMIT;');
+$holder->query_safe(
+ 'BEGIN; DECLARE c CURSOR FOR SELECT * FROM vacstat_iso;'
+ . ' FETCH NEXT FROM c;');
+
+# 4. The deleted tuples are now removable, but the page is pinned, so a plain
+# VACUUM skips it without a cleanup lock and counts the tuples as missed.
+# (Counters accumulate, so recently_dead_tuples stays at 10.)
+$node->safe_psql('postgres', 'VACUUM vacstat_iso;');
+is( $node->safe_psql('postgres', $stats_query),
+ "0|10|10|1",
+ 'missed_dead_tuples/missed_dead_pages counted while the page is pinned');
+
+# 5. Release the pin and vacuum once more; the tuples are finally removed.
+$holder->query_safe('COMMIT;');
+$node->safe_psql('postgres', 'VACUUM vacstat_iso;');
+is( $node->safe_psql('postgres', $stats_query),
+ "10|10|10|1",
+ 'dead tuples removed once the pin is released');
+
+$holder->quit;
+$node->stop;
+done_testing();
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 8f779a92ff..a2b0472a2d 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -2440,10 +2440,13 @@ pg_stat_vacuum_tables| SELECT n.nspname AS schemaname,
s.pages_scanned,
s.pages_removed,
s.tuples_deleted,
- s.tuples_frozen
+ s.tuples_frozen,
+ s.recently_dead_tuples,
+ s.missed_dead_pages,
+ s.missed_dead_tuples
FROM (pg_class c
JOIN pg_namespace n ON ((n.oid = c.relnamespace))),
- LATERAL pg_stat_get_vacuum_tables(c.oid) s(relid, pages_scanned, pages_removed, tuples_deleted, tuples_frozen)
+ LATERAL pg_stat_get_vacuum_tables(c.oid) s(relid, pages_scanned, pages_removed, tuples_deleted, tuples_frozen, recently_dead_tuples, missed_dead_pages, missed_dead_tuples)
WHERE (c.relkind = ANY (ARRAY['r'::"char", 't'::"char", 'm'::"char"]));
pg_stat_wal| SELECT wal_records,
wal_fpi,
diff --git a/src/test/regress/expected/vacuum_stats.out b/src/test/regress/expected/vacuum_stats.out
index cafaa9bdd9..cb11d6381c 100644
--- a/src/test/regress/expected/vacuum_stats.out
+++ b/src/test/regress/expected/vacuum_stats.out
@@ -69,6 +69,21 @@ SELECT tuples_frozen > 0 AS tuples_frozen
(1 row)
DROP TABLE vacstat_freeze;
+-- dead tuples that survived this vacuum: recently_dead_tuples are still visible
+-- to some transaction, while missed_dead_pages/missed_dead_tuples could not be
+-- removed because the page was pinned by another backend (cleanup lock not
+-- acquired). None occur here, since this VACUUM runs without concurrent
+-- activity (all = 0). The non-zero paths are covered by the
+-- vacuum-extending-in-repetable-read isolation test.
+SELECT recently_dead_tuples = 0 AS recently_dead_tuples,
+ missed_dead_pages = 0 AS missed_dead_pages,
+ missed_dead_tuples = 0 AS missed_dead_tuples
+ FROM pg_stat_vacuum_tables WHERE relname = 'vacstat_t';
+ recently_dead_tuples | missed_dead_pages | missed_dead_tuples
+----------------------+-------------------+--------------------
+ t | t | t
+(1 row)
+
-- per-index view: the primary key index is processed by the same VACUUM.
-- No btree leaf empties out (interleaved deletions), so pages_deleted = 0,
-- while every index entry for a removed heap tuple is deleted.
diff --git a/src/test/regress/sql/vacuum_stats.sql b/src/test/regress/sql/vacuum_stats.sql
index 6061f433a3..75a5a0052b 100644
--- a/src/test/regress/sql/vacuum_stats.sql
+++ b/src/test/regress/sql/vacuum_stats.sql
@@ -44,6 +44,17 @@ SELECT tuples_frozen > 0 AS tuples_frozen
FROM pg_stat_vacuum_tables WHERE relname = 'vacstat_freeze';
DROP TABLE vacstat_freeze;
+-- dead tuples that survived this vacuum: recently_dead_tuples are still visible
+-- to some transaction, while missed_dead_pages/missed_dead_tuples could not be
+-- removed because the page was pinned by another backend (cleanup lock not
+-- acquired). None occur here, since this VACUUM runs without concurrent
+-- activity (all = 0). The non-zero paths are covered by the
+-- vacuum-extending-in-repetable-read isolation test.
+SELECT recently_dead_tuples = 0 AS recently_dead_tuples,
+ missed_dead_pages = 0 AS missed_dead_pages,
+ missed_dead_tuples = 0 AS missed_dead_tuples
+ FROM pg_stat_vacuum_tables WHERE relname = 'vacstat_t';
+
-- per-index view: the primary key index is processed by the same VACUUM.
-- No btree leaf empties out (interleaved deletions), so pages_deleted = 0,
-- while every index entry for a removed heap tuple is deleted.
--
2.39.5 (Apple Git-154)
From ad0a8bad2cc31840b9eba83216b3be49dc3b0b93 Mon Sep 17 00:00:00 2001
From: Alena Rybakina <[email protected]>
Date: Wed, 17 Jun 2026 21:21:44 +0300
Subject: [PATCH 4/9] Extended vacuum statistics: WAL metrics and the sampling
machinery
Introduce the resource-usage sampling machinery for extended vacuum
statistics together with the first sampled metric, the WAL counters.
Around the processing of each relation, vacuum snapshots WAL usage and
records the difference, so the metric reflects only that vacuum's work.
The buffer and timing metrics are sampled through the same machinery and
are added in the following commits. Index processing is sampled on its
own -- including in parallel workers -- and its usage is kept out of the
parent table's figures, so heap and index work are accounted separately.
The PGSTAT_KIND_VACUUM_DB stats kind and pgstat_accumulate_common() are
added here as well: they aggregate the common per-relation counters of
every relation vacuumed in the database, which is only meaningful once the
sampling exists. The two remaining views, pg_stat_vacuum_indexes and
pg_stat_vacuum_database, are created here alongside pg_stat_vacuum_tables;
like the table view they then grow column by column in the following
commits. The per-index view also exposes the index-specific pages_deleted
and tuples_deleted counters from the start.
This commit exposes the WAL counters in all three views, with documentation
and regression coverage:
wal_records number of WAL records generated by the vacuum
wal_fpi number of WAL full page images generated by the vacuum
wal_bytes total amount of WAL generated by the vacuum, in bytes
The separate sampling of index processing, including through the parallel
vacuum path, is covered by a dedicated TAP test
(src/test/modules/test_misc/t/016_vacuum_stats_parallel.pl).
---
doc/src/sgml/system-views.sgml | 130 ++++++
src/backend/access/heap/vacuumlazy.c | 391 ++++++++++++++----
src/backend/catalog/system_views.sql | 25 +-
src/backend/commands/dbcommands.c | 1 +
src/backend/commands/vacuum.c | 4 +
src/backend/commands/vacuumparallel.c | 78 +++-
src/backend/utils/activity/pgstat.c | 15 +
src/backend/utils/activity/pgstat_database.c | 9 +
src/backend/utils/activity/pgstat_vacuum.c | 105 ++++-
src/backend/utils/adt/pgstatfuncs.c | 63 ++-
src/include/catalog/pg_proc.dat | 21 +-
src/include/commands/vacuum.h | 32 +-
src/include/pgstat.h | 15 +
src/include/utils/pgstat_internal.h | 7 +
src/include/utils/pgstat_kind.h | 3 +-
src/test/modules/test_misc/meson.build | 1 +
.../test_misc/t/016_vacuum_stats_parallel.pl | 104 +++++
src/test/regress/expected/rules.out | 22 +-
src/test/regress/expected/vacuum_stats.out | 89 +++-
src/test/regress/sql/vacuum_stats.sql | 56 ++-
20 files changed, 1045 insertions(+), 126 deletions(-)
create mode 100644 src/test/modules/test_misc/t/016_vacuum_stats_parallel.pl
diff --git a/doc/src/sgml/system-views.sgml b/doc/src/sgml/system-views.sgml
index b96c653929..7c95dcea6b 100644
--- a/doc/src/sgml/system-views.sgml
+++ b/doc/src/sgml/system-views.sgml
@@ -5892,6 +5892,30 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx
Number of dead tuples that the vacuum could not remove because it failed to acquire a cleanup lock on their page.
</para></entry>
</row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>wal_records</structfield> <type>bigint</type>
+ </para>
+ <para>
+ Total number of WAL records generated by the vacuum.
+ </para></entry>
+ </row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>wal_fpi</structfield> <type>bigint</type>
+ </para>
+ <para>
+ Total number of WAL full page images generated by the vacuum.
+ </para></entry>
+ </row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>wal_bytes</structfield> <type>numeric</type>
+ </para>
+ <para>
+ Total amount of WAL generated by the vacuum, in bytes.
+ </para></entry>
+ </row>
</tbody>
</tgroup>
</table>
@@ -5983,6 +6007,112 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx
Number of index entries removed by the vacuum.
</para></entry>
</row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>wal_records</structfield> <type>bigint</type>
+ </para>
+ <para>
+ Number of WAL records generated while vacuuming this index.
+ </para></entry>
+ </row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>wal_fpi</structfield> <type>bigint</type>
+ </para>
+ <para>
+ Number of WAL full page images generated while vacuuming this index.
+ </para></entry>
+ </row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>wal_bytes</structfield> <type>numeric</type>
+ </para>
+ <para>
+ Total amount of WAL generated while vacuuming this index, in bytes.
+ </para></entry>
+ </row>
+ </tbody>
+ </tgroup>
+ </table>
+ </sect1>
+
+ <sect1 id="view-pg-stat-vacuum-database">
+ <title><structname>pg_stat_vacuum_database</structname></title>
+
+ <indexterm zone="view-pg-stat-vacuum-database">
+ <primary>pg_stat_vacuum_database</primary>
+ </indexterm>
+
+ <para>
+ The <structname>pg_stat_vacuum_database</structname> view will contain one row
+ for each database, showing extended statistics aggregated over all
+ <command>VACUUM</command> activity in that database. These statistics are
+ accumulated only while <varname>track_vacuum_statistics</varname> is enabled.
+ </para>
+
+ <table id="view-pg-stat-vacuum-database-cols">
+ <title><structname>pg_stat_vacuum_database</structname> Columns</title>
+ <tgroup cols="1">
+ <thead>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ Column Type
+ </para>
+ <para>
+ Description
+ </para></entry>
+ </row>
+ </thead>
+
+ <tbody>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>dboid</structfield> <type>oid</type>
+ </para>
+ <para>
+ OID of the database.
+ </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.
+ </para></entry>
+ </row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>errors</structfield> <type>integer</type>
+ </para>
+ <para>
+ Number of vacuum operations in this database that failed with an error.
+ </para></entry>
+ </row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>wal_records</structfield> <type>bigint</type>
+ </para>
+ <para>
+ Number of WAL records generated by vacuum operations in this database.
+ </para></entry>
+ </row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>wal_fpi</structfield> <type>bigint</type>
+ </para>
+ <para>
+ Number of WAL full page images generated by vacuum operations in this database.
+ </para></entry>
+ </row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>wal_bytes</structfield> <type>numeric</type>
+ </para>
+ <para>
+ Total amount of WAL generated by vacuum operations in this database, in bytes.
+ </para></entry>
+ </row>
</tbody>
</tgroup>
</table>
diff --git a/src/backend/access/heap/vacuumlazy.c b/src/backend/access/heap/vacuumlazy.c
index 7abc83dbfd..c06c538f57 100644
--- a/src/backend/access/heap/vacuumlazy.c
+++ b/src/backend/access/heap/vacuumlazy.c
@@ -411,6 +411,25 @@ typedef struct LVRelState
* been permanently disabled.
*/
BlockNumber eager_scan_remaining_fails;
+
+ int32 wraparound_failsafe_count; /* number of emergency vacuums to
+ * prevent anti-wraparound
+ * shutdown */
+
+ PgStat_VacuumRelationCounts extVacReportIdx;
+
+ /*
+ * Per-index accumulated extended vacuum statistics. VACUUM may process an
+ * index several times (a bulkdelete pass per index scan plus a final
+ * cleanup pass); we sum those passes here and report them to the
+ * cumulative stats system exactly once per index at the end of the vacuum.
+ * Only used by the non-parallel path -- parallel index vacuuming keeps the
+ * equivalent running totals in the DSM and reports from
+ * parallel_vacuum_end(). Sized vacrel->nindexes; NULL if there are no
+ * indexes.
+ */
+ PgStat_VacuumRelationCounts *extVacIdxReports;
+ bool *extVacIdxTouched; /* was the matching index processed? */
} LVRelState;
@@ -422,7 +441,6 @@ typedef struct LVSavedErrInfo
VacErrPhase phase;
} LVSavedErrInfo;
-
/* non-export function prototypes */
static void lazy_scan_heap(LVRelState *vacrel);
static void heap_vacuum_eager_scan_setup(LVRelState *vacrel,
@@ -452,12 +470,12 @@ static void lazy_cleanup_all_indexes(LVRelState *vacrel);
static IndexBulkDeleteResult *lazy_vacuum_one_index(Relation indrel,
IndexBulkDeleteResult *istat,
double reltuples,
- LVRelState *vacrel);
+ LVRelState *vacrel, int idx);
static IndexBulkDeleteResult *lazy_cleanup_one_index(Relation indrel,
IndexBulkDeleteResult *istat,
double reltuples,
bool estimated_count,
- LVRelState *vacrel);
+ LVRelState *vacrel, int idx);
static bool should_attempt_truncation(LVRelState *vacrel);
static void lazy_truncate_heap(LVRelState *vacrel);
static BlockNumber count_nondeletable_pages(LVRelState *vacrel,
@@ -484,9 +502,195 @@ static void update_vacuum_error_info(LVRelState *vacrel,
OffsetNumber offnum);
static void restore_vacuum_error_info(LVRelState *vacrel,
const LVSavedErrInfo *saved_vacrel);
-static void accumulate_heap_vacuum_statistics(LVRelState *vacrel,
- PgStat_VacuumRelationCounts *extVacStats);
+/* ----------
+ * extvac_stats_start() -
+ *
+ * Save cut-off values of extended vacuum counters before start of a relation
+ * processing.
+ * ----------
+ */
+static void
+extvac_stats_start(Relation rel, LVExtStatCounters * counters)
+{
+ if (!pgstat_track_vacuum_statistics)
+ return;
+
+ memset(counters, 0, sizeof(LVExtStatCounters));
+
+ counters->walusage = pgWalUsage;
+}
+
+/* ----------
+ * extvac_stats_end() -
+ *
+ * Called to finish an extended vacuum statistic gathering and form a report.
+ * ----------
+ */
+static void
+extvac_stats_end(Relation rel, LVExtStatCounters * counters,
+ PgStat_CommonCounts * report)
+{
+ WalUsage walusage;
+
+ if (!pgstat_track_vacuum_statistics)
+ return;
+
+ memset(report, 0, sizeof(PgStat_CommonCounts));
+
+ /* Calculate diffs of global stat parameters on WAL usage. */
+ memset(&walusage, 0, sizeof(WalUsage));
+ WalUsageAccumDiff(&walusage, &pgWalUsage, &counters->walusage);
+
+ /*
+ * Fill additional statistics on a vacuum processing operation.
+ */
+ report->wal_records += walusage.wal_records;
+ report->wal_fpi += walusage.wal_fpi;
+ report->wal_bytes += walusage.wal_bytes;
+}
+
+void
+extvac_stats_start_idx(Relation rel, IndexBulkDeleteResult *stats,
+ LVExtStatCountersIdx * counters)
+{
+ if (!pgstat_track_vacuum_statistics)
+ return;
+
+ /* Set initial values for common heap and index statistics */
+ extvac_stats_start(rel, &counters->common);
+ counters->pages_deleted = counters->tuples_removed = 0;
+
+ if (stats != NULL)
+ {
+ /*
+ * XXX: Why do we need this code here? If it is needed, I feel lack of
+ * comments, describing the reason.
+ */
+ counters->tuples_removed = stats->tuples_removed;
+ counters->pages_deleted = stats->pages_deleted;
+ }
+}
+
+void
+extvac_stats_end_idx(Relation rel, IndexBulkDeleteResult *stats,
+ LVExtStatCountersIdx * counters, PgStat_VacuumRelationCounts * report)
+{
+ if (!pgstat_track_vacuum_statistics)
+ return;
+
+ memset(report, 0, sizeof(PgStat_VacuumRelationCounts));
+
+ extvac_stats_end(rel, &counters->common, &report->common);
+
+ report->type = PGSTAT_EXTVAC_INDEX;
+
+ if (stats != NULL)
+ {
+ /*
+ * if something goes wrong or an user doesn't want to track a database
+ * activity - just suppress it.
+ */
+
+ /* Fill index-specific extended stats fields */
+ report->common.tuples_deleted =
+ stats->tuples_removed - counters->tuples_removed;
+ report->index.pages_deleted =
+ stats->pages_deleted - counters->pages_deleted;
+ }
+}
+
+/* Accumulate vacuum statistics for heap.
+ *
+ * Because of complexity of vacuum processing: it switch procesing between
+ * the heap relation to index relations and visa versa, we need to store
+ * gathered statistics information for heap relations several times before
+ * the vacuum starts processing the indexes again.
+ *
+ * It is necessary to gather correct statistics information for heap and indexes
+ * otherwice the index statistics information would be added to his parent heap
+ * statistics information and it would be difficult to analyze it later.
+ *
+ * We can't subtract union vacuum statistics information for index from the heap relations
+ * because of total and delay time time statistics collecting during parallel vacuum
+ * procudure.
+*/
+static void
+accumulate_heap_vacuum_statistics(LVRelState *vacrel, PgStat_VacuumRelationCounts * extVacStats)
+{
+ if (!pgstat_track_vacuum_statistics)
+ return;
+
+ /* Fill heap-specific extended stats fields */
+ extVacStats->type = PGSTAT_EXTVAC_TABLE;
+ extVacStats->table.pages_scanned = vacrel->scanned_pages;
+ extVacStats->table.pages_removed = vacrel->removed_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->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;
+}
+
+static void
+accumulate_idxs_vacuum_statistics(LVRelState *vacrel, PgStat_VacuumRelationCounts * extVacIdxStats)
+{
+ /* Fill heap-specific extended stats fields */
+ 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;
+}
+
+/*
+ * Accumulate one index extended-vacuum report into a per-index running total.
+ *
+ * VACUUM may touch an index more than once: a bulkdelete pass for every index
+ * scan, plus a final cleanup pass. Rather than reporting to the cumulative
+ * stats system on every pass, callers sum the passes here and report the
+ * totals exactly once per index at the end of the vacuum. Shared between the
+ * non-parallel path (accumulating in vacrel) and the parallel path
+ * (accumulating in the DSM), hence not static.
+ */
+void
+extvac_accumulate_idx_report(PgStat_VacuumRelationCounts * dst,
+ const PgStat_VacuumRelationCounts * src)
+{
+ dst->type = PGSTAT_EXTVAC_INDEX;
+
+ dst->common.wal_records += src->common.wal_records;
+ dst->common.wal_fpi += src->common.wal_fpi;
+ dst->common.wal_bytes += src->common.wal_bytes;
+ dst->common.tuples_deleted += src->common.tuples_deleted;
+
+ dst->index.pages_deleted += src->index.pages_deleted;
+}
+
+/*
+ * Report the accumulated per-index extended vacuum statistics, one report per
+ * index. Used by the non-parallel path only; the parallel path reports its
+ * DSM-resident totals from parallel_vacuum_end().
+ */
+static void
+report_index_vacuum_extstats(LVRelState *vacrel)
+{
+ if (!pgstat_track_vacuum_statistics)
+ return;
+
+ for (int idx = 0; idx < vacrel->nindexes; idx++)
+ {
+ Relation indrel = vacrel->indrels[idx];
+
+ if (!vacrel->extVacIdxTouched[idx])
+ continue;
+
+ pgstat_report_vacuum_extstats(RelationGetRelid(indrel),
+ indrel->rd_rel->relisshared,
+ &vacrel->extVacIdxReports[idx]);
+ }
+}
/*
@@ -612,66 +816,6 @@ heap_vacuum_eager_scan_setup(LVRelState *vacrel, const VacuumParams *params)
first_region_ratio;
}
-/*
- * Fill the extended vacuum statistics report for a heap relation with the
- * counters that are derived directly from the LVRelState. Buffer, WAL and
- * timing counters require sampling resource usage around the vacuum and are
- * gathered separately; they are not touched here.
- */
-static void
-accumulate_heap_vacuum_statistics(LVRelState *vacrel,
- PgStat_VacuumRelationCounts *extVacStats)
-{
- if (!pgstat_track_vacuum_statistics)
- return;
-
- extVacStats->type = PGSTAT_EXTVAC_TABLE;
- extVacStats->table.pages_scanned = vacrel->scanned_pages;
- extVacStats->table.pages_removed = vacrel->removed_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_pages = vacrel->missed_dead_pages;
- extVacStats->table.missed_dead_tuples = vacrel->missed_dead_tuples;
-}
-
-/*
- * Report the per-index extended vacuum statistics, one report per index.
- *
- * The per-index counters (pages_deleted and the number of removed index
- * entries) are derived directly from each index's final IndexBulkDeleteResult,
- * which already holds the totals accumulated across all bulkdelete and cleanup
- * passes -- so no per-pass sampling is needed here. Used by the non-parallel
- * path only; the parallel path reports its DSM-resident results from
- * parallel_vacuum_end().
- */
-static void
-report_index_vacuum_extstats(LVRelState *vacrel)
-{
- if (!pgstat_track_vacuum_statistics)
- return;
-
- for (int idx = 0; idx < vacrel->nindexes; idx++)
- {
- Relation indrel = vacrel->indrels[idx];
- IndexBulkDeleteResult *istat = vacrel->indstats[idx];
- PgStat_VacuumRelationCounts report;
-
- /* Skip indexes that this vacuum did not process */
- if (istat == NULL)
- continue;
-
- memset(&report, 0, sizeof(report));
- report.type = PGSTAT_EXTVAC_INDEX;
- report.common.tuples_deleted = istat->tuples_removed;
- report.index.pages_deleted = istat->pages_deleted;
-
- pgstat_report_vacuum_extstats(RelationGetRelid(indrel),
- indrel->rd_rel->relisshared,
- &report);
- }
-}
-
/*
* heap_vacuum_rel() -- perform VACUUM for one heap relation
*
@@ -698,7 +842,6 @@ heap_vacuum_rel(Relation rel, const VacuumParams *params,
new_rel_allvisible,
new_rel_allfrozen;
PGRUsage ru0;
- TimestampTz starttime = 0;
PgStat_Counter startreadtime = 0,
startwritetime = 0;
WalUsage startwalusage = pgWalUsage;
@@ -706,9 +849,11 @@ heap_vacuum_rel(Relation rel, const VacuumParams *params,
ErrorContextCallback errcallback;
char **indnames = NULL;
Size dead_items_max_bytes = 0;
+ LVExtStatCounters extVacCounters;
PgStat_VacuumRelationCounts extVacReport;
+ TimestampTz starttime;
- /* Initialize the extended vacuum statistics report */
+ /* Initialize vacuum statistics */
memset(&extVacReport, 0, sizeof(PgStat_VacuumRelationCounts));
verbose = (params->options & VACOPT_VERBOSE) != 0;
@@ -724,8 +869,8 @@ heap_vacuum_rel(Relation rel, const VacuumParams *params,
}
}
- /* Used for instrumentation and stats report */
starttime = GetCurrentTimestamp();
+ extvac_stats_start(rel, &extVacCounters);
pgstat_progress_start_command(PROGRESS_COMMAND_VACUUM,
RelationGetRelid(rel));
@@ -753,8 +898,8 @@ heap_vacuum_rel(Relation rel, const VacuumParams *params,
vacrel = palloc0_object(LVRelState);
vacrel->dbname = get_database_name(MyDatabaseId);
vacrel->relnamespace = get_namespace_name(RelationGetNamespace(rel));
- vacrel->reloid = RelationGetRelid(rel);
vacrel->relname = pstrdup(RelationGetRelationName(rel));
+ vacrel->reloid = RelationGetRelid(rel);
vacrel->indname = NULL;
vacrel->phase = VACUUM_ERRCB_PHASE_UNKNOWN;
vacrel->verbose = verbose;
@@ -763,12 +908,26 @@ heap_vacuum_rel(Relation rel, const VacuumParams *params,
errcallback.previous = error_context_stack;
error_context_stack = &errcallback;
+ memset(&vacrel->extVacReportIdx, 0, sizeof(PgStat_VacuumRelationCounts));
+ memset(&extVacReport.common, 0, sizeof(PgStat_CommonCounts));
+
/* Set up high level stuff about rel and its indexes */
vacrel->rel = rel;
vac_open_indexes(vacrel->rel, RowExclusiveLock, &vacrel->nindexes,
&vacrel->indrels);
vacrel->bstrategy = bstrategy;
+ /*
+ * Allocate per-index accumulators for extended vacuum statistics. Index
+ * passes add into these and the totals are reported once per index at the
+ * end of the vacuum (see report_index_vacuum_extstats()).
+ */
+ if (vacrel->nindexes > 0)
+ {
+ vacrel->extVacIdxReports =
+ palloc0_array(PgStat_VacuumRelationCounts, vacrel->nindexes);
+ vacrel->extVacIdxTouched = palloc0_array(bool, vacrel->nindexes);
+ }
if (instrument && vacrel->nindexes > 0)
{
/* Copy index names used by instrumentation (not error reporting) */
@@ -871,6 +1030,7 @@ heap_vacuum_rel(Relation rel, const VacuumParams *params,
vacrel->aggressive = vacuum_get_cutoffs(rel, params, &vacrel->cutoffs);
vacrel->rel_pages = orig_rel_pages = RelationGetNumberOfBlocks(rel);
vacrel->vistest = GlobalVisTestFor(rel);
+ vacrel->wraparound_failsafe_count = 0;
/* Initialize state used to track oldest extant XID/MXID */
vacrel->NewRelfrozenXid = vacrel->cutoffs.OldestXmin;
@@ -1054,9 +1214,14 @@ heap_vacuum_rel(Relation rel, const VacuumParams *params,
* soon in cases where the failsafe prevented significant amounts of heap
* vacuuming.
*/
+
+ /*
+ * Make generic extended vacuum stats report and fill heap-specific
+ * extended stats fields.
+ */
+ extvac_stats_end(vacrel->rel, &extVacCounters, &extVacReport.common);
accumulate_heap_vacuum_statistics(vacrel, &extVacReport);
- pgstat_report_vacuum_extstats(vacrel->reloid, rel->rd_rel->relisshared,
- &extVacReport);
+ pgstat_report_vacuum_extstats(vacrel->reloid, rel->rd_rel->relisshared, &extVacReport);
pgstat_report_vacuum(rel,
Max(vacrel->new_live_tuples, 0),
vacrel->recently_dead_tuples +
@@ -1695,7 +1860,8 @@ lazy_scan_heap(LVRelState *vacrel)
* Report the per-index extended vacuum statistics accumulated over all
* bulkdelete and cleanup passes, exactly once per index. The parallel
* path reports its DSM-resident totals from parallel_vacuum_end() instead,
- * so only do it here when index vacuuming ran in the leader.
+ * so only do it here when index vacuuming ran in the leader. pvs is still
+ * alive at this point (it is torn down later in dead_items_cleanup()).
*/
if (vacrel->nindexes > 0 && !ParallelVacuumIsActive(vacrel))
report_index_vacuum_extstats(vacrel);
@@ -2616,7 +2782,7 @@ lazy_vacuum_all_indexes(LVRelState *vacrel)
vacrel->indstats[idx] = lazy_vacuum_one_index(indrel, istat,
old_live_tuples,
- vacrel);
+ vacrel, idx);
/* Report the number of indexes vacuumed */
pgstat_progress_update_param(PROGRESS_VACUUM_INDEXES_PROCESSED,
@@ -2632,11 +2798,21 @@ lazy_vacuum_all_indexes(LVRelState *vacrel)
}
else
{
+ LVExtStatCounters counters;
+ PgStat_VacuumRelationCounts extVacReport;
+
+ memset(&extVacReport.common, 0, sizeof(PgStat_CommonCounts));
+
+ extvac_stats_start(vacrel->rel, &counters);
+
/* Outsource everything to parallel variant */
parallel_vacuum_bulkdel_all_indexes(vacrel->pvs, old_live_tuples,
vacrel->num_index_scans,
&(vacrel->worker_usage.vacuum));
+ extvac_stats_end(vacrel->rel, &counters, &extVacReport.common);
+ accumulate_idxs_vacuum_statistics(vacrel, &extVacReport);
+
/*
* Do a postcheck to consider applying wraparound failsafe now. Note
* that parallel VACUUM only gets the precheck and this postcheck.
@@ -2984,6 +3160,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
@@ -3057,7 +3234,7 @@ lazy_cleanup_all_indexes(LVRelState *vacrel)
vacrel->indstats[idx] =
lazy_cleanup_one_index(indrel, istat, reltuples,
- estimated_count, vacrel);
+ estimated_count, vacrel, idx);
/* Report the number of indexes cleaned up */
pgstat_progress_update_param(PROGRESS_VACUUM_INDEXES_PROCESSED,
@@ -3066,11 +3243,21 @@ lazy_cleanup_all_indexes(LVRelState *vacrel)
}
else
{
+ LVExtStatCounters counters;
+ PgStat_VacuumRelationCounts extVacReport;
+
+ memset(&extVacReport.common, 0, sizeof(PgStat_CommonCounts));
+
+ extvac_stats_start(vacrel->rel, &counters);
+
/* Outsource everything to parallel variant */
parallel_vacuum_cleanup_all_indexes(vacrel->pvs, reltuples,
vacrel->num_index_scans,
estimated_count,
&(vacrel->worker_usage.cleanup));
+
+ extvac_stats_end(vacrel->rel, &counters, &extVacReport.common);
+ accumulate_idxs_vacuum_statistics(vacrel, &extVacReport);
}
/* Reset the progress counters */
@@ -3092,10 +3279,22 @@ lazy_cleanup_all_indexes(LVRelState *vacrel)
*/
static IndexBulkDeleteResult *
lazy_vacuum_one_index(Relation indrel, IndexBulkDeleteResult *istat,
- double reltuples, LVRelState *vacrel)
+ double reltuples, LVRelState *vacrel, int idx)
{
IndexVacuumInfo ivinfo;
LVSavedErrInfo saved_err_info;
+ LVExtStatCountersIdx extVacCounters;
+ PgStat_VacuumRelationCounts extVacReport;
+
+ /*
+ * Zero the report up front: extvac_stats_end_idx() leaves it untouched
+ * when statistics tracking is disabled, but the accumulators below read it
+ * unconditionally.
+ */
+ memset(&extVacReport, 0, sizeof(PgStat_VacuumRelationCounts));
+
+ /* Set initial statistics values to gather vacuum statistics for the index */
+ extvac_stats_start_idx(indrel, istat, &extVacCounters);
ivinfo.index = indrel;
ivinfo.heaprel = vacrel->rel;
@@ -3122,6 +3321,19 @@ lazy_vacuum_one_index(Relation indrel, IndexBulkDeleteResult *istat,
istat = vac_bulkdel_one_index(&ivinfo, istat, vacrel->dead_items,
vacrel->dead_items_info);
+ /* Make extended vacuum stats report for index */
+ extvac_stats_end_idx(indrel, istat, &extVacCounters, &extVacReport);
+
+ /* This index's time must be excluded from the parent heap's totals */
+ accumulate_idxs_vacuum_statistics(vacrel, &extVacReport);
+
+ /*
+ * Accumulate this pass into the index's running totals. They are reported
+ * to the cumulative stats system once per index at the end of the vacuum.
+ */
+ extvac_accumulate_idx_report(&vacrel->extVacIdxReports[idx], &extVacReport);
+ vacrel->extVacIdxTouched[idx] = true;
+
/* Revert to the previous phase information for error traceback */
restore_vacuum_error_info(vacrel, &saved_err_info);
pfree(vacrel->indname);
@@ -3142,10 +3354,22 @@ lazy_vacuum_one_index(Relation indrel, IndexBulkDeleteResult *istat,
static IndexBulkDeleteResult *
lazy_cleanup_one_index(Relation indrel, IndexBulkDeleteResult *istat,
double reltuples, bool estimated_count,
- LVRelState *vacrel)
+ LVRelState *vacrel, int idx)
{
IndexVacuumInfo ivinfo;
LVSavedErrInfo saved_err_info;
+ LVExtStatCountersIdx extVacCounters;
+ PgStat_VacuumRelationCounts extVacReport;
+
+ /*
+ * Zero the report up front: extvac_stats_end_idx() leaves it untouched
+ * when statistics tracking is disabled, but the accumulators below read it
+ * unconditionally.
+ */
+ memset(&extVacReport, 0, sizeof(PgStat_VacuumRelationCounts));
+
+ /* Set initial statistics values to gather vacuum statistics for the index */
+ extvac_stats_start_idx(indrel, istat, &extVacCounters);
ivinfo.index = indrel;
ivinfo.heaprel = vacrel->rel;
@@ -3171,6 +3395,19 @@ lazy_cleanup_one_index(Relation indrel, IndexBulkDeleteResult *istat,
istat = vac_cleanup_one_index(&ivinfo, istat);
+ /* Make extended vacuum stats report for index */
+ extvac_stats_end_idx(indrel, istat, &extVacCounters, &extVacReport);
+
+ /* This index's time must be excluded from the parent heap's totals */
+ accumulate_idxs_vacuum_statistics(vacrel, &extVacReport);
+
+ /*
+ * Accumulate this pass into the index's running totals. They are reported
+ * to the cumulative stats system once per index at the end of the vacuum.
+ */
+ extvac_accumulate_idx_report(&vacrel->extVacIdxReports[idx], &extVacReport);
+ vacrel->extVacIdxTouched[idx] = true;
+
/* Revert to the previous phase information for error traceback */
restore_vacuum_error_info(vacrel, &saved_err_info);
pfree(vacrel->indname);
@@ -3616,7 +3853,7 @@ dead_items_cleanup(LVRelState *vacrel)
}
/* End parallel mode */
- parallel_vacuum_end(vacrel->pvs, vacrel->indstats);
+ parallel_vacuum_end(vacrel->pvs, vacrel->indstats, &vacrel->extVacReportIdx);
vacrel->pvs = NULL;
}
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index 6f697ab390..47d6aa6aea 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -1572,7 +1572,10 @@ CREATE VIEW pg_stat_vacuum_tables AS
S.tuples_frozen AS tuples_frozen,
S.recently_dead_tuples AS recently_dead_tuples,
S.missed_dead_pages AS missed_dead_pages,
- S.missed_dead_tuples AS missed_dead_tuples
+ S.missed_dead_tuples AS missed_dead_tuples,
+ S.wal_records AS wal_records,
+ S.wal_fpi AS wal_fpi,
+ S.wal_bytes AS wal_bytes
FROM pg_class C JOIN
pg_namespace N ON N.oid = C.relnamespace,
@@ -1588,7 +1591,11 @@ CREATE VIEW pg_stat_vacuum_indexes AS
I.relname AS indexrelname,
S.pages_deleted AS pages_deleted,
- S.tuples_deleted AS tuples_deleted
+ S.tuples_deleted AS tuples_deleted,
+
+ S.wal_records AS wal_records,
+ S.wal_fpi AS wal_fpi,
+ S.wal_bytes AS wal_bytes
FROM
pg_class C JOIN
pg_index X ON C.oid = X.indrelid JOIN
@@ -1596,3 +1603,17 @@ CREATE VIEW pg_stat_vacuum_indexes AS
LEFT JOIN pg_namespace N ON (N.oid = C.relnamespace),
LATERAL pg_stat_get_vacuum_indexes(I.oid) S
WHERE C.relkind IN ('r', 't', 'm');
+
+CREATE VIEW pg_stat_vacuum_database AS
+ SELECT
+ D.oid AS dboid,
+ D.datname AS dbname,
+
+ S.errors AS errors,
+
+ S.wal_records AS wal_records,
+ S.wal_fpi AS wal_fpi,
+ S.wal_bytes AS wal_bytes
+ FROM
+ pg_database D,
+ LATERAL pg_stat_get_vacuum_database(D.oid) S;
diff --git a/src/backend/commands/dbcommands.c b/src/backend/commands/dbcommands.c
index f0819d15ab..49c3b40c35 100644
--- a/src/backend/commands/dbcommands.c
+++ b/src/backend/commands/dbcommands.c
@@ -1830,6 +1830,7 @@ dropdb(const char *dbname, bool missing_ok, bool force)
* Tell the cumulative stats system to forget it immediately, too.
*/
pgstat_drop_database(db_id);
+ pgstat_drop_vacuum_database(db_id);
/*
* Except for the deletion of the catalog row, subsequent actions are not
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index a4abb29cf6..aacecee58a 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. */
+double VacuumDelayTime = 0; /* msec. */
+
/* non-export function prototypes */
static List *expand_vacuum_rel(VacuumRelation *vrel,
MemoryContext vac_context, int options);
@@ -2566,6 +2569,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 7725c4ecc1..ea13859485 100644
--- a/src/backend/commands/vacuumparallel.c
+++ b/src/backend/commands/vacuumparallel.c
@@ -203,6 +203,16 @@ typedef struct PVIndStats
*/
bool istat_updated; /* are the stats updated? */
IndexBulkDeleteResult istat;
+
+ /*
+ * Extended vacuum statistics accumulated across all bulkdelete and cleanup
+ * passes for this index, by whichever process (leader or worker) ran each
+ * pass. The leader reports the totals to the cumulative stats system once
+ * per index in parallel_vacuum_end(), and also feeds them back so that the
+ * index work can be subtracted from the parent heap's figures.
+ */
+ bool extvac_touched; /* was this index processed at all? */
+ PgStat_VacuumRelationCounts extvacstats;
} PVIndStats;
/*
@@ -512,10 +522,22 @@ parallel_vacuum_init(Relation rel, Relation *indrels, int nindexes,
* context, but that won't be safe (see ExitParallelMode).
*/
void
-parallel_vacuum_end(ParallelVacuumState *pvs, IndexBulkDeleteResult **istats)
+parallel_vacuum_end(ParallelVacuumState *pvs, IndexBulkDeleteResult **istats,
+ PgStat_VacuumRelationCounts *idx_heap_total)
{
+ PgStat_VacuumRelationCounts *extvacstats = NULL;
+
Assert(!IsParallelWorker());
+ /*
+ * Stash the per-index extended vacuum statistics while the DSM is still
+ * mapped; they are reported once per index after we have left parallel
+ * mode. An index that was never processed keeps its zeroed slot (type
+ * PGSTAT_EXTVAC_INVALID) and is skipped there.
+ */
+ if (pgstat_track_vacuum_statistics && pvs->nindexes > 0)
+ extvacstats = palloc0_array(PgStat_VacuumRelationCounts, pvs->nindexes);
+
/* Copy the updated statistics */
for (int i = 0; i < pvs->nindexes; i++)
{
@@ -528,6 +550,10 @@ parallel_vacuum_end(ParallelVacuumState *pvs, IndexBulkDeleteResult **istats)
}
else
istats[i] = NULL;
+
+ if (extvacstats != NULL && indstats->extvac_touched)
+ memcpy(&extvacstats[i], &indstats->extvacstats,
+ sizeof(PgStat_VacuumRelationCounts));
}
TidStoreDestroy(pvs->dead_items);
@@ -537,29 +563,30 @@ parallel_vacuum_end(ParallelVacuumState *pvs, IndexBulkDeleteResult **istats)
/*
* Report the per-index extended vacuum statistics, one report per index,
- * derived directly from each index's final IndexBulkDeleteResult. The
- * indexes are still open here (pvs->indrels is the leader's own array, not
- * in the now-destroyed DSM).
+ * and add each index's resource usage to idx_heap_total so the caller can
+ * subtract it from the parent heap's figures (the leader folds the workers'
+ * buffer/WAL usage into its own, so without this the index work would be
+ * attributed to the heap). The indexes are still open here (pvs->indrels
+ * is the leader's own array, not in the now-destroyed DSM).
*/
- if (pgstat_track_vacuum_statistics)
+ if (extvacstats != NULL)
{
for (int i = 0; i < pvs->nindexes; i++)
{
Relation indrel = pvs->indrels[i];
- PgStat_VacuumRelationCounts report;
- if (istats[i] == NULL)
+ if (extvacstats[i].type != PGSTAT_EXTVAC_INDEX)
continue;
- memset(&report, 0, sizeof(report));
- report.type = PGSTAT_EXTVAC_INDEX;
- report.common.tuples_deleted = istats[i]->tuples_removed;
- report.index.pages_deleted = istats[i]->pages_deleted;
-
pgstat_report_vacuum_extstats(RelationGetRelid(indrel),
indrel->rd_rel->relisshared,
- &report);
+ &extvacstats[i]);
+
+ if (idx_heap_total != NULL)
+ extvac_accumulate_idx_report(idx_heap_total, &extvacstats[i]);
}
+
+ pfree(extvacstats);
}
if (AmAutoVacuumWorkerProcess())
@@ -1103,6 +1130,14 @@ parallel_vacuum_process_one_index(ParallelVacuumState *pvs, Relation indrel,
IndexBulkDeleteResult *istat = NULL;
IndexBulkDeleteResult *istat_res;
IndexVacuumInfo ivinfo;
+ LVExtStatCountersIdx extVacCounters;
+ PgStat_VacuumRelationCounts extVacReport;
+
+ /*
+ * Zero the report up front: extvac_stats_end_idx() leaves it untouched when
+ * statistics tracking is disabled.
+ */
+ memset(&extVacReport, 0, sizeof(PgStat_VacuumRelationCounts));
/*
* Update the pointer to the corresponding bulk-deletion result if someone
@@ -1111,6 +1146,9 @@ parallel_vacuum_process_one_index(ParallelVacuumState *pvs, Relation indrel,
if (indstats->istat_updated)
istat = &(indstats->istat);
+ /* Snapshot the resource usage before processing this index pass */
+ extvac_stats_start_idx(indrel, istat, &extVacCounters);
+
ivinfo.index = indrel;
ivinfo.heaprel = pvs->heaprel;
ivinfo.analyze_only = false;
@@ -1139,6 +1177,20 @@ parallel_vacuum_process_one_index(ParallelVacuumState *pvs, Relation indrel,
RelationGetRelationName(indrel));
}
+ /*
+ * Accumulate this pass's extended vacuum statistics into the index's
+ * DSM-resident running totals. The leader reports them, and subtracts
+ * them from the parent heap, once per index in parallel_vacuum_end(). Each
+ * index is processed by a single process per pass, so no locking is needed
+ * here (same as the istat update below).
+ */
+ extvac_stats_end_idx(indrel, istat_res, &extVacCounters, &extVacReport);
+ if (pgstat_track_vacuum_statistics)
+ {
+ extvac_accumulate_idx_report(&indstats->extvacstats, &extVacReport);
+ indstats->extvac_touched = true;
+ }
+
/*
* Copy the index bulk-deletion result returned from ambulkdelete and
* amvacuumcleanup to the DSM segment if it's the first cycle because they
diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index 4e950672e2..3741e4e54a 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -500,6 +500,21 @@ static const PgStat_KindInfo pgstat_kind_builtin_infos[PGSTAT_KIND_BUILTIN_SIZE]
.reset_all_cb = pgstat_wal_reset_all_cb,
.snapshot_cb = pgstat_wal_snapshot_cb,
},
+ [PGSTAT_KIND_VACUUM_DB] = {
+ .name = "vacuum statistics",
+
+ .fixed_amount = false,
+ .write_to_file = true,
+ /* so pg_stat_database entries can be seen in all databases */
+ .accessed_across_databases = true,
+
+ .shared_size = sizeof(PgStatShared_VacuumDB),
+ .shared_data_off = offsetof(PgStatShared_VacuumDB, stats),
+ .shared_data_len = sizeof(((PgStatShared_VacuumDB *) 0)->stats),
+ .pending_size = sizeof(PgStat_VacuumDBCounts),
+
+ .flush_pending_cb = pgstat_vacuum_db_flush_cb,
+ },
[PGSTAT_KIND_VACUUM_RELATION] = {
.name = "vacuum statistics",
diff --git a/src/backend/utils/activity/pgstat_database.c b/src/backend/utils/activity/pgstat_database.c
index 7f3bc01659..cdff619200 100644
--- a/src/backend/utils/activity/pgstat_database.c
+++ b/src/backend/utils/activity/pgstat_database.c
@@ -46,6 +46,15 @@ pgstat_drop_database(Oid databaseid)
pgstat_drop_transactional(PGSTAT_KIND_DATABASE, databaseid, InvalidOid);
}
+/*
+ * Remove entry for the database being dropped.
+ */
+void
+pgstat_drop_vacuum_database(Oid databaseid)
+{
+ pgstat_drop_transactional(PGSTAT_KIND_VACUUM_DB, databaseid, InvalidOid);
+}
+
/*
* Called from autovacuum.c to report startup of an autovacuum process.
* We are called before InitPostgres is done, so can't rely on MyDatabaseId;
diff --git a/src/backend/utils/activity/pgstat_vacuum.c b/src/backend/utils/activity/pgstat_vacuum.c
index 644a732592..99c4932ded 100644
--- a/src/backend/utils/activity/pgstat_vacuum.c
+++ b/src/backend/utils/activity/pgstat_vacuum.c
@@ -7,7 +7,7 @@
* kept separate from pgstat_relation.c and pgstat_database.c to reduce the
* memory footprint of the regular relation and database statistics: vacuum
* metrics require significantly more space per relation, so they live in their
- * own PGSTAT_KIND_VACUUM_RELATION stats kind.
+ * own PGSTAT_KIND_VACUUM_RELATION and PGSTAT_KIND_VACUUM_DB stats kinds.
*
* Copyright (c) 2001-2026, PostgreSQL Global Development Group
*
@@ -22,15 +22,31 @@
#include "utils/memutils.h"
#include "utils/pgstat_internal.h"
+/* ----------
+ * GUC parameters
+ * ----------
+ */
+bool pgstat_track_vacuum_statistics_for_relations = false;
+
+#define ACCUMULATE_FIELD(field) (dst->field += src->field)
#define ACCUMULATE_SUBFIELD(substruct, field) (dst->substruct.field += src->substruct.field)
/*
- * Accumulate the per-table extended vacuum counters collected so far.
- *
- * Only the counters derived directly from the vacuum's own bookkeeping are
- * summed here. The buffer, WAL and timing counters (and the per-index
- * counters) are accumulated by additional code added together with the
- * helpers that gather them.
+ * Accumulate the counters that are common to heap relations, indexes and
+ * databases.
+ */
+static void
+pgstat_accumulate_common(PgStat_CommonCounts *dst, const PgStat_CommonCounts *src)
+{
+ ACCUMULATE_FIELD(wal_records);
+ ACCUMULATE_FIELD(wal_fpi);
+ ACCUMULATE_FIELD(wal_bytes);
+
+ ACCUMULATE_FIELD(tuples_deleted);
+}
+
+/*
+ * Accumulate per-relation (heap or index) extended vacuum counters.
*/
static void
pgstat_accumulate_extvac_stats_relations(PgStat_VacuumRelationCounts *dst,
@@ -46,15 +62,15 @@ pgstat_accumulate_extvac_stats_relations(PgStat_VacuumRelationCounts *dst,
src->type != PGSTAT_EXTVAC_DB &&
src->type == dst->type);
- ACCUMULATE_SUBFIELD(common, tuples_deleted);
+ pgstat_accumulate_common(&dst->common, &src->common);
if (dst->type == PGSTAT_EXTVAC_TABLE)
{
ACCUMULATE_SUBFIELD(table, pages_scanned);
ACCUMULATE_SUBFIELD(table, pages_removed);
- ACCUMULATE_SUBFIELD(table, tuples_frozen);
- ACCUMULATE_SUBFIELD(table, recently_dead_tuples);
ACCUMULATE_SUBFIELD(table, missed_dead_pages);
+ ACCUMULATE_SUBFIELD(table, tuples_frozen);
+ ACCUMULATE_SUBFIELD(table, recently_dead_tuples);
ACCUMULATE_SUBFIELD(table, missed_dead_tuples);
}
else if (dst->type == PGSTAT_EXTVAC_INDEX)
@@ -64,8 +80,22 @@ pgstat_accumulate_extvac_stats_relations(PgStat_VacuumRelationCounts *dst,
}
/*
- * Report that the relation was just vacuumed, accumulating its extended
- * statistics into the per-relation entry.
+ * Accumulate per-database extended vacuum counters.
+ */
+static void
+pgstat_accumulate_extvac_stats_db(PgStat_VacuumDBCounts *dst,
+ PgStat_VacuumDBCounts *src)
+{
+ if (!pgstat_track_vacuum_statistics)
+ return;
+
+ pgstat_accumulate_common(&dst->common, &src->common);
+ dst->errors += src->errors;
+}
+
+/*
+ * Report that the relation was just vacuumed, accumulating both its own
+ * extended statistics and the database-wide aggregate.
*/
void
pgstat_report_vacuum_extstats(Oid tableoid, bool shared,
@@ -73,20 +103,29 @@ pgstat_report_vacuum_extstats(Oid tableoid, bool shared,
{
PgStat_EntryRef *entry_ref;
PgStat_RelationVacuumPending *relpending;
+ PgStat_VacuumDBCounts *dbpending;
Oid dboid = (shared ? InvalidOid : MyDatabaseId);
if (!pgstat_track_vacuum_statistics)
return;
/*
- * Accumulate into a pending entry instead of taking a shared-stats lock
- * here; pgstat_report_stat() flushes it to shared memory through the
- * registered flush callback once the vacuum's transaction completes.
+ * Accumulate into pending entries instead of taking a shared-stats lock
+ * here; pgstat_report_stat() flushes them to shared memory through the
+ * registered flush callbacks once the vacuum's transaction completes.
*/
+
+ /* Per-relation extended vacuum statistics */
entry_ref = pgstat_prep_pending_entry(PGSTAT_KIND_VACUUM_RELATION,
dboid, tableoid, NULL);
relpending = (PgStat_RelationVacuumPending *) entry_ref->pending;
pgstat_accumulate_extvac_stats_relations(&relpending->counts, params);
+
+ /* Database-wide aggregate of the same work */
+ entry_ref = pgstat_prep_pending_entry(PGSTAT_KIND_VACUUM_DB,
+ dboid, InvalidOid, NULL);
+ dbpending = (PgStat_VacuumDBCounts *) entry_ref->pending;
+ pgstat_accumulate_common(&dbpending->common, ¶ms->common);
}
/*
@@ -120,6 +159,31 @@ pgstat_vacuum_relation_flush_cb(PgStat_EntryRef *entry_ref, bool nowait)
return true;
}
+/*
+ * Flush out pending per-database extended vacuum stats for the entry.
+ */
+bool
+pgstat_vacuum_db_flush_cb(PgStat_EntryRef *entry_ref, bool nowait)
+{
+ PgStatShared_VacuumDB *sharedent;
+ PgStat_VacuumDBCounts *pendingent;
+
+ pendingent = (PgStat_VacuumDBCounts *) entry_ref->pending;
+ sharedent = (PgStatShared_VacuumDB *) entry_ref->shared_stats;
+
+ if (pg_memory_is_all_zeros(pendingent, sizeof(PgStat_VacuumDBCounts)))
+ return true;
+
+ if (!pgstat_lock_entry(entry_ref, nowait))
+ return false;
+
+ pgstat_accumulate_extvac_stats_db(&sharedent->stats, pendingent);
+
+ pgstat_unlock_entry(entry_ref);
+
+ return true;
+}
+
/*
* Support function for the SQL-callable pgstat* functions. Returns the vacuum
* collected statistics for one relation or NULL.
@@ -130,3 +194,14 @@ pgstat_fetch_stat_vacuum_tabentry(Oid relid, Oid dbid)
return (PgStat_VacuumRelationCounts *)
pgstat_fetch_entry(PGSTAT_KIND_VACUUM_RELATION, dbid, relid, NULL);
}
+
+/*
+ * Support function for the SQL-callable pgstat* functions. Returns the vacuum
+ * collected statistics for one database or NULL.
+ */
+PgStat_VacuumDBCounts *
+pgstat_fetch_stat_vacuum_dbentry(Oid dbid)
+{
+ return (PgStat_VacuumDBCounts *)
+ pgstat_fetch_entry(PGSTAT_KIND_VACUUM_DB, dbid, InvalidOid, NULL);
+}
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 742f4974d5..aad4427469 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -2374,13 +2374,14 @@ pg_stat_have_stats(PG_FUNCTION_ARGS)
Datum
pg_stat_get_vacuum_tables(PG_FUNCTION_ARGS)
{
-#define PG_STAT_GET_VACUUM_TABLES_STATS_COLS 8
+#define PG_STAT_GET_VACUUM_TABLES_STATS_COLS 11
Oid relid = PG_GETARG_OID(0);
PgStat_VacuumRelationCounts *extvacuum;
TupleDesc tupdesc;
Datum values[PG_STAT_GET_VACUUM_TABLES_STATS_COLS] = {0};
bool nulls[PG_STAT_GET_VACUUM_TABLES_STATS_COLS] = {0};
+ char buf[256];
int i = 0;
/* Build a tuple descriptor for our result type */
@@ -2407,6 +2408,13 @@ pg_stat_get_vacuum_tables(PG_FUNCTION_ARGS)
values[i++] = Int64GetDatum(extvacuum->table.recently_dead_tuples);
values[i++] = Int64GetDatum(extvacuum->table.missed_dead_pages);
values[i++] = Int64GetDatum(extvacuum->table.missed_dead_tuples);
+ values[i++] = Int64GetDatum(extvacuum->common.wal_records);
+ values[i++] = Int64GetDatum(extvacuum->common.wal_fpi);
+ snprintf(buf, sizeof buf, UINT64_FORMAT, extvacuum->common.wal_bytes);
+ values[i++] = DirectFunctionCall3(numeric_in,
+ CStringGetDatum(buf),
+ ObjectIdGetDatum(0),
+ Int32GetDatum(-1));
Assert(i == PG_STAT_GET_VACUUM_TABLES_STATS_COLS);
@@ -2420,13 +2428,14 @@ pg_stat_get_vacuum_tables(PG_FUNCTION_ARGS)
Datum
pg_stat_get_vacuum_indexes(PG_FUNCTION_ARGS)
{
-#define PG_STAT_GET_VACUUM_INDEX_STATS_COLS 3
+#define PG_STAT_GET_VACUUM_INDEX_STATS_COLS 6
Oid relid = PG_GETARG_OID(0);
PgStat_VacuumRelationCounts *extvacuum;
TupleDesc tupdesc;
Datum values[PG_STAT_GET_VACUUM_INDEX_STATS_COLS] = {0};
bool nulls[PG_STAT_GET_VACUUM_INDEX_STATS_COLS] = {0};
+ char buf[256];
int i = 0;
if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
@@ -2448,8 +2457,58 @@ pg_stat_get_vacuum_indexes(PG_FUNCTION_ARGS)
values[i++] = Int64GetDatum(extvacuum->index.pages_deleted);
values[i++] = Int64GetDatum(extvacuum->common.tuples_deleted);
+ values[i++] = Int64GetDatum(extvacuum->common.wal_records);
+ values[i++] = Int64GetDatum(extvacuum->common.wal_fpi);
+ snprintf(buf, sizeof buf, UINT64_FORMAT, extvacuum->common.wal_bytes);
+ values[i++] = DirectFunctionCall3(numeric_in,
+ CStringGetDatum(buf),
+ ObjectIdGetDatum(0),
+ Int32GetDatum(-1));
Assert(i == PG_STAT_GET_VACUUM_INDEX_STATS_COLS);
/* Returns the record as Datum */
PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
}
+
+/*
+ * Get the extended vacuum statistics for a database.
+ */
+Datum
+pg_stat_get_vacuum_database(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_GET_VACUUM_DATABASE_STATS_COLS 5
+
+ Oid dbid = PG_GETARG_OID(0);
+ PgStat_VacuumDBCounts *extvacuum;
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_GET_VACUUM_DATABASE_STATS_COLS] = {0};
+ bool nulls[PG_STAT_GET_VACUUM_DATABASE_STATS_COLS] = {0};
+ char buf[256];
+ int i = 0;
+
+ if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
+ elog(ERROR, "return type must be a row type");
+
+ extvacuum = OidIsValid(dbid) ? pgstat_fetch_stat_vacuum_dbentry(dbid) : NULL;
+ if (!extvacuum)
+ {
+ InitMaterializedSRF(fcinfo, 0);
+ PG_RETURN_VOID();
+ }
+
+ values[i++] = ObjectIdGetDatum(dbid);
+
+ values[i++] = Int32GetDatum(extvacuum->errors);
+
+ values[i++] = Int64GetDatum(extvacuum->common.wal_records);
+ values[i++] = Int64GetDatum(extvacuum->common.wal_fpi);
+ snprintf(buf, sizeof buf, UINT64_FORMAT, extvacuum->common.wal_bytes);
+ values[i++] = DirectFunctionCall3(numeric_in,
+ CStringGetDatum(buf),
+ ObjectIdGetDatum(0),
+ Int32GetDatum(-1));
+ Assert(i == PG_STAT_GET_VACUUM_DATABASE_STATS_COLS);
+
+ /* Returns the record as Datum */
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 6d683413a4..3dd3d81a13 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12643,9 +12643,9 @@
proname => 'pg_stat_get_vacuum_tables', prorows => 1000, provolatile => 's', prorettype => 'record', proisstrict => 'f',
proretset => 't',
proargtypes => 'oid',
- proallargtypes => '{oid,oid,int8,int8,int8,int8,int8,int8,int8}',
- proargmodes => '{i,o,o,o,o,o,o,o,o}',
- proargnames => '{reloid,relid,pages_scanned,pages_removed,tuples_deleted,tuples_frozen,recently_dead_tuples,missed_dead_pages,missed_dead_tuples}',
+ proallargtypes => '{oid,oid,int8,int8,int8,int8,int8,int8,int8,int8,int8,numeric}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o,o,o,o}',
+ proargnames => '{reloid,relid,pages_scanned,pages_removed,tuples_deleted,tuples_frozen,recently_dead_tuples,missed_dead_pages,missed_dead_tuples,wal_records,wal_fpi,wal_bytes}',
prosrc => 'pg_stat_get_vacuum_tables' }
# oid8 related functions
@@ -12718,8 +12718,17 @@
proname => 'pg_stat_get_vacuum_indexes', prorows => 1000, provolatile => 's', prorettype => 'record', proisstrict => 'f',
proretset => 't',
proargtypes => 'oid',
- proallargtypes => '{oid,oid,int8,int8}',
- proargmodes => '{i,o,o,o}',
- proargnames => '{reloid,relid,pages_deleted,tuples_deleted}',
+ proallargtypes => '{oid,oid,int8,int8,int8,int8,numeric}',
+ proargmodes => '{i,o,o,o,o,o,o}',
+ proargnames => '{reloid,relid,pages_deleted,tuples_deleted,wal_records,wal_fpi,wal_bytes}',
prosrc => 'pg_stat_get_vacuum_indexes' },
+{ oid => '8005',
+ descr => 'pg_stat_get_vacuum_database returns vacuum stats values for database',
+ proname => 'pg_stat_get_vacuum_database', prorows => 1000, provolatile => 's', prorettype => 'record', proisstrict => 'f',
+ proretset => 't',
+ proargtypes => 'oid',
+ proallargtypes => '{oid,oid,int4,int8,int8,numeric}',
+ proargmodes => '{i,o,o,o,o,o}',
+ proargnames => '{dbid,dboid,errors,wal_records,wal_fpi,wal_bytes}',
+ prosrc => 'pg_stat_get_vacuum_database' },
]
diff --git a/src/include/commands/vacuum.h b/src/include/commands/vacuum.h
index 956d9cea36..e3808b8587 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
@@ -321,6 +323,26 @@ typedef struct PVWorkerUsage
PVWorkerStats cleanup;
} PVWorkerUsage;
+/*
+ * Counters and usage data for extended stats tracking.
+ */
+typedef struct LVExtStatCounters
+{
+ TimestampTz starttime;
+ WalUsage walusage;
+ BufferUsage bufusage;
+ double VacuumDelayTime;
+ PgStat_Counter blocks_fetched;
+ PgStat_Counter blocks_hit;
+} LVExtStatCounters;
+
+typedef struct LVExtStatCountersIdx
+{
+ LVExtStatCounters common;
+ int64 pages_deleted;
+ int64 tuples_removed;
+} LVExtStatCountersIdx;
+
/* GUC parameters */
extern PGDLLIMPORT int default_statistics_target; /* PGDLLIMPORT for PostGIS */
extern PGDLLIMPORT int vacuum_freeze_min_age;
@@ -353,6 +375,7 @@ extern PGDLLIMPORT double vacuum_max_eager_freeze_failure_rate;
extern PGDLLIMPORT pg_atomic_uint32 *VacuumSharedCostBalance;
extern PGDLLIMPORT pg_atomic_uint32 *VacuumActiveNWorkers;
extern PGDLLIMPORT int VacuumCostBalanceLocal;
+extern PGDLLIMPORT double VacuumDelayTime;
extern PGDLLIMPORT bool VacuumFailsafeActive;
extern PGDLLIMPORT double vacuum_cost_delay;
@@ -409,7 +432,8 @@ extern ParallelVacuumState *parallel_vacuum_init(Relation rel, Relation *indrels
int nindexes, int nrequested_workers,
int vac_work_mem, int elevel,
BufferAccessStrategy bstrategy);
-extern void parallel_vacuum_end(ParallelVacuumState *pvs, IndexBulkDeleteResult **istats);
+extern void parallel_vacuum_end(ParallelVacuumState *pvs, IndexBulkDeleteResult **istats,
+ PgStat_VacuumRelationCounts *idx_heap_total);
extern TidStore *parallel_vacuum_get_dead_items(ParallelVacuumState *pvs,
VacDeadItemsInfo **dead_items_info_p);
extern void parallel_vacuum_reset_dead_items(ParallelVacuumState *pvs);
@@ -439,4 +463,10 @@ extern double anl_random_fract(void);
extern double anl_init_selection_state(int n);
extern double anl_get_next_S(double t, int n, double *stateptr);
+extern void extvac_stats_start_idx(Relation rel, IndexBulkDeleteResult *stats,
+ LVExtStatCountersIdx * counters);
+extern void extvac_stats_end_idx(Relation rel, IndexBulkDeleteResult *stats,
+ LVExtStatCountersIdx * counters, PgStat_VacuumRelationCounts * report);
+extern void extvac_accumulate_idx_report(PgStat_VacuumRelationCounts * dst,
+ const PgStat_VacuumRelationCounts * src);
#endif /* VACUUM_H */
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index bdcf758441..f704a15003 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -175,6 +175,11 @@ typedef struct PgStat_TableCounts
typedef struct PgStat_CommonCounts
{
+ /* WAL */
+ int64 wal_records;
+ int64 wal_fpi;
+ uint64 wal_bytes;
+
/* tuples */
int64 tuples_deleted;
} PgStat_CommonCounts;
@@ -237,6 +242,13 @@ typedef struct PgStat_VacuumRelationStatus
PgStat_VacuumRelationCounts counts; /* event counts to be sent */
} PgStat_VacuumRelationStatus;
+typedef struct PgStat_VacuumDBCounts
+{
+ Oid dbjid;
+ PgStat_CommonCounts common;
+ int32 errors;
+} PgStat_VacuumDBCounts;
+
/* ----------
* PgStat_TableStatus Per-table status within a backend
*
@@ -917,11 +929,13 @@ extern int pgstat_get_transactional_drops(bool isCommit, struct xl_xact_stats_it
extern void pgstat_execute_transactional_drops(int ndrops, struct xl_xact_stats_item *items, bool is_redo);
+extern void pgstat_drop_vacuum_database(Oid databaseid);
extern void pgstat_vacuum_relation_delete_pending_cb(Oid relid);
extern void
pgstat_report_vacuum_extstats(Oid tableoid, bool shared,
PgStat_VacuumRelationCounts * params);
extern PgStat_VacuumRelationCounts * pgstat_fetch_stat_vacuum_tabentry(Oid relid, Oid dbid);
+extern PgStat_VacuumDBCounts * pgstat_fetch_stat_vacuum_dbentry(Oid dbid);
/*
* Functions in pgstat_wal.c
@@ -940,6 +954,7 @@ extern PGDLLIMPORT bool pgstat_track_counts;
extern PGDLLIMPORT int pgstat_track_functions;
extern PGDLLIMPORT int pgstat_fetch_consistency;
extern PGDLLIMPORT bool pgstat_track_vacuum_statistics;
+extern PGDLLIMPORT bool pgstat_track_vacuum_statistics_for_relations;
/*
* Variables in pgstat_bgwriter.c
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index 46a127af2b..a234c797e0 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -507,6 +507,12 @@ typedef struct PgStatShared_Relation
PgStat_StatTabEntry stats;
} PgStatShared_Relation;
+typedef struct PgStatShared_VacuumDB
+{
+ PgStatShared_Common header;
+ PgStat_VacuumDBCounts stats;
+} PgStatShared_VacuumDB;
+
typedef struct PgStatShared_VacuumRelation
{
PgStatShared_Common header;
@@ -695,6 +701,7 @@ extern void *pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid,
bool *may_free);
extern void pgstat_snapshot_fixed(PgStat_Kind kind);
+bool pgstat_vacuum_db_flush_cb(PgStat_EntryRef *entry_ref, bool nowait);
extern bool pgstat_vacuum_relation_flush_cb(PgStat_EntryRef *entry_ref, bool nowait);
diff --git a/src/include/utils/pgstat_kind.h b/src/include/utils/pgstat_kind.h
index f92149066c..52d9367fbe 100644
--- a/src/include/utils/pgstat_kind.h
+++ b/src/include/utils/pgstat_kind.h
@@ -40,9 +40,10 @@
#define PGSTAT_KIND_SLRU 12
#define PGSTAT_KIND_WAL 13
#define PGSTAT_KIND_VACUUM_RELATION 14
+#define PGSTAT_KIND_VACUUM_DB 15
#define PGSTAT_KIND_BUILTIN_MIN PGSTAT_KIND_DATABASE
-#define PGSTAT_KIND_BUILTIN_MAX PGSTAT_KIND_VACUUM_RELATION
+#define PGSTAT_KIND_BUILTIN_MAX PGSTAT_KIND_VACUUM_DB
#define PGSTAT_KIND_BUILTIN_SIZE (PGSTAT_KIND_BUILTIN_MAX + 1)
/* Custom stats kinds */
diff --git a/src/test/modules/test_misc/meson.build b/src/test/modules/test_misc/meson.build
index 267145163a..805c6c2c39 100644
--- a/src/test/modules/test_misc/meson.build
+++ b/src/test/modules/test_misc/meson.build
@@ -23,6 +23,7 @@ tests += {
't/012_ddlutils.pl',
't/013_temp_obj_multisession.pl',
't/014_vacuum_stats.pl',
+ 't/016_vacuum_stats_parallel.pl',
],
# The injection points are cluster-wide, so disable installcheck
'runningcheck': false,
diff --git a/src/test/modules/test_misc/t/016_vacuum_stats_parallel.pl b/src/test/modules/test_misc/t/016_vacuum_stats_parallel.pl
new file mode 100644
index 0000000000..8b23e138fa
--- /dev/null
+++ b/src/test/modules/test_misc/t/016_vacuum_stats_parallel.pl
@@ -0,0 +1,104 @@
+# Copyright (c) 2024-2026, PostgreSQL Global Development Group
+
+# Test that per-index vacuum statistics are collected for indexes that are
+# vacuumed through the PARALLEL path.
+#
+# When VACUUM distributes index cleanup to parallel workers, the WAL/buffer
+# usage and the tuple counters of each index are produced in a different
+# process than the leader. The sampling machinery has to gather those numbers
+# from the workers and attribute them to the right index in pg_stat_vacuum_indexes
+# -- otherwise the index work is silently lost (or folded into the heap's
+# figures). A regression test cannot guarantee that the parallel path is taken,
+# so this is done in a TAP cluster where we can force it and verify, from
+# VACUUM (VERBOSE) output, that a worker was actually launched.
+
+use strict;
+use warnings FATAL => 'all';
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+my $node = PostgreSQL::Test::Cluster->new('main');
+$node->init;
+$node->append_conf(
+ 'postgresql.conf', qq[
+autovacuum = off
+track_vacuum_statistics = on
+# Make every index eligible for the parallel path and make sure workers can run.
+min_parallel_index_scan_size = 0
+max_parallel_maintenance_workers = 4
+max_worker_processes = 8
+max_parallel_workers = 8
+]);
+$node->start;
+
+# Build the relation that we will vacuum in parallel. Two indexes are needed
+# so the parallel path has more than one index to distribute, and the table is
+# large enough that the leftover indexes are worth scanning.
+my $setup = sub {
+ my ($tab) = @_;
+ $node->safe_psql(
+ 'postgres', qq[
+CREATE TABLE $tab (id int, val int) WITH (autovacuum_enabled = off);
+INSERT INTO $tab SELECT g, g FROM generate_series(1, 100000) g;
+CREATE INDEX ${tab}_i1 ON $tab (id);
+CREATE INDEX ${tab}_i2 ON $tab (val);
+DELETE FROM $tab WHERE id % 2 = 0; -- 50000 dead index entries per index
+]);
+};
+
+# Per-index statistics of $tab, with the table-specific prefix stripped from the
+# index name so the parallel and serial relations can be compared directly. We
+# compare the counters that are fully determined by the index content and are
+# therefore identical no matter which process did the work: the number of
+# removed index entries, the number of deleted index pages and the number of WAL
+# records. (wal_fpi/wal_bytes are left out: full-page images depend on when the
+# last checkpoint happened, so they legitimately differ between the two runs.)
+my $index_stats = sub {
+ my ($tab) = @_;
+ return $node->safe_psql(
+ 'postgres', qq[
+SELECT string_agg(
+ format('%s:%s:%s:%s', replace(indexrelname, '$tab', 'tab'),
+ tuples_deleted, pages_deleted, wal_records),
+ ',' ORDER BY indexrelname)
+ FROM pg_stat_vacuum_indexes WHERE relname = '$tab']);
+};
+
+# --- Parallel vacuum -------------------------------------------------------
+$setup->('vacparstat_par');
+
+# Run with VERBOSE so we can confirm a parallel worker was really launched;
+# without that confirmation the test could silently degrade to a serial vacuum.
+my ($ret, $stdout, $stderr) =
+ $node->psql('postgres', 'VACUUM (VERBOSE, PARALLEL 2) vacparstat_par');
+is($ret, 0, 'parallel VACUUM succeeded');
+like(
+ $stderr,
+ qr/launched [1-9]\d* parallel vacuum worker.* for index vacuuming/,
+ 'parallel index vacuum actually launched a worker');
+
+# --- Serial vacuum (control) -----------------------------------------------
+# The same workload vacuumed without parallelism.
+$setup->('vacparstat_ser');
+$node->safe_psql('postgres', 'VACUUM (PARALLEL 0) vacparstat_ser');
+
+$node->safe_psql('postgres', 'SELECT pg_stat_force_next_flush()');
+
+my $par = $index_stats->('vacparstat_par');
+my $ser = $index_stats->('vacparstat_ser');
+
+# Sanity-check the parallel numbers so the equality assertion below cannot pass
+# just because both paths captured nothing: 50000 removed entries and some WAL.
+like(
+ $par,
+ qr/^tab_i1:50000:\d+:[1-9]\d*,tab_i2:50000:\d+:[1-9]\d*$/,
+ 'parallel index vacuum captured the expected per-index counters');
+
+# The whole point: the parallel path must attribute exactly the same per-index
+# statistics to each index as the leader-only path does.
+is($par, $ser,
+ 'parallel and serial index vacuum capture identical per-index statistics');
+
+$node->stop;
+done_testing();
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index a2b0472a2d..9b35f0779e 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -2421,18 +2421,29 @@ pg_stat_user_tables| SELECT relid,
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_vacuum_database| SELECT d.oid AS dboid,
+ d.datname AS dbname,
+ s.errors,
+ s.wal_records,
+ s.wal_fpi,
+ s.wal_bytes
+ FROM pg_database d,
+ LATERAL pg_stat_get_vacuum_database(d.oid) s(dboid, errors, wal_records, wal_fpi, wal_bytes);
pg_stat_vacuum_indexes| SELECT c.oid AS relid,
i.oid AS indexrelid,
n.nspname AS schemaname,
c.relname,
i.relname AS indexrelname,
s.pages_deleted,
- s.tuples_deleted
+ s.tuples_deleted,
+ s.wal_records,
+ s.wal_fpi,
+ s.wal_bytes
FROM (((pg_class c
JOIN pg_index x ON ((c.oid = x.indrelid)))
JOIN pg_class i ON ((i.oid = x.indexrelid)))
LEFT JOIN pg_namespace n ON ((n.oid = c.relnamespace))),
- LATERAL pg_stat_get_vacuum_indexes(i.oid) s(relid, pages_deleted, tuples_deleted)
+ LATERAL pg_stat_get_vacuum_indexes(i.oid) s(relid, pages_deleted, tuples_deleted, wal_records, wal_fpi, wal_bytes)
WHERE (c.relkind = ANY (ARRAY['r'::"char", 't'::"char", 'm'::"char"]));
pg_stat_vacuum_tables| SELECT n.nspname AS schemaname,
c.relname,
@@ -2443,10 +2454,13 @@ pg_stat_vacuum_tables| SELECT n.nspname AS schemaname,
s.tuples_frozen,
s.recently_dead_tuples,
s.missed_dead_pages,
- s.missed_dead_tuples
+ s.missed_dead_tuples,
+ s.wal_records,
+ s.wal_fpi,
+ s.wal_bytes
FROM (pg_class c
JOIN pg_namespace n ON ((n.oid = c.relnamespace))),
- LATERAL pg_stat_get_vacuum_tables(c.oid) s(relid, pages_scanned, pages_removed, tuples_deleted, tuples_frozen, recently_dead_tuples, missed_dead_pages, missed_dead_tuples)
+ LATERAL pg_stat_get_vacuum_tables(c.oid) s(relid, pages_scanned, pages_removed, tuples_deleted, tuples_frozen, recently_dead_tuples, missed_dead_pages, missed_dead_tuples, wal_records, wal_fpi, wal_bytes)
WHERE (c.relkind = ANY (ARRAY['r'::"char", 't'::"char", 'm'::"char"]));
pg_stat_wal| SELECT wal_records,
wal_fpi,
diff --git a/src/test/regress/expected/vacuum_stats.out b/src/test/regress/expected/vacuum_stats.out
index cb11d6381c..64639c75dc 100644
--- a/src/test/regress/expected/vacuum_stats.out
+++ b/src/test/regress/expected/vacuum_stats.out
@@ -84,16 +84,57 @@ SELECT recently_dead_tuples = 0 AS recently_dead_tuples,
t | t | t
(1 row)
+-- WAL metrics. A vacuum that removes tuples always emits WAL
+-- (wal_records > 0, wal_bytes > 0). wal_fpi depends on whether a checkpoint
+-- happened recently, so it is only checked for being non-negative here; the
+-- positive wal_fpi path is exercised separately below.
+SELECT wal_records > 0 AS wal_records,
+ wal_fpi >= 0 AS wal_fpi,
+ wal_bytes > 0 AS wal_bytes
+ FROM pg_stat_vacuum_tables WHERE relname = 'vacstat_t';
+ wal_records | wal_fpi | wal_bytes
+-------------+---------+-----------
+ t | t | t
+(1 row)
+
+-- WAL full-page-image path: a CHECKPOINT immediately before the vacuum forces
+-- the first modification of each page to emit a full page image, so wal_fpi
+-- advances.
+CREATE TABLE vacstat_fpi (id int PRIMARY KEY, v text)
+ WITH (autovacuum_enabled = off);
+INSERT INTO vacstat_fpi SELECT g, repeat('x', 20) FROM generate_series(1, 1000) g;
+DELETE FROM vacstat_fpi WHERE id % 2 = 0;
+CHECKPOINT;
+VACUUM vacstat_fpi;
+SELECT pg_stat_force_next_flush();
+ pg_stat_force_next_flush
+--------------------------
+
+(1 row)
+
+SELECT wal_records > 0 AS wal_records,
+ wal_fpi > 0 AS wal_fpi,
+ wal_bytes > 0 AS wal_bytes
+ FROM pg_stat_vacuum_tables WHERE relname = 'vacstat_fpi';
+ wal_records | wal_fpi | wal_bytes
+-------------+---------+-----------
+ t | t | t
+(1 row)
+
+DROP TABLE vacstat_fpi;
-- per-index view: the primary key index is processed by the same VACUUM.
-- No btree leaf empties out (interleaved deletions), so pages_deleted = 0,
-- while every index entry for a removed heap tuple is deleted.
SELECT indexrelname,
pages_deleted = 0 AS pages_deleted,
- tuples_deleted = 500 AS tuples_deleted
+ tuples_deleted = 500 AS tuples_deleted,
+ wal_records > 0 AS wal_records,
+ wal_fpi >= 0 AS wal_fpi,
+ wal_bytes > 0 AS wal_bytes
FROM pg_stat_vacuum_indexes WHERE relname = 'vacstat_t' ORDER BY indexrelname;
- indexrelname | pages_deleted | tuples_deleted
-----------------+---------------+----------------
- vacstat_t_pkey | t | t
+ indexrelname | pages_deleted | tuples_deleted | wal_records | wal_fpi | wal_bytes
+----------------+---------------+----------------+-------------+---------+-----------
+ vacstat_t_pkey | t | t | t | t | t
(1 row)
-- index page-deletion path: deleting a contiguous key range empties whole
@@ -127,3 +168,43 @@ SELECT indexrelname,
(1 row)
DROP TABLE vacstat_idxdel;
+-- per-database aggregate view: no vacuum errors occurred in this database, and
+-- the vacuums in this database emit WAL (wal_records > 0).
+SELECT errors = 0 AS errors,
+ wal_records > 0 AS wal_records,
+ wal_fpi >= 0 AS wal_fpi,
+ wal_bytes > 0 AS wal_bytes
+ FROM pg_stat_vacuum_database WHERE dbname = current_database();
+ errors | wal_records | wal_fpi | wal_bytes
+--------+-------------+---------+-----------
+ t | t | t | t
+(1 row)
+
+-- parallel index vacuum: index statistics must be captured for indexes
+-- vacuumed through the parallel path, not folded into the heap. Force the
+-- parallel path with min_parallel_index_scan_size = 0.
+SET min_parallel_index_scan_size = 0;
+CREATE TABLE vacstat_par (id int, x int) WITH (autovacuum_enabled = off);
+INSERT INTO vacstat_par SELECT g, g FROM generate_series(1, 50000) g;
+CREATE INDEX vacstat_par_i1 ON vacstat_par (id);
+CREATE INDEX vacstat_par_i2 ON vacstat_par (x);
+DELETE FROM vacstat_par WHERE id % 2 = 0;
+VACUUM (PARALLEL 2) vacstat_par;
+SELECT pg_stat_force_next_flush();
+ pg_stat_force_next_flush
+--------------------------
+
+(1 row)
+
+SELECT indexrelname,
+ tuples_deleted = 25000 AS tuples_deleted,
+ wal_records > 0 AS wal_records
+ FROM pg_stat_vacuum_indexes WHERE relname = 'vacstat_par' ORDER BY indexrelname;
+ indexrelname | tuples_deleted | wal_records
+----------------+----------------+-------------
+ vacstat_par_i1 | t | t
+ vacstat_par_i2 | t | t
+(2 rows)
+
+RESET min_parallel_index_scan_size;
+DROP TABLE vacstat_par;
diff --git a/src/test/regress/sql/vacuum_stats.sql b/src/test/regress/sql/vacuum_stats.sql
index 75a5a0052b..13491b87f0 100644
--- a/src/test/regress/sql/vacuum_stats.sql
+++ b/src/test/regress/sql/vacuum_stats.sql
@@ -55,12 +55,40 @@ SELECT recently_dead_tuples = 0 AS recently_dead_tuples,
missed_dead_tuples = 0 AS missed_dead_tuples
FROM pg_stat_vacuum_tables WHERE relname = 'vacstat_t';
+-- WAL metrics. A vacuum that removes tuples always emits WAL
+-- (wal_records > 0, wal_bytes > 0). wal_fpi depends on whether a checkpoint
+-- happened recently, so it is only checked for being non-negative here; the
+-- positive wal_fpi path is exercised separately below.
+SELECT wal_records > 0 AS wal_records,
+ wal_fpi >= 0 AS wal_fpi,
+ wal_bytes > 0 AS wal_bytes
+ FROM pg_stat_vacuum_tables WHERE relname = 'vacstat_t';
+
+-- WAL full-page-image path: a CHECKPOINT immediately before the vacuum forces
+-- the first modification of each page to emit a full page image, so wal_fpi
+-- advances.
+CREATE TABLE vacstat_fpi (id int PRIMARY KEY, v text)
+ WITH (autovacuum_enabled = off);
+INSERT INTO vacstat_fpi SELECT g, repeat('x', 20) FROM generate_series(1, 1000) g;
+DELETE FROM vacstat_fpi WHERE id % 2 = 0;
+CHECKPOINT;
+VACUUM vacstat_fpi;
+SELECT pg_stat_force_next_flush();
+SELECT wal_records > 0 AS wal_records,
+ wal_fpi > 0 AS wal_fpi,
+ wal_bytes > 0 AS wal_bytes
+ FROM pg_stat_vacuum_tables WHERE relname = 'vacstat_fpi';
+DROP TABLE vacstat_fpi;
+
-- per-index view: the primary key index is processed by the same VACUUM.
-- No btree leaf empties out (interleaved deletions), so pages_deleted = 0,
-- while every index entry for a removed heap tuple is deleted.
SELECT indexrelname,
pages_deleted = 0 AS pages_deleted,
- tuples_deleted = 500 AS tuples_deleted
+ tuples_deleted = 500 AS tuples_deleted,
+ wal_records > 0 AS wal_records,
+ wal_fpi >= 0 AS wal_fpi,
+ wal_bytes > 0 AS wal_bytes
FROM pg_stat_vacuum_indexes WHERE relname = 'vacstat_t' ORDER BY indexrelname;
-- index page-deletion path: deleting a contiguous key range empties whole
@@ -79,3 +107,29 @@ SELECT indexrelname,
tuples_deleted = 9000 AS tuples_deleted
FROM pg_stat_vacuum_indexes WHERE relname = 'vacstat_idxdel' ORDER BY indexrelname;
DROP TABLE vacstat_idxdel;
+
+-- per-database aggregate view: no vacuum errors occurred in this database, and
+-- the vacuums in this database emit WAL (wal_records > 0).
+SELECT errors = 0 AS errors,
+ wal_records > 0 AS wal_records,
+ wal_fpi >= 0 AS wal_fpi,
+ wal_bytes > 0 AS wal_bytes
+ FROM pg_stat_vacuum_database WHERE dbname = current_database();
+
+-- parallel index vacuum: index statistics must be captured for indexes
+-- vacuumed through the parallel path, not folded into the heap. Force the
+-- parallel path with min_parallel_index_scan_size = 0.
+SET min_parallel_index_scan_size = 0;
+CREATE TABLE vacstat_par (id int, x int) WITH (autovacuum_enabled = off);
+INSERT INTO vacstat_par SELECT g, g FROM generate_series(1, 50000) g;
+CREATE INDEX vacstat_par_i1 ON vacstat_par (id);
+CREATE INDEX vacstat_par_i2 ON vacstat_par (x);
+DELETE FROM vacstat_par WHERE id % 2 = 0;
+VACUUM (PARALLEL 2) vacstat_par;
+SELECT pg_stat_force_next_flush();
+SELECT indexrelname,
+ tuples_deleted = 25000 AS tuples_deleted,
+ wal_records > 0 AS wal_records
+ FROM pg_stat_vacuum_indexes WHERE relname = 'vacstat_par' ORDER BY indexrelname;
+RESET min_parallel_index_scan_size;
+DROP TABLE vacstat_par;
--
2.39.5 (Apple Git-154)
From 71989a7bdc2f330f9d6f8aff07a4fba216df9005 Mon Sep 17 00:00:00 2001
From: Alena Rybakina <[email protected]>
Date: Wed, 17 Jun 2026 21:32:19 +0300
Subject: [PATCH 5/9] Extended vacuum statistics: interrupted vacuums and the
wraparound failsafe
Expose two database-/relation-level reliability counters with documentation
and regression coverage:
wraparound_failsafe whether (per relation) or how many times (per
database) a vacuum engaged the wraparound failsafe
interrupts_count number of times a vacuum of a table in the database
was interrupted by an error (database aggregate only)
The wraparound failsafe is recorded as a per-relation flag and summed into
a count at the database level. interrupts_count is reported from the vacuum
error callback straight to shared memory, because an interrupted vacuum
aborts its transaction and a pending entry might never be flushed; it is a
database-wide counter only. The positive wraparound_failsafe path is
covered by a TAP test under src/test/modules/xid_wraparound and the
interrupted-vacuum path by a TAP test under src/test/modules/test_misc.
---
doc/src/sgml/system-views.sgml | 25 +++++++
src/backend/access/heap/vacuumlazy.c | 20 ++++--
src/backend/catalog/system_views.sql | 5 +-
src/backend/utils/activity/pgstat_vacuum.c | 46 ++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 8 ++-
src/backend/utils/error/elog.c | 17 +++++
src/include/catalog/pg_proc.dat | 12 ++--
src/include/pgstat.h | 7 ++
src/include/utils/elog.h | 1 +
src/test/modules/test_misc/meson.build | 1 +
.../t/015_vacuum_stats_interrupts.pl | 71 +++++++++++++++++++
src/test/modules/xid_wraparound/meson.build | 1 +
.../t/005_vacuum_stats_failsafe.pl | 66 +++++++++++++++++
src/test/regress/expected/rules.out | 9 ++-
src/test/regress/expected/vacuum_stats.out | 18 ++++-
src/test/regress/sql/vacuum_stats.sql | 8 +++
16 files changed, 295 insertions(+), 20 deletions(-)
create mode 100644 src/test/modules/test_misc/t/015_vacuum_stats_interrupts.pl
create mode 100644 src/test/modules/xid_wraparound/t/005_vacuum_stats_failsafe.pl
diff --git a/doc/src/sgml/system-views.sgml b/doc/src/sgml/system-views.sgml
index 7c95dcea6b..8bca17f3ef 100644
--- a/doc/src/sgml/system-views.sgml
+++ b/doc/src/sgml/system-views.sgml
@@ -5892,6 +5892,14 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx
Number of dead tuples that the vacuum could not remove because it failed to acquire a cleanup lock on their page.
</para></entry>
</row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>wraparound_failsafe</structfield> <type>integer</type>
+ </para>
+ <para>
+ Number of times the failsafe mechanism was triggered to prevent transaction ID wraparound during the vacuum.
+ </para></entry>
+ </row>
<row>
<entry role="catalog_table_entry"><para role="column_definition">
<structfield>wal_records</structfield> <type>bigint</type>
@@ -6089,6 +6097,14 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx
Number of vacuum operations in this database that failed with an error.
</para></entry>
</row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>wraparound_failsafe</structfield> <type>integer</type>
+ </para>
+ <para>
+ Number of vacuum operations in this database that engaged the wraparound failsafe mechanism.
+ </para></entry>
+ </row>
<row>
<entry role="catalog_table_entry"><para role="column_definition">
<structfield>wal_records</structfield> <type>bigint</type>
@@ -6113,6 +6129,15 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx
Total amount of WAL generated by vacuum operations in this database, in bytes.
</para></entry>
</row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>interrupts_count</structfield> <type>integer</type>
+ </para>
+ <para>
+ Number of times a vacuum of a table in this database was interrupted by
+ an error.
+ </para></entry>
+ </row>
</tbody>
</tgroup>
</table>
diff --git a/src/backend/access/heap/vacuumlazy.c b/src/backend/access/heap/vacuumlazy.c
index c06c538f57..5089656c18 100644
--- a/src/backend/access/heap/vacuumlazy.c
+++ b/src/backend/access/heap/vacuumlazy.c
@@ -412,9 +412,8 @@ typedef struct LVRelState
*/
BlockNumber eager_scan_remaining_fails;
- int32 wraparound_failsafe_count; /* number of emergency vacuums to
- * prevent anti-wraparound
- * shutdown */
+ bool wraparound_failsafe; /* did this vacuum engage the
+ * wraparound failsafe? */
PgStat_VacuumRelationCounts extVacReportIdx;
@@ -630,6 +629,8 @@ accumulate_heap_vacuum_statistics(LVRelState *vacrel, PgStat_VacuumRelationCount
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->common.wraparound_failsafe_count = vacrel->wraparound_failsafe;
+
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;
@@ -1030,7 +1031,7 @@ heap_vacuum_rel(Relation rel, const VacuumParams *params,
vacrel->aggressive = vacuum_get_cutoffs(rel, params, &vacrel->cutoffs);
vacrel->rel_pages = orig_rel_pages = RelationGetNumberOfBlocks(rel);
vacrel->vistest = GlobalVisTestFor(rel);
- vacrel->wraparound_failsafe_count = 0;
+ vacrel->wraparound_failsafe = false;
/* Initialize state used to track oldest extant XID/MXID */
vacrel->NewRelfrozenXid = vacrel->cutoffs.OldestXmin;
@@ -3160,7 +3161,7 @@ lazy_check_wraparound_failsafe(LVRelState *vacrel)
int64 progress_val[3] = {0, 0, PROGRESS_VACUUM_MODE_FAILSAFE};
VacuumFailsafeActive = true;
- vacrel->wraparound_failsafe_count++;
+ vacrel->wraparound_failsafe = true;
/*
* Abandon use of a buffer access strategy to allow use of all of
@@ -4113,6 +4114,15 @@ vacuum_error_callback(void *arg)
{
LVRelState *errinfo = arg;
+ /*
+ * If an actual ERROR (not a lower-severity report that merely carries this
+ * vacuum error context) is being raised while we have a relation in hand,
+ * record at the database level that a vacuum was interrupted. Any error
+ * here aborts the vacuum, so the exact phase does not matter.
+ */
+ if (errinfo->rel != NULL && geterrlevel() == ERROR)
+ pgstat_report_vacuum_error(errinfo->rel->rd_rel->relisshared);
+
switch (errinfo->phase)
{
case VACUUM_ERRCB_PHASE_SCAN_HEAP:
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index 47d6aa6aea..837e78d292 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -1573,6 +1573,7 @@ CREATE VIEW pg_stat_vacuum_tables AS
S.recently_dead_tuples AS recently_dead_tuples,
S.missed_dead_pages AS missed_dead_pages,
S.missed_dead_tuples AS missed_dead_tuples,
+ S.wraparound_failsafe AS wraparound_failsafe,
S.wal_records AS wal_records,
S.wal_fpi AS wal_fpi,
S.wal_bytes AS wal_bytes
@@ -1611,9 +1612,11 @@ CREATE VIEW pg_stat_vacuum_database AS
S.errors AS errors,
+ S.wraparound_failsafe AS wraparound_failsafe,
S.wal_records AS wal_records,
S.wal_fpi AS wal_fpi,
- S.wal_bytes AS wal_bytes
+ S.wal_bytes AS wal_bytes,
+ S.interrupts_count AS interrupts_count
FROM
pg_database D,
LATERAL pg_stat_get_vacuum_database(D.oid) S;
diff --git a/src/backend/utils/activity/pgstat_vacuum.c b/src/backend/utils/activity/pgstat_vacuum.c
index 99c4932ded..8e099f3ade 100644
--- a/src/backend/utils/activity/pgstat_vacuum.c
+++ b/src/backend/utils/activity/pgstat_vacuum.c
@@ -43,6 +43,7 @@ pgstat_accumulate_common(PgStat_CommonCounts *dst, const PgStat_CommonCounts *sr
ACCUMULATE_FIELD(wal_bytes);
ACCUMULATE_FIELD(tuples_deleted);
+ ACCUMULATE_FIELD(interrupts_count);
}
/*
@@ -64,6 +65,13 @@ pgstat_accumulate_extvac_stats_relations(PgStat_VacuumRelationCounts *dst,
pgstat_accumulate_common(&dst->common, &src->common);
+ /*
+ * The wraparound failsafe is a per-relation flag (0/1), not a running
+ * count: reflect whether the latest vacuum of this relation engaged it,
+ * rather than summing across vacuums.
+ */
+ dst->common.wraparound_failsafe_count = src->common.wraparound_failsafe_count;
+
if (dst->type == PGSTAT_EXTVAC_TABLE)
{
ACCUMULATE_SUBFIELD(table, pages_scanned);
@@ -90,6 +98,12 @@ pgstat_accumulate_extvac_stats_db(PgStat_VacuumDBCounts *dst,
return;
pgstat_accumulate_common(&dst->common, &src->common);
+
+ /*
+ * At the database level the failsafe is a count: how many relation vacuums
+ * engaged the wraparound failsafe.
+ */
+ dst->common.wraparound_failsafe_count += src->common.wraparound_failsafe_count;
dst->errors += src->errors;
}
@@ -126,6 +140,38 @@ pgstat_report_vacuum_extstats(Oid tableoid, bool shared,
dboid, InvalidOid, NULL);
dbpending = (PgStat_VacuumDBCounts *) entry_ref->pending;
pgstat_accumulate_common(&dbpending->common, ¶ms->common);
+ /* count this relation's failsafe flag into the database-wide total */
+ dbpending->common.wraparound_failsafe_count += params->common.wraparound_failsafe_count;
+}
+
+/*
+ * Report that a vacuum was interrupted by an error.
+ *
+ * This is a database-wide counter only: an interrupted vacuum aborts its
+ * transaction, so reporting per-relation would require creating a relation
+ * stats entry from the error path (which the aborting transaction may roll
+ * back). Called from the vacuum error callback, we therefore update shared
+ * memory directly rather than going through pending entries, which might never
+ * be flushed.
+ *
+ * The database id is InvalidOid for shared relations, just as in
+ * pgstat_report_vacuum_extstats(); it must not be hard-coded to MyDatabaseId.
+ */
+void
+pgstat_report_vacuum_error(bool shared)
+{
+ PgStat_EntryRef *entry_ref;
+ PgStatShared_VacuumDB *shdbentry;
+ Oid dboid = (shared ? InvalidOid : MyDatabaseId);
+
+ if (!pgstat_track_vacuum_statistics)
+ return;
+
+ entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_VACUUM_DB,
+ dboid, InvalidOid, false);
+ shdbentry = (PgStatShared_VacuumDB *) entry_ref->shared_stats;
+ shdbentry->stats.common.interrupts_count++;
+ pgstat_unlock_entry(entry_ref);
}
/*
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index aad4427469..7927668bf8 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -2374,7 +2374,7 @@ pg_stat_have_stats(PG_FUNCTION_ARGS)
Datum
pg_stat_get_vacuum_tables(PG_FUNCTION_ARGS)
{
-#define PG_STAT_GET_VACUUM_TABLES_STATS_COLS 11
+#define PG_STAT_GET_VACUUM_TABLES_STATS_COLS 12
Oid relid = PG_GETARG_OID(0);
PgStat_VacuumRelationCounts *extvacuum;
@@ -2408,6 +2408,7 @@ pg_stat_get_vacuum_tables(PG_FUNCTION_ARGS)
values[i++] = Int64GetDatum(extvacuum->table.recently_dead_tuples);
values[i++] = Int64GetDatum(extvacuum->table.missed_dead_pages);
values[i++] = Int64GetDatum(extvacuum->table.missed_dead_tuples);
+ values[i++] = Int32GetDatum(extvacuum->common.wraparound_failsafe_count);
values[i++] = Int64GetDatum(extvacuum->common.wal_records);
values[i++] = Int64GetDatum(extvacuum->common.wal_fpi);
snprintf(buf, sizeof buf, UINT64_FORMAT, extvacuum->common.wal_bytes);
@@ -2476,7 +2477,7 @@ pg_stat_get_vacuum_indexes(PG_FUNCTION_ARGS)
Datum
pg_stat_get_vacuum_database(PG_FUNCTION_ARGS)
{
-#define PG_STAT_GET_VACUUM_DATABASE_STATS_COLS 5
+#define PG_STAT_GET_VACUUM_DATABASE_STATS_COLS 7
Oid dbid = PG_GETARG_OID(0);
PgStat_VacuumDBCounts *extvacuum;
@@ -2500,6 +2501,8 @@ pg_stat_get_vacuum_database(PG_FUNCTION_ARGS)
values[i++] = Int32GetDatum(extvacuum->errors);
+ values[i++] = Int32GetDatum(extvacuum->common.wraparound_failsafe_count);
+
values[i++] = Int64GetDatum(extvacuum->common.wal_records);
values[i++] = Int64GetDatum(extvacuum->common.wal_fpi);
snprintf(buf, sizeof buf, UINT64_FORMAT, extvacuum->common.wal_bytes);
@@ -2507,6 +2510,7 @@ pg_stat_get_vacuum_database(PG_FUNCTION_ARGS)
CStringGetDatum(buf),
ObjectIdGetDatum(0),
Int32GetDatum(-1));
+ values[i++] = Int32GetDatum(extvacuum->common.interrupts_count);
Assert(i == PG_STAT_GET_VACUUM_DATABASE_STATS_COLS);
/* Returns the record as Datum */
diff --git a/src/backend/utils/error/elog.c b/src/backend/utils/error/elog.c
index a6936a0c66..04f5b0a819 100644
--- a/src/backend/utils/error/elog.c
+++ b/src/backend/utils/error/elog.c
@@ -1815,6 +1815,23 @@ getinternalerrposition(void)
return edata->internalpos;
}
+/*
+ * geterrlevel --- return the elevel of the error currently being constructed
+ *
+ * This is only intended for use in error callback subroutines, where it lets
+ * a callback tell a genuine error apart from a lower-severity report.
+ */
+int
+geterrlevel(void)
+{
+ ErrorData *edata = &errordata[errordata_stack_depth];
+
+ /* we don't bother incrementing recursion_depth */
+ CHECK_STACK_DEPTH();
+
+ return edata->elevel;
+}
+
/*
* Functions to allow construction of error message strings separately from
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3dd3d81a13..51cd0e5ed1 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12643,9 +12643,9 @@
proname => 'pg_stat_get_vacuum_tables', prorows => 1000, provolatile => 's', prorettype => 'record', proisstrict => 'f',
proretset => 't',
proargtypes => 'oid',
- proallargtypes => '{oid,oid,int8,int8,int8,int8,int8,int8,int8,int8,int8,numeric}',
- proargmodes => '{i,o,o,o,o,o,o,o,o,o,o,o}',
- proargnames => '{reloid,relid,pages_scanned,pages_removed,tuples_deleted,tuples_frozen,recently_dead_tuples,missed_dead_pages,missed_dead_tuples,wal_records,wal_fpi,wal_bytes}',
+ proallargtypes => '{oid,oid,int8,int8,int8,int8,int8,int8,int8,int4,int8,int8,numeric}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o,o,o,o,o}',
+ proargnames => '{reloid,relid,pages_scanned,pages_removed,tuples_deleted,tuples_frozen,recently_dead_tuples,missed_dead_pages,missed_dead_tuples,wraparound_failsafe,wal_records,wal_fpi,wal_bytes}',
prosrc => 'pg_stat_get_vacuum_tables' }
# oid8 related functions
@@ -12727,8 +12727,8 @@
proname => 'pg_stat_get_vacuum_database', prorows => 1000, provolatile => 's', prorettype => 'record', proisstrict => 'f',
proretset => 't',
proargtypes => 'oid',
- proallargtypes => '{oid,oid,int4,int8,int8,numeric}',
- proargmodes => '{i,o,o,o,o,o}',
- proargnames => '{dbid,dboid,errors,wal_records,wal_fpi,wal_bytes}',
+ proallargtypes => '{oid,oid,int4,int4,int8,int8,numeric,int4}',
+ proargmodes => '{i,o,o,o,o,o,o,o}',
+ proargnames => '{dbid,dboid,errors,wraparound_failsafe,wal_records,wal_fpi,wal_bytes,interrupts_count}',
prosrc => 'pg_stat_get_vacuum_database' },
]
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index f704a15003..d041359e72 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -182,6 +182,12 @@ typedef struct PgStat_CommonCounts
/* tuples */
int64 tuples_deleted;
+
+ /* failsafe */
+ int32 wraparound_failsafe_count;
+
+ /* number of times a vacuum of the object was interrupted by an error */
+ int32 interrupts_count;
} PgStat_CommonCounts;
/* ----------
@@ -934,6 +940,7 @@ extern void pgstat_vacuum_relation_delete_pending_cb(Oid relid);
extern void
pgstat_report_vacuum_extstats(Oid tableoid, bool shared,
PgStat_VacuumRelationCounts * params);
+extern void pgstat_report_vacuum_error(bool shared);
extern PgStat_VacuumRelationCounts * pgstat_fetch_stat_vacuum_tabentry(Oid relid, Oid dbid);
extern PgStat_VacuumDBCounts * pgstat_fetch_stat_vacuum_dbentry(Oid dbid);
diff --git a/src/include/utils/elog.h b/src/include/utils/elog.h
index 6ae376ba00..7c1369524e 100644
--- a/src/include/utils/elog.h
+++ b/src/include/utils/elog.h
@@ -230,6 +230,7 @@ extern int internalerrquery(const char *query);
extern int err_generic_string(int field, const char *str);
extern int geterrcode(void);
+extern int geterrlevel(void);
extern int geterrposition(void);
extern int getinternalerrposition(void);
diff --git a/src/test/modules/test_misc/meson.build b/src/test/modules/test_misc/meson.build
index 805c6c2c39..8761d79d70 100644
--- a/src/test/modules/test_misc/meson.build
+++ b/src/test/modules/test_misc/meson.build
@@ -23,6 +23,7 @@ tests += {
't/012_ddlutils.pl',
't/013_temp_obj_multisession.pl',
't/014_vacuum_stats.pl',
+ 't/015_vacuum_stats_interrupts.pl',
't/016_vacuum_stats_parallel.pl',
],
# The injection points are cluster-wide, so disable installcheck
diff --git a/src/test/modules/test_misc/t/015_vacuum_stats_interrupts.pl b/src/test/modules/test_misc/t/015_vacuum_stats_interrupts.pl
new file mode 100644
index 0000000000..80dd2b7b3c
--- /dev/null
+++ b/src/test/modules/test_misc/t/015_vacuum_stats_interrupts.pl
@@ -0,0 +1,71 @@
+# Copyright (c) 2024-2026, PostgreSQL Global Development Group
+
+# Test the interrupts_count counter of pg_stat_vacuum_database.
+#
+# interrupts_count records how many times a vacuum in the database was
+# interrupted by an error. We provoke that by starting a vacuum that sleeps at
+# its cost-based delay points and canceling it, with pg_cancel_backend(), while
+# it is still running.
+
+use strict;
+use warnings FATAL => 'all';
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+my $node = PostgreSQL::Test::Cluster->new('main');
+$node->init;
+$node->append_conf(
+ 'postgresql.conf', qq[
+autovacuum = off
+track_vacuum_statistics = on
+]);
+$node->start;
+
+# fillfactor = 10 spreads the rows over many pages so the vacuum hits enough
+# cost-delay points to stay running until we cancel it.
+$node->safe_psql(
+ 'postgres', qq[
+CREATE TABLE vacstat_int (id int) WITH (autovacuum_enabled = off, fillfactor = 10);
+INSERT INTO vacstat_int SELECT generate_series(1, 1000);
+DELETE FROM vacstat_int;
+]);
+
+# Start a vacuum that sleeps at every cost-delay point, in the background. The
+# \echo lets query_until() return as soon as the VACUUM has been launched.
+my $appname = 'vacuum_interrupt_test';
+my $vac = $node->background_psql('postgres', on_error_stop => 0);
+$vac->query_until(
+ qr/start/, qq[
+SET application_name = '$appname';
+SET vacuum_cost_delay = '100ms';
+SET vacuum_cost_limit = 1;
+\\echo start
+VACUUM vacstat_int;
+]);
+
+# Wait until the vacuum is actually running, then cancel it.
+$node->poll_query_until(
+ 'postgres', qq[
+SELECT count(*) = 1 FROM pg_stat_activity
+ WHERE application_name = '$appname' AND query LIKE 'VACUUM%' AND state = 'active'])
+ or die "timed out waiting for the vacuum to start";
+
+my $cancelled = $node->safe_psql(
+ 'postgres', qq[
+SELECT pg_cancel_backend(pid) FROM pg_stat_activity
+ WHERE application_name = '$appname' AND query LIKE 'VACUUM%']);
+is($cancelled, 't', 'canceled the running vacuum');
+
+$vac->quit;
+like($vac->{stderr}, qr/canceling statement due to user request/,
+ 'vacuum canceled by user request');
+
+is( $node->safe_psql(
+ 'postgres', qq[
+SELECT interrupts_count > 0 FROM pg_stat_vacuum_database WHERE dbname = current_database()]),
+ 't',
+ 'interrupts_count advanced in pg_stat_vacuum_database');
+
+$node->stop;
+done_testing();
diff --git a/src/test/modules/xid_wraparound/meson.build b/src/test/modules/xid_wraparound/meson.build
index 97ce670f9a..9224e59d1d 100644
--- a/src/test/modules/xid_wraparound/meson.build
+++ b/src/test/modules/xid_wraparound/meson.build
@@ -31,6 +31,7 @@ tests += {
't/002_limits.pl',
't/003_wraparounds.pl',
't/004_notify_freeze.pl',
+ 't/005_vacuum_stats_failsafe.pl',
],
},
}
diff --git a/src/test/modules/xid_wraparound/t/005_vacuum_stats_failsafe.pl b/src/test/modules/xid_wraparound/t/005_vacuum_stats_failsafe.pl
new file mode 100644
index 0000000000..eff77fe876
--- /dev/null
+++ b/src/test/modules/xid_wraparound/t/005_vacuum_stats_failsafe.pl
@@ -0,0 +1,66 @@
+# Copyright (c) 2024-2026, PostgreSQL Global Development Group
+
+# Test that the wraparound failsafe counter exposed by the extended vacuum
+# statistics views advances when a VACUUM engages the wraparound failsafe.
+#
+# The failsafe only triggers once a relation's age exceeds
+# max(vacuum_failsafe_age, autovacuum_freeze_max_age * 1.05), which cannot be
+# reached by an ordinary regression test. Here we lower
+# autovacuum_freeze_max_age to its minimum and use the xid_wraparound
+# extension to burn enough transaction IDs to age the table past that
+# threshold, then check that the wraparound_failsafe counters advance.
+
+use strict;
+use warnings FATAL => 'all';
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+if (!$ENV{PG_TEST_EXTRA} || $ENV{PG_TEST_EXTRA} !~ /\bxid_wraparound\b/)
+{
+ plan skip_all => "test xid_wraparound not enabled in PG_TEST_EXTRA";
+}
+
+my $node = PostgreSQL::Test::Cluster->new('main');
+$node->init;
+$node->append_conf(
+ 'postgresql.conf', qq[
+autovacuum = off
+track_vacuum_statistics = on
+# Lower the wraparound-failsafe threshold as far as possible so that a modest
+# number of consumed XIDs is enough to engage the failsafe.
+autovacuum_freeze_max_age = 100000
+vacuum_failsafe_age = 0
+]);
+$node->start;
+$node->safe_psql('postgres', 'CREATE EXTENSION xid_wraparound');
+
+# A table whose relfrozenxid age we will push past the failsafe threshold.
+$node->safe_psql(
+ 'postgres', qq[
+CREATE TABLE fs_tab (id int) WITH (autovacuum_enabled = off);
+INSERT INTO fs_tab SELECT generate_series(1, 1000);
+]);
+
+# Advance the XID counter well past the failsafe threshold.
+$node->safe_psql('postgres', 'SELECT consume_xids(200000)');
+
+# This VACUUM must engage the wraparound failsafe.
+$node->safe_psql('postgres', 'VACUUM fs_tab');
+
+# The per-table view records that the failsafe was engaged for this relation.
+my $tab = $node->safe_psql(
+ 'postgres', qq[
+SELECT wraparound_failsafe > 0
+ FROM pg_stat_vacuum_tables WHERE relname = 'fs_tab']);
+is($tab, 't', 'wraparound_failsafe advanced in pg_stat_vacuum_tables');
+
+# The per-database aggregate counts the failsafe as well.
+my $db = $node->safe_psql(
+ 'postgres', qq[
+SELECT wraparound_failsafe > 0
+ FROM pg_stat_vacuum_database WHERE dbname = current_database()]);
+is($db, 't', 'wraparound_failsafe advanced in pg_stat_vacuum_database');
+
+$node->stop;
+done_testing();
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 9b35f0779e..467c2e1843 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -2424,11 +2424,13 @@ pg_stat_user_tables| SELECT relid,
pg_stat_vacuum_database| SELECT d.oid AS dboid,
d.datname AS dbname,
s.errors,
+ s.wraparound_failsafe,
s.wal_records,
s.wal_fpi,
- s.wal_bytes
+ s.wal_bytes,
+ s.interrupts_count
FROM pg_database d,
- LATERAL pg_stat_get_vacuum_database(d.oid) s(dboid, errors, wal_records, wal_fpi, wal_bytes);
+ LATERAL pg_stat_get_vacuum_database(d.oid) s(dboid, errors, wraparound_failsafe, wal_records, wal_fpi, wal_bytes, interrupts_count);
pg_stat_vacuum_indexes| SELECT c.oid AS relid,
i.oid AS indexrelid,
n.nspname AS schemaname,
@@ -2455,12 +2457,13 @@ pg_stat_vacuum_tables| SELECT n.nspname AS schemaname,
s.recently_dead_tuples,
s.missed_dead_pages,
s.missed_dead_tuples,
+ s.wraparound_failsafe,
s.wal_records,
s.wal_fpi,
s.wal_bytes
FROM (pg_class c
JOIN pg_namespace n ON ((n.oid = c.relnamespace))),
- LATERAL pg_stat_get_vacuum_tables(c.oid) s(relid, pages_scanned, pages_removed, tuples_deleted, tuples_frozen, recently_dead_tuples, missed_dead_pages, missed_dead_tuples, wal_records, wal_fpi, wal_bytes)
+ LATERAL pg_stat_get_vacuum_tables(c.oid) s(relid, pages_scanned, pages_removed, tuples_deleted, tuples_frozen, recently_dead_tuples, missed_dead_pages, missed_dead_tuples, wraparound_failsafe, wal_records, wal_fpi, wal_bytes)
WHERE (c.relkind = ANY (ARRAY['r'::"char", 't'::"char", 'm'::"char"]));
pg_stat_wal| SELECT wal_records,
wal_fpi,
diff --git a/src/test/regress/expected/vacuum_stats.out b/src/test/regress/expected/vacuum_stats.out
index 64639c75dc..c3024e5fc4 100644
--- a/src/test/regress/expected/vacuum_stats.out
+++ b/src/test/regress/expected/vacuum_stats.out
@@ -122,6 +122,17 @@ SELECT wal_records > 0 AS wal_records,
(1 row)
DROP TABLE vacstat_fpi;
+-- wraparound failsafe. A normal vacuum does not engage the wraparound
+-- failsafe (wraparound_failsafe = 0). The positive path requires reaching the
+-- failsafe XID age and is covered by a separate TAP test under
+-- src/test/modules/xid_wraparound.
+SELECT wraparound_failsafe = 0 AS wraparound_failsafe
+ FROM pg_stat_vacuum_tables WHERE relname = 'vacstat_t';
+ wraparound_failsafe
+---------------------
+ t
+(1 row)
+
-- per-index view: the primary key index is processed by the same VACUUM.
-- No btree leaf empties out (interleaved deletions), so pages_deleted = 0,
-- while every index entry for a removed heap tuple is deleted.
@@ -171,13 +182,14 @@ DROP TABLE vacstat_idxdel;
-- per-database aggregate view: no vacuum errors occurred in this database, and
-- the vacuums in this database emit WAL (wal_records > 0).
SELECT errors = 0 AS errors,
+ wraparound_failsafe = 0 AS wraparound_failsafe,
wal_records > 0 AS wal_records,
wal_fpi >= 0 AS wal_fpi,
wal_bytes > 0 AS wal_bytes
FROM pg_stat_vacuum_database WHERE dbname = current_database();
- errors | wal_records | wal_fpi | wal_bytes
---------+-------------+---------+-----------
- t | t | t | t
+ errors | wraparound_failsafe | wal_records | wal_fpi | wal_bytes
+--------+---------------------+-------------+---------+-----------
+ t | t | t | t | t
(1 row)
-- parallel index vacuum: index statistics must be captured for indexes
diff --git a/src/test/regress/sql/vacuum_stats.sql b/src/test/regress/sql/vacuum_stats.sql
index 13491b87f0..8ce60dac77 100644
--- a/src/test/regress/sql/vacuum_stats.sql
+++ b/src/test/regress/sql/vacuum_stats.sql
@@ -80,6 +80,13 @@ SELECT wal_records > 0 AS wal_records,
FROM pg_stat_vacuum_tables WHERE relname = 'vacstat_fpi';
DROP TABLE vacstat_fpi;
+-- wraparound failsafe. A normal vacuum does not engage the wraparound
+-- failsafe (wraparound_failsafe = 0). The positive path requires reaching the
+-- failsafe XID age and is covered by a separate TAP test under
+-- src/test/modules/xid_wraparound.
+SELECT wraparound_failsafe = 0 AS wraparound_failsafe
+ FROM pg_stat_vacuum_tables WHERE relname = 'vacstat_t';
+
-- per-index view: the primary key index is processed by the same VACUUM.
-- No btree leaf empties out (interleaved deletions), so pages_deleted = 0,
-- while every index entry for a removed heap tuple is deleted.
@@ -111,6 +118,7 @@ DROP TABLE vacstat_idxdel;
-- per-database aggregate view: no vacuum errors occurred in this database, and
-- the vacuums in this database emit WAL (wal_records > 0).
SELECT errors = 0 AS errors,
+ wraparound_failsafe = 0 AS wraparound_failsafe,
wal_records > 0 AS wal_records,
wal_fpi >= 0 AS wal_fpi,
wal_bytes > 0 AS wal_bytes
--
2.39.5 (Apple Git-154)
From 11e2d4eac5d1d8225c23c8640454a66064825ca3 Mon Sep 17 00:00:00 2001
From: Alena Rybakina <[email protected]>
Date: Wed, 17 Jun 2026 21:40:32 +0300
Subject: [PATCH 6/9] Extended vacuum statistics: visibility-map page
transitions for tables
Expose the counters that track how the vacuum updated the visibility map of
the table, with documentation and regression coverage:
vm_new_frozen_pages pages newly marked all-frozen in the VM
vm_new_visible_pages pages newly marked all-visible in the VM
vm_new_visible_frozen_pages pages newly marked all-visible and all-frozen
---
doc/src/sgml/system-views.sgml | 24 +++++++++++++
src/backend/access/heap/vacuumlazy.c | 3 ++
src/backend/catalog/system_views.sql | 3 ++
src/backend/utils/activity/pgstat_vacuum.c | 3 ++
src/backend/utils/adt/pgstatfuncs.c | 5 ++-
src/include/catalog/pg_proc.dat | 6 ++--
src/include/pgstat.h | 7 ++++
src/test/regress/expected/rules.out | 5 ++-
src/test/regress/expected/vacuum_stats.out | 39 ++++++++++++++++++++++
src/test/regress/sql/vacuum_stats.sql | 26 +++++++++++++++
10 files changed, 116 insertions(+), 5 deletions(-)
diff --git a/doc/src/sgml/system-views.sgml b/doc/src/sgml/system-views.sgml
index 8bca17f3ef..1be54673bf 100644
--- a/doc/src/sgml/system-views.sgml
+++ b/doc/src/sgml/system-views.sgml
@@ -5892,6 +5892,30 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx
Number of dead tuples that the vacuum could not remove because it failed to acquire a cleanup lock on their page.
</para></entry>
</row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>vm_new_frozen_pages</structfield> <type>bigint</type>
+ </para>
+ <para>
+ Number of heap pages newly marked all-frozen in the visibility map by the vacuum.
+ </para></entry>
+ </row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>vm_new_visible_pages</structfield> <type>bigint</type>
+ </para>
+ <para>
+ Number of heap pages newly marked all-visible in the visibility map by the vacuum.
+ </para></entry>
+ </row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>vm_new_visible_frozen_pages</structfield> <type>bigint</type>
+ </para>
+ <para>
+ Number of heap pages newly marked both all-visible and all-frozen in the visibility map by the vacuum.
+ </para></entry>
+ </row>
<row>
<entry role="catalog_table_entry"><para role="column_definition">
<structfield>wraparound_failsafe</structfield> <type>integer</type>
diff --git a/src/backend/access/heap/vacuumlazy.c b/src/backend/access/heap/vacuumlazy.c
index 5089656c18..46b7418136 100644
--- a/src/backend/access/heap/vacuumlazy.c
+++ b/src/backend/access/heap/vacuumlazy.c
@@ -624,6 +624,9 @@ accumulate_heap_vacuum_statistics(LVRelState *vacrel, PgStat_VacuumRelationCount
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;
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index 837e78d292..a7ac037bc3 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -1573,6 +1573,9 @@ CREATE VIEW pg_stat_vacuum_tables AS
S.recently_dead_tuples AS recently_dead_tuples,
S.missed_dead_pages AS missed_dead_pages,
S.missed_dead_tuples AS missed_dead_tuples,
+ S.vm_new_frozen_pages AS vm_new_frozen_pages,
+ S.vm_new_visible_pages AS vm_new_visible_pages,
+ S.vm_new_visible_frozen_pages AS vm_new_visible_frozen_pages,
S.wraparound_failsafe AS wraparound_failsafe,
S.wal_records AS wal_records,
S.wal_fpi AS wal_fpi,
diff --git a/src/backend/utils/activity/pgstat_vacuum.c b/src/backend/utils/activity/pgstat_vacuum.c
index 8e099f3ade..ec3d50c59c 100644
--- a/src/backend/utils/activity/pgstat_vacuum.c
+++ b/src/backend/utils/activity/pgstat_vacuum.c
@@ -76,6 +76,9 @@ pgstat_accumulate_extvac_stats_relations(PgStat_VacuumRelationCounts *dst,
{
ACCUMULATE_SUBFIELD(table, pages_scanned);
ACCUMULATE_SUBFIELD(table, pages_removed);
+ ACCUMULATE_SUBFIELD(table, vm_new_frozen_pages);
+ ACCUMULATE_SUBFIELD(table, vm_new_visible_pages);
+ ACCUMULATE_SUBFIELD(table, vm_new_visible_frozen_pages);
ACCUMULATE_SUBFIELD(table, missed_dead_pages);
ACCUMULATE_SUBFIELD(table, tuples_frozen);
ACCUMULATE_SUBFIELD(table, recently_dead_tuples);
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 7927668bf8..694fd220b0 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -2374,7 +2374,7 @@ pg_stat_have_stats(PG_FUNCTION_ARGS)
Datum
pg_stat_get_vacuum_tables(PG_FUNCTION_ARGS)
{
-#define PG_STAT_GET_VACUUM_TABLES_STATS_COLS 12
+#define PG_STAT_GET_VACUUM_TABLES_STATS_COLS 15
Oid relid = PG_GETARG_OID(0);
PgStat_VacuumRelationCounts *extvacuum;
@@ -2408,6 +2408,9 @@ pg_stat_get_vacuum_tables(PG_FUNCTION_ARGS)
values[i++] = Int64GetDatum(extvacuum->table.recently_dead_tuples);
values[i++] = Int64GetDatum(extvacuum->table.missed_dead_pages);
values[i++] = Int64GetDatum(extvacuum->table.missed_dead_tuples);
+ values[i++] = Int64GetDatum(extvacuum->table.vm_new_frozen_pages);
+ values[i++] = Int64GetDatum(extvacuum->table.vm_new_visible_pages);
+ values[i++] = Int64GetDatum(extvacuum->table.vm_new_visible_frozen_pages);
values[i++] = Int32GetDatum(extvacuum->common.wraparound_failsafe_count);
values[i++] = Int64GetDatum(extvacuum->common.wal_records);
values[i++] = Int64GetDatum(extvacuum->common.wal_fpi);
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 51cd0e5ed1..d2fb1b4ec0 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12643,9 +12643,9 @@
proname => 'pg_stat_get_vacuum_tables', prorows => 1000, provolatile => 's', prorettype => 'record', proisstrict => 'f',
proretset => 't',
proargtypes => 'oid',
- proallargtypes => '{oid,oid,int8,int8,int8,int8,int8,int8,int8,int4,int8,int8,numeric}',
- proargmodes => '{i,o,o,o,o,o,o,o,o,o,o,o,o}',
- proargnames => '{reloid,relid,pages_scanned,pages_removed,tuples_deleted,tuples_frozen,recently_dead_tuples,missed_dead_pages,missed_dead_tuples,wraparound_failsafe,wal_records,wal_fpi,wal_bytes}',
+ proallargtypes => '{oid,oid,int8,int8,int8,int8,int8,int8,int8,int8,int8,int8,int4,int8,int8,numeric}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o}',
+ proargnames => '{reloid,relid,pages_scanned,pages_removed,tuples_deleted,tuples_frozen,recently_dead_tuples,missed_dead_pages,missed_dead_tuples,vm_new_frozen_pages,vm_new_visible_pages,vm_new_visible_frozen_pages,wraparound_failsafe,wal_records,wal_fpi,wal_bytes}',
prosrc => 'pg_stat_get_vacuum_tables' }
# oid8 related functions
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index d041359e72..bcc84dbd41 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -233,6 +233,13 @@ typedef struct PgStat_VacuumRelationCounts
int64 missed_dead_tuples; /* tuples not pruned by vacuum due
* to failure to get a cleanup
* lock */
+ int64 vm_new_frozen_pages; /* pages marked in VM as
+ * frozen */
+ int64 vm_new_visible_pages; /* pages marked in VM as
+ * all-visible */
+ int64 vm_new_visible_frozen_pages; /* pages marked in VM as
+ * all-visible and
+ * frozen */
} table;
struct
{
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 467c2e1843..27a17ee0c2 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -2457,13 +2457,16 @@ pg_stat_vacuum_tables| SELECT n.nspname AS schemaname,
s.recently_dead_tuples,
s.missed_dead_pages,
s.missed_dead_tuples,
+ s.vm_new_frozen_pages,
+ s.vm_new_visible_pages,
+ s.vm_new_visible_frozen_pages,
s.wraparound_failsafe,
s.wal_records,
s.wal_fpi,
s.wal_bytes
FROM (pg_class c
JOIN pg_namespace n ON ((n.oid = c.relnamespace))),
- LATERAL pg_stat_get_vacuum_tables(c.oid) s(relid, pages_scanned, pages_removed, tuples_deleted, tuples_frozen, recently_dead_tuples, missed_dead_pages, missed_dead_tuples, wraparound_failsafe, wal_records, wal_fpi, wal_bytes)
+ LATERAL pg_stat_get_vacuum_tables(c.oid) s(relid, pages_scanned, pages_removed, tuples_deleted, tuples_frozen, recently_dead_tuples, missed_dead_pages, missed_dead_tuples, vm_new_frozen_pages, vm_new_visible_pages, vm_new_visible_frozen_pages, wraparound_failsafe, wal_records, wal_fpi, wal_bytes)
WHERE (c.relkind = ANY (ARRAY['r'::"char", 't'::"char", 'm'::"char"]));
pg_stat_wal| SELECT wal_records,
wal_fpi,
diff --git a/src/test/regress/expected/vacuum_stats.out b/src/test/regress/expected/vacuum_stats.out
index c3024e5fc4..c996699804 100644
--- a/src/test/regress/expected/vacuum_stats.out
+++ b/src/test/regress/expected/vacuum_stats.out
@@ -84,6 +84,45 @@ SELECT recently_dead_tuples = 0 AS recently_dead_tuples,
t | t | t
(1 row)
+-- visibility-map page transitions. Removing the interleaved dead tuples lets
+-- VACUUM mark every heap page all-visible (vm_new_visible_pages > 0). Whether
+-- VACUUM also freezes those pages (vm_new_frozen_pages /
+-- vm_new_visible_frozen_pages) depends on opportunistic freezing, which is not
+-- deterministic here, so those are only checked for being non-negative; the
+-- positive freeze path is covered by the dedicated VACUUM (FREEZE) scenario
+-- below.
+SELECT vm_new_frozen_pages >= 0 AS vm_new_frozen_pages,
+ vm_new_visible_pages > 0 AS vm_new_visible_pages,
+ vm_new_visible_frozen_pages >= 0 AS vm_new_visible_frozen_pages
+ FROM pg_stat_vacuum_tables WHERE relname = 'vacstat_t';
+ vm_new_frozen_pages | vm_new_visible_pages | vm_new_visible_frozen_pages
+---------------------+----------------------+-----------------------------
+ t | t | t
+(1 row)
+
+-- freeze path: a dedicated VACUUM (FREEZE) marks freshly-loaded heap pages
+-- all-visible and all-frozen in one pass, so vm_new_visible_frozen_pages
+-- advances. This restores the coverage of the former
+-- 053_vacuum_extending_freeze TAP test.
+CREATE TABLE vacstat_frz (x int)
+ WITH (autovacuum_enabled = off, fillfactor = 10);
+INSERT INTO vacstat_frz SELECT g FROM generate_series(1, 1000) g;
+VACUUM (FREEZE) vacstat_frz;
+SELECT pg_stat_force_next_flush();
+ pg_stat_force_next_flush
+--------------------------
+
+(1 row)
+
+SELECT vm_new_visible_pages > 0 AS vm_new_visible_pages,
+ vm_new_visible_frozen_pages > 0 AS vm_new_visible_frozen_pages
+ FROM pg_stat_vacuum_tables WHERE relname = 'vacstat_frz';
+ vm_new_visible_pages | vm_new_visible_frozen_pages
+----------------------+-----------------------------
+ t | t
+(1 row)
+
+DROP TABLE vacstat_frz;
-- WAL metrics. A vacuum that removes tuples always emits WAL
-- (wal_records > 0, wal_bytes > 0). wal_fpi depends on whether a checkpoint
-- happened recently, so it is only checked for being non-negative here; the
diff --git a/src/test/regress/sql/vacuum_stats.sql b/src/test/regress/sql/vacuum_stats.sql
index 8ce60dac77..f106b7c37b 100644
--- a/src/test/regress/sql/vacuum_stats.sql
+++ b/src/test/regress/sql/vacuum_stats.sql
@@ -55,6 +55,32 @@ SELECT recently_dead_tuples = 0 AS recently_dead_tuples,
missed_dead_tuples = 0 AS missed_dead_tuples
FROM pg_stat_vacuum_tables WHERE relname = 'vacstat_t';
+-- visibility-map page transitions. Removing the interleaved dead tuples lets
+-- VACUUM mark every heap page all-visible (vm_new_visible_pages > 0). Whether
+-- VACUUM also freezes those pages (vm_new_frozen_pages /
+-- vm_new_visible_frozen_pages) depends on opportunistic freezing, which is not
+-- deterministic here, so those are only checked for being non-negative; the
+-- positive freeze path is covered by the dedicated VACUUM (FREEZE) scenario
+-- below.
+SELECT vm_new_frozen_pages >= 0 AS vm_new_frozen_pages,
+ vm_new_visible_pages > 0 AS vm_new_visible_pages,
+ vm_new_visible_frozen_pages >= 0 AS vm_new_visible_frozen_pages
+ FROM pg_stat_vacuum_tables WHERE relname = 'vacstat_t';
+
+-- freeze path: a dedicated VACUUM (FREEZE) marks freshly-loaded heap pages
+-- all-visible and all-frozen in one pass, so vm_new_visible_frozen_pages
+-- advances. This restores the coverage of the former
+-- 053_vacuum_extending_freeze TAP test.
+CREATE TABLE vacstat_frz (x int)
+ WITH (autovacuum_enabled = off, fillfactor = 10);
+INSERT INTO vacstat_frz SELECT g FROM generate_series(1, 1000) g;
+VACUUM (FREEZE) vacstat_frz;
+SELECT pg_stat_force_next_flush();
+SELECT vm_new_visible_pages > 0 AS vm_new_visible_pages,
+ vm_new_visible_frozen_pages > 0 AS vm_new_visible_frozen_pages
+ FROM pg_stat_vacuum_tables WHERE relname = 'vacstat_frz';
+DROP TABLE vacstat_frz;
+
-- WAL metrics. A vacuum that removes tuples always emits WAL
-- (wal_records > 0, wal_bytes > 0). wal_fpi depends on whether a checkpoint
-- happened recently, so it is only checked for being non-negative here; the
--
2.39.5 (Apple Git-154)
From 66aafd7ac70686a164c35bf6bb3b8b757c16cf43 Mon Sep 17 00:00:00 2001
From: Alena Rybakina <[email protected]>
Date: Wed, 17 Jun 2026 21:46:14 +0300
Subject: [PATCH 7/9] Extended vacuum statistics: total shared-buffer access
counters
Expose the shared-buffer access counters in pg_stat_vacuum_tables,
pg_stat_vacuum_indexes and the pg_stat_vacuum_database aggregate, with
documentation and regression coverage:
total_blks_read shared buffer blocks missed (read from disk)
total_blks_hit shared buffer blocks found in the buffer cache (hits)
total_blks_dirtied shared buffer blocks dirtied by this vacuum
total_blks_written shared buffer blocks written out
These counters are common to tables, indexes and the database aggregate
(where the first two are named db_blks_read/db_blks_hit); total_blks_dirtied
counts only the pages dirtied by this vacuum.
---
doc/src/sgml/system-views.sgml | 96 ++++++++++++++++++++++
src/backend/access/heap/vacuumlazy.c | 23 +++++-
src/backend/catalog/system_views.sql | 12 +++
src/backend/utils/activity/pgstat_vacuum.c | 5 ++
src/backend/utils/adt/pgstatfuncs.c | 20 ++++-
src/include/catalog/pg_proc.dat | 18 ++--
src/include/pgstat.h | 6 ++
src/test/regress/expected/rules.out | 18 +++-
src/test/regress/expected/vacuum_stats.out | 42 ++++++++--
src/test/regress/sql/vacuum_stats.sql | 26 +++++-
10 files changed, 240 insertions(+), 26 deletions(-)
diff --git a/doc/src/sgml/system-views.sgml b/doc/src/sgml/system-views.sgml
index 1be54673bf..f37a3b64c1 100644
--- a/doc/src/sgml/system-views.sgml
+++ b/doc/src/sgml/system-views.sgml
@@ -5916,6 +5916,38 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx
Number of heap pages newly marked both all-visible and all-frozen in the visibility map by the vacuum.
</para></entry>
</row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>total_blks_read</structfield> <type>bigint</type>
+ </para>
+ <para>
+ Number of shared buffer blocks missed (read from disk) by the vacuum.
+ </para></entry>
+ </row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>total_blks_hit</structfield> <type>bigint</type>
+ </para>
+ <para>
+ Number of shared buffer blocks found in the buffer cache (hits) by the vacuum.
+ </para></entry>
+ </row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>total_blks_dirtied</structfield> <type>bigint</type>
+ </para>
+ <para>
+ Number of shared buffer blocks dirtied by the vacuum.
+ </para></entry>
+ </row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>total_blks_written</structfield> <type>bigint</type>
+ </para>
+ <para>
+ Number of shared buffer blocks written out by the vacuum.
+ </para></entry>
+ </row>
<row>
<entry role="catalog_table_entry"><para role="column_definition">
<structfield>wraparound_failsafe</structfield> <type>integer</type>
@@ -6039,6 +6071,38 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx
Number of index entries removed by the vacuum.
</para></entry>
</row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>total_blks_read</structfield> <type>bigint</type>
+ </para>
+ <para>
+ Number of shared buffer blocks read while vacuuming the index.
+ </para></entry>
+ </row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>total_blks_hit</structfield> <type>bigint</type>
+ </para>
+ <para>
+ Number of shared buffer block hits while vacuuming the index.
+ </para></entry>
+ </row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>total_blks_dirtied</structfield> <type>bigint</type>
+ </para>
+ <para>
+ Number of shared buffer blocks dirtied while vacuuming the index.
+ </para></entry>
+ </row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>total_blks_written</structfield> <type>bigint</type>
+ </para>
+ <para>
+ Number of shared buffer blocks written out while vacuuming the index.
+ </para></entry>
+ </row>
<row>
<entry role="catalog_table_entry"><para role="column_definition">
<structfield>wal_records</structfield> <type>bigint</type>
@@ -6121,6 +6185,38 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx
Number of vacuum operations in this database that failed with an error.
</para></entry>
</row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>db_blks_read</structfield> <type>bigint</type>
+ </para>
+ <para>
+ Number of shared buffer blocks read by vacuum operations in this database.
+ </para></entry>
+ </row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>db_blks_hit</structfield> <type>bigint</type>
+ </para>
+ <para>
+ Number of shared buffer block hits by vacuum operations in this database.
+ </para></entry>
+ </row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>total_blks_dirtied</structfield> <type>bigint</type>
+ </para>
+ <para>
+ Number of shared buffer blocks dirtied by vacuum operations in this database.
+ </para></entry>
+ </row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>total_blks_written</structfield> <type>bigint</type>
+ </para>
+ <para>
+ Number of shared buffer blocks written out by vacuum operations in this database.
+ </para></entry>
+ </row>
<row>
<entry role="catalog_table_entry"><para role="column_definition">
<structfield>wraparound_failsafe</structfield> <type>integer</type>
diff --git a/src/backend/access/heap/vacuumlazy.c b/src/backend/access/heap/vacuumlazy.c
index 46b7418136..1d6db2a91d 100644
--- a/src/backend/access/heap/vacuumlazy.c
+++ b/src/backend/access/heap/vacuumlazy.c
@@ -518,6 +518,7 @@ extvac_stats_start(Relation rel, LVExtStatCounters * counters)
memset(counters, 0, sizeof(LVExtStatCounters));
counters->walusage = pgWalUsage;
+ counters->bufusage = pgBufferUsage;
}
/* ----------
@@ -531,19 +532,27 @@ extvac_stats_end(Relation rel, LVExtStatCounters * counters,
PgStat_CommonCounts * report)
{
WalUsage walusage;
+ BufferUsage bufusage;
if (!pgstat_track_vacuum_statistics)
return;
memset(report, 0, sizeof(PgStat_CommonCounts));
- /* Calculate diffs of global stat parameters on WAL usage. */
+ /* Calculate diffs of global stat parameters on WAL and buffer usage. */
memset(&walusage, 0, sizeof(WalUsage));
WalUsageAccumDiff(&walusage, &pgWalUsage, &counters->walusage);
+ memset(&bufusage, 0, sizeof(BufferUsage));
+ BufferUsageAccumDiff(&bufusage, &pgBufferUsage, &counters->bufusage);
+
/*
* Fill additional statistics on a vacuum processing operation.
*/
+ report->total_blks_read += bufusage.local_blks_read + bufusage.shared_blks_read;
+ report->total_blks_hit += bufusage.local_blks_hit + bufusage.shared_blks_hit;
+ report->total_blks_dirtied += bufusage.local_blks_dirtied + bufusage.shared_blks_dirtied;
+ report->total_blks_written += bufusage.shared_blks_written;
report->wal_records += walusage.wal_records;
report->wal_fpi += walusage.wal_fpi;
report->wal_bytes += walusage.wal_bytes;
@@ -634,6 +643,10 @@ accumulate_heap_vacuum_statistics(LVRelState *vacrel, PgStat_VacuumRelationCount
extVacStats->table.missed_dead_pages = vacrel->missed_dead_pages;
extVacStats->common.wraparound_failsafe_count = vacrel->wraparound_failsafe;
+ extVacStats->common.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;
@@ -643,6 +656,10 @@ static void
accumulate_idxs_vacuum_statistics(LVRelState *vacrel, PgStat_VacuumRelationCounts * extVacIdxStats)
{
/* Fill heap-specific extended stats fields */
+ 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;
@@ -664,6 +681,10 @@ extvac_accumulate_idx_report(PgStat_VacuumRelationCounts * dst,
{
dst->type = PGSTAT_EXTVAC_INDEX;
+ dst->common.total_blks_read += src->common.total_blks_read;
+ dst->common.total_blks_hit += src->common.total_blks_hit;
+ dst->common.total_blks_dirtied += src->common.total_blks_dirtied;
+ dst->common.total_blks_written += src->common.total_blks_written;
dst->common.wal_records += src->common.wal_records;
dst->common.wal_fpi += src->common.wal_fpi;
dst->common.wal_bytes += src->common.wal_bytes;
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index a7ac037bc3..1ee74f9273 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -1576,6 +1576,10 @@ CREATE VIEW pg_stat_vacuum_tables AS
S.vm_new_frozen_pages AS vm_new_frozen_pages,
S.vm_new_visible_pages AS vm_new_visible_pages,
S.vm_new_visible_frozen_pages AS vm_new_visible_frozen_pages,
+ S.total_blks_read AS total_blks_read,
+ S.total_blks_hit AS total_blks_hit,
+ S.total_blks_dirtied AS total_blks_dirtied,
+ S.total_blks_written AS total_blks_written,
S.wraparound_failsafe AS wraparound_failsafe,
S.wal_records AS wal_records,
S.wal_fpi AS wal_fpi,
@@ -1597,6 +1601,10 @@ CREATE VIEW pg_stat_vacuum_indexes AS
S.pages_deleted AS pages_deleted,
S.tuples_deleted AS tuples_deleted,
+ S.total_blks_read AS total_blks_read,
+ S.total_blks_hit AS total_blks_hit,
+ S.total_blks_dirtied AS total_blks_dirtied,
+ S.total_blks_written AS total_blks_written,
S.wal_records AS wal_records,
S.wal_fpi AS wal_fpi,
S.wal_bytes AS wal_bytes
@@ -1615,6 +1623,10 @@ CREATE VIEW pg_stat_vacuum_database AS
S.errors AS errors,
+ S.db_blks_read AS db_blks_read,
+ S.db_blks_hit AS db_blks_hit,
+ S.total_blks_dirtied AS total_blks_dirtied,
+ S.total_blks_written AS total_blks_written,
S.wraparound_failsafe AS wraparound_failsafe,
S.wal_records AS wal_records,
S.wal_fpi AS wal_fpi,
diff --git a/src/backend/utils/activity/pgstat_vacuum.c b/src/backend/utils/activity/pgstat_vacuum.c
index ec3d50c59c..2bb44da510 100644
--- a/src/backend/utils/activity/pgstat_vacuum.c
+++ b/src/backend/utils/activity/pgstat_vacuum.c
@@ -38,6 +38,11 @@ bool pgstat_track_vacuum_statistics_for_relations = false;
static void
pgstat_accumulate_common(PgStat_CommonCounts *dst, const PgStat_CommonCounts *src)
{
+ ACCUMULATE_FIELD(total_blks_read);
+ ACCUMULATE_FIELD(total_blks_hit);
+ ACCUMULATE_FIELD(total_blks_dirtied);
+ ACCUMULATE_FIELD(total_blks_written);
+
ACCUMULATE_FIELD(wal_records);
ACCUMULATE_FIELD(wal_fpi);
ACCUMULATE_FIELD(wal_bytes);
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 694fd220b0..eeca140354 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -2374,7 +2374,7 @@ pg_stat_have_stats(PG_FUNCTION_ARGS)
Datum
pg_stat_get_vacuum_tables(PG_FUNCTION_ARGS)
{
-#define PG_STAT_GET_VACUUM_TABLES_STATS_COLS 15
+#define PG_STAT_GET_VACUUM_TABLES_STATS_COLS 19
Oid relid = PG_GETARG_OID(0);
PgStat_VacuumRelationCounts *extvacuum;
@@ -2411,6 +2411,10 @@ pg_stat_get_vacuum_tables(PG_FUNCTION_ARGS)
values[i++] = Int64GetDatum(extvacuum->table.vm_new_frozen_pages);
values[i++] = Int64GetDatum(extvacuum->table.vm_new_visible_pages);
values[i++] = Int64GetDatum(extvacuum->table.vm_new_visible_frozen_pages);
+ values[i++] = Int64GetDatum(extvacuum->common.total_blks_read);
+ values[i++] = Int64GetDatum(extvacuum->common.total_blks_hit);
+ values[i++] = Int64GetDatum(extvacuum->common.total_blks_dirtied);
+ values[i++] = Int64GetDatum(extvacuum->common.total_blks_written);
values[i++] = Int32GetDatum(extvacuum->common.wraparound_failsafe_count);
values[i++] = Int64GetDatum(extvacuum->common.wal_records);
values[i++] = Int64GetDatum(extvacuum->common.wal_fpi);
@@ -2432,7 +2436,7 @@ pg_stat_get_vacuum_tables(PG_FUNCTION_ARGS)
Datum
pg_stat_get_vacuum_indexes(PG_FUNCTION_ARGS)
{
-#define PG_STAT_GET_VACUUM_INDEX_STATS_COLS 6
+#define PG_STAT_GET_VACUUM_INDEX_STATS_COLS 10
Oid relid = PG_GETARG_OID(0);
PgStat_VacuumRelationCounts *extvacuum;
@@ -2461,6 +2465,11 @@ pg_stat_get_vacuum_indexes(PG_FUNCTION_ARGS)
values[i++] = Int64GetDatum(extvacuum->index.pages_deleted);
values[i++] = Int64GetDatum(extvacuum->common.tuples_deleted);
+ values[i++] = Int64GetDatum(extvacuum->common.total_blks_read);
+ values[i++] = Int64GetDatum(extvacuum->common.total_blks_hit);
+ values[i++] = Int64GetDatum(extvacuum->common.total_blks_dirtied);
+ values[i++] = Int64GetDatum(extvacuum->common.total_blks_written);
+
values[i++] = Int64GetDatum(extvacuum->common.wal_records);
values[i++] = Int64GetDatum(extvacuum->common.wal_fpi);
snprintf(buf, sizeof buf, UINT64_FORMAT, extvacuum->common.wal_bytes);
@@ -2480,7 +2489,7 @@ pg_stat_get_vacuum_indexes(PG_FUNCTION_ARGS)
Datum
pg_stat_get_vacuum_database(PG_FUNCTION_ARGS)
{
-#define PG_STAT_GET_VACUUM_DATABASE_STATS_COLS 7
+#define PG_STAT_GET_VACUUM_DATABASE_STATS_COLS 11
Oid dbid = PG_GETARG_OID(0);
PgStat_VacuumDBCounts *extvacuum;
@@ -2504,6 +2513,11 @@ pg_stat_get_vacuum_database(PG_FUNCTION_ARGS)
values[i++] = Int32GetDatum(extvacuum->errors);
+ values[i++] = Int64GetDatum(extvacuum->common.total_blks_read);
+ values[i++] = Int64GetDatum(extvacuum->common.total_blks_hit);
+ values[i++] = Int64GetDatum(extvacuum->common.total_blks_dirtied);
+ values[i++] = Int64GetDatum(extvacuum->common.total_blks_written);
+
values[i++] = Int32GetDatum(extvacuum->common.wraparound_failsafe_count);
values[i++] = Int64GetDatum(extvacuum->common.wal_records);
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index d2fb1b4ec0..718ba80a3a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12643,9 +12643,9 @@
proname => 'pg_stat_get_vacuum_tables', prorows => 1000, provolatile => 's', prorettype => 'record', proisstrict => 'f',
proretset => 't',
proargtypes => 'oid',
- proallargtypes => '{oid,oid,int8,int8,int8,int8,int8,int8,int8,int8,int8,int8,int4,int8,int8,numeric}',
- proargmodes => '{i,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o}',
- proargnames => '{reloid,relid,pages_scanned,pages_removed,tuples_deleted,tuples_frozen,recently_dead_tuples,missed_dead_pages,missed_dead_tuples,vm_new_frozen_pages,vm_new_visible_pages,vm_new_visible_frozen_pages,wraparound_failsafe,wal_records,wal_fpi,wal_bytes}',
+ proallargtypes => '{oid,oid,int8,int8,int8,int8,int8,int8,int8,int8,int8,int8,int8,int8,int8,int8,int4,int8,int8,numeric}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o}',
+ proargnames => '{reloid,relid,pages_scanned,pages_removed,tuples_deleted,tuples_frozen,recently_dead_tuples,missed_dead_pages,missed_dead_tuples,vm_new_frozen_pages,vm_new_visible_pages,vm_new_visible_frozen_pages,total_blks_read,total_blks_hit,total_blks_dirtied,total_blks_written,wraparound_failsafe,wal_records,wal_fpi,wal_bytes}',
prosrc => 'pg_stat_get_vacuum_tables' }
# oid8 related functions
@@ -12718,17 +12718,17 @@
proname => 'pg_stat_get_vacuum_indexes', prorows => 1000, provolatile => 's', prorettype => 'record', proisstrict => 'f',
proretset => 't',
proargtypes => 'oid',
- proallargtypes => '{oid,oid,int8,int8,int8,int8,numeric}',
- proargmodes => '{i,o,o,o,o,o,o}',
- proargnames => '{reloid,relid,pages_deleted,tuples_deleted,wal_records,wal_fpi,wal_bytes}',
+ proallargtypes => '{oid,oid,int8,int8,int8,int8,int8,int8,int8,int8,numeric}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o,o,o}',
+ proargnames => '{reloid,relid,pages_deleted,tuples_deleted,total_blks_read,total_blks_hit,total_blks_dirtied,total_blks_written,wal_records,wal_fpi,wal_bytes}',
prosrc => 'pg_stat_get_vacuum_indexes' },
{ oid => '8005',
descr => 'pg_stat_get_vacuum_database returns vacuum stats values for database',
proname => 'pg_stat_get_vacuum_database', prorows => 1000, provolatile => 's', prorettype => 'record', proisstrict => 'f',
proretset => 't',
proargtypes => 'oid',
- proallargtypes => '{oid,oid,int4,int4,int8,int8,numeric,int4}',
- proargmodes => '{i,o,o,o,o,o,o,o}',
- proargnames => '{dbid,dboid,errors,wraparound_failsafe,wal_records,wal_fpi,wal_bytes,interrupts_count}',
+ proallargtypes => '{oid,oid,int4,int8,int8,int8,int8,int4,int8,int8,numeric,int4}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o,o,o,o}',
+ proargnames => '{dbid,dboid,errors,db_blks_read,db_blks_hit,total_blks_dirtied,total_blks_written,wraparound_failsafe,wal_records,wal_fpi,wal_bytes,interrupts_count}',
prosrc => 'pg_stat_get_vacuum_database' },
]
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index bcc84dbd41..92f8b9b8ec 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -175,6 +175,12 @@ typedef struct PgStat_TableCounts
typedef struct PgStat_CommonCounts
{
+ /* blocks */
+ int64 total_blks_read;
+ int64 total_blks_hit;
+ int64 total_blks_dirtied;
+ int64 total_blks_written;
+
/* WAL */
int64 wal_records;
int64 wal_fpi;
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 27a17ee0c2..2b879192aa 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -2424,13 +2424,17 @@ pg_stat_user_tables| SELECT relid,
pg_stat_vacuum_database| SELECT d.oid AS dboid,
d.datname AS dbname,
s.errors,
+ s.db_blks_read,
+ s.db_blks_hit,
+ s.total_blks_dirtied,
+ s.total_blks_written,
s.wraparound_failsafe,
s.wal_records,
s.wal_fpi,
s.wal_bytes,
s.interrupts_count
FROM pg_database d,
- LATERAL pg_stat_get_vacuum_database(d.oid) s(dboid, errors, wraparound_failsafe, wal_records, wal_fpi, wal_bytes, interrupts_count);
+ LATERAL pg_stat_get_vacuum_database(d.oid) s(dboid, errors, db_blks_read, db_blks_hit, total_blks_dirtied, total_blks_written, wraparound_failsafe, wal_records, wal_fpi, wal_bytes, interrupts_count);
pg_stat_vacuum_indexes| SELECT c.oid AS relid,
i.oid AS indexrelid,
n.nspname AS schemaname,
@@ -2438,6 +2442,10 @@ pg_stat_vacuum_indexes| SELECT c.oid AS relid,
i.relname AS indexrelname,
s.pages_deleted,
s.tuples_deleted,
+ s.total_blks_read,
+ s.total_blks_hit,
+ s.total_blks_dirtied,
+ s.total_blks_written,
s.wal_records,
s.wal_fpi,
s.wal_bytes
@@ -2445,7 +2453,7 @@ pg_stat_vacuum_indexes| SELECT c.oid AS relid,
JOIN pg_index x ON ((c.oid = x.indrelid)))
JOIN pg_class i ON ((i.oid = x.indexrelid)))
LEFT JOIN pg_namespace n ON ((n.oid = c.relnamespace))),
- LATERAL pg_stat_get_vacuum_indexes(i.oid) s(relid, pages_deleted, tuples_deleted, wal_records, wal_fpi, wal_bytes)
+ LATERAL pg_stat_get_vacuum_indexes(i.oid) s(relid, pages_deleted, tuples_deleted, total_blks_read, total_blks_hit, total_blks_dirtied, total_blks_written, wal_records, wal_fpi, wal_bytes)
WHERE (c.relkind = ANY (ARRAY['r'::"char", 't'::"char", 'm'::"char"]));
pg_stat_vacuum_tables| SELECT n.nspname AS schemaname,
c.relname,
@@ -2460,13 +2468,17 @@ pg_stat_vacuum_tables| SELECT n.nspname AS schemaname,
s.vm_new_frozen_pages,
s.vm_new_visible_pages,
s.vm_new_visible_frozen_pages,
+ s.total_blks_read,
+ s.total_blks_hit,
+ s.total_blks_dirtied,
+ s.total_blks_written,
s.wraparound_failsafe,
s.wal_records,
s.wal_fpi,
s.wal_bytes
FROM (pg_class c
JOIN pg_namespace n ON ((n.oid = c.relnamespace))),
- LATERAL pg_stat_get_vacuum_tables(c.oid) s(relid, pages_scanned, pages_removed, tuples_deleted, tuples_frozen, recently_dead_tuples, missed_dead_pages, missed_dead_tuples, vm_new_frozen_pages, vm_new_visible_pages, vm_new_visible_frozen_pages, wraparound_failsafe, wal_records, wal_fpi, wal_bytes)
+ LATERAL pg_stat_get_vacuum_tables(c.oid) s(relid, pages_scanned, pages_removed, tuples_deleted, tuples_frozen, recently_dead_tuples, missed_dead_pages, missed_dead_tuples, vm_new_frozen_pages, vm_new_visible_pages, vm_new_visible_frozen_pages, total_blks_read, total_blks_hit, total_blks_dirtied, total_blks_written, wraparound_failsafe, wal_records, wal_fpi, wal_bytes)
WHERE (c.relkind = ANY (ARRAY['r'::"char", 't'::"char", 'm'::"char"]));
pg_stat_wal| SELECT wal_records,
wal_fpi,
diff --git a/src/test/regress/expected/vacuum_stats.out b/src/test/regress/expected/vacuum_stats.out
index c996699804..87f49cb76c 100644
--- a/src/test/regress/expected/vacuum_stats.out
+++ b/src/test/regress/expected/vacuum_stats.out
@@ -123,6 +123,21 @@ SELECT vm_new_visible_pages > 0 AS vm_new_visible_pages,
(1 row)
DROP TABLE vacstat_frz;
+-- total buffer access counters. The vacuum always touches the table's pages
+-- through the buffer cache (total_blks_hit > 0) and dirties some of them while
+-- removing dead tuples (total_blks_dirtied > 0). total_blks_read and
+-- total_blks_written depend on the buffer-cache and checkpoint state at run
+-- time, so they are only checked for being non-negative.
+SELECT total_blks_read >= 0 AS total_blks_read,
+ total_blks_hit > 0 AS total_blks_hit,
+ total_blks_dirtied > 0 AS total_blks_dirtied,
+ total_blks_written >= 0 AS total_blks_written
+ FROM pg_stat_vacuum_tables WHERE relname = 'vacstat_t';
+ total_blks_read | total_blks_hit | total_blks_dirtied | total_blks_written
+-----------------+----------------+--------------------+--------------------
+ t | t | t | t
+(1 row)
+
-- WAL metrics. A vacuum that removes tuples always emits WAL
-- (wal_records > 0, wal_bytes > 0). wal_fpi depends on whether a checkpoint
-- happened recently, so it is only checked for being non-negative here; the
@@ -174,17 +189,23 @@ SELECT wraparound_failsafe = 0 AS wraparound_failsafe
-- per-index view: the primary key index is processed by the same VACUUM.
-- No btree leaf empties out (interleaved deletions), so pages_deleted = 0,
--- while every index entry for a removed heap tuple is deleted.
+-- while every index entry for a removed heap tuple is deleted. The index is
+-- read through the buffer cache (total_blks_hit > 0); the read/written/dirtied
+-- counters depend on run-time cache state.
SELECT indexrelname,
pages_deleted = 0 AS pages_deleted,
tuples_deleted = 500 AS tuples_deleted,
+ total_blks_read >= 0 AS total_blks_read,
+ total_blks_hit > 0 AS total_blks_hit,
+ total_blks_dirtied >= 0 AS total_blks_dirtied,
+ total_blks_written >= 0 AS total_blks_written,
wal_records > 0 AS wal_records,
wal_fpi >= 0 AS wal_fpi,
wal_bytes > 0 AS wal_bytes
FROM pg_stat_vacuum_indexes WHERE relname = 'vacstat_t' ORDER BY indexrelname;
- indexrelname | pages_deleted | tuples_deleted | wal_records | wal_fpi | wal_bytes
-----------------+---------------+----------------+-------------+---------+-----------
- vacstat_t_pkey | t | t | t | t | t
+ indexrelname | pages_deleted | tuples_deleted | total_blks_read | total_blks_hit | total_blks_dirtied | total_blks_written | wal_records | wal_fpi | wal_bytes
+----------------+---------------+----------------+-----------------+----------------+--------------------+--------------------+-------------+---------+-----------
+ vacstat_t_pkey | t | t | t | t | t | t | t | t | t
(1 row)
-- index page-deletion path: deleting a contiguous key range empties whole
@@ -219,16 +240,21 @@ SELECT indexrelname,
DROP TABLE vacstat_idxdel;
-- per-database aggregate view: no vacuum errors occurred in this database, and
--- the vacuums in this database emit WAL (wal_records > 0).
+-- the vacuums in this database touched pages through the buffer cache
+-- (db_blks_hit > 0) and emit WAL (wal_records > 0).
SELECT errors = 0 AS errors,
+ db_blks_read >= 0 AS db_blks_read,
+ db_blks_hit > 0 AS db_blks_hit,
+ total_blks_dirtied >= 0 AS total_blks_dirtied,
+ total_blks_written >= 0 AS total_blks_written,
wraparound_failsafe = 0 AS wraparound_failsafe,
wal_records > 0 AS wal_records,
wal_fpi >= 0 AS wal_fpi,
wal_bytes > 0 AS wal_bytes
FROM pg_stat_vacuum_database WHERE dbname = current_database();
- errors | wraparound_failsafe | wal_records | wal_fpi | wal_bytes
---------+---------------------+-------------+---------+-----------
- t | t | t | t | t
+ errors | db_blks_read | db_blks_hit | total_blks_dirtied | total_blks_written | wraparound_failsafe | wal_records | wal_fpi | wal_bytes
+--------+--------------+-------------+--------------------+--------------------+---------------------+-------------+---------+-----------
+ t | t | t | t | t | t | t | t | t
(1 row)
-- parallel index vacuum: index statistics must be captured for indexes
diff --git a/src/test/regress/sql/vacuum_stats.sql b/src/test/regress/sql/vacuum_stats.sql
index f106b7c37b..6e364d183c 100644
--- a/src/test/regress/sql/vacuum_stats.sql
+++ b/src/test/regress/sql/vacuum_stats.sql
@@ -81,6 +81,17 @@ SELECT vm_new_visible_pages > 0 AS vm_new_visible_pages,
FROM pg_stat_vacuum_tables WHERE relname = 'vacstat_frz';
DROP TABLE vacstat_frz;
+-- total buffer access counters. The vacuum always touches the table's pages
+-- through the buffer cache (total_blks_hit > 0) and dirties some of them while
+-- removing dead tuples (total_blks_dirtied > 0). total_blks_read and
+-- total_blks_written depend on the buffer-cache and checkpoint state at run
+-- time, so they are only checked for being non-negative.
+SELECT total_blks_read >= 0 AS total_blks_read,
+ total_blks_hit > 0 AS total_blks_hit,
+ total_blks_dirtied > 0 AS total_blks_dirtied,
+ total_blks_written >= 0 AS total_blks_written
+ FROM pg_stat_vacuum_tables WHERE relname = 'vacstat_t';
+
-- WAL metrics. A vacuum that removes tuples always emits WAL
-- (wal_records > 0, wal_bytes > 0). wal_fpi depends on whether a checkpoint
-- happened recently, so it is only checked for being non-negative here; the
@@ -115,10 +126,16 @@ SELECT wraparound_failsafe = 0 AS wraparound_failsafe
-- per-index view: the primary key index is processed by the same VACUUM.
-- No btree leaf empties out (interleaved deletions), so pages_deleted = 0,
--- while every index entry for a removed heap tuple is deleted.
+-- while every index entry for a removed heap tuple is deleted. The index is
+-- read through the buffer cache (total_blks_hit > 0); the read/written/dirtied
+-- counters depend on run-time cache state.
SELECT indexrelname,
pages_deleted = 0 AS pages_deleted,
tuples_deleted = 500 AS tuples_deleted,
+ total_blks_read >= 0 AS total_blks_read,
+ total_blks_hit > 0 AS total_blks_hit,
+ total_blks_dirtied >= 0 AS total_blks_dirtied,
+ total_blks_written >= 0 AS total_blks_written,
wal_records > 0 AS wal_records,
wal_fpi >= 0 AS wal_fpi,
wal_bytes > 0 AS wal_bytes
@@ -142,8 +159,13 @@ SELECT indexrelname,
DROP TABLE vacstat_idxdel;
-- per-database aggregate view: no vacuum errors occurred in this database, and
--- the vacuums in this database emit WAL (wal_records > 0).
+-- the vacuums in this database touched pages through the buffer cache
+-- (db_blks_hit > 0) and emit WAL (wal_records > 0).
SELECT errors = 0 AS errors,
+ db_blks_read >= 0 AS db_blks_read,
+ db_blks_hit > 0 AS db_blks_hit,
+ total_blks_dirtied >= 0 AS total_blks_dirtied,
+ total_blks_written >= 0 AS total_blks_written,
wraparound_failsafe = 0 AS wraparound_failsafe,
wal_records > 0 AS wal_records,
wal_fpi >= 0 AS wal_fpi,
--
2.39.5 (Apple Git-154)
From 690556053f3abce9b91abed7ff29e309dfbc8ed6 Mon Sep 17 00:00:00 2001
From: Alena Rybakina <[email protected]>
Date: Wed, 17 Jun 2026 21:49:18 +0300
Subject: [PATCH 8/9] Extended vacuum statistics: per-relation buffer access
for tables and indexes
Expose the per-relation buffer access counters in pg_stat_vacuum_tables and
pg_stat_vacuum_indexes, with documentation and regression coverage:
rel_blks_read this relation's blocks read from disk by the vacuum
rel_blks_hit this relation's blocks found in shared buffers
Unlike total_blks_*, which count all shared-buffer access during the vacuum,
these are restricted to the target relation's own blocks.
---
doc/src/sgml/system-views.sgml | 32 ++++++++++++++++++++++
src/backend/access/heap/vacuumlazy.c | 28 +++++++++++++++++++
src/backend/catalog/system_views.sql | 4 +++
src/backend/utils/activity/pgstat_vacuum.c | 3 ++
src/backend/utils/adt/pgstatfuncs.c | 9 ++++--
src/include/catalog/pg_proc.dat | 12 ++++----
src/include/pgstat.h | 4 +++
src/test/regress/expected/rules.out | 8 ++++--
src/test/regress/expected/vacuum_stats.out | 18 ++++++++++--
src/test/regress/sql/vacuum_stats.sql | 8 ++++++
10 files changed, 113 insertions(+), 13 deletions(-)
diff --git a/doc/src/sgml/system-views.sgml b/doc/src/sgml/system-views.sgml
index f37a3b64c1..2a93896d5d 100644
--- a/doc/src/sgml/system-views.sgml
+++ b/doc/src/sgml/system-views.sgml
@@ -5948,6 +5948,22 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx
Number of shared buffer blocks written out by the vacuum.
</para></entry>
</row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>rel_blks_read</structfield> <type>bigint</type>
+ </para>
+ <para>
+ Number of this relation's blocks read from disk by the vacuum.
+ </para></entry>
+ </row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>rel_blks_hit</structfield> <type>bigint</type>
+ </para>
+ <para>
+ Number of this relation's blocks found in shared buffers by the vacuum.
+ </para></entry>
+ </row>
<row>
<entry role="catalog_table_entry"><para role="column_definition">
<structfield>wraparound_failsafe</structfield> <type>integer</type>
@@ -6103,6 +6119,22 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx
Number of shared buffer blocks written out while vacuuming the index.
</para></entry>
</row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>rel_blks_read</structfield> <type>bigint</type>
+ </para>
+ <para>
+ Number of blocks of this index read from disk by the vacuum.
+ </para></entry>
+ </row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>rel_blks_hit</structfield> <type>bigint</type>
+ </para>
+ <para>
+ Number of block hits within this index during the vacuum.
+ </para></entry>
+ </row>
<row>
<entry role="catalog_table_entry"><para role="column_definition">
<structfield>wal_records</structfield> <type>bigint</type>
diff --git a/src/backend/access/heap/vacuumlazy.c b/src/backend/access/heap/vacuumlazy.c
index 1d6db2a91d..683bfcf9e8 100644
--- a/src/backend/access/heap/vacuumlazy.c
+++ b/src/backend/access/heap/vacuumlazy.c
@@ -519,6 +519,19 @@ extvac_stats_start(Relation rel, LVExtStatCounters * counters)
counters->walusage = pgWalUsage;
counters->bufusage = pgBufferUsage;
+ counters->blocks_fetched = 0;
+ counters->blocks_hit = 0;
+
+ if (!rel->pgstat_info || !pgstat_track_counts)
+
+ /*
+ * if something goes wrong or user doesn't want to track a database
+ * activity - just suppress it.
+ */
+ return;
+
+ counters->blocks_fetched = rel->pgstat_info->counts.blocks_fetched;
+ counters->blocks_hit = rel->pgstat_info->counts.blocks_hit;
}
/* ----------
@@ -556,6 +569,19 @@ extvac_stats_end(Relation rel, LVExtStatCounters * counters,
report->wal_records += walusage.wal_records;
report->wal_fpi += walusage.wal_fpi;
report->wal_bytes += walusage.wal_bytes;
+
+ if (!rel->pgstat_info || !pgstat_track_counts)
+
+ /*
+ * if something goes wrong or an user doesn't want to track a database
+ * activity - just suppress it.
+ */
+ return;
+
+ report->blks_fetched +=
+ rel->pgstat_info->counts.blocks_fetched - counters->blocks_fetched;
+ report->blks_hit +=
+ rel->pgstat_info->counts.blocks_hit - counters->blocks_hit;
}
void
@@ -685,6 +711,8 @@ extvac_accumulate_idx_report(PgStat_VacuumRelationCounts * dst,
dst->common.total_blks_hit += src->common.total_blks_hit;
dst->common.total_blks_dirtied += src->common.total_blks_dirtied;
dst->common.total_blks_written += src->common.total_blks_written;
+ dst->common.blks_fetched += src->common.blks_fetched;
+ dst->common.blks_hit += src->common.blks_hit;
dst->common.wal_records += src->common.wal_records;
dst->common.wal_fpi += src->common.wal_fpi;
dst->common.wal_bytes += src->common.wal_bytes;
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index 1ee74f9273..d0a58f221c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -1580,6 +1580,8 @@ CREATE VIEW pg_stat_vacuum_tables AS
S.total_blks_hit AS total_blks_hit,
S.total_blks_dirtied AS total_blks_dirtied,
S.total_blks_written AS total_blks_written,
+ S.rel_blks_read AS rel_blks_read,
+ S.rel_blks_hit AS rel_blks_hit,
S.wraparound_failsafe AS wraparound_failsafe,
S.wal_records AS wal_records,
S.wal_fpi AS wal_fpi,
@@ -1605,6 +1607,8 @@ CREATE VIEW pg_stat_vacuum_indexes AS
S.total_blks_hit AS total_blks_hit,
S.total_blks_dirtied AS total_blks_dirtied,
S.total_blks_written AS total_blks_written,
+ S.rel_blks_read AS rel_blks_read,
+ S.rel_blks_hit AS rel_blks_hit,
S.wal_records AS wal_records,
S.wal_fpi AS wal_fpi,
S.wal_bytes AS wal_bytes
diff --git a/src/backend/utils/activity/pgstat_vacuum.c b/src/backend/utils/activity/pgstat_vacuum.c
index 2bb44da510..b1c747f255 100644
--- a/src/backend/utils/activity/pgstat_vacuum.c
+++ b/src/backend/utils/activity/pgstat_vacuum.c
@@ -43,6 +43,9 @@ pgstat_accumulate_common(PgStat_CommonCounts *dst, const PgStat_CommonCounts *sr
ACCUMULATE_FIELD(total_blks_dirtied);
ACCUMULATE_FIELD(total_blks_written);
+ ACCUMULATE_FIELD(blks_fetched);
+ ACCUMULATE_FIELD(blks_hit);
+
ACCUMULATE_FIELD(wal_records);
ACCUMULATE_FIELD(wal_fpi);
ACCUMULATE_FIELD(wal_bytes);
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index eeca140354..7473ae1eb6 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -2374,7 +2374,7 @@ pg_stat_have_stats(PG_FUNCTION_ARGS)
Datum
pg_stat_get_vacuum_tables(PG_FUNCTION_ARGS)
{
-#define PG_STAT_GET_VACUUM_TABLES_STATS_COLS 19
+#define PG_STAT_GET_VACUUM_TABLES_STATS_COLS 21
Oid relid = PG_GETARG_OID(0);
PgStat_VacuumRelationCounts *extvacuum;
@@ -2415,6 +2415,8 @@ pg_stat_get_vacuum_tables(PG_FUNCTION_ARGS)
values[i++] = Int64GetDatum(extvacuum->common.total_blks_hit);
values[i++] = Int64GetDatum(extvacuum->common.total_blks_dirtied);
values[i++] = Int64GetDatum(extvacuum->common.total_blks_written);
+ values[i++] = Int64GetDatum(extvacuum->common.blks_fetched - extvacuum->common.blks_hit);
+ values[i++] = Int64GetDatum(extvacuum->common.blks_hit);
values[i++] = Int32GetDatum(extvacuum->common.wraparound_failsafe_count);
values[i++] = Int64GetDatum(extvacuum->common.wal_records);
values[i++] = Int64GetDatum(extvacuum->common.wal_fpi);
@@ -2436,7 +2438,7 @@ pg_stat_get_vacuum_tables(PG_FUNCTION_ARGS)
Datum
pg_stat_get_vacuum_indexes(PG_FUNCTION_ARGS)
{
-#define PG_STAT_GET_VACUUM_INDEX_STATS_COLS 10
+#define PG_STAT_GET_VACUUM_INDEX_STATS_COLS 12
Oid relid = PG_GETARG_OID(0);
PgStat_VacuumRelationCounts *extvacuum;
@@ -2470,6 +2472,9 @@ pg_stat_get_vacuum_indexes(PG_FUNCTION_ARGS)
values[i++] = Int64GetDatum(extvacuum->common.total_blks_dirtied);
values[i++] = Int64GetDatum(extvacuum->common.total_blks_written);
+ values[i++] = Int64GetDatum(extvacuum->common.blks_fetched - extvacuum->common.blks_hit);
+ values[i++] = Int64GetDatum(extvacuum->common.blks_hit);
+
values[i++] = Int64GetDatum(extvacuum->common.wal_records);
values[i++] = Int64GetDatum(extvacuum->common.wal_fpi);
snprintf(buf, sizeof buf, UINT64_FORMAT, extvacuum->common.wal_bytes);
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 718ba80a3a..743ce9dfd6 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12643,9 +12643,9 @@
proname => 'pg_stat_get_vacuum_tables', prorows => 1000, provolatile => 's', prorettype => 'record', proisstrict => 'f',
proretset => 't',
proargtypes => 'oid',
- proallargtypes => '{oid,oid,int8,int8,int8,int8,int8,int8,int8,int8,int8,int8,int8,int8,int8,int8,int4,int8,int8,numeric}',
- proargmodes => '{i,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o}',
- proargnames => '{reloid,relid,pages_scanned,pages_removed,tuples_deleted,tuples_frozen,recently_dead_tuples,missed_dead_pages,missed_dead_tuples,vm_new_frozen_pages,vm_new_visible_pages,vm_new_visible_frozen_pages,total_blks_read,total_blks_hit,total_blks_dirtied,total_blks_written,wraparound_failsafe,wal_records,wal_fpi,wal_bytes}',
+ proallargtypes => '{oid,oid,int8,int8,int8,int8,int8,int8,int8,int8,int8,int8,int8,int8,int8,int8,int8,int8,int4,int8,int8,numeric}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o}',
+ proargnames => '{reloid,relid,pages_scanned,pages_removed,tuples_deleted,tuples_frozen,recently_dead_tuples,missed_dead_pages,missed_dead_tuples,vm_new_frozen_pages,vm_new_visible_pages,vm_new_visible_frozen_pages,total_blks_read,total_blks_hit,total_blks_dirtied,total_blks_written,rel_blks_read,rel_blks_hit,wraparound_failsafe,wal_records,wal_fpi,wal_bytes}',
prosrc => 'pg_stat_get_vacuum_tables' }
# oid8 related functions
@@ -12718,9 +12718,9 @@
proname => 'pg_stat_get_vacuum_indexes', prorows => 1000, provolatile => 's', prorettype => 'record', proisstrict => 'f',
proretset => 't',
proargtypes => 'oid',
- proallargtypes => '{oid,oid,int8,int8,int8,int8,int8,int8,int8,int8,numeric}',
- proargmodes => '{i,o,o,o,o,o,o,o,o,o,o}',
- proargnames => '{reloid,relid,pages_deleted,tuples_deleted,total_blks_read,total_blks_hit,total_blks_dirtied,total_blks_written,wal_records,wal_fpi,wal_bytes}',
+ proallargtypes => '{oid,oid,int8,int8,int8,int8,int8,int8,int8,int8,int8,int8,numeric}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o,o,o,o,o}',
+ proargnames => '{reloid,relid,pages_deleted,tuples_deleted,total_blks_read,total_blks_hit,total_blks_dirtied,total_blks_written,rel_blks_read,rel_blks_hit,wal_records,wal_fpi,wal_bytes}',
prosrc => 'pg_stat_get_vacuum_indexes' },
{ oid => '8005',
descr => 'pg_stat_get_vacuum_database returns vacuum stats values for database',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 92f8b9b8ec..18fa9ac5ba 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -181,6 +181,10 @@ typedef struct PgStat_CommonCounts
int64 total_blks_dirtied;
int64 total_blks_written;
+ /* heap blocks */
+ int64 blks_fetched;
+ int64 blks_hit;
+
/* WAL */
int64 wal_records;
int64 wal_fpi;
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b879192aa..c650cfd664 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -2446,6 +2446,8 @@ pg_stat_vacuum_indexes| SELECT c.oid AS relid,
s.total_blks_hit,
s.total_blks_dirtied,
s.total_blks_written,
+ s.rel_blks_read,
+ s.rel_blks_hit,
s.wal_records,
s.wal_fpi,
s.wal_bytes
@@ -2453,7 +2455,7 @@ pg_stat_vacuum_indexes| SELECT c.oid AS relid,
JOIN pg_index x ON ((c.oid = x.indrelid)))
JOIN pg_class i ON ((i.oid = x.indexrelid)))
LEFT JOIN pg_namespace n ON ((n.oid = c.relnamespace))),
- LATERAL pg_stat_get_vacuum_indexes(i.oid) s(relid, pages_deleted, tuples_deleted, total_blks_read, total_blks_hit, total_blks_dirtied, total_blks_written, wal_records, wal_fpi, wal_bytes)
+ LATERAL pg_stat_get_vacuum_indexes(i.oid) s(relid, pages_deleted, tuples_deleted, total_blks_read, total_blks_hit, total_blks_dirtied, total_blks_written, rel_blks_read, rel_blks_hit, wal_records, wal_fpi, wal_bytes)
WHERE (c.relkind = ANY (ARRAY['r'::"char", 't'::"char", 'm'::"char"]));
pg_stat_vacuum_tables| SELECT n.nspname AS schemaname,
c.relname,
@@ -2472,13 +2474,15 @@ pg_stat_vacuum_tables| SELECT n.nspname AS schemaname,
s.total_blks_hit,
s.total_blks_dirtied,
s.total_blks_written,
+ s.rel_blks_read,
+ s.rel_blks_hit,
s.wraparound_failsafe,
s.wal_records,
s.wal_fpi,
s.wal_bytes
FROM (pg_class c
JOIN pg_namespace n ON ((n.oid = c.relnamespace))),
- LATERAL pg_stat_get_vacuum_tables(c.oid) s(relid, pages_scanned, pages_removed, tuples_deleted, tuples_frozen, recently_dead_tuples, missed_dead_pages, missed_dead_tuples, vm_new_frozen_pages, vm_new_visible_pages, vm_new_visible_frozen_pages, total_blks_read, total_blks_hit, total_blks_dirtied, total_blks_written, wraparound_failsafe, wal_records, wal_fpi, wal_bytes)
+ LATERAL pg_stat_get_vacuum_tables(c.oid) s(relid, pages_scanned, pages_removed, tuples_deleted, tuples_frozen, recently_dead_tuples, missed_dead_pages, missed_dead_tuples, vm_new_frozen_pages, vm_new_visible_pages, vm_new_visible_frozen_pages, total_blks_read, total_blks_hit, total_blks_dirtied, total_blks_written, rel_blks_read, rel_blks_hit, wraparound_failsafe, wal_records, wal_fpi, wal_bytes)
WHERE (c.relkind = ANY (ARRAY['r'::"char", 't'::"char", 'm'::"char"]));
pg_stat_wal| SELECT wal_records,
wal_fpi,
diff --git a/src/test/regress/expected/vacuum_stats.out b/src/test/regress/expected/vacuum_stats.out
index 87f49cb76c..3d823ef7b6 100644
--- a/src/test/regress/expected/vacuum_stats.out
+++ b/src/test/regress/expected/vacuum_stats.out
@@ -138,6 +138,16 @@ SELECT total_blks_read >= 0 AS total_blks_read,
t | t | t | t
(1 row)
+-- per-relation buffer access. The heap is read through the buffer cache
+-- (rel_blks_hit > 0); rel_blks_read depends on the run-time cache state.
+SELECT rel_blks_read >= 0 AS rel_blks_read,
+ rel_blks_hit > 0 AS rel_blks_hit
+ FROM pg_stat_vacuum_tables WHERE relname = 'vacstat_t';
+ rel_blks_read | rel_blks_hit
+---------------+--------------
+ t | t
+(1 row)
+
-- WAL metrics. A vacuum that removes tuples always emits WAL
-- (wal_records > 0, wal_bytes > 0). wal_fpi depends on whether a checkpoint
-- happened recently, so it is only checked for being non-negative here; the
@@ -199,13 +209,15 @@ SELECT indexrelname,
total_blks_hit > 0 AS total_blks_hit,
total_blks_dirtied >= 0 AS total_blks_dirtied,
total_blks_written >= 0 AS total_blks_written,
+ rel_blks_read >= 0 AS rel_blks_read,
+ rel_blks_hit > 0 AS rel_blks_hit,
wal_records > 0 AS wal_records,
wal_fpi >= 0 AS wal_fpi,
wal_bytes > 0 AS wal_bytes
FROM pg_stat_vacuum_indexes WHERE relname = 'vacstat_t' ORDER BY indexrelname;
- indexrelname | pages_deleted | tuples_deleted | total_blks_read | total_blks_hit | total_blks_dirtied | total_blks_written | wal_records | wal_fpi | wal_bytes
-----------------+---------------+----------------+-----------------+----------------+--------------------+--------------------+-------------+---------+-----------
- vacstat_t_pkey | t | t | t | t | t | t | t | t | t
+ indexrelname | pages_deleted | tuples_deleted | total_blks_read | total_blks_hit | total_blks_dirtied | total_blks_written | rel_blks_read | rel_blks_hit | wal_records | wal_fpi | wal_bytes
+----------------+---------------+----------------+-----------------+----------------+--------------------+--------------------+---------------+--------------+-------------+---------+-----------
+ vacstat_t_pkey | t | t | t | t | t | t | t | t | t | t | t
(1 row)
-- index page-deletion path: deleting a contiguous key range empties whole
diff --git a/src/test/regress/sql/vacuum_stats.sql b/src/test/regress/sql/vacuum_stats.sql
index 6e364d183c..40f7d0985d 100644
--- a/src/test/regress/sql/vacuum_stats.sql
+++ b/src/test/regress/sql/vacuum_stats.sql
@@ -92,6 +92,12 @@ SELECT total_blks_read >= 0 AS total_blks_read,
total_blks_written >= 0 AS total_blks_written
FROM pg_stat_vacuum_tables WHERE relname = 'vacstat_t';
+-- per-relation buffer access. The heap is read through the buffer cache
+-- (rel_blks_hit > 0); rel_blks_read depends on the run-time cache state.
+SELECT rel_blks_read >= 0 AS rel_blks_read,
+ rel_blks_hit > 0 AS rel_blks_hit
+ FROM pg_stat_vacuum_tables WHERE relname = 'vacstat_t';
+
-- WAL metrics. A vacuum that removes tuples always emits WAL
-- (wal_records > 0, wal_bytes > 0). wal_fpi depends on whether a checkpoint
-- happened recently, so it is only checked for being non-negative here; the
@@ -136,6 +142,8 @@ SELECT indexrelname,
total_blks_hit > 0 AS total_blks_hit,
total_blks_dirtied >= 0 AS total_blks_dirtied,
total_blks_written >= 0 AS total_blks_written,
+ rel_blks_read >= 0 AS rel_blks_read,
+ rel_blks_hit > 0 AS rel_blks_hit,
wal_records > 0 AS wal_records,
wal_fpi >= 0 AS wal_fpi,
wal_bytes > 0 AS wal_bytes
--
2.39.5 (Apple Git-154)
From f995078939b0180ef6e59603297b35db7031fbfb Mon Sep 17 00:00:00 2001
From: Alena Rybakina <[email protected]>
Date: Wed, 17 Jun 2026 21:54:17 +0300
Subject: [PATCH 9/9] Extended vacuum statistics: timing metrics for tables,
indexes and database
Expose the timing counters in pg_stat_vacuum_tables, pg_stat_vacuum_indexes
and pg_stat_vacuum_database, with documentation and regression coverage:
blk_read_time time spent reading blocks (with track_io_timing)
blk_write_time time spent writing blocks (with track_io_timing)
delay_time time spent in vacuum cost-based delay
total_time total wall-clock time of the vacuum
total_time is always positive; the regression test also exercises the
positive delay_time path with a dedicated cost-delayed vacuum.
---
doc/src/sgml/system-views.sgml | 97 ++++++++++++++++++++++
src/backend/access/heap/vacuumlazy.c | 42 ++++++++--
src/backend/catalog/system_views.sql | 18 ++++
src/backend/commands/vacuumparallel.c | 1 +
src/backend/utils/activity/pgstat_vacuum.c | 5 ++
src/backend/utils/adt/pgstatfuncs.c | 22 ++++-
src/include/catalog/pg_proc.dat | 18 ++--
src/include/pgstat.h | 6 ++
src/test/regress/expected/rules.out | 18 +++-
src/test/regress/expected/vacuum_stats.out | 76 +++++++++++++----
src/test/regress/sql/vacuum_stats.sql | 47 +++++++++--
11 files changed, 304 insertions(+), 46 deletions(-)
diff --git a/doc/src/sgml/system-views.sgml b/doc/src/sgml/system-views.sgml
index 2a93896d5d..e15f4952cc 100644
--- a/doc/src/sgml/system-views.sgml
+++ b/doc/src/sgml/system-views.sgml
@@ -5964,6 +5964,38 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx
Number of this relation's blocks found in shared buffers by the vacuum.
</para></entry>
</row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>blk_read_time</structfield> <type>double precision</type>
+ </para>
+ <para>
+ Time spent reading blocks by the vacuum, in milliseconds (when <varname>track_io_timing</varname> is enabled).
+ </para></entry>
+ </row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>blk_write_time</structfield> <type>double precision</type>
+ </para>
+ <para>
+ Time spent writing (flushing) blocks by the vacuum, in milliseconds; remains zero if no flushes occurred.
+ </para></entry>
+ </row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>delay_time</structfield> <type>double precision</type>
+ </para>
+ <para>
+ Total time the vacuum spent sleeping in vacuum delay points, in milliseconds.
+ </para></entry>
+ </row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>total_time</structfield> <type>double precision</type>
+ </para>
+ <para>
+ Total wall-clock time of the vacuum, in milliseconds, including time spent waiting for I/O and locks.
+ </para></entry>
+ </row>
<row>
<entry role="catalog_table_entry"><para role="column_definition">
<structfield>wraparound_failsafe</structfield> <type>integer</type>
@@ -6135,6 +6167,38 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx
Number of block hits within this index during the vacuum.
</para></entry>
</row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>blk_read_time</structfield> <type>double precision</type>
+ </para>
+ <para>
+ Time spent reading index blocks, in milliseconds (if <varname>track_io_timing</varname> is enabled).
+ </para></entry>
+ </row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>blk_write_time</structfield> <type>double precision</type>
+ </para>
+ <para>
+ Time spent writing index blocks, in milliseconds (if <varname>track_io_timing</varname> is enabled).
+ </para></entry>
+ </row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>delay_time</structfield> <type>double precision</type>
+ </para>
+ <para>
+ Time spent in vacuum cost-based delay while processing this index, in milliseconds.
+ </para></entry>
+ </row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>total_time</structfield> <type>double precision</type>
+ </para>
+ <para>
+ Total time spent vacuuming this index, in milliseconds.
+ </para></entry>
+ </row>
<row>
<entry role="catalog_table_entry"><para role="column_definition">
<structfield>wal_records</structfield> <type>bigint</type>
@@ -6257,6 +6321,38 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx
Number of vacuum operations in this database that engaged the wraparound failsafe mechanism.
</para></entry>
</row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>blk_read_time</structfield> <type>double precision</type>
+ </para>
+ <para>
+ Time spent reading blocks by vacuum operations in this database, in milliseconds (if <varname>track_io_timing</varname> is enabled).
+ </para></entry>
+ </row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>blk_write_time</structfield> <type>double precision</type>
+ </para>
+ <para>
+ Time spent writing blocks by vacuum operations in this database, in milliseconds (if <varname>track_io_timing</varname> is enabled).
+ </para></entry>
+ </row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>delay_time</structfield> <type>double precision</type>
+ </para>
+ <para>
+ Time spent in vacuum cost-based delay by vacuum operations in this database, in milliseconds.
+ </para></entry>
+ </row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>total_time</structfield> <type>double precision</type>
+ </para>
+ <para>
+ Total time spent by vacuum operations in this database, in milliseconds.
+ </para></entry>
+ </row>
<row>
<entry role="catalog_table_entry"><para role="column_definition">
<structfield>wal_records</structfield> <type>bigint</type>
@@ -6281,6 +6377,7 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx
Total amount of WAL generated by vacuum operations in this database, in bytes.
</para></entry>
</row>
+
<row>
<entry role="catalog_table_entry"><para role="column_definition">
<structfield>interrupts_count</structfield> <type>integer</type>
diff --git a/src/backend/access/heap/vacuumlazy.c b/src/backend/access/heap/vacuumlazy.c
index 683bfcf9e8..6ae2ccb2de 100644
--- a/src/backend/access/heap/vacuumlazy.c
+++ b/src/backend/access/heap/vacuumlazy.c
@@ -512,13 +512,19 @@ static void restore_vacuum_error_info(LVRelState *vacrel,
static void
extvac_stats_start(Relation rel, LVExtStatCounters * counters)
{
+ TimestampTz starttime;
+
if (!pgstat_track_vacuum_statistics)
return;
memset(counters, 0, sizeof(LVExtStatCounters));
+ starttime = GetCurrentTimestamp();
+
+ counters->starttime = starttime;
counters->walusage = pgWalUsage;
counters->bufusage = pgBufferUsage;
+ counters->VacuumDelayTime = VacuumDelayTime;
counters->blocks_fetched = 0;
counters->blocks_hit = 0;
@@ -546,6 +552,9 @@ extvac_stats_end(Relation rel, LVExtStatCounters * counters,
{
WalUsage walusage;
BufferUsage bufusage;
+ TimestampTz endtime;
+ long secs;
+ int usecs;
if (!pgstat_track_vacuum_statistics)
return;
@@ -559,6 +568,9 @@ extvac_stats_end(Relation rel, LVExtStatCounters * counters,
memset(&bufusage, 0, sizeof(BufferUsage));
BufferUsageAccumDiff(&bufusage, &pgBufferUsage, &counters->bufusage);
+ endtime = GetCurrentTimestamp();
+ TimestampDifference(counters->starttime, endtime, &secs, &usecs);
+
/*
* Fill additional statistics on a vacuum processing operation.
*/
@@ -566,10 +578,19 @@ extvac_stats_end(Relation rel, LVExtStatCounters * counters,
report->total_blks_hit += bufusage.local_blks_hit + bufusage.shared_blks_hit;
report->total_blks_dirtied += bufusage.local_blks_dirtied + bufusage.shared_blks_dirtied;
report->total_blks_written += bufusage.shared_blks_written;
+
report->wal_records += walusage.wal_records;
report->wal_fpi += walusage.wal_fpi;
report->wal_bytes += walusage.wal_bytes;
+ report->blk_read_time += INSTR_TIME_GET_MILLISEC(bufusage.local_blk_read_time);
+ report->blk_read_time += INSTR_TIME_GET_MILLISEC(bufusage.shared_blk_read_time);
+ report->blk_write_time += INSTR_TIME_GET_MILLISEC(bufusage.local_blk_write_time);
+ report->blk_write_time += INSTR_TIME_GET_MILLISEC(bufusage.shared_blk_write_time);
+ report->delay_time += VacuumDelayTime - counters->VacuumDelayTime;
+
+ report->total_time += secs * 1000. + usecs / 1000.;
+
if (!rel->pgstat_info || !pgstat_track_counts)
/*
@@ -669,6 +690,8 @@ accumulate_heap_vacuum_statistics(LVRelState *vacrel, PgStat_VacuumRelationCount
extVacStats->table.missed_dead_pages = vacrel->missed_dead_pages;
extVacStats->common.wraparound_failsafe_count = vacrel->wraparound_failsafe;
+ extVacStats->common.blk_read_time -= vacrel->extVacReportIdx.common.blk_read_time;
+ extVacStats->common.blk_write_time -= vacrel->extVacReportIdx.common.blk_write_time;
extVacStats->common.total_blks_dirtied -= vacrel->extVacReportIdx.common.total_blks_dirtied;
extVacStats->common.total_blks_hit -= vacrel->extVacReportIdx.common.total_blks_hit;
extVacStats->common.total_blks_read -= vacrel->extVacReportIdx.common.total_blks_read;
@@ -676,12 +699,17 @@ accumulate_heap_vacuum_statistics(LVRelState *vacrel, PgStat_VacuumRelationCount
extVacStats->common.wal_bytes -= vacrel->extVacReportIdx.common.wal_bytes;
extVacStats->common.wal_fpi -= vacrel->extVacReportIdx.common.wal_fpi;
extVacStats->common.wal_records -= vacrel->extVacReportIdx.common.wal_records;
+
+ extVacStats->common.total_time -= vacrel->extVacReportIdx.common.total_time;
+ extVacStats->common.delay_time -= vacrel->extVacReportIdx.common.delay_time;
}
static void
accumulate_idxs_vacuum_statistics(LVRelState *vacrel, PgStat_VacuumRelationCounts * extVacIdxStats)
{
/* Fill heap-specific extended stats fields */
+ vacrel->extVacReportIdx.common.blk_read_time += extVacIdxStats->common.blk_read_time;
+ vacrel->extVacReportIdx.common.blk_write_time += extVacIdxStats->common.blk_write_time;
vacrel->extVacReportIdx.common.total_blks_dirtied += extVacIdxStats->common.total_blks_dirtied;
vacrel->extVacReportIdx.common.total_blks_hit += extVacIdxStats->common.total_blks_hit;
vacrel->extVacReportIdx.common.total_blks_read += extVacIdxStats->common.total_blks_read;
@@ -689,6 +717,8 @@ accumulate_idxs_vacuum_statistics(LVRelState *vacrel, PgStat_VacuumRelationCount
vacrel->extVacReportIdx.common.wal_bytes += extVacIdxStats->common.wal_bytes;
vacrel->extVacReportIdx.common.wal_fpi += extVacIdxStats->common.wal_fpi;
vacrel->extVacReportIdx.common.wal_records += extVacIdxStats->common.wal_records;
+ vacrel->extVacReportIdx.common.delay_time += extVacIdxStats->common.delay_time;
+ vacrel->extVacReportIdx.common.total_time += extVacIdxStats->common.total_time;
}
/*
@@ -716,6 +746,10 @@ extvac_accumulate_idx_report(PgStat_VacuumRelationCounts * dst,
dst->common.wal_records += src->common.wal_records;
dst->common.wal_fpi += src->common.wal_fpi;
dst->common.wal_bytes += src->common.wal_bytes;
+ dst->common.blk_read_time += src->common.blk_read_time;
+ dst->common.blk_write_time += src->common.blk_write_time;
+ dst->common.delay_time += src->common.delay_time;
+ dst->common.total_time += src->common.total_time;
dst->common.tuples_deleted += src->common.tuples_deleted;
dst->index.pages_deleted += src->index.pages_deleted;
@@ -904,7 +938,6 @@ heap_vacuum_rel(Relation rel, const VacuumParams *params,
Size dead_items_max_bytes = 0;
LVExtStatCounters extVacCounters;
PgStat_VacuumRelationCounts extVacReport;
- TimestampTz starttime;
/* Initialize vacuum statistics */
memset(&extVacReport, 0, sizeof(PgStat_VacuumRelationCounts));
@@ -922,7 +955,6 @@ heap_vacuum_rel(Relation rel, const VacuumParams *params,
}
}
- starttime = GetCurrentTimestamp();
extvac_stats_start(rel, &extVacCounters);
pgstat_progress_start_command(PROGRESS_COMMAND_VACUUM,
@@ -1279,7 +1311,7 @@ heap_vacuum_rel(Relation rel, const VacuumParams *params,
Max(vacrel->new_live_tuples, 0),
vacrel->recently_dead_tuples +
vacrel->missed_dead_tuples,
- starttime);
+ extVacCounters.starttime);
pgstat_progress_end_command();
if (instrument)
@@ -1287,7 +1319,7 @@ heap_vacuum_rel(Relation rel, const VacuumParams *params,
TimestampTz endtime = GetCurrentTimestamp();
if (verbose || params->log_vacuum_min_duration == 0 ||
- TimestampDifferenceExceeds(starttime, endtime,
+ TimestampDifferenceExceeds(extVacCounters.starttime, endtime,
params->log_vacuum_min_duration))
{
long secs_dur;
@@ -1303,7 +1335,7 @@ heap_vacuum_rel(Relation rel, const VacuumParams *params,
int64 total_blks_read;
int64 total_blks_dirtied;
- TimestampDifference(starttime, endtime, &secs_dur, &usecs_dur);
+ TimestampDifference(extVacCounters.starttime, endtime, &secs_dur, &usecs_dur);
memset(&walusage, 0, sizeof(WalUsage));
WalUsageAccumDiff(&walusage, &pgWalUsage, &startwalusage);
memset(&bufferusage, 0, sizeof(BufferUsage));
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index d0a58f221c..11029edae3 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -1582,6 +1582,10 @@ CREATE VIEW pg_stat_vacuum_tables AS
S.total_blks_written AS total_blks_written,
S.rel_blks_read AS rel_blks_read,
S.rel_blks_hit AS rel_blks_hit,
+ S.blk_read_time AS blk_read_time,
+ S.blk_write_time AS blk_write_time,
+ S.delay_time AS delay_time,
+ S.total_time AS total_time,
S.wraparound_failsafe AS wraparound_failsafe,
S.wal_records AS wal_records,
S.wal_fpi AS wal_fpi,
@@ -1607,8 +1611,15 @@ CREATE VIEW pg_stat_vacuum_indexes AS
S.total_blks_hit AS total_blks_hit,
S.total_blks_dirtied AS total_blks_dirtied,
S.total_blks_written AS total_blks_written,
+
S.rel_blks_read AS rel_blks_read,
S.rel_blks_hit AS rel_blks_hit,
+
+ S.blk_read_time AS blk_read_time,
+ S.blk_write_time AS blk_write_time,
+ S.delay_time AS delay_time,
+ S.total_time AS total_time,
+
S.wal_records AS wal_records,
S.wal_fpi AS wal_fpi,
S.wal_bytes AS wal_bytes
@@ -1631,7 +1642,14 @@ CREATE VIEW pg_stat_vacuum_database AS
S.db_blks_hit AS db_blks_hit,
S.total_blks_dirtied AS total_blks_dirtied,
S.total_blks_written AS total_blks_written,
+
S.wraparound_failsafe AS wraparound_failsafe,
+
+ S.blk_read_time AS blk_read_time,
+ S.blk_write_time AS blk_write_time,
+ S.delay_time AS delay_time,
+ S.total_time AS total_time,
+
S.wal_records AS wal_records,
S.wal_fpi AS wal_fpi,
S.wal_bytes AS wal_bytes,
diff --git a/src/backend/commands/vacuumparallel.c b/src/backend/commands/vacuumparallel.c
index ea13859485..563a1db912 100644
--- a/src/backend/commands/vacuumparallel.c
+++ b/src/backend/commands/vacuumparallel.c
@@ -1355,6 +1355,7 @@ parallel_vacuum_main(dsm_segment *seg, shm_toc *toc)
VacuumUpdateCosts();
VacuumCostBalance = 0;
+ VacuumDelayTime = 0;
VacuumCostBalanceLocal = 0;
VacuumSharedCostBalance = &(shared->cost_balance);
VacuumActiveNWorkers = &(shared->active_nworkers);
diff --git a/src/backend/utils/activity/pgstat_vacuum.c b/src/backend/utils/activity/pgstat_vacuum.c
index b1c747f255..c5ea644060 100644
--- a/src/backend/utils/activity/pgstat_vacuum.c
+++ b/src/backend/utils/activity/pgstat_vacuum.c
@@ -50,6 +50,11 @@ pgstat_accumulate_common(PgStat_CommonCounts *dst, const PgStat_CommonCounts *sr
ACCUMULATE_FIELD(wal_fpi);
ACCUMULATE_FIELD(wal_bytes);
+ ACCUMULATE_FIELD(blk_read_time);
+ ACCUMULATE_FIELD(blk_write_time);
+ ACCUMULATE_FIELD(delay_time);
+ ACCUMULATE_FIELD(total_time);
+
ACCUMULATE_FIELD(tuples_deleted);
ACCUMULATE_FIELD(interrupts_count);
}
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 7473ae1eb6..2d13604bd0 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -2374,7 +2374,7 @@ pg_stat_have_stats(PG_FUNCTION_ARGS)
Datum
pg_stat_get_vacuum_tables(PG_FUNCTION_ARGS)
{
-#define PG_STAT_GET_VACUUM_TABLES_STATS_COLS 21
+#define PG_STAT_GET_VACUUM_TABLES_STATS_COLS 25
Oid relid = PG_GETARG_OID(0);
PgStat_VacuumRelationCounts *extvacuum;
@@ -2417,6 +2417,10 @@ pg_stat_get_vacuum_tables(PG_FUNCTION_ARGS)
values[i++] = Int64GetDatum(extvacuum->common.total_blks_written);
values[i++] = Int64GetDatum(extvacuum->common.blks_fetched - extvacuum->common.blks_hit);
values[i++] = Int64GetDatum(extvacuum->common.blks_hit);
+ values[i++] = Float8GetDatum(extvacuum->common.blk_read_time);
+ values[i++] = Float8GetDatum(extvacuum->common.blk_write_time);
+ values[i++] = Float8GetDatum(extvacuum->common.delay_time);
+ values[i++] = Float8GetDatum(extvacuum->common.total_time);
values[i++] = Int32GetDatum(extvacuum->common.wraparound_failsafe_count);
values[i++] = Int64GetDatum(extvacuum->common.wal_records);
values[i++] = Int64GetDatum(extvacuum->common.wal_fpi);
@@ -2438,7 +2442,7 @@ pg_stat_get_vacuum_tables(PG_FUNCTION_ARGS)
Datum
pg_stat_get_vacuum_indexes(PG_FUNCTION_ARGS)
{
-#define PG_STAT_GET_VACUUM_INDEX_STATS_COLS 12
+#define PG_STAT_GET_VACUUM_INDEX_STATS_COLS 16
Oid relid = PG_GETARG_OID(0);
PgStat_VacuumRelationCounts *extvacuum;
@@ -2475,6 +2479,11 @@ pg_stat_get_vacuum_indexes(PG_FUNCTION_ARGS)
values[i++] = Int64GetDatum(extvacuum->common.blks_fetched - extvacuum->common.blks_hit);
values[i++] = Int64GetDatum(extvacuum->common.blks_hit);
+ values[i++] = Float8GetDatum(extvacuum->common.blk_read_time);
+ values[i++] = Float8GetDatum(extvacuum->common.blk_write_time);
+ values[i++] = Float8GetDatum(extvacuum->common.delay_time);
+ values[i++] = Float8GetDatum(extvacuum->common.total_time);
+
values[i++] = Int64GetDatum(extvacuum->common.wal_records);
values[i++] = Int64GetDatum(extvacuum->common.wal_fpi);
snprintf(buf, sizeof buf, UINT64_FORMAT, extvacuum->common.wal_bytes);
@@ -2482,6 +2491,7 @@ pg_stat_get_vacuum_indexes(PG_FUNCTION_ARGS)
CStringGetDatum(buf),
ObjectIdGetDatum(0),
Int32GetDatum(-1));
+
Assert(i == PG_STAT_GET_VACUUM_INDEX_STATS_COLS);
/* Returns the record as Datum */
@@ -2494,7 +2504,7 @@ pg_stat_get_vacuum_indexes(PG_FUNCTION_ARGS)
Datum
pg_stat_get_vacuum_database(PG_FUNCTION_ARGS)
{
-#define PG_STAT_GET_VACUUM_DATABASE_STATS_COLS 11
+#define PG_STAT_GET_VACUUM_DATABASE_STATS_COLS 15
Oid dbid = PG_GETARG_OID(0);
PgStat_VacuumDBCounts *extvacuum;
@@ -2525,6 +2535,11 @@ pg_stat_get_vacuum_database(PG_FUNCTION_ARGS)
values[i++] = Int32GetDatum(extvacuum->common.wraparound_failsafe_count);
+ values[i++] = Float8GetDatum(extvacuum->common.blk_read_time);
+ values[i++] = Float8GetDatum(extvacuum->common.blk_write_time);
+ values[i++] = Float8GetDatum(extvacuum->common.delay_time);
+ values[i++] = Float8GetDatum(extvacuum->common.total_time);
+
values[i++] = Int64GetDatum(extvacuum->common.wal_records);
values[i++] = Int64GetDatum(extvacuum->common.wal_fpi);
snprintf(buf, sizeof buf, UINT64_FORMAT, extvacuum->common.wal_bytes);
@@ -2533,6 +2548,7 @@ pg_stat_get_vacuum_database(PG_FUNCTION_ARGS)
ObjectIdGetDatum(0),
Int32GetDatum(-1));
values[i++] = Int32GetDatum(extvacuum->common.interrupts_count);
+
Assert(i == PG_STAT_GET_VACUUM_DATABASE_STATS_COLS);
/* Returns the record as Datum */
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 743ce9dfd6..e02d386944 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12643,9 +12643,9 @@
proname => 'pg_stat_get_vacuum_tables', prorows => 1000, provolatile => 's', prorettype => 'record', proisstrict => 'f',
proretset => 't',
proargtypes => 'oid',
- proallargtypes => '{oid,oid,int8,int8,int8,int8,int8,int8,int8,int8,int8,int8,int8,int8,int8,int8,int8,int8,int4,int8,int8,numeric}',
- proargmodes => '{i,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o}',
- proargnames => '{reloid,relid,pages_scanned,pages_removed,tuples_deleted,tuples_frozen,recently_dead_tuples,missed_dead_pages,missed_dead_tuples,vm_new_frozen_pages,vm_new_visible_pages,vm_new_visible_frozen_pages,total_blks_read,total_blks_hit,total_blks_dirtied,total_blks_written,rel_blks_read,rel_blks_hit,wraparound_failsafe,wal_records,wal_fpi,wal_bytes}',
+ proallargtypes => '{oid,oid,int8,int8,int8,int8,int8,int8,int8,int8,int8,int8,int8,int8,int8,int8,int8,int8,float8,float8,float8,float8,int4,int8,int8,numeric}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o}',
+ proargnames => '{reloid,relid,pages_scanned,pages_removed,tuples_deleted,tuples_frozen,recently_dead_tuples,missed_dead_pages,missed_dead_tuples,vm_new_frozen_pages,vm_new_visible_pages,vm_new_visible_frozen_pages,total_blks_read,total_blks_hit,total_blks_dirtied,total_blks_written,rel_blks_read,rel_blks_hit,blk_read_time,blk_write_time,delay_time,total_time,wraparound_failsafe,wal_records,wal_fpi,wal_bytes}',
prosrc => 'pg_stat_get_vacuum_tables' }
# oid8 related functions
@@ -12718,17 +12718,17 @@
proname => 'pg_stat_get_vacuum_indexes', prorows => 1000, provolatile => 's', prorettype => 'record', proisstrict => 'f',
proretset => 't',
proargtypes => 'oid',
- proallargtypes => '{oid,oid,int8,int8,int8,int8,int8,int8,int8,int8,int8,int8,numeric}',
- proargmodes => '{i,o,o,o,o,o,o,o,o,o,o,o,o}',
- proargnames => '{reloid,relid,pages_deleted,tuples_deleted,total_blks_read,total_blks_hit,total_blks_dirtied,total_blks_written,rel_blks_read,rel_blks_hit,wal_records,wal_fpi,wal_bytes}',
+ proallargtypes => '{oid,oid,int8,int8,int8,int8,int8,int8,int8,int8,float8,float8,float8,float8,int8,int8,numeric}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o}',
+ proargnames => '{reloid,relid,pages_deleted,tuples_deleted,total_blks_read,total_blks_hit,total_blks_dirtied,total_blks_written,rel_blks_read,rel_blks_hit,blk_read_time,blk_write_time,delay_time,total_time,wal_records,wal_fpi,wal_bytes}',
prosrc => 'pg_stat_get_vacuum_indexes' },
{ oid => '8005',
descr => 'pg_stat_get_vacuum_database returns vacuum stats values for database',
proname => 'pg_stat_get_vacuum_database', prorows => 1000, provolatile => 's', prorettype => 'record', proisstrict => 'f',
proretset => 't',
proargtypes => 'oid',
- proallargtypes => '{oid,oid,int4,int8,int8,int8,int8,int4,int8,int8,numeric,int4}',
- proargmodes => '{i,o,o,o,o,o,o,o,o,o,o,o}',
- proargnames => '{dbid,dboid,errors,db_blks_read,db_blks_hit,total_blks_dirtied,total_blks_written,wraparound_failsafe,wal_records,wal_fpi,wal_bytes,interrupts_count}',
+ proallargtypes => '{oid,oid,int4,int8,int8,int8,int8,int4,float8,float8,float8,float8,int8,int8,numeric,int4}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o}',
+ proargnames => '{dbid,dboid,errors,db_blks_read,db_blks_hit,total_blks_dirtied,total_blks_written,wraparound_failsafe,blk_read_time,blk_write_time,delay_time,total_time,wal_records,wal_fpi,wal_bytes,interrupts_count}',
prosrc => 'pg_stat_get_vacuum_database' },
]
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 18fa9ac5ba..6a9e87cf0b 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -190,6 +190,12 @@ typedef struct PgStat_CommonCounts
int64 wal_fpi;
uint64 wal_bytes;
+ /* Time */
+ double blk_read_time;
+ double blk_write_time;
+ double delay_time;
+ double total_time;
+
/* tuples */
int64 tuples_deleted;
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index c650cfd664..942361dd42 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -2429,12 +2429,16 @@ pg_stat_vacuum_database| SELECT d.oid AS dboid,
s.total_blks_dirtied,
s.total_blks_written,
s.wraparound_failsafe,
+ s.blk_read_time,
+ s.blk_write_time,
+ s.delay_time,
+ s.total_time,
s.wal_records,
s.wal_fpi,
s.wal_bytes,
s.interrupts_count
FROM pg_database d,
- LATERAL pg_stat_get_vacuum_database(d.oid) s(dboid, errors, db_blks_read, db_blks_hit, total_blks_dirtied, total_blks_written, wraparound_failsafe, wal_records, wal_fpi, wal_bytes, interrupts_count);
+ LATERAL pg_stat_get_vacuum_database(d.oid) s(dboid, errors, db_blks_read, db_blks_hit, total_blks_dirtied, total_blks_written, wraparound_failsafe, blk_read_time, blk_write_time, delay_time, total_time, wal_records, wal_fpi, wal_bytes, interrupts_count);
pg_stat_vacuum_indexes| SELECT c.oid AS relid,
i.oid AS indexrelid,
n.nspname AS schemaname,
@@ -2448,6 +2452,10 @@ pg_stat_vacuum_indexes| SELECT c.oid AS relid,
s.total_blks_written,
s.rel_blks_read,
s.rel_blks_hit,
+ s.blk_read_time,
+ s.blk_write_time,
+ s.delay_time,
+ s.total_time,
s.wal_records,
s.wal_fpi,
s.wal_bytes
@@ -2455,7 +2463,7 @@ pg_stat_vacuum_indexes| SELECT c.oid AS relid,
JOIN pg_index x ON ((c.oid = x.indrelid)))
JOIN pg_class i ON ((i.oid = x.indexrelid)))
LEFT JOIN pg_namespace n ON ((n.oid = c.relnamespace))),
- LATERAL pg_stat_get_vacuum_indexes(i.oid) s(relid, pages_deleted, tuples_deleted, total_blks_read, total_blks_hit, total_blks_dirtied, total_blks_written, rel_blks_read, rel_blks_hit, wal_records, wal_fpi, wal_bytes)
+ LATERAL pg_stat_get_vacuum_indexes(i.oid) s(relid, pages_deleted, tuples_deleted, total_blks_read, total_blks_hit, total_blks_dirtied, total_blks_written, rel_blks_read, rel_blks_hit, blk_read_time, blk_write_time, delay_time, total_time, wal_records, wal_fpi, wal_bytes)
WHERE (c.relkind = ANY (ARRAY['r'::"char", 't'::"char", 'm'::"char"]));
pg_stat_vacuum_tables| SELECT n.nspname AS schemaname,
c.relname,
@@ -2476,13 +2484,17 @@ pg_stat_vacuum_tables| SELECT n.nspname AS schemaname,
s.total_blks_written,
s.rel_blks_read,
s.rel_blks_hit,
+ s.blk_read_time,
+ s.blk_write_time,
+ s.delay_time,
+ s.total_time,
s.wraparound_failsafe,
s.wal_records,
s.wal_fpi,
s.wal_bytes
FROM (pg_class c
JOIN pg_namespace n ON ((n.oid = c.relnamespace))),
- LATERAL pg_stat_get_vacuum_tables(c.oid) s(relid, pages_scanned, pages_removed, tuples_deleted, tuples_frozen, recently_dead_tuples, missed_dead_pages, missed_dead_tuples, vm_new_frozen_pages, vm_new_visible_pages, vm_new_visible_frozen_pages, total_blks_read, total_blks_hit, total_blks_dirtied, total_blks_written, rel_blks_read, rel_blks_hit, wraparound_failsafe, wal_records, wal_fpi, wal_bytes)
+ LATERAL pg_stat_get_vacuum_tables(c.oid) s(relid, pages_scanned, pages_removed, tuples_deleted, tuples_frozen, recently_dead_tuples, missed_dead_pages, missed_dead_tuples, vm_new_frozen_pages, vm_new_visible_pages, vm_new_visible_frozen_pages, total_blks_read, total_blks_hit, total_blks_dirtied, total_blks_written, rel_blks_read, rel_blks_hit, blk_read_time, blk_write_time, delay_time, total_time, wraparound_failsafe, wal_records, wal_fpi, wal_bytes)
WHERE (c.relkind = ANY (ARRAY['r'::"char", 't'::"char", 'm'::"char"]));
pg_stat_wal| SELECT wal_records,
wal_fpi,
diff --git a/src/test/regress/expected/vacuum_stats.out b/src/test/regress/expected/vacuum_stats.out
index 3d823ef7b6..bd3c1512d8 100644
--- a/src/test/regress/expected/vacuum_stats.out
+++ b/src/test/regress/expected/vacuum_stats.out
@@ -148,6 +148,49 @@ SELECT rel_blks_read >= 0 AS rel_blks_read,
t | t
(1 row)
+-- timing metrics and failsafe. The vacuum always takes some wall-clock time
+-- (total_time > 0) and does not engage the wraparound failsafe under normal
+-- conditions (wraparound_failsafe = 0). blk_read_time/blk_write_time are only
+-- non-zero when track_io_timing is enabled, and delay_time only when a vacuum
+-- cost delay is configured, so those are merely checked for being
+-- non-negative; the positive delay_time path is exercised separately below.
+SELECT blk_read_time >= 0 AS blk_read_time,
+ blk_write_time >= 0 AS blk_write_time,
+ delay_time >= 0 AS delay_time,
+ total_time > 0 AS total_time,
+ wraparound_failsafe = 0 AS wraparound_failsafe
+ FROM pg_stat_vacuum_tables WHERE relname = 'vacstat_t';
+ blk_read_time | blk_write_time | delay_time | total_time | wraparound_failsafe
+---------------+----------------+------------+------------+---------------------
+ t | t | t | t | t
+(1 row)
+
+-- delay path: with a vacuum cost delay configured, a vacuum that accrues cost
+-- sleeps, so delay_time advances.
+CREATE TABLE vacstat_delay (id int PRIMARY KEY, v text)
+ WITH (autovacuum_enabled = off, fillfactor = 10);
+INSERT INTO vacstat_delay SELECT g, repeat('x', 100) FROM generate_series(1, 3000) g;
+DELETE FROM vacstat_delay WHERE id % 2 = 0;
+SET vacuum_cost_delay = '1ms';
+SET vacuum_cost_limit = 1;
+VACUUM vacstat_delay;
+SELECT pg_stat_force_next_flush();
+ pg_stat_force_next_flush
+--------------------------
+
+(1 row)
+
+RESET vacuum_cost_delay;
+RESET vacuum_cost_limit;
+SELECT delay_time > 0 AS delay_time,
+ total_time > 0 AS total_time
+ FROM pg_stat_vacuum_tables WHERE relname = 'vacstat_delay';
+ delay_time | total_time
+------------+------------
+ t | t
+(1 row)
+
+DROP TABLE vacstat_delay;
-- WAL metrics. A vacuum that removes tuples always emits WAL
-- (wal_records > 0, wal_bytes > 0). wal_fpi depends on whether a checkpoint
-- happened recently, so it is only checked for being non-negative here; the
@@ -186,17 +229,6 @@ SELECT wal_records > 0 AS wal_records,
(1 row)
DROP TABLE vacstat_fpi;
--- wraparound failsafe. A normal vacuum does not engage the wraparound
--- failsafe (wraparound_failsafe = 0). The positive path requires reaching the
--- failsafe XID age and is covered by a separate TAP test under
--- src/test/modules/xid_wraparound.
-SELECT wraparound_failsafe = 0 AS wraparound_failsafe
- FROM pg_stat_vacuum_tables WHERE relname = 'vacstat_t';
- wraparound_failsafe
----------------------
- t
-(1 row)
-
-- per-index view: the primary key index is processed by the same VACUUM.
-- No btree leaf empties out (interleaved deletions), so pages_deleted = 0,
-- while every index entry for a removed heap tuple is deleted. The index is
@@ -211,13 +243,17 @@ SELECT indexrelname,
total_blks_written >= 0 AS total_blks_written,
rel_blks_read >= 0 AS rel_blks_read,
rel_blks_hit > 0 AS rel_blks_hit,
+ blk_read_time >= 0 AS blk_read_time,
+ blk_write_time >= 0 AS blk_write_time,
+ delay_time >= 0 AS delay_time,
+ total_time > 0 AS total_time,
wal_records > 0 AS wal_records,
wal_fpi >= 0 AS wal_fpi,
wal_bytes > 0 AS wal_bytes
FROM pg_stat_vacuum_indexes WHERE relname = 'vacstat_t' ORDER BY indexrelname;
- indexrelname | pages_deleted | tuples_deleted | total_blks_read | total_blks_hit | total_blks_dirtied | total_blks_written | rel_blks_read | rel_blks_hit | wal_records | wal_fpi | wal_bytes
-----------------+---------------+----------------+-----------------+----------------+--------------------+--------------------+---------------+--------------+-------------+---------+-----------
- vacstat_t_pkey | t | t | t | t | t | t | t | t | t | t | t
+ indexrelname | pages_deleted | tuples_deleted | total_blks_read | total_blks_hit | total_blks_dirtied | total_blks_written | rel_blks_read | rel_blks_hit | blk_read_time | blk_write_time | delay_time | total_time | wal_records | wal_fpi | wal_bytes
+----------------+---------------+----------------+-----------------+----------------+--------------------+--------------------+---------------+--------------+---------------+----------------+------------+------------+-------------+---------+-----------
+ vacstat_t_pkey | t | t | t | t | t | t | t | t | t | t | t | t | t | t | t
(1 row)
-- index page-deletion path: deleting a contiguous key range empties whole
@@ -253,20 +289,24 @@ SELECT indexrelname,
DROP TABLE vacstat_idxdel;
-- per-database aggregate view: no vacuum errors occurred in this database, and
-- the vacuums in this database touched pages through the buffer cache
--- (db_blks_hit > 0) and emit WAL (wal_records > 0).
+-- (db_blks_hit > 0).
SELECT errors = 0 AS errors,
db_blks_read >= 0 AS db_blks_read,
db_blks_hit > 0 AS db_blks_hit,
total_blks_dirtied >= 0 AS total_blks_dirtied,
total_blks_written >= 0 AS total_blks_written,
wraparound_failsafe = 0 AS wraparound_failsafe,
+ blk_read_time >= 0 AS blk_read_time,
+ blk_write_time >= 0 AS blk_write_time,
+ delay_time >= 0 AS delay_time,
+ total_time > 0 AS total_time,
wal_records > 0 AS wal_records,
wal_fpi >= 0 AS wal_fpi,
wal_bytes > 0 AS wal_bytes
FROM pg_stat_vacuum_database WHERE dbname = current_database();
- errors | db_blks_read | db_blks_hit | total_blks_dirtied | total_blks_written | wraparound_failsafe | wal_records | wal_fpi | wal_bytes
---------+--------------+-------------+--------------------+--------------------+---------------------+-------------+---------+-----------
- t | t | t | t | t | t | t | t | t
+ errors | db_blks_read | db_blks_hit | total_blks_dirtied | total_blks_written | wraparound_failsafe | blk_read_time | blk_write_time | delay_time | total_time | wal_records | wal_fpi | wal_bytes
+--------+--------------+-------------+--------------------+--------------------+---------------------+---------------+----------------+------------+------------+-------------+---------+-----------
+ t | t | t | t | t | t | t | t | t | t | t | t | t
(1 row)
-- parallel index vacuum: index statistics must be captured for indexes
diff --git a/src/test/regress/sql/vacuum_stats.sql b/src/test/regress/sql/vacuum_stats.sql
index 40f7d0985d..baf5acdafd 100644
--- a/src/test/regress/sql/vacuum_stats.sql
+++ b/src/test/regress/sql/vacuum_stats.sql
@@ -98,6 +98,36 @@ SELECT rel_blks_read >= 0 AS rel_blks_read,
rel_blks_hit > 0 AS rel_blks_hit
FROM pg_stat_vacuum_tables WHERE relname = 'vacstat_t';
+-- timing metrics and failsafe. The vacuum always takes some wall-clock time
+-- (total_time > 0) and does not engage the wraparound failsafe under normal
+-- conditions (wraparound_failsafe = 0). blk_read_time/blk_write_time are only
+-- non-zero when track_io_timing is enabled, and delay_time only when a vacuum
+-- cost delay is configured, so those are merely checked for being
+-- non-negative; the positive delay_time path is exercised separately below.
+SELECT blk_read_time >= 0 AS blk_read_time,
+ blk_write_time >= 0 AS blk_write_time,
+ delay_time >= 0 AS delay_time,
+ total_time > 0 AS total_time,
+ wraparound_failsafe = 0 AS wraparound_failsafe
+ FROM pg_stat_vacuum_tables WHERE relname = 'vacstat_t';
+
+-- delay path: with a vacuum cost delay configured, a vacuum that accrues cost
+-- sleeps, so delay_time advances.
+CREATE TABLE vacstat_delay (id int PRIMARY KEY, v text)
+ WITH (autovacuum_enabled = off, fillfactor = 10);
+INSERT INTO vacstat_delay SELECT g, repeat('x', 100) FROM generate_series(1, 3000) g;
+DELETE FROM vacstat_delay WHERE id % 2 = 0;
+SET vacuum_cost_delay = '1ms';
+SET vacuum_cost_limit = 1;
+VACUUM vacstat_delay;
+SELECT pg_stat_force_next_flush();
+RESET vacuum_cost_delay;
+RESET vacuum_cost_limit;
+SELECT delay_time > 0 AS delay_time,
+ total_time > 0 AS total_time
+ FROM pg_stat_vacuum_tables WHERE relname = 'vacstat_delay';
+DROP TABLE vacstat_delay;
+
-- WAL metrics. A vacuum that removes tuples always emits WAL
-- (wal_records > 0, wal_bytes > 0). wal_fpi depends on whether a checkpoint
-- happened recently, so it is only checked for being non-negative here; the
@@ -123,13 +153,6 @@ SELECT wal_records > 0 AS wal_records,
FROM pg_stat_vacuum_tables WHERE relname = 'vacstat_fpi';
DROP TABLE vacstat_fpi;
--- wraparound failsafe. A normal vacuum does not engage the wraparound
--- failsafe (wraparound_failsafe = 0). The positive path requires reaching the
--- failsafe XID age and is covered by a separate TAP test under
--- src/test/modules/xid_wraparound.
-SELECT wraparound_failsafe = 0 AS wraparound_failsafe
- FROM pg_stat_vacuum_tables WHERE relname = 'vacstat_t';
-
-- per-index view: the primary key index is processed by the same VACUUM.
-- No btree leaf empties out (interleaved deletions), so pages_deleted = 0,
-- while every index entry for a removed heap tuple is deleted. The index is
@@ -144,6 +167,10 @@ SELECT indexrelname,
total_blks_written >= 0 AS total_blks_written,
rel_blks_read >= 0 AS rel_blks_read,
rel_blks_hit > 0 AS rel_blks_hit,
+ blk_read_time >= 0 AS blk_read_time,
+ blk_write_time >= 0 AS blk_write_time,
+ delay_time >= 0 AS delay_time,
+ total_time > 0 AS total_time,
wal_records > 0 AS wal_records,
wal_fpi >= 0 AS wal_fpi,
wal_bytes > 0 AS wal_bytes
@@ -168,13 +195,17 @@ DROP TABLE vacstat_idxdel;
-- per-database aggregate view: no vacuum errors occurred in this database, and
-- the vacuums in this database touched pages through the buffer cache
--- (db_blks_hit > 0) and emit WAL (wal_records > 0).
+-- (db_blks_hit > 0).
SELECT errors = 0 AS errors,
db_blks_read >= 0 AS db_blks_read,
db_blks_hit > 0 AS db_blks_hit,
total_blks_dirtied >= 0 AS total_blks_dirtied,
total_blks_written >= 0 AS total_blks_written,
wraparound_failsafe = 0 AS wraparound_failsafe,
+ blk_read_time >= 0 AS blk_read_time,
+ blk_write_time >= 0 AS blk_write_time,
+ delay_time >= 0 AS delay_time,
+ total_time > 0 AS total_time,
wal_records > 0 AS wal_records,
wal_fpi >= 0 AS wal_fpi,
wal_bytes > 0 AS wal_bytes
--
2.39.5 (Apple Git-154)
Attachments:
[text/plain] 0001-Track-table-VM-stability.patch (21.7K, 3-0001-Track-table-VM-stability.patch)
download | inline diff:
From 4a88c028674fdad4f5db26e8e8c0a608a7f2b797 Mon Sep 17 00:00:00 2001
From: Alena Rybakina <[email protected]>
Date: Mon, 30 Mar 2026 09:07:24 +0300
Subject: [PATCH 1/9] Track table VM stability.
Add visible_page_marks_cleared and frozen_page_marks_cleared 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 visible_page_marks_cleared 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 frozen_page_marks_cleared 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 08d5b82455..3467abf6d8 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 4fd470702a..f055ec3819 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 8f129baec9..b57128bb12 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 b2ca28f83b..92e1f60a08 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 6f9c9c72de..6409987d66 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 be157a5fbe..291b039859 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12693,4 +12693,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 dfa2e83763..7db36cf8ad 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 0000000000..994a8df56d
--- /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 15c33fad4c..b48758a211 100644
--- a/src/test/isolation/isolation_schedule
+++ b/src/test/isolation/isolation_schedule
@@ -127,3 +127,4 @@ test: matview-write-skew
test: lock-nowait
test: for-portion-of
test: ddl-dependency-locking
+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 0000000000..17c204e232
--- /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 a65a5bf0c4..096e4f763f 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] 0002-Extended-vacuum-statistics-core-heap-and-tuple-metri.patch (45.9K, 4-0002-Extended-vacuum-statistics-core-heap-and-tuple-metri.patch)
download | inline diff:
From db60e288bea35ca4ae140be37466d7898e5d5920 Mon Sep 17 00:00:00 2001
From: Alena Rybakina <[email protected]>
Date: Tue, 16 Jun 2026 10:06:53 +0300
Subject: [PATCH 2/9] Extended vacuum statistics: core heap and tuple metrics
for tables and indexes
Expose the core per-table heap and tuple metrics of the last VACUUM through
the new pg_stat_vacuum_tables view and pg_stat_get_vacuum_tables() function,
with documentation and regression coverage:
pages_scanned heap pages examined (not skipped via the VM)
pages_removed heap pages by which storage was truncated
tuples_deleted dead tuples removed by the vacuum
tuples_frozen tuples frozen by the vacuum
The matching per-index core metrics are exposed at the same time through the
new pg_stat_vacuum_indexes view and pg_stat_get_vacuum_indexes() function:
pages_deleted index pages deleted (made reusable) by the vacuum
tuples_deleted index entries removed by the vacuum
This first commit also adds the infrastructure the rest of the series builds
on. Extended vacuum statistics are stored in the cumulative statistics system
under a dedicated stats kind, PGSTAT_KIND_VACUUM_RELATION, kept separate from
the regular relation statistics. They are collected during vacuum and
accumulated per relation, only while the track_vacuum_statistics GUC is
enabled. Indexes are tracked under the same stats kind, with their totals
reported once per index at the end of the vacuum.
---
doc/src/sgml/config.sgml | 20 ++
doc/src/sgml/system-views.sgml | 182 ++++++++++++++++++
src/backend/access/heap/vacuumlazy.c | 78 ++++++++
src/backend/catalog/heap.c | 1 +
src/backend/catalog/index.c | 1 +
src/backend/catalog/system_views.sql | 34 ++++
src/backend/commands/vacuumparallel.c | 27 +++
src/backend/utils/activity/Makefile | 1 +
src/backend/utils/activity/meson.build | 1 +
src/backend/utils/activity/pgstat.c | 15 +-
src/backend/utils/activity/pgstat_relation.c | 6 +
src/backend/utils/activity/pgstat_vacuum.c | 129 +++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 83 ++++++++
src/backend/utils/misc/guc_parameters.dat | 6 +
src/backend/utils/misc/postgresql.conf.sample | 1 +
src/include/catalog/pg_proc.dat | 19 ++
src/include/pgstat.h | 80 +++++++-
src/include/utils/pgstat_internal.h | 8 +
src/include/utils/pgstat_kind.h | 3 +-
src/test/regress/expected/rules.out | 24 +++
src/test/regress/expected/vacuum_stats.out | 114 +++++++++++
src/test/regress/parallel_schedule | 3 +
src/test/regress/sql/vacuum_stats.sql | 70 +++++++
23 files changed, 903 insertions(+), 3 deletions(-)
create mode 100644 src/backend/utils/activity/pgstat_vacuum.c
create mode 100644 src/test/regress/expected/vacuum_stats.out
create mode 100644 src/test/regress/sql/vacuum_stats.sql
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index fa566c9e55..57d7ecfd6f 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -9110,6 +9110,26 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
</listitem>
</varlistentry>
+ <varlistentry id="guc-track-vacuum-statistics" xreflabel="track_vacuum_statistics">
+ <term><varname>track_vacuum_statistics</varname> (<type>boolean</type>)
+ <indexterm>
+ <primary><varname>track_vacuum_statistics</varname> configuration parameter</primary>
+ </indexterm>
+ </term>
+ <listitem>
+ <para>
+ Enables collection of extended statistics about the work performed by
+ <command>VACUUM</command>, reported in the
+ <structname>pg_stat_vacuum_tables</structname>,
+ <structname>pg_stat_vacuum_indexes</structname> and
+ <structname>pg_stat_vacuum_database</structname> views.
+ This parameter is off by default.
+ Only superusers and users with the appropriate <literal>SET</literal>
+ privilege can change this setting.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry id="guc-track-functions" xreflabel="track_functions">
<term><varname>track_functions</varname> (<type>enum</type>)
<indexterm>
diff --git a/doc/src/sgml/system-views.sgml b/doc/src/sgml/system-views.sgml
index 2ebec6928d..19576018df 100644
--- a/doc/src/sgml/system-views.sgml
+++ b/doc/src/sgml/system-views.sgml
@@ -5782,4 +5782,186 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx
</table>
</sect1>
+ <sect1 id="view-pg-stat-vacuum-tables">
+ <title><structname>pg_stat_vacuum_tables</structname></title>
+
+ <indexterm zone="view-pg-stat-vacuum-tables">
+ <primary>pg_stat_vacuum_tables</primary>
+ </indexterm>
+
+ <para>
+ The <structname>pg_stat_vacuum_tables</structname> view will contain one row
+ for each table in the current database, showing extended statistics about
+ the activity of the most recent <command>VACUUM</command> on that table.
+ These statistics are accumulated only while
+ <varname>track_vacuum_statistics</varname> is enabled.
+ </para>
+
+ <table id="view-pg-stat-vacuum-tables-cols">
+ <title><structname>pg_stat_vacuum_tables</structname> Columns</title>
+ <tgroup cols="1">
+ <thead>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ Column Type
+ </para>
+ <para>
+ Description
+ </para></entry>
+ </row>
+ </thead>
+
+ <tbody>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>schemaname</structfield> <type>name</type>
+ </para>
+ <para>
+ Name of the schema that the 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 the table.
+ </para></entry>
+ </row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>relid</structfield> <type>oid</type>
+ </para>
+ <para>
+ OID of the table.
+ </para></entry>
+ </row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>pages_scanned</structfield> <type>bigint</type>
+ </para>
+ <para>
+ Number of heap pages examined by the vacuum (pages that were not skipped using the visibility map).
+ </para></entry>
+ </row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>pages_removed</structfield> <type>bigint</type>
+ </para>
+ <para>
+ Number of heap pages by which the table's storage was physically reduced (truncated) by the vacuum.
+ </para></entry>
+ </row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>tuples_deleted</structfield> <type>bigint</type>
+ </para>
+ <para>
+ Number of dead tuples removed by the vacuum.
+ </para></entry>
+ </row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>tuples_frozen</structfield> <type>bigint</type>
+ </para>
+ <para>
+ Number of tuples frozen by the vacuum.
+ </para></entry>
+ </row>
+ </tbody>
+ </tgroup>
+ </table>
+ </sect1>
+
+ <sect1 id="view-pg-stat-vacuum-indexes">
+ <title><structname>pg_stat_vacuum_indexes</structname></title>
+
+ <indexterm zone="view-pg-stat-vacuum-indexes">
+ <primary>pg_stat_vacuum_indexes</primary>
+ </indexterm>
+
+ <para>
+ The <structname>pg_stat_vacuum_indexes</structname> view will contain one row
+ for each index in the current database, showing extended statistics about
+ the activity of the most recent <command>VACUUM</command> on that index.
+ These statistics are accumulated only while
+ <varname>track_vacuum_statistics</varname> is enabled.
+ </para>
+
+ <table id="view-pg-stat-vacuum-indexes-cols">
+ <title><structname>pg_stat_vacuum_indexes</structname> Columns</title>
+ <tgroup cols="1">
+ <thead>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ Column Type
+ </para>
+ <para>
+ Description
+ </para></entry>
+ </row>
+ </thead>
+
+ <tbody>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>relid</structfield> <type>oid</type>
+ </para>
+ <para>
+ OID of the table the index is on.
+ </para></entry>
+ </row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>indexrelid</structfield> <type>oid</type>
+ </para>
+ <para>
+ OID of the index.
+ </para></entry>
+ </row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>schemaname</structfield> <type>name</type>
+ </para>
+ <para>
+ Name of the schema that the 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 the table.
+ </para></entry>
+ </row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>indexrelname</structfield> <type>name</type>
+ </para>
+ <para>
+ Name of the index.
+ </para></entry>
+ </row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>pages_deleted</structfield> <type>bigint</type>
+ </para>
+ <para>
+ Number of index pages deleted (made reusable) by the vacuum.
+ </para></entry>
+ </row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>tuples_deleted</structfield> <type>bigint</type>
+ </para>
+ <para>
+ Number of index entries removed by the vacuum.
+ </para></entry>
+ </row>
+ </tbody>
+ </tgroup>
+ </table>
+ </sect1>
+
</chapter>
diff --git a/src/backend/access/heap/vacuumlazy.c b/src/backend/access/heap/vacuumlazy.c
index 39395aed0d..ffcfa3d16a 100644
--- a/src/backend/access/heap/vacuumlazy.c
+++ b/src/backend/access/heap/vacuumlazy.c
@@ -283,6 +283,7 @@ typedef struct LVRelState
/* Error reporting state */
char *dbname;
char *relnamespace;
+ Oid reloid;
char *relname;
char *indname; /* Current index name */
BlockNumber blkno; /* used only for heap operations */
@@ -483,6 +484,8 @@ static void update_vacuum_error_info(LVRelState *vacrel,
OffsetNumber offnum);
static void restore_vacuum_error_info(LVRelState *vacrel,
const LVSavedErrInfo *saved_vacrel);
+static void accumulate_heap_vacuum_statistics(LVRelState *vacrel,
+ PgStat_VacuumRelationCounts *extVacStats);
@@ -609,6 +612,63 @@ heap_vacuum_eager_scan_setup(LVRelState *vacrel, const VacuumParams *params)
first_region_ratio;
}
+/*
+ * Fill the extended vacuum statistics report for a heap relation with the
+ * counters that are derived directly from the LVRelState. Buffer, WAL and
+ * timing counters require sampling resource usage around the vacuum and are
+ * gathered separately; they are not touched here.
+ */
+static void
+accumulate_heap_vacuum_statistics(LVRelState *vacrel,
+ PgStat_VacuumRelationCounts *extVacStats)
+{
+ if (!pgstat_track_vacuum_statistics)
+ return;
+
+ extVacStats->type = PGSTAT_EXTVAC_TABLE;
+ extVacStats->table.pages_scanned = vacrel->scanned_pages;
+ extVacStats->table.pages_removed = vacrel->removed_pages;
+ extVacStats->common.tuples_deleted = vacrel->tuples_deleted;
+ extVacStats->table.tuples_frozen = vacrel->tuples_frozen;
+}
+
+/*
+ * Report the per-index extended vacuum statistics, one report per index.
+ *
+ * The per-index counters (pages_deleted and the number of removed index
+ * entries) are derived directly from each index's final IndexBulkDeleteResult,
+ * which already holds the totals accumulated across all bulkdelete and cleanup
+ * passes -- so no per-pass sampling is needed here. Used by the non-parallel
+ * path only; the parallel path reports its DSM-resident results from
+ * parallel_vacuum_end().
+ */
+static void
+report_index_vacuum_extstats(LVRelState *vacrel)
+{
+ if (!pgstat_track_vacuum_statistics)
+ return;
+
+ for (int idx = 0; idx < vacrel->nindexes; idx++)
+ {
+ Relation indrel = vacrel->indrels[idx];
+ IndexBulkDeleteResult *istat = vacrel->indstats[idx];
+ PgStat_VacuumRelationCounts report;
+
+ /* Skip indexes that this vacuum did not process */
+ if (istat == NULL)
+ continue;
+
+ memset(&report, 0, sizeof(report));
+ report.type = PGSTAT_EXTVAC_INDEX;
+ report.common.tuples_deleted = istat->tuples_removed;
+ report.index.pages_deleted = istat->pages_deleted;
+
+ pgstat_report_vacuum_extstats(RelationGetRelid(indrel),
+ indrel->rd_rel->relisshared,
+ &report);
+ }
+}
+
/*
* heap_vacuum_rel() -- perform VACUUM for one heap relation
*
@@ -643,6 +703,10 @@ heap_vacuum_rel(Relation rel, const VacuumParams *params,
ErrorContextCallback errcallback;
char **indnames = NULL;
Size dead_items_max_bytes = 0;
+ PgStat_VacuumRelationCounts extVacReport;
+
+ /* Initialize the extended vacuum statistics report */
+ memset(&extVacReport, 0, sizeof(PgStat_VacuumRelationCounts));
verbose = (params->options & VACOPT_VERBOSE) != 0;
instrument = (verbose || (AmAutoVacuumWorkerProcess() &&
@@ -686,6 +750,7 @@ heap_vacuum_rel(Relation rel, const VacuumParams *params,
vacrel = palloc0_object(LVRelState);
vacrel->dbname = get_database_name(MyDatabaseId);
vacrel->relnamespace = get_namespace_name(RelationGetNamespace(rel));
+ vacrel->reloid = RelationGetRelid(rel);
vacrel->relname = pstrdup(RelationGetRelationName(rel));
vacrel->indname = NULL;
vacrel->phase = VACUUM_ERRCB_PHASE_UNKNOWN;
@@ -700,6 +765,7 @@ heap_vacuum_rel(Relation rel, const VacuumParams *params,
vac_open_indexes(vacrel->rel, RowExclusiveLock, &vacrel->nindexes,
&vacrel->indrels);
vacrel->bstrategy = bstrategy;
+
if (instrument && vacrel->nindexes > 0)
{
/* Copy index names used by instrumentation (not error reporting) */
@@ -985,6 +1051,9 @@ heap_vacuum_rel(Relation rel, const VacuumParams *params,
* soon in cases where the failsafe prevented significant amounts of heap
* vacuuming.
*/
+ accumulate_heap_vacuum_statistics(vacrel, &extVacReport);
+ pgstat_report_vacuum_extstats(vacrel->reloid, rel->rd_rel->relisshared,
+ &extVacReport);
pgstat_report_vacuum(rel,
Max(vacrel->new_live_tuples, 0),
vacrel->recently_dead_tuples +
@@ -1618,6 +1687,15 @@ lazy_scan_heap(LVRelState *vacrel)
/* Do final index cleanup (call each index's amvacuumcleanup routine) */
if (vacrel->nindexes > 0 && vacrel->do_index_cleanup)
lazy_cleanup_all_indexes(vacrel);
+
+ /*
+ * Report the per-index extended vacuum statistics accumulated over all
+ * bulkdelete and cleanup passes, exactly once per index. The parallel
+ * path reports its DSM-resident totals from parallel_vacuum_end() instead,
+ * so only do it here when index vacuuming ran in the leader.
+ */
+ if (vacrel->nindexes > 0 && !ParallelVacuumIsActive(vacrel))
+ report_index_vacuum_extstats(vacrel);
}
/*
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index 88087654de..709fe33e61 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -1903,6 +1903,7 @@ heap_drop_with_catalog(Oid relid)
/* ensure that stats are dropped if transaction commits */
pgstat_drop_relation(rel);
+ pgstat_vacuum_relation_delete_pending_cb(RelationGetRelid(rel));
/*
* Close relcache entry, but *keep* AccessExclusiveLock on the relation
diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c
index 9407c357f2..f986276677 100644
--- a/src/backend/catalog/index.c
+++ b/src/backend/catalog/index.c
@@ -2345,6 +2345,7 @@ index_drop(Oid indexId, bool concurrent, bool concurrent_lock_mode)
/* ensure that stats are dropped if transaction commits */
pgstat_drop_relation(userIndexRelation);
+ pgstat_vacuum_relation_delete_pending_cb(RelationGetRelid(userIndexRelation));
/*
* Close and flush the index's relcache entry, to ensure relcache doesn't
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index b57128bb12..04ca38e51c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -1559,3 +1559,37 @@ CREATE VIEW pg_aios AS
SELECT * FROM pg_get_aios();
REVOKE ALL ON pg_aios FROM PUBLIC;
GRANT SELECT ON pg_aios TO pg_read_all_stats;
+
+CREATE VIEW pg_stat_vacuum_tables AS
+ SELECT
+ N.nspname AS schemaname,
+ C.relname AS relname,
+ S.relid AS relid,
+
+ S.pages_scanned AS pages_scanned,
+ S.pages_removed AS pages_removed,
+ S.tuples_deleted AS tuples_deleted,
+ S.tuples_frozen AS tuples_frozen
+
+ FROM pg_class C JOIN
+ pg_namespace N ON N.oid = C.relnamespace,
+ LATERAL pg_stat_get_vacuum_tables(C.oid) S
+ WHERE C.relkind IN ('r', 't', 'm');
+
+CREATE VIEW pg_stat_vacuum_indexes AS
+ SELECT
+ C.oid AS relid,
+ I.oid AS indexrelid,
+ N.nspname AS schemaname,
+ C.relname AS relname,
+ I.relname AS indexrelname,
+
+ S.pages_deleted AS pages_deleted,
+ S.tuples_deleted AS tuples_deleted
+ FROM
+ pg_class C JOIN
+ pg_index X ON C.oid = X.indrelid JOIN
+ pg_class I ON I.oid = X.indexrelid
+ LEFT JOIN pg_namespace N ON (N.oid = C.relnamespace),
+ LATERAL pg_stat_get_vacuum_indexes(I.oid) S
+ WHERE C.relkind IN ('r', 't', 'm');
diff --git a/src/backend/commands/vacuumparallel.c b/src/backend/commands/vacuumparallel.c
index 41cefcfde5..7725c4ecc1 100644
--- a/src/backend/commands/vacuumparallel.c
+++ b/src/backend/commands/vacuumparallel.c
@@ -535,6 +535,33 @@ parallel_vacuum_end(ParallelVacuumState *pvs, IndexBulkDeleteResult **istats)
DestroyParallelContext(pvs->pcxt);
ExitParallelMode();
+ /*
+ * Report the per-index extended vacuum statistics, one report per index,
+ * derived directly from each index's final IndexBulkDeleteResult. The
+ * indexes are still open here (pvs->indrels is the leader's own array, not
+ * in the now-destroyed DSM).
+ */
+ if (pgstat_track_vacuum_statistics)
+ {
+ for (int i = 0; i < pvs->nindexes; i++)
+ {
+ Relation indrel = pvs->indrels[i];
+ PgStat_VacuumRelationCounts report;
+
+ if (istats[i] == NULL)
+ continue;
+
+ memset(&report, 0, sizeof(report));
+ report.type = PGSTAT_EXTVAC_INDEX;
+ report.common.tuples_deleted = istats[i]->tuples_removed;
+ report.index.pages_deleted = istats[i]->pages_deleted;
+
+ pgstat_report_vacuum_extstats(RelationGetRelid(indrel),
+ indrel->rd_rel->relisshared,
+ &report);
+ }
+ }
+
if (AmAutoVacuumWorkerProcess())
pv_shared_cost_params = NULL;
diff --git a/src/backend/utils/activity/Makefile b/src/backend/utils/activity/Makefile
index ca3ef89bf5..b7db9c034a 100644
--- a/src/backend/utils/activity/Makefile
+++ b/src/backend/utils/activity/Makefile
@@ -28,6 +28,7 @@ OBJS = \
pgstat_io.o \
pgstat_lock.o \
pgstat_relation.o \
+ pgstat_vacuum.o \
pgstat_replslot.o \
pgstat_shmem.o \
pgstat_slru.o \
diff --git a/src/backend/utils/activity/meson.build b/src/backend/utils/activity/meson.build
index 1aa7ece529..2a0b50d07d 100644
--- a/src/backend/utils/activity/meson.build
+++ b/src/backend/utils/activity/meson.build
@@ -17,6 +17,7 @@ backend_sources += files(
'pgstat_shmem.c',
'pgstat_slru.c',
'pgstat_subscription.c',
+ 'pgstat_vacuum.c',
'pgstat_wal.c',
'pgstat_xact.c',
)
diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index b67da88c7d..4e950672e2 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -204,7 +204,7 @@ static inline bool pgstat_is_kind_valid(PgStat_Kind kind);
bool pgstat_track_counts = false;
int pgstat_fetch_consistency = PGSTAT_FETCH_CONSISTENCY_CACHE;
-
+bool pgstat_track_vacuum_statistics = false;
/* ----------
* state shared with pgstat_*.c
@@ -500,6 +500,19 @@ static const PgStat_KindInfo pgstat_kind_builtin_infos[PGSTAT_KIND_BUILTIN_SIZE]
.reset_all_cb = pgstat_wal_reset_all_cb,
.snapshot_cb = pgstat_wal_snapshot_cb,
},
+ [PGSTAT_KIND_VACUUM_RELATION] = {
+ .name = "vacuum statistics",
+
+ .fixed_amount = false,
+ .write_to_file = true,
+
+ .shared_size = sizeof(PgStatShared_VacuumRelation),
+ .shared_data_off = offsetof(PgStatShared_VacuumRelation, stats),
+ .shared_data_len = sizeof(((PgStatShared_VacuumRelation *) 0)->stats),
+ .pending_size = sizeof(PgStat_RelationVacuumPending),
+
+ .flush_pending_cb = pgstat_vacuum_relation_flush_cb
+ },
};
/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index 92e1f60a08..141e2af607 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -904,6 +904,12 @@ pgstat_relation_flush_cb(PgStat_EntryRef *entry_ref, bool nowait)
return true;
}
+void
+pgstat_vacuum_relation_delete_pending_cb(Oid relid)
+{
+ pgstat_drop_transactional(PGSTAT_KIND_VACUUM_RELATION, relid, InvalidOid);
+}
+
void
pgstat_relation_delete_pending_cb(PgStat_EntryRef *entry_ref)
{
diff --git a/src/backend/utils/activity/pgstat_vacuum.c b/src/backend/utils/activity/pgstat_vacuum.c
new file mode 100644
index 0000000000..ac6baee516
--- /dev/null
+++ b/src/backend/utils/activity/pgstat_vacuum.c
@@ -0,0 +1,129 @@
+/* -------------------------------------------------------------------------
+ *
+ * pgstat_vacuum.c
+ * Implementation of extended vacuum statistics.
+ *
+ * This file contains the implementation of extended vacuum statistics. It is
+ * kept separate from pgstat_relation.c and pgstat_database.c to reduce the
+ * memory footprint of the regular relation and database statistics: vacuum
+ * metrics require significantly more space per relation, so they live in their
+ * own PGSTAT_KIND_VACUUM_RELATION stats kind.
+ *
+ * Copyright (c) 2001-2026, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * src/backend/utils/activity/pgstat_vacuum.c
+ * -------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "pgstat.h"
+#include "utils/memutils.h"
+#include "utils/pgstat_internal.h"
+
+#define ACCUMULATE_SUBFIELD(substruct, field) (dst->substruct.field += src->substruct.field)
+
+/*
+ * Accumulate the per-table extended vacuum counters collected so far.
+ *
+ * Only the counters derived directly from the vacuum's own bookkeeping are
+ * summed here. The buffer, WAL and timing counters (and the per-index
+ * counters) are accumulated by additional code added together with the
+ * helpers that gather them.
+ */
+static void
+pgstat_accumulate_extvac_stats_relations(PgStat_VacuumRelationCounts *dst,
+ PgStat_VacuumRelationCounts *src)
+{
+ if (!pgstat_track_vacuum_statistics)
+ return;
+
+ if (dst->type == PGSTAT_EXTVAC_INVALID)
+ dst->type = src->type;
+
+ Assert(src->type != PGSTAT_EXTVAC_INVALID &&
+ src->type != PGSTAT_EXTVAC_DB &&
+ src->type == dst->type);
+
+ ACCUMULATE_SUBFIELD(common, tuples_deleted);
+
+ if (dst->type == PGSTAT_EXTVAC_TABLE)
+ {
+ ACCUMULATE_SUBFIELD(table, pages_scanned);
+ ACCUMULATE_SUBFIELD(table, pages_removed);
+ ACCUMULATE_SUBFIELD(table, tuples_frozen);
+ }
+ else if (dst->type == PGSTAT_EXTVAC_INDEX)
+ {
+ ACCUMULATE_SUBFIELD(index, pages_deleted);
+ }
+}
+
+/*
+ * Report that the relation was just vacuumed, accumulating its extended
+ * statistics into the per-relation entry.
+ */
+void
+pgstat_report_vacuum_extstats(Oid tableoid, bool shared,
+ PgStat_VacuumRelationCounts *params)
+{
+ PgStat_EntryRef *entry_ref;
+ PgStat_RelationVacuumPending *relpending;
+ Oid dboid = (shared ? InvalidOid : MyDatabaseId);
+
+ if (!pgstat_track_vacuum_statistics)
+ return;
+
+ /*
+ * Accumulate into a pending entry instead of taking a shared-stats lock
+ * here; pgstat_report_stat() flushes it to shared memory through the
+ * registered flush callback once the vacuum's transaction completes.
+ */
+ entry_ref = pgstat_prep_pending_entry(PGSTAT_KIND_VACUUM_RELATION,
+ dboid, tableoid, NULL);
+ relpending = (PgStat_RelationVacuumPending *) entry_ref->pending;
+ pgstat_accumulate_extvac_stats_relations(&relpending->counts, params);
+}
+
+/*
+ * Flush out pending per-relation extended vacuum stats for the entry.
+ *
+ * If nowait is true, this function returns false if the lock could not be
+ * acquired immediately, otherwise true is returned.
+ */
+bool
+pgstat_vacuum_relation_flush_cb(PgStat_EntryRef *entry_ref, bool nowait)
+{
+ PgStatShared_VacuumRelation *shtabstats;
+ PgStat_RelationVacuumPending *pendingent;
+
+ pendingent = (PgStat_RelationVacuumPending *) entry_ref->pending;
+ shtabstats = (PgStatShared_VacuumRelation *) entry_ref->shared_stats;
+
+ /* Ignore entries that didn't accumulate any actual counts. */
+ if (pg_memory_is_all_zeros(pendingent,
+ sizeof(PgStat_RelationVacuumPending)))
+ return true;
+
+ if (!pgstat_lock_entry(entry_ref, nowait))
+ return false;
+
+ pgstat_accumulate_extvac_stats_relations(&shtabstats->stats,
+ &pendingent->counts);
+
+ pgstat_unlock_entry(entry_ref);
+
+ return true;
+}
+
+/*
+ * Support function for the SQL-callable pgstat* functions. Returns the vacuum
+ * collected statistics for one relation or NULL.
+ */
+PgStat_VacuumRelationCounts *
+pgstat_fetch_stat_vacuum_tabentry(Oid relid, Oid dbid)
+{
+ return (PgStat_VacuumRelationCounts *)
+ pgstat_fetch_entry(PGSTAT_KIND_VACUUM_RELATION, dbid, relid, NULL);
+}
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 6409987d66..e72f76180e 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -2367,3 +2367,86 @@ pg_stat_have_stats(PG_FUNCTION_ARGS)
PG_RETURN_BOOL(pgstat_have_entry(kind, dboid, objid));
}
+
+/*
+ * Get the extended vacuum statistics for a heap table.
+ */
+Datum
+pg_stat_get_vacuum_tables(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_GET_VACUUM_TABLES_STATS_COLS 5
+
+ Oid relid = PG_GETARG_OID(0);
+ PgStat_VacuumRelationCounts *extvacuum;
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_GET_VACUUM_TABLES_STATS_COLS] = {0};
+ bool nulls[PG_STAT_GET_VACUUM_TABLES_STATS_COLS] = {0};
+ int i = 0;
+
+ /* Build a tuple descriptor for our result type */
+ if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
+ elog(ERROR, "return type must be a row type");
+
+ extvacuum = pgstat_fetch_stat_vacuum_tabentry(relid, MyDatabaseId);
+ if (!extvacuum)
+ {
+ /* Retry as a shared relation before giving up. */
+ extvacuum = pgstat_fetch_stat_vacuum_tabentry(relid, InvalidOid);
+ if (!extvacuum)
+ {
+ InitMaterializedSRF(fcinfo, 0);
+ PG_RETURN_VOID();
+ }
+ }
+
+ values[i++] = ObjectIdGetDatum(relid);
+ values[i++] = Int64GetDatum(extvacuum->table.pages_scanned);
+ values[i++] = Int64GetDatum(extvacuum->table.pages_removed);
+ values[i++] = Int64GetDatum(extvacuum->common.tuples_deleted);
+ values[i++] = Int64GetDatum(extvacuum->table.tuples_frozen);
+
+ Assert(i == PG_STAT_GET_VACUUM_TABLES_STATS_COLS);
+
+ /* Returns the record as Datum */
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
+/*
+ * Get the extended vacuum statistics for an index.
+ */
+Datum
+pg_stat_get_vacuum_indexes(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_GET_VACUUM_INDEX_STATS_COLS 3
+
+ Oid relid = PG_GETARG_OID(0);
+ PgStat_VacuumRelationCounts *extvacuum;
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_GET_VACUUM_INDEX_STATS_COLS] = {0};
+ bool nulls[PG_STAT_GET_VACUUM_INDEX_STATS_COLS] = {0};
+ int i = 0;
+
+ if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
+ elog(ERROR, "return type must be a row type");
+
+ extvacuum = pgstat_fetch_stat_vacuum_tabentry(relid, MyDatabaseId);
+ if (!extvacuum)
+ {
+ extvacuum = pgstat_fetch_stat_vacuum_tabentry(relid, InvalidOid);
+ if (!extvacuum)
+ {
+ InitMaterializedSRF(fcinfo, 0);
+ PG_RETURN_VOID();
+ }
+ }
+
+ values[i++] = ObjectIdGetDatum(relid);
+
+ values[i++] = Int64GetDatum(extvacuum->index.pages_deleted);
+ values[i++] = Int64GetDatum(extvacuum->common.tuples_deleted);
+
+ Assert(i == PG_STAT_GET_VACUUM_INDEX_STATS_COLS);
+
+ /* Returns the record as Datum */
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
diff --git a/src/backend/utils/misc/guc_parameters.dat b/src/backend/utils/misc/guc_parameters.dat
index afaa058b04..bb8eb41394 100644
--- a/src/backend/utils/misc/guc_parameters.dat
+++ b/src/backend/utils/misc/guc_parameters.dat
@@ -3220,6 +3220,12 @@
boot_val => 'false',
},
+{ name => 'track_vacuum_statistics', type => 'bool', context => 'PGC_SUSET', group => 'STATS_CUMULATIVE',
+ short_desc => 'Collects vacuum statistics for vacuum activity.',
+ variable => 'pgstat_track_vacuum_statistics',
+ boot_val => 'false',
+},
+
{ name => 'track_wal_io_timing', type => 'bool', context => 'PGC_SUSET', group => 'STATS_CUMULATIVE',
short_desc => 'Collects timing statistics for WAL I/O activity.',
variable => 'track_wal_io_timing',
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index ac38cddaaf..4cf28f0f2e 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -702,6 +702,7 @@
#track_wal_io_timing = off
#track_functions = none # none, pl, all
#stats_fetch_consistency = cache # cache, none, snapshot
+#track_vacuum_statistics = off
# - Monitoring -
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 291b039859..9616a01466 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12638,6 +12638,16 @@
proargnames => '{pid,io_id,io_generation,state,operation,off,length,target,handle_data_len,raw_result,result,target_desc,f_sync,f_localmem,f_buffered}',
prosrc => 'pg_get_aios', proacl => '{POSTGRES=X,pg_read_all_stats=X}' },
+{ oid => '8001',
+ descr => 'pg_stat_get_vacuum_tables returns vacuum stats values for table',
+ proname => 'pg_stat_get_vacuum_tables', prorows => 1000, provolatile => 's', prorettype => 'record', proisstrict => 'f',
+ proretset => 't',
+ proargtypes => 'oid',
+ proallargtypes => '{oid,oid,int8,int8,int8,int8}',
+ proargmodes => '{i,o,o,o,o,o}',
+ proargnames => '{reloid,relid,pages_scanned,pages_removed,tuples_deleted,tuples_frozen}',
+ prosrc => 'pg_stat_get_vacuum_tables' }
+
# oid8 related functions
{ oid => '6436', descr => 'convert oid to oid8',
proname => 'oid8', prorettype => 'oid8', proargtypes => 'oid',
@@ -12703,4 +12713,13 @@
proname => 'pg_stat_get_frozen_page_marks_cleared', provolatile => 's',
proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
prosrc => 'pg_stat_get_frozen_page_marks_cleared' },
+{ oid => '8004',
+ descr => 'pg_stat_get_vacuum_indexes returns vacuum stats values for index',
+ proname => 'pg_stat_get_vacuum_indexes', prorows => 1000, provolatile => 's', prorettype => 'record', proisstrict => 'f',
+ proretset => 't',
+ proargtypes => 'oid',
+ proallargtypes => '{oid,oid,int8,int8}',
+ proargmodes => '{i,o,o,o}',
+ proargnames => '{reloid,relid,pages_deleted,tuples_deleted}',
+ prosrc => 'pg_stat_get_vacuum_indexes' },
]
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 7db36cf8ad..568624de5b 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -118,6 +118,15 @@ typedef struct PgStat_BackendSubEntry
PgStat_Counter conflict_count[CONFLICT_NUM_TYPES];
} PgStat_BackendSubEntry;
+/* Type of ExtVacReport */
+typedef enum ExtVacReportType
+{
+ PGSTAT_EXTVAC_INVALID = 0,
+ PGSTAT_EXTVAC_TABLE = 1,
+ PGSTAT_EXTVAC_INDEX = 2,
+ PGSTAT_EXTVAC_DB = 3,
+} ExtVacReportType;
+
/* ----------
* PgStat_TableCounts The actual per-table counts kept by a backend
*
@@ -164,6 +173,63 @@ typedef struct PgStat_TableCounts
PgStat_Counter frozen_page_marks_cleared;
} PgStat_TableCounts;
+typedef struct PgStat_CommonCounts
+{
+ /* tuples */
+ int64 tuples_deleted;
+} PgStat_CommonCounts;
+
+/* ----------
+ *
+ * PgStat_VacuumRelationCounts
+ *
+ * Additional statistics of vacuum processing over a relation. Counters that
+ * require sampling buffer/WAL/timing usage, and the per-index counters, are
+ * added to the common and per-type members later, together with the code that
+ * gathers them.
+ * ----------
+ */
+typedef struct PgStat_VacuumRelationCounts
+{
+ PgStat_CommonCounts common;
+
+ ExtVacReportType type; /* heap, index, etc. */
+
+ /* ----------
+ *
+ * There are separate metrics of statistic for tables and indexes,
+ * which collect during vacuum.
+ * The union operator allows to combine these statistics
+ * so that each metric is assigned to a specific class of collected statistics.
+ * Such a combined structure was called per_type_stats.
+ * The name of the structure itself is not used anywhere,
+ * it exists only for understanding the code.
+ * ----------
+ */
+ union
+ {
+ struct
+ {
+ int64 tuples_frozen; /* tuples frozen up by vacuum */
+ int64 pages_scanned; /* heap pages examined (not skipped by
+ * VM) */
+ int64 pages_removed; /* heap pages removed by vacuum
+ * "truncation" */
+ } table;
+ struct
+ {
+ int64 pages_deleted; /* number of pages deleted by vacuum */
+ } index;
+ } /* per_type_stats */ ;
+} PgStat_VacuumRelationCounts;
+
+typedef struct PgStat_VacuumRelationStatus
+{
+ Oid id; /* table's OID */
+ bool shared; /* is it a shared catalog? */
+ PgStat_VacuumRelationCounts counts; /* event counts to be sent */
+} PgStat_VacuumRelationStatus;
+
/* ----------
* PgStat_TableStatus Per-table status within a backend
*
@@ -188,6 +254,12 @@ typedef struct PgStat_TableStatus
Relation relation; /* rel that is using this entry */
} PgStat_TableStatus;
+typedef struct PgStat_RelationVacuumPending
+{
+ Oid id; /* table's OID */
+ PgStat_VacuumRelationCounts counts; /* event counts to be sent */
+} PgStat_RelationVacuumPending;
+
/* ----------
* PgStat_TableXactStatus Per-table, per-subtransaction status
* ----------
@@ -838,6 +910,12 @@ extern int pgstat_get_transactional_drops(bool isCommit, struct xl_xact_stats_it
extern void pgstat_execute_transactional_drops(int ndrops, struct xl_xact_stats_item *items, bool is_redo);
+extern void pgstat_vacuum_relation_delete_pending_cb(Oid relid);
+extern void
+ pgstat_report_vacuum_extstats(Oid tableoid, bool shared,
+ PgStat_VacuumRelationCounts * params);
+extern PgStat_VacuumRelationCounts * pgstat_fetch_stat_vacuum_tabentry(Oid relid, Oid dbid);
+
/*
* Functions in pgstat_wal.c
*/
@@ -854,7 +932,7 @@ extern PgStat_WalStats *pgstat_fetch_stat_wal(void);
extern PGDLLIMPORT bool pgstat_track_counts;
extern PGDLLIMPORT int pgstat_track_functions;
extern PGDLLIMPORT int pgstat_fetch_consistency;
-
+extern PGDLLIMPORT bool pgstat_track_vacuum_statistics;
/*
* Variables in pgstat_bgwriter.c
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index fe463faaf6..46a127af2b 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -507,6 +507,12 @@ typedef struct PgStatShared_Relation
PgStat_StatTabEntry stats;
} PgStatShared_Relation;
+typedef struct PgStatShared_VacuumRelation
+{
+ PgStatShared_Common header;
+ PgStat_VacuumRelationCounts stats;
+} PgStatShared_VacuumRelation;
+
typedef struct PgStatShared_Function
{
PgStatShared_Common header;
@@ -689,6 +695,8 @@ extern void *pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid,
bool *may_free);
extern void pgstat_snapshot_fixed(PgStat_Kind kind);
+extern bool pgstat_vacuum_relation_flush_cb(PgStat_EntryRef *entry_ref, bool nowait);
+
/*
* Functions in pgstat_archiver.c
diff --git a/src/include/utils/pgstat_kind.h b/src/include/utils/pgstat_kind.h
index 2d78a02968..f92149066c 100644
--- a/src/include/utils/pgstat_kind.h
+++ b/src/include/utils/pgstat_kind.h
@@ -39,9 +39,10 @@
#define PGSTAT_KIND_LOCK 11
#define PGSTAT_KIND_SLRU 12
#define PGSTAT_KIND_WAL 13
+#define PGSTAT_KIND_VACUUM_RELATION 14
#define PGSTAT_KIND_BUILTIN_MIN PGSTAT_KIND_DATABASE
-#define PGSTAT_KIND_BUILTIN_MAX PGSTAT_KIND_WAL
+#define PGSTAT_KIND_BUILTIN_MAX PGSTAT_KIND_VACUUM_RELATION
#define PGSTAT_KIND_BUILTIN_SIZE (PGSTAT_KIND_BUILTIN_MAX + 1)
/* Custom stats kinds */
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 096e4f763f..8f779a92ff 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -2421,6 +2421,30 @@ pg_stat_user_tables| SELECT relid,
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_vacuum_indexes| SELECT c.oid AS relid,
+ i.oid AS indexrelid,
+ n.nspname AS schemaname,
+ c.relname,
+ i.relname AS indexrelname,
+ s.pages_deleted,
+ s.tuples_deleted
+ FROM (((pg_class c
+ JOIN pg_index x ON ((c.oid = x.indrelid)))
+ JOIN pg_class i ON ((i.oid = x.indexrelid)))
+ LEFT JOIN pg_namespace n ON ((n.oid = c.relnamespace))),
+ LATERAL pg_stat_get_vacuum_indexes(i.oid) s(relid, pages_deleted, tuples_deleted)
+ WHERE (c.relkind = ANY (ARRAY['r'::"char", 't'::"char", 'm'::"char"]));
+pg_stat_vacuum_tables| SELECT n.nspname AS schemaname,
+ c.relname,
+ s.relid,
+ s.pages_scanned,
+ s.pages_removed,
+ s.tuples_deleted,
+ s.tuples_frozen
+ FROM (pg_class c
+ JOIN pg_namespace n ON ((n.oid = c.relnamespace))),
+ LATERAL pg_stat_get_vacuum_tables(c.oid) s(relid, pages_scanned, pages_removed, tuples_deleted, tuples_frozen)
+ WHERE (c.relkind = ANY (ARRAY['r'::"char", 't'::"char", 'm'::"char"]));
pg_stat_wal| SELECT wal_records,
wal_fpi,
wal_bytes,
diff --git a/src/test/regress/expected/vacuum_stats.out b/src/test/regress/expected/vacuum_stats.out
new file mode 100644
index 0000000000..cafaa9bdd9
--- /dev/null
+++ b/src/test/regress/expected/vacuum_stats.out
@@ -0,0 +1,114 @@
+--
+-- Extended vacuum statistics views (pg_stat_vacuum_tables, _indexes, _database)
+--
+SET track_vacuum_statistics = on;
+CREATE TABLE vacstat_t (id int PRIMARY KEY, v text)
+ WITH (autovacuum_enabled = off);
+INSERT INTO vacstat_t SELECT g, repeat('x', 20) FROM generate_series(1, 1000) g;
+DELETE FROM vacstat_t WHERE id % 2 = 0;
+VACUUM vacstat_t;
+SELECT pg_stat_force_next_flush();
+ pg_stat_force_next_flush
+--------------------------
+
+(1 row)
+
+-- core heap-page and tuple metrics. This VACUUM runs without concurrent
+-- activity: the surviving tuples are too fresh to be frozen (tuples_frozen = 0)
+-- and the interleaved deletions leave no trailing empty pages to truncate
+-- (pages_removed = 0).
+SELECT pages_scanned > 0 AS pages_scanned,
+ pages_removed = 0 AS pages_removed,
+ tuples_deleted = 500 AS tuples_deleted,
+ tuples_frozen = 0 AS tuples_frozen
+ FROM pg_stat_vacuum_tables WHERE relname = 'vacstat_t';
+ pages_scanned | pages_removed | tuples_deleted | tuples_frozen
+---------------+---------------+----------------+---------------
+ t | t | t | t
+(1 row)
+
+-- pages_removed path: deleting every tuple lets VACUUM truncate the now-empty
+-- trailing heap pages, so pages_removed advances.
+CREATE TABLE vacstat_trunc (id int)
+ WITH (autovacuum_enabled = off);
+INSERT INTO vacstat_trunc SELECT generate_series(1, 10000);
+DELETE FROM vacstat_trunc;
+VACUUM vacstat_trunc;
+SELECT pg_stat_force_next_flush();
+ pg_stat_force_next_flush
+--------------------------
+
+(1 row)
+
+SELECT pages_removed > 0 AS pages_removed,
+ tuples_deleted = 10000 AS tuples_deleted
+ FROM pg_stat_vacuum_tables WHERE relname = 'vacstat_trunc';
+ pages_removed | tuples_deleted
+---------------+----------------
+ t | t
+(1 row)
+
+DROP TABLE vacstat_trunc;
+-- tuples_frozen path: an aggressive VACUUM (FREEZE) freezes all live tuples,
+-- so tuples_frozen advances.
+CREATE TABLE vacstat_freeze (x int)
+ WITH (autovacuum_enabled = off);
+INSERT INTO vacstat_freeze SELECT generate_series(1, 1000);
+VACUUM (FREEZE) vacstat_freeze;
+SELECT pg_stat_force_next_flush();
+ pg_stat_force_next_flush
+--------------------------
+
+(1 row)
+
+SELECT tuples_frozen > 0 AS tuples_frozen
+ FROM pg_stat_vacuum_tables WHERE relname = 'vacstat_freeze';
+ tuples_frozen
+---------------
+ t
+(1 row)
+
+DROP TABLE vacstat_freeze;
+-- per-index view: the primary key index is processed by the same VACUUM.
+-- No btree leaf empties out (interleaved deletions), so pages_deleted = 0,
+-- while every index entry for a removed heap tuple is deleted.
+SELECT indexrelname,
+ pages_deleted = 0 AS pages_deleted,
+ tuples_deleted = 500 AS tuples_deleted
+ FROM pg_stat_vacuum_indexes WHERE relname = 'vacstat_t' ORDER BY indexrelname;
+ indexrelname | pages_deleted | tuples_deleted
+----------------+---------------+----------------
+ vacstat_t_pkey | t | t
+(1 row)
+
+-- index page-deletion path: deleting a contiguous key range empties whole
+-- btree leaf pages, which VACUUM then deletes (pages_deleted > 0), and every
+-- removed index entry is counted (tuples_deleted).
+CREATE TABLE vacstat_idxdel (id int PRIMARY KEY, v text)
+ WITH (autovacuum_enabled = off);
+INSERT INTO vacstat_idxdel SELECT g, repeat('x', 20) FROM generate_series(1, 10000) g;
+VACUUM vacstat_idxdel;
+SELECT pg_stat_force_next_flush();
+ pg_stat_force_next_flush
+--------------------------
+
+(1 row)
+
+DELETE FROM vacstat_idxdel WHERE id <= 9000;
+VACUUM vacstat_idxdel;
+SELECT pg_stat_force_next_flush();
+ pg_stat_force_next_flush
+--------------------------
+
+(1 row)
+
+SELECT indexrelname,
+ pages_deleted > 0 AS pages_deleted,
+ tuples_deleted = 9000 AS tuples_deleted
+ FROM pg_stat_vacuum_indexes WHERE relname = 'vacstat_idxdel' ORDER BY indexrelname;
+ indexrelname | pages_deleted | tuples_deleted
+---------------------+---------------+----------------
+ vacstat_idxdel_pkey | t | t
+(1 row)
+
+DROP TABLE vacstat_idxdel;
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 8fa0a6c47f..3c73207a9f 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -91,6 +91,9 @@ test: select_parallel
test: write_parallel
test: vacuum_parallel
+# extended vacuum statistics views
+test: vacuum_stats
+
# Run this alone, because concurrent DROP TABLE would make non-superuser
# "ANALYZE;" fail with "relation with OID $n does not exist".
test: maintain_every
diff --git a/src/test/regress/sql/vacuum_stats.sql b/src/test/regress/sql/vacuum_stats.sql
new file mode 100644
index 0000000000..6061f433a3
--- /dev/null
+++ b/src/test/regress/sql/vacuum_stats.sql
@@ -0,0 +1,70 @@
+--
+-- Extended vacuum statistics views (pg_stat_vacuum_tables, _indexes, _database)
+--
+SET track_vacuum_statistics = on;
+
+CREATE TABLE vacstat_t (id int PRIMARY KEY, v text)
+ WITH (autovacuum_enabled = off);
+INSERT INTO vacstat_t SELECT g, repeat('x', 20) FROM generate_series(1, 1000) g;
+DELETE FROM vacstat_t WHERE id % 2 = 0;
+VACUUM vacstat_t;
+SELECT pg_stat_force_next_flush();
+
+-- core heap-page and tuple metrics. This VACUUM runs without concurrent
+-- activity: the surviving tuples are too fresh to be frozen (tuples_frozen = 0)
+-- and the interleaved deletions leave no trailing empty pages to truncate
+-- (pages_removed = 0).
+SELECT pages_scanned > 0 AS pages_scanned,
+ pages_removed = 0 AS pages_removed,
+ tuples_deleted = 500 AS tuples_deleted,
+ tuples_frozen = 0 AS tuples_frozen
+ FROM pg_stat_vacuum_tables WHERE relname = 'vacstat_t';
+
+-- pages_removed path: deleting every tuple lets VACUUM truncate the now-empty
+-- trailing heap pages, so pages_removed advances.
+CREATE TABLE vacstat_trunc (id int)
+ WITH (autovacuum_enabled = off);
+INSERT INTO vacstat_trunc SELECT generate_series(1, 10000);
+DELETE FROM vacstat_trunc;
+VACUUM vacstat_trunc;
+SELECT pg_stat_force_next_flush();
+SELECT pages_removed > 0 AS pages_removed,
+ tuples_deleted = 10000 AS tuples_deleted
+ FROM pg_stat_vacuum_tables WHERE relname = 'vacstat_trunc';
+DROP TABLE vacstat_trunc;
+
+-- tuples_frozen path: an aggressive VACUUM (FREEZE) freezes all live tuples,
+-- so tuples_frozen advances.
+CREATE TABLE vacstat_freeze (x int)
+ WITH (autovacuum_enabled = off);
+INSERT INTO vacstat_freeze SELECT generate_series(1, 1000);
+VACUUM (FREEZE) vacstat_freeze;
+SELECT pg_stat_force_next_flush();
+SELECT tuples_frozen > 0 AS tuples_frozen
+ FROM pg_stat_vacuum_tables WHERE relname = 'vacstat_freeze';
+DROP TABLE vacstat_freeze;
+
+-- per-index view: the primary key index is processed by the same VACUUM.
+-- No btree leaf empties out (interleaved deletions), so pages_deleted = 0,
+-- while every index entry for a removed heap tuple is deleted.
+SELECT indexrelname,
+ pages_deleted = 0 AS pages_deleted,
+ tuples_deleted = 500 AS tuples_deleted
+ FROM pg_stat_vacuum_indexes WHERE relname = 'vacstat_t' ORDER BY indexrelname;
+
+-- index page-deletion path: deleting a contiguous key range empties whole
+-- btree leaf pages, which VACUUM then deletes (pages_deleted > 0), and every
+-- removed index entry is counted (tuples_deleted).
+CREATE TABLE vacstat_idxdel (id int PRIMARY KEY, v text)
+ WITH (autovacuum_enabled = off);
+INSERT INTO vacstat_idxdel SELECT g, repeat('x', 20) FROM generate_series(1, 10000) g;
+VACUUM vacstat_idxdel;
+SELECT pg_stat_force_next_flush();
+DELETE FROM vacstat_idxdel WHERE id <= 9000;
+VACUUM vacstat_idxdel;
+SELECT pg_stat_force_next_flush();
+SELECT indexrelname,
+ pages_deleted > 0 AS pages_deleted,
+ tuples_deleted = 9000 AS tuples_deleted
+ FROM pg_stat_vacuum_indexes WHERE relname = 'vacstat_idxdel' ORDER BY indexrelname;
+DROP TABLE vacstat_idxdel;
--
2.39.5 (Apple Git-154)
[text/plain] 0003-Extended-vacuum-statistics-recently-dead-and-missed-.patch (15.6K, 5-0003-Extended-vacuum-statistics-recently-dead-and-missed-.patch)
download | inline diff:
From ffb8a24d9772ac5bc408962750280f9d3886990f Mon Sep 17 00:00:00 2001
From: Alena Rybakina <[email protected]>
Date: Tue, 16 Jun 2026 10:41:24 +0300
Subject: [PATCH 3/9] Extended vacuum statistics: recently-dead and missed dead
tuples for tables
Expose the counters for dead tuples that VACUUM could not remove, with
documentation and regression coverage:
recently_dead_tuples dead tuples still visible to some transaction and
therefore not yet removable
missed_dead_tuples dead tuples skipped because their heap page could not
be cleanup-locked
missed_dead_pages heap pages that contained such skipped dead tuples
These non-zero paths require concurrent activity that an ordinary regression
test cannot create deterministically, so they are covered by a dedicated TAP
test, src/test/modules/test_misc/t/014_vacuum_stats.pl: a held REPEATABLE READ
snapshot keeps recently deleted tuples visible (recently_dead_tuples), and a
concurrently pinned heap page prevents cleanup (missed_dead_tuples /
missed_dead_pages).
---
doc/src/sgml/system-views.sgml | 24 ++++++
src/backend/access/heap/vacuumlazy.c | 3 +
src/backend/catalog/system_views.sql | 5 +-
src/backend/utils/activity/pgstat_vacuum.c | 3 +
src/backend/utils/adt/pgstatfuncs.c | 5 +-
src/include/catalog/pg_proc.dat | 6 +-
src/include/pgstat.h | 7 ++
src/test/modules/test_misc/meson.build | 1 +
.../modules/test_misc/t/014_vacuum_stats.pl | 84 +++++++++++++++++++
src/test/regress/expected/rules.out | 7 +-
src/test/regress/expected/vacuum_stats.out | 15 ++++
src/test/regress/sql/vacuum_stats.sql | 11 +++
12 files changed, 164 insertions(+), 7 deletions(-)
create mode 100644 src/test/modules/test_misc/t/014_vacuum_stats.pl
diff --git a/doc/src/sgml/system-views.sgml b/doc/src/sgml/system-views.sgml
index 19576018df..b96c653929 100644
--- a/doc/src/sgml/system-views.sgml
+++ b/doc/src/sgml/system-views.sgml
@@ -5868,6 +5868,30 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx
Number of tuples frozen by the vacuum.
</para></entry>
</row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>recently_dead_tuples</structfield> <type>bigint</type>
+ </para>
+ <para>
+ Number of dead tuples that are still visible to some transaction and therefore could not yet be removed.
+ </para></entry>
+ </row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>missed_dead_pages</structfield> <type>bigint</type>
+ </para>
+ <para>
+ Number of heap pages containing dead tuples that the vacuum could not remove because it failed to acquire a cleanup lock.
+ </para></entry>
+ </row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>missed_dead_tuples</structfield> <type>bigint</type>
+ </para>
+ <para>
+ Number of dead tuples that the vacuum could not remove because it failed to acquire a cleanup lock on their page.
+ </para></entry>
+ </row>
</tbody>
</tgroup>
</table>
diff --git a/src/backend/access/heap/vacuumlazy.c b/src/backend/access/heap/vacuumlazy.c
index ffcfa3d16a..7abc83dbfd 100644
--- a/src/backend/access/heap/vacuumlazy.c
+++ b/src/backend/access/heap/vacuumlazy.c
@@ -630,6 +630,9 @@ accumulate_heap_vacuum_statistics(LVRelState *vacrel,
extVacStats->table.pages_removed = vacrel->removed_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_pages = vacrel->missed_dead_pages;
+ extVacStats->table.missed_dead_tuples = vacrel->missed_dead_tuples;
}
/*
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index 04ca38e51c..6f697ab390 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -1569,7 +1569,10 @@ CREATE VIEW pg_stat_vacuum_tables AS
S.pages_scanned AS pages_scanned,
S.pages_removed AS pages_removed,
S.tuples_deleted AS tuples_deleted,
- S.tuples_frozen AS tuples_frozen
+ S.tuples_frozen AS tuples_frozen,
+ S.recently_dead_tuples AS recently_dead_tuples,
+ S.missed_dead_pages AS missed_dead_pages,
+ S.missed_dead_tuples AS missed_dead_tuples
FROM pg_class C JOIN
pg_namespace N ON N.oid = C.relnamespace,
diff --git a/src/backend/utils/activity/pgstat_vacuum.c b/src/backend/utils/activity/pgstat_vacuum.c
index ac6baee516..644a732592 100644
--- a/src/backend/utils/activity/pgstat_vacuum.c
+++ b/src/backend/utils/activity/pgstat_vacuum.c
@@ -53,6 +53,9 @@ pgstat_accumulate_extvac_stats_relations(PgStat_VacuumRelationCounts *dst,
ACCUMULATE_SUBFIELD(table, pages_scanned);
ACCUMULATE_SUBFIELD(table, pages_removed);
ACCUMULATE_SUBFIELD(table, tuples_frozen);
+ ACCUMULATE_SUBFIELD(table, recently_dead_tuples);
+ ACCUMULATE_SUBFIELD(table, missed_dead_pages);
+ ACCUMULATE_SUBFIELD(table, missed_dead_tuples);
}
else if (dst->type == PGSTAT_EXTVAC_INDEX)
{
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index e72f76180e..742f4974d5 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -2374,7 +2374,7 @@ pg_stat_have_stats(PG_FUNCTION_ARGS)
Datum
pg_stat_get_vacuum_tables(PG_FUNCTION_ARGS)
{
-#define PG_STAT_GET_VACUUM_TABLES_STATS_COLS 5
+#define PG_STAT_GET_VACUUM_TABLES_STATS_COLS 8
Oid relid = PG_GETARG_OID(0);
PgStat_VacuumRelationCounts *extvacuum;
@@ -2404,6 +2404,9 @@ pg_stat_get_vacuum_tables(PG_FUNCTION_ARGS)
values[i++] = Int64GetDatum(extvacuum->table.pages_removed);
values[i++] = Int64GetDatum(extvacuum->common.tuples_deleted);
values[i++] = Int64GetDatum(extvacuum->table.tuples_frozen);
+ values[i++] = Int64GetDatum(extvacuum->table.recently_dead_tuples);
+ values[i++] = Int64GetDatum(extvacuum->table.missed_dead_pages);
+ values[i++] = Int64GetDatum(extvacuum->table.missed_dead_tuples);
Assert(i == PG_STAT_GET_VACUUM_TABLES_STATS_COLS);
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 9616a01466..6d683413a4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12643,9 +12643,9 @@
proname => 'pg_stat_get_vacuum_tables', prorows => 1000, provolatile => 's', prorettype => 'record', proisstrict => 'f',
proretset => 't',
proargtypes => 'oid',
- proallargtypes => '{oid,oid,int8,int8,int8,int8}',
- proargmodes => '{i,o,o,o,o,o}',
- proargnames => '{reloid,relid,pages_scanned,pages_removed,tuples_deleted,tuples_frozen}',
+ proallargtypes => '{oid,oid,int8,int8,int8,int8,int8,int8,int8}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{reloid,relid,pages_scanned,pages_removed,tuples_deleted,tuples_frozen,recently_dead_tuples,missed_dead_pages,missed_dead_tuples}',
prosrc => 'pg_stat_get_vacuum_tables' }
# oid8 related functions
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 568624de5b..bdcf758441 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -211,10 +211,17 @@ typedef struct PgStat_VacuumRelationCounts
struct
{
int64 tuples_frozen; /* tuples frozen up by vacuum */
+ int64 recently_dead_tuples; /* deleted tuples that are
+ * still visible to some
+ * transaction */
int64 pages_scanned; /* heap pages examined (not skipped by
* VM) */
int64 pages_removed; /* heap pages removed by vacuum
* "truncation" */
+ int64 missed_dead_pages; /* pages with missed dead tuples */
+ int64 missed_dead_tuples; /* tuples not pruned by vacuum due
+ * to failure to get a cleanup
+ * lock */
} table;
struct
{
diff --git a/src/test/modules/test_misc/meson.build b/src/test/modules/test_misc/meson.build
index 969e90b396..267145163a 100644
--- a/src/test/modules/test_misc/meson.build
+++ b/src/test/modules/test_misc/meson.build
@@ -22,6 +22,7 @@ tests += {
't/011_lock_stats.pl',
't/012_ddlutils.pl',
't/013_temp_obj_multisession.pl',
+ 't/014_vacuum_stats.pl',
],
# The injection points are cluster-wide, so disable installcheck
'runningcheck': false,
diff --git a/src/test/modules/test_misc/t/014_vacuum_stats.pl b/src/test/modules/test_misc/t/014_vacuum_stats.pl
new file mode 100644
index 0000000000..9b64b0996c
--- /dev/null
+++ b/src/test/modules/test_misc/t/014_vacuum_stats.pl
@@ -0,0 +1,84 @@
+# Copyright (c) 2024-2026, PostgreSQL Global Development Group
+
+# Test the recently_dead_tuples and missed_dead_tuples/missed_dead_pages
+# counters of the extended vacuum statistics view pg_stat_vacuum_tables.
+#
+# These counters depend on inter-session visibility and on VACUUM's ability to
+# acquire a cleanup lock, so they cannot be exercised by an ordinary
+# single-session regression test. A dedicated TAP cluster gives us full
+# control over the removal horizon (no concurrent backends hold it back), which
+# makes the outcome deterministic -- unlike an isolation test running against a
+# shared regression cluster.
+
+use strict;
+use warnings FATAL => 'all';
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+my $node = PostgreSQL::Test::Cluster->new('main');
+$node->init;
+$node->append_conf(
+ 'postgresql.conf', qq[
+autovacuum = off
+track_vacuum_statistics = on
+]);
+$node->start;
+
+# A small table that fits on a single heap page, so the deleted tuples and the
+# page pinned by the cursor below are the same page.
+$node->safe_psql(
+ 'postgres', qq[
+CREATE TABLE vacstat_iso (id int, ival int) WITH (autovacuum_enabled = off);
+INSERT INTO vacstat_iso SELECT i, i FROM generate_series(1, 50) i;
+]);
+
+# Helper: fetch the four interesting counters for the table.
+my $stats_query = qq[
+SELECT tuples_deleted, recently_dead_tuples, missed_dead_tuples, missed_dead_pages
+ FROM pg_stat_vacuum_tables WHERE relname = 'vacstat_iso'];
+
+# This session first holds an old snapshot (so the deleted tuples stay
+# recently dead), and later pins the heap page (so VACUUM cannot get a cleanup
+# lock and the now-removable tuples are missed instead).
+my $holder = $node->background_psql('postgres', on_error_stop => 1);
+
+# 1. Hold a repeatable-read snapshot that can still see the soon-to-be-deleted
+# tuples, preventing VACUUM from removing them.
+$holder->query_safe(
+ 'BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ;'
+ . ' SELECT count(*) FROM vacstat_iso;');
+
+# 2. Delete ten tuples and vacuum. They are dead but not yet removable, so
+# they are counted as recently dead and not removed.
+$node->safe_psql('postgres', 'DELETE FROM vacstat_iso WHERE id <= 10;');
+$node->safe_psql('postgres', 'VACUUM vacstat_iso;');
+is( $node->safe_psql('postgres', $stats_query),
+ "0|10|0|0",
+ 'recently_dead_tuples counted while an old snapshot is held');
+
+# 3. Release the old snapshot, then pin the table's single heap page with a
+# cursor so VACUUM cannot acquire a cleanup lock on it.
+$holder->query_safe('COMMIT;');
+$holder->query_safe(
+ 'BEGIN; DECLARE c CURSOR FOR SELECT * FROM vacstat_iso;'
+ . ' FETCH NEXT FROM c;');
+
+# 4. The deleted tuples are now removable, but the page is pinned, so a plain
+# VACUUM skips it without a cleanup lock and counts the tuples as missed.
+# (Counters accumulate, so recently_dead_tuples stays at 10.)
+$node->safe_psql('postgres', 'VACUUM vacstat_iso;');
+is( $node->safe_psql('postgres', $stats_query),
+ "0|10|10|1",
+ 'missed_dead_tuples/missed_dead_pages counted while the page is pinned');
+
+# 5. Release the pin and vacuum once more; the tuples are finally removed.
+$holder->query_safe('COMMIT;');
+$node->safe_psql('postgres', 'VACUUM vacstat_iso;');
+is( $node->safe_psql('postgres', $stats_query),
+ "10|10|10|1",
+ 'dead tuples removed once the pin is released');
+
+$holder->quit;
+$node->stop;
+done_testing();
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 8f779a92ff..a2b0472a2d 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -2440,10 +2440,13 @@ pg_stat_vacuum_tables| SELECT n.nspname AS schemaname,
s.pages_scanned,
s.pages_removed,
s.tuples_deleted,
- s.tuples_frozen
+ s.tuples_frozen,
+ s.recently_dead_tuples,
+ s.missed_dead_pages,
+ s.missed_dead_tuples
FROM (pg_class c
JOIN pg_namespace n ON ((n.oid = c.relnamespace))),
- LATERAL pg_stat_get_vacuum_tables(c.oid) s(relid, pages_scanned, pages_removed, tuples_deleted, tuples_frozen)
+ LATERAL pg_stat_get_vacuum_tables(c.oid) s(relid, pages_scanned, pages_removed, tuples_deleted, tuples_frozen, recently_dead_tuples, missed_dead_pages, missed_dead_tuples)
WHERE (c.relkind = ANY (ARRAY['r'::"char", 't'::"char", 'm'::"char"]));
pg_stat_wal| SELECT wal_records,
wal_fpi,
diff --git a/src/test/regress/expected/vacuum_stats.out b/src/test/regress/expected/vacuum_stats.out
index cafaa9bdd9..cb11d6381c 100644
--- a/src/test/regress/expected/vacuum_stats.out
+++ b/src/test/regress/expected/vacuum_stats.out
@@ -69,6 +69,21 @@ SELECT tuples_frozen > 0 AS tuples_frozen
(1 row)
DROP TABLE vacstat_freeze;
+-- dead tuples that survived this vacuum: recently_dead_tuples are still visible
+-- to some transaction, while missed_dead_pages/missed_dead_tuples could not be
+-- removed because the page was pinned by another backend (cleanup lock not
+-- acquired). None occur here, since this VACUUM runs without concurrent
+-- activity (all = 0). The non-zero paths are covered by the
+-- vacuum-extending-in-repetable-read isolation test.
+SELECT recently_dead_tuples = 0 AS recently_dead_tuples,
+ missed_dead_pages = 0 AS missed_dead_pages,
+ missed_dead_tuples = 0 AS missed_dead_tuples
+ FROM pg_stat_vacuum_tables WHERE relname = 'vacstat_t';
+ recently_dead_tuples | missed_dead_pages | missed_dead_tuples
+----------------------+-------------------+--------------------
+ t | t | t
+(1 row)
+
-- per-index view: the primary key index is processed by the same VACUUM.
-- No btree leaf empties out (interleaved deletions), so pages_deleted = 0,
-- while every index entry for a removed heap tuple is deleted.
diff --git a/src/test/regress/sql/vacuum_stats.sql b/src/test/regress/sql/vacuum_stats.sql
index 6061f433a3..75a5a0052b 100644
--- a/src/test/regress/sql/vacuum_stats.sql
+++ b/src/test/regress/sql/vacuum_stats.sql
@@ -44,6 +44,17 @@ SELECT tuples_frozen > 0 AS tuples_frozen
FROM pg_stat_vacuum_tables WHERE relname = 'vacstat_freeze';
DROP TABLE vacstat_freeze;
+-- dead tuples that survived this vacuum: recently_dead_tuples are still visible
+-- to some transaction, while missed_dead_pages/missed_dead_tuples could not be
+-- removed because the page was pinned by another backend (cleanup lock not
+-- acquired). None occur here, since this VACUUM runs without concurrent
+-- activity (all = 0). The non-zero paths are covered by the
+-- vacuum-extending-in-repetable-read isolation test.
+SELECT recently_dead_tuples = 0 AS recently_dead_tuples,
+ missed_dead_pages = 0 AS missed_dead_pages,
+ missed_dead_tuples = 0 AS missed_dead_tuples
+ FROM pg_stat_vacuum_tables WHERE relname = 'vacstat_t';
+
-- per-index view: the primary key index is processed by the same VACUUM.
-- No btree leaf empties out (interleaved deletions), so pages_deleted = 0,
-- while every index entry for a removed heap tuple is deleted.
--
2.39.5 (Apple Git-154)
[text/plain] 0004-Extended-vacuum-statistics-WAL-metrics-and-the-sampl.patch (72.5K, 6-0004-Extended-vacuum-statistics-WAL-metrics-and-the-sampl.patch)
download | inline diff:
From ad0a8bad2cc31840b9eba83216b3be49dc3b0b93 Mon Sep 17 00:00:00 2001
From: Alena Rybakina <[email protected]>
Date: Wed, 17 Jun 2026 21:21:44 +0300
Subject: [PATCH 4/9] Extended vacuum statistics: WAL metrics and the sampling
machinery
Introduce the resource-usage sampling machinery for extended vacuum
statistics together with the first sampled metric, the WAL counters.
Around the processing of each relation, vacuum snapshots WAL usage and
records the difference, so the metric reflects only that vacuum's work.
The buffer and timing metrics are sampled through the same machinery and
are added in the following commits. Index processing is sampled on its
own -- including in parallel workers -- and its usage is kept out of the
parent table's figures, so heap and index work are accounted separately.
The PGSTAT_KIND_VACUUM_DB stats kind and pgstat_accumulate_common() are
added here as well: they aggregate the common per-relation counters of
every relation vacuumed in the database, which is only meaningful once the
sampling exists. The two remaining views, pg_stat_vacuum_indexes and
pg_stat_vacuum_database, are created here alongside pg_stat_vacuum_tables;
like the table view they then grow column by column in the following
commits. The per-index view also exposes the index-specific pages_deleted
and tuples_deleted counters from the start.
This commit exposes the WAL counters in all three views, with documentation
and regression coverage:
wal_records number of WAL records generated by the vacuum
wal_fpi number of WAL full page images generated by the vacuum
wal_bytes total amount of WAL generated by the vacuum, in bytes
The separate sampling of index processing, including through the parallel
vacuum path, is covered by a dedicated TAP test
(src/test/modules/test_misc/t/016_vacuum_stats_parallel.pl).
---
doc/src/sgml/system-views.sgml | 130 ++++++
src/backend/access/heap/vacuumlazy.c | 391 ++++++++++++++----
src/backend/catalog/system_views.sql | 25 +-
src/backend/commands/dbcommands.c | 1 +
src/backend/commands/vacuum.c | 4 +
src/backend/commands/vacuumparallel.c | 78 +++-
src/backend/utils/activity/pgstat.c | 15 +
src/backend/utils/activity/pgstat_database.c | 9 +
src/backend/utils/activity/pgstat_vacuum.c | 105 ++++-
src/backend/utils/adt/pgstatfuncs.c | 63 ++-
src/include/catalog/pg_proc.dat | 21 +-
src/include/commands/vacuum.h | 32 +-
src/include/pgstat.h | 15 +
src/include/utils/pgstat_internal.h | 7 +
src/include/utils/pgstat_kind.h | 3 +-
src/test/modules/test_misc/meson.build | 1 +
.../test_misc/t/016_vacuum_stats_parallel.pl | 104 +++++
src/test/regress/expected/rules.out | 22 +-
src/test/regress/expected/vacuum_stats.out | 89 +++-
src/test/regress/sql/vacuum_stats.sql | 56 ++-
20 files changed, 1045 insertions(+), 126 deletions(-)
create mode 100644 src/test/modules/test_misc/t/016_vacuum_stats_parallel.pl
diff --git a/doc/src/sgml/system-views.sgml b/doc/src/sgml/system-views.sgml
index b96c653929..7c95dcea6b 100644
--- a/doc/src/sgml/system-views.sgml
+++ b/doc/src/sgml/system-views.sgml
@@ -5892,6 +5892,30 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx
Number of dead tuples that the vacuum could not remove because it failed to acquire a cleanup lock on their page.
</para></entry>
</row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>wal_records</structfield> <type>bigint</type>
+ </para>
+ <para>
+ Total number of WAL records generated by the vacuum.
+ </para></entry>
+ </row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>wal_fpi</structfield> <type>bigint</type>
+ </para>
+ <para>
+ Total number of WAL full page images generated by the vacuum.
+ </para></entry>
+ </row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>wal_bytes</structfield> <type>numeric</type>
+ </para>
+ <para>
+ Total amount of WAL generated by the vacuum, in bytes.
+ </para></entry>
+ </row>
</tbody>
</tgroup>
</table>
@@ -5983,6 +6007,112 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx
Number of index entries removed by the vacuum.
</para></entry>
</row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>wal_records</structfield> <type>bigint</type>
+ </para>
+ <para>
+ Number of WAL records generated while vacuuming this index.
+ </para></entry>
+ </row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>wal_fpi</structfield> <type>bigint</type>
+ </para>
+ <para>
+ Number of WAL full page images generated while vacuuming this index.
+ </para></entry>
+ </row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>wal_bytes</structfield> <type>numeric</type>
+ </para>
+ <para>
+ Total amount of WAL generated while vacuuming this index, in bytes.
+ </para></entry>
+ </row>
+ </tbody>
+ </tgroup>
+ </table>
+ </sect1>
+
+ <sect1 id="view-pg-stat-vacuum-database">
+ <title><structname>pg_stat_vacuum_database</structname></title>
+
+ <indexterm zone="view-pg-stat-vacuum-database">
+ <primary>pg_stat_vacuum_database</primary>
+ </indexterm>
+
+ <para>
+ The <structname>pg_stat_vacuum_database</structname> view will contain one row
+ for each database, showing extended statistics aggregated over all
+ <command>VACUUM</command> activity in that database. These statistics are
+ accumulated only while <varname>track_vacuum_statistics</varname> is enabled.
+ </para>
+
+ <table id="view-pg-stat-vacuum-database-cols">
+ <title><structname>pg_stat_vacuum_database</structname> Columns</title>
+ <tgroup cols="1">
+ <thead>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ Column Type
+ </para>
+ <para>
+ Description
+ </para></entry>
+ </row>
+ </thead>
+
+ <tbody>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>dboid</structfield> <type>oid</type>
+ </para>
+ <para>
+ OID of the database.
+ </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.
+ </para></entry>
+ </row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>errors</structfield> <type>integer</type>
+ </para>
+ <para>
+ Number of vacuum operations in this database that failed with an error.
+ </para></entry>
+ </row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>wal_records</structfield> <type>bigint</type>
+ </para>
+ <para>
+ Number of WAL records generated by vacuum operations in this database.
+ </para></entry>
+ </row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>wal_fpi</structfield> <type>bigint</type>
+ </para>
+ <para>
+ Number of WAL full page images generated by vacuum operations in this database.
+ </para></entry>
+ </row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>wal_bytes</structfield> <type>numeric</type>
+ </para>
+ <para>
+ Total amount of WAL generated by vacuum operations in this database, in bytes.
+ </para></entry>
+ </row>
</tbody>
</tgroup>
</table>
diff --git a/src/backend/access/heap/vacuumlazy.c b/src/backend/access/heap/vacuumlazy.c
index 7abc83dbfd..c06c538f57 100644
--- a/src/backend/access/heap/vacuumlazy.c
+++ b/src/backend/access/heap/vacuumlazy.c
@@ -411,6 +411,25 @@ typedef struct LVRelState
* been permanently disabled.
*/
BlockNumber eager_scan_remaining_fails;
+
+ int32 wraparound_failsafe_count; /* number of emergency vacuums to
+ * prevent anti-wraparound
+ * shutdown */
+
+ PgStat_VacuumRelationCounts extVacReportIdx;
+
+ /*
+ * Per-index accumulated extended vacuum statistics. VACUUM may process an
+ * index several times (a bulkdelete pass per index scan plus a final
+ * cleanup pass); we sum those passes here and report them to the
+ * cumulative stats system exactly once per index at the end of the vacuum.
+ * Only used by the non-parallel path -- parallel index vacuuming keeps the
+ * equivalent running totals in the DSM and reports from
+ * parallel_vacuum_end(). Sized vacrel->nindexes; NULL if there are no
+ * indexes.
+ */
+ PgStat_VacuumRelationCounts *extVacIdxReports;
+ bool *extVacIdxTouched; /* was the matching index processed? */
} LVRelState;
@@ -422,7 +441,6 @@ typedef struct LVSavedErrInfo
VacErrPhase phase;
} LVSavedErrInfo;
-
/* non-export function prototypes */
static void lazy_scan_heap(LVRelState *vacrel);
static void heap_vacuum_eager_scan_setup(LVRelState *vacrel,
@@ -452,12 +470,12 @@ static void lazy_cleanup_all_indexes(LVRelState *vacrel);
static IndexBulkDeleteResult *lazy_vacuum_one_index(Relation indrel,
IndexBulkDeleteResult *istat,
double reltuples,
- LVRelState *vacrel);
+ LVRelState *vacrel, int idx);
static IndexBulkDeleteResult *lazy_cleanup_one_index(Relation indrel,
IndexBulkDeleteResult *istat,
double reltuples,
bool estimated_count,
- LVRelState *vacrel);
+ LVRelState *vacrel, int idx);
static bool should_attempt_truncation(LVRelState *vacrel);
static void lazy_truncate_heap(LVRelState *vacrel);
static BlockNumber count_nondeletable_pages(LVRelState *vacrel,
@@ -484,9 +502,195 @@ static void update_vacuum_error_info(LVRelState *vacrel,
OffsetNumber offnum);
static void restore_vacuum_error_info(LVRelState *vacrel,
const LVSavedErrInfo *saved_vacrel);
-static void accumulate_heap_vacuum_statistics(LVRelState *vacrel,
- PgStat_VacuumRelationCounts *extVacStats);
+/* ----------
+ * extvac_stats_start() -
+ *
+ * Save cut-off values of extended vacuum counters before start of a relation
+ * processing.
+ * ----------
+ */
+static void
+extvac_stats_start(Relation rel, LVExtStatCounters * counters)
+{
+ if (!pgstat_track_vacuum_statistics)
+ return;
+
+ memset(counters, 0, sizeof(LVExtStatCounters));
+
+ counters->walusage = pgWalUsage;
+}
+
+/* ----------
+ * extvac_stats_end() -
+ *
+ * Called to finish an extended vacuum statistic gathering and form a report.
+ * ----------
+ */
+static void
+extvac_stats_end(Relation rel, LVExtStatCounters * counters,
+ PgStat_CommonCounts * report)
+{
+ WalUsage walusage;
+
+ if (!pgstat_track_vacuum_statistics)
+ return;
+
+ memset(report, 0, sizeof(PgStat_CommonCounts));
+
+ /* Calculate diffs of global stat parameters on WAL usage. */
+ memset(&walusage, 0, sizeof(WalUsage));
+ WalUsageAccumDiff(&walusage, &pgWalUsage, &counters->walusage);
+
+ /*
+ * Fill additional statistics on a vacuum processing operation.
+ */
+ report->wal_records += walusage.wal_records;
+ report->wal_fpi += walusage.wal_fpi;
+ report->wal_bytes += walusage.wal_bytes;
+}
+
+void
+extvac_stats_start_idx(Relation rel, IndexBulkDeleteResult *stats,
+ LVExtStatCountersIdx * counters)
+{
+ if (!pgstat_track_vacuum_statistics)
+ return;
+
+ /* Set initial values for common heap and index statistics */
+ extvac_stats_start(rel, &counters->common);
+ counters->pages_deleted = counters->tuples_removed = 0;
+
+ if (stats != NULL)
+ {
+ /*
+ * XXX: Why do we need this code here? If it is needed, I feel lack of
+ * comments, describing the reason.
+ */
+ counters->tuples_removed = stats->tuples_removed;
+ counters->pages_deleted = stats->pages_deleted;
+ }
+}
+
+void
+extvac_stats_end_idx(Relation rel, IndexBulkDeleteResult *stats,
+ LVExtStatCountersIdx * counters, PgStat_VacuumRelationCounts * report)
+{
+ if (!pgstat_track_vacuum_statistics)
+ return;
+
+ memset(report, 0, sizeof(PgStat_VacuumRelationCounts));
+
+ extvac_stats_end(rel, &counters->common, &report->common);
+
+ report->type = PGSTAT_EXTVAC_INDEX;
+
+ if (stats != NULL)
+ {
+ /*
+ * if something goes wrong or an user doesn't want to track a database
+ * activity - just suppress it.
+ */
+
+ /* Fill index-specific extended stats fields */
+ report->common.tuples_deleted =
+ stats->tuples_removed - counters->tuples_removed;
+ report->index.pages_deleted =
+ stats->pages_deleted - counters->pages_deleted;
+ }
+}
+
+/* Accumulate vacuum statistics for heap.
+ *
+ * Because of complexity of vacuum processing: it switch procesing between
+ * the heap relation to index relations and visa versa, we need to store
+ * gathered statistics information for heap relations several times before
+ * the vacuum starts processing the indexes again.
+ *
+ * It is necessary to gather correct statistics information for heap and indexes
+ * otherwice the index statistics information would be added to his parent heap
+ * statistics information and it would be difficult to analyze it later.
+ *
+ * We can't subtract union vacuum statistics information for index from the heap relations
+ * because of total and delay time time statistics collecting during parallel vacuum
+ * procudure.
+*/
+static void
+accumulate_heap_vacuum_statistics(LVRelState *vacrel, PgStat_VacuumRelationCounts * extVacStats)
+{
+ if (!pgstat_track_vacuum_statistics)
+ return;
+
+ /* Fill heap-specific extended stats fields */
+ extVacStats->type = PGSTAT_EXTVAC_TABLE;
+ extVacStats->table.pages_scanned = vacrel->scanned_pages;
+ extVacStats->table.pages_removed = vacrel->removed_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->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;
+}
+
+static void
+accumulate_idxs_vacuum_statistics(LVRelState *vacrel, PgStat_VacuumRelationCounts * extVacIdxStats)
+{
+ /* Fill heap-specific extended stats fields */
+ 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;
+}
+
+/*
+ * Accumulate one index extended-vacuum report into a per-index running total.
+ *
+ * VACUUM may touch an index more than once: a bulkdelete pass for every index
+ * scan, plus a final cleanup pass. Rather than reporting to the cumulative
+ * stats system on every pass, callers sum the passes here and report the
+ * totals exactly once per index at the end of the vacuum. Shared between the
+ * non-parallel path (accumulating in vacrel) and the parallel path
+ * (accumulating in the DSM), hence not static.
+ */
+void
+extvac_accumulate_idx_report(PgStat_VacuumRelationCounts * dst,
+ const PgStat_VacuumRelationCounts * src)
+{
+ dst->type = PGSTAT_EXTVAC_INDEX;
+
+ dst->common.wal_records += src->common.wal_records;
+ dst->common.wal_fpi += src->common.wal_fpi;
+ dst->common.wal_bytes += src->common.wal_bytes;
+ dst->common.tuples_deleted += src->common.tuples_deleted;
+
+ dst->index.pages_deleted += src->index.pages_deleted;
+}
+
+/*
+ * Report the accumulated per-index extended vacuum statistics, one report per
+ * index. Used by the non-parallel path only; the parallel path reports its
+ * DSM-resident totals from parallel_vacuum_end().
+ */
+static void
+report_index_vacuum_extstats(LVRelState *vacrel)
+{
+ if (!pgstat_track_vacuum_statistics)
+ return;
+
+ for (int idx = 0; idx < vacrel->nindexes; idx++)
+ {
+ Relation indrel = vacrel->indrels[idx];
+
+ if (!vacrel->extVacIdxTouched[idx])
+ continue;
+
+ pgstat_report_vacuum_extstats(RelationGetRelid(indrel),
+ indrel->rd_rel->relisshared,
+ &vacrel->extVacIdxReports[idx]);
+ }
+}
/*
@@ -612,66 +816,6 @@ heap_vacuum_eager_scan_setup(LVRelState *vacrel, const VacuumParams *params)
first_region_ratio;
}
-/*
- * Fill the extended vacuum statistics report for a heap relation with the
- * counters that are derived directly from the LVRelState. Buffer, WAL and
- * timing counters require sampling resource usage around the vacuum and are
- * gathered separately; they are not touched here.
- */
-static void
-accumulate_heap_vacuum_statistics(LVRelState *vacrel,
- PgStat_VacuumRelationCounts *extVacStats)
-{
- if (!pgstat_track_vacuum_statistics)
- return;
-
- extVacStats->type = PGSTAT_EXTVAC_TABLE;
- extVacStats->table.pages_scanned = vacrel->scanned_pages;
- extVacStats->table.pages_removed = vacrel->removed_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_pages = vacrel->missed_dead_pages;
- extVacStats->table.missed_dead_tuples = vacrel->missed_dead_tuples;
-}
-
-/*
- * Report the per-index extended vacuum statistics, one report per index.
- *
- * The per-index counters (pages_deleted and the number of removed index
- * entries) are derived directly from each index's final IndexBulkDeleteResult,
- * which already holds the totals accumulated across all bulkdelete and cleanup
- * passes -- so no per-pass sampling is needed here. Used by the non-parallel
- * path only; the parallel path reports its DSM-resident results from
- * parallel_vacuum_end().
- */
-static void
-report_index_vacuum_extstats(LVRelState *vacrel)
-{
- if (!pgstat_track_vacuum_statistics)
- return;
-
- for (int idx = 0; idx < vacrel->nindexes; idx++)
- {
- Relation indrel = vacrel->indrels[idx];
- IndexBulkDeleteResult *istat = vacrel->indstats[idx];
- PgStat_VacuumRelationCounts report;
-
- /* Skip indexes that this vacuum did not process */
- if (istat == NULL)
- continue;
-
- memset(&report, 0, sizeof(report));
- report.type = PGSTAT_EXTVAC_INDEX;
- report.common.tuples_deleted = istat->tuples_removed;
- report.index.pages_deleted = istat->pages_deleted;
-
- pgstat_report_vacuum_extstats(RelationGetRelid(indrel),
- indrel->rd_rel->relisshared,
- &report);
- }
-}
-
/*
* heap_vacuum_rel() -- perform VACUUM for one heap relation
*
@@ -698,7 +842,6 @@ heap_vacuum_rel(Relation rel, const VacuumParams *params,
new_rel_allvisible,
new_rel_allfrozen;
PGRUsage ru0;
- TimestampTz starttime = 0;
PgStat_Counter startreadtime = 0,
startwritetime = 0;
WalUsage startwalusage = pgWalUsage;
@@ -706,9 +849,11 @@ heap_vacuum_rel(Relation rel, const VacuumParams *params,
ErrorContextCallback errcallback;
char **indnames = NULL;
Size dead_items_max_bytes = 0;
+ LVExtStatCounters extVacCounters;
PgStat_VacuumRelationCounts extVacReport;
+ TimestampTz starttime;
- /* Initialize the extended vacuum statistics report */
+ /* Initialize vacuum statistics */
memset(&extVacReport, 0, sizeof(PgStat_VacuumRelationCounts));
verbose = (params->options & VACOPT_VERBOSE) != 0;
@@ -724,8 +869,8 @@ heap_vacuum_rel(Relation rel, const VacuumParams *params,
}
}
- /* Used for instrumentation and stats report */
starttime = GetCurrentTimestamp();
+ extvac_stats_start(rel, &extVacCounters);
pgstat_progress_start_command(PROGRESS_COMMAND_VACUUM,
RelationGetRelid(rel));
@@ -753,8 +898,8 @@ heap_vacuum_rel(Relation rel, const VacuumParams *params,
vacrel = palloc0_object(LVRelState);
vacrel->dbname = get_database_name(MyDatabaseId);
vacrel->relnamespace = get_namespace_name(RelationGetNamespace(rel));
- vacrel->reloid = RelationGetRelid(rel);
vacrel->relname = pstrdup(RelationGetRelationName(rel));
+ vacrel->reloid = RelationGetRelid(rel);
vacrel->indname = NULL;
vacrel->phase = VACUUM_ERRCB_PHASE_UNKNOWN;
vacrel->verbose = verbose;
@@ -763,12 +908,26 @@ heap_vacuum_rel(Relation rel, const VacuumParams *params,
errcallback.previous = error_context_stack;
error_context_stack = &errcallback;
+ memset(&vacrel->extVacReportIdx, 0, sizeof(PgStat_VacuumRelationCounts));
+ memset(&extVacReport.common, 0, sizeof(PgStat_CommonCounts));
+
/* Set up high level stuff about rel and its indexes */
vacrel->rel = rel;
vac_open_indexes(vacrel->rel, RowExclusiveLock, &vacrel->nindexes,
&vacrel->indrels);
vacrel->bstrategy = bstrategy;
+ /*
+ * Allocate per-index accumulators for extended vacuum statistics. Index
+ * passes add into these and the totals are reported once per index at the
+ * end of the vacuum (see report_index_vacuum_extstats()).
+ */
+ if (vacrel->nindexes > 0)
+ {
+ vacrel->extVacIdxReports =
+ palloc0_array(PgStat_VacuumRelationCounts, vacrel->nindexes);
+ vacrel->extVacIdxTouched = palloc0_array(bool, vacrel->nindexes);
+ }
if (instrument && vacrel->nindexes > 0)
{
/* Copy index names used by instrumentation (not error reporting) */
@@ -871,6 +1030,7 @@ heap_vacuum_rel(Relation rel, const VacuumParams *params,
vacrel->aggressive = vacuum_get_cutoffs(rel, params, &vacrel->cutoffs);
vacrel->rel_pages = orig_rel_pages = RelationGetNumberOfBlocks(rel);
vacrel->vistest = GlobalVisTestFor(rel);
+ vacrel->wraparound_failsafe_count = 0;
/* Initialize state used to track oldest extant XID/MXID */
vacrel->NewRelfrozenXid = vacrel->cutoffs.OldestXmin;
@@ -1054,9 +1214,14 @@ heap_vacuum_rel(Relation rel, const VacuumParams *params,
* soon in cases where the failsafe prevented significant amounts of heap
* vacuuming.
*/
+
+ /*
+ * Make generic extended vacuum stats report and fill heap-specific
+ * extended stats fields.
+ */
+ extvac_stats_end(vacrel->rel, &extVacCounters, &extVacReport.common);
accumulate_heap_vacuum_statistics(vacrel, &extVacReport);
- pgstat_report_vacuum_extstats(vacrel->reloid, rel->rd_rel->relisshared,
- &extVacReport);
+ pgstat_report_vacuum_extstats(vacrel->reloid, rel->rd_rel->relisshared, &extVacReport);
pgstat_report_vacuum(rel,
Max(vacrel->new_live_tuples, 0),
vacrel->recently_dead_tuples +
@@ -1695,7 +1860,8 @@ lazy_scan_heap(LVRelState *vacrel)
* Report the per-index extended vacuum statistics accumulated over all
* bulkdelete and cleanup passes, exactly once per index. The parallel
* path reports its DSM-resident totals from parallel_vacuum_end() instead,
- * so only do it here when index vacuuming ran in the leader.
+ * so only do it here when index vacuuming ran in the leader. pvs is still
+ * alive at this point (it is torn down later in dead_items_cleanup()).
*/
if (vacrel->nindexes > 0 && !ParallelVacuumIsActive(vacrel))
report_index_vacuum_extstats(vacrel);
@@ -2616,7 +2782,7 @@ lazy_vacuum_all_indexes(LVRelState *vacrel)
vacrel->indstats[idx] = lazy_vacuum_one_index(indrel, istat,
old_live_tuples,
- vacrel);
+ vacrel, idx);
/* Report the number of indexes vacuumed */
pgstat_progress_update_param(PROGRESS_VACUUM_INDEXES_PROCESSED,
@@ -2632,11 +2798,21 @@ lazy_vacuum_all_indexes(LVRelState *vacrel)
}
else
{
+ LVExtStatCounters counters;
+ PgStat_VacuumRelationCounts extVacReport;
+
+ memset(&extVacReport.common, 0, sizeof(PgStat_CommonCounts));
+
+ extvac_stats_start(vacrel->rel, &counters);
+
/* Outsource everything to parallel variant */
parallel_vacuum_bulkdel_all_indexes(vacrel->pvs, old_live_tuples,
vacrel->num_index_scans,
&(vacrel->worker_usage.vacuum));
+ extvac_stats_end(vacrel->rel, &counters, &extVacReport.common);
+ accumulate_idxs_vacuum_statistics(vacrel, &extVacReport);
+
/*
* Do a postcheck to consider applying wraparound failsafe now. Note
* that parallel VACUUM only gets the precheck and this postcheck.
@@ -2984,6 +3160,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
@@ -3057,7 +3234,7 @@ lazy_cleanup_all_indexes(LVRelState *vacrel)
vacrel->indstats[idx] =
lazy_cleanup_one_index(indrel, istat, reltuples,
- estimated_count, vacrel);
+ estimated_count, vacrel, idx);
/* Report the number of indexes cleaned up */
pgstat_progress_update_param(PROGRESS_VACUUM_INDEXES_PROCESSED,
@@ -3066,11 +3243,21 @@ lazy_cleanup_all_indexes(LVRelState *vacrel)
}
else
{
+ LVExtStatCounters counters;
+ PgStat_VacuumRelationCounts extVacReport;
+
+ memset(&extVacReport.common, 0, sizeof(PgStat_CommonCounts));
+
+ extvac_stats_start(vacrel->rel, &counters);
+
/* Outsource everything to parallel variant */
parallel_vacuum_cleanup_all_indexes(vacrel->pvs, reltuples,
vacrel->num_index_scans,
estimated_count,
&(vacrel->worker_usage.cleanup));
+
+ extvac_stats_end(vacrel->rel, &counters, &extVacReport.common);
+ accumulate_idxs_vacuum_statistics(vacrel, &extVacReport);
}
/* Reset the progress counters */
@@ -3092,10 +3279,22 @@ lazy_cleanup_all_indexes(LVRelState *vacrel)
*/
static IndexBulkDeleteResult *
lazy_vacuum_one_index(Relation indrel, IndexBulkDeleteResult *istat,
- double reltuples, LVRelState *vacrel)
+ double reltuples, LVRelState *vacrel, int idx)
{
IndexVacuumInfo ivinfo;
LVSavedErrInfo saved_err_info;
+ LVExtStatCountersIdx extVacCounters;
+ PgStat_VacuumRelationCounts extVacReport;
+
+ /*
+ * Zero the report up front: extvac_stats_end_idx() leaves it untouched
+ * when statistics tracking is disabled, but the accumulators below read it
+ * unconditionally.
+ */
+ memset(&extVacReport, 0, sizeof(PgStat_VacuumRelationCounts));
+
+ /* Set initial statistics values to gather vacuum statistics for the index */
+ extvac_stats_start_idx(indrel, istat, &extVacCounters);
ivinfo.index = indrel;
ivinfo.heaprel = vacrel->rel;
@@ -3122,6 +3321,19 @@ lazy_vacuum_one_index(Relation indrel, IndexBulkDeleteResult *istat,
istat = vac_bulkdel_one_index(&ivinfo, istat, vacrel->dead_items,
vacrel->dead_items_info);
+ /* Make extended vacuum stats report for index */
+ extvac_stats_end_idx(indrel, istat, &extVacCounters, &extVacReport);
+
+ /* This index's time must be excluded from the parent heap's totals */
+ accumulate_idxs_vacuum_statistics(vacrel, &extVacReport);
+
+ /*
+ * Accumulate this pass into the index's running totals. They are reported
+ * to the cumulative stats system once per index at the end of the vacuum.
+ */
+ extvac_accumulate_idx_report(&vacrel->extVacIdxReports[idx], &extVacReport);
+ vacrel->extVacIdxTouched[idx] = true;
+
/* Revert to the previous phase information for error traceback */
restore_vacuum_error_info(vacrel, &saved_err_info);
pfree(vacrel->indname);
@@ -3142,10 +3354,22 @@ lazy_vacuum_one_index(Relation indrel, IndexBulkDeleteResult *istat,
static IndexBulkDeleteResult *
lazy_cleanup_one_index(Relation indrel, IndexBulkDeleteResult *istat,
double reltuples, bool estimated_count,
- LVRelState *vacrel)
+ LVRelState *vacrel, int idx)
{
IndexVacuumInfo ivinfo;
LVSavedErrInfo saved_err_info;
+ LVExtStatCountersIdx extVacCounters;
+ PgStat_VacuumRelationCounts extVacReport;
+
+ /*
+ * Zero the report up front: extvac_stats_end_idx() leaves it untouched
+ * when statistics tracking is disabled, but the accumulators below read it
+ * unconditionally.
+ */
+ memset(&extVacReport, 0, sizeof(PgStat_VacuumRelationCounts));
+
+ /* Set initial statistics values to gather vacuum statistics for the index */
+ extvac_stats_start_idx(indrel, istat, &extVacCounters);
ivinfo.index = indrel;
ivinfo.heaprel = vacrel->rel;
@@ -3171,6 +3395,19 @@ lazy_cleanup_one_index(Relation indrel, IndexBulkDeleteResult *istat,
istat = vac_cleanup_one_index(&ivinfo, istat);
+ /* Make extended vacuum stats report for index */
+ extvac_stats_end_idx(indrel, istat, &extVacCounters, &extVacReport);
+
+ /* This index's time must be excluded from the parent heap's totals */
+ accumulate_idxs_vacuum_statistics(vacrel, &extVacReport);
+
+ /*
+ * Accumulate this pass into the index's running totals. They are reported
+ * to the cumulative stats system once per index at the end of the vacuum.
+ */
+ extvac_accumulate_idx_report(&vacrel->extVacIdxReports[idx], &extVacReport);
+ vacrel->extVacIdxTouched[idx] = true;
+
/* Revert to the previous phase information for error traceback */
restore_vacuum_error_info(vacrel, &saved_err_info);
pfree(vacrel->indname);
@@ -3616,7 +3853,7 @@ dead_items_cleanup(LVRelState *vacrel)
}
/* End parallel mode */
- parallel_vacuum_end(vacrel->pvs, vacrel->indstats);
+ parallel_vacuum_end(vacrel->pvs, vacrel->indstats, &vacrel->extVacReportIdx);
vacrel->pvs = NULL;
}
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index 6f697ab390..47d6aa6aea 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -1572,7 +1572,10 @@ CREATE VIEW pg_stat_vacuum_tables AS
S.tuples_frozen AS tuples_frozen,
S.recently_dead_tuples AS recently_dead_tuples,
S.missed_dead_pages AS missed_dead_pages,
- S.missed_dead_tuples AS missed_dead_tuples
+ S.missed_dead_tuples AS missed_dead_tuples,
+ S.wal_records AS wal_records,
+ S.wal_fpi AS wal_fpi,
+ S.wal_bytes AS wal_bytes
FROM pg_class C JOIN
pg_namespace N ON N.oid = C.relnamespace,
@@ -1588,7 +1591,11 @@ CREATE VIEW pg_stat_vacuum_indexes AS
I.relname AS indexrelname,
S.pages_deleted AS pages_deleted,
- S.tuples_deleted AS tuples_deleted
+ S.tuples_deleted AS tuples_deleted,
+
+ S.wal_records AS wal_records,
+ S.wal_fpi AS wal_fpi,
+ S.wal_bytes AS wal_bytes
FROM
pg_class C JOIN
pg_index X ON C.oid = X.indrelid JOIN
@@ -1596,3 +1603,17 @@ CREATE VIEW pg_stat_vacuum_indexes AS
LEFT JOIN pg_namespace N ON (N.oid = C.relnamespace),
LATERAL pg_stat_get_vacuum_indexes(I.oid) S
WHERE C.relkind IN ('r', 't', 'm');
+
+CREATE VIEW pg_stat_vacuum_database AS
+ SELECT
+ D.oid AS dboid,
+ D.datname AS dbname,
+
+ S.errors AS errors,
+
+ S.wal_records AS wal_records,
+ S.wal_fpi AS wal_fpi,
+ S.wal_bytes AS wal_bytes
+ FROM
+ pg_database D,
+ LATERAL pg_stat_get_vacuum_database(D.oid) S;
diff --git a/src/backend/commands/dbcommands.c b/src/backend/commands/dbcommands.c
index f0819d15ab..49c3b40c35 100644
--- a/src/backend/commands/dbcommands.c
+++ b/src/backend/commands/dbcommands.c
@@ -1830,6 +1830,7 @@ dropdb(const char *dbname, bool missing_ok, bool force)
* Tell the cumulative stats system to forget it immediately, too.
*/
pgstat_drop_database(db_id);
+ pgstat_drop_vacuum_database(db_id);
/*
* Except for the deletion of the catalog row, subsequent actions are not
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index a4abb29cf6..aacecee58a 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. */
+double VacuumDelayTime = 0; /* msec. */
+
/* non-export function prototypes */
static List *expand_vacuum_rel(VacuumRelation *vrel,
MemoryContext vac_context, int options);
@@ -2566,6 +2569,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 7725c4ecc1..ea13859485 100644
--- a/src/backend/commands/vacuumparallel.c
+++ b/src/backend/commands/vacuumparallel.c
@@ -203,6 +203,16 @@ typedef struct PVIndStats
*/
bool istat_updated; /* are the stats updated? */
IndexBulkDeleteResult istat;
+
+ /*
+ * Extended vacuum statistics accumulated across all bulkdelete and cleanup
+ * passes for this index, by whichever process (leader or worker) ran each
+ * pass. The leader reports the totals to the cumulative stats system once
+ * per index in parallel_vacuum_end(), and also feeds them back so that the
+ * index work can be subtracted from the parent heap's figures.
+ */
+ bool extvac_touched; /* was this index processed at all? */
+ PgStat_VacuumRelationCounts extvacstats;
} PVIndStats;
/*
@@ -512,10 +522,22 @@ parallel_vacuum_init(Relation rel, Relation *indrels, int nindexes,
* context, but that won't be safe (see ExitParallelMode).
*/
void
-parallel_vacuum_end(ParallelVacuumState *pvs, IndexBulkDeleteResult **istats)
+parallel_vacuum_end(ParallelVacuumState *pvs, IndexBulkDeleteResult **istats,
+ PgStat_VacuumRelationCounts *idx_heap_total)
{
+ PgStat_VacuumRelationCounts *extvacstats = NULL;
+
Assert(!IsParallelWorker());
+ /*
+ * Stash the per-index extended vacuum statistics while the DSM is still
+ * mapped; they are reported once per index after we have left parallel
+ * mode. An index that was never processed keeps its zeroed slot (type
+ * PGSTAT_EXTVAC_INVALID) and is skipped there.
+ */
+ if (pgstat_track_vacuum_statistics && pvs->nindexes > 0)
+ extvacstats = palloc0_array(PgStat_VacuumRelationCounts, pvs->nindexes);
+
/* Copy the updated statistics */
for (int i = 0; i < pvs->nindexes; i++)
{
@@ -528,6 +550,10 @@ parallel_vacuum_end(ParallelVacuumState *pvs, IndexBulkDeleteResult **istats)
}
else
istats[i] = NULL;
+
+ if (extvacstats != NULL && indstats->extvac_touched)
+ memcpy(&extvacstats[i], &indstats->extvacstats,
+ sizeof(PgStat_VacuumRelationCounts));
}
TidStoreDestroy(pvs->dead_items);
@@ -537,29 +563,30 @@ parallel_vacuum_end(ParallelVacuumState *pvs, IndexBulkDeleteResult **istats)
/*
* Report the per-index extended vacuum statistics, one report per index,
- * derived directly from each index's final IndexBulkDeleteResult. The
- * indexes are still open here (pvs->indrels is the leader's own array, not
- * in the now-destroyed DSM).
+ * and add each index's resource usage to idx_heap_total so the caller can
+ * subtract it from the parent heap's figures (the leader folds the workers'
+ * buffer/WAL usage into its own, so without this the index work would be
+ * attributed to the heap). The indexes are still open here (pvs->indrels
+ * is the leader's own array, not in the now-destroyed DSM).
*/
- if (pgstat_track_vacuum_statistics)
+ if (extvacstats != NULL)
{
for (int i = 0; i < pvs->nindexes; i++)
{
Relation indrel = pvs->indrels[i];
- PgStat_VacuumRelationCounts report;
- if (istats[i] == NULL)
+ if (extvacstats[i].type != PGSTAT_EXTVAC_INDEX)
continue;
- memset(&report, 0, sizeof(report));
- report.type = PGSTAT_EXTVAC_INDEX;
- report.common.tuples_deleted = istats[i]->tuples_removed;
- report.index.pages_deleted = istats[i]->pages_deleted;
-
pgstat_report_vacuum_extstats(RelationGetRelid(indrel),
indrel->rd_rel->relisshared,
- &report);
+ &extvacstats[i]);
+
+ if (idx_heap_total != NULL)
+ extvac_accumulate_idx_report(idx_heap_total, &extvacstats[i]);
}
+
+ pfree(extvacstats);
}
if (AmAutoVacuumWorkerProcess())
@@ -1103,6 +1130,14 @@ parallel_vacuum_process_one_index(ParallelVacuumState *pvs, Relation indrel,
IndexBulkDeleteResult *istat = NULL;
IndexBulkDeleteResult *istat_res;
IndexVacuumInfo ivinfo;
+ LVExtStatCountersIdx extVacCounters;
+ PgStat_VacuumRelationCounts extVacReport;
+
+ /*
+ * Zero the report up front: extvac_stats_end_idx() leaves it untouched when
+ * statistics tracking is disabled.
+ */
+ memset(&extVacReport, 0, sizeof(PgStat_VacuumRelationCounts));
/*
* Update the pointer to the corresponding bulk-deletion result if someone
@@ -1111,6 +1146,9 @@ parallel_vacuum_process_one_index(ParallelVacuumState *pvs, Relation indrel,
if (indstats->istat_updated)
istat = &(indstats->istat);
+ /* Snapshot the resource usage before processing this index pass */
+ extvac_stats_start_idx(indrel, istat, &extVacCounters);
+
ivinfo.index = indrel;
ivinfo.heaprel = pvs->heaprel;
ivinfo.analyze_only = false;
@@ -1139,6 +1177,20 @@ parallel_vacuum_process_one_index(ParallelVacuumState *pvs, Relation indrel,
RelationGetRelationName(indrel));
}
+ /*
+ * Accumulate this pass's extended vacuum statistics into the index's
+ * DSM-resident running totals. The leader reports them, and subtracts
+ * them from the parent heap, once per index in parallel_vacuum_end(). Each
+ * index is processed by a single process per pass, so no locking is needed
+ * here (same as the istat update below).
+ */
+ extvac_stats_end_idx(indrel, istat_res, &extVacCounters, &extVacReport);
+ if (pgstat_track_vacuum_statistics)
+ {
+ extvac_accumulate_idx_report(&indstats->extvacstats, &extVacReport);
+ indstats->extvac_touched = true;
+ }
+
/*
* Copy the index bulk-deletion result returned from ambulkdelete and
* amvacuumcleanup to the DSM segment if it's the first cycle because they
diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index 4e950672e2..3741e4e54a 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -500,6 +500,21 @@ static const PgStat_KindInfo pgstat_kind_builtin_infos[PGSTAT_KIND_BUILTIN_SIZE]
.reset_all_cb = pgstat_wal_reset_all_cb,
.snapshot_cb = pgstat_wal_snapshot_cb,
},
+ [PGSTAT_KIND_VACUUM_DB] = {
+ .name = "vacuum statistics",
+
+ .fixed_amount = false,
+ .write_to_file = true,
+ /* so pg_stat_database entries can be seen in all databases */
+ .accessed_across_databases = true,
+
+ .shared_size = sizeof(PgStatShared_VacuumDB),
+ .shared_data_off = offsetof(PgStatShared_VacuumDB, stats),
+ .shared_data_len = sizeof(((PgStatShared_VacuumDB *) 0)->stats),
+ .pending_size = sizeof(PgStat_VacuumDBCounts),
+
+ .flush_pending_cb = pgstat_vacuum_db_flush_cb,
+ },
[PGSTAT_KIND_VACUUM_RELATION] = {
.name = "vacuum statistics",
diff --git a/src/backend/utils/activity/pgstat_database.c b/src/backend/utils/activity/pgstat_database.c
index 7f3bc01659..cdff619200 100644
--- a/src/backend/utils/activity/pgstat_database.c
+++ b/src/backend/utils/activity/pgstat_database.c
@@ -46,6 +46,15 @@ pgstat_drop_database(Oid databaseid)
pgstat_drop_transactional(PGSTAT_KIND_DATABASE, databaseid, InvalidOid);
}
+/*
+ * Remove entry for the database being dropped.
+ */
+void
+pgstat_drop_vacuum_database(Oid databaseid)
+{
+ pgstat_drop_transactional(PGSTAT_KIND_VACUUM_DB, databaseid, InvalidOid);
+}
+
/*
* Called from autovacuum.c to report startup of an autovacuum process.
* We are called before InitPostgres is done, so can't rely on MyDatabaseId;
diff --git a/src/backend/utils/activity/pgstat_vacuum.c b/src/backend/utils/activity/pgstat_vacuum.c
index 644a732592..99c4932ded 100644
--- a/src/backend/utils/activity/pgstat_vacuum.c
+++ b/src/backend/utils/activity/pgstat_vacuum.c
@@ -7,7 +7,7 @@
* kept separate from pgstat_relation.c and pgstat_database.c to reduce the
* memory footprint of the regular relation and database statistics: vacuum
* metrics require significantly more space per relation, so they live in their
- * own PGSTAT_KIND_VACUUM_RELATION stats kind.
+ * own PGSTAT_KIND_VACUUM_RELATION and PGSTAT_KIND_VACUUM_DB stats kinds.
*
* Copyright (c) 2001-2026, PostgreSQL Global Development Group
*
@@ -22,15 +22,31 @@
#include "utils/memutils.h"
#include "utils/pgstat_internal.h"
+/* ----------
+ * GUC parameters
+ * ----------
+ */
+bool pgstat_track_vacuum_statistics_for_relations = false;
+
+#define ACCUMULATE_FIELD(field) (dst->field += src->field)
#define ACCUMULATE_SUBFIELD(substruct, field) (dst->substruct.field += src->substruct.field)
/*
- * Accumulate the per-table extended vacuum counters collected so far.
- *
- * Only the counters derived directly from the vacuum's own bookkeeping are
- * summed here. The buffer, WAL and timing counters (and the per-index
- * counters) are accumulated by additional code added together with the
- * helpers that gather them.
+ * Accumulate the counters that are common to heap relations, indexes and
+ * databases.
+ */
+static void
+pgstat_accumulate_common(PgStat_CommonCounts *dst, const PgStat_CommonCounts *src)
+{
+ ACCUMULATE_FIELD(wal_records);
+ ACCUMULATE_FIELD(wal_fpi);
+ ACCUMULATE_FIELD(wal_bytes);
+
+ ACCUMULATE_FIELD(tuples_deleted);
+}
+
+/*
+ * Accumulate per-relation (heap or index) extended vacuum counters.
*/
static void
pgstat_accumulate_extvac_stats_relations(PgStat_VacuumRelationCounts *dst,
@@ -46,15 +62,15 @@ pgstat_accumulate_extvac_stats_relations(PgStat_VacuumRelationCounts *dst,
src->type != PGSTAT_EXTVAC_DB &&
src->type == dst->type);
- ACCUMULATE_SUBFIELD(common, tuples_deleted);
+ pgstat_accumulate_common(&dst->common, &src->common);
if (dst->type == PGSTAT_EXTVAC_TABLE)
{
ACCUMULATE_SUBFIELD(table, pages_scanned);
ACCUMULATE_SUBFIELD(table, pages_removed);
- ACCUMULATE_SUBFIELD(table, tuples_frozen);
- ACCUMULATE_SUBFIELD(table, recently_dead_tuples);
ACCUMULATE_SUBFIELD(table, missed_dead_pages);
+ ACCUMULATE_SUBFIELD(table, tuples_frozen);
+ ACCUMULATE_SUBFIELD(table, recently_dead_tuples);
ACCUMULATE_SUBFIELD(table, missed_dead_tuples);
}
else if (dst->type == PGSTAT_EXTVAC_INDEX)
@@ -64,8 +80,22 @@ pgstat_accumulate_extvac_stats_relations(PgStat_VacuumRelationCounts *dst,
}
/*
- * Report that the relation was just vacuumed, accumulating its extended
- * statistics into the per-relation entry.
+ * Accumulate per-database extended vacuum counters.
+ */
+static void
+pgstat_accumulate_extvac_stats_db(PgStat_VacuumDBCounts *dst,
+ PgStat_VacuumDBCounts *src)
+{
+ if (!pgstat_track_vacuum_statistics)
+ return;
+
+ pgstat_accumulate_common(&dst->common, &src->common);
+ dst->errors += src->errors;
+}
+
+/*
+ * Report that the relation was just vacuumed, accumulating both its own
+ * extended statistics and the database-wide aggregate.
*/
void
pgstat_report_vacuum_extstats(Oid tableoid, bool shared,
@@ -73,20 +103,29 @@ pgstat_report_vacuum_extstats(Oid tableoid, bool shared,
{
PgStat_EntryRef *entry_ref;
PgStat_RelationVacuumPending *relpending;
+ PgStat_VacuumDBCounts *dbpending;
Oid dboid = (shared ? InvalidOid : MyDatabaseId);
if (!pgstat_track_vacuum_statistics)
return;
/*
- * Accumulate into a pending entry instead of taking a shared-stats lock
- * here; pgstat_report_stat() flushes it to shared memory through the
- * registered flush callback once the vacuum's transaction completes.
+ * Accumulate into pending entries instead of taking a shared-stats lock
+ * here; pgstat_report_stat() flushes them to shared memory through the
+ * registered flush callbacks once the vacuum's transaction completes.
*/
+
+ /* Per-relation extended vacuum statistics */
entry_ref = pgstat_prep_pending_entry(PGSTAT_KIND_VACUUM_RELATION,
dboid, tableoid, NULL);
relpending = (PgStat_RelationVacuumPending *) entry_ref->pending;
pgstat_accumulate_extvac_stats_relations(&relpending->counts, params);
+
+ /* Database-wide aggregate of the same work */
+ entry_ref = pgstat_prep_pending_entry(PGSTAT_KIND_VACUUM_DB,
+ dboid, InvalidOid, NULL);
+ dbpending = (PgStat_VacuumDBCounts *) entry_ref->pending;
+ pgstat_accumulate_common(&dbpending->common, ¶ms->common);
}
/*
@@ -120,6 +159,31 @@ pgstat_vacuum_relation_flush_cb(PgStat_EntryRef *entry_ref, bool nowait)
return true;
}
+/*
+ * Flush out pending per-database extended vacuum stats for the entry.
+ */
+bool
+pgstat_vacuum_db_flush_cb(PgStat_EntryRef *entry_ref, bool nowait)
+{
+ PgStatShared_VacuumDB *sharedent;
+ PgStat_VacuumDBCounts *pendingent;
+
+ pendingent = (PgStat_VacuumDBCounts *) entry_ref->pending;
+ sharedent = (PgStatShared_VacuumDB *) entry_ref->shared_stats;
+
+ if (pg_memory_is_all_zeros(pendingent, sizeof(PgStat_VacuumDBCounts)))
+ return true;
+
+ if (!pgstat_lock_entry(entry_ref, nowait))
+ return false;
+
+ pgstat_accumulate_extvac_stats_db(&sharedent->stats, pendingent);
+
+ pgstat_unlock_entry(entry_ref);
+
+ return true;
+}
+
/*
* Support function for the SQL-callable pgstat* functions. Returns the vacuum
* collected statistics for one relation or NULL.
@@ -130,3 +194,14 @@ pgstat_fetch_stat_vacuum_tabentry(Oid relid, Oid dbid)
return (PgStat_VacuumRelationCounts *)
pgstat_fetch_entry(PGSTAT_KIND_VACUUM_RELATION, dbid, relid, NULL);
}
+
+/*
+ * Support function for the SQL-callable pgstat* functions. Returns the vacuum
+ * collected statistics for one database or NULL.
+ */
+PgStat_VacuumDBCounts *
+pgstat_fetch_stat_vacuum_dbentry(Oid dbid)
+{
+ return (PgStat_VacuumDBCounts *)
+ pgstat_fetch_entry(PGSTAT_KIND_VACUUM_DB, dbid, InvalidOid, NULL);
+}
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 742f4974d5..aad4427469 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -2374,13 +2374,14 @@ pg_stat_have_stats(PG_FUNCTION_ARGS)
Datum
pg_stat_get_vacuum_tables(PG_FUNCTION_ARGS)
{
-#define PG_STAT_GET_VACUUM_TABLES_STATS_COLS 8
+#define PG_STAT_GET_VACUUM_TABLES_STATS_COLS 11
Oid relid = PG_GETARG_OID(0);
PgStat_VacuumRelationCounts *extvacuum;
TupleDesc tupdesc;
Datum values[PG_STAT_GET_VACUUM_TABLES_STATS_COLS] = {0};
bool nulls[PG_STAT_GET_VACUUM_TABLES_STATS_COLS] = {0};
+ char buf[256];
int i = 0;
/* Build a tuple descriptor for our result type */
@@ -2407,6 +2408,13 @@ pg_stat_get_vacuum_tables(PG_FUNCTION_ARGS)
values[i++] = Int64GetDatum(extvacuum->table.recently_dead_tuples);
values[i++] = Int64GetDatum(extvacuum->table.missed_dead_pages);
values[i++] = Int64GetDatum(extvacuum->table.missed_dead_tuples);
+ values[i++] = Int64GetDatum(extvacuum->common.wal_records);
+ values[i++] = Int64GetDatum(extvacuum->common.wal_fpi);
+ snprintf(buf, sizeof buf, UINT64_FORMAT, extvacuum->common.wal_bytes);
+ values[i++] = DirectFunctionCall3(numeric_in,
+ CStringGetDatum(buf),
+ ObjectIdGetDatum(0),
+ Int32GetDatum(-1));
Assert(i == PG_STAT_GET_VACUUM_TABLES_STATS_COLS);
@@ -2420,13 +2428,14 @@ pg_stat_get_vacuum_tables(PG_FUNCTION_ARGS)
Datum
pg_stat_get_vacuum_indexes(PG_FUNCTION_ARGS)
{
-#define PG_STAT_GET_VACUUM_INDEX_STATS_COLS 3
+#define PG_STAT_GET_VACUUM_INDEX_STATS_COLS 6
Oid relid = PG_GETARG_OID(0);
PgStat_VacuumRelationCounts *extvacuum;
TupleDesc tupdesc;
Datum values[PG_STAT_GET_VACUUM_INDEX_STATS_COLS] = {0};
bool nulls[PG_STAT_GET_VACUUM_INDEX_STATS_COLS] = {0};
+ char buf[256];
int i = 0;
if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
@@ -2448,8 +2457,58 @@ pg_stat_get_vacuum_indexes(PG_FUNCTION_ARGS)
values[i++] = Int64GetDatum(extvacuum->index.pages_deleted);
values[i++] = Int64GetDatum(extvacuum->common.tuples_deleted);
+ values[i++] = Int64GetDatum(extvacuum->common.wal_records);
+ values[i++] = Int64GetDatum(extvacuum->common.wal_fpi);
+ snprintf(buf, sizeof buf, UINT64_FORMAT, extvacuum->common.wal_bytes);
+ values[i++] = DirectFunctionCall3(numeric_in,
+ CStringGetDatum(buf),
+ ObjectIdGetDatum(0),
+ Int32GetDatum(-1));
Assert(i == PG_STAT_GET_VACUUM_INDEX_STATS_COLS);
/* Returns the record as Datum */
PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
}
+
+/*
+ * Get the extended vacuum statistics for a database.
+ */
+Datum
+pg_stat_get_vacuum_database(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_GET_VACUUM_DATABASE_STATS_COLS 5
+
+ Oid dbid = PG_GETARG_OID(0);
+ PgStat_VacuumDBCounts *extvacuum;
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_GET_VACUUM_DATABASE_STATS_COLS] = {0};
+ bool nulls[PG_STAT_GET_VACUUM_DATABASE_STATS_COLS] = {0};
+ char buf[256];
+ int i = 0;
+
+ if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
+ elog(ERROR, "return type must be a row type");
+
+ extvacuum = OidIsValid(dbid) ? pgstat_fetch_stat_vacuum_dbentry(dbid) : NULL;
+ if (!extvacuum)
+ {
+ InitMaterializedSRF(fcinfo, 0);
+ PG_RETURN_VOID();
+ }
+
+ values[i++] = ObjectIdGetDatum(dbid);
+
+ values[i++] = Int32GetDatum(extvacuum->errors);
+
+ values[i++] = Int64GetDatum(extvacuum->common.wal_records);
+ values[i++] = Int64GetDatum(extvacuum->common.wal_fpi);
+ snprintf(buf, sizeof buf, UINT64_FORMAT, extvacuum->common.wal_bytes);
+ values[i++] = DirectFunctionCall3(numeric_in,
+ CStringGetDatum(buf),
+ ObjectIdGetDatum(0),
+ Int32GetDatum(-1));
+ Assert(i == PG_STAT_GET_VACUUM_DATABASE_STATS_COLS);
+
+ /* Returns the record as Datum */
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 6d683413a4..3dd3d81a13 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12643,9 +12643,9 @@
proname => 'pg_stat_get_vacuum_tables', prorows => 1000, provolatile => 's', prorettype => 'record', proisstrict => 'f',
proretset => 't',
proargtypes => 'oid',
- proallargtypes => '{oid,oid,int8,int8,int8,int8,int8,int8,int8}',
- proargmodes => '{i,o,o,o,o,o,o,o,o}',
- proargnames => '{reloid,relid,pages_scanned,pages_removed,tuples_deleted,tuples_frozen,recently_dead_tuples,missed_dead_pages,missed_dead_tuples}',
+ proallargtypes => '{oid,oid,int8,int8,int8,int8,int8,int8,int8,int8,int8,numeric}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o,o,o,o}',
+ proargnames => '{reloid,relid,pages_scanned,pages_removed,tuples_deleted,tuples_frozen,recently_dead_tuples,missed_dead_pages,missed_dead_tuples,wal_records,wal_fpi,wal_bytes}',
prosrc => 'pg_stat_get_vacuum_tables' }
# oid8 related functions
@@ -12718,8 +12718,17 @@
proname => 'pg_stat_get_vacuum_indexes', prorows => 1000, provolatile => 's', prorettype => 'record', proisstrict => 'f',
proretset => 't',
proargtypes => 'oid',
- proallargtypes => '{oid,oid,int8,int8}',
- proargmodes => '{i,o,o,o}',
- proargnames => '{reloid,relid,pages_deleted,tuples_deleted}',
+ proallargtypes => '{oid,oid,int8,int8,int8,int8,numeric}',
+ proargmodes => '{i,o,o,o,o,o,o}',
+ proargnames => '{reloid,relid,pages_deleted,tuples_deleted,wal_records,wal_fpi,wal_bytes}',
prosrc => 'pg_stat_get_vacuum_indexes' },
+{ oid => '8005',
+ descr => 'pg_stat_get_vacuum_database returns vacuum stats values for database',
+ proname => 'pg_stat_get_vacuum_database', prorows => 1000, provolatile => 's', prorettype => 'record', proisstrict => 'f',
+ proretset => 't',
+ proargtypes => 'oid',
+ proallargtypes => '{oid,oid,int4,int8,int8,numeric}',
+ proargmodes => '{i,o,o,o,o,o}',
+ proargnames => '{dbid,dboid,errors,wal_records,wal_fpi,wal_bytes}',
+ prosrc => 'pg_stat_get_vacuum_database' },
]
diff --git a/src/include/commands/vacuum.h b/src/include/commands/vacuum.h
index 956d9cea36..e3808b8587 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
@@ -321,6 +323,26 @@ typedef struct PVWorkerUsage
PVWorkerStats cleanup;
} PVWorkerUsage;
+/*
+ * Counters and usage data for extended stats tracking.
+ */
+typedef struct LVExtStatCounters
+{
+ TimestampTz starttime;
+ WalUsage walusage;
+ BufferUsage bufusage;
+ double VacuumDelayTime;
+ PgStat_Counter blocks_fetched;
+ PgStat_Counter blocks_hit;
+} LVExtStatCounters;
+
+typedef struct LVExtStatCountersIdx
+{
+ LVExtStatCounters common;
+ int64 pages_deleted;
+ int64 tuples_removed;
+} LVExtStatCountersIdx;
+
/* GUC parameters */
extern PGDLLIMPORT int default_statistics_target; /* PGDLLIMPORT for PostGIS */
extern PGDLLIMPORT int vacuum_freeze_min_age;
@@ -353,6 +375,7 @@ extern PGDLLIMPORT double vacuum_max_eager_freeze_failure_rate;
extern PGDLLIMPORT pg_atomic_uint32 *VacuumSharedCostBalance;
extern PGDLLIMPORT pg_atomic_uint32 *VacuumActiveNWorkers;
extern PGDLLIMPORT int VacuumCostBalanceLocal;
+extern PGDLLIMPORT double VacuumDelayTime;
extern PGDLLIMPORT bool VacuumFailsafeActive;
extern PGDLLIMPORT double vacuum_cost_delay;
@@ -409,7 +432,8 @@ extern ParallelVacuumState *parallel_vacuum_init(Relation rel, Relation *indrels
int nindexes, int nrequested_workers,
int vac_work_mem, int elevel,
BufferAccessStrategy bstrategy);
-extern void parallel_vacuum_end(ParallelVacuumState *pvs, IndexBulkDeleteResult **istats);
+extern void parallel_vacuum_end(ParallelVacuumState *pvs, IndexBulkDeleteResult **istats,
+ PgStat_VacuumRelationCounts *idx_heap_total);
extern TidStore *parallel_vacuum_get_dead_items(ParallelVacuumState *pvs,
VacDeadItemsInfo **dead_items_info_p);
extern void parallel_vacuum_reset_dead_items(ParallelVacuumState *pvs);
@@ -439,4 +463,10 @@ extern double anl_random_fract(void);
extern double anl_init_selection_state(int n);
extern double anl_get_next_S(double t, int n, double *stateptr);
+extern void extvac_stats_start_idx(Relation rel, IndexBulkDeleteResult *stats,
+ LVExtStatCountersIdx * counters);
+extern void extvac_stats_end_idx(Relation rel, IndexBulkDeleteResult *stats,
+ LVExtStatCountersIdx * counters, PgStat_VacuumRelationCounts * report);
+extern void extvac_accumulate_idx_report(PgStat_VacuumRelationCounts * dst,
+ const PgStat_VacuumRelationCounts * src);
#endif /* VACUUM_H */
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index bdcf758441..f704a15003 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -175,6 +175,11 @@ typedef struct PgStat_TableCounts
typedef struct PgStat_CommonCounts
{
+ /* WAL */
+ int64 wal_records;
+ int64 wal_fpi;
+ uint64 wal_bytes;
+
/* tuples */
int64 tuples_deleted;
} PgStat_CommonCounts;
@@ -237,6 +242,13 @@ typedef struct PgStat_VacuumRelationStatus
PgStat_VacuumRelationCounts counts; /* event counts to be sent */
} PgStat_VacuumRelationStatus;
+typedef struct PgStat_VacuumDBCounts
+{
+ Oid dbjid;
+ PgStat_CommonCounts common;
+ int32 errors;
+} PgStat_VacuumDBCounts;
+
/* ----------
* PgStat_TableStatus Per-table status within a backend
*
@@ -917,11 +929,13 @@ extern int pgstat_get_transactional_drops(bool isCommit, struct xl_xact_stats_it
extern void pgstat_execute_transactional_drops(int ndrops, struct xl_xact_stats_item *items, bool is_redo);
+extern void pgstat_drop_vacuum_database(Oid databaseid);
extern void pgstat_vacuum_relation_delete_pending_cb(Oid relid);
extern void
pgstat_report_vacuum_extstats(Oid tableoid, bool shared,
PgStat_VacuumRelationCounts * params);
extern PgStat_VacuumRelationCounts * pgstat_fetch_stat_vacuum_tabentry(Oid relid, Oid dbid);
+extern PgStat_VacuumDBCounts * pgstat_fetch_stat_vacuum_dbentry(Oid dbid);
/*
* Functions in pgstat_wal.c
@@ -940,6 +954,7 @@ extern PGDLLIMPORT bool pgstat_track_counts;
extern PGDLLIMPORT int pgstat_track_functions;
extern PGDLLIMPORT int pgstat_fetch_consistency;
extern PGDLLIMPORT bool pgstat_track_vacuum_statistics;
+extern PGDLLIMPORT bool pgstat_track_vacuum_statistics_for_relations;
/*
* Variables in pgstat_bgwriter.c
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index 46a127af2b..a234c797e0 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -507,6 +507,12 @@ typedef struct PgStatShared_Relation
PgStat_StatTabEntry stats;
} PgStatShared_Relation;
+typedef struct PgStatShared_VacuumDB
+{
+ PgStatShared_Common header;
+ PgStat_VacuumDBCounts stats;
+} PgStatShared_VacuumDB;
+
typedef struct PgStatShared_VacuumRelation
{
PgStatShared_Common header;
@@ -695,6 +701,7 @@ extern void *pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid,
bool *may_free);
extern void pgstat_snapshot_fixed(PgStat_Kind kind);
+bool pgstat_vacuum_db_flush_cb(PgStat_EntryRef *entry_ref, bool nowait);
extern bool pgstat_vacuum_relation_flush_cb(PgStat_EntryRef *entry_ref, bool nowait);
diff --git a/src/include/utils/pgstat_kind.h b/src/include/utils/pgstat_kind.h
index f92149066c..52d9367fbe 100644
--- a/src/include/utils/pgstat_kind.h
+++ b/src/include/utils/pgstat_kind.h
@@ -40,9 +40,10 @@
#define PGSTAT_KIND_SLRU 12
#define PGSTAT_KIND_WAL 13
#define PGSTAT_KIND_VACUUM_RELATION 14
+#define PGSTAT_KIND_VACUUM_DB 15
#define PGSTAT_KIND_BUILTIN_MIN PGSTAT_KIND_DATABASE
-#define PGSTAT_KIND_BUILTIN_MAX PGSTAT_KIND_VACUUM_RELATION
+#define PGSTAT_KIND_BUILTIN_MAX PGSTAT_KIND_VACUUM_DB
#define PGSTAT_KIND_BUILTIN_SIZE (PGSTAT_KIND_BUILTIN_MAX + 1)
/* Custom stats kinds */
diff --git a/src/test/modules/test_misc/meson.build b/src/test/modules/test_misc/meson.build
index 267145163a..805c6c2c39 100644
--- a/src/test/modules/test_misc/meson.build
+++ b/src/test/modules/test_misc/meson.build
@@ -23,6 +23,7 @@ tests += {
't/012_ddlutils.pl',
't/013_temp_obj_multisession.pl',
't/014_vacuum_stats.pl',
+ 't/016_vacuum_stats_parallel.pl',
],
# The injection points are cluster-wide, so disable installcheck
'runningcheck': false,
diff --git a/src/test/modules/test_misc/t/016_vacuum_stats_parallel.pl b/src/test/modules/test_misc/t/016_vacuum_stats_parallel.pl
new file mode 100644
index 0000000000..8b23e138fa
--- /dev/null
+++ b/src/test/modules/test_misc/t/016_vacuum_stats_parallel.pl
@@ -0,0 +1,104 @@
+# Copyright (c) 2024-2026, PostgreSQL Global Development Group
+
+# Test that per-index vacuum statistics are collected for indexes that are
+# vacuumed through the PARALLEL path.
+#
+# When VACUUM distributes index cleanup to parallel workers, the WAL/buffer
+# usage and the tuple counters of each index are produced in a different
+# process than the leader. The sampling machinery has to gather those numbers
+# from the workers and attribute them to the right index in pg_stat_vacuum_indexes
+# -- otherwise the index work is silently lost (or folded into the heap's
+# figures). A regression test cannot guarantee that the parallel path is taken,
+# so this is done in a TAP cluster where we can force it and verify, from
+# VACUUM (VERBOSE) output, that a worker was actually launched.
+
+use strict;
+use warnings FATAL => 'all';
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+my $node = PostgreSQL::Test::Cluster->new('main');
+$node->init;
+$node->append_conf(
+ 'postgresql.conf', qq[
+autovacuum = off
+track_vacuum_statistics = on
+# Make every index eligible for the parallel path and make sure workers can run.
+min_parallel_index_scan_size = 0
+max_parallel_maintenance_workers = 4
+max_worker_processes = 8
+max_parallel_workers = 8
+]);
+$node->start;
+
+# Build the relation that we will vacuum in parallel. Two indexes are needed
+# so the parallel path has more than one index to distribute, and the table is
+# large enough that the leftover indexes are worth scanning.
+my $setup = sub {
+ my ($tab) = @_;
+ $node->safe_psql(
+ 'postgres', qq[
+CREATE TABLE $tab (id int, val int) WITH (autovacuum_enabled = off);
+INSERT INTO $tab SELECT g, g FROM generate_series(1, 100000) g;
+CREATE INDEX ${tab}_i1 ON $tab (id);
+CREATE INDEX ${tab}_i2 ON $tab (val);
+DELETE FROM $tab WHERE id % 2 = 0; -- 50000 dead index entries per index
+]);
+};
+
+# Per-index statistics of $tab, with the table-specific prefix stripped from the
+# index name so the parallel and serial relations can be compared directly. We
+# compare the counters that are fully determined by the index content and are
+# therefore identical no matter which process did the work: the number of
+# removed index entries, the number of deleted index pages and the number of WAL
+# records. (wal_fpi/wal_bytes are left out: full-page images depend on when the
+# last checkpoint happened, so they legitimately differ between the two runs.)
+my $index_stats = sub {
+ my ($tab) = @_;
+ return $node->safe_psql(
+ 'postgres', qq[
+SELECT string_agg(
+ format('%s:%s:%s:%s', replace(indexrelname, '$tab', 'tab'),
+ tuples_deleted, pages_deleted, wal_records),
+ ',' ORDER BY indexrelname)
+ FROM pg_stat_vacuum_indexes WHERE relname = '$tab']);
+};
+
+# --- Parallel vacuum -------------------------------------------------------
+$setup->('vacparstat_par');
+
+# Run with VERBOSE so we can confirm a parallel worker was really launched;
+# without that confirmation the test could silently degrade to a serial vacuum.
+my ($ret, $stdout, $stderr) =
+ $node->psql('postgres', 'VACUUM (VERBOSE, PARALLEL 2) vacparstat_par');
+is($ret, 0, 'parallel VACUUM succeeded');
+like(
+ $stderr,
+ qr/launched [1-9]\d* parallel vacuum worker.* for index vacuuming/,
+ 'parallel index vacuum actually launched a worker');
+
+# --- Serial vacuum (control) -----------------------------------------------
+# The same workload vacuumed without parallelism.
+$setup->('vacparstat_ser');
+$node->safe_psql('postgres', 'VACUUM (PARALLEL 0) vacparstat_ser');
+
+$node->safe_psql('postgres', 'SELECT pg_stat_force_next_flush()');
+
+my $par = $index_stats->('vacparstat_par');
+my $ser = $index_stats->('vacparstat_ser');
+
+# Sanity-check the parallel numbers so the equality assertion below cannot pass
+# just because both paths captured nothing: 50000 removed entries and some WAL.
+like(
+ $par,
+ qr/^tab_i1:50000:\d+:[1-9]\d*,tab_i2:50000:\d+:[1-9]\d*$/,
+ 'parallel index vacuum captured the expected per-index counters');
+
+# The whole point: the parallel path must attribute exactly the same per-index
+# statistics to each index as the leader-only path does.
+is($par, $ser,
+ 'parallel and serial index vacuum capture identical per-index statistics');
+
+$node->stop;
+done_testing();
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index a2b0472a2d..9b35f0779e 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -2421,18 +2421,29 @@ pg_stat_user_tables| SELECT relid,
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_vacuum_database| SELECT d.oid AS dboid,
+ d.datname AS dbname,
+ s.errors,
+ s.wal_records,
+ s.wal_fpi,
+ s.wal_bytes
+ FROM pg_database d,
+ LATERAL pg_stat_get_vacuum_database(d.oid) s(dboid, errors, wal_records, wal_fpi, wal_bytes);
pg_stat_vacuum_indexes| SELECT c.oid AS relid,
i.oid AS indexrelid,
n.nspname AS schemaname,
c.relname,
i.relname AS indexrelname,
s.pages_deleted,
- s.tuples_deleted
+ s.tuples_deleted,
+ s.wal_records,
+ s.wal_fpi,
+ s.wal_bytes
FROM (((pg_class c
JOIN pg_index x ON ((c.oid = x.indrelid)))
JOIN pg_class i ON ((i.oid = x.indexrelid)))
LEFT JOIN pg_namespace n ON ((n.oid = c.relnamespace))),
- LATERAL pg_stat_get_vacuum_indexes(i.oid) s(relid, pages_deleted, tuples_deleted)
+ LATERAL pg_stat_get_vacuum_indexes(i.oid) s(relid, pages_deleted, tuples_deleted, wal_records, wal_fpi, wal_bytes)
WHERE (c.relkind = ANY (ARRAY['r'::"char", 't'::"char", 'm'::"char"]));
pg_stat_vacuum_tables| SELECT n.nspname AS schemaname,
c.relname,
@@ -2443,10 +2454,13 @@ pg_stat_vacuum_tables| SELECT n.nspname AS schemaname,
s.tuples_frozen,
s.recently_dead_tuples,
s.missed_dead_pages,
- s.missed_dead_tuples
+ s.missed_dead_tuples,
+ s.wal_records,
+ s.wal_fpi,
+ s.wal_bytes
FROM (pg_class c
JOIN pg_namespace n ON ((n.oid = c.relnamespace))),
- LATERAL pg_stat_get_vacuum_tables(c.oid) s(relid, pages_scanned, pages_removed, tuples_deleted, tuples_frozen, recently_dead_tuples, missed_dead_pages, missed_dead_tuples)
+ LATERAL pg_stat_get_vacuum_tables(c.oid) s(relid, pages_scanned, pages_removed, tuples_deleted, tuples_frozen, recently_dead_tuples, missed_dead_pages, missed_dead_tuples, wal_records, wal_fpi, wal_bytes)
WHERE (c.relkind = ANY (ARRAY['r'::"char", 't'::"char", 'm'::"char"]));
pg_stat_wal| SELECT wal_records,
wal_fpi,
diff --git a/src/test/regress/expected/vacuum_stats.out b/src/test/regress/expected/vacuum_stats.out
index cb11d6381c..64639c75dc 100644
--- a/src/test/regress/expected/vacuum_stats.out
+++ b/src/test/regress/expected/vacuum_stats.out
@@ -84,16 +84,57 @@ SELECT recently_dead_tuples = 0 AS recently_dead_tuples,
t | t | t
(1 row)
+-- WAL metrics. A vacuum that removes tuples always emits WAL
+-- (wal_records > 0, wal_bytes > 0). wal_fpi depends on whether a checkpoint
+-- happened recently, so it is only checked for being non-negative here; the
+-- positive wal_fpi path is exercised separately below.
+SELECT wal_records > 0 AS wal_records,
+ wal_fpi >= 0 AS wal_fpi,
+ wal_bytes > 0 AS wal_bytes
+ FROM pg_stat_vacuum_tables WHERE relname = 'vacstat_t';
+ wal_records | wal_fpi | wal_bytes
+-------------+---------+-----------
+ t | t | t
+(1 row)
+
+-- WAL full-page-image path: a CHECKPOINT immediately before the vacuum forces
+-- the first modification of each page to emit a full page image, so wal_fpi
+-- advances.
+CREATE TABLE vacstat_fpi (id int PRIMARY KEY, v text)
+ WITH (autovacuum_enabled = off);
+INSERT INTO vacstat_fpi SELECT g, repeat('x', 20) FROM generate_series(1, 1000) g;
+DELETE FROM vacstat_fpi WHERE id % 2 = 0;
+CHECKPOINT;
+VACUUM vacstat_fpi;
+SELECT pg_stat_force_next_flush();
+ pg_stat_force_next_flush
+--------------------------
+
+(1 row)
+
+SELECT wal_records > 0 AS wal_records,
+ wal_fpi > 0 AS wal_fpi,
+ wal_bytes > 0 AS wal_bytes
+ FROM pg_stat_vacuum_tables WHERE relname = 'vacstat_fpi';
+ wal_records | wal_fpi | wal_bytes
+-------------+---------+-----------
+ t | t | t
+(1 row)
+
+DROP TABLE vacstat_fpi;
-- per-index view: the primary key index is processed by the same VACUUM.
-- No btree leaf empties out (interleaved deletions), so pages_deleted = 0,
-- while every index entry for a removed heap tuple is deleted.
SELECT indexrelname,
pages_deleted = 0 AS pages_deleted,
- tuples_deleted = 500 AS tuples_deleted
+ tuples_deleted = 500 AS tuples_deleted,
+ wal_records > 0 AS wal_records,
+ wal_fpi >= 0 AS wal_fpi,
+ wal_bytes > 0 AS wal_bytes
FROM pg_stat_vacuum_indexes WHERE relname = 'vacstat_t' ORDER BY indexrelname;
- indexrelname | pages_deleted | tuples_deleted
-----------------+---------------+----------------
- vacstat_t_pkey | t | t
+ indexrelname | pages_deleted | tuples_deleted | wal_records | wal_fpi | wal_bytes
+----------------+---------------+----------------+-------------+---------+-----------
+ vacstat_t_pkey | t | t | t | t | t
(1 row)
-- index page-deletion path: deleting a contiguous key range empties whole
@@ -127,3 +168,43 @@ SELECT indexrelname,
(1 row)
DROP TABLE vacstat_idxdel;
+-- per-database aggregate view: no vacuum errors occurred in this database, and
+-- the vacuums in this database emit WAL (wal_records > 0).
+SELECT errors = 0 AS errors,
+ wal_records > 0 AS wal_records,
+ wal_fpi >= 0 AS wal_fpi,
+ wal_bytes > 0 AS wal_bytes
+ FROM pg_stat_vacuum_database WHERE dbname = current_database();
+ errors | wal_records | wal_fpi | wal_bytes
+--------+-------------+---------+-----------
+ t | t | t | t
+(1 row)
+
+-- parallel index vacuum: index statistics must be captured for indexes
+-- vacuumed through the parallel path, not folded into the heap. Force the
+-- parallel path with min_parallel_index_scan_size = 0.
+SET min_parallel_index_scan_size = 0;
+CREATE TABLE vacstat_par (id int, x int) WITH (autovacuum_enabled = off);
+INSERT INTO vacstat_par SELECT g, g FROM generate_series(1, 50000) g;
+CREATE INDEX vacstat_par_i1 ON vacstat_par (id);
+CREATE INDEX vacstat_par_i2 ON vacstat_par (x);
+DELETE FROM vacstat_par WHERE id % 2 = 0;
+VACUUM (PARALLEL 2) vacstat_par;
+SELECT pg_stat_force_next_flush();
+ pg_stat_force_next_flush
+--------------------------
+
+(1 row)
+
+SELECT indexrelname,
+ tuples_deleted = 25000 AS tuples_deleted,
+ wal_records > 0 AS wal_records
+ FROM pg_stat_vacuum_indexes WHERE relname = 'vacstat_par' ORDER BY indexrelname;
+ indexrelname | tuples_deleted | wal_records
+----------------+----------------+-------------
+ vacstat_par_i1 | t | t
+ vacstat_par_i2 | t | t
+(2 rows)
+
+RESET min_parallel_index_scan_size;
+DROP TABLE vacstat_par;
diff --git a/src/test/regress/sql/vacuum_stats.sql b/src/test/regress/sql/vacuum_stats.sql
index 75a5a0052b..13491b87f0 100644
--- a/src/test/regress/sql/vacuum_stats.sql
+++ b/src/test/regress/sql/vacuum_stats.sql
@@ -55,12 +55,40 @@ SELECT recently_dead_tuples = 0 AS recently_dead_tuples,
missed_dead_tuples = 0 AS missed_dead_tuples
FROM pg_stat_vacuum_tables WHERE relname = 'vacstat_t';
+-- WAL metrics. A vacuum that removes tuples always emits WAL
+-- (wal_records > 0, wal_bytes > 0). wal_fpi depends on whether a checkpoint
+-- happened recently, so it is only checked for being non-negative here; the
+-- positive wal_fpi path is exercised separately below.
+SELECT wal_records > 0 AS wal_records,
+ wal_fpi >= 0 AS wal_fpi,
+ wal_bytes > 0 AS wal_bytes
+ FROM pg_stat_vacuum_tables WHERE relname = 'vacstat_t';
+
+-- WAL full-page-image path: a CHECKPOINT immediately before the vacuum forces
+-- the first modification of each page to emit a full page image, so wal_fpi
+-- advances.
+CREATE TABLE vacstat_fpi (id int PRIMARY KEY, v text)
+ WITH (autovacuum_enabled = off);
+INSERT INTO vacstat_fpi SELECT g, repeat('x', 20) FROM generate_series(1, 1000) g;
+DELETE FROM vacstat_fpi WHERE id % 2 = 0;
+CHECKPOINT;
+VACUUM vacstat_fpi;
+SELECT pg_stat_force_next_flush();
+SELECT wal_records > 0 AS wal_records,
+ wal_fpi > 0 AS wal_fpi,
+ wal_bytes > 0 AS wal_bytes
+ FROM pg_stat_vacuum_tables WHERE relname = 'vacstat_fpi';
+DROP TABLE vacstat_fpi;
+
-- per-index view: the primary key index is processed by the same VACUUM.
-- No btree leaf empties out (interleaved deletions), so pages_deleted = 0,
-- while every index entry for a removed heap tuple is deleted.
SELECT indexrelname,
pages_deleted = 0 AS pages_deleted,
- tuples_deleted = 500 AS tuples_deleted
+ tuples_deleted = 500 AS tuples_deleted,
+ wal_records > 0 AS wal_records,
+ wal_fpi >= 0 AS wal_fpi,
+ wal_bytes > 0 AS wal_bytes
FROM pg_stat_vacuum_indexes WHERE relname = 'vacstat_t' ORDER BY indexrelname;
-- index page-deletion path: deleting a contiguous key range empties whole
@@ -79,3 +107,29 @@ SELECT indexrelname,
tuples_deleted = 9000 AS tuples_deleted
FROM pg_stat_vacuum_indexes WHERE relname = 'vacstat_idxdel' ORDER BY indexrelname;
DROP TABLE vacstat_idxdel;
+
+-- per-database aggregate view: no vacuum errors occurred in this database, and
+-- the vacuums in this database emit WAL (wal_records > 0).
+SELECT errors = 0 AS errors,
+ wal_records > 0 AS wal_records,
+ wal_fpi >= 0 AS wal_fpi,
+ wal_bytes > 0 AS wal_bytes
+ FROM pg_stat_vacuum_database WHERE dbname = current_database();
+
+-- parallel index vacuum: index statistics must be captured for indexes
+-- vacuumed through the parallel path, not folded into the heap. Force the
+-- parallel path with min_parallel_index_scan_size = 0.
+SET min_parallel_index_scan_size = 0;
+CREATE TABLE vacstat_par (id int, x int) WITH (autovacuum_enabled = off);
+INSERT INTO vacstat_par SELECT g, g FROM generate_series(1, 50000) g;
+CREATE INDEX vacstat_par_i1 ON vacstat_par (id);
+CREATE INDEX vacstat_par_i2 ON vacstat_par (x);
+DELETE FROM vacstat_par WHERE id % 2 = 0;
+VACUUM (PARALLEL 2) vacstat_par;
+SELECT pg_stat_force_next_flush();
+SELECT indexrelname,
+ tuples_deleted = 25000 AS tuples_deleted,
+ wal_records > 0 AS wal_records
+ FROM pg_stat_vacuum_indexes WHERE relname = 'vacstat_par' ORDER BY indexrelname;
+RESET min_parallel_index_scan_size;
+DROP TABLE vacstat_par;
--
2.39.5 (Apple Git-154)
[text/plain] 0005-Extended-vacuum-statistics-interrupted-vacuums-and-t.patch (27.5K, 7-0005-Extended-vacuum-statistics-interrupted-vacuums-and-t.patch)
download | inline diff:
From 71989a7bdc2f330f9d6f8aff07a4fba216df9005 Mon Sep 17 00:00:00 2001
From: Alena Rybakina <[email protected]>
Date: Wed, 17 Jun 2026 21:32:19 +0300
Subject: [PATCH 5/9] Extended vacuum statistics: interrupted vacuums and the
wraparound failsafe
Expose two database-/relation-level reliability counters with documentation
and regression coverage:
wraparound_failsafe whether (per relation) or how many times (per
database) a vacuum engaged the wraparound failsafe
interrupts_count number of times a vacuum of a table in the database
was interrupted by an error (database aggregate only)
The wraparound failsafe is recorded as a per-relation flag and summed into
a count at the database level. interrupts_count is reported from the vacuum
error callback straight to shared memory, because an interrupted vacuum
aborts its transaction and a pending entry might never be flushed; it is a
database-wide counter only. The positive wraparound_failsafe path is
covered by a TAP test under src/test/modules/xid_wraparound and the
interrupted-vacuum path by a TAP test under src/test/modules/test_misc.
---
doc/src/sgml/system-views.sgml | 25 +++++++
src/backend/access/heap/vacuumlazy.c | 20 ++++--
src/backend/catalog/system_views.sql | 5 +-
src/backend/utils/activity/pgstat_vacuum.c | 46 ++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 8 ++-
src/backend/utils/error/elog.c | 17 +++++
src/include/catalog/pg_proc.dat | 12 ++--
src/include/pgstat.h | 7 ++
src/include/utils/elog.h | 1 +
src/test/modules/test_misc/meson.build | 1 +
.../t/015_vacuum_stats_interrupts.pl | 71 +++++++++++++++++++
src/test/modules/xid_wraparound/meson.build | 1 +
.../t/005_vacuum_stats_failsafe.pl | 66 +++++++++++++++++
src/test/regress/expected/rules.out | 9 ++-
src/test/regress/expected/vacuum_stats.out | 18 ++++-
src/test/regress/sql/vacuum_stats.sql | 8 +++
16 files changed, 295 insertions(+), 20 deletions(-)
create mode 100644 src/test/modules/test_misc/t/015_vacuum_stats_interrupts.pl
create mode 100644 src/test/modules/xid_wraparound/t/005_vacuum_stats_failsafe.pl
diff --git a/doc/src/sgml/system-views.sgml b/doc/src/sgml/system-views.sgml
index 7c95dcea6b..8bca17f3ef 100644
--- a/doc/src/sgml/system-views.sgml
+++ b/doc/src/sgml/system-views.sgml
@@ -5892,6 +5892,14 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx
Number of dead tuples that the vacuum could not remove because it failed to acquire a cleanup lock on their page.
</para></entry>
</row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>wraparound_failsafe</structfield> <type>integer</type>
+ </para>
+ <para>
+ Number of times the failsafe mechanism was triggered to prevent transaction ID wraparound during the vacuum.
+ </para></entry>
+ </row>
<row>
<entry role="catalog_table_entry"><para role="column_definition">
<structfield>wal_records</structfield> <type>bigint</type>
@@ -6089,6 +6097,14 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx
Number of vacuum operations in this database that failed with an error.
</para></entry>
</row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>wraparound_failsafe</structfield> <type>integer</type>
+ </para>
+ <para>
+ Number of vacuum operations in this database that engaged the wraparound failsafe mechanism.
+ </para></entry>
+ </row>
<row>
<entry role="catalog_table_entry"><para role="column_definition">
<structfield>wal_records</structfield> <type>bigint</type>
@@ -6113,6 +6129,15 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx
Total amount of WAL generated by vacuum operations in this database, in bytes.
</para></entry>
</row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>interrupts_count</structfield> <type>integer</type>
+ </para>
+ <para>
+ Number of times a vacuum of a table in this database was interrupted by
+ an error.
+ </para></entry>
+ </row>
</tbody>
</tgroup>
</table>
diff --git a/src/backend/access/heap/vacuumlazy.c b/src/backend/access/heap/vacuumlazy.c
index c06c538f57..5089656c18 100644
--- a/src/backend/access/heap/vacuumlazy.c
+++ b/src/backend/access/heap/vacuumlazy.c
@@ -412,9 +412,8 @@ typedef struct LVRelState
*/
BlockNumber eager_scan_remaining_fails;
- int32 wraparound_failsafe_count; /* number of emergency vacuums to
- * prevent anti-wraparound
- * shutdown */
+ bool wraparound_failsafe; /* did this vacuum engage the
+ * wraparound failsafe? */
PgStat_VacuumRelationCounts extVacReportIdx;
@@ -630,6 +629,8 @@ accumulate_heap_vacuum_statistics(LVRelState *vacrel, PgStat_VacuumRelationCount
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->common.wraparound_failsafe_count = vacrel->wraparound_failsafe;
+
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;
@@ -1030,7 +1031,7 @@ heap_vacuum_rel(Relation rel, const VacuumParams *params,
vacrel->aggressive = vacuum_get_cutoffs(rel, params, &vacrel->cutoffs);
vacrel->rel_pages = orig_rel_pages = RelationGetNumberOfBlocks(rel);
vacrel->vistest = GlobalVisTestFor(rel);
- vacrel->wraparound_failsafe_count = 0;
+ vacrel->wraparound_failsafe = false;
/* Initialize state used to track oldest extant XID/MXID */
vacrel->NewRelfrozenXid = vacrel->cutoffs.OldestXmin;
@@ -3160,7 +3161,7 @@ lazy_check_wraparound_failsafe(LVRelState *vacrel)
int64 progress_val[3] = {0, 0, PROGRESS_VACUUM_MODE_FAILSAFE};
VacuumFailsafeActive = true;
- vacrel->wraparound_failsafe_count++;
+ vacrel->wraparound_failsafe = true;
/*
* Abandon use of a buffer access strategy to allow use of all of
@@ -4113,6 +4114,15 @@ vacuum_error_callback(void *arg)
{
LVRelState *errinfo = arg;
+ /*
+ * If an actual ERROR (not a lower-severity report that merely carries this
+ * vacuum error context) is being raised while we have a relation in hand,
+ * record at the database level that a vacuum was interrupted. Any error
+ * here aborts the vacuum, so the exact phase does not matter.
+ */
+ if (errinfo->rel != NULL && geterrlevel() == ERROR)
+ pgstat_report_vacuum_error(errinfo->rel->rd_rel->relisshared);
+
switch (errinfo->phase)
{
case VACUUM_ERRCB_PHASE_SCAN_HEAP:
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index 47d6aa6aea..837e78d292 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -1573,6 +1573,7 @@ CREATE VIEW pg_stat_vacuum_tables AS
S.recently_dead_tuples AS recently_dead_tuples,
S.missed_dead_pages AS missed_dead_pages,
S.missed_dead_tuples AS missed_dead_tuples,
+ S.wraparound_failsafe AS wraparound_failsafe,
S.wal_records AS wal_records,
S.wal_fpi AS wal_fpi,
S.wal_bytes AS wal_bytes
@@ -1611,9 +1612,11 @@ CREATE VIEW pg_stat_vacuum_database AS
S.errors AS errors,
+ S.wraparound_failsafe AS wraparound_failsafe,
S.wal_records AS wal_records,
S.wal_fpi AS wal_fpi,
- S.wal_bytes AS wal_bytes
+ S.wal_bytes AS wal_bytes,
+ S.interrupts_count AS interrupts_count
FROM
pg_database D,
LATERAL pg_stat_get_vacuum_database(D.oid) S;
diff --git a/src/backend/utils/activity/pgstat_vacuum.c b/src/backend/utils/activity/pgstat_vacuum.c
index 99c4932ded..8e099f3ade 100644
--- a/src/backend/utils/activity/pgstat_vacuum.c
+++ b/src/backend/utils/activity/pgstat_vacuum.c
@@ -43,6 +43,7 @@ pgstat_accumulate_common(PgStat_CommonCounts *dst, const PgStat_CommonCounts *sr
ACCUMULATE_FIELD(wal_bytes);
ACCUMULATE_FIELD(tuples_deleted);
+ ACCUMULATE_FIELD(interrupts_count);
}
/*
@@ -64,6 +65,13 @@ pgstat_accumulate_extvac_stats_relations(PgStat_VacuumRelationCounts *dst,
pgstat_accumulate_common(&dst->common, &src->common);
+ /*
+ * The wraparound failsafe is a per-relation flag (0/1), not a running
+ * count: reflect whether the latest vacuum of this relation engaged it,
+ * rather than summing across vacuums.
+ */
+ dst->common.wraparound_failsafe_count = src->common.wraparound_failsafe_count;
+
if (dst->type == PGSTAT_EXTVAC_TABLE)
{
ACCUMULATE_SUBFIELD(table, pages_scanned);
@@ -90,6 +98,12 @@ pgstat_accumulate_extvac_stats_db(PgStat_VacuumDBCounts *dst,
return;
pgstat_accumulate_common(&dst->common, &src->common);
+
+ /*
+ * At the database level the failsafe is a count: how many relation vacuums
+ * engaged the wraparound failsafe.
+ */
+ dst->common.wraparound_failsafe_count += src->common.wraparound_failsafe_count;
dst->errors += src->errors;
}
@@ -126,6 +140,38 @@ pgstat_report_vacuum_extstats(Oid tableoid, bool shared,
dboid, InvalidOid, NULL);
dbpending = (PgStat_VacuumDBCounts *) entry_ref->pending;
pgstat_accumulate_common(&dbpending->common, ¶ms->common);
+ /* count this relation's failsafe flag into the database-wide total */
+ dbpending->common.wraparound_failsafe_count += params->common.wraparound_failsafe_count;
+}
+
+/*
+ * Report that a vacuum was interrupted by an error.
+ *
+ * This is a database-wide counter only: an interrupted vacuum aborts its
+ * transaction, so reporting per-relation would require creating a relation
+ * stats entry from the error path (which the aborting transaction may roll
+ * back). Called from the vacuum error callback, we therefore update shared
+ * memory directly rather than going through pending entries, which might never
+ * be flushed.
+ *
+ * The database id is InvalidOid for shared relations, just as in
+ * pgstat_report_vacuum_extstats(); it must not be hard-coded to MyDatabaseId.
+ */
+void
+pgstat_report_vacuum_error(bool shared)
+{
+ PgStat_EntryRef *entry_ref;
+ PgStatShared_VacuumDB *shdbentry;
+ Oid dboid = (shared ? InvalidOid : MyDatabaseId);
+
+ if (!pgstat_track_vacuum_statistics)
+ return;
+
+ entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_VACUUM_DB,
+ dboid, InvalidOid, false);
+ shdbentry = (PgStatShared_VacuumDB *) entry_ref->shared_stats;
+ shdbentry->stats.common.interrupts_count++;
+ pgstat_unlock_entry(entry_ref);
}
/*
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index aad4427469..7927668bf8 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -2374,7 +2374,7 @@ pg_stat_have_stats(PG_FUNCTION_ARGS)
Datum
pg_stat_get_vacuum_tables(PG_FUNCTION_ARGS)
{
-#define PG_STAT_GET_VACUUM_TABLES_STATS_COLS 11
+#define PG_STAT_GET_VACUUM_TABLES_STATS_COLS 12
Oid relid = PG_GETARG_OID(0);
PgStat_VacuumRelationCounts *extvacuum;
@@ -2408,6 +2408,7 @@ pg_stat_get_vacuum_tables(PG_FUNCTION_ARGS)
values[i++] = Int64GetDatum(extvacuum->table.recently_dead_tuples);
values[i++] = Int64GetDatum(extvacuum->table.missed_dead_pages);
values[i++] = Int64GetDatum(extvacuum->table.missed_dead_tuples);
+ values[i++] = Int32GetDatum(extvacuum->common.wraparound_failsafe_count);
values[i++] = Int64GetDatum(extvacuum->common.wal_records);
values[i++] = Int64GetDatum(extvacuum->common.wal_fpi);
snprintf(buf, sizeof buf, UINT64_FORMAT, extvacuum->common.wal_bytes);
@@ -2476,7 +2477,7 @@ pg_stat_get_vacuum_indexes(PG_FUNCTION_ARGS)
Datum
pg_stat_get_vacuum_database(PG_FUNCTION_ARGS)
{
-#define PG_STAT_GET_VACUUM_DATABASE_STATS_COLS 5
+#define PG_STAT_GET_VACUUM_DATABASE_STATS_COLS 7
Oid dbid = PG_GETARG_OID(0);
PgStat_VacuumDBCounts *extvacuum;
@@ -2500,6 +2501,8 @@ pg_stat_get_vacuum_database(PG_FUNCTION_ARGS)
values[i++] = Int32GetDatum(extvacuum->errors);
+ values[i++] = Int32GetDatum(extvacuum->common.wraparound_failsafe_count);
+
values[i++] = Int64GetDatum(extvacuum->common.wal_records);
values[i++] = Int64GetDatum(extvacuum->common.wal_fpi);
snprintf(buf, sizeof buf, UINT64_FORMAT, extvacuum->common.wal_bytes);
@@ -2507,6 +2510,7 @@ pg_stat_get_vacuum_database(PG_FUNCTION_ARGS)
CStringGetDatum(buf),
ObjectIdGetDatum(0),
Int32GetDatum(-1));
+ values[i++] = Int32GetDatum(extvacuum->common.interrupts_count);
Assert(i == PG_STAT_GET_VACUUM_DATABASE_STATS_COLS);
/* Returns the record as Datum */
diff --git a/src/backend/utils/error/elog.c b/src/backend/utils/error/elog.c
index a6936a0c66..04f5b0a819 100644
--- a/src/backend/utils/error/elog.c
+++ b/src/backend/utils/error/elog.c
@@ -1815,6 +1815,23 @@ getinternalerrposition(void)
return edata->internalpos;
}
+/*
+ * geterrlevel --- return the elevel of the error currently being constructed
+ *
+ * This is only intended for use in error callback subroutines, where it lets
+ * a callback tell a genuine error apart from a lower-severity report.
+ */
+int
+geterrlevel(void)
+{
+ ErrorData *edata = &errordata[errordata_stack_depth];
+
+ /* we don't bother incrementing recursion_depth */
+ CHECK_STACK_DEPTH();
+
+ return edata->elevel;
+}
+
/*
* Functions to allow construction of error message strings separately from
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3dd3d81a13..51cd0e5ed1 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12643,9 +12643,9 @@
proname => 'pg_stat_get_vacuum_tables', prorows => 1000, provolatile => 's', prorettype => 'record', proisstrict => 'f',
proretset => 't',
proargtypes => 'oid',
- proallargtypes => '{oid,oid,int8,int8,int8,int8,int8,int8,int8,int8,int8,numeric}',
- proargmodes => '{i,o,o,o,o,o,o,o,o,o,o,o}',
- proargnames => '{reloid,relid,pages_scanned,pages_removed,tuples_deleted,tuples_frozen,recently_dead_tuples,missed_dead_pages,missed_dead_tuples,wal_records,wal_fpi,wal_bytes}',
+ proallargtypes => '{oid,oid,int8,int8,int8,int8,int8,int8,int8,int4,int8,int8,numeric}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o,o,o,o,o}',
+ proargnames => '{reloid,relid,pages_scanned,pages_removed,tuples_deleted,tuples_frozen,recently_dead_tuples,missed_dead_pages,missed_dead_tuples,wraparound_failsafe,wal_records,wal_fpi,wal_bytes}',
prosrc => 'pg_stat_get_vacuum_tables' }
# oid8 related functions
@@ -12727,8 +12727,8 @@
proname => 'pg_stat_get_vacuum_database', prorows => 1000, provolatile => 's', prorettype => 'record', proisstrict => 'f',
proretset => 't',
proargtypes => 'oid',
- proallargtypes => '{oid,oid,int4,int8,int8,numeric}',
- proargmodes => '{i,o,o,o,o,o}',
- proargnames => '{dbid,dboid,errors,wal_records,wal_fpi,wal_bytes}',
+ proallargtypes => '{oid,oid,int4,int4,int8,int8,numeric,int4}',
+ proargmodes => '{i,o,o,o,o,o,o,o}',
+ proargnames => '{dbid,dboid,errors,wraparound_failsafe,wal_records,wal_fpi,wal_bytes,interrupts_count}',
prosrc => 'pg_stat_get_vacuum_database' },
]
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index f704a15003..d041359e72 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -182,6 +182,12 @@ typedef struct PgStat_CommonCounts
/* tuples */
int64 tuples_deleted;
+
+ /* failsafe */
+ int32 wraparound_failsafe_count;
+
+ /* number of times a vacuum of the object was interrupted by an error */
+ int32 interrupts_count;
} PgStat_CommonCounts;
/* ----------
@@ -934,6 +940,7 @@ extern void pgstat_vacuum_relation_delete_pending_cb(Oid relid);
extern void
pgstat_report_vacuum_extstats(Oid tableoid, bool shared,
PgStat_VacuumRelationCounts * params);
+extern void pgstat_report_vacuum_error(bool shared);
extern PgStat_VacuumRelationCounts * pgstat_fetch_stat_vacuum_tabentry(Oid relid, Oid dbid);
extern PgStat_VacuumDBCounts * pgstat_fetch_stat_vacuum_dbentry(Oid dbid);
diff --git a/src/include/utils/elog.h b/src/include/utils/elog.h
index 6ae376ba00..7c1369524e 100644
--- a/src/include/utils/elog.h
+++ b/src/include/utils/elog.h
@@ -230,6 +230,7 @@ extern int internalerrquery(const char *query);
extern int err_generic_string(int field, const char *str);
extern int geterrcode(void);
+extern int geterrlevel(void);
extern int geterrposition(void);
extern int getinternalerrposition(void);
diff --git a/src/test/modules/test_misc/meson.build b/src/test/modules/test_misc/meson.build
index 805c6c2c39..8761d79d70 100644
--- a/src/test/modules/test_misc/meson.build
+++ b/src/test/modules/test_misc/meson.build
@@ -23,6 +23,7 @@ tests += {
't/012_ddlutils.pl',
't/013_temp_obj_multisession.pl',
't/014_vacuum_stats.pl',
+ 't/015_vacuum_stats_interrupts.pl',
't/016_vacuum_stats_parallel.pl',
],
# The injection points are cluster-wide, so disable installcheck
diff --git a/src/test/modules/test_misc/t/015_vacuum_stats_interrupts.pl b/src/test/modules/test_misc/t/015_vacuum_stats_interrupts.pl
new file mode 100644
index 0000000000..80dd2b7b3c
--- /dev/null
+++ b/src/test/modules/test_misc/t/015_vacuum_stats_interrupts.pl
@@ -0,0 +1,71 @@
+# Copyright (c) 2024-2026, PostgreSQL Global Development Group
+
+# Test the interrupts_count counter of pg_stat_vacuum_database.
+#
+# interrupts_count records how many times a vacuum in the database was
+# interrupted by an error. We provoke that by starting a vacuum that sleeps at
+# its cost-based delay points and canceling it, with pg_cancel_backend(), while
+# it is still running.
+
+use strict;
+use warnings FATAL => 'all';
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+my $node = PostgreSQL::Test::Cluster->new('main');
+$node->init;
+$node->append_conf(
+ 'postgresql.conf', qq[
+autovacuum = off
+track_vacuum_statistics = on
+]);
+$node->start;
+
+# fillfactor = 10 spreads the rows over many pages so the vacuum hits enough
+# cost-delay points to stay running until we cancel it.
+$node->safe_psql(
+ 'postgres', qq[
+CREATE TABLE vacstat_int (id int) WITH (autovacuum_enabled = off, fillfactor = 10);
+INSERT INTO vacstat_int SELECT generate_series(1, 1000);
+DELETE FROM vacstat_int;
+]);
+
+# Start a vacuum that sleeps at every cost-delay point, in the background. The
+# \echo lets query_until() return as soon as the VACUUM has been launched.
+my $appname = 'vacuum_interrupt_test';
+my $vac = $node->background_psql('postgres', on_error_stop => 0);
+$vac->query_until(
+ qr/start/, qq[
+SET application_name = '$appname';
+SET vacuum_cost_delay = '100ms';
+SET vacuum_cost_limit = 1;
+\\echo start
+VACUUM vacstat_int;
+]);
+
+# Wait until the vacuum is actually running, then cancel it.
+$node->poll_query_until(
+ 'postgres', qq[
+SELECT count(*) = 1 FROM pg_stat_activity
+ WHERE application_name = '$appname' AND query LIKE 'VACUUM%' AND state = 'active'])
+ or die "timed out waiting for the vacuum to start";
+
+my $cancelled = $node->safe_psql(
+ 'postgres', qq[
+SELECT pg_cancel_backend(pid) FROM pg_stat_activity
+ WHERE application_name = '$appname' AND query LIKE 'VACUUM%']);
+is($cancelled, 't', 'canceled the running vacuum');
+
+$vac->quit;
+like($vac->{stderr}, qr/canceling statement due to user request/,
+ 'vacuum canceled by user request');
+
+is( $node->safe_psql(
+ 'postgres', qq[
+SELECT interrupts_count > 0 FROM pg_stat_vacuum_database WHERE dbname = current_database()]),
+ 't',
+ 'interrupts_count advanced in pg_stat_vacuum_database');
+
+$node->stop;
+done_testing();
diff --git a/src/test/modules/xid_wraparound/meson.build b/src/test/modules/xid_wraparound/meson.build
index 97ce670f9a..9224e59d1d 100644
--- a/src/test/modules/xid_wraparound/meson.build
+++ b/src/test/modules/xid_wraparound/meson.build
@@ -31,6 +31,7 @@ tests += {
't/002_limits.pl',
't/003_wraparounds.pl',
't/004_notify_freeze.pl',
+ 't/005_vacuum_stats_failsafe.pl',
],
},
}
diff --git a/src/test/modules/xid_wraparound/t/005_vacuum_stats_failsafe.pl b/src/test/modules/xid_wraparound/t/005_vacuum_stats_failsafe.pl
new file mode 100644
index 0000000000..eff77fe876
--- /dev/null
+++ b/src/test/modules/xid_wraparound/t/005_vacuum_stats_failsafe.pl
@@ -0,0 +1,66 @@
+# Copyright (c) 2024-2026, PostgreSQL Global Development Group
+
+# Test that the wraparound failsafe counter exposed by the extended vacuum
+# statistics views advances when a VACUUM engages the wraparound failsafe.
+#
+# The failsafe only triggers once a relation's age exceeds
+# max(vacuum_failsafe_age, autovacuum_freeze_max_age * 1.05), which cannot be
+# reached by an ordinary regression test. Here we lower
+# autovacuum_freeze_max_age to its minimum and use the xid_wraparound
+# extension to burn enough transaction IDs to age the table past that
+# threshold, then check that the wraparound_failsafe counters advance.
+
+use strict;
+use warnings FATAL => 'all';
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+if (!$ENV{PG_TEST_EXTRA} || $ENV{PG_TEST_EXTRA} !~ /\bxid_wraparound\b/)
+{
+ plan skip_all => "test xid_wraparound not enabled in PG_TEST_EXTRA";
+}
+
+my $node = PostgreSQL::Test::Cluster->new('main');
+$node->init;
+$node->append_conf(
+ 'postgresql.conf', qq[
+autovacuum = off
+track_vacuum_statistics = on
+# Lower the wraparound-failsafe threshold as far as possible so that a modest
+# number of consumed XIDs is enough to engage the failsafe.
+autovacuum_freeze_max_age = 100000
+vacuum_failsafe_age = 0
+]);
+$node->start;
+$node->safe_psql('postgres', 'CREATE EXTENSION xid_wraparound');
+
+# A table whose relfrozenxid age we will push past the failsafe threshold.
+$node->safe_psql(
+ 'postgres', qq[
+CREATE TABLE fs_tab (id int) WITH (autovacuum_enabled = off);
+INSERT INTO fs_tab SELECT generate_series(1, 1000);
+]);
+
+# Advance the XID counter well past the failsafe threshold.
+$node->safe_psql('postgres', 'SELECT consume_xids(200000)');
+
+# This VACUUM must engage the wraparound failsafe.
+$node->safe_psql('postgres', 'VACUUM fs_tab');
+
+# The per-table view records that the failsafe was engaged for this relation.
+my $tab = $node->safe_psql(
+ 'postgres', qq[
+SELECT wraparound_failsafe > 0
+ FROM pg_stat_vacuum_tables WHERE relname = 'fs_tab']);
+is($tab, 't', 'wraparound_failsafe advanced in pg_stat_vacuum_tables');
+
+# The per-database aggregate counts the failsafe as well.
+my $db = $node->safe_psql(
+ 'postgres', qq[
+SELECT wraparound_failsafe > 0
+ FROM pg_stat_vacuum_database WHERE dbname = current_database()]);
+is($db, 't', 'wraparound_failsafe advanced in pg_stat_vacuum_database');
+
+$node->stop;
+done_testing();
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 9b35f0779e..467c2e1843 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -2424,11 +2424,13 @@ pg_stat_user_tables| SELECT relid,
pg_stat_vacuum_database| SELECT d.oid AS dboid,
d.datname AS dbname,
s.errors,
+ s.wraparound_failsafe,
s.wal_records,
s.wal_fpi,
- s.wal_bytes
+ s.wal_bytes,
+ s.interrupts_count
FROM pg_database d,
- LATERAL pg_stat_get_vacuum_database(d.oid) s(dboid, errors, wal_records, wal_fpi, wal_bytes);
+ LATERAL pg_stat_get_vacuum_database(d.oid) s(dboid, errors, wraparound_failsafe, wal_records, wal_fpi, wal_bytes, interrupts_count);
pg_stat_vacuum_indexes| SELECT c.oid AS relid,
i.oid AS indexrelid,
n.nspname AS schemaname,
@@ -2455,12 +2457,13 @@ pg_stat_vacuum_tables| SELECT n.nspname AS schemaname,
s.recently_dead_tuples,
s.missed_dead_pages,
s.missed_dead_tuples,
+ s.wraparound_failsafe,
s.wal_records,
s.wal_fpi,
s.wal_bytes
FROM (pg_class c
JOIN pg_namespace n ON ((n.oid = c.relnamespace))),
- LATERAL pg_stat_get_vacuum_tables(c.oid) s(relid, pages_scanned, pages_removed, tuples_deleted, tuples_frozen, recently_dead_tuples, missed_dead_pages, missed_dead_tuples, wal_records, wal_fpi, wal_bytes)
+ LATERAL pg_stat_get_vacuum_tables(c.oid) s(relid, pages_scanned, pages_removed, tuples_deleted, tuples_frozen, recently_dead_tuples, missed_dead_pages, missed_dead_tuples, wraparound_failsafe, wal_records, wal_fpi, wal_bytes)
WHERE (c.relkind = ANY (ARRAY['r'::"char", 't'::"char", 'm'::"char"]));
pg_stat_wal| SELECT wal_records,
wal_fpi,
diff --git a/src/test/regress/expected/vacuum_stats.out b/src/test/regress/expected/vacuum_stats.out
index 64639c75dc..c3024e5fc4 100644
--- a/src/test/regress/expected/vacuum_stats.out
+++ b/src/test/regress/expected/vacuum_stats.out
@@ -122,6 +122,17 @@ SELECT wal_records > 0 AS wal_records,
(1 row)
DROP TABLE vacstat_fpi;
+-- wraparound failsafe. A normal vacuum does not engage the wraparound
+-- failsafe (wraparound_failsafe = 0). The positive path requires reaching the
+-- failsafe XID age and is covered by a separate TAP test under
+-- src/test/modules/xid_wraparound.
+SELECT wraparound_failsafe = 0 AS wraparound_failsafe
+ FROM pg_stat_vacuum_tables WHERE relname = 'vacstat_t';
+ wraparound_failsafe
+---------------------
+ t
+(1 row)
+
-- per-index view: the primary key index is processed by the same VACUUM.
-- No btree leaf empties out (interleaved deletions), so pages_deleted = 0,
-- while every index entry for a removed heap tuple is deleted.
@@ -171,13 +182,14 @@ DROP TABLE vacstat_idxdel;
-- per-database aggregate view: no vacuum errors occurred in this database, and
-- the vacuums in this database emit WAL (wal_records > 0).
SELECT errors = 0 AS errors,
+ wraparound_failsafe = 0 AS wraparound_failsafe,
wal_records > 0 AS wal_records,
wal_fpi >= 0 AS wal_fpi,
wal_bytes > 0 AS wal_bytes
FROM pg_stat_vacuum_database WHERE dbname = current_database();
- errors | wal_records | wal_fpi | wal_bytes
---------+-------------+---------+-----------
- t | t | t | t
+ errors | wraparound_failsafe | wal_records | wal_fpi | wal_bytes
+--------+---------------------+-------------+---------+-----------
+ t | t | t | t | t
(1 row)
-- parallel index vacuum: index statistics must be captured for indexes
diff --git a/src/test/regress/sql/vacuum_stats.sql b/src/test/regress/sql/vacuum_stats.sql
index 13491b87f0..8ce60dac77 100644
--- a/src/test/regress/sql/vacuum_stats.sql
+++ b/src/test/regress/sql/vacuum_stats.sql
@@ -80,6 +80,13 @@ SELECT wal_records > 0 AS wal_records,
FROM pg_stat_vacuum_tables WHERE relname = 'vacstat_fpi';
DROP TABLE vacstat_fpi;
+-- wraparound failsafe. A normal vacuum does not engage the wraparound
+-- failsafe (wraparound_failsafe = 0). The positive path requires reaching the
+-- failsafe XID age and is covered by a separate TAP test under
+-- src/test/modules/xid_wraparound.
+SELECT wraparound_failsafe = 0 AS wraparound_failsafe
+ FROM pg_stat_vacuum_tables WHERE relname = 'vacstat_t';
+
-- per-index view: the primary key index is processed by the same VACUUM.
-- No btree leaf empties out (interleaved deletions), so pages_deleted = 0,
-- while every index entry for a removed heap tuple is deleted.
@@ -111,6 +118,7 @@ DROP TABLE vacstat_idxdel;
-- per-database aggregate view: no vacuum errors occurred in this database, and
-- the vacuums in this database emit WAL (wal_records > 0).
SELECT errors = 0 AS errors,
+ wraparound_failsafe = 0 AS wraparound_failsafe,
wal_records > 0 AS wal_records,
wal_fpi >= 0 AS wal_fpi,
wal_bytes > 0 AS wal_bytes
--
2.39.5 (Apple Git-154)
[text/plain] 0006-Extended-vacuum-statistics-visibility-map-page-trans.patch (13.7K, 8-0006-Extended-vacuum-statistics-visibility-map-page-trans.patch)
download | inline diff:
From 11e2d4eac5d1d8225c23c8640454a66064825ca3 Mon Sep 17 00:00:00 2001
From: Alena Rybakina <[email protected]>
Date: Wed, 17 Jun 2026 21:40:32 +0300
Subject: [PATCH 6/9] Extended vacuum statistics: visibility-map page
transitions for tables
Expose the counters that track how the vacuum updated the visibility map of
the table, with documentation and regression coverage:
vm_new_frozen_pages pages newly marked all-frozen in the VM
vm_new_visible_pages pages newly marked all-visible in the VM
vm_new_visible_frozen_pages pages newly marked all-visible and all-frozen
---
doc/src/sgml/system-views.sgml | 24 +++++++++++++
src/backend/access/heap/vacuumlazy.c | 3 ++
src/backend/catalog/system_views.sql | 3 ++
src/backend/utils/activity/pgstat_vacuum.c | 3 ++
src/backend/utils/adt/pgstatfuncs.c | 5 ++-
src/include/catalog/pg_proc.dat | 6 ++--
src/include/pgstat.h | 7 ++++
src/test/regress/expected/rules.out | 5 ++-
src/test/regress/expected/vacuum_stats.out | 39 ++++++++++++++++++++++
src/test/regress/sql/vacuum_stats.sql | 26 +++++++++++++++
10 files changed, 116 insertions(+), 5 deletions(-)
diff --git a/doc/src/sgml/system-views.sgml b/doc/src/sgml/system-views.sgml
index 8bca17f3ef..1be54673bf 100644
--- a/doc/src/sgml/system-views.sgml
+++ b/doc/src/sgml/system-views.sgml
@@ -5892,6 +5892,30 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx
Number of dead tuples that the vacuum could not remove because it failed to acquire a cleanup lock on their page.
</para></entry>
</row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>vm_new_frozen_pages</structfield> <type>bigint</type>
+ </para>
+ <para>
+ Number of heap pages newly marked all-frozen in the visibility map by the vacuum.
+ </para></entry>
+ </row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>vm_new_visible_pages</structfield> <type>bigint</type>
+ </para>
+ <para>
+ Number of heap pages newly marked all-visible in the visibility map by the vacuum.
+ </para></entry>
+ </row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>vm_new_visible_frozen_pages</structfield> <type>bigint</type>
+ </para>
+ <para>
+ Number of heap pages newly marked both all-visible and all-frozen in the visibility map by the vacuum.
+ </para></entry>
+ </row>
<row>
<entry role="catalog_table_entry"><para role="column_definition">
<structfield>wraparound_failsafe</structfield> <type>integer</type>
diff --git a/src/backend/access/heap/vacuumlazy.c b/src/backend/access/heap/vacuumlazy.c
index 5089656c18..46b7418136 100644
--- a/src/backend/access/heap/vacuumlazy.c
+++ b/src/backend/access/heap/vacuumlazy.c
@@ -624,6 +624,9 @@ accumulate_heap_vacuum_statistics(LVRelState *vacrel, PgStat_VacuumRelationCount
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;
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index 837e78d292..a7ac037bc3 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -1573,6 +1573,9 @@ CREATE VIEW pg_stat_vacuum_tables AS
S.recently_dead_tuples AS recently_dead_tuples,
S.missed_dead_pages AS missed_dead_pages,
S.missed_dead_tuples AS missed_dead_tuples,
+ S.vm_new_frozen_pages AS vm_new_frozen_pages,
+ S.vm_new_visible_pages AS vm_new_visible_pages,
+ S.vm_new_visible_frozen_pages AS vm_new_visible_frozen_pages,
S.wraparound_failsafe AS wraparound_failsafe,
S.wal_records AS wal_records,
S.wal_fpi AS wal_fpi,
diff --git a/src/backend/utils/activity/pgstat_vacuum.c b/src/backend/utils/activity/pgstat_vacuum.c
index 8e099f3ade..ec3d50c59c 100644
--- a/src/backend/utils/activity/pgstat_vacuum.c
+++ b/src/backend/utils/activity/pgstat_vacuum.c
@@ -76,6 +76,9 @@ pgstat_accumulate_extvac_stats_relations(PgStat_VacuumRelationCounts *dst,
{
ACCUMULATE_SUBFIELD(table, pages_scanned);
ACCUMULATE_SUBFIELD(table, pages_removed);
+ ACCUMULATE_SUBFIELD(table, vm_new_frozen_pages);
+ ACCUMULATE_SUBFIELD(table, vm_new_visible_pages);
+ ACCUMULATE_SUBFIELD(table, vm_new_visible_frozen_pages);
ACCUMULATE_SUBFIELD(table, missed_dead_pages);
ACCUMULATE_SUBFIELD(table, tuples_frozen);
ACCUMULATE_SUBFIELD(table, recently_dead_tuples);
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 7927668bf8..694fd220b0 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -2374,7 +2374,7 @@ pg_stat_have_stats(PG_FUNCTION_ARGS)
Datum
pg_stat_get_vacuum_tables(PG_FUNCTION_ARGS)
{
-#define PG_STAT_GET_VACUUM_TABLES_STATS_COLS 12
+#define PG_STAT_GET_VACUUM_TABLES_STATS_COLS 15
Oid relid = PG_GETARG_OID(0);
PgStat_VacuumRelationCounts *extvacuum;
@@ -2408,6 +2408,9 @@ pg_stat_get_vacuum_tables(PG_FUNCTION_ARGS)
values[i++] = Int64GetDatum(extvacuum->table.recently_dead_tuples);
values[i++] = Int64GetDatum(extvacuum->table.missed_dead_pages);
values[i++] = Int64GetDatum(extvacuum->table.missed_dead_tuples);
+ values[i++] = Int64GetDatum(extvacuum->table.vm_new_frozen_pages);
+ values[i++] = Int64GetDatum(extvacuum->table.vm_new_visible_pages);
+ values[i++] = Int64GetDatum(extvacuum->table.vm_new_visible_frozen_pages);
values[i++] = Int32GetDatum(extvacuum->common.wraparound_failsafe_count);
values[i++] = Int64GetDatum(extvacuum->common.wal_records);
values[i++] = Int64GetDatum(extvacuum->common.wal_fpi);
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 51cd0e5ed1..d2fb1b4ec0 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12643,9 +12643,9 @@
proname => 'pg_stat_get_vacuum_tables', prorows => 1000, provolatile => 's', prorettype => 'record', proisstrict => 'f',
proretset => 't',
proargtypes => 'oid',
- proallargtypes => '{oid,oid,int8,int8,int8,int8,int8,int8,int8,int4,int8,int8,numeric}',
- proargmodes => '{i,o,o,o,o,o,o,o,o,o,o,o,o}',
- proargnames => '{reloid,relid,pages_scanned,pages_removed,tuples_deleted,tuples_frozen,recently_dead_tuples,missed_dead_pages,missed_dead_tuples,wraparound_failsafe,wal_records,wal_fpi,wal_bytes}',
+ proallargtypes => '{oid,oid,int8,int8,int8,int8,int8,int8,int8,int8,int8,int8,int4,int8,int8,numeric}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o}',
+ proargnames => '{reloid,relid,pages_scanned,pages_removed,tuples_deleted,tuples_frozen,recently_dead_tuples,missed_dead_pages,missed_dead_tuples,vm_new_frozen_pages,vm_new_visible_pages,vm_new_visible_frozen_pages,wraparound_failsafe,wal_records,wal_fpi,wal_bytes}',
prosrc => 'pg_stat_get_vacuum_tables' }
# oid8 related functions
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index d041359e72..bcc84dbd41 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -233,6 +233,13 @@ typedef struct PgStat_VacuumRelationCounts
int64 missed_dead_tuples; /* tuples not pruned by vacuum due
* to failure to get a cleanup
* lock */
+ int64 vm_new_frozen_pages; /* pages marked in VM as
+ * frozen */
+ int64 vm_new_visible_pages; /* pages marked in VM as
+ * all-visible */
+ int64 vm_new_visible_frozen_pages; /* pages marked in VM as
+ * all-visible and
+ * frozen */
} table;
struct
{
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 467c2e1843..27a17ee0c2 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -2457,13 +2457,16 @@ pg_stat_vacuum_tables| SELECT n.nspname AS schemaname,
s.recently_dead_tuples,
s.missed_dead_pages,
s.missed_dead_tuples,
+ s.vm_new_frozen_pages,
+ s.vm_new_visible_pages,
+ s.vm_new_visible_frozen_pages,
s.wraparound_failsafe,
s.wal_records,
s.wal_fpi,
s.wal_bytes
FROM (pg_class c
JOIN pg_namespace n ON ((n.oid = c.relnamespace))),
- LATERAL pg_stat_get_vacuum_tables(c.oid) s(relid, pages_scanned, pages_removed, tuples_deleted, tuples_frozen, recently_dead_tuples, missed_dead_pages, missed_dead_tuples, wraparound_failsafe, wal_records, wal_fpi, wal_bytes)
+ LATERAL pg_stat_get_vacuum_tables(c.oid) s(relid, pages_scanned, pages_removed, tuples_deleted, tuples_frozen, recently_dead_tuples, missed_dead_pages, missed_dead_tuples, vm_new_frozen_pages, vm_new_visible_pages, vm_new_visible_frozen_pages, wraparound_failsafe, wal_records, wal_fpi, wal_bytes)
WHERE (c.relkind = ANY (ARRAY['r'::"char", 't'::"char", 'm'::"char"]));
pg_stat_wal| SELECT wal_records,
wal_fpi,
diff --git a/src/test/regress/expected/vacuum_stats.out b/src/test/regress/expected/vacuum_stats.out
index c3024e5fc4..c996699804 100644
--- a/src/test/regress/expected/vacuum_stats.out
+++ b/src/test/regress/expected/vacuum_stats.out
@@ -84,6 +84,45 @@ SELECT recently_dead_tuples = 0 AS recently_dead_tuples,
t | t | t
(1 row)
+-- visibility-map page transitions. Removing the interleaved dead tuples lets
+-- VACUUM mark every heap page all-visible (vm_new_visible_pages > 0). Whether
+-- VACUUM also freezes those pages (vm_new_frozen_pages /
+-- vm_new_visible_frozen_pages) depends on opportunistic freezing, which is not
+-- deterministic here, so those are only checked for being non-negative; the
+-- positive freeze path is covered by the dedicated VACUUM (FREEZE) scenario
+-- below.
+SELECT vm_new_frozen_pages >= 0 AS vm_new_frozen_pages,
+ vm_new_visible_pages > 0 AS vm_new_visible_pages,
+ vm_new_visible_frozen_pages >= 0 AS vm_new_visible_frozen_pages
+ FROM pg_stat_vacuum_tables WHERE relname = 'vacstat_t';
+ vm_new_frozen_pages | vm_new_visible_pages | vm_new_visible_frozen_pages
+---------------------+----------------------+-----------------------------
+ t | t | t
+(1 row)
+
+-- freeze path: a dedicated VACUUM (FREEZE) marks freshly-loaded heap pages
+-- all-visible and all-frozen in one pass, so vm_new_visible_frozen_pages
+-- advances. This restores the coverage of the former
+-- 053_vacuum_extending_freeze TAP test.
+CREATE TABLE vacstat_frz (x int)
+ WITH (autovacuum_enabled = off, fillfactor = 10);
+INSERT INTO vacstat_frz SELECT g FROM generate_series(1, 1000) g;
+VACUUM (FREEZE) vacstat_frz;
+SELECT pg_stat_force_next_flush();
+ pg_stat_force_next_flush
+--------------------------
+
+(1 row)
+
+SELECT vm_new_visible_pages > 0 AS vm_new_visible_pages,
+ vm_new_visible_frozen_pages > 0 AS vm_new_visible_frozen_pages
+ FROM pg_stat_vacuum_tables WHERE relname = 'vacstat_frz';
+ vm_new_visible_pages | vm_new_visible_frozen_pages
+----------------------+-----------------------------
+ t | t
+(1 row)
+
+DROP TABLE vacstat_frz;
-- WAL metrics. A vacuum that removes tuples always emits WAL
-- (wal_records > 0, wal_bytes > 0). wal_fpi depends on whether a checkpoint
-- happened recently, so it is only checked for being non-negative here; the
diff --git a/src/test/regress/sql/vacuum_stats.sql b/src/test/regress/sql/vacuum_stats.sql
index 8ce60dac77..f106b7c37b 100644
--- a/src/test/regress/sql/vacuum_stats.sql
+++ b/src/test/regress/sql/vacuum_stats.sql
@@ -55,6 +55,32 @@ SELECT recently_dead_tuples = 0 AS recently_dead_tuples,
missed_dead_tuples = 0 AS missed_dead_tuples
FROM pg_stat_vacuum_tables WHERE relname = 'vacstat_t';
+-- visibility-map page transitions. Removing the interleaved dead tuples lets
+-- VACUUM mark every heap page all-visible (vm_new_visible_pages > 0). Whether
+-- VACUUM also freezes those pages (vm_new_frozen_pages /
+-- vm_new_visible_frozen_pages) depends on opportunistic freezing, which is not
+-- deterministic here, so those are only checked for being non-negative; the
+-- positive freeze path is covered by the dedicated VACUUM (FREEZE) scenario
+-- below.
+SELECT vm_new_frozen_pages >= 0 AS vm_new_frozen_pages,
+ vm_new_visible_pages > 0 AS vm_new_visible_pages,
+ vm_new_visible_frozen_pages >= 0 AS vm_new_visible_frozen_pages
+ FROM pg_stat_vacuum_tables WHERE relname = 'vacstat_t';
+
+-- freeze path: a dedicated VACUUM (FREEZE) marks freshly-loaded heap pages
+-- all-visible and all-frozen in one pass, so vm_new_visible_frozen_pages
+-- advances. This restores the coverage of the former
+-- 053_vacuum_extending_freeze TAP test.
+CREATE TABLE vacstat_frz (x int)
+ WITH (autovacuum_enabled = off, fillfactor = 10);
+INSERT INTO vacstat_frz SELECT g FROM generate_series(1, 1000) g;
+VACUUM (FREEZE) vacstat_frz;
+SELECT pg_stat_force_next_flush();
+SELECT vm_new_visible_pages > 0 AS vm_new_visible_pages,
+ vm_new_visible_frozen_pages > 0 AS vm_new_visible_frozen_pages
+ FROM pg_stat_vacuum_tables WHERE relname = 'vacstat_frz';
+DROP TABLE vacstat_frz;
+
-- WAL metrics. A vacuum that removes tuples always emits WAL
-- (wal_records > 0, wal_bytes > 0). wal_fpi depends on whether a checkpoint
-- happened recently, so it is only checked for being non-negative here; the
--
2.39.5 (Apple Git-154)
[text/plain] 0007-Extended-vacuum-statistics-total-shared-buffer-acces.patch (29.1K, 9-0007-Extended-vacuum-statistics-total-shared-buffer-acces.patch)
download | inline diff:
From 66aafd7ac70686a164c35bf6bb3b8b757c16cf43 Mon Sep 17 00:00:00 2001
From: Alena Rybakina <[email protected]>
Date: Wed, 17 Jun 2026 21:46:14 +0300
Subject: [PATCH 7/9] Extended vacuum statistics: total shared-buffer access
counters
Expose the shared-buffer access counters in pg_stat_vacuum_tables,
pg_stat_vacuum_indexes and the pg_stat_vacuum_database aggregate, with
documentation and regression coverage:
total_blks_read shared buffer blocks missed (read from disk)
total_blks_hit shared buffer blocks found in the buffer cache (hits)
total_blks_dirtied shared buffer blocks dirtied by this vacuum
total_blks_written shared buffer blocks written out
These counters are common to tables, indexes and the database aggregate
(where the first two are named db_blks_read/db_blks_hit); total_blks_dirtied
counts only the pages dirtied by this vacuum.
---
doc/src/sgml/system-views.sgml | 96 ++++++++++++++++++++++
src/backend/access/heap/vacuumlazy.c | 23 +++++-
src/backend/catalog/system_views.sql | 12 +++
src/backend/utils/activity/pgstat_vacuum.c | 5 ++
src/backend/utils/adt/pgstatfuncs.c | 20 ++++-
src/include/catalog/pg_proc.dat | 18 ++--
src/include/pgstat.h | 6 ++
src/test/regress/expected/rules.out | 18 +++-
src/test/regress/expected/vacuum_stats.out | 42 ++++++++--
src/test/regress/sql/vacuum_stats.sql | 26 +++++-
10 files changed, 240 insertions(+), 26 deletions(-)
diff --git a/doc/src/sgml/system-views.sgml b/doc/src/sgml/system-views.sgml
index 1be54673bf..f37a3b64c1 100644
--- a/doc/src/sgml/system-views.sgml
+++ b/doc/src/sgml/system-views.sgml
@@ -5916,6 +5916,38 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx
Number of heap pages newly marked both all-visible and all-frozen in the visibility map by the vacuum.
</para></entry>
</row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>total_blks_read</structfield> <type>bigint</type>
+ </para>
+ <para>
+ Number of shared buffer blocks missed (read from disk) by the vacuum.
+ </para></entry>
+ </row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>total_blks_hit</structfield> <type>bigint</type>
+ </para>
+ <para>
+ Number of shared buffer blocks found in the buffer cache (hits) by the vacuum.
+ </para></entry>
+ </row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>total_blks_dirtied</structfield> <type>bigint</type>
+ </para>
+ <para>
+ Number of shared buffer blocks dirtied by the vacuum.
+ </para></entry>
+ </row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>total_blks_written</structfield> <type>bigint</type>
+ </para>
+ <para>
+ Number of shared buffer blocks written out by the vacuum.
+ </para></entry>
+ </row>
<row>
<entry role="catalog_table_entry"><para role="column_definition">
<structfield>wraparound_failsafe</structfield> <type>integer</type>
@@ -6039,6 +6071,38 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx
Number of index entries removed by the vacuum.
</para></entry>
</row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>total_blks_read</structfield> <type>bigint</type>
+ </para>
+ <para>
+ Number of shared buffer blocks read while vacuuming the index.
+ </para></entry>
+ </row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>total_blks_hit</structfield> <type>bigint</type>
+ </para>
+ <para>
+ Number of shared buffer block hits while vacuuming the index.
+ </para></entry>
+ </row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>total_blks_dirtied</structfield> <type>bigint</type>
+ </para>
+ <para>
+ Number of shared buffer blocks dirtied while vacuuming the index.
+ </para></entry>
+ </row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>total_blks_written</structfield> <type>bigint</type>
+ </para>
+ <para>
+ Number of shared buffer blocks written out while vacuuming the index.
+ </para></entry>
+ </row>
<row>
<entry role="catalog_table_entry"><para role="column_definition">
<structfield>wal_records</structfield> <type>bigint</type>
@@ -6121,6 +6185,38 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx
Number of vacuum operations in this database that failed with an error.
</para></entry>
</row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>db_blks_read</structfield> <type>bigint</type>
+ </para>
+ <para>
+ Number of shared buffer blocks read by vacuum operations in this database.
+ </para></entry>
+ </row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>db_blks_hit</structfield> <type>bigint</type>
+ </para>
+ <para>
+ Number of shared buffer block hits by vacuum operations in this database.
+ </para></entry>
+ </row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>total_blks_dirtied</structfield> <type>bigint</type>
+ </para>
+ <para>
+ Number of shared buffer blocks dirtied by vacuum operations in this database.
+ </para></entry>
+ </row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>total_blks_written</structfield> <type>bigint</type>
+ </para>
+ <para>
+ Number of shared buffer blocks written out by vacuum operations in this database.
+ </para></entry>
+ </row>
<row>
<entry role="catalog_table_entry"><para role="column_definition">
<structfield>wraparound_failsafe</structfield> <type>integer</type>
diff --git a/src/backend/access/heap/vacuumlazy.c b/src/backend/access/heap/vacuumlazy.c
index 46b7418136..1d6db2a91d 100644
--- a/src/backend/access/heap/vacuumlazy.c
+++ b/src/backend/access/heap/vacuumlazy.c
@@ -518,6 +518,7 @@ extvac_stats_start(Relation rel, LVExtStatCounters * counters)
memset(counters, 0, sizeof(LVExtStatCounters));
counters->walusage = pgWalUsage;
+ counters->bufusage = pgBufferUsage;
}
/* ----------
@@ -531,19 +532,27 @@ extvac_stats_end(Relation rel, LVExtStatCounters * counters,
PgStat_CommonCounts * report)
{
WalUsage walusage;
+ BufferUsage bufusage;
if (!pgstat_track_vacuum_statistics)
return;
memset(report, 0, sizeof(PgStat_CommonCounts));
- /* Calculate diffs of global stat parameters on WAL usage. */
+ /* Calculate diffs of global stat parameters on WAL and buffer usage. */
memset(&walusage, 0, sizeof(WalUsage));
WalUsageAccumDiff(&walusage, &pgWalUsage, &counters->walusage);
+ memset(&bufusage, 0, sizeof(BufferUsage));
+ BufferUsageAccumDiff(&bufusage, &pgBufferUsage, &counters->bufusage);
+
/*
* Fill additional statistics on a vacuum processing operation.
*/
+ report->total_blks_read += bufusage.local_blks_read + bufusage.shared_blks_read;
+ report->total_blks_hit += bufusage.local_blks_hit + bufusage.shared_blks_hit;
+ report->total_blks_dirtied += bufusage.local_blks_dirtied + bufusage.shared_blks_dirtied;
+ report->total_blks_written += bufusage.shared_blks_written;
report->wal_records += walusage.wal_records;
report->wal_fpi += walusage.wal_fpi;
report->wal_bytes += walusage.wal_bytes;
@@ -634,6 +643,10 @@ accumulate_heap_vacuum_statistics(LVRelState *vacrel, PgStat_VacuumRelationCount
extVacStats->table.missed_dead_pages = vacrel->missed_dead_pages;
extVacStats->common.wraparound_failsafe_count = vacrel->wraparound_failsafe;
+ extVacStats->common.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;
@@ -643,6 +656,10 @@ static void
accumulate_idxs_vacuum_statistics(LVRelState *vacrel, PgStat_VacuumRelationCounts * extVacIdxStats)
{
/* Fill heap-specific extended stats fields */
+ 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;
@@ -664,6 +681,10 @@ extvac_accumulate_idx_report(PgStat_VacuumRelationCounts * dst,
{
dst->type = PGSTAT_EXTVAC_INDEX;
+ dst->common.total_blks_read += src->common.total_blks_read;
+ dst->common.total_blks_hit += src->common.total_blks_hit;
+ dst->common.total_blks_dirtied += src->common.total_blks_dirtied;
+ dst->common.total_blks_written += src->common.total_blks_written;
dst->common.wal_records += src->common.wal_records;
dst->common.wal_fpi += src->common.wal_fpi;
dst->common.wal_bytes += src->common.wal_bytes;
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index a7ac037bc3..1ee74f9273 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -1576,6 +1576,10 @@ CREATE VIEW pg_stat_vacuum_tables AS
S.vm_new_frozen_pages AS vm_new_frozen_pages,
S.vm_new_visible_pages AS vm_new_visible_pages,
S.vm_new_visible_frozen_pages AS vm_new_visible_frozen_pages,
+ S.total_blks_read AS total_blks_read,
+ S.total_blks_hit AS total_blks_hit,
+ S.total_blks_dirtied AS total_blks_dirtied,
+ S.total_blks_written AS total_blks_written,
S.wraparound_failsafe AS wraparound_failsafe,
S.wal_records AS wal_records,
S.wal_fpi AS wal_fpi,
@@ -1597,6 +1601,10 @@ CREATE VIEW pg_stat_vacuum_indexes AS
S.pages_deleted AS pages_deleted,
S.tuples_deleted AS tuples_deleted,
+ S.total_blks_read AS total_blks_read,
+ S.total_blks_hit AS total_blks_hit,
+ S.total_blks_dirtied AS total_blks_dirtied,
+ S.total_blks_written AS total_blks_written,
S.wal_records AS wal_records,
S.wal_fpi AS wal_fpi,
S.wal_bytes AS wal_bytes
@@ -1615,6 +1623,10 @@ CREATE VIEW pg_stat_vacuum_database AS
S.errors AS errors,
+ S.db_blks_read AS db_blks_read,
+ S.db_blks_hit AS db_blks_hit,
+ S.total_blks_dirtied AS total_blks_dirtied,
+ S.total_blks_written AS total_blks_written,
S.wraparound_failsafe AS wraparound_failsafe,
S.wal_records AS wal_records,
S.wal_fpi AS wal_fpi,
diff --git a/src/backend/utils/activity/pgstat_vacuum.c b/src/backend/utils/activity/pgstat_vacuum.c
index ec3d50c59c..2bb44da510 100644
--- a/src/backend/utils/activity/pgstat_vacuum.c
+++ b/src/backend/utils/activity/pgstat_vacuum.c
@@ -38,6 +38,11 @@ bool pgstat_track_vacuum_statistics_for_relations = false;
static void
pgstat_accumulate_common(PgStat_CommonCounts *dst, const PgStat_CommonCounts *src)
{
+ ACCUMULATE_FIELD(total_blks_read);
+ ACCUMULATE_FIELD(total_blks_hit);
+ ACCUMULATE_FIELD(total_blks_dirtied);
+ ACCUMULATE_FIELD(total_blks_written);
+
ACCUMULATE_FIELD(wal_records);
ACCUMULATE_FIELD(wal_fpi);
ACCUMULATE_FIELD(wal_bytes);
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 694fd220b0..eeca140354 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -2374,7 +2374,7 @@ pg_stat_have_stats(PG_FUNCTION_ARGS)
Datum
pg_stat_get_vacuum_tables(PG_FUNCTION_ARGS)
{
-#define PG_STAT_GET_VACUUM_TABLES_STATS_COLS 15
+#define PG_STAT_GET_VACUUM_TABLES_STATS_COLS 19
Oid relid = PG_GETARG_OID(0);
PgStat_VacuumRelationCounts *extvacuum;
@@ -2411,6 +2411,10 @@ pg_stat_get_vacuum_tables(PG_FUNCTION_ARGS)
values[i++] = Int64GetDatum(extvacuum->table.vm_new_frozen_pages);
values[i++] = Int64GetDatum(extvacuum->table.vm_new_visible_pages);
values[i++] = Int64GetDatum(extvacuum->table.vm_new_visible_frozen_pages);
+ values[i++] = Int64GetDatum(extvacuum->common.total_blks_read);
+ values[i++] = Int64GetDatum(extvacuum->common.total_blks_hit);
+ values[i++] = Int64GetDatum(extvacuum->common.total_blks_dirtied);
+ values[i++] = Int64GetDatum(extvacuum->common.total_blks_written);
values[i++] = Int32GetDatum(extvacuum->common.wraparound_failsafe_count);
values[i++] = Int64GetDatum(extvacuum->common.wal_records);
values[i++] = Int64GetDatum(extvacuum->common.wal_fpi);
@@ -2432,7 +2436,7 @@ pg_stat_get_vacuum_tables(PG_FUNCTION_ARGS)
Datum
pg_stat_get_vacuum_indexes(PG_FUNCTION_ARGS)
{
-#define PG_STAT_GET_VACUUM_INDEX_STATS_COLS 6
+#define PG_STAT_GET_VACUUM_INDEX_STATS_COLS 10
Oid relid = PG_GETARG_OID(0);
PgStat_VacuumRelationCounts *extvacuum;
@@ -2461,6 +2465,11 @@ pg_stat_get_vacuum_indexes(PG_FUNCTION_ARGS)
values[i++] = Int64GetDatum(extvacuum->index.pages_deleted);
values[i++] = Int64GetDatum(extvacuum->common.tuples_deleted);
+ values[i++] = Int64GetDatum(extvacuum->common.total_blks_read);
+ values[i++] = Int64GetDatum(extvacuum->common.total_blks_hit);
+ values[i++] = Int64GetDatum(extvacuum->common.total_blks_dirtied);
+ values[i++] = Int64GetDatum(extvacuum->common.total_blks_written);
+
values[i++] = Int64GetDatum(extvacuum->common.wal_records);
values[i++] = Int64GetDatum(extvacuum->common.wal_fpi);
snprintf(buf, sizeof buf, UINT64_FORMAT, extvacuum->common.wal_bytes);
@@ -2480,7 +2489,7 @@ pg_stat_get_vacuum_indexes(PG_FUNCTION_ARGS)
Datum
pg_stat_get_vacuum_database(PG_FUNCTION_ARGS)
{
-#define PG_STAT_GET_VACUUM_DATABASE_STATS_COLS 7
+#define PG_STAT_GET_VACUUM_DATABASE_STATS_COLS 11
Oid dbid = PG_GETARG_OID(0);
PgStat_VacuumDBCounts *extvacuum;
@@ -2504,6 +2513,11 @@ pg_stat_get_vacuum_database(PG_FUNCTION_ARGS)
values[i++] = Int32GetDatum(extvacuum->errors);
+ values[i++] = Int64GetDatum(extvacuum->common.total_blks_read);
+ values[i++] = Int64GetDatum(extvacuum->common.total_blks_hit);
+ values[i++] = Int64GetDatum(extvacuum->common.total_blks_dirtied);
+ values[i++] = Int64GetDatum(extvacuum->common.total_blks_written);
+
values[i++] = Int32GetDatum(extvacuum->common.wraparound_failsafe_count);
values[i++] = Int64GetDatum(extvacuum->common.wal_records);
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index d2fb1b4ec0..718ba80a3a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12643,9 +12643,9 @@
proname => 'pg_stat_get_vacuum_tables', prorows => 1000, provolatile => 's', prorettype => 'record', proisstrict => 'f',
proretset => 't',
proargtypes => 'oid',
- proallargtypes => '{oid,oid,int8,int8,int8,int8,int8,int8,int8,int8,int8,int8,int4,int8,int8,numeric}',
- proargmodes => '{i,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o}',
- proargnames => '{reloid,relid,pages_scanned,pages_removed,tuples_deleted,tuples_frozen,recently_dead_tuples,missed_dead_pages,missed_dead_tuples,vm_new_frozen_pages,vm_new_visible_pages,vm_new_visible_frozen_pages,wraparound_failsafe,wal_records,wal_fpi,wal_bytes}',
+ proallargtypes => '{oid,oid,int8,int8,int8,int8,int8,int8,int8,int8,int8,int8,int8,int8,int8,int8,int4,int8,int8,numeric}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o}',
+ proargnames => '{reloid,relid,pages_scanned,pages_removed,tuples_deleted,tuples_frozen,recently_dead_tuples,missed_dead_pages,missed_dead_tuples,vm_new_frozen_pages,vm_new_visible_pages,vm_new_visible_frozen_pages,total_blks_read,total_blks_hit,total_blks_dirtied,total_blks_written,wraparound_failsafe,wal_records,wal_fpi,wal_bytes}',
prosrc => 'pg_stat_get_vacuum_tables' }
# oid8 related functions
@@ -12718,17 +12718,17 @@
proname => 'pg_stat_get_vacuum_indexes', prorows => 1000, provolatile => 's', prorettype => 'record', proisstrict => 'f',
proretset => 't',
proargtypes => 'oid',
- proallargtypes => '{oid,oid,int8,int8,int8,int8,numeric}',
- proargmodes => '{i,o,o,o,o,o,o}',
- proargnames => '{reloid,relid,pages_deleted,tuples_deleted,wal_records,wal_fpi,wal_bytes}',
+ proallargtypes => '{oid,oid,int8,int8,int8,int8,int8,int8,int8,int8,numeric}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o,o,o}',
+ proargnames => '{reloid,relid,pages_deleted,tuples_deleted,total_blks_read,total_blks_hit,total_blks_dirtied,total_blks_written,wal_records,wal_fpi,wal_bytes}',
prosrc => 'pg_stat_get_vacuum_indexes' },
{ oid => '8005',
descr => 'pg_stat_get_vacuum_database returns vacuum stats values for database',
proname => 'pg_stat_get_vacuum_database', prorows => 1000, provolatile => 's', prorettype => 'record', proisstrict => 'f',
proretset => 't',
proargtypes => 'oid',
- proallargtypes => '{oid,oid,int4,int4,int8,int8,numeric,int4}',
- proargmodes => '{i,o,o,o,o,o,o,o}',
- proargnames => '{dbid,dboid,errors,wraparound_failsafe,wal_records,wal_fpi,wal_bytes,interrupts_count}',
+ proallargtypes => '{oid,oid,int4,int8,int8,int8,int8,int4,int8,int8,numeric,int4}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o,o,o,o}',
+ proargnames => '{dbid,dboid,errors,db_blks_read,db_blks_hit,total_blks_dirtied,total_blks_written,wraparound_failsafe,wal_records,wal_fpi,wal_bytes,interrupts_count}',
prosrc => 'pg_stat_get_vacuum_database' },
]
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index bcc84dbd41..92f8b9b8ec 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -175,6 +175,12 @@ typedef struct PgStat_TableCounts
typedef struct PgStat_CommonCounts
{
+ /* blocks */
+ int64 total_blks_read;
+ int64 total_blks_hit;
+ int64 total_blks_dirtied;
+ int64 total_blks_written;
+
/* WAL */
int64 wal_records;
int64 wal_fpi;
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 27a17ee0c2..2b879192aa 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -2424,13 +2424,17 @@ pg_stat_user_tables| SELECT relid,
pg_stat_vacuum_database| SELECT d.oid AS dboid,
d.datname AS dbname,
s.errors,
+ s.db_blks_read,
+ s.db_blks_hit,
+ s.total_blks_dirtied,
+ s.total_blks_written,
s.wraparound_failsafe,
s.wal_records,
s.wal_fpi,
s.wal_bytes,
s.interrupts_count
FROM pg_database d,
- LATERAL pg_stat_get_vacuum_database(d.oid) s(dboid, errors, wraparound_failsafe, wal_records, wal_fpi, wal_bytes, interrupts_count);
+ LATERAL pg_stat_get_vacuum_database(d.oid) s(dboid, errors, db_blks_read, db_blks_hit, total_blks_dirtied, total_blks_written, wraparound_failsafe, wal_records, wal_fpi, wal_bytes, interrupts_count);
pg_stat_vacuum_indexes| SELECT c.oid AS relid,
i.oid AS indexrelid,
n.nspname AS schemaname,
@@ -2438,6 +2442,10 @@ pg_stat_vacuum_indexes| SELECT c.oid AS relid,
i.relname AS indexrelname,
s.pages_deleted,
s.tuples_deleted,
+ s.total_blks_read,
+ s.total_blks_hit,
+ s.total_blks_dirtied,
+ s.total_blks_written,
s.wal_records,
s.wal_fpi,
s.wal_bytes
@@ -2445,7 +2453,7 @@ pg_stat_vacuum_indexes| SELECT c.oid AS relid,
JOIN pg_index x ON ((c.oid = x.indrelid)))
JOIN pg_class i ON ((i.oid = x.indexrelid)))
LEFT JOIN pg_namespace n ON ((n.oid = c.relnamespace))),
- LATERAL pg_stat_get_vacuum_indexes(i.oid) s(relid, pages_deleted, tuples_deleted, wal_records, wal_fpi, wal_bytes)
+ LATERAL pg_stat_get_vacuum_indexes(i.oid) s(relid, pages_deleted, tuples_deleted, total_blks_read, total_blks_hit, total_blks_dirtied, total_blks_written, wal_records, wal_fpi, wal_bytes)
WHERE (c.relkind = ANY (ARRAY['r'::"char", 't'::"char", 'm'::"char"]));
pg_stat_vacuum_tables| SELECT n.nspname AS schemaname,
c.relname,
@@ -2460,13 +2468,17 @@ pg_stat_vacuum_tables| SELECT n.nspname AS schemaname,
s.vm_new_frozen_pages,
s.vm_new_visible_pages,
s.vm_new_visible_frozen_pages,
+ s.total_blks_read,
+ s.total_blks_hit,
+ s.total_blks_dirtied,
+ s.total_blks_written,
s.wraparound_failsafe,
s.wal_records,
s.wal_fpi,
s.wal_bytes
FROM (pg_class c
JOIN pg_namespace n ON ((n.oid = c.relnamespace))),
- LATERAL pg_stat_get_vacuum_tables(c.oid) s(relid, pages_scanned, pages_removed, tuples_deleted, tuples_frozen, recently_dead_tuples, missed_dead_pages, missed_dead_tuples, vm_new_frozen_pages, vm_new_visible_pages, vm_new_visible_frozen_pages, wraparound_failsafe, wal_records, wal_fpi, wal_bytes)
+ LATERAL pg_stat_get_vacuum_tables(c.oid) s(relid, pages_scanned, pages_removed, tuples_deleted, tuples_frozen, recently_dead_tuples, missed_dead_pages, missed_dead_tuples, vm_new_frozen_pages, vm_new_visible_pages, vm_new_visible_frozen_pages, total_blks_read, total_blks_hit, total_blks_dirtied, total_blks_written, wraparound_failsafe, wal_records, wal_fpi, wal_bytes)
WHERE (c.relkind = ANY (ARRAY['r'::"char", 't'::"char", 'm'::"char"]));
pg_stat_wal| SELECT wal_records,
wal_fpi,
diff --git a/src/test/regress/expected/vacuum_stats.out b/src/test/regress/expected/vacuum_stats.out
index c996699804..87f49cb76c 100644
--- a/src/test/regress/expected/vacuum_stats.out
+++ b/src/test/regress/expected/vacuum_stats.out
@@ -123,6 +123,21 @@ SELECT vm_new_visible_pages > 0 AS vm_new_visible_pages,
(1 row)
DROP TABLE vacstat_frz;
+-- total buffer access counters. The vacuum always touches the table's pages
+-- through the buffer cache (total_blks_hit > 0) and dirties some of them while
+-- removing dead tuples (total_blks_dirtied > 0). total_blks_read and
+-- total_blks_written depend on the buffer-cache and checkpoint state at run
+-- time, so they are only checked for being non-negative.
+SELECT total_blks_read >= 0 AS total_blks_read,
+ total_blks_hit > 0 AS total_blks_hit,
+ total_blks_dirtied > 0 AS total_blks_dirtied,
+ total_blks_written >= 0 AS total_blks_written
+ FROM pg_stat_vacuum_tables WHERE relname = 'vacstat_t';
+ total_blks_read | total_blks_hit | total_blks_dirtied | total_blks_written
+-----------------+----------------+--------------------+--------------------
+ t | t | t | t
+(1 row)
+
-- WAL metrics. A vacuum that removes tuples always emits WAL
-- (wal_records > 0, wal_bytes > 0). wal_fpi depends on whether a checkpoint
-- happened recently, so it is only checked for being non-negative here; the
@@ -174,17 +189,23 @@ SELECT wraparound_failsafe = 0 AS wraparound_failsafe
-- per-index view: the primary key index is processed by the same VACUUM.
-- No btree leaf empties out (interleaved deletions), so pages_deleted = 0,
--- while every index entry for a removed heap tuple is deleted.
+-- while every index entry for a removed heap tuple is deleted. The index is
+-- read through the buffer cache (total_blks_hit > 0); the read/written/dirtied
+-- counters depend on run-time cache state.
SELECT indexrelname,
pages_deleted = 0 AS pages_deleted,
tuples_deleted = 500 AS tuples_deleted,
+ total_blks_read >= 0 AS total_blks_read,
+ total_blks_hit > 0 AS total_blks_hit,
+ total_blks_dirtied >= 0 AS total_blks_dirtied,
+ total_blks_written >= 0 AS total_blks_written,
wal_records > 0 AS wal_records,
wal_fpi >= 0 AS wal_fpi,
wal_bytes > 0 AS wal_bytes
FROM pg_stat_vacuum_indexes WHERE relname = 'vacstat_t' ORDER BY indexrelname;
- indexrelname | pages_deleted | tuples_deleted | wal_records | wal_fpi | wal_bytes
-----------------+---------------+----------------+-------------+---------+-----------
- vacstat_t_pkey | t | t | t | t | t
+ indexrelname | pages_deleted | tuples_deleted | total_blks_read | total_blks_hit | total_blks_dirtied | total_blks_written | wal_records | wal_fpi | wal_bytes
+----------------+---------------+----------------+-----------------+----------------+--------------------+--------------------+-------------+---------+-----------
+ vacstat_t_pkey | t | t | t | t | t | t | t | t | t
(1 row)
-- index page-deletion path: deleting a contiguous key range empties whole
@@ -219,16 +240,21 @@ SELECT indexrelname,
DROP TABLE vacstat_idxdel;
-- per-database aggregate view: no vacuum errors occurred in this database, and
--- the vacuums in this database emit WAL (wal_records > 0).
+-- the vacuums in this database touched pages through the buffer cache
+-- (db_blks_hit > 0) and emit WAL (wal_records > 0).
SELECT errors = 0 AS errors,
+ db_blks_read >= 0 AS db_blks_read,
+ db_blks_hit > 0 AS db_blks_hit,
+ total_blks_dirtied >= 0 AS total_blks_dirtied,
+ total_blks_written >= 0 AS total_blks_written,
wraparound_failsafe = 0 AS wraparound_failsafe,
wal_records > 0 AS wal_records,
wal_fpi >= 0 AS wal_fpi,
wal_bytes > 0 AS wal_bytes
FROM pg_stat_vacuum_database WHERE dbname = current_database();
- errors | wraparound_failsafe | wal_records | wal_fpi | wal_bytes
---------+---------------------+-------------+---------+-----------
- t | t | t | t | t
+ errors | db_blks_read | db_blks_hit | total_blks_dirtied | total_blks_written | wraparound_failsafe | wal_records | wal_fpi | wal_bytes
+--------+--------------+-------------+--------------------+--------------------+---------------------+-------------+---------+-----------
+ t | t | t | t | t | t | t | t | t
(1 row)
-- parallel index vacuum: index statistics must be captured for indexes
diff --git a/src/test/regress/sql/vacuum_stats.sql b/src/test/regress/sql/vacuum_stats.sql
index f106b7c37b..6e364d183c 100644
--- a/src/test/regress/sql/vacuum_stats.sql
+++ b/src/test/regress/sql/vacuum_stats.sql
@@ -81,6 +81,17 @@ SELECT vm_new_visible_pages > 0 AS vm_new_visible_pages,
FROM pg_stat_vacuum_tables WHERE relname = 'vacstat_frz';
DROP TABLE vacstat_frz;
+-- total buffer access counters. The vacuum always touches the table's pages
+-- through the buffer cache (total_blks_hit > 0) and dirties some of them while
+-- removing dead tuples (total_blks_dirtied > 0). total_blks_read and
+-- total_blks_written depend on the buffer-cache and checkpoint state at run
+-- time, so they are only checked for being non-negative.
+SELECT total_blks_read >= 0 AS total_blks_read,
+ total_blks_hit > 0 AS total_blks_hit,
+ total_blks_dirtied > 0 AS total_blks_dirtied,
+ total_blks_written >= 0 AS total_blks_written
+ FROM pg_stat_vacuum_tables WHERE relname = 'vacstat_t';
+
-- WAL metrics. A vacuum that removes tuples always emits WAL
-- (wal_records > 0, wal_bytes > 0). wal_fpi depends on whether a checkpoint
-- happened recently, so it is only checked for being non-negative here; the
@@ -115,10 +126,16 @@ SELECT wraparound_failsafe = 0 AS wraparound_failsafe
-- per-index view: the primary key index is processed by the same VACUUM.
-- No btree leaf empties out (interleaved deletions), so pages_deleted = 0,
--- while every index entry for a removed heap tuple is deleted.
+-- while every index entry for a removed heap tuple is deleted. The index is
+-- read through the buffer cache (total_blks_hit > 0); the read/written/dirtied
+-- counters depend on run-time cache state.
SELECT indexrelname,
pages_deleted = 0 AS pages_deleted,
tuples_deleted = 500 AS tuples_deleted,
+ total_blks_read >= 0 AS total_blks_read,
+ total_blks_hit > 0 AS total_blks_hit,
+ total_blks_dirtied >= 0 AS total_blks_dirtied,
+ total_blks_written >= 0 AS total_blks_written,
wal_records > 0 AS wal_records,
wal_fpi >= 0 AS wal_fpi,
wal_bytes > 0 AS wal_bytes
@@ -142,8 +159,13 @@ SELECT indexrelname,
DROP TABLE vacstat_idxdel;
-- per-database aggregate view: no vacuum errors occurred in this database, and
--- the vacuums in this database emit WAL (wal_records > 0).
+-- the vacuums in this database touched pages through the buffer cache
+-- (db_blks_hit > 0) and emit WAL (wal_records > 0).
SELECT errors = 0 AS errors,
+ db_blks_read >= 0 AS db_blks_read,
+ db_blks_hit > 0 AS db_blks_hit,
+ total_blks_dirtied >= 0 AS total_blks_dirtied,
+ total_blks_written >= 0 AS total_blks_written,
wraparound_failsafe = 0 AS wraparound_failsafe,
wal_records > 0 AS wal_records,
wal_fpi >= 0 AS wal_fpi,
--
2.39.5 (Apple Git-154)
[text/plain] 0008-Extended-vacuum-statistics-per-relation-buffer-acces.patch (17.8K, 10-0008-Extended-vacuum-statistics-per-relation-buffer-acces.patch)
download | inline diff:
From 690556053f3abce9b91abed7ff29e309dfbc8ed6 Mon Sep 17 00:00:00 2001
From: Alena Rybakina <[email protected]>
Date: Wed, 17 Jun 2026 21:49:18 +0300
Subject: [PATCH 8/9] Extended vacuum statistics: per-relation buffer access
for tables and indexes
Expose the per-relation buffer access counters in pg_stat_vacuum_tables and
pg_stat_vacuum_indexes, with documentation and regression coverage:
rel_blks_read this relation's blocks read from disk by the vacuum
rel_blks_hit this relation's blocks found in shared buffers
Unlike total_blks_*, which count all shared-buffer access during the vacuum,
these are restricted to the target relation's own blocks.
---
doc/src/sgml/system-views.sgml | 32 ++++++++++++++++++++++
src/backend/access/heap/vacuumlazy.c | 28 +++++++++++++++++++
src/backend/catalog/system_views.sql | 4 +++
src/backend/utils/activity/pgstat_vacuum.c | 3 ++
src/backend/utils/adt/pgstatfuncs.c | 9 ++++--
src/include/catalog/pg_proc.dat | 12 ++++----
src/include/pgstat.h | 4 +++
src/test/regress/expected/rules.out | 8 ++++--
src/test/regress/expected/vacuum_stats.out | 18 ++++++++++--
src/test/regress/sql/vacuum_stats.sql | 8 ++++++
10 files changed, 113 insertions(+), 13 deletions(-)
diff --git a/doc/src/sgml/system-views.sgml b/doc/src/sgml/system-views.sgml
index f37a3b64c1..2a93896d5d 100644
--- a/doc/src/sgml/system-views.sgml
+++ b/doc/src/sgml/system-views.sgml
@@ -5948,6 +5948,22 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx
Number of shared buffer blocks written out by the vacuum.
</para></entry>
</row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>rel_blks_read</structfield> <type>bigint</type>
+ </para>
+ <para>
+ Number of this relation's blocks read from disk by the vacuum.
+ </para></entry>
+ </row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>rel_blks_hit</structfield> <type>bigint</type>
+ </para>
+ <para>
+ Number of this relation's blocks found in shared buffers by the vacuum.
+ </para></entry>
+ </row>
<row>
<entry role="catalog_table_entry"><para role="column_definition">
<structfield>wraparound_failsafe</structfield> <type>integer</type>
@@ -6103,6 +6119,22 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx
Number of shared buffer blocks written out while vacuuming the index.
</para></entry>
</row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>rel_blks_read</structfield> <type>bigint</type>
+ </para>
+ <para>
+ Number of blocks of this index read from disk by the vacuum.
+ </para></entry>
+ </row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>rel_blks_hit</structfield> <type>bigint</type>
+ </para>
+ <para>
+ Number of block hits within this index during the vacuum.
+ </para></entry>
+ </row>
<row>
<entry role="catalog_table_entry"><para role="column_definition">
<structfield>wal_records</structfield> <type>bigint</type>
diff --git a/src/backend/access/heap/vacuumlazy.c b/src/backend/access/heap/vacuumlazy.c
index 1d6db2a91d..683bfcf9e8 100644
--- a/src/backend/access/heap/vacuumlazy.c
+++ b/src/backend/access/heap/vacuumlazy.c
@@ -519,6 +519,19 @@ extvac_stats_start(Relation rel, LVExtStatCounters * counters)
counters->walusage = pgWalUsage;
counters->bufusage = pgBufferUsage;
+ counters->blocks_fetched = 0;
+ counters->blocks_hit = 0;
+
+ if (!rel->pgstat_info || !pgstat_track_counts)
+
+ /*
+ * if something goes wrong or user doesn't want to track a database
+ * activity - just suppress it.
+ */
+ return;
+
+ counters->blocks_fetched = rel->pgstat_info->counts.blocks_fetched;
+ counters->blocks_hit = rel->pgstat_info->counts.blocks_hit;
}
/* ----------
@@ -556,6 +569,19 @@ extvac_stats_end(Relation rel, LVExtStatCounters * counters,
report->wal_records += walusage.wal_records;
report->wal_fpi += walusage.wal_fpi;
report->wal_bytes += walusage.wal_bytes;
+
+ if (!rel->pgstat_info || !pgstat_track_counts)
+
+ /*
+ * if something goes wrong or an user doesn't want to track a database
+ * activity - just suppress it.
+ */
+ return;
+
+ report->blks_fetched +=
+ rel->pgstat_info->counts.blocks_fetched - counters->blocks_fetched;
+ report->blks_hit +=
+ rel->pgstat_info->counts.blocks_hit - counters->blocks_hit;
}
void
@@ -685,6 +711,8 @@ extvac_accumulate_idx_report(PgStat_VacuumRelationCounts * dst,
dst->common.total_blks_hit += src->common.total_blks_hit;
dst->common.total_blks_dirtied += src->common.total_blks_dirtied;
dst->common.total_blks_written += src->common.total_blks_written;
+ dst->common.blks_fetched += src->common.blks_fetched;
+ dst->common.blks_hit += src->common.blks_hit;
dst->common.wal_records += src->common.wal_records;
dst->common.wal_fpi += src->common.wal_fpi;
dst->common.wal_bytes += src->common.wal_bytes;
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index 1ee74f9273..d0a58f221c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -1580,6 +1580,8 @@ CREATE VIEW pg_stat_vacuum_tables AS
S.total_blks_hit AS total_blks_hit,
S.total_blks_dirtied AS total_blks_dirtied,
S.total_blks_written AS total_blks_written,
+ S.rel_blks_read AS rel_blks_read,
+ S.rel_blks_hit AS rel_blks_hit,
S.wraparound_failsafe AS wraparound_failsafe,
S.wal_records AS wal_records,
S.wal_fpi AS wal_fpi,
@@ -1605,6 +1607,8 @@ CREATE VIEW pg_stat_vacuum_indexes AS
S.total_blks_hit AS total_blks_hit,
S.total_blks_dirtied AS total_blks_dirtied,
S.total_blks_written AS total_blks_written,
+ S.rel_blks_read AS rel_blks_read,
+ S.rel_blks_hit AS rel_blks_hit,
S.wal_records AS wal_records,
S.wal_fpi AS wal_fpi,
S.wal_bytes AS wal_bytes
diff --git a/src/backend/utils/activity/pgstat_vacuum.c b/src/backend/utils/activity/pgstat_vacuum.c
index 2bb44da510..b1c747f255 100644
--- a/src/backend/utils/activity/pgstat_vacuum.c
+++ b/src/backend/utils/activity/pgstat_vacuum.c
@@ -43,6 +43,9 @@ pgstat_accumulate_common(PgStat_CommonCounts *dst, const PgStat_CommonCounts *sr
ACCUMULATE_FIELD(total_blks_dirtied);
ACCUMULATE_FIELD(total_blks_written);
+ ACCUMULATE_FIELD(blks_fetched);
+ ACCUMULATE_FIELD(blks_hit);
+
ACCUMULATE_FIELD(wal_records);
ACCUMULATE_FIELD(wal_fpi);
ACCUMULATE_FIELD(wal_bytes);
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index eeca140354..7473ae1eb6 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -2374,7 +2374,7 @@ pg_stat_have_stats(PG_FUNCTION_ARGS)
Datum
pg_stat_get_vacuum_tables(PG_FUNCTION_ARGS)
{
-#define PG_STAT_GET_VACUUM_TABLES_STATS_COLS 19
+#define PG_STAT_GET_VACUUM_TABLES_STATS_COLS 21
Oid relid = PG_GETARG_OID(0);
PgStat_VacuumRelationCounts *extvacuum;
@@ -2415,6 +2415,8 @@ pg_stat_get_vacuum_tables(PG_FUNCTION_ARGS)
values[i++] = Int64GetDatum(extvacuum->common.total_blks_hit);
values[i++] = Int64GetDatum(extvacuum->common.total_blks_dirtied);
values[i++] = Int64GetDatum(extvacuum->common.total_blks_written);
+ values[i++] = Int64GetDatum(extvacuum->common.blks_fetched - extvacuum->common.blks_hit);
+ values[i++] = Int64GetDatum(extvacuum->common.blks_hit);
values[i++] = Int32GetDatum(extvacuum->common.wraparound_failsafe_count);
values[i++] = Int64GetDatum(extvacuum->common.wal_records);
values[i++] = Int64GetDatum(extvacuum->common.wal_fpi);
@@ -2436,7 +2438,7 @@ pg_stat_get_vacuum_tables(PG_FUNCTION_ARGS)
Datum
pg_stat_get_vacuum_indexes(PG_FUNCTION_ARGS)
{
-#define PG_STAT_GET_VACUUM_INDEX_STATS_COLS 10
+#define PG_STAT_GET_VACUUM_INDEX_STATS_COLS 12
Oid relid = PG_GETARG_OID(0);
PgStat_VacuumRelationCounts *extvacuum;
@@ -2470,6 +2472,9 @@ pg_stat_get_vacuum_indexes(PG_FUNCTION_ARGS)
values[i++] = Int64GetDatum(extvacuum->common.total_blks_dirtied);
values[i++] = Int64GetDatum(extvacuum->common.total_blks_written);
+ values[i++] = Int64GetDatum(extvacuum->common.blks_fetched - extvacuum->common.blks_hit);
+ values[i++] = Int64GetDatum(extvacuum->common.blks_hit);
+
values[i++] = Int64GetDatum(extvacuum->common.wal_records);
values[i++] = Int64GetDatum(extvacuum->common.wal_fpi);
snprintf(buf, sizeof buf, UINT64_FORMAT, extvacuum->common.wal_bytes);
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 718ba80a3a..743ce9dfd6 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12643,9 +12643,9 @@
proname => 'pg_stat_get_vacuum_tables', prorows => 1000, provolatile => 's', prorettype => 'record', proisstrict => 'f',
proretset => 't',
proargtypes => 'oid',
- proallargtypes => '{oid,oid,int8,int8,int8,int8,int8,int8,int8,int8,int8,int8,int8,int8,int8,int8,int4,int8,int8,numeric}',
- proargmodes => '{i,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o}',
- proargnames => '{reloid,relid,pages_scanned,pages_removed,tuples_deleted,tuples_frozen,recently_dead_tuples,missed_dead_pages,missed_dead_tuples,vm_new_frozen_pages,vm_new_visible_pages,vm_new_visible_frozen_pages,total_blks_read,total_blks_hit,total_blks_dirtied,total_blks_written,wraparound_failsafe,wal_records,wal_fpi,wal_bytes}',
+ proallargtypes => '{oid,oid,int8,int8,int8,int8,int8,int8,int8,int8,int8,int8,int8,int8,int8,int8,int8,int8,int4,int8,int8,numeric}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o}',
+ proargnames => '{reloid,relid,pages_scanned,pages_removed,tuples_deleted,tuples_frozen,recently_dead_tuples,missed_dead_pages,missed_dead_tuples,vm_new_frozen_pages,vm_new_visible_pages,vm_new_visible_frozen_pages,total_blks_read,total_blks_hit,total_blks_dirtied,total_blks_written,rel_blks_read,rel_blks_hit,wraparound_failsafe,wal_records,wal_fpi,wal_bytes}',
prosrc => 'pg_stat_get_vacuum_tables' }
# oid8 related functions
@@ -12718,9 +12718,9 @@
proname => 'pg_stat_get_vacuum_indexes', prorows => 1000, provolatile => 's', prorettype => 'record', proisstrict => 'f',
proretset => 't',
proargtypes => 'oid',
- proallargtypes => '{oid,oid,int8,int8,int8,int8,int8,int8,int8,int8,numeric}',
- proargmodes => '{i,o,o,o,o,o,o,o,o,o,o}',
- proargnames => '{reloid,relid,pages_deleted,tuples_deleted,total_blks_read,total_blks_hit,total_blks_dirtied,total_blks_written,wal_records,wal_fpi,wal_bytes}',
+ proallargtypes => '{oid,oid,int8,int8,int8,int8,int8,int8,int8,int8,int8,int8,numeric}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o,o,o,o,o}',
+ proargnames => '{reloid,relid,pages_deleted,tuples_deleted,total_blks_read,total_blks_hit,total_blks_dirtied,total_blks_written,rel_blks_read,rel_blks_hit,wal_records,wal_fpi,wal_bytes}',
prosrc => 'pg_stat_get_vacuum_indexes' },
{ oid => '8005',
descr => 'pg_stat_get_vacuum_database returns vacuum stats values for database',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 92f8b9b8ec..18fa9ac5ba 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -181,6 +181,10 @@ typedef struct PgStat_CommonCounts
int64 total_blks_dirtied;
int64 total_blks_written;
+ /* heap blocks */
+ int64 blks_fetched;
+ int64 blks_hit;
+
/* WAL */
int64 wal_records;
int64 wal_fpi;
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b879192aa..c650cfd664 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -2446,6 +2446,8 @@ pg_stat_vacuum_indexes| SELECT c.oid AS relid,
s.total_blks_hit,
s.total_blks_dirtied,
s.total_blks_written,
+ s.rel_blks_read,
+ s.rel_blks_hit,
s.wal_records,
s.wal_fpi,
s.wal_bytes
@@ -2453,7 +2455,7 @@ pg_stat_vacuum_indexes| SELECT c.oid AS relid,
JOIN pg_index x ON ((c.oid = x.indrelid)))
JOIN pg_class i ON ((i.oid = x.indexrelid)))
LEFT JOIN pg_namespace n ON ((n.oid = c.relnamespace))),
- LATERAL pg_stat_get_vacuum_indexes(i.oid) s(relid, pages_deleted, tuples_deleted, total_blks_read, total_blks_hit, total_blks_dirtied, total_blks_written, wal_records, wal_fpi, wal_bytes)
+ LATERAL pg_stat_get_vacuum_indexes(i.oid) s(relid, pages_deleted, tuples_deleted, total_blks_read, total_blks_hit, total_blks_dirtied, total_blks_written, rel_blks_read, rel_blks_hit, wal_records, wal_fpi, wal_bytes)
WHERE (c.relkind = ANY (ARRAY['r'::"char", 't'::"char", 'm'::"char"]));
pg_stat_vacuum_tables| SELECT n.nspname AS schemaname,
c.relname,
@@ -2472,13 +2474,15 @@ pg_stat_vacuum_tables| SELECT n.nspname AS schemaname,
s.total_blks_hit,
s.total_blks_dirtied,
s.total_blks_written,
+ s.rel_blks_read,
+ s.rel_blks_hit,
s.wraparound_failsafe,
s.wal_records,
s.wal_fpi,
s.wal_bytes
FROM (pg_class c
JOIN pg_namespace n ON ((n.oid = c.relnamespace))),
- LATERAL pg_stat_get_vacuum_tables(c.oid) s(relid, pages_scanned, pages_removed, tuples_deleted, tuples_frozen, recently_dead_tuples, missed_dead_pages, missed_dead_tuples, vm_new_frozen_pages, vm_new_visible_pages, vm_new_visible_frozen_pages, total_blks_read, total_blks_hit, total_blks_dirtied, total_blks_written, wraparound_failsafe, wal_records, wal_fpi, wal_bytes)
+ LATERAL pg_stat_get_vacuum_tables(c.oid) s(relid, pages_scanned, pages_removed, tuples_deleted, tuples_frozen, recently_dead_tuples, missed_dead_pages, missed_dead_tuples, vm_new_frozen_pages, vm_new_visible_pages, vm_new_visible_frozen_pages, total_blks_read, total_blks_hit, total_blks_dirtied, total_blks_written, rel_blks_read, rel_blks_hit, wraparound_failsafe, wal_records, wal_fpi, wal_bytes)
WHERE (c.relkind = ANY (ARRAY['r'::"char", 't'::"char", 'm'::"char"]));
pg_stat_wal| SELECT wal_records,
wal_fpi,
diff --git a/src/test/regress/expected/vacuum_stats.out b/src/test/regress/expected/vacuum_stats.out
index 87f49cb76c..3d823ef7b6 100644
--- a/src/test/regress/expected/vacuum_stats.out
+++ b/src/test/regress/expected/vacuum_stats.out
@@ -138,6 +138,16 @@ SELECT total_blks_read >= 0 AS total_blks_read,
t | t | t | t
(1 row)
+-- per-relation buffer access. The heap is read through the buffer cache
+-- (rel_blks_hit > 0); rel_blks_read depends on the run-time cache state.
+SELECT rel_blks_read >= 0 AS rel_blks_read,
+ rel_blks_hit > 0 AS rel_blks_hit
+ FROM pg_stat_vacuum_tables WHERE relname = 'vacstat_t';
+ rel_blks_read | rel_blks_hit
+---------------+--------------
+ t | t
+(1 row)
+
-- WAL metrics. A vacuum that removes tuples always emits WAL
-- (wal_records > 0, wal_bytes > 0). wal_fpi depends on whether a checkpoint
-- happened recently, so it is only checked for being non-negative here; the
@@ -199,13 +209,15 @@ SELECT indexrelname,
total_blks_hit > 0 AS total_blks_hit,
total_blks_dirtied >= 0 AS total_blks_dirtied,
total_blks_written >= 0 AS total_blks_written,
+ rel_blks_read >= 0 AS rel_blks_read,
+ rel_blks_hit > 0 AS rel_blks_hit,
wal_records > 0 AS wal_records,
wal_fpi >= 0 AS wal_fpi,
wal_bytes > 0 AS wal_bytes
FROM pg_stat_vacuum_indexes WHERE relname = 'vacstat_t' ORDER BY indexrelname;
- indexrelname | pages_deleted | tuples_deleted | total_blks_read | total_blks_hit | total_blks_dirtied | total_blks_written | wal_records | wal_fpi | wal_bytes
-----------------+---------------+----------------+-----------------+----------------+--------------------+--------------------+-------------+---------+-----------
- vacstat_t_pkey | t | t | t | t | t | t | t | t | t
+ indexrelname | pages_deleted | tuples_deleted | total_blks_read | total_blks_hit | total_blks_dirtied | total_blks_written | rel_blks_read | rel_blks_hit | wal_records | wal_fpi | wal_bytes
+----------------+---------------+----------------+-----------------+----------------+--------------------+--------------------+---------------+--------------+-------------+---------+-----------
+ vacstat_t_pkey | t | t | t | t | t | t | t | t | t | t | t
(1 row)
-- index page-deletion path: deleting a contiguous key range empties whole
diff --git a/src/test/regress/sql/vacuum_stats.sql b/src/test/regress/sql/vacuum_stats.sql
index 6e364d183c..40f7d0985d 100644
--- a/src/test/regress/sql/vacuum_stats.sql
+++ b/src/test/regress/sql/vacuum_stats.sql
@@ -92,6 +92,12 @@ SELECT total_blks_read >= 0 AS total_blks_read,
total_blks_written >= 0 AS total_blks_written
FROM pg_stat_vacuum_tables WHERE relname = 'vacstat_t';
+-- per-relation buffer access. The heap is read through the buffer cache
+-- (rel_blks_hit > 0); rel_blks_read depends on the run-time cache state.
+SELECT rel_blks_read >= 0 AS rel_blks_read,
+ rel_blks_hit > 0 AS rel_blks_hit
+ FROM pg_stat_vacuum_tables WHERE relname = 'vacstat_t';
+
-- WAL metrics. A vacuum that removes tuples always emits WAL
-- (wal_records > 0, wal_bytes > 0). wal_fpi depends on whether a checkpoint
-- happened recently, so it is only checked for being non-negative here; the
@@ -136,6 +142,8 @@ SELECT indexrelname,
total_blks_hit > 0 AS total_blks_hit,
total_blks_dirtied >= 0 AS total_blks_dirtied,
total_blks_written >= 0 AS total_blks_written,
+ rel_blks_read >= 0 AS rel_blks_read,
+ rel_blks_hit > 0 AS rel_blks_hit,
wal_records > 0 AS wal_records,
wal_fpi >= 0 AS wal_fpi,
wal_bytes > 0 AS wal_bytes
--
2.39.5 (Apple Git-154)
[text/plain] 0009-Extended-vacuum-statistics-timing-metrics-for-tables.patch (38.7K, 11-0009-Extended-vacuum-statistics-timing-metrics-for-tables.patch)
download | inline diff:
From f995078939b0180ef6e59603297b35db7031fbfb Mon Sep 17 00:00:00 2001
From: Alena Rybakina <[email protected]>
Date: Wed, 17 Jun 2026 21:54:17 +0300
Subject: [PATCH 9/9] Extended vacuum statistics: timing metrics for tables,
indexes and database
Expose the timing counters in pg_stat_vacuum_tables, pg_stat_vacuum_indexes
and pg_stat_vacuum_database, with documentation and regression coverage:
blk_read_time time spent reading blocks (with track_io_timing)
blk_write_time time spent writing blocks (with track_io_timing)
delay_time time spent in vacuum cost-based delay
total_time total wall-clock time of the vacuum
total_time is always positive; the regression test also exercises the
positive delay_time path with a dedicated cost-delayed vacuum.
---
doc/src/sgml/system-views.sgml | 97 ++++++++++++++++++++++
src/backend/access/heap/vacuumlazy.c | 42 ++++++++--
src/backend/catalog/system_views.sql | 18 ++++
src/backend/commands/vacuumparallel.c | 1 +
src/backend/utils/activity/pgstat_vacuum.c | 5 ++
src/backend/utils/adt/pgstatfuncs.c | 22 ++++-
src/include/catalog/pg_proc.dat | 18 ++--
src/include/pgstat.h | 6 ++
src/test/regress/expected/rules.out | 18 +++-
src/test/regress/expected/vacuum_stats.out | 76 +++++++++++++----
src/test/regress/sql/vacuum_stats.sql | 47 +++++++++--
11 files changed, 304 insertions(+), 46 deletions(-)
diff --git a/doc/src/sgml/system-views.sgml b/doc/src/sgml/system-views.sgml
index 2a93896d5d..e15f4952cc 100644
--- a/doc/src/sgml/system-views.sgml
+++ b/doc/src/sgml/system-views.sgml
@@ -5964,6 +5964,38 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx
Number of this relation's blocks found in shared buffers by the vacuum.
</para></entry>
</row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>blk_read_time</structfield> <type>double precision</type>
+ </para>
+ <para>
+ Time spent reading blocks by the vacuum, in milliseconds (when <varname>track_io_timing</varname> is enabled).
+ </para></entry>
+ </row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>blk_write_time</structfield> <type>double precision</type>
+ </para>
+ <para>
+ Time spent writing (flushing) blocks by the vacuum, in milliseconds; remains zero if no flushes occurred.
+ </para></entry>
+ </row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>delay_time</structfield> <type>double precision</type>
+ </para>
+ <para>
+ Total time the vacuum spent sleeping in vacuum delay points, in milliseconds.
+ </para></entry>
+ </row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>total_time</structfield> <type>double precision</type>
+ </para>
+ <para>
+ Total wall-clock time of the vacuum, in milliseconds, including time spent waiting for I/O and locks.
+ </para></entry>
+ </row>
<row>
<entry role="catalog_table_entry"><para role="column_definition">
<structfield>wraparound_failsafe</structfield> <type>integer</type>
@@ -6135,6 +6167,38 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx
Number of block hits within this index during the vacuum.
</para></entry>
</row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>blk_read_time</structfield> <type>double precision</type>
+ </para>
+ <para>
+ Time spent reading index blocks, in milliseconds (if <varname>track_io_timing</varname> is enabled).
+ </para></entry>
+ </row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>blk_write_time</structfield> <type>double precision</type>
+ </para>
+ <para>
+ Time spent writing index blocks, in milliseconds (if <varname>track_io_timing</varname> is enabled).
+ </para></entry>
+ </row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>delay_time</structfield> <type>double precision</type>
+ </para>
+ <para>
+ Time spent in vacuum cost-based delay while processing this index, in milliseconds.
+ </para></entry>
+ </row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>total_time</structfield> <type>double precision</type>
+ </para>
+ <para>
+ Total time spent vacuuming this index, in milliseconds.
+ </para></entry>
+ </row>
<row>
<entry role="catalog_table_entry"><para role="column_definition">
<structfield>wal_records</structfield> <type>bigint</type>
@@ -6257,6 +6321,38 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx
Number of vacuum operations in this database that engaged the wraparound failsafe mechanism.
</para></entry>
</row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>blk_read_time</structfield> <type>double precision</type>
+ </para>
+ <para>
+ Time spent reading blocks by vacuum operations in this database, in milliseconds (if <varname>track_io_timing</varname> is enabled).
+ </para></entry>
+ </row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>blk_write_time</structfield> <type>double precision</type>
+ </para>
+ <para>
+ Time spent writing blocks by vacuum operations in this database, in milliseconds (if <varname>track_io_timing</varname> is enabled).
+ </para></entry>
+ </row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>delay_time</structfield> <type>double precision</type>
+ </para>
+ <para>
+ Time spent in vacuum cost-based delay by vacuum operations in this database, in milliseconds.
+ </para></entry>
+ </row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>total_time</structfield> <type>double precision</type>
+ </para>
+ <para>
+ Total time spent by vacuum operations in this database, in milliseconds.
+ </para></entry>
+ </row>
<row>
<entry role="catalog_table_entry"><para role="column_definition">
<structfield>wal_records</structfield> <type>bigint</type>
@@ -6281,6 +6377,7 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx
Total amount of WAL generated by vacuum operations in this database, in bytes.
</para></entry>
</row>
+
<row>
<entry role="catalog_table_entry"><para role="column_definition">
<structfield>interrupts_count</structfield> <type>integer</type>
diff --git a/src/backend/access/heap/vacuumlazy.c b/src/backend/access/heap/vacuumlazy.c
index 683bfcf9e8..6ae2ccb2de 100644
--- a/src/backend/access/heap/vacuumlazy.c
+++ b/src/backend/access/heap/vacuumlazy.c
@@ -512,13 +512,19 @@ static void restore_vacuum_error_info(LVRelState *vacrel,
static void
extvac_stats_start(Relation rel, LVExtStatCounters * counters)
{
+ TimestampTz starttime;
+
if (!pgstat_track_vacuum_statistics)
return;
memset(counters, 0, sizeof(LVExtStatCounters));
+ starttime = GetCurrentTimestamp();
+
+ counters->starttime = starttime;
counters->walusage = pgWalUsage;
counters->bufusage = pgBufferUsage;
+ counters->VacuumDelayTime = VacuumDelayTime;
counters->blocks_fetched = 0;
counters->blocks_hit = 0;
@@ -546,6 +552,9 @@ extvac_stats_end(Relation rel, LVExtStatCounters * counters,
{
WalUsage walusage;
BufferUsage bufusage;
+ TimestampTz endtime;
+ long secs;
+ int usecs;
if (!pgstat_track_vacuum_statistics)
return;
@@ -559,6 +568,9 @@ extvac_stats_end(Relation rel, LVExtStatCounters * counters,
memset(&bufusage, 0, sizeof(BufferUsage));
BufferUsageAccumDiff(&bufusage, &pgBufferUsage, &counters->bufusage);
+ endtime = GetCurrentTimestamp();
+ TimestampDifference(counters->starttime, endtime, &secs, &usecs);
+
/*
* Fill additional statistics on a vacuum processing operation.
*/
@@ -566,10 +578,19 @@ extvac_stats_end(Relation rel, LVExtStatCounters * counters,
report->total_blks_hit += bufusage.local_blks_hit + bufusage.shared_blks_hit;
report->total_blks_dirtied += bufusage.local_blks_dirtied + bufusage.shared_blks_dirtied;
report->total_blks_written += bufusage.shared_blks_written;
+
report->wal_records += walusage.wal_records;
report->wal_fpi += walusage.wal_fpi;
report->wal_bytes += walusage.wal_bytes;
+ report->blk_read_time += INSTR_TIME_GET_MILLISEC(bufusage.local_blk_read_time);
+ report->blk_read_time += INSTR_TIME_GET_MILLISEC(bufusage.shared_blk_read_time);
+ report->blk_write_time += INSTR_TIME_GET_MILLISEC(bufusage.local_blk_write_time);
+ report->blk_write_time += INSTR_TIME_GET_MILLISEC(bufusage.shared_blk_write_time);
+ report->delay_time += VacuumDelayTime - counters->VacuumDelayTime;
+
+ report->total_time += secs * 1000. + usecs / 1000.;
+
if (!rel->pgstat_info || !pgstat_track_counts)
/*
@@ -669,6 +690,8 @@ accumulate_heap_vacuum_statistics(LVRelState *vacrel, PgStat_VacuumRelationCount
extVacStats->table.missed_dead_pages = vacrel->missed_dead_pages;
extVacStats->common.wraparound_failsafe_count = vacrel->wraparound_failsafe;
+ extVacStats->common.blk_read_time -= vacrel->extVacReportIdx.common.blk_read_time;
+ extVacStats->common.blk_write_time -= vacrel->extVacReportIdx.common.blk_write_time;
extVacStats->common.total_blks_dirtied -= vacrel->extVacReportIdx.common.total_blks_dirtied;
extVacStats->common.total_blks_hit -= vacrel->extVacReportIdx.common.total_blks_hit;
extVacStats->common.total_blks_read -= vacrel->extVacReportIdx.common.total_blks_read;
@@ -676,12 +699,17 @@ accumulate_heap_vacuum_statistics(LVRelState *vacrel, PgStat_VacuumRelationCount
extVacStats->common.wal_bytes -= vacrel->extVacReportIdx.common.wal_bytes;
extVacStats->common.wal_fpi -= vacrel->extVacReportIdx.common.wal_fpi;
extVacStats->common.wal_records -= vacrel->extVacReportIdx.common.wal_records;
+
+ extVacStats->common.total_time -= vacrel->extVacReportIdx.common.total_time;
+ extVacStats->common.delay_time -= vacrel->extVacReportIdx.common.delay_time;
}
static void
accumulate_idxs_vacuum_statistics(LVRelState *vacrel, PgStat_VacuumRelationCounts * extVacIdxStats)
{
/* Fill heap-specific extended stats fields */
+ vacrel->extVacReportIdx.common.blk_read_time += extVacIdxStats->common.blk_read_time;
+ vacrel->extVacReportIdx.common.blk_write_time += extVacIdxStats->common.blk_write_time;
vacrel->extVacReportIdx.common.total_blks_dirtied += extVacIdxStats->common.total_blks_dirtied;
vacrel->extVacReportIdx.common.total_blks_hit += extVacIdxStats->common.total_blks_hit;
vacrel->extVacReportIdx.common.total_blks_read += extVacIdxStats->common.total_blks_read;
@@ -689,6 +717,8 @@ accumulate_idxs_vacuum_statistics(LVRelState *vacrel, PgStat_VacuumRelationCount
vacrel->extVacReportIdx.common.wal_bytes += extVacIdxStats->common.wal_bytes;
vacrel->extVacReportIdx.common.wal_fpi += extVacIdxStats->common.wal_fpi;
vacrel->extVacReportIdx.common.wal_records += extVacIdxStats->common.wal_records;
+ vacrel->extVacReportIdx.common.delay_time += extVacIdxStats->common.delay_time;
+ vacrel->extVacReportIdx.common.total_time += extVacIdxStats->common.total_time;
}
/*
@@ -716,6 +746,10 @@ extvac_accumulate_idx_report(PgStat_VacuumRelationCounts * dst,
dst->common.wal_records += src->common.wal_records;
dst->common.wal_fpi += src->common.wal_fpi;
dst->common.wal_bytes += src->common.wal_bytes;
+ dst->common.blk_read_time += src->common.blk_read_time;
+ dst->common.blk_write_time += src->common.blk_write_time;
+ dst->common.delay_time += src->common.delay_time;
+ dst->common.total_time += src->common.total_time;
dst->common.tuples_deleted += src->common.tuples_deleted;
dst->index.pages_deleted += src->index.pages_deleted;
@@ -904,7 +938,6 @@ heap_vacuum_rel(Relation rel, const VacuumParams *params,
Size dead_items_max_bytes = 0;
LVExtStatCounters extVacCounters;
PgStat_VacuumRelationCounts extVacReport;
- TimestampTz starttime;
/* Initialize vacuum statistics */
memset(&extVacReport, 0, sizeof(PgStat_VacuumRelationCounts));
@@ -922,7 +955,6 @@ heap_vacuum_rel(Relation rel, const VacuumParams *params,
}
}
- starttime = GetCurrentTimestamp();
extvac_stats_start(rel, &extVacCounters);
pgstat_progress_start_command(PROGRESS_COMMAND_VACUUM,
@@ -1279,7 +1311,7 @@ heap_vacuum_rel(Relation rel, const VacuumParams *params,
Max(vacrel->new_live_tuples, 0),
vacrel->recently_dead_tuples +
vacrel->missed_dead_tuples,
- starttime);
+ extVacCounters.starttime);
pgstat_progress_end_command();
if (instrument)
@@ -1287,7 +1319,7 @@ heap_vacuum_rel(Relation rel, const VacuumParams *params,
TimestampTz endtime = GetCurrentTimestamp();
if (verbose || params->log_vacuum_min_duration == 0 ||
- TimestampDifferenceExceeds(starttime, endtime,
+ TimestampDifferenceExceeds(extVacCounters.starttime, endtime,
params->log_vacuum_min_duration))
{
long secs_dur;
@@ -1303,7 +1335,7 @@ heap_vacuum_rel(Relation rel, const VacuumParams *params,
int64 total_blks_read;
int64 total_blks_dirtied;
- TimestampDifference(starttime, endtime, &secs_dur, &usecs_dur);
+ TimestampDifference(extVacCounters.starttime, endtime, &secs_dur, &usecs_dur);
memset(&walusage, 0, sizeof(WalUsage));
WalUsageAccumDiff(&walusage, &pgWalUsage, &startwalusage);
memset(&bufferusage, 0, sizeof(BufferUsage));
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index d0a58f221c..11029edae3 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -1582,6 +1582,10 @@ CREATE VIEW pg_stat_vacuum_tables AS
S.total_blks_written AS total_blks_written,
S.rel_blks_read AS rel_blks_read,
S.rel_blks_hit AS rel_blks_hit,
+ S.blk_read_time AS blk_read_time,
+ S.blk_write_time AS blk_write_time,
+ S.delay_time AS delay_time,
+ S.total_time AS total_time,
S.wraparound_failsafe AS wraparound_failsafe,
S.wal_records AS wal_records,
S.wal_fpi AS wal_fpi,
@@ -1607,8 +1611,15 @@ CREATE VIEW pg_stat_vacuum_indexes AS
S.total_blks_hit AS total_blks_hit,
S.total_blks_dirtied AS total_blks_dirtied,
S.total_blks_written AS total_blks_written,
+
S.rel_blks_read AS rel_blks_read,
S.rel_blks_hit AS rel_blks_hit,
+
+ S.blk_read_time AS blk_read_time,
+ S.blk_write_time AS blk_write_time,
+ S.delay_time AS delay_time,
+ S.total_time AS total_time,
+
S.wal_records AS wal_records,
S.wal_fpi AS wal_fpi,
S.wal_bytes AS wal_bytes
@@ -1631,7 +1642,14 @@ CREATE VIEW pg_stat_vacuum_database AS
S.db_blks_hit AS db_blks_hit,
S.total_blks_dirtied AS total_blks_dirtied,
S.total_blks_written AS total_blks_written,
+
S.wraparound_failsafe AS wraparound_failsafe,
+
+ S.blk_read_time AS blk_read_time,
+ S.blk_write_time AS blk_write_time,
+ S.delay_time AS delay_time,
+ S.total_time AS total_time,
+
S.wal_records AS wal_records,
S.wal_fpi AS wal_fpi,
S.wal_bytes AS wal_bytes,
diff --git a/src/backend/commands/vacuumparallel.c b/src/backend/commands/vacuumparallel.c
index ea13859485..563a1db912 100644
--- a/src/backend/commands/vacuumparallel.c
+++ b/src/backend/commands/vacuumparallel.c
@@ -1355,6 +1355,7 @@ parallel_vacuum_main(dsm_segment *seg, shm_toc *toc)
VacuumUpdateCosts();
VacuumCostBalance = 0;
+ VacuumDelayTime = 0;
VacuumCostBalanceLocal = 0;
VacuumSharedCostBalance = &(shared->cost_balance);
VacuumActiveNWorkers = &(shared->active_nworkers);
diff --git a/src/backend/utils/activity/pgstat_vacuum.c b/src/backend/utils/activity/pgstat_vacuum.c
index b1c747f255..c5ea644060 100644
--- a/src/backend/utils/activity/pgstat_vacuum.c
+++ b/src/backend/utils/activity/pgstat_vacuum.c
@@ -50,6 +50,11 @@ pgstat_accumulate_common(PgStat_CommonCounts *dst, const PgStat_CommonCounts *sr
ACCUMULATE_FIELD(wal_fpi);
ACCUMULATE_FIELD(wal_bytes);
+ ACCUMULATE_FIELD(blk_read_time);
+ ACCUMULATE_FIELD(blk_write_time);
+ ACCUMULATE_FIELD(delay_time);
+ ACCUMULATE_FIELD(total_time);
+
ACCUMULATE_FIELD(tuples_deleted);
ACCUMULATE_FIELD(interrupts_count);
}
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 7473ae1eb6..2d13604bd0 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -2374,7 +2374,7 @@ pg_stat_have_stats(PG_FUNCTION_ARGS)
Datum
pg_stat_get_vacuum_tables(PG_FUNCTION_ARGS)
{
-#define PG_STAT_GET_VACUUM_TABLES_STATS_COLS 21
+#define PG_STAT_GET_VACUUM_TABLES_STATS_COLS 25
Oid relid = PG_GETARG_OID(0);
PgStat_VacuumRelationCounts *extvacuum;
@@ -2417,6 +2417,10 @@ pg_stat_get_vacuum_tables(PG_FUNCTION_ARGS)
values[i++] = Int64GetDatum(extvacuum->common.total_blks_written);
values[i++] = Int64GetDatum(extvacuum->common.blks_fetched - extvacuum->common.blks_hit);
values[i++] = Int64GetDatum(extvacuum->common.blks_hit);
+ values[i++] = Float8GetDatum(extvacuum->common.blk_read_time);
+ values[i++] = Float8GetDatum(extvacuum->common.blk_write_time);
+ values[i++] = Float8GetDatum(extvacuum->common.delay_time);
+ values[i++] = Float8GetDatum(extvacuum->common.total_time);
values[i++] = Int32GetDatum(extvacuum->common.wraparound_failsafe_count);
values[i++] = Int64GetDatum(extvacuum->common.wal_records);
values[i++] = Int64GetDatum(extvacuum->common.wal_fpi);
@@ -2438,7 +2442,7 @@ pg_stat_get_vacuum_tables(PG_FUNCTION_ARGS)
Datum
pg_stat_get_vacuum_indexes(PG_FUNCTION_ARGS)
{
-#define PG_STAT_GET_VACUUM_INDEX_STATS_COLS 12
+#define PG_STAT_GET_VACUUM_INDEX_STATS_COLS 16
Oid relid = PG_GETARG_OID(0);
PgStat_VacuumRelationCounts *extvacuum;
@@ -2475,6 +2479,11 @@ pg_stat_get_vacuum_indexes(PG_FUNCTION_ARGS)
values[i++] = Int64GetDatum(extvacuum->common.blks_fetched - extvacuum->common.blks_hit);
values[i++] = Int64GetDatum(extvacuum->common.blks_hit);
+ values[i++] = Float8GetDatum(extvacuum->common.blk_read_time);
+ values[i++] = Float8GetDatum(extvacuum->common.blk_write_time);
+ values[i++] = Float8GetDatum(extvacuum->common.delay_time);
+ values[i++] = Float8GetDatum(extvacuum->common.total_time);
+
values[i++] = Int64GetDatum(extvacuum->common.wal_records);
values[i++] = Int64GetDatum(extvacuum->common.wal_fpi);
snprintf(buf, sizeof buf, UINT64_FORMAT, extvacuum->common.wal_bytes);
@@ -2482,6 +2491,7 @@ pg_stat_get_vacuum_indexes(PG_FUNCTION_ARGS)
CStringGetDatum(buf),
ObjectIdGetDatum(0),
Int32GetDatum(-1));
+
Assert(i == PG_STAT_GET_VACUUM_INDEX_STATS_COLS);
/* Returns the record as Datum */
@@ -2494,7 +2504,7 @@ pg_stat_get_vacuum_indexes(PG_FUNCTION_ARGS)
Datum
pg_stat_get_vacuum_database(PG_FUNCTION_ARGS)
{
-#define PG_STAT_GET_VACUUM_DATABASE_STATS_COLS 11
+#define PG_STAT_GET_VACUUM_DATABASE_STATS_COLS 15
Oid dbid = PG_GETARG_OID(0);
PgStat_VacuumDBCounts *extvacuum;
@@ -2525,6 +2535,11 @@ pg_stat_get_vacuum_database(PG_FUNCTION_ARGS)
values[i++] = Int32GetDatum(extvacuum->common.wraparound_failsafe_count);
+ values[i++] = Float8GetDatum(extvacuum->common.blk_read_time);
+ values[i++] = Float8GetDatum(extvacuum->common.blk_write_time);
+ values[i++] = Float8GetDatum(extvacuum->common.delay_time);
+ values[i++] = Float8GetDatum(extvacuum->common.total_time);
+
values[i++] = Int64GetDatum(extvacuum->common.wal_records);
values[i++] = Int64GetDatum(extvacuum->common.wal_fpi);
snprintf(buf, sizeof buf, UINT64_FORMAT, extvacuum->common.wal_bytes);
@@ -2533,6 +2548,7 @@ pg_stat_get_vacuum_database(PG_FUNCTION_ARGS)
ObjectIdGetDatum(0),
Int32GetDatum(-1));
values[i++] = Int32GetDatum(extvacuum->common.interrupts_count);
+
Assert(i == PG_STAT_GET_VACUUM_DATABASE_STATS_COLS);
/* Returns the record as Datum */
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 743ce9dfd6..e02d386944 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12643,9 +12643,9 @@
proname => 'pg_stat_get_vacuum_tables', prorows => 1000, provolatile => 's', prorettype => 'record', proisstrict => 'f',
proretset => 't',
proargtypes => 'oid',
- proallargtypes => '{oid,oid,int8,int8,int8,int8,int8,int8,int8,int8,int8,int8,int8,int8,int8,int8,int8,int8,int4,int8,int8,numeric}',
- proargmodes => '{i,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o}',
- proargnames => '{reloid,relid,pages_scanned,pages_removed,tuples_deleted,tuples_frozen,recently_dead_tuples,missed_dead_pages,missed_dead_tuples,vm_new_frozen_pages,vm_new_visible_pages,vm_new_visible_frozen_pages,total_blks_read,total_blks_hit,total_blks_dirtied,total_blks_written,rel_blks_read,rel_blks_hit,wraparound_failsafe,wal_records,wal_fpi,wal_bytes}',
+ proallargtypes => '{oid,oid,int8,int8,int8,int8,int8,int8,int8,int8,int8,int8,int8,int8,int8,int8,int8,int8,float8,float8,float8,float8,int4,int8,int8,numeric}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o}',
+ proargnames => '{reloid,relid,pages_scanned,pages_removed,tuples_deleted,tuples_frozen,recently_dead_tuples,missed_dead_pages,missed_dead_tuples,vm_new_frozen_pages,vm_new_visible_pages,vm_new_visible_frozen_pages,total_blks_read,total_blks_hit,total_blks_dirtied,total_blks_written,rel_blks_read,rel_blks_hit,blk_read_time,blk_write_time,delay_time,total_time,wraparound_failsafe,wal_records,wal_fpi,wal_bytes}',
prosrc => 'pg_stat_get_vacuum_tables' }
# oid8 related functions
@@ -12718,17 +12718,17 @@
proname => 'pg_stat_get_vacuum_indexes', prorows => 1000, provolatile => 's', prorettype => 'record', proisstrict => 'f',
proretset => 't',
proargtypes => 'oid',
- proallargtypes => '{oid,oid,int8,int8,int8,int8,int8,int8,int8,int8,int8,int8,numeric}',
- proargmodes => '{i,o,o,o,o,o,o,o,o,o,o,o,o}',
- proargnames => '{reloid,relid,pages_deleted,tuples_deleted,total_blks_read,total_blks_hit,total_blks_dirtied,total_blks_written,rel_blks_read,rel_blks_hit,wal_records,wal_fpi,wal_bytes}',
+ proallargtypes => '{oid,oid,int8,int8,int8,int8,int8,int8,int8,int8,float8,float8,float8,float8,int8,int8,numeric}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o}',
+ proargnames => '{reloid,relid,pages_deleted,tuples_deleted,total_blks_read,total_blks_hit,total_blks_dirtied,total_blks_written,rel_blks_read,rel_blks_hit,blk_read_time,blk_write_time,delay_time,total_time,wal_records,wal_fpi,wal_bytes}',
prosrc => 'pg_stat_get_vacuum_indexes' },
{ oid => '8005',
descr => 'pg_stat_get_vacuum_database returns vacuum stats values for database',
proname => 'pg_stat_get_vacuum_database', prorows => 1000, provolatile => 's', prorettype => 'record', proisstrict => 'f',
proretset => 't',
proargtypes => 'oid',
- proallargtypes => '{oid,oid,int4,int8,int8,int8,int8,int4,int8,int8,numeric,int4}',
- proargmodes => '{i,o,o,o,o,o,o,o,o,o,o,o}',
- proargnames => '{dbid,dboid,errors,db_blks_read,db_blks_hit,total_blks_dirtied,total_blks_written,wraparound_failsafe,wal_records,wal_fpi,wal_bytes,interrupts_count}',
+ proallargtypes => '{oid,oid,int4,int8,int8,int8,int8,int4,float8,float8,float8,float8,int8,int8,numeric,int4}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o}',
+ proargnames => '{dbid,dboid,errors,db_blks_read,db_blks_hit,total_blks_dirtied,total_blks_written,wraparound_failsafe,blk_read_time,blk_write_time,delay_time,total_time,wal_records,wal_fpi,wal_bytes,interrupts_count}',
prosrc => 'pg_stat_get_vacuum_database' },
]
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 18fa9ac5ba..6a9e87cf0b 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -190,6 +190,12 @@ typedef struct PgStat_CommonCounts
int64 wal_fpi;
uint64 wal_bytes;
+ /* Time */
+ double blk_read_time;
+ double blk_write_time;
+ double delay_time;
+ double total_time;
+
/* tuples */
int64 tuples_deleted;
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index c650cfd664..942361dd42 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -2429,12 +2429,16 @@ pg_stat_vacuum_database| SELECT d.oid AS dboid,
s.total_blks_dirtied,
s.total_blks_written,
s.wraparound_failsafe,
+ s.blk_read_time,
+ s.blk_write_time,
+ s.delay_time,
+ s.total_time,
s.wal_records,
s.wal_fpi,
s.wal_bytes,
s.interrupts_count
FROM pg_database d,
- LATERAL pg_stat_get_vacuum_database(d.oid) s(dboid, errors, db_blks_read, db_blks_hit, total_blks_dirtied, total_blks_written, wraparound_failsafe, wal_records, wal_fpi, wal_bytes, interrupts_count);
+ LATERAL pg_stat_get_vacuum_database(d.oid) s(dboid, errors, db_blks_read, db_blks_hit, total_blks_dirtied, total_blks_written, wraparound_failsafe, blk_read_time, blk_write_time, delay_time, total_time, wal_records, wal_fpi, wal_bytes, interrupts_count);
pg_stat_vacuum_indexes| SELECT c.oid AS relid,
i.oid AS indexrelid,
n.nspname AS schemaname,
@@ -2448,6 +2452,10 @@ pg_stat_vacuum_indexes| SELECT c.oid AS relid,
s.total_blks_written,
s.rel_blks_read,
s.rel_blks_hit,
+ s.blk_read_time,
+ s.blk_write_time,
+ s.delay_time,
+ s.total_time,
s.wal_records,
s.wal_fpi,
s.wal_bytes
@@ -2455,7 +2463,7 @@ pg_stat_vacuum_indexes| SELECT c.oid AS relid,
JOIN pg_index x ON ((c.oid = x.indrelid)))
JOIN pg_class i ON ((i.oid = x.indexrelid)))
LEFT JOIN pg_namespace n ON ((n.oid = c.relnamespace))),
- LATERAL pg_stat_get_vacuum_indexes(i.oid) s(relid, pages_deleted, tuples_deleted, total_blks_read, total_blks_hit, total_blks_dirtied, total_blks_written, rel_blks_read, rel_blks_hit, wal_records, wal_fpi, wal_bytes)
+ LATERAL pg_stat_get_vacuum_indexes(i.oid) s(relid, pages_deleted, tuples_deleted, total_blks_read, total_blks_hit, total_blks_dirtied, total_blks_written, rel_blks_read, rel_blks_hit, blk_read_time, blk_write_time, delay_time, total_time, wal_records, wal_fpi, wal_bytes)
WHERE (c.relkind = ANY (ARRAY['r'::"char", 't'::"char", 'm'::"char"]));
pg_stat_vacuum_tables| SELECT n.nspname AS schemaname,
c.relname,
@@ -2476,13 +2484,17 @@ pg_stat_vacuum_tables| SELECT n.nspname AS schemaname,
s.total_blks_written,
s.rel_blks_read,
s.rel_blks_hit,
+ s.blk_read_time,
+ s.blk_write_time,
+ s.delay_time,
+ s.total_time,
s.wraparound_failsafe,
s.wal_records,
s.wal_fpi,
s.wal_bytes
FROM (pg_class c
JOIN pg_namespace n ON ((n.oid = c.relnamespace))),
- LATERAL pg_stat_get_vacuum_tables(c.oid) s(relid, pages_scanned, pages_removed, tuples_deleted, tuples_frozen, recently_dead_tuples, missed_dead_pages, missed_dead_tuples, vm_new_frozen_pages, vm_new_visible_pages, vm_new_visible_frozen_pages, total_blks_read, total_blks_hit, total_blks_dirtied, total_blks_written, rel_blks_read, rel_blks_hit, wraparound_failsafe, wal_records, wal_fpi, wal_bytes)
+ LATERAL pg_stat_get_vacuum_tables(c.oid) s(relid, pages_scanned, pages_removed, tuples_deleted, tuples_frozen, recently_dead_tuples, missed_dead_pages, missed_dead_tuples, vm_new_frozen_pages, vm_new_visible_pages, vm_new_visible_frozen_pages, total_blks_read, total_blks_hit, total_blks_dirtied, total_blks_written, rel_blks_read, rel_blks_hit, blk_read_time, blk_write_time, delay_time, total_time, wraparound_failsafe, wal_records, wal_fpi, wal_bytes)
WHERE (c.relkind = ANY (ARRAY['r'::"char", 't'::"char", 'm'::"char"]));
pg_stat_wal| SELECT wal_records,
wal_fpi,
diff --git a/src/test/regress/expected/vacuum_stats.out b/src/test/regress/expected/vacuum_stats.out
index 3d823ef7b6..bd3c1512d8 100644
--- a/src/test/regress/expected/vacuum_stats.out
+++ b/src/test/regress/expected/vacuum_stats.out
@@ -148,6 +148,49 @@ SELECT rel_blks_read >= 0 AS rel_blks_read,
t | t
(1 row)
+-- timing metrics and failsafe. The vacuum always takes some wall-clock time
+-- (total_time > 0) and does not engage the wraparound failsafe under normal
+-- conditions (wraparound_failsafe = 0). blk_read_time/blk_write_time are only
+-- non-zero when track_io_timing is enabled, and delay_time only when a vacuum
+-- cost delay is configured, so those are merely checked for being
+-- non-negative; the positive delay_time path is exercised separately below.
+SELECT blk_read_time >= 0 AS blk_read_time,
+ blk_write_time >= 0 AS blk_write_time,
+ delay_time >= 0 AS delay_time,
+ total_time > 0 AS total_time,
+ wraparound_failsafe = 0 AS wraparound_failsafe
+ FROM pg_stat_vacuum_tables WHERE relname = 'vacstat_t';
+ blk_read_time | blk_write_time | delay_time | total_time | wraparound_failsafe
+---------------+----------------+------------+------------+---------------------
+ t | t | t | t | t
+(1 row)
+
+-- delay path: with a vacuum cost delay configured, a vacuum that accrues cost
+-- sleeps, so delay_time advances.
+CREATE TABLE vacstat_delay (id int PRIMARY KEY, v text)
+ WITH (autovacuum_enabled = off, fillfactor = 10);
+INSERT INTO vacstat_delay SELECT g, repeat('x', 100) FROM generate_series(1, 3000) g;
+DELETE FROM vacstat_delay WHERE id % 2 = 0;
+SET vacuum_cost_delay = '1ms';
+SET vacuum_cost_limit = 1;
+VACUUM vacstat_delay;
+SELECT pg_stat_force_next_flush();
+ pg_stat_force_next_flush
+--------------------------
+
+(1 row)
+
+RESET vacuum_cost_delay;
+RESET vacuum_cost_limit;
+SELECT delay_time > 0 AS delay_time,
+ total_time > 0 AS total_time
+ FROM pg_stat_vacuum_tables WHERE relname = 'vacstat_delay';
+ delay_time | total_time
+------------+------------
+ t | t
+(1 row)
+
+DROP TABLE vacstat_delay;
-- WAL metrics. A vacuum that removes tuples always emits WAL
-- (wal_records > 0, wal_bytes > 0). wal_fpi depends on whether a checkpoint
-- happened recently, so it is only checked for being non-negative here; the
@@ -186,17 +229,6 @@ SELECT wal_records > 0 AS wal_records,
(1 row)
DROP TABLE vacstat_fpi;
--- wraparound failsafe. A normal vacuum does not engage the wraparound
--- failsafe (wraparound_failsafe = 0). The positive path requires reaching the
--- failsafe XID age and is covered by a separate TAP test under
--- src/test/modules/xid_wraparound.
-SELECT wraparound_failsafe = 0 AS wraparound_failsafe
- FROM pg_stat_vacuum_tables WHERE relname = 'vacstat_t';
- wraparound_failsafe
----------------------
- t
-(1 row)
-
-- per-index view: the primary key index is processed by the same VACUUM.
-- No btree leaf empties out (interleaved deletions), so pages_deleted = 0,
-- while every index entry for a removed heap tuple is deleted. The index is
@@ -211,13 +243,17 @@ SELECT indexrelname,
total_blks_written >= 0 AS total_blks_written,
rel_blks_read >= 0 AS rel_blks_read,
rel_blks_hit > 0 AS rel_blks_hit,
+ blk_read_time >= 0 AS blk_read_time,
+ blk_write_time >= 0 AS blk_write_time,
+ delay_time >= 0 AS delay_time,
+ total_time > 0 AS total_time,
wal_records > 0 AS wal_records,
wal_fpi >= 0 AS wal_fpi,
wal_bytes > 0 AS wal_bytes
FROM pg_stat_vacuum_indexes WHERE relname = 'vacstat_t' ORDER BY indexrelname;
- indexrelname | pages_deleted | tuples_deleted | total_blks_read | total_blks_hit | total_blks_dirtied | total_blks_written | rel_blks_read | rel_blks_hit | wal_records | wal_fpi | wal_bytes
-----------------+---------------+----------------+-----------------+----------------+--------------------+--------------------+---------------+--------------+-------------+---------+-----------
- vacstat_t_pkey | t | t | t | t | t | t | t | t | t | t | t
+ indexrelname | pages_deleted | tuples_deleted | total_blks_read | total_blks_hit | total_blks_dirtied | total_blks_written | rel_blks_read | rel_blks_hit | blk_read_time | blk_write_time | delay_time | total_time | wal_records | wal_fpi | wal_bytes
+----------------+---------------+----------------+-----------------+----------------+--------------------+--------------------+---------------+--------------+---------------+----------------+------------+------------+-------------+---------+-----------
+ vacstat_t_pkey | t | t | t | t | t | t | t | t | t | t | t | t | t | t | t
(1 row)
-- index page-deletion path: deleting a contiguous key range empties whole
@@ -253,20 +289,24 @@ SELECT indexrelname,
DROP TABLE vacstat_idxdel;
-- per-database aggregate view: no vacuum errors occurred in this database, and
-- the vacuums in this database touched pages through the buffer cache
--- (db_blks_hit > 0) and emit WAL (wal_records > 0).
+-- (db_blks_hit > 0).
SELECT errors = 0 AS errors,
db_blks_read >= 0 AS db_blks_read,
db_blks_hit > 0 AS db_blks_hit,
total_blks_dirtied >= 0 AS total_blks_dirtied,
total_blks_written >= 0 AS total_blks_written,
wraparound_failsafe = 0 AS wraparound_failsafe,
+ blk_read_time >= 0 AS blk_read_time,
+ blk_write_time >= 0 AS blk_write_time,
+ delay_time >= 0 AS delay_time,
+ total_time > 0 AS total_time,
wal_records > 0 AS wal_records,
wal_fpi >= 0 AS wal_fpi,
wal_bytes > 0 AS wal_bytes
FROM pg_stat_vacuum_database WHERE dbname = current_database();
- errors | db_blks_read | db_blks_hit | total_blks_dirtied | total_blks_written | wraparound_failsafe | wal_records | wal_fpi | wal_bytes
---------+--------------+-------------+--------------------+--------------------+---------------------+-------------+---------+-----------
- t | t | t | t | t | t | t | t | t
+ errors | db_blks_read | db_blks_hit | total_blks_dirtied | total_blks_written | wraparound_failsafe | blk_read_time | blk_write_time | delay_time | total_time | wal_records | wal_fpi | wal_bytes
+--------+--------------+-------------+--------------------+--------------------+---------------------+---------------+----------------+------------+------------+-------------+---------+-----------
+ t | t | t | t | t | t | t | t | t | t | t | t | t
(1 row)
-- parallel index vacuum: index statistics must be captured for indexes
diff --git a/src/test/regress/sql/vacuum_stats.sql b/src/test/regress/sql/vacuum_stats.sql
index 40f7d0985d..baf5acdafd 100644
--- a/src/test/regress/sql/vacuum_stats.sql
+++ b/src/test/regress/sql/vacuum_stats.sql
@@ -98,6 +98,36 @@ SELECT rel_blks_read >= 0 AS rel_blks_read,
rel_blks_hit > 0 AS rel_blks_hit
FROM pg_stat_vacuum_tables WHERE relname = 'vacstat_t';
+-- timing metrics and failsafe. The vacuum always takes some wall-clock time
+-- (total_time > 0) and does not engage the wraparound failsafe under normal
+-- conditions (wraparound_failsafe = 0). blk_read_time/blk_write_time are only
+-- non-zero when track_io_timing is enabled, and delay_time only when a vacuum
+-- cost delay is configured, so those are merely checked for being
+-- non-negative; the positive delay_time path is exercised separately below.
+SELECT blk_read_time >= 0 AS blk_read_time,
+ blk_write_time >= 0 AS blk_write_time,
+ delay_time >= 0 AS delay_time,
+ total_time > 0 AS total_time,
+ wraparound_failsafe = 0 AS wraparound_failsafe
+ FROM pg_stat_vacuum_tables WHERE relname = 'vacstat_t';
+
+-- delay path: with a vacuum cost delay configured, a vacuum that accrues cost
+-- sleeps, so delay_time advances.
+CREATE TABLE vacstat_delay (id int PRIMARY KEY, v text)
+ WITH (autovacuum_enabled = off, fillfactor = 10);
+INSERT INTO vacstat_delay SELECT g, repeat('x', 100) FROM generate_series(1, 3000) g;
+DELETE FROM vacstat_delay WHERE id % 2 = 0;
+SET vacuum_cost_delay = '1ms';
+SET vacuum_cost_limit = 1;
+VACUUM vacstat_delay;
+SELECT pg_stat_force_next_flush();
+RESET vacuum_cost_delay;
+RESET vacuum_cost_limit;
+SELECT delay_time > 0 AS delay_time,
+ total_time > 0 AS total_time
+ FROM pg_stat_vacuum_tables WHERE relname = 'vacstat_delay';
+DROP TABLE vacstat_delay;
+
-- WAL metrics. A vacuum that removes tuples always emits WAL
-- (wal_records > 0, wal_bytes > 0). wal_fpi depends on whether a checkpoint
-- happened recently, so it is only checked for being non-negative here; the
@@ -123,13 +153,6 @@ SELECT wal_records > 0 AS wal_records,
FROM pg_stat_vacuum_tables WHERE relname = 'vacstat_fpi';
DROP TABLE vacstat_fpi;
--- wraparound failsafe. A normal vacuum does not engage the wraparound
--- failsafe (wraparound_failsafe = 0). The positive path requires reaching the
--- failsafe XID age and is covered by a separate TAP test under
--- src/test/modules/xid_wraparound.
-SELECT wraparound_failsafe = 0 AS wraparound_failsafe
- FROM pg_stat_vacuum_tables WHERE relname = 'vacstat_t';
-
-- per-index view: the primary key index is processed by the same VACUUM.
-- No btree leaf empties out (interleaved deletions), so pages_deleted = 0,
-- while every index entry for a removed heap tuple is deleted. The index is
@@ -144,6 +167,10 @@ SELECT indexrelname,
total_blks_written >= 0 AS total_blks_written,
rel_blks_read >= 0 AS rel_blks_read,
rel_blks_hit > 0 AS rel_blks_hit,
+ blk_read_time >= 0 AS blk_read_time,
+ blk_write_time >= 0 AS blk_write_time,
+ delay_time >= 0 AS delay_time,
+ total_time > 0 AS total_time,
wal_records > 0 AS wal_records,
wal_fpi >= 0 AS wal_fpi,
wal_bytes > 0 AS wal_bytes
@@ -168,13 +195,17 @@ DROP TABLE vacstat_idxdel;
-- per-database aggregate view: no vacuum errors occurred in this database, and
-- the vacuums in this database touched pages through the buffer cache
--- (db_blks_hit > 0) and emit WAL (wal_records > 0).
+-- (db_blks_hit > 0).
SELECT errors = 0 AS errors,
db_blks_read >= 0 AS db_blks_read,
db_blks_hit > 0 AS db_blks_hit,
total_blks_dirtied >= 0 AS total_blks_dirtied,
total_blks_written >= 0 AS total_blks_written,
wraparound_failsafe = 0 AS wraparound_failsafe,
+ blk_read_time >= 0 AS blk_read_time,
+ blk_write_time >= 0 AS blk_write_time,
+ delay_time >= 0 AS delay_time,
+ total_time > 0 AS total_time,
wal_records > 0 AS wal_records,
wal_fpi >= 0 AS wal_fpi,
wal_bytes > 0 AS wal_bytes
--
2.39.5 (Apple Git-154)
view thread (78+ messages)
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