Обсуждение: Re: plan shape work

Поиск
Список
Период
Сортировка

Re: plan shape work

От
Robert Haas
Дата:
On Mon, May 19, 2025 at 2:01 PM Robert Haas <robertmhaas@gmail.com> wrote:
> A couple of people at pgconf.dev seemed to want to know more about my
> ongoing plan shape work, so here are the patches I have currently.

Here's an updated patch set. My goal for the September CommitFest is
to get patches 0001-0004 committed. Of course, if there are too many
objections or too little review, that might not happen, but that's my
goal.

This patch set is basically unchanged from the previous patch set,
except that I've added one new patch. 0007 records information about
Append node consolidation into the final plan tree. Without this, when
we build an AppendPath or MergeAppendPath and pull up the subpaths
from a similar underlying node, we can lose the RTIs from the
subordinate node, making it very difficult to analyze the plan after
the fact.

Just to remark a bit further on the structure of the patch set,
0001-0003 are closely related. The only one I really need committed in
order to move forward is 0001, but I think the others are a good idea.
There is probably room for some bikeshedding on the output produced by
0002. Then after that, 0004 stands alone as an incredibly important
and foundational patch: without it, there's no way to know what the
name of a subplan will be until after it's already been planned. I am
fairly confident in the approach that I've taken here, but it does
cause user-visible changes in EXPLAIN output about which people might
conceivably have strong opinions. Getting agreement either on what
I've done here or some variant of the approach is essential for me to
be able to move forward. Then, 0005-0007 all have to do with
preserving in the final plan various details that today would be
discarded at the end of planning. While I'm happy to have comments on
these now, I'm still not completely confident that I've found all
issues in this area or handled them perfectly; hence, I'm not in a
hurry to move forward with those just yet.

--
Robert Haas
EDB: http://www.enterprisedb.com

Вложения

Re: plan shape work

От
Bruce Momjian
Дата:
On Tue, Aug 26, 2025 at 10:58:33AM -0400, Robert Haas wrote:
> During planning, there is one range table per subquery; at the end if
> planning, those separate range tables are flattened into a single
> range table. Prior to this change, it was impractical for code
> examining the final plan to understand which parts of the flattened
> range table came from which subquery's range table.
> 
> If the only consumer of the final plan is the executor, that is
> completely fine. However, if some code wants to examine the final
> plan, or what happens when we execute it, and extract information from
> it that be used in future planning cycles, it's inconvenient.

I am very interested in how plans can be used for future planning.

-- 
  Bruce Momjian  <bruce@momjian.us>        https://momjian.us
  EDB                                      https://enterprisedb.com

  Do not let urgent matters crowd out time for investment in the future.



Re: plan shape work

От
Alexandra Wang
Дата:
Hi Robert,

On Tue, Aug 26, 2025 at 7:59 AM Robert Haas <robertmhaas@gmail.com> wrote:
On Mon, May 19, 2025 at 2:01 PM Robert Haas <robertmhaas@gmail.com> wrote:
> A couple of people at pgconf.dev seemed to want to know more about my
> ongoing plan shape work, so here are the patches I have currently.

Here's an updated patch set. My goal for the September CommitFest is
to get patches 0001-0004 committed. Of course, if there are too many
objections or too little review, that might not happen, but that's my
goal.

Thanks for the patches!

I don’t know enough about the history in this area to object to your
approach or suggest an alternative design. That said, I’ve reviewed
patches 0001-0004, and as individual patches they make sense to me.

Below are some more detailed comments, which would only be relevant if
you decide to proceed in this direction.

0002:

                         QUERY PLAN                        
 ----------------------------------------------------------
  Nested Loop Left Join
-   Output: t1.i, (1), t2.i2, i3, t4.i4
+   Output: t1.i, (1), t2.i2, t3.i3, t4.i4
    ->  Nested Loop Left Join
-         Output: t1.i, t2.i2, (1), i3
+         Output: t1.i, t2.i2, (1), t3.i3
          Join Filter: false
          ->  Hash Left Join
                Output: t1.i, t2.i2, (1)

These plan changes introduced by 0002, which adds schema qualifiers,
make me very happy. I think it’s a nice improvement on its own.

In reply to 0002's commit message:
> XXX. I have broken this out as a separate commit for now; however,
> it could be merged with the commit to add 'relids' to 'Result'; or
> the patch series could even be rejiggered to present this as the
> primary benefit of that change, leaving the EXPLAIN changes as a
> secondary benefit, instead of the current organization, which does
> the reverse.

I’m fine with the current organization. I agree that if we just
compare the EXPLAIN changes in 0001, which add additional “Replaces:”
information for the simple Result node, with the EXPLAIN changes in
0002, the changes in 0002 are arguably more attractive. However, I
think the EXPLAIN changes in 0001 are a more direct reflection of what
the rest of 0001 is trying to achieve: keeping track of the RTIs a
Result node is scanning. The changes in 0002 feel more like a side
benefit.

With that said, this is just my personal preference. If other
reviewers feel differently, I won’t object.

0003:

In get_scanned_rtindexes():
+       case T_NestLoop:
+           {
+               Bitmapset  *outer_scanrelids;
+               Bitmapset  *inner_scanrelids;
+               Bitmapset  *combined_scanrelids;
+
+               outer_scanrelids =
+                   get_scanned_rtindexes(root, plan->lefttree);
+               inner_scanrelids =
+                   get_scanned_rtindexes(root, plan->righttree);
+               combined_scanrelids =
+                   bms_union(outer_scanrelids, inner_scanrelids);
+               inner_scanrelids = remove_join_rtis(root, inner_scanrelids);
+
+               return remove_join_rtis(root, combined_scanrelids);
+               break;
+           }

It looks like there is an redundant assignment to inner_scanrelids:
+               inner_scanrelids = remove_join_rtis(root, inner_scanrelids);

0004:

There is a compiler warning reported in the CommitFest build:
https://cirrus-ci.com/task/6248981396717568

[23:03:57.811] subselect.c: In function ‘sublinktype_to_string’:
[23:03:57.811] subselect.c:3232:1: error: control reaches end of non-void function [-Werror=return-type]
[23:03:57.811] 3232 | }
[23:03:57.811] | ^
[23:03:57.811] cc1: all warnings being treated as errors
[23:03:57.812] make[4]: *** [<builtin>: subselect.o] Error 1
[23:03:57.812] make[3]: *** [../../../src/backend/common.mk:37: plan-recursive] Error 2
[23:03:57.812] make[2]: *** [common.mk:37: optimizer-recursive] Error 2
[23:03:57.812] make[2]: *** Waiting for unfinished jobs....
[23:04:05.513] make[1]: *** [Makefile:42: all-backend-recurse] Error 2
[23:04:05.514] make: *** [GNUmakefile:21: world-bin-src-recurse] Error 2

You might want to add a return to get rid of the warning.

Still in 0004:
--- a/src/backend/optimizer/plan/planagg.c
+++ b/src/backend/optimizer/plan/planagg.c
@@ -339,6 +340,8 @@ build_minmax_path(PlannerInfo *root, MinMaxAggInfo *mminfo,
    memcpy(subroot, root, sizeof(PlannerInfo));
    subroot->query_level++;
    subroot->parent_root = root;
+   subroot->plan_name = choose_plan_name(root->glob, "minmax", true);
+
    /* reset subplan-related stuff */
    subroot->plan_params = NIL;
    subroot->outer_params = NULL;
@@ -359,6 +362,9 @@ build_minmax_path(PlannerInfo *root, MinMaxAggInfo *mminfo,
    /* and we haven't created PlaceHolderInfos, either */
    Assert(subroot->placeholder_list == NIL);
 
+   /* Add this to list of all PlannerInfo objects. */
+   root->glob->allroots = lappend(root->glob->allroots, root);
+

In the last diff, it should add "subroot" instead of "root" to the
list of all PlannerInfos. Currently, if there are multiple minmax
expressions, we end up with the following plan showing duplicate
names:

test=# explain (costs off) SELECT MIN(value), MAX(value) FROM test_minmax;
                                           QUERY PLAN
-------------------------------------------------------------------------------------------------
 Result
   Replaces: Aggregate
   InitPlan minmax_1
     ->  Limit
           ->  Index Only Scan using test_minmax_value_idx on test_minmax
                 Index Cond: (value IS NOT NULL)
   InitPlan minmax_1
     ->  Limit
           ->  Index Only Scan Backward using test_minmax_value_idx on test_minmax test_minmax_1
                 Index Cond: (value IS NOT NULL)
(10 rows)

Best,
Alex

Re: plan shape work

От
Robert Haas
Дата:
On Thu, Aug 28, 2025 at 1:22 PM Alexandra Wang
<alexandra.wang.oss@gmail.com> wrote:
> Thanks for the patches!

Thanks for the review. Responding just briefly to avoid quoting too much text:

- I'm glad to hear that you like 0002 and consider it an improvement
independent of what follows.
- I'm glad to hear that you're OK with the current split between 0001 and 0002.
- I would like opinions on those topics from more people.
- I have attempted to fix all of the other mistakes that you pointed
out in the attached v3, which is also rebased.

--
Robert Haas
EDB: http://www.enterprisedb.com

Вложения

Re: plan shape work

От
Richard Guo
Дата:
On Wed, Sep 3, 2025 at 5:07 AM Robert Haas <robertmhaas@gmail.com> wrote:
> Thanks for the review. Responding just briefly to avoid quoting too much text:
>
> - I'm glad to hear that you like 0002 and consider it an improvement
> independent of what follows.
> - I'm glad to hear that you're OK with the current split between 0001 and 0002.
> - I would like opinions on those topics from more people.
> - I have attempted to fix all of the other mistakes that you pointed
> out in the attached v3, which is also rebased.

I've reviewed 0001 to 0003, and they all make sense to me.  The
changes to the EXPLAIN output in 0001 and 0002 are very nice
improvements.

I found some issues with 0003 though.  It seems get_scanned_rtindexes
is intended to return RTI sets with outer join relids excluded.  For
some node types, such as Append and MergeAppend, it fails to do so,
which can cause the assertion in assert_join_preserves_scan_rtis to
fail.  For example:

create table p (a int, b int) partition by range(a);
create table p1 partition of p for values from (0) to (10);
create table p2 partition of p for values from (10) to (20);

set enable_partitionwise_join to on;

explain (costs off)
select * from p t1
    left join p t2 on t1.a = t2.a
    left join p t3 on t2.b = t3.b;
server closed the connection unexpectedly

Besides, to exclude outer join relids, it iterates over the RTI sets,
checks each RTE for type RTE_JOIN, and bms_del_member it if found (cf.
remove_join_rtis).  I think a simpler approach would be to leverage
PlannerInfo.outer_join_rels:

    scanrelids = bms_difference(scanrelids, root->outer_join_rels);

Therefore, I suggest that we don't try to remove the join RTIs in
get_scanned_rtindexes.  Instead, we do that in
assert_join_preserves_scan_rtis -- before comparing the RTIs from the
outer and inner subplans with the join's RTIs -- by leveraging
PlannerInfo.outer_join_rels.  And remove_join_rtis can be retired.

- Richard



Re: plan shape work

От
Robert Haas
Дата:
On Thu, Sep 4, 2025 at 4:21 AM Richard Guo <guofenglinux@gmail.com> wrote:
> I found some issues with 0003 though.  It seems get_scanned_rtindexes
> is intended to return RTI sets with outer join relids excluded.  For
> some node types, such as Append and MergeAppend, it fails to do so,
> which can cause the assertion in assert_join_preserves_scan_rtis to
> fail.  For example:
>
> create table p (a int, b int) partition by range(a);
> create table p1 partition of p for values from (0) to (10);
> create table p2 partition of p for values from (10) to (20);
>
> set enable_partitionwise_join to on;
>
> explain (costs off)
> select * from p t1
>     left join p t2 on t1.a = t2.a
>     left join p t3 on t2.b = t3.b;
> server closed the connection unexpectedly

Ouch. Good catch.

> Besides, to exclude outer join relids, it iterates over the RTI sets,
> checks each RTE for type RTE_JOIN, and bms_del_member it if found (cf.
> remove_join_rtis).  I think a simpler approach would be to leverage
> PlannerInfo.outer_join_rels:
>
>     scanrelids = bms_difference(scanrelids, root->outer_join_rels);

I was not aware of outer_join_rels, so thank you for pointing it out.
However, consider this query:

select 1 from pg_class a inner join pg_class b on a.relfilenode = b.relfilenode;

Here, we end up with a three-item range table: one for a, one for b,
and one for the join. But the join is not an outer join, and does not
appear in root->outer_join_rels. Therefore, I'm not sure we can rely
on outer_join_rels in this scenario.

--
Robert Haas
EDB: http://www.enterprisedb.com



Re: plan shape work

От
Tom Lane
Дата:
Robert Haas <robertmhaas@gmail.com> writes:
> I was not aware of outer_join_rels, so thank you for pointing it out.
> However, consider this query:

> select 1 from pg_class a inner join pg_class b on a.relfilenode = b.relfilenode;

> Here, we end up with a three-item range table: one for a, one for b,
> and one for the join. But the join is not an outer join, and does not
> appear in root->outer_join_rels. Therefore, I'm not sure we can rely
> on outer_join_rels in this scenario.

Plain (not-outer) joins will never be included in a relid set in the
first place.

            regards, tom lane



Re: plan shape work

От
Robert Haas
Дата:
On Fri, Sep 5, 2025 at 12:00 PM Tom Lane <tgl@sss.pgh.pa.us> wrote:
> Plain (not-outer) joins will never be included in a relid set in the
> first place.

Ah, well then Richard's idea might work! Let me try it and see what happens...

Thanks,

--
Robert Haas
EDB: http://www.enterprisedb.com



Re: plan shape work

От
Robert Haas
Дата:

Re: plan shape work

От
Richard Guo
Дата:
On Sat, Sep 6, 2025 at 1:00 AM Tom Lane <tgl@sss.pgh.pa.us> wrote:
> Robert Haas <robertmhaas@gmail.com> writes:
> > I was not aware of outer_join_rels, so thank you for pointing it out.
> > However, consider this query:
>
> > select 1 from pg_class a inner join pg_class b on a.relfilenode = b.relfilenode;
>
> > Here, we end up with a three-item range table: one for a, one for b,
> > and one for the join. But the join is not an outer join, and does not
> > appear in root->outer_join_rels. Therefore, I'm not sure we can rely
> > on outer_join_rels in this scenario.

> Plain (not-outer) joins will never be included in a relid set in the
> first place.

Exactly.  Non-outer joins wouldn't cause Vars to become null, so we
never include them in the joinrel's relids.

BTW, I'm wondering if we can take outer join relids into account in
assert_join_preserves_scan_rtis(), which could make the check more
useful.  A joinrel's relids consists of three parts: the outer plan's
relids, the inner plan's relids, and the relids of outer joins that
are calculated at this join.  We already have the first two.  If we
can find a way to determine the third, we'd be able to assert that:

 outer_relids U inner_relids U outerjoin_relids == joinrel->relids

Determining the third part can be tricky though, especially due to
outer-join identity 3: the "outerjoin_relids" of one outer join might
include more than one outer join relids.  But I think this is till
doable.

(This may not be useful for your overall goal in this patchset, so
feel free to ignore it if it's not of interest.)

- Richard



Re: plan shape work

От
Robert Haas
Дата:
On Mon, Sep 8, 2025 at 5:51 AM Richard Guo <guofenglinux@gmail.com> wrote:
> > Plain (not-outer) joins will never be included in a relid set in the
> > first place.
>
> Exactly.  Non-outer joins wouldn't cause Vars to become null, so we
> never include them in the joinrel's relids.

OK. I didn't understand what the rules were there.

> BTW, I'm wondering if we can take outer join relids into account in
> assert_join_preserves_scan_rtis(), which could make the check more
> useful.  A joinrel's relids consists of three parts: the outer plan's
> relids, the inner plan's relids, and the relids of outer joins that
> are calculated at this join.  We already have the first two.  If we
> can find a way to determine the third, we'd be able to assert that:
>
>  outer_relids U inner_relids U outerjoin_relids == joinrel->relids
>
> Determining the third part can be tricky though, especially due to
> outer-join identity 3: the "outerjoin_relids" of one outer join might
> include more than one outer join relids.  But I think this is till
> doable.
>
> (This may not be useful for your overall goal in this patchset, so
> feel free to ignore it if it's not of interest.)

I don't mind doing the work if there's a reasonable and useful way of
accomplishing the goal. However, one concern I have is that it seems
pointless if we're computing outerjoin_relids by essentially redoing
the same computation that set the join's relid set in the first place.
In that case, the cross-check has no real probative value. All it
would be demonstrating is that if you calculate outerjoin_relids twice
using essentially the same methodology, you get the same answer. That
seems like a waste of code to me. If there's a way to calculate
outerjoin_relids using a different methodology than what we used when
populating the joinrelids, that would be interesting. It would be
similar to how the existing code recomputes the outer and inner relids
in a way that can potentially find issues that otherwise would not
have been spotted (such as the Result node case).

Do you have a proposal?

--
Robert Haas
EDB: http://www.enterprisedb.com



Re: plan shape work

От
Richard Guo
Дата:
On Mon, Sep 8, 2025 at 10:56 PM Robert Haas <robertmhaas@gmail.com> wrote:
> On Mon, Sep 8, 2025 at 5:51 AM Richard Guo <guofenglinux@gmail.com> wrote:
> > BTW, I'm wondering if we can take outer join relids into account in
> > assert_join_preserves_scan_rtis(), which could make the check more
> > useful.  A joinrel's relids consists of three parts: the outer plan's
> > relids, the inner plan's relids, and the relids of outer joins that
> > are calculated at this join.  We already have the first two.  If we
> > can find a way to determine the third, we'd be able to assert that:
> >
> >  outer_relids U inner_relids U outerjoin_relids == joinrel->relids
> >
> > Determining the third part can be tricky though, especially due to
> > outer-join identity 3: the "outerjoin_relids" of one outer join might
> > include more than one outer join relids.  But I think this is till
> > doable.
> >
> > (This may not be useful for your overall goal in this patchset, so
> > feel free to ignore it if it's not of interest.)

> I don't mind doing the work if there's a reasonable and useful way of
> accomplishing the goal. However, one concern I have is that it seems
> pointless if we're computing outerjoin_relids by essentially redoing
> the same computation that set the join's relid set in the first place.
> In that case, the cross-check has no real probative value. All it
> would be demonstrating is that if you calculate outerjoin_relids twice
> using essentially the same methodology, you get the same answer. That
> seems like a waste of code to me. If there's a way to calculate
> outerjoin_relids using a different methodology than what we used when
> populating the joinrelids, that would be interesting. It would be
> similar to how the existing code recomputes the outer and inner relids
> in a way that can potentially find issues that otherwise would not
> have been spotted (such as the Result node case).
>
> Do you have a proposal?

One idea (not fully thought through) is that we record the calculated
outerjoin_relids for each outer join in its JoinPaths.  (We cannot
store this in the joinrel's RelOptInfo because it varies depending on
the join sequence we use.)  And then we could use the recorded
outerjoin_relids for the assertion here:

 outer_relids U inner_relids U joinpath->ojrelids == joinrel->relids

The value of this approach, IMO, is that it could help verify the
correctness of how we compute outer joins' outerjoin_relids, ie. the
logic in add_outer_joins_to_relids(), which is quite complex due to
outer-join identity 3.  If we miscalculate the outerjoin_relids for
one certain outer join, this assertion could catch it effectively.

However, this shouldn't be a requirement for committing your patches.
Maybe we should discuss it in a separate thread.

- Richard



Re: plan shape work

От
Robert Haas
Дата:
First of all, as an administrative note, since both you and Alex seem
to like 0001 and 0002 and no suggestions for improvement have been
offered, I plan to commit those soon unless there are objections or
additional review comments. I will likely do the same for 0003 as
well, pending the results of the current conversation, but maybe not
quite as quickly. I believe that 0004 still needs more review, and its
effects will be more user-visible than 0001-0003, so I don't plan to
move forward with that immediately, but I invite review comments.

On Mon, Sep 8, 2025 at 10:22 PM Richard Guo <guofenglinux@gmail.com> wrote:
> One idea (not fully thought through) is that we record the calculated
> outerjoin_relids for each outer join in its JoinPaths.  (We cannot
> store this in the joinrel's RelOptInfo because it varies depending on
> the join sequence we use.)  And then we could use the recorded
> outerjoin_relids for the assertion here:
>
>  outer_relids U inner_relids U joinpath->ojrelids == joinrel->relids
>
> The value of this approach, IMO, is that it could help verify the
> correctness of how we compute outer joins' outerjoin_relids, ie. the
> logic in add_outer_joins_to_relids(), which is quite complex due to
> outer-join identity 3.  If we miscalculate the outerjoin_relids for
> one certain outer join, this assertion could catch it effectively.
>
> However, this shouldn't be a requirement for committing your patches.
> Maybe we should discuss it in a separate thread.

I'm OK with moving the conversation to a separate thread, but can you
clarify from where you believe that joinpath->ojrelids would be
populated? It seems to me that the assertion couldn't pass unless
every join path ended up with the same value of joinpath->ojrelids.
That's because, for a given joinrel, there is only one value of
joinrel->relids; and all of those RTIs must be either RTE_JOIN or
non-RTE_JOIN. The non-RTE_JOIN RTIs will be found only in outer_relids
U inner_relids, and the RTE_JOIN RTIs will be found only in
joinpath->ojrelids. Therefore, it seems impossible for the assertion
to pass unless the value is the same for all join paths. If that is
correct, then I don't think we should store the value in the join
path. Instead, if we want to cross-check it, we could calculate the
value that would have been stored into joinpath->ojrelids at whatever
earlier stage we had the information available to do so, and it should
be equal to bms_intersect(joinrel->relids, root->outer_join_rels),
which I think would have to be already initialized before we can think
of building a join path.

Please feel free to correct me if I am misunderstanding.

Thanks,

--
Robert Haas
EDB: http://www.enterprisedb.com



Re: plan shape work

От
Tom Lane
Дата:
Robert Haas <robertmhaas@gmail.com> writes:
> First of all, as an administrative note, since both you and Alex seem
> to like 0001 and 0002 and no suggestions for improvement have been
> offered, I plan to commit those soon unless there are objections or
> additional review comments.

FWIW, I don't love the details of 0001: I think it's going in the
right direction, but could use more polish.  In particular, you've
defined Result.relids in a way that seems ambiguous.  There are two
different meanings for NULL, and one of them is being interpreted as
an "Aggregate" without a lot of principle behind that.  I think you
need to store more data in order to make that less of a hack.

So far as I can see from the regression-test changes, the "Aggregate"
case only occurs when we've replaced a aggregate calculation with
a MinMaxAggPath representing an index endpoint probe.  What I would
like to see come out when that's the case is something like

    Replaces: MIN or MAX aggregate over scan on tab1

This means first that the Result.relids needs to include the relid of
the table being scanned by the indexscan, and second that EXPLAIN will
then need some other cue to help it distinguish this case from a
case where it should just say "Replaces: Scan on tab1".  It's possible
that you could intuit that by examining the initplans attached to the
Result node, but I think what would make a ton more sense is to add
an enum field to Result that explicitly identifies why it's there.
We've got at least "apply one-time filter to subplan", "apply per-row
gating filter to subplan", "represent a relation proven empty", and
"place-hold for a MinMaxAgg InitPlan".  Tracing back from all the
calls to make_result() might identify some more cases.  I'm not
arguing that the user-visible EXPLAIN output should distinguish
all of these (but probably overexplain should).  I think though
that it'd be useful to have this recorded in the plan tree.

> On Mon, Sep 8, 2025 at 10:22 PM Richard Guo <guofenglinux@gmail.com> wrote:
>> One idea (not fully thought through) is that we record the calculated
>> outerjoin_relids for each outer join in its JoinPaths.

> I'm OK with moving the conversation to a separate thread, but can you
> clarify from where you believe that joinpath->ojrelids would be
> populated? It seems to me that the assertion couldn't pass unless
> every join path ended up with the same value of joinpath->ojrelids.

What I have been intending to suggest is that you should add a field
to join plan nodes that is zero if an inner join, but the relid of
the outer join RTE if it's an outer join.  This is uniquely defined
because any given join node implements a specific outer join, even
though the planner's relids for the completed join are (intentionally)
ambiguous about the order in which multiple joins were done.

The reason I wanted to do this is that I think it'd become possible to
tighten the assertions in setrefs.c about whether Vars' varnullingrels
are correct, so that we can always assert that those relid sets are
exactly thus-and-so and not have to settle for superset/subset tests.
I've not worked through the details to be entirely sure that this is
possible, so I didn't bring it up before.  But maybe labeling join
nodes this way would also address Richard's concern.  In any case
it fits into your overall goal of decorating plan trees with more
information.

            regards, tom lane



Re: plan shape work

От
Robert Haas
Дата:
On Tue, Sep 9, 2025 at 11:12 AM Tom Lane <tgl@sss.pgh.pa.us> wrote:
> I think what would make a ton more sense is to add
> an enum field to Result that explicitly identifies why it's there.
> We've got at least "apply one-time filter to subplan", "apply per-row
> gating filter to subplan", "represent a relation proven empty", and
> "place-hold for a MinMaxAgg InitPlan".

Thanks, I'll look into this.

> What I have been intending to suggest is that you should add a field
> to join plan nodes that is zero if an inner join, but the relid of
> the outer join RTE if it's an outer join.  This is uniquely defined
> because any given join node implements a specific outer join, even
> though the planner's relids for the completed join are (intentionally)
> ambiguous about the order in which multiple joins were done.
>
> The reason I wanted to do this is that I think it'd become possible to
> tighten the assertions in setrefs.c about whether Vars' varnullingrels
> are correct, so that we can always assert that those relid sets are
> exactly thus-and-so and not have to settle for superset/subset tests.
> I've not worked through the details to be entirely sure that this is
> possible, so I didn't bring it up before.  But maybe labeling join
> nodes this way would also address Richard's concern.  In any case
> it fits into your overall goal of decorating plan trees with more
> information.

Oh, that seems quite elegant! Then we could reasonably expect to
re-find all the relevant RTIs and no others with an appropriate tree
traversal.

--
Robert Haas
EDB: http://www.enterprisedb.com



Re: plan shape work

От
Robert Haas
Дата:
On Tue, Sep 9, 2025 at 12:00 PM Robert Haas <robertmhaas@gmail.com> wrote:
> On Tue, Sep 9, 2025 at 11:12 AM Tom Lane <tgl@sss.pgh.pa.us> wrote:
> > I think what would make a ton more sense is to add
> > an enum field to Result that explicitly identifies why it's there.
> > We've got at least "apply one-time filter to subplan", "apply per-row
> > gating filter to subplan", "represent a relation proven empty", and
> > "place-hold for a MinMaxAgg InitPlan".
>
> Thanks, I'll look into this.

Just a random thought, but another idea that crossed my mind here at
one point was to actually split the Result node up into Result nodes
with subplans and Result nodes without subplans. We could call the
version with a subplan "Project" and the version without a subplan
"Result", for example. This seems a little silly because both variants
would need to be able to handle resconstantqual, or alternatively we'd
have to be OK with getting "Project" on top of "Result" in some cases
where a single "Result" node currently does both jobs. On the other
hand, only Project needs a subplan, and only Result needs relids.

--
Robert Haas
EDB: http://www.enterprisedb.com



Re: plan shape work

От
Tom Lane
Дата:
Robert Haas <robertmhaas@gmail.com> writes:
> Just a random thought, but another idea that crossed my mind here at
> one point was to actually split the Result node up into Result nodes
> with subplans and Result nodes without subplans. We could call the
> version with a subplan "Project" and the version without a subplan
> "Result", for example. This seems a little silly because both variants
> would need to be able to handle resconstantqual, or alternatively we'd
> have to be OK with getting "Project" on top of "Result" in some cases
> where a single "Result" node currently does both jobs. On the other
> hand, only Project needs a subplan, and only Result needs relids.

Maybe.  I kinda feel that actually redesigning plan trees is outside
the scope of this project, but maybe we should consider it.

I think though that you might be underestimating the amount of
commonality.  For instance, we might need a projecting node on top of
a subquery-in-FROM subplan, but that node would still have to bear
a relid --- the relid of the RTE_SUBQUERY RTE in the upper query,
not that of any RTE in the subquery, but nonetheless it's a relid.

Another variant of this is that that RTE_SUBQUERY relid would normally
be borne by a SubqueryScan plan node, but if we elide the SubqueryScan
because it isn't doing anything useful, where shall we put that relid?
If we don't store it anywhere then we will not be able to reconstruct
correct join relids for the upper plan level.

            regards, tom lane



Re: plan shape work

От
Richard Guo
Дата:
On Tue, Sep 9, 2025 at 10:18 PM Robert Haas <robertmhaas@gmail.com> wrote:
> On Mon, Sep 8, 2025 at 10:22 PM Richard Guo <guofenglinux@gmail.com> wrote:
> > One idea (not fully thought through) is that we record the calculated
> > outerjoin_relids for each outer join in its JoinPaths.  (We cannot
> > store this in the joinrel's RelOptInfo because it varies depending on
> > the join sequence we use.)  And then we could use the recorded
> > outerjoin_relids for the assertion here:
> >
> >  outer_relids U inner_relids U joinpath->ojrelids == joinrel->relids

> I'm OK with moving the conversation to a separate thread, but can you
> clarify from where you believe that joinpath->ojrelids would be
> populated? It seems to me that the assertion couldn't pass unless
> every join path ended up with the same value of joinpath->ojrelids.
> That's because, for a given joinrel, there is only one value of
> joinrel->relids; and all of those RTIs must be either RTE_JOIN or
> non-RTE_JOIN. The non-RTE_JOIN RTIs will be found only in outer_relids
> U inner_relids, and the RTE_JOIN RTIs will be found only in
> joinpath->ojrelids. Therefore, it seems impossible for the assertion
> to pass unless the value is the same for all join paths.

Hmm, this isn't quite what I had in mind.  What I was thinking is that
the outer join relids included in joinrel->relids can also be found
from its outer or inner.  For example, consider a query like:

  (A leftjoin B on (Pab)) leftjoin C on (Pbc)

For the join with joinrel->relids being {1, 2, 3, 4, 5}, {1, 2, 3}
comes from the outer side, {4} comes from the inner side, and {5} is
the outer join being calculated at this join.  So the Assert I
proposed earlier becomes:

  {1, 2, 3} U {4} U {5} == {1, 2, 3, 4, 5}

However, if we have transformed it to:

  A leftjoin (B leftjoin C on (Pbc)) on (Pab)

For this same join, {1} comes from the outer side, {2, 4} comes from
the inner side, and {3, 5} are the outer joins being calculated at
this join.  So the Assert becomes:

  {1} U {2, 4} U {3, 5} == {1, 2, 3, 4, 5}

Either way, the assertion should always hold -- if it doesn't, there's
likely a bug in how we're calculating the relids.

As you can see, the set of outer joins calculated at the same join can
vary depending on the join order.  What I suggested is to record this
information in JoinPaths (or maybe also in Join plan nodes so that
get_scanned_rtindexes can collect it) for the assertion.

- Richard



Re: plan shape work

От
Robert Haas
Дата:
On Wed, Sep 10, 2025 at 3:16 AM Richard Guo <guofenglinux@gmail.com> wrote:
> Hmm, this isn't quite what I had in mind.  What I was thinking is that
> the outer join relids included in joinrel->relids can also be found
> from its outer or inner.  For example, consider a query like:
>
>   (A leftjoin B on (Pab)) leftjoin C on (Pbc)
>
> For the join with joinrel->relids being {1, 2, 3, 4, 5}, {1, 2, 3}
> comes from the outer side, {4} comes from the inner side, and {5} is
> the outer join being calculated at this join.  So the Assert I
> proposed earlier becomes:
>
>   {1, 2, 3} U {4} U {5} == {1, 2, 3, 4, 5}

Makes sense.

> However, if we have transformed it to:
>
>   A leftjoin (B leftjoin C on (Pbc)) on (Pab)
>
> For this same join, {1} comes from the outer side, {2, 4} comes from
> the inner side, and {3, 5} are the outer joins being calculated at
> this join.  So the Assert becomes:
>
>   {1} U {2, 4} U {3, 5} == {1, 2, 3, 4, 5}

Hmm. As I understood it, Tom was proposing a single, optional ojrelid
for each join, with all outer joins having a value and no inner join
having one. What you are proposing seems to be a very similar concept,
but as I understand it, you're saying that each join would carry a
*set* of ojrelids, which might be empty and might contain more than
one element.

Experimenting with this example, it looks like you're correct and Tom,
or my understanding of Tom, is incorrect. What I see is that I get a
structure like this:

   {MERGEPATH
   :parent_relids (b 1 2 3 4 5)
   :jpath.outerjoinpath
      {PATH
      :parent_relids (b 1)
   :jpath.innerjoinpath
      {MERGEPATH
      :parent_relids (b 2 4)
      :jpath.outerjoinpath
         {PATH
         :parent_relids (b 2)
         }
      :jpath.innerjoinpath
         {PATH
         :parent_relids (b 4)
         }
      }
   }

So, for the assertion to pass, the more deeply nested merge join would
need to have ojrelids = {} and the less-deeply nested one would need
ojrelids={3,5}.

--
Robert Haas
EDB: http://www.enterprisedb.com



Re: plan shape work

От
Robert Haas
Дата:
On Tue, Sep 9, 2025 at 4:37 PM Tom Lane <tgl@sss.pgh.pa.us> wrote:
> Maybe.  I kinda feel that actually redesigning plan trees is outside
> the scope of this project, but maybe we should consider it.

Yeah, I would rather not go there, all things being equal. We could
also consider displaying things differently even if the underlying
node type is the same. ExplainNode already has examples where
pname/sname are set to something other than the node tag, e.g. we set
EXPLAIN designation based on (((ModifyTable *) plan)->operation) or
agg->aggstrategy. I'm not sure if this is the right way to go, but
it's worth a thought.

> I think though that you might be underestimating the amount of
> commonality.  For instance, we might need a projecting node on top of
> a subquery-in-FROM subplan, but that node would still have to bear
> a relid --- the relid of the RTE_SUBQUERY RTE in the upper query,
> not that of any RTE in the subquery, but nonetheless it's a relid.
>
> Another variant of this is that that RTE_SUBQUERY relid would normally
> be borne by a SubqueryScan plan node, but if we elide the SubqueryScan
> because it isn't doing anything useful, where shall we put that relid?
> If we don't store it anywhere then we will not be able to reconstruct
> correct join relids for the upper plan level.

I don't quite understand how these two scenarios are different, but I
have found it critical to distinguish problems that happen at setrefs
time from problems that happen during main planning. If we're talking
about feeding information from one planning cycle forward to the next,
we need be able to look at the plan tree and understand what the
pre-setrefs state of affairs was, because when the next planning cycle
happens, the decisions we want to influence are happening pre-setrefs.
The later patches in this patch series deal with exactly this problem:
0004 and 0005 make it possible to match up an RTI from the flattened
range table that pops out of one planning cycle with a specific
subroot and RTI relative to that subroot during the following cycle;
and 0006 and 0007 arrange to leave a breadcrumb trail in cases where
setrefs-time processing deletes plan nodes. The setrefs-time elision
of SubqueryScan nodes is recorded by the mechanism in 0006.

I went back and studied 0001 some more today in reference to your
comments about classifying Result nodes. 0001 already loosely
classifies Result nodes as either "gating" result nodes (that have a
subplan) or "simple" result notes (that don't). "Gating" result notes
happen for target-list projection and/or to apply a one-time filter. I
don't think the reasons for gating nodes need to be recorded anywhere;
either the tlist matches the underlying node or it doesn't, and either
resconstantqual contains something or not. "Simple" result have more
interesting reasons for existing:

1. MinMaxAgg placeholders
2. Degenerate grouping
3. No-FROM-clause cases (these go through create_group_result_plan
like the previous case, but are arguably distinct)
4. Relations proven empty

There's a sort of hybrid case when we want a gating result node but
the underlying node is a simple result. In that case, the patch builds
a new simple result that is similar to the existing one but with the
gating result's target list and one-time filter. It seems OK to me to
forget all about the gating result node and its reason for existence
in this case and just consider ourselves to have updated the
underlying Result node. Otherwise, you'd have to consider that a
Result node might have multiple reasons for existence: whatever caused
the "simple" Result note to get created, plus possibly projection or
one-time filtering.

Now, looking at (1)-(4) above, (3) is actually a special case of (4):

robert.haas=# explain (range_table) select 1;
                QUERY PLAN
------------------------------------------
 Result  (cost=0.00..0.01 rows=1 width=4)
   RTIs: 1
 RTI 1 (result):
   Eref: "*RESULT*" ()
(4 rows)

So the reason why the patch set feels justified in printing "Replaces:
Aggregate" when there are no relids is because we must have case (1)
or (2) from the above list. But it does seem fragile. Not only can we
confuse (1) and (2), but also, only the top-level grouping rel
necessarily has empty relids. We already have child grouping rels that
have relid sets, and I suspect Richard's pending work on eager
aggregation will introduce more of them. This patch won't be able to
distinguish those from case (4).

So maybe what we want for Result reasons is something like
RESULT_REPLACES_BASEREL, RESULT_REPLACES_JOINREL,
RESULT_REPLACES_GROUPING_REL, RESULT_IMPLEMENTS_MINMAX_AGGREGATE?
That's a bit verbose; shorter alternatives welcome. The first two
could be merged, since the cardinality of the relid set should
distinguish them. Or it could be more like RESULT_REPLACES_SCAN,
RESULT_REPLACES_JOIN, RESULT_REPLACES_AGGREGATE,
RESULT_IMPLEMENTS_MINMAX_AGGREGATE, to more closely match what we
would presumably show in the EXPLAIN output.

Thoughts?

--
Robert Haas
EDB: http://www.enterprisedb.com



Re: plan shape work

От
Tom Lane
Дата:
Richard Guo <guofenglinux@gmail.com> writes:
> As you can see, the set of outer joins calculated at the same join can
> vary depending on the join order.  What I suggested is to record this
> information in JoinPaths (or maybe also in Join plan nodes so that
> get_scanned_rtindexes can collect it) for the assertion.

I do not think this is correct, or at least it's not the most useful
way to think about it.  As I stated earlier, each join plan node that
is doing a non-inner join has a unique corresponding outer-join RTE
that describes what it's doing.  The point that you are making is
that if we've applied identity 3 to swap the order of two outer
joins, then we can't claim that the output of the lower plan node is a
fully-correct representation of the semantics of the join that it's
doing: there may be values that should have gone to NULL but won't
until after the upper plan node processes the rows.

If we're going to attach more labeling to the plan nodes, I'd
prefer to do what I suggested and label the nodes with the specific
outer join that they think they are implementing.  With Richard's
proposal it will remain impossible to tell which node is doing what.

While I've still not worked through the details, it might be that
we can't implement my desire to make setrefs.c's nullingrels
checks exact unless we *also* store the bitmap sets that Richard
is proposing.  I don't know if it's worth carrying two new fields
in order to make that work.

I don't entirely buy that Richard's proposed assertion is worth
doing: I think I agree with Robert's original opinion that it's
redundant.  I do think that tightening the nullingrels checks
would be useful.

            regards, tom lane



Re: plan shape work

От
Robert Haas
Дата:
On Thu, Sep 11, 2025 at 2:19 PM Tom Lane <tgl@sss.pgh.pa.us> wrote:
> I do not think this is correct, or at least it's not the most useful
> way to think about it.  As I stated earlier, each join plan node that
> is doing a non-inner join has a unique corresponding outer-join RTE
> that describes what it's doing.  The point that you are making is
> that if we've applied identity 3 to swap the order of two outer
> joins, then we can't claim that the output of the lower plan node is a
> fully-correct representation of the semantics of the join that it's
> doing: there may be values that should have gone to NULL but won't
> until after the upper plan node processes the rows.
>
> If we're going to attach more labeling to the plan nodes, I'd
> prefer to do what I suggested and label the nodes with the specific
> outer join that they think they are implementing.  With Richard's
> proposal it will remain impossible to tell which node is doing what.

Conceptually, I prefer your idea of one RTI per join node, but I don't
understand how to make it work. Let's say that, as in Richard's
example, the query is written as (A leftjoin B on (Pab)) leftjoin C on
(Pbc) but we end up with a plan tree that looks like this:

Something Join (RTIs: 1 2 3 4 5)
-> Scan on A (RTI: 1)
-> Whatever Join (RTIs: 2 4)
    -> Scan on B (RTI: 2)
    -> Scan on C (RTI: 4)

RTE 3 is the join between A and B, and RTI 5 is the join between A-B
and C. It makes plenty of sense to associate RTI 5 with the Something
Join, so your model seems to require us to associate RTI 3 with the
Whatever Join, because there's no place else for it to go. That seems
to create two problems.

First, RTI 3 is for an A-B join, and the Whatever Join is for a B-C
join, and it sounds wrong to associate an RTI with a join when not all
of the rels being joined are present at that level. Can we really say
that the Whatever Join is implementing RTI 3 given that RTI 3 includes
A?

Second, even ignoring that problem, if we now try to assert that the
RTIs of a joinrel are the union of the RTIs we see in the plan tree,
the assertion is going to fail, because now the Whatever Join sees
RTIs 2 and 4 through its children and RTI 3 through its own ojrelid,
but the joinrel's RTIs are {2,4}, not {2,3,4}.

--
Robert Haas
EDB: http://www.enterprisedb.com



Re: plan shape work

От
Robert Haas
Дата:
Here's a likely-doomed new version of just the first three patches.
0002 is unchanged. 0001 has been reworked so that a Result node
contains a result_type. This is as per Tom's suggestion, but it's
unclear to me that he will like the details. 0003 has been reworked so
that when we build a Join plan, we annotate it with the ojrelids
completed at that level, which Tom said earlier that he thought was
the wrong idea (but after I'd already written the code, and I've
already replied to say I don't understand what the alternative is).

Hence, I expect this version to crash and burn, but maybe it will do
so in such a way that I have some idea what to propose instead.

--
Robert Haas
EDB: http://www.enterprisedb.com

Вложения

Re: plan shape work

От
Tom Lane
Дата:
Robert Haas <robertmhaas@gmail.com> writes:
> On Thu, Sep 11, 2025 at 2:19 PM Tom Lane <tgl@sss.pgh.pa.us> wrote:
>> If we're going to attach more labeling to the plan nodes, I'd
>> prefer to do what I suggested and label the nodes with the specific
>> outer join that they think they are implementing.  With Richard's
>> proposal it will remain impossible to tell which node is doing what.

> Conceptually, I prefer your idea of one RTI per join node, but I don't
> understand how to make it work. Let's say that, as in Richard's
> example, the query is written as (A leftjoin B on (Pab)) leftjoin C on
> (Pbc) but we end up with a plan tree that looks like this:

> Something Join (RTIs: 1 2 3 4 5)
> -> Scan on A (RTI: 1)
> -> Whatever Join (RTIs: 2 4)
>     -> Scan on B (RTI: 2)
>     -> Scan on C (RTI: 4)

After thinking about this for awhile, I believe that Richard and I
each had half of the right solution ;-).  Let me propose some new
terminology in hopes of clarifying matters:

* A join plan node "starts" an outer join if it performs the
null-extension step corresponding to that OJ (specifically,
if it is the first join doing null-extension over the minimum
RHS of that OJ).

* A join plan node "completes" an outer join if its output
nulls all the values that that OJ should null when done
according to syntactic order.

In simple cases where we have not applied OJ identity 3, every
outer-join plan node starts and completes a single OJ relid.
But if we have applied identity 3 in the forward direction,
as per your example above, it's different.  The physically
lower join node starts OJ 5, but doesn't complete it.  The
upper node starts OJ 3, and completes both 3 and 5.  I think
that it's possible for the topmost join to complete more than
two OJs, if we have a nest of multiple OJs that can all be
re-ordered via identity 3.

I was arguing for labeling plan nodes according to which OJ they
start (always a unique relid).  Richard was arguing for labeling
according to which OJ(s) they complete (zero, one, or more relids).
But I now think it's probably worth doing both.  We need the
completion bitmapsets if we want to cross-check Var nullingrels,
because those correspond to the nullingrels that should get added
at each join's output.  I think that we also want the start labels
though.  For one thing, if the start nodes are not identified,
it's impossible to understand how much of the tree is the "no
man's land" where a C variable may or may not have gone to null
on its way to becoming a C* variable.  But in general I think
that we'll want to be able to identify an outer-join plan node
even if it does not complete its OJ.

            regards, tom lane



Re: plan shape work

От
Robert Haas
Дата:
On Fri, Sep 12, 2025 at 11:08 AM Tom Lane <tgl@sss.pgh.pa.us> wrote:
> I was arguing for labeling plan nodes according to which OJ they
> start (always a unique relid).  Richard was arguing for labeling
> according to which OJ(s) they complete (zero, one, or more relids).

I agree with everything in your reply up to and including this part.

> But I now think it's probably worth doing both.  We need the
> completion bitmapsets if we want to cross-check Var nullingrels,
> because those correspond to the nullingrels that should get added
> at each join's output.  I think that we also want the start labels
> though.  For one thing, if the start nodes are not identified,
> it's impossible to understand how much of the tree is the "no
> man's land" where a C variable may or may not have gone to null
> on its way to becoming a C* variable.  But in general I think
> that we'll want to be able to identify an outer-join plan node
> even if it does not complete its OJ.

So, it looks to me like the way this works today is that
join_is_legal() figures out the relevant SpecialJoinInfo and then
add_outer_joins_to_relids() decides what to add to the joinrel's relid
set. So I think, though I am not quite sure, that if somewhere around
that point in the code we copied sjinfo->ojrelid into the RelOptInfo,
and then propagated that through to the final plan, that might be what
you're looking for here. However, that assumes that the choice of
SpecialJoinInfo is fixed for all possible ways of constructing a given
joinrel, which I think might not be true. In Richard's test case, one
simply can't go wrong, because the lower join only draws from two
baserels. But in a case like A LJ B ON A.x = B.x LJ C ON B.y = C.y LJ
D ON B.z = D.z, the B-C-D joinrel could presumably be constructed by
joining either B-D to C or B-C to D, and I'm guessing that will result
in a different choice of SpecialJoinInfo in each case. That would mean
that the ojrelid has to be per-Path rather than per-RelOptInfo.

While in theory that's fine, it sounds expensive. I was hoping that we
could piggyback mostly on existing calculations here, exposing data we
already have instead of calculating new things. If we want to expose
the starting-the-outer-join-RTI value that you want here, it seems
like we're going to have to redo some of the join_is_legal() work and
some of the add_outer_joins_to_relids() work for every path. I'm
skeptical about expending those cycles. It's not clear to me that I
need to care about outer join RTIs at all for what I'm trying to do --
focusing where RTIs originating from baserels end up in the final plan
tree is, as far as I can see, completely adequate. There might be
other people who want to do other things that would benefit more from
seeing that stuff, though. I'm not against exposing information that
is easily calculated and might be useful to somebody, even if it's
just for planner debugging. But it seems to me that what you're asking
here might be going quite a bit further than that.

So my counter-proposal is that this patch set should either (1) expose
nothing at all about join RTIs because I don't have a need for them or
(2) expose the join RTIs completed at a certain level because that's
easily calculated from the data we already have; and if you want to
later also expose the single join RTI started at a certain level for a
varnullingrels cross-check or any other purpose, then you can propose
a patch for that. Alternatively, if you want to edit my 0003 patch to
work the way you think it should, cool.  Or if you can describe what
you think it should do, I'm somewhat willing to try implementing that,
but that's definitely not such a great choice from my perspective. I'm
afraid that I'm getting pulled a bit further down the garden path than
I really want to go trying to satisfy your desire to perform a
cross-check that I don't really understand and/or expose information
for which I don't see a clear need in my own work. What I need is for
all the baserels that appear in the final Plan tree to be properly
labelled.

--
Robert Haas
EDB: http://www.enterprisedb.com



Re: plan shape work

От
Tom Lane
Дата:
Robert Haas <robertmhaas@gmail.com> writes:
> So, it looks to me like the way this works today is that
> join_is_legal() figures out the relevant SpecialJoinInfo and then
> add_outer_joins_to_relids() decides what to add to the joinrel's relid
> set. So I think, though I am not quite sure, that if somewhere around
> that point in the code we copied sjinfo->ojrelid into the RelOptInfo,
> and then propagated that through to the final plan, that might be what
> you're looking for here.

Not the RelOptInfo, but the Path.  I have not looked at details, but
it might be necessary to identify these labels at Path construction
time rather than reconstructing them during createplan.c.  I'd rather
not, because bloating Path nodes with hopefully-redundant information
isn't attractive.  But if it turns out that createplan.c doesn't have
enough information then we might have to.

I'm also thinking that this notion of starting/completing OJs might be
useful in its own right to clarify and even simplify some of the Path
manipulations we do.  But that will require reviewing the code.

> So my counter-proposal is that this patch set should either (1) expose
> nothing at all about join RTIs because I don't have a need for them or
> (2) expose the join RTIs completed at a certain level because that's
> easily calculated from the data we already have; and if you want to
> later also expose the single join RTI started at a certain level for a
> varnullingrels cross-check or any other purpose, then you can propose
> a patch for that.

If what you want to do is only interested in baserel RTIs, then I
think we should leave outer join RTIs out of the discussion for the
present.  I was not looking to make you do work you aren't interested
in.  I reserve the right to do said work later ...

            regards, tom lane



Re: plan shape work

От
Robert Haas
Дата:
On Fri, Sep 12, 2025 at 12:18 PM Tom Lane <tgl@sss.pgh.pa.us> wrote:
> Not the RelOptInfo, but the Path.  I have not looked at details, but
> it might be necessary to identify these labels at Path construction
> time rather than reconstructing them during createplan.c.  I'd rather
> not, because bloating Path nodes with hopefully-redundant information
> isn't attractive.  But if it turns out that createplan.c doesn't have
> enough information then we might have to.

My intuition (which might be wrong) is that there's enough information
available to do it during createplan.c, but I'm not sure that there's
enough information available to do it efficiently at that stage. You
could grovel through the whole Plan tree and find all of the scan
RTIs, and then from there you should be able to work out what joins
were commuted, and then from there you should be able to work out
which SpecialJoinInfo goes with each Join that appears in the final
plan tree. That doesn't sound like a lot of fun, though. On the other
hand, I'm not sure doing this at Path creation time is going to be a
picnic either. Right now, we're sort of cheating by only caring about
what OJs are finished at a certain level: that is consistent across
all possible ways of forming a joinrel, but which OJs are started at a
certain level is not, and I'm not currently seeing how to fix that
without adding cycles.

> I'm also thinking that this notion of starting/completing OJs might be
> useful in its own right to clarify and even simplify some of the Path
> manipulations we do.  But that will require reviewing the code.

Makes sense.

> If what you want to do is only interested in baserel RTIs, then I
> think we should leave outer join RTIs out of the discussion for the
> present.  I was not looking to make you do work you aren't interested
> in.  I reserve the right to do said work later ...

Absolutely. I'm more than happy to have you do that.

We sort of got started down this path because, reviewing v4-0003,
Richard commented that I might be able to sanity-check
something-or-other about RTE_JOIN RTIs instead of just focusing on
baserels. From there, this sub-thread has turned into a discussion of
exactly what that sanity check should be. v5 exposes the
completed-at-this-level OJs in the final plan tree, which is easy to
compute and could be useful for somebody's plan introspection, but (1)
I don't need it and (2) it just derives them from the joinrel's RTI
set rather than in any independent per-path way that might lead to a
more meaningful cross-check. Having done the work to create v5-0003, I
find myself thinking it feels a little tidier than v4 and am somewhat
inclined to prefer it; I think that it's very possible that you or
Richard might find it a useful basis for future work to further
strengthen the way things work in this area. However, on the other
hand, maybe not, and going back to v4-0003 is also completely
reasonable. I don't care much one way or the other as long as nobody's
too mad when the dust settles.

But, I also can't commit either v4-0003 or v5-0003 or any variant
thereof until we agree on what to do about 0001, and you're the
holdout there. v5-0001 adds a result_type field to the Result node in
response to your previous review comments, so knowing whether that
looks like what you want or whether you would prefer something else is
the blocker for me as of this moment.

Thanks,

--
Robert Haas
EDB: http://www.enterprisedb.com



Re: plan shape work

От
Tom Lane
Дата:
Robert Haas <robertmhaas@gmail.com> writes:
> But, I also can't commit either v4-0003 or v5-0003 or any variant
> thereof until we agree on what to do about 0001, and you're the
> holdout there.

Yeah, I owe you a review, hope to get to it over the weekend.

            regards, tom lane



Re: plan shape work

От
Robert Haas
Дата:
On Fri, Sep 12, 2025 at 1:44 PM Tom Lane <tgl@sss.pgh.pa.us> wrote:
> Robert Haas <robertmhaas@gmail.com> writes:
> > But, I also can't commit either v4-0003 or v5-0003 or any variant
> > thereof until we agree on what to do about 0001, and you're the
> > holdout there.
>
> Yeah, I owe you a review, hope to get to it over the weekend.

Thanks. This is a good time to mention that I appreciate your
engagement in this thread. Let me know if there's something that you'd
like me to pay attention to in turn.

--
Robert Haas
EDB: http://www.enterprisedb.com



Re: plan shape work

От
Richard Guo
Дата:
On Sat, Sep 13, 2025 at 12:08 AM Tom Lane <tgl@sss.pgh.pa.us> wrote:
> After thinking about this for awhile, I believe that Richard and I
> each had half of the right solution ;-).  Let me propose some new
> terminology in hopes of clarifying matters:
>
> * A join plan node "starts" an outer join if it performs the
> null-extension step corresponding to that OJ (specifically,
> if it is the first join doing null-extension over the minimum
> RHS of that OJ).
>
> * A join plan node "completes" an outer join if its output
> nulls all the values that that OJ should null when done
> according to syntactic order.

This new notion makes a lot of sense to me.  I feel that it could help
us optimize some existing logic, or at least make certain parts of it
easier to understand.  It might be worth adding to the README, maybe
under the section "Relation Identification and Qual Clause Placement",
where we explain the idea behind pushed-down joins.

- Richard



Re: plan shape work

От
Richard Guo
Дата:
On Sat, Sep 13, 2025 at 2:32 AM Robert Haas <robertmhaas@gmail.com> wrote:
> We sort of got started down this path because, reviewing v4-0003,
> Richard commented that I might be able to sanity-check
> something-or-other about RTE_JOIN RTIs instead of just focusing on
> baserels. From there, this sub-thread has turned into a discussion of
> exactly what that sanity check should be.

Yeah, when commenting on v4-0003 about including outer join relids in
the assertion, I had a feeling that it might take the discussion off
topic -- sorry that it did.  That's why I suggested moving this part
of discussion to a separate thread.

I still think that cross-checking outer join relids isn't a
requirement for committing your patches, so I'm totally fine if you
end up with a version that doesn't assert outer join relids.

- Richard



Re: plan shape work

От
Tom Lane
Дата:
I wrote:
> Robert Haas <robertmhaas@gmail.com> writes:
>> But, I also can't commit either v4-0003 or v5-0003 or any variant
>> thereof until we agree on what to do about 0001, and you're the
>> holdout there.

> Yeah, I owe you a review, hope to get to it over the weekend.

I'm running out of weekend, but I found time to look at v5-0001,
so here are some comments.  In general, it's in pretty good shape
and these are nitpicks.

* I think your placement of the show_result_replacement_info call
site suffers from add-at-the-end syndrome.  It should certainly go
before the show_instrumentation_count call: IMO we expect stuff
added by EXPLAIN ANALYZE to appear after stuff that's there in plain
EXPLAIN.  But I'd really argue that from a user's standpoint this
information is part of the fundamental plan structure and so it
deserves more prominence.  I'd lean to putting it first in the
T_Result case, before the "One-Time Filter".  (Thought experiment:
if we'd had this EXPLAIN field from day one, where do you think
it would have been placed?)

* Even more nitpicky:

+            if (plan->lefttree == NULL)
+                show_result_replacement_info(castNode(Result, plan), es);

I think show_result_replacement_info should have responsibility for
deciding whether to print anything in the lefttree == NULL case.
(This will affect the Asserts in show_result_replacement_info,
but those seem a little odd anyway.)

+        case RESULT_TYPE_UPPER:
+            /* a small white lie */
+            replacement_type = "Aggregate";
+            break;

I find this unconvincing: is it really an aggregate?  It doesn't
help that this case doesn't seem to be reached anywhere in the
regression tests.

In general I suspect that we'll have to refine RESULT_TYPE_UPPER
in the future.  I don't think this fear needs to block committing
of what you have though.

+        /* Work out what reference name to use and added it the string. */

The grammar police will be after you.

+     * (Arguably, we should instead display the RTE name in some other way in
+     * such cases, but in typical cases the RTE name is *RESULT* and printing
+     * "Result on *RESULT*" or similar doesn't seem especially useful, so for
+     * now we don't print anything at all.)

Right offhand, I think that RTE_RESULT *always* has the name *RESULT*,
so the "typical" bit seems misleading.  Personally I'd drop this
para altogether.

+    /*
+     * We're replacing either a scan or a join, according to the number of
+     * rels in the relids set.
+     */
+    if (nrels == 0)
+        ExplainPropertyText("Replaces", replacement_type, es);
+    else
+    {
+        char *s = psprintf("%s on %s", replacement_type, buf.data);
+
+        ExplainPropertyText("Replaces", s, es);
+    }

This comment seems to neither have anything to do with the logic,
or to be adding anything.  But do you need nrels at all?  I'd
be inclined to check for "buf.len > 0" to see if you want to
insert the "on ..." bit.

+ * make_simple_result
+ *      Build a Result plan node that returns a single row (or possibly no rows,
+ *      if the one-time filtered defined by resconstantqual returns false)

I don't love the name "make_simple_result", as the cases this handles
are frequently far from simple.  I don't have a clearly-better idea
offhand though.  Maybe "make_one_row_result"?  In any case, "one-time
filtered" needs help.

In general, I wonder if it'd be better for the callers of
make_xxx_result to pass in the result_type to use.  Those
functions have such a narrow view of the available info
that I'm dubious that they can get it right.  Especially
if/when we decide that RESULT_TYPE_UPPER needs subdivision.

+ * relids identifies the relation for which this Result node is generating the
+ * tuples. When subplan is not NULL, it should be empty: this node is not
+ * generating anything in that case, just acting on tuples generated by the
+ * subplan. Otherwise, it may contain a single RTI (as when this Result node
+ * is substituted for a scan); multiple RTIs (as when this Result node is
+ * substituted for a join); or no RTIs at all (as when this Result node is
+ * substituted for an upper rel).

I doubt this claim that the relid set will be empty for an upper rel.
I think it's more likely that it will include all the rels for the
query.

            regards, tom lane



Re: plan shape work

От
Robert Haas
Дата:
Thanks for the review. Comments to which I don't respond below are duly noted.

On Sun, Sep 14, 2025 at 7:42 PM Tom Lane <tgl@sss.pgh.pa.us> wrote:
> * I think your placement of the show_result_replacement_info call
> site suffers from add-at-the-end syndrome.  It should certainly go
> before the show_instrumentation_count call: IMO we expect stuff
> added by EXPLAIN ANALYZE to appear after stuff that's there in plain
> EXPLAIN.  But I'd really argue that from a user's standpoint this
> information is part of the fundamental plan structure and so it
> deserves more prominence.  I'd lean to putting it first in the
> T_Result case, before the "One-Time Filter".  (Thought experiment:
> if we'd had this EXPLAIN field from day one, where do you think
> it would have been placed?)

Yes, I wondered if it should actually look more like this:

Degenerate Scan on blah
Degenerate Join on blah, blah, blah
Degenerate Aggregate
MinMaxAggregate Result

So getting rid of Replaces: altogether. In fact, "Replaces" is really
a complete misnomer in the case of a MinMaxAggregate; I'm just not
sure exactly what to do instead.


> +               case RESULT_TYPE_UPPER:
> +                       /* a small white lie */
> +                       replacement_type = "Aggregate";
> +                       break;
>
> I find this unconvincing: is it really an aggregate?  It doesn't
> help that this case doesn't seem to be reached anywhere in the
> regression tests.

This is the case I know about where that can be reached.

robert.haas=# explain select 1 as a, 2 as b having false;
                QUERY PLAN
------------------------------------------
 Result  (cost=0.00..0.01 rows=1 width=8)
   One-Time Filter: false
   Replaces: Aggregate
(3 rows)

> In general I suspect that we'll have to refine RESULT_TYPE_UPPER
> in the future.  I don't think this fear needs to block committing
> of what you have though.

+1.

> +     * (Arguably, we should instead display the RTE name in some other way in
> +     * such cases, but in typical cases the RTE name is *RESULT* and printing
> +     * "Result on *RESULT*" or similar doesn't seem especially useful, so for
> +     * now we don't print anything at all.)
>
> Right offhand, I think that RTE_RESULT *always* has the name *RESULT*,
> so the "typical" bit seems misleading.  Personally I'd drop this
> para altogether.

Counterexample:

robert.haas=# explain verbose select * from pgbench_accounts where 0 = 1;
                QUERY PLAN
------------------------------------------
 Result  (cost=0.00..0.00 rows=0 width=0)
   Output: aid, bid, abalance, filler
   One-Time Filter: false
   Replaces: Scan on pgbench_accounts
(4 rows)

debug_print_plan says:

      :alias <>
      :eref
         {ALIAS
         :aliasname pgbench_accounts
         :colnames ("aid" "bid" "abalance" "filler")
         }

> In general, I wonder if it'd be better for the callers of
> make_xxx_result to pass in the result_type to use.  Those
> functions have such a narrow view of the available info
> that I'm dubious that they can get it right.  Especially
> if/when we decide that RESULT_TYPE_UPPER needs subdivision.

That was my first thought, but after experimentation I think it sucks,
especially because of this:

                /*
                 * The only path for it is a trivial Result path.  We cheat a
                 * bit here by using a GroupResultPath, because that way we
                 * can just jam the quals into it without preprocessing them.
                 * (But, if you hold your head at the right angle, a FROM-less
                 * SELECT is a kind of degenerate-grouping case, so it's not
                 * that much of a cheat.)
                 */

I would argue that if you hold your head at that angle, you need your
head examined. Perhaps that wasn't the case when this comment was
written, but from the viewpoint of this project, I think it's pretty
clear. What this does is use create_group_result_path() not only for
actual degenerate-grouping cases (where we're replacing an aggregate)
but also for no-FROM-clause cases (where we're replacing a scan). We
could adjust things so that the no-FROM-clause case doesn't take this
code path, or passes down a flag, but it's clear from examination of
the RelOptInfo. Likewise, for scans vs. joins, peaking at the
reloptkind is a very easy way to tell whether we've got a baserel or a
joinrel and having the caller go to extra trouble to pass it down just
seems silly.

> + * relids identifies the relation for which this Result node is generating the
> + * tuples. When subplan is not NULL, it should be empty: this node is not
> + * generating anything in that case, just acting on tuples generated by the
> + * subplan. Otherwise, it may contain a single RTI (as when this Result node
> + * is substituted for a scan); multiple RTIs (as when this Result node is
> + * substituted for a join); or no RTIs at all (as when this Result node is
> + * substituted for an upper rel).
>
> I doubt this claim that the relid set will be empty for an upper rel.
> I think it's more likely that it will include all the rels for the
> query.

Upper rels are created by fetch_upper_rel(). The third argument
becomes the relids set. Most call sites pass that argument as NULL:

[robert.haas pgsql]$ git grep fetch_upper_rel src/backend/optimizer/
src/backend/optimizer/path/allpaths.c:   sub_final_rel =
fetch_upper_rel(rel->subroot, UPPERREL_FINAL, NULL);
src/backend/optimizer/path/costsize.c:   sub_final_rel =
fetch_upper_rel(subroot, UPPERREL_FINAL, NULL);
src/backend/optimizer/plan/planagg.c:    grouped_rel =
fetch_upper_rel(root, UPPERREL_GROUP_AGG, NULL);
src/backend/optimizer/plan/planner.c:    final_rel =
fetch_upper_rel(root, UPPERREL_FINAL, NULL);
src/backend/optimizer/plan/planner.c:    final_rel =
fetch_upper_rel(root, UPPERREL_FINAL, NULL);
src/backend/optimizer/plan/planner.c:    final_rel =
fetch_upper_rel(root, UPPERREL_FINAL, NULL);
src/backend/optimizer/plan/planner.c:        grouped_rel =
fetch_upper_rel(root, UPPERREL_GROUP_AGG,
src/backend/optimizer/plan/planner.c:        grouped_rel =
fetch_upper_rel(root, UPPERREL_GROUP_AGG, NULL);
src/backend/optimizer/plan/planner.c:    window_rel =
fetch_upper_rel(root, UPPERREL_WINDOW, NULL);
src/backend/optimizer/plan/planner.c:    distinct_rel =
fetch_upper_rel(root, UPPERREL_DISTINCT, NULL);
src/backend/optimizer/plan/planner.c:    partial_distinct_rel =
fetch_upper_rel(root, UPPERREL_PARTIAL_DISTINCT,
src/backend/optimizer/plan/planner.c:    ordered_rel =
fetch_upper_rel(root, UPPERREL_ORDERED, NULL);
src/backend/optimizer/plan/planner.c:    partially_grouped_rel =
fetch_upper_rel(root,
src/backend/optimizer/plan/setrefs.c:
IS_DUMMY_REL(fetch_upper_rel(rel->subroot,
src/backend/optimizer/plan/subselect.c:  final_rel =
fetch_upper_rel(subroot, UPPERREL_FINAL, NULL);
src/backend/optimizer/plan/subselect.c:          final_rel =
fetch_upper_rel(subroot, UPPERREL_FINAL, NULL);
src/backend/optimizer/plan/subselect.c:      final_rel =
fetch_upper_rel(subroot, UPPERREL_FINAL, NULL);
src/backend/optimizer/prep/prepunion.c:  result_rel =
fetch_upper_rel(root, UPPERREL_SETOP,
src/backend/optimizer/prep/prepunion.c:  final_rel =
fetch_upper_rel(rel->subroot, UPPERREL_FINAL, NULL);
src/backend/optimizer/prep/prepunion.c:  result_rel =
fetch_upper_rel(root, UPPERREL_SETOP, relids);
src/backend/optimizer/prep/prepunion.c:  result_rel =
fetch_upper_rel(root, UPPERREL_SETOP,
src/backend/optimizer/util/relnode.c: * fetch_upper_rel
src/backend/optimizer/util/relnode.c:fetch_upper_rel(PlannerInfo
*root, UpperRelationKind kind, Relids relids)

The exceptions are: make_grouping_rel() passes a relids set when
IS_OTHER_REL(input_rel); create_partial_grouping_paths() passes a
relids set when creating UPPERREL_PARTIAL_GROUP_AGG; and
generate_union_paths() and generate_nonunion_paths() in prepunion.c
bubble up the underlying relids. AFAICS, a non-parallel,
non-partitionwise aggregate ends up with an empty relid set, and even
those cases end up with an empty relid set for the topmost grouping
rel, even if they create some other upper rels that do have non-empty
relid sets.

--
Robert Haas
EDB: http://www.enterprisedb.com



Re: plan shape work

От
Tom Lane
Дата:
Robert Haas <robertmhaas@gmail.com> writes:
> On Sun, Sep 14, 2025 at 7:42 PM Tom Lane <tgl@sss.pgh.pa.us> wrote:
>> ... But I'd really argue that from a user's standpoint this
>> information is part of the fundamental plan structure and so it
>> deserves more prominence.  I'd lean to putting it first in the
>> T_Result case, before the "One-Time Filter".  (Thought experiment:
>> if we'd had this EXPLAIN field from day one, where do you think
>> it would have been placed?)

> Yes, I wondered if it should actually look more like this:
> Degenerate Scan on blah
> Degenerate Join on blah, blah, blah
> Degenerate Aggregate
> MinMaxAggregate Result
> So getting rid of Replaces: altogether.

Yeah, that would be pretty tempting if we were working in a green
field.  I think it might be too much change though.  Also, from a
developer's standpoint it's better if what EXPLAIN prints agrees
with what the node types are internally.

>> +               case RESULT_TYPE_UPPER:
>> +                       /* a small white lie */
>> +                       replacement_type = "Aggregate";
>> +                       break;
>>
>> I find this unconvincing: is it really an aggregate?  It doesn't
>> help that this case doesn't seem to be reached anywhere in the
>> regression tests.

> This is the case I know about where that can be reached.

> robert.haas=# explain select 1 as a, 2 as b having false;
>                 QUERY PLAN
> ------------------------------------------
>  Result  (cost=0.00..0.01 rows=1 width=8)
>    One-Time Filter: false
>    Replaces: Aggregate
> (3 rows)

Hmm, okay.  "Replaces: Aggregate" doesn't seem like a great
explanation, but I don't have a better idea offhand.
In any case I'm not sure this result_type value is going to
have a long shelf-life, so arguing about how to spell it
may not be a productive use of time.

I do suggest adding the above as a regression test.

>> Right offhand, I think that RTE_RESULT *always* has the name *RESULT*,
>> so the "typical" bit seems misleading.  Personally I'd drop this
>> para altogether.

> Counterexample:
> robert.haas=# explain verbose select * from pgbench_accounts where 0 = 1;
>                 QUERY PLAN
> ------------------------------------------
>  Result  (cost=0.00..0.00 rows=0 width=0)
>    Output: aid, bid, abalance, filler
>    One-Time Filter: false
>    Replaces: Scan on pgbench_accounts
> (4 rows)

Uh ... this example does not involve an RTE_RESULT does it?
I see a regular RTE_RELATION RTE for pgbench_accounts, which
your code duly prints.

Looking at the code, there are just three places (all in
prepjointree.c) that create RTE_RESULT RTEs.  Two of them
are building the RTE from scratch, and they both do
    rte->eref = makeAlias("*RESULT*", NIL);
However the third one (pull_up_constant_function) is changing
a pulled-up RTE_FUNCTION into RTE_RESULT, and it doesn't do
anything to the RTE's eref.  While that makes your argument
nominally true, I'd be inclined to argue that this was an oversight
and it should have changed the alias/eref fields to look like other
RTE_RESULTs.  (I've not investigated, but I wonder what your
patch prints for such cases.)

Bottom line remains that I don't think the comment para I quoted
is adding any useful info.  The first half of that comment block
was sufficient.

>> In general, I wonder if it'd be better for the callers of
>> make_xxx_result to pass in the result_type to use.

> That was my first thought, but after experimentation I think it sucks,

Hmph.  Okay, if you tried it and it's bad, I'll accept your opinion.

>> I doubt this claim that the relid set will be empty for an upper rel.
>> I think it's more likely that it will include all the rels for the
>> query.

> Upper rels are created by fetch_upper_rel(). The third argument
> becomes the relids set. Most call sites pass that argument as NULL:

Okay, but "most" is not "all".  I suggest that that comment is just
too specific in the first place.  Even if it's 100% correct today,
it's likely to be falsified in future and no one will remember to
update it.  Something like this might have a longer shelf life:

+ * relids identifies the relation for which this Result node is generating the
+ * tuples. When subplan is not NULL, it should be empty: this node is not
+ * generating anything in that case, just acting on tuples generated by the
+ * subplan. Otherwise, it contains the relids of the planner relation that
+ * the Result represents.

            regards, tom lane



Re: plan shape work

От
Robert Haas
Дата:
On Mon, Sep 15, 2025 at 3:43 PM Tom Lane <tgl@sss.pgh.pa.us> wrote:
> > Yes, I wondered if it should actually look more like this:
> > Degenerate Scan on blah
> > Degenerate Join on blah, blah, blah
> > Degenerate Aggregate
> > MinMaxAggregate Result
> > So getting rid of Replaces: altogether.
>
> Yeah, that would be pretty tempting if we were working in a green
> field.  I think it might be too much change though.  Also, from a
> developer's standpoint it's better if what EXPLAIN prints agrees
> with what the node types are internally.

Well, I was just bringing it up because you seemed to think that the
Replaces: line was not necessarily where we would have put it in a
green field. I am not sure about where we would have put it, but I
think in a green field we wouldn't have it at all, and I actually
think the above is worth considering. I think people would get used to
it pretty fast and that it might be more clear than "Result", which
doesn't really mean a whole lot. However, if you or others don't like
that and you want to just move the Replaces line slightly higher in
the output, I mean, I don't mind that, I just don't know that it's
really a material difference.

> I do suggest adding the above as a regression test.

Makes sense.

> Uh ... this example does not involve an RTE_RESULT does it?
> I see a regular RTE_RELATION RTE for pgbench_accounts, which
> your code duly prints.
>
> Looking at the code, there are just three places (all in
> prepjointree.c) that create RTE_RESULT RTEs.  Two of them
> are building the RTE from scratch, and they both do
>         rte->eref = makeAlias("*RESULT*", NIL);
> However the third one (pull_up_constant_function) is changing
> a pulled-up RTE_FUNCTION into RTE_RESULT, and it doesn't do
> anything to the RTE's eref.  While that makes your argument
> nominally true, I'd be inclined to argue that this was an oversight
> and it should have changed the alias/eref fields to look like other
> RTE_RESULTs.  (I've not investigated, but I wonder what your
> patch prints for such cases.)

Will investigate.

> + * relids identifies the relation for which this Result node is generating the
> + * tuples. When subplan is not NULL, it should be empty: this node is not
> + * generating anything in that case, just acting on tuples generated by the
> + * subplan. Otherwise, it contains the relids of the planner relation that
> + * the Result represents.

OK.

--
Robert Haas
EDB: http://www.enterprisedb.com



Re: plan shape work

От
Tom Lane
Дата:
Robert Haas <robertmhaas@gmail.com> writes:
> Well, I was just bringing it up because you seemed to think that the
> Replaces: line was not necessarily where we would have put it in a
> green field. I am not sure about where we would have put it, but I
> think in a green field we wouldn't have it at all, and I actually
> think the above is worth considering. I think people would get used to
> it pretty fast and that it might be more clear than "Result", which
> doesn't really mean a whole lot. However, if you or others don't like
> that and you want to just move the Replaces line slightly higher in
> the output, I mean, I don't mind that, I just don't know that it's
> really a material difference.

Yeah, I'm content with making it a "Replaces" attribute, I'm just
complaining about the ordering.  I'd be the first to agree that
this is purely cosmetic, but in my mind there is a pretty clear
precedence ordering among the attributes that EXPLAIN prints.

            regards, tom lane



Re: plan shape work

От
Robert Haas
Дата:
On Mon, Sep 15, 2025 at 3:43 PM Tom Lane <tgl@sss.pgh.pa.us> wrote:
> anything to the RTE's eref.  While that makes your argument
> nominally true, I'd be inclined to argue that this was an oversight
> and it should have changed the alias/eref fields to look like other
> RTE_RESULTs.  (I've not investigated, but I wonder what your
> patch prints for such cases.)

It just prints "-> Result" and that's it, as in this example:

robert.haas=# create or replace function absolutely_not() returns bool
return false;
CREATE FUNCTION
robert.haas=# explain (costs off) select * from generate_series(1,3) g
full join absolutely_not() n on true;
                QUERY PLAN
------------------------------------------
 Merge Full Join
   ->  Function Scan on generate_series g
   ->  Materialize
         ->  Result
(4 rows)

robert.haas=# explain (costs off, range_table) select * from
generate_series(1,3) g full join absolutely_not() n on true;
                QUERY PLAN
------------------------------------------
 Merge Full Join
   ->  Function Scan on generate_series g
         Scan RTI: 1
   ->  Materialize
         ->  Result
               RTIs: 2
 RTI 1 (function, in-from-clause):
   Alias: g ()
   Eref: g (g)
   WITH ORDINALITY: false
 RTI 2 (result, in-from-clause):
   Alias: n ()
   Eref: n (n)
 RTI 3 (join, in-from-clause):
   Eref: unnamed_join (g, n)
   Join Type: Full
(16 rows)

Here's a new patch set. My main questions are:

1. Did I miss anything you wanted fixed in 0001?

2. Should 0001 be combined with 0002 or kept separate?

3. Do you have a preference between this version of 0003 and the older
revision that just ignored outer-join relids?

--
Robert Haas
EDB: http://www.enterprisedb.com

Вложения

Re: plan shape work

От
Robert Haas
Дата:
CI is unhappy with v6 because:

[14:37:37.742] In function ‘show_result_replacement_info’,
[14:37:37.742]     inlined from ‘ExplainNode’ at explain.c:2240:4:
[14:37:37.742] explain.c:4849:33: error: ‘replacement_type’ may be
used uninitialized [-Werror=maybe-uninitialized]
[14:37:37.742]  4849 |                 char       *s = psprintf("%s on
%s", replacement_type, buf.data);
[14:37:37.742]       |
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[14:37:37.742] explain.c: In function ‘ExplainNode’:
[14:37:37.742] explain.c:4769:21: note: ‘replacement_type’ was declared here
[14:37:37.742]  4769 |         char       *replacement_type;
[14:37:37.742]       |                     ^~~~~~~~~~~~~~~~

So apparently an "enum" over every value of the switch is not good
enough for the value to be assigned.

I'm inclined to change the code like this to fix it:

    char       *replacement_type = "???";

...in the hopes of still producing a warning here if somebody adds
another label to the enum.

--
Robert Haas
EDB: http://www.enterprisedb.com



Re: plan shape work

От
Robert Haas
Дата:
On Tue, Sep 16, 2025 at 11:27 AM Robert Haas <robertmhaas@gmail.com> wrote:
> I'm inclined to change the code like this to fix it:
>
>     char       *replacement_type = "???";
>
> ...in the hopes of still producing a warning here if somebody adds
> another label to the enum.

Done in this version. I've also now gone back and rebased the rest of
the patches as well, so this email includes all 7 patches instead of
just the first 3. To recall, my goal for this CF was to get 1-4
committed.

--
Robert Haas
EDB: http://www.enterprisedb.com

Вложения

Re: plan shape work

От
Junwang Zhao
Дата:
Hi Robert,

On Fri, Sep 19, 2025 at 1:23 AM Robert Haas <robertmhaas@gmail.com> wrote:
>
> On Tue, Sep 16, 2025 at 11:27 AM Robert Haas <robertmhaas@gmail.com> wrote:
> > I'm inclined to change the code like this to fix it:
> >
> >     char       *replacement_type = "???";
> >
> > ...in the hopes of still producing a warning here if somebody adds
> > another label to the enum.
>
> Done in this version. I've also now gone back and rebased the rest of
> the patches as well, so this email includes all 7 patches instead of
> just the first 3. To recall, my goal for this CF was to get 1-4
> committed.
>
> --
> Robert Haas
> EDB: http://www.enterprisedb.com

I have a question about the following changes:

- splan = plan;
  if (IsA(plan, Result))
  {
  Result    *rplan = (Result *) plan;

- if (rplan->plan.lefttree == NULL &&
- rplan->resconstantqual == NULL)
- splan = NULL;
+ gplan->plan.lefttree = NULL;
+ gplan->relids = rplan->relids;
+ gplan->result_type = rplan->result_type;
  }

You set gplan->relids and gplan->result_type, but at the end you
returned &gplan->plan, what's the point of setting these two fields?


--
Regards
Junwang Zhao



Re: plan shape work

От
Junwang Zhao
Дата:
On Sun, Sep 21, 2025 at 5:52 PM Junwang Zhao <zhjwpku@gmail.com> wrote:
>
> Hi Robert,
>
> On Fri, Sep 19, 2025 at 1:23 AM Robert Haas <robertmhaas@gmail.com> wrote:
> >
> > On Tue, Sep 16, 2025 at 11:27 AM Robert Haas <robertmhaas@gmail.com> wrote:
> > > I'm inclined to change the code like this to fix it:
> > >
> > >     char       *replacement_type = "???";
> > >
> > > ...in the hopes of still producing a warning here if somebody adds
> > > another label to the enum.
> >
> > Done in this version. I've also now gone back and rebased the rest of
> > the patches as well, so this email includes all 7 patches instead of
> > just the first 3. To recall, my goal for this CF was to get 1-4
> > committed.
> >
> > --
> > Robert Haas
> > EDB: http://www.enterprisedb.com
>
> I have a question about the following changes:
>
> - splan = plan;
>   if (IsA(plan, Result))
>   {
>   Result    *rplan = (Result *) plan;
>
> - if (rplan->plan.lefttree == NULL &&
> - rplan->resconstantqual == NULL)
> - splan = NULL;
> + gplan->plan.lefttree = NULL;
> + gplan->relids = rplan->relids;
> + gplan->result_type = rplan->result_type;
>   }
>
> You set gplan->relids and gplan->result_type, but at the end you
> returned &gplan->plan, what's the point of setting these two fields?

After further study, this should not be a problem, since the address of
&gplan->plan is the same as gplan, but the code is a little bit confusing
at first glance, I think *return (Plan *) gplan* is easier to understand
but I don't insist ;)

>
>
> --
> Regards
> Junwang Zhao



--
Regards
Junwang Zhao



Re: plan shape work

От
Robert Haas
Дата:
On Sun, Sep 21, 2025 at 6:35 AM Junwang Zhao <zhjwpku@gmail.com> wrote:
> After further study, this should not be a problem, since the address of
> &gplan->plan is the same as gplan, but the code is a little bit confusing
> at first glance, I think *return (Plan *) gplan* is easier to understand
> but I don't insist ;)

Thanks for looking into it. Stylistically, I prefer the style without
the cast, but the effect is the same.

--
Robert Haas
EDB: http://www.enterprisedb.com



Re: plan shape work

От
Tom Lane
Дата:
Robert Haas <robertmhaas@gmail.com> writes:
> Done in this version. I've also now gone back and rebased the rest of
> the patches as well, so this email includes all 7 patches instead of
> just the first 3. To recall, my goal for this CF was to get 1-4
> committed.

I found time finally to look through all of these.  Some notes:

0001: committable, no further comments.

0002: Yeah, this does seem like an improvement.  There is something
faintly weird about output like

          ->  Result
-               Output: i3
+               Output: t3.i3
                Replaces: Scan on t3
                One-Time Filter: false

because the whole point of this construct is that we're *not*
scanning t3.  However, failing to supply the Var prefix is surely
not better.  I like the fact that the output in upper plan
levels is now just like what you'd see with a non-phony t3 scan.
I think you could commit this too.  Merging it with 0001 is
reasonable, but if you'd rather keep them separate that's okay too.

0003: doesn't feel ready for commit.  In the first place, you've used
the "completed an outer join" terminology in the commit message and
a couple of comments, but defined it nowhere.  I think a para or
two in optimizer/README to define "starting" and "completing"
outer joins is essential.  (I'm also still quite bemused by marking
nodes that complete outer joins but not those that start them.)
In the second place, we should not need to add two hundred lines
of new code to createplan.c to accomplish this.  Why not simply
bms_difference the joinrel's relids from the union of the inputs'
relids?  (And no, I do not believe that computing the value two
different ways so you can assert they're the same is adding anything
whatsoever except wasted cycles.)

By and large, I don't believe that it's necessary for 0003 to depend
on 0001 either.  We already have the information available from the
paths' parent RelOptInfos.

0004: commit msg is not very convincing about why this is a good idea.
It really looks like change for the sake of change, so you need to
make a better case for it.  Also, this output seems inconsistent:

  Function Scan on pg_catalog.generate_series x
-   Output: ARRAY(SubPlan 1)
+   Output: ARRAY(array_1)
    Function Call: generate_series(1, 3)
-   SubPlan 1
+   SubPlan array_1

Why isn't it now "ARRAY(SubPlan array_1)"?  The previous notation was
chosen to be not-confusable with a plain function call, but you've
lost that.  Likewise for cases like
-   Group Key: (InitPlan 1).col1
+   Group Key: (minmax_1).col1
which now looks exactly like an ordinary Var.

nitpick: sublinktype_to_string() should return const char *.

0005: I'm even less convinced about there being a need for this.
It's not that hard to see which RTIs are in which subplan,
especially after the other changes in this patchset.

0006: I agree with the need for this, but the details seem messy,
and it's not very clear that the proposed data structure would
be convenient to use.  Do we really need to rely on plan_node_id?
Why haven't you integrated record_elided_node into
clean_up_removed_plan_level?

An idea maybe worth thinking about is that instead of completely
eliding the plan node, we could replace it with a "no-op" plan node
that both EXPLAIN and the executor will look right through.
That node could carry the relid(s) that we lost.  Not sure how
messy this'd be to integrate, though.

0007: not sure about this either.  Why not simply add those
relid(s) to the surviving node's apprelids?  Again, I don't
love the amount of code and data structure that's being added
for a hypothetical use-case.  It's not even clear that this
form of the data structure would be helpful to anyone.

            regards, tom lane



Re: plan shape work

От
Robert Haas
Дата:
On Mon, Sep 22, 2025 at 2:15 PM Tom Lane <tgl@sss.pgh.pa.us> wrote:
> I found time finally to look through all of these.  Some notes:

Thanks. Committed 0001 and 0002 together. I agree with you that 0002
is a bit weird, but I think it is less weird than the status quo ante,
and it seems you agree. I think it's best to regard a Result node that
replaces a scan or join as being a form of scan or join that just so
happens to be able to be optimized to a degree not normally possible.
There's a good argument for calling something like this a dummy
scan/join, as I mentioned before. There's no compelling reason we have
to make that terminology change, as we noted in the earlier
discussion, but it's a useful and not incorrect way of thinking about
it.

> 0003: doesn't feel ready for commit.  In the first place, you've used
> the "completed an outer join" terminology in the commit message and
> a couple of comments, but defined it nowhere.  I think a para or
> two in optimizer/README to define "starting" and "completing"
> outer joins is essential.  (I'm also still quite bemused by marking
> nodes that complete outer joins but not those that start them.)
> In the second place, we should not need to add two hundred lines
> of new code to createplan.c to accomplish this.  Why not simply
> bms_difference the joinrel's relids from the union of the inputs'
> relids?  (And no, I do not believe that computing the value two
> different ways so you can assert they're the same is adding anything
> whatsoever except wasted cycles.)

So, let me just back up a minute here and talk about overall goals.
What I originally wanted to do with this patch is ensure that the
non-RTE_JOIN RTIs from the joinrel are all mentioned in the final
plan. Without the changes to the Result node, that's not the case;
with the changes to the Result node, that is, AFAICT, now the case. It
can be argued that what has been numbered 0003 up to now is not really
necessary at all, since all it does is make it less likely that we
will introduce cases with similar problems to the Result-node case in
the future, and that could be judged unlikely enough to make 0003 not
worth committing. However, it seems like you might be proposing
keeping the patch in some form but throwing out the part of the code
that walks the plan tree to collect RTIs, and that doesn't make a
whole lot of sense to me. I have some confidence that the joinrel's
RTI set will include all of the RTIs from the input rels; what I'm
worried about is whether those RTIs survive into the final Plan.

On the other hand, I'm not sure that I'm interpreting your remarks
correctly. When you say "bms_difference the joinrel's relids from the
union of the inputs' relids" maybe you're specifically talking about
the handling of the RTE_JOIN relids, and I don't care very much how we
account for those. So I guess I need some clarification here as to
what your thinking is.

With regard to your other comments, I'm not opposed to updating the
README, or alternatively, I'm also not opposed to adjusting the
wording of the comments and commit message to avoid that particular
terminology. As far as your parenthetical comment about not marking
outer joins started, I don't actually care what we do, but as I said
before, I don't see an efficient way to identify outer joins started
at a particular level, and it isn't worth adding planner cycles for
information for which I have no concrete need.

> 0004: commit msg is not very convincing about why this is a good idea.
> It really looks like change for the sake of change, so you need to
> make a better case for it.

You're right. That commit message presupposes that the goal is already
understood, instead of explaining it. See the first message on this
thread, in the paragraph that begins "Now let's talk about problem
#2," for the justification. Quoting the most relevant part:

Subqueries sort of have names right
now, at least some of them, but it's an odd system: a CTE subquery,
for example, has the name mentioned by the user, but other kinds of
subplans just get names like "InitPlan 3" or "SubPlan 2". The real
problem, though, is that those names are only assigned after we've
FINISHED planned the subquery. If we begin planning our very first
subquery, it might turn out to be InitPlan 1 or SubPlan 1, or if while
planning it we recurse into some further subquery then *that* subquery
might become InitPlan 1 or SubPlan 1 and OUR subquery might become
InitPlan 2 or SubPlan 2 (or higher, if we find more subqueries and
recurse into them too). Thus, being given some information about how
the user wants, say, SubPlan 2 to be planned is completely useless
because we won't know whether that is us until after we've done the
planning that the user is trying to influence.

I need to figure out some good way of explaining this (hopefully not
too verbosely) in the commit message.

> Also, this output seems inconsistent:
>
>   Function Scan on pg_catalog.generate_series x
> -   Output: ARRAY(SubPlan 1)
> +   Output: ARRAY(array_1)
>     Function Call: generate_series(1, 3)
> -   SubPlan 1
> +   SubPlan array_1
>
> Why isn't it now "ARRAY(SubPlan array_1)"?  The previous notation was
> chosen to be not-confusable with a plain function call, but you've
> lost that.  Likewise for cases like
> -   Group Key: (InitPlan 1).col1
> +   Group Key: (minmax_1).col1
> which now looks exactly like an ordinary Var.

I don't think there's anything keeping me from making that print
InitPlan/SubPlan there. I just thought it looked a little verbose that
way. I thought that the "InitPlan 1" notation was probably due to the
fact that "1" is not a proper name, rather than (as you suggest here)
to avoid confusion with other syntax. Compare CTEs, which are also
subplans, but for which we simply print the CTE's alias name, rather
than "CTE alias_name".

> nitpick: sublinktype_to_string() should return const char *.

This bleeds into a bunch of other things. I'll poke at it and try to
figure something out. I may need help from someone with superior const
skills, but I'll give it a go.

> 0005: I'm even less convinced about there being a need for this.
> It's not that hard to see which RTIs are in which subplan,
> especially after the other changes in this patchset.

Let me just say that everything in this patch set is the result of
experimentation -- trying to write code that attempts to interpret a
Plan tree and discovering problems doing so along the way. I'm not
altogether convinced that patches 0005-0008 attack the problems in the
best possible way and I'm happy to hear suggestions for how to do it
better, but I'm pretty confident that they're all trying to tackle
real problems. The way to think about 0004 and 0005, IMHO, is this:
suppose we look at the final Plan, and we see that RTI 17 was planned
in such-and-such a way. If we want to reproduce that planning decision
in the next cycle -- or change that planning decision in the next
planning cycle -- we need to be able to figure out which subroot RTI
17 came from and what RTI it had within that subroot. With 0004 and
0005 applied, you can figure out that RTI 17 was originally RTI 5
within a subroot that was called expr_1, or whatever, and when we
replan the same query we will assign the name expr_1 to the same
subroot before we begin planning it and RTI 5 within that subroot will
refer to the same thing it did before. This assumes, of course, that
the query is the same and that inlining decisions and so forth haven't
changed and that no objects have been swapped out for objects with the
same name; but the point is that even if none of that has happened we
can't match things up in an automated way without this infrastructure,
or at least I don't know how to do it. If you do, I'm all ears. I
would much rather minimize the amount of information that we have to
store in the Plan tree or PlannedStmt and just have code to do the
necessary interpretation based on the data that's already available,
but I could not see how to make it work.

> 0006: I agree with the need for this, but the details seem messy,
> and it's not very clear that the proposed data structure would
> be convenient to use.  Do we really need to rely on plan_node_id?
> Why haven't you integrated record_elided_node into
> clean_up_removed_plan_level?
>
> An idea maybe worth thinking about is that instead of completely
> eliding the plan node, we could replace it with a "no-op" plan node
> that both EXPLAIN and the executor will look right through.
> That node could carry the relid(s) that we lost.  Not sure how
> messy this'd be to integrate, though.

There's certainly nothing that requires us to use this particular
scheme for identifying the locations where certain plan nodes were
elided. For example, we could put an additional List * in each plan
node and stash a list of elided nodes there rather than indexing by
plan_node_id. The disadvantage of that is simply that it increases the
size of the Plan struct for every node, and most nodes will end up
with an empty list. Indexing by plan_node_id was just my way of
getting the information that I wanted to store out of the tree that
the executor examines. We could potentially also do as you say here
and keep the Plan nodes around and then somehow arrange to ignore them
at explain and execution time, but I don't have a good idea of how we
would do that without adding overhead. In terms of the usability of
the data structure, experimentation has shown it to be serviceable.
There is obviously a risk that with many elided nodes, having to
grovel through the list of nodes looking for a certain plan ID over
and over again could be inefficient -- but large numbers of elided
nodes don't seem all that likely, so maybe it's OK; and I suppose any
code that uses this could always build a hash table if warranted. But
I'm not saying it's a perfect solution.

In terms of why record_elided_node is not integrated into
clean_up_removed_plan_level, that was just a stylistic choice.
clean_up_removed_plan_level() could instead call record_elided_node(),
or record_elided_node() could cease to exist and the logic be inlined
into clean_up_removed_plan_level().

> 0007: not sure about this either.  Why not simply add those
> relid(s) to the surviving node's apprelids?  Again, I don't
> love the amount of code and data structure that's being added
> for a hypothetical use-case.  It's not even clear that this
> form of the data structure would be helpful to anyone.

I'd say that is the patch I'm least sure about. Note that my goal for
this commitfest was to get 0001-0004 committed, partly because I
wasn't too sure whether the later patches might need some adjustment.
My intuition is that flattening the relid sets together loses
important information, but I don't have a test case proving that near
at hand, so maybe it doesn't. However, I'm fairly certain that it is
at least a lot more convenient to have the information in this form.
In general, if we see an Append or MergeAppend node generated from a
RelOptInfo with a single RTI, that's a partition-wise scan of a
partitioned relation, and if we see an Append or MergeAppend node
generated from a RelOptInfo with more than one RTI, that's a
partition-wise join of all those relations. So, very naively, if we
just combine the relid sets for a bunch of partitionwise scans into a
single RTI set, it looks like we've got a partitionwise join, but we
don't. Now, if it's only set operations that result in multiple levels
of Append/MergeAppend nodes getting collapsed, it might be possible to
disentangle what actually happened by partitioning the final set of
RTIs by the subroot from which they originate. I'm not sure that's how
it works, though. At any rate, I agree with you that this is not
adequately motivated at present. Having said that, at ten thousand
feet, the motivation here is to be able to figure out from the final
plan tree where exactly we switched to partitionwise operation --
whether that was done for some joinrel or only when we got down to the
underlying baserel.

--
Robert Haas
EDB: http://www.enterprisedb.com



Re: plan shape work

От
Tom Lane
Дата:
Robert Haas <robertmhaas@gmail.com> writes:
> On Mon, Sep 22, 2025 at 2:15 PM Tom Lane <tgl@sss.pgh.pa.us> wrote:
>> In the second place, we should not need to add two hundred lines
>> of new code to createplan.c to accomplish this.  Why not simply
>> bms_difference the joinrel's relids from the union of the inputs'
>> relids?

> ... On the other hand, I'm not sure that I'm interpreting your remarks
> correctly. When you say "bms_difference the joinrel's relids from the
> union of the inputs' relids" maybe you're specifically talking about
> the handling of the RTE_JOIN relids, and I don't care very much how we
> account for those. So I guess I need some clarification here as to
> what your thinking is.

What I'm saying is that I'd be much happier with 0003 if it looked
about like the attached.  We do not need a heap of mechanism
redundantly proving that the planner is getting these things right
(and potentially containing its own bugs).

> Note that my goal for
> this commitfest was to get 0001-0004 committed, partly because I
> wasn't too sure whether the later patches might need some adjustment.

Fair enough.  I think we can reach agreement on that much pretty quickly.

            regards, tom lane

From 1a3a6162691105ac23522f66b54dba42850e993e Mon Sep 17 00:00:00 2001
From: Tom Lane <tgl@sss.pgh.pa.us>
Date: Tue, 23 Sep 2025 17:18:33 -0400
Subject: [PATCH v8] Ensure that all joinrel RTIs are discoverable from join
 plans.

Every RTI associated with a joinrel appears either on the outer or inner
side of the joinrel or is an outer join completed by the joinrel.
Previously, the RTIs of outer joins cmopleted by the joinrel were not
stored anywhere; now, we store them in a new 'ojrelids' field of the
Join itself, for the benefit of code that wants to study Plan trees.

All of this is intended as infrastructure to make it possible to
reliably determine the chosen join order from the final plan, although
it's not sufficient for that goal of itself, due to further problems
created by setrefs-time processing.
---
 .../expected/pg_overexplain.out               | 40 ++++++++++++++++++-
 contrib/pg_overexplain/pg_overexplain.c       | 21 ++++++++++
 contrib/pg_overexplain/sql/pg_overexplain.sql | 14 ++++++-
 src/backend/optimizer/plan/createplan.c       | 39 ++++++++++++++++--
 src/include/nodes/plannodes.h                 |  2 +
 5 files changed, 109 insertions(+), 7 deletions(-)

diff --git a/contrib/pg_overexplain/expected/pg_overexplain.out b/contrib/pg_overexplain/expected/pg_overexplain.out
index 55d34666d87..57c997e8b32 100644
--- a/contrib/pg_overexplain/expected/pg_overexplain.out
+++ b/contrib/pg_overexplain/expected/pg_overexplain.out
@@ -377,14 +377,15 @@ $$);
 (15 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
--- actually running the query, but try to suppress potentially variable output.
+-- scan so that we can see parameter-related information.
 CREATE INDEX ON vegetables (id);
 ANALYZE vegetables;
 SET enable_hashjoin = false;
 SET enable_material = false;
 SET enable_mergejoin = false;
 SET enable_seqscan = false;
+-- Let's try actually running the query, but try to suppress potentially
+-- variable output.
 SELECT explain_filter($$
 EXPLAIN (BUFFERS OFF, COSTS OFF, SUMMARY OFF, TIMING OFF, ANALYZE, DEBUG)
 SELECT * FROM vegetables v1, vegetables v2 WHERE v1.id = v2.id;
@@ -440,6 +441,41 @@ $$);
    Parse Location: 0 to end
 (47 rows)

+-- Test the RANGE_TABLE otion with a case that involves an outer join.
+SELECT explain_filter($$
+EXPLAIN (RANGE_TABLE, COSTS OFF)
+SELECT * FROM daucus d LEFT JOIN brassica b ON d.id = b.id;
+$$);
+                     explain_filter
+---------------------------------------------------------
+ Nested Loop Left Join
+   Outer Join RTIs: 3
+   ->  Index Scan using daucus_id_idx on daucus d
+         Scan RTI: 1
+   ->  Index Scan using brassica_id_idx on brassica b
+         Index Cond: (id = d.id)
+         Scan RTI: 2
+ RTI 1 (relation, in-from-clause):
+   Alias: d ()
+   Eref: d (id, name, genus)
+   Relation: daucus
+   Relation Kind: relation
+   Relation Lock Mode: AccessShareLock
+   Permission Info Index: 1
+ RTI 2 (relation, in-from-clause):
+   Alias: b ()
+   Eref: b (id, name, genus)
+   Relation: brassica
+   Relation Kind: relation
+   Relation Lock Mode: AccessShareLock
+   Permission Info Index: 2
+ RTI 3 (join, in-from-clause):
+   Eref: unnamed_join (id, name, genus, id, name, genus)
+   Join Type: Left
+ Unprunable RTIs: 1 2
+(25 rows)
+
+-- Restore default settings.
 RESET enable_hashjoin;
 RESET enable_material;
 RESET enable_mergejoin;
diff --git a/contrib/pg_overexplain/pg_overexplain.c b/contrib/pg_overexplain/pg_overexplain.c
index bd70b6d9d5e..92cfd8af2eb 100644
--- a/contrib/pg_overexplain/pg_overexplain.c
+++ b/contrib/pg_overexplain/pg_overexplain.c
@@ -248,6 +248,27 @@ overexplain_per_node_hook(PlanState *planstate, List *ancestors,
                     overexplain_bitmapset("RTIs",
                                           ((Result *) plan)->relids,
                                           es);
+                break;
+
+            case T_MergeJoin:
+            case T_NestLoop:
+            case T_HashJoin:
+                {
+                    Join       *join = (Join *) plan;
+
+                    /*
+                     * 'ojrelids' is only meaningful for non-inner joins, but
+                     * if it somehow ends up set for an inner join, print it
+                     * anyway.
+                     */
+                    if (join->jointype != JOIN_INNER ||
+                        join->ojrelids != NULL)
+                        overexplain_bitmapset("Outer Join RTIs",
+                                              join->ojrelids,
+                                              es);
+                    break;
+                }
+
             default:
                 break;
         }
diff --git a/contrib/pg_overexplain/sql/pg_overexplain.sql b/contrib/pg_overexplain/sql/pg_overexplain.sql
index 42e275ac2f9..53aa9ff788e 100644
--- a/contrib/pg_overexplain/sql/pg_overexplain.sql
+++ b/contrib/pg_overexplain/sql/pg_overexplain.sql
@@ -86,18 +86,28 @@ INSERT INTO vegetables (name, genus)
 $$);

 -- 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
--- actually running the query, but try to suppress potentially variable output.
+-- scan so that we can see parameter-related information.
 CREATE INDEX ON vegetables (id);
 ANALYZE vegetables;
 SET enable_hashjoin = false;
 SET enable_material = false;
 SET enable_mergejoin = false;
 SET enable_seqscan = false;
+
+-- Let's try actually running the query, but try to suppress potentially
+-- variable output.
 SELECT explain_filter($$
 EXPLAIN (BUFFERS OFF, COSTS OFF, SUMMARY OFF, TIMING OFF, ANALYZE, DEBUG)
 SELECT * FROM vegetables v1, vegetables v2 WHERE v1.id = v2.id;
 $$);
+
+-- Test the RANGE_TABLE otion with a case that involves an outer join.
+SELECT explain_filter($$
+EXPLAIN (RANGE_TABLE, COSTS OFF)
+SELECT * FROM daucus d LEFT JOIN brassica b ON d.id = b.id;
+$$);
+
+-- Restore default settings.
 RESET enable_hashjoin;
 RESET enable_material;
 RESET enable_mergejoin;
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index c9dba7ff346..e6bb16ff7c0 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -232,14 +232,18 @@ static BitmapOr *make_bitmap_or(List *bitmapplans);
 static NestLoop *make_nestloop(List *tlist,
                                List *joinclauses, List *otherclauses, List *nestParams,
                                Plan *lefttree, Plan *righttree,
-                               JoinType jointype, bool inner_unique);
+                               JoinType jointype,
+                               Relids ojrelids,
+                               bool inner_unique);
 static HashJoin *make_hashjoin(List *tlist,
                                List *joinclauses, List *otherclauses,
                                List *hashclauses,
                                List *hashoperators, List *hashcollations,
                                List *hashkeys,
                                Plan *lefttree, Plan *righttree,
-                               JoinType jointype, bool inner_unique);
+                               JoinType jointype,
+                               Relids ojrelids,
+                               bool inner_unique);
 static Hash *make_hash(Plan *lefttree,
                        List *hashkeys,
                        Oid skewTable,
@@ -253,7 +257,9 @@ static MergeJoin *make_mergejoin(List *tlist,
                                  bool *mergereversals,
                                  bool *mergenullsfirst,
                                  Plan *lefttree, Plan *righttree,
-                                 JoinType jointype, bool inner_unique,
+                                 JoinType jointype,
+                                 Relids ojrelids,
+                                 bool inner_unique,
                                  bool skip_mark_restore);
 static Sort *make_sort(Plan *lefttree, int numCols,
                        AttrNumber *sortColIdx, Oid *sortOperators,
@@ -4199,6 +4205,7 @@ create_nestloop_plan(PlannerInfo *root,
     Plan       *outer_plan;
     Plan       *inner_plan;
     Relids        outerrelids;
+    Relids        ojrelids;
     List       *tlist = build_path_tlist(root, &best_path->jpath.path);
     List       *joinrestrictclauses = best_path->jpath.joinrestrictinfo;
     List       *joinclauses;
@@ -4265,6 +4272,11 @@ create_nestloop_plan(PlannerInfo *root,
             replace_nestloop_params(root, (Node *) otherclauses);
     }

+    /* Identify any outer joins computed at this level */
+    ojrelids = bms_difference(best_path->jpath.path.parent->relids,
+                              bms_union(best_path->jpath.outerjoinpath->parent->relids,
+                                        best_path->jpath.innerjoinpath->parent->relids));
+
     /*
      * Identify any nestloop parameters that should be supplied by this join
      * node, and remove them from root->curOuterParams.
@@ -4336,6 +4348,7 @@ create_nestloop_plan(PlannerInfo *root,
                               outer_plan,
                               inner_plan,
                               best_path->jpath.jointype,
+                              ojrelids,
                               best_path->jpath.inner_unique);

     copy_generic_path_info(&join_plan->join.plan, &best_path->jpath.path);
@@ -4350,6 +4363,7 @@ create_mergejoin_plan(PlannerInfo *root,
     MergeJoin  *join_plan;
     Plan       *outer_plan;
     Plan       *inner_plan;
+    Relids        ojrelids;
     List       *tlist = build_path_tlist(root, &best_path->jpath.path);
     List       *joinclauses;
     List       *otherclauses;
@@ -4428,6 +4442,11 @@ create_mergejoin_plan(PlannerInfo *root,
     mergeclauses = get_switched_clauses(best_path->path_mergeclauses,
                                         best_path->jpath.outerjoinpath->parent->relids);

+    /* Identify any outer joins computed at this level */
+    ojrelids = bms_difference(best_path->jpath.path.parent->relids,
+                              bms_union(outer_path->parent->relids,
+                                        inner_path->parent->relids));
+
     /*
      * Create explicit sort nodes for the outer and inner paths if necessary.
      */
@@ -4688,6 +4707,7 @@ create_mergejoin_plan(PlannerInfo *root,
                                outer_plan,
                                inner_plan,
                                best_path->jpath.jointype,
+                               ojrelids,
                                best_path->jpath.inner_unique,
                                best_path->skip_mark_restore);

@@ -4705,6 +4725,7 @@ create_hashjoin_plan(PlannerInfo *root,
     Hash       *hash_plan;
     Plan       *outer_plan;
     Plan       *inner_plan;
+    Relids        ojrelids;
     List       *tlist = build_path_tlist(root, &best_path->jpath.path);
     List       *joinclauses;
     List       *otherclauses;
@@ -4853,6 +4874,11 @@ create_hashjoin_plan(PlannerInfo *root,
         hash_plan->rows_total = best_path->inner_rows_total;
     }

+    /* Identify any outer joins computed at this level */
+    ojrelids = bms_difference(best_path->jpath.path.parent->relids,
+                              bms_union(best_path->jpath.outerjoinpath->parent->relids,
+                                        best_path->jpath.innerjoinpath->parent->relids));
+
     join_plan = make_hashjoin(tlist,
                               joinclauses,
                               otherclauses,
@@ -4863,6 +4889,7 @@ create_hashjoin_plan(PlannerInfo *root,
                               outer_plan,
                               (Plan *) hash_plan,
                               best_path->jpath.jointype,
+                              ojrelids,
                               best_path->jpath.inner_unique);

     copy_generic_path_info(&join_plan->join.plan, &best_path->jpath.path);
@@ -5935,6 +5962,7 @@ make_nestloop(List *tlist,
               Plan *lefttree,
               Plan *righttree,
               JoinType jointype,
+              Relids ojrelids,
               bool inner_unique)
 {
     NestLoop   *node = makeNode(NestLoop);
@@ -5947,6 +5975,7 @@ make_nestloop(List *tlist,
     node->join.jointype = jointype;
     node->join.inner_unique = inner_unique;
     node->join.joinqual = joinclauses;
+    node->join.ojrelids = ojrelids;
     node->nestParams = nestParams;

     return node;
@@ -5963,6 +5992,7 @@ make_hashjoin(List *tlist,
               Plan *lefttree,
               Plan *righttree,
               JoinType jointype,
+              Relids ojrelids,
               bool inner_unique)
 {
     HashJoin   *node = makeNode(HashJoin);
@@ -5979,6 +6009,7 @@ make_hashjoin(List *tlist,
     node->join.jointype = jointype;
     node->join.inner_unique = inner_unique;
     node->join.joinqual = joinclauses;
+    node->join.ojrelids = ojrelids;

     return node;
 }
@@ -6018,6 +6049,7 @@ make_mergejoin(List *tlist,
                Plan *lefttree,
                Plan *righttree,
                JoinType jointype,
+               Relids ojrelids,
                bool inner_unique,
                bool skip_mark_restore)
 {
@@ -6037,6 +6069,7 @@ make_mergejoin(List *tlist,
     node->join.jointype = jointype;
     node->join.inner_unique = inner_unique;
     node->join.joinqual = joinclauses;
+    node->join.ojrelids = ojrelids;

     return node;
 }
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index 3d196f5078e..16f3f5a7925 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -938,6 +938,7 @@ typedef struct CustomScan
  * inner_unique each outer tuple can match to no more than one inner tuple
  * joinqual:    qual conditions that came from JOIN/ON or JOIN/USING
  *                (plan.qual contains conditions that came from WHERE)
+ * ojrelids:    outer joins completed at this level
  *
  * When jointype is INNER, joinqual and plan.qual are semantically
  * interchangeable.  For OUTER jointypes, the two are *not* interchangeable;
@@ -962,6 +963,7 @@ typedef struct Join
     bool        inner_unique;
     /* JOIN quals (in addition to plan.qual) */
     List       *joinqual;
+    Bitmapset  *ojrelids;
 } Join;

 /* ----------------
--
2.43.7


Re: plan shape work

От
Robert Haas
Дата:
On Tue, Sep 23, 2025 at 5:27 PM Tom Lane <tgl@sss.pgh.pa.us> wrote:
> What I'm saying is that I'd be much happier with 0003 if it looked
> about like the attached.  We do not need a heap of mechanism
> redundantly proving that the planner is getting these things right
> (and potentially containing its own bugs).

Thanks for the demo. That doesn't actually Assert anything, so the
commit message is a lie, but I get the point, and based on that, I
think what we should do is just drop 0003 altogether for now. I don't
need the ojrelids field for anything currently, and if I or someone
else does later, we can always revisit this idea. Or, if it turns out
that we later introduce more bugs that my version of 0003 would have
caught, we can re-ask the question of whether we want to Assert
something. I don't agree with your judgement that this is an
unreasonable amount of mechanism for what it checks, but I'm entirely
prepared to concede that 0003 is kind of clunky, and I also think that
the rate at which we do new things in the planner is low enough that
it could easily be decades or forever before we have another problem
that this would have caught. Hence, I'm fine with dropping this patch.
Let's call it "some code that Robert found useful for personal
testing" and move on.

> > Note that my goal for
> > this commitfest was to get 0001-0004 committed, partly because I
> > wasn't too sure whether the later patches might need some adjustment.
>
> Fair enough.  I think we can reach agreement on that much pretty quickly.

Cool. Let's focus on 0004 then, and possibly 0005 since it's somewhat
related and you seem to have an idea that there could be a better way
of solving that problem. That's not necessarily to say that 0005 would
get committed this CF, unless we happen to agree vigorously on
something, but if there's a way to work around needing 0005 or if it
needs to be redone in some other form, it would be good to have some
idea around that sooner rather than later.

--
Robert Haas
EDB: http://www.enterprisedb.com



Re: plan shape work

От
Robert Haas
Дата:
On Wed, Sep 24, 2025 at 8:27 AM Robert Haas <robertmhaas@gmail.com> wrote:
> Cool. Let's focus on 0004 then, and possibly 0005 since it's somewhat
> related and you seem to have an idea that there could be a better way
> of solving that problem. That's not necessarily to say that 0005 would
> get committed this CF, unless we happen to agree vigorously on
> something, but if there's a way to work around needing 0005 or if it
> needs to be redone in some other form, it would be good to have some
> idea around that sooner rather than later.

Here's a new patch set. 0004 is now 0001 and similarly all other patch
numbers are -3, since the old 0001 and 0002 were committed together
and the 0003 is abandoned. I made the following changes to
old-0004/new-0001:

- I rewrote the commit message. I'm not really sure this is any
clearer about the motivation for this patch, but I tried. Suggestions
appreciated.

- CI was complaining about a warning from sublinktype_to_string, which
I've tried to suppress by adding a dummy return to the end of the
function.

- You (Tom) complained about the lack of const on
sublinktype_to_string, so this version has been const-ified. The const
bled into the arguments to choose_plan_name() and subquery_planner(),
and into the plan_name structure members within PlannerInfo and
SubPlan. I don't know if this is the right thing to do, so feel free
to set me straight.

- You (Tom) also asked why not print InitPlan/SubPlan wherever we
refer to subplans, so this version restores that behavior. I did that
by putting logic to print the InitPlan or SubPlan prefix in
ruleutils.c, while not including InitPlan or SubPlan in the SubPlan's
plan_name field any more. The reason for this is that the purpose of
the patch set is to assign names before planning, and the decision as
to whether something is an InitPlan or a SubPlan is made after
planning, so keeping "InitPlan" or "SubPlan" in the actual plan name
undermines the whole point of the patch. I would argue that this is
actually better on philosophical grounds, because I think that the
fact that something is an expression rather than an EXISTS clause or
whatever is part of the identify of the resulting object, whereas I
think whether something is an InitPlan or a SubPlan is mostly
interesting as a performance characteristic rather than as a definer
of identity. I don't necessarily expect this position to be accepted
without debate, but I prefer the EXPLAIN output with the patch to the
pre-patch output.

The remaining patches are simply rebased and are only included here in
case we want to discuss how they could be
better/different/unnecessary, especially what's now 0002, rather than
because I'm looking to get something committed right away.

Thanks,

--
Robert Haas
EDB: http://www.enterprisedb.com

Вложения

Re: plan shape work

От
Tom Lane
Дата:
Robert Haas <robertmhaas@gmail.com> writes:
> Here's a new patch set. 0004 is now 0001 and similarly all other patch
> numbers are -3, since the old 0001 and 0002 were committed together
> and the 0003 is abandoned. I made the following changes to
> old-0004/new-0001:

I've not looked at all of these patches, but here's a review of v9-0001.

> - I rewrote the commit message. I'm not really sure this is any
> clearer about the motivation for this patch, but I tried. Suggestions
> appreciated.

It's much better, thanks.

> - You (Tom) complained about the lack of const on
> sublinktype_to_string, so this version has been const-ified. The const
> bled into the arguments to choose_plan_name() and subquery_planner(),
> and into the plan_name structure members within PlannerInfo and
> SubPlan. I don't know if this is the right thing to do, so feel free
> to set me straight.

I don't think so.  We do not have a nice story on marking Node fields
const: it's very unclear for example what consequences that ought to
have for copyObject().  Maybe somebody will tackle that issue someday,
but it's not something to touch casually in a patch with other
objectives.  So I don't think we can make the plan_name fields const.
The best solution I think is to make choose_plan_name() take a const
string and return a non-const one.  The attached v10-0001 is just like
your v9-0001 except for doing the const stuff this way.  I chose to
fix the impedance mismatch within choose_plan_name() by having it
pstrdup when it wants to just return the "name" string, but you could
make a case for holding your nose and just casting away const there.

> - You (Tom) also asked why not print InitPlan/SubPlan wherever we
> refer to subplans, so this version restores that behavior.

Thanks.  I'm good with the output now (modulo the bug described
below).  Someone could potentially argue that this exposes more
of the internals than we really ought to, such as the difference
between expr and multiexpr SubLinks, but I'm okay with that.

Aside from the const issue, something I don't really like at the
coding level is the use of an "allroots" list.  One reason is that
it's partially redundant with the adjacent "subroots" list, but
a bigger one is that we have transient roots that shouldn't be
in there.  An example here is pull_up_simple_subquery: it builds
a clone of the query's PlannerInfo to help it use various
infrastructure along the way to flattening the subquery, but
that clone is not referenced anymore after the function exits.
You were putting that into allroots, which seems to me to be
a fundamental error, even more so because it went in with the
same plan_name as the root it was cloned from.

I think a better idea is to keep a list of just the subplan
names that we've assigned so far.  That has a far clearer
charter, plus it can be updated immediately by choose_plan_name()
instead of relying on the caller to do the right thing later.
I coded this up, and was rather surprised to find that it changed
some regression outputs.  On investigation, that's because
build_minmax_path() was actually doing the wrong thing later:
it was putting the wrong root into allroots, so that "minmax_1"
never became assigned and could be re-used later.

I also observed that SS_process_ctes() was not using
choose_plan_name() but simply assigning the user-written CTE
name.  I believe it's possible to use the same CTE name in
different parts of a query tree, so this fails to achieve
the stated purpose of making the names unique.

I'm still a little bit uncomfortable about whether
it's okay for pull_up_simple_subquery() to just do

+    subroot->plan_name = root->plan_name;

rather than giving some other name to the transient subroot.
I think it's okay because we are not making any meaningful planning
decisions during the life of the subroot, just seeing if we can
transform the subquery into a form that allows it to be pulled up.
But you might think differently.  Perhaps a potential compromise
is to set the transient subroot's plan_name to NULL instead?

Anyway, v10-0002 is a delta patch to use a list of subplan
names instead of "allroots", and there are a couple of trivial
cosmetic changes too.

            regards, tom lane

From 1dae91029098fb60c1dfeb5d713601336d674c18 Mon Sep 17 00:00:00 2001
From: Tom Lane <tgl@sss.pgh.pa.us>
Date: Wed, 24 Sep 2025 16:13:28 -0400
Subject: [PATCH v10 1/2] Assign each subquery a unique name prior to planning
 it.

This is the same as v9-0001 except for rearranging the use of
"const" a bit, so that we're not assuming that we can mark random
Node fields const.
---
 .../postgres_fdw/expected/postgres_fdw.out    |  70 ++--
 src/backend/commands/explain.c                |  15 +-
 src/backend/optimizer/path/allpaths.c         |   6 +-
 src/backend/optimizer/plan/planagg.c          |   6 +
 src/backend/optimizer/plan/planner.c          |  71 ++++-
 src/backend/optimizer/plan/subselect.c        |  84 +++--
 src/backend/optimizer/prep/prepjointree.c     |   4 +
 src/backend/optimizer/prep/prepunion.c        |   5 +-
 src/backend/utils/adt/ruleutils.c             |  33 +-
 src/include/nodes/pathnodes.h                 |   6 +
 src/include/nodes/primnodes.h                 |   1 +
 src/include/optimizer/planner.h               |   4 +
 src/test/regress/expected/aggregates.out      |  58 ++--
 src/test/regress/expected/create_index.out    |  14 +-
 src/test/regress/expected/groupingsets.out    |  94 +++---
 .../regress/expected/incremental_sort.out     |   8 +-
 src/test/regress/expected/inherit.out         |  32 +-
 src/test/regress/expected/insert_conflict.out |   4 +-
 src/test/regress/expected/join.out            |  78 ++---
 src/test/regress/expected/join_hash.out       |  32 +-
 src/test/regress/expected/memoize.out         |   4 +-
 src/test/regress/expected/merge.out           |  12 +-
 src/test/regress/expected/partition_prune.out | 300 +++++++++---------
 src/test/regress/expected/portals.out         |  12 +-
 src/test/regress/expected/predicate.out       |   8 +-
 src/test/regress/expected/returning.out       |  24 +-
 src/test/regress/expected/rowsecurity.out     | 138 ++++----
 src/test/regress/expected/rowtypes.out        |  12 +-
 src/test/regress/expected/select_parallel.out |  56 ++--
 src/test/regress/expected/sqljson.out         |   4 +-
 src/test/regress/expected/subselect.out       | 174 +++++-----
 src/test/regress/expected/updatable_views.out |  52 +--
 src/test/regress/expected/update.out          |   8 +-
 src/test/regress/expected/window.out          |  10 +-
 src/test/regress/expected/with.out            |  20 +-
 35 files changed, 807 insertions(+), 652 deletions(-)

diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 6dc04e916dc..f2f8130af87 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -3175,13 +3175,13 @@ select sum(c1) from ft1 group by c2 having avg(c1 * (random() <= 1)::int) > 100
 -- of an initplan) can be trouble, per bug #15781
 explain (verbose, costs off)
 select exists(select 1 from pg_enum), sum(c1) from ft1;
-                    QUERY PLAN
---------------------------------------------------
+                    QUERY PLAN
+---------------------------------------------------
  Foreign Scan
-   Output: (InitPlan 1).col1, (sum(ft1.c1))
+   Output: (InitPlan exists_1).col1, (sum(ft1.c1))
    Relations: Aggregate on (public.ft1)
    Remote SQL: SELECT sum("C 1") FROM "S 1"."T 1"
-   InitPlan 1
+   InitPlan exists_1
      ->  Seq Scan on pg_catalog.pg_enum
 (6 rows)

@@ -3196,8 +3196,8 @@ select exists(select 1 from pg_enum), sum(c1) from ft1 group by 1;
                     QUERY PLAN
 ---------------------------------------------------
  GroupAggregate
-   Output: (InitPlan 1).col1, sum(ft1.c1)
-   InitPlan 1
+   Output: (InitPlan exists_1).col1, sum(ft1.c1)
+   InitPlan exists_1
      ->  Seq Scan on pg_catalog.pg_enum
    ->  Foreign Scan on public.ft1
          Output: ft1.c1
@@ -3356,15 +3356,15 @@ select distinct (select count(*) filter (where t2.c2 = 6 and t2.c1 < 10) from ft
                                                           QUERY PLAN
       

------------------------------------------------------------------------------------------------------------------------------
  Unique
-   Output: ((SubPlan 1))
+   Output: ((SubPlan expr_1))
    ->  Sort
-         Output: ((SubPlan 1))
-         Sort Key: ((SubPlan 1))
+         Output: ((SubPlan expr_1))
+         Sort Key: ((SubPlan expr_1))
          ->  Foreign Scan
-               Output: (SubPlan 1)
+               Output: (SubPlan expr_1)
                Relations: Aggregate on (public.ft2 t2)
                Remote SQL: SELECT count(*) FILTER (WHERE ((c2 = 6) AND ("C 1" < 10))) FROM "S 1"."T 1" WHERE (((c2 %
6)= 0)) 
-               SubPlan 1
+               SubPlan expr_1
                  ->  Foreign Scan on public.ft1 t1
                        Output: (count(*) FILTER (WHERE ((t2.c2 = 6) AND (t2.c1 < 10))))
                        Remote SQL: SELECT NULL FROM "S 1"."T 1" WHERE (("C 1" = 6))
@@ -3382,14 +3382,14 @@ select distinct (select count(t1.c1) filter (where t2.c2 = 6 and t2.c1 < 10) fro
                                                                       QUERY PLAN
                               

------------------------------------------------------------------------------------------------------------------------------------------------------
  Unique
-   Output: ((SubPlan 1))
+   Output: ((SubPlan expr_1))
    ->  Sort
-         Output: ((SubPlan 1))
-         Sort Key: ((SubPlan 1))
+         Output: ((SubPlan expr_1))
+         Sort Key: ((SubPlan expr_1))
          ->  Foreign Scan on public.ft2 t2
-               Output: (SubPlan 1)
+               Output: (SubPlan expr_1)
                Remote SQL: SELECT "C 1", c2 FROM "S 1"."T 1" WHERE (((c2 % 6) = 0))
-               SubPlan 1
+               SubPlan expr_1
                  ->  Foreign Scan
                        Output: (count(t1.c1) FILTER (WHERE ((t2.c2 = 6) AND (t2.c1 < 10))))
                        Relations: Aggregate on (public.ft1 t1)
@@ -3421,14 +3421,14 @@ select sum(c1) filter (where (c1 / c1) * random() <= 1) from ft1 group by c2 ord

 explain (verbose, costs off)
 select sum(c2) filter (where c2 in (select c2 from ft1 where c2 < 5)) from ft1;
-                                  QUERY PLAN
--------------------------------------------------------------------------------
+                                    QUERY PLAN
+-----------------------------------------------------------------------------------
  Aggregate
-   Output: sum(ft1.c2) FILTER (WHERE (ANY (ft1.c2 = (hashed SubPlan 1).col1)))
+   Output: sum(ft1.c2) FILTER (WHERE (ANY (ft1.c2 = (hashed SubPlan any_1).col1)))
    ->  Foreign Scan on public.ft1
          Output: ft1.c2
          Remote SQL: SELECT c2 FROM "S 1"."T 1"
-   SubPlan 1
+   SubPlan any_1
      ->  Foreign Scan on public.ft1 ft1_1
            Output: ft1_1.c2
            Remote SQL: SELECT c2 FROM "S 1"."T 1" WHERE ((c2 < 5))
@@ -6444,14 +6444,14 @@ UPDATE ft2 AS target SET (c2, c7) = (
         FROM ft2 AS src
         WHERE target.c1 = src.c1
 ) WHERE c1 > 1100;
-                                                      QUERY PLAN


------------------------------------------------------------------------------------------------------------------------
+                                                         QUERY PLAN
      

+-----------------------------------------------------------------------------------------------------------------------------
  Update on public.ft2 target
    Remote SQL: UPDATE "S 1"."T 1" SET c2 = $2, c7 = $3 WHERE ctid = $1
    ->  Foreign Scan on public.ft2 target
-         Output: (SubPlan 1).col1, (SubPlan 1).col2, (rescan SubPlan 1), target.ctid, target.*
+         Output: (SubPlan multiexpr_1).col1, (SubPlan multiexpr_1).col2, (rescan SubPlan multiexpr_1), target.ctid,
target.*
          Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7, c8, ctid FROM "S 1"."T 1" WHERE (("C 1" > 1100)) FOR UPDATE
-         SubPlan 1
+         SubPlan multiexpr_1
            ->  Foreign Scan on public.ft2 src
                  Output: (src.c2 * 10), src.c7
                  Remote SQL: SELECT c2, c7 FROM "S 1"."T 1" WHERE (($1::integer = "C 1"))
@@ -12132,12 +12132,12 @@ INSERT INTO local_tbl VALUES (1505, 505, 'foo');
 ANALYZE local_tbl;
 EXPLAIN (VERBOSE, COSTS OFF)
 SELECT * FROM local_tbl t1 LEFT JOIN (SELECT *, (SELECT count(*) FROM async_pt WHERE a < 3000) FROM async_pt WHERE a <
3000)t2 ON t1.a = t2.a; 
-                                       QUERY PLAN
-----------------------------------------------------------------------------------------
+                                        QUERY PLAN
+------------------------------------------------------------------------------------------
  Nested Loop Left Join
-   Output: t1.a, t1.b, t1.c, async_pt.a, async_pt.b, async_pt.c, ((InitPlan 1).col1)
+   Output: t1.a, t1.b, t1.c, async_pt.a, async_pt.b, async_pt.c, ((InitPlan expr_1).col1)
    Join Filter: (t1.a = async_pt.a)
-   InitPlan 1
+   InitPlan expr_1
      ->  Aggregate
            Output: count(*)
            ->  Append
@@ -12149,10 +12149,10 @@ SELECT * FROM local_tbl t1 LEFT JOIN (SELECT *, (SELECT count(*) FROM async_pt W
          Output: t1.a, t1.b, t1.c
    ->  Append
          ->  Async Foreign Scan on public.async_p1 async_pt_1
-               Output: async_pt_1.a, async_pt_1.b, async_pt_1.c, (InitPlan 1).col1
+               Output: async_pt_1.a, async_pt_1.b, async_pt_1.c, (InitPlan expr_1).col1
                Remote SQL: SELECT a, b, c FROM public.base_tbl1 WHERE ((a < 3000))
          ->  Async Foreign Scan on public.async_p2 async_pt_2
-               Output: async_pt_2.a, async_pt_2.b, async_pt_2.c, (InitPlan 1).col1
+               Output: async_pt_2.a, async_pt_2.b, async_pt_2.c, (InitPlan expr_1).col1
                Remote SQL: SELECT a, b, c FROM public.base_tbl2 WHERE ((a < 3000))
 (20 rows)

@@ -12163,7 +12163,7 @@ SELECT * FROM local_tbl t1 LEFT JOIN (SELECT *, (SELECT count(*) FROM async_pt W
  Nested Loop Left Join (actual rows=1.00 loops=1)
    Join Filter: (t1.a = async_pt.a)
    Rows Removed by Join Filter: 399
-   InitPlan 1
+   InitPlan expr_1
      ->  Aggregate (actual rows=1.00 loops=1)
            ->  Append (actual rows=400.00 loops=1)
                  ->  Async Foreign Scan on async_p1 async_pt_4 (actual rows=200.00 loops=1)
@@ -12386,12 +12386,12 @@ CREATE FOREIGN TABLE foreign_tbl2 () INHERITS (foreign_tbl)
   SERVER loopback OPTIONS (table_name 'base_tbl');
 EXPLAIN (VERBOSE, COSTS OFF)
 SELECT a FROM base_tbl WHERE (a, random() > 0) IN (SELECT a, random() > 0 FROM foreign_tbl);
-                                                  QUERY PLAN
----------------------------------------------------------------------------------------------------------------
+                                                      QUERY PLAN


+-----------------------------------------------------------------------------------------------------------------------
  Seq Scan on public.base_tbl
    Output: base_tbl.a
-   Filter: (ANY ((base_tbl.a = (SubPlan 1).col1) AND ((random() > '0'::double precision) = (SubPlan 1).col2)))
-   SubPlan 1
+   Filter: (ANY ((base_tbl.a = (SubPlan any_1).col1) AND ((random() > '0'::double precision) = (SubPlan any_1).col2)))
+   SubPlan any_1
      ->  Result
            Output: base_tbl.a, (random() > '0'::double precision)
            ->  Append
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 207f86f1d39..06191cd8a85 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -4901,6 +4901,7 @@ ExplainSubPlans(List *plans, List *ancestors,
     {
         SubPlanState *sps = (SubPlanState *) lfirst(lst);
         SubPlan    *sp = sps->subplan;
+        char       *cooked_plan_name;

         /*
          * There can be multiple SubPlan nodes referencing the same physical
@@ -4924,8 +4925,20 @@ ExplainSubPlans(List *plans, List *ancestors,
          */
         ancestors = lcons(sp, ancestors);

+        /*
+         * The plan has a name like exists_1 or rowcompare_2, but here we want
+         * to prefix that with CTE, InitPlan, or SubPlan, as appropriate, for
+         * display purposes.
+         */
+        if (sp->subLinkType == CTE_SUBLINK)
+            cooked_plan_name = psprintf("CTE %s", sp->plan_name);
+        else if (sp->isInitPlan)
+            cooked_plan_name = psprintf("InitPlan %s", sp->plan_name);
+        else
+            cooked_plan_name = psprintf("SubPlan %s", sp->plan_name);
+
         ExplainNode(sps->planstate, ancestors,
-                    relationship, sp->plan_name, es);
+                    relationship, cooked_plan_name, es);

         ancestors = list_delete_first(ancestors);
     }
diff --git a/src/backend/optimizer/path/allpaths.c b/src/backend/optimizer/path/allpaths.c
index 6cc6966b060..593f5361b58 100644
--- a/src/backend/optimizer/path/allpaths.c
+++ b/src/backend/optimizer/path/allpaths.c
@@ -2532,6 +2532,7 @@ set_subquery_pathlist(PlannerInfo *root, RelOptInfo *rel,
     RelOptInfo *sub_final_rel;
     Bitmapset  *run_cond_attrs = NULL;
     ListCell   *lc;
+    char       *plan_name;

     /*
      * Must copy the Query so that planning doesn't mess up the RTE contents
@@ -2674,8 +2675,9 @@ set_subquery_pathlist(PlannerInfo *root, RelOptInfo *rel,
     Assert(root->plan_params == NIL);

     /* Generate a subroot and Paths for the subquery */
-    rel->subroot = subquery_planner(root->glob, subquery, root, false,
-                                    tuple_fraction, NULL);
+    plan_name = choose_plan_name(root->glob, rte->eref->aliasname, false);
+    rel->subroot = subquery_planner(root->glob, subquery, plan_name,
+                                    root, false, tuple_fraction, NULL);

     /* Isolate the params needed by this specific subplan */
     rel->subplan_params = root->plan_params;
diff --git a/src/backend/optimizer/plan/planagg.c b/src/backend/optimizer/plan/planagg.c
index 2ef0bb7f663..0ce35cabaf5 100644
--- a/src/backend/optimizer/plan/planagg.c
+++ b/src/backend/optimizer/plan/planagg.c
@@ -38,6 +38,7 @@
 #include "optimizer/pathnode.h"
 #include "optimizer/paths.h"
 #include "optimizer/planmain.h"
+#include "optimizer/planner.h"
 #include "optimizer/subselect.h"
 #include "optimizer/tlist.h"
 #include "parser/parse_clause.h"
@@ -339,6 +340,8 @@ build_minmax_path(PlannerInfo *root, MinMaxAggInfo *mminfo,
     memcpy(subroot, root, sizeof(PlannerInfo));
     subroot->query_level++;
     subroot->parent_root = root;
+    subroot->plan_name = choose_plan_name(root->glob, "minmax", true);
+
     /* reset subplan-related stuff */
     subroot->plan_params = NIL;
     subroot->outer_params = NULL;
@@ -359,6 +362,9 @@ build_minmax_path(PlannerInfo *root, MinMaxAggInfo *mminfo,
     /* and we haven't created PlaceHolderInfos, either */
     Assert(subroot->placeholder_list == NIL);

+    /* Add this to list of all PlannerInfo objects. */
+    root->glob->allroots = lappend(root->glob->allroots, root);
+
     /*----------
      * Generate modified query of the form
      *        (SELECT col FROM tab
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index 41bd8353430..acd1356a721 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -439,7 +439,8 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions,
     }

     /* primary planning entry point (may recurse for subqueries) */
-    root = subquery_planner(glob, parse, NULL, false, tuple_fraction, NULL);
+    root = subquery_planner(glob, parse, NULL, NULL, false, tuple_fraction,
+                            NULL);

     /* Select best Path and turn it into a Plan */
     final_rel = fetch_upper_rel(root, UPPERREL_FINAL, NULL);
@@ -656,9 +657,9 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions,
  *--------------------
  */
 PlannerInfo *
-subquery_planner(PlannerGlobal *glob, Query *parse, PlannerInfo *parent_root,
-                 bool hasRecursion, double tuple_fraction,
-                 SetOperationStmt *setops)
+subquery_planner(PlannerGlobal *glob, Query *parse, char *plan_name,
+                 PlannerInfo *parent_root, bool hasRecursion,
+                 double tuple_fraction, SetOperationStmt *setops)
 {
     PlannerInfo *root;
     List       *newWithCheckOptions;
@@ -673,6 +674,7 @@ subquery_planner(PlannerGlobal *glob, Query *parse, PlannerInfo *parent_root,
     root->parse = parse;
     root->glob = glob;
     root->query_level = parent_root ? parent_root->query_level + 1 : 1;
+    root->plan_name = plan_name;
     root->parent_root = parent_root;
     root->plan_params = NIL;
     root->outer_params = NULL;
@@ -710,6 +712,9 @@ subquery_planner(PlannerGlobal *glob, Query *parse, PlannerInfo *parent_root,
     root->non_recursive_path = NULL;
     root->partColsUpdated = false;

+    /* Add this to list of all PlannerInfo objects. */
+    root->glob->allroots = lappend(root->glob->allroots, root);
+
     /*
      * Create the top-level join domain.  This won't have valid contents until
      * deconstruct_jointree fills it in, but the node needs to exist before
@@ -8833,3 +8838,61 @@ create_partial_unique_paths(PlannerInfo *root, RelOptInfo *input_rel,
                                   sjinfo, unique_rel);
     }
 }
+
+/*
+ * Choose a unique plan name for subroot.
+ */
+char *
+choose_plan_name(PlannerGlobal *glob, const char *name, bool always_number)
+{
+    unsigned    n;
+
+    /*
+     * If a numeric suffix is not required, then search the list of roots for
+     * a plan with the requested name. If none is found, then we can use the
+     * provided name without modification.
+     */
+    if (!always_number)
+    {
+        bool        found = false;
+
+        foreach_node(PlannerInfo, root, glob->allroots)
+        {
+            if (root->plan_name != NULL &&
+                strcmp(name, root->plan_name) == 0)
+            {
+                found = true;
+                break;
+            }
+        }
+
+        if (!found)
+            return pstrdup(name);
+    }
+
+    /*
+     * If a numeric suffix is required or if the un-suffixed name is already
+     * in use, then loop until we find a positive integer that produces a
+     * novel name.
+     */
+    for (n = 1; true; ++n)
+    {
+        char       *proposed_name = psprintf("%s_%u", name, n);
+        bool        found = false;
+
+        foreach_node(PlannerInfo, root, glob->allroots)
+        {
+            if (root->plan_name != NULL &&
+                strcmp(proposed_name, root->plan_name) == 0)
+            {
+                found = true;
+                break;
+            }
+        }
+
+        if (!found)
+            return proposed_name;
+
+        pfree(proposed_name);
+    }
+}
diff --git a/src/backend/optimizer/plan/subselect.c b/src/backend/optimizer/plan/subselect.c
index fae18548e07..5f8306bc421 100644
--- a/src/backend/optimizer/plan/subselect.c
+++ b/src/backend/optimizer/plan/subselect.c
@@ -103,6 +103,7 @@ static Bitmapset *finalize_plan(PlannerInfo *root,
                                 Bitmapset *scan_params);
 static bool finalize_primnode(Node *node, finalize_primnode_context *context);
 static bool finalize_agg_primnode(Node *node, finalize_primnode_context *context);
+static const char *sublinktype_to_string(SubLinkType subLinkType);


 /*
@@ -172,6 +173,7 @@ make_subplan(PlannerInfo *root, Query *orig_subquery,
     Plan       *plan;
     List       *plan_params;
     Node       *result;
+    const char *sublinkstr = sublinktype_to_string(subLinkType);

     /*
      * Copy the source Query node.  This is a quick and dirty kluge to resolve
@@ -218,8 +220,9 @@ make_subplan(PlannerInfo *root, Query *orig_subquery,
     Assert(root->plan_params == NIL);

     /* Generate Paths for the subquery */
-    subroot = subquery_planner(root->glob, subquery, root, false,
-                               tuple_fraction, NULL);
+    subroot = subquery_planner(root->glob, subquery,
+                               choose_plan_name(root->glob, sublinkstr, true),
+                               root, false, tuple_fraction, NULL);

     /* Isolate the params needed by this specific subplan */
     plan_params = root->plan_params;
@@ -264,9 +267,12 @@ make_subplan(PlannerInfo *root, Query *orig_subquery,
                                          &newtestexpr, ¶mIds);
         if (subquery)
         {
+            char       *plan_name;
+
             /* Generate Paths for the ANY subquery; we'll need all rows */
-            subroot = subquery_planner(root->glob, subquery, root, false, 0.0,
-                                       NULL);
+            plan_name = choose_plan_name(root->glob, sublinkstr, true);
+            subroot = subquery_planner(root->glob, subquery, plan_name,
+                                       root, false, 0.0, NULL);

             /* Isolate the params needed by this specific subplan */
             plan_params = root->plan_params;
@@ -324,15 +330,16 @@ build_subplan(PlannerInfo *root, Plan *plan, Path *path,
 {
     Node       *result;
     SubPlan    *splan;
-    bool        isInitPlan;
     ListCell   *lc;

     /*
-     * Initialize the SubPlan node.  Note plan_id, plan_name, and cost fields
-     * are set further down.
+     * Initialize the SubPlan node.
+     *
+     * Note: plan_id and cost fields are set further down.
      */
     splan = makeNode(SubPlan);
     splan->subLinkType = subLinkType;
+    splan->plan_name = subroot->plan_name;
     splan->testexpr = NULL;
     splan->paramIds = NIL;
     get_first_col_type(plan, &splan->firstColType, &splan->firstColTypmod,
@@ -391,7 +398,7 @@ build_subplan(PlannerInfo *root, Plan *plan, Path *path,
         Assert(testexpr == NULL);
         prm = generate_new_exec_param(root, BOOLOID, -1, InvalidOid);
         splan->setParam = list_make1_int(prm->paramid);
-        isInitPlan = true;
+        splan->isInitPlan = true;
         result = (Node *) prm;
     }
     else if (splan->parParam == NIL && subLinkType == EXPR_SUBLINK)
@@ -406,7 +413,7 @@ build_subplan(PlannerInfo *root, Plan *plan, Path *path,
                                       exprTypmod((Node *) te->expr),
                                       exprCollation((Node *) te->expr));
         splan->setParam = list_make1_int(prm->paramid);
-        isInitPlan = true;
+        splan->isInitPlan = true;
         result = (Node *) prm;
     }
     else if (splan->parParam == NIL && subLinkType == ARRAY_SUBLINK)
@@ -426,7 +433,7 @@ build_subplan(PlannerInfo *root, Plan *plan, Path *path,
                                       exprTypmod((Node *) te->expr),
                                       exprCollation((Node *) te->expr));
         splan->setParam = list_make1_int(prm->paramid);
-        isInitPlan = true;
+        splan->isInitPlan = true;
         result = (Node *) prm;
     }
     else if (splan->parParam == NIL && subLinkType == ROWCOMPARE_SUBLINK)
@@ -442,7 +449,7 @@ build_subplan(PlannerInfo *root, Plan *plan, Path *path,
                                   testexpr,
                                   params);
         splan->setParam = list_copy(splan->paramIds);
-        isInitPlan = true;
+        splan->isInitPlan = true;

         /*
          * The executable expression is returned to become part of the outer
@@ -476,12 +483,12 @@ build_subplan(PlannerInfo *root, Plan *plan, Path *path,
         /* It can be an initplan if there are no parParams. */
         if (splan->parParam == NIL)
         {
-            isInitPlan = true;
+            splan->isInitPlan = true;
             result = (Node *) makeNullConst(RECORDOID, -1, InvalidOid);
         }
         else
         {
-            isInitPlan = false;
+            splan->isInitPlan = false;
             result = (Node *) splan;
         }
     }
@@ -536,7 +543,7 @@ build_subplan(PlannerInfo *root, Plan *plan, Path *path,
             plan = materialize_finished_plan(plan);

         result = (Node *) splan;
-        isInitPlan = false;
+        splan->isInitPlan = false;
     }

     /*
@@ -547,7 +554,7 @@ build_subplan(PlannerInfo *root, Plan *plan, Path *path,
     root->glob->subroots = lappend(root->glob->subroots, subroot);
     splan->plan_id = list_length(root->glob->subplans);

-    if (isInitPlan)
+    if (splan->isInitPlan)
         root->init_plans = lappend(root->init_plans, splan);

     /*
@@ -557,15 +564,10 @@ build_subplan(PlannerInfo *root, Plan *plan, Path *path,
      * there's no point since it won't get re-run without parameter changes
      * anyway.  The input of a hashed subplan doesn't need REWIND either.
      */
-    if (splan->parParam == NIL && !isInitPlan && !splan->useHashTable)
+    if (splan->parParam == NIL && !splan->isInitPlan && !splan->useHashTable)
         root->glob->rewindPlanIDs = bms_add_member(root->glob->rewindPlanIDs,
                                                    splan->plan_id);

-    /* Label the subplan for EXPLAIN purposes */
-    splan->plan_name = psprintf("%s %d",
-                                isInitPlan ? "InitPlan" : "SubPlan",
-                                splan->plan_id);
-
     /* Lastly, fill in the cost estimates for use later */
     cost_subplan(root, splan, plan);

@@ -965,7 +967,7 @@ SS_process_ctes(PlannerInfo *root)
          * Generate Paths for the CTE query.  Always plan for full retrieval
          * --- we don't have enough info to predict otherwise.
          */
-        subroot = subquery_planner(root->glob, subquery, root,
+        subroot = subquery_planner(root->glob, subquery, cte->ctename, root,
                                    cte->cterecursive, 0.0, NULL);

         /*
@@ -989,10 +991,11 @@ SS_process_ctes(PlannerInfo *root)
          * Make a SubPlan node for it.  This is just enough unlike
          * build_subplan that we can't share code.
          *
-         * Note plan_id, plan_name, and cost fields are set further down.
+         * Note: plan_id and cost fields are set further down.
          */
         splan = makeNode(SubPlan);
         splan->subLinkType = CTE_SUBLINK;
+        splan->plan_name = subroot->plan_name;
         splan->testexpr = NULL;
         splan->paramIds = NIL;
         get_first_col_type(plan, &splan->firstColType, &splan->firstColTypmod,
@@ -1039,9 +1042,6 @@ SS_process_ctes(PlannerInfo *root)

         root->cte_plan_ids = lappend_int(root->cte_plan_ids, splan->plan_id);

-        /* Label the subplan for EXPLAIN purposes */
-        splan->plan_name = psprintf("CTE %s", cte->ctename);
-
         /* Lastly, fill in the cost estimates for use later */
         cost_subplan(root, splan, plan);
     }
@@ -3185,7 +3185,8 @@ SS_make_initplan_from_plan(PlannerInfo *root,
     node = makeNode(SubPlan);
     node->subLinkType = EXPR_SUBLINK;
     node->plan_id = list_length(root->glob->subplans);
-    node->plan_name = psprintf("InitPlan %d", node->plan_id);
+    node->plan_name = subroot->plan_name;
+    node->isInitPlan = true;
     get_first_col_type(plan, &node->firstColType, &node->firstColTypmod,
                        &node->firstColCollation);
     node->parallel_safe = plan->parallel_safe;
@@ -3201,3 +3202,32 @@ SS_make_initplan_from_plan(PlannerInfo *root,
     /* Set costs of SubPlan using info from the plan tree */
     cost_subplan(subroot, node, plan);
 }
+
+/*
+ * Get a string equivalent of a given subLinkType.
+ */
+static const char *
+sublinktype_to_string(SubLinkType subLinkType)
+{
+    switch (subLinkType)
+    {
+        case EXISTS_SUBLINK:
+            return "exists";
+        case ALL_SUBLINK:
+            return "all";
+        case ANY_SUBLINK:
+            return "any";
+        case ROWCOMPARE_SUBLINK:
+            return "rowcompare";
+        case EXPR_SUBLINK:
+            return "expr";
+        case MULTIEXPR_SUBLINK:
+            return "multiexpr";
+        case ARRAY_SUBLINK:
+            return "array";
+        case CTE_SUBLINK:
+            return "cte";
+    }
+    Assert(false);
+    return "???";
+}
diff --git a/src/backend/optimizer/prep/prepjointree.c b/src/backend/optimizer/prep/prepjointree.c
index 35e8d3c183b..2ec13637d16 100644
--- a/src/backend/optimizer/prep/prepjointree.c
+++ b/src/backend/optimizer/prep/prepjointree.c
@@ -1356,6 +1356,7 @@ pull_up_simple_subquery(PlannerInfo *root, Node *jtnode, RangeTblEntry *rte,
     subroot->parse = subquery;
     subroot->glob = root->glob;
     subroot->query_level = root->query_level;
+    subroot->plan_name = root->plan_name;
     subroot->parent_root = root->parent_root;
     subroot->plan_params = NIL;
     subroot->outer_params = NULL;
@@ -1387,6 +1388,9 @@ pull_up_simple_subquery(PlannerInfo *root, Node *jtnode, RangeTblEntry *rte,
     subroot->non_recursive_path = NULL;
     /* We don't currently need a top JoinDomain for the subroot */

+    /* Add new subroot to master list of PlannerInfo objects. */
+    root->glob->allroots = lappend(root->glob->allroots, subroot);
+
     /* No CTEs to worry about */
     Assert(subquery->cteList == NIL);

diff --git a/src/backend/optimizer/prep/prepunion.c b/src/backend/optimizer/prep/prepunion.c
index 28a4ae64440..d55eb39e552 100644
--- a/src/backend/optimizer/prep/prepunion.c
+++ b/src/backend/optimizer/prep/prepunion.c
@@ -228,6 +228,7 @@ recurse_set_operations(Node *setOp, PlannerInfo *root,
         PlannerInfo *subroot;
         List       *tlist;
         bool        trivial_tlist;
+        char       *plan_name;

         Assert(subquery != NULL);

@@ -242,7 +243,9 @@ recurse_set_operations(Node *setOp, PlannerInfo *root,
          * parentOp, pass that down to encourage subquery_planner to consider
          * suitably-sorted Paths.
          */
-        subroot = rel->subroot = subquery_planner(root->glob, subquery, root,
+        plan_name = choose_plan_name(root->glob, "setop", true);
+        subroot = rel->subroot = subquery_planner(root->glob, subquery,
+                                                  plan_name, root,
                                                   false, root->tuple_fraction,
                                                   parentOp);

diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 0408a95941d..277a4ffabbc 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -8750,8 +8750,16 @@ get_parameter(Param *param, deparse_context *context)
     subplan = find_param_generator(param, context, &column);
     if (subplan)
     {
-        appendStringInfo(context->buf, "(%s%s).col%d",
+        const char *nameprefix;
+
+        if (subplan->isInitPlan)
+            nameprefix = "InitPlan ";
+        else
+            nameprefix = "SubPlan ";
+
+        appendStringInfo(context->buf, "(%s%s%s).col%d",
                          subplan->useHashTable ? "hashed " : "",
+                         nameprefix,
                          subplan->plan_name, column + 1);

         return;
@@ -9588,11 +9596,19 @@ get_rule_expr(Node *node, deparse_context *context,
                 }
                 else
                 {
+                    const char *nameprefix;
+
                     /* No referencing Params, so show the SubPlan's name */
+                    if (subplan->isInitPlan)
+                        nameprefix = "InitPlan ";
+                    else
+                        nameprefix = "SubPlan ";
                     if (subplan->useHashTable)
-                        appendStringInfo(buf, "hashed %s)", subplan->plan_name);
+                        appendStringInfo(buf, "hashed %s%s)",
+                                         nameprefix, subplan->plan_name);
                     else
-                        appendStringInfo(buf, "%s)", subplan->plan_name);
+                        appendStringInfo(buf, "%s%s)",
+                                         nameprefix, subplan->plan_name);
                 }
             }
             break;
@@ -9612,11 +9628,18 @@ get_rule_expr(Node *node, deparse_context *context,
                 foreach(lc, asplan->subplans)
                 {
                     SubPlan    *splan = lfirst_node(SubPlan, lc);
+                    const char *nameprefix;

+                    if (splan->isInitPlan)
+                        nameprefix = "InitPlan ";
+                    else
+                        nameprefix = "SubPlan ";
                     if (splan->useHashTable)
-                        appendStringInfo(buf, "hashed %s", splan->plan_name);
+                        appendStringInfo(buf, "hashed %s%s", nameprefix,
+                                         splan->plan_name);
                     else
-                        appendStringInfoString(buf, splan->plan_name);
+                        appendStringInfo(buf, "%s%s", nameprefix,
+                                         splan->plan_name);
                     if (lnext(asplan->subplans, lc))
                         appendStringInfoString(buf, " or ");
                 }
diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h
index b12a2508d8c..a341b01a1e1 100644
--- a/src/include/nodes/pathnodes.h
+++ b/src/include/nodes/pathnodes.h
@@ -110,6 +110,9 @@ typedef struct PlannerGlobal
     /* PlannerInfos for SubPlan nodes */
     List       *subroots pg_node_attr(read_write_ignore);

+    /* every PlannerInfo regardless of whether it's an InitPlan/SubPlan */
+    List       *allroots pg_node_attr(read_write_ignore);
+
     /* indices of subplans that require REWIND */
     Bitmapset  *rewindPlanIDs;

@@ -228,6 +231,9 @@ struct PlannerInfo
     /* NULL at outermost Query */
     PlannerInfo *parent_root pg_node_attr(read_write_ignore);

+    /* Name for EXPLAIN and debugging purposes */
+    char       *plan_name;
+
     /*
      * plan_params contains the expressions that this query level needs to
      * make available to a lower query level that is currently being planned.
diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h
index 6dfca3cb35b..1e84321a478 100644
--- a/src/include/nodes/primnodes.h
+++ b/src/include/nodes/primnodes.h
@@ -1095,6 +1095,7 @@ typedef struct SubPlan
     Oid            firstColCollation;    /* Collation of first column of subplan
                                      * result */
     /* Information about execution strategy: */
+    bool        isInitPlan;        /* true if it's an InitPlan */
     bool        useHashTable;    /* true to store subselect output in a hash
                                  * table (implies we are doing "IN") */
     bool        unknownEqFalse; /* true if it's okay to return FALSE when the
diff --git a/src/include/optimizer/planner.h b/src/include/optimizer/planner.h
index f220e9a270d..1bbef0018d5 100644
--- a/src/include/optimizer/planner.h
+++ b/src/include/optimizer/planner.h
@@ -43,6 +43,7 @@ extern PlannedStmt *standard_planner(Query *parse, const char *query_string,
                                      ParamListInfo boundParams);

 extern PlannerInfo *subquery_planner(PlannerGlobal *glob, Query *parse,
+                                     char *plan_name,
                                      PlannerInfo *parent_root,
                                      bool hasRecursion, double tuple_fraction,
                                      SetOperationStmt *setops);
@@ -62,4 +63,7 @@ extern Expr *preprocess_phv_expression(PlannerInfo *root, Expr *expr);
 extern RelOptInfo *create_unique_paths(PlannerInfo *root, RelOptInfo *rel,
                                        SpecialJoinInfo *sjinfo);

+extern char *choose_plan_name(PlannerGlobal *glob, const char *name,
+                              bool always_number);
+
 #endif                            /* PLANNER_H */
diff --git a/src/test/regress/expected/aggregates.out b/src/test/regress/expected/aggregates.out
index 1f24f6ffd1f..a9503e810c5 100644
--- a/src/test/regress/expected/aggregates.out
+++ b/src/test/regress/expected/aggregates.out
@@ -782,9 +782,9 @@ select array(select sum(x+y) s
                             QUERY PLAN
 -------------------------------------------------------------------
  Function Scan on pg_catalog.generate_series x
-   Output: ARRAY(SubPlan 1)
+   Output: ARRAY(SubPlan array_1)
    Function Call: generate_series(1, 3)
-   SubPlan 1
+   SubPlan array_1
      ->  Sort
            Output: (sum((x.x + y.y))), y.y
            Sort Key: (sum((x.x + y.y)))
@@ -960,7 +960,7 @@ explain (costs off)
 ------------------------------------------------------------
  Result
    Replaces: MinMaxAggregate
-   InitPlan 1
+   InitPlan minmax_1
      ->  Limit
            ->  Index Only Scan using tenk1_unique1 on tenk1
                  Index Cond: (unique1 IS NOT NULL)
@@ -978,7 +978,7 @@ explain (costs off)
 ---------------------------------------------------------------------
  Result
    Replaces: MinMaxAggregate
-   InitPlan 1
+   InitPlan minmax_1
      ->  Limit
            ->  Index Only Scan Backward using tenk1_unique1 on tenk1
                  Index Cond: (unique1 IS NOT NULL)
@@ -996,7 +996,7 @@ explain (costs off)
 ------------------------------------------------------------------------
  Result
    Replaces: MinMaxAggregate
-   InitPlan 1
+   InitPlan minmax_1
      ->  Limit
            ->  Index Only Scan Backward using tenk1_unique1 on tenk1
                  Index Cond: ((unique1 IS NOT NULL) AND (unique1 < 42))
@@ -1014,7 +1014,7 @@ explain (costs off)
 ------------------------------------------------------------------------
  Result
    Replaces: MinMaxAggregate
-   InitPlan 1
+   InitPlan minmax_1
      ->  Limit
            ->  Index Only Scan Backward using tenk1_unique1 on tenk1
                  Index Cond: ((unique1 IS NOT NULL) AND (unique1 > 42))
@@ -1038,7 +1038,7 @@ explain (costs off)
 ---------------------------------------------------------------------------
  Result
    Replaces: MinMaxAggregate
-   InitPlan 1
+   InitPlan minmax_1
      ->  Limit
            ->  Index Only Scan Backward using tenk1_unique1 on tenk1
                  Index Cond: ((unique1 IS NOT NULL) AND (unique1 > 42000))
@@ -1058,7 +1058,7 @@ explain (costs off)
 ----------------------------------------------------------------------------
  Result
    Replaces: MinMaxAggregate
-   InitPlan 1
+   InitPlan minmax_1
      ->  Limit
            ->  Index Only Scan Backward using tenk1_thous_tenthous on tenk1
                  Index Cond: ((thousand = 33) AND (tenthous IS NOT NULL))
@@ -1076,7 +1076,7 @@ explain (costs off)
 --------------------------------------------------------------------------
  Result
    Replaces: MinMaxAggregate
-   InitPlan 1
+   InitPlan minmax_1
      ->  Limit
            ->  Index Only Scan using tenk1_thous_tenthous on tenk1
                  Index Cond: ((thousand = 33) AND (tenthous IS NOT NULL))
@@ -1095,10 +1095,10 @@ explain (costs off)
                                        QUERY PLAN
 -----------------------------------------------------------------------------------------
  Seq Scan on int4_tbl
-   SubPlan 2
+   SubPlan expr_1
      ->  Result
            Replaces: MinMaxAggregate
-           InitPlan 1
+           InitPlan minmax_1
              ->  Limit
                    ->  Index Only Scan using tenk1_unique1 on tenk1
                          Index Cond: ((unique1 IS NOT NULL) AND (unique1 > int4_tbl.f1))
@@ -1121,8 +1121,8 @@ explain (costs off)
                              QUERY PLAN
 ---------------------------------------------------------------------
  HashAggregate
-   Group Key: (InitPlan 1).col1
-   InitPlan 1
+   Group Key: (InitPlan minmax_1).col1
+   InitPlan minmax_1
      ->  Limit
            ->  Index Only Scan Backward using tenk1_unique2 on tenk1
                  Index Cond: (unique2 IS NOT NULL)
@@ -1141,8 +1141,8 @@ explain (costs off)
                              QUERY PLAN
 ---------------------------------------------------------------------
  Sort
-   Sort Key: ((InitPlan 1).col1)
-   InitPlan 1
+   Sort Key: ((InitPlan minmax_1).col1)
+   InitPlan minmax_1
      ->  Limit
            ->  Index Only Scan Backward using tenk1_unique2 on tenk1
                  Index Cond: (unique2 IS NOT NULL)
@@ -1161,8 +1161,8 @@ explain (costs off)
                              QUERY PLAN
 ---------------------------------------------------------------------
  Sort
-   Sort Key: ((InitPlan 1).col1)
-   InitPlan 1
+   Sort Key: ((InitPlan minmax_1).col1)
+   InitPlan minmax_1
      ->  Limit
            ->  Index Only Scan Backward using tenk1_unique2 on tenk1
                  Index Cond: (unique2 IS NOT NULL)
@@ -1181,8 +1181,8 @@ explain (costs off)
                              QUERY PLAN
 ---------------------------------------------------------------------
  Sort
-   Sort Key: (((InitPlan 1).col1 + 1))
-   InitPlan 1
+   Sort Key: (((InitPlan minmax_1).col1 + 1))
+   InitPlan minmax_1
      ->  Limit
            ->  Index Only Scan Backward using tenk1_unique2 on tenk1
                  Index Cond: (unique2 IS NOT NULL)
@@ -1202,7 +1202,7 @@ explain (costs off)
 ---------------------------------------------------------------------
  Sort
    Sort Key: (generate_series(1, 3)) DESC
-   InitPlan 1
+   InitPlan minmax_1
      ->  Limit
            ->  Index Only Scan Backward using tenk1_unique2 on tenk1
                  Index Cond: (unique2 IS NOT NULL)
@@ -1226,7 +1226,7 @@ explain (costs off)
 ----------------------------------------------------
  Result
    Replaces: MinMaxAggregate
-   InitPlan 1
+   InitPlan minmax_1
      ->  Limit
            ->  Result
                  One-Time Filter: (100 IS NOT NULL)
@@ -1258,7 +1258,7 @@ explain (costs off)
 ---------------------------------------------------------------------------------------------
  Result
    Replaces: MinMaxAggregate
-   InitPlan 1
+   InitPlan minmax_1
      ->  Limit
            ->  Merge Append
                  Sort Key: minmaxtest.f1
@@ -1269,7 +1269,7 @@ explain (costs off)
                  ->  Index Only Scan Backward using minmaxtest2i on minmaxtest2 minmaxtest_3
                        Index Cond: (f1 IS NOT NULL)
                  ->  Index Only Scan using minmaxtest3i on minmaxtest3 minmaxtest_4
-   InitPlan 2
+   InitPlan minmax_1
      ->  Limit
            ->  Merge Append
                  Sort Key: minmaxtest_5.f1 DESC
@@ -1294,7 +1294,7 @@ explain (costs off)
                                          QUERY PLAN
 ---------------------------------------------------------------------------------------------
  Unique
-   InitPlan 1
+   InitPlan minmax_1
      ->  Limit
            ->  Merge Append
                  Sort Key: minmaxtest.f1
@@ -1305,7 +1305,7 @@ explain (costs off)
                  ->  Index Only Scan Backward using minmaxtest2i on minmaxtest2 minmaxtest_3
                        Index Cond: (f1 IS NOT NULL)
                  ->  Index Only Scan using minmaxtest3i on minmaxtest3 minmaxtest_4
-   InitPlan 2
+   InitPlan minmax_1
      ->  Limit
            ->  Merge Append
                  Sort Key: minmaxtest_5.f1 DESC
@@ -1317,7 +1317,7 @@ explain (costs off)
                        Index Cond: (f1 IS NOT NULL)
                  ->  Index Only Scan Backward using minmaxtest3i on minmaxtest3 minmaxtest_9
    ->  Sort
-         Sort Key: ((InitPlan 1).col1), ((InitPlan 2).col1)
+         Sort Key: ((InitPlan minmax_1).col1), ((InitPlan minmax_1).col1)
          ->  Result
                Replaces: MinMaxAggregate
 (27 rows)
@@ -1342,10 +1342,10 @@ explain (costs off)
                              QUERY PLAN
 ---------------------------------------------------------------------
  Seq Scan on int4_tbl t0
-   SubPlan 2
+   SubPlan expr_1
      ->  HashAggregate
-           Group Key: (InitPlan 1).col1
-           InitPlan 1
+           Group Key: (InitPlan minmax_1).col1
+           InitPlan minmax_1
              ->  Limit
                    ->  Seq Scan on int4_tbl t1
                          Filter: ((f1 IS NOT NULL) AND (f1 = t0.f1))
diff --git a/src/test/regress/expected/create_index.out b/src/test/regress/expected/create_index.out
index 98e68e972be..c743fc769cb 100644
--- a/src/test/regress/expected/create_index.out
+++ b/src/test/regress/expected/create_index.out
@@ -593,7 +593,7 @@ SELECT point(x,x), (SELECT f1 FROM gpolygon_tbl ORDER BY f1 <-> point(x,x) LIMIT
                                          QUERY PLAN
 --------------------------------------------------------------------------------------------
  Function Scan on generate_series x
-   SubPlan 1
+   SubPlan expr_1
      ->  Limit
            ->  Index Scan using ggpolygonind on gpolygon_tbl
                  Order By: (f1 <-> point((x.x)::double precision, (x.x)::double precision))
@@ -1908,11 +1908,11 @@ SELECT * FROM tenk1
 EXPLAIN (COSTS OFF)
 SELECT * FROM tenk1
   WHERE thousand = 42 AND (tenthous = 1 OR tenthous = (SELECT 1 + 2) OR tenthous = 42);
-                                       QUERY PLAN
-----------------------------------------------------------------------------------------
+                                         QUERY PLAN
+---------------------------------------------------------------------------------------------
  Index Scan using tenk1_thous_tenthous on tenk1
-   Index Cond: ((thousand = 42) AND (tenthous = ANY (ARRAY[1, (InitPlan 1).col1, 42])))
-   InitPlan 1
+   Index Cond: ((thousand = 42) AND (tenthous = ANY (ARRAY[1, (InitPlan expr_1).col1, 42])))
+   InitPlan expr_1
      ->  Result
 (4 rows)

@@ -2043,8 +2043,8 @@ SELECT count(*) FROM tenk1 t1
 ----------------------------------------------------------------------------
  Aggregate
    ->  Index Only Scan using tenk1_thous_tenthous on tenk1 t1
-         Filter: ((thousand = 42) OR (thousand = (SubPlan 1)))
-         SubPlan 1
+         Filter: ((thousand = 42) OR (thousand = (SubPlan expr_1)))
+         SubPlan expr_1
            ->  Limit
                  ->  Index Only Scan using tenk1_thous_tenthous on tenk1 t2
                        Index Cond: (thousand = (t1.tenthous + 1))
diff --git a/src/test/regress/expected/groupingsets.out b/src/test/regress/expected/groupingsets.out
index 210bbe307a7..991121545c5 100644
--- a/src/test/regress/expected/groupingsets.out
+++ b/src/test/regress/expected/groupingsets.out
@@ -504,17 +504,17 @@ select grouping(ss.x)
 from int8_tbl i1
 cross join lateral (select (select i1.q1) as x) ss
 group by ss.x;
-                   QUERY PLAN
-------------------------------------------------
+                        QUERY PLAN
+----------------------------------------------------------
  GroupAggregate
-   Output: GROUPING((SubPlan 1)), ((SubPlan 2))
-   Group Key: ((SubPlan 2))
+   Output: GROUPING((SubPlan expr_1)), ((SubPlan expr_2))
+   Group Key: ((SubPlan expr_2))
    ->  Sort
-         Output: ((SubPlan 2)), i1.q1
-         Sort Key: ((SubPlan 2))
+         Output: ((SubPlan expr_2)), i1.q1
+         Sort Key: ((SubPlan expr_2))
          ->  Seq Scan on public.int8_tbl i1
-               Output: (SubPlan 2), i1.q1
-               SubPlan 2
+               Output: (SubPlan expr_2), i1.q1
+               SubPlan expr_2
                  ->  Result
                        Output: i1.q1
 (11 rows)
@@ -534,22 +534,22 @@ select (select grouping(ss.x))
 from int8_tbl i1
 cross join lateral (select (select i1.q1) as x) ss
 group by ss.x;
-                 QUERY PLAN
---------------------------------------------
+                   QUERY PLAN
+------------------------------------------------
  GroupAggregate
-   Output: (SubPlan 2), ((SubPlan 3))
-   Group Key: ((SubPlan 3))
+   Output: (SubPlan expr_1), ((SubPlan expr_3))
+   Group Key: ((SubPlan expr_3))
    ->  Sort
-         Output: ((SubPlan 3)), i1.q1
-         Sort Key: ((SubPlan 3))
+         Output: ((SubPlan expr_3)), i1.q1
+         Sort Key: ((SubPlan expr_3))
          ->  Seq Scan on public.int8_tbl i1
-               Output: (SubPlan 3), i1.q1
-               SubPlan 3
+               Output: (SubPlan expr_3), i1.q1
+               SubPlan expr_3
                  ->  Result
                        Output: i1.q1
-   SubPlan 2
+   SubPlan expr_1
      ->  Result
-           Output: GROUPING((SubPlan 1))
+           Output: GROUPING((SubPlan expr_2))
 (14 rows)

 select (select grouping(ss.x))
@@ -592,7 +592,7 @@ explain (costs off)
 ------------------------------------------------------------
  Result
    Replaces: MinMaxAggregate
-   InitPlan 1
+   InitPlan minmax_1
      ->  Limit
            ->  Index Only Scan using tenk1_unique1 on tenk1
                  Index Cond: (unique1 IS NOT NULL)
@@ -881,7 +881,7 @@ explain (costs off)
  Sort
    Sort Key: "*VALUES*".column1
    ->  Values Scan on "*VALUES*"
-         SubPlan 1
+         SubPlan expr_1
            ->  Aggregate
                  Group Key: ()
                  Filter: "*VALUES*".column1
@@ -2169,17 +2169,17 @@ order by a, b, c;
 -- test handling of outer GroupingFunc within subqueries
 explain (costs off)
 select (select grouping(v1)) from (values ((select 1))) v(v1) group by cube(v1);
-          QUERY PLAN
--------------------------------
+             QUERY PLAN
+------------------------------------
  MixedAggregate
-   Hash Key: (InitPlan 3).col1
+   Hash Key: (InitPlan expr_3).col1
    Group Key: ()
-   InitPlan 1
+   InitPlan expr_2
      ->  Result
-   InitPlan 3
+   InitPlan expr_3
      ->  Result
    ->  Result
-   SubPlan 2
+   SubPlan expr_1
      ->  Result
 (10 rows)

@@ -2192,15 +2192,15 @@ select (select grouping(v1)) from (values ((select 1))) v(v1) group by cube(v1);

 explain (costs off)
 select (select grouping(v1)) from (values ((select 1))) v(v1) group by v1;
-   QUERY PLAN
-----------------
+    QUERY PLAN
+-------------------
  GroupAggregate
-   InitPlan 1
+   InitPlan expr_2
      ->  Result
-   InitPlan 3
+   InitPlan expr_3
      ->  Result
    ->  Result
-   SubPlan 2
+   SubPlan expr_1
      ->  Result
 (8 rows)

@@ -2222,18 +2222,18 @@ order by case when grouping((select t1.v from gstest5 t2 where id = t1.id)) = 0
               then (select t1.v from gstest5 t2 where id = t1.id)
               else null end
          nulls first;
-                                                                 QUERY PLAN
                      

----------------------------------------------------------------------------------------------------------------------------------------------
+                                                                           QUERY PLAN
                                          

+-----------------------------------------------------------------------------------------------------------------------------------------------------------------
  Sort
-   Output: (GROUPING((SubPlan 1))), ((SubPlan 3)), (CASE WHEN (GROUPING((SubPlan 2)) = 0) THEN ((SubPlan 3)) ELSE
NULL::integerEND), t1.v 
-   Sort Key: (CASE WHEN (GROUPING((SubPlan 2)) = 0) THEN ((SubPlan 3)) ELSE NULL::integer END) NULLS FIRST
+   Output: (GROUPING((SubPlan expr_1))), ((SubPlan expr_3)), (CASE WHEN (GROUPING((SubPlan expr_2)) = 0) THEN
((SubPlanexpr_3)) ELSE NULL::integer END), t1.v 
+   Sort Key: (CASE WHEN (GROUPING((SubPlan expr_2)) = 0) THEN ((SubPlan expr_3)) ELSE NULL::integer END) NULLS FIRST
    ->  HashAggregate
-         Output: GROUPING((SubPlan 1)), ((SubPlan 3)), CASE WHEN (GROUPING((SubPlan 2)) = 0) THEN ((SubPlan 3)) ELSE
NULL::integerEND, t1.v 
+         Output: GROUPING((SubPlan expr_1)), ((SubPlan expr_3)), CASE WHEN (GROUPING((SubPlan expr_2)) = 0) THEN
((SubPlanexpr_3)) ELSE NULL::integer END, t1.v 
          Hash Key: t1.v
-         Hash Key: (SubPlan 3)
+         Hash Key: (SubPlan expr_3)
          ->  Seq Scan on pg_temp.gstest5 t1
-               Output: (SubPlan 3), t1.v, t1.id
-               SubPlan 3
+               Output: (SubPlan expr_3), t1.v, t1.id
+               SubPlan expr_3
                  ->  Bitmap Heap Scan on pg_temp.gstest5 t2
                        Output: t1.v
                        Recheck Cond: (t2.id = t1.id)
@@ -2272,18 +2272,18 @@ select grouping((select t1.v from gstest5 t2 where id = t1.id)),
 from gstest5 t1
 group by grouping sets(v, s)
 order by o nulls first;
-                                                                 QUERY PLAN
                      

----------------------------------------------------------------------------------------------------------------------------------------------
+                                                                           QUERY PLAN
                                          

+-----------------------------------------------------------------------------------------------------------------------------------------------------------------
  Sort
-   Output: (GROUPING((SubPlan 1))), ((SubPlan 3)), (CASE WHEN (GROUPING((SubPlan 2)) = 0) THEN ((SubPlan 3)) ELSE
NULL::integerEND), t1.v 
-   Sort Key: (CASE WHEN (GROUPING((SubPlan 2)) = 0) THEN ((SubPlan 3)) ELSE NULL::integer END) NULLS FIRST
+   Output: (GROUPING((SubPlan expr_1))), ((SubPlan expr_3)), (CASE WHEN (GROUPING((SubPlan expr_2)) = 0) THEN
((SubPlanexpr_3)) ELSE NULL::integer END), t1.v 
+   Sort Key: (CASE WHEN (GROUPING((SubPlan expr_2)) = 0) THEN ((SubPlan expr_3)) ELSE NULL::integer END) NULLS FIRST
    ->  HashAggregate
-         Output: GROUPING((SubPlan 1)), ((SubPlan 3)), CASE WHEN (GROUPING((SubPlan 2)) = 0) THEN ((SubPlan 3)) ELSE
NULL::integerEND, t1.v 
+         Output: GROUPING((SubPlan expr_1)), ((SubPlan expr_3)), CASE WHEN (GROUPING((SubPlan expr_2)) = 0) THEN
((SubPlanexpr_3)) ELSE NULL::integer END, t1.v 
          Hash Key: t1.v
-         Hash Key: (SubPlan 3)
+         Hash Key: (SubPlan expr_3)
          ->  Seq Scan on pg_temp.gstest5 t1
-               Output: (SubPlan 3), t1.v, t1.id
-               SubPlan 3
+               Output: (SubPlan expr_3), t1.v, t1.id
+               SubPlan expr_3
                  ->  Bitmap Heap Scan on pg_temp.gstest5 t2
                        Output: t1.v
                        Recheck Cond: (t2.id = t1.id)
diff --git a/src/test/regress/expected/incremental_sort.out b/src/test/regress/expected/incremental_sort.out
index 5a1dd9fc022..fdec5b9ba52 100644
--- a/src/test/regress/expected/incremental_sort.out
+++ b/src/test/regress/expected/incremental_sort.out
@@ -1609,13 +1609,13 @@ from tenk1 t, generate_series(1, 1000);
 ---------------------------------------------------------------------------------
  Unique
    ->  Sort
-         Sort Key: t.unique1, ((SubPlan 1))
+         Sort Key: t.unique1, ((SubPlan expr_1))
          ->  Gather
                Workers Planned: 2
                ->  Nested Loop
                      ->  Parallel Index Only Scan using tenk1_unique1 on tenk1 t
                      ->  Function Scan on generate_series
-               SubPlan 1
+               SubPlan expr_1
                  ->  Index Only Scan using tenk1_unique1 on tenk1
                        Index Cond: (unique1 = t.unique1)
 (11 rows)
@@ -1628,13 +1628,13 @@ order by 1, 2;
                                 QUERY PLAN
 ---------------------------------------------------------------------------
  Sort
-   Sort Key: t.unique1, ((SubPlan 1))
+   Sort Key: t.unique1, ((SubPlan expr_1))
    ->  Gather
          Workers Planned: 2
          ->  Nested Loop
                ->  Parallel Index Only Scan using tenk1_unique1 on tenk1 t
                ->  Function Scan on generate_series
-         SubPlan 1
+         SubPlan expr_1
            ->  Index Only Scan using tenk1_unique1 on tenk1
                  Index Cond: (unique1 = t.unique1)
 (10 rows)
diff --git a/src/test/regress/expected/inherit.out b/src/test/regress/expected/inherit.out
index 031dd87424a..6dbbd26f56b 100644
--- a/src/test/regress/expected/inherit.out
+++ b/src/test/regress/expected/inherit.out
@@ -1758,9 +1758,9 @@ explain (verbose, costs off) select min(1-id) from matest0;
                                    QUERY PLAN
 ---------------------------------------------------------------------------------
  Result
-   Output: (InitPlan 1).col1
+   Output: (InitPlan minmax_1).col1
    Replaces: MinMaxAggregate
-   InitPlan 1
+   InitPlan minmax_1
      ->  Limit
            Output: ((1 - matest0.id))
            ->  Result
@@ -1948,7 +1948,7 @@ SELECT min(x) FROM
 --------------------------------------------------------------------
  Result
    Replaces: MinMaxAggregate
-   InitPlan 1
+   InitPlan minmax_1
      ->  Limit
            ->  Merge Append
                  Sort Key: a.unique1
@@ -1967,7 +1967,7 @@ SELECT min(y) FROM
 --------------------------------------------------------------------
  Result
    Replaces: MinMaxAggregate
-   InitPlan 1
+   InitPlan minmax_1
      ->  Limit
            ->  Merge Append
                  Sort Key: a.unique1
@@ -2008,7 +2008,7 @@ FROM generate_series(1, 3) g(i);
                            QUERY PLAN
 ----------------------------------------------------------------
  Function Scan on generate_series g
-   SubPlan 1
+   SubPlan array_1
      ->  Limit
            ->  Merge Append
                  Sort Key: ((d.d + g.i))
@@ -2048,19 +2048,19 @@ insert into inhpar select x, x::text from generate_series(1,5) x;
 insert into inhcld select x::text, x from generate_series(6,10) x;
 explain (verbose, costs off)
 update inhpar i set (f1, f2) = (select i.f1, i.f2 || '-' from int4_tbl limit 1);
-                                         QUERY PLAN
---------------------------------------------------------------------------------------------
+                                                        QUERY PLAN
   

+--------------------------------------------------------------------------------------------------------------------------
  Update on public.inhpar i
    Update on public.inhpar i_1
    Update on public.inhcld i_2
    ->  Result
-         Output: (SubPlan 1).col1, (SubPlan 1).col2, (rescan SubPlan 1), i.tableoid, i.ctid
+         Output: (SubPlan multiexpr_1).col1, (SubPlan multiexpr_1).col2, (rescan SubPlan multiexpr_1), i.tableoid,
i.ctid
          ->  Append
                ->  Seq Scan on public.inhpar i_1
                      Output: i_1.f1, i_1.f2, i_1.tableoid, i_1.ctid
                ->  Seq Scan on public.inhcld i_2
                      Output: i_2.f1, i_2.f2, i_2.tableoid, i_2.ctid
-         SubPlan 1
+         SubPlan multiexpr_1
            ->  Limit
                  Output: (i.f1), (((i.f2)::text || '-'::text))
                  ->  Seq Scan on public.int4_tbl
@@ -2096,21 +2096,21 @@ alter table inhpar attach partition inhcld2 for values from (5) to (100);
 insert into inhpar select x, x::text from generate_series(1,10) x;
 explain (verbose, costs off)
 update inhpar i set (f1, f2) = (select i.f1, i.f2 || '-' from int4_tbl limit 1);
-                                              QUERY PLAN
-------------------------------------------------------------------------------------------------------
+                                                             QUERY PLAN
             

+------------------------------------------------------------------------------------------------------------------------------------
  Update on public.inhpar i
    Update on public.inhcld1 i_1
    Update on public.inhcld2 i_2
    ->  Append
          ->  Seq Scan on public.inhcld1 i_1
-               Output: (SubPlan 1).col1, (SubPlan 1).col2, (rescan SubPlan 1), i_1.tableoid, i_1.ctid
-               SubPlan 1
+               Output: (SubPlan multiexpr_1).col1, (SubPlan multiexpr_1).col2, (rescan SubPlan multiexpr_1),
i_1.tableoid,i_1.ctid 
+               SubPlan multiexpr_1
                  ->  Limit
                        Output: (i_1.f1), (((i_1.f2)::text || '-'::text))
                        ->  Seq Scan on public.int4_tbl
                              Output: i_1.f1, ((i_1.f2)::text || '-'::text)
          ->  Seq Scan on public.inhcld2 i_2
-               Output: (SubPlan 1).col1, (SubPlan 1).col2, (rescan SubPlan 1), i_2.tableoid, i_2.ctid
+               Output: (SubPlan multiexpr_1).col1, (SubPlan multiexpr_1).col2, (rescan SubPlan multiexpr_1),
i_2.tableoid,i_2.ctid 
 (13 rows)

 update inhpar i set (f1, f2) = (select i.f1, i.f2 || '-' from int4_tbl limit 1);
@@ -3260,11 +3260,11 @@ explain (costs off) select min(a), max(a) from parted_minmax where b = '12345';
 ------------------------------------------------------------------------------------------------
  Result
    Replaces: MinMaxAggregate
-   InitPlan 1
+   InitPlan minmax_1
      ->  Limit
            ->  Index Only Scan using parted_minmax1i on parted_minmax1 parted_minmax
                  Index Cond: ((a IS NOT NULL) AND (b = '12345'::text))
-   InitPlan 2
+   InitPlan minmax_1
      ->  Limit
            ->  Index Only Scan Backward using parted_minmax1i on parted_minmax1 parted_minmax_1
                  Index Cond: ((a IS NOT NULL) AND (b = '12345'::text))
diff --git a/src/test/regress/expected/insert_conflict.out b/src/test/regress/expected/insert_conflict.out
index fdd0f6c8f25..db668474684 100644
--- a/src/test/regress/expected/insert_conflict.out
+++ b/src/test/regress/expected/insert_conflict.out
@@ -61,9 +61,9 @@ explain (costs off) insert into insertconflicttest values(0, 'Crowberry') on con
  Insert on insertconflicttest
    Conflict Resolution: UPDATE
    Conflict Arbiter Indexes: op_index_key, collation_index_key, both_index_key
-   Conflict Filter: EXISTS(SubPlan 1)
+   Conflict Filter: EXISTS(SubPlan exists_1)
    ->  Result
-   SubPlan 1
+   SubPlan exists_1
      ->  Index Only Scan using both_index_expr_key on insertconflicttest ii
            Index Cond: (key = excluded.key)
 (8 rows)
diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out
index cd37f549b5a..14a6d7513aa 100644
--- a/src/test/regress/expected/join.out
+++ b/src/test/regress/expected/join.out
@@ -2375,7 +2375,7 @@ order by t1.unique1;
  Sort
    Sort Key: t1.unique1
    ->  Hash Join
-         Hash Cond: ((t1.two = t2.two) AND (t1.unique1 = (SubPlan 2)))
+         Hash Cond: ((t1.two = t2.two) AND (t1.unique1 = (SubPlan expr_1)))
          ->  Bitmap Heap Scan on tenk1 t1
                Recheck Cond: (unique1 < 10)
                ->  Bitmap Index Scan on tenk1_unique1
@@ -2385,10 +2385,10 @@ order by t1.unique1;
                      Recheck Cond: (unique1 < 10)
                      ->  Bitmap Index Scan on tenk1_unique1
                            Index Cond: (unique1 < 10)
-               SubPlan 2
+               SubPlan expr_1
                  ->  Result
                        Replaces: MinMaxAggregate
-                       InitPlan 1
+                       InitPlan minmax_1
                          ->  Limit
                                ->  Index Only Scan using tenk1_unique1 on tenk1
                                      Index Cond: ((unique1 IS NOT NULL) AND (unique1 = t2.unique1))
@@ -3181,11 +3181,11 @@ where unique1 in (select unique2 from tenk1 b);
 explain (costs off)
 select a.* from tenk1 a
 where unique1 not in (select unique2 from tenk1 b);
-                        QUERY PLAN
------------------------------------------------------------
+                          QUERY PLAN
+---------------------------------------------------------------
  Seq Scan on tenk1 a
-   Filter: (NOT (ANY (unique1 = (hashed SubPlan 1).col1)))
-   SubPlan 1
+   Filter: (NOT (ANY (unique1 = (hashed SubPlan any_1).col1)))
+   SubPlan any_1
      ->  Index Only Scan using tenk1_unique2 on tenk1 b
 (4 rows)

@@ -3706,11 +3706,11 @@ order by 1,2;
    Sort Key: t1.q1, t1.q2
    ->  Hash Left Join
          Hash Cond: (t1.q2 = t2.q1)
-         Filter: (1 = (SubPlan 1))
+         Filter: (1 = (SubPlan expr_1))
          ->  Seq Scan on int8_tbl t1
          ->  Hash
                ->  Seq Scan on int8_tbl t2
-         SubPlan 1
+         SubPlan expr_1
            ->  Limit
                  ->  Result
                        One-Time Filter: ((42) IS NOT NULL)
@@ -4225,14 +4225,14 @@ from int8_tbl i8
   right join (select false as z) ss3 on true,
   lateral (select i8.q2 as q2l where x limit 1) ss4
 where i8.q2 = 123;
-                           QUERY PLAN
-----------------------------------------------------------------
+                             QUERY PLAN
+---------------------------------------------------------------------
  Nested Loop
-   Output: i8.q1, i8.q2, (InitPlan 1).col1, false, (i8.q2)
-   InitPlan 1
+   Output: i8.q1, i8.q2, (InitPlan expr_1).col1, false, (i8.q2)
+   InitPlan expr_1
      ->  Result
            Output: true
-   InitPlan 2
+   InitPlan expr_2
      ->  Result
            Output: true
    ->  Seq Scan on public.int4_tbl i4
@@ -4241,7 +4241,7 @@ where i8.q2 = 123;
    ->  Nested Loop
          Output: i8.q1, i8.q2, (i8.q2)
          ->  Subquery Scan on ss1
-               Output: ss1.y, (InitPlan 1).col1
+               Output: ss1.y, (InitPlan expr_1).col1
                ->  Limit
                      Output: NULL::integer
                      ->  Result
@@ -4255,7 +4255,7 @@ where i8.q2 = 123;
                      Output: (i8.q2)
                      ->  Result
                            Output: i8.q2
-                           One-Time Filter: ((InitPlan 1).col1)
+                           One-Time Filter: ((InitPlan expr_1).col1)
 (29 rows)

 explain (verbose, costs off)
@@ -4268,14 +4268,14 @@ from int8_tbl i8
   right join (select false as z) ss3 on true,
   lateral (select i8.q2 as q2l where x limit 1) ss4
 where i8.q2 = 123;
-                           QUERY PLAN
-----------------------------------------------------------------
+                             QUERY PLAN
+---------------------------------------------------------------------
  Nested Loop
-   Output: i8.q1, i8.q2, (InitPlan 1).col1, false, (i8.q2)
-   InitPlan 1
+   Output: i8.q1, i8.q2, (InitPlan expr_1).col1, false, (i8.q2)
+   InitPlan expr_1
      ->  Result
            Output: true
-   InitPlan 2
+   InitPlan expr_2
      ->  Result
            Output: true
    ->  Limit
@@ -4285,7 +4285,7 @@ where i8.q2 = 123;
    ->  Nested Loop
          Output: i8.q1, i8.q2, (i8.q2)
          ->  Seq Scan on public.int4_tbl i4
-               Output: i4.f1, (InitPlan 1).col1
+               Output: i4.f1, (InitPlan expr_1).col1
                Filter: (i4.f1 = 0)
          ->  Nested Loop
                Output: i8.q1, i8.q2, (i8.q2)
@@ -4296,7 +4296,7 @@ where i8.q2 = 123;
                      Output: (i8.q2)
                      ->  Result
                            Output: i8.q2
-                           One-Time Filter: ((InitPlan 1).col1)
+                           One-Time Filter: ((InitPlan expr_1).col1)
 (27 rows)

 -- Test proper handling of appendrel PHVs during useless-RTE removal
@@ -5757,13 +5757,13 @@ explain (costs off)
 select a.unique1, b.unique2
   from onek a left join onek b on a.unique1 = b.unique2
   where (b.unique2, random() > 0) = any (select q1, random() > 0 from int8_tbl c where c.q1 < b.unique1);
-                                                    QUERY PLAN
-------------------------------------------------------------------------------------------------------------------
+                                                        QUERY PLAN
   

+--------------------------------------------------------------------------------------------------------------------------
  Hash Join
    Hash Cond: (b.unique2 = a.unique1)
    ->  Seq Scan on onek b
-         Filter: (ANY ((unique2 = (SubPlan 1).col1) AND ((random() > '0'::double precision) = (SubPlan 1).col2)))
-         SubPlan 1
+         Filter: (ANY ((unique2 = (SubPlan any_1).col1) AND ((random() > '0'::double precision) = (SubPlan
any_1).col2)))
+         SubPlan any_1
            ->  Seq Scan on int8_tbl c
                  Filter: (q1 < b.unique1)
    ->  Hash
@@ -6105,7 +6105,7 @@ select exists(
                              QUERY PLAN
 ---------------------------------------------------------------------
  Seq Scan on int4_tbl x0
-   SubPlan 1
+   SubPlan exists_1
      ->  Nested Loop Left Join
            Join Filter: (t2.q2 = t4.q2)
            ->  Nested Loop Left Join
@@ -6956,7 +6956,7 @@ where t1.a = t2.a;
 ------------------------------------------
  Seq Scan on sj t2
    Filter: (a IS NOT NULL)
-   SubPlan 1
+   SubPlan expr_1
      ->  Result
            One-Time Filter: (t2.a = t2.a)
            ->  Seq Scan on sj
@@ -8983,8 +8983,8 @@ lateral (select * from int8_tbl t1,
                                      where q2 = (select greatest(t1.q1,t2.q2))
                                        and (select v.id=0)) offset 0) ss2) ss
          where t1.q1 = ss.q2) ss0;
-                                                         QUERY PLAN
     

-----------------------------------------------------------------------------------------------------------------------------
+                                                             QUERY PLAN
             

+------------------------------------------------------------------------------------------------------------------------------------
  Nested Loop
    Output: "*VALUES*".column1, t1.q1, t1.q2, ss2.q1, ss2.q2
    ->  Seq Scan on public.int8_tbl t1
@@ -8998,20 +8998,20 @@ lateral (select * from int8_tbl t1,
                Filter: (t1.q1 = ss2.q2)
                ->  Seq Scan on public.int8_tbl t2
                      Output: t2.q1, t2.q2
-                     Filter: (ANY ((t2.q1 = (SubPlan 3).col1) AND ((random() > '0'::double precision) = (SubPlan
3).col2)))
-                     SubPlan 3
+                     Filter: (ANY ((t2.q1 = (SubPlan any_1).col1) AND ((random() > '0'::double precision) = (SubPlan
any_1).col2)))
+                     SubPlan any_1
                        ->  Result
                              Output: t3.q2, (random() > '0'::double precision)
-                             One-Time Filter: (InitPlan 2).col1
-                             InitPlan 1
+                             One-Time Filter: (InitPlan expr_2).col1
+                             InitPlan expr_1
                                ->  Result
                                      Output: GREATEST(t1.q1, t2.q2)
-                             InitPlan 2
+                             InitPlan expr_2
                                ->  Result
                                      Output: ("*VALUES*".column1 = 0)
                              ->  Seq Scan on public.int8_tbl t3
                                    Output: t3.q1, t3.q2
-                                   Filter: (t3.q2 = (InitPlan 1).col1)
+                                   Filter: (t3.q2 = (InitPlan expr_1).col1)
 (27 rows)

 select * from (values (0), (1)) v(id),
@@ -9723,13 +9723,13 @@ SELECT * FROM rescan_bhs t1 LEFT JOIN rescan_bhs t2 ON t1.a IN
                         QUERY PLAN
 -----------------------------------------------------------
  Nested Loop Left Join
-   Join Filter: (ANY (t1.a = (SubPlan 1).col1))
+   Join Filter: (ANY (t1.a = (SubPlan any_1).col1))
    ->  Bitmap Heap Scan on rescan_bhs t1
          ->  Bitmap Index Scan on rescan_bhs_a_idx
    ->  Materialize
          ->  Bitmap Heap Scan on rescan_bhs t2
                ->  Bitmap Index Scan on rescan_bhs_a_idx
-   SubPlan 1
+   SubPlan any_1
      ->  Result
            One-Time Filter: (t2.a > 1)
            ->  Bitmap Heap Scan on rescan_bhs t3
diff --git a/src/test/regress/expected/join_hash.out b/src/test/regress/expected/join_hash.out
index 4fc34a0e72a..a45e1450040 100644
--- a/src/test/regress/expected/join_hash.out
+++ b/src/test/regress/expected/join_hash.out
@@ -1031,30 +1031,30 @@ WHERE
 ------------------------------------------------------------------------------------------------
  Hash Join
    Output: hjtest_1.a, hjtest_2.a, (hjtest_1.tableoid)::regclass, (hjtest_2.tableoid)::regclass
-   Hash Cond: ((hjtest_1.id = (SubPlan 1)) AND ((SubPlan 2) = (SubPlan 3)))
+   Hash Cond: ((hjtest_1.id = (SubPlan expr_1)) AND ((SubPlan expr_2) = (SubPlan expr_3)))
    Join Filter: (hjtest_1.a <> hjtest_2.b)
    ->  Seq Scan on public.hjtest_1
          Output: hjtest_1.a, hjtest_1.tableoid, hjtest_1.id, hjtest_1.b
-         Filter: ((SubPlan 4) < 50)
-         SubPlan 4
+         Filter: ((SubPlan expr_4) < 50)
+         SubPlan expr_4
            ->  Result
                  Output: (hjtest_1.b * 5)
    ->  Hash
          Output: hjtest_2.a, hjtest_2.tableoid, hjtest_2.id, hjtest_2.c, hjtest_2.b
          ->  Seq Scan on public.hjtest_2
                Output: hjtest_2.a, hjtest_2.tableoid, hjtest_2.id, hjtest_2.c, hjtest_2.b
-               Filter: ((SubPlan 5) < 55)
-               SubPlan 5
+               Filter: ((SubPlan expr_5) < 55)
+               SubPlan expr_5
                  ->  Result
                        Output: (hjtest_2.c * 5)
-         SubPlan 1
+         SubPlan expr_1
            ->  Result
                  Output: 1
                  One-Time Filter: (hjtest_2.id = 1)
-         SubPlan 3
+         SubPlan expr_3
            ->  Result
                  Output: (hjtest_2.c * 5)
-   SubPlan 2
+   SubPlan expr_2
      ->  Result
            Output: (hjtest_1.b * 5)
 (28 rows)
@@ -1085,30 +1085,30 @@ WHERE
 ------------------------------------------------------------------------------------------------
  Hash Join
    Output: hjtest_1.a, hjtest_2.a, (hjtest_1.tableoid)::regclass, (hjtest_2.tableoid)::regclass
-   Hash Cond: (((SubPlan 1) = hjtest_1.id) AND ((SubPlan 3) = (SubPlan 2)))
+   Hash Cond: (((SubPlan expr_1) = hjtest_1.id) AND ((SubPlan expr_3) = (SubPlan expr_2)))
    Join Filter: (hjtest_1.a <> hjtest_2.b)
    ->  Seq Scan on public.hjtest_2
          Output: hjtest_2.a, hjtest_2.tableoid, hjtest_2.id, hjtest_2.c, hjtest_2.b
-         Filter: ((SubPlan 5) < 55)
-         SubPlan 5
+         Filter: ((SubPlan expr_5) < 55)
+         SubPlan expr_5
            ->  Result
                  Output: (hjtest_2.c * 5)
    ->  Hash
          Output: hjtest_1.a, hjtest_1.tableoid, hjtest_1.id, hjtest_1.b
          ->  Seq Scan on public.hjtest_1
                Output: hjtest_1.a, hjtest_1.tableoid, hjtest_1.id, hjtest_1.b
-               Filter: ((SubPlan 4) < 50)
-               SubPlan 4
+               Filter: ((SubPlan expr_4) < 50)
+               SubPlan expr_4
                  ->  Result
                        Output: (hjtest_1.b * 5)
-         SubPlan 2
+         SubPlan expr_2
            ->  Result
                  Output: (hjtest_1.b * 5)
-   SubPlan 1
+   SubPlan expr_1
      ->  Result
            Output: 1
            One-Time Filter: (hjtest_2.id = 1)
-   SubPlan 3
+   SubPlan expr_3
      ->  Result
            Output: (hjtest_2.c * 5)
 (28 rows)
diff --git a/src/test/regress/expected/memoize.out b/src/test/regress/expected/memoize.out
index fbcaf113266..00c30b91459 100644
--- a/src/test/regress/expected/memoize.out
+++ b/src/test/regress/expected/memoize.out
@@ -429,8 +429,8 @@ WHERE unique1 < 3
 ----------------------------------------------------------------
  Index Scan using tenk1_unique1 on tenk1 t0
    Index Cond: (unique1 < 3)
-   Filter: EXISTS(SubPlan 1)
-   SubPlan 1
+   Filter: EXISTS(SubPlan exists_1)
+   SubPlan exists_1
      ->  Nested Loop
            ->  Index Scan using tenk1_hundred on tenk1 t2
                  Filter: (t0.two <> four)
diff --git a/src/test/regress/expected/merge.out b/src/test/regress/expected/merge.out
index 44df626c40c..9cb1d87066a 100644
--- a/src/test/regress/expected/merge.out
+++ b/src/test/regress/expected/merge.out
@@ -1828,29 +1828,29 @@ WHEN MATCHED AND t.c > s.cnt THEN
    ->  Hash Join
          Output: t.ctid, s.a, s.b, s.c, s.d, s.ctid
          Hash Cond: (t.a = s.a)
-         Join Filter: (t.b < (SubPlan 1))
+         Join Filter: (t.b < (SubPlan expr_1))
          ->  Seq Scan on public.tgt t
                Output: t.ctid, t.a, t.b
          ->  Hash
                Output: s.a, s.b, s.c, s.d, s.ctid
                ->  Seq Scan on public.src s
                      Output: s.a, s.b, s.c, s.d, s.ctid
-         SubPlan 1
+         SubPlan expr_1
            ->  Aggregate
                  Output: count(*)
                  ->  Seq Scan on public.ref r
                        Output: r.ab, r.cd
                        Filter: ((r.ab = (s.a + s.b)) AND (r.cd = (s.c - s.d)))
-   SubPlan 4
+   SubPlan expr_3
      ->  Aggregate
            Output: count(*)
            ->  Seq Scan on public.ref r_2
                  Output: r_2.ab, r_2.cd
                  Filter: ((r_2.ab = (s.a + s.b)) AND (r_2.cd = (s.c - s.d)))
-   SubPlan 3
+   SubPlan multiexpr_1
      ->  Result
-           Output: s.b, (InitPlan 2).col1
-           InitPlan 2
+           Output: s.b, (InitPlan expr_2).col1
+           InitPlan expr_2
              ->  Aggregate
                    Output: count(*)
                    ->  Seq Scan on public.ref r_1
diff --git a/src/test/regress/expected/partition_prune.out b/src/test/regress/expected/partition_prune.out
index 7499cdb2cdf..deacdd75807 100644
--- a/src/test/regress/expected/partition_prune.out
+++ b/src/test/regress/expected/partition_prune.out
@@ -1915,21 +1915,21 @@ select * from
    from int4_tbl touter) ss,
   asptab
 where asptab.id > ss.b::int;
-                             QUERY PLAN
---------------------------------------------------------------------
+                              QUERY PLAN
+----------------------------------------------------------------------
  Nested Loop
    ->  Seq Scan on int4_tbl touter
    ->  Append
          ->  Index Only Scan using asptab0_pkey on asptab0 asptab_1
-               Index Cond: (id > (EXISTS(SubPlan 3))::integer)
-               SubPlan 4
+               Index Cond: (id > (EXISTS(SubPlan exists_3))::integer)
+               SubPlan exists_4
                  ->  Seq Scan on int4_tbl tinner_2
          ->  Index Only Scan using asptab1_pkey on asptab1 asptab_2
-               Index Cond: (id > (EXISTS(SubPlan 3))::integer)
-         SubPlan 3
+               Index Cond: (id > (EXISTS(SubPlan exists_3))::integer)
+         SubPlan exists_3
            ->  Seq Scan on int4_tbl tinner_1
                  Filter: (f1 = touter.f1)
-   SubPlan 2
+   SubPlan exists_2
      ->  Seq Scan on int4_tbl tinner
 (14 rows)

@@ -2236,36 +2236,36 @@ explain (analyze, costs off, summary off, timing off, buffers off) execute ab_q1
 prepare ab_q2 (int, int) as
 select a from ab where a between $1 and $2 and b < (select 3);
 explain (analyze, costs off, summary off, timing off, buffers off) execute ab_q2 (2, 2);
-                              QUERY PLAN
------------------------------------------------------------------------
+                                 QUERY PLAN
+----------------------------------------------------------------------------
  Append (actual rows=0.00 loops=1)
    Subplans Removed: 6
-   InitPlan 1
+   InitPlan expr_1
      ->  Result (actual rows=1.00 loops=1)
    ->  Seq Scan on ab_a2_b1 ab_1 (actual rows=0.00 loops=1)
-         Filter: ((a >= $1) AND (a <= $2) AND (b < (InitPlan 1).col1))
+         Filter: ((a >= $1) AND (a <= $2) AND (b < (InitPlan expr_1).col1))
    ->  Seq Scan on ab_a2_b2 ab_2 (actual rows=0.00 loops=1)
-         Filter: ((a >= $1) AND (a <= $2) AND (b < (InitPlan 1).col1))
+         Filter: ((a >= $1) AND (a <= $2) AND (b < (InitPlan expr_1).col1))
    ->  Seq Scan on ab_a2_b3 ab_3 (never executed)
-         Filter: ((a >= $1) AND (a <= $2) AND (b < (InitPlan 1).col1))
+         Filter: ((a >= $1) AND (a <= $2) AND (b < (InitPlan expr_1).col1))
 (10 rows)

 -- As above, but swap the PARAM_EXEC Param to the first partition level
 prepare ab_q3 (int, int) as
 select a from ab where b between $1 and $2 and a < (select 3);
 explain (analyze, costs off, summary off, timing off, buffers off) execute ab_q3 (2, 2);
-                              QUERY PLAN
------------------------------------------------------------------------
+                                 QUERY PLAN
+----------------------------------------------------------------------------
  Append (actual rows=0.00 loops=1)
    Subplans Removed: 6
-   InitPlan 1
+   InitPlan expr_1
      ->  Result (actual rows=1.00 loops=1)
    ->  Seq Scan on ab_a1_b2 ab_1 (actual rows=0.00 loops=1)
-         Filter: ((b >= $1) AND (b <= $2) AND (a < (InitPlan 1).col1))
+         Filter: ((b >= $1) AND (b <= $2) AND (a < (InitPlan expr_1).col1))
    ->  Seq Scan on ab_a2_b2 ab_2 (actual rows=0.00 loops=1)
-         Filter: ((b >= $1) AND (b <= $2) AND (a < (InitPlan 1).col1))
+         Filter: ((b >= $1) AND (b <= $2) AND (a < (InitPlan expr_1).col1))
    ->  Seq Scan on ab_a3_b2 ab_3 (never executed)
-         Filter: ((b >= $1) AND (b <= $2) AND (a < (InitPlan 1).col1))
+         Filter: ((b >= $1) AND (b <= $2) AND (a < (InitPlan expr_1).col1))
 (10 rows)

 --
@@ -2475,23 +2475,23 @@ select explain_parallel_append('execute ab_q5 (33, 44, 55)');

 -- Test Parallel Append with PARAM_EXEC Params
 select explain_parallel_append('select count(*) from ab where (a = (select 1) or a = (select 3)) and b = 2');
-                                    explain_parallel_append
-------------------------------------------------------------------------------------------------
+                                         explain_parallel_append
+----------------------------------------------------------------------------------------------------------
  Aggregate (actual rows=N loops=N)
-   InitPlan 1
+   InitPlan expr_1
      ->  Result (actual rows=N loops=N)
-   InitPlan 2
+   InitPlan expr_2
      ->  Result (actual rows=N loops=N)
    ->  Gather (actual rows=N loops=N)
          Workers Planned: 2
          Workers Launched: N
          ->  Parallel Append (actual rows=N loops=N)
                ->  Parallel Seq Scan on ab_a1_b2 ab_1 (actual rows=N loops=N)
-                     Filter: ((b = 2) AND ((a = (InitPlan 1).col1) OR (a = (InitPlan 2).col1)))
+                     Filter: ((b = 2) AND ((a = (InitPlan expr_1).col1) OR (a = (InitPlan expr_2).col1)))
                ->  Parallel Seq Scan on ab_a2_b2 ab_2 (never executed)
-                     Filter: ((b = 2) AND ((a = (InitPlan 1).col1) OR (a = (InitPlan 2).col1)))
+                     Filter: ((b = 2) AND ((a = (InitPlan expr_1).col1) OR (a = (InitPlan expr_2).col1)))
                ->  Parallel Seq Scan on ab_a3_b2 ab_3 (actual rows=N loops=N)
-                     Filter: ((b = 2) AND ((a = (InitPlan 1).col1) OR (a = (InitPlan 2).col1)))
+                     Filter: ((b = 2) AND ((a = (InitPlan expr_1).col1) OR (a = (InitPlan expr_2).col1)))
 (15 rows)

 -- Test pruning during parallel nested loop query
@@ -2692,65 +2692,65 @@ select * from ab where a = (select max(a) from lprt_a) and b = (select max(a)-1
                                  QUERY PLAN
 ----------------------------------------------------------------------------
  Append (actual rows=0.00 loops=1)
-   InitPlan 1
+   InitPlan expr_1
      ->  Aggregate (actual rows=1.00 loops=1)
            ->  Seq Scan on lprt_a (actual rows=102.00 loops=1)
-   InitPlan 2
+   InitPlan expr_2
      ->  Aggregate (actual rows=1.00 loops=1)
            ->  Seq Scan on lprt_a lprt_a_1 (actual rows=102.00 loops=1)
    ->  Bitmap Heap Scan on ab_a1_b1 ab_1 (never executed)
-         Recheck Cond: (a = (InitPlan 1).col1)
-         Filter: (b = (InitPlan 2).col1)
+         Recheck Cond: (a = (InitPlan expr_1).col1)
+         Filter: (b = (InitPlan expr_2).col1)
          ->  Bitmap Index Scan on ab_a1_b1_a_idx (never executed)
-               Index Cond: (a = (InitPlan 1).col1)
+               Index Cond: (a = (InitPlan expr_1).col1)
                Index Searches: 0
    ->  Bitmap Heap Scan on ab_a1_b2 ab_2 (never executed)
-         Recheck Cond: (a = (InitPlan 1).col1)
-         Filter: (b = (InitPlan 2).col1)
+         Recheck Cond: (a = (InitPlan expr_1).col1)
+         Filter: (b = (InitPlan expr_2).col1)
          ->  Bitmap Index Scan on ab_a1_b2_a_idx (never executed)
-               Index Cond: (a = (InitPlan 1).col1)
+               Index Cond: (a = (InitPlan expr_1).col1)
                Index Searches: 0
    ->  Bitmap Heap Scan on ab_a1_b3 ab_3 (never executed)
-         Recheck Cond: (a = (InitPlan 1).col1)
-         Filter: (b = (InitPlan 2).col1)
+         Recheck Cond: (a = (InitPlan expr_1).col1)
+         Filter: (b = (InitPlan expr_2).col1)
          ->  Bitmap Index Scan on ab_a1_b3_a_idx (never executed)
-               Index Cond: (a = (InitPlan 1).col1)
+               Index Cond: (a = (InitPlan expr_1).col1)
                Index Searches: 0
    ->  Bitmap Heap Scan on ab_a2_b1 ab_4 (never executed)
-         Recheck Cond: (a = (InitPlan 1).col1)
-         Filter: (b = (InitPlan 2).col1)
+         Recheck Cond: (a = (InitPlan expr_1).col1)
+         Filter: (b = (InitPlan expr_2).col1)
          ->  Bitmap Index Scan on ab_a2_b1_a_idx (never executed)
-               Index Cond: (a = (InitPlan 1).col1)
+               Index Cond: (a = (InitPlan expr_1).col1)
                Index Searches: 0
    ->  Bitmap Heap Scan on ab_a2_b2 ab_5 (never executed)
-         Recheck Cond: (a = (InitPlan 1).col1)
-         Filter: (b = (InitPlan 2).col1)
+         Recheck Cond: (a = (InitPlan expr_1).col1)
+         Filter: (b = (InitPlan expr_2).col1)
          ->  Bitmap Index Scan on ab_a2_b2_a_idx (never executed)
-               Index Cond: (a = (InitPlan 1).col1)
+               Index Cond: (a = (InitPlan expr_1).col1)
                Index Searches: 0
    ->  Bitmap Heap Scan on ab_a2_b3 ab_6 (never executed)
-         Recheck Cond: (a = (InitPlan 1).col1)
-         Filter: (b = (InitPlan 2).col1)
+         Recheck Cond: (a = (InitPlan expr_1).col1)
+         Filter: (b = (InitPlan expr_2).col1)
          ->  Bitmap Index Scan on ab_a2_b3_a_idx (never executed)
-               Index Cond: (a = (InitPlan 1).col1)
+               Index Cond: (a = (InitPlan expr_1).col1)
                Index Searches: 0
    ->  Bitmap Heap Scan on ab_a3_b1 ab_7 (never executed)
-         Recheck Cond: (a = (InitPlan 1).col1)
-         Filter: (b = (InitPlan 2).col1)
+         Recheck Cond: (a = (InitPlan expr_1).col1)
+         Filter: (b = (InitPlan expr_2).col1)
          ->  Bitmap Index Scan on ab_a3_b1_a_idx (never executed)
-               Index Cond: (a = (InitPlan 1).col1)
+               Index Cond: (a = (InitPlan expr_1).col1)
                Index Searches: 0
    ->  Bitmap Heap Scan on ab_a3_b2 ab_8 (actual rows=0.00 loops=1)
-         Recheck Cond: (a = (InitPlan 1).col1)
-         Filter: (b = (InitPlan 2).col1)
+         Recheck Cond: (a = (InitPlan expr_1).col1)
+         Filter: (b = (InitPlan expr_2).col1)
          ->  Bitmap Index Scan on ab_a3_b2_a_idx (actual rows=0.00 loops=1)
-               Index Cond: (a = (InitPlan 1).col1)
+               Index Cond: (a = (InitPlan expr_1).col1)
                Index Searches: 1
    ->  Bitmap Heap Scan on ab_a3_b3 ab_9 (never executed)
-         Recheck Cond: (a = (InitPlan 1).col1)
-         Filter: (b = (InitPlan 2).col1)
+         Recheck Cond: (a = (InitPlan expr_1).col1)
+         Filter: (b = (InitPlan expr_2).col1)
          ->  Bitmap Index Scan on ab_a3_b3_a_idx (never executed)
-               Index Cond: (a = (InitPlan 1).col1)
+               Index Cond: (a = (InitPlan expr_1).col1)
                Index Searches: 0
 (61 rows)

@@ -2760,45 +2760,45 @@ select * from (select * from ab where a = 1 union all select * from ab) ab where
                                     QUERY PLAN
 ----------------------------------------------------------------------------------
  Append (actual rows=0.00 loops=1)
-   InitPlan 1
+   InitPlan expr_1
      ->  Result (actual rows=1.00 loops=1)
    ->  Append (actual rows=0.00 loops=1)
          ->  Bitmap Heap Scan on ab_a1_b1 ab_11 (actual rows=0.00 loops=1)
                Recheck Cond: (a = 1)
-               Filter: (b = (InitPlan 1).col1)
+               Filter: (b = (InitPlan expr_1).col1)
                ->  Bitmap Index Scan on ab_a1_b1_a_idx (actual rows=0.00 loops=1)
                      Index Cond: (a = 1)
                      Index Searches: 1
          ->  Bitmap Heap Scan on ab_a1_b2 ab_12 (never executed)
                Recheck Cond: (a = 1)
-               Filter: (b = (InitPlan 1).col1)
+               Filter: (b = (InitPlan expr_1).col1)
                ->  Bitmap Index Scan on ab_a1_b2_a_idx (never executed)
                      Index Cond: (a = 1)
                      Index Searches: 0
          ->  Bitmap Heap Scan on ab_a1_b3 ab_13 (never executed)
                Recheck Cond: (a = 1)
-               Filter: (b = (InitPlan 1).col1)
+               Filter: (b = (InitPlan expr_1).col1)
                ->  Bitmap Index Scan on ab_a1_b3_a_idx (never executed)
                      Index Cond: (a = 1)
                      Index Searches: 0
    ->  Seq Scan on ab_a1_b1 ab_1 (actual rows=0.00 loops=1)
-         Filter: (b = (InitPlan 1).col1)
+         Filter: (b = (InitPlan expr_1).col1)
    ->  Seq Scan on ab_a1_b2 ab_2 (never executed)
-         Filter: (b = (InitPlan 1).col1)
+         Filter: (b = (InitPlan expr_1).col1)
    ->  Seq Scan on ab_a1_b3 ab_3 (never executed)
-         Filter: (b = (InitPlan 1).col1)
+         Filter: (b = (InitPlan expr_1).col1)
    ->  Seq Scan on ab_a2_b1 ab_4 (actual rows=0.00 loops=1)
-         Filter: (b = (InitPlan 1).col1)
+         Filter: (b = (InitPlan expr_1).col1)
    ->  Seq Scan on ab_a2_b2 ab_5 (never executed)
-         Filter: (b = (InitPlan 1).col1)
+         Filter: (b = (InitPlan expr_1).col1)
    ->  Seq Scan on ab_a2_b3 ab_6 (never executed)
-         Filter: (b = (InitPlan 1).col1)
+         Filter: (b = (InitPlan expr_1).col1)
    ->  Seq Scan on ab_a3_b1 ab_7 (actual rows=0.00 loops=1)
-         Filter: (b = (InitPlan 1).col1)
+         Filter: (b = (InitPlan expr_1).col1)
    ->  Seq Scan on ab_a3_b2 ab_8 (never executed)
-         Filter: (b = (InitPlan 1).col1)
+         Filter: (b = (InitPlan expr_1).col1)
    ->  Seq Scan on ab_a3_b3 ab_9 (never executed)
-         Filter: (b = (InitPlan 1).col1)
+         Filter: (b = (InitPlan expr_1).col1)
 (40 rows)

 -- A case containing a UNION ALL with a non-partitioned child.
@@ -2807,47 +2807,47 @@ select * from (select * from ab where a = 1 union all (values(10,5)) union all s
                                     QUERY PLAN
 ----------------------------------------------------------------------------------
  Append (actual rows=0.00 loops=1)
-   InitPlan 1
+   InitPlan expr_1
      ->  Result (actual rows=1.00 loops=1)
    ->  Append (actual rows=0.00 loops=1)
          ->  Bitmap Heap Scan on ab_a1_b1 ab_11 (actual rows=0.00 loops=1)
                Recheck Cond: (a = 1)
-               Filter: (b = (InitPlan 1).col1)
+               Filter: (b = (InitPlan expr_1).col1)
                ->  Bitmap Index Scan on ab_a1_b1_a_idx (actual rows=0.00 loops=1)
                      Index Cond: (a = 1)
                      Index Searches: 1
          ->  Bitmap Heap Scan on ab_a1_b2 ab_12 (never executed)
                Recheck Cond: (a = 1)
-               Filter: (b = (InitPlan 1).col1)
+               Filter: (b = (InitPlan expr_1).col1)
                ->  Bitmap Index Scan on ab_a1_b2_a_idx (never executed)
                      Index Cond: (a = 1)
                      Index Searches: 0
          ->  Bitmap Heap Scan on ab_a1_b3 ab_13 (never executed)
                Recheck Cond: (a = 1)
-               Filter: (b = (InitPlan 1).col1)
+               Filter: (b = (InitPlan expr_1).col1)
                ->  Bitmap Index Scan on ab_a1_b3_a_idx (never executed)
                      Index Cond: (a = 1)
                      Index Searches: 0
    ->  Result (actual rows=0.00 loops=1)
-         One-Time Filter: (5 = (InitPlan 1).col1)
+         One-Time Filter: (5 = (InitPlan expr_1).col1)
    ->  Seq Scan on ab_a1_b1 ab_1 (actual rows=0.00 loops=1)
-         Filter: (b = (InitPlan 1).col1)
+         Filter: (b = (InitPlan expr_1).col1)
    ->  Seq Scan on ab_a1_b2 ab_2 (never executed)
-         Filter: (b = (InitPlan 1).col1)
+         Filter: (b = (InitPlan expr_1).col1)
    ->  Seq Scan on ab_a1_b3 ab_3 (never executed)
-         Filter: (b = (InitPlan 1).col1)
+         Filter: (b = (InitPlan expr_1).col1)
    ->  Seq Scan on ab_a2_b1 ab_4 (actual rows=0.00 loops=1)
-         Filter: (b = (InitPlan 1).col1)
+         Filter: (b = (InitPlan expr_1).col1)
    ->  Seq Scan on ab_a2_b2 ab_5 (never executed)
-         Filter: (b = (InitPlan 1).col1)
+         Filter: (b = (InitPlan expr_1).col1)
    ->  Seq Scan on ab_a2_b3 ab_6 (never executed)
-         Filter: (b = (InitPlan 1).col1)
+         Filter: (b = (InitPlan expr_1).col1)
    ->  Seq Scan on ab_a3_b1 ab_7 (actual rows=0.00 loops=1)
-         Filter: (b = (InitPlan 1).col1)
+         Filter: (b = (InitPlan expr_1).col1)
    ->  Seq Scan on ab_a3_b2 ab_8 (never executed)
-         Filter: (b = (InitPlan 1).col1)
+         Filter: (b = (InitPlan expr_1).col1)
    ->  Seq Scan on ab_a3_b3 ab_9 (never executed)
-         Filter: (b = (InitPlan 1).col1)
+         Filter: (b = (InitPlan expr_1).col1)
 (42 rows)

 -- Another UNION ALL test, but containing a mix of exec init and exec run-time pruning.
@@ -2865,27 +2865,27 @@ union all
 ) ab where a = $1 and b = (select -10);
 -- Ensure the xy_1 subplan is not pruned.
 explain (analyze, costs off, summary off, timing off, buffers off) execute ab_q6(1);
-                       QUERY PLAN
---------------------------------------------------------
+                         QUERY PLAN
+-------------------------------------------------------------
  Append (actual rows=0.00 loops=1)
    Subplans Removed: 12
-   InitPlan 1
+   InitPlan expr_1
      ->  Result (actual rows=1.00 loops=1)
    ->  Seq Scan on ab_a1_b1 ab_1 (never executed)
-         Filter: ((a = $1) AND (b = (InitPlan 1).col1))
+         Filter: ((a = $1) AND (b = (InitPlan expr_1).col1))
    ->  Seq Scan on ab_a1_b2 ab_2 (never executed)
-         Filter: ((a = $1) AND (b = (InitPlan 1).col1))
+         Filter: ((a = $1) AND (b = (InitPlan expr_1).col1))
    ->  Seq Scan on ab_a1_b3 ab_3 (never executed)
-         Filter: ((a = $1) AND (b = (InitPlan 1).col1))
+         Filter: ((a = $1) AND (b = (InitPlan expr_1).col1))
    ->  Seq Scan on xy_1 (actual rows=0.00 loops=1)
-         Filter: ((x = $1) AND (y = (InitPlan 1).col1))
+         Filter: ((x = $1) AND (y = (InitPlan expr_1).col1))
          Rows Removed by Filter: 1
    ->  Seq Scan on ab_a1_b1 ab_4 (never executed)
-         Filter: ((a = $1) AND (b = (InitPlan 1).col1))
+         Filter: ((a = $1) AND (b = (InitPlan expr_1).col1))
    ->  Seq Scan on ab_a1_b2 ab_5 (never executed)
-         Filter: ((a = $1) AND (b = (InitPlan 1).col1))
+         Filter: ((a = $1) AND (b = (InitPlan expr_1).col1))
    ->  Seq Scan on ab_a1_b3 ab_6 (never executed)
-         Filter: ((a = $1) AND (b = (InitPlan 1).col1))
+         Filter: ((a = $1) AND (b = (InitPlan expr_1).col1))
 (19 rows)

 -- Ensure we see just the xy_1 row.
@@ -2971,7 +2971,7 @@ update ab_a1 set b = 3 from ab_a2 where ab_a2.b = (select 1);');
    Update on ab_a1_b1 ab_a1_1
    Update on ab_a1_b2 ab_a1_2
    Update on ab_a1_b3 ab_a1_3
-   InitPlan 1
+   InitPlan expr_1
      ->  Result (actual rows=1.00 loops=1)
    ->  Nested Loop (actual rows=3.00 loops=1)
          ->  Append (actual rows=3.00 loops=1)
@@ -2982,11 +2982,11 @@ update ab_a1 set b = 3 from ab_a2 where ab_a2.b = (select 1);');
                Storage: Memory  Maximum Storage: NkB
                ->  Append (actual rows=1.00 loops=1)
                      ->  Seq Scan on ab_a2_b1 ab_a2_1 (actual rows=1.00 loops=1)
-                           Filter: (b = (InitPlan 1).col1)
+                           Filter: (b = (InitPlan expr_1).col1)
                      ->  Seq Scan on ab_a2_b2 ab_a2_2 (never executed)
-                           Filter: (b = (InitPlan 1).col1)
+                           Filter: (b = (InitPlan expr_1).col1)
                      ->  Seq Scan on ab_a2_b3 ab_a2_3 (never executed)
-                           Filter: (b = (InitPlan 1).col1)
+                           Filter: (b = (InitPlan expr_1).col1)
 (20 rows)

 select tableoid::regclass, * from ab;
@@ -3356,12 +3356,12 @@ select * from listp where a = (select null::int);
                       QUERY PLAN
 ------------------------------------------------------
  Append (actual rows=0.00 loops=1)
-   InitPlan 1
+   InitPlan expr_1
      ->  Result (actual rows=1.00 loops=1)
    ->  Seq Scan on listp_1_1 listp_1 (never executed)
-         Filter: (a = (InitPlan 1).col1)
+         Filter: (a = (InitPlan expr_1).col1)
    ->  Seq Scan on listp_2_1 listp_2 (never executed)
-         Filter: (a = (InitPlan 1).col1)
+         Filter: (a = (InitPlan expr_1).col1)
 (7 rows)

 drop table listp;
@@ -3500,14 +3500,14 @@ prepare ps1 as
   select * from mc3p where a = $1 and abs(b) < (select 3);
 explain (analyze, costs off, summary off, timing off, buffers off)
 execute ps1(1);
-                         QUERY PLAN
--------------------------------------------------------------
+                            QUERY PLAN
+------------------------------------------------------------------
  Append (actual rows=1.00 loops=1)
    Subplans Removed: 2
-   InitPlan 1
+   InitPlan expr_1
      ->  Result (actual rows=1.00 loops=1)
    ->  Seq Scan on mc3p1 mc3p_1 (actual rows=1.00 loops=1)
-         Filter: ((a = $1) AND (abs(b) < (InitPlan 1).col1))
+         Filter: ((a = $1) AND (abs(b) < (InitPlan expr_1).col1))
 (6 rows)

 deallocate ps1;
@@ -3515,16 +3515,16 @@ prepare ps2 as
   select * from mc3p where a <= $1 and abs(b) < (select 3);
 explain (analyze, costs off, summary off, timing off, buffers off)
 execute ps2(1);
-                          QUERY PLAN
---------------------------------------------------------------
+                            QUERY PLAN
+-------------------------------------------------------------------
  Append (actual rows=2.00 loops=1)
    Subplans Removed: 1
-   InitPlan 1
+   InitPlan expr_1
      ->  Result (actual rows=1.00 loops=1)
    ->  Seq Scan on mc3p0 mc3p_1 (actual rows=1.00 loops=1)
-         Filter: ((a <= $1) AND (abs(b) < (InitPlan 1).col1))
+         Filter: ((a <= $1) AND (abs(b) < (InitPlan expr_1).col1))
    ->  Seq Scan on mc3p1 mc3p_2 (actual rows=1.00 loops=1)
-         Filter: ((a <= $1) AND (abs(b) < (InitPlan 1).col1))
+         Filter: ((a <= $1) AND (abs(b) < (InitPlan expr_1).col1))
 (8 rows)

 deallocate ps2;
@@ -3540,14 +3540,14 @@ select * from boolp where a = (select value from boolvalues where value);
                           QUERY PLAN
 --------------------------------------------------------------
  Append (actual rows=0.00 loops=1)
-   InitPlan 1
+   InitPlan expr_1
      ->  Seq Scan on boolvalues (actual rows=1.00 loops=1)
            Filter: value
            Rows Removed by Filter: 1
    ->  Seq Scan on boolp_f boolp_1 (never executed)
-         Filter: (a = (InitPlan 1).col1)
+         Filter: (a = (InitPlan expr_1).col1)
    ->  Seq Scan on boolp_t boolp_2 (actual rows=0.00 loops=1)
-         Filter: (a = (InitPlan 1).col1)
+         Filter: (a = (InitPlan expr_1).col1)
 (9 rows)

 explain (analyze, costs off, summary off, timing off, buffers off)
@@ -3555,14 +3555,14 @@ select * from boolp where a = (select value from boolvalues where not value);
                           QUERY PLAN
 --------------------------------------------------------------
  Append (actual rows=0.00 loops=1)
-   InitPlan 1
+   InitPlan expr_1
      ->  Seq Scan on boolvalues (actual rows=1.00 loops=1)
            Filter: (NOT value)
            Rows Removed by Filter: 1
    ->  Seq Scan on boolp_f boolp_1 (actual rows=0.00 loops=1)
-         Filter: (a = (InitPlan 1).col1)
+         Filter: (a = (InitPlan expr_1).col1)
    ->  Seq Scan on boolp_t boolp_2 (never executed)
-         Filter: (a = (InitPlan 1).col1)
+         Filter: (a = (InitPlan expr_1).col1)
 (9 rows)

 drop table boolp;
@@ -3654,22 +3654,22 @@ explain (analyze, costs off, summary off, timing off, buffers off) select * from
 --------------------------------------------------------------------------------------------------
  Merge Append (actual rows=20.00 loops=1)
    Sort Key: ma_test.b
-   InitPlan 2
+   InitPlan expr_1
      ->  Result (actual rows=1.00 loops=1)
            Replaces: MinMaxAggregate
-           InitPlan 1
+           InitPlan minmax_1
              ->  Limit (actual rows=1.00 loops=1)
                    ->  Index Scan using ma_test_p2_b_idx on ma_test_p2 (actual rows=1.00 loops=1)
                          Index Cond: (b IS NOT NULL)
                          Index Searches: 1
    ->  Index Scan using ma_test_p1_b_idx on ma_test_p1 ma_test_1 (never executed)
-         Filter: (a >= (InitPlan 2).col1)
+         Filter: (a >= (InitPlan expr_1).col1)
          Index Searches: 0
    ->  Index Scan using ma_test_p2_b_idx on ma_test_p2 ma_test_2 (actual rows=10.00 loops=1)
-         Filter: (a >= (InitPlan 2).col1)
+         Filter: (a >= (InitPlan expr_1).col1)
          Index Searches: 1
    ->  Index Scan using ma_test_p3_b_idx on ma_test_p3 ma_test_3 (actual rows=10.00 loops=1)
-         Filter: (a >= (InitPlan 2).col1)
+         Filter: (a >= (InitPlan expr_1).col1)
          Index Searches: 1
 (19 rows)

@@ -4043,17 +4043,17 @@ from (
       select 1, 1, 1
      ) s(a, b, c)
 where s.a = 1 and s.b = 1 and s.c = (select 1);
-                            QUERY PLAN
--------------------------------------------------------------------
+                               QUERY PLAN
+------------------------------------------------------------------------
  Append
-   InitPlan 1
+   InitPlan expr_1
      ->  Result
    ->  Seq Scan on p1 p
-         Filter: ((a = 1) AND (b = 1) AND (c = (InitPlan 1).col1))
+         Filter: ((a = 1) AND (b = 1) AND (c = (InitPlan expr_1).col1))
    ->  Seq Scan on q111 q1
-         Filter: ((a = 1) AND (b = 1) AND (c = (InitPlan 1).col1))
+         Filter: ((a = 1) AND (b = 1) AND (c = (InitPlan expr_1).col1))
    ->  Result
-         One-Time Filter: (1 = (InitPlan 1).col1)
+         One-Time Filter: (1 = (InitPlan expr_1).col1)
 (9 rows)

 select *
@@ -4081,18 +4081,18 @@ from (
      ) s(a, b, c)
 where s.a = $1 and s.b = $2 and s.c = (select 1);
 explain (costs off) execute q (1, 1);
-                                  QUERY PLAN
-------------------------------------------------------------------------------
+                                    QUERY PLAN
+-----------------------------------------------------------------------------------
  Append
    Subplans Removed: 1
-   InitPlan 1
+   InitPlan expr_1
      ->  Result
    ->  Seq Scan on p1 p
-         Filter: ((a = $1) AND (b = $2) AND (c = (InitPlan 1).col1))
+         Filter: ((a = $1) AND (b = $2) AND (c = (InitPlan expr_1).col1))
    ->  Seq Scan on q111 q1
-         Filter: ((a = $1) AND (b = $2) AND (c = (InitPlan 1).col1))
+         Filter: ((a = $1) AND (b = $2) AND (c = (InitPlan expr_1).col1))
    ->  Result
-         One-Time Filter: ((1 = $1) AND (1 = $2) AND (1 = (InitPlan 1).col1))
+         One-Time Filter: ((1 = $1) AND (1 = $2) AND (1 = (InitPlan expr_1).col1))
 (10 rows)

 execute q (1, 1);
@@ -4110,11 +4110,11 @@ create table listp2 partition of listp for values in(2) partition by list(b);
 create table listp2_10 partition of listp2 for values in (10);
 explain (analyze, costs off, summary off, timing off, buffers off)
 select * from listp where a = (select 2) and b <> 10;
-                     QUERY PLAN
------------------------------------------------------
+                       QUERY PLAN
+--------------------------------------------------------
  Seq Scan on listp1 listp (actual rows=0.00 loops=1)
-   Filter: ((b <> 10) AND (a = (InitPlan 1).col1))
-   InitPlan 1
+   Filter: ((b <> 10) AND (a = (InitPlan expr_1).col1))
+   InitPlan expr_1
      ->  Result (never executed)
 (4 rows)

@@ -4182,13 +4182,13 @@ select explain_parallel_append('select * from listp where a = (select 1);');
  Gather (actual rows=N loops=N)
    Workers Planned: 2
    Workers Launched: N
-   InitPlan 1
+   InitPlan expr_1
      ->  Result (actual rows=N loops=N)
    ->  Parallel Append (actual rows=N loops=N)
          ->  Seq Scan on listp_12_1 listp_1 (actual rows=N loops=N)
-               Filter: (a = (InitPlan 1).col1)
+               Filter: (a = (InitPlan expr_1).col1)
          ->  Parallel Seq Scan on listp_12_2 listp_2 (never executed)
-               Filter: (a = (InitPlan 1).col1)
+               Filter: (a = (InitPlan expr_1).col1)
 (10 rows)

 -- Like the above but throw some more complexity at the planner by adding
@@ -4205,19 +4205,19 @@ select * from listp where a = (select 2);');
    Workers Launched: N
    ->  Parallel Append (actual rows=N loops=N)
          ->  Parallel Append (actual rows=N loops=N)
-               InitPlan 2
+               InitPlan expr_2
                  ->  Result (actual rows=N loops=N)
                ->  Seq Scan on listp_12_1 listp_1 (never executed)
-                     Filter: (a = (InitPlan 2).col1)
+                     Filter: (a = (InitPlan expr_2).col1)
                ->  Parallel Seq Scan on listp_12_2 listp_2 (actual rows=N loops=N)
-                     Filter: (a = (InitPlan 2).col1)
+                     Filter: (a = (InitPlan expr_2).col1)
          ->  Parallel Append (actual rows=N loops=N)
-               InitPlan 1
+               InitPlan expr_1
                  ->  Result (actual rows=N loops=N)
                ->  Seq Scan on listp_12_1 listp_4 (actual rows=N loops=N)
-                     Filter: (a = (InitPlan 1).col1)
+                     Filter: (a = (InitPlan expr_1).col1)
                ->  Parallel Seq Scan on listp_12_2 listp_5 (never executed)
-                     Filter: (a = (InitPlan 1).col1)
+                     Filter: (a = (InitPlan expr_1).col1)
 (18 rows)

 drop table listp;
@@ -4240,23 +4240,23 @@ select * from rangep where b IN((select 1),(select 2)) order by a;
                                                   QUERY PLAN
 ---------------------------------------------------------------------------------------------------------------
  Append (actual rows=0.00 loops=1)
-   InitPlan 1
+   InitPlan expr_1
      ->  Result (actual rows=1.00 loops=1)
-   InitPlan 2
+   InitPlan expr_2
      ->  Result (actual rows=1.00 loops=1)
    ->  Merge Append (actual rows=0.00 loops=1)
          Sort Key: rangep_2.a
          ->  Index Scan using rangep_0_to_100_1_a_idx on rangep_0_to_100_1 rangep_2 (actual rows=0.00 loops=1)
-               Filter: (b = ANY (ARRAY[(InitPlan 1).col1, (InitPlan 2).col1]))
+               Filter: (b = ANY (ARRAY[(InitPlan expr_1).col1, (InitPlan expr_2).col1]))
                Index Searches: 1
          ->  Index Scan using rangep_0_to_100_2_a_idx on rangep_0_to_100_2 rangep_3 (actual rows=0.00 loops=1)
-               Filter: (b = ANY (ARRAY[(InitPlan 1).col1, (InitPlan 2).col1]))
+               Filter: (b = ANY (ARRAY[(InitPlan expr_1).col1, (InitPlan expr_2).col1]))
                Index Searches: 1
          ->  Index Scan using rangep_0_to_100_3_a_idx on rangep_0_to_100_3 rangep_4 (never executed)
-               Filter: (b = ANY (ARRAY[(InitPlan 1).col1, (InitPlan 2).col1]))
+               Filter: (b = ANY (ARRAY[(InitPlan expr_1).col1, (InitPlan expr_2).col1]))
                Index Searches: 0
    ->  Index Scan using rangep_100_to_200_a_idx on rangep_100_to_200 rangep_5 (actual rows=0.00 loops=1)
-         Filter: (b = ANY (ARRAY[(InitPlan 1).col1, (InitPlan 2).col1]))
+         Filter: (b = ANY (ARRAY[(InitPlan expr_1).col1, (InitPlan expr_2).col1]))
          Index Searches: 1
 (19 rows)

diff --git a/src/test/regress/expected/portals.out b/src/test/regress/expected/portals.out
index 06726ed4ab7..31f77abc446 100644
--- a/src/test/regress/expected/portals.out
+++ b/src/test/regress/expected/portals.out
@@ -1472,18 +1472,18 @@ rollback;
 -- Check handling of non-backwards-scan-capable plans with scroll cursors
 begin;
 explain (costs off) declare c1 cursor for select (select 42) as x;
-   QUERY PLAN
-----------------
+    QUERY PLAN
+-------------------
  Result
-   InitPlan 1
+   InitPlan expr_1
      ->  Result
 (3 rows)

 explain (costs off) declare c1 scroll cursor for select (select 42) as x;
-   QUERY PLAN
-----------------
+    QUERY PLAN
+-------------------
  Materialize
-   InitPlan 1
+   InitPlan expr_1
      ->  Result
    ->  Result
 (4 rows)
diff --git a/src/test/regress/expected/predicate.out b/src/test/regress/expected/predicate.out
index 304b6868b90..66fb0854b88 100644
--- a/src/test/regress/expected/predicate.out
+++ b/src/test/regress/expected/predicate.out
@@ -247,11 +247,11 @@ SELECT * FROM pred_tab t1
                        QUERY PLAN
 ---------------------------------------------------------
  Nested Loop Left Join
-   Join Filter: EXISTS(SubPlan 1)
+   Join Filter: EXISTS(SubPlan exists_1)
    ->  Seq Scan on pred_tab t1
    ->  Materialize
          ->  Seq Scan on pred_tab t2
-   SubPlan 1
+   SubPlan exists_1
      ->  Nested Loop
            ->  Nested Loop
                  ->  Nested Loop
@@ -274,8 +274,8 @@ SELECT * FROM pred_tab t1
                  QUERY PLAN
 --------------------------------------------
  Nested Loop Left Join
-   Join Filter: (InitPlan 1).col1
-   InitPlan 1
+   Join Filter: (InitPlan exists_1).col1
+   InitPlan exists_1
      ->  Result
            Replaces: Join on t3, t4, t5, t6
            One-Time Filter: false
diff --git a/src/test/regress/expected/returning.out b/src/test/regress/expected/returning.out
index 341b689f766..d02c2ceab53 100644
--- a/src/test/regress/expected/returning.out
+++ b/src/test/regress/expected/returning.out
@@ -548,16 +548,16 @@ INSERT INTO foo VALUES (5, 'subquery test')
                           QUERY PLAN
 ---------------------------------------------------------------
  Insert on pg_temp.foo
-   Output: (SubPlan 1), (SubPlan 2)
+   Output: (SubPlan expr_1), (SubPlan expr_2)
    ->  Result
          Output: 5, 'subquery test'::text, 42, '99'::bigint
-   SubPlan 1
+   SubPlan expr_1
      ->  Aggregate
            Output: max((old.f4 + x.x))
            ->  Function Scan on pg_catalog.generate_series x
                  Output: x.x
                  Function Call: generate_series(1, 10)
-   SubPlan 2
+   SubPlan expr_2
      ->  Aggregate
            Output: max((new.f4 + x_1.x))
            ->  Function Scan on pg_catalog.generate_series x_1
@@ -578,26 +578,26 @@ UPDATE foo SET f4 = 100 WHERE f1 = 5
   RETURNING (SELECT old.f4 = new.f4),
             (SELECT max(old.f4 + x) FROM generate_series(1, 10) x) old_max,
             (SELECT max(new.f4 + x) FROM generate_series(1, 10) x) new_max;
-                          QUERY PLAN
----------------------------------------------------------------
+                           QUERY PLAN
+----------------------------------------------------------------
  Update on pg_temp.foo
-   Output: (SubPlan 1), (SubPlan 2), (SubPlan 3)
+   Output: (SubPlan expr_1), (SubPlan expr_2), (SubPlan expr_3)
    Update on pg_temp.foo foo_1
    ->  Result
          Output: '100'::bigint, foo_1.tableoid, foo_1.ctid
          ->  Seq Scan on pg_temp.foo foo_1
                Output: foo_1.tableoid, foo_1.ctid
                Filter: (foo_1.f1 = 5)
-   SubPlan 1
+   SubPlan expr_1
      ->  Result
            Output: (old.f4 = new.f4)
-   SubPlan 2
+   SubPlan expr_2
      ->  Aggregate
            Output: max((old.f4 + x.x))
            ->  Function Scan on pg_catalog.generate_series x
                  Output: x.x
                  Function Call: generate_series(1, 10)
-   SubPlan 3
+   SubPlan expr_3
      ->  Aggregate
            Output: max((new.f4 + x_1.x))
            ->  Function Scan on pg_catalog.generate_series x_1
@@ -621,18 +621,18 @@ DELETE FROM foo WHERE f1 = 5
                           QUERY PLAN
 ---------------------------------------------------------------
  Delete on pg_temp.foo
-   Output: (SubPlan 1), (SubPlan 2)
+   Output: (SubPlan expr_1), (SubPlan expr_2)
    Delete on pg_temp.foo foo_1
    ->  Seq Scan on pg_temp.foo foo_1
          Output: foo_1.tableoid, foo_1.ctid
          Filter: (foo_1.f1 = 5)
-   SubPlan 1
+   SubPlan expr_1
      ->  Aggregate
            Output: max((old.f4 + x.x))
            ->  Function Scan on pg_catalog.generate_series x
                  Output: x.x
                  Function Call: generate_series(1, 10)
-   SubPlan 2
+   SubPlan expr_2
      ->  Aggregate
            Output: max((new.f4 + x_1.x))
            ->  Function Scan on pg_catalog.generate_series x_1
diff --git a/src/test/regress/expected/rowsecurity.out b/src/test/regress/expected/rowsecurity.out
index 7153ebba521..5a172c5d91c 100644
--- a/src/test/regress/expected/rowsecurity.out
+++ b/src/test/regress/expected/rowsecurity.out
@@ -265,27 +265,27 @@ NOTICE:  f_leak => awesome science fiction
 (5 rows)

 EXPLAIN (COSTS OFF) SELECT * FROM document WHERE f_leak(dtitle);
-                          QUERY PLAN
---------------------------------------------------------------
+                            QUERY PLAN
+-------------------------------------------------------------------
  Seq Scan on document
-   Filter: ((dlevel <= (InitPlan 1).col1) AND f_leak(dtitle))
-   InitPlan 1
+   Filter: ((dlevel <= (InitPlan expr_1).col1) AND f_leak(dtitle))
+   InitPlan expr_1
      ->  Index Scan using uaccount_pkey on uaccount
            Index Cond: (pguser = CURRENT_USER)
 (5 rows)

 EXPLAIN (COSTS OFF) SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
-                                QUERY PLAN
---------------------------------------------------------------------------
+                                  QUERY PLAN
+-------------------------------------------------------------------------------
  Hash Join
    Hash Cond: (category.cid = document.cid)
-   InitPlan 1
+   InitPlan expr_1
      ->  Index Scan using uaccount_pkey on uaccount
            Index Cond: (pguser = CURRENT_USER)
    ->  Seq Scan on category
    ->  Hash
          ->  Seq Scan on document
-               Filter: ((dlevel <= (InitPlan 1).col1) AND f_leak(dtitle))
+               Filter: ((dlevel <= (InitPlan expr_1).col1) AND f_leak(dtitle))
 (9 rows)

 -- viewpoint from regress_rls_dave
@@ -329,27 +329,27 @@ NOTICE:  f_leak => awesome technology book
 (7 rows)

 EXPLAIN (COSTS OFF) SELECT * FROM document WHERE f_leak(dtitle);
-                                                 QUERY PLAN
--------------------------------------------------------------------------------------------------------------
+                                                    QUERY PLAN
+------------------------------------------------------------------------------------------------------------------
  Seq Scan on document
-   Filter: ((cid <> 44) AND (cid <> 44) AND (cid < 50) AND (dlevel <= (InitPlan 1).col1) AND f_leak(dtitle))
-   InitPlan 1
+   Filter: ((cid <> 44) AND (cid <> 44) AND (cid < 50) AND (dlevel <= (InitPlan expr_1).col1) AND f_leak(dtitle))
+   InitPlan expr_1
      ->  Index Scan using uaccount_pkey on uaccount
            Index Cond: (pguser = CURRENT_USER)
 (5 rows)

 EXPLAIN (COSTS OFF) SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
-                                                       QUERY PLAN
  

--------------------------------------------------------------------------------------------------------------------------
+                                                          QUERY PLAN
       

+------------------------------------------------------------------------------------------------------------------------------
  Hash Join
    Hash Cond: (category.cid = document.cid)
-   InitPlan 1
+   InitPlan expr_1
      ->  Index Scan using uaccount_pkey on uaccount
            Index Cond: (pguser = CURRENT_USER)
    ->  Seq Scan on category
    ->  Hash
          ->  Seq Scan on document
-               Filter: ((cid <> 44) AND (cid <> 44) AND (cid < 50) AND (dlevel <= (InitPlan 1).col1) AND
f_leak(dtitle))
+               Filter: ((cid <> 44) AND (cid <> 44) AND (cid < 50) AND (dlevel <= (InitPlan expr_1).col1) AND
f_leak(dtitle))
 (9 rows)

 -- 44 would technically fail for both p2r and p1r, but we should get an error
@@ -987,18 +987,18 @@ NOTICE:  f_leak => my first satire
 (4 rows)

 EXPLAIN (COSTS OFF) SELECT * FROM part_document WHERE f_leak(dtitle);
-                             QUERY PLAN
---------------------------------------------------------------------
+                               QUERY PLAN
+-------------------------------------------------------------------------
  Append
-   InitPlan 1
+   InitPlan expr_1
      ->  Index Scan using uaccount_pkey on uaccount
            Index Cond: (pguser = CURRENT_USER)
    ->  Seq Scan on part_document_fiction part_document_1
-         Filter: ((dlevel <= (InitPlan 1).col1) AND f_leak(dtitle))
+         Filter: ((dlevel <= (InitPlan expr_1).col1) AND f_leak(dtitle))
    ->  Seq Scan on part_document_satire part_document_2
-         Filter: ((dlevel <= (InitPlan 1).col1) AND f_leak(dtitle))
+         Filter: ((dlevel <= (InitPlan expr_1).col1) AND f_leak(dtitle))
    ->  Seq Scan on part_document_nonfiction part_document_3
-         Filter: ((dlevel <= (InitPlan 1).col1) AND f_leak(dtitle))
+         Filter: ((dlevel <= (InitPlan expr_1).col1) AND f_leak(dtitle))
 (10 rows)

 -- viewpoint from regress_rls_carol
@@ -1029,18 +1029,18 @@ NOTICE:  f_leak => awesome technology book
 (10 rows)

 EXPLAIN (COSTS OFF) SELECT * FROM part_document WHERE f_leak(dtitle);
-                             QUERY PLAN
---------------------------------------------------------------------
+                               QUERY PLAN
+-------------------------------------------------------------------------
  Append
-   InitPlan 1
+   InitPlan expr_1
      ->  Index Scan using uaccount_pkey on uaccount
            Index Cond: (pguser = CURRENT_USER)
    ->  Seq Scan on part_document_fiction part_document_1
-         Filter: ((dlevel <= (InitPlan 1).col1) AND f_leak(dtitle))
+         Filter: ((dlevel <= (InitPlan expr_1).col1) AND f_leak(dtitle))
    ->  Seq Scan on part_document_satire part_document_2
-         Filter: ((dlevel <= (InitPlan 1).col1) AND f_leak(dtitle))
+         Filter: ((dlevel <= (InitPlan expr_1).col1) AND f_leak(dtitle))
    ->  Seq Scan on part_document_nonfiction part_document_3
-         Filter: ((dlevel <= (InitPlan 1).col1) AND f_leak(dtitle))
+         Filter: ((dlevel <= (InitPlan expr_1).col1) AND f_leak(dtitle))
 (10 rows)

 -- viewpoint from regress_rls_dave
@@ -1059,11 +1059,11 @@ NOTICE:  f_leak => awesome science fiction
 (4 rows)

 EXPLAIN (COSTS OFF) SELECT * FROM part_document WHERE f_leak(dtitle);
-                                 QUERY PLAN
------------------------------------------------------------------------------
+                                    QUERY PLAN
+----------------------------------------------------------------------------------
  Seq Scan on part_document_fiction part_document
-   Filter: ((cid < 55) AND (dlevel <= (InitPlan 1).col1) AND f_leak(dtitle))
-   InitPlan 1
+   Filter: ((cid < 55) AND (dlevel <= (InitPlan expr_1).col1) AND f_leak(dtitle))
+   InitPlan expr_1
      ->  Index Scan using uaccount_pkey on uaccount
            Index Cond: (pguser = CURRENT_USER)
 (5 rows)
@@ -1137,11 +1137,11 @@ NOTICE:  f_leak => awesome science fiction
 (4 rows)

 EXPLAIN (COSTS OFF) SELECT * FROM part_document WHERE f_leak(dtitle);
-                                 QUERY PLAN
------------------------------------------------------------------------------
+                                    QUERY PLAN
+----------------------------------------------------------------------------------
  Seq Scan on part_document_fiction part_document
-   Filter: ((cid < 55) AND (dlevel <= (InitPlan 1).col1) AND f_leak(dtitle))
-   InitPlan 1
+   Filter: ((cid < 55) AND (dlevel <= (InitPlan expr_1).col1) AND f_leak(dtitle))
+   InitPlan expr_1
      ->  Index Scan using uaccount_pkey on uaccount
            Index Cond: (pguser = CURRENT_USER)
 (5 rows)
@@ -1176,18 +1176,18 @@ NOTICE:  f_leak => awesome technology book
 (11 rows)

 EXPLAIN (COSTS OFF) SELECT * FROM part_document WHERE f_leak(dtitle);
-                             QUERY PLAN
---------------------------------------------------------------------
+                               QUERY PLAN
+-------------------------------------------------------------------------
  Append
-   InitPlan 1
+   InitPlan expr_1
      ->  Index Scan using uaccount_pkey on uaccount
            Index Cond: (pguser = CURRENT_USER)
    ->  Seq Scan on part_document_fiction part_document_1
-         Filter: ((dlevel <= (InitPlan 1).col1) AND f_leak(dtitle))
+         Filter: ((dlevel <= (InitPlan expr_1).col1) AND f_leak(dtitle))
    ->  Seq Scan on part_document_satire part_document_2
-         Filter: ((dlevel <= (InitPlan 1).col1) AND f_leak(dtitle))
+         Filter: ((dlevel <= (InitPlan expr_1).col1) AND f_leak(dtitle))
    ->  Seq Scan on part_document_nonfiction part_document_3
-         Filter: ((dlevel <= (InitPlan 1).col1) AND f_leak(dtitle))
+         Filter: ((dlevel <= (InitPlan expr_1).col1) AND f_leak(dtitle))
 (10 rows)

 -- only owner can change policies
@@ -1437,11 +1437,11 @@ NOTICE:  f_leak => 03b26944890929ff751653acb2f2af79
 (1 row)

 EXPLAIN (COSTS OFF) SELECT * FROM only s1 WHERE f_leak(b);
-                          QUERY PLAN
----------------------------------------------------------------
+                            QUERY PLAN
+-------------------------------------------------------------------
  Seq Scan on s1
-   Filter: ((ANY (a = (hashed SubPlan 1).col1)) AND f_leak(b))
-   SubPlan 1
+   Filter: ((ANY (a = (hashed SubPlan any_1).col1)) AND f_leak(b))
+   SubPlan any_1
      ->  Seq Scan on s2
            Filter: (((x % 2) = 0) AND (y ~~ '%2f%'::text))
 (5 rows)
@@ -1457,11 +1457,11 @@ NOTICE:  f_leak => 03b26944890929ff751653acb2f2af79
 (1 row)

 EXPLAIN (COSTS OFF) SELECT * FROM s1 WHERE f_leak(b);
-                          QUERY PLAN
----------------------------------------------------------------
+                            QUERY PLAN
+-------------------------------------------------------------------
  Seq Scan on s1
-   Filter: ((ANY (a = (hashed SubPlan 1).col1)) AND f_leak(b))
-   SubPlan 1
+   Filter: ((ANY (a = (hashed SubPlan any_1).col1)) AND f_leak(b))
+   SubPlan any_1
      ->  Seq Scan on s2
            Filter: (((x % 2) = 0) AND (y ~~ '%af%'::text))
 (5 rows)
@@ -1477,11 +1477,11 @@ EXPLAIN (COSTS OFF) SELECT (SELECT x FROM s1 LIMIT 1) xx, * FROM s2 WHERE y like
 -------------------------------------------------------------------------
  Seq Scan on s2
    Filter: (((x % 2) = 0) AND (y ~~ '%28%'::text))
-   SubPlan 2
+   SubPlan expr_1
      ->  Limit
            ->  Seq Scan on s1
-                 Filter: (ANY (a = (hashed SubPlan 1).col1))
-                 SubPlan 1
+                 Filter: (ANY (a = (hashed SubPlan any_1).col1))
+                 SubPlan any_1
                    ->  Seq Scan on s2 s2_1
                          Filter: (((x % 2) = 0) AND (y ~~ '%af%'::text))
 (9 rows)
@@ -2717,11 +2717,11 @@ NOTICE:  f_leak => bbb
 (1 row)

 EXPLAIN (COSTS OFF) SELECT * FROM rls_view;
-                                      QUERY PLAN
----------------------------------------------------------------------------------------
+                                        QUERY PLAN
+-------------------------------------------------------------------------------------------
  Seq Scan on z1
-   Filter: ((NOT (ANY (a = (hashed SubPlan 1).col1))) AND ((a % 2) = 0) AND f_leak(b))
-   SubPlan 1
+   Filter: ((NOT (ANY (a = (hashed SubPlan any_1).col1))) AND ((a % 2) = 0) AND f_leak(b))
+   SubPlan any_1
      ->  Seq Scan on z1_blacklist
 (4 rows)

@@ -2735,11 +2735,11 @@ NOTICE:  f_leak => bbb
 (1 row)

 EXPLAIN (COSTS OFF) SELECT * FROM rls_view;
-                                      QUERY PLAN
----------------------------------------------------------------------------------------
+                                        QUERY PLAN
+-------------------------------------------------------------------------------------------
  Seq Scan on z1
-   Filter: ((NOT (ANY (a = (hashed SubPlan 1).col1))) AND ((a % 2) = 0) AND f_leak(b))
-   SubPlan 1
+   Filter: ((NOT (ANY (a = (hashed SubPlan any_1).col1))) AND ((a % 2) = 0) AND f_leak(b))
+   SubPlan any_1
      ->  Seq Scan on z1_blacklist
 (4 rows)

@@ -2907,11 +2907,11 @@ NOTICE:  f_leak => bbb
 (1 row)

 EXPLAIN (COSTS OFF) SELECT * FROM rls_view;
-                                      QUERY PLAN
----------------------------------------------------------------------------------------
+                                        QUERY PLAN
+-------------------------------------------------------------------------------------------
  Seq Scan on z1
-   Filter: ((NOT (ANY (a = (hashed SubPlan 1).col1))) AND ((a % 2) = 0) AND f_leak(b))
-   SubPlan 1
+   Filter: ((NOT (ANY (a = (hashed SubPlan any_1).col1))) AND ((a % 2) = 0) AND f_leak(b))
+   SubPlan any_1
      ->  Seq Scan on z1_blacklist
 (4 rows)

@@ -2933,11 +2933,11 @@ NOTICE:  f_leak => aba
 (1 row)

 EXPLAIN (COSTS OFF) SELECT * FROM rls_view;
-                                      QUERY PLAN
----------------------------------------------------------------------------------------
+                                        QUERY PLAN
+-------------------------------------------------------------------------------------------
  Seq Scan on z1
-   Filter: ((NOT (ANY (a = (hashed SubPlan 1).col1))) AND ((a % 2) = 1) AND f_leak(b))
-   SubPlan 1
+   Filter: ((NOT (ANY (a = (hashed SubPlan any_1).col1))) AND ((a % 2) = 1) AND f_leak(b))
+   SubPlan any_1
      ->  Seq Scan on z1_blacklist
 (4 rows)

diff --git a/src/test/regress/expected/rowtypes.out b/src/test/regress/expected/rowtypes.out
index dd52d96d50f..677ad2ab9ad 100644
--- a/src/test/regress/expected/rowtypes.out
+++ b/src/test/regress/expected/rowtypes.out
@@ -1251,19 +1251,19 @@ with cte(c) as materialized (select row(1, 2)),
 select * from cte2 as t
 where (select * from (select c as c1) s
        where (select (c1).f1 > 0)) is not null;
-                  QUERY PLAN
-----------------------------------------------
+                    QUERY PLAN
+---------------------------------------------------
  CTE Scan on cte
    Output: cte.c
-   Filter: ((SubPlan 3) IS NOT NULL)
+   Filter: ((SubPlan expr_1) IS NOT NULL)
    CTE cte
      ->  Result
            Output: '(1,2)'::record
-   SubPlan 3
+   SubPlan expr_1
      ->  Result
            Output: cte.c
-           One-Time Filter: (InitPlan 2).col1
-           InitPlan 2
+           One-Time Filter: (InitPlan expr_2).col1
+           InitPlan expr_2
              ->  Result
                    Output: ((cte.c).f1 > 0)
 (13 rows)
diff --git a/src/test/regress/expected/select_parallel.out b/src/test/regress/expected/select_parallel.out
index 0185ef661b1..933921d1860 100644
--- a/src/test/regress/expected/select_parallel.out
+++ b/src/test/regress/expected/select_parallel.out
@@ -156,9 +156,9 @@ explain (costs off)
          ->  Parallel Append
                ->  Parallel Seq Scan on part_pa_test_p1 pa2_1
                ->  Parallel Seq Scan on part_pa_test_p2 pa2_2
-   SubPlan 2
+   SubPlan expr_1
      ->  Result
-   SubPlan 1
+   SubPlan expr_2
      ->  Append
            ->  Seq Scan on part_pa_test_p1 pa1_1
                  Filter: (a = pa2.a)
@@ -302,15 +302,15 @@ alter table tenk2 set (parallel_workers = 0);
 explain (costs off)
     select count(*) from tenk1 where (two, four) not in
     (select hundred, thousand from tenk2 where thousand > 100);
-                                                   QUERY PLAN
-----------------------------------------------------------------------------------------------------------------
+                                                       QUERY PLAN
 

+------------------------------------------------------------------------------------------------------------------------
  Finalize Aggregate
    ->  Gather
          Workers Planned: 4
          ->  Partial Aggregate
                ->  Parallel Seq Scan on tenk1
-                     Filter: (NOT (ANY ((two = (hashed SubPlan 1).col1) AND (four = (hashed SubPlan 1).col2))))
-                     SubPlan 1
+                     Filter: (NOT (ANY ((two = (hashed SubPlan any_1).col1) AND (four = (hashed SubPlan
any_1).col2))))
+                     SubPlan any_1
                        ->  Seq Scan on tenk2
                              Filter: (thousand > 100)
 (9 rows)
@@ -326,11 +326,11 @@ select count(*) from tenk1 where (two, four) not in
 explain (costs off)
     select * from tenk1 where (unique1 + random())::integer not in
     (select ten from tenk2);
-                                              QUERY PLAN
--------------------------------------------------------------------------------------------------------
+                                                QUERY PLAN
+-----------------------------------------------------------------------------------------------------------
  Seq Scan on tenk1
-   Filter: (NOT (ANY ((((unique1)::double precision + random()))::integer = (hashed SubPlan 1).col1)))
-   SubPlan 1
+   Filter: (NOT (ANY ((((unique1)::double precision + random()))::integer = (hashed SubPlan any_1).col1)))
+   SubPlan any_1
      ->  Seq Scan on tenk2
 (4 rows)

@@ -343,10 +343,10 @@ alter table tenk2 set (parallel_workers = 2);
 explain (costs off)
     select count(*) from tenk1
         where tenk1.unique1 = (Select max(tenk2.unique1) from tenk2);
-                      QUERY PLAN
-------------------------------------------------------
+                        QUERY PLAN
+----------------------------------------------------------
  Aggregate
-   InitPlan 1
+   InitPlan expr_1
      ->  Finalize Aggregate
            ->  Gather
                  Workers Planned: 2
@@ -355,7 +355,7 @@ explain (costs off)
    ->  Gather
          Workers Planned: 4
          ->  Parallel Seq Scan on tenk1
-               Filter: (unique1 = (InitPlan 1).col1)
+               Filter: (unique1 = (InitPlan expr_1).col1)
 (11 rows)

 select count(*) from tenk1
@@ -395,17 +395,17 @@ select  count((unique1)) from tenk1 where hundred > 1;
 explain (costs off)
   select count((unique1)) from tenk1
   where hundred = any ((select array_agg(i) from generate_series(1, 100, 15) i)::int[]);
-                             QUERY PLAN
----------------------------------------------------------------------
+                                QUERY PLAN
+--------------------------------------------------------------------------
  Finalize Aggregate
-   InitPlan 1
+   InitPlan expr_1
      ->  Aggregate
            ->  Function Scan on generate_series i
    ->  Gather
          Workers Planned: 4
          ->  Partial Aggregate
                ->  Parallel Index Scan using tenk1_hundred on tenk1
-                     Index Cond: (hundred = ANY ((InitPlan 1).col1))
+                     Index Cond: (hundred = ANY ((InitPlan expr_1).col1))
 (9 rows)

 select count((unique1)) from tenk1
@@ -1224,24 +1224,24 @@ ORDER BY 1;
    ->  Append
          ->  Gather
                Workers Planned: 4
-               InitPlan 1
+               InitPlan expr_1
                  ->  Limit
                        ->  Gather
                              Workers Planned: 4
                              ->  Parallel Seq Scan on tenk1 tenk1_2
                                    Filter: (fivethous = 1)
                ->  Parallel Seq Scan on tenk1
-                     Filter: (fivethous = (InitPlan 1).col1)
+                     Filter: (fivethous = (InitPlan expr_1).col1)
          ->  Gather
                Workers Planned: 4
-               InitPlan 2
+               InitPlan expr_2
                  ->  Limit
                        ->  Gather
                              Workers Planned: 4
                              ->  Parallel Seq Scan on tenk1 tenk1_3
                                    Filter: (fivethous = 1)
                ->  Parallel Seq Scan on tenk1 tenk1_1
-                     Filter: (fivethous = (InitPlan 2).col1)
+                     Filter: (fivethous = (InitPlan expr_2).col1)
 (23 rows)

 -- test interaction with SRFs
@@ -1254,10 +1254,10 @@ ORDER BY 1, 2, 3;
 EXPLAIN (VERBOSE, COSTS OFF)
 SELECT generate_series(1, two), array(select generate_series(1, two))
   FROM tenk1 ORDER BY tenthous;
-                                QUERY PLAN
----------------------------------------------------------------------------
+                                   QUERY PLAN
+---------------------------------------------------------------------------------
  ProjectSet
-   Output: generate_series(1, tenk1.two), ARRAY(SubPlan 1), tenk1.tenthous
+   Output: generate_series(1, tenk1.two), ARRAY(SubPlan array_1), tenk1.tenthous
    ->  Gather Merge
          Output: tenk1.two, tenk1.tenthous
          Workers Planned: 4
@@ -1268,7 +1268,7 @@ SELECT generate_series(1, two), array(select generate_series(1, two))
                      Sort Key: tenk1.tenthous
                      ->  Parallel Seq Scan on public.tenk1
                            Output: tenk1.tenthous, tenk1.two
-   SubPlan 1
+   SubPlan array_1
      ->  ProjectSet
            Output: generate_series(1, tenk1.two)
            ->  Result
@@ -1333,11 +1333,11 @@ SELECT 1 FROM tenk1_vw_sec
                             QUERY PLAN
 -------------------------------------------------------------------
  Subquery Scan on tenk1_vw_sec
-   Filter: ((SubPlan 1) < 100)
+   Filter: ((SubPlan expr_1) < 100)
    ->  Gather
          Workers Planned: 4
          ->  Parallel Index Only Scan using tenk1_unique1 on tenk1
-   SubPlan 1
+   SubPlan expr_1
      ->  Aggregate
            ->  Seq Scan on int4_tbl
                  Filter: (f1 < tenk1_vw_sec.unique1)
diff --git a/src/test/regress/expected/sqljson.out b/src/test/regress/expected/sqljson.out
index 625acf3019a..c7b9e575445 100644
--- a/src/test/regress/expected/sqljson.out
+++ b/src/test/regress/expected/sqljson.out
@@ -1093,8 +1093,8 @@ SELECT JSON_ARRAY(SELECT i FROM (VALUES (1), (2), (NULL), (4)) foo(i) RETURNING
                              QUERY PLAN
 ---------------------------------------------------------------------
  Result
-   Output: (InitPlan 1).col1
-   InitPlan 1
+   Output: (InitPlan expr_1).col1
+   InitPlan expr_1
      ->  Aggregate
            Output: JSON_ARRAYAGG("*VALUES*".column1 RETURNING jsonb)
            ->  Values Scan on "*VALUES*"
diff --git a/src/test/regress/expected/subselect.out b/src/test/regress/expected/subselect.out
index 47b2af7b2e1..cf6b32d1173 100644
--- a/src/test/regress/expected/subselect.out
+++ b/src/test/regress/expected/subselect.out
@@ -205,11 +205,11 @@ SELECT f1 AS "Correlated Field"
 -- Check ROWCOMPARE cases, both correlated and not
 EXPLAIN (VERBOSE, COSTS OFF)
 SELECT ROW(1, 2) = (SELECT f1, f2) AS eq FROM SUBSELECT_TBL;
-                           QUERY PLAN
------------------------------------------------------------------
+                                      QUERY PLAN
+---------------------------------------------------------------------------------------
  Seq Scan on public.subselect_tbl
-   Output: (((1 = (SubPlan 1).col1) AND (2 = (SubPlan 1).col2)))
-   SubPlan 1
+   Output: (((1 = (SubPlan rowcompare_1).col1) AND (2 = (SubPlan rowcompare_1).col2)))
+   SubPlan rowcompare_1
      ->  Result
            Output: subselect_tbl.f1, subselect_tbl.f2
 (5 rows)
@@ -229,11 +229,11 @@ SELECT ROW(1, 2) = (SELECT f1, f2) AS eq FROM SUBSELECT_TBL;

 EXPLAIN (VERBOSE, COSTS OFF)
 SELECT ROW(1, 2) = (SELECT 3, 4) AS eq FROM SUBSELECT_TBL;
-                           QUERY PLAN
------------------------------------------------------------------
+                                      QUERY PLAN
+---------------------------------------------------------------------------------------
  Seq Scan on public.subselect_tbl
-   Output: ((1 = (InitPlan 1).col1) AND (2 = (InitPlan 1).col2))
-   InitPlan 1
+   Output: ((1 = (InitPlan rowcompare_1).col1) AND (2 = (InitPlan rowcompare_1).col2))
+   InitPlan rowcompare_1
      ->  Result
            Output: 3, 4
 (5 rows)
@@ -375,18 +375,18 @@ explain (verbose, costs off) select '42' union all select 43;
 -- check materialization of an initplan reference (bug #14524)
 explain (verbose, costs off)
 select 1 = all (select (select 1));
-                QUERY PLAN
--------------------------------------------
+                   QUERY PLAN
+------------------------------------------------
  Result
-   Output: (ALL (1 = (SubPlan 2).col1))
-   SubPlan 2
+   Output: (ALL (1 = (SubPlan all_1).col1))
+   SubPlan all_1
      ->  Materialize
-           Output: ((InitPlan 1).col1)
-           InitPlan 1
+           Output: ((InitPlan expr_1).col1)
+           InitPlan expr_1
              ->  Result
                    Output: 1
            ->  Result
-                 Output: (InitPlan 1).col1
+                 Output: (InitPlan expr_1).col1
 (10 rows)

 select 1 = all (select (select 1));
@@ -428,8 +428,8 @@ select * from int4_tbl o where exists
               QUERY PLAN
 --------------------------------------
  Seq Scan on int4_tbl o
-   Filter: EXISTS(SubPlan 1)
-   SubPlan 1
+   Filter: EXISTS(SubPlan exists_1)
+   SubPlan exists_1
      ->  Limit
            ->  Seq Scan on int4_tbl i
                  Filter: (f1 = o.f1)
@@ -988,7 +988,7 @@ select (1 = any(array_agg(f1))) = any (select false) from int4_tbl;
 ----------------------------
  Aggregate
    ->  Seq Scan on int4_tbl
-   SubPlan 1
+   SubPlan any_1
      ->  Result
 (4 rows)

@@ -1116,11 +1116,11 @@ select * from outer_text where (f1, f2) not in (select * from inner_text);
 --
 explain (verbose, costs off)
 select 'foo'::text in (select 'bar'::name union all select 'bar'::name);
-                       QUERY PLAN
----------------------------------------------------------
+                         QUERY PLAN
+-------------------------------------------------------------
  Result
-   Output: (ANY ('foo'::text = (hashed SubPlan 1).col1))
-   SubPlan 1
+   Output: (ANY ('foo'::text = (hashed SubPlan any_1).col1))
+   SubPlan any_1
      ->  Append
            ->  Result
                  Output: 'bar'::name
@@ -1140,11 +1140,11 @@ select 'foo'::text in (select 'bar'::name union all select 'bar'::name);
 --
 explain (verbose, costs off)
 select row(row(row(1))) = any (select row(row(1)));
-                       QUERY PLAN
---------------------------------------------------------
+                         QUERY PLAN
+------------------------------------------------------------
  Result
-   Output: (ANY ('("(1)")'::record = (SubPlan 1).col1))
-   SubPlan 1
+   Output: (ANY ('("(1)")'::record = (SubPlan any_1).col1))
+   SubPlan any_1
      ->  Materialize
            Output: '("(1)")'::record
            ->  Result
@@ -1184,11 +1184,11 @@ language sql as 'select $1::text = $2';
 create operator = (procedure=bogus_int8_text_eq, leftarg=int8, rightarg=text);
 explain (costs off)
 select * from int8_tbl where q1 in (select c1 from inner_text);
-                       QUERY PLAN
---------------------------------------------------------
+                         QUERY PLAN
+------------------------------------------------------------
  Seq Scan on int8_tbl
-   Filter: (ANY ((q1)::text = (hashed SubPlan 1).col1))
-   SubPlan 1
+   Filter: (ANY ((q1)::text = (hashed SubPlan any_1).col1))
+   SubPlan any_1
      ->  Seq Scan on inner_text
 (4 rows)

@@ -1205,11 +1205,11 @@ create or replace function bogus_int8_text_eq(int8, text) returns boolean
 language sql as 'select $1::text = $2 and $1::text = $2';
 explain (costs off)
 select * from int8_tbl where q1 in (select c1 from inner_text);
-                                             QUERY PLAN
------------------------------------------------------------------------------------------------------
+                                                 QUERY PLAN
+-------------------------------------------------------------------------------------------------------------
  Seq Scan on int8_tbl
-   Filter: (ANY (((q1)::text = (hashed SubPlan 1).col1) AND ((q1)::text = (hashed SubPlan 1).col1)))
-   SubPlan 1
+   Filter: (ANY (((q1)::text = (hashed SubPlan any_1).col1) AND ((q1)::text = (hashed SubPlan any_1).col1)))
+   SubPlan any_1
      ->  Seq Scan on inner_text
 (4 rows)

@@ -1226,11 +1226,11 @@ create or replace function bogus_int8_text_eq(int8, text) returns boolean
 language sql as 'select $2 = $1::text';
 explain (costs off)
 select * from int8_tbl where q1 in (select c1 from inner_text);
-                   QUERY PLAN
--------------------------------------------------
+                     QUERY PLAN
+-----------------------------------------------------
  Seq Scan on int8_tbl
-   Filter: (ANY ((SubPlan 1).col1 = (q1)::text))
-   SubPlan 1
+   Filter: (ANY ((SubPlan any_1).col1 = (q1)::text))
+   SubPlan any_1
      ->  Materialize
            ->  Seq Scan on inner_text
 (5 rows)
@@ -1249,12 +1249,12 @@ rollback;  -- to get rid of the bogus operator
 explain (costs off)
 select count(*) from tenk1 t
 where (exists(select 1 from tenk1 k where k.unique1 = t.unique2) or ten < 0);
-                                QUERY PLAN
---------------------------------------------------------------------------
+                                   QUERY PLAN
+---------------------------------------------------------------------------------
  Aggregate
    ->  Seq Scan on tenk1 t
-         Filter: ((ANY (unique2 = (hashed SubPlan 2).col1)) OR (ten < 0))
-         SubPlan 2
+         Filter: ((ANY (unique2 = (hashed SubPlan exists_2).col1)) OR (ten < 0))
+         SubPlan exists_2
            ->  Index Only Scan using tenk1_unique1 on tenk1 k
 (5 rows)

@@ -1274,10 +1274,10 @@ where (exists(select 1 from tenk1 k where k.unique1 = t.unique2) or ten < 0)
  Aggregate
    ->  Bitmap Heap Scan on tenk1 t
          Recheck Cond: (thousand = 1)
-         Filter: (EXISTS(SubPlan 1) OR (ten < 0))
+         Filter: (EXISTS(SubPlan exists_1) OR (ten < 0))
          ->  Bitmap Index Scan on tenk1_thous_tenthous
                Index Cond: (thousand = 1)
-         SubPlan 1
+         SubPlan exists_1
            ->  Index Only Scan using tenk1_unique1 on tenk1 k
                  Index Cond: (unique1 = t.unique2)
 (9 rows)
@@ -1299,20 +1299,20 @@ analyze exists_tbl;
 explain (costs off)
 select * from exists_tbl t1
   where (exists(select 1 from exists_tbl t2 where t1.c1 = t2.c2) or c3 < 0);
-                             QUERY PLAN
---------------------------------------------------------------------
+                                QUERY PLAN
+---------------------------------------------------------------------------
  Append
    ->  Seq Scan on exists_tbl_null t1_1
-         Filter: (EXISTS(SubPlan 1) OR (c3 < 0))
-         SubPlan 1
+         Filter: (EXISTS(SubPlan exists_1) OR (c3 < 0))
+         SubPlan exists_1
            ->  Append
                  ->  Seq Scan on exists_tbl_null t2_1
                        Filter: (t1_1.c1 = c2)
                  ->  Seq Scan on exists_tbl_def t2_2
                        Filter: (t1_1.c1 = c2)
    ->  Seq Scan on exists_tbl_def t1_2
-         Filter: ((ANY (c1 = (hashed SubPlan 2).col1)) OR (c3 < 0))
-         SubPlan 2
+         Filter: ((ANY (c1 = (hashed SubPlan exists_2).col1)) OR (c3 < 0))
+         SubPlan exists_2
            ->  Append
                  ->  Seq Scan on exists_tbl_null t2_4
                  ->  Seq Scan on exists_tbl_def t2_5
@@ -1348,14 +1348,14 @@ where a.thousand = b.thousand
 explain (verbose, costs off)
   select x, x from
     (select (select now()) as x from (values(1),(2)) v(y)) ss;
-                   QUERY PLAN
-------------------------------------------------
+                        QUERY PLAN
+----------------------------------------------------------
  Values Scan on "*VALUES*"
-   Output: (InitPlan 1).col1, (InitPlan 2).col1
-   InitPlan 1
+   Output: (InitPlan expr_1).col1, (InitPlan expr_2).col1
+   InitPlan expr_1
      ->  Result
            Output: now()
-   InitPlan 2
+   InitPlan expr_2
      ->  Result
            Output: now()
 (8 rows)
@@ -1363,13 +1363,13 @@ explain (verbose, costs off)
 explain (verbose, costs off)
   select x, x from
     (select (select random()) as x from (values(1),(2)) v(y)) ss;
-            QUERY PLAN
------------------------------------
+               QUERY PLAN
+----------------------------------------
  Subquery Scan on ss
    Output: ss.x, ss.x
    ->  Values Scan on "*VALUES*"
-         Output: (InitPlan 1).col1
-         InitPlan 1
+         Output: (InitPlan expr_1).col1
+         InitPlan expr_1
            ->  Result
                  Output: random()
 (7 rows)
@@ -1380,12 +1380,12 @@ explain (verbose, costs off)
                               QUERY PLAN
 ----------------------------------------------------------------------
  Values Scan on "*VALUES*"
-   Output: (SubPlan 1), (SubPlan 2)
-   SubPlan 1
+   Output: (SubPlan expr_1), (SubPlan expr_2)
+   SubPlan expr_1
      ->  Result
            Output: now()
            One-Time Filter: ("*VALUES*".column1 = "*VALUES*".column1)
-   SubPlan 2
+   SubPlan expr_2
      ->  Result
            Output: now()
            One-Time Filter: ("*VALUES*".column1 = "*VALUES*".column1)
@@ -1399,8 +1399,8 @@ explain (verbose, costs off)
  Subquery Scan on ss
    Output: ss.x, ss.x
    ->  Values Scan on "*VALUES*"
-         Output: (SubPlan 1)
-         SubPlan 1
+         Output: (SubPlan expr_1)
+         SubPlan expr_1
            ->  Result
                  Output: random()
                  One-Time Filter: ("*VALUES*".column1 = "*VALUES*".column1)
@@ -1420,16 +1420,16 @@ where o.ten = 0;
                                                                                          QUERY PLAN
                                                                      

---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  Aggregate
-   Output: sum((((ANY (i.ten = (hashed SubPlan 1).col1))))::integer)
+   Output: sum((((ANY (i.ten = (hashed SubPlan any_1).col1))))::integer)
    ->  Nested Loop
-         Output: ((ANY (i.ten = (hashed SubPlan 1).col1)))
+         Output: ((ANY (i.ten = (hashed SubPlan any_1).col1)))
          ->  Seq Scan on public.onek o
                Output: o.unique1, o.unique2, o.two, o.four, o.ten, o.twenty, o.hundred, o.thousand, o.twothousand,
o.fivethous,o.tenthous, o.odd, o.even, o.stringu1, o.stringu2, o.string4 
                Filter: (o.ten = 0)
          ->  Index Scan using onek_unique1 on public.onek i
-               Output: (ANY (i.ten = (hashed SubPlan 1).col1)), random()
+               Output: (ANY (i.ten = (hashed SubPlan any_1).col1)), random()
                Index Cond: (i.unique1 = o.unique1)
-               SubPlan 1
+               SubPlan any_1
                  ->  Seq Scan on public.int4_tbl
                        Output: int4_tbl.f1
                        Filter: (int4_tbl.f1 <= o.hundred)
@@ -1638,7 +1638,7 @@ select * from
 ----------------------------------------
  Values Scan on "*VALUES*"
    Output: "*VALUES*".column1
-   SubPlan 1
+   SubPlan any_1
      ->  Values Scan on "*VALUES*_1"
            Output: "*VALUES*_1".column1
 (5 rows)
@@ -1665,12 +1665,12 @@ select * from int4_tbl where

---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  Nested Loop Semi Join
    Output: int4_tbl.f1
-   Join Filter: (CASE WHEN (ANY (int4_tbl.f1 = (hashed SubPlan 1).col1)) THEN int4_tbl.f1 ELSE NULL::integer END =
b.ten)
+   Join Filter: (CASE WHEN (ANY (int4_tbl.f1 = (hashed SubPlan any_1).col1)) THEN int4_tbl.f1 ELSE NULL::integer END =
b.ten)
    ->  Seq Scan on public.int4_tbl
          Output: int4_tbl.f1
    ->  Seq Scan on public.tenk1 b
          Output: b.unique1, b.unique2, b.two, b.four, b.ten, b.twenty, b.hundred, b.thousand, b.twothousand,
b.fivethous,b.tenthous, b.odd, b.even, b.stringu1, b.stringu2, b.string4 
-   SubPlan 1
+   SubPlan any_1
      ->  Index Only Scan using tenk1_unique1 on public.tenk1 a
            Output: a.unique1
 (10 rows)
@@ -2798,14 +2798,14 @@ select * from tenk1 A where exists
 (select 1 from tenk2 B
 where A.hundred in (select C.hundred FROM tenk2 C
 WHERE c.odd = b.odd));
-                     QUERY PLAN
------------------------------------------------------
+                       QUERY PLAN
+---------------------------------------------------------
  Nested Loop Semi Join
-   Join Filter: (ANY (a.hundred = (SubPlan 1).col1))
+   Join Filter: (ANY (a.hundred = (SubPlan any_1).col1))
    ->  Seq Scan on tenk1 a
    ->  Materialize
          ->  Seq Scan on tenk2 b
-   SubPlan 1
+   SubPlan any_1
      ->  Seq Scan on tenk2 c
            Filter: (odd = b.odd)
 (8 rows)
@@ -2815,14 +2815,14 @@ WHERE c.odd = b.odd));
 explain (costs off)
 SELECT * FROM tenk1 A LEFT JOIN tenk2 B
 ON A.hundred in (SELECT c.hundred FROM tenk2 C WHERE c.odd = b.odd);
-                     QUERY PLAN
------------------------------------------------------
+                       QUERY PLAN
+---------------------------------------------------------
  Nested Loop Left Join
-   Join Filter: (ANY (a.hundred = (SubPlan 1).col1))
+   Join Filter: (ANY (a.hundred = (SubPlan any_1).col1))
    ->  Seq Scan on tenk1 a
    ->  Materialize
          ->  Seq Scan on tenk2 b
-   SubPlan 1
+   SubPlan any_1
      ->  Seq Scan on tenk2 c
            Filter: (odd = b.odd)
 (8 rows)
@@ -2832,14 +2832,14 @@ ON A.hundred in (SELECT c.hundred FROM tenk2 C WHERE c.odd = b.odd);
 explain (costs off)
 SELECT * FROM tenk1 A LEFT JOIN tenk2 B
 ON B.hundred in (SELECT c.hundred FROM tenk2 C WHERE c.odd = a.odd);
-                     QUERY PLAN
------------------------------------------------------
+                       QUERY PLAN
+---------------------------------------------------------
  Nested Loop Left Join
-   Join Filter: (ANY (b.hundred = (SubPlan 1).col1))
+   Join Filter: (ANY (b.hundred = (SubPlan any_1).col1))
    ->  Seq Scan on tenk1 a
    ->  Materialize
          ->  Seq Scan on tenk2 b
-   SubPlan 1
+   SubPlan any_1
      ->  Seq Scan on tenk2 c
            Filter: (odd = a.odd)
 (8 rows)
@@ -2901,7 +2901,7 @@ ON B.hundred in (SELECT min(c.hundred) FROM tenk2 C WHERE c.odd = b.odd);
                            Filter: (b.hundred = unnamed_subquery.min)
                            ->  Result
                                  Replaces: MinMaxAggregate
-                                 InitPlan 1
+                                 InitPlan minmax_1
                                    ->  Limit
                                          ->  Index Scan using tenk2_hundred on tenk2 c
                                                Index Cond: (hundred IS NOT NULL)
@@ -3142,7 +3142,7 @@ WHERE unique1 IN (VALUES (0), ((2 IN (SELECT unique2 FROM onek c
    ->  Seq Scan on onek t
    ->  Values Scan on "*VALUES*"
          Filter: (t.unique1 = column1)
-         SubPlan 1
+         SubPlan any_1
            ->  Index Only Scan using onek_unique2 on onek c
                  Index Cond: (unique2 = t.unique1)
 (7 rows)
@@ -3158,7 +3158,7 @@ WHERE unique1 IN (VALUES (0), ((2 IN (SELECT unique2 FROM onek c
          ->  Sort
                Sort Key: "*VALUES*".column1
                ->  Values Scan on "*VALUES*"
-                     SubPlan 1
+                     SubPlan any_1
                        ->  Index Only Scan using onek_unique2 on onek c
                              Filter: ((unique2)::double precision = ANY ('{0.479425538604203,2}'::double precision[]))
    ->  Index Scan using onek_unique1 on onek t
@@ -3177,7 +3177,7 @@ SELECT ten FROM onek t WHERE unique1 IN (VALUES (0), ((2 IN
          ->  Sort
                Sort Key: "*VALUES*".column1
                ->  Values Scan on "*VALUES*"
-                     SubPlan 1
+                     SubPlan any_1
                        ->  Result
    ->  Index Scan using onek_unique1 on onek t
          Index Cond: (unique1 = "*VALUES*".column1)
diff --git a/src/test/regress/expected/updatable_views.out b/src/test/regress/expected/updatable_views.out
index 095df0a670c..03df7e75b7b 100644
--- a/src/test/regress/expected/updatable_views.out
+++ b/src/test/regress/expected/updatable_views.out
@@ -2750,7 +2750,7 @@ EXPLAIN (costs off) INSERT INTO rw_view1 VALUES (5);
 ---------------------------------------------------------
  Insert on base_tbl b
    ->  Result
-   SubPlan 1
+   SubPlan exists_1
      ->  Index Only Scan using ref_tbl_pkey on ref_tbl r
            Index Cond: (a = b.a)
 (5 rows)
@@ -2764,7 +2764,7 @@ EXPLAIN (costs off) UPDATE rw_view1 SET a = a + 5;
          ->  Seq Scan on base_tbl b
          ->  Hash
                ->  Seq Scan on ref_tbl r
-   SubPlan 1
+   SubPlan exists_1
      ->  Index Only Scan using ref_tbl_pkey on ref_tbl r_1
            Index Cond: (a = b.a)
 (9 rows)
@@ -3167,21 +3167,21 @@ EXPLAIN (costs off) DELETE FROM rw_view1 WHERE id = 1 AND snoop(data);
 DELETE FROM rw_view1 WHERE id = 1 AND snoop(data);
 NOTICE:  snooped value: Row 1
 EXPLAIN (costs off) INSERT INTO rw_view1 VALUES (2, 'New row 2');
-                        QUERY PLAN
------------------------------------------------------------
+                           QUERY PLAN
+-----------------------------------------------------------------
  Insert on base_tbl
-   InitPlan 1
+   InitPlan exists_1
      ->  Index Only Scan using base_tbl_pkey on base_tbl t
            Index Cond: (id = 2)
    ->  Result
-         One-Time Filter: ((InitPlan 1).col1 IS NOT TRUE)
+         One-Time Filter: ((InitPlan exists_1).col1 IS NOT TRUE)

  Update on base_tbl
-   InitPlan 1
+   InitPlan exists_1
      ->  Index Only Scan using base_tbl_pkey on base_tbl t
            Index Cond: (id = 2)
    ->  Result
-         One-Time Filter: (InitPlan 1).col1
+         One-Time Filter: (InitPlan exists_1).col1
          ->  Index Scan using base_tbl_pkey on base_tbl
                Index Cond: (id = 2)
 (15 rows)
@@ -3240,8 +3240,8 @@ SELECT * FROM v1 WHERE a=8;

 EXPLAIN (VERBOSE, COSTS OFF)
 UPDATE v1 SET a=100 WHERE snoop(a) AND leakproof(a) AND a < 7 AND a != 6;
-                                                QUERY PLAN
------------------------------------------------------------------------------------------------------------
+                                                    QUERY PLAN
+------------------------------------------------------------------------------------------------------------------
  Update on public.t1
    Update on public.t1 t1_1
    Update on public.t11 t1_2
@@ -3253,8 +3253,8 @@ UPDATE v1 SET a=100 WHERE snoop(a) AND leakproof(a) AND a < 7 AND a != 6;
                ->  Index Scan using t1_a_idx on public.t1 t1_1
                      Output: t1_1.tableoid, t1_1.ctid
                      Index Cond: ((t1_1.a > 5) AND (t1_1.a < 7))
-                     Filter: ((t1_1.a <> 6) AND EXISTS(SubPlan 1) AND snoop(t1_1.a) AND leakproof(t1_1.a))
-                     SubPlan 1
+                     Filter: ((t1_1.a <> 6) AND EXISTS(SubPlan exists_1) AND snoop(t1_1.a) AND leakproof(t1_1.a))
+                     SubPlan exists_1
                        ->  Append
                              ->  Seq Scan on public.t12 t12_1
                                    Filter: (t12_1.a = t1_1.a)
@@ -3263,15 +3263,15 @@ UPDATE v1 SET a=100 WHERE snoop(a) AND leakproof(a) AND a < 7 AND a != 6;
                ->  Index Scan using t11_a_idx on public.t11 t1_2
                      Output: t1_2.tableoid, t1_2.ctid
                      Index Cond: ((t1_2.a > 5) AND (t1_2.a < 7))
-                     Filter: ((t1_2.a <> 6) AND EXISTS(SubPlan 1) AND snoop(t1_2.a) AND leakproof(t1_2.a))
+                     Filter: ((t1_2.a <> 6) AND EXISTS(SubPlan exists_1) AND snoop(t1_2.a) AND leakproof(t1_2.a))
                ->  Index Scan using t12_a_idx on public.t12 t1_3
                      Output: t1_3.tableoid, t1_3.ctid
                      Index Cond: ((t1_3.a > 5) AND (t1_3.a < 7))
-                     Filter: ((t1_3.a <> 6) AND EXISTS(SubPlan 1) AND snoop(t1_3.a) AND leakproof(t1_3.a))
+                     Filter: ((t1_3.a <> 6) AND EXISTS(SubPlan exists_1) AND snoop(t1_3.a) AND leakproof(t1_3.a))
                ->  Index Scan using t111_a_idx on public.t111 t1_4
                      Output: t1_4.tableoid, t1_4.ctid
                      Index Cond: ((t1_4.a > 5) AND (t1_4.a < 7))
-                     Filter: ((t1_4.a <> 6) AND EXISTS(SubPlan 1) AND snoop(t1_4.a) AND leakproof(t1_4.a))
+                     Filter: ((t1_4.a <> 6) AND EXISTS(SubPlan exists_1) AND snoop(t1_4.a) AND leakproof(t1_4.a))
 (30 rows)

 UPDATE v1 SET a=100 WHERE snoop(a) AND leakproof(a) AND a < 7 AND a != 6;
@@ -3287,8 +3287,8 @@ SELECT * FROM t1 WHERE a=100; -- Nothing should have been changed to 100

 EXPLAIN (VERBOSE, COSTS OFF)
 UPDATE v1 SET a=a+1 WHERE snoop(a) AND leakproof(a) AND a = 8;
-                                       QUERY PLAN
------------------------------------------------------------------------------------------
+                                           QUERY PLAN
+------------------------------------------------------------------------------------------------
  Update on public.t1
    Update on public.t1 t1_1
    Update on public.t11 t1_2
@@ -3300,8 +3300,8 @@ UPDATE v1 SET a=a+1 WHERE snoop(a) AND leakproof(a) AND a = 8;
                ->  Index Scan using t1_a_idx on public.t1 t1_1
                      Output: t1_1.a, t1_1.tableoid, t1_1.ctid
                      Index Cond: ((t1_1.a > 5) AND (t1_1.a = 8))
-                     Filter: (EXISTS(SubPlan 1) AND snoop(t1_1.a) AND leakproof(t1_1.a))
-                     SubPlan 1
+                     Filter: (EXISTS(SubPlan exists_1) AND snoop(t1_1.a) AND leakproof(t1_1.a))
+                     SubPlan exists_1
                        ->  Append
                              ->  Seq Scan on public.t12 t12_1
                                    Filter: (t12_1.a = t1_1.a)
@@ -3310,15 +3310,15 @@ UPDATE v1 SET a=a+1 WHERE snoop(a) AND leakproof(a) AND a = 8;
                ->  Index Scan using t11_a_idx on public.t11 t1_2
                      Output: t1_2.a, t1_2.tableoid, t1_2.ctid
                      Index Cond: ((t1_2.a > 5) AND (t1_2.a = 8))
-                     Filter: (EXISTS(SubPlan 1) AND snoop(t1_2.a) AND leakproof(t1_2.a))
+                     Filter: (EXISTS(SubPlan exists_1) AND snoop(t1_2.a) AND leakproof(t1_2.a))
                ->  Index Scan using t12_a_idx on public.t12 t1_3
                      Output: t1_3.a, t1_3.tableoid, t1_3.ctid
                      Index Cond: ((t1_3.a > 5) AND (t1_3.a = 8))
-                     Filter: (EXISTS(SubPlan 1) AND snoop(t1_3.a) AND leakproof(t1_3.a))
+                     Filter: (EXISTS(SubPlan exists_1) AND snoop(t1_3.a) AND leakproof(t1_3.a))
                ->  Index Scan using t111_a_idx on public.t111 t1_4
                      Output: t1_4.a, t1_4.tableoid, t1_4.ctid
                      Index Cond: ((t1_4.a > 5) AND (t1_4.a = 8))
-                     Filter: (EXISTS(SubPlan 1) AND snoop(t1_4.a) AND leakproof(t1_4.a))
+                     Filter: (EXISTS(SubPlan exists_1) AND snoop(t1_4.a) AND leakproof(t1_4.a))
 (30 rows)

 UPDATE v1 SET a=a+1 WHERE snoop(a) AND leakproof(a) AND a = 8;
@@ -3502,10 +3502,10 @@ CREATE RULE v1_upd_rule AS ON UPDATE TO v1 DO INSTEAD
 CREATE VIEW v2 WITH (security_barrier = true) AS
   SELECT * FROM v1 WHERE EXISTS (SELECT 1);
 EXPLAIN (COSTS OFF) UPDATE v2 SET a = 1;
-                          QUERY PLAN
---------------------------------------------------------------
+                             QUERY PLAN
+---------------------------------------------------------------------
  Update on t1
-   InitPlan 1
+   InitPlan exists_1
      ->  Result
    ->  Merge Join
          Merge Cond: (t1.a = v1.a)
@@ -3516,7 +3516,7 @@ EXPLAIN (COSTS OFF) UPDATE v2 SET a = 1;
                Sort Key: v1.a
                ->  Subquery Scan on v1
                      ->  Result
-                           One-Time Filter: (InitPlan 1).col1
+                           One-Time Filter: (InitPlan exists_1).col1
                            ->  Seq Scan on t1 t1_1
 (14 rows)

diff --git a/src/test/regress/expected/update.out b/src/test/regress/expected/update.out
index 1b27d132d7b..eef2bac1cbf 100644
--- a/src/test/regress/expected/update.out
+++ b/src/test/regress/expected/update.out
@@ -178,15 +178,15 @@ EXPLAIN (VERBOSE, COSTS OFF)
 UPDATE update_test t
   SET (a, b) = (SELECT b, a FROM update_test s WHERE s.a = t.a)
   WHERE CURRENT_USER = SESSION_USER;
-                                   QUERY PLAN
---------------------------------------------------------------------------------
+                                                  QUERY PLAN
+--------------------------------------------------------------------------------------------------------------
  Update on public.update_test t
    ->  Result
-         Output: (SubPlan 1).col1, (SubPlan 1).col2, (rescan SubPlan 1), t.ctid
+         Output: (SubPlan multiexpr_1).col1, (SubPlan multiexpr_1).col2, (rescan SubPlan multiexpr_1), t.ctid
          One-Time Filter: (CURRENT_USER = SESSION_USER)
          ->  Seq Scan on public.update_test t
                Output: t.a, t.ctid
-         SubPlan 1
+         SubPlan multiexpr_1
            ->  Seq Scan on public.update_test s
                  Output: s.b, s.a
                  Filter: (s.a = t.a)
diff --git a/src/test/regress/expected/window.out b/src/test/regress/expected/window.out
index b86b668f433..4ccc349eec7 100644
--- a/src/test/regress/expected/window.out
+++ b/src/test/regress/expected/window.out
@@ -4250,14 +4250,14 @@ SELECT 1 FROM
   (SELECT ntile(s1.x) OVER () AS c
    FROM (SELECT (SELECT 1) AS x) AS s1) s
 WHERE s.c = 1;
-                           QUERY PLAN
-----------------------------------------------------------------
+                             QUERY PLAN
+---------------------------------------------------------------------
  Subquery Scan on s
    Filter: (s.c = 1)
    ->  WindowAgg
          Window: w1 AS (ROWS UNBOUNDED PRECEDING)
-         Run Condition: (ntile((InitPlan 1).col1) OVER w1 <= 1)
-         InitPlan 1
+         Run Condition: (ntile((InitPlan expr_1).col1) OVER w1 <= 1)
+         InitPlan expr_1
            ->  Result
          ->  Result
 (8 rows)
@@ -4338,7 +4338,7 @@ WHERE c = 1;
    Filter: (emp.c = 1)
    ->  WindowAgg
          Window: w1 AS (ORDER BY empsalary.empno)
-         InitPlan 1
+         InitPlan expr_1
            ->  Result
          ->  Sort
                Sort Key: empsalary.empno DESC
diff --git a/src/test/regress/expected/with.out b/src/test/regress/expected/with.out
index c3932c7b94c..86fdb85c6c5 100644
--- a/src/test/regress/expected/with.out
+++ b/src/test/regress/expected/with.out
@@ -2306,14 +2306,14 @@ explain (verbose, costs off)
 select f1, (with cte1(x,y) as (select 1,2)
             select count((select i4.f1 from cte1))) as ss
 from int4_tbl i4;
-                 QUERY PLAN
---------------------------------------------
+                   QUERY PLAN
+-------------------------------------------------
  Seq Scan on public.int4_tbl i4
-   Output: i4.f1, (SubPlan 2)
-   SubPlan 2
+   Output: i4.f1, (SubPlan expr_1)
+   SubPlan expr_1
      ->  Aggregate
-           Output: count((InitPlan 1).col1)
-           InitPlan 1
+           Output: count((InitPlan expr_2).col1)
+           InitPlan expr_2
              ->  Result
                    Output: i4.f1
            ->  Result
@@ -3203,7 +3203,7 @@ WHEN NOT MATCHED THEN INSERT VALUES(o.k, o.v);
                      Output: o.k, o.v, o.*
                      ->  Result
                            Output: 0, 'merge source SubPlan'::text
-   SubPlan 2
+   SubPlan expr_1
      ->  Limit
            Output: ((cte_basic.b || ' merge update'::text))
            ->  CTE Scan on cte_basic
@@ -3235,7 +3235,7 @@ WHEN NOT MATCHED THEN INSERT VALUES(o.k, o.v);
    CTE cte_init
      ->  Result
            Output: 1, 'cte_init val'::text
-   InitPlan 2
+   InitPlan expr_1
      ->  Limit
            Output: ((cte_init.b || ' merge update'::text))
            ->  CTE Scan on cte_init
@@ -3278,11 +3278,11 @@ WHEN NOT MATCHED THEN INSERT VALUES(o.a, o.b || (SELECT merge_source_cte.*::text
    CTE merge_source_cte
      ->  Result
            Output: 15, 'merge_source_cte val'::text
-   InitPlan 2
+   InitPlan expr_1
      ->  CTE Scan on merge_source_cte merge_source_cte_1
            Output: ((merge_source_cte_1.b || (merge_source_cte_1.*)::text) || ' merge update'::text)
            Filter: (merge_source_cte_1.a = 15)
-   InitPlan 3
+   InitPlan expr_2
      ->  CTE Scan on merge_source_cte merge_source_cte_2
            Output: ((merge_source_cte_2.*)::text || ' merge insert'::text)
    ->  Hash Right Join
--
2.43.7

From b6736b127a93b0203ad8a5ab9dd1514135606517 Mon Sep 17 00:00:00 2001
From: Tom Lane <tgl@sss.pgh.pa.us>
Date: Wed, 24 Sep 2025 17:30:22 -0400
Subject: [PATCH v10 2/2] Use a list of subplan names instead of a list of
 "allroots".

I don't like constructing a list of all PlannerInfo roots: it's
partially redundant with the subroots list, and it's far from clear
how to handle transient roots such as the one manufactured by
pull_up_simple_subquery.  This patch proposes that we should
instead just store a list of assigned subplan names.  One advantage
is that choose_plan_name can immediately enter the new name into
the list, instead of relying on the caller to get it right later.

Apropos to that point, I found that build_minmax_path was putting
the wrong root into the list, so that it was perfectly capable
of assigning "minmax_1" over and over, as indeed is visible in
a couple of incorrect regression test changes in v9-0001.

I remain unsure whether it's OK for pull_up_simple_subquery to just do
+    subroot->plan_name = root->plan_name;
but at least now we won't have two roots with the same plan_name
in the allroots list.  There's no way that that's helpful.

Also fix the missed use of choose_plan_name in SS_process_ctes:
without that, we can't really promise that we'll attach unique
names to CTE subplans.
---
 src/backend/optimizer/plan/planagg.c      |  3 --
 src/backend/optimizer/plan/planner.c      | 35 ++++++++++++++---------
 src/backend/optimizer/plan/subselect.c    |  5 ++--
 src/backend/optimizer/prep/prepjointree.c |  3 --
 src/include/nodes/pathnodes.h             |  6 ++--
 src/test/regress/expected/aggregates.out  |  6 ++--
 src/test/regress/expected/inherit.out     |  2 +-
 7 files changed, 31 insertions(+), 29 deletions(-)

diff --git a/src/backend/optimizer/plan/planagg.c b/src/backend/optimizer/plan/planagg.c
index 0ce35cabaf5..a2ac58d246e 100644
--- a/src/backend/optimizer/plan/planagg.c
+++ b/src/backend/optimizer/plan/planagg.c
@@ -362,9 +362,6 @@ build_minmax_path(PlannerInfo *root, MinMaxAggInfo *mminfo,
     /* and we haven't created PlaceHolderInfos, either */
     Assert(subroot->placeholder_list == NIL);

-    /* Add this to list of all PlannerInfo objects. */
-    root->glob->allroots = lappend(root->glob->allroots, root);
-
     /*----------
      * Generate modified query of the form
      *        (SELECT col FROM tab
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index acd1356a721..3b130e724f7 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -631,6 +631,7 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions,
  *
  * glob is the global state for the current planner run.
  * parse is the querytree produced by the parser & rewriter.
+ * plan_name is the name to assign to this subplan (NULL at the top level).
  * parent_root is the immediate parent Query's info (NULL at the top level).
  * hasRecursion is true if this is a recursive WITH query.
  * tuple_fraction is the fraction of tuples we expect will be retrieved.
@@ -712,9 +713,6 @@ subquery_planner(PlannerGlobal *glob, Query *parse, char *plan_name,
     root->non_recursive_path = NULL;
     root->partColsUpdated = false;

-    /* Add this to list of all PlannerInfo objects. */
-    root->glob->allroots = lappend(root->glob->allroots, root);
-
     /*
      * Create the top-level join domain.  This won't have valid contents until
      * deconstruct_jointree fills it in, but the node needs to exist before
@@ -8840,7 +8838,9 @@ create_partial_unique_paths(PlannerInfo *root, RelOptInfo *input_rel,
 }

 /*
- * Choose a unique plan name for subroot.
+ * Choose a unique name for some subroot.
+ *
+ * Modifies glob->subplanNames to track names already used.
  */
 char *
 choose_plan_name(PlannerGlobal *glob, const char *name, bool always_number)
@@ -8848,18 +8848,17 @@ choose_plan_name(PlannerGlobal *glob, const char *name, bool always_number)
     unsigned    n;

     /*
-     * If a numeric suffix is not required, then search the list of roots for
-     * a plan with the requested name. If none is found, then we can use the
-     * provided name without modification.
+     * If a numeric suffix is not required, then search the list of
+     * previously-assigned names for a match. If none is found, then we can
+     * use the provided name without modification.
      */
     if (!always_number)
     {
         bool        found = false;

-        foreach_node(PlannerInfo, root, glob->allroots)
+        foreach_ptr(char, subplan_name, glob->subplanNames)
         {
-            if (root->plan_name != NULL &&
-                strcmp(name, root->plan_name) == 0)
+            if (strcmp(subplan_name, name) == 0)
             {
                 found = true;
                 break;
@@ -8867,7 +8866,13 @@ choose_plan_name(PlannerGlobal *glob, const char *name, bool always_number)
         }

         if (!found)
-            return pstrdup(name);
+        {
+            /* pstrdup here is just to avoid cast-away-const */
+            char       *chosen_name = pstrdup(name);
+
+            glob->subplanNames = lappend(glob->subplanNames, chosen_name);
+            return chosen_name;
+        }
     }

     /*
@@ -8880,10 +8885,9 @@ choose_plan_name(PlannerGlobal *glob, const char *name, bool always_number)
         char       *proposed_name = psprintf("%s_%u", name, n);
         bool        found = false;

-        foreach_node(PlannerInfo, root, glob->allroots)
+        foreach_ptr(char, subplan_name, glob->subplanNames)
         {
-            if (root->plan_name != NULL &&
-                strcmp(proposed_name, root->plan_name) == 0)
+            if (strcmp(subplan_name, proposed_name) == 0)
             {
                 found = true;
                 break;
@@ -8891,7 +8895,10 @@ choose_plan_name(PlannerGlobal *glob, const char *name, bool always_number)
         }

         if (!found)
+        {
+            glob->subplanNames = lappend(glob->subplanNames, proposed_name);
             return proposed_name;
+        }

         pfree(proposed_name);
     }
diff --git a/src/backend/optimizer/plan/subselect.c b/src/backend/optimizer/plan/subselect.c
index 5f8306bc421..14192a13236 100644
--- a/src/backend/optimizer/plan/subselect.c
+++ b/src/backend/optimizer/plan/subselect.c
@@ -967,8 +967,9 @@ SS_process_ctes(PlannerInfo *root)
          * Generate Paths for the CTE query.  Always plan for full retrieval
          * --- we don't have enough info to predict otherwise.
          */
-        subroot = subquery_planner(root->glob, subquery, cte->ctename, root,
-                                   cte->cterecursive, 0.0, NULL);
+        subroot = subquery_planner(root->glob, subquery,
+                                   choose_plan_name(root->glob, cte->ctename, false),
+                                   root, cte->cterecursive, 0.0, NULL);

         /*
          * Since the current query level doesn't yet contain any RTEs, it
diff --git a/src/backend/optimizer/prep/prepjointree.c b/src/backend/optimizer/prep/prepjointree.c
index 2ec13637d16..563be151a4d 100644
--- a/src/backend/optimizer/prep/prepjointree.c
+++ b/src/backend/optimizer/prep/prepjointree.c
@@ -1388,9 +1388,6 @@ pull_up_simple_subquery(PlannerInfo *root, Node *jtnode, RangeTblEntry *rte,
     subroot->non_recursive_path = NULL;
     /* We don't currently need a top JoinDomain for the subroot */

-    /* Add new subroot to master list of PlannerInfo objects. */
-    root->glob->allroots = lappend(root->glob->allroots, subroot);
-
     /* No CTEs to worry about */
     Assert(subquery->cteList == NIL);

diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h
index a341b01a1e1..7ee9a7a68d8 100644
--- a/src/include/nodes/pathnodes.h
+++ b/src/include/nodes/pathnodes.h
@@ -110,8 +110,8 @@ typedef struct PlannerGlobal
     /* PlannerInfos for SubPlan nodes */
     List       *subroots pg_node_attr(read_write_ignore);

-    /* every PlannerInfo regardless of whether it's an InitPlan/SubPlan */
-    List       *allroots pg_node_attr(read_write_ignore);
+    /* names already used for subplans (list of C strings) */
+    List       *subplanNames pg_node_attr(read_write_ignore);

     /* indices of subplans that require REWIND */
     Bitmapset  *rewindPlanIDs;
@@ -231,7 +231,7 @@ struct PlannerInfo
     /* NULL at outermost Query */
     PlannerInfo *parent_root pg_node_attr(read_write_ignore);

-    /* Name for EXPLAIN and debugging purposes */
+    /* Subplan name for EXPLAIN and debugging purposes (NULL at top level) */
     char       *plan_name;

     /*
diff --git a/src/test/regress/expected/aggregates.out b/src/test/regress/expected/aggregates.out
index a9503e810c5..1f84db2f361 100644
--- a/src/test/regress/expected/aggregates.out
+++ b/src/test/regress/expected/aggregates.out
@@ -1269,7 +1269,7 @@ explain (costs off)
                  ->  Index Only Scan Backward using minmaxtest2i on minmaxtest2 minmaxtest_3
                        Index Cond: (f1 IS NOT NULL)
                  ->  Index Only Scan using minmaxtest3i on minmaxtest3 minmaxtest_4
-   InitPlan minmax_1
+   InitPlan minmax_2
      ->  Limit
            ->  Merge Append
                  Sort Key: minmaxtest_5.f1 DESC
@@ -1305,7 +1305,7 @@ explain (costs off)
                  ->  Index Only Scan Backward using minmaxtest2i on minmaxtest2 minmaxtest_3
                        Index Cond: (f1 IS NOT NULL)
                  ->  Index Only Scan using minmaxtest3i on minmaxtest3 minmaxtest_4
-   InitPlan minmax_1
+   InitPlan minmax_2
      ->  Limit
            ->  Merge Append
                  Sort Key: minmaxtest_5.f1 DESC
@@ -1317,7 +1317,7 @@ explain (costs off)
                        Index Cond: (f1 IS NOT NULL)
                  ->  Index Only Scan Backward using minmaxtest3i on minmaxtest3 minmaxtest_9
    ->  Sort
-         Sort Key: ((InitPlan minmax_1).col1), ((InitPlan minmax_1).col1)
+         Sort Key: ((InitPlan minmax_1).col1), ((InitPlan minmax_2).col1)
          ->  Result
                Replaces: MinMaxAggregate
 (27 rows)
diff --git a/src/test/regress/expected/inherit.out b/src/test/regress/expected/inherit.out
index 6dbbd26f56b..0490a746555 100644
--- a/src/test/regress/expected/inherit.out
+++ b/src/test/regress/expected/inherit.out
@@ -3264,7 +3264,7 @@ explain (costs off) select min(a), max(a) from parted_minmax where b = '12345';
      ->  Limit
            ->  Index Only Scan using parted_minmax1i on parted_minmax1 parted_minmax
                  Index Cond: ((a IS NOT NULL) AND (b = '12345'::text))
-   InitPlan minmax_1
+   InitPlan minmax_2
      ->  Limit
            ->  Index Only Scan Backward using parted_minmax1i on parted_minmax1 parted_minmax_1
                  Index Cond: ((a IS NOT NULL) AND (b = '12345'::text))
--
2.43.7


Re: plan shape work

От
Robert Haas
Дата:
On Wed, Sep 24, 2025 at 6:03 PM Tom Lane <tgl@sss.pgh.pa.us> wrote:
> I don't think so.  We do not have a nice story on marking Node fields
> const: it's very unclear for example what consequences that ought to
> have for copyObject().  Maybe somebody will tackle that issue someday,
> but it's not something to touch casually in a patch with other
> objectives.  So I don't think we can make the plan_name fields const.
> The best solution I think is to make choose_plan_name() take a const
> string and return a non-const one.  The attached v10-0001 is just like
> your v9-0001 except for doing the const stuff this way.  I chose to
> fix the impedance mismatch within choose_plan_name() by having it
> pstrdup when it wants to just return the "name" string, but you could
> make a case for holding your nose and just casting away const there.

Yeah, these are the kinds of trade-offs I never know how to make. I
have a little difficulty believing that it's worth a pstrdup() to have
the benefit of marking things const, but maybe it is, and the
difference is probably microscopic either way. It's not like we're
going to be faced with many planning problems involving gigantic
numbers of subqueries. If this were a hot code path we'd want to think
harder.

> > - You (Tom) also asked why not print InitPlan/SubPlan wherever we
> > refer to subplans, so this version restores that behavior.
>
> Thanks.  I'm good with the output now (modulo the bug described
> below).  Someone could potentially argue that this exposes more
> of the internals than we really ought to, such as the difference
> between expr and multiexpr SubLinks, but I'm okay with that.

I actually thought it looked kind of nice exposing some of that
detail, but it's certainly a judgement call.

> Aside from the const issue, something I don't really like at the
> coding level is the use of an "allroots" list.  One reason is that
> it's partially redundant with the adjacent "subroots" list, but
> a bigger one is that we have transient roots that shouldn't be
> in there.  An example here is pull_up_simple_subquery: it builds
> a clone of the query's PlannerInfo to help it use various
> infrastructure along the way to flattening the subquery, but
> that clone is not referenced anymore after the function exits.
> You were putting that into allroots, which seems to me to be
> a fundamental error, even more so because it went in with the
> same plan_name as the root it was cloned from.
>
> I think a better idea is to keep a list of just the subplan
> names that we've assigned so far.  That has a far clearer
> charter, plus it can be updated immediately by choose_plan_name()
> instead of relying on the caller to do the right thing later.
> I coded this up, and was rather surprised to find that it changed
> some regression outputs.  On investigation, that's because
> build_minmax_path() was actually doing the wrong thing later:
> it was putting the wrong root into allroots, so that "minmax_1"
> never became assigned and could be re-used later.

Ooph, that's embarrassing. I think the reason that I ended up making a
list of the roots themselves rather than the strings is that I was
thinking that everything in this data structure would need to be a
node, and I didn't want to cons up String nodes for every list
element. Then later I marked that structure member read_write_ignore
and never stopped to think that maybe then we didn't need nodes at
all. So, in short, I think this is a great solution and thanks a ton
for putting in the legwork to figure it out.

> I also observed that SS_process_ctes() was not using
> choose_plan_name() but simply assigning the user-written CTE
> name.  I believe it's possible to use the same CTE name in
> different parts of a query tree, so this fails to achieve
> the stated purpose of making the names unique.

Ouch, that's another good catch. Also, even if the CTEs had to be
unique among themselves, there could be a collision between a CTE name
and a generated name.

> I'm still a little bit uncomfortable about whether
> it's okay for pull_up_simple_subquery() to just do
>
> +       subroot->plan_name = root->plan_name;
>
> rather than giving some other name to the transient subroot.
> I think it's okay because we are not making any meaningful planning
> decisions during the life of the subroot, just seeing if we can
> transform the subquery into a form that allows it to be pulled up.
> But you might think differently.  Perhaps a potential compromise
> is to set the transient subroot's plan_name to NULL instead?

I don't like NULL. Imagine that some meaningful planning decision does
get made during the life of the subroot, and imagine further that some
hook is called by means of which some extension can influence that
decision. What plan_name do we want that extension to see? I think
there's some argument for letting it see the plan name of the parent
plan into which we're thinking about inlining the subquery, which I
believe is the effect of the current coding, and there's perhaps also
an argument for having a wholly new plan name there just to really
identify that particular decision clearly, but if we put NULL, that's
the name we use for the topmost query level. If we changed things so
that the top query level gets called "main" or some other constant
string, then it would make sense to use NULL as a sentinel value here
to mean "undefined," but I kind of think we probably don't want to go
there.

> Anyway, v10-0002 is a delta patch to use a list of subplan
> names instead of "allroots", and there are a couple of trivial
> cosmetic changes too.

OK, I'll try out these versions and let you know what I find out. Thanks much.

--
Robert Haas
EDB: http://www.enterprisedb.com



Re: plan shape work

От
Tom Lane
Дата:
Robert Haas <robertmhaas@gmail.com> writes:
> On Wed, Sep 24, 2025 at 6:03 PM Tom Lane <tgl@sss.pgh.pa.us> wrote:
>> I think a better idea is to keep a list of just the subplan
>> names that we've assigned so far.  That has a far clearer
>> charter, plus it can be updated immediately by choose_plan_name()
>> instead of relying on the caller to do the right thing later.
>> I coded this up, and was rather surprised to find that it changed
>> some regression outputs.  On investigation, that's because
>> build_minmax_path() was actually doing the wrong thing later:
>> it was putting the wrong root into allroots, so that "minmax_1"
>> never became assigned and could be re-used later.

> Ooph, that's embarrassing. I think the reason that I ended up making a
> list of the roots themselves rather than the strings is that I was
> thinking that everything in this data structure would need to be a
> node, and I didn't want to cons up String nodes for every list
> element. Then later I marked that structure member read_write_ignore
> and never stopped to think that maybe then we didn't need nodes at
> all. So, in short, I think this is a great solution and thanks a ton
> for putting in the legwork to figure it out.

I think if we do decide later that the list-of-names needs to be
Nodes, then converting them to String nodes is a perfectly fine
solution.  As you remark elsewhere, none of this is going to be
more than microscopic compared to the overall cost of planning
a subplan.  But for right now, yeah, we don't need it.

Anyway, it seems the only remaining issue is what name
pull_up_simple_subquery should give to its transient root clone.
I don't actually believe that it matters, so if you're content
with re-using the parent name, let's just roll with that until
someone complains.

            regards, tom lane



Re: plan shape work

От
Alexandra Wang
Дата:
Hi there,

I've tried v10-000{1,2}+v9-0002 and v9-000{1,2}. I was curious whether
the names choose_plan_name() chose for subqueries match the Subquery
Scan names in the EXPLAIN plan. My guess is that since the former is
chosen before planning and the latter after planning, they might
differ. I think it's probably ok to have different naming mechanisms
as long as the names are unique within themselves. But in case anyone
else cares about the naming inconsistency, here's an example that
shows it.

-- applied patches
I've applied v10-0001, v10-0002 and v9-0002. I needed v9-0002 because
I want to see the plan_names in the debug plan and in the
EXPLAIN(RANGE_TABLE) plan with pg_overexplain.
(Applying v9-000{1,2} instead should give the same results)

-- setup
CREATE TABLE r (a int, b int);
CREATE TABLE s (c int, d int);
LOAD 'pg_overexplain';
SET debug_print_plan to on;
SET client_min_messages to 'log';

-- query
EXPLAIN (range_table, costs off)
SELECT *
FROM
  (SELECT a FROM
     (SELECT a, b FROM r WHERE b > 42 ORDER BY a)
   UNION ALL
     (SELECT c FROM
(SELECT c, d FROM s WHERE d > 24 ORDER BY d)));

-- plan
                  QUERY PLAN
----------------------------------------------
 Append
   Append RTIs: 1
   ->  Subquery Scan on unnamed_subquery_1
         Scan RTI: 4
         ->  Sort
               Sort Key: r.a
               ->  Seq Scan on r
                     Filter: (b > 42)
                     Scan RTI: 6
   ->  Subquery Scan on unnamed_subquery_2
         Scan RTI: 5
         ->  Sort
               Sort Key: s.d
               ->  Seq Scan on s
                     Filter: (d > 24)
                     Scan RTI: 7
 RTI 1 (subquery, inherited, in-from-clause):
   Eref: unnamed_subquery (a)
 RTI 2 (subquery):
   Eref: unnamed_subquery (a)
 RTI 3 (subquery):
   Eref: unnamed_subquery (c)
 RTI 4 (subquery, in-from-clause):
   Eref: unnamed_subquery (a, b)
 RTI 5 (subquery, in-from-clause):
   Eref: unnamed_subquery (c, d)
 RTI 6 (relation, in-from-clause):
   Subplan: unnamed_subquery
   Eref: r (a, b)
   Relation: r
   Relation Kind: relation
   Relation Lock Mode: AccessShareLock
   Permission Info Index: 1
 RTI 7 (relation, in-from-clause):
   Subplan: unnamed_subquery_1
   Eref: s (c, d)
   Relation: s
   Relation Kind: relation
   Relation Lock Mode: AccessShareLock
   Permission Info Index: 2
 Unprunable RTIs: 6 7
(41 rows)

-- interesting part of the debug plan:
   :subplans <>
   :subrtinfos (
      {SUBPLANRTINFO
      :plan_name unnamed_subquery
      :rtoffset 5
      :dummy false
      }
      {SUBPLANRTINFO
      :plan_name unnamed_subquery_1
      :rtoffset 6
      :dummy false
      }
 
It appears that in the EXPLAIN plan the subqueries are named
"unnamed_subquery" (does not show up in the EXPLAIN output),
"unnamed_subquery_1", and "unnamed_subquery_2"; whereas in the RTIs
section from pg_overexplain, as well as in the debug plan's
:subrtinfos section, the subplans are named "unnamed_subquery" and
"unnamed_subquery_1".

IIUC, the Subquery Scan names in the query plan, for example:
   ->  Subquery Scan on unnamed_subquery_2
is the name assigned to this Subquery Scan node of RTI: 5, after
planning.

And the Subplan name of an RTI with pg_overexplain, for example:
 RTI 7 (relation, in-from-clause):
   Subplan: unnamed_subquery_1
   Eref: s (c, d)
is the Subplan name chosen before planning. RTI 7 here maps to the
SUBPLANRTINFO with ":rtoffset 6" in the debug plan, which means it
belongs to the Subplan named "unnamed_subquery_1". This is what I
think causes confusion, because from the query plan we see that RTI 7
is under the Subquery Scan on "unnamed_subquery_2", not
"unnamed_subquery_1".

I think technically this is not a problem, since we can uniquely
identify the Subplans using the names assigned before planning, and we
can also uniquely identify the Subquery Scans in the EXPLAIN plan
using the names assigned after planning. Still, I found it a bit
confusing when looking at the EXPLAIN(RANGE_TABLE) output, where the
same name "unnamed_subquery_1" not only doesn't mean the same plan
node, but also not in the same branch of the plan tree.

Thoughts?

Best,
Alex

Re: plan shape work

От
Robert Haas
Дата:
On Thu, Sep 25, 2025 at 9:21 PM Alexandra Wang
<alexandra.wang.oss@gmail.com> wrote:
> I've tried v10-000{1,2}+v9-0002 and v9-000{1,2}. I was curious whether
> the names choose_plan_name() chose for subqueries match the Subquery
> Scan names in the EXPLAIN plan. My guess is that since the former is
> chosen before planning and the latter after planning, they might
> differ. I think it's probably ok to have different naming mechanisms
> as long as the names are unique within themselves. But in case anyone
> else cares about the naming inconsistency, here's an example that
> shows it.

Yeah. Technically, these are not the same names: one set of names is
the names assigned to the subqueries, and the other is the set of
names assigned to the relations that get scanned by subquery scans.
This may seem like a technicality, but that's not entirely the case,
because in each case the chosen names are unique. If, for example,
there were a table named unnamed_subquery that we were
subquery-scanning, then you couldn't also have a subquery scan of that
table, but you could still have a subquery with that name.

But maybe there's still some way to improve this. It would probably be
hard to make it perfect because of the fact that EXPLAIN names are
unique across all relations in the query, as noted above. However, we
might be able to make it so the names match in the absence of name
conflicts with user specified aliases or table names. Or maybe we
should consider some larger change to the EXPLAIN format so that we
display subquery names instead of relation names.

--
Robert Haas
EDB: http://www.enterprisedb.com



Re: plan shape work

От
Tom Lane
Дата:
Robert Haas <robertmhaas@gmail.com> writes:
> But maybe there's still some way to improve this. It would probably be
> hard to make it perfect because of the fact that EXPLAIN names are
> unique across all relations in the query, as noted above. However, we
> might be able to make it so the names match in the absence of name
> conflicts with user specified aliases or table names. Or maybe we
> should consider some larger change to the EXPLAIN format so that we
> display subquery names instead of relation names.

I'm kind of down on that last idea, because relation names/aliases
are user-supplied or at least user-suppliable, whereas what
choose_plan_name() generates is not under user control.  So if we
try to make this match, it should be in the direction of using
the parent query's subquery alias whenever possible.  But I fear
that it'll be hard to make this match up exactly unless we choose
to take the selection of unique relation aliases out of EXPLAIN
altogether and make the planner do it.  Which seems like a bad
idea, because then we're paying that cost all the time, and
it's not small for big queries.

            regards, tom lane



Re: plan shape work

От
Richard Guo
Дата:
FWIW, I'm a bit concerned about the double for loop inside
choose_plan_name(), especially since the outer loop runs with a true
condition.  Maybe I'm just worrying over nothing, as we probably don't
expect a large number of subroots in practice, but the nested loops
still make me a little uneasy.

- Richard



Re: plan shape work

От
Tom Lane
Дата:
Richard Guo <guofenglinux@gmail.com> writes:
> FWIW, I'm a bit concerned about the double for loop inside
> choose_plan_name(), especially since the outer loop runs with a true
> condition.  Maybe I'm just worrying over nothing, as we probably don't
> expect a large number of subroots in practice, but the nested loops
> still make me a little uneasy.

I really doubt that a query could have enough subplans to make
that a problem.  But if I'm wrong, it's surely something we could
improve in a localized way later.

            regards, tom lane



Re: plan shape work

От
Richard Guo
Дата:
On Fri, Sep 26, 2025 at 11:37 AM Tom Lane <tgl@sss.pgh.pa.us> wrote:
> Richard Guo <guofenglinux@gmail.com> writes:
> > FWIW, I'm a bit concerned about the double for loop inside
> > choose_plan_name(), especially since the outer loop runs with a true
> > condition.  Maybe I'm just worrying over nothing, as we probably don't
> > expect a large number of subroots in practice, but the nested loops
> > still make me a little uneasy.

> I really doubt that a query could have enough subplans to make
> that a problem.  But if I'm wrong, it's surely something we could
> improve in a localized way later.

I'm concerned not only about the potential for a large number of
subplans but also because if there happens to be a bug within the
nested loops, the always-true condition in the outer loop could cause
an infinite loop.  However, if you're confident that the code inside
the loop is completely bug-free and will remain so through future
changes, then this shouldn't be an issue.

Looking at choose_plan_name(), IIUC, the nested loop is used to find
the next unused suffix number for a given name.  I'm wondering why not
simply iterate through glob->subplanNames once, check the suffix
number for each name matching the given base name, determine the
current maximum suffix, and then use "max_suffix + 1" as the next
unused suffix.  This approach requires only a single pass through the
list, and if there's a bug, the worst-case scenario would be a
duplicate name rather than an infinite loop.  It seems to me that this
approach is both more efficient and less risky.

- Richard



Re: plan shape work

От
Tom Lane
Дата:
Richard Guo <guofenglinux@gmail.com> writes:
> Looking at choose_plan_name(), IIUC, the nested loop is used to find
> the next unused suffix number for a given name.  I'm wondering why not
> simply iterate through glob->subplanNames once, check the suffix
> number for each name matching the given base name, determine the
> current maximum suffix, and then use "max_suffix + 1" as the next
> unused suffix.  This approach requires only a single pass through the
> list, and if there's a bug, the worst-case scenario would be a
> duplicate name rather than an infinite loop.  It seems to me that this
> approach is both more efficient and less risky.

"simply" is perhaps not the right adjective there.  My guess is that
this approach nets out to more code, more possibilities for bugs
(especially in cases where one name is a prefix of another), and
will be slower in typical cases with just a few subplan names.

As an example of edge cases that your idea introduces, what happens
if a user-written subquery name is "expr_999999999999999999999999"
and then we need to generate a unique name based on "expr"?  Now
we have an integer-overflow situation to worry about, with possibly
platform-dependent results.

But it's Robert's patch, so he gets to make the call.

            regards, tom lane



Re: plan shape work

От
Robert Haas
Дата:
On Fri, Sep 26, 2025 at 2:29 AM Richard Guo <guofenglinux@gmail.com> wrote:
> Looking at choose_plan_name(), IIUC, the nested loop is used to find
> the next unused suffix number for a given name.  I'm wondering why not
> simply iterate through glob->subplanNames once, check the suffix
> number for each name matching the given base name, determine the
> current maximum suffix, and then use "max_suffix + 1" as the next
> unused suffix.  This approach requires only a single pass through the
> list, and if there's a bug, the worst-case scenario would be a
> duplicate name rather than an infinite loop.  It seems to me that this
> approach is both more efficient and less risky.

I feel like the current coding is more straightforward and this should
be considered in terms of the chance of having bugs. Doing as you
propose here would require starting with a prefix match of the string
and then attempting a string-to-integer conversion on the remaining
bytes. That's certainly doable but such things tend to be a bit
fiddly: you have to make sure you do the right thing when you see a
non-digit and when the value overflows or is zero. It wouldn't be that
hard to get it right, but I think it would be a little trickier than
we have now. If we find that the performance cost of this function is
too high in some scenario, replacing it within an implementation along
these lines would make sense to me, but I am not too worried about the
current logic accidentally causing an infinite loop. I don't think
that will happen but even if it does it should be a simple fix.

--
Robert Haas
EDB: http://www.enterprisedb.com



Re: plan shape work

От
Richard Guo
Дата:
On Fri, Sep 26, 2025 at 11:23 PM Tom Lane <tgl@sss.pgh.pa.us> wrote:
> Richard Guo <guofenglinux@gmail.com> writes:
> > Looking at choose_plan_name(), IIUC, the nested loop is used to find
> > the next unused suffix number for a given name.  I'm wondering why not
> > simply iterate through glob->subplanNames once, check the suffix
> > number for each name matching the given base name, determine the
> > current maximum suffix, and then use "max_suffix + 1" as the next
> > unused suffix.  This approach requires only a single pass through the
> > list, and if there's a bug, the worst-case scenario would be a
> > duplicate name rather than an infinite loop.  It seems to me that this
> > approach is both more efficient and less risky.

> "simply" is perhaps not the right adjective there.  My guess is that
> this approach nets out to more code, more possibilities for bugs
> (especially in cases where one name is a prefix of another), and
> will be slower in typical cases with just a few subplan names.
>
> As an example of edge cases that your idea introduces, what happens
> if a user-written subquery name is "expr_999999999999999999999999"
> and then we need to generate a unique name based on "expr"?  Now
> we have an integer-overflow situation to worry about, with possibly
> platform-dependent results.

I'd argue that this hypothetical edge case can be resolved with a bit
of canonicalization in how subplan names are represented internally.
I think the issue you mentioned arises because there is no clearly
distinction between the base name and the numeric suffix.  I haven't
spent much time thinking about it, but an off-the-cuff idea is to
require that all subplan names in glob->subplanNames end with a suffix
of the form "_<number>".  (If no numeric suffix is required, we can
use the suffix "_0".)  With this convention, we can simply split
on the last underscore: everything before it is the base name, and
everything after is the numeric suffix.

The user-written subquery name "expr_999999999999999999999999" would
be internally represented as "expr_999999999999999999999999_0".  Then,
when we need to generate a unique name based on "expr", it won't match
with the base name of that subquery name.

With this canonicalization in place, my proposed approach is simply a
matter of applying strrchr(name, '_') and tracking the maximum suffix
number in a single pass over glob->subplanNames.  I think this can be
handled with straightforward, basic C code.  It seems to me that
this could also eliminate the need for the additional loop under the
"if (!always_number)" branch in choose_plan_name().

By replacing a pass over the subplanNames list plus a nested loop with
a single pass, I doubt that this would be slower in typical cases.

- Richard



Re: plan shape work

От
Tom Lane
Дата:
Richard Guo <guofenglinux@gmail.com> writes:
> On Fri, Sep 26, 2025 at 11:23 PM Tom Lane <tgl@sss.pgh.pa.us> wrote:
>> As an example of edge cases that your idea introduces, what happens
>> if a user-written subquery name is "expr_999999999999999999999999"
>> and then we need to generate a unique name based on "expr"?  Now
>> we have an integer-overflow situation to worry about, with possibly
>> platform-dependent results.

> I'd argue that this hypothetical edge case can be resolved with a bit
> of canonicalization in how subplan names are represented internally.

[ raised eyebrow... ]  How did you get to that from the complaint
that Robert's patch was not obviously bug-free?  (A complaint I
thought was unmerited, but nevermind.)  This proposal is neither
simple, nor obviously bug-free.  Moreover, in view of comments
upthread, I think we should look with great suspicion on any
proposal that involves changing user-supplied subquery aliases
unnecessarily.

            regards, tom lane



Re: plan shape work

От
Richard Guo
Дата:
On Mon, Sep 29, 2025 at 11:41 AM Tom Lane <tgl@sss.pgh.pa.us> wrote:
> Richard Guo <guofenglinux@gmail.com> writes:
> > On Fri, Sep 26, 2025 at 11:23 PM Tom Lane <tgl@sss.pgh.pa.us> wrote:
> >> As an example of edge cases that your idea introduces, what happens
> >> if a user-written subquery name is "expr_999999999999999999999999"
> >> and then we need to generate a unique name based on "expr"?  Now
> >> we have an integer-overflow situation to worry about, with possibly
> >> platform-dependent results.

> > I'd argue that this hypothetical edge case can be resolved with a bit
> > of canonicalization in how subplan names are represented internally.

> [ raised eyebrow... ]  How did you get to that from the complaint
> that Robert's patch was not obviously bug-free?  (A complaint I
> thought was unmerited, but nevermind.)

I'm not sure I fully understand your point here.  Apologies if I got
it wrong.

Firstly, my intention in the previous email was merely to propose a
solution for my approach to address the edge case you raised.  I don't
see how this relates to my so-called "complaint" about Robert's patch
not being obviously bug-free.  You raised a case where my approach
won't work, and I provided a solution to address it.  That's all.

Secondly, I don't think it's fair to characterize my concern as a
complaint when I expressed that the nested loop with an always-true
condition is vulnerable to bugs and could potentially cause an
infinite loop if such a bug exists.

In a nearby thread, I was asked whether I can guarantee my code is
100% bug-free.  After some consideration, I think I cannot make such a
guarantee, and I doubt that anyone realistically can.  Given this, I
think it's important that we try our best to write code that minimizes
the potential bad-effect should a bug occur.

Therefore, upon observing a nested loop with an always-true condition
in the patch, I expressed my concern and suggested a possible
improvement.  However, I did not expect that concern to be treated as
an unmerited complaint.

> This proposal is neither
> simple, nor obviously bug-free.  Moreover, in view of comments
> upthread, I think we should look with great suspicion on any
> proposal that involves changing user-supplied subquery aliases
> unnecessarily.

It seems no one has attempted to code up the approach I suggested, so
I went ahead and did it; please see the attached PoC patch.  It's just
a proof of concept to show what I have in mind, so please excuse the
lack of comments and necessary assertions for now.

I agree that this implementation cannot be guaranteed to be bug-free,
but I'm not sure I agree that it's not simple.  I'm also not convinced
that it would be slower in typical cases.

BTW, a small nit I just noticed: I suggest explicitly initializing
glob->subplanNames in standard_planner().  It may be argued that this
is pointless, as makeNode() zeroes all fields by default.  But AFAICS
subplanNames is the only field in PlannerGlobal that is not explicitly
initialized.

- Richard

Вложения

Re: plan shape work

От
Robert Haas
Дата:
On Mon, Sep 29, 2025 at 4:52 AM Richard Guo <guofenglinux@gmail.com> wrote:
> It seems no one has attempted to code up the approach I suggested, so
> I went ahead and did it; please see the attached PoC patch.  It's just
> a proof of concept to show what I have in mind, so please excuse the
> lack of comments and necessary assertions for now.

I think that if there are subqueries named expr_1 and expr_3, this
will assign the name expr_4 next, whereas the previous patch assigns
the name expr_2. That might not be a bug, but it's different and I
don't like it as well.

I also think that if there are subqueries named expr_1a and expr_2a,
this will assign the name expr_3 next, whereas the previous patch
assigns the name expr_1. I would consider that a clear bug.

Your code will also see expr_01 and decide that the next name should
be expr_2 rather than expr_1. I don't think that's right, either.

I also think that it's a bug that your function sometimes returns a
value different from the one it appends to canon_names.

I don't really understand why you're so fixed on this point. I think
that the code as I wrote it is quite a normal way to write code for
that kind of thing. Sure, there are other things that we could do, but
I wrote the code that way I did precisely in order to avoid behaviors
like the ones I mention above. It would be possible to rearrange the
code so that the termination condition for the loop was something like
"!found", or to rewrite the loop as a do { ... } while (!found)
construct as we do in set_rtable_names() for a very similar problem to
the code that this is solving, but I think the generated machine code
would be exactly the same and the code would not look as intuitive for
a human to read. Somebody else might have had a different stylistic
preference, but if you are going to object every time I write for (;;)
or while (1), you're going to hate an awful lot of my code for, IMHO,
very little reason. It's reasonable to be concerned about whether a
loop will ever terminate, but the mere fact of putting the loop exit
someplace other than the top of the loop isn't enough to say that
there's a problem.

--
Robert Haas
EDB: http://www.enterprisedb.com



Re: plan shape work

От
Tom Lane
Дата:
Robert Haas <robertmhaas@gmail.com> writes:
> I don't really understand why you're so fixed on this point. I think
> that the code as I wrote it is quite a normal way to write code for
> that kind of thing.

Also, we have numerous other places that generate de-duplicated names
in pretty much this way (ruleutils.c's set_rtable_names being a very
closely related case).  I don't think we should go inventing some
random new way to do that.

If it turns out that Robert's code is too slow in practice, I would
prefer to deal with that by using a hashtable to keep track of
already-allocated names, not by changing the user-visible behavior.
I'm content to wait for field complaints before building such logic
though, because I really doubt that queries would ever have so
many subplans as to be a problem.

            regards, tom lane



Re: plan shape work

От
Richard Guo
Дата:
On Mon, Sep 29, 2025 at 11:12 PM Robert Haas <robertmhaas@gmail.com> wrote:
> On Mon, Sep 29, 2025 at 4:52 AM Richard Guo <guofenglinux@gmail.com> wrote:
> > It seems no one has attempted to code up the approach I suggested, so
> > I went ahead and did it; please see the attached PoC patch.  It's just
> > a proof of concept to show what I have in mind, so please excuse the
> > lack of comments and necessary assertions for now.

> I think that if there are subqueries named expr_1 and expr_3, this
> will assign the name expr_4 next, whereas the previous patch assigns
> the name expr_2. That might not be a bug, but it's different and I
> don't like it as well.
>
> I also think that if there are subqueries named expr_1a and expr_2a,
> this will assign the name expr_3 next, whereas the previous patch
> assigns the name expr_1. I would consider that a clear bug.
>
> Your code will also see expr_01 and decide that the next name should
> be expr_2 rather than expr_1. I don't think that's right, either.
>
> I also think that it's a bug that your function sometimes returns a
> value different from the one it appends to canon_names.

I don't think any of the bugs you described upthread exist in the PoC
patch.  The patch ensures that all names stored in glob->subplanNames
are canonicalized to the format "$basename_$suffixnum".  If no numeric
suffix is required, the name is stored with a "_0" suffix.  This
guarantees a clear distinction between the base name and the numeric
suffix for all names stored in the list.  (Please note that this
canonicalization applies only to how names are stored internally, not
to user-visible names.  So no user-visible behavior change here.)

I don't really see how uncanonicalized names like expr_1a, expr_2a, or
expr_01 would appear in the subplanNames list to begin with.  Perhaps
you're referring to user-supplied subquery aliases?  In that case, the
patch deliberately avoids matching expr to those names, since it only
compares the base name.  As a result, it would assign expr_1 as the
next name, which is the expected behavior.

This PoC patch passes all the regression tests, which at least, IMHO,
suggests that it avoids such basic bugs.

However, since both you and Tom feel this proposal doesn't make
sense, I'll withdraw it.  Apologies for any trouble this has caused.

- Richard