From 41e5aeac7c3443912e0e020816eb0cbb649c2dbb Mon Sep 17 00:00:00 2001 From: Alena Rybakina Date: Tue, 16 Jun 2026 10:06:53 +0300 Subject: [PATCH 2/8] Extended vacuum statistics: core heap and tuple metrics for tables and indexes Expose the core per-table heap and dead-tuple metrics of the last VACUUM through the new pg_stat_vacuum_tables view and pg_stat_get_vacuum_tables() function, with documentation and regression coverage: pages_scanned heap pages examined (not skipped via the VM) pages_removed heap pages by which storage was truncated tuples_deleted dead tuples removed by the vacuum tuples_frozen tuples frozen by the vacuum recently_dead_tuples dead tuples still visible to some transaction and therefore not yet removable The matching per-index core metrics are exposed at the same time through the new pg_stat_vacuum_indexes view and pg_stat_get_vacuum_indexes() function: pages_deleted index pages deleted (made reusable) by the vacuum tuples_deleted index entries removed by the vacuum This first commit also adds the infrastructure the rest of the series builds on. Extended vacuum statistics are stored in the cumulative statistics system under a dedicated stats kind, PGSTAT_KIND_VACUUM_RELATION, kept separate from the regular relation statistics. They are collected during vacuum and accumulated per relation, only while the track_vacuum_statistics GUC is enabled. Indexes are tracked under the same stats kind, with their totals reported once per index at the end of the vacuum. The vacuum-extending-in-repetable-read isolation test is added here to cover recently_dead_tuples: a REPEATABLE READ transaction keeps recently deleted tuples visible, so VACUUM cannot remove them and reports them as recently_dead_tuples rather than tuples_deleted. --- doc/src/sgml/system-views.sgml | 190 ++++++++++++++++++ src/backend/access/heap/vacuumlazy.c | 79 ++++++++ src/backend/catalog/heap.c | 1 + src/backend/catalog/index.c | 1 + src/backend/catalog/system_views.sql | 35 ++++ src/backend/commands/vacuumparallel.c | 27 +++ src/backend/utils/activity/Makefile | 1 + src/backend/utils/activity/meson.build | 1 + src/backend/utils/activity/pgstat.c | 15 +- src/backend/utils/activity/pgstat_relation.c | 6 + src/backend/utils/activity/pgstat_vacuum.c | 126 ++++++++++++ src/backend/utils/adt/pgstatfuncs.c | 84 ++++++++ src/backend/utils/misc/guc_parameters.dat | 6 + src/backend/utils/misc/postgresql.conf.sample | 1 + src/include/catalog/pg_proc.dat | 19 ++ src/include/pgstat.h | 83 +++++++- src/include/utils/pgstat_internal.h | 8 + src/include/utils/pgstat_kind.h | 3 +- .../vacuum-extending-in-repetable-read.out | 38 ++++ .../vacuum-extending-in-repetable-read.spec | 53 +++++ src/test/regress/expected/rules.out | 25 +++ src/test/regress/expected/vacuum_stats.out | 87 ++++++++ src/test/regress/parallel_schedule | 3 + src/test/regress/sql/vacuum_stats.sql | 68 +++++++ 24 files changed, 957 insertions(+), 3 deletions(-) create mode 100644 src/backend/utils/activity/pgstat_vacuum.c create mode 100644 src/test/isolation/expected/vacuum-extending-in-repetable-read.out create mode 100644 src/test/isolation/specs/vacuum-extending-in-repetable-read.spec create mode 100644 src/test/regress/expected/vacuum_stats.out create mode 100644 src/test/regress/sql/vacuum_stats.sql diff --git a/doc/src/sgml/system-views.sgml b/doc/src/sgml/system-views.sgml index 2ebec6928d..bdcd8d9f47 100644 --- a/doc/src/sgml/system-views.sgml +++ b/doc/src/sgml/system-views.sgml @@ -5782,4 +5782,194 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx + + <structname>pg_stat_vacuum_tables</structname> + + + pg_stat_vacuum_tables + + + + The pg_stat_vacuum_tables view will contain one row + for each table in the current database, showing extended statistics about + the activity of the most recent VACUUM on that table. + These statistics are accumulated only while + track_vacuum_statistics is enabled. + + + + <structname>pg_stat_vacuum_tables</structname> Columns + + + + + Column Type + + + Description + + + + + + + + schemaname name + + + Name of the schema that the table is in. + + + + + relname name + + + Name of the table. + + + + + relid oid + + + OID of the table. + + + + + pages_scanned bigint + + + Number of heap pages examined by the vacuum (pages that were not skipped using the visibility map). + + + + + pages_removed bigint + + + Number of heap pages by which the table's storage was physically reduced (truncated) by the vacuum. + + + + + tuples_deleted bigint + + + Number of dead tuples removed by the vacuum. + + + + + tuples_frozen bigint + + + Number of tuples frozen by the vacuum. + + + + + recently_dead_tuples bigint + + + Number of dead tuples that are still visible to some transaction and therefore could not yet be removed. + + + + +
+
+ + + <structname>pg_stat_vacuum_indexes</structname> + + + pg_stat_vacuum_indexes + + + + The pg_stat_vacuum_indexes view will contain one row + for each index in the current database, showing extended statistics about + the activity of the most recent VACUUM on that index. + These statistics are accumulated only while + track_vacuum_statistics is enabled. + + + + <structname>pg_stat_vacuum_indexes</structname> Columns + + + + + Column Type + + + Description + + + + + + + + relid oid + + + OID of the table the index is on. + + + + + indexrelid oid + + + OID of the index. + + + + + schemaname name + + + Name of the schema that the table is in. + + + + + relname name + + + Name of the table. + + + + + indexrelname name + + + Name of the index. + + + + + pages_deleted bigint + + + Number of index pages deleted (made reusable) by the vacuum. + + + + + tuples_deleted bigint + + + Number of index entries removed by the vacuum. + + + + +
+
+ diff --git a/src/backend/access/heap/vacuumlazy.c b/src/backend/access/heap/vacuumlazy.c index 39395aed0d..3c403e86f0 100644 --- a/src/backend/access/heap/vacuumlazy.c +++ b/src/backend/access/heap/vacuumlazy.c @@ -283,6 +283,7 @@ typedef struct LVRelState /* Error reporting state */ char *dbname; char *relnamespace; + Oid reloid; char *relname; char *indname; /* Current index name */ BlockNumber blkno; /* used only for heap operations */ @@ -483,6 +484,8 @@ static void update_vacuum_error_info(LVRelState *vacrel, OffsetNumber offnum); static void restore_vacuum_error_info(LVRelState *vacrel, const LVSavedErrInfo *saved_vacrel); +static void accumulate_heap_vacuum_statistics(LVRelState *vacrel, + PgStat_VacuumRelationCounts *extVacStats); @@ -609,6 +612,64 @@ heap_vacuum_eager_scan_setup(LVRelState *vacrel, const VacuumParams *params) first_region_ratio; } +/* + * Fill the extended vacuum statistics report for a heap relation with the + * counters that are derived directly from the LVRelState. Buffer, WAL and + * timing counters require sampling resource usage around the vacuum and are + * gathered separately; they are not touched here. + */ +static void +accumulate_heap_vacuum_statistics(LVRelState *vacrel, + PgStat_VacuumRelationCounts *extVacStats) +{ + if (!pgstat_track_vacuum_statistics) + return; + + extVacStats->type = PGSTAT_EXTVAC_TABLE; + extVacStats->table.pages_scanned = vacrel->scanned_pages; + extVacStats->table.pages_removed = vacrel->removed_pages; + extVacStats->common.tuples_deleted = vacrel->tuples_deleted; + extVacStats->table.tuples_frozen = vacrel->tuples_frozen; + extVacStats->table.recently_dead_tuples = vacrel->recently_dead_tuples; +} + +/* + * Report the per-index extended vacuum statistics, one report per index. + * + * The per-index counters (pages_deleted and the number of removed index + * entries) are derived directly from each index's final IndexBulkDeleteResult, + * which already holds the totals accumulated across all bulkdelete and cleanup + * passes -- so no per-pass sampling is needed here. Used by the non-parallel + * path only; the parallel path reports its DSM-resident results from + * parallel_vacuum_end(). + */ +static void +report_index_vacuum_extstats(LVRelState *vacrel) +{ + if (!pgstat_track_vacuum_statistics) + return; + + for (int idx = 0; idx < vacrel->nindexes; idx++) + { + Relation indrel = vacrel->indrels[idx]; + IndexBulkDeleteResult *istat = vacrel->indstats[idx]; + PgStat_VacuumRelationCounts report; + + /* Skip indexes that this vacuum did not process */ + if (istat == NULL) + continue; + + memset(&report, 0, sizeof(report)); + report.type = PGSTAT_EXTVAC_INDEX; + report.common.tuples_deleted = istat->tuples_removed; + report.index.pages_deleted = istat->pages_deleted; + + pgstat_report_vacuum_extstats(RelationGetRelid(indrel), + indrel->rd_rel->relisshared, + &report); + } +} + /* * heap_vacuum_rel() -- perform VACUUM for one heap relation * @@ -643,6 +704,10 @@ heap_vacuum_rel(Relation rel, const VacuumParams *params, ErrorContextCallback errcallback; char **indnames = NULL; Size dead_items_max_bytes = 0; + PgStat_VacuumRelationCounts extVacReport; + + /* Initialize the extended vacuum statistics report */ + memset(&extVacReport, 0, sizeof(PgStat_VacuumRelationCounts)); verbose = (params->options & VACOPT_VERBOSE) != 0; instrument = (verbose || (AmAutoVacuumWorkerProcess() && @@ -686,6 +751,7 @@ heap_vacuum_rel(Relation rel, const VacuumParams *params, vacrel = palloc0_object(LVRelState); vacrel->dbname = get_database_name(MyDatabaseId); vacrel->relnamespace = get_namespace_name(RelationGetNamespace(rel)); + vacrel->reloid = RelationGetRelid(rel); vacrel->relname = pstrdup(RelationGetRelationName(rel)); vacrel->indname = NULL; vacrel->phase = VACUUM_ERRCB_PHASE_UNKNOWN; @@ -700,6 +766,7 @@ heap_vacuum_rel(Relation rel, const VacuumParams *params, vac_open_indexes(vacrel->rel, RowExclusiveLock, &vacrel->nindexes, &vacrel->indrels); vacrel->bstrategy = bstrategy; + if (instrument && vacrel->nindexes > 0) { /* Copy index names used by instrumentation (not error reporting) */ @@ -985,6 +1052,9 @@ heap_vacuum_rel(Relation rel, const VacuumParams *params, * soon in cases where the failsafe prevented significant amounts of heap * vacuuming. */ + accumulate_heap_vacuum_statistics(vacrel, &extVacReport); + pgstat_report_vacuum_extstats(vacrel->reloid, rel->rd_rel->relisshared, + &extVacReport); pgstat_report_vacuum(rel, Max(vacrel->new_live_tuples, 0), vacrel->recently_dead_tuples + @@ -1618,6 +1688,15 @@ lazy_scan_heap(LVRelState *vacrel) /* Do final index cleanup (call each index's amvacuumcleanup routine) */ if (vacrel->nindexes > 0 && vacrel->do_index_cleanup) lazy_cleanup_all_indexes(vacrel); + + /* + * Report the per-index extended vacuum statistics accumulated over all + * bulkdelete and cleanup passes, exactly once per index. The parallel + * path reports its DSM-resident totals from parallel_vacuum_end() instead, + * so only do it here when index vacuuming ran in the leader. + */ + if (vacrel->nindexes > 0 && !ParallelVacuumIsActive(vacrel)) + report_index_vacuum_extstats(vacrel); } /* diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c index 88087654de..709fe33e61 100644 --- a/src/backend/catalog/heap.c +++ b/src/backend/catalog/heap.c @@ -1903,6 +1903,7 @@ heap_drop_with_catalog(Oid relid) /* ensure that stats are dropped if transaction commits */ pgstat_drop_relation(rel); + pgstat_vacuum_relation_delete_pending_cb(RelationGetRelid(rel)); /* * Close relcache entry, but *keep* AccessExclusiveLock on the relation diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c index 9407c357f2..f986276677 100644 --- a/src/backend/catalog/index.c +++ b/src/backend/catalog/index.c @@ -2345,6 +2345,7 @@ index_drop(Oid indexId, bool concurrent, bool concurrent_lock_mode) /* ensure that stats are dropped if transaction commits */ pgstat_drop_relation(userIndexRelation); + pgstat_vacuum_relation_delete_pending_cb(RelationGetRelid(userIndexRelation)); /* * Close and flush the index's relcache entry, to ensure relcache doesn't diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql index b57128bb12..01487f1665 100644 --- a/src/backend/catalog/system_views.sql +++ b/src/backend/catalog/system_views.sql @@ -1559,3 +1559,38 @@ CREATE VIEW pg_aios AS SELECT * FROM pg_get_aios(); REVOKE ALL ON pg_aios FROM PUBLIC; GRANT SELECT ON pg_aios TO pg_read_all_stats; + +CREATE VIEW pg_stat_vacuum_tables AS + SELECT + N.nspname AS schemaname, + C.relname AS relname, + S.relid AS relid, + + S.pages_scanned AS pages_scanned, + S.pages_removed AS pages_removed, + S.tuples_deleted AS tuples_deleted, + S.tuples_frozen AS tuples_frozen, + S.recently_dead_tuples AS recently_dead_tuples + + FROM pg_class C JOIN + pg_namespace N ON N.oid = C.relnamespace, + LATERAL pg_stat_get_vacuum_tables(C.oid) S + WHERE C.relkind IN ('r', 't', 'm'); + +CREATE VIEW pg_stat_vacuum_indexes AS + SELECT + C.oid AS relid, + I.oid AS indexrelid, + N.nspname AS schemaname, + C.relname AS relname, + I.relname AS indexrelname, + + S.pages_deleted AS pages_deleted, + S.tuples_deleted AS tuples_deleted + FROM + pg_class C JOIN + pg_index X ON C.oid = X.indrelid JOIN + pg_class I ON I.oid = X.indexrelid + LEFT JOIN pg_namespace N ON (N.oid = C.relnamespace), + LATERAL pg_stat_get_vacuum_indexes(I.oid) S + WHERE C.relkind IN ('r', 't', 'm'); diff --git a/src/backend/commands/vacuumparallel.c b/src/backend/commands/vacuumparallel.c index 41cefcfde5..7725c4ecc1 100644 --- a/src/backend/commands/vacuumparallel.c +++ b/src/backend/commands/vacuumparallel.c @@ -535,6 +535,33 @@ parallel_vacuum_end(ParallelVacuumState *pvs, IndexBulkDeleteResult **istats) DestroyParallelContext(pvs->pcxt); ExitParallelMode(); + /* + * Report the per-index extended vacuum statistics, one report per index, + * derived directly from each index's final IndexBulkDeleteResult. The + * indexes are still open here (pvs->indrels is the leader's own array, not + * in the now-destroyed DSM). + */ + if (pgstat_track_vacuum_statistics) + { + for (int i = 0; i < pvs->nindexes; i++) + { + Relation indrel = pvs->indrels[i]; + PgStat_VacuumRelationCounts report; + + if (istats[i] == NULL) + continue; + + memset(&report, 0, sizeof(report)); + report.type = PGSTAT_EXTVAC_INDEX; + report.common.tuples_deleted = istats[i]->tuples_removed; + report.index.pages_deleted = istats[i]->pages_deleted; + + pgstat_report_vacuum_extstats(RelationGetRelid(indrel), + indrel->rd_rel->relisshared, + &report); + } + } + if (AmAutoVacuumWorkerProcess()) pv_shared_cost_params = NULL; diff --git a/src/backend/utils/activity/Makefile b/src/backend/utils/activity/Makefile index ca3ef89bf5..b7db9c034a 100644 --- a/src/backend/utils/activity/Makefile +++ b/src/backend/utils/activity/Makefile @@ -28,6 +28,7 @@ OBJS = \ pgstat_io.o \ pgstat_lock.o \ pgstat_relation.o \ + pgstat_vacuum.o \ pgstat_replslot.o \ pgstat_shmem.o \ pgstat_slru.o \ diff --git a/src/backend/utils/activity/meson.build b/src/backend/utils/activity/meson.build index 1aa7ece529..2a0b50d07d 100644 --- a/src/backend/utils/activity/meson.build +++ b/src/backend/utils/activity/meson.build @@ -17,6 +17,7 @@ backend_sources += files( 'pgstat_shmem.c', 'pgstat_slru.c', 'pgstat_subscription.c', + 'pgstat_vacuum.c', 'pgstat_wal.c', 'pgstat_xact.c', ) diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c index b67da88c7d..4e950672e2 100644 --- a/src/backend/utils/activity/pgstat.c +++ b/src/backend/utils/activity/pgstat.c @@ -204,7 +204,7 @@ static inline bool pgstat_is_kind_valid(PgStat_Kind kind); bool pgstat_track_counts = false; int pgstat_fetch_consistency = PGSTAT_FETCH_CONSISTENCY_CACHE; - +bool pgstat_track_vacuum_statistics = false; /* ---------- * state shared with pgstat_*.c @@ -500,6 +500,19 @@ static const PgStat_KindInfo pgstat_kind_builtin_infos[PGSTAT_KIND_BUILTIN_SIZE] .reset_all_cb = pgstat_wal_reset_all_cb, .snapshot_cb = pgstat_wal_snapshot_cb, }, + [PGSTAT_KIND_VACUUM_RELATION] = { + .name = "vacuum statistics", + + .fixed_amount = false, + .write_to_file = true, + + .shared_size = sizeof(PgStatShared_VacuumRelation), + .shared_data_off = offsetof(PgStatShared_VacuumRelation, stats), + .shared_data_len = sizeof(((PgStatShared_VacuumRelation *) 0)->stats), + .pending_size = sizeof(PgStat_RelationVacuumPending), + + .flush_pending_cb = pgstat_vacuum_relation_flush_cb + }, }; /* diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c index 92e1f60a08..141e2af607 100644 --- a/src/backend/utils/activity/pgstat_relation.c +++ b/src/backend/utils/activity/pgstat_relation.c @@ -904,6 +904,12 @@ pgstat_relation_flush_cb(PgStat_EntryRef *entry_ref, bool nowait) return true; } +void +pgstat_vacuum_relation_delete_pending_cb(Oid relid) +{ + pgstat_drop_transactional(PGSTAT_KIND_VACUUM_RELATION, relid, InvalidOid); +} + void pgstat_relation_delete_pending_cb(PgStat_EntryRef *entry_ref) { diff --git a/src/backend/utils/activity/pgstat_vacuum.c b/src/backend/utils/activity/pgstat_vacuum.c new file mode 100644 index 0000000000..5a625132dd --- /dev/null +++ b/src/backend/utils/activity/pgstat_vacuum.c @@ -0,0 +1,126 @@ +/* ------------------------------------------------------------------------- + * + * pgstat_vacuum.c + * Implementation of extended vacuum statistics. + * + * This file contains the implementation of extended vacuum statistics. It is + * kept separate from pgstat_relation.c and pgstat_database.c to reduce the + * memory footprint of the regular relation and database statistics: vacuum + * metrics require significantly more space per relation, so they live in their + * own PGSTAT_KIND_VACUUM_RELATION stats kind. + * + * Copyright (c) 2001-2026, PostgreSQL Global Development Group + * + * IDENTIFICATION + * src/backend/utils/activity/pgstat_vacuum.c + * ------------------------------------------------------------------------- + */ + +#include "postgres.h" + +#include "pgstat.h" +#include "utils/memutils.h" +#include "utils/pgstat_internal.h" + +#define ACCUMULATE_SUBFIELD(substruct, field) (dst->substruct.field += src->substruct.field) + +/* + * Accumulate the per-table extended vacuum counters collected so far. + * + * Only the counters derived directly from the vacuum's own bookkeeping are + * summed here. The buffer, WAL and timing counters (and the per-index + * counters) are accumulated by additional code added together with the + * helpers that gather them. + */ +static void +pgstat_accumulate_extvac_stats_relations(PgStat_VacuumRelationCounts *dst, + PgStat_VacuumRelationCounts *src) +{ + if (!pgstat_track_vacuum_statistics) + return; + + if (dst->type == PGSTAT_EXTVAC_INVALID) + dst->type = src->type; + + Assert(src->type != PGSTAT_EXTVAC_INVALID && + src->type != PGSTAT_EXTVAC_DB && + src->type == dst->type); + + ACCUMULATE_SUBFIELD(common, tuples_deleted); + + if (dst->type == PGSTAT_EXTVAC_TABLE) + { + ACCUMULATE_SUBFIELD(table, pages_scanned); + ACCUMULATE_SUBFIELD(table, pages_removed); + ACCUMULATE_SUBFIELD(table, tuples_frozen); + ACCUMULATE_SUBFIELD(table, recently_dead_tuples); + } + else if (dst->type == PGSTAT_EXTVAC_INDEX) + { + ACCUMULATE_SUBFIELD(index, pages_deleted); + } +} + +/* + * Report that the relation was just vacuumed, accumulating its extended + * statistics into the per-relation entry. + */ +void +pgstat_report_vacuum_extstats(Oid tableoid, bool shared, + PgStat_VacuumRelationCounts *params) +{ + PgStat_EntryRef *entry_ref; + PgStatShared_VacuumRelation *shtabentry; + Oid dboid = (shared ? InvalidOid : MyDatabaseId); + + if (!pgstat_track_vacuum_statistics) + return; + + entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_VACUUM_RELATION, + dboid, tableoid, false); + shtabentry = (PgStatShared_VacuumRelation *) entry_ref->shared_stats; + pgstat_accumulate_extvac_stats_relations(&shtabentry->stats, params); + pgstat_unlock_entry(entry_ref); +} + +/* + * Flush out pending per-relation extended vacuum stats for the entry. + * + * If nowait is true, this function returns false if the lock could not be + * acquired immediately, otherwise true is returned. + */ +bool +pgstat_vacuum_relation_flush_cb(PgStat_EntryRef *entry_ref, bool nowait) +{ + PgStatShared_VacuumRelation *shtabstats; + PgStat_RelationVacuumPending *pendingent; + + pendingent = (PgStat_RelationVacuumPending *) entry_ref->pending; + shtabstats = (PgStatShared_VacuumRelation *) entry_ref->shared_stats; + + /* Ignore entries that didn't accumulate any actual counts. */ + if (pg_memory_is_all_zeros(pendingent, + sizeof(PgStat_RelationVacuumPending))) + return true; + + if (!pgstat_lock_entry(entry_ref, nowait)) + return false; + + pgstat_accumulate_extvac_stats_relations(&shtabstats->stats, + &pendingent->counts); + + pgstat_unlock_entry(entry_ref); + + return true; +} + +/* + * Support function for the SQL-callable pgstat* functions. Returns the vacuum + * collected statistics for one relation or NULL. + */ +PgStat_VacuumRelationCounts * +pgstat_fetch_stat_vacuum_tabentry(Oid relid, Oid dbid) +{ + return (PgStat_VacuumRelationCounts *) + pgstat_fetch_entry(PGSTAT_KIND_VACUUM_RELATION, dbid, relid, NULL); +} diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c index 6409987d66..d6927d945d 100644 --- a/src/backend/utils/adt/pgstatfuncs.c +++ b/src/backend/utils/adt/pgstatfuncs.c @@ -2367,3 +2367,87 @@ pg_stat_have_stats(PG_FUNCTION_ARGS) PG_RETURN_BOOL(pgstat_have_entry(kind, dboid, objid)); } + +/* + * Get the extended vacuum statistics for a heap table. + */ +Datum +pg_stat_get_vacuum_tables(PG_FUNCTION_ARGS) +{ +#define PG_STAT_GET_VACUUM_TABLES_STATS_COLS 6 + + Oid relid = PG_GETARG_OID(0); + PgStat_VacuumRelationCounts *extvacuum; + TupleDesc tupdesc; + Datum values[PG_STAT_GET_VACUUM_TABLES_STATS_COLS] = {0}; + bool nulls[PG_STAT_GET_VACUUM_TABLES_STATS_COLS] = {0}; + int i = 0; + + /* Build a tuple descriptor for our result type */ + if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE) + elog(ERROR, "return type must be a row type"); + + extvacuum = pgstat_fetch_stat_vacuum_tabentry(relid, MyDatabaseId); + if (!extvacuum) + { + /* Retry as a shared relation before giving up. */ + extvacuum = pgstat_fetch_stat_vacuum_tabentry(relid, InvalidOid); + if (!extvacuum) + { + InitMaterializedSRF(fcinfo, 0); + PG_RETURN_VOID(); + } + } + + values[i++] = ObjectIdGetDatum(relid); + values[i++] = Int64GetDatum(extvacuum->table.pages_scanned); + values[i++] = Int64GetDatum(extvacuum->table.pages_removed); + values[i++] = Int64GetDatum(extvacuum->common.tuples_deleted); + values[i++] = Int64GetDatum(extvacuum->table.tuples_frozen); + values[i++] = Int64GetDatum(extvacuum->table.recently_dead_tuples); + + Assert(i == PG_STAT_GET_VACUUM_TABLES_STATS_COLS); + + /* Returns the record as Datum */ + PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls))); +} + +/* + * Get the extended vacuum statistics for an index. + */ +Datum +pg_stat_get_vacuum_indexes(PG_FUNCTION_ARGS) +{ +#define PG_STAT_GET_VACUUM_INDEX_STATS_COLS 3 + + Oid relid = PG_GETARG_OID(0); + PgStat_VacuumRelationCounts *extvacuum; + TupleDesc tupdesc; + Datum values[PG_STAT_GET_VACUUM_INDEX_STATS_COLS] = {0}; + bool nulls[PG_STAT_GET_VACUUM_INDEX_STATS_COLS] = {0}; + int i = 0; + + if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE) + elog(ERROR, "return type must be a row type"); + + extvacuum = pgstat_fetch_stat_vacuum_tabentry(relid, MyDatabaseId); + if (!extvacuum) + { + extvacuum = pgstat_fetch_stat_vacuum_tabentry(relid, InvalidOid); + if (!extvacuum) + { + InitMaterializedSRF(fcinfo, 0); + PG_RETURN_VOID(); + } + } + + values[i++] = ObjectIdGetDatum(relid); + + values[i++] = Int64GetDatum(extvacuum->index.pages_deleted); + values[i++] = Int64GetDatum(extvacuum->common.tuples_deleted); + + Assert(i == PG_STAT_GET_VACUUM_INDEX_STATS_COLS); + + /* Returns the record as Datum */ + PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls))); +} diff --git a/src/backend/utils/misc/guc_parameters.dat b/src/backend/utils/misc/guc_parameters.dat index afaa058b04..bb8eb41394 100644 --- a/src/backend/utils/misc/guc_parameters.dat +++ b/src/backend/utils/misc/guc_parameters.dat @@ -3220,6 +3220,12 @@ boot_val => 'false', }, +{ name => 'track_vacuum_statistics', type => 'bool', context => 'PGC_SUSET', group => 'STATS_CUMULATIVE', + short_desc => 'Collects vacuum statistics for vacuum activity.', + variable => 'pgstat_track_vacuum_statistics', + boot_val => 'false', +}, + { name => 'track_wal_io_timing', type => 'bool', context => 'PGC_SUSET', group => 'STATS_CUMULATIVE', short_desc => 'Collects timing statistics for WAL I/O activity.', variable => 'track_wal_io_timing', diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample index ac38cddaaf..4cf28f0f2e 100644 --- a/src/backend/utils/misc/postgresql.conf.sample +++ b/src/backend/utils/misc/postgresql.conf.sample @@ -702,6 +702,7 @@ #track_wal_io_timing = off #track_functions = none # none, pl, all #stats_fetch_consistency = cache # cache, none, snapshot +#track_vacuum_statistics = off # - Monitoring - diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 291b039859..6bc3bd909b 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -12638,6 +12638,16 @@ proargnames => '{pid,io_id,io_generation,state,operation,off,length,target,handle_data_len,raw_result,result,target_desc,f_sync,f_localmem,f_buffered}', prosrc => 'pg_get_aios', proacl => '{POSTGRES=X,pg_read_all_stats=X}' }, +{ oid => '8001', + descr => 'pg_stat_get_vacuum_tables returns vacuum stats values for table', + proname => 'pg_stat_get_vacuum_tables', prorows => 1000, provolatile => 's', prorettype => 'record', proisstrict => 'f', + proretset => 't', + proargtypes => 'oid', + proallargtypes => '{oid,oid,int8,int8,int8,int8,int8}', + proargmodes => '{i,o,o,o,o,o,o}', + proargnames => '{reloid,relid,pages_scanned,pages_removed,tuples_deleted,tuples_frozen,recently_dead_tuples}', + prosrc => 'pg_stat_get_vacuum_tables' } + # oid8 related functions { oid => '6436', descr => 'convert oid to oid8', proname => 'oid8', prorettype => 'oid8', proargtypes => 'oid', @@ -12703,4 +12713,13 @@ proname => 'pg_stat_get_frozen_page_marks_cleared', provolatile => 's', proparallel => 'r', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_stat_get_frozen_page_marks_cleared' }, +{ oid => '8004', + descr => 'pg_stat_get_vacuum_indexes returns vacuum stats values for index', + proname => 'pg_stat_get_vacuum_indexes', prorows => 1000, provolatile => 's', prorettype => 'record', proisstrict => 'f', + proretset => 't', + proargtypes => 'oid', + proallargtypes => '{oid,oid,int8,int8}', + proargmodes => '{i,o,o,o}', + proargnames => '{reloid,relid,pages_deleted,tuples_deleted}', + prosrc => 'pg_stat_get_vacuum_indexes' }, ] diff --git a/src/include/pgstat.h b/src/include/pgstat.h index 7db36cf8ad..404e7d0297 100644 --- a/src/include/pgstat.h +++ b/src/include/pgstat.h @@ -118,6 +118,15 @@ typedef struct PgStat_BackendSubEntry PgStat_Counter conflict_count[CONFLICT_NUM_TYPES]; } PgStat_BackendSubEntry; +/* Type of ExtVacReport */ +typedef enum ExtVacReportType +{ + PGSTAT_EXTVAC_INVALID = 0, + PGSTAT_EXTVAC_TABLE = 1, + PGSTAT_EXTVAC_INDEX = 2, + PGSTAT_EXTVAC_DB = 3, +} ExtVacReportType; + /* ---------- * PgStat_TableCounts The actual per-table counts kept by a backend * @@ -164,6 +173,66 @@ typedef struct PgStat_TableCounts PgStat_Counter frozen_page_marks_cleared; } PgStat_TableCounts; +typedef struct PgStat_CommonCounts +{ + /* tuples */ + int64 tuples_deleted; +} PgStat_CommonCounts; + +/* ---------- + * + * PgStat_VacuumRelationCounts + * + * Additional statistics of vacuum processing over a relation. Counters that + * require sampling buffer/WAL/timing usage, and the per-index counters, are + * added to the common and per-type members later, together with the code that + * gathers them. + * ---------- + */ +typedef struct PgStat_VacuumRelationCounts +{ + PgStat_CommonCounts common; + + ExtVacReportType type; /* heap, index, etc. */ + + /* ---------- + * + * There are separate metrics of statistic for tables and indexes, + * which collect during vacuum. + * The union operator allows to combine these statistics + * so that each metric is assigned to a specific class of collected statistics. + * Such a combined structure was called per_type_stats. + * The name of the structure itself is not used anywhere, + * it exists only for understanding the code. + * ---------- + */ + union + { + struct + { + int64 tuples_frozen; /* tuples frozen up by vacuum */ + int64 recently_dead_tuples; /* deleted tuples that are + * still visible to some + * transaction */ + int64 pages_scanned; /* heap pages examined (not skipped by + * VM) */ + int64 pages_removed; /* heap pages removed by vacuum + * "truncation" */ + } table; + struct + { + int64 pages_deleted; /* number of pages deleted by vacuum */ + } index; + } /* per_type_stats */ ; +} PgStat_VacuumRelationCounts; + +typedef struct PgStat_VacuumRelationStatus +{ + Oid id; /* table's OID */ + bool shared; /* is it a shared catalog? */ + PgStat_VacuumRelationCounts counts; /* event counts to be sent */ +} PgStat_VacuumRelationStatus; + /* ---------- * PgStat_TableStatus Per-table status within a backend * @@ -188,6 +257,12 @@ typedef struct PgStat_TableStatus Relation relation; /* rel that is using this entry */ } PgStat_TableStatus; +typedef struct PgStat_RelationVacuumPending +{ + Oid id; /* table's OID */ + PgStat_VacuumRelationCounts counts; /* event counts to be sent */ +} PgStat_RelationVacuumPending; + /* ---------- * PgStat_TableXactStatus Per-table, per-subtransaction status * ---------- @@ -838,6 +913,12 @@ extern int pgstat_get_transactional_drops(bool isCommit, struct xl_xact_stats_it extern void pgstat_execute_transactional_drops(int ndrops, struct xl_xact_stats_item *items, bool is_redo); +extern void pgstat_vacuum_relation_delete_pending_cb(Oid relid); +extern void + pgstat_report_vacuum_extstats(Oid tableoid, bool shared, + PgStat_VacuumRelationCounts * params); +extern PgStat_VacuumRelationCounts * pgstat_fetch_stat_vacuum_tabentry(Oid relid, Oid dbid); + /* * Functions in pgstat_wal.c */ @@ -854,7 +935,7 @@ extern PgStat_WalStats *pgstat_fetch_stat_wal(void); extern PGDLLIMPORT bool pgstat_track_counts; extern PGDLLIMPORT int pgstat_track_functions; extern PGDLLIMPORT int pgstat_fetch_consistency; - +extern PGDLLIMPORT bool pgstat_track_vacuum_statistics; /* * Variables in pgstat_bgwriter.c diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h index fe463faaf6..46a127af2b 100644 --- a/src/include/utils/pgstat_internal.h +++ b/src/include/utils/pgstat_internal.h @@ -507,6 +507,12 @@ typedef struct PgStatShared_Relation PgStat_StatTabEntry stats; } PgStatShared_Relation; +typedef struct PgStatShared_VacuumRelation +{ + PgStatShared_Common header; + PgStat_VacuumRelationCounts stats; +} PgStatShared_VacuumRelation; + typedef struct PgStatShared_Function { PgStatShared_Common header; @@ -689,6 +695,8 @@ extern void *pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free); extern void pgstat_snapshot_fixed(PgStat_Kind kind); +extern bool pgstat_vacuum_relation_flush_cb(PgStat_EntryRef *entry_ref, bool nowait); + /* * Functions in pgstat_archiver.c diff --git a/src/include/utils/pgstat_kind.h b/src/include/utils/pgstat_kind.h index 2d78a02968..f92149066c 100644 --- a/src/include/utils/pgstat_kind.h +++ b/src/include/utils/pgstat_kind.h @@ -39,9 +39,10 @@ #define PGSTAT_KIND_LOCK 11 #define PGSTAT_KIND_SLRU 12 #define PGSTAT_KIND_WAL 13 +#define PGSTAT_KIND_VACUUM_RELATION 14 #define PGSTAT_KIND_BUILTIN_MIN PGSTAT_KIND_DATABASE -#define PGSTAT_KIND_BUILTIN_MAX PGSTAT_KIND_WAL +#define PGSTAT_KIND_BUILTIN_MAX PGSTAT_KIND_VACUUM_RELATION #define PGSTAT_KIND_BUILTIN_SIZE (PGSTAT_KIND_BUILTIN_MAX + 1) /* Custom stats kinds */ diff --git a/src/test/isolation/expected/vacuum-extending-in-repetable-read.out b/src/test/isolation/expected/vacuum-extending-in-repetable-read.out new file mode 100644 index 0000000000..a9d977c520 --- /dev/null +++ b/src/test/isolation/expected/vacuum-extending-in-repetable-read.out @@ -0,0 +1,38 @@ +Parsed test spec with 2 sessions + +starting permutation: s1_begin_repeatable_read s2_delete s2_vacuum s2_print_vacuum_stats_table s1_commit s2_vacuum s2_print_vacuum_stats_table +step s1_begin_repeatable_read: + BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ; + SELECT count(*) FROM test_vacuum_stat_isolation; + +count +----- + 50 +(1 row) + +step s2_delete: DELETE FROM test_vacuum_stat_isolation WHERE id <= 10; +step s2_vacuum: VACUUM test_vacuum_stat_isolation; +step s2_print_vacuum_stats_table: + SELECT + vt.relname, vt.tuples_deleted, vt.recently_dead_tuples + FROM pg_stat_vacuum_tables vt, pg_class c + WHERE vt.relname = 'test_vacuum_stat_isolation' AND vt.relid = c.oid; + +relname |tuples_deleted|recently_dead_tuples +--------------------------+--------------+-------------------- +test_vacuum_stat_isolation| 0| 10 +(1 row) + +step s1_commit: COMMIT; +step s2_vacuum: VACUUM test_vacuum_stat_isolation; +step s2_print_vacuum_stats_table: + SELECT + vt.relname, vt.tuples_deleted, vt.recently_dead_tuples + FROM pg_stat_vacuum_tables vt, pg_class c + WHERE vt.relname = 'test_vacuum_stat_isolation' AND vt.relid = c.oid; + +relname |tuples_deleted|recently_dead_tuples +--------------------------+--------------+-------------------- +test_vacuum_stat_isolation| 10| 10 +(1 row) + diff --git a/src/test/isolation/specs/vacuum-extending-in-repetable-read.spec b/src/test/isolation/specs/vacuum-extending-in-repetable-read.spec new file mode 100644 index 0000000000..50b79715f3 --- /dev/null +++ b/src/test/isolation/specs/vacuum-extending-in-repetable-read.spec @@ -0,0 +1,53 @@ +# Test for recently_dead_tuples in pg_stat_vacuum_tables. +# +# A tuple deleted while an older snapshot can still see it is counted as +# recently_dead_tuples, because VACUUM is not allowed to remove it yet; once +# the older snapshot is gone, the next VACUUM removes it and counts it in +# tuples_deleted instead. + +setup +{ + CREATE TABLE test_vacuum_stat_isolation (id int, ival int) + WITH (autovacuum_enabled = off); + INSERT INTO test_vacuum_stat_isolation + SELECT i, i FROM generate_series(1, 50) i; + SET track_vacuum_statistics TO 'on'; +} + +teardown +{ + DROP TABLE test_vacuum_stat_isolation CASCADE; + RESET track_vacuum_statistics; +} + +# Reader holding an old snapshot, so deleted tuples stay recently dead: +session s1 +setup { SET track_vacuum_statistics TO 'on'; } +step s1_begin_repeatable_read +{ + BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ; + SELECT count(*) FROM test_vacuum_stat_isolation; +} +step s1_commit { COMMIT; } + +# Performs the DML, the vacuums and prints the collected statistics: +session s2 +setup { SET track_vacuum_statistics TO 'on'; } +step s2_delete { DELETE FROM test_vacuum_stat_isolation WHERE id <= 10; } +step s2_vacuum { VACUUM test_vacuum_stat_isolation; } +step s2_print_vacuum_stats_table +{ + SELECT + vt.relname, vt.tuples_deleted, vt.recently_dead_tuples + FROM pg_stat_vacuum_tables vt, pg_class c + WHERE vt.relname = 'test_vacuum_stat_isolation' AND vt.relid = c.oid; +} + +permutation + s1_begin_repeatable_read + s2_delete + s2_vacuum + s2_print_vacuum_stats_table + s1_commit + s2_vacuum + s2_print_vacuum_stats_table diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out index 096e4f763f..40c6aca96e 100644 --- a/src/test/regress/expected/rules.out +++ b/src/test/regress/expected/rules.out @@ -2421,6 +2421,31 @@ pg_stat_user_tables| SELECT relid, frozen_page_marks_cleared FROM pg_stat_all_tables WHERE ((schemaname <> ALL (ARRAY['pg_catalog'::name, 'information_schema'::name])) AND (schemaname !~ '^pg_toast'::text)); +pg_stat_vacuum_indexes| SELECT c.oid AS relid, + i.oid AS indexrelid, + n.nspname AS schemaname, + c.relname, + i.relname AS indexrelname, + s.pages_deleted, + s.tuples_deleted + FROM (((pg_class c + JOIN pg_index x ON ((c.oid = x.indrelid))) + JOIN pg_class i ON ((i.oid = x.indexrelid))) + LEFT JOIN pg_namespace n ON ((n.oid = c.relnamespace))), + LATERAL pg_stat_get_vacuum_indexes(i.oid) s(relid, pages_deleted, tuples_deleted) + WHERE (c.relkind = ANY (ARRAY['r'::"char", 't'::"char", 'm'::"char"])); +pg_stat_vacuum_tables| SELECT n.nspname AS schemaname, + c.relname, + s.relid, + s.pages_scanned, + s.pages_removed, + s.tuples_deleted, + s.tuples_frozen, + s.recently_dead_tuples + FROM (pg_class c + JOIN pg_namespace n ON ((n.oid = c.relnamespace))), + LATERAL pg_stat_get_vacuum_tables(c.oid) s(relid, pages_scanned, pages_removed, tuples_deleted, tuples_frozen, recently_dead_tuples) + WHERE (c.relkind = ANY (ARRAY['r'::"char", 't'::"char", 'm'::"char"])); pg_stat_wal| SELECT wal_records, wal_fpi, wal_bytes, diff --git a/src/test/regress/expected/vacuum_stats.out b/src/test/regress/expected/vacuum_stats.out new file mode 100644 index 0000000000..c3079e3379 --- /dev/null +++ b/src/test/regress/expected/vacuum_stats.out @@ -0,0 +1,87 @@ +-- +-- Extended vacuum statistics views (pg_stat_vacuum_tables, _indexes, _database) +-- +SET track_vacuum_statistics = on; +CREATE TABLE vacstat_t (id int PRIMARY KEY, v text) + WITH (autovacuum_enabled = off); +INSERT INTO vacstat_t SELECT g, repeat('x', 20) FROM generate_series(1, 1000) g; +DELETE FROM vacstat_t WHERE id % 2 = 0; +VACUUM vacstat_t; +-- core heap-page and dead-tuple metrics. This VACUUM runs without concurrent +-- activity: every deleted tuple is fully removable (recently_dead_tuples = 0), +-- the surviving tuples are too fresh to be frozen (tuples_frozen = 0), and the +-- interleaved deletions leave no trailing empty pages to truncate +-- (pages_removed = 0). The recently_dead_tuples non-zero path is covered by +-- the vacuum-extending-in-repetable-read isolation test. +SELECT pages_scanned > 0 AS pages_scanned, + pages_removed = 0 AS pages_removed, + tuples_deleted = 500 AS tuples_deleted, + tuples_frozen = 0 AS tuples_frozen, + recently_dead_tuples = 0 AS recently_dead_tuples + FROM pg_stat_vacuum_tables WHERE relname = 'vacstat_t'; + pages_scanned | pages_removed | tuples_deleted | tuples_frozen | recently_dead_tuples +---------------+---------------+----------------+---------------+---------------------- + t | t | t | t | t +(1 row) + +-- pages_removed path: deleting every tuple lets VACUUM truncate the now-empty +-- trailing heap pages, so pages_removed advances. +CREATE TABLE vacstat_trunc (id int) + WITH (autovacuum_enabled = off); +INSERT INTO vacstat_trunc SELECT generate_series(1, 10000); +DELETE FROM vacstat_trunc; +VACUUM vacstat_trunc; +SELECT pages_removed > 0 AS pages_removed, + tuples_deleted = 10000 AS tuples_deleted + FROM pg_stat_vacuum_tables WHERE relname = 'vacstat_trunc'; + pages_removed | tuples_deleted +---------------+---------------- + t | t +(1 row) + +DROP TABLE vacstat_trunc; +-- tuples_frozen path: an aggressive VACUUM (FREEZE) freezes all live tuples, +-- so tuples_frozen advances. +CREATE TABLE vacstat_freeze (x int) + WITH (autovacuum_enabled = off); +INSERT INTO vacstat_freeze SELECT generate_series(1, 1000); +VACUUM (FREEZE) vacstat_freeze; +SELECT tuples_frozen > 0 AS tuples_frozen + FROM pg_stat_vacuum_tables WHERE relname = 'vacstat_freeze'; + tuples_frozen +--------------- + t +(1 row) + +DROP TABLE vacstat_freeze; +-- per-index view: the primary key index is processed by the same VACUUM. +-- No btree leaf empties out (interleaved deletions), so pages_deleted = 0, +-- while every index entry for a removed heap tuple is deleted. +SELECT indexrelname, + pages_deleted = 0 AS pages_deleted, + tuples_deleted = 500 AS tuples_deleted + FROM pg_stat_vacuum_indexes WHERE relname = 'vacstat_t' ORDER BY indexrelname; + indexrelname | pages_deleted | tuples_deleted +----------------+---------------+---------------- + vacstat_t_pkey | t | t +(1 row) + +-- index page-deletion path: deleting a contiguous key range empties whole +-- btree leaf pages, which VACUUM then deletes (pages_deleted > 0), and every +-- removed index entry is counted (tuples_deleted). +CREATE TABLE vacstat_idxdel (id int PRIMARY KEY, v text) + WITH (autovacuum_enabled = off); +INSERT INTO vacstat_idxdel SELECT g, repeat('x', 20) FROM generate_series(1, 10000) g; +VACUUM vacstat_idxdel; +DELETE FROM vacstat_idxdel WHERE id <= 9000; +VACUUM vacstat_idxdel; +SELECT indexrelname, + pages_deleted > 0 AS pages_deleted, + tuples_deleted = 9000 AS tuples_deleted + FROM pg_stat_vacuum_indexes WHERE relname = 'vacstat_idxdel' ORDER BY indexrelname; + indexrelname | pages_deleted | tuples_deleted +---------------------+---------------+---------------- + vacstat_idxdel_pkey | t | t +(1 row) + +DROP TABLE vacstat_idxdel; diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule index 8fa0a6c47f..3c73207a9f 100644 --- a/src/test/regress/parallel_schedule +++ b/src/test/regress/parallel_schedule @@ -91,6 +91,9 @@ test: select_parallel test: write_parallel test: vacuum_parallel +# extended vacuum statistics views +test: vacuum_stats + # Run this alone, because concurrent DROP TABLE would make non-superuser # "ANALYZE;" fail with "relation with OID $n does not exist". test: maintain_every diff --git a/src/test/regress/sql/vacuum_stats.sql b/src/test/regress/sql/vacuum_stats.sql new file mode 100644 index 0000000000..04696ed824 --- /dev/null +++ b/src/test/regress/sql/vacuum_stats.sql @@ -0,0 +1,68 @@ +-- +-- Extended vacuum statistics views (pg_stat_vacuum_tables, _indexes, _database) +-- +SET track_vacuum_statistics = on; + +CREATE TABLE vacstat_t (id int PRIMARY KEY, v text) + WITH (autovacuum_enabled = off); +INSERT INTO vacstat_t SELECT g, repeat('x', 20) FROM generate_series(1, 1000) g; +DELETE FROM vacstat_t WHERE id % 2 = 0; +VACUUM vacstat_t; + +-- core heap-page and dead-tuple metrics. This VACUUM runs without concurrent +-- activity: every deleted tuple is fully removable (recently_dead_tuples = 0), +-- the surviving tuples are too fresh to be frozen (tuples_frozen = 0), and the +-- interleaved deletions leave no trailing empty pages to truncate +-- (pages_removed = 0). The recently_dead_tuples non-zero path is covered by +-- the vacuum-extending-in-repetable-read isolation test. +SELECT pages_scanned > 0 AS pages_scanned, + pages_removed = 0 AS pages_removed, + tuples_deleted = 500 AS tuples_deleted, + tuples_frozen = 0 AS tuples_frozen, + recently_dead_tuples = 0 AS recently_dead_tuples + FROM pg_stat_vacuum_tables WHERE relname = 'vacstat_t'; + +-- pages_removed path: deleting every tuple lets VACUUM truncate the now-empty +-- trailing heap pages, so pages_removed advances. +CREATE TABLE vacstat_trunc (id int) + WITH (autovacuum_enabled = off); +INSERT INTO vacstat_trunc SELECT generate_series(1, 10000); +DELETE FROM vacstat_trunc; +VACUUM vacstat_trunc; +SELECT pages_removed > 0 AS pages_removed, + tuples_deleted = 10000 AS tuples_deleted + FROM pg_stat_vacuum_tables WHERE relname = 'vacstat_trunc'; +DROP TABLE vacstat_trunc; + +-- tuples_frozen path: an aggressive VACUUM (FREEZE) freezes all live tuples, +-- so tuples_frozen advances. +CREATE TABLE vacstat_freeze (x int) + WITH (autovacuum_enabled = off); +INSERT INTO vacstat_freeze SELECT generate_series(1, 1000); +VACUUM (FREEZE) vacstat_freeze; +SELECT tuples_frozen > 0 AS tuples_frozen + FROM pg_stat_vacuum_tables WHERE relname = 'vacstat_freeze'; +DROP TABLE vacstat_freeze; + +-- per-index view: the primary key index is processed by the same VACUUM. +-- No btree leaf empties out (interleaved deletions), so pages_deleted = 0, +-- while every index entry for a removed heap tuple is deleted. +SELECT indexrelname, + pages_deleted = 0 AS pages_deleted, + tuples_deleted = 500 AS tuples_deleted + FROM pg_stat_vacuum_indexes WHERE relname = 'vacstat_t' ORDER BY indexrelname; + +-- index page-deletion path: deleting a contiguous key range empties whole +-- btree leaf pages, which VACUUM then deletes (pages_deleted > 0), and every +-- removed index entry is counted (tuples_deleted). +CREATE TABLE vacstat_idxdel (id int PRIMARY KEY, v text) + WITH (autovacuum_enabled = off); +INSERT INTO vacstat_idxdel SELECT g, repeat('x', 20) FROM generate_series(1, 10000) g; +VACUUM vacstat_idxdel; +DELETE FROM vacstat_idxdel WHERE id <= 9000; +VACUUM vacstat_idxdel; +SELECT indexrelname, + pages_deleted > 0 AS pages_deleted, + tuples_deleted = 9000 AS tuples_deleted + FROM pg_stat_vacuum_indexes WHERE relname = 'vacstat_idxdel' ORDER BY indexrelname; +DROP TABLE vacstat_idxdel; -- 2.39.5 (Apple Git-154)