diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml index bb75ed1..c8dd257 100644 --- a/doc/src/sgml/monitoring.sgml +++ b/doc/src/sgml/monitoring.sgml @@ -551,6 +551,15 @@ postgres 27093 0.0 0.0 30096 2752 ? Ss 11:34 0:00 postgres: ser + + pg_stat_wal_recordspg_stat_wal_records + One row per WAL record type, showing how many WAL records of + each type have been generated. See + + pg_stat_wal_records for details. + + + @@ -3689,6 +3698,92 @@ description | Waiting for a newly initialized WAL file to reach durable storage + + <structname>pg_stat_wal_records</structname> + + + pg_stat_wal_records + + + + The pg_stat_wal_records view contains one row + for each WAL record type that has been generated since the last statistics + reset. It provides a breakdown of WAL generation by resource manager and + record type, which can be used to identify which operations are + responsible for WAL volume. Record types with a count of zero are omitted. + + + + Unlike pg_stat_wal, which provides aggregate + totals, this view shows the composition of WAL traffic. Unlike + pg_get_wal_stats() from + , this view tracks live cumulative + counters without needing to read WAL files from disk. + + + + <structname>pg_stat_wal_records</structname> View + + + + + Column Type + + + Description + + + + + + + + resource_manager text + + + Name of the WAL resource manager (e.g. + Heap, Btree, + Transaction, XLOG). + + + + + + record_type text + + + Name of the WAL record type within the resource manager (e.g. + INSERT, HOT_UPDATE, + COMMIT, INSERT_LEAF). + These names are provided by each resource manager's + rm_identify callback. + + + + + + count bigint + + + Number of WAL records of this type generated since the last + statistics reset. + + + + + + stats_reset timestamp with time zone + + + Time at which WAL record statistics were last reset. + + + + +
+ +
+ <structname>pg_stat_database</structname> @@ -5500,6 +5595,12 @@ description | Waiting for a newly initialized WAL file to reach durable storage pg_stat_wal view. + + + wal_records: Reset all the counters shown in the + pg_stat_wal_records view. + + NULL or not specified: All the counters from the diff --git a/src/backend/access/transam/xloginsert.c b/src/backend/access/transam/xloginsert.c index e4a819e..f2556f5 100644 --- a/src/backend/access/transam/xloginsert.c +++ b/src/backend/access/transam/xloginsert.c @@ -36,6 +36,7 @@ #include "executor/instrument.h" #include "miscadmin.h" #include "pg_trace.h" +#include "pgstat.h" #include "replication/origin.h" #include "storage/bufmgr.h" #include "storage/proc.h" @@ -531,6 +532,8 @@ XLogInsert(RmgrId rmid, uint8 info) fpi_bytes, topxid_included); } while (!XLogRecPtrIsValid(EndPos)); + pgstat_count_wal_record(rmid, info); + XLogResetInsertion(); return EndPos; diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql index e540180..a8b7d5c 100644 --- a/src/backend/catalog/system_views.sql +++ b/src/backend/catalog/system_views.sql @@ -1274,6 +1274,14 @@ CREATE VIEW pg_stat_wal AS w.stats_reset FROM pg_stat_get_wal() w; +CREATE VIEW pg_stat_wal_records AS + SELECT + w.resource_manager, + w.record_type, + w.count, + w.stats_reset + FROM pg_stat_get_wal_records() w; + CREATE VIEW pg_stat_progress_analyze AS SELECT S.pid AS pid, S.datid AS datid, D.datname AS datname, diff --git a/src/backend/utils/activity/meson.build b/src/backend/utils/activity/meson.build index 1aa7ece..23c6c6c 100644 --- a/src/backend/utils/activity/meson.build +++ b/src/backend/utils/activity/meson.build @@ -18,6 +18,7 @@ backend_sources += files( 'pgstat_slru.c', 'pgstat_subscription.c', 'pgstat_wal.c', + 'pgstat_walrecords.c', 'pgstat_xact.c', ) diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c index eb8ccba..dde41b8 100644 --- a/src/backend/utils/activity/pgstat.c +++ b/src/backend/utils/activity/pgstat.c @@ -500,6 +500,23 @@ 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_WAL_RECORDS] = { + .name = "wal_records", + + .fixed_amount = true, + .write_to_file = true, + + .snapshot_ctl_off = offsetof(PgStat_Snapshot, wal_records), + .shared_ctl_off = offsetof(PgStat_ShmemControl, wal_records), + .shared_data_off = offsetof(PgStatShared_WalRecords, stats), + .shared_data_len = sizeof(((PgStatShared_WalRecords *) 0)->stats), + + .flush_static_cb = pgstat_walrecords_flush_cb, + .init_shmem_cb = pgstat_walrecords_init_shmem_cb, + .reset_all_cb = pgstat_walrecords_reset_all_cb, + .snapshot_cb = pgstat_walrecords_snapshot_cb, + }, }; /* diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c index 9185a8e..49951d3 100644 --- a/src/backend/utils/adt/pgstatfuncs.c +++ b/src/backend/utils/adt/pgstatfuncs.c @@ -16,6 +16,7 @@ #include "access/htup_details.h" #include "access/xlog.h" +#include "access/xlog_internal.h" #include "access/xlogprefetcher.h" #include "catalog/catalog.h" #include "catalog/pg_authid.h" @@ -1741,6 +1742,75 @@ pg_stat_get_wal(PG_FUNCTION_ARGS) wal_stats->stat_reset_timestamp)); } +/* + * Returns per-resource-manager WAL record type statistics. + */ +Datum +pg_stat_get_wal_records(PG_FUNCTION_ARGS) +{ +#define PG_STAT_GET_WAL_RECORDS_COLS 4 + ReturnSetInfo *rsinfo; + PgStat_WalRecordStats *stats; + + InitMaterializedSRF(fcinfo, 0); + rsinfo = (ReturnSetInfo *) fcinfo->resultinfo; + + stats = pgstat_fetch_stat_wal_records(); + + for (int rmid = 0; rmid < PGSTAT_NUM_WAL_RMGRS; rmid++) + { + const char *rm_name; + + /* Skip resource managers that don't exist */ + if (!RmgrIdExists((RmgrId) rmid)) + continue; + + rm_name = RmgrTable[rmid].rm_name; + + for (int info_idx = 0; info_idx < PGSTAT_NUM_WAL_REC_TYPES; info_idx++) + { + Datum values[PG_STAT_GET_WAL_RECORDS_COLS] = {0}; + bool nulls[PG_STAT_GET_WAL_RECORDS_COLS] = {0}; + const char *record_type = NULL; + char record_type_buf[32]; + + /* Skip entries with no recorded activity */ + if (stats->counts[rmid][info_idx] == 0) + continue; + + /* Resource manager name */ + values[0] = CStringGetTextDatum(rm_name); + + /* Record type name from rm_identify, with numeric fallback */ + if (RmgrTable[rmid].rm_identify) + record_type = RmgrTable[rmid].rm_identify((uint8) (info_idx << 4)); + + if (record_type) + values[1] = CStringGetTextDatum(record_type); + else + { + snprintf(record_type_buf, sizeof(record_type_buf), + "0x%02X", info_idx << 4); + values[1] = CStringGetTextDatum(record_type_buf); + } + + /* Count */ + values[2] = Int64GetDatum(stats->counts[rmid][info_idx]); + + /* Stats reset timestamp */ + if (stats->stat_reset_timestamp != 0) + values[3] = TimestampTzGetDatum(stats->stat_reset_timestamp); + else + nulls[3] = true; + + tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, + values, nulls); + } + } + + return (Datum) 0; +} + Datum pg_stat_get_lock(PG_FUNCTION_ARGS) { @@ -1965,6 +2035,7 @@ pg_stat_reset_shared(PG_FUNCTION_ARGS) XLogPrefetchResetStats(); pgstat_reset_of_kind(PGSTAT_KIND_SLRU); pgstat_reset_of_kind(PGSTAT_KIND_WAL); + pgstat_reset_of_kind(PGSTAT_KIND_WAL_RECORDS); PG_RETURN_VOID(); } @@ -1987,11 +2058,13 @@ pg_stat_reset_shared(PG_FUNCTION_ARGS) pgstat_reset_of_kind(PGSTAT_KIND_SLRU); else if (strcmp(target, "wal") == 0) pgstat_reset_of_kind(PGSTAT_KIND_WAL); + else if (strcmp(target, "wal_records") == 0) + pgstat_reset_of_kind(PGSTAT_KIND_WAL_RECORDS); else ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("unrecognized reset target: \"%s\"", target), - errhint("Target must be \"archiver\", \"bgwriter\", \"checkpointer\", \"io\", \"recovery_prefetch\", \"slru\", or \"wal\"."))); + errhint("Target must be \"archiver\", \"bgwriter\", \"checkpointer\", \"io\", \"recovery_prefetch\", \"slru\", \"wal\", or \"wal_records\"."))); PG_RETURN_VOID(); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 0118e97..ecc9fb2 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -6079,6 +6079,15 @@ proargmodes => '{i,o,o,o,o,o,o}', proargnames => '{backend_pid,wal_records,wal_fpi,wal_bytes,wal_fpi_bytes,wal_buffers_full,stats_reset}', prosrc => 'pg_stat_get_backend_wal' }, +{ oid => '6', + descr => 'statistics: per-resource-manager WAL record type counts', + proname => 'pg_stat_get_wal_records', prorows => '100', proisstrict => 'f', + proretset => 't', provolatile => 's', proparallel => 'r', + prorettype => 'record', proargtypes => '', + proallargtypes => '{text,text,int8,timestamptz}', + proargmodes => '{o,o,o,o}', + proargnames => '{resource_manager,record_type,count,stats_reset}', + prosrc => 'pg_stat_get_wal_records' }, { oid => '6248', descr => 'statistics: information about WAL prefetching', proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't', provolatile => 'v', prorettype => 'record', proargtypes => '', diff --git a/src/include/pgstat.h b/src/include/pgstat.h index 8e3549c..15a1d63 100644 --- a/src/include/pgstat.h +++ b/src/include/pgstat.h @@ -514,6 +514,23 @@ typedef struct PgStat_WalStats TimestampTz stat_reset_timestamp; } PgStat_WalStats; +/* + * Number of WAL resource managers (must match RM_N_IDS from rmgr.h) and + * record info types per resource manager (top 4 bits of info byte). + */ +#define PGSTAT_NUM_WAL_RMGRS 256 +#define PGSTAT_NUM_WAL_REC_TYPES 16 + +/* ------- + * PgStat_WalRecordStats Per-resource-manager WAL record type counts + * ------- + */ +typedef struct PgStat_WalRecordStats +{ + PgStat_Counter counts[PGSTAT_NUM_WAL_RMGRS][PGSTAT_NUM_WAL_REC_TYPES]; + TimestampTz stat_reset_timestamp; +} PgStat_WalRecordStats; + /* ------- * PgStat_Backend Backend statistics * ------- @@ -834,6 +851,24 @@ extern void pgstat_report_wal(bool force); extern PgStat_WalStats *pgstat_fetch_stat_wal(void); +/* + * Functions in pgstat_walrecords.c + */ + +extern PgStat_WalRecordStats *pgstat_fetch_stat_wal_records(void); + +/* + * Variables in pgstat_walrecords.c + */ + +extern PGDLLIMPORT PgStat_Counter pgstat_pending_wal_records[PGSTAT_NUM_WAL_RMGRS][PGSTAT_NUM_WAL_REC_TYPES]; + +#define pgstat_count_wal_record(rmid, info) \ + do { \ + pgstat_pending_wal_records[(rmid)][((info) >> 4) & 0x0F]++; \ + } while (0) + + /* * Variables in pgstat.c */ diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h index 9770442..060a44a 100644 --- a/src/include/utils/pgstat_internal.h +++ b/src/include/utils/pgstat_internal.h @@ -488,6 +488,13 @@ typedef struct PgStatShared_Wal PgStat_WalStats stats; } PgStatShared_Wal; +typedef struct PgStatShared_WalRecords +{ + /* lock protects ->stats */ + LWLock lock; + PgStat_WalRecordStats stats; +} PgStatShared_WalRecords; + /* ---------- @@ -583,6 +590,7 @@ typedef struct PgStat_ShmemControl PgStatShared_Lock lock; PgStatShared_SLRU slru; PgStatShared_Wal wal; + PgStatShared_WalRecords wal_records; /* * Custom stats data with fixed-numbered objects, indexed by (PgStat_Kind @@ -619,6 +627,8 @@ typedef struct PgStat_Snapshot PgStat_WalStats wal; + PgStat_WalRecordStats wal_records; + /* * Data in snapshot for custom fixed-numbered statistics, indexed by * (PgStat_Kind - PGSTAT_KIND_CUSTOM_MIN). Each entry is allocated in @@ -847,6 +857,16 @@ extern void pgstat_wal_reset_all_cb(TimestampTz ts); extern void pgstat_wal_snapshot_cb(void); +/* + * Functions in pgstat_walrecords.c + */ + +extern bool pgstat_walrecords_flush_cb(bool nowait); +extern void pgstat_walrecords_init_shmem_cb(void *stats); +extern void pgstat_walrecords_reset_all_cb(TimestampTz ts); +extern void pgstat_walrecords_snapshot_cb(void); + + /* * Functions in pgstat_subscription.c */ diff --git a/src/include/utils/pgstat_kind.h b/src/include/utils/pgstat_kind.h index 2d78a02..85fc070 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_WAL_RECORDS 14 #define PGSTAT_KIND_BUILTIN_MIN PGSTAT_KIND_DATABASE -#define PGSTAT_KIND_BUILTIN_MAX PGSTAT_KIND_WAL +#define PGSTAT_KIND_BUILTIN_MAX PGSTAT_KIND_WAL_RECORDS #define PGSTAT_KIND_BUILTIN_SIZE (PGSTAT_KIND_BUILTIN_MAX + 1) /* Custom stats kinds */ diff --git a/src/test/regress/expected/stats.out b/src/test/regress/expected/stats.out index ea7f784..279d7be 100644 --- a/src/test/regress/expected/stats.out +++ b/src/test/regress/expected/stats.out @@ -982,6 +982,38 @@ SELECT wal_bytes > :wal_bytes_before FROM pg_stat_wal; t (1 row) +-- Test pg_stat_wal_records +-- WAL-generating activity above should have produced records +SELECT count(*) > 0 AS has_records FROM pg_stat_wal_records; + has_records +------------- + t +(1 row) + +SELECT count(DISTINCT resource_manager) > 0 AS has_rmgrs FROM pg_stat_wal_records; + has_rmgrs +----------- + t +(1 row) + +-- Every row should have non-null resource_manager and record_type +SELECT count(*) = 0 AS no_nulls + FROM pg_stat_wal_records + WHERE resource_manager IS NULL OR record_type IS NULL; + no_nulls +---------- + t +(1 row) + +-- Counts should all be positive +SELECT count(*) = 0 AS all_positive + FROM pg_stat_wal_records + WHERE count <= 0; + all_positive +-------------- + t +(1 row) + SELECT pg_stat_force_next_flush(); pg_stat_force_next_flush -------------------------- @@ -1127,10 +1159,25 @@ SELECT stats_reset > :'wal_reset_ts'::timestamptz FROM pg_stat_wal; t (1 row) +-- Test that reset_shared with wal_records specified as the stats type works +SELECT stats_reset AS wal_records_reset_ts FROM pg_stat_wal_records LIMIT 1 \gset +SELECT pg_stat_reset_shared('wal_records'); + pg_stat_reset_shared +---------------------- + +(1 row) + +SELECT count(*) AS wal_records_post_reset FROM pg_stat_wal_records \gset +SELECT :wal_records_post_reset = 0 AS reset_cleared_counts; + reset_cleared_counts +---------------------- + t +(1 row) + -- Test error case for reset_shared with unknown stats type SELECT pg_stat_reset_shared('unknown'); ERROR: unrecognized reset target: "unknown" -HINT: Target must be "archiver", "bgwriter", "checkpointer", "io", "recovery_prefetch", "slru", or "wal". +HINT: Target must be "archiver", "bgwriter", "checkpointer", "io", "recovery_prefetch", "slru", "wal", or "wal_records". -- Test that reset works for pg_stat_database and pg_stat_database_conflicts -- Since pg_stat_database stats_reset starts out as NULL, reset it once first so that we -- have a baseline for comparison. The same for pg_stat_database_conflicts as it shares diff --git a/src/test/regress/sql/stats.sql b/src/test/regress/sql/stats.sql index 65d8968..a3378ef 100644 --- a/src/test/regress/sql/stats.sql +++ b/src/test/regress/sql/stats.sql @@ -464,6 +464,19 @@ CHECKPOINT (FLUSH_UNLOGGED); SELECT num_requested > :rqst_ckpts_before FROM pg_stat_checkpointer; SELECT wal_bytes > :wal_bytes_before FROM pg_stat_wal; +-- Test pg_stat_wal_records +-- WAL-generating activity above should have produced records +SELECT count(*) > 0 AS has_records FROM pg_stat_wal_records; +SELECT count(DISTINCT resource_manager) > 0 AS has_rmgrs FROM pg_stat_wal_records; +-- Every row should have non-null resource_manager and record_type +SELECT count(*) = 0 AS no_nulls + FROM pg_stat_wal_records + WHERE resource_manager IS NULL OR record_type IS NULL; +-- Counts should all be positive +SELECT count(*) = 0 AS all_positive + FROM pg_stat_wal_records + WHERE count <= 0; + SELECT pg_stat_force_next_flush(); SELECT wal_bytes > :backend_wal_bytes_before FROM pg_stat_get_backend_wal(pg_backend_pid()); @@ -520,6 +533,12 @@ SELECT stats_reset AS wal_reset_ts FROM pg_stat_wal \gset SELECT pg_stat_reset_shared('wal'); SELECT stats_reset > :'wal_reset_ts'::timestamptz FROM pg_stat_wal; +-- Test that reset_shared with wal_records specified as the stats type works +SELECT stats_reset AS wal_records_reset_ts FROM pg_stat_wal_records LIMIT 1 \gset +SELECT pg_stat_reset_shared('wal_records'); +SELECT count(*) AS wal_records_post_reset FROM pg_stat_wal_records \gset +SELECT :wal_records_post_reset = 0 AS reset_cleared_counts; + -- Test error case for reset_shared with unknown stats type SELECT pg_stat_reset_shared('unknown');