RFC: extensible planner state
I've been working on planner extensibility for some time now, and am
still not quite ready to make a full-fledged proposal, but see
partially-fledged proposals here and here:
/messages/by-id/CA+TgmoZY+baV-T-5ifDn6P=L=aV-VkVBrPmi0TQkcEq-5Finww@mail.gmail.com
/messages/by-id/CA+TgmoZxQO8svE_vtNCkEubnCYrnrCEnhftdbkdZ496Nfhg=wQ@mail.gmail.com
While trying to build out a real, working example based on those
patches, I ran into the problem that it's rather difficult for
multiple planner hooks to coordinate with each other. For example, you
might want to do some calculation once per query, or once per
RelOptnfo, and that's somewhat difficult to arrange right now. I tried
having my planner hook push an item onto a state stack before calling
standard_planner() and pop it afterward, and then any hooks called
during planning can look at the top of the state stack. But that
doesn't quite work because plan_cluster_use_sort() and
plan_create_index_workers() can provide a backdoor into the planner
code, allowing get_relation_info() to be called not in reference to
the most recent call to planner(). My first instinct was to invent
QSRC_DUMMY and have those functions use that, which as far as I can
see is an adequate solution to that immediate problem, since
get_relation_info() can now identify those cases cleanly.
But that still requires the extension to do a lot of bookkeeping just
for the privilege of storing some per-query private state, and it
seems to me that you might well want to store some private state
per-RelOptInfo or possibly per-PlannerInfo, which seems to require an
even-more-unreasonable amount of effort. An extension might be able to
spin up a hash table keyed by pointer address or maybe some
identifying properties of a RelOptInfo, but I think it's going to be
slow, fragile, and ugly. So what I'd like to propose instead is
something along the lines of the private-ExplainState-data system:
/messages/by-id/CA+TgmoYSzg58hPuBmei46o8D3SKX+SZoO4K_aGQGwiRzvRApLg@mail.gmail.com
https://git.postgresql.org/pg/commitdiff/c65bc2e1d14a2d4daed7c1921ac518f2c5ac3d17
The attached (untested) patch shows how this could work, allowing
extensible state in each PlannerGlobal, PlannerInfo, and RelOptInfo,
which seem like the logical places to me. I have use cases for the
first and the third at present, so the second could be omitted on
suspicion of being unuseful, but I bet it isn't. As compared with
c65bc2e1d14a2d4daed7c1921ac518f2c5ac3d17, I reduced the initial
allocation size to 4 from 16 and made the getter functions static
inline, out of the feeling that you're not likely to have more than
one ExplainState and the speed of EXPLAIN doesn't matter much, but you
might store and access private per-RelOptInfo state a lot of times in
one query planner invocation.
I'm not altogether convinced this is the right design. It seems
slightly unwieldy, and having to allocate an extra array, even if a
small one, for every RelOptInfo that has private state seems like it
could add a noticeable amount of overhead. On the other hand, I
strongly suspect that assuming that there's only ever one planner
extension in operation is short-sighted. The fact that we have none
right now seems to me to be evidence of the absence of infrastructure
rather than the absence of demand. If that is correct then I don't
quite see how to do better than this. But I'm interested in hearing
what other people think. If people like this design, I will propose it
here or on another thread for commit, after suitable testing and
polishing. If people do not like this design, then I would like to
know what alternative they would prefer.
Thanks,
--
Robert Haas
EDB: http://www.enterprisedb.com
Attachments:
v1-0001-Allow-private-state-in-certain-planner-data-struc.patchapplication/octet-stream; name=v1-0001-Allow-private-state-in-certain-planner-data-struc.patchDownload
From 79905f068e4cada3b8b7700c01d0d25bbfbe4e72 Mon Sep 17 00:00:00 2001
From: Robert Haas <rhaas@postgresql.org>
Date: Mon, 18 Aug 2025 16:11:10 -0400
Subject: [PATCH v1] Allow private state in certain planner data structures.
Extension that make extensive use of planner hooks may want to
coordinate their efforts, for example to avoid duplicate computation,
but that's currently difficult because there's no really good way to
pass data between different hooks.
To make that easier, allow for storage of extension-managed private
state in PlannerGlobal, PlannerInfo, and RelOptInfo, along very
similar lines to what we have permitted for ExplainState since commit
c65bc2e1d14a2d4daed7c1921ac518f2c5ac3d17.
---
src/backend/optimizer/util/Makefile | 1 +
src/backend/optimizer/util/extendplan.c | 170 ++++++++++++++++++++++++
src/backend/optimizer/util/meson.build | 1 +
src/include/nodes/pathnodes.h | 12 ++
src/include/optimizer/extendplan.h | 72 ++++++++++
5 files changed, 256 insertions(+)
create mode 100644 src/backend/optimizer/util/extendplan.c
create mode 100644 src/include/optimizer/extendplan.h
diff --git a/src/backend/optimizer/util/Makefile b/src/backend/optimizer/util/Makefile
index 4fb115cb118..308730f392e 100644
--- a/src/backend/optimizer/util/Makefile
+++ b/src/backend/optimizer/util/Makefile
@@ -14,6 +14,7 @@ include $(top_builddir)/src/Makefile.global
OBJS = \
appendinfo.o \
+ extendplan.o \
clauses.o \
inherit.o \
joininfo.o \
diff --git a/src/backend/optimizer/util/extendplan.c b/src/backend/optimizer/util/extendplan.c
new file mode 100644
index 00000000000..c62a5a66868
--- /dev/null
+++ b/src/backend/optimizer/util/extendplan.c
@@ -0,0 +1,170 @@
+/*-------------------------------------------------------------------------
+ *
+ * extendplan.c
+ * Extend core planner objects with additional private state
+ *
+ * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994-5, Regents of the University of California
+ *
+ * The interfaces defined in this file make it possible for loadable
+ * modules to their own private state inside of key planner data
+ * structures -- specifically, the PlannerGlobal, PlannerInfo, and
+ * RelOptInfo structures. This can make it much easier to write
+ * reasonably efficient planner extensions; for instance, code that
+ * uses set_join_pathlist_hook can arrange to compute a key intermediate
+ * result once per joinrel rather than on every call.
+ *
+ * IDENTIFICATION
+ * src/backend/commands/extendplan.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "optimizer/extendplan.h"
+#include "port/pg_bitutils.h"
+#include "utils/memutils.h"
+
+static const char **PlannerExtensionNameArray = NULL;
+static int PlannerExtensionNamesAssigned = 0;
+static int PlannerExtensionNamesAllocated = 0;
+
+/*
+ * Map the name of a planner extension to an integer ID.
+ *
+ * Within the lifetime of a particular backend, the same name will be mapped
+ * to the same ID every time. IDs are not stable across backends. Use the ID
+ * that you get from this function to call the remaining functions in this
+ * file.
+ */
+int
+GetPlannerExtensionId(const char *extension_name)
+{
+ /* Search for an existing extension by this name; if found, return ID. */
+ for (int i = 0; i < PlannerExtensionNamesAssigned; ++i)
+ if (strcmp(PlannerExtensionNameArray[i], extension_name) == 0)
+ return i;
+
+ /* If there is no array yet, create one. */
+ if (PlannerExtensionNameArray == NULL)
+ {
+ PlannerExtensionNamesAllocated = 16;
+ PlannerExtensionNameArray = (const char **)
+ MemoryContextAlloc(TopMemoryContext,
+ PlannerExtensionNamesAllocated
+ * sizeof(char *));
+ }
+
+ /* If there's an array but it's currently full, expand it. */
+ if (PlannerExtensionNamesAssigned >= PlannerExtensionNamesAllocated)
+ {
+ int i = pg_nextpower2_32(PlannerExtensionNamesAssigned + 1);
+
+ PlannerExtensionNameArray = (const char **)
+ repalloc(PlannerExtensionNameArray, i * sizeof(char *));
+ PlannerExtensionNamesAllocated = i;
+ }
+
+ /* Assign and return new ID. */
+ PlannerExtensionNameArray[PlannerExtensionNamesAssigned] = extension_name;
+ return PlannerExtensionNamesAssigned++;
+}
+
+/*
+ * Store extension-specific state into a PlannerGlobal.
+ */
+void
+SetPlannerGlobalExtensionState(PlannerGlobal *glob, int extension_id,
+ void *opaque)
+{
+ Assert(extension_id >= 0);
+
+ /* If there is no array yet, create one. */
+ if (glob->extension_state == NULL)
+ {
+ glob->extension_state_allocated = 4;
+ glob->extension_state =
+ palloc0(glob->extension_state_allocated * sizeof(void *));
+ }
+
+ /* If there's an array but it's currently full, expand it. */
+ if (extension_id >= glob->extension_state_allocated)
+ {
+ int i;
+
+ i = pg_nextpower2_32(glob->extension_state_allocated + 1);
+ glob->extension_state = (void **)
+ repalloc0(glob->extension_state,
+ glob->extension_state_allocated * sizeof(void *),
+ i * sizeof(void *));
+ glob->extension_state_allocated = i;
+ }
+
+ glob->extension_state[extension_id] = opaque;
+}
+
+/*
+ * Store extension-specific state into a PlannerInfo.
+ */
+void
+SetPlannerInfoExtensionState(PlannerInfo *root, int extension_id,
+ void *opaque)
+{
+ Assert(extension_id >= 0);
+
+ /* If there is no array yet, create one. */
+ if (root->extension_state == NULL)
+ {
+ root->extension_state_allocated = 4;
+ root->extension_state =
+ palloc0(root->extension_state_allocated * sizeof(void *));
+ }
+
+ /* If there's an array but it's currently full, expand it. */
+ if (extension_id >= root->extension_state_allocated)
+ {
+ int i;
+
+ i = pg_nextpower2_32(root->extension_state_allocated + 1);
+ root->extension_state = (void **)
+ repalloc0(root->extension_state,
+ root->extension_state_allocated * sizeof(void *),
+ i * sizeof(void *));
+ root->extension_state_allocated = i;
+ }
+
+ root->extension_state[extension_id] = opaque;
+}
+
+/*
+ * Store extension-specific state into a RelOptInfo.
+ */
+void
+SetRelOptInfoExtensionState(RelOptInfo *rel, int extension_id,
+ void *opaque)
+{
+ Assert(extension_id >= 0);
+
+ /* If there is no array yet, create one. */
+ if (rel->extension_state == NULL)
+ {
+ rel->extension_state_allocated = 4;
+ rel->extension_state =
+ palloc0(rel->extension_state_allocated * sizeof(void *));
+ }
+
+ /* If there's an array but it's currently full, expand it. */
+ if (extension_id >= rel->extension_state_allocated)
+ {
+ int i;
+
+ i = pg_nextpower2_32(rel->extension_state_allocated + 1);
+ rel->extension_state = (void **)
+ repalloc0(rel->extension_state,
+ rel->extension_state_allocated * sizeof(void *),
+ i * sizeof(void *));
+ rel->extension_state_allocated = i;
+ }
+
+ rel->extension_state[extension_id] = opaque;
+}
diff --git a/src/backend/optimizer/util/meson.build b/src/backend/optimizer/util/meson.build
index b3bf913d096..f71f56e37a1 100644
--- a/src/backend/optimizer/util/meson.build
+++ b/src/backend/optimizer/util/meson.build
@@ -3,6 +3,7 @@
backend_sources += files(
'appendinfo.c',
'clauses.c',
+ 'extendplan.c',
'inherit.c',
'joininfo.c',
'orclauses.c',
diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h
index ad2726f026f..a32546c8848 100644
--- a/src/include/nodes/pathnodes.h
+++ b/src/include/nodes/pathnodes.h
@@ -182,6 +182,10 @@ typedef struct PlannerGlobal
/* hash table for NOT NULL attnums of relations */
struct HTAB *rel_notnullatts_hash pg_node_attr(read_write_ignore);
+
+ /* extension state */
+ void **extension_state pg_node_attr(read_write_ignore);
+ int extension_state_allocated;
} PlannerGlobal;
/* macro for fetching the Plan associated with a SubPlan node */
@@ -586,6 +590,10 @@ struct PlannerInfo
/* PartitionPruneInfos added in this query's plan. */
List *partPruneInfos;
+
+ /* extension state */
+ void **extension_state pg_node_attr(read_write_ignore);
+ int extension_state_allocated;
};
@@ -1075,6 +1083,10 @@ typedef struct RelOptInfo
List **partexprs pg_node_attr(read_write_ignore);
/* Nullable partition key expressions */
List **nullable_partexprs pg_node_attr(read_write_ignore);
+
+ /* extension state */
+ void **extension_state pg_node_attr(read_write_ignore);
+ int extension_state_allocated;
} RelOptInfo;
/*
diff --git a/src/include/optimizer/extendplan.h b/src/include/optimizer/extendplan.h
new file mode 100644
index 00000000000..a6c89af31ef
--- /dev/null
+++ b/src/include/optimizer/extendplan.h
@@ -0,0 +1,72 @@
+/*-------------------------------------------------------------------------
+ *
+ * extendplan.h
+ * Extend core planner objects with additional private state
+ *
+ *
+ * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/optimizer/extendplan.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef EXTENDPLAN_H
+#define EXTENDPLAN_H
+
+#include "nodes/pathnodes.h"
+
+extern int GetPlannerExtensionId(const char *extension_name);
+
+/*
+ * Get extension-specific state from a PlannerGlobal.
+ */
+static inline void *
+GetPlannerGlobalExtensionState(PlannerGlobal *glob, int extension_id)
+{
+ Assert(extension_id >= 0);
+
+ if (extension_id >= glob->extension_state_allocated)
+ return NULL;
+
+ return glob->extension_state[extension_id];
+}
+
+/*
+ * Get extension-specific state from a PlannerInfo.
+ */
+static inline void *
+GetPlannerInfoExtensionState(PlannerInfo *root, int extension_id)
+{
+ Assert(extension_id >= 0);
+
+ if (extension_id >= root->extension_state_allocated)
+ return NULL;
+
+ return root->extension_state[extension_id];
+}
+
+/*
+ * Get extension-specific state from a PlannerInfo.
+ */
+static inline void *
+GetRelOptInfoExtensionState(RelOptInfo *rel, int extension_id)
+{
+ Assert(extension_id >= 0);
+
+ if (extension_id >= rel->extension_state_allocated)
+ return NULL;
+
+ return rel->extension_state[extension_id];
+}
+
+/* Functions to store private state into various planner objects */
+extern void SetPlannerGlobalExtensionState(PlannerGlobal *glob,
+ int extension_id,
+ void *opaque);
+extern void SetPlannerInfoExtensionState(PlannerInfo *root, int extension_id,
+ void *opaque);
+extern void SetRelOptInfoExtensionState(RelOptInfo *root, int extension_id,
+ void *opaque);
+
+#endif
--
2.39.5 (Apple Git-154)
Robert Haas <robertmhaas@gmail.com> writes:
But that still requires the extension to do a lot of bookkeeping just
for the privilege of storing some per-query private state, and it
seems to me that you might well want to store some private state
per-RelOptInfo or possibly per-PlannerInfo, which seems to require an
even-more-unreasonable amount of effort. An extension might be able to
spin up a hash table keyed by pointer address or maybe some
identifying properties of a RelOptInfo, but I think it's going to be
slow, fragile, and ugly. So what I'd like to propose instead is
something along the lines of the private-ExplainState-data system:
/messages/by-id/CA+TgmoYSzg58hPuBmei46o8D3SKX+SZoO4K_aGQGwiRzvRApLg@mail.gmail.com
https://git.postgresql.org/pg/commitdiff/c65bc2e1d14a2d4daed7c1921ac518f2c5ac3d17
This seems generally reasonable to me. I agree that it's slightly
annoying to bloat every RelOptInfo with two more fields, but I don't
see a better alternative to that. In any case, sizeof(RelOptInfo)
is 448 right now on my dev machine; making it 464 isn't going to
change anything.
I wonder if we couldn't get rid of PlannerInfo.join_search_private
in favor of expecting join search hooks to use this mechanism
(thus, GEQO would become an in-core consumer of the mechanism).
Another idea is to get rid of RelOptInfo.fdw_private, although
that has a little more excuse to live in that it's reasonably
clear who gets to use it, namely the FDW supporting the relation.
(Too bad there's no comment explaining that.)
Nitpicks:
* The initial allocations of the arrays need to take
more care than this about which context the arrays go into,
ie it had better be planner_cxt for PlannerInfo or PlannerGlobal,
and the same context the RelOptInfo is in for RelOptInfo.
Otherwise you risk a mess under GEQO.
* Surely, if extension_state etc is read_write_ignore, then
extension_state_allocated etc had better be as well? I don't
understand the rationale for preserving one without the other.
regards, tom lane
On Tue, Aug 19, 2025 at 1:18 PM Tom Lane <tgl@sss.pgh.pa.us> wrote:
This seems generally reasonable to me.
Cool.
I wonder if we couldn't get rid of PlannerInfo.join_search_private
in favor of expecting join search hooks to use this mechanism
(thus, GEQO would become an in-core consumer of the mechanism).
Let me try that.
* The initial allocations of the arrays need to take
more care than this about which context the arrays go into,
ie it had better be planner_cxt for PlannerInfo or PlannerGlobal,
and the same context the RelOptInfo is in for RelOptInfo.
Otherwise you risk a mess under GEQO.
It's easy to do this for PlannerInfo, but PlannerGlobal has no
planner_cxt member. GetMemoryChunkContext() could be used but I'm not
sure we want to spread reliance on that to more places. What's your
thought?
* Surely, if extension_state etc is read_write_ignore, then
extension_state_allocated etc had better be as well? I don't
understand the rationale for preserving one without the other.
I figured we can't print a void** but we can print an integer and the
user might want to see it. Wrong idea?
--
Robert Haas
EDB: http://www.enterprisedb.com
Robert Haas <robertmhaas@gmail.com> writes:
On Tue, Aug 19, 2025 at 1:18 PM Tom Lane <tgl@sss.pgh.pa.us> wrote:
* The initial allocations of the arrays need to take
more care than this about which context the arrays go into,
ie it had better be planner_cxt for PlannerInfo or PlannerGlobal,
and the same context the RelOptInfo is in for RelOptInfo.
Otherwise you risk a mess under GEQO.
It's easy to do this for PlannerInfo, but PlannerGlobal has no
planner_cxt member. GetMemoryChunkContext() could be used but I'm not
sure we want to spread reliance on that to more places. What's your
thought?
You'll presumably have to use GetMemoryChunkContext() for RelOptInfo,
so I don't see much downside from using it in one or even both of the
other cases too.
* Surely, if extension_state etc is read_write_ignore, then
extension_state_allocated etc had better be as well? I don't
understand the rationale for preserving one without the other.
I figured we can't print a void** but we can print an integer and the
user might want to see it. Wrong idea?
Hm. We don't have read support for these structs, so maybe it's fine.
It looks weird though.
regards, tom lane
On 19/8/2025 18:47, Robert Haas wrote:
polishing. If people do not like this design, then I would like to
know what alternative they would prefer.Thanks for these efforts!
Generally, such an interface seems good for the extension's purposes. It
is OK in this specific context because all these structures are created
during the planning process. Going further into the plan, which is more
stable and reusable, you would need to think about read/write rules.
I utilise PlannerGlobal extensibility to notify my extension when a
transformation or optimisation occurs, enabling it to initiate
replanning from the top level with alternative settings if necessary.
RelOptInfo extensibility serves multiple purposes, but its most notable
feature is the inclusion of a node signature that enables the
identification of a specific RelOptInfo instance during re-optimisation
of all or only a part of the query. I haven't used PlannerInfo
extensibility yet, but I think it makes sense - if an extension performs
a complicated planning job that spans multiple planning stages, it makes
sense to store intermediate data in this 'cache'.
The weak points of this approach are:
1. Needs a new core routine for each node to be extended.
2. Doesn't propose copy/read/write node machinery.
3. Allocates more memory than needed. I frequently see installations
with 5-10 modules installed. If the 9th extension employs the RelOptInfo
extensibility, it would be unfortunate to see another eight elements
allocated unnecessarily. What if we ever consider extending the Path node?
I have been using a slightly different approach [1]https://commitfest.postgresql.org/patch/5965/ for years, which
involves adding a List at the end of each structure. Any extension, by
convention, may add elements as an ExtensibleNode. Such an approach
saves memory, resolves read/write/copy node issues and allows an
extension to correctly identify its data in parallel workers and across
backends (see [2]/messages/by-id/aKQIeXKMifXqV58R@jrouhaud for the reasoning). This approach appears more general
(though less restrictive) and can be applied to extend any node in the
same way, which offers a clear benefit, because tracking query planning
decisions often requires extensibility in Query, RelOptInfo, and
PlannedStmt as well.
Although I prefer the ExtensibleNode / extension list approach, I will
be OK with your method as well, especially if you add extensibility to
the PlannedStmt node too.
[1]: https://commitfest.postgresql.org/patch/5965/
[2]: /messages/by-id/aKQIeXKMifXqV58R@jrouhaud
--
regards, Andrei Lepikhov
On Tue, Aug 19, 2025 at 2:28 PM Tom Lane <tgl@sss.pgh.pa.us> wrote:
You'll presumably have to use GetMemoryChunkContext() for RelOptInfo,
so I don't see much downside from using it in one or even both of the
other cases too.
Pointer dereference must be faster than a function call.
Hm. We don't have read support for these structs, so maybe it's fine.
It looks weird though.
Left this one as-is for now.
Here's v2. 0001 is what you saw before with an attempt to fix the
memory context handling. 0002 removes join_search_private. All I've
tested is that the tests pass.
--
Robert Haas
EDB: http://www.enterprisedb.com
Attachments:
v2-0002-Remove-PlannerInfo-s-join_search_private-method.patchapplication/octet-stream; name=v2-0002-Remove-PlannerInfo-s-join_search_private-method.patchDownload
From ee075b61cc988b165d8b697d4df287b462b9bf38 Mon Sep 17 00:00:00 2001
From: Robert Haas <rhaas@postgresql.org>
Date: Wed, 20 Aug 2025 15:10:52 -0400
Subject: [PATCH v2 2/2] Remove PlannerInfo's join_search_private method.
Instead, use the new mechanism that allows planner extensions to store
private state inside a PlannerInfo, treating GEQO as an in-core planner
extension. This is a useful test of the new facility, and also buys
back a few bytes of storage.
To make this work, we must remove innerrel_is_unique_ext's hack of
testing whether join_search_private is set as a proxy for whether
the join search might be retried. Add a flag that extensions can
use to explicitly signal their intentions instead.
---
src/backend/optimizer/geqo/geqo_eval.c | 2 +-
src/backend/optimizer/geqo/geqo_main.c | 12 ++++++++++--
src/backend/optimizer/geqo/geqo_random.c | 7 +++----
src/backend/optimizer/plan/analyzejoins.c | 9 +++------
src/backend/optimizer/plan/planner.c | 1 +
src/backend/optimizer/prep/prepjointree.c | 1 +
src/include/nodes/pathnodes.h | 5 ++---
src/include/optimizer/geqo.h | 10 +++++++++-
8 files changed, 30 insertions(+), 17 deletions(-)
diff --git a/src/backend/optimizer/geqo/geqo_eval.c b/src/backend/optimizer/geqo/geqo_eval.c
index f07d1dc8ac6..7fcb1aa70d1 100644
--- a/src/backend/optimizer/geqo/geqo_eval.c
+++ b/src/backend/optimizer/geqo/geqo_eval.c
@@ -162,7 +162,7 @@ geqo_eval(PlannerInfo *root, Gene *tour, int num_gene)
RelOptInfo *
gimme_tree(PlannerInfo *root, Gene *tour, int num_gene)
{
- GeqoPrivateData *private = (GeqoPrivateData *) root->join_search_private;
+ GeqoPrivateData *private = GetGeqoPrivateData(root);
List *clumps;
int rel_count;
diff --git a/src/backend/optimizer/geqo/geqo_main.c b/src/backend/optimizer/geqo/geqo_main.c
index 38402ce58db..0064556087a 100644
--- a/src/backend/optimizer/geqo/geqo_main.c
+++ b/src/backend/optimizer/geqo/geqo_main.c
@@ -47,6 +47,8 @@ int Geqo_generations;
double Geqo_selection_bias;
double Geqo_seed;
+/* GEQO is treated as an in-core planner extension */
+int Geqo_planner_extension_id = -1;
static int gimme_pool_size(int nr_rel);
static int gimme_number_generations(int pool_size);
@@ -98,10 +100,16 @@ geqo(PlannerInfo *root, int number_of_rels, List *initial_rels)
int mutations = 0;
#endif
+ if (Geqo_planner_extension_id < 0)
+ Geqo_planner_extension_id = GetPlannerExtensionId("geqo");
+
/* set up private information */
- root->join_search_private = &private;
+ SetPlannerInfoExtensionState(root, Geqo_planner_extension_id, &private);
private.initial_rels = initial_rels;
+/* inform core planner that we may replan */
+ root->assumeReplanning = true;
+
/* initialize private number generator */
geqo_set_seed(root, Geqo_seed);
@@ -304,7 +312,7 @@ geqo(PlannerInfo *root, int number_of_rels, List *initial_rels)
free_pool(root, pool);
/* ... clear root pointer to our private storage */
- root->join_search_private = NULL;
+ SetPlannerInfoExtensionState(root, Geqo_planner_extension_id, NULL);
return best_rel;
}
diff --git a/src/backend/optimizer/geqo/geqo_random.c b/src/backend/optimizer/geqo/geqo_random.c
index 6c7a411f69f..46d28baa2e6 100644
--- a/src/backend/optimizer/geqo/geqo_random.c
+++ b/src/backend/optimizer/geqo/geqo_random.c
@@ -15,11 +15,10 @@
#include "optimizer/geqo_random.h"
-
void
geqo_set_seed(PlannerInfo *root, double seed)
{
- GeqoPrivateData *private = (GeqoPrivateData *) root->join_search_private;
+ GeqoPrivateData *private = GetGeqoPrivateData(root);
pg_prng_fseed(&private->random_state, seed);
}
@@ -27,7 +26,7 @@ geqo_set_seed(PlannerInfo *root, double seed)
double
geqo_rand(PlannerInfo *root)
{
- GeqoPrivateData *private = (GeqoPrivateData *) root->join_search_private;
+ GeqoPrivateData *private = GetGeqoPrivateData(root);
return pg_prng_double(&private->random_state);
}
@@ -35,7 +34,7 @@ geqo_rand(PlannerInfo *root)
int
geqo_randint(PlannerInfo *root, int upper, int lower)
{
- GeqoPrivateData *private = (GeqoPrivateData *) root->join_search_private;
+ GeqoPrivateData *private = GetGeqoPrivateData(root);
/*
* In current usage, "lower" is never negative so we can just use
diff --git a/src/backend/optimizer/plan/analyzejoins.c b/src/backend/optimizer/plan/analyzejoins.c
index 4d55c2ea591..82b3ab2e9f4 100644
--- a/src/backend/optimizer/plan/analyzejoins.c
+++ b/src/backend/optimizer/plan/analyzejoins.c
@@ -1425,17 +1425,14 @@ innerrel_is_unique_ext(PlannerInfo *root,
*
* However, in normal planning mode, caching this knowledge is totally
* pointless; it won't be queried again, because we build up joinrels
- * from smaller to larger. It is useful in GEQO mode, where the
- * knowledge can be carried across successive planning attempts; and
- * it's likely to be useful when using join-search plugins, too. Hence
- * cache when join_search_private is non-NULL. (Yeah, that's a hack,
- * but it seems reasonable.)
+ * from smaller to larger. It's only useful when using GEQO or
+ * another planner extension that attempts planning multiple times.
*
* Also, allow callers to override that heuristic and force caching;
* that's useful for reduce_unique_semijoins, which calls here before
* the normal join search starts.
*/
- if (force_cache || root->join_search_private)
+ if (force_cache || root->assumeReplanning)
{
old_context = MemoryContextSwitchTo(root->planner_cxt);
innerrel->non_unique_for_rels =
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index 0d5a692e5fd..926256da5f4 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -697,6 +697,7 @@ subquery_planner(PlannerGlobal *glob, Query *parse, PlannerInfo *parent_root,
root->hasAlternativeSubPlans = false;
root->placeholdersFrozen = false;
root->hasRecursion = hasRecursion;
+ root->assumeReplanning = false;
if (hasRecursion)
root->wt_param_id = assign_special_exec_param(root);
else
diff --git a/src/backend/optimizer/prep/prepjointree.c b/src/backend/optimizer/prep/prepjointree.c
index 35e8d3c183b..4075f7519ca 100644
--- a/src/backend/optimizer/prep/prepjointree.c
+++ b/src/backend/optimizer/prep/prepjointree.c
@@ -1383,6 +1383,7 @@ pull_up_simple_subquery(PlannerInfo *root, Node *jtnode, RangeTblEntry *rte,
subroot->qual_security_level = 0;
subroot->placeholdersFrozen = false;
subroot->hasRecursion = false;
+ subroot->assumeReplanning = false;
subroot->wt_param_id = -1;
subroot->non_recursive_path = NULL;
/* We don't currently need a top JoinDomain for the subroot */
diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h
index a32546c8848..8c1f0183413 100644
--- a/src/include/nodes/pathnodes.h
+++ b/src/include/nodes/pathnodes.h
@@ -536,6 +536,8 @@ struct PlannerInfo
bool placeholdersFrozen;
/* true if planning a recursive WITH item */
bool hasRecursion;
+ /* true if a planner extension may replan this subquery */
+ bool assumeReplanning;
/*
* The rangetable index for the RTE_GROUP RTE, or 0 if there is no
@@ -582,9 +584,6 @@ struct PlannerInfo
bool *isAltSubplan pg_node_attr(read_write_ignore);
bool *isUsedSubplan pg_node_attr(read_write_ignore);
- /* optional private data for join_search_hook, e.g., GEQO */
- void *join_search_private pg_node_attr(read_write_ignore);
-
/* Does this query modify any partition key columns? */
bool partColsUpdated;
diff --git a/src/include/optimizer/geqo.h b/src/include/optimizer/geqo.h
index 9f8e0f337aa..3f4872e25e3 100644
--- a/src/include/optimizer/geqo.h
+++ b/src/include/optimizer/geqo.h
@@ -24,6 +24,7 @@
#include "common/pg_prng.h"
#include "nodes/pathnodes.h"
+#include "optimizer/extendplan.h"
#include "optimizer/geqo_gene.h"
@@ -62,6 +63,8 @@ extern PGDLLIMPORT int Geqo_generations; /* 1 .. inf, or 0 to use default */
extern PGDLLIMPORT double Geqo_selection_bias;
+extern PGDLLIMPORT int Geqo_planner_extension_id;
+
#define DEFAULT_GEQO_SELECTION_BIAS 2.0
#define MIN_GEQO_SELECTION_BIAS 1.5
#define MAX_GEQO_SELECTION_BIAS 2.0
@@ -70,7 +73,7 @@ extern PGDLLIMPORT double Geqo_seed; /* 0 .. 1 */
/*
- * Private state for a GEQO run --- accessible via root->join_search_private
+ * Private state for a GEQO run --- accessible via GetGeqoPrivateData
*/
typedef struct
{
@@ -78,6 +81,11 @@ typedef struct
pg_prng_state random_state; /* PRNG state */
} GeqoPrivateData;
+static inline GeqoPrivateData *
+GetGeqoPrivateData(PlannerInfo *root)
+{
+ return GetPlannerInfoExtensionState(root, Geqo_planner_extension_id);
+}
/* routines in geqo_main.c */
extern RelOptInfo *geqo(PlannerInfo *root,
--
2.39.5 (Apple Git-154)
v2-0001-Allow-private-state-in-certain-planner-data-struc.patchapplication/octet-stream; name=v2-0001-Allow-private-state-in-certain-planner-data-struc.patchDownload
From d9bedbfb654d632c5c1b7c85c8bdca08f347f90b Mon Sep 17 00:00:00 2001
From: Robert Haas <rhaas@postgresql.org>
Date: Mon, 18 Aug 2025 16:11:10 -0400
Subject: [PATCH v2 1/2] Allow private state in certain planner data
structures.
Extension that make extensive use of planner hooks may want to
coordinate their efforts, for example to avoid duplicate computation,
but that's currently difficult because there's no really good way to
pass data between different hooks.
To make that easier, allow for storage of extension-managed private
state in PlannerGlobal, PlannerInfo, and RelOptInfo, along very
similar lines to what we have permitted for ExplainState since commit
c65bc2e1d14a2d4daed7c1921ac518f2c5ac3d17.
---
src/backend/optimizer/util/Makefile | 1 +
src/backend/optimizer/util/extendplan.c | 181 ++++++++++++++++++++++++
src/backend/optimizer/util/meson.build | 1 +
src/include/nodes/pathnodes.h | 12 ++
src/include/optimizer/extendplan.h | 72 ++++++++++
5 files changed, 267 insertions(+)
create mode 100644 src/backend/optimizer/util/extendplan.c
create mode 100644 src/include/optimizer/extendplan.h
diff --git a/src/backend/optimizer/util/Makefile b/src/backend/optimizer/util/Makefile
index 4fb115cb118..308730f392e 100644
--- a/src/backend/optimizer/util/Makefile
+++ b/src/backend/optimizer/util/Makefile
@@ -14,6 +14,7 @@ include $(top_builddir)/src/Makefile.global
OBJS = \
appendinfo.o \
+ extendplan.o \
clauses.o \
inherit.o \
joininfo.o \
diff --git a/src/backend/optimizer/util/extendplan.c b/src/backend/optimizer/util/extendplan.c
new file mode 100644
index 00000000000..26b324d2ee6
--- /dev/null
+++ b/src/backend/optimizer/util/extendplan.c
@@ -0,0 +1,181 @@
+/*-------------------------------------------------------------------------
+ *
+ * extendplan.c
+ * Extend core planner objects with additional private state
+ *
+ * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994-5, Regents of the University of California
+ *
+ * The interfaces defined in this file make it possible for loadable
+ * modules to their own private state inside of key planner data
+ * structures -- specifically, the PlannerGlobal, PlannerInfo, and
+ * RelOptInfo structures. This can make it much easier to write
+ * reasonably efficient planner extensions; for instance, code that
+ * uses set_join_pathlist_hook can arrange to compute a key intermediate
+ * result once per joinrel rather than on every call.
+ *
+ * IDENTIFICATION
+ * src/backend/commands/extendplan.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "optimizer/extendplan.h"
+#include "port/pg_bitutils.h"
+#include "utils/memutils.h"
+#include "utils/palloc.h"
+
+static const char **PlannerExtensionNameArray = NULL;
+static int PlannerExtensionNamesAssigned = 0;
+static int PlannerExtensionNamesAllocated = 0;
+
+/*
+ * Map the name of a planner extension to an integer ID.
+ *
+ * Within the lifetime of a particular backend, the same name will be mapped
+ * to the same ID every time. IDs are not stable across backends. Use the ID
+ * that you get from this function to call the remaining functions in this
+ * file.
+ */
+int
+GetPlannerExtensionId(const char *extension_name)
+{
+ /* Search for an existing extension by this name; if found, return ID. */
+ for (int i = 0; i < PlannerExtensionNamesAssigned; ++i)
+ if (strcmp(PlannerExtensionNameArray[i], extension_name) == 0)
+ return i;
+
+ /* If there is no array yet, create one. */
+ if (PlannerExtensionNameArray == NULL)
+ {
+ PlannerExtensionNamesAllocated = 16;
+ PlannerExtensionNameArray = (const char **)
+ MemoryContextAlloc(TopMemoryContext,
+ PlannerExtensionNamesAllocated
+ * sizeof(char *));
+ }
+
+ /* If there's an array but it's currently full, expand it. */
+ if (PlannerExtensionNamesAssigned >= PlannerExtensionNamesAllocated)
+ {
+ int i = pg_nextpower2_32(PlannerExtensionNamesAssigned + 1);
+
+ PlannerExtensionNameArray = (const char **)
+ repalloc(PlannerExtensionNameArray, i * sizeof(char *));
+ PlannerExtensionNamesAllocated = i;
+ }
+
+ /* Assign and return new ID. */
+ PlannerExtensionNameArray[PlannerExtensionNamesAssigned] = extension_name;
+ return PlannerExtensionNamesAssigned++;
+}
+
+/*
+ * Store extension-specific state into a PlannerGlobal.
+ */
+void
+SetPlannerGlobalExtensionState(PlannerGlobal *glob, int extension_id,
+ void *opaque)
+{
+ Assert(extension_id >= 0);
+
+ /* If there is no array yet, create one. */
+ if (glob->extension_state == NULL)
+ {
+ MemoryContext planner_cxt;
+ Size sz;
+
+ planner_cxt = GetMemoryChunkContext(glob);
+ glob->extension_state_allocated = 4;
+ sz = glob->extension_state_allocated * sizeof(void *);
+ glob->extension_state = MemoryContextAllocZero(planner_cxt, sz);
+ }
+
+ /* If there's an array but it's currently full, expand it. */
+ if (extension_id >= glob->extension_state_allocated)
+ {
+ int i;
+
+ i = pg_nextpower2_32(glob->extension_state_allocated + 1);
+ glob->extension_state = (void **)
+ repalloc0(glob->extension_state,
+ glob->extension_state_allocated * sizeof(void *),
+ i * sizeof(void *));
+ glob->extension_state_allocated = i;
+ }
+
+ glob->extension_state[extension_id] = opaque;
+}
+
+/*
+ * Store extension-specific state into a PlannerInfo.
+ */
+void
+SetPlannerInfoExtensionState(PlannerInfo *root, int extension_id,
+ void *opaque)
+{
+ Assert(extension_id >= 0);
+
+ /* If there is no array yet, create one. */
+ if (root->extension_state == NULL)
+ {
+ Size sz;
+
+ root->extension_state_allocated = 4;
+ sz = root->extension_state_allocated * sizeof(void *);
+ root->extension_state = MemoryContextAllocZero(root->planner_cxt, sz);
+ }
+
+ /* If there's an array but it's currently full, expand it. */
+ if (extension_id >= root->extension_state_allocated)
+ {
+ int i;
+
+ i = pg_nextpower2_32(root->extension_state_allocated + 1);
+ root->extension_state = (void **)
+ repalloc0(root->extension_state,
+ root->extension_state_allocated * sizeof(void *),
+ i * sizeof(void *));
+ root->extension_state_allocated = i;
+ }
+
+ root->extension_state[extension_id] = opaque;
+}
+
+/*
+ * Store extension-specific state into a RelOptInfo.
+ */
+void
+SetRelOptInfoExtensionState(RelOptInfo *rel, int extension_id,
+ void *opaque)
+{
+ Assert(extension_id >= 0);
+
+ /* If there is no array yet, create one. */
+ if (rel->extension_state == NULL)
+ {
+ MemoryContext planner_cxt;
+ Size sz;
+
+ planner_cxt = GetMemoryChunkContext(rel);
+ rel->extension_state_allocated = 4;
+ sz = rel->extension_state_allocated * sizeof(void *);
+ rel->extension_state = MemoryContextAllocZero(planner_cxt, sz);
+ }
+
+ /* If there's an array but it's currently full, expand it. */
+ if (extension_id >= rel->extension_state_allocated)
+ {
+ int i;
+
+ i = pg_nextpower2_32(rel->extension_state_allocated + 1);
+ rel->extension_state = (void **)
+ repalloc0(rel->extension_state,
+ rel->extension_state_allocated * sizeof(void *),
+ i * sizeof(void *));
+ rel->extension_state_allocated = i;
+ }
+
+ rel->extension_state[extension_id] = opaque;
+}
diff --git a/src/backend/optimizer/util/meson.build b/src/backend/optimizer/util/meson.build
index b3bf913d096..f71f56e37a1 100644
--- a/src/backend/optimizer/util/meson.build
+++ b/src/backend/optimizer/util/meson.build
@@ -3,6 +3,7 @@
backend_sources += files(
'appendinfo.c',
'clauses.c',
+ 'extendplan.c',
'inherit.c',
'joininfo.c',
'orclauses.c',
diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h
index ad2726f026f..a32546c8848 100644
--- a/src/include/nodes/pathnodes.h
+++ b/src/include/nodes/pathnodes.h
@@ -182,6 +182,10 @@ typedef struct PlannerGlobal
/* hash table for NOT NULL attnums of relations */
struct HTAB *rel_notnullatts_hash pg_node_attr(read_write_ignore);
+
+ /* extension state */
+ void **extension_state pg_node_attr(read_write_ignore);
+ int extension_state_allocated;
} PlannerGlobal;
/* macro for fetching the Plan associated with a SubPlan node */
@@ -586,6 +590,10 @@ struct PlannerInfo
/* PartitionPruneInfos added in this query's plan. */
List *partPruneInfos;
+
+ /* extension state */
+ void **extension_state pg_node_attr(read_write_ignore);
+ int extension_state_allocated;
};
@@ -1075,6 +1083,10 @@ typedef struct RelOptInfo
List **partexprs pg_node_attr(read_write_ignore);
/* Nullable partition key expressions */
List **nullable_partexprs pg_node_attr(read_write_ignore);
+
+ /* extension state */
+ void **extension_state pg_node_attr(read_write_ignore);
+ int extension_state_allocated;
} RelOptInfo;
/*
diff --git a/src/include/optimizer/extendplan.h b/src/include/optimizer/extendplan.h
new file mode 100644
index 00000000000..a6c89af31ef
--- /dev/null
+++ b/src/include/optimizer/extendplan.h
@@ -0,0 +1,72 @@
+/*-------------------------------------------------------------------------
+ *
+ * extendplan.h
+ * Extend core planner objects with additional private state
+ *
+ *
+ * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/optimizer/extendplan.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef EXTENDPLAN_H
+#define EXTENDPLAN_H
+
+#include "nodes/pathnodes.h"
+
+extern int GetPlannerExtensionId(const char *extension_name);
+
+/*
+ * Get extension-specific state from a PlannerGlobal.
+ */
+static inline void *
+GetPlannerGlobalExtensionState(PlannerGlobal *glob, int extension_id)
+{
+ Assert(extension_id >= 0);
+
+ if (extension_id >= glob->extension_state_allocated)
+ return NULL;
+
+ return glob->extension_state[extension_id];
+}
+
+/*
+ * Get extension-specific state from a PlannerInfo.
+ */
+static inline void *
+GetPlannerInfoExtensionState(PlannerInfo *root, int extension_id)
+{
+ Assert(extension_id >= 0);
+
+ if (extension_id >= root->extension_state_allocated)
+ return NULL;
+
+ return root->extension_state[extension_id];
+}
+
+/*
+ * Get extension-specific state from a PlannerInfo.
+ */
+static inline void *
+GetRelOptInfoExtensionState(RelOptInfo *rel, int extension_id)
+{
+ Assert(extension_id >= 0);
+
+ if (extension_id >= rel->extension_state_allocated)
+ return NULL;
+
+ return rel->extension_state[extension_id];
+}
+
+/* Functions to store private state into various planner objects */
+extern void SetPlannerGlobalExtensionState(PlannerGlobal *glob,
+ int extension_id,
+ void *opaque);
+extern void SetPlannerInfoExtensionState(PlannerInfo *root, int extension_id,
+ void *opaque);
+extern void SetRelOptInfoExtensionState(RelOptInfo *root, int extension_id,
+ void *opaque);
+
+#endif
--
2.39.5 (Apple Git-154)
On Wed, Aug 20, 2025 at 3:13 PM Robert Haas <robertmhaas@gmail.com> wrote:
Here's v2. 0001 is what you saw before with an attempt to fix the
memory context handling. 0002 removes join_search_private. All I've
tested is that the tests pass.
Here's v3 with a few more patches. I'm now fairly confident I have the
basic approach correct here, but let's see what others think.
0001 is the core "private state" patch for PlannerGlobal, PlannerInfo,
and RelOptInfo. It is unchanged since v2, and contains only the fix
for memory context handling since v1. However, I've now tested it, and
I think it's OK to commit, barring further review comments.
0002 removes join_search_private, as before. Whether it makes sense to
go ahead with this is debatable. Needs review, and needs an opinion on
whether this should be considered a PoC only (and discarded) or
something that should go forward to commit.
0003 adds two new planner hooks. In experimenting with 0001, I
discovered that it was a little hard to use. PlannerGlobal has to do
with what happens in a whole planning cycle, but the only hook we have
that's in approximately the right place is planner_hook, and it can't
see the PlannerGlobal object. So, I added these hooks. The first fires
after PlannerGlobal is fully initialized and before we start using it,
and the second fires just before we throw PlannerGlobal away. I
considered some other approaches, specifically: (1) making
subquery_planner a hook, (2) making grouping_planner a hook, and (3)
doing as the patch does but with the call before rather than after
assembling the PlannedStmt. Those proved inferior; the hook at the
very end of planner() just before we discard the PlannerGlobal object
appears quite valuable to me. Needs review.
0004 adds an extension_state member to PlannedStmt. Unlike the stuff
added by 0001, whatever goes into a PlannedStmt has to be a node tree.
The proposed usage convention is noted in the comment. There was
previous discussion of this kind of thing in
/messages/by-id/CA+TgmobrkCquFovDMZKRZ9cYQHnrS9sPE98aK0g2A=N1HFk3yQ@mail.gmail.com
and the messages leading up to it and I now believe this is exactly
the right way to enable what we were talking about over there: give a
plugin a chance to propagate whatever it likes from the PlannerGlobal
(including extension state) into the PlannedStmt, and then you can use
EXPLAIN hooks to print that stuff out -- or use it from anywhere that
has access to the PlannedStmt. Again, needs review.
0005 is a demo, not for commit, just to show how these pieces fit
together. It uses the hooks from 0001 to count the number of times
set_join_pathlist_hook is called and the number of those that are for
distinct joinrels. Then it uses planner_shutdown_hook to propagate
that into the PlannedStmt, and makes EXPLAIN (DEBUG) print those
values out. I think there are far more interesting bits of information
that could be preserved and propagated using this infrastructure,
though some of them probably also require other changes to make it all
work. But this is a simple example to show that the concept is valid
even without anything else.
For another example of how these patches could be used, see
/messages/by-id/CA+TgmoZ=6jJi9TGyZCm33vads46HFkyz6Aju_saLT6GFS-iFug@mail.gmail.com
and in particular 0001 and 0002. This patch set's planner_setup_hook
call would go write after those patches compute default_ssa_mask and
default_jsa_mask, allowing the hook to override those values. That's
not necessarily the very most interesting thing in the whole world,
because the real power of those patches is about manipulating ssa_mask
at the per-rel level and jsa_mask at the
per-call-to-add_paths_to_joinrel level; setting them for an entire
query isn't much better than we ca already do now by frobbing GUCs.
But it is a little better, because it allows automatically adjusting
the masks on a per-planner-invocation basis without regard to the
prevailing GUC values, so you could e.g. decide that whenever the
query ID has value X, we automatically set jsa_mask or ssa_mask to
value Y. Perhaps more interestingly, I think that planner_setup_hook
will prove to be the right place to set up a data structure at the
PlannerGlobal level that can be accessed by calls to
get_relation_info_hook and others to decide how to these masks should
be configured for each RelOptInfo.
I guess my point here is that I know this patch set (and the others
I've posted) seem a little thin in isolation, but the value starts to
compound when you think about them together. That's not to say that
I've got everything figured out here, only that I'd request that
nobody be too quick to dismiss any of these changes because they don't
do enough. The planner is extremely low on extension-author-friendly
infrastructure, and no single patch can or should try to solve that
problem completely.
Thanks,
--
Robert Haas
EDB: http://www.enterprisedb.com
Attachments:
v3-0001-Allow-private-state-in-certain-planner-data-struc.patchapplication/octet-stream; name=v3-0001-Allow-private-state-in-certain-planner-data-struc.patchDownload
From 1aa43c063edb325548fa3db30b9991bf0831f6f5 Mon Sep 17 00:00:00 2001
From: Robert Haas <rhaas@postgresql.org>
Date: Mon, 18 Aug 2025 16:11:10 -0400
Subject: [PATCH v3 1/5] Allow private state in certain planner data
structures.
Extension that make extensive use of planner hooks may want to
coordinate their efforts, for example to avoid duplicate computation,
but that's currently difficult because there's no really good way to
pass data between different hooks.
To make that easier, allow for storage of extension-managed private
state in PlannerGlobal, PlannerInfo, and RelOptInfo, along very
similar lines to what we have permitted for ExplainState since commit
c65bc2e1d14a2d4daed7c1921ac518f2c5ac3d17.
---
src/backend/optimizer/util/Makefile | 1 +
src/backend/optimizer/util/extendplan.c | 181 ++++++++++++++++++++++++
src/backend/optimizer/util/meson.build | 1 +
src/include/nodes/pathnodes.h | 12 ++
src/include/optimizer/extendplan.h | 72 ++++++++++
5 files changed, 267 insertions(+)
create mode 100644 src/backend/optimizer/util/extendplan.c
create mode 100644 src/include/optimizer/extendplan.h
diff --git a/src/backend/optimizer/util/Makefile b/src/backend/optimizer/util/Makefile
index 4fb115cb118..308730f392e 100644
--- a/src/backend/optimizer/util/Makefile
+++ b/src/backend/optimizer/util/Makefile
@@ -14,6 +14,7 @@ include $(top_builddir)/src/Makefile.global
OBJS = \
appendinfo.o \
+ extendplan.o \
clauses.o \
inherit.o \
joininfo.o \
diff --git a/src/backend/optimizer/util/extendplan.c b/src/backend/optimizer/util/extendplan.c
new file mode 100644
index 00000000000..26b324d2ee6
--- /dev/null
+++ b/src/backend/optimizer/util/extendplan.c
@@ -0,0 +1,181 @@
+/*-------------------------------------------------------------------------
+ *
+ * extendplan.c
+ * Extend core planner objects with additional private state
+ *
+ * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994-5, Regents of the University of California
+ *
+ * The interfaces defined in this file make it possible for loadable
+ * modules to their own private state inside of key planner data
+ * structures -- specifically, the PlannerGlobal, PlannerInfo, and
+ * RelOptInfo structures. This can make it much easier to write
+ * reasonably efficient planner extensions; for instance, code that
+ * uses set_join_pathlist_hook can arrange to compute a key intermediate
+ * result once per joinrel rather than on every call.
+ *
+ * IDENTIFICATION
+ * src/backend/commands/extendplan.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "optimizer/extendplan.h"
+#include "port/pg_bitutils.h"
+#include "utils/memutils.h"
+#include "utils/palloc.h"
+
+static const char **PlannerExtensionNameArray = NULL;
+static int PlannerExtensionNamesAssigned = 0;
+static int PlannerExtensionNamesAllocated = 0;
+
+/*
+ * Map the name of a planner extension to an integer ID.
+ *
+ * Within the lifetime of a particular backend, the same name will be mapped
+ * to the same ID every time. IDs are not stable across backends. Use the ID
+ * that you get from this function to call the remaining functions in this
+ * file.
+ */
+int
+GetPlannerExtensionId(const char *extension_name)
+{
+ /* Search for an existing extension by this name; if found, return ID. */
+ for (int i = 0; i < PlannerExtensionNamesAssigned; ++i)
+ if (strcmp(PlannerExtensionNameArray[i], extension_name) == 0)
+ return i;
+
+ /* If there is no array yet, create one. */
+ if (PlannerExtensionNameArray == NULL)
+ {
+ PlannerExtensionNamesAllocated = 16;
+ PlannerExtensionNameArray = (const char **)
+ MemoryContextAlloc(TopMemoryContext,
+ PlannerExtensionNamesAllocated
+ * sizeof(char *));
+ }
+
+ /* If there's an array but it's currently full, expand it. */
+ if (PlannerExtensionNamesAssigned >= PlannerExtensionNamesAllocated)
+ {
+ int i = pg_nextpower2_32(PlannerExtensionNamesAssigned + 1);
+
+ PlannerExtensionNameArray = (const char **)
+ repalloc(PlannerExtensionNameArray, i * sizeof(char *));
+ PlannerExtensionNamesAllocated = i;
+ }
+
+ /* Assign and return new ID. */
+ PlannerExtensionNameArray[PlannerExtensionNamesAssigned] = extension_name;
+ return PlannerExtensionNamesAssigned++;
+}
+
+/*
+ * Store extension-specific state into a PlannerGlobal.
+ */
+void
+SetPlannerGlobalExtensionState(PlannerGlobal *glob, int extension_id,
+ void *opaque)
+{
+ Assert(extension_id >= 0);
+
+ /* If there is no array yet, create one. */
+ if (glob->extension_state == NULL)
+ {
+ MemoryContext planner_cxt;
+ Size sz;
+
+ planner_cxt = GetMemoryChunkContext(glob);
+ glob->extension_state_allocated = 4;
+ sz = glob->extension_state_allocated * sizeof(void *);
+ glob->extension_state = MemoryContextAllocZero(planner_cxt, sz);
+ }
+
+ /* If there's an array but it's currently full, expand it. */
+ if (extension_id >= glob->extension_state_allocated)
+ {
+ int i;
+
+ i = pg_nextpower2_32(glob->extension_state_allocated + 1);
+ glob->extension_state = (void **)
+ repalloc0(glob->extension_state,
+ glob->extension_state_allocated * sizeof(void *),
+ i * sizeof(void *));
+ glob->extension_state_allocated = i;
+ }
+
+ glob->extension_state[extension_id] = opaque;
+}
+
+/*
+ * Store extension-specific state into a PlannerInfo.
+ */
+void
+SetPlannerInfoExtensionState(PlannerInfo *root, int extension_id,
+ void *opaque)
+{
+ Assert(extension_id >= 0);
+
+ /* If there is no array yet, create one. */
+ if (root->extension_state == NULL)
+ {
+ Size sz;
+
+ root->extension_state_allocated = 4;
+ sz = root->extension_state_allocated * sizeof(void *);
+ root->extension_state = MemoryContextAllocZero(root->planner_cxt, sz);
+ }
+
+ /* If there's an array but it's currently full, expand it. */
+ if (extension_id >= root->extension_state_allocated)
+ {
+ int i;
+
+ i = pg_nextpower2_32(root->extension_state_allocated + 1);
+ root->extension_state = (void **)
+ repalloc0(root->extension_state,
+ root->extension_state_allocated * sizeof(void *),
+ i * sizeof(void *));
+ root->extension_state_allocated = i;
+ }
+
+ root->extension_state[extension_id] = opaque;
+}
+
+/*
+ * Store extension-specific state into a RelOptInfo.
+ */
+void
+SetRelOptInfoExtensionState(RelOptInfo *rel, int extension_id,
+ void *opaque)
+{
+ Assert(extension_id >= 0);
+
+ /* If there is no array yet, create one. */
+ if (rel->extension_state == NULL)
+ {
+ MemoryContext planner_cxt;
+ Size sz;
+
+ planner_cxt = GetMemoryChunkContext(rel);
+ rel->extension_state_allocated = 4;
+ sz = rel->extension_state_allocated * sizeof(void *);
+ rel->extension_state = MemoryContextAllocZero(planner_cxt, sz);
+ }
+
+ /* If there's an array but it's currently full, expand it. */
+ if (extension_id >= rel->extension_state_allocated)
+ {
+ int i;
+
+ i = pg_nextpower2_32(rel->extension_state_allocated + 1);
+ rel->extension_state = (void **)
+ repalloc0(rel->extension_state,
+ rel->extension_state_allocated * sizeof(void *),
+ i * sizeof(void *));
+ rel->extension_state_allocated = i;
+ }
+
+ rel->extension_state[extension_id] = opaque;
+}
diff --git a/src/backend/optimizer/util/meson.build b/src/backend/optimizer/util/meson.build
index b3bf913d096..f71f56e37a1 100644
--- a/src/backend/optimizer/util/meson.build
+++ b/src/backend/optimizer/util/meson.build
@@ -3,6 +3,7 @@
backend_sources += files(
'appendinfo.c',
'clauses.c',
+ 'extendplan.c',
'inherit.c',
'joininfo.c',
'orclauses.c',
diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h
index 4a903d1ec18..21df16f5b04 100644
--- a/src/include/nodes/pathnodes.h
+++ b/src/include/nodes/pathnodes.h
@@ -182,6 +182,10 @@ typedef struct PlannerGlobal
/* hash table for NOT NULL attnums of relations */
struct HTAB *rel_notnullatts_hash pg_node_attr(read_write_ignore);
+
+ /* extension state */
+ void **extension_state pg_node_attr(read_write_ignore);
+ int extension_state_allocated;
} PlannerGlobal;
/* macro for fetching the Plan associated with a SubPlan node */
@@ -586,6 +590,10 @@ struct PlannerInfo
/* PartitionPruneInfos added in this query's plan. */
List *partPruneInfos;
+
+ /* extension state */
+ void **extension_state pg_node_attr(read_write_ignore);
+ int extension_state_allocated;
};
@@ -1097,6 +1105,10 @@ typedef struct RelOptInfo
List **partexprs pg_node_attr(read_write_ignore);
/* Nullable partition key expressions */
List **nullable_partexprs pg_node_attr(read_write_ignore);
+
+ /* extension state */
+ void **extension_state pg_node_attr(read_write_ignore);
+ int extension_state_allocated;
} RelOptInfo;
/*
diff --git a/src/include/optimizer/extendplan.h b/src/include/optimizer/extendplan.h
new file mode 100644
index 00000000000..a6c89af31ef
--- /dev/null
+++ b/src/include/optimizer/extendplan.h
@@ -0,0 +1,72 @@
+/*-------------------------------------------------------------------------
+ *
+ * extendplan.h
+ * Extend core planner objects with additional private state
+ *
+ *
+ * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/optimizer/extendplan.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef EXTENDPLAN_H
+#define EXTENDPLAN_H
+
+#include "nodes/pathnodes.h"
+
+extern int GetPlannerExtensionId(const char *extension_name);
+
+/*
+ * Get extension-specific state from a PlannerGlobal.
+ */
+static inline void *
+GetPlannerGlobalExtensionState(PlannerGlobal *glob, int extension_id)
+{
+ Assert(extension_id >= 0);
+
+ if (extension_id >= glob->extension_state_allocated)
+ return NULL;
+
+ return glob->extension_state[extension_id];
+}
+
+/*
+ * Get extension-specific state from a PlannerInfo.
+ */
+static inline void *
+GetPlannerInfoExtensionState(PlannerInfo *root, int extension_id)
+{
+ Assert(extension_id >= 0);
+
+ if (extension_id >= root->extension_state_allocated)
+ return NULL;
+
+ return root->extension_state[extension_id];
+}
+
+/*
+ * Get extension-specific state from a PlannerInfo.
+ */
+static inline void *
+GetRelOptInfoExtensionState(RelOptInfo *rel, int extension_id)
+{
+ Assert(extension_id >= 0);
+
+ if (extension_id >= rel->extension_state_allocated)
+ return NULL;
+
+ return rel->extension_state[extension_id];
+}
+
+/* Functions to store private state into various planner objects */
+extern void SetPlannerGlobalExtensionState(PlannerGlobal *glob,
+ int extension_id,
+ void *opaque);
+extern void SetPlannerInfoExtensionState(PlannerInfo *root, int extension_id,
+ void *opaque);
+extern void SetRelOptInfoExtensionState(RelOptInfo *root, int extension_id,
+ void *opaque);
+
+#endif
--
2.39.5 (Apple Git-154)
v3-0002-Remove-PlannerInfo-s-join_search_private-method.patchapplication/octet-stream; name=v3-0002-Remove-PlannerInfo-s-join_search_private-method.patchDownload
From 6e17282a078941a669d4e0da720d9642daeee4b1 Mon Sep 17 00:00:00 2001
From: Robert Haas <rhaas@postgresql.org>
Date: Wed, 20 Aug 2025 15:10:52 -0400
Subject: [PATCH v3 2/5] Remove PlannerInfo's join_search_private method.
Instead, use the new mechanism that allows planner extensions to store
private state inside a PlannerInfo, treating GEQO as an in-core planner
extension. This is a useful test of the new facility, and also buys
back a few bytes of storage.
To make this work, we must remove innerrel_is_unique_ext's hack of
testing whether join_search_private is set as a proxy for whether
the join search might be retried. Add a flag that extensions can
use to explicitly signal their intentions instead.
---
src/backend/optimizer/geqo/geqo_eval.c | 2 +-
src/backend/optimizer/geqo/geqo_main.c | 12 ++++++++++--
src/backend/optimizer/geqo/geqo_random.c | 7 +++----
src/backend/optimizer/plan/analyzejoins.c | 9 +++------
src/backend/optimizer/plan/planner.c | 1 +
src/backend/optimizer/prep/prepjointree.c | 1 +
src/include/nodes/pathnodes.h | 5 ++---
src/include/optimizer/geqo.h | 10 +++++++++-
8 files changed, 30 insertions(+), 17 deletions(-)
diff --git a/src/backend/optimizer/geqo/geqo_eval.c b/src/backend/optimizer/geqo/geqo_eval.c
index f07d1dc8ac6..7fcb1aa70d1 100644
--- a/src/backend/optimizer/geqo/geqo_eval.c
+++ b/src/backend/optimizer/geqo/geqo_eval.c
@@ -162,7 +162,7 @@ geqo_eval(PlannerInfo *root, Gene *tour, int num_gene)
RelOptInfo *
gimme_tree(PlannerInfo *root, Gene *tour, int num_gene)
{
- GeqoPrivateData *private = (GeqoPrivateData *) root->join_search_private;
+ GeqoPrivateData *private = GetGeqoPrivateData(root);
List *clumps;
int rel_count;
diff --git a/src/backend/optimizer/geqo/geqo_main.c b/src/backend/optimizer/geqo/geqo_main.c
index 38402ce58db..0064556087a 100644
--- a/src/backend/optimizer/geqo/geqo_main.c
+++ b/src/backend/optimizer/geqo/geqo_main.c
@@ -47,6 +47,8 @@ int Geqo_generations;
double Geqo_selection_bias;
double Geqo_seed;
+/* GEQO is treated as an in-core planner extension */
+int Geqo_planner_extension_id = -1;
static int gimme_pool_size(int nr_rel);
static int gimme_number_generations(int pool_size);
@@ -98,10 +100,16 @@ geqo(PlannerInfo *root, int number_of_rels, List *initial_rels)
int mutations = 0;
#endif
+ if (Geqo_planner_extension_id < 0)
+ Geqo_planner_extension_id = GetPlannerExtensionId("geqo");
+
/* set up private information */
- root->join_search_private = &private;
+ SetPlannerInfoExtensionState(root, Geqo_planner_extension_id, &private);
private.initial_rels = initial_rels;
+/* inform core planner that we may replan */
+ root->assumeReplanning = true;
+
/* initialize private number generator */
geqo_set_seed(root, Geqo_seed);
@@ -304,7 +312,7 @@ geqo(PlannerInfo *root, int number_of_rels, List *initial_rels)
free_pool(root, pool);
/* ... clear root pointer to our private storage */
- root->join_search_private = NULL;
+ SetPlannerInfoExtensionState(root, Geqo_planner_extension_id, NULL);
return best_rel;
}
diff --git a/src/backend/optimizer/geqo/geqo_random.c b/src/backend/optimizer/geqo/geqo_random.c
index 6c7a411f69f..46d28baa2e6 100644
--- a/src/backend/optimizer/geqo/geqo_random.c
+++ b/src/backend/optimizer/geqo/geqo_random.c
@@ -15,11 +15,10 @@
#include "optimizer/geqo_random.h"
-
void
geqo_set_seed(PlannerInfo *root, double seed)
{
- GeqoPrivateData *private = (GeqoPrivateData *) root->join_search_private;
+ GeqoPrivateData *private = GetGeqoPrivateData(root);
pg_prng_fseed(&private->random_state, seed);
}
@@ -27,7 +26,7 @@ geqo_set_seed(PlannerInfo *root, double seed)
double
geqo_rand(PlannerInfo *root)
{
- GeqoPrivateData *private = (GeqoPrivateData *) root->join_search_private;
+ GeqoPrivateData *private = GetGeqoPrivateData(root);
return pg_prng_double(&private->random_state);
}
@@ -35,7 +34,7 @@ geqo_rand(PlannerInfo *root)
int
geqo_randint(PlannerInfo *root, int upper, int lower)
{
- GeqoPrivateData *private = (GeqoPrivateData *) root->join_search_private;
+ GeqoPrivateData *private = GetGeqoPrivateData(root);
/*
* In current usage, "lower" is never negative so we can just use
diff --git a/src/backend/optimizer/plan/analyzejoins.c b/src/backend/optimizer/plan/analyzejoins.c
index da92d8ee414..60b37ac6cd0 100644
--- a/src/backend/optimizer/plan/analyzejoins.c
+++ b/src/backend/optimizer/plan/analyzejoins.c
@@ -1424,17 +1424,14 @@ innerrel_is_unique_ext(PlannerInfo *root,
*
* However, in normal planning mode, caching this knowledge is totally
* pointless; it won't be queried again, because we build up joinrels
- * from smaller to larger. It is useful in GEQO mode, where the
- * knowledge can be carried across successive planning attempts; and
- * it's likely to be useful when using join-search plugins, too. Hence
- * cache when join_search_private is non-NULL. (Yeah, that's a hack,
- * but it seems reasonable.)
+ * from smaller to larger. It's only useful when using GEQO or
+ * another planner extension that attempts planning multiple times.
*
* Also, allow callers to override that heuristic and force caching;
* that's useful for reduce_unique_semijoins, which calls here before
* the normal join search starts.
*/
- if (force_cache || root->join_search_private)
+ if (force_cache || root->assumeReplanning)
{
old_context = MemoryContextSwitchTo(root->planner_cxt);
innerrel->non_unique_for_rels =
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index 65f17101591..0ffe8cf12c6 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -703,6 +703,7 @@ subquery_planner(PlannerGlobal *glob, Query *parse, PlannerInfo *parent_root,
root->hasAlternativeSubPlans = false;
root->placeholdersFrozen = false;
root->hasRecursion = hasRecursion;
+ root->assumeReplanning = false;
if (hasRecursion)
root->wt_param_id = assign_special_exec_param(root);
else
diff --git a/src/backend/optimizer/prep/prepjointree.c b/src/backend/optimizer/prep/prepjointree.c
index 35e8d3c183b..4075f7519ca 100644
--- a/src/backend/optimizer/prep/prepjointree.c
+++ b/src/backend/optimizer/prep/prepjointree.c
@@ -1383,6 +1383,7 @@ pull_up_simple_subquery(PlannerInfo *root, Node *jtnode, RangeTblEntry *rte,
subroot->qual_security_level = 0;
subroot->placeholdersFrozen = false;
subroot->hasRecursion = false;
+ subroot->assumeReplanning = false;
subroot->wt_param_id = -1;
subroot->non_recursive_path = NULL;
/* We don't currently need a top JoinDomain for the subroot */
diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h
index 21df16f5b04..641e77a1326 100644
--- a/src/include/nodes/pathnodes.h
+++ b/src/include/nodes/pathnodes.h
@@ -536,6 +536,8 @@ struct PlannerInfo
bool placeholdersFrozen;
/* true if planning a recursive WITH item */
bool hasRecursion;
+ /* true if a planner extension may replan this subquery */
+ bool assumeReplanning;
/*
* The rangetable index for the RTE_GROUP RTE, or 0 if there is no
@@ -582,9 +584,6 @@ struct PlannerInfo
bool *isAltSubplan pg_node_attr(read_write_ignore);
bool *isUsedSubplan pg_node_attr(read_write_ignore);
- /* optional private data for join_search_hook, e.g., GEQO */
- void *join_search_private pg_node_attr(read_write_ignore);
-
/* Does this query modify any partition key columns? */
bool partColsUpdated;
diff --git a/src/include/optimizer/geqo.h b/src/include/optimizer/geqo.h
index 9f8e0f337aa..3f4872e25e3 100644
--- a/src/include/optimizer/geqo.h
+++ b/src/include/optimizer/geqo.h
@@ -24,6 +24,7 @@
#include "common/pg_prng.h"
#include "nodes/pathnodes.h"
+#include "optimizer/extendplan.h"
#include "optimizer/geqo_gene.h"
@@ -62,6 +63,8 @@ extern PGDLLIMPORT int Geqo_generations; /* 1 .. inf, or 0 to use default */
extern PGDLLIMPORT double Geqo_selection_bias;
+extern PGDLLIMPORT int Geqo_planner_extension_id;
+
#define DEFAULT_GEQO_SELECTION_BIAS 2.0
#define MIN_GEQO_SELECTION_BIAS 1.5
#define MAX_GEQO_SELECTION_BIAS 2.0
@@ -70,7 +73,7 @@ extern PGDLLIMPORT double Geqo_seed; /* 0 .. 1 */
/*
- * Private state for a GEQO run --- accessible via root->join_search_private
+ * Private state for a GEQO run --- accessible via GetGeqoPrivateData
*/
typedef struct
{
@@ -78,6 +81,11 @@ typedef struct
pg_prng_state random_state; /* PRNG state */
} GeqoPrivateData;
+static inline GeqoPrivateData *
+GetGeqoPrivateData(PlannerInfo *root)
+{
+ return GetPlannerInfoExtensionState(root, Geqo_planner_extension_id);
+}
/* routines in geqo_main.c */
extern RelOptInfo *geqo(PlannerInfo *root,
--
2.39.5 (Apple Git-154)
v3-0005-not-for-commit-count-distinct-joinrels-and-joinre.patchapplication/octet-stream; name=v3-0005-not-for-commit-count-distinct-joinrels-and-joinre.patchDownload
From 641cf91bf28d2f449f2b6b87d75bfe5cd27298f3 Mon Sep 17 00:00:00 2001
From: Robert Haas <rhaas@postgresql.org>
Date: Mon, 25 Aug 2025 15:22:57 -0400
Subject: [PATCH v3 5/5] not for commit: count distinct joinrels and joinrel
planning attempts
---
.../expected/pg_overexplain.out | 22 +++-
contrib/pg_overexplain/pg_overexplain.c | 107 ++++++++++++++++++
2 files changed, 124 insertions(+), 5 deletions(-)
diff --git a/contrib/pg_overexplain/expected/pg_overexplain.out b/contrib/pg_overexplain/expected/pg_overexplain.out
index 6de02323d7c..d8d3e36d7f1 100644
--- a/contrib/pg_overexplain/expected/pg_overexplain.out
+++ b/contrib/pg_overexplain/expected/pg_overexplain.out
@@ -38,7 +38,9 @@ EXPLAIN (DEBUG) SELECT 1;
Relation OIDs: none
Executor Parameter Types: none
Parse Location: 0 to end
-(11 rows)
+ Total Joinrel Attempts: 0
+ Distinct Joinrels: 0
+(13 rows)
EXPLAIN (RANGE_TABLE) SELECT 1;
QUERY PLAN
@@ -120,6 +122,8 @@ $$);
Relation OIDs: NNN...
Executor Parameter Types: none
Parse Location: 0 to end
+ Total Joinrel Attempts: 0
+ Distinct Joinrels: 0
RTI 1 (relation, inherited, in-from-clause):
Eref: vegetables (id, name, genus)
Relation: vegetables
@@ -141,7 +145,7 @@ $$);
Relation Kind: relation
Relation Lock Mode: AccessShareLock
Unprunable RTIs: 1 3 4
-(53 rows)
+(55 rows)
-- Test a different output format.
SELECT explain_filter($$
@@ -241,6 +245,8 @@ $$);
<Relation-OIDs>NNN...</Relation-OIDs> +
<Executor-Parameter-Types>none</Executor-Parameter-Types> +
<Parse-Location>0 to end</Parse-Location> +
+ <Total-Joinrel-Attempts>0</Total-Joinrel-Attempts> +
+ <Distinct-Joinrels>0</Distinct-Joinrels> +
</PlannedStmt> +
<Range-Table> +
<Range-Table-Entry> +
@@ -345,7 +351,9 @@ $$);
Relation OIDs: NNN...
Executor Parameter Types: none
Parse Location: 0 to end
-(37 rows)
+ Total Joinrel Attempts: 0
+ Distinct Joinrels: 0
+(39 rows)
SET debug_parallel_query = false;
RESET enable_seqscan;
@@ -373,7 +381,9 @@ $$);
Relation OIDs: NNN...
Executor Parameter Types: 0
Parse Location: 0 to end
-(15 rows)
+ Total Joinrel Attempts: 0
+ Distinct Joinrels: 0
+(17 rows)
-- Create an index, and then attempt to force a nested loop with inner index
-- scan so that we can see parameter-related information. Also, let's try
@@ -437,7 +447,9 @@ $$);
Relation OIDs: NNN...
Executor Parameter Types: 23
Parse Location: 0 to end
-(47 rows)
+ Total Joinrel Attempts: 2
+ Distinct Joinrels: 1
+(49 rows)
RESET enable_hashjoin;
RESET enable_material;
diff --git a/contrib/pg_overexplain/pg_overexplain.c b/contrib/pg_overexplain/pg_overexplain.c
index de824566f8c..bf4852fcadc 100644
--- a/contrib/pg_overexplain/pg_overexplain.c
+++ b/contrib/pg_overexplain/pg_overexplain.c
@@ -16,6 +16,10 @@
#include "commands/explain_format.h"
#include "commands/explain_state.h"
#include "fmgr.h"
+#include "nodes/makefuncs.h"
+#include "optimizer/extendplan.h"
+#include "optimizer/paths.h"
+#include "optimizer/planner.h"
#include "parser/parsetree.h"
#include "storage/lock.h"
#include "utils/builtins.h"
@@ -32,6 +36,12 @@ typedef struct
bool range_table;
} overexplain_options;
+typedef struct
+{
+ int total_joinrel_attempts;
+ int distinct_joinrel_count;
+} overexplain_plannerglobal;
+
static overexplain_options *overexplain_ensure_options(ExplainState *es);
static void overexplain_debug_handler(ExplainState *es, DefElem *opt,
ParseState *pstate);
@@ -57,9 +67,27 @@ static void overexplain_bitmapset(const char *qlabel, Bitmapset *bms,
static void overexplain_intlist(const char *qlabel, List *list,
ExplainState *es);
+static void overexplain_planner_setup_hook(PlannerGlobal *glob, Query *parse,
+ const char *query_string,
+ double *tuple_fraction);
+static void overexplain_planner_shutdown_hook(PlannerGlobal *glob,
+ Query *parse,
+ const char *query_string,
+ PlannedStmt *pstmt);
+static void overexplain_set_join_pathlist_hook(PlannerInfo *root,
+ RelOptInfo *joinrel,
+ RelOptInfo *outerrel,
+ RelOptInfo *innerrel,
+ JoinType jointype,
+ JoinPathExtraData *extra);
+
static int es_extension_id;
+static int planner_extension_id = -1;
static explain_per_node_hook_type prev_explain_per_node_hook;
static explain_per_plan_hook_type prev_explain_per_plan_hook;
+static planner_setup_hook_type prev_planner_setup_hook;
+static planner_shutdown_hook_type prev_planner_shutdown_hook;
+static set_join_pathlist_hook_type prev_set_join_pathlist_hook;
/*
* Initialization we do when this module is loaded.
@@ -70,6 +98,9 @@ _PG_init(void)
/* Get an ID that we can use to cache data in an ExplainState. */
es_extension_id = GetExplainExtensionId("pg_overexplain");
+ /* Get an ID that we can use to cache data in the planner. */
+ planner_extension_id = GetPlannerExtensionId("pg_overexplain");
+
/* Register the new EXPLAIN options implemented by this module. */
RegisterExtensionExplainOption("debug", overexplain_debug_handler);
RegisterExtensionExplainOption("range_table",
@@ -80,6 +111,16 @@ _PG_init(void)
explain_per_node_hook = overexplain_per_node_hook;
prev_explain_per_plan_hook = explain_per_plan_hook;
explain_per_plan_hook = overexplain_per_plan_hook;
+
+ /* Example of planner_setup_hook/planner_shutdown_hook use */
+ prev_planner_setup_hook = planner_setup_hook;
+ planner_setup_hook = overexplain_planner_setup_hook;
+ prev_planner_shutdown_hook = planner_shutdown_hook;
+ planner_shutdown_hook = overexplain_planner_shutdown_hook;
+
+ /* Support for above example */
+ prev_set_join_pathlist_hook = set_join_pathlist_hook;
+ set_join_pathlist_hook = overexplain_set_join_pathlist_hook;
}
/*
@@ -369,6 +410,29 @@ overexplain_debug(PlannedStmt *plannedstmt, ExplainState *es)
plannedstmt->stmt_len),
es);
+ {
+ DefElem *elem = NULL;
+
+ foreach_node(DefElem, de, plannedstmt->extension_state)
+ {
+ if (strcmp(de->defname, "pg_overexplain") == 0)
+ {
+ elem = de;
+ break;
+ }
+ }
+
+ if (elem != NULL)
+ {
+ List *l = castNode(List, elem->arg);
+
+ ExplainPropertyInteger("Total Joinrel Attempts", NULL,
+ intVal(linitial(l)), es);
+ ExplainPropertyInteger("Distinct Joinrels", NULL,
+ intVal(lsecond(l)), es);
+ }
+ }
+
/* Done with this group. */
if (es->format == EXPLAIN_FORMAT_TEXT)
es->indent--;
@@ -772,3 +836,46 @@ overexplain_intlist(const char *qlabel, List *list, ExplainState *es)
pfree(buf.data);
}
+
+static void
+overexplain_planner_setup_hook(PlannerGlobal *glob, Query *parse,
+ const char *query_string,
+ double *tuple_fraction)
+{
+ overexplain_plannerglobal *g = palloc0_object(overexplain_plannerglobal);
+
+ SetPlannerGlobalExtensionState(glob, planner_extension_id, g);
+}
+
+static void
+overexplain_planner_shutdown_hook(PlannerGlobal *glob, Query *parse,
+ const char *query_string, PlannedStmt *pstmt)
+{
+ overexplain_plannerglobal *g;
+ DefElem *elem;
+ List *l;
+
+ g = GetPlannerGlobalExtensionState(glob, planner_extension_id);
+ l = list_make2(makeInteger(g->total_joinrel_attempts),
+ makeInteger(g->distinct_joinrel_count));
+ elem = makeDefElem("pg_overexplain", (Node *) l, -1);
+ pstmt->extension_state = lappend(pstmt->extension_state, elem);
+}
+
+static void
+overexplain_set_join_pathlist_hook(PlannerInfo *root, RelOptInfo *joinrel,
+ RelOptInfo *outerrel, RelOptInfo *innerrel,
+ JoinType jointype, JoinPathExtraData *extra)
+{
+ overexplain_plannerglobal *g;
+
+ g = GetPlannerGlobalExtensionState(root->glob, planner_extension_id);
+ g->total_joinrel_attempts++;
+
+ if (GetRelOptInfoExtensionState(joinrel, planner_extension_id) == NULL)
+ {
+ g->distinct_joinrel_count++;
+ /* set any non-NULL value to avoid double-counting */
+ SetRelOptInfoExtensionState(joinrel, planner_extension_id, g);
+ }
+}
--
2.39.5 (Apple Git-154)
v3-0003-Add-planner_setup_hook-and-planner_shutdown_hook.patchapplication/octet-stream; name=v3-0003-Add-planner_setup_hook-and-planner_shutdown_hook.patchDownload
From 85dc4e21a7c660e4966bd461f64b5ba3d9a743b3 Mon Sep 17 00:00:00 2001
From: Robert Haas <rhaas@postgresql.org>
Date: Mon, 25 Aug 2025 13:04:50 -0400
Subject: [PATCH v3 3/5] Add planner_setup_hook and planner_shutdown_hook.
These hooks allow plugins to get control at the earliest point at
which the PlannerGlobal object is fully initialized, and then just
before it gets destroyed. This is useful in combination with the
extendable plan state facilities (see extendplan.h) and perhaps for
other purposes as well.
---
src/backend/optimizer/plan/planner.c | 14 ++++++++++++++
src/include/optimizer/planner.h | 12 ++++++++++++
2 files changed, 26 insertions(+)
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index 0ffe8cf12c6..407ba34fc47 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -73,6 +73,12 @@ bool enable_distinct_reordering = true;
/* Hook for plugins to get control in planner() */
planner_hook_type planner_hook = NULL;
+/* Hook for plugins to get control after PlannerGlobal is initialized */
+planner_setup_hook_type planner_setup_hook = NULL;
+
+/* Hook for plugins to get control before PlannerGlobal is discarded */
+planner_shutdown_hook_type planner_shutdown_hook = NULL;
+
/* Hook for plugins to get control when grouping_planner() plans upper rels */
create_upper_paths_hook_type create_upper_paths_hook = NULL;
@@ -438,6 +444,10 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions,
tuple_fraction = 0.0;
}
+ /* Allow plugins to take control after we've initialized "glob" */
+ if (planner_setup_hook)
+ (*planner_setup_hook) (glob, parse, query_string, &tuple_fraction);
+
/* primary planning entry point (may recurse for subqueries) */
root = subquery_planner(glob, parse, NULL, false, tuple_fraction, NULL);
@@ -619,6 +629,10 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions,
if (glob->partition_directory != NULL)
DestroyPartitionDirectory(glob->partition_directory);
+ /* Allow plugins to take control before we discard "glob" */
+ if (planner_shutdown_hook)
+ (*planner_shutdown_hook) (glob, parse, query_string, result);
+
return result;
}
diff --git a/src/include/optimizer/planner.h b/src/include/optimizer/planner.h
index f220e9a270d..732f8f81171 100644
--- a/src/include/optimizer/planner.h
+++ b/src/include/optimizer/planner.h
@@ -29,6 +29,18 @@ typedef PlannedStmt *(*planner_hook_type) (Query *parse,
ParamListInfo boundParams);
extern PGDLLIMPORT planner_hook_type planner_hook;
+/* Hook for plugins to get control after PlannerGlobal is initialized */
+typedef void (*planner_setup_hook_type) (PlannerGlobal *glob, Query *parse,
+ const char *query_string,
+ double *tuple_fraction);
+extern PGDLLIMPORT planner_setup_hook_type planner_setup_hook;
+
+/* Hook for plugins to get control before PlannerGlobal is discarded */
+typedef void (*planner_shutdown_hook_type) (PlannerGlobal *glob, Query *parse,
+ const char *query_string,
+ PlannedStmt *pstmt);
+extern PGDLLIMPORT planner_shutdown_hook_type planner_shutdown_hook;
+
/* Hook for plugins to get control when grouping_planner() plans upper rels */
typedef void (*create_upper_paths_hook_type) (PlannerInfo *root,
UpperRelationKind stage,
--
2.39.5 (Apple Git-154)
v3-0004-Add-extension_state-member-to-PlannedStmt.patchapplication/octet-stream; name=v3-0004-Add-extension_state-member-to-PlannedStmt.patchDownload
From b91c3d7792ba19fc57871f7fb62352f9cb2e0337 Mon Sep 17 00:00:00 2001
From: Robert Haas <rhaas@postgresql.org>
Date: Mon, 25 Aug 2025 14:29:02 -0400
Subject: [PATCH v3 4/5] Add extension_state member to PlannedStmt.
Extensions can stash data computed at plan time into this list using
planner_shutdown_hook (or perhaps other mechanisms) and then access
it from any code that has access to the PlannedStmt (such as explain
hooks), allowing for extensible debugging and instrumentation of
plans.
---
src/include/nodes/plannodes.h | 9 +++++++++
1 file changed, 9 insertions(+)
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index 29d7732d6a0..326c3f7fe6f 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -149,6 +149,15 @@ typedef struct PlannedStmt
/* non-null if this is utility stmt */
Node *utilityStmt;
+ /*
+ * DefElem objects added by extensions, e.g. using planner_shutdown_hook
+ *
+ * Set each DefElem's defname to the name of the plugin or extension, and
+ * the argument to a tree of nodes that all have copy and read/write
+ * support.
+ */
+ List *extension_state;
+
/* statement location in source string (copied from Query) */
/* start location, or -1 if unknown */
ParseLoc stmt_location;
--
2.39.5 (Apple Git-154)
On 25/8/2025 21:46, Robert Haas wrote:
On Wed, Aug 20, 2025 at 3:13 PM Robert Haas <robertmhaas@gmail.com> wrote:
Here's v2. 0001 is what you saw before with an attempt to fix the
memory context handling. 0002 removes join_search_private. All I've
tested is that the tests pass.Here's v3 with a few more patches. I'm now fairly confident I have the
basic approach correct here, but let's see what others think.0001 is the core "private state" patch for PlannerGlobal, PlannerInfo,
and RelOptInfo. It is unchanged since v2, and contains only the fix
for memory context handling since v1. However, I've now tested it, and
I think it's OK to commit, barring further review comments.
Reading this patch, I didn't find reasoning for the two decisions:
1. Why is it necessary to maintain the GetExplainExtensionId and
GetPlannerExtensionId routines? It seems that using a single
extension_id (related to the order of the library inside the
file_scanner) is more transparent and more straightforward if necessary.
2. Why does the extensibility approach in 0001 differ from that in 0004?
I can imagine it is all about limiting extensions, but anyway, a module
has access to PlannerInfo, PlannerGlobal, etc. So, this machinery looks
a little redundant, doesn't it?
0003 adds two new planner hooks. In experimenting with 0001, I
discovered that it was a little hard to use. PlannerGlobal has to do
with what happens in a whole planning cycle, but the only hook we have
that's in approximately the right place is planner_hook, and it can't
see the PlannerGlobal object. So, I added these hooks. The first fires
after PlannerGlobal is fully initialized and before we start using it,
and the second fires just before we throw PlannerGlobal away. I
considered some other approaches, specifically: (1) making
subquery_planner a hook, (2) making grouping_planner a hook, and (3)
doing as the patch does but with the call before rather than after
assembling the PlannedStmt. Those proved inferior; the hook at the
very end of planner() just before we discard the PlannerGlobal object
appears quite valuable to me. Needs review.These hooks look contradictory to me. If we store data inside a
RelOptInfo, it will be challenging to match this RelOptInfo with
specific Plan node(s) in the shutdown hook. That's why I prefer to use
create_plan_hook, which may also utilise PlannerGlobal and store the
extension's data within the plan.
I support the subquery_planner hook idea because each subplan represents
a separate planning space, and it can be challenging to distinguish
between two similar subplans that exist at the same query level.
--
Andrei Lepikhov
--
regards, Andrei Lepikhov
On Mon, Aug 25, 2025 at 3:46 PM Robert Haas <robertmhaas@gmail.com> wrote:
0005 is a demo, not for commit, just to show how these pieces fit
together. It uses the hooks from 0001 to count the number of times
set_join_pathlist_hook is called and the number of those that are for
distinct joinrels. Then it uses planner_shutdown_hook to propagate
that into the PlannedStmt, and makes EXPLAIN (DEBUG) print those
values out. I think there are far more interesting bits of information
that could be preserved and propagated using this infrastructure,
though some of them probably also require other changes to make it all
work. But this is a simple example to show that the concept is valid
even without anything else.
While mulling this over, I realized that this only works if you don't
mind propagating information into the final plan regardless without
knowing whether or not EXPLAIN was actually used. That's pretty sad,
because whatever you want to propagate into the final plan has to be a
node tree, and you probably had the data in some more digestible form
during planning, and so now you have to go do a whole bunch of work to
convert it into a format that the node infrastructure can digest and
most of the time that's going to be useless. Now, as far as I can see
that's not really an argument against anything in the patch set, which
I'm still hoping someone will review, but it's probably a good
argument that something more is needed.
The simplest idea that comes to mind for me is to make pg_plan_query()
take an ExplainState * argument and pass it through to planner().
Non-EXPLAIN callers can pass NULL, and planner extensions can get
control in via planner_hook() and GetExplainExtensionState() on any
provided ExplainState to figure out what they want to do. However, a
hole in this plan is the case where we call ExplainExecuteQuery(). In
that case, and I believe only that case, pg_plan_query() is not
called; instead, we fetch a plan from the plan cache, and that may be
an already-existing plan, in which case there's no option to
retroactively go back in time and save more or different information.
I don't really know what to do about that. We could ignore that case
and let extensions that work in this way document this as a caveat, or
we could try to force GetCachedPlan() to re-plan if an ExplainState is
provided, or maybe there's some other option.
--
Robert Haas
EDB: http://www.enterprisedb.com
On Tue, Aug 26, 2025 at 4:58 AM Andrei Lepikhov <lepihov@gmail.com> wrote:
1. Why is it necessary to maintain the GetExplainExtensionId and
GetPlannerExtensionId routines? It seems that using a single
extension_id (related to the order of the library inside the
file_scanner) is more transparent and more straightforward if necessary.
But this wouldn't work for in-core use cases like GEQO, right? Also,
how would it work if there are multiple "extensions" in the same .so
file?
2. Why does the extensibility approach in 0001 differ from that in 0004?
I can imagine it is all about limiting extensions, but anyway, a module
has access to PlannerInfo, PlannerGlobal, etc. So, this machinery looks
a little redundant, doesn't it?
What do you mean that the extensibility approach differs? Like that
the type of extension_state is different?
- Melanie
On Mon, Aug 25, 2025 at 3:47 PM Robert Haas <robertmhaas@gmail.com> wrote:
0001 is the core "private state" patch for PlannerGlobal, PlannerInfo,
and RelOptInfo. It is unchanged since v2, and contains only the fix
for memory context handling since v1. However, I've now tested it, and
I think it's OK to commit, barring further review comments.
A few nits on 0001
From 1aa43c063edb325548fa3db30b9991bf0831f6f5 Mon Sep 17 00:00:00 2001 From: Robert Haas <rhaas@postgresql.org> Date: Mon, 18 Aug 2025 16:11:10 -0400 Subject: [PATCH v3 1/5] Allow private state in certain planner data + * extendplan.c + * Extend core planner objects with additional private state + * + * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1994-5, Regents of the University of California + * + * The interfaces defined in this file make it possible for loadable + * modules to their own private state inside of key planner data
You're missing a word above -- like "modules to store their own"
+ * uses set_join_pathlist_hook can arrange to compute a key intermediate + * result once per joinrel rather than on every call. + * + * IDENTIFICATION + * src/backend/commands/extendplan.c
This path does not reflect where you put the file
+ * +int +GetPlannerExtensionId(const char *extension_name) +{
<--snip-->
+ + /* If there's an array but it's currently full, expand it. */ + if (PlannerExtensionNamesAssigned >= PlannerExtensionNamesAllocated) + { + int i = pg_nextpower2_32(PlannerExtensionNamesAssigned + 1);
Storing a uint32 in a signed int that could be 32-bit stuck out to me.
+ + PlannerExtensionNameArray = (const char **) + repalloc(PlannerExtensionNameArray, i * sizeof(char *)); + PlannerExtensionNamesAllocated = i; + } + + /* Assign and return new ID. */ + PlannerExtensionNameArray[PlannerExtensionNamesAssigned] = extension_name;
Since you don't copy the extension name, it might be worth mentioning
that the caller should provide a literal or at least something that
will be around later.
diff --git a/src/include/optimizer/extendplan.h b/src/include/optimizer/extendplan.h new file mode 100644 +extern void SetPlannerInfoExtensionState(PlannerInfo *root, int extension_id, + void *opaque); +extern void SetRelOptInfoExtensionState(RelOptInfo *root, int extension_id, + void *opaque);
You used a different variable name here than in the implementation for
the RelOptInfo parameter.
0002 removes join_search_private, as before. Whether it makes sense to
go ahead with this is debatable. Needs review, and needs an opinion on
whether this should be considered a PoC only (and discarded) or
something that should go forward to commit.
Is there a downside to going forward with it?
As for the code itself, I thought assumeReplanning was a bit vague
since it seems like whether or not replanning is allowed could come up
outside of join order search -- but perhaps that's okay.
For another example of how these patches could be used, see
/messages/by-id/CA+TgmoZ=6jJi9TGyZCm33vads46HFkyz6Aju_saLT6GFS-iFug@mail.gmail.com
and in particular 0001 and 0002. This patch set's planner_setup_hook
call would go write after those patches compute default_ssa_mask and
default_jsa_mask, allowing the hook to override those values.
So, are you saying that you would rewrite the patches in that set to
use the infrastructure in this set -- e.g. remove that set's
PlannerGlobal.default_jsa_mask and instead put it in
PlannerGlobal.extension_state? Or am I misunderstanding?
- Melanie
Hmm. I don't have a copy of Andrei's email in my gmail. I see it in
the archives but I have not got it. I don't understand how that
happened. I now wonder if there are other emails from Andrei I haven't
received.
On Fri, Sep 12, 2025 at 4:34 PM Melanie Plageman
<melanieplageman@gmail.com> wrote:
On Tue, Aug 26, 2025 at 4:58 AM Andrei Lepikhov <lepihov@gmail.com> wrote:
1. Why is it necessary to maintain the GetExplainExtensionId and
GetPlannerExtensionId routines? It seems that using a single
extension_id (related to the order of the library inside the
file_scanner) is more transparent and more straightforward if necessary.But this wouldn't work for in-core use cases like GEQO, right? Also,
how would it work if there are multiple "extensions" in the same .so
file?
We probably don't want to all extensions on any topic to be allocating
extension IDs from the same space, because it's used as a list index
and we don't want to have to null-pad lists excessively. Combining the
explain and planner cases wouldn't be too much of a stretch, perhaps,
but it's also not really costing us anything to have separate IDs for
those cases.
2. Why does the extensibility approach in 0001 differ from that in 0004?
I can imagine it is all about limiting extensions, but anyway, a module
has access to PlannerInfo, PlannerGlobal, etc. So, this machinery looks
a little redundant, doesn't it?What do you mean that the extensibility approach differs? Like that
the type of extension_state is different?
I suspect the question here is about why not use the
index-by-planner-extension-ID approach for 0004. That could maybe
work, but here everything has to be a Node, so I feel like it would be
more contorted than the existing cases.
--
Robert Haas
EDB: http://www.enterprisedb.com
On Fri, Sep 12, 2025 at 6:34 PM Melanie Plageman
<melanieplageman@gmail.com> wrote:
You're missing a word above -- like "modules to store their own"
This path does not reflect where you put the file
Thanks.
Storing a uint32 in a signed int that could be 32-bit stuck out to me.
"git grep pg_nextpower2_32" finds examples of assigning the result to
both "int" and "uint32", and I see no practical risk here.
Since you don't copy the extension name, it might be worth mentioning
that the caller should provide a literal or at least something that
will be around later.
Maybe, but there's no obvious reason for any caller to use anything
other than a string literal.
You used a different variable name here than in the implementation for
the RelOptInfo parameter.
Oops.
0002 removes join_search_private, as before. Whether it makes sense to
go ahead with this is debatable. Needs review, and needs an opinion on
whether this should be considered a PoC only (and discarded) or
something that should go forward to commit.Is there a downside to going forward with it?
I think it's just a stylistic preference, whether people like it this
way better or not.
As for the code itself, I thought assumeReplanning was a bit vague
since it seems like whether or not replanning is allowed could come up
outside of join order search -- but perhaps that's okay.
Yeah, there is room for bikeshedding that name.
For another example of how these patches could be used, see
/messages/by-id/CA+TgmoZ=6jJi9TGyZCm33vads46HFkyz6Aju_saLT6GFS-iFug@mail.gmail.com
and in particular 0001 and 0002. This patch set's planner_setup_hook
call would go write after those patches compute default_ssa_mask and
default_jsa_mask, allowing the hook to override those values.So, are you saying that you would rewrite the patches in that set to
use the infrastructure in this set -- e.g. remove that set's
PlannerGlobal.default_jsa_mask and instead put it in
PlannerGlobal.extension_state? Or am I misunderstanding?
No, that's not what I'm saying. What I'm saying is that with both
patches applied, planner_setup_hook() from this patch ends up getting
called right after default_jsa_mask is set, so another thing this hook
can do is adjust that value. Or, for example, you can write a patch
that uses this infrastructure to associate state with each RelOptInfo,
and then you can use that state to decide how to set jsa_mask in
join_path_setup_hook. In other words, it's easier to make effective
use of those patches if you have the infrastructure provided by these
patches.
--
Robert Haas
EDB: http://www.enterprisedb.com
On Fri, Sep 5, 2025 at 4:19 PM Robert Haas <robertmhaas@gmail.com> wrote:
While mulling this over, I realized that this only works if you don't
mind propagating information into the final plan regardless without
knowing whether or not EXPLAIN was actually used. That's pretty sad,
[...]
The simplest idea that comes to mind for me is to make pg_plan_query()
take an ExplainState * argument and pass it through to planner().
Here's a new version that implements this idea and also cleans up a
few points that Melanie noted.
--
Robert Haas
EDB: http://www.enterprisedb.com
Attachments:
v4-0003-Add-ExplainState-argument-to-pg_plan_query-and-pl.patchapplication/octet-stream; name=v4-0003-Add-ExplainState-argument-to-pg_plan_query-and-pl.patchDownload
From 8c911c8ddc832e56a5711e5e34a58b254e5f68f9 Mon Sep 17 00:00:00 2001
From: Robert Haas <rhaas@postgresql.org>
Date: Tue, 16 Sep 2025 13:13:24 -0400
Subject: [PATCH v4 3/6] Add ExplainState argument to pg_plan_query() and
planner().
This allows extensions to have access to any data they've stored
in the ExplainState during planning. Unfortunately, it won't help
with EXPLAIN EXECUTE is used, but since that case is less common,
this still seems like an improvement.
---
contrib/pg_stat_statements/pg_stat_statements.c | 14 ++++++++------
src/backend/commands/copyto.c | 2 +-
src/backend/commands/createas.c | 2 +-
src/backend/commands/explain.c | 4 ++--
src/backend/commands/matview.c | 2 +-
src/backend/commands/portalcmds.c | 3 ++-
src/backend/optimizer/plan/planner.c | 10 ++++++----
src/backend/tcop/postgres.c | 7 ++++---
src/include/optimizer/optimizer.h | 6 +++++-
src/include/optimizer/planner.h | 9 +++++++--
src/include/tcop/tcopprot.h | 5 ++++-
src/test/modules/delay_execution/delay_execution.c | 7 ++++---
12 files changed, 45 insertions(+), 26 deletions(-)
diff --git a/contrib/pg_stat_statements/pg_stat_statements.c b/contrib/pg_stat_statements/pg_stat_statements.c
index 0bb0f933399..0eb208f58fc 100644
--- a/contrib/pg_stat_statements/pg_stat_statements.c
+++ b/contrib/pg_stat_statements/pg_stat_statements.c
@@ -337,7 +337,8 @@ static void pgss_post_parse_analyze(ParseState *pstate, Query *query,
static PlannedStmt *pgss_planner(Query *parse,
const char *query_string,
int cursorOptions,
- ParamListInfo boundParams);
+ ParamListInfo boundParams,
+ struct ExplainState *es);
static void pgss_ExecutorStart(QueryDesc *queryDesc, int eflags);
static void pgss_ExecutorRun(QueryDesc *queryDesc,
ScanDirection direction,
@@ -893,7 +894,8 @@ static PlannedStmt *
pgss_planner(Query *parse,
const char *query_string,
int cursorOptions,
- ParamListInfo boundParams)
+ ParamListInfo boundParams,
+ struct ExplainState *es)
{
PlannedStmt *result;
@@ -928,10 +930,10 @@ pgss_planner(Query *parse,
{
if (prev_planner_hook)
result = prev_planner_hook(parse, query_string, cursorOptions,
- boundParams);
+ boundParams, es);
else
result = standard_planner(parse, query_string, cursorOptions,
- boundParams);
+ boundParams, es);
}
PG_FINALLY();
{
@@ -977,10 +979,10 @@ pgss_planner(Query *parse,
{
if (prev_planner_hook)
result = prev_planner_hook(parse, query_string, cursorOptions,
- boundParams);
+ boundParams, es);
else
result = standard_planner(parse, query_string, cursorOptions,
- boundParams);
+ boundParams, es);
}
PG_FINALLY();
{
diff --git a/src/backend/commands/copyto.c b/src/backend/commands/copyto.c
index 67b94b91cae..e5781155cdf 100644
--- a/src/backend/commands/copyto.c
+++ b/src/backend/commands/copyto.c
@@ -796,7 +796,7 @@ BeginCopyTo(ParseState *pstate,
/* plan the query */
plan = pg_plan_query(query, pstate->p_sourcetext,
- CURSOR_OPT_PARALLEL_OK, NULL);
+ CURSOR_OPT_PARALLEL_OK, NULL, NULL);
/*
* With row-level security and a user using "COPY relation TO", we
diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c
index dfd2ab8e862..1ccc2e55c64 100644
--- a/src/backend/commands/createas.c
+++ b/src/backend/commands/createas.c
@@ -321,7 +321,7 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt,
/* plan the query */
plan = pg_plan_query(query, pstate->p_sourcetext,
- CURSOR_OPT_PARALLEL_OK, params);
+ CURSOR_OPT_PARALLEL_OK, params, NULL);
/*
* Use a snapshot with an updated command ID to ensure this query sees
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index c3d449fdec1..9f0fc3206e3 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -351,7 +351,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, params, es);
INSTR_TIME_SET_CURRENT(planduration);
INSTR_TIME_SUBTRACT(planduration, planstart);
@@ -4853,7 +4853,7 @@ show_result_replacement_info(Result *result, ExplainState *es)
ExplainPropertyText("Replaces", replacement_type, es);
else
{
- char *s = psprintf("%s on %s", replacement_type, buf.data);
+ char *s = psprintf("%s on %s", replacement_type, buf.data);
ExplainPropertyText("Replaces", s, es);
}
diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index 188e26f0e6e..441de55ac24 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -426,7 +426,7 @@ refresh_matview_datafill(DestReceiver *dest, Query *query,
CHECK_FOR_INTERRUPTS();
/* Plan the query which will generate data for the refresh. */
- plan = pg_plan_query(query, queryString, CURSOR_OPT_PARALLEL_OK, NULL);
+ plan = pg_plan_query(query, queryString, CURSOR_OPT_PARALLEL_OK, NULL, NULL);
/*
* Use a snapshot with an updated command ID to ensure this query sees
diff --git a/src/backend/commands/portalcmds.c b/src/backend/commands/portalcmds.c
index e7c8171c102..ec96c2efcd3 100644
--- a/src/backend/commands/portalcmds.c
+++ b/src/backend/commands/portalcmds.c
@@ -99,7 +99,8 @@ PerformCursorOpen(ParseState *pstate, DeclareCursorStmt *cstmt, ParamListInfo pa
elog(ERROR, "non-SELECT statement in DECLARE CURSOR");
/* Plan the query, applying the specified options */
- plan = pg_plan_query(query, pstate->p_sourcetext, cstmt->options, params);
+ plan = pg_plan_query(query, pstate->p_sourcetext, cstmt->options, params,
+ NULL);
/*
* Create a portal and copy the plan and query string into its memory.
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index 9de39da1757..205d8886a2a 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -291,14 +291,16 @@ static void create_partial_unique_paths(PlannerInfo *root, RelOptInfo *input_rel
*****************************************************************************/
PlannedStmt *
planner(Query *parse, const char *query_string, int cursorOptions,
- ParamListInfo boundParams)
+ ParamListInfo boundParams, ExplainState *es)
{
PlannedStmt *result;
if (planner_hook)
- result = (*planner_hook) (parse, query_string, cursorOptions, boundParams);
+ result = (*planner_hook) (parse, query_string, cursorOptions,
+ boundParams, es);
else
- result = standard_planner(parse, query_string, cursorOptions, boundParams);
+ result = standard_planner(parse, query_string, cursorOptions,
+ boundParams, es);
pgstat_report_plan_id(result->planId, false);
@@ -307,7 +309,7 @@ planner(Query *parse, const char *query_string, int cursorOptions,
PlannedStmt *
standard_planner(Query *parse, const char *query_string, int cursorOptions,
- ParamListInfo boundParams)
+ ParamListInfo boundParams, ExplainState *es)
{
PlannedStmt *result;
PlannerGlobal *glob;
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index d356830f756..7dd75a490aa 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -37,6 +37,7 @@
#include "catalog/pg_type.h"
#include "commands/async.h"
#include "commands/event_trigger.h"
+#include "commands/explain_state.h"
#include "commands/prepare.h"
#include "common/pg_prng.h"
#include "jit/jit.h"
@@ -884,7 +885,7 @@ pg_rewrite_query(Query *query)
*/
PlannedStmt *
pg_plan_query(Query *querytree, const char *query_string, int cursorOptions,
- ParamListInfo boundParams)
+ ParamListInfo boundParams, ExplainState *es)
{
PlannedStmt *plan;
@@ -901,7 +902,7 @@ pg_plan_query(Query *querytree, const char *query_string, int cursorOptions,
ResetUsage();
/* call the optimizer */
- plan = planner(querytree, query_string, cursorOptions, boundParams);
+ plan = planner(querytree, query_string, cursorOptions, boundParams, es);
if (log_planner_stats)
ShowUsage("PLANNER STATISTICS");
@@ -997,7 +998,7 @@ pg_plan_queries(List *querytrees, const char *query_string, int cursorOptions,
else
{
stmt = pg_plan_query(query, query_string, cursorOptions,
- boundParams);
+ boundParams, NULL);
}
stmt_list = lappend(stmt_list, stmt);
diff --git a/src/include/optimizer/optimizer.h b/src/include/optimizer/optimizer.h
index 04878f1f1c2..a8aaefd27e9 100644
--- a/src/include/optimizer/optimizer.h
+++ b/src/include/optimizer/optimizer.h
@@ -24,6 +24,9 @@
#include "nodes/parsenodes.h"
+/* avoid including commands/explain_state.h here */
+struct ExplainState;
+
/*
* We don't want to include nodes/pathnodes.h here, because non-planner
* code should generally treat PlannerInfo as an opaque typedef.
@@ -104,7 +107,8 @@ extern PGDLLIMPORT bool enable_distinct_reordering;
extern PlannedStmt *planner(Query *parse, const char *query_string,
int cursorOptions,
- ParamListInfo boundParams);
+ ParamListInfo boundParams,
+ struct ExplainState *es);
extern Expr *expression_planner(Expr *expr);
extern Expr *expression_planner_with_deps(Expr *expr,
diff --git a/src/include/optimizer/planner.h b/src/include/optimizer/planner.h
index f220e9a270d..6c07711913d 100644
--- a/src/include/optimizer/planner.h
+++ b/src/include/optimizer/planner.h
@@ -22,11 +22,15 @@
#include "nodes/plannodes.h"
+/* avoid including commands/explain_state.h here */
+struct ExplainState;
+
/* Hook for plugins to get control in planner() */
typedef PlannedStmt *(*planner_hook_type) (Query *parse,
const char *query_string,
int cursorOptions,
- ParamListInfo boundParams);
+ ParamListInfo boundParams,
+ struct ExplainState *es);
extern PGDLLIMPORT planner_hook_type planner_hook;
/* Hook for plugins to get control when grouping_planner() plans upper rels */
@@ -40,7 +44,8 @@ extern PGDLLIMPORT create_upper_paths_hook_type create_upper_paths_hook;
extern PlannedStmt *standard_planner(Query *parse, const char *query_string,
int cursorOptions,
- ParamListInfo boundParams);
+ ParamListInfo boundParams,
+ struct ExplainState *es);
extern PlannerInfo *subquery_planner(PlannerGlobal *glob, Query *parse,
PlannerInfo *parent_root,
diff --git a/src/include/tcop/tcopprot.h b/src/include/tcop/tcopprot.h
index a83cc4f4850..90323b10554 100644
--- a/src/include/tcop/tcopprot.h
+++ b/src/include/tcop/tcopprot.h
@@ -20,6 +20,8 @@
#include "utils/guc.h"
#include "utils/queryenvironment.h"
+/* avoid including commands/explain_state.h here */
+struct ExplainState;
extern PGDLLIMPORT CommandDest whereToSendOutput;
extern PGDLLIMPORT const char *debug_query_string;
@@ -63,7 +65,8 @@ extern List *pg_analyze_and_rewrite_withcb(RawStmt *parsetree,
QueryEnvironment *queryEnv);
extern PlannedStmt *pg_plan_query(Query *querytree, const char *query_string,
int cursorOptions,
- ParamListInfo boundParams);
+ ParamListInfo boundParams,
+ struct ExplainState *es);
extern List *pg_plan_queries(List *querytrees, const char *query_string,
int cursorOptions,
ParamListInfo boundParams);
diff --git a/src/test/modules/delay_execution/delay_execution.c b/src/test/modules/delay_execution/delay_execution.c
index 7bc97f84a1c..53c4073fce6 100644
--- a/src/test/modules/delay_execution/delay_execution.c
+++ b/src/test/modules/delay_execution/delay_execution.c
@@ -40,17 +40,18 @@ static planner_hook_type prev_planner_hook = NULL;
/* planner_hook function to provide the desired delay */
static PlannedStmt *
delay_execution_planner(Query *parse, const char *query_string,
- int cursorOptions, ParamListInfo boundParams)
+ int cursorOptions, ParamListInfo boundParams,
+ struct ExplainState *es)
{
PlannedStmt *result;
/* Invoke the planner, possibly via a previous hook user */
if (prev_planner_hook)
result = prev_planner_hook(parse, query_string, cursorOptions,
- boundParams);
+ boundParams, es);
else
result = standard_planner(parse, query_string, cursorOptions,
- boundParams);
+ boundParams, es);
/* If enabled, delay by taking and releasing the specified lock */
if (post_planning_lock_id != 0)
--
2.39.5 (Apple Git-154)
v4-0002-Remove-PlannerInfo-s-join_search_private-method.patchapplication/octet-stream; name=v4-0002-Remove-PlannerInfo-s-join_search_private-method.patchDownload
From 747df97779d412f8d5008ed88559ce7242d5e0c1 Mon Sep 17 00:00:00 2001
From: Robert Haas <rhaas@postgresql.org>
Date: Wed, 20 Aug 2025 15:10:52 -0400
Subject: [PATCH v4 2/6] Remove PlannerInfo's join_search_private method.
Instead, use the new mechanism that allows planner extensions to store
private state inside a PlannerInfo, treating GEQO as an in-core planner
extension. This is a useful test of the new facility, and also buys
back a few bytes of storage.
To make this work, we must remove innerrel_is_unique_ext's hack of
testing whether join_search_private is set as a proxy for whether
the join search might be retried. Add a flag that extensions can
use to explicitly signal their intentions instead.
---
src/backend/optimizer/geqo/geqo_eval.c | 2 +-
src/backend/optimizer/geqo/geqo_main.c | 12 ++++++++++--
src/backend/optimizer/geqo/geqo_random.c | 7 +++----
src/backend/optimizer/plan/analyzejoins.c | 9 +++------
src/backend/optimizer/plan/planner.c | 1 +
src/backend/optimizer/prep/prepjointree.c | 1 +
src/include/nodes/pathnodes.h | 5 ++---
src/include/optimizer/geqo.h | 10 +++++++++-
8 files changed, 30 insertions(+), 17 deletions(-)
diff --git a/src/backend/optimizer/geqo/geqo_eval.c b/src/backend/optimizer/geqo/geqo_eval.c
index f07d1dc8ac6..7fcb1aa70d1 100644
--- a/src/backend/optimizer/geqo/geqo_eval.c
+++ b/src/backend/optimizer/geqo/geqo_eval.c
@@ -162,7 +162,7 @@ geqo_eval(PlannerInfo *root, Gene *tour, int num_gene)
RelOptInfo *
gimme_tree(PlannerInfo *root, Gene *tour, int num_gene)
{
- GeqoPrivateData *private = (GeqoPrivateData *) root->join_search_private;
+ GeqoPrivateData *private = GetGeqoPrivateData(root);
List *clumps;
int rel_count;
diff --git a/src/backend/optimizer/geqo/geqo_main.c b/src/backend/optimizer/geqo/geqo_main.c
index 38402ce58db..0064556087a 100644
--- a/src/backend/optimizer/geqo/geqo_main.c
+++ b/src/backend/optimizer/geqo/geqo_main.c
@@ -47,6 +47,8 @@ int Geqo_generations;
double Geqo_selection_bias;
double Geqo_seed;
+/* GEQO is treated as an in-core planner extension */
+int Geqo_planner_extension_id = -1;
static int gimme_pool_size(int nr_rel);
static int gimme_number_generations(int pool_size);
@@ -98,10 +100,16 @@ geqo(PlannerInfo *root, int number_of_rels, List *initial_rels)
int mutations = 0;
#endif
+ if (Geqo_planner_extension_id < 0)
+ Geqo_planner_extension_id = GetPlannerExtensionId("geqo");
+
/* set up private information */
- root->join_search_private = &private;
+ SetPlannerInfoExtensionState(root, Geqo_planner_extension_id, &private);
private.initial_rels = initial_rels;
+/* inform core planner that we may replan */
+ root->assumeReplanning = true;
+
/* initialize private number generator */
geqo_set_seed(root, Geqo_seed);
@@ -304,7 +312,7 @@ geqo(PlannerInfo *root, int number_of_rels, List *initial_rels)
free_pool(root, pool);
/* ... clear root pointer to our private storage */
- root->join_search_private = NULL;
+ SetPlannerInfoExtensionState(root, Geqo_planner_extension_id, NULL);
return best_rel;
}
diff --git a/src/backend/optimizer/geqo/geqo_random.c b/src/backend/optimizer/geqo/geqo_random.c
index 6c7a411f69f..46d28baa2e6 100644
--- a/src/backend/optimizer/geqo/geqo_random.c
+++ b/src/backend/optimizer/geqo/geqo_random.c
@@ -15,11 +15,10 @@
#include "optimizer/geqo_random.h"
-
void
geqo_set_seed(PlannerInfo *root, double seed)
{
- GeqoPrivateData *private = (GeqoPrivateData *) root->join_search_private;
+ GeqoPrivateData *private = GetGeqoPrivateData(root);
pg_prng_fseed(&private->random_state, seed);
}
@@ -27,7 +26,7 @@ geqo_set_seed(PlannerInfo *root, double seed)
double
geqo_rand(PlannerInfo *root)
{
- GeqoPrivateData *private = (GeqoPrivateData *) root->join_search_private;
+ GeqoPrivateData *private = GetGeqoPrivateData(root);
return pg_prng_double(&private->random_state);
}
@@ -35,7 +34,7 @@ geqo_rand(PlannerInfo *root)
int
geqo_randint(PlannerInfo *root, int upper, int lower)
{
- GeqoPrivateData *private = (GeqoPrivateData *) root->join_search_private;
+ GeqoPrivateData *private = GetGeqoPrivateData(root);
/*
* In current usage, "lower" is never negative so we can just use
diff --git a/src/backend/optimizer/plan/analyzejoins.c b/src/backend/optimizer/plan/analyzejoins.c
index 2a3dea88a94..6a3c030e8ef 100644
--- a/src/backend/optimizer/plan/analyzejoins.c
+++ b/src/backend/optimizer/plan/analyzejoins.c
@@ -1425,17 +1425,14 @@ innerrel_is_unique_ext(PlannerInfo *root,
*
* However, in normal planning mode, caching this knowledge is totally
* pointless; it won't be queried again, because we build up joinrels
- * from smaller to larger. It is useful in GEQO mode, where the
- * knowledge can be carried across successive planning attempts; and
- * it's likely to be useful when using join-search plugins, too. Hence
- * cache when join_search_private is non-NULL. (Yeah, that's a hack,
- * but it seems reasonable.)
+ * from smaller to larger. It's only useful when using GEQO or
+ * another planner extension that attempts planning multiple times.
*
* Also, allow callers to override that heuristic and force caching;
* that's useful for reduce_unique_semijoins, which calls here before
* the normal join search starts.
*/
- if (force_cache || root->join_search_private)
+ if (force_cache || root->assumeReplanning)
{
old_context = MemoryContextSwitchTo(root->planner_cxt);
innerrel->non_unique_for_rels =
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index 41bd8353430..9de39da1757 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -703,6 +703,7 @@ subquery_planner(PlannerGlobal *glob, Query *parse, PlannerInfo *parent_root,
root->hasAlternativeSubPlans = false;
root->placeholdersFrozen = false;
root->hasRecursion = hasRecursion;
+ root->assumeReplanning = false;
if (hasRecursion)
root->wt_param_id = assign_special_exec_param(root);
else
diff --git a/src/backend/optimizer/prep/prepjointree.c b/src/backend/optimizer/prep/prepjointree.c
index 35e8d3c183b..4075f7519ca 100644
--- a/src/backend/optimizer/prep/prepjointree.c
+++ b/src/backend/optimizer/prep/prepjointree.c
@@ -1383,6 +1383,7 @@ pull_up_simple_subquery(PlannerInfo *root, Node *jtnode, RangeTblEntry *rte,
subroot->qual_security_level = 0;
subroot->placeholdersFrozen = false;
subroot->hasRecursion = false;
+ subroot->assumeReplanning = false;
subroot->wt_param_id = -1;
subroot->non_recursive_path = NULL;
/* We don't currently need a top JoinDomain for the subroot */
diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h
index 5cf23cba596..bc1e0c1b5cc 100644
--- a/src/include/nodes/pathnodes.h
+++ b/src/include/nodes/pathnodes.h
@@ -530,6 +530,8 @@ struct PlannerInfo
bool placeholdersFrozen;
/* true if planning a recursive WITH item */
bool hasRecursion;
+ /* true if a planner extension may replan this subquery */
+ bool assumeReplanning;
/*
* The rangetable index for the RTE_GROUP RTE, or 0 if there is no
@@ -576,9 +578,6 @@ struct PlannerInfo
bool *isAltSubplan pg_node_attr(read_write_ignore);
bool *isUsedSubplan pg_node_attr(read_write_ignore);
- /* optional private data for join_search_hook, e.g., GEQO */
- void *join_search_private pg_node_attr(read_write_ignore);
-
/* Does this query modify any partition key columns? */
bool partColsUpdated;
diff --git a/src/include/optimizer/geqo.h b/src/include/optimizer/geqo.h
index 9f8e0f337aa..3f4872e25e3 100644
--- a/src/include/optimizer/geqo.h
+++ b/src/include/optimizer/geqo.h
@@ -24,6 +24,7 @@
#include "common/pg_prng.h"
#include "nodes/pathnodes.h"
+#include "optimizer/extendplan.h"
#include "optimizer/geqo_gene.h"
@@ -62,6 +63,8 @@ extern PGDLLIMPORT int Geqo_generations; /* 1 .. inf, or 0 to use default */
extern PGDLLIMPORT double Geqo_selection_bias;
+extern PGDLLIMPORT int Geqo_planner_extension_id;
+
#define DEFAULT_GEQO_SELECTION_BIAS 2.0
#define MIN_GEQO_SELECTION_BIAS 1.5
#define MAX_GEQO_SELECTION_BIAS 2.0
@@ -70,7 +73,7 @@ extern PGDLLIMPORT double Geqo_seed; /* 0 .. 1 */
/*
- * Private state for a GEQO run --- accessible via root->join_search_private
+ * Private state for a GEQO run --- accessible via GetGeqoPrivateData
*/
typedef struct
{
@@ -78,6 +81,11 @@ typedef struct
pg_prng_state random_state; /* PRNG state */
} GeqoPrivateData;
+static inline GeqoPrivateData *
+GetGeqoPrivateData(PlannerInfo *root)
+{
+ return GetPlannerInfoExtensionState(root, Geqo_planner_extension_id);
+}
/* routines in geqo_main.c */
extern RelOptInfo *geqo(PlannerInfo *root,
--
2.39.5 (Apple Git-154)
v4-0004-Add-planner_setup_hook-and-planner_shutdown_hook.patchapplication/octet-stream; name=v4-0004-Add-planner_setup_hook-and-planner_shutdown_hook.patchDownload
From f00ff216cb3c7fe9db7782b22fb3a220edef9589 Mon Sep 17 00:00:00 2001
From: Robert Haas <rhaas@postgresql.org>
Date: Mon, 25 Aug 2025 13:04:50 -0400
Subject: [PATCH v4 4/6] Add planner_setup_hook and planner_shutdown_hook.
These hooks allow plugins to get control at the earliest point at
which the PlannerGlobal object is fully initialized, and then just
before it gets destroyed. This is useful in combination with the
extendable plan state facilities (see extendplan.h) and perhaps for
other purposes as well.
---
src/backend/optimizer/plan/planner.c | 14 ++++++++++++++
src/include/optimizer/planner.h | 13 +++++++++++++
2 files changed, 27 insertions(+)
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index 205d8886a2a..ba69098e0df 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -73,6 +73,12 @@ bool enable_distinct_reordering = true;
/* Hook for plugins to get control in planner() */
planner_hook_type planner_hook = NULL;
+/* Hook for plugins to get control after PlannerGlobal is initialized */
+planner_setup_hook_type planner_setup_hook = NULL;
+
+/* Hook for plugins to get control before PlannerGlobal is discarded */
+planner_shutdown_hook_type planner_shutdown_hook = NULL;
+
/* Hook for plugins to get control when grouping_planner() plans upper rels */
create_upper_paths_hook_type create_upper_paths_hook = NULL;
@@ -440,6 +446,10 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions,
tuple_fraction = 0.0;
}
+ /* Allow plugins to take control after we've initialized "glob" */
+ if (planner_setup_hook)
+ (*planner_setup_hook) (glob, parse, query_string, &tuple_fraction, es);
+
/* primary planning entry point (may recurse for subqueries) */
root = subquery_planner(glob, parse, NULL, false, tuple_fraction, NULL);
@@ -621,6 +631,10 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions,
if (glob->partition_directory != NULL)
DestroyPartitionDirectory(glob->partition_directory);
+ /* Allow plugins to take control before we discard "glob" */
+ if (planner_shutdown_hook)
+ (*planner_shutdown_hook) (glob, parse, query_string, result);
+
return result;
}
diff --git a/src/include/optimizer/planner.h b/src/include/optimizer/planner.h
index 6c07711913d..6b893f76444 100644
--- a/src/include/optimizer/planner.h
+++ b/src/include/optimizer/planner.h
@@ -33,6 +33,19 @@ typedef PlannedStmt *(*planner_hook_type) (Query *parse,
struct ExplainState *es);
extern PGDLLIMPORT planner_hook_type planner_hook;
+/* Hook for plugins to get control after PlannerGlobal is initialized */
+typedef void (*planner_setup_hook_type) (PlannerGlobal *glob, Query *parse,
+ const char *query_string,
+ double *tuple_fraction,
+ struct ExplainState *es);
+extern PGDLLIMPORT planner_setup_hook_type planner_setup_hook;
+
+/* Hook for plugins to get control before PlannerGlobal is discarded */
+typedef void (*planner_shutdown_hook_type) (PlannerGlobal *glob, Query *parse,
+ const char *query_string,
+ PlannedStmt *pstmt);
+extern PGDLLIMPORT planner_shutdown_hook_type planner_shutdown_hook;
+
/* Hook for plugins to get control when grouping_planner() plans upper rels */
typedef void (*create_upper_paths_hook_type) (PlannerInfo *root,
UpperRelationKind stage,
--
2.39.5 (Apple Git-154)
v4-0001-Allow-private-state-in-certain-planner-data-struc.patchapplication/octet-stream; name=v4-0001-Allow-private-state-in-certain-planner-data-struc.patchDownload
From c227de07914c48026838687fc3700f78d1e33329 Mon Sep 17 00:00:00 2001
From: Robert Haas <rhaas@postgresql.org>
Date: Mon, 18 Aug 2025 16:11:10 -0400
Subject: [PATCH v4 1/6] Allow private state in certain planner data
structures.
Extension that make extensive use of planner hooks may want to
coordinate their efforts, for example to avoid duplicate computation,
but that's currently difficult because there's no really good way to
pass data between different hooks.
To make that easier, allow for storage of extension-managed private
state in PlannerGlobal, PlannerInfo, and RelOptInfo, along very
similar lines to what we have permitted for ExplainState since commit
c65bc2e1d14a2d4daed7c1921ac518f2c5ac3d17.
---
src/backend/optimizer/util/Makefile | 1 +
src/backend/optimizer/util/extendplan.c | 181 ++++++++++++++++++++++++
src/backend/optimizer/util/meson.build | 1 +
src/include/nodes/pathnodes.h | 12 ++
src/include/optimizer/extendplan.h | 72 ++++++++++
5 files changed, 267 insertions(+)
create mode 100644 src/backend/optimizer/util/extendplan.c
create mode 100644 src/include/optimizer/extendplan.h
diff --git a/src/backend/optimizer/util/Makefile b/src/backend/optimizer/util/Makefile
index 4fb115cb118..308730f392e 100644
--- a/src/backend/optimizer/util/Makefile
+++ b/src/backend/optimizer/util/Makefile
@@ -14,6 +14,7 @@ include $(top_builddir)/src/Makefile.global
OBJS = \
appendinfo.o \
+ extendplan.o \
clauses.o \
inherit.o \
joininfo.o \
diff --git a/src/backend/optimizer/util/extendplan.c b/src/backend/optimizer/util/extendplan.c
new file mode 100644
index 00000000000..55c26cf8411
--- /dev/null
+++ b/src/backend/optimizer/util/extendplan.c
@@ -0,0 +1,181 @@
+/*-------------------------------------------------------------------------
+ *
+ * extendplan.c
+ * Extend core planner objects with additional private state
+ *
+ * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994-5, Regents of the University of California
+ *
+ * The interfaces defined in this file make it possible for loadable
+ * modules to store their own private state inside of key planner data
+ * structures -- specifically, the PlannerGlobal, PlannerInfo, and
+ * RelOptInfo structures. This can make it much easier to write
+ * reasonably efficient planner extensions; for instance, code that
+ * uses set_join_pathlist_hook can arrange to compute a key intermediate
+ * result once per joinrel rather than on every call.
+ *
+ * IDENTIFICATION
+ * src/backend/optimizer/util/extendplan.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "optimizer/extendplan.h"
+#include "port/pg_bitutils.h"
+#include "utils/memutils.h"
+#include "utils/palloc.h"
+
+static const char **PlannerExtensionNameArray = NULL;
+static int PlannerExtensionNamesAssigned = 0;
+static int PlannerExtensionNamesAllocated = 0;
+
+/*
+ * Map the name of a planner extension to an integer ID.
+ *
+ * Within the lifetime of a particular backend, the same name will be mapped
+ * to the same ID every time. IDs are not stable across backends. Use the ID
+ * that you get from this function to call the remaining functions in this
+ * file.
+ */
+int
+GetPlannerExtensionId(const char *extension_name)
+{
+ /* Search for an existing extension by this name; if found, return ID. */
+ for (int i = 0; i < PlannerExtensionNamesAssigned; ++i)
+ if (strcmp(PlannerExtensionNameArray[i], extension_name) == 0)
+ return i;
+
+ /* If there is no array yet, create one. */
+ if (PlannerExtensionNameArray == NULL)
+ {
+ PlannerExtensionNamesAllocated = 16;
+ PlannerExtensionNameArray = (const char **)
+ MemoryContextAlloc(TopMemoryContext,
+ PlannerExtensionNamesAllocated
+ * sizeof(char *));
+ }
+
+ /* If there's an array but it's currently full, expand it. */
+ if (PlannerExtensionNamesAssigned >= PlannerExtensionNamesAllocated)
+ {
+ int i = pg_nextpower2_32(PlannerExtensionNamesAssigned + 1);
+
+ PlannerExtensionNameArray = (const char **)
+ repalloc(PlannerExtensionNameArray, i * sizeof(char *));
+ PlannerExtensionNamesAllocated = i;
+ }
+
+ /* Assign and return new ID. */
+ PlannerExtensionNameArray[PlannerExtensionNamesAssigned] = extension_name;
+ return PlannerExtensionNamesAssigned++;
+}
+
+/*
+ * Store extension-specific state into a PlannerGlobal.
+ */
+void
+SetPlannerGlobalExtensionState(PlannerGlobal *glob, int extension_id,
+ void *opaque)
+{
+ Assert(extension_id >= 0);
+
+ /* If there is no array yet, create one. */
+ if (glob->extension_state == NULL)
+ {
+ MemoryContext planner_cxt;
+ Size sz;
+
+ planner_cxt = GetMemoryChunkContext(glob);
+ glob->extension_state_allocated = 4;
+ sz = glob->extension_state_allocated * sizeof(void *);
+ glob->extension_state = MemoryContextAllocZero(planner_cxt, sz);
+ }
+
+ /* If there's an array but it's currently full, expand it. */
+ if (extension_id >= glob->extension_state_allocated)
+ {
+ int i;
+
+ i = pg_nextpower2_32(glob->extension_state_allocated + 1);
+ glob->extension_state = (void **)
+ repalloc0(glob->extension_state,
+ glob->extension_state_allocated * sizeof(void *),
+ i * sizeof(void *));
+ glob->extension_state_allocated = i;
+ }
+
+ glob->extension_state[extension_id] = opaque;
+}
+
+/*
+ * Store extension-specific state into a PlannerInfo.
+ */
+void
+SetPlannerInfoExtensionState(PlannerInfo *root, int extension_id,
+ void *opaque)
+{
+ Assert(extension_id >= 0);
+
+ /* If there is no array yet, create one. */
+ if (root->extension_state == NULL)
+ {
+ Size sz;
+
+ root->extension_state_allocated = 4;
+ sz = root->extension_state_allocated * sizeof(void *);
+ root->extension_state = MemoryContextAllocZero(root->planner_cxt, sz);
+ }
+
+ /* If there's an array but it's currently full, expand it. */
+ if (extension_id >= root->extension_state_allocated)
+ {
+ int i;
+
+ i = pg_nextpower2_32(root->extension_state_allocated + 1);
+ root->extension_state = (void **)
+ repalloc0(root->extension_state,
+ root->extension_state_allocated * sizeof(void *),
+ i * sizeof(void *));
+ root->extension_state_allocated = i;
+ }
+
+ root->extension_state[extension_id] = opaque;
+}
+
+/*
+ * Store extension-specific state into a RelOptInfo.
+ */
+void
+SetRelOptInfoExtensionState(RelOptInfo *rel, int extension_id,
+ void *opaque)
+{
+ Assert(extension_id >= 0);
+
+ /* If there is no array yet, create one. */
+ if (rel->extension_state == NULL)
+ {
+ MemoryContext planner_cxt;
+ Size sz;
+
+ planner_cxt = GetMemoryChunkContext(rel);
+ rel->extension_state_allocated = 4;
+ sz = rel->extension_state_allocated * sizeof(void *);
+ rel->extension_state = MemoryContextAllocZero(planner_cxt, sz);
+ }
+
+ /* If there's an array but it's currently full, expand it. */
+ if (extension_id >= rel->extension_state_allocated)
+ {
+ int i;
+
+ i = pg_nextpower2_32(rel->extension_state_allocated + 1);
+ rel->extension_state = (void **)
+ repalloc0(rel->extension_state,
+ rel->extension_state_allocated * sizeof(void *),
+ i * sizeof(void *));
+ rel->extension_state_allocated = i;
+ }
+
+ rel->extension_state[extension_id] = opaque;
+}
diff --git a/src/backend/optimizer/util/meson.build b/src/backend/optimizer/util/meson.build
index b3bf913d096..f71f56e37a1 100644
--- a/src/backend/optimizer/util/meson.build
+++ b/src/backend/optimizer/util/meson.build
@@ -3,6 +3,7 @@
backend_sources += files(
'appendinfo.c',
'clauses.c',
+ 'extendplan.c',
'inherit.c',
'joininfo.c',
'orclauses.c',
diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h
index b12a2508d8c..5cf23cba596 100644
--- a/src/include/nodes/pathnodes.h
+++ b/src/include/nodes/pathnodes.h
@@ -182,6 +182,10 @@ typedef struct PlannerGlobal
/* hash table for NOT NULL attnums of relations */
struct HTAB *rel_notnullatts_hash pg_node_attr(read_write_ignore);
+
+ /* extension state */
+ void **extension_state pg_node_attr(read_write_ignore);
+ int extension_state_allocated;
} PlannerGlobal;
/* macro for fetching the Plan associated with a SubPlan node */
@@ -580,6 +584,10 @@ struct PlannerInfo
/* PartitionPruneInfos added in this query's plan. */
List *partPruneInfos;
+
+ /* extension state */
+ void **extension_state pg_node_attr(read_write_ignore);
+ int extension_state_allocated;
};
@@ -1091,6 +1099,10 @@ typedef struct RelOptInfo
List **partexprs pg_node_attr(read_write_ignore);
/* Nullable partition key expressions */
List **nullable_partexprs pg_node_attr(read_write_ignore);
+
+ /* extension state */
+ void **extension_state pg_node_attr(read_write_ignore);
+ int extension_state_allocated;
} RelOptInfo;
/*
diff --git a/src/include/optimizer/extendplan.h b/src/include/optimizer/extendplan.h
new file mode 100644
index 00000000000..de9618761dd
--- /dev/null
+++ b/src/include/optimizer/extendplan.h
@@ -0,0 +1,72 @@
+/*-------------------------------------------------------------------------
+ *
+ * extendplan.h
+ * Extend core planner objects with additional private state
+ *
+ *
+ * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/optimizer/extendplan.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef EXTENDPLAN_H
+#define EXTENDPLAN_H
+
+#include "nodes/pathnodes.h"
+
+extern int GetPlannerExtensionId(const char *extension_name);
+
+/*
+ * Get extension-specific state from a PlannerGlobal.
+ */
+static inline void *
+GetPlannerGlobalExtensionState(PlannerGlobal *glob, int extension_id)
+{
+ Assert(extension_id >= 0);
+
+ if (extension_id >= glob->extension_state_allocated)
+ return NULL;
+
+ return glob->extension_state[extension_id];
+}
+
+/*
+ * Get extension-specific state from a PlannerInfo.
+ */
+static inline void *
+GetPlannerInfoExtensionState(PlannerInfo *root, int extension_id)
+{
+ Assert(extension_id >= 0);
+
+ if (extension_id >= root->extension_state_allocated)
+ return NULL;
+
+ return root->extension_state[extension_id];
+}
+
+/*
+ * Get extension-specific state from a PlannerInfo.
+ */
+static inline void *
+GetRelOptInfoExtensionState(RelOptInfo *rel, int extension_id)
+{
+ Assert(extension_id >= 0);
+
+ if (extension_id >= rel->extension_state_allocated)
+ return NULL;
+
+ return rel->extension_state[extension_id];
+}
+
+/* Functions to store private state into various planner objects */
+extern void SetPlannerGlobalExtensionState(PlannerGlobal *glob,
+ int extension_id,
+ void *opaque);
+extern void SetPlannerInfoExtensionState(PlannerInfo *root, int extension_id,
+ void *opaque);
+extern void SetRelOptInfoExtensionState(RelOptInfo *rel, int extension_id,
+ void *opaque);
+
+#endif
--
2.39.5 (Apple Git-154)
v4-0005-Add-extension_state-member-to-PlannedStmt.patchapplication/octet-stream; name=v4-0005-Add-extension_state-member-to-PlannedStmt.patchDownload
From a9757078ede132a93a746eece0b0b605b19fb8af Mon Sep 17 00:00:00 2001
From: Robert Haas <rhaas@postgresql.org>
Date: Mon, 25 Aug 2025 14:29:02 -0400
Subject: [PATCH v4 5/6] Add extension_state member to PlannedStmt.
Extensions can stash data computed at plan time into this list using
planner_shutdown_hook (or perhaps other mechanisms) and then access
it from any code that has access to the PlannedStmt (such as explain
hooks), allowing for extensible debugging and instrumentation of
plans.
---
src/include/nodes/plannodes.h | 9 +++++++++
1 file changed, 9 insertions(+)
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index 3908847e3bf..afc00228396 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -149,6 +149,15 @@ typedef struct PlannedStmt
/* non-null if this is utility stmt */
Node *utilityStmt;
+ /*
+ * DefElem objects added by extensions, e.g. using planner_shutdown_hook
+ *
+ * Set each DefElem's defname to the name of the plugin or extension, and
+ * the argument to a tree of nodes that all have copy and read/write
+ * support.
+ */
+ List *extension_state;
+
/* statement location in source string (copied from Query) */
/* start location, or -1 if unknown */
ParseLoc stmt_location;
--
2.39.5 (Apple Git-154)
v4-0006-not-for-commit-count-distinct-joinrels-and-joinre.patchapplication/octet-stream; name=v4-0006-not-for-commit-count-distinct-joinrels-and-joinre.patchDownload
From 6291ca3a75fefad7578294e5b7976cb8d503186c Mon Sep 17 00:00:00 2001
From: Robert Haas <rhaas@postgresql.org>
Date: Mon, 25 Aug 2025 15:22:57 -0400
Subject: [PATCH v4 6/6] not for commit: count distinct joinrels and joinrel
planning attempts
---
.../expected/pg_overexplain.out | 22 ++-
contrib/pg_overexplain/pg_overexplain.c | 125 ++++++++++++++++++
2 files changed, 142 insertions(+), 5 deletions(-)
diff --git a/contrib/pg_overexplain/expected/pg_overexplain.out b/contrib/pg_overexplain/expected/pg_overexplain.out
index 55d34666d87..48251298f05 100644
--- a/contrib/pg_overexplain/expected/pg_overexplain.out
+++ b/contrib/pg_overexplain/expected/pg_overexplain.out
@@ -38,7 +38,9 @@ EXPLAIN (DEBUG) SELECT 1;
Relation OIDs: none
Executor Parameter Types: none
Parse Location: 0 to end
-(11 rows)
+ Total Joinrel Attempts: 0
+ Distinct Joinrels: 0
+(13 rows)
EXPLAIN (RANGE_TABLE) SELECT 1;
QUERY PLAN
@@ -121,6 +123,8 @@ $$);
Relation OIDs: NNN...
Executor Parameter Types: none
Parse Location: 0 to end
+ Total Joinrel Attempts: 0
+ Distinct Joinrels: 0
RTI 1 (relation, inherited, in-from-clause):
Eref: vegetables (id, name, genus)
Relation: vegetables
@@ -142,7 +146,7 @@ $$);
Relation Kind: relation
Relation Lock Mode: AccessShareLock
Unprunable RTIs: 1 3 4
-(53 rows)
+(55 rows)
-- Test a different output format.
SELECT explain_filter($$
@@ -242,6 +246,8 @@ $$);
<Relation-OIDs>NNN...</Relation-OIDs> +
<Executor-Parameter-Types>none</Executor-Parameter-Types> +
<Parse-Location>0 to end</Parse-Location> +
+ <Total-Joinrel-Attempts>0</Total-Joinrel-Attempts> +
+ <Distinct-Joinrels>0</Distinct-Joinrels> +
</PlannedStmt> +
<Range-Table> +
<Range-Table-Entry> +
@@ -346,7 +352,9 @@ $$);
Relation OIDs: NNN...
Executor Parameter Types: none
Parse Location: 0 to end
-(37 rows)
+ Total Joinrel Attempts: 0
+ Distinct Joinrels: 0
+(39 rows)
SET debug_parallel_query = false;
RESET enable_seqscan;
@@ -374,7 +382,9 @@ $$);
Relation OIDs: NNN...
Executor Parameter Types: 0
Parse Location: 0 to end
-(15 rows)
+ Total Joinrel Attempts: 0
+ Distinct Joinrels: 0
+(17 rows)
-- Create an index, and then attempt to force a nested loop with inner index
-- scan so that we can see parameter-related information. Also, let's try
@@ -438,7 +448,9 @@ $$);
Relation OIDs: NNN...
Executor Parameter Types: 23
Parse Location: 0 to end
-(47 rows)
+ Total Joinrel Attempts: 2
+ Distinct Joinrels: 1
+(49 rows)
RESET enable_hashjoin;
RESET enable_material;
diff --git a/contrib/pg_overexplain/pg_overexplain.c b/contrib/pg_overexplain/pg_overexplain.c
index bd70b6d9d5e..93d2051f4fb 100644
--- a/contrib/pg_overexplain/pg_overexplain.c
+++ b/contrib/pg_overexplain/pg_overexplain.c
@@ -16,6 +16,10 @@
#include "commands/explain_format.h"
#include "commands/explain_state.h"
#include "fmgr.h"
+#include "nodes/makefuncs.h"
+#include "optimizer/extendplan.h"
+#include "optimizer/paths.h"
+#include "optimizer/planner.h"
#include "parser/parsetree.h"
#include "storage/lock.h"
#include "utils/builtins.h"
@@ -32,6 +36,12 @@ typedef struct
bool range_table;
} overexplain_options;
+typedef struct
+{
+ int total_joinrel_attempts;
+ int distinct_joinrel_count;
+} overexplain_plannerglobal;
+
static overexplain_options *overexplain_ensure_options(ExplainState *es);
static void overexplain_debug_handler(ExplainState *es, DefElem *opt,
ParseState *pstate);
@@ -57,9 +67,28 @@ static void overexplain_bitmapset(const char *qlabel, Bitmapset *bms,
static void overexplain_intlist(const char *qlabel, List *list,
ExplainState *es);
+static void overexplain_planner_setup_hook(PlannerGlobal *glob, Query *parse,
+ const char *query_string,
+ double *tuple_fraction,
+ struct ExplainState *es);
+static void overexplain_planner_shutdown_hook(PlannerGlobal *glob,
+ Query *parse,
+ const char *query_string,
+ PlannedStmt *pstmt);
+static void overexplain_set_join_pathlist_hook(PlannerInfo *root,
+ RelOptInfo *joinrel,
+ RelOptInfo *outerrel,
+ RelOptInfo *innerrel,
+ JoinType jointype,
+ JoinPathExtraData *extra);
+
static int es_extension_id;
+static int planner_extension_id = -1;
static explain_per_node_hook_type prev_explain_per_node_hook;
static explain_per_plan_hook_type prev_explain_per_plan_hook;
+static planner_setup_hook_type prev_planner_setup_hook;
+static planner_shutdown_hook_type prev_planner_shutdown_hook;
+static set_join_pathlist_hook_type prev_set_join_pathlist_hook;
/*
* Initialization we do when this module is loaded.
@@ -70,6 +99,9 @@ _PG_init(void)
/* Get an ID that we can use to cache data in an ExplainState. */
es_extension_id = GetExplainExtensionId("pg_overexplain");
+ /* Get an ID that we can use to cache data in the planner. */
+ planner_extension_id = GetPlannerExtensionId("pg_overexplain");
+
/* Register the new EXPLAIN options implemented by this module. */
RegisterExtensionExplainOption("debug", overexplain_debug_handler);
RegisterExtensionExplainOption("range_table",
@@ -80,6 +112,16 @@ _PG_init(void)
explain_per_node_hook = overexplain_per_node_hook;
prev_explain_per_plan_hook = explain_per_plan_hook;
explain_per_plan_hook = overexplain_per_plan_hook;
+
+ /* Example of planner_setup_hook/planner_shutdown_hook use */
+ prev_planner_setup_hook = planner_setup_hook;
+ planner_setup_hook = overexplain_planner_setup_hook;
+ prev_planner_shutdown_hook = planner_shutdown_hook;
+ planner_shutdown_hook = overexplain_planner_shutdown_hook;
+
+ /* Support for above example */
+ prev_set_join_pathlist_hook = set_join_pathlist_hook;
+ set_join_pathlist_hook = overexplain_set_join_pathlist_hook;
}
/*
@@ -381,6 +423,29 @@ overexplain_debug(PlannedStmt *plannedstmt, ExplainState *es)
plannedstmt->stmt_len),
es);
+ {
+ DefElem *elem = NULL;
+
+ foreach_node(DefElem, de, plannedstmt->extension_state)
+ {
+ if (strcmp(de->defname, "pg_overexplain") == 0)
+ {
+ elem = de;
+ break;
+ }
+ }
+
+ if (elem != NULL)
+ {
+ List *l = castNode(List, elem->arg);
+
+ ExplainPropertyInteger("Total Joinrel Attempts", NULL,
+ intVal(linitial(l)), es);
+ ExplainPropertyInteger("Distinct Joinrels", NULL,
+ intVal(lsecond(l)), es);
+ }
+ }
+
/* Done with this group. */
if (es->format == EXPLAIN_FORMAT_TEXT)
es->indent--;
@@ -784,3 +849,63 @@ overexplain_intlist(const char *qlabel, List *list, ExplainState *es)
pfree(buf.data);
}
+
+static void
+overexplain_planner_setup_hook(PlannerGlobal *glob, Query *parse,
+ const char *query_string,
+ double *tuple_fraction,
+ struct ExplainState *es)
+{
+ overexplain_options *options;
+ overexplain_plannerglobal *g;
+
+ if (es != NULL)
+ {
+ options = GetExplainExtensionState(es, es_extension_id);
+ if (options != NULL && options->debug)
+ {
+ g = palloc0_object(overexplain_plannerglobal);
+ SetPlannerGlobalExtensionState(glob, planner_extension_id, g);
+ }
+ }
+}
+
+static void
+overexplain_planner_shutdown_hook(PlannerGlobal *glob, Query *parse,
+ const char *query_string, PlannedStmt *pstmt)
+{
+ overexplain_plannerglobal *g;
+ DefElem *elem;
+ List *l;
+
+ g = GetPlannerGlobalExtensionState(glob, planner_extension_id);
+ if (g != NULL)
+ {
+ l = list_make2(makeInteger(g->total_joinrel_attempts),
+ makeInteger(g->distinct_joinrel_count));
+ elem = makeDefElem("pg_overexplain", (Node *) l, -1);
+ pstmt->extension_state = lappend(pstmt->extension_state, elem);
+ }
+}
+
+static void
+overexplain_set_join_pathlist_hook(PlannerInfo *root, RelOptInfo *joinrel,
+ RelOptInfo *outerrel, RelOptInfo *innerrel,
+ JoinType jointype, JoinPathExtraData *extra)
+{
+ overexplain_plannerglobal *g;
+
+ g = GetPlannerGlobalExtensionState(root->glob, planner_extension_id);
+
+ if (g != NULL)
+ {
+ g->total_joinrel_attempts++;
+
+ if (GetRelOptInfoExtensionState(joinrel, planner_extension_id) == NULL)
+ {
+ g->distinct_joinrel_count++;
+ /* set any non-NULL value to avoid double-counting */
+ SetRelOptInfoExtensionState(joinrel, planner_extension_id, g);
+ }
+ }
+}
--
2.39.5 (Apple Git-154)
On 13/9/2025 02:16, Robert Haas wrote:
On Fri, Sep 12, 2025 at 4:34 PM Melanie Plageman
<melanieplageman@gmail.com> wrote:On Tue, Aug 26, 2025 at 4:58 AM Andrei Lepikhov <lepihov@gmail.com> wrote:
1. Why is it necessary to maintain the GetExplainExtensionId and
GetPlannerExtensionId routines? It seems that using a single
extension_id (related to the order of the library inside the
file_scanner) is more transparent and more straightforward if necessary.But this wouldn't work for in-core use cases like GEQO, right? Also,
how would it work if there are multiple "extensions" in the same .so
file?
As I see, the core has never utilised extensibility machinery and
implemented separate fields/hooks for personal needs (FDW is a suitable
example). And I think there are reasons for that. Not the last one, I
guess, security issues.
I have never seen cases with multiple extensions in the same module. I
wonder what the reason is for doing this and why the core should support
it?>
We probably don't want to all extensions on any topic to be allocating
extension IDs from the same space, because it's used as a list index
and we don't want to have to null-pad lists excessively. Combining the
explain and planner cases wouldn't be too much of a stretch, perhaps,
but it's also not really costing us anything to have separate IDs for
those cases.
Yes, but it costs extension developers to complicate the code.
Considering that extensions, implementing planner tricks usually want to
show the user (on an EXPLAIN request) how they impacted the query plan,
I guess it makes sense to suggest the same ID.
But I still vote against extension_id in the planner. The main reason
for me to let go such a solution in EXPLAIN was the 'settings' option,
because extensions may fight for a 'nice' name. But each extension has a
native ID - its personal name, isn't it?>
2. Why does the extensibility approach in 0001 differ from that in 0004?
I can imagine it is all about limiting extensions, but anyway, a module
has access to PlannerInfo, PlannerGlobal, etc. So, this machinery looks
a little redundant, doesn't it?What do you mean that the extensibility approach differs? Like that
the type of extension_state is different?
PlannedStmt in 0004 has an extension list that should contain DefElem
nodes. However, the optimiser nodes use a different approach: the
extension developer must operate with an ID allocation. I propose using
a unified way for extensibility - a list with DefElem, uniquely
identified by module name, looks much more flexible.>
I suspect the question here is about why not use the
index-by-planner-extension-ID approach for 0004. That could maybe
work, but here everything has to be a Node, so I feel like it would be
more contorted than the existing cases.
I tried to highlight here that 0004 looks better and more universal than
0001.
--
regards, Andrei Lepikhov
On Tue, Sep 16, 2025 at 4:12 PM Robert Haas <robertmhaas@gmail.com> wrote:
On Fri, Sep 5, 2025 at 4:19 PM Robert Haas <robertmhaas@gmail.com> wrote:
While mulling this over, I realized that this only works if you don't
mind propagating information into the final plan regardless without
knowing whether or not EXPLAIN was actually used. That's pretty sad,[...]
The simplest idea that comes to mind for me is to make pg_plan_query()
take an ExplainState * argument and pass it through to planner().Here's a new version that implements this idea and also cleans up a
few points that Melanie noted.
I think that was rebased over a patch I inadvertently committed to my
local master branch. Trying again.
--
Robert Haas
EDB: http://www.enterprisedb.com
Attachments:
v5-0001-Allow-private-state-in-certain-planner-data-struc.patchapplication/octet-stream; name=v5-0001-Allow-private-state-in-certain-planner-data-struc.patchDownload
From f364769028ba4b0b2ea5c4ab0ff51ff004c3c547 Mon Sep 17 00:00:00 2001
From: Robert Haas <rhaas@postgresql.org>
Date: Mon, 18 Aug 2025 16:11:10 -0400
Subject: [PATCH v5 1/6] Allow private state in certain planner data
structures.
Extension that make extensive use of planner hooks may want to
coordinate their efforts, for example to avoid duplicate computation,
but that's currently difficult because there's no really good way to
pass data between different hooks.
To make that easier, allow for storage of extension-managed private
state in PlannerGlobal, PlannerInfo, and RelOptInfo, along very
similar lines to what we have permitted for ExplainState since commit
c65bc2e1d14a2d4daed7c1921ac518f2c5ac3d17.
---
src/backend/optimizer/util/Makefile | 1 +
src/backend/optimizer/util/extendplan.c | 181 ++++++++++++++++++++++++
src/backend/optimizer/util/meson.build | 1 +
src/include/nodes/pathnodes.h | 12 ++
src/include/optimizer/extendplan.h | 72 ++++++++++
5 files changed, 267 insertions(+)
create mode 100644 src/backend/optimizer/util/extendplan.c
create mode 100644 src/include/optimizer/extendplan.h
diff --git a/src/backend/optimizer/util/Makefile b/src/backend/optimizer/util/Makefile
index 4fb115cb118..308730f392e 100644
--- a/src/backend/optimizer/util/Makefile
+++ b/src/backend/optimizer/util/Makefile
@@ -14,6 +14,7 @@ include $(top_builddir)/src/Makefile.global
OBJS = \
appendinfo.o \
+ extendplan.o \
clauses.o \
inherit.o \
joininfo.o \
diff --git a/src/backend/optimizer/util/extendplan.c b/src/backend/optimizer/util/extendplan.c
new file mode 100644
index 00000000000..55c26cf8411
--- /dev/null
+++ b/src/backend/optimizer/util/extendplan.c
@@ -0,0 +1,181 @@
+/*-------------------------------------------------------------------------
+ *
+ * extendplan.c
+ * Extend core planner objects with additional private state
+ *
+ * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994-5, Regents of the University of California
+ *
+ * The interfaces defined in this file make it possible for loadable
+ * modules to store their own private state inside of key planner data
+ * structures -- specifically, the PlannerGlobal, PlannerInfo, and
+ * RelOptInfo structures. This can make it much easier to write
+ * reasonably efficient planner extensions; for instance, code that
+ * uses set_join_pathlist_hook can arrange to compute a key intermediate
+ * result once per joinrel rather than on every call.
+ *
+ * IDENTIFICATION
+ * src/backend/optimizer/util/extendplan.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "optimizer/extendplan.h"
+#include "port/pg_bitutils.h"
+#include "utils/memutils.h"
+#include "utils/palloc.h"
+
+static const char **PlannerExtensionNameArray = NULL;
+static int PlannerExtensionNamesAssigned = 0;
+static int PlannerExtensionNamesAllocated = 0;
+
+/*
+ * Map the name of a planner extension to an integer ID.
+ *
+ * Within the lifetime of a particular backend, the same name will be mapped
+ * to the same ID every time. IDs are not stable across backends. Use the ID
+ * that you get from this function to call the remaining functions in this
+ * file.
+ */
+int
+GetPlannerExtensionId(const char *extension_name)
+{
+ /* Search for an existing extension by this name; if found, return ID. */
+ for (int i = 0; i < PlannerExtensionNamesAssigned; ++i)
+ if (strcmp(PlannerExtensionNameArray[i], extension_name) == 0)
+ return i;
+
+ /* If there is no array yet, create one. */
+ if (PlannerExtensionNameArray == NULL)
+ {
+ PlannerExtensionNamesAllocated = 16;
+ PlannerExtensionNameArray = (const char **)
+ MemoryContextAlloc(TopMemoryContext,
+ PlannerExtensionNamesAllocated
+ * sizeof(char *));
+ }
+
+ /* If there's an array but it's currently full, expand it. */
+ if (PlannerExtensionNamesAssigned >= PlannerExtensionNamesAllocated)
+ {
+ int i = pg_nextpower2_32(PlannerExtensionNamesAssigned + 1);
+
+ PlannerExtensionNameArray = (const char **)
+ repalloc(PlannerExtensionNameArray, i * sizeof(char *));
+ PlannerExtensionNamesAllocated = i;
+ }
+
+ /* Assign and return new ID. */
+ PlannerExtensionNameArray[PlannerExtensionNamesAssigned] = extension_name;
+ return PlannerExtensionNamesAssigned++;
+}
+
+/*
+ * Store extension-specific state into a PlannerGlobal.
+ */
+void
+SetPlannerGlobalExtensionState(PlannerGlobal *glob, int extension_id,
+ void *opaque)
+{
+ Assert(extension_id >= 0);
+
+ /* If there is no array yet, create one. */
+ if (glob->extension_state == NULL)
+ {
+ MemoryContext planner_cxt;
+ Size sz;
+
+ planner_cxt = GetMemoryChunkContext(glob);
+ glob->extension_state_allocated = 4;
+ sz = glob->extension_state_allocated * sizeof(void *);
+ glob->extension_state = MemoryContextAllocZero(planner_cxt, sz);
+ }
+
+ /* If there's an array but it's currently full, expand it. */
+ if (extension_id >= glob->extension_state_allocated)
+ {
+ int i;
+
+ i = pg_nextpower2_32(glob->extension_state_allocated + 1);
+ glob->extension_state = (void **)
+ repalloc0(glob->extension_state,
+ glob->extension_state_allocated * sizeof(void *),
+ i * sizeof(void *));
+ glob->extension_state_allocated = i;
+ }
+
+ glob->extension_state[extension_id] = opaque;
+}
+
+/*
+ * Store extension-specific state into a PlannerInfo.
+ */
+void
+SetPlannerInfoExtensionState(PlannerInfo *root, int extension_id,
+ void *opaque)
+{
+ Assert(extension_id >= 0);
+
+ /* If there is no array yet, create one. */
+ if (root->extension_state == NULL)
+ {
+ Size sz;
+
+ root->extension_state_allocated = 4;
+ sz = root->extension_state_allocated * sizeof(void *);
+ root->extension_state = MemoryContextAllocZero(root->planner_cxt, sz);
+ }
+
+ /* If there's an array but it's currently full, expand it. */
+ if (extension_id >= root->extension_state_allocated)
+ {
+ int i;
+
+ i = pg_nextpower2_32(root->extension_state_allocated + 1);
+ root->extension_state = (void **)
+ repalloc0(root->extension_state,
+ root->extension_state_allocated * sizeof(void *),
+ i * sizeof(void *));
+ root->extension_state_allocated = i;
+ }
+
+ root->extension_state[extension_id] = opaque;
+}
+
+/*
+ * Store extension-specific state into a RelOptInfo.
+ */
+void
+SetRelOptInfoExtensionState(RelOptInfo *rel, int extension_id,
+ void *opaque)
+{
+ Assert(extension_id >= 0);
+
+ /* If there is no array yet, create one. */
+ if (rel->extension_state == NULL)
+ {
+ MemoryContext planner_cxt;
+ Size sz;
+
+ planner_cxt = GetMemoryChunkContext(rel);
+ rel->extension_state_allocated = 4;
+ sz = rel->extension_state_allocated * sizeof(void *);
+ rel->extension_state = MemoryContextAllocZero(planner_cxt, sz);
+ }
+
+ /* If there's an array but it's currently full, expand it. */
+ if (extension_id >= rel->extension_state_allocated)
+ {
+ int i;
+
+ i = pg_nextpower2_32(rel->extension_state_allocated + 1);
+ rel->extension_state = (void **)
+ repalloc0(rel->extension_state,
+ rel->extension_state_allocated * sizeof(void *),
+ i * sizeof(void *));
+ rel->extension_state_allocated = i;
+ }
+
+ rel->extension_state[extension_id] = opaque;
+}
diff --git a/src/backend/optimizer/util/meson.build b/src/backend/optimizer/util/meson.build
index b3bf913d096..f71f56e37a1 100644
--- a/src/backend/optimizer/util/meson.build
+++ b/src/backend/optimizer/util/meson.build
@@ -3,6 +3,7 @@
backend_sources += files(
'appendinfo.c',
'clauses.c',
+ 'extendplan.c',
'inherit.c',
'joininfo.c',
'orclauses.c',
diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h
index b12a2508d8c..5cf23cba596 100644
--- a/src/include/nodes/pathnodes.h
+++ b/src/include/nodes/pathnodes.h
@@ -182,6 +182,10 @@ typedef struct PlannerGlobal
/* hash table for NOT NULL attnums of relations */
struct HTAB *rel_notnullatts_hash pg_node_attr(read_write_ignore);
+
+ /* extension state */
+ void **extension_state pg_node_attr(read_write_ignore);
+ int extension_state_allocated;
} PlannerGlobal;
/* macro for fetching the Plan associated with a SubPlan node */
@@ -580,6 +584,10 @@ struct PlannerInfo
/* PartitionPruneInfos added in this query's plan. */
List *partPruneInfos;
+
+ /* extension state */
+ void **extension_state pg_node_attr(read_write_ignore);
+ int extension_state_allocated;
};
@@ -1091,6 +1099,10 @@ typedef struct RelOptInfo
List **partexprs pg_node_attr(read_write_ignore);
/* Nullable partition key expressions */
List **nullable_partexprs pg_node_attr(read_write_ignore);
+
+ /* extension state */
+ void **extension_state pg_node_attr(read_write_ignore);
+ int extension_state_allocated;
} RelOptInfo;
/*
diff --git a/src/include/optimizer/extendplan.h b/src/include/optimizer/extendplan.h
new file mode 100644
index 00000000000..de9618761dd
--- /dev/null
+++ b/src/include/optimizer/extendplan.h
@@ -0,0 +1,72 @@
+/*-------------------------------------------------------------------------
+ *
+ * extendplan.h
+ * Extend core planner objects with additional private state
+ *
+ *
+ * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/optimizer/extendplan.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef EXTENDPLAN_H
+#define EXTENDPLAN_H
+
+#include "nodes/pathnodes.h"
+
+extern int GetPlannerExtensionId(const char *extension_name);
+
+/*
+ * Get extension-specific state from a PlannerGlobal.
+ */
+static inline void *
+GetPlannerGlobalExtensionState(PlannerGlobal *glob, int extension_id)
+{
+ Assert(extension_id >= 0);
+
+ if (extension_id >= glob->extension_state_allocated)
+ return NULL;
+
+ return glob->extension_state[extension_id];
+}
+
+/*
+ * Get extension-specific state from a PlannerInfo.
+ */
+static inline void *
+GetPlannerInfoExtensionState(PlannerInfo *root, int extension_id)
+{
+ Assert(extension_id >= 0);
+
+ if (extension_id >= root->extension_state_allocated)
+ return NULL;
+
+ return root->extension_state[extension_id];
+}
+
+/*
+ * Get extension-specific state from a PlannerInfo.
+ */
+static inline void *
+GetRelOptInfoExtensionState(RelOptInfo *rel, int extension_id)
+{
+ Assert(extension_id >= 0);
+
+ if (extension_id >= rel->extension_state_allocated)
+ return NULL;
+
+ return rel->extension_state[extension_id];
+}
+
+/* Functions to store private state into various planner objects */
+extern void SetPlannerGlobalExtensionState(PlannerGlobal *glob,
+ int extension_id,
+ void *opaque);
+extern void SetPlannerInfoExtensionState(PlannerInfo *root, int extension_id,
+ void *opaque);
+extern void SetRelOptInfoExtensionState(RelOptInfo *rel, int extension_id,
+ void *opaque);
+
+#endif
--
2.39.5 (Apple Git-154)
v5-0005-Add-extension_state-member-to-PlannedStmt.patchapplication/octet-stream; name=v5-0005-Add-extension_state-member-to-PlannedStmt.patchDownload
From 0f3e44ab990436bedf74fcaede4105efbb976b5c Mon Sep 17 00:00:00 2001
From: Robert Haas <rhaas@postgresql.org>
Date: Mon, 25 Aug 2025 14:29:02 -0400
Subject: [PATCH v5 5/6] Add extension_state member to PlannedStmt.
Extensions can stash data computed at plan time into this list using
planner_shutdown_hook (or perhaps other mechanisms) and then access
it from any code that has access to the PlannedStmt (such as explain
hooks), allowing for extensible debugging and instrumentation of
plans.
---
src/include/nodes/plannodes.h | 9 +++++++++
1 file changed, 9 insertions(+)
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index 29d7732d6a0..326c3f7fe6f 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -149,6 +149,15 @@ typedef struct PlannedStmt
/* non-null if this is utility stmt */
Node *utilityStmt;
+ /*
+ * DefElem objects added by extensions, e.g. using planner_shutdown_hook
+ *
+ * Set each DefElem's defname to the name of the plugin or extension, and
+ * the argument to a tree of nodes that all have copy and read/write
+ * support.
+ */
+ List *extension_state;
+
/* statement location in source string (copied from Query) */
/* start location, or -1 if unknown */
ParseLoc stmt_location;
--
2.39.5 (Apple Git-154)
v5-0002-Remove-PlannerInfo-s-join_search_private-method.patchapplication/octet-stream; name=v5-0002-Remove-PlannerInfo-s-join_search_private-method.patchDownload
From eb88b611b936780db66bd66afbd56a9793a07e25 Mon Sep 17 00:00:00 2001
From: Robert Haas <rhaas@postgresql.org>
Date: Wed, 20 Aug 2025 15:10:52 -0400
Subject: [PATCH v5 2/6] Remove PlannerInfo's join_search_private method.
Instead, use the new mechanism that allows planner extensions to store
private state inside a PlannerInfo, treating GEQO as an in-core planner
extension. This is a useful test of the new facility, and also buys
back a few bytes of storage.
To make this work, we must remove innerrel_is_unique_ext's hack of
testing whether join_search_private is set as a proxy for whether
the join search might be retried. Add a flag that extensions can
use to explicitly signal their intentions instead.
---
src/backend/optimizer/geqo/geqo_eval.c | 2 +-
src/backend/optimizer/geqo/geqo_main.c | 12 ++++++++++--
src/backend/optimizer/geqo/geqo_random.c | 7 +++----
src/backend/optimizer/plan/analyzejoins.c | 9 +++------
src/backend/optimizer/plan/planner.c | 1 +
src/backend/optimizer/prep/prepjointree.c | 1 +
src/include/nodes/pathnodes.h | 5 ++---
src/include/optimizer/geqo.h | 10 +++++++++-
8 files changed, 30 insertions(+), 17 deletions(-)
diff --git a/src/backend/optimizer/geqo/geqo_eval.c b/src/backend/optimizer/geqo/geqo_eval.c
index f07d1dc8ac6..7fcb1aa70d1 100644
--- a/src/backend/optimizer/geqo/geqo_eval.c
+++ b/src/backend/optimizer/geqo/geqo_eval.c
@@ -162,7 +162,7 @@ geqo_eval(PlannerInfo *root, Gene *tour, int num_gene)
RelOptInfo *
gimme_tree(PlannerInfo *root, Gene *tour, int num_gene)
{
- GeqoPrivateData *private = (GeqoPrivateData *) root->join_search_private;
+ GeqoPrivateData *private = GetGeqoPrivateData(root);
List *clumps;
int rel_count;
diff --git a/src/backend/optimizer/geqo/geqo_main.c b/src/backend/optimizer/geqo/geqo_main.c
index 38402ce58db..0064556087a 100644
--- a/src/backend/optimizer/geqo/geqo_main.c
+++ b/src/backend/optimizer/geqo/geqo_main.c
@@ -47,6 +47,8 @@ int Geqo_generations;
double Geqo_selection_bias;
double Geqo_seed;
+/* GEQO is treated as an in-core planner extension */
+int Geqo_planner_extension_id = -1;
static int gimme_pool_size(int nr_rel);
static int gimme_number_generations(int pool_size);
@@ -98,10 +100,16 @@ geqo(PlannerInfo *root, int number_of_rels, List *initial_rels)
int mutations = 0;
#endif
+ if (Geqo_planner_extension_id < 0)
+ Geqo_planner_extension_id = GetPlannerExtensionId("geqo");
+
/* set up private information */
- root->join_search_private = &private;
+ SetPlannerInfoExtensionState(root, Geqo_planner_extension_id, &private);
private.initial_rels = initial_rels;
+/* inform core planner that we may replan */
+ root->assumeReplanning = true;
+
/* initialize private number generator */
geqo_set_seed(root, Geqo_seed);
@@ -304,7 +312,7 @@ geqo(PlannerInfo *root, int number_of_rels, List *initial_rels)
free_pool(root, pool);
/* ... clear root pointer to our private storage */
- root->join_search_private = NULL;
+ SetPlannerInfoExtensionState(root, Geqo_planner_extension_id, NULL);
return best_rel;
}
diff --git a/src/backend/optimizer/geqo/geqo_random.c b/src/backend/optimizer/geqo/geqo_random.c
index 6c7a411f69f..46d28baa2e6 100644
--- a/src/backend/optimizer/geqo/geqo_random.c
+++ b/src/backend/optimizer/geqo/geqo_random.c
@@ -15,11 +15,10 @@
#include "optimizer/geqo_random.h"
-
void
geqo_set_seed(PlannerInfo *root, double seed)
{
- GeqoPrivateData *private = (GeqoPrivateData *) root->join_search_private;
+ GeqoPrivateData *private = GetGeqoPrivateData(root);
pg_prng_fseed(&private->random_state, seed);
}
@@ -27,7 +26,7 @@ geqo_set_seed(PlannerInfo *root, double seed)
double
geqo_rand(PlannerInfo *root)
{
- GeqoPrivateData *private = (GeqoPrivateData *) root->join_search_private;
+ GeqoPrivateData *private = GetGeqoPrivateData(root);
return pg_prng_double(&private->random_state);
}
@@ -35,7 +34,7 @@ geqo_rand(PlannerInfo *root)
int
geqo_randint(PlannerInfo *root, int upper, int lower)
{
- GeqoPrivateData *private = (GeqoPrivateData *) root->join_search_private;
+ GeqoPrivateData *private = GetGeqoPrivateData(root);
/*
* In current usage, "lower" is never negative so we can just use
diff --git a/src/backend/optimizer/plan/analyzejoins.c b/src/backend/optimizer/plan/analyzejoins.c
index 2a3dea88a94..6a3c030e8ef 100644
--- a/src/backend/optimizer/plan/analyzejoins.c
+++ b/src/backend/optimizer/plan/analyzejoins.c
@@ -1425,17 +1425,14 @@ innerrel_is_unique_ext(PlannerInfo *root,
*
* However, in normal planning mode, caching this knowledge is totally
* pointless; it won't be queried again, because we build up joinrels
- * from smaller to larger. It is useful in GEQO mode, where the
- * knowledge can be carried across successive planning attempts; and
- * it's likely to be useful when using join-search plugins, too. Hence
- * cache when join_search_private is non-NULL. (Yeah, that's a hack,
- * but it seems reasonable.)
+ * from smaller to larger. It's only useful when using GEQO or
+ * another planner extension that attempts planning multiple times.
*
* Also, allow callers to override that heuristic and force caching;
* that's useful for reduce_unique_semijoins, which calls here before
* the normal join search starts.
*/
- if (force_cache || root->join_search_private)
+ if (force_cache || root->assumeReplanning)
{
old_context = MemoryContextSwitchTo(root->planner_cxt);
innerrel->non_unique_for_rels =
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index 41bd8353430..9de39da1757 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -703,6 +703,7 @@ subquery_planner(PlannerGlobal *glob, Query *parse, PlannerInfo *parent_root,
root->hasAlternativeSubPlans = false;
root->placeholdersFrozen = false;
root->hasRecursion = hasRecursion;
+ root->assumeReplanning = false;
if (hasRecursion)
root->wt_param_id = assign_special_exec_param(root);
else
diff --git a/src/backend/optimizer/prep/prepjointree.c b/src/backend/optimizer/prep/prepjointree.c
index 35e8d3c183b..4075f7519ca 100644
--- a/src/backend/optimizer/prep/prepjointree.c
+++ b/src/backend/optimizer/prep/prepjointree.c
@@ -1383,6 +1383,7 @@ pull_up_simple_subquery(PlannerInfo *root, Node *jtnode, RangeTblEntry *rte,
subroot->qual_security_level = 0;
subroot->placeholdersFrozen = false;
subroot->hasRecursion = false;
+ subroot->assumeReplanning = false;
subroot->wt_param_id = -1;
subroot->non_recursive_path = NULL;
/* We don't currently need a top JoinDomain for the subroot */
diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h
index 5cf23cba596..bc1e0c1b5cc 100644
--- a/src/include/nodes/pathnodes.h
+++ b/src/include/nodes/pathnodes.h
@@ -530,6 +530,8 @@ struct PlannerInfo
bool placeholdersFrozen;
/* true if planning a recursive WITH item */
bool hasRecursion;
+ /* true if a planner extension may replan this subquery */
+ bool assumeReplanning;
/*
* The rangetable index for the RTE_GROUP RTE, or 0 if there is no
@@ -576,9 +578,6 @@ struct PlannerInfo
bool *isAltSubplan pg_node_attr(read_write_ignore);
bool *isUsedSubplan pg_node_attr(read_write_ignore);
- /* optional private data for join_search_hook, e.g., GEQO */
- void *join_search_private pg_node_attr(read_write_ignore);
-
/* Does this query modify any partition key columns? */
bool partColsUpdated;
diff --git a/src/include/optimizer/geqo.h b/src/include/optimizer/geqo.h
index 9f8e0f337aa..3f4872e25e3 100644
--- a/src/include/optimizer/geqo.h
+++ b/src/include/optimizer/geqo.h
@@ -24,6 +24,7 @@
#include "common/pg_prng.h"
#include "nodes/pathnodes.h"
+#include "optimizer/extendplan.h"
#include "optimizer/geqo_gene.h"
@@ -62,6 +63,8 @@ extern PGDLLIMPORT int Geqo_generations; /* 1 .. inf, or 0 to use default */
extern PGDLLIMPORT double Geqo_selection_bias;
+extern PGDLLIMPORT int Geqo_planner_extension_id;
+
#define DEFAULT_GEQO_SELECTION_BIAS 2.0
#define MIN_GEQO_SELECTION_BIAS 1.5
#define MAX_GEQO_SELECTION_BIAS 2.0
@@ -70,7 +73,7 @@ extern PGDLLIMPORT double Geqo_seed; /* 0 .. 1 */
/*
- * Private state for a GEQO run --- accessible via root->join_search_private
+ * Private state for a GEQO run --- accessible via GetGeqoPrivateData
*/
typedef struct
{
@@ -78,6 +81,11 @@ typedef struct
pg_prng_state random_state; /* PRNG state */
} GeqoPrivateData;
+static inline GeqoPrivateData *
+GetGeqoPrivateData(PlannerInfo *root)
+{
+ return GetPlannerInfoExtensionState(root, Geqo_planner_extension_id);
+}
/* routines in geqo_main.c */
extern RelOptInfo *geqo(PlannerInfo *root,
--
2.39.5 (Apple Git-154)
v5-0004-Add-planner_setup_hook-and-planner_shutdown_hook.patchapplication/octet-stream; name=v5-0004-Add-planner_setup_hook-and-planner_shutdown_hook.patchDownload
From ec8779119aeccc560f2c891102f559814a3a3963 Mon Sep 17 00:00:00 2001
From: Robert Haas <rhaas@postgresql.org>
Date: Mon, 25 Aug 2025 13:04:50 -0400
Subject: [PATCH v5 4/6] Add planner_setup_hook and planner_shutdown_hook.
These hooks allow plugins to get control at the earliest point at
which the PlannerGlobal object is fully initialized, and then just
before it gets destroyed. This is useful in combination with the
extendable plan state facilities (see extendplan.h) and perhaps for
other purposes as well.
---
src/backend/optimizer/plan/planner.c | 14 ++++++++++++++
src/include/optimizer/planner.h | 13 +++++++++++++
2 files changed, 27 insertions(+)
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index 205d8886a2a..ba69098e0df 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -73,6 +73,12 @@ bool enable_distinct_reordering = true;
/* Hook for plugins to get control in planner() */
planner_hook_type planner_hook = NULL;
+/* Hook for plugins to get control after PlannerGlobal is initialized */
+planner_setup_hook_type planner_setup_hook = NULL;
+
+/* Hook for plugins to get control before PlannerGlobal is discarded */
+planner_shutdown_hook_type planner_shutdown_hook = NULL;
+
/* Hook for plugins to get control when grouping_planner() plans upper rels */
create_upper_paths_hook_type create_upper_paths_hook = NULL;
@@ -440,6 +446,10 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions,
tuple_fraction = 0.0;
}
+ /* Allow plugins to take control after we've initialized "glob" */
+ if (planner_setup_hook)
+ (*planner_setup_hook) (glob, parse, query_string, &tuple_fraction, es);
+
/* primary planning entry point (may recurse for subqueries) */
root = subquery_planner(glob, parse, NULL, false, tuple_fraction, NULL);
@@ -621,6 +631,10 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions,
if (glob->partition_directory != NULL)
DestroyPartitionDirectory(glob->partition_directory);
+ /* Allow plugins to take control before we discard "glob" */
+ if (planner_shutdown_hook)
+ (*planner_shutdown_hook) (glob, parse, query_string, result);
+
return result;
}
diff --git a/src/include/optimizer/planner.h b/src/include/optimizer/planner.h
index 6c07711913d..6b893f76444 100644
--- a/src/include/optimizer/planner.h
+++ b/src/include/optimizer/planner.h
@@ -33,6 +33,19 @@ typedef PlannedStmt *(*planner_hook_type) (Query *parse,
struct ExplainState *es);
extern PGDLLIMPORT planner_hook_type planner_hook;
+/* Hook for plugins to get control after PlannerGlobal is initialized */
+typedef void (*planner_setup_hook_type) (PlannerGlobal *glob, Query *parse,
+ const char *query_string,
+ double *tuple_fraction,
+ struct ExplainState *es);
+extern PGDLLIMPORT planner_setup_hook_type planner_setup_hook;
+
+/* Hook for plugins to get control before PlannerGlobal is discarded */
+typedef void (*planner_shutdown_hook_type) (PlannerGlobal *glob, Query *parse,
+ const char *query_string,
+ PlannedStmt *pstmt);
+extern PGDLLIMPORT planner_shutdown_hook_type planner_shutdown_hook;
+
/* Hook for plugins to get control when grouping_planner() plans upper rels */
typedef void (*create_upper_paths_hook_type) (PlannerInfo *root,
UpperRelationKind stage,
--
2.39.5 (Apple Git-154)
v5-0003-Add-ExplainState-argument-to-pg_plan_query-and-pl.patchapplication/octet-stream; name=v5-0003-Add-ExplainState-argument-to-pg_plan_query-and-pl.patchDownload
From d54722ba6e94aebe142be25055f73dfdc002654a Mon Sep 17 00:00:00 2001
From: Robert Haas <rhaas@postgresql.org>
Date: Tue, 16 Sep 2025 13:13:24 -0400
Subject: [PATCH v5 3/6] Add ExplainState argument to pg_plan_query() and
planner().
This allows extensions to have access to any data they've stored
in the ExplainState during planning. Unfortunately, it won't help
with EXPLAIN EXECUTE is used, but since that case is less common,
this still seems like an improvement.
---
contrib/pg_stat_statements/pg_stat_statements.c | 14 ++++++++------
src/backend/commands/copyto.c | 2 +-
src/backend/commands/createas.c | 2 +-
src/backend/commands/explain.c | 2 +-
src/backend/commands/matview.c | 2 +-
src/backend/commands/portalcmds.c | 3 ++-
src/backend/optimizer/plan/planner.c | 10 ++++++----
src/backend/tcop/postgres.c | 7 ++++---
src/include/optimizer/optimizer.h | 6 +++++-
src/include/optimizer/planner.h | 9 +++++++--
src/include/tcop/tcopprot.h | 5 ++++-
src/test/modules/delay_execution/delay_execution.c | 7 ++++---
12 files changed, 44 insertions(+), 25 deletions(-)
diff --git a/contrib/pg_stat_statements/pg_stat_statements.c b/contrib/pg_stat_statements/pg_stat_statements.c
index 0bb0f933399..0eb208f58fc 100644
--- a/contrib/pg_stat_statements/pg_stat_statements.c
+++ b/contrib/pg_stat_statements/pg_stat_statements.c
@@ -337,7 +337,8 @@ static void pgss_post_parse_analyze(ParseState *pstate, Query *query,
static PlannedStmt *pgss_planner(Query *parse,
const char *query_string,
int cursorOptions,
- ParamListInfo boundParams);
+ ParamListInfo boundParams,
+ struct ExplainState *es);
static void pgss_ExecutorStart(QueryDesc *queryDesc, int eflags);
static void pgss_ExecutorRun(QueryDesc *queryDesc,
ScanDirection direction,
@@ -893,7 +894,8 @@ static PlannedStmt *
pgss_planner(Query *parse,
const char *query_string,
int cursorOptions,
- ParamListInfo boundParams)
+ ParamListInfo boundParams,
+ struct ExplainState *es)
{
PlannedStmt *result;
@@ -928,10 +930,10 @@ pgss_planner(Query *parse,
{
if (prev_planner_hook)
result = prev_planner_hook(parse, query_string, cursorOptions,
- boundParams);
+ boundParams, es);
else
result = standard_planner(parse, query_string, cursorOptions,
- boundParams);
+ boundParams, es);
}
PG_FINALLY();
{
@@ -977,10 +979,10 @@ pgss_planner(Query *parse,
{
if (prev_planner_hook)
result = prev_planner_hook(parse, query_string, cursorOptions,
- boundParams);
+ boundParams, es);
else
result = standard_planner(parse, query_string, cursorOptions,
- boundParams);
+ boundParams, es);
}
PG_FINALLY();
{
diff --git a/src/backend/commands/copyto.c b/src/backend/commands/copyto.c
index 67b94b91cae..e5781155cdf 100644
--- a/src/backend/commands/copyto.c
+++ b/src/backend/commands/copyto.c
@@ -796,7 +796,7 @@ BeginCopyTo(ParseState *pstate,
/* plan the query */
plan = pg_plan_query(query, pstate->p_sourcetext,
- CURSOR_OPT_PARALLEL_OK, NULL);
+ CURSOR_OPT_PARALLEL_OK, NULL, NULL);
/*
* With row-level security and a user using "COPY relation TO", we
diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c
index dfd2ab8e862..1ccc2e55c64 100644
--- a/src/backend/commands/createas.c
+++ b/src/backend/commands/createas.c
@@ -321,7 +321,7 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt,
/* plan the query */
plan = pg_plan_query(query, pstate->p_sourcetext,
- CURSOR_OPT_PARALLEL_OK, params);
+ CURSOR_OPT_PARALLEL_OK, params, NULL);
/*
* Use a snapshot with an updated command ID to ensure this query sees
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 8345bc0264b..ca64fa5d18a 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -350,7 +350,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, params, es);
INSTR_TIME_SET_CURRENT(planduration);
INSTR_TIME_SUBTRACT(planduration, planstart);
diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index 188e26f0e6e..441de55ac24 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -426,7 +426,7 @@ refresh_matview_datafill(DestReceiver *dest, Query *query,
CHECK_FOR_INTERRUPTS();
/* Plan the query which will generate data for the refresh. */
- plan = pg_plan_query(query, queryString, CURSOR_OPT_PARALLEL_OK, NULL);
+ plan = pg_plan_query(query, queryString, CURSOR_OPT_PARALLEL_OK, NULL, NULL);
/*
* Use a snapshot with an updated command ID to ensure this query sees
diff --git a/src/backend/commands/portalcmds.c b/src/backend/commands/portalcmds.c
index e7c8171c102..ec96c2efcd3 100644
--- a/src/backend/commands/portalcmds.c
+++ b/src/backend/commands/portalcmds.c
@@ -99,7 +99,8 @@ PerformCursorOpen(ParseState *pstate, DeclareCursorStmt *cstmt, ParamListInfo pa
elog(ERROR, "non-SELECT statement in DECLARE CURSOR");
/* Plan the query, applying the specified options */
- plan = pg_plan_query(query, pstate->p_sourcetext, cstmt->options, params);
+ plan = pg_plan_query(query, pstate->p_sourcetext, cstmt->options, params,
+ NULL);
/*
* Create a portal and copy the plan and query string into its memory.
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index 9de39da1757..205d8886a2a 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -291,14 +291,16 @@ static void create_partial_unique_paths(PlannerInfo *root, RelOptInfo *input_rel
*****************************************************************************/
PlannedStmt *
planner(Query *parse, const char *query_string, int cursorOptions,
- ParamListInfo boundParams)
+ ParamListInfo boundParams, ExplainState *es)
{
PlannedStmt *result;
if (planner_hook)
- result = (*planner_hook) (parse, query_string, cursorOptions, boundParams);
+ result = (*planner_hook) (parse, query_string, cursorOptions,
+ boundParams, es);
else
- result = standard_planner(parse, query_string, cursorOptions, boundParams);
+ result = standard_planner(parse, query_string, cursorOptions,
+ boundParams, es);
pgstat_report_plan_id(result->planId, false);
@@ -307,7 +309,7 @@ planner(Query *parse, const char *query_string, int cursorOptions,
PlannedStmt *
standard_planner(Query *parse, const char *query_string, int cursorOptions,
- ParamListInfo boundParams)
+ ParamListInfo boundParams, ExplainState *es)
{
PlannedStmt *result;
PlannerGlobal *glob;
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index d356830f756..7dd75a490aa 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -37,6 +37,7 @@
#include "catalog/pg_type.h"
#include "commands/async.h"
#include "commands/event_trigger.h"
+#include "commands/explain_state.h"
#include "commands/prepare.h"
#include "common/pg_prng.h"
#include "jit/jit.h"
@@ -884,7 +885,7 @@ pg_rewrite_query(Query *query)
*/
PlannedStmt *
pg_plan_query(Query *querytree, const char *query_string, int cursorOptions,
- ParamListInfo boundParams)
+ ParamListInfo boundParams, ExplainState *es)
{
PlannedStmt *plan;
@@ -901,7 +902,7 @@ pg_plan_query(Query *querytree, const char *query_string, int cursorOptions,
ResetUsage();
/* call the optimizer */
- plan = planner(querytree, query_string, cursorOptions, boundParams);
+ plan = planner(querytree, query_string, cursorOptions, boundParams, es);
if (log_planner_stats)
ShowUsage("PLANNER STATISTICS");
@@ -997,7 +998,7 @@ pg_plan_queries(List *querytrees, const char *query_string, int cursorOptions,
else
{
stmt = pg_plan_query(query, query_string, cursorOptions,
- boundParams);
+ boundParams, NULL);
}
stmt_list = lappend(stmt_list, stmt);
diff --git a/src/include/optimizer/optimizer.h b/src/include/optimizer/optimizer.h
index 04878f1f1c2..a8aaefd27e9 100644
--- a/src/include/optimizer/optimizer.h
+++ b/src/include/optimizer/optimizer.h
@@ -24,6 +24,9 @@
#include "nodes/parsenodes.h"
+/* avoid including commands/explain_state.h here */
+struct ExplainState;
+
/*
* We don't want to include nodes/pathnodes.h here, because non-planner
* code should generally treat PlannerInfo as an opaque typedef.
@@ -104,7 +107,8 @@ extern PGDLLIMPORT bool enable_distinct_reordering;
extern PlannedStmt *planner(Query *parse, const char *query_string,
int cursorOptions,
- ParamListInfo boundParams);
+ ParamListInfo boundParams,
+ struct ExplainState *es);
extern Expr *expression_planner(Expr *expr);
extern Expr *expression_planner_with_deps(Expr *expr,
diff --git a/src/include/optimizer/planner.h b/src/include/optimizer/planner.h
index f220e9a270d..6c07711913d 100644
--- a/src/include/optimizer/planner.h
+++ b/src/include/optimizer/planner.h
@@ -22,11 +22,15 @@
#include "nodes/plannodes.h"
+/* avoid including commands/explain_state.h here */
+struct ExplainState;
+
/* Hook for plugins to get control in planner() */
typedef PlannedStmt *(*planner_hook_type) (Query *parse,
const char *query_string,
int cursorOptions,
- ParamListInfo boundParams);
+ ParamListInfo boundParams,
+ struct ExplainState *es);
extern PGDLLIMPORT planner_hook_type planner_hook;
/* Hook for plugins to get control when grouping_planner() plans upper rels */
@@ -40,7 +44,8 @@ extern PGDLLIMPORT create_upper_paths_hook_type create_upper_paths_hook;
extern PlannedStmt *standard_planner(Query *parse, const char *query_string,
int cursorOptions,
- ParamListInfo boundParams);
+ ParamListInfo boundParams,
+ struct ExplainState *es);
extern PlannerInfo *subquery_planner(PlannerGlobal *glob, Query *parse,
PlannerInfo *parent_root,
diff --git a/src/include/tcop/tcopprot.h b/src/include/tcop/tcopprot.h
index a83cc4f4850..90323b10554 100644
--- a/src/include/tcop/tcopprot.h
+++ b/src/include/tcop/tcopprot.h
@@ -20,6 +20,8 @@
#include "utils/guc.h"
#include "utils/queryenvironment.h"
+/* avoid including commands/explain_state.h here */
+struct ExplainState;
extern PGDLLIMPORT CommandDest whereToSendOutput;
extern PGDLLIMPORT const char *debug_query_string;
@@ -63,7 +65,8 @@ extern List *pg_analyze_and_rewrite_withcb(RawStmt *parsetree,
QueryEnvironment *queryEnv);
extern PlannedStmt *pg_plan_query(Query *querytree, const char *query_string,
int cursorOptions,
- ParamListInfo boundParams);
+ ParamListInfo boundParams,
+ struct ExplainState *es);
extern List *pg_plan_queries(List *querytrees, const char *query_string,
int cursorOptions,
ParamListInfo boundParams);
diff --git a/src/test/modules/delay_execution/delay_execution.c b/src/test/modules/delay_execution/delay_execution.c
index 7bc97f84a1c..53c4073fce6 100644
--- a/src/test/modules/delay_execution/delay_execution.c
+++ b/src/test/modules/delay_execution/delay_execution.c
@@ -40,17 +40,18 @@ static planner_hook_type prev_planner_hook = NULL;
/* planner_hook function to provide the desired delay */
static PlannedStmt *
delay_execution_planner(Query *parse, const char *query_string,
- int cursorOptions, ParamListInfo boundParams)
+ int cursorOptions, ParamListInfo boundParams,
+ struct ExplainState *es)
{
PlannedStmt *result;
/* Invoke the planner, possibly via a previous hook user */
if (prev_planner_hook)
result = prev_planner_hook(parse, query_string, cursorOptions,
- boundParams);
+ boundParams, es);
else
result = standard_planner(parse, query_string, cursorOptions,
- boundParams);
+ boundParams, es);
/* If enabled, delay by taking and releasing the specified lock */
if (post_planning_lock_id != 0)
--
2.39.5 (Apple Git-154)
v5-0006-not-for-commit-count-distinct-joinrels-and-joinre.patchapplication/octet-stream; name=v5-0006-not-for-commit-count-distinct-joinrels-and-joinre.patchDownload
From 4228110340d9c5b3d6f0d8a1b51ef8f912a1c412 Mon Sep 17 00:00:00 2001
From: Robert Haas <rhaas@postgresql.org>
Date: Mon, 25 Aug 2025 15:22:57 -0400
Subject: [PATCH v5 6/6] not for commit: count distinct joinrels and joinrel
planning attempts
---
.../expected/pg_overexplain.out | 22 ++-
contrib/pg_overexplain/pg_overexplain.c | 125 ++++++++++++++++++
2 files changed, 142 insertions(+), 5 deletions(-)
diff --git a/contrib/pg_overexplain/expected/pg_overexplain.out b/contrib/pg_overexplain/expected/pg_overexplain.out
index 6de02323d7c..d8d3e36d7f1 100644
--- a/contrib/pg_overexplain/expected/pg_overexplain.out
+++ b/contrib/pg_overexplain/expected/pg_overexplain.out
@@ -38,7 +38,9 @@ EXPLAIN (DEBUG) SELECT 1;
Relation OIDs: none
Executor Parameter Types: none
Parse Location: 0 to end
-(11 rows)
+ Total Joinrel Attempts: 0
+ Distinct Joinrels: 0
+(13 rows)
EXPLAIN (RANGE_TABLE) SELECT 1;
QUERY PLAN
@@ -120,6 +122,8 @@ $$);
Relation OIDs: NNN...
Executor Parameter Types: none
Parse Location: 0 to end
+ Total Joinrel Attempts: 0
+ Distinct Joinrels: 0
RTI 1 (relation, inherited, in-from-clause):
Eref: vegetables (id, name, genus)
Relation: vegetables
@@ -141,7 +145,7 @@ $$);
Relation Kind: relation
Relation Lock Mode: AccessShareLock
Unprunable RTIs: 1 3 4
-(53 rows)
+(55 rows)
-- Test a different output format.
SELECT explain_filter($$
@@ -241,6 +245,8 @@ $$);
<Relation-OIDs>NNN...</Relation-OIDs> +
<Executor-Parameter-Types>none</Executor-Parameter-Types> +
<Parse-Location>0 to end</Parse-Location> +
+ <Total-Joinrel-Attempts>0</Total-Joinrel-Attempts> +
+ <Distinct-Joinrels>0</Distinct-Joinrels> +
</PlannedStmt> +
<Range-Table> +
<Range-Table-Entry> +
@@ -345,7 +351,9 @@ $$);
Relation OIDs: NNN...
Executor Parameter Types: none
Parse Location: 0 to end
-(37 rows)
+ Total Joinrel Attempts: 0
+ Distinct Joinrels: 0
+(39 rows)
SET debug_parallel_query = false;
RESET enable_seqscan;
@@ -373,7 +381,9 @@ $$);
Relation OIDs: NNN...
Executor Parameter Types: 0
Parse Location: 0 to end
-(15 rows)
+ Total Joinrel Attempts: 0
+ Distinct Joinrels: 0
+(17 rows)
-- Create an index, and then attempt to force a nested loop with inner index
-- scan so that we can see parameter-related information. Also, let's try
@@ -437,7 +447,9 @@ $$);
Relation OIDs: NNN...
Executor Parameter Types: 23
Parse Location: 0 to end
-(47 rows)
+ Total Joinrel Attempts: 2
+ Distinct Joinrels: 1
+(49 rows)
RESET enable_hashjoin;
RESET enable_material;
diff --git a/contrib/pg_overexplain/pg_overexplain.c b/contrib/pg_overexplain/pg_overexplain.c
index de824566f8c..e0169fbf7f2 100644
--- a/contrib/pg_overexplain/pg_overexplain.c
+++ b/contrib/pg_overexplain/pg_overexplain.c
@@ -16,6 +16,10 @@
#include "commands/explain_format.h"
#include "commands/explain_state.h"
#include "fmgr.h"
+#include "nodes/makefuncs.h"
+#include "optimizer/extendplan.h"
+#include "optimizer/paths.h"
+#include "optimizer/planner.h"
#include "parser/parsetree.h"
#include "storage/lock.h"
#include "utils/builtins.h"
@@ -32,6 +36,12 @@ typedef struct
bool range_table;
} overexplain_options;
+typedef struct
+{
+ int total_joinrel_attempts;
+ int distinct_joinrel_count;
+} overexplain_plannerglobal;
+
static overexplain_options *overexplain_ensure_options(ExplainState *es);
static void overexplain_debug_handler(ExplainState *es, DefElem *opt,
ParseState *pstate);
@@ -57,9 +67,28 @@ static void overexplain_bitmapset(const char *qlabel, Bitmapset *bms,
static void overexplain_intlist(const char *qlabel, List *list,
ExplainState *es);
+static void overexplain_planner_setup_hook(PlannerGlobal *glob, Query *parse,
+ const char *query_string,
+ double *tuple_fraction,
+ struct ExplainState *es);
+static void overexplain_planner_shutdown_hook(PlannerGlobal *glob,
+ Query *parse,
+ const char *query_string,
+ PlannedStmt *pstmt);
+static void overexplain_set_join_pathlist_hook(PlannerInfo *root,
+ RelOptInfo *joinrel,
+ RelOptInfo *outerrel,
+ RelOptInfo *innerrel,
+ JoinType jointype,
+ JoinPathExtraData *extra);
+
static int es_extension_id;
+static int planner_extension_id = -1;
static explain_per_node_hook_type prev_explain_per_node_hook;
static explain_per_plan_hook_type prev_explain_per_plan_hook;
+static planner_setup_hook_type prev_planner_setup_hook;
+static planner_shutdown_hook_type prev_planner_shutdown_hook;
+static set_join_pathlist_hook_type prev_set_join_pathlist_hook;
/*
* Initialization we do when this module is loaded.
@@ -70,6 +99,9 @@ _PG_init(void)
/* Get an ID that we can use to cache data in an ExplainState. */
es_extension_id = GetExplainExtensionId("pg_overexplain");
+ /* Get an ID that we can use to cache data in the planner. */
+ planner_extension_id = GetPlannerExtensionId("pg_overexplain");
+
/* Register the new EXPLAIN options implemented by this module. */
RegisterExtensionExplainOption("debug", overexplain_debug_handler);
RegisterExtensionExplainOption("range_table",
@@ -80,6 +112,16 @@ _PG_init(void)
explain_per_node_hook = overexplain_per_node_hook;
prev_explain_per_plan_hook = explain_per_plan_hook;
explain_per_plan_hook = overexplain_per_plan_hook;
+
+ /* Example of planner_setup_hook/planner_shutdown_hook use */
+ prev_planner_setup_hook = planner_setup_hook;
+ planner_setup_hook = overexplain_planner_setup_hook;
+ prev_planner_shutdown_hook = planner_shutdown_hook;
+ planner_shutdown_hook = overexplain_planner_shutdown_hook;
+
+ /* Support for above example */
+ prev_set_join_pathlist_hook = set_join_pathlist_hook;
+ set_join_pathlist_hook = overexplain_set_join_pathlist_hook;
}
/*
@@ -369,6 +411,29 @@ overexplain_debug(PlannedStmt *plannedstmt, ExplainState *es)
plannedstmt->stmt_len),
es);
+ {
+ DefElem *elem = NULL;
+
+ foreach_node(DefElem, de, plannedstmt->extension_state)
+ {
+ if (strcmp(de->defname, "pg_overexplain") == 0)
+ {
+ elem = de;
+ break;
+ }
+ }
+
+ if (elem != NULL)
+ {
+ List *l = castNode(List, elem->arg);
+
+ ExplainPropertyInteger("Total Joinrel Attempts", NULL,
+ intVal(linitial(l)), es);
+ ExplainPropertyInteger("Distinct Joinrels", NULL,
+ intVal(lsecond(l)), es);
+ }
+ }
+
/* Done with this group. */
if (es->format == EXPLAIN_FORMAT_TEXT)
es->indent--;
@@ -772,3 +837,63 @@ overexplain_intlist(const char *qlabel, List *list, ExplainState *es)
pfree(buf.data);
}
+
+static void
+overexplain_planner_setup_hook(PlannerGlobal *glob, Query *parse,
+ const char *query_string,
+ double *tuple_fraction,
+ struct ExplainState *es)
+{
+ overexplain_options *options;
+ overexplain_plannerglobal *g;
+
+ if (es != NULL)
+ {
+ options = GetExplainExtensionState(es, es_extension_id);
+ if (options != NULL && options->debug)
+ {
+ g = palloc0_object(overexplain_plannerglobal);
+ SetPlannerGlobalExtensionState(glob, planner_extension_id, g);
+ }
+ }
+}
+
+static void
+overexplain_planner_shutdown_hook(PlannerGlobal *glob, Query *parse,
+ const char *query_string, PlannedStmt *pstmt)
+{
+ overexplain_plannerglobal *g;
+ DefElem *elem;
+ List *l;
+
+ g = GetPlannerGlobalExtensionState(glob, planner_extension_id);
+ if (g != NULL)
+ {
+ l = list_make2(makeInteger(g->total_joinrel_attempts),
+ makeInteger(g->distinct_joinrel_count));
+ elem = makeDefElem("pg_overexplain", (Node *) l, -1);
+ pstmt->extension_state = lappend(pstmt->extension_state, elem);
+ }
+}
+
+static void
+overexplain_set_join_pathlist_hook(PlannerInfo *root, RelOptInfo *joinrel,
+ RelOptInfo *outerrel, RelOptInfo *innerrel,
+ JoinType jointype, JoinPathExtraData *extra)
+{
+ overexplain_plannerglobal *g;
+
+ g = GetPlannerGlobalExtensionState(root->glob, planner_extension_id);
+
+ if (g != NULL)
+ {
+ g->total_joinrel_attempts++;
+
+ if (GetRelOptInfoExtensionState(joinrel, planner_extension_id) == NULL)
+ {
+ g->distinct_joinrel_count++;
+ /* set any non-NULL value to avoid double-counting */
+ SetRelOptInfoExtensionState(joinrel, planner_extension_id, g);
+ }
+ }
+}
--
2.39.5 (Apple Git-154)
Robert Haas <robertmhaas@gmail.com> writes:
I think that was rebased over a patch I inadvertently committed to my
local master branch. Trying again.
I looked through the v5 patchset.
0001: The allocation logic in Set*ExtensionState fails to guarantee
that it's made the array(s) big enough to hold the passed ID, which
would be problematic in the face of lots of extensions.
I think this'd be enough to fix it:
- i = pg_nextpower2_32(glob->extension_state_allocated + 1);
+ i = pg_nextpower2_32(extension_id + 1);
But maybe you should also reconsider whether blindly starting
the array sizes at 4, rather than say Max(4, extension_id + 1),
is good.
Now that I look, SetExplainExtensionState also has these issues.
Also a couple nitpicks:
* alphabetization fail in util/Makefile
* utils/palloc.h is already included by postgres.h
0002: LGTM
0003: In the wake of 70407d39b, you should avoid "struct
ExplainState" in favor of using duplicate typedefs.
Also, although you're not the first sinner, I really think
this new argument to planner() should be documented. Maybe
the rest too while we're at it.
0004: maybe the planner_shutdown_hook should be called before
DestroyPartitionDirectory? It's not entirely clear whether the hook
might like to look at that. Also "struct ExplainState" again.
0005: okay
regards, tom lane
On Mon, Sep 22, 2025 at 3:51 PM Tom Lane <tgl@sss.pgh.pa.us> wrote:
I looked through the v5 patchset.
Thanks!
Here's a new set of patches. I've added a new 0001 at the beginning to
fix the bug you identified in SetExplainExtensionState; this will need
to be back-patched to v18 once the release freeze lifts. I've also
adjusted the previous patch, now 0002, to fix the equivalent problem
with the new Set*ExplainState functions,and I've attempted to fix the
other problems you mentioned, with this exception:
Also, although you're not the first sinner, I really think
this new argument to planner() should be documented. Maybe
the rest too while we're at it.
I'm a little nervous about this. I fear that the comments are all
going to be of the form "to save a file, click the File menu, then
click Save," which doesn't actually help anyone. That might be a
slight exaggeration, but I feel like it's pretty obvious on visual
inspection that Query *parse is what we're planning, char
*query_string is the text version of that, cursorOptions is some kind
of flag mask, boundParams is the parameter values. It's fair to say
that ExplainState *es is a little less obvious than the others, but
saying "If this function is being invoked by EXPLAIN, then
ExplainState *es is the ExplainState, else it is NULL" doesn't really
seem all that helpful to me. I mean, what else would it be? My thought
here would be that if you want to write some comments that you
consider helpful for the existing arguments, I'll try to write a new
comment for this one in the same style (or you can suggest one) and
hold my nose if I don't find it helpful, or alternatively, we could
proceed with these patches without the comment and you can add
whatever comment text you want after-the-fact. If you want the comment
changes in this patch set, then I need a suggestion as to what that
should look like.
--
Robert Haas
EDB: http://www.enterprisedb.com
Attachments:
v6-0005-Add-planner_setup_hook-and-planner_shutdown_hook.patchapplication/octet-stream; name=v6-0005-Add-planner_setup_hook-and-planner_shutdown_hook.patchDownload
From 7b32e6ebacb79508eae5b699fc016294b48ee32a Mon Sep 17 00:00:00 2001
From: Robert Haas <rhaas@postgresql.org>
Date: Wed, 24 Sep 2025 11:21:55 -0400
Subject: [PATCH v6 5/7] Add planner_setup_hook and planner_shutdown_hook.
These hooks allow plugins to get control at the earliest point at
which the PlannerGlobal object is fully initialized, and then just
before it gets destroyed. This is useful in combination with the
extendable plan state facilities (see extendplan.h) and perhaps for
other purposes as well.
---
src/backend/optimizer/plan/planner.c | 14 ++++++++++++++
src/include/optimizer/planner.h | 13 +++++++++++++
2 files changed, 27 insertions(+)
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index 205d8886a2a..ebcf9ea5851 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -73,6 +73,12 @@ bool enable_distinct_reordering = true;
/* Hook for plugins to get control in planner() */
planner_hook_type planner_hook = NULL;
+/* Hook for plugins to get control after PlannerGlobal is initialized */
+planner_setup_hook_type planner_setup_hook = NULL;
+
+/* Hook for plugins to get control before PlannerGlobal is discarded */
+planner_shutdown_hook_type planner_shutdown_hook = NULL;
+
/* Hook for plugins to get control when grouping_planner() plans upper rels */
create_upper_paths_hook_type create_upper_paths_hook = NULL;
@@ -440,6 +446,10 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions,
tuple_fraction = 0.0;
}
+ /* Allow plugins to take control after we've initialized "glob" */
+ if (planner_setup_hook)
+ (*planner_setup_hook) (glob, parse, query_string, &tuple_fraction, es);
+
/* primary planning entry point (may recurse for subqueries) */
root = subquery_planner(glob, parse, NULL, false, tuple_fraction, NULL);
@@ -618,6 +628,10 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions,
result->jitFlags |= PGJIT_DEFORM;
}
+ /* Allow plugins to take control before we discard "glob" */
+ if (planner_shutdown_hook)
+ (*planner_shutdown_hook) (glob, parse, query_string, result);
+
if (glob->partition_directory != NULL)
DestroyPartitionDirectory(glob->partition_directory);
diff --git a/src/include/optimizer/planner.h b/src/include/optimizer/planner.h
index b31ea2fbbdc..a39389728aa 100644
--- a/src/include/optimizer/planner.h
+++ b/src/include/optimizer/planner.h
@@ -32,6 +32,19 @@ typedef PlannedStmt *(*planner_hook_type) (Query *parse,
ExplainState *es);
extern PGDLLIMPORT planner_hook_type planner_hook;
+/* Hook for plugins to get control after PlannerGlobal is initialized */
+typedef void (*planner_setup_hook_type) (PlannerGlobal *glob, Query *parse,
+ const char *query_string,
+ double *tuple_fraction,
+ ExplainState *es);
+extern PGDLLIMPORT planner_setup_hook_type planner_setup_hook;
+
+/* Hook for plugins to get control before PlannerGlobal is discarded */
+typedef void (*planner_shutdown_hook_type) (PlannerGlobal *glob, Query *parse,
+ const char *query_string,
+ PlannedStmt *pstmt);
+extern PGDLLIMPORT planner_shutdown_hook_type planner_shutdown_hook;
+
/* Hook for plugins to get control when grouping_planner() plans upper rels */
typedef void (*create_upper_paths_hook_type) (PlannerInfo *root,
UpperRelationKind stage,
--
2.39.5 (Apple Git-154)
v6-0001-Fix-array-allocation-bugs-in-SetExplainExtensionS.patchapplication/octet-stream; name=v6-0001-Fix-array-allocation-bugs-in-SetExplainExtensionS.patchDownload
From 261412985f971bb43ea7fd6c9a076c2e3781ccd4 Mon Sep 17 00:00:00 2001
From: Robert Haas <rhaas@postgresql.org>
Date: Wed, 24 Sep 2025 10:48:22 -0400
Subject: [PATCH v6 1/7] Fix array allocation bugs in SetExplainExtensionState.
If we already have an extension_state array but see a new extension_id
much larger than the highest the extension_id we've previously seen,
the old code might have failed to expand the array to a large enough
size, leading to disaster. Also, if we don't have an extension array
at all and need to create one, we should make sure that it's big enough
that we don't have to resize it instantly.
Reported-by: Tom Lane <tgl@sss.pgh.pa.us>
---
src/backend/commands/explain_state.c | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/src/backend/commands/explain_state.c b/src/backend/commands/explain_state.c
index 60d98d63a62..9fdeeab6436 100644
--- a/src/backend/commands/explain_state.c
+++ b/src/backend/commands/explain_state.c
@@ -281,7 +281,8 @@ SetExplainExtensionState(ExplainState *es, int extension_id, void *opaque)
/* If there is no array yet, create one. */
if (es->extension_state == NULL)
{
- es->extension_state_allocated = 16;
+ es->extension_state_allocated =
+ Max(16, pg_nextpower2_32(extension_id + 1));
es->extension_state =
palloc0(es->extension_state_allocated * sizeof(void *));
}
@@ -291,7 +292,7 @@ SetExplainExtensionState(ExplainState *es, int extension_id, void *opaque)
{
int i;
- i = pg_nextpower2_32(es->extension_state_allocated + 1);
+ i = pg_nextpower2_32(extension_id + 1);
es->extension_state = (void **)
repalloc0(es->extension_state,
es->extension_state_allocated * sizeof(void *),
--
2.39.5 (Apple Git-154)
v6-0002-Allow-private-state-in-certain-planner-data-struc.patchapplication/octet-stream; name=v6-0002-Allow-private-state-in-certain-planner-data-struc.patchDownload
From 924dc8d7eafbaeb9cc112bea4e87604413d125df Mon Sep 17 00:00:00 2001
From: Robert Haas <rhaas@postgresql.org>
Date: Wed, 24 Sep 2025 11:00:07 -0400
Subject: [PATCH v6 2/7] Allow private state in certain planner data
structures.
Extension that make extensive use of planner hooks may want to
coordinate their efforts, for example to avoid duplicate computation,
but that's currently difficult because there's no really good way to
pass data between different hooks.
To make that easier, allow for storage of extension-managed private
state in PlannerGlobal, PlannerInfo, and RelOptInfo, along very
similar lines to what we have permitted for ExplainState since commit
c65bc2e1d14a2d4daed7c1921ac518f2c5ac3d17.
---
src/backend/optimizer/util/Makefile | 1 +
src/backend/optimizer/util/extendplan.c | 183 ++++++++++++++++++++++++
src/backend/optimizer/util/meson.build | 1 +
src/include/nodes/pathnodes.h | 12 ++
src/include/optimizer/extendplan.h | 72 ++++++++++
5 files changed, 269 insertions(+)
create mode 100644 src/backend/optimizer/util/extendplan.c
create mode 100644 src/include/optimizer/extendplan.h
diff --git a/src/backend/optimizer/util/Makefile b/src/backend/optimizer/util/Makefile
index 4fb115cb118..87b4c3c0869 100644
--- a/src/backend/optimizer/util/Makefile
+++ b/src/backend/optimizer/util/Makefile
@@ -15,6 +15,7 @@ include $(top_builddir)/src/Makefile.global
OBJS = \
appendinfo.o \
clauses.o \
+ extendplan.o \
inherit.o \
joininfo.o \
orclauses.o \
diff --git a/src/backend/optimizer/util/extendplan.c b/src/backend/optimizer/util/extendplan.c
new file mode 100644
index 00000000000..03d32277ba1
--- /dev/null
+++ b/src/backend/optimizer/util/extendplan.c
@@ -0,0 +1,183 @@
+/*-------------------------------------------------------------------------
+ *
+ * extendplan.c
+ * Extend core planner objects with additional private state
+ *
+ * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994-5, Regents of the University of California
+ *
+ * The interfaces defined in this file make it possible for loadable
+ * modules to store their own private state inside of key planner data
+ * structures -- specifically, the PlannerGlobal, PlannerInfo, and
+ * RelOptInfo structures. This can make it much easier to write
+ * reasonably efficient planner extensions; for instance, code that
+ * uses set_join_pathlist_hook can arrange to compute a key intermediate
+ * result once per joinrel rather than on every call.
+ *
+ * IDENTIFICATION
+ * src/backend/optimizer/util/extendplan.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "optimizer/extendplan.h"
+#include "port/pg_bitutils.h"
+#include "utils/memutils.h"
+
+static const char **PlannerExtensionNameArray = NULL;
+static int PlannerExtensionNamesAssigned = 0;
+static int PlannerExtensionNamesAllocated = 0;
+
+/*
+ * Map the name of a planner extension to an integer ID.
+ *
+ * Within the lifetime of a particular backend, the same name will be mapped
+ * to the same ID every time. IDs are not stable across backends. Use the ID
+ * that you get from this function to call the remaining functions in this
+ * file.
+ */
+int
+GetPlannerExtensionId(const char *extension_name)
+{
+ /* Search for an existing extension by this name; if found, return ID. */
+ for (int i = 0; i < PlannerExtensionNamesAssigned; ++i)
+ if (strcmp(PlannerExtensionNameArray[i], extension_name) == 0)
+ return i;
+
+ /* If there is no array yet, create one. */
+ if (PlannerExtensionNameArray == NULL)
+ {
+ PlannerExtensionNamesAllocated = 16;
+ PlannerExtensionNameArray = (const char **)
+ MemoryContextAlloc(TopMemoryContext,
+ PlannerExtensionNamesAllocated
+ * sizeof(char *));
+ }
+
+ /* If there's an array but it's currently full, expand it. */
+ if (PlannerExtensionNamesAssigned >= PlannerExtensionNamesAllocated)
+ {
+ int i = pg_nextpower2_32(PlannerExtensionNamesAssigned + 1);
+
+ PlannerExtensionNameArray = (const char **)
+ repalloc(PlannerExtensionNameArray, i * sizeof(char *));
+ PlannerExtensionNamesAllocated = i;
+ }
+
+ /* Assign and return new ID. */
+ PlannerExtensionNameArray[PlannerExtensionNamesAssigned] = extension_name;
+ return PlannerExtensionNamesAssigned++;
+}
+
+/*
+ * Store extension-specific state into a PlannerGlobal.
+ */
+void
+SetPlannerGlobalExtensionState(PlannerGlobal *glob, int extension_id,
+ void *opaque)
+{
+ Assert(extension_id >= 0);
+
+ /* If there is no array yet, create one. */
+ if (glob->extension_state == NULL)
+ {
+ MemoryContext planner_cxt;
+ Size sz;
+
+ planner_cxt = GetMemoryChunkContext(glob);
+ glob->extension_state_allocated =
+ Max(4, pg_nextpower2_32(extension_id + 1));
+ sz = glob->extension_state_allocated * sizeof(void *);
+ glob->extension_state = MemoryContextAllocZero(planner_cxt, sz);
+ }
+
+ /* If there's an array but it's currently full, expand it. */
+ if (extension_id >= glob->extension_state_allocated)
+ {
+ int i;
+
+ i = pg_nextpower2_32(extension_id + 1);
+ glob->extension_state = (void **)
+ repalloc0(glob->extension_state,
+ glob->extension_state_allocated * sizeof(void *),
+ i * sizeof(void *));
+ glob->extension_state_allocated = i;
+ }
+
+ glob->extension_state[extension_id] = opaque;
+}
+
+/*
+ * Store extension-specific state into a PlannerInfo.
+ */
+void
+SetPlannerInfoExtensionState(PlannerInfo *root, int extension_id,
+ void *opaque)
+{
+ Assert(extension_id >= 0);
+
+ /* If there is no array yet, create one. */
+ if (root->extension_state == NULL)
+ {
+ Size sz;
+
+ root->extension_state_allocated =
+ Max(4, pg_nextpower2_32(extension_id + 1));
+ sz = root->extension_state_allocated * sizeof(void *);
+ root->extension_state = MemoryContextAllocZero(root->planner_cxt, sz);
+ }
+
+ /* If there's an array but it's currently full, expand it. */
+ if (extension_id >= root->extension_state_allocated)
+ {
+ int i;
+
+ i = pg_nextpower2_32(extension_id + 1);
+ root->extension_state = (void **)
+ repalloc0(root->extension_state,
+ root->extension_state_allocated * sizeof(void *),
+ i * sizeof(void *));
+ root->extension_state_allocated = i;
+ }
+
+ root->extension_state[extension_id] = opaque;
+}
+
+/*
+ * Store extension-specific state into a RelOptInfo.
+ */
+void
+SetRelOptInfoExtensionState(RelOptInfo *rel, int extension_id,
+ void *opaque)
+{
+ Assert(extension_id >= 0);
+
+ /* If there is no array yet, create one. */
+ if (rel->extension_state == NULL)
+ {
+ MemoryContext planner_cxt;
+ Size sz;
+
+ planner_cxt = GetMemoryChunkContext(rel);
+ rel->extension_state_allocated =
+ Max(4, pg_nextpower2_32(extension_id + 1));
+ sz = rel->extension_state_allocated * sizeof(void *);
+ rel->extension_state = MemoryContextAllocZero(planner_cxt, sz);
+ }
+
+ /* If there's an array but it's currently full, expand it. */
+ if (extension_id >= rel->extension_state_allocated)
+ {
+ int i;
+
+ i = pg_nextpower2_32(extension_id + 1);
+ rel->extension_state = (void **)
+ repalloc0(rel->extension_state,
+ rel->extension_state_allocated * sizeof(void *),
+ i * sizeof(void *));
+ rel->extension_state_allocated = i;
+ }
+
+ rel->extension_state[extension_id] = opaque;
+}
diff --git a/src/backend/optimizer/util/meson.build b/src/backend/optimizer/util/meson.build
index b3bf913d096..f71f56e37a1 100644
--- a/src/backend/optimizer/util/meson.build
+++ b/src/backend/optimizer/util/meson.build
@@ -3,6 +3,7 @@
backend_sources += files(
'appendinfo.c',
'clauses.c',
+ 'extendplan.c',
'inherit.c',
'joininfo.c',
'orclauses.c',
diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h
index b12a2508d8c..5cf23cba596 100644
--- a/src/include/nodes/pathnodes.h
+++ b/src/include/nodes/pathnodes.h
@@ -182,6 +182,10 @@ typedef struct PlannerGlobal
/* hash table for NOT NULL attnums of relations */
struct HTAB *rel_notnullatts_hash pg_node_attr(read_write_ignore);
+
+ /* extension state */
+ void **extension_state pg_node_attr(read_write_ignore);
+ int extension_state_allocated;
} PlannerGlobal;
/* macro for fetching the Plan associated with a SubPlan node */
@@ -580,6 +584,10 @@ struct PlannerInfo
/* PartitionPruneInfos added in this query's plan. */
List *partPruneInfos;
+
+ /* extension state */
+ void **extension_state pg_node_attr(read_write_ignore);
+ int extension_state_allocated;
};
@@ -1091,6 +1099,10 @@ typedef struct RelOptInfo
List **partexprs pg_node_attr(read_write_ignore);
/* Nullable partition key expressions */
List **nullable_partexprs pg_node_attr(read_write_ignore);
+
+ /* extension state */
+ void **extension_state pg_node_attr(read_write_ignore);
+ int extension_state_allocated;
} RelOptInfo;
/*
diff --git a/src/include/optimizer/extendplan.h b/src/include/optimizer/extendplan.h
new file mode 100644
index 00000000000..de9618761dd
--- /dev/null
+++ b/src/include/optimizer/extendplan.h
@@ -0,0 +1,72 @@
+/*-------------------------------------------------------------------------
+ *
+ * extendplan.h
+ * Extend core planner objects with additional private state
+ *
+ *
+ * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/optimizer/extendplan.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef EXTENDPLAN_H
+#define EXTENDPLAN_H
+
+#include "nodes/pathnodes.h"
+
+extern int GetPlannerExtensionId(const char *extension_name);
+
+/*
+ * Get extension-specific state from a PlannerGlobal.
+ */
+static inline void *
+GetPlannerGlobalExtensionState(PlannerGlobal *glob, int extension_id)
+{
+ Assert(extension_id >= 0);
+
+ if (extension_id >= glob->extension_state_allocated)
+ return NULL;
+
+ return glob->extension_state[extension_id];
+}
+
+/*
+ * Get extension-specific state from a PlannerInfo.
+ */
+static inline void *
+GetPlannerInfoExtensionState(PlannerInfo *root, int extension_id)
+{
+ Assert(extension_id >= 0);
+
+ if (extension_id >= root->extension_state_allocated)
+ return NULL;
+
+ return root->extension_state[extension_id];
+}
+
+/*
+ * Get extension-specific state from a PlannerInfo.
+ */
+static inline void *
+GetRelOptInfoExtensionState(RelOptInfo *rel, int extension_id)
+{
+ Assert(extension_id >= 0);
+
+ if (extension_id >= rel->extension_state_allocated)
+ return NULL;
+
+ return rel->extension_state[extension_id];
+}
+
+/* Functions to store private state into various planner objects */
+extern void SetPlannerGlobalExtensionState(PlannerGlobal *glob,
+ int extension_id,
+ void *opaque);
+extern void SetPlannerInfoExtensionState(PlannerInfo *root, int extension_id,
+ void *opaque);
+extern void SetRelOptInfoExtensionState(RelOptInfo *rel, int extension_id,
+ void *opaque);
+
+#endif
--
2.39.5 (Apple Git-154)
v6-0003-Remove-PlannerInfo-s-join_search_private-method.patchapplication/octet-stream; name=v6-0003-Remove-PlannerInfo-s-join_search_private-method.patchDownload
From de2958ea5a7db4098935cd6754007694a5145ed1 Mon Sep 17 00:00:00 2001
From: Robert Haas <rhaas@postgresql.org>
Date: Wed, 20 Aug 2025 15:10:52 -0400
Subject: [PATCH v6 3/7] Remove PlannerInfo's join_search_private method.
Instead, use the new mechanism that allows planner extensions to store
private state inside a PlannerInfo, treating GEQO as an in-core planner
extension. This is a useful test of the new facility, and also buys
back a few bytes of storage.
To make this work, we must remove innerrel_is_unique_ext's hack of
testing whether join_search_private is set as a proxy for whether
the join search might be retried. Add a flag that extensions can
use to explicitly signal their intentions instead.
---
src/backend/optimizer/geqo/geqo_eval.c | 2 +-
src/backend/optimizer/geqo/geqo_main.c | 12 ++++++++++--
src/backend/optimizer/geqo/geqo_random.c | 7 +++----
src/backend/optimizer/plan/analyzejoins.c | 9 +++------
src/backend/optimizer/plan/planner.c | 1 +
src/backend/optimizer/prep/prepjointree.c | 1 +
src/include/nodes/pathnodes.h | 5 ++---
src/include/optimizer/geqo.h | 10 +++++++++-
8 files changed, 30 insertions(+), 17 deletions(-)
diff --git a/src/backend/optimizer/geqo/geqo_eval.c b/src/backend/optimizer/geqo/geqo_eval.c
index f07d1dc8ac6..7fcb1aa70d1 100644
--- a/src/backend/optimizer/geqo/geqo_eval.c
+++ b/src/backend/optimizer/geqo/geqo_eval.c
@@ -162,7 +162,7 @@ geqo_eval(PlannerInfo *root, Gene *tour, int num_gene)
RelOptInfo *
gimme_tree(PlannerInfo *root, Gene *tour, int num_gene)
{
- GeqoPrivateData *private = (GeqoPrivateData *) root->join_search_private;
+ GeqoPrivateData *private = GetGeqoPrivateData(root);
List *clumps;
int rel_count;
diff --git a/src/backend/optimizer/geqo/geqo_main.c b/src/backend/optimizer/geqo/geqo_main.c
index 38402ce58db..0064556087a 100644
--- a/src/backend/optimizer/geqo/geqo_main.c
+++ b/src/backend/optimizer/geqo/geqo_main.c
@@ -47,6 +47,8 @@ int Geqo_generations;
double Geqo_selection_bias;
double Geqo_seed;
+/* GEQO is treated as an in-core planner extension */
+int Geqo_planner_extension_id = -1;
static int gimme_pool_size(int nr_rel);
static int gimme_number_generations(int pool_size);
@@ -98,10 +100,16 @@ geqo(PlannerInfo *root, int number_of_rels, List *initial_rels)
int mutations = 0;
#endif
+ if (Geqo_planner_extension_id < 0)
+ Geqo_planner_extension_id = GetPlannerExtensionId("geqo");
+
/* set up private information */
- root->join_search_private = &private;
+ SetPlannerInfoExtensionState(root, Geqo_planner_extension_id, &private);
private.initial_rels = initial_rels;
+/* inform core planner that we may replan */
+ root->assumeReplanning = true;
+
/* initialize private number generator */
geqo_set_seed(root, Geqo_seed);
@@ -304,7 +312,7 @@ geqo(PlannerInfo *root, int number_of_rels, List *initial_rels)
free_pool(root, pool);
/* ... clear root pointer to our private storage */
- root->join_search_private = NULL;
+ SetPlannerInfoExtensionState(root, Geqo_planner_extension_id, NULL);
return best_rel;
}
diff --git a/src/backend/optimizer/geqo/geqo_random.c b/src/backend/optimizer/geqo/geqo_random.c
index 6c7a411f69f..46d28baa2e6 100644
--- a/src/backend/optimizer/geqo/geqo_random.c
+++ b/src/backend/optimizer/geqo/geqo_random.c
@@ -15,11 +15,10 @@
#include "optimizer/geqo_random.h"
-
void
geqo_set_seed(PlannerInfo *root, double seed)
{
- GeqoPrivateData *private = (GeqoPrivateData *) root->join_search_private;
+ GeqoPrivateData *private = GetGeqoPrivateData(root);
pg_prng_fseed(&private->random_state, seed);
}
@@ -27,7 +26,7 @@ geqo_set_seed(PlannerInfo *root, double seed)
double
geqo_rand(PlannerInfo *root)
{
- GeqoPrivateData *private = (GeqoPrivateData *) root->join_search_private;
+ GeqoPrivateData *private = GetGeqoPrivateData(root);
return pg_prng_double(&private->random_state);
}
@@ -35,7 +34,7 @@ geqo_rand(PlannerInfo *root)
int
geqo_randint(PlannerInfo *root, int upper, int lower)
{
- GeqoPrivateData *private = (GeqoPrivateData *) root->join_search_private;
+ GeqoPrivateData *private = GetGeqoPrivateData(root);
/*
* In current usage, "lower" is never negative so we can just use
diff --git a/src/backend/optimizer/plan/analyzejoins.c b/src/backend/optimizer/plan/analyzejoins.c
index 2a3dea88a94..6a3c030e8ef 100644
--- a/src/backend/optimizer/plan/analyzejoins.c
+++ b/src/backend/optimizer/plan/analyzejoins.c
@@ -1425,17 +1425,14 @@ innerrel_is_unique_ext(PlannerInfo *root,
*
* However, in normal planning mode, caching this knowledge is totally
* pointless; it won't be queried again, because we build up joinrels
- * from smaller to larger. It is useful in GEQO mode, where the
- * knowledge can be carried across successive planning attempts; and
- * it's likely to be useful when using join-search plugins, too. Hence
- * cache when join_search_private is non-NULL. (Yeah, that's a hack,
- * but it seems reasonable.)
+ * from smaller to larger. It's only useful when using GEQO or
+ * another planner extension that attempts planning multiple times.
*
* Also, allow callers to override that heuristic and force caching;
* that's useful for reduce_unique_semijoins, which calls here before
* the normal join search starts.
*/
- if (force_cache || root->join_search_private)
+ if (force_cache || root->assumeReplanning)
{
old_context = MemoryContextSwitchTo(root->planner_cxt);
innerrel->non_unique_for_rels =
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index 41bd8353430..9de39da1757 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -703,6 +703,7 @@ subquery_planner(PlannerGlobal *glob, Query *parse, PlannerInfo *parent_root,
root->hasAlternativeSubPlans = false;
root->placeholdersFrozen = false;
root->hasRecursion = hasRecursion;
+ root->assumeReplanning = false;
if (hasRecursion)
root->wt_param_id = assign_special_exec_param(root);
else
diff --git a/src/backend/optimizer/prep/prepjointree.c b/src/backend/optimizer/prep/prepjointree.c
index 35e8d3c183b..4075f7519ca 100644
--- a/src/backend/optimizer/prep/prepjointree.c
+++ b/src/backend/optimizer/prep/prepjointree.c
@@ -1383,6 +1383,7 @@ pull_up_simple_subquery(PlannerInfo *root, Node *jtnode, RangeTblEntry *rte,
subroot->qual_security_level = 0;
subroot->placeholdersFrozen = false;
subroot->hasRecursion = false;
+ subroot->assumeReplanning = false;
subroot->wt_param_id = -1;
subroot->non_recursive_path = NULL;
/* We don't currently need a top JoinDomain for the subroot */
diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h
index 5cf23cba596..bc1e0c1b5cc 100644
--- a/src/include/nodes/pathnodes.h
+++ b/src/include/nodes/pathnodes.h
@@ -530,6 +530,8 @@ struct PlannerInfo
bool placeholdersFrozen;
/* true if planning a recursive WITH item */
bool hasRecursion;
+ /* true if a planner extension may replan this subquery */
+ bool assumeReplanning;
/*
* The rangetable index for the RTE_GROUP RTE, or 0 if there is no
@@ -576,9 +578,6 @@ struct PlannerInfo
bool *isAltSubplan pg_node_attr(read_write_ignore);
bool *isUsedSubplan pg_node_attr(read_write_ignore);
- /* optional private data for join_search_hook, e.g., GEQO */
- void *join_search_private pg_node_attr(read_write_ignore);
-
/* Does this query modify any partition key columns? */
bool partColsUpdated;
diff --git a/src/include/optimizer/geqo.h b/src/include/optimizer/geqo.h
index 9f8e0f337aa..3f4872e25e3 100644
--- a/src/include/optimizer/geqo.h
+++ b/src/include/optimizer/geqo.h
@@ -24,6 +24,7 @@
#include "common/pg_prng.h"
#include "nodes/pathnodes.h"
+#include "optimizer/extendplan.h"
#include "optimizer/geqo_gene.h"
@@ -62,6 +63,8 @@ extern PGDLLIMPORT int Geqo_generations; /* 1 .. inf, or 0 to use default */
extern PGDLLIMPORT double Geqo_selection_bias;
+extern PGDLLIMPORT int Geqo_planner_extension_id;
+
#define DEFAULT_GEQO_SELECTION_BIAS 2.0
#define MIN_GEQO_SELECTION_BIAS 1.5
#define MAX_GEQO_SELECTION_BIAS 2.0
@@ -70,7 +73,7 @@ extern PGDLLIMPORT double Geqo_seed; /* 0 .. 1 */
/*
- * Private state for a GEQO run --- accessible via root->join_search_private
+ * Private state for a GEQO run --- accessible via GetGeqoPrivateData
*/
typedef struct
{
@@ -78,6 +81,11 @@ typedef struct
pg_prng_state random_state; /* PRNG state */
} GeqoPrivateData;
+static inline GeqoPrivateData *
+GetGeqoPrivateData(PlannerInfo *root)
+{
+ return GetPlannerInfoExtensionState(root, Geqo_planner_extension_id);
+}
/* routines in geqo_main.c */
extern RelOptInfo *geqo(PlannerInfo *root,
--
2.39.5 (Apple Git-154)
v6-0004-Add-ExplainState-argument-to-pg_plan_query-and-pl.patchapplication/octet-stream; name=v6-0004-Add-ExplainState-argument-to-pg_plan_query-and-pl.patchDownload
From e81d1a150b3501b3993a578d559de74f4179ad1f Mon Sep 17 00:00:00 2001
From: Robert Haas <rhaas@postgresql.org>
Date: Wed, 24 Sep 2025 11:18:12 -0400
Subject: [PATCH v6 4/7] Add ExplainState argument to pg_plan_query() and
planner().
This allows extensions to have access to any data they've stored
in the ExplainState during planning. Unfortunately, it won't help
with EXPLAIN EXECUTE is used, but since that case is less common,
this still seems like an improvement.
---
contrib/pg_stat_statements/pg_stat_statements.c | 14 ++++++++------
src/backend/commands/copyto.c | 2 +-
src/backend/commands/createas.c | 2 +-
src/backend/commands/explain.c | 2 +-
src/backend/commands/matview.c | 2 +-
src/backend/commands/portalcmds.c | 3 ++-
src/backend/optimizer/plan/planner.c | 10 ++++++----
src/backend/tcop/postgres.c | 7 ++++---
src/include/optimizer/optimizer.h | 5 ++++-
src/include/optimizer/planner.h | 8 ++++++--
src/include/tcop/tcopprot.h | 4 +++-
src/test/modules/delay_execution/delay_execution.c | 7 ++++---
12 files changed, 41 insertions(+), 25 deletions(-)
diff --git a/contrib/pg_stat_statements/pg_stat_statements.c b/contrib/pg_stat_statements/pg_stat_statements.c
index 0bb0f933399..d6af2f8efbf 100644
--- a/contrib/pg_stat_statements/pg_stat_statements.c
+++ b/contrib/pg_stat_statements/pg_stat_statements.c
@@ -337,7 +337,8 @@ static void pgss_post_parse_analyze(ParseState *pstate, Query *query,
static PlannedStmt *pgss_planner(Query *parse,
const char *query_string,
int cursorOptions,
- ParamListInfo boundParams);
+ ParamListInfo boundParams,
+ ExplainState *es);
static void pgss_ExecutorStart(QueryDesc *queryDesc, int eflags);
static void pgss_ExecutorRun(QueryDesc *queryDesc,
ScanDirection direction,
@@ -893,7 +894,8 @@ static PlannedStmt *
pgss_planner(Query *parse,
const char *query_string,
int cursorOptions,
- ParamListInfo boundParams)
+ ParamListInfo boundParams,
+ ExplainState *es)
{
PlannedStmt *result;
@@ -928,10 +930,10 @@ pgss_planner(Query *parse,
{
if (prev_planner_hook)
result = prev_planner_hook(parse, query_string, cursorOptions,
- boundParams);
+ boundParams, es);
else
result = standard_planner(parse, query_string, cursorOptions,
- boundParams);
+ boundParams, es);
}
PG_FINALLY();
{
@@ -977,10 +979,10 @@ pgss_planner(Query *parse,
{
if (prev_planner_hook)
result = prev_planner_hook(parse, query_string, cursorOptions,
- boundParams);
+ boundParams, es);
else
result = standard_planner(parse, query_string, cursorOptions,
- boundParams);
+ boundParams, es);
}
PG_FINALLY();
{
diff --git a/src/backend/commands/copyto.c b/src/backend/commands/copyto.c
index 67b94b91cae..e5781155cdf 100644
--- a/src/backend/commands/copyto.c
+++ b/src/backend/commands/copyto.c
@@ -796,7 +796,7 @@ BeginCopyTo(ParseState *pstate,
/* plan the query */
plan = pg_plan_query(query, pstate->p_sourcetext,
- CURSOR_OPT_PARALLEL_OK, NULL);
+ CURSOR_OPT_PARALLEL_OK, NULL, NULL);
/*
* With row-level security and a user using "COPY relation TO", we
diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c
index dfd2ab8e862..1ccc2e55c64 100644
--- a/src/backend/commands/createas.c
+++ b/src/backend/commands/createas.c
@@ -321,7 +321,7 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt,
/* plan the query */
plan = pg_plan_query(query, pstate->p_sourcetext,
- CURSOR_OPT_PARALLEL_OK, params);
+ CURSOR_OPT_PARALLEL_OK, params, NULL);
/*
* Use a snapshot with an updated command ID to ensure this query sees
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 207f86f1d39..82d14db8d68 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -351,7 +351,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, params, es);
INSTR_TIME_SET_CURRENT(planduration);
INSTR_TIME_SUBTRACT(planduration, planstart);
diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index 188e26f0e6e..441de55ac24 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -426,7 +426,7 @@ refresh_matview_datafill(DestReceiver *dest, Query *query,
CHECK_FOR_INTERRUPTS();
/* Plan the query which will generate data for the refresh. */
- plan = pg_plan_query(query, queryString, CURSOR_OPT_PARALLEL_OK, NULL);
+ plan = pg_plan_query(query, queryString, CURSOR_OPT_PARALLEL_OK, NULL, NULL);
/*
* Use a snapshot with an updated command ID to ensure this query sees
diff --git a/src/backend/commands/portalcmds.c b/src/backend/commands/portalcmds.c
index e7c8171c102..ec96c2efcd3 100644
--- a/src/backend/commands/portalcmds.c
+++ b/src/backend/commands/portalcmds.c
@@ -99,7 +99,8 @@ PerformCursorOpen(ParseState *pstate, DeclareCursorStmt *cstmt, ParamListInfo pa
elog(ERROR, "non-SELECT statement in DECLARE CURSOR");
/* Plan the query, applying the specified options */
- plan = pg_plan_query(query, pstate->p_sourcetext, cstmt->options, params);
+ plan = pg_plan_query(query, pstate->p_sourcetext, cstmt->options, params,
+ NULL);
/*
* Create a portal and copy the plan and query string into its memory.
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index 9de39da1757..205d8886a2a 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -291,14 +291,16 @@ static void create_partial_unique_paths(PlannerInfo *root, RelOptInfo *input_rel
*****************************************************************************/
PlannedStmt *
planner(Query *parse, const char *query_string, int cursorOptions,
- ParamListInfo boundParams)
+ ParamListInfo boundParams, ExplainState *es)
{
PlannedStmt *result;
if (planner_hook)
- result = (*planner_hook) (parse, query_string, cursorOptions, boundParams);
+ result = (*planner_hook) (parse, query_string, cursorOptions,
+ boundParams, es);
else
- result = standard_planner(parse, query_string, cursorOptions, boundParams);
+ result = standard_planner(parse, query_string, cursorOptions,
+ boundParams, es);
pgstat_report_plan_id(result->planId, false);
@@ -307,7 +309,7 @@ planner(Query *parse, const char *query_string, int cursorOptions,
PlannedStmt *
standard_planner(Query *parse, const char *query_string, int cursorOptions,
- ParamListInfo boundParams)
+ ParamListInfo boundParams, ExplainState *es)
{
PlannedStmt *result;
PlannerGlobal *glob;
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index d356830f756..7dd75a490aa 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -37,6 +37,7 @@
#include "catalog/pg_type.h"
#include "commands/async.h"
#include "commands/event_trigger.h"
+#include "commands/explain_state.h"
#include "commands/prepare.h"
#include "common/pg_prng.h"
#include "jit/jit.h"
@@ -884,7 +885,7 @@ pg_rewrite_query(Query *query)
*/
PlannedStmt *
pg_plan_query(Query *querytree, const char *query_string, int cursorOptions,
- ParamListInfo boundParams)
+ ParamListInfo boundParams, ExplainState *es)
{
PlannedStmt *plan;
@@ -901,7 +902,7 @@ pg_plan_query(Query *querytree, const char *query_string, int cursorOptions,
ResetUsage();
/* call the optimizer */
- plan = planner(querytree, query_string, cursorOptions, boundParams);
+ plan = planner(querytree, query_string, cursorOptions, boundParams, es);
if (log_planner_stats)
ShowUsage("PLANNER STATISTICS");
@@ -997,7 +998,7 @@ pg_plan_queries(List *querytrees, const char *query_string, int cursorOptions,
else
{
stmt = pg_plan_query(query, query_string, cursorOptions,
- boundParams);
+ boundParams, NULL);
}
stmt_list = lappend(stmt_list, stmt);
diff --git a/src/include/optimizer/optimizer.h b/src/include/optimizer/optimizer.h
index 04878f1f1c2..a34113903c0 100644
--- a/src/include/optimizer/optimizer.h
+++ b/src/include/optimizer/optimizer.h
@@ -24,6 +24,8 @@
#include "nodes/parsenodes.h"
+typedef struct ExplainState ExplainState; /* defined in explain_state.h */
+
/*
* We don't want to include nodes/pathnodes.h here, because non-planner
* code should generally treat PlannerInfo as an opaque typedef.
@@ -104,7 +106,8 @@ extern PGDLLIMPORT bool enable_distinct_reordering;
extern PlannedStmt *planner(Query *parse, const char *query_string,
int cursorOptions,
- ParamListInfo boundParams);
+ ParamListInfo boundParams,
+ ExplainState *es);
extern Expr *expression_planner(Expr *expr);
extern Expr *expression_planner_with_deps(Expr *expr,
diff --git a/src/include/optimizer/planner.h b/src/include/optimizer/planner.h
index f220e9a270d..b31ea2fbbdc 100644
--- a/src/include/optimizer/planner.h
+++ b/src/include/optimizer/planner.h
@@ -22,11 +22,14 @@
#include "nodes/plannodes.h"
+typedef struct ExplainState ExplainState; /* defined in explain_state.h */
+
/* Hook for plugins to get control in planner() */
typedef PlannedStmt *(*planner_hook_type) (Query *parse,
const char *query_string,
int cursorOptions,
- ParamListInfo boundParams);
+ ParamListInfo boundParams,
+ ExplainState *es);
extern PGDLLIMPORT planner_hook_type planner_hook;
/* Hook for plugins to get control when grouping_planner() plans upper rels */
@@ -40,7 +43,8 @@ extern PGDLLIMPORT create_upper_paths_hook_type create_upper_paths_hook;
extern PlannedStmt *standard_planner(Query *parse, const char *query_string,
int cursorOptions,
- ParamListInfo boundParams);
+ ParamListInfo boundParams,
+ ExplainState *es);
extern PlannerInfo *subquery_planner(PlannerGlobal *glob, Query *parse,
PlannerInfo *parent_root,
diff --git a/src/include/tcop/tcopprot.h b/src/include/tcop/tcopprot.h
index a83cc4f4850..c1bcfdec673 100644
--- a/src/include/tcop/tcopprot.h
+++ b/src/include/tcop/tcopprot.h
@@ -20,6 +20,7 @@
#include "utils/guc.h"
#include "utils/queryenvironment.h"
+typedef struct ExplainState ExplainState; /* defined in explain_state.h */
extern PGDLLIMPORT CommandDest whereToSendOutput;
extern PGDLLIMPORT const char *debug_query_string;
@@ -63,7 +64,8 @@ extern List *pg_analyze_and_rewrite_withcb(RawStmt *parsetree,
QueryEnvironment *queryEnv);
extern PlannedStmt *pg_plan_query(Query *querytree, const char *query_string,
int cursorOptions,
- ParamListInfo boundParams);
+ ParamListInfo boundParams,
+ ExplainState *es);
extern List *pg_plan_queries(List *querytrees, const char *query_string,
int cursorOptions,
ParamListInfo boundParams);
diff --git a/src/test/modules/delay_execution/delay_execution.c b/src/test/modules/delay_execution/delay_execution.c
index 7bc97f84a1c..d933e9a6e53 100644
--- a/src/test/modules/delay_execution/delay_execution.c
+++ b/src/test/modules/delay_execution/delay_execution.c
@@ -40,17 +40,18 @@ static planner_hook_type prev_planner_hook = NULL;
/* planner_hook function to provide the desired delay */
static PlannedStmt *
delay_execution_planner(Query *parse, const char *query_string,
- int cursorOptions, ParamListInfo boundParams)
+ int cursorOptions, ParamListInfo boundParams,
+ ExplainState *es)
{
PlannedStmt *result;
/* Invoke the planner, possibly via a previous hook user */
if (prev_planner_hook)
result = prev_planner_hook(parse, query_string, cursorOptions,
- boundParams);
+ boundParams, es);
else
result = standard_planner(parse, query_string, cursorOptions,
- boundParams);
+ boundParams, es);
/* If enabled, delay by taking and releasing the specified lock */
if (post_planning_lock_id != 0)
--
2.39.5 (Apple Git-154)
v6-0006-Add-extension_state-member-to-PlannedStmt.patchapplication/octet-stream; name=v6-0006-Add-extension_state-member-to-PlannedStmt.patchDownload
From 96921b906a3d0c811b433ce1b9c00f1c58f6c29a Mon Sep 17 00:00:00 2001
From: Robert Haas <rhaas@postgresql.org>
Date: Mon, 25 Aug 2025 14:29:02 -0400
Subject: [PATCH v6 6/7] Add extension_state member to PlannedStmt.
Extensions can stash data computed at plan time into this list using
planner_shutdown_hook (or perhaps other mechanisms) and then access
it from any code that has access to the PlannedStmt (such as explain
hooks), allowing for extensible debugging and instrumentation of
plans.
---
src/include/nodes/plannodes.h | 9 +++++++++
1 file changed, 9 insertions(+)
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index 3d196f5078e..77ec2bc10b2 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -149,6 +149,15 @@ typedef struct PlannedStmt
/* non-null if this is utility stmt */
Node *utilityStmt;
+ /*
+ * DefElem objects added by extensions, e.g. using planner_shutdown_hook
+ *
+ * Set each DefElem's defname to the name of the plugin or extension, and
+ * the argument to a tree of nodes that all have copy and read/write
+ * support.
+ */
+ List *extension_state;
+
/* statement location in source string (copied from Query) */
/* start location, or -1 if unknown */
ParseLoc stmt_location;
--
2.39.5 (Apple Git-154)
v6-0007-not-for-commit-count-distinct-joinrels-and-joinre.patchapplication/octet-stream; name=v6-0007-not-for-commit-count-distinct-joinrels-and-joinre.patchDownload
From 19613e063b93bd67cf452cc6d72afb853301fb65 Mon Sep 17 00:00:00 2001
From: Robert Haas <rhaas@postgresql.org>
Date: Mon, 25 Aug 2025 15:22:57 -0400
Subject: [PATCH v6 7/7] not for commit: count distinct joinrels and joinrel
planning attempts
---
.../expected/pg_overexplain.out | 22 ++-
contrib/pg_overexplain/pg_overexplain.c | 125 ++++++++++++++++++
2 files changed, 142 insertions(+), 5 deletions(-)
diff --git a/contrib/pg_overexplain/expected/pg_overexplain.out b/contrib/pg_overexplain/expected/pg_overexplain.out
index 55d34666d87..48251298f05 100644
--- a/contrib/pg_overexplain/expected/pg_overexplain.out
+++ b/contrib/pg_overexplain/expected/pg_overexplain.out
@@ -38,7 +38,9 @@ EXPLAIN (DEBUG) SELECT 1;
Relation OIDs: none
Executor Parameter Types: none
Parse Location: 0 to end
-(11 rows)
+ Total Joinrel Attempts: 0
+ Distinct Joinrels: 0
+(13 rows)
EXPLAIN (RANGE_TABLE) SELECT 1;
QUERY PLAN
@@ -121,6 +123,8 @@ $$);
Relation OIDs: NNN...
Executor Parameter Types: none
Parse Location: 0 to end
+ Total Joinrel Attempts: 0
+ Distinct Joinrels: 0
RTI 1 (relation, inherited, in-from-clause):
Eref: vegetables (id, name, genus)
Relation: vegetables
@@ -142,7 +146,7 @@ $$);
Relation Kind: relation
Relation Lock Mode: AccessShareLock
Unprunable RTIs: 1 3 4
-(53 rows)
+(55 rows)
-- Test a different output format.
SELECT explain_filter($$
@@ -242,6 +246,8 @@ $$);
<Relation-OIDs>NNN...</Relation-OIDs> +
<Executor-Parameter-Types>none</Executor-Parameter-Types> +
<Parse-Location>0 to end</Parse-Location> +
+ <Total-Joinrel-Attempts>0</Total-Joinrel-Attempts> +
+ <Distinct-Joinrels>0</Distinct-Joinrels> +
</PlannedStmt> +
<Range-Table> +
<Range-Table-Entry> +
@@ -346,7 +352,9 @@ $$);
Relation OIDs: NNN...
Executor Parameter Types: none
Parse Location: 0 to end
-(37 rows)
+ Total Joinrel Attempts: 0
+ Distinct Joinrels: 0
+(39 rows)
SET debug_parallel_query = false;
RESET enable_seqscan;
@@ -374,7 +382,9 @@ $$);
Relation OIDs: NNN...
Executor Parameter Types: 0
Parse Location: 0 to end
-(15 rows)
+ Total Joinrel Attempts: 0
+ Distinct Joinrels: 0
+(17 rows)
-- Create an index, and then attempt to force a nested loop with inner index
-- scan so that we can see parameter-related information. Also, let's try
@@ -438,7 +448,9 @@ $$);
Relation OIDs: NNN...
Executor Parameter Types: 23
Parse Location: 0 to end
-(47 rows)
+ Total Joinrel Attempts: 2
+ Distinct Joinrels: 1
+(49 rows)
RESET enable_hashjoin;
RESET enable_material;
diff --git a/contrib/pg_overexplain/pg_overexplain.c b/contrib/pg_overexplain/pg_overexplain.c
index bd70b6d9d5e..93d2051f4fb 100644
--- a/contrib/pg_overexplain/pg_overexplain.c
+++ b/contrib/pg_overexplain/pg_overexplain.c
@@ -16,6 +16,10 @@
#include "commands/explain_format.h"
#include "commands/explain_state.h"
#include "fmgr.h"
+#include "nodes/makefuncs.h"
+#include "optimizer/extendplan.h"
+#include "optimizer/paths.h"
+#include "optimizer/planner.h"
#include "parser/parsetree.h"
#include "storage/lock.h"
#include "utils/builtins.h"
@@ -32,6 +36,12 @@ typedef struct
bool range_table;
} overexplain_options;
+typedef struct
+{
+ int total_joinrel_attempts;
+ int distinct_joinrel_count;
+} overexplain_plannerglobal;
+
static overexplain_options *overexplain_ensure_options(ExplainState *es);
static void overexplain_debug_handler(ExplainState *es, DefElem *opt,
ParseState *pstate);
@@ -57,9 +67,28 @@ static void overexplain_bitmapset(const char *qlabel, Bitmapset *bms,
static void overexplain_intlist(const char *qlabel, List *list,
ExplainState *es);
+static void overexplain_planner_setup_hook(PlannerGlobal *glob, Query *parse,
+ const char *query_string,
+ double *tuple_fraction,
+ struct ExplainState *es);
+static void overexplain_planner_shutdown_hook(PlannerGlobal *glob,
+ Query *parse,
+ const char *query_string,
+ PlannedStmt *pstmt);
+static void overexplain_set_join_pathlist_hook(PlannerInfo *root,
+ RelOptInfo *joinrel,
+ RelOptInfo *outerrel,
+ RelOptInfo *innerrel,
+ JoinType jointype,
+ JoinPathExtraData *extra);
+
static int es_extension_id;
+static int planner_extension_id = -1;
static explain_per_node_hook_type prev_explain_per_node_hook;
static explain_per_plan_hook_type prev_explain_per_plan_hook;
+static planner_setup_hook_type prev_planner_setup_hook;
+static planner_shutdown_hook_type prev_planner_shutdown_hook;
+static set_join_pathlist_hook_type prev_set_join_pathlist_hook;
/*
* Initialization we do when this module is loaded.
@@ -70,6 +99,9 @@ _PG_init(void)
/* Get an ID that we can use to cache data in an ExplainState. */
es_extension_id = GetExplainExtensionId("pg_overexplain");
+ /* Get an ID that we can use to cache data in the planner. */
+ planner_extension_id = GetPlannerExtensionId("pg_overexplain");
+
/* Register the new EXPLAIN options implemented by this module. */
RegisterExtensionExplainOption("debug", overexplain_debug_handler);
RegisterExtensionExplainOption("range_table",
@@ -80,6 +112,16 @@ _PG_init(void)
explain_per_node_hook = overexplain_per_node_hook;
prev_explain_per_plan_hook = explain_per_plan_hook;
explain_per_plan_hook = overexplain_per_plan_hook;
+
+ /* Example of planner_setup_hook/planner_shutdown_hook use */
+ prev_planner_setup_hook = planner_setup_hook;
+ planner_setup_hook = overexplain_planner_setup_hook;
+ prev_planner_shutdown_hook = planner_shutdown_hook;
+ planner_shutdown_hook = overexplain_planner_shutdown_hook;
+
+ /* Support for above example */
+ prev_set_join_pathlist_hook = set_join_pathlist_hook;
+ set_join_pathlist_hook = overexplain_set_join_pathlist_hook;
}
/*
@@ -381,6 +423,29 @@ overexplain_debug(PlannedStmt *plannedstmt, ExplainState *es)
plannedstmt->stmt_len),
es);
+ {
+ DefElem *elem = NULL;
+
+ foreach_node(DefElem, de, plannedstmt->extension_state)
+ {
+ if (strcmp(de->defname, "pg_overexplain") == 0)
+ {
+ elem = de;
+ break;
+ }
+ }
+
+ if (elem != NULL)
+ {
+ List *l = castNode(List, elem->arg);
+
+ ExplainPropertyInteger("Total Joinrel Attempts", NULL,
+ intVal(linitial(l)), es);
+ ExplainPropertyInteger("Distinct Joinrels", NULL,
+ intVal(lsecond(l)), es);
+ }
+ }
+
/* Done with this group. */
if (es->format == EXPLAIN_FORMAT_TEXT)
es->indent--;
@@ -784,3 +849,63 @@ overexplain_intlist(const char *qlabel, List *list, ExplainState *es)
pfree(buf.data);
}
+
+static void
+overexplain_planner_setup_hook(PlannerGlobal *glob, Query *parse,
+ const char *query_string,
+ double *tuple_fraction,
+ struct ExplainState *es)
+{
+ overexplain_options *options;
+ overexplain_plannerglobal *g;
+
+ if (es != NULL)
+ {
+ options = GetExplainExtensionState(es, es_extension_id);
+ if (options != NULL && options->debug)
+ {
+ g = palloc0_object(overexplain_plannerglobal);
+ SetPlannerGlobalExtensionState(glob, planner_extension_id, g);
+ }
+ }
+}
+
+static void
+overexplain_planner_shutdown_hook(PlannerGlobal *glob, Query *parse,
+ const char *query_string, PlannedStmt *pstmt)
+{
+ overexplain_plannerglobal *g;
+ DefElem *elem;
+ List *l;
+
+ g = GetPlannerGlobalExtensionState(glob, planner_extension_id);
+ if (g != NULL)
+ {
+ l = list_make2(makeInteger(g->total_joinrel_attempts),
+ makeInteger(g->distinct_joinrel_count));
+ elem = makeDefElem("pg_overexplain", (Node *) l, -1);
+ pstmt->extension_state = lappend(pstmt->extension_state, elem);
+ }
+}
+
+static void
+overexplain_set_join_pathlist_hook(PlannerInfo *root, RelOptInfo *joinrel,
+ RelOptInfo *outerrel, RelOptInfo *innerrel,
+ JoinType jointype, JoinPathExtraData *extra)
+{
+ overexplain_plannerglobal *g;
+
+ g = GetPlannerGlobalExtensionState(root->glob, planner_extension_id);
+
+ if (g != NULL)
+ {
+ g->total_joinrel_attempts++;
+
+ if (GetRelOptInfoExtensionState(joinrel, planner_extension_id) == NULL)
+ {
+ g->distinct_joinrel_count++;
+ /* set any non-NULL value to avoid double-counting */
+ SetRelOptInfoExtensionState(joinrel, planner_extension_id, g);
+ }
+ }
+}
--
2.39.5 (Apple Git-154)
Robert Haas <robertmhaas@gmail.com> writes:
Here's a new set of patches. I've added a new 0001 at the beginning to
fix the bug you identified in SetExplainExtensionState; this will need
to be back-patched to v18 once the release freeze lifts.
I'm good with 0001, and the release freeze is over, so push that
whenever you like. I'll try to look at the rest soon.
regards, tom lane
On Wed, Sep 24, 2025 at 12:18 PM Tom Lane <tgl@sss.pgh.pa.us> wrote:
I'm good with 0001, and the release freeze is over, so push that
whenever you like. I'll try to look at the rest soon.
Cool... except I thought that the release freeze wouldn't lift until
we release, which I thought was tomorrow?
--
Robert Haas
EDB: http://www.enterprisedb.com
Robert Haas <robertmhaas@gmail.com> writes:
Cool... except I thought that the release freeze wouldn't lift until
we release, which I thought was tomorrow?
Nope, the freeze is over as soon as the git tag for the release is
pushed, cf
https://wiki.postgresql.org/wiki/Committing_checklist#Release_freezes
We'd unfreeze when the stamping commit is made, except that we want
some breathing room for a re-wrap if a packager discovers a critical
problem. We give them 24 hours to report that.
(What happens if a critical problem is discovered shortly later?
We'd probably use a new minor release number in that case.)
regards, tom lane
On Wed, Sep 24, 2025 at 12:18 PM Tom Lane <tgl@sss.pgh.pa.us> wrote:
I'm good with 0001, and the release freeze is over, so push that
whenever you like. I'll try to look at the rest soon.
Done now. Here's a rebase of the rest, plus I tweaked the GEQO patch
to try to avoid a compiler warning that cfbot was complaining about.
--
Robert Haas
EDB: http://www.enterprisedb.com
Attachments:
v7-0004-Add-planner_setup_hook-and-planner_shutdown_hook.patchapplication/octet-stream; name=v7-0004-Add-planner_setup_hook-and-planner_shutdown_hook.patchDownload
From efdf3e1dafa0b509a9a14692c100ade688f91398 Mon Sep 17 00:00:00 2001
From: Robert Haas <rhaas@postgresql.org>
Date: Wed, 24 Sep 2025 11:21:55 -0400
Subject: [PATCH v7 4/6] Add planner_setup_hook and planner_shutdown_hook.
These hooks allow plugins to get control at the earliest point at
which the PlannerGlobal object is fully initialized, and then just
before it gets destroyed. This is useful in combination with the
extendable plan state facilities (see extendplan.h) and perhaps for
other purposes as well.
---
src/backend/optimizer/plan/planner.c | 14 ++++++++++++++
src/include/optimizer/planner.h | 13 +++++++++++++
2 files changed, 27 insertions(+)
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index 205d8886a2a..ebcf9ea5851 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -73,6 +73,12 @@ bool enable_distinct_reordering = true;
/* Hook for plugins to get control in planner() */
planner_hook_type planner_hook = NULL;
+/* Hook for plugins to get control after PlannerGlobal is initialized */
+planner_setup_hook_type planner_setup_hook = NULL;
+
+/* Hook for plugins to get control before PlannerGlobal is discarded */
+planner_shutdown_hook_type planner_shutdown_hook = NULL;
+
/* Hook for plugins to get control when grouping_planner() plans upper rels */
create_upper_paths_hook_type create_upper_paths_hook = NULL;
@@ -440,6 +446,10 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions,
tuple_fraction = 0.0;
}
+ /* Allow plugins to take control after we've initialized "glob" */
+ if (planner_setup_hook)
+ (*planner_setup_hook) (glob, parse, query_string, &tuple_fraction, es);
+
/* primary planning entry point (may recurse for subqueries) */
root = subquery_planner(glob, parse, NULL, false, tuple_fraction, NULL);
@@ -618,6 +628,10 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions,
result->jitFlags |= PGJIT_DEFORM;
}
+ /* Allow plugins to take control before we discard "glob" */
+ if (planner_shutdown_hook)
+ (*planner_shutdown_hook) (glob, parse, query_string, result);
+
if (glob->partition_directory != NULL)
DestroyPartitionDirectory(glob->partition_directory);
diff --git a/src/include/optimizer/planner.h b/src/include/optimizer/planner.h
index b31ea2fbbdc..a39389728aa 100644
--- a/src/include/optimizer/planner.h
+++ b/src/include/optimizer/planner.h
@@ -32,6 +32,19 @@ typedef PlannedStmt *(*planner_hook_type) (Query *parse,
ExplainState *es);
extern PGDLLIMPORT planner_hook_type planner_hook;
+/* Hook for plugins to get control after PlannerGlobal is initialized */
+typedef void (*planner_setup_hook_type) (PlannerGlobal *glob, Query *parse,
+ const char *query_string,
+ double *tuple_fraction,
+ ExplainState *es);
+extern PGDLLIMPORT planner_setup_hook_type planner_setup_hook;
+
+/* Hook for plugins to get control before PlannerGlobal is discarded */
+typedef void (*planner_shutdown_hook_type) (PlannerGlobal *glob, Query *parse,
+ const char *query_string,
+ PlannedStmt *pstmt);
+extern PGDLLIMPORT planner_shutdown_hook_type planner_shutdown_hook;
+
/* Hook for plugins to get control when grouping_planner() plans upper rels */
typedef void (*create_upper_paths_hook_type) (PlannerInfo *root,
UpperRelationKind stage,
--
2.39.5 (Apple Git-154)
v7-0001-Allow-private-state-in-certain-planner-data-struc.patchapplication/octet-stream; name=v7-0001-Allow-private-state-in-certain-planner-data-struc.patchDownload
From 086b9395a959ad9bc640286a1e24da06fc915b7c Mon Sep 17 00:00:00 2001
From: Robert Haas <rhaas@postgresql.org>
Date: Wed, 24 Sep 2025 11:00:07 -0400
Subject: [PATCH v7 1/6] Allow private state in certain planner data
structures.
Extension that make extensive use of planner hooks may want to
coordinate their efforts, for example to avoid duplicate computation,
but that's currently difficult because there's no really good way to
pass data between different hooks.
To make that easier, allow for storage of extension-managed private
state in PlannerGlobal, PlannerInfo, and RelOptInfo, along very
similar lines to what we have permitted for ExplainState since commit
c65bc2e1d14a2d4daed7c1921ac518f2c5ac3d17.
---
src/backend/optimizer/util/Makefile | 1 +
src/backend/optimizer/util/extendplan.c | 183 ++++++++++++++++++++++++
src/backend/optimizer/util/meson.build | 1 +
src/include/nodes/pathnodes.h | 12 ++
src/include/optimizer/extendplan.h | 72 ++++++++++
5 files changed, 269 insertions(+)
create mode 100644 src/backend/optimizer/util/extendplan.c
create mode 100644 src/include/optimizer/extendplan.h
diff --git a/src/backend/optimizer/util/Makefile b/src/backend/optimizer/util/Makefile
index 4fb115cb118..87b4c3c0869 100644
--- a/src/backend/optimizer/util/Makefile
+++ b/src/backend/optimizer/util/Makefile
@@ -15,6 +15,7 @@ include $(top_builddir)/src/Makefile.global
OBJS = \
appendinfo.o \
clauses.o \
+ extendplan.o \
inherit.o \
joininfo.o \
orclauses.o \
diff --git a/src/backend/optimizer/util/extendplan.c b/src/backend/optimizer/util/extendplan.c
new file mode 100644
index 00000000000..03d32277ba1
--- /dev/null
+++ b/src/backend/optimizer/util/extendplan.c
@@ -0,0 +1,183 @@
+/*-------------------------------------------------------------------------
+ *
+ * extendplan.c
+ * Extend core planner objects with additional private state
+ *
+ * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994-5, Regents of the University of California
+ *
+ * The interfaces defined in this file make it possible for loadable
+ * modules to store their own private state inside of key planner data
+ * structures -- specifically, the PlannerGlobal, PlannerInfo, and
+ * RelOptInfo structures. This can make it much easier to write
+ * reasonably efficient planner extensions; for instance, code that
+ * uses set_join_pathlist_hook can arrange to compute a key intermediate
+ * result once per joinrel rather than on every call.
+ *
+ * IDENTIFICATION
+ * src/backend/optimizer/util/extendplan.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "optimizer/extendplan.h"
+#include "port/pg_bitutils.h"
+#include "utils/memutils.h"
+
+static const char **PlannerExtensionNameArray = NULL;
+static int PlannerExtensionNamesAssigned = 0;
+static int PlannerExtensionNamesAllocated = 0;
+
+/*
+ * Map the name of a planner extension to an integer ID.
+ *
+ * Within the lifetime of a particular backend, the same name will be mapped
+ * to the same ID every time. IDs are not stable across backends. Use the ID
+ * that you get from this function to call the remaining functions in this
+ * file.
+ */
+int
+GetPlannerExtensionId(const char *extension_name)
+{
+ /* Search for an existing extension by this name; if found, return ID. */
+ for (int i = 0; i < PlannerExtensionNamesAssigned; ++i)
+ if (strcmp(PlannerExtensionNameArray[i], extension_name) == 0)
+ return i;
+
+ /* If there is no array yet, create one. */
+ if (PlannerExtensionNameArray == NULL)
+ {
+ PlannerExtensionNamesAllocated = 16;
+ PlannerExtensionNameArray = (const char **)
+ MemoryContextAlloc(TopMemoryContext,
+ PlannerExtensionNamesAllocated
+ * sizeof(char *));
+ }
+
+ /* If there's an array but it's currently full, expand it. */
+ if (PlannerExtensionNamesAssigned >= PlannerExtensionNamesAllocated)
+ {
+ int i = pg_nextpower2_32(PlannerExtensionNamesAssigned + 1);
+
+ PlannerExtensionNameArray = (const char **)
+ repalloc(PlannerExtensionNameArray, i * sizeof(char *));
+ PlannerExtensionNamesAllocated = i;
+ }
+
+ /* Assign and return new ID. */
+ PlannerExtensionNameArray[PlannerExtensionNamesAssigned] = extension_name;
+ return PlannerExtensionNamesAssigned++;
+}
+
+/*
+ * Store extension-specific state into a PlannerGlobal.
+ */
+void
+SetPlannerGlobalExtensionState(PlannerGlobal *glob, int extension_id,
+ void *opaque)
+{
+ Assert(extension_id >= 0);
+
+ /* If there is no array yet, create one. */
+ if (glob->extension_state == NULL)
+ {
+ MemoryContext planner_cxt;
+ Size sz;
+
+ planner_cxt = GetMemoryChunkContext(glob);
+ glob->extension_state_allocated =
+ Max(4, pg_nextpower2_32(extension_id + 1));
+ sz = glob->extension_state_allocated * sizeof(void *);
+ glob->extension_state = MemoryContextAllocZero(planner_cxt, sz);
+ }
+
+ /* If there's an array but it's currently full, expand it. */
+ if (extension_id >= glob->extension_state_allocated)
+ {
+ int i;
+
+ i = pg_nextpower2_32(extension_id + 1);
+ glob->extension_state = (void **)
+ repalloc0(glob->extension_state,
+ glob->extension_state_allocated * sizeof(void *),
+ i * sizeof(void *));
+ glob->extension_state_allocated = i;
+ }
+
+ glob->extension_state[extension_id] = opaque;
+}
+
+/*
+ * Store extension-specific state into a PlannerInfo.
+ */
+void
+SetPlannerInfoExtensionState(PlannerInfo *root, int extension_id,
+ void *opaque)
+{
+ Assert(extension_id >= 0);
+
+ /* If there is no array yet, create one. */
+ if (root->extension_state == NULL)
+ {
+ Size sz;
+
+ root->extension_state_allocated =
+ Max(4, pg_nextpower2_32(extension_id + 1));
+ sz = root->extension_state_allocated * sizeof(void *);
+ root->extension_state = MemoryContextAllocZero(root->planner_cxt, sz);
+ }
+
+ /* If there's an array but it's currently full, expand it. */
+ if (extension_id >= root->extension_state_allocated)
+ {
+ int i;
+
+ i = pg_nextpower2_32(extension_id + 1);
+ root->extension_state = (void **)
+ repalloc0(root->extension_state,
+ root->extension_state_allocated * sizeof(void *),
+ i * sizeof(void *));
+ root->extension_state_allocated = i;
+ }
+
+ root->extension_state[extension_id] = opaque;
+}
+
+/*
+ * Store extension-specific state into a RelOptInfo.
+ */
+void
+SetRelOptInfoExtensionState(RelOptInfo *rel, int extension_id,
+ void *opaque)
+{
+ Assert(extension_id >= 0);
+
+ /* If there is no array yet, create one. */
+ if (rel->extension_state == NULL)
+ {
+ MemoryContext planner_cxt;
+ Size sz;
+
+ planner_cxt = GetMemoryChunkContext(rel);
+ rel->extension_state_allocated =
+ Max(4, pg_nextpower2_32(extension_id + 1));
+ sz = rel->extension_state_allocated * sizeof(void *);
+ rel->extension_state = MemoryContextAllocZero(planner_cxt, sz);
+ }
+
+ /* If there's an array but it's currently full, expand it. */
+ if (extension_id >= rel->extension_state_allocated)
+ {
+ int i;
+
+ i = pg_nextpower2_32(extension_id + 1);
+ rel->extension_state = (void **)
+ repalloc0(rel->extension_state,
+ rel->extension_state_allocated * sizeof(void *),
+ i * sizeof(void *));
+ rel->extension_state_allocated = i;
+ }
+
+ rel->extension_state[extension_id] = opaque;
+}
diff --git a/src/backend/optimizer/util/meson.build b/src/backend/optimizer/util/meson.build
index b3bf913d096..f71f56e37a1 100644
--- a/src/backend/optimizer/util/meson.build
+++ b/src/backend/optimizer/util/meson.build
@@ -3,6 +3,7 @@
backend_sources += files(
'appendinfo.c',
'clauses.c',
+ 'extendplan.c',
'inherit.c',
'joininfo.c',
'orclauses.c',
diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h
index b12a2508d8c..5cf23cba596 100644
--- a/src/include/nodes/pathnodes.h
+++ b/src/include/nodes/pathnodes.h
@@ -182,6 +182,10 @@ typedef struct PlannerGlobal
/* hash table for NOT NULL attnums of relations */
struct HTAB *rel_notnullatts_hash pg_node_attr(read_write_ignore);
+
+ /* extension state */
+ void **extension_state pg_node_attr(read_write_ignore);
+ int extension_state_allocated;
} PlannerGlobal;
/* macro for fetching the Plan associated with a SubPlan node */
@@ -580,6 +584,10 @@ struct PlannerInfo
/* PartitionPruneInfos added in this query's plan. */
List *partPruneInfos;
+
+ /* extension state */
+ void **extension_state pg_node_attr(read_write_ignore);
+ int extension_state_allocated;
};
@@ -1091,6 +1099,10 @@ typedef struct RelOptInfo
List **partexprs pg_node_attr(read_write_ignore);
/* Nullable partition key expressions */
List **nullable_partexprs pg_node_attr(read_write_ignore);
+
+ /* extension state */
+ void **extension_state pg_node_attr(read_write_ignore);
+ int extension_state_allocated;
} RelOptInfo;
/*
diff --git a/src/include/optimizer/extendplan.h b/src/include/optimizer/extendplan.h
new file mode 100644
index 00000000000..de9618761dd
--- /dev/null
+++ b/src/include/optimizer/extendplan.h
@@ -0,0 +1,72 @@
+/*-------------------------------------------------------------------------
+ *
+ * extendplan.h
+ * Extend core planner objects with additional private state
+ *
+ *
+ * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/optimizer/extendplan.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef EXTENDPLAN_H
+#define EXTENDPLAN_H
+
+#include "nodes/pathnodes.h"
+
+extern int GetPlannerExtensionId(const char *extension_name);
+
+/*
+ * Get extension-specific state from a PlannerGlobal.
+ */
+static inline void *
+GetPlannerGlobalExtensionState(PlannerGlobal *glob, int extension_id)
+{
+ Assert(extension_id >= 0);
+
+ if (extension_id >= glob->extension_state_allocated)
+ return NULL;
+
+ return glob->extension_state[extension_id];
+}
+
+/*
+ * Get extension-specific state from a PlannerInfo.
+ */
+static inline void *
+GetPlannerInfoExtensionState(PlannerInfo *root, int extension_id)
+{
+ Assert(extension_id >= 0);
+
+ if (extension_id >= root->extension_state_allocated)
+ return NULL;
+
+ return root->extension_state[extension_id];
+}
+
+/*
+ * Get extension-specific state from a PlannerInfo.
+ */
+static inline void *
+GetRelOptInfoExtensionState(RelOptInfo *rel, int extension_id)
+{
+ Assert(extension_id >= 0);
+
+ if (extension_id >= rel->extension_state_allocated)
+ return NULL;
+
+ return rel->extension_state[extension_id];
+}
+
+/* Functions to store private state into various planner objects */
+extern void SetPlannerGlobalExtensionState(PlannerGlobal *glob,
+ int extension_id,
+ void *opaque);
+extern void SetPlannerInfoExtensionState(PlannerInfo *root, int extension_id,
+ void *opaque);
+extern void SetRelOptInfoExtensionState(RelOptInfo *rel, int extension_id,
+ void *opaque);
+
+#endif
--
2.39.5 (Apple Git-154)
v7-0002-Remove-PlannerInfo-s-join_search_private-method.patchapplication/octet-stream; name=v7-0002-Remove-PlannerInfo-s-join_search_private-method.patchDownload
From 924d15c2058c4300b5eaccc5e5a7608b47f643c0 Mon Sep 17 00:00:00 2001
From: Robert Haas <rhaas@postgresql.org>
Date: Wed, 20 Aug 2025 15:10:52 -0400
Subject: [PATCH v7 2/6] Remove PlannerInfo's join_search_private method.
Instead, use the new mechanism that allows planner extensions to store
private state inside a PlannerInfo, treating GEQO as an in-core planner
extension. This is a useful test of the new facility, and also buys
back a few bytes of storage.
To make this work, we must remove innerrel_is_unique_ext's hack of
testing whether join_search_private is set as a proxy for whether
the join search might be retried. Add a flag that extensions can
use to explicitly signal their intentions instead.
---
src/backend/optimizer/geqo/geqo_eval.c | 2 +-
src/backend/optimizer/geqo/geqo_main.c | 12 ++++++++++--
src/backend/optimizer/geqo/geqo_random.c | 7 +++----
src/backend/optimizer/plan/analyzejoins.c | 9 +++------
src/backend/optimizer/plan/planner.c | 1 +
src/backend/optimizer/prep/prepjointree.c | 1 +
src/include/nodes/pathnodes.h | 5 ++---
src/include/optimizer/geqo.h | 12 +++++++++++-
8 files changed, 32 insertions(+), 17 deletions(-)
diff --git a/src/backend/optimizer/geqo/geqo_eval.c b/src/backend/optimizer/geqo/geqo_eval.c
index f07d1dc8ac6..7fcb1aa70d1 100644
--- a/src/backend/optimizer/geqo/geqo_eval.c
+++ b/src/backend/optimizer/geqo/geqo_eval.c
@@ -162,7 +162,7 @@ geqo_eval(PlannerInfo *root, Gene *tour, int num_gene)
RelOptInfo *
gimme_tree(PlannerInfo *root, Gene *tour, int num_gene)
{
- GeqoPrivateData *private = (GeqoPrivateData *) root->join_search_private;
+ GeqoPrivateData *private = GetGeqoPrivateData(root);
List *clumps;
int rel_count;
diff --git a/src/backend/optimizer/geqo/geqo_main.c b/src/backend/optimizer/geqo/geqo_main.c
index 38402ce58db..0064556087a 100644
--- a/src/backend/optimizer/geqo/geqo_main.c
+++ b/src/backend/optimizer/geqo/geqo_main.c
@@ -47,6 +47,8 @@ int Geqo_generations;
double Geqo_selection_bias;
double Geqo_seed;
+/* GEQO is treated as an in-core planner extension */
+int Geqo_planner_extension_id = -1;
static int gimme_pool_size(int nr_rel);
static int gimme_number_generations(int pool_size);
@@ -98,10 +100,16 @@ geqo(PlannerInfo *root, int number_of_rels, List *initial_rels)
int mutations = 0;
#endif
+ if (Geqo_planner_extension_id < 0)
+ Geqo_planner_extension_id = GetPlannerExtensionId("geqo");
+
/* set up private information */
- root->join_search_private = &private;
+ SetPlannerInfoExtensionState(root, Geqo_planner_extension_id, &private);
private.initial_rels = initial_rels;
+/* inform core planner that we may replan */
+ root->assumeReplanning = true;
+
/* initialize private number generator */
geqo_set_seed(root, Geqo_seed);
@@ -304,7 +312,7 @@ geqo(PlannerInfo *root, int number_of_rels, List *initial_rels)
free_pool(root, pool);
/* ... clear root pointer to our private storage */
- root->join_search_private = NULL;
+ SetPlannerInfoExtensionState(root, Geqo_planner_extension_id, NULL);
return best_rel;
}
diff --git a/src/backend/optimizer/geqo/geqo_random.c b/src/backend/optimizer/geqo/geqo_random.c
index 6c7a411f69f..46d28baa2e6 100644
--- a/src/backend/optimizer/geqo/geqo_random.c
+++ b/src/backend/optimizer/geqo/geqo_random.c
@@ -15,11 +15,10 @@
#include "optimizer/geqo_random.h"
-
void
geqo_set_seed(PlannerInfo *root, double seed)
{
- GeqoPrivateData *private = (GeqoPrivateData *) root->join_search_private;
+ GeqoPrivateData *private = GetGeqoPrivateData(root);
pg_prng_fseed(&private->random_state, seed);
}
@@ -27,7 +26,7 @@ geqo_set_seed(PlannerInfo *root, double seed)
double
geqo_rand(PlannerInfo *root)
{
- GeqoPrivateData *private = (GeqoPrivateData *) root->join_search_private;
+ GeqoPrivateData *private = GetGeqoPrivateData(root);
return pg_prng_double(&private->random_state);
}
@@ -35,7 +34,7 @@ geqo_rand(PlannerInfo *root)
int
geqo_randint(PlannerInfo *root, int upper, int lower)
{
- GeqoPrivateData *private = (GeqoPrivateData *) root->join_search_private;
+ GeqoPrivateData *private = GetGeqoPrivateData(root);
/*
* In current usage, "lower" is never negative so we can just use
diff --git a/src/backend/optimizer/plan/analyzejoins.c b/src/backend/optimizer/plan/analyzejoins.c
index 2a3dea88a94..6a3c030e8ef 100644
--- a/src/backend/optimizer/plan/analyzejoins.c
+++ b/src/backend/optimizer/plan/analyzejoins.c
@@ -1425,17 +1425,14 @@ innerrel_is_unique_ext(PlannerInfo *root,
*
* However, in normal planning mode, caching this knowledge is totally
* pointless; it won't be queried again, because we build up joinrels
- * from smaller to larger. It is useful in GEQO mode, where the
- * knowledge can be carried across successive planning attempts; and
- * it's likely to be useful when using join-search plugins, too. Hence
- * cache when join_search_private is non-NULL. (Yeah, that's a hack,
- * but it seems reasonable.)
+ * from smaller to larger. It's only useful when using GEQO or
+ * another planner extension that attempts planning multiple times.
*
* Also, allow callers to override that heuristic and force caching;
* that's useful for reduce_unique_semijoins, which calls here before
* the normal join search starts.
*/
- if (force_cache || root->join_search_private)
+ if (force_cache || root->assumeReplanning)
{
old_context = MemoryContextSwitchTo(root->planner_cxt);
innerrel->non_unique_for_rels =
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index 41bd8353430..9de39da1757 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -703,6 +703,7 @@ subquery_planner(PlannerGlobal *glob, Query *parse, PlannerInfo *parent_root,
root->hasAlternativeSubPlans = false;
root->placeholdersFrozen = false;
root->hasRecursion = hasRecursion;
+ root->assumeReplanning = false;
if (hasRecursion)
root->wt_param_id = assign_special_exec_param(root);
else
diff --git a/src/backend/optimizer/prep/prepjointree.c b/src/backend/optimizer/prep/prepjointree.c
index 35e8d3c183b..4075f7519ca 100644
--- a/src/backend/optimizer/prep/prepjointree.c
+++ b/src/backend/optimizer/prep/prepjointree.c
@@ -1383,6 +1383,7 @@ pull_up_simple_subquery(PlannerInfo *root, Node *jtnode, RangeTblEntry *rte,
subroot->qual_security_level = 0;
subroot->placeholdersFrozen = false;
subroot->hasRecursion = false;
+ subroot->assumeReplanning = false;
subroot->wt_param_id = -1;
subroot->non_recursive_path = NULL;
/* We don't currently need a top JoinDomain for the subroot */
diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h
index 5cf23cba596..bc1e0c1b5cc 100644
--- a/src/include/nodes/pathnodes.h
+++ b/src/include/nodes/pathnodes.h
@@ -530,6 +530,8 @@ struct PlannerInfo
bool placeholdersFrozen;
/* true if planning a recursive WITH item */
bool hasRecursion;
+ /* true if a planner extension may replan this subquery */
+ bool assumeReplanning;
/*
* The rangetable index for the RTE_GROUP RTE, or 0 if there is no
@@ -576,9 +578,6 @@ struct PlannerInfo
bool *isAltSubplan pg_node_attr(read_write_ignore);
bool *isUsedSubplan pg_node_attr(read_write_ignore);
- /* optional private data for join_search_hook, e.g., GEQO */
- void *join_search_private pg_node_attr(read_write_ignore);
-
/* Does this query modify any partition key columns? */
bool partColsUpdated;
diff --git a/src/include/optimizer/geqo.h b/src/include/optimizer/geqo.h
index 9f8e0f337aa..b3017dd8ec4 100644
--- a/src/include/optimizer/geqo.h
+++ b/src/include/optimizer/geqo.h
@@ -24,6 +24,7 @@
#include "common/pg_prng.h"
#include "nodes/pathnodes.h"
+#include "optimizer/extendplan.h"
#include "optimizer/geqo_gene.h"
@@ -62,6 +63,8 @@ extern PGDLLIMPORT int Geqo_generations; /* 1 .. inf, or 0 to use default */
extern PGDLLIMPORT double Geqo_selection_bias;
+extern PGDLLIMPORT int Geqo_planner_extension_id;
+
#define DEFAULT_GEQO_SELECTION_BIAS 2.0
#define MIN_GEQO_SELECTION_BIAS 1.5
#define MAX_GEQO_SELECTION_BIAS 2.0
@@ -70,7 +73,7 @@ extern PGDLLIMPORT double Geqo_seed; /* 0 .. 1 */
/*
- * Private state for a GEQO run --- accessible via root->join_search_private
+ * Private state for a GEQO run --- accessible via GetGeqoPrivateData
*/
typedef struct
{
@@ -78,6 +81,13 @@ typedef struct
pg_prng_state random_state; /* PRNG state */
} GeqoPrivateData;
+static inline GeqoPrivateData *
+GetGeqoPrivateData(PlannerInfo *root)
+{
+ /* headers must be C++-compliant, so the cast is required here */
+ return (GeqoPrivateData *)
+ GetPlannerInfoExtensionState(root, Geqo_planner_extension_id);
+}
/* routines in geqo_main.c */
extern RelOptInfo *geqo(PlannerInfo *root,
--
2.39.5 (Apple Git-154)
v7-0003-Add-ExplainState-argument-to-pg_plan_query-and-pl.patchapplication/octet-stream; name=v7-0003-Add-ExplainState-argument-to-pg_plan_query-and-pl.patchDownload
From c8d9a04dc2f04b7d520bc747b839c3f7e9b0683e Mon Sep 17 00:00:00 2001
From: Robert Haas <rhaas@postgresql.org>
Date: Wed, 24 Sep 2025 11:18:12 -0400
Subject: [PATCH v7 3/6] Add ExplainState argument to pg_plan_query() and
planner().
This allows extensions to have access to any data they've stored
in the ExplainState during planning. Unfortunately, it won't help
with EXPLAIN EXECUTE is used, but since that case is less common,
this still seems like an improvement.
---
contrib/pg_stat_statements/pg_stat_statements.c | 14 ++++++++------
src/backend/commands/copyto.c | 2 +-
src/backend/commands/createas.c | 2 +-
src/backend/commands/explain.c | 2 +-
src/backend/commands/matview.c | 2 +-
src/backend/commands/portalcmds.c | 3 ++-
src/backend/optimizer/plan/planner.c | 10 ++++++----
src/backend/tcop/postgres.c | 7 ++++---
src/include/optimizer/optimizer.h | 5 ++++-
src/include/optimizer/planner.h | 8 ++++++--
src/include/tcop/tcopprot.h | 4 +++-
src/test/modules/delay_execution/delay_execution.c | 7 ++++---
12 files changed, 41 insertions(+), 25 deletions(-)
diff --git a/contrib/pg_stat_statements/pg_stat_statements.c b/contrib/pg_stat_statements/pg_stat_statements.c
index 0bb0f933399..d6af2f8efbf 100644
--- a/contrib/pg_stat_statements/pg_stat_statements.c
+++ b/contrib/pg_stat_statements/pg_stat_statements.c
@@ -337,7 +337,8 @@ static void pgss_post_parse_analyze(ParseState *pstate, Query *query,
static PlannedStmt *pgss_planner(Query *parse,
const char *query_string,
int cursorOptions,
- ParamListInfo boundParams);
+ ParamListInfo boundParams,
+ ExplainState *es);
static void pgss_ExecutorStart(QueryDesc *queryDesc, int eflags);
static void pgss_ExecutorRun(QueryDesc *queryDesc,
ScanDirection direction,
@@ -893,7 +894,8 @@ static PlannedStmt *
pgss_planner(Query *parse,
const char *query_string,
int cursorOptions,
- ParamListInfo boundParams)
+ ParamListInfo boundParams,
+ ExplainState *es)
{
PlannedStmt *result;
@@ -928,10 +930,10 @@ pgss_planner(Query *parse,
{
if (prev_planner_hook)
result = prev_planner_hook(parse, query_string, cursorOptions,
- boundParams);
+ boundParams, es);
else
result = standard_planner(parse, query_string, cursorOptions,
- boundParams);
+ boundParams, es);
}
PG_FINALLY();
{
@@ -977,10 +979,10 @@ pgss_planner(Query *parse,
{
if (prev_planner_hook)
result = prev_planner_hook(parse, query_string, cursorOptions,
- boundParams);
+ boundParams, es);
else
result = standard_planner(parse, query_string, cursorOptions,
- boundParams);
+ boundParams, es);
}
PG_FINALLY();
{
diff --git a/src/backend/commands/copyto.c b/src/backend/commands/copyto.c
index 67b94b91cae..e5781155cdf 100644
--- a/src/backend/commands/copyto.c
+++ b/src/backend/commands/copyto.c
@@ -796,7 +796,7 @@ BeginCopyTo(ParseState *pstate,
/* plan the query */
plan = pg_plan_query(query, pstate->p_sourcetext,
- CURSOR_OPT_PARALLEL_OK, NULL);
+ CURSOR_OPT_PARALLEL_OK, NULL, NULL);
/*
* With row-level security and a user using "COPY relation TO", we
diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c
index dfd2ab8e862..1ccc2e55c64 100644
--- a/src/backend/commands/createas.c
+++ b/src/backend/commands/createas.c
@@ -321,7 +321,7 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt,
/* plan the query */
plan = pg_plan_query(query, pstate->p_sourcetext,
- CURSOR_OPT_PARALLEL_OK, params);
+ CURSOR_OPT_PARALLEL_OK, params, NULL);
/*
* Use a snapshot with an updated command ID to ensure this query sees
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 207f86f1d39..82d14db8d68 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -351,7 +351,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, params, es);
INSTR_TIME_SET_CURRENT(planduration);
INSTR_TIME_SUBTRACT(planduration, planstart);
diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index 188e26f0e6e..441de55ac24 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -426,7 +426,7 @@ refresh_matview_datafill(DestReceiver *dest, Query *query,
CHECK_FOR_INTERRUPTS();
/* Plan the query which will generate data for the refresh. */
- plan = pg_plan_query(query, queryString, CURSOR_OPT_PARALLEL_OK, NULL);
+ plan = pg_plan_query(query, queryString, CURSOR_OPT_PARALLEL_OK, NULL, NULL);
/*
* Use a snapshot with an updated command ID to ensure this query sees
diff --git a/src/backend/commands/portalcmds.c b/src/backend/commands/portalcmds.c
index e7c8171c102..ec96c2efcd3 100644
--- a/src/backend/commands/portalcmds.c
+++ b/src/backend/commands/portalcmds.c
@@ -99,7 +99,8 @@ PerformCursorOpen(ParseState *pstate, DeclareCursorStmt *cstmt, ParamListInfo pa
elog(ERROR, "non-SELECT statement in DECLARE CURSOR");
/* Plan the query, applying the specified options */
- plan = pg_plan_query(query, pstate->p_sourcetext, cstmt->options, params);
+ plan = pg_plan_query(query, pstate->p_sourcetext, cstmt->options, params,
+ NULL);
/*
* Create a portal and copy the plan and query string into its memory.
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index 9de39da1757..205d8886a2a 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -291,14 +291,16 @@ static void create_partial_unique_paths(PlannerInfo *root, RelOptInfo *input_rel
*****************************************************************************/
PlannedStmt *
planner(Query *parse, const char *query_string, int cursorOptions,
- ParamListInfo boundParams)
+ ParamListInfo boundParams, ExplainState *es)
{
PlannedStmt *result;
if (planner_hook)
- result = (*planner_hook) (parse, query_string, cursorOptions, boundParams);
+ result = (*planner_hook) (parse, query_string, cursorOptions,
+ boundParams, es);
else
- result = standard_planner(parse, query_string, cursorOptions, boundParams);
+ result = standard_planner(parse, query_string, cursorOptions,
+ boundParams, es);
pgstat_report_plan_id(result->planId, false);
@@ -307,7 +309,7 @@ planner(Query *parse, const char *query_string, int cursorOptions,
PlannedStmt *
standard_planner(Query *parse, const char *query_string, int cursorOptions,
- ParamListInfo boundParams)
+ ParamListInfo boundParams, ExplainState *es)
{
PlannedStmt *result;
PlannerGlobal *glob;
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index d356830f756..7dd75a490aa 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -37,6 +37,7 @@
#include "catalog/pg_type.h"
#include "commands/async.h"
#include "commands/event_trigger.h"
+#include "commands/explain_state.h"
#include "commands/prepare.h"
#include "common/pg_prng.h"
#include "jit/jit.h"
@@ -884,7 +885,7 @@ pg_rewrite_query(Query *query)
*/
PlannedStmt *
pg_plan_query(Query *querytree, const char *query_string, int cursorOptions,
- ParamListInfo boundParams)
+ ParamListInfo boundParams, ExplainState *es)
{
PlannedStmt *plan;
@@ -901,7 +902,7 @@ pg_plan_query(Query *querytree, const char *query_string, int cursorOptions,
ResetUsage();
/* call the optimizer */
- plan = planner(querytree, query_string, cursorOptions, boundParams);
+ plan = planner(querytree, query_string, cursorOptions, boundParams, es);
if (log_planner_stats)
ShowUsage("PLANNER STATISTICS");
@@ -997,7 +998,7 @@ pg_plan_queries(List *querytrees, const char *query_string, int cursorOptions,
else
{
stmt = pg_plan_query(query, query_string, cursorOptions,
- boundParams);
+ boundParams, NULL);
}
stmt_list = lappend(stmt_list, stmt);
diff --git a/src/include/optimizer/optimizer.h b/src/include/optimizer/optimizer.h
index 04878f1f1c2..a34113903c0 100644
--- a/src/include/optimizer/optimizer.h
+++ b/src/include/optimizer/optimizer.h
@@ -24,6 +24,8 @@
#include "nodes/parsenodes.h"
+typedef struct ExplainState ExplainState; /* defined in explain_state.h */
+
/*
* We don't want to include nodes/pathnodes.h here, because non-planner
* code should generally treat PlannerInfo as an opaque typedef.
@@ -104,7 +106,8 @@ extern PGDLLIMPORT bool enable_distinct_reordering;
extern PlannedStmt *planner(Query *parse, const char *query_string,
int cursorOptions,
- ParamListInfo boundParams);
+ ParamListInfo boundParams,
+ ExplainState *es);
extern Expr *expression_planner(Expr *expr);
extern Expr *expression_planner_with_deps(Expr *expr,
diff --git a/src/include/optimizer/planner.h b/src/include/optimizer/planner.h
index f220e9a270d..b31ea2fbbdc 100644
--- a/src/include/optimizer/planner.h
+++ b/src/include/optimizer/planner.h
@@ -22,11 +22,14 @@
#include "nodes/plannodes.h"
+typedef struct ExplainState ExplainState; /* defined in explain_state.h */
+
/* Hook for plugins to get control in planner() */
typedef PlannedStmt *(*planner_hook_type) (Query *parse,
const char *query_string,
int cursorOptions,
- ParamListInfo boundParams);
+ ParamListInfo boundParams,
+ ExplainState *es);
extern PGDLLIMPORT planner_hook_type planner_hook;
/* Hook for plugins to get control when grouping_planner() plans upper rels */
@@ -40,7 +43,8 @@ extern PGDLLIMPORT create_upper_paths_hook_type create_upper_paths_hook;
extern PlannedStmt *standard_planner(Query *parse, const char *query_string,
int cursorOptions,
- ParamListInfo boundParams);
+ ParamListInfo boundParams,
+ ExplainState *es);
extern PlannerInfo *subquery_planner(PlannerGlobal *glob, Query *parse,
PlannerInfo *parent_root,
diff --git a/src/include/tcop/tcopprot.h b/src/include/tcop/tcopprot.h
index a83cc4f4850..c1bcfdec673 100644
--- a/src/include/tcop/tcopprot.h
+++ b/src/include/tcop/tcopprot.h
@@ -20,6 +20,7 @@
#include "utils/guc.h"
#include "utils/queryenvironment.h"
+typedef struct ExplainState ExplainState; /* defined in explain_state.h */
extern PGDLLIMPORT CommandDest whereToSendOutput;
extern PGDLLIMPORT const char *debug_query_string;
@@ -63,7 +64,8 @@ extern List *pg_analyze_and_rewrite_withcb(RawStmt *parsetree,
QueryEnvironment *queryEnv);
extern PlannedStmt *pg_plan_query(Query *querytree, const char *query_string,
int cursorOptions,
- ParamListInfo boundParams);
+ ParamListInfo boundParams,
+ ExplainState *es);
extern List *pg_plan_queries(List *querytrees, const char *query_string,
int cursorOptions,
ParamListInfo boundParams);
diff --git a/src/test/modules/delay_execution/delay_execution.c b/src/test/modules/delay_execution/delay_execution.c
index 7bc97f84a1c..d933e9a6e53 100644
--- a/src/test/modules/delay_execution/delay_execution.c
+++ b/src/test/modules/delay_execution/delay_execution.c
@@ -40,17 +40,18 @@ static planner_hook_type prev_planner_hook = NULL;
/* planner_hook function to provide the desired delay */
static PlannedStmt *
delay_execution_planner(Query *parse, const char *query_string,
- int cursorOptions, ParamListInfo boundParams)
+ int cursorOptions, ParamListInfo boundParams,
+ ExplainState *es)
{
PlannedStmt *result;
/* Invoke the planner, possibly via a previous hook user */
if (prev_planner_hook)
result = prev_planner_hook(parse, query_string, cursorOptions,
- boundParams);
+ boundParams, es);
else
result = standard_planner(parse, query_string, cursorOptions,
- boundParams);
+ boundParams, es);
/* If enabled, delay by taking and releasing the specified lock */
if (post_planning_lock_id != 0)
--
2.39.5 (Apple Git-154)
v7-0005-Add-extension_state-member-to-PlannedStmt.patchapplication/octet-stream; name=v7-0005-Add-extension_state-member-to-PlannedStmt.patchDownload
From 859ad61547be85695a17158249ec3cad2710b018 Mon Sep 17 00:00:00 2001
From: Robert Haas <rhaas@postgresql.org>
Date: Mon, 25 Aug 2025 14:29:02 -0400
Subject: [PATCH v7 5/6] Add extension_state member to PlannedStmt.
Extensions can stash data computed at plan time into this list using
planner_shutdown_hook (or perhaps other mechanisms) and then access
it from any code that has access to the PlannedStmt (such as explain
hooks), allowing for extensible debugging and instrumentation of
plans.
---
src/include/nodes/plannodes.h | 9 +++++++++
1 file changed, 9 insertions(+)
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index 3d196f5078e..77ec2bc10b2 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -149,6 +149,15 @@ typedef struct PlannedStmt
/* non-null if this is utility stmt */
Node *utilityStmt;
+ /*
+ * DefElem objects added by extensions, e.g. using planner_shutdown_hook
+ *
+ * Set each DefElem's defname to the name of the plugin or extension, and
+ * the argument to a tree of nodes that all have copy and read/write
+ * support.
+ */
+ List *extension_state;
+
/* statement location in source string (copied from Query) */
/* start location, or -1 if unknown */
ParseLoc stmt_location;
--
2.39.5 (Apple Git-154)
v7-0006-not-for-commit-count-distinct-joinrels-and-joinre.patchapplication/octet-stream; name=v7-0006-not-for-commit-count-distinct-joinrels-and-joinre.patchDownload
From a35f6e8367dfd96ea79b60b82c742e203f534422 Mon Sep 17 00:00:00 2001
From: Robert Haas <rhaas@postgresql.org>
Date: Mon, 25 Aug 2025 15:22:57 -0400
Subject: [PATCH v7 6/6] not for commit: count distinct joinrels and joinrel
planning attempts
---
.../expected/pg_overexplain.out | 22 ++-
contrib/pg_overexplain/pg_overexplain.c | 125 ++++++++++++++++++
2 files changed, 142 insertions(+), 5 deletions(-)
diff --git a/contrib/pg_overexplain/expected/pg_overexplain.out b/contrib/pg_overexplain/expected/pg_overexplain.out
index 55d34666d87..48251298f05 100644
--- a/contrib/pg_overexplain/expected/pg_overexplain.out
+++ b/contrib/pg_overexplain/expected/pg_overexplain.out
@@ -38,7 +38,9 @@ EXPLAIN (DEBUG) SELECT 1;
Relation OIDs: none
Executor Parameter Types: none
Parse Location: 0 to end
-(11 rows)
+ Total Joinrel Attempts: 0
+ Distinct Joinrels: 0
+(13 rows)
EXPLAIN (RANGE_TABLE) SELECT 1;
QUERY PLAN
@@ -121,6 +123,8 @@ $$);
Relation OIDs: NNN...
Executor Parameter Types: none
Parse Location: 0 to end
+ Total Joinrel Attempts: 0
+ Distinct Joinrels: 0
RTI 1 (relation, inherited, in-from-clause):
Eref: vegetables (id, name, genus)
Relation: vegetables
@@ -142,7 +146,7 @@ $$);
Relation Kind: relation
Relation Lock Mode: AccessShareLock
Unprunable RTIs: 1 3 4
-(53 rows)
+(55 rows)
-- Test a different output format.
SELECT explain_filter($$
@@ -242,6 +246,8 @@ $$);
<Relation-OIDs>NNN...</Relation-OIDs> +
<Executor-Parameter-Types>none</Executor-Parameter-Types> +
<Parse-Location>0 to end</Parse-Location> +
+ <Total-Joinrel-Attempts>0</Total-Joinrel-Attempts> +
+ <Distinct-Joinrels>0</Distinct-Joinrels> +
</PlannedStmt> +
<Range-Table> +
<Range-Table-Entry> +
@@ -346,7 +352,9 @@ $$);
Relation OIDs: NNN...
Executor Parameter Types: none
Parse Location: 0 to end
-(37 rows)
+ Total Joinrel Attempts: 0
+ Distinct Joinrels: 0
+(39 rows)
SET debug_parallel_query = false;
RESET enable_seqscan;
@@ -374,7 +382,9 @@ $$);
Relation OIDs: NNN...
Executor Parameter Types: 0
Parse Location: 0 to end
-(15 rows)
+ Total Joinrel Attempts: 0
+ Distinct Joinrels: 0
+(17 rows)
-- Create an index, and then attempt to force a nested loop with inner index
-- scan so that we can see parameter-related information. Also, let's try
@@ -438,7 +448,9 @@ $$);
Relation OIDs: NNN...
Executor Parameter Types: 23
Parse Location: 0 to end
-(47 rows)
+ Total Joinrel Attempts: 2
+ Distinct Joinrels: 1
+(49 rows)
RESET enable_hashjoin;
RESET enable_material;
diff --git a/contrib/pg_overexplain/pg_overexplain.c b/contrib/pg_overexplain/pg_overexplain.c
index bd70b6d9d5e..93d2051f4fb 100644
--- a/contrib/pg_overexplain/pg_overexplain.c
+++ b/contrib/pg_overexplain/pg_overexplain.c
@@ -16,6 +16,10 @@
#include "commands/explain_format.h"
#include "commands/explain_state.h"
#include "fmgr.h"
+#include "nodes/makefuncs.h"
+#include "optimizer/extendplan.h"
+#include "optimizer/paths.h"
+#include "optimizer/planner.h"
#include "parser/parsetree.h"
#include "storage/lock.h"
#include "utils/builtins.h"
@@ -32,6 +36,12 @@ typedef struct
bool range_table;
} overexplain_options;
+typedef struct
+{
+ int total_joinrel_attempts;
+ int distinct_joinrel_count;
+} overexplain_plannerglobal;
+
static overexplain_options *overexplain_ensure_options(ExplainState *es);
static void overexplain_debug_handler(ExplainState *es, DefElem *opt,
ParseState *pstate);
@@ -57,9 +67,28 @@ static void overexplain_bitmapset(const char *qlabel, Bitmapset *bms,
static void overexplain_intlist(const char *qlabel, List *list,
ExplainState *es);
+static void overexplain_planner_setup_hook(PlannerGlobal *glob, Query *parse,
+ const char *query_string,
+ double *tuple_fraction,
+ struct ExplainState *es);
+static void overexplain_planner_shutdown_hook(PlannerGlobal *glob,
+ Query *parse,
+ const char *query_string,
+ PlannedStmt *pstmt);
+static void overexplain_set_join_pathlist_hook(PlannerInfo *root,
+ RelOptInfo *joinrel,
+ RelOptInfo *outerrel,
+ RelOptInfo *innerrel,
+ JoinType jointype,
+ JoinPathExtraData *extra);
+
static int es_extension_id;
+static int planner_extension_id = -1;
static explain_per_node_hook_type prev_explain_per_node_hook;
static explain_per_plan_hook_type prev_explain_per_plan_hook;
+static planner_setup_hook_type prev_planner_setup_hook;
+static planner_shutdown_hook_type prev_planner_shutdown_hook;
+static set_join_pathlist_hook_type prev_set_join_pathlist_hook;
/*
* Initialization we do when this module is loaded.
@@ -70,6 +99,9 @@ _PG_init(void)
/* Get an ID that we can use to cache data in an ExplainState. */
es_extension_id = GetExplainExtensionId("pg_overexplain");
+ /* Get an ID that we can use to cache data in the planner. */
+ planner_extension_id = GetPlannerExtensionId("pg_overexplain");
+
/* Register the new EXPLAIN options implemented by this module. */
RegisterExtensionExplainOption("debug", overexplain_debug_handler);
RegisterExtensionExplainOption("range_table",
@@ -80,6 +112,16 @@ _PG_init(void)
explain_per_node_hook = overexplain_per_node_hook;
prev_explain_per_plan_hook = explain_per_plan_hook;
explain_per_plan_hook = overexplain_per_plan_hook;
+
+ /* Example of planner_setup_hook/planner_shutdown_hook use */
+ prev_planner_setup_hook = planner_setup_hook;
+ planner_setup_hook = overexplain_planner_setup_hook;
+ prev_planner_shutdown_hook = planner_shutdown_hook;
+ planner_shutdown_hook = overexplain_planner_shutdown_hook;
+
+ /* Support for above example */
+ prev_set_join_pathlist_hook = set_join_pathlist_hook;
+ set_join_pathlist_hook = overexplain_set_join_pathlist_hook;
}
/*
@@ -381,6 +423,29 @@ overexplain_debug(PlannedStmt *plannedstmt, ExplainState *es)
plannedstmt->stmt_len),
es);
+ {
+ DefElem *elem = NULL;
+
+ foreach_node(DefElem, de, plannedstmt->extension_state)
+ {
+ if (strcmp(de->defname, "pg_overexplain") == 0)
+ {
+ elem = de;
+ break;
+ }
+ }
+
+ if (elem != NULL)
+ {
+ List *l = castNode(List, elem->arg);
+
+ ExplainPropertyInteger("Total Joinrel Attempts", NULL,
+ intVal(linitial(l)), es);
+ ExplainPropertyInteger("Distinct Joinrels", NULL,
+ intVal(lsecond(l)), es);
+ }
+ }
+
/* Done with this group. */
if (es->format == EXPLAIN_FORMAT_TEXT)
es->indent--;
@@ -784,3 +849,63 @@ overexplain_intlist(const char *qlabel, List *list, ExplainState *es)
pfree(buf.data);
}
+
+static void
+overexplain_planner_setup_hook(PlannerGlobal *glob, Query *parse,
+ const char *query_string,
+ double *tuple_fraction,
+ struct ExplainState *es)
+{
+ overexplain_options *options;
+ overexplain_plannerglobal *g;
+
+ if (es != NULL)
+ {
+ options = GetExplainExtensionState(es, es_extension_id);
+ if (options != NULL && options->debug)
+ {
+ g = palloc0_object(overexplain_plannerglobal);
+ SetPlannerGlobalExtensionState(glob, planner_extension_id, g);
+ }
+ }
+}
+
+static void
+overexplain_planner_shutdown_hook(PlannerGlobal *glob, Query *parse,
+ const char *query_string, PlannedStmt *pstmt)
+{
+ overexplain_plannerglobal *g;
+ DefElem *elem;
+ List *l;
+
+ g = GetPlannerGlobalExtensionState(glob, planner_extension_id);
+ if (g != NULL)
+ {
+ l = list_make2(makeInteger(g->total_joinrel_attempts),
+ makeInteger(g->distinct_joinrel_count));
+ elem = makeDefElem("pg_overexplain", (Node *) l, -1);
+ pstmt->extension_state = lappend(pstmt->extension_state, elem);
+ }
+}
+
+static void
+overexplain_set_join_pathlist_hook(PlannerInfo *root, RelOptInfo *joinrel,
+ RelOptInfo *outerrel, RelOptInfo *innerrel,
+ JoinType jointype, JoinPathExtraData *extra)
+{
+ overexplain_plannerglobal *g;
+
+ g = GetPlannerGlobalExtensionState(root->glob, planner_extension_id);
+
+ if (g != NULL)
+ {
+ g->total_joinrel_attempts++;
+
+ if (GetRelOptInfoExtensionState(joinrel, planner_extension_id) == NULL)
+ {
+ g->distinct_joinrel_count++;
+ /* set any non-NULL value to avoid double-counting */
+ SetRelOptInfoExtensionState(joinrel, planner_extension_id, g);
+ }
+ }
+}
--
2.39.5 (Apple Git-154)
Robert Haas <robertmhaas@gmail.com> writes:
Done now. Here's a rebase of the rest, plus I tweaked the GEQO patch
to try to avoid a compiler warning that cfbot was complaining about.
I'm good with the v7 patch set, except for the complaint I raised
previously that we really ought to have more than zero documentation
for planner()'s parameters. If you don't care to write such text,
attached is a cut at it.
regards, tom lane
Attachments:
v1-document-planner-arguments.patchtext/x-diff; charset=us-ascii; name=v1-document-planner-arguments.patchDownload
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index 41bd8353430..c249f34eb8e 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -280,6 +280,23 @@ static void create_partial_unique_paths(PlannerInfo *root, RelOptInfo *input_rel
*
* Query optimizer entry point
*
+ * Inputs:
+ * parse: an analyzed-and-rewritten query tree for an optimizable statement
+ * query_string: source text for the query tree (used for error reports)
+ * cursorOptions: bitmask of CURSOR_OPT_XXX flags, see parsenodes.h
+ * boundParams: passed-in parameter values, or NULL if none
+ * es: ExplainState if being called from EXPLAIN, else NULL
+ *
+ * The result is a PlannedStmt tree.
+ *
+ * PARAM_EXTERN Param nodes within the parse tree can be replaced by Consts
+ * using values from boundParams, if those values are marked PARAM_FLAG_CONST.
+ * Parameter values not so marked are still relied on for estimation purposes.
+ *
+ * The ExplainState pointer is not currently used by the core planner, but it
+ * is passed through to some planner hooks so that they can report information
+ * back to EXPLAIN extension hooks.
+ *
* To support loadable plugins that monitor or modify planner behavior,
* we provide a hook variable that lets a plugin get control before and
* after the standard planning process. The plugin would normally call
On Sun, Sep 28, 2025 at 11:41 AM Tom Lane <tgl@sss.pgh.pa.us> wrote:
Robert Haas <robertmhaas@gmail.com> writes:
Done now. Here's a rebase of the rest, plus I tweaked the GEQO patch
to try to avoid a compiler warning that cfbot was complaining about.I'm good with the v7 patch set, except for the complaint I raised
previously that we really ought to have more than zero documentation
for planner()'s parameters. If you don't care to write such text,
attached is a cut at it.
Oh, nice, thanks! I'm going to be on vacation the rest of this week so
I plan to deal with this next week. However, if you feel like
committing it before then, please feel free.
--
Robert Haas
EDB: http://www.enterprisedb.com
On Mon, Sep 29, 2025 at 9:28 AM Robert Haas <robertmhaas@gmail.com> wrote:
Oh, nice, thanks! I'm going to be on vacation the rest of this week so
I plan to deal with this next week. However, if you feel like
committing it before then, please feel free.
I committed 0001 and 0002. That left three remaining patches. I merged
your (Tom's) comment changes into the first of those, and here's the
result of that.
--
Robert Haas
EDB: http://www.enterprisedb.com
Attachments:
v8-0001-Add-ExplainState-argument-to-pg_plan_query-and-pl.patchapplication/octet-stream; name=v8-0001-Add-ExplainState-argument-to-pg_plan_query-and-pl.patchDownload
From a417152e17f5a5fdba8acb9404a3a2079c83efd8 Mon Sep 17 00:00:00 2001
From: Robert Haas <rhaas@postgresql.org>
Date: Wed, 24 Sep 2025 11:18:12 -0400
Subject: [PATCH v8 1/3] Add ExplainState argument to pg_plan_query() and
planner().
This allows extensions to have access to any data they've stored
in the ExplainState during planning. Unfortunately, it won't help
with EXPLAIN EXECUTE is used, but since that case is less common,
this still seems like an improvement.
Since planner() has quite a few arguments now, also add some
documentation of those arguments and the return value.
Author: Robert Haas <rhaas@postgresql.org>
Co-authored-by: Tom Lane <tgl@sss.pgh.pa.us>
Reviewed-by: Andrei Lepikhov <lepihov@gmail.com>
Reviewed-by: Tom Lane <tgl@sss.pgh.pa.us>
Discussion: http://postgr.es/m/CA+TgmoYWKHU2hKr62Toyzh-kTDEnMDeLw7gkOOnjL-TnOUq0kQ@mail.gmail.com
---
.../pg_stat_statements/pg_stat_statements.c | 14 +++++-----
src/backend/commands/copyto.c | 2 +-
src/backend/commands/createas.c | 2 +-
src/backend/commands/explain.c | 2 +-
src/backend/commands/matview.c | 2 +-
src/backend/commands/portalcmds.c | 3 ++-
src/backend/optimizer/plan/planner.c | 27 ++++++++++++++++---
src/backend/tcop/postgres.c | 7 ++---
src/include/optimizer/optimizer.h | 5 +++-
src/include/optimizer/planner.h | 8 ++++--
src/include/tcop/tcopprot.h | 4 ++-
.../modules/delay_execution/delay_execution.c | 7 ++---
12 files changed, 58 insertions(+), 25 deletions(-)
diff --git a/contrib/pg_stat_statements/pg_stat_statements.c b/contrib/pg_stat_statements/pg_stat_statements.c
index db1af36a705..f2187167c5c 100644
--- a/contrib/pg_stat_statements/pg_stat_statements.c
+++ b/contrib/pg_stat_statements/pg_stat_statements.c
@@ -338,7 +338,8 @@ static void pgss_post_parse_analyze(ParseState *pstate, Query *query,
static PlannedStmt *pgss_planner(Query *parse,
const char *query_string,
int cursorOptions,
- ParamListInfo boundParams);
+ ParamListInfo boundParams,
+ ExplainState *es);
static void pgss_ExecutorStart(QueryDesc *queryDesc, int eflags);
static void pgss_ExecutorRun(QueryDesc *queryDesc,
ScanDirection direction,
@@ -894,7 +895,8 @@ static PlannedStmt *
pgss_planner(Query *parse,
const char *query_string,
int cursorOptions,
- ParamListInfo boundParams)
+ ParamListInfo boundParams,
+ ExplainState *es)
{
PlannedStmt *result;
@@ -929,10 +931,10 @@ pgss_planner(Query *parse,
{
if (prev_planner_hook)
result = prev_planner_hook(parse, query_string, cursorOptions,
- boundParams);
+ boundParams, es);
else
result = standard_planner(parse, query_string, cursorOptions,
- boundParams);
+ boundParams, es);
}
PG_FINALLY();
{
@@ -978,10 +980,10 @@ pgss_planner(Query *parse,
{
if (prev_planner_hook)
result = prev_planner_hook(parse, query_string, cursorOptions,
- boundParams);
+ boundParams, es);
else
result = standard_planner(parse, query_string, cursorOptions,
- boundParams);
+ boundParams, es);
}
PG_FINALLY();
{
diff --git a/src/backend/commands/copyto.c b/src/backend/commands/copyto.c
index 67b94b91cae..e5781155cdf 100644
--- a/src/backend/commands/copyto.c
+++ b/src/backend/commands/copyto.c
@@ -796,7 +796,7 @@ BeginCopyTo(ParseState *pstate,
/* plan the query */
plan = pg_plan_query(query, pstate->p_sourcetext,
- CURSOR_OPT_PARALLEL_OK, NULL);
+ CURSOR_OPT_PARALLEL_OK, NULL, NULL);
/*
* With row-level security and a user using "COPY relation TO", we
diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c
index dfd2ab8e862..1ccc2e55c64 100644
--- a/src/backend/commands/createas.c
+++ b/src/backend/commands/createas.c
@@ -321,7 +321,7 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt,
/* plan the query */
plan = pg_plan_query(query, pstate->p_sourcetext,
- CURSOR_OPT_PARALLEL_OK, params);
+ CURSOR_OPT_PARALLEL_OK, params, NULL);
/*
* Use a snapshot with an updated command ID to ensure this query sees
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 06191cd8a85..e6edae0845c 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -351,7 +351,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, params, es);
INSTR_TIME_SET_CURRENT(planduration);
INSTR_TIME_SUBTRACT(planduration, planstart);
diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index 188e26f0e6e..441de55ac24 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -426,7 +426,7 @@ refresh_matview_datafill(DestReceiver *dest, Query *query,
CHECK_FOR_INTERRUPTS();
/* Plan the query which will generate data for the refresh. */
- plan = pg_plan_query(query, queryString, CURSOR_OPT_PARALLEL_OK, NULL);
+ plan = pg_plan_query(query, queryString, CURSOR_OPT_PARALLEL_OK, NULL, NULL);
/*
* Use a snapshot with an updated command ID to ensure this query sees
diff --git a/src/backend/commands/portalcmds.c b/src/backend/commands/portalcmds.c
index e7c8171c102..ec96c2efcd3 100644
--- a/src/backend/commands/portalcmds.c
+++ b/src/backend/commands/portalcmds.c
@@ -99,7 +99,8 @@ PerformCursorOpen(ParseState *pstate, DeclareCursorStmt *cstmt, ParamListInfo pa
elog(ERROR, "non-SELECT statement in DECLARE CURSOR");
/* Plan the query, applying the specified options */
- plan = pg_plan_query(query, pstate->p_sourcetext, cstmt->options, params);
+ plan = pg_plan_query(query, pstate->p_sourcetext, cstmt->options, params,
+ NULL);
/*
* Create a portal and copy the plan and query string into its memory.
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index 0c9397a36c3..8d1d45eab8d 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -280,6 +280,23 @@ static void create_partial_unique_paths(PlannerInfo *root, RelOptInfo *input_rel
*
* Query optimizer entry point
*
+ * Inputs:
+ * parse: an analyzed-and-rewritten query tree for an optimizable statement
+ * query_string: source text for the query tree (used for error reports)
+ * cursorOptions: bitmask of CURSOR_OPT_XXX flags, see parsenodes.h
+ * boundParams: passed-in parameter values, or NULL if none
+ * es: ExplainState if being called from EXPLAIN, else NULL
+ *
+ * The result is a PlannedStmt tree.
+ *
+ * PARAM_EXTERN Param nodes within the parse tree can be replaced by Consts
+ * using values from boundParams, if those values are marked PARAM_FLAG_CONST.
+ * Parameter values not so marked are still relied on for estimation purposes.
+ *
+ * The ExplainState pointer is not currently used by the core planner, but it
+ * is passed through to some planner hooks so that they can report information
+ * back to EXPLAIN extension hooks.
+ *
* To support loadable plugins that monitor or modify planner behavior,
* we provide a hook variable that lets a plugin get control before and
* after the standard planning process. The plugin would normally call
@@ -291,14 +308,16 @@ static void create_partial_unique_paths(PlannerInfo *root, RelOptInfo *input_rel
*****************************************************************************/
PlannedStmt *
planner(Query *parse, const char *query_string, int cursorOptions,
- ParamListInfo boundParams)
+ ParamListInfo boundParams, ExplainState *es)
{
PlannedStmt *result;
if (planner_hook)
- result = (*planner_hook) (parse, query_string, cursorOptions, boundParams);
+ result = (*planner_hook) (parse, query_string, cursorOptions,
+ boundParams, es);
else
- result = standard_planner(parse, query_string, cursorOptions, boundParams);
+ result = standard_planner(parse, query_string, cursorOptions,
+ boundParams, es);
pgstat_report_plan_id(result->planId, false);
@@ -307,7 +326,7 @@ planner(Query *parse, const char *query_string, int cursorOptions,
PlannedStmt *
standard_planner(Query *parse, const char *query_string, int cursorOptions,
- ParamListInfo boundParams)
+ ParamListInfo boundParams, ExplainState *es)
{
PlannedStmt *result;
PlannerGlobal *glob;
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index d356830f756..7dd75a490aa 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -37,6 +37,7 @@
#include "catalog/pg_type.h"
#include "commands/async.h"
#include "commands/event_trigger.h"
+#include "commands/explain_state.h"
#include "commands/prepare.h"
#include "common/pg_prng.h"
#include "jit/jit.h"
@@ -884,7 +885,7 @@ pg_rewrite_query(Query *query)
*/
PlannedStmt *
pg_plan_query(Query *querytree, const char *query_string, int cursorOptions,
- ParamListInfo boundParams)
+ ParamListInfo boundParams, ExplainState *es)
{
PlannedStmt *plan;
@@ -901,7 +902,7 @@ pg_plan_query(Query *querytree, const char *query_string, int cursorOptions,
ResetUsage();
/* call the optimizer */
- plan = planner(querytree, query_string, cursorOptions, boundParams);
+ plan = planner(querytree, query_string, cursorOptions, boundParams, es);
if (log_planner_stats)
ShowUsage("PLANNER STATISTICS");
@@ -997,7 +998,7 @@ pg_plan_queries(List *querytrees, const char *query_string, int cursorOptions,
else
{
stmt = pg_plan_query(query, query_string, cursorOptions,
- boundParams);
+ boundParams, NULL);
}
stmt_list = lappend(stmt_list, stmt);
diff --git a/src/include/optimizer/optimizer.h b/src/include/optimizer/optimizer.h
index 04878f1f1c2..a34113903c0 100644
--- a/src/include/optimizer/optimizer.h
+++ b/src/include/optimizer/optimizer.h
@@ -24,6 +24,8 @@
#include "nodes/parsenodes.h"
+typedef struct ExplainState ExplainState; /* defined in explain_state.h */
+
/*
* We don't want to include nodes/pathnodes.h here, because non-planner
* code should generally treat PlannerInfo as an opaque typedef.
@@ -104,7 +106,8 @@ extern PGDLLIMPORT bool enable_distinct_reordering;
extern PlannedStmt *planner(Query *parse, const char *query_string,
int cursorOptions,
- ParamListInfo boundParams);
+ ParamListInfo boundParams,
+ ExplainState *es);
extern Expr *expression_planner(Expr *expr);
extern Expr *expression_planner_with_deps(Expr *expr,
diff --git a/src/include/optimizer/planner.h b/src/include/optimizer/planner.h
index 1bbef0018d5..c7ab466f5f1 100644
--- a/src/include/optimizer/planner.h
+++ b/src/include/optimizer/planner.h
@@ -22,11 +22,14 @@
#include "nodes/plannodes.h"
+typedef struct ExplainState ExplainState; /* defined in explain_state.h */
+
/* Hook for plugins to get control in planner() */
typedef PlannedStmt *(*planner_hook_type) (Query *parse,
const char *query_string,
int cursorOptions,
- ParamListInfo boundParams);
+ ParamListInfo boundParams,
+ ExplainState *es);
extern PGDLLIMPORT planner_hook_type planner_hook;
/* Hook for plugins to get control when grouping_planner() plans upper rels */
@@ -40,7 +43,8 @@ extern PGDLLIMPORT create_upper_paths_hook_type create_upper_paths_hook;
extern PlannedStmt *standard_planner(Query *parse, const char *query_string,
int cursorOptions,
- ParamListInfo boundParams);
+ ParamListInfo boundParams,
+ ExplainState *es);
extern PlannerInfo *subquery_planner(PlannerGlobal *glob, Query *parse,
char *plan_name,
diff --git a/src/include/tcop/tcopprot.h b/src/include/tcop/tcopprot.h
index a83cc4f4850..c1bcfdec673 100644
--- a/src/include/tcop/tcopprot.h
+++ b/src/include/tcop/tcopprot.h
@@ -20,6 +20,7 @@
#include "utils/guc.h"
#include "utils/queryenvironment.h"
+typedef struct ExplainState ExplainState; /* defined in explain_state.h */
extern PGDLLIMPORT CommandDest whereToSendOutput;
extern PGDLLIMPORT const char *debug_query_string;
@@ -63,7 +64,8 @@ extern List *pg_analyze_and_rewrite_withcb(RawStmt *parsetree,
QueryEnvironment *queryEnv);
extern PlannedStmt *pg_plan_query(Query *querytree, const char *query_string,
int cursorOptions,
- ParamListInfo boundParams);
+ ParamListInfo boundParams,
+ ExplainState *es);
extern List *pg_plan_queries(List *querytrees, const char *query_string,
int cursorOptions,
ParamListInfo boundParams);
diff --git a/src/test/modules/delay_execution/delay_execution.c b/src/test/modules/delay_execution/delay_execution.c
index 7bc97f84a1c..d933e9a6e53 100644
--- a/src/test/modules/delay_execution/delay_execution.c
+++ b/src/test/modules/delay_execution/delay_execution.c
@@ -40,17 +40,18 @@ static planner_hook_type prev_planner_hook = NULL;
/* planner_hook function to provide the desired delay */
static PlannedStmt *
delay_execution_planner(Query *parse, const char *query_string,
- int cursorOptions, ParamListInfo boundParams)
+ int cursorOptions, ParamListInfo boundParams,
+ ExplainState *es)
{
PlannedStmt *result;
/* Invoke the planner, possibly via a previous hook user */
if (prev_planner_hook)
result = prev_planner_hook(parse, query_string, cursorOptions,
- boundParams);
+ boundParams, es);
else
result = standard_planner(parse, query_string, cursorOptions,
- boundParams);
+ boundParams, es);
/* If enabled, delay by taking and releasing the specified lock */
if (post_planning_lock_id != 0)
--
2.50.1 (Apple Git-155)
v8-0003-Add-extension_state-member-to-PlannedStmt.patchapplication/octet-stream; name=v8-0003-Add-extension_state-member-to-PlannedStmt.patchDownload
From fb682e0c7587a425693e3435fb0833fc9339f525 Mon Sep 17 00:00:00 2001
From: Robert Haas <rhaas@postgresql.org>
Date: Mon, 25 Aug 2025 14:29:02 -0400
Subject: [PATCH v8 3/3] Add extension_state member to PlannedStmt.
Extensions can stash data computed at plan time into this list using
planner_shutdown_hook (or perhaps other mechanisms) and then access
it from any code that has access to the PlannedStmt (such as explain
hooks), allowing for extensible debugging and instrumentation of
plans.
Reviewed-by: Andrei Lepikhov <lepihov@gmail.com>
Reviewed-by: Tom Lane <tgl@sss.pgh.pa.us>
Discussion: http://postgr.es/m/CA+TgmoYWKHU2hKr62Toyzh-kTDEnMDeLw7gkOOnjL-TnOUq0kQ@mail.gmail.com
---
src/include/nodes/plannodes.h | 9 +++++++++
1 file changed, 9 insertions(+)
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index 3d196f5078e..77ec2bc10b2 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -149,6 +149,15 @@ typedef struct PlannedStmt
/* non-null if this is utility stmt */
Node *utilityStmt;
+ /*
+ * DefElem objects added by extensions, e.g. using planner_shutdown_hook
+ *
+ * Set each DefElem's defname to the name of the plugin or extension, and
+ * the argument to a tree of nodes that all have copy and read/write
+ * support.
+ */
+ List *extension_state;
+
/* statement location in source string (copied from Query) */
/* start location, or -1 if unknown */
ParseLoc stmt_location;
--
2.50.1 (Apple Git-155)
v8-0002-Add-planner_setup_hook-and-planner_shutdown_hook.patchapplication/octet-stream; name=v8-0002-Add-planner_setup_hook-and-planner_shutdown_hook.patchDownload
From 65ff8de84599c626d32e1485593d1b59c06f9bb3 Mon Sep 17 00:00:00 2001
From: Robert Haas <rhaas@postgresql.org>
Date: Wed, 24 Sep 2025 11:21:55 -0400
Subject: [PATCH v8 2/3] Add planner_setup_hook and planner_shutdown_hook.
These hooks allow plugins to get control at the earliest point at
which the PlannerGlobal object is fully initialized, and then just
before it gets destroyed. This is useful in combination with the
extendable plan state facilities (see extendplan.h) and perhaps for
other purposes as well.
Reviewed-by: Andrei Lepikhov <lepihov@gmail.com>
Reviewed-by: Tom Lane <tgl@sss.pgh.pa.us>
Discussion: http://postgr.es/m/CA+TgmoYWKHU2hKr62Toyzh-kTDEnMDeLw7gkOOnjL-TnOUq0kQ@mail.gmail.com
---
src/backend/optimizer/plan/planner.c | 14 ++++++++++++++
src/include/optimizer/planner.h | 13 +++++++++++++
2 files changed, 27 insertions(+)
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index 8d1d45eab8d..b3c169d2b9e 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -73,6 +73,12 @@ bool enable_distinct_reordering = true;
/* Hook for plugins to get control in planner() */
planner_hook_type planner_hook = NULL;
+/* Hook for plugins to get control after PlannerGlobal is initialized */
+planner_setup_hook_type planner_setup_hook = NULL;
+
+/* Hook for plugins to get control before PlannerGlobal is discarded */
+planner_shutdown_hook_type planner_shutdown_hook = NULL;
+
/* Hook for plugins to get control when grouping_planner() plans upper rels */
create_upper_paths_hook_type create_upper_paths_hook = NULL;
@@ -457,6 +463,10 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions,
tuple_fraction = 0.0;
}
+ /* Allow plugins to take control after we've initialized "glob" */
+ if (planner_setup_hook)
+ (*planner_setup_hook) (glob, parse, query_string, &tuple_fraction, es);
+
/* primary planning entry point (may recurse for subqueries) */
root = subquery_planner(glob, parse, NULL, NULL, false, tuple_fraction,
NULL);
@@ -636,6 +646,10 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions,
result->jitFlags |= PGJIT_DEFORM;
}
+ /* Allow plugins to take control before we discard "glob" */
+ if (planner_shutdown_hook)
+ (*planner_shutdown_hook) (glob, parse, query_string, result);
+
if (glob->partition_directory != NULL)
DestroyPartitionDirectory(glob->partition_directory);
diff --git a/src/include/optimizer/planner.h b/src/include/optimizer/planner.h
index c7ab466f5f1..55d9b7940aa 100644
--- a/src/include/optimizer/planner.h
+++ b/src/include/optimizer/planner.h
@@ -32,6 +32,19 @@ typedef PlannedStmt *(*planner_hook_type) (Query *parse,
ExplainState *es);
extern PGDLLIMPORT planner_hook_type planner_hook;
+/* Hook for plugins to get control after PlannerGlobal is initialized */
+typedef void (*planner_setup_hook_type) (PlannerGlobal *glob, Query *parse,
+ const char *query_string,
+ double *tuple_fraction,
+ ExplainState *es);
+extern PGDLLIMPORT planner_setup_hook_type planner_setup_hook;
+
+/* Hook for plugins to get control before PlannerGlobal is discarded */
+typedef void (*planner_shutdown_hook_type) (PlannerGlobal *glob, Query *parse,
+ const char *query_string,
+ PlannedStmt *pstmt);
+extern PGDLLIMPORT planner_shutdown_hook_type planner_shutdown_hook;
+
/* Hook for plugins to get control when grouping_planner() plans upper rels */
typedef void (*create_upper_paths_hook_type) (PlannerInfo *root,
UpperRelationKind stage,
--
2.50.1 (Apple Git-155)
On Tue, Oct 7, 2025 at 1:19 PM Robert Haas <robertmhaas@gmail.com> wrote:
On Mon, Sep 29, 2025 at 9:28 AM Robert Haas <robertmhaas@gmail.com> wrote:
Oh, nice, thanks! I'm going to be on vacation the rest of this week so
I plan to deal with this next week. However, if you feel like
committing it before then, please feel free.I committed 0001 and 0002. That left three remaining patches. I merged
your (Tom's) comment changes into the first of those, and here's the
result of that.
And committed. Thanks to all, and especially to Tom, for the reviews.
--
Robert Haas
EDB: http://www.enterprisedb.com
Robert Haas <robertmhaas@gmail.com> writes:
And committed. Thanks to all, and especially to Tom, for the reviews.
Should the CF entry be closed out now?
https://commitfest.postgresql.org/patch/5994/
regards, tom lane
On Fri, Oct 10, 2025 at 11:58 AM Tom Lane <tgl@sss.pgh.pa.us> wrote:
Should the CF entry be closed out now?
Yes, done now.
--
Robert Haas
EDB: http://www.enterprisedb.com