track generic and custom plans in pg_stat_statements

Started by Sami Imseih10 months ago73 messages
#1Sami Imseih
samimseih@gmail.com
1 attachment(s)

Hi,

Currently, there is little visibility for queries that are being executed
using generic or custom plans. There is pg_prepared_statements which
show generic_plans and custom_plans as of d05b172a760, but this
information is backend local and not very useful to a DBA that wishes
to track this information cumulatively and over time. [0]/messages/by-id/add1e591fbe8874107e75d04328859ec@oss.nttdata.com had intentions
to add these counters to pg_stat_statements, but was withdrawn due
to timing with the commitfest at the time [0]/messages/by-id/add1e591fbe8874107e75d04328859ec@oss.nttdata.com and never picked back up again.

I think it's useful to add these counters.

Therefore, the attached patch adds two new columns to pg_stat_statements:
"generic_plan_calls" and "custom_plan_calls". These columns track the
number of executions performed using a generic or custom plan.

Only non-utility statements are counted, as utility statements with an
optimizable parameterized query (i.e. CTAS) cannot be called with PREPARE.
Additionally, when such statements are repeatedly executed using an extended
protocol prepared statement, pg_stat_statements may not handle them properly,
since queryId is set to 0 during pgss_ProcessUtility.

To avoid introducing two additional counters in CachedPlan, the existing
boolean is_reusable—which determines whether a generic plan is reused to
manage lock requirements—has been repurposed as an enum. This enum now
tracks different plan states, including "generic reused", "generic first time"
and "custom". pg_stat_statements uses these states to differentiate between
generic and custom plans for tracking purposes. ( I felt this was better than
having to carry 2 extra booleans in CachedPlan for this purpose, but that will
be the alternative approach ).

Not included in this patch and maybe for follow-up work, is this information
can be added to EXPLAIN output and perhaps pg_stat_database. Maybe that's
a good idea also?

This patch bumps the version pf pg_stat_statements.

--
Sami Imseih
Amazon Web Services (AWS)

[0]: /messages/by-id/add1e591fbe8874107e75d04328859ec@oss.nttdata.com

Attachments:

v1-0001-add-plan_cache-counters-to-pg_stat_statements.patchapplication/octet-stream; name=v1-0001-add-plan_cache-counters-to-pg_stat_statements.patchDownload
From 8d5bdca84bf3aaa2f341ada9f024a66a2c6ed8fc Mon Sep 17 00:00:00 2001
From: Sami Imseih <simseih@amazon.com>
Date: Tue, 4 Mar 2025 08:54:56 -0600
Subject: [PATCH v1 1/1] add plan_cache counters to pg_stat_statements.

---
 contrib/pg_stat_statements/Makefile           |   3 +-
 .../expected/plan_cache.out                   | 500 ++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |   4 +-
 .../pg_stat_statements--1.12--1.13.sql        |  78 +++
 .../pg_stat_statements/pg_stat_statements.c   |  51 +-
 .../pg_stat_statements.control                |   2 +-
 contrib/pg_stat_statements/sql/plan_cache.sql | 155 ++++++
 doc/src/sgml/pgstatstatements.sgml            |  18 +
 src/backend/utils/cache/plancache.c           |  10 +-
 src/include/utils/plancache.h                 |  12 +-
 10 files changed, 818 insertions(+), 15 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/plan_cache.out
 create mode 100644 contrib/pg_stat_statements/pg_stat_statements--1.12--1.13.sql
 create mode 100644 contrib/pg_stat_statements/sql/plan_cache.sql

diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index 241c02587b..bb81256548 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -7,6 +7,7 @@ OBJS = \
 
 EXTENSION = pg_stat_statements
 DATA = pg_stat_statements--1.4.sql \
+	pg_stat_statements--1.12--1.13.sql \
 	pg_stat_statements--1.11--1.12.sql pg_stat_statements--1.10--1.11.sql \
 	pg_stat_statements--1.9--1.10.sql pg_stat_statements--1.8--1.9.sql \
 	pg_stat_statements--1.7--1.8.sql pg_stat_statements--1.6--1.7.sql \
@@ -20,7 +21,7 @@ LDFLAGS_SL += $(filter -lm, $(LIBS))
 REGRESS_OPTS = --temp-config $(top_srcdir)/contrib/pg_stat_statements/pg_stat_statements.conf
 REGRESS = select dml cursors utility level_tracking planning \
 	user_activity wal entry_timestamp privileges extended \
-	parallel cleanup oldextversions
+	parallel plan_cache cleanup oldextversions
 # Disabled because these tests require "shared_preload_libraries=pg_stat_statements",
 # which typical installcheck users do not have (e.g. buildfarm clients).
 NO_INSTALLCHECK = 1
diff --git a/contrib/pg_stat_statements/expected/plan_cache.out b/contrib/pg_stat_statements/expected/plan_cache.out
new file mode 100644
index 0000000000..24b6e61710
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/plan_cache.out
@@ -0,0 +1,500 @@
+--
+-- Information related to plan cache
+--
+-- setup test
+CREATE TABLE plan_cache_tab (x int, y int);
+CREATE OR REPLACE FUNCTION plan_cache_func(x_in int) RETURNS int AS $$
+    DECLARE
+        y_out int;
+    BEGIN
+        select y into y_out from plan_cache_tab where x = x_in;
+        return y_out;
+    END;
+$$ LANGUAGE plpgsql;
+CREATE OR REPLACE PROCEDURE plan_cache_proc(x_in int) AS $$
+    DECLARE
+        y_out int;
+    BEGIN
+        select y into y_out from plan_cache_tab where x = x_in;
+    END;
+$$ LANGUAGE plpgsql;
+-- plan cache counters for extended query protocol and sql prepared statements
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+select y from plan_cache_tab where x = $1 \parse p1
+\bind_named p1 1
+;
+ y 
+---
+(0 rows)
+
+\bind_named p1 1
+;
+ y 
+---
+(0 rows)
+
+\bind_named p1 1
+;
+ y 
+---
+(0 rows)
+
+\bind_named p1 1
+;
+ y 
+---
+(0 rows)
+
+\bind_named p1 1
+;
+ y 
+---
+(0 rows)
+
+\bind_named p1 1
+;
+ y 
+---
+(0 rows)
+
+SELECT calls, generic_plan_calls, custom_plan_calls, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | generic_plan_calls | custom_plan_calls |                       query                        
+-------+--------------------+-------------------+----------------------------------------------------
+     1 |                  0 |                 0 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+     6 |                  1 |                 5 | select y from plan_cache_tab where x = $1
+(2 rows)
+
+DEALLOCATE p1;
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+PREPARE P1(int) AS select y from plan_cache_tab where x = $1;
+EXECUTE p1(1);
+ y 
+---
+(0 rows)
+
+EXECUTE p1(1);
+ y 
+---
+(0 rows)
+
+EXECUTE p1(1);
+ y 
+---
+(0 rows)
+
+EXECUTE p1(1);
+ y 
+---
+(0 rows)
+
+EXECUTE p1(1);
+ y 
+---
+(0 rows)
+
+EXECUTE p1(1);
+ y 
+---
+(0 rows)
+
+SELECT calls, generic_plan_calls, custom_plan_calls, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | generic_plan_calls | custom_plan_calls |                       query                        
+-------+--------------------+-------------------+----------------------------------------------------
+     1 |                  0 |                 0 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+     6 |                  1 |                 5 | select y from plan_cache_tab where x = $1
+(2 rows)
+
+DEALLOCATE p1;
+-- plan cache counters with plan invalidation.
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+PREPARE P1(int) AS select y from plan_cache_tab where x = $1;
+EXECUTE p1(1);
+ y 
+---
+(0 rows)
+
+EXECUTE p1(1);
+ y 
+---
+(0 rows)
+
+EXECUTE p1(1);
+ y 
+---
+(0 rows)
+
+CREATE INDEX ON plan_cache_tab (x);
+EXECUTE p1(1);
+ y 
+---
+(0 rows)
+
+EXECUTE p1(1);
+ y 
+---
+(0 rows)
+
+EXECUTE p1(1);
+ y 
+---
+(0 rows)
+
+EXECUTE p1(1);
+ y 
+---
+(0 rows)
+
+EXECUTE p1(1);
+ y 
+---
+(0 rows)
+
+EXECUTE p1(1);
+ y 
+---
+(0 rows)
+
+SELECT calls, generic_plan_calls, custom_plan_calls, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | generic_plan_calls | custom_plan_calls |                       query                        
+-------+--------------------+-------------------+----------------------------------------------------
+     1 |                  0 |                 0 | CREATE INDEX ON plan_cache_tab (x)
+     1 |                  0 |                 0 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+     9 |                  4 |                 5 | select y from plan_cache_tab where x = $1
+(3 rows)
+
+DEALLOCATE p1;
+-- plan cache counters for functions and procedures
+SET pg_stat_statements.track = "all";
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+SELECT plan_cache_func(1);
+ plan_cache_func 
+-----------------
+                
+(1 row)
+
+SELECT plan_cache_func(1);
+ plan_cache_func 
+-----------------
+                
+(1 row)
+
+SELECT plan_cache_func(1);
+ plan_cache_func 
+-----------------
+                
+(1 row)
+
+SELECT plan_cache_func(1);
+ plan_cache_func 
+-----------------
+                
+(1 row)
+
+SELECT plan_cache_func(1);
+ plan_cache_func 
+-----------------
+                
+(1 row)
+
+SELECT plan_cache_func(1);
+ plan_cache_func 
+-----------------
+                
+(1 row)
+
+SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | generic_plan_calls | custom_plan_calls | toplevel |                         query                          
+-------+--------------------+-------------------+----------+--------------------------------------------------------
+     1 |                  0 |                 0 | t        | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+     6 |                  0 |                 0 | t        | SELECT plan_cache_func($1)
+     6 |                  1 |                 5 | f        | select y            from plan_cache_tab where x = x_in
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+CALL plan_cache_proc(1);
+CALL plan_cache_proc(1);
+CALL plan_cache_proc(1);
+CALL plan_cache_proc(1);
+CALL plan_cache_proc(1);
+CALL plan_cache_proc(1);
+SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | generic_plan_calls | custom_plan_calls | toplevel |                         query                          
+-------+--------------------+-------------------+----------+--------------------------------------------------------
+     6 |                  0 |                 0 | t        | CALL plan_cache_proc($1)
+     1 |                  0 |                 0 | t        | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+     6 |                  1 |                 5 | f        | select y            from plan_cache_tab where x = x_in
+(3 rows)
+
+-- plan cache counters are not tracked for utility statements
+DROP TABLE plan_cache_tab;
+CREATE TABLE plan_cache_tab AS SELECT $1::int AS x, $2::int AS y \parse p1
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+\bind_named p1 1 2
+;
+DROP TABLE plan_cache_tab;
+\bind_named p1 1 2
+;
+SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | generic_plan_calls | custom_plan_calls | toplevel |                              query                               
+-------+--------------------+-------------------+----------+------------------------------------------------------------------
+     2 |                  0 |                 0 | t        | CREATE TABLE plan_cache_tab AS SELECT $1::int AS x, $2::int AS y
+     2 |                  0 |                 0 | f        | CREATE TABLE plan_cache_tab AS SELECT $1::int AS x, $2::int AS y
+     1 |                  0 |                 0 | t        | DROP TABLE plan_cache_tab
+     1 |                  0 |                 0 | t        | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(4 rows)
+
+DEALLOCATE p1;
+-- plan cache mode generic
+SET plan_cache_mode = force_generic_plan;
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+PREPARE P1(int) AS select y from plan_cache_tab where x = $1;
+EXECUTE p1(1);
+ y 
+---
+ 2
+(1 row)
+
+EXECUTE p1(1);
+ y 
+---
+ 2
+(1 row)
+
+EXECUTE p1(1);
+ y 
+---
+ 2
+(1 row)
+
+EXECUTE p1(1);
+ y 
+---
+ 2
+(1 row)
+
+EXECUTE p1(1);
+ y 
+---
+ 2
+(1 row)
+
+EXECUTE p1(1);
+ y 
+---
+ 2
+(1 row)
+
+SELECT calls, generic_plan_calls, custom_plan_calls, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | generic_plan_calls | custom_plan_calls |                       query                        
+-------+--------------------+-------------------+----------------------------------------------------
+     1 |                  0 |                 0 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+     6 |                  6 |                 0 | select y from plan_cache_tab where x = $1
+(2 rows)
+
+DEALLOCATE p1;
+SET pg_stat_statements.track = "all";
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+SELECT plan_cache_func(1);
+ plan_cache_func 
+-----------------
+               2
+(1 row)
+
+SELECT plan_cache_func(1);
+ plan_cache_func 
+-----------------
+               2
+(1 row)
+
+SELECT plan_cache_func(1);
+ plan_cache_func 
+-----------------
+               2
+(1 row)
+
+SELECT plan_cache_func(1);
+ plan_cache_func 
+-----------------
+               2
+(1 row)
+
+SELECT plan_cache_func(1);
+ plan_cache_func 
+-----------------
+               2
+(1 row)
+
+SELECT plan_cache_func(1);
+ plan_cache_func 
+-----------------
+               2
+(1 row)
+
+CALL plan_cache_proc(1);
+CALL plan_cache_proc(1);
+CALL plan_cache_proc(1);
+CALL plan_cache_proc(1);
+CALL plan_cache_proc(1);
+CALL plan_cache_proc(1);
+SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | generic_plan_calls | custom_plan_calls | toplevel |                         query                          
+-------+--------------------+-------------------+----------+--------------------------------------------------------
+     6 |                  0 |                 0 | t        | CALL plan_cache_proc($1)
+     1 |                  0 |                 0 | t        | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+     6 |                  0 |                 0 | t        | SELECT plan_cache_func($1)
+    12 |                 12 |                 0 | f        | select y            from plan_cache_tab where x = x_in
+(4 rows)
+
+RESET pg_stat_statements.track;
+-- custom plan mode
+SET plan_cache_mode = force_custom_plan;
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+PREPARE P1(int) AS select y from plan_cache_tab where x = $1;
+EXECUTE p1(1);
+ y 
+---
+ 2
+(1 row)
+
+EXECUTE p1(1);
+ y 
+---
+ 2
+(1 row)
+
+EXECUTE p1(1);
+ y 
+---
+ 2
+(1 row)
+
+EXECUTE p1(1);
+ y 
+---
+ 2
+(1 row)
+
+EXECUTE p1(1);
+ y 
+---
+ 2
+(1 row)
+
+EXECUTE p1(1);
+ y 
+---
+ 2
+(1 row)
+
+SELECT calls, generic_plan_calls, custom_plan_calls, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | generic_plan_calls | custom_plan_calls |                       query                        
+-------+--------------------+-------------------+----------------------------------------------------
+     1 |                  0 |                 0 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+     6 |                  0 |                 6 | select y from plan_cache_tab where x = $1
+(2 rows)
+
+DEALLOCATE p1;
+SET pg_stat_statements.track = "all";
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+SELECT plan_cache_func(1);
+ plan_cache_func 
+-----------------
+               2
+(1 row)
+
+SELECT plan_cache_func(1);
+ plan_cache_func 
+-----------------
+               2
+(1 row)
+
+SELECT plan_cache_func(1);
+ plan_cache_func 
+-----------------
+               2
+(1 row)
+
+SELECT plan_cache_func(1);
+ plan_cache_func 
+-----------------
+               2
+(1 row)
+
+SELECT plan_cache_func(1);
+ plan_cache_func 
+-----------------
+               2
+(1 row)
+
+SELECT plan_cache_func(1);
+ plan_cache_func 
+-----------------
+               2
+(1 row)
+
+CALL plan_cache_proc(1);
+CALL plan_cache_proc(1);
+CALL plan_cache_proc(1);
+CALL plan_cache_proc(1);
+CALL plan_cache_proc(1);
+CALL plan_cache_proc(1);
+SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | generic_plan_calls | custom_plan_calls | toplevel |                         query                          
+-------+--------------------+-------------------+----------+--------------------------------------------------------
+     6 |                  0 |                 0 | t        | CALL plan_cache_proc($1)
+     1 |                  0 |                 0 | t        | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+     6 |                  0 |                 0 | t        | SELECT plan_cache_func($1)
+    12 |                  0 |                12 | f        | select y            from plan_cache_tab where x = x_in
+(4 rows)
+
+RESET pg_stat_statements.track;
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 4446af58c5..95f175848f 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -21,6 +21,7 @@ contrib_targets += pg_stat_statements
 install_data(
   'pg_stat_statements.control',
   'pg_stat_statements--1.4.sql',
+  'pg_stat_statements--1.12--1.13.sql',
   'pg_stat_statements--1.11--1.12.sql',
   'pg_stat_statements--1.10--1.11.sql',
   'pg_stat_statements--1.9--1.10.sql',
@@ -54,8 +55,9 @@ tests += {
       'privileges',
       'extended',
       'parallel',
+      'plan_cache'
       'cleanup',
-      'oldextversions',
+      'oldextversions'
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/pg_stat_statements--1.12--1.13.sql b/contrib/pg_stat_statements/pg_stat_statements--1.12--1.13.sql
new file mode 100644
index 0000000000..67f2f7dbca
--- /dev/null
+++ b/contrib/pg_stat_statements/pg_stat_statements--1.12--1.13.sql
@@ -0,0 +1,78 @@
+/* contrib/pg_stat_statements/pg_stat_statements--1.12--1.13.sql */
+
+-- complain if script is sourced in psql, rather than via ALTER EXTENSION
+\echo Use "ALTER EXTENSION pg_stat_statements UPDATE TO '1.13'" to load this file. \quit
+
+/* First we have to remove them from the extension */
+ALTER EXTENSION pg_stat_statements DROP VIEW pg_stat_statements;
+ALTER EXTENSION pg_stat_statements DROP FUNCTION pg_stat_statements(boolean);
+
+/* Then we can drop them */
+DROP VIEW pg_stat_statements;
+DROP FUNCTION pg_stat_statements(boolean);
+
+/* Now redefine */
+CREATE FUNCTION pg_stat_statements(IN showtext boolean,
+    OUT userid oid,
+    OUT dbid oid,
+    OUT toplevel bool,
+    OUT queryid bigint,
+    OUT query text,
+    OUT plans int8,
+    OUT total_plan_time float8,
+    OUT min_plan_time float8,
+    OUT max_plan_time float8,
+    OUT mean_plan_time float8,
+    OUT stddev_plan_time float8,
+    OUT calls int8,
+    OUT total_exec_time float8,
+    OUT min_exec_time float8,
+    OUT max_exec_time float8,
+    OUT mean_exec_time float8,
+    OUT stddev_exec_time float8,
+    OUT rows int8,
+    OUT shared_blks_hit int8,
+    OUT shared_blks_read int8,
+    OUT shared_blks_dirtied int8,
+    OUT shared_blks_written int8,
+    OUT local_blks_hit int8,
+    OUT local_blks_read int8,
+    OUT local_blks_dirtied int8,
+    OUT local_blks_written int8,
+    OUT temp_blks_read int8,
+    OUT temp_blks_written int8,
+    OUT shared_blk_read_time float8,
+    OUT shared_blk_write_time float8,
+    OUT local_blk_read_time float8,
+    OUT local_blk_write_time float8,
+    OUT temp_blk_read_time float8,
+    OUT temp_blk_write_time float8,
+    OUT wal_records int8,
+    OUT wal_fpi int8,
+    OUT wal_bytes numeric,
+    OUT wal_buffers_full int8,
+    OUT jit_functions int8,
+    OUT jit_generation_time float8,
+    OUT jit_inlining_count int8,
+    OUT jit_inlining_time float8,
+    OUT jit_optimization_count int8,
+    OUT jit_optimization_time float8,
+    OUT jit_emission_count int8,
+    OUT jit_emission_time float8,
+    OUT jit_deform_count int8,
+    OUT jit_deform_time float8,
+    OUT parallel_workers_to_launch int8,
+    OUT parallel_workers_launched int8,
+    OUT stats_since timestamp with time zone,
+    OUT minmax_stats_since timestamp with time zone,
+    OUT generic_plan_calls int8,
+    OUT custom_plan_calls int8
+)
+RETURNS SETOF record
+AS 'MODULE_PATHNAME', 'pg_stat_statements_1_13'
+LANGUAGE C STRICT VOLATILE PARALLEL SAFE;
+
+CREATE VIEW pg_stat_statements AS
+  SELECT * FROM pg_stat_statements(true);
+
+GRANT SELECT ON pg_stat_statements TO PUBLIC;
diff --git a/contrib/pg_stat_statements/pg_stat_statements.c b/contrib/pg_stat_statements/pg_stat_statements.c
index b245d04097..e5ff32ab58 100644
--- a/contrib/pg_stat_statements/pg_stat_statements.c
+++ b/contrib/pg_stat_statements/pg_stat_statements.c
@@ -69,6 +69,7 @@
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/memutils.h"
+#include "utils/plancache.h"
 #include "utils/timestamp.h"
 
 PG_MODULE_MAGIC;
@@ -111,6 +112,7 @@ typedef enum pgssVersion
 	PGSS_V1_10,
 	PGSS_V1_11,
 	PGSS_V1_12,
+	PGSS_V1_13,
 } pgssVersion;
 
 typedef enum pgssStoreKind
@@ -207,6 +209,8 @@ typedef struct Counters
 											 * to be launched */
 	int64		parallel_workers_launched;	/* # of parallel workers actually
 											 * launched */
+	int64		generic_plan_calls;		/* number of calls using a generic plan */
+	int64		custom_plan_calls;		/* number of calls using a custom plan */
 } Counters;
 
 /*
@@ -321,6 +325,7 @@ PG_FUNCTION_INFO_V1(pg_stat_statements_1_9);
 PG_FUNCTION_INFO_V1(pg_stat_statements_1_10);
 PG_FUNCTION_INFO_V1(pg_stat_statements_1_11);
 PG_FUNCTION_INFO_V1(pg_stat_statements_1_12);
+PG_FUNCTION_INFO_V1(pg_stat_statements_1_13);
 PG_FUNCTION_INFO_V1(pg_stat_statements);
 PG_FUNCTION_INFO_V1(pg_stat_statements_info);
 
@@ -353,7 +358,8 @@ static void pgss_store(const char *query, uint64 queryId,
 					   const struct JitInstrumentation *jitusage,
 					   JumbleState *jstate,
 					   int parallel_workers_to_launch,
-					   int parallel_workers_launched);
+					   int parallel_workers_launched,
+					   CachedPlan *cplan);
 static void pg_stat_statements_internal(FunctionCallInfo fcinfo,
 										pgssVersion api_version,
 										bool showtext);
@@ -875,7 +881,8 @@ pgss_post_parse_analyze(ParseState *pstate, Query *query, JumbleState *jstate)
 				   NULL,
 				   jstate,
 				   0,
-				   0);
+				   0,
+				   NULL);
 }
 
 /*
@@ -955,7 +962,8 @@ pgss_planner(Query *parse,
 				   NULL,
 				   NULL,
 				   0,
-				   0);
+				   0,
+				   NULL);
 	}
 	else
 	{
@@ -1097,7 +1105,8 @@ pgss_ExecutorEnd(QueryDesc *queryDesc)
 				   queryDesc->estate->es_jit ? &queryDesc->estate->es_jit->instr : NULL,
 				   NULL,
 				   queryDesc->estate->es_parallel_workers_to_launch,
-				   queryDesc->estate->es_parallel_workers_launched);
+				   queryDesc->estate->es_parallel_workers_launched,
+				   queryDesc->cplan);
 	}
 
 	if (prev_ExecutorEnd)
@@ -1230,7 +1239,8 @@ pgss_ProcessUtility(PlannedStmt *pstmt, const char *queryString,
 				   NULL,
 				   NULL,
 				   0,
-				   0);
+				   0,
+				   NULL);
 	}
 	else
 	{
@@ -1293,7 +1303,8 @@ pgss_store(const char *query, uint64 queryId,
 		   const struct JitInstrumentation *jitusage,
 		   JumbleState *jstate,
 		   int parallel_workers_to_launch,
-		   int parallel_workers_launched)
+		   int parallel_workers_launched,
+		   CachedPlan *cplan)
 {
 	pgssHashKey key;
 	pgssEntry  *entry;
@@ -1501,6 +1512,11 @@ pgss_store(const char *query, uint64 queryId,
 		entry->counters.parallel_workers_to_launch += parallel_workers_to_launch;
 		entry->counters.parallel_workers_launched += parallel_workers_launched;
 
+		if (cplan && cplan->status > PLAN_CACHE_STATUS_CUSTOM_PLAN)
+			entry->counters.generic_plan_calls++;
+		if (cplan && cplan->status == PLAN_CACHE_STATUS_CUSTOM_PLAN)
+			entry->counters.custom_plan_calls++;
+
 		SpinLockRelease(&entry->mutex);
 	}
 
@@ -1568,7 +1584,8 @@ pg_stat_statements_reset(PG_FUNCTION_ARGS)
 #define PG_STAT_STATEMENTS_COLS_V1_10	43
 #define PG_STAT_STATEMENTS_COLS_V1_11	49
 #define PG_STAT_STATEMENTS_COLS_V1_12	52
-#define PG_STAT_STATEMENTS_COLS			52	/* maximum of above */
+#define PG_STAT_STATEMENTS_COLS_V1_13	54
+#define PG_STAT_STATEMENTS_COLS			54	/* maximum of above */
 
 /*
  * Retrieve statement statistics.
@@ -1580,6 +1597,16 @@ pg_stat_statements_reset(PG_FUNCTION_ARGS)
  * expected API version is identified by embedding it in the C name of the
  * function.  Unfortunately we weren't bright enough to do that for 1.1.
  */
+Datum
+pg_stat_statements_1_13(PG_FUNCTION_ARGS)
+{
+	bool		showtext = PG_GETARG_BOOL(0);
+
+	pg_stat_statements_internal(fcinfo, PGSS_V1_13, showtext);
+
+	return (Datum) 0;
+}
+
 Datum
 pg_stat_statements_1_12(PG_FUNCTION_ARGS)
 {
@@ -1738,6 +1765,10 @@ pg_stat_statements_internal(FunctionCallInfo fcinfo,
 			if (api_version != PGSS_V1_12)
 				elog(ERROR, "incorrect number of output arguments");
 			break;
+		case PG_STAT_STATEMENTS_COLS_V1_13:
+			if (api_version != PGSS_V1_13)
+				elog(ERROR, "incorrect number of output arguments");
+			break;
 		default:
 			elog(ERROR, "incorrect number of output arguments");
 	}
@@ -1995,6 +2026,11 @@ pg_stat_statements_internal(FunctionCallInfo fcinfo,
 			values[i++] = TimestampTzGetDatum(stats_since);
 			values[i++] = TimestampTzGetDatum(minmax_stats_since);
 		}
+		if (api_version >= PGSS_V1_13)
+		{
+			values[i++] = Int64GetDatumFast(tmp.generic_plan_calls);
+			values[i++] = Int64GetDatumFast(tmp.custom_plan_calls);
+		}
 
 		Assert(i == (api_version == PGSS_V1_0 ? PG_STAT_STATEMENTS_COLS_V1_0 :
 					 api_version == PGSS_V1_1 ? PG_STAT_STATEMENTS_COLS_V1_1 :
@@ -2005,6 +2041,7 @@ pg_stat_statements_internal(FunctionCallInfo fcinfo,
 					 api_version == PGSS_V1_10 ? PG_STAT_STATEMENTS_COLS_V1_10 :
 					 api_version == PGSS_V1_11 ? PG_STAT_STATEMENTS_COLS_V1_11 :
 					 api_version == PGSS_V1_12 ? PG_STAT_STATEMENTS_COLS_V1_12 :
+					 api_version == PGSS_V1_13 ? PG_STAT_STATEMENTS_COLS_V1_13 :
 					 -1 /* fail if you forget to update this assert */ ));
 
 		tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, values, nulls);
diff --git a/contrib/pg_stat_statements/pg_stat_statements.control b/contrib/pg_stat_statements/pg_stat_statements.control
index d45ebc12e3..2eee0ceffa 100644
--- a/contrib/pg_stat_statements/pg_stat_statements.control
+++ b/contrib/pg_stat_statements/pg_stat_statements.control
@@ -1,5 +1,5 @@
 # pg_stat_statements extension
 comment = 'track planning and execution statistics of all SQL statements executed'
-default_version = '1.12'
+default_version = '1.13'
 module_pathname = '$libdir/pg_stat_statements'
 relocatable = true
diff --git a/contrib/pg_stat_statements/sql/plan_cache.sql b/contrib/pg_stat_statements/sql/plan_cache.sql
new file mode 100644
index 0000000000..69ed75a278
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/plan_cache.sql
@@ -0,0 +1,155 @@
+--
+-- Information related to plan cache
+--
+
+-- setup test
+CREATE TABLE plan_cache_tab (x int, y int);
+CREATE OR REPLACE FUNCTION plan_cache_func(x_in int) RETURNS int AS $$
+    DECLARE
+        y_out int;
+    BEGIN
+        select y into y_out from plan_cache_tab where x = x_in;
+        return y_out;
+    END;
+$$ LANGUAGE plpgsql;
+CREATE OR REPLACE PROCEDURE plan_cache_proc(x_in int) AS $$
+    DECLARE
+        y_out int;
+    BEGIN
+        select y into y_out from plan_cache_tab where x = x_in;
+    END;
+$$ LANGUAGE plpgsql;
+
+-- plan cache counters for extended query protocol and sql prepared statements
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+select y from plan_cache_tab where x = $1 \parse p1
+\bind_named p1 1
+;
+\bind_named p1 1
+;
+\bind_named p1 1
+;
+\bind_named p1 1
+;
+\bind_named p1 1
+;
+\bind_named p1 1
+;
+SELECT calls, generic_plan_calls, custom_plan_calls, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+DEALLOCATE p1;
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+PREPARE P1(int) AS select y from plan_cache_tab where x = $1;
+EXECUTE p1(1);
+EXECUTE p1(1);
+EXECUTE p1(1);
+EXECUTE p1(1);
+EXECUTE p1(1);
+EXECUTE p1(1);
+SELECT calls, generic_plan_calls, custom_plan_calls, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+DEALLOCATE p1;
+
+-- plan cache counters with plan invalidation.
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+PREPARE P1(int) AS select y from plan_cache_tab where x = $1;
+EXECUTE p1(1);
+EXECUTE p1(1);
+EXECUTE p1(1);
+CREATE INDEX ON plan_cache_tab (x);
+EXECUTE p1(1);
+EXECUTE p1(1);
+EXECUTE p1(1);
+EXECUTE p1(1);
+EXECUTE p1(1);
+EXECUTE p1(1);
+SELECT calls, generic_plan_calls, custom_plan_calls, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+DEALLOCATE p1;
+
+-- plan cache counters for functions and procedures
+SET pg_stat_statements.track = "all";
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+SELECT plan_cache_func(1);
+SELECT plan_cache_func(1);
+SELECT plan_cache_func(1);
+SELECT plan_cache_func(1);
+SELECT plan_cache_func(1);
+SELECT plan_cache_func(1);
+SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+CALL plan_cache_proc(1);
+CALL plan_cache_proc(1);
+CALL plan_cache_proc(1);
+CALL plan_cache_proc(1);
+CALL plan_cache_proc(1);
+CALL plan_cache_proc(1);
+SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+-- plan cache counters are not tracked for utility statements
+DROP TABLE plan_cache_tab;
+CREATE TABLE plan_cache_tab AS SELECT $1::int AS x, $2::int AS y \parse p1
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+\bind_named p1 1 2
+;
+DROP TABLE plan_cache_tab;
+\bind_named p1 1 2
+;
+SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+DEALLOCATE p1;
+
+-- plan cache mode generic
+SET plan_cache_mode = force_generic_plan;
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+PREPARE P1(int) AS select y from plan_cache_tab where x = $1;
+EXECUTE p1(1);
+EXECUTE p1(1);
+EXECUTE p1(1);
+EXECUTE p1(1);
+EXECUTE p1(1);
+EXECUTE p1(1);
+SELECT calls, generic_plan_calls, custom_plan_calls, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+DEALLOCATE p1;
+SET pg_stat_statements.track = "all";
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+SELECT plan_cache_func(1);
+SELECT plan_cache_func(1);
+SELECT plan_cache_func(1);
+SELECT plan_cache_func(1);
+SELECT plan_cache_func(1);
+SELECT plan_cache_func(1);
+CALL plan_cache_proc(1);
+CALL plan_cache_proc(1);
+CALL plan_cache_proc(1);
+CALL plan_cache_proc(1);
+CALL plan_cache_proc(1);
+CALL plan_cache_proc(1);
+SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+RESET pg_stat_statements.track;
+
+-- custom plan mode
+SET plan_cache_mode = force_custom_plan;
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+PREPARE P1(int) AS select y from plan_cache_tab where x = $1;
+EXECUTE p1(1);
+EXECUTE p1(1);
+EXECUTE p1(1);
+EXECUTE p1(1);
+EXECUTE p1(1);
+EXECUTE p1(1);
+SELECT calls, generic_plan_calls, custom_plan_calls, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+DEALLOCATE p1;
+SET pg_stat_statements.track = "all";
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+SELECT plan_cache_func(1);
+SELECT plan_cache_func(1);
+SELECT plan_cache_func(1);
+SELECT plan_cache_func(1);
+SELECT plan_cache_func(1);
+SELECT plan_cache_func(1);
+CALL plan_cache_proc(1);
+CALL plan_cache_proc(1);
+CALL plan_cache_proc(1);
+CALL plan_cache_proc(1);
+CALL plan_cache_proc(1);
+CALL plan_cache_proc(1);
+SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+RESET pg_stat_statements.track;
\ No newline at end of file
diff --git a/doc/src/sgml/pgstatstatements.sgml b/doc/src/sgml/pgstatstatements.sgml
index e2ac1c2d50..8a2379e22c 100644
--- a/doc/src/sgml/pgstatstatements.sgml
+++ b/doc/src/sgml/pgstatstatements.sgml
@@ -575,6 +575,24 @@
        <structfield>max_exec_time</structfield>)
       </para></entry>
      </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>generic_plan_calls</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Total number of non-utility statements executed using a generic plan
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>custom_plan_calls</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Total number of non-utility statements executed using a custom plan
+      </para></entry>
+     </row>
     </tbody>
    </tgroup>
   </table>
diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c
index 6c2979d5c8..510e2d8286 100644
--- a/src/backend/utils/cache/plancache.c
+++ b/src/backend/utils/cache/plancache.c
@@ -1056,7 +1056,7 @@ BuildCachedPlan(CachedPlanSource *plansource, List *qlist,
 	plan->stmt_context = stmt_context;
 	plan->is_oneshot = plansource->is_oneshot;
 	plan->is_saved = false;
-	plan->is_reused = false;
+	plan->status = PLAN_CACHE_STATUS_UNKNOWN;
 	plan->is_valid = true;
 
 	/* assign generation number to new plan */
@@ -1294,7 +1294,8 @@ cached_plan_cost(CachedPlan *plan, bool include_planner)
  * locks are acquired. In such cases, CheckCachedPlan() does not take locks
  * on relations subject to initial runtime pruning; instead, these locks are
  * deferred until execution startup, when ExecDoInitialPruning() performs
- * initial pruning.  The plan's "is_reused" flag is set to indicate that
+ * initial pruning.  The plan's "status" flag is set to
+ * PLAN_CACHE_STATUS_GENERIC_PLAN_REUSE to indicate that
  * CachedPlanRequiresLocking() should return true when called by
  * ExecDoInitialPruning().
  *
@@ -1335,7 +1336,7 @@ GetCachedPlan(CachedPlanSource *plansource, ParamListInfo boundParams,
 			plan = plansource->gplan;
 			Assert(plan->magic == CACHEDPLAN_MAGIC);
 			/* Reusing the existing plan, so not all locks may be acquired. */
-			plan->is_reused = true;
+			plan->status = PLAN_CACHE_STATUS_GENERIC_PLAN_REUSE;
 		}
 		else
 		{
@@ -1379,6 +1380,8 @@ GetCachedPlan(CachedPlanSource *plansource, ParamListInfo boundParams,
 			 * BuildCachedPlan to do that by passing NIL.
 			 */
 			qlist = NIL;
+
+			plan->status = PLAN_CACHE_STATUS_GENERIC_PLAN_BUILD;
 		}
 	}
 
@@ -1390,6 +1393,7 @@ GetCachedPlan(CachedPlanSource *plansource, ParamListInfo boundParams,
 		plansource->total_custom_cost += cached_plan_cost(plan, true);
 
 		plansource->num_custom_plans++;
+		plan->status = PLAN_CACHE_STATUS_CUSTOM_PLAN;
 	}
 	else
 	{
diff --git a/src/include/utils/plancache.h b/src/include/utils/plancache.h
index f1fc770733..72b756864b 100644
--- a/src/include/utils/plancache.h
+++ b/src/include/utils/plancache.h
@@ -36,6 +36,14 @@ typedef enum
 	PLAN_CACHE_MODE_FORCE_CUSTOM_PLAN,
 }			PlanCacheMode;
 
+typedef enum
+{
+	PLAN_CACHE_STATUS_UNKNOWN = 0,
+	PLAN_CACHE_STATUS_CUSTOM_PLAN,
+	PLAN_CACHE_STATUS_GENERIC_PLAN_BUILD,
+	PLAN_CACHE_STATUS_GENERIC_PLAN_REUSE
+}			PlanCacheStatus;
+
 /* GUC parameter */
 extern PGDLLIMPORT int plan_cache_mode;
 
@@ -153,7 +161,7 @@ typedef struct CachedPlan
 	List	   *stmt_list;		/* list of PlannedStmts */
 	bool		is_oneshot;		/* is it a "oneshot" plan? */
 	bool		is_saved;		/* is CachedPlan in a long-lived context? */
-	bool		is_reused;		/* is it a reused generic plan? */
+	PlanCacheStatus status;		/* is it a reused generic plan? */
 	bool		is_valid;		/* is the stmt_list currently valid? */
 	Oid			planRoleId;		/* Role ID the plan was created for */
 	bool		dependsOnRole;	/* is plan specific to that role? */
@@ -257,7 +265,7 @@ extern void FreeCachedExpression(CachedExpression *cexpr);
 static inline bool
 CachedPlanRequiresLocking(CachedPlan *cplan)
 {
-	return !cplan->is_oneshot && cplan->is_reused;
+	return !cplan->is_oneshot && (cplan->status == PLAN_CACHE_STATUS_GENERIC_PLAN_REUSE);
 }
 
 /*
-- 
2.47.1

#2Greg Sabino Mullane
htamfids@gmail.com
In reply to: Sami Imseih (#1)
Re: track generic and custom plans in pg_stat_statements

Overall I like the idea; adds some nice visibility to something that has
been very ephemeral in the past.

Not included in this patch and maybe for follow-up work, is this

information a good idea also?

can be added to EXPLAIN output and perhaps pg_stat_database.

I could see EXPLAIN being somewhat useful (especially for non-interactive
things like auto_explain), so a weak +1 on that.

Definitely not useful for pg_stat_database IMHO.

Some quick comments on the patch:

FILE: contrib/pg_stat_statements/expected/plan_cache.out

These tests seem very verbose. Why not just prepare a simple query:

prepare foo as select $1 > 0;
execute foo(1);
...then tweak things via plan_cache_mode to ensure we test the right things?

Also would be nice to constrain the pg_stat_statement SELECTs with some
careful WHERE clauses. Would also allow you to remove the ORDER BY and the
COLLATE.

FILE: contrib/pg_stat_statements/meson.build

oldextversions should still have a trailing comma

FILE: contrib/pg_stat_statements/pg_stat_statements.c

+ if (cplan && cplan->status > PLAN_CACHE_STATUS_CUSTOM_PLAN)

Not really comfortable with using the enum like this. Better would be
explicitly listing the two states that lead to the increment. Not as good
but still better:
cplan->status >= PLAN_CACHE_STATUS_GENERIC_PLAN_BUILD

+ PlanCacheStatus status; /* is it a reused generic plan? */

The comment should be updated

FILE: contrib/pg_stat_statements/sql/plan_cache.sql

Missing a newline at the end

FILE: doc/src/sgml/pgstatstatements.sgml

+ Total number of non-utility statements executed using a generic plan

I'm not sure we need to specify non-utility here.

Cheers,
Greg

--
Crunchy Data - https://www.crunchydata.com
Enterprise Postgres Software Products & Tech Support

#3Sami Imseih
samimseih@gmail.com
In reply to: Greg Sabino Mullane (#2)
1 attachment(s)
Re: track generic and custom plans in pg_stat_statements

Thanks for the review!

I could see EXPLAIN being somewhat useful (especially for non-interactive things like auto_explain), so a weak +1 on that.

I'll make this a follow-up to this patch.

Definitely not useful for pg_stat_database IMHO.

agree as well. I did not have strong feelings about this.

FILE: contrib/pg_stat_statements/expected/plan_cache.out

These tests seem very verbose. Why not just prepare a simple query:

prepare foo as select $1 > 0;
execute foo(1);
...then tweak things via plan_cache_mode to ensure we test the right things?

Yeah, I went overboard to see what others think.
I toned it down drastically for v2 following your advice.

oldextversions should still have a trailing comma

done

FILE: contrib/pg_stat_statements/pg_stat_statements.c

+ if (cplan && cplan->status > PLAN_CACHE_STATUS_CUSTOM_PLAN)

Not really comfortable with using the enum like this. Better would be explicitly listing the two states that lead to the increment. Not as good but still better:
cplan->status >= PLAN_CACHE_STATUS_GENERIC_PLAN_BUILD

fair, I use explicit values for each plan type.

+ PlanCacheStatus status; /* is it a reused generic plan? */

The comment should be updated

done

FILE: contrib/pg_stat_statements/sql/plan_cache.sql

Missing a newline at the end

done

FILE: doc/src/sgml/pgstatstatements.sgml

+ Total number of non-utility statements executed using a generic plan

I'm not sure we need to specify non-utility here.

fair, I did not have strong feeling about this either.

Please see v2

Thanks!

--
Sami Imseih
Amazon Web Services (AWS)

Attachments:

v2-0001-add-plan_cache-counters-to-pg_stat_statements.patchapplication/octet-stream; name=v2-0001-add-plan_cache-counters-to-pg_stat_statements.patchDownload
From 6ff9149e67268c3be2405f580666ba1bd45401c3 Mon Sep 17 00:00:00 2001
From: Sami Imseih <simseih@amazon.com>
Date: Tue, 4 Mar 2025 08:54:56 -0600
Subject: [PATCH v2 1/1] add plan_cache counters to pg_stat_statements.

---
 contrib/pg_stat_statements/Makefile           |  3 +-
 .../expected/plan_cache.out                   | 87 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  2 +
 .../pg_stat_statements--1.12--1.13.sql        | 78 +++++++++++++++++
 .../pg_stat_statements/pg_stat_statements.c   | 55 ++++++++++--
 .../pg_stat_statements.control                |  2 +-
 contrib/pg_stat_statements/sql/plan_cache.sql | 50 +++++++++++
 doc/src/sgml/pgstatstatements.sgml            | 18 ++++
 src/backend/utils/cache/plancache.c           | 10 ++-
 src/include/utils/plancache.h                 | 12 ++-
 10 files changed, 303 insertions(+), 14 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/plan_cache.out
 create mode 100644 contrib/pg_stat_statements/pg_stat_statements--1.12--1.13.sql
 create mode 100644 contrib/pg_stat_statements/sql/plan_cache.sql

diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index 241c02587b..bb81256548 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -7,6 +7,7 @@ OBJS = \
 
 EXTENSION = pg_stat_statements
 DATA = pg_stat_statements--1.4.sql \
+	pg_stat_statements--1.12--1.13.sql \
 	pg_stat_statements--1.11--1.12.sql pg_stat_statements--1.10--1.11.sql \
 	pg_stat_statements--1.9--1.10.sql pg_stat_statements--1.8--1.9.sql \
 	pg_stat_statements--1.7--1.8.sql pg_stat_statements--1.6--1.7.sql \
@@ -20,7 +21,7 @@ LDFLAGS_SL += $(filter -lm, $(LIBS))
 REGRESS_OPTS = --temp-config $(top_srcdir)/contrib/pg_stat_statements/pg_stat_statements.conf
 REGRESS = select dml cursors utility level_tracking planning \
 	user_activity wal entry_timestamp privileges extended \
-	parallel cleanup oldextversions
+	parallel plan_cache cleanup oldextversions
 # Disabled because these tests require "shared_preload_libraries=pg_stat_statements",
 # which typical installcheck users do not have (e.g. buildfarm clients).
 NO_INSTALLCHECK = 1
diff --git a/contrib/pg_stat_statements/expected/plan_cache.out b/contrib/pg_stat_statements/expected/plan_cache.out
new file mode 100644
index 0000000000..bf11794528
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/plan_cache.out
@@ -0,0 +1,87 @@
+--
+-- Information related to plan cache
+--
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- plan cache counters for prepared statements
+PREPARE p1 AS SELECT $1;
+-- plan cache auto
+SET plan_cache_mode TO auto;
+EXECUTE p1(1);
+ ?column? 
+----------
+ 1
+(1 row)
+
+-- force generic plan
+SET plan_cache_mode TO force_generic_plan;
+EXECUTE p1(1);
+ ?column? 
+----------
+ 1
+(1 row)
+
+-- force custom plan
+SET plan_cache_mode TO force_custom_plan;
+EXECUTE p1(1);
+ ?column? 
+----------
+ 1
+(1 row)
+
+-- plan cache counters for functions and procedures
+SET pg_stat_statements.track = 'all';
+CREATE OR REPLACE FUNCTION select_one_func(int) RETURNS VOID AS $$
+DECLARE
+    ret INT;
+BEGIN
+    SELECT $1 INTO ret;
+END;
+$$ LANGUAGE plpgsql;
+CREATE OR REPLACE PROCEDURE select_one_proc(int) AS $$
+DECLARE
+    ret INT;
+BEGIN
+    select $1 INTO ret;
+END;
+$$ LANGUAGE plpgsql;
+-- plan cache auto
+SET plan_cache_mode TO auto;
+SELECT select_one_func(1);
+ select_one_func 
+-----------------
+ 
+(1 row)
+
+CALL select_one_proc(1);
+-- force generic plan
+SET plan_cache_mode TO force_generic_plan;
+SELECT select_one_func(1);
+ select_one_func 
+-----------------
+ 
+(1 row)
+
+CALL select_one_proc(1);
+-- force custom plan
+SET plan_cache_mode TO force_custom_plan;
+SELECT select_one_func(1);
+ select_one_func 
+-----------------
+ 
+(1 row)
+
+CALL select_one_proc(1);
+-- get the plan cache counters
+SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, calls, query FROM pg_stat_statements
+  WHERE query = 'SELECT $1' ORDER BY query COLLATE "C";
+ calls | generic_plan_calls | custom_plan_calls | toplevel | calls |   query   
+-------+--------------------+-------------------+----------+-------+-----------
+     6 |                  2 |                 4 | f        |     6 | SELECT $1
+     3 |                  1 |                 2 | t        |     3 | SELECT $1
+(2 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 4446af58c5..4d506ad285 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -21,6 +21,7 @@ contrib_targets += pg_stat_statements
 install_data(
   'pg_stat_statements.control',
   'pg_stat_statements--1.4.sql',
+  'pg_stat_statements--1.12--1.13.sql',
   'pg_stat_statements--1.11--1.12.sql',
   'pg_stat_statements--1.10--1.11.sql',
   'pg_stat_statements--1.9--1.10.sql',
@@ -54,6 +55,7 @@ tests += {
       'privileges',
       'extended',
       'parallel',
+      'plan_cache'
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/pg_stat_statements--1.12--1.13.sql b/contrib/pg_stat_statements/pg_stat_statements--1.12--1.13.sql
new file mode 100644
index 0000000000..67f2f7dbca
--- /dev/null
+++ b/contrib/pg_stat_statements/pg_stat_statements--1.12--1.13.sql
@@ -0,0 +1,78 @@
+/* contrib/pg_stat_statements/pg_stat_statements--1.12--1.13.sql */
+
+-- complain if script is sourced in psql, rather than via ALTER EXTENSION
+\echo Use "ALTER EXTENSION pg_stat_statements UPDATE TO '1.13'" to load this file. \quit
+
+/* First we have to remove them from the extension */
+ALTER EXTENSION pg_stat_statements DROP VIEW pg_stat_statements;
+ALTER EXTENSION pg_stat_statements DROP FUNCTION pg_stat_statements(boolean);
+
+/* Then we can drop them */
+DROP VIEW pg_stat_statements;
+DROP FUNCTION pg_stat_statements(boolean);
+
+/* Now redefine */
+CREATE FUNCTION pg_stat_statements(IN showtext boolean,
+    OUT userid oid,
+    OUT dbid oid,
+    OUT toplevel bool,
+    OUT queryid bigint,
+    OUT query text,
+    OUT plans int8,
+    OUT total_plan_time float8,
+    OUT min_plan_time float8,
+    OUT max_plan_time float8,
+    OUT mean_plan_time float8,
+    OUT stddev_plan_time float8,
+    OUT calls int8,
+    OUT total_exec_time float8,
+    OUT min_exec_time float8,
+    OUT max_exec_time float8,
+    OUT mean_exec_time float8,
+    OUT stddev_exec_time float8,
+    OUT rows int8,
+    OUT shared_blks_hit int8,
+    OUT shared_blks_read int8,
+    OUT shared_blks_dirtied int8,
+    OUT shared_blks_written int8,
+    OUT local_blks_hit int8,
+    OUT local_blks_read int8,
+    OUT local_blks_dirtied int8,
+    OUT local_blks_written int8,
+    OUT temp_blks_read int8,
+    OUT temp_blks_written int8,
+    OUT shared_blk_read_time float8,
+    OUT shared_blk_write_time float8,
+    OUT local_blk_read_time float8,
+    OUT local_blk_write_time float8,
+    OUT temp_blk_read_time float8,
+    OUT temp_blk_write_time float8,
+    OUT wal_records int8,
+    OUT wal_fpi int8,
+    OUT wal_bytes numeric,
+    OUT wal_buffers_full int8,
+    OUT jit_functions int8,
+    OUT jit_generation_time float8,
+    OUT jit_inlining_count int8,
+    OUT jit_inlining_time float8,
+    OUT jit_optimization_count int8,
+    OUT jit_optimization_time float8,
+    OUT jit_emission_count int8,
+    OUT jit_emission_time float8,
+    OUT jit_deform_count int8,
+    OUT jit_deform_time float8,
+    OUT parallel_workers_to_launch int8,
+    OUT parallel_workers_launched int8,
+    OUT stats_since timestamp with time zone,
+    OUT minmax_stats_since timestamp with time zone,
+    OUT generic_plan_calls int8,
+    OUT custom_plan_calls int8
+)
+RETURNS SETOF record
+AS 'MODULE_PATHNAME', 'pg_stat_statements_1_13'
+LANGUAGE C STRICT VOLATILE PARALLEL SAFE;
+
+CREATE VIEW pg_stat_statements AS
+  SELECT * FROM pg_stat_statements(true);
+
+GRANT SELECT ON pg_stat_statements TO PUBLIC;
diff --git a/contrib/pg_stat_statements/pg_stat_statements.c b/contrib/pg_stat_statements/pg_stat_statements.c
index b245d04097..aab4ac15f7 100644
--- a/contrib/pg_stat_statements/pg_stat_statements.c
+++ b/contrib/pg_stat_statements/pg_stat_statements.c
@@ -69,6 +69,7 @@
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/memutils.h"
+#include "utils/plancache.h"
 #include "utils/timestamp.h"
 
 PG_MODULE_MAGIC;
@@ -111,6 +112,7 @@ typedef enum pgssVersion
 	PGSS_V1_10,
 	PGSS_V1_11,
 	PGSS_V1_12,
+	PGSS_V1_13,
 } pgssVersion;
 
 typedef enum pgssStoreKind
@@ -207,6 +209,8 @@ typedef struct Counters
 											 * to be launched */
 	int64		parallel_workers_launched;	/* # of parallel workers actually
 											 * launched */
+	int64		generic_plan_calls;		/* number of calls using a generic plan */
+	int64		custom_plan_calls;		/* number of calls using a custom plan */
 } Counters;
 
 /*
@@ -321,6 +325,7 @@ PG_FUNCTION_INFO_V1(pg_stat_statements_1_9);
 PG_FUNCTION_INFO_V1(pg_stat_statements_1_10);
 PG_FUNCTION_INFO_V1(pg_stat_statements_1_11);
 PG_FUNCTION_INFO_V1(pg_stat_statements_1_12);
+PG_FUNCTION_INFO_V1(pg_stat_statements_1_13);
 PG_FUNCTION_INFO_V1(pg_stat_statements);
 PG_FUNCTION_INFO_V1(pg_stat_statements_info);
 
@@ -353,7 +358,8 @@ static void pgss_store(const char *query, uint64 queryId,
 					   const struct JitInstrumentation *jitusage,
 					   JumbleState *jstate,
 					   int parallel_workers_to_launch,
-					   int parallel_workers_launched);
+					   int parallel_workers_launched,
+					   CachedPlan *cplan);
 static void pg_stat_statements_internal(FunctionCallInfo fcinfo,
 										pgssVersion api_version,
 										bool showtext);
@@ -875,7 +881,8 @@ pgss_post_parse_analyze(ParseState *pstate, Query *query, JumbleState *jstate)
 				   NULL,
 				   jstate,
 				   0,
-				   0);
+				   0,
+				   NULL);
 }
 
 /*
@@ -955,7 +962,8 @@ pgss_planner(Query *parse,
 				   NULL,
 				   NULL,
 				   0,
-				   0);
+				   0,
+				   NULL);
 	}
 	else
 	{
@@ -1097,7 +1105,8 @@ pgss_ExecutorEnd(QueryDesc *queryDesc)
 				   queryDesc->estate->es_jit ? &queryDesc->estate->es_jit->instr : NULL,
 				   NULL,
 				   queryDesc->estate->es_parallel_workers_to_launch,
-				   queryDesc->estate->es_parallel_workers_launched);
+				   queryDesc->estate->es_parallel_workers_launched,
+				   queryDesc->cplan);
 	}
 
 	if (prev_ExecutorEnd)
@@ -1230,7 +1239,8 @@ pgss_ProcessUtility(PlannedStmt *pstmt, const char *queryString,
 				   NULL,
 				   NULL,
 				   0,
-				   0);
+				   0,
+				   NULL);
 	}
 	else
 	{
@@ -1293,7 +1303,8 @@ pgss_store(const char *query, uint64 queryId,
 		   const struct JitInstrumentation *jitusage,
 		   JumbleState *jstate,
 		   int parallel_workers_to_launch,
-		   int parallel_workers_launched)
+		   int parallel_workers_launched,
+		   CachedPlan *cplan)
 {
 	pgssHashKey key;
 	pgssEntry  *entry;
@@ -1501,6 +1512,15 @@ pgss_store(const char *query, uint64 queryId,
 		entry->counters.parallel_workers_to_launch += parallel_workers_to_launch;
 		entry->counters.parallel_workers_launched += parallel_workers_launched;
 
+		if (cplan)
+		{
+			if (cplan->status == PLAN_CACHE_STATUS_GENERIC_PLAN_BUILD ||
+				cplan->status == PLAN_CACHE_STATUS_GENERIC_PLAN_REUSE)
+				entry->counters.generic_plan_calls++;
+			if (cplan && cplan->status == PLAN_CACHE_STATUS_CUSTOM_PLAN)
+				entry->counters.custom_plan_calls++;
+		}
+
 		SpinLockRelease(&entry->mutex);
 	}
 
@@ -1568,7 +1588,8 @@ pg_stat_statements_reset(PG_FUNCTION_ARGS)
 #define PG_STAT_STATEMENTS_COLS_V1_10	43
 #define PG_STAT_STATEMENTS_COLS_V1_11	49
 #define PG_STAT_STATEMENTS_COLS_V1_12	52
-#define PG_STAT_STATEMENTS_COLS			52	/* maximum of above */
+#define PG_STAT_STATEMENTS_COLS_V1_13	54
+#define PG_STAT_STATEMENTS_COLS			54	/* maximum of above */
 
 /*
  * Retrieve statement statistics.
@@ -1580,6 +1601,16 @@ pg_stat_statements_reset(PG_FUNCTION_ARGS)
  * expected API version is identified by embedding it in the C name of the
  * function.  Unfortunately we weren't bright enough to do that for 1.1.
  */
+Datum
+pg_stat_statements_1_13(PG_FUNCTION_ARGS)
+{
+	bool		showtext = PG_GETARG_BOOL(0);
+
+	pg_stat_statements_internal(fcinfo, PGSS_V1_13, showtext);
+
+	return (Datum) 0;
+}
+
 Datum
 pg_stat_statements_1_12(PG_FUNCTION_ARGS)
 {
@@ -1738,6 +1769,10 @@ pg_stat_statements_internal(FunctionCallInfo fcinfo,
 			if (api_version != PGSS_V1_12)
 				elog(ERROR, "incorrect number of output arguments");
 			break;
+		case PG_STAT_STATEMENTS_COLS_V1_13:
+			if (api_version != PGSS_V1_13)
+				elog(ERROR, "incorrect number of output arguments");
+			break;
 		default:
 			elog(ERROR, "incorrect number of output arguments");
 	}
@@ -1995,6 +2030,11 @@ pg_stat_statements_internal(FunctionCallInfo fcinfo,
 			values[i++] = TimestampTzGetDatum(stats_since);
 			values[i++] = TimestampTzGetDatum(minmax_stats_since);
 		}
+		if (api_version >= PGSS_V1_13)
+		{
+			values[i++] = Int64GetDatumFast(tmp.generic_plan_calls);
+			values[i++] = Int64GetDatumFast(tmp.custom_plan_calls);
+		}
 
 		Assert(i == (api_version == PGSS_V1_0 ? PG_STAT_STATEMENTS_COLS_V1_0 :
 					 api_version == PGSS_V1_1 ? PG_STAT_STATEMENTS_COLS_V1_1 :
@@ -2005,6 +2045,7 @@ pg_stat_statements_internal(FunctionCallInfo fcinfo,
 					 api_version == PGSS_V1_10 ? PG_STAT_STATEMENTS_COLS_V1_10 :
 					 api_version == PGSS_V1_11 ? PG_STAT_STATEMENTS_COLS_V1_11 :
 					 api_version == PGSS_V1_12 ? PG_STAT_STATEMENTS_COLS_V1_12 :
+					 api_version == PGSS_V1_13 ? PG_STAT_STATEMENTS_COLS_V1_13 :
 					 -1 /* fail if you forget to update this assert */ ));
 
 		tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, values, nulls);
diff --git a/contrib/pg_stat_statements/pg_stat_statements.control b/contrib/pg_stat_statements/pg_stat_statements.control
index d45ebc12e3..2eee0ceffa 100644
--- a/contrib/pg_stat_statements/pg_stat_statements.control
+++ b/contrib/pg_stat_statements/pg_stat_statements.control
@@ -1,5 +1,5 @@
 # pg_stat_statements extension
 comment = 'track planning and execution statistics of all SQL statements executed'
-default_version = '1.12'
+default_version = '1.13'
 module_pathname = '$libdir/pg_stat_statements'
 relocatable = true
diff --git a/contrib/pg_stat_statements/sql/plan_cache.sql b/contrib/pg_stat_statements/sql/plan_cache.sql
new file mode 100644
index 0000000000..729ca488da
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/plan_cache.sql
@@ -0,0 +1,50 @@
+--
+-- Information related to plan cache
+--
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- plan cache counters for prepared statements
+PREPARE p1 AS SELECT $1;
+-- plan cache auto
+SET plan_cache_mode TO auto;
+EXECUTE p1(1);
+-- force generic plan
+SET plan_cache_mode TO force_generic_plan;
+EXECUTE p1(1);
+-- force custom plan
+SET plan_cache_mode TO force_custom_plan;
+EXECUTE p1(1);
+
+-- plan cache counters for functions and procedures
+SET pg_stat_statements.track = 'all';
+CREATE OR REPLACE FUNCTION select_one_func(int) RETURNS VOID AS $$
+DECLARE
+    ret INT;
+BEGIN
+    SELECT $1 INTO ret;
+END;
+$$ LANGUAGE plpgsql;
+CREATE OR REPLACE PROCEDURE select_one_proc(int) AS $$
+DECLARE
+    ret INT;
+BEGIN
+    select $1 INTO ret;
+END;
+$$ LANGUAGE plpgsql;
+-- plan cache auto
+SET plan_cache_mode TO auto;
+SELECT select_one_func(1);
+CALL select_one_proc(1);
+-- force generic plan
+SET plan_cache_mode TO force_generic_plan;
+SELECT select_one_func(1);
+CALL select_one_proc(1);
+-- force custom plan
+SET plan_cache_mode TO force_custom_plan;
+SELECT select_one_func(1);
+CALL select_one_proc(1);
+
+-- get the plan cache counters
+SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, calls, query FROM pg_stat_statements
+  WHERE query = 'SELECT $1' ORDER BY query COLLATE "C";
diff --git a/doc/src/sgml/pgstatstatements.sgml b/doc/src/sgml/pgstatstatements.sgml
index e2ac1c2d50..e4d043321d 100644
--- a/doc/src/sgml/pgstatstatements.sgml
+++ b/doc/src/sgml/pgstatstatements.sgml
@@ -575,6 +575,24 @@
        <structfield>max_exec_time</structfield>)
       </para></entry>
      </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>generic_plan_calls</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Total number of statements executed using a generic plan
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>custom_plan_calls</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Total number of statements executed using a custom plan
+      </para></entry>
+     </row>
     </tbody>
    </tgroup>
   </table>
diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c
index 6c2979d5c8..510e2d8286 100644
--- a/src/backend/utils/cache/plancache.c
+++ b/src/backend/utils/cache/plancache.c
@@ -1056,7 +1056,7 @@ BuildCachedPlan(CachedPlanSource *plansource, List *qlist,
 	plan->stmt_context = stmt_context;
 	plan->is_oneshot = plansource->is_oneshot;
 	plan->is_saved = false;
-	plan->is_reused = false;
+	plan->status = PLAN_CACHE_STATUS_UNKNOWN;
 	plan->is_valid = true;
 
 	/* assign generation number to new plan */
@@ -1294,7 +1294,8 @@ cached_plan_cost(CachedPlan *plan, bool include_planner)
  * locks are acquired. In such cases, CheckCachedPlan() does not take locks
  * on relations subject to initial runtime pruning; instead, these locks are
  * deferred until execution startup, when ExecDoInitialPruning() performs
- * initial pruning.  The plan's "is_reused" flag is set to indicate that
+ * initial pruning.  The plan's "status" flag is set to
+ * PLAN_CACHE_STATUS_GENERIC_PLAN_REUSE to indicate that
  * CachedPlanRequiresLocking() should return true when called by
  * ExecDoInitialPruning().
  *
@@ -1335,7 +1336,7 @@ GetCachedPlan(CachedPlanSource *plansource, ParamListInfo boundParams,
 			plan = plansource->gplan;
 			Assert(plan->magic == CACHEDPLAN_MAGIC);
 			/* Reusing the existing plan, so not all locks may be acquired. */
-			plan->is_reused = true;
+			plan->status = PLAN_CACHE_STATUS_GENERIC_PLAN_REUSE;
 		}
 		else
 		{
@@ -1379,6 +1380,8 @@ GetCachedPlan(CachedPlanSource *plansource, ParamListInfo boundParams,
 			 * BuildCachedPlan to do that by passing NIL.
 			 */
 			qlist = NIL;
+
+			plan->status = PLAN_CACHE_STATUS_GENERIC_PLAN_BUILD;
 		}
 	}
 
@@ -1390,6 +1393,7 @@ GetCachedPlan(CachedPlanSource *plansource, ParamListInfo boundParams,
 		plansource->total_custom_cost += cached_plan_cost(plan, true);
 
 		plansource->num_custom_plans++;
+		plan->status = PLAN_CACHE_STATUS_CUSTOM_PLAN;
 	}
 	else
 	{
diff --git a/src/include/utils/plancache.h b/src/include/utils/plancache.h
index f1fc770733..1890c6362c 100644
--- a/src/include/utils/plancache.h
+++ b/src/include/utils/plancache.h
@@ -36,6 +36,14 @@ typedef enum
 	PLAN_CACHE_MODE_FORCE_CUSTOM_PLAN,
 }			PlanCacheMode;
 
+typedef enum
+{
+	PLAN_CACHE_STATUS_UNKNOWN = 0,
+	PLAN_CACHE_STATUS_CUSTOM_PLAN,
+	PLAN_CACHE_STATUS_GENERIC_PLAN_BUILD,
+	PLAN_CACHE_STATUS_GENERIC_PLAN_REUSE
+}			PlanCacheStatus;
+
 /* GUC parameter */
 extern PGDLLIMPORT int plan_cache_mode;
 
@@ -153,7 +161,7 @@ typedef struct CachedPlan
 	List	   *stmt_list;		/* list of PlannedStmts */
 	bool		is_oneshot;		/* is it a "oneshot" plan? */
 	bool		is_saved;		/* is CachedPlan in a long-lived context? */
-	bool		is_reused;		/* is it a reused generic plan? */
+	PlanCacheStatus status;		/* status of the cached plan */
 	bool		is_valid;		/* is the stmt_list currently valid? */
 	Oid			planRoleId;		/* Role ID the plan was created for */
 	bool		dependsOnRole;	/* is plan specific to that role? */
@@ -257,7 +265,7 @@ extern void FreeCachedExpression(CachedExpression *cexpr);
 static inline bool
 CachedPlanRequiresLocking(CachedPlan *cplan)
 {
-	return !cplan->is_oneshot && cplan->is_reused;
+	return !cplan->is_oneshot && (cplan->status == PLAN_CACHE_STATUS_GENERIC_PLAN_REUSE);
 }
 
 /*
-- 
2.47.1

#4Sami Imseih
samimseih@gmail.com
In reply to: Sami Imseih (#3)
1 attachment(s)
Re: track generic and custom plans in pg_stat_statements

Please see v2

oops, had to fix a typo in meson.build. Please see v3.

--
Sami

Attachments:

v3-0001-add-plan_cache-counters-to-pg_stat_statements.patchapplication/octet-stream; name=v3-0001-add-plan_cache-counters-to-pg_stat_statements.patchDownload
From 96aeadac9fa55409c681abb05d8c9c76c1c7d9b1 Mon Sep 17 00:00:00 2001
From: Sami Imseih <simseih@amazon.com>
Date: Tue, 4 Mar 2025 08:54:56 -0600
Subject: [PATCH v3 1/1] add plan_cache counters to pg_stat_statements.

---
 contrib/pg_stat_statements/Makefile           |  3 +-
 .../expected/plan_cache.out                   | 87 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  2 +
 .../pg_stat_statements--1.12--1.13.sql        | 78 +++++++++++++++++
 .../pg_stat_statements/pg_stat_statements.c   | 55 ++++++++++--
 .../pg_stat_statements.control                |  2 +-
 contrib/pg_stat_statements/sql/plan_cache.sql | 50 +++++++++++
 doc/src/sgml/pgstatstatements.sgml            | 18 ++++
 src/backend/utils/cache/plancache.c           | 10 ++-
 src/include/utils/plancache.h                 | 12 ++-
 10 files changed, 303 insertions(+), 14 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/plan_cache.out
 create mode 100644 contrib/pg_stat_statements/pg_stat_statements--1.12--1.13.sql
 create mode 100644 contrib/pg_stat_statements/sql/plan_cache.sql

diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index 241c02587b..bb81256548 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -7,6 +7,7 @@ OBJS = \
 
 EXTENSION = pg_stat_statements
 DATA = pg_stat_statements--1.4.sql \
+	pg_stat_statements--1.12--1.13.sql \
 	pg_stat_statements--1.11--1.12.sql pg_stat_statements--1.10--1.11.sql \
 	pg_stat_statements--1.9--1.10.sql pg_stat_statements--1.8--1.9.sql \
 	pg_stat_statements--1.7--1.8.sql pg_stat_statements--1.6--1.7.sql \
@@ -20,7 +21,7 @@ LDFLAGS_SL += $(filter -lm, $(LIBS))
 REGRESS_OPTS = --temp-config $(top_srcdir)/contrib/pg_stat_statements/pg_stat_statements.conf
 REGRESS = select dml cursors utility level_tracking planning \
 	user_activity wal entry_timestamp privileges extended \
-	parallel cleanup oldextversions
+	parallel plan_cache cleanup oldextversions
 # Disabled because these tests require "shared_preload_libraries=pg_stat_statements",
 # which typical installcheck users do not have (e.g. buildfarm clients).
 NO_INSTALLCHECK = 1
diff --git a/contrib/pg_stat_statements/expected/plan_cache.out b/contrib/pg_stat_statements/expected/plan_cache.out
new file mode 100644
index 0000000000..bf11794528
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/plan_cache.out
@@ -0,0 +1,87 @@
+--
+-- Information related to plan cache
+--
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- plan cache counters for prepared statements
+PREPARE p1 AS SELECT $1;
+-- plan cache auto
+SET plan_cache_mode TO auto;
+EXECUTE p1(1);
+ ?column? 
+----------
+ 1
+(1 row)
+
+-- force generic plan
+SET plan_cache_mode TO force_generic_plan;
+EXECUTE p1(1);
+ ?column? 
+----------
+ 1
+(1 row)
+
+-- force custom plan
+SET plan_cache_mode TO force_custom_plan;
+EXECUTE p1(1);
+ ?column? 
+----------
+ 1
+(1 row)
+
+-- plan cache counters for functions and procedures
+SET pg_stat_statements.track = 'all';
+CREATE OR REPLACE FUNCTION select_one_func(int) RETURNS VOID AS $$
+DECLARE
+    ret INT;
+BEGIN
+    SELECT $1 INTO ret;
+END;
+$$ LANGUAGE plpgsql;
+CREATE OR REPLACE PROCEDURE select_one_proc(int) AS $$
+DECLARE
+    ret INT;
+BEGIN
+    select $1 INTO ret;
+END;
+$$ LANGUAGE plpgsql;
+-- plan cache auto
+SET plan_cache_mode TO auto;
+SELECT select_one_func(1);
+ select_one_func 
+-----------------
+ 
+(1 row)
+
+CALL select_one_proc(1);
+-- force generic plan
+SET plan_cache_mode TO force_generic_plan;
+SELECT select_one_func(1);
+ select_one_func 
+-----------------
+ 
+(1 row)
+
+CALL select_one_proc(1);
+-- force custom plan
+SET plan_cache_mode TO force_custom_plan;
+SELECT select_one_func(1);
+ select_one_func 
+-----------------
+ 
+(1 row)
+
+CALL select_one_proc(1);
+-- get the plan cache counters
+SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, calls, query FROM pg_stat_statements
+  WHERE query = 'SELECT $1' ORDER BY query COLLATE "C";
+ calls | generic_plan_calls | custom_plan_calls | toplevel | calls |   query   
+-------+--------------------+-------------------+----------+-------+-----------
+     6 |                  2 |                 4 | f        |     6 | SELECT $1
+     3 |                  1 |                 2 | t        |     3 | SELECT $1
+(2 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 4446af58c5..1c1650aed7 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -21,6 +21,7 @@ contrib_targets += pg_stat_statements
 install_data(
   'pg_stat_statements.control',
   'pg_stat_statements--1.4.sql',
+  'pg_stat_statements--1.12--1.13.sql',
   'pg_stat_statements--1.11--1.12.sql',
   'pg_stat_statements--1.10--1.11.sql',
   'pg_stat_statements--1.9--1.10.sql',
@@ -54,6 +55,7 @@ tests += {
       'privileges',
       'extended',
       'parallel',
+      'plan_cache',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/pg_stat_statements--1.12--1.13.sql b/contrib/pg_stat_statements/pg_stat_statements--1.12--1.13.sql
new file mode 100644
index 0000000000..67f2f7dbca
--- /dev/null
+++ b/contrib/pg_stat_statements/pg_stat_statements--1.12--1.13.sql
@@ -0,0 +1,78 @@
+/* contrib/pg_stat_statements/pg_stat_statements--1.12--1.13.sql */
+
+-- complain if script is sourced in psql, rather than via ALTER EXTENSION
+\echo Use "ALTER EXTENSION pg_stat_statements UPDATE TO '1.13'" to load this file. \quit
+
+/* First we have to remove them from the extension */
+ALTER EXTENSION pg_stat_statements DROP VIEW pg_stat_statements;
+ALTER EXTENSION pg_stat_statements DROP FUNCTION pg_stat_statements(boolean);
+
+/* Then we can drop them */
+DROP VIEW pg_stat_statements;
+DROP FUNCTION pg_stat_statements(boolean);
+
+/* Now redefine */
+CREATE FUNCTION pg_stat_statements(IN showtext boolean,
+    OUT userid oid,
+    OUT dbid oid,
+    OUT toplevel bool,
+    OUT queryid bigint,
+    OUT query text,
+    OUT plans int8,
+    OUT total_plan_time float8,
+    OUT min_plan_time float8,
+    OUT max_plan_time float8,
+    OUT mean_plan_time float8,
+    OUT stddev_plan_time float8,
+    OUT calls int8,
+    OUT total_exec_time float8,
+    OUT min_exec_time float8,
+    OUT max_exec_time float8,
+    OUT mean_exec_time float8,
+    OUT stddev_exec_time float8,
+    OUT rows int8,
+    OUT shared_blks_hit int8,
+    OUT shared_blks_read int8,
+    OUT shared_blks_dirtied int8,
+    OUT shared_blks_written int8,
+    OUT local_blks_hit int8,
+    OUT local_blks_read int8,
+    OUT local_blks_dirtied int8,
+    OUT local_blks_written int8,
+    OUT temp_blks_read int8,
+    OUT temp_blks_written int8,
+    OUT shared_blk_read_time float8,
+    OUT shared_blk_write_time float8,
+    OUT local_blk_read_time float8,
+    OUT local_blk_write_time float8,
+    OUT temp_blk_read_time float8,
+    OUT temp_blk_write_time float8,
+    OUT wal_records int8,
+    OUT wal_fpi int8,
+    OUT wal_bytes numeric,
+    OUT wal_buffers_full int8,
+    OUT jit_functions int8,
+    OUT jit_generation_time float8,
+    OUT jit_inlining_count int8,
+    OUT jit_inlining_time float8,
+    OUT jit_optimization_count int8,
+    OUT jit_optimization_time float8,
+    OUT jit_emission_count int8,
+    OUT jit_emission_time float8,
+    OUT jit_deform_count int8,
+    OUT jit_deform_time float8,
+    OUT parallel_workers_to_launch int8,
+    OUT parallel_workers_launched int8,
+    OUT stats_since timestamp with time zone,
+    OUT minmax_stats_since timestamp with time zone,
+    OUT generic_plan_calls int8,
+    OUT custom_plan_calls int8
+)
+RETURNS SETOF record
+AS 'MODULE_PATHNAME', 'pg_stat_statements_1_13'
+LANGUAGE C STRICT VOLATILE PARALLEL SAFE;
+
+CREATE VIEW pg_stat_statements AS
+  SELECT * FROM pg_stat_statements(true);
+
+GRANT SELECT ON pg_stat_statements TO PUBLIC;
diff --git a/contrib/pg_stat_statements/pg_stat_statements.c b/contrib/pg_stat_statements/pg_stat_statements.c
index b245d04097..aab4ac15f7 100644
--- a/contrib/pg_stat_statements/pg_stat_statements.c
+++ b/contrib/pg_stat_statements/pg_stat_statements.c
@@ -69,6 +69,7 @@
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/memutils.h"
+#include "utils/plancache.h"
 #include "utils/timestamp.h"
 
 PG_MODULE_MAGIC;
@@ -111,6 +112,7 @@ typedef enum pgssVersion
 	PGSS_V1_10,
 	PGSS_V1_11,
 	PGSS_V1_12,
+	PGSS_V1_13,
 } pgssVersion;
 
 typedef enum pgssStoreKind
@@ -207,6 +209,8 @@ typedef struct Counters
 											 * to be launched */
 	int64		parallel_workers_launched;	/* # of parallel workers actually
 											 * launched */
+	int64		generic_plan_calls;		/* number of calls using a generic plan */
+	int64		custom_plan_calls;		/* number of calls using a custom plan */
 } Counters;
 
 /*
@@ -321,6 +325,7 @@ PG_FUNCTION_INFO_V1(pg_stat_statements_1_9);
 PG_FUNCTION_INFO_V1(pg_stat_statements_1_10);
 PG_FUNCTION_INFO_V1(pg_stat_statements_1_11);
 PG_FUNCTION_INFO_V1(pg_stat_statements_1_12);
+PG_FUNCTION_INFO_V1(pg_stat_statements_1_13);
 PG_FUNCTION_INFO_V1(pg_stat_statements);
 PG_FUNCTION_INFO_V1(pg_stat_statements_info);
 
@@ -353,7 +358,8 @@ static void pgss_store(const char *query, uint64 queryId,
 					   const struct JitInstrumentation *jitusage,
 					   JumbleState *jstate,
 					   int parallel_workers_to_launch,
-					   int parallel_workers_launched);
+					   int parallel_workers_launched,
+					   CachedPlan *cplan);
 static void pg_stat_statements_internal(FunctionCallInfo fcinfo,
 										pgssVersion api_version,
 										bool showtext);
@@ -875,7 +881,8 @@ pgss_post_parse_analyze(ParseState *pstate, Query *query, JumbleState *jstate)
 				   NULL,
 				   jstate,
 				   0,
-				   0);
+				   0,
+				   NULL);
 }
 
 /*
@@ -955,7 +962,8 @@ pgss_planner(Query *parse,
 				   NULL,
 				   NULL,
 				   0,
-				   0);
+				   0,
+				   NULL);
 	}
 	else
 	{
@@ -1097,7 +1105,8 @@ pgss_ExecutorEnd(QueryDesc *queryDesc)
 				   queryDesc->estate->es_jit ? &queryDesc->estate->es_jit->instr : NULL,
 				   NULL,
 				   queryDesc->estate->es_parallel_workers_to_launch,
-				   queryDesc->estate->es_parallel_workers_launched);
+				   queryDesc->estate->es_parallel_workers_launched,
+				   queryDesc->cplan);
 	}
 
 	if (prev_ExecutorEnd)
@@ -1230,7 +1239,8 @@ pgss_ProcessUtility(PlannedStmt *pstmt, const char *queryString,
 				   NULL,
 				   NULL,
 				   0,
-				   0);
+				   0,
+				   NULL);
 	}
 	else
 	{
@@ -1293,7 +1303,8 @@ pgss_store(const char *query, uint64 queryId,
 		   const struct JitInstrumentation *jitusage,
 		   JumbleState *jstate,
 		   int parallel_workers_to_launch,
-		   int parallel_workers_launched)
+		   int parallel_workers_launched,
+		   CachedPlan *cplan)
 {
 	pgssHashKey key;
 	pgssEntry  *entry;
@@ -1501,6 +1512,15 @@ pgss_store(const char *query, uint64 queryId,
 		entry->counters.parallel_workers_to_launch += parallel_workers_to_launch;
 		entry->counters.parallel_workers_launched += parallel_workers_launched;
 
+		if (cplan)
+		{
+			if (cplan->status == PLAN_CACHE_STATUS_GENERIC_PLAN_BUILD ||
+				cplan->status == PLAN_CACHE_STATUS_GENERIC_PLAN_REUSE)
+				entry->counters.generic_plan_calls++;
+			if (cplan && cplan->status == PLAN_CACHE_STATUS_CUSTOM_PLAN)
+				entry->counters.custom_plan_calls++;
+		}
+
 		SpinLockRelease(&entry->mutex);
 	}
 
@@ -1568,7 +1588,8 @@ pg_stat_statements_reset(PG_FUNCTION_ARGS)
 #define PG_STAT_STATEMENTS_COLS_V1_10	43
 #define PG_STAT_STATEMENTS_COLS_V1_11	49
 #define PG_STAT_STATEMENTS_COLS_V1_12	52
-#define PG_STAT_STATEMENTS_COLS			52	/* maximum of above */
+#define PG_STAT_STATEMENTS_COLS_V1_13	54
+#define PG_STAT_STATEMENTS_COLS			54	/* maximum of above */
 
 /*
  * Retrieve statement statistics.
@@ -1580,6 +1601,16 @@ pg_stat_statements_reset(PG_FUNCTION_ARGS)
  * expected API version is identified by embedding it in the C name of the
  * function.  Unfortunately we weren't bright enough to do that for 1.1.
  */
+Datum
+pg_stat_statements_1_13(PG_FUNCTION_ARGS)
+{
+	bool		showtext = PG_GETARG_BOOL(0);
+
+	pg_stat_statements_internal(fcinfo, PGSS_V1_13, showtext);
+
+	return (Datum) 0;
+}
+
 Datum
 pg_stat_statements_1_12(PG_FUNCTION_ARGS)
 {
@@ -1738,6 +1769,10 @@ pg_stat_statements_internal(FunctionCallInfo fcinfo,
 			if (api_version != PGSS_V1_12)
 				elog(ERROR, "incorrect number of output arguments");
 			break;
+		case PG_STAT_STATEMENTS_COLS_V1_13:
+			if (api_version != PGSS_V1_13)
+				elog(ERROR, "incorrect number of output arguments");
+			break;
 		default:
 			elog(ERROR, "incorrect number of output arguments");
 	}
@@ -1995,6 +2030,11 @@ pg_stat_statements_internal(FunctionCallInfo fcinfo,
 			values[i++] = TimestampTzGetDatum(stats_since);
 			values[i++] = TimestampTzGetDatum(minmax_stats_since);
 		}
+		if (api_version >= PGSS_V1_13)
+		{
+			values[i++] = Int64GetDatumFast(tmp.generic_plan_calls);
+			values[i++] = Int64GetDatumFast(tmp.custom_plan_calls);
+		}
 
 		Assert(i == (api_version == PGSS_V1_0 ? PG_STAT_STATEMENTS_COLS_V1_0 :
 					 api_version == PGSS_V1_1 ? PG_STAT_STATEMENTS_COLS_V1_1 :
@@ -2005,6 +2045,7 @@ pg_stat_statements_internal(FunctionCallInfo fcinfo,
 					 api_version == PGSS_V1_10 ? PG_STAT_STATEMENTS_COLS_V1_10 :
 					 api_version == PGSS_V1_11 ? PG_STAT_STATEMENTS_COLS_V1_11 :
 					 api_version == PGSS_V1_12 ? PG_STAT_STATEMENTS_COLS_V1_12 :
+					 api_version == PGSS_V1_13 ? PG_STAT_STATEMENTS_COLS_V1_13 :
 					 -1 /* fail if you forget to update this assert */ ));
 
 		tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, values, nulls);
diff --git a/contrib/pg_stat_statements/pg_stat_statements.control b/contrib/pg_stat_statements/pg_stat_statements.control
index d45ebc12e3..2eee0ceffa 100644
--- a/contrib/pg_stat_statements/pg_stat_statements.control
+++ b/contrib/pg_stat_statements/pg_stat_statements.control
@@ -1,5 +1,5 @@
 # pg_stat_statements extension
 comment = 'track planning and execution statistics of all SQL statements executed'
-default_version = '1.12'
+default_version = '1.13'
 module_pathname = '$libdir/pg_stat_statements'
 relocatable = true
diff --git a/contrib/pg_stat_statements/sql/plan_cache.sql b/contrib/pg_stat_statements/sql/plan_cache.sql
new file mode 100644
index 0000000000..729ca488da
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/plan_cache.sql
@@ -0,0 +1,50 @@
+--
+-- Information related to plan cache
+--
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- plan cache counters for prepared statements
+PREPARE p1 AS SELECT $1;
+-- plan cache auto
+SET plan_cache_mode TO auto;
+EXECUTE p1(1);
+-- force generic plan
+SET plan_cache_mode TO force_generic_plan;
+EXECUTE p1(1);
+-- force custom plan
+SET plan_cache_mode TO force_custom_plan;
+EXECUTE p1(1);
+
+-- plan cache counters for functions and procedures
+SET pg_stat_statements.track = 'all';
+CREATE OR REPLACE FUNCTION select_one_func(int) RETURNS VOID AS $$
+DECLARE
+    ret INT;
+BEGIN
+    SELECT $1 INTO ret;
+END;
+$$ LANGUAGE plpgsql;
+CREATE OR REPLACE PROCEDURE select_one_proc(int) AS $$
+DECLARE
+    ret INT;
+BEGIN
+    select $1 INTO ret;
+END;
+$$ LANGUAGE plpgsql;
+-- plan cache auto
+SET plan_cache_mode TO auto;
+SELECT select_one_func(1);
+CALL select_one_proc(1);
+-- force generic plan
+SET plan_cache_mode TO force_generic_plan;
+SELECT select_one_func(1);
+CALL select_one_proc(1);
+-- force custom plan
+SET plan_cache_mode TO force_custom_plan;
+SELECT select_one_func(1);
+CALL select_one_proc(1);
+
+-- get the plan cache counters
+SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, calls, query FROM pg_stat_statements
+  WHERE query = 'SELECT $1' ORDER BY query COLLATE "C";
diff --git a/doc/src/sgml/pgstatstatements.sgml b/doc/src/sgml/pgstatstatements.sgml
index e2ac1c2d50..e4d043321d 100644
--- a/doc/src/sgml/pgstatstatements.sgml
+++ b/doc/src/sgml/pgstatstatements.sgml
@@ -575,6 +575,24 @@
        <structfield>max_exec_time</structfield>)
       </para></entry>
      </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>generic_plan_calls</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Total number of statements executed using a generic plan
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>custom_plan_calls</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Total number of statements executed using a custom plan
+      </para></entry>
+     </row>
     </tbody>
    </tgroup>
   </table>
diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c
index 6c2979d5c8..510e2d8286 100644
--- a/src/backend/utils/cache/plancache.c
+++ b/src/backend/utils/cache/plancache.c
@@ -1056,7 +1056,7 @@ BuildCachedPlan(CachedPlanSource *plansource, List *qlist,
 	plan->stmt_context = stmt_context;
 	plan->is_oneshot = plansource->is_oneshot;
 	plan->is_saved = false;
-	plan->is_reused = false;
+	plan->status = PLAN_CACHE_STATUS_UNKNOWN;
 	plan->is_valid = true;
 
 	/* assign generation number to new plan */
@@ -1294,7 +1294,8 @@ cached_plan_cost(CachedPlan *plan, bool include_planner)
  * locks are acquired. In such cases, CheckCachedPlan() does not take locks
  * on relations subject to initial runtime pruning; instead, these locks are
  * deferred until execution startup, when ExecDoInitialPruning() performs
- * initial pruning.  The plan's "is_reused" flag is set to indicate that
+ * initial pruning.  The plan's "status" flag is set to
+ * PLAN_CACHE_STATUS_GENERIC_PLAN_REUSE to indicate that
  * CachedPlanRequiresLocking() should return true when called by
  * ExecDoInitialPruning().
  *
@@ -1335,7 +1336,7 @@ GetCachedPlan(CachedPlanSource *plansource, ParamListInfo boundParams,
 			plan = plansource->gplan;
 			Assert(plan->magic == CACHEDPLAN_MAGIC);
 			/* Reusing the existing plan, so not all locks may be acquired. */
-			plan->is_reused = true;
+			plan->status = PLAN_CACHE_STATUS_GENERIC_PLAN_REUSE;
 		}
 		else
 		{
@@ -1379,6 +1380,8 @@ GetCachedPlan(CachedPlanSource *plansource, ParamListInfo boundParams,
 			 * BuildCachedPlan to do that by passing NIL.
 			 */
 			qlist = NIL;
+
+			plan->status = PLAN_CACHE_STATUS_GENERIC_PLAN_BUILD;
 		}
 	}
 
@@ -1390,6 +1393,7 @@ GetCachedPlan(CachedPlanSource *plansource, ParamListInfo boundParams,
 		plansource->total_custom_cost += cached_plan_cost(plan, true);
 
 		plansource->num_custom_plans++;
+		plan->status = PLAN_CACHE_STATUS_CUSTOM_PLAN;
 	}
 	else
 	{
diff --git a/src/include/utils/plancache.h b/src/include/utils/plancache.h
index f1fc770733..1890c6362c 100644
--- a/src/include/utils/plancache.h
+++ b/src/include/utils/plancache.h
@@ -36,6 +36,14 @@ typedef enum
 	PLAN_CACHE_MODE_FORCE_CUSTOM_PLAN,
 }			PlanCacheMode;
 
+typedef enum
+{
+	PLAN_CACHE_STATUS_UNKNOWN = 0,
+	PLAN_CACHE_STATUS_CUSTOM_PLAN,
+	PLAN_CACHE_STATUS_GENERIC_PLAN_BUILD,
+	PLAN_CACHE_STATUS_GENERIC_PLAN_REUSE
+}			PlanCacheStatus;
+
 /* GUC parameter */
 extern PGDLLIMPORT int plan_cache_mode;
 
@@ -153,7 +161,7 @@ typedef struct CachedPlan
 	List	   *stmt_list;		/* list of PlannedStmts */
 	bool		is_oneshot;		/* is it a "oneshot" plan? */
 	bool		is_saved;		/* is CachedPlan in a long-lived context? */
-	bool		is_reused;		/* is it a reused generic plan? */
+	PlanCacheStatus status;		/* status of the cached plan */
 	bool		is_valid;		/* is the stmt_list currently valid? */
 	Oid			planRoleId;		/* Role ID the plan was created for */
 	bool		dependsOnRole;	/* is plan specific to that role? */
@@ -257,7 +265,7 @@ extern void FreeCachedExpression(CachedExpression *cexpr);
 static inline bool
 CachedPlanRequiresLocking(CachedPlan *cplan)
 {
-	return !cplan->is_oneshot && cplan->is_reused;
+	return !cplan->is_oneshot && (cplan->status == PLAN_CACHE_STATUS_GENERIC_PLAN_REUSE);
 }
 
 /*
-- 
2.39.5 (Apple Git-154)

#5Ilia Evdokimov
ilya.evdokimov@tantorlabs.com
In reply to: Sami Imseih (#4)
Re: track generic and custom plans in pg_stat_statements

Hi,

Thank you for your patch. It is really useful for tracking the history
of generic and custom plan usage.

At first glance, I have the following suggestions for improvement:

1. Is there any reason for the double check of cplan != NULL? It seems
unnecessary, and we could simplify it to:

-if (cplan && cplan->status == PLAN_CACHE_STATUS_CUSTOM_PLAN)
+if (cplan->status == PLAN_CACHE_STATUS_CUSTOM_PLAN)

2. Should we add Assert(kind == PGSS_EXEC) at this place  to ensure that
generic_plan_calls and custom_plan_calls are only incremented when
appropriate?

--
Best regards,
Ilia Evdokimov,
Tantor Labs LLC.

#6Sami Imseih
samimseih@gmail.com
In reply to: Ilia Evdokimov (#5)
1 attachment(s)
Re: track generic and custom plans in pg_stat_statements

Thank you for your patch. It is really useful for tracking the history
of generic and custom plan usage.

Thanks for the review!

1. Is there any reason for the double check of cplan != NULL? It seems
unnecessary, and we could simplify it to:

-if (cplan && cplan->status == PLAN_CACHE_STATUS_CUSTOM_PLAN)
+if (cplan->status == PLAN_CACHE_STATUS_CUSTOM_PLAN)

No, it's not necessary and an oversight. removed.

2. Should we add Assert(kind == PGSS_EXEC) at this place to ensure that
generic_plan_calls and custom_plan_calls are only incremented when
appropriate?

I don't think an assert is needed here. There is an assert at the start of
the block for PGSS_EXEC and PGSS_PLAN, but cplan is only available
in the executor.

v4 attached

--
Sami

Attachments:

v4-0001-add-plan_cache-counters-to-pg_stat_statements.patchapplication/octet-stream; name=v4-0001-add-plan_cache-counters-to-pg_stat_statements.patchDownload
From ff62a92cffd4d78cbd12f70fa31c62e34402b2cc Mon Sep 17 00:00:00 2001
From: Sami Imseih <simseih@amazon.com>
Date: Tue, 4 Mar 2025 08:54:56 -0600
Subject: [PATCH v4 1/1] add plan_cache counters to pg_stat_statements.

Reviewers: Greg Sabino Mullane,	Ilia Evdokimov
---
 contrib/pg_stat_statements/Makefile           |  3 +-
 .../expected/plan_cache.out                   | 87 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  2 +
 .../pg_stat_statements--1.12--1.13.sql        | 78 +++++++++++++++++
 .../pg_stat_statements/pg_stat_statements.c   | 55 ++++++++++--
 .../pg_stat_statements.control                |  2 +-
 contrib/pg_stat_statements/sql/plan_cache.sql | 50 +++++++++++
 doc/src/sgml/pgstatstatements.sgml            | 18 ++++
 src/backend/utils/cache/plancache.c           | 10 ++-
 src/include/utils/plancache.h                 | 12 ++-
 10 files changed, 303 insertions(+), 14 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/plan_cache.out
 create mode 100644 contrib/pg_stat_statements/pg_stat_statements--1.12--1.13.sql
 create mode 100644 contrib/pg_stat_statements/sql/plan_cache.sql

diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index 241c02587b..bb81256548 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -7,6 +7,7 @@ OBJS = \
 
 EXTENSION = pg_stat_statements
 DATA = pg_stat_statements--1.4.sql \
+	pg_stat_statements--1.12--1.13.sql \
 	pg_stat_statements--1.11--1.12.sql pg_stat_statements--1.10--1.11.sql \
 	pg_stat_statements--1.9--1.10.sql pg_stat_statements--1.8--1.9.sql \
 	pg_stat_statements--1.7--1.8.sql pg_stat_statements--1.6--1.7.sql \
@@ -20,7 +21,7 @@ LDFLAGS_SL += $(filter -lm, $(LIBS))
 REGRESS_OPTS = --temp-config $(top_srcdir)/contrib/pg_stat_statements/pg_stat_statements.conf
 REGRESS = select dml cursors utility level_tracking planning \
 	user_activity wal entry_timestamp privileges extended \
-	parallel cleanup oldextversions
+	parallel plan_cache cleanup oldextversions
 # Disabled because these tests require "shared_preload_libraries=pg_stat_statements",
 # which typical installcheck users do not have (e.g. buildfarm clients).
 NO_INSTALLCHECK = 1
diff --git a/contrib/pg_stat_statements/expected/plan_cache.out b/contrib/pg_stat_statements/expected/plan_cache.out
new file mode 100644
index 0000000000..bf11794528
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/plan_cache.out
@@ -0,0 +1,87 @@
+--
+-- Information related to plan cache
+--
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- plan cache counters for prepared statements
+PREPARE p1 AS SELECT $1;
+-- plan cache auto
+SET plan_cache_mode TO auto;
+EXECUTE p1(1);
+ ?column? 
+----------
+ 1
+(1 row)
+
+-- force generic plan
+SET plan_cache_mode TO force_generic_plan;
+EXECUTE p1(1);
+ ?column? 
+----------
+ 1
+(1 row)
+
+-- force custom plan
+SET plan_cache_mode TO force_custom_plan;
+EXECUTE p1(1);
+ ?column? 
+----------
+ 1
+(1 row)
+
+-- plan cache counters for functions and procedures
+SET pg_stat_statements.track = 'all';
+CREATE OR REPLACE FUNCTION select_one_func(int) RETURNS VOID AS $$
+DECLARE
+    ret INT;
+BEGIN
+    SELECT $1 INTO ret;
+END;
+$$ LANGUAGE plpgsql;
+CREATE OR REPLACE PROCEDURE select_one_proc(int) AS $$
+DECLARE
+    ret INT;
+BEGIN
+    select $1 INTO ret;
+END;
+$$ LANGUAGE plpgsql;
+-- plan cache auto
+SET plan_cache_mode TO auto;
+SELECT select_one_func(1);
+ select_one_func 
+-----------------
+ 
+(1 row)
+
+CALL select_one_proc(1);
+-- force generic plan
+SET plan_cache_mode TO force_generic_plan;
+SELECT select_one_func(1);
+ select_one_func 
+-----------------
+ 
+(1 row)
+
+CALL select_one_proc(1);
+-- force custom plan
+SET plan_cache_mode TO force_custom_plan;
+SELECT select_one_func(1);
+ select_one_func 
+-----------------
+ 
+(1 row)
+
+CALL select_one_proc(1);
+-- get the plan cache counters
+SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, calls, query FROM pg_stat_statements
+  WHERE query = 'SELECT $1' ORDER BY query COLLATE "C";
+ calls | generic_plan_calls | custom_plan_calls | toplevel | calls |   query   
+-------+--------------------+-------------------+----------+-------+-----------
+     6 |                  2 |                 4 | f        |     6 | SELECT $1
+     3 |                  1 |                 2 | t        |     3 | SELECT $1
+(2 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 4446af58c5..1c1650aed7 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -21,6 +21,7 @@ contrib_targets += pg_stat_statements
 install_data(
   'pg_stat_statements.control',
   'pg_stat_statements--1.4.sql',
+  'pg_stat_statements--1.12--1.13.sql',
   'pg_stat_statements--1.11--1.12.sql',
   'pg_stat_statements--1.10--1.11.sql',
   'pg_stat_statements--1.9--1.10.sql',
@@ -54,6 +55,7 @@ tests += {
       'privileges',
       'extended',
       'parallel',
+      'plan_cache',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/pg_stat_statements--1.12--1.13.sql b/contrib/pg_stat_statements/pg_stat_statements--1.12--1.13.sql
new file mode 100644
index 0000000000..67f2f7dbca
--- /dev/null
+++ b/contrib/pg_stat_statements/pg_stat_statements--1.12--1.13.sql
@@ -0,0 +1,78 @@
+/* contrib/pg_stat_statements/pg_stat_statements--1.12--1.13.sql */
+
+-- complain if script is sourced in psql, rather than via ALTER EXTENSION
+\echo Use "ALTER EXTENSION pg_stat_statements UPDATE TO '1.13'" to load this file. \quit
+
+/* First we have to remove them from the extension */
+ALTER EXTENSION pg_stat_statements DROP VIEW pg_stat_statements;
+ALTER EXTENSION pg_stat_statements DROP FUNCTION pg_stat_statements(boolean);
+
+/* Then we can drop them */
+DROP VIEW pg_stat_statements;
+DROP FUNCTION pg_stat_statements(boolean);
+
+/* Now redefine */
+CREATE FUNCTION pg_stat_statements(IN showtext boolean,
+    OUT userid oid,
+    OUT dbid oid,
+    OUT toplevel bool,
+    OUT queryid bigint,
+    OUT query text,
+    OUT plans int8,
+    OUT total_plan_time float8,
+    OUT min_plan_time float8,
+    OUT max_plan_time float8,
+    OUT mean_plan_time float8,
+    OUT stddev_plan_time float8,
+    OUT calls int8,
+    OUT total_exec_time float8,
+    OUT min_exec_time float8,
+    OUT max_exec_time float8,
+    OUT mean_exec_time float8,
+    OUT stddev_exec_time float8,
+    OUT rows int8,
+    OUT shared_blks_hit int8,
+    OUT shared_blks_read int8,
+    OUT shared_blks_dirtied int8,
+    OUT shared_blks_written int8,
+    OUT local_blks_hit int8,
+    OUT local_blks_read int8,
+    OUT local_blks_dirtied int8,
+    OUT local_blks_written int8,
+    OUT temp_blks_read int8,
+    OUT temp_blks_written int8,
+    OUT shared_blk_read_time float8,
+    OUT shared_blk_write_time float8,
+    OUT local_blk_read_time float8,
+    OUT local_blk_write_time float8,
+    OUT temp_blk_read_time float8,
+    OUT temp_blk_write_time float8,
+    OUT wal_records int8,
+    OUT wal_fpi int8,
+    OUT wal_bytes numeric,
+    OUT wal_buffers_full int8,
+    OUT jit_functions int8,
+    OUT jit_generation_time float8,
+    OUT jit_inlining_count int8,
+    OUT jit_inlining_time float8,
+    OUT jit_optimization_count int8,
+    OUT jit_optimization_time float8,
+    OUT jit_emission_count int8,
+    OUT jit_emission_time float8,
+    OUT jit_deform_count int8,
+    OUT jit_deform_time float8,
+    OUT parallel_workers_to_launch int8,
+    OUT parallel_workers_launched int8,
+    OUT stats_since timestamp with time zone,
+    OUT minmax_stats_since timestamp with time zone,
+    OUT generic_plan_calls int8,
+    OUT custom_plan_calls int8
+)
+RETURNS SETOF record
+AS 'MODULE_PATHNAME', 'pg_stat_statements_1_13'
+LANGUAGE C STRICT VOLATILE PARALLEL SAFE;
+
+CREATE VIEW pg_stat_statements AS
+  SELECT * FROM pg_stat_statements(true);
+
+GRANT SELECT ON pg_stat_statements TO PUBLIC;
diff --git a/contrib/pg_stat_statements/pg_stat_statements.c b/contrib/pg_stat_statements/pg_stat_statements.c
index b245d04097..c6b88c9c4e 100644
--- a/contrib/pg_stat_statements/pg_stat_statements.c
+++ b/contrib/pg_stat_statements/pg_stat_statements.c
@@ -69,6 +69,7 @@
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/memutils.h"
+#include "utils/plancache.h"
 #include "utils/timestamp.h"
 
 PG_MODULE_MAGIC;
@@ -111,6 +112,7 @@ typedef enum pgssVersion
 	PGSS_V1_10,
 	PGSS_V1_11,
 	PGSS_V1_12,
+	PGSS_V1_13,
 } pgssVersion;
 
 typedef enum pgssStoreKind
@@ -207,6 +209,8 @@ typedef struct Counters
 											 * to be launched */
 	int64		parallel_workers_launched;	/* # of parallel workers actually
 											 * launched */
+	int64		generic_plan_calls;		/* number of calls using a generic plan */
+	int64		custom_plan_calls;		/* number of calls using a custom plan */
 } Counters;
 
 /*
@@ -321,6 +325,7 @@ PG_FUNCTION_INFO_V1(pg_stat_statements_1_9);
 PG_FUNCTION_INFO_V1(pg_stat_statements_1_10);
 PG_FUNCTION_INFO_V1(pg_stat_statements_1_11);
 PG_FUNCTION_INFO_V1(pg_stat_statements_1_12);
+PG_FUNCTION_INFO_V1(pg_stat_statements_1_13);
 PG_FUNCTION_INFO_V1(pg_stat_statements);
 PG_FUNCTION_INFO_V1(pg_stat_statements_info);
 
@@ -353,7 +358,8 @@ static void pgss_store(const char *query, uint64 queryId,
 					   const struct JitInstrumentation *jitusage,
 					   JumbleState *jstate,
 					   int parallel_workers_to_launch,
-					   int parallel_workers_launched);
+					   int parallel_workers_launched,
+					   CachedPlan *cplan);
 static void pg_stat_statements_internal(FunctionCallInfo fcinfo,
 										pgssVersion api_version,
 										bool showtext);
@@ -875,7 +881,8 @@ pgss_post_parse_analyze(ParseState *pstate, Query *query, JumbleState *jstate)
 				   NULL,
 				   jstate,
 				   0,
-				   0);
+				   0,
+				   NULL);
 }
 
 /*
@@ -955,7 +962,8 @@ pgss_planner(Query *parse,
 				   NULL,
 				   NULL,
 				   0,
-				   0);
+				   0,
+				   NULL);
 	}
 	else
 	{
@@ -1097,7 +1105,8 @@ pgss_ExecutorEnd(QueryDesc *queryDesc)
 				   queryDesc->estate->es_jit ? &queryDesc->estate->es_jit->instr : NULL,
 				   NULL,
 				   queryDesc->estate->es_parallel_workers_to_launch,
-				   queryDesc->estate->es_parallel_workers_launched);
+				   queryDesc->estate->es_parallel_workers_launched,
+				   queryDesc->cplan);
 	}
 
 	if (prev_ExecutorEnd)
@@ -1230,7 +1239,8 @@ pgss_ProcessUtility(PlannedStmt *pstmt, const char *queryString,
 				   NULL,
 				   NULL,
 				   0,
-				   0);
+				   0,
+				   NULL);
 	}
 	else
 	{
@@ -1293,7 +1303,8 @@ pgss_store(const char *query, uint64 queryId,
 		   const struct JitInstrumentation *jitusage,
 		   JumbleState *jstate,
 		   int parallel_workers_to_launch,
-		   int parallel_workers_launched)
+		   int parallel_workers_launched,
+		   CachedPlan *cplan)
 {
 	pgssHashKey key;
 	pgssEntry  *entry;
@@ -1501,6 +1512,15 @@ pgss_store(const char *query, uint64 queryId,
 		entry->counters.parallel_workers_to_launch += parallel_workers_to_launch;
 		entry->counters.parallel_workers_launched += parallel_workers_launched;
 
+		if (cplan)
+		{
+			if (cplan->status == PLAN_CACHE_STATUS_GENERIC_PLAN_BUILD ||
+				cplan->status == PLAN_CACHE_STATUS_GENERIC_PLAN_REUSE)
+				entry->counters.generic_plan_calls++;
+			if (cplan->status == PLAN_CACHE_STATUS_CUSTOM_PLAN)
+				entry->counters.custom_plan_calls++;
+		}
+
 		SpinLockRelease(&entry->mutex);
 	}
 
@@ -1568,7 +1588,8 @@ pg_stat_statements_reset(PG_FUNCTION_ARGS)
 #define PG_STAT_STATEMENTS_COLS_V1_10	43
 #define PG_STAT_STATEMENTS_COLS_V1_11	49
 #define PG_STAT_STATEMENTS_COLS_V1_12	52
-#define PG_STAT_STATEMENTS_COLS			52	/* maximum of above */
+#define PG_STAT_STATEMENTS_COLS_V1_13	54
+#define PG_STAT_STATEMENTS_COLS			54	/* maximum of above */
 
 /*
  * Retrieve statement statistics.
@@ -1580,6 +1601,16 @@ pg_stat_statements_reset(PG_FUNCTION_ARGS)
  * expected API version is identified by embedding it in the C name of the
  * function.  Unfortunately we weren't bright enough to do that for 1.1.
  */
+Datum
+pg_stat_statements_1_13(PG_FUNCTION_ARGS)
+{
+	bool		showtext = PG_GETARG_BOOL(0);
+
+	pg_stat_statements_internal(fcinfo, PGSS_V1_13, showtext);
+
+	return (Datum) 0;
+}
+
 Datum
 pg_stat_statements_1_12(PG_FUNCTION_ARGS)
 {
@@ -1738,6 +1769,10 @@ pg_stat_statements_internal(FunctionCallInfo fcinfo,
 			if (api_version != PGSS_V1_12)
 				elog(ERROR, "incorrect number of output arguments");
 			break;
+		case PG_STAT_STATEMENTS_COLS_V1_13:
+			if (api_version != PGSS_V1_13)
+				elog(ERROR, "incorrect number of output arguments");
+			break;
 		default:
 			elog(ERROR, "incorrect number of output arguments");
 	}
@@ -1995,6 +2030,11 @@ pg_stat_statements_internal(FunctionCallInfo fcinfo,
 			values[i++] = TimestampTzGetDatum(stats_since);
 			values[i++] = TimestampTzGetDatum(minmax_stats_since);
 		}
+		if (api_version >= PGSS_V1_13)
+		{
+			values[i++] = Int64GetDatumFast(tmp.generic_plan_calls);
+			values[i++] = Int64GetDatumFast(tmp.custom_plan_calls);
+		}
 
 		Assert(i == (api_version == PGSS_V1_0 ? PG_STAT_STATEMENTS_COLS_V1_0 :
 					 api_version == PGSS_V1_1 ? PG_STAT_STATEMENTS_COLS_V1_1 :
@@ -2005,6 +2045,7 @@ pg_stat_statements_internal(FunctionCallInfo fcinfo,
 					 api_version == PGSS_V1_10 ? PG_STAT_STATEMENTS_COLS_V1_10 :
 					 api_version == PGSS_V1_11 ? PG_STAT_STATEMENTS_COLS_V1_11 :
 					 api_version == PGSS_V1_12 ? PG_STAT_STATEMENTS_COLS_V1_12 :
+					 api_version == PGSS_V1_13 ? PG_STAT_STATEMENTS_COLS_V1_13 :
 					 -1 /* fail if you forget to update this assert */ ));
 
 		tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, values, nulls);
diff --git a/contrib/pg_stat_statements/pg_stat_statements.control b/contrib/pg_stat_statements/pg_stat_statements.control
index d45ebc12e3..2eee0ceffa 100644
--- a/contrib/pg_stat_statements/pg_stat_statements.control
+++ b/contrib/pg_stat_statements/pg_stat_statements.control
@@ -1,5 +1,5 @@
 # pg_stat_statements extension
 comment = 'track planning and execution statistics of all SQL statements executed'
-default_version = '1.12'
+default_version = '1.13'
 module_pathname = '$libdir/pg_stat_statements'
 relocatable = true
diff --git a/contrib/pg_stat_statements/sql/plan_cache.sql b/contrib/pg_stat_statements/sql/plan_cache.sql
new file mode 100644
index 0000000000..729ca488da
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/plan_cache.sql
@@ -0,0 +1,50 @@
+--
+-- Information related to plan cache
+--
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- plan cache counters for prepared statements
+PREPARE p1 AS SELECT $1;
+-- plan cache auto
+SET plan_cache_mode TO auto;
+EXECUTE p1(1);
+-- force generic plan
+SET plan_cache_mode TO force_generic_plan;
+EXECUTE p1(1);
+-- force custom plan
+SET plan_cache_mode TO force_custom_plan;
+EXECUTE p1(1);
+
+-- plan cache counters for functions and procedures
+SET pg_stat_statements.track = 'all';
+CREATE OR REPLACE FUNCTION select_one_func(int) RETURNS VOID AS $$
+DECLARE
+    ret INT;
+BEGIN
+    SELECT $1 INTO ret;
+END;
+$$ LANGUAGE plpgsql;
+CREATE OR REPLACE PROCEDURE select_one_proc(int) AS $$
+DECLARE
+    ret INT;
+BEGIN
+    select $1 INTO ret;
+END;
+$$ LANGUAGE plpgsql;
+-- plan cache auto
+SET plan_cache_mode TO auto;
+SELECT select_one_func(1);
+CALL select_one_proc(1);
+-- force generic plan
+SET plan_cache_mode TO force_generic_plan;
+SELECT select_one_func(1);
+CALL select_one_proc(1);
+-- force custom plan
+SET plan_cache_mode TO force_custom_plan;
+SELECT select_one_func(1);
+CALL select_one_proc(1);
+
+-- get the plan cache counters
+SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, calls, query FROM pg_stat_statements
+  WHERE query = 'SELECT $1' ORDER BY query COLLATE "C";
diff --git a/doc/src/sgml/pgstatstatements.sgml b/doc/src/sgml/pgstatstatements.sgml
index e2ac1c2d50..e4d043321d 100644
--- a/doc/src/sgml/pgstatstatements.sgml
+++ b/doc/src/sgml/pgstatstatements.sgml
@@ -575,6 +575,24 @@
        <structfield>max_exec_time</structfield>)
       </para></entry>
      </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>generic_plan_calls</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Total number of statements executed using a generic plan
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>custom_plan_calls</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Total number of statements executed using a custom plan
+      </para></entry>
+     </row>
     </tbody>
    </tgroup>
   </table>
diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c
index 6c2979d5c8..510e2d8286 100644
--- a/src/backend/utils/cache/plancache.c
+++ b/src/backend/utils/cache/plancache.c
@@ -1056,7 +1056,7 @@ BuildCachedPlan(CachedPlanSource *plansource, List *qlist,
 	plan->stmt_context = stmt_context;
 	plan->is_oneshot = plansource->is_oneshot;
 	plan->is_saved = false;
-	plan->is_reused = false;
+	plan->status = PLAN_CACHE_STATUS_UNKNOWN;
 	plan->is_valid = true;
 
 	/* assign generation number to new plan */
@@ -1294,7 +1294,8 @@ cached_plan_cost(CachedPlan *plan, bool include_planner)
  * locks are acquired. In such cases, CheckCachedPlan() does not take locks
  * on relations subject to initial runtime pruning; instead, these locks are
  * deferred until execution startup, when ExecDoInitialPruning() performs
- * initial pruning.  The plan's "is_reused" flag is set to indicate that
+ * initial pruning.  The plan's "status" flag is set to
+ * PLAN_CACHE_STATUS_GENERIC_PLAN_REUSE to indicate that
  * CachedPlanRequiresLocking() should return true when called by
  * ExecDoInitialPruning().
  *
@@ -1335,7 +1336,7 @@ GetCachedPlan(CachedPlanSource *plansource, ParamListInfo boundParams,
 			plan = plansource->gplan;
 			Assert(plan->magic == CACHEDPLAN_MAGIC);
 			/* Reusing the existing plan, so not all locks may be acquired. */
-			plan->is_reused = true;
+			plan->status = PLAN_CACHE_STATUS_GENERIC_PLAN_REUSE;
 		}
 		else
 		{
@@ -1379,6 +1380,8 @@ GetCachedPlan(CachedPlanSource *plansource, ParamListInfo boundParams,
 			 * BuildCachedPlan to do that by passing NIL.
 			 */
 			qlist = NIL;
+
+			plan->status = PLAN_CACHE_STATUS_GENERIC_PLAN_BUILD;
 		}
 	}
 
@@ -1390,6 +1393,7 @@ GetCachedPlan(CachedPlanSource *plansource, ParamListInfo boundParams,
 		plansource->total_custom_cost += cached_plan_cost(plan, true);
 
 		plansource->num_custom_plans++;
+		plan->status = PLAN_CACHE_STATUS_CUSTOM_PLAN;
 	}
 	else
 	{
diff --git a/src/include/utils/plancache.h b/src/include/utils/plancache.h
index f1fc770733..1890c6362c 100644
--- a/src/include/utils/plancache.h
+++ b/src/include/utils/plancache.h
@@ -36,6 +36,14 @@ typedef enum
 	PLAN_CACHE_MODE_FORCE_CUSTOM_PLAN,
 }			PlanCacheMode;
 
+typedef enum
+{
+	PLAN_CACHE_STATUS_UNKNOWN = 0,
+	PLAN_CACHE_STATUS_CUSTOM_PLAN,
+	PLAN_CACHE_STATUS_GENERIC_PLAN_BUILD,
+	PLAN_CACHE_STATUS_GENERIC_PLAN_REUSE
+}			PlanCacheStatus;
+
 /* GUC parameter */
 extern PGDLLIMPORT int plan_cache_mode;
 
@@ -153,7 +161,7 @@ typedef struct CachedPlan
 	List	   *stmt_list;		/* list of PlannedStmts */
 	bool		is_oneshot;		/* is it a "oneshot" plan? */
 	bool		is_saved;		/* is CachedPlan in a long-lived context? */
-	bool		is_reused;		/* is it a reused generic plan? */
+	PlanCacheStatus status;		/* status of the cached plan */
 	bool		is_valid;		/* is the stmt_list currently valid? */
 	Oid			planRoleId;		/* Role ID the plan was created for */
 	bool		dependsOnRole;	/* is plan specific to that role? */
@@ -257,7 +265,7 @@ extern void FreeCachedExpression(CachedExpression *cexpr);
 static inline bool
 CachedPlanRequiresLocking(CachedPlan *cplan)
 {
-	return !cplan->is_oneshot && cplan->is_reused;
+	return !cplan->is_oneshot && (cplan->status == PLAN_CACHE_STATUS_GENERIC_PLAN_REUSE);
 }
 
 /*
-- 
2.39.5 (Apple Git-154)

#7Ilia Evdokimov
ilya.evdokimov@tantorlabs.com
In reply to: Sami Imseih (#6)
Re: track generic and custom plans in pg_stat_statements

On 06.03.2025 18:04, Sami Imseih wrote:

2. Should we add Assert(kind == PGSS_EXEC) at this place to ensure that
generic_plan_calls and custom_plan_calls are only incremented when
appropriate?

I don't think an assert is needed here. There is an assert at the start of
the block for PGSS_EXEC and PGSS_PLAN, but cplan is only available
in the executor.

You're right! Moreover, I didn't account for the fact that we pass NULL
to pgss_ProcessUtility. In that case, Assert shouldn't be here.

I don't quite understand why do we need to differentiate between
PLAN_CACHE_STATUS_GENERIC_PLAN_BUILD and
PLAN_CACHE_STATUS_GENERIC_PLAN_REUSE? We could simply keep
PLAN_CACHE_STATUS_GENERIC_PLAN_REUSE. I don't think users would see much
of a difference in either pg_stat_statements or EXPLAIN.

As for EXPLAIN, maybe we should include this in VERBOSE mode?

--
Best regards,
Ilia Evdokimov,
Tantor Labs LLC.

#8Sami Imseih
samimseih@gmail.com
In reply to: Ilia Evdokimov (#7)
Re: track generic and custom plans in pg_stat_statements

I don't quite understand why do we need to differentiate between
PLAN_CACHE_STATUS_GENERIC_PLAN_BUILD and
PLAN_CACHE_STATUS_GENERIC_PLAN_REUSE?
We could simply keep PLAN_CACHE_STATUS_GENERIC_PLAN_REUSE.
I don't think users would see much of a difference in either pg_stat_statements or EXPLAIN.

If we removed GENERIC_PLAN_BUILD, I suppose we can simply rely on
CPlan != NULL && cplan->status != PLAN_CACHE_STATUS_CUSTOM_PLAN
to determine that we have a generic plan. However, I rather keep the status(s)
defined this way for clarity.

As for EXPLAIN, maybe we should include this in VERBOSE mode?

This could be a fast follow-up patch as there appears to be support
for this idea.

--
Sami

#9Sami Imseih
samimseih@gmail.com
In reply to: Sami Imseih (#8)
1 attachment(s)
Re: track generic and custom plans in pg_stat_statements

rebased in the attached v5.

--
Sami Imseih
Amazon Web Services (AWS)

Attachments:

v5-0001-Add-plan_cache-counters-to-pg_stat_statements.patchapplication/octet-stream; name=v5-0001-Add-plan_cache-counters-to-pg_stat_statements.patchDownload
From e998e296a3730938e1bef634c40e43480849b9cc Mon Sep 17 00:00:00 2001
From: Sami Imseih <simseih@amazon.com>
Date: Mon, 7 Apr 2025 15:14:05 -0500
Subject: [PATCH v5 1/1] Add plan_cache counters to pg_stat_statements.

This adds the ability for users to track how many
times a prepared statement was executed using either
a generic plan or a custom plan, by introducing two new
counters in pg_stat_statements: generic_plan_calls and
custom_plan_calls.

Discussion: https://www.postgresql.org/message-id/flat/CAA5RZ0uFw8Y9GCFvafhC=OA8NnMqVZyzXPfv_EePOt+iv1T-qQ@mail.gmail.com
Reviewers: Greg Sabino Mullane, Ilia Evdokimov
---
 contrib/pg_stat_statements/Makefile           |  3 +-
 .../expected/plan_cache.out                   | 87 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  2 +
 .../pg_stat_statements--1.12--1.13.sql        | 78 +++++++++++++++++
 .../pg_stat_statements/pg_stat_statements.c   | 55 ++++++++++--
 .../pg_stat_statements.control                |  2 +-
 contrib/pg_stat_statements/sql/plan_cache.sql | 50 +++++++++++
 doc/src/sgml/pgstatstatements.sgml            | 18 ++++
 src/backend/utils/cache/plancache.c           | 10 ++-
 src/include/utils/plancache.h                 | 12 ++-
 10 files changed, 303 insertions(+), 14 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/plan_cache.out
 create mode 100644 contrib/pg_stat_statements/pg_stat_statements--1.12--1.13.sql
 create mode 100644 contrib/pg_stat_statements/sql/plan_cache.sql

diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index b2bd8794d2a..fc395e16e2c 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -7,6 +7,7 @@ OBJS = \
 
 EXTENSION = pg_stat_statements
 DATA = pg_stat_statements--1.4.sql \
+	pg_stat_statements--1.12--1.13.sql \
 	pg_stat_statements--1.11--1.12.sql pg_stat_statements--1.10--1.11.sql \
 	pg_stat_statements--1.9--1.10.sql pg_stat_statements--1.8--1.9.sql \
 	pg_stat_statements--1.7--1.8.sql pg_stat_statements--1.6--1.7.sql \
@@ -20,7 +21,7 @@ LDFLAGS_SL += $(filter -lm, $(LIBS))
 REGRESS_OPTS = --temp-config $(top_srcdir)/contrib/pg_stat_statements/pg_stat_statements.conf
 REGRESS = select dml cursors utility level_tracking planning \
 	user_activity wal entry_timestamp privileges extended \
-	parallel cleanup oldextversions squashing
+	parallel cleanup oldextversions squashing plan_cache
 # Disabled because these tests require "shared_preload_libraries=pg_stat_statements",
 # which typical installcheck users do not have (e.g. buildfarm clients).
 NO_INSTALLCHECK = 1
diff --git a/contrib/pg_stat_statements/expected/plan_cache.out b/contrib/pg_stat_statements/expected/plan_cache.out
new file mode 100644
index 00000000000..bf11794528c
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/plan_cache.out
@@ -0,0 +1,87 @@
+--
+-- Information related to plan cache
+--
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- plan cache counters for prepared statements
+PREPARE p1 AS SELECT $1;
+-- plan cache auto
+SET plan_cache_mode TO auto;
+EXECUTE p1(1);
+ ?column? 
+----------
+ 1
+(1 row)
+
+-- force generic plan
+SET plan_cache_mode TO force_generic_plan;
+EXECUTE p1(1);
+ ?column? 
+----------
+ 1
+(1 row)
+
+-- force custom plan
+SET plan_cache_mode TO force_custom_plan;
+EXECUTE p1(1);
+ ?column? 
+----------
+ 1
+(1 row)
+
+-- plan cache counters for functions and procedures
+SET pg_stat_statements.track = 'all';
+CREATE OR REPLACE FUNCTION select_one_func(int) RETURNS VOID AS $$
+DECLARE
+    ret INT;
+BEGIN
+    SELECT $1 INTO ret;
+END;
+$$ LANGUAGE plpgsql;
+CREATE OR REPLACE PROCEDURE select_one_proc(int) AS $$
+DECLARE
+    ret INT;
+BEGIN
+    select $1 INTO ret;
+END;
+$$ LANGUAGE plpgsql;
+-- plan cache auto
+SET plan_cache_mode TO auto;
+SELECT select_one_func(1);
+ select_one_func 
+-----------------
+ 
+(1 row)
+
+CALL select_one_proc(1);
+-- force generic plan
+SET plan_cache_mode TO force_generic_plan;
+SELECT select_one_func(1);
+ select_one_func 
+-----------------
+ 
+(1 row)
+
+CALL select_one_proc(1);
+-- force custom plan
+SET plan_cache_mode TO force_custom_plan;
+SELECT select_one_func(1);
+ select_one_func 
+-----------------
+ 
+(1 row)
+
+CALL select_one_proc(1);
+-- get the plan cache counters
+SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, calls, query FROM pg_stat_statements
+  WHERE query = 'SELECT $1' ORDER BY query COLLATE "C";
+ calls | generic_plan_calls | custom_plan_calls | toplevel | calls |   query   
+-------+--------------------+-------------------+----------+-------+-----------
+     6 |                  2 |                 4 | f        |     6 | SELECT $1
+     3 |                  1 |                 2 | t        |     3 | SELECT $1
+(2 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 01a6cbdcf61..3246c03106a 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -21,6 +21,7 @@ contrib_targets += pg_stat_statements
 install_data(
   'pg_stat_statements.control',
   'pg_stat_statements--1.4.sql',
+  'pg_stat_statements--1.12--1.13.sql',
   'pg_stat_statements--1.11--1.12.sql',
   'pg_stat_statements--1.10--1.11.sql',
   'pg_stat_statements--1.9--1.10.sql',
@@ -57,6 +58,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'plan_cache',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/pg_stat_statements--1.12--1.13.sql b/contrib/pg_stat_statements/pg_stat_statements--1.12--1.13.sql
new file mode 100644
index 00000000000..67f2f7dbca3
--- /dev/null
+++ b/contrib/pg_stat_statements/pg_stat_statements--1.12--1.13.sql
@@ -0,0 +1,78 @@
+/* contrib/pg_stat_statements/pg_stat_statements--1.12--1.13.sql */
+
+-- complain if script is sourced in psql, rather than via ALTER EXTENSION
+\echo Use "ALTER EXTENSION pg_stat_statements UPDATE TO '1.13'" to load this file. \quit
+
+/* First we have to remove them from the extension */
+ALTER EXTENSION pg_stat_statements DROP VIEW pg_stat_statements;
+ALTER EXTENSION pg_stat_statements DROP FUNCTION pg_stat_statements(boolean);
+
+/* Then we can drop them */
+DROP VIEW pg_stat_statements;
+DROP FUNCTION pg_stat_statements(boolean);
+
+/* Now redefine */
+CREATE FUNCTION pg_stat_statements(IN showtext boolean,
+    OUT userid oid,
+    OUT dbid oid,
+    OUT toplevel bool,
+    OUT queryid bigint,
+    OUT query text,
+    OUT plans int8,
+    OUT total_plan_time float8,
+    OUT min_plan_time float8,
+    OUT max_plan_time float8,
+    OUT mean_plan_time float8,
+    OUT stddev_plan_time float8,
+    OUT calls int8,
+    OUT total_exec_time float8,
+    OUT min_exec_time float8,
+    OUT max_exec_time float8,
+    OUT mean_exec_time float8,
+    OUT stddev_exec_time float8,
+    OUT rows int8,
+    OUT shared_blks_hit int8,
+    OUT shared_blks_read int8,
+    OUT shared_blks_dirtied int8,
+    OUT shared_blks_written int8,
+    OUT local_blks_hit int8,
+    OUT local_blks_read int8,
+    OUT local_blks_dirtied int8,
+    OUT local_blks_written int8,
+    OUT temp_blks_read int8,
+    OUT temp_blks_written int8,
+    OUT shared_blk_read_time float8,
+    OUT shared_blk_write_time float8,
+    OUT local_blk_read_time float8,
+    OUT local_blk_write_time float8,
+    OUT temp_blk_read_time float8,
+    OUT temp_blk_write_time float8,
+    OUT wal_records int8,
+    OUT wal_fpi int8,
+    OUT wal_bytes numeric,
+    OUT wal_buffers_full int8,
+    OUT jit_functions int8,
+    OUT jit_generation_time float8,
+    OUT jit_inlining_count int8,
+    OUT jit_inlining_time float8,
+    OUT jit_optimization_count int8,
+    OUT jit_optimization_time float8,
+    OUT jit_emission_count int8,
+    OUT jit_emission_time float8,
+    OUT jit_deform_count int8,
+    OUT jit_deform_time float8,
+    OUT parallel_workers_to_launch int8,
+    OUT parallel_workers_launched int8,
+    OUT stats_since timestamp with time zone,
+    OUT minmax_stats_since timestamp with time zone,
+    OUT generic_plan_calls int8,
+    OUT custom_plan_calls int8
+)
+RETURNS SETOF record
+AS 'MODULE_PATHNAME', 'pg_stat_statements_1_13'
+LANGUAGE C STRICT VOLATILE PARALLEL SAFE;
+
+CREATE VIEW pg_stat_statements AS
+  SELECT * FROM pg_stat_statements(true);
+
+GRANT SELECT ON pg_stat_statements TO PUBLIC;
diff --git a/contrib/pg_stat_statements/pg_stat_statements.c b/contrib/pg_stat_statements/pg_stat_statements.c
index 9778407cba3..26a956bf917 100644
--- a/contrib/pg_stat_statements/pg_stat_statements.c
+++ b/contrib/pg_stat_statements/pg_stat_statements.c
@@ -69,6 +69,7 @@
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/memutils.h"
+#include "utils/plancache.h"
 #include "utils/timestamp.h"
 
 PG_MODULE_MAGIC_EXT(
@@ -114,6 +115,7 @@ typedef enum pgssVersion
 	PGSS_V1_10,
 	PGSS_V1_11,
 	PGSS_V1_12,
+	PGSS_V1_13,
 } pgssVersion;
 
 typedef enum pgssStoreKind
@@ -210,6 +212,8 @@ typedef struct Counters
 											 * to be launched */
 	int64		parallel_workers_launched;	/* # of parallel workers actually
 											 * launched */
+	int64		generic_plan_calls;		/* number of calls using a generic plan */
+	int64		custom_plan_calls;		/* number of calls using a custom plan */
 } Counters;
 
 /*
@@ -323,6 +327,7 @@ PG_FUNCTION_INFO_V1(pg_stat_statements_1_9);
 PG_FUNCTION_INFO_V1(pg_stat_statements_1_10);
 PG_FUNCTION_INFO_V1(pg_stat_statements_1_11);
 PG_FUNCTION_INFO_V1(pg_stat_statements_1_12);
+PG_FUNCTION_INFO_V1(pg_stat_statements_1_13);
 PG_FUNCTION_INFO_V1(pg_stat_statements);
 PG_FUNCTION_INFO_V1(pg_stat_statements_info);
 
@@ -355,7 +360,8 @@ static void pgss_store(const char *query, uint64 queryId,
 					   const struct JitInstrumentation *jitusage,
 					   JumbleState *jstate,
 					   int parallel_workers_to_launch,
-					   int parallel_workers_launched);
+					   int parallel_workers_launched,
+					   CachedPlan *cplan);
 static void pg_stat_statements_internal(FunctionCallInfo fcinfo,
 										pgssVersion api_version,
 										bool showtext);
@@ -877,7 +883,8 @@ pgss_post_parse_analyze(ParseState *pstate, Query *query, JumbleState *jstate)
 				   NULL,
 				   jstate,
 				   0,
-				   0);
+				   0,
+				   NULL);
 }
 
 /*
@@ -957,7 +964,8 @@ pgss_planner(Query *parse,
 				   NULL,
 				   NULL,
 				   0,
-				   0);
+				   0,
+				   NULL);
 	}
 	else
 	{
@@ -1099,7 +1107,8 @@ pgss_ExecutorEnd(QueryDesc *queryDesc)
 				   queryDesc->estate->es_jit ? &queryDesc->estate->es_jit->instr : NULL,
 				   NULL,
 				   queryDesc->estate->es_parallel_workers_to_launch,
-				   queryDesc->estate->es_parallel_workers_launched);
+				   queryDesc->estate->es_parallel_workers_launched,
+				   queryDesc->cplan);
 	}
 
 	if (prev_ExecutorEnd)
@@ -1232,7 +1241,8 @@ pgss_ProcessUtility(PlannedStmt *pstmt, const char *queryString,
 				   NULL,
 				   NULL,
 				   0,
-				   0);
+				   0,
+				   NULL);
 	}
 	else
 	{
@@ -1295,7 +1305,8 @@ pgss_store(const char *query, uint64 queryId,
 		   const struct JitInstrumentation *jitusage,
 		   JumbleState *jstate,
 		   int parallel_workers_to_launch,
-		   int parallel_workers_launched)
+		   int parallel_workers_launched,
+		   CachedPlan *cplan)
 {
 	pgssHashKey key;
 	pgssEntry  *entry;
@@ -1503,6 +1514,15 @@ pgss_store(const char *query, uint64 queryId,
 		entry->counters.parallel_workers_to_launch += parallel_workers_to_launch;
 		entry->counters.parallel_workers_launched += parallel_workers_launched;
 
+		if (cplan)
+		{
+			if (cplan->status == PLAN_CACHE_STATUS_GENERIC_PLAN_BUILD ||
+				cplan->status == PLAN_CACHE_STATUS_GENERIC_PLAN_REUSE)
+				entry->counters.generic_plan_calls++;
+			if (cplan->status == PLAN_CACHE_STATUS_CUSTOM_PLAN)
+				entry->counters.custom_plan_calls++;
+		}
+
 		SpinLockRelease(&entry->mutex);
 	}
 
@@ -1570,7 +1590,8 @@ pg_stat_statements_reset(PG_FUNCTION_ARGS)
 #define PG_STAT_STATEMENTS_COLS_V1_10	43
 #define PG_STAT_STATEMENTS_COLS_V1_11	49
 #define PG_STAT_STATEMENTS_COLS_V1_12	52
-#define PG_STAT_STATEMENTS_COLS			52	/* maximum of above */
+#define PG_STAT_STATEMENTS_COLS_V1_13	54
+#define PG_STAT_STATEMENTS_COLS			54	/* maximum of above */
 
 /*
  * Retrieve statement statistics.
@@ -1582,6 +1603,16 @@ pg_stat_statements_reset(PG_FUNCTION_ARGS)
  * expected API version is identified by embedding it in the C name of the
  * function.  Unfortunately we weren't bright enough to do that for 1.1.
  */
+Datum
+pg_stat_statements_1_13(PG_FUNCTION_ARGS)
+{
+	bool		showtext = PG_GETARG_BOOL(0);
+
+	pg_stat_statements_internal(fcinfo, PGSS_V1_13, showtext);
+
+	return (Datum) 0;
+}
+
 Datum
 pg_stat_statements_1_12(PG_FUNCTION_ARGS)
 {
@@ -1740,6 +1771,10 @@ pg_stat_statements_internal(FunctionCallInfo fcinfo,
 			if (api_version != PGSS_V1_12)
 				elog(ERROR, "incorrect number of output arguments");
 			break;
+		case PG_STAT_STATEMENTS_COLS_V1_13:
+			if (api_version != PGSS_V1_13)
+				elog(ERROR, "incorrect number of output arguments");
+			break;
 		default:
 			elog(ERROR, "incorrect number of output arguments");
 	}
@@ -1997,6 +2032,11 @@ pg_stat_statements_internal(FunctionCallInfo fcinfo,
 			values[i++] = TimestampTzGetDatum(stats_since);
 			values[i++] = TimestampTzGetDatum(minmax_stats_since);
 		}
+		if (api_version >= PGSS_V1_13)
+		{
+			values[i++] = Int64GetDatumFast(tmp.generic_plan_calls);
+			values[i++] = Int64GetDatumFast(tmp.custom_plan_calls);
+		}
 
 		Assert(i == (api_version == PGSS_V1_0 ? PG_STAT_STATEMENTS_COLS_V1_0 :
 					 api_version == PGSS_V1_1 ? PG_STAT_STATEMENTS_COLS_V1_1 :
@@ -2007,6 +2047,7 @@ pg_stat_statements_internal(FunctionCallInfo fcinfo,
 					 api_version == PGSS_V1_10 ? PG_STAT_STATEMENTS_COLS_V1_10 :
 					 api_version == PGSS_V1_11 ? PG_STAT_STATEMENTS_COLS_V1_11 :
 					 api_version == PGSS_V1_12 ? PG_STAT_STATEMENTS_COLS_V1_12 :
+					 api_version == PGSS_V1_13 ? PG_STAT_STATEMENTS_COLS_V1_13 :
 					 -1 /* fail if you forget to update this assert */ ));
 
 		tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, values, nulls);
diff --git a/contrib/pg_stat_statements/pg_stat_statements.control b/contrib/pg_stat_statements/pg_stat_statements.control
index d45ebc12e36..2eee0ceffa8 100644
--- a/contrib/pg_stat_statements/pg_stat_statements.control
+++ b/contrib/pg_stat_statements/pg_stat_statements.control
@@ -1,5 +1,5 @@
 # pg_stat_statements extension
 comment = 'track planning and execution statistics of all SQL statements executed'
-default_version = '1.12'
+default_version = '1.13'
 module_pathname = '$libdir/pg_stat_statements'
 relocatable = true
diff --git a/contrib/pg_stat_statements/sql/plan_cache.sql b/contrib/pg_stat_statements/sql/plan_cache.sql
new file mode 100644
index 00000000000..729ca488da2
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/plan_cache.sql
@@ -0,0 +1,50 @@
+--
+-- Information related to plan cache
+--
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- plan cache counters for prepared statements
+PREPARE p1 AS SELECT $1;
+-- plan cache auto
+SET plan_cache_mode TO auto;
+EXECUTE p1(1);
+-- force generic plan
+SET plan_cache_mode TO force_generic_plan;
+EXECUTE p1(1);
+-- force custom plan
+SET plan_cache_mode TO force_custom_plan;
+EXECUTE p1(1);
+
+-- plan cache counters for functions and procedures
+SET pg_stat_statements.track = 'all';
+CREATE OR REPLACE FUNCTION select_one_func(int) RETURNS VOID AS $$
+DECLARE
+    ret INT;
+BEGIN
+    SELECT $1 INTO ret;
+END;
+$$ LANGUAGE plpgsql;
+CREATE OR REPLACE PROCEDURE select_one_proc(int) AS $$
+DECLARE
+    ret INT;
+BEGIN
+    select $1 INTO ret;
+END;
+$$ LANGUAGE plpgsql;
+-- plan cache auto
+SET plan_cache_mode TO auto;
+SELECT select_one_func(1);
+CALL select_one_proc(1);
+-- force generic plan
+SET plan_cache_mode TO force_generic_plan;
+SELECT select_one_func(1);
+CALL select_one_proc(1);
+-- force custom plan
+SET plan_cache_mode TO force_custom_plan;
+SELECT select_one_func(1);
+CALL select_one_proc(1);
+
+-- get the plan cache counters
+SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, calls, query FROM pg_stat_statements
+  WHERE query = 'SELECT $1' ORDER BY query COLLATE "C";
diff --git a/doc/src/sgml/pgstatstatements.sgml b/doc/src/sgml/pgstatstatements.sgml
index 7baa07dcdbf..cc1e0a1c00a 100644
--- a/doc/src/sgml/pgstatstatements.sgml
+++ b/doc/src/sgml/pgstatstatements.sgml
@@ -575,6 +575,24 @@
        <structfield>max_exec_time</structfield>)
       </para></entry>
      </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>generic_plan_calls</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Total number of statements executed using a generic plan
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>custom_plan_calls</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Total number of statements executed using a custom plan
+      </para></entry>
+     </row>
     </tbody>
    </tgroup>
   </table>
diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c
index 3b681647060..ee9636f6352 100644
--- a/src/backend/utils/cache/plancache.c
+++ b/src/backend/utils/cache/plancache.c
@@ -1168,7 +1168,7 @@ BuildCachedPlan(CachedPlanSource *plansource, List *qlist,
 	plan->stmt_context = stmt_context;
 	plan->is_oneshot = plansource->is_oneshot;
 	plan->is_saved = false;
-	plan->is_reused = false;
+	plan->status = PLAN_CACHE_STATUS_UNKNOWN;
 	plan->is_valid = true;
 
 	/* assign generation number to new plan */
@@ -1406,7 +1406,8 @@ cached_plan_cost(CachedPlan *plan, bool include_planner)
  * locks are acquired. In such cases, CheckCachedPlan() does not take locks
  * on relations subject to initial runtime pruning; instead, these locks are
  * deferred until execution startup, when ExecDoInitialPruning() performs
- * initial pruning.  The plan's "is_reused" flag is set to indicate that
+ * initial pruning.  The plan's "status" flag is set to
+ * PLAN_CACHE_STATUS_GENERIC_PLAN_REUSE to indicate that
  * CachedPlanRequiresLocking() should return true when called by
  * ExecDoInitialPruning().
  *
@@ -1447,7 +1448,7 @@ GetCachedPlan(CachedPlanSource *plansource, ParamListInfo boundParams,
 			plan = plansource->gplan;
 			Assert(plan->magic == CACHEDPLAN_MAGIC);
 			/* Reusing the existing plan, so not all locks may be acquired. */
-			plan->is_reused = true;
+			plan->status = PLAN_CACHE_STATUS_GENERIC_PLAN_REUSE;
 		}
 		else
 		{
@@ -1491,6 +1492,8 @@ GetCachedPlan(CachedPlanSource *plansource, ParamListInfo boundParams,
 			 * BuildCachedPlan to do that by passing NIL.
 			 */
 			qlist = NIL;
+
+			plan->status = PLAN_CACHE_STATUS_GENERIC_PLAN_BUILD;
 		}
 	}
 
@@ -1502,6 +1505,7 @@ GetCachedPlan(CachedPlanSource *plansource, ParamListInfo boundParams,
 		plansource->total_custom_cost += cached_plan_cost(plan, true);
 
 		plansource->num_custom_plans++;
+		plan->status = PLAN_CACHE_STATUS_CUSTOM_PLAN;
 	}
 	else
 	{
diff --git a/src/include/utils/plancache.h b/src/include/utils/plancache.h
index 07ec5318db7..5ff3ea2e1f2 100644
--- a/src/include/utils/plancache.h
+++ b/src/include/utils/plancache.h
@@ -37,6 +37,14 @@ typedef enum
 	PLAN_CACHE_MODE_FORCE_CUSTOM_PLAN,
 }			PlanCacheMode;
 
+typedef enum
+{
+	PLAN_CACHE_STATUS_UNKNOWN = 0,
+	PLAN_CACHE_STATUS_CUSTOM_PLAN,
+	PLAN_CACHE_STATUS_GENERIC_PLAN_BUILD,
+	PLAN_CACHE_STATUS_GENERIC_PLAN_REUSE
+}			PlanCacheStatus;
+
 /* GUC parameter */
 extern PGDLLIMPORT int plan_cache_mode;
 
@@ -165,7 +173,7 @@ typedef struct CachedPlan
 	List	   *stmt_list;		/* list of PlannedStmts */
 	bool		is_oneshot;		/* is it a "oneshot" plan? */
 	bool		is_saved;		/* is CachedPlan in a long-lived context? */
-	bool		is_reused;		/* is it a reused generic plan? */
+	PlanCacheStatus status;		/* status of the cached plan */
 	bool		is_valid;		/* is the stmt_list currently valid? */
 	Oid			planRoleId;		/* Role ID the plan was created for */
 	bool		dependsOnRole;	/* is plan specific to that role? */
@@ -275,7 +283,7 @@ extern void FreeCachedExpression(CachedExpression *cexpr);
 static inline bool
 CachedPlanRequiresLocking(CachedPlan *cplan)
 {
-	return !cplan->is_oneshot && cplan->is_reused;
+	return !cplan->is_oneshot && (cplan->status == PLAN_CACHE_STATUS_GENERIC_PLAN_REUSE);
 }
 
 /*
-- 
2.39.5 (Apple Git-154)

#10Ilia Evdokimov
ilya.evdokimov@tantorlabs.com
In reply to: Sami Imseih (#9)
Re: track generic and custom plans in pg_stat_statements

After the introduction of pg_overexplain extension and Robert's comment
[0]: /messages/by-id/CA+TgmoZ8qXiZmmn4P9Mk1cf2mjMMLFPOjSasCjuKSiHFcm-ncw@mail.gmail.com
add this information to EXPLAIN itself. If there are compelling reasons
that showing the plan type would be broadly useful to users in EXPLAIN,
I’m happy to proceed. Otherwise, perhaps this is something better suited
for extension.

[0]: /messages/by-id/CA+TgmoZ8qXiZmmn4P9Mk1cf2mjMMLFPOjSasCjuKSiHFcm-ncw@mail.gmail.com
/messages/by-id/CA+TgmoZ8qXiZmmn4P9Mk1cf2mjMMLFPOjSasCjuKSiHFcm-ncw@mail.gmail.com

--
Best regards,
Ilia Evdokimov,
Tantor Labs LLC.

#11Sami Imseih
samimseih@gmail.com
In reply to: Ilia Evdokimov (#10)
Re: track generic and custom plans in pg_stat_statements

After the introduction of pg_overexplain extension and Robert's comment
[0], I'm starting to have doubts about whether it's still appropriate to
add this information to EXPLAIN itself. If there are compelling reasons
that showing the plan type would be broadly useful to users in EXPLAIN,
I’m happy to proceed. Otherwise, perhaps this is something better suited
for extension.

[0]:

/messages/by-id/CA+TgmoZ8qXiZmmn4P9Mk1cf2mjMMLFPOjSasCjuKSiHFcm-ncw@mail.gmail.com

Yes, pg_overexplain can be used for the explain, and we
may not even need it at all since generic plans
are already displayed with $1, $2 parameters.

Let me know if you have other comments for v5, the
pg_stat_statements enhancements.


Sami Imseih
Amazon Web Services (AWS)

#12Sami Imseih
samimseih@gmail.com
In reply to: Sami Imseih (#11)
1 attachment(s)
Re: track generic and custom plans in pg_stat_statements

It turns out that 1722d5eb05d8e reverted 525392d5727f, which
made CachedPlan available in QueryDesc and thus
available to pgss_ExecutorEnd.

So now we have to make CachedPlan available to QueryDesc as
part of this change. The reason the patch was reverted is related
to a memory leak [0]/messages/by-id/605328.1747710381@sss.pgh.pa.us in the BuildCachedPlan code and is not related
to the part that made CachedPlan available to QueryDesc.

See v6 for the rebase of the patch and addition of testing for EXPLAIN
and EXPLAIN ANALYZE which was missing from v5.

[0]: /messages/by-id/605328.1747710381@sss.pgh.pa.us

--
Sami Imseih
Amazon Web Services (AWS)

Attachments:

v6-0001-Add-plan_cache-counters-to-pg_stat_statements.patchapplication/octet-stream; name=v6-0001-Add-plan_cache-counters-to-pg_stat_statements.patchDownload
From c359b73184735ca5280fbb6e2ce85ae6c4bc660b Mon Sep 17 00:00:00 2001
From: Ubuntu <ubuntu@ip-172-31-38-230.ec2.internal>
Date: Thu, 29 May 2025 18:26:08 +0000
Subject: [PATCH v6 1/1] Add plan_cache counters to pg_stat_statements.

This adds the ability for users to track how many
times a prepared statement was executed using either
a generic plan or a custom plan, by introducing two new
counters in pg_stat_statements: generic_plan_calls and
custom_plan_calls.

Discussion: https://www.postgresql.org/message-id/flat/CAA5RZ0uFw8Y9GCFvafhC=OA8NnMqVZyzXPfv_EePOt+iv1T-qQ@mail.gmail.com
Reviewers: Greg Sabino Mullane, Ilia Evdokimov
---
 contrib/pg_stat_statements/Makefile           |   3 +-
 .../pg_stat_statements/expected/plancache.out | 251 ++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |   2 +
 .../pg_stat_statements--1.12--1.13.sql        |  78 ++++++
 .../pg_stat_statements/pg_stat_statements.c   |  54 +++-
 .../pg_stat_statements.control                |   2 +-
 contrib/pg_stat_statements/sql/plancache.sql  | 111 ++++++++
 doc/src/sgml/pgstatstatements.sgml            |  18 ++
 src/backend/commands/copyto.c                 |   2 +-
 src/backend/commands/createas.c               |   2 +-
 src/backend/commands/explain.c                |   6 +-
 src/backend/commands/extension.c              |   1 +
 src/backend/commands/matview.c                |   2 +-
 src/backend/commands/prepare.c                |   2 +-
 src/backend/executor/execParallel.c           |   1 +
 src/backend/executor/functions.c              |   1 +
 src/backend/executor/spi.c                    |   1 +
 src/backend/tcop/pquery.c                     |  11 +-
 src/backend/utils/cache/plancache.c           |   2 +
 src/include/commands/explain.h                |  10 +-
 src/include/executor/execdesc.h               |   3 +
 src/include/utils/plancache.h                 |   8 +
 22 files changed, 551 insertions(+), 20 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/plancache.out
 create mode 100644 contrib/pg_stat_statements/pg_stat_statements--1.12--1.13.sql
 create mode 100644 contrib/pg_stat_statements/sql/plancache.sql

diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index b2bd8794d2a..996a5fac448 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -7,6 +7,7 @@ OBJS = \
 
 EXTENSION = pg_stat_statements
 DATA = pg_stat_statements--1.4.sql \
+	pg_stat_statements--1.12--1.13.sql \
 	pg_stat_statements--1.11--1.12.sql pg_stat_statements--1.10--1.11.sql \
 	pg_stat_statements--1.9--1.10.sql pg_stat_statements--1.8--1.9.sql \
 	pg_stat_statements--1.7--1.8.sql pg_stat_statements--1.6--1.7.sql \
@@ -20,7 +21,7 @@ LDFLAGS_SL += $(filter -lm, $(LIBS))
 REGRESS_OPTS = --temp-config $(top_srcdir)/contrib/pg_stat_statements/pg_stat_statements.conf
 REGRESS = select dml cursors utility level_tracking planning \
 	user_activity wal entry_timestamp privileges extended \
-	parallel cleanup oldextversions squashing
+	parallel cleanup oldextversions squashing plancache
 # Disabled because these tests require "shared_preload_libraries=pg_stat_statements",
 # which typical installcheck users do not have (e.g. buildfarm clients).
 NO_INSTALLCHECK = 1
diff --git a/contrib/pg_stat_statements/expected/plancache.out b/contrib/pg_stat_statements/expected/plancache.out
new file mode 100644
index 00000000000..a695b4d32ca
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/plancache.out
@@ -0,0 +1,251 @@
+--
+-- Information related to plan cache
+--
+--
+-- Setup
+--
+CREATE OR REPLACE FUNCTION select_one_func(int) RETURNS VOID AS $$
+DECLARE
+    ret INT;
+BEGIN
+    SELECT $1 INTO ret;
+END;
+$$ LANGUAGE plpgsql;
+CREATE OR REPLACE PROCEDURE select_one_proc(int) AS $$
+DECLARE
+    ret INT;
+BEGIN
+    select $1 INTO ret;
+END;
+$$ LANGUAGE plpgsql;
+--
+-- plan cache counters for prepared statements
+--
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+PREPARE p1 AS SELECT $1;
+-- plan cache auto
+SET plan_cache_mode TO auto;
+EXECUTE p1(1);
+ ?column? 
+----------
+ 1
+(1 row)
+
+-- force generic plan
+SET plan_cache_mode TO force_generic_plan;
+EXECUTE p1(1);
+ ?column? 
+----------
+ 1
+(1 row)
+
+-- force custom plan
+SET plan_cache_mode TO force_custom_plan;
+EXECUTE p1(1);
+ ?column? 
+----------
+ 1
+(1 row)
+
+SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, calls, query FROM pg_stat_statements
+    ORDER BY query COLLATE "C";
+ calls | generic_plan_calls | custom_plan_calls | toplevel | calls |                       query                        
+-------+--------------------+-------------------+----------+-------+----------------------------------------------------
+     3 |                  1 |                 2 | t        |     3 | SELECT $1
+     1 |                  0 |                 0 | t        |     1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+     3 |                  0 |                 0 | t        |     3 | SET plan_cache_mode TO $1
+(3 rows)
+
+DEALLOCATE p1;
+--
+-- plan cache counters for explain|explain (analyze) with prepared statements
+--
+SET pg_stat_statements.track = 'all';
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+PREPARE p1 AS SELECT $1;
+-- plan cache auto
+SET plan_cache_mode TO auto;
+EXPLAIN (COSTS OFF) EXECUTE p1(1);
+ QUERY PLAN 
+------------
+ Result
+(1 row)
+
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) EXECUTE p1(1);
+            QUERY PLAN             
+-----------------------------------
+ Result (actual rows=1.00 loops=1)
+(1 row)
+
+-- force generic plan
+SET plan_cache_mode TO force_generic_plan;
+EXPLAIN (COSTS OFF) EXECUTE p1(1);
+ QUERY PLAN 
+------------
+ Result
+(1 row)
+
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) EXECUTE p1(1);
+            QUERY PLAN             
+-----------------------------------
+ Result (actual rows=1.00 loops=1)
+(1 row)
+
+-- force custom plan
+SET plan_cache_mode TO force_custom_plan;
+EXPLAIN (COSTS OFF) EXECUTE p1(1);
+ QUERY PLAN 
+------------
+ Result
+(1 row)
+
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) EXECUTE p1(1);
+            QUERY PLAN             
+-----------------------------------
+ Result (actual rows=1.00 loops=1)
+(1 row)
+
+SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, calls, query FROM pg_stat_statements
+    ORDER BY query COLLATE "C";
+ calls | generic_plan_calls | custom_plan_calls | toplevel | calls |                                      query                                       
+-------+--------------------+-------------------+----------+-------+----------------------------------------------------------------------------------
+     3 |                  0 |                 0 | t        |     3 | EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) EXECUTE p1(1)
+     3 |                  0 |                 0 | t        |     3 | EXPLAIN (COSTS OFF) EXECUTE p1(1)
+     6 |                  2 |                 4 | f        |     6 | SELECT $1
+     1 |                  0 |                 0 | t        |     1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+     3 |                  0 |                 0 | t        |     3 | SET plan_cache_mode TO $1
+(5 rows)
+
+RESET pg_stat_statements.track;
+DEALLOCATE p1;
+--
+-- plan cache counters for functions and procedures
+--
+SET pg_stat_statements.track = 'all';
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- plan cache auto
+SET plan_cache_mode TO auto;
+SELECT select_one_func(1);
+ select_one_func 
+-----------------
+ 
+(1 row)
+
+CALL select_one_proc(1);
+-- force generic plan
+SET plan_cache_mode TO force_generic_plan;
+SELECT select_one_func(1);
+ select_one_func 
+-----------------
+ 
+(1 row)
+
+CALL select_one_proc(1);
+-- force custom plan
+SET plan_cache_mode TO force_custom_plan;
+SELECT select_one_func(1);
+ select_one_func 
+-----------------
+ 
+(1 row)
+
+CALL select_one_proc(1);
+SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, calls, query FROM pg_stat_statements
+    ORDER BY query COLLATE "C";
+ calls | generic_plan_calls | custom_plan_calls | toplevel | calls |                       query                        
+-------+--------------------+-------------------+----------+-------+----------------------------------------------------
+     3 |                  0 |                 0 | t        |     3 | CALL select_one_proc($1)
+     6 |                  2 |                 4 | f        |     6 | SELECT $1
+     1 |                  0 |                 0 | t        |     1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+     3 |                  0 |                 0 | t        |     3 | SELECT select_one_func($1)
+     3 |                  0 |                 0 | t        |     3 | SET plan_cache_mode TO $1
+(5 rows)
+
+--
+-- plan cache counters for explain|explain (analyze) with functions and procedures
+--
+SET pg_stat_statements.track = 'all';
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- plan cache auto
+SET plan_cache_mode TO auto;
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) SELECT select_one_func(1);
+            QUERY PLAN             
+-----------------------------------
+ Result (actual rows=1.00 loops=1)
+(1 row)
+
+EXPLAIN (COSTS OFF) SELECT select_one_func(1);
+ QUERY PLAN 
+------------
+ Result
+(1 row)
+
+CALL select_one_proc(1);
+-- force generic plan
+SET plan_cache_mode TO force_generic_plan;
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) SELECT select_one_func(1);
+            QUERY PLAN             
+-----------------------------------
+ Result (actual rows=1.00 loops=1)
+(1 row)
+
+EXPLAIN (COSTS OFF) SELECT select_one_func(1);
+ QUERY PLAN 
+------------
+ Result
+(1 row)
+
+CALL select_one_proc(1);
+-- force custom plan
+SET plan_cache_mode TO force_custom_plan;
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) SELECT select_one_func(1);
+            QUERY PLAN             
+-----------------------------------
+ Result (actual rows=1.00 loops=1)
+(1 row)
+
+EXPLAIN (COSTS OFF) SELECT select_one_func(1);
+ QUERY PLAN 
+------------
+ Result
+(1 row)
+
+CALL select_one_proc(1);
+SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, calls, query FROM pg_stat_statements
+    ORDER BY query COLLATE "C";
+ calls | generic_plan_calls | custom_plan_calls | toplevel | calls |                                             query                                             
+-------+--------------------+-------------------+----------+-------+-----------------------------------------------------------------------------------------------
+     3 |                  0 |                 0 | t        |     3 | CALL select_one_proc($1)
+     3 |                  0 |                 0 | t        |     3 | EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) SELECT select_one_func($1)
+     3 |                  0 |                 0 | t        |     3 | EXPLAIN (COSTS OFF) SELECT select_one_func($1)
+     6 |                  2 |                 4 | f        |     6 | SELECT $1
+     1 |                  0 |                 0 | t        |     1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+     6 |                  0 |                 0 | f        |     6 | SELECT select_one_func($1)
+     3 |                  0 |                 0 | t        |     3 | SET plan_cache_mode TO $1
+(7 rows)
+
+--
+-- Cleanup
+--
+DROP FUNCTION select_one_func(int);
+DROP PROCEDURE select_one_proc(int);
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 01a6cbdcf61..110fb82fe12 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -21,6 +21,7 @@ contrib_targets += pg_stat_statements
 install_data(
   'pg_stat_statements.control',
   'pg_stat_statements--1.4.sql',
+  'pg_stat_statements--1.12--1.13.sql',
   'pg_stat_statements--1.11--1.12.sql',
   'pg_stat_statements--1.10--1.11.sql',
   'pg_stat_statements--1.9--1.10.sql',
@@ -57,6 +58,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'plancache',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/pg_stat_statements--1.12--1.13.sql b/contrib/pg_stat_statements/pg_stat_statements--1.12--1.13.sql
new file mode 100644
index 00000000000..67f2f7dbca3
--- /dev/null
+++ b/contrib/pg_stat_statements/pg_stat_statements--1.12--1.13.sql
@@ -0,0 +1,78 @@
+/* contrib/pg_stat_statements/pg_stat_statements--1.12--1.13.sql */
+
+-- complain if script is sourced in psql, rather than via ALTER EXTENSION
+\echo Use "ALTER EXTENSION pg_stat_statements UPDATE TO '1.13'" to load this file. \quit
+
+/* First we have to remove them from the extension */
+ALTER EXTENSION pg_stat_statements DROP VIEW pg_stat_statements;
+ALTER EXTENSION pg_stat_statements DROP FUNCTION pg_stat_statements(boolean);
+
+/* Then we can drop them */
+DROP VIEW pg_stat_statements;
+DROP FUNCTION pg_stat_statements(boolean);
+
+/* Now redefine */
+CREATE FUNCTION pg_stat_statements(IN showtext boolean,
+    OUT userid oid,
+    OUT dbid oid,
+    OUT toplevel bool,
+    OUT queryid bigint,
+    OUT query text,
+    OUT plans int8,
+    OUT total_plan_time float8,
+    OUT min_plan_time float8,
+    OUT max_plan_time float8,
+    OUT mean_plan_time float8,
+    OUT stddev_plan_time float8,
+    OUT calls int8,
+    OUT total_exec_time float8,
+    OUT min_exec_time float8,
+    OUT max_exec_time float8,
+    OUT mean_exec_time float8,
+    OUT stddev_exec_time float8,
+    OUT rows int8,
+    OUT shared_blks_hit int8,
+    OUT shared_blks_read int8,
+    OUT shared_blks_dirtied int8,
+    OUT shared_blks_written int8,
+    OUT local_blks_hit int8,
+    OUT local_blks_read int8,
+    OUT local_blks_dirtied int8,
+    OUT local_blks_written int8,
+    OUT temp_blks_read int8,
+    OUT temp_blks_written int8,
+    OUT shared_blk_read_time float8,
+    OUT shared_blk_write_time float8,
+    OUT local_blk_read_time float8,
+    OUT local_blk_write_time float8,
+    OUT temp_blk_read_time float8,
+    OUT temp_blk_write_time float8,
+    OUT wal_records int8,
+    OUT wal_fpi int8,
+    OUT wal_bytes numeric,
+    OUT wal_buffers_full int8,
+    OUT jit_functions int8,
+    OUT jit_generation_time float8,
+    OUT jit_inlining_count int8,
+    OUT jit_inlining_time float8,
+    OUT jit_optimization_count int8,
+    OUT jit_optimization_time float8,
+    OUT jit_emission_count int8,
+    OUT jit_emission_time float8,
+    OUT jit_deform_count int8,
+    OUT jit_deform_time float8,
+    OUT parallel_workers_to_launch int8,
+    OUT parallel_workers_launched int8,
+    OUT stats_since timestamp with time zone,
+    OUT minmax_stats_since timestamp with time zone,
+    OUT generic_plan_calls int8,
+    OUT custom_plan_calls int8
+)
+RETURNS SETOF record
+AS 'MODULE_PATHNAME', 'pg_stat_statements_1_13'
+LANGUAGE C STRICT VOLATILE PARALLEL SAFE;
+
+CREATE VIEW pg_stat_statements AS
+  SELECT * FROM pg_stat_statements(true);
+
+GRANT SELECT ON pg_stat_statements TO PUBLIC;
diff --git a/contrib/pg_stat_statements/pg_stat_statements.c b/contrib/pg_stat_statements/pg_stat_statements.c
index c58f34e9f30..8b43bb5a24b 100644
--- a/contrib/pg_stat_statements/pg_stat_statements.c
+++ b/contrib/pg_stat_statements/pg_stat_statements.c
@@ -69,6 +69,7 @@
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/memutils.h"
+#include "utils/plancache.h"
 #include "utils/timestamp.h"
 
 PG_MODULE_MAGIC_EXT(
@@ -114,6 +115,7 @@ typedef enum pgssVersion
 	PGSS_V1_10,
 	PGSS_V1_11,
 	PGSS_V1_12,
+	PGSS_V1_13,
 } pgssVersion;
 
 typedef enum pgssStoreKind
@@ -210,6 +212,8 @@ typedef struct Counters
 											 * to be launched */
 	int64		parallel_workers_launched;	/* # of parallel workers actually
 											 * launched */
+	int64		generic_plan_calls; /* number of calls using a generic plan */
+	int64		custom_plan_calls;	/* number of calls using a custom plan */
 } Counters;
 
 /*
@@ -323,6 +327,7 @@ PG_FUNCTION_INFO_V1(pg_stat_statements_1_9);
 PG_FUNCTION_INFO_V1(pg_stat_statements_1_10);
 PG_FUNCTION_INFO_V1(pg_stat_statements_1_11);
 PG_FUNCTION_INFO_V1(pg_stat_statements_1_12);
+PG_FUNCTION_INFO_V1(pg_stat_statements_1_13);
 PG_FUNCTION_INFO_V1(pg_stat_statements);
 PG_FUNCTION_INFO_V1(pg_stat_statements_info);
 
@@ -355,7 +360,8 @@ static void pgss_store(const char *query, uint64 queryId,
 					   const struct JitInstrumentation *jitusage,
 					   JumbleState *jstate,
 					   int parallel_workers_to_launch,
-					   int parallel_workers_launched);
+					   int parallel_workers_launched,
+					   CachedPlan *cplan);
 static void pg_stat_statements_internal(FunctionCallInfo fcinfo,
 										pgssVersion api_version,
 										bool showtext);
@@ -877,7 +883,8 @@ pgss_post_parse_analyze(ParseState *pstate, Query *query, JumbleState *jstate)
 				   NULL,
 				   jstate,
 				   0,
-				   0);
+				   0,
+				   NULL);
 }
 
 /*
@@ -957,7 +964,8 @@ pgss_planner(Query *parse,
 				   NULL,
 				   NULL,
 				   0,
-				   0);
+				   0,
+				   NULL);
 	}
 	else
 	{
@@ -1091,7 +1099,8 @@ pgss_ExecutorEnd(QueryDesc *queryDesc)
 				   queryDesc->estate->es_jit ? &queryDesc->estate->es_jit->instr : NULL,
 				   NULL,
 				   queryDesc->estate->es_parallel_workers_to_launch,
-				   queryDesc->estate->es_parallel_workers_launched);
+				   queryDesc->estate->es_parallel_workers_launched,
+				   queryDesc->cplan);
 	}
 
 	if (prev_ExecutorEnd)
@@ -1224,7 +1233,8 @@ pgss_ProcessUtility(PlannedStmt *pstmt, const char *queryString,
 				   NULL,
 				   NULL,
 				   0,
-				   0);
+				   0,
+				   NULL);
 	}
 	else
 	{
@@ -1287,7 +1297,8 @@ pgss_store(const char *query, uint64 queryId,
 		   const struct JitInstrumentation *jitusage,
 		   JumbleState *jstate,
 		   int parallel_workers_to_launch,
-		   int parallel_workers_launched)
+		   int parallel_workers_launched,
+		   CachedPlan *cplan)
 {
 	pgssHashKey key;
 	pgssEntry  *entry;
@@ -1495,6 +1506,14 @@ pgss_store(const char *query, uint64 queryId,
 		entry->counters.parallel_workers_to_launch += parallel_workers_to_launch;
 		entry->counters.parallel_workers_launched += parallel_workers_launched;
 
+		if (cplan)
+		{
+			if (cplan->status == PLAN_CACHE_STATUS_GENERIC_PLAN)
+				entry->counters.generic_plan_calls++;
+			if (cplan->status == PLAN_CACHE_STATUS_CUSTOM_PLAN)
+				entry->counters.custom_plan_calls++;
+		}
+
 		SpinLockRelease(&entry->mutex);
 	}
 
@@ -1562,7 +1581,8 @@ pg_stat_statements_reset(PG_FUNCTION_ARGS)
 #define PG_STAT_STATEMENTS_COLS_V1_10	43
 #define PG_STAT_STATEMENTS_COLS_V1_11	49
 #define PG_STAT_STATEMENTS_COLS_V1_12	52
-#define PG_STAT_STATEMENTS_COLS			52	/* maximum of above */
+#define PG_STAT_STATEMENTS_COLS_V1_13	54
+#define PG_STAT_STATEMENTS_COLS			54	/* maximum of above */
 
 /*
  * Retrieve statement statistics.
@@ -1574,6 +1594,16 @@ pg_stat_statements_reset(PG_FUNCTION_ARGS)
  * expected API version is identified by embedding it in the C name of the
  * function.  Unfortunately we weren't bright enough to do that for 1.1.
  */
+Datum
+pg_stat_statements_1_13(PG_FUNCTION_ARGS)
+{
+	bool		showtext = PG_GETARG_BOOL(0);
+
+	pg_stat_statements_internal(fcinfo, PGSS_V1_13, showtext);
+
+	return (Datum) 0;
+}
+
 Datum
 pg_stat_statements_1_12(PG_FUNCTION_ARGS)
 {
@@ -1732,6 +1762,10 @@ pg_stat_statements_internal(FunctionCallInfo fcinfo,
 			if (api_version != PGSS_V1_12)
 				elog(ERROR, "incorrect number of output arguments");
 			break;
+		case PG_STAT_STATEMENTS_COLS_V1_13:
+			if (api_version != PGSS_V1_13)
+				elog(ERROR, "incorrect number of output arguments");
+			break;
 		default:
 			elog(ERROR, "incorrect number of output arguments");
 	}
@@ -1989,6 +2023,11 @@ pg_stat_statements_internal(FunctionCallInfo fcinfo,
 			values[i++] = TimestampTzGetDatum(stats_since);
 			values[i++] = TimestampTzGetDatum(minmax_stats_since);
 		}
+		if (api_version >= PGSS_V1_13)
+		{
+			values[i++] = Int64GetDatumFast(tmp.generic_plan_calls);
+			values[i++] = Int64GetDatumFast(tmp.custom_plan_calls);
+		}
 
 		Assert(i == (api_version == PGSS_V1_0 ? PG_STAT_STATEMENTS_COLS_V1_0 :
 					 api_version == PGSS_V1_1 ? PG_STAT_STATEMENTS_COLS_V1_1 :
@@ -1999,6 +2038,7 @@ pg_stat_statements_internal(FunctionCallInfo fcinfo,
 					 api_version == PGSS_V1_10 ? PG_STAT_STATEMENTS_COLS_V1_10 :
 					 api_version == PGSS_V1_11 ? PG_STAT_STATEMENTS_COLS_V1_11 :
 					 api_version == PGSS_V1_12 ? PG_STAT_STATEMENTS_COLS_V1_12 :
+					 api_version == PGSS_V1_13 ? PG_STAT_STATEMENTS_COLS_V1_13 :
 					 -1 /* fail if you forget to update this assert */ ));
 
 		tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, values, nulls);
diff --git a/contrib/pg_stat_statements/pg_stat_statements.control b/contrib/pg_stat_statements/pg_stat_statements.control
index d45ebc12e36..2eee0ceffa8 100644
--- a/contrib/pg_stat_statements/pg_stat_statements.control
+++ b/contrib/pg_stat_statements/pg_stat_statements.control
@@ -1,5 +1,5 @@
 # pg_stat_statements extension
 comment = 'track planning and execution statistics of all SQL statements executed'
-default_version = '1.12'
+default_version = '1.13'
 module_pathname = '$libdir/pg_stat_statements'
 relocatable = true
diff --git a/contrib/pg_stat_statements/sql/plancache.sql b/contrib/pg_stat_statements/sql/plancache.sql
new file mode 100644
index 00000000000..9f25aef92d3
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/plancache.sql
@@ -0,0 +1,111 @@
+--
+-- Information related to plan cache
+--
+
+--
+-- Setup
+--
+CREATE OR REPLACE FUNCTION select_one_func(int) RETURNS VOID AS $$
+DECLARE
+    ret INT;
+BEGIN
+    SELECT $1 INTO ret;
+END;
+$$ LANGUAGE plpgsql;
+CREATE OR REPLACE PROCEDURE select_one_proc(int) AS $$
+DECLARE
+    ret INT;
+BEGIN
+    select $1 INTO ret;
+END;
+$$ LANGUAGE plpgsql;
+
+--
+-- plan cache counters for prepared statements
+--
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+PREPARE p1 AS SELECT $1;
+-- plan cache auto
+SET plan_cache_mode TO auto;
+EXECUTE p1(1);
+-- force generic plan
+SET plan_cache_mode TO force_generic_plan;
+EXECUTE p1(1);
+-- force custom plan
+SET plan_cache_mode TO force_custom_plan;
+EXECUTE p1(1);
+SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, calls, query FROM pg_stat_statements
+    ORDER BY query COLLATE "C";
+DEALLOCATE p1;
+
+--
+-- plan cache counters for explain|explain (analyze) with prepared statements
+--
+SET pg_stat_statements.track = 'all';
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+PREPARE p1 AS SELECT $1;
+-- plan cache auto
+SET plan_cache_mode TO auto;
+EXPLAIN (COSTS OFF) EXECUTE p1(1);
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) EXECUTE p1(1);
+-- force generic plan
+SET plan_cache_mode TO force_generic_plan;
+EXPLAIN (COSTS OFF) EXECUTE p1(1);
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) EXECUTE p1(1);
+-- force custom plan
+SET plan_cache_mode TO force_custom_plan;
+EXPLAIN (COSTS OFF) EXECUTE p1(1);
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) EXECUTE p1(1);
+SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, calls, query FROM pg_stat_statements
+    ORDER BY query COLLATE "C";
+RESET pg_stat_statements.track;
+DEALLOCATE p1;
+
+--
+-- plan cache counters for functions and procedures
+--
+SET pg_stat_statements.track = 'all';
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+-- plan cache auto
+SET plan_cache_mode TO auto;
+SELECT select_one_func(1);
+CALL select_one_proc(1);
+-- force generic plan
+SET plan_cache_mode TO force_generic_plan;
+SELECT select_one_func(1);
+CALL select_one_proc(1);
+-- force custom plan
+SET plan_cache_mode TO force_custom_plan;
+SELECT select_one_func(1);
+CALL select_one_proc(1);
+SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, calls, query FROM pg_stat_statements
+    ORDER BY query COLLATE "C";
+
+--
+-- plan cache counters for explain|explain (analyze) with functions and procedures
+--
+SET pg_stat_statements.track = 'all';
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+-- plan cache auto
+SET plan_cache_mode TO auto;
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) SELECT select_one_func(1);
+EXPLAIN (COSTS OFF) SELECT select_one_func(1);
+CALL select_one_proc(1);
+-- force generic plan
+SET plan_cache_mode TO force_generic_plan;
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) SELECT select_one_func(1);
+EXPLAIN (COSTS OFF) SELECT select_one_func(1);
+CALL select_one_proc(1);
+-- force custom plan
+SET plan_cache_mode TO force_custom_plan;
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) SELECT select_one_func(1);
+EXPLAIN (COSTS OFF) SELECT select_one_func(1);
+CALL select_one_proc(1);
+SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, calls, query FROM pg_stat_statements
+    ORDER BY query COLLATE "C";
+
+--
+-- Cleanup
+--
+DROP FUNCTION select_one_func(int);
+DROP PROCEDURE select_one_proc(int);
diff --git a/doc/src/sgml/pgstatstatements.sgml b/doc/src/sgml/pgstatstatements.sgml
index 7baa07dcdbf..cc1e0a1c00a 100644
--- a/doc/src/sgml/pgstatstatements.sgml
+++ b/doc/src/sgml/pgstatstatements.sgml
@@ -575,6 +575,24 @@
        <structfield>max_exec_time</structfield>)
       </para></entry>
      </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>generic_plan_calls</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Total number of statements executed using a generic plan
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>custom_plan_calls</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Total number of statements executed using a custom plan
+      </para></entry>
+     </row>
     </tbody>
    </tgroup>
   </table>
diff --git a/src/backend/commands/copyto.c b/src/backend/commands/copyto.c
index ea6f18f2c80..55a2719d768 100644
--- a/src/backend/commands/copyto.c
+++ b/src/backend/commands/copyto.c
@@ -835,7 +835,7 @@ BeginCopyTo(ParseState *pstate,
 		((DR_copy *) dest)->cstate = cstate;
 
 		/* Create a QueryDesc requesting no output */
-		cstate->queryDesc = CreateQueryDesc(plan, pstate->p_sourcetext,
+		cstate->queryDesc = CreateQueryDesc(plan, NULL, pstate->p_sourcetext,
 											GetActiveSnapshot(),
 											InvalidSnapshot,
 											dest, NULL, NULL, 0);
diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c
index dfd2ab8e862..18c8a11b70c 100644
--- a/src/backend/commands/createas.c
+++ b/src/backend/commands/createas.c
@@ -334,7 +334,7 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt,
 		UpdateActiveSnapshotCommandId();
 
 		/* Create a QueryDesc, redirecting output to our tuple receiver */
-		queryDesc = CreateQueryDesc(plan, pstate->p_sourcetext,
+		queryDesc = CreateQueryDesc(plan, NULL, pstate->p_sourcetext,
 									GetActiveSnapshot(), InvalidSnapshot,
 									dest, params, queryEnv, 0);
 
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index bfa83fbc3fe..9c92655af34 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -369,7 +369,7 @@ standard_ExplainOneQuery(Query *query, int cursorOptions,
 	}
 
 	/* run it (if needed) and produce output */
-	ExplainOnePlan(plan, into, es, queryString, params, queryEnv,
+	ExplainOnePlan(plan, NULL, into, es, queryString, params, queryEnv,
 				   &planduration, (es->buffers ? &bufusage : NULL),
 				   es->memory ? &mem_counters : NULL);
 }
@@ -491,7 +491,7 @@ ExplainOneUtility(Node *utilityStmt, IntoClause *into, ExplainState *es,
  * to call it.
  */
 void
-ExplainOnePlan(PlannedStmt *plannedstmt, IntoClause *into, ExplainState *es,
+ExplainOnePlan(PlannedStmt *plannedstmt, CachedPlan *cplan, IntoClause *into, ExplainState *es,
 			   const char *queryString, ParamListInfo params,
 			   QueryEnvironment *queryEnv, const instr_time *planduration,
 			   const BufferUsage *bufusage,
@@ -547,7 +547,7 @@ ExplainOnePlan(PlannedStmt *plannedstmt, IntoClause *into, ExplainState *es,
 		dest = None_Receiver;
 
 	/* Create a QueryDesc for the query */
-	queryDesc = CreateQueryDesc(plannedstmt, queryString,
+	queryDesc = CreateQueryDesc(plannedstmt, cplan, queryString,
 								GetActiveSnapshot(), InvalidSnapshot,
 								dest, params, queryEnv, instrument_option);
 
diff --git a/src/backend/commands/extension.c b/src/backend/commands/extension.c
index e6f9ab6dfd6..bc3e7bb808f 100644
--- a/src/backend/commands/extension.c
+++ b/src/backend/commands/extension.c
@@ -993,6 +993,7 @@ execute_sql_string(const char *sql, const char *filename)
 				QueryDesc  *qdesc;
 
 				qdesc = CreateQueryDesc(stmt,
+										NULL,
 										sql,
 										GetActiveSnapshot(), NULL,
 										dest, NULL, NULL, 0);
diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index 27c2cb26ef5..c9e7b6f5195 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -438,7 +438,7 @@ refresh_matview_datafill(DestReceiver *dest, Query *query,
 	UpdateActiveSnapshotCommandId();
 
 	/* Create a QueryDesc, redirecting output to our tuple receiver */
-	queryDesc = CreateQueryDesc(plan, queryString,
+	queryDesc = CreateQueryDesc(plan, NULL, queryString,
 								GetActiveSnapshot(), InvalidSnapshot,
 								dest, NULL, NULL, 0);
 
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..a5f3a662e51 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -657,7 +657,7 @@ ExplainExecuteQuery(ExecuteStmt *execstmt, IntoClause *into, ExplainState *es,
 		PlannedStmt *pstmt = lfirst_node(PlannedStmt, p);
 
 		if (pstmt->commandType != CMD_UTILITY)
-			ExplainOnePlan(pstmt, into, es, query_string, paramLI, pstate->p_queryEnv,
+			ExplainOnePlan(pstmt, cplan, into, es, query_string, paramLI, pstate->p_queryEnv,
 						   &planduration, (es->buffers ? &bufusage : NULL),
 						   es->memory ? &mem_counters : NULL);
 		else
diff --git a/src/backend/executor/execParallel.c b/src/backend/executor/execParallel.c
index f3e77bda279..58b1c09223a 100644
--- a/src/backend/executor/execParallel.c
+++ b/src/backend/executor/execParallel.c
@@ -1280,6 +1280,7 @@ ExecParallelGetQueryDesc(shm_toc *toc, DestReceiver *receiver,
 
 	/* Create a QueryDesc for the query. */
 	return CreateQueryDesc(pstmt,
+						   NULL,
 						   queryString,
 						   GetActiveSnapshot(), InvalidSnapshot,
 						   receiver, paramLI, NULL, instrument_options);
diff --git a/src/backend/executor/functions.c b/src/backend/executor/functions.c
index 359aafea681..635ce762eb3 100644
--- a/src/backend/executor/functions.c
+++ b/src/backend/executor/functions.c
@@ -1339,6 +1339,7 @@ postquel_start(execution_state *es, SQLFunctionCachePtr fcache)
 		dest = None_Receiver;
 
 	es->qd = CreateQueryDesc(es->stmt,
+							 NULL,
 							 fcache->func->src,
 							 GetActiveSnapshot(),
 							 InvalidSnapshot,
diff --git a/src/backend/executor/spi.c b/src/backend/executor/spi.c
index ecb2e4ccaa1..5ffff06ec0e 100644
--- a/src/backend/executor/spi.c
+++ b/src/backend/executor/spi.c
@@ -2690,6 +2690,7 @@ _SPI_execute_plan(SPIPlanPtr plan, const SPIExecuteOptions *options,
 					snap = InvalidSnapshot;
 
 				qdesc = CreateQueryDesc(stmt,
+										cplan,
 										plansource->query_string,
 										snap, crosscheck_snapshot,
 										dest,
diff --git a/src/backend/tcop/pquery.c b/src/backend/tcop/pquery.c
index d1593f38b35..dc4f5ce9d5e 100644
--- a/src/backend/tcop/pquery.c
+++ b/src/backend/tcop/pquery.c
@@ -26,6 +26,7 @@
 #include "tcop/pquery.h"
 #include "tcop/utility.h"
 #include "utils/memutils.h"
+#include "utils/plancache.h"
 #include "utils/snapmgr.h"
 
 
@@ -37,6 +38,7 @@ Portal		ActivePortal = NULL;
 
 
 static void ProcessQuery(PlannedStmt *plan,
+						 CachedPlan *cplan,
 						 const char *sourceText,
 						 ParamListInfo params,
 						 QueryEnvironment *queryEnv,
@@ -66,6 +68,7 @@ static void DoPortalRewind(Portal portal);
  */
 QueryDesc *
 CreateQueryDesc(PlannedStmt *plannedstmt,
+				CachedPlan *cplan,
 				const char *sourceText,
 				Snapshot snapshot,
 				Snapshot crosscheck_snapshot,
@@ -93,6 +96,8 @@ CreateQueryDesc(PlannedStmt *plannedstmt,
 	qd->planstate = NULL;
 	qd->totaltime = NULL;
 
+	qd->cplan = cplan;
+
 	/* not yet executed */
 	qd->already_executed = false;
 
@@ -135,6 +140,7 @@ FreeQueryDesc(QueryDesc *qdesc)
  */
 static void
 ProcessQuery(PlannedStmt *plan,
+			 CachedPlan *cplan,
 			 const char *sourceText,
 			 ParamListInfo params,
 			 QueryEnvironment *queryEnv,
@@ -146,7 +152,7 @@ ProcessQuery(PlannedStmt *plan,
 	/*
 	 * Create the QueryDesc object
 	 */
-	queryDesc = CreateQueryDesc(plan, sourceText,
+	queryDesc = CreateQueryDesc(plan, cplan, sourceText,
 								GetActiveSnapshot(), InvalidSnapshot,
 								dest, params, queryEnv, 0);
 
@@ -494,6 +500,7 @@ PortalStart(Portal portal, ParamListInfo params,
 				 * the destination to DestNone.
 				 */
 				queryDesc = CreateQueryDesc(linitial_node(PlannedStmt, portal->stmts),
+											portal->cplan,
 											portal->sourceText,
 											GetActiveSnapshot(),
 											InvalidSnapshot,
@@ -1270,6 +1277,7 @@ PortalRunMulti(Portal portal,
 			{
 				/* statement can set tag string */
 				ProcessQuery(pstmt,
+							 portal->cplan,
 							 portal->sourceText,
 							 portal->portalParams,
 							 portal->queryEnv,
@@ -1279,6 +1287,7 @@ PortalRunMulti(Portal portal,
 			{
 				/* stmt added by rewrite cannot set tag */
 				ProcessQuery(pstmt,
+							 portal->cplan,
 							 portal->sourceText,
 							 portal->portalParams,
 							 portal->queryEnv,
diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c
index 89a1c79e984..4d955ca1880 100644
--- a/src/backend/utils/cache/plancache.c
+++ b/src/backend/utils/cache/plancache.c
@@ -1358,10 +1358,12 @@ GetCachedPlan(CachedPlanSource *plansource, ParamListInfo boundParams,
 		plansource->total_custom_cost += cached_plan_cost(plan, true);
 
 		plansource->num_custom_plans++;
+		plan->status = PLAN_CACHE_STATUS_CUSTOM_PLAN;
 	}
 	else
 	{
 		plansource->num_generic_plans++;
+		plan->status = PLAN_CACHE_STATUS_GENERIC_PLAN;
 	}
 
 	Assert(plan != NULL);
diff --git a/src/include/commands/explain.h b/src/include/commands/explain.h
index 3b122f79ed8..36579590ec2 100644
--- a/src/include/commands/explain.h
+++ b/src/include/commands/explain.h
@@ -63,9 +63,13 @@ extern void ExplainOneUtility(Node *utilityStmt, IntoClause *into,
 							  struct ExplainState *es, ParseState *pstate,
 							  ParamListInfo params);
 
-extern void ExplainOnePlan(PlannedStmt *plannedstmt, IntoClause *into,
-						   struct ExplainState *es, const char *queryString,
-						   ParamListInfo params, QueryEnvironment *queryEnv,
+extern void ExplainOnePlan(PlannedStmt *plannedstmt,
+						   CachedPlan *cplan,
+						   IntoClause *into,
+						   struct ExplainState *es,
+						   const char *queryString,
+						   ParamListInfo params,
+						   QueryEnvironment *queryEnv,
 						   const instr_time *planduration,
 						   const BufferUsage *bufusage,
 						   const MemoryContextCounters *mem_counters);
diff --git a/src/include/executor/execdesc.h b/src/include/executor/execdesc.h
index 86db3dc8d0d..f7cbe0981a5 100644
--- a/src/include/executor/execdesc.h
+++ b/src/include/executor/execdesc.h
@@ -17,6 +17,7 @@
 
 #include "nodes/execnodes.h"
 #include "tcop/dest.h"
+#include "utils/plancache.h"
 
 
 /* ----------------
@@ -35,6 +36,7 @@ typedef struct QueryDesc
 	/* These fields are provided by CreateQueryDesc */
 	CmdType		operation;		/* CMD_SELECT, CMD_UPDATE, etc. */
 	PlannedStmt *plannedstmt;	/* planner's output (could be utility, too) */
+	CachedPlan *cplan;			/* CachedPlan that supplies the plannedstmt */
 	const char *sourceText;		/* source text of the query */
 	Snapshot	snapshot;		/* snapshot to use for query */
 	Snapshot	crosscheck_snapshot;	/* crosscheck for RI update/delete */
@@ -57,6 +59,7 @@ typedef struct QueryDesc
 
 /* in pquery.c */
 extern QueryDesc *CreateQueryDesc(PlannedStmt *plannedstmt,
+								  CachedPlan *cplan,
 								  const char *sourceText,
 								  Snapshot snapshot,
 								  Snapshot crosscheck_snapshot,
diff --git a/src/include/utils/plancache.h b/src/include/utils/plancache.h
index 1baa6d50bfd..b9d00991140 100644
--- a/src/include/utils/plancache.h
+++ b/src/include/utils/plancache.h
@@ -38,6 +38,13 @@ typedef enum
 /* GUC parameter */
 extern PGDLLIMPORT int plan_cache_mode;
 
+typedef enum
+{
+	PLAN_CACHE_STATUS_UNKNOWN = 0,
+	PLAN_CACHE_STATUS_GENERIC_PLAN,
+	PLAN_CACHE_STATUS_CUSTOM_PLAN,
+}			PlanCacheStatus;
+
 /* Optional callback to editorialize on rewritten parse trees */
 typedef void (*PostRewriteHook) (List *querytree_list, void *arg);
 
@@ -163,6 +170,7 @@ typedef struct CachedPlan
 	bool		is_oneshot;		/* is it a "oneshot" plan? */
 	bool		is_saved;		/* is CachedPlan in a long-lived context? */
 	bool		is_valid;		/* is the stmt_list currently valid? */
+	PlanCacheStatus status;		/* status of the cached plan */
 	Oid			planRoleId;		/* Role ID the plan was created for */
 	bool		dependsOnRole;	/* is plan specific to that role? */
 	TransactionId saved_xmin;	/* if valid, replan when TransactionXmin
-- 
2.43.0

#13Sami Imseih
samimseih@gmail.com
In reply to: Sami Imseih (#12)
Re: track generic and custom plans in pg_stat_statements

It turns out that 1722d5eb05d8e reverted 525392d5727f, which
made CachedPlan available in QueryDesc and thus
available to pgss_ExecutorEnd.

I also forgot to mention, the revert also removes the generic plan
is_reused logic:

```
bool is_reused; /* is it a reused generic plan? */
```
which we had to account for up until v5. So this simplifies the
tracking a bit more as the only states to track are "generic plan"
or "custom plan" only.

--
Sami Imseih
Amazon Web Services (AWS)

#14Nikolay Samokhvalov
nik@postgres.ai
In reply to: Sami Imseih (#12)
Re: track generic and custom plans in pg_stat_statements

On Thu, May 29, 2025 at 11:56 AM Sami Imseih <samimseih@gmail.com> wrote:

It turns out that 1722d5eb05d8e reverted 525392d5727f, which
made CachedPlan available in QueryDesc and thus
available to pgss_ExecutorEnd.

So now we have to make CachedPlan available to QueryDesc as
part of this change. The reason the patch was reverted is related
to a memory leak [0] in the BuildCachedPlan code and is not related
to the part that made CachedPlan available to QueryDesc.

See v6 for the rebase of the patch and addition of testing for EXPLAIN
and EXPLAIN ANALYZE which was missing from v5.

I reviewed v6:

- applies to master cleanly, builds, tests pass and all works as expected
- overall, the patch looks great and I found no major issues
- tests and docs look good overall
- in docs, one minor comment:

"Total number of statements executed using a generic plan" vs. what

we already have for `calls`
here, in "Number of times the statement was executed", I see some
inconsistencies:
- the word "total" should be removed, I think
- and maybe we should make wording consistent with the existing
text – "number of times the statement ...")
- Also very minor, the test queries have duplicate `calls` columns:

SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, calls,

...
- plan->status is set in GetCachedPlan() but I don't see explicit
initialization in plan creation paths; maybe it's worth having a defensive
initialization for possible edge cases:

plan->status = PLAN_CACHE_STATUS_UNKNOWN;

(here I'm not 100% sure, as palloc0 in CreateCachedPlan should
zero-initialize it to PLAN_CACHE_STATUS_UNKNOWN anyway)

that's all I could find – and overall it's a great addition,

thank you, looking forward to having these two columns in prod.

Nik

#15Nikolay Samokhvalov
nik@postgres.ai
In reply to: Nikolay Samokhvalov (#14)
Re: track generic and custom plans in pg_stat_statements

On Sat, May 31, 2025 at 5:06 PM Nikolay Samokhvalov <nik@postgres.ai> wrote:

On Thu, May 29, 2025 at 11:56 AM Sami Imseih <samimseih@gmail.com> wrote:

It turns out that 1722d5eb05d8e reverted 525392d5727f, which
made CachedPlan available in QueryDesc and thus
available to pgss_ExecutorEnd.

So now we have to make CachedPlan available to QueryDesc as
part of this change. The reason the patch was reverted is related
to a memory leak [0] in the BuildCachedPlan code and is not related
to the part that made CachedPlan available to QueryDesc.

See v6 for the rebase of the patch and addition of testing for EXPLAIN
and EXPLAIN ANALYZE which was missing from v5.

I reviewed v6:

- applies to master cleanly, builds, tests pass and all works as expected
- overall, the patch looks great and I found no major issues
- tests and docs look good overall
- in docs, one minor comment:

"Total number of statements executed using a generic plan" vs. what

we already have for `calls`
here, in "Number of times the statement was executed", I see some
inconsistencies:
- the word "total" should be removed, I think
- and maybe we should make wording consistent with the existing
text – "number of times the statement ...")
- Also very minor, the test queries have duplicate `calls` columns:

SELECT calls, generic_plan_calls, custom_plan_calls, toplevel,

calls, ...
- plan->status is set in GetCachedPlan() but I don't see explicit
initialization in plan creation paths; maybe it's worth having a defensive
initialization for possible edge cases:

plan->status = PLAN_CACHE_STATUS_UNKNOWN;

(here I'm not 100% sure, as palloc0 in CreateCachedPlan should
zero-initialize it to PLAN_CACHE_STATUS_UNKNOWN anyway)

that's all I could find – and overall it's a great addition,

thank you, looking forward to having these two columns in prod.

Ah, one more thing: the subject here and in CommitFest entry, "track
generic and custom plans" slightly confused me at first, I think it's worth
including words "calls" or "execution" there, and in the commit message,
for clarity. Or just including the both column names as is.

Nik

#16Sami Imseih
samimseih@gmail.com
In reply to: Nikolay Samokhvalov (#14)
1 attachment(s)
Re: track generic and custom plans in pg_stat_statements

I reviewed v6:

- applies to master cleanly, builds, tests pass and all works as expected
- overall, the patch looks great and I found no major issues
- tests and docs look good overall

Thanks for the valuable feedback!

- in docs, one minor comment:

"Total number of statements executed using a generic plan" vs. what we already have for `calls`

here, in "Number of times the statement was executed", I see some inconsistencies:
- the word "total" should be removed, I think
- and maybe we should make wording consistent with the existing text – "number of times the statement ...")

good point. Changed to:

+       Number of times the statement was executed using a generic plan
+       Number of times the statement was executed using a custom plan

- Also very minor, the test queries have duplicate `calls` columns:

good catch. fixed.

plan->status = PLAN_CACHE_STATUS_UNKNOWN;

(here I'm not 100% sure, as palloc0 in CreateCachedPlan should zero-initialize it to PLAN_CACHE_STATUS_UNKNOWN anyway)

Yeah, good catch also. The initialization actually occurs in
GetCachedPlan which returns
a CachedPlan for the CachedPlanSource, so I fixed this by initializing
the status
there.

Ah, one more thing: the subject here and in CommitFest entry, "track generic and custom plans"
slightly confused me at first, I think it's worth including words "calls" or "execution" there, and in
the commit message, for clarity. Or just including the both column names as is.

changed the subject of the CF entry to be more clear.

attached v7 which addresses the comments.

Now, one thing I don't like is the fact that the columns stats_since
and minmax_stats_since
are in between counters all of the sudden. I think we either need to
move them to
the front of the view, after the query field or within this patch
move them after the
new generic/custom plan counters. I prefer the former since we do it
once in a major version
and do not have to worry about it once new counters are added.

It just feels odd that they sit in between the counters as they have a
high level purpose.

Thanks!

Sami Imseih
Amazon Web Services (AWS)

Attachments:

v7-0001-Add-plan_cache-counters-to-pg_stat_statements.patchapplication/octet-stream; name=v7-0001-Add-plan_cache-counters-to-pg_stat_statements.patchDownload
From 1c07532e7dac78659a5a4b6131e2dd38ec61d0fb Mon Sep 17 00:00:00 2001
From: Sami Imseih <simseih@amazon.com>
Date: Mon, 2 Jun 2025 10:27:41 -0500
Subject: [PATCH v7 1/1] Add plan_cache counters to pg_stat_statements.

This adds the ability for users to track how many
times a prepared statement was executed using either
a generic plan or a custom plan, by introducing two new
counters in pg_stat_statements: generic_plan_calls and
custom_plan_calls.

Discussion: https://www.postgresql.org/message-id/flat/CAA5RZ0uFw8Y9GCFvafhC=OA8NnMqVZyzXPfv_EePOt+iv1T-qQ@mail.gmail.com
Reviewers: Greg Sabino Mullane, Ilia Evdokimov, Nikolay Samokhvalov
---
 contrib/pg_stat_statements/Makefile           |   3 +-
 .../pg_stat_statements/expected/plancache.out | 251 ++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |   2 +
 .../pg_stat_statements--1.12--1.13.sql        |  78 ++++++
 .../pg_stat_statements/pg_stat_statements.c   |  54 +++-
 .../pg_stat_statements.control                |   2 +-
 contrib/pg_stat_statements/sql/plancache.sql  | 111 ++++++++
 doc/src/sgml/pgstatstatements.sgml            |  18 ++
 src/backend/commands/copyto.c                 |   2 +-
 src/backend/commands/createas.c               |   2 +-
 src/backend/commands/explain.c                |   6 +-
 src/backend/commands/extension.c              |   1 +
 src/backend/commands/matview.c                |   2 +-
 src/backend/commands/prepare.c                |   2 +-
 src/backend/executor/execParallel.c           |   1 +
 src/backend/executor/functions.c              |   1 +
 src/backend/executor/spi.c                    |   1 +
 src/backend/tcop/pquery.c                     |  11 +-
 src/backend/utils/cache/plancache.c           |   3 +
 src/include/commands/explain.h                |  10 +-
 src/include/executor/execdesc.h               |   3 +
 src/include/utils/plancache.h                 |   8 +
 22 files changed, 552 insertions(+), 20 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/plancache.out
 create mode 100644 contrib/pg_stat_statements/pg_stat_statements--1.12--1.13.sql
 create mode 100644 contrib/pg_stat_statements/sql/plancache.sql

diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index b2bd8794d2a1..996a5fac4487 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -7,6 +7,7 @@ OBJS = \
 
 EXTENSION = pg_stat_statements
 DATA = pg_stat_statements--1.4.sql \
+	pg_stat_statements--1.12--1.13.sql \
 	pg_stat_statements--1.11--1.12.sql pg_stat_statements--1.10--1.11.sql \
 	pg_stat_statements--1.9--1.10.sql pg_stat_statements--1.8--1.9.sql \
 	pg_stat_statements--1.7--1.8.sql pg_stat_statements--1.6--1.7.sql \
@@ -20,7 +21,7 @@ LDFLAGS_SL += $(filter -lm, $(LIBS))
 REGRESS_OPTS = --temp-config $(top_srcdir)/contrib/pg_stat_statements/pg_stat_statements.conf
 REGRESS = select dml cursors utility level_tracking planning \
 	user_activity wal entry_timestamp privileges extended \
-	parallel cleanup oldextversions squashing
+	parallel cleanup oldextversions squashing plancache
 # Disabled because these tests require "shared_preload_libraries=pg_stat_statements",
 # which typical installcheck users do not have (e.g. buildfarm clients).
 NO_INSTALLCHECK = 1
diff --git a/contrib/pg_stat_statements/expected/plancache.out b/contrib/pg_stat_statements/expected/plancache.out
new file mode 100644
index 000000000000..46025e0aa2a1
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/plancache.out
@@ -0,0 +1,251 @@
+--
+-- Information related to plan cache
+--
+--
+-- Setup
+--
+CREATE OR REPLACE FUNCTION select_one_func(int) RETURNS VOID AS $$
+DECLARE
+    ret INT;
+BEGIN
+    SELECT $1 INTO ret;
+END;
+$$ LANGUAGE plpgsql;
+CREATE OR REPLACE PROCEDURE select_one_proc(int) AS $$
+DECLARE
+    ret INT;
+BEGIN
+    select $1 INTO ret;
+END;
+$$ LANGUAGE plpgsql;
+--
+-- plan cache counters for prepared statements
+--
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+PREPARE p1 AS SELECT $1;
+-- plan cache auto
+SET plan_cache_mode TO auto;
+EXECUTE p1(1);
+ ?column? 
+----------
+ 1
+(1 row)
+
+-- force generic plan
+SET plan_cache_mode TO force_generic_plan;
+EXECUTE p1(1);
+ ?column? 
+----------
+ 1
+(1 row)
+
+-- force custom plan
+SET plan_cache_mode TO force_custom_plan;
+EXECUTE p1(1);
+ ?column? 
+----------
+ 1
+(1 row)
+
+SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, query FROM pg_stat_statements
+    ORDER BY query COLLATE "C";
+ calls | generic_plan_calls | custom_plan_calls | toplevel |                       query                        
+-------+--------------------+-------------------+----------+----------------------------------------------------
+     3 |                  1 |                 2 | t        | SELECT $1
+     1 |                  0 |                 0 | t        | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+     3 |                  0 |                 0 | t        | SET plan_cache_mode TO $1
+(3 rows)
+
+DEALLOCATE p1;
+--
+-- plan cache counters for explain|explain (analyze) with prepared statements
+--
+SET pg_stat_statements.track = 'all';
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+PREPARE p1 AS SELECT $1;
+-- plan cache auto
+SET plan_cache_mode TO auto;
+EXPLAIN (COSTS OFF) EXECUTE p1(1);
+ QUERY PLAN 
+------------
+ Result
+(1 row)
+
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) EXECUTE p1(1);
+            QUERY PLAN             
+-----------------------------------
+ Result (actual rows=1.00 loops=1)
+(1 row)
+
+-- force generic plan
+SET plan_cache_mode TO force_generic_plan;
+EXPLAIN (COSTS OFF) EXECUTE p1(1);
+ QUERY PLAN 
+------------
+ Result
+(1 row)
+
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) EXECUTE p1(1);
+            QUERY PLAN             
+-----------------------------------
+ Result (actual rows=1.00 loops=1)
+(1 row)
+
+-- force custom plan
+SET plan_cache_mode TO force_custom_plan;
+EXPLAIN (COSTS OFF) EXECUTE p1(1);
+ QUERY PLAN 
+------------
+ Result
+(1 row)
+
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) EXECUTE p1(1);
+            QUERY PLAN             
+-----------------------------------
+ Result (actual rows=1.00 loops=1)
+(1 row)
+
+SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, query FROM pg_stat_statements
+    ORDER BY query COLLATE "C";
+ calls | generic_plan_calls | custom_plan_calls | toplevel |                                      query                                       
+-------+--------------------+-------------------+----------+----------------------------------------------------------------------------------
+     3 |                  0 |                 0 | t        | EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) EXECUTE p1(1)
+     3 |                  0 |                 0 | t        | EXPLAIN (COSTS OFF) EXECUTE p1(1)
+     6 |                  2 |                 4 | f        | SELECT $1
+     1 |                  0 |                 0 | t        | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+     3 |                  0 |                 0 | t        | SET plan_cache_mode TO $1
+(5 rows)
+
+RESET pg_stat_statements.track;
+DEALLOCATE p1;
+--
+-- plan cache counters for functions and procedures
+--
+SET pg_stat_statements.track = 'all';
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- plan cache auto
+SET plan_cache_mode TO auto;
+SELECT select_one_func(1);
+ select_one_func 
+-----------------
+ 
+(1 row)
+
+CALL select_one_proc(1);
+-- force generic plan
+SET plan_cache_mode TO force_generic_plan;
+SELECT select_one_func(1);
+ select_one_func 
+-----------------
+ 
+(1 row)
+
+CALL select_one_proc(1);
+-- force custom plan
+SET plan_cache_mode TO force_custom_plan;
+SELECT select_one_func(1);
+ select_one_func 
+-----------------
+ 
+(1 row)
+
+CALL select_one_proc(1);
+SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, query FROM pg_stat_statements
+    ORDER BY query COLLATE "C";
+ calls | generic_plan_calls | custom_plan_calls | toplevel |                       query                        
+-------+--------------------+-------------------+----------+----------------------------------------------------
+     3 |                  0 |                 0 | t        | CALL select_one_proc($1)
+     6 |                  2 |                 4 | f        | SELECT $1
+     1 |                  0 |                 0 | t        | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+     3 |                  0 |                 0 | t        | SELECT select_one_func($1)
+     3 |                  0 |                 0 | t        | SET plan_cache_mode TO $1
+(5 rows)
+
+--
+-- plan cache counters for explain|explain (analyze) with functions and procedures
+--
+SET pg_stat_statements.track = 'all';
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- plan cache auto
+SET plan_cache_mode TO auto;
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) SELECT select_one_func(1);
+            QUERY PLAN             
+-----------------------------------
+ Result (actual rows=1.00 loops=1)
+(1 row)
+
+EXPLAIN (COSTS OFF) SELECT select_one_func(1);
+ QUERY PLAN 
+------------
+ Result
+(1 row)
+
+CALL select_one_proc(1);
+-- force generic plan
+SET plan_cache_mode TO force_generic_plan;
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) SELECT select_one_func(1);
+            QUERY PLAN             
+-----------------------------------
+ Result (actual rows=1.00 loops=1)
+(1 row)
+
+EXPLAIN (COSTS OFF) SELECT select_one_func(1);
+ QUERY PLAN 
+------------
+ Result
+(1 row)
+
+CALL select_one_proc(1);
+-- force custom plan
+SET plan_cache_mode TO force_custom_plan;
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) SELECT select_one_func(1);
+            QUERY PLAN             
+-----------------------------------
+ Result (actual rows=1.00 loops=1)
+(1 row)
+
+EXPLAIN (COSTS OFF) SELECT select_one_func(1);
+ QUERY PLAN 
+------------
+ Result
+(1 row)
+
+CALL select_one_proc(1);
+SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, query FROM pg_stat_statements
+    ORDER BY query COLLATE "C";
+ calls | generic_plan_calls | custom_plan_calls | toplevel |                                             query                                             
+-------+--------------------+-------------------+----------+-----------------------------------------------------------------------------------------------
+     3 |                  0 |                 0 | t        | CALL select_one_proc($1)
+     3 |                  0 |                 0 | t        | EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) SELECT select_one_func($1)
+     3 |                  0 |                 0 | t        | EXPLAIN (COSTS OFF) SELECT select_one_func($1)
+     6 |                  2 |                 4 | f        | SELECT $1
+     1 |                  0 |                 0 | t        | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+     6 |                  0 |                 0 | f        | SELECT select_one_func($1)
+     3 |                  0 |                 0 | t        | SET plan_cache_mode TO $1
+(7 rows)
+
+--
+-- Cleanup
+--
+DROP FUNCTION select_one_func(int);
+DROP PROCEDURE select_one_proc(int);
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 01a6cbdcf613..110fb82fe120 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -21,6 +21,7 @@ contrib_targets += pg_stat_statements
 install_data(
   'pg_stat_statements.control',
   'pg_stat_statements--1.4.sql',
+  'pg_stat_statements--1.12--1.13.sql',
   'pg_stat_statements--1.11--1.12.sql',
   'pg_stat_statements--1.10--1.11.sql',
   'pg_stat_statements--1.9--1.10.sql',
@@ -57,6 +58,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'plancache',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/pg_stat_statements--1.12--1.13.sql b/contrib/pg_stat_statements/pg_stat_statements--1.12--1.13.sql
new file mode 100644
index 000000000000..67f2f7dbca35
--- /dev/null
+++ b/contrib/pg_stat_statements/pg_stat_statements--1.12--1.13.sql
@@ -0,0 +1,78 @@
+/* contrib/pg_stat_statements/pg_stat_statements--1.12--1.13.sql */
+
+-- complain if script is sourced in psql, rather than via ALTER EXTENSION
+\echo Use "ALTER EXTENSION pg_stat_statements UPDATE TO '1.13'" to load this file. \quit
+
+/* First we have to remove them from the extension */
+ALTER EXTENSION pg_stat_statements DROP VIEW pg_stat_statements;
+ALTER EXTENSION pg_stat_statements DROP FUNCTION pg_stat_statements(boolean);
+
+/* Then we can drop them */
+DROP VIEW pg_stat_statements;
+DROP FUNCTION pg_stat_statements(boolean);
+
+/* Now redefine */
+CREATE FUNCTION pg_stat_statements(IN showtext boolean,
+    OUT userid oid,
+    OUT dbid oid,
+    OUT toplevel bool,
+    OUT queryid bigint,
+    OUT query text,
+    OUT plans int8,
+    OUT total_plan_time float8,
+    OUT min_plan_time float8,
+    OUT max_plan_time float8,
+    OUT mean_plan_time float8,
+    OUT stddev_plan_time float8,
+    OUT calls int8,
+    OUT total_exec_time float8,
+    OUT min_exec_time float8,
+    OUT max_exec_time float8,
+    OUT mean_exec_time float8,
+    OUT stddev_exec_time float8,
+    OUT rows int8,
+    OUT shared_blks_hit int8,
+    OUT shared_blks_read int8,
+    OUT shared_blks_dirtied int8,
+    OUT shared_blks_written int8,
+    OUT local_blks_hit int8,
+    OUT local_blks_read int8,
+    OUT local_blks_dirtied int8,
+    OUT local_blks_written int8,
+    OUT temp_blks_read int8,
+    OUT temp_blks_written int8,
+    OUT shared_blk_read_time float8,
+    OUT shared_blk_write_time float8,
+    OUT local_blk_read_time float8,
+    OUT local_blk_write_time float8,
+    OUT temp_blk_read_time float8,
+    OUT temp_blk_write_time float8,
+    OUT wal_records int8,
+    OUT wal_fpi int8,
+    OUT wal_bytes numeric,
+    OUT wal_buffers_full int8,
+    OUT jit_functions int8,
+    OUT jit_generation_time float8,
+    OUT jit_inlining_count int8,
+    OUT jit_inlining_time float8,
+    OUT jit_optimization_count int8,
+    OUT jit_optimization_time float8,
+    OUT jit_emission_count int8,
+    OUT jit_emission_time float8,
+    OUT jit_deform_count int8,
+    OUT jit_deform_time float8,
+    OUT parallel_workers_to_launch int8,
+    OUT parallel_workers_launched int8,
+    OUT stats_since timestamp with time zone,
+    OUT minmax_stats_since timestamp with time zone,
+    OUT generic_plan_calls int8,
+    OUT custom_plan_calls int8
+)
+RETURNS SETOF record
+AS 'MODULE_PATHNAME', 'pg_stat_statements_1_13'
+LANGUAGE C STRICT VOLATILE PARALLEL SAFE;
+
+CREATE VIEW pg_stat_statements AS
+  SELECT * FROM pg_stat_statements(true);
+
+GRANT SELECT ON pg_stat_statements TO PUBLIC;
diff --git a/contrib/pg_stat_statements/pg_stat_statements.c b/contrib/pg_stat_statements/pg_stat_statements.c
index 129001c70c81..eb3bae591ea4 100644
--- a/contrib/pg_stat_statements/pg_stat_statements.c
+++ b/contrib/pg_stat_statements/pg_stat_statements.c
@@ -69,6 +69,7 @@
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/memutils.h"
+#include "utils/plancache.h"
 #include "utils/timestamp.h"
 
 PG_MODULE_MAGIC_EXT(
@@ -114,6 +115,7 @@ typedef enum pgssVersion
 	PGSS_V1_10,
 	PGSS_V1_11,
 	PGSS_V1_12,
+	PGSS_V1_13,
 } pgssVersion;
 
 typedef enum pgssStoreKind
@@ -210,6 +212,8 @@ typedef struct Counters
 											 * to be launched */
 	int64		parallel_workers_launched;	/* # of parallel workers actually
 											 * launched */
+	int64		generic_plan_calls; /* number of calls using a generic plan */
+	int64		custom_plan_calls;	/* number of calls using a custom plan */
 } Counters;
 
 /*
@@ -323,6 +327,7 @@ PG_FUNCTION_INFO_V1(pg_stat_statements_1_9);
 PG_FUNCTION_INFO_V1(pg_stat_statements_1_10);
 PG_FUNCTION_INFO_V1(pg_stat_statements_1_11);
 PG_FUNCTION_INFO_V1(pg_stat_statements_1_12);
+PG_FUNCTION_INFO_V1(pg_stat_statements_1_13);
 PG_FUNCTION_INFO_V1(pg_stat_statements);
 PG_FUNCTION_INFO_V1(pg_stat_statements_info);
 
@@ -355,7 +360,8 @@ static void pgss_store(const char *query, int64 queryId,
 					   const struct JitInstrumentation *jitusage,
 					   JumbleState *jstate,
 					   int parallel_workers_to_launch,
-					   int parallel_workers_launched);
+					   int parallel_workers_launched,
+					   CachedPlan *cplan);
 static void pg_stat_statements_internal(FunctionCallInfo fcinfo,
 										pgssVersion api_version,
 										bool showtext);
@@ -877,7 +883,8 @@ pgss_post_parse_analyze(ParseState *pstate, Query *query, JumbleState *jstate)
 				   NULL,
 				   jstate,
 				   0,
-				   0);
+				   0,
+				   NULL);
 }
 
 /*
@@ -957,7 +964,8 @@ pgss_planner(Query *parse,
 				   NULL,
 				   NULL,
 				   0,
-				   0);
+				   0,
+				   NULL);
 	}
 	else
 	{
@@ -1091,7 +1099,8 @@ pgss_ExecutorEnd(QueryDesc *queryDesc)
 				   queryDesc->estate->es_jit ? &queryDesc->estate->es_jit->instr : NULL,
 				   NULL,
 				   queryDesc->estate->es_parallel_workers_to_launch,
-				   queryDesc->estate->es_parallel_workers_launched);
+				   queryDesc->estate->es_parallel_workers_launched,
+				   queryDesc->cplan);
 	}
 
 	if (prev_ExecutorEnd)
@@ -1224,7 +1233,8 @@ pgss_ProcessUtility(PlannedStmt *pstmt, const char *queryString,
 				   NULL,
 				   NULL,
 				   0,
-				   0);
+				   0,
+				   NULL);
 	}
 	else
 	{
@@ -1287,7 +1297,8 @@ pgss_store(const char *query, int64 queryId,
 		   const struct JitInstrumentation *jitusage,
 		   JumbleState *jstate,
 		   int parallel_workers_to_launch,
-		   int parallel_workers_launched)
+		   int parallel_workers_launched,
+		   CachedPlan *cplan)
 {
 	pgssHashKey key;
 	pgssEntry  *entry;
@@ -1495,6 +1506,14 @@ pgss_store(const char *query, int64 queryId,
 		entry->counters.parallel_workers_to_launch += parallel_workers_to_launch;
 		entry->counters.parallel_workers_launched += parallel_workers_launched;
 
+		if (cplan)
+		{
+			if (cplan->status == PLAN_CACHE_STATUS_GENERIC_PLAN)
+				entry->counters.generic_plan_calls++;
+			if (cplan->status == PLAN_CACHE_STATUS_CUSTOM_PLAN)
+				entry->counters.custom_plan_calls++;
+		}
+
 		SpinLockRelease(&entry->mutex);
 	}
 
@@ -1562,7 +1581,8 @@ pg_stat_statements_reset(PG_FUNCTION_ARGS)
 #define PG_STAT_STATEMENTS_COLS_V1_10	43
 #define PG_STAT_STATEMENTS_COLS_V1_11	49
 #define PG_STAT_STATEMENTS_COLS_V1_12	52
-#define PG_STAT_STATEMENTS_COLS			52	/* maximum of above */
+#define PG_STAT_STATEMENTS_COLS_V1_13	54
+#define PG_STAT_STATEMENTS_COLS			54	/* maximum of above */
 
 /*
  * Retrieve statement statistics.
@@ -1574,6 +1594,16 @@ pg_stat_statements_reset(PG_FUNCTION_ARGS)
  * expected API version is identified by embedding it in the C name of the
  * function.  Unfortunately we weren't bright enough to do that for 1.1.
  */
+Datum
+pg_stat_statements_1_13(PG_FUNCTION_ARGS)
+{
+	bool		showtext = PG_GETARG_BOOL(0);
+
+	pg_stat_statements_internal(fcinfo, PGSS_V1_13, showtext);
+
+	return (Datum) 0;
+}
+
 Datum
 pg_stat_statements_1_12(PG_FUNCTION_ARGS)
 {
@@ -1732,6 +1762,10 @@ pg_stat_statements_internal(FunctionCallInfo fcinfo,
 			if (api_version != PGSS_V1_12)
 				elog(ERROR, "incorrect number of output arguments");
 			break;
+		case PG_STAT_STATEMENTS_COLS_V1_13:
+			if (api_version != PGSS_V1_13)
+				elog(ERROR, "incorrect number of output arguments");
+			break;
 		default:
 			elog(ERROR, "incorrect number of output arguments");
 	}
@@ -1989,6 +2023,11 @@ pg_stat_statements_internal(FunctionCallInfo fcinfo,
 			values[i++] = TimestampTzGetDatum(stats_since);
 			values[i++] = TimestampTzGetDatum(minmax_stats_since);
 		}
+		if (api_version >= PGSS_V1_13)
+		{
+			values[i++] = Int64GetDatumFast(tmp.generic_plan_calls);
+			values[i++] = Int64GetDatumFast(tmp.custom_plan_calls);
+		}
 
 		Assert(i == (api_version == PGSS_V1_0 ? PG_STAT_STATEMENTS_COLS_V1_0 :
 					 api_version == PGSS_V1_1 ? PG_STAT_STATEMENTS_COLS_V1_1 :
@@ -1999,6 +2038,7 @@ pg_stat_statements_internal(FunctionCallInfo fcinfo,
 					 api_version == PGSS_V1_10 ? PG_STAT_STATEMENTS_COLS_V1_10 :
 					 api_version == PGSS_V1_11 ? PG_STAT_STATEMENTS_COLS_V1_11 :
 					 api_version == PGSS_V1_12 ? PG_STAT_STATEMENTS_COLS_V1_12 :
+					 api_version == PGSS_V1_13 ? PG_STAT_STATEMENTS_COLS_V1_13 :
 					 -1 /* fail if you forget to update this assert */ ));
 
 		tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, values, nulls);
diff --git a/contrib/pg_stat_statements/pg_stat_statements.control b/contrib/pg_stat_statements/pg_stat_statements.control
index d45ebc12e360..2eee0ceffa89 100644
--- a/contrib/pg_stat_statements/pg_stat_statements.control
+++ b/contrib/pg_stat_statements/pg_stat_statements.control
@@ -1,5 +1,5 @@
 # pg_stat_statements extension
 comment = 'track planning and execution statistics of all SQL statements executed'
-default_version = '1.12'
+default_version = '1.13'
 module_pathname = '$libdir/pg_stat_statements'
 relocatable = true
diff --git a/contrib/pg_stat_statements/sql/plancache.sql b/contrib/pg_stat_statements/sql/plancache.sql
new file mode 100644
index 000000000000..97d84bfdbede
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/plancache.sql
@@ -0,0 +1,111 @@
+--
+-- Information related to plan cache
+--
+
+--
+-- Setup
+--
+CREATE OR REPLACE FUNCTION select_one_func(int) RETURNS VOID AS $$
+DECLARE
+    ret INT;
+BEGIN
+    SELECT $1 INTO ret;
+END;
+$$ LANGUAGE plpgsql;
+CREATE OR REPLACE PROCEDURE select_one_proc(int) AS $$
+DECLARE
+    ret INT;
+BEGIN
+    select $1 INTO ret;
+END;
+$$ LANGUAGE plpgsql;
+
+--
+-- plan cache counters for prepared statements
+--
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+PREPARE p1 AS SELECT $1;
+-- plan cache auto
+SET plan_cache_mode TO auto;
+EXECUTE p1(1);
+-- force generic plan
+SET plan_cache_mode TO force_generic_plan;
+EXECUTE p1(1);
+-- force custom plan
+SET plan_cache_mode TO force_custom_plan;
+EXECUTE p1(1);
+SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, query FROM pg_stat_statements
+    ORDER BY query COLLATE "C";
+DEALLOCATE p1;
+
+--
+-- plan cache counters for explain|explain (analyze) with prepared statements
+--
+SET pg_stat_statements.track = 'all';
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+PREPARE p1 AS SELECT $1;
+-- plan cache auto
+SET plan_cache_mode TO auto;
+EXPLAIN (COSTS OFF) EXECUTE p1(1);
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) EXECUTE p1(1);
+-- force generic plan
+SET plan_cache_mode TO force_generic_plan;
+EXPLAIN (COSTS OFF) EXECUTE p1(1);
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) EXECUTE p1(1);
+-- force custom plan
+SET plan_cache_mode TO force_custom_plan;
+EXPLAIN (COSTS OFF) EXECUTE p1(1);
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) EXECUTE p1(1);
+SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, query FROM pg_stat_statements
+    ORDER BY query COLLATE "C";
+RESET pg_stat_statements.track;
+DEALLOCATE p1;
+
+--
+-- plan cache counters for functions and procedures
+--
+SET pg_stat_statements.track = 'all';
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+-- plan cache auto
+SET plan_cache_mode TO auto;
+SELECT select_one_func(1);
+CALL select_one_proc(1);
+-- force generic plan
+SET plan_cache_mode TO force_generic_plan;
+SELECT select_one_func(1);
+CALL select_one_proc(1);
+-- force custom plan
+SET plan_cache_mode TO force_custom_plan;
+SELECT select_one_func(1);
+CALL select_one_proc(1);
+SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, query FROM pg_stat_statements
+    ORDER BY query COLLATE "C";
+
+--
+-- plan cache counters for explain|explain (analyze) with functions and procedures
+--
+SET pg_stat_statements.track = 'all';
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+-- plan cache auto
+SET plan_cache_mode TO auto;
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) SELECT select_one_func(1);
+EXPLAIN (COSTS OFF) SELECT select_one_func(1);
+CALL select_one_proc(1);
+-- force generic plan
+SET plan_cache_mode TO force_generic_plan;
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) SELECT select_one_func(1);
+EXPLAIN (COSTS OFF) SELECT select_one_func(1);
+CALL select_one_proc(1);
+-- force custom plan
+SET plan_cache_mode TO force_custom_plan;
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) SELECT select_one_func(1);
+EXPLAIN (COSTS OFF) SELECT select_one_func(1);
+CALL select_one_proc(1);
+SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, query FROM pg_stat_statements
+    ORDER BY query COLLATE "C";
+
+--
+-- Cleanup
+--
+DROP FUNCTION select_one_func(int);
+DROP PROCEDURE select_one_proc(int);
diff --git a/doc/src/sgml/pgstatstatements.sgml b/doc/src/sgml/pgstatstatements.sgml
index 7baa07dcdbf7..0bce696aac50 100644
--- a/doc/src/sgml/pgstatstatements.sgml
+++ b/doc/src/sgml/pgstatstatements.sgml
@@ -575,6 +575,24 @@
        <structfield>max_exec_time</structfield>)
       </para></entry>
      </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>generic_plan_calls</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Number of times the statement was executed using a generic plan
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>custom_plan_calls</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Number of times the statement was executed using a custom plan
+      </para></entry>
+     </row>
     </tbody>
    </tgroup>
   </table>
diff --git a/src/backend/commands/copyto.c b/src/backend/commands/copyto.c
index ea6f18f2c800..55a2719d7685 100644
--- a/src/backend/commands/copyto.c
+++ b/src/backend/commands/copyto.c
@@ -835,7 +835,7 @@ BeginCopyTo(ParseState *pstate,
 		((DR_copy *) dest)->cstate = cstate;
 
 		/* Create a QueryDesc requesting no output */
-		cstate->queryDesc = CreateQueryDesc(plan, pstate->p_sourcetext,
+		cstate->queryDesc = CreateQueryDesc(plan, NULL, pstate->p_sourcetext,
 											GetActiveSnapshot(),
 											InvalidSnapshot,
 											dest, NULL, NULL, 0);
diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c
index dfd2ab8e8628..18c8a11b70c4 100644
--- a/src/backend/commands/createas.c
+++ b/src/backend/commands/createas.c
@@ -334,7 +334,7 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt,
 		UpdateActiveSnapshotCommandId();
 
 		/* Create a QueryDesc, redirecting output to our tuple receiver */
-		queryDesc = CreateQueryDesc(plan, pstate->p_sourcetext,
+		queryDesc = CreateQueryDesc(plan, NULL, pstate->p_sourcetext,
 									GetActiveSnapshot(), InvalidSnapshot,
 									dest, params, queryEnv, 0);
 
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 7e2792ead715..b2b998bc2db8 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -369,7 +369,7 @@ standard_ExplainOneQuery(Query *query, int cursorOptions,
 	}
 
 	/* run it (if needed) and produce output */
-	ExplainOnePlan(plan, into, es, queryString, params, queryEnv,
+	ExplainOnePlan(plan, NULL, into, es, queryString, params, queryEnv,
 				   &planduration, (es->buffers ? &bufusage : NULL),
 				   es->memory ? &mem_counters : NULL);
 }
@@ -491,7 +491,7 @@ ExplainOneUtility(Node *utilityStmt, IntoClause *into, ExplainState *es,
  * to call it.
  */
 void
-ExplainOnePlan(PlannedStmt *plannedstmt, IntoClause *into, ExplainState *es,
+ExplainOnePlan(PlannedStmt *plannedstmt, CachedPlan *cplan, IntoClause *into, ExplainState *es,
 			   const char *queryString, ParamListInfo params,
 			   QueryEnvironment *queryEnv, const instr_time *planduration,
 			   const BufferUsage *bufusage,
@@ -547,7 +547,7 @@ ExplainOnePlan(PlannedStmt *plannedstmt, IntoClause *into, ExplainState *es,
 		dest = None_Receiver;
 
 	/* Create a QueryDesc for the query */
-	queryDesc = CreateQueryDesc(plannedstmt, queryString,
+	queryDesc = CreateQueryDesc(plannedstmt, cplan, queryString,
 								GetActiveSnapshot(), InvalidSnapshot,
 								dest, params, queryEnv, instrument_option);
 
diff --git a/src/backend/commands/extension.c b/src/backend/commands/extension.c
index e6f9ab6dfd66..bc3e7bb808fa 100644
--- a/src/backend/commands/extension.c
+++ b/src/backend/commands/extension.c
@@ -993,6 +993,7 @@ execute_sql_string(const char *sql, const char *filename)
 				QueryDesc  *qdesc;
 
 				qdesc = CreateQueryDesc(stmt,
+										NULL,
 										sql,
 										GetActiveSnapshot(), NULL,
 										dest, NULL, NULL, 0);
diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index 27c2cb26ef5f..c9e7b6f5195e 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -438,7 +438,7 @@ refresh_matview_datafill(DestReceiver *dest, Query *query,
 	UpdateActiveSnapshotCommandId();
 
 	/* Create a QueryDesc, redirecting output to our tuple receiver */
-	queryDesc = CreateQueryDesc(plan, queryString,
+	queryDesc = CreateQueryDesc(plan, NULL, queryString,
 								GetActiveSnapshot(), InvalidSnapshot,
 								dest, NULL, NULL, 0);
 
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a26..a5f3a662e514 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -657,7 +657,7 @@ ExplainExecuteQuery(ExecuteStmt *execstmt, IntoClause *into, ExplainState *es,
 		PlannedStmt *pstmt = lfirst_node(PlannedStmt, p);
 
 		if (pstmt->commandType != CMD_UTILITY)
-			ExplainOnePlan(pstmt, into, es, query_string, paramLI, pstate->p_queryEnv,
+			ExplainOnePlan(pstmt, cplan, into, es, query_string, paramLI, pstate->p_queryEnv,
 						   &planduration, (es->buffers ? &bufusage : NULL),
 						   es->memory ? &mem_counters : NULL);
 		else
diff --git a/src/backend/executor/execParallel.c b/src/backend/executor/execParallel.c
index f3e77bda2790..58b1c09223a8 100644
--- a/src/backend/executor/execParallel.c
+++ b/src/backend/executor/execParallel.c
@@ -1280,6 +1280,7 @@ ExecParallelGetQueryDesc(shm_toc *toc, DestReceiver *receiver,
 
 	/* Create a QueryDesc for the query. */
 	return CreateQueryDesc(pstmt,
+						   NULL,
 						   queryString,
 						   GetActiveSnapshot(), InvalidSnapshot,
 						   receiver, paramLI, NULL, instrument_options);
diff --git a/src/backend/executor/functions.c b/src/backend/executor/functions.c
index 359aafea681b..635ce762eb37 100644
--- a/src/backend/executor/functions.c
+++ b/src/backend/executor/functions.c
@@ -1339,6 +1339,7 @@ postquel_start(execution_state *es, SQLFunctionCachePtr fcache)
 		dest = None_Receiver;
 
 	es->qd = CreateQueryDesc(es->stmt,
+							 NULL,
 							 fcache->func->src,
 							 GetActiveSnapshot(),
 							 InvalidSnapshot,
diff --git a/src/backend/executor/spi.c b/src/backend/executor/spi.c
index ecb2e4ccaa1c..5ffff06ec0ef 100644
--- a/src/backend/executor/spi.c
+++ b/src/backend/executor/spi.c
@@ -2690,6 +2690,7 @@ _SPI_execute_plan(SPIPlanPtr plan, const SPIExecuteOptions *options,
 					snap = InvalidSnapshot;
 
 				qdesc = CreateQueryDesc(stmt,
+										cplan,
 										plansource->query_string,
 										snap, crosscheck_snapshot,
 										dest,
diff --git a/src/backend/tcop/pquery.c b/src/backend/tcop/pquery.c
index d1593f38b35f..dc4f5ce9d5e7 100644
--- a/src/backend/tcop/pquery.c
+++ b/src/backend/tcop/pquery.c
@@ -26,6 +26,7 @@
 #include "tcop/pquery.h"
 #include "tcop/utility.h"
 #include "utils/memutils.h"
+#include "utils/plancache.h"
 #include "utils/snapmgr.h"
 
 
@@ -37,6 +38,7 @@ Portal		ActivePortal = NULL;
 
 
 static void ProcessQuery(PlannedStmt *plan,
+						 CachedPlan *cplan,
 						 const char *sourceText,
 						 ParamListInfo params,
 						 QueryEnvironment *queryEnv,
@@ -66,6 +68,7 @@ static void DoPortalRewind(Portal portal);
  */
 QueryDesc *
 CreateQueryDesc(PlannedStmt *plannedstmt,
+				CachedPlan *cplan,
 				const char *sourceText,
 				Snapshot snapshot,
 				Snapshot crosscheck_snapshot,
@@ -93,6 +96,8 @@ CreateQueryDesc(PlannedStmt *plannedstmt,
 	qd->planstate = NULL;
 	qd->totaltime = NULL;
 
+	qd->cplan = cplan;
+
 	/* not yet executed */
 	qd->already_executed = false;
 
@@ -135,6 +140,7 @@ FreeQueryDesc(QueryDesc *qdesc)
  */
 static void
 ProcessQuery(PlannedStmt *plan,
+			 CachedPlan *cplan,
 			 const char *sourceText,
 			 ParamListInfo params,
 			 QueryEnvironment *queryEnv,
@@ -146,7 +152,7 @@ ProcessQuery(PlannedStmt *plan,
 	/*
 	 * Create the QueryDesc object
 	 */
-	queryDesc = CreateQueryDesc(plan, sourceText,
+	queryDesc = CreateQueryDesc(plan, cplan, sourceText,
 								GetActiveSnapshot(), InvalidSnapshot,
 								dest, params, queryEnv, 0);
 
@@ -494,6 +500,7 @@ PortalStart(Portal portal, ParamListInfo params,
 				 * the destination to DestNone.
 				 */
 				queryDesc = CreateQueryDesc(linitial_node(PlannedStmt, portal->stmts),
+											portal->cplan,
 											portal->sourceText,
 											GetActiveSnapshot(),
 											InvalidSnapshot,
@@ -1270,6 +1277,7 @@ PortalRunMulti(Portal portal,
 			{
 				/* statement can set tag string */
 				ProcessQuery(pstmt,
+							 portal->cplan,
 							 portal->sourceText,
 							 portal->portalParams,
 							 portal->queryEnv,
@@ -1279,6 +1287,7 @@ PortalRunMulti(Portal portal,
 			{
 				/* stmt added by rewrite cannot set tag */
 				ProcessQuery(pstmt,
+							 portal->cplan,
 							 portal->sourceText,
 							 portal->portalParams,
 							 portal->queryEnv,
diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c
index 89a1c79e984d..63df3ecd881e 100644
--- a/src/backend/utils/cache/plancache.c
+++ b/src/backend/utils/cache/plancache.c
@@ -1140,6 +1140,7 @@ BuildCachedPlan(CachedPlanSource *plansource, List *qlist,
 	plan->is_oneshot = plansource->is_oneshot;
 	plan->is_saved = false;
 	plan->is_valid = true;
+	plan->status = PLAN_CACHE_STATUS_UNKNOWN;
 
 	/* assign generation number to new plan */
 	plan->generation = ++(plansource->generation);
@@ -1358,10 +1359,12 @@ GetCachedPlan(CachedPlanSource *plansource, ParamListInfo boundParams,
 		plansource->total_custom_cost += cached_plan_cost(plan, true);
 
 		plansource->num_custom_plans++;
+		plan->status = PLAN_CACHE_STATUS_CUSTOM_PLAN;
 	}
 	else
 	{
 		plansource->num_generic_plans++;
+		plan->status = PLAN_CACHE_STATUS_GENERIC_PLAN;
 	}
 
 	Assert(plan != NULL);
diff --git a/src/include/commands/explain.h b/src/include/commands/explain.h
index 3b122f79ed84..36579590ec2b 100644
--- a/src/include/commands/explain.h
+++ b/src/include/commands/explain.h
@@ -63,9 +63,13 @@ extern void ExplainOneUtility(Node *utilityStmt, IntoClause *into,
 							  struct ExplainState *es, ParseState *pstate,
 							  ParamListInfo params);
 
-extern void ExplainOnePlan(PlannedStmt *plannedstmt, IntoClause *into,
-						   struct ExplainState *es, const char *queryString,
-						   ParamListInfo params, QueryEnvironment *queryEnv,
+extern void ExplainOnePlan(PlannedStmt *plannedstmt,
+						   CachedPlan *cplan,
+						   IntoClause *into,
+						   struct ExplainState *es,
+						   const char *queryString,
+						   ParamListInfo params,
+						   QueryEnvironment *queryEnv,
 						   const instr_time *planduration,
 						   const BufferUsage *bufusage,
 						   const MemoryContextCounters *mem_counters);
diff --git a/src/include/executor/execdesc.h b/src/include/executor/execdesc.h
index 86db3dc8d0de..f7cbe0981a54 100644
--- a/src/include/executor/execdesc.h
+++ b/src/include/executor/execdesc.h
@@ -17,6 +17,7 @@
 
 #include "nodes/execnodes.h"
 #include "tcop/dest.h"
+#include "utils/plancache.h"
 
 
 /* ----------------
@@ -35,6 +36,7 @@ typedef struct QueryDesc
 	/* These fields are provided by CreateQueryDesc */
 	CmdType		operation;		/* CMD_SELECT, CMD_UPDATE, etc. */
 	PlannedStmt *plannedstmt;	/* planner's output (could be utility, too) */
+	CachedPlan *cplan;			/* CachedPlan that supplies the plannedstmt */
 	const char *sourceText;		/* source text of the query */
 	Snapshot	snapshot;		/* snapshot to use for query */
 	Snapshot	crosscheck_snapshot;	/* crosscheck for RI update/delete */
@@ -57,6 +59,7 @@ typedef struct QueryDesc
 
 /* in pquery.c */
 extern QueryDesc *CreateQueryDesc(PlannedStmt *plannedstmt,
+								  CachedPlan *cplan,
 								  const char *sourceText,
 								  Snapshot snapshot,
 								  Snapshot crosscheck_snapshot,
diff --git a/src/include/utils/plancache.h b/src/include/utils/plancache.h
index 1baa6d50bfd7..b9d009911403 100644
--- a/src/include/utils/plancache.h
+++ b/src/include/utils/plancache.h
@@ -38,6 +38,13 @@ typedef enum
 /* GUC parameter */
 extern PGDLLIMPORT int plan_cache_mode;
 
+typedef enum
+{
+	PLAN_CACHE_STATUS_UNKNOWN = 0,
+	PLAN_CACHE_STATUS_GENERIC_PLAN,
+	PLAN_CACHE_STATUS_CUSTOM_PLAN,
+}			PlanCacheStatus;
+
 /* Optional callback to editorialize on rewritten parse trees */
 typedef void (*PostRewriteHook) (List *querytree_list, void *arg);
 
@@ -163,6 +170,7 @@ typedef struct CachedPlan
 	bool		is_oneshot;		/* is it a "oneshot" plan? */
 	bool		is_saved;		/* is CachedPlan in a long-lived context? */
 	bool		is_valid;		/* is the stmt_list currently valid? */
+	PlanCacheStatus status;		/* status of the cached plan */
 	Oid			planRoleId;		/* Role ID the plan was created for */
 	bool		dependsOnRole;	/* is plan specific to that role? */
 	TransactionId saved_xmin;	/* if valid, replan when TransactionXmin
-- 
2.39.5 (Apple Git-154)

#17Sami Imseih
samimseih@gmail.com
In reply to: Sami Imseih (#16)
1 attachment(s)
Re: track generic and custom plans in pg_stat_statements

Now, one thing I don't like is the fact that the columns stats_since
and minmax_stats_since
are in between counters all of the sudden. I think we either need to
move them to
the front of the view, after the query field or within this patch
move them after the
new generic/custom plan counters. I prefer the former since we do it
once in a major version
and do not have to worry about it once new counters are added.

It just feels odd that they sit in between the counters as they have a
high level purpose.

It occurred to me after a bit that we added the parallel_workers_to_launch and
parallel_workers_launched fields after stats_since and minmax_stats_since
were introduced, but the stats related fields were kept at the end. So,
I will do the same with the new counters. I still don't like the stats fields
being at the end, but that could still be taken up after.

See v8 with the field names reorganized.

--
Sami Imseih
Amazon Web Services (AWS)

Attachments:

v8-0001-Add-plan_cache-counters-to-pg_stat_statements.patchapplication/octet-stream; name=v8-0001-Add-plan_cache-counters-to-pg_stat_statements.patchDownload
From bbdfc0cc0c7b076ffcd49f80eb0e6df1498ac5aa Mon Sep 17 00:00:00 2001
From: Sami Imseih <simseih@amazon.com>
Date: Mon, 2 Jun 2025 10:27:41 -0500
Subject: [PATCH v8 1/1] Add plan_cache counters to pg_stat_statements.

This adds the ability for users to track how many
times a prepared statement was executed using either
a generic plan or a custom plan, by introducing two new
counters in pg_stat_statements: generic_plan_calls and
custom_plan_calls.

Discussion: https://www.postgresql.org/message-id/flat/CAA5RZ0uFw8Y9GCFvafhC=OA8NnMqVZyzXPfv_EePOt+iv1T-qQ@mail.gmail.com
Reviewers: Greg Sabino Mullane, Ilia Evdokimov, Nikolay Samokhvalov
---
 contrib/pg_stat_statements/Makefile           |   3 +-
 .../pg_stat_statements/expected/plancache.out | 251 ++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |   2 +
 .../pg_stat_statements--1.12--1.13.sql        |  78 ++++++
 .../pg_stat_statements/pg_stat_statements.c   |  54 +++-
 .../pg_stat_statements.control                |   2 +-
 contrib/pg_stat_statements/sql/plancache.sql  | 111 ++++++++
 doc/src/sgml/pgstatstatements.sgml            |  18 ++
 src/backend/commands/copyto.c                 |   2 +-
 src/backend/commands/createas.c               |   2 +-
 src/backend/commands/explain.c                |   6 +-
 src/backend/commands/extension.c              |   1 +
 src/backend/commands/matview.c                |   2 +-
 src/backend/commands/prepare.c                |   2 +-
 src/backend/executor/execParallel.c           |   1 +
 src/backend/executor/functions.c              |   1 +
 src/backend/executor/spi.c                    |   1 +
 src/backend/tcop/pquery.c                     |  11 +-
 src/backend/utils/cache/plancache.c           |   3 +
 src/include/commands/explain.h                |  10 +-
 src/include/executor/execdesc.h               |   3 +
 src/include/utils/plancache.h                 |   8 +
 22 files changed, 552 insertions(+), 20 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/plancache.out
 create mode 100644 contrib/pg_stat_statements/pg_stat_statements--1.12--1.13.sql
 create mode 100644 contrib/pg_stat_statements/sql/plancache.sql

diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index b2bd8794d2a1..996a5fac4487 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -7,6 +7,7 @@ OBJS = \
 
 EXTENSION = pg_stat_statements
 DATA = pg_stat_statements--1.4.sql \
+	pg_stat_statements--1.12--1.13.sql \
 	pg_stat_statements--1.11--1.12.sql pg_stat_statements--1.10--1.11.sql \
 	pg_stat_statements--1.9--1.10.sql pg_stat_statements--1.8--1.9.sql \
 	pg_stat_statements--1.7--1.8.sql pg_stat_statements--1.6--1.7.sql \
@@ -20,7 +21,7 @@ LDFLAGS_SL += $(filter -lm, $(LIBS))
 REGRESS_OPTS = --temp-config $(top_srcdir)/contrib/pg_stat_statements/pg_stat_statements.conf
 REGRESS = select dml cursors utility level_tracking planning \
 	user_activity wal entry_timestamp privileges extended \
-	parallel cleanup oldextversions squashing
+	parallel cleanup oldextversions squashing plancache
 # Disabled because these tests require "shared_preload_libraries=pg_stat_statements",
 # which typical installcheck users do not have (e.g. buildfarm clients).
 NO_INSTALLCHECK = 1
diff --git a/contrib/pg_stat_statements/expected/plancache.out b/contrib/pg_stat_statements/expected/plancache.out
new file mode 100644
index 000000000000..46025e0aa2a1
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/plancache.out
@@ -0,0 +1,251 @@
+--
+-- Information related to plan cache
+--
+--
+-- Setup
+--
+CREATE OR REPLACE FUNCTION select_one_func(int) RETURNS VOID AS $$
+DECLARE
+    ret INT;
+BEGIN
+    SELECT $1 INTO ret;
+END;
+$$ LANGUAGE plpgsql;
+CREATE OR REPLACE PROCEDURE select_one_proc(int) AS $$
+DECLARE
+    ret INT;
+BEGIN
+    select $1 INTO ret;
+END;
+$$ LANGUAGE plpgsql;
+--
+-- plan cache counters for prepared statements
+--
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+PREPARE p1 AS SELECT $1;
+-- plan cache auto
+SET plan_cache_mode TO auto;
+EXECUTE p1(1);
+ ?column? 
+----------
+ 1
+(1 row)
+
+-- force generic plan
+SET plan_cache_mode TO force_generic_plan;
+EXECUTE p1(1);
+ ?column? 
+----------
+ 1
+(1 row)
+
+-- force custom plan
+SET plan_cache_mode TO force_custom_plan;
+EXECUTE p1(1);
+ ?column? 
+----------
+ 1
+(1 row)
+
+SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, query FROM pg_stat_statements
+    ORDER BY query COLLATE "C";
+ calls | generic_plan_calls | custom_plan_calls | toplevel |                       query                        
+-------+--------------------+-------------------+----------+----------------------------------------------------
+     3 |                  1 |                 2 | t        | SELECT $1
+     1 |                  0 |                 0 | t        | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+     3 |                  0 |                 0 | t        | SET plan_cache_mode TO $1
+(3 rows)
+
+DEALLOCATE p1;
+--
+-- plan cache counters for explain|explain (analyze) with prepared statements
+--
+SET pg_stat_statements.track = 'all';
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+PREPARE p1 AS SELECT $1;
+-- plan cache auto
+SET plan_cache_mode TO auto;
+EXPLAIN (COSTS OFF) EXECUTE p1(1);
+ QUERY PLAN 
+------------
+ Result
+(1 row)
+
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) EXECUTE p1(1);
+            QUERY PLAN             
+-----------------------------------
+ Result (actual rows=1.00 loops=1)
+(1 row)
+
+-- force generic plan
+SET plan_cache_mode TO force_generic_plan;
+EXPLAIN (COSTS OFF) EXECUTE p1(1);
+ QUERY PLAN 
+------------
+ Result
+(1 row)
+
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) EXECUTE p1(1);
+            QUERY PLAN             
+-----------------------------------
+ Result (actual rows=1.00 loops=1)
+(1 row)
+
+-- force custom plan
+SET plan_cache_mode TO force_custom_plan;
+EXPLAIN (COSTS OFF) EXECUTE p1(1);
+ QUERY PLAN 
+------------
+ Result
+(1 row)
+
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) EXECUTE p1(1);
+            QUERY PLAN             
+-----------------------------------
+ Result (actual rows=1.00 loops=1)
+(1 row)
+
+SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, query FROM pg_stat_statements
+    ORDER BY query COLLATE "C";
+ calls | generic_plan_calls | custom_plan_calls | toplevel |                                      query                                       
+-------+--------------------+-------------------+----------+----------------------------------------------------------------------------------
+     3 |                  0 |                 0 | t        | EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) EXECUTE p1(1)
+     3 |                  0 |                 0 | t        | EXPLAIN (COSTS OFF) EXECUTE p1(1)
+     6 |                  2 |                 4 | f        | SELECT $1
+     1 |                  0 |                 0 | t        | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+     3 |                  0 |                 0 | t        | SET plan_cache_mode TO $1
+(5 rows)
+
+RESET pg_stat_statements.track;
+DEALLOCATE p1;
+--
+-- plan cache counters for functions and procedures
+--
+SET pg_stat_statements.track = 'all';
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- plan cache auto
+SET plan_cache_mode TO auto;
+SELECT select_one_func(1);
+ select_one_func 
+-----------------
+ 
+(1 row)
+
+CALL select_one_proc(1);
+-- force generic plan
+SET plan_cache_mode TO force_generic_plan;
+SELECT select_one_func(1);
+ select_one_func 
+-----------------
+ 
+(1 row)
+
+CALL select_one_proc(1);
+-- force custom plan
+SET plan_cache_mode TO force_custom_plan;
+SELECT select_one_func(1);
+ select_one_func 
+-----------------
+ 
+(1 row)
+
+CALL select_one_proc(1);
+SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, query FROM pg_stat_statements
+    ORDER BY query COLLATE "C";
+ calls | generic_plan_calls | custom_plan_calls | toplevel |                       query                        
+-------+--------------------+-------------------+----------+----------------------------------------------------
+     3 |                  0 |                 0 | t        | CALL select_one_proc($1)
+     6 |                  2 |                 4 | f        | SELECT $1
+     1 |                  0 |                 0 | t        | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+     3 |                  0 |                 0 | t        | SELECT select_one_func($1)
+     3 |                  0 |                 0 | t        | SET plan_cache_mode TO $1
+(5 rows)
+
+--
+-- plan cache counters for explain|explain (analyze) with functions and procedures
+--
+SET pg_stat_statements.track = 'all';
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- plan cache auto
+SET plan_cache_mode TO auto;
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) SELECT select_one_func(1);
+            QUERY PLAN             
+-----------------------------------
+ Result (actual rows=1.00 loops=1)
+(1 row)
+
+EXPLAIN (COSTS OFF) SELECT select_one_func(1);
+ QUERY PLAN 
+------------
+ Result
+(1 row)
+
+CALL select_one_proc(1);
+-- force generic plan
+SET plan_cache_mode TO force_generic_plan;
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) SELECT select_one_func(1);
+            QUERY PLAN             
+-----------------------------------
+ Result (actual rows=1.00 loops=1)
+(1 row)
+
+EXPLAIN (COSTS OFF) SELECT select_one_func(1);
+ QUERY PLAN 
+------------
+ Result
+(1 row)
+
+CALL select_one_proc(1);
+-- force custom plan
+SET plan_cache_mode TO force_custom_plan;
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) SELECT select_one_func(1);
+            QUERY PLAN             
+-----------------------------------
+ Result (actual rows=1.00 loops=1)
+(1 row)
+
+EXPLAIN (COSTS OFF) SELECT select_one_func(1);
+ QUERY PLAN 
+------------
+ Result
+(1 row)
+
+CALL select_one_proc(1);
+SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, query FROM pg_stat_statements
+    ORDER BY query COLLATE "C";
+ calls | generic_plan_calls | custom_plan_calls | toplevel |                                             query                                             
+-------+--------------------+-------------------+----------+-----------------------------------------------------------------------------------------------
+     3 |                  0 |                 0 | t        | CALL select_one_proc($1)
+     3 |                  0 |                 0 | t        | EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) SELECT select_one_func($1)
+     3 |                  0 |                 0 | t        | EXPLAIN (COSTS OFF) SELECT select_one_func($1)
+     6 |                  2 |                 4 | f        | SELECT $1
+     1 |                  0 |                 0 | t        | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+     6 |                  0 |                 0 | f        | SELECT select_one_func($1)
+     3 |                  0 |                 0 | t        | SET plan_cache_mode TO $1
+(7 rows)
+
+--
+-- Cleanup
+--
+DROP FUNCTION select_one_func(int);
+DROP PROCEDURE select_one_proc(int);
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 01a6cbdcf613..110fb82fe120 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -21,6 +21,7 @@ contrib_targets += pg_stat_statements
 install_data(
   'pg_stat_statements.control',
   'pg_stat_statements--1.4.sql',
+  'pg_stat_statements--1.12--1.13.sql',
   'pg_stat_statements--1.11--1.12.sql',
   'pg_stat_statements--1.10--1.11.sql',
   'pg_stat_statements--1.9--1.10.sql',
@@ -57,6 +58,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'plancache',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/pg_stat_statements--1.12--1.13.sql b/contrib/pg_stat_statements/pg_stat_statements--1.12--1.13.sql
new file mode 100644
index 000000000000..2f0eaf14ec34
--- /dev/null
+++ b/contrib/pg_stat_statements/pg_stat_statements--1.12--1.13.sql
@@ -0,0 +1,78 @@
+/* contrib/pg_stat_statements/pg_stat_statements--1.12--1.13.sql */
+
+-- complain if script is sourced in psql, rather than via ALTER EXTENSION
+\echo Use "ALTER EXTENSION pg_stat_statements UPDATE TO '1.13'" to load this file. \quit
+
+/* First we have to remove them from the extension */
+ALTER EXTENSION pg_stat_statements DROP VIEW pg_stat_statements;
+ALTER EXTENSION pg_stat_statements DROP FUNCTION pg_stat_statements(boolean);
+
+/* Then we can drop them */
+DROP VIEW pg_stat_statements;
+DROP FUNCTION pg_stat_statements(boolean);
+
+/* Now redefine */
+CREATE FUNCTION pg_stat_statements(IN showtext boolean,
+    OUT userid oid,
+    OUT dbid oid,
+    OUT toplevel bool,
+    OUT queryid bigint,
+    OUT query text,
+    OUT plans int8,
+    OUT total_plan_time float8,
+    OUT min_plan_time float8,
+    OUT max_plan_time float8,
+    OUT mean_plan_time float8,
+    OUT stddev_plan_time float8,
+    OUT calls int8,
+    OUT total_exec_time float8,
+    OUT min_exec_time float8,
+    OUT max_exec_time float8,
+    OUT mean_exec_time float8,
+    OUT stddev_exec_time float8,
+    OUT rows int8,
+    OUT shared_blks_hit int8,
+    OUT shared_blks_read int8,
+    OUT shared_blks_dirtied int8,
+    OUT shared_blks_written int8,
+    OUT local_blks_hit int8,
+    OUT local_blks_read int8,
+    OUT local_blks_dirtied int8,
+    OUT local_blks_written int8,
+    OUT temp_blks_read int8,
+    OUT temp_blks_written int8,
+    OUT shared_blk_read_time float8,
+    OUT shared_blk_write_time float8,
+    OUT local_blk_read_time float8,
+    OUT local_blk_write_time float8,
+    OUT temp_blk_read_time float8,
+    OUT temp_blk_write_time float8,
+    OUT wal_records int8,
+    OUT wal_fpi int8,
+    OUT wal_bytes numeric,
+    OUT wal_buffers_full int8,
+    OUT jit_functions int8,
+    OUT jit_generation_time float8,
+    OUT jit_inlining_count int8,
+    OUT jit_inlining_time float8,
+    OUT jit_optimization_count int8,
+    OUT jit_optimization_time float8,
+    OUT jit_emission_count int8,
+    OUT jit_emission_time float8,
+    OUT jit_deform_count int8,
+    OUT jit_deform_time float8,
+    OUT parallel_workers_to_launch int8,
+    OUT parallel_workers_launched int8,
+    OUT generic_plan_calls int8,
+    OUT custom_plan_calls int8,
+    OUT stats_since timestamp with time zone,
+    OUT minmax_stats_since timestamp with time zone
+)
+RETURNS SETOF record
+AS 'MODULE_PATHNAME', 'pg_stat_statements_1_13'
+LANGUAGE C STRICT VOLATILE PARALLEL SAFE;
+
+CREATE VIEW pg_stat_statements AS
+  SELECT * FROM pg_stat_statements(true);
+
+GRANT SELECT ON pg_stat_statements TO PUBLIC;
diff --git a/contrib/pg_stat_statements/pg_stat_statements.c b/contrib/pg_stat_statements/pg_stat_statements.c
index 129001c70c81..6f400fa150c2 100644
--- a/contrib/pg_stat_statements/pg_stat_statements.c
+++ b/contrib/pg_stat_statements/pg_stat_statements.c
@@ -69,6 +69,7 @@
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/memutils.h"
+#include "utils/plancache.h"
 #include "utils/timestamp.h"
 
 PG_MODULE_MAGIC_EXT(
@@ -114,6 +115,7 @@ typedef enum pgssVersion
 	PGSS_V1_10,
 	PGSS_V1_11,
 	PGSS_V1_12,
+	PGSS_V1_13,
 } pgssVersion;
 
 typedef enum pgssStoreKind
@@ -210,6 +212,8 @@ typedef struct Counters
 											 * to be launched */
 	int64		parallel_workers_launched;	/* # of parallel workers actually
 											 * launched */
+	int64		generic_plan_calls; /* number of calls using a generic plan */
+	int64		custom_plan_calls;	/* number of calls using a custom plan */
 } Counters;
 
 /*
@@ -323,6 +327,7 @@ PG_FUNCTION_INFO_V1(pg_stat_statements_1_9);
 PG_FUNCTION_INFO_V1(pg_stat_statements_1_10);
 PG_FUNCTION_INFO_V1(pg_stat_statements_1_11);
 PG_FUNCTION_INFO_V1(pg_stat_statements_1_12);
+PG_FUNCTION_INFO_V1(pg_stat_statements_1_13);
 PG_FUNCTION_INFO_V1(pg_stat_statements);
 PG_FUNCTION_INFO_V1(pg_stat_statements_info);
 
@@ -355,7 +360,8 @@ static void pgss_store(const char *query, int64 queryId,
 					   const struct JitInstrumentation *jitusage,
 					   JumbleState *jstate,
 					   int parallel_workers_to_launch,
-					   int parallel_workers_launched);
+					   int parallel_workers_launched,
+					   CachedPlan *cplan);
 static void pg_stat_statements_internal(FunctionCallInfo fcinfo,
 										pgssVersion api_version,
 										bool showtext);
@@ -877,7 +883,8 @@ pgss_post_parse_analyze(ParseState *pstate, Query *query, JumbleState *jstate)
 				   NULL,
 				   jstate,
 				   0,
-				   0);
+				   0,
+				   NULL);
 }
 
 /*
@@ -957,7 +964,8 @@ pgss_planner(Query *parse,
 				   NULL,
 				   NULL,
 				   0,
-				   0);
+				   0,
+				   NULL);
 	}
 	else
 	{
@@ -1091,7 +1099,8 @@ pgss_ExecutorEnd(QueryDesc *queryDesc)
 				   queryDesc->estate->es_jit ? &queryDesc->estate->es_jit->instr : NULL,
 				   NULL,
 				   queryDesc->estate->es_parallel_workers_to_launch,
-				   queryDesc->estate->es_parallel_workers_launched);
+				   queryDesc->estate->es_parallel_workers_launched,
+				   queryDesc->cplan);
 	}
 
 	if (prev_ExecutorEnd)
@@ -1224,7 +1233,8 @@ pgss_ProcessUtility(PlannedStmt *pstmt, const char *queryString,
 				   NULL,
 				   NULL,
 				   0,
-				   0);
+				   0,
+				   NULL);
 	}
 	else
 	{
@@ -1287,7 +1297,8 @@ pgss_store(const char *query, int64 queryId,
 		   const struct JitInstrumentation *jitusage,
 		   JumbleState *jstate,
 		   int parallel_workers_to_launch,
-		   int parallel_workers_launched)
+		   int parallel_workers_launched,
+		   CachedPlan *cplan)
 {
 	pgssHashKey key;
 	pgssEntry  *entry;
@@ -1495,6 +1506,14 @@ pgss_store(const char *query, int64 queryId,
 		entry->counters.parallel_workers_to_launch += parallel_workers_to_launch;
 		entry->counters.parallel_workers_launched += parallel_workers_launched;
 
+		if (cplan)
+		{
+			if (cplan->status == PLAN_CACHE_STATUS_GENERIC_PLAN)
+				entry->counters.generic_plan_calls++;
+			if (cplan->status == PLAN_CACHE_STATUS_CUSTOM_PLAN)
+				entry->counters.custom_plan_calls++;
+		}
+
 		SpinLockRelease(&entry->mutex);
 	}
 
@@ -1562,7 +1581,8 @@ pg_stat_statements_reset(PG_FUNCTION_ARGS)
 #define PG_STAT_STATEMENTS_COLS_V1_10	43
 #define PG_STAT_STATEMENTS_COLS_V1_11	49
 #define PG_STAT_STATEMENTS_COLS_V1_12	52
-#define PG_STAT_STATEMENTS_COLS			52	/* maximum of above */
+#define PG_STAT_STATEMENTS_COLS_V1_13	54
+#define PG_STAT_STATEMENTS_COLS			54	/* maximum of above */
 
 /*
  * Retrieve statement statistics.
@@ -1574,6 +1594,16 @@ pg_stat_statements_reset(PG_FUNCTION_ARGS)
  * expected API version is identified by embedding it in the C name of the
  * function.  Unfortunately we weren't bright enough to do that for 1.1.
  */
+Datum
+pg_stat_statements_1_13(PG_FUNCTION_ARGS)
+{
+	bool		showtext = PG_GETARG_BOOL(0);
+
+	pg_stat_statements_internal(fcinfo, PGSS_V1_13, showtext);
+
+	return (Datum) 0;
+}
+
 Datum
 pg_stat_statements_1_12(PG_FUNCTION_ARGS)
 {
@@ -1732,6 +1762,10 @@ pg_stat_statements_internal(FunctionCallInfo fcinfo,
 			if (api_version != PGSS_V1_12)
 				elog(ERROR, "incorrect number of output arguments");
 			break;
+		case PG_STAT_STATEMENTS_COLS_V1_13:
+			if (api_version != PGSS_V1_13)
+				elog(ERROR, "incorrect number of output arguments");
+			break;
 		default:
 			elog(ERROR, "incorrect number of output arguments");
 	}
@@ -1984,6 +2018,11 @@ pg_stat_statements_internal(FunctionCallInfo fcinfo,
 			values[i++] = Int64GetDatumFast(tmp.parallel_workers_to_launch);
 			values[i++] = Int64GetDatumFast(tmp.parallel_workers_launched);
 		}
+		if (api_version >= PGSS_V1_13)
+		{
+			values[i++] = Int64GetDatumFast(tmp.generic_plan_calls);
+			values[i++] = Int64GetDatumFast(tmp.custom_plan_calls);
+		}
 		if (api_version >= PGSS_V1_11)
 		{
 			values[i++] = TimestampTzGetDatum(stats_since);
@@ -1999,6 +2038,7 @@ pg_stat_statements_internal(FunctionCallInfo fcinfo,
 					 api_version == PGSS_V1_10 ? PG_STAT_STATEMENTS_COLS_V1_10 :
 					 api_version == PGSS_V1_11 ? PG_STAT_STATEMENTS_COLS_V1_11 :
 					 api_version == PGSS_V1_12 ? PG_STAT_STATEMENTS_COLS_V1_12 :
+					 api_version == PGSS_V1_13 ? PG_STAT_STATEMENTS_COLS_V1_13 :
 					 -1 /* fail if you forget to update this assert */ ));
 
 		tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, values, nulls);
diff --git a/contrib/pg_stat_statements/pg_stat_statements.control b/contrib/pg_stat_statements/pg_stat_statements.control
index d45ebc12e360..2eee0ceffa89 100644
--- a/contrib/pg_stat_statements/pg_stat_statements.control
+++ b/contrib/pg_stat_statements/pg_stat_statements.control
@@ -1,5 +1,5 @@
 # pg_stat_statements extension
 comment = 'track planning and execution statistics of all SQL statements executed'
-default_version = '1.12'
+default_version = '1.13'
 module_pathname = '$libdir/pg_stat_statements'
 relocatable = true
diff --git a/contrib/pg_stat_statements/sql/plancache.sql b/contrib/pg_stat_statements/sql/plancache.sql
new file mode 100644
index 000000000000..97d84bfdbede
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/plancache.sql
@@ -0,0 +1,111 @@
+--
+-- Information related to plan cache
+--
+
+--
+-- Setup
+--
+CREATE OR REPLACE FUNCTION select_one_func(int) RETURNS VOID AS $$
+DECLARE
+    ret INT;
+BEGIN
+    SELECT $1 INTO ret;
+END;
+$$ LANGUAGE plpgsql;
+CREATE OR REPLACE PROCEDURE select_one_proc(int) AS $$
+DECLARE
+    ret INT;
+BEGIN
+    select $1 INTO ret;
+END;
+$$ LANGUAGE plpgsql;
+
+--
+-- plan cache counters for prepared statements
+--
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+PREPARE p1 AS SELECT $1;
+-- plan cache auto
+SET plan_cache_mode TO auto;
+EXECUTE p1(1);
+-- force generic plan
+SET plan_cache_mode TO force_generic_plan;
+EXECUTE p1(1);
+-- force custom plan
+SET plan_cache_mode TO force_custom_plan;
+EXECUTE p1(1);
+SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, query FROM pg_stat_statements
+    ORDER BY query COLLATE "C";
+DEALLOCATE p1;
+
+--
+-- plan cache counters for explain|explain (analyze) with prepared statements
+--
+SET pg_stat_statements.track = 'all';
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+PREPARE p1 AS SELECT $1;
+-- plan cache auto
+SET plan_cache_mode TO auto;
+EXPLAIN (COSTS OFF) EXECUTE p1(1);
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) EXECUTE p1(1);
+-- force generic plan
+SET plan_cache_mode TO force_generic_plan;
+EXPLAIN (COSTS OFF) EXECUTE p1(1);
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) EXECUTE p1(1);
+-- force custom plan
+SET plan_cache_mode TO force_custom_plan;
+EXPLAIN (COSTS OFF) EXECUTE p1(1);
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) EXECUTE p1(1);
+SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, query FROM pg_stat_statements
+    ORDER BY query COLLATE "C";
+RESET pg_stat_statements.track;
+DEALLOCATE p1;
+
+--
+-- plan cache counters for functions and procedures
+--
+SET pg_stat_statements.track = 'all';
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+-- plan cache auto
+SET plan_cache_mode TO auto;
+SELECT select_one_func(1);
+CALL select_one_proc(1);
+-- force generic plan
+SET plan_cache_mode TO force_generic_plan;
+SELECT select_one_func(1);
+CALL select_one_proc(1);
+-- force custom plan
+SET plan_cache_mode TO force_custom_plan;
+SELECT select_one_func(1);
+CALL select_one_proc(1);
+SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, query FROM pg_stat_statements
+    ORDER BY query COLLATE "C";
+
+--
+-- plan cache counters for explain|explain (analyze) with functions and procedures
+--
+SET pg_stat_statements.track = 'all';
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+-- plan cache auto
+SET plan_cache_mode TO auto;
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) SELECT select_one_func(1);
+EXPLAIN (COSTS OFF) SELECT select_one_func(1);
+CALL select_one_proc(1);
+-- force generic plan
+SET plan_cache_mode TO force_generic_plan;
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) SELECT select_one_func(1);
+EXPLAIN (COSTS OFF) SELECT select_one_func(1);
+CALL select_one_proc(1);
+-- force custom plan
+SET plan_cache_mode TO force_custom_plan;
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) SELECT select_one_func(1);
+EXPLAIN (COSTS OFF) SELECT select_one_func(1);
+CALL select_one_proc(1);
+SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, query FROM pg_stat_statements
+    ORDER BY query COLLATE "C";
+
+--
+-- Cleanup
+--
+DROP FUNCTION select_one_func(int);
+DROP PROCEDURE select_one_proc(int);
diff --git a/doc/src/sgml/pgstatstatements.sgml b/doc/src/sgml/pgstatstatements.sgml
index 7baa07dcdbf7..0bce696aac50 100644
--- a/doc/src/sgml/pgstatstatements.sgml
+++ b/doc/src/sgml/pgstatstatements.sgml
@@ -575,6 +575,24 @@
        <structfield>max_exec_time</structfield>)
       </para></entry>
      </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>generic_plan_calls</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Number of times the statement was executed using a generic plan
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>custom_plan_calls</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Number of times the statement was executed using a custom plan
+      </para></entry>
+     </row>
     </tbody>
    </tgroup>
   </table>
diff --git a/src/backend/commands/copyto.c b/src/backend/commands/copyto.c
index ea6f18f2c800..55a2719d7685 100644
--- a/src/backend/commands/copyto.c
+++ b/src/backend/commands/copyto.c
@@ -835,7 +835,7 @@ BeginCopyTo(ParseState *pstate,
 		((DR_copy *) dest)->cstate = cstate;
 
 		/* Create a QueryDesc requesting no output */
-		cstate->queryDesc = CreateQueryDesc(plan, pstate->p_sourcetext,
+		cstate->queryDesc = CreateQueryDesc(plan, NULL, pstate->p_sourcetext,
 											GetActiveSnapshot(),
 											InvalidSnapshot,
 											dest, NULL, NULL, 0);
diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c
index dfd2ab8e8628..18c8a11b70c4 100644
--- a/src/backend/commands/createas.c
+++ b/src/backend/commands/createas.c
@@ -334,7 +334,7 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt,
 		UpdateActiveSnapshotCommandId();
 
 		/* Create a QueryDesc, redirecting output to our tuple receiver */
-		queryDesc = CreateQueryDesc(plan, pstate->p_sourcetext,
+		queryDesc = CreateQueryDesc(plan, NULL, pstate->p_sourcetext,
 									GetActiveSnapshot(), InvalidSnapshot,
 									dest, params, queryEnv, 0);
 
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 7e2792ead715..b2b998bc2db8 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -369,7 +369,7 @@ standard_ExplainOneQuery(Query *query, int cursorOptions,
 	}
 
 	/* run it (if needed) and produce output */
-	ExplainOnePlan(plan, into, es, queryString, params, queryEnv,
+	ExplainOnePlan(plan, NULL, into, es, queryString, params, queryEnv,
 				   &planduration, (es->buffers ? &bufusage : NULL),
 				   es->memory ? &mem_counters : NULL);
 }
@@ -491,7 +491,7 @@ ExplainOneUtility(Node *utilityStmt, IntoClause *into, ExplainState *es,
  * to call it.
  */
 void
-ExplainOnePlan(PlannedStmt *plannedstmt, IntoClause *into, ExplainState *es,
+ExplainOnePlan(PlannedStmt *plannedstmt, CachedPlan *cplan, IntoClause *into, ExplainState *es,
 			   const char *queryString, ParamListInfo params,
 			   QueryEnvironment *queryEnv, const instr_time *planduration,
 			   const BufferUsage *bufusage,
@@ -547,7 +547,7 @@ ExplainOnePlan(PlannedStmt *plannedstmt, IntoClause *into, ExplainState *es,
 		dest = None_Receiver;
 
 	/* Create a QueryDesc for the query */
-	queryDesc = CreateQueryDesc(plannedstmt, queryString,
+	queryDesc = CreateQueryDesc(plannedstmt, cplan, queryString,
 								GetActiveSnapshot(), InvalidSnapshot,
 								dest, params, queryEnv, instrument_option);
 
diff --git a/src/backend/commands/extension.c b/src/backend/commands/extension.c
index e6f9ab6dfd66..bc3e7bb808fa 100644
--- a/src/backend/commands/extension.c
+++ b/src/backend/commands/extension.c
@@ -993,6 +993,7 @@ execute_sql_string(const char *sql, const char *filename)
 				QueryDesc  *qdesc;
 
 				qdesc = CreateQueryDesc(stmt,
+										NULL,
 										sql,
 										GetActiveSnapshot(), NULL,
 										dest, NULL, NULL, 0);
diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index 27c2cb26ef5f..c9e7b6f5195e 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -438,7 +438,7 @@ refresh_matview_datafill(DestReceiver *dest, Query *query,
 	UpdateActiveSnapshotCommandId();
 
 	/* Create a QueryDesc, redirecting output to our tuple receiver */
-	queryDesc = CreateQueryDesc(plan, queryString,
+	queryDesc = CreateQueryDesc(plan, NULL, queryString,
 								GetActiveSnapshot(), InvalidSnapshot,
 								dest, NULL, NULL, 0);
 
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a26..a5f3a662e514 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -657,7 +657,7 @@ ExplainExecuteQuery(ExecuteStmt *execstmt, IntoClause *into, ExplainState *es,
 		PlannedStmt *pstmt = lfirst_node(PlannedStmt, p);
 
 		if (pstmt->commandType != CMD_UTILITY)
-			ExplainOnePlan(pstmt, into, es, query_string, paramLI, pstate->p_queryEnv,
+			ExplainOnePlan(pstmt, cplan, into, es, query_string, paramLI, pstate->p_queryEnv,
 						   &planduration, (es->buffers ? &bufusage : NULL),
 						   es->memory ? &mem_counters : NULL);
 		else
diff --git a/src/backend/executor/execParallel.c b/src/backend/executor/execParallel.c
index f3e77bda2790..58b1c09223a8 100644
--- a/src/backend/executor/execParallel.c
+++ b/src/backend/executor/execParallel.c
@@ -1280,6 +1280,7 @@ ExecParallelGetQueryDesc(shm_toc *toc, DestReceiver *receiver,
 
 	/* Create a QueryDesc for the query. */
 	return CreateQueryDesc(pstmt,
+						   NULL,
 						   queryString,
 						   GetActiveSnapshot(), InvalidSnapshot,
 						   receiver, paramLI, NULL, instrument_options);
diff --git a/src/backend/executor/functions.c b/src/backend/executor/functions.c
index 359aafea681b..635ce762eb37 100644
--- a/src/backend/executor/functions.c
+++ b/src/backend/executor/functions.c
@@ -1339,6 +1339,7 @@ postquel_start(execution_state *es, SQLFunctionCachePtr fcache)
 		dest = None_Receiver;
 
 	es->qd = CreateQueryDesc(es->stmt,
+							 NULL,
 							 fcache->func->src,
 							 GetActiveSnapshot(),
 							 InvalidSnapshot,
diff --git a/src/backend/executor/spi.c b/src/backend/executor/spi.c
index ecb2e4ccaa1c..5ffff06ec0ef 100644
--- a/src/backend/executor/spi.c
+++ b/src/backend/executor/spi.c
@@ -2690,6 +2690,7 @@ _SPI_execute_plan(SPIPlanPtr plan, const SPIExecuteOptions *options,
 					snap = InvalidSnapshot;
 
 				qdesc = CreateQueryDesc(stmt,
+										cplan,
 										plansource->query_string,
 										snap, crosscheck_snapshot,
 										dest,
diff --git a/src/backend/tcop/pquery.c b/src/backend/tcop/pquery.c
index d1593f38b35f..dc4f5ce9d5e7 100644
--- a/src/backend/tcop/pquery.c
+++ b/src/backend/tcop/pquery.c
@@ -26,6 +26,7 @@
 #include "tcop/pquery.h"
 #include "tcop/utility.h"
 #include "utils/memutils.h"
+#include "utils/plancache.h"
 #include "utils/snapmgr.h"
 
 
@@ -37,6 +38,7 @@ Portal		ActivePortal = NULL;
 
 
 static void ProcessQuery(PlannedStmt *plan,
+						 CachedPlan *cplan,
 						 const char *sourceText,
 						 ParamListInfo params,
 						 QueryEnvironment *queryEnv,
@@ -66,6 +68,7 @@ static void DoPortalRewind(Portal portal);
  */
 QueryDesc *
 CreateQueryDesc(PlannedStmt *plannedstmt,
+				CachedPlan *cplan,
 				const char *sourceText,
 				Snapshot snapshot,
 				Snapshot crosscheck_snapshot,
@@ -93,6 +96,8 @@ CreateQueryDesc(PlannedStmt *plannedstmt,
 	qd->planstate = NULL;
 	qd->totaltime = NULL;
 
+	qd->cplan = cplan;
+
 	/* not yet executed */
 	qd->already_executed = false;
 
@@ -135,6 +140,7 @@ FreeQueryDesc(QueryDesc *qdesc)
  */
 static void
 ProcessQuery(PlannedStmt *plan,
+			 CachedPlan *cplan,
 			 const char *sourceText,
 			 ParamListInfo params,
 			 QueryEnvironment *queryEnv,
@@ -146,7 +152,7 @@ ProcessQuery(PlannedStmt *plan,
 	/*
 	 * Create the QueryDesc object
 	 */
-	queryDesc = CreateQueryDesc(plan, sourceText,
+	queryDesc = CreateQueryDesc(plan, cplan, sourceText,
 								GetActiveSnapshot(), InvalidSnapshot,
 								dest, params, queryEnv, 0);
 
@@ -494,6 +500,7 @@ PortalStart(Portal portal, ParamListInfo params,
 				 * the destination to DestNone.
 				 */
 				queryDesc = CreateQueryDesc(linitial_node(PlannedStmt, portal->stmts),
+											portal->cplan,
 											portal->sourceText,
 											GetActiveSnapshot(),
 											InvalidSnapshot,
@@ -1270,6 +1277,7 @@ PortalRunMulti(Portal portal,
 			{
 				/* statement can set tag string */
 				ProcessQuery(pstmt,
+							 portal->cplan,
 							 portal->sourceText,
 							 portal->portalParams,
 							 portal->queryEnv,
@@ -1279,6 +1287,7 @@ PortalRunMulti(Portal portal,
 			{
 				/* stmt added by rewrite cannot set tag */
 				ProcessQuery(pstmt,
+							 portal->cplan,
 							 portal->sourceText,
 							 portal->portalParams,
 							 portal->queryEnv,
diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c
index 89a1c79e984d..63df3ecd881e 100644
--- a/src/backend/utils/cache/plancache.c
+++ b/src/backend/utils/cache/plancache.c
@@ -1140,6 +1140,7 @@ BuildCachedPlan(CachedPlanSource *plansource, List *qlist,
 	plan->is_oneshot = plansource->is_oneshot;
 	plan->is_saved = false;
 	plan->is_valid = true;
+	plan->status = PLAN_CACHE_STATUS_UNKNOWN;
 
 	/* assign generation number to new plan */
 	plan->generation = ++(plansource->generation);
@@ -1358,10 +1359,12 @@ GetCachedPlan(CachedPlanSource *plansource, ParamListInfo boundParams,
 		plansource->total_custom_cost += cached_plan_cost(plan, true);
 
 		plansource->num_custom_plans++;
+		plan->status = PLAN_CACHE_STATUS_CUSTOM_PLAN;
 	}
 	else
 	{
 		plansource->num_generic_plans++;
+		plan->status = PLAN_CACHE_STATUS_GENERIC_PLAN;
 	}
 
 	Assert(plan != NULL);
diff --git a/src/include/commands/explain.h b/src/include/commands/explain.h
index 3b122f79ed84..36579590ec2b 100644
--- a/src/include/commands/explain.h
+++ b/src/include/commands/explain.h
@@ -63,9 +63,13 @@ extern void ExplainOneUtility(Node *utilityStmt, IntoClause *into,
 							  struct ExplainState *es, ParseState *pstate,
 							  ParamListInfo params);
 
-extern void ExplainOnePlan(PlannedStmt *plannedstmt, IntoClause *into,
-						   struct ExplainState *es, const char *queryString,
-						   ParamListInfo params, QueryEnvironment *queryEnv,
+extern void ExplainOnePlan(PlannedStmt *plannedstmt,
+						   CachedPlan *cplan,
+						   IntoClause *into,
+						   struct ExplainState *es,
+						   const char *queryString,
+						   ParamListInfo params,
+						   QueryEnvironment *queryEnv,
 						   const instr_time *planduration,
 						   const BufferUsage *bufusage,
 						   const MemoryContextCounters *mem_counters);
diff --git a/src/include/executor/execdesc.h b/src/include/executor/execdesc.h
index 86db3dc8d0de..f7cbe0981a54 100644
--- a/src/include/executor/execdesc.h
+++ b/src/include/executor/execdesc.h
@@ -17,6 +17,7 @@
 
 #include "nodes/execnodes.h"
 #include "tcop/dest.h"
+#include "utils/plancache.h"
 
 
 /* ----------------
@@ -35,6 +36,7 @@ typedef struct QueryDesc
 	/* These fields are provided by CreateQueryDesc */
 	CmdType		operation;		/* CMD_SELECT, CMD_UPDATE, etc. */
 	PlannedStmt *plannedstmt;	/* planner's output (could be utility, too) */
+	CachedPlan *cplan;			/* CachedPlan that supplies the plannedstmt */
 	const char *sourceText;		/* source text of the query */
 	Snapshot	snapshot;		/* snapshot to use for query */
 	Snapshot	crosscheck_snapshot;	/* crosscheck for RI update/delete */
@@ -57,6 +59,7 @@ typedef struct QueryDesc
 
 /* in pquery.c */
 extern QueryDesc *CreateQueryDesc(PlannedStmt *plannedstmt,
+								  CachedPlan *cplan,
 								  const char *sourceText,
 								  Snapshot snapshot,
 								  Snapshot crosscheck_snapshot,
diff --git a/src/include/utils/plancache.h b/src/include/utils/plancache.h
index 1baa6d50bfd7..b9d009911403 100644
--- a/src/include/utils/plancache.h
+++ b/src/include/utils/plancache.h
@@ -38,6 +38,13 @@ typedef enum
 /* GUC parameter */
 extern PGDLLIMPORT int plan_cache_mode;
 
+typedef enum
+{
+	PLAN_CACHE_STATUS_UNKNOWN = 0,
+	PLAN_CACHE_STATUS_GENERIC_PLAN,
+	PLAN_CACHE_STATUS_CUSTOM_PLAN,
+}			PlanCacheStatus;
+
 /* Optional callback to editorialize on rewritten parse trees */
 typedef void (*PostRewriteHook) (List *querytree_list, void *arg);
 
@@ -163,6 +170,7 @@ typedef struct CachedPlan
 	bool		is_oneshot;		/* is it a "oneshot" plan? */
 	bool		is_saved;		/* is CachedPlan in a long-lived context? */
 	bool		is_valid;		/* is the stmt_list currently valid? */
+	PlanCacheStatus status;		/* status of the cached plan */
 	Oid			planRoleId;		/* Role ID the plan was created for */
 	bool		dependsOnRole;	/* is plan specific to that role? */
 	TransactionId saved_xmin;	/* if valid, replan when TransactionXmin
-- 
2.39.5 (Apple Git-154)

#18Ilia Evdokimov
ilya.evdokimov@tantorlabs.com
In reply to: Sami Imseih (#17)
Re: track generic and custom plans in pg_stat_statements

On 03.06.2025 06:31, Sami Imseih wrote:

See v8 with the field names reorganized.

Thanks for your work on this.

Since we've changed the placement of these parameters between
parallel_workers and stats_since, we should also update the
documentation to reflect their new location.

--
Best regards,
Ilia Evdokimov,
Tantor Labs LLC.

#19Ilia Evdokimov
ilya.evdokimov@tantorlabs.com
In reply to: Sami Imseih (#17)
Re: track generic and custom plans in pg_stat_statements

On 03.06.2025 06:31, Sami Imseih wrote:

See v8 with the field names reorganized.

Apologies if you received two identical emails.

Since we've changed the placement of these parameters between
parallel_workers and stats_since, we should also update the
documentation to reflect their new location.

--
Best regards,
Ilia Evdokimov,
Tantor Labs LLC.

#20Sami Imseih
samimseih@gmail.com
In reply to: Ilia Evdokimov (#18)
1 attachment(s)
Re: track generic and custom plans in pg_stat_statements

Thanks for your work on this.

Since we've changed the placement of these parameters
between parallel_workers and stats_since, we should also update
the documentation to reflect their new location.

Absolutely. My miss. Here is v9 with the doc updated.

Thanks!

--
Sami

Attachments:

v9-0001-Add-plan_cache-counters-to-pg_stat_statements.patchapplication/octet-stream; name=v9-0001-Add-plan_cache-counters-to-pg_stat_statements.patchDownload
From 914ffb2bf24012d518802a16e21e89454499bd26 Mon Sep 17 00:00:00 2001
From: Sami Imseih <simseih@amazon.com>
Date: Thu, 5 Jun 2025 15:58:07 -0500
Subject: [PATCH v9 1/1] Add plan_cache counters to pg_stat_statements.

This adds the ability for users to track how many
times a prepared statement was executed using either
a generic plan or a custom plan, by introducing two new
counters in pg_stat_statements: generic_plan_calls and
custom_plan_calls.

Discussion: https://www.postgresql.org/message-id/flat/CAA5RZ0uFw8Y9GCFvafhC=OA8NnMqVZyzXPfv_EePOt+iv1T-qQ@mail.gmail.com
Reviewers: Greg Sabino Mullane, Ilia Evdokimov, Nikolay Samokhvalov
---
 contrib/pg_stat_statements/Makefile           |   3 +-
 .../pg_stat_statements/expected/plancache.out | 251 ++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |   2 +
 .../pg_stat_statements--1.12--1.13.sql        |  78 ++++++
 .../pg_stat_statements/pg_stat_statements.c   |  54 +++-
 .../pg_stat_statements.control                |   2 +-
 contrib/pg_stat_statements/sql/plancache.sql  | 111 ++++++++
 doc/src/sgml/pgstatstatements.sgml            |  18 ++
 src/backend/commands/copyto.c                 |   2 +-
 src/backend/commands/createas.c               |   2 +-
 src/backend/commands/explain.c                |   6 +-
 src/backend/commands/extension.c              |   1 +
 src/backend/commands/matview.c                |   2 +-
 src/backend/commands/prepare.c                |   2 +-
 src/backend/executor/execParallel.c           |   1 +
 src/backend/executor/functions.c              |   1 +
 src/backend/executor/spi.c                    |   1 +
 src/backend/tcop/pquery.c                     |  11 +-
 src/backend/utils/cache/plancache.c           |   3 +
 src/include/commands/explain.h                |  10 +-
 src/include/executor/execdesc.h               |   3 +
 src/include/utils/plancache.h                 |   8 +
 22 files changed, 552 insertions(+), 20 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/plancache.out
 create mode 100644 contrib/pg_stat_statements/pg_stat_statements--1.12--1.13.sql
 create mode 100644 contrib/pg_stat_statements/sql/plancache.sql

diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index b2bd8794d2a1..996a5fac4487 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -7,6 +7,7 @@ OBJS = \
 
 EXTENSION = pg_stat_statements
 DATA = pg_stat_statements--1.4.sql \
+	pg_stat_statements--1.12--1.13.sql \
 	pg_stat_statements--1.11--1.12.sql pg_stat_statements--1.10--1.11.sql \
 	pg_stat_statements--1.9--1.10.sql pg_stat_statements--1.8--1.9.sql \
 	pg_stat_statements--1.7--1.8.sql pg_stat_statements--1.6--1.7.sql \
@@ -20,7 +21,7 @@ LDFLAGS_SL += $(filter -lm, $(LIBS))
 REGRESS_OPTS = --temp-config $(top_srcdir)/contrib/pg_stat_statements/pg_stat_statements.conf
 REGRESS = select dml cursors utility level_tracking planning \
 	user_activity wal entry_timestamp privileges extended \
-	parallel cleanup oldextversions squashing
+	parallel cleanup oldextversions squashing plancache
 # Disabled because these tests require "shared_preload_libraries=pg_stat_statements",
 # which typical installcheck users do not have (e.g. buildfarm clients).
 NO_INSTALLCHECK = 1
diff --git a/contrib/pg_stat_statements/expected/plancache.out b/contrib/pg_stat_statements/expected/plancache.out
new file mode 100644
index 000000000000..46025e0aa2a1
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/plancache.out
@@ -0,0 +1,251 @@
+--
+-- Information related to plan cache
+--
+--
+-- Setup
+--
+CREATE OR REPLACE FUNCTION select_one_func(int) RETURNS VOID AS $$
+DECLARE
+    ret INT;
+BEGIN
+    SELECT $1 INTO ret;
+END;
+$$ LANGUAGE plpgsql;
+CREATE OR REPLACE PROCEDURE select_one_proc(int) AS $$
+DECLARE
+    ret INT;
+BEGIN
+    select $1 INTO ret;
+END;
+$$ LANGUAGE plpgsql;
+--
+-- plan cache counters for prepared statements
+--
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+PREPARE p1 AS SELECT $1;
+-- plan cache auto
+SET plan_cache_mode TO auto;
+EXECUTE p1(1);
+ ?column? 
+----------
+ 1
+(1 row)
+
+-- force generic plan
+SET plan_cache_mode TO force_generic_plan;
+EXECUTE p1(1);
+ ?column? 
+----------
+ 1
+(1 row)
+
+-- force custom plan
+SET plan_cache_mode TO force_custom_plan;
+EXECUTE p1(1);
+ ?column? 
+----------
+ 1
+(1 row)
+
+SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, query FROM pg_stat_statements
+    ORDER BY query COLLATE "C";
+ calls | generic_plan_calls | custom_plan_calls | toplevel |                       query                        
+-------+--------------------+-------------------+----------+----------------------------------------------------
+     3 |                  1 |                 2 | t        | SELECT $1
+     1 |                  0 |                 0 | t        | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+     3 |                  0 |                 0 | t        | SET plan_cache_mode TO $1
+(3 rows)
+
+DEALLOCATE p1;
+--
+-- plan cache counters for explain|explain (analyze) with prepared statements
+--
+SET pg_stat_statements.track = 'all';
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+PREPARE p1 AS SELECT $1;
+-- plan cache auto
+SET plan_cache_mode TO auto;
+EXPLAIN (COSTS OFF) EXECUTE p1(1);
+ QUERY PLAN 
+------------
+ Result
+(1 row)
+
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) EXECUTE p1(1);
+            QUERY PLAN             
+-----------------------------------
+ Result (actual rows=1.00 loops=1)
+(1 row)
+
+-- force generic plan
+SET plan_cache_mode TO force_generic_plan;
+EXPLAIN (COSTS OFF) EXECUTE p1(1);
+ QUERY PLAN 
+------------
+ Result
+(1 row)
+
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) EXECUTE p1(1);
+            QUERY PLAN             
+-----------------------------------
+ Result (actual rows=1.00 loops=1)
+(1 row)
+
+-- force custom plan
+SET plan_cache_mode TO force_custom_plan;
+EXPLAIN (COSTS OFF) EXECUTE p1(1);
+ QUERY PLAN 
+------------
+ Result
+(1 row)
+
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) EXECUTE p1(1);
+            QUERY PLAN             
+-----------------------------------
+ Result (actual rows=1.00 loops=1)
+(1 row)
+
+SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, query FROM pg_stat_statements
+    ORDER BY query COLLATE "C";
+ calls | generic_plan_calls | custom_plan_calls | toplevel |                                      query                                       
+-------+--------------------+-------------------+----------+----------------------------------------------------------------------------------
+     3 |                  0 |                 0 | t        | EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) EXECUTE p1(1)
+     3 |                  0 |                 0 | t        | EXPLAIN (COSTS OFF) EXECUTE p1(1)
+     6 |                  2 |                 4 | f        | SELECT $1
+     1 |                  0 |                 0 | t        | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+     3 |                  0 |                 0 | t        | SET plan_cache_mode TO $1
+(5 rows)
+
+RESET pg_stat_statements.track;
+DEALLOCATE p1;
+--
+-- plan cache counters for functions and procedures
+--
+SET pg_stat_statements.track = 'all';
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- plan cache auto
+SET plan_cache_mode TO auto;
+SELECT select_one_func(1);
+ select_one_func 
+-----------------
+ 
+(1 row)
+
+CALL select_one_proc(1);
+-- force generic plan
+SET plan_cache_mode TO force_generic_plan;
+SELECT select_one_func(1);
+ select_one_func 
+-----------------
+ 
+(1 row)
+
+CALL select_one_proc(1);
+-- force custom plan
+SET plan_cache_mode TO force_custom_plan;
+SELECT select_one_func(1);
+ select_one_func 
+-----------------
+ 
+(1 row)
+
+CALL select_one_proc(1);
+SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, query FROM pg_stat_statements
+    ORDER BY query COLLATE "C";
+ calls | generic_plan_calls | custom_plan_calls | toplevel |                       query                        
+-------+--------------------+-------------------+----------+----------------------------------------------------
+     3 |                  0 |                 0 | t        | CALL select_one_proc($1)
+     6 |                  2 |                 4 | f        | SELECT $1
+     1 |                  0 |                 0 | t        | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+     3 |                  0 |                 0 | t        | SELECT select_one_func($1)
+     3 |                  0 |                 0 | t        | SET plan_cache_mode TO $1
+(5 rows)
+
+--
+-- plan cache counters for explain|explain (analyze) with functions and procedures
+--
+SET pg_stat_statements.track = 'all';
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- plan cache auto
+SET plan_cache_mode TO auto;
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) SELECT select_one_func(1);
+            QUERY PLAN             
+-----------------------------------
+ Result (actual rows=1.00 loops=1)
+(1 row)
+
+EXPLAIN (COSTS OFF) SELECT select_one_func(1);
+ QUERY PLAN 
+------------
+ Result
+(1 row)
+
+CALL select_one_proc(1);
+-- force generic plan
+SET plan_cache_mode TO force_generic_plan;
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) SELECT select_one_func(1);
+            QUERY PLAN             
+-----------------------------------
+ Result (actual rows=1.00 loops=1)
+(1 row)
+
+EXPLAIN (COSTS OFF) SELECT select_one_func(1);
+ QUERY PLAN 
+------------
+ Result
+(1 row)
+
+CALL select_one_proc(1);
+-- force custom plan
+SET plan_cache_mode TO force_custom_plan;
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) SELECT select_one_func(1);
+            QUERY PLAN             
+-----------------------------------
+ Result (actual rows=1.00 loops=1)
+(1 row)
+
+EXPLAIN (COSTS OFF) SELECT select_one_func(1);
+ QUERY PLAN 
+------------
+ Result
+(1 row)
+
+CALL select_one_proc(1);
+SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, query FROM pg_stat_statements
+    ORDER BY query COLLATE "C";
+ calls | generic_plan_calls | custom_plan_calls | toplevel |                                             query                                             
+-------+--------------------+-------------------+----------+-----------------------------------------------------------------------------------------------
+     3 |                  0 |                 0 | t        | CALL select_one_proc($1)
+     3 |                  0 |                 0 | t        | EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) SELECT select_one_func($1)
+     3 |                  0 |                 0 | t        | EXPLAIN (COSTS OFF) SELECT select_one_func($1)
+     6 |                  2 |                 4 | f        | SELECT $1
+     1 |                  0 |                 0 | t        | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+     6 |                  0 |                 0 | f        | SELECT select_one_func($1)
+     3 |                  0 |                 0 | t        | SET plan_cache_mode TO $1
+(7 rows)
+
+--
+-- Cleanup
+--
+DROP FUNCTION select_one_func(int);
+DROP PROCEDURE select_one_proc(int);
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 01a6cbdcf613..110fb82fe120 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -21,6 +21,7 @@ contrib_targets += pg_stat_statements
 install_data(
   'pg_stat_statements.control',
   'pg_stat_statements--1.4.sql',
+  'pg_stat_statements--1.12--1.13.sql',
   'pg_stat_statements--1.11--1.12.sql',
   'pg_stat_statements--1.10--1.11.sql',
   'pg_stat_statements--1.9--1.10.sql',
@@ -57,6 +58,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'plancache',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/pg_stat_statements--1.12--1.13.sql b/contrib/pg_stat_statements/pg_stat_statements--1.12--1.13.sql
new file mode 100644
index 000000000000..2f0eaf14ec34
--- /dev/null
+++ b/contrib/pg_stat_statements/pg_stat_statements--1.12--1.13.sql
@@ -0,0 +1,78 @@
+/* contrib/pg_stat_statements/pg_stat_statements--1.12--1.13.sql */
+
+-- complain if script is sourced in psql, rather than via ALTER EXTENSION
+\echo Use "ALTER EXTENSION pg_stat_statements UPDATE TO '1.13'" to load this file. \quit
+
+/* First we have to remove them from the extension */
+ALTER EXTENSION pg_stat_statements DROP VIEW pg_stat_statements;
+ALTER EXTENSION pg_stat_statements DROP FUNCTION pg_stat_statements(boolean);
+
+/* Then we can drop them */
+DROP VIEW pg_stat_statements;
+DROP FUNCTION pg_stat_statements(boolean);
+
+/* Now redefine */
+CREATE FUNCTION pg_stat_statements(IN showtext boolean,
+    OUT userid oid,
+    OUT dbid oid,
+    OUT toplevel bool,
+    OUT queryid bigint,
+    OUT query text,
+    OUT plans int8,
+    OUT total_plan_time float8,
+    OUT min_plan_time float8,
+    OUT max_plan_time float8,
+    OUT mean_plan_time float8,
+    OUT stddev_plan_time float8,
+    OUT calls int8,
+    OUT total_exec_time float8,
+    OUT min_exec_time float8,
+    OUT max_exec_time float8,
+    OUT mean_exec_time float8,
+    OUT stddev_exec_time float8,
+    OUT rows int8,
+    OUT shared_blks_hit int8,
+    OUT shared_blks_read int8,
+    OUT shared_blks_dirtied int8,
+    OUT shared_blks_written int8,
+    OUT local_blks_hit int8,
+    OUT local_blks_read int8,
+    OUT local_blks_dirtied int8,
+    OUT local_blks_written int8,
+    OUT temp_blks_read int8,
+    OUT temp_blks_written int8,
+    OUT shared_blk_read_time float8,
+    OUT shared_blk_write_time float8,
+    OUT local_blk_read_time float8,
+    OUT local_blk_write_time float8,
+    OUT temp_blk_read_time float8,
+    OUT temp_blk_write_time float8,
+    OUT wal_records int8,
+    OUT wal_fpi int8,
+    OUT wal_bytes numeric,
+    OUT wal_buffers_full int8,
+    OUT jit_functions int8,
+    OUT jit_generation_time float8,
+    OUT jit_inlining_count int8,
+    OUT jit_inlining_time float8,
+    OUT jit_optimization_count int8,
+    OUT jit_optimization_time float8,
+    OUT jit_emission_count int8,
+    OUT jit_emission_time float8,
+    OUT jit_deform_count int8,
+    OUT jit_deform_time float8,
+    OUT parallel_workers_to_launch int8,
+    OUT parallel_workers_launched int8,
+    OUT generic_plan_calls int8,
+    OUT custom_plan_calls int8,
+    OUT stats_since timestamp with time zone,
+    OUT minmax_stats_since timestamp with time zone
+)
+RETURNS SETOF record
+AS 'MODULE_PATHNAME', 'pg_stat_statements_1_13'
+LANGUAGE C STRICT VOLATILE PARALLEL SAFE;
+
+CREATE VIEW pg_stat_statements AS
+  SELECT * FROM pg_stat_statements(true);
+
+GRANT SELECT ON pg_stat_statements TO PUBLIC;
diff --git a/contrib/pg_stat_statements/pg_stat_statements.c b/contrib/pg_stat_statements/pg_stat_statements.c
index 129001c70c81..6f400fa150c2 100644
--- a/contrib/pg_stat_statements/pg_stat_statements.c
+++ b/contrib/pg_stat_statements/pg_stat_statements.c
@@ -69,6 +69,7 @@
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/memutils.h"
+#include "utils/plancache.h"
 #include "utils/timestamp.h"
 
 PG_MODULE_MAGIC_EXT(
@@ -114,6 +115,7 @@ typedef enum pgssVersion
 	PGSS_V1_10,
 	PGSS_V1_11,
 	PGSS_V1_12,
+	PGSS_V1_13,
 } pgssVersion;
 
 typedef enum pgssStoreKind
@@ -210,6 +212,8 @@ typedef struct Counters
 											 * to be launched */
 	int64		parallel_workers_launched;	/* # of parallel workers actually
 											 * launched */
+	int64		generic_plan_calls; /* number of calls using a generic plan */
+	int64		custom_plan_calls;	/* number of calls using a custom plan */
 } Counters;
 
 /*
@@ -323,6 +327,7 @@ PG_FUNCTION_INFO_V1(pg_stat_statements_1_9);
 PG_FUNCTION_INFO_V1(pg_stat_statements_1_10);
 PG_FUNCTION_INFO_V1(pg_stat_statements_1_11);
 PG_FUNCTION_INFO_V1(pg_stat_statements_1_12);
+PG_FUNCTION_INFO_V1(pg_stat_statements_1_13);
 PG_FUNCTION_INFO_V1(pg_stat_statements);
 PG_FUNCTION_INFO_V1(pg_stat_statements_info);
 
@@ -355,7 +360,8 @@ static void pgss_store(const char *query, int64 queryId,
 					   const struct JitInstrumentation *jitusage,
 					   JumbleState *jstate,
 					   int parallel_workers_to_launch,
-					   int parallel_workers_launched);
+					   int parallel_workers_launched,
+					   CachedPlan *cplan);
 static void pg_stat_statements_internal(FunctionCallInfo fcinfo,
 										pgssVersion api_version,
 										bool showtext);
@@ -877,7 +883,8 @@ pgss_post_parse_analyze(ParseState *pstate, Query *query, JumbleState *jstate)
 				   NULL,
 				   jstate,
 				   0,
-				   0);
+				   0,
+				   NULL);
 }
 
 /*
@@ -957,7 +964,8 @@ pgss_planner(Query *parse,
 				   NULL,
 				   NULL,
 				   0,
-				   0);
+				   0,
+				   NULL);
 	}
 	else
 	{
@@ -1091,7 +1099,8 @@ pgss_ExecutorEnd(QueryDesc *queryDesc)
 				   queryDesc->estate->es_jit ? &queryDesc->estate->es_jit->instr : NULL,
 				   NULL,
 				   queryDesc->estate->es_parallel_workers_to_launch,
-				   queryDesc->estate->es_parallel_workers_launched);
+				   queryDesc->estate->es_parallel_workers_launched,
+				   queryDesc->cplan);
 	}
 
 	if (prev_ExecutorEnd)
@@ -1224,7 +1233,8 @@ pgss_ProcessUtility(PlannedStmt *pstmt, const char *queryString,
 				   NULL,
 				   NULL,
 				   0,
-				   0);
+				   0,
+				   NULL);
 	}
 	else
 	{
@@ -1287,7 +1297,8 @@ pgss_store(const char *query, int64 queryId,
 		   const struct JitInstrumentation *jitusage,
 		   JumbleState *jstate,
 		   int parallel_workers_to_launch,
-		   int parallel_workers_launched)
+		   int parallel_workers_launched,
+		   CachedPlan *cplan)
 {
 	pgssHashKey key;
 	pgssEntry  *entry;
@@ -1495,6 +1506,14 @@ pgss_store(const char *query, int64 queryId,
 		entry->counters.parallel_workers_to_launch += parallel_workers_to_launch;
 		entry->counters.parallel_workers_launched += parallel_workers_launched;
 
+		if (cplan)
+		{
+			if (cplan->status == PLAN_CACHE_STATUS_GENERIC_PLAN)
+				entry->counters.generic_plan_calls++;
+			if (cplan->status == PLAN_CACHE_STATUS_CUSTOM_PLAN)
+				entry->counters.custom_plan_calls++;
+		}
+
 		SpinLockRelease(&entry->mutex);
 	}
 
@@ -1562,7 +1581,8 @@ pg_stat_statements_reset(PG_FUNCTION_ARGS)
 #define PG_STAT_STATEMENTS_COLS_V1_10	43
 #define PG_STAT_STATEMENTS_COLS_V1_11	49
 #define PG_STAT_STATEMENTS_COLS_V1_12	52
-#define PG_STAT_STATEMENTS_COLS			52	/* maximum of above */
+#define PG_STAT_STATEMENTS_COLS_V1_13	54
+#define PG_STAT_STATEMENTS_COLS			54	/* maximum of above */
 
 /*
  * Retrieve statement statistics.
@@ -1574,6 +1594,16 @@ pg_stat_statements_reset(PG_FUNCTION_ARGS)
  * expected API version is identified by embedding it in the C name of the
  * function.  Unfortunately we weren't bright enough to do that for 1.1.
  */
+Datum
+pg_stat_statements_1_13(PG_FUNCTION_ARGS)
+{
+	bool		showtext = PG_GETARG_BOOL(0);
+
+	pg_stat_statements_internal(fcinfo, PGSS_V1_13, showtext);
+
+	return (Datum) 0;
+}
+
 Datum
 pg_stat_statements_1_12(PG_FUNCTION_ARGS)
 {
@@ -1732,6 +1762,10 @@ pg_stat_statements_internal(FunctionCallInfo fcinfo,
 			if (api_version != PGSS_V1_12)
 				elog(ERROR, "incorrect number of output arguments");
 			break;
+		case PG_STAT_STATEMENTS_COLS_V1_13:
+			if (api_version != PGSS_V1_13)
+				elog(ERROR, "incorrect number of output arguments");
+			break;
 		default:
 			elog(ERROR, "incorrect number of output arguments");
 	}
@@ -1984,6 +2018,11 @@ pg_stat_statements_internal(FunctionCallInfo fcinfo,
 			values[i++] = Int64GetDatumFast(tmp.parallel_workers_to_launch);
 			values[i++] = Int64GetDatumFast(tmp.parallel_workers_launched);
 		}
+		if (api_version >= PGSS_V1_13)
+		{
+			values[i++] = Int64GetDatumFast(tmp.generic_plan_calls);
+			values[i++] = Int64GetDatumFast(tmp.custom_plan_calls);
+		}
 		if (api_version >= PGSS_V1_11)
 		{
 			values[i++] = TimestampTzGetDatum(stats_since);
@@ -1999,6 +2038,7 @@ pg_stat_statements_internal(FunctionCallInfo fcinfo,
 					 api_version == PGSS_V1_10 ? PG_STAT_STATEMENTS_COLS_V1_10 :
 					 api_version == PGSS_V1_11 ? PG_STAT_STATEMENTS_COLS_V1_11 :
 					 api_version == PGSS_V1_12 ? PG_STAT_STATEMENTS_COLS_V1_12 :
+					 api_version == PGSS_V1_13 ? PG_STAT_STATEMENTS_COLS_V1_13 :
 					 -1 /* fail if you forget to update this assert */ ));
 
 		tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, values, nulls);
diff --git a/contrib/pg_stat_statements/pg_stat_statements.control b/contrib/pg_stat_statements/pg_stat_statements.control
index d45ebc12e360..2eee0ceffa89 100644
--- a/contrib/pg_stat_statements/pg_stat_statements.control
+++ b/contrib/pg_stat_statements/pg_stat_statements.control
@@ -1,5 +1,5 @@
 # pg_stat_statements extension
 comment = 'track planning and execution statistics of all SQL statements executed'
-default_version = '1.12'
+default_version = '1.13'
 module_pathname = '$libdir/pg_stat_statements'
 relocatable = true
diff --git a/contrib/pg_stat_statements/sql/plancache.sql b/contrib/pg_stat_statements/sql/plancache.sql
new file mode 100644
index 000000000000..97d84bfdbede
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/plancache.sql
@@ -0,0 +1,111 @@
+--
+-- Information related to plan cache
+--
+
+--
+-- Setup
+--
+CREATE OR REPLACE FUNCTION select_one_func(int) RETURNS VOID AS $$
+DECLARE
+    ret INT;
+BEGIN
+    SELECT $1 INTO ret;
+END;
+$$ LANGUAGE plpgsql;
+CREATE OR REPLACE PROCEDURE select_one_proc(int) AS $$
+DECLARE
+    ret INT;
+BEGIN
+    select $1 INTO ret;
+END;
+$$ LANGUAGE plpgsql;
+
+--
+-- plan cache counters for prepared statements
+--
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+PREPARE p1 AS SELECT $1;
+-- plan cache auto
+SET plan_cache_mode TO auto;
+EXECUTE p1(1);
+-- force generic plan
+SET plan_cache_mode TO force_generic_plan;
+EXECUTE p1(1);
+-- force custom plan
+SET plan_cache_mode TO force_custom_plan;
+EXECUTE p1(1);
+SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, query FROM pg_stat_statements
+    ORDER BY query COLLATE "C";
+DEALLOCATE p1;
+
+--
+-- plan cache counters for explain|explain (analyze) with prepared statements
+--
+SET pg_stat_statements.track = 'all';
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+PREPARE p1 AS SELECT $1;
+-- plan cache auto
+SET plan_cache_mode TO auto;
+EXPLAIN (COSTS OFF) EXECUTE p1(1);
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) EXECUTE p1(1);
+-- force generic plan
+SET plan_cache_mode TO force_generic_plan;
+EXPLAIN (COSTS OFF) EXECUTE p1(1);
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) EXECUTE p1(1);
+-- force custom plan
+SET plan_cache_mode TO force_custom_plan;
+EXPLAIN (COSTS OFF) EXECUTE p1(1);
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) EXECUTE p1(1);
+SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, query FROM pg_stat_statements
+    ORDER BY query COLLATE "C";
+RESET pg_stat_statements.track;
+DEALLOCATE p1;
+
+--
+-- plan cache counters for functions and procedures
+--
+SET pg_stat_statements.track = 'all';
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+-- plan cache auto
+SET plan_cache_mode TO auto;
+SELECT select_one_func(1);
+CALL select_one_proc(1);
+-- force generic plan
+SET plan_cache_mode TO force_generic_plan;
+SELECT select_one_func(1);
+CALL select_one_proc(1);
+-- force custom plan
+SET plan_cache_mode TO force_custom_plan;
+SELECT select_one_func(1);
+CALL select_one_proc(1);
+SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, query FROM pg_stat_statements
+    ORDER BY query COLLATE "C";
+
+--
+-- plan cache counters for explain|explain (analyze) with functions and procedures
+--
+SET pg_stat_statements.track = 'all';
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+-- plan cache auto
+SET plan_cache_mode TO auto;
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) SELECT select_one_func(1);
+EXPLAIN (COSTS OFF) SELECT select_one_func(1);
+CALL select_one_proc(1);
+-- force generic plan
+SET plan_cache_mode TO force_generic_plan;
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) SELECT select_one_func(1);
+EXPLAIN (COSTS OFF) SELECT select_one_func(1);
+CALL select_one_proc(1);
+-- force custom plan
+SET plan_cache_mode TO force_custom_plan;
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) SELECT select_one_func(1);
+EXPLAIN (COSTS OFF) SELECT select_one_func(1);
+CALL select_one_proc(1);
+SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, query FROM pg_stat_statements
+    ORDER BY query COLLATE "C";
+
+--
+-- Cleanup
+--
+DROP FUNCTION select_one_func(int);
+DROP PROCEDURE select_one_proc(int);
diff --git a/doc/src/sgml/pgstatstatements.sgml b/doc/src/sgml/pgstatstatements.sgml
index 7baa07dcdbf7..08b5fec9e10b 100644
--- a/doc/src/sgml/pgstatstatements.sgml
+++ b/doc/src/sgml/pgstatstatements.sgml
@@ -554,6 +554,24 @@
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>generic_plan_calls</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Number of times the statement was executed using a generic plan
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>custom_plan_calls</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Number of times the statement was executed using a custom plan
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>stats_since</structfield> <type>timestamp with time zone</type>
diff --git a/src/backend/commands/copyto.c b/src/backend/commands/copyto.c
index ea6f18f2c800..55a2719d7685 100644
--- a/src/backend/commands/copyto.c
+++ b/src/backend/commands/copyto.c
@@ -835,7 +835,7 @@ BeginCopyTo(ParseState *pstate,
 		((DR_copy *) dest)->cstate = cstate;
 
 		/* Create a QueryDesc requesting no output */
-		cstate->queryDesc = CreateQueryDesc(plan, pstate->p_sourcetext,
+		cstate->queryDesc = CreateQueryDesc(plan, NULL, pstate->p_sourcetext,
 											GetActiveSnapshot(),
 											InvalidSnapshot,
 											dest, NULL, NULL, 0);
diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c
index dfd2ab8e8628..18c8a11b70c4 100644
--- a/src/backend/commands/createas.c
+++ b/src/backend/commands/createas.c
@@ -334,7 +334,7 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt,
 		UpdateActiveSnapshotCommandId();
 
 		/* Create a QueryDesc, redirecting output to our tuple receiver */
-		queryDesc = CreateQueryDesc(plan, pstate->p_sourcetext,
+		queryDesc = CreateQueryDesc(plan, NULL, pstate->p_sourcetext,
 									GetActiveSnapshot(), InvalidSnapshot,
 									dest, params, queryEnv, 0);
 
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 7e2792ead715..b2b998bc2db8 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -369,7 +369,7 @@ standard_ExplainOneQuery(Query *query, int cursorOptions,
 	}
 
 	/* run it (if needed) and produce output */
-	ExplainOnePlan(plan, into, es, queryString, params, queryEnv,
+	ExplainOnePlan(plan, NULL, into, es, queryString, params, queryEnv,
 				   &planduration, (es->buffers ? &bufusage : NULL),
 				   es->memory ? &mem_counters : NULL);
 }
@@ -491,7 +491,7 @@ ExplainOneUtility(Node *utilityStmt, IntoClause *into, ExplainState *es,
  * to call it.
  */
 void
-ExplainOnePlan(PlannedStmt *plannedstmt, IntoClause *into, ExplainState *es,
+ExplainOnePlan(PlannedStmt *plannedstmt, CachedPlan *cplan, IntoClause *into, ExplainState *es,
 			   const char *queryString, ParamListInfo params,
 			   QueryEnvironment *queryEnv, const instr_time *planduration,
 			   const BufferUsage *bufusage,
@@ -547,7 +547,7 @@ ExplainOnePlan(PlannedStmt *plannedstmt, IntoClause *into, ExplainState *es,
 		dest = None_Receiver;
 
 	/* Create a QueryDesc for the query */
-	queryDesc = CreateQueryDesc(plannedstmt, queryString,
+	queryDesc = CreateQueryDesc(plannedstmt, cplan, queryString,
 								GetActiveSnapshot(), InvalidSnapshot,
 								dest, params, queryEnv, instrument_option);
 
diff --git a/src/backend/commands/extension.c b/src/backend/commands/extension.c
index e6f9ab6dfd66..bc3e7bb808fa 100644
--- a/src/backend/commands/extension.c
+++ b/src/backend/commands/extension.c
@@ -993,6 +993,7 @@ execute_sql_string(const char *sql, const char *filename)
 				QueryDesc  *qdesc;
 
 				qdesc = CreateQueryDesc(stmt,
+										NULL,
 										sql,
 										GetActiveSnapshot(), NULL,
 										dest, NULL, NULL, 0);
diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index 27c2cb26ef5f..c9e7b6f5195e 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -438,7 +438,7 @@ refresh_matview_datafill(DestReceiver *dest, Query *query,
 	UpdateActiveSnapshotCommandId();
 
 	/* Create a QueryDesc, redirecting output to our tuple receiver */
-	queryDesc = CreateQueryDesc(plan, queryString,
+	queryDesc = CreateQueryDesc(plan, NULL, queryString,
 								GetActiveSnapshot(), InvalidSnapshot,
 								dest, NULL, NULL, 0);
 
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a26..a5f3a662e514 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -657,7 +657,7 @@ ExplainExecuteQuery(ExecuteStmt *execstmt, IntoClause *into, ExplainState *es,
 		PlannedStmt *pstmt = lfirst_node(PlannedStmt, p);
 
 		if (pstmt->commandType != CMD_UTILITY)
-			ExplainOnePlan(pstmt, into, es, query_string, paramLI, pstate->p_queryEnv,
+			ExplainOnePlan(pstmt, cplan, into, es, query_string, paramLI, pstate->p_queryEnv,
 						   &planduration, (es->buffers ? &bufusage : NULL),
 						   es->memory ? &mem_counters : NULL);
 		else
diff --git a/src/backend/executor/execParallel.c b/src/backend/executor/execParallel.c
index f3e77bda2790..58b1c09223a8 100644
--- a/src/backend/executor/execParallel.c
+++ b/src/backend/executor/execParallel.c
@@ -1280,6 +1280,7 @@ ExecParallelGetQueryDesc(shm_toc *toc, DestReceiver *receiver,
 
 	/* Create a QueryDesc for the query. */
 	return CreateQueryDesc(pstmt,
+						   NULL,
 						   queryString,
 						   GetActiveSnapshot(), InvalidSnapshot,
 						   receiver, paramLI, NULL, instrument_options);
diff --git a/src/backend/executor/functions.c b/src/backend/executor/functions.c
index 359aafea681b..635ce762eb37 100644
--- a/src/backend/executor/functions.c
+++ b/src/backend/executor/functions.c
@@ -1339,6 +1339,7 @@ postquel_start(execution_state *es, SQLFunctionCachePtr fcache)
 		dest = None_Receiver;
 
 	es->qd = CreateQueryDesc(es->stmt,
+							 NULL,
 							 fcache->func->src,
 							 GetActiveSnapshot(),
 							 InvalidSnapshot,
diff --git a/src/backend/executor/spi.c b/src/backend/executor/spi.c
index ecb2e4ccaa1c..5ffff06ec0ef 100644
--- a/src/backend/executor/spi.c
+++ b/src/backend/executor/spi.c
@@ -2690,6 +2690,7 @@ _SPI_execute_plan(SPIPlanPtr plan, const SPIExecuteOptions *options,
 					snap = InvalidSnapshot;
 
 				qdesc = CreateQueryDesc(stmt,
+										cplan,
 										plansource->query_string,
 										snap, crosscheck_snapshot,
 										dest,
diff --git a/src/backend/tcop/pquery.c b/src/backend/tcop/pquery.c
index d1593f38b35f..dc4f5ce9d5e7 100644
--- a/src/backend/tcop/pquery.c
+++ b/src/backend/tcop/pquery.c
@@ -26,6 +26,7 @@
 #include "tcop/pquery.h"
 #include "tcop/utility.h"
 #include "utils/memutils.h"
+#include "utils/plancache.h"
 #include "utils/snapmgr.h"
 
 
@@ -37,6 +38,7 @@ Portal		ActivePortal = NULL;
 
 
 static void ProcessQuery(PlannedStmt *plan,
+						 CachedPlan *cplan,
 						 const char *sourceText,
 						 ParamListInfo params,
 						 QueryEnvironment *queryEnv,
@@ -66,6 +68,7 @@ static void DoPortalRewind(Portal portal);
  */
 QueryDesc *
 CreateQueryDesc(PlannedStmt *plannedstmt,
+				CachedPlan *cplan,
 				const char *sourceText,
 				Snapshot snapshot,
 				Snapshot crosscheck_snapshot,
@@ -93,6 +96,8 @@ CreateQueryDesc(PlannedStmt *plannedstmt,
 	qd->planstate = NULL;
 	qd->totaltime = NULL;
 
+	qd->cplan = cplan;
+
 	/* not yet executed */
 	qd->already_executed = false;
 
@@ -135,6 +140,7 @@ FreeQueryDesc(QueryDesc *qdesc)
  */
 static void
 ProcessQuery(PlannedStmt *plan,
+			 CachedPlan *cplan,
 			 const char *sourceText,
 			 ParamListInfo params,
 			 QueryEnvironment *queryEnv,
@@ -146,7 +152,7 @@ ProcessQuery(PlannedStmt *plan,
 	/*
 	 * Create the QueryDesc object
 	 */
-	queryDesc = CreateQueryDesc(plan, sourceText,
+	queryDesc = CreateQueryDesc(plan, cplan, sourceText,
 								GetActiveSnapshot(), InvalidSnapshot,
 								dest, params, queryEnv, 0);
 
@@ -494,6 +500,7 @@ PortalStart(Portal portal, ParamListInfo params,
 				 * the destination to DestNone.
 				 */
 				queryDesc = CreateQueryDesc(linitial_node(PlannedStmt, portal->stmts),
+											portal->cplan,
 											portal->sourceText,
 											GetActiveSnapshot(),
 											InvalidSnapshot,
@@ -1270,6 +1277,7 @@ PortalRunMulti(Portal portal,
 			{
 				/* statement can set tag string */
 				ProcessQuery(pstmt,
+							 portal->cplan,
 							 portal->sourceText,
 							 portal->portalParams,
 							 portal->queryEnv,
@@ -1279,6 +1287,7 @@ PortalRunMulti(Portal portal,
 			{
 				/* stmt added by rewrite cannot set tag */
 				ProcessQuery(pstmt,
+							 portal->cplan,
 							 portal->sourceText,
 							 portal->portalParams,
 							 portal->queryEnv,
diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c
index 89a1c79e984d..63df3ecd881e 100644
--- a/src/backend/utils/cache/plancache.c
+++ b/src/backend/utils/cache/plancache.c
@@ -1140,6 +1140,7 @@ BuildCachedPlan(CachedPlanSource *plansource, List *qlist,
 	plan->is_oneshot = plansource->is_oneshot;
 	plan->is_saved = false;
 	plan->is_valid = true;
+	plan->status = PLAN_CACHE_STATUS_UNKNOWN;
 
 	/* assign generation number to new plan */
 	plan->generation = ++(plansource->generation);
@@ -1358,10 +1359,12 @@ GetCachedPlan(CachedPlanSource *plansource, ParamListInfo boundParams,
 		plansource->total_custom_cost += cached_plan_cost(plan, true);
 
 		plansource->num_custom_plans++;
+		plan->status = PLAN_CACHE_STATUS_CUSTOM_PLAN;
 	}
 	else
 	{
 		plansource->num_generic_plans++;
+		plan->status = PLAN_CACHE_STATUS_GENERIC_PLAN;
 	}
 
 	Assert(plan != NULL);
diff --git a/src/include/commands/explain.h b/src/include/commands/explain.h
index 3b122f79ed84..36579590ec2b 100644
--- a/src/include/commands/explain.h
+++ b/src/include/commands/explain.h
@@ -63,9 +63,13 @@ extern void ExplainOneUtility(Node *utilityStmt, IntoClause *into,
 							  struct ExplainState *es, ParseState *pstate,
 							  ParamListInfo params);
 
-extern void ExplainOnePlan(PlannedStmt *plannedstmt, IntoClause *into,
-						   struct ExplainState *es, const char *queryString,
-						   ParamListInfo params, QueryEnvironment *queryEnv,
+extern void ExplainOnePlan(PlannedStmt *plannedstmt,
+						   CachedPlan *cplan,
+						   IntoClause *into,
+						   struct ExplainState *es,
+						   const char *queryString,
+						   ParamListInfo params,
+						   QueryEnvironment *queryEnv,
 						   const instr_time *planduration,
 						   const BufferUsage *bufusage,
 						   const MemoryContextCounters *mem_counters);
diff --git a/src/include/executor/execdesc.h b/src/include/executor/execdesc.h
index 86db3dc8d0de..f7cbe0981a54 100644
--- a/src/include/executor/execdesc.h
+++ b/src/include/executor/execdesc.h
@@ -17,6 +17,7 @@
 
 #include "nodes/execnodes.h"
 #include "tcop/dest.h"
+#include "utils/plancache.h"
 
 
 /* ----------------
@@ -35,6 +36,7 @@ typedef struct QueryDesc
 	/* These fields are provided by CreateQueryDesc */
 	CmdType		operation;		/* CMD_SELECT, CMD_UPDATE, etc. */
 	PlannedStmt *plannedstmt;	/* planner's output (could be utility, too) */
+	CachedPlan *cplan;			/* CachedPlan that supplies the plannedstmt */
 	const char *sourceText;		/* source text of the query */
 	Snapshot	snapshot;		/* snapshot to use for query */
 	Snapshot	crosscheck_snapshot;	/* crosscheck for RI update/delete */
@@ -57,6 +59,7 @@ typedef struct QueryDesc
 
 /* in pquery.c */
 extern QueryDesc *CreateQueryDesc(PlannedStmt *plannedstmt,
+								  CachedPlan *cplan,
 								  const char *sourceText,
 								  Snapshot snapshot,
 								  Snapshot crosscheck_snapshot,
diff --git a/src/include/utils/plancache.h b/src/include/utils/plancache.h
index 1baa6d50bfd7..b9d009911403 100644
--- a/src/include/utils/plancache.h
+++ b/src/include/utils/plancache.h
@@ -38,6 +38,13 @@ typedef enum
 /* GUC parameter */
 extern PGDLLIMPORT int plan_cache_mode;
 
+typedef enum
+{
+	PLAN_CACHE_STATUS_UNKNOWN = 0,
+	PLAN_CACHE_STATUS_GENERIC_PLAN,
+	PLAN_CACHE_STATUS_CUSTOM_PLAN,
+}			PlanCacheStatus;
+
 /* Optional callback to editorialize on rewritten parse trees */
 typedef void (*PostRewriteHook) (List *querytree_list, void *arg);
 
@@ -163,6 +170,7 @@ typedef struct CachedPlan
 	bool		is_oneshot;		/* is it a "oneshot" plan? */
 	bool		is_saved;		/* is CachedPlan in a long-lived context? */
 	bool		is_valid;		/* is the stmt_list currently valid? */
+	PlanCacheStatus status;		/* status of the cached plan */
 	Oid			planRoleId;		/* Role ID the plan was created for */
 	bool		dependsOnRole;	/* is plan specific to that role? */
 	TransactionId saved_xmin;	/* if valid, replan when TransactionXmin
-- 
2.39.5 (Apple Git-154)

#21Sami Imseih
samimseih@gmail.com
In reply to: Sami Imseih (#20)
1 attachment(s)
Re: track generic and custom plans in pg_stat_statements

rebased patch.

Only changes to the tests due to the revert of nested query
tracking in f85f6ab051b

Regards,

Sami

Attachments:

v10-0001-Add-plan_cache-counters-to-pg_stat_statements.patchapplication/octet-stream; name=v10-0001-Add-plan_cache-counters-to-pg_stat_statements.patchDownload
From 7325f2e83b30ce3c26ae6fb4f6be73c9578b1f0d Mon Sep 17 00:00:00 2001
From: Sami Imseih <simseih@amazon.com>
Date: Thu, 5 Jun 2025 15:58:07 -0500
Subject: [PATCH 1/1] Add plan_cache counters to pg_stat_statements.

This adds the ability for users to track how many
times a prepared statement was executed using either
a generic plan or a custom plan, by introducing two new
counters in pg_stat_statements: generic_plan_calls and
custom_plan_calls.

Discussion: https://www.postgresql.org/message-id/flat/CAA5RZ0uFw8Y9GCFvafhC=OA8NnMqVZyzXPfv_EePOt+iv1T-qQ@mail.gmail.com
Reviewers: Greg Sabino Mullane, Ilia Evdokimov, Nikolay Samokhvalov
---
 contrib/pg_stat_statements/Makefile           |   3 +-
 .../pg_stat_statements/expected/plancache.out | 251 ++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |   2 +
 .../pg_stat_statements--1.12--1.13.sql        |  78 ++++++
 .../pg_stat_statements/pg_stat_statements.c   |  54 +++-
 .../pg_stat_statements.control                |   2 +-
 contrib/pg_stat_statements/sql/plancache.sql  | 111 ++++++++
 doc/src/sgml/pgstatstatements.sgml            |  18 ++
 src/backend/commands/copyto.c                 |   2 +-
 src/backend/commands/createas.c               |   2 +-
 src/backend/commands/explain.c                |   6 +-
 src/backend/commands/extension.c              |   1 +
 src/backend/commands/matview.c                |   2 +-
 src/backend/commands/prepare.c                |   2 +-
 src/backend/executor/execParallel.c           |   1 +
 src/backend/executor/functions.c              |   1 +
 src/backend/executor/spi.c                    |   1 +
 src/backend/tcop/pquery.c                     |  11 +-
 src/backend/utils/cache/plancache.c           |   3 +
 src/include/commands/explain.h                |  10 +-
 src/include/executor/execdesc.h               |   3 +
 src/include/utils/plancache.h                 |   8 +
 22 files changed, 552 insertions(+), 20 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/plancache.out
 create mode 100644 contrib/pg_stat_statements/pg_stat_statements--1.12--1.13.sql
 create mode 100644 contrib/pg_stat_statements/sql/plancache.sql

diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index b2bd8794d2a..996a5fac448 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -7,6 +7,7 @@ OBJS = \
 
 EXTENSION = pg_stat_statements
 DATA = pg_stat_statements--1.4.sql \
+	pg_stat_statements--1.12--1.13.sql \
 	pg_stat_statements--1.11--1.12.sql pg_stat_statements--1.10--1.11.sql \
 	pg_stat_statements--1.9--1.10.sql pg_stat_statements--1.8--1.9.sql \
 	pg_stat_statements--1.7--1.8.sql pg_stat_statements--1.6--1.7.sql \
@@ -20,7 +21,7 @@ LDFLAGS_SL += $(filter -lm, $(LIBS))
 REGRESS_OPTS = --temp-config $(top_srcdir)/contrib/pg_stat_statements/pg_stat_statements.conf
 REGRESS = select dml cursors utility level_tracking planning \
 	user_activity wal entry_timestamp privileges extended \
-	parallel cleanup oldextversions squashing
+	parallel cleanup oldextversions squashing plancache
 # Disabled because these tests require "shared_preload_libraries=pg_stat_statements",
 # which typical installcheck users do not have (e.g. buildfarm clients).
 NO_INSTALLCHECK = 1
diff --git a/contrib/pg_stat_statements/expected/plancache.out b/contrib/pg_stat_statements/expected/plancache.out
new file mode 100644
index 00000000000..856a59b9f42
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/plancache.out
@@ -0,0 +1,251 @@
+--
+-- Information related to plan cache
+--
+--
+-- Setup
+--
+CREATE OR REPLACE FUNCTION select_one_func(int) RETURNS VOID AS $$
+DECLARE
+    ret INT;
+BEGIN
+    SELECT $1 INTO ret;
+END;
+$$ LANGUAGE plpgsql;
+CREATE OR REPLACE PROCEDURE select_one_proc(int) AS $$
+DECLARE
+    ret INT;
+BEGIN
+    select $1 INTO ret;
+END;
+$$ LANGUAGE plpgsql;
+--
+-- plan cache counters for prepared statements
+--
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+PREPARE p1 AS SELECT $1;
+-- plan cache auto
+SET plan_cache_mode TO auto;
+EXECUTE p1(1);
+ ?column? 
+----------
+ 1
+(1 row)
+
+-- force generic plan
+SET plan_cache_mode TO force_generic_plan;
+EXECUTE p1(1);
+ ?column? 
+----------
+ 1
+(1 row)
+
+-- force custom plan
+SET plan_cache_mode TO force_custom_plan;
+EXECUTE p1(1);
+ ?column? 
+----------
+ 1
+(1 row)
+
+SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, query FROM pg_stat_statements
+    ORDER BY query COLLATE "C";
+ calls | generic_plan_calls | custom_plan_calls | toplevel |                       query                        
+-------+--------------------+-------------------+----------+----------------------------------------------------
+     3 |                  1 |                 2 | t        | PREPARE p1 AS SELECT $1
+     1 |                  0 |                 0 | t        | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+     3 |                  0 |                 0 | t        | SET plan_cache_mode TO $1
+(3 rows)
+
+DEALLOCATE p1;
+--
+-- plan cache counters for explain|explain (analyze) with prepared statements
+--
+SET pg_stat_statements.track = 'all';
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+PREPARE p1 AS SELECT $1;
+-- plan cache auto
+SET plan_cache_mode TO auto;
+EXPLAIN (COSTS OFF) EXECUTE p1(1);
+ QUERY PLAN 
+------------
+ Result
+(1 row)
+
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) EXECUTE p1(1);
+            QUERY PLAN             
+-----------------------------------
+ Result (actual rows=1.00 loops=1)
+(1 row)
+
+-- force generic plan
+SET plan_cache_mode TO force_generic_plan;
+EXPLAIN (COSTS OFF) EXECUTE p1(1);
+ QUERY PLAN 
+------------
+ Result
+(1 row)
+
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) EXECUTE p1(1);
+            QUERY PLAN             
+-----------------------------------
+ Result (actual rows=1.00 loops=1)
+(1 row)
+
+-- force custom plan
+SET plan_cache_mode TO force_custom_plan;
+EXPLAIN (COSTS OFF) EXECUTE p1(1);
+ QUERY PLAN 
+------------
+ Result
+(1 row)
+
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) EXECUTE p1(1);
+            QUERY PLAN             
+-----------------------------------
+ Result (actual rows=1.00 loops=1)
+(1 row)
+
+SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, query FROM pg_stat_statements
+    ORDER BY query COLLATE "C";
+ calls | generic_plan_calls | custom_plan_calls | toplevel |                                      query                                       
+-------+--------------------+-------------------+----------+----------------------------------------------------------------------------------
+     3 |                  0 |                 0 | t        | EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) EXECUTE p1(1)
+     3 |                  0 |                 0 | t        | EXPLAIN (COSTS OFF) EXECUTE p1(1)
+     6 |                  2 |                 4 | f        | PREPARE p1 AS SELECT $1
+     1 |                  0 |                 0 | t        | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+     3 |                  0 |                 0 | t        | SET plan_cache_mode TO $1
+(5 rows)
+
+RESET pg_stat_statements.track;
+DEALLOCATE p1;
+--
+-- plan cache counters for functions and procedures
+--
+SET pg_stat_statements.track = 'all';
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- plan cache auto
+SET plan_cache_mode TO auto;
+SELECT select_one_func(1);
+ select_one_func 
+-----------------
+ 
+(1 row)
+
+CALL select_one_proc(1);
+-- force generic plan
+SET plan_cache_mode TO force_generic_plan;
+SELECT select_one_func(1);
+ select_one_func 
+-----------------
+ 
+(1 row)
+
+CALL select_one_proc(1);
+-- force custom plan
+SET plan_cache_mode TO force_custom_plan;
+SELECT select_one_func(1);
+ select_one_func 
+-----------------
+ 
+(1 row)
+
+CALL select_one_proc(1);
+SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, query FROM pg_stat_statements
+    ORDER BY query COLLATE "C";
+ calls | generic_plan_calls | custom_plan_calls | toplevel |                       query                        
+-------+--------------------+-------------------+----------+----------------------------------------------------
+     3 |                  0 |                 0 | t        | CALL select_one_proc($1)
+     6 |                  2 |                 4 | f        | SELECT $1
+     1 |                  0 |                 0 | t        | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+     3 |                  0 |                 0 | t        | SELECT select_one_func($1)
+     3 |                  0 |                 0 | t        | SET plan_cache_mode TO $1
+(5 rows)
+
+--
+-- plan cache counters for explain|explain (analyze) with functions and procedures
+--
+SET pg_stat_statements.track = 'all';
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- plan cache auto
+SET plan_cache_mode TO auto;
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) SELECT select_one_func(1);
+            QUERY PLAN             
+-----------------------------------
+ Result (actual rows=1.00 loops=1)
+(1 row)
+
+EXPLAIN (COSTS OFF) SELECT select_one_func(1);
+ QUERY PLAN 
+------------
+ Result
+(1 row)
+
+CALL select_one_proc(1);
+-- force generic plan
+SET plan_cache_mode TO force_generic_plan;
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) SELECT select_one_func(1);
+            QUERY PLAN             
+-----------------------------------
+ Result (actual rows=1.00 loops=1)
+(1 row)
+
+EXPLAIN (COSTS OFF) SELECT select_one_func(1);
+ QUERY PLAN 
+------------
+ Result
+(1 row)
+
+CALL select_one_proc(1);
+-- force custom plan
+SET plan_cache_mode TO force_custom_plan;
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) SELECT select_one_func(1);
+            QUERY PLAN             
+-----------------------------------
+ Result (actual rows=1.00 loops=1)
+(1 row)
+
+EXPLAIN (COSTS OFF) SELECT select_one_func(1);
+ QUERY PLAN 
+------------
+ Result
+(1 row)
+
+CALL select_one_proc(1);
+SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, query FROM pg_stat_statements
+    ORDER BY query COLLATE "C";
+ calls | generic_plan_calls | custom_plan_calls | toplevel |                                             query                                              
+-------+--------------------+-------------------+----------+------------------------------------------------------------------------------------------------
+     3 |                  0 |                 0 | t        | CALL select_one_proc($1)
+     3 |                  0 |                 0 | t        | EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) SELECT select_one_func($1)
+     6 |                  0 |                 0 | f        | EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) SELECT select_one_func($1);
+     3 |                  0 |                 0 | t        | EXPLAIN (COSTS OFF) SELECT select_one_func($1)
+     6 |                  2 |                 4 | f        | SELECT $1
+     1 |                  0 |                 0 | t        | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+     3 |                  0 |                 0 | t        | SET plan_cache_mode TO $1
+(7 rows)
+
+--
+-- Cleanup
+--
+DROP FUNCTION select_one_func(int);
+DROP PROCEDURE select_one_proc(int);
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 01a6cbdcf61..110fb82fe12 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -21,6 +21,7 @@ contrib_targets += pg_stat_statements
 install_data(
   'pg_stat_statements.control',
   'pg_stat_statements--1.4.sql',
+  'pg_stat_statements--1.12--1.13.sql',
   'pg_stat_statements--1.11--1.12.sql',
   'pg_stat_statements--1.10--1.11.sql',
   'pg_stat_statements--1.9--1.10.sql',
@@ -57,6 +58,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'plancache',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/pg_stat_statements--1.12--1.13.sql b/contrib/pg_stat_statements/pg_stat_statements--1.12--1.13.sql
new file mode 100644
index 00000000000..2f0eaf14ec3
--- /dev/null
+++ b/contrib/pg_stat_statements/pg_stat_statements--1.12--1.13.sql
@@ -0,0 +1,78 @@
+/* contrib/pg_stat_statements/pg_stat_statements--1.12--1.13.sql */
+
+-- complain if script is sourced in psql, rather than via ALTER EXTENSION
+\echo Use "ALTER EXTENSION pg_stat_statements UPDATE TO '1.13'" to load this file. \quit
+
+/* First we have to remove them from the extension */
+ALTER EXTENSION pg_stat_statements DROP VIEW pg_stat_statements;
+ALTER EXTENSION pg_stat_statements DROP FUNCTION pg_stat_statements(boolean);
+
+/* Then we can drop them */
+DROP VIEW pg_stat_statements;
+DROP FUNCTION pg_stat_statements(boolean);
+
+/* Now redefine */
+CREATE FUNCTION pg_stat_statements(IN showtext boolean,
+    OUT userid oid,
+    OUT dbid oid,
+    OUT toplevel bool,
+    OUT queryid bigint,
+    OUT query text,
+    OUT plans int8,
+    OUT total_plan_time float8,
+    OUT min_plan_time float8,
+    OUT max_plan_time float8,
+    OUT mean_plan_time float8,
+    OUT stddev_plan_time float8,
+    OUT calls int8,
+    OUT total_exec_time float8,
+    OUT min_exec_time float8,
+    OUT max_exec_time float8,
+    OUT mean_exec_time float8,
+    OUT stddev_exec_time float8,
+    OUT rows int8,
+    OUT shared_blks_hit int8,
+    OUT shared_blks_read int8,
+    OUT shared_blks_dirtied int8,
+    OUT shared_blks_written int8,
+    OUT local_blks_hit int8,
+    OUT local_blks_read int8,
+    OUT local_blks_dirtied int8,
+    OUT local_blks_written int8,
+    OUT temp_blks_read int8,
+    OUT temp_blks_written int8,
+    OUT shared_blk_read_time float8,
+    OUT shared_blk_write_time float8,
+    OUT local_blk_read_time float8,
+    OUT local_blk_write_time float8,
+    OUT temp_blk_read_time float8,
+    OUT temp_blk_write_time float8,
+    OUT wal_records int8,
+    OUT wal_fpi int8,
+    OUT wal_bytes numeric,
+    OUT wal_buffers_full int8,
+    OUT jit_functions int8,
+    OUT jit_generation_time float8,
+    OUT jit_inlining_count int8,
+    OUT jit_inlining_time float8,
+    OUT jit_optimization_count int8,
+    OUT jit_optimization_time float8,
+    OUT jit_emission_count int8,
+    OUT jit_emission_time float8,
+    OUT jit_deform_count int8,
+    OUT jit_deform_time float8,
+    OUT parallel_workers_to_launch int8,
+    OUT parallel_workers_launched int8,
+    OUT generic_plan_calls int8,
+    OUT custom_plan_calls int8,
+    OUT stats_since timestamp with time zone,
+    OUT minmax_stats_since timestamp with time zone
+)
+RETURNS SETOF record
+AS 'MODULE_PATHNAME', 'pg_stat_statements_1_13'
+LANGUAGE C STRICT VOLATILE PARALLEL SAFE;
+
+CREATE VIEW pg_stat_statements AS
+  SELECT * FROM pg_stat_statements(true);
+
+GRANT SELECT ON pg_stat_statements TO PUBLIC;
diff --git a/contrib/pg_stat_statements/pg_stat_statements.c b/contrib/pg_stat_statements/pg_stat_statements.c
index e7857f81ec0..4f05da380b3 100644
--- a/contrib/pg_stat_statements/pg_stat_statements.c
+++ b/contrib/pg_stat_statements/pg_stat_statements.c
@@ -69,6 +69,7 @@
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/memutils.h"
+#include "utils/plancache.h"
 #include "utils/timestamp.h"
 
 PG_MODULE_MAGIC_EXT(
@@ -114,6 +115,7 @@ typedef enum pgssVersion
 	PGSS_V1_10,
 	PGSS_V1_11,
 	PGSS_V1_12,
+	PGSS_V1_13,
 } pgssVersion;
 
 typedef enum pgssStoreKind
@@ -210,6 +212,8 @@ typedef struct Counters
 											 * to be launched */
 	int64		parallel_workers_launched;	/* # of parallel workers actually
 											 * launched */
+	int64		generic_plan_calls; /* number of calls using a generic plan */
+	int64		custom_plan_calls;	/* number of calls using a custom plan */
 } Counters;
 
 /*
@@ -323,6 +327,7 @@ PG_FUNCTION_INFO_V1(pg_stat_statements_1_9);
 PG_FUNCTION_INFO_V1(pg_stat_statements_1_10);
 PG_FUNCTION_INFO_V1(pg_stat_statements_1_11);
 PG_FUNCTION_INFO_V1(pg_stat_statements_1_12);
+PG_FUNCTION_INFO_V1(pg_stat_statements_1_13);
 PG_FUNCTION_INFO_V1(pg_stat_statements);
 PG_FUNCTION_INFO_V1(pg_stat_statements_info);
 
@@ -355,7 +360,8 @@ static void pgss_store(const char *query, int64 queryId,
 					   const struct JitInstrumentation *jitusage,
 					   JumbleState *jstate,
 					   int parallel_workers_to_launch,
-					   int parallel_workers_launched);
+					   int parallel_workers_launched,
+					   CachedPlan *cplan);
 static void pg_stat_statements_internal(FunctionCallInfo fcinfo,
 										pgssVersion api_version,
 										bool showtext);
@@ -877,7 +883,8 @@ pgss_post_parse_analyze(ParseState *pstate, Query *query, JumbleState *jstate)
 				   NULL,
 				   jstate,
 				   0,
-				   0);
+				   0,
+				   NULL);
 }
 
 /*
@@ -957,7 +964,8 @@ pgss_planner(Query *parse,
 				   NULL,
 				   NULL,
 				   0,
-				   0);
+				   0,
+				   NULL);
 	}
 	else
 	{
@@ -1091,7 +1099,8 @@ pgss_ExecutorEnd(QueryDesc *queryDesc)
 				   queryDesc->estate->es_jit ? &queryDesc->estate->es_jit->instr : NULL,
 				   NULL,
 				   queryDesc->estate->es_parallel_workers_to_launch,
-				   queryDesc->estate->es_parallel_workers_launched);
+				   queryDesc->estate->es_parallel_workers_launched,
+				   queryDesc->cplan);
 	}
 
 	if (prev_ExecutorEnd)
@@ -1224,7 +1233,8 @@ pgss_ProcessUtility(PlannedStmt *pstmt, const char *queryString,
 				   NULL,
 				   NULL,
 				   0,
-				   0);
+				   0,
+				   NULL);
 	}
 	else
 	{
@@ -1287,7 +1297,8 @@ pgss_store(const char *query, int64 queryId,
 		   const struct JitInstrumentation *jitusage,
 		   JumbleState *jstate,
 		   int parallel_workers_to_launch,
-		   int parallel_workers_launched)
+		   int parallel_workers_launched,
+		   CachedPlan *cplan)
 {
 	pgssHashKey key;
 	pgssEntry  *entry;
@@ -1495,6 +1506,14 @@ pgss_store(const char *query, int64 queryId,
 		entry->counters.parallel_workers_to_launch += parallel_workers_to_launch;
 		entry->counters.parallel_workers_launched += parallel_workers_launched;
 
+		if (cplan)
+		{
+			if (cplan->status == PLAN_CACHE_STATUS_GENERIC_PLAN)
+				entry->counters.generic_plan_calls++;
+			if (cplan->status == PLAN_CACHE_STATUS_CUSTOM_PLAN)
+				entry->counters.custom_plan_calls++;
+		}
+
 		SpinLockRelease(&entry->mutex);
 	}
 
@@ -1562,7 +1581,8 @@ pg_stat_statements_reset(PG_FUNCTION_ARGS)
 #define PG_STAT_STATEMENTS_COLS_V1_10	43
 #define PG_STAT_STATEMENTS_COLS_V1_11	49
 #define PG_STAT_STATEMENTS_COLS_V1_12	52
-#define PG_STAT_STATEMENTS_COLS			52	/* maximum of above */
+#define PG_STAT_STATEMENTS_COLS_V1_13	54
+#define PG_STAT_STATEMENTS_COLS			54	/* maximum of above */
 
 /*
  * Retrieve statement statistics.
@@ -1574,6 +1594,16 @@ pg_stat_statements_reset(PG_FUNCTION_ARGS)
  * expected API version is identified by embedding it in the C name of the
  * function.  Unfortunately we weren't bright enough to do that for 1.1.
  */
+Datum
+pg_stat_statements_1_13(PG_FUNCTION_ARGS)
+{
+	bool		showtext = PG_GETARG_BOOL(0);
+
+	pg_stat_statements_internal(fcinfo, PGSS_V1_13, showtext);
+
+	return (Datum) 0;
+}
+
 Datum
 pg_stat_statements_1_12(PG_FUNCTION_ARGS)
 {
@@ -1732,6 +1762,10 @@ pg_stat_statements_internal(FunctionCallInfo fcinfo,
 			if (api_version != PGSS_V1_12)
 				elog(ERROR, "incorrect number of output arguments");
 			break;
+		case PG_STAT_STATEMENTS_COLS_V1_13:
+			if (api_version != PGSS_V1_13)
+				elog(ERROR, "incorrect number of output arguments");
+			break;
 		default:
 			elog(ERROR, "incorrect number of output arguments");
 	}
@@ -1984,6 +2018,11 @@ pg_stat_statements_internal(FunctionCallInfo fcinfo,
 			values[i++] = Int64GetDatumFast(tmp.parallel_workers_to_launch);
 			values[i++] = Int64GetDatumFast(tmp.parallel_workers_launched);
 		}
+		if (api_version >= PGSS_V1_13)
+		{
+			values[i++] = Int64GetDatumFast(tmp.generic_plan_calls);
+			values[i++] = Int64GetDatumFast(tmp.custom_plan_calls);
+		}
 		if (api_version >= PGSS_V1_11)
 		{
 			values[i++] = TimestampTzGetDatum(stats_since);
@@ -1999,6 +2038,7 @@ pg_stat_statements_internal(FunctionCallInfo fcinfo,
 					 api_version == PGSS_V1_10 ? PG_STAT_STATEMENTS_COLS_V1_10 :
 					 api_version == PGSS_V1_11 ? PG_STAT_STATEMENTS_COLS_V1_11 :
 					 api_version == PGSS_V1_12 ? PG_STAT_STATEMENTS_COLS_V1_12 :
+					 api_version == PGSS_V1_13 ? PG_STAT_STATEMENTS_COLS_V1_13 :
 					 -1 /* fail if you forget to update this assert */ ));
 
 		tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, values, nulls);
diff --git a/contrib/pg_stat_statements/pg_stat_statements.control b/contrib/pg_stat_statements/pg_stat_statements.control
index d45ebc12e36..2eee0ceffa8 100644
--- a/contrib/pg_stat_statements/pg_stat_statements.control
+++ b/contrib/pg_stat_statements/pg_stat_statements.control
@@ -1,5 +1,5 @@
 # pg_stat_statements extension
 comment = 'track planning and execution statistics of all SQL statements executed'
-default_version = '1.12'
+default_version = '1.13'
 module_pathname = '$libdir/pg_stat_statements'
 relocatable = true
diff --git a/contrib/pg_stat_statements/sql/plancache.sql b/contrib/pg_stat_statements/sql/plancache.sql
new file mode 100644
index 00000000000..97d84bfdbed
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/plancache.sql
@@ -0,0 +1,111 @@
+--
+-- Information related to plan cache
+--
+
+--
+-- Setup
+--
+CREATE OR REPLACE FUNCTION select_one_func(int) RETURNS VOID AS $$
+DECLARE
+    ret INT;
+BEGIN
+    SELECT $1 INTO ret;
+END;
+$$ LANGUAGE plpgsql;
+CREATE OR REPLACE PROCEDURE select_one_proc(int) AS $$
+DECLARE
+    ret INT;
+BEGIN
+    select $1 INTO ret;
+END;
+$$ LANGUAGE plpgsql;
+
+--
+-- plan cache counters for prepared statements
+--
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+PREPARE p1 AS SELECT $1;
+-- plan cache auto
+SET plan_cache_mode TO auto;
+EXECUTE p1(1);
+-- force generic plan
+SET plan_cache_mode TO force_generic_plan;
+EXECUTE p1(1);
+-- force custom plan
+SET plan_cache_mode TO force_custom_plan;
+EXECUTE p1(1);
+SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, query FROM pg_stat_statements
+    ORDER BY query COLLATE "C";
+DEALLOCATE p1;
+
+--
+-- plan cache counters for explain|explain (analyze) with prepared statements
+--
+SET pg_stat_statements.track = 'all';
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+PREPARE p1 AS SELECT $1;
+-- plan cache auto
+SET plan_cache_mode TO auto;
+EXPLAIN (COSTS OFF) EXECUTE p1(1);
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) EXECUTE p1(1);
+-- force generic plan
+SET plan_cache_mode TO force_generic_plan;
+EXPLAIN (COSTS OFF) EXECUTE p1(1);
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) EXECUTE p1(1);
+-- force custom plan
+SET plan_cache_mode TO force_custom_plan;
+EXPLAIN (COSTS OFF) EXECUTE p1(1);
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) EXECUTE p1(1);
+SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, query FROM pg_stat_statements
+    ORDER BY query COLLATE "C";
+RESET pg_stat_statements.track;
+DEALLOCATE p1;
+
+--
+-- plan cache counters for functions and procedures
+--
+SET pg_stat_statements.track = 'all';
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+-- plan cache auto
+SET plan_cache_mode TO auto;
+SELECT select_one_func(1);
+CALL select_one_proc(1);
+-- force generic plan
+SET plan_cache_mode TO force_generic_plan;
+SELECT select_one_func(1);
+CALL select_one_proc(1);
+-- force custom plan
+SET plan_cache_mode TO force_custom_plan;
+SELECT select_one_func(1);
+CALL select_one_proc(1);
+SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, query FROM pg_stat_statements
+    ORDER BY query COLLATE "C";
+
+--
+-- plan cache counters for explain|explain (analyze) with functions and procedures
+--
+SET pg_stat_statements.track = 'all';
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+-- plan cache auto
+SET plan_cache_mode TO auto;
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) SELECT select_one_func(1);
+EXPLAIN (COSTS OFF) SELECT select_one_func(1);
+CALL select_one_proc(1);
+-- force generic plan
+SET plan_cache_mode TO force_generic_plan;
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) SELECT select_one_func(1);
+EXPLAIN (COSTS OFF) SELECT select_one_func(1);
+CALL select_one_proc(1);
+-- force custom plan
+SET plan_cache_mode TO force_custom_plan;
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) SELECT select_one_func(1);
+EXPLAIN (COSTS OFF) SELECT select_one_func(1);
+CALL select_one_proc(1);
+SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, query FROM pg_stat_statements
+    ORDER BY query COLLATE "C";
+
+--
+-- Cleanup
+--
+DROP FUNCTION select_one_func(int);
+DROP PROCEDURE select_one_proc(int);
diff --git a/doc/src/sgml/pgstatstatements.sgml b/doc/src/sgml/pgstatstatements.sgml
index 7baa07dcdbf..08b5fec9e10 100644
--- a/doc/src/sgml/pgstatstatements.sgml
+++ b/doc/src/sgml/pgstatstatements.sgml
@@ -554,6 +554,24 @@
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>generic_plan_calls</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Number of times the statement was executed using a generic plan
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>custom_plan_calls</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Number of times the statement was executed using a custom plan
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>stats_since</structfield> <type>timestamp with time zone</type>
diff --git a/src/backend/commands/copyto.c b/src/backend/commands/copyto.c
index ea6f18f2c80..55a2719d768 100644
--- a/src/backend/commands/copyto.c
+++ b/src/backend/commands/copyto.c
@@ -835,7 +835,7 @@ BeginCopyTo(ParseState *pstate,
 		((DR_copy *) dest)->cstate = cstate;
 
 		/* Create a QueryDesc requesting no output */
-		cstate->queryDesc = CreateQueryDesc(plan, pstate->p_sourcetext,
+		cstate->queryDesc = CreateQueryDesc(plan, NULL, pstate->p_sourcetext,
 											GetActiveSnapshot(),
 											InvalidSnapshot,
 											dest, NULL, NULL, 0);
diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c
index dfd2ab8e862..18c8a11b70c 100644
--- a/src/backend/commands/createas.c
+++ b/src/backend/commands/createas.c
@@ -334,7 +334,7 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt,
 		UpdateActiveSnapshotCommandId();
 
 		/* Create a QueryDesc, redirecting output to our tuple receiver */
-		queryDesc = CreateQueryDesc(plan, pstate->p_sourcetext,
+		queryDesc = CreateQueryDesc(plan, NULL, pstate->p_sourcetext,
 									GetActiveSnapshot(), InvalidSnapshot,
 									dest, params, queryEnv, 0);
 
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 7e2792ead71..b2b998bc2db 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -369,7 +369,7 @@ standard_ExplainOneQuery(Query *query, int cursorOptions,
 	}
 
 	/* run it (if needed) and produce output */
-	ExplainOnePlan(plan, into, es, queryString, params, queryEnv,
+	ExplainOnePlan(plan, NULL, into, es, queryString, params, queryEnv,
 				   &planduration, (es->buffers ? &bufusage : NULL),
 				   es->memory ? &mem_counters : NULL);
 }
@@ -491,7 +491,7 @@ ExplainOneUtility(Node *utilityStmt, IntoClause *into, ExplainState *es,
  * to call it.
  */
 void
-ExplainOnePlan(PlannedStmt *plannedstmt, IntoClause *into, ExplainState *es,
+ExplainOnePlan(PlannedStmt *plannedstmt, CachedPlan *cplan, IntoClause *into, ExplainState *es,
 			   const char *queryString, ParamListInfo params,
 			   QueryEnvironment *queryEnv, const instr_time *planduration,
 			   const BufferUsage *bufusage,
@@ -547,7 +547,7 @@ ExplainOnePlan(PlannedStmt *plannedstmt, IntoClause *into, ExplainState *es,
 		dest = None_Receiver;
 
 	/* Create a QueryDesc for the query */
-	queryDesc = CreateQueryDesc(plannedstmt, queryString,
+	queryDesc = CreateQueryDesc(plannedstmt, cplan, queryString,
 								GetActiveSnapshot(), InvalidSnapshot,
 								dest, params, queryEnv, instrument_option);
 
diff --git a/src/backend/commands/extension.c b/src/backend/commands/extension.c
index e6f9ab6dfd6..bc3e7bb808f 100644
--- a/src/backend/commands/extension.c
+++ b/src/backend/commands/extension.c
@@ -993,6 +993,7 @@ execute_sql_string(const char *sql, const char *filename)
 				QueryDesc  *qdesc;
 
 				qdesc = CreateQueryDesc(stmt,
+										NULL,
 										sql,
 										GetActiveSnapshot(), NULL,
 										dest, NULL, NULL, 0);
diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index 188e26f0e6e..5b2a20a4f8f 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -438,7 +438,7 @@ refresh_matview_datafill(DestReceiver *dest, Query *query,
 	UpdateActiveSnapshotCommandId();
 
 	/* Create a QueryDesc, redirecting output to our tuple receiver */
-	queryDesc = CreateQueryDesc(plan, queryString,
+	queryDesc = CreateQueryDesc(plan, NULL, queryString,
 								GetActiveSnapshot(), InvalidSnapshot,
 								dest, NULL, NULL, 0);
 
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..a5f3a662e51 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -657,7 +657,7 @@ ExplainExecuteQuery(ExecuteStmt *execstmt, IntoClause *into, ExplainState *es,
 		PlannedStmt *pstmt = lfirst_node(PlannedStmt, p);
 
 		if (pstmt->commandType != CMD_UTILITY)
-			ExplainOnePlan(pstmt, into, es, query_string, paramLI, pstate->p_queryEnv,
+			ExplainOnePlan(pstmt, cplan, into, es, query_string, paramLI, pstate->p_queryEnv,
 						   &planduration, (es->buffers ? &bufusage : NULL),
 						   es->memory ? &mem_counters : NULL);
 		else
diff --git a/src/backend/executor/execParallel.c b/src/backend/executor/execParallel.c
index f3e77bda279..58b1c09223a 100644
--- a/src/backend/executor/execParallel.c
+++ b/src/backend/executor/execParallel.c
@@ -1280,6 +1280,7 @@ ExecParallelGetQueryDesc(shm_toc *toc, DestReceiver *receiver,
 
 	/* Create a QueryDesc for the query. */
 	return CreateQueryDesc(pstmt,
+						   NULL,
 						   queryString,
 						   GetActiveSnapshot(), InvalidSnapshot,
 						   receiver, paramLI, NULL, instrument_options);
diff --git a/src/backend/executor/functions.c b/src/backend/executor/functions.c
index 359aafea681..635ce762eb3 100644
--- a/src/backend/executor/functions.c
+++ b/src/backend/executor/functions.c
@@ -1339,6 +1339,7 @@ postquel_start(execution_state *es, SQLFunctionCachePtr fcache)
 		dest = None_Receiver;
 
 	es->qd = CreateQueryDesc(es->stmt,
+							 NULL,
 							 fcache->func->src,
 							 GetActiveSnapshot(),
 							 InvalidSnapshot,
diff --git a/src/backend/executor/spi.c b/src/backend/executor/spi.c
index ecb2e4ccaa1..5ffff06ec0e 100644
--- a/src/backend/executor/spi.c
+++ b/src/backend/executor/spi.c
@@ -2690,6 +2690,7 @@ _SPI_execute_plan(SPIPlanPtr plan, const SPIExecuteOptions *options,
 					snap = InvalidSnapshot;
 
 				qdesc = CreateQueryDesc(stmt,
+										cplan,
 										plansource->query_string,
 										snap, crosscheck_snapshot,
 										dest,
diff --git a/src/backend/tcop/pquery.c b/src/backend/tcop/pquery.c
index d1593f38b35..dc4f5ce9d5e 100644
--- a/src/backend/tcop/pquery.c
+++ b/src/backend/tcop/pquery.c
@@ -26,6 +26,7 @@
 #include "tcop/pquery.h"
 #include "tcop/utility.h"
 #include "utils/memutils.h"
+#include "utils/plancache.h"
 #include "utils/snapmgr.h"
 
 
@@ -37,6 +38,7 @@ Portal		ActivePortal = NULL;
 
 
 static void ProcessQuery(PlannedStmt *plan,
+						 CachedPlan *cplan,
 						 const char *sourceText,
 						 ParamListInfo params,
 						 QueryEnvironment *queryEnv,
@@ -66,6 +68,7 @@ static void DoPortalRewind(Portal portal);
  */
 QueryDesc *
 CreateQueryDesc(PlannedStmt *plannedstmt,
+				CachedPlan *cplan,
 				const char *sourceText,
 				Snapshot snapshot,
 				Snapshot crosscheck_snapshot,
@@ -93,6 +96,8 @@ CreateQueryDesc(PlannedStmt *plannedstmt,
 	qd->planstate = NULL;
 	qd->totaltime = NULL;
 
+	qd->cplan = cplan;
+
 	/* not yet executed */
 	qd->already_executed = false;
 
@@ -135,6 +140,7 @@ FreeQueryDesc(QueryDesc *qdesc)
  */
 static void
 ProcessQuery(PlannedStmt *plan,
+			 CachedPlan *cplan,
 			 const char *sourceText,
 			 ParamListInfo params,
 			 QueryEnvironment *queryEnv,
@@ -146,7 +152,7 @@ ProcessQuery(PlannedStmt *plan,
 	/*
 	 * Create the QueryDesc object
 	 */
-	queryDesc = CreateQueryDesc(plan, sourceText,
+	queryDesc = CreateQueryDesc(plan, cplan, sourceText,
 								GetActiveSnapshot(), InvalidSnapshot,
 								dest, params, queryEnv, 0);
 
@@ -494,6 +500,7 @@ PortalStart(Portal portal, ParamListInfo params,
 				 * the destination to DestNone.
 				 */
 				queryDesc = CreateQueryDesc(linitial_node(PlannedStmt, portal->stmts),
+											portal->cplan,
 											portal->sourceText,
 											GetActiveSnapshot(),
 											InvalidSnapshot,
@@ -1270,6 +1277,7 @@ PortalRunMulti(Portal portal,
 			{
 				/* statement can set tag string */
 				ProcessQuery(pstmt,
+							 portal->cplan,
 							 portal->sourceText,
 							 portal->portalParams,
 							 portal->queryEnv,
@@ -1279,6 +1287,7 @@ PortalRunMulti(Portal portal,
 			{
 				/* stmt added by rewrite cannot set tag */
 				ProcessQuery(pstmt,
+							 portal->cplan,
 							 portal->sourceText,
 							 portal->portalParams,
 							 portal->queryEnv,
diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c
index 89a1c79e984..63df3ecd881 100644
--- a/src/backend/utils/cache/plancache.c
+++ b/src/backend/utils/cache/plancache.c
@@ -1140,6 +1140,7 @@ BuildCachedPlan(CachedPlanSource *plansource, List *qlist,
 	plan->is_oneshot = plansource->is_oneshot;
 	plan->is_saved = false;
 	plan->is_valid = true;
+	plan->status = PLAN_CACHE_STATUS_UNKNOWN;
 
 	/* assign generation number to new plan */
 	plan->generation = ++(plansource->generation);
@@ -1358,10 +1359,12 @@ GetCachedPlan(CachedPlanSource *plansource, ParamListInfo boundParams,
 		plansource->total_custom_cost += cached_plan_cost(plan, true);
 
 		plansource->num_custom_plans++;
+		plan->status = PLAN_CACHE_STATUS_CUSTOM_PLAN;
 	}
 	else
 	{
 		plansource->num_generic_plans++;
+		plan->status = PLAN_CACHE_STATUS_GENERIC_PLAN;
 	}
 
 	Assert(plan != NULL);
diff --git a/src/include/commands/explain.h b/src/include/commands/explain.h
index 3b122f79ed8..36579590ec2 100644
--- a/src/include/commands/explain.h
+++ b/src/include/commands/explain.h
@@ -63,9 +63,13 @@ extern void ExplainOneUtility(Node *utilityStmt, IntoClause *into,
 							  struct ExplainState *es, ParseState *pstate,
 							  ParamListInfo params);
 
-extern void ExplainOnePlan(PlannedStmt *plannedstmt, IntoClause *into,
-						   struct ExplainState *es, const char *queryString,
-						   ParamListInfo params, QueryEnvironment *queryEnv,
+extern void ExplainOnePlan(PlannedStmt *plannedstmt,
+						   CachedPlan *cplan,
+						   IntoClause *into,
+						   struct ExplainState *es,
+						   const char *queryString,
+						   ParamListInfo params,
+						   QueryEnvironment *queryEnv,
 						   const instr_time *planduration,
 						   const BufferUsage *bufusage,
 						   const MemoryContextCounters *mem_counters);
diff --git a/src/include/executor/execdesc.h b/src/include/executor/execdesc.h
index 86db3dc8d0d..f7cbe0981a5 100644
--- a/src/include/executor/execdesc.h
+++ b/src/include/executor/execdesc.h
@@ -17,6 +17,7 @@
 
 #include "nodes/execnodes.h"
 #include "tcop/dest.h"
+#include "utils/plancache.h"
 
 
 /* ----------------
@@ -35,6 +36,7 @@ typedef struct QueryDesc
 	/* These fields are provided by CreateQueryDesc */
 	CmdType		operation;		/* CMD_SELECT, CMD_UPDATE, etc. */
 	PlannedStmt *plannedstmt;	/* planner's output (could be utility, too) */
+	CachedPlan *cplan;			/* CachedPlan that supplies the plannedstmt */
 	const char *sourceText;		/* source text of the query */
 	Snapshot	snapshot;		/* snapshot to use for query */
 	Snapshot	crosscheck_snapshot;	/* crosscheck for RI update/delete */
@@ -57,6 +59,7 @@ typedef struct QueryDesc
 
 /* in pquery.c */
 extern QueryDesc *CreateQueryDesc(PlannedStmt *plannedstmt,
+								  CachedPlan *cplan,
 								  const char *sourceText,
 								  Snapshot snapshot,
 								  Snapshot crosscheck_snapshot,
diff --git a/src/include/utils/plancache.h b/src/include/utils/plancache.h
index 1baa6d50bfd..b9d00991140 100644
--- a/src/include/utils/plancache.h
+++ b/src/include/utils/plancache.h
@@ -38,6 +38,13 @@ typedef enum
 /* GUC parameter */
 extern PGDLLIMPORT int plan_cache_mode;
 
+typedef enum
+{
+	PLAN_CACHE_STATUS_UNKNOWN = 0,
+	PLAN_CACHE_STATUS_GENERIC_PLAN,
+	PLAN_CACHE_STATUS_CUSTOM_PLAN,
+}			PlanCacheStatus;
+
 /* Optional callback to editorialize on rewritten parse trees */
 typedef void (*PostRewriteHook) (List *querytree_list, void *arg);
 
@@ -163,6 +170,7 @@ typedef struct CachedPlan
 	bool		is_oneshot;		/* is it a "oneshot" plan? */
 	bool		is_saved;		/* is CachedPlan in a long-lived context? */
 	bool		is_valid;		/* is the stmt_list currently valid? */
+	PlanCacheStatus status;		/* status of the cached plan */
 	Oid			planRoleId;		/* Role ID the plan was created for */
 	bool		dependsOnRole;	/* is plan specific to that role? */
 	TransactionId saved_xmin;	/* if valid, replan when TransactionXmin
-- 
2.43.0

#22Michael Paquier
michael@paquier.xyz
In reply to: Sami Imseih (#21)
Re: track generic and custom plans in pg_stat_statements

On Mon, Jun 30, 2025 at 02:45:49PM +0300, Sami Imseih wrote:

Only changes to the tests due to the revert of nested query
tracking in f85f6ab051b

@@ -35,6 +36,7 @@ typedef struct QueryDesc
 	/* These fields are provided by CreateQueryDesc */
 	CmdType		operation;		/* CMD_SELECT, CMD_UPDATE, etc. */
 	PlannedStmt *plannedstmt;	/* planner's output (could be utility, too) */
+	CachedPlan *cplan;			/* CachedPlan that supplies the plannedstmt */

Ugh. This is plugging into an executor-related structure a completely
different layer, so that looks like an invasive layer violation to
me.. This is passed through ProcessQuery() from a Portal, changing
while on it ExplainOnePlan. If we want to get access from a cached
plan, wouldn't it be simpler to check if we have an active portal in
one of the executor hooks of PGSS and retrieve the status of the plan
from it? Okay, that's perhaps a bit hack-ish, but it is less invasive
and it removes the dependency to the plan cache facilities from
QueryDesc.

Note: the size of the change in pg_stat_statements--1.12--1.13.sql
points that we should seriously consider splitting these attributes
into multiple sub-functions. That would make future upgrades simpler,
and the main function simpler even if we are a bit more lossy when
doing scans of PGSS entries across multiple functions with joins based
on the query ID, database ID and toplevel at least, because that's
what I guess we would use as common point for all the sub-functions
when joining them.
--
Michael

#23Andrei Lepikhov
lepihov@gmail.com
In reply to: Sami Imseih (#21)
Re: track generic and custom plans in pg_stat_statements

On 6/30/25 13:45, Sami Imseih wrote:

rebased patch.

Only changes to the tests due to the revert of nested query
tracking in f85f6ab051b

Thank you for your efforts.

I would like to share a few thoughts about this patch. First, I believe
the 'status' field could be renamed to 'mode,' as it aligns better with
the plan_cache_mode GUC name. Additionally, why introduce an unknown
status? I can't foresee a scenario where it would be necessary, as we
typically encounter either a custom or a generic plan during execution.

On a more general note, currently, CachedPlanSource does not refer to a
custom version of the plan. However, it seems reasonable to implement
this for better tracking. By adding a CachedPlanSource::cplan link, we
can establish a connection to the entire PlanCache entry instead of only
CachedPlan within a queryDesc and, consequently, make it accessible from
the executor. This would give an access to statistics on costs and the
number of replannings.

Furthermore, this could lead to interesting opportunities for
extensions, such as resetting auto-mode decisions and planning
statistics based on actual execution statistics, and manipulating
generic/custom plan switching logic.

--
regards, Andrei Lepikhov

#24Sami Imseih
samimseih@gmail.com
In reply to: Michael Paquier (#22)
Re: track generic and custom plans in pg_stat_statements

Ugh. This is plugging into an executor-related structure a completely
different layer, so that looks like an invasive layer violation to
me.. This is passed through ProcessQuery() from a Portal, changing
while on it ExplainOnePlan. If we want to get access from a cached
plan, wouldn't it be simpler to check if we have an active portal in
one of the executor hooks of PGSS and retrieve the status of the plan
from it? Okay, that's perhaps a bit hack-ish, but it is less invasive
and it removes the dependency to the plan cache facilities from
QueryDesc.

I found that ActivePortal is to always "active" in ExecutorEnd for all cases.
Also, ActivePortal->cplan may not always be available at ExecutorStart.

I think we can rely on ActivePortal if we add a new field to portal which
tracks the cached plan status; i.e. we set ActivePortal->cache_plan_status
inside GetCachedPlan. Then in ExecutorStart, we read back this value and
store it in a new field in QueryDesc->estate. This will make the value
available to ExecutorEnd. I really don't want us making an extra pgss_store
call in ExecutorStart since it will add significant overhead.

What do you think about adding these couple of fields?

--
Sami

#25Sami Imseih
samimseih@gmail.com
In reply to: Andrei Lepikhov (#23)
Re: track generic and custom plans in pg_stat_statements

this for better tracking. By adding a CachedPlanSource::cplan link, we
can establish a connection to the entire PlanCache entry instead of only
CachedPlan within a queryDesc and, consequently, make it accessible from
the executor. This would give an access to statistics on costs and the
number of replannings.

This maybe out of scope for this patch, but can you elaborate on what you mean
by "CachedPlanSource::cplan link" ?

--
Sami

#26Sami Imseih
samimseih@gmail.com
In reply to: Sami Imseih (#24)
Re: track generic and custom plans in pg_stat_statements

Ugh. This is plugging into an executor-related structure a completely
different layer, so that looks like an invasive layer violation to
me.. This is passed through ProcessQuery() from a Portal, changing
while on it ExplainOnePlan. If we want to get access from a cached
plan, wouldn't it be simpler to check if we have an active portal in
one of the executor hooks of PGSS and retrieve the status of the plan
from it? Okay, that's perhaps a bit hack-ish, but it is less invasive
and it removes the dependency to the plan cache facilities from
QueryDesc.

I found that ActivePortal is to always "active" in ExecutorEnd for all
cases.
Also, ActivePortal->cplan may not always be available at ExecutorStart.

I think we can rely on ActivePortal if we add a new field to portal which
tracks the cached plan status; i.e. we set ActivePortal->cache_plan_status
inside GetCachedPlan. Then in ExecutorStart, we read back this value and
store it in a new field in QueryDesc->estate. This will make the value
available to ExecutorEnd. I really don't want us making an extra pgss_store
call in ExecutorStart since it will add significant overhead.

What do you think about adding these couple of fields?

--
Sami

But I also have doubts about calling ActivePortal
Inside GetCachedPlan. It should only be used in the Executor
So, I’m not sure ActivePortal could
be very helpful here they way I describe it above.

--
Sami

#27Andrei Lepikhov
lepihov@gmail.com
In reply to: Sami Imseih (#25)
Re: track generic and custom plans in pg_stat_statements

On 17/7/2025 00:58, Sami Imseih wrote:

this for better tracking. By adding a CachedPlanSource::cplan link, we
can establish a connection to the entire PlanCache entry instead of only
CachedPlan within a queryDesc and, consequently, make it accessible from
the executor. This would give an access to statistics on costs and the
number of replannings.

This maybe out of scope for this patch, but can you elaborate on what you mean
by "CachedPlanSource::cplan link" ?

You need to introduce a 'status' field, right? - to allow someone to
identify the plan's type, which was previously somewhat complicated.
However, it may be implemented in a slightly different way, by adding
CachedPlanSource::cplan (meaning 'Current Plan') and a trivial
convention: 'cplan' references the gplan field or it refers a custom
plan. Instead of the CachedPlan, we may provide the executor with a link
to more stable and informative CachedPlanSource entry. That's the main idea.
As I see it, CachedPlan doesn't make sense without plancache and a
CachedPlanSource entry. So, it is at least a valid solution.

With that link, you can access various statistics: num_custom_plans,
num_generic_plans, total_custom_cost, and generic_cost. It would also be
possible to clear the *_cost fields and allow Postgres to make a new
attempt at choosing the plan type - who knows, maybe the previous
decision is already outdated?

My point is that we can address one of the common issues with generic
plans in a more extensible way, enabling modules to access the
CachedPlanSource data at the time they have access to the execution
instrumentation.

It seems impractical to me to invent one more patch: since you've
already modified the CreateQueryDesc interface and introduced a plan
type identification logic, why do it twice?

--
regards, Andrei Lepikhov

#28Sami Imseih
samimseih@gmail.com
In reply to: Andrei Lepikhov (#27)
Re: track generic and custom plans in pg_stat_statements

this for better tracking. By adding a CachedPlanSource::cplan link, we
can establish a connection to the entire PlanCache entry instead of only
CachedPlan within a queryDesc and, consequently, make it accessible from
the executor. This would give an access to statistics on costs and the
number of replannings.

This maybe out of scope for this patch, but can you elaborate on what you mean
by "CachedPlanSource::cplan link" ?

You need to introduce a 'status' field, right? - to allow someone to
identify the plan's type, which was previously somewhat complicated.
However, it may be implemented in a slightly different way, by adding
CachedPlanSource::cplan (meaning 'Current Plan') and a trivial
convention: 'cplan' references the gplan field or it refers a custom
plan. Instead of the CachedPlan, we may provide the executor with a link
to more stable and informative CachedPlanSource entry. That's the main idea.
As I see it, CachedPlan doesn't make sense without plancache and a
CachedPlanSource entry. So, it is at least a valid solution.

With that link, you can access various statistics: num_custom_plans,
num_generic_plans, total_custom_cost, and generic_cost. It would also be
possible to clear the *_cost fields and allow Postgres to make a new
attempt at choosing the plan type - who knows, maybe the previous
decision is already outdated?

My point is that we can address one of the common issues with generic
plans in a more extensible way, enabling modules to access the
CachedPlanSource data at the time they have access to the execution
instrumentation.

It seems impractical to me to invent one more patch: since you've
already modified the CreateQueryDesc interface and introduced a plan
type identification logic, why do it twice?

Thanks for the clarification. I see what you're getting at now.

You're suggesting adding CachedPlanSource to QueryDesc instead of
CachedPlan. This would allow extensions to access the statistics and cost
information from the CachedPlanSource, which would help tools like
pg_stat_statements track planning data, as we are trying to do with this
patch. It could also support other use cases, such as allowing extensions to
modify the costs in order to force a generic or custom plan. I had not
considered that second use case, but if there is a good case for it, I am not
opposed.

Adding CachedPlanSource to QueryDesc seems doable. However, Michael
previously objected to adding CachedPlan to QueryDesc. Is there any
similar hesitation about including CachedPlanSource?

The best way to support the functionality needed by pg_stat_statements is
to expose either CachedPlan or CachedPlanSource via QueryDesc. I
support using CachedPlanSource, since it also enables the additional use
cases that Andrei has mentioned.

Andrei, do we actually need access to CachedPlanSource::cplan? For
tracking the plan cache mode in pg_stat_statements, it be sufficient
to add a new boolean field such as is_last_plan_generic to
CachedPlanSource. Do you have another use case you have in mind
that would require a cplan field that references either the generic or
custom plan?

--
Sami

#29Andrei Lepikhov
lepihov@gmail.com
In reply to: Sami Imseih (#28)
Re: track generic and custom plans in pg_stat_statements

On 17/7/2025 20:19, Sami Imseih wrote:

Thanks for the clarification. I see what you're getting at now.

Thanks for reading this!

You're suggesting adding CachedPlanSource to QueryDesc instead of
CachedPlan. This would allow extensions to access the statistics and cost
information from the CachedPlanSource, which would help tools like
pg_stat_statements track planning data, as we are trying to do with this
patch. It could also support other use cases, such as allowing extensions to
modify the costs in order to force a generic or custom plan. I had not
considered that second use case, but if there is a good case for it, I am not
opposed.

Hmm, I don't propose modifying costs. The focus is on resetting the plan
cache decision that PostgreSQL has made in automatic mode. During the
DBMS operation, various factors may cause a generic plan to be
suboptimal or make it more desirable as well. Discussions from 2010 to
2013 indicate that the community recognised the problem and discovered
an approach based on execution time and real efforts rather than a
cost-based method. While I doubt it could be ideal as a core solution,
an extension may potentially do it for the sake of TPS maximisation.
What we need is a way to access the plan cache entry.

Adding CachedPlanSource to QueryDesc seems doable. However, Michael
previously objected to adding CachedPlan to QueryDesc. Is there any
similar hesitation about including CachedPlanSource?

I agree that we should investigate further to find the most optimal
solution. Personally, I'm open to including an internal reference to a
plan cache entry within the QueryDesc, as long as the plan has its roots
there.

Andrei, do we actually need access to CachedPlanSource::cplan? For
tracking the plan cache mode in pg_stat_statements, it be sufficient
to add a new boolean field such as is_last_plan_generic to
CachedPlanSource. Do you have another use case you have in mind
that would require a cplan field that references either the generic or
custom plan?

I'm not entirely sure. I followed your idea of referencing the entire
list of planned statements during the execution of a single statement.
The is_last_plan_generic field may be sufficient at first glance.

--
regards, Andrei Lepikhov

#30Sami Imseih
samimseih@gmail.com
In reply to: Andrei Lepikhov (#29)
1 attachment(s)
Re: track generic and custom plans in pg_stat_statements

Hmm, I don't propose modifying costs. The focus is on resetting the plan
cache decision that PostgreSQL has made in automatic mode. During the
DBMS operation, various factors may cause a generic plan to be
suboptimal or make it more desirable as well. Discussions from 2010 to
2013 indicate that the community recognised the problem and discovered
an approach based on execution time and real efforts rather than a
cost-based method. While I doubt it could be ideal as a core solution,
an extension may potentially do it for the sake of TPS maximisation.
What we need is a way to access the plan cache entry.

Thanks for clearing up my understanding. Essentially, override the
current cost-based
method of determining custom vs. generic by using something like execution time,
which is somehow tracked by the extension. That is how I understand this.
Now, I wonder if it may be a good idea to add some hooks in GetCachedPlan
to make this work?

Adding CachedPlanSource to QueryDesc seems doable. However, Michael
previously objected to adding CachedPlan to QueryDesc. Is there any
similar hesitation about including CachedPlanSource?

I agree that we should investigate further to find the most optimal
solution. Personally, I'm open to including an internal reference to a
plan cache entry within the QueryDesc, as long as the plan has its roots
there.

For the sake of this feature, I suspect making CachedPlanSource available
in QueryDesc will be a non-starter, but I would like to hear other opinions.
To accomplish the goals of this patch, we definitely need the current
execution’s
"plan cache mode" to be accessible in ExecutorEnd somehow.

Since carrying over the plan cache structures to QueryDesc does not seem
suitable, I wonder if the solution used in v11 is better. In v11, the plan cache
mode is tracked in both CachedPlan and QueryDesc, and CachedPlan is passed
to CreateQueryDesc. At this point, the value is set and made available to the
executor hooks.

I also want to note that we need to track the possible values in an enum with
three states: two for custom or generic, and one for an unset mode.
The unset mode
is necessary to allow an extension to determine whether a plan cache
was actually
used or not.

For pg_stat_statements, only pg_stat_statements.c was modified, and I
added tests
for extended query protocol.

--
Sami

Attachments:

v11-0001-Add-counters-for-generic-and-custom-plan-executi.patchapplication/octet-stream; name=v11-0001-Add-counters-for-generic-and-custom-plan-executi.patchDownload
From 72af9534181ff0b286905be91aa29fcc1793a4b1 Mon Sep 17 00:00:00 2001
From: Sami Imseih <simseih@amazon.com>
Date: Fri, 18 Jul 2025 11:35:39 -0500
Subject: [PATCH v11 1/1] Add counters for generic and custom plan executions
 to pg_stat_statements

This patch introduces two new counters in pg_stat_statements:
- generic_plan_calls
- custom_plan_calls

These track how many times a prepared statement was executed using a
generic or custom plan, respectively.

To make the current plan cache mode available to Executor hooks (e.g.,
ExecutorEnd), a new "mode" field of type "CachedPlanMode" is added to
CachedPlan.

This mode is passed to "CreateQueryDesc", which accepts it as an argument
and stores it in a new "cached_plan_mode" field in QueryDesc. This makes
the mode accessible to the Executor via QueryDesc.

Although there are only two actual plan cache modes (custom and generic),
CachedPlanMode includes a third value to represent an unset state.
This "not set" mode can also be used by extensions in cases where the
plan cache is not involved but statistics are still relevant.
---
 contrib/pg_stat_statements/Makefile           |   3 +-
 .../pg_stat_statements/expected/plancache.out | 295 ++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |   2 +
 .../pg_stat_statements--1.12--1.13.sql        |  78 +++++
 .../pg_stat_statements/pg_stat_statements.c   |  51 ++-
 .../pg_stat_statements.control                |   2 +-
 contrib/pg_stat_statements/sql/plancache.sql  | 129 ++++++++
 doc/src/sgml/pgstatstatements.sgml            |  18 ++
 src/backend/commands/copyto.c                 |   2 +-
 src/backend/commands/createas.c               |   2 +-
 src/backend/commands/explain.c                |   7 +-
 src/backend/commands/extension.c              |   2 +-
 src/backend/commands/matview.c                |   2 +-
 src/backend/commands/prepare.c                |   2 +-
 src/backend/executor/execParallel.c           |   2 +-
 src/backend/executor/functions.c              |   3 +-
 src/backend/executor/spi.c                    |   4 +-
 src/backend/tcop/pquery.c                     |  22 +-
 src/backend/utils/cache/plancache.c           |   4 +
 src/include/commands/explain.h                |   4 +-
 src/include/executor/execdesc.h               |   7 +-
 src/include/utils/plancache.h                 |   9 +
 22 files changed, 621 insertions(+), 29 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/plancache.out
 create mode 100644 contrib/pg_stat_statements/pg_stat_statements--1.12--1.13.sql
 create mode 100644 contrib/pg_stat_statements/sql/plancache.sql

diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index b2bd8794d2a..996a5fac448 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -7,6 +7,7 @@ OBJS = \
 
 EXTENSION = pg_stat_statements
 DATA = pg_stat_statements--1.4.sql \
+	pg_stat_statements--1.12--1.13.sql \
 	pg_stat_statements--1.11--1.12.sql pg_stat_statements--1.10--1.11.sql \
 	pg_stat_statements--1.9--1.10.sql pg_stat_statements--1.8--1.9.sql \
 	pg_stat_statements--1.7--1.8.sql pg_stat_statements--1.6--1.7.sql \
@@ -20,7 +21,7 @@ LDFLAGS_SL += $(filter -lm, $(LIBS))
 REGRESS_OPTS = --temp-config $(top_srcdir)/contrib/pg_stat_statements/pg_stat_statements.conf
 REGRESS = select dml cursors utility level_tracking planning \
 	user_activity wal entry_timestamp privileges extended \
-	parallel cleanup oldextversions squashing
+	parallel cleanup oldextversions squashing plancache
 # Disabled because these tests require "shared_preload_libraries=pg_stat_statements",
 # which typical installcheck users do not have (e.g. buildfarm clients).
 NO_INSTALLCHECK = 1
diff --git a/contrib/pg_stat_statements/expected/plancache.out b/contrib/pg_stat_statements/expected/plancache.out
new file mode 100644
index 00000000000..7b05d5a425f
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/plancache.out
@@ -0,0 +1,295 @@
+--
+-- Information related to plan cache
+--
+--
+-- Setup
+--
+CREATE OR REPLACE FUNCTION select_one_func(int) RETURNS VOID AS $$
+DECLARE
+    ret INT;
+BEGIN
+    SELECT $1 INTO ret;
+END;
+$$ LANGUAGE plpgsql;
+CREATE OR REPLACE PROCEDURE select_one_proc(int) AS $$
+DECLARE
+    ret INT;
+BEGIN
+    select $1 INTO ret;
+END;
+$$ LANGUAGE plpgsql;
+--
+-- plan cache counters for prepared statements
+--
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+PREPARE p1 AS SELECT $1;
+-- plan cache auto
+SET plan_cache_mode TO auto;
+EXECUTE p1(1);
+ ?column? 
+----------
+ 1
+(1 row)
+
+-- force generic plan
+SET plan_cache_mode TO force_generic_plan;
+EXECUTE p1(1);
+ ?column? 
+----------
+ 1
+(1 row)
+
+-- force custom plan
+SET plan_cache_mode TO force_custom_plan;
+EXECUTE p1(1);
+ ?column? 
+----------
+ 1
+(1 row)
+
+SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, query FROM pg_stat_statements
+    ORDER BY query COLLATE "C";
+ calls | generic_plan_calls | custom_plan_calls | toplevel |                       query                        
+-------+--------------------+-------------------+----------+----------------------------------------------------
+     3 |                  1 |                 2 | t        | PREPARE p1 AS SELECT $1
+     1 |                  0 |                 0 | t        | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+     3 |                  0 |                 0 | t        | SET plan_cache_mode TO $1
+(3 rows)
+
+DEALLOCATE p1;
+--
+-- plan cache counters for extended query protocol
+--
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+SELECT $1 \parse p1
+SET plan_cache_mode TO auto;
+\bind_named p1 1
+;
+ ?column? 
+----------
+ 1
+(1 row)
+
+SET plan_cache_mode TO force_generic_plan;
+\bind_named p1 1
+;
+ ?column? 
+----------
+ 1
+(1 row)
+
+SET plan_cache_mode TO force_custom_plan;
+\bind_named p1 1
+;
+ ?column? 
+----------
+ 1
+(1 row)
+
+SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, query FROM pg_stat_statements
+    ORDER BY query COLLATE "C";
+ calls | generic_plan_calls | custom_plan_calls | toplevel |                       query                        
+-------+--------------------+-------------------+----------+----------------------------------------------------
+     3 |                  1 |                 2 | t        | SELECT $1
+     1 |                  0 |                 0 | t        | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+     3 |                  0 |                 0 | t        | SET plan_cache_mode TO $1
+(3 rows)
+
+\close_prepared p1
+--
+-- plan cache counters for explain|explain (analyze) with prepared statements
+--
+SET pg_stat_statements.track = 'all';
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+PREPARE p1 AS SELECT $1;
+-- plan cache auto
+SET plan_cache_mode TO auto;
+EXPLAIN (COSTS OFF) EXECUTE p1(1);
+ QUERY PLAN 
+------------
+ Result
+(1 row)
+
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) EXECUTE p1(1);
+            QUERY PLAN             
+-----------------------------------
+ Result (actual rows=1.00 loops=1)
+(1 row)
+
+-- force generic plan
+SET plan_cache_mode TO force_generic_plan;
+EXPLAIN (COSTS OFF) EXECUTE p1(1);
+ QUERY PLAN 
+------------
+ Result
+(1 row)
+
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) EXECUTE p1(1);
+            QUERY PLAN             
+-----------------------------------
+ Result (actual rows=1.00 loops=1)
+(1 row)
+
+-- force custom plan
+SET plan_cache_mode TO force_custom_plan;
+EXPLAIN (COSTS OFF) EXECUTE p1(1);
+ QUERY PLAN 
+------------
+ Result
+(1 row)
+
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) EXECUTE p1(1);
+            QUERY PLAN             
+-----------------------------------
+ Result (actual rows=1.00 loops=1)
+(1 row)
+
+SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, query FROM pg_stat_statements
+    ORDER BY query COLLATE "C";
+ calls | generic_plan_calls | custom_plan_calls | toplevel |                                      query                                       
+-------+--------------------+-------------------+----------+----------------------------------------------------------------------------------
+     3 |                  0 |                 0 | t        | EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) EXECUTE p1(1)
+     3 |                  0 |                 0 | t        | EXPLAIN (COSTS OFF) EXECUTE p1(1)
+     6 |                  2 |                 4 | f        | PREPARE p1 AS SELECT $1
+     1 |                  0 |                 0 | t        | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+     3 |                  0 |                 0 | t        | SET plan_cache_mode TO $1
+(5 rows)
+
+RESET pg_stat_statements.track;
+DEALLOCATE p1;
+--
+-- plan cache counters for functions and procedures
+--
+SET pg_stat_statements.track = 'all';
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- plan cache auto
+SET plan_cache_mode TO auto;
+SELECT select_one_func(1);
+ select_one_func 
+-----------------
+ 
+(1 row)
+
+CALL select_one_proc(1);
+-- force generic plan
+SET plan_cache_mode TO force_generic_plan;
+SELECT select_one_func(1);
+ select_one_func 
+-----------------
+ 
+(1 row)
+
+CALL select_one_proc(1);
+-- force custom plan
+SET plan_cache_mode TO force_custom_plan;
+SELECT select_one_func(1);
+ select_one_func 
+-----------------
+ 
+(1 row)
+
+CALL select_one_proc(1);
+SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, query FROM pg_stat_statements
+    ORDER BY query COLLATE "C";
+ calls | generic_plan_calls | custom_plan_calls | toplevel |                       query                        
+-------+--------------------+-------------------+----------+----------------------------------------------------
+     3 |                  0 |                 0 | t        | CALL select_one_proc($1)
+     6 |                  2 |                 4 | f        | SELECT $1
+     1 |                  0 |                 0 | t        | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+     3 |                  0 |                 0 | t        | SELECT select_one_func($1)
+     3 |                  0 |                 0 | t        | SET plan_cache_mode TO $1
+(5 rows)
+
+--
+-- plan cache counters for explain|explain (analyze) with functions and procedures
+--
+SET pg_stat_statements.track = 'all';
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- plan cache auto
+SET plan_cache_mode TO auto;
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) SELECT select_one_func(1);
+            QUERY PLAN             
+-----------------------------------
+ Result (actual rows=1.00 loops=1)
+(1 row)
+
+EXPLAIN (COSTS OFF) SELECT select_one_func(1);
+ QUERY PLAN 
+------------
+ Result
+(1 row)
+
+CALL select_one_proc(1);
+-- force generic plan
+SET plan_cache_mode TO force_generic_plan;
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) SELECT select_one_func(1);
+            QUERY PLAN             
+-----------------------------------
+ Result (actual rows=1.00 loops=1)
+(1 row)
+
+EXPLAIN (COSTS OFF) SELECT select_one_func(1);
+ QUERY PLAN 
+------------
+ Result
+(1 row)
+
+CALL select_one_proc(1);
+-- force custom plan
+SET plan_cache_mode TO force_custom_plan;
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) SELECT select_one_func(1);
+            QUERY PLAN             
+-----------------------------------
+ Result (actual rows=1.00 loops=1)
+(1 row)
+
+EXPLAIN (COSTS OFF) SELECT select_one_func(1);
+ QUERY PLAN 
+------------
+ Result
+(1 row)
+
+CALL select_one_proc(1);
+SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, query FROM pg_stat_statements
+    ORDER BY query COLLATE "C";
+ calls | generic_plan_calls | custom_plan_calls | toplevel |                                             query                                              
+-------+--------------------+-------------------+----------+------------------------------------------------------------------------------------------------
+     3 |                  0 |                 0 | t        | CALL select_one_proc($1)
+     3 |                  0 |                 0 | t        | EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) SELECT select_one_func($1)
+     6 |                  0 |                 0 | f        | EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) SELECT select_one_func($1);
+     3 |                  0 |                 0 | t        | EXPLAIN (COSTS OFF) SELECT select_one_func($1)
+     6 |                  2 |                 4 | f        | SELECT $1
+     1 |                  0 |                 0 | t        | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+     3 |                  0 |                 0 | t        | SET plan_cache_mode TO $1
+(7 rows)
+
+--
+-- Cleanup
+--
+DROP FUNCTION select_one_func(int);
+DROP PROCEDURE select_one_proc(int);
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 01a6cbdcf61..110fb82fe12 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -21,6 +21,7 @@ contrib_targets += pg_stat_statements
 install_data(
   'pg_stat_statements.control',
   'pg_stat_statements--1.4.sql',
+  'pg_stat_statements--1.12--1.13.sql',
   'pg_stat_statements--1.11--1.12.sql',
   'pg_stat_statements--1.10--1.11.sql',
   'pg_stat_statements--1.9--1.10.sql',
@@ -57,6 +58,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'plancache',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/pg_stat_statements--1.12--1.13.sql b/contrib/pg_stat_statements/pg_stat_statements--1.12--1.13.sql
new file mode 100644
index 00000000000..2f0eaf14ec3
--- /dev/null
+++ b/contrib/pg_stat_statements/pg_stat_statements--1.12--1.13.sql
@@ -0,0 +1,78 @@
+/* contrib/pg_stat_statements/pg_stat_statements--1.12--1.13.sql */
+
+-- complain if script is sourced in psql, rather than via ALTER EXTENSION
+\echo Use "ALTER EXTENSION pg_stat_statements UPDATE TO '1.13'" to load this file. \quit
+
+/* First we have to remove them from the extension */
+ALTER EXTENSION pg_stat_statements DROP VIEW pg_stat_statements;
+ALTER EXTENSION pg_stat_statements DROP FUNCTION pg_stat_statements(boolean);
+
+/* Then we can drop them */
+DROP VIEW pg_stat_statements;
+DROP FUNCTION pg_stat_statements(boolean);
+
+/* Now redefine */
+CREATE FUNCTION pg_stat_statements(IN showtext boolean,
+    OUT userid oid,
+    OUT dbid oid,
+    OUT toplevel bool,
+    OUT queryid bigint,
+    OUT query text,
+    OUT plans int8,
+    OUT total_plan_time float8,
+    OUT min_plan_time float8,
+    OUT max_plan_time float8,
+    OUT mean_plan_time float8,
+    OUT stddev_plan_time float8,
+    OUT calls int8,
+    OUT total_exec_time float8,
+    OUT min_exec_time float8,
+    OUT max_exec_time float8,
+    OUT mean_exec_time float8,
+    OUT stddev_exec_time float8,
+    OUT rows int8,
+    OUT shared_blks_hit int8,
+    OUT shared_blks_read int8,
+    OUT shared_blks_dirtied int8,
+    OUT shared_blks_written int8,
+    OUT local_blks_hit int8,
+    OUT local_blks_read int8,
+    OUT local_blks_dirtied int8,
+    OUT local_blks_written int8,
+    OUT temp_blks_read int8,
+    OUT temp_blks_written int8,
+    OUT shared_blk_read_time float8,
+    OUT shared_blk_write_time float8,
+    OUT local_blk_read_time float8,
+    OUT local_blk_write_time float8,
+    OUT temp_blk_read_time float8,
+    OUT temp_blk_write_time float8,
+    OUT wal_records int8,
+    OUT wal_fpi int8,
+    OUT wal_bytes numeric,
+    OUT wal_buffers_full int8,
+    OUT jit_functions int8,
+    OUT jit_generation_time float8,
+    OUT jit_inlining_count int8,
+    OUT jit_inlining_time float8,
+    OUT jit_optimization_count int8,
+    OUT jit_optimization_time float8,
+    OUT jit_emission_count int8,
+    OUT jit_emission_time float8,
+    OUT jit_deform_count int8,
+    OUT jit_deform_time float8,
+    OUT parallel_workers_to_launch int8,
+    OUT parallel_workers_launched int8,
+    OUT generic_plan_calls int8,
+    OUT custom_plan_calls int8,
+    OUT stats_since timestamp with time zone,
+    OUT minmax_stats_since timestamp with time zone
+)
+RETURNS SETOF record
+AS 'MODULE_PATHNAME', 'pg_stat_statements_1_13'
+LANGUAGE C STRICT VOLATILE PARALLEL SAFE;
+
+CREATE VIEW pg_stat_statements AS
+  SELECT * FROM pg_stat_statements(true);
+
+GRANT SELECT ON pg_stat_statements TO PUBLIC;
diff --git a/contrib/pg_stat_statements/pg_stat_statements.c b/contrib/pg_stat_statements/pg_stat_statements.c
index e7857f81ec0..887e87c2ed4 100644
--- a/contrib/pg_stat_statements/pg_stat_statements.c
+++ b/contrib/pg_stat_statements/pg_stat_statements.c
@@ -69,6 +69,7 @@
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/memutils.h"
+#include "utils/plancache.h"
 #include "utils/timestamp.h"
 
 PG_MODULE_MAGIC_EXT(
@@ -114,6 +115,7 @@ typedef enum pgssVersion
 	PGSS_V1_10,
 	PGSS_V1_11,
 	PGSS_V1_12,
+	PGSS_V1_13,
 } pgssVersion;
 
 typedef enum pgssStoreKind
@@ -210,6 +212,8 @@ typedef struct Counters
 											 * to be launched */
 	int64		parallel_workers_launched;	/* # of parallel workers actually
 											 * launched */
+	int64		generic_plan_calls; /* number of calls using a generic plan */
+	int64		custom_plan_calls;	/* number of calls using a custom plan */
 } Counters;
 
 /*
@@ -323,6 +327,7 @@ PG_FUNCTION_INFO_V1(pg_stat_statements_1_9);
 PG_FUNCTION_INFO_V1(pg_stat_statements_1_10);
 PG_FUNCTION_INFO_V1(pg_stat_statements_1_11);
 PG_FUNCTION_INFO_V1(pg_stat_statements_1_12);
+PG_FUNCTION_INFO_V1(pg_stat_statements_1_13);
 PG_FUNCTION_INFO_V1(pg_stat_statements);
 PG_FUNCTION_INFO_V1(pg_stat_statements_info);
 
@@ -355,7 +360,8 @@ static void pgss_store(const char *query, int64 queryId,
 					   const struct JitInstrumentation *jitusage,
 					   JumbleState *jstate,
 					   int parallel_workers_to_launch,
-					   int parallel_workers_launched);
+					   int parallel_workers_launched,
+					   CachedPlanMode plan_cache_mode);
 static void pg_stat_statements_internal(FunctionCallInfo fcinfo,
 										pgssVersion api_version,
 										bool showtext);
@@ -877,7 +883,8 @@ pgss_post_parse_analyze(ParseState *pstate, Query *query, JumbleState *jstate)
 				   NULL,
 				   jstate,
 				   0,
-				   0);
+				   0,
+				   PLAN_CACHE_MODE_NOT_SET);
 }
 
 /*
@@ -957,7 +964,8 @@ pgss_planner(Query *parse,
 				   NULL,
 				   NULL,
 				   0,
-				   0);
+				   0,
+				   PLAN_CACHE_MODE_NOT_SET);
 	}
 	else
 	{
@@ -1091,7 +1099,8 @@ pgss_ExecutorEnd(QueryDesc *queryDesc)
 				   queryDesc->estate->es_jit ? &queryDesc->estate->es_jit->instr : NULL,
 				   NULL,
 				   queryDesc->estate->es_parallel_workers_to_launch,
-				   queryDesc->estate->es_parallel_workers_launched);
+				   queryDesc->estate->es_parallel_workers_launched,
+				   queryDesc->cached_plan_mode);
 	}
 
 	if (prev_ExecutorEnd)
@@ -1224,7 +1233,8 @@ pgss_ProcessUtility(PlannedStmt *pstmt, const char *queryString,
 				   NULL,
 				   NULL,
 				   0,
-				   0);
+				   0,
+				   PLAN_CACHE_MODE_NOT_SET);
 	}
 	else
 	{
@@ -1287,7 +1297,8 @@ pgss_store(const char *query, int64 queryId,
 		   const struct JitInstrumentation *jitusage,
 		   JumbleState *jstate,
 		   int parallel_workers_to_launch,
-		   int parallel_workers_launched)
+		   int parallel_workers_launched,
+		   CachedPlanMode plan_cache_mode)
 {
 	pgssHashKey key;
 	pgssEntry  *entry;
@@ -1495,6 +1506,11 @@ pgss_store(const char *query, int64 queryId,
 		entry->counters.parallel_workers_to_launch += parallel_workers_to_launch;
 		entry->counters.parallel_workers_launched += parallel_workers_launched;
 
+		if (plan_cache_mode == PLAN_CACHE_MODE_GENERIC)
+			entry->counters.generic_plan_calls++;
+		else if (plan_cache_mode == PLAN_CACHE_MODE_CUSTOM)
+			entry->counters.custom_plan_calls++;
+
 		SpinLockRelease(&entry->mutex);
 	}
 
@@ -1562,7 +1578,8 @@ pg_stat_statements_reset(PG_FUNCTION_ARGS)
 #define PG_STAT_STATEMENTS_COLS_V1_10	43
 #define PG_STAT_STATEMENTS_COLS_V1_11	49
 #define PG_STAT_STATEMENTS_COLS_V1_12	52
-#define PG_STAT_STATEMENTS_COLS			52	/* maximum of above */
+#define PG_STAT_STATEMENTS_COLS_V1_13	54
+#define PG_STAT_STATEMENTS_COLS			54	/* maximum of above */
 
 /*
  * Retrieve statement statistics.
@@ -1574,6 +1591,16 @@ pg_stat_statements_reset(PG_FUNCTION_ARGS)
  * expected API version is identified by embedding it in the C name of the
  * function.  Unfortunately we weren't bright enough to do that for 1.1.
  */
+Datum
+pg_stat_statements_1_13(PG_FUNCTION_ARGS)
+{
+	bool		showtext = PG_GETARG_BOOL(0);
+
+	pg_stat_statements_internal(fcinfo, PGSS_V1_13, showtext);
+
+	return (Datum) 0;
+}
+
 Datum
 pg_stat_statements_1_12(PG_FUNCTION_ARGS)
 {
@@ -1732,6 +1759,10 @@ pg_stat_statements_internal(FunctionCallInfo fcinfo,
 			if (api_version != PGSS_V1_12)
 				elog(ERROR, "incorrect number of output arguments");
 			break;
+		case PG_STAT_STATEMENTS_COLS_V1_13:
+			if (api_version != PGSS_V1_13)
+				elog(ERROR, "incorrect number of output arguments");
+			break;
 		default:
 			elog(ERROR, "incorrect number of output arguments");
 	}
@@ -1984,6 +2015,11 @@ pg_stat_statements_internal(FunctionCallInfo fcinfo,
 			values[i++] = Int64GetDatumFast(tmp.parallel_workers_to_launch);
 			values[i++] = Int64GetDatumFast(tmp.parallel_workers_launched);
 		}
+		if (api_version >= PGSS_V1_13)
+		{
+			values[i++] = Int64GetDatumFast(tmp.generic_plan_calls);
+			values[i++] = Int64GetDatumFast(tmp.custom_plan_calls);
+		}
 		if (api_version >= PGSS_V1_11)
 		{
 			values[i++] = TimestampTzGetDatum(stats_since);
@@ -1999,6 +2035,7 @@ pg_stat_statements_internal(FunctionCallInfo fcinfo,
 					 api_version == PGSS_V1_10 ? PG_STAT_STATEMENTS_COLS_V1_10 :
 					 api_version == PGSS_V1_11 ? PG_STAT_STATEMENTS_COLS_V1_11 :
 					 api_version == PGSS_V1_12 ? PG_STAT_STATEMENTS_COLS_V1_12 :
+					 api_version == PGSS_V1_13 ? PG_STAT_STATEMENTS_COLS_V1_13 :
 					 -1 /* fail if you forget to update this assert */ ));
 
 		tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, values, nulls);
diff --git a/contrib/pg_stat_statements/pg_stat_statements.control b/contrib/pg_stat_statements/pg_stat_statements.control
index d45ebc12e36..2eee0ceffa8 100644
--- a/contrib/pg_stat_statements/pg_stat_statements.control
+++ b/contrib/pg_stat_statements/pg_stat_statements.control
@@ -1,5 +1,5 @@
 # pg_stat_statements extension
 comment = 'track planning and execution statistics of all SQL statements executed'
-default_version = '1.12'
+default_version = '1.13'
 module_pathname = '$libdir/pg_stat_statements'
 relocatable = true
diff --git a/contrib/pg_stat_statements/sql/plancache.sql b/contrib/pg_stat_statements/sql/plancache.sql
new file mode 100644
index 00000000000..f3878889ea6
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/plancache.sql
@@ -0,0 +1,129 @@
+--
+-- Information related to plan cache
+--
+
+--
+-- Setup
+--
+CREATE OR REPLACE FUNCTION select_one_func(int) RETURNS VOID AS $$
+DECLARE
+    ret INT;
+BEGIN
+    SELECT $1 INTO ret;
+END;
+$$ LANGUAGE plpgsql;
+CREATE OR REPLACE PROCEDURE select_one_proc(int) AS $$
+DECLARE
+    ret INT;
+BEGIN
+    select $1 INTO ret;
+END;
+$$ LANGUAGE plpgsql;
+
+--
+-- plan cache counters for prepared statements
+--
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+PREPARE p1 AS SELECT $1;
+-- plan cache auto
+SET plan_cache_mode TO auto;
+EXECUTE p1(1);
+-- force generic plan
+SET plan_cache_mode TO force_generic_plan;
+EXECUTE p1(1);
+-- force custom plan
+SET plan_cache_mode TO force_custom_plan;
+EXECUTE p1(1);
+SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, query FROM pg_stat_statements
+    ORDER BY query COLLATE "C";
+DEALLOCATE p1;
+
+--
+-- plan cache counters for extended query protocol
+--
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+SELECT $1 \parse p1
+SET plan_cache_mode TO auto;
+\bind_named p1 1
+;
+SET plan_cache_mode TO force_generic_plan;
+\bind_named p1 1
+;
+SET plan_cache_mode TO force_custom_plan;
+\bind_named p1 1
+;
+SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, query FROM pg_stat_statements
+    ORDER BY query COLLATE "C";
+\close_prepared p1
+
+--
+-- plan cache counters for explain|explain (analyze) with prepared statements
+--
+SET pg_stat_statements.track = 'all';
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+PREPARE p1 AS SELECT $1;
+-- plan cache auto
+SET plan_cache_mode TO auto;
+EXPLAIN (COSTS OFF) EXECUTE p1(1);
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) EXECUTE p1(1);
+-- force generic plan
+SET plan_cache_mode TO force_generic_plan;
+EXPLAIN (COSTS OFF) EXECUTE p1(1);
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) EXECUTE p1(1);
+-- force custom plan
+SET plan_cache_mode TO force_custom_plan;
+EXPLAIN (COSTS OFF) EXECUTE p1(1);
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) EXECUTE p1(1);
+SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, query FROM pg_stat_statements
+    ORDER BY query COLLATE "C";
+RESET pg_stat_statements.track;
+DEALLOCATE p1;
+
+--
+-- plan cache counters for functions and procedures
+--
+SET pg_stat_statements.track = 'all';
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+-- plan cache auto
+SET plan_cache_mode TO auto;
+SELECT select_one_func(1);
+CALL select_one_proc(1);
+-- force generic plan
+SET plan_cache_mode TO force_generic_plan;
+SELECT select_one_func(1);
+CALL select_one_proc(1);
+-- force custom plan
+SET plan_cache_mode TO force_custom_plan;
+SELECT select_one_func(1);
+CALL select_one_proc(1);
+SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, query FROM pg_stat_statements
+    ORDER BY query COLLATE "C";
+
+--
+-- plan cache counters for explain|explain (analyze) with functions and procedures
+--
+SET pg_stat_statements.track = 'all';
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+-- plan cache auto
+SET plan_cache_mode TO auto;
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) SELECT select_one_func(1);
+EXPLAIN (COSTS OFF) SELECT select_one_func(1);
+CALL select_one_proc(1);
+-- force generic plan
+SET plan_cache_mode TO force_generic_plan;
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) SELECT select_one_func(1);
+EXPLAIN (COSTS OFF) SELECT select_one_func(1);
+CALL select_one_proc(1);
+-- force custom plan
+SET plan_cache_mode TO force_custom_plan;
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) SELECT select_one_func(1);
+EXPLAIN (COSTS OFF) SELECT select_one_func(1);
+CALL select_one_proc(1);
+SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, query FROM pg_stat_statements
+    ORDER BY query COLLATE "C";
+
+--
+-- Cleanup
+--
+DROP FUNCTION select_one_func(int);
+DROP PROCEDURE select_one_proc(int);
diff --git a/doc/src/sgml/pgstatstatements.sgml b/doc/src/sgml/pgstatstatements.sgml
index 7baa07dcdbf..08b5fec9e10 100644
--- a/doc/src/sgml/pgstatstatements.sgml
+++ b/doc/src/sgml/pgstatstatements.sgml
@@ -554,6 +554,24 @@
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>generic_plan_calls</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Number of times the statement was executed using a generic plan
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>custom_plan_calls</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Number of times the statement was executed using a custom plan
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>stats_since</structfield> <type>timestamp with time zone</type>
diff --git a/src/backend/commands/copyto.c b/src/backend/commands/copyto.c
index 67b94b91cae..224d273b2b9 100644
--- a/src/backend/commands/copyto.c
+++ b/src/backend/commands/copyto.c
@@ -838,7 +838,7 @@ BeginCopyTo(ParseState *pstate,
 		cstate->queryDesc = CreateQueryDesc(plan, pstate->p_sourcetext,
 											GetActiveSnapshot(),
 											InvalidSnapshot,
-											dest, NULL, NULL, 0);
+											dest, NULL, NULL, 0, NULL);
 
 		/*
 		 * Call ExecutorStart to prepare the plan for execution.
diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c
index dfd2ab8e862..40ba1183207 100644
--- a/src/backend/commands/createas.c
+++ b/src/backend/commands/createas.c
@@ -336,7 +336,7 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt,
 		/* Create a QueryDesc, redirecting output to our tuple receiver */
 		queryDesc = CreateQueryDesc(plan, pstate->p_sourcetext,
 									GetActiveSnapshot(), InvalidSnapshot,
-									dest, params, queryEnv, 0);
+									dest, params, queryEnv, 0, NULL);
 
 		/* call ExecutorStart to prepare the plan for execution */
 		ExecutorStart(queryDesc, GetIntoRelEFlags(into));
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 7e2792ead71..1b0b7b64c1b 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -371,7 +371,7 @@ standard_ExplainOneQuery(Query *query, int cursorOptions,
 	/* run it (if needed) and produce output */
 	ExplainOnePlan(plan, into, es, queryString, params, queryEnv,
 				   &planduration, (es->buffers ? &bufusage : NULL),
-				   es->memory ? &mem_counters : NULL);
+				   es->memory ? &mem_counters : NULL, NULL);
 }
 
 /*
@@ -495,7 +495,8 @@ ExplainOnePlan(PlannedStmt *plannedstmt, IntoClause *into, ExplainState *es,
 			   const char *queryString, ParamListInfo params,
 			   QueryEnvironment *queryEnv, const instr_time *planduration,
 			   const BufferUsage *bufusage,
-			   const MemoryContextCounters *mem_counters)
+			   const MemoryContextCounters *mem_counters,
+			   CachedPlan *cplan)
 {
 	DestReceiver *dest;
 	QueryDesc  *queryDesc;
@@ -549,7 +550,7 @@ ExplainOnePlan(PlannedStmt *plannedstmt, IntoClause *into, ExplainState *es,
 	/* Create a QueryDesc for the query */
 	queryDesc = CreateQueryDesc(plannedstmt, queryString,
 								GetActiveSnapshot(), InvalidSnapshot,
-								dest, params, queryEnv, instrument_option);
+								dest, params, queryEnv, instrument_option, cplan);
 
 	/* Select execution options */
 	if (es->analyze)
diff --git a/src/backend/commands/extension.c b/src/backend/commands/extension.c
index e6f9ab6dfd6..b1394b10b0c 100644
--- a/src/backend/commands/extension.c
+++ b/src/backend/commands/extension.c
@@ -995,7 +995,7 @@ execute_sql_string(const char *sql, const char *filename)
 				qdesc = CreateQueryDesc(stmt,
 										sql,
 										GetActiveSnapshot(), NULL,
-										dest, NULL, NULL, 0);
+										dest, NULL, NULL, 0, NULL);
 
 				ExecutorStart(qdesc, 0);
 				ExecutorRun(qdesc, ForwardScanDirection, 0);
diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index 188e26f0e6e..a415209fc85 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -440,7 +440,7 @@ refresh_matview_datafill(DestReceiver *dest, Query *query,
 	/* Create a QueryDesc, redirecting output to our tuple receiver */
 	queryDesc = CreateQueryDesc(plan, queryString,
 								GetActiveSnapshot(), InvalidSnapshot,
-								dest, NULL, NULL, 0);
+								dest, NULL, NULL, 0, NULL);
 
 	/* call ExecutorStart to prepare the plan for execution */
 	ExecutorStart(queryDesc, 0);
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..55e08b2a63f 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -659,7 +659,7 @@ ExplainExecuteQuery(ExecuteStmt *execstmt, IntoClause *into, ExplainState *es,
 		if (pstmt->commandType != CMD_UTILITY)
 			ExplainOnePlan(pstmt, into, es, query_string, paramLI, pstate->p_queryEnv,
 						   &planduration, (es->buffers ? &bufusage : NULL),
-						   es->memory ? &mem_counters : NULL);
+						   es->memory ? &mem_counters : NULL, cplan);
 		else
 			ExplainOneUtility(pstmt->utilityStmt, into, es, pstate, paramLI);
 
diff --git a/src/backend/executor/execParallel.c b/src/backend/executor/execParallel.c
index f3e77bda279..2acb8e738ac 100644
--- a/src/backend/executor/execParallel.c
+++ b/src/backend/executor/execParallel.c
@@ -1282,7 +1282,7 @@ ExecParallelGetQueryDesc(shm_toc *toc, DestReceiver *receiver,
 	return CreateQueryDesc(pstmt,
 						   queryString,
 						   GetActiveSnapshot(), InvalidSnapshot,
-						   receiver, paramLI, NULL, instrument_options);
+						   receiver, paramLI, NULL, instrument_options, NULL);
 }
 
 /*
diff --git a/src/backend/executor/functions.c b/src/backend/executor/functions.c
index 359aafea681..041d44e77b4 100644
--- a/src/backend/executor/functions.c
+++ b/src/backend/executor/functions.c
@@ -1345,7 +1345,8 @@ postquel_start(execution_state *es, SQLFunctionCachePtr fcache)
 							 dest,
 							 fcache->paramLI,
 							 es->qd ? es->qd->queryEnv : NULL,
-							 0);
+							 0,
+							 fcache->cplan);
 
 	/* Utility commands don't need Executor. */
 	if (es->qd->operation != CMD_UTILITY)
diff --git a/src/backend/executor/spi.c b/src/backend/executor/spi.c
index ecb2e4ccaa1..afc9e094594 100644
--- a/src/backend/executor/spi.c
+++ b/src/backend/executor/spi.c
@@ -2695,7 +2695,9 @@ _SPI_execute_plan(SPIPlanPtr plan, const SPIExecuteOptions *options,
 										dest,
 										options->params,
 										_SPI_current->queryEnv,
-										0);
+										0,
+										cplan);
+
 				res = _SPI_pquery(qdesc, fire_triggers,
 								  canSetTag ? options->tcount : 0);
 				FreeQueryDesc(qdesc);
diff --git a/src/backend/tcop/pquery.c b/src/backend/tcop/pquery.c
index 08791b8f75e..d70f34b5061 100644
--- a/src/backend/tcop/pquery.c
+++ b/src/backend/tcop/pquery.c
@@ -26,6 +26,7 @@
 #include "tcop/pquery.h"
 #include "tcop/utility.h"
 #include "utils/memutils.h"
+#include "utils/plancache.h"
 #include "utils/snapmgr.h"
 
 
@@ -41,7 +42,8 @@ static void ProcessQuery(PlannedStmt *plan,
 						 ParamListInfo params,
 						 QueryEnvironment *queryEnv,
 						 DestReceiver *dest,
-						 QueryCompletion *qc);
+						 QueryCompletion *qc,
+						 CachedPlan *cplan);
 static void FillPortalStore(Portal portal, bool isTopLevel);
 static uint64 RunFromStore(Portal portal, ScanDirection direction, uint64 count,
 						   DestReceiver *dest);
@@ -72,7 +74,8 @@ CreateQueryDesc(PlannedStmt *plannedstmt,
 				DestReceiver *dest,
 				ParamListInfo params,
 				QueryEnvironment *queryEnv,
-				int instrument_options)
+				int instrument_options,
+				CachedPlan *cplan)
 {
 	QueryDesc  *qd = (QueryDesc *) palloc(sizeof(QueryDesc));
 
@@ -93,6 +96,9 @@ CreateQueryDesc(PlannedStmt *plannedstmt,
 	qd->planstate = NULL;
 	qd->totaltime = NULL;
 
+	/* set cached_plan_mode, if applicable */
+	qd->cached_plan_mode = cplan ? cplan->mode : PLAN_CACHE_MODE_NOT_SET;
+
 	/* not yet executed */
 	qd->already_executed = false;
 
@@ -139,7 +145,8 @@ ProcessQuery(PlannedStmt *plan,
 			 ParamListInfo params,
 			 QueryEnvironment *queryEnv,
 			 DestReceiver *dest,
-			 QueryCompletion *qc)
+			 QueryCompletion *qc,
+			 CachedPlan *cplan)
 {
 	QueryDesc  *queryDesc;
 
@@ -148,7 +155,7 @@ ProcessQuery(PlannedStmt *plan,
 	 */
 	queryDesc = CreateQueryDesc(plan, sourceText,
 								GetActiveSnapshot(), InvalidSnapshot,
-								dest, params, queryEnv, 0);
+								dest, params, queryEnv, 0, cplan);
 
 	/*
 	 * Call ExecutorStart to prepare the plan for execution
@@ -500,7 +507,8 @@ PortalStart(Portal portal, ParamListInfo params,
 											None_Receiver,
 											params,
 											portal->queryEnv,
-											0);
+											0,
+											portal->cplan);
 
 				/*
 				 * If it's a scrollable cursor, executor needs to support
@@ -1273,7 +1281,7 @@ PortalRunMulti(Portal portal,
 							 portal->sourceText,
 							 portal->portalParams,
 							 portal->queryEnv,
-							 dest, qc);
+							 dest, qc, portal->cplan);
 			}
 			else
 			{
@@ -1282,7 +1290,7 @@ PortalRunMulti(Portal portal,
 							 portal->sourceText,
 							 portal->portalParams,
 							 portal->queryEnv,
-							 altdest, NULL);
+							 altdest, NULL, portal->cplan);
 			}
 
 			if (log_executor_stats)
diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c
index 89a1c79e984..af7f9932fab 100644
--- a/src/backend/utils/cache/plancache.c
+++ b/src/backend/utils/cache/plancache.c
@@ -1140,6 +1140,7 @@ BuildCachedPlan(CachedPlanSource *plansource, List *qlist,
 	plan->is_oneshot = plansource->is_oneshot;
 	plan->is_saved = false;
 	plan->is_valid = true;
+	plan->mode = PLAN_CACHE_MODE_NOT_SET;
 
 	/* assign generation number to new plan */
 	plan->generation = ++(plansource->generation);
@@ -1358,10 +1359,13 @@ GetCachedPlan(CachedPlanSource *plansource, ParamListInfo boundParams,
 		plansource->total_custom_cost += cached_plan_cost(plan, true);
 
 		plansource->num_custom_plans++;
+
+		plan->mode = PLAN_CACHE_MODE_CUSTOM;
 	}
 	else
 	{
 		plansource->num_generic_plans++;
+		plan->mode = PLAN_CACHE_MODE_GENERIC;
 	}
 
 	Assert(plan != NULL);
diff --git a/src/include/commands/explain.h b/src/include/commands/explain.h
index 3b122f79ed8..bf8e5f5d90d 100644
--- a/src/include/commands/explain.h
+++ b/src/include/commands/explain.h
@@ -15,6 +15,7 @@
 
 #include "executor/executor.h"
 #include "parser/parse_node.h"
+#include "utils/plancache.h"
 
 struct ExplainState;			/* defined in explain_state.h */
 
@@ -68,7 +69,8 @@ extern void ExplainOnePlan(PlannedStmt *plannedstmt, IntoClause *into,
 						   ParamListInfo params, QueryEnvironment *queryEnv,
 						   const instr_time *planduration,
 						   const BufferUsage *bufusage,
-						   const MemoryContextCounters *mem_counters);
+						   const MemoryContextCounters *mem_counters,
+						   CachedPlan *cplan);
 
 extern void ExplainPrintPlan(struct ExplainState *es, QueryDesc *queryDesc);
 extern void ExplainPrintTriggers(struct ExplainState *es,
diff --git a/src/include/executor/execdesc.h b/src/include/executor/execdesc.h
index 86db3dc8d0d..bdffb2184e7 100644
--- a/src/include/executor/execdesc.h
+++ b/src/include/executor/execdesc.h
@@ -17,6 +17,7 @@
 
 #include "nodes/execnodes.h"
 #include "tcop/dest.h"
+#include "utils/plancache.h"
 
 
 /* ----------------
@@ -51,6 +52,9 @@ typedef struct QueryDesc
 	/* This field is set by ExecutePlan */
 	bool		already_executed;	/* true if previously executed */
 
+	CachedPlanMode cached_plan_mode;	/* the plan cache mode of the cached
+										 * plan, if there is one */
+
 	/* This is always set NULL by the core system, but plugins can change it */
 	struct Instrumentation *totaltime;	/* total time spent in ExecutorRun */
 } QueryDesc;
@@ -63,7 +67,8 @@ extern QueryDesc *CreateQueryDesc(PlannedStmt *plannedstmt,
 								  DestReceiver *dest,
 								  ParamListInfo params,
 								  QueryEnvironment *queryEnv,
-								  int instrument_options);
+								  int instrument_options,
+								  CachedPlan *cplan);
 
 extern void FreeQueryDesc(QueryDesc *qdesc);
 
diff --git a/src/include/utils/plancache.h b/src/include/utils/plancache.h
index 1baa6d50bfd..434d6406853 100644
--- a/src/include/utils/plancache.h
+++ b/src/include/utils/plancache.h
@@ -35,6 +35,14 @@ typedef enum
 	PLAN_CACHE_MODE_FORCE_CUSTOM_PLAN,
 }			PlanCacheMode;
 
+/* possible values for a CachedPlan mode */
+typedef enum
+{
+	PLAN_CACHE_MODE_NOT_SET = 0,
+	PLAN_CACHE_MODE_GENERIC,
+	PLAN_CACHE_MODE_CUSTOM,
+}			CachedPlanMode;
+
 /* GUC parameter */
 extern PGDLLIMPORT int plan_cache_mode;
 
@@ -169,6 +177,7 @@ typedef struct CachedPlan
 								 * changes from this value */
 	int			generation;		/* parent's generation number for this plan */
 	int			refcount;		/* count of live references to this struct */
+	CachedPlanMode mode;		/* The plan cache mode of this cached plan */
 	MemoryContext context;		/* context containing this CachedPlan */
 } CachedPlan;
 
-- 
2.39.5 (Apple Git-154)

#31Andrei Lepikhov
lepihov@gmail.com
In reply to: Sami Imseih (#30)
Re: track generic and custom plans in pg_stat_statements

On 18/7/2025 21:37, Sami Imseih wrote:

Thanks for clearing up my understanding. Essentially, override the
current cost-based
method of determining custom vs. generic by using something like execution time,
which is somehow tracked by the extension. That is how I understand this.
Now, I wonder if it may be a good idea to add some hooks in GetCachedPlan
to make this work?

Yes, it may work. But inside the GetCachedPlan, we still don't have
access to the executed statement yet.

Adding CachedPlanSource to QueryDesc seems doable. However, Michael
previously objected to adding CachedPlan to QueryDesc. Is there any
similar hesitation about including CachedPlanSource?

I agree that we should investigate further to find the most optimal
solution. Personally, I'm open to including an internal reference to a
plan cache entry within the QueryDesc, as long as the plan has its roots
there.

For the sake of this feature, I suspect making CachedPlanSource available
in QueryDesc will be a non-starter, but I would like to hear other opinions.
To accomplish the goals of this patch, we definitely need the current
execution’s
"plan cache mode" to be accessible in ExecutorEnd somehow.

I agree with this - adding references to CachedPlan into the QueryDesc
looks kludge.
The most boring aspect of pg_stat_statements for me is the multiple
statements case: a single incoming query (the only case in the cache of
a generic plan) may be rewritten as various statements with the same
query ID. So, the execution time of the initial statement should be the
sum of the executions of all rewritten parts.
I wonder if the pg_stat_statements processes it correctly at the moment.
Because in this case, we'd need a hook inside the Portal execution code.
However, I don't recall any threads on the mailing list discussing that.

If the multiple-statements case didn't exist for a cached statement,
Michael's idea with an active portal would be much better, and it would
make sense to add PlanCacheSource::cplan and replace Portal::CachedPlan
with Portal::PlanCacheSource reference.

--
regards, Andrei Lepikhov

#32Sami Imseih
samimseih@gmail.com
In reply to: Andrei Lepikhov (#31)
1 attachment(s)
Re: track generic and custom plans in pg_stat_statements

"plan cache mode" to be accessible in ExecutorEnd somehow.

I agree with this - adding references to CachedPlan into the QueryDesc
looks kludge.
The most boring aspect of pg_stat_statements for me is the multiple
statements case: a single incoming query (the only case in the cache of
a generic plan) may be rewritten as various statements with the same
query ID. So, the execution time of the initial statement should be the
sum of the executions of all rewritten parts.

AFAICT, It does sum up the stats of rewritten statements of a single statement,
since each one of those statements does call ExecutorEnd ( under the same
queryId ).

If the multiple-statements case didn't exist for a cached statement,
Michael's idea with an active portal would be much better, and it would
make sense to add PlanCacheSource::cplan and replace Portal::CachedPlan
with Portal::PlanCacheSource reference.

I don't like ActivePortal because cplan itself does not live by ExecutorEnd [0]/messages/by-id/CAA5RZ0t4kmbBM+etEQVb1c8bMSWjKOY8zTEA43X2UhSu0A8dCA@mail.gmail.com
and in some cases it's not available in ExecutorStart ( think of
Explain/Explain Analyze),
so you still need to add some handling for those cases, which I don't
think we can
ignore. Also, if we say we can forgo tracking the Explain/Explain Analyze case,
we surely don't want to call pgss_store at ExecutorStart to set the plan cache
mode stats and ExecutorEnd to set everything else for a particular execution.
In short, we still need some fields to be added to QueryDesc to track the
plan cache mode info.

Last week I published a v11 that adds a field to QueryDesc, but as I thought
about this more, how about we just add 2 bool fields in QueryDesc->estate
( es_cached_plan and es_is_generic_plan ), a field in CachedPlan (
is_generic_plan )
and after ExecutorStart, and if we have a cplan, we set the
appropriate plan cache
mode value. I think it's better to confine these new fields in Estate
rather than
at a higher level like QueryDesc. See v12.

[0]: /messages/by-id/CAA5RZ0t4kmbBM+etEQVb1c8bMSWjKOY8zTEA43X2UhSu0A8dCA@mail.gmail.com

--

Sami

Attachments:

v12-0001-Add-counters-for-generic-and-custom-plan-executi.patchapplication/octet-stream; name=v12-0001-Add-counters-for-generic-and-custom-plan-executi.patchDownload
From 1508510c47e8c5189db5efc7feb2afbc91acfcdb Mon Sep 17 00:00:00 2001
From: Sami Imseih <simseih@amazon.com>
Date: Mon, 21 Jul 2025 14:43:01 -0500
Subject: [PATCH v12 1/1] Add counters for generic and custom plan executions
 to pg_stat_statements

This patch adds two new counters to pg_stat_statements:
- generic_plan_calls
- custom_plan_calls

These track how many times a prepared statement was executed using a
generic or custom plan, respectively.

To support this, two new fields are added to QueryDesc->estate:
- es_cached_plan
- es_is_generic_plan

These fields are set when a CachedPlan is used, with es_cached_plan
set to true and es_is_generic_plan set to cplan->is_generic_plan.
A new is_generic_plan field is also added to CachedPlan and set
during GetCachedPlan.
---
 contrib/pg_stat_statements/Makefile           |   3 +-
 .../pg_stat_statements/expected/plancache.out | 295 ++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |   2 +
 .../pg_stat_statements--1.12--1.13.sql        |  78 +++++
 .../pg_stat_statements/pg_stat_statements.c   |  67 +++-
 .../pg_stat_statements.control                |   2 +-
 contrib/pg_stat_statements/sql/plancache.sql  | 129 ++++++++
 doc/src/sgml/pgstatstatements.sgml            |  18 ++
 src/backend/commands/explain.c                |  11 +-
 src/backend/commands/prepare.c                |   2 +-
 src/backend/executor/execUtils.c              |   3 +
 src/backend/executor/functions.c              |   6 +
 src/backend/executor/spi.c                    |  14 +-
 src/backend/tcop/pquery.c                     |  21 +-
 src/backend/utils/cache/plancache.c           |   3 +
 src/include/commands/explain.h                |   4 +-
 src/include/nodes/execnodes.h                 |   4 +
 src/include/utils/plancache.h                 |   1 +
 18 files changed, 643 insertions(+), 20 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/plancache.out
 create mode 100644 contrib/pg_stat_statements/pg_stat_statements--1.12--1.13.sql
 create mode 100644 contrib/pg_stat_statements/sql/plancache.sql

diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index b2bd8794d2a..996a5fac448 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -7,6 +7,7 @@ OBJS = \
 
 EXTENSION = pg_stat_statements
 DATA = pg_stat_statements--1.4.sql \
+	pg_stat_statements--1.12--1.13.sql \
 	pg_stat_statements--1.11--1.12.sql pg_stat_statements--1.10--1.11.sql \
 	pg_stat_statements--1.9--1.10.sql pg_stat_statements--1.8--1.9.sql \
 	pg_stat_statements--1.7--1.8.sql pg_stat_statements--1.6--1.7.sql \
@@ -20,7 +21,7 @@ LDFLAGS_SL += $(filter -lm, $(LIBS))
 REGRESS_OPTS = --temp-config $(top_srcdir)/contrib/pg_stat_statements/pg_stat_statements.conf
 REGRESS = select dml cursors utility level_tracking planning \
 	user_activity wal entry_timestamp privileges extended \
-	parallel cleanup oldextversions squashing
+	parallel cleanup oldextversions squashing plancache
 # Disabled because these tests require "shared_preload_libraries=pg_stat_statements",
 # which typical installcheck users do not have (e.g. buildfarm clients).
 NO_INSTALLCHECK = 1
diff --git a/contrib/pg_stat_statements/expected/plancache.out b/contrib/pg_stat_statements/expected/plancache.out
new file mode 100644
index 00000000000..7b05d5a425f
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/plancache.out
@@ -0,0 +1,295 @@
+--
+-- Information related to plan cache
+--
+--
+-- Setup
+--
+CREATE OR REPLACE FUNCTION select_one_func(int) RETURNS VOID AS $$
+DECLARE
+    ret INT;
+BEGIN
+    SELECT $1 INTO ret;
+END;
+$$ LANGUAGE plpgsql;
+CREATE OR REPLACE PROCEDURE select_one_proc(int) AS $$
+DECLARE
+    ret INT;
+BEGIN
+    select $1 INTO ret;
+END;
+$$ LANGUAGE plpgsql;
+--
+-- plan cache counters for prepared statements
+--
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+PREPARE p1 AS SELECT $1;
+-- plan cache auto
+SET plan_cache_mode TO auto;
+EXECUTE p1(1);
+ ?column? 
+----------
+ 1
+(1 row)
+
+-- force generic plan
+SET plan_cache_mode TO force_generic_plan;
+EXECUTE p1(1);
+ ?column? 
+----------
+ 1
+(1 row)
+
+-- force custom plan
+SET plan_cache_mode TO force_custom_plan;
+EXECUTE p1(1);
+ ?column? 
+----------
+ 1
+(1 row)
+
+SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, query FROM pg_stat_statements
+    ORDER BY query COLLATE "C";
+ calls | generic_plan_calls | custom_plan_calls | toplevel |                       query                        
+-------+--------------------+-------------------+----------+----------------------------------------------------
+     3 |                  1 |                 2 | t        | PREPARE p1 AS SELECT $1
+     1 |                  0 |                 0 | t        | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+     3 |                  0 |                 0 | t        | SET plan_cache_mode TO $1
+(3 rows)
+
+DEALLOCATE p1;
+--
+-- plan cache counters for extended query protocol
+--
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+SELECT $1 \parse p1
+SET plan_cache_mode TO auto;
+\bind_named p1 1
+;
+ ?column? 
+----------
+ 1
+(1 row)
+
+SET plan_cache_mode TO force_generic_plan;
+\bind_named p1 1
+;
+ ?column? 
+----------
+ 1
+(1 row)
+
+SET plan_cache_mode TO force_custom_plan;
+\bind_named p1 1
+;
+ ?column? 
+----------
+ 1
+(1 row)
+
+SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, query FROM pg_stat_statements
+    ORDER BY query COLLATE "C";
+ calls | generic_plan_calls | custom_plan_calls | toplevel |                       query                        
+-------+--------------------+-------------------+----------+----------------------------------------------------
+     3 |                  1 |                 2 | t        | SELECT $1
+     1 |                  0 |                 0 | t        | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+     3 |                  0 |                 0 | t        | SET plan_cache_mode TO $1
+(3 rows)
+
+\close_prepared p1
+--
+-- plan cache counters for explain|explain (analyze) with prepared statements
+--
+SET pg_stat_statements.track = 'all';
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+PREPARE p1 AS SELECT $1;
+-- plan cache auto
+SET plan_cache_mode TO auto;
+EXPLAIN (COSTS OFF) EXECUTE p1(1);
+ QUERY PLAN 
+------------
+ Result
+(1 row)
+
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) EXECUTE p1(1);
+            QUERY PLAN             
+-----------------------------------
+ Result (actual rows=1.00 loops=1)
+(1 row)
+
+-- force generic plan
+SET plan_cache_mode TO force_generic_plan;
+EXPLAIN (COSTS OFF) EXECUTE p1(1);
+ QUERY PLAN 
+------------
+ Result
+(1 row)
+
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) EXECUTE p1(1);
+            QUERY PLAN             
+-----------------------------------
+ Result (actual rows=1.00 loops=1)
+(1 row)
+
+-- force custom plan
+SET plan_cache_mode TO force_custom_plan;
+EXPLAIN (COSTS OFF) EXECUTE p1(1);
+ QUERY PLAN 
+------------
+ Result
+(1 row)
+
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) EXECUTE p1(1);
+            QUERY PLAN             
+-----------------------------------
+ Result (actual rows=1.00 loops=1)
+(1 row)
+
+SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, query FROM pg_stat_statements
+    ORDER BY query COLLATE "C";
+ calls | generic_plan_calls | custom_plan_calls | toplevel |                                      query                                       
+-------+--------------------+-------------------+----------+----------------------------------------------------------------------------------
+     3 |                  0 |                 0 | t        | EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) EXECUTE p1(1)
+     3 |                  0 |                 0 | t        | EXPLAIN (COSTS OFF) EXECUTE p1(1)
+     6 |                  2 |                 4 | f        | PREPARE p1 AS SELECT $1
+     1 |                  0 |                 0 | t        | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+     3 |                  0 |                 0 | t        | SET plan_cache_mode TO $1
+(5 rows)
+
+RESET pg_stat_statements.track;
+DEALLOCATE p1;
+--
+-- plan cache counters for functions and procedures
+--
+SET pg_stat_statements.track = 'all';
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- plan cache auto
+SET plan_cache_mode TO auto;
+SELECT select_one_func(1);
+ select_one_func 
+-----------------
+ 
+(1 row)
+
+CALL select_one_proc(1);
+-- force generic plan
+SET plan_cache_mode TO force_generic_plan;
+SELECT select_one_func(1);
+ select_one_func 
+-----------------
+ 
+(1 row)
+
+CALL select_one_proc(1);
+-- force custom plan
+SET plan_cache_mode TO force_custom_plan;
+SELECT select_one_func(1);
+ select_one_func 
+-----------------
+ 
+(1 row)
+
+CALL select_one_proc(1);
+SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, query FROM pg_stat_statements
+    ORDER BY query COLLATE "C";
+ calls | generic_plan_calls | custom_plan_calls | toplevel |                       query                        
+-------+--------------------+-------------------+----------+----------------------------------------------------
+     3 |                  0 |                 0 | t        | CALL select_one_proc($1)
+     6 |                  2 |                 4 | f        | SELECT $1
+     1 |                  0 |                 0 | t        | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+     3 |                  0 |                 0 | t        | SELECT select_one_func($1)
+     3 |                  0 |                 0 | t        | SET plan_cache_mode TO $1
+(5 rows)
+
+--
+-- plan cache counters for explain|explain (analyze) with functions and procedures
+--
+SET pg_stat_statements.track = 'all';
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- plan cache auto
+SET plan_cache_mode TO auto;
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) SELECT select_one_func(1);
+            QUERY PLAN             
+-----------------------------------
+ Result (actual rows=1.00 loops=1)
+(1 row)
+
+EXPLAIN (COSTS OFF) SELECT select_one_func(1);
+ QUERY PLAN 
+------------
+ Result
+(1 row)
+
+CALL select_one_proc(1);
+-- force generic plan
+SET plan_cache_mode TO force_generic_plan;
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) SELECT select_one_func(1);
+            QUERY PLAN             
+-----------------------------------
+ Result (actual rows=1.00 loops=1)
+(1 row)
+
+EXPLAIN (COSTS OFF) SELECT select_one_func(1);
+ QUERY PLAN 
+------------
+ Result
+(1 row)
+
+CALL select_one_proc(1);
+-- force custom plan
+SET plan_cache_mode TO force_custom_plan;
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) SELECT select_one_func(1);
+            QUERY PLAN             
+-----------------------------------
+ Result (actual rows=1.00 loops=1)
+(1 row)
+
+EXPLAIN (COSTS OFF) SELECT select_one_func(1);
+ QUERY PLAN 
+------------
+ Result
+(1 row)
+
+CALL select_one_proc(1);
+SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, query FROM pg_stat_statements
+    ORDER BY query COLLATE "C";
+ calls | generic_plan_calls | custom_plan_calls | toplevel |                                             query                                              
+-------+--------------------+-------------------+----------+------------------------------------------------------------------------------------------------
+     3 |                  0 |                 0 | t        | CALL select_one_proc($1)
+     3 |                  0 |                 0 | t        | EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) SELECT select_one_func($1)
+     6 |                  0 |                 0 | f        | EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) SELECT select_one_func($1);
+     3 |                  0 |                 0 | t        | EXPLAIN (COSTS OFF) SELECT select_one_func($1)
+     6 |                  2 |                 4 | f        | SELECT $1
+     1 |                  0 |                 0 | t        | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+     3 |                  0 |                 0 | t        | SET plan_cache_mode TO $1
+(7 rows)
+
+--
+-- Cleanup
+--
+DROP FUNCTION select_one_func(int);
+DROP PROCEDURE select_one_proc(int);
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 01a6cbdcf61..110fb82fe12 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -21,6 +21,7 @@ contrib_targets += pg_stat_statements
 install_data(
   'pg_stat_statements.control',
   'pg_stat_statements--1.4.sql',
+  'pg_stat_statements--1.12--1.13.sql',
   'pg_stat_statements--1.11--1.12.sql',
   'pg_stat_statements--1.10--1.11.sql',
   'pg_stat_statements--1.9--1.10.sql',
@@ -57,6 +58,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'plancache',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/pg_stat_statements--1.12--1.13.sql b/contrib/pg_stat_statements/pg_stat_statements--1.12--1.13.sql
new file mode 100644
index 00000000000..2f0eaf14ec3
--- /dev/null
+++ b/contrib/pg_stat_statements/pg_stat_statements--1.12--1.13.sql
@@ -0,0 +1,78 @@
+/* contrib/pg_stat_statements/pg_stat_statements--1.12--1.13.sql */
+
+-- complain if script is sourced in psql, rather than via ALTER EXTENSION
+\echo Use "ALTER EXTENSION pg_stat_statements UPDATE TO '1.13'" to load this file. \quit
+
+/* First we have to remove them from the extension */
+ALTER EXTENSION pg_stat_statements DROP VIEW pg_stat_statements;
+ALTER EXTENSION pg_stat_statements DROP FUNCTION pg_stat_statements(boolean);
+
+/* Then we can drop them */
+DROP VIEW pg_stat_statements;
+DROP FUNCTION pg_stat_statements(boolean);
+
+/* Now redefine */
+CREATE FUNCTION pg_stat_statements(IN showtext boolean,
+    OUT userid oid,
+    OUT dbid oid,
+    OUT toplevel bool,
+    OUT queryid bigint,
+    OUT query text,
+    OUT plans int8,
+    OUT total_plan_time float8,
+    OUT min_plan_time float8,
+    OUT max_plan_time float8,
+    OUT mean_plan_time float8,
+    OUT stddev_plan_time float8,
+    OUT calls int8,
+    OUT total_exec_time float8,
+    OUT min_exec_time float8,
+    OUT max_exec_time float8,
+    OUT mean_exec_time float8,
+    OUT stddev_exec_time float8,
+    OUT rows int8,
+    OUT shared_blks_hit int8,
+    OUT shared_blks_read int8,
+    OUT shared_blks_dirtied int8,
+    OUT shared_blks_written int8,
+    OUT local_blks_hit int8,
+    OUT local_blks_read int8,
+    OUT local_blks_dirtied int8,
+    OUT local_blks_written int8,
+    OUT temp_blks_read int8,
+    OUT temp_blks_written int8,
+    OUT shared_blk_read_time float8,
+    OUT shared_blk_write_time float8,
+    OUT local_blk_read_time float8,
+    OUT local_blk_write_time float8,
+    OUT temp_blk_read_time float8,
+    OUT temp_blk_write_time float8,
+    OUT wal_records int8,
+    OUT wal_fpi int8,
+    OUT wal_bytes numeric,
+    OUT wal_buffers_full int8,
+    OUT jit_functions int8,
+    OUT jit_generation_time float8,
+    OUT jit_inlining_count int8,
+    OUT jit_inlining_time float8,
+    OUT jit_optimization_count int8,
+    OUT jit_optimization_time float8,
+    OUT jit_emission_count int8,
+    OUT jit_emission_time float8,
+    OUT jit_deform_count int8,
+    OUT jit_deform_time float8,
+    OUT parallel_workers_to_launch int8,
+    OUT parallel_workers_launched int8,
+    OUT generic_plan_calls int8,
+    OUT custom_plan_calls int8,
+    OUT stats_since timestamp with time zone,
+    OUT minmax_stats_since timestamp with time zone
+)
+RETURNS SETOF record
+AS 'MODULE_PATHNAME', 'pg_stat_statements_1_13'
+LANGUAGE C STRICT VOLATILE PARALLEL SAFE;
+
+CREATE VIEW pg_stat_statements AS
+  SELECT * FROM pg_stat_statements(true);
+
+GRANT SELECT ON pg_stat_statements TO PUBLIC;
diff --git a/contrib/pg_stat_statements/pg_stat_statements.c b/contrib/pg_stat_statements/pg_stat_statements.c
index e7857f81ec0..9fea390bc29 100644
--- a/contrib/pg_stat_statements/pg_stat_statements.c
+++ b/contrib/pg_stat_statements/pg_stat_statements.c
@@ -114,6 +114,7 @@ typedef enum pgssVersion
 	PGSS_V1_10,
 	PGSS_V1_11,
 	PGSS_V1_12,
+	PGSS_V1_13,
 } pgssVersion;
 
 typedef enum pgssStoreKind
@@ -131,6 +132,13 @@ typedef enum pgssStoreKind
 
 #define PGSS_NUMKIND (PGSS_EXEC + 1)
 
+typedef enum pgssCachedPlanMode
+{
+	PGSS_PLAN_CACHE_MODE_INVALID = 0,
+	PGSS_PLAN_CACHE_MODE_GENERIC = 1,
+	PGSS_PLAN_CACHE_MODE_CUSTOM = 2,
+}			pgssCachedPlanMode;
+
 /*
  * Hashtable key that defines the identity of a hashtable entry.  We separate
  * queries by user and by database even if they are otherwise identical.
@@ -210,6 +218,8 @@ typedef struct Counters
 											 * to be launched */
 	int64		parallel_workers_launched;	/* # of parallel workers actually
 											 * launched */
+	int64		generic_plan_calls; /* number of calls using a generic plan */
+	int64		custom_plan_calls;	/* number of calls using a custom plan */
 } Counters;
 
 /*
@@ -323,6 +333,7 @@ PG_FUNCTION_INFO_V1(pg_stat_statements_1_9);
 PG_FUNCTION_INFO_V1(pg_stat_statements_1_10);
 PG_FUNCTION_INFO_V1(pg_stat_statements_1_11);
 PG_FUNCTION_INFO_V1(pg_stat_statements_1_12);
+PG_FUNCTION_INFO_V1(pg_stat_statements_1_13);
 PG_FUNCTION_INFO_V1(pg_stat_statements);
 PG_FUNCTION_INFO_V1(pg_stat_statements_info);
 
@@ -355,7 +366,8 @@ static void pgss_store(const char *query, int64 queryId,
 					   const struct JitInstrumentation *jitusage,
 					   JumbleState *jstate,
 					   int parallel_workers_to_launch,
-					   int parallel_workers_launched);
+					   int parallel_workers_launched,
+					   pgssCachedPlanMode plan_cache_mode);
 static void pg_stat_statements_internal(FunctionCallInfo fcinfo,
 										pgssVersion api_version,
 										bool showtext);
@@ -877,7 +889,8 @@ pgss_post_parse_analyze(ParseState *pstate, Query *query, JumbleState *jstate)
 				   NULL,
 				   jstate,
 				   0,
-				   0);
+				   0,
+				   PGSS_PLAN_CACHE_MODE_INVALID);
 }
 
 /*
@@ -957,7 +970,8 @@ pgss_planner(Query *parse,
 				   NULL,
 				   NULL,
 				   0,
-				   0);
+				   0,
+				   PGSS_PLAN_CACHE_MODE_INVALID);
 	}
 	else
 	{
@@ -1069,10 +1083,17 @@ static void
 pgss_ExecutorEnd(QueryDesc *queryDesc)
 {
 	int64		queryId = queryDesc->plannedstmt->queryId;
+	pgssCachedPlanMode plan_cache_mode = PGSS_PLAN_CACHE_MODE_INVALID;
 
 	if (queryId != INT64CONST(0) && queryDesc->totaltime &&
 		pgss_enabled(nesting_level))
 	{
+		/* set plan cache mode */
+		if (queryDesc->estate->es_cached_plan)
+			plan_cache_mode = queryDesc->estate->es_is_generic_plan ?
+				PGSS_PLAN_CACHE_MODE_GENERIC :
+				PGSS_PLAN_CACHE_MODE_CUSTOM;
+
 		/*
 		 * Make sure stats accumulation is done.  (Note: it's okay if several
 		 * levels of hook all do this.)
@@ -1091,7 +1112,8 @@ pgss_ExecutorEnd(QueryDesc *queryDesc)
 				   queryDesc->estate->es_jit ? &queryDesc->estate->es_jit->instr : NULL,
 				   NULL,
 				   queryDesc->estate->es_parallel_workers_to_launch,
-				   queryDesc->estate->es_parallel_workers_launched);
+				   queryDesc->estate->es_parallel_workers_launched,
+				   plan_cache_mode);
 	}
 
 	if (prev_ExecutorEnd)
@@ -1224,7 +1246,8 @@ pgss_ProcessUtility(PlannedStmt *pstmt, const char *queryString,
 				   NULL,
 				   NULL,
 				   0,
-				   0);
+				   0,
+				   PGSS_PLAN_CACHE_MODE_INVALID);
 	}
 	else
 	{
@@ -1287,7 +1310,8 @@ pgss_store(const char *query, int64 queryId,
 		   const struct JitInstrumentation *jitusage,
 		   JumbleState *jstate,
 		   int parallel_workers_to_launch,
-		   int parallel_workers_launched)
+		   int parallel_workers_launched,
+		   pgssCachedPlanMode plan_cache_mode)
 {
 	pgssHashKey key;
 	pgssEntry  *entry;
@@ -1495,6 +1519,14 @@ pgss_store(const char *query, int64 queryId,
 		entry->counters.parallel_workers_to_launch += parallel_workers_to_launch;
 		entry->counters.parallel_workers_launched += parallel_workers_launched;
 
+		if (plan_cache_mode > PGSS_PLAN_CACHE_MODE_INVALID)
+		{
+			if (plan_cache_mode == PGSS_PLAN_CACHE_MODE_GENERIC)
+				entry->counters.generic_plan_calls++;
+			else
+				entry->counters.custom_plan_calls++;
+		}
+
 		SpinLockRelease(&entry->mutex);
 	}
 
@@ -1562,7 +1594,8 @@ pg_stat_statements_reset(PG_FUNCTION_ARGS)
 #define PG_STAT_STATEMENTS_COLS_V1_10	43
 #define PG_STAT_STATEMENTS_COLS_V1_11	49
 #define PG_STAT_STATEMENTS_COLS_V1_12	52
-#define PG_STAT_STATEMENTS_COLS			52	/* maximum of above */
+#define PG_STAT_STATEMENTS_COLS_V1_13	54
+#define PG_STAT_STATEMENTS_COLS			54	/* maximum of above */
 
 /*
  * Retrieve statement statistics.
@@ -1574,6 +1607,16 @@ pg_stat_statements_reset(PG_FUNCTION_ARGS)
  * expected API version is identified by embedding it in the C name of the
  * function.  Unfortunately we weren't bright enough to do that for 1.1.
  */
+Datum
+pg_stat_statements_1_13(PG_FUNCTION_ARGS)
+{
+	bool		showtext = PG_GETARG_BOOL(0);
+
+	pg_stat_statements_internal(fcinfo, PGSS_V1_13, showtext);
+
+	return (Datum) 0;
+}
+
 Datum
 pg_stat_statements_1_12(PG_FUNCTION_ARGS)
 {
@@ -1732,6 +1775,10 @@ pg_stat_statements_internal(FunctionCallInfo fcinfo,
 			if (api_version != PGSS_V1_12)
 				elog(ERROR, "incorrect number of output arguments");
 			break;
+		case PG_STAT_STATEMENTS_COLS_V1_13:
+			if (api_version != PGSS_V1_13)
+				elog(ERROR, "incorrect number of output arguments");
+			break;
 		default:
 			elog(ERROR, "incorrect number of output arguments");
 	}
@@ -1984,6 +2031,11 @@ pg_stat_statements_internal(FunctionCallInfo fcinfo,
 			values[i++] = Int64GetDatumFast(tmp.parallel_workers_to_launch);
 			values[i++] = Int64GetDatumFast(tmp.parallel_workers_launched);
 		}
+		if (api_version >= PGSS_V1_13)
+		{
+			values[i++] = Int64GetDatumFast(tmp.generic_plan_calls);
+			values[i++] = Int64GetDatumFast(tmp.custom_plan_calls);
+		}
 		if (api_version >= PGSS_V1_11)
 		{
 			values[i++] = TimestampTzGetDatum(stats_since);
@@ -1999,6 +2051,7 @@ pg_stat_statements_internal(FunctionCallInfo fcinfo,
 					 api_version == PGSS_V1_10 ? PG_STAT_STATEMENTS_COLS_V1_10 :
 					 api_version == PGSS_V1_11 ? PG_STAT_STATEMENTS_COLS_V1_11 :
 					 api_version == PGSS_V1_12 ? PG_STAT_STATEMENTS_COLS_V1_12 :
+					 api_version == PGSS_V1_13 ? PG_STAT_STATEMENTS_COLS_V1_13 :
 					 -1 /* fail if you forget to update this assert */ ));
 
 		tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, values, nulls);
diff --git a/contrib/pg_stat_statements/pg_stat_statements.control b/contrib/pg_stat_statements/pg_stat_statements.control
index d45ebc12e36..2eee0ceffa8 100644
--- a/contrib/pg_stat_statements/pg_stat_statements.control
+++ b/contrib/pg_stat_statements/pg_stat_statements.control
@@ -1,5 +1,5 @@
 # pg_stat_statements extension
 comment = 'track planning and execution statistics of all SQL statements executed'
-default_version = '1.12'
+default_version = '1.13'
 module_pathname = '$libdir/pg_stat_statements'
 relocatable = true
diff --git a/contrib/pg_stat_statements/sql/plancache.sql b/contrib/pg_stat_statements/sql/plancache.sql
new file mode 100644
index 00000000000..f3878889ea6
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/plancache.sql
@@ -0,0 +1,129 @@
+--
+-- Information related to plan cache
+--
+
+--
+-- Setup
+--
+CREATE OR REPLACE FUNCTION select_one_func(int) RETURNS VOID AS $$
+DECLARE
+    ret INT;
+BEGIN
+    SELECT $1 INTO ret;
+END;
+$$ LANGUAGE plpgsql;
+CREATE OR REPLACE PROCEDURE select_one_proc(int) AS $$
+DECLARE
+    ret INT;
+BEGIN
+    select $1 INTO ret;
+END;
+$$ LANGUAGE plpgsql;
+
+--
+-- plan cache counters for prepared statements
+--
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+PREPARE p1 AS SELECT $1;
+-- plan cache auto
+SET plan_cache_mode TO auto;
+EXECUTE p1(1);
+-- force generic plan
+SET plan_cache_mode TO force_generic_plan;
+EXECUTE p1(1);
+-- force custom plan
+SET plan_cache_mode TO force_custom_plan;
+EXECUTE p1(1);
+SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, query FROM pg_stat_statements
+    ORDER BY query COLLATE "C";
+DEALLOCATE p1;
+
+--
+-- plan cache counters for extended query protocol
+--
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+SELECT $1 \parse p1
+SET plan_cache_mode TO auto;
+\bind_named p1 1
+;
+SET plan_cache_mode TO force_generic_plan;
+\bind_named p1 1
+;
+SET plan_cache_mode TO force_custom_plan;
+\bind_named p1 1
+;
+SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, query FROM pg_stat_statements
+    ORDER BY query COLLATE "C";
+\close_prepared p1
+
+--
+-- plan cache counters for explain|explain (analyze) with prepared statements
+--
+SET pg_stat_statements.track = 'all';
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+PREPARE p1 AS SELECT $1;
+-- plan cache auto
+SET plan_cache_mode TO auto;
+EXPLAIN (COSTS OFF) EXECUTE p1(1);
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) EXECUTE p1(1);
+-- force generic plan
+SET plan_cache_mode TO force_generic_plan;
+EXPLAIN (COSTS OFF) EXECUTE p1(1);
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) EXECUTE p1(1);
+-- force custom plan
+SET plan_cache_mode TO force_custom_plan;
+EXPLAIN (COSTS OFF) EXECUTE p1(1);
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) EXECUTE p1(1);
+SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, query FROM pg_stat_statements
+    ORDER BY query COLLATE "C";
+RESET pg_stat_statements.track;
+DEALLOCATE p1;
+
+--
+-- plan cache counters for functions and procedures
+--
+SET pg_stat_statements.track = 'all';
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+-- plan cache auto
+SET plan_cache_mode TO auto;
+SELECT select_one_func(1);
+CALL select_one_proc(1);
+-- force generic plan
+SET plan_cache_mode TO force_generic_plan;
+SELECT select_one_func(1);
+CALL select_one_proc(1);
+-- force custom plan
+SET plan_cache_mode TO force_custom_plan;
+SELECT select_one_func(1);
+CALL select_one_proc(1);
+SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, query FROM pg_stat_statements
+    ORDER BY query COLLATE "C";
+
+--
+-- plan cache counters for explain|explain (analyze) with functions and procedures
+--
+SET pg_stat_statements.track = 'all';
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+-- plan cache auto
+SET plan_cache_mode TO auto;
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) SELECT select_one_func(1);
+EXPLAIN (COSTS OFF) SELECT select_one_func(1);
+CALL select_one_proc(1);
+-- force generic plan
+SET plan_cache_mode TO force_generic_plan;
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) SELECT select_one_func(1);
+EXPLAIN (COSTS OFF) SELECT select_one_func(1);
+CALL select_one_proc(1);
+-- force custom plan
+SET plan_cache_mode TO force_custom_plan;
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) SELECT select_one_func(1);
+EXPLAIN (COSTS OFF) SELECT select_one_func(1);
+CALL select_one_proc(1);
+SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, query FROM pg_stat_statements
+    ORDER BY query COLLATE "C";
+
+--
+-- Cleanup
+--
+DROP FUNCTION select_one_func(int);
+DROP PROCEDURE select_one_proc(int);
diff --git a/doc/src/sgml/pgstatstatements.sgml b/doc/src/sgml/pgstatstatements.sgml
index 7baa07dcdbf..08b5fec9e10 100644
--- a/doc/src/sgml/pgstatstatements.sgml
+++ b/doc/src/sgml/pgstatstatements.sgml
@@ -554,6 +554,24 @@
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>generic_plan_calls</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Number of times the statement was executed using a generic plan
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>custom_plan_calls</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Number of times the statement was executed using a custom plan
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>stats_since</structfield> <type>timestamp with time zone</type>
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 7e2792ead71..49109290719 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -371,7 +371,7 @@ standard_ExplainOneQuery(Query *query, int cursorOptions,
 	/* run it (if needed) and produce output */
 	ExplainOnePlan(plan, into, es, queryString, params, queryEnv,
 				   &planduration, (es->buffers ? &bufusage : NULL),
-				   es->memory ? &mem_counters : NULL);
+				   es->memory ? &mem_counters : NULL, NULL);
 }
 
 /*
@@ -495,7 +495,8 @@ ExplainOnePlan(PlannedStmt *plannedstmt, IntoClause *into, ExplainState *es,
 			   const char *queryString, ParamListInfo params,
 			   QueryEnvironment *queryEnv, const instr_time *planduration,
 			   const BufferUsage *bufusage,
-			   const MemoryContextCounters *mem_counters)
+			   const MemoryContextCounters *mem_counters,
+			   CachedPlan *cplan)
 {
 	DestReceiver *dest;
 	QueryDesc  *queryDesc;
@@ -564,6 +565,12 @@ ExplainOnePlan(PlannedStmt *plannedstmt, IntoClause *into, ExplainState *es,
 	/* call ExecutorStart to prepare the plan for execution */
 	ExecutorStart(queryDesc, eflags);
 
+	if (cplan)
+	{
+		queryDesc->estate->es_cached_plan = true;
+		queryDesc->estate->es_is_generic_plan = cplan->is_generic_plan;
+	}
+
 	/* Execute the plan for statistics if asked for */
 	if (es->analyze)
 	{
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..55e08b2a63f 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -659,7 +659,7 @@ ExplainExecuteQuery(ExecuteStmt *execstmt, IntoClause *into, ExplainState *es,
 		if (pstmt->commandType != CMD_UTILITY)
 			ExplainOnePlan(pstmt, into, es, query_string, paramLI, pstate->p_queryEnv,
 						   &planduration, (es->buffers ? &bufusage : NULL),
-						   es->memory ? &mem_counters : NULL);
+						   es->memory ? &mem_counters : NULL, cplan);
 		else
 			ExplainOneUtility(pstmt->utilityStmt, into, es, pstate, paramLI);
 
diff --git a/src/backend/executor/execUtils.c b/src/backend/executor/execUtils.c
index fdc65c2b42b..14d320b8c18 100644
--- a/src/backend/executor/execUtils.c
+++ b/src/backend/executor/execUtils.c
@@ -165,6 +165,9 @@ CreateExecutorState(void)
 	estate->es_jit_flags = 0;
 	estate->es_jit = NULL;
 
+	estate->es_cached_plan = false;
+	estate->es_is_generic_plan = false;
+
 	/*
 	 * Return the executor state structure
 	 */
diff --git a/src/backend/executor/functions.c b/src/backend/executor/functions.c
index 359aafea681..1722f142dba 100644
--- a/src/backend/executor/functions.c
+++ b/src/backend/executor/functions.c
@@ -1364,6 +1364,12 @@ postquel_start(execution_state *es, SQLFunctionCachePtr fcache)
 		else
 			eflags = 0;			/* default run-to-completion flags */
 		ExecutorStart(es->qd, eflags);
+
+		if (fcache->cplan)
+		{
+			es->qd->estate->es_cached_plan = true;
+			es->qd->estate->es_is_generic_plan = fcache->cplan->is_generic_plan;
+		}
 	}
 
 	es->status = F_EXEC_RUN;
diff --git a/src/backend/executor/spi.c b/src/backend/executor/spi.c
index ecb2e4ccaa1..51401d61de1 100644
--- a/src/backend/executor/spi.c
+++ b/src/backend/executor/spi.c
@@ -70,7 +70,8 @@ static int	_SPI_execute_plan(SPIPlanPtr plan, const SPIExecuteOptions *options,
 static ParamListInfo _SPI_convert_params(int nargs, Oid *argtypes,
 										 Datum *Values, const char *Nulls);
 
-static int	_SPI_pquery(QueryDesc *queryDesc, bool fire_triggers, uint64 tcount);
+static int	_SPI_pquery(QueryDesc *queryDesc, CachedPlan *cplan,
+						bool fire_triggers, uint64 tcount);
 
 static void _SPI_error_callback(void *arg);
 
@@ -2696,7 +2697,7 @@ _SPI_execute_plan(SPIPlanPtr plan, const SPIExecuteOptions *options,
 										options->params,
 										_SPI_current->queryEnv,
 										0);
-				res = _SPI_pquery(qdesc, fire_triggers,
+				res = _SPI_pquery(qdesc, cplan, fire_triggers,
 								  canSetTag ? options->tcount : 0);
 				FreeQueryDesc(qdesc);
 			}
@@ -2871,7 +2872,8 @@ _SPI_convert_params(int nargs, Oid *argtypes,
 }
 
 static int
-_SPI_pquery(QueryDesc *queryDesc, bool fire_triggers, uint64 tcount)
+_SPI_pquery(QueryDesc *queryDesc, CachedPlan *cplan,
+			bool fire_triggers, uint64 tcount)
 {
 	int			operation = queryDesc->operation;
 	int			eflags;
@@ -2929,6 +2931,12 @@ _SPI_pquery(QueryDesc *queryDesc, bool fire_triggers, uint64 tcount)
 
 	ExecutorStart(queryDesc, eflags);
 
+	if (cplan)
+	{
+		queryDesc->estate->es_cached_plan = true;
+		queryDesc->estate->es_is_generic_plan = cplan->is_generic_plan;
+	}
+
 	ExecutorRun(queryDesc, ForwardScanDirection, tcount);
 
 	_SPI_current->processed = queryDesc->estate->es_processed;
diff --git a/src/backend/tcop/pquery.c b/src/backend/tcop/pquery.c
index 08791b8f75e..5d7a52a9532 100644
--- a/src/backend/tcop/pquery.c
+++ b/src/backend/tcop/pquery.c
@@ -41,7 +41,7 @@ static void ProcessQuery(PlannedStmt *plan,
 						 ParamListInfo params,
 						 QueryEnvironment *queryEnv,
 						 DestReceiver *dest,
-						 QueryCompletion *qc);
+						 QueryCompletion *qc, CachedPlan *cplan);
 static void FillPortalStore(Portal portal, bool isTopLevel);
 static uint64 RunFromStore(Portal portal, ScanDirection direction, uint64 count,
 						   DestReceiver *dest);
@@ -139,7 +139,8 @@ ProcessQuery(PlannedStmt *plan,
 			 ParamListInfo params,
 			 QueryEnvironment *queryEnv,
 			 DestReceiver *dest,
-			 QueryCompletion *qc)
+			 QueryCompletion *qc,
+			 CachedPlan *cplan)
 {
 	QueryDesc  *queryDesc;
 
@@ -155,6 +156,12 @@ ProcessQuery(PlannedStmt *plan,
 	 */
 	ExecutorStart(queryDesc, 0);
 
+	if (cplan)
+	{
+		queryDesc->estate->es_cached_plan = true;
+		queryDesc->estate->es_is_generic_plan = cplan->is_generic_plan;
+	}
+
 	/*
 	 * Run the plan to completion.
 	 */
@@ -517,6 +524,12 @@ PortalStart(Portal portal, ParamListInfo params,
 				 */
 				ExecutorStart(queryDesc, myeflags);
 
+				if (portal->cplan)
+				{
+					queryDesc->estate->es_cached_plan = true;
+					queryDesc->estate->es_is_generic_plan = portal->cplan->is_generic_plan;
+				}
+
 				/*
 				 * This tells PortalCleanup to shut down the executor
 				 */
@@ -1273,7 +1286,7 @@ PortalRunMulti(Portal portal,
 							 portal->sourceText,
 							 portal->portalParams,
 							 portal->queryEnv,
-							 dest, qc);
+							 dest, qc, portal->cplan);
 			}
 			else
 			{
@@ -1282,7 +1295,7 @@ PortalRunMulti(Portal portal,
 							 portal->sourceText,
 							 portal->portalParams,
 							 portal->queryEnv,
-							 altdest, NULL);
+							 altdest, NULL, portal->cplan);
 			}
 
 			if (log_executor_stats)
diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c
index 89a1c79e984..d6750b8d1bd 100644
--- a/src/backend/utils/cache/plancache.c
+++ b/src/backend/utils/cache/plancache.c
@@ -1140,6 +1140,7 @@ BuildCachedPlan(CachedPlanSource *plansource, List *qlist,
 	plan->is_oneshot = plansource->is_oneshot;
 	plan->is_saved = false;
 	plan->is_valid = true;
+	plan->is_generic_plan = false;
 
 	/* assign generation number to new plan */
 	plan->generation = ++(plansource->generation);
@@ -1358,10 +1359,12 @@ GetCachedPlan(CachedPlanSource *plansource, ParamListInfo boundParams,
 		plansource->total_custom_cost += cached_plan_cost(plan, true);
 
 		plansource->num_custom_plans++;
+		plan->is_generic_plan = false;
 	}
 	else
 	{
 		plansource->num_generic_plans++;
+		plan->is_generic_plan = true;
 	}
 
 	Assert(plan != NULL);
diff --git a/src/include/commands/explain.h b/src/include/commands/explain.h
index 3b122f79ed8..bf8e5f5d90d 100644
--- a/src/include/commands/explain.h
+++ b/src/include/commands/explain.h
@@ -15,6 +15,7 @@
 
 #include "executor/executor.h"
 #include "parser/parse_node.h"
+#include "utils/plancache.h"
 
 struct ExplainState;			/* defined in explain_state.h */
 
@@ -68,7 +69,8 @@ extern void ExplainOnePlan(PlannedStmt *plannedstmt, IntoClause *into,
 						   ParamListInfo params, QueryEnvironment *queryEnv,
 						   const instr_time *planduration,
 						   const BufferUsage *bufusage,
-						   const MemoryContextCounters *mem_counters);
+						   const MemoryContextCounters *mem_counters,
+						   CachedPlan *cplan);
 
 extern void ExplainPrintPlan(struct ExplainState *es, QueryDesc *queryDesc);
 extern void ExplainPrintTriggers(struct ExplainState *es,
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index e107d6e5f81..877c7663a04 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -774,6 +774,10 @@ typedef struct EState
 	 */
 	List	   *es_insert_pending_result_relations;
 	List	   *es_insert_pending_modifytables;
+
+	/* Cached Plan statistics. */
+	bool		es_cached_plan;
+	bool		es_is_generic_plan;
 } EState;
 
 
diff --git a/src/include/utils/plancache.h b/src/include/utils/plancache.h
index 1baa6d50bfd..9b3e8994ed9 100644
--- a/src/include/utils/plancache.h
+++ b/src/include/utils/plancache.h
@@ -163,6 +163,7 @@ typedef struct CachedPlan
 	bool		is_oneshot;		/* is it a "oneshot" plan? */
 	bool		is_saved;		/* is CachedPlan in a long-lived context? */
 	bool		is_valid;		/* is the stmt_list currently valid? */
+	bool		is_generic_plan;	/* is the plan generic */
 	Oid			planRoleId;		/* Role ID the plan was created for */
 	bool		dependsOnRole;	/* is plan specific to that role? */
 	TransactionId saved_xmin;	/* if valid, replan when TransactionXmin
-- 
2.39.5 (Apple Git-154)

#33Michael Paquier
michael@paquier.xyz
In reply to: Sami Imseih (#32)
Re: track generic and custom plans in pg_stat_statements

On Mon, Jul 21, 2025 at 04:47:31PM -0500, Sami Imseih wrote:

Last week I published a v11 that adds a field to QueryDesc, but as I thought
about this more, how about we just add 2 bool fields in QueryDesc->estate
( es_cached_plan and es_is_generic_plan ), a field in CachedPlan (
is_generic_plan )
and after ExecutorStart, and if we have a cplan, we set the
appropriate plan cache
mode value. I think it's better to confine these new fields in Estate
rather than
at a higher level like QueryDesc. See v12.

Yes, I think that this is a much better idea to isolate the whole
concept and let pgss grab these values. We have lived with such
additions for monitoring in EState a few times already, see for
example de3a2ea3b264 and 1d477a907e63 that are tainted with my
fingerprints.
--
Michael

#34Sami Imseih
samimseih@gmail.com
In reply to: Michael Paquier (#33)
Re: track generic and custom plans in pg_stat_statements

Yes, I think that this is a much better idea to isolate the whole
concept and let pgss grab these values. We have lived with such
additions for monitoring in EState a few times already, see for
example de3a2ea3b264 and 1d477a907e63 that are tainted with my
fingerprints.

correct, there is precedence for this.

This seems like the best way to proceed.

--

Sami

#35Sami Imseih
samimseih@gmail.com
In reply to: Michael Paquier (#22)
Re: track generic and custom plans in pg_stat_statements

Note: the size of the change in pg_stat_statements--1.12--1.13.sql
points that we should seriously consider splitting these attributes
into multiple sub-functions.

So we don't lose track of this. This should be a follow-up thread. I do
agree something has to be done about the exploding list of attributes
in pg_s_s.

--
Sami

#36Andrei Lepikhov
lepihov@gmail.com
In reply to: Sami Imseih (#35)
Re: track generic and custom plans in pg_stat_statements

On 22/7/2025 01:22, Sami Imseih wrote:

Note: the size of the change in pg_stat_statements--1.12--1.13.sql
points that we should seriously consider splitting these attributes
into multiple sub-functions.

So we don't lose track of this. This should be a follow-up thread. I do
agree something has to be done about the exploding list of attributes
in pg_s_s.

+1

Not once I encountered people who want to track only a specific number
of parameters and do not have much fun burdening themselves with all the
data set, querying a whole huge stat view to analyse performance profiles.
In another scenario, an extension needs to track a limited number of
parameters - let's say, blocks hit and blocks read. Another dimension -
sometimes we are only interested in queries that involve complex join
trees or partitioned tables and would be happy to avoid tracking all
other queries.
It seems that a callback-based or subscription-based model could be
worth exploring.

--
regards, Andrei Lepikhov

#37Andrei Lepikhov
lepihov@gmail.com
In reply to: Michael Paquier (#33)
Re: track generic and custom plans in pg_stat_statements

On 22/7/2025 01:07, Michael Paquier wrote:

On Mon, Jul 21, 2025 at 04:47:31PM -0500, Sami Imseih wrote:

Last week I published a v11 that adds a field to QueryDesc, but as I thought
about this more, how about we just add 2 bool fields in QueryDesc->estate
( es_cached_plan and es_is_generic_plan ), a field in CachedPlan (
is_generic_plan )
and after ExecutorStart, and if we have a cplan, we set the
appropriate plan cache
mode value. I think it's better to confine these new fields in Estate
rather than
at a higher level like QueryDesc. See v12.

Yes, I think that this is a much better idea to isolate the whole
concept and let pgss grab these values. We have lived with such
additions for monitoring in EState a few times already, see for
example de3a2ea3b264 and 1d477a907e63 that are tainted with my
fingerprints.

I would like to oppose to the current version a little.

Commits de3a2ea3b264 and 1d477a907e63 introduced elements that are more
closely related to the execution phase. While the parameter
es_parallel_workers_to_launch could be considered a planning parameter,
es_parallel_workers_launched and es_total_processed should not.
Furthermore, any new code that utilises ExecutorStart/Run/End must
ensure that the fields es_cached_plan and es_is_generic_plan are set
correctly.

IMO, the concept of a generic or custom plan is a fundamental property
of the plan and should be stored within it - essentially in the
PlannedStmt structure.
Additionally, it is unclear why we incur significant costs to alter the
interface of ExplainOnePlan solely to track these parameters.

It may be more efficient to set the is_generic_plan option at the top
plan node (PlannedStmt) and reference it wherever necessary. To identify
a cached plan, we may consider pushing the CachedPlan/CachedPlanSource
pointer down throughout pg_plan_query and maintaining a reference to the
plan (or simply setting a boolean flag) at the same location — inside
the PlannedStmt.

Such knowledge may provide an extra profit for planning - a generic
plan, stored in the plan cache, may be reused later, and it may be
profitable for an analytics-related extension to spend extra cycles and
apply optimisation tricks more boldly.

--
regards, Andrei Lepikhov

#38Sami Imseih
samimseih@gmail.com
In reply to: Andrei Lepikhov (#37)
Re: track generic and custom plans in pg_stat_statements

I would like to oppose to the current version a little.

Commits de3a2ea3b264 and 1d477a907e63 introduced elements that are more
closely related to the execution phase. While the parameter
es_parallel_workers_to_launch could be considered a planning parameter,
es_parallel_workers_launched and es_total_processed should not.

Sure, I saw these new fields more in the realm of
"es_parallel_workers_to_launch",
which are planning related but are tracked in Estate for statistical purposes.

Furthermore, any new code that utilises ExecutorStart/Run/End must
ensure that the fields es_cached_plan and es_is_generic_plan are set
correctly.

We really need to make sure ExecutorStart sets this correctly only. Estate
is carried over.

IMO, the concept of a generic or custom plan is a fundamental property
of the plan and should be stored within it - essentially in the
PlannedStmt structure.

I do agree with this statement, and this idea did cross my mind; but I felt
that Estate is more ideal for statistics related fields, since we have a
precedent for this. But, I am open to the PlannedStmt idea as well.

Additionally, it is unclear why we incur significant costs to alter the
interface of ExplainOnePlan solely to track these parameters.

If we want to get this info to Estate, we have to.... if we go with PlannedStmt,
we do not.

It may be more efficient to set the is_generic_plan option at the top
plan node (PlannedStmt) and reference it wherever necessary. To identify
a cached plan, we may consider pushing the CachedPlan/CachedPlanSource
pointer down throughout pg_plan_query and maintaining a reference to the
plan (or simply setting a boolean flag) at the same location — inside
the PlannedStmt.

We will need a field to store an enum. let's call it CachedPlanType
with the types of cached plan. We need to be able to differentiate
when cached plans are not used, so a simple boolean is not
sufficient.
```
typedef enum CachedPlanType
{
PLAN_CACHE_NOT_SET, /* Not a cached plan */
PLAN_CACHE_GENERIC, /* Generic cached plan */
PLAN_CACHE_CUSTOM, /* Custom cached plan */
} CachedPlanType;
```
We can track a field for this enum in PlannedStmt and initialize
it to PLAN_CACHE_NOT_SET whenever we makeNode(PlannedStmt).
In GetCachedPlan, we iterate through plan->stmt_list to set the
value for the PlannedStmt.

CachedPlanType will have to be defined in nodes.h for this to work.

We can avoid the struct altogether and define the new PlannedStmt
field as an "int" and bit-pack the types.

I prefer the CachedPlanType struct to do this, as it's cleaner and
self-documenting.

Such knowledge may provide an extra profit for planning - a generic
plan, stored in the plan cache, may be reused later, and it may be
profitable for an analytics-related extension to spend extra cycles and
apply optimisation tricks more boldly.

Sure, either solution will provide this benefit.

--
Sami

#39Sami Imseih
samimseih@gmail.com
In reply to: Sami Imseih (#38)
1 attachment(s)
Re: track generic and custom plans in pg_stat_statements

It may be more efficient to set the is_generic_plan option at the top
plan node (PlannedStmt) and reference it wherever necessary. To identify
a cached plan, we may consider pushing the CachedPlan/CachedPlanSource
pointer down throughout pg_plan_query and maintaining a reference to the
plan (or simply setting a boolean flag) at the same location — inside
the PlannedStmt.

We will need a field to store an enum. let's call it CachedPlanType
with the types of cached plan. We need to be able to differentiate
when cached plans are not used, so a simple boolean is not
sufficient.
```
typedef enum CachedPlanType
{
PLAN_CACHE_NOT_SET, /* Not a cached plan */
PLAN_CACHE_GENERIC, /* Generic cached plan */
PLAN_CACHE_CUSTOM, /* Custom cached plan */
} CachedPlanType;
```
We can track a field for this enum in PlannedStmt and initialize
it to PLAN_CACHE_NOT_SET whenever we makeNode(PlannedStmt).
In GetCachedPlan, we iterate through plan->stmt_list to set the
value for the PlannedStmt.

CachedPlanType will have to be defined in nodes.h for this to work.

We can avoid the struct altogether and define the new PlannedStmt
field as an "int" and bit-pack the types.

I prefer the CachedPlanType struct to do this, as it's cleaner and
self-documenting.

v13 is the implementation using PlannedStmt as described above.

--
Sami

Attachments:

v13-0001-Add-counters-for-generic-and-custom-plan-executi.patchapplication/octet-stream; name=v13-0001-Add-counters-for-generic-and-custom-plan-executi.patchDownload
From 45e010f4808320747a0d7abc6f181fc71146ab76 Mon Sep 17 00:00:00 2001
From: Sami Imseih <simseih@amazon.com>
Date: Tue, 22 Jul 2025 14:06:56 -0500
Subject: [PATCH v13 1/1] Add counters for generic and custom plan executions
 to pg_stat_statements

This patch adds two new counters to pg_stat_statements:

- generic_plan_calls
- custom_plan_calls

These track how many times a prepared statement was executed using a
generic or custom plan, respectively.

To support this, a new field called `cached_plan_type` is added to
PlannedStmt. It tracks one of three possible states that a cached plan
can have: NOT_SET, GENERIC, or CUSTOM. The field is initially set to
NOT_SET when a PlannedStmt is created, and later updated to the
appropriate value during GetCachedPlan.

The three-state enum is necessary so that extensions can
differentiate between non-cached plans and cached plans using either
generic or custom plans.

This information in PlannedStmt allows extensions to access the
cached plan type via Executor hooks for statistical purposes.
---
 contrib/pg_stat_statements/Makefile           |   3 +-
 .../pg_stat_statements/expected/plancache.out | 295 ++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |   2 +
 .../pg_stat_statements--1.12--1.13.sql        |  78 +++++
 .../pg_stat_statements/pg_stat_statements.c   |  60 +++-
 .../pg_stat_statements.control                |   2 +-
 contrib/pg_stat_statements/sql/plancache.sql  | 129 ++++++++
 doc/src/sgml/pgstatstatements.sgml            |  18 ++
 src/backend/commands/foreigncmds.c            |   1 +
 src/backend/commands/schemacmds.c             |   1 +
 src/backend/executor/execParallel.c           |   1 +
 src/backend/optimizer/plan/planner.c          |   1 +
 src/backend/tcop/postgres.c                   |   1 +
 src/backend/tcop/utility.c                    |   2 +
 src/backend/utils/cache/plancache.c           |   8 +
 src/include/nodes/nodes.h                     |  12 +
 src/include/nodes/plannodes.h                 |   3 +
 17 files changed, 608 insertions(+), 9 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/plancache.out
 create mode 100644 contrib/pg_stat_statements/pg_stat_statements--1.12--1.13.sql
 create mode 100644 contrib/pg_stat_statements/sql/plancache.sql

diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index b2bd8794d2a..996a5fac448 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -7,6 +7,7 @@ OBJS = \
 
 EXTENSION = pg_stat_statements
 DATA = pg_stat_statements--1.4.sql \
+	pg_stat_statements--1.12--1.13.sql \
 	pg_stat_statements--1.11--1.12.sql pg_stat_statements--1.10--1.11.sql \
 	pg_stat_statements--1.9--1.10.sql pg_stat_statements--1.8--1.9.sql \
 	pg_stat_statements--1.7--1.8.sql pg_stat_statements--1.6--1.7.sql \
@@ -20,7 +21,7 @@ LDFLAGS_SL += $(filter -lm, $(LIBS))
 REGRESS_OPTS = --temp-config $(top_srcdir)/contrib/pg_stat_statements/pg_stat_statements.conf
 REGRESS = select dml cursors utility level_tracking planning \
 	user_activity wal entry_timestamp privileges extended \
-	parallel cleanup oldextversions squashing
+	parallel cleanup oldextversions squashing plancache
 # Disabled because these tests require "shared_preload_libraries=pg_stat_statements",
 # which typical installcheck users do not have (e.g. buildfarm clients).
 NO_INSTALLCHECK = 1
diff --git a/contrib/pg_stat_statements/expected/plancache.out b/contrib/pg_stat_statements/expected/plancache.out
new file mode 100644
index 00000000000..7b05d5a425f
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/plancache.out
@@ -0,0 +1,295 @@
+--
+-- Information related to plan cache
+--
+--
+-- Setup
+--
+CREATE OR REPLACE FUNCTION select_one_func(int) RETURNS VOID AS $$
+DECLARE
+    ret INT;
+BEGIN
+    SELECT $1 INTO ret;
+END;
+$$ LANGUAGE plpgsql;
+CREATE OR REPLACE PROCEDURE select_one_proc(int) AS $$
+DECLARE
+    ret INT;
+BEGIN
+    select $1 INTO ret;
+END;
+$$ LANGUAGE plpgsql;
+--
+-- plan cache counters for prepared statements
+--
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+PREPARE p1 AS SELECT $1;
+-- plan cache auto
+SET plan_cache_mode TO auto;
+EXECUTE p1(1);
+ ?column? 
+----------
+ 1
+(1 row)
+
+-- force generic plan
+SET plan_cache_mode TO force_generic_plan;
+EXECUTE p1(1);
+ ?column? 
+----------
+ 1
+(1 row)
+
+-- force custom plan
+SET plan_cache_mode TO force_custom_plan;
+EXECUTE p1(1);
+ ?column? 
+----------
+ 1
+(1 row)
+
+SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, query FROM pg_stat_statements
+    ORDER BY query COLLATE "C";
+ calls | generic_plan_calls | custom_plan_calls | toplevel |                       query                        
+-------+--------------------+-------------------+----------+----------------------------------------------------
+     3 |                  1 |                 2 | t        | PREPARE p1 AS SELECT $1
+     1 |                  0 |                 0 | t        | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+     3 |                  0 |                 0 | t        | SET plan_cache_mode TO $1
+(3 rows)
+
+DEALLOCATE p1;
+--
+-- plan cache counters for extended query protocol
+--
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+SELECT $1 \parse p1
+SET plan_cache_mode TO auto;
+\bind_named p1 1
+;
+ ?column? 
+----------
+ 1
+(1 row)
+
+SET plan_cache_mode TO force_generic_plan;
+\bind_named p1 1
+;
+ ?column? 
+----------
+ 1
+(1 row)
+
+SET plan_cache_mode TO force_custom_plan;
+\bind_named p1 1
+;
+ ?column? 
+----------
+ 1
+(1 row)
+
+SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, query FROM pg_stat_statements
+    ORDER BY query COLLATE "C";
+ calls | generic_plan_calls | custom_plan_calls | toplevel |                       query                        
+-------+--------------------+-------------------+----------+----------------------------------------------------
+     3 |                  1 |                 2 | t        | SELECT $1
+     1 |                  0 |                 0 | t        | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+     3 |                  0 |                 0 | t        | SET plan_cache_mode TO $1
+(3 rows)
+
+\close_prepared p1
+--
+-- plan cache counters for explain|explain (analyze) with prepared statements
+--
+SET pg_stat_statements.track = 'all';
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+PREPARE p1 AS SELECT $1;
+-- plan cache auto
+SET plan_cache_mode TO auto;
+EXPLAIN (COSTS OFF) EXECUTE p1(1);
+ QUERY PLAN 
+------------
+ Result
+(1 row)
+
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) EXECUTE p1(1);
+            QUERY PLAN             
+-----------------------------------
+ Result (actual rows=1.00 loops=1)
+(1 row)
+
+-- force generic plan
+SET plan_cache_mode TO force_generic_plan;
+EXPLAIN (COSTS OFF) EXECUTE p1(1);
+ QUERY PLAN 
+------------
+ Result
+(1 row)
+
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) EXECUTE p1(1);
+            QUERY PLAN             
+-----------------------------------
+ Result (actual rows=1.00 loops=1)
+(1 row)
+
+-- force custom plan
+SET plan_cache_mode TO force_custom_plan;
+EXPLAIN (COSTS OFF) EXECUTE p1(1);
+ QUERY PLAN 
+------------
+ Result
+(1 row)
+
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) EXECUTE p1(1);
+            QUERY PLAN             
+-----------------------------------
+ Result (actual rows=1.00 loops=1)
+(1 row)
+
+SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, query FROM pg_stat_statements
+    ORDER BY query COLLATE "C";
+ calls | generic_plan_calls | custom_plan_calls | toplevel |                                      query                                       
+-------+--------------------+-------------------+----------+----------------------------------------------------------------------------------
+     3 |                  0 |                 0 | t        | EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) EXECUTE p1(1)
+     3 |                  0 |                 0 | t        | EXPLAIN (COSTS OFF) EXECUTE p1(1)
+     6 |                  2 |                 4 | f        | PREPARE p1 AS SELECT $1
+     1 |                  0 |                 0 | t        | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+     3 |                  0 |                 0 | t        | SET plan_cache_mode TO $1
+(5 rows)
+
+RESET pg_stat_statements.track;
+DEALLOCATE p1;
+--
+-- plan cache counters for functions and procedures
+--
+SET pg_stat_statements.track = 'all';
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- plan cache auto
+SET plan_cache_mode TO auto;
+SELECT select_one_func(1);
+ select_one_func 
+-----------------
+ 
+(1 row)
+
+CALL select_one_proc(1);
+-- force generic plan
+SET plan_cache_mode TO force_generic_plan;
+SELECT select_one_func(1);
+ select_one_func 
+-----------------
+ 
+(1 row)
+
+CALL select_one_proc(1);
+-- force custom plan
+SET plan_cache_mode TO force_custom_plan;
+SELECT select_one_func(1);
+ select_one_func 
+-----------------
+ 
+(1 row)
+
+CALL select_one_proc(1);
+SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, query FROM pg_stat_statements
+    ORDER BY query COLLATE "C";
+ calls | generic_plan_calls | custom_plan_calls | toplevel |                       query                        
+-------+--------------------+-------------------+----------+----------------------------------------------------
+     3 |                  0 |                 0 | t        | CALL select_one_proc($1)
+     6 |                  2 |                 4 | f        | SELECT $1
+     1 |                  0 |                 0 | t        | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+     3 |                  0 |                 0 | t        | SELECT select_one_func($1)
+     3 |                  0 |                 0 | t        | SET plan_cache_mode TO $1
+(5 rows)
+
+--
+-- plan cache counters for explain|explain (analyze) with functions and procedures
+--
+SET pg_stat_statements.track = 'all';
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- plan cache auto
+SET plan_cache_mode TO auto;
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) SELECT select_one_func(1);
+            QUERY PLAN             
+-----------------------------------
+ Result (actual rows=1.00 loops=1)
+(1 row)
+
+EXPLAIN (COSTS OFF) SELECT select_one_func(1);
+ QUERY PLAN 
+------------
+ Result
+(1 row)
+
+CALL select_one_proc(1);
+-- force generic plan
+SET plan_cache_mode TO force_generic_plan;
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) SELECT select_one_func(1);
+            QUERY PLAN             
+-----------------------------------
+ Result (actual rows=1.00 loops=1)
+(1 row)
+
+EXPLAIN (COSTS OFF) SELECT select_one_func(1);
+ QUERY PLAN 
+------------
+ Result
+(1 row)
+
+CALL select_one_proc(1);
+-- force custom plan
+SET plan_cache_mode TO force_custom_plan;
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) SELECT select_one_func(1);
+            QUERY PLAN             
+-----------------------------------
+ Result (actual rows=1.00 loops=1)
+(1 row)
+
+EXPLAIN (COSTS OFF) SELECT select_one_func(1);
+ QUERY PLAN 
+------------
+ Result
+(1 row)
+
+CALL select_one_proc(1);
+SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, query FROM pg_stat_statements
+    ORDER BY query COLLATE "C";
+ calls | generic_plan_calls | custom_plan_calls | toplevel |                                             query                                              
+-------+--------------------+-------------------+----------+------------------------------------------------------------------------------------------------
+     3 |                  0 |                 0 | t        | CALL select_one_proc($1)
+     3 |                  0 |                 0 | t        | EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) SELECT select_one_func($1)
+     6 |                  0 |                 0 | f        | EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) SELECT select_one_func($1);
+     3 |                  0 |                 0 | t        | EXPLAIN (COSTS OFF) SELECT select_one_func($1)
+     6 |                  2 |                 4 | f        | SELECT $1
+     1 |                  0 |                 0 | t        | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+     3 |                  0 |                 0 | t        | SET plan_cache_mode TO $1
+(7 rows)
+
+--
+-- Cleanup
+--
+DROP FUNCTION select_one_func(int);
+DROP PROCEDURE select_one_proc(int);
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 01a6cbdcf61..110fb82fe12 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -21,6 +21,7 @@ contrib_targets += pg_stat_statements
 install_data(
   'pg_stat_statements.control',
   'pg_stat_statements--1.4.sql',
+  'pg_stat_statements--1.12--1.13.sql',
   'pg_stat_statements--1.11--1.12.sql',
   'pg_stat_statements--1.10--1.11.sql',
   'pg_stat_statements--1.9--1.10.sql',
@@ -57,6 +58,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'plancache',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/pg_stat_statements--1.12--1.13.sql b/contrib/pg_stat_statements/pg_stat_statements--1.12--1.13.sql
new file mode 100644
index 00000000000..2f0eaf14ec3
--- /dev/null
+++ b/contrib/pg_stat_statements/pg_stat_statements--1.12--1.13.sql
@@ -0,0 +1,78 @@
+/* contrib/pg_stat_statements/pg_stat_statements--1.12--1.13.sql */
+
+-- complain if script is sourced in psql, rather than via ALTER EXTENSION
+\echo Use "ALTER EXTENSION pg_stat_statements UPDATE TO '1.13'" to load this file. \quit
+
+/* First we have to remove them from the extension */
+ALTER EXTENSION pg_stat_statements DROP VIEW pg_stat_statements;
+ALTER EXTENSION pg_stat_statements DROP FUNCTION pg_stat_statements(boolean);
+
+/* Then we can drop them */
+DROP VIEW pg_stat_statements;
+DROP FUNCTION pg_stat_statements(boolean);
+
+/* Now redefine */
+CREATE FUNCTION pg_stat_statements(IN showtext boolean,
+    OUT userid oid,
+    OUT dbid oid,
+    OUT toplevel bool,
+    OUT queryid bigint,
+    OUT query text,
+    OUT plans int8,
+    OUT total_plan_time float8,
+    OUT min_plan_time float8,
+    OUT max_plan_time float8,
+    OUT mean_plan_time float8,
+    OUT stddev_plan_time float8,
+    OUT calls int8,
+    OUT total_exec_time float8,
+    OUT min_exec_time float8,
+    OUT max_exec_time float8,
+    OUT mean_exec_time float8,
+    OUT stddev_exec_time float8,
+    OUT rows int8,
+    OUT shared_blks_hit int8,
+    OUT shared_blks_read int8,
+    OUT shared_blks_dirtied int8,
+    OUT shared_blks_written int8,
+    OUT local_blks_hit int8,
+    OUT local_blks_read int8,
+    OUT local_blks_dirtied int8,
+    OUT local_blks_written int8,
+    OUT temp_blks_read int8,
+    OUT temp_blks_written int8,
+    OUT shared_blk_read_time float8,
+    OUT shared_blk_write_time float8,
+    OUT local_blk_read_time float8,
+    OUT local_blk_write_time float8,
+    OUT temp_blk_read_time float8,
+    OUT temp_blk_write_time float8,
+    OUT wal_records int8,
+    OUT wal_fpi int8,
+    OUT wal_bytes numeric,
+    OUT wal_buffers_full int8,
+    OUT jit_functions int8,
+    OUT jit_generation_time float8,
+    OUT jit_inlining_count int8,
+    OUT jit_inlining_time float8,
+    OUT jit_optimization_count int8,
+    OUT jit_optimization_time float8,
+    OUT jit_emission_count int8,
+    OUT jit_emission_time float8,
+    OUT jit_deform_count int8,
+    OUT jit_deform_time float8,
+    OUT parallel_workers_to_launch int8,
+    OUT parallel_workers_launched int8,
+    OUT generic_plan_calls int8,
+    OUT custom_plan_calls int8,
+    OUT stats_since timestamp with time zone,
+    OUT minmax_stats_since timestamp with time zone
+)
+RETURNS SETOF record
+AS 'MODULE_PATHNAME', 'pg_stat_statements_1_13'
+LANGUAGE C STRICT VOLATILE PARALLEL SAFE;
+
+CREATE VIEW pg_stat_statements AS
+  SELECT * FROM pg_stat_statements(true);
+
+GRANT SELECT ON pg_stat_statements TO PUBLIC;
diff --git a/contrib/pg_stat_statements/pg_stat_statements.c b/contrib/pg_stat_statements/pg_stat_statements.c
index e7857f81ec0..4209dcbe623 100644
--- a/contrib/pg_stat_statements/pg_stat_statements.c
+++ b/contrib/pg_stat_statements/pg_stat_statements.c
@@ -114,6 +114,7 @@ typedef enum pgssVersion
 	PGSS_V1_10,
 	PGSS_V1_11,
 	PGSS_V1_12,
+	PGSS_V1_13,
 } pgssVersion;
 
 typedef enum pgssStoreKind
@@ -131,6 +132,13 @@ typedef enum pgssStoreKind
 
 #define PGSS_NUMKIND (PGSS_EXEC + 1)
 
+typedef enum pgssCachedPlanMode
+{
+	PGSS_PLAN_CACHE_MODE_INVALID = 0,
+	PGSS_PLAN_CACHE_MODE_GENERIC = 1,
+	PGSS_PLAN_CACHE_MODE_CUSTOM = 2,
+}			pgssCachedPlanMode;
+
 /*
  * Hashtable key that defines the identity of a hashtable entry.  We separate
  * queries by user and by database even if they are otherwise identical.
@@ -210,6 +218,8 @@ typedef struct Counters
 											 * to be launched */
 	int64		parallel_workers_launched;	/* # of parallel workers actually
 											 * launched */
+	int64		generic_plan_calls; /* number of calls using a generic plan */
+	int64		custom_plan_calls;	/* number of calls using a custom plan */
 } Counters;
 
 /*
@@ -323,6 +333,7 @@ PG_FUNCTION_INFO_V1(pg_stat_statements_1_9);
 PG_FUNCTION_INFO_V1(pg_stat_statements_1_10);
 PG_FUNCTION_INFO_V1(pg_stat_statements_1_11);
 PG_FUNCTION_INFO_V1(pg_stat_statements_1_12);
+PG_FUNCTION_INFO_V1(pg_stat_statements_1_13);
 PG_FUNCTION_INFO_V1(pg_stat_statements);
 PG_FUNCTION_INFO_V1(pg_stat_statements_info);
 
@@ -355,7 +366,8 @@ static void pgss_store(const char *query, int64 queryId,
 					   const struct JitInstrumentation *jitusage,
 					   JumbleState *jstate,
 					   int parallel_workers_to_launch,
-					   int parallel_workers_launched);
+					   int parallel_workers_launched,
+					   CachedPlanType cached_plan_type);
 static void pg_stat_statements_internal(FunctionCallInfo fcinfo,
 										pgssVersion api_version,
 										bool showtext);
@@ -877,7 +889,8 @@ pgss_post_parse_analyze(ParseState *pstate, Query *query, JumbleState *jstate)
 				   NULL,
 				   jstate,
 				   0,
-				   0);
+				   0,
+				   PLAN_CACHE_NOT_SET);
 }
 
 /*
@@ -957,7 +970,8 @@ pgss_planner(Query *parse,
 				   NULL,
 				   NULL,
 				   0,
-				   0);
+				   0,
+				   PLAN_CACHE_NOT_SET);
 	}
 	else
 	{
@@ -1091,7 +1105,8 @@ pgss_ExecutorEnd(QueryDesc *queryDesc)
 				   queryDesc->estate->es_jit ? &queryDesc->estate->es_jit->instr : NULL,
 				   NULL,
 				   queryDesc->estate->es_parallel_workers_to_launch,
-				   queryDesc->estate->es_parallel_workers_launched);
+				   queryDesc->estate->es_parallel_workers_launched,
+				   queryDesc->plannedstmt->cached_plan_type);
 	}
 
 	if (prev_ExecutorEnd)
@@ -1224,7 +1239,8 @@ pgss_ProcessUtility(PlannedStmt *pstmt, const char *queryString,
 				   NULL,
 				   NULL,
 				   0,
-				   0);
+				   0,
+				   PLAN_CACHE_NOT_SET);
 	}
 	else
 	{
@@ -1287,7 +1303,8 @@ pgss_store(const char *query, int64 queryId,
 		   const struct JitInstrumentation *jitusage,
 		   JumbleState *jstate,
 		   int parallel_workers_to_launch,
-		   int parallel_workers_launched)
+		   int parallel_workers_launched,
+		   CachedPlanType cached_plan_type)
 {
 	pgssHashKey key;
 	pgssEntry  *entry;
@@ -1495,6 +1512,14 @@ pgss_store(const char *query, int64 queryId,
 		entry->counters.parallel_workers_to_launch += parallel_workers_to_launch;
 		entry->counters.parallel_workers_launched += parallel_workers_launched;
 
+		if (cached_plan_type > PLAN_CACHE_NOT_SET)
+		{
+			if (cached_plan_type == PLAN_CACHE_GENERIC)
+				entry->counters.generic_plan_calls++;
+			else
+				entry->counters.custom_plan_calls++;
+		}
+
 		SpinLockRelease(&entry->mutex);
 	}
 
@@ -1562,7 +1587,8 @@ pg_stat_statements_reset(PG_FUNCTION_ARGS)
 #define PG_STAT_STATEMENTS_COLS_V1_10	43
 #define PG_STAT_STATEMENTS_COLS_V1_11	49
 #define PG_STAT_STATEMENTS_COLS_V1_12	52
-#define PG_STAT_STATEMENTS_COLS			52	/* maximum of above */
+#define PG_STAT_STATEMENTS_COLS_V1_13	54
+#define PG_STAT_STATEMENTS_COLS			54	/* maximum of above */
 
 /*
  * Retrieve statement statistics.
@@ -1574,6 +1600,16 @@ pg_stat_statements_reset(PG_FUNCTION_ARGS)
  * expected API version is identified by embedding it in the C name of the
  * function.  Unfortunately we weren't bright enough to do that for 1.1.
  */
+Datum
+pg_stat_statements_1_13(PG_FUNCTION_ARGS)
+{
+	bool		showtext = PG_GETARG_BOOL(0);
+
+	pg_stat_statements_internal(fcinfo, PGSS_V1_13, showtext);
+
+	return (Datum) 0;
+}
+
 Datum
 pg_stat_statements_1_12(PG_FUNCTION_ARGS)
 {
@@ -1732,6 +1768,10 @@ pg_stat_statements_internal(FunctionCallInfo fcinfo,
 			if (api_version != PGSS_V1_12)
 				elog(ERROR, "incorrect number of output arguments");
 			break;
+		case PG_STAT_STATEMENTS_COLS_V1_13:
+			if (api_version != PGSS_V1_13)
+				elog(ERROR, "incorrect number of output arguments");
+			break;
 		default:
 			elog(ERROR, "incorrect number of output arguments");
 	}
@@ -1984,6 +2024,11 @@ pg_stat_statements_internal(FunctionCallInfo fcinfo,
 			values[i++] = Int64GetDatumFast(tmp.parallel_workers_to_launch);
 			values[i++] = Int64GetDatumFast(tmp.parallel_workers_launched);
 		}
+		if (api_version >= PGSS_V1_13)
+		{
+			values[i++] = Int64GetDatumFast(tmp.generic_plan_calls);
+			values[i++] = Int64GetDatumFast(tmp.custom_plan_calls);
+		}
 		if (api_version >= PGSS_V1_11)
 		{
 			values[i++] = TimestampTzGetDatum(stats_since);
@@ -1999,6 +2044,7 @@ pg_stat_statements_internal(FunctionCallInfo fcinfo,
 					 api_version == PGSS_V1_10 ? PG_STAT_STATEMENTS_COLS_V1_10 :
 					 api_version == PGSS_V1_11 ? PG_STAT_STATEMENTS_COLS_V1_11 :
 					 api_version == PGSS_V1_12 ? PG_STAT_STATEMENTS_COLS_V1_12 :
+					 api_version == PGSS_V1_13 ? PG_STAT_STATEMENTS_COLS_V1_13 :
 					 -1 /* fail if you forget to update this assert */ ));
 
 		tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, values, nulls);
diff --git a/contrib/pg_stat_statements/pg_stat_statements.control b/contrib/pg_stat_statements/pg_stat_statements.control
index d45ebc12e36..2eee0ceffa8 100644
--- a/contrib/pg_stat_statements/pg_stat_statements.control
+++ b/contrib/pg_stat_statements/pg_stat_statements.control
@@ -1,5 +1,5 @@
 # pg_stat_statements extension
 comment = 'track planning and execution statistics of all SQL statements executed'
-default_version = '1.12'
+default_version = '1.13'
 module_pathname = '$libdir/pg_stat_statements'
 relocatable = true
diff --git a/contrib/pg_stat_statements/sql/plancache.sql b/contrib/pg_stat_statements/sql/plancache.sql
new file mode 100644
index 00000000000..f3878889ea6
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/plancache.sql
@@ -0,0 +1,129 @@
+--
+-- Information related to plan cache
+--
+
+--
+-- Setup
+--
+CREATE OR REPLACE FUNCTION select_one_func(int) RETURNS VOID AS $$
+DECLARE
+    ret INT;
+BEGIN
+    SELECT $1 INTO ret;
+END;
+$$ LANGUAGE plpgsql;
+CREATE OR REPLACE PROCEDURE select_one_proc(int) AS $$
+DECLARE
+    ret INT;
+BEGIN
+    select $1 INTO ret;
+END;
+$$ LANGUAGE plpgsql;
+
+--
+-- plan cache counters for prepared statements
+--
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+PREPARE p1 AS SELECT $1;
+-- plan cache auto
+SET plan_cache_mode TO auto;
+EXECUTE p1(1);
+-- force generic plan
+SET plan_cache_mode TO force_generic_plan;
+EXECUTE p1(1);
+-- force custom plan
+SET plan_cache_mode TO force_custom_plan;
+EXECUTE p1(1);
+SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, query FROM pg_stat_statements
+    ORDER BY query COLLATE "C";
+DEALLOCATE p1;
+
+--
+-- plan cache counters for extended query protocol
+--
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+SELECT $1 \parse p1
+SET plan_cache_mode TO auto;
+\bind_named p1 1
+;
+SET plan_cache_mode TO force_generic_plan;
+\bind_named p1 1
+;
+SET plan_cache_mode TO force_custom_plan;
+\bind_named p1 1
+;
+SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, query FROM pg_stat_statements
+    ORDER BY query COLLATE "C";
+\close_prepared p1
+
+--
+-- plan cache counters for explain|explain (analyze) with prepared statements
+--
+SET pg_stat_statements.track = 'all';
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+PREPARE p1 AS SELECT $1;
+-- plan cache auto
+SET plan_cache_mode TO auto;
+EXPLAIN (COSTS OFF) EXECUTE p1(1);
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) EXECUTE p1(1);
+-- force generic plan
+SET plan_cache_mode TO force_generic_plan;
+EXPLAIN (COSTS OFF) EXECUTE p1(1);
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) EXECUTE p1(1);
+-- force custom plan
+SET plan_cache_mode TO force_custom_plan;
+EXPLAIN (COSTS OFF) EXECUTE p1(1);
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) EXECUTE p1(1);
+SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, query FROM pg_stat_statements
+    ORDER BY query COLLATE "C";
+RESET pg_stat_statements.track;
+DEALLOCATE p1;
+
+--
+-- plan cache counters for functions and procedures
+--
+SET pg_stat_statements.track = 'all';
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+-- plan cache auto
+SET plan_cache_mode TO auto;
+SELECT select_one_func(1);
+CALL select_one_proc(1);
+-- force generic plan
+SET plan_cache_mode TO force_generic_plan;
+SELECT select_one_func(1);
+CALL select_one_proc(1);
+-- force custom plan
+SET plan_cache_mode TO force_custom_plan;
+SELECT select_one_func(1);
+CALL select_one_proc(1);
+SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, query FROM pg_stat_statements
+    ORDER BY query COLLATE "C";
+
+--
+-- plan cache counters for explain|explain (analyze) with functions and procedures
+--
+SET pg_stat_statements.track = 'all';
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+-- plan cache auto
+SET plan_cache_mode TO auto;
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) SELECT select_one_func(1);
+EXPLAIN (COSTS OFF) SELECT select_one_func(1);
+CALL select_one_proc(1);
+-- force generic plan
+SET plan_cache_mode TO force_generic_plan;
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) SELECT select_one_func(1);
+EXPLAIN (COSTS OFF) SELECT select_one_func(1);
+CALL select_one_proc(1);
+-- force custom plan
+SET plan_cache_mode TO force_custom_plan;
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) SELECT select_one_func(1);
+EXPLAIN (COSTS OFF) SELECT select_one_func(1);
+CALL select_one_proc(1);
+SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, query FROM pg_stat_statements
+    ORDER BY query COLLATE "C";
+
+--
+-- Cleanup
+--
+DROP FUNCTION select_one_func(int);
+DROP PROCEDURE select_one_proc(int);
diff --git a/doc/src/sgml/pgstatstatements.sgml b/doc/src/sgml/pgstatstatements.sgml
index 7baa07dcdbf..08b5fec9e10 100644
--- a/doc/src/sgml/pgstatstatements.sgml
+++ b/doc/src/sgml/pgstatstatements.sgml
@@ -554,6 +554,24 @@
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>generic_plan_calls</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Number of times the statement was executed using a generic plan
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>custom_plan_calls</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Number of times the statement was executed using a custom plan
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>stats_since</structfield> <type>timestamp with time zone</type>
diff --git a/src/backend/commands/foreigncmds.c b/src/backend/commands/foreigncmds.c
index 8d2d7431544..37342cb94ef 100644
--- a/src/backend/commands/foreigncmds.c
+++ b/src/backend/commands/foreigncmds.c
@@ -1588,6 +1588,7 @@ ImportForeignSchema(ImportForeignSchemaStmt *stmt)
 			pstmt->utilityStmt = (Node *) cstmt;
 			pstmt->stmt_location = rs->stmt_location;
 			pstmt->stmt_len = rs->stmt_len;
+			pstmt->cached_plan_type = PLAN_CACHE_NOT_SET;
 
 			/* Execute statement */
 			ProcessUtility(pstmt, cmd, false,
diff --git a/src/backend/commands/schemacmds.c b/src/backend/commands/schemacmds.c
index 546160f0941..3cf0031c991 100644
--- a/src/backend/commands/schemacmds.c
+++ b/src/backend/commands/schemacmds.c
@@ -215,6 +215,7 @@ CreateSchemaCommand(CreateSchemaStmt *stmt, const char *queryString,
 		wrapper->utilityStmt = stmt;
 		wrapper->stmt_location = stmt_location;
 		wrapper->stmt_len = stmt_len;
+		wrapper->cached_plan_type = PLAN_CACHE_NOT_SET;
 
 		/* do this step */
 		ProcessUtility(wrapper,
diff --git a/src/backend/executor/execParallel.c b/src/backend/executor/execParallel.c
index f3e77bda279..b0e386461d2 100644
--- a/src/backend/executor/execParallel.c
+++ b/src/backend/executor/execParallel.c
@@ -189,6 +189,7 @@ ExecSerializePlan(Plan *plan, EState *estate)
 	pstmt->permInfos = estate->es_rteperminfos;
 	pstmt->resultRelations = NIL;
 	pstmt->appendRelations = NIL;
+	pstmt->cached_plan_type = PLAN_CACHE_NOT_SET;
 
 	/*
 	 * Transfer only parallel-safe subplans, leaving a NULL "hole" in the list
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index c989e72cac5..c3f41d5274a 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -582,6 +582,7 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions,
 	result->utilityStmt = parse->utilityStmt;
 	result->stmt_location = parse->stmt_location;
 	result->stmt_len = parse->stmt_len;
+	result->cached_plan_type = PLAN_CACHE_NOT_SET;
 
 	result->jitFlags = PGJIT_NONE;
 	if (jit_enabled && jit_above_cost >= 0 &&
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 2f8c3d5f918..4a92d1e560e 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -988,6 +988,7 @@ pg_plan_queries(List *querytrees, const char *query_string, int cursorOptions,
 			stmt->stmt_location = query->stmt_location;
 			stmt->stmt_len = query->stmt_len;
 			stmt->queryId = query->queryId;
+			stmt->cached_plan_type = PLAN_CACHE_NOT_SET;
 		}
 		else
 		{
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 4c1faf5575c..046b04b6ca9 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -1234,6 +1234,7 @@ ProcessUtilitySlow(ParseState *pstate,
 							wrapper->utilityStmt = stmt;
 							wrapper->stmt_location = pstmt->stmt_location;
 							wrapper->stmt_len = pstmt->stmt_len;
+							wrapper->cached_plan_type = PLAN_CACHE_NOT_SET;
 
 							ProcessUtility(wrapper,
 										   queryString,
@@ -1964,6 +1965,7 @@ ProcessUtilityForAlterTable(Node *stmt, AlterTableUtilityContext *context)
 	wrapper->utilityStmt = stmt;
 	wrapper->stmt_location = context->pstmt->stmt_location;
 	wrapper->stmt_len = context->pstmt->stmt_len;
+	wrapper->cached_plan_type = PLAN_CACHE_NOT_SET;
 
 	ProcessUtility(wrapper,
 				   context->queryString,
diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c
index 89a1c79e984..f4d2b9458a5 100644
--- a/src/backend/utils/cache/plancache.c
+++ b/src/backend/utils/cache/plancache.c
@@ -1283,6 +1283,7 @@ GetCachedPlan(CachedPlanSource *plansource, ParamListInfo boundParams,
 	CachedPlan *plan = NULL;
 	List	   *qlist;
 	bool		customplan;
+	ListCell   *lc;
 
 	/* Assert caller is doing things in a sane order */
 	Assert(plansource->magic == CACHEDPLANSOURCE_MAGIC);
@@ -1385,6 +1386,13 @@ GetCachedPlan(CachedPlanSource *plansource, ParamListInfo boundParams,
 		plan->is_saved = true;
 	}
 
+	foreach(lc, plan->stmt_list)
+	{
+		PlannedStmt *pstmt = (PlannedStmt *) lfirst(lc);
+
+		pstmt->cached_plan_type = customplan ? PLAN_CACHE_CUSTOM : PLAN_CACHE_GENERIC;
+	}
+
 	return plan;
 }
 
diff --git a/src/include/nodes/nodes.h b/src/include/nodes/nodes.h
index fbe333d88fa..7f2bbec714d 100644
--- a/src/include/nodes/nodes.h
+++ b/src/include/nodes/nodes.h
@@ -279,6 +279,18 @@ typedef enum CmdType
 								 * with qual */
 } CmdType;
 
+/*
+ * Identifies whether a PlannedStmt is a cached plan, and if so,
+ * whether it is generic or custom.
+ *
+ * Used by executor hooks and instrumentation tools.
+ */
+typedef enum CachedPlanType
+{
+	PLAN_CACHE_NOT_SET,			/* Not a cached plan */
+	PLAN_CACHE_GENERIC,			/* Generic cached plan */
+	PLAN_CACHE_CUSTOM,			/* Custom cached plan */
+}			CachedPlanType;
 
 /*
  * JoinType -
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index 4f59e30d62d..c61794f1fb6 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -131,6 +131,9 @@ typedef struct PlannedStmt
 	/* non-null if this is utility stmt */
 	Node	   *utilityStmt;
 
+	/* type of cached plan, if it is one */
+	CachedPlanType cached_plan_type;
+
 	/* statement location in source string (copied from Query) */
 	/* start location, or -1 if unknown */
 	ParseLoc	stmt_location;
-- 
2.39.5 (Apple Git-154)

#40Andrei Lepikhov
lepihov@gmail.com
In reply to: Sami Imseih (#38)
Re: track generic and custom plans in pg_stat_statements

On 7/22/25 19:13, Sami Imseih wrote:

It may be more efficient to set the is_generic_plan option at the top
plan node (PlannedStmt) and reference it wherever necessary. To identify
a cached plan, we may consider pushing the CachedPlan/CachedPlanSource
pointer down throughout pg_plan_query and maintaining a reference to the
plan (or simply setting a boolean flag) at the same location — inside
the PlannedStmt.

We will need a field to store an enum. let's call it CachedPlanType
with the types of cached plan. We need to be able to differentiate
when cached plans are not used, so a simple boolean is not
sufficient.

Sure. But I modestly hope you would add a CachedPlanSource pointer
solely to the PlannedStmt and restructure it a little as we discussed
above. And no new structures are needed. Am I wrong?

--
regards, Andrei Lepikhov

#41Sami Imseih
samimseih@gmail.com
In reply to: Andrei Lepikhov (#40)
Re: track generic and custom plans in pg_stat_statements

with the types of cached plan. We need to be able to differentiate
when cached plans are not used, so a simple boolean is not
sufficient.

Sure. But I modestly hope you would add a CachedPlanSource pointer
solely to the PlannedStmt and restructure it a little as we discussed
above. And no new structures are needed. Am I wrong?

That was my initial intention somehow to get CachedPlan available
to Executor hooks. But, as you pointed out there is more value in
CachedPlanSource.

I know Michael opposed the idea of carrying these structures,
at least CachedPlan, to Executor hooks ( or maybe just not QueryDesc?? ).
It will be good to see what he think, or if others an opinion about this,
about
adding a pointer to CachedPlanSource in PlannedStmt vs setting a flag in
PlannedStmt to track plan cache type for the current execution? The former
does provide more capability for extensions, as Andrei has pointed out
earlier.

--
Sami

#42Michael Paquier
michael@paquier.xyz
In reply to: Sami Imseih (#41)
Re: track generic and custom plans in pg_stat_statements

On Tue, Jul 22, 2025 at 03:28:24PM -0500, Sami Imseih wrote:

I know Michael opposed the idea of carrying these structures,
at least CachedPlan, to Executor hooks ( or maybe just not QueryDesc?? ).
It will be good to see what he think, or if others an opinion about this,
about
adding a pointer to CachedPlanSource in PlannedStmt vs setting a flag in
PlannedStmt to track plan cache type for the current execution? The former
does provide more capability for extensions, as Andrei has pointed out
earlier.

My line is pretty simple here: we should never include in
executor-specific or plan-specific areas structures that are handled
by entirely different memory contexts or portions of the code, all
parts (executor, query description and or [cached] plan) relying on
entirely different assumptions and contexts, because I suspect that it
would bite us back hard in the shape of corrupted stacks depending on
the timing where these resources are freed. One of the upthread
proposals where a reference of the cached plan pointer was added to an
executor state structure crossed this line.

Simple fields are no-brainers, they are just carried in the same
context as the caller, independently of other states. When it comes
to stats and monitoring, we don't have to be exact, we can sometimes
even be a bit lossy in the reports.

A simple "plan type" field in a PlannedStmt should be more than OK, as
it would be carried across the contexts where we want to know about it
and handle it, aka PGSS entry storing.
--
Michael

#43Sami Imseih
samimseih@gmail.com
In reply to: Michael Paquier (#42)
Re: track generic and custom plans in pg_stat_statements

I know Michael opposed the idea of carrying these structures,
at least CachedPlan, to Executor hooks ( or maybe just not QueryDesc?? ).
It will be good to see what he think, or if others an opinion about this,
about
adding a pointer to CachedPlanSource in PlannedStmt vs setting a flag in
PlannedStmt to track plan cache type for the current execution? The

former

does provide more capability for extensions, as Andrei has pointed out
earlier.

My line is pretty simple here: we should never include in
executor-specific or plan-specific areas structures that are handled
by entirely different memory contexts or portions of the code, all
parts (executor, query description and or [cached] plan) relying on
entirely different assumptions and contexts, because I suspect that it
would bite us back hard in the shape of corrupted stacks depending on
the timing where these resources are freed. One of the upthread
proposals where a reference of the cached plan pointer was added to an
executor state structure crossed this line.

Simple fields are no-brainers, they are just carried in the same
context as the caller, independently of other states. When it comes
to stats and monitoring, we don't have to be exact, we can sometimes
even be a bit lossy in the reports.

A simple "plan type" field in a PlannedStmt should be more than OK, as
it would be carried across the contexts where we want to know about it
and handle it, aka PGSS entry storing.

Thanks!

You make valid point about memory contexts and opening ourselves to
bugs/crashes.

[0]: /messages/by-id/CAA5RZ0tA43qf1HF1g6oYUGNBGFr23F29pf4F9Hymp2z8PyTsPg@mail.gmail.com

[0]: /messages/by-id/CAA5RZ0tA43qf1HF1g6oYUGNBGFr23F29pf4F9Hymp2z8PyTsPg@mail.gmail.com
/messages/by-id/CAA5RZ0tA43qf1HF1g6oYUGNBGFr23F29pf4F9Hymp2z8PyTsPg@mail.gmail.com

--

Sami

#44Michael Paquier
michael@paquier.xyz
In reply to: Sami Imseih (#39)
Re: track generic and custom plans in pg_stat_statements

On Tue, Jul 22, 2025 at 02:52:49PM -0500, Sami Imseih wrote:

We will need a field to store an enum. let's call it CachedPlanType
with the types of cached plan. We need to be able to differentiate
when cached plans are not used, so a simple boolean is not
sufficient.

Guess so.

We can track a field for this enum in PlannedStmt and initialize
it to PLAN_CACHE_NOT_SET whenever we makeNode(PlannedStmt).
In GetCachedPlan, we iterate through plan->stmt_list to set the
value for the PlannedStmt.

CachedPlanType will have to be defined in nodes.h for this to work.

We can avoid the struct altogether and define the new PlannedStmt
field as an "int" and bit-pack the types.

I prefer the CachedPlanType struct to do this, as it's cleaner and
self-documenting.

v13 is the implementation using PlannedStmt as described above.

+ PLAN_CACHE_NOT_SET, /* Not a cached plan */

This should be set to 0 by default, but we could enforce the
definition as well with a PLAN_CACHE_NOT_SET = 0? Nit: perhaps name
it PLAN_CACHE_NONE to outline the fact that it is just not a cached
plan?

--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -131,6 +131,9 @@ typedef struct PlannedStmt
 	/* non-null if this is utility stmt */
 	Node	   *utilityStmt;
+	/* type of cached plan, if it is one */
+	CachedPlanType cached_plan_type;
[...]
+	foreach(lc, plan->stmt_list)
+	{
+		PlannedStmt *pstmt = (PlannedStmt *) lfirst(lc);
+
+		pstmt->cached_plan_type = customplan ? PLAN_CACHE_CUSTOM : PLAN_CACHE_GENERIC;
+	}

Yes, this implementation feels much more natural, with a state
set to the result that we get when directly retrieving it from the
cached plan layer, pushing the data in the executor end hook in PGSS.
No objections to this approach; I can live with that.

A small thing that would be cleaner is to split the patch into two
parts: one for the in-core backend addition and a second for PGSS.
Code paths are different, so it's simple to do.

Andrei, what do you think about all that?
--
Michael

#45Michael Paquier
michael@paquier.xyz
In reply to: Michael Paquier (#44)
1 attachment(s)
Re: track generic and custom plans in pg_stat_statements

On Wed, Jul 23, 2025 at 12:15:06PM +0900, Michael Paquier wrote:

A small thing that would be cleaner is to split the patch into two
parts: one for the in-core backend addition and a second for PGSS.
Code paths are different, so it's simple to do.

I have been looking at the backend part of the change to add the
cached plan type to PlannedStmt, and found the concept clean. I have
moved the definition of the new enum to plannodes.h, tweaked a couple
of comments and the result seemed OK, so applied this part.

Attached is the rest of the patch for PGSS, not really reviewed yet.
I have noticed while going through it that pgssCachedPlanMode was not
required.
--
Michael

Attachments:

v14-0001-pg_stat_statements-Add-counters-for-generic-and-.patchtext/x-diff; charset=us-asciiDownload
From 2954e92d1cff6bc2dd4f8142211e5e36233588e0 Mon Sep 17 00:00:00 2001
From: Sami Imseih <simseih@amazon.com>
Date: Tue, 22 Jul 2025 14:06:56 -0500
Subject: [PATCH v14] pg_stat_statements: Add counters for generic and custom
 plans

This patch adds two new counters to pg_stat_statements:
- generic_plan_calls
- custom_plan_calls

These track how many times a prepared statement was executed using a
generic or custom plan, respectively.
---
 doc/src/sgml/pgstatstatements.sgml            |  18 ++
 contrib/pg_stat_statements/Makefile           |   3 +-
 .../pg_stat_statements/expected/plancache.out | 295 ++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |   2 +
 .../pg_stat_statements--1.12--1.13.sql        |  78 +++++
 .../pg_stat_statements/pg_stat_statements.c   |  53 +++-
 .../pg_stat_statements.control                |   2 +-
 contrib/pg_stat_statements/sql/plancache.sql  | 129 ++++++++
 8 files changed, 571 insertions(+), 9 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/plancache.out
 create mode 100644 contrib/pg_stat_statements/pg_stat_statements--1.12--1.13.sql
 create mode 100644 contrib/pg_stat_statements/sql/plancache.sql

diff --git a/doc/src/sgml/pgstatstatements.sgml b/doc/src/sgml/pgstatstatements.sgml
index 7baa07dcdbf7..08b5fec9e10b 100644
--- a/doc/src/sgml/pgstatstatements.sgml
+++ b/doc/src/sgml/pgstatstatements.sgml
@@ -554,6 +554,24 @@
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>generic_plan_calls</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Number of times the statement was executed using a generic plan
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>custom_plan_calls</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Number of times the statement was executed using a custom plan
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>stats_since</structfield> <type>timestamp with time zone</type>
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index b2bd8794d2a1..996a5fac4487 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -7,6 +7,7 @@ OBJS = \
 
 EXTENSION = pg_stat_statements
 DATA = pg_stat_statements--1.4.sql \
+	pg_stat_statements--1.12--1.13.sql \
 	pg_stat_statements--1.11--1.12.sql pg_stat_statements--1.10--1.11.sql \
 	pg_stat_statements--1.9--1.10.sql pg_stat_statements--1.8--1.9.sql \
 	pg_stat_statements--1.7--1.8.sql pg_stat_statements--1.6--1.7.sql \
@@ -20,7 +21,7 @@ LDFLAGS_SL += $(filter -lm, $(LIBS))
 REGRESS_OPTS = --temp-config $(top_srcdir)/contrib/pg_stat_statements/pg_stat_statements.conf
 REGRESS = select dml cursors utility level_tracking planning \
 	user_activity wal entry_timestamp privileges extended \
-	parallel cleanup oldextversions squashing
+	parallel cleanup oldextversions squashing plancache
 # Disabled because these tests require "shared_preload_libraries=pg_stat_statements",
 # which typical installcheck users do not have (e.g. buildfarm clients).
 NO_INSTALLCHECK = 1
diff --git a/contrib/pg_stat_statements/expected/plancache.out b/contrib/pg_stat_statements/expected/plancache.out
new file mode 100644
index 000000000000..7b05d5a425fc
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/plancache.out
@@ -0,0 +1,295 @@
+--
+-- Information related to plan cache
+--
+--
+-- Setup
+--
+CREATE OR REPLACE FUNCTION select_one_func(int) RETURNS VOID AS $$
+DECLARE
+    ret INT;
+BEGIN
+    SELECT $1 INTO ret;
+END;
+$$ LANGUAGE plpgsql;
+CREATE OR REPLACE PROCEDURE select_one_proc(int) AS $$
+DECLARE
+    ret INT;
+BEGIN
+    select $1 INTO ret;
+END;
+$$ LANGUAGE plpgsql;
+--
+-- plan cache counters for prepared statements
+--
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+PREPARE p1 AS SELECT $1;
+-- plan cache auto
+SET plan_cache_mode TO auto;
+EXECUTE p1(1);
+ ?column? 
+----------
+ 1
+(1 row)
+
+-- force generic plan
+SET plan_cache_mode TO force_generic_plan;
+EXECUTE p1(1);
+ ?column? 
+----------
+ 1
+(1 row)
+
+-- force custom plan
+SET plan_cache_mode TO force_custom_plan;
+EXECUTE p1(1);
+ ?column? 
+----------
+ 1
+(1 row)
+
+SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, query FROM pg_stat_statements
+    ORDER BY query COLLATE "C";
+ calls | generic_plan_calls | custom_plan_calls | toplevel |                       query                        
+-------+--------------------+-------------------+----------+----------------------------------------------------
+     3 |                  1 |                 2 | t        | PREPARE p1 AS SELECT $1
+     1 |                  0 |                 0 | t        | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+     3 |                  0 |                 0 | t        | SET plan_cache_mode TO $1
+(3 rows)
+
+DEALLOCATE p1;
+--
+-- plan cache counters for extended query protocol
+--
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+SELECT $1 \parse p1
+SET plan_cache_mode TO auto;
+\bind_named p1 1
+;
+ ?column? 
+----------
+ 1
+(1 row)
+
+SET plan_cache_mode TO force_generic_plan;
+\bind_named p1 1
+;
+ ?column? 
+----------
+ 1
+(1 row)
+
+SET plan_cache_mode TO force_custom_plan;
+\bind_named p1 1
+;
+ ?column? 
+----------
+ 1
+(1 row)
+
+SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, query FROM pg_stat_statements
+    ORDER BY query COLLATE "C";
+ calls | generic_plan_calls | custom_plan_calls | toplevel |                       query                        
+-------+--------------------+-------------------+----------+----------------------------------------------------
+     3 |                  1 |                 2 | t        | SELECT $1
+     1 |                  0 |                 0 | t        | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+     3 |                  0 |                 0 | t        | SET plan_cache_mode TO $1
+(3 rows)
+
+\close_prepared p1
+--
+-- plan cache counters for explain|explain (analyze) with prepared statements
+--
+SET pg_stat_statements.track = 'all';
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+PREPARE p1 AS SELECT $1;
+-- plan cache auto
+SET plan_cache_mode TO auto;
+EXPLAIN (COSTS OFF) EXECUTE p1(1);
+ QUERY PLAN 
+------------
+ Result
+(1 row)
+
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) EXECUTE p1(1);
+            QUERY PLAN             
+-----------------------------------
+ Result (actual rows=1.00 loops=1)
+(1 row)
+
+-- force generic plan
+SET plan_cache_mode TO force_generic_plan;
+EXPLAIN (COSTS OFF) EXECUTE p1(1);
+ QUERY PLAN 
+------------
+ Result
+(1 row)
+
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) EXECUTE p1(1);
+            QUERY PLAN             
+-----------------------------------
+ Result (actual rows=1.00 loops=1)
+(1 row)
+
+-- force custom plan
+SET plan_cache_mode TO force_custom_plan;
+EXPLAIN (COSTS OFF) EXECUTE p1(1);
+ QUERY PLAN 
+------------
+ Result
+(1 row)
+
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) EXECUTE p1(1);
+            QUERY PLAN             
+-----------------------------------
+ Result (actual rows=1.00 loops=1)
+(1 row)
+
+SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, query FROM pg_stat_statements
+    ORDER BY query COLLATE "C";
+ calls | generic_plan_calls | custom_plan_calls | toplevel |                                      query                                       
+-------+--------------------+-------------------+----------+----------------------------------------------------------------------------------
+     3 |                  0 |                 0 | t        | EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) EXECUTE p1(1)
+     3 |                  0 |                 0 | t        | EXPLAIN (COSTS OFF) EXECUTE p1(1)
+     6 |                  2 |                 4 | f        | PREPARE p1 AS SELECT $1
+     1 |                  0 |                 0 | t        | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+     3 |                  0 |                 0 | t        | SET plan_cache_mode TO $1
+(5 rows)
+
+RESET pg_stat_statements.track;
+DEALLOCATE p1;
+--
+-- plan cache counters for functions and procedures
+--
+SET pg_stat_statements.track = 'all';
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- plan cache auto
+SET plan_cache_mode TO auto;
+SELECT select_one_func(1);
+ select_one_func 
+-----------------
+ 
+(1 row)
+
+CALL select_one_proc(1);
+-- force generic plan
+SET plan_cache_mode TO force_generic_plan;
+SELECT select_one_func(1);
+ select_one_func 
+-----------------
+ 
+(1 row)
+
+CALL select_one_proc(1);
+-- force custom plan
+SET plan_cache_mode TO force_custom_plan;
+SELECT select_one_func(1);
+ select_one_func 
+-----------------
+ 
+(1 row)
+
+CALL select_one_proc(1);
+SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, query FROM pg_stat_statements
+    ORDER BY query COLLATE "C";
+ calls | generic_plan_calls | custom_plan_calls | toplevel |                       query                        
+-------+--------------------+-------------------+----------+----------------------------------------------------
+     3 |                  0 |                 0 | t        | CALL select_one_proc($1)
+     6 |                  2 |                 4 | f        | SELECT $1
+     1 |                  0 |                 0 | t        | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+     3 |                  0 |                 0 | t        | SELECT select_one_func($1)
+     3 |                  0 |                 0 | t        | SET plan_cache_mode TO $1
+(5 rows)
+
+--
+-- plan cache counters for explain|explain (analyze) with functions and procedures
+--
+SET pg_stat_statements.track = 'all';
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- plan cache auto
+SET plan_cache_mode TO auto;
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) SELECT select_one_func(1);
+            QUERY PLAN             
+-----------------------------------
+ Result (actual rows=1.00 loops=1)
+(1 row)
+
+EXPLAIN (COSTS OFF) SELECT select_one_func(1);
+ QUERY PLAN 
+------------
+ Result
+(1 row)
+
+CALL select_one_proc(1);
+-- force generic plan
+SET plan_cache_mode TO force_generic_plan;
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) SELECT select_one_func(1);
+            QUERY PLAN             
+-----------------------------------
+ Result (actual rows=1.00 loops=1)
+(1 row)
+
+EXPLAIN (COSTS OFF) SELECT select_one_func(1);
+ QUERY PLAN 
+------------
+ Result
+(1 row)
+
+CALL select_one_proc(1);
+-- force custom plan
+SET plan_cache_mode TO force_custom_plan;
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) SELECT select_one_func(1);
+            QUERY PLAN             
+-----------------------------------
+ Result (actual rows=1.00 loops=1)
+(1 row)
+
+EXPLAIN (COSTS OFF) SELECT select_one_func(1);
+ QUERY PLAN 
+------------
+ Result
+(1 row)
+
+CALL select_one_proc(1);
+SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, query FROM pg_stat_statements
+    ORDER BY query COLLATE "C";
+ calls | generic_plan_calls | custom_plan_calls | toplevel |                                             query                                              
+-------+--------------------+-------------------+----------+------------------------------------------------------------------------------------------------
+     3 |                  0 |                 0 | t        | CALL select_one_proc($1)
+     3 |                  0 |                 0 | t        | EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) SELECT select_one_func($1)
+     6 |                  0 |                 0 | f        | EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) SELECT select_one_func($1);
+     3 |                  0 |                 0 | t        | EXPLAIN (COSTS OFF) SELECT select_one_func($1)
+     6 |                  2 |                 4 | f        | SELECT $1
+     1 |                  0 |                 0 | t        | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+     3 |                  0 |                 0 | t        | SET plan_cache_mode TO $1
+(7 rows)
+
+--
+-- Cleanup
+--
+DROP FUNCTION select_one_func(int);
+DROP PROCEDURE select_one_proc(int);
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 01a6cbdcf613..110fb82fe120 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -21,6 +21,7 @@ contrib_targets += pg_stat_statements
 install_data(
   'pg_stat_statements.control',
   'pg_stat_statements--1.4.sql',
+  'pg_stat_statements--1.12--1.13.sql',
   'pg_stat_statements--1.11--1.12.sql',
   'pg_stat_statements--1.10--1.11.sql',
   'pg_stat_statements--1.9--1.10.sql',
@@ -57,6 +58,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'plancache',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/pg_stat_statements--1.12--1.13.sql b/contrib/pg_stat_statements/pg_stat_statements--1.12--1.13.sql
new file mode 100644
index 000000000000..2f0eaf14ec34
--- /dev/null
+++ b/contrib/pg_stat_statements/pg_stat_statements--1.12--1.13.sql
@@ -0,0 +1,78 @@
+/* contrib/pg_stat_statements/pg_stat_statements--1.12--1.13.sql */
+
+-- complain if script is sourced in psql, rather than via ALTER EXTENSION
+\echo Use "ALTER EXTENSION pg_stat_statements UPDATE TO '1.13'" to load this file. \quit
+
+/* First we have to remove them from the extension */
+ALTER EXTENSION pg_stat_statements DROP VIEW pg_stat_statements;
+ALTER EXTENSION pg_stat_statements DROP FUNCTION pg_stat_statements(boolean);
+
+/* Then we can drop them */
+DROP VIEW pg_stat_statements;
+DROP FUNCTION pg_stat_statements(boolean);
+
+/* Now redefine */
+CREATE FUNCTION pg_stat_statements(IN showtext boolean,
+    OUT userid oid,
+    OUT dbid oid,
+    OUT toplevel bool,
+    OUT queryid bigint,
+    OUT query text,
+    OUT plans int8,
+    OUT total_plan_time float8,
+    OUT min_plan_time float8,
+    OUT max_plan_time float8,
+    OUT mean_plan_time float8,
+    OUT stddev_plan_time float8,
+    OUT calls int8,
+    OUT total_exec_time float8,
+    OUT min_exec_time float8,
+    OUT max_exec_time float8,
+    OUT mean_exec_time float8,
+    OUT stddev_exec_time float8,
+    OUT rows int8,
+    OUT shared_blks_hit int8,
+    OUT shared_blks_read int8,
+    OUT shared_blks_dirtied int8,
+    OUT shared_blks_written int8,
+    OUT local_blks_hit int8,
+    OUT local_blks_read int8,
+    OUT local_blks_dirtied int8,
+    OUT local_blks_written int8,
+    OUT temp_blks_read int8,
+    OUT temp_blks_written int8,
+    OUT shared_blk_read_time float8,
+    OUT shared_blk_write_time float8,
+    OUT local_blk_read_time float8,
+    OUT local_blk_write_time float8,
+    OUT temp_blk_read_time float8,
+    OUT temp_blk_write_time float8,
+    OUT wal_records int8,
+    OUT wal_fpi int8,
+    OUT wal_bytes numeric,
+    OUT wal_buffers_full int8,
+    OUT jit_functions int8,
+    OUT jit_generation_time float8,
+    OUT jit_inlining_count int8,
+    OUT jit_inlining_time float8,
+    OUT jit_optimization_count int8,
+    OUT jit_optimization_time float8,
+    OUT jit_emission_count int8,
+    OUT jit_emission_time float8,
+    OUT jit_deform_count int8,
+    OUT jit_deform_time float8,
+    OUT parallel_workers_to_launch int8,
+    OUT parallel_workers_launched int8,
+    OUT generic_plan_calls int8,
+    OUT custom_plan_calls int8,
+    OUT stats_since timestamp with time zone,
+    OUT minmax_stats_since timestamp with time zone
+)
+RETURNS SETOF record
+AS 'MODULE_PATHNAME', 'pg_stat_statements_1_13'
+LANGUAGE C STRICT VOLATILE PARALLEL SAFE;
+
+CREATE VIEW pg_stat_statements AS
+  SELECT * FROM pg_stat_statements(true);
+
+GRANT SELECT ON pg_stat_statements TO PUBLIC;
diff --git a/contrib/pg_stat_statements/pg_stat_statements.c b/contrib/pg_stat_statements/pg_stat_statements.c
index e7857f81ec05..8d761c3e1e91 100644
--- a/contrib/pg_stat_statements/pg_stat_statements.c
+++ b/contrib/pg_stat_statements/pg_stat_statements.c
@@ -114,6 +114,7 @@ typedef enum pgssVersion
 	PGSS_V1_10,
 	PGSS_V1_11,
 	PGSS_V1_12,
+	PGSS_V1_13,
 } pgssVersion;
 
 typedef enum pgssStoreKind
@@ -210,6 +211,8 @@ typedef struct Counters
 											 * to be launched */
 	int64		parallel_workers_launched;	/* # of parallel workers actually
 											 * launched */
+	int64		generic_plan_calls; /* number of calls using a generic plan */
+	int64		custom_plan_calls;	/* number of calls using a custom plan */
 } Counters;
 
 /*
@@ -323,6 +326,7 @@ PG_FUNCTION_INFO_V1(pg_stat_statements_1_9);
 PG_FUNCTION_INFO_V1(pg_stat_statements_1_10);
 PG_FUNCTION_INFO_V1(pg_stat_statements_1_11);
 PG_FUNCTION_INFO_V1(pg_stat_statements_1_12);
+PG_FUNCTION_INFO_V1(pg_stat_statements_1_13);
 PG_FUNCTION_INFO_V1(pg_stat_statements);
 PG_FUNCTION_INFO_V1(pg_stat_statements_info);
 
@@ -355,7 +359,8 @@ static void pgss_store(const char *query, int64 queryId,
 					   const struct JitInstrumentation *jitusage,
 					   JumbleState *jstate,
 					   int parallel_workers_to_launch,
-					   int parallel_workers_launched);
+					   int parallel_workers_launched,
+					   CachedPlanType cached_plan_type);
 static void pg_stat_statements_internal(FunctionCallInfo fcinfo,
 										pgssVersion api_version,
 										bool showtext);
@@ -877,7 +882,8 @@ pgss_post_parse_analyze(ParseState *pstate, Query *query, JumbleState *jstate)
 				   NULL,
 				   jstate,
 				   0,
-				   0);
+				   0,
+				   PLAN_CACHE_NONE);
 }
 
 /*
@@ -957,7 +963,8 @@ pgss_planner(Query *parse,
 				   NULL,
 				   NULL,
 				   0,
-				   0);
+				   0,
+				   PLAN_CACHE_NONE);
 	}
 	else
 	{
@@ -1091,7 +1098,8 @@ pgss_ExecutorEnd(QueryDesc *queryDesc)
 				   queryDesc->estate->es_jit ? &queryDesc->estate->es_jit->instr : NULL,
 				   NULL,
 				   queryDesc->estate->es_parallel_workers_to_launch,
-				   queryDesc->estate->es_parallel_workers_launched);
+				   queryDesc->estate->es_parallel_workers_launched,
+				   queryDesc->plannedstmt->cached_plan_type);
 	}
 
 	if (prev_ExecutorEnd)
@@ -1224,7 +1232,8 @@ pgss_ProcessUtility(PlannedStmt *pstmt, const char *queryString,
 				   NULL,
 				   NULL,
 				   0,
-				   0);
+				   0,
+				   PLAN_CACHE_NONE);
 	}
 	else
 	{
@@ -1287,7 +1296,8 @@ pgss_store(const char *query, int64 queryId,
 		   const struct JitInstrumentation *jitusage,
 		   JumbleState *jstate,
 		   int parallel_workers_to_launch,
-		   int parallel_workers_launched)
+		   int parallel_workers_launched,
+		   CachedPlanType cached_plan_type)
 {
 	pgssHashKey key;
 	pgssEntry  *entry;
@@ -1495,6 +1505,14 @@ pgss_store(const char *query, int64 queryId,
 		entry->counters.parallel_workers_to_launch += parallel_workers_to_launch;
 		entry->counters.parallel_workers_launched += parallel_workers_launched;
 
+		if (cached_plan_type > PLAN_CACHE_NONE)
+		{
+			if (cached_plan_type == PLAN_CACHE_GENERIC)
+				entry->counters.generic_plan_calls++;
+			else
+				entry->counters.custom_plan_calls++;
+		}
+
 		SpinLockRelease(&entry->mutex);
 	}
 
@@ -1562,7 +1580,8 @@ pg_stat_statements_reset(PG_FUNCTION_ARGS)
 #define PG_STAT_STATEMENTS_COLS_V1_10	43
 #define PG_STAT_STATEMENTS_COLS_V1_11	49
 #define PG_STAT_STATEMENTS_COLS_V1_12	52
-#define PG_STAT_STATEMENTS_COLS			52	/* maximum of above */
+#define PG_STAT_STATEMENTS_COLS_V1_13	54
+#define PG_STAT_STATEMENTS_COLS			54	/* maximum of above */
 
 /*
  * Retrieve statement statistics.
@@ -1574,6 +1593,16 @@ pg_stat_statements_reset(PG_FUNCTION_ARGS)
  * expected API version is identified by embedding it in the C name of the
  * function.  Unfortunately we weren't bright enough to do that for 1.1.
  */
+Datum
+pg_stat_statements_1_13(PG_FUNCTION_ARGS)
+{
+	bool		showtext = PG_GETARG_BOOL(0);
+
+	pg_stat_statements_internal(fcinfo, PGSS_V1_13, showtext);
+
+	return (Datum) 0;
+}
+
 Datum
 pg_stat_statements_1_12(PG_FUNCTION_ARGS)
 {
@@ -1732,6 +1761,10 @@ pg_stat_statements_internal(FunctionCallInfo fcinfo,
 			if (api_version != PGSS_V1_12)
 				elog(ERROR, "incorrect number of output arguments");
 			break;
+		case PG_STAT_STATEMENTS_COLS_V1_13:
+			if (api_version != PGSS_V1_13)
+				elog(ERROR, "incorrect number of output arguments");
+			break;
 		default:
 			elog(ERROR, "incorrect number of output arguments");
 	}
@@ -1984,6 +2017,11 @@ pg_stat_statements_internal(FunctionCallInfo fcinfo,
 			values[i++] = Int64GetDatumFast(tmp.parallel_workers_to_launch);
 			values[i++] = Int64GetDatumFast(tmp.parallel_workers_launched);
 		}
+		if (api_version >= PGSS_V1_13)
+		{
+			values[i++] = Int64GetDatumFast(tmp.generic_plan_calls);
+			values[i++] = Int64GetDatumFast(tmp.custom_plan_calls);
+		}
 		if (api_version >= PGSS_V1_11)
 		{
 			values[i++] = TimestampTzGetDatum(stats_since);
@@ -1999,6 +2037,7 @@ pg_stat_statements_internal(FunctionCallInfo fcinfo,
 					 api_version == PGSS_V1_10 ? PG_STAT_STATEMENTS_COLS_V1_10 :
 					 api_version == PGSS_V1_11 ? PG_STAT_STATEMENTS_COLS_V1_11 :
 					 api_version == PGSS_V1_12 ? PG_STAT_STATEMENTS_COLS_V1_12 :
+					 api_version == PGSS_V1_13 ? PG_STAT_STATEMENTS_COLS_V1_13 :
 					 -1 /* fail if you forget to update this assert */ ));
 
 		tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, values, nulls);
diff --git a/contrib/pg_stat_statements/pg_stat_statements.control b/contrib/pg_stat_statements/pg_stat_statements.control
index d45ebc12e360..2eee0ceffa89 100644
--- a/contrib/pg_stat_statements/pg_stat_statements.control
+++ b/contrib/pg_stat_statements/pg_stat_statements.control
@@ -1,5 +1,5 @@
 # pg_stat_statements extension
 comment = 'track planning and execution statistics of all SQL statements executed'
-default_version = '1.12'
+default_version = '1.13'
 module_pathname = '$libdir/pg_stat_statements'
 relocatable = true
diff --git a/contrib/pg_stat_statements/sql/plancache.sql b/contrib/pg_stat_statements/sql/plancache.sql
new file mode 100644
index 000000000000..f3878889ea6f
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/plancache.sql
@@ -0,0 +1,129 @@
+--
+-- Information related to plan cache
+--
+
+--
+-- Setup
+--
+CREATE OR REPLACE FUNCTION select_one_func(int) RETURNS VOID AS $$
+DECLARE
+    ret INT;
+BEGIN
+    SELECT $1 INTO ret;
+END;
+$$ LANGUAGE plpgsql;
+CREATE OR REPLACE PROCEDURE select_one_proc(int) AS $$
+DECLARE
+    ret INT;
+BEGIN
+    select $1 INTO ret;
+END;
+$$ LANGUAGE plpgsql;
+
+--
+-- plan cache counters for prepared statements
+--
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+PREPARE p1 AS SELECT $1;
+-- plan cache auto
+SET plan_cache_mode TO auto;
+EXECUTE p1(1);
+-- force generic plan
+SET plan_cache_mode TO force_generic_plan;
+EXECUTE p1(1);
+-- force custom plan
+SET plan_cache_mode TO force_custom_plan;
+EXECUTE p1(1);
+SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, query FROM pg_stat_statements
+    ORDER BY query COLLATE "C";
+DEALLOCATE p1;
+
+--
+-- plan cache counters for extended query protocol
+--
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+SELECT $1 \parse p1
+SET plan_cache_mode TO auto;
+\bind_named p1 1
+;
+SET plan_cache_mode TO force_generic_plan;
+\bind_named p1 1
+;
+SET plan_cache_mode TO force_custom_plan;
+\bind_named p1 1
+;
+SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, query FROM pg_stat_statements
+    ORDER BY query COLLATE "C";
+\close_prepared p1
+
+--
+-- plan cache counters for explain|explain (analyze) with prepared statements
+--
+SET pg_stat_statements.track = 'all';
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+PREPARE p1 AS SELECT $1;
+-- plan cache auto
+SET plan_cache_mode TO auto;
+EXPLAIN (COSTS OFF) EXECUTE p1(1);
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) EXECUTE p1(1);
+-- force generic plan
+SET plan_cache_mode TO force_generic_plan;
+EXPLAIN (COSTS OFF) EXECUTE p1(1);
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) EXECUTE p1(1);
+-- force custom plan
+SET plan_cache_mode TO force_custom_plan;
+EXPLAIN (COSTS OFF) EXECUTE p1(1);
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) EXECUTE p1(1);
+SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, query FROM pg_stat_statements
+    ORDER BY query COLLATE "C";
+RESET pg_stat_statements.track;
+DEALLOCATE p1;
+
+--
+-- plan cache counters for functions and procedures
+--
+SET pg_stat_statements.track = 'all';
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+-- plan cache auto
+SET plan_cache_mode TO auto;
+SELECT select_one_func(1);
+CALL select_one_proc(1);
+-- force generic plan
+SET plan_cache_mode TO force_generic_plan;
+SELECT select_one_func(1);
+CALL select_one_proc(1);
+-- force custom plan
+SET plan_cache_mode TO force_custom_plan;
+SELECT select_one_func(1);
+CALL select_one_proc(1);
+SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, query FROM pg_stat_statements
+    ORDER BY query COLLATE "C";
+
+--
+-- plan cache counters for explain|explain (analyze) with functions and procedures
+--
+SET pg_stat_statements.track = 'all';
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+-- plan cache auto
+SET plan_cache_mode TO auto;
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) SELECT select_one_func(1);
+EXPLAIN (COSTS OFF) SELECT select_one_func(1);
+CALL select_one_proc(1);
+-- force generic plan
+SET plan_cache_mode TO force_generic_plan;
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) SELECT select_one_func(1);
+EXPLAIN (COSTS OFF) SELECT select_one_func(1);
+CALL select_one_proc(1);
+-- force custom plan
+SET plan_cache_mode TO force_custom_plan;
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) SELECT select_one_func(1);
+EXPLAIN (COSTS OFF) SELECT select_one_func(1);
+CALL select_one_proc(1);
+SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, query FROM pg_stat_statements
+    ORDER BY query COLLATE "C";
+
+--
+-- Cleanup
+--
+DROP FUNCTION select_one_func(int);
+DROP PROCEDURE select_one_proc(int);
-- 
2.50.0

#46Andrei Lepikhov
lepihov@gmail.com
In reply to: Michael Paquier (#45)
Re: track generic and custom plans in pg_stat_statements

On 24/7/2025 09:03, Michael Paquier wrote:

On Wed, Jul 23, 2025 at 12:15:06PM +0900, Michael Paquier wrote:

A small thing that would be cleaner is to split the patch into two
parts: one for the in-core backend addition and a second for PGSS.
Code paths are different, so it's simple to do.

I have been looking at the backend part of the change to add the
cached plan type to PlannedStmt, and found the concept clean. I have
moved the definition of the new enum to plannodes.h, tweaked a couple
of comments and the result seemed OK, so applied this part.

I see you have chosen a variant with a new enum instead of a pointer to
a plan cache entry. I wonder if you could write the arguments
supporting this choice?

Currently, we are unable to track specific queries and analyse how planning
decisions affect their execution. IMO, this is a missed opportunity, as even
an extension-based feedback system could pave the way for developments
in self-tuning DBMS. Plan cache entries seem to be the most suitable target
for this purpose, as they usually refer to a long-lived statement and already
contain some valuable data.

--
regards, Andrei Lepikhov

#47Tom Lane
tgl@sss.pgh.pa.us
In reply to: Andrei Lepikhov (#46)
Re: track generic and custom plans in pg_stat_statements

Andrei Lepikhov <lepihov@gmail.com> writes:

I see you have chosen a variant with a new enum instead of a pointer to
a plan cache entry. I wonder if you could write the arguments
supporting this choice?

Pointing to a plan cache entry would often mean that the data
structure as a whole is circular (since a plan cache entry
will have a pointer to a plan). That would in particular
make it unsafe for the plan to protect its pointer by incrementing
the cache entry's refcount --- the assemblage could never go away.
So I concur with Michael that what you propose is a bad idea.

That is not to say that I think 719dcf3c4 was a good idea: it looks
rather useless from here. It seems to me that the right place to
accumulate these sorts of stats is in CachedPlanSources, and I don't
see how this helps. What likely *would* help is some hooks in
plancache.c for pg_stat_statements to connect into so it can count
replanning events.

regards, tom lane

#48Sami Imseih
samimseih@gmail.com
In reply to: Tom Lane (#47)
Re: track generic and custom plans in pg_stat_statements

Andrei Lepikhov <lepihov@gmail.com> writes:

I see you have chosen a variant with a new enum instead of a pointer to
a plan cache entry. I wonder if you could write the arguments
supporting this choice?

Pointing to a plan cache entry would often mean that the data
structure as a whole is circular (since a plan cache entry
will have a pointer to a plan). That would in particular
make it unsafe for the plan to protect its pointer by incrementing
the cache entry's refcount --- the assemblage could never go away.
So I concur with Michael that what you propose is a bad idea.

That is not to say that I think 719dcf3c4 was a good idea: it looks
rather useless from here. It seems to me that the right place to
accumulate these sorts of stats is in CachedPlanSources, and I don't
see how this helps. What likely *would* help is some hooks in
plancache.c for pg_stat_statements to connect into so it can count

One possible hook for accumulating custom and generic plans per
queryId would be inside GetCachedPlan. However, this would require
calling pgss_store an extra time, in addition to ExecutorEnd, every time
GetCachedPlan is executed, which could introduce non-negligible
overhead.

--
Sami

#49Tom Lane
tgl@sss.pgh.pa.us
In reply to: Sami Imseih (#48)
Re: track generic and custom plans in pg_stat_statements

Sami Imseih <samimseih@gmail.com> writes:

That is not to say that I think 719dcf3c4 was a good idea: it looks
rather useless from here. It seems to me that the right place to
accumulate these sorts of stats is in CachedPlanSources, and I don't
see how this helps. What likely *would* help is some hooks in
plancache.c for pg_stat_statements to connect into so it can count

One possible hook for accumulating custom and generic plans per
queryId would be inside GetCachedPlan. However, this would require
calling pgss_store an extra time, in addition to ExecutorEnd, every time
GetCachedPlan is executed, which could introduce non-negligible
overhead.

Only if you insist that the way to handle this is to call pgss_store
at that time. I'd be inclined to think about having some transient
process-local data structure that can remember the info until
ExecutorEnd. But the plan tree is not the right place, because of
the circularity problem.

regards, tom lane

#50Sami Imseih
samimseih@gmail.com
In reply to: Tom Lane (#49)
Re: track generic and custom plans in pg_stat_statements

Sami Imseih <samimseih@gmail.com> writes:

That is not to say that I think 719dcf3c4 was a good idea: it looks
rather useless from here. It seems to me that the right place to
accumulate these sorts of stats is in CachedPlanSources, and I don't
see how this helps. What likely *would* help is some hooks in
plancache.c for pg_stat_statements to connect into so it can count

One possible hook for accumulating custom and generic plans per
queryId would be inside GetCachedPlan. However, this would require
calling pgss_store an extra time, in addition to ExecutorEnd, every time
GetCachedPlan is executed, which could introduce non-negligible
overhead.

Only if you insist that the way to handle this is to call pgss_store
at that time. I'd be inclined to think about having some transient
process-local data structure that can remember the info until
ExecutorEnd. But the plan tree is not the right place, because of
the circularity problem.

One option might be to use a local hash table, keyed the same way as the
shared pgss hash (excluding dbid), to handle cases where a backend has
more than one active cached plan. Then at ExecutorEnd, the local entry
could
be looked up and passed to pgss_store. Not sure if this is worth the effort
vs
what has been committed.

--
Sami

#51Sami Imseih
samimseih@gmail.com
In reply to: Sami Imseih (#50)
Re: track generic and custom plans in pg_stat_statements

One option might be to use a local hash table, keyed the same way as the
shared pgss hash (excluding dbid), to handle cases where a backend has
more than one active cached plan. Then at ExecutorEnd, the local entry could
be looked up and passed to pgss_store. Not sure if this is worth the effort vs
what has been committed.

I should also add that making this information available in PlannedStmt allows
for EXPLAIN to also utilize this information. I am thinking we can add
this information
as part of core EXPLAIN or as an option in pg_overexplain.

--
Sami

#52Michael Paquier
michael@paquier.xyz
In reply to: Sami Imseih (#50)
Re: track generic and custom plans in pg_stat_statements

On Thu, Jul 24, 2025 at 01:14:47PM -0500, Sami Imseih wrote:

Sami Imseih <samimseih@gmail.com> writes:

That is not to say that I think 719dcf3c4 was a good idea: it looks
rather useless from here. It seems to me that the right place to
accumulate these sorts of stats is in CachedPlanSources, and I don't
see how this helps. What likely *would* help is some hooks in
plancache.c for pg_stat_statements to connect into so it can count

One possible hook for accumulating custom and generic plans per
queryId would be inside GetCachedPlan. However, this would require
calling pgss_store an extra time, in addition to ExecutorEnd, every time
GetCachedPlan is executed, which could introduce non-negligible
overhead.

I would suspect more contention on the PGSS locks in the prepared
statement and/or extended query protocol case if we were to do that.
With the amount of information we want to track in PGSS, which is that
we want to know from where a plan is coming from (generic cache,
custom cache or something else), FWIW I'm still OK with this
information added in PlannedStmt itself to be able to link back to the
state retrieved at the end of the executor because it's also
cost-free.

I'm also OK to be proved wrong based on arguments that could justify
the addition of an extra hook, but I don't really see the use-cases
that could justify such additions yet. Perhaps CachedPlanType is
misnamed, though, would it be more suited to name that as a sort of
"origin" or "source" field concept? We want to know which which
source we have retrieved a plan that a PlannedStmt refers to.
--
Michael

#53Andrei Lepikhov
lepihov@gmail.com
In reply to: Tom Lane (#47)
Re: track generic and custom plans in pg_stat_statements

On 24/7/2025 17:05, Tom Lane wrote:

Andrei Lepikhov <lepihov@gmail.com> writes:

I see you have chosen a variant with a new enum instead of a pointer to
a plan cache entry. I wonder if you could write the arguments
supporting this choice?

Pointing to a plan cache entry would often mean that the data
structure as a whole is circular (since a plan cache entry
will have a pointer to a plan). That would in particular
make it unsafe for the plan to protect its pointer by incrementing
the cache entry's refcount --- the assemblage could never go away.
So I concur with Michael that what you propose is a bad idea.

I was expecting more substantial arguments. The PostgreSQL code does not
restrict back-linking to the source in principle - see
IndexClause::rinfo for an example. I'm not sure what the problem is with
refcount - in extensions, we currently have access to the plan cache
entry for prepared statements.

In this particular case, I suggested storing a pointer to the
CachedPlanSource instead of the CachedPlan. For a custom plan, we won't
encounter any circular references, and for a generic plan, the reference
will be indirect.

Of course, read and write operations should be disabled for such a
pointer, and the copy operation should only duplicate the pointer itself.

--
regards, Andrei Lepikhov

#54Sami Imseih
samimseih@gmail.com
In reply to: Michael Paquier (#52)
Re: track generic and custom plans in pg_stat_statements

Perhaps CachedPlanType is
misnamed, though, would it be more suited to name that as a sort of
"origin" or "source" field concept? We want to know which which
source we have retrieved a plan that a PlannedStmt refers to.

Hmm, I’m not sure I see this as an improvement. In my opinion,
CachedPlanType is a clear name that describes its purpose.

--
Sami

#55Tom Lane
tgl@sss.pgh.pa.us
In reply to: Sami Imseih (#54)
Re: track generic and custom plans in pg_stat_statements

Sami Imseih <samimseih@gmail.com> writes:

Perhaps CachedPlanType is
misnamed, though, would it be more suited to name that as a sort of
"origin" or "source" field concept? We want to know which which
source we have retrieved a plan that a PlannedStmt refers to.

Hmm, I’m not sure I see this as an improvement. In my opinion,
CachedPlanType is a clear name that describes its purpose.

I think Michael's got a point. As of HEAD there are seven different
places that are setting this to PLAN_CACHE_NONE; who's to say that
pg_stat_statements or some other extension might not wish to
distinguish some of those sources? At the very least, user-submitted
versus internally-generated queries might be an interesting
distinction. I don't have a concrete proposal for a different
categorization than what we've got, but it seems worth considering
while we still have the flexibility to change it easily.

regards, tom lane

#56Sami Imseih
samimseih@gmail.com
In reply to: Tom Lane (#55)
Re: track generic and custom plans in pg_stat_statements

Sami Imseih <samimseih@gmail.com> writes:

Perhaps CachedPlanType is
misnamed, though, would it be more suited to name that as a sort of
"origin" or "source" field concept? We want to know which which
source we have retrieved a plan that a PlannedStmt refers to.

Hmm, I’m not sure I see this as an improvement. In my opinion,
CachedPlanType is a clear name that describes its purpose.

I think Michael's got a point. As of HEAD there are seven different
places that are setting this to PLAN_CACHE_NONE; who's to say that
pg_stat_statements or some other extension might not wish to
distinguish some of those sources? At the very least, user-submitted
versus internally-generated queries might be an interesting
distinction. I don't have a concrete proposal for a different
categorization than what we've got, but it seems worth considering
while we still have the flexibility to change it easily.

Sure, I get it now, I think. An example is the cached plan from a sql
in a utility statement of a prepared statement, as an example. right?

I can see that being useful, If I understood that correctly.

--
Sami

#57Michael Paquier
michael@paquier.xyz
In reply to: Sami Imseih (#56)
1 attachment(s)
Re: track generic and custom plans in pg_stat_statements

On Fri, Jul 25, 2025 at 02:34:07PM -0500, Sami Imseih wrote:

Sami Imseih <samimseih@gmail.com> writes:

I think Michael's got a point. As of HEAD there are seven different
places that are setting this to PLAN_CACHE_NONE; who's to say that
pg_stat_statements or some other extension might not wish to
distinguish some of those sources? At the very least, user-submitted
versus internally-generated queries might be an interesting
distinction. I don't have a concrete proposal for a different
categorization than what we've got, but it seems worth considering
while we still have the flexibility to change it easily.

Sure, I get it now, I think. An example is the cached plan from a sql
in a utility statement of a prepared statement, as an example. right?

Attached is my counter-proposal, where I have settled down to four
categories of PlannedStmt:
- "standard" PlannedStmt, when going through the planner.
- internally-generated "fake" PlannedStmt.
- custom cache
- generic cache

We could decide if a few more of the internal "fake" ones are worth
subdividing, but I am feeling that this is kind of OK to start with.
If we want more granularity, I was wondering about some bits to be
able to mix one or more of these categories, but they are all
exclusive as far as I know.
--
Michael

Attachments:

review-plan-categories.patchtext/x-diff; charset=us-asciiDownload
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index 46e2e09ea35b..0d125e7d47c1 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -29,18 +29,18 @@
  */
 
 /* ----------------
- *		CachedPlanType
+ *		PlannedStmtOrigin
  *
- * CachedPlanType identifies whether a PlannedStmt is a cached plan, and if
- * so, whether it is generic or custom.
+ * PlannedStmtOrigin identifies from where a PlannedStmt comes from.
  * ----------------
  */
-typedef enum CachedPlanType
+typedef enum PlannedStmtOrigin
 {
-	PLAN_CACHE_NONE = 0,		/* Not a cached plan */
-	PLAN_CACHE_GENERIC,			/* Generic cached plan */
-	PLAN_CACHE_CUSTOM,			/* Custom cached plan */
-} CachedPlanType;
+	PLAN_STMT_INTERNAL = 0,		/* generated internally by a query */
+	PLAN_STMT_STANDARD,			/* standard planned statement */
+	PLAN_STMT_CACHE_GENERIC,	/* Generic cached plan */
+	PLAN_STMT_CACHE_CUSTOM,		/* Custom cached plan */
+} PlannedStmtOrigin;
 
 /* ----------------
  *		PlannedStmt node
@@ -72,8 +72,8 @@ typedef struct PlannedStmt
 	/* plan identifier (can be set by plugins) */
 	int64		planId;
 
-	/* type of cached plan */
-	CachedPlanType cached_plan_type;
+	/* origin of plan */
+	PlannedStmtOrigin planOrigin;
 
 	/* is it insert|update|delete|merge RETURNING? */
 	bool		hasReturning;
diff --git a/src/backend/commands/foreigncmds.c b/src/backend/commands/foreigncmds.c
index fcd5fcd8915e..77f8461f42ee 100644
--- a/src/backend/commands/foreigncmds.c
+++ b/src/backend/commands/foreigncmds.c
@@ -1588,7 +1588,7 @@ ImportForeignSchema(ImportForeignSchemaStmt *stmt)
 			pstmt->utilityStmt = (Node *) cstmt;
 			pstmt->stmt_location = rs->stmt_location;
 			pstmt->stmt_len = rs->stmt_len;
-			pstmt->cached_plan_type = PLAN_CACHE_NONE;
+			pstmt->planOrigin = PLAN_STMT_INTERNAL;
 
 			/* Execute statement */
 			ProcessUtility(pstmt, cmd, false,
diff --git a/src/backend/commands/schemacmds.c b/src/backend/commands/schemacmds.c
index c00f1a11384f..0f03d9743d20 100644
--- a/src/backend/commands/schemacmds.c
+++ b/src/backend/commands/schemacmds.c
@@ -215,7 +215,7 @@ CreateSchemaCommand(CreateSchemaStmt *stmt, const char *queryString,
 		wrapper->utilityStmt = stmt;
 		wrapper->stmt_location = stmt_location;
 		wrapper->stmt_len = stmt_len;
-		wrapper->cached_plan_type = PLAN_CACHE_NONE;
+		wrapper->planOrigin = PLAN_STMT_INTERNAL;
 
 		/* do this step */
 		ProcessUtility(wrapper,
diff --git a/src/backend/executor/execParallel.c b/src/backend/executor/execParallel.c
index fc76f22fb823..f098a5557cf0 100644
--- a/src/backend/executor/execParallel.c
+++ b/src/backend/executor/execParallel.c
@@ -189,7 +189,7 @@ ExecSerializePlan(Plan *plan, EState *estate)
 	pstmt->permInfos = estate->es_rteperminfos;
 	pstmt->resultRelations = NIL;
 	pstmt->appendRelations = NIL;
-	pstmt->cached_plan_type = PLAN_CACHE_NONE;
+	pstmt->planOrigin = PLAN_STMT_INTERNAL;
 
 	/*
 	 * Transfer only parallel-safe subplans, leaving a NULL "hole" in the list
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index a77b2147e959..d59d6e4c6a02 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -558,6 +558,7 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions,
 
 	result->commandType = parse->commandType;
 	result->queryId = parse->queryId;
+	result->planOrigin = PLAN_STMT_STANDARD;
 	result->hasReturning = (parse->returningList != NIL);
 	result->hasModifyingCTE = parse->hasModifyingCTE;
 	result->canSetTag = parse->canSetTag;
@@ -582,7 +583,6 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions,
 	result->utilityStmt = parse->utilityStmt;
 	result->stmt_location = parse->stmt_location;
 	result->stmt_len = parse->stmt_len;
-	result->cached_plan_type = PLAN_CACHE_NONE;
 
 	result->jitFlags = PGJIT_NONE;
 	if (jit_enabled && jit_above_cost >= 0 &&
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index a297606cdd7f..0cecd4649020 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -988,7 +988,7 @@ pg_plan_queries(List *querytrees, const char *query_string, int cursorOptions,
 			stmt->stmt_location = query->stmt_location;
 			stmt->stmt_len = query->stmt_len;
 			stmt->queryId = query->queryId;
-			stmt->cached_plan_type = PLAN_CACHE_NONE;
+			stmt->planOrigin = PLAN_STMT_INTERNAL;
 		}
 		else
 		{
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index babc34d0cbe1..4f4191b0ea6b 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -1234,7 +1234,7 @@ ProcessUtilitySlow(ParseState *pstate,
 							wrapper->utilityStmt = stmt;
 							wrapper->stmt_location = pstmt->stmt_location;
 							wrapper->stmt_len = pstmt->stmt_len;
-							wrapper->cached_plan_type = PLAN_CACHE_NONE;
+							wrapper->planOrigin = PLAN_STMT_INTERNAL;
 
 							ProcessUtility(wrapper,
 										   queryString,
@@ -1965,7 +1965,7 @@ ProcessUtilityForAlterTable(Node *stmt, AlterTableUtilityContext *context)
 	wrapper->utilityStmt = stmt;
 	wrapper->stmt_location = context->pstmt->stmt_location;
 	wrapper->stmt_len = context->pstmt->stmt_len;
-	wrapper->cached_plan_type = PLAN_CACHE_NONE;
+	wrapper->planOrigin = PLAN_STMT_INTERNAL;
 
 	ProcessUtility(wrapper,
 				   context->queryString,
diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c
index f4d2b9458a5e..0c506d320b13 100644
--- a/src/backend/utils/cache/plancache.c
+++ b/src/backend/utils/cache/plancache.c
@@ -1390,7 +1390,7 @@ GetCachedPlan(CachedPlanSource *plansource, ParamListInfo boundParams,
 	{
 		PlannedStmt *pstmt = (PlannedStmt *) lfirst(lc);
 
-		pstmt->cached_plan_type = customplan ? PLAN_CACHE_CUSTOM : PLAN_CACHE_GENERIC;
+		pstmt->planOrigin = customplan ? PLAN_STMT_CACHE_CUSTOM : PLAN_STMT_CACHE_GENERIC;
 	}
 
 	return plan;
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 3daba26b2372..e6f2e93b2d6f 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -391,7 +391,6 @@ CachedFunctionHashEntry
 CachedFunctionHashKey
 CachedPlan
 CachedPlanSource
-CachedPlanType
 CallContext
 CallStmt
 CancelRequestPacket
@@ -2276,6 +2275,7 @@ PlanInvalItem
 PlanRowMark
 PlanState
 PlannedStmt
+PlannedStmtOrigin
 PlannerGlobal
 PlannerInfo
 PlannerParamItem
#58Andrei Lepikhov
lepihov@gmail.com
In reply to: Michael Paquier (#57)
Re: track generic and custom plans in pg_stat_statements

On 28/7/2025 08:18, Michael Paquier wrote:

We could decide if a few more of the internal "fake" ones are worth
subdividing, but I am feeling that this is kind of OK to start with.
If we want more granularity, I was wondering about some bits to be
able to mix one or more of these categories, but they are all
exclusive as far as I know.

It looks good, but doesn't it seem too narrow?

For the sake of tracking queries, their parse tree and versions of the
plan, it seems worth adding an ext_list field to the Query and
PlannedStmt structures with a convention to add only Extensible nodes.
The core will be responsible only for copying this list from the
Query::ext_list to PlannedStmt::ext_list inside a correct memory
context.

This minor change allows an extension to track a specific query from
the parse tree up to the end of execution and carry as much data as
needed. The extension (pg_stat_statements as well) may add all the
necessary data in the parse hook, planner hook, or any of the
execution hooks. With a trivial naming convention, Extensible nodes of
different extensions will not interfere.
To identify the cached plan, the GetCachedPlan hook may be introduced.

--
regards, Andrei Lepikhov

#59Michael Paquier
michael@paquier.xyz
In reply to: Andrei Lepikhov (#58)
Re: track generic and custom plans in pg_stat_statements

On Mon, Jul 28, 2025 at 08:41:29AM +0200, Andrei Lepikhov wrote:

It looks good, but doesn't it seem too narrow?

For the use case of the thread which is to count the number of custom
vs generic plans, it would be good enough.

This minor change allows an extension to track a specific query from
the parse tree up to the end of execution and carry as much data as
needed. The extension (pg_stat_statements as well) may add all the
necessary data in the parse hook, planner hook, or any of the
execution hooks. With a trivial naming convention, Extensible nodes of
different extensions will not interfere.
To identify the cached plan, the GetCachedPlan hook may be introduced.

Without knowing the actual use cases where these additions can be
useful, introducing this extra amount of infrastructure may not be
justified. Just my 2c and my impressions after studying the whole
thread.
--
Michael

#60Andrei Lepikhov
lepihov@gmail.com
In reply to: Michael Paquier (#59)
Re: track generic and custom plans in pg_stat_statements

On 28/7/2025 08:46, Michael Paquier wrote:

On Mon, Jul 28, 2025 at 08:41:29AM +0200, Andrei Lepikhov wrote:

It looks good, but doesn't it seem too narrow?

For the use case of the thread which is to count the number of custom
vs generic plans, it would be good enough.

Sure, no objections.

This minor change allows an extension to track a specific query from
the parse tree up to the end of execution and carry as much data as
needed. The extension (pg_stat_statements as well) may add all the
necessary data in the parse hook, planner hook, or any of the
execution hooks. With a trivial naming convention, Extensible nodes of
different extensions will not interfere.
To identify the cached plan, the GetCachedPlan hook may be introduced.

Without knowing the actual use cases where these additions can be
useful, introducing this extra amount of infrastructure may not be
justified. Just my 2c and my impressions after studying the whole
thread.

The case is as I already described: in Postgres, extensions have no way
to identify how their path hooks really influenced specific query plan
and its execution. During planning, they have only a pointer to the
Query structure, but at the end of execution, only a PlannedStmt is
available. I propose improving this 'blind' approach to planning.
The only argument I usually receive in response to such a proposal is to
add a beneficial example. The current pg_stat_statements change may be a
good example of the employment of such infrastructure, isn't it?

--
regards, Andrei Lepikhov

#61Sami Imseih
samimseih@gmail.com
In reply to: Andrei Lepikhov (#60)
Re: track generic and custom plans in pg_stat_statements

The current pg_stat_statements change may be a
good example of the employment of such infrastructure, isn't it?

So, the current set of patches now will help move forward the specific
use-case of this thread. If we need something different that will be
useful for more use-cases, perhaps it's better to start a new thread as
a follow-up to this? I think there may be more debate on what that
looks like.

--
Sami

#62Sami Imseih
samimseih@gmail.com
In reply to: Michael Paquier (#57)
Re: track generic and custom plans in pg_stat_statements

Attached is my counter-proposal, where I have settled down to four
categories of PlannedStmt:
- "standard" PlannedStmt, when going through the planner.
- internally-generated "fake" PlannedStmt.
- custom cache
- generic cache

Thanks for the update! I plan on reviewing this tomorrow.

--
Sami

#63Sami Imseih
samimseih@gmail.com
In reply to: Sami Imseih (#62)
2 attachment(s)
Re: track generic and custom plans in pg_stat_statements

Attached is my counter-proposal, where I have settled down to four
categories of PlannedStmt:
- "standard" PlannedStmt, when going through the planner.
- internally-generated "fake" PlannedStmt.
- custom cache
- generic cache

Thanks for the update! I plan on reviewing this tomorrow.

The only comment I have is I think we need a NOT_SET
member, so it can simplify the life of extensions that have code
paths which may or may not have a PlannedStmt, such as
pgss_store. In pgss_store, I don't want to pass the entire PlannedStmt,
nor do I want to pass PLAN_STMT_INTERNAL in the call during
post_parse_analyze, in which case we don't have a plan. Using
PLAN_STMT_INTERNAL as a default value if odd.

v15 includes the change with the above as well as the pg_stat_statements
changes.

--
Sami

Attachments:

v15-0001-Introduce-planOrigin-field-in-PlannedStmt-to-rep.patchapplication/octet-stream; name=v15-0001-Introduce-planOrigin-field-in-PlannedStmt-to-rep.patchDownload
From 62b1f0341ceb399efd709a32951144efa7aabbbe Mon Sep 17 00:00:00 2001
From: Sami Imseih <simseih@amazon.com>
Date: Tue, 29 Jul 2025 15:37:33 -0500
Subject: [PATCH v15 1/2] Introduce planOrigin field in PlannedStmt to replace
 CachedPlanType

Commit 719dcf3c42 introduced a field called CachedPlanType in
PlannedStmt to allow extensions to determine whether a cached plan is
generic or custom.

Since this field is attached to PlannedStmt, it was proposed to
generalize it into a new field called planOrigin. This field captures
where the PlannedStmt originated, such as internal, standard_planner,
custom, or generic. While these are the currently defined origins, it
is conceivable that more granular categories may be added in the
future, for example, internal plans from CTAS.

The planOrigin field also defines a default NOT_SET value to simplify
handling in code paths where a PlannedStmt may not yet be available.
This makes it easier to distinguish unset states from valid origins.

Discussion: https://www.postgresql.org/message-id/1933906.1753462272%40sss.pgh.pa.us
---
 src/backend/commands/foreigncmds.c   |  2 +-
 src/backend/commands/schemacmds.c    |  2 +-
 src/backend/executor/execParallel.c  |  2 +-
 src/backend/optimizer/plan/planner.c |  2 +-
 src/backend/tcop/postgres.c          |  2 +-
 src/backend/tcop/utility.c           |  4 ++--
 src/backend/utils/cache/plancache.c  |  2 +-
 src/include/nodes/plannodes.h        | 21 +++++++++++----------
 src/tools/pgindent/typedefs.list     |  2 +-
 9 files changed, 20 insertions(+), 19 deletions(-)

diff --git a/src/backend/commands/foreigncmds.c b/src/backend/commands/foreigncmds.c
index fcd5fcd8915..77f8461f42e 100644
--- a/src/backend/commands/foreigncmds.c
+++ b/src/backend/commands/foreigncmds.c
@@ -1588,7 +1588,7 @@ ImportForeignSchema(ImportForeignSchemaStmt *stmt)
 			pstmt->utilityStmt = (Node *) cstmt;
 			pstmt->stmt_location = rs->stmt_location;
 			pstmt->stmt_len = rs->stmt_len;
-			pstmt->cached_plan_type = PLAN_CACHE_NONE;
+			pstmt->planOrigin = PLAN_STMT_INTERNAL;
 
 			/* Execute statement */
 			ProcessUtility(pstmt, cmd, false,
diff --git a/src/backend/commands/schemacmds.c b/src/backend/commands/schemacmds.c
index c00f1a11384..0f03d9743d2 100644
--- a/src/backend/commands/schemacmds.c
+++ b/src/backend/commands/schemacmds.c
@@ -215,7 +215,7 @@ CreateSchemaCommand(CreateSchemaStmt *stmt, const char *queryString,
 		wrapper->utilityStmt = stmt;
 		wrapper->stmt_location = stmt_location;
 		wrapper->stmt_len = stmt_len;
-		wrapper->cached_plan_type = PLAN_CACHE_NONE;
+		wrapper->planOrigin = PLAN_STMT_INTERNAL;
 
 		/* do this step */
 		ProcessUtility(wrapper,
diff --git a/src/backend/executor/execParallel.c b/src/backend/executor/execParallel.c
index fc76f22fb82..f098a5557cf 100644
--- a/src/backend/executor/execParallel.c
+++ b/src/backend/executor/execParallel.c
@@ -189,7 +189,7 @@ ExecSerializePlan(Plan *plan, EState *estate)
 	pstmt->permInfos = estate->es_rteperminfos;
 	pstmt->resultRelations = NIL;
 	pstmt->appendRelations = NIL;
-	pstmt->cached_plan_type = PLAN_CACHE_NONE;
+	pstmt->planOrigin = PLAN_STMT_INTERNAL;
 
 	/*
 	 * Transfer only parallel-safe subplans, leaving a NULL "hole" in the list
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index a77b2147e95..d59d6e4c6a0 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -558,6 +558,7 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions,
 
 	result->commandType = parse->commandType;
 	result->queryId = parse->queryId;
+	result->planOrigin = PLAN_STMT_STANDARD;
 	result->hasReturning = (parse->returningList != NIL);
 	result->hasModifyingCTE = parse->hasModifyingCTE;
 	result->canSetTag = parse->canSetTag;
@@ -582,7 +583,6 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions,
 	result->utilityStmt = parse->utilityStmt;
 	result->stmt_location = parse->stmt_location;
 	result->stmt_len = parse->stmt_len;
-	result->cached_plan_type = PLAN_CACHE_NONE;
 
 	result->jitFlags = PGJIT_NONE;
 	if (jit_enabled && jit_above_cost >= 0 &&
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index a297606cdd7..0cecd464902 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -988,7 +988,7 @@ pg_plan_queries(List *querytrees, const char *query_string, int cursorOptions,
 			stmt->stmt_location = query->stmt_location;
 			stmt->stmt_len = query->stmt_len;
 			stmt->queryId = query->queryId;
-			stmt->cached_plan_type = PLAN_CACHE_NONE;
+			stmt->planOrigin = PLAN_STMT_INTERNAL;
 		}
 		else
 		{
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index babc34d0cbe..4f4191b0ea6 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -1234,7 +1234,7 @@ ProcessUtilitySlow(ParseState *pstate,
 							wrapper->utilityStmt = stmt;
 							wrapper->stmt_location = pstmt->stmt_location;
 							wrapper->stmt_len = pstmt->stmt_len;
-							wrapper->cached_plan_type = PLAN_CACHE_NONE;
+							wrapper->planOrigin = PLAN_STMT_INTERNAL;
 
 							ProcessUtility(wrapper,
 										   queryString,
@@ -1965,7 +1965,7 @@ ProcessUtilityForAlterTable(Node *stmt, AlterTableUtilityContext *context)
 	wrapper->utilityStmt = stmt;
 	wrapper->stmt_location = context->pstmt->stmt_location;
 	wrapper->stmt_len = context->pstmt->stmt_len;
-	wrapper->cached_plan_type = PLAN_CACHE_NONE;
+	wrapper->planOrigin = PLAN_STMT_INTERNAL;
 
 	ProcessUtility(wrapper,
 				   context->queryString,
diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c
index f4d2b9458a5..0c506d320b1 100644
--- a/src/backend/utils/cache/plancache.c
+++ b/src/backend/utils/cache/plancache.c
@@ -1390,7 +1390,7 @@ GetCachedPlan(CachedPlanSource *plansource, ParamListInfo boundParams,
 	{
 		PlannedStmt *pstmt = (PlannedStmt *) lfirst(lc);
 
-		pstmt->cached_plan_type = customplan ? PLAN_CACHE_CUSTOM : PLAN_CACHE_GENERIC;
+		pstmt->planOrigin = customplan ? PLAN_STMT_CACHE_CUSTOM : PLAN_STMT_CACHE_GENERIC;
 	}
 
 	return plan;
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index 6d8e1e99db3..fdd8d1286d3 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -29,18 +29,19 @@
  */
 
 /* ----------------
- *		CachedPlanType
+ *		PlannedStmtOrigin
  *
- * CachedPlanType identifies whether a PlannedStmt is a cached plan, and if
- * so, whether it is generic or custom.
+ * PlannedStmtOrigin identifies from where a PlannedStmt comes from.
  * ----------------
  */
-typedef enum CachedPlanType
+typedef enum PlannedStmtOrigin
 {
-	PLAN_CACHE_NONE = 0,		/* Not a cached plan */
-	PLAN_CACHE_GENERIC,			/* Generic cached plan */
-	PLAN_CACHE_CUSTOM,			/* Custom cached plan */
-} CachedPlanType;
+	PLAN_STMT_NOT_SET = 0,		/* origin not yet set */
+	PLAN_STMT_INTERNAL,			/* generated internally by a query */
+	PLAN_STMT_STANDARD,			/* standard planned statement */
+	PLAN_STMT_CACHE_GENERIC,	/* Generic cached plan */
+	PLAN_STMT_CACHE_CUSTOM,		/* Custom cached plan */
+} PlannedStmtOrigin;
 
 /* ----------------
  *		PlannedStmt node
@@ -72,8 +73,8 @@ typedef struct PlannedStmt
 	/* plan identifier (can be set by plugins) */
 	int64		planId;
 
-	/* type of cached plan */
-	CachedPlanType cached_plan_type;
+	/* origin of plan */
+	PlannedStmtOrigin planOrigin;
 
 	/* is it insert|update|delete|merge RETURNING? */
 	bool		hasReturning;
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 3daba26b237..e6f2e93b2d6 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -391,7 +391,6 @@ CachedFunctionHashEntry
 CachedFunctionHashKey
 CachedPlan
 CachedPlanSource
-CachedPlanType
 CallContext
 CallStmt
 CancelRequestPacket
@@ -2276,6 +2275,7 @@ PlanInvalItem
 PlanRowMark
 PlanState
 PlannedStmt
+PlannedStmtOrigin
 PlannerGlobal
 PlannerInfo
 PlannerParamItem
-- 
2.39.5 (Apple Git-154)

v15-0002-Add-counters-for-generic-and-custom-plans.patchapplication/octet-stream; name=v15-0002-Add-counters-for-generic-and-custom-plans.patchDownload
From 6a4de99dbbc81a50e823ba1456113b8e6ab0ab5b Mon Sep 17 00:00:00 2001
From: Sami Imseih <simseih@amazon.com>
Date: Tue, 29 Jul 2025 16:40:35 -0500
Subject: [PATCH v15 2/2] Add counters for generic and custom plans

This patch adds two new counters to pg_stat_statements:
- generic_plan_calls
- custom_plan_calls

These track how many times a prepared statement was executed using a
generic or custom plan, respectively.
---
 .../pg_stat_statements/pg_stat_statements.c   | 50 ++++++++++++++++---
 1 file changed, 43 insertions(+), 7 deletions(-)

diff --git a/contrib/pg_stat_statements/pg_stat_statements.c b/contrib/pg_stat_statements/pg_stat_statements.c
index e7857f81ec0..75633910efe 100644
--- a/contrib/pg_stat_statements/pg_stat_statements.c
+++ b/contrib/pg_stat_statements/pg_stat_statements.c
@@ -114,6 +114,7 @@ typedef enum pgssVersion
 	PGSS_V1_10,
 	PGSS_V1_11,
 	PGSS_V1_12,
+	PGSS_V1_13,
 } pgssVersion;
 
 typedef enum pgssStoreKind
@@ -210,6 +211,8 @@ typedef struct Counters
 											 * to be launched */
 	int64		parallel_workers_launched;	/* # of parallel workers actually
 											 * launched */
+	int64		generic_plan_calls; /* number of calls using a generic plan */
+	int64		custom_plan_calls;	/* number of calls using a custom plan */
 } Counters;
 
 /*
@@ -323,6 +326,7 @@ PG_FUNCTION_INFO_V1(pg_stat_statements_1_9);
 PG_FUNCTION_INFO_V1(pg_stat_statements_1_10);
 PG_FUNCTION_INFO_V1(pg_stat_statements_1_11);
 PG_FUNCTION_INFO_V1(pg_stat_statements_1_12);
+PG_FUNCTION_INFO_V1(pg_stat_statements_1_13);
 PG_FUNCTION_INFO_V1(pg_stat_statements);
 PG_FUNCTION_INFO_V1(pg_stat_statements_info);
 
@@ -355,7 +359,8 @@ static void pgss_store(const char *query, int64 queryId,
 					   const struct JitInstrumentation *jitusage,
 					   JumbleState *jstate,
 					   int parallel_workers_to_launch,
-					   int parallel_workers_launched);
+					   int parallel_workers_launched,
+					   PlannedStmtOrigin planOrigin);
 static void pg_stat_statements_internal(FunctionCallInfo fcinfo,
 										pgssVersion api_version,
 										bool showtext);
@@ -877,7 +882,8 @@ pgss_post_parse_analyze(ParseState *pstate, Query *query, JumbleState *jstate)
 				   NULL,
 				   jstate,
 				   0,
-				   0);
+				   0,
+				   PLAN_STMT_NOT_SET);
 }
 
 /*
@@ -957,7 +963,8 @@ pgss_planner(Query *parse,
 				   NULL,
 				   NULL,
 				   0,
-				   0);
+				   0,
+				   result->planOrigin);
 	}
 	else
 	{
@@ -1091,7 +1098,8 @@ pgss_ExecutorEnd(QueryDesc *queryDesc)
 				   queryDesc->estate->es_jit ? &queryDesc->estate->es_jit->instr : NULL,
 				   NULL,
 				   queryDesc->estate->es_parallel_workers_to_launch,
-				   queryDesc->estate->es_parallel_workers_launched);
+				   queryDesc->estate->es_parallel_workers_launched,
+				   queryDesc->plannedstmt->planOrigin);
 	}
 
 	if (prev_ExecutorEnd)
@@ -1224,7 +1232,8 @@ pgss_ProcessUtility(PlannedStmt *pstmt, const char *queryString,
 				   NULL,
 				   NULL,
 				   0,
-				   0);
+				   0,
+				   pstmt->planOrigin);
 	}
 	else
 	{
@@ -1287,7 +1296,8 @@ pgss_store(const char *query, int64 queryId,
 		   const struct JitInstrumentation *jitusage,
 		   JumbleState *jstate,
 		   int parallel_workers_to_launch,
-		   int parallel_workers_launched)
+		   int parallel_workers_launched,
+		   PlannedStmtOrigin planOrigin)
 {
 	pgssHashKey key;
 	pgssEntry  *entry;
@@ -1495,6 +1505,11 @@ pgss_store(const char *query, int64 queryId,
 		entry->counters.parallel_workers_to_launch += parallel_workers_to_launch;
 		entry->counters.parallel_workers_launched += parallel_workers_launched;
 
+		if (planOrigin == PLAN_STMT_CACHE_GENERIC)
+			entry->counters.generic_plan_calls++;
+		else if (planOrigin == PLAN_STMT_CACHE_CUSTOM)
+			entry->counters.custom_plan_calls++;
+
 		SpinLockRelease(&entry->mutex);
 	}
 
@@ -1562,7 +1577,8 @@ pg_stat_statements_reset(PG_FUNCTION_ARGS)
 #define PG_STAT_STATEMENTS_COLS_V1_10	43
 #define PG_STAT_STATEMENTS_COLS_V1_11	49
 #define PG_STAT_STATEMENTS_COLS_V1_12	52
-#define PG_STAT_STATEMENTS_COLS			52	/* maximum of above */
+#define PG_STAT_STATEMENTS_COLS_V1_13	54
+#define PG_STAT_STATEMENTS_COLS			54	/* maximum of above */
 
 /*
  * Retrieve statement statistics.
@@ -1574,6 +1590,16 @@ pg_stat_statements_reset(PG_FUNCTION_ARGS)
  * expected API version is identified by embedding it in the C name of the
  * function.  Unfortunately we weren't bright enough to do that for 1.1.
  */
+Datum
+pg_stat_statements_1_13(PG_FUNCTION_ARGS)
+{
+	bool		showtext = PG_GETARG_BOOL(0);
+
+	pg_stat_statements_internal(fcinfo, PGSS_V1_13, showtext);
+
+	return (Datum) 0;
+}
+
 Datum
 pg_stat_statements_1_12(PG_FUNCTION_ARGS)
 {
@@ -1732,6 +1758,10 @@ pg_stat_statements_internal(FunctionCallInfo fcinfo,
 			if (api_version != PGSS_V1_12)
 				elog(ERROR, "incorrect number of output arguments");
 			break;
+		case PG_STAT_STATEMENTS_COLS_V1_13:
+			if (api_version != PGSS_V1_13)
+				elog(ERROR, "incorrect number of output arguments");
+			break;
 		default:
 			elog(ERROR, "incorrect number of output arguments");
 	}
@@ -1984,6 +2014,11 @@ pg_stat_statements_internal(FunctionCallInfo fcinfo,
 			values[i++] = Int64GetDatumFast(tmp.parallel_workers_to_launch);
 			values[i++] = Int64GetDatumFast(tmp.parallel_workers_launched);
 		}
+		if (api_version >= PGSS_V1_13)
+		{
+			values[i++] = Int64GetDatumFast(tmp.generic_plan_calls);
+			values[i++] = Int64GetDatumFast(tmp.custom_plan_calls);
+		}
 		if (api_version >= PGSS_V1_11)
 		{
 			values[i++] = TimestampTzGetDatum(stats_since);
@@ -1999,6 +2034,7 @@ pg_stat_statements_internal(FunctionCallInfo fcinfo,
 					 api_version == PGSS_V1_10 ? PG_STAT_STATEMENTS_COLS_V1_10 :
 					 api_version == PGSS_V1_11 ? PG_STAT_STATEMENTS_COLS_V1_11 :
 					 api_version == PGSS_V1_12 ? PG_STAT_STATEMENTS_COLS_V1_12 :
+					 api_version == PGSS_V1_13 ? PG_STAT_STATEMENTS_COLS_V1_13 :
 					 -1 /* fail if you forget to update this assert */ ));
 
 		tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, values, nulls);
-- 
2.39.5 (Apple Git-154)

#64Michael Paquier
michael@paquier.xyz
In reply to: Sami Imseih (#63)
Re: track generic and custom plans in pg_stat_statements

On Tue, Jul 29, 2025 at 05:08:09PM -0500, Sami Imseih wrote:

The only comment I have is I think we need a NOT_SET
member, so it can simplify the life of extensions that have code
paths which may or may not have a PlannedStmt, such as
pgss_store.

Okay by me for having a default that maps to something else than the
rest.

+ PLAN_STMT_NOT_SET = 0, /* origin not yet set */

The term "NOT_SET" makes me itch a little bit, even if there is an
existing parallel with OverridingKind. Perhaps your proposal is OK,
still how about "UNKNOWN" instead to use as term for the default?

In pgss_store, I don't want to pass the entire PlannedStmt,

Neither do I.

nor do I want to pass PLAN_STMT_INTERNAL in the call during
post_parse_analyze, in which case we don't have a plan.

Okay.
--
Michael

#65Andrei Lepikhov
lepihov@gmail.com
In reply to: Michael Paquier (#64)
Re: track generic and custom plans in pg_stat_statements

On 30/7/2025 09:20, Michael Paquier wrote:

On Tue, Jul 29, 2025 at 05:08:09PM -0500, Sami Imseih wrote:

The only comment I have is I think we need a NOT_SET
member, so it can simplify the life of extensions that have code
paths which may or may not have a PlannedStmt, such as
pgss_store.

Okay by me for having a default that maps to something else than the
rest.

+ PLAN_STMT_NOT_SET = 0, /* origin not yet set */

The term "NOT_SET" makes me itch a little bit, even if there is an
existing parallel with OverridingKind. Perhaps your proposal is OK,
still how about "UNKNOWN" instead to use as term for the default?

+1 to "UNKNOWN".

There may be various sources for query plans. For instance, in the plan
freezing extension, I often bypass the standard planner completely by
using a plan created in another backend and serialised in shared memory.
Additionally, there is a concept of comparing hinted plans with those
generated freely after an upgrade, which would serve as an extra source
of plans.
One extra example - I sometimes build a 'referenced generic plan', using
incoming constants as a source for clause selectivity calculations,
building a generic plan, likewise SQL Server or Oracle implemented
generics. It seems like a 'hybrid' type of plan ;).

But generally, classification in the PlannedStmtOrigin structure seems a
little strange: a generic plan has a qualitative difference from any
custom one. And any other plan also will be generic or custom, doesn't
it? It is interesting information about the plan source, of course, but
for the sake of performance analysis, it would be profitable to
understand the type of plan. However, the last sentence may be a subject
for another thread.

--
regards, Andrei Lepikhov

#66Sami Imseih
samimseih@gmail.com
In reply to: Andrei Lepikhov (#65)
2 attachment(s)
Re: track generic and custom plans in pg_stat_statements

The term "NOT_SET" makes me itch a little bit, even if there is an
existing parallel with OverridingKind. Perhaps your proposal is OK,
still how about "UNKNOWN" instead to use as term for the default?

+1 to "UNKNOWN".

We currently use both UNKNOWN and NOT_SET in different places.
However, I'm okay with using UNKNOWN, and I've updated it in v16.

But generally, classification in the PlannedStmtOrigin structure seems a
little strange: a generic plan has a qualitative difference from any
custom one. And any other plan also will be generic or custom, doesn't
it?

I am not sure I understand the reasoning here. Can you provide more details/
specific examples?

--
Sami

Attachments:

v16-0001-Introduce-planOrigin-field-in-PlannedStmt-to-rep.patchapplication/octet-stream; name=v16-0001-Introduce-planOrigin-field-in-PlannedStmt-to-rep.patchDownload
From d8dc62eb1c22d0d9a38a3938e501740501756a6a Mon Sep 17 00:00:00 2001
From: Sami Imseih <simseih@amazon.com>
Date: Tue, 29 Jul 2025 15:37:33 -0500
Subject: [PATCH v16 1/2] Introduce planOrigin field in PlannedStmt to replace
 CachedPlanType

Commit 719dcf3c42 introduced a field called CachedPlanType in
PlannedStmt to allow extensions to determine whether a cached plan is
generic or custom.

Since this field is attached to PlannedStmt, it was proposed to
generalize it into a new field called planOrigin. This field captures
where the PlannedStmt originated, such as internal, standard_planner,
custom, or generic. While these are the currently defined origins, it
is conceivable that more granular categories may be added in the
future, for example, internal plans from CTAS.

The planOrigin field also defines a default UNKNOWN value to simplify
handling in code paths where a PlannedStmt may not yet be available.
This makes it easier to distinguish unset states from valid origins.

Discussion: https://www.postgresql.org/message-id/1933906.1753462272%40sss.pgh.pa.us
---
 src/backend/commands/foreigncmds.c   |  2 +-
 src/backend/commands/schemacmds.c    |  2 +-
 src/backend/executor/execParallel.c  |  2 +-
 src/backend/optimizer/plan/planner.c |  2 +-
 src/backend/tcop/postgres.c          |  2 +-
 src/backend/tcop/utility.c           |  4 ++--
 src/backend/utils/cache/plancache.c  |  2 +-
 src/include/nodes/plannodes.h        | 21 +++++++++++----------
 src/tools/pgindent/typedefs.list     |  2 +-
 9 files changed, 20 insertions(+), 19 deletions(-)

diff --git a/src/backend/commands/foreigncmds.c b/src/backend/commands/foreigncmds.c
index fcd5fcd8915..77f8461f42e 100644
--- a/src/backend/commands/foreigncmds.c
+++ b/src/backend/commands/foreigncmds.c
@@ -1588,7 +1588,7 @@ ImportForeignSchema(ImportForeignSchemaStmt *stmt)
 			pstmt->utilityStmt = (Node *) cstmt;
 			pstmt->stmt_location = rs->stmt_location;
 			pstmt->stmt_len = rs->stmt_len;
-			pstmt->cached_plan_type = PLAN_CACHE_NONE;
+			pstmt->planOrigin = PLAN_STMT_INTERNAL;
 
 			/* Execute statement */
 			ProcessUtility(pstmt, cmd, false,
diff --git a/src/backend/commands/schemacmds.c b/src/backend/commands/schemacmds.c
index c00f1a11384..0f03d9743d2 100644
--- a/src/backend/commands/schemacmds.c
+++ b/src/backend/commands/schemacmds.c
@@ -215,7 +215,7 @@ CreateSchemaCommand(CreateSchemaStmt *stmt, const char *queryString,
 		wrapper->utilityStmt = stmt;
 		wrapper->stmt_location = stmt_location;
 		wrapper->stmt_len = stmt_len;
-		wrapper->cached_plan_type = PLAN_CACHE_NONE;
+		wrapper->planOrigin = PLAN_STMT_INTERNAL;
 
 		/* do this step */
 		ProcessUtility(wrapper,
diff --git a/src/backend/executor/execParallel.c b/src/backend/executor/execParallel.c
index fc76f22fb82..f098a5557cf 100644
--- a/src/backend/executor/execParallel.c
+++ b/src/backend/executor/execParallel.c
@@ -189,7 +189,7 @@ ExecSerializePlan(Plan *plan, EState *estate)
 	pstmt->permInfos = estate->es_rteperminfos;
 	pstmt->resultRelations = NIL;
 	pstmt->appendRelations = NIL;
-	pstmt->cached_plan_type = PLAN_CACHE_NONE;
+	pstmt->planOrigin = PLAN_STMT_INTERNAL;
 
 	/*
 	 * Transfer only parallel-safe subplans, leaving a NULL "hole" in the list
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index a77b2147e95..d59d6e4c6a0 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -558,6 +558,7 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions,
 
 	result->commandType = parse->commandType;
 	result->queryId = parse->queryId;
+	result->planOrigin = PLAN_STMT_STANDARD;
 	result->hasReturning = (parse->returningList != NIL);
 	result->hasModifyingCTE = parse->hasModifyingCTE;
 	result->canSetTag = parse->canSetTag;
@@ -582,7 +583,6 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions,
 	result->utilityStmt = parse->utilityStmt;
 	result->stmt_location = parse->stmt_location;
 	result->stmt_len = parse->stmt_len;
-	result->cached_plan_type = PLAN_CACHE_NONE;
 
 	result->jitFlags = PGJIT_NONE;
 	if (jit_enabled && jit_above_cost >= 0 &&
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index a297606cdd7..0cecd464902 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -988,7 +988,7 @@ pg_plan_queries(List *querytrees, const char *query_string, int cursorOptions,
 			stmt->stmt_location = query->stmt_location;
 			stmt->stmt_len = query->stmt_len;
 			stmt->queryId = query->queryId;
-			stmt->cached_plan_type = PLAN_CACHE_NONE;
+			stmt->planOrigin = PLAN_STMT_INTERNAL;
 		}
 		else
 		{
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index babc34d0cbe..4f4191b0ea6 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -1234,7 +1234,7 @@ ProcessUtilitySlow(ParseState *pstate,
 							wrapper->utilityStmt = stmt;
 							wrapper->stmt_location = pstmt->stmt_location;
 							wrapper->stmt_len = pstmt->stmt_len;
-							wrapper->cached_plan_type = PLAN_CACHE_NONE;
+							wrapper->planOrigin = PLAN_STMT_INTERNAL;
 
 							ProcessUtility(wrapper,
 										   queryString,
@@ -1965,7 +1965,7 @@ ProcessUtilityForAlterTable(Node *stmt, AlterTableUtilityContext *context)
 	wrapper->utilityStmt = stmt;
 	wrapper->stmt_location = context->pstmt->stmt_location;
 	wrapper->stmt_len = context->pstmt->stmt_len;
-	wrapper->cached_plan_type = PLAN_CACHE_NONE;
+	wrapper->planOrigin = PLAN_STMT_INTERNAL;
 
 	ProcessUtility(wrapper,
 				   context->queryString,
diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c
index f4d2b9458a5..0c506d320b1 100644
--- a/src/backend/utils/cache/plancache.c
+++ b/src/backend/utils/cache/plancache.c
@@ -1390,7 +1390,7 @@ GetCachedPlan(CachedPlanSource *plansource, ParamListInfo boundParams,
 	{
 		PlannedStmt *pstmt = (PlannedStmt *) lfirst(lc);
 
-		pstmt->cached_plan_type = customplan ? PLAN_CACHE_CUSTOM : PLAN_CACHE_GENERIC;
+		pstmt->planOrigin = customplan ? PLAN_STMT_CACHE_CUSTOM : PLAN_STMT_CACHE_GENERIC;
 	}
 
 	return plan;
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index 6d8e1e99db3..29d7732d6a0 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -29,18 +29,19 @@
  */
 
 /* ----------------
- *		CachedPlanType
+ *		PlannedStmtOrigin
  *
- * CachedPlanType identifies whether a PlannedStmt is a cached plan, and if
- * so, whether it is generic or custom.
+ * PlannedStmtOrigin identifies from where a PlannedStmt comes from.
  * ----------------
  */
-typedef enum CachedPlanType
+typedef enum PlannedStmtOrigin
 {
-	PLAN_CACHE_NONE = 0,		/* Not a cached plan */
-	PLAN_CACHE_GENERIC,			/* Generic cached plan */
-	PLAN_CACHE_CUSTOM,			/* Custom cached plan */
-} CachedPlanType;
+	PLAN_STMT_UNKNOWN = 0,		/* plan origin is not yet known */
+	PLAN_STMT_INTERNAL,			/* generated internally by a query */
+	PLAN_STMT_STANDARD,			/* standard planned statement */
+	PLAN_STMT_CACHE_GENERIC,	/* Generic cached plan */
+	PLAN_STMT_CACHE_CUSTOM,		/* Custom cached plan */
+} PlannedStmtOrigin;
 
 /* ----------------
  *		PlannedStmt node
@@ -72,8 +73,8 @@ typedef struct PlannedStmt
 	/* plan identifier (can be set by plugins) */
 	int64		planId;
 
-	/* type of cached plan */
-	CachedPlanType cached_plan_type;
+	/* origin of plan */
+	PlannedStmtOrigin planOrigin;
 
 	/* is it insert|update|delete|merge RETURNING? */
 	bool		hasReturning;
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 3daba26b237..e6f2e93b2d6 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -391,7 +391,6 @@ CachedFunctionHashEntry
 CachedFunctionHashKey
 CachedPlan
 CachedPlanSource
-CachedPlanType
 CallContext
 CallStmt
 CancelRequestPacket
@@ -2276,6 +2275,7 @@ PlanInvalItem
 PlanRowMark
 PlanState
 PlannedStmt
+PlannedStmtOrigin
 PlannerGlobal
 PlannerInfo
 PlannerParamItem
-- 
2.39.5 (Apple Git-154)

v16-0002-pg_stat_statements-Add-counters-for-generic-and-.patchapplication/octet-stream; name=v16-0002-pg_stat_statements-Add-counters-for-generic-and-.patchDownload
From 136527530600eb7881090d7b57a17d3add9b9c9e Mon Sep 17 00:00:00 2001
From: Sami Imseih <simseih@amazon.com>
Date: Wed, 30 Jul 2025 12:21:52 -0500
Subject: [PATCH v16 2/2] pg_stat_statements: Add counters for generic and
 custom plans

This patch adds two new counters to pg_stat_statements:
- generic_plan_calls
- custom_plan_calls

These track how many times a prepared statement was executed using a
generic or custom plan, respectively.

Discussion: https://www.postgresql.org/message-id/CAA5RZ0uFw8Y9GCFvafhC%3DOA8NnMqVZyzXPfv_EePOt%2Biv1T-qQ%40mail.gmail.com
---
 contrib/pg_stat_statements/Makefile           |   3 +-
 .../pg_stat_statements/expected/plancache.out | 295 ++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |   2 +
 .../pg_stat_statements--1.12--1.13.sql        |  78 +++++
 .../pg_stat_statements/pg_stat_statements.c   |  51 ++-
 .../pg_stat_statements.control                |   2 +-
 contrib/pg_stat_statements/sql/plancache.sql  | 129 ++++++++
 doc/src/sgml/pgstatstatements.sgml            |  18 ++
 8 files changed, 569 insertions(+), 9 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/plancache.out
 create mode 100644 contrib/pg_stat_statements/pg_stat_statements--1.12--1.13.sql
 create mode 100644 contrib/pg_stat_statements/sql/plancache.sql

diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index b2bd8794d2a..996a5fac448 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -7,6 +7,7 @@ OBJS = \
 
 EXTENSION = pg_stat_statements
 DATA = pg_stat_statements--1.4.sql \
+	pg_stat_statements--1.12--1.13.sql \
 	pg_stat_statements--1.11--1.12.sql pg_stat_statements--1.10--1.11.sql \
 	pg_stat_statements--1.9--1.10.sql pg_stat_statements--1.8--1.9.sql \
 	pg_stat_statements--1.7--1.8.sql pg_stat_statements--1.6--1.7.sql \
@@ -20,7 +21,7 @@ LDFLAGS_SL += $(filter -lm, $(LIBS))
 REGRESS_OPTS = --temp-config $(top_srcdir)/contrib/pg_stat_statements/pg_stat_statements.conf
 REGRESS = select dml cursors utility level_tracking planning \
 	user_activity wal entry_timestamp privileges extended \
-	parallel cleanup oldextversions squashing
+	parallel cleanup oldextversions squashing plancache
 # Disabled because these tests require "shared_preload_libraries=pg_stat_statements",
 # which typical installcheck users do not have (e.g. buildfarm clients).
 NO_INSTALLCHECK = 1
diff --git a/contrib/pg_stat_statements/expected/plancache.out b/contrib/pg_stat_statements/expected/plancache.out
new file mode 100644
index 00000000000..7b05d5a425f
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/plancache.out
@@ -0,0 +1,295 @@
+--
+-- Information related to plan cache
+--
+--
+-- Setup
+--
+CREATE OR REPLACE FUNCTION select_one_func(int) RETURNS VOID AS $$
+DECLARE
+    ret INT;
+BEGIN
+    SELECT $1 INTO ret;
+END;
+$$ LANGUAGE plpgsql;
+CREATE OR REPLACE PROCEDURE select_one_proc(int) AS $$
+DECLARE
+    ret INT;
+BEGIN
+    select $1 INTO ret;
+END;
+$$ LANGUAGE plpgsql;
+--
+-- plan cache counters for prepared statements
+--
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+PREPARE p1 AS SELECT $1;
+-- plan cache auto
+SET plan_cache_mode TO auto;
+EXECUTE p1(1);
+ ?column? 
+----------
+ 1
+(1 row)
+
+-- force generic plan
+SET plan_cache_mode TO force_generic_plan;
+EXECUTE p1(1);
+ ?column? 
+----------
+ 1
+(1 row)
+
+-- force custom plan
+SET plan_cache_mode TO force_custom_plan;
+EXECUTE p1(1);
+ ?column? 
+----------
+ 1
+(1 row)
+
+SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, query FROM pg_stat_statements
+    ORDER BY query COLLATE "C";
+ calls | generic_plan_calls | custom_plan_calls | toplevel |                       query                        
+-------+--------------------+-------------------+----------+----------------------------------------------------
+     3 |                  1 |                 2 | t        | PREPARE p1 AS SELECT $1
+     1 |                  0 |                 0 | t        | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+     3 |                  0 |                 0 | t        | SET plan_cache_mode TO $1
+(3 rows)
+
+DEALLOCATE p1;
+--
+-- plan cache counters for extended query protocol
+--
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+SELECT $1 \parse p1
+SET plan_cache_mode TO auto;
+\bind_named p1 1
+;
+ ?column? 
+----------
+ 1
+(1 row)
+
+SET plan_cache_mode TO force_generic_plan;
+\bind_named p1 1
+;
+ ?column? 
+----------
+ 1
+(1 row)
+
+SET plan_cache_mode TO force_custom_plan;
+\bind_named p1 1
+;
+ ?column? 
+----------
+ 1
+(1 row)
+
+SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, query FROM pg_stat_statements
+    ORDER BY query COLLATE "C";
+ calls | generic_plan_calls | custom_plan_calls | toplevel |                       query                        
+-------+--------------------+-------------------+----------+----------------------------------------------------
+     3 |                  1 |                 2 | t        | SELECT $1
+     1 |                  0 |                 0 | t        | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+     3 |                  0 |                 0 | t        | SET plan_cache_mode TO $1
+(3 rows)
+
+\close_prepared p1
+--
+-- plan cache counters for explain|explain (analyze) with prepared statements
+--
+SET pg_stat_statements.track = 'all';
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+PREPARE p1 AS SELECT $1;
+-- plan cache auto
+SET plan_cache_mode TO auto;
+EXPLAIN (COSTS OFF) EXECUTE p1(1);
+ QUERY PLAN 
+------------
+ Result
+(1 row)
+
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) EXECUTE p1(1);
+            QUERY PLAN             
+-----------------------------------
+ Result (actual rows=1.00 loops=1)
+(1 row)
+
+-- force generic plan
+SET plan_cache_mode TO force_generic_plan;
+EXPLAIN (COSTS OFF) EXECUTE p1(1);
+ QUERY PLAN 
+------------
+ Result
+(1 row)
+
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) EXECUTE p1(1);
+            QUERY PLAN             
+-----------------------------------
+ Result (actual rows=1.00 loops=1)
+(1 row)
+
+-- force custom plan
+SET plan_cache_mode TO force_custom_plan;
+EXPLAIN (COSTS OFF) EXECUTE p1(1);
+ QUERY PLAN 
+------------
+ Result
+(1 row)
+
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) EXECUTE p1(1);
+            QUERY PLAN             
+-----------------------------------
+ Result (actual rows=1.00 loops=1)
+(1 row)
+
+SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, query FROM pg_stat_statements
+    ORDER BY query COLLATE "C";
+ calls | generic_plan_calls | custom_plan_calls | toplevel |                                      query                                       
+-------+--------------------+-------------------+----------+----------------------------------------------------------------------------------
+     3 |                  0 |                 0 | t        | EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) EXECUTE p1(1)
+     3 |                  0 |                 0 | t        | EXPLAIN (COSTS OFF) EXECUTE p1(1)
+     6 |                  2 |                 4 | f        | PREPARE p1 AS SELECT $1
+     1 |                  0 |                 0 | t        | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+     3 |                  0 |                 0 | t        | SET plan_cache_mode TO $1
+(5 rows)
+
+RESET pg_stat_statements.track;
+DEALLOCATE p1;
+--
+-- plan cache counters for functions and procedures
+--
+SET pg_stat_statements.track = 'all';
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- plan cache auto
+SET plan_cache_mode TO auto;
+SELECT select_one_func(1);
+ select_one_func 
+-----------------
+ 
+(1 row)
+
+CALL select_one_proc(1);
+-- force generic plan
+SET plan_cache_mode TO force_generic_plan;
+SELECT select_one_func(1);
+ select_one_func 
+-----------------
+ 
+(1 row)
+
+CALL select_one_proc(1);
+-- force custom plan
+SET plan_cache_mode TO force_custom_plan;
+SELECT select_one_func(1);
+ select_one_func 
+-----------------
+ 
+(1 row)
+
+CALL select_one_proc(1);
+SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, query FROM pg_stat_statements
+    ORDER BY query COLLATE "C";
+ calls | generic_plan_calls | custom_plan_calls | toplevel |                       query                        
+-------+--------------------+-------------------+----------+----------------------------------------------------
+     3 |                  0 |                 0 | t        | CALL select_one_proc($1)
+     6 |                  2 |                 4 | f        | SELECT $1
+     1 |                  0 |                 0 | t        | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+     3 |                  0 |                 0 | t        | SELECT select_one_func($1)
+     3 |                  0 |                 0 | t        | SET plan_cache_mode TO $1
+(5 rows)
+
+--
+-- plan cache counters for explain|explain (analyze) with functions and procedures
+--
+SET pg_stat_statements.track = 'all';
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- plan cache auto
+SET plan_cache_mode TO auto;
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) SELECT select_one_func(1);
+            QUERY PLAN             
+-----------------------------------
+ Result (actual rows=1.00 loops=1)
+(1 row)
+
+EXPLAIN (COSTS OFF) SELECT select_one_func(1);
+ QUERY PLAN 
+------------
+ Result
+(1 row)
+
+CALL select_one_proc(1);
+-- force generic plan
+SET plan_cache_mode TO force_generic_plan;
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) SELECT select_one_func(1);
+            QUERY PLAN             
+-----------------------------------
+ Result (actual rows=1.00 loops=1)
+(1 row)
+
+EXPLAIN (COSTS OFF) SELECT select_one_func(1);
+ QUERY PLAN 
+------------
+ Result
+(1 row)
+
+CALL select_one_proc(1);
+-- force custom plan
+SET plan_cache_mode TO force_custom_plan;
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) SELECT select_one_func(1);
+            QUERY PLAN             
+-----------------------------------
+ Result (actual rows=1.00 loops=1)
+(1 row)
+
+EXPLAIN (COSTS OFF) SELECT select_one_func(1);
+ QUERY PLAN 
+------------
+ Result
+(1 row)
+
+CALL select_one_proc(1);
+SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, query FROM pg_stat_statements
+    ORDER BY query COLLATE "C";
+ calls | generic_plan_calls | custom_plan_calls | toplevel |                                             query                                              
+-------+--------------------+-------------------+----------+------------------------------------------------------------------------------------------------
+     3 |                  0 |                 0 | t        | CALL select_one_proc($1)
+     3 |                  0 |                 0 | t        | EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) SELECT select_one_func($1)
+     6 |                  0 |                 0 | f        | EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) SELECT select_one_func($1);
+     3 |                  0 |                 0 | t        | EXPLAIN (COSTS OFF) SELECT select_one_func($1)
+     6 |                  2 |                 4 | f        | SELECT $1
+     1 |                  0 |                 0 | t        | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+     3 |                  0 |                 0 | t        | SET plan_cache_mode TO $1
+(7 rows)
+
+--
+-- Cleanup
+--
+DROP FUNCTION select_one_func(int);
+DROP PROCEDURE select_one_proc(int);
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 01a6cbdcf61..110fb82fe12 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -21,6 +21,7 @@ contrib_targets += pg_stat_statements
 install_data(
   'pg_stat_statements.control',
   'pg_stat_statements--1.4.sql',
+  'pg_stat_statements--1.12--1.13.sql',
   'pg_stat_statements--1.11--1.12.sql',
   'pg_stat_statements--1.10--1.11.sql',
   'pg_stat_statements--1.9--1.10.sql',
@@ -57,6 +58,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'plancache',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/pg_stat_statements--1.12--1.13.sql b/contrib/pg_stat_statements/pg_stat_statements--1.12--1.13.sql
new file mode 100644
index 00000000000..2f0eaf14ec3
--- /dev/null
+++ b/contrib/pg_stat_statements/pg_stat_statements--1.12--1.13.sql
@@ -0,0 +1,78 @@
+/* contrib/pg_stat_statements/pg_stat_statements--1.12--1.13.sql */
+
+-- complain if script is sourced in psql, rather than via ALTER EXTENSION
+\echo Use "ALTER EXTENSION pg_stat_statements UPDATE TO '1.13'" to load this file. \quit
+
+/* First we have to remove them from the extension */
+ALTER EXTENSION pg_stat_statements DROP VIEW pg_stat_statements;
+ALTER EXTENSION pg_stat_statements DROP FUNCTION pg_stat_statements(boolean);
+
+/* Then we can drop them */
+DROP VIEW pg_stat_statements;
+DROP FUNCTION pg_stat_statements(boolean);
+
+/* Now redefine */
+CREATE FUNCTION pg_stat_statements(IN showtext boolean,
+    OUT userid oid,
+    OUT dbid oid,
+    OUT toplevel bool,
+    OUT queryid bigint,
+    OUT query text,
+    OUT plans int8,
+    OUT total_plan_time float8,
+    OUT min_plan_time float8,
+    OUT max_plan_time float8,
+    OUT mean_plan_time float8,
+    OUT stddev_plan_time float8,
+    OUT calls int8,
+    OUT total_exec_time float8,
+    OUT min_exec_time float8,
+    OUT max_exec_time float8,
+    OUT mean_exec_time float8,
+    OUT stddev_exec_time float8,
+    OUT rows int8,
+    OUT shared_blks_hit int8,
+    OUT shared_blks_read int8,
+    OUT shared_blks_dirtied int8,
+    OUT shared_blks_written int8,
+    OUT local_blks_hit int8,
+    OUT local_blks_read int8,
+    OUT local_blks_dirtied int8,
+    OUT local_blks_written int8,
+    OUT temp_blks_read int8,
+    OUT temp_blks_written int8,
+    OUT shared_blk_read_time float8,
+    OUT shared_blk_write_time float8,
+    OUT local_blk_read_time float8,
+    OUT local_blk_write_time float8,
+    OUT temp_blk_read_time float8,
+    OUT temp_blk_write_time float8,
+    OUT wal_records int8,
+    OUT wal_fpi int8,
+    OUT wal_bytes numeric,
+    OUT wal_buffers_full int8,
+    OUT jit_functions int8,
+    OUT jit_generation_time float8,
+    OUT jit_inlining_count int8,
+    OUT jit_inlining_time float8,
+    OUT jit_optimization_count int8,
+    OUT jit_optimization_time float8,
+    OUT jit_emission_count int8,
+    OUT jit_emission_time float8,
+    OUT jit_deform_count int8,
+    OUT jit_deform_time float8,
+    OUT parallel_workers_to_launch int8,
+    OUT parallel_workers_launched int8,
+    OUT generic_plan_calls int8,
+    OUT custom_plan_calls int8,
+    OUT stats_since timestamp with time zone,
+    OUT minmax_stats_since timestamp with time zone
+)
+RETURNS SETOF record
+AS 'MODULE_PATHNAME', 'pg_stat_statements_1_13'
+LANGUAGE C STRICT VOLATILE PARALLEL SAFE;
+
+CREATE VIEW pg_stat_statements AS
+  SELECT * FROM pg_stat_statements(true);
+
+GRANT SELECT ON pg_stat_statements TO PUBLIC;
diff --git a/contrib/pg_stat_statements/pg_stat_statements.c b/contrib/pg_stat_statements/pg_stat_statements.c
index e7857f81ec0..bd4850737d2 100644
--- a/contrib/pg_stat_statements/pg_stat_statements.c
+++ b/contrib/pg_stat_statements/pg_stat_statements.c
@@ -114,6 +114,7 @@ typedef enum pgssVersion
 	PGSS_V1_10,
 	PGSS_V1_11,
 	PGSS_V1_12,
+	PGSS_V1_13,
 } pgssVersion;
 
 typedef enum pgssStoreKind
@@ -210,6 +211,8 @@ typedef struct Counters
 											 * to be launched */
 	int64		parallel_workers_launched;	/* # of parallel workers actually
 											 * launched */
+	int64		generic_plan_calls; /* number of calls using a generic plan */
+	int64		custom_plan_calls;	/* number of calls using a custom plan */
 } Counters;
 
 /*
@@ -323,6 +326,7 @@ PG_FUNCTION_INFO_V1(pg_stat_statements_1_9);
 PG_FUNCTION_INFO_V1(pg_stat_statements_1_10);
 PG_FUNCTION_INFO_V1(pg_stat_statements_1_11);
 PG_FUNCTION_INFO_V1(pg_stat_statements_1_12);
+PG_FUNCTION_INFO_V1(pg_stat_statements_1_13);
 PG_FUNCTION_INFO_V1(pg_stat_statements);
 PG_FUNCTION_INFO_V1(pg_stat_statements_info);
 
@@ -355,7 +359,8 @@ static void pgss_store(const char *query, int64 queryId,
 					   const struct JitInstrumentation *jitusage,
 					   JumbleState *jstate,
 					   int parallel_workers_to_launch,
-					   int parallel_workers_launched);
+					   int parallel_workers_launched,
+					   PlannedStmtOrigin planOrigin);
 static void pg_stat_statements_internal(FunctionCallInfo fcinfo,
 										pgssVersion api_version,
 										bool showtext);
@@ -877,7 +882,8 @@ pgss_post_parse_analyze(ParseState *pstate, Query *query, JumbleState *jstate)
 				   NULL,
 				   jstate,
 				   0,
-				   0);
+				   0,
+				   PLAN_STMT_UNKNOWN);
 }
 
 /*
@@ -957,7 +963,8 @@ pgss_planner(Query *parse,
 				   NULL,
 				   NULL,
 				   0,
-				   0);
+				   0,
+				   result->planOrigin);
 	}
 	else
 	{
@@ -1091,7 +1098,8 @@ pgss_ExecutorEnd(QueryDesc *queryDesc)
 				   queryDesc->estate->es_jit ? &queryDesc->estate->es_jit->instr : NULL,
 				   NULL,
 				   queryDesc->estate->es_parallel_workers_to_launch,
-				   queryDesc->estate->es_parallel_workers_launched);
+				   queryDesc->estate->es_parallel_workers_launched,
+				   queryDesc->plannedstmt->planOrigin);
 	}
 
 	if (prev_ExecutorEnd)
@@ -1224,7 +1232,8 @@ pgss_ProcessUtility(PlannedStmt *pstmt, const char *queryString,
 				   NULL,
 				   NULL,
 				   0,
-				   0);
+				   0,
+				   pstmt->planOrigin);
 	}
 	else
 	{
@@ -1287,7 +1296,8 @@ pgss_store(const char *query, int64 queryId,
 		   const struct JitInstrumentation *jitusage,
 		   JumbleState *jstate,
 		   int parallel_workers_to_launch,
-		   int parallel_workers_launched)
+		   int parallel_workers_launched,
+		   PlannedStmtOrigin planOrigin)
 {
 	pgssHashKey key;
 	pgssEntry  *entry;
@@ -1495,6 +1505,12 @@ pgss_store(const char *query, int64 queryId,
 		entry->counters.parallel_workers_to_launch += parallel_workers_to_launch;
 		entry->counters.parallel_workers_launched += parallel_workers_launched;
 
+		/* plan cache counters */
+		if (planOrigin == PLAN_STMT_CACHE_GENERIC)
+			entry->counters.generic_plan_calls++;
+		else if (planOrigin == PLAN_STMT_CACHE_CUSTOM)
+			entry->counters.custom_plan_calls++;
+
 		SpinLockRelease(&entry->mutex);
 	}
 
@@ -1562,7 +1578,8 @@ pg_stat_statements_reset(PG_FUNCTION_ARGS)
 #define PG_STAT_STATEMENTS_COLS_V1_10	43
 #define PG_STAT_STATEMENTS_COLS_V1_11	49
 #define PG_STAT_STATEMENTS_COLS_V1_12	52
-#define PG_STAT_STATEMENTS_COLS			52	/* maximum of above */
+#define PG_STAT_STATEMENTS_COLS_V1_13	54
+#define PG_STAT_STATEMENTS_COLS			54	/* maximum of above */
 
 /*
  * Retrieve statement statistics.
@@ -1574,6 +1591,16 @@ pg_stat_statements_reset(PG_FUNCTION_ARGS)
  * expected API version is identified by embedding it in the C name of the
  * function.  Unfortunately we weren't bright enough to do that for 1.1.
  */
+Datum
+pg_stat_statements_1_13(PG_FUNCTION_ARGS)
+{
+	bool		showtext = PG_GETARG_BOOL(0);
+
+	pg_stat_statements_internal(fcinfo, PGSS_V1_13, showtext);
+
+	return (Datum) 0;
+}
+
 Datum
 pg_stat_statements_1_12(PG_FUNCTION_ARGS)
 {
@@ -1732,6 +1759,10 @@ pg_stat_statements_internal(FunctionCallInfo fcinfo,
 			if (api_version != PGSS_V1_12)
 				elog(ERROR, "incorrect number of output arguments");
 			break;
+		case PG_STAT_STATEMENTS_COLS_V1_13:
+			if (api_version != PGSS_V1_13)
+				elog(ERROR, "incorrect number of output arguments");
+			break;
 		default:
 			elog(ERROR, "incorrect number of output arguments");
 	}
@@ -1984,6 +2015,11 @@ pg_stat_statements_internal(FunctionCallInfo fcinfo,
 			values[i++] = Int64GetDatumFast(tmp.parallel_workers_to_launch);
 			values[i++] = Int64GetDatumFast(tmp.parallel_workers_launched);
 		}
+		if (api_version >= PGSS_V1_13)
+		{
+			values[i++] = Int64GetDatumFast(tmp.generic_plan_calls);
+			values[i++] = Int64GetDatumFast(tmp.custom_plan_calls);
+		}
 		if (api_version >= PGSS_V1_11)
 		{
 			values[i++] = TimestampTzGetDatum(stats_since);
@@ -1999,6 +2035,7 @@ pg_stat_statements_internal(FunctionCallInfo fcinfo,
 					 api_version == PGSS_V1_10 ? PG_STAT_STATEMENTS_COLS_V1_10 :
 					 api_version == PGSS_V1_11 ? PG_STAT_STATEMENTS_COLS_V1_11 :
 					 api_version == PGSS_V1_12 ? PG_STAT_STATEMENTS_COLS_V1_12 :
+					 api_version == PGSS_V1_13 ? PG_STAT_STATEMENTS_COLS_V1_13 :
 					 -1 /* fail if you forget to update this assert */ ));
 
 		tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, values, nulls);
diff --git a/contrib/pg_stat_statements/pg_stat_statements.control b/contrib/pg_stat_statements/pg_stat_statements.control
index d45ebc12e36..2eee0ceffa8 100644
--- a/contrib/pg_stat_statements/pg_stat_statements.control
+++ b/contrib/pg_stat_statements/pg_stat_statements.control
@@ -1,5 +1,5 @@
 # pg_stat_statements extension
 comment = 'track planning and execution statistics of all SQL statements executed'
-default_version = '1.12'
+default_version = '1.13'
 module_pathname = '$libdir/pg_stat_statements'
 relocatable = true
diff --git a/contrib/pg_stat_statements/sql/plancache.sql b/contrib/pg_stat_statements/sql/plancache.sql
new file mode 100644
index 00000000000..f3878889ea6
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/plancache.sql
@@ -0,0 +1,129 @@
+--
+-- Information related to plan cache
+--
+
+--
+-- Setup
+--
+CREATE OR REPLACE FUNCTION select_one_func(int) RETURNS VOID AS $$
+DECLARE
+    ret INT;
+BEGIN
+    SELECT $1 INTO ret;
+END;
+$$ LANGUAGE plpgsql;
+CREATE OR REPLACE PROCEDURE select_one_proc(int) AS $$
+DECLARE
+    ret INT;
+BEGIN
+    select $1 INTO ret;
+END;
+$$ LANGUAGE plpgsql;
+
+--
+-- plan cache counters for prepared statements
+--
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+PREPARE p1 AS SELECT $1;
+-- plan cache auto
+SET plan_cache_mode TO auto;
+EXECUTE p1(1);
+-- force generic plan
+SET plan_cache_mode TO force_generic_plan;
+EXECUTE p1(1);
+-- force custom plan
+SET plan_cache_mode TO force_custom_plan;
+EXECUTE p1(1);
+SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, query FROM pg_stat_statements
+    ORDER BY query COLLATE "C";
+DEALLOCATE p1;
+
+--
+-- plan cache counters for extended query protocol
+--
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+SELECT $1 \parse p1
+SET plan_cache_mode TO auto;
+\bind_named p1 1
+;
+SET plan_cache_mode TO force_generic_plan;
+\bind_named p1 1
+;
+SET plan_cache_mode TO force_custom_plan;
+\bind_named p1 1
+;
+SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, query FROM pg_stat_statements
+    ORDER BY query COLLATE "C";
+\close_prepared p1
+
+--
+-- plan cache counters for explain|explain (analyze) with prepared statements
+--
+SET pg_stat_statements.track = 'all';
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+PREPARE p1 AS SELECT $1;
+-- plan cache auto
+SET plan_cache_mode TO auto;
+EXPLAIN (COSTS OFF) EXECUTE p1(1);
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) EXECUTE p1(1);
+-- force generic plan
+SET plan_cache_mode TO force_generic_plan;
+EXPLAIN (COSTS OFF) EXECUTE p1(1);
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) EXECUTE p1(1);
+-- force custom plan
+SET plan_cache_mode TO force_custom_plan;
+EXPLAIN (COSTS OFF) EXECUTE p1(1);
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) EXECUTE p1(1);
+SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, query FROM pg_stat_statements
+    ORDER BY query COLLATE "C";
+RESET pg_stat_statements.track;
+DEALLOCATE p1;
+
+--
+-- plan cache counters for functions and procedures
+--
+SET pg_stat_statements.track = 'all';
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+-- plan cache auto
+SET plan_cache_mode TO auto;
+SELECT select_one_func(1);
+CALL select_one_proc(1);
+-- force generic plan
+SET plan_cache_mode TO force_generic_plan;
+SELECT select_one_func(1);
+CALL select_one_proc(1);
+-- force custom plan
+SET plan_cache_mode TO force_custom_plan;
+SELECT select_one_func(1);
+CALL select_one_proc(1);
+SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, query FROM pg_stat_statements
+    ORDER BY query COLLATE "C";
+
+--
+-- plan cache counters for explain|explain (analyze) with functions and procedures
+--
+SET pg_stat_statements.track = 'all';
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+-- plan cache auto
+SET plan_cache_mode TO auto;
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) SELECT select_one_func(1);
+EXPLAIN (COSTS OFF) SELECT select_one_func(1);
+CALL select_one_proc(1);
+-- force generic plan
+SET plan_cache_mode TO force_generic_plan;
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) SELECT select_one_func(1);
+EXPLAIN (COSTS OFF) SELECT select_one_func(1);
+CALL select_one_proc(1);
+-- force custom plan
+SET plan_cache_mode TO force_custom_plan;
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) SELECT select_one_func(1);
+EXPLAIN (COSTS OFF) SELECT select_one_func(1);
+CALL select_one_proc(1);
+SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, query FROM pg_stat_statements
+    ORDER BY query COLLATE "C";
+
+--
+-- Cleanup
+--
+DROP FUNCTION select_one_func(int);
+DROP PROCEDURE select_one_proc(int);
diff --git a/doc/src/sgml/pgstatstatements.sgml b/doc/src/sgml/pgstatstatements.sgml
index 7baa07dcdbf..08b5fec9e10 100644
--- a/doc/src/sgml/pgstatstatements.sgml
+++ b/doc/src/sgml/pgstatstatements.sgml
@@ -554,6 +554,24 @@
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>generic_plan_calls</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Number of times the statement was executed using a generic plan
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>custom_plan_calls</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Number of times the statement was executed using a custom plan
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>stats_since</structfield> <type>timestamp with time zone</type>
-- 
2.39.5 (Apple Git-154)

#67Andrei Lepikhov
lepihov@gmail.com
In reply to: Sami Imseih (#66)
Re: track generic and custom plans in pg_stat_statements

On 7/30/25 21:05, Sami Imseih wrote:

The term "NOT_SET" makes me itch a little bit, even if there is an
existing parallel with OverridingKind. Perhaps your proposal is OK,
still how about "UNKNOWN" instead to use as term for the default?

+1 to "UNKNOWN".

We currently use both UNKNOWN and NOT_SET in different places.
However, I'm okay with using UNKNOWN, and I've updated it in v16.

But generally, classification in the PlannedStmtOrigin structure seems a
little strange: a generic plan has a qualitative difference from any
custom one. And any other plan also will be generic or custom, doesn't
it?

I am not sure I understand the reasoning here. Can you provide more details/
specific examples?

Yep,
When building a generic plan, you don't apply any constant to the
clause, such as 'x<$1'.
That means you can't use histograms or MCV statistics when building a
custom plan. The optimiser should guess and frequently uses just a
'magic constant', like 0.05 or 0.33 for selectivity estimation. It
sometimes drastically reduces the quality of the plan. So, analysing
pg_s_s data, it would be beneficial to determine if a generic plan is
effective or not.

In practice, with this knowledge, we can access the CachedPlanSource of
the corresponding PREPARED statement via an extension and override the
decision made in 'auto' mode. Unfortunately, we cannot obtain a pointer
to plan cache entries for plans prepared by the extended protocol, but
this may be possible in the future.

So, I meant that the source of the plan is one important characteristic,
and the type (custom or generic) is another, independent characteristic

--
regards, Andrei Lepikhov

#68Sami Imseih
samimseih@gmail.com
In reply to: Andrei Lepikhov (#67)
Re: track generic and custom plans in pg_stat_statements

So, analysing
pg_s_s data, it would be beneficial to determine if a generic plan is
effective or not.

Yes, this is the point of adding these statistics to pg_s_s.

In practice, with this knowledge, we can access the CachedPlanSource of
the corresponding PREPARED statement via an extension and override the
decision made in 'auto' mode. Unfortunately, we cannot obtain a pointer
to plan cache entries for plans prepared by the extended protocol, but
this may be possible in the future.

So, I meant that the source of the plan is one important characteristic,
and the type (custom or generic) is another, independent characteristic

The concepts of custom and generic plan types are associated with plan caches,
so they cannot have a different source. right?

--
Sami

#69Michael Paquier
michael@paquier.xyz
In reply to: Sami Imseih (#68)
Re: track generic and custom plans in pg_stat_statements

On Wed, Jul 30, 2025 at 03:09:08PM -0500, Sami Imseih wrote:

In practice, with this knowledge, we can access the CachedPlanSource of
the corresponding PREPARED statement via an extension and override the
decision made in 'auto' mode. Unfortunately, we cannot obtain a pointer
to plan cache entries for plans prepared by the extended protocol, but
this may be possible in the future.

So, I meant that the source of the plan is one important characteristic,
and the type (custom or generic) is another, independent characteristic

It seems to me that we're going to need a bit more than a design
concept here, explaining how this can be useful for core to provide
this knowledge to extensions. A lot of concepts can be defined as
"useful", but it's hard to evaluate what you'd want here and how much
it can be useful in terms of manipulations of the cached plans
depending on what you may aim for. It's also unclear up to which
extent the current state of the code would be able to help in the
concepts you're describing. Anyway, I am not inclined to have pointer
references in structures pointing to other parts of the system, just
because these can be "useful".
--
Michael

#70Michael Paquier
michael@paquier.xyz
In reply to: Sami Imseih (#66)
Re: track generic and custom plans in pg_stat_statements

On Wed, Jul 30, 2025 at 02:05:09PM -0500, Sami Imseih wrote:

The term "NOT_SET" makes me itch a little bit, even if there is an
existing parallel with OverridingKind. Perhaps your proposal is OK,
still how about "UNKNOWN" instead to use as term for the default?

+1 to "UNKNOWN".

We currently use both UNKNOWN and NOT_SET in different places.
However, I'm okay with using UNKNOWN, and I've updated it in v16.

Okay, applied both patches to get all that done, then.

Patch 0002 was not without turbulences:
- Order of the tests in meson.build and Makefile, where plancache
should be placed before the cleanup. The squashing tests get that
wrong, actually..
- Some tweaks to the style of the SQL queries in the tests, two tweaks
in the wording of the docs.
- Lack of coverage for oldextversions for the transfer from 1.12 to
1.13.
- PGSS_FILE_HEADER should be bumped. We have been very bad at that
since 2022, myself included, better later than never.
- I have decided to remove the cases with plan_cache_mode = auto, due
to a lack of predictibility depending on what the backend may decide
to use.
- For the last case with EXPLAIN + procedures, I have added an extra
"toplevel" in the ORDER BY clause, to ensure the order of the results.
"toplevel" was not required for the first three cases, where we don't
track the non-top-level queries.
- Added one round of code indentation.
--
Michael

#71Andrei Lepikhov
lepihov@gmail.com
In reply to: Sami Imseih (#68)
Re: track generic and custom plans in pg_stat_statements

On 30/7/2025 22:09, Sami Imseih wrote:

The concepts of custom and generic plan types are associated with plan caches,
so they cannot have a different source. right?

That is exactly what confused me: what does the 'origin' of the plan
mean? See the comment:

PlannedStmtOrigin identifies from where a PlannedStmt comes from.

The first thing that comes to mind after reading this comment is a
subsystem, such as the SPI, simple or extended protocol, or an
extension. Another meaning is a type of plan, such as 'custom',
'generic', or 'referenced'. As I see, here is a bit different
classification used, not so obvious, at least for me.

--
regards, Andrei Lepikhov

#72Andrei Lepikhov
lepihov@gmail.com
In reply to: Michael Paquier (#69)
Re: track generic and custom plans in pg_stat_statements

On 31/7/2025 02:09, Michael Paquier wrote:

On Wed, Jul 30, 2025 at 03:09:08PM -0500, Sami Imseih wrote:

In practice, with this knowledge, we can access the CachedPlanSource of
the corresponding PREPARED statement via an extension and override the
decision made in 'auto' mode. Unfortunately, we cannot obtain a pointer
to plan cache entries for plans prepared by the extended protocol, but
this may be possible in the future.

So, I meant that the source of the plan is one important characteristic,
and the type (custom or generic) is another, independent characteristic

It seems to me that we're going to need a bit more than a design
concept here, explaining how this can be useful for core to provide
this knowledge to extensions. A lot of concepts can be defined as
"useful", but it's hard to evaluate what you'd want here and how much
it can be useful in terms of manipulations of the cached plans
depending on what you may aim for. It's also unclear up to which
extent the current state of the code would be able to help in the
concepts you're describing. Anyway, I am not inclined to have pointer
references in structures pointing to other parts of the system, just
because these can be "useful".

Don't mind, I also think it wasn't a great idea to save a pointer.
After the discussion, I came up with the idea that an 'extended list' in
some key nodes is a much more helpful and flexible way, serving multiple
purposes.
Anyway, the extensibility model has never been an easy part of the
system to design. So, let's think about that as a first draft approach.

--
regards, Andrei Lepikhov

#73Sami Imseih
samimseih@gmail.com
In reply to: Michael Paquier (#70)
Re: track generic and custom plans in pg_stat_statements

Patch 0002 was not without turbulences:

Thanks! And for sorting out the misses in the patch.

--
Sami