From c8b510788518b3548a5a56502728e9bf5683c63a Mon Sep 17 00:00:00 2001 From: Alena Rybakina Date: Mon, 20 Jul 2026 17:06:11 +0300 Subject: [PATCH v42 5/9] Vacuum report hook and the ext_vacuum_statistics extension. Add a hook, set_report_vacuum_hook, through which extensions receive a per-vacuum report for the vacuumed table and each processed index, and a new extension, ext_vacuum_statistics, that stores those reports through pgstat's custom statistics infrastructure and exposes them via the pg_stats_vacuum_tables and pg_stats_vacuum_indexes views. The report carries the counters lazy vacuum already maintains; no resource usage sampling (WAL, buffers) is part of this commit - it arrives with the corresponding metric categories. This commit carries the tuple counters; the page counters follow in the next one. The statistics are: * tuples_deleted - tuples removed by the vacuum; for an index, the index entries removed by its bulkdelete passes, derived per pass from the bulkdelete/cleanup results, including passes executed in parallel workers; * tuples_frozen - tuples frozen by the vacuum; * recently_dead_tuples - dead tuples that could not be removed because they are still visible to at least one open transaction; * missed_dead_tuples - fully dead tuples that could not be pruned because the cleanup lock on the page was not acquired. The extension is controlled by a single GUC, vacuum_statistics.enabled, which can also be attached to individual databases with ALTER DATABASE ... SET to restrict collection to specific databases. Reset functions and a shared_memory_size() helper are provided; an isolation test covers the dead-tuple counters and a TAP test the enabled GUC. Authors: Alena Rybakina , Andrei Lepikhov , Andrei Zubkov Reviewed-by: Dilip Kumar , Masahiko Sawada , Ilia Evdokimov , jian he , Kirill Reshke , Alexander Korotkov , Jim Nasby , Sami Imseih , Karina Litskevich --- contrib/Makefile | 1 + contrib/ext_vacuum_statistics/.gitignore | 5 + contrib/ext_vacuum_statistics/Makefile | 23 ++ contrib/ext_vacuum_statistics/README.md | 87 +++++ .../expected/ext_vacuum_statistics.out | 52 +++ .../vacuum-extending-in-repetable-read.out | 52 +++ .../ext_vacuum_statistics--1.0.sql | 109 ++++++ .../ext_vacuum_statistics.conf | 2 + .../ext_vacuum_statistics.control | 5 + contrib/ext_vacuum_statistics/meson.build | 40 +++ .../vacuum-extending-in-repetable-read.spec | 59 ++++ .../t/054_vacuum_extending_gucs_test.pl | 100 ++++++ .../ext_vacuum_statistics/vacuum_statistics.c | 323 ++++++++++++++++++ contrib/meson.build | 1 + doc/src/sgml/contrib.sgml | 1 + doc/src/sgml/extvacuumstatistics.sgml | 255 ++++++++++++++ doc/src/sgml/filelist.sgml | 1 + src/backend/access/heap/vacuumlazy.c | 102 +++++- src/backend/commands/vacuumparallel.c | 18 + src/backend/utils/activity/pgstat_relation.c | 29 ++ src/include/commands/vacuum.h | 1 + src/include/pgstat.h | 46 +++ 22 files changed, 1305 insertions(+), 7 deletions(-) create mode 100644 contrib/ext_vacuum_statistics/.gitignore create mode 100644 contrib/ext_vacuum_statistics/Makefile create mode 100644 contrib/ext_vacuum_statistics/README.md create mode 100644 contrib/ext_vacuum_statistics/expected/ext_vacuum_statistics.out create mode 100644 contrib/ext_vacuum_statistics/expected/vacuum-extending-in-repetable-read.out create mode 100644 contrib/ext_vacuum_statistics/ext_vacuum_statistics--1.0.sql create mode 100644 contrib/ext_vacuum_statistics/ext_vacuum_statistics.conf create mode 100644 contrib/ext_vacuum_statistics/ext_vacuum_statistics.control create mode 100644 contrib/ext_vacuum_statistics/meson.build create mode 100644 contrib/ext_vacuum_statistics/specs/vacuum-extending-in-repetable-read.spec create mode 100644 contrib/ext_vacuum_statistics/t/054_vacuum_extending_gucs_test.pl create mode 100644 contrib/ext_vacuum_statistics/vacuum_statistics.c create mode 100644 doc/src/sgml/extvacuumstatistics.sgml diff --git a/contrib/Makefile b/contrib/Makefile index 7d91fe77db3..3140f2bf844 100644 --- a/contrib/Makefile +++ b/contrib/Makefile @@ -19,6 +19,7 @@ SUBDIRS = \ dict_int \ dict_xsyn \ earthdistance \ + ext_vacuum_statistics \ file_fdw \ fuzzystrmatch \ hstore \ diff --git a/contrib/ext_vacuum_statistics/.gitignore b/contrib/ext_vacuum_statistics/.gitignore new file mode 100644 index 00000000000..4d88e8bdc41 --- /dev/null +++ b/contrib/ext_vacuum_statistics/.gitignore @@ -0,0 +1,5 @@ +# Generated subdirectories +/log/ +/results/ +/tmp_check/ +/output_iso/ diff --git a/contrib/ext_vacuum_statistics/Makefile b/contrib/ext_vacuum_statistics/Makefile new file mode 100644 index 00000000000..0c30e2856a6 --- /dev/null +++ b/contrib/ext_vacuum_statistics/Makefile @@ -0,0 +1,23 @@ +# contrib/ext_vacuum_statistics/Makefile + +EXTENSION = ext_vacuum_statistics +MODULE_big = ext_vacuum_statistics +OBJS = vacuum_statistics.o +DATA = ext_vacuum_statistics--1.0.sql +PGFILEDESC = "ext_vacuum_statistics - convenience views for extended vacuum statistics" + +ISOLATION = vacuum-extending-in-repetable-read +ISOLATION_OPTS = --temp-config=$(top_srcdir)/contrib/ext_vacuum_statistics/ext_vacuum_statistics.conf +TAP_TESTS = 1 +NO_INSTALLCHECK = 1 + +ifdef USE_PGXS +PG_CONFIG = pg_config +PGXS := $(shell $(PG_CONFIG) --pgxs) +include $(PGXS) +else +subdir = contrib/ext_vacuum_statistics +top_builddir = ../.. +include $(top_builddir)/src/Makefile.global +include $(top_srcdir)/contrib/contrib-global.mk +endif diff --git a/contrib/ext_vacuum_statistics/README.md b/contrib/ext_vacuum_statistics/README.md new file mode 100644 index 00000000000..3e77bfffb3a --- /dev/null +++ b/contrib/ext_vacuum_statistics/README.md @@ -0,0 +1,87 @@ +# ext_vacuum_statistics + +Extended vacuum statistics extension for PostgreSQL. It collects and exposes detailed per-table and per-index vacuum statistics via convenient views in the `ext_vacuum_statistics` schema. + +## Installation + +``` +./configure tmp_install="$(pwd)/my/inst" +make clean && make && make install +cd contrib/ext_vacuum_statistics +make && make install +``` + +It is essential that the extension is listed in `shared_preload_libraries` because it registers a vacuum hook at server startup. + +In your `postgresql.conf`: + +``` +shared_preload_libraries = 'ext_vacuum_statistics' +``` + +Restart PostgreSQL. + +In your database: + +```sql +CREATE EXTENSION ext_vacuum_statistics; +``` + +## Usage + +Query vacuum statistics via the provided views: + +```sql +-- Per-table heap vacuum statistics +SELECT * FROM ext_vacuum_statistics.pg_stats_vacuum_tables; + +-- Per-index vacuum statistics +SELECT * FROM ext_vacuum_statistics.pg_stats_vacuum_indexes; +``` + +Example output: + +``` + relname | tuples_deleted | pages_removed +-----------+----------------+--------------- + mytable | 500 | 10 +``` + +Reset statistics when needed: + +```sql +SELECT ext_vacuum_statistics.vacuum_statistics_reset(); +``` + +## Configuration (GUCs) + +| GUC | Default | Description | +|-----|---------|-------------| +| `vacuum_statistics.enabled` | on | Enable extended vacuum statistics collection | + +## Memory usage + +Each tracked object (table or index) uses a fixed-size shared memory entry; the exact size depends on the platform. + +The exact size depends on the platform. Call `ext_vacuum_statistics.shared_memory_size()` to get the total shared memory used by the extension. + +Example: a database with 1000 tables and 2000 indexes, all tracked, uses about **700 KB** on Ubuntu (3001 entries × 232 bytes). Per-database entries add one entry per tracked database. + +## Recipes + +**Disable statistics collection temporarily:** + +```sql +SET vacuum_statistics.enabled = off; +``` + +## Views + +| View | Description | +|------|-------------| +| `ext_vacuum_statistics.pg_stats_vacuum_tables` | Per-table heap vacuum stats (pages scanned, tuples deleted, dead tuples, etc.) | +| `ext_vacuum_statistics.pg_stats_vacuum_indexes` | Per-index vacuum stats | + +## Limitations + +- Must be loaded via `shared_preload_libraries`; it cannot be loaded on demand. diff --git a/contrib/ext_vacuum_statistics/expected/ext_vacuum_statistics.out b/contrib/ext_vacuum_statistics/expected/ext_vacuum_statistics.out new file mode 100644 index 00000000000..89c9594dea8 --- /dev/null +++ b/contrib/ext_vacuum_statistics/expected/ext_vacuum_statistics.out @@ -0,0 +1,52 @@ +-- ext_vacuum_statistics regression test + +-- Create extension +CREATE EXTENSION ext_vacuum_statistics; + +-- Verify schema and views exist +SELECT nspname FROM pg_namespace WHERE nspname = 'ext_vacuum_statistics'; + nspname +------------------ + ext_vacuum_statistics +(1 row) + +-- Views should be queryable (may return empty if no vacuum has run) +SELECT COUNT(*) >= 0 FROM ext_vacuum_statistics.pg_stats_vacuum_tables; + ?column? +---------- + t +(1 row) + +SELECT COUNT(*) >= 0 FROM ext_vacuum_statistics.pg_stats_vacuum_indexes; + ?column? +---------- + t +(1 row) + +SELECT COUNT(*) >= 0 FROM ext_vacuum_statistics.pg_stats_vacuum_database; + ?column? +---------- + t +(1 row) + +-- Verify views have expected columns +SELECT COUNT(*) AS tables_cols FROM information_schema.columns +WHERE table_schema = 'ext_vacuum_statistics' AND table_name = 'tables'; + tables_cols +------------- + 28 +(1 row) + +SELECT COUNT(*) AS indexes_cols FROM information_schema.columns +WHERE table_schema = 'ext_vacuum_statistics' AND table_name = 'indexes'; + indexes_cols +-------------- + 20 +(1 row) + +SELECT COUNT(*) AS database_cols FROM information_schema.columns +WHERE table_schema = 'ext_vacuum_statistics' AND table_name = 'database'; + database_cols +--------------- + 15 +(1 row) diff --git a/contrib/ext_vacuum_statistics/expected/vacuum-extending-in-repetable-read.out b/contrib/ext_vacuum_statistics/expected/vacuum-extending-in-repetable-read.out new file mode 100644 index 00000000000..f8962fad325 --- /dev/null +++ b/contrib/ext_vacuum_statistics/expected/vacuum-extending-in-repetable-read.out @@ -0,0 +1,52 @@ +unused step name: s2_delete +Parsed test spec with 2 sessions + +starting permutation: s2_insert s2_print_vacuum_stats_table s1_begin_repeatable_read s2_update s2_insert_interrupt s2_vacuum s2_print_vacuum_stats_table s1_commit s2_checkpoint s2_vacuum s2_print_vacuum_stats_table +step s2_insert: INSERT INTO test_vacuum_stat_isolation(id, ival) SELECT ival, ival%10 FROM generate_series(1,1000) As ival; +step s2_print_vacuum_stats_table: + SELECT + vt.relname, vt.tuples_deleted, vt.recently_dead_tuples, vt.missed_dead_tuples, vt.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 +-------+--------------+--------------------+------------------+------------- +(0 rows) + +step s1_begin_repeatable_read: + BEGIN transaction ISOLATION LEVEL REPEATABLE READ; + select count(ival) from test_vacuum_stat_isolation where id>900; + +count +----- + 100 +(1 row) + +step s2_update: UPDATE test_vacuum_stat_isolation SET ival = ival + 2 where id > 900; +step s2_insert_interrupt: INSERT INTO test_vacuum_stat_isolation values (1,1); +step s2_vacuum: VACUUM test_vacuum_stat_isolation; +step s2_print_vacuum_stats_table: + SELECT + vt.relname, vt.tuples_deleted, vt.recently_dead_tuples, vt.missed_dead_tuples, vt.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 +(1 row) + +step s1_commit: COMMIT; +step s2_checkpoint: CHECKPOINT; +step s2_vacuum: VACUUM test_vacuum_stat_isolation; +step s2_print_vacuum_stats_table: + SELECT + vt.relname, vt.tuples_deleted, vt.recently_dead_tuples, vt.missed_dead_tuples, vt.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 +(1 row) + diff --git a/contrib/ext_vacuum_statistics/ext_vacuum_statistics--1.0.sql b/contrib/ext_vacuum_statistics/ext_vacuum_statistics--1.0.sql new file mode 100644 index 00000000000..d2408c3b920 --- /dev/null +++ b/contrib/ext_vacuum_statistics/ext_vacuum_statistics--1.0.sql @@ -0,0 +1,109 @@ +/*------------------------------------------------------------------------- + * + * ext_vacuum_statistics--1.0.sql + * Extended vacuum statistics via hook and custom storage + * + * This extension collects extended vacuum statistics via set_report_vacuum_hook + * and stores them in shared memory. + * + *------------------------------------------------------------------------- + */ + +\echo Use "CREATE EXTENSION ext_vacuum_statistics" to load this file. \quit + +CREATE SCHEMA IF NOT EXISTS ext_vacuum_statistics; + +COMMENT ON SCHEMA ext_vacuum_statistics IS + 'Extended vacuum statistics (heap, index, database)'; + +-- Reset functions +CREATE OR REPLACE FUNCTION ext_vacuum_statistics.extvac_reset_entry( + dboid oid, + relid oid, + type int4 +) +RETURNS boolean +AS 'MODULE_PATHNAME', 'extvac_reset_entry' +LANGUAGE C STRICT PARALLEL SAFE; + +CREATE OR REPLACE FUNCTION ext_vacuum_statistics.vacuum_statistics_reset() +RETURNS bigint +AS 'MODULE_PATHNAME', 'vacuum_statistics_reset' +LANGUAGE C STRICT PARALLEL SAFE; + +CREATE OR REPLACE FUNCTION ext_vacuum_statistics.shared_memory_size() +RETURNS bigint +AS 'MODULE_PATHNAME', 'extvac_shared_memory_size' +LANGUAGE C STRICT PARALLEL SAFE; + +COMMENT ON FUNCTION ext_vacuum_statistics.shared_memory_size() IS + 'Total shared memory in bytes used by the extension for vacuum statistics.'; + +-- Internal C function to fetch table vacuum stats +CREATE OR REPLACE FUNCTION ext_vacuum_statistics.pg_stats_get_vacuum_tables( + IN dboid oid, + IN reloid oid, + OUT relid oid, + OUT tuples_deleted bigint, + OUT tuples_frozen bigint, + OUT recently_dead_tuples bigint, + OUT missed_dead_tuples bigint +) +RETURNS SETOF record +AS 'MODULE_PATHNAME', 'pg_stats_get_vacuum_tables' +LANGUAGE C STRICT STABLE; + +-- Internal C function to fetch index vacuum stats +CREATE OR REPLACE FUNCTION ext_vacuum_statistics.pg_stats_get_vacuum_indexes( + IN dboid oid, + IN reloid oid, + OUT relid oid, + OUT tuples_deleted bigint +) +RETURNS SETOF record +AS 'MODULE_PATHNAME', 'pg_stats_get_vacuum_indexes' +LANGUAGE C STRICT STABLE; + +-- View: vacuum statistics per table (heap) +CREATE VIEW ext_vacuum_statistics.pg_stats_vacuum_tables AS +SELECT + rel.oid AS relid, + ns.nspname AS schema, + rel.relname AS relname, + db.datname AS dbname, + stats.tuples_deleted, + stats.tuples_frozen, + stats.recently_dead_tuples, + stats.missed_dead_tuples +FROM pg_database db, + pg_class rel, + pg_namespace ns, + LATERAL ext_vacuum_statistics.pg_stats_get_vacuum_tables(db.oid, rel.oid) stats +WHERE db.datname = current_database() + AND rel.relkind = 'r' + AND rel.relnamespace = ns.oid + AND rel.oid = stats.relid; + +COMMENT ON VIEW ext_vacuum_statistics.pg_stats_vacuum_tables IS + 'Extended vacuum statistics per table (heap)'; + +-- View: vacuum statistics per index +CREATE VIEW ext_vacuum_statistics.pg_stats_vacuum_indexes AS +SELECT + rel.oid AS indexrelid, + ns.nspname AS schema, + rel.relname AS indexrelname, + db.datname AS dbname, + stats.tuples_deleted +FROM pg_database db, + pg_class rel, + pg_namespace ns, + LATERAL ext_vacuum_statistics.pg_stats_get_vacuum_indexes(db.oid, rel.oid) stats +WHERE db.datname = current_database() + AND rel.relkind = 'i' + AND rel.relnamespace = ns.oid + AND rel.oid = stats.relid; + +COMMENT ON VIEW ext_vacuum_statistics.pg_stats_vacuum_indexes IS + 'Extended vacuum statistics per index'; + diff --git a/contrib/ext_vacuum_statistics/ext_vacuum_statistics.conf b/contrib/ext_vacuum_statistics/ext_vacuum_statistics.conf new file mode 100644 index 00000000000..9b711487623 --- /dev/null +++ b/contrib/ext_vacuum_statistics/ext_vacuum_statistics.conf @@ -0,0 +1,2 @@ +# Config for ext_vacuum_statistics regression tests +shared_preload_libraries = 'ext_vacuum_statistics' diff --git a/contrib/ext_vacuum_statistics/ext_vacuum_statistics.control b/contrib/ext_vacuum_statistics/ext_vacuum_statistics.control new file mode 100644 index 00000000000..518350a64b7 --- /dev/null +++ b/contrib/ext_vacuum_statistics/ext_vacuum_statistics.control @@ -0,0 +1,5 @@ +# ext_vacuum_statistics extension +comment = 'Extended vacuum statistics via hook (requires shared_preload_libraries)' +default_version = '1.0' +relocatable = true +module_pathname = '$libdir/ext_vacuum_statistics' diff --git a/contrib/ext_vacuum_statistics/meson.build b/contrib/ext_vacuum_statistics/meson.build new file mode 100644 index 00000000000..1cf0dc5da9d --- /dev/null +++ b/contrib/ext_vacuum_statistics/meson.build @@ -0,0 +1,40 @@ +# Copyright (c) 2022-2026, PostgreSQL Global Development Group +# +# ext_vacuum_statistics - extended vacuum statistics via hook +# Requires shared_preload_libraries = 'ext_vacuum_statistics' + +ext_vacuum_statistics_sources = files( + 'vacuum_statistics.c', +) + +ext_vacuum_statistics = shared_module('ext_vacuum_statistics', + ext_vacuum_statistics_sources, + kwargs: contrib_mod_args + { + 'dependencies': contrib_mod_args['dependencies'], + }, +) +contrib_targets += ext_vacuum_statistics + +install_data( + 'ext_vacuum_statistics.control', + 'ext_vacuum_statistics--1.0.sql', + kwargs: contrib_data_args, +) + +tests += { + 'name': 'ext_vacuum_statistics', + 'sd': meson.current_source_dir(), + 'bd': meson.current_build_dir(), + 'isolation': { + 'specs': [ + 'vacuum-extending-in-repetable-read', + ], + 'regress_args': ['--temp-config', files('ext_vacuum_statistics.conf')], + 'runningcheck': false, + }, + 'tap': { + 'tests': [ + 't/054_vacuum_extending_gucs_test.pl', + ], + }, +} diff --git a/contrib/ext_vacuum_statistics/specs/vacuum-extending-in-repetable-read.spec b/contrib/ext_vacuum_statistics/specs/vacuum-extending-in-repetable-read.spec new file mode 100644 index 00000000000..3001b56ac2b --- /dev/null +++ b/contrib/ext_vacuum_statistics/specs/vacuum-extending-in-repetable-read.spec @@ -0,0 +1,59 @@ +# Test for checking recently_dead_tuples, tuples_deleted and frozen tuples in ext_vacuum_statistics.pg_stats_vacuum_tables. +# recently_dead_tuples values are counted when vacuum hasn't cleared tuples because they were deleted recently. +# recently_dead_tuples aren't increased after releasing lock compared with tuples_deleted, which increased +# by the value of the cleared tuples that the vacuum managed to clear. + +setup +{ + CREATE TABLE test_vacuum_stat_isolation(id int, ival int) WITH (autovacuum_enabled = off); + CREATE EXTENSION ext_vacuum_statistics; + SET track_io_timing = on; +} + +teardown +{ + DROP EXTENSION ext_vacuum_statistics CASCADE; + DROP TABLE test_vacuum_stat_isolation CASCADE; + RESET track_io_timing; +} + +session s1 +setup { + SET track_io_timing = on; +} +step s1_begin_repeatable_read { + BEGIN transaction ISOLATION LEVEL REPEATABLE READ; + select count(ival) from test_vacuum_stat_isolation where id>900; +} +step s1_commit { COMMIT; } + +session s2 +setup { + SET track_io_timing = on; +} +step s2_insert { INSERT INTO test_vacuum_stat_isolation(id, ival) SELECT ival, ival%10 FROM generate_series(1,1000) As ival; } +step s2_update { UPDATE test_vacuum_stat_isolation SET ival = ival + 2 where id > 900; } +step s2_delete { DELETE FROM test_vacuum_stat_isolation where id > 900; } +step s2_insert_interrupt { INSERT INTO test_vacuum_stat_isolation values (1,1); } +step s2_vacuum { VACUUM test_vacuum_stat_isolation; } +step s2_checkpoint { CHECKPOINT; } +step s2_print_vacuum_stats_table +{ + SELECT + vt.relname, vt.tuples_deleted, vt.recently_dead_tuples, vt.missed_dead_tuples, vt.tuples_frozen + FROM ext_vacuum_statistics.pg_stats_vacuum_tables vt, pg_class c + WHERE vt.relname = 'test_vacuum_stat_isolation' AND vt.relid = c.oid; +} + +permutation + s2_insert + s2_print_vacuum_stats_table + s1_begin_repeatable_read + s2_update + s2_insert_interrupt + s2_vacuum + s2_print_vacuum_stats_table + s1_commit + s2_checkpoint + s2_vacuum + s2_print_vacuum_stats_table diff --git a/contrib/ext_vacuum_statistics/t/054_vacuum_extending_gucs_test.pl b/contrib/ext_vacuum_statistics/t/054_vacuum_extending_gucs_test.pl new file mode 100644 index 00000000000..e44d54d040f --- /dev/null +++ b/contrib/ext_vacuum_statistics/t/054_vacuum_extending_gucs_test.pl @@ -0,0 +1,100 @@ +# Copyright (c) 2025 PostgreSQL Global Development Group +# +# Test GUC parameters for ext_vacuum_statistics extension: +# vacuum_statistics.enabled +use strict; +use warnings; +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; + +use Test::More; + +#------------------------------------------------------------------------------ +# Test cluster setup +#------------------------------------------------------------------------------ + +my $node = PostgreSQL::Test::Cluster->new('ext_stat_vacuum_gucs'); +$node->init; + +$node->append_conf('postgresql.conf', q{ + shared_preload_libraries = 'ext_vacuum_statistics' + log_min_messages = notice +}); + +$node->start; + +#------------------------------------------------------------------------------ +# Database creation and initialization +#------------------------------------------------------------------------------ + +$node->safe_psql('postgres', q{ + CREATE DATABASE statistic_vacuum_gucs; +}); + +my $dbname = 'statistic_vacuum_gucs'; + +$node->safe_psql($dbname, q{ + CREATE EXTENSION ext_vacuum_statistics; + CREATE TABLE guc_test (x int PRIMARY KEY) + WITH (autovacuum_enabled = off); + INSERT INTO guc_test SELECT x FROM generate_series(1, 100) AS g(x); + ANALYZE guc_test; +}); + +#------------------------------------------------------------------------------ +# Reset stats and run vacuum (all in one session so GUCs persist) +#------------------------------------------------------------------------------ + +sub reset_and_vacuum { + my ($db, $table, $opts) = @_; + $table ||= 'guc_test'; + my $gucs = $opts && $opts->{gucs} ? $opts->{gucs} : []; + my $modify = $opts && $opts->{modify}; + my $extra = $opts && $opts->{extra_vacuum} ? $opts->{extra_vacuum} : []; + $extra = [$extra] unless ref $extra eq 'ARRAY'; + my $sql = join("\n", (map { "SET $_;" } @$gucs), + "SELECT ext_vacuum_statistics.vacuum_statistics_reset();", + $modify ? ( + "TRUNCATE $table;", + "INSERT INTO $table SELECT x FROM generate_series(1, 100) AS g(x);", + "DELETE FROM $table;", + ) : (), + "VACUUM $table;", + (map { "VACUUM $_;" } @$extra), + # Make pending stats visible to subsequent sessions without sleeping. + "SELECT pg_stat_force_next_flush();"); + $node->safe_psql($db, $sql); +} + +#------------------------------------------------------------------------------ +# Test 1: vacuum_statistics.enabled +#------------------------------------------------------------------------------ +subtest 'vacuum_statistics.enabled' => sub { + reset_and_vacuum($dbname); + + # Default: enabled - should have stats + my $count = $node->safe_psql($dbname, + "SELECT COUNT(*) FROM ext_vacuum_statistics.pg_stats_vacuum_tables WHERE relname = 'guc_test'"); + ok($count > 0, 'stats collected when enabled'); + + # Disable, reset and vacuum in same session. Assert not only that the + # row count is zero, but that the specific counters remain zero: a stray + # row with zero counters would otherwise pass a bare COUNT(*)=0 check. + reset_and_vacuum($dbname, 'guc_test', { gucs => ['vacuum_statistics.enabled = off'] }); + + $count = $node->safe_psql($dbname, + "SELECT COUNT(*) FROM ext_vacuum_statistics.pg_stats_vacuum_tables WHERE relname = 'guc_test'"); + is($count, 0, 'no rows when disabled'); + + my $sums = $node->safe_psql($dbname, q{ + SELECT COALESCE(SUM(tuples_deleted), 0) + + COALESCE(SUM(tuples_frozen), 0) + FROM ext_vacuum_statistics.pg_stats_vacuum_tables + WHERE relname = 'guc_test' + }); + is($sums, '0', 'no counters accumulated when disabled'); +}; + +$node->stop; + +done_testing(); diff --git a/contrib/ext_vacuum_statistics/vacuum_statistics.c b/contrib/ext_vacuum_statistics/vacuum_statistics.c new file mode 100644 index 00000000000..8d0d942d3ad --- /dev/null +++ b/contrib/ext_vacuum_statistics/vacuum_statistics.c @@ -0,0 +1,323 @@ +/* + * ext_vacuum_statistics - Extended vacuum statistics for PostgreSQL + * + * This module collects detailed vacuum statistics (I/O, WAL, timing, etc.) + * at relation and database level by hooking into the vacuum reporting path. + * Statistics are stored via pgstat custom statistics. Management of statistics + * storage and output functions are implemented in this module. + */ +#include "postgres.h" + +#include "fmgr.h" +#include "funcapi.h" +#include "miscadmin.h" +#include "pgstat.h" +#include "utils/builtins.h" +#include "utils/fmgrprotos.h" +#include "utils/guc.h" +#include "utils/pgstat_kind.h" +#include "utils/pgstat_internal.h" +#include "utils/tuplestore.h" + +#ifdef PG_MODULE_MAGIC +PG_MODULE_MAGIC; +#endif + +/* Custom stats kind for per-relation vacuum statistics */ +#define PGSTAT_KIND_EXTVAC_RELATION 24 + +#define SJ_NODENAME "vacuum_statistics" + +/* GUCs */ +static bool evs_enabled = true; + +/* Hooks */ +static set_report_vacuum_hook_type prev_report_vacuum_hook = NULL; + +/* Forward declarations */ +static void pgstat_report_vacuum_extstats(Oid tableoid, bool shared, + PgStat_VacuumRelationCounts * params); + +/* + * objid encoding for relations: (relid << 2) | (type & 3) + */ +#define EXTVAC_OBJID(relid, type) (((uint64) (relid)) << 2 | ((type) & 3)) + +/* Shared memory entry for vacuum stats; one per relation or database. */ +typedef struct PgStatShared_ExtVacEntry +{ + PgStatShared_Common header; + PgStat_VacuumRelationCounts stats; +} PgStatShared_ExtVacEntry; + +/* PgStat kind for per-relation vacuum statistics (tables/indexes) */ +static const PgStat_KindInfo extvac_relation_kind_info = { + .name = "ext_vacuum_statistics_relation", + .fixed_amount = false, + .accessed_across_databases = true, + .write_to_file = true, + .track_entry_count = true, + .shared_size = sizeof(PgStatShared_ExtVacEntry), + .shared_data_off = offsetof(PgStatShared_ExtVacEntry, stats), + .shared_data_len = sizeof(PgStat_VacuumRelationCounts), + .pending_size = 0, + .flush_pending_cb = NULL, +}; + +static inline void +pgstat_accumulate_common(PgStat_CommonCounts * dst, const PgStat_CommonCounts * src) +{ + dst->tuples_deleted += src->tuples_deleted; +} + +static inline void +pgstat_accumulate_extvac_stats(PgStat_VacuumRelationCounts * dst, + const PgStat_VacuumRelationCounts * src) +{ + if (dst->type == PGSTAT_EXTVAC_INVALID) + dst->type = src->type; + + Assert(src->type != PGSTAT_EXTVAC_INVALID && src->type != PGSTAT_EXTVAC_DB); + Assert(src->type == dst->type); + + pgstat_accumulate_common(&dst->common, &src->common); + + if (dst->type == PGSTAT_EXTVAC_TABLE) + { + dst->table.tuples_frozen += src->table.tuples_frozen; + dst->table.recently_dead_tuples += src->table.recently_dead_tuples; + dst->table.missed_dead_tuples += src->table.missed_dead_tuples; + } +} + +void +_PG_init(void) +{ + if (!process_shared_preload_libraries_in_progress) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("ext_vacuum_statistics module could be loaded only on startup."), + errdetail("Add 'ext_vacuum_statistics' into the shared_preload_libraries list."))); + + DefineCustomBoolVariable("vacuum_statistics.enabled", + "Enable extended vacuum statistics collection.", + NULL, &evs_enabled, true, + PGC_SUSET, 0, NULL, NULL, NULL); + + MarkGUCPrefixReserved(SJ_NODENAME); + + pgstat_register_kind(PGSTAT_KIND_EXTVAC_RELATION, &extvac_relation_kind_info); + + prev_report_vacuum_hook = set_report_vacuum_hook; + set_report_vacuum_hook = pgstat_report_vacuum_extstats; +} + +/* + * Store incoming vacuum stats into a per-relation pgstat custom statistics + * entry. Uses pgstat_get_entry_ref_locked and pgstat_accumulate_* for + * atomic updates. + */ +static void +extvac_store(Oid dboid, Oid relid, int type, + PgStat_VacuumRelationCounts * params) +{ + PgStat_EntryRef *entry_ref; + PgStatShared_ExtVacEntry *shared; + uint64 objid; + + if (!evs_enabled) + return; + + objid = EXTVAC_OBJID(relid, type); + entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_EXTVAC_RELATION, dboid, objid, false); + if (entry_ref) + { + shared = (PgStatShared_ExtVacEntry *) entry_ref->shared_stats; + if (shared->stats.type == PGSTAT_EXTVAC_INVALID) + { + memset(&shared->stats, 0, sizeof(shared->stats)); + shared->stats.type = params->type; + } + pgstat_accumulate_extvac_stats(&shared->stats, params); + pgstat_unlock_entry(entry_ref); + } +} + +/* + * Vacuum report hook: called when vacuum finishes. Stores stats per-relation, + * then chains to previous hook. + */ +static void +pgstat_report_vacuum_extstats(Oid tableoid, bool shared, + PgStat_VacuumRelationCounts * params) +{ + Oid dboid = shared ? InvalidOid : MyDatabaseId; + + if (evs_enabled) + extvac_store(dboid, tableoid, params->type, params); + if (prev_report_vacuum_hook) + prev_report_vacuum_hook(tableoid, shared, params); +} + +/* Reset statistics for a single relation entry. */ +static bool +extvac_reset_by_relid(Oid dboid, Oid relid, int type) +{ + uint64 objid = EXTVAC_OBJID(relid, type); + + pgstat_reset_entry(PGSTAT_KIND_EXTVAC_RELATION, dboid, objid, 0); + return true; +} + +/* Reset all vacuum statistics. */ +static int64 +extvac_stat_reset(void) +{ + pgstat_reset_of_kind(PGSTAT_KIND_EXTVAC_RELATION); + return 0; /* count not available */ +} + +PG_FUNCTION_INFO_V1(vacuum_statistics_reset); +PG_FUNCTION_INFO_V1(extvac_shared_memory_size); +PG_FUNCTION_INFO_V1(extvac_reset_entry); + +Datum +vacuum_statistics_reset(PG_FUNCTION_ARGS) +{ + PG_RETURN_INT64(extvac_stat_reset()); +} + +Datum +extvac_reset_entry(PG_FUNCTION_ARGS) +{ + Oid dboid = PG_GETARG_OID(0); + Oid relid = PG_GETARG_OID(1); + int type = PG_GETARG_INT32(2); + + PG_RETURN_BOOL(extvac_reset_by_relid(dboid, relid, type)); +} + +/* + * Return total shared memory in bytes used by the extension for vacuum stats. + * Used for monitoring and capacity planning: memory grows with the number of + * tracked relations and databases. + */ +Datum +extvac_shared_memory_size(PG_FUNCTION_ARGS) +{ + uint64 rel_count; + uint64 total; + size_t entry_size = sizeof(PgStatShared_ExtVacEntry); + + rel_count = pgstat_get_entry_count(PGSTAT_KIND_EXTVAC_RELATION); + total = rel_count; + + PG_RETURN_INT64((int64) (total * entry_size)); +} + +/* + * Output vacuum statistics (tables or indexes). + */ +#define EXTVAC_HEAP_STAT_COLS 5 +#define EXTVAC_IDX_STAT_COLS 2 +#define EXTVAC_MAX_STAT_COLS Max(EXTVAC_HEAP_STAT_COLS, EXTVAC_IDX_STAT_COLS) + +static void +tuplestore_put_for_relation(Oid relid, Tuplestorestate *tupstore, + TupleDesc tupdesc, PgStat_VacuumRelationCounts * vacuum_ext) +{ + Datum values[EXTVAC_MAX_STAT_COLS]; + bool nulls[EXTVAC_MAX_STAT_COLS]; + int i = 0; + + memset(nulls, 0, sizeof(nulls)); + values[i++] = ObjectIdGetDatum(relid); + + if (vacuum_ext->type == PGSTAT_EXTVAC_TABLE) + { + values[i++] = Int64GetDatum(vacuum_ext->common.tuples_deleted); + 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_tuples); + } + else if (vacuum_ext->type == PGSTAT_EXTVAC_INDEX) + { + values[i++] = Int64GetDatum(vacuum_ext->common.tuples_deleted); + } + + Assert(i == ((vacuum_ext->type == PGSTAT_EXTVAC_TABLE) ? EXTVAC_HEAP_STAT_COLS : EXTVAC_IDX_STAT_COLS)); + tuplestore_putvalues(tupstore, tupdesc, values, nulls); +} + +static Datum +pg_stats_vacuum(FunctionCallInfo fcinfo, int type) +{ + ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo; + MemoryContext per_query_ctx; + MemoryContext oldcontext; + Tuplestorestate *tupstore; + TupleDesc tupdesc; + Oid dbid = PG_GETARG_OID(0); + + if (rsinfo == NULL || !IsA(rsinfo, ReturnSetInfo)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("ext_vacuum_statistics: set-valued function called in context that cannot accept a set"))); + if (!(rsinfo->allowedModes & SFRM_Materialize)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("ext_vacuum_statistics: materialize mode required"))); + + per_query_ctx = rsinfo->econtext->ecxt_per_query_memory; + oldcontext = MemoryContextSwitchTo(per_query_ctx); + + if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE) + elog(ERROR, "ext_vacuum_statistics: return type must be a row type"); + + tupstore = tuplestore_begin_heap(true, false, work_mem); + rsinfo->returnMode = SFRM_Materialize; + rsinfo->setResult = tupstore; + rsinfo->setDesc = tupdesc; + + MemoryContextSwitchTo(oldcontext); + + if (type == PGSTAT_EXTVAC_INDEX || type == PGSTAT_EXTVAC_TABLE) + { + Oid relid = PG_GETARG_OID(1); + PgStat_VacuumRelationCounts *stats; + + if (!OidIsValid(relid)) + return (Datum) 0; + + stats = (PgStat_VacuumRelationCounts *) + pgstat_fetch_entry(PGSTAT_KIND_EXTVAC_RELATION, dbid, + EXTVAC_OBJID(relid, type), NULL); + + if (!stats) + stats = (PgStat_VacuumRelationCounts *) + pgstat_fetch_entry(PGSTAT_KIND_EXTVAC_RELATION, InvalidOid, + EXTVAC_OBJID(relid, type), NULL); + + if (stats && stats->type == type) + tuplestore_put_for_relation(relid, tupstore, tupdesc, stats); + } + else + elog(PANIC, "ext_vacuum_statistics: invalid type %d", type); + + return (Datum) 0; +} + +PG_FUNCTION_INFO_V1(pg_stats_get_vacuum_tables); +PG_FUNCTION_INFO_V1(pg_stats_get_vacuum_indexes); + +Datum +pg_stats_get_vacuum_tables(PG_FUNCTION_ARGS) +{ + return pg_stats_vacuum(fcinfo, PGSTAT_EXTVAC_TABLE); +} + +Datum +pg_stats_get_vacuum_indexes(PG_FUNCTION_ARGS) +{ + return pg_stats_vacuum(fcinfo, PGSTAT_EXTVAC_INDEX); +} diff --git a/contrib/meson.build b/contrib/meson.build index ebb7f83d8c5..d7dc0fd07f0 100644 --- a/contrib/meson.build +++ b/contrib/meson.build @@ -26,6 +26,7 @@ subdir('cube') subdir('dblink') subdir('dict_int') subdir('dict_xsyn') +subdir('ext_vacuum_statistics') subdir('earthdistance') subdir('file_fdw') subdir('fuzzystrmatch') diff --git a/doc/src/sgml/contrib.sgml b/doc/src/sgml/contrib.sgml index b9b03654aad..2a38f9042bb 100644 --- a/doc/src/sgml/contrib.sgml +++ b/doc/src/sgml/contrib.sgml @@ -141,6 +141,7 @@ CREATE EXTENSION extension_name; &dict-int; &dict-xsyn; &earthdistance; + &extvacuumstatistics; &file-fdw; &fuzzystrmatch; &hstore; diff --git a/doc/src/sgml/extvacuumstatistics.sgml b/doc/src/sgml/extvacuumstatistics.sgml new file mode 100644 index 00000000000..ff91f360474 --- /dev/null +++ b/doc/src/sgml/extvacuumstatistics.sgml @@ -0,0 +1,255 @@ + + + + ext_vacuum_statistics — extended vacuum statistics + + + ext_vacuum_statistics + + + + The ext_vacuum_statistics module provides + extended per-table and per-index vacuum statistics via views in the + ext_vacuum_statistics schema. + + + + The module must be loaded by adding ext_vacuum_statistics to + in + postgresql.conf, because it registers a vacuum hook at + server startup. This means that a server restart is needed to add or remove + the module. After installation, run + CREATE EXTENSION ext_vacuum_statistics in each database + where you want to use it. + + + + When active, the module provides views + ext_vacuum_statistics.pg_stats_vacuum_tables and + ext_vacuum_statistics.pg_stats_vacuum_indexes, + plus functions to reset statistics. + + + + Each tracked object (one table or one index) uses a fixed-size shared + memory entry; the exact size depends on the platform. Call + ext_vacuum_statistics.shared_memory_size() to get + the total shared memory used by the extension. + + + + The <structname>ext_vacuum_statistics.pg_stats_vacuum_tables</structname> View + + + pg_stats_vacuum_tables + + + + The view ext_vacuum_statistics.pg_stats_vacuum_tables + contains one row for each table in the current database (including TOAST + tables), showing statistics about vacuuming that specific table. The columns + are shown in . + + + + <structname>ext_vacuum_statistics.pg_stats_vacuum_tables</structname> Columns + + + + + Column Type + + + Description + + + + + + + relid oid + + + OID of a table + + + + + schema name + + + Name of the schema this table is in + + + + + relname name + + + Name of this table + + + + + dbname name + + + Name of the database containing this table + + + + + tuples_deleted int8 + + + Number of dead tuples vacuum operations deleted from this table + + + + + tuples_frozen int8 + + + Number of tuples that vacuum operations marked as frozen + + + + + recently_dead_tuples int8 + + + Number of dead tuples left due to visibility in transactions + + + + + missed_dead_tuples int8 + + + Number of fully DEAD tuples that could not be pruned due to failure to acquire a cleanup lock + + + + +
+
+ + + The <structname>ext_vacuum_statistics.pg_stats_vacuum_indexes</structname> View + + + pg_stats_vacuum_indexes + + + + The view ext_vacuum_statistics.pg_stats_vacuum_indexes + contains one row for each index in the current database, showing statistics + about vacuuming that specific index. The columns are shown in + . + + + + <structname>ext_vacuum_statistics.pg_stats_vacuum_indexes</structname> Columns + + + + + Column Type + + + Description + + + + + + + indexrelid oid + + + OID of this index + + + + + schema name + + + Name of the schema this index is in + + + + + indexrelname name + + + Name of this index + + + + + dbname name + + + Name of the database containing this index + + + + + tuples_deleted int8 + + + Number of index entries removed by the bulkdelete passes of vacuums + of the parent table + + + + +
+
+ + + Functions + + + + + ext_vacuum_statistics.shared_memory_size() + bigint + + + + Returns the total shared memory in bytes used by the extension for + vacuum statistics (relations plus databases). + + + + + + ext_vacuum_statistics.vacuum_statistics_reset() + bigint + + + + Resets all vacuum statistics. Returns the number of entries reset. + + + + + + + + Configuration Parameters + + + + vacuum_statistics.enabled (boolean) + + + Enables extended vacuum statistics collection. Default: on. + + + + + +
diff --git a/doc/src/sgml/filelist.sgml b/doc/src/sgml/filelist.sgml index 66ea8b988a1..3e893f5ab04 100644 --- a/doc/src/sgml/filelist.sgml +++ b/doc/src/sgml/filelist.sgml @@ -133,6 +133,7 @@ + diff --git a/src/backend/access/heap/vacuumlazy.c b/src/backend/access/heap/vacuumlazy.c index 7e379707464..71d1d97ac7f 100644 --- a/src/backend/access/heap/vacuumlazy.c +++ b/src/backend/access/heap/vacuumlazy.c @@ -285,6 +285,8 @@ typedef struct LVRelState /* Error reporting state */ char *dbname; char *relnamespace; + Oid reloid; + Oid indoid; char *relname; char *indname; /* Current index name */ BlockNumber blkno; /* used only for heap operations */ @@ -412,6 +414,11 @@ typedef struct LVRelState * been permanently disabled. */ BlockNumber eager_scan_remaining_fails; + + /* + * We need to accumulate index statistics for later subtraction from heap + * stats. + */ } LVRelState; @@ -487,6 +494,29 @@ static void restore_vacuum_error_info(LVRelState *vacrel, const LVSavedErrInfo *saved_vacrel); +/* Extended vacuum statistics functions */ + +/* + * Build the heap-specific part of the extended vacuum report from the + * counters gathered in vacrel. + */ +static void +accumulate_heap_vacuum_statistics(LVRelState *vacrel, PgStat_VacuumRelationCounts * extVacStats) +{ + extVacStats->type = PGSTAT_EXTVAC_TABLE; + 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; + + /* + * The wraparound failsafe is reported as a per-relation flag (0/1): did + * the latest vacuum of this relation engage it? Consumers can sum the + * flags to build a per-database total. + */ + + /* Hook is invoked from pgstat_report_vacuum() when extstats is passed */ +} /* * Helper to set up the eager scanning state for vacuuming a single relation. @@ -646,7 +676,9 @@ heap_vacuum_rel(Relation rel, const VacuumParams *params, ErrorContextCallback errcallback; char **indnames = NULL; Size dead_items_max_bytes = 0; + PgStat_VacuumRelationCounts extVacReport; + memset(&extVacReport, 0, sizeof(extVacReport)); verbose = (params->options & VACOPT_VERBOSE) != 0; instrument = (verbose || (AmAutoVacuumWorkerProcess() && params->log_vacuum_min_duration >= 0)); @@ -691,6 +723,7 @@ heap_vacuum_rel(Relation rel, const VacuumParams *params, vacrel->dbname = get_database_name(MyDatabaseId); vacrel->relnamespace = get_namespace_name(RelationGetNamespace(rel)); vacrel->relname = pstrdup(RelationGetRelationName(rel)); + vacrel->reloid = RelationGetRelid(rel); vacrel->indname = NULL; vacrel->phase = VACUUM_ERRCB_PHASE_UNKNOWN; vacrel->verbose = verbose; @@ -995,13 +1028,30 @@ heap_vacuum_rel(Relation rel, const VacuumParams *params, * leader's sleeps only; parallel workers account their own sleeps to the * indexes they process. */ - pgstat_report_vacuum(rel, - Max(vacrel->new_live_tuples, 0), - vacrel->recently_dead_tuples + - vacrel->missed_dead_tuples, - starttime, - (PgStat_Counter) rint(VacuumDelayTime - startdelaytime), - VacuumFailsafeActive); + if (set_report_vacuum_hook) + { + accumulate_heap_vacuum_statistics(vacrel, &extVacReport); + + pgstat_report_vacuum_ext(rel, + Max(vacrel->new_live_tuples, 0), + vacrel->recently_dead_tuples + + vacrel->missed_dead_tuples, + starttime, + (PgStat_Counter) rint(VacuumDelayTime - + startdelaytime), + VacuumFailsafeActive, + &extVacReport); + } + else + pgstat_report_vacuum_ext(rel, + Max(vacrel->new_live_tuples, 0), + vacrel->recently_dead_tuples + + vacrel->missed_dead_tuples, + starttime, + (PgStat_Counter) rint(VacuumDelayTime - + startdelaytime), + VacuumFailsafeActive, + NULL); pgstat_progress_end_command(); if (instrument) @@ -3030,7 +3080,15 @@ lazy_vacuum_one_index(Relation indrel, IndexBulkDeleteResult *istat, LVSavedErrInfo saved_err_info; TimestampTz istarttime = GetCurrentTimestamp(); double startdelaytime = VacuumDelayTime; + double prev_tuples_removed = 0; + PgStat_VacuumRelationCounts extVacReport; + /* + * Snapshot the running bulkdelete totals: an index may be processed + * several times per vacuum, and the report below covers this pass only. + */ + if (istat != NULL) + prev_tuples_removed = istat->tuples_removed; ivinfo.index = indrel; ivinfo.heaprel = vacrel->rel; ivinfo.analyze_only = false; @@ -3048,6 +3106,7 @@ lazy_vacuum_one_index(Relation indrel, IndexBulkDeleteResult *istat, */ Assert(vacrel->indname == NULL); vacrel->indname = pstrdup(RelationGetRelationName(indrel)); + vacrel->indoid = RelationGetRelid(indrel); update_vacuum_error_info(vacrel, &saved_err_info, VACUUM_ERRCB_PHASE_VACUUM_INDEX, InvalidBlockNumber, InvalidOffsetNumber); @@ -3064,6 +3123,16 @@ lazy_vacuum_one_index(Relation indrel, IndexBulkDeleteResult *istat, startdelaytime), AmAutoVacuumWorkerProcess()); + if (set_report_vacuum_hook) + { + memset(&extVacReport, 0, sizeof(extVacReport)); + extVacReport.type = PGSTAT_EXTVAC_INDEX; + if (istat != NULL) + extVacReport.common.tuples_deleted = + istat->tuples_removed - prev_tuples_removed; + pgstat_report_vacuum_ext(indrel, -1, -1, 0, 0, false, &extVacReport); + } + /* Revert to the previous phase information for error traceback */ restore_vacuum_error_info(vacrel, &saved_err_info); pfree(vacrel->indname); @@ -3090,7 +3159,15 @@ lazy_cleanup_one_index(Relation indrel, IndexBulkDeleteResult *istat, LVSavedErrInfo saved_err_info; TimestampTz istarttime = GetCurrentTimestamp(); double startdelaytime = VacuumDelayTime; + double prev_tuples_removed = 0; + PgStat_VacuumRelationCounts extVacReport; + /* + * Snapshot the running bulkdelete totals: an index may be processed + * several times per vacuum, and the report below covers this pass only. + */ + if (istat != NULL) + prev_tuples_removed = istat->tuples_removed; ivinfo.index = indrel; ivinfo.heaprel = vacrel->rel; ivinfo.analyze_only = false; @@ -3109,6 +3186,7 @@ lazy_cleanup_one_index(Relation indrel, IndexBulkDeleteResult *istat, */ Assert(vacrel->indname == NULL); vacrel->indname = pstrdup(RelationGetRelationName(indrel)); + vacrel->indoid = RelationGetRelid(indrel); update_vacuum_error_info(vacrel, &saved_err_info, VACUUM_ERRCB_PHASE_INDEX_CLEANUP, InvalidBlockNumber, InvalidOffsetNumber); @@ -3123,6 +3201,16 @@ lazy_cleanup_one_index(Relation indrel, IndexBulkDeleteResult *istat, startdelaytime), AmAutoVacuumWorkerProcess()); + if (set_report_vacuum_hook) + { + memset(&extVacReport, 0, sizeof(extVacReport)); + extVacReport.type = PGSTAT_EXTVAC_INDEX; + if (istat != NULL) + extVacReport.common.tuples_deleted = + istat->tuples_removed - prev_tuples_removed; + pgstat_report_vacuum_ext(indrel, -1, -1, 0, 0, false, &extVacReport); + } + /* Revert to the previous phase information for error traceback */ restore_vacuum_error_info(vacrel, &saved_err_info); pfree(vacrel->indname); diff --git a/src/backend/commands/vacuumparallel.c b/src/backend/commands/vacuumparallel.c index 82e1e12501a..958bb5bd8a3 100644 --- a/src/backend/commands/vacuumparallel.c +++ b/src/backend/commands/vacuumparallel.c @@ -1080,6 +1080,8 @@ parallel_vacuum_process_one_index(ParallelVacuumState *pvs, Relation indrel, IndexVacuumInfo ivinfo; TimestampTz istarttime = GetCurrentTimestamp(); double startdelaytime = VacuumDelayTime; + double prev_tuples_removed = 0; + PgStat_VacuumRelationCounts extVacReport; /* * Update the pointer to the corresponding bulk-deletion result if someone @@ -1088,6 +1090,12 @@ parallel_vacuum_process_one_index(ParallelVacuumState *pvs, Relation indrel, if (indstats->istat_updated) istat = &(indstats->istat); + /* + * Snapshot the running bulkdelete totals: an index may be processed + * several times per vacuum, and the report below covers this pass only. + */ + if (istat != NULL) + prev_tuples_removed = istat->tuples_removed; ivinfo.index = indrel; ivinfo.heaprel = pvs->heaprel; ivinfo.analyze_only = false; @@ -1116,6 +1124,16 @@ parallel_vacuum_process_one_index(ParallelVacuumState *pvs, Relation indrel, RelationGetRelationName(indrel)); } + if (set_report_vacuum_hook) + { + 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; + pgstat_report_vacuum_ext(indrel, -1, -1, 0, 0, false, &extVacReport); + } + /* * Accumulate this pass into the index's cumulative vacuum times. Use the * leader's DSM flag to classify autovacuum: a parallel worker of an diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c index 466617cda60..01d6966390b 100644 --- a/src/backend/utils/activity/pgstat_relation.c +++ b/src/backend/utils/activity/pgstat_relation.c @@ -345,6 +345,35 @@ pgstat_report_index_vacuum_time(Relation rel, PgStat_Counter elapsedtime, pgstat_unlock_entry(entry_ref); } +/* + * Hook for extensions to receive extended vacuum statistics. + * NULL when no extension has registered. + */ +set_report_vacuum_hook_type set_report_vacuum_hook = NULL; + +/* + * Report extended vacuum statistics to extensions via set_report_vacuum_hook. + * When livetuples/deadtuples/starttime are provided (heap case), also calls + * pgstat_report_vacuum. For indexes, pass -1, -1, 0, 0 to skip pgstat_report_vacuum. + */ +void +pgstat_report_vacuum_ext(Relation rel, PgStat_Counter livetuples, + PgStat_Counter deadtuples, TimestampTz starttime, + PgStat_Counter delaytime, bool failsafe, + PgStat_VacuumRelationCounts * extstats) +{ + /* Index reports pass starttime = 0: no per-vacuum pgstat side-effects */ + if (starttime != 0) + pgstat_report_vacuum(rel, livetuples, deadtuples, starttime, delaytime, + failsafe); + + if (extstats != NULL && set_report_vacuum_hook) + (*set_report_vacuum_hook) (RelationGetRelid(rel), + rel->rd_rel->relisshared, + extstats); +} + + /* * Report that the table was just analyzed and flush IO statistics. * diff --git a/src/include/commands/vacuum.h b/src/include/commands/vacuum.h index fbc20fe2047..ffd2918b0f3 100644 --- a/src/include/commands/vacuum.h +++ b/src/include/commands/vacuum.h @@ -24,6 +24,7 @@ #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 diff --git a/src/include/pgstat.h b/src/include/pgstat.h index 8aa257e58cb..b17670610bc 100644 --- a/src/include/pgstat.h +++ b/src/include/pgstat.h @@ -93,6 +93,38 @@ typedef struct PgStat_FunctionCounts /* * Working state needed to accumulate per-function-call timing statistics. */ +/* + * Extended vacuum statistics - passed to extensions via set_report_vacuum_hook. + * Type of entry: table (heap), index, or database aggregate. + */ +typedef enum ExtVacReportType +{ + PGSTAT_EXTVAC_INVALID = 0, + PGSTAT_EXTVAC_TABLE = 1, + PGSTAT_EXTVAC_INDEX = 2, + PGSTAT_EXTVAC_DB = 3, +} ExtVacReportType; + +typedef struct PgStat_CommonCounts +{ + int64 tuples_deleted; +} PgStat_CommonCounts; + +typedef struct PgStat_VacuumRelationCounts +{ + PgStat_CommonCounts common; + ExtVacReportType type; + union + { + struct + { + int64 tuples_frozen; + int64 recently_dead_tuples; + int64 missed_dead_tuples; + } table; + }; +} PgStat_VacuumRelationCounts; + typedef struct PgStat_FunctionCallUsage { /* Link to function's hashtable entry (must still be there at exit!) */ @@ -753,6 +785,20 @@ extern void pgstat_report_index_vacuum_time(Relation rel, PgStat_Counter delaytime, bool is_autovacuum); extern void pgstat_report_vacuum_error(bool shared); + +extern void pgstat_report_vacuum_ext(Relation rel, + PgStat_Counter livetuples, + PgStat_Counter deadtuples, + TimestampTz starttime, + PgStat_Counter delaytime, + bool failsafe, + PgStat_VacuumRelationCounts * extstats); + +/* Hook for extensions to receive extended vacuum statistics */ +typedef void (*set_report_vacuum_hook_type) (Oid tableoid, bool shared, + PgStat_VacuumRelationCounts * params); +extern PGDLLIMPORT set_report_vacuum_hook_type set_report_vacuum_hook; + extern void pgstat_report_analyze(Relation rel, PgStat_Counter livetuples, PgStat_Counter deadtuples, bool resetcounter, TimestampTz starttime); -- 2.39.5 (Apple Git-154)