diff --git a/contrib/Makefile b/contrib/Makefile index d41d20614f2..45d08c2d429 100644 --- a/contrib/Makefile +++ b/contrib/Makefile @@ -60,6 +60,7 @@ SUBDIRS = \ tsm_system_rows \ tsm_system_time \ unaccent \ + ext_vacuum_statistics \ vacuumlo # Cloudberry-specific additions (to ease merge pain). diff --git a/contrib/bloom/blvacuum.c b/contrib/bloom/blvacuum.c index 2340d49e00a..6a6609be16f 100644 --- a/contrib/bloom/blvacuum.c +++ b/contrib/bloom/blvacuum.c @@ -61,7 +61,7 @@ blbulkdelete(IndexVacuumInfo *info, IndexBulkDeleteResult *stats, *itupPtr, *itupEnd; - vacuum_delay_point(); + vacuum_delay_point(false); buffer = ReadBufferExtended(index, MAIN_FORKNUM, blkno, RBM_NORMAL, info->strategy); @@ -191,7 +191,7 @@ blvacuumcleanup(IndexVacuumInfo *info, IndexBulkDeleteResult *stats) Buffer buffer; Page page; - vacuum_delay_point(); + vacuum_delay_point(false); buffer = ReadBufferExtended(index, MAIN_FORKNUM, blkno, RBM_NORMAL, info->strategy); 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..4d90451588a --- /dev/null +++ b/contrib/ext_vacuum_statistics/Makefile @@ -0,0 +1,31 @@ +# 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" + +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 + +# Cloudberry: the test of the cluster-wide views needs a running cluster +# with the module in shared_preload_libraries of every instance, which the +# temporary instances of "make check" are not, so it has a target of its +# own, run against such a cluster. +.PHONY: installcheck-cluster + +installcheck-cluster: + $(pg_regress_installcheck) --dbname=contrib_regression_vacstat gp_vacuum_statistics diff --git a/contrib/ext_vacuum_statistics/README.md b/contrib/ext_vacuum_statistics/README.md new file mode 100644 index 00000000000..f8c7bb950bc --- /dev/null +++ b/contrib/ext_vacuum_statistics/README.md @@ -0,0 +1,256 @@ +# ext_vacuum_statistics + +Extended vacuum statistics extension for PostgreSQL. It collects and exposes detailed per-table, per-index, and per-database vacuum statistics (buffer I/O, WAL, general, timing) via convenient views in the `ext_vacuum_statistics` schema. + +## Installation + +``` +./configure tmp_install="$(pwd)/my/inst" +make clean && make && make install +cd contrib/ext_vacuum_statistics +make && make install +``` + +It is essential that the extension is listed in `shared_preload_libraries` because it registers a vacuum hook at server startup. + +In your `postgresql.conf`: + +``` +shared_preload_libraries = 'ext_vacuum_statistics' +``` + +Restart PostgreSQL. + +In your database: + +```sql +CREATE EXTENSION ext_vacuum_statistics; +``` + +## Usage + +From a Cloudberry coordinator, query statistics across the cluster: + +```sql +-- Per-table heap vacuum statistics, with gp_segment_id +SELECT * FROM ext_vacuum_statistics.gp_stats_vacuum_tables; + +-- Per-index vacuum statistics +SELECT * FROM ext_vacuum_statistics.gp_stats_vacuum_indexes; + +-- Per-database aggregate vacuum statistics +SELECT * FROM ext_vacuum_statistics.gp_stats_vacuum_database; +``` + +The corresponding `pg_stats_vacuum_*` views read only the connected instance. +Use them for a standalone server or a utility connection to a single segment. +Use the `gp_stats_vacuum_*_summary` views for cluster totals. + +Example projection of table statistics: + +``` + relname | total_blks_read | total_blks_hit | wal_records | tuples_deleted | pages_removed +-----------+-----------------+----------------+-------------+----------------+--------------- + mytable | 120 | 340 | 15 | 500 | 10 +``` + +Reset statistics across the cluster from the coordinator: + +```sql +SELECT ext_vacuum_statistics.gp_vacuum_statistics_reset(); +``` + +These functions reset only extension-owned metrics; native work is unchanged. +Use the `gp_` wrappers from a normal coordinator connection to reset them +on the coordinator and all primary segments. + +**Local functions reset only the current node.** Calling +`ext_vacuum_statistics.vacuum_statistics_reset()` on the coordinator (QD) +resets only the coordinator's counters; segment counters remain unchanged. +The same applies to `extvac_reset_entry()` and `extvac_reset_db_entry()`. +Use the local functions for a utility connection to an individual segment. + +All functions below belong to the `ext_vacuum_statistics` schema: + +| Reset scope | Cluster wrapper (call on the coordinator) | Local function (current node only) | +|-------------|------------------------------------------|-----------------------------------| +| One table or index | `gp_extvac_reset_entry(dboid, relid)` | `extvac_reset_entry(dboid, relid)` | +| One database and all its relations | `gp_extvac_reset_db_entry(dboid)` | `extvac_reset_db_entry(dboid)` | +| All databases | `gp_vacuum_statistics_reset()` | `vacuum_statistics_reset()` | + +For example, reset only the current database across the cluster: + +```sql +SELECT ext_vacuum_statistics.gp_extvac_reset_db_entry(oid) +FROM pg_database WHERE datname = current_database(); +``` + +## Configuration (GUCs) + +| GUC | Default | Description | +|-----|---------|-------------| +| `vacuum_statistics.enabled` | on | Collect extension resource metrics; native work obeys `track_counts` | + +## Memory usage + +Each tracked object (table or index) uses a fixed-size shared memory entry; the exact size depends on the platform. + +Per-database aggregates add one entry per tracked database. Entry size includes +pgstat bookkeeping; memory estimates must use the structures of the actual +build rather than a fixed byte count from an earlier version. + +The entry of a table or an index is dropped when the relation is dropped (at +commit, so a rolled back `DROP` keeps it), and a new relation that gets the OID +of an old one starts from zero. The module does that with an +`object_access_hook`. + +## Recipes + +**Disable extension resource collection temporarily (native work continues):** + +```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 and resource usage) | +| `ext_vacuum_statistics.pg_stats_vacuum_indexes` | Per-index vacuum stats | +| `ext_vacuum_statistics.pg_stats_vacuum_database` | Per-database aggregate vacuum stats | + +## Limitations + +- Must be loaded via `shared_preload_libraries`; it cannot be loaded on demand. +- Starting a server without the module, even once, makes it treat the whole + statistics file as corrupted and reset all cumulative statistics, the + built-in ones included. Use `vacuum_statistics.enabled = off` rather than + removing the module. + +## Native work counters + +Heap, index and AO work counters are accumulated once, by native pgstat. +The extension's table and index views read those same counters through +`pg_stat_get_vacuum_stats()`, alongside separately stored buffer, WAL and I/O +measurements. They have the same work-counter history as `pg_stat_vacuum_tables`, +`pg_stat_vacuum_indexes` and their `gp_stat_*` counterparts; enabling the +extension does not start a second history. + +Work counters obey `track_counts`. `vacuum_statistics.enabled` controls only +extension resource metrics. +With extension collection disabled, native work remains visible in these views. +Relations without a resource entry show zero resource counters. + +Extension reset functions clear only the extension's own metrics, without +touching native work or any other core statistics. To reset native work, use +`pg_stat_reset_vacuum_stats(relid)` for one table or index, or +`pg_stat_reset_vacuum_stats()` for the current database and its non-shared +relations. These functions preserve native timing, vacuum/analyze invocation +counts, and other statistics. On Cloudberry, call the corresponding +`gp_stat_reset_vacuum_stats(relid)` or `gp_stat_reset_vacuum_stats()` on the +coordinator to reach all primary segments. The broader native reset +`pg_stat_reset_single_table_counters()` +clears native work but leaves extension resources intact; `pg_stat_reset()` +clears both for the current database. The two sets of metrics can therefore +cover different periods after a reset or a change in collection settings. + +The resource getter signatures and shared payload changed in this development +series. Rebuild the server and extension together and recreate the extension's +SQL objects when updating an earlier 1.0 installation. The new statistics-file +format discards previously saved counters; the new core function also requires +the matching catalog version. + +## Cloudberry + +Each instance (the coordinator and every segment) keeps the statistics of the +vacuums it runs itself. The local `pg_stats_vacuum_*` views show only that +instance; use `gp_stats_vacuum_*` to read the coordinator and primary segments. + +Install the matching extension library, control and SQL files in the Cloudberry +installation on every host, including mirror and standby coordinator hosts. +`gpconfig` changes configuration files; it does not distribute extension files. +Load the module on every instance so it is also available after promotion. + +Inspect both the active setting and the configuration files first: + +```sh +gpconfig -s shared_preload_libraries +gpconfig -s shared_preload_libraries --file +``` + +When the existing list is empty, configure it with: + +```sh +gpconfig -c shared_preload_libraries -v 'ext_vacuum_statistics' +``` + +Otherwise pass the complete existing list with `ext_vacuum_statistics` appended. +Preserve any intentional differences between coordinator, primary and mirror +settings; do not replace their other preloaded libraries. With `-v` and no +role restriction, `gpconfig` updates the coordinator, standby, primaries and +mirrors. Resolve any reported unreachable hosts before relying on their setup. +Check the resulting files, then restart the cluster to load the library: + +```sh +gpconfig -s shared_preload_libraries --file +gpstop -ar +gpconfig -s shared_preload_libraries --file-compare +``` + +A configuration reload (`gpstop -u`) alone cannot load a new preload library. +Check that mirror and standby instances also restarted successfully. After the +restart, run the following on the coordinator in each database where the views +are needed; `CREATE EXTENSION` is dispatched to the segments: + +```sql +CREATE EXTENSION IF NOT EXISTS ext_vacuum_statistics; +``` + +Cluster-wide views, like the `gp_stat_*` views of the core: + +| View | Description | +|------|-------------| +| `ext_vacuum_statistics.gp_stats_vacuum_tables` | `pg_stats_vacuum_tables` of every instance, with `gp_segment_id` (-1 for the coordinator) | +| `ext_vacuum_statistics.gp_stats_vacuum_indexes` | the same for indexes | +| `ext_vacuum_statistics.gp_stats_vacuum_database` | the same for databases | +| `ext_vacuum_statistics.gp_stats_vacuum_tables_summary` | one row per table: summed over the segments (divided by their number for replicated tables); catalogs as on the coordinator | +| `ext_vacuum_statistics.gp_stats_vacuum_indexes_summary` | the same for indexes | +| `ext_vacuum_statistics.gp_stats_vacuum_database_summary` | one row per database, summed over all instances | + +The reset functions without the `gp_` prefix act only on the connected instance; +`gp_vacuum_statistics_reset()`, `gp_extvac_reset_entry(dboid, relid)` and +`gp_extvac_reset_db_entry(dboid)` dispatch the resets from the coordinator to +all primary segments and also reset the coordinator. A `SET` of +`vacuum_statistics.enabled` on the coordinator is passed on to the segments. + +The counters are local pgstat state and are not replicated through WAL. +Preloading on a mirror enables collection after promotion; it does not copy +its primary's accumulated counters. A promoted instance reports its own local +statistics, so it cannot continue the former primary's counter history. This +also applies when the standby coordinator is promoted. A compatible clean +restart can preserve an instance's own counters; crash recovery resets them. + +Treat promotion as a new measurement interval for the affected instance, even +if its `gp_segment_id` is unchanged. Other instances keep their own counters, +but cluster summaries can decrease when a primary is replaced. Monitoring +should retain previous samples externally and begin a new baseline after +failover, without interpreting the change as negative vacuum work. + +The test of the cluster-wide views runs against such a cluster: + +``` +make -C contrib/ext_vacuum_statistics installcheck-cluster +``` + +Reset functions return `void` and require superuser privileges by default. +An administrator can delegate access with `GRANT EXECUTE`; cluster wrappers +also require permission to execute the corresponding local reset function. +An extension relation reset clears only that relation's resource counters, leaving its indexes and +the database aggregate unchanged. A database reset clears its aggregate and +all its relation entries. The global reset affects all databases on the current +instance; the `gp_` wrappers apply these operations on the coordinator and all +primary segments. +Existing rows remain visible with zero extension counters after a reset. +Native work counters remain unchanged. `pg_stat_reset()` also resets the +extension's entries for the current database. Resetting does not remove the type of a table, index, or database entry. diff --git a/contrib/ext_vacuum_statistics/expected/gp_vacuum_statistics.out b/contrib/ext_vacuum_statistics/expected/gp_vacuum_statistics.out new file mode 100644 index 00000000000..8ed711ced3f --- /dev/null +++ b/contrib/ext_vacuum_statistics/expected/gp_vacuum_statistics.out @@ -0,0 +1,250 @@ +-- Licensed to the Apache Software Foundation (ASF) under one or more +-- contributor license agreements. See the NOTICE file distributed with +-- this work for additional information regarding copyright ownership. +-- The ASF licenses this file to you under the Apache License, Version 2.0 +-- (the "License"); you may not use this file except in compliance with +-- the License. You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. +-- +-- Cluster-wide vacuum statistics: aggregation, collection control, reset, +-- and cleanup after DROP. +-- +-- This test runs against a Cloudberry cluster that has ext_vacuum_statistics +-- in shared_preload_libraries of every instance ("make installcheck-cluster"). +-- +CREATE EXTENSION IF NOT EXISTS ext_vacuum_statistics; +SELECT ext_vacuum_statistics.gp_vacuum_statistics_reset(); + gp_vacuum_statistics_reset +---------------------------- + +(1 row) + +SELECT oid AS dboid FROM pg_database WHERE datname = current_database() \gset +-- Prepare dead tuples: 10000 in a distributed table and 300 copies on +-- each segment in a replicated table. VACUUM also cleans the index. +CREATE TABLE gpvs_dist (id int, v text) WITH (autovacuum_enabled = off) + DISTRIBUTED BY (id); +CREATE INDEX gpvs_dist_idx ON gpvs_dist (id); +INSERT INTO gpvs_dist SELECT g, 'x' FROM generate_series(1, 10000) g; +CREATE TABLE gpvs_repl (id int) WITH (autovacuum_enabled = off) + DISTRIBUTED REPLICATED; +INSERT INTO gpvs_repl SELECT generate_series(1, 300); +DELETE FROM gpvs_dist; +DELETE FROM gpvs_repl; +VACUUM gpvs_dist, gpvs_repl; +-- Collection: expect an entry on every primary segment and the coordinator. +-- role = 'p' includes the coordinator and excludes mirrors. +SELECT count(*) = (SELECT count(*) FROM gp_segment_configuration + WHERE role = 'p') AS one_row_per_instance + FROM ext_vacuum_statistics.gp_stats_vacuum_tables + WHERE relname = 'gpvs_dist'; + one_row_per_instance +---------------------- + t +(1 row) + +-- Expect 10000 removed tuples across the segments and 0 on the coordinator. +SELECT gp_segment_id = -1 AS coordinator, sum(tuples_deleted) AS tuples_deleted + FROM ext_vacuum_statistics.gp_stats_vacuum_tables + WHERE relname = 'gpvs_dist' + GROUP BY 1 ORDER BY 1; + coordinator | tuples_deleted +-------------+---------------- + f | 10000 + t | 0 +(2 rows) + +-- Aggregation: expect 10000 for the distributed table and 300 for the +-- replicated table. Replicated counts must be averaged across segments. +SELECT relname, tuples_deleted + FROM ext_vacuum_statistics.gp_stats_vacuum_tables_summary + WHERE relname LIKE 'gpvs%' + ORDER BY relname; + relname | tuples_deleted +-----------+---------------- + gpvs_dist | 10000 + gpvs_repl | 300 +(2 rows) + +-- The index summary must also report 10000 removed tuples. +SELECT indexrelname, tuples_deleted + FROM ext_vacuum_statistics.gp_stats_vacuum_indexes_summary + WHERE indexrelname = 'gpvs_dist_idx'; + indexrelname | tuples_deleted +---------------+---------------- + gpvs_dist_idx | 10000 +(1 row) + +-- WAL volume varies, so compare the summary with the segment counters. +SELECT s.wal_records = (SELECT sum(wal_records) + FROM ext_vacuum_statistics.gp_stats_vacuum_tables + WHERE relname = 'gpvs_dist' AND gp_segment_id >= 0) + AS summary_matches_segments + FROM ext_vacuum_statistics.gp_stats_vacuum_tables_summary s + WHERE s.relname = 'gpvs_dist'; + summary_matches_segments +-------------------------- + t +(1 row) + +-- Database WAL includes these tables plus indexes and other relations; +-- it must be positive and at least as large as the table WAL total. +SELECT db_wal_records > 0 AS database_has_wal, + db_wal_records >= (SELECT sum(wal_records) + FROM ext_vacuum_statistics.gp_stats_vacuum_tables + WHERE relname LIKE 'gpvs%') AS database_covers_tables + FROM ext_vacuum_statistics.gp_stats_vacuum_database_summary + WHERE dbname = current_database(); + database_has_wal | database_covers_tables +------------------+------------------------ + t | t +(1 row) + +-- Collection control: disable collection on the coordinator, then vacuum +-- another 1000 dead tuples. Native work grows; resource counters stay unchanged. +CREATE TEMP TABLE gpvs_resources_before AS + SELECT gp_segment_id AS segid, total_blks_read, total_blks_hit, wal_records + FROM ext_vacuum_statistics.gp_stats_vacuum_tables + WHERE relname = 'gpvs_dist' + DISTRIBUTED BY (segid); +INSERT INTO gpvs_dist SELECT g, 'y' FROM generate_series(1, 1000) g; +DELETE FROM gpvs_dist; +SET vacuum_statistics.enabled = off; +VACUUM gpvs_dist; +RESET vacuum_statistics.enabled; +SELECT bool_and(s.total_blks_read = b.total_blks_read + AND s.total_blks_hit = b.total_blks_hit + AND s.wal_records = b.wal_records) AS resources_unchanged + FROM ext_vacuum_statistics.gp_stats_vacuum_tables s + JOIN gpvs_resources_before b ON b.segid = s.gp_segment_id + WHERE s.relname = 'gpvs_dist'; + resources_unchanged +--------------------- + t +(1 row) + +DROP TABLE gpvs_resources_before; +SELECT bool_and(e.tuples_deleted = n.tuples_deleted) AS native_work_matches + FROM ext_vacuum_statistics.gp_stats_vacuum_tables e + JOIN gp_stat_vacuum_tables n USING (gp_segment_id, relid) + WHERE e.relname = 'gpvs_dist'; + native_work_matches +--------------------- + t +(1 row) + +SELECT sum(tuples_deleted) AS tuples_deleted + FROM ext_vacuum_statistics.gp_stats_vacuum_tables + WHERE relname = 'gpvs_dist'; + tuples_deleted +---------------- + 11000 +(1 row) + +-- Relation reset clears extension resources across the cluster, preserving +-- native work for gpvs_dist and gpvs_repl. +SELECT ext_vacuum_statistics.gp_extvac_reset_entry(:dboid, 'gpvs_dist'::regclass); + gp_extvac_reset_entry +----------------------- + +(1 row) + +SELECT bool_and(total_blks_read = 0 AND total_blks_hit = 0 AND wal_records = 0) + AS resources_reset + FROM ext_vacuum_statistics.gp_stats_vacuum_tables + WHERE relname = 'gpvs_dist'; + resources_reset +----------------- + t +(1 row) + +SELECT coalesce(sum(tuples_deleted), 0) AS tuples_deleted + FROM ext_vacuum_statistics.gp_stats_vacuum_tables + WHERE relname = 'gpvs_dist'; + tuples_deleted +---------------- + 11000 +(1 row) + +SELECT relname, tuples_deleted + FROM ext_vacuum_statistics.gp_stats_vacuum_tables_summary + WHERE relname LIKE 'gpvs%' + ORDER BY relname; + relname | tuples_deleted +-----------+---------------- + gpvs_dist | 11000 + gpvs_repl | 300 +(2 rows) + +-- DROP cleanup: save the OID and read the statistics directly. The public +-- views join pg_class, so they would hide an orphaned entry after DROP. +-- gp_id makes this helper view executable on each segment via gp_dist_random. +-- Expect 3 segment entries and 1 coordinator entry in the regression cluster. +SELECT 'gpvs_repl'::regclass::oid AS repl_oid \gset +CREATE VIEW gpvs_repl_entry AS + SELECT s.relid + FROM gp_id, + LATERAL ext_vacuum_statistics.pg_stats_get_vacuum_tables(:dboid, :repl_oid) s; +SELECT count(*) AS segment_entries FROM gp_dist_random('gpvs_repl_entry'); + segment_entries +----------------- + 3 +(1 row) + +SELECT count(*) AS coordinator_entries FROM gpvs_repl_entry; + coordinator_entries +--------------------- + 1 +(1 row) + +-- Rolling back DROP must preserve the 3 segment entries. +BEGIN; +DROP TABLE gpvs_repl; +ROLLBACK; +SELECT count(*) AS segment_entries FROM gp_dist_random('gpvs_repl_entry'); + segment_entries +----------------- + 3 +(1 row) + +-- Committing DROP must remove entries from both segments and coordinator. +DROP TABLE gpvs_repl; +SELECT count(*) AS segment_entries FROM gp_dist_random('gpvs_repl_entry'); + segment_entries +----------------- + 0 +(1 row) + +SELECT count(*) AS coordinator_entries FROM gpvs_repl_entry; + coordinator_entries +--------------------- + 0 +(1 row) + +-- Cluster reset must preserve the remaining table's entries and zero +-- its resources on every instance, preserving native work. +SELECT ext_vacuum_statistics.gp_vacuum_statistics_reset(); + gp_vacuum_statistics_reset +---------------------------- + +(1 row) + +SELECT count(*) = (SELECT count(*) FROM gp_segment_configuration WHERE role = 'p') AS rows_preserved, + bool_and(total_blks_read = 0 AND total_blks_hit = 0 AND wal_records = 0) AS resources_reset, + sum(tuples_deleted) = 11000 AS work_preserved + FROM ext_vacuum_statistics.gp_stats_vacuum_tables + WHERE relname = 'gpvs_dist'; + rows_preserved | resources_reset | work_preserved +----------------+-----------------+---------------- + t | t | t +(1 row) + +DROP VIEW gpvs_repl_entry; +DROP TABLE gpvs_dist; 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..802bda5684c --- /dev/null +++ b/contrib/ext_vacuum_statistics/ext_vacuum_statistics--1.0.sql @@ -0,0 +1,422 @@ +-- Licensed to the Apache Software Foundation (ASF) under one or more +-- contributor license agreements. See the NOTICE file distributed with +-- this work for additional information regarding copyright ownership. +-- The ASF licenses this file to you under the Apache License, Version 2.0 +-- (the "License"); you may not use this file except in compliance with +-- the License. You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. + +/*------------------------------------------------------------------------- + * + * 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)'; + +-- These resets leave native VACUUM work counters untouched. +-- Local reset functions; use the gp_ wrappers below for a cluster-wide reset. +CREATE OR REPLACE FUNCTION ext_vacuum_statistics.extvac_reset_entry( + dboid oid, + relid oid +) +RETURNS void +AS 'MODULE_PATHNAME', 'extvac_reset_entry' +LANGUAGE C STRICT VOLATILE PARALLEL UNSAFE; + +CREATE OR REPLACE FUNCTION ext_vacuum_statistics.extvac_reset_db_entry(dboid oid) +RETURNS void +AS 'MODULE_PATHNAME', 'extvac_reset_db_entry' +LANGUAGE C STRICT VOLATILE PARALLEL UNSAFE; + +CREATE OR REPLACE FUNCTION ext_vacuum_statistics.vacuum_statistics_reset() +RETURNS void +AS 'MODULE_PATHNAME', 'vacuum_statistics_reset' +LANGUAGE C STRICT VOLATILE PARALLEL UNSAFE; + +COMMENT ON FUNCTION ext_vacuum_statistics.extvac_reset_entry(oid, oid) IS + 'Reset extension-owned vacuum metrics for one table or index on the connected instance only'; +COMMENT ON FUNCTION ext_vacuum_statistics.extvac_reset_db_entry(oid) IS + 'Reset extension-owned vacuum metrics for a database and its relations on the connected instance only'; +COMMENT ON FUNCTION ext_vacuum_statistics.vacuum_statistics_reset() IS + 'Reset extension-owned vacuum metrics for all databases on the connected instance only'; + +-- Reset privileges can be delegated explicitly, as for pg_stat_reset(). +REVOKE EXECUTE ON FUNCTION ext_vacuum_statistics.extvac_reset_entry(oid, oid) FROM PUBLIC; +REVOKE EXECUTE ON FUNCTION ext_vacuum_statistics.extvac_reset_db_entry(oid) FROM PUBLIC; +REVOKE EXECUTE ON FUNCTION ext_vacuum_statistics.vacuum_statistics_reset() FROM PUBLIC; + +-- Internal C function to fetch table VACUUM resources +CREATE OR REPLACE FUNCTION ext_vacuum_statistics.pg_stats_get_vacuum_tables( + IN dboid oid, + IN reloid oid, + OUT relid oid, + OUT total_blks_read bigint, + OUT total_blks_hit bigint, + OUT total_blks_dirtied bigint, + OUT total_blks_written bigint, + OUT wal_records bigint, + OUT wal_fpi bigint, + OUT wal_bytes numeric, + OUT blk_read_time double precision, + OUT blk_write_time double precision, + OUT rel_blks_read bigint, + OUT rel_blks_hit bigint +) +RETURNS SETOF record +AS 'MODULE_PATHNAME', 'pg_stats_get_vacuum_tables' +LANGUAGE C STRICT STABLE; + +-- Internal C function to fetch index VACUUM resources +CREATE OR REPLACE FUNCTION ext_vacuum_statistics.pg_stats_get_vacuum_indexes( + IN dboid oid, + IN reloid oid, + OUT relid oid, + OUT total_blks_read bigint, + OUT total_blks_hit bigint, + OUT total_blks_dirtied bigint, + OUT total_blks_written bigint, + OUT wal_records bigint, + OUT wal_fpi bigint, + OUT wal_bytes numeric, + OUT blk_read_time double precision, + OUT blk_write_time double precision, + OUT rel_blks_read bigint, + OUT rel_blks_hit bigint +) +RETURNS SETOF record +AS 'MODULE_PATHNAME', 'pg_stats_get_vacuum_indexes' +LANGUAGE C STRICT STABLE; + +-- Internal C function to fetch database vacuum stats +CREATE OR REPLACE FUNCTION ext_vacuum_statistics.pg_stats_get_vacuum_database( + IN dboid oid, + OUT dbid oid, + OUT total_blks_read bigint, + OUT total_blks_hit bigint, + OUT total_blks_dirtied bigint, + OUT total_blks_written bigint, + OUT wal_records bigint, + OUT wal_fpi bigint, + OUT wal_bytes numeric, + OUT blk_read_time double precision, + OUT blk_write_time double precision +) +RETURNS SETOF record +AS 'MODULE_PATHNAME', 'pg_stats_get_vacuum_database' +LANGUAGE C STRICT STABLE; + +-- View: vacuum statistics per table (heap) +CREATE VIEW ext_vacuum_statistics.pg_stats_vacuum_tables AS +SELECT + rel.oid AS relid, + ns.nspname AS schema, + rel.relname AS relname, + db.datname AS dbname, + COALESCE(stats.total_blks_read, 0) AS total_blks_read, + COALESCE(stats.total_blks_hit, 0) AS total_blks_hit, + COALESCE(stats.total_blks_dirtied, 0) AS total_blks_dirtied, + COALESCE(stats.total_blks_written, 0) AS total_blks_written, + COALESCE(stats.wal_records, 0) AS wal_records, + COALESCE(stats.wal_fpi, 0) AS wal_fpi, + COALESCE(stats.wal_bytes, 0) AS wal_bytes, + COALESCE(stats.blk_read_time, 0) AS blk_read_time, + COALESCE(stats.blk_write_time, 0) AS blk_write_time, + COALESCE(stats.rel_blks_read, 0) AS rel_blks_read, + COALESCE(stats.rel_blks_hit, 0) AS rel_blks_hit, + work.tuples_deleted, + work.pages_scanned, + work.pages_removed +FROM pg_class rel +JOIN pg_namespace ns ON ns.oid = rel.relnamespace +JOIN pg_database db ON db.datname = current_database() +CROSS JOIN LATERAL pg_catalog.pg_stat_get_vacuum_stats(rel.oid) work +LEFT JOIN LATERAL ext_vacuum_statistics.pg_stats_get_vacuum_tables(db.oid, rel.oid) stats ON true +WHERE rel.relkind IN ('r', 'm', 't', 'o', 'b', 'M'); + +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, + COALESCE(stats.total_blks_read, 0) AS total_blks_read, + COALESCE(stats.total_blks_hit, 0) AS total_blks_hit, + COALESCE(stats.total_blks_dirtied, 0) AS total_blks_dirtied, + COALESCE(stats.total_blks_written, 0) AS total_blks_written, + COALESCE(stats.wal_records, 0) AS wal_records, + COALESCE(stats.wal_fpi, 0) AS wal_fpi, + COALESCE(stats.wal_bytes, 0) AS wal_bytes, + COALESCE(stats.blk_read_time, 0) AS blk_read_time, + COALESCE(stats.blk_write_time, 0) AS blk_write_time, + COALESCE(stats.rel_blks_read, 0) AS rel_blks_read, + COALESCE(stats.rel_blks_hit, 0) AS rel_blks_hit, + work.tuples_deleted, + work.pages_deleted +FROM pg_class rel +JOIN pg_namespace ns ON ns.oid = rel.relnamespace +JOIN pg_database db ON db.datname = current_database() +CROSS JOIN LATERAL pg_catalog.pg_stat_get_vacuum_stats(rel.oid) work +LEFT JOIN LATERAL ext_vacuum_statistics.pg_stats_get_vacuum_indexes(db.oid, rel.oid) stats ON true +WHERE rel.relkind = 'i'; + +COMMENT ON VIEW ext_vacuum_statistics.pg_stats_vacuum_indexes IS + 'Extended vacuum statistics per index'; + +-- View: vacuum statistics per database (aggregate) +CREATE VIEW ext_vacuum_statistics.pg_stats_vacuum_database AS +SELECT + db.oid AS dboid, + db.datname AS dbname, + stats.total_blks_read AS db_blks_read, + stats.total_blks_hit AS db_blks_hit, + stats.total_blks_dirtied AS db_blks_dirtied, + stats.total_blks_written AS db_blks_written, + stats.wal_records AS db_wal_records, + stats.wal_fpi AS db_wal_fpi, + stats.wal_bytes AS db_wal_bytes, + stats.blk_read_time AS db_blk_read_time, + stats.blk_write_time AS db_blk_write_time +FROM pg_database db +LEFT JOIN LATERAL ext_vacuum_statistics.pg_stats_get_vacuum_database(db.oid) stats ON db.oid = stats.dbid; + +COMMENT ON VIEW ext_vacuum_statistics.pg_stats_vacuum_database IS + 'Extended vacuum statistics per database (aggregate)'; + +-- +-- Cloudberry: cluster-wide views. +-- +-- Every instance of the cluster keeps the statistics of the vacuums it runs +-- itself, and the pg_stats_vacuum_* views above only show those of the +-- instance they are queried on. The gp_stats_vacuum_* views show the rows +-- of all instances, with the gp_segment_id of each (-1 for the +-- coordinator), like the gp_stat_* views of the core. +-- +CREATE VIEW ext_vacuum_statistics.gp_stats_vacuum_tables AS +SELECT gp_execution_segment() AS gp_segment_id, * + FROM gp_dist_random('ext_vacuum_statistics.pg_stats_vacuum_tables') +UNION ALL +SELECT -1 AS gp_segment_id, * + FROM ext_vacuum_statistics.pg_stats_vacuum_tables; + +COMMENT ON VIEW ext_vacuum_statistics.gp_stats_vacuum_tables IS + 'Extended vacuum statistics per table, on every instance of the cluster'; + +CREATE VIEW ext_vacuum_statistics.gp_stats_vacuum_indexes AS +SELECT gp_execution_segment() AS gp_segment_id, * + FROM gp_dist_random('ext_vacuum_statistics.pg_stats_vacuum_indexes') +UNION ALL +SELECT -1 AS gp_segment_id, * + FROM ext_vacuum_statistics.pg_stats_vacuum_indexes; + +COMMENT ON VIEW ext_vacuum_statistics.gp_stats_vacuum_indexes IS + 'Extended vacuum statistics per index, on every instance of the cluster'; + +CREATE VIEW ext_vacuum_statistics.gp_stats_vacuum_database AS +SELECT gp_execution_segment() AS gp_segment_id, * + FROM gp_dist_random('ext_vacuum_statistics.pg_stats_vacuum_database') +UNION ALL +SELECT -1 AS gp_segment_id, * + FROM ext_vacuum_statistics.pg_stats_vacuum_database; + +COMMENT ON VIEW ext_vacuum_statistics.gp_stats_vacuum_database IS + 'Extended vacuum statistics per database, on every instance of the cluster'; + +-- +-- The *_summary views add the numbers up over the cluster, the way the +-- gp_stat_*_summary views of the core do: user relations are summed over +-- the segments (a replicated table is stored and vacuumed in full on every +-- segment, so its sums are divided by the number of segments), and the +-- system catalogs, which every instance keeps its own copy of, are shown as +-- the coordinator counts them. +-- +CREATE VIEW ext_vacuum_statistics.gp_stats_vacuum_tables_summary AS +SELECT + s.relid, + s.schema, + s.relname, + s.dbname, + (sum(s.total_blks_read) / s.divisor)::bigint AS total_blks_read, + (sum(s.total_blks_hit) / s.divisor)::bigint AS total_blks_hit, + (sum(s.total_blks_dirtied) / s.divisor)::bigint AS total_blks_dirtied, + (sum(s.total_blks_written) / s.divisor)::bigint AS total_blks_written, + (sum(s.wal_records) / s.divisor)::bigint AS wal_records, + (sum(s.wal_fpi) / s.divisor)::bigint AS wal_fpi, + (sum(s.wal_bytes) / s.divisor) AS wal_bytes, + (sum(s.blk_read_time) / s.divisor) AS blk_read_time, + (sum(s.blk_write_time) / s.divisor) AS blk_write_time, + (sum(s.rel_blks_read) / s.divisor)::bigint AS rel_blks_read, + (sum(s.rel_blks_hit) / s.divisor)::bigint AS rel_blks_hit, + (sum(s.tuples_deleted) / s.divisor)::bigint AS tuples_deleted, + (sum(s.pages_scanned) / s.divisor)::bigint AS pages_scanned, + (sum(s.pages_removed) / s.divisor)::bigint AS pages_removed +FROM ( + SELECT v.*, + CASE WHEN d.policytype = 'r' THEN d.numsegments ELSE 1 END AS divisor + FROM gp_dist_random('ext_vacuum_statistics.pg_stats_vacuum_tables') v + LEFT JOIN (SELECT relid, unnest(ARRAY[segrelid, blkdirrelid, visimaprelid]) AS auxrelid + FROM pg_appendonly) a ON a.auxrelid = v.relid + LEFT JOIN gp_distribution_policy d ON d.localoid = coalesce(a.relid, v.relid) + WHERE v.relid >= 16384 +) s +GROUP BY s.relid, s.schema, s.relname, s.dbname, s.divisor +UNION ALL +SELECT + relid, + schema, + relname, + dbname, + total_blks_read, + total_blks_hit, + total_blks_dirtied, + total_blks_written, + wal_records, + wal_fpi, + wal_bytes, + blk_read_time, + blk_write_time, + rel_blks_read, + rel_blks_hit, + tuples_deleted, + pages_scanned, + pages_removed +FROM ext_vacuum_statistics.pg_stats_vacuum_tables +WHERE relid < 16384; + +COMMENT ON VIEW ext_vacuum_statistics.gp_stats_vacuum_tables_summary IS + 'Extended vacuum statistics per table, summed over the cluster'; + +CREATE VIEW ext_vacuum_statistics.gp_stats_vacuum_indexes_summary AS +SELECT + s.indexrelid, + s.schema, + s.indexrelname, + s.dbname, + (sum(s.total_blks_read) / s.divisor)::bigint AS total_blks_read, + (sum(s.total_blks_hit) / s.divisor)::bigint AS total_blks_hit, + (sum(s.total_blks_dirtied) / s.divisor)::bigint AS total_blks_dirtied, + (sum(s.total_blks_written) / s.divisor)::bigint AS total_blks_written, + (sum(s.wal_records) / s.divisor)::bigint AS wal_records, + (sum(s.wal_fpi) / s.divisor)::bigint AS wal_fpi, + (sum(s.wal_bytes) / s.divisor) AS wal_bytes, + (sum(s.blk_read_time) / s.divisor) AS blk_read_time, + (sum(s.blk_write_time) / s.divisor) AS blk_write_time, + (sum(s.rel_blks_read) / s.divisor)::bigint AS rel_blks_read, + (sum(s.rel_blks_hit) / s.divisor)::bigint AS rel_blks_hit, + (sum(s.tuples_deleted) / s.divisor)::bigint AS tuples_deleted, + (sum(s.pages_deleted) / s.divisor)::bigint AS pages_deleted +FROM ( + SELECT v.*, + CASE WHEN d.policytype = 'r' THEN d.numsegments ELSE 1 END AS divisor + FROM gp_dist_random('ext_vacuum_statistics.pg_stats_vacuum_indexes') v + JOIN pg_index i ON i.indexrelid = v.indexrelid + LEFT JOIN (SELECT relid, unnest(ARRAY[segrelid, blkdirrelid, visimaprelid]) AS auxrelid + FROM pg_appendonly) a ON a.auxrelid = i.indrelid + LEFT JOIN gp_distribution_policy d ON d.localoid = coalesce(a.relid, i.indrelid) + WHERE v.indexrelid >= 16384 +) s +GROUP BY s.indexrelid, s.schema, s.indexrelname, s.dbname, s.divisor +UNION ALL +SELECT + indexrelid, + schema, + indexrelname, + dbname, + total_blks_read, + total_blks_hit, + total_blks_dirtied, + total_blks_written, + wal_records, + wal_fpi, + wal_bytes, + blk_read_time, + blk_write_time, + rel_blks_read, + rel_blks_hit, + tuples_deleted, + pages_deleted +FROM ext_vacuum_statistics.pg_stats_vacuum_indexes +WHERE indexrelid < 16384; + +COMMENT ON VIEW ext_vacuum_statistics.gp_stats_vacuum_indexes_summary IS + 'Extended vacuum statistics per index, summed over the cluster'; + +-- The database aggregates are summed over all instances, the coordinator +-- included: they are the vacuum work done in the database cluster-wide. +CREATE VIEW ext_vacuum_statistics.gp_stats_vacuum_database_summary AS +SELECT + dboid, + dbname, + sum(db_blks_read)::bigint AS db_blks_read, + sum(db_blks_hit)::bigint AS db_blks_hit, + sum(db_blks_dirtied)::bigint AS db_blks_dirtied, + sum(db_blks_written)::bigint AS db_blks_written, + sum(db_wal_records)::bigint AS db_wal_records, + sum(db_wal_fpi)::bigint AS db_wal_fpi, + sum(db_wal_bytes) AS db_wal_bytes, + sum(db_blk_read_time) AS db_blk_read_time, + sum(db_blk_write_time) AS db_blk_write_time +FROM ext_vacuum_statistics.gp_stats_vacuum_database +GROUP BY dboid, dbname; + +COMMENT ON VIEW ext_vacuum_statistics.gp_stats_vacuum_database_summary IS + 'Extended vacuum statistics per database, summed over the cluster'; + +-- +-- Cloudberry: resetting on the whole cluster. The reset functions above act +-- on the instance they are called on; these run them on the coordinator and +-- on every primary segment. Call these wrappers from a normal coordinator +-- connection; use the local functions in utility mode. +-- +CREATE FUNCTION ext_vacuum_statistics.gp_vacuum_statistics_reset() +RETURNS void +AS $$ + SELECT ext_vacuum_statistics.vacuum_statistics_reset() FROM gp_dist_random('gp_id'); + SELECT ext_vacuum_statistics.vacuum_statistics_reset(); +$$ LANGUAGE sql VOLATILE; + +CREATE FUNCTION ext_vacuum_statistics.gp_extvac_reset_entry(dboid oid, relid oid) +RETURNS void +AS $$ + SELECT ext_vacuum_statistics.extvac_reset_entry(dboid, relid) FROM gp_dist_random('gp_id'); + SELECT ext_vacuum_statistics.extvac_reset_entry(dboid, relid); +$$ LANGUAGE sql STRICT VOLATILE; + +CREATE FUNCTION ext_vacuum_statistics.gp_extvac_reset_db_entry(dboid oid) +RETURNS void +AS $$ + SELECT ext_vacuum_statistics.extvac_reset_db_entry(dboid) FROM gp_dist_random('gp_id'); + SELECT ext_vacuum_statistics.extvac_reset_db_entry(dboid); +$$ LANGUAGE sql STRICT VOLATILE; + +COMMENT ON FUNCTION ext_vacuum_statistics.gp_extvac_reset_entry(oid, oid) IS + 'Reset extension-owned vacuum metrics for one table or index on the coordinator and all primary segments; call on the coordinator'; +COMMENT ON FUNCTION ext_vacuum_statistics.gp_extvac_reset_db_entry(oid) IS + 'Reset extension-owned vacuum metrics for a database and its relations on the coordinator and all primary segments; call on the coordinator'; +COMMENT ON FUNCTION ext_vacuum_statistics.gp_vacuum_statistics_reset() IS + 'Reset extension-owned vacuum metrics for all databases on the coordinator and all primary segments; call on the coordinator'; + +REVOKE EXECUTE ON FUNCTION ext_vacuum_statistics.gp_extvac_reset_entry(oid, oid) FROM PUBLIC; +REVOKE EXECUTE ON FUNCTION ext_vacuum_statistics.gp_extvac_reset_db_entry(oid) FROM PUBLIC; +REVOKE EXECUTE ON FUNCTION ext_vacuum_statistics.gp_vacuum_statistics_reset() FROM PUBLIC; 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..c18eb26c54b --- /dev/null +++ b/contrib/ext_vacuum_statistics/ext_vacuum_statistics.conf @@ -0,0 +1,17 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to you under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# 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..555d2131233 --- /dev/null +++ b/contrib/ext_vacuum_statistics/meson.build @@ -0,0 +1,36 @@ +# 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(), + 'tap': { + 'tests': [ + 't/052_vacuum_extending_basic_test.pl', + 't/054_vacuum_extending_gucs_test.pl', + 't/056_vacuum_stats_gc.pl', + 't/060_vacuum_stats_reset.pl', + ], + }, +} diff --git a/contrib/ext_vacuum_statistics/sql/gp_vacuum_statistics.sql b/contrib/ext_vacuum_statistics/sql/gp_vacuum_statistics.sql new file mode 100644 index 00000000000..1535470a12e --- /dev/null +++ b/contrib/ext_vacuum_statistics/sql/gp_vacuum_statistics.sql @@ -0,0 +1,156 @@ +-- Licensed to the Apache Software Foundation (ASF) under one or more +-- contributor license agreements. See the NOTICE file distributed with +-- this work for additional information regarding copyright ownership. +-- The ASF licenses this file to you under the Apache License, Version 2.0 +-- (the "License"); you may not use this file except in compliance with +-- the License. You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. + +-- +-- Cluster-wide vacuum statistics: aggregation, collection control, reset, +-- and cleanup after DROP. +-- +-- This test runs against a Cloudberry cluster that has ext_vacuum_statistics +-- in shared_preload_libraries of every instance ("make installcheck-cluster"). +-- +CREATE EXTENSION IF NOT EXISTS ext_vacuum_statistics; +SELECT ext_vacuum_statistics.gp_vacuum_statistics_reset(); + +SELECT oid AS dboid FROM pg_database WHERE datname = current_database() \gset + +-- Prepare dead tuples: 10000 in a distributed table and 300 copies on +-- each segment in a replicated table. VACUUM also cleans the index. +CREATE TABLE gpvs_dist (id int, v text) WITH (autovacuum_enabled = off) + DISTRIBUTED BY (id); +CREATE INDEX gpvs_dist_idx ON gpvs_dist (id); +INSERT INTO gpvs_dist SELECT g, 'x' FROM generate_series(1, 10000) g; +CREATE TABLE gpvs_repl (id int) WITH (autovacuum_enabled = off) + DISTRIBUTED REPLICATED; +INSERT INTO gpvs_repl SELECT generate_series(1, 300); +DELETE FROM gpvs_dist; +DELETE FROM gpvs_repl; +VACUUM gpvs_dist, gpvs_repl; + +-- Collection: expect an entry on every primary segment and the coordinator. +-- role = 'p' includes the coordinator and excludes mirrors. +SELECT count(*) = (SELECT count(*) FROM gp_segment_configuration + WHERE role = 'p') AS one_row_per_instance + FROM ext_vacuum_statistics.gp_stats_vacuum_tables + WHERE relname = 'gpvs_dist'; + +-- Expect 10000 removed tuples across the segments and 0 on the coordinator. +SELECT gp_segment_id = -1 AS coordinator, sum(tuples_deleted) AS tuples_deleted + FROM ext_vacuum_statistics.gp_stats_vacuum_tables + WHERE relname = 'gpvs_dist' + GROUP BY 1 ORDER BY 1; + +-- Aggregation: expect 10000 for the distributed table and 300 for the +-- replicated table. Replicated counts must be averaged across segments. +SELECT relname, tuples_deleted + FROM ext_vacuum_statistics.gp_stats_vacuum_tables_summary + WHERE relname LIKE 'gpvs%' + ORDER BY relname; + +-- The index summary must also report 10000 removed tuples. +SELECT indexrelname, tuples_deleted + FROM ext_vacuum_statistics.gp_stats_vacuum_indexes_summary + WHERE indexrelname = 'gpvs_dist_idx'; + +-- WAL volume varies, so compare the summary with the segment counters. +SELECT s.wal_records = (SELECT sum(wal_records) + FROM ext_vacuum_statistics.gp_stats_vacuum_tables + WHERE relname = 'gpvs_dist' AND gp_segment_id >= 0) + AS summary_matches_segments + FROM ext_vacuum_statistics.gp_stats_vacuum_tables_summary s + WHERE s.relname = 'gpvs_dist'; + +-- Database WAL includes these tables plus indexes and other relations; +-- it must be positive and at least as large as the table WAL total. +SELECT db_wal_records > 0 AS database_has_wal, + db_wal_records >= (SELECT sum(wal_records) + FROM ext_vacuum_statistics.gp_stats_vacuum_tables + WHERE relname LIKE 'gpvs%') AS database_covers_tables + FROM ext_vacuum_statistics.gp_stats_vacuum_database_summary + WHERE dbname = current_database(); + +-- Collection control: disable collection on the coordinator, then vacuum +-- another 1000 dead tuples. Native work grows; resource counters stay unchanged. +CREATE TEMP TABLE gpvs_resources_before AS + SELECT gp_segment_id AS segid, total_blks_read, total_blks_hit, wal_records + FROM ext_vacuum_statistics.gp_stats_vacuum_tables + WHERE relname = 'gpvs_dist' + DISTRIBUTED BY (segid); +INSERT INTO gpvs_dist SELECT g, 'y' FROM generate_series(1, 1000) g; +DELETE FROM gpvs_dist; +SET vacuum_statistics.enabled = off; +VACUUM gpvs_dist; +RESET vacuum_statistics.enabled; +SELECT bool_and(s.total_blks_read = b.total_blks_read + AND s.total_blks_hit = b.total_blks_hit + AND s.wal_records = b.wal_records) AS resources_unchanged + FROM ext_vacuum_statistics.gp_stats_vacuum_tables s + JOIN gpvs_resources_before b ON b.segid = s.gp_segment_id + WHERE s.relname = 'gpvs_dist'; +DROP TABLE gpvs_resources_before; +SELECT bool_and(e.tuples_deleted = n.tuples_deleted) AS native_work_matches + FROM ext_vacuum_statistics.gp_stats_vacuum_tables e + JOIN gp_stat_vacuum_tables n USING (gp_segment_id, relid) + WHERE e.relname = 'gpvs_dist'; +SELECT sum(tuples_deleted) AS tuples_deleted + FROM ext_vacuum_statistics.gp_stats_vacuum_tables + WHERE relname = 'gpvs_dist'; + +-- Relation reset clears extension resources across the cluster, preserving +-- native work for gpvs_dist and gpvs_repl. +SELECT ext_vacuum_statistics.gp_extvac_reset_entry(:dboid, 'gpvs_dist'::regclass); +SELECT bool_and(total_blks_read = 0 AND total_blks_hit = 0 AND wal_records = 0) + AS resources_reset + FROM ext_vacuum_statistics.gp_stats_vacuum_tables + WHERE relname = 'gpvs_dist'; +SELECT coalesce(sum(tuples_deleted), 0) AS tuples_deleted + FROM ext_vacuum_statistics.gp_stats_vacuum_tables + WHERE relname = 'gpvs_dist'; +SELECT relname, tuples_deleted + FROM ext_vacuum_statistics.gp_stats_vacuum_tables_summary + WHERE relname LIKE 'gpvs%' + ORDER BY relname; + +-- DROP cleanup: save the OID and read the statistics directly. The public +-- views join pg_class, so they would hide an orphaned entry after DROP. +-- gp_id makes this helper view executable on each segment via gp_dist_random. +-- Expect 3 segment entries and 1 coordinator entry in the regression cluster. +SELECT 'gpvs_repl'::regclass::oid AS repl_oid \gset +CREATE VIEW gpvs_repl_entry AS + SELECT s.relid + FROM gp_id, + LATERAL ext_vacuum_statistics.pg_stats_get_vacuum_tables(:dboid, :repl_oid) s; +SELECT count(*) AS segment_entries FROM gp_dist_random('gpvs_repl_entry'); +SELECT count(*) AS coordinator_entries FROM gpvs_repl_entry; +-- Rolling back DROP must preserve the 3 segment entries. +BEGIN; +DROP TABLE gpvs_repl; +ROLLBACK; +SELECT count(*) AS segment_entries FROM gp_dist_random('gpvs_repl_entry'); +-- Committing DROP must remove entries from both segments and coordinator. +DROP TABLE gpvs_repl; +SELECT count(*) AS segment_entries FROM gp_dist_random('gpvs_repl_entry'); +SELECT count(*) AS coordinator_entries FROM gpvs_repl_entry; + +-- Cluster reset must preserve the remaining table's entries and zero +-- its resources on every instance, preserving native work. +SELECT ext_vacuum_statistics.gp_vacuum_statistics_reset(); +SELECT count(*) = (SELECT count(*) FROM gp_segment_configuration WHERE role = 'p') AS rows_preserved, + bool_and(total_blks_read = 0 AND total_blks_hit = 0 AND wal_records = 0) AS resources_reset, + sum(tuples_deleted) = 11000 AS work_preserved + FROM ext_vacuum_statistics.gp_stats_vacuum_tables + WHERE relname = 'gpvs_dist'; + +DROP VIEW gpvs_repl_entry; +DROP TABLE gpvs_dist; diff --git a/contrib/ext_vacuum_statistics/t/052_vacuum_extending_basic_test.pl b/contrib/ext_vacuum_statistics/t/052_vacuum_extending_basic_test.pl new file mode 100644 index 00000000000..e97d2eb9a29 --- /dev/null +++ b/contrib/ext_vacuum_statistics/t/052_vacuum_extending_basic_test.pl @@ -0,0 +1,368 @@ +# Copyright (c) 2025 PostgreSQL Global Development Group +# Test cumulative vacuum stats system using TAP +# +# This test validates the accuracy and behavior of cumulative vacuum statistics +# across heap tables, indexes, and databases using: +# +# • ext_vacuum_statistics.pg_stats_vacuum_tables +# • ext_vacuum_statistics.pg_stats_vacuum_indexes +# • ext_vacuum_statistics.pg_stats_vacuum_database +# +# A polling helper function repeatedly checks the stats views until expected +# deltas appear or a configurable timeout expires. This guarantees that +# stats-collector propagation delays do not lead to flaky test behavior. + +use strict; +use warnings; +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +#------------------------------------------------------------------------------ +# Test harness setup +#------------------------------------------------------------------------------ + +my $node = PostgreSQL::Test::Cluster->new('stat_vacuum'); +$node->init; + +# Configure the server: preload extension and logging level +$node->append_conf('postgresql.conf', q{ + shared_preload_libraries = 'ext_vacuum_statistics' + log_min_messages = notice +}); + +my $stderr; +my $base_stats; +my $wals; +my $ibase_stats; +my $iwals; + +$node->start( + '>' => \$base_stats, + '2>' => \$stderr +); + +#------------------------------------------------------------------------------ +# Database creation and initialization +#------------------------------------------------------------------------------ + +$node->safe_psql('postgres', q{ + CREATE DATABASE statistic_vacuum_database_regression; + CREATE EXTENSION ext_vacuum_statistics; +}); +# Main test database name and number of rows to insert +my $dbname = 'statistic_vacuum_database_regression'; +# The scenarios below expect the table and its index to span several pages +# of the default 8kB size; scale the row count with the block size, so that +# builds with larger blocks (32kB in Cloudberry) keep the same page counts. +my $size_tab = 1000 * + ($node->safe_psql('postgres', 'SHOW block_size') / 8192); + +# Enable required session settings and force the stats collector to flush next +$node->safe_psql($dbname, q{ + SET track_functions = 'all'; + SELECT pg_stat_force_next_flush(); +}); + +#------------------------------------------------------------------------------ +# Create test table and populate it +#------------------------------------------------------------------------------ + +$node->safe_psql( + $dbname, + "CREATE EXTENSION ext_vacuum_statistics; + CREATE TABLE vestat (x int PRIMARY KEY) + WITH (autovacuum_enabled = off, fillfactor = 10); + INSERT INTO vestat SELECT x FROM generate_series(1, $size_tab) AS g(x); + ANALYZE vestat;" +); + +#------------------------------------------------------------------------------ +# Timing parameters for polling loops +#------------------------------------------------------------------------------ + +my $timeout = 30; # overall wait timeout in seconds +my $interval = 0.015; # poll interval in seconds (15 ms) +my $start_time = time(); +my $updated = 0; + +#------------------------------------------------------------------------------ +# wait_for_vacuum_stats +# +# Polls ext_vacuum_statistics.pg_stats_vacuum_tables and ext_vacuum_statistics.pg_stats_vacuum_indexes until both the +# table-level and index-level counters exceed the provided baselines, or until +# the configured timeout elapses. +# +# Expected named args (baseline values): +# tab_tuples_deleted +# tab_wal_records +# idx_tuples_deleted +# idx_wal_records +# +# Returns: 1 if the condition is met before timeout, 0 otherwise. +#------------------------------------------------------------------------------ + +sub wait_for_vacuum_stats { + my (%args) = @_; + my $tab_tuples_deleted = ($args{tab_tuples_deleted} or 0); + my $tab_wal_records = ($args{tab_wal_records} or 0); + my $idx_tuples_deleted = ($args{idx_tuples_deleted} or 0); + my $idx_wal_records = ($args{idx_wal_records} or 0); + + my $start = time(); + while ((time() - $start) < $timeout) { + + my $result_query = $node->safe_psql( + $dbname, + "VACUUM vestat; + SELECT + (SELECT (tuples_deleted > $tab_tuples_deleted AND wal_records > $tab_wal_records) + FROM ext_vacuum_statistics.pg_stats_vacuum_tables + WHERE relname = 'vestat') + AND + (SELECT (tuples_deleted > $idx_tuples_deleted AND wal_records > $idx_wal_records) + FROM ext_vacuum_statistics.pg_stats_vacuum_indexes + WHERE indexrelname = 'vestat_pkey');" + ); + + return 1 if ($result_query eq 't'); + + sleep($interval); + } + + return 0; +} + +#------------------------------------------------------------------------------ +# Variables to hold vacuum-stat snapshots for later comparisons +#------------------------------------------------------------------------------ + +my $tuples_deleted = 0; +my $pages_scanned = 0; +my $pages_removed = 0; +my $wal_records = 0; +my $wal_bytes = 0; +my $wal_fpi = 0; + +my $index_tuples_deleted = 0; +my $index_pages_deleted = 0; +my $index_wal_records = 0; +my $index_wal_bytes = 0; +my $index_wal_fpi = 0; + +my $tuples_deleted_prev = 0; +my $pages_scanned_prev = 0; +my $pages_removed_prev = 0; +my $wal_records_prev = 0; +my $wal_bytes_prev = 0; +my $wal_fpi_prev = 0; + +my $index_tuples_deleted_prev = 0; +my $index_pages_deleted_prev = 0; +my $index_wal_records_prev = 0; +my $index_wal_bytes_prev = 0; +my $index_wal_fpi_prev = 0; + +#------------------------------------------------------------------------------ +# fetch_vacuum_stats +# +# Reads current values of relevant vacuum counters for the test table and its +# primary index, storing them in package variables for subsequent comparisons. +#------------------------------------------------------------------------------ + +sub fetch_vacuum_stats { + # fetch actual base vacuum statistics + my $base_statistics = $node->safe_psql( + $dbname, + "SELECT tuples_deleted, pages_scanned, pages_removed, wal_records, wal_bytes, wal_fpi + FROM ext_vacuum_statistics.pg_stats_vacuum_tables + WHERE relname = 'vestat';" + ); + + $base_statistics =~ s/\s*\|\s*/ /g; # transform " | " into space + ($tuples_deleted, $pages_scanned, $pages_removed, $wal_records, $wal_bytes, $wal_fpi) + = split /\s+/, $base_statistics; + + # --- index stats --- + my $index_base_statistics = $node->safe_psql( + $dbname, + "SELECT tuples_deleted, pages_deleted, wal_records, wal_bytes, wal_fpi + FROM ext_vacuum_statistics.pg_stats_vacuum_indexes + WHERE indexrelname = 'vestat_pkey';" + ); + + $index_base_statistics =~ s/\s*\|\s*/ /g; # transform " | " into space + ($index_tuples_deleted, $index_pages_deleted, $index_wal_records, $index_wal_bytes, $index_wal_fpi) + = split /\s+/, $index_base_statistics; +} + +#------------------------------------------------------------------------------ +# save_vacuum_stats +# +# Save current values (previously fetched by fetch_vacuum_stats) so that we +# later fetch new values and compare them. +#------------------------------------------------------------------------------ +sub save_vacuum_stats { + $tuples_deleted_prev = $tuples_deleted; + $pages_scanned_prev = $pages_scanned; + $pages_removed_prev = $pages_removed; + $wal_records_prev = $wal_records; + $wal_bytes_prev = $wal_bytes; + $wal_fpi_prev = $wal_fpi; + + $index_tuples_deleted_prev = $index_tuples_deleted; + $index_pages_deleted_prev = $index_pages_deleted; + $index_wal_records_prev = $index_wal_records; + $index_wal_bytes_prev = $index_wal_bytes; + $index_wal_fpi_prev = $index_wal_fpi; +} + +#------------------------------------------------------------------------------ +# print_vacuum_stats_on_error +# +# Print values in case of an error +#------------------------------------------------------------------------------ +sub print_vacuum_stats_on_error { + diag( + "Statistics in the failed test\n" . + "Table statistics:\n" . + " Before test:\n" . + " tuples_deleted = $tuples_deleted_prev\n" . + " pages_scanned = $pages_scanned_prev\n" . + " pages_removed = $pages_removed_prev\n" . + " wal_records = $wal_records_prev\n" . + " wal_bytes = $wal_bytes_prev\n" . + " wal_fpi = $wal_fpi_prev\n" . + " After test:\n" . + " tuples_deleted = $tuples_deleted\n" . + " pages_scanned = $pages_scanned\n" . + " pages_removed = $pages_removed\n" . + " wal_records = $wal_records\n" . + " wal_bytes = $wal_bytes\n" . + " wal_fpi = $wal_fpi\n" . + "Index statistics:\n" . + " Before test:\n" . + " tuples_deleted = $index_tuples_deleted_prev\n" . + " pages_deleted = $index_pages_deleted_prev\n" . + " wal_records = $index_wal_records_prev\n" . + " wal_bytes = $index_wal_bytes_prev\n" . + " wal_fpi = $index_wal_fpi_prev\n" . + " After test:\n" . + " tuples_deleted = $index_tuples_deleted\n" . + " pages_deleted = $index_pages_deleted\n" . + " wal_records = $index_wal_records\n" . + " wal_bytes = $index_wal_bytes\n" . + " wal_fpi = $index_wal_fpi\n" + ); +}; + + + +#------------------------------------------------------------------------------ +# Test 1: Delete half the rows, run VACUUM, and wait for stats to advance +#------------------------------------------------------------------------------ +subtest 'Test 1: Delete half the rows, run VACUUM' => sub +{ + +$node->safe_psql($dbname, "DELETE FROM vestat WHERE x % 2 = 0;"); +$node->safe_psql($dbname, "VACUUM vestat;"); + +# Poll the stats view until expected deltas appear or timeout +$updated = wait_for_vacuum_stats( + tab_tuples_deleted => 0, + tab_wal_records => 0, + idx_tuples_deleted => 0, + idx_wal_records => 0, +); +ok($updated, 'vacuum stats updated after vacuuming half-deleted table (tuples_deleted and wal_fpi advanced)') + or diag "Timeout waiting for ext_vacuum_statistics update after $timeout seconds after vacuuming half-deleted table"; + +fetch_vacuum_stats(); + +ok($tuples_deleted > $tuples_deleted_prev, 'table tuples_deleted has increased'); +ok($pages_scanned > $pages_scanned_prev, 'table pages_scanned has increased'); +ok($pages_removed == $pages_removed_prev, 'table pages_removed stay the same'); +ok($wal_records > $wal_records_prev, 'table wal_records has increased'); +ok($wal_bytes > $wal_bytes_prev, 'table wal_bytes has increased'); +ok($wal_fpi > $wal_fpi_prev, 'table wal_fpi has increased'); + +ok($index_pages_deleted == $index_pages_deleted_prev, 'index pages_deleted stay the same'); +ok($index_tuples_deleted > $index_tuples_deleted_prev, 'index tuples_deleted has increased'); +ok($index_wal_records > $index_wal_records_prev, 'index wal_records has increased'); +ok($index_wal_bytes > $index_wal_bytes_prev, 'index wal_bytes has increased'); +ok($index_wal_fpi == $index_wal_fpi_prev, 'index wal_fpi stay the same'); + +} or print_vacuum_stats_on_error(); + +#------------------------------------------------------------------------------ +# Test 2: Delete all rows, run VACUUM, and wait for stats to advance +#------------------------------------------------------------------------------ +subtest 'Test 2: Delete all rows, run VACUUM' => sub +{ +save_vacuum_stats(); + +$node->safe_psql($dbname, "DELETE FROM vestat;"); +$node->safe_psql($dbname, "VACUUM vestat;"); + +$updated = wait_for_vacuum_stats( + tab_tuples_deleted => $tuples_deleted_prev, + tab_wal_records => $wal_records_prev, + idx_tuples_deleted => $index_tuples_deleted_prev, + idx_wal_records => $index_wal_records_prev, +); + +ok($updated, 'vacuum stats updated after vacuuming all-deleted table (tuples_deleted and wal_records advanced)') + or diag "Timeout waiting for ext_vacuum_statistics update after $timeout seconds after vacuuming all-deleted table"; + +fetch_vacuum_stats(); + +ok($tuples_deleted > $tuples_deleted_prev, 'table tuples_deleted has increased'); +ok($pages_scanned > $pages_scanned_prev, 'table pages_scanned has increased'); +ok($pages_removed > $pages_removed_prev, 'table pages_removed has increased'); +ok($wal_records > $wal_records_prev, 'table wal_records has increased'); +ok($wal_bytes > $wal_bytes_prev, 'table wal_bytes has increased'); +ok($wal_fpi > 0, 'table wal_fpi has increased'); + +ok($index_pages_deleted > $index_pages_deleted_prev, 'index pages_deleted has increased'); +ok($index_tuples_deleted > $index_tuples_deleted_prev, 'index tuples_deleted has increased'); +ok($index_wal_records > $index_wal_records_prev, 'index wal_records has increased'); +ok($index_wal_bytes > $index_wal_bytes_prev, 'index wal_bytes has increased'); +ok($index_wal_fpi == $index_wal_fpi_prev, 'index wal_fpi stay the same'); + +} or print_vacuum_stats_on_error(); + +#------------------------------------------------------------------------------ +# Test 3: Test VACUUM FULL — it should not report to the stats collector +#------------------------------------------------------------------------------ +subtest 'Test 3: Test VACUUM FULL — it should not report to the stats collector' => sub +{ +save_vacuum_stats(); + +$node->safe_psql( + $dbname, + "INSERT INTO vestat SELECT x FROM generate_series(1, $size_tab) AS g(x); + CHECKPOINT; + DELETE FROM vestat; + VACUUM FULL vestat;" +); + +fetch_vacuum_stats(); + +ok($tuples_deleted == $tuples_deleted_prev, 'table tuples_deleted stay the same'); +ok($pages_scanned == $pages_scanned_prev, 'table pages_scanned stay the same'); +ok($pages_removed == $pages_removed_prev, 'table pages_removed stay the same'); +ok($wal_records == $wal_records_prev, 'table wal_records stay the same'); +ok($wal_bytes == $wal_bytes_prev, 'table wal_bytes stay the same'); +ok($wal_fpi == $wal_fpi_prev, 'table wal_fpi stay the same'); + +ok($index_pages_deleted == $index_pages_deleted_prev, 'index pages_deleted stay the same'); +ok($index_tuples_deleted == $index_tuples_deleted_prev, 'index tuples_deleted stay the same'); +ok($index_wal_records == $index_wal_records_prev, 'index wal_records stay the same'); +ok($index_wal_bytes == $index_wal_bytes_prev, 'index wal_bytes stay the same'); +ok($index_wal_fpi == $index_wal_fpi_prev, 'index wal_fpi stay the same'); + +} or print_vacuum_stats_on_error(); + +$node->stop; + +done_testing(); diff --git a/contrib/ext_vacuum_statistics/t/054_vacuum_extending_gucs_test.pl b/contrib/ext_vacuum_statistics/t/054_vacuum_extending_gucs_test.pl new file mode 100644 index 00000000000..b22f4f74a0b --- /dev/null +++ b/contrib/ext_vacuum_statistics/t/054_vacuum_extending_gucs_test.pl @@ -0,0 +1,105 @@ +# 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'); + + # Reset preserves existing rows. With collection disabled, their counters + # keep resources at zero while native work continues. + 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, 1, 'reset preserves the row when collection is disabled'); + + my $sums = $node->safe_psql($dbname, q{ + SELECT COALESCE(SUM(total_blks_read), 0) + + COALESCE(SUM(total_blks_hit), 0) + FROM ext_vacuum_statistics.pg_stats_vacuum_tables + WHERE relname = 'guc_test' + }); + is($sums, '0', 'no resource counters accumulated when disabled'); + is($node->safe_psql($dbname, q{ + SELECT e.pages_scanned > 0 AND e.pages_scanned = n.pages_scanned + FROM ext_vacuum_statistics.pg_stats_vacuum_tables e + JOIN pg_stat_vacuum_tables n USING (relid) + WHERE e.relname = 'guc_test' + }), 't', 'disabled resource collection still displays native work'); +}; + +$node->stop; + +done_testing(); diff --git a/contrib/ext_vacuum_statistics/t/056_vacuum_stats_gc.pl b/contrib/ext_vacuum_statistics/t/056_vacuum_stats_gc.pl new file mode 100644 index 00000000000..30f8317a197 --- /dev/null +++ b/contrib/ext_vacuum_statistics/t/056_vacuum_stats_gc.pl @@ -0,0 +1,228 @@ +# Copyright (c) 2026, PostgreSQL Global Development Group + +# Test that vacuum statistics entries do not outlive the relations they +# describe: DROP TABLE, a rolled back DROP, a restart, DROP DATABASE, and a +# new relation that gets the OID of an old one. +# +# Entries are looked up with pg_stats_get_vacuum_tables() and +# pg_stats_get_vacuum_indexes(), which find an entry by OID even when the +# relation is no longer in pg_class. + +use strict; +use warnings FATAL => 'all'; +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +my $node = PostgreSQL::Test::Cluster->new('vacstat_gc'); +$node->init; +$node->append_conf( + 'postgresql.conf', q{ +shared_preload_libraries = 'ext_vacuum_statistics' +autovacuum = off +}); +$node->start; + +$node->safe_psql('postgres', 'CREATE EXTENSION ext_vacuum_statistics'); + +# SQL that counts the statistics entries of the given tables and indexes. +sub entries_sql +{ + my ($dboid, $tables, $indexes) = @_; + my $t = join(',', @$tables); + my $i = join(',', @$indexes); + + return qq{ +SELECT (SELECT count(*) + FROM unnest('{$t}'::oid[]) AS r, + ext_vacuum_statistics.pg_stats_get_vacuum_tables($dboid, r)) + + (SELECT count(*) + FROM unnest('{$i}'::oid[]) AS r, + ext_vacuum_statistics.pg_stats_get_vacuum_indexes($dboid, r))}; +} + +sub count_entries +{ + return $node->safe_psql('postgres', entries_sql(@_)); +} + +# Wait until the entries are gone. +sub wait_for_no_entries +{ + my ($what, @args) = @_; + + $node->poll_query_until('postgres', + 'SELECT (' . entries_sql(@args) . ') = 0') + or die "timed out waiting for $what"; + return; +} + +# OIDs of the relations of the given kind whose names match a pattern. +sub relids +{ + my ($dbname, $relkind, $pattern) = @_; + + return split /\n/, + $node->safe_psql($dbname, + "SELECT oid FROM pg_class WHERE relkind = '$relkind' AND relname LIKE '$pattern' ORDER BY oid" + ); +} + +my $dboid = $node->safe_psql('postgres', + "SELECT oid FROM pg_database WHERE datname = 'postgres'"); + +# Vacuum a handful of tables, each with an index. +my @tables = map { "gc_tab$_" } (1 .. 20); + +$node->safe_psql('postgres', + join('', map { qq[ +CREATE TABLE $_ (id int); +CREATE INDEX ${_}_idx ON $_ (id); +INSERT INTO $_ SELECT generate_series(1, 100); +DELETE FROM $_;] } @tables)); +$node->safe_psql('postgres', 'VACUUM ' . join(', ', @tables)); + +my @tabids = relids('postgres', 'r', 'gc_tab%'); +my @idxids = relids('postgres', 'i', 'gc_tab%'); +is(scalar(@tabids) + scalar(@idxids), 40, 'found the tables and indexes'); +is(count_entries($dboid, \@tabids, \@idxids), + 40, 'vacuum created an entry for each table and index'); + +# A rolled back DROP must keep the statistics. +$node->safe_psql('postgres', 'BEGIN; DROP TABLE gc_tab1; ROLLBACK;'); +is(count_entries($dboid, \@tabids, \@idxids), + 40, 'rolled back DROP TABLE kept the statistics'); + +# Dropping the tables drops their entries and those of their indexes. +$node->safe_psql('postgres', 'DROP TABLE ' . join(', ', @tables)); +wait_for_no_entries('DROP TABLE to drop the statistics', + $dboid, \@tabids, \@idxids); +is(count_entries($dboid, \@tabids, \@idxids), + 0, 'DROP TABLE dropped the statistics entries'); + +# Nothing may come back from the statistics file either. +$node->restart; +is(count_entries($dboid, \@tabids, \@idxids), + 0, 'no dropped entries came back after a restart'); + +# The same for a whole database. +$node->safe_psql('postgres', 'CREATE DATABASE vacstat_gc_db'); +$node->safe_psql( + 'vacstat_gc_db', q{ +CREATE TABLE gc_dbtab (id int); +CREATE INDEX gc_dbtab_idx ON gc_dbtab (id); +INSERT INTO gc_dbtab SELECT generate_series(1, 100); +DELETE FROM gc_dbtab; +VACUUM gc_dbtab; +}); +my $otherdb = $node->safe_psql('postgres', + "SELECT oid FROM pg_database WHERE datname = 'vacstat_gc_db'"); +my @dbtabids = relids('vacstat_gc_db', 'r', 'gc_dbtab'); +my @dbidxids = relids('vacstat_gc_db', 'i', 'gc_dbtab_idx'); +is(count_entries($otherdb, \@dbtabids, \@dbidxids), + 2, 'vacuum in another database created statistics entries'); + +$node->safe_psql('postgres', 'DROP DATABASE vacstat_gc_db'); +wait_for_no_entries('DROP DATABASE to drop the statistics', + $otherdb, \@dbtabids, \@dbidxids); +is(count_entries($otherdb, \@dbtabids, \@dbidxids), + 0, 'DROP DATABASE dropped the statistics entries'); + +# A relation created on the OID of an earlier one must not inherit its +# statistics. DROP never leaves an entry behind, and OIDs are only handed +# out again after a wraparound, so fake the situation: take a vacuumed table +# out of the catalogs behind the back of DROP, which leaves its entry +# orphaned, then create a new table with the very same OIDs in binary upgrade +# mode. +$node->safe_psql( + 'postgres', q{ +CREATE TABLE gc_reuse (id int); +INSERT INTO gc_reuse SELECT generate_series(1, 100); +DELETE FROM gc_reuse; +VACUUM gc_reuse; +}); +my ($relid, $reltype, $typarray) = split /\|/, + $node->safe_psql( + 'postgres', q{ +SELECT c.oid, c.reltype, t.typarray + FROM pg_class c JOIN pg_type t ON t.oid = c.reltype + WHERE c.relname = 'gc_reuse'}); +is( $node->safe_psql( + 'postgres', + "SELECT tuples_deleted FROM ext_vacuum_statistics.pg_stats_vacuum_tables WHERE relid = $relid" + ), + '100', + 'the table whose OID gets recycled has statistics'); + +$node->safe_psql( + 'postgres', qq{ +SET allow_system_table_mods = on; +DELETE FROM pg_depend + WHERE (classid = 'pg_class'::regclass AND objid = $relid) + OR (refclassid = 'pg_class'::regclass AND refobjid = $relid) + OR (classid = 'pg_type'::regclass AND objid IN ($reltype, $typarray)) + OR (refclassid = 'pg_type'::regclass AND refobjid IN ($reltype, $typarray)); +DELETE FROM pg_attribute WHERE attrelid = $relid; +DELETE FROM pg_type WHERE oid IN ($reltype, $typarray); +DELETE FROM pg_class WHERE oid = $relid; +}); + +# Binary upgrade mode is a postmaster switch, so start the node by hand. +# Cloudberry: a standalone node also needs the options that +# PostgreSQL::Test::Cluster::start() passes to it. +$node->stop; +command_ok( + [ + 'pg_ctl', + '--pgdata' => $node->data_dir, + '--log' => $node->logfile, + '--options' => "-b -c gp_role=utility --gp_dbid=$node->{_dbid} " + . '--gp_contentid=0 -c maintenance_mode=on', + '--wait', 'start' + ], + 'started in binary upgrade mode'); + +# The relfilenumber only has to be unused; the old file is still on disk. +# Cloudberry: the OIDs are preassigned for a given namespace and name. +my ($ret, $stdout, $stderr) = $node->psql( + 'postgres', qq{ +SELECT binary_upgrade_set_next_heap_pg_class_oid('$relid'::oid, + 'public'::regnamespace, 'gc_reuse_new'); +SELECT binary_upgrade_set_next_heap_relfilenode('3000000000'::oid); +SELECT binary_upgrade_set_next_pg_type_oid('$reltype'::oid, + 'public'::regnamespace, 'gc_reuse_new'); +SELECT binary_upgrade_set_next_array_pg_type_oid('$typarray'::oid, + 'public'::regnamespace, '_gc_reuse_new'); +CREATE TABLE gc_reuse_new (id int); +}); +is($ret, 0, 'created a table on the recycled OID'); +like( + $stderr, + qr/resetting existing statistics for kind ext_vacuum_statistics_relation/, + 'creating the table reset the orphaned statistics entry'); + +command_ok( + [ + 'pg_ctl', + '--pgdata' => $node->data_dir, + '--mode' => 'fast', + '--wait', 'stop' + ], + 'stopped binary upgrade mode'); +$node->start; + +is( $node->safe_psql( + 'postgres', "SELECT oid FROM pg_class WHERE relname = 'gc_reuse_new'"), + $relid, + 'the new table got the recycled OID'); +is( $node->safe_psql( + 'postgres', q{ +SELECT coalesce((SELECT tuples_deleted + FROM ext_vacuum_statistics.pg_stats_vacuum_tables + WHERE relname = 'gc_reuse_new'), 0)}), + '0', + 'the new table did not inherit the statistics of the old one'); + +$node->stop; + +done_testing(); diff --git a/contrib/ext_vacuum_statistics/t/060_vacuum_stats_reset.pl b/contrib/ext_vacuum_statistics/t/060_vacuum_stats_reset.pl new file mode 100644 index 00000000000..0158bc436fd --- /dev/null +++ b/contrib/ext_vacuum_statistics/t/060_vacuum_stats_reset.pl @@ -0,0 +1,115 @@ +# Copyright (c) 2026, PostgreSQL Global Development Group + +# Reset privileges, scope, and preservation of the entry's type, including +# after a restart and through the built-in pg_stat_reset() path. +use strict; +use warnings FATAL => 'all'; +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +my $node = PostgreSQL::Test::Cluster->new('vacstat_reset'); +$node->init; +$node->append_conf('postgresql.conf', q{ +shared_preload_libraries = 'ext_vacuum_statistics' +autovacuum = off +}); +$node->start; +$node->safe_psql('postgres', 'CREATE DATABASE other_reset'); +for my $db ('postgres', 'other_reset') +{ + $node->safe_psql($db, q{ +CREATE EXTENSION ext_vacuum_statistics; +CREATE TABLE reset_t (id int PRIMARY KEY) WITH (autovacuum_enabled = off); +INSERT INTO reset_t SELECT generate_series(1, 1000); +DELETE FROM reset_t WHERE id < 500; +}); + $node->safe_psql($db, 'VACUUM reset_t'); +} +my $dboid = $node->safe_psql('postgres', + q[SELECT oid FROM pg_database WHERE datname = current_database()]); +my $relid = $node->safe_psql('postgres', q[SELECT 'reset_t'::regclass::oid]); +my $idxid = $node->safe_psql('postgres', q[SELECT 'reset_t_pkey'::regclass::oid]); +my $table = q[SELECT total_blks_read + total_blks_hit FROM ext_vacuum_statistics.pg_stats_vacuum_tables WHERE relname = 'reset_t']; +my $index = q[SELECT total_blks_read + total_blks_hit FROM ext_vacuum_statistics.pg_stats_vacuum_indexes WHERE indexrelname = 'reset_t_pkey']; +my $database = q[SELECT db_blks_read + db_blks_hit FROM ext_vacuum_statistics.pg_stats_vacuum_database WHERE dbname = current_database()]; +my $table_before = $node->safe_psql('postgres', $table); +my $index_before = $node->safe_psql('postgres', $index); +my $db_before = $node->safe_psql('postgres', $database); +cmp_ok($table_before, '>', 0, 'table statistics populated'); +cmp_ok($index_before, '>', 0, 'index statistics populated'); +cmp_ok($db_before, '>', 0, 'database statistics populated'); + +$node->safe_psql('postgres', q{ +CREATE ROLE reset_reader; +GRANT USAGE ON SCHEMA ext_vacuum_statistics TO reset_reader; +}); +for my $call ('extvac_reset_entry(0, 0)', 'extvac_reset_db_entry(0)', + 'vacuum_statistics_reset()', 'gp_extvac_reset_entry(0, 0)', + 'gp_extvac_reset_db_entry(0)', 'gp_vacuum_statistics_reset()') +{ + my ($stdout, $stderr); + my $ret = $node->psql('postgres', + "SET ROLE reset_reader; SELECT ext_vacuum_statistics.$call", + stdout => \$stdout, stderr => \$stderr); + isnt($ret, 0, "$call denied to a schema reader"); + like($stderr, qr/permission denied for function/, 'function ACL rejects reset'); +} +is($node->safe_psql('postgres', $table), $table_before, + 'denied resets leave statistics unchanged'); + +$node->safe_psql('postgres', qq{ +GRANT EXECUTE ON FUNCTION ext_vacuum_statistics.extvac_reset_entry(oid, oid) TO reset_reader; +SET ROLE reset_reader; +SELECT ext_vacuum_statistics.extvac_reset_entry($dboid, $relid); +}); +is($node->safe_psql('postgres', $table), '0', + 'delegated relation reset preserves a visible zero row'); +is($node->safe_psql('postgres', $index), $index_before, + 'table reset leaves index counters unchanged'); +is($node->safe_psql('postgres', $database), $db_before, + 'table reset leaves database aggregate unchanged'); +$node->safe_psql('postgres', + "SELECT ext_vacuum_statistics.extvac_reset_entry($dboid, $idxid)"); +is($node->safe_psql('postgres', $index), '0', + 'index reset preserves a visible zero row'); + +# SQL return types do not claim to count reset entries. Missing entries are +# harmless no-ops, consistent with built-in reset functions. +is($node->safe_psql('postgres', q{ +SELECT bool_and(p.prorettype = 'void'::regtype AND p.proparallel = 'u' AND p.provolatile = 'v') +FROM pg_proc p JOIN pg_namespace n ON n.oid = p.pronamespace +WHERE n.nspname = 'ext_vacuum_statistics' AND p.proname LIKE '%reset%' +}), 't', 'all resets return void and are volatile and parallel unsafe'); +$node->safe_psql('postgres', q{ +SELECT ext_vacuum_statistics.extvac_reset_entry(4294967294, 4294967294); +SELECT ext_vacuum_statistics.extvac_reset_db_entry(4294967294); +}); + +$node->safe_psql('postgres', 'VACUUM reset_t'); +$node->restart; +$node->safe_psql('postgres', + "SELECT ext_vacuum_statistics.extvac_reset_db_entry($dboid)"); +is($node->safe_psql('postgres', $table), '0', 'database reset retains table type after restart'); +is($node->safe_psql('postgres', $index), '0', 'database reset retains index type after restart'); +is($node->safe_psql('postgres', $database), '0', 'database reset retains aggregate type after restart'); +cmp_ok($node->safe_psql('other_reset', $table), '>', 0, + 'database reset leaves another database untouched'); + +$node->safe_psql('postgres', 'VACUUM reset_t'); +$node->safe_psql('postgres', 'SELECT pg_stat_reset()'); +is($node->safe_psql('postgres', $table), '0', 'core reset retains table row'); +is($node->safe_psql('postgres', $index), '0', 'core reset retains index row'); +is($node->safe_psql('postgres', $database), '0', 'core reset retains database row'); +$node->safe_psql('postgres', 'VACUUM reset_t'); +cmp_ok($node->safe_psql('postgres', $table), '>', 0, + 'vacuum resumes accumulation after reset'); +$node->safe_psql('postgres', 'SELECT ext_vacuum_statistics.vacuum_statistics_reset()'); +for my $db ('postgres', 'other_reset') +{ + is($node->safe_psql($db, $table), '0', "global reset clears table in $db"); + is($node->safe_psql($db, $index), '0', "global reset clears index in $db"); + is($node->safe_psql($db, $database), '0', "global reset clears aggregate in $db"); +} +$node->stop; +done_testing(); diff --git a/contrib/ext_vacuum_statistics/t/064_native_work_source.pl b/contrib/ext_vacuum_statistics/t/064_native_work_source.pl new file mode 100644 index 00000000000..3c5f9b013cb --- /dev/null +++ b/contrib/ext_vacuum_statistics/t/064_native_work_source.pl @@ -0,0 +1,126 @@ +# Copyright (c) 2026, PostgreSQL Global Development Group + +# Extension views reuse native work history; collection and resets of +# extension resources must never change native counters. +use strict; +use warnings FATAL => 'all'; +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +my $node = PostgreSQL::Test::Cluster->new('vacstat_native_source'); +$node->init; +$node->append_conf('postgresql.conf', q{ +shared_preload_libraries = 'ext_vacuum_statistics' +autovacuum = off +}); +$node->start; +$node->safe_psql('postgres', q{ +CREATE EXTENSION ext_vacuum_statistics; +CREATE TABLE native_heap (id int PRIMARY KEY); +INSERT INTO native_heap SELECT generate_series(1, 1000); +DELETE FROM native_heap WHERE id <= 100; +SET vacuum_statistics.enabled = off; +VACUUM (FREEZE) native_heap; +SELECT pg_stat_force_next_flush(); +}); + +is($node->safe_psql('postgres', q{ +SELECT count(*) FROM ext_vacuum_statistics.pg_stats_get_vacuum_tables( + (SELECT oid FROM pg_database WHERE datname = current_database()), + 'native_heap'::regclass) +}), '0', 'disabled first vacuum creates no extension resource entry'); +is($node->safe_psql('postgres', q{ +SELECT tuples_deleted = 100 AND pages_scanned > 0 + AND total_blks_read = 0 AND total_blks_hit = 0 AND wal_records = 0 + FROM ext_vacuum_statistics.pg_stats_vacuum_tables + WHERE relname = 'native_heap' +}), 't', 'view exposes native work even without a resource entry'); +is($node->safe_psql('postgres', q{ +SELECT tuples_deleted = 100 AND total_blks_read = 0 AND total_blks_hit = 0 + FROM ext_vacuum_statistics.pg_stats_vacuum_indexes + WHERE indexrelname = 'native_heap_pkey' +}), 't', 'index view exposes native work without a resource entry'); + +my @table_fields = qw(tuples_deleted pages_scanned pages_removed); +my @index_fields = qw(tuples_deleted pages_deleted); +for my $consistency ('none', 'cache', 'snapshot') +{ + my $tables_match = join(' AND ', map { "e.$_ = n.$_" } @table_fields); + my $indexes_match = join(' AND ', map { "e.$_ = n.$_" } @index_fields); + is($node->safe_psql('postgres', qq{ +SET stats_fetch_consistency = '$consistency'; +SELECT count(*) = 1 AND bool_and($tables_match) + FROM ext_vacuum_statistics.pg_stats_vacuum_tables e + CROSS JOIN LATERAL pg_stat_get_vacuum_stats(e.relid) n + WHERE e.relname = 'native_heap'; +}), 't', "$consistency: all heap work fields match native counters"); + is($node->safe_psql('postgres', qq{ +SET stats_fetch_consistency = '$consistency'; +SELECT $indexes_match + FROM ext_vacuum_statistics.pg_stats_vacuum_indexes e + CROSS JOIN LATERAL pg_stat_get_vacuum_stats(e.indexrelid) n + WHERE e.indexrelname = 'native_heap_pkey'; +}), 't', "$consistency: all index work fields match native counters"); +} + +my $work = q{ +SELECT row_to_json(n) FROM pg_stat_get_vacuum_stats('native_heap'::regclass) n +}; +my $resources = q{ +SELECT total_blks_read + total_blks_hit + FROM ext_vacuum_statistics.pg_stats_vacuum_tables WHERE relname = 'native_heap' +}; +my $work_before = $node->safe_psql('postgres', $work); +$node->safe_psql('postgres', q{ +SET track_counts = off; +VACUUM (DISABLE_PAGE_SKIPPING) native_heap; +}); +is($node->safe_psql('postgres', $work), $work_before, + 'track_counts disables native work collection independently'); +cmp_ok($node->safe_psql('postgres', $resources), '>', 0, + 'resource collection continues with track_counts off'); + +my $dboid = $node->safe_psql('postgres', + q[SELECT oid FROM pg_database WHERE datname = current_database()]); +for my $call ("extvac_reset_entry($dboid, 'native_heap'::regclass)", + "extvac_reset_db_entry($dboid)", 'vacuum_statistics_reset()') +{ + $node->safe_psql('postgres', q{ +VACUUM (DISABLE_PAGE_SKIPPING) native_heap; +SELECT pg_stat_force_next_flush(); +}); + my $native = $node->safe_psql('postgres', $work); + my $db_native_sql = qq{ +SELECT row_to_json(n) FROM pg_stat_get_vacuum_database_stats($dboid) n +}; + my $db_native = $node->safe_psql('postgres', $db_native_sql); + my $other_sql = q{ +SELECT n_tup_ins, n_tup_del, vacuum_count, last_vacuum + FROM pg_stat_all_tables WHERE relname = 'native_heap' +}; + my $other = $node->safe_psql('postgres', $other_sql); + cmp_ok($node->safe_psql('postgres', $resources), '>', 0, + "$call: resources collected before reset"); + $node->safe_psql('postgres', "SELECT ext_vacuum_statistics.$call"); + is($node->safe_psql('postgres', $resources), '0', "$call clears resources"); + is($node->safe_psql('postgres', $work), $native, "$call preserves native relation work"); + is($node->safe_psql('postgres', $db_native_sql), $db_native, + "$call preserves native database work"); + is($node->safe_psql('postgres', $other_sql), $other, + "$call preserves other native relation statistics"); +} + +$node->safe_psql('postgres', 'VACUUM (DISABLE_PAGE_SKIPPING) native_heap'); +my $resources_before = $node->safe_psql('postgres', $resources); +$node->safe_psql('postgres', + q[SELECT pg_stat_reset_vacuum_stats('native_heap'::regclass)]); +is($node->safe_psql('postgres', q{ +SELECT tuples_deleted = 0 AND pages_scanned = 0 + FROM ext_vacuum_statistics.pg_stats_vacuum_tables WHERE relname = 'native_heap' +}), 't', 'native reset is visible in the extension work columns'); +is($node->safe_psql('postgres', $resources), $resources_before, + 'native relation reset preserves extension resources'); + +$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..1731260aa32 --- /dev/null +++ b/contrib/ext_vacuum_statistics/vacuum_statistics.c @@ -0,0 +1,516 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to you under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * 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 "catalog/objectaccess.h" +#include "catalog/pg_class.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_internal.h" +#include "utils/pgstat_kind.h" +#include "utils/tuplestore.h" +#include "utils/timestamp.h" + +#ifdef PG_MODULE_MAGIC +PG_MODULE_MAGIC; +#endif + +/* Two kinds: relations (tables/indexes) and database aggregates */ +#define PGSTAT_KIND_EXTVAC_RELATION 24 +#define PGSTAT_KIND_EXTVAC_DB 25 + +#define SJ_NODENAME "vacuum_statistics" + +/* GUCs */ +static bool evs_enabled = true; + +/* Hooks */ +static set_report_vacuum_hook_type prev_report_vacuum_hook = NULL; +static object_access_hook_type prev_object_access_hook = NULL; + +/* Forward declarations */ +static void pgstat_report_vacuum_extstats(Oid tableoid, bool shared, + PgStat_VacuumRelationCounts * params); +static void extvac_object_access(ObjectAccessType access, Oid classId, + Oid objectId, int subId, void *arg); + +/* Shared memory entry for vacuum stats; one per relation or database. */ +typedef struct PgStatShared_ExtVacEntry +{ + PgStatShared_Common header; + PgStat_VacuumRelationCounts stats; +} PgStatShared_ExtVacEntry; + +/* A reset clears counters, not the table/index/database discriminator. */ +static void +extvac_reset_data(PgStatShared_Common *header) +{ + PgStatShared_ExtVacEntry *entry = (PgStatShared_ExtVacEntry *) header; + ExtVacReportType type = entry->stats.type; + + memset(&entry->stats, 0, sizeof(entry->stats)); + entry->stats.type = type; +} + +/* 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, + .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, + .reset_data_cb = extvac_reset_data, +}; + +/* PgStat kind for per-database aggregated vacuum statistics */ +static const PgStat_KindInfo extvac_db_kind_info = { + .name = "ext_vacuum_statistics_db", + .fixed_amount = false, + .accessed_across_databases = true, + .write_to_file = true, + .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, + .reset_data_cb = extvac_reset_data, +}; + +static inline void +pgstat_accumulate_common(PgStat_CommonCounts * dst, const PgStat_CommonCounts * src) +{ + dst->total_blks_read += src->total_blks_read; + dst->total_blks_hit += src->total_blks_hit; + dst->total_blks_dirtied += src->total_blks_dirtied; + dst->total_blks_written += src->total_blks_written; + dst->blks_fetched += src->blks_fetched; + dst->blks_hit += src->blks_hit; + dst->blk_read_time += src->blk_read_time; + dst->blk_write_time += src->blk_write_time; + dst->wal_records += src->wal_records; + dst->wal_fpi += src->wal_fpi; + dst->wal_bytes += src->wal_bytes; +} + +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); +} + +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", + "Collect VACUUM resources and extension-only metrics; work counters obey track_counts.", + NULL, &evs_enabled, true, + PGC_SUSET, GUC_GPDB_NEED_SYNC, + NULL, NULL, NULL); + + MarkGUCPrefixReserved(SJ_NODENAME); + + pgstat_register_kind(PGSTAT_KIND_EXTVAC_RELATION, &extvac_relation_kind_info); + pgstat_register_kind(PGSTAT_KIND_EXTVAC_DB, &extvac_db_kind_info); + + prev_report_vacuum_hook = set_report_vacuum_hook; + set_report_vacuum_hook = pgstat_report_vacuum_extstats; + + prev_object_access_hook = object_access_hook; + object_access_hook = extvac_object_access; +} + +/* + * Object access hook: drop the statistics of a dropped relation, and reset + * old statistics when a new relation is created. + * + * Otherwise the statistics of a dropped relation stay in memory and in the + * statistics file, and a new relation with the same OID gets them. Both + * actions are transactional, as for the built-in relation statistics. + * + * Statistics are keyed by the relation OID, and shared catalogs are never + * created or dropped after initdb, so we do not need to read pg_class. + * + * Dropped databases need no handling: dropping a database drops all its + * statistics entries, ours included. + */ +static void +extvac_object_access(ObjectAccessType access, Oid classId, Oid objectId, + int subId, void *arg) +{ + if (prev_object_access_hook) + prev_object_access_hook(access, classId, objectId, subId, arg); + + /* only whole relations are of interest, not their columns */ + if (classId != RelationRelationId || subId != 0) + return; + + if (access == OAT_DROP) + pgstat_drop_transactional(PGSTAT_KIND_EXTVAC_RELATION, MyDatabaseId, + objectId); + else if (access == OAT_POST_CREATE) + { + PgStat_EntryRef *entry_ref; + + /* An OID can now identify a different kind of relation. */ + entry_ref = pgstat_get_entry_ref(PGSTAT_KIND_EXTVAC_RELATION, + MyDatabaseId, objectId, false, NULL); + if (entry_ref && !entry_ref->shared_entry->dropped) + { + PgStatShared_ExtVacEntry *entry; + + (void) pgstat_lock_entry(entry_ref, false); + entry = (PgStatShared_ExtVacEntry *) entry_ref->shared_stats; + entry->stats.type = PGSTAT_EXTVAC_INVALID; + pgstat_unlock_entry(entry_ref); + } + + /* + * Discard whatever an earlier owner of this OID left behind, and + * arrange for the entry to be dropped again should the creating + * transaction roll back. This mirrors what pgstat_create_relation() + * does for the built-in relation statistics. + */ + pgstat_create_transactional(PGSTAT_KIND_EXTVAC_RELATION, MyDatabaseId, + objectId); + } +} + +/* Accumulate common counts for database-level stats. */ +static inline void +pgstat_accumulate_common_for_db(PgStat_CommonCounts * dst, + const PgStat_CommonCounts * src) +{ + pgstat_accumulate_common(dst, src); +} + +/* + * Store incoming vacuum stats into pgstat custom statistics. + * store_relation: create/update per-relation entry + * store_db: accumulate into database-level entry (dboid, objid=0). + * Uses pgstat_get_entry_ref_locked and pgstat_accumulate_* for atomic updates. + */ +static void +extvac_store(Oid dboid, Oid relid, PgStat_VacuumRelationCounts * params, + bool store_relation, bool store_db) +{ + PgStat_EntryRef *entry_ref; + PgStatShared_ExtVacEntry *shared; + + if (!evs_enabled) + return; + + if (store_relation) + { + entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_EXTVAC_RELATION, dboid, relid, false); + if (entry_ref) + { + shared = (PgStatShared_ExtVacEntry *) entry_ref->shared_stats; + if (shared->stats.type == PGSTAT_EXTVAC_INVALID) + { + memset(&shared->stats, 0, sizeof(shared->stats)); + shared->stats.type = params->type; + } + pgstat_accumulate_extvac_stats(&shared->stats, params); + pgstat_unlock_entry(entry_ref); + } + } + + if (store_db) + { + entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_EXTVAC_DB, dboid, InvalidOid, false); + if (entry_ref) + { + shared = (PgStatShared_ExtVacEntry *) entry_ref->shared_stats; + if (shared->stats.type == PGSTAT_EXTVAC_INVALID) + { + memset(&shared->stats, 0, sizeof(shared->stats)); + shared->stats.type = PGSTAT_EXTVAC_DB; + } + pgstat_accumulate_common_for_db(&shared->stats.common, ¶ms->common); + pgstat_unlock_entry(entry_ref); + } + } +} + +/* + * Vacuum report hook: called when vacuum finishes. Stores stats per-relation + * and per-database, then chains to previous hook. + */ +static void +pgstat_report_vacuum_extstats(Oid tableoid, bool shared, + PgStat_VacuumRelationCounts * params) +{ + Oid dboid = shared ? InvalidOid : MyDatabaseId; + + if (evs_enabled) + extvac_store(dboid, tableoid, params, true, true); + if (prev_report_vacuum_hook) + prev_report_vacuum_hook(tableoid, shared, params); +} + +/* Reset statistics for a single relation entry. */ +static void +extvac_reset_by_relid(Oid dboid, Oid relid) +{ + pgstat_reset_entry(PGSTAT_KIND_EXTVAC_RELATION, dboid, relid, + GetCurrentTimestamp()); +} + +/* Callback for pgstat_reset_matching_entries: match relation entries for given db */ +static bool +match_extvac_relations_for_db(PgStatShared_HashEntry *entry, Datum match_data) +{ + return entry->key.kind == PGSTAT_KIND_EXTVAC_RELATION && + entry->key.dboid == DatumGetObjectId(match_data); +} + +/* + * Reset statistics for a database (aggregate entry) and all its relations. + */ +static void +extvac_database_reset(Oid dboid) +{ + TimestampTz ts = GetCurrentTimestamp(); + + pgstat_reset_matching_entries(match_extvac_relations_for_db, + ObjectIdGetDatum(dboid), ts); + pgstat_reset_entry(PGSTAT_KIND_EXTVAC_DB, dboid, InvalidOid, ts); +} + +/* Reset all vacuum statistics (both relation and database entries). */ +static void +extvac_stat_reset(void) +{ + pgstat_reset_of_kind(PGSTAT_KIND_EXTVAC_RELATION); + pgstat_reset_of_kind(PGSTAT_KIND_EXTVAC_DB); +} + +PG_FUNCTION_INFO_V1(vacuum_statistics_reset); +PG_FUNCTION_INFO_V1(extvac_reset_entry); +PG_FUNCTION_INFO_V1(extvac_reset_db_entry); + +Datum +vacuum_statistics_reset(PG_FUNCTION_ARGS) +{ + extvac_stat_reset(); + PG_RETURN_VOID(); +} + +Datum +extvac_reset_entry(PG_FUNCTION_ARGS) +{ + Oid dboid = PG_GETARG_OID(0); + Oid relid = PG_GETARG_OID(1); + + extvac_reset_by_relid(dboid, relid); + PG_RETURN_VOID(); +} + +Datum +extvac_reset_db_entry(PG_FUNCTION_ARGS) +{ + Oid dboid = PG_GETARG_OID(0); + + extvac_database_reset(dboid); + PG_RETURN_VOID(); +} + +/* + * Output vacuum statistics (tables, indexes, or per-database aggregates). + */ +#define EXTVAC_COMMON_STAT_COLS 9 + +static void +tuplestore_put_common(PgStat_CommonCounts * vacuum_ext, + Datum *values, bool *nulls, int *i) +{ + char buf[256]; + const int base PG_USED_FOR_ASSERTS_ONLY = *i; + + values[(*i)++] = Int64GetDatum(vacuum_ext->total_blks_read); + values[(*i)++] = Int64GetDatum(vacuum_ext->total_blks_hit); + values[(*i)++] = Int64GetDatum(vacuum_ext->total_blks_dirtied); + values[(*i)++] = Int64GetDatum(vacuum_ext->total_blks_written); + values[(*i)++] = Int64GetDatum(vacuum_ext->wal_records); + values[(*i)++] = Int64GetDatum(vacuum_ext->wal_fpi); + snprintf(buf, sizeof buf, UINT64_FORMAT, vacuum_ext->wal_bytes); + values[(*i)++] = DirectFunctionCall3(numeric_in, + CStringGetDatum(buf), + ObjectIdGetDatum(0), + Int32GetDatum(-1)); + values[(*i)++] = Float8GetDatum(vacuum_ext->blk_read_time); + values[(*i)++] = Float8GetDatum(vacuum_ext->blk_write_time); + Assert((*i - base) == EXTVAC_COMMON_STAT_COLS); +} + +#define EXTVAC_HEAP_STAT_COLS 12 +#define EXTVAC_IDX_STAT_COLS 12 +#define EXTVAC_MAX_STAT_COLS Max(EXTVAC_HEAP_STAT_COLS, EXTVAC_IDX_STAT_COLS) + +static void +tuplestore_put_for_relation(Oid relid, Tuplestorestate *tupstore, + TupleDesc tupdesc, PgStat_VacuumRelationCounts * vacuum_ext) +{ + Datum values[EXTVAC_MAX_STAT_COLS]; + bool nulls[EXTVAC_MAX_STAT_COLS]; + int i = 0; + + memset(nulls, 0, sizeof(nulls)); + values[i++] = ObjectIdGetDatum(relid); + + tuplestore_put_common(&vacuum_ext->common, values, nulls, &i); + values[i++] = Int64GetDatum(vacuum_ext->common.blks_fetched - vacuum_ext->common.blks_hit); + values[i++] = Int64GetDatum(vacuum_ext->common.blks_hit); + + + 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, relid); + + if (!stats) + stats = (PgStat_VacuumRelationCounts *) + pgstat_fetch_entry(PGSTAT_KIND_EXTVAC_RELATION, InvalidOid, + relid); + + if (stats && stats->type == type) + tuplestore_put_for_relation(relid, tupstore, tupdesc, stats); + } + else if (type == PGSTAT_EXTVAC_DB) + { + if (OidIsValid(dbid)) + { +#define EXTVAC_DB_STAT_COLS 10 + Datum values[EXTVAC_DB_STAT_COLS]; + bool nulls[EXTVAC_DB_STAT_COLS]; + int i = 0; + PgStat_VacuumRelationCounts *stats; + + stats = (PgStat_VacuumRelationCounts *) + pgstat_fetch_entry(PGSTAT_KIND_EXTVAC_DB, dbid, + InvalidOid); + if (stats && stats->type == PGSTAT_EXTVAC_DB) + { + memset(nulls, 0, sizeof(nulls)); + values[i++] = ObjectIdGetDatum(dbid); + tuplestore_put_common(&stats->common, values, nulls, &i); + Assert(i == EXTVAC_DB_STAT_COLS); + tuplestore_putvalues(tupstore, tupdesc, values, nulls); + } + } + /* invalid dbid: return empty set */ + } + else + elog(PANIC, "ext_vacuum_statistics: invalid type %d", type); + + return (Datum) 0; +} + +PG_FUNCTION_INFO_V1(pg_stats_get_vacuum_tables); +PG_FUNCTION_INFO_V1(pg_stats_get_vacuum_indexes); +PG_FUNCTION_INFO_V1(pg_stats_get_vacuum_database); + +Datum +pg_stats_get_vacuum_tables(PG_FUNCTION_ARGS) +{ + return pg_stats_vacuum(fcinfo, PGSTAT_EXTVAC_TABLE); +} + +Datum +pg_stats_get_vacuum_indexes(PG_FUNCTION_ARGS) +{ + return pg_stats_vacuum(fcinfo, PGSTAT_EXTVAC_INDEX); +} + +Datum +pg_stats_get_vacuum_database(PG_FUNCTION_ARGS) +{ + return pg_stats_vacuum(fcinfo, PGSTAT_EXTVAC_DB); +} diff --git a/contrib/file_fdw/file_fdw.c b/contrib/file_fdw/file_fdw.c index b5ffde4131b..59fcc8ae729 100644 --- a/contrib/file_fdw/file_fdw.c +++ b/contrib/file_fdw/file_fdw.c @@ -1197,7 +1197,7 @@ file_acquire_sample_rows(Relation onerel, int elevel, for (;;) { /* Check for user-requested abort or sleep */ - vacuum_delay_point(); + vacuum_delay_point(true); /* Fetch next row */ MemoryContextReset(tupcontext); diff --git a/contrib/meson.build b/contrib/meson.build index bd4a57c43c0..67b38432c55 100644 --- a/contrib/meson.build +++ b/contrib/meson.build @@ -22,6 +22,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/catalogs.sgml b/doc/src/sgml/catalogs.sgml index 9b498953f45..45102c5d654 100644 --- a/doc/src/sgml/catalogs.sgml +++ b/doc/src/sgml/catalogs.sgml @@ -2061,6 +2061,26 @@ SCRAM-SHA-256$<iteration count>:&l + + + relallfrozen int4 + + + Number of pages that are marked all-frozen in the table's visibility + map. This is only an estimate and can be used along with + relallvisible for scheduling vacuums and + tuning vacuum's freezing + behavior. + + It is updated by + VACUUM, + ANALYZE, + and a few DDL commands such as + CREATE INDEX. + + + + reltoastrelid oid diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml index 3b8b60d9347..1873cf1f972 100644 --- a/doc/src/sgml/config.sgml +++ b/doc/src/sgml/config.sgml @@ -8155,6 +8155,30 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv; + + track_cost_delay_timing (boolean) + + track_cost_delay_timing configuration parameter + + + + + Enables timing of cost-based vacuum delay (see + ). This parameter + is off by default, as it will repeatedly query the operating system for + the current time, which may cause significant overhead on some + platforms. You can use the tool to + measure the overhead of timing on your system. Cost-based vacuum delay + timing information is displayed in + pg_stat_progress_vacuum + and + pg_stat_progress_analyze. + Only superusers and users with the appropriate SET + privilege can change this setting. + + + + track_io_timing (boolean) diff --git a/doc/src/sgml/contrib.sgml b/doc/src/sgml/contrib.sgml index 922421a8971..2471595334b 100644 --- a/doc/src/sgml/contrib.sgml +++ b/doc/src/sgml/contrib.sgml @@ -142,6 +142,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..c8ca317414e --- /dev/null +++ b/doc/src/sgml/extvacuumstatistics.sgml @@ -0,0 +1,427 @@ + + + + ext_vacuum_statistics — extended vacuum statistics + + + ext_vacuum_statistics + + + + The ext_vacuum_statistics module provides + extended per-table, per-index, and per-database vacuum statistics + (buffer I/O, WAL, general, timing) 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. + + + + Note that starting the server without the module in + , even once, discards + all cumulative statistics, not only those of this + module. The statistics file then holds entries of a statistics kind that + is not registered, so the server reports the file as corrupted and resets + every statistics kind, built-in ones such as those shown in + pg_stat_user_tables included. To stop collecting + extension resource statistics without that side effect, keep the module loaded and + set vacuum_statistics.enabled to off + instead. + + + + When active, the module provides views + ext_vacuum_statistics.pg_stats_vacuum_tables, + ext_vacuum_statistics.pg_stats_vacuum_indexes, and + ext_vacuum_statistics.pg_stats_vacuum_database, + plus functions to reset statistics and manage tracking. + + + + Each tracked object (one table or one index) uses a fixed-size shared + memory entry; the exact size depends on the platform. + + + + The statistics of a table or an index are dropped together with it, when + the transaction that drops it commits, and a new table or index starts + from empty statistics even if it gets the OID of an old one; both work + the way they do for the built-in relation statistics. The module does + that with an object_access_hook, so the statistics file + does not accumulate the entries of relations long gone. The statistics + of a dropped database are dropped along with the rest of its cumulative + statistics. + + + + Local Counters + + The table and index views identify entries by relid + or indexrelid, together with the schema, relation + name and database name. They include materialized views and TOAST relations. + Counters accumulate over completed vacuum work on the current instance. + Heap VACUUM FULL does not update these counters. + + + + total_blks_read, total_blks_hit, + total_blks_dirtied, total_blks_written + bigint buffer-manager activity during vacuum. + Table-only totals exclude index work, which is reported separately. + + + rel_blks_read, rel_blks_hit + bigint reads and buffer hits attributed to the + relation itself, excluding accesses to other relations. + + + wal_records, wal_fpi, + wal_bytes + WAL records and full-page images (bigint) and + WAL bytes (numeric) generated during vacuum. + + + blk_read_time, blk_write_time + double precision block I/O time in milliseconds, + available when is enabled. + + + tuples_deleted, pages_scanned, + pages_removed, pages_deleted + bigint tuples removed from a table or index, + table pages scanned and removed, and newly deleted index pages. + Repeated index passes count only their additional deletion work. + + + + The database view identifies entries by dboid + and dbname. Its resource totals sum table-only + and index reports. Columns use the db_ prefix: + db_blks_read, db_blks_hit, + db_blks_dirtied, db_blks_written, + db_wal_records, db_wal_fpi, + db_wal_bytes, db_blk_read_time and + db_blk_write_time. + + + The views read the corresponding + ext_vacuum_statistics.pg_stats_get_vacuum_tables(dboid, reloid), + ext_vacuum_statistics.pg_stats_get_vacuum_indexes(dboid, reloid) + and ext_vacuum_statistics.pg_stats_get_vacuum_database(dboid) + functions for resource metrics. Missing resource entries produce zero + resource counters in the table and index views. Work counters are read + from pg_stat_get_vacuum_stats() and remain visible + when resource collection is disabled. Extension-specific resets + do not clear native VACUUM work counters. pg_stat_reset() + also clears the extension's counters for the current database. + + + + + Functions + + + These functions reset only the extension's own metrics. Native VACUUM + work counters and other core statistics are never reset by the module. + Use pg_stat_reset_vacuum_stats(relid) for one relation + or pg_stat_reset_vacuum_stats() for the current database + and its non-shared relations. These preserve timing, invocation counts and + other native statistics. On Cloudberry, use the corresponding + gp_stat_reset_vacuum_stats() wrapper from the coordinator + to include every primary segment. A native relation reset leaves extension resources intact; + pg_stat_reset() clears both for the current database. + The counters can cover different periods after a reset or a change in + collection settings. + + + + Reset functions require superuser privileges by default. Access can be + delegated with GRANT EXECUTE. Existing entries remain + visible with zero extension counters after a reset; native work is unchanged; their table, index, or database + type is preserved. Missing entries are not created by a reset. + pg_stat_reset() also resets the module's counters for + the current database. + + + + For Cloudberry, use the gp_ wrappers from a normal + coordinator connection to reset statistics across the cluster. These + wrappers reset the coordinator and all primary segments. For example, + to reset vacuum statistics for all databases across the cluster: + +SELECT ext_vacuum_statistics.gp_vacuum_statistics_reset(); + + Delegating access to a cluster wrapper also requires granting access to + its corresponding local reset function. + + + + + Functions without the gp_ prefix reset statistics + only on the current node. Calling + ext_vacuum_statistics.vacuum_statistics_reset() + on the coordinator (QD) resets only the coordinator's counters; + segment counters remain unchanged. The same applies to + ext_vacuum_statistics.extvac_reset_entry() and + ext_vacuum_statistics.extvac_reset_db_entry(). + Use the local functions in a utility connection to an individual segment. + + + + + + + ext_vacuum_statistics.vacuum_statistics_reset() + void + + + + Resets all vacuum statistics of the module only on the current node, + including relation and database statistics for all databases. + + + + + + + ext_vacuum_statistics.extvac_reset_entry(dboid oid, relid oid) + void + + + + Resets the module's own vacuum metrics for one table or index only on the current + node. Use + 0 as dboid for a shared + catalog. The database aggregate and other relations are unchanged. + + + + + + + ext_vacuum_statistics.extvac_reset_db_entry(dboid oid) + void + + + + Resets the module's own vacuum metrics for a database only on the current node: + its aggregate and the statistics of all its tables and indexes. + + + + + + + ext_vacuum_statistics.gp_vacuum_statistics_reset() + void + + + + Calls vacuum_statistics_reset() on the coordinator + and all primary segments, resetting statistics for all databases. + + + + + + + ext_vacuum_statistics.gp_extvac_reset_entry(dboid oid, relid oid) + void + + + + Calls extvac_reset_entry(dboid, relid) + on the coordinator and all primary segments, resetting statistics for + one table or index. The database aggregate and other relations are + unchanged. + + + + + + + ext_vacuum_statistics.gp_extvac_reset_db_entry(dboid oid) + void + + + + Calls extvac_reset_db_entry(dboid) + on the coordinator and all primary segments, resetting the database + aggregate and the statistics of all its tables and indexes. + + + + + + + To reset the current database across the cluster, run on the coordinator: + +SELECT ext_vacuum_statistics.gp_extvac_reset_db_entry(oid) +FROM pg_database WHERE datname = current_database(); + + + + + + Using the Module in a Cloudberry Cluster + + + Every instance of the cluster, the coordinator and each segment, keeps + the statistics of the vacuums it runs itself: a VACUUM + issued on the coordinator is dispatched to the segments, and each segment + vacuums and counts its own part of the tables. The + pg_stats_vacuum_* views show the statistics of + the instance they are queried on, so on the coordinator they show only + what the coordinator itself vacuumed, which for distributed tables is next + to nothing. The module therefore also provides cluster-wide views, which + query every instance: + + + + + + ext_vacuum_statistics.gp_stats_vacuum_tables, + ext_vacuum_statistics.gp_stats_vacuum_indexes and + ext_vacuum_statistics.gp_stats_vacuum_database + have the columns of the corresponding pg_stats_vacuum_* + view, preceded by gp_segment_id: one row per + instance, -1 being the coordinator, like the + gp_stat_* views of the core. + + + + + ext_vacuum_statistics.gp_stats_vacuum_tables_summary + and ext_vacuum_statistics.gp_stats_vacuum_indexes_summary + have one row per relation, like the + gp_stat_*_summary views of the core: the + counters of user relations are summed over the segments, divided by the + number of segments for replicated tables, which every segment stores and + vacuums in full; system catalogs are shown as the coordinator counts + them. + + + + + ext_vacuum_statistics.gp_stats_vacuum_database_summary + sums the database aggregates over all instances, the coordinator + included. + + + + + + Like the other gp_* views, the cluster-wide views + must not be queried in utility mode, where they read the local instance + twice. + + + + The reset functions without the gp_ prefix act only on + the connected instance. + ext_vacuum_statistics.gp_vacuum_statistics_reset(), + ext_vacuum_statistics.gp_extvac_reset_entry(dboid, relid) + and + ext_vacuum_statistics.gp_extvac_reset_db_entry(dboid) + call them on the coordinator and on every primary segment. Call these + wrappers from a normal coordinator connection; use the local functions + in utility mode. See . + + + + Install the matching extension library, control and SQL files in the + Cloudberry installation on every host, including mirror and standby + coordinator hosts. gpconfig updates configuration files; + it does not distribute the extension files. The module must be preloaded + on every instance so that it is available after promotion. + + + + Inspect the active setting and the configuration files: + +gpconfig -s shared_preload_libraries +gpconfig -s shared_preload_libraries --file + + If the existing list is empty, set it as follows: + +gpconfig -c shared_preload_libraries -v 'ext_vacuum_statistics' + + Otherwise pass the complete existing list with + ext_vacuum_statistics appended. Preserve other preloaded + libraries and any intentional differences between coordinator, primary + and mirror settings. With and no role restriction, + gpconfig updates the coordinator, standby, primaries and + mirrors. Resolve any reported unreachable hosts before relying on their + configuration. Check the resulting files and restart the cluster: + +gpconfig -s shared_preload_libraries --file +gpstop -ar +gpconfig -s shared_preload_libraries --file-compare + + A reload with gpstop -u alone cannot load a new preload + library. Check that the mirror and standby instances also restarted + successfully. Then run + CREATE EXTENSION IF NOT EXISTS ext_vacuum_statistics + on the coordinator in each database where the views are needed; this + command is dispatched to the segments. + + + + The warning above about starting without the module applies separately + to each instance that has saved statistics of its custom kinds. Keep the + module preloaded when disabling collection to preserve that instance's + cumulative statistics. + + + + The counters are local pgstat state, not replicated + through WAL. Preloading the module on a mirror enables collection after + promotion but does not copy its primary's accumulated counters. A promoted + instance reports its own local statistics and cannot continue the former + primary's counter history. This also applies to standby coordinator + promotion. A compatible clean restart can preserve an instance's own + statistics; crash recovery resets them. + + + + Start a new measurement interval for an instance after promotion, even + when its gp_segment_id is unchanged. Other + instances keep their counters, but cluster summaries can decrease when + a primary is replaced. Monitoring should retain earlier samples externally + and establish a new baseline after failover; the discontinuity does not + represent negative vacuum work. + + + + vacuum_statistics.enabled is read by the instance that + runs the vacuum. A SET on the coordinator is passed on + to the segments of the session; to stop collecting extension resources in the whole cluster, + set it with gpconfig and reload the configuration + (gpstop -u). + + + + + Configuration Parameters + + + + vacuum_statistics.enabled (boolean) + + + Enables collection of extension resource metrics. Native work counters + obey track_counts independently. Default: on. + + + + + + diff --git a/doc/src/sgml/filelist.sgml b/doc/src/sgml/filelist.sgml index 4873c85b5f8..e8731e9f29a 100644 --- a/doc/src/sgml/filelist.sgml +++ b/doc/src/sgml/filelist.sgml @@ -129,6 +129,7 @@ + diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml index f9ceead292f..c57b6ece3c7 100644 --- a/doc/src/sgml/monitoring.sgml +++ b/doc/src/sgml/monitoring.sgml @@ -459,6 +459,19 @@ postgres 27093 0.0 0.0 30096 2752 ? Ss 11:34 0:00 postgres: ser + + pg_stat_vacuum + One row per database, plus one for shared objects, showing cumulative + vacuum statistics. See . + + + + pg_stat_vacuum_tables, + pg_stat_vacuum_indexes + One row per table or index, showing vacuum work counters. + See . + + pg_stat_database_conflictspg_stat_database_conflicts @@ -4697,6 +4710,254 @@ SELECT pid, wait_event_type, wait_event FROM pg_stat_activity WHERE wait_event i + + <structname>pg_stat_vacuum</structname> + pg_stat_vacuum + + The pg_stat_vacuum view contains one row for each + database, plus one for shared objects, showing cumulative vacuum statistics + on the current instance. The current database's counters are reset + together with its other statistics by pg_stat_reset(). + + + In Cloudberry, gp_stat_vacuum shows these columns + for every instance, with gp_segment_id identifying + the instance. gp_stat_vacuum_summary sums the times + across instances and takes the maximum failsafe and interrupt counts and + the latest reset time for each database. + + + <structname>pg_stat_vacuum</structname> View + + + + + Column Type + + + Description + + + + + + + datid oid + + + OID of this database, or 0 for objects belonging to a shared + relation + + + + + + datname name + + + Name of this database, or NULL for shared + objects. + + + + + + total_vacuum_time double precision + + + Total time manually invoked vacuums spent processing tables of this + database, in milliseconds (index processing is included in the owning + table's run) + + + + + + total_autovacuum_time double precision + + + Total time the autovacuum daemon spent processing tables of this + database, in milliseconds + + + + + + total_vacuum_delay_time double precision + + + Total time manually invoked vacuums of this database spent sleeping in + cost-based delay points, in milliseconds (counted only when + is enabled) + + + + + + total_autovacuum_delay_time double precision + + + Total time autovacuums of this database spent sleeping in cost-based + delay points, in milliseconds (counted only when + is enabled) + + + + + + vacuum_failsafe_count bigint + + + Number of vacuums in this database that entered the wraparound + failsafe mode (see ) + + + + + + vacuum_interrupt_count bigint + + + Number of times a vacuum in this database was interrupted by an error, + the vacuum of an append-optimized table included. In a Cloudberry + cluster the count is kept by the instance on which the vacuum failed: + a vacuum dispatched to the segments that fails there is counted by + those segments, not by the coordinator. A segment reports the count + once its part of the failed distributed transaction has ended, which + may be as late as the next command of the session. + + + + + + stats_reset timestamp with time zone + + + Time at which these statistics were last reset + + + + +
+
+ + + VACUUM Work Statistics + pg_stat_vacuum_tables + pg_stat_vacuum_indexes + + pg_stat_vacuum_tables and + pg_stat_vacuum_indexes report work performed by + VACUUM, including autovacuum. Collection uses + the built-in cumulative statistics system and + ; no extension or preload library is + required. Heap VACUUM FULL rewrites the relation and does + not update these work counters; AO VACUUM FULL uses the + AO compaction phases and reports their work. + Each view identifies the relation by relid or + indexrelid, its schema, + relname or indexrelname, + and dbname. Table statistics include materialized + views and TOAST relations. Relations without statistics have zero counters. + + + VACUUM Work Counters + ColumnsMeaning + + tuples_deleted + Cumulative tuples removed from a table, or entries removed from an index. + Index reports add only the work of each bulkdelete or cleanup call. + + dead_tuples + Table dead tuples observed after the latest completed vacuum, including + missed removable tuples for heap and hidden rows for AO. This is a + snapshot; skipped pages may limit the heap observation. + + recently_dead_tuples, + missed_dead_tuples + Cumulative observations of heap tuples not yet removable, and removable + tuples missed because a cleanup lock could not be obtained. For AO, + recently dead tuples accumulate hidden rows left after each vacuum; + missed tuples remain zero. Repeated observations count again. + + dead_pages, + missed_dead_pages + Cumulative observations of heap pages with not-yet-removable tuples, + and pages with missed removable tuples. For indexes, dead pages are + deleted pages not yet reusable, observed once after final cleanup. + AO tables have no heap dead-page counters. + + pages_scanned, + pages_removed, + pages_deleted + Cumulative table pages scanned and removed by vacuum; AO uses + heap-equivalent pages, rounded up per scanned segment or per completed + vacuum's released space. The index-only pages_deleted counter counts + newly deleted pages, excluding already empty or deleted pages. + + tuples_frozen, + pages_frozen, + pages_all_visible + Cumulative heap tuples frozen, pages on which tuples were frozen, and + pages whose all-visible VM bit was newly set. Rescanning unchanged + pages adds no work. These counters are zero for AO. + + freeze_age_vacuum_count + Heap vacuums made aggressive by the XID or MultiXact freeze table age, + including VACUUM FREEZE. Forcing an aggressive scan + with DISABLE_PAGE_SKIPPING alone does not count. + This is separate from the wraparound-failsafe counter. + + total_file_segs, + compacted_segments, + tuples_moved + AO segment metadata entries at the last vacuum, cumulative segments + actually compacted, and cumulative live tuples moved. Skipped candidates + add no compaction work. Heap tables report zero for these columns. + + + +
+ + All work columns have type bigint. The relation and database + getters are pg_stat_get_vacuum_stats(oid) and + pg_stat_get_vacuum_database_stats(oid). + pg_stat_vacuum additionally exposes cumulative table + work per database. It excludes index reports to avoid counting table work + twice, and excludes the dead_tuples and + total_file_segs snapshots. Shared relations use + the database entry with OID zero. + + + The gp_stat_vacuum_tables and + gp_stat_vacuum_indexes views expose individual + instances. Their _summary views sum distributed table + work and normalize replicated table work by the segment count. System + relations use coordinator statistics. gp_stat_vacuum_summary + sums database work across instances. + + + Core counters survive clean shutdown and restart, use the ordinary + statistics snapshot rules, and are removed when their relation or database + is dropped. pg_stat_reset_single_table_counters(oid) + resets a relation's counters; pg_stat_reset() resets + the current database statistics. The table and index views of + ext_vacuum_statistics read these same work counters. + A native relation reset leaves extension resource counters intact; + pg_stat_reset() resets both for the current database. + Extension reset functions clear only extension metrics and never modify + native work or other core statistics. + + + pg_stat_reset_vacuum_stats() resets just native work + counters, preserving other cumulative statistics. Supply a relation OID + to reset one table or index, or omit it to reset the current database and + its non-shared relations. On Cloudberry, the corresponding + gp_stat_reset_vacuum_stats() wrapper applies the same + reset on the coordinator and every primary segment. + +
+ <structname>pg_stat_database_conflicts</structname> @@ -4926,6 +5187,62 @@ SELECT pid, wait_event_type, wait_event FROM pg_stat_activity WHERE wait_event i
+ + + total_vacuum_time double precision + + + Total time this index has been processed by manually invoked vacuums, + in milliseconds. (This includes the time spent sleeping due to + cost-based delays.) + + + + + + total_autovacuum_time double precision + + + Total time this index has been processed by the autovacuum daemon, + in milliseconds. (This includes the time spent sleeping due to + cost-based delays.) + + + + + + total_vacuum_delay_time double precision + + + Total time manually invoked vacuums spent sleeping in cost-based delay + points while processing this index, in milliseconds (counted only when + is enabled) + + + + + + total_autovacuum_delay_time double precision + + + Total time the autovacuum daemon spent sleeping in cost-based delay + points while processing this index, in milliseconds (counted only when + is enabled) + + + + + + vacuum_failsafe_count bigint + + + Number of vacuums of this table that entered the wraparound failsafe + mode (see ). Always zero for + append-optimized tables, which have no tuple transaction IDs to + freeze. + + + n_tup_ins bigint @@ -5017,6 +5334,42 @@ SELECT pid, wait_event_type, wait_event FROM pg_stat_activity WHERE wait_event i + + + + + visible_page_marks_cleared bigint + + + Number of times the all-visible mark in the + visibility map 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 + VACUUM 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. Always zero for append-optimized + tables, which have no visibility map. + + + + + + frozen_page_marks_cleared bigint + + + Number of times the all-frozen mark in the + visibility map 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 VACUUM + or autovacuum). The page must then be processed again by vacuum on + the next freeze run for this table. Always zero for append-optimized + tables. + + + last_vacuum timestamp with time zone @@ -5094,6 +5447,66 @@ SELECT pid, wait_event_type, wait_event FROM pg_stat_activity WHERE wait_event i daemon + + + + total_vacuum_time double precision + + + Total time this table has been manually vacuumed, in milliseconds + + + + + + total_autovacuum_time double precision + + + Total time this table has been vacuumed by the autovacuum daemon, + in milliseconds + + + + + + total_analyze_time double precision + + + Total time this table has been manually analyzed, in milliseconds + + + + + + total_autoanalyze_time double precision + + + Total time this table has been analyzed by the autovacuum daemon, + in milliseconds + + + + + + total_vacuum_delay_time double precision + + + Total time this table's vacuums spent sleeping in cost-based delay + points, in milliseconds (counted only when + is enabled) + + + + + + total_autovacuum_delay_time double precision + + + Total time autovacuums of this table spent sleeping in cost-based + delay points, in milliseconds (counted only when + is enabled) + + @@ -5947,6 +6360,36 @@ SELECT pid, wait_event_type, wait_event FROM pg_stat_activity WHERE wait_event i + + + pg_stat_reset_vacuum_stats + pg_stat_reset_vacuum_stats ( + relid oid DEFAULT NULL ) + void + + + Resets only native VACUUM work counters for one table or index, including + a shared catalog. With no argument, or NULL, resets + those counters for the current database and all its non-shared relations. + A relation reset leaves its database aggregate and indexes unchanged. + Missing relation entries are not created. OID zero is a no-op. + + + Timing, last-vacuum timestamps, vacuum and analyze invocation counts, + other native counters, and extension resource statistics are preserved. + Cached snapshots follow the ordinary statistics reset rules; concurrently + running backends can subsequently publish pending work. + + + The function acts only on the connected instance. From a Cloudberry + coordinator, use gp_stat_reset_vacuum_stats with + the same argument to reset the coordinator and every primary segment. + Both functions require superuser privileges by default; access can be + delegated with GRANT EXECUTE. A delegated cluster + wrapper also requires access to its local function. + + + @@ -6496,6 +6939,18 @@ FROM pg_stat_get_backend_idset() AS backendid; acquiring inherited sample rows. + + + + delay_time double precision + + + Total time spent sleeping due to cost-based delay (see + , in milliseconds + (if is enabled, otherwise + zero). + + @@ -7377,6 +7832,18 @@ FROM pg_stat_get_backend_idset() AS backendid; Number of dead tuples collected since the last index vacuum cycle. + + + + delay_time double precision + + + Total time spent sleeping due to cost-based delay (see + ), in milliseconds + (if is enabled, otherwise + zero). + + diff --git a/doc/src/sgml/ref/vacuum.sgml b/doc/src/sgml/ref/vacuum.sgml index 2b85c3d385d..93d31af2543 100644 --- a/doc/src/sgml/ref/vacuum.sgml +++ b/doc/src/sgml/ref/vacuum.sgml @@ -140,6 +140,13 @@ VACUUM [ FULL ] [ FREEZE ] [ VERBOSE ] [ ANALYZE ] [ is enabled. Heap table resources + exclude the separately reported index passes. For append-optimized + tables, resources are reported separately for pre-cleanup, compaction + and post-cleanup, also excluding index work. These reports do not + require a statistics extension to be loaded. diff --git a/doc/src/sgml/xfunc.sgml b/doc/src/sgml/xfunc.sgml index 9620ea9ae38..f2c97afa1a2 100644 --- a/doc/src/sgml/xfunc.sgml +++ b/doc/src/sgml/xfunc.sgml @@ -3453,6 +3453,63 @@ if (!ptr) + + Custom Cumulative Statistics + + + Is is possible for add-ins written in C-language to use custom types + of cumulative statistics registered in the + Cumulative Statistics System. + + + + First, define a PgStat_KindInfo that includes all + the information related to the custom type registered. For example: + +static const PgStat_KindInfo custom_stats = { + .name = "custom_stats", + .fixed_amount = false, + .shared_size = sizeof(PgStatShared_Custom), + .shared_data_off = offsetof(PgStatShared_Custom, stats), + .shared_data_len = sizeof(((PgStatShared_Custom *) 0)->stats), + .pending_size = sizeof(PgStat_StatCustomEntry), +} + + + Then, each backend that needs to use this custom type needs to register + it with pgstat_register_kind and a unique ID used to + store the entries related to this type of statistics: + +extern PgStat_Kind pgstat_add_kind(PgStat_Kind kind, + const PgStat_KindInfo *kind_info); + + While developing a new extension, use + PGSTAT_KIND_EXPERIMENTAL for + kind. When you are ready to release the extension + to users, reserve a kind ID at the + + Custom Cumulative Statistics page. + + + + The details of the API for PgStat_KindInfo can + be found in src/include/utils/pgstat_internal.h. + + + + The type of statistics registered is associated with a name and a unique + ID shared across the server in shared memory. Each backend using a + custom type of statistics maintains a local cache storing the information + of each custom PgStat_KindInfo. + + + + Place the extension module implementing the custom cumulative statistics + type in so that it will + be loaded early during PostgreSQL startup. + + + Using C++ for Extensibility diff --git a/src/backend/access/aocs/aocs_compaction.c b/src/backend/access/aocs/aocs_compaction.c index 12df08f3d67..8c3ce22d11f 100644 --- a/src/backend/access/aocs/aocs_compaction.c +++ b/src/backend/access/aocs/aocs_compaction.c @@ -323,7 +323,7 @@ AOCSSegmentFileFullCompaction(Relation aorel, tupleCount++; if (VacuumCostActive && tupleCount % tuplePerPage == 0) { - vacuum_delay_point(); + vacuum_delay_point(false); } /* @@ -350,6 +350,12 @@ AOCSSegmentFileFullCompaction(Relation aorel, MarkAOCSFileSegInfoAwaitingDrop(aorel, compact_segno); + /* Count completed compaction, including a scan with no surviving tuples. */ + vacrelstats->compacted_segments++; + vacrelstats->tuples_moved += tupleCount; + vacrelstats->pages_scanned += + scanDesc->totalBytesRead / BLCKSZ + (scanDesc->totalBytesRead % BLCKSZ != 0); + AppendOnlyVisimap_DeleteSegmentFile(&visiMap, compact_segno); diff --git a/src/backend/access/aocs/aocsam_handler.c b/src/backend/access/aocs/aocsam_handler.c index 4dc7d541c5f..0256c1f4e00 100644 --- a/src/backend/access/aocs/aocsam_handler.c +++ b/src/backend/access/aocs/aocsam_handler.c @@ -1661,7 +1661,7 @@ aoco_acquire_sample_rows(Relation onerel, int elevel, HeapTuple *rows, { aocoscan->targrow = RowSampler_Next(&rs); - vacuum_delay_point(); + vacuum_delay_point(true); if (aocs_get_target_tuple(aocoscan, aocoscan->targrow, slot)) { diff --git a/src/backend/access/appendonly/aomd.c b/src/backend/access/appendonly/aomd.c index 0793c1a6fa3..1a80f74789f 100644 --- a/src/backend/access/appendonly/aomd.c +++ b/src/backend/access/appendonly/aomd.c @@ -216,7 +216,8 @@ TruncateAOSegmentFile(File fd, Relation rel, int32 segFileNum, int64 offset, AOV /* report heap-equivalent blocks vacuumed */ vacrelstats->nbytes_truncated += filesize_before - offset; pgstat_progress_update_param(PROGRESS_VACUUM_HEAP_BLKS_VACUUMED, - RelationGuessNumberOfBlocksFromSize(vacrelstats->nbytes_truncated)); + vacrelstats->nbytes_truncated / BLCKSZ + + (vacrelstats->nbytes_truncated % BLCKSZ != 0)); } if (XLogIsNeeded() && RelationNeedsWAL(rel)) diff --git a/src/backend/access/appendonly/appendonly_compaction.c b/src/backend/access/appendonly/appendonly_compaction.c index b8cae6ebf6c..2c28e211cab 100644 --- a/src/backend/access/appendonly/appendonly_compaction.c +++ b/src/backend/access/appendonly/appendonly_compaction.c @@ -506,7 +506,7 @@ AppendOnlySegmentFileFullCompaction(Relation aorel, tupleCount++; if (VacuumCostActive && tupleCount % tuplePerPage == 0) { - vacuum_delay_point(); + vacuum_delay_point(false); } } @@ -517,6 +517,12 @@ AppendOnlySegmentFileFullCompaction(Relation aorel, MarkFileSegInfoAwaitingDrop(aorel, compact_segno); + /* Count completed compaction, not merely a candidate considered by VACUUM. */ + vacrelstats->compacted_segments++; + vacrelstats->tuples_moved += movedTupleCount; + vacrelstats->pages_scanned += + fsinfo->eof / BLCKSZ + (fsinfo->eof % BLCKSZ != 0); + AppendOnlyVisimap_DeleteSegmentFile(&visiMap, compact_segno); /* Delete all mini pages of the segment files if block directory exists */ diff --git a/src/backend/access/appendonly/appendonlyam_handler.c b/src/backend/access/appendonly/appendonlyam_handler.c index cae12e110ff..7a7942c5e24 100644 --- a/src/backend/access/appendonly/appendonlyam_handler.c +++ b/src/backend/access/appendonly/appendonlyam_handler.c @@ -1544,7 +1544,7 @@ appendonly_acquire_sample_rows(Relation onerel, int elevel, HeapTuple *rows, { aoscan->targrow = RowSampler_Next(&rs); - vacuum_delay_point(); + vacuum_delay_point(true); if (appendonly_get_target_tuple(aoscan, aoscan->targrow, slot)) { diff --git a/src/backend/access/gin/ginfast.c b/src/backend/access/gin/ginfast.c index 2c23afe3a03..65b7f7ef3b7 100644 --- a/src/backend/access/gin/ginfast.c +++ b/src/backend/access/gin/ginfast.c @@ -891,7 +891,7 @@ ginInsertCleanup(GinState *ginstate, bool full_clean, */ processPendingPage(&accum, &datums, page, FirstOffsetNumber); - vacuum_delay_point(); + vacuum_delay_point(false); /* * Is it time to flush memory to disk? Flush if we are at the end of @@ -928,7 +928,7 @@ ginInsertCleanup(GinState *ginstate, bool full_clean, { ginEntryInsert(ginstate, attnum, key, category, list, nlist, NULL); - vacuum_delay_point(); + vacuum_delay_point(false); } /* @@ -1001,7 +1001,7 @@ ginInsertCleanup(GinState *ginstate, bool full_clean, /* * Read next page in pending list */ - vacuum_delay_point(); + vacuum_delay_point(false); buffer = ReadBuffer(index, blkno); LockBuffer(buffer, GIN_SHARE); page = BufferGetPage(buffer); diff --git a/src/backend/access/gin/ginvacuum.c b/src/backend/access/gin/ginvacuum.c index e5d310d8362..067224ab475 100644 --- a/src/backend/access/gin/ginvacuum.c +++ b/src/backend/access/gin/ginvacuum.c @@ -663,12 +663,12 @@ ginbulkdelete(IndexVacuumInfo *info, IndexBulkDeleteResult *stats, UnlockReleaseBuffer(buffer); } - vacuum_delay_point(); + vacuum_delay_point(false); for (i = 0; i < nRoot; i++) { ginVacuumPostingTree(&gvs, rootOfPostingTree[i]); - vacuum_delay_point(); + vacuum_delay_point(false); } if (blkno == InvalidBlockNumber) /* rightmost page */ @@ -749,7 +749,7 @@ ginvacuumcleanup(IndexVacuumInfo *info, IndexBulkDeleteResult *stats) Buffer buffer; Page page; - vacuum_delay_point(); + vacuum_delay_point(false); buffer = ReadBufferExtended(index, MAIN_FORKNUM, blkno, RBM_NORMAL, info->strategy); diff --git a/src/backend/access/gist/gistvacuum.c b/src/backend/access/gist/gistvacuum.c index 3f60d3274d2..f6608b9c3cf 100644 --- a/src/backend/access/gist/gistvacuum.c +++ b/src/backend/access/gist/gistvacuum.c @@ -283,7 +283,7 @@ gistvacuumpage(GistVacState *vstate, BlockNumber blkno, BlockNumber orig_blkno) recurse_to = InvalidBlockNumber; /* call vacuum_delay_point while not holding any buffer lock */ - vacuum_delay_point(); + vacuum_delay_point(false); buffer = ReadBufferExtended(rel, MAIN_FORKNUM, blkno, RBM_NORMAL, info->strategy); diff --git a/src/backend/access/hash/hash.c b/src/backend/access/hash/hash.c index e50f117bcb9..bb5fd890d76 100644 --- a/src/backend/access/hash/hash.c +++ b/src/backend/access/hash/hash.c @@ -732,7 +732,7 @@ hashbucketcleanup(Relation rel, Bucket cur_bucket, Buffer bucket_buf, bool retain_pin = false; bool clear_dead_marking = false; - vacuum_delay_point(); + vacuum_delay_point(false); page = BufferGetPage(buf); opaque = HashPageGetOpaque(page); diff --git a/src/backend/access/heap/heapam.c b/src/backend/access/heap/heapam.c index 19c69270371..7fd1244ed66 100644 --- a/src/backend/access/heap/heapam.c +++ b/src/backend/access/heap/heapam.c @@ -1974,7 +1974,7 @@ heap_insert(Relation relation, HeapTuple tup, CommandId cid, { all_visible_cleared = true; PageClearAllVisible(BufferGetPage(buffer)); - visibilitymap_clear(relation, + visibilitymap_clear_rel(relation, ItemPointerGetBlockNumber(&(heaptup->t_self)), vmbuffer, VISIBILITYMAP_VALID_BITS); } @@ -2350,7 +2350,7 @@ heap_multi_insert(Relation relation, TupleTableSlot **slots, int ntuples, { all_visible_cleared = true; PageClearAllVisible(page); - visibilitymap_clear(relation, + visibilitymap_clear_rel(relation, BufferGetBlockNumber(buffer), vmbuffer, VISIBILITYMAP_VALID_BITS); } @@ -2935,7 +2935,7 @@ heap_delete(Relation relation, ItemPointer tid, { all_visible_cleared = true; PageClearAllVisible(page); - visibilitymap_clear(relation, BufferGetBlockNumber(buffer), + visibilitymap_clear_rel(relation, BufferGetBlockNumber(buffer), vmbuffer, VISIBILITYMAP_VALID_BITS); } @@ -3764,7 +3764,7 @@ heap_update(Relation relation, ItemPointer otid, HeapTuple newtup, * worthwhile. */ if (PageIsAllVisible(page) && - visibilitymap_clear(relation, block, vmbuffer, + visibilitymap_clear_rel(relation, block, vmbuffer, VISIBILITYMAP_ALL_FROZEN)) cleared_all_frozen = true; @@ -3998,14 +3998,14 @@ heap_update(Relation relation, ItemPointer otid, HeapTuple newtup, { all_visible_cleared = true; PageClearAllVisible(BufferGetPage(buffer)); - visibilitymap_clear(relation, BufferGetBlockNumber(buffer), + visibilitymap_clear_rel(relation, BufferGetBlockNumber(buffer), vmbuffer, VISIBILITYMAP_VALID_BITS); } if (newbuf != buffer && PageIsAllVisible(BufferGetPage(newbuf))) { all_visible_cleared_new = true; PageClearAllVisible(BufferGetPage(newbuf)); - visibilitymap_clear(relation, BufferGetBlockNumber(newbuf), + visibilitymap_clear_rel(relation, BufferGetBlockNumber(newbuf), vmbuffer_new, VISIBILITYMAP_VALID_BITS); } @@ -5095,7 +5095,7 @@ heap_lock_tuple(Relation relation, HeapTuple tuple, /* Clear only the all-frozen bit on visibility map if needed */ if (PageIsAllVisible(page) && - visibilitymap_clear(relation, block, vmbuffer, + visibilitymap_clear_rel(relation, block, vmbuffer, VISIBILITYMAP_ALL_FROZEN)) cleared_all_frozen = true; @@ -5852,7 +5852,7 @@ heap_lock_updated_tuple_rec(Relation rel, ItemPointer tid, TransactionId xid, &new_xmax, &new_infomask, &new_infomask2); if (PageIsAllVisible(BufferGetPage(buf)) && - visibilitymap_clear(rel, block, vmbuffer, + visibilitymap_clear_rel(rel, block, vmbuffer, VISIBILITYMAP_ALL_FROZEN)) cleared_all_frozen = true; diff --git a/src/backend/access/heap/vacuumlazy.c b/src/backend/access/heap/vacuumlazy.c index e3261b49a6c..5c82196d853 100644 --- a/src/backend/access/heap/vacuumlazy.c +++ b/src/backend/access/heap/vacuumlazy.c @@ -166,6 +166,7 @@ typedef struct LVRelState /* Aggressive VACUUM? (must set relfrozenxid >= FreezeLimit) */ bool aggressive; + bool freeze_age_vacuum; /* aggressive before DISABLE_PAGE_SKIPPING */ /* Use visibility map to skip? (disabled by DISABLE_PAGE_SKIPPING) */ bool skipwithvm; /* Consider index vacuuming bypass optimization? */ @@ -187,6 +188,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 */ @@ -206,7 +209,9 @@ typedef struct LVRelState BlockNumber scanned_pages; /* # pages examined (not skipped via VM) */ BlockNumber removed_pages; /* # pages removed by relation truncation */ BlockNumber frozen_pages; /* # pages with newly frozen tuples */ + BlockNumber all_visible_pages; /* # pages newly marked all-visible in VM */ BlockNumber lpdead_item_pages; /* # pages with LP_DEAD items */ + BlockNumber dead_pages; /* # pages with not-yet-removable tuples */ BlockNumber missed_dead_pages; /* # pages with missed dead tuples */ BlockNumber nonempty_pages; /* actually, last nonempty page + 1 */ @@ -225,6 +230,18 @@ typedef struct LVRelState int64 live_tuples; /* # live tuples remaining */ int64 recently_dead_tuples; /* # dead, but not yet removable */ int64 missed_dead_tuples; /* # removable, but not removed */ + + /* + * Resource usage of the index passes this process ran, subtracted from + * the heap report to avoid double-counting (see + * accumulate_heap_vacuum_statistics). + */ + PgStat_CommonCounts *extVacReportIdx; + + /* + * We need to accumulate index statistics for later subtraction from heap + * stats. + */ } LVRelState; /* @@ -312,6 +329,149 @@ static void update_vacuum_error_info(LVRelState *vacrel, static void restore_vacuum_error_info(LVRelState *vacrel, const LVSavedErrInfo *saved_vacrel); +/* Extended vacuum statistics functions */ + +/* + * extvac_stats_start - Allocate and snapshot extended instrumentation. + * + * Return NULL without allocating anything when neither VERBOSE nor a hook + * consumes the measurements. The caller frees a non-NULL result after + * reporting the phase, or its vacuum memory context cleans it up on error. + */ +LVExtStatCounters * +extvac_stats_start(Relation rel, bool verbose) +{ + LVExtStatCounters *counters; + + if (!verbose && set_report_vacuum_hook == NULL) + return NULL; + + counters = palloc0(sizeof(LVExtStatCounters)); + counters->walusage = pgWalUsage; + counters->bufusage = pgBufferUsage; + + if (rel->pgstat_info && pgstat_track_counts) + { + counters->blocks_fetched = rel->pgstat_info->counts.blocks_fetched; + counters->blocks_hit = rel->pgstat_info->counts.blocks_hit; + } + return counters; +} + +/* + * extvac_stats_end - Diff resource usage since extvac_stats_start into report. + */ +void +extvac_stats_end(Relation rel, LVExtStatCounters * counters, + PgStat_CommonCounts * report) +{ + WalUsage walusage; + BufferUsage bufusage; + + memset(&walusage, 0, sizeof(WalUsage)); + WalUsageAccumDiff(&walusage, &pgWalUsage, &counters->walusage); + memset(&bufusage, 0, sizeof(BufferUsage)); + BufferUsageAccumDiff(&bufusage, &pgBufferUsage, &counters->bufusage); + + 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.local_blks_written + bufusage.shared_blks_written; + /* PostgreSQL 16 keeps a single timer for shared and local blocks */ + report->blk_read_time = INSTR_TIME_GET_MILLISEC(bufusage.blk_read_time); + report->blk_write_time = INSTR_TIME_GET_MILLISEC(bufusage.blk_write_time); + 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) + { + report->blks_fetched = rel->pgstat_info->counts.blocks_fetched - counters->blocks_fetched; + report->blks_hit = rel->pgstat_info->counts.blocks_hit - counters->blocks_hit; + } +} + +/* + * Print the same resource measurements consumed by extensions. VERBOSE is + * an independent consumer: no shared_preload_libraries entry is required. + */ +void +extvac_stats_log(Relation rel, const char *phase, const PgStat_CommonCounts *report) +{ + StringInfoData buf; + + initStringInfo(&buf); + appendStringInfo(&buf, _("vacuum resource usage for \"%s.%s\" (%s):\n"), + get_namespace_name(RelationGetNamespace(rel)), + RelationGetRelationName(rel), phase); + appendStringInfo(&buf, + _("buffer usage: %lld hits, %lld misses, %lld dirtied, %lld written\n"), + (long long) report->total_blks_hit, + (long long) report->total_blks_read, + (long long) report->total_blks_dirtied, + (long long) report->total_blks_written); + if (pgstat_track_counts) + appendStringInfo(&buf, _("relation buffer usage: %lld hits, %lld misses\n"), + (long long) report->blks_hit, + (long long) (report->blks_fetched - report->blks_hit)); + appendStringInfo(&buf, + _("WAL usage: %lld records, %lld full page images, %llu bytes\n"), + (long long) report->wal_records, + (long long) report->wal_fpi, + (unsigned long long) report->wal_bytes); + if (track_io_timing) + appendStringInfo(&buf, _("I/O timings: read: %.3f ms, write: %.3f ms\n"), + report->blk_read_time, report->blk_write_time); + + ereport(INFO, (errmsg_internal("%s", buf.data))); + pfree(buf.data); +} + +/* + * 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; + + /* + * Subtract the resource usage of the index passes this process ran: they + * are reported per index, and the database-wide aggregate would count + * them twice otherwise. Parallel workers report their own usage with + * their index passes, so it never enters the leader's counters. + */ + extVacStats->common.total_blks_read -= vacrel->extVacReportIdx->total_blks_read; + extVacStats->common.total_blks_hit -= vacrel->extVacReportIdx->total_blks_hit; + extVacStats->common.total_blks_dirtied -= vacrel->extVacReportIdx->total_blks_dirtied; + extVacStats->common.total_blks_written -= vacrel->extVacReportIdx->total_blks_written; + extVacStats->common.blk_read_time -= vacrel->extVacReportIdx->blk_read_time; + extVacStats->common.blk_write_time -= vacrel->extVacReportIdx->blk_write_time; + extVacStats->common.wal_records -= vacrel->extVacReportIdx->wal_records; + extVacStats->common.wal_fpi -= vacrel->extVacReportIdx->wal_fpi; + extVacStats->common.wal_bytes -= vacrel->extVacReportIdx->wal_bytes; +} + +/* + * Accumulate an index pass report into the running total that is later + * subtracted from the heap report. + */ +static void +accumulate_idxs_vacuum_statistics(LVRelState *vacrel, + PgStat_VacuumRelationCounts * extVacIdxStats) +{ + vacrel->extVacReportIdx->total_blks_read += extVacIdxStats->common.total_blks_read; + vacrel->extVacReportIdx->total_blks_hit += extVacIdxStats->common.total_blks_hit; + vacrel->extVacReportIdx->total_blks_dirtied += extVacIdxStats->common.total_blks_dirtied; + vacrel->extVacReportIdx->total_blks_written += extVacIdxStats->common.total_blks_written; + vacrel->extVacReportIdx->blk_read_time += extVacIdxStats->common.blk_read_time; + vacrel->extVacReportIdx->blk_write_time += extVacIdxStats->common.blk_write_time; + vacrel->extVacReportIdx->wal_records += extVacIdxStats->common.wal_records; + vacrel->extVacReportIdx->wal_fpi += extVacIdxStats->common.wal_fpi; + vacrel->extVacReportIdx->wal_bytes += extVacIdxStats->common.wal_bytes; +} + /* * lazy_vacuum_rel_heap() -- perform VACUUM for one heap relation * @@ -335,11 +495,14 @@ heap_vacuum_rel(Relation rel, VacuumParams *params, minmulti_updated; BlockNumber orig_rel_pages, new_rel_pages, - new_rel_allvisible; + new_rel_allvisible, + new_rel_allfrozen; PGRUsage ru0; TimestampTz starttime = 0; PgStat_Counter startreadtime = 0, startwritetime = 0; + double startdelaytime; + LVExtStatCounters *extVacCounters; WalUsage startwalusage = pgWalUsage; BufferUsage startbufferusage = pgBufferUsage; ErrorContextCallback errcallback; @@ -351,7 +514,6 @@ heap_vacuum_rel(Relation rel, VacuumParams *params, if (instrument) { pg_rusage_init(&ru0); - starttime = GetCurrentTimestamp(); if (track_io_timing) { startreadtime = pgStatBlockReadTime; @@ -359,6 +521,12 @@ heap_vacuum_rel(Relation rel, VacuumParams *params, } } + /* Used for instrumentation and stats report */ + starttime = GetCurrentTimestamp(); + startdelaytime = VacuumDelayTime; + + extVacCounters = extvac_stats_start(rel, verbose); + pgstat_progress_start_command(PROGRESS_COMMAND_VACUUM, RelationGetRelid(rel)); @@ -375,9 +543,12 @@ heap_vacuum_rel(Relation rel, VacuumParams *params, * these temp copies. */ vacrel = (LVRelState *) palloc0(sizeof(LVRelState)); + if (extVacCounters != NULL) + vacrel->extVacReportIdx = palloc0(sizeof(PgStat_CommonCounts)); 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; @@ -442,6 +613,7 @@ heap_vacuum_rel(Relation rel, VacuumParams *params, vacrel->scanned_pages = 0; vacrel->removed_pages = 0; vacrel->frozen_pages = 0; + vacrel->all_visible_pages = 0; vacrel->lpdead_item_pages = 0; vacrel->missed_dead_pages = 0; vacrel->nonempty_pages = 0; @@ -478,6 +650,8 @@ heap_vacuum_rel(Relation rel, VacuumParams *params, * time to time, to increase the number of dead tuples it can prune away.) */ vacrel->aggressive = vacuum_get_cutoffs(rel, params, &vacrel->cutoffs); + /* DISABLE_PAGE_SKIPPING alone must not count as a freeze-age run. */ + vacrel->freeze_age_vacuum = vacrel->aggressive; vacrel->rel_pages = orig_rel_pages = RelationGetNumberOfBlocks(rel); vacrel->vistest = GlobalVisTestFor(rel); /* Initialize state used to track oldest extant XID/MXID */ @@ -589,10 +763,18 @@ heap_vacuum_rel(Relation rel, VacuumParams *params, * pg_class.relpages to */ new_rel_pages = vacrel->rel_pages; /* After possible rel truncation */ - visibilitymap_count(rel, &new_rel_allvisible, NULL); + visibilitymap_count(rel, &new_rel_allvisible, &new_rel_allfrozen); if (new_rel_allvisible > new_rel_pages) new_rel_allvisible = new_rel_pages; + /* + * An all-frozen block _must_ be all-visible. As such, clamp the count of + * all-frozen blocks to the count of all-visible blocks. This matches the + * clamping of relallvisible above. + */ + if (new_rel_allfrozen > new_rel_allvisible) + new_rel_allfrozen = new_rel_allvisible; + /* * Now actually update rel's pg_class entry. * @@ -601,10 +783,30 @@ heap_vacuum_rel(Relation rel, VacuumParams *params, * scan every page that isn't skipped using the visibility map. */ vac_update_relstats(rel, new_rel_pages, vacrel->new_live_tuples, - new_rel_allvisible, vacrel->nindexes > 0, + new_rel_allvisible, new_rel_allfrozen, + vacrel->nindexes > 0, vacrel->NewRelfrozenXid, vacrel->NewRelminMxid, &frozenxid_updated, &minmulti_updated, false, true); + /* Core work counters are collected even without an extension hook. */ + { + PgStat_VacuumStats stats = {0}; + + stats.tuples_deleted = vacrel->tuples_deleted; + stats.dead_tuples = vacrel->recently_dead_tuples + vacrel->missed_dead_tuples; + stats.dead_pages = vacrel->dead_pages; + stats.pages_frozen = vacrel->frozen_pages; + stats.pages_all_visible = vacrel->all_visible_pages; + stats.freeze_age_vacuum_count = vacrel->freeze_age_vacuum ? 1 : 0; + stats.tuples_frozen = vacrel->tuples_frozen; + stats.recently_dead_tuples = vacrel->recently_dead_tuples; + stats.missed_dead_tuples = vacrel->missed_dead_tuples; + stats.pages_scanned = vacrel->scanned_pages; + stats.pages_removed = vacrel->removed_pages; + stats.missed_dead_pages = vacrel->missed_dead_pages; + pgstat_report_vacuum_stats(rel, &stats); + } + /* * Report results to the cumulative stats system, too. * @@ -615,11 +817,44 @@ heap_vacuum_rel(Relation rel, VacuumParams *params, * soon in cases where the failsafe prevented significant amounts of heap * vacuuming. */ - pgstat_report_vacuum(RelationGetRelid(rel), - rel->rd_rel->relisshared, - Max(vacrel->new_live_tuples, 0), - vacrel->recently_dead_tuples + - vacrel->missed_dead_tuples); + /* + * The delay counter covers the whole heap_vacuum_rel() run, matching the + * scope of total_vacuum_time. In a parallel vacuum it covers the + * leader's sleeps only; parallel workers account their own sleeps to the + * indexes they process. + */ + if (extVacCounters != NULL) + { + PgStat_VacuumRelationCounts *extVacReport = &extVacCounters->report; + + extvac_stats_end(rel, extVacCounters, &extVacReport->common); + accumulate_heap_vacuum_statistics(vacrel, extVacReport); + if (verbose) + extvac_stats_log(rel, _("heap (excluding indexes)"), + &extVacReport->common); + + 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); + pfree(extVacCounters); + pfree(vacrel->extVacReportIdx); + } + 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) @@ -754,8 +989,9 @@ heap_vacuum_rel(Relation rel, VacuumParams *params, continue; appendStringInfo(&buf, - _("index \"%s\": pages: %u in total, %u newly deleted, %u currently deleted, %u reusable\n"), + _("index \"%s\": tuples: %.0f removed; pages: %u in total, %u newly deleted, %u currently deleted, %u reusable\n"), indnames[i], + istat->tuples_removed, istat->num_pages, istat->pages_newly_deleted, istat->pages_deleted, @@ -911,7 +1147,7 @@ lazy_scan_heap(LVRelState *vacrel) update_vacuum_error_info(vacrel, NULL, VACUUM_ERRCB_PHASE_SCAN_HEAP, blkno, InvalidOffsetNumber); - vacuum_delay_point(); + vacuum_delay_point(false); /* * Regularly check if wraparound failsafe should trigger. @@ -1135,6 +1371,12 @@ lazy_scan_heap(LVRelState *vacrel) */ PageSetAllVisible(page); MarkBufferDirty(buf); + /* + * Count only new VM marks, using the current bit under the heap + * lock rather than the possibly stale lazy_scan_skip() result. + */ + if (!VM_ALL_VISIBLE(vacrel->rel, blkno, &vmbuffer)) + vacrel->all_visible_pages++; visibilitymap_set(vacrel->rel, blkno, buf, InvalidXLogRecPtr, vmbuffer, prunestate.visibility_cutoff_xid, flags); @@ -1151,7 +1393,7 @@ lazy_scan_heap(LVRelState *vacrel) { elog(WARNING, "page is not marked all-visible but visibility map bit is set in relation \"%s\" page %u", vacrel->relname, blkno); - visibilitymap_clear(vacrel->rel, blkno, vmbuffer, + visibilitymap_clear_rel(vacrel->rel, blkno, vmbuffer, VISIBILITYMAP_VALID_BITS); } @@ -1176,7 +1418,7 @@ lazy_scan_heap(LVRelState *vacrel) vacrel->relname, blkno); PageClearAllVisible(page); MarkBufferDirty(buf); - visibilitymap_clear(vacrel->rel, blkno, vmbuffer, + visibilitymap_clear_rel(vacrel->rel, blkno, vmbuffer, VISIBILITYMAP_VALID_BITS); } @@ -1208,6 +1450,8 @@ lazy_scan_heap(LVRelState *vacrel) * safe for REDO was logged when the page's tuples were frozen. */ Assert(!TransactionIdIsValid(prunestate.visibility_cutoff_xid)); + if (!VM_ALL_VISIBLE(vacrel->rel, blkno, &vmbuffer)) + vacrel->all_visible_pages++; visibilitymap_set(vacrel->rel, blkno, buf, InvalidXLogRecPtr, vmbuffer, InvalidTransactionId, VISIBILITYMAP_ALL_VISIBLE | @@ -1374,7 +1618,7 @@ lazy_scan_skip(LVRelState *vacrel, Buffer *vmbuffer, BlockNumber next_block, skipsallvis = true; } - vacuum_delay_point(); + vacuum_delay_point(false); next_unskippable_block++; nskippable_blocks++; } @@ -1516,6 +1760,8 @@ lazy_scan_new_or_empty(LVRelState *vacrel, Buffer buf, BlockNumber blkno, log_newpage_buffer(buf, true); PageSetAllVisible(page); + if (!VM_ALL_VISIBLE(vacrel->rel, blkno, &vmbuffer)) + vacrel->all_visible_pages++; visibilitymap_set(vacrel->rel, blkno, buf, InvalidXLogRecPtr, vmbuffer, InvalidTransactionId, VISIBILITYMAP_ALL_VISIBLE | VISIBILITYMAP_ALL_FROZEN); @@ -1956,6 +2202,8 @@ lazy_scan_prune(LVRelState *vacrel, vacrel->lpdead_items += lpdead_items; vacrel->live_tuples += live_tuples; vacrel->recently_dead_tuples += recently_dead_tuples; + if (recently_dead_tuples > 0) + vacrel->dead_pages++; } /* @@ -2190,6 +2438,8 @@ lazy_scan_noprune(LVRelState *vacrel, */ vacrel->live_tuples += live_tuples; vacrel->recently_dead_tuples += recently_dead_tuples; + if (recently_dead_tuples > 0) + vacrel->dead_pages++; vacrel->missed_dead_tuples += missed_dead_tuples; if (missed_dead_tuples > 0) vacrel->missed_dead_pages++; @@ -2460,7 +2710,7 @@ lazy_vacuum_heap_rel(LVRelState *vacrel) Page page; Size freespace; - vacuum_delay_point(); + vacuum_delay_point(false); blkno = ItemPointerGetBlockNumber(&vacrel->dead_items->items[index]); vacrel->blkno = blkno; @@ -2616,6 +2866,8 @@ lazy_vacuum_heap_page(LVRelState *vacrel, BlockNumber blkno, Buffer buffer, } PageSetAllVisible(page); + if (!VM_ALL_VISIBLE(vacrel->rel, blkno, &vmbuffer)) + vacrel->all_visible_pages++; visibilitymap_set(vacrel->rel, blkno, buffer, InvalidXLogRecPtr, vmbuffer, visibility_cutoff_xid, flags); } @@ -2734,7 +2986,22 @@ lazy_vacuum_one_index(Relation indrel, IndexBulkDeleteResult *istat, { IndexVacuumInfo ivinfo; LVSavedErrInfo saved_err_info; + TimestampTz istarttime = GetCurrentTimestamp(); + double startdelaytime = VacuumDelayTime; + double prev_tuples_removed = 0; + BlockNumber prev_pages_newly_deleted = 0; + LVExtStatCounters *extVacCounters; + /* + * 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; + prev_pages_newly_deleted = istat->pages_newly_deleted; + } + extVacCounters = extvac_stats_start(indrel, vacrel->verbose); ivinfo.index = indrel; ivinfo.heaprel = vacrel->rel; ivinfo.analyze_only = false; @@ -2752,12 +3019,36 @@ 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); /* Do bulk deletion */ istat = vac_bulkdel_one_index(&ivinfo, istat, (void *) vacrel->dead_items); + vacuum_report_index_stats(indrel, istat, prev_tuples_removed, + prev_pages_newly_deleted, false); + + /* Accumulate this pass into the index's cumulative vacuum times */ + pgstat_report_index_vacuum_time(indrel, + TimestampDifferenceMilliseconds(istarttime, + GetCurrentTimestamp()), + (PgStat_Counter) rint(VacuumDelayTime - + startdelaytime), + IsAutoVacuumWorkerProcess()); + + if (extVacCounters != NULL) + { + PgStat_VacuumRelationCounts *extVacReport = &extVacCounters->report; + + extvac_stats_end(indrel, extVacCounters, &extVacReport->common); + if (vacrel->verbose) + extvac_stats_log(indrel, _("index bulk delete"), &extVacReport->common); + extVacReport->type = PGSTAT_EXTVAC_INDEX; + pgstat_report_vacuum_ext(indrel, -1, -1, 0, 0, false, extVacReport); + accumulate_idxs_vacuum_statistics(vacrel, extVacReport); + pfree(extVacCounters); + } /* Revert to the previous phase information for error traceback */ restore_vacuum_error_info(vacrel, &saved_err_info); @@ -2783,7 +3074,22 @@ lazy_cleanup_one_index(Relation indrel, IndexBulkDeleteResult *istat, { IndexVacuumInfo ivinfo; LVSavedErrInfo saved_err_info; + TimestampTz istarttime = GetCurrentTimestamp(); + double startdelaytime = VacuumDelayTime; + double prev_tuples_removed = 0; + BlockNumber prev_pages_newly_deleted = 0; + LVExtStatCounters *extVacCounters; + /* + * 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; + prev_pages_newly_deleted = istat->pages_newly_deleted; + } + extVacCounters = extvac_stats_start(indrel, vacrel->verbose); ivinfo.index = indrel; ivinfo.heaprel = vacrel->rel; ivinfo.analyze_only = false; @@ -2802,11 +3108,35 @@ lazy_cleanup_one_index(Relation indrel, IndexBulkDeleteResult *istat, */ Assert(vacrel->indname == NULL); vacrel->indname = pstrdup(RelationGetRelationName(indrel)); + vacrel->indoid = RelationGetRelid(indrel); update_vacuum_error_info(vacrel, &saved_err_info, VACUUM_ERRCB_PHASE_INDEX_CLEANUP, InvalidBlockNumber, InvalidOffsetNumber); istat = vac_cleanup_one_index(&ivinfo, istat); + vacuum_report_index_stats(indrel, istat, prev_tuples_removed, + prev_pages_newly_deleted, true); + + /* Accumulate this pass into the index's cumulative vacuum times */ + pgstat_report_index_vacuum_time(indrel, + TimestampDifferenceMilliseconds(istarttime, + GetCurrentTimestamp()), + (PgStat_Counter) rint(VacuumDelayTime - + startdelaytime), + IsAutoVacuumWorkerProcess()); + + if (extVacCounters != NULL) + { + PgStat_VacuumRelationCounts *extVacReport = &extVacCounters->report; + + extvac_stats_end(indrel, extVacCounters, &extVacReport->common); + if (vacrel->verbose) + extvac_stats_log(indrel, _("index cleanup"), &extVacReport->common); + extVacReport->type = PGSTAT_EXTVAC_INDEX; + pgstat_report_vacuum_ext(indrel, -1, -1, 0, 0, false, extVacReport); + accumulate_idxs_vacuum_statistics(vacrel, extVacReport); + pfree(extVacCounters); + } /* Revert to the previous phase information for error traceback */ restore_vacuum_error_info(vacrel, &saved_err_info); @@ -3400,7 +3730,7 @@ update_relstats_all_indexes(LVRelState *vacrel) vac_update_relstats(indrel, istat->num_pages, istat->num_index_tuples, - 0, + 0, 0, false, InvalidTransactionId, InvalidMultiXactId, @@ -3419,6 +3749,10 @@ vacuum_error_callback(void *arg) { LVRelState *errinfo = arg; + /* Ignore non-error reports that also invoke this callback. */ + if (errinfo->rel != NULL && geterrlevel() == ERROR) + pgstat_count_vacuum_error(errinfo->rel->rd_rel->relisshared); + switch (errinfo->phase) { case VACUUM_ERRCB_PHASE_SCAN_HEAP: diff --git a/src/backend/access/heap/visibilitymap.c b/src/backend/access/heap/visibilitymap.c index 2e18cd88bcf..562c7fdc10f 100644 --- a/src/backend/access/heap/visibilitymap.c +++ b/src/backend/access/heap/visibilitymap.c @@ -12,6 +12,8 @@ * * INTERFACE ROUTINES * visibilitymap_clear - clear bits for one page in the visibility map + * visibilitymap_clear_rel - clear bits for an open relation, counting the + * clears in its statistics * visibilitymap_pin - pin a map page for setting a bit * visibilitymap_pin_ok - check whether correct map page is already pinned * visibilitymap_set - set a bit in a previously pinned page @@ -91,6 +93,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/lmgr.h" @@ -125,6 +128,8 @@ * bit pair */ /* prototypes for internal routines */ +static bool vm_do_clear(Relation rel, BlockNumber heapBlk, Buffer vmbuf, + uint8 flags, bool count_stats); static Buffer vm_readbuf(Relation rel, BlockNumber blkno, bool extend); static Buffer vm_extend(Relation rel, BlockNumber vm_nblocks); @@ -135,9 +140,38 @@ static Buffer vm_extend(Relation rel, BlockNumber vm_nblocks); * You must pass a buffer containing the correct map page to this function. * Call visibilitymap_pin first to pin the right one. This function doesn't do * any I/O. Returns true if any bits have been cleared and false otherwise. + * + * This variant is for callers such as recovery, which use a fake relcache + * entry and cannot count the clears in the relation's statistics; callers + * holding an open relation should use visibilitymap_clear_rel() instead. */ bool visibilitymap_clear(Relation rel, BlockNumber heapBlk, Buffer vmbuf, uint8 flags) +{ + return vm_do_clear(rel, heapBlk, vmbuf, flags, false); +} + +/* + * visibilitymap_clear_rel - visibilitymap_clear() for an open relation + * + * Same as visibilitymap_clear(), but additionally counts the cleared + * all-visible and all-frozen marks in the relation's statistics, which + * makes the stability of its visibility map observable. + */ +bool +visibilitymap_clear_rel(Relation rel, BlockNumber heapBlk, Buffer vmbuf, + uint8 flags) +{ + return vm_do_clear(rel, heapBlk, vmbuf, flags, true); +} + +/* + * Workhorse of visibilitymap_clear() and visibilitymap_clear_rel(). + * count_stats is false when the caller has no real relcache entry at hand. + */ +static bool +vm_do_clear(Relation rel, BlockNumber heapBlk, Buffer vmbuf, uint8 flags, + bool count_stats) { BlockNumber mapBlock = HEAPBLK_TO_MAPBLOCK(heapBlk); int mapByte = HEAPBLK_TO_MAPBYTE(heapBlk); @@ -162,6 +196,18 @@ 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 (count_stats) + { + 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/access/nbtree/nbtree.c b/src/backend/access/nbtree/nbtree.c index 3ad1c7ea0d8..2345a5bcb02 100644 --- a/src/backend/access/nbtree/nbtree.c +++ b/src/backend/access/nbtree/nbtree.c @@ -1171,7 +1171,7 @@ btvacuumpage(BTVacState *vstate, BlockNumber scanblkno) backtrack_to = P_NONE; /* call vacuum_delay_point while not holding any buffer lock */ - vacuum_delay_point(); + vacuum_delay_point(false); /* * We can't use _bt_getbuf() here because it always applies diff --git a/src/backend/access/spgist/spgvacuum.c b/src/backend/access/spgist/spgvacuum.c index 85bc02b92c8..2352845c630 100644 --- a/src/backend/access/spgist/spgvacuum.c +++ b/src/backend/access/spgist/spgvacuum.c @@ -624,14 +624,16 @@ spgvacuumpage(spgBulkDeleteState *bds, BlockNumber blkno) Relation index = bds->info->index; Buffer buffer; Page page; + bool was_empty; /* call vacuum_delay_point while not holding any buffer lock */ - vacuum_delay_point(); + vacuum_delay_point(false); buffer = ReadBufferExtended(index, MAIN_FORKNUM, blkno, RBM_NORMAL, bds->info->strategy); LockBuffer(buffer, BUFFER_LOCK_EXCLUSIVE); page = (Page) BufferGetPage(buffer); + was_empty = PageIsNew(page) || PageIsEmpty(page); if (PageIsNew(page)) { @@ -675,6 +677,8 @@ spgvacuumpage(spgBulkDeleteState *bds, BlockNumber blkno) { RecordFreeIndexPage(index, blkno); bds->stats->pages_deleted++; + if (!was_empty) + bds->stats->pages_newly_deleted++; } else { @@ -705,7 +709,7 @@ spgprocesspending(spgBulkDeleteState *bds) continue; /* ignore already-done items */ /* call vacuum_delay_point while not holding any buffer lock */ - vacuum_delay_point(); + vacuum_delay_point(false); /* examine the referenced page */ blkno = ItemPointerGetBlockNumber(&pitem->tid); @@ -902,7 +906,6 @@ spgvacuumscan(spgBulkDeleteState *bds) /* Report final stats */ bds->stats->num_pages = num_pages; - bds->stats->pages_newly_deleted = bds->stats->pages_deleted; bds->stats->pages_free = bds->stats->pages_deleted; } diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c index 0b62b174afa..52adec9f666 100644 --- a/src/backend/catalog/heap.c +++ b/src/backend/catalog/heap.c @@ -1264,6 +1264,7 @@ InsertPgClassTuple(Relation pg_class_desc, values[Anum_pg_class_relpages - 1] = Int32GetDatum(rd_rel->relpages); values[Anum_pg_class_reltuples - 1] = Float4GetDatum(rd_rel->reltuples); values[Anum_pg_class_relallvisible - 1] = Int32GetDatum(rd_rel->relallvisible); + values[Anum_pg_class_relallfrozen - 1] = Int32GetDatum(rd_rel->relallfrozen); values[Anum_pg_class_reltoastrelid - 1] = ObjectIdGetDatum(rd_rel->reltoastrelid); values[Anum_pg_class_relhasindex - 1] = BoolGetDatum(rd_rel->relhasindex); values[Anum_pg_class_relisshared - 1] = BoolGetDatum(rd_rel->relisshared); @@ -1337,6 +1338,7 @@ AddNewRelationTuple(Relation pg_class_desc, new_rel_reltup->relpages = 0; new_rel_reltup->reltuples = -1; new_rel_reltup->relallvisible = 0; + new_rel_reltup->relallfrozen = 0; /* Sequences always have a known size */ if (relkind == RELKIND_SEQUENCE) diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c index fadd32fde9a..7c7df5d4c58 100644 --- a/src/backend/catalog/index.c +++ b/src/backend/catalog/index.c @@ -2945,8 +2945,8 @@ FormIndexDatum(struct IndexInfo *indexInfo, * hasindex: set relhasindex to this value * reltuples: if >= 0, set reltuples to this value; else no change * - * If reltuples >= 0, relpages and relallvisible are also updated (using - * RelationGetNumberOfBlocks() and visibilitymap_count()). + * If reltuples >= 0, relpages, relallvisible, and relallfrozen are also + * updated (using RelationGetNumberOfBlocks() and visibilitymap_count()). * * NOTE: an important side-effect of this operation is that an SI invalidation * message is sent out to all backends --- including me --- causing relcache @@ -2964,6 +2964,7 @@ index_update_stats(Relation rel, bool update_stats; BlockNumber relpages = 0; /* keep compiler quiet */ BlockNumber relallvisible = 0; + BlockNumber relallfrozen = 0; Oid relid = RelationGetRelid(rel); Relation pg_class; ScanKeyData key[1]; @@ -2985,7 +2986,7 @@ index_update_stats(Relation rel, if (reltuples == 0 && rel->rd_rel->reltuples < 0) reltuples = -1; - update_stats = reltuples >= 0; + update_stats = reltuples >= 0 && Gp_role != GP_ROLE_DISPATCH; /* * Finish I/O and visibility map buffer locks before @@ -2998,8 +2999,8 @@ index_update_stats(Relation rel, { relpages = RelationGetNumberOfBlocks(rel); - if (rel->rd_rel->relkind != RELKIND_INDEX) - visibilitymap_count(rel, &relallvisible, NULL); + if (rel->rd_rel->relkind != RELKIND_INDEX && !RelationStorageIsAO(rel)) + visibilitymap_count(rel, &relallvisible, &relallfrozen); } /* @@ -3055,16 +3056,8 @@ index_update_stats(Relation rel, dirty = true; } - if (reltuples >= 0 && Gp_role != GP_ROLE_DISPATCH) + if (update_stats) { - /* - * GPDB: We don't maintain relallvisible for AO/CO tables. - */ - if (rd_rel->relkind != RELKIND_INDEX && !RelationStorageIsAO(rel)) - visibilitymap_count(rel, &relallvisible, NULL); - else /* don't bother for indexes */ - relallvisible = 0; - if (rd_rel->relpages != (int32) relpages) { rd_rel->relpages = (int32) relpages; @@ -3080,6 +3073,11 @@ index_update_stats(Relation rel, rd_rel->relallvisible = (int32) relallvisible; dirty = true; } + if (rd_rel->relallfrozen != (int32) relallfrozen) + { + rd_rel->relallfrozen = (int32) relallfrozen; + dirty = true; + } } /* diff --git a/src/backend/catalog/system_functions.sql b/src/backend/catalog/system_functions.sql index 39c4c309a0b..89222ff88d6 100644 --- a/src/backend/catalog/system_functions.sql +++ b/src/backend/catalog/system_functions.sql @@ -659,6 +659,13 @@ REVOKE EXECUTE ON FUNCTION pg_current_logfile(text) FROM public; REVOKE EXECUTE ON FUNCTION pg_promote(boolean, integer) FROM public; +CREATE OR REPLACE FUNCTION pg_stat_reset_vacuum_stats(relid oid DEFAULT NULL) +RETURNS void +LANGUAGE internal VOLATILE PARALLEL UNSAFE +AS 'pg_stat_reset_vacuum_stats'; + +REVOKE EXECUTE ON FUNCTION pg_stat_reset_vacuum_stats(oid) FROM public; + REVOKE EXECUTE ON FUNCTION pg_stat_reset() FROM public; REVOKE EXECUTE ON FUNCTION pg_stat_reset_shared(text) FROM public; diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql index 24b7fc1d33b..601f0d46484 100644 --- a/src/backend/catalog/system_views.sql +++ b/src/backend/catalog/system_views.sql @@ -701,7 +701,16 @@ CREATE VIEW pg_stat_all_tables AS pg_stat_get_vacuum_count(C.oid) AS vacuum_count, pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count, pg_stat_get_analyze_count(C.oid) AS analyze_count, - pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count + pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count, + pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time, + pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time, + pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time, + pg_stat_get_total_autoanalyze_time(C.oid) AS total_autoanalyze_time, + pg_stat_get_total_vacuum_delay_time(C.oid) AS total_vacuum_delay_time, + pg_stat_get_total_autovacuum_delay_time(C.oid) AS total_autovacuum_delay_time, + pg_stat_get_vacuum_failsafe_count(C.oid) AS vacuum_failsafe_count, + 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) @@ -821,7 +830,11 @@ CREATE VIEW pg_stat_all_indexes AS pg_stat_get_numscans(I.oid) AS idx_scan, pg_stat_get_lastscan(I.oid) AS last_idx_scan, pg_stat_get_tuples_returned(I.oid) AS idx_tup_read, - pg_stat_get_tuples_fetched(I.oid) AS idx_tup_fetch + pg_stat_get_tuples_fetched(I.oid) AS idx_tup_fetch, + pg_stat_get_total_vacuum_time(I.oid) AS total_vacuum_time, + pg_stat_get_total_autovacuum_time(I.oid) AS total_autovacuum_time, + pg_stat_get_total_vacuum_delay_time(I.oid) AS total_vacuum_delay_time, + pg_stat_get_total_autovacuum_delay_time(I.oid) AS total_autovacuum_delay_time FROM pg_class C JOIN pg_index X ON C.oid = X.indrelid JOIN pg_class I ON I.oid = X.indexrelid @@ -1205,6 +1218,69 @@ CREATE VIEW pg_stat_database AS UNION ALL SELECT oid, datname FROM pg_database ) D; + +CREATE VIEW pg_stat_vacuum AS + SELECT + D.oid AS datid, + D.datname AS datname, + pg_stat_get_db_total_vacuum_time(D.oid) AS total_vacuum_time, + pg_stat_get_db_total_autovacuum_time(D.oid) AS total_autovacuum_time, + pg_stat_get_db_total_vacuum_delay_time(D.oid) AS total_vacuum_delay_time, + pg_stat_get_db_total_autovacuum_delay_time(D.oid) AS total_autovacuum_delay_time, + pg_stat_get_db_vacuum_failsafe_count(D.oid) AS vacuum_failsafe_count, + pg_stat_get_db_vacuum_interrupt_count(D.oid) AS vacuum_interrupt_count, + pg_stat_get_db_stat_reset_time(D.oid) AS stats_reset, + V.tuples_deleted, + V.dead_pages, + V.pages_frozen, + V.pages_all_visible, + V.freeze_age_vacuum_count, + V.tuples_frozen, + V.recently_dead_tuples, + V.missed_dead_tuples, + V.pages_scanned, + V.pages_removed, + V.missed_dead_pages, + V.compacted_segments, + V.tuples_moved + FROM ( + SELECT 0 AS oid, NULL::name AS datname + UNION ALL + SELECT oid, datname FROM pg_database + ) D, LATERAL pg_stat_get_vacuum_database_stats(D.oid) V; + +CREATE VIEW pg_stat_vacuum_tables AS + SELECT C.oid AS relid, N.nspname AS schema, C.relname, + current_database() AS dbname, + V.tuples_deleted, + V.dead_tuples, + V.dead_pages, + V.pages_frozen, + V.pages_all_visible, + V.freeze_age_vacuum_count, + V.tuples_frozen, + V.recently_dead_tuples, + V.missed_dead_tuples, + V.pages_scanned, + V.pages_removed, + V.missed_dead_pages, + V.total_file_segs, + V.compacted_segments, + V.tuples_moved + FROM pg_class C JOIN pg_namespace N ON N.oid = C.relnamespace, + LATERAL pg_stat_get_vacuum_stats(C.oid) V + WHERE C.relkind IN ('r', 't', 'm'); + +CREATE VIEW pg_stat_vacuum_indexes AS + SELECT C.oid AS indexrelid, N.nspname AS schema, C.relname AS indexrelname, + current_database() AS dbname, + V.tuples_deleted, + V.pages_deleted, + V.dead_pages + FROM pg_class C JOIN pg_namespace N ON N.oid = C.relnamespace, + LATERAL pg_stat_get_vacuum_stats(C.oid) V + WHERE C.relkind IN ('i'); + CREATE VIEW pg_resqueue_status AS SELECT q.rsqname, @@ -1564,7 +1640,8 @@ CREATE VIEW pg_stat_progress_analyze AS S.param5 AS ext_stats_computed, S.param6 AS child_tables_total, S.param7 AS child_tables_done, - CAST(S.param8 AS oid) AS current_child_table_relid + CAST(S.param8 AS oid) AS current_child_table_relid, + S.param9 / 1000000::double precision AS delay_time FROM pg_stat_get_progress_info('ANALYZE') AS S LEFT JOIN pg_database D ON S.datid = D.oid; @@ -1585,7 +1662,8 @@ CREATE VIEW pg_stat_progress_vacuum AS END AS phase, S.param2 AS heap_blks_total, S.param3 AS heap_blks_scanned, S.param4 AS heap_blks_vacuumed, S.param5 AS index_vacuum_count, - S.param6 AS max_dead_tuples, S.param7 AS num_dead_tuples + S.param6 AS max_dead_tuples, S.param7 AS num_dead_tuples, + S.param8 / 1000000::double precision AS delay_time FROM pg_stat_get_progress_info('VACUUM') AS S LEFT JOIN pg_database D ON S.datid = D.oid; @@ -1879,6 +1957,15 @@ $$ SELECT pg_stat_reset_shared(target) FROM gp_dist_random('gp_id'); $$ LANGUAGE SQL; +-- Reset native VACUUM work on the coordinator and every primary segment. +CREATE FUNCTION gp_stat_reset_vacuum_stats(relid oid DEFAULT NULL) RETURNS void AS +$$ + SELECT pg_stat_reset_vacuum_stats(relid) FROM gp_dist_random('gp_id'); + SELECT pg_stat_reset_vacuum_stats(relid); +$$ LANGUAGE SQL VOLATILE PARALLEL UNSAFE; + +REVOKE EXECUTE ON FUNCTION gp_stat_reset_vacuum_stats(oid) FROM public; + CREATE FUNCTION gp_stat_reset_single_table_counters(tableoid oid) RETURNS VOID AS $$ WITH table_info AS ( @@ -1933,4 +2020,4 @@ $$ WHERE pg_subscription.subname = sub_info.subname ) from gp_dist_random('gp_id'); -$$ LANGUAGE SQL; \ No newline at end of file +$$ LANGUAGE SQL; diff --git a/src/backend/catalog/system_views_gp.in b/src/backend/catalog/system_views_gp.in index 41e4a2468e5..025afa3f45a 100644 --- a/src/backend/catalog/system_views_gp.in +++ b/src/backend/catalog/system_views_gp.in @@ -14,6 +14,9 @@ pg_stat_all_tables pg_stat_archiver pg_stat_bgwriter pg_stat_database +pg_stat_vacuum +pg_stat_vacuum_tables +pg_stat_vacuum_indexes pg_stat_database_conflicts pg_stat_gssapi pg_stat_operations diff --git a/src/backend/catalog/system_views_gp_summary.sql b/src/backend/catalog/system_views_gp_summary.sql index 62bde4e3d96..2b1b40898d8 100644 --- a/src/backend/catalog/system_views_gp_summary.sql +++ b/src/backend/catalog/system_views_gp_summary.sql @@ -94,6 +94,36 @@ GROUP BY sdb.datname; +CREATE VIEW gp_stat_vacuum_summary AS +SELECT + sdb.datid, + sdb.datname, + sum(sdb.total_vacuum_time) as total_vacuum_time, + sum(sdb.total_autovacuum_time) as total_autovacuum_time, + sum(sdb.total_vacuum_delay_time) as total_vacuum_delay_time, + sum(sdb.total_autovacuum_delay_time) as total_autovacuum_delay_time, + max(sdb.vacuum_failsafe_count) as vacuum_failsafe_count, + max(sdb.vacuum_interrupt_count) as vacuum_interrupt_count, + max(sdb.stats_reset) as stats_reset, + sum(sdb.tuples_deleted)::bigint AS tuples_deleted, + sum(sdb.dead_pages)::bigint AS dead_pages, + sum(sdb.pages_frozen)::bigint AS pages_frozen, + sum(sdb.pages_all_visible)::bigint AS pages_all_visible, + sum(sdb.freeze_age_vacuum_count)::bigint AS freeze_age_vacuum_count, + sum(sdb.tuples_frozen)::bigint AS tuples_frozen, + sum(sdb.recently_dead_tuples)::bigint AS recently_dead_tuples, + sum(sdb.missed_dead_tuples)::bigint AS missed_dead_tuples, + sum(sdb.pages_scanned)::bigint AS pages_scanned, + sum(sdb.pages_removed)::bigint AS pages_removed, + sum(sdb.missed_dead_pages)::bigint AS missed_dead_pages, + sum(sdb.compacted_segments)::bigint AS compacted_segments, + sum(sdb.tuples_moved)::bigint AS tuples_moved +FROM + gp_stat_vacuum sdb +GROUP BY + sdb.datid, + sdb.datname; + -- Gather data from segments on user tables, and use data on coordinator on system tables. CREATE VIEW gp_stat_all_tables_summary AS SELECT @@ -120,7 +150,16 @@ SELECT s.vacuum_count, s.autovacuum_count, s.analyze_count, - s.autoanalyze_count + s.autoanalyze_count, + m.total_vacuum_time, + m.total_autovacuum_time, + m.total_analyze_time, + m.total_autoanalyze_time, + m.total_vacuum_delay_time, + m.total_autovacuum_delay_time, + m.vacuum_failsafe_count, + m.visible_page_marks_cleared, + m.frozen_page_marks_cleared FROM (SELECT allt.relid, @@ -148,7 +187,16 @@ FROM max(vacuum_count) as vacuum_count, max(autovacuum_count) as autovacuum_count, max(analyze_count) as analyze_count, - max(autoanalyze_count) as autoanalyze_count + max(autoanalyze_count) as autoanalyze_count, + case when d.policytype = 'r' then (sum(total_vacuum_time)/d.numsegments) else sum(total_vacuum_time) end total_vacuum_time, + case when d.policytype = 'r' then (sum(total_autovacuum_time)/d.numsegments) else sum(total_autovacuum_time) end total_autovacuum_time, + case when d.policytype = 'r' then (sum(total_analyze_time)/d.numsegments) else sum(total_analyze_time) end total_analyze_time, + case when d.policytype = 'r' then (sum(total_autoanalyze_time)/d.numsegments) else sum(total_autoanalyze_time) end total_autoanalyze_time, + case when d.policytype = 'r' then (sum(total_vacuum_delay_time)/d.numsegments) else sum(total_vacuum_delay_time) end total_vacuum_delay_time, + case when d.policytype = 'r' then (sum(total_autovacuum_delay_time)/d.numsegments) else sum(total_autovacuum_delay_time) end total_autovacuum_delay_time, + max(vacuum_failsafe_count) as vacuum_failsafe_count, + case when d.policytype = 'r' then (sum(visible_page_marks_cleared)/d.numsegments)::bigint else sum(visible_page_marks_cleared) end visible_page_marks_cleared, + case when d.policytype = 'r' then (sum(frozen_page_marks_cleared)/d.numsegments)::bigint else sum(frozen_page_marks_cleared) end frozen_page_marks_cleared FROM gp_dist_random('pg_stat_all_tables') allt inner join pg_class c @@ -229,7 +277,11 @@ SELECT m.idx_scan, m.last_idx_scan, m.idx_tup_read, - m.idx_tup_fetch + m.idx_tup_fetch, + m.total_vacuum_time, + m.total_autovacuum_time, + m.total_vacuum_delay_time, + m.total_autovacuum_delay_time FROM (SELECT alli.relid, @@ -240,7 +292,11 @@ FROM case when d.policytype = 'r' then (sum(alli.idx_scan)/d.numsegments)::bigint else sum(alli.idx_scan) end idx_scan, max(last_idx_scan) as last_idx_scan, case when d.policytype = 'r' then (sum(alli.idx_tup_read)/d.numsegments)::bigint else sum(alli.idx_tup_read) end idx_tup_read, - case when d.policytype = 'r' then (sum(alli.idx_tup_fetch)/d.numsegments)::bigint else sum(alli.idx_tup_fetch) end idx_tup_fetch + case when d.policytype = 'r' then (sum(alli.idx_tup_fetch)/d.numsegments)::bigint else sum(alli.idx_tup_fetch) end idx_tup_fetch, + case when d.policytype = 'r' then (sum(alli.total_vacuum_time)/d.numsegments) else sum(alli.total_vacuum_time) end total_vacuum_time, + case when d.policytype = 'r' then (sum(alli.total_autovacuum_time)/d.numsegments) else sum(alli.total_autovacuum_time) end total_autovacuum_time, + case when d.policytype = 'r' then (sum(alli.total_vacuum_delay_time)/d.numsegments) else sum(alli.total_vacuum_delay_time) end total_vacuum_delay_time, + case when d.policytype = 'r' then (sum(alli.total_autovacuum_delay_time)/d.numsegments) else sum(alli.total_autovacuum_delay_time) end total_autovacuum_delay_time FROM gp_dist_random('pg_stat_all_indexes') alli inner join pg_class c @@ -259,7 +315,7 @@ FROM pg_stat_all_indexes WHERE relid < 16384) m, pg_stat_all_indexes s -WHERE m.relid = s.relid; +WHERE m.relid = s.relid AND m.indexrelid = s.indexrelid; CREATE VIEW gp_stat_sys_indexes_summary AS SELECT * FROM gp_stat_all_indexes_summary @@ -520,3 +576,92 @@ SELECT max(stats_reset) as stats_reset FROM gp_stat_io GROUP BY backend_type, object, context; + +CREATE VIEW gp_stat_vacuum_tables_summary AS +SELECT + s.relid, + s.schema, + s.relname, + s.dbname, + (sum(s.tuples_deleted) / s.divisor)::bigint AS tuples_deleted, + (sum(s.pages_scanned) / s.divisor)::bigint AS pages_scanned, + (sum(s.pages_removed) / s.divisor)::bigint AS pages_removed, + (sum(s.tuples_frozen) / s.divisor)::bigint AS tuples_frozen, + (sum(s.recently_dead_tuples) / s.divisor)::bigint AS recently_dead_tuples, + (sum(s.missed_dead_pages) / s.divisor)::bigint AS missed_dead_pages, + (sum(s.missed_dead_tuples) / s.divisor)::bigint AS missed_dead_tuples, + (sum(s.pages_frozen) / s.divisor)::bigint AS pages_frozen, + (sum(s.pages_all_visible) / s.divisor)::bigint AS pages_all_visible, + (sum(s.freeze_age_vacuum_count) / s.divisor)::bigint AS freeze_age_vacuum_count, + (sum(s.total_file_segs) / s.divisor)::bigint AS total_file_segs, + (sum(s.compacted_segments) / s.divisor)::bigint AS compacted_segments, + (sum(s.tuples_moved) / s.divisor)::bigint AS tuples_moved, + (sum(s.dead_tuples) / s.divisor)::bigint AS dead_tuples, + (sum(s.dead_pages) / s.divisor)::bigint AS dead_pages +FROM ( + SELECT v.*, + CASE WHEN d.policytype = 'r' THEN d.numsegments ELSE 1 END AS divisor + FROM gp_dist_random('pg_stat_vacuum_tables') v + LEFT JOIN gp_distribution_policy d ON d.localoid = v.relid + WHERE v.relid >= 16384 +) s +GROUP BY s.relid, s.schema, s.relname, s.dbname, s.divisor +UNION ALL +SELECT + relid, + schema, + relname, + dbname, + tuples_deleted, + pages_scanned, + pages_removed, + tuples_frozen, + recently_dead_tuples, + missed_dead_pages, + missed_dead_tuples, + pages_frozen, + pages_all_visible, + freeze_age_vacuum_count, + total_file_segs, + compacted_segments, + tuples_moved, + dead_tuples, + dead_pages +FROM pg_stat_vacuum_tables +WHERE relid < 16384; + +COMMENT ON VIEW gp_stat_vacuum_tables_summary IS + 'Vacuum work statistics per table, summed over the cluster'; + +CREATE VIEW gp_stat_vacuum_indexes_summary AS +SELECT + s.indexrelid, + s.schema, + s.indexrelname, + s.dbname, + (sum(s.tuples_deleted) / s.divisor)::bigint AS tuples_deleted, + (sum(s.pages_deleted) / s.divisor)::bigint AS pages_deleted, + (sum(s.dead_pages) / s.divisor)::bigint AS dead_pages +FROM ( + SELECT v.*, + CASE WHEN d.policytype = 'r' THEN d.numsegments ELSE 1 END AS divisor + FROM gp_dist_random('pg_stat_vacuum_indexes') v + JOIN pg_index i ON i.indexrelid = v.indexrelid + LEFT JOIN gp_distribution_policy d ON d.localoid = i.indrelid + WHERE v.indexrelid >= 16384 +) s +GROUP BY s.indexrelid, s.schema, s.indexrelname, s.dbname, s.divisor +UNION ALL +SELECT + indexrelid, + schema, + indexrelname, + dbname, + tuples_deleted, + pages_deleted, + dead_pages +FROM pg_stat_vacuum_indexes +WHERE indexrelid < 16384; + +COMMENT ON VIEW gp_stat_vacuum_indexes_summary IS + 'Vacuum work statistics per index, summed over the cluster'; diff --git a/src/backend/commands/analyze.c b/src/backend/commands/analyze.c index e7c5e68082c..1059586cede 100644 --- a/src/backend/commands/analyze.c +++ b/src/backend/commands/analyze.c @@ -541,7 +541,10 @@ do_analyze_rel(Relation onerel, VacuumParams *params, save_sec_context | SECURITY_RESTRICTED_OPERATION); save_nestlevel = NewGUCNestLevel(); - /* measure elapsed time iff autovacuum logging requires it */ + /* + * When autovacuum logging is used, initialize a resource usage snapshot + * and optionally track I/O timing. + */ if (IsAutoVacuumWorkerProcess() && params->log_min_duration >= 0) { if (track_io_timing) @@ -551,9 +554,11 @@ do_analyze_rel(Relation onerel, VacuumParams *params, } pg_rusage_init(&ru0); - starttime = GetCurrentTimestamp(); } + /* Used for instrumentation and stats report */ + starttime = GetCurrentTimestamp(); + /* * Determine which columns to analyze * @@ -1100,12 +1105,12 @@ do_analyze_rel(Relation onerel, VacuumParams *params, */ if (!inh) { - BlockNumber relallvisible; + BlockNumber relallvisible = 0; + BlockNumber relallfrozen = 0; - if (RelationStorageIsAO(onerel)) - relallvisible = 0; - else - relallvisible = AcquireNumberOfAllVisibleBlocks(onerel); + if (RELKIND_HAS_STORAGE(onerel->rd_rel->relkind) && + !RelationStorageIsAO(onerel)) + AcquireVisibilityMapCounts(onerel, &relallvisible, &relallfrozen); /* * Update pg_class for table relation. CCI first, in case acquirefunc @@ -1116,6 +1121,7 @@ do_analyze_rel(Relation onerel, VacuumParams *params, relpages, totalrows, relallvisible, + relallfrozen, hasindex, InvalidTransactionId, InvalidMultiXactId, @@ -1187,7 +1193,7 @@ do_analyze_rel(Relation onerel, VacuumParams *params, vac_update_relstats(Irel[ind], estimatedIndexPages, totalindexrows, - 0, + 0, 0, false, InvalidTransactionId, InvalidMultiXactId, @@ -1204,7 +1210,7 @@ do_analyze_rel(Relation onerel, VacuumParams *params, */ CommandCounterIncrement(); vac_update_relstats(onerel, -1, totalrows, - 0, hasindex, InvalidTransactionId, + 0, 0, hasindex, InvalidTransactionId, InvalidMultiXactId, NULL, NULL, in_outer_xact, @@ -1222,9 +1228,9 @@ do_analyze_rel(Relation onerel, VacuumParams *params, */ if (!inh) pgstat_report_analyze(onerel, totalrows, totaldeadrows, - (va_cols == NIL)); + (va_cols == NIL), starttime); else if (onerel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE) - pgstat_report_analyze(onerel, 0, 0, (va_cols == NIL)); + pgstat_report_analyze(onerel, 0, 0, (va_cols == NIL), starttime); /* * If this isn't part of VACUUM ANALYZE, let index AMs do cleanup. @@ -1423,7 +1429,7 @@ compute_index_stats(Relation onerel, double totalrows, { HeapTuple heapTuple = rows[rowno]; - vacuum_delay_point(); + vacuum_delay_point(true); /* * Reset the per-tuple context each time, to reclaim any cruft @@ -1843,7 +1849,7 @@ acquire_sample_rows(Relation onerel, int elevel, prefetch_targblock = BlockSampler_Next(&prefetch_bs); #endif - vacuum_delay_point(); + vacuum_delay_point(true); block_accepted = table_scan_analyze_next_block(scan, targblock, vac_strategy); @@ -2428,46 +2434,63 @@ AcquireNumberOfBlocks(Relation onerel) } /* - * Collect visibility map of relation in dispatcher. - * - * In GPDB if we're in the dispatcher, we need to collect the number of - * visibility map in pg_class from segments. + * Collect both visibility-map counts locally or from the segment catalogs. + * Replicated relations contribute one copy, just like relpages. */ -BlockNumber -AcquireNumberOfAllVisibleBlocks(Relation onerel) +void +AcquireVisibilityMapCounts(Relation onerel, BlockNumber *all_visible, + BlockNumber *all_frozen) { - int64 totalvms; - - /* collect total vms from segments in master */ - if (Gp_role == GP_ROLE_DISPATCH && - onerel->rd_cdbpolicy && !GpPolicyIsEntry(onerel->rd_cdbpolicy)) - { - /* Query the segments pg_class. */ - char relvm_sql[80]; - - snprintf(relvm_sql, sizeof(relvm_sql), - "select relallvisible from pg_catalog.pg_class where oid = %u", RelationGetRelid(onerel)); - totalvms = get_size_from_segDBs(relvm_sql); - if (GpPolicyIsReplicated(onerel->rd_cdbpolicy)) - { - /* - * If the distribution of the relation is replicated, we will sum up - * vms much twice which we expecting only once. So we need to divide - * up totalvms by numsegments. - */ - totalvms /= onerel->rd_cdbpolicy->numsegments; - } - - return (BlockNumber)totalvms; - } - /* get vms from local in segment */ - else - { - BlockNumber all_visible = 0; - visibilitymap_count(onerel, &all_visible, NULL); - - return all_visible; - } + if (Gp_role == GP_ROLE_DISPATCH && + onerel->rd_cdbpolicy && !GpPolicyIsEntry(onerel->rd_cdbpolicy)) + { + char relvm_sql[128]; + int64 visible = 0; + int64 frozen = 0; + CdbPgResults cdb_pgresults = {NULL, 0}; + + /* Read both counters from the same catalog row in one dispatch. */ + snprintf(relvm_sql, sizeof(relvm_sql), + "select relallvisible, relallfrozen from pg_catalog.pg_class where oid = %u", + RelationGetRelid(onerel)); + CdbDispatchCommand(relvm_sql, DF_WITH_SNAPSHOT, &cdb_pgresults); + PG_TRY(); + { + for (int i = 0; i < cdb_pgresults.numResults; i++) + { + PGresult *result = cdb_pgresults.pg_results[i]; + ExecStatusType status = PQresultStatus(result); + int ntuples = PQntuples(result); + int nfields = PQnfields(result); + + if (status != PGRES_TUPLES_OK) + elog(ERROR, "unexpected result from segment: %d", status); + if (ntuples != 1 || nfields != 2) + elog(ERROR, "unexpected shape of visibility-map result from segment (%d rows, %d cols)", + ntuples, nfields); + if (PQgetisnull(result, 0, 0) || PQgetisnull(result, 0, 1)) + elog(ERROR, "unexpected NULL visibility-map count from segment"); + + visible += pg_strtoint64(PQgetvalue(result, 0, 0)); + frozen += pg_strtoint64(PQgetvalue(result, 0, 1)); + } + } + PG_FINALLY(); + { + cdbdisp_clearCdbPgResults(&cdb_pgresults); + } + PG_END_TRY(); + + if (GpPolicyIsReplicated(onerel->rd_cdbpolicy)) + { + visible /= onerel->rd_cdbpolicy->numsegments; + frozen /= onerel->rd_cdbpolicy->numsegments; + } + *all_visible = (BlockNumber) visible; + *all_frozen = (BlockNumber) frozen; + } + else + visibilitymap_count(onerel, all_visible, all_frozen); } /* @@ -3471,7 +3494,7 @@ compute_trivial_stats(VacAttrStatsP stats, Datum value; bool isnull; - vacuum_delay_point(); + vacuum_delay_point(true); value = fetchfunc(stats, i, &isnull); @@ -3593,7 +3616,7 @@ compute_distinct_stats(VacAttrStatsP stats, int firstcount1, j; - vacuum_delay_point(); + vacuum_delay_point(true); value = fetchfunc(stats, i, &isnull); @@ -3953,7 +3976,7 @@ compute_scalar_stats(VacAttrStatsP stats, Datum value; bool isnull; - vacuum_delay_point(); + vacuum_delay_point(true); value = fetchfunc(stats, i, &isnull); diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c index a3d2ad87bfb..1c12029334a 100644 --- a/src/backend/commands/cluster.c +++ b/src/backend/commands/cluster.c @@ -1511,6 +1511,7 @@ swap_relation_files(Oid r1, Oid r2, bool target_is_pg_class, int32 swap_pages; float4 swap_tuples; int32 swap_allvisible; + int32 swap_allfrozen; swap_pages = relform1->relpages; relform1->relpages = relform2->relpages; @@ -1523,6 +1524,10 @@ swap_relation_files(Oid r1, Oid r2, bool target_is_pg_class, swap_allvisible = relform1->relallvisible; relform1->relallvisible = relform2->relallvisible; relform2->relallvisible = swap_allvisible; + + swap_allfrozen = relform1->relallfrozen; + relform1->relallfrozen = relform2->relallfrozen; + relform2->relallfrozen = swap_allfrozen; } /* @@ -1728,7 +1733,8 @@ swap_relation_files(Oid r1, Oid r2, bool target_is_pg_class, vac_send_relstats_to_qd(rel, relform1->relpages, relform1->reltuples, - relform1->relallvisible); + relform1->relallvisible, + relform1->relallfrozen); relation_close(rel, AccessShareLock); } diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c index f4ba5615d5a..3470b6bf613 100644 --- a/src/backend/commands/vacuum.c +++ b/src/backend/commands/vacuum.c @@ -47,6 +47,7 @@ #include "catalog/pg_namespace.h" #include "commands/cluster.h" #include "commands/defrem.h" +#include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/vacuum.h" #include "miscadmin.h" @@ -106,6 +107,7 @@ int vacuum_multixact_freeze_min_age; int vacuum_multixact_freeze_table_age; int vacuum_failsafe_age; int vacuum_multixact_failsafe_age; +bool track_cost_delay_timing; /* * Variables for cost-based vacuum delay. The defaults differ between @@ -116,6 +118,16 @@ int vacuum_multixact_failsafe_age; double vacuum_cost_delay = 0; int vacuum_cost_limit = 200; +/* Variable for reporting cost-based vacuum delay from parallel workers. */ +int64 parallel_vacuum_worker_delay_ns = 0; + +/* + * Cumulative cost-based VACUUM delay time (ms) for this process, excluding + * ANALYZE. + * Callers save the starting value and report the delta. + */ +double VacuumDelayTime = 0; + /* * VacuumFailsafeActive is a defined as a global so that we can determine * whether or not to re-enable cost-based vacuum delay when vacuuming a table. @@ -1788,6 +1800,7 @@ void vac_update_relstats(Relation relation, BlockNumber num_pages, double num_tuples, BlockNumber num_all_visible_pages, + BlockNumber num_all_frozen_pages, bool hasindex, TransactionId frozenxid, MultiXactId minmulti, bool *frozenxid_updated, bool *minmulti_updated, @@ -1825,13 +1838,15 @@ vac_update_relstats(Relation relation, num_pages = relation->rd_rel->relpages; num_tuples = relation->rd_rel->reltuples; num_all_visible_pages = relation->rd_rel->relallvisible; + num_all_frozen_pages = relation->rd_rel->relallfrozen; } else if (Gp_role == GP_ROLE_EXECUTE) { vac_send_relstats_to_qd(relation, num_pages, num_tuples, - num_all_visible_pages); + num_all_visible_pages, + num_all_frozen_pages); } } @@ -1911,6 +1926,11 @@ vac_update_relstats(Relation relation, pgcform->relallvisible = (int32) num_all_visible_pages; dirty = true; } + if (pgcform->relallfrozen != (int32) num_all_frozen_pages) + { + pgcform->relallfrozen = (int32) num_all_frozen_pages; + dirty = true; + } elog(DEBUG2, "Vacuum oid=%u pages=%d tuples=%f", relid, pgcform->relpages, pgcform->reltuples); @@ -3179,7 +3199,7 @@ vac_close_indexes(int nindexes, Relation *Irel, LOCKMODE lockmode) * typically once per page processed. */ void -vacuum_delay_point(void) +vacuum_delay_point(bool is_analyze) { double msec = 0; @@ -3222,13 +3242,54 @@ vacuum_delay_point(void) /* Nap if appropriate */ if (msec > 0) { + instr_time delay_start; + if (msec > vacuum_cost_delay * 4) msec = vacuum_cost_delay * 4; + if (track_cost_delay_timing) + INSTR_TIME_SET_CURRENT(delay_start); + pgstat_report_wait_start(WAIT_EVENT_VACUUM_DELAY); pg_usleep(msec * 1000); pgstat_report_wait_end(); + if (track_cost_delay_timing) + { + instr_time delay_end; + instr_time delay; + + INSTR_TIME_SET_CURRENT(delay_end); + INSTR_TIME_SET_ZERO(delay); + INSTR_TIME_ACCUM_DIFF(delay, delay_end, delay_start); + if (!is_analyze) + VacuumDelayTime += INSTR_TIME_GET_MILLISEC(delay); + + /* + * For parallel workers, we only report the delay time every once + * in a while to avoid overloading the leader with messages and + * interrupts. + */ + if (IsParallelWorker()) + { + /* + * Upstream sends the sleep time of a parallel worker to the + * leader's progress entry now and then, which needs + * pgstat_progress_parallel_incr_param() of PostgreSQL 17. + * Cloudberry does not run parallel vacuum, so just + * accumulate it. + */ + Assert(!is_analyze); + parallel_vacuum_worker_delay_ns += INSTR_TIME_GET_NANOSEC(delay); + } + else if (is_analyze) + pgstat_progress_incr_param(PROGRESS_ANALYZE_DELAY_TIME, + INSTR_TIME_GET_NANOSEC(delay)); + else + pgstat_progress_incr_param(PROGRESS_VACUUM_DELAY_TIME, + INSTR_TIME_GET_NANOSEC(delay)); + } + /* * We don't want to ignore postmaster death during very long vacuums * with vacuum_cost_delay configured. We can't use the usual @@ -3563,6 +3624,7 @@ vacuum_combine_stats(VacuumStatsContext *stats_context, CdbPgResults *cdb_pgresu tmp_stats_combo->rel_pages += pgclass_stats_combo->rel_pages; tmp_stats_combo->rel_tuples += pgclass_stats_combo->rel_tuples; tmp_stats_combo->relallvisible += pgclass_stats_combo->relallvisible; + tmp_stats_combo->relallfrozen += pgclass_stats_combo->relallfrozen; /* * Accumulate the number of QEs, assuming sending only once * per QE for each relid in the VACUUM scenario. @@ -3616,6 +3678,7 @@ vac_update_relstats_from_list(VacuumStatsContext *stats_context) stats->rel_pages = stats->rel_pages / rel->rd_cdbpolicy->numsegments; stats->rel_tuples = stats->rel_tuples / rel->rd_cdbpolicy->numsegments; stats->relallvisible = stats->relallvisible / rel->rd_cdbpolicy->numsegments; + stats->relallfrozen = stats->relallfrozen / rel->rd_cdbpolicy->numsegments; } if (RelationIsAppendOptimized(rel)) @@ -3667,6 +3730,7 @@ vac_update_relstats_from_list(VacuumStatsContext *stats_context) vac_update_relstats(rel, stats->rel_pages, stats->rel_tuples, stats->relallvisible, + stats->relallfrozen, rel->rd_rel->relhasindex, InvalidTransactionId, InvalidMultiXactId, @@ -3718,7 +3782,8 @@ void vac_send_relstats_to_qd(Relation relation, BlockNumber num_pages, double num_tuples, - BlockNumber num_all_visible_pages) + BlockNumber num_all_visible_pages, + BlockNumber num_all_frozen_pages) { StringInfoData buf; @@ -3732,6 +3797,7 @@ vac_send_relstats_to_qd(Relation relation, stats.rel_pages = num_pages; stats.rel_tuples = num_tuples; stats.relallvisible = num_all_visible_pages; + stats.relallfrozen = num_all_frozen_pages; pq_sendbyte(&buf, true); /* Mark the result ready when receive this message */ pq_sendint(&buf, PGExtraTypeVacuumStats, sizeof(PGExtraType)); pq_sendint(&buf, sizeof(VPgClassStats), sizeof(int)); @@ -3914,3 +3980,23 @@ vac_cmp_itemptr(const void *left, const void *right) return 0; } +/* Report index work once per call; report remaining dead pages at cleanup. */ +void +vacuum_report_index_stats(Relation indrel, const IndexBulkDeleteResult *istat, + double prev_tuples_removed, + BlockNumber prev_pages_newly_deleted, bool cleanup) +{ + PgStat_VacuumStats stats = {0}; + + if (istat == NULL) + return; + + stats.tuples_deleted = istat->tuples_removed - prev_tuples_removed; + stats.pages_deleted = + istat->pages_newly_deleted >= prev_pages_newly_deleted ? + istat->pages_newly_deleted - prev_pages_newly_deleted : + istat->pages_newly_deleted; + if (cleanup && istat->pages_deleted > istat->pages_free) + stats.dead_pages = istat->pages_deleted - istat->pages_free; + pgstat_report_vacuum_stats(indrel, &stats); +} diff --git a/src/backend/commands/vacuum_ao.c b/src/backend/commands/vacuum_ao.c index 075c73fb1c1..cb7964d8799 100644 --- a/src/backend/commands/vacuum_ao.c +++ b/src/backend/commands/vacuum_ao.c @@ -114,6 +114,8 @@ */ #include "postgres.h" +#include + #include "access/table.h" #include "access/aocs_compaction.h" #include "access/aomd.h" @@ -150,19 +152,30 @@ static void vacuum_appendonly_index(Relation indexRelation, Bitmapset *dead_segs, int elevel, BufferAccessStrategy bstrategy, - AOVacuumRelStats *vacrelstats); + AOVacuumRelStats *vacrelstats, + IndexBulkDeleteResult *result); static bool appendonly_tid_reaped(ItemPointer itemptr, void *state); static void vacuum_appendonly_fill_stats(Relation aorel, Snapshot snapshot, int elevel, BlockNumber *rel_pages, double *rel_tuples, - double *dead_tuples, bool *relhasindex, BlockNumber *total_file_segs); + int64 *dead_tuples, bool *relhasindex, BlockNumber *total_file_segs); static int vacuum_appendonly_indexes(Relation aoRelation, int options, Bitmapset *dead_segs, BufferAccessStrategy bstrategy, AOVacuumRelStats *vacrelstats); static void ao_vacuum_rel_recycle_dead_segments(Relation onerel, VacuumParams *params, BufferAccessStrategy bstrategy, AOVacuumRelStats *vacrelstats); -static AOVacuumRelStats *init_vacrelstats(void); +static AOVacuumRelStats *init_vacrelstats(bool instrument); static void cleanup_vacrelstats(AOVacuumRelStats **vacrelstatsp); +static void ao_report_index_vacuum_time(Relation indrel, TimestampTz starttime, + double startdelaytime); +static void ao_vacuum_error_callback(void *arg); +static void ao_accum_resources(PgStat_CommonCounts *dst, + const PgStat_CommonCounts *src, bool subtract); +static void ao_measure_index_resources(Relation indrel, + LVExtStatCounters *counters, + AOVacuumRelStats *vacrelstats, bool verbose); +static void ao_measure_table_resources(Relation rel, + AOVacuumRelStats *vacrelstats); static void ao_vacuum_rel_pre_cleanup(Relation onerel, VacuumParams *params, BufferAccessStrategy bstrategy, AOVacuumRelStats *vacrelstats) @@ -237,7 +250,7 @@ ao_vacuum_rel_post_cleanup(Relation onerel, VacuumParams *params, BufferAccessSt { BlockNumber relpages; double reltuples; - double deadtuples; + int64 deadtuples; bool relhasindex; /* AO/AOCO total file segment number, use type BlockNumber to * represent same type with num_all_visible_pages in libpq. @@ -294,6 +307,7 @@ ao_vacuum_rel_post_cleanup(Relation onerel, VacuumParams *params, BufferAccessSt total_file_segs, /* AO/AOCO does not currently have an equivalent to Heap's 'all visible pages', use this field to represent AO/AOCO's total segment file count */ + 0, /* relallfrozen: AO has no visibility map */ relhasindex, InvalidTransactionId, InvalidMultiXactId, @@ -302,11 +316,11 @@ ao_vacuum_rel_post_cleanup(Relation onerel, VacuumParams *params, BufferAccessSt false, true /* isvacuum */); - /* report results to the stats collector, too */ - pgstat_report_vacuum(RelationGetRelid(onerel), - onerel->rd_rel->relisshared, - reltuples, - deadtuples); + vacrelstats->total_file_segs = total_file_segs; + + /* Report once all phase timers have been stopped. */ + vacrelstats->live_tuples = reltuples; + vacrelstats->dead_tuples = deadtuples; SIMPLE_FAULT_INJECTOR("vacuum_ao_post_cleanup_end"); } @@ -407,13 +421,15 @@ ao_vacuum_rel_compact(Relation onerel, VacuumParams *params, BufferAccessStrateg } static AOVacuumRelStats * -init_vacrelstats() +init_vacrelstats(bool instrument) { AOVacuumRelStats *vacrelstats; MemoryContext old_context; old_context = MemoryContextSwitchTo(TopMemoryContext); vacrelstats = (AOVacuumRelStats *) palloc0(sizeof(AOVacuumRelStats)); + if (instrument) + vacrelstats->extstats = palloc0(sizeof(AOVacuumExtStats)); MemoryContextSwitchTo(old_context); return vacrelstats; @@ -428,11 +444,32 @@ void ao_vacuum_rel(Relation rel, VacuumParams *params, BufferAccessStrategy bstrategy) { static AOVacuumRelStats *vacrelstats = NULL; + ErrorContextCallback errcallback; + instr_time phase_start; + instr_time phase_end; + double phase_start_delay; + LVExtStatCounters *extcounters; + bool verbose = (params->options & VACOPT_VERBOSE) != 0; + bool extstats = verbose || (set_report_vacuum_hook != NULL); + PgStat_CommonCounts index_start = {0}; Assert(RelationStorageIsAO(rel)); Assert(params != NULL); int ao_vacuum_phase = (params->options & VACUUM_AO_PHASE_MASK); + /* + * The phases of one vacuum share these stats and free them at the end of + * the last phase, so a vacuum that failed in between leaves them behind. + * Drop such leftovers: otherwise the counters of the failed vacuum are + * taken for the next one, whose progress is never reported because the + * progress command is only + * started along with the stats. + */ + if (vacrelstats != NULL && + (ao_vacuum_phase == VACOPT_AO_PRE_CLEANUP_PHASE || + vacrelstats->relid != RelationGetRelid(rel))) + cleanup_vacrelstats(&vacrelstats); + if (!vacrelstats) { if (ao_vacuum_phase != VACOPT_AO_PRE_CLEANUP_PHASE && Gp_role == GP_ROLE_EXECUTE) @@ -449,9 +486,23 @@ ao_vacuum_rel(Relation rel, VacuumParams *params, BufferAccessStrategy bstrategy } pgstat_progress_start_command(PROGRESS_COMMAND_VACUUM, RelationGetRelid(rel)); - vacrelstats = init_vacrelstats(); + vacrelstats = init_vacrelstats(extstats); + vacrelstats->relid = RelationGetRelid(rel); } + /* Count the vacuums of the relation that an error interrupts */ + errcallback.callback = ao_vacuum_error_callback; + errcallback.arg = rel; + errcallback.previous = error_context_stack; + error_context_stack = &errcallback; + + /* Sample the resource usage of the phase for the extended statistics */ + extcounters = extvac_stats_start(rel, verbose); + if (verbose) + index_start = vacrelstats->extstats->indexes; + INSTR_TIME_SET_CURRENT(phase_start); + phase_start_delay = VacuumDelayTime; + /* * Do the actual work --- either FULL or "lazy" vacuum */ @@ -460,14 +511,183 @@ ao_vacuum_rel(Relation rel, VacuumParams *params, BufferAccessStrategy bstrategy else if (ao_vacuum_phase == VACOPT_AO_COMPACT_PHASE) ao_vacuum_rel_compact(rel, params, bstrategy, vacrelstats); else if (ao_vacuum_phase == VACOPT_AO_POST_CLEANUP_PHASE) - { ao_vacuum_rel_post_cleanup(rel, params, bstrategy, vacrelstats); + else + /* Do nothing here, we will launch the stages later */ + Assert(ao_vacuum_phase == 0); + + if (ao_vacuum_phase != 0) + { + INSTR_TIME_SET_CURRENT(phase_end); + INSTR_TIME_SUBTRACT(phase_end, phase_start); + vacrelstats->vacuum_time += INSTR_TIME_GET_MILLISEC(phase_end); + vacrelstats->delay_time += VacuumDelayTime - phase_start_delay; + } + + if (extstats && ao_vacuum_phase != 0) + { + extvac_stats_end(rel, extcounters, &extcounters->report.common); + if (verbose && Gp_role != GP_ROLE_DISPATCH) + { + PgStat_CommonCounts phase_usage = extcounters->report.common; + PgStat_CommonCounts index_usage = vacrelstats->extstats->indexes; + const char *phase_name = + ao_vacuum_phase == VACOPT_AO_PRE_CLEANUP_PHASE ? _("AO pre-cleanup (excluding indexes)") : + ao_vacuum_phase == VACOPT_AO_COMPACT_PHASE ? _("AO compaction (excluding indexes)") : + _("AO post-cleanup (excluding indexes)"); + + /* Only index passes executed during this phase are excluded. */ + ao_accum_resources(&index_usage, &index_start, true); + ao_accum_resources(&phase_usage, &index_usage, true); + extvac_stats_log(rel, phase_name, &phase_usage); + } + ao_accum_resources(&vacrelstats->extstats->phases, + &extcounters->report.common, false); + } + + if (ao_vacuum_phase == VACOPT_AO_POST_CLEANUP_PHASE) + { + PgStat_VacuumStats stats = {0}; + + stats.tuples_deleted = vacrelstats->num_dead_tuples; + stats.dead_tuples = vacrelstats->dead_tuples; + stats.recently_dead_tuples = vacrelstats->dead_tuples; + stats.pages_scanned = vacrelstats->pages_scanned; + stats.total_file_segs = vacrelstats->total_file_segs; + stats.compacted_segments = vacrelstats->compacted_segments; + stats.tuples_moved = vacrelstats->tuples_moved; + stats.pages_removed = vacrelstats->nbytes_truncated / BLCKSZ + + (vacrelstats->nbytes_truncated % BLCKSZ != 0); + pgstat_report_vacuum_stats(rel, &stats); + + /* The last phase: report only the phases executed by this worker. */ + pgstat_report_vacuum_elapsed(RelationGetRelid(rel), + rel->rd_rel->relisshared, + vacrelstats->live_tuples, vacrelstats->dead_tuples, + (PgStat_Counter) rint(vacrelstats->vacuum_time), + (PgStat_Counter) rint(vacrelstats->delay_time), + false); /* AO has no failsafe mode. */ + if (extstats) + ao_measure_table_resources(rel, vacrelstats); pgstat_progress_end_command(); cleanup_vacrelstats(&vacrelstats); } + + if (extcounters != NULL) + pfree(extcounters); + error_context_stack = errcallback.previous; +} + +/* + * Error context callback of an append-optimized vacuum phase. + * + * Like vacuum_error_callback() of the heap, count a vacuum interrupted by an + * actual ERROR, not by a lower-severity report that merely carries this + * context, in vacuum_interrupt_count of the database. We are inside the + * error handler, so pgstat_count_vacuum_error() only bumps a counter. The + * callback adds no context line: the phases report their own progress. + */ +static void +ao_vacuum_error_callback(void *arg) +{ + Relation rel = (Relation) arg; + + if (geterrlevel() == ERROR) + pgstat_count_vacuum_error(rel->rd_rel->relisshared); +} + +/* + * Accumulate one vacuum pass over an index of an append-optimized table + * into the index's cumulative vacuum and delay times, as the heap does in + * lazy_vacuum_one_index() and lazy_cleanup_one_index(). + */ +static void +ao_report_index_vacuum_time(Relation indrel, TimestampTz starttime, + double startdelaytime) +{ + pgstat_report_index_vacuum_time(indrel, + TimestampDifferenceMilliseconds(starttime, + GetCurrentTimestamp()), + (PgStat_Counter) rint(VacuumDelayTime - + startdelaytime), + IsAutoVacuumWorkerProcess()); +} + +/* + * Add the resource usage counters of src to dst, or subtract them from it. + */ +static void +ao_accum_resources(PgStat_CommonCounts *dst, const PgStat_CommonCounts *src, + bool subtract) +{ + if (subtract) + { + dst->total_blks_read -= src->total_blks_read; + dst->total_blks_hit -= src->total_blks_hit; + dst->total_blks_dirtied -= src->total_blks_dirtied; + dst->total_blks_written -= src->total_blks_written; + /* Per-relation block counts do not include the indexes. */ + dst->blk_read_time -= src->blk_read_time; + dst->blk_write_time -= src->blk_write_time; + dst->wal_records -= src->wal_records; + dst->wal_fpi -= src->wal_fpi; + dst->wal_bytes -= src->wal_bytes; + } else - /* Do nothing here, we will launch the stages later */ - Assert(ao_vacuum_phase == 0); + { + dst->total_blks_read += src->total_blks_read; + dst->total_blks_hit += src->total_blks_hit; + dst->total_blks_dirtied += src->total_blks_dirtied; + dst->total_blks_written += src->total_blks_written; + dst->blks_fetched += src->blks_fetched; + dst->blks_hit += src->blks_hit; + dst->blk_read_time += src->blk_read_time; + dst->blk_write_time += src->blk_write_time; + dst->wal_records += src->wal_records; + dst->wal_fpi += src->wal_fpi; + dst->wal_bytes += src->wal_bytes; + } +} + +/* + * Report one pass over an index of an append-optimized table to + * set_report_vacuum_hook, as lazy_vacuum_one_index() does for the heap, and + * remember its resource usage, which the table's report must not count + * again. + */ +static void +ao_measure_index_resources(Relation indrel, LVExtStatCounters *counters, + AOVacuumRelStats *vacrelstats, bool verbose) +{ + PgStat_VacuumRelationCounts *report = &counters->report; + + extvac_stats_end(indrel, counters, &report->common); + if (verbose) + extvac_stats_log(indrel, _("AO index vacuum"), &report->common); + report->type = PGSTAT_EXTVAC_INDEX; + + + ao_accum_resources(&vacrelstats->extstats->indexes, &report->common, false); + pfree(counters); +} + +/* + * Report the vacuum of an append-optimized table, all of its phases, to + * set_report_vacuum_hook. + * + * Work counters are reported separately to native pgstat. The hook carries + * resources and the extension-only awaiting-drop segment snapshot. + */ +static void +ao_measure_table_resources(Relation rel, AOVacuumRelStats *vacrelstats) +{ + PgStat_VacuumRelationCounts report; + + memset(&report, 0, sizeof(report)); + report.type = PGSTAT_EXTVAC_TABLE; + report.common = vacrelstats->extstats->phases; + ao_accum_resources(&report.common, &vacrelstats->extstats->indexes, true); + } /* @@ -544,6 +764,8 @@ vacuum_appendonly_indexes(Relation aoRelation, int options, Bitmapset *dead_segs int i; Relation *Irel; int nindexes; + bool final_cleanup = + (options & VACUUM_AO_PHASE_MASK) == VACOPT_AO_POST_CLEANUP_PHASE; Assert(RelationStorageIsAO(aoRelation)); @@ -574,29 +796,55 @@ vacuum_appendonly_indexes(Relation aoRelation, int options, Bitmapset *dead_segs { for (i = 0; i < nindexes; i++) { + TimestampTz istarttime = GetCurrentTimestamp(); + double startdelaytime = VacuumDelayTime; + LVExtStatCounters *extcounters; + IndexBulkDeleteResult result = {0}; + + extcounters = extvac_stats_start(Irel[i], elevel == INFO); scan_index(Irel[i], aoRelation, elevel, - bstrategy); + bstrategy, + &result); + vacuum_report_index_stats(Irel[i], &result, 0, 0, final_cleanup); + ao_report_index_vacuum_time(Irel[i], istarttime, startdelaytime); + if (extcounters != NULL) + ao_measure_index_resources(Irel[i], extcounters, + vacrelstats, + elevel == INFO && Gp_role != GP_ROLE_DISPATCH); } } else { for (i = 0; i < nindexes; i++) { + TimestampTz istarttime = GetCurrentTimestamp(); + double startdelaytime = VacuumDelayTime; + LVExtStatCounters *extcounters; + IndexBulkDeleteResult result = {0}; + + extcounters = extvac_stats_start(Irel[i], elevel == INFO); vacuum_appendonly_index(Irel[i], aoRelation, dead_segs, elevel, bstrategy, - vacrelstats); + vacrelstats, + &result); + vacuum_report_index_stats(Irel[i], &result, 0, 0, final_cleanup); + ao_report_index_vacuum_time(Irel[i], istarttime, startdelaytime); + if (extcounters != NULL) + ao_measure_index_resources(Irel[i], extcounters, + vacrelstats, + elevel == INFO && Gp_role != GP_ROLE_DISPATCH); } } } vac_close_indexes(nindexes, Irel, NoLock); pgstat_progress_update_param(PROGRESS_VACUUM_PHASE, - ((options & VACUUM_AO_PHASE_MASK) == VACOPT_AO_POST_CLEANUP_PHASE) ? + final_cleanup ? PROGRESS_VACUUM_PHASE_AO_POST_CLEANUP : PROGRESS_VACUUM_PHASE_AO_PRE_CLEANUP); return nindexes; } @@ -613,7 +861,8 @@ vacuum_appendonly_index(Relation indexRelation, Bitmapset *dead_segs, int elevel, BufferAccessStrategy bstrategy, - AOVacuumRelStats *vacrelstats) + AOVacuumRelStats *vacrelstats, + IndexBulkDeleteResult *result) { IndexBulkDeleteResult *stats; IndexVacuumInfo ivinfo = {0}; @@ -651,6 +900,10 @@ vacuum_appendonly_index(Relation indexRelation, if (!stats) return; + /* Hand the counts of the pass to the caller, for the statistics */ + if (result) + *result = *stats; + /* * Now update statistics in pg_class, but only if the index says the count * is accurate. @@ -659,6 +912,7 @@ vacuum_appendonly_index(Relation indexRelation, vac_update_relstats(indexRelation, stats->num_pages, stats->num_index_tuples, 0, /* relallvisible */ + 0, /* relallfrozen */ false, InvalidTransactionId, InvalidMultiXactId, @@ -714,7 +968,7 @@ appendonly_tid_reaped(ItemPointer itemptr, void *state) static void vacuum_appendonly_fill_stats(Relation aorel, Snapshot snapshot, int elevel, BlockNumber *rel_pages, double *rel_tuples, - double *dead_tuples, bool *relhasindex, BlockNumber *total_file_segs) + int64 *dead_tuples, bool *relhasindex, BlockNumber *total_file_segs) { FileSegTotals *fstotal; BlockNumber nblocks; @@ -780,6 +1034,8 @@ vacuum_appendonly_fill_stats(Relation aorel, Snapshot snapshot, int elevel, static void cleanup_vacrelstats(AOVacuumRelStats **vacrelstats) { + if ((*vacrelstats)->extstats != NULL) + pfree((*vacrelstats)->extstats); pfree(*vacrelstats); *vacrelstats = NULL; } @@ -790,7 +1046,8 @@ cleanup_vacrelstats(AOVacuumRelStats **vacrelstats) * We use this when we have no deletions to do. */ void -scan_index(Relation indrel, Relation aorel, int elevel, BufferAccessStrategy vac_strategy) +scan_index(Relation indrel, Relation aorel, int elevel, BufferAccessStrategy vac_strategy, + IndexBulkDeleteResult *result) { IndexBulkDeleteResult *stats; IndexVacuumInfo ivinfo = {0}; @@ -818,6 +1075,10 @@ scan_index(Relation indrel, Relation aorel, int elevel, BufferAccessStrategy vac if (!stats) return; + /* Hand the counts of the pass to the caller, for the statistics */ + if (result) + *result = *stats; + /* * Now update statistics in pg_class, but only if the index says the count * is accurate. @@ -826,6 +1087,7 @@ scan_index(Relation indrel, Relation aorel, int elevel, BufferAccessStrategy vac vac_update_relstats(indrel, stats->num_pages, stats->num_index_tuples, 0, /* relallvisible, don't bother for indexes */ + 0, /* relallfrozen */ false, InvalidTransactionId, InvalidMultiXactId, diff --git a/src/backend/commands/vacuumparallel.c b/src/backend/commands/vacuumparallel.c index a79067fd461..dd7fda5e66c 100644 --- a/src/backend/commands/vacuumparallel.c +++ b/src/backend/commands/vacuumparallel.c @@ -26,6 +26,8 @@ */ #include "postgres.h" +#include + #include "access/amapi.h" #include "access/table.h" #include "access/xact.h" @@ -33,6 +35,7 @@ #include "commands/vacuum.h" #include "optimizer/paths.h" #include "pgstat.h" +#include "postmaster/autovacuum.h" #include "storage/bufmgr.h" #include "tcop/tcopprot.h" #include "utils/lsyscache.h" @@ -64,6 +67,9 @@ typedef struct PVShared Oid relid; int elevel; + /* Leader's autovacuum flag, used for worker timing statistics. */ + bool is_autovacuum; + /* * Fields for both index vacuum and cleanup. * @@ -365,6 +371,7 @@ parallel_vacuum_init(Relation rel, Relation *indrels, int nindexes, shared = (PVShared *) shm_toc_allocate(pcxt->toc, est_shared_len); MemSet(shared, 0, est_shared_len); shared->relid = RelationGetRelid(rel); + shared->is_autovacuum = IsAutoVacuumWorkerProcess(); shared->elevel = elevel; shared->maintenance_work_mem_worker = (nindexes_mwm > 0) ? @@ -836,6 +843,11 @@ parallel_vacuum_process_one_index(ParallelVacuumState *pvs, Relation indrel, IndexBulkDeleteResult *istat = NULL; IndexBulkDeleteResult *istat_res; IndexVacuumInfo ivinfo; + TimestampTz istarttime = GetCurrentTimestamp(); + double startdelaytime = VacuumDelayTime; + double prev_tuples_removed = 0; + BlockNumber prev_pages_newly_deleted = 0; + LVExtStatCounters *extVacCounters; /* * Update the pointer to the corresponding bulk-deletion result if someone @@ -844,6 +856,16 @@ 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; + prev_pages_newly_deleted = istat->pages_newly_deleted; + } + extVacCounters = extvac_stats_start(indrel, pvs->shared->elevel == INFO); ivinfo.index = indrel; ivinfo.heaprel = pvs->heaprel; ivinfo.analyze_only = false; @@ -871,6 +893,37 @@ parallel_vacuum_process_one_index(ParallelVacuumState *pvs, Relation indrel, RelationGetRelationName(indrel)); } + vacuum_report_index_stats(indrel, istat_res, prev_tuples_removed, + prev_pages_newly_deleted, + indstats->status == PARALLEL_INDVAC_STATUS_NEED_CLEANUP); + + if (extVacCounters != NULL) + { + PgStat_VacuumRelationCounts *extVacReport = &extVacCounters->report; + + extvac_stats_end(indrel, extVacCounters, &extVacReport->common); + if (pvs->shared->elevel == INFO) + extvac_stats_log(indrel, + indstats->status == PARALLEL_INDVAC_STATUS_NEED_CLEANUP ? + _("index cleanup") : _("index bulk delete"), + &extVacReport->common); + extVacReport->type = PGSTAT_EXTVAC_INDEX; + pgstat_report_vacuum_ext(indrel, -1, -1, 0, 0, false, extVacReport); + pfree(extVacCounters); + } + + /* + * Accumulate this pass into the index's cumulative vacuum times. Use the + * leader's DSM flag to classify autovacuum: a parallel worker of an + * autovacuum leader is a regular background worker. + */ + pgstat_report_index_vacuum_time(indrel, + TimestampDifferenceMilliseconds(istarttime, + GetCurrentTimestamp()), + (PgStat_Counter) rint(VacuumDelayTime - + startdelaytime), + pvs->shared->is_autovacuum); + /* * 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/tsearch/ts_typanalyze.c b/src/backend/tsearch/ts_typanalyze.c index c24243b11b4..6aa685ed55f 100644 --- a/src/backend/tsearch/ts_typanalyze.c +++ b/src/backend/tsearch/ts_typanalyze.c @@ -206,7 +206,7 @@ compute_tsvector_stats(VacAttrStats *stats, char *lexemesptr; int j; - vacuum_delay_point(); + vacuum_delay_point(true); value = fetchfunc(stats, vector_no, &isnull); diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c index beaeb669266..cea1f90a99c 100644 --- a/src/backend/utils/activity/pgstat.c +++ b/src/backend/utils/activity/pgstat.c @@ -12,7 +12,8 @@ * Statistics are loaded from the filesystem during startup (by the startup * process), unless preceded by a crash, in which case all stats are * discarded. They are written out by the checkpointer process just before - * shutting down, except when shutting down in immediate mode. + * shutting down (if the stats kind allows it), except when shutting down in + * immediate mode. * * Fixed-numbered stats are stored in plain (non-dynamic) shared memory. * @@ -49,8 +50,16 @@ * pgStatPending list. Pending statistics updates are flushed out by * pgstat_report_stat(). * + * It is possible for external modules to define custom statistics kinds, + * that can use the same properties as any built-in stats kinds. Each custom + * stats kind needs to assign a unique ID to ensure that it does not overlap + * with other extensions. In order to reserve a unique stats kind ID, refer + * to https://wiki.postgresql.org/wiki/CustomCumulativeStats. + * * The behavior of different kinds of statistics is determined by the kind's - * entry in pgstat_kind_infos, see PgStat_KindInfo for details. + * entry in pgstat_kind_builtin_infos for all the built-in statistics kinds + * defined, and pgstat_kind_custom_infos for custom kinds registered at + * startup by pgstat_register_kind(). See PgStat_KindInfo for details. * * The consistency of read accesses to statistics can be configured using the * stats_fetch_consistency GUC (see config.sgml and monitoring.sgml for the @@ -134,6 +143,15 @@ #define PGSTAT_SNAPSHOT_HASH_SIZE 512 +/* --------- + * Identifiers in stats file. + * --------- + */ +#define PGSTAT_FILE_ENTRY_END 'E' /* end of file */ +#define PGSTAT_FILE_ENTRY_FIXED 'F' /* fixed-numbered stats entry */ +#define PGSTAT_FILE_ENTRY_NAME 'N' /* stats entry identified by name */ +#define PGSTAT_FILE_ENTRY_HASH 'S' /* stats entry identified by + * PgStat_HashKey */ /* hash table for statistics snapshots entry */ typedef struct PgStat_SnapshotEntry @@ -172,6 +190,8 @@ typedef struct PgStat_SnapshotEntry static void pgstat_write_statsfile(void); static void pgstat_read_statsfile(void); +static void pgstat_init_snapshot_fixed(void); + static void pgstat_reset_after_failure(void); static bool pgstat_flush_pending_entries(bool nowait); @@ -180,7 +200,7 @@ static void pgstat_prep_snapshot(void); static void pgstat_build_snapshot(void); static void pgstat_build_snapshot_fixed(PgStat_Kind kind); -static inline bool pgstat_is_kind_valid(int ikind); +static inline bool pgstat_is_kind_valid(PgStat_Kind kind); /* ---------- @@ -249,7 +269,7 @@ static bool pgstat_is_shutdown = false; /* - * The different kinds of statistics. + * The different kinds of built-in statistics. * * If reasonably possible, handling specific to one kind of stats should go * through this abstraction, rather than making more of pgstat.c aware. @@ -261,7 +281,7 @@ static bool pgstat_is_shutdown = false; * seem to be a great way of doing that, given the split across multiple * files. */ -static const PgStat_KindInfo pgstat_kind_infos[PGSTAT_NUM_KINDS] = { +static const PgStat_KindInfo pgstat_kind_builtin_infos[PGSTAT_KIND_BUILTIN_SIZE] = { /* stats kinds for variable-numbered objects */ @@ -269,6 +289,7 @@ static const PgStat_KindInfo pgstat_kind_infos[PGSTAT_NUM_KINDS] = { .name = "database", .fixed_amount = false, + .write_to_file = true, /* so pg_stat_database entries can be seen in all databases */ .accessed_across_databases = true, @@ -285,6 +306,7 @@ static const PgStat_KindInfo pgstat_kind_infos[PGSTAT_NUM_KINDS] = { .name = "relation", .fixed_amount = false, + .write_to_file = true, .shared_size = sizeof(PgStatShared_Relation), .shared_data_off = offsetof(PgStatShared_Relation, stats), @@ -299,6 +321,7 @@ static const PgStat_KindInfo pgstat_kind_infos[PGSTAT_NUM_KINDS] = { .name = "function", .fixed_amount = false, + .write_to_file = true, .shared_size = sizeof(PgStatShared_Function), .shared_data_off = offsetof(PgStatShared_Function, stats), @@ -312,9 +335,9 @@ static const PgStat_KindInfo pgstat_kind_infos[PGSTAT_NUM_KINDS] = { .name = "replslot", .fixed_amount = false, + .write_to_file = true, .accessed_across_databases = true, - .named_on_disk = true, .shared_size = sizeof(PgStatShared_ReplSlot), .shared_data_off = offsetof(PgStatShared_ReplSlot, stats), @@ -329,6 +352,7 @@ static const PgStat_KindInfo pgstat_kind_infos[PGSTAT_NUM_KINDS] = { .name = "subscription", .fixed_amount = false, + .write_to_file = true, /* so pg_stat_subscription_stats entries can be seen in all databases */ .accessed_across_databases = true, @@ -345,6 +369,7 @@ static const PgStat_KindInfo pgstat_kind_infos[PGSTAT_NUM_KINDS] = { .name = "resqueue", .fixed_amount = false, + .write_to_file = true, /* resource queues are cluster-wide objects, visible across databases */ .accessed_across_databases = true, @@ -364,7 +389,14 @@ static const PgStat_KindInfo pgstat_kind_infos[PGSTAT_NUM_KINDS] = { .name = "archiver", .fixed_amount = true, + .write_to_file = true, + .snapshot_ctl_off = offsetof(PgStat_Snapshot, archiver), + .shared_ctl_off = offsetof(PgStat_ShmemControl, archiver), + .shared_data_off = offsetof(PgStatShared_Archiver, stats), + .shared_data_len = sizeof(((PgStatShared_Archiver *) 0)->stats), + + .init_shmem_cb = pgstat_archiver_init_shmem_cb, .reset_all_cb = pgstat_archiver_reset_all_cb, .snapshot_cb = pgstat_archiver_snapshot_cb, }, @@ -373,7 +405,14 @@ static const PgStat_KindInfo pgstat_kind_infos[PGSTAT_NUM_KINDS] = { .name = "bgwriter", .fixed_amount = true, + .write_to_file = true, + + .snapshot_ctl_off = offsetof(PgStat_Snapshot, bgwriter), + .shared_ctl_off = offsetof(PgStat_ShmemControl, bgwriter), + .shared_data_off = offsetof(PgStatShared_BgWriter, stats), + .shared_data_len = sizeof(((PgStatShared_BgWriter *) 0)->stats), + .init_shmem_cb = pgstat_bgwriter_init_shmem_cb, .reset_all_cb = pgstat_bgwriter_reset_all_cb, .snapshot_cb = pgstat_bgwriter_snapshot_cb, }, @@ -382,7 +421,14 @@ static const PgStat_KindInfo pgstat_kind_infos[PGSTAT_NUM_KINDS] = { .name = "checkpointer", .fixed_amount = true, + .write_to_file = true, + + .snapshot_ctl_off = offsetof(PgStat_Snapshot, checkpointer), + .shared_ctl_off = offsetof(PgStat_ShmemControl, checkpointer), + .shared_data_off = offsetof(PgStatShared_Checkpointer, stats), + .shared_data_len = sizeof(((PgStatShared_Checkpointer *) 0)->stats), + .init_shmem_cb = pgstat_checkpointer_init_shmem_cb, .reset_all_cb = pgstat_checkpointer_reset_all_cb, .snapshot_cb = pgstat_checkpointer_snapshot_cb, }, @@ -391,7 +437,14 @@ static const PgStat_KindInfo pgstat_kind_infos[PGSTAT_NUM_KINDS] = { .name = "io", .fixed_amount = true, + .write_to_file = true, + .snapshot_ctl_off = offsetof(PgStat_Snapshot, io), + .shared_ctl_off = offsetof(PgStat_ShmemControl, io), + .shared_data_off = offsetof(PgStatShared_IO, stats), + .shared_data_len = sizeof(((PgStatShared_IO *) 0)->stats), + + .init_shmem_cb = pgstat_io_init_shmem_cb, .reset_all_cb = pgstat_io_reset_all_cb, .snapshot_cb = pgstat_io_snapshot_cb, }, @@ -400,7 +453,14 @@ static const PgStat_KindInfo pgstat_kind_infos[PGSTAT_NUM_KINDS] = { .name = "slru", .fixed_amount = true, + .write_to_file = true, + + .snapshot_ctl_off = offsetof(PgStat_Snapshot, slru), + .shared_ctl_off = offsetof(PgStat_ShmemControl, slru), + .shared_data_off = offsetof(PgStatShared_SLRU, stats), + .shared_data_len = sizeof(((PgStatShared_SLRU *) 0)->stats), + .init_shmem_cb = pgstat_slru_init_shmem_cb, .reset_all_cb = pgstat_slru_reset_all_cb, .snapshot_cb = pgstat_slru_snapshot_cb, }, @@ -409,12 +469,28 @@ static const PgStat_KindInfo pgstat_kind_infos[PGSTAT_NUM_KINDS] = { .name = "wal", .fixed_amount = true, + .write_to_file = true, + + .snapshot_ctl_off = offsetof(PgStat_Snapshot, wal), + .shared_ctl_off = offsetof(PgStat_ShmemControl, wal), + .shared_data_off = offsetof(PgStatShared_Wal, stats), + .shared_data_len = sizeof(((PgStatShared_Wal *) 0)->stats), + .init_shmem_cb = pgstat_wal_init_shmem_cb, .reset_all_cb = pgstat_wal_reset_all_cb, .snapshot_cb = pgstat_wal_snapshot_cb, }, }; +/* + * Information about custom statistics kinds. + * + * These are saved in a different array than the built-in kinds to save + * in clarity with the initializations. + * + * Indexed by PGSTAT_KIND_CUSTOM_MIN, of size PGSTAT_KIND_CUSTOM_SIZE. + */ +static const PgStat_KindInfo **pgstat_kind_custom_infos = NULL; /* ------------------------------------------------------------ * Functions managing the state of the stats system for all backends. @@ -565,6 +641,8 @@ pgstat_initialize(void) pgstat_init_wal(); + pgstat_init_snapshot_fixed(); + /* Set up a process-exit hook to clean up */ before_shmem_exit(pgstat_shutdown_hook, 0); @@ -807,6 +885,8 @@ pgstat_clear_snapshot(void) memset(&pgStatLocal.snapshot.fixed_valid, 0, sizeof(pgStatLocal.snapshot.fixed_valid)); + memset(&pgStatLocal.snapshot.custom_valid, 0, + sizeof(pgStatLocal.snapshot.custom_valid)); pgStatLocal.snapshot.stats = NULL; pgStatLocal.snapshot.mode = PGSTAT_FETCH_CONSISTENCY_NONE; @@ -973,7 +1053,29 @@ pgstat_snapshot_fixed(PgStat_Kind kind) else pgstat_build_snapshot_fixed(kind); - Assert(pgStatLocal.snapshot.fixed_valid[kind]); + if (pgstat_is_kind_builtin(kind)) + Assert(pgStatLocal.snapshot.fixed_valid[kind]); + else if (pgstat_is_kind_custom(kind)) + Assert(pgStatLocal.snapshot.custom_valid[kind - PGSTAT_KIND_CUSTOM_MIN]); +} + +static void +pgstat_init_snapshot_fixed(void) +{ + /* + * Initialize fixed-numbered statistics data in snapshots, only for custom + * stats kinds. + */ + for (PgStat_Kind kind = PGSTAT_KIND_CUSTOM_MIN; kind <= PGSTAT_KIND_CUSTOM_MAX; kind++) + { + const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind); + + if (!kind_info || !kind_info->fixed_amount) + continue; + + pgStatLocal.snapshot.custom_data[kind - PGSTAT_KIND_CUSTOM_MIN] = + MemoryContextAlloc(TopMemoryContext, kind_info->shared_data_len); + } } static void @@ -1069,10 +1171,12 @@ pgstat_build_snapshot(void) /* * Build snapshot of all fixed-numbered stats. */ - for (int kind = PGSTAT_KIND_FIRST_VALID; kind <= PGSTAT_KIND_LAST; kind++) + for (PgStat_Kind kind = PGSTAT_KIND_MIN; kind <= PGSTAT_KIND_MAX; kind++) { const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind); + if (!kind_info) + continue; if (!kind_info->fixed_amount) { Assert(kind_info->snapshot_cb == NULL); @@ -1089,6 +1193,20 @@ static void pgstat_build_snapshot_fixed(PgStat_Kind kind) { const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind); + int idx; + bool *valid; + + /* Position in fixed_valid or custom_valid */ + if (pgstat_is_kind_builtin(kind)) + { + idx = kind; + valid = pgStatLocal.snapshot.fixed_valid; + } + else + { + idx = kind - PGSTAT_KIND_CUSTOM_MIN; + valid = pgStatLocal.snapshot.custom_valid; + } Assert(kind_info->fixed_amount); Assert(kind_info->snapshot_cb != NULL); @@ -1096,21 +1214,21 @@ pgstat_build_snapshot_fixed(PgStat_Kind kind) if (pgstat_fetch_consistency == PGSTAT_FETCH_CONSISTENCY_NONE) { /* rebuild every time */ - pgStatLocal.snapshot.fixed_valid[kind] = false; + valid[idx] = false; } - else if (pgStatLocal.snapshot.fixed_valid[kind]) + else if (valid[idx]) { /* in snapshot mode we shouldn't get called again */ Assert(pgstat_fetch_consistency == PGSTAT_FETCH_CONSISTENCY_CACHE); return; } - Assert(!pgStatLocal.snapshot.fixed_valid[kind]); + Assert(!valid[idx]); kind_info->snapshot_cb(); - Assert(!pgStatLocal.snapshot.fixed_valid[kind]); - pgStatLocal.snapshot.fixed_valid[kind] = true; + Assert(!valid[idx]); + valid[idx] = true; } @@ -1266,30 +1384,131 @@ pgstat_flush_pending_entries(bool nowait) PgStat_Kind pgstat_get_kind_from_str(char *kind_str) { - for (int kind = PGSTAT_KIND_FIRST_VALID; kind <= PGSTAT_KIND_LAST; kind++) + for (PgStat_Kind kind = PGSTAT_KIND_BUILTIN_MIN; kind <= PGSTAT_KIND_BUILTIN_MAX; kind++) { - if (pg_strcasecmp(kind_str, pgstat_kind_infos[kind].name) == 0) + if (pg_strcasecmp(kind_str, pgstat_kind_builtin_infos[kind].name) == 0) return kind; } + /* Check the custom set of cumulative stats */ + if (pgstat_kind_custom_infos) + { + for (PgStat_Kind kind = PGSTAT_KIND_CUSTOM_MIN; kind <= PGSTAT_KIND_CUSTOM_MAX; kind++) + { + uint32 idx = kind - PGSTAT_KIND_CUSTOM_MIN; + + if (pgstat_kind_custom_infos[idx] && + pg_strcasecmp(kind_str, pgstat_kind_custom_infos[idx]->name) == 0) + return kind; + } + } + ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("invalid statistics kind: \"%s\"", kind_str))); - return PGSTAT_KIND_DATABASE; /* avoid compiler warnings */ + return PGSTAT_KIND_INVALID; /* avoid compiler warnings */ } static inline bool -pgstat_is_kind_valid(int ikind) +pgstat_is_kind_valid(PgStat_Kind kind) { - return ikind >= PGSTAT_KIND_FIRST_VALID && ikind <= PGSTAT_KIND_LAST; + return pgstat_is_kind_builtin(kind) || pgstat_is_kind_custom(kind); } const PgStat_KindInfo * pgstat_get_kind_info(PgStat_Kind kind) { - Assert(pgstat_is_kind_valid(kind)); + if (pgstat_is_kind_builtin(kind)) + return &pgstat_kind_builtin_infos[kind]; - return &pgstat_kind_infos[kind]; + if (pgstat_is_kind_custom(kind)) + { + uint32 idx = kind - PGSTAT_KIND_CUSTOM_MIN; + + if (pgstat_kind_custom_infos == NULL || + pgstat_kind_custom_infos[idx] == NULL) + return NULL; + return pgstat_kind_custom_infos[idx]; + } + + return NULL; +} + +/* + * Register a new stats kind. + * + * PgStat_Kinds must be globally unique across all extensions. Refer + * to https://wiki.postgresql.org/wiki/CustomCumulativeStats to reserve a + * unique ID for your extension, to avoid conflicts with other extension + * developers. During development, use PGSTAT_KIND_EXPERIMENTAL to avoid + * needlessly reserving a new ID. + */ +void +pgstat_register_kind(PgStat_Kind kind, const PgStat_KindInfo *kind_info) +{ + uint32 idx = kind - PGSTAT_KIND_CUSTOM_MIN; + + if (kind_info->name == NULL || strlen(kind_info->name) == 0) + ereport(ERROR, + (errmsg("custom cumulative statistics name is invalid"), + errhint("Provide a non-empty name for the custom cumulative statistics."))); + + if (!pgstat_is_kind_custom(kind)) + ereport(ERROR, (errmsg("custom cumulative statistics ID %u is out of range", kind), + errhint("Provide a custom cumulative statistics ID between %u and %u.", + PGSTAT_KIND_CUSTOM_MIN, PGSTAT_KIND_CUSTOM_MAX))); + + if (!process_shared_preload_libraries_in_progress) + ereport(ERROR, + (errmsg("failed to register custom cumulative statistics \"%s\" with ID %u", kind_info->name, kind), + errdetail("Custom cumulative statistics must be registered while initializing modules in \"shared_preload_libraries\"."))); + + /* + * Check some data for fixed-numbered stats. + */ + if (kind_info->fixed_amount) + { + if (kind_info->shared_size == 0) + ereport(ERROR, + (errmsg("custom cumulative statistics property is invalid"), + errhint("Custom cumulative statistics require a shared memory size for fixed-numbered objects."))); + } + + /* + * If pgstat_kind_custom_infos is not available yet, allocate it. + */ + if (pgstat_kind_custom_infos == NULL) + { + pgstat_kind_custom_infos = (const PgStat_KindInfo **) + MemoryContextAllocZero(TopMemoryContext, + sizeof(PgStat_KindInfo *) * PGSTAT_KIND_CUSTOM_SIZE); + } + + if (pgstat_kind_custom_infos[idx] != NULL && + pgstat_kind_custom_infos[idx]->name != NULL) + ereport(ERROR, + (errmsg("failed to register custom cumulative statistics \"%s\" with ID %u", kind_info->name, kind), + errdetail("Custom cumulative statistics \"%s\" already registered with the same ID.", + pgstat_kind_custom_infos[idx]->name))); + + /* check for existing custom stats with the same name */ + for (PgStat_Kind existing_kind = PGSTAT_KIND_CUSTOM_MIN; existing_kind <= PGSTAT_KIND_CUSTOM_MAX; existing_kind++) + { + uint32 existing_idx = existing_kind - PGSTAT_KIND_CUSTOM_MIN; + + if (pgstat_kind_custom_infos[existing_idx] == NULL) + continue; + if (!pg_strcasecmp(pgstat_kind_custom_infos[existing_idx]->name, kind_info->name)) + ereport(ERROR, + (errmsg("failed to register custom cumulative statistics \"%s\" with ID %u", kind_info->name, kind), + errdetail("Existing cumulative statistics with ID %u has the same name.", existing_kind))); + } + + /* Register it */ + pgstat_kind_custom_infos[idx] = kind_info; + ereport(LOG, + (errmsg("registered custom cumulative statistics \"%s\" with ID %u", + kind_info->name, kind))); } /* @@ -1365,47 +1584,32 @@ pgstat_write_statsfile(void) format_id = PGSTAT_FILE_FORMAT_ID; write_chunk_s(fpout, &format_id); - /* - * XXX: The following could now be generalized to just iterate over - * pgstat_kind_infos instead of knowing about the different kinds of - * stats. - */ - - /* - * Write archiver stats struct - */ - pgstat_build_snapshot_fixed(PGSTAT_KIND_ARCHIVER); - write_chunk_s(fpout, &pgStatLocal.snapshot.archiver); + /* Write various stats structs for fixed number of objects */ + for (PgStat_Kind kind = PGSTAT_KIND_MIN; kind <= PGSTAT_KIND_MAX; kind++) + { + char *ptr; + const PgStat_KindInfo *info = pgstat_get_kind_info(kind); - /* - * Write bgwriter stats struct - */ - pgstat_build_snapshot_fixed(PGSTAT_KIND_BGWRITER); - write_chunk_s(fpout, &pgStatLocal.snapshot.bgwriter); + if (!info || !info->fixed_amount) + continue; - /* - * Write checkpointer stats struct - */ - pgstat_build_snapshot_fixed(PGSTAT_KIND_CHECKPOINTER); - write_chunk_s(fpout, &pgStatLocal.snapshot.checkpointer); + if (pgstat_is_kind_builtin(kind)) + Assert(info->snapshot_ctl_off != 0); - /* - * Write IO stats struct - */ - pgstat_build_snapshot_fixed(PGSTAT_KIND_IO); - write_chunk_s(fpout, &pgStatLocal.snapshot.io); + /* skip if no need to write to file */ + if (!info->write_to_file) + continue; - /* - * Write SLRU stats struct - */ - pgstat_build_snapshot_fixed(PGSTAT_KIND_SLRU); - write_chunk_s(fpout, &pgStatLocal.snapshot.slru); + pgstat_build_snapshot_fixed(kind); + if (pgstat_is_kind_builtin(kind)) + ptr = ((char *) &pgStatLocal.snapshot) + info->snapshot_ctl_off; + else + ptr = pgStatLocal.snapshot.custom_data[kind - PGSTAT_KIND_CUSTOM_MIN]; - /* - * Write WAL stats struct - */ - pgstat_build_snapshot_fixed(PGSTAT_KIND_WAL); - write_chunk_s(fpout, &pgStatLocal.snapshot.wal); + fputc(PGSTAT_FILE_ENTRY_FIXED, fpout); + write_chunk_s(fpout, &kind); + write_chunk(fpout, ptr, info->shared_data_len); + } /* * Walk through the stats entries @@ -1431,6 +1635,17 @@ pgstat_write_statsfile(void) if (ps->dropped) continue; + /* + * This discards data related to custom stats kinds that are unknown + * to this process. + */ + if (!pgstat_is_kind_valid(ps->key.kind)) + { + elog(WARNING, "found unknown stats entry %u/%u/%u", + ps->key.kind, ps->key.dboid, ps->key.objoid); + continue; + } + shstats = (PgStatShared_Common *) dsa_get_address(pgStatLocal.dsa, ps->body); kind_info = pgstat_get_kind_info(ps->key.kind); @@ -1438,10 +1653,14 @@ pgstat_write_statsfile(void) /* if not dropped the valid-entry refcount should exist */ Assert(pg_atomic_read_u32(&ps->refcount) > 0); + /* skip if no need to write to file */ + if (!kind_info->write_to_file) + continue; + if (!kind_info->to_serialized_name) { /* normal stats entry, identified by PgStat_HashKey */ - fputc('S', fpout); + fputc(PGSTAT_FILE_ENTRY_HASH, fpout); write_chunk_s(fpout, &ps->key); } else @@ -1451,7 +1670,7 @@ pgstat_write_statsfile(void) kind_info->to_serialized_name(&ps->key, shstats, &name); - fputc('N', fpout); + fputc(PGSTAT_FILE_ENTRY_NAME, fpout); write_chunk_s(fpout, &ps->key.kind); write_chunk_s(fpout, &name); } @@ -1468,7 +1687,7 @@ pgstat_write_statsfile(void) * pgstat.stat with it. The ferror() check replaces testing for error * after each individual fputc or fwrite (in write_chunk()) above. */ - fputc('E', fpout); + fputc(PGSTAT_FILE_ENTRY_END, fpout); if (ferror(fpout)) { @@ -1549,55 +1768,22 @@ pgstat_read_statsfile(void) /* * Verify it's of the expected format. */ - if (!read_chunk_s(fpin, &format_id) || - format_id != PGSTAT_FILE_FORMAT_ID) - goto error; - - /* - * XXX: The following could now be generalized to just iterate over - * pgstat_kind_infos instead of knowing about the different kinds of - * stats. - */ - - /* - * Read archiver stats struct - */ - if (!read_chunk_s(fpin, &shmem->archiver.stats)) - goto error; - - /* - * Read bgwriter stats struct - */ - if (!read_chunk_s(fpin, &shmem->bgwriter.stats)) - goto error; - - /* - * Read checkpointer stats struct - */ - if (!read_chunk_s(fpin, &shmem->checkpointer.stats)) - goto error; - - /* - * Read IO stats struct - */ - if (!read_chunk_s(fpin, &shmem->io.stats)) - goto error; - - /* - * Read SLRU stats struct - */ - if (!read_chunk_s(fpin, &shmem->slru.stats)) + if (!read_chunk_s(fpin, &format_id)) + { + elog(WARNING, "could not read format ID"); goto error; + } - /* - * Read WAL stats struct - */ - if (!read_chunk_s(fpin, &shmem->wal.stats)) + if (format_id != PGSTAT_FILE_FORMAT_ID) + { + elog(WARNING, "found incorrect format ID %d (expected %d)", + format_id, PGSTAT_FILE_FORMAT_ID); goto error; + } /* - * We found an existing statistics file. Read it and put all the hash - * table entries into place. + * We found an existing statistics file. Read it and put all the stats + * data into place. */ for (;;) { @@ -1605,8 +1791,64 @@ pgstat_read_statsfile(void) switch (t) { - case 'S': - case 'N': + case PGSTAT_FILE_ENTRY_FIXED: + { + PgStat_Kind kind; + const PgStat_KindInfo *info; + char *ptr; + + /* entry for fixed-numbered stats */ + if (!read_chunk_s(fpin, &kind)) + { + elog(WARNING, "could not read stats kind for entry of type %c", t); + goto error; + } + + if (!pgstat_is_kind_valid(kind)) + { + elog(WARNING, "invalid stats kind %u for entry of type %c", + kind, t); + goto error; + } + + info = pgstat_get_kind_info(kind); + if (!info) + { + elog(WARNING, "could not find information of kind %u for entry of type %c", + kind, t); + goto error; + } + + if (!info->fixed_amount) + { + elog(WARNING, "invalid fixed_amount in stats kind %u for entry of type %c", + kind, t); + goto error; + } + + /* Load back stats into shared memory */ + if (pgstat_is_kind_builtin(kind)) + ptr = ((char *) shmem) + info->shared_ctl_off + + info->shared_data_off; + else + { + int idx = kind - PGSTAT_KIND_CUSTOM_MIN; + + ptr = ((char *) shmem->custom_data[idx]) + + info->shared_data_off; + } + + if (!read_chunk(fpin, ptr, info->shared_data_len)) + { + elog(WARNING, "could not read data of stats kind %u for entry of type %c with size %u", + kind, t, info->shared_data_len); + goto error; + } + + break; + } + case PGSTAT_FILE_ENTRY_HASH: + case PGSTAT_FILE_ENTRY_NAME: { PgStat_HashKey key; PgStatShared_HashEntry *p; @@ -1614,14 +1856,21 @@ pgstat_read_statsfile(void) CHECK_FOR_INTERRUPTS(); - if (t == 'S') + if (t == PGSTAT_FILE_ENTRY_HASH) { /* normal stats entry, identified by PgStat_HashKey */ if (!read_chunk_s(fpin, &key)) + { + elog(WARNING, "could not read key for entry of type %c", t); goto error; + } if (!pgstat_is_kind_valid(key.kind)) + { + elog(WARNING, "invalid stats kind for entry %u/%u/%u of type %c", + key.kind, key.dboid, key.objoid, t); goto error; + } } else { @@ -1631,22 +1880,47 @@ pgstat_read_statsfile(void) NameData name; if (!read_chunk_s(fpin, &kind)) + { + elog(WARNING, "could not read stats kind for entry of type %c", t); goto error; + } if (!read_chunk_s(fpin, &name)) + { + elog(WARNING, "could not read name of stats kind %u for entry of type %c", + kind, t); goto error; + } if (!pgstat_is_kind_valid(kind)) + { + elog(WARNING, "invalid stats kind %u for entry of type %c", + kind, t); goto error; + } kind_info = pgstat_get_kind_info(kind); + if (!kind_info) + { + elog(WARNING, "could not find information of kind %u for entry of type %c", + kind, t); + goto error; + } if (!kind_info->from_serialized_name) + { + elog(WARNING, "invalid from_serialized_name in stats kind %u for entry of type %c", + kind, t); goto error; + } if (!kind_info->from_serialized_name(&name, &key)) { /* skip over data for entry we don't care about */ if (fseek(fpin, pgstat_get_entry_len(kind), SEEK_CUR) != 0) + { + elog(WARNING, "could not seek \"%s\" of stats kind %u for entry of type %c", + NameStr(name), kind, t); goto error; + } continue; } @@ -1665,8 +1939,8 @@ pgstat_read_statsfile(void) if (found) { dshash_release_lock(pgStatLocal.shared_hash, p); - elog(WARNING, "found duplicate stats entry %d/%u/%u", - key.kind, key.dboid, key.objoid); + elog(WARNING, "found duplicate stats entry %u/%u/%u of type %c", + key.kind, key.dboid, key.objoid, t); goto error; } @@ -1676,18 +1950,30 @@ pgstat_read_statsfile(void) if (!read_chunk(fpin, pgstat_get_entry_data(key.kind, header), pgstat_get_entry_len(key.kind))) + { + elog(WARNING, "could not read data for entry %u/%u/%u of type %c", + key.kind, key.dboid, key.objoid, t); goto error; + } break; } - case 'E': - /* check that 'E' actually signals end of file */ + case PGSTAT_FILE_ENTRY_END: + + /* + * check that PGSTAT_FILE_ENTRY_END actually signals end of + * file + */ if (fgetc(fpin) != EOF) + { + elog(WARNING, "could not read end-of-file"); goto error; + } goto done; default: + elog(WARNING, "could not read entry of type %c", t); goto error; } } @@ -1719,11 +2005,11 @@ pgstat_reset_after_failure(void) TimestampTz ts = GetCurrentTimestamp(); /* reset fixed-numbered stats */ - for (int kind = PGSTAT_KIND_FIRST_VALID; kind <= PGSTAT_KIND_LAST; kind++) + for (PgStat_Kind kind = PGSTAT_KIND_MIN; kind <= PGSTAT_KIND_MAX; kind++) { const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind); - if (!kind_info->fixed_amount) + if (!kind_info || !kind_info->fixed_amount) continue; kind_info->reset_all_cb(ts); diff --git a/src/backend/utils/activity/pgstat_archiver.c b/src/backend/utils/activity/pgstat_archiver.c index f6af36e60cd..e972b3aeb8b 100644 --- a/src/backend/utils/activity/pgstat_archiver.c +++ b/src/backend/utils/activity/pgstat_archiver.c @@ -62,6 +62,14 @@ pgstat_fetch_stat_archiver(void) return &pgStatLocal.snapshot.archiver; } +void +pgstat_archiver_init_shmem_cb(void *stats) +{ + PgStatShared_Archiver *stats_shmem = (PgStatShared_Archiver *) stats; + + LWLockInitialize(&stats_shmem->lock, LWTRANCHE_PGSTATS_DATA); +} + void pgstat_archiver_reset_all_cb(TimestampTz ts) { diff --git a/src/backend/utils/activity/pgstat_bgwriter.c b/src/backend/utils/activity/pgstat_bgwriter.c index 92be384b0d0..76225097f7b 100644 --- a/src/backend/utils/activity/pgstat_bgwriter.c +++ b/src/backend/utils/activity/pgstat_bgwriter.c @@ -75,6 +75,14 @@ pgstat_fetch_stat_bgwriter(void) return &pgStatLocal.snapshot.bgwriter; } +void +pgstat_bgwriter_init_shmem_cb(void *stats) +{ + PgStatShared_BgWriter *stats_shmem = (PgStatShared_BgWriter *) stats; + + LWLockInitialize(&stats_shmem->lock, LWTRANCHE_PGSTATS_DATA); +} + void pgstat_bgwriter_reset_all_cb(TimestampTz ts) { diff --git a/src/backend/utils/activity/pgstat_checkpointer.c b/src/backend/utils/activity/pgstat_checkpointer.c index 26dec112f6c..6628bcce307 100644 --- a/src/backend/utils/activity/pgstat_checkpointer.c +++ b/src/backend/utils/activity/pgstat_checkpointer.c @@ -83,6 +83,14 @@ pgstat_fetch_stat_checkpointer(void) return &pgStatLocal.snapshot.checkpointer; } +void +pgstat_checkpointer_init_shmem_cb(void *stats) +{ + PgStatShared_Checkpointer *stats_shmem = (PgStatShared_Checkpointer *) stats; + + LWLockInitialize(&stats_shmem->lock, LWTRANCHE_PGSTATS_DATA); +} + void pgstat_checkpointer_reset_all_cb(TimestampTz ts) { diff --git a/src/backend/utils/activity/pgstat_database.c b/src/backend/utils/activity/pgstat_database.c index 813aa359fa0..b0976740fb4 100644 --- a/src/backend/utils/activity/pgstat_database.c +++ b/src/backend/utils/activity/pgstat_database.c @@ -36,6 +36,13 @@ static int pgStatXactCommit = 0; static int pgStatXactRollback = 0; static PgStat_Counter pgLastSessionReportTime = 0; +/* + * Vacuums interrupted by an error, counted from the vacuum error callback + * and folded into the pending database entries at transaction end. + */ +static PgStat_Counter pgStatVacuumErrors = 0; +static PgStat_Counter pgStatSharedVacuumErrors = 0; + /* * Remove entry for the database being dropped. @@ -245,6 +252,20 @@ pgstat_fetch_stat_dbentry(Oid dboid) pgstat_fetch_entry(PGSTAT_KIND_DATABASE, dboid, InvalidOid); } +/* + * Add the vacuum errors counted for a database to its pending entry, and + * reset the counter. + */ +static void +pgstat_fold_vacuum_errors(Oid dboid, PgStat_Counter *count) +{ + if (*count == 0) + return; + + pgstat_prep_database_pending(dboid)->vacuum_interrupt_count += *count; + *count = 0; +} + void AtEOXact_PgStat_Database(bool isCommit, bool parallel) { @@ -260,6 +281,15 @@ AtEOXact_PgStat_Database(bool isCommit, bool parallel) else pgStatXactRollback++; } + + /* + * Fold in the vacuums that pgstat_count_vacuum_error() saw failing. We + * are past LWLockReleaseAll() of the aborting transaction here, so it is + * safe to touch the pending entries, which the next pgstat_report_stat() + * flushes to shared memory as usual. + */ + pgstat_fold_vacuum_errors(MyDatabaseId, &pgStatVacuumErrors); + pgstat_fold_vacuum_errors(InvalidOid, &pgStatSharedVacuumErrors); } /* @@ -326,6 +356,31 @@ pgstat_should_report_connstat(void) return MyBackendType == B_BACKEND; } +/* + * Count a vacuum that was interrupted by an error. + * + * This is called from the vacuum error callback, that is from inside the + * error handler, so it must not do anything that could fail or block: the + * error may well have been raised by the locking code itself, possibly + * while holding an LWLock. Just bump a counter here; the counter reaches + * the pending database entry in AtEOXact_PgStat_Database(), once the + * transaction end has released the locks we might have been holding. + * + * Vacuums of shared relations are counted in the InvalidOid entry, matching + * how their other stats are accounted. + */ +void +pgstat_count_vacuum_error(bool shared) +{ + if (!pgstat_track_counts) + return; + + if (shared) + pgStatSharedVacuumErrors++; + else + pgStatVacuumErrors++; +} + /* * Find or create a local PgStat_StatDBEntry entry for dboid. */ @@ -417,6 +472,26 @@ pgstat_database_flush_cb(PgStat_EntryRef *entry_ref, bool nowait) PGSTAT_ACCUM_DBCOUNT(blk_read_time); PGSTAT_ACCUM_DBCOUNT(blk_write_time); + PGSTAT_ACCUM_DBCOUNT(total_vacuum_time); + PGSTAT_ACCUM_DBCOUNT(total_autovacuum_time); + PGSTAT_ACCUM_DBCOUNT(total_vacuum_delay_time); + PGSTAT_ACCUM_DBCOUNT(total_autovacuum_delay_time); + PGSTAT_ACCUM_DBCOUNT(vacuum_failsafe_count); + PGSTAT_ACCUM_DBCOUNT(vacuum_interrupt_count); + PGSTAT_ACCUM_DBCOUNT(vacuum_stats.tuples_deleted); + PGSTAT_ACCUM_DBCOUNT(vacuum_stats.pages_deleted); + PGSTAT_ACCUM_DBCOUNT(vacuum_stats.dead_pages); + PGSTAT_ACCUM_DBCOUNT(vacuum_stats.pages_frozen); + PGSTAT_ACCUM_DBCOUNT(vacuum_stats.pages_all_visible); + PGSTAT_ACCUM_DBCOUNT(vacuum_stats.freeze_age_vacuum_count); + PGSTAT_ACCUM_DBCOUNT(vacuum_stats.tuples_frozen); + PGSTAT_ACCUM_DBCOUNT(vacuum_stats.recently_dead_tuples); + PGSTAT_ACCUM_DBCOUNT(vacuum_stats.missed_dead_tuples); + PGSTAT_ACCUM_DBCOUNT(vacuum_stats.pages_scanned); + PGSTAT_ACCUM_DBCOUNT(vacuum_stats.pages_removed); + PGSTAT_ACCUM_DBCOUNT(vacuum_stats.missed_dead_pages); + PGSTAT_ACCUM_DBCOUNT(vacuum_stats.compacted_segments); + PGSTAT_ACCUM_DBCOUNT(vacuum_stats.tuples_moved); PGSTAT_ACCUM_DBCOUNT(sessions); PGSTAT_ACCUM_DBCOUNT(session_time); diff --git a/src/backend/utils/activity/pgstat_io.c b/src/backend/utils/activity/pgstat_io.c index 4fd4d95291b..6ffcf4771b2 100644 --- a/src/backend/utils/activity/pgstat_io.c +++ b/src/backend/utils/activity/pgstat_io.c @@ -237,6 +237,15 @@ pgstat_get_io_object_name(IOObject io_object) pg_unreachable(); } +void +pgstat_io_init_shmem_cb(void *stats) +{ + PgStatShared_IO *stat_shmem = (PgStatShared_IO *) stats; + + for (int i = 0; i < BACKEND_NUM_TYPES; i++) + LWLockInitialize(&stat_shmem->locks[i], LWTRANCHE_PGSTATS_DATA); +} + void pgstat_io_reset_all_cb(TimestampTz ts) { diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c index 45bbf9abcb7..8d6b8ea9057 100644 --- a/src/backend/utils/activity/pgstat_relation.c +++ b/src/backend/utils/activity/pgstat_relation.c @@ -215,7 +215,22 @@ pgstat_drop_relation(Relation rel) */ void pgstat_report_vacuum(Oid tableoid, bool shared, - PgStat_Counter livetuples, PgStat_Counter deadtuples) + PgStat_Counter livetuples, PgStat_Counter deadtuples, + TimestampTz starttime, PgStat_Counter delaytime, + bool failsafe) +{ + pgstat_report_vacuum_elapsed(tableoid, shared, livetuples, deadtuples, + TimestampDifferenceMilliseconds(starttime, + GetCurrentTimestamp()), + delaytime, failsafe); +} + +/* Report a completed vacuum whose active phases were timed separately (ms). */ +void +pgstat_report_vacuum_elapsed(Oid tableoid, bool shared, + PgStat_Counter livetuples, PgStat_Counter deadtuples, + PgStat_Counter elapsedtime, PgStat_Counter delaytime, + bool failsafe) { PgStat_EntryRef *entry_ref; PgStatShared_Relation *shtabentry; @@ -255,15 +270,46 @@ pgstat_report_vacuum(Oid tableoid, bool shared, { tabentry->last_autovacuum_time = ts; tabentry->autovacuum_count++; + tabentry->total_autovacuum_time += elapsedtime; + tabentry->total_autovacuum_delay_time += delaytime; } else { tabentry->last_vacuum_time = ts; tabentry->vacuum_count++; + tabentry->total_vacuum_time += elapsedtime; + tabentry->total_vacuum_delay_time += delaytime; } + if (failsafe) + tabentry->vacuum_failsafe_count++; + pgstat_unlock_entry(entry_ref); + /* + * Accumulate the same times into the database-wide totals. Index + * processing happens inside the table's run, so per-index times reported + * via pgstat_report_index_vacuum_time() are not added here again. The + * database entry is stored in microseconds, as its other time counters. + */ + { + PgStat_StatDBEntry *dbentry = pgstat_prep_database_pending(dboid); + + if (IsAutoVacuumWorkerProcess()) + { + dbentry->total_autovacuum_time += elapsedtime * 1000; + dbentry->total_autovacuum_delay_time += delaytime * 1000; + } + else + { + dbentry->total_vacuum_time += elapsedtime * 1000; + dbentry->total_vacuum_delay_time += delaytime * 1000; + } + + if (failsafe) + dbentry->vacuum_failsafe_count++; + } + /* * Flush IO statistics now. pgstat_report_stat() will flush IO stats, * however this will not be called until after an entire autovacuum cycle @@ -273,6 +319,140 @@ pgstat_report_vacuum(Oid tableoid, bool shared, pgstat_flush_io(false); } +/* + * Report the time spent vacuuming an index. + * + * Vacuum may process an index several times: a bulkdelete pass per index + * scan cycle plus a final cleanup pass, possibly spread across parallel + * workers. Each pass adds its elapsed and delay time here, accumulating + * into the index's total_vacuum_time or total_autovacuum_time, mirroring + * the table-level counters. The caller says whether this is autovacuum: + * parallel workers of an autovacuum leader are regular background workers, + * so IsAutoVacuumWorkerProcess() cannot be relied upon here. + */ +void +pgstat_report_index_vacuum_time(Relation rel, PgStat_Counter elapsedtime, + PgStat_Counter delaytime, bool is_autovacuum) +{ + PgStat_EntryRef *entry_ref; + PgStatShared_Relation *shtabentry; + PgStat_StatTabEntry *tabentry; + Oid dboid = (rel->rd_rel->relisshared ? InvalidOid : MyDatabaseId); + + if (!pgstat_track_counts) + return; + + /* block acquiring lock for the same reason as pgstat_report_autovac() */ + entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION, dboid, + RelationGetRelid(rel), false); + shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats; + tabentry = &shtabentry->stats; + + if (is_autovacuum) + { + tabentry->total_autovacuum_time += elapsedtime; + tabentry->total_autovacuum_delay_time += delaytime; + } + else + { + tabentry->total_vacuum_time += elapsedtime; + tabentry->total_vacuum_delay_time += delaytime; + } + + pgstat_unlock_entry(entry_ref); +} + +/* + * Persist the work measured by VACUUM independently of any extension hook. + * Index calls report deltas, while a table reports once on completion. The + * database aggregate describes table work only. Use the regular relation + * kind so snapshots, resets, drop handling and persistence work as usual. + */ +void +pgstat_report_vacuum_stats(Relation rel, const PgStat_VacuumStats *stats) +{ + Oid dboid = rel->rd_rel->relisshared ? InvalidOid : MyDatabaseId; + PgStat_EntryRef *entry_ref; + PgStatShared_Relation *shared; + PgStat_VacuumStats *dst; + + if (!pgstat_track_counts) + return; + + entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION, dboid, + RelationGetRelid(rel), false); + shared = (PgStatShared_Relation *) entry_ref->shared_stats; + dst = &shared->stats.vacuum_stats; + dst->tuples_deleted += stats->tuples_deleted; + dst->pages_deleted += stats->pages_deleted; + dst->dead_pages += stats->dead_pages; + dst->pages_frozen += stats->pages_frozen; + dst->pages_all_visible += stats->pages_all_visible; + dst->freeze_age_vacuum_count += stats->freeze_age_vacuum_count; + dst->tuples_frozen += stats->tuples_frozen; + dst->recently_dead_tuples += stats->recently_dead_tuples; + dst->missed_dead_tuples += stats->missed_dead_tuples; + dst->pages_scanned += stats->pages_scanned; + dst->pages_removed += stats->pages_removed; + dst->missed_dead_pages += stats->missed_dead_pages; + dst->compacted_segments += stats->compacted_segments; + dst->tuples_moved += stats->tuples_moved; + dst->dead_tuples = stats->dead_tuples; + dst->total_file_segs = stats->total_file_segs; + pgstat_unlock_entry(entry_ref); + + if (rel->rd_rel->relkind != RELKIND_INDEX) + { + PgStat_StatDBEntry *dbentry = pgstat_prep_database_pending(dboid); + + dst = &dbentry->vacuum_stats; + dst->tuples_deleted += stats->tuples_deleted; + dst->pages_deleted += stats->pages_deleted; + dst->dead_pages += stats->dead_pages; + dst->pages_frozen += stats->pages_frozen; + dst->pages_all_visible += stats->pages_all_visible; + dst->freeze_age_vacuum_count += stats->freeze_age_vacuum_count; + dst->tuples_frozen += stats->tuples_frozen; + dst->recently_dead_tuples += stats->recently_dead_tuples; + dst->missed_dead_tuples += stats->missed_dead_tuples; + dst->pages_scanned += stats->pages_scanned; + dst->pages_removed += stats->pages_removed; + dst->missed_dead_pages += stats->missed_dead_pages; + dst->compacted_segments += stats->compacted_segments; + dst->tuples_moved += stats->tuples_moved; + } +} + +/* + * 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(RelationGetRelid(rel), rel->rd_rel->relisshared, + 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. * @@ -282,12 +462,14 @@ pgstat_report_vacuum(Oid tableoid, bool shared, void pgstat_report_analyze(Relation rel, PgStat_Counter livetuples, PgStat_Counter deadtuples, - bool resetcounter) + bool resetcounter, TimestampTz starttime) { PgStat_EntryRef *entry_ref; PgStatShared_Relation *shtabentry; PgStat_StatTabEntry *tabentry; Oid dboid = (rel->rd_rel->relisshared ? InvalidOid : MyDatabaseId); + TimestampTz ts; + PgStat_Counter elapsedtime; if (!pgstat_track_counts) return; @@ -321,6 +503,10 @@ pgstat_report_analyze(Relation rel, deadtuples = Max(deadtuples, 0); } + /* Store the data in the table's hash table entry. */ + ts = GetCurrentTimestamp(); + elapsedtime = TimestampDifferenceMilliseconds(starttime, ts); + /* block acquiring lock for the same reason as pgstat_report_autovac() */ entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION, dboid, RelationGetRelid(rel), @@ -344,13 +530,15 @@ pgstat_report_analyze(Relation rel, if (IsAutoVacuumWorkerProcess()) { - tabentry->last_autoanalyze_time = GetCurrentTimestamp(); + tabentry->last_autoanalyze_time = ts; tabentry->autoanalyze_count++; + tabentry->total_autoanalyze_time += elapsedtime; } else { - tabentry->last_analyze_time = GetCurrentTimestamp(); + tabentry->last_analyze_time = ts; tabentry->analyze_count++; + tabentry->total_analyze_time += elapsedtime; } pgstat_unlock_entry(entry_ref); @@ -834,6 +1022,8 @@ pgstat_relation_flush_cb(PgStat_EntryRef *entry_ref, bool nowait) tabentry->ins_since_vacuum += lstats->counts.tuples_inserted; 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/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c index 0225c794774..639c58bdd72 100644 --- a/src/backend/utils/activity/pgstat_shmem.c +++ b/src/backend/utils/activity/pgstat_shmem.c @@ -130,6 +130,21 @@ StatsShmemSize(void) sz = MAXALIGN(sizeof(PgStat_ShmemControl)); sz = add_size(sz, pgstat_dsa_init_size()); + /* Add shared memory for all the custom fixed-numbered statistics */ + for (PgStat_Kind kind = PGSTAT_KIND_CUSTOM_MIN; kind <= PGSTAT_KIND_CUSTOM_MAX; kind++) + { + const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind); + + if (!kind_info) + continue; + if (!kind_info->fixed_amount) + continue; + + Assert(kind_info->shared_size != 0); + + sz += MAXALIGN(kind_info->shared_size); + } + return sz; } @@ -195,17 +210,28 @@ StatsShmemInit(void) pg_atomic_init_u64(&ctl->gc_request_count, 1); - /* initialize fixed-numbered stats */ - LWLockInitialize(&ctl->archiver.lock, LWTRANCHE_PGSTATS_DATA); - LWLockInitialize(&ctl->bgwriter.lock, LWTRANCHE_PGSTATS_DATA); - LWLockInitialize(&ctl->checkpointer.lock, LWTRANCHE_PGSTATS_DATA); - LWLockInitialize(&ctl->slru.lock, LWTRANCHE_PGSTATS_DATA); - LWLockInitialize(&ctl->wal.lock, LWTRANCHE_PGSTATS_DATA); - - for (int i = 0; i < BACKEND_NUM_TYPES; i++) - LWLockInitialize(&ctl->io.locks[i], - LWTRANCHE_PGSTATS_DATA); + for (PgStat_Kind kind = PGSTAT_KIND_MIN; kind <= PGSTAT_KIND_MAX; kind++) + { + const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind); + char *ptr; + + if (!kind_info || !kind_info->fixed_amount) + continue; + + if (pgstat_is_kind_builtin(kind)) + ptr = ((char *) ctl) + kind_info->shared_ctl_off; + else + { + int idx = kind - PGSTAT_KIND_CUSTOM_MIN; + + Assert(kind_info->shared_size != 0); + ctl->custom_data[idx] = ShmemAlloc(kind_info->shared_size); + ptr = ctl->custom_data[idx]; + } + + kind_info->init_shmem_cb(ptr); + } } else { @@ -993,8 +1019,11 @@ shared_stat_reset_contents(PgStat_Kind kind, PgStatShared_Common *header, { const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind); - memset(pgstat_get_entry_data(kind, header), 0, - pgstat_get_entry_len(kind)); + if (kind_info->reset_data_cb) + kind_info->reset_data_cb(header); + else + memset(pgstat_get_entry_data(kind, header), 0, + pgstat_get_entry_len(kind)); if (kind_info->reset_timestamp_cb) kind_info->reset_timestamp_cb(header, ts); @@ -1053,6 +1082,78 @@ pgstat_reset_matching_entries(bool (*do_reset) (PgStatShared_HashEntry *, Datum) dshash_seq_term(&hstat); } +/* Reset just the work payload while the caller holds the entry lock. */ +static void +shared_vacuum_stat_reset(PgStat_Kind kind, PgStatShared_Common *header) +{ + PgStat_VacuumStats *stats; + + Assert(kind == PGSTAT_KIND_RELATION || kind == PGSTAT_KIND_DATABASE); + if (kind == PGSTAT_KIND_RELATION) + stats = &((PgStatShared_Relation *) header)->stats.vacuum_stats; + else + stats = &((PgStatShared_Database *) header)->stats.vacuum_stats; + memset(stats, 0, sizeof(*stats)); +} + +/* + * Reset native VACUUM work for a relation, or for a database and all its + * relations when relid is InvalidOid. Shared catalogs require dboid InvalidOid + * and a valid relid; a database-wide reset leaves shared entries alone. + * Do not reset timing, vacuum/analyze counts, timestamps, other native + * counters, or custom statistics kinds. Cached snapshots follow the usual + * pgstat reset rules; concurrent backends can later flush pending work. + */ +void +pgstat_reset_vacuum_counters(Oid dboid, Oid relid) +{ + PgStat_EntryRef *entry_ref; + dshash_seq_status hstat; + PgStatShared_HashEntry *p; + + Assert(OidIsValid(dboid) || OidIsValid(relid)); + + if (OidIsValid(relid)) + { + entry_ref = pgstat_get_entry_ref(PGSTAT_KIND_RELATION, dboid, relid, + false, NULL); + if (!entry_ref || entry_ref->shared_entry->dropped) + return; + (void) pgstat_lock_entry(entry_ref, false); + shared_vacuum_stat_reset(PGSTAT_KIND_RELATION, entry_ref->shared_stats); + pgstat_unlock_entry(entry_ref); + return; + } + + /* Discard this backend's unflushed database work too. */ + entry_ref = pgstat_get_entry_ref(PGSTAT_KIND_DATABASE, dboid, InvalidOid, + false, NULL); + if (entry_ref && entry_ref->pending) + { + PgStat_StatDBEntry *pending = entry_ref->pending; + + memset(&pending->vacuum_stats, 0, sizeof(pending->vacuum_stats)); + } + + /* The hash keys are read-only; each payload has its own exclusive lock. */ + dshash_seq_init(&hstat, pgStatLocal.shared_hash, false); + while ((p = dshash_seq_next(&hstat)) != NULL) + { + PgStatShared_Common *header; + + if (p->dropped || p->key.dboid != dboid || + (p->key.kind != PGSTAT_KIND_RELATION && + p->key.kind != PGSTAT_KIND_DATABASE)) + continue; + + header = dsa_get_address(pgStatLocal.dsa, p->body); + LWLockAcquire(&header->lock, LW_EXCLUSIVE); + shared_vacuum_stat_reset(p->key.kind, header); + LWLockRelease(&header->lock); + } + dshash_seq_term(&hstat); +} + static bool match_kind(PgStatShared_HashEntry *p, Datum match_data) { diff --git a/src/backend/utils/activity/pgstat_slru.c b/src/backend/utils/activity/pgstat_slru.c index 1b16740f396..ffe2c168b48 100644 --- a/src/backend/utils/activity/pgstat_slru.c +++ b/src/backend/utils/activity/pgstat_slru.c @@ -192,6 +192,14 @@ pgstat_slru_flush(bool nowait) return false; } +void +pgstat_slru_init_shmem_cb(void *stats) +{ + PgStatShared_SLRU *stats_shmem = (PgStatShared_SLRU *) stats; + + LWLockInitialize(&stats_shmem->lock, LWTRANCHE_PGSTATS_DATA); +} + void pgstat_slru_reset_all_cb(TimestampTz ts) { diff --git a/src/backend/utils/activity/pgstat_wal.c b/src/backend/utils/activity/pgstat_wal.c index 6a81b781357..7d440419c3f 100644 --- a/src/backend/utils/activity/pgstat_wal.c +++ b/src/backend/utils/activity/pgstat_wal.c @@ -163,6 +163,14 @@ pgstat_have_pending_wal(void) PendingWalStats.wal_sync != 0; } +void +pgstat_wal_init_shmem_cb(void *stats) +{ + PgStatShared_Wal *stats_shmem = (PgStatShared_Wal *) stats; + + LWLockInitialize(&stats_shmem->lock, LWTRANCHE_PGSTATS_DATA); +} + void pgstat_wal_reset_all_cb(TimestampTz ts) { diff --git a/src/backend/utils/activity/pgstat_xact.c b/src/backend/utils/activity/pgstat_xact.c index 110ddca3c91..8ec1b1ae05f 100644 --- a/src/backend/utils/activity/pgstat_xact.c +++ b/src/backend/utils/activity/pgstat_xact.c @@ -14,6 +14,7 @@ #include "access/transam.h" #include "access/xact.h" +#include "cdb/cdbvars.h" #include "pgstat.h" #include "utils/memutils.h" #include "utils/pgstat_internal.h" @@ -369,7 +370,13 @@ pgstat_create_transactional(PgStat_Kind kind, Oid dboid, Oid objoid) { if (pgstat_get_entry_ref(kind, dboid, objoid, false, NULL)) { - ereport(WARNING, + /* + * Distributed CREATE can reuse the same OID on every segment. + * Keep segment diagnostics in the server log instead of forwarding + * a warning from each QE to the client. Direct utility connections + * and the coordinator retain the warning. + */ + ereport(Gp_role == GP_ROLE_EXECUTE ? LOG_SERVER_ONLY : WARNING, errmsg("resetting existing statistics for kind %s, db=%u, oid=%u", (pgstat_get_kind_info(kind))->name, dboid, objoid)); diff --git a/src/backend/utils/adt/array_typanalyze.c b/src/backend/utils/adt/array_typanalyze.c index 52e160d6bbb..2db192d7544 100644 --- a/src/backend/utils/adt/array_typanalyze.c +++ b/src/backend/utils/adt/array_typanalyze.c @@ -314,7 +314,7 @@ compute_array_stats(VacAttrStats *stats, AnalyzeAttrFetchFunc fetchfunc, int distinct_count; bool count_item_found; - vacuum_delay_point(); + vacuum_delay_point(true); value = fetchfunc(stats, array_no, &isnull); if (isnull) diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c index 61c69b3ca4e..84611f1653e 100644 --- a/src/backend/utils/adt/pgstatfuncs.c +++ b/src/backend/utils/adt/pgstatfuncs.c @@ -120,6 +120,49 @@ PG_STAT_GET_RELENTRY_INT64(tuples_updated) /* pg_stat_get_vacuum_count */ PG_STAT_GET_RELENTRY_INT64(vacuum_count) +/* pg_stat_get_vacuum_failsafe_count */ +PG_STAT_GET_RELENTRY_INT64(vacuum_failsafe_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) \ +{ \ + Oid relid = PG_GETARG_OID(0); \ + double result; \ + PgStat_StatTabEntry *tabentry; \ + \ + if ((tabentry = pgstat_fetch_stat_tabentry(relid)) == NULL) \ + result = 0; \ + else \ + result = (double) (tabentry->stat); \ + \ + PG_RETURN_FLOAT8(result); \ +} + +/* pg_stat_get_total_vacuum_time */ +PG_STAT_GET_RELENTRY_FLOAT8(total_vacuum_time) + +/* pg_stat_get_total_autovacuum_time */ +PG_STAT_GET_RELENTRY_FLOAT8(total_autovacuum_time) + +/* pg_stat_get_total_analyze_time */ +PG_STAT_GET_RELENTRY_FLOAT8(total_analyze_time) + +/* pg_stat_get_total_autoanalyze_time */ +PG_STAT_GET_RELENTRY_FLOAT8(total_autoanalyze_time) + +/* pg_stat_get_total_vacuum_delay_time */ +PG_STAT_GET_RELENTRY_FLOAT8(total_vacuum_delay_time) + +/* pg_stat_get_total_autovacuum_delay_time */ +PG_STAT_GET_RELENTRY_FLOAT8(total_autovacuum_delay_time) + #define PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat) \ Datum \ CppConcat(pg_stat_get_,stat)(PG_FUNCTION_ARGS) \ @@ -1099,6 +1142,9 @@ PG_STAT_GET_DBENTRY_INT64(conflict_tablespace) /* pg_stat_get_db_deadlocks */ PG_STAT_GET_DBENTRY_INT64(deadlocks) +/* pg_stat_get_db_vacuum_interrupt_count */ +PG_STAT_GET_DBENTRY_INT64(vacuum_interrupt_count) + /* pg_stat_get_db_sessions */ PG_STAT_GET_DBENTRY_INT64(sessions) @@ -1248,6 +1294,21 @@ PG_STAT_GET_DBENTRY_FLOAT8_MS(blk_write_time) /* pg_stat_get_db_idle_in_transaction_time */ PG_STAT_GET_DBENTRY_FLOAT8_MS(idle_in_transaction_time) +/* pg_stat_get_db_total_vacuum_time */ +PG_STAT_GET_DBENTRY_FLOAT8_MS(total_vacuum_time) + +/* pg_stat_get_db_total_autovacuum_time */ +PG_STAT_GET_DBENTRY_FLOAT8_MS(total_autovacuum_time) + +/* pg_stat_get_db_total_vacuum_delay_time */ +PG_STAT_GET_DBENTRY_FLOAT8_MS(total_vacuum_delay_time) + +/* pg_stat_get_db_total_autovacuum_delay_time */ +PG_STAT_GET_DBENTRY_FLOAT8_MS(total_autovacuum_delay_time) + +/* pg_stat_get_db_vacuum_failsafe_count */ +PG_STAT_GET_DBENTRY_INT64(vacuum_failsafe_count) + /* pg_stat_get_db_session_time */ PG_STAT_GET_DBENTRY_FLOAT8_MS(session_time) @@ -1810,7 +1871,7 @@ pg_stat_reset(PG_FUNCTION_ARGS) * Reset some shared cluster-wide counters * * When adding a new reset target, ideally the name should match that in - * pgstat_kind_infos, if relevant. + * pgstat_kind_builtin_infos, if relevant. */ Datum pg_stat_reset_shared(PG_FUNCTION_ARGS) @@ -1858,6 +1919,25 @@ pg_stat_reset_single_table_counters(PG_FUNCTION_ARGS) PG_RETURN_VOID(); } +/* Reset native VACUUM work, without affecting other cumulative statistics. */ +Datum +pg_stat_reset_vacuum_stats(PG_FUNCTION_ARGS) +{ + if (PG_ARGISNULL(0)) + pgstat_reset_vacuum_counters(MyDatabaseId, InvalidOid); + else + { + Oid relid = PG_GETARG_OID(0); + Oid dboid = IsSharedRelation(relid) ? InvalidOid : MyDatabaseId; + + /* OID zero is a missing relation, not a request to reset a database. */ + if (OidIsValid(relid)) + pgstat_reset_vacuum_counters(dboid, relid); + } + + PG_RETURN_VOID(); +} + Datum pg_stat_reset_single_function_counters(PG_FUNCTION_ARGS) { @@ -2211,3 +2291,57 @@ pg_stat_have_stats(PG_FUNCTION_ARGS) PG_RETURN_BOOL(pgstat_have_entry(kind, dboid, objoid)); } + +/* Native VACUUM work statistics, independent of ext_vacuum_statistics. */ +static Datum +pgstat_vacuum_stats_tuple(FunctionCallInfo fcinfo, const PgStat_VacuumStats *stats, + bool database) +{ + TupleDesc tupdesc; + Datum values[16]; + bool nulls[16] = {false}; + int i = 0; + + if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE) + elog(ERROR, "return type must be a row type"); + + values[i++] = Int64GetDatum(stats ? stats->tuples_deleted : 0); + if (!database) + values[i++] = Int64GetDatum(stats ? stats->dead_tuples : 0); + if (!database) + values[i++] = Int64GetDatum(stats ? stats->pages_deleted : 0); + values[i++] = Int64GetDatum(stats ? stats->dead_pages : 0); + values[i++] = Int64GetDatum(stats ? stats->pages_frozen : 0); + values[i++] = Int64GetDatum(stats ? stats->pages_all_visible : 0); + values[i++] = Int64GetDatum(stats ? stats->freeze_age_vacuum_count : 0); + values[i++] = Int64GetDatum(stats ? stats->tuples_frozen : 0); + values[i++] = Int64GetDatum(stats ? stats->recently_dead_tuples : 0); + values[i++] = Int64GetDatum(stats ? stats->missed_dead_tuples : 0); + values[i++] = Int64GetDatum(stats ? stats->pages_scanned : 0); + values[i++] = Int64GetDatum(stats ? stats->pages_removed : 0); + values[i++] = Int64GetDatum(stats ? stats->missed_dead_pages : 0); + if (!database) + values[i++] = Int64GetDatum(stats ? stats->total_file_segs : 0); + values[i++] = Int64GetDatum(stats ? stats->compacted_segments : 0); + values[i++] = Int64GetDatum(stats ? stats->tuples_moved : 0); + Assert(i == tupdesc->natts); + return HeapTupleGetDatum(heap_form_tuple(BlessTupleDesc(tupdesc), values, nulls)); +} + +Datum +pg_stat_get_vacuum_stats(PG_FUNCTION_ARGS) +{ + PgStat_StatTabEntry *entry = pgstat_fetch_stat_tabentry(PG_GETARG_OID(0)); + + return pgstat_vacuum_stats_tuple(fcinfo, entry ? &entry->vacuum_stats : NULL, + false); +} + +Datum +pg_stat_get_vacuum_database_stats(PG_FUNCTION_ARGS) +{ + PgStat_StatDBEntry *entry = pgstat_fetch_stat_dbentry(PG_GETARG_OID(0)); + + return pgstat_vacuum_stats_tuple(fcinfo, entry ? &entry->vacuum_stats : NULL, + true); +} diff --git a/src/backend/utils/adt/rangetypes_typanalyze.c b/src/backend/utils/adt/rangetypes_typanalyze.c index ff8700e8e43..a3e98fcf766 100644 --- a/src/backend/utils/adt/rangetypes_typanalyze.c +++ b/src/backend/utils/adt/rangetypes_typanalyze.c @@ -169,7 +169,7 @@ compute_range_stats(VacAttrStats *stats, AnalyzeAttrFetchFunc fetchfunc, upper; float8 length; - vacuum_delay_point(); + vacuum_delay_point(true); value = fetchfunc(stats, range_no, &isnull); if (isnull) diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c index 812782aabd1..0466a2d561f 100644 --- a/src/backend/utils/cache/relcache.c +++ b/src/backend/utils/cache/relcache.c @@ -1994,6 +1994,7 @@ formrdesc(const char *relationName, Oid relationReltype, relation->rd_rel->relpages = 0; relation->rd_rel->reltuples = -1; relation->rd_rel->relallvisible = 0; + relation->rd_rel->relallfrozen = 0; relation->rd_rel->relkind = RELKIND_RELATION; relation->rd_rel->relnatts = (int16) natts; relation->rd_rel->relam = HEAP_TABLE_AM_OID; @@ -4040,6 +4041,7 @@ RelationSetNewRelfilenumber(Relation relation, char persistence) classform->relpages = 0; /* it's empty until further notice */ classform->reltuples = -1; classform->relallvisible = 0; + classform->relallfrozen = 0; } classform->relfrozenxid = freezeXid; classform->relminmxid = minmulti; diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c index 1ff92994527..6e9313b393b 100644 --- a/src/backend/utils/misc/guc_tables.c +++ b/src/backend/utils/misc/guc_tables.c @@ -1480,6 +1480,15 @@ struct config_bool ConfigureNamesBool[] = true, NULL, NULL, NULL }, + { + {"track_cost_delay_timing", PGC_SUSET, STATS_CUMULATIVE, + gettext_noop("Collects timing statistics for cost-based vacuum delay."), + NULL + }, + &track_cost_delay_timing, + false, + NULL, NULL, NULL + }, { {"track_io_timing", PGC_SUSET, STATS_CUMULATIVE, gettext_noop("Collects timing statistics for database I/O activity."), diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample index f1848a3d072..b8f3e6586ab 100644 --- a/src/backend/utils/misc/postgresql.conf.sample +++ b/src/backend/utils/misc/postgresql.conf.sample @@ -646,6 +646,7 @@ optimizer_analyze_root_partition = on # stats collection on root partitions #track_activities = on #track_activity_query_size = 1024 # (change requires restart) #track_counts = off +#track_cost_delay_timing = off #track_io_timing = off #track_wal_io_timing = off #track_functions = none # none, pl, all diff --git a/src/include/access/appendonly_compaction.h b/src/include/access/appendonly_compaction.h index 44aa78a39fb..6c22f1a4d05 100644 --- a/src/include/access/appendonly_compaction.h +++ b/src/include/access/appendonly_compaction.h @@ -13,6 +13,8 @@ #ifndef APPENDONLY_COMPACTION_H #define APPENDONLY_COMPACTION_H +#include "datatype/timestamp.h" +#include "pgstat.h" #include "nodes/pg_list.h" #include "access/appendonly_visimap.h" #include "utils/rel.h" @@ -21,6 +23,13 @@ #define APPENDONLY_COMPACTION_SEGNO_INVALID (-1) +/* Optional resource counters for VACUUM VERBOSE and extensions. */ +typedef struct AOVacuumExtStats +{ + PgStat_CommonCounts phases; + PgStat_CommonCounts indexes; +} AOVacuumExtStats; + /* * Stats for progress reporting. * This is AO/AOCO counterpart of LVRelStats for Heap. It lives throughout @@ -28,9 +37,27 @@ */ typedef struct AOVacuumRelStats { - int nbytes_truncated; /* current # of bytes truncated from segment file */ - int num_dead_tuples; /* current # of dead tuples */ + int64 nbytes_truncated; /* current # of bytes truncated from segment file */ + int64 num_dead_tuples; /* current # of dead tuples */ int num_index_vacuumed; /* current # of indexes been vacuumed */ + /* Active phase durations and delays in milliseconds, excluding phase gaps. */ + double vacuum_time; + double delay_time; + int64 live_tuples; + int64 dead_tuples; + int64 pages_scanned; + int64 total_file_segs; + int64 compacted_segments; + int64 tuples_moved; + + /* the relation these stats were started for */ + Oid relid; + /* + * Resource usage for set_report_vacuum_hook, accumulated over the phases: + * of the phases as a whole, and of the index passes among them, which are + * reported per index and subtracted from the table's report. + */ + AOVacuumExtStats *extstats; /* allocated for VERBOSE or an installed hook */ } AOVacuumRelStats; extern Bitmapset *AppendOptimizedCollectDeadSegments(Relation aorel); diff --git a/src/include/access/visibilitymap.h b/src/include/access/visibilitymap.h index daaa01a2578..6af0fd5304d 100644 --- a/src/include/access/visibilitymap.h +++ b/src/include/access/visibilitymap.h @@ -26,6 +26,8 @@ #define VM_ALL_FROZEN(r, b, v) \ ((visibilitymap_get_status((r), (b), (v)) & VISIBILITYMAP_ALL_FROZEN) != 0) +extern bool visibilitymap_clear_rel(Relation rel, BlockNumber heapBlk, + Buffer vmbuf, uint8 flags); extern bool visibilitymap_clear(Relation rel, BlockNumber heapBlk, Buffer vmbuf, uint8 flags); extern void visibilitymap_pin(Relation rel, BlockNumber heapBlk, diff --git a/src/include/catalog/catversion.h b/src/include/catalog/catversion.h index c9c2fd83275..0e1d1e17f7f 100644 --- a/src/include/catalog/catversion.h +++ b/src/include/catalog/catversion.h @@ -60,6 +60,6 @@ */ /* 3yyymmddN */ -#define CATALOG_VERSION_NO 302608261 +#define CATALOG_VERSION_NO 302610091 #endif diff --git a/src/include/catalog/pg_class.h b/src/include/catalog/pg_class.h index 0b1964b6288..b4aa1c9db38 100644 --- a/src/include/catalog/pg_class.h +++ b/src/include/catalog/pg_class.h @@ -68,6 +68,9 @@ CATALOG(pg_class,1259,RelationRelationId) BKI_BOOTSTRAP BKI_ROWTYPE_OID(83,Relat /* # of all-visible blocks (not always up-to-date) */ int32 relallvisible BKI_DEFAULT(0); + /* # of all-frozen blocks (not always up-to-date) */ + int32 relallfrozen BKI_DEFAULT(0); + /* OID of toast table; 0 if none */ Oid reltoastrelid BKI_DEFAULT(0) BKI_LOOKUP_OPT(pg_class); diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 509c2e3be2c..973ceb728e4 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -5473,6 +5473,22 @@ proname => 'pg_stat_get_autoanalyze_count', provolatile => 's', proparallel => 'r', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_stat_get_autoanalyze_count' }, +{ oid => '8406', descr => 'total vacuum time, in milliseconds', + proname => 'pg_stat_get_total_vacuum_time', provolatile => 's', + proparallel => 'r', prorettype => 'float8', proargtypes => 'oid', + prosrc => 'pg_stat_get_total_vacuum_time' }, +{ oid => '8407', descr => 'total autovacuum time, in milliseconds', + proname => 'pg_stat_get_total_autovacuum_time', provolatile => 's', + proparallel => 'r', prorettype => 'float8', proargtypes => 'oid', + prosrc => 'pg_stat_get_total_autovacuum_time' }, +{ oid => '8408', descr => 'total analyze time, in milliseconds', + proname => 'pg_stat_get_total_analyze_time', provolatile => 's', + proparallel => 'r', prorettype => 'float8', proargtypes => 'oid', + prosrc => 'pg_stat_get_total_analyze_time' }, +{ oid => '8409', descr => 'total autoanalyze time, in milliseconds', + proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's', + proparallel => 'r', prorettype => 'float8', proargtypes => 'oid', + prosrc => 'pg_stat_get_total_autoanalyze_time' }, { oid => '1936', descr => 'statistics: currently active backend IDs', proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't', provolatile => 's', proparallel => 'r', prorettype => 'int4', @@ -5709,6 +5725,54 @@ proname => 'pg_stat_get_db_blk_write_time', provolatile => 's', proparallel => 'r', prorettype => 'float8', proargtypes => 'oid', prosrc => 'pg_stat_get_db_blk_write_time' }, +{ oid => '4707', + 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 => '4708', + 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' }, +{ oid => '4706', descr => 'statistics: number of vacuums interrupted by errors in database', + proname => 'pg_stat_get_db_vacuum_interrupt_count', provolatile => 's', + proparallel => 'r', prorettype => 'int8', proargtypes => 'oid', + prosrc => 'pg_stat_get_db_vacuum_interrupt_count' }, +{ oid => '4700', descr => 'total vacuum delay time, in milliseconds', + proname => 'pg_stat_get_total_vacuum_delay_time', provolatile => 's', + proparallel => 'r', prorettype => 'float8', proargtypes => 'oid', + prosrc => 'pg_stat_get_total_vacuum_delay_time' }, +{ oid => '4701', descr => 'total autovacuum delay time, in milliseconds', + proname => 'pg_stat_get_total_autovacuum_delay_time', provolatile => 's', + proparallel => 'r', prorettype => 'float8', proargtypes => 'oid', + prosrc => 'pg_stat_get_total_autovacuum_delay_time' }, +{ oid => '4702', descr => 'statistics: total vacuum time of tables in database, in milliseconds', + proname => 'pg_stat_get_db_total_vacuum_time', provolatile => 's', + proparallel => 'r', prorettype => 'float8', proargtypes => 'oid', + prosrc => 'pg_stat_get_db_total_vacuum_time' }, +{ oid => '4703', descr => 'statistics: total autovacuum time of tables in database, in milliseconds', + proname => 'pg_stat_get_db_total_autovacuum_time', provolatile => 's', + proparallel => 'r', prorettype => 'float8', proargtypes => 'oid', + prosrc => 'pg_stat_get_db_total_autovacuum_time' }, +{ oid => '4704', descr => 'statistics: total vacuum delay time in database, in milliseconds', + proname => 'pg_stat_get_db_total_vacuum_delay_time', provolatile => 's', + proparallel => 'r', prorettype => 'float8', proargtypes => 'oid', + prosrc => 'pg_stat_get_db_total_vacuum_delay_time' }, +{ oid => '4705', descr => 'statistics: total autovacuum delay time in database, in milliseconds', + proname => 'pg_stat_get_db_total_autovacuum_delay_time', provolatile => 's', + proparallel => 'r', prorettype => 'float8', proargtypes => 'oid', + prosrc => 'pg_stat_get_db_total_autovacuum_delay_time' }, +{ oid => '8691', + descr => 'statistics: number of vacuums that entered the wraparound failsafe mode', + proname => 'pg_stat_get_vacuum_failsafe_count', provolatile => 's', + proparallel => 'r', prorettype => 'int8', proargtypes => 'oid', + prosrc => 'pg_stat_get_vacuum_failsafe_count' }, +{ oid => '8692', + descr => 'statistics: number of vacuums in database that entered the wraparound failsafe mode', + proname => 'pg_stat_get_db_vacuum_failsafe_count', provolatile => 's', + proparallel => 'r', prorettype => 'int8', proargtypes => 'oid', + prosrc => 'pg_stat_get_db_vacuum_failsafe_count' }, { oid => '6185', descr => 'statistics: session time, in milliseconds', proname => 'pg_stat_get_db_session_time', provolatile => 's', proparallel => 'r', prorettype => 'float8', proargtypes => 'oid', @@ -13019,3 +13083,24 @@ proargmodes => '{i,o,o,o,o,o,o,o,o,o,o,o,o,o}', proargnames => '{queueid,queueid,queries_submitted,queries_admitted,queries_rejected,queries_completed,elapsed_wait_secs,max_wait_secs,elapsed_exec_secs,max_exec_secs,total_cost,total_memory_kb,stat_reset_timestamp,have_stats}', prosrc => 'pg_stat_get_resqueue_stats' }, + +{ oid => '9222', descr => 'statistics: reset native vacuum work counters', + proname => 'pg_stat_reset_vacuum_stats', proisstrict => 'f', provolatile => 'v', + prorettype => 'void', proargtypes => 'oid', proargnames => '{relid}', + prosrc => 'pg_stat_reset_vacuum_stats' }, + +{ oid => '9220', descr => 'statistics: vacuum work counters', + proname => 'pg_stat_get_vacuum_stats', provolatile => 's', proparallel => 'r', + prorettype => 'record', proargtypes => 'oid', + proallargtypes => '{oid,int8,int8,int8,int8,int8,int8,int8,int8,int8,int8,int8,int8,int8,int8,int8,int8}', + proargmodes => '{i,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o}', + proargnames => '{relid,tuples_deleted,dead_tuples,pages_deleted,dead_pages,pages_frozen,pages_all_visible,freeze_age_vacuum_count,tuples_frozen,recently_dead_tuples,missed_dead_tuples,pages_scanned,pages_removed,missed_dead_pages,total_file_segs,compacted_segments,tuples_moved}', + prosrc => 'pg_stat_get_vacuum_stats' }, + +{ oid => '9221', descr => 'statistics: vacuum work counters', + proname => 'pg_stat_get_vacuum_database_stats', provolatile => 's', proparallel => 'r', + prorettype => 'record', proargtypes => 'oid', + proallargtypes => '{oid,int8,int8,int8,int8,int8,int8,int8,int8,int8,int8,int8,int8,int8}', + proargmodes => '{i,o,o,o,o,o,o,o,o,o,o,o,o,o}', + proargnames => '{datid,tuples_deleted,dead_pages,pages_frozen,pages_all_visible,freeze_age_vacuum_count,tuples_frozen,recently_dead_tuples,missed_dead_tuples,pages_scanned,pages_removed,missed_dead_pages,compacted_segments,tuples_moved}', + prosrc => 'pg_stat_get_vacuum_database_stats' }, diff --git a/src/include/commands/progress.h b/src/include/commands/progress.h index fc9ce7c1a66..01e0b766cd3 100644 --- a/src/include/commands/progress.h +++ b/src/include/commands/progress.h @@ -25,6 +25,7 @@ #define PROGRESS_VACUUM_NUM_INDEX_VACUUMS 4 #define PROGRESS_VACUUM_MAX_DEAD_TUPLES 5 #define PROGRESS_VACUUM_NUM_DEAD_TUPLES 6 +#define PROGRESS_VACUUM_DELAY_TIME 7 /* Phases of vacuum (as advertised via PROGRESS_VACUUM_PHASE) */ #define PROGRESS_VACUUM_PHASE_SCAN_HEAP 1 @@ -47,6 +48,7 @@ #define PROGRESS_ANALYZE_CHILD_TABLES_TOTAL 5 #define PROGRESS_ANALYZE_CHILD_TABLES_DONE 6 #define PROGRESS_ANALYZE_CURRENT_CHILD_TABLE_RELID 7 +#define PROGRESS_ANALYZE_DELAY_TIME 8 /* Phases of analyze (as advertised via PROGRESS_ANALYZE_PHASE) */ #define PROGRESS_ANALYZE_PHASE_ACQUIRE_SAMPLE_ROWS 1 diff --git a/src/include/commands/vacuum.h b/src/include/commands/vacuum.h index 431cd7221a8..57e675734bb 100644 --- a/src/include/commands/vacuum.h +++ b/src/include/commands/vacuum.h @@ -21,10 +21,12 @@ #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 "storage/lock.h" #include "utils/relcache.h" +#include "pgstat.h" #include "utils/snapshot.h" /* @@ -276,6 +278,7 @@ typedef struct VPgClassStats BlockNumber rel_pages; double rel_tuples; BlockNumber relallvisible; + BlockNumber relallfrozen; } VPgClassStats; typedef struct VPgClassStatsCombo @@ -284,6 +287,7 @@ typedef struct VPgClassStatsCombo BlockNumber rel_pages; double rel_tuples; BlockNumber relallvisible; + BlockNumber relallfrozen; int count; /* expect to equal to the number of dispatched segments */ } VPgClassStatsCombo; @@ -434,16 +438,37 @@ extern PGDLLIMPORT int vacuum_multixact_freeze_min_age; extern PGDLLIMPORT int vacuum_multixact_freeze_table_age; extern PGDLLIMPORT int vacuum_failsafe_age; extern PGDLLIMPORT int vacuum_multixact_failsafe_age; +extern PGDLLIMPORT bool track_cost_delay_timing; /* Variables for cost-based parallel vacuum */ extern PGDLLIMPORT pg_atomic_uint32 *VacuumSharedCostBalance; extern PGDLLIMPORT pg_atomic_uint32 *VacuumActiveNWorkers; extern PGDLLIMPORT int VacuumCostBalanceLocal; +/* Allocated only for VERBOSE or an extended-statistics hook. */ +typedef struct LVExtStatCounters +{ + WalUsage walusage; + BufferUsage bufusage; + PgStat_Counter blocks_fetched; + PgStat_Counter blocks_hit; + PgStat_VacuumRelationCounts report; +} LVExtStatCounters; + +extern LVExtStatCounters *extvac_stats_start(Relation rel, bool verbose); +extern void extvac_stats_end(Relation rel, LVExtStatCounters *counters, + PgStat_CommonCounts *report); + +extern void extvac_stats_log(Relation rel, const char *phase, + const PgStat_CommonCounts *report); + extern PGDLLIMPORT bool VacuumFailsafeActive; extern PGDLLIMPORT double vacuum_cost_delay; extern PGDLLIMPORT int vacuum_cost_limit; +extern PGDLLIMPORT int64 parallel_vacuum_worker_delay_ns; +extern PGDLLIMPORT double VacuumDelayTime; + /* in commands/vacuum.c */ extern void ExecVacuum(ParseState *pstate, VacuumStmt *vacstmt, bool isTopLevel, bool auto_stats); extern void vacuum(List *relations, VacuumParams *params, @@ -459,11 +484,13 @@ extern double vac_estimate_reltuples(Relation relation, extern void vac_send_relstats_to_qd(Relation relation, BlockNumber num_pages, double num_tuples, - BlockNumber num_all_visible_pages); + BlockNumber num_all_visible_pages, + BlockNumber num_all_frozen_pages); extern void vac_update_relstats(Relation relation, BlockNumber num_pages, double num_tuples, BlockNumber num_all_visible_pages, + BlockNumber num_all_frozen_pages, bool hasindex, TransactionId frozenxid, MultiXactId minmulti, @@ -475,7 +502,11 @@ extern bool vacuum_get_cutoffs(Relation rel, const VacuumParams *params, struct VacuumCutoffs *cutoffs); extern bool vacuum_xid_failsafe_check(const struct VacuumCutoffs *cutoffs); extern void vac_update_datfrozenxid(void); -extern void vacuum_delay_point(void); +extern void vacuum_report_index_stats(Relation indrel, + const IndexBulkDeleteResult *istat, + double prev_tuples_removed, + BlockNumber prev_pages_newly_deleted, bool cleanup); +extern void vacuum_delay_point(bool is_analyze); extern bool vacuum_is_relation_owner(Oid relid, Form_pg_class reltuple, bits32 options); extern Relation vacuum_open_relation(Oid relid, RangeVar *relation, @@ -517,7 +548,8 @@ extern void analyze_rel(Oid relid, RangeVar *relation, /* in commands/vacuumlazy.c */ extern void lazy_vacuum_rel_heap(Relation onerel, VacuumParams *params, BufferAccessStrategy bstrategy); -extern void scan_index(Relation indrel, Relation aorel, int elevel, BufferAccessStrategy bstrategy); +extern void scan_index(Relation indrel, Relation aorel, int elevel, BufferAccessStrategy bstrategy, + IndexBulkDeleteResult *result); /* in commands/vacuum_ao.c */ extern void ao_vacuum_rel(Relation rel, VacuumParams *params, BufferAccessStrategy bstrategy); diff --git a/src/include/pgstat.h b/src/include/pgstat.h index 2a85773c30c..16a1044681e 100644 --- a/src/include/pgstat.h +++ b/src/include/pgstat.h @@ -16,6 +16,7 @@ #include "postmaster/pgarch.h" /* for MAX_XFN_CHARS */ #include "utils/backend_progress.h" /* for backward compatibility */ #include "utils/backend_status.h" /* for backward compatibility */ +#include "utils/pgstat_kind.h" #include "utils/relcache.h" #include "utils/wait_event.h" /* for backward compatibility */ @@ -33,33 +34,6 @@ /* Default directory to store temporary statistics data in */ #define PG_STAT_TMP_DIR "pg_stat_tmp" -/* The types of statistics entries */ -typedef enum PgStat_Kind -{ - /* use 0 for INVALID, to catch zero-initialized data */ - PGSTAT_KIND_INVALID = 0, - - /* stats for variable-numbered objects */ - PGSTAT_KIND_DATABASE, /* database-wide statistics */ - PGSTAT_KIND_RELATION, /* per-table statistics */ - PGSTAT_KIND_FUNCTION, /* per-function statistics */ - PGSTAT_KIND_REPLSLOT, /* per-slot statistics */ - PGSTAT_KIND_SUBSCRIPTION, /* per-subscription statistics */ - PGSTAT_KIND_RESQUEUE, /* per-resource-queue statistics */ - - /* stats for fixed-numbered objects */ - PGSTAT_KIND_ARCHIVER, - PGSTAT_KIND_BGWRITER, - PGSTAT_KIND_CHECKPOINTER, - PGSTAT_KIND_IO, - PGSTAT_KIND_SLRU, - PGSTAT_KIND_WAL, -} PgStat_Kind; - -#define PGSTAT_KIND_FIRST_VALID PGSTAT_KIND_DATABASE -#define PGSTAT_KIND_LAST PGSTAT_KIND_WAL -#define PGSTAT_NUM_KINDS (PGSTAT_KIND_LAST + 1) - /* Values for track_functions GUC variable --- order is significant! */ typedef enum TrackFunctionsLevel { @@ -91,6 +65,32 @@ typedef enum SessionEndType */ typedef int64 PgStat_Counter; +/* + * Work measured by VACUUM, stored in ordinary cumulative statistics. + * dead_tuples and total_file_segs are samples from the last completed run; + * all other fields accumulate across runs. Database totals omit snapshots + * and index reports, whose work is already part of the table vacuum. + */ +typedef struct PgStat_VacuumStats +{ + PgStat_Counter tuples_deleted; + PgStat_Counter dead_tuples; + PgStat_Counter pages_deleted; + PgStat_Counter dead_pages; + PgStat_Counter pages_frozen; + PgStat_Counter pages_all_visible; + PgStat_Counter freeze_age_vacuum_count; + PgStat_Counter tuples_frozen; + PgStat_Counter recently_dead_tuples; + PgStat_Counter missed_dead_tuples; + PgStat_Counter pages_scanned; + PgStat_Counter pages_removed; + PgStat_Counter missed_dead_pages; + PgStat_Counter total_file_segs; + PgStat_Counter compacted_segments; + PgStat_Counter tuples_moved; +} PgStat_VacuumStats; + /* ------------------------------------------------------------ * Structures kept in backend local memory while accumulating counts @@ -117,6 +117,39 @@ typedef struct PgStat_FunctionCounts /* * Working state needed to accumulate per-function-call timing statistics. */ +/* + * Extended vacuum statistics - passed to extensions via set_report_vacuum_hook. + * Type of entry: table (heap), index, or database aggregate. + */ +typedef enum ExtVacReportType +{ + PGSTAT_EXTVAC_INVALID = 0, + PGSTAT_EXTVAC_TABLE = 1, + PGSTAT_EXTVAC_INDEX = 2, + PGSTAT_EXTVAC_DB = 3, +} ExtVacReportType; + +typedef struct PgStat_CommonCounts +{ + int64 total_blks_read; + int64 total_blks_hit; + int64 total_blks_dirtied; + int64 total_blks_written; + int64 blks_fetched; + int64 blks_hit; + double blk_read_time; + double blk_write_time; + int64 wal_records; + int64 wal_fpi; + uint64 wal_bytes; +} PgStat_CommonCounts; + +typedef struct PgStat_VacuumRelationCounts +{ + PgStat_CommonCounts common; + ExtVacReportType type; +} PgStat_VacuumRelationCounts; + typedef struct PgStat_FunctionCallUsage { /* Link to function's hashtable entry (must still be there at exit!) */ @@ -180,6 +213,14 @@ typedef struct PgStat_TableCounts PgStat_Counter blocks_fetched; PgStat_Counter blocks_hit; + + /* + * Revocations of all-visible and all-frozen marks in the visibility map. + * The bits are cleared by the page modification itself, so these count + * whether or not the transaction commits. + */ + PgStat_Counter visible_page_marks_cleared; + PgStat_Counter frozen_page_marks_cleared; } PgStat_TableCounts; /* ---------- @@ -238,7 +279,7 @@ typedef struct PgStat_TableXactStatus * ------------------------------------------------------------ */ -#define PGSTAT_FILE_FORMAT_ID 0x01A5BCAD +#define PGSTAT_FILE_FORMAT_ID 0x01A5BCB7 typedef struct PgStat_ArchiverStats { @@ -354,7 +395,28 @@ typedef struct PgStat_StatDBEntry PgStat_Counter sessions_fatal; PgStat_Counter sessions_killed; + /* + * Cumulative time vacuums spent processing tables of this database and + * sleeping in cost-based delay points, in microseconds (the delay parts + * advance only when track_cost_delay_timing is enabled). Times of index + * processing are included in the owning table's run. + */ + PgStat_Counter total_vacuum_time; + PgStat_Counter total_autovacuum_time; + PgStat_Counter total_vacuum_delay_time; + PgStat_Counter total_autovacuum_delay_time; + + /* + * Number of vacuums in this database that entered the wraparound + * failsafe mode (see vacuum_failsafe_age). + */ + PgStat_Counter vacuum_failsafe_count; + + /* # of vacuums in this database interrupted by errors */ + PgStat_Counter vacuum_interrupt_count; + TimestampTz stat_reset_timestamp; + PgStat_VacuumStats vacuum_stats; } PgStat_StatDBEntry; typedef struct PgStat_StatFuncEntry @@ -418,6 +480,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; @@ -427,6 +491,26 @@ typedef struct PgStat_StatTabEntry PgStat_Counter analyze_count; TimestampTz last_autoanalyze_time; /* autovacuum initiated */ PgStat_Counter autoanalyze_count; + + PgStat_Counter total_vacuum_time; /* times in milliseconds */ + PgStat_Counter total_autovacuum_time; + PgStat_Counter total_analyze_time; + PgStat_Counter total_autoanalyze_time; + + /* + * Time slept in cost-based vacuum delay points while vacuuming this + * relation, in milliseconds; nonzero only when track_cost_delay_timing + * is enabled. + */ + PgStat_Counter total_vacuum_delay_time; + PgStat_Counter total_autovacuum_delay_time; + + /* + * Number of vacuums of this relation that entered the wraparound + * failsafe mode (see vacuum_failsafe_age). + */ + PgStat_Counter vacuum_failsafe_count; + PgStat_VacuumStats vacuum_stats; } PgStat_StatTabEntry; typedef struct PgStat_WalStats @@ -540,6 +624,7 @@ extern void pgstat_force_next_flush(void); extern void pgstat_reset_counters(void); extern void pgstat_reset(PgStat_Kind kind, Oid dboid, Oid objoid); extern void pgstat_reset_of_kind(PgStat_Kind kind); +extern void pgstat_reset_vacuum_counters(Oid dboid, Oid relid); /* stats accessors */ extern void pgstat_clear_snapshot(void); @@ -650,11 +735,38 @@ extern void pgstat_init_relation(Relation rel); extern void pgstat_assoc_relation(Relation rel); extern void pgstat_unlink_relation(Relation rel); +extern void pgstat_report_vacuum_stats(Relation rel, + const PgStat_VacuumStats *stats); +extern void pgstat_report_vacuum_elapsed(Oid tableoid, bool shared, + PgStat_Counter livetuples, PgStat_Counter deadtuples, + PgStat_Counter elapsedtime, PgStat_Counter delaytime, + bool failsafe); extern void pgstat_report_vacuum(Oid tableoid, bool shared, - PgStat_Counter livetuples, PgStat_Counter deadtuples); + PgStat_Counter livetuples, PgStat_Counter deadtuples, + TimestampTz starttime, PgStat_Counter delaytime, + bool failsafe); +extern void pgstat_report_index_vacuum_time(Relation rel, + PgStat_Counter elapsedtime, + PgStat_Counter delaytime, + bool is_autovacuum); +extern void pgstat_count_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); + bool resetcounter, TimestampTz starttime); /* * If stats are enabled, but pending data hasn't been prepared yet, call @@ -703,6 +815,26 @@ extern void pgstat_report_analyze(Relation rel, (rel)->pgstat_info->counts.blocks_hit++; \ } while (0) +/* + * Count revocations of all-visible and all-frozen marks in the visibility + * map. Only tables have a visibility map, so these always work on the + * table's pending counters. + */ +#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); extern void pgstat_count_heap_delete(Relation rel); diff --git a/src/include/storage/bufmgr.h b/src/include/storage/bufmgr.h index c42970d35b2..3f6f3887d46 100644 --- a/src/include/storage/bufmgr.h +++ b/src/include/storage/bufmgr.h @@ -401,7 +401,8 @@ RelationGuessNumberOfBlocksFromSize(uint64 szbytes) RelationGetNumberOfBlocksInFork(reln, MAIN_FORKNUM) extern BlockNumber AcquireNumberOfBlocks(Relation onerel); -extern BlockNumber AcquireNumberOfAllVisibleBlocks(Relation onerel); +extern void AcquireVisibilityMapCounts(Relation onerel, BlockNumber *all_visible, + BlockNumber *all_frozen); extern bool BufferIsPermanent(Buffer buffer); extern XLogRecPtr BufferGetLSNAtomic(Buffer buffer); diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h index 861dd7d7184..5f316f98a1b 100644 --- a/src/include/utils/pgstat_internal.h +++ b/src/include/utils/pgstat_internal.h @@ -212,18 +212,28 @@ typedef struct PgStat_KindInfo */ bool accessed_across_databases:1; - /* - * For variable-numbered stats: Identified on-disk using a name, rather - * than PgStat_HashKey. Probably only needed for replication slot stats. - */ - bool named_on_disk:1; + /* Should stats be written to the on-disk stats file? */ + bool write_to_file:1; /* * The size of an entry in the shared stats hash table (pointed to by - * PgStatShared_HashEntry->body). + * PgStatShared_HashEntry->body). For fixed-numbered statistics, this is + * the size of an entry in PgStat_ShmemControl->custom_data. */ uint32 shared_size; + /* + * The offset of the statistics struct in the cached statistics snapshot + * PgStat_Snapshot, for fixed-numbered statistics. + */ + uint32 snapshot_ctl_off; + + /* + * The offset of the statistics struct in the containing shared memory + * control structure PgStat_ShmemControl, for fixed-numbered statistics. + */ + uint32 shared_ctl_off; + /* * The offset/size of statistics inside the shared stats entry. Used when * [de-]serializing statistics to / from disk respectively. Separate from @@ -252,18 +262,32 @@ typedef struct PgStat_KindInfo */ void (*delete_pending_cb) (PgStat_EntryRef *sr); + /* + * For variable-numbered stats: reset counters while preserving identifying + * metadata in the payload. Optional; otherwise the whole payload is zeroed. + * Called with the entry's lock held exclusively, before reset_timestamp_cb. + */ + void (*reset_data_cb) (PgStatShared_Common *header); + /* * For variable-numbered stats: reset the reset timestamp. Optional. */ void (*reset_timestamp_cb) (PgStatShared_Common *header, TimestampTz ts); /* - * For variable-numbered stats with named_on_disk. Optional. + * For variable-numbered stats. Optional. */ void (*to_serialized_name) (const PgStat_HashKey *key, const PgStatShared_Common *header, NameData *name); bool (*from_serialized_name) (const NameData *name, PgStat_HashKey *key); + /* + * For fixed-numbered statistics: Initialize shared memory state. + * + * "stats" is the pointer to the allocated shared memory area. + */ + void (*init_shmem_cb) (void *stats); + /* * For fixed-numbered statistics: Reset All. */ @@ -483,6 +507,13 @@ typedef struct PgStat_ShmemControl PgStatShared_IO io; PgStatShared_SLRU slru; PgStatShared_Wal wal; + + /* + * Custom stats data with fixed-numbered objects, indexed by (PgStat_Kind + * - PGSTAT_KIND_CUSTOM_MIN). + */ + void *custom_data[PGSTAT_KIND_CUSTOM_SIZE]; + } PgStat_ShmemControl; @@ -496,7 +527,7 @@ typedef struct PgStat_Snapshot /* time at which snapshot was taken */ TimestampTz snapshot_timestamp; - bool fixed_valid[PGSTAT_NUM_KINDS]; + bool fixed_valid[PGSTAT_KIND_BUILTIN_SIZE]; PgStat_ArchiverStats archiver; @@ -510,6 +541,14 @@ typedef struct PgStat_Snapshot PgStat_WalStats wal; + /* + * Data in snapshot for custom fixed-numbered statistics, indexed by + * (PgStat_Kind - PGSTAT_KIND_CUSTOM_MIN). Each entry is allocated in + * TopMemoryContext, for a size of PgStat_KindInfo->shared_data_len. + */ + bool custom_valid[PGSTAT_KIND_CUSTOM_SIZE]; + void *custom_data[PGSTAT_KIND_CUSTOM_SIZE]; + /* to free snapshot in bulk */ MemoryContext context; struct pgstat_snapshot_hash *stats; @@ -546,6 +585,8 @@ static inline int pgstat_cmp_hash_key(const void *a, const void *b, size_t size, static inline uint32 pgstat_hash_hash_key(const void *d, size_t size, void *arg); static inline size_t pgstat_get_entry_len(PgStat_Kind kind); static inline void *pgstat_get_entry_data(PgStat_Kind kind, PgStatShared_Common *entry); +static inline void *pgstat_get_custom_shmem_data(PgStat_Kind kind); +static inline void *pgstat_get_custom_snapshot_data(PgStat_Kind kind); /* @@ -553,6 +594,8 @@ static inline void *pgstat_get_entry_data(PgStat_Kind kind, PgStatShared_Common */ extern const PgStat_KindInfo *pgstat_get_kind_info(PgStat_Kind kind); +extern void pgstat_register_kind(PgStat_Kind kind, + const PgStat_KindInfo *kind_info); #ifdef USE_ASSERT_CHECKING extern void pgstat_assert_is_up(void); @@ -572,6 +615,7 @@ extern void pgstat_snapshot_fixed(PgStat_Kind kind); * Functions in pgstat_archiver.c */ +extern void pgstat_archiver_init_shmem_cb(void *stats); extern void pgstat_archiver_reset_all_cb(TimestampTz ts); extern void pgstat_archiver_snapshot_cb(void); @@ -580,6 +624,7 @@ extern void pgstat_archiver_snapshot_cb(void); * Functions in pgstat_bgwriter.c */ +extern void pgstat_bgwriter_init_shmem_cb(void *stats); extern void pgstat_bgwriter_reset_all_cb(TimestampTz ts); extern void pgstat_bgwriter_snapshot_cb(void); @@ -588,6 +633,7 @@ extern void pgstat_bgwriter_snapshot_cb(void); * Functions in pgstat_checkpointer.c */ +extern void pgstat_checkpointer_init_shmem_cb(void *stats); extern void pgstat_checkpointer_reset_all_cb(TimestampTz ts); extern void pgstat_checkpointer_snapshot_cb(void); @@ -618,6 +664,7 @@ extern bool pgstat_function_flush_cb(PgStat_EntryRef *entry_ref, bool nowait); */ extern bool pgstat_flush_io(bool nowait); +extern void pgstat_io_init_shmem_cb(void *stats); extern void pgstat_io_reset_all_cb(TimestampTz ts); extern void pgstat_io_snapshot_cb(void); @@ -676,6 +723,7 @@ extern PgStatShared_Common *pgstat_init_entry(PgStat_Kind kind, */ extern bool pgstat_slru_flush(bool nowait); +extern void pgstat_slru_init_shmem_cb(void *stats); extern void pgstat_slru_reset_all_cb(TimestampTz ts); extern void pgstat_slru_snapshot_cb(void); @@ -688,6 +736,7 @@ extern bool pgstat_flush_wal(bool nowait); extern void pgstat_init_wal(void); extern bool pgstat_have_pending_wal(void); +extern void pgstat_wal_init_shmem_cb(void *stats); extern void pgstat_wal_reset_all_cb(TimestampTz ts); extern void pgstat_wal_snapshot_cb(void); @@ -862,4 +911,34 @@ pgstat_get_entry_data(PgStat_Kind kind, PgStatShared_Common *entry) return ((char *) (entry)) + off; } +/* + * Returns a pointer to the shared memory area of custom stats for + * fixed-numbered statistics. + */ +static inline void * +pgstat_get_custom_shmem_data(PgStat_Kind kind) +{ + int idx = kind - PGSTAT_KIND_CUSTOM_MIN; + + Assert(pgstat_is_kind_custom(kind)); + Assert(pgstat_get_kind_info(kind)->fixed_amount); + + return pgStatLocal.shmem->custom_data[idx]; +} + +/* + * Returns a pointer to the portion of custom data for fixed-numbered + * statistics in the current snapshot. + */ +static inline void * +pgstat_get_custom_snapshot_data(PgStat_Kind kind) +{ + int idx = kind - PGSTAT_KIND_CUSTOM_MIN; + + Assert(pgstat_is_kind_custom(kind)); + Assert(pgstat_get_kind_info(kind)->fixed_amount); + + return pgStatLocal.snapshot.custom_data[idx]; +} + #endif /* PGSTAT_INTERNAL_H */ diff --git a/src/include/utils/pgstat_kind.h b/src/include/utils/pgstat_kind.h new file mode 100644 index 00000000000..dd1f4022bf0 --- /dev/null +++ b/src/include/utils/pgstat_kind.h @@ -0,0 +1,72 @@ +/* ---------- + * pgstat_kind.h + * + * Definitions related to the statistics kinds for the PostgreSQL + * cumulative statistics system. Can be included in backend or + * frontend code. + * + * Copyright (c) 2001-2025, PostgreSQL Global Development Group + * + * src/include/utils/pgstat_kind.h + * ---------- + */ +#ifndef PGSTAT_KIND_H +#define PGSTAT_KIND_H + +/* The types of statistics entries */ +#define PgStat_Kind uint32 + +/* Range of IDs allowed, for built-in and custom kinds */ +#define PGSTAT_KIND_MIN 1 /* Minimum ID allowed */ +#define PGSTAT_KIND_MAX 32 /* Maximum ID allowed */ + +/* use 0 for INVALID, to catch zero-initialized data */ +#define PGSTAT_KIND_INVALID 0 + +/* stats for variable-numbered objects */ +#define PGSTAT_KIND_DATABASE 1 /* database-wide statistics */ +#define PGSTAT_KIND_RELATION 2 /* per-table statistics */ +#define PGSTAT_KIND_FUNCTION 3 /* per-function statistics */ +#define PGSTAT_KIND_REPLSLOT 4 /* per-slot statistics */ +#define PGSTAT_KIND_SUBSCRIPTION 5 /* per-subscription statistics */ +#define PGSTAT_KIND_RESQUEUE 6 /* per-resource-queue statistics */ + +/* stats for fixed-numbered objects */ +#define PGSTAT_KIND_ARCHIVER 7 +#define PGSTAT_KIND_BGWRITER 8 +#define PGSTAT_KIND_CHECKPOINTER 9 +#define PGSTAT_KIND_IO 10 +#define PGSTAT_KIND_SLRU 11 +#define PGSTAT_KIND_WAL 12 + +#define PGSTAT_KIND_BUILTIN_MIN PGSTAT_KIND_DATABASE +#define PGSTAT_KIND_BUILTIN_MAX PGSTAT_KIND_WAL +#define PGSTAT_KIND_BUILTIN_SIZE (PGSTAT_KIND_BUILTIN_MAX + 1) + +/* Custom stats kinds */ + +/* Range of IDs allowed for custom stats kinds */ +#define PGSTAT_KIND_CUSTOM_MIN 24 +#define PGSTAT_KIND_CUSTOM_MAX PGSTAT_KIND_MAX +#define PGSTAT_KIND_CUSTOM_SIZE (PGSTAT_KIND_CUSTOM_MAX - PGSTAT_KIND_CUSTOM_MIN + 1) + +/* + * PgStat_Kind to use for extensions that require an ID, but are still in + * development and have not reserved their own unique kind ID yet. See: + * https://wiki.postgresql.org/wiki/CustomCumulativeStats + */ +#define PGSTAT_KIND_EXPERIMENTAL 24 + +static inline bool +pgstat_is_kind_builtin(PgStat_Kind kind) +{ + return kind >= PGSTAT_KIND_BUILTIN_MIN && kind <= PGSTAT_KIND_BUILTIN_MAX; +} + +static inline bool +pgstat_is_kind_custom(PgStat_Kind kind) +{ + return kind >= PGSTAT_KIND_CUSTOM_MIN && kind <= PGSTAT_KIND_CUSTOM_MAX; +} + +#endif /* PGSTAT_KIND_H */ diff --git a/src/include/utils/unsync_guc_name.h b/src/include/utils/unsync_guc_name.h index 02b04fdc5c4..235732f7266 100644 --- a/src/include/utils/unsync_guc_name.h +++ b/src/include/utils/unsync_guc_name.h @@ -616,6 +616,7 @@ "track_activities", "track_activity_query_size", "track_commit_timestamp", + "track_cost_delay_timing", "track_counts", "track_functions", "track_io_timing", diff --git a/src/test/isolation/expected/vacuum-extending-freeze.out b/src/test/isolation/expected/vacuum-extending-freeze.out new file mode 100644 index 00000000000..fbd03d8da92 --- /dev/null +++ b/src/test/isolation/expected/vacuum-extending-freeze.out @@ -0,0 +1,197 @@ +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.oid = 'vestat'::regclass; + + UPDATE stats_state + SET frozen_flag_count = c.relallfrozen, + all_visibile_flag_count = c.relallvisible + FROM pg_class c + WHERE c.oid = 'vestat'::regclass; + +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.oid = 'vestat'::regclass; + + UPDATE stats_state + SET frozen_flag_count = c.relallfrozen, + all_visibile_flag_count = c.relallvisible + FROM pg_class c + WHERE c.oid = 'vestat'::regclass; + +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 ISOLATION LEVEL REPEATABLE READ; + SELECT count(x) FROM vestat WHERE x > 2000; + + 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'; + +count +----- + 3000 +(1 row) + +step s2_delete_from_table: + DELETE FROM vestat WHERE x > 4930; + 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 +--------------------------+------------------------- +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.oid = 'vestat'::regclass; + + UPDATE stats_state + SET frozen_flag_count = c.relallfrozen, + all_visibile_flag_count = c.relallvisible + FROM pg_class c + WHERE c.oid = 'vestat'::regclass; + +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 5418fbf8b39..45cd0847b9f 100644 --- a/src/test/isolation/isolation_schedule +++ b/src/test/isolation/isolation_schedule @@ -173,3 +173,4 @@ test: vacuum-ao-concurrent-drop test: serializable-parallel-3 test: matview-write-skew +test: vacuum-extending-freeze diff --git a/src/test/isolation/specs/vacuum-extending-freeze.spec b/src/test/isolation/specs/vacuum-extending-freeze.spec new file mode 100644 index 00000000000..21173bfaff7 --- /dev/null +++ b/src/test/isolation/specs/vacuum-extending-freeze.spec @@ -0,0 +1,127 @@ +# Check VM counts after freezing and cumulative counts of cleared VM bits. +# A repeatable-read transaction keeps deleted tuples visible to its snapshot. +# It also caches the table statistics before the other session deletes rows: +# the VM bits clear during DELETE, while the cached counters stay unchanged +# until the reader commits. Force the writer's counters to flush so this +# distinction does not depend on the statistics flush interval. + +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; + DROP TABLE IF EXISTS stats_state; + RESET vacuum_freeze_min_age; + RESET vacuum_freeze_table_age; + +} + +session s1 +setup +{ + SET stats_fetch_consistency = 'cache'; +} + +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.oid = 'vestat'::regclass; + + UPDATE stats_state + SET frozen_flag_count = c.relallfrozen, + all_visibile_flag_count = c.relallvisible + FROM pg_class c + WHERE c.oid = 'vestat'::regclass; +} + +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 ISOLATION LEVEL REPEATABLE READ; + SELECT count(x) FROM vestat WHERE x > 2000; + + 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_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; + SELECT pg_stat_force_next_flush(); +} +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 diff --git a/src/test/isolation2/expected/vacuum_ao_stats.out b/src/test/isolation2/expected/vacuum_ao_stats.out new file mode 100644 index 00000000000..d70951bc01e --- /dev/null +++ b/src/test/isolation2/expected/vacuum_ao_stats.out @@ -0,0 +1,140 @@ +-- start_matchsubs +-- +-- # the segment that reports the injected error first is not deterministic +-- +-- m/^ERROR: fault triggered, fault name:'vacuum_ao_after_compact' fault type:'error' \(seg\d+ .*\)/ +-- s/^ERROR: fault triggered, fault name:'vacuum_ao_after_compact' fault type:'error' \(seg\d+ .*\)/ERROR: fault triggered, fault name:'vacuum_ao_after_compact' fault type:'error'/ +-- end_matchsubs +-- +-- The vacuum statistics of the core for append-optimized tables: the vacuum +-- and delay times of the tables and their indexes, and vacuum_interrupt_count. +-- +CREATE TABLE vacstat_ao_row (i int, v text) WITH (appendonly=true) DISTRIBUTED BY (i); +CREATE +CREATE TABLE vacstat_ao_col (i int, v text) WITH (appendonly=true, orientation=column) DISTRIBUTED BY (i); +CREATE +CREATE INDEX vacstat_ao_row_idx ON vacstat_ao_row (i); +CREATE +CREATE INDEX vacstat_ao_col_idx ON vacstat_ao_col (i); +CREATE +INSERT INTO vacstat_ao_row SELECT g, repeat('x', 100) FROM generate_series(1, 3000) g; +INSERT 3000 +INSERT INTO vacstat_ao_col SELECT g, repeat('x', 100) FROM generate_series(1, 3000) g; +INSERT 3000 +DELETE FROM vacstat_ao_row WHERE i % 2 = 0; +DELETE 1500 +DELETE FROM vacstat_ao_col WHERE i % 2 = 0; +DELETE 1500 + +-- A cost-limited vacuum sleeps, in the index passes at least, so both the +-- tables and the indexes get vacuum and delay time on every segment. +1: SET track_cost_delay_timing = on; +SET +1: SET vacuum_cost_delay = '1ms'; +SET +1: SET vacuum_cost_limit = 10; +SET +1: VACUUM vacstat_ao_row, vacstat_ao_col; +VACUUM +1: SELECT relname, count(*) FILTER (WHERE total_vacuum_time > 0) AS segments_timed, count(*) FILTER (WHERE total_vacuum_delay_time > 0) AS segments_delayed FROM gp_dist_random('pg_stat_all_tables') WHERE relname LIKE 'vacstat_ao_%' GROUP BY relname ORDER BY relname; + relname | segments_timed | segments_delayed +----------------+----------------+------------------ + vacstat_ao_col | 3 | 3 + vacstat_ao_row | 3 | 3 +(2 rows) +1: SELECT indexrelname, count(*) FILTER (WHERE total_vacuum_time > 0) AS segments_timed, count(*) FILTER (WHERE total_vacuum_delay_time > 0) AS segments_delayed FROM gp_dist_random('pg_stat_all_indexes') WHERE indexrelname LIKE 'vacstat_ao_%' GROUP BY indexrelname ORDER BY indexrelname; + indexrelname | segments_timed | segments_delayed +--------------------+----------------+------------------ + vacstat_ao_col_idx | 3 | 3 + vacstat_ao_row_idx | 3 | 3 +(2 rows) +1: RESET vacuum_cost_limit; +RESET +1: RESET vacuum_cost_delay; +RESET +1: RESET track_cost_delay_timing; +RESET + +-- A vacuum interrupted by an error. Start from zero on every instance. +1: SELECT pg_stat_reset() FROM gp_dist_random('gp_id'); + pg_stat_reset +--------------- + + + +(3 rows) +1: SELECT pg_stat_reset(); + pg_stat_reset +--------------- + +(1 row) +INSERT INTO vacstat_ao_row SELECT g, repeat('y', 100) FROM generate_series(1, 100) g; +INSERT 100 +DELETE FROM vacstat_ao_row WHERE i % 2 = 1; +DELETE 1550 + +-- Make the compact phase fail on the segments. The fault is scoped to this +-- session, so that no other backend can consume it. +1: SELECT gp_inject_fault('vacuum_ao_after_compact', 'error', '', '', '', 1, -1, 0, dbid, current_setting('gp_session_id')::int) FROM gp_segment_configuration WHERE role = 'p' AND content > -1; + gp_inject_fault +----------------- + Success: + Success: + Success: +(3 rows) +1: VACUUM vacstat_ao_row; +ERROR: fault triggered, fault name:'vacuum_ao_after_compact' fault type:'error' +1: SELECT gp_inject_fault('vacuum_ao_after_compact', 'reset', dbid) FROM gp_segment_configuration WHERE role = 'p' AND content > -1; + gp_inject_fault +----------------- + Success: + Success: + Success: +(3 rows) + +-- The phases of an append-optimized vacuum share one AOVacuumRelStats, which +-- is freed at the end of the last phase. A vacuum that fails in between used +-- to leave it behind, and the next vacuum of the relation then reused its +-- start time, so the time of everything that happened in between was added +-- to the relation's cumulative vacuum time. Let time pass. +1: SELECT pg_sleep(5); + pg_sleep +---------- + +(1 row) + +1: VACUUM vacstat_ao_row; +VACUUM + +-- The second vacuum takes milliseconds; without resetting the stats of the +-- failed one it would be credited with the five seconds slept above. +1: SELECT sum(total_vacuum_time) < 3000 AS vacuum_time_is_sane FROM gp_dist_random('pg_stat_all_tables') WHERE relname = 'vacstat_ao_row'; + vacuum_time_is_sane +--------------------- + t +(1 row) + +-- The segments that raised the error counted it; the coordinator, which only +-- dispatched the failing phase, did not. A segment may also be canceled +-- after its phase is already done, so check for "some", not for "all". +-- +-- A segment adds the count to its pending statistics when its transaction +-- ends, and flushes them when it is idle outside a transaction, which it is +-- not while its part of the failed distributed transaction waits for the +-- coordinator; the second vacuum above has ended that. Give the flush the +-- second it may wait for (PGSTAT_MIN_INTERVAL). +1: SELECT pg_sleep(2); + pg_sleep +---------- + +(1 row) +1: SELECT gp_segment_id = -1 AS coordinator, sum(vacuum_interrupt_count) > 0 AS interrupted FROM gp_stat_vacuum WHERE datname = current_database() GROUP BY 1 ORDER BY 1; + coordinator | interrupted +-------------+------------- + f | t + t | f +(2 rows) + +1: DROP TABLE vacstat_ao_row, vacstat_ao_col; +DROP +1q: ... diff --git a/src/test/isolation2/isolation2_schedule b/src/test/isolation2/isolation2_schedule index 82d23731fb6..3e72b37ce0a 100644 --- a/src/test/isolation2/isolation2_schedule +++ b/src/test/isolation2/isolation2_schedule @@ -152,6 +152,7 @@ test: uao/fast_analyze_row test: uao/create_index_allows_readonly_row test: reorganize_after_ao_vacuum_skip_drop truncate_after_ao_vacuum_skip_drop mark_all_aoseg_await_drop +test: vacuum_ao_stats # below test(s) inject faults so each of them need to be in a separate group test: segwalrep/master_wal_switch diff --git a/src/test/isolation2/sql/vacuum_ao_stats.sql b/src/test/isolation2/sql/vacuum_ao_stats.sql new file mode 100644 index 00000000000..4fc15732c6c --- /dev/null +++ b/src/test/isolation2/sql/vacuum_ao_stats.sql @@ -0,0 +1,84 @@ +-- start_matchsubs +-- +-- # the segment that reports the injected error first is not deterministic +-- +-- m/^ERROR: fault triggered, fault name:'vacuum_ao_after_compact' fault type:'error' \(seg\d+ .*\)/ +-- s/^ERROR: fault triggered, fault name:'vacuum_ao_after_compact' fault type:'error' \(seg\d+ .*\)/ERROR: fault triggered, fault name:'vacuum_ao_after_compact' fault type:'error'/ +-- end_matchsubs +-- +-- The vacuum statistics of the core for append-optimized tables: the vacuum +-- and delay times of the tables and their indexes, and vacuum_interrupt_count. +-- +CREATE TABLE vacstat_ao_row (i int, v text) WITH (appendonly=true) DISTRIBUTED BY (i); +CREATE TABLE vacstat_ao_col (i int, v text) WITH (appendonly=true, orientation=column) DISTRIBUTED BY (i); +CREATE INDEX vacstat_ao_row_idx ON vacstat_ao_row (i); +CREATE INDEX vacstat_ao_col_idx ON vacstat_ao_col (i); +INSERT INTO vacstat_ao_row SELECT g, repeat('x', 100) FROM generate_series(1, 3000) g; +INSERT INTO vacstat_ao_col SELECT g, repeat('x', 100) FROM generate_series(1, 3000) g; +DELETE FROM vacstat_ao_row WHERE i % 2 = 0; +DELETE FROM vacstat_ao_col WHERE i % 2 = 0; + +-- A cost-limited vacuum sleeps, in the index passes at least, so both the +-- tables and the indexes get vacuum and delay time on every segment. +1: SET track_cost_delay_timing = on; +1: SET vacuum_cost_delay = '1ms'; +1: SET vacuum_cost_limit = 10; +1: VACUUM vacstat_ao_row, vacstat_ao_col; +1: SELECT relname, count(*) FILTER (WHERE total_vacuum_time > 0) AS segments_timed, + count(*) FILTER (WHERE total_vacuum_delay_time > 0) AS segments_delayed + FROM gp_dist_random('pg_stat_all_tables') + WHERE relname LIKE 'vacstat_ao_%' GROUP BY relname ORDER BY relname; +1: SELECT indexrelname, count(*) FILTER (WHERE total_vacuum_time > 0) AS segments_timed, + count(*) FILTER (WHERE total_vacuum_delay_time > 0) AS segments_delayed + FROM gp_dist_random('pg_stat_all_indexes') + WHERE indexrelname LIKE 'vacstat_ao_%' GROUP BY indexrelname ORDER BY indexrelname; +1: RESET vacuum_cost_limit; +1: RESET vacuum_cost_delay; +1: RESET track_cost_delay_timing; + +-- A vacuum interrupted by an error. Start from zero on every instance. +1: SELECT pg_stat_reset() FROM gp_dist_random('gp_id'); +1: SELECT pg_stat_reset(); +INSERT INTO vacstat_ao_row SELECT g, repeat('y', 100) FROM generate_series(1, 100) g; +DELETE FROM vacstat_ao_row WHERE i % 2 = 1; + +-- Make the compact phase fail on the segments. The fault is scoped to this +-- session, so that no other backend can consume it. +1: SELECT gp_inject_fault('vacuum_ao_after_compact', 'error', '', '', '', 1, -1, 0, dbid, + current_setting('gp_session_id')::int) + FROM gp_segment_configuration WHERE role = 'p' AND content > -1; +1: VACUUM vacstat_ao_row; +1: SELECT gp_inject_fault('vacuum_ao_after_compact', 'reset', dbid) + FROM gp_segment_configuration WHERE role = 'p' AND content > -1; + +-- The phases of an append-optimized vacuum share one AOVacuumRelStats, which +-- is freed at the end of the last phase. A vacuum that fails in between used +-- to leave it behind, and the next vacuum of the relation then reused its +-- start time, so the time of everything that happened in between was added +-- to the relation's cumulative vacuum time. Let time pass. +1: SELECT pg_sleep(5); + +1: VACUUM vacstat_ao_row; + +-- The second vacuum takes milliseconds; without resetting the stats of the +-- failed one it would be credited with the five seconds slept above. +1: SELECT sum(total_vacuum_time) < 3000 AS vacuum_time_is_sane + FROM gp_dist_random('pg_stat_all_tables') WHERE relname = 'vacstat_ao_row'; + +-- The segments that raised the error counted it; the coordinator, which only +-- dispatched the failing phase, did not. A segment may also be canceled +-- after its phase is already done, so check for "some", not for "all". +-- +-- A segment adds the count to its pending statistics when its transaction +-- ends, and flushes them when it is idle outside a transaction, which it is +-- not while its part of the failed distributed transaction waits for the +-- coordinator; the second vacuum above has ended that. Give the flush the +-- second it may wait for (PGSTAT_MIN_INTERVAL). +1: SELECT pg_sleep(2); +1: SELECT gp_segment_id = -1 AS coordinator, + sum(vacuum_interrupt_count) > 0 AS interrupted + FROM gp_stat_vacuum WHERE datname = current_database() + GROUP BY 1 ORDER BY 1; + +1: DROP TABLE vacstat_ao_row, vacstat_ao_col; +1q: diff --git a/src/test/modules/test_misc/meson.build b/src/test/modules/test_misc/meson.build index 911084ac0fe..6022a0a60e4 100644 --- a/src/test/modules/test_misc/meson.build +++ b/src/test/modules/test_misc/meson.build @@ -10,6 +10,18 @@ tests += { 't/002_tablespace.pl', 't/003_check_guc.pl', 't/004_io_direct.pl', + 't/016_index_vacuum_time.pl', + 't/017_vacuum_failsafe.pl', + 't/018_vacuum_interrupts.pl', + 't/019_resqueue_stats_restart.pl', + 't/020_relallfrozen.pl', + 't/021_heap_page_counters.pl', + 't/022_ao_hidden_tuples.pl', + 't/023_dead_pages.pl', + 't/024_ao_dead_pages.pl', + 't/027_freeze_age_vacuum_count.pl', + 't/028_vacuum_work_core.pl', + 't/029_vacuum_resource_verbose.pl', ], }, } diff --git a/src/test/modules/test_misc/t/016_index_vacuum_time.pl b/src/test/modules/test_misc/t/016_index_vacuum_time.pl new file mode 100644 index 00000000000..4eb02ab2517 --- /dev/null +++ b/src/test/modules/test_misc/t/016_index_vacuum_time.pl @@ -0,0 +1,80 @@ +# Copyright (c) 2026, PostgreSQL Global Development Group + +# Test the total_vacuum_time counter of pg_stat_all_indexes: the time vacuum +# spent processing each index, accumulated over bulkdelete and cleanup passes, +# mirroring the table-level counter in pg_stat_all_tables. + +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_cost_delay_timing = on +]); +$node->start; + +# The index has to span enough pages for its vacuum passes to exceed +# vacuum_cost_limit on buffer hits alone (its pages are still dirty from the +# load, so dirtying them costs nothing). Scale the row count with the block +# size, so that builds with larger blocks (32kB in Cloudberry) keep the page +# count of the default 8kB. +my $nrows = 100000 * + ($node->safe_psql('postgres', 'SHOW block_size') / 8192); + +# Enough rows that the bulkdelete pass over the index takes measurable time; +# a small vacuum_cost_delay adds deterministic delay time on fast machines. +$node->safe_psql( + 'postgres', qq[ +CREATE TABLE vactime_t (id int PRIMARY KEY, v text) WITH (autovacuum_enabled = off); +INSERT INTO vactime_t SELECT g, repeat('x', 10) FROM generate_series(1, $nrows) g; +DELETE FROM vactime_t WHERE id % 2 = 0; +]); +$node->safe_psql( + 'postgres', qq[ +SET vacuum_cost_delay = '1ms'; +SET vacuum_cost_limit = 200; +VACUUM vactime_t; +]); + +# The upper bound guards against garbage such as an epoch-based elapsed time +# leaking into the counter. +is( $node->safe_psql( + 'postgres', qq[ +SELECT total_vacuum_time > 0 AND total_vacuum_time < 600000 + FROM pg_stat_all_indexes WHERE indexrelname = 'vactime_t_pkey']), + 't', + 'total_vacuum_time advanced sanely for the index in pg_stat_all_indexes'); + +is( $node->safe_psql( + 'postgres', qq[ +SELECT total_vacuum_delay_time > 0 AND total_vacuum_delay_time <= total_vacuum_time + FROM pg_stat_all_indexes WHERE indexrelname = 'vactime_t_pkey']), + 't', + 'total_vacuum_delay_time advanced and not above total_vacuum_time'); + +is( $node->safe_psql( + 'postgres', qq[ +SELECT total_autovacuum_time = 0 FROM pg_stat_all_indexes + WHERE indexrelname = 'vactime_t_pkey']), + 't', + 'manual vacuum did not count into total_autovacuum_time'); + +# The same run must have accumulated into the database-wide totals. +is( $node->safe_psql( + 'postgres', qq[ +SELECT total_vacuum_time > 0 AND total_vacuum_time < 600000 + AND total_vacuum_delay_time > 0 + AND total_vacuum_delay_time <= total_vacuum_time + FROM pg_stat_vacuum WHERE datname = current_database()]), + 't', + 'database-wide vacuum times advanced sanely in pg_stat_vacuum'); + +$node->stop; + +done_testing(); diff --git a/src/test/modules/test_misc/t/017_vacuum_failsafe.pl b/src/test/modules/test_misc/t/017_vacuum_failsafe.pl new file mode 100644 index 00000000000..9332c250477 --- /dev/null +++ b/src/test/modules/test_misc/t/017_vacuum_failsafe.pl @@ -0,0 +1,71 @@ +# Copyright (c) 2026, PostgreSQL Global Development Group + +# Test that vacuums entering the wraparound failsafe mode are counted in +# pg_stat_all_tables.vacuum_failsafe_count and aggregated per database in +# pg_stat_vacuum.vacuum_failsafe_count. +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; +# The failsafe cutoff is clamped to 1.05 * autovacuum_freeze_max_age, so use +# the minimum allowed value to keep the number of XIDs to burn small. +$node->append_conf( + 'postgresql.conf', qq[ +autovacuum = off +autovacuum_freeze_max_age = 100000 +]); +$node->start; + +$node->safe_psql( + 'postgres', qq[ + CREATE TABLE tab_failsafe (i int); + INSERT INTO tab_failsafe SELECT generate_series(1, 100); +]); + +# A vacuum without failsafe pressure must not bump the counter. +$node->safe_psql('postgres', 'VACUUM tab_failsafe;'); +my $count = $node->safe_psql('postgres', + q[SELECT vacuum_failsafe_count FROM pg_stat_all_tables WHERE relname = 'tab_failsafe';] +); +is($count, '0', 'normal vacuum does not count as failsafe'); + +# Age the table past 1.05 * autovacuum_freeze_max_age: burn XIDs with +# aborted subtransactions (each aborted subxact consumes an assigned XID). +$node->safe_psql( + 'postgres', qq[ + CREATE TABLE burn_xids (i int); + DO \$\$ + BEGIN + FOR i IN 1..110000 LOOP + BEGIN + INSERT INTO burn_xids VALUES (1); + RAISE EXCEPTION 'burn'; + EXCEPTION WHEN OTHERS THEN + END; + END LOOP; + END \$\$; +]); + +# vacuum_failsafe_age = 0 makes the (clamped) cutoff kick in immediately. +$node->safe_psql( + 'postgres', qq[ + SET vacuum_failsafe_age = 0; + SET vacuum_multixact_failsafe_age = 0; + VACUUM tab_failsafe; +]); + +$count = $node->safe_psql('postgres', + q[SELECT vacuum_failsafe_count FROM pg_stat_all_tables WHERE relname = 'tab_failsafe';] +); +is($count, '1', 'failsafe vacuum counted in pg_stat_all_tables'); + +my $db_count = $node->safe_psql('postgres', + q[SELECT vacuum_failsafe_count FROM pg_stat_vacuum WHERE datname = 'postgres';] +); +is($db_count, '1', 'failsafe vacuum counted in pg_stat_vacuum'); + +done_testing(); diff --git a/src/test/modules/test_misc/t/018_vacuum_interrupts.pl b/src/test/modules/test_misc/t/018_vacuum_interrupts.pl new file mode 100644 index 00000000000..f1cb11462ac --- /dev/null +++ b/src/test/modules/test_misc/t/018_vacuum_interrupts.pl @@ -0,0 +1,71 @@ +# Copyright (c) 2026, PostgreSQL Global Development Group + +# Test the vacuum_interrupt_count counter of pg_stat_vacuum. +# +# vacuum_interrupt_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 +]); +$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 vacuum_interrupt_count > 0 FROM pg_stat_vacuum WHERE datname = current_database()]), + 't', + 'vacuum_interrupt_count advanced in pg_stat_vacuum'); + +$node->stop; + +done_testing(); diff --git a/src/test/modules/test_misc/t/019_resqueue_stats_restart.pl b/src/test/modules/test_misc/t/019_resqueue_stats_restart.pl new file mode 100644 index 00000000000..b8435641a71 --- /dev/null +++ b/src/test/modules/test_misc/t/019_resqueue_stats_restart.pl @@ -0,0 +1,76 @@ +# Copyright (c) 2026, PostgreSQL Global Development Group + +# Resource queue statistics must survive a clean restart. Queue admission +# needs a dispatcher, so use a coordinator and one primary segment rather +# than the standalone utility-mode instances normally used by TAP. +use strict; +use warnings FATAL => 'all'; +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +plan skip_all => 'temporary cluster requires internal FTS' + unless check_pg_config('#define USE_INTERNAL_FTS 1'); + +my $node = PostgreSQL::Test::Cluster->new('resqueue_coordinator'); +my $segment = PostgreSQL::Test::Cluster->new('resqueue_segment'); +for my $instance ($node, $segment) +{ + $instance->init; + $instance->append_conf('postgresql.conf', q{ +autovacuum = off +gp_resource_manager = 'queue' +listen_addresses = '127.0.0.1' +}); +} + +sub start_instance +{ + my ($instance, $role, $content) = @_; + command_ok( + ['pg_ctl', '-D', $instance->data_dir, '-l', $instance->logfile, + '-o', "-c gp_role=$role --gp_dbid=$instance->{_dbid} --gp_contentid=$content", + '-w', 'start'], + "started content $content in $role mode"); + $instance->_update_pid(1); +} + +# Populate the cluster configuration in utility mode before starting the +# dispatcher, as gpinitsystem does. +start_instance($node, 'utility', -1); +for my $entry ([$node, -1], [$segment, 0]) +{ + my ($instance, $content) = @$entry; + my $port = $instance->port; + my $datadir = $instance->data_dir; + $node->safe_psql('postgres', qq{ +SELECT gp_add_segment($instance->{_dbid}::int2, ${content}::int2, + 'p', 'p', 's', 'u', $port, + 'localhost', '127.0.0.1', '$datadir'); +}); +} +$node->stop; +start_instance($segment, 'execute', 0); +start_instance($node, 'dispatch', -1); + +# Explicit connection options override the utility-mode default of psql(). +my $connstr = $node->connstr('postgres') . " options=''"; +$node->safe_psql('postgres', q{ +CREATE RESOURCE QUEUE restart_queue WITH (active_statements = 1); +CREATE ROLE restart_user RESOURCE QUEUE restart_queue; +SET ROLE restart_user; +SELECT 1; +RESET ROLE; +}, connstr => $connstr); +my $stats_sql = q{ +SELECT queries_submitted, queries_admitted, queries_completed + FROM pg_stat_resqueues WHERE queuename = 'restart_queue'}; +my $before = $node->safe_psql('postgres', $stats_sql, connstr => $connstr); +like($before, qr/^[1-9][0-9]*[|][1-9][0-9]*[|][1-9][0-9]*$/, + 'query populated the resource queue statistics'); +$node->restart; +is($node->safe_psql('postgres', $stats_sql, connstr => $connstr), $before, + 'resource queue statistics survive a clean restart'); +$node->stop; +$segment->stop; +done_testing(); diff --git a/src/test/modules/test_misc/t/020_relallfrozen.pl b/src/test/modules/test_misc/t/020_relallfrozen.pl new file mode 100644 index 00000000000..5bc2f0b085b --- /dev/null +++ b/src/test/modules/test_misc/t/020_relallfrozen.pl @@ -0,0 +1,67 @@ +# Copyright (c) 2026, PostgreSQL Global Development Group + +# Exercise the catalog update paths independently of vacuum reporting. +use strict; +use warnings FATAL => 'all'; +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +my $node = PostgreSQL::Test::Cluster->new('relallfrozen'); +$node->init; +$node->append_conf('postgresql.conf', 'autovacuum = off'); +$node->start; +$node->safe_psql('postgres', q{ +CREATE EXTENSION pg_visibility; +CREATE TABLE frozen_heap (id int, padding text); +INSERT INTO frozen_heap SELECT i, repeat('x', 100) FROM generate_series(1, 2000) i; +VACUUM (FREEZE) frozen_heap; +}); + +sub check_map +{ + my ($label) = @_; + is($node->safe_psql('postgres', q{ +SELECT relallfrozen = all_frozen AND relallvisible = all_visible + AND relallfrozen <= relallvisible AND relallvisible <= relpages +FROM pg_class, pg_visibility_map_summary('frozen_heap') +WHERE oid = 'frozen_heap'::regclass; +}), 't', $label); +} + +check_map('VACUUM FREEZE records both visibility-map counts'); +is($node->safe_psql('postgres', q{ +SELECT relallfrozen > 0 FROM pg_class WHERE oid = 'frozen_heap'::regclass +}), 't', 'frozen heap has all-frozen pages'); +$node->safe_psql('postgres', q{ +UPDATE frozen_heap SET padding = 'updated' WHERE id = 1; +ANALYZE frozen_heap; +}); +check_map('ANALYZE refreshes both counts after a VM bit is cleared'); +$node->safe_psql('postgres', 'CREATE INDEX frozen_heap_idx ON frozen_heap(id)'); +check_map('CREATE INDEX preserves heap visibility-map counts'); +is($node->safe_psql('postgres', q{ +SELECT relallfrozen = 0 AND relallvisible = 0 +FROM pg_class WHERE oid = 'frozen_heap_idx'::regclass +}), 't', 'index has zero visibility-map counts'); +$node->safe_psql('postgres', 'VACUUM (FULL) frozen_heap'); +check_map('VACUUM FULL swaps visibility-map statistics with the new heap'); +$node->safe_psql('postgres', 'TRUNCATE frozen_heap'); +check_map('TRUNCATE resets both visibility-map counts'); +for my $orientation ('row', 'column') +{ + $node->safe_psql('postgres', qq{ +CREATE TABLE frozen_ao_$orientation (id int) +WITH (appendoptimized=true, orientation=$orientation); +INSERT INTO frozen_ao_$orientation SELECT generate_series(1, 100); +CREATE INDEX frozen_ao_${orientation}_idx ON frozen_ao_$orientation(id); +VACUUM (ANALYZE) frozen_ao_$orientation; +}); + is($node->safe_psql('postgres', qq{ +SELECT bool_and(relallfrozen = 0 AND relallvisible = 0) +FROM pg_class WHERE oid IN ('frozen_ao_$orientation'::regclass, + 'frozen_ao_${orientation}_idx'::regclass) +}), 't', "AO $orientation table and index have no visibility map"); +} +$node->stop; +done_testing(); diff --git a/src/test/modules/test_misc/t/021_heap_page_counters.pl b/src/test/modules/test_misc/t/021_heap_page_counters.pl new file mode 100644 index 00000000000..89ebf852963 --- /dev/null +++ b/src/test/modules/test_misc/t/021_heap_page_counters.pl @@ -0,0 +1,92 @@ +# Copyright (c) 2026, PostgreSQL Global Development Group + +# Count work on heap pages, rather than the current visibility-map state. +use strict; +use warnings FATAL => 'all'; +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +my $node = PostgreSQL::Test::Cluster->new('vacstat_heap_pages'); +$node->init; +$node->append_conf('postgresql.conf', q{ +autovacuum = off +}); +$node->start; +$node->safe_psql('postgres', q{ +CREATE TABLE heap_pages (id int PRIMARY KEY, padding text); +INSERT INTO heap_pages SELECT g, repeat('x', 100) FROM generate_series(1, 10000) g; +}); +my $pages = $node->safe_psql('postgres', q{ +SELECT count(DISTINCT split_part(ctid::text, ',', 1)) FROM heap_pages}); +my $counts_sql = q{ +SELECT pages_frozen, pages_all_visible + FROM pg_stat_vacuum_tables WHERE relname = 'heap_pages'}; + +# No pruning or freezing is needed, but all populated pages become visible. +$node->safe_psql('postgres', 'VACUUM heap_pages'); +is($node->safe_psql('postgres', $counts_sql), "0|$pages", + 'ordinary vacuum marks all populated pages visible without freezing'); + +# Upgrading all-visible pages to all-frozen must not count them visible again. +$node->safe_psql('postgres', 'VACUUM (FREEZE) heap_pages'); +is($node->safe_psql('postgres', $counts_sql), "$pages|$pages", + 'freeze counts each affected page without recounting its all-visible bit'); +is($node->safe_psql('postgres', q{ +SELECT tuples_frozen FROM pg_stat_vacuum_tables + WHERE relname = 'heap_pages'}), '10000', + 'tuple and page freezing counters have distinct units'); +$node->safe_psql('postgres', 'VACUUM (FREEZE, DISABLE_PAGE_SKIPPING) heap_pages'); +is($node->safe_psql('postgres', $counts_sql), "$pages|$pages", + 'rescanning already frozen and visible pages adds no work'); + +# Changing the indexed key requires index cleanup and a second heap pass. +# The old and new tuple can occupy either one or two distinct heap pages. +$node->safe_psql('postgres', q{ +CREATE TABLE changed_pages AS + SELECT split_part(ctid::text, ',', 1) AS blk FROM heap_pages WHERE id = 1; +UPDATE heap_pages SET id = 10001 WHERE id = 1; +INSERT INTO changed_pages + SELECT split_part(ctid::text, ',', 1) FROM heap_pages WHERE id = 10001; +}); +my $changed = $node->safe_psql('postgres', 'SELECT count(DISTINCT blk) FROM changed_pages'); +$node->safe_psql('postgres', 'VACUUM (FREEZE, INDEX_CLEANUP ON) heap_pages'); +my $expected = ($pages + 1) . '|' . ($pages + $changed); +is($node->safe_psql('postgres', $counts_sql), $expected, + 'index cleanup restores changed visibility bits and freezes the new tuple'); +$node->safe_psql('postgres', 'VACUUM (FREEZE, DISABLE_PAGE_SKIPPING) heap_pages'); +is($node->safe_psql('postgres', $counts_sql), $expected, + 'another full scan does not count second-pass visibility work again'); + +# Empty pages can become all-visible without any tuples being frozen. +$node->safe_psql('postgres', q{ +CREATE TABLE empty_pages (id int); +INSERT INTO empty_pages SELECT generate_series(1, 10000); +}); +my $empty_pages = $node->safe_psql('postgres', q{ +SELECT count(DISTINCT split_part(ctid::text, ',', 1)) FROM empty_pages}); +$node->safe_psql('postgres', 'DELETE FROM empty_pages'); +$node->safe_psql('postgres', 'VACUUM (FREEZE, TRUNCATE OFF) empty_pages'); +my $empty_sql = q{ +SELECT pages_frozen, pages_all_visible + FROM pg_stat_vacuum_tables WHERE relname = 'empty_pages'}; +is($node->safe_psql('postgres', $empty_sql), "0|$empty_pages", + 'empty pages become visible without adding frozen pages'); +$node->safe_psql('postgres', 'VACUUM (FREEZE, TRUNCATE OFF, DISABLE_PAGE_SKIPPING) empty_pages'); +is($node->safe_psql('postgres', $empty_sql), "0|$empty_pages", + 'rescanning empty pages does not count existing visibility marks'); + +$node->restart; +is($node->safe_psql('postgres', $counts_sql), $expected, + 'heap page counters survive a clean restart'); + +$node->safe_psql('postgres', q{ +SELECT pg_stat_reset_single_table_counters('heap_pages'::regclass)}); +is($node->safe_psql('postgres', $counts_sql), '0|0', + 'relation reset clears both heap page counters'); +$node->safe_psql('postgres', 'VACUUM (FREEZE, DISABLE_PAGE_SKIPPING) heap_pages'); +is($node->safe_psql('postgres', $counts_sql), '0|0', + 'post-reset vacuum does not report existing frozen or visible pages as work'); + +$node->stop; +done_testing(); diff --git a/src/test/modules/test_misc/t/022_ao_hidden_tuples.pl b/src/test/modules/test_misc/t/022_ao_hidden_tuples.pl new file mode 100644 index 00000000000..80ee4ba4f51 --- /dev/null +++ b/src/test/modules/test_misc/t/022_ao_hidden_tuples.pl @@ -0,0 +1,104 @@ +# Copyright (c) 2026, PostgreSQL Global Development Group + +# AO hidden rows have both a cumulative observation count and a latest sample. +use strict; +use warnings FATAL => 'all'; +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +my $node = PostgreSQL::Test::Cluster->new('vacstat_ao_hidden'); +$node->init; +$node->append_conf('postgresql.conf', q{ +autovacuum = off +}); +$node->start; +$node->safe_psql('postgres', q{ +CREATE TABLE hidden_heap (id int); +INSERT INTO hidden_heap VALUES (1); +VACUUM hidden_heap; +}); +is($node->safe_psql('postgres', q{ +SELECT dead_tuples FROM pg_stat_vacuum_tables + WHERE relname = 'hidden_heap'}), '0', 'AO hidden-row snapshot stays zero for heap'); + +for my $orientation ('row', 'column') +{ + my $table = "hidden_$orientation"; + my $count_sql = qq{ +SELECT recently_dead_tuples, dead_tuples FROM pg_stat_vacuum_tables + WHERE relname = '$table'}; + $node->safe_psql('postgres', qq{ +CREATE TABLE $table (id int) WITH (appendonly = true, orientation = $orientation); +INSERT INTO $table SELECT generate_series(1, 1000); +DELETE FROM $table WHERE id <= 100; +SET gp_appendonly_compaction_threshold = 100; +SET vacuum_freeze_table_age = 0; +VACUUM $table; +}); + is($node->safe_psql('postgres', qq{ +SELECT recently_dead_tuples = 100 AND tuples_deleted = 0 AND compacted_segments = 0 + AND dead_pages = 0 AND dead_tuples = 100 AND freeze_age_vacuum_count = 0 + FROM pg_stat_vacuum_tables WHERE relname = '$table'}), 't', + "$orientation AO counts hidden rows left below the compaction threshold"); + + $node->safe_psql('postgres', "SET gp_appendonly_compaction = off; VACUUM $table"); + is($node->safe_psql('postgres', $count_sql), '200|100', + "$orientation AO accumulates remaining rows but replaces the latest snapshot"); + + $node->safe_psql('postgres', qq{ +DELETE FROM $table WHERE id = 101; +SET gp_appendonly_compaction = off; +SET track_counts = off; +VACUUM $table; +}); + is($node->safe_psql('postgres', $count_sql), '200|100', + "$orientation AO disabled collection preserves both counts despite another hidden row"); + + $node->safe_psql('postgres', "SET gp_appendonly_compaction_threshold = 1; VACUUM $table"); + is($node->safe_psql('postgres', qq{ +SELECT recently_dead_tuples = 200 AND dead_tuples = 0 + AND tuples_deleted = 101 AND compacted_segments > 0 + FROM pg_stat_vacuum_tables WHERE relname = '$table'}), 't', + "$orientation AO samples after compaction and clears the latest hidden-row count"); + $node->safe_psql('postgres', "VACUUM $table"); + is($node->safe_psql('postgres', $count_sql), '200|0', + "$orientation AO empty hidden-row sample does not reuse an earlier count"); + + $node->safe_psql('postgres', qq{ +DELETE FROM $table WHERE id = 102; +SET gp_appendonly_compaction_threshold = 100; +VACUUM $table; +}); + is($node->safe_psql('postgres', $count_sql), '201|1', + "$orientation AO adds a fresh remaining-row sample after compaction"); +} + +$node->restart; +for my $orientation ('row', 'column') +{ + my $table = "hidden_$orientation"; + my $count_sql = qq{ +SELECT recently_dead_tuples, dead_tuples FROM pg_stat_vacuum_tables + WHERE relname = '$table'}; + is($node->safe_psql('postgres', $count_sql), '201|1', + "$orientation AO remaining-row total and snapshot survive a clean restart"); + $node->safe_psql('postgres', qq{ +SELECT pg_stat_reset_single_table_counters('$table'::regclass)}); + is($node->safe_psql('postgres', $count_sql), '0|0', + "$orientation AO relation reset clears the remaining-row total and snapshot"); + $node->safe_psql('postgres', "SET gp_appendonly_compaction_threshold = 100; VACUUM $table"); + is($node->safe_psql('postgres', $count_sql), '1|1', + "$orientation AO resumes accumulating remaining rows after reset"); + + # Unlike heap's rewrite, AO VACUUM FULL still runs compaction phases. + $node->safe_psql('postgres', "VACUUM FULL $table"); + is($node->safe_psql('postgres', qq{ +SELECT tuples_deleted = 1 AND compacted_segments > 0 AND dead_tuples = 0 + AND freeze_age_vacuum_count = 0 + FROM pg_stat_vacuum_tables WHERE relname = '$table'}), 't', + "$orientation AO VACUUM FULL reports compaction work without heap freezing"); +} + +$node->stop; +done_testing(); diff --git a/src/test/modules/test_misc/t/023_dead_pages.pl b/src/test/modules/test_misc/t/023_dead_pages.pl new file mode 100644 index 00000000000..4931d1a000c --- /dev/null +++ b/src/test/modules/test_misc/t/023_dead_pages.pl @@ -0,0 +1,145 @@ +# Copyright (c) 2026, PostgreSQL Global Development Group + +use strict; +use warnings FATAL => 'all'; +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +my $node = PostgreSQL::Test::Cluster->new('vacstat_dead_pages'); +$node->init; +$node->append_conf('postgresql.conf', q{ +autovacuum = off +max_parallel_maintenance_workers = 2 +min_parallel_index_scan_size = 0 +}); +$node->start; +$node->safe_psql('postgres', q{ +CREATE TABLE retained_heap (id int, padding text); +INSERT INTO retained_heap SELECT g, repeat('x', 100) FROM generate_series(1, 2000) g; +}); +my $pages = $node->safe_psql('postgres', q{ +SELECT count(DISTINCT split_part(ctid::text, ',', 1)) FROM retained_heap WHERE id <= 1000}); +my $heap_sql = q{ +SELECT dead_pages FROM pg_stat_vacuum_tables WHERE relname = 'retained_heap'}; +my $snapshot = $node->background_psql('postgres'); +$snapshot->query_safe('BEGIN ISOLATION LEVEL REPEATABLE READ; SELECT count(*) FROM retained_heap'); +$node->safe_psql('postgres', 'DELETE FROM retained_heap WHERE id <= 1000; VACUUM retained_heap'); +is($node->safe_psql('postgres', $heap_sql), $pages, + 'heap counts pages containing tuples retained for an older snapshot'); +is($node->safe_psql('postgres', q{ +SELECT recently_dead_tuples = 1000 AND missed_dead_pages = 0 + FROM pg_stat_vacuum_tables WHERE relname = 'retained_heap'}), 't', + 'retained dead pages are independent of cleanup-lock misses'); +$node->safe_psql('postgres', 'VACUUM retained_heap'); +is($node->safe_psql('postgres', $heap_sql), 2 * $pages, + 'another vacuum accumulates the same retained heap pages again'); +is($node->safe_psql('postgres', q{ +SELECT tuples_deleted, recently_dead_tuples FROM pg_stat_vacuum_tables + WHERE relname = 'retained_heap'}), '0|2000', + 'both vacuums accumulate retained tuples without reporting removals'); +$snapshot->query_safe('COMMIT'); +$snapshot->quit; +$node->safe_psql('postgres', 'VACUUM retained_heap'); +is($node->safe_psql('postgres', $heap_sql), 2 * $pages, + 'removing the retained tuples does not add dead pages'); +is($node->safe_psql('postgres', q{ +SELECT tuples_deleted, recently_dead_tuples FROM pg_stat_vacuum_tables + WHERE relname = 'retained_heap'}), '1000|2000', + 'releasing the snapshot permits removal without adding retained tuples'); + +# A cursor holds a buffer pin, forcing the scan without a cleanup lock. +for my $recent (0, 1) +{ + my $table = "pinned_$recent"; + $node->safe_psql('postgres', qq{ +CREATE TABLE $table (id int); +INSERT INTO $table SELECT generate_series(1, 20); +}); + $node->safe_psql('postgres', "DELETE FROM $table WHERE id = 20") unless $recent; + my $pin = $node->background_psql('postgres'); + $pin->query_safe("BEGIN ISOLATION LEVEL REPEATABLE READ; DECLARE c CURSOR FOR SELECT * FROM $table; FETCH 1 FROM c"); + $node->safe_psql('postgres', "DELETE FROM $table WHERE id = 20") if $recent; + $node->safe_psql('postgres', "VACUUM (TRUNCATE OFF) $table"); + is($node->safe_psql('postgres', qq{ +SELECT dead_pages, missed_dead_pages FROM pg_stat_vacuum_tables + WHERE relname = '$table'}), $recent ? '1|0' : '0|1', + "pinned page distinguishes recently-dead tuples from missed removable tuples ($recent)"); + $pin->query_safe('COMMIT'); + $pin->quit; +} + +# A snapshot taken after DELETE permits removal of heap tuples, but prevents +# recycling index pages deleted by the following VACUUM. Two indexes exercise +# serial reporting with PARALLEL 0 and Cloudberry's PARALLEL 2 fallback. +my $nrows = 20000 * ($node->safe_psql('postgres', 'SHOW block_size') / 8192); +my %index_totals; +for my $parallel (0, 2) +{ + my $table = "index_pages_$parallel"; + $node->safe_psql('postgres', qq{ +CREATE TABLE $table (id int); +CREATE INDEX ${table}_a ON $table (id); +CREATE INDEX ${table}_b ON $table ((id + 1)); +INSERT INTO $table SELECT generate_series(1, $nrows); +DELETE FROM $table WHERE id <= $nrows / 2; +}); + my $hold = $node->background_psql('postgres'); + $hold->query_safe('BEGIN ISOLATION LEVEL REPEATABLE READ; SELECT txid_current_snapshot()'); + my ($stdout, $stderr); + my $result = $node->psql('postgres', "VACUUM (VERBOSE, INDEX_CLEANUP ON, PARALLEL $parallel) $table", + stdout => \$stdout, stderr => \$stderr); + is($result, 0, "vacuum with PARALLEL $parallel succeeds"); + like($stderr, qr/disabling parallel option of vacuum.*cannot vacuum tables in parallel/, + 'Cloudberry reports the serial fallback for PARALLEL') if $parallel; + for my $suffix ('a', 'b') + { + my $index = "${table}_$suffix"; + my $deleted = $node->safe_psql('postgres', qq{ +SELECT pages_deleted FROM pg_stat_vacuum_indexes WHERE indexrelname = '$index'}); + cmp_ok($deleted, '>', 0, "$index has newly deleted pages"); + is($node->safe_psql('postgres', qq{ +SELECT dead_pages FROM pg_stat_vacuum_indexes WHERE indexrelname = '$index'}), + $deleted, "$index counts its non-reusable pages once after cleanup"); + $index_totals{$index} = 2 * $deleted; + } + $node->safe_psql('postgres', "VACUUM (INDEX_CLEANUP ON, PARALLEL $parallel) $table"); + for my $suffix ('a', 'b') + { + my $index = "${table}_$suffix"; + is($node->safe_psql('postgres', qq{ +SELECT dead_pages FROM pg_stat_vacuum_indexes WHERE indexrelname = '$index'}), + $index_totals{$index}, "$index accumulates pages still waiting for reuse"); + } + $hold->query_safe('COMMIT'); + $hold->quit; + $node->safe_psql('postgres', 'SELECT txid_current()'); + $node->safe_psql('postgres', "VACUUM (INDEX_CLEANUP ON, PARALLEL $parallel) $table"); + for my $suffix ('a', 'b') + { + my $index = "${table}_$suffix"; + is($node->safe_psql('postgres', qq{ +SELECT dead_pages FROM pg_stat_vacuum_indexes WHERE indexrelname = '$index'}), + $index_totals{$index}, "$index does not count pages that became reusable"); + } +} + +$node->restart; +is($node->safe_psql('postgres', $heap_sql), 2 * $pages, + 'heap dead-page counter survives a clean restart'); +for my $index (sort keys %index_totals) +{ + is($node->safe_psql('postgres', qq{ +SELECT dead_pages FROM pg_stat_vacuum_indexes WHERE indexrelname = '$index'}), + $index_totals{$index}, "$index dead-page counter survives a clean restart"); +} +$node->safe_psql('postgres', 'SELECT pg_stat_reset()'); +is($node->safe_psql('postgres', qq{ +SELECT dead_pages FROM pg_stat_vacuum_tables WHERE relname = 'retained_heap'}), + '0', 'reset clears heap dead pages'); +is($node->safe_psql('postgres', q{ +SELECT bool_and(dead_pages = 0) FROM pg_stat_vacuum_indexes + WHERE indexrelname LIKE 'index_pages_%'}), 't', 'reset clears index dead pages'); + +$node->stop; +done_testing(); diff --git a/src/test/modules/test_misc/t/024_ao_dead_pages.pl b/src/test/modules/test_misc/t/024_ao_dead_pages.pl new file mode 100644 index 00000000000..e5388558ba8 --- /dev/null +++ b/src/test/modules/test_misc/t/024_ao_dead_pages.pl @@ -0,0 +1,67 @@ +# Copyright (c) 2026, PostgreSQL Global Development Group + +# AO pre-cleanup and post-cleanup can both scan an index. Only the final +# state contributes dead_pages; the AO table itself has no heap dead pages. +use strict; +use warnings FATAL => 'all'; +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +my $node = PostgreSQL::Test::Cluster->new('vacstat_ao_dead_pages'); +$node->init; +$node->append_conf('postgresql.conf', q{ +autovacuum = off +gp_appendonly_compaction_threshold = 1 +}); +$node->start; +my $nrows = 20000 * ($node->safe_psql('postgres', 'SHOW block_size') / 8192); +for my $orientation ('row', 'column') +{ + my $table = "ao_pages_$orientation"; + my $index = "${table}_idx"; + $node->safe_psql('postgres', qq{ +CREATE TABLE $table (id int) WITH (appendonly = true, orientation = $orientation); +CREATE INDEX $index ON $table (id); +INSERT INTO $table SELECT generate_series(1, $nrows); +DELETE FROM $table WHERE id <= $nrows / 2; +}); + # This snapshot defers recycling the segment compacted by the first run. + my $old = $node->background_psql('postgres'); + $old->query_safe("BEGIN ISOLATION LEVEL REPEATABLE READ; SELECT count(*) FROM $table"); + $node->safe_psql('postgres', "VACUUM $table"); + is($node->safe_psql('postgres', qq{ +SELECT dead_pages = 0 AND tuples_deleted = $nrows / 2 + FROM pg_stat_vacuum_tables WHERE relname = '$table'}), 't', + "$orientation AO compaction leaves the heap-only dead-pages counter zero"); + $old->query_safe('COMMIT'); + $old->quit; + + # This later snapshot permits segment recycling, but holds back reuse + # of index pages newly deleted during the second run's pre-cleanup. + my $hold = $node->background_psql('postgres'); + $hold->query_safe('BEGIN ISOLATION LEVEL REPEATABLE READ; SELECT txid_current_snapshot()'); + $node->safe_psql('postgres', "VACUUM $table"); + my $deleted = $node->safe_psql('postgres', qq{ +SELECT pages_deleted FROM pg_stat_vacuum_indexes WHERE indexrelname = '$index'}); + cmp_ok($deleted, '>', 0, "$orientation AO recycling deletes index pages"); + my $count_sql = qq{ +SELECT dead_pages FROM pg_stat_vacuum_indexes WHERE indexrelname = '$index'}; + is($node->safe_psql('postgres', $count_sql), $deleted, + "$orientation AO counts non-reusable index pages once across both cleanup phases"); + $node->safe_psql('postgres', "VACUUM $table"); + is($node->safe_psql('postgres', $count_sql), 2 * $deleted, + "$orientation AO accumulates index pages still waiting for reuse"); + $hold->query_safe('COMMIT'); + $hold->quit; + $node->safe_psql('postgres', 'SELECT txid_current()'); + $node->safe_psql('postgres', "VACUUM $table"); + # Cloudberry's BTPageIsRecyclable deliberately permits recycling only + # for heap relations. AO index tombstones remain non-reusable even after + # the snapshot ends, and each subsequent cleanup observes them once. + is($node->safe_psql('postgres', $count_sql), 3 * $deleted, + "$orientation AO retains non-reusable index pages after the snapshot ends"); +} + +$node->stop; +done_testing(); diff --git a/src/test/modules/test_misc/t/027_freeze_age_vacuum_count.pl b/src/test/modules/test_misc/t/027_freeze_age_vacuum_count.pl new file mode 100644 index 00000000000..75521fb26a6 --- /dev/null +++ b/src/test/modules/test_misc/t/027_freeze_age_vacuum_count.pl @@ -0,0 +1,81 @@ +# Copyright (c) 2026, PostgreSQL Global Development Group + +use strict; +use warnings FATAL => 'all'; +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +my $node = PostgreSQL::Test::Cluster->new('vacstat_freeze_age'); +$node->init; +$node->append_conf('postgresql.conf', q{ +autovacuum = off +}); +$node->start; +$node->safe_psql('postgres', q{ +CREATE TABLE freeze_age_heap (id int); +INSERT INTO freeze_age_heap SELECT generate_series(1, 100); +VACUUM freeze_age_heap; +}); +my $count_sql = q{ +SELECT freeze_age_vacuum_count FROM pg_stat_vacuum_tables + WHERE relname = 'freeze_age_heap'}; +is($node->safe_psql('postgres', $count_sql), '0', + 'ordinary vacuum of a young table does not count as freeze-age driven'); + +$node->safe_psql('postgres', 'VACUUM (DISABLE_PAGE_SKIPPING) freeze_age_heap'); +is($node->safe_psql('postgres', $count_sql), '0', + 'DISABLE_PAGE_SKIPPING alone does not count despite an aggressive scan'); + +$node->safe_psql('postgres', q{ +SET vacuum_freeze_table_age = 0; +VACUUM freeze_age_heap; +}); +is($node->safe_psql('postgres', $count_sql), '1', + 'XID freeze age counts a manual vacuum'); +is($node->safe_psql('postgres', q{ +SELECT vacuum_failsafe_count FROM pg_stat_all_tables + WHERE relname = 'freeze_age_heap'}), '0', + 'a freeze-age vacuum need not activate the failsafe'); + +# safe_psql opens a new connection, restoring the default XID freeze age. +$node->safe_psql('postgres', q{ +SET vacuum_multixact_freeze_table_age = 0; +VACUUM freeze_age_heap; +}); +is($node->safe_psql('postgres', $count_sql), '2', + 'MultiXact freeze age independently counts a vacuum'); + +$node->safe_psql('postgres', 'VACUUM (FREEZE) freeze_age_heap'); +is($node->safe_psql('postgres', $count_sql), '3', + 'VACUUM FREEZE counts once even when both age conditions hold'); +$node->safe_psql('postgres', 'VACUUM (FREEZE, DISABLE_PAGE_SKIPPING) freeze_age_heap'); +is($node->safe_psql('postgres', $count_sql), '4', + 'explicit page scanning does not double count a freeze-age vacuum'); +$node->safe_psql('postgres', 'VACUUM freeze_age_heap'); +is($node->safe_psql('postgres', $count_sql), '4', + 'a later ordinary vacuum does not reuse the preceding classification'); +$node->safe_psql('postgres', 'VACUUM (FULL, FREEZE) freeze_age_heap'); +is($node->safe_psql('postgres', $count_sql), '4', + 'VACUUM FULL does not enter the lazy-vacuum counter'); + +$node->safe_psql('postgres', q{ +SET track_counts = off; +VACUUM (FREEZE) freeze_age_heap; +}); +is($node->safe_psql('postgres', $count_sql), '4', + 'disabled collection does not count a freeze-age vacuum'); +$node->restart; +is($node->safe_psql('postgres', $count_sql), '4', + 'freeze-age vacuum count survives a clean restart'); + +$node->safe_psql('postgres', q{ +SELECT pg_stat_reset_single_table_counters('freeze_age_heap'::regclass)}); +is($node->safe_psql('postgres', $count_sql), '0', + 'relation reset clears the freeze-age vacuum count'); +$node->safe_psql('postgres', 'VACUUM (FREEZE) freeze_age_heap'); +is($node->safe_psql('postgres', $count_sql), '1', + 'a freeze-age vacuum after reset starts a new total'); + +$node->stop; +done_testing(); diff --git a/src/test/modules/test_misc/t/028_vacuum_work_core.pl b/src/test/modules/test_misc/t/028_vacuum_work_core.pl new file mode 100644 index 00000000000..6e776e3657b --- /dev/null +++ b/src/test/modules/test_misc/t/028_vacuum_work_core.pl @@ -0,0 +1,97 @@ +# Copyright (c) 2026, PostgreSQL Global Development Group + +# Ordinary statistics storage must collect VACUUM work without an extension. +use strict; +use warnings FATAL => 'all'; +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +my $node = PostgreSQL::Test::Cluster->new('vacuum_work_core'); +$node->init; +$node->append_conf('postgresql.conf', 'autovacuum = off'); +$node->start; +is($node->safe_psql('postgres', 'SHOW shared_preload_libraries'), '', + 'no statistics extension is preloaded'); +$node->safe_psql('postgres', q{ +CREATE TABLE core_work (id int PRIMARY KEY); +INSERT INTO core_work SELECT generate_series(1, 2000); +DELETE FROM core_work WHERE id <= 1000; +SELECT pg_stat_force_next_flush(); +}); +my $db_sql = q{SELECT tuples_deleted FROM pg_stat_vacuum WHERE datname = current_database()}; +my $before = $node->safe_psql('postgres', $db_sql); +$node->safe_psql('postgres', q{ +VACUUM (INDEX_CLEANUP ON) core_work; +SELECT pg_stat_force_next_flush(); +}); +is($node->safe_psql('postgres', q{ +SELECT tuples_deleted FROM pg_stat_vacuum_tables WHERE relname = 'core_work' +}), '1000', 'core reports heap tuples removed'); +is($node->safe_psql('postgres', q{ +SELECT tuples_deleted FROM pg_stat_vacuum_indexes WHERE indexrelname = 'core_work_pkey' +}), '1000', 'core separately reports index entries removed'); +$node->poll_query_until('postgres', "SELECT tuples_deleted = " . ($before + 1000) . " FROM pg_stat_vacuum WHERE datname = current_database()") + or die 'database vacuum work did not flush'; +is($node->safe_psql('postgres', $db_sql), $before + 1000, + 'database counts table work once, excluding index entries'); +my $saved = $node->safe_psql('postgres', q{ +SELECT pg_stat_get_vacuum_stats('core_work'::regclass)::text +}); +$node->restart; +is($node->safe_psql('postgres', q{ +SELECT pg_stat_get_vacuum_stats('core_work'::regclass)::text +}), $saved, 'ordinary statistics persistence preserves vacuum work'); +$node->safe_psql('postgres', q{ +SELECT pg_stat_reset_single_table_counters('core_work'::regclass); +}); +is($node->safe_psql('postgres', q{ +SELECT tuples_deleted = 0 AND pages_scanned = 0 AND pages_all_visible = 0 +FROM pg_stat_vacuum_tables WHERE relname = 'core_work' +}), 't', 'ordinary relation reset clears core vacuum counters'); +is($node->safe_psql('postgres', $db_sql), $before + 1000, + 'relation reset leaves database totals intact'); +$node->safe_psql('postgres', 'SELECT pg_stat_reset()'); +is($node->safe_psql('postgres', $db_sql), '0', + 'ordinary database reset clears database vacuum work'); + +# Dropping a relation discards its native work counters transactionally. +my $relid = $node->safe_psql('postgres', "SELECT 'core_work'::regclass::oid"); +$node->safe_psql('postgres', 'VACUUM (FREEZE) core_work'); +$node->safe_psql('postgres', 'BEGIN; DROP TABLE core_work; ROLLBACK;'); +is($node->safe_psql('postgres', q{ +SELECT freeze_age_vacuum_count FROM pg_stat_vacuum_tables WHERE relname = 'core_work'}), + '1', 'rolled-back DROP preserves core counters'); +$node->safe_psql('postgres', 'DROP TABLE core_work'); +is($node->safe_psql('postgres', "SELECT tuples_deleted = 0 AND freeze_age_vacuum_count = 0 FROM pg_stat_get_vacuum_stats($relid)"), + 't', 'committed DROP removes core vacuum statistics'); + +# Simulate the zero page that can remain after a crash during index extension. +# It is reusable before vacuum starts, so discovering it is not deletion work. +$node->safe_psql('postgres', q{ +CREATE TABLE spg_pages (p point); +CREATE INDEX spg_pages_idx ON spg_pages USING spgist (p); +INSERT INTO spg_pages SELECT point(g, g) FROM generate_series(1, 10) g; +}); +my $index_path = $node->safe_psql('postgres', q{ +SELECT pg_relation_filepath('spg_pages_idx')}); +my $block_size = $node->safe_psql('postgres', 'SHOW block_size'); +$node->stop; +append_to_file($node->data_dir . '/' . $index_path, "\0" x $block_size); +$node->start; +$node->safe_psql('postgres', 'VACUUM (INDEX_CLEANUP ON) spg_pages'); +my $spg_sql = q{ +SELECT pages_deleted FROM pg_stat_vacuum_indexes + WHERE indexrelname = 'spg_pages_idx'}; +my $deleted = $node->safe_psql('postgres', $spg_sql); +is($deleted, '0', 'SP-GiST does not count a pre-existing zero page as newly deleted'); +$node->safe_psql('postgres', 'VACUUM (INDEX_CLEANUP ON, DISABLE_PAGE_SKIPPING) spg_pages'); +is($node->safe_psql('postgres', $spg_sql), $deleted, + 'SP-GiST rescan does not count previously emptied pages again'); +is($node->safe_psql('postgres', q{ +SELECT dead_pages FROM pg_stat_vacuum_indexes + WHERE indexrelname = 'spg_pages_idx'}), '0', + 'reusable SP-GiST pages are not counted as dead pages'); + +$node->stop; +done_testing(); diff --git a/src/test/modules/test_misc/t/029_vacuum_resource_verbose.pl b/src/test/modules/test_misc/t/029_vacuum_resource_verbose.pl new file mode 100644 index 00000000000..94e5a413553 --- /dev/null +++ b/src/test/modules/test_misc/t/029_vacuum_resource_verbose.pl @@ -0,0 +1,75 @@ +# Copyright (c) 2026, PostgreSQL Global Development Group + +# Resource instrumentation has a core consumer even without an extension. +use strict; +use warnings FATAL => 'all'; +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +my $node = PostgreSQL::Test::Cluster->new('vacuum_resource_verbose'); +$node->init; +$node->append_conf('postgresql.conf', q{ +autovacuum = off +track_io_timing = on +gp_appendonly_compaction_threshold = 1 +}); +$node->start; +is($node->safe_psql('postgres', 'SHOW shared_preload_libraries'), '', + 'VERBOSE resource reporting requires no preloaded extension'); + +for my $storage ('heap', 'row', 'column') +{ + my $table = "verbose_$storage"; + my $options = $storage eq 'heap' ? '' : + "WITH (appendonly = true, orientation = $storage)"; + $node->safe_psql('postgres', qq{ +CREATE TABLE $table (id int, padding text) $options; +CREATE INDEX ${table}_idx ON $table (id); +INSERT INTO $table SELECT g, repeat('x', 100) FROM generate_series(1, 4000) g; +DELETE FROM $table WHERE id <= 1000; +}); + my ($stdout, $stderr); + is($node->psql('postgres', "VACUUM $table", + stdout => \$stdout, stderr => \$stderr), 0, + "$storage plain vacuum succeeds"); + unlike($stderr, qr/vacuum resource usage for/, + "$storage plain vacuum does not emit resource reports"); + $node->safe_psql('postgres', "DELETE FROM $table WHERE id <= 2000"); + is($node->psql('postgres', "VACUUM (VERBOSE, INDEX_CLEANUP ON) $table", + stdout => \$stdout, stderr => \$stderr), 0, + "$storage verbose vacuum succeeds"); + my @phases = $storage eq 'heap' ? ('heap (excluding indexes)') : + ('AO pre-cleanup (excluding indexes)', 'AO compaction (excluding indexes)', + 'AO post-cleanup (excluding indexes)'); + for my $phase (@phases) + { + my ($report) = $stderr =~ + /vacuum resource usage for "public\.\Q$table\E" \(\Q$phase\E\):\n(.*?)(?=\n(?:INFO|CONTEXT|WARNING):|\z)/s; + ok(defined $report, "$storage reports $phase without an extension"); + $report //= ''; + like($report, qr/buffer usage: \d+ hits, \d+ misses, \d+ dirtied, \d+ written/, + "$phase reports nonnegative buffer counters"); + like($report, qr/WAL usage: \d+ records, \d+ full page images, \d+ bytes/, + "$phase reports WAL counters"); + like($report, qr/I\/O timings: read: \d+\.\d+ ms, write: \d+\.\d+ ms/, + "$phase reports I/O times in milliseconds"); + like($report, qr/WAL usage: [1-9]\d* records/, + 'compaction produces WAL') if $phase eq 'AO compaction (excluding indexes)'; + } + my $index_phase = $storage eq 'heap' ? 'index bulk delete' : 'AO index vacuum'; + like($stderr, qr/vacuum resource usage for "public\.\Q${table}_idx\E" \(\Q$index_phase\E\):/, + "$storage reports index resources separately"); +} + +# Timing output is conditional, while collection of buffer/WAL counters is not. +my ($stdout, $stderr); +is($node->psql('postgres', + 'SET track_io_timing = off; VACUUM (VERBOSE) verbose_heap', + stdout => \$stdout, stderr => \$stderr), 0, 'verbose vacuum with I/O timing disabled'); +like($stderr, qr/vacuum resource usage for "public\.verbose_heap"/, + 'resource report remains available without I/O timing'); +unlike($stderr, qr/I\/O timings:/, 'disabled I/O timing is not printed'); + +$node->stop; +done_testing(); diff --git a/src/test/modules/test_misc/t/030_vacuum_work_reset.pl b/src/test/modules/test_misc/t/030_vacuum_work_reset.pl new file mode 100644 index 00000000000..a5b19ac3e1b --- /dev/null +++ b/src/test/modules/test_misc/t/030_vacuum_work_reset.pl @@ -0,0 +1,132 @@ +# Copyright (c) 2026, PostgreSQL Global Development Group + +# VACUUM work has a native reset that does not clear other statistics. +use strict; +use warnings FATAL => 'all'; +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +my $node = PostgreSQL::Test::Cluster->new('vacuum_work_reset'); +$node->init; +$node->append_conf('postgresql.conf', 'autovacuum = off'); +$node->start; +$node->safe_psql('postgres', 'CREATE DATABASE other_work'); +for my $db ('postgres', 'other_work') +{ + $node->safe_psql($db, q{ +CREATE TABLE reset_work (id int PRIMARY KEY); +INSERT INTO reset_work SELECT generate_series(1, 1000); +DELETE FROM reset_work WHERE id <= 100; +ANALYZE reset_work; +VACUUM (FREEZE) reset_work; +SELECT pg_stat_force_next_flush(); +}); +} +my $work_sql = q{ +SELECT row_to_json(s) FROM pg_stat_get_vacuum_stats('reset_work'::regclass) s +}; +my $db_work_sql = q{ +SELECT row_to_json(s) FROM pg_stat_get_vacuum_database_stats( + (SELECT oid FROM pg_database WHERE datname = current_database())) s +}; +my $other_sql = q{ +SELECT n_tup_ins, n_tup_del, n_mod_since_analyze, vacuum_count, analyze_count, + last_vacuum, last_analyze, total_vacuum_time, total_vacuum_delay_time + FROM pg_stat_all_tables WHERE relname = 'reset_work' +}; +my $zero_sql = q{ +SELECT bool_and(value::bigint = 0) + FROM pg_stat_get_vacuum_stats('reset_work'::regclass) s, + LATERAL jsonb_each_text(to_jsonb(s)) +}; +my $before = $node->safe_psql('postgres', $work_sql); +my $other_db_before = $node->safe_psql('other_work', $work_sql); +my $db_before = $node->safe_psql('postgres', $db_work_sql); +my $other_before = $node->safe_psql('postgres', $other_sql); + +$node->safe_psql('postgres', 'CREATE ROLE work_reader'); +for my $call ('pg_stat_reset_vacuum_stats()', + q[pg_stat_reset_vacuum_stats('reset_work'::regclass)], + 'gp_stat_reset_vacuum_stats()') +{ + my ($stdout, $stderr); + my $ret = $node->psql('postgres', "SET ROLE work_reader; SELECT $call", + stdout => \$stdout, stderr => \$stderr); + isnt($ret, 0, "$call denied without reset privilege"); + like($stderr, qr/permission denied for function/, 'function ACL protects reset'); +} +is($node->safe_psql('postgres', $work_sql), $before, 'denied reset preserves work'); +$node->safe_psql('postgres', q{ +SELECT pg_stat_reset_vacuum_stats(0); +SELECT pg_stat_reset_vacuum_stats(4294967294); +}); +is($node->safe_psql('postgres', $work_sql), $before, + 'missing relation and OID zero do not reset the database'); + +$node->safe_psql('postgres', q{ +GRANT EXECUTE ON FUNCTION pg_stat_reset_vacuum_stats(oid) TO work_reader; +SET ROLE work_reader; +SELECT pg_stat_reset_vacuum_stats('reset_work'::regclass); +}); +is($node->safe_psql('postgres', $zero_sql), 't', 'relation reset clears every work field'); +is($node->safe_psql('postgres', $other_sql), $other_before, + 'relation reset preserves timing, invocation counts and other statistics'); +is($node->safe_psql('postgres', $db_work_sql), $db_before, + 'relation reset preserves database work aggregate'); +is($node->safe_psql('postgres', q{ +SELECT tuples_deleted FROM pg_stat_get_vacuum_stats('reset_work_pkey'::regclass) +}), '100', 'relation reset leaves index work untouched'); +is($node->safe_psql('other_work', $work_sql), $other_db_before, + 'relation reset leaves other databases untouched'); + +$node->safe_psql('postgres', q{ +SELECT pg_stat_reset_vacuum_stats('reset_work_pkey'::regclass); +VACUUM (DISABLE_PAGE_SKIPPING) reset_work; +VACUUM pg_shdepend; +SELECT pg_stat_force_next_flush(); +}); +is($node->safe_psql('postgres', q{ +SELECT pages_scanned > 0 FROM pg_stat_get_vacuum_stats('reset_work'::regclass) +}), 't', 'work accumulation resumes after reset'); +my $shared_sql = q{ +SELECT row_to_json(s) FROM pg_stat_get_vacuum_stats('pg_shdepend'::regclass) s +}; +my $shared_before = $node->safe_psql('postgres', $shared_sql); +$other_before = $node->safe_psql('postgres', $other_sql); +$node->safe_psql('postgres', 'SELECT pg_stat_reset_vacuum_stats()'); +is($node->safe_psql('postgres', $zero_sql), 't', 'database reset clears relation work'); +is($node->safe_psql('postgres', q{ +SELECT bool_and(value::bigint = 0) + FROM pg_stat_get_vacuum_database_stats( + (SELECT oid FROM pg_database WHERE datname = current_database())) s, + LATERAL jsonb_each_text(to_jsonb(s)) +}), 't', 'database reset clears every database work field'); +is($node->safe_psql('postgres', $other_sql), $other_before, + 'database reset preserves other relation statistics'); +is($node->safe_psql('postgres', $shared_sql), $shared_before, + 'database reset leaves shared catalogs untouched'); +is($node->safe_psql('other_work', $work_sql), $other_db_before, + 'database reset leaves another database untouched'); +$node->safe_psql('postgres', + q[SELECT pg_stat_reset_vacuum_stats('pg_shdepend'::regclass)]); +is($node->safe_psql('other_work', q{ +SELECT pages_scanned FROM pg_stat_get_vacuum_stats('pg_shdepend'::regclass) +}), '0', 'explicit shared-catalog reset is visible from another database'); + +# A database reset must not republish this backend's pre-reset pending work. +$node->safe_psql('postgres', q{ +VACUUM (DISABLE_PAGE_SKIPPING) reset_work; +SELECT pg_stat_reset_vacuum_stats(NULL); +SELECT pg_stat_force_next_flush(); +}); +is($node->safe_psql('postgres', q{ +SELECT pages_scanned FROM pg_stat_get_vacuum_database_stats( + (SELECT oid FROM pg_database WHERE datname = current_database())) +}), '0', 'reset discards local pending database work before a later flush'); +$node->restart; +is($node->safe_psql('postgres', $zero_sql), 't', 'work stays reset after a clean restart'); +is($node->safe_psql('other_work', $work_sql), $other_db_before, + 'untouched database history survives restart'); +$node->stop; +done_testing(); diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out index 614ffea1784..3f9746a2d20 100644 --- a/src/test/regress/expected/rules.out +++ b/src/test/regress/expected/rules.out @@ -1798,7 +1798,16 @@ pg_stat_all_tables| SELECT c.oid AS relid, pg_stat_get_vacuum_count(c.oid) AS vacuum_count, pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count, pg_stat_get_analyze_count(c.oid) AS analyze_count, - pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count + pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count, + pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time, + pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time, + pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time, + pg_stat_get_total_autoanalyze_time(c.oid) AS total_autoanalyze_time, + pg_stat_get_total_vacuum_delay_time(c.oid) AS total_vacuum_delay_time, + pg_stat_get_total_autovacuum_delay_time(c.oid) AS total_autovacuum_delay_time, + pg_stat_get_vacuum_failsafe_count(c.oid) AS vacuum_failsafe_count, + 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))) @@ -1916,7 +1925,8 @@ pg_stat_progress_analyze| SELECT d.gp_segment_id, s.param5 AS ext_stats_computed, s.param6 AS child_tables_total, s.param7 AS child_tables_done, - (s.param8)::oid AS current_child_table_relid + (s.param8)::oid AS current_child_table_relid, + ((s.param9)::double precision / (1000000)::double precision) AS delay_time FROM (pg_stat_get_progress_info('ANALYZE'::text) s(pid, datid, relid, param1, param2, param3, param4, param5, param6, param7, param8, param9, param10, param11, param12, param13, param14, param15, param16, param17, param18, param19, param20) LEFT JOIN pg_database d ON ((s.datid = d.oid))); pg_stat_progress_basebackup| SELECT gp_execution_segment() AS gp_segment_id, @@ -2043,7 +2053,8 @@ pg_stat_progress_vacuum| SELECT d.gp_segment_id, s.param4 AS heap_blks_vacuumed, s.param5 AS index_vacuum_count, s.param6 AS max_dead_tuples, - s.param7 AS num_dead_tuples + s.param7 AS num_dead_tuples, + ((s.param8)::double precision / (1000000)::double precision) AS delay_time FROM (pg_stat_get_progress_info('VACUUM'::text) s(pid, datid, relid, param1, param2, param3, param4, param5, param6, param7, param8, param9, param10, param11, param12, param13, param14, param15, param16, param17, param18, param19, param20) LEFT JOIN pg_database d ON ((s.datid = d.oid))); pg_stat_recovery_prefetch| SELECT stats_reset, @@ -2168,7 +2179,16 @@ pg_stat_sys_tables| SELECT relid, vacuum_count, autovacuum_count, analyze_count, - autoanalyze_count + autoanalyze_count, + total_vacuum_time, + total_autovacuum_time, + total_analyze_time, + total_autoanalyze_time, + total_vacuum_delay_time, + total_autovacuum_delay_time, + vacuum_failsafe_count, + 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, @@ -2216,9 +2236,81 @@ pg_stat_user_tables| SELECT relid, vacuum_count, autovacuum_count, analyze_count, - autoanalyze_count + autoanalyze_count, + total_vacuum_time, + total_autovacuum_time, + total_analyze_time, + total_autoanalyze_time, + total_vacuum_delay_time, + total_autovacuum_delay_time, + vacuum_failsafe_count, + 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_vacuum| SELECT d.oid AS datid, + d.datname, + pg_stat_get_db_total_vacuum_time(d.oid) AS total_vacuum_time, + pg_stat_get_db_total_autovacuum_time(d.oid) AS total_autovacuum_time, + pg_stat_get_db_total_vacuum_delay_time(d.oid) AS total_vacuum_delay_time, + pg_stat_get_db_total_autovacuum_delay_time(d.oid) AS total_autovacuum_delay_time, + pg_stat_get_db_vacuum_failsafe_count(d.oid) AS vacuum_failsafe_count, + pg_stat_get_db_vacuum_interrupt_count(d.oid) AS vacuum_interrupt_count, + pg_stat_get_db_stat_reset_time(d.oid) AS stats_reset, + v.tuples_deleted, + v.dead_pages, + v.pages_frozen, + v.pages_all_visible, + v.freeze_age_vacuum_count, + v.tuples_frozen, + v.recently_dead_tuples, + v.missed_dead_tuples, + v.pages_scanned, + v.pages_removed, + v.missed_dead_pages, + v.compacted_segments, + v.tuples_moved + FROM ( SELECT 0 AS oid, + NULL::name AS datname + UNION ALL + SELECT pg_database.oid, + pg_database.datname + FROM pg_database) d, + LATERAL pg_stat_get_vacuum_database_stats(d.oid) v(tuples_deleted, dead_pages, pages_frozen, pages_all_visible, freeze_age_vacuum_count, tuples_frozen, recently_dead_tuples, missed_dead_tuples, pages_scanned, pages_removed, missed_dead_pages, compacted_segments, tuples_moved); +pg_stat_vacuum_indexes| SELECT c.oid AS indexrelid, + n.nspname AS schema, + c.relname AS indexrelname, + current_database() AS dbname, + v.tuples_deleted, + v.pages_deleted, + v.dead_pages + FROM (pg_class c + JOIN pg_namespace n ON ((n.oid = c.relnamespace))), + LATERAL pg_stat_get_vacuum_stats(c.oid) v(tuples_deleted, dead_tuples, pages_deleted, dead_pages, pages_frozen, pages_all_visible, freeze_age_vacuum_count, tuples_frozen, recently_dead_tuples, missed_dead_tuples, pages_scanned, pages_removed, missed_dead_pages, total_file_segs, compacted_segments, tuples_moved) + WHERE (c.relkind = 'i'::"char"); +pg_stat_vacuum_tables| SELECT c.oid AS relid, + n.nspname AS schema, + c.relname, + current_database() AS dbname, + v.tuples_deleted, + v.dead_tuples, + v.dead_pages, + v.pages_frozen, + v.pages_all_visible, + v.freeze_age_vacuum_count, + v.tuples_frozen, + v.recently_dead_tuples, + v.missed_dead_tuples, + v.pages_scanned, + v.pages_removed, + v.missed_dead_pages, + v.total_file_segs, + v.compacted_segments, + v.tuples_moved + FROM (pg_class c + JOIN pg_namespace n ON ((n.oid = c.relnamespace))), + LATERAL pg_stat_get_vacuum_stats(c.oid) v(tuples_deleted, dead_tuples, pages_deleted, dead_pages, pages_frozen, pages_all_visible, freeze_age_vacuum_count, tuples_frozen, recently_dead_tuples, missed_dead_tuples, pages_scanned, pages_removed, missed_dead_pages, total_file_segs, compacted_segments, tuples_moved) + 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 index 573f7527fde..be30c445eaa 100644 --- a/src/test/regress/expected/vacuum_stats.out +++ b/src/test/regress/expected/vacuum_stats.out @@ -28,4 +28,323 @@ AND relallvisible = t (1 row) -DROP TABLE vacstat_test +DROP TABLE vacstat_test; +-- relallfrozen follows segment catalogs for dist tables. +CREATE TABLE frozen_stats_dist (a int) DISTRIBUTED BY (a); +INSERT INTO frozen_stats_dist SELECT generate_series(1, 2000); +ANALYZE frozen_stats_dist; +VACUUM (FREEZE) frozen_stats_dist; +SELECT relallfrozen > 0 AND relallfrozen <= relallvisible + AND relallfrozen = (SELECT sum(relallfrozen) FROM gp_dist_random('pg_class') + WHERE oid = 'frozen_stats_dist'::regclass) + AND relallvisible = (SELECT sum(relallvisible) FROM gp_dist_random('pg_class') + WHERE oid = 'frozen_stats_dist'::regclass) + AS frozen_stats_match + FROM pg_class WHERE oid = 'frozen_stats_dist'::regclass; + frozen_stats_match +-------------------- + t +(1 row) + +UPDATE frozen_stats_dist SET a = a + 10000 WHERE a = 1; +ANALYZE frozen_stats_dist; +SELECT relallfrozen > 0 AND relallfrozen <= relallvisible + AND relallfrozen = (SELECT sum(relallfrozen) FROM gp_dist_random('pg_class') + WHERE oid = 'frozen_stats_dist'::regclass) + AND relallvisible = (SELECT sum(relallvisible) FROM gp_dist_random('pg_class') + WHERE oid = 'frozen_stats_dist'::regclass) + AS frozen_stats_match + FROM pg_class WHERE oid = 'frozen_stats_dist'::regclass; + frozen_stats_match +-------------------- + t +(1 row) + +VACUUM (FULL) frozen_stats_dist; +SELECT relallfrozen = 0 AND relallvisible = 0 AS rewrite_clears_map + FROM pg_class WHERE oid = 'frozen_stats_dist'::regclass; + rewrite_clears_map +-------------------- + t +(1 row) + +DROP TABLE frozen_stats_dist; +-- relallfrozen follows segment catalogs for repl tables. +CREATE TABLE frozen_stats_repl (a int) DISTRIBUTED REPLICATED; +INSERT INTO frozen_stats_repl SELECT generate_series(1, 2000); +ANALYZE frozen_stats_repl; +VACUUM (FREEZE) frozen_stats_repl; +SELECT relallfrozen > 0 AND relallfrozen <= relallvisible + AND relallfrozen = (SELECT sum(relallfrozen) FROM gp_dist_random('pg_class') + WHERE oid = 'frozen_stats_repl'::regclass) / (SELECT numsegments FROM gp_distribution_policy WHERE localoid = 'frozen_stats_repl'::regclass) + AND relallvisible = (SELECT sum(relallvisible) FROM gp_dist_random('pg_class') + WHERE oid = 'frozen_stats_repl'::regclass) / (SELECT numsegments FROM gp_distribution_policy WHERE localoid = 'frozen_stats_repl'::regclass) + AS frozen_stats_match + FROM pg_class WHERE oid = 'frozen_stats_repl'::regclass; + frozen_stats_match +-------------------- + t +(1 row) + +UPDATE frozen_stats_repl SET a = a + 10000 WHERE a = 1; +ANALYZE frozen_stats_repl; +SELECT relallfrozen > 0 AND relallfrozen <= relallvisible + AND relallfrozen = (SELECT sum(relallfrozen) FROM gp_dist_random('pg_class') + WHERE oid = 'frozen_stats_repl'::regclass) / (SELECT numsegments FROM gp_distribution_policy WHERE localoid = 'frozen_stats_repl'::regclass) + AND relallvisible = (SELECT sum(relallvisible) FROM gp_dist_random('pg_class') + WHERE oid = 'frozen_stats_repl'::regclass) / (SELECT numsegments FROM gp_distribution_policy WHERE localoid = 'frozen_stats_repl'::regclass) + AS frozen_stats_match + FROM pg_class WHERE oid = 'frozen_stats_repl'::regclass; + frozen_stats_match +-------------------- + t +(1 row) + +VACUUM (FULL) frozen_stats_repl; +SELECT relallfrozen = 0 AND relallvisible = 0 AS rewrite_clears_map + FROM pg_class WHERE oid = 'frozen_stats_repl'::regclass; + rewrite_clears_map +-------------------- + t +(1 row) + +DROP TABLE frozen_stats_repl; +-- Core GUC regression: reconnect with timing enabled at session startup. +-- Segment processes must keep the default off across coordinator startup +-- options, SET, and ROLLBACK. All three checks expect t. +\connect -reuse-previous=on "options='-c track_cost_delay_timing=on'" +SELECT bool_and(setting = 'off') AS segment_timing_local + FROM gp_dist_random('pg_settings') WHERE name = 'track_cost_delay_timing'; + segment_timing_local +---------------------- + t +(1 row) + +BEGIN; +SET track_cost_delay_timing = off; +SELECT bool_and(setting = 'off') AS segment_timing_local + FROM gp_dist_random('pg_settings') WHERE name = 'track_cost_delay_timing'; + segment_timing_local +---------------------- + t +(1 row) + +ROLLBACK; +SELECT bool_and(setting = 'off') AS segment_timing_local + FROM gp_dist_random('pg_settings') WHERE name = 'track_cost_delay_timing'; + segment_timing_local +---------------------- + t +(1 row) + +RESET track_cost_delay_timing; +-- Core index-summary regression: two indexes on the same table must give +-- exactly two summary rows. Joining by table OID alone used to multiply +-- rows and mix index timings; compare each index with its own segment sum. +CREATE TABLE core_summary_test (a int, b int) DISTRIBUTED BY (a); +CREATE INDEX core_summary_a ON core_summary_test(a); +CREATE INDEX core_summary_b ON core_summary_test(b); +INSERT INTO core_summary_test SELECT g, g FROM generate_series(1, 10000) g; +DELETE FROM core_summary_test WHERE a % 2 = 0; +CREATE TABLE core_timing_repl (a int) DISTRIBUTED REPLICATED; +INSERT INTO core_timing_repl SELECT generate_series(1, 1000); +DELETE FROM core_timing_repl; +VACUUM core_summary_test, core_timing_repl; +SELECT count(*) AS summary_indexes, + count(DISTINCT indexrelid) AS distinct_indexes + FROM gp_stat_all_indexes_summary WHERE relname = 'core_summary_test'; + summary_indexes | distinct_indexes +-----------------+------------------ + 2 | 2 +(1 row) + +SELECT bool_and(s.total_vacuum_time = d.total_vacuum_time) AS index_totals_match + FROM gp_stat_all_indexes_summary s + JOIN (SELECT indexrelid, sum(total_vacuum_time) AS total_vacuum_time + FROM gp_dist_random('pg_stat_all_indexes') + WHERE relname = 'core_summary_test' GROUP BY indexrelid) d + USING (indexrelid) + WHERE s.relname = 'core_summary_test'; + index_totals_match +-------------------- + t +(1 row) + +-- Native timing summaries work without a statistics extension. +SELECT count(*) = 2 AND bool_and(total_vacuum_time > 0) AS tables_vacuum_timed + FROM gp_stat_all_tables_summary + WHERE relname IN ('core_summary_test', 'core_timing_repl'); + tables_vacuum_timed +--------------------- + t +(1 row) + +-- Flush database timing before checking its cluster aggregate. +SELECT gp_stat_force_next_flush(); + gp_stat_force_next_flush +-------------------------- + +(1 row) + +SELECT total_vacuum_time > 0 AS database_vacuum_timed + FROM gp_stat_vacuum_summary + WHERE datname = current_database(); + database_vacuum_timed +----------------------- + t +(1 row) + +DROP TABLE core_summary_test, core_timing_repl; +-- Native work counters and cluster summaries need no statistics extension. +CREATE TABLE work_stats_dist (a int PRIMARY KEY) DISTRIBUTED BY (a); +CREATE TABLE work_stats_repl (a int) DISTRIBUTED REPLICATED; +INSERT INTO work_stats_dist SELECT generate_series(1, 2000); +ANALYZE work_stats_dist; +INSERT INTO work_stats_repl SELECT generate_series(1, 2000); +ANALYZE work_stats_repl; +VACUUM (FREEZE) work_stats_dist, work_stats_repl; +SELECT relname, freeze_age_vacuum_count = + CASE WHEN relname = 'work_stats_repl' THEN 1 + ELSE (SELECT numsegments FROM gp_distribution_policy + WHERE localoid = 'work_stats_dist'::regclass) END AS freeze_count_matches, + pages_frozen > 0 AND pages_all_visible > 0 AS page_work_recorded + FROM gp_stat_vacuum_tables_summary + WHERE relname IN ('work_stats_dist', 'work_stats_repl') ORDER BY relname; + relname | freeze_count_matches | page_work_recorded +-----------------+----------------------+-------------------- + work_stats_dist | t | t + work_stats_repl | t | t +(2 rows) + +SELECT count(*) = 2 * (SELECT numsegments FROM gp_distribution_policy + WHERE localoid = 'work_stats_dist'::regclass) + AND bool_and(freeze_age_vacuum_count = 1) AS per_segment_counts + FROM gp_stat_vacuum_tables + WHERE relname IN ('work_stats_dist', 'work_stats_repl') AND gp_segment_id >= 0; + per_segment_counts +-------------------- + t +(1 row) + +DELETE FROM work_stats_dist WHERE a <= 100; +DELETE FROM work_stats_repl WHERE a <= 100; +VACUUM work_stats_dist, work_stats_repl; +SELECT relname, tuples_deleted = 100 AS deleted_matches + FROM gp_stat_vacuum_tables_summary + WHERE relname IN ('work_stats_dist', 'work_stats_repl') ORDER BY relname; + relname | deleted_matches +-----------------+----------------- + work_stats_dist | t + work_stats_repl | t +(2 rows) + +SELECT tuples_deleted = 100 AS index_work_matches + FROM gp_stat_vacuum_indexes_summary WHERE indexrelname = 'work_stats_dist_pkey'; + index_work_matches +-------------------- + t +(1 row) + +-- Native work-only resets preserve other counters and relation scope. +CREATE TEMP TABLE work_other_before AS + SELECT relid, sum(n_tup_ins) AS n_tup_ins, sum(n_tup_del) AS n_tup_del, + sum(vacuum_count) AS vacuum_count, sum(analyze_count) AS analyze_count, + sum(total_vacuum_time) AS total_vacuum_time + FROM gp_stat_all_tables WHERE relname = 'work_stats_dist' GROUP BY relid + DISTRIBUTED BY (relid); +SELECT gp_stat_reset_vacuum_stats('work_stats_dist'::regclass); + gp_stat_reset_vacuum_stats +---------------------------- + +(1 row) + +SELECT count(*) = (SELECT count(*) FROM gp_segment_configuration WHERE role = 'p') + AND bool_and(tuples_deleted = 0 AND pages_scanned = 0 + AND pages_frozen = 0 AND freeze_age_vacuum_count = 0) + AS work_reset_on_all_instances + FROM gp_stat_vacuum_tables WHERE relname = 'work_stats_dist'; + work_reset_on_all_instances +----------------------------- + t +(1 row) + +SELECT tuples_deleted = 100 AS other_relation_preserved + FROM gp_stat_vacuum_tables_summary WHERE relname = 'work_stats_repl'; + other_relation_preserved +-------------------------- + t +(1 row) + +SELECT tuples_deleted = 100 AS index_preserved + FROM gp_stat_vacuum_indexes_summary WHERE indexrelname = 'work_stats_dist_pkey'; + index_preserved +----------------- + t +(1 row) + +SELECT s.n_tup_ins = b.n_tup_ins AND s.n_tup_del = b.n_tup_del + AND s.vacuum_count = b.vacuum_count AND s.analyze_count = b.analyze_count + AND s.total_vacuum_time = b.total_vacuum_time AS other_counters_preserved + FROM (SELECT relid, sum(n_tup_ins) AS n_tup_ins, sum(n_tup_del) AS n_tup_del, + sum(vacuum_count) AS vacuum_count, sum(analyze_count) AS analyze_count, + sum(total_vacuum_time) AS total_vacuum_time + FROM gp_stat_all_tables WHERE relname = 'work_stats_dist' GROUP BY relid) s + JOIN work_other_before b USING (relid); + other_counters_preserved +-------------------------- + t +(1 row) + +DROP TABLE work_other_before; +DROP TABLE work_stats_dist, work_stats_repl; +CREATE TABLE work_stats_ao (a int) WITH (appendonly = true) DISTRIBUTED BY (a); +CREATE TABLE work_stats_aoco (a int) WITH (appendonly = true, orientation = column) DISTRIBUTED BY (a); +INSERT INTO work_stats_ao SELECT generate_series(1, 2000); +ANALYZE work_stats_ao; +INSERT INTO work_stats_aoco SELECT generate_series(1, 2000); +ANALYZE work_stats_aoco; +DELETE FROM work_stats_ao WHERE a <= 1000; +DELETE FROM work_stats_aoco WHERE a <= 1000; +VACUUM work_stats_ao, work_stats_aoco; +SELECT relname, tuples_deleted = 1000 AND tuples_moved = 1000 + AND compacted_segments > 0 AND pages_scanned > 0 AS compaction_matches + FROM gp_stat_vacuum_tables_summary + WHERE relname IN ('work_stats_ao', 'work_stats_aoco') ORDER BY relname; + relname | compaction_matches +-----------------+-------------------- + work_stats_ao | t + work_stats_aoco | t +(2 rows) + +-- The database form clears AO work and aggregates on all instances. +SELECT gp_stat_force_next_flush(); + gp_stat_force_next_flush +-------------------------- + +(1 row) + +SELECT gp_stat_reset_vacuum_stats(); + gp_stat_reset_vacuum_stats +---------------------------- + +(1 row) + +SELECT bool_and(tuples_deleted = 0 AND tuples_moved = 0 + AND compacted_segments = 0 AND pages_scanned = 0 + AND total_file_segs = 0) AS ao_work_reset + FROM gp_stat_vacuum_tables + WHERE relname IN ('work_stats_ao', 'work_stats_aoco'); + ao_work_reset +--------------- + t +(1 row) + +SELECT bool_and(tuples_deleted = 0 AND pages_scanned = 0 + AND pages_frozen = 0 AND compacted_segments = 0) AS database_work_reset + FROM gp_stat_vacuum WHERE datname = current_database(); + database_work_reset +--------------------- + t +(1 row) + +DROP TABLE work_stats_ao, work_stats_aoco; diff --git a/src/test/regress/sql/vacuum_stats.sql b/src/test/regress/sql/vacuum_stats.sql index 45a9b6466c4..01caaf3a0e1 100644 --- a/src/test/regress/sql/vacuum_stats.sql +++ b/src/test/regress/sql/vacuum_stats.sql @@ -19,4 +19,186 @@ AND relallvisible = (SELECT SUM(relallvisible) FROM gp_dist_random('pg_class') WHERE oid='vacstat_test'::regclass); -DROP TABLE vacstat_test +DROP TABLE vacstat_test; + +-- relallfrozen follows segment catalogs for dist tables. +CREATE TABLE frozen_stats_dist (a int) DISTRIBUTED BY (a); +INSERT INTO frozen_stats_dist SELECT generate_series(1, 2000); +ANALYZE frozen_stats_dist; +VACUUM (FREEZE) frozen_stats_dist; +SELECT relallfrozen > 0 AND relallfrozen <= relallvisible + AND relallfrozen = (SELECT sum(relallfrozen) FROM gp_dist_random('pg_class') + WHERE oid = 'frozen_stats_dist'::regclass) + AND relallvisible = (SELECT sum(relallvisible) FROM gp_dist_random('pg_class') + WHERE oid = 'frozen_stats_dist'::regclass) + AS frozen_stats_match + FROM pg_class WHERE oid = 'frozen_stats_dist'::regclass; +UPDATE frozen_stats_dist SET a = a + 10000 WHERE a = 1; +ANALYZE frozen_stats_dist; +SELECT relallfrozen > 0 AND relallfrozen <= relallvisible + AND relallfrozen = (SELECT sum(relallfrozen) FROM gp_dist_random('pg_class') + WHERE oid = 'frozen_stats_dist'::regclass) + AND relallvisible = (SELECT sum(relallvisible) FROM gp_dist_random('pg_class') + WHERE oid = 'frozen_stats_dist'::regclass) + AS frozen_stats_match + FROM pg_class WHERE oid = 'frozen_stats_dist'::regclass; +VACUUM (FULL) frozen_stats_dist; +SELECT relallfrozen = 0 AND relallvisible = 0 AS rewrite_clears_map + FROM pg_class WHERE oid = 'frozen_stats_dist'::regclass; +DROP TABLE frozen_stats_dist; + +-- relallfrozen follows segment catalogs for repl tables. +CREATE TABLE frozen_stats_repl (a int) DISTRIBUTED REPLICATED; +INSERT INTO frozen_stats_repl SELECT generate_series(1, 2000); +ANALYZE frozen_stats_repl; +VACUUM (FREEZE) frozen_stats_repl; +SELECT relallfrozen > 0 AND relallfrozen <= relallvisible + AND relallfrozen = (SELECT sum(relallfrozen) FROM gp_dist_random('pg_class') + WHERE oid = 'frozen_stats_repl'::regclass) / (SELECT numsegments FROM gp_distribution_policy WHERE localoid = 'frozen_stats_repl'::regclass) + AND relallvisible = (SELECT sum(relallvisible) FROM gp_dist_random('pg_class') + WHERE oid = 'frozen_stats_repl'::regclass) / (SELECT numsegments FROM gp_distribution_policy WHERE localoid = 'frozen_stats_repl'::regclass) + AS frozen_stats_match + FROM pg_class WHERE oid = 'frozen_stats_repl'::regclass; +UPDATE frozen_stats_repl SET a = a + 10000 WHERE a = 1; +ANALYZE frozen_stats_repl; +SELECT relallfrozen > 0 AND relallfrozen <= relallvisible + AND relallfrozen = (SELECT sum(relallfrozen) FROM gp_dist_random('pg_class') + WHERE oid = 'frozen_stats_repl'::regclass) / (SELECT numsegments FROM gp_distribution_policy WHERE localoid = 'frozen_stats_repl'::regclass) + AND relallvisible = (SELECT sum(relallvisible) FROM gp_dist_random('pg_class') + WHERE oid = 'frozen_stats_repl'::regclass) / (SELECT numsegments FROM gp_distribution_policy WHERE localoid = 'frozen_stats_repl'::regclass) + AS frozen_stats_match + FROM pg_class WHERE oid = 'frozen_stats_repl'::regclass; +VACUUM (FULL) frozen_stats_repl; +SELECT relallfrozen = 0 AND relallvisible = 0 AS rewrite_clears_map + FROM pg_class WHERE oid = 'frozen_stats_repl'::regclass; +DROP TABLE frozen_stats_repl; + +-- Core GUC regression: reconnect with timing enabled at session startup. +-- Segment processes must keep the default off across coordinator startup +-- options, SET, and ROLLBACK. All three checks expect t. +\connect -reuse-previous=on "options='-c track_cost_delay_timing=on'" +SELECT bool_and(setting = 'off') AS segment_timing_local + FROM gp_dist_random('pg_settings') WHERE name = 'track_cost_delay_timing'; +BEGIN; +SET track_cost_delay_timing = off; +SELECT bool_and(setting = 'off') AS segment_timing_local + FROM gp_dist_random('pg_settings') WHERE name = 'track_cost_delay_timing'; +ROLLBACK; +SELECT bool_and(setting = 'off') AS segment_timing_local + FROM gp_dist_random('pg_settings') WHERE name = 'track_cost_delay_timing'; +RESET track_cost_delay_timing; + +-- Core index-summary regression: two indexes on the same table must give +-- exactly two summary rows. Joining by table OID alone used to multiply +-- rows and mix index timings; compare each index with its own segment sum. +CREATE TABLE core_summary_test (a int, b int) DISTRIBUTED BY (a); +CREATE INDEX core_summary_a ON core_summary_test(a); +CREATE INDEX core_summary_b ON core_summary_test(b); +INSERT INTO core_summary_test SELECT g, g FROM generate_series(1, 10000) g; +DELETE FROM core_summary_test WHERE a % 2 = 0; +CREATE TABLE core_timing_repl (a int) DISTRIBUTED REPLICATED; +INSERT INTO core_timing_repl SELECT generate_series(1, 1000); +DELETE FROM core_timing_repl; +VACUUM core_summary_test, core_timing_repl; +SELECT count(*) AS summary_indexes, + count(DISTINCT indexrelid) AS distinct_indexes + FROM gp_stat_all_indexes_summary WHERE relname = 'core_summary_test'; +SELECT bool_and(s.total_vacuum_time = d.total_vacuum_time) AS index_totals_match + FROM gp_stat_all_indexes_summary s + JOIN (SELECT indexrelid, sum(total_vacuum_time) AS total_vacuum_time + FROM gp_dist_random('pg_stat_all_indexes') + WHERE relname = 'core_summary_test' GROUP BY indexrelid) d + USING (indexrelid) + WHERE s.relname = 'core_summary_test'; +-- Native timing summaries work without a statistics extension. +SELECT count(*) = 2 AND bool_and(total_vacuum_time > 0) AS tables_vacuum_timed + FROM gp_stat_all_tables_summary + WHERE relname IN ('core_summary_test', 'core_timing_repl'); +-- Flush database timing before checking its cluster aggregate. +SELECT gp_stat_force_next_flush(); +SELECT total_vacuum_time > 0 AS database_vacuum_timed + FROM gp_stat_vacuum_summary + WHERE datname = current_database(); +DROP TABLE core_summary_test, core_timing_repl; + +-- Native work counters and cluster summaries need no statistics extension. +CREATE TABLE work_stats_dist (a int PRIMARY KEY) DISTRIBUTED BY (a); +CREATE TABLE work_stats_repl (a int) DISTRIBUTED REPLICATED; +INSERT INTO work_stats_dist SELECT generate_series(1, 2000); +ANALYZE work_stats_dist; +INSERT INTO work_stats_repl SELECT generate_series(1, 2000); +ANALYZE work_stats_repl; +VACUUM (FREEZE) work_stats_dist, work_stats_repl; +SELECT relname, freeze_age_vacuum_count = + CASE WHEN relname = 'work_stats_repl' THEN 1 + ELSE (SELECT numsegments FROM gp_distribution_policy + WHERE localoid = 'work_stats_dist'::regclass) END AS freeze_count_matches, + pages_frozen > 0 AND pages_all_visible > 0 AS page_work_recorded + FROM gp_stat_vacuum_tables_summary + WHERE relname IN ('work_stats_dist', 'work_stats_repl') ORDER BY relname; +SELECT count(*) = 2 * (SELECT numsegments FROM gp_distribution_policy + WHERE localoid = 'work_stats_dist'::regclass) + AND bool_and(freeze_age_vacuum_count = 1) AS per_segment_counts + FROM gp_stat_vacuum_tables + WHERE relname IN ('work_stats_dist', 'work_stats_repl') AND gp_segment_id >= 0; +DELETE FROM work_stats_dist WHERE a <= 100; +DELETE FROM work_stats_repl WHERE a <= 100; +VACUUM work_stats_dist, work_stats_repl; +SELECT relname, tuples_deleted = 100 AS deleted_matches + FROM gp_stat_vacuum_tables_summary + WHERE relname IN ('work_stats_dist', 'work_stats_repl') ORDER BY relname; +SELECT tuples_deleted = 100 AS index_work_matches + FROM gp_stat_vacuum_indexes_summary WHERE indexrelname = 'work_stats_dist_pkey'; +-- Native work-only resets preserve other counters and relation scope. +CREATE TEMP TABLE work_other_before AS + SELECT relid, sum(n_tup_ins) AS n_tup_ins, sum(n_tup_del) AS n_tup_del, + sum(vacuum_count) AS vacuum_count, sum(analyze_count) AS analyze_count, + sum(total_vacuum_time) AS total_vacuum_time + FROM gp_stat_all_tables WHERE relname = 'work_stats_dist' GROUP BY relid + DISTRIBUTED BY (relid); +SELECT gp_stat_reset_vacuum_stats('work_stats_dist'::regclass); +SELECT count(*) = (SELECT count(*) FROM gp_segment_configuration WHERE role = 'p') + AND bool_and(tuples_deleted = 0 AND pages_scanned = 0 + AND pages_frozen = 0 AND freeze_age_vacuum_count = 0) + AS work_reset_on_all_instances + FROM gp_stat_vacuum_tables WHERE relname = 'work_stats_dist'; +SELECT tuples_deleted = 100 AS other_relation_preserved + FROM gp_stat_vacuum_tables_summary WHERE relname = 'work_stats_repl'; +SELECT tuples_deleted = 100 AS index_preserved + FROM gp_stat_vacuum_indexes_summary WHERE indexrelname = 'work_stats_dist_pkey'; +SELECT s.n_tup_ins = b.n_tup_ins AND s.n_tup_del = b.n_tup_del + AND s.vacuum_count = b.vacuum_count AND s.analyze_count = b.analyze_count + AND s.total_vacuum_time = b.total_vacuum_time AS other_counters_preserved + FROM (SELECT relid, sum(n_tup_ins) AS n_tup_ins, sum(n_tup_del) AS n_tup_del, + sum(vacuum_count) AS vacuum_count, sum(analyze_count) AS analyze_count, + sum(total_vacuum_time) AS total_vacuum_time + FROM gp_stat_all_tables WHERE relname = 'work_stats_dist' GROUP BY relid) s + JOIN work_other_before b USING (relid); +DROP TABLE work_other_before; +DROP TABLE work_stats_dist, work_stats_repl; + +CREATE TABLE work_stats_ao (a int) WITH (appendonly = true) DISTRIBUTED BY (a); +CREATE TABLE work_stats_aoco (a int) WITH (appendonly = true, orientation = column) DISTRIBUTED BY (a); +INSERT INTO work_stats_ao SELECT generate_series(1, 2000); +ANALYZE work_stats_ao; +INSERT INTO work_stats_aoco SELECT generate_series(1, 2000); +ANALYZE work_stats_aoco; +DELETE FROM work_stats_ao WHERE a <= 1000; +DELETE FROM work_stats_aoco WHERE a <= 1000; +VACUUM work_stats_ao, work_stats_aoco; +SELECT relname, tuples_deleted = 1000 AND tuples_moved = 1000 + AND compacted_segments > 0 AND pages_scanned > 0 AS compaction_matches + FROM gp_stat_vacuum_tables_summary + WHERE relname IN ('work_stats_ao', 'work_stats_aoco') ORDER BY relname; +-- The database form clears AO work and aggregates on all instances. +SELECT gp_stat_force_next_flush(); +SELECT gp_stat_reset_vacuum_stats(); +SELECT bool_and(tuples_deleted = 0 AND tuples_moved = 0 + AND compacted_segments = 0 AND pages_scanned = 0 + AND total_file_segs = 0) AS ao_work_reset + FROM gp_stat_vacuum_tables + WHERE relname IN ('work_stats_ao', 'work_stats_aoco'); +SELECT bool_and(tuples_deleted = 0 AND pages_scanned = 0 + AND pages_frozen = 0 AND compacted_segments = 0) AS database_work_reset + FROM gp_stat_vacuum WHERE datname = current_database(); +DROP TABLE work_stats_ao, work_stats_aoco;