From 186be14397e81152e1ae2efc3d8eef4bf7fe0e80 Mon Sep 17 00:00:00 2001 From: Alena Rybakina Date: Wed, 22 Jul 2026 22:57:35 +0300 Subject: [PATCH v46 6/9] ext_vacuum_statistics: page counters for tables and indexes. Add the page-level counters to the vacuum report and the extension views: * pages_scanned - heap pages inspected by the vacuum; * pages_removed - pages by which the table storage was truncated at the end of the vacuum; * missed_dead_pages - pages containing fully dead tuples that could not be pruned because the cleanup lock on the page was not acquired; * pages_deleted (indexes only) - index pages marked deleted by the vacuum, derived per bulkdelete/cleanup pass, including passes executed in parallel workers. Authors: Alena Rybakina , Andrei Lepikhov , Andrei Zubkov --- .../vacuum-extending-in-repetable-read.out | 22 +++--- .../ext_vacuum_statistics--1.0.sql | 12 ++- contrib/ext_vacuum_statistics/meson.build | 1 + .../vacuum-extending-in-repetable-read.spec | 2 +- .../t/053_index_pages_deleted.pl | 78 +++++++++++++++++++ .../t/054_vacuum_extending_gucs_test.pl | 2 +- .../ext_vacuum_statistics/vacuum_statistics.c | 15 +++- doc/src/sgml/extvacuumstatistics.sgml | 34 ++++++++ src/backend/access/heap/vacuumlazy.c | 23 ++++++ src/backend/commands/vacuumparallel.c | 10 +++ src/include/pgstat.h | 9 ++- 11 files changed, 190 insertions(+), 18 deletions(-) create mode 100644 contrib/ext_vacuum_statistics/t/053_index_pages_deleted.pl diff --git a/contrib/ext_vacuum_statistics/expected/vacuum-extending-in-repetable-read.out b/contrib/ext_vacuum_statistics/expected/vacuum-extending-in-repetable-read.out index f8962fad32..6b381f9d23 100644 --- a/contrib/ext_vacuum_statistics/expected/vacuum-extending-in-repetable-read.out +++ b/contrib/ext_vacuum_statistics/expected/vacuum-extending-in-repetable-read.out @@ -5,12 +5,12 @@ starting permutation: s2_insert s2_print_vacuum_stats_table s1_begin_repeatable_ step s2_insert: INSERT INTO test_vacuum_stat_isolation(id, ival) SELECT ival, ival%10 FROM generate_series(1,1000) As ival; step s2_print_vacuum_stats_table: SELECT - vt.relname, vt.tuples_deleted, vt.recently_dead_tuples, vt.missed_dead_tuples, vt.tuples_frozen + vt.relname, vt.tuples_deleted, vt.recently_dead_tuples, vt.missed_dead_tuples, vt.missed_dead_pages, vt.tuples_frozen FROM ext_vacuum_statistics.pg_stats_vacuum_tables vt, pg_class c WHERE vt.relname = 'test_vacuum_stat_isolation' AND vt.relid = c.oid; -relname|tuples_deleted|recently_dead_tuples|missed_dead_tuples|tuples_frozen --------+--------------+--------------------+------------------+------------- +relname|tuples_deleted|recently_dead_tuples|missed_dead_tuples|missed_dead_pages|tuples_frozen +-------+--------------+--------------------+------------------+-----------------+------------- (0 rows) step s1_begin_repeatable_read: @@ -27,13 +27,13 @@ step s2_insert_interrupt: INSERT INTO test_vacuum_stat_isolation values (1,1); step s2_vacuum: VACUUM test_vacuum_stat_isolation; step s2_print_vacuum_stats_table: SELECT - vt.relname, vt.tuples_deleted, vt.recently_dead_tuples, vt.missed_dead_tuples, vt.tuples_frozen + vt.relname, vt.tuples_deleted, vt.recently_dead_tuples, vt.missed_dead_tuples, vt.missed_dead_pages, vt.tuples_frozen FROM ext_vacuum_statistics.pg_stats_vacuum_tables vt, pg_class c WHERE vt.relname = 'test_vacuum_stat_isolation' AND vt.relid = c.oid; -relname |tuples_deleted|recently_dead_tuples|missed_dead_tuples|tuples_frozen ---------------------------+--------------+--------------------+------------------+------------- -test_vacuum_stat_isolation| 0| 100| 0| 0 +relname |tuples_deleted|recently_dead_tuples|missed_dead_tuples|missed_dead_pages|tuples_frozen +--------------------------+--------------+--------------------+------------------+-----------------+------------- +test_vacuum_stat_isolation| 0| 100| 0| 0| 0 (1 row) step s1_commit: COMMIT; @@ -41,12 +41,12 @@ step s2_checkpoint: CHECKPOINT; step s2_vacuum: VACUUM test_vacuum_stat_isolation; step s2_print_vacuum_stats_table: SELECT - vt.relname, vt.tuples_deleted, vt.recently_dead_tuples, vt.missed_dead_tuples, vt.tuples_frozen + vt.relname, vt.tuples_deleted, vt.recently_dead_tuples, vt.missed_dead_tuples, vt.missed_dead_pages, vt.tuples_frozen FROM ext_vacuum_statistics.pg_stats_vacuum_tables vt, pg_class c WHERE vt.relname = 'test_vacuum_stat_isolation' AND vt.relid = c.oid; -relname |tuples_deleted|recently_dead_tuples|missed_dead_tuples|tuples_frozen ---------------------------+--------------+--------------------+------------------+------------- -test_vacuum_stat_isolation| 100| 100| 0| 101 +relname |tuples_deleted|recently_dead_tuples|missed_dead_tuples|missed_dead_pages|tuples_frozen +--------------------------+--------------+--------------------+------------------+-----------------+------------- +test_vacuum_stat_isolation| 100| 100| 0| 0| 101 (1 row) diff --git a/contrib/ext_vacuum_statistics/ext_vacuum_statistics--1.0.sql b/contrib/ext_vacuum_statistics/ext_vacuum_statistics--1.0.sql index 45ebb0335f..28b8dcad9e 100644 --- a/contrib/ext_vacuum_statistics/ext_vacuum_statistics--1.0.sql +++ b/contrib/ext_vacuum_statistics/ext_vacuum_statistics--1.0.sql @@ -39,8 +39,11 @@ CREATE OR REPLACE FUNCTION ext_vacuum_statistics.pg_stats_get_vacuum_tables( IN reloid oid, OUT relid oid, OUT tuples_deleted bigint, + OUT pages_scanned bigint, + OUT pages_removed bigint, OUT tuples_frozen bigint, OUT recently_dead_tuples bigint, + OUT missed_dead_pages bigint, OUT missed_dead_tuples bigint ) RETURNS SETOF record @@ -52,7 +55,8 @@ CREATE OR REPLACE FUNCTION ext_vacuum_statistics.pg_stats_get_vacuum_indexes( IN dboid oid, IN reloid oid, OUT relid oid, - OUT tuples_deleted bigint + OUT tuples_deleted bigint, + OUT pages_deleted bigint ) RETURNS SETOF record AS 'MODULE_PATHNAME', 'pg_stats_get_vacuum_indexes' @@ -66,8 +70,11 @@ SELECT rel.relname AS relname, db.datname AS dbname, stats.tuples_deleted, + stats.pages_scanned, + stats.pages_removed, stats.tuples_frozen, stats.recently_dead_tuples, + stats.missed_dead_pages, stats.missed_dead_tuples FROM pg_database db, pg_class rel, @@ -88,7 +95,8 @@ SELECT ns.nspname AS schema, rel.relname AS indexrelname, db.datname AS dbname, - stats.tuples_deleted + stats.tuples_deleted, + stats.pages_deleted FROM pg_database db, pg_class rel, pg_namespace ns, diff --git a/contrib/ext_vacuum_statistics/meson.build b/contrib/ext_vacuum_statistics/meson.build index 85eb2d6b85..7b1baf569b 100644 --- a/contrib/ext_vacuum_statistics/meson.build +++ b/contrib/ext_vacuum_statistics/meson.build @@ -34,6 +34,7 @@ tests += { }, 'tap': { 'tests': [ + 't/053_index_pages_deleted.pl', 't/054_vacuum_extending_gucs_test.pl', 't/056_vacuum_stats_gc.pl', ], diff --git a/contrib/ext_vacuum_statistics/specs/vacuum-extending-in-repetable-read.spec b/contrib/ext_vacuum_statistics/specs/vacuum-extending-in-repetable-read.spec index 3001b56ac2..4891e248cc 100644 --- a/contrib/ext_vacuum_statistics/specs/vacuum-extending-in-repetable-read.spec +++ b/contrib/ext_vacuum_statistics/specs/vacuum-extending-in-repetable-read.spec @@ -40,7 +40,7 @@ step s2_checkpoint { CHECKPOINT; } step s2_print_vacuum_stats_table { SELECT - vt.relname, vt.tuples_deleted, vt.recently_dead_tuples, vt.missed_dead_tuples, vt.tuples_frozen + vt.relname, vt.tuples_deleted, vt.recently_dead_tuples, vt.missed_dead_tuples, vt.missed_dead_pages, vt.tuples_frozen FROM ext_vacuum_statistics.pg_stats_vacuum_tables vt, pg_class c WHERE vt.relname = 'test_vacuum_stat_isolation' AND vt.relid = c.oid; } diff --git a/contrib/ext_vacuum_statistics/t/053_index_pages_deleted.pl b/contrib/ext_vacuum_statistics/t/053_index_pages_deleted.pl new file mode 100644 index 0000000000..d14c901b53 --- /dev/null +++ b/contrib/ext_vacuum_statistics/t/053_index_pages_deleted.pl @@ -0,0 +1,78 @@ +# Copyright (c) 2026, PostgreSQL Global Development Group + +# Count pages newly deleted by each vacuum, without recounting pages that +# were already deleted. Exercise serial and parallel bulkdelete and cleanup. +use strict; +use warnings FATAL => 'all'; +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +my $node = PostgreSQL::Test::Cluster->new('index_pages'); +$node->init; +$node->append_conf('postgresql.conf', q{ +shared_preload_libraries = 'ext_vacuum_statistics' +autovacuum = off +max_parallel_maintenance_workers = 1 +min_parallel_index_scan_size = 0 +}); +$node->start; +$node->safe_psql('postgres', q{ +CREATE EXTENSION ext_vacuum_statistics; +CREATE TABLE pages_test (id integer, other integer) + WITH (autovacuum_enabled = off); +CREATE INDEX pages_test_id ON pages_test (id); +CREATE INDEX pages_test_other ON pages_test (other); +}); + +for my $parallel (0, 1) +{ + $node->safe_psql('postgres', q{ +TRUNCATE pages_test; +INSERT INTO pages_test SELECT i, -i FROM generate_series(1, 100000) i; +DELETE FROM pages_test WHERE id < 90000; +}); + + for my $pass (1, 2) + { + $node->safe_psql('postgres', + 'SELECT ext_vacuum_statistics.vacuum_statistics_reset()'); + my ($stdout, $stderr) = ('', ''); + my $label = "parallel=$parallel, pass=$pass"; + is($node->psql('postgres', qq{ +VACUUM (VERBOSE, DISABLE_PAGE_SKIPPING, INDEX_CLEANUP ON, + PARALLEL $parallel, TRUNCATE OFF) pages_test; +}, stdout => \$stdout, stderr => \$stderr), 0, "$label vacuum succeeds"); + if ($parallel) + { + like($stderr, qr/launched 1 parallel vacuum worker/, + "$label uses a parallel worker"); + } + + for my $index ('pages_test_id', 'pages_test_other') + { + my ($newly_deleted, $currently_deleted) = $stderr =~ + /index "$index":.*?pages: \d+ in total, (\d+) newly deleted, (\d+) currently deleted/; + ok(defined $newly_deleted, "$label $index instrumentation available"); + cmp_ok($currently_deleted, '>', 0, + "$label $index has deleted pages"); + if ($pass == 1) + { + cmp_ok($newly_deleted, '>', 0, + "$label $index deletes new pages"); + } + else + { + is($newly_deleted, '0', + "$label $index deletes no additional pages"); + } + is($node->safe_psql('postgres', qq{ +SELECT pages_deleted FROM ext_vacuum_statistics.pg_stats_vacuum_indexes +WHERE indexrelname = '$index'; +}), $newly_deleted, "$label $index reports only newly deleted pages"); + } + } +} + +$node->stop; +done_testing(); diff --git a/contrib/ext_vacuum_statistics/t/054_vacuum_extending_gucs_test.pl b/contrib/ext_vacuum_statistics/t/054_vacuum_extending_gucs_test.pl index 4dc4336433..345e6a3f96 100644 --- a/contrib/ext_vacuum_statistics/t/054_vacuum_extending_gucs_test.pl +++ b/contrib/ext_vacuum_statistics/t/054_vacuum_extending_gucs_test.pl @@ -88,7 +88,7 @@ subtest 'vacuum_statistics.enabled' => sub { my $sums = $node->safe_psql($dbname, q{ SELECT COALESCE(SUM(tuples_deleted), 0) - + COALESCE(SUM(tuples_frozen), 0) + + COALESCE(SUM(pages_scanned), 0) FROM ext_vacuum_statistics.pg_stats_vacuum_tables WHERE relname = 'guc_test' }); diff --git a/contrib/ext_vacuum_statistics/vacuum_statistics.c b/contrib/ext_vacuum_statistics/vacuum_statistics.c index 2aa3c05426..7b4cc9b533 100644 --- a/contrib/ext_vacuum_statistics/vacuum_statistics.c +++ b/contrib/ext_vacuum_statistics/vacuum_statistics.c @@ -83,10 +83,17 @@ pgstat_accumulate_extvac_stats(PgStat_VacuumRelationCounts * dst, if (dst->type == PGSTAT_EXTVAC_TABLE) { + dst->table.pages_scanned += src->table.pages_scanned; + dst->table.pages_removed += src->table.pages_removed; dst->table.tuples_frozen += src->table.tuples_frozen; dst->table.recently_dead_tuples += src->table.recently_dead_tuples; + dst->table.missed_dead_pages += src->table.missed_dead_pages; dst->table.missed_dead_tuples += src->table.missed_dead_tuples; } + else if (dst->type == PGSTAT_EXTVAC_INDEX) + { + dst->index.pages_deleted += src->index.pages_deleted; + } } void @@ -236,8 +243,8 @@ extvac_reset_entry(PG_FUNCTION_ARGS) /* * Output vacuum statistics (tables or indexes). */ -#define EXTVAC_HEAP_STAT_COLS 5 -#define EXTVAC_IDX_STAT_COLS 2 +#define EXTVAC_HEAP_STAT_COLS 8 +#define EXTVAC_IDX_STAT_COLS 3 #define EXTVAC_MAX_STAT_COLS Max(EXTVAC_HEAP_STAT_COLS, EXTVAC_IDX_STAT_COLS) static void @@ -254,13 +261,17 @@ tuplestore_put_for_relation(Oid relid, Tuplestorestate *tupstore, if (vacuum_ext->type == PGSTAT_EXTVAC_TABLE) { values[i++] = Int64GetDatum(vacuum_ext->common.tuples_deleted); + values[i++] = Int64GetDatum(vacuum_ext->table.pages_scanned); + values[i++] = Int64GetDatum(vacuum_ext->table.pages_removed); values[i++] = Int64GetDatum(vacuum_ext->table.tuples_frozen); values[i++] = Int64GetDatum(vacuum_ext->table.recently_dead_tuples); + values[i++] = Int64GetDatum(vacuum_ext->table.missed_dead_pages); values[i++] = Int64GetDatum(vacuum_ext->table.missed_dead_tuples); } else if (vacuum_ext->type == PGSTAT_EXTVAC_INDEX) { values[i++] = Int64GetDatum(vacuum_ext->common.tuples_deleted); + values[i++] = Int64GetDatum(vacuum_ext->index.pages_deleted); } Assert(i == ((vacuum_ext->type == PGSTAT_EXTVAC_TABLE) ? EXTVAC_HEAP_STAT_COLS : EXTVAC_IDX_STAT_COLS)); diff --git a/doc/src/sgml/extvacuumstatistics.sgml b/doc/src/sgml/extvacuumstatistics.sgml index de0c68e932..7b3b486d29 100644 --- a/doc/src/sgml/extvacuumstatistics.sgml +++ b/doc/src/sgml/extvacuumstatistics.sgml @@ -116,6 +116,22 @@ Number of dead tuples vacuum operations deleted from this table + + + pages_scanned int8 + + + Number of pages examined by vacuum operations + + + + + pages_removed int8 + + + Number of pages removed from physical storage by vacuum operations + + tuples_frozen int8 @@ -132,6 +148,14 @@ Number of dead tuples left due to visibility in transactions + + + missed_dead_pages int8 + + + Number of pages that had at least one missed dead tuple + + missed_dead_tuples int8 @@ -214,6 +238,16 @@ of the parent table + + + pages_deleted int8 + + + Cumulative number of index pages newly marked deleted by vacuum + operations. Pages already deleted by an earlier vacuum are not counted + again. + + diff --git a/src/backend/access/heap/vacuumlazy.c b/src/backend/access/heap/vacuumlazy.c index 2aa5f4b2c6..92c95a34a7 100644 --- a/src/backend/access/heap/vacuumlazy.c +++ b/src/backend/access/heap/vacuumlazy.c @@ -504,10 +504,13 @@ static void accumulate_heap_vacuum_statistics(LVRelState *vacrel, PgStat_VacuumRelationCounts * extVacStats) { extVacStats->type = PGSTAT_EXTVAC_TABLE; + extVacStats->table.pages_scanned = vacrel->scanned_pages; + extVacStats->table.pages_removed = vacrel->removed_pages; extVacStats->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; /* * The wraparound failsafe is reported as a per-relation flag (0/1): did @@ -3104,6 +3107,7 @@ lazy_vacuum_one_index(Relation indrel, IndexBulkDeleteResult *istat, TimestampTz istarttime = GetCurrentTimestamp(); double startdelaytime = VacuumDelayTime; double prev_tuples_removed = 0; + BlockNumber prev_pages_newly_deleted = 0; PgStat_VacuumRelationCounts extVacReport; const int reset_index[] = { PROGRESS_VACUUM_CURRENT_INDEX_RELID, @@ -3115,9 +3119,14 @@ lazy_vacuum_one_index(Relation indrel, IndexBulkDeleteResult *istat, /* * Snapshot the running bulkdelete totals: an index may be processed * several times per vacuum, and the report below covers this pass only. + * pages_deleted also includes pages deleted by earlier vacuums, so use + * pages_newly_deleted, which accumulates only this vacuum's deletions. */ if (istat != NULL) + { prev_tuples_removed = istat->tuples_removed; + prev_pages_newly_deleted = istat->pages_newly_deleted; + } ivinfo.index = indrel; ivinfo.heaprel = vacrel->rel; ivinfo.analyze_only = false; @@ -3162,8 +3171,12 @@ lazy_vacuum_one_index(Relation indrel, IndexBulkDeleteResult *istat, memset(&extVacReport, 0, sizeof(extVacReport)); extVacReport.type = PGSTAT_EXTVAC_INDEX; if (istat != NULL) + { extVacReport.common.tuples_deleted = istat->tuples_removed - prev_tuples_removed; + extVacReport.index.pages_deleted = + istat->pages_newly_deleted - prev_pages_newly_deleted; + } pgstat_report_vacuum_ext(indrel, -1, -1, 0, 0, false, &extVacReport); } @@ -3196,6 +3209,7 @@ lazy_cleanup_one_index(Relation indrel, IndexBulkDeleteResult *istat, TimestampTz istarttime = GetCurrentTimestamp(); double startdelaytime = VacuumDelayTime; double prev_tuples_removed = 0; + BlockNumber prev_pages_newly_deleted = 0; PgStat_VacuumRelationCounts extVacReport; const int reset_index[] = { PROGRESS_VACUUM_CURRENT_INDEX_RELID, @@ -3207,9 +3221,14 @@ lazy_cleanup_one_index(Relation indrel, IndexBulkDeleteResult *istat, /* * Snapshot the running bulkdelete totals: an index may be processed * several times per vacuum, and the report below covers this pass only. + * pages_deleted also includes pages deleted by earlier vacuums, so use + * pages_newly_deleted, which accumulates only this vacuum's deletions. */ if (istat != NULL) + { prev_tuples_removed = istat->tuples_removed; + prev_pages_newly_deleted = istat->pages_newly_deleted; + } ivinfo.index = indrel; ivinfo.heaprel = vacrel->rel; ivinfo.analyze_only = false; @@ -3253,8 +3272,12 @@ lazy_cleanup_one_index(Relation indrel, IndexBulkDeleteResult *istat, memset(&extVacReport, 0, sizeof(extVacReport)); extVacReport.type = PGSTAT_EXTVAC_INDEX; if (istat != NULL) + { extVacReport.common.tuples_deleted = istat->tuples_removed - prev_tuples_removed; + extVacReport.index.pages_deleted = + istat->pages_newly_deleted - prev_pages_newly_deleted; + } pgstat_report_vacuum_ext(indrel, -1, -1, 0, 0, false, &extVacReport); } diff --git a/src/backend/commands/vacuumparallel.c b/src/backend/commands/vacuumparallel.c index 2507e09c93..edecac426f 100644 --- a/src/backend/commands/vacuumparallel.c +++ b/src/backend/commands/vacuumparallel.c @@ -1125,6 +1125,7 @@ parallel_vacuum_process_one_index(ParallelVacuumState *pvs, Relation indrel, TimestampTz istarttime = GetCurrentTimestamp(); double startdelaytime = VacuumDelayTime; double prev_tuples_removed = 0; + BlockNumber prev_pages_newly_deleted = 0; PgStat_VacuumRelationCounts extVacReport; const int progress_index[] = { PROGRESS_VACUUM_PHASE, @@ -1148,9 +1149,14 @@ parallel_vacuum_process_one_index(ParallelVacuumState *pvs, Relation indrel, /* * Snapshot the running bulkdelete totals: an index may be processed * several times per vacuum, and the report below covers this pass only. + * pages_deleted also includes pages deleted by earlier vacuums, so use + * pages_newly_deleted, which accumulates only this vacuum's deletions. */ if (istat != NULL) + { prev_tuples_removed = istat->tuples_removed; + prev_pages_newly_deleted = istat->pages_newly_deleted; + } ivinfo.index = indrel; ivinfo.heaprel = pvs->heaprel; ivinfo.analyze_only = false; @@ -1192,8 +1198,12 @@ parallel_vacuum_process_one_index(ParallelVacuumState *pvs, Relation indrel, memset(&extVacReport, 0, sizeof(extVacReport)); extVacReport.type = PGSTAT_EXTVAC_INDEX; if (istat_res != NULL) + { extVacReport.common.tuples_deleted = istat_res->tuples_removed - prev_tuples_removed; + extVacReport.index.pages_deleted = + istat_res->pages_newly_deleted - prev_pages_newly_deleted; + } pgstat_report_vacuum_ext(indrel, -1, -1, 0, 0, false, &extVacReport); } pgstat_progress_update_multi_param(3, reset_index, reset_val); diff --git a/src/include/pgstat.h b/src/include/pgstat.h index 9c2960ec33..6e05f09285 100644 --- a/src/include/pgstat.h +++ b/src/include/pgstat.h @@ -121,7 +121,14 @@ typedef struct PgStat_VacuumRelationCounts int64 tuples_frozen; int64 recently_dead_tuples; int64 missed_dead_tuples; + int64 pages_scanned; + int64 pages_removed; + int64 missed_dead_pages; } table; + struct + { + int64 pages_deleted; + } index; }; } PgStat_VacuumRelationCounts; @@ -307,7 +314,7 @@ typedef struct PgStat_TableXactStatus * ------------------------------------------------------------ */ -#define PGSTAT_FILE_FORMAT_ID 0x01A5BCC0 +#define PGSTAT_FILE_FORMAT_ID 0x01A5BCC1 typedef struct PgStat_ArchiverStats { -- 2.50.1 (Apple Git-155)