From 2bcee1f2d01c207e7a5646f3f9c3b7eb31f7cfb0 Mon Sep 17 00:00:00 2001
From: Tristan Partin <tristan@partin.io>
Date: Thu, 30 Apr 2026 16:54:29 +0000
Subject: [PATCH v1] Add pg_state_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 statisitics kind including information about the
kind's id, name, entry count, whether it is builtin, and shared memory
size per entry.

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                  | 100 ++++++++++++++++++
 src/backend/catalog/system_views.sql          |   9 ++
 src/backend/utils/adt/pgstatfuncs.c           |  49 +++++++++
 src/include/catalog/pg_proc.dat               |   8 ++
 .../test_custom_stats/t/001_custom_stats.pl   |  10 +-
 src/test/regress/expected/rules.out           |   6 ++
 src/test/regress/expected/stats.out           |  20 ++++
 src/test/regress/expected/sysviews.out        |  15 +++
 src/test/regress/sql/stats.sql                |   6 ++
 src/test/regress/sql/sysviews.sql             |   7 ++
 10 files changed, 229 insertions(+), 1 deletion(-)

diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 08d5b82455..357605dc1b 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,97 @@ 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>shared_size</structfield> <type>bigint</type>
+       </para>
+       <para>
+        Size in bytes of a shared memory entry for this statistics kind.
+       </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..c3374b56bb 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -1282,6 +1282,15 @@ 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.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 7a9dfa9ba3..8392b4382f 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,53 @@ pg_stat_get_backend_io(PG_FUNCTION_ARGS)
 	return (Datum) 0;
 }
 
+Datum
+pg_stat_get_kind_info(PG_FUNCTION_ARGS)
+{
+#define NUM_KIND_INFO_COLUMNS 5
+	ReturnSetInfo *rsinfo;
+
+	InitMaterializedSRF(fcinfo, 0);
+	rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
+
+	for (int kind = PGSTAT_KIND_MIN; kind <= PGSTAT_KIND_MAX; kind++)
+	{
+		Datum		values[NUM_KIND_INFO_COLUMNS] = {0};
+		bool		nulls[NUM_KIND_INFO_COLUMNS] = {0};
+		const PgStat_KindInfo *info;
+
+		info = pgstat_get_kind_info(kind);
+		if (info == NULL)
+			continue;
+
+		values[0] = Int32GetDatum(kind);
+		values[1] = CStringGetTextDatum(info->name);
+		values[3] = BoolGetDatum(pgstat_is_kind_builtin(kind));
+		values[4] = Int64GetDatum(info->shared_size);
+
+		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;
+			}
+		}
+
+		tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, values, nulls);
+	}
+
+	return (Datum) 0;
+#undef NUM_KIND_INFO_COLUMNS
+}
+
 /*
  * pg_stat_wal_build_tuple
  *
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082..acc1ba5262 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,int8}',
+  proargmodes => '{o,o,o,o,o}',
+  proargnames => '{id,name,count,builtin,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..1967962723 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,14 @@
 $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, shared_size > 0 FROM pg_stat_kind_info
+	    WHERE name LIKE 'test_custom%' ORDER BY name));
+is($result,
+	"26|test_custom_fixed_stats|f|t\n25|test_custom_var_stats|f|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 +71,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..1d43a23508 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1967,6 +1967,12 @@ 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,
+    shared_size
+   FROM pg_stat_get_kind_info() k(id, name, count, builtin, 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 c551abb117..a706bbd4ef 100644
--- a/src/test/regress/expected/stats.out
+++ b/src/test/regress/expected/stats.out
@@ -113,6 +113,26 @@ walwriter|wal|init
 walwriter|wal|normal
 (95 rows)
 \a
+-- List of loaded statistics kinds.
+\a
+SELECT name, builtin FROM pg_stat_kind_info
+  ORDER BY name COLLATE "C";
+name|builtin
+archiver|t
+backend|t
+bgwriter|t
+checkpointer|t
+database|t
+function|t
+io|t
+lock|t
+relation|t
+replslot|t
+slru|t
+subscription|t
+wal|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..5f1f75f69f 100644
--- a/src/test/regress/expected/sysviews.out
+++ b/src/test/regress/expected/sysviews.out
@@ -129,6 +129,21 @@ 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)
+
+-- All rows must have a name and builtin flag
+select count(*) = 0 as ok from pg_stat_kind_info
+  where name is null or builtin is null;
+ 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..b3c59bc0d4 100644
--- a/src/test/regress/sql/stats.sql
+++ b/src/test/regress/sql/stats.sql
@@ -14,6 +14,12 @@ 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, builtin FROM pg_stat_kind_info
+  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..22c55e7c33 100644
--- a/src/test/regress/sql/sysviews.sql
+++ b/src/test/regress/sql/sysviews.sql
@@ -70,6 +70,13 @@ 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;
+
+-- All rows must have a name and builtin flag
+select count(*) = 0 as ok from pg_stat_kind_info
+  where name is null or builtin is null;
+
 -- There must be only one record
 select count(*) = 1 as ok from pg_stat_wal;
 
-- 
Tristan Partin
https://tristan.partin.io

