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
+
+ pg_stat_wal_records
+
+
+ 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.
+
+
+
+ pg_stat_wal_records View
+
+
+
+
+ Column Type
+
+
+ Description
+
+
+
+
+
+
+
+ resource_managertext
+
+
+ Name of the WAL resource manager (e.g.
+ Heap, Btree,
+ Transaction, XLOG).
+
+
+
+
+
+ record_typetext
+
+
+ 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.
+
+
+
+
+
+ countbigint
+
+
+ Number of WAL records of this type generated since the last
+ statistics reset.
+
+
+
+
+
+ stats_resettimestamp with time zone
+
+
+ Time at which WAL record statistics were last reset.
+
+
+
+
+
+
+
+
pg_stat_database
@@ -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');