From 62c2dac137d72bb76cbd01a48019c89d01535565 Mon Sep 17 00:00:00 2001
From: Tristan Partin <tristan@partin.io>
Date: Thu, 30 Apr 2026 16:54:29 +0000
Subject: [PATCH v2] Add pg_stat_kind_info view and pg_stat_get_kind_info()

Expose metadata about loaded statistics kinds through the
pg_stat_kind_info view backed by the pg_stat_get_kind_info() function.
Each row describes one statistics kind:

id: numeric identifier of the kind
name: name of the kind of stats
count: number of entries counted
builtin: whether this is a builtin kind
fixed_amount: whether this kind tracks a fixed amount of data
accessed_across_databases: whether this kind can be accessed across
  databases
written_to_file: whether this kind is written out to the statistics file
shared_size: the size in bytes of a shared memory entry for this kind

With the view, extension authors can easily confirm that their custom
statistics kinds are loaded. We can also figure out how shared memory is
being used by statistics.

Signed-off-by: Tristan Partin <tristan@partin.io>
---
 doc/src/sgml/monitoring.sgml                  | 141 ++++++++++++++++++
 src/backend/catalog/system_views.sql          |  12 ++
 src/backend/utils/adt/pgstatfuncs.c           |  59 ++++++++
 src/include/catalog/pg_proc.dat               |   8 +
 .../test_custom_stats/t/001_custom_stats.pl   |  13 +-
 src/test/regress/expected/rules.out           |   9 ++
 src/test/regress/expected/stats.out           |  22 +++
 src/test/regress/expected/sysviews.out        |   7 +
 src/test/regress/sql/stats.sql                |   8 +
 src/test/regress/sql/sysviews.sql             |   3 +
 10 files changed, 281 insertions(+), 1 deletion(-)

diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 08d5b82455..a3f33de96d 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -509,6 +509,15 @@ postgres   27093  0.0  0.0  30096  2752 ?        Ss   11:34   0:00 postgres: ser
      </entry>
      </row>
 
+     <row>
+      <entry><structname>pg_stat_kind_info</structname><indexterm><primary>pg_stat_kind_info</primary></indexterm></entry>
+      <entry>
+       One row for each loaded statistics kind, showing metadata about the kind.
+       See <link linkend="monitoring-pg-stat-kind-info-view">
+       <structname>pg_stat_kind_info</structname></link> for details.
+      </entry>
+     </row>
+
      <row>
       <entry><structname>pg_stat_lock</structname><indexterm><primary>pg_stat_lock</primary></indexterm></entry>
       <entry>
@@ -3303,6 +3312,138 @@ description | Waiting for a newly initialized WAL file to reach durable storage
  </sect2>
 
 
+ <sect2 id="monitoring-pg-stat-kind-info-view">
+  <title><structname>pg_stat_kind_info</structname></title>
+
+  <indexterm>
+   <primary>pg_stat_kind_info</primary>
+  </indexterm>
+
+  <para>
+   The <structname>pg_stat_kind_info</structname> view contains one row for each
+   loaded statistics kind, including both built-in and custom kinds.
+  </para>
+
+  <table id="pg-stat-kind-info-view" xreflabel="pg_stat_kind_info">
+   <title><structname>pg_stat_kind_info</structname> View</title>
+   <tgroup cols="1">
+    <thead>
+     <row>
+      <entry role="catalog_table_entry">
+       <para role="column_definition">
+        Column Type
+       </para>
+       <para>
+        Description
+       </para>
+      </entry>
+     </row>
+    </thead>
+    <tbody>
+     <row>
+      <entry role="catalog_table_entry">
+       <para role="column_definition">
+        <structfield>id</structfield> <type>integer</type>
+       </para>
+       <para>
+        Numeric identifier of the statistics kind.
+       </para>
+      </entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry">
+       <para role="column_definition">
+        <structfield>name</structfield> <type>text</type>
+       </para>
+       <para>
+        Name of the statistics kind.
+       </para>
+      </entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry">
+       <para role="column_definition">
+        <structfield>count</structfield> <type>bigint</type>
+       </para>
+       <para>
+        Number of tracked entries for this kind. For fixed-amount kinds, this is
+        always 1. For variable-numbered kinds, this is the number of objects
+        currently tracked. <literal>NULL</literal> if the kind does not track
+        entry counts.
+       </para>
+      </entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry">
+       <para role="column_definition">
+        <structfield>builtin</structfield> <type>boolean</type>
+       </para>
+       <para>
+        True if this is a built-in statistics kind, false if it was registered
+        by an extension.
+       </para>
+      </entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry">
+       <para role="column_definition">
+        <structfield>fixed_amount</structfield> <type>boolean</type>
+       </para>
+       <para>
+        True if this kind tracks a fixed amount of data (a single, statically
+        allocated entry), false if it tracks a variable number of entries
+        keyed by object identifier.
+       </para>
+      </entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry">
+       <para role="column_definition">
+        <structfield>accessed_across_databases</structfield> <type>boolean</type>
+       </para>
+       <para>
+        True if entries of this kind are accessed across databases (cluster-wide
+        statistics), false if they are scoped to a single database.
+       </para>
+      </entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry">
+       <para role="column_definition">
+        <structfield>written_to_file</structfield> <type>boolean</type>
+       </para>
+       <para>
+        True if entries of this kind are persisted to the statistics file at
+        shutdown and reloaded on startup, false if they are kept only in
+        shared memory.
+       </para>
+      </entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry">
+       <para role="column_definition">
+        <structfield>shared_size</structfield> <type>bigint</type>
+       </para>
+       <para>
+        Size in bytes of a shared memory entry for this statistics kind.
+        <literal>NULL</literal> for built-in fixed-amount kinds, whose
+        single entry lives in a statically-allocated slot rather than a
+        sized shared memory entry.
+       </para>
+      </entry>
+     </row>
+    </tbody>
+   </tgroup>
+  </table>
+ </sect2>
+
  <sect2 id="monitoring-pg-stat-lock-view">
   <title><structname>pg_stat_lock</structname></title>
 
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index 73a1c1c467..5ce455f7ec 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -1282,6 +1282,18 @@ SELECT
        b.stats_reset
 FROM pg_stat_get_io() b;
 
+CREATE VIEW pg_stat_kind_info AS
+    SELECT
+        k.id,
+        k.name,
+        k.count,
+        k.builtin,
+        k.fixed_amount,
+        k.accessed_across_databases,
+        k.written_to_file,
+        k.shared_size
+    FROM pg_stat_get_kind_info() k;
+
 CREATE VIEW pg_stat_wal AS
     SELECT
         w.wal_records,
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 6f9c9c72de..acb8c4883e 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -30,6 +30,8 @@
 #include "storage/procarray.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
+#include "utils/pgstat_internal.h"
+#include "utils/pgstat_kind.h"
 #include "utils/timestamp.h"
 #include "utils/tuplestore.h"
 #include "utils/wait_event.h"
@@ -1638,6 +1640,63 @@ pg_stat_get_backend_io(PG_FUNCTION_ARGS)
 	return (Datum) 0;
 }
 
+Datum
+pg_stat_get_kind_info(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_KIND_INFO_COLS	8
+	ReturnSetInfo *rsinfo;
+
+	InitMaterializedSRF(fcinfo, 0);
+	rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
+
+	for (int kind = PGSTAT_KIND_MIN; kind <= PGSTAT_KIND_MAX; kind++)
+	{
+		Datum		values[PG_STAT_KIND_INFO_COLS] = {0};
+		bool		nulls[PG_STAT_KIND_INFO_COLS] = {0};
+		const PgStat_KindInfo *info;
+
+		info = pgstat_get_kind_info(kind);
+		if (info == NULL)
+			continue;
+
+		values[0] = Int32GetDatum(kind);
+		values[1] = CStringGetTextDatum(info->name);
+
+		/* For fixed-amount kinds, count is always 1. The entry is stored in
+		 * PgStat_ShmemControl. If it is not a fixed-amount, then report the
+		 * count of entries if tracked, or NULL if not tracked.
+		 */
+		if (info->fixed_amount)
+			values[2] = Int64GetDatum(1);
+		else if (info->track_entry_count)
+			values[2] = Int64GetDatum(pgstat_get_entry_count(kind));
+		else
+			nulls[2] = true;
+
+		values[3] = BoolGetDatum(pgstat_is_kind_builtin(kind));
+		values[4] = BoolGetDatum(info->fixed_amount);
+		values[5] = BoolGetDatum(info->accessed_across_databases);
+		values[6] = BoolGetDatum(info->write_to_file);
+
+		/*
+		 * Built-in fixed-amount kinds store their single entry in
+		 * PgStat_ShmemControl, so shared_size is unused and left zero in their
+		 * PgStat_KindInfo. Reporting it would be misleading, so report NULL
+		 * instead. Custom fixed-amount kinds declare shared_size to size their
+		 * slot in custom_data[], so it is meaningful and reported as-is.
+		 */
+		if (info->fixed_amount && pgstat_is_kind_builtin(kind))
+			nulls[7] = true;
+		else
+			values[7] = Int64GetDatum(info->shared_size);
+
+		tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, values, nulls);
+	}
+
+	return (Datum) 0;
+#undef PG_STAT_KIND_INFO_COLS
+}
+
 /*
  * pg_stat_wal_build_tuple
  *
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index be157a5fbe..a9ac9304b9 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6078,6 +6078,14 @@
   proargnames => '{backend_pid,backend_type,object,context,reads,read_bytes,read_time,writes,write_bytes,write_time,writebacks,writeback_time,extends,extend_bytes,extend_time,hits,evictions,reuses,fsyncs,fsync_time,stats_reset}',
   prosrc => 'pg_stat_get_backend_io' },
 
+{ oid => '8683', descr => 'statistics: information about loaded statistics kinds',
+  proname => 'pg_stat_get_kind_info', prorows => '20', proretset => 't',
+  provolatile => 'v', proparallel => 'r', prorettype => 'record',
+  proargtypes => '', proallargtypes => '{int4,text,int8,bool,bool,bool,bool,int8}',
+  proargmodes => '{o,o,o,o,o,o,o,o}',
+  proargnames => '{id,name,count,builtin,fixed_amount,accessed_across_databases,written_to_file,shared_size}',
+  prosrc => 'pg_stat_get_kind_info' },
+
 { oid => '1136', descr => 'statistics: information about WAL activity',
   proname => 'pg_stat_get_wal', proisstrict => 'f', provolatile => 's',
   proparallel => 'r', prorettype => 'record', proargtypes => '',
diff --git a/src/test/modules/test_custom_stats/t/001_custom_stats.pl b/src/test/modules/test_custom_stats/t/001_custom_stats.pl
index 9e6a7a3857..1b11d81687 100644
--- a/src/test/modules/test_custom_stats/t/001_custom_stats.pl
+++ b/src/test/modules/test_custom_stats/t/001_custom_stats.pl
@@ -27,6 +27,17 @@
 $node->safe_psql('postgres', q(CREATE EXTENSION test_custom_var_stats));
 $node->safe_psql('postgres', q(CREATE EXTENSION test_custom_fixed_stats));
 
+# Verify custom stats kinds appear in pg_stat_kind_info.
+my $result = $node->safe_psql('postgres',
+	q(SELECT id, name, builtin, fixed_amount, accessed_across_databases,
+	         written_to_file, shared_size > 0
+	    FROM pg_stat_kind_info
+	    WHERE name LIKE 'test_custom%' ORDER BY id));
+is($result,
+	"25|test_custom_var_stats|f|f|t|t|t\n" .
+	"26|test_custom_fixed_stats|f|t|f|t|t",
+	"custom stats kinds visible in pg_stat_kind_info");
+
 # Create entries for variable-sized stats.
 $node->safe_psql('postgres',
 	q(select test_custom_stats_var_create('entry1', 'Test entry 1')));
@@ -63,7 +74,7 @@
 $node->safe_psql('postgres', q(select test_custom_stats_fixed_update()));
 
 # Test data reports.
-my $result = $node->safe_psql('postgres',
+$result = $node->safe_psql('postgres',
 	q(select * from test_custom_stats_var_report('entry1')));
 is( $result,
 	"entry1|2|Test entry 1",
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index a65a5bf0c4..4ee3191f9d 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1967,6 +1967,15 @@ pg_stat_io| SELECT backend_type,
     fsync_time,
     stats_reset
    FROM pg_stat_get_io() b(backend_type, object, context, reads, read_bytes, read_time, writes, write_bytes, write_time, writebacks, writeback_time, extends, extend_bytes, extend_time, hits, evictions, reuses, fsyncs, fsync_time, stats_reset);
+pg_stat_kind_info| SELECT id,
+    name,
+    count,
+    builtin,
+    fixed_amount,
+    accessed_across_databases,
+    written_to_file,
+    shared_size
+   FROM pg_stat_get_kind_info() k(id, name, count, builtin, fixed_amount, accessed_across_databases, written_to_file, shared_size);
 pg_stat_lock| SELECT locktype,
     waits,
     wait_time,
diff --git a/src/test/regress/expected/stats.out b/src/test/regress/expected/stats.out
index bbb1db3c43..33197a62a5 100644
--- a/src/test/regress/expected/stats.out
+++ b/src/test/regress/expected/stats.out
@@ -113,6 +113,28 @@ walwriter|wal|init
 walwriter|wal|normal
 (95 rows)
 \a
+-- List of loaded statistics kinds.
+\a
+SELECT name, id, fixed_amount, accessed_across_databases, written_to_file
+  FROM pg_stat_kind_info
+  WHERE builtin
+  ORDER BY name COLLATE "C";
+name|id|fixed_amount|accessed_across_databases|written_to_file
+archiver|7|t|f|t
+backend|6|f|t|f
+bgwriter|8|t|f|t
+checkpointer|9|t|f|t
+database|1|f|t|t
+function|3|f|f|t
+io|10|t|f|t
+lock|11|t|f|t
+relation|2|f|f|t
+replslot|4|f|t|t
+slru|12|t|f|t
+subscription|5|f|t|t
+wal|13|t|f|t
+(13 rows)
+\a
 -- ensure that both seqscan and indexscan plans are allowed
 SET enable_seqscan TO on;
 SET enable_indexscan TO on;
diff --git a/src/test/regress/expected/sysviews.out b/src/test/regress/expected/sysviews.out
index 132b56a586..ac9e033c2a 100644
--- a/src/test/regress/expected/sysviews.out
+++ b/src/test/regress/expected/sysviews.out
@@ -129,6 +129,13 @@ select count(*) > 0 as ok from pg_stat_slru;
  t
 (1 row)
 
+-- There should be at least one statistics kind loaded
+select count(*) > 0 as ok from pg_stat_kind_info;
+ ok 
+----
+ t
+(1 row)
+
 -- There must be only one record
 select count(*) = 1 as ok from pg_stat_wal;
  ok 
diff --git a/src/test/regress/sql/stats.sql b/src/test/regress/sql/stats.sql
index 610fd21fae..f79cbbd9d8 100644
--- a/src/test/regress/sql/stats.sql
+++ b/src/test/regress/sql/stats.sql
@@ -14,6 +14,14 @@ SELECT backend_type, object, context FROM pg_stat_io
   ORDER BY backend_type COLLATE "C", object COLLATE "C", context COLLATE "C";
 \a
 
+-- List of loaded statistics kinds.
+\a
+SELECT name, id, fixed_amount, accessed_across_databases, written_to_file
+  FROM pg_stat_kind_info
+  WHERE builtin
+  ORDER BY name COLLATE "C";
+\a
+
 -- ensure that both seqscan and indexscan plans are allowed
 SET enable_seqscan TO on;
 SET enable_indexscan TO on;
diff --git a/src/test/regress/sql/sysviews.sql b/src/test/regress/sql/sysviews.sql
index 507e400ad4..193bf84dad 100644
--- a/src/test/regress/sql/sysviews.sql
+++ b/src/test/regress/sql/sysviews.sql
@@ -70,6 +70,9 @@ select count(*) >= 0 as ok from pg_prepared_xacts;
 -- There will surely be at least one SLRU cache
 select count(*) > 0 as ok from pg_stat_slru;
 
+-- There should be at least one statistics kind loaded
+select count(*) > 0 as ok from pg_stat_kind_info;
+
 -- There must be only one record
 select count(*) = 1 as ok from pg_stat_wal;
 
-- 
Tristan Partin
https://tristan.partin.io

