Hi all,
I’d like to propose a patch that introduces a*referenced generic plan*mode.
The patch adds:
*
new EXPLAIN option|REF_GENERIC_PLAN|(not allowed with ANALYZE);
*
new|plan_cache_mode|values|ref_auto|and|force_ref_generic_plan|;
*
a flag to prevent constant folding of Params when building a generic
plan.
Generic and custom plan behavior stays unchanged unless the new option
is used.
Similar ideas exist in other systems: Oracle (Adaptive Cursor Sharing),
SQL Server (Parameter Sensitive Plan Optimization), and Db2 (REOPT).
I’ve attached the patch and a minimal regression test.
Best regards,
Vlada Pogozhelskaya
From 7383e7e57bca1a19ab05d52a7981152edce90192 Mon Sep 17 00:00:00 2001
From: Vlada Pogozhelskaya <pogozhelsk...@gmail.com>
Date: Wed, 3 Sep 2025 17:13:28 +0300
Subject: [PATCH] Referenced optimisation of Generic plans
---
src/backend/commands/explain.c | 25 ++++++++++++++--
src/backend/commands/explain_state.c | 9 ++++++
src/backend/optimizer/util/clauses.c | 6 ++++
src/backend/utils/cache/plancache.c | 43 +++++++++++++++++++++++----
src/backend/utils/misc/guc_tables.c | 2 ++
src/include/commands/explain_state.h | 1 +
src/include/nodes/params.h | 1 +
src/include/nodes/parsenodes.h | 1 +
src/include/utils/plancache.h | 2 ++
src/test/regress/expected/prepare.out | 28 +++++++++++++++++
src/test/regress/sql/prepare.sql | 31 +++++++++++++++++++
11 files changed, 141 insertions(+), 8 deletions(-)
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 8345bc0264b..a83ee584fdd 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -220,8 +220,12 @@ ExplainQuery(ParseState *pstate, ExplainStmt *stmt,
/* Explain every plan */
foreach(l, rewritten)
{
+ int cursor_opts = CURSOR_OPT_PARALLEL_OK;
+ if (es->ref_generic)
+ cursor_opts |= CURSOR_OPT_REF_GENERIC_PLAN;
+
ExplainOneQuery(lfirst_node(Query, l),
- CURSOR_OPT_PARALLEL_OK,
NULL, es,
+ cursor_opts, NULL, es,
pstate, params);
/* Separate plans with an appropriate separator */
@@ -329,6 +333,23 @@ standard_ExplainOneQuery(Query *query, int cursorOptions,
MemoryContext planner_ctx = NULL;
MemoryContext saved_ctx = NULL;
+ ParamListInfo paramLI = params;
+
+ if (es->generic)
+ {
+ if (es->ref_generic && params)
+ {
+ ParamListInfo ref = copyParamList(params);
+ for (int i = 0; ref && i < ref->numParams; i++)
+ ref->params[i].pflags |= PARAM_FLAG_REFVALUE;
+ paramLI = ref;
+ }
+ else
+ {
+ paramLI = NULL;
+ }
+ }
+
if (es->memory)
{
/*
@@ -350,7 +371,7 @@ standard_ExplainOneQuery(Query *query, int cursorOptions,
INSTR_TIME_SET_CURRENT(planstart);
/* plan the query */
- plan = pg_plan_query(query, queryString, cursorOptions, params);
+ plan = pg_plan_query(query, queryString, cursorOptions, paramLI);
INSTR_TIME_SET_CURRENT(planduration);
INSTR_TIME_SUBTRACT(planduration, planstart);
diff --git a/src/backend/commands/explain_state.c
b/src/backend/commands/explain_state.c
index 60d98d63a62..89c3cdd1349 100644
--- a/src/backend/commands/explain_state.c
+++ b/src/backend/commands/explain_state.c
@@ -62,6 +62,7 @@ NewExplainState(void)
{
ExplainState *es = (ExplainState *) palloc0(sizeof(ExplainState));
+ es->ref_generic = false;
/* Set default options (most fields can be left as zeroes). */
es->costs = true;
/* Prepare output buffer. */
@@ -103,6 +104,8 @@ ParseExplainOptionList(ExplainState *es, List *options,
ParseState *pstate)
es->settings = defGetBoolean(opt);
else if (strcmp(opt->defname, "generic_plan") == 0)
es->generic = defGetBoolean(opt);
+ else if (strcmp(opt->defname, "ref_generic_plan") == 0)
+ es->ref_generic = defGetBoolean(opt);
else if (strcmp(opt->defname, "timing") == 0)
{
timing_set = true;
@@ -197,6 +200,12 @@ ParseExplainOptionList(ExplainState *es, List *options,
ParseState *pstate)
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("EXPLAIN options ANALYZE and
GENERIC_PLAN cannot be used together")));
+ /* check that REF_GENERIC_PLAN is not used with EXPLAIN ANALYZE */
+ if (es->ref_generic && es->analyze)
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("EXPLAIN options ANALYZE and
REF_GENERIC_PLAN cannot be used together")));
+
/* if the summary was not set explicitly, set default value */
es->summary = (summary_set) ? es->summary : es->analyze;
diff --git a/src/backend/optimizer/util/clauses.c
b/src/backend/optimizer/util/clauses.c
index 6f0b338d2cd..b7e323823b8 100644
--- a/src/backend/optimizer/util/clauses.c
+++ b/src/backend/optimizer/util/clauses.c
@@ -2488,6 +2488,12 @@ eval_const_expressions_mutator(Node *node,
if (OidIsValid(prm->ptype) &&
prm->ptype == param->paramtype)
{
+ if (!context->estimate &&
+ (prm->pflags &
PARAM_FLAG_REFVALUE))
+ {
+ /* Keep as Param */
+ return (Node *)
copyObject(param);
+ }
/* OK to substitute parameter
value? */
if (context->estimate ||
(prm->pflags &
PARAM_FLAG_CONST))
diff --git a/src/backend/utils/cache/plancache.c
b/src/backend/utils/cache/plancache.c
index 6661d2c6b73..e0e50f33020 100644
--- a/src/backend/utils/cache/plancache.c
+++ b/src/backend/utils/cache/plancache.c
@@ -1186,13 +1186,15 @@ choose_custom_plan(CachedPlanSource *plansource,
ParamListInfo boundParams)
return false;
/* Let settings force the decision */
- if (plan_cache_mode == PLAN_CACHE_MODE_FORCE_GENERIC_PLAN)
+ if (plan_cache_mode == PLAN_CACHE_MODE_FORCE_GENERIC_PLAN ||
+ plan_cache_mode == PLAN_CACHE_MODE_FORCE_REF_GENERIC_PLAN)
return false;
if (plan_cache_mode == PLAN_CACHE_MODE_FORCE_CUSTOM_PLAN)
return true;
/* See if caller wants to force the decision */
- if (plansource->cursor_options & CURSOR_OPT_GENERIC_PLAN)
+ if (plansource->cursor_options & CURSOR_OPT_GENERIC_PLAN ||
+ plansource->cursor_options & CURSOR_OPT_REF_GENERIC_PLAN)
return false;
if (plansource->cursor_options & CURSOR_OPT_CUSTOM_PLAN)
return true;
@@ -1324,7 +1326,21 @@ GetCachedPlan(CachedPlanSource *plansource,
ParamListInfo boundParams,
else
{
/* Build a new generic plan */
- plan = BuildCachedPlan(plansource, qlist, NULL,
queryEnv);
+ if (boundParams &&
+ ((plansource->cursor_options &
CURSOR_OPT_REF_GENERIC_PLAN) ||
+ plan_cache_mode ==
PLAN_CACHE_MODE_FORCE_REF_GENERIC_PLAN ||
+ plan_cache_mode ==
PLAN_CACHE_MODE_REF_AUTO))
+ {
+ ParamListInfo pli = copyParamList(boundParams);
+ if (pli)
+ for (int i = 0; i < pli->numParams; i++)
+ pli->params[i].pflags |=
PARAM_FLAG_REFVALUE;
+ plan = BuildCachedPlan(plansource, qlist, pli,
queryEnv);
+ }
+ else
+ {
+ plan = BuildCachedPlan(plansource, qlist, NULL,
queryEnv);
+ }
/* Just make real sure plansource->gplan is clear */
ReleaseGenericPlan(plansource);
/* Link the new generic plan into the plansource */
@@ -1355,7 +1371,11 @@ GetCachedPlan(CachedPlanSource *plansource,
ParamListInfo boundParams,
* find it's a loser, but we don't want to actually
execute that
* plan.
*/
- customplan = choose_custom_plan(plansource,
boundParams);
+ if (plan_cache_mode ==
PLAN_CACHE_MODE_FORCE_GENERIC_PLAN ||
+ plan_cache_mode ==
PLAN_CACHE_MODE_FORCE_REF_GENERIC_PLAN)
+ customplan = false;
+ else
+ customplan = choose_custom_plan(plansource,
boundParams);
/*
* If we choose to plan again, we need to re-copy the
query_list,
@@ -1368,8 +1388,19 @@ GetCachedPlan(CachedPlanSource *plansource,
ParamListInfo boundParams,
if (customplan)
{
- /* Build a custom plan */
- plan = BuildCachedPlan(plansource, qlist, boundParams,
queryEnv);
+ /* Build a custom plan: inline params as Consts, purge REFVALUE
*/
+ ParamListInfo pli = boundParams;
+ if (pli)
+ {
+ pli = copyParamList(pli);
+ for (int i = 0; i < pli->numParams; i++)
+ {
+ pli->params[i].pflags |= PARAM_FLAG_CONST;
+ pli->params[i].pflags &= ~PARAM_FLAG_REFVALUE;
+ }
+ }
+ plan = BuildCachedPlan(plansource, qlist, pli, queryEnv);
+
/* Accumulate total costs of custom plans */
plansource->total_custom_cost += cached_plan_cost(plan, true);
diff --git a/src/backend/utils/misc/guc_tables.c
b/src/backend/utils/misc/guc_tables.c
index f137129209f..732fa82556f 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -406,7 +406,9 @@ static const struct config_enum_entry
debug_parallel_query_options[] = {
static const struct config_enum_entry plan_cache_mode_options[] = {
{"auto", PLAN_CACHE_MODE_AUTO, false},
+ {"ref_auto", PLAN_CACHE_MODE_REF_AUTO, false},
{"force_generic_plan", PLAN_CACHE_MODE_FORCE_GENERIC_PLAN, false},
+ {"force_ref_generic_plan", PLAN_CACHE_MODE_FORCE_REF_GENERIC_PLAN,
false},
{"force_custom_plan", PLAN_CACHE_MODE_FORCE_CUSTOM_PLAN, false},
{NULL, 0, false}
};
diff --git a/src/include/commands/explain_state.h
b/src/include/commands/explain_state.h
index 32728f5d1a1..59a79d22119 100644
--- a/src/include/commands/explain_state.h
+++ b/src/include/commands/explain_state.h
@@ -62,6 +62,7 @@ typedef struct ExplainState
List *grouping_stack; /* format-specific grouping state */
/* state related to the current plan tree (filled by ExplainPrintPlan)
*/
PlannedStmt *pstmt; /* top of plan */
+ bool ref_generic; /* show reference-based generic
plan */
List *rtable; /* range table */
List *rtable_names; /* alias names for RTEs */
List *deparse_cxt; /* context list for deparsing
expressions */
diff --git a/src/include/nodes/params.h b/src/include/nodes/params.h
index 4321ca6329b..bdb755554fc 100644
--- a/src/include/nodes/params.h
+++ b/src/include/nodes/params.h
@@ -86,6 +86,7 @@ struct ParseState;
*/
#define PARAM_FLAG_CONST 0x0001 /* parameter is constant */
+#define PARAM_FLAG_REFVALUE 0x0002
typedef struct ParamExternData
{
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 86a236bd58b..0ab60446783 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -3391,6 +3391,7 @@ typedef struct SecLabelStmt
#define CURSOR_OPT_GENERIC_PLAN 0x0200 /* force use of generic plan */
#define CURSOR_OPT_CUSTOM_PLAN 0x0400 /* force use of custom plan */
#define CURSOR_OPT_PARALLEL_OK 0x0800 /* parallel mode OK */
+#define CURSOR_OPT_REF_GENERIC_PLAN 0x1000 /* force use of generic plan
(constants are treated as references) */
typedef struct DeclareCursorStmt
{
diff --git a/src/include/utils/plancache.h b/src/include/utils/plancache.h
index 1baa6d50bfd..f0d9faa7a38 100644
--- a/src/include/utils/plancache.h
+++ b/src/include/utils/plancache.h
@@ -31,7 +31,9 @@ struct RawStmt;
typedef enum
{
PLAN_CACHE_MODE_AUTO,
+ PLAN_CACHE_MODE_REF_AUTO,
PLAN_CACHE_MODE_FORCE_GENERIC_PLAN,
+ PLAN_CACHE_MODE_FORCE_REF_GENERIC_PLAN,
PLAN_CACHE_MODE_FORCE_CUSTOM_PLAN,
} PlanCacheMode;
diff --git a/src/test/regress/expected/prepare.out
b/src/test/regress/expected/prepare.out
index 5815e17b39c..a3f8abdc3e8 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -192,3 +192,31 @@ SELECT name, statement, parameter_types FROM
pg_prepared_statements
------+-----------+-----------------
(0 rows)
+CREATE TABLE rg_test (x int, y int);
+INSERT INTO rg_test SELECT g, g FROM generate_series(1,1000) g;
+CREATE INDEX rg_test_x_idx ON rg_test(x);
+VACUUM ANALYZE rg_test;
+SET plan_cache_mode = 'force_ref_generic_plan';
+SET enable_seqscan = off;
+SET enable_indexonlyscan = off;
+PREPARE rgq(int) AS
+ SELECT * FROM rg_test WHERE x < $1;
+EXPLAIN (COSTS OFF, GENERIC_PLAN, REF_GENERIC_PLAN true)
+EXECUTE rgq(10);
+ QUERY PLAN
+-------------------------------------------
+ Index Scan using rg_test_x_idx on rg_test
+ Index Cond: (x < $1)
+(2 rows)
+
+DEALLOCATE rgq;
+PREPARE rgq2(int) AS
+ SELECT * FROM rg_test WHERE x > $1;
+EXPLAIN (ANALYZE, REF_GENERIC_PLAN true)
+EXECUTE rgq2(500);
+ERROR: EXPLAIN options ANALYZE and REF_GENERIC_PLAN cannot be used together
+DEALLOCATE rgq2;
+RESET enable_seqscan;
+RESET enable_indexonlyscan;
+RESET plan_cache_mode;
+DROP TABLE rg_test;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..fd218ecf58b 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -82,3 +82,34 @@ SELECT name, statement, parameter_types, result_types FROM
pg_prepared_statement
DEALLOCATE ALL;
SELECT name, statement, parameter_types FROM pg_prepared_statements
ORDER BY name;
+
+CREATE TABLE rg_test (x int, y int);
+INSERT INTO rg_test SELECT g, g FROM generate_series(1,1000) g;
+CREATE INDEX rg_test_x_idx ON rg_test(x);
+VACUUM ANALYZE rg_test;
+
+SET plan_cache_mode = 'force_ref_generic_plan';
+SET enable_seqscan = off;
+SET enable_indexonlyscan = off;
+
+PREPARE rgq(int) AS
+ SELECT * FROM rg_test WHERE x < $1;
+
+EXPLAIN (COSTS OFF, GENERIC_PLAN, REF_GENERIC_PLAN true)
+EXECUTE rgq(10);
+
+DEALLOCATE rgq;
+
+PREPARE rgq2(int) AS
+ SELECT * FROM rg_test WHERE x > $1;
+
+EXPLAIN (ANALYZE, REF_GENERIC_PLAN true)
+EXECUTE rgq2(500);
+
+DEALLOCATE rgq2;
+
+RESET enable_seqscan;
+RESET enable_indexonlyscan;
+RESET plan_cache_mode;
+DROP TABLE rg_test;
+
--
2.46.0