plan shape work

Started by Robert Haas8 months ago82 messages
#1Robert Haas
robertmhaas@gmail.com
6 attachment(s)

Hi,

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.
This is a long way from something that actually looks like a usable
feature, but these are bits of infrastructure that I think will be
necessary to get to a usable feature. As a recap, my overall goal here
is to make it so that you can examine a finished plan, figure out what
decisions the planner made, and then somehow get the planner to make
those same decisions over again in a future planning cycle. Since
doing this for all types of planner decisions seems too difficult for
an initial goal, I'm focusing on scans and joins for now. A further
goal is that I want it to be possible for extensions to use this
infrastructure to implement a variety of different policies that they
might feel to be beneficial, so I'm looking to minimize the amount of
stuff that has to be done in core PostgreSQL or can only be used by
core PostgreSQL.

So far I've identified two main problems. First, you need to be able
to reconstruct the planner decisions from the final plan, which you
almost can do already but we're missing a few key pieces of
information in the final plan tree. Second, you need to be able to
write those decisions down in a way that can be correctly and
unambiguously reinterpreted during a future planning cycle for the
same query. For example, if we say that the planner chose a sequential
scan of table foo, what exactly does that mean? Table foo could appear
multiple times in the query, either in different subqueries or the
same one, and it could be a partitioned table with a different scan
method for each partition.

Let's start by talking about problem #1. I've found two subproblems in
this area so far. The first is that a Result node does not store the
relids of the scan or join that it replaces. Note that a Result note
whose job is to apply a one-time filter or a projection to some
subordinate node is not an issue here -- we can just look through the
Result node to whatever scan or join is beneath it. The concern here
is about the case where a scan or join is proven empty and entirely
replaced by a Result node that has "One-Time Filter: false". Patch
0001 adds that field, and patch 0002 teaches ExplainPreScanNodes about
it, which results in a number of regression test output changes that I
personally consider to be improvements -- with these patches, we
properly qualify some column references with a table alias as EXPLAIN
does in all other cases, as opposed to printing them as bare column
names with no alias. Patch 0003 checks that this is the only problem
of this type that is visible at the stage where we are constructing
join paths.

Still talking about problem #1, the second subproblem I've identified
is that during setrefs processing, we elide trivial SubqueryScan,
Append, and MergeAppend nodes from the final plan. So during planning
we might see, for example, that a certain join is between RTI 4 and
RTI 5 and it's, say, a hash join. But after setrefs processing, it may
appear that RTI was joined to, say, RTI 13, which might not even have
been part of the same subquery level. If we record that we want to see
RTI 4 joined to RTI 13 via a hash join, that will be completely
useless -- the join planning code will never see those two RTIs as
options to be joined to each other. What I've done in 0006 is made it
so that we keep a record of each node elided during setrefs
processing. This list of elided nodes is stored in the PlannedStmt
outside of the portion of the tree that actually gets executed, so
that code that is doing plan tree inspection can look at it but
execution doesn't get any slower (except possibly by having to copy a
slightly larger amount of data around when reading and writing
PlannedStmt objects, which seems like it should be negligible).

Now let's talk about problem #2. I believe that we do not actually
want to refer to what happened to RTI 4 and RTI 5 as I mooted in the
previous paragraph, but rather to refer to relations by some kind of
name. However, we can't use the names shown in the EXPLAIN output,
because those are not really present in the plan and are only assigned
on-the-fly by EXPLAIN; hence, they can't be known at plan time. Since
planning proceeds one subquery at a time, I think the right way to
approach this problem is to first name the subquery and then to name
the table within that subquery. 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.

To solve that problem, I decided to arrange for every subquery to have
a unique name that is assigned before we begin planning it. Patch 0004
does this. Then, 0005 records those names in the final plan. That's
enough that you can look at the scanrelid (or apprelids, etc.) of a
Plan node and relate that back to a named subquery and a particular
RTI within that subquery. There is still the problem of how to name
relations within a single subquery, since it's possible to reuse
aliases within the same subquery level (simple cases are rejected, but
there are at least two ways to bypass the error check, and they look
intentional, so we can't just block it). Then, references to a certain
alias name can be further multiplied by inheritance expansion. This is
all a bit hairy but I haven't really found any fundamental problems
here that keep you from deciding on a workable naming convention.

Hope you find this interesting. If you do, let me know what you think.

Thanks,

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

Attachments:

v1-0005-Store-information-about-range-table-flattening-in.patchapplication/octet-stream; name=v1-0005-Store-information-about-range-table-flattening-in.patchDownload
From 08fbb26856f36ca36b9842b6caf2ba32492e3720 Mon Sep 17 00:00:00 2001
From: Robert Haas <rhaas@postgresql.org>
Date: Fri, 21 Mar 2025 11:06:35 -0400
Subject: [PATCH v1 5/6] Store information about range-table flattening in the
 final plan.

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.  So,
this commit remembers in the final plan which part of the final range
table came from which subquery's range table.

Additionally, this commit teaches pg_overexplain'e RANGE_TABLE option
to display the subquery name for each range table entry.
---
 contrib/pg_overexplain/pg_overexplain.c | 36 +++++++++++++++++++++++++
 src/backend/optimizer/plan/planner.c    |  1 +
 src/backend/optimizer/plan/setrefs.c    | 20 ++++++++++++++
 src/include/nodes/pathnodes.h           |  3 +++
 src/include/nodes/plannodes.h           | 17 ++++++++++++
 src/tools/pgindent/typedefs.list        |  1 +
 6 files changed, 78 insertions(+)

diff --git a/contrib/pg_overexplain/pg_overexplain.c b/contrib/pg_overexplain/pg_overexplain.c
index bd70b6d9d5e..5dc707d69e3 100644
--- a/contrib/pg_overexplain/pg_overexplain.c
+++ b/contrib/pg_overexplain/pg_overexplain.c
@@ -395,6 +395,8 @@ static void
 overexplain_range_table(PlannedStmt *plannedstmt, ExplainState *es)
 {
 	Index		rti;
+	ListCell   *lc_subrtinfo = list_head(plannedstmt->subrtinfos);
+	SubPlanRTInfo *rtinfo = NULL;
 
 	/* Open group, one entry per RangeTblEntry */
 	ExplainOpenGroup("Range Table", "Range Table", false, es);
@@ -405,6 +407,18 @@ overexplain_range_table(PlannedStmt *plannedstmt, ExplainState *es)
 		RangeTblEntry *rte = rt_fetch(rti, plannedstmt->rtable);
 		char	   *kind = NULL;
 		char	   *relkind;
+		SubPlanRTInfo *next_rtinfo;
+
+		/* Advance to next SubRTInfo, if it's time. */
+		if (lc_subrtinfo != NULL)
+		{
+			next_rtinfo = lfirst(lc_subrtinfo);
+			if (rti > next_rtinfo->rtoffset)
+			{
+				rtinfo = next_rtinfo;
+				lc_subrtinfo = lnext(plannedstmt->subrtinfos, lc_subrtinfo);
+			}
+		}
 
 		/* NULL entries are possible; skip them */
 		if (rte == NULL)
@@ -469,6 +483,28 @@ overexplain_range_table(PlannedStmt *plannedstmt, ExplainState *es)
 			ExplainPropertyBool("In From Clause", rte->inFromCl, es);
 		}
 
+		/*
+		 * Indicate which subplan is the origin of which RTE. Note dummy
+		 * subplans. Here again, we crunch more onto one line in text format.
+		 */
+		if (rtinfo != NULL)
+		{
+			if (es->format == EXPLAIN_FORMAT_TEXT)
+			{
+				if (!rtinfo->dummy)
+					ExplainPropertyText("Subplan", rtinfo->plan_name, es);
+				else
+					ExplainPropertyText("Subplan",
+										psprintf("%s (dummy)",
+												 rtinfo->plan_name), es);
+			}
+			else
+			{
+				ExplainPropertyText("Subplan", rtinfo->plan_name, es);
+				ExplainPropertyBool("Subplan Is Dummy", rtinfo->dummy, es);
+			}
+		}
+
 		/* rte->alias is optional; rte->eref is requested */
 		if (rte->alias != NULL)
 			overexplain_alias("Alias", rte->alias, es);
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index 215491ccfd4..5dcd09e712a 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -571,6 +571,7 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions,
 	result->unprunableRelids = bms_difference(glob->allRelids,
 											  glob->prunableRelids);
 	result->permInfos = glob->finalrteperminfos;
+	result->subrtinfos = glob->subrtinfos;
 	result->resultRelations = glob->resultRelations;
 	result->firstResultRels = glob->firstResultRels;
 	result->appendRelations = glob->appendRelations;
diff --git a/src/backend/optimizer/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.c
index 7f241cddb4c..6f0d97f3936 100644
--- a/src/backend/optimizer/plan/setrefs.c
+++ b/src/backend/optimizer/plan/setrefs.c
@@ -395,6 +395,26 @@ add_rtes_to_flat_rtable(PlannerInfo *root, bool recursing)
 	Index		rti;
 	ListCell   *lc;
 
+	/*
+	 * Record enough information to make it possible for code that looks at
+	 * the final range table to understand how it was constructed. (If
+	 * finalrtable is still NIL, then this is the very topmost PlannerInfo,
+	 * which will always have plan_name == NULL and rtoffset == 0; we omit the
+	 * degenerate list entry.)
+	 */
+	if (root->glob->finalrtable != NIL)
+	{
+		SubPlanRTInfo *rtinfo = makeNode(SubPlanRTInfo);
+
+		rtinfo->plan_name = root->plan_name;
+		rtinfo->rtoffset = list_length(root->glob->finalrtable);
+
+		/* When recursing = true, it's an unplanned or dummy subquery. */
+		rtinfo->dummy = recursing;
+
+		root->glob->subrtinfos = lappend(root->glob->subrtinfos, rtinfo);
+	}
+
 	/*
 	 * Add the query's own RTEs to the flattened rangetable.
 	 *
diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h
index e009e131bc3..61ba04d014f 100644
--- a/src/include/nodes/pathnodes.h
+++ b/src/include/nodes/pathnodes.h
@@ -135,6 +135,9 @@ typedef struct PlannerGlobal
 	/* "flat" list of RTEPermissionInfos */
 	List	   *finalrteperminfos;
 
+	/* list of SubPlanRTInfo nodes */
+	List	   *subrtinfos;
+
 	/* "flat" list of PlanRowMarks */
 	List	   *finalrowmarks;
 
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index 782fb471b66..9df11cd394a 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -120,6 +120,9 @@ typedef struct PlannedStmt
 	 */
 	List	   *subplans;
 
+	/* a list of SubPlanRTInfo objects */
+	List	   *subrtinfos;
+
 	/* indices of subplans that require REWIND */
 	Bitmapset  *rewindPlanIDs;
 
@@ -1780,4 +1783,18 @@ typedef enum MonotonicFunction
 	MONOTONICFUNC_BOTH = MONOTONICFUNC_INCREASING | MONOTONICFUNC_DECREASING,
 } MonotonicFunction;
 
+/*
+ * SubPlanRTInfo
+ *
+ * Information about which range table entries came from which subquery
+ * planning cycles.
+ */
+typedef struct SubPlanRTInfo
+{
+	NodeTag		type;
+	char	   *plan_name;
+	Index		rtoffset;
+	bool		dummy;
+} SubPlanRTInfo;
+
 #endif							/* PLANNODES_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9ea573fae21..1c6a7252ee4 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -4309,3 +4309,4 @@ zic_t
 ExplainExtensionOption
 ExplainOptionHandler
 overexplain_options
+SubPlanRTInfo
-- 
2.39.3 (Apple Git-145)

v1-0004-Give-subplans-names-that-are-known-while-planning.patchapplication/octet-stream; name=v1-0004-Give-subplans-names-that-are-known-while-planning.patchDownload
From 624854b1dfec7f6c603b0275b44e4eaeb69783e0 Mon Sep 17 00:00:00 2001
From: Robert Haas <rhaas@postgresql.org>
Date: Thu, 5 Dec 2024 15:19:17 -0500
Subject: [PATCH v1 4/6] Give subplans names that are known while planning that
 subplan.

Previously, subplans were shown in EXPLAIN output identified by
a number, like "InitPlan 1", and some were identified by a name,
like "CTE foo". Now, each subplan gets a name, which for InitPlans
and SubPlans is based on the type of sublink e.g. expr_1 or any_1,
and these names are guaranteed to be unique across the whole plan.

The numerical portion of the name may be different than it was
previously, because InitPlan 1 meant the first subplan that we
finished planning (which happened to be an InitPlan). This number
couldn't be known at the time we began planning that subplan,
because the query planner might recurse into other subplans which
would then be fully planned before finishing the plan at the outer
level. These new subplan names are assigned when we *start* planning
a subplan, which allows extensions that affect planning to know the
name that will ultimately be assigned while planning is still in
progress.

Some subplans aren't shown as subplans in EXPLAIN output. This
happens when the subquery is a FROM-cluse item or a branch of a
set operation, rather than, for example, an expression that will
be transformed into something render as an InitPlan or SubPlan.
These subplans also get unique names, although those names are not
currently shown in the EXPLAIN output. This means that it's now
possible to use unique, human-readable names to refer to any
subplan within a query; only the topmost query level is nameless.
---
 .../postgres_fdw/expected/postgres_fdw.out    |  58 ++--
 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        |  83 +++--
 src/backend/optimizer/prep/prepjointree.c     |   4 +
 src/backend/optimizer/prep/prepunion.c        |   5 +-
 src/include/nodes/pathnodes.h                 |   6 +
 src/include/nodes/primnodes.h                 |   1 +
 src/include/optimizer/planner.h               |   5 +-
 src/test/regress/expected/aggregates.out      |  58 ++--
 src/test/regress/expected/create_index.out    |  14 +-
 src/test/regress/expected/groupingsets.out    |  90 +++---
 .../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            |  50 +--
 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 | 292 +++++++++---------
 src/test/regress/expected/portals.out         |  12 +-
 src/test/regress/expected/returning.out       |  20 +-
 src/test/regress/expected/rowsecurity.out     | 138 ++++-----
 src/test/regress/expected/rowtypes.out        |  12 +-
 src/test/regress/expected/select_parallel.out |  52 ++--
 src/test/regress/expected/sqljson.out         |   4 +-
 src/test/regress/expected/subselect.out       | 172 +++++------
 src/test/regress/expected/updatable_views.out |  48 +--
 src/test/regress/expected/update.out          |   8 +-
 src/test/regress/expected/window.out          |  10 +-
 src/test/regress/expected/with.out            |   8 +-
 33 files changed, 735 insertions(+), 605 deletions(-)

diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 536b1baa104..c8f70bed3e4 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -3169,10 +3169,10 @@ select exists(select 1 from pg_enum), sum(c1) from ft1;
                     QUERY PLAN                    
 --------------------------------------------------
  Foreign Scan
-   Output: (InitPlan 1).col1, (sum(ft1.c1))
+   Output: (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)
 
@@ -3187,8 +3187,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: (exists_1).col1, sum(ft1.c1)
+   InitPlan exists_1
      ->  Seq Scan on pg_catalog.pg_enum
    ->  Foreign Scan on public.ft1
          Output: ft1.c1
@@ -3347,15 +3347,15 @@ select distinct (select count(*) filter (where t2.c2 = 6 and t2.c1 < 10) from ft
                                                           QUERY PLAN                                                          
 ------------------------------------------------------------------------------------------------------------------------------
  Unique
-   Output: ((SubPlan 1))
+   Output: ((expr_1))
    ->  Sort
-         Output: ((SubPlan 1))
-         Sort Key: ((SubPlan 1))
+         Output: ((expr_1))
+         Sort Key: ((expr_1))
          ->  Foreign Scan
-               Output: (SubPlan 1)
+               Output: (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))
@@ -3373,14 +3373,14 @@ select distinct (select count(t1.c1) filter (where t2.c2 = 6 and t2.c1 < 10) fro
                                                                       QUERY PLAN                                                                      
 ------------------------------------------------------------------------------------------------------------------------------------------------------
  Unique
-   Output: ((SubPlan 1))
+   Output: ((expr_1))
    ->  Sort
-         Output: ((SubPlan 1))
-         Sort Key: ((SubPlan 1))
+         Output: ((expr_1))
+         Sort Key: ((expr_1))
          ->  Foreign Scan on public.ft2 t2
-               Output: (SubPlan 1)
+               Output: (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)
@@ -3412,14 +3412,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 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))
@@ -6438,9 +6438,9 @@ UPDATE ft2 AS target SET (c2, c7) = (
  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: (multiexpr_1).col1, (multiexpr_1).col2, (rescan 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"))
@@ -12017,9 +12017,9 @@ SELECT * FROM local_tbl t1 LEFT JOIN (SELECT *, (SELECT count(*) FROM async_pt W
                                        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, ((expr_1).col1)
    Join Filter: (t1.a = async_pt.a)
-   InitPlan 1
+   InitPlan expr_1
      ->  Aggregate
            Output: count(*)
            ->  Append
@@ -12031,10 +12031,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, (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, (expr_1).col1
                Remote SQL: SELECT a, b, c FROM public.base_tbl2 WHERE ((a < 3000))
 (20 rows)
 
@@ -12045,7 +12045,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)
@@ -12268,12 +12268,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 = (any_1).col1) AND ((random() > '0'::double precision) = (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 a4bec7ac323..7ba81fe4442 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -4891,6 +4891,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
@@ -4914,8 +4915,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 64605be3178..a3dc2e54eb9 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 49ad6e83578..215491ccfd4 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -432,7 +432,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);
@@ -649,9 +650,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;
@@ -666,6 +667,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;
@@ -703,6 +705,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
@@ -8250,3 +8255,61 @@ generate_setop_child_grouplist(SetOperationStmt *op, List *targetlist)
 
 	return grouplist;
 }
+
+/*
+ * Choose a unique plan name for subroot.
+ */
+char *
+choose_plan_name(PlannerGlobal *glob, 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 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 e7cb3fede66..d1af5a28a08 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 char *sublinktype_to_string(SubLinkType subLinkType);
 
 
 /*
@@ -172,6 +173,7 @@ make_subplan(PlannerInfo *root, Query *orig_subquery,
 	Plan	   *plan;
 	List	   *plan_params;
 	Node	   *result;
+	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, &paramIds);
 		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);
 	}
@@ -3151,7 +3151,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;
@@ -3167,3 +3168,31 @@ 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 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);
+}
diff --git a/src/backend/optimizer/prep/prepjointree.c b/src/backend/optimizer/prep/prepjointree.c
index 87dc6f56b57..c5547aa6877 100644
--- a/src/backend/optimizer/prep/prepjointree.c
+++ b/src/backend/optimizer/prep/prepjointree.c
@@ -1299,6 +1299,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;
@@ -1330,6 +1331,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 eab44da65b8..0f03f921e85 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/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h
index 1dd2d1560cb..e009e131bc3 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;
 
@@ -234,6 +237,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 7d3b4198f26..410cdbfa09e 100644
--- a/src/include/nodes/primnodes.h
+++ b/src/include/nodes/primnodes.h
@@ -1093,6 +1093,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 347c582a789..24e46473528 100644
--- a/src/include/optimizer/planner.h
+++ b/src/include/optimizer/planner.h
@@ -43,7 +43,7 @@ extern PlannedStmt *standard_planner(Query *parse, const char *query_string,
 									 ParamListInfo boundParams);
 
 extern PlannerInfo *subquery_planner(PlannerGlobal *glob, Query *parse,
-									 PlannerInfo *parent_root,
+									 char *plan_name, PlannerInfo *parent_root,
 									 bool hasRecursion, double tuple_fraction,
 									 SetOperationStmt *setops);
 
@@ -59,4 +59,7 @@ extern Path *get_cheapest_fractional_path(RelOptInfo *rel,
 
 extern Expr *preprocess_phv_expression(PlannerInfo *root, Expr *expr);
 
+extern char *choose_plan_name(PlannerGlobal *glob, 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 712a42e667c..51bf351feba 100644
--- a/src/test/regress/expected/aggregates.out
+++ b/src/test/regress/expected/aggregates.out
@@ -763,9 +763,9 @@ select array(select sum(x+y) s
                             QUERY PLAN                             
 -------------------------------------------------------------------
  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
      ->  Sort
            Output: (sum((x.x + y.y))), y.y
            Sort Key: (sum((x.x + y.y)))
@@ -941,7 +941,7 @@ explain (costs off)
 ------------------------------------------------------------
  Result
    Replaces: Aggregate
-   InitPlan 1
+   InitPlan minmax_1
      ->  Limit
            ->  Index Only Scan using tenk1_unique1 on tenk1
                  Index Cond: (unique1 IS NOT NULL)
@@ -959,7 +959,7 @@ explain (costs off)
 ---------------------------------------------------------------------
  Result
    Replaces: Aggregate
-   InitPlan 1
+   InitPlan minmax_1
      ->  Limit
            ->  Index Only Scan Backward using tenk1_unique1 on tenk1
                  Index Cond: (unique1 IS NOT NULL)
@@ -977,7 +977,7 @@ explain (costs off)
 ------------------------------------------------------------------------
  Result
    Replaces: Aggregate
-   InitPlan 1
+   InitPlan minmax_1
      ->  Limit
            ->  Index Only Scan Backward using tenk1_unique1 on tenk1
                  Index Cond: ((unique1 IS NOT NULL) AND (unique1 < 42))
@@ -995,7 +995,7 @@ explain (costs off)
 ------------------------------------------------------------------------
  Result
    Replaces: Aggregate
-   InitPlan 1
+   InitPlan minmax_1
      ->  Limit
            ->  Index Only Scan Backward using tenk1_unique1 on tenk1
                  Index Cond: ((unique1 IS NOT NULL) AND (unique1 > 42))
@@ -1019,7 +1019,7 @@ explain (costs off)
 ---------------------------------------------------------------------------
  Result
    Replaces: Aggregate
-   InitPlan 1
+   InitPlan minmax_1
      ->  Limit
            ->  Index Only Scan Backward using tenk1_unique1 on tenk1
                  Index Cond: ((unique1 IS NOT NULL) AND (unique1 > 42000))
@@ -1039,7 +1039,7 @@ explain (costs off)
 ----------------------------------------------------------------------------
  Result
    Replaces: Aggregate
-   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))
@@ -1057,7 +1057,7 @@ explain (costs off)
 --------------------------------------------------------------------------
  Result
    Replaces: Aggregate
-   InitPlan 1
+   InitPlan minmax_1
      ->  Limit
            ->  Index Only Scan using tenk1_thous_tenthous on tenk1
                  Index Cond: ((thousand = 33) AND (tenthous IS NOT NULL))
@@ -1076,10 +1076,10 @@ explain (costs off)
                                        QUERY PLAN                                        
 -----------------------------------------------------------------------------------------
  Seq Scan on int4_tbl
-   SubPlan 2
+   SubPlan expr_1
      ->  Result
            Replaces: Aggregate
-           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))
@@ -1102,8 +1102,8 @@ explain (costs off)
                              QUERY PLAN                              
 ---------------------------------------------------------------------
  HashAggregate
-   Group Key: (InitPlan 1).col1
-   InitPlan 1
+   Group Key: (minmax_1).col1
+   InitPlan minmax_1
      ->  Limit
            ->  Index Only Scan Backward using tenk1_unique2 on tenk1
                  Index Cond: (unique2 IS NOT NULL)
@@ -1122,8 +1122,8 @@ explain (costs off)
                              QUERY PLAN                              
 ---------------------------------------------------------------------
  Sort
-   Sort Key: ((InitPlan 1).col1)
-   InitPlan 1
+   Sort Key: ((minmax_1).col1)
+   InitPlan minmax_1
      ->  Limit
            ->  Index Only Scan Backward using tenk1_unique2 on tenk1
                  Index Cond: (unique2 IS NOT NULL)
@@ -1142,8 +1142,8 @@ explain (costs off)
                              QUERY PLAN                              
 ---------------------------------------------------------------------
  Sort
-   Sort Key: ((InitPlan 1).col1)
-   InitPlan 1
+   Sort Key: ((minmax_1).col1)
+   InitPlan minmax_1
      ->  Limit
            ->  Index Only Scan Backward using tenk1_unique2 on tenk1
                  Index Cond: (unique2 IS NOT NULL)
@@ -1162,8 +1162,8 @@ explain (costs off)
                              QUERY PLAN                              
 ---------------------------------------------------------------------
  Sort
-   Sort Key: (((InitPlan 1).col1 + 1))
-   InitPlan 1
+   Sort Key: (((minmax_1).col1 + 1))
+   InitPlan minmax_1
      ->  Limit
            ->  Index Only Scan Backward using tenk1_unique2 on tenk1
                  Index Cond: (unique2 IS NOT NULL)
@@ -1183,7 +1183,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)
@@ -1207,7 +1207,7 @@ explain (costs off)
 ----------------------------------------------------
  Result
    Replaces: Aggregate
-   InitPlan 1
+   InitPlan minmax_1
      ->  Limit
            ->  Result
                  One-Time Filter: (100 IS NOT NULL)
@@ -1239,7 +1239,7 @@ explain (costs off)
 ---------------------------------------------------------------------------------------------
  Result
    Replaces: Aggregate
-   InitPlan 1
+   InitPlan minmax_1
      ->  Limit
            ->  Merge Append
                  Sort Key: minmaxtest.f1
@@ -1250,7 +1250,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
@@ -1275,7 +1275,7 @@ explain (costs off)
                                          QUERY PLAN                                          
 ---------------------------------------------------------------------------------------------
  Unique
-   InitPlan 1
+   InitPlan minmax_1
      ->  Limit
            ->  Merge Append
                  Sort Key: minmaxtest.f1
@@ -1286,7 +1286,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
@@ -1298,7 +1298,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: ((minmax_1).col1), ((minmax_1).col1)
          ->  Result
                Replaces: Aggregate
 (27 rows)
@@ -1323,10 +1323,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: (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 9ade7b835e6..2119315190d 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, (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 = (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 07a37da79dd..39b7e1d2a35 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((expr_1)), ((expr_2))
+   Group Key: ((expr_2))
    ->  Sort
-         Output: ((SubPlan 2)), i1.q1
-         Sort Key: ((SubPlan 2))
+         Output: ((expr_2)), i1.q1
+         Sort Key: ((expr_2))
          ->  Seq Scan on public.int8_tbl i1
-               Output: (SubPlan 2), i1.q1
-               SubPlan 2
+               Output: (expr_2), i1.q1
+               SubPlan expr_2
                  ->  Result
                        Output: i1.q1
 (11 rows)
@@ -537,19 +537,19 @@ group by ss.x;
                  QUERY PLAN                 
 --------------------------------------------
  GroupAggregate
-   Output: (SubPlan 2), ((SubPlan 3))
-   Group Key: ((SubPlan 3))
+   Output: (expr_1), ((expr_3))
+   Group Key: ((expr_3))
    ->  Sort
-         Output: ((SubPlan 3)), i1.q1
-         Sort Key: ((SubPlan 3))
+         Output: ((expr_3)), i1.q1
+         Sort Key: ((expr_3))
          ->  Seq Scan on public.int8_tbl i1
-               Output: (SubPlan 3), i1.q1
-               SubPlan 3
+               Output: (expr_3), i1.q1
+               SubPlan expr_3
                  ->  Result
                        Output: i1.q1
-   SubPlan 2
+   SubPlan expr_1
      ->  Result
-           Output: GROUPING((SubPlan 1))
+           Output: GROUPING((expr_2))
 (14 rows)
 
 select (select grouping(ss.x))
@@ -592,7 +592,7 @@ explain (costs off)
 ------------------------------------------------------------
  Result
    Replaces: Aggregate
-   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: (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::integer END), t1.v
-   Sort Key: (CASE WHEN (GROUPING((SubPlan 2)) = 0) THEN ((SubPlan 3)) ELSE NULL::integer END) NULLS FIRST
+   Output: (GROUPING((expr_1))), ((expr_3)), (CASE WHEN (GROUPING((expr_2)) = 0) THEN ((expr_3)) ELSE NULL::integer END), t1.v
+   Sort Key: (CASE WHEN (GROUPING((expr_2)) = 0) THEN ((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::integer END, t1.v
+         Output: GROUPING((expr_1)), ((expr_3)), CASE WHEN (GROUPING((expr_2)) = 0) THEN ((expr_3)) ELSE NULL::integer END, t1.v
          Hash Key: t1.v
-         Hash Key: (SubPlan 3)
+         Hash Key: (expr_3)
          ->  Seq Scan on pg_temp.gstest5 t1
-               Output: (SubPlan 3), t1.v, t1.id
-               SubPlan 3
+               Output: (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::integer END), t1.v
-   Sort Key: (CASE WHEN (GROUPING((SubPlan 2)) = 0) THEN ((SubPlan 3)) ELSE NULL::integer END) NULLS FIRST
+   Output: (GROUPING((expr_1))), ((expr_3)), (CASE WHEN (GROUPING((expr_2)) = 0) THEN ((expr_3)) ELSE NULL::integer END), t1.v
+   Sort Key: (CASE WHEN (GROUPING((expr_2)) = 0) THEN ((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::integer END, t1.v
+         Output: GROUPING((expr_1)), ((expr_3)), CASE WHEN (GROUPING((expr_2)) = 0) THEN ((expr_3)) ELSE NULL::integer END, t1.v
          Hash Key: t1.v
-         Hash Key: (SubPlan 3)
+         Hash Key: (expr_3)
          ->  Seq Scan on pg_temp.gstest5 t1
-               Output: (SubPlan 3), t1.v, t1.id
-               SubPlan 3
+               Output: (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 b00219643b9..a505d8b19da 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, ((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, ((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 820e914caba..119112a4e33 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: (minmax_1).col1
    Replaces: Aggregate
-   InitPlan 1
+   InitPlan minmax_1
      ->  Limit
            Output: ((1 - matest0.id))
            ->  Result
@@ -1947,7 +1947,7 @@ SELECT min(x) FROM
 --------------------------------------------------------------------
  Result
    Replaces: Aggregate
-   InitPlan 1
+   InitPlan minmax_1
      ->  Limit
            ->  Merge Append
                  Sort Key: a.unique1
@@ -1966,7 +1966,7 @@ SELECT min(y) FROM
 --------------------------------------------------------------------
  Result
    Replaces: Aggregate
-   InitPlan 1
+   InitPlan minmax_1
      ->  Limit
            ->  Merge Append
                  Sort Key: a.unique1
@@ -2006,7 +2006,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))
@@ -2046,19 +2046,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: (multiexpr_1).col1, (multiexpr_1).col2, (rescan 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
@@ -2094,21 +2094,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: (multiexpr_1).col1, (multiexpr_1).col2, (rescan 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: (multiexpr_1).col1, (multiexpr_1).col2, (rescan 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);
@@ -3258,11 +3258,11 @@ explain (costs off) select min(a), max(a) from parted_minmax where b = '12345';
 ------------------------------------------------------------------------------------------------
  Result
    Replaces: Aggregate
-   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..028ae01e307 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(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 23913249a8f..e214bcb76f7 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 = (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: Aggregate
-                       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 any_1).col1)))
+   SubPlan any_1
      ->  Index Only Scan using tenk1_unique2 on tenk1 b
 (4 rows)
 
@@ -3688,11 +3688,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 = (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)
@@ -5496,13 +5496,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 = (any_1).col1) AND ((random() > '0'::double precision) = (any_1).col2)))
+         SubPlan any_1
            ->  Seq Scan on int8_tbl c
                  Filter: (q1 < b.unique1)
    ->  Hash
@@ -5844,7 +5844,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
@@ -6573,7 +6573,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
@@ -8600,8 +8600,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
@@ -8615,20 +8615,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 = (any_1).col1) AND ((random() > '0'::double precision) = (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: (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 = (expr_1).col1)
 (27 rows)
 
 select * from (values (0), (1)) v(id),
@@ -9343,13 +9343,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 = (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..e96769114db 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 = (expr_1)) AND ((expr_2) = (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: ((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: ((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: (((expr_1) = hjtest_1.id) AND ((expr_3) = (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: ((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: ((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 38dfaf021c9..7d4f8887cb6 100644
--- a/src/test/regress/expected/memoize.out
+++ b/src/test/regress/expected/memoize.out
@@ -428,8 +428,8 @@ WHERE unique1 < 3
 ----------------------------------------------------------------
  Index Scan using tenk1_unique1 on tenk1 t0
    Index Cond: (unique1 < 3)
-   Filter: EXISTS(SubPlan 1)
-   SubPlan 1
+   Filter: EXISTS(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 1025bdf86d0..9dc2509a2cb 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 < (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, (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 6b51f39f88b..c9c9ceb6d71 100644
--- a/src/test/regress/expected/partition_prune.out
+++ b/src/test/regress/expected/partition_prune.out
@@ -1921,15 +1921,15 @@ where asptab.id > ss.b::int;
    ->  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(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(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 < (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 < (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 < (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 < (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 < (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 < (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 = (expr_1).col1) OR (a = (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 = (expr_1).col1) OR (a = (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 = (expr_1).col1) OR (a = (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 = (expr_1).col1)
+         Filter: (b = (expr_2).col1)
          ->  Bitmap Index Scan on ab_a1_b1_a_idx (never executed)
-               Index Cond: (a = (InitPlan 1).col1)
+               Index Cond: (a = (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 = (expr_1).col1)
+         Filter: (b = (expr_2).col1)
          ->  Bitmap Index Scan on ab_a1_b2_a_idx (never executed)
-               Index Cond: (a = (InitPlan 1).col1)
+               Index Cond: (a = (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 = (expr_1).col1)
+         Filter: (b = (expr_2).col1)
          ->  Bitmap Index Scan on ab_a1_b3_a_idx (never executed)
-               Index Cond: (a = (InitPlan 1).col1)
+               Index Cond: (a = (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 = (expr_1).col1)
+         Filter: (b = (expr_2).col1)
          ->  Bitmap Index Scan on ab_a2_b1_a_idx (never executed)
-               Index Cond: (a = (InitPlan 1).col1)
+               Index Cond: (a = (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 = (expr_1).col1)
+         Filter: (b = (expr_2).col1)
          ->  Bitmap Index Scan on ab_a2_b2_a_idx (never executed)
-               Index Cond: (a = (InitPlan 1).col1)
+               Index Cond: (a = (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 = (expr_1).col1)
+         Filter: (b = (expr_2).col1)
          ->  Bitmap Index Scan on ab_a2_b3_a_idx (never executed)
-               Index Cond: (a = (InitPlan 1).col1)
+               Index Cond: (a = (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 = (expr_1).col1)
+         Filter: (b = (expr_2).col1)
          ->  Bitmap Index Scan on ab_a3_b1_a_idx (never executed)
-               Index Cond: (a = (InitPlan 1).col1)
+               Index Cond: (a = (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 = (expr_1).col1)
+         Filter: (b = (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 = (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 = (expr_1).col1)
+         Filter: (b = (expr_2).col1)
          ->  Bitmap Index Scan on ab_a3_b3_a_idx (never executed)
-               Index Cond: (a = (InitPlan 1).col1)
+               Index Cond: (a = (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 = (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 = (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 = (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 = (expr_1).col1)
    ->  Seq Scan on ab_a1_b2 ab_2 (never executed)
-         Filter: (b = (InitPlan 1).col1)
+         Filter: (b = (expr_1).col1)
    ->  Seq Scan on ab_a1_b3 ab_3 (never executed)
-         Filter: (b = (InitPlan 1).col1)
+         Filter: (b = (expr_1).col1)
    ->  Seq Scan on ab_a2_b1 ab_4 (actual rows=0.00 loops=1)
-         Filter: (b = (InitPlan 1).col1)
+         Filter: (b = (expr_1).col1)
    ->  Seq Scan on ab_a2_b2 ab_5 (never executed)
-         Filter: (b = (InitPlan 1).col1)
+         Filter: (b = (expr_1).col1)
    ->  Seq Scan on ab_a2_b3 ab_6 (never executed)
-         Filter: (b = (InitPlan 1).col1)
+         Filter: (b = (expr_1).col1)
    ->  Seq Scan on ab_a3_b1 ab_7 (actual rows=0.00 loops=1)
-         Filter: (b = (InitPlan 1).col1)
+         Filter: (b = (expr_1).col1)
    ->  Seq Scan on ab_a3_b2 ab_8 (never executed)
-         Filter: (b = (InitPlan 1).col1)
+         Filter: (b = (expr_1).col1)
    ->  Seq Scan on ab_a3_b3 ab_9 (never executed)
-         Filter: (b = (InitPlan 1).col1)
+         Filter: (b = (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 = (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 = (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 = (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 = (expr_1).col1)
    ->  Seq Scan on ab_a1_b1 ab_1 (actual rows=0.00 loops=1)
-         Filter: (b = (InitPlan 1).col1)
+         Filter: (b = (expr_1).col1)
    ->  Seq Scan on ab_a1_b2 ab_2 (never executed)
-         Filter: (b = (InitPlan 1).col1)
+         Filter: (b = (expr_1).col1)
    ->  Seq Scan on ab_a1_b3 ab_3 (never executed)
-         Filter: (b = (InitPlan 1).col1)
+         Filter: (b = (expr_1).col1)
    ->  Seq Scan on ab_a2_b1 ab_4 (actual rows=0.00 loops=1)
-         Filter: (b = (InitPlan 1).col1)
+         Filter: (b = (expr_1).col1)
    ->  Seq Scan on ab_a2_b2 ab_5 (never executed)
-         Filter: (b = (InitPlan 1).col1)
+         Filter: (b = (expr_1).col1)
    ->  Seq Scan on ab_a2_b3 ab_6 (never executed)
-         Filter: (b = (InitPlan 1).col1)
+         Filter: (b = (expr_1).col1)
    ->  Seq Scan on ab_a3_b1 ab_7 (actual rows=0.00 loops=1)
-         Filter: (b = (InitPlan 1).col1)
+         Filter: (b = (expr_1).col1)
    ->  Seq Scan on ab_a3_b2 ab_8 (never executed)
-         Filter: (b = (InitPlan 1).col1)
+         Filter: (b = (expr_1).col1)
    ->  Seq Scan on ab_a3_b3 ab_9 (never executed)
-         Filter: (b = (InitPlan 1).col1)
+         Filter: (b = (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 = (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 = (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 = (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 = (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 = (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 = (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 = (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 = (expr_1).col1)
                      ->  Seq Scan on ab_a2_b2 ab_a2_2 (never executed)
-                           Filter: (b = (InitPlan 1).col1)
+                           Filter: (b = (expr_1).col1)
                      ->  Seq Scan on ab_a2_b3 ab_a2_3 (never executed)
-                           Filter: (b = (InitPlan 1).col1)
+                           Filter: (b = (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 = (expr_1).col1)
    ->  Seq Scan on listp_2_1 listp_2 (never executed)
-         Filter: (a = (InitPlan 1).col1)
+         Filter: (a = (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) < (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) < (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) < (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 = (expr_1).col1)
    ->  Seq Scan on boolp_t boolp_2 (actual rows=0.00 loops=1)
-         Filter: (a = (InitPlan 1).col1)
+         Filter: (a = (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 = (expr_1).col1)
    ->  Seq Scan on boolp_t boolp_2 (never executed)
-         Filter: (a = (InitPlan 1).col1)
+         Filter: (a = (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: Aggregate
-           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 >= (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 >= (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 >= (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 = (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 = (expr_1).col1))
    ->  Result
-         One-Time Filter: (1 = (InitPlan 1).col1)
+         One-Time Filter: (1 = (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 = (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 = (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 = (expr_1).col1))
 (10 rows)
 
 execute q (1, 1);
@@ -4113,8 +4113,8 @@ select * from listp where a = (select 2) and b <> 10;
                      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 = (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 = (expr_1).col1)
          ->  Parallel Seq Scan on listp_12_2 listp_2 (never executed)
-               Filter: (a = (InitPlan 1).col1)
+               Filter: (a = (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 = (expr_2).col1)
                ->  Parallel Seq Scan on listp_12_2 listp_2 (actual rows=N loops=N)
-                     Filter: (a = (InitPlan 2).col1)
+                     Filter: (a = (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 = (expr_1).col1)
                ->  Parallel Seq Scan on listp_12_2 listp_5 (never executed)
-                     Filter: (a = (InitPlan 1).col1)
+                     Filter: (a = (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[(expr_1).col1, (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[(expr_1).col1, (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[(expr_1).col1, (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[(expr_1).col1, (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/returning.out b/src/test/regress/expected/returning.out
index 341b689f766..3df940ee8fc 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: (expr_1), (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
@@ -581,23 +581,23 @@ UPDATE foo SET f4 = 100 WHERE f1 = 5
                           QUERY PLAN                           
 ---------------------------------------------------------------
  Update on pg_temp.foo
-   Output: (SubPlan 1), (SubPlan 2), (SubPlan 3)
+   Output: (expr_1), (expr_2), (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: (expr_1), (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 729ea4d7604..5b987fa9d6b 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 <= (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 <= (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 <= (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 <= (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 <= (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 <= (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 <= (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 <= (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 <= (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 <= (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 <= (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 <= (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 <= (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 <= (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 <= (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 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 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 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 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 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 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 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 328b1e142c0..878baee2d59 100644
--- a/src/test/regress/expected/rowtypes.out
+++ b/src/test/regress/expected/rowtypes.out
@@ -1249,19 +1249,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: ((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: (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..3671d261f1f 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 any_1).col1) AND (four = (hashed 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 any_1).col1)))
+   SubPlan any_1
      ->  Seq Scan on tenk2
 (4 rows)
 
@@ -346,7 +346,7 @@ explain (costs off)
                       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 = (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 ((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 = (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 = (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(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: ((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..d660049f134 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: (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 bd25ceabdf5..8f786cac625 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 = (rowcompare_1).col1) AND (2 = (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 = (rowcompare_1).col1) AND (2 = (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 = (all_1).col1))
+   SubPlan all_1
      ->  Materialize
-           Output: ((InitPlan 1).col1)
-           InitPlan 1
+           Output: ((expr_1).col1)
+           InitPlan expr_1
              ->  Result
                    Output: 1
            ->  Result
-                 Output: (InitPlan 1).col1
+                 Output: (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(exists_1)
+   SubPlan exists_1
      ->  Limit
            ->  Seq Scan on int4_tbl i
                  Filter: (f1 = o.f1)
@@ -891,11 +891,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 any_1).col1))
+   SubPlan any_1
      ->  Append
            ->  Result
                  Output: 'bar'::name
@@ -915,11 +915,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 = (any_1).col1))
+   SubPlan any_1
      ->  Materialize
            Output: '("(1)")'::record
            ->  Result
@@ -958,11 +958,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 any_1).col1))
+   SubPlan any_1
      ->  Seq Scan on inner_text
 (4 rows)
 
@@ -979,11 +979,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 any_1).col1) AND ((q1)::text = (hashed any_1).col1)))
+   SubPlan any_1
      ->  Seq Scan on inner_text
 (4 rows)
 
@@ -1000,11 +1000,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 ((any_1).col1 = (q1)::text))
+   SubPlan any_1
      ->  Materialize
            ->  Seq Scan on inner_text
 (5 rows)
@@ -1023,12 +1023,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 exists_2).col1)) OR (ten < 0))
+         SubPlan exists_2
            ->  Index Only Scan using tenk1_unique1 on tenk1 k
 (5 rows)
 
@@ -1048,10 +1048,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(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)
@@ -1073,20 +1073,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(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 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
@@ -1122,14 +1122,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: (expr_1).col1, (expr_2).col1
+   InitPlan expr_1
      ->  Result
            Output: now()
-   InitPlan 2
+   InitPlan expr_2
      ->  Result
            Output: now()
 (8 rows)
@@ -1137,13 +1137,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: (expr_1).col1
+         InitPlan expr_1
            ->  Result
                  Output: random()
 (7 rows)
@@ -1154,12 +1154,12 @@ explain (verbose, costs off)
                               QUERY PLAN                              
 ----------------------------------------------------------------------
  Values Scan on "*VALUES*"
-   Output: (SubPlan 1), (SubPlan 2)
-   SubPlan 1
+   Output: (expr_1), (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)
@@ -1173,8 +1173,8 @@ explain (verbose, costs off)
  Subquery Scan on ss
    Output: ss.x, ss.x
    ->  Values Scan on "*VALUES*"
-         Output: (SubPlan 1)
-         SubPlan 1
+         Output: (expr_1)
+         SubPlan expr_1
            ->  Result
                  Output: random()
                  One-Time Filter: ("*VALUES*".column1 = "*VALUES*".column1)
@@ -1194,16 +1194,16 @@ where o.ten = 0;
                                                                                          QUERY PLAN                                                                                          
 ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  Aggregate
-   Output: sum((((ANY (i.ten = (hashed SubPlan 1).col1))))::integer)
+   Output: sum((((ANY (i.ten = (hashed any_1).col1))))::integer)
    ->  Nested Loop
-         Output: ((ANY (i.ten = (hashed SubPlan 1).col1)))
+         Output: ((ANY (i.ten = (hashed 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 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)
@@ -1412,7 +1412,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)
@@ -1439,12 +1439,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 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)
@@ -2543,14 +2543,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 = (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)
@@ -2560,14 +2560,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 = (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)
@@ -2577,14 +2577,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 = (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)
@@ -2646,7 +2646,7 @@ ON B.hundred in (SELECT min(c.hundred) FROM tenk2 C WHERE c.odd = b.odd);
                            Filter: (b.hundred = "ANY_subquery".min)
                            ->  Result
                                  Replaces: Aggregate
-                                 InitPlan 1
+                                 InitPlan minmax_1
                                    ->  Limit
                                          ->  Index Scan using tenk2_hundred on tenk2 c
                                                Index Cond: (hundred IS NOT NULL)
@@ -2890,7 +2890,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)
@@ -2906,7 +2906,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
@@ -2925,7 +2925,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..66747f8af82 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)
@@ -3170,18 +3170,18 @@ EXPLAIN (costs off) INSERT INTO rw_view1 VALUES (2, 'New row 2');
                         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: ((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: (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(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(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(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(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(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(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(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(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: (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..a0aac9d4377 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: (multiexpr_1).col1, (multiexpr_1).col2, (rescan 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..55719226bef 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((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 26c88505140..4156105685f 100644
--- a/src/test/regress/expected/with.out
+++ b/src/test/regress/expected/with.out
@@ -3168,7 +3168,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
@@ -3200,7 +3200,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
@@ -3243,11 +3243,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.39.3 (Apple Git-145)

v1-0003-Assert-that-RTIs-of-joined-rels-are-discoverable-.patchapplication/octet-stream; name=v1-0003-Assert-that-RTIs-of-joined-rels-are-discoverable-.patchDownload
From f28f3c2498855f8def62d65aed9db70cf3bc4f36 Mon Sep 17 00:00:00 2001
From: Robert Haas <rhaas@postgresql.org>
Date: Wed, 16 Apr 2025 08:32:00 -0400
Subject: [PATCH v1 3/6] Assert that RTIs of joined rels are discoverable from
 join plans.

Every RTI that appears in the joinrel's relid set should be findable
via the outer or inner plan, except for join RTIs which aren't
necessarily preserved in the final plan. This is a requirement if
we want to be able 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.

Note that this depends on the earlier commit to add a relids field to
Result nodes; without that change, a join tree involving two or more
Result nodes would be fundamentally ambiguous (and even a join tree
involving one could only be interpreted by guessing at its origin).
---
 src/backend/optimizer/plan/createplan.c | 169 ++++++++++++++++++++++++
 1 file changed, 169 insertions(+)

diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index ce0e7ee6b9d..ccc5f5d3e27 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -320,7 +320,14 @@ static ModifyTable *make_modifytable(PlannerInfo *root, Plan *subplan,
 									 int epqParam);
 static GatherMerge *create_gather_merge_plan(PlannerInfo *root,
 											 GatherMergePath *best_path);
+static void assert_join_preserves_scan_rtis(PlannerInfo *root, Path *best_path,
+											Plan *outer_plan,
+											Plan *inner_plan);
 
+#ifdef USE_ASSERT_CHECKING
+static Bitmapset *get_scanned_rtindexes(PlannerInfo *root, Plan *plan);
+static Bitmapset *remove_join_rtis(PlannerInfo *root, Bitmapset *bms);
+#endif
 
 /*
  * create_plan
@@ -4441,6 +4448,9 @@ create_nestloop_plan(PlannerInfo *root,
 
 	copy_generic_path_info(&join_plan->join.plan, &best_path->jpath.path);
 
+	assert_join_preserves_scan_rtis(root, &best_path->jpath.path, outer_plan,
+									inner_plan);
+
 	return join_plan;
 }
 
@@ -4795,6 +4805,9 @@ create_mergejoin_plan(PlannerInfo *root,
 	/* Costs of sort and material steps are included in path cost already */
 	copy_generic_path_info(&join_plan->join.plan, &best_path->jpath.path);
 
+	assert_join_preserves_scan_rtis(root, &best_path->jpath.path,
+									outer_plan, inner_plan);
+
 	return join_plan;
 }
 
@@ -4968,6 +4981,9 @@ create_hashjoin_plan(PlannerInfo *root,
 
 	copy_generic_path_info(&join_plan->join.plan, &best_path->jpath.path);
 
+	assert_join_preserves_scan_rtis(root, &best_path->jpath.path,
+									outer_plan, inner_plan);
+
 	return join_plan;
 }
 
@@ -7444,3 +7460,156 @@ is_projection_capable_plan(Plan *plan)
 	}
 	return true;
 }
+
+/*
+ * Check that the RTIs of the relations being joined at this level are
+ * properly reflected in the Plan tree.
+ *
+ * We expect to find every non-RTE_JOIN RTI from best_path->parent.relids
+ * mentioned in either the outer or inner subplan.
+ */
+static void
+assert_join_preserves_scan_rtis(PlannerInfo *root, Path *best_path,
+								Plan *outer_plan, Plan *inner_plan)
+{
+#ifdef USE_ASSERT_CHECKING
+	Bitmapset  *outer_scanrelids;
+	Bitmapset  *inner_scanrelids;
+	Bitmapset  *calculated_scanrelids;
+	Bitmapset  *filtered_joinrelids;
+
+	outer_scanrelids = get_scanned_rtindexes(root, outer_plan);
+	inner_scanrelids = get_scanned_rtindexes(root, inner_plan);
+	calculated_scanrelids = bms_union(outer_scanrelids, inner_scanrelids);
+	filtered_joinrelids = remove_join_rtis(root, best_path->parent->relids);
+
+	/* Any given scan RTI should appear on only one side or the other. */
+	Assert(!bms_overlap(inner_scanrelids, outer_scanrelids));
+
+	/*
+	 * If this assertion fails, it means that the set of range table indexes
+	 * that we found in the inner and outer path tree did not equal the set of
+	 * range table indexes that we found for this joinrel, even after
+	 * excluding RTE_JOIN range table indexes which are not expect to appear
+	 * in the plan tree.
+	 *
+	 * If this assertion fails due to the addition of a new executor node
+	 * type, you probably just need to update get_scanned_rtindexes to know
+	 * about the new node. See the header comments for that function for other
+	 * places to update at the same time.
+	 */
+	Assert(bms_equal(calculated_scanrelids, filtered_joinrelids));
+#endif
+}
+
+#ifdef USE_ASSERT_CHECKING
+/*
+ * Get the set of range table indexes that are scanned by a scan or join node,
+ * or any executor node that could appear beneath a scan or join node.
+ *
+ * We are uninterested in join RTIs here; we're only interested in which RTIs
+ * are scanned at or below a particular plan node, and only if that node can
+ * appear beneath a join.
+ *
+ * When adding new cases to this function, be sure to also update
+ * ExplainPreScanNode, ExplainNode, and overexplain_per_node_hook as
+ * appropriate.
+ */
+static Bitmapset *
+get_scanned_rtindexes(PlannerInfo *root, Plan *plan)
+{
+	switch (nodeTag(plan))
+	{
+		case T_SeqScan:
+		case T_SampleScan:
+		case T_IndexScan:
+		case T_IndexOnlyScan:
+		case T_BitmapHeapScan:
+		case T_TidScan:
+		case T_TidRangeScan:
+		case T_SubqueryScan:
+		case T_FunctionScan:
+		case T_TableFuncScan:
+		case T_ValuesScan:
+		case T_CteScan:
+		case T_NamedTuplestoreScan:
+		case T_WorkTableScan:
+			return bms_make_singleton(((Scan *) plan)->scanrelid);
+			break;
+		case T_ForeignScan:
+			return ((ForeignScan *) plan)->fs_base_relids;
+			break;
+		case T_CustomScan:
+			return ((CustomScan *) plan)->custom_relids;
+			break;
+		case T_Append:
+			return ((Append *) plan)->apprelids;
+			break;
+		case T_MergeAppend:
+			return ((MergeAppend *) plan)->apprelids;
+			break;
+		case T_Result:
+			if (plan->lefttree)
+				return get_scanned_rtindexes(root, plan->lefttree);
+			else
+				return remove_join_rtis(root, ((Result *) plan)->relids);
+			break;
+		case T_HashJoin:
+		case T_MergeJoin:
+		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;
+			}
+		case T_Sort:
+		case T_IncrementalSort:
+		case T_Unique:
+		case T_Agg:
+		case T_Hash:
+		case T_Gather:
+		case T_GatherMerge:
+		case T_Material:
+		case T_Memoize:
+			return get_scanned_rtindexes(root, plan->lefttree);
+			break;
+		default:
+			break;
+	}
+
+	return NULL;
+}
+
+/*
+ * Return a new Bitmapset containing only those range table indexes from the
+ * input set that do not reference an RTE where rtekind == RTE_JOIN.
+ */
+static Bitmapset *
+remove_join_rtis(PlannerInfo *root, Bitmapset *bms)
+{
+	int			rti = -1;
+
+	bms = bms_copy(bms);
+
+	while ((rti = bms_next_member(bms, rti)) >= 0)
+	{
+		RangeTblEntry *rte = planner_rt_fetch(rti, root);
+
+		if (rte->rtekind == RTE_JOIN)
+			bms = bms_del_member(bms, rti);
+	}
+
+	return bms;
+}
+#endif
-- 
2.39.3 (Apple Git-145)

v1-0001-Keep-track-of-what-RTIs-a-Result-node-is-scanning.patchapplication/octet-stream; name=v1-0001-Keep-track-of-what-RTIs-a-Result-node-is-scanning.patchDownload
From 9733f433fe23372b0b7522f384a88825cf59050e Mon Sep 17 00:00:00 2001
From: Robert Haas <rhaas@postgresql.org>
Date: Wed, 9 Apr 2025 10:14:31 -0400
Subject: [PATCH v1 1/6] Keep track of what RTIs a Result node is scanning.

Result nodes now include an RTI set, which is only non-NULL when they
have no subplan, and is taken from the relid set of the RelOptInfo
that the Result is generating.

Using that information, EXPLAIN now emits, where relevant, a "Replaces" line
that says whether it replaced a scan, a join, or an aggregate; and in the
former two cases, which relations were involved.

Likewise, pg_overexplain's EXPLAIN (RANGE_TABLE) now displays the RTIs
stored in a Result node just as it already does for other RTI-bearing
node types.
---
 contrib/file_fdw/expected/file_fdw.out        |   1 +
 .../expected/pg_overexplain.out               |   6 +-
 contrib/pg_overexplain/pg_overexplain.c       |  12 ++
 .../postgres_fdw/expected/postgres_fdw.out    |   9 +-
 src/backend/commands/explain.c                |  95 ++++++++++++
 src/backend/optimizer/plan/createplan.c       |  93 +++++++++---
 src/backend/optimizer/plan/setrefs.c          |   2 +
 src/include/nodes/plannodes.h                 |   9 ++
 src/test/regress/expected/aggregates.out      |  51 ++++---
 src/test/regress/expected/case.out            |  21 +--
 .../regress/expected/generated_virtual.out    |   3 +-
 src/test/regress/expected/groupingsets.out    |   3 +-
 src/test/regress/expected/inherit.out         |  28 ++--
 src/test/regress/expected/join.out            | 110 ++++++++------
 src/test/regress/expected/merge.out           |   3 +-
 .../regress/expected/partition_aggregate.out  |  21 +--
 src/test/regress/expected/partition_join.out  |  23 +--
 src/test/regress/expected/partition_prune.out | 141 ++++++++++--------
 src/test/regress/expected/predicate.out       |  12 +-
 src/test/regress/expected/rowsecurity.out     |  13 +-
 src/test/regress/expected/rowtypes.out        |   6 +-
 src/test/regress/expected/select.out          |   7 +-
 src/test/regress/expected/subselect.out       |   6 +-
 src/test/regress/expected/tsrf.out            |   6 +-
 24 files changed, 477 insertions(+), 204 deletions(-)

diff --git a/contrib/file_fdw/expected/file_fdw.out b/contrib/file_fdw/expected/file_fdw.out
index df8d43b3749..2b8bee8f506 100644
--- a/contrib/file_fdw/expected/file_fdw.out
+++ b/contrib/file_fdw/expected/file_fdw.out
@@ -319,6 +319,7 @@ SELECT explain_filter('EXPLAIN (VERBOSE, COSTS FALSE) SELECT * FROM agg_csv WHER
  Result
    Output: a, b
    One-Time Filter: false
+   Replaces: Scan on agg_csv
 
 \t off
 SELECT * FROM agg_csv WHERE a < 0;
diff --git a/contrib/pg_overexplain/expected/pg_overexplain.out b/contrib/pg_overexplain/expected/pg_overexplain.out
index 44120c388af..dd8adddb4a3 100644
--- a/contrib/pg_overexplain/expected/pg_overexplain.out
+++ b/contrib/pg_overexplain/expected/pg_overexplain.out
@@ -44,9 +44,10 @@ EXPLAIN (RANGE_TABLE) SELECT 1;
                 QUERY PLAN                
 ------------------------------------------
  Result  (cost=0.00..0.01 rows=1 width=4)
+   RTIs: 1
  RTI 1 (result):
    Eref: "*RESULT*" ()
-(3 rows)
+(4 rows)
 
 -- Create a partitioned table.
 CREATE TABLE vegetables (id serial, name text, genus text)
@@ -475,6 +476,7 @@ INSERT INTO vegetables (name, genus) VALUES ('broccoflower', 'brassica');
    Nominal RTI: 1
    Exclude Relation RTI: 0
    ->  Result
+         RTIs: 2
  RTI 1 (relation):
    Eref: vegetables (id, name, genus)
    Relation: vegetables
@@ -485,5 +487,5 @@ INSERT INTO vegetables (name, genus) VALUES ('broccoflower', 'brassica');
    Eref: "*RESULT*" ()
  Unprunable RTIs: 1
  Result RTIs: 1
-(14 rows)
+(15 rows)
 
diff --git a/contrib/pg_overexplain/pg_overexplain.c b/contrib/pg_overexplain/pg_overexplain.c
index de824566f8c..bd70b6d9d5e 100644
--- a/contrib/pg_overexplain/pg_overexplain.c
+++ b/contrib/pg_overexplain/pg_overexplain.c
@@ -236,6 +236,18 @@ overexplain_per_node_hook(PlanState *planstate, List *ancestors,
 									  ((MergeAppend *) plan)->apprelids,
 									  es);
 				break;
+			case T_Result:
+
+				/*
+				 * 'relids' is only meaningful when plan->lefttree is NULL,
+				 * but if somehow it ends up set when plan->lefttree is not
+				 * NULL, print it anyway.
+				 */
+				if (plan->lefttree == NULL ||
+					((Result *) plan)->relids != NULL)
+					overexplain_bitmapset("RTIs",
+										  ((Result *) plan)->relids,
+										  es);
 			default:
 				break;
 		}
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 2185b42bb4f..536b1baa104 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -7149,7 +7149,8 @@ EXPLAIN (VERBOSE, COSTS OFF) SELECT count(*) FROM ft1 WHERE c2 < 0;
    Output: count(*)
    ->  Result
          One-Time Filter: false
-(4 rows)
+         Replaces: Scan on ft1
+(5 rows)
 
 SELECT count(*) FROM ft1 WHERE c2 < 0;
  count 
@@ -7193,7 +7194,8 @@ EXPLAIN (VERBOSE, COSTS OFF) SELECT count(*) FROM ft1 WHERE c2 >= 0;
    Output: count(*)
    ->  Result
          One-Time Filter: false
-(4 rows)
+         Replaces: Scan on ft1
+(5 rows)
 
 SELECT count(*) FROM ft1 WHERE c2 >= 0;
  count 
@@ -8022,7 +8024,8 @@ DELETE FROM rem1 WHERE false;     -- currently can't be pushed down
    ->  Result
          Output: ctid
          One-Time Filter: false
-(5 rows)
+         Replaces: Scan on rem1
+(6 rows)
 
 -- Test with statement-level triggers
 CREATE TRIGGER trig_stmt_before
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 786ee865f14..bf0185e1ca1 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -147,6 +147,7 @@ static void show_buffer_usage(ExplainState *es, const BufferUsage *usage);
 static void show_wal_usage(ExplainState *es, const WalUsage *usage);
 static void show_memory_counters(ExplainState *es,
 								 const MemoryContextCounters *mem_counters);
+static void show_result_replacement_info(Result *result, ExplainState *es);
 static void ExplainIndexScanDetails(Oid indexid, ScanDirection indexorderdir,
 									ExplainState *es);
 static void ExplainScanTarget(Scan *plan, ExplainState *es);
@@ -2250,6 +2251,8 @@ ExplainNode(PlanState *planstate, List *ancestors,
 			if (plan->qual)
 				show_instrumentation_count("Rows Removed by Filter", 1,
 										   planstate, es);
+			if (plan->lefttree == NULL)
+				show_result_replacement_info(castNode(Result, plan), es);
 			break;
 		case T_ModifyTable:
 			show_modifytable_info(castNode(ModifyTableState, planstate), ancestors,
@@ -4743,6 +4746,98 @@ show_modifytable_info(ModifyTableState *mtstate, List *ancestors,
 		ExplainCloseGroup("Target Tables", "Target Tables", false, es);
 }
 
+/*
+ * Explain what a "Result" node replaced.
+ */
+static void
+show_result_replacement_info(Result *result, ExplainState *es)
+{
+	StringInfoData buf;
+	int			nrels = 0;
+	int			rti = -1;
+	bool		found_non_result = false;
+	char	   *s;
+
+	/*
+	 * If the Result node has a subplan, it didn't replace a scan, join, or
+	 * aggregate;
+	 */
+	Assert(result->plan.lefttree == NULL);
+
+	/*
+	 * If the relids set is empty, it didn't replace a scan or a join, so it
+	 * must have come from an upper rel. Currently, an aggregate seems to be
+	 * the only possibility. (We might want to add more details to the Result
+	 * node in the future to disambiguate; or to show the rels being
+	 * aggregated in the case of partitionwise aggregate.)
+	 */
+	if (bms_is_empty(result->relids))
+	{
+		ExplainPropertyText("Replaces", "Aggregate", es);
+		return;
+	}
+
+	/*
+	 * Build up a comma-separated list of user-facing names for the range
+	 * table entries in the relids set.
+	 */
+	initStringInfo(&buf);
+	while ((rti = bms_next_member(result->relids, rti)) >= 0)
+	{
+		RangeTblEntry *rte = rt_fetch(rti, es->rtable);
+		char	   *refname;
+
+		/*
+		 * add_outer_joins_to_relids will add join RTIs to the relids set of a
+		 * join; if that join is then replaced with a Result node, we may see
+		 * such RTIs here. But we want to completely ignore those here,
+		 * because "a LEFT JOIN b ON whatever" is a join between a and b, not
+		 * a join between a, b, and an unnamed join.
+		 */
+		if (rte->rtekind == RTE_JOIN)
+			continue;
+
+		/* Count the number of rels that aren't ignored completely. */
+		++nrels;
+
+		/* Work out what reference name to use and added it the string. */
+		refname = (char *) list_nth(es->rtable_names, rti - 1);
+		if (refname == NULL)
+			refname = rte->eref->aliasname;
+		if (buf.len > 0)
+			appendStringInfoString(&buf, ", ");
+		appendStringInfoString(&buf, refname);
+
+		/* Keep track of whether we see anything other than RTE_RESULT. */
+		if (rte->rtekind != RTE_RESULT)
+			found_non_result = true;
+	}
+
+	/*
+	 * If this Result node is because of a single RTE that is RTE_RESULT, it
+	 * is not really replacing anything at all, because there's no other
+	 * method for implementing a scan of such an RTE, so we don't display the
+	 * Replaces line in such cases.
+	 *
+	 * (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.)
+	 */
+	if (nrels <= 1 && !found_non_result)
+		return;
+
+	/*
+	 * We're replacing either a scan or a join, according to the number of
+	 * rels in the relids set.
+	 */
+	if (nrels > 1)
+		s = psprintf("Join on %s", buf.data);
+	else
+		s = psprintf("Scan on %s", buf.data);
+	ExplainPropertyText("Replaces", s, es);
+}
+
 /*
  * Explain the constituent plans of an Append, MergeAppend,
  * BitmapAnd, or BitmapOr node.
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index 4ad30b7627e..ce0e7ee6b9d 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -101,7 +101,8 @@ static Gather *create_gather_plan(PlannerInfo *root, GatherPath *best_path);
 static Plan *create_projection_plan(PlannerInfo *root,
 									ProjectionPath *best_path,
 									int flags);
-static Plan *inject_projection_plan(Plan *subplan, List *tlist, bool parallel_safe);
+static Plan *inject_projection_plan(Plan *subplan, List *tlist,
+									bool parallel_safe);
 static Sort *create_sort_plan(PlannerInfo *root, SortPath *best_path, int flags);
 static IncrementalSort *create_incrementalsort_plan(PlannerInfo *root,
 													IncrementalSortPath *best_path, int flags);
@@ -302,7 +303,10 @@ static SetOp *make_setop(SetOpCmd cmd, SetOpStrategy strategy,
 						 List *tlist, Plan *lefttree, Plan *righttree,
 						 List *groupList, long numGroups);
 static LockRows *make_lockrows(Plan *lefttree, List *rowMarks, int epqParam);
-static Result *make_result(List *tlist, Node *resconstantqual, Plan *subplan);
+static Result *make_gating_result(List *tlist, Node *resconstantqual,
+								  Plan *subplan);
+static Result *make_simple_result(List *tlist, Node *resconstantqual,
+								  Relids relids);
 static ProjectSet *make_project_set(List *tlist, Plan *subplan);
 static ModifyTable *make_modifytable(PlannerInfo *root, Plan *subplan,
 									 CmdType operation, bool canSetTag,
@@ -1024,6 +1028,7 @@ create_gating_plan(PlannerInfo *root, Path *path, Plan *plan,
 {
 	Plan	   *gplan;
 	Plan	   *splan;
+	Relids		relids = NULL;
 
 	Assert(gating_quals);
 
@@ -1031,7 +1036,9 @@ create_gating_plan(PlannerInfo *root, Path *path, Plan *plan,
 	 * We might have a trivial Result plan already.  Stacking one Result atop
 	 * another is silly, so if that applies, just discard the input plan.
 	 * (We're assuming its targetlist is uninteresting; it should be either
-	 * the same as the result of build_path_tlist, or a simplified version.)
+	 * the same as the result of build_path_tlist, or a simplified version.
+	 * However, we preserve the set of relids that it purports to scan and
+	 * attribute that to our replacement Result instead.)
 	 */
 	splan = plan;
 	if (IsA(plan, Result))
@@ -1040,7 +1047,10 @@ create_gating_plan(PlannerInfo *root, Path *path, Plan *plan,
 
 		if (rplan->plan.lefttree == NULL &&
 			rplan->resconstantqual == NULL)
+		{
 			splan = NULL;
+			relids = rplan->relids;
+		}
 	}
 
 	/*
@@ -1048,9 +1058,12 @@ create_gating_plan(PlannerInfo *root, Path *path, Plan *plan,
 	 * tlist; that's never a wrong choice, even if the parent node didn't ask
 	 * for CP_EXACT_TLIST.
 	 */
-	gplan = (Plan *) make_result(build_path_tlist(root, path),
-								 (Node *) gating_quals,
-								 splan);
+	if (splan == NULL)
+		gplan = (Plan *) make_simple_result(build_path_tlist(root, path),
+											(Node *) gating_quals, relids);
+	else
+		gplan = (Plan *) make_gating_result(build_path_tlist(root, path),
+											(Node *) gating_quals, splan);
 
 	/*
 	 * Notice that we don't change cost or size estimates when doing gating.
@@ -1245,10 +1258,10 @@ create_append_plan(PlannerInfo *root, AppendPath *best_path, int flags)
 		/* Generate a Result plan with constant-FALSE gating qual */
 		Plan	   *plan;
 
-		plan = (Plan *) make_result(tlist,
-									(Node *) list_make1(makeBoolConst(false,
-																	  false)),
-									NULL);
+		plan = (Plan *) make_simple_result(tlist,
+										   (Node *) list_make1(makeBoolConst(false,
+																			 false)),
+										   best_path->path.parent->relids);
 
 		copy_generic_path_info(plan, (Path *) best_path);
 
@@ -1596,7 +1609,8 @@ create_group_result_plan(PlannerInfo *root, GroupResultPath *best_path)
 	/* best_path->quals is just bare clauses */
 	quals = order_qual_clauses(root, best_path->quals);
 
-	plan = make_result(tlist, (Node *) quals, NULL);
+	plan = make_simple_result(tlist, (Node *) quals,
+							  best_path->path.parent->relids);
 
 	copy_generic_path_info(&plan->plan, (Path *) best_path);
 
@@ -2093,8 +2107,7 @@ create_projection_plan(PlannerInfo *root, ProjectionPath *best_path, int flags)
 	}
 	else
 	{
-		/* We need a Result node */
-		plan = (Plan *) make_result(tlist, NULL, subplan);
+		plan = (Plan *) make_gating_result(tlist, NULL, subplan);
 
 		copy_generic_path_info(plan, (Path *) best_path);
 	}
@@ -2118,7 +2131,7 @@ inject_projection_plan(Plan *subplan, List *tlist, bool parallel_safe)
 {
 	Plan	   *plan;
 
-	plan = (Plan *) make_result(tlist, NULL, subplan);
+	plan = (Plan *) make_gating_result(tlist, NULL, subplan);
 
 	/*
 	 * In principle, we should charge tlist eval cost plus cpu_per_tuple per
@@ -2588,7 +2601,8 @@ create_minmaxagg_plan(PlannerInfo *root, MinMaxAggPath *best_path)
 	/* Generate the output plan --- basically just a Result */
 	tlist = build_path_tlist(root, &best_path->path);
 
-	plan = make_result(tlist, (Node *) best_path->quals, NULL);
+	plan = make_simple_result(tlist, (Node *) best_path->quals,
+							  best_path->path.parent->relids);
 
 	copy_generic_path_info(&plan->plan, (Path *) best_path);
 
@@ -4039,7 +4053,8 @@ create_resultscan_plan(PlannerInfo *root, Path *best_path,
 			replace_nestloop_params(root, (Node *) scan_clauses);
 	}
 
-	scan_plan = make_result(tlist, (Node *) scan_clauses, NULL);
+	scan_plan = make_simple_result(tlist, (Node *) scan_clauses,
+								   best_path->parent->relids);
 
 	copy_generic_path_info(&scan_plan->plan, best_path);
 
@@ -7056,22 +7071,58 @@ make_limit(Plan *lefttree, Node *limitOffset, Node *limitCount,
 }
 
 /*
- * make_result
- *	  Build a Result plan node
+ * make_gating_result
+ *	  Build a Result plan node that performs projection of a subplan, and/or
+ *	  applies a one time filter (resconstantqual)
  */
 static Result *
-make_result(List *tlist,
-			Node *resconstantqual,
-			Plan *subplan)
+make_gating_result(List *tlist,
+				   Node *resconstantqual,
+				   Plan *subplan)
 {
 	Result	   *node = makeNode(Result);
 	Plan	   *plan = &node->plan;
 
+	Assert(subplan != NULL);
+
 	plan->targetlist = tlist;
 	plan->qual = NIL;
 	plan->lefttree = subplan;
 	plan->righttree = NULL;
 	node->resconstantqual = resconstantqual;
+	node->relids = NULL;
+
+	return node;
+}
+
+/*
+ * 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)
+ *
+ * 'relids' should be the relids set for this path's RelOptInfo. In essence,
+ * we're saying that this Result node generates all the tuples for that
+ * RelOptInfo. Note that the same consideration can never arise in
+ * make_gating_result(), because in that case the tuples are always coming
+ * from some subordinate node.
+ *
+ * NB: It would be nice to assert that the relids set is non-empty here,
+ * but it might be, because this could be a path for an upper rel.
+ */
+static Result *
+make_simple_result(List *tlist,
+				   Node *resconstantqual,
+				   Relids relids)
+{
+	Result	   *node = makeNode(Result);
+	Plan	   *plan = &node->plan;
+
+	plan->targetlist = tlist;
+	plan->qual = NIL;
+	plan->lefttree = NULL;
+	plan->righttree = NULL;
+	node->resconstantqual = resconstantqual;
+	node->relids = relids;
 
 	return node;
 }
diff --git a/src/backend/optimizer/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.c
index 150e9f060ee..7f241cddb4c 100644
--- a/src/backend/optimizer/plan/setrefs.c
+++ b/src/backend/optimizer/plan/setrefs.c
@@ -1052,6 +1052,8 @@ set_plan_refs(PlannerInfo *root, Plan *plan, int rtoffset)
 				/* resconstantqual can't contain any subplan variable refs */
 				splan->resconstantqual =
 					fix_scan_expr(root, splan->resconstantqual, rtoffset, 1);
+				/* adjust the relids set */
+				splan->relids = offset_relid_set(splan->relids, rtoffset);
 			}
 			break;
 		case T_ProjectSet:
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index 658d76225e4..782fb471b66 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -250,12 +250,21 @@ typedef struct Plan
  * If resconstantqual isn't NULL, it represents a one-time qualification
  * test (i.e., one that doesn't depend on any variables from the outer plan,
  * so needs to be evaluated only once).
+ *
+ * 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).
  * ----------------
  */
 typedef struct Result
 {
 	Plan		plan;
 	Node	   *resconstantqual;
+	Bitmapset  *relids;
 } Result;
 
 /* ----------------
diff --git a/src/test/regress/expected/aggregates.out b/src/test/regress/expected/aggregates.out
index 1f1ce2380af..712a42e667c 100644
--- a/src/test/regress/expected/aggregates.out
+++ b/src/test/regress/expected/aggregates.out
@@ -940,11 +940,12 @@ explain (costs off)
                          QUERY PLAN                         
 ------------------------------------------------------------
  Result
+   Replaces: Aggregate
    InitPlan 1
      ->  Limit
            ->  Index Only Scan using tenk1_unique1 on tenk1
                  Index Cond: (unique1 IS NOT NULL)
-(5 rows)
+(6 rows)
 
 select min(unique1) from tenk1;
  min 
@@ -957,11 +958,12 @@ explain (costs off)
                              QUERY PLAN                              
 ---------------------------------------------------------------------
  Result
+   Replaces: Aggregate
    InitPlan 1
      ->  Limit
            ->  Index Only Scan Backward using tenk1_unique1 on tenk1
                  Index Cond: (unique1 IS NOT NULL)
-(5 rows)
+(6 rows)
 
 select max(unique1) from tenk1;
  max  
@@ -974,11 +976,12 @@ explain (costs off)
                                QUERY PLAN                               
 ------------------------------------------------------------------------
  Result
+   Replaces: Aggregate
    InitPlan 1
      ->  Limit
            ->  Index Only Scan Backward using tenk1_unique1 on tenk1
                  Index Cond: ((unique1 IS NOT NULL) AND (unique1 < 42))
-(5 rows)
+(6 rows)
 
 select max(unique1) from tenk1 where unique1 < 42;
  max 
@@ -991,11 +994,12 @@ explain (costs off)
                                QUERY PLAN                               
 ------------------------------------------------------------------------
  Result
+   Replaces: Aggregate
    InitPlan 1
      ->  Limit
            ->  Index Only Scan Backward using tenk1_unique1 on tenk1
                  Index Cond: ((unique1 IS NOT NULL) AND (unique1 > 42))
-(5 rows)
+(6 rows)
 
 select max(unique1) from tenk1 where unique1 > 42;
  max  
@@ -1014,11 +1018,12 @@ explain (costs off)
                                 QUERY PLAN                                 
 ---------------------------------------------------------------------------
  Result
+   Replaces: Aggregate
    InitPlan 1
      ->  Limit
            ->  Index Only Scan Backward using tenk1_unique1 on tenk1
                  Index Cond: ((unique1 IS NOT NULL) AND (unique1 > 42000))
-(5 rows)
+(6 rows)
 
 select max(unique1) from tenk1 where unique1 > 42000;
  max 
@@ -1033,11 +1038,12 @@ explain (costs off)
                                  QUERY PLAN                                 
 ----------------------------------------------------------------------------
  Result
+   Replaces: Aggregate
    InitPlan 1
      ->  Limit
            ->  Index Only Scan Backward using tenk1_thous_tenthous on tenk1
                  Index Cond: ((thousand = 33) AND (tenthous IS NOT NULL))
-(5 rows)
+(6 rows)
 
 select max(tenthous) from tenk1 where thousand = 33;
  max  
@@ -1050,11 +1056,12 @@ explain (costs off)
                                 QUERY PLAN                                
 --------------------------------------------------------------------------
  Result
+   Replaces: Aggregate
    InitPlan 1
      ->  Limit
            ->  Index Only Scan using tenk1_thous_tenthous on tenk1
                  Index Cond: ((thousand = 33) AND (tenthous IS NOT NULL))
-(5 rows)
+(6 rows)
 
 select min(tenthous) from tenk1 where thousand = 33;
  min 
@@ -1071,11 +1078,12 @@ explain (costs off)
  Seq Scan on int4_tbl
    SubPlan 2
      ->  Result
+           Replaces: Aggregate
            InitPlan 1
              ->  Limit
                    ->  Index Only Scan using tenk1_unique1 on tenk1
                          Index Cond: ((unique1 IS NOT NULL) AND (unique1 > int4_tbl.f1))
-(7 rows)
+(8 rows)
 
 select f1, (select min(unique1) from tenk1 where unique1 > f1) AS gt
   from int4_tbl;
@@ -1100,7 +1108,8 @@ explain (costs off)
            ->  Index Only Scan Backward using tenk1_unique2 on tenk1
                  Index Cond: (unique2 IS NOT NULL)
    ->  Result
-(7 rows)
+         Replaces: Aggregate
+(8 rows)
 
 select distinct max(unique2) from tenk1;
  max  
@@ -1119,7 +1128,8 @@ explain (costs off)
            ->  Index Only Scan Backward using tenk1_unique2 on tenk1
                  Index Cond: (unique2 IS NOT NULL)
    ->  Result
-(7 rows)
+         Replaces: Aggregate
+(8 rows)
 
 select max(unique2) from tenk1 order by 1;
  max  
@@ -1138,7 +1148,8 @@ explain (costs off)
            ->  Index Only Scan Backward using tenk1_unique2 on tenk1
                  Index Cond: (unique2 IS NOT NULL)
    ->  Result
-(7 rows)
+         Replaces: Aggregate
+(8 rows)
 
 select max(unique2) from tenk1 order by max(unique2);
  max  
@@ -1157,7 +1168,8 @@ explain (costs off)
            ->  Index Only Scan Backward using tenk1_unique2 on tenk1
                  Index Cond: (unique2 IS NOT NULL)
    ->  Result
-(7 rows)
+         Replaces: Aggregate
+(8 rows)
 
 select max(unique2) from tenk1 order by max(unique2)+1;
  max  
@@ -1177,7 +1189,8 @@ explain (costs off)
                  Index Cond: (unique2 IS NOT NULL)
    ->  ProjectSet
          ->  Result
-(8 rows)
+               Replaces: Aggregate
+(9 rows)
 
 select max(unique2), generate_series(1,3) as g from tenk1 order by g desc;
  max  | g 
@@ -1193,12 +1206,13 @@ explain (costs off)
                      QUERY PLAN                     
 ----------------------------------------------------
  Result
+   Replaces: Aggregate
    InitPlan 1
      ->  Limit
            ->  Result
                  One-Time Filter: (100 IS NOT NULL)
                  ->  Seq Scan on tenk1
-(6 rows)
+(7 rows)
 
 select max(100) from tenk1;
  max 
@@ -1224,6 +1238,7 @@ explain (costs off)
                                          QUERY PLAN                                          
 ---------------------------------------------------------------------------------------------
  Result
+   Replaces: Aggregate
    InitPlan 1
      ->  Limit
            ->  Merge Append
@@ -1246,7 +1261,7 @@ explain (costs off)
                  ->  Index Only Scan using minmaxtest2i on minmaxtest2 minmaxtest_8
                        Index Cond: (f1 IS NOT NULL)
                  ->  Index Only Scan Backward using minmaxtest3i on minmaxtest3 minmaxtest_9
-(23 rows)
+(24 rows)
 
 select min(f1), max(f1) from minmaxtest;
  min | max 
@@ -1285,7 +1300,8 @@ explain (costs off)
    ->  Sort
          Sort Key: ((InitPlan 1).col1), ((InitPlan 2).col1)
          ->  Result
-(26 rows)
+               Replaces: Aggregate
+(27 rows)
 
 select distinct min(f1), max(f1) from minmaxtest;
  min | max 
@@ -1315,7 +1331,8 @@ explain (costs off)
                    ->  Seq Scan on int4_tbl t1
                          Filter: ((f1 IS NOT NULL) AND (f1 = t0.f1))
            ->  Result
-(9 rows)
+                 Replaces: Aggregate
+(10 rows)
 
 select f1, (select distinct min(t1.f1) from int4_tbl t1 where t1.f1 = t0.f1)
 from int4_tbl t0;
diff --git a/src/test/regress/expected/case.out b/src/test/regress/expected/case.out
index efee7fc4317..d74b0dd68cc 100644
--- a/src/test/regress/expected/case.out
+++ b/src/test/regress/expected/case.out
@@ -266,27 +266,30 @@ SELECT *
 -- Tests for constant subexpression simplification
 explain (costs off)
 SELECT * FROM CASE_TBL WHERE NULLIF(1, 2) = 2;
-        QUERY PLAN        
---------------------------
+          QUERY PLAN          
+------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on case_tbl
+(3 rows)
 
 explain (costs off)
 SELECT * FROM CASE_TBL WHERE NULLIF(1, 1) IS NOT NULL;
-        QUERY PLAN        
---------------------------
+          QUERY PLAN          
+------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on case_tbl
+(3 rows)
 
 explain (costs off)
 SELECT * FROM CASE_TBL WHERE NULLIF(1, null) = 2;
-        QUERY PLAN        
---------------------------
+          QUERY PLAN          
+------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on case_tbl
+(3 rows)
 
 --
 -- Examples of updates involving tables
diff --git a/src/test/regress/expected/generated_virtual.out b/src/test/regress/expected/generated_virtual.out
index 6300e7c1d96..1793c3f3578 100644
--- a/src/test/regress/expected/generated_virtual.out
+++ b/src/test/regress/expected/generated_virtual.out
@@ -1561,7 +1561,8 @@ select t2.* from gtest32 t1 left join gtest32 t2 on false;
    ->  Result
          Output: a, 20, COALESCE(a, 100)
          One-Time Filter: false
-(8 rows)
+         Replaces: Scan on t2
+(9 rows)
 
 select t2.* from gtest32 t1 left join gtest32 t2 on false;
  a | b | c | d 
diff --git a/src/test/regress/expected/groupingsets.out b/src/test/regress/expected/groupingsets.out
index 35e4cb47ebe..07a37da79dd 100644
--- a/src/test/regress/expected/groupingsets.out
+++ b/src/test/regress/expected/groupingsets.out
@@ -591,11 +591,12 @@ explain (costs off)
                          QUERY PLAN                         
 ------------------------------------------------------------
  Result
+   Replaces: Aggregate
    InitPlan 1
      ->  Limit
            ->  Index Only Scan using tenk1_unique1 on tenk1
                  Index Cond: (unique1 IS NOT NULL)
-(5 rows)
+(6 rows)
 
 -- Views with GROUPING SET queries
 CREATE VIEW gstest_view AS select a, b, grouping(a,b), sum(c), count(*), max(c)
diff --git a/src/test/regress/expected/inherit.out b/src/test/regress/expected/inherit.out
index f9b0c415cfd..820e914caba 100644
--- a/src/test/regress/expected/inherit.out
+++ b/src/test/regress/expected/inherit.out
@@ -581,7 +581,8 @@ update some_tab set a = a + 1 where false;
    ->  Result
          Output: (some_tab.a + 1), NULL::oid, NULL::tid
          One-Time Filter: false
-(4 rows)
+         Replaces: Scan on some_tab
+(5 rows)
 
 update some_tab set a = a + 1 where false;
 explain (verbose, costs off)
@@ -593,7 +594,8 @@ update some_tab set a = a + 1 where false returning b, a;
    ->  Result
          Output: (some_tab.a + 1), NULL::oid, NULL::tid
          One-Time Filter: false
-(5 rows)
+         Replaces: Scan on some_tab
+(6 rows)
 
 update some_tab set a = a + 1 where false returning b, a;
  b | a 
@@ -700,7 +702,8 @@ explain update parted_tab set a = 2 where false;
  Update on parted_tab  (cost=0.00..0.00 rows=0 width=0)
    ->  Result  (cost=0.00..0.00 rows=0 width=10)
          One-Time Filter: false
-(3 rows)
+         Replaces: Scan on parted_tab
+(4 rows)
 
 drop table parted_tab;
 -- Check UPDATE with multi-level partitioned inherited target
@@ -1756,6 +1759,7 @@ explain (verbose, costs off) select min(1-id) from matest0;
 ---------------------------------------------------------------------------------
  Result
    Output: (InitPlan 1).col1
+   Replaces: Aggregate
    InitPlan 1
      ->  Limit
            Output: ((1 - matest0.id))
@@ -1779,7 +1783,7 @@ explain (verbose, costs off) select min(1-id) from matest0;
                        ->  Index Scan using matest3i on public.matest3 matest0_4
                              Output: matest0_4.id, (1 - matest0_4.id)
                              Index Cond: ((1 - matest0_4.id) IS NOT NULL)
-(25 rows)
+(26 rows)
 
 select min(1-id) from matest0;
  min 
@@ -1942,6 +1946,7 @@ SELECT min(x) FROM
                              QUERY PLAN                             
 --------------------------------------------------------------------
  Result
+   Replaces: Aggregate
    InitPlan 1
      ->  Limit
            ->  Merge Append
@@ -1950,7 +1955,7 @@ SELECT min(x) FROM
                        Index Cond: (unique1 IS NOT NULL)
                  ->  Index Only Scan using tenk1_unique2 on tenk1 b
                        Index Cond: (unique2 IS NOT NULL)
-(9 rows)
+(10 rows)
 
 explain (costs off)
 SELECT min(y) FROM
@@ -1960,6 +1965,7 @@ SELECT min(y) FROM
                              QUERY PLAN                             
 --------------------------------------------------------------------
  Result
+   Replaces: Aggregate
    InitPlan 1
      ->  Limit
            ->  Merge Append
@@ -1968,7 +1974,7 @@ SELECT min(y) FROM
                        Index Cond: (unique1 IS NOT NULL)
                  ->  Index Only Scan using tenk1_unique2 on tenk1 b
                        Index Cond: (unique2 IS NOT NULL)
-(9 rows)
+(10 rows)
 
 -- XXX planner doesn't recognize that index on unique2 is sufficiently sorted
 explain (costs off)
@@ -3085,11 +3091,12 @@ explain (costs off) select * from range_list_parted where a between 3 and 23 and
 
 /* Should select no rows because range partition key cannot be null */
 explain (costs off) select * from range_list_parted where a is null;
-        QUERY PLAN        
---------------------------
+              QUERY PLAN               
+---------------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on range_list_parted
+(3 rows)
 
 /* Should only select rows from the null-accepting partition */
 explain (costs off) select * from range_list_parted where b is null;
@@ -3250,6 +3257,7 @@ explain (costs off) select min(a), max(a) from parted_minmax where b = '12345';
                                            QUERY PLAN                                           
 ------------------------------------------------------------------------------------------------
  Result
+   Replaces: Aggregate
    InitPlan 1
      ->  Limit
            ->  Index Only Scan using parted_minmax1i on parted_minmax1 parted_minmax
@@ -3258,7 +3266,7 @@ explain (costs off) select min(a), max(a) from parted_minmax where b = '12345';
      ->  Limit
            ->  Index Only Scan Backward using parted_minmax1i on parted_minmax1 parted_minmax_1
                  Index Cond: ((a IS NOT NULL) AND (b = '12345'::text))
-(9 rows)
+(10 rows)
 
 select min(a), max(a) from parted_minmax where b = '12345';
  min | max 
diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out
index f35a0b18c37..f298aa7c1dc 100644
--- a/src/test/regress/expected/join.out
+++ b/src/test/regress/expected/join.out
@@ -2264,11 +2264,12 @@ explain (costs off)
 select aa, bb, unique1, unique1
   from tenk1 right join b_star on aa = unique1
   where bb < bb and bb is null;
-        QUERY PLAN        
---------------------------
+            QUERY PLAN             
+-----------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Join on tenk1, b_star
+(3 rows)
 
 select aa, bb, unique1, unique1
   from tenk1 right join b_star on aa = unique1
@@ -2386,11 +2387,12 @@ order by t1.unique1;
                            Index Cond: (unique1 < 10)
                SubPlan 2
                  ->  Result
+                       Replaces: Aggregate
                        InitPlan 1
                          ->  Limit
                                ->  Index Only Scan using tenk1_unique1 on tenk1
                                      Index Cond: ((unique1 IS NOT NULL) AND (unique1 = t2.unique1))
-(19 rows)
+(20 rows)
 
 -- Ensure we get the expected result
 select t1.unique1,t2.unique1 from tenk1 t1
@@ -2655,8 +2657,8 @@ select * from int8_tbl t1 left join
     (int8_tbl t2 left join int8_tbl t3 full join int8_tbl t4 on false on false)
     left join int8_tbl t5 on t2.q1 = t5.q1
 on t2.q2 = 123;
-                    QUERY PLAN                    
---------------------------------------------------
+                     QUERY PLAN                     
+----------------------------------------------------
  Nested Loop Left Join
    ->  Seq Scan on int8_tbl t1
    ->  Materialize
@@ -2668,8 +2670,9 @@ on t2.q2 = 123;
                            Filter: (q2 = 123)
                      ->  Result
                            One-Time Filter: false
+                           Replaces: Join on t3, t4
                ->  Seq Scan on int8_tbl t5
-(12 rows)
+(13 rows)
 
 explain (costs off)
 select * from int8_tbl t1
@@ -4103,7 +4106,8 @@ from int4_tbl t1
  Result
    Output: (current_database())::information_schema.sql_identifier, (c.relname)::information_schema.sql_identifier
    One-Time Filter: false
-(3 rows)
+   Replaces: Join on t1, t2, a, c, nc, t, nt, bt, nbt
+(4 rows)
 
 -- Test handling of qual pushdown to appendrel members with non-Var outputs
 explain (verbose, costs off)
@@ -4175,11 +4179,12 @@ select unique1 from tenk1, lateral f_immutable_int4(1) x where x = unique1;
 
 explain (costs off)
 select unique1 from tenk1, lateral f_immutable_int4(1) x where x in (select 17);
-        QUERY PLAN        
---------------------------
+        QUERY PLAN         
+---------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on tenk1
+(3 rows)
 
 explain (costs off)
 select unique1, x from tenk1 join f_immutable_int4(1) x on unique1 = x;
@@ -4225,11 +4230,12 @@ select unique1, x from tenk1 full join f_immutable_int4(1) x on unique1 = x;
 -- check that pullup of a const function allows further const-folding
 explain (costs off)
 select unique1 from tenk1, f_immutable_int4(1) x where x = 42;
-        QUERY PLAN        
---------------------------
+        QUERY PLAN         
+---------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on tenk1
+(3 rows)
 
 -- test inlining of immutable functions with PlaceHolderVars
 explain (costs off)
@@ -5086,7 +5092,8 @@ left join
    ->  Result
    ->  Result
          One-Time Filter: false
-(5 rows)
+         Replaces: Join on c, n
+(6 rows)
 
 -- check handling of apparently-commutable outer joins with non-commutable
 -- joins between them
@@ -5280,12 +5287,13 @@ select 1 from
      right join (select 1 as z) as ss2 on true)
   on false,
   lateral (select i4.f1, ss1.n from int8_tbl as i8 limit 1) as ss3;
-        QUERY PLAN        
---------------------------
+                  QUERY PLAN                   
+-----------------------------------------------
  Result
    Output: 1
    One-Time Filter: false
-(3 rows)
+   Replaces: Join on i4, ss3, x1, x2, *RESULT*
+(4 rows)
 
 select 1 from
   int4_tbl as i4
@@ -5314,11 +5322,12 @@ select 1 from t t1
        on false
      where t3.a = coalesce(t5.a,1)) as s2
   on true;
-        QUERY PLAN        
---------------------------
+                 QUERY PLAN                 
+--------------------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Join on t1, s1, t2, t3, t4, t5
+(3 rows)
 
 rollback;
 --
@@ -5715,14 +5724,15 @@ from int4_tbl as t1
                         inner join int8_tbl as t7 on null)
                on t5.q1 = t7.q2)
     on false;
-           QUERY PLAN           
---------------------------------
+                    QUERY PLAN                    
+--------------------------------------------------
  Nested Loop Left Join
    Join Filter: false
    ->  Seq Scan on int4_tbl t1
    ->  Result
          One-Time Filter: false
-(5 rows)
+         Replaces: Join on t2, t3, t4, t5, t7, t6
+(6 rows)
 
 -- variant with Var rather than PHV coming from t6
 explain (costs off)
@@ -5737,14 +5747,15 @@ from int4_tbl as t1
                         inner join int8_tbl as t7 on null)
                on t5.q1 = t7.q2)
     on false;
-           QUERY PLAN           
---------------------------------
+                    QUERY PLAN                    
+--------------------------------------------------
  Nested Loop Left Join
    Join Filter: false
    ->  Seq Scan on int4_tbl t1
    ->  Result
          One-Time Filter: false
-(5 rows)
+         Replaces: Join on t2, t3, t4, t5, t7, t6
+(6 rows)
 
 -- per further discussion of bug #17781
 explain (costs off)
@@ -5794,15 +5805,16 @@ select * from int8_tbl t1 left join
   (int8_tbl t2 inner join int8_tbl t3 on false
    left join int8_tbl t4 on t2.q2 = t4.q2)
 on t1.q1 = t2.q1;
-              QUERY PLAN              
---------------------------------------
+                 QUERY PLAN                 
+--------------------------------------------
  Hash Left Join
    Hash Cond: (t1.q1 = q1)
    ->  Seq Scan on int8_tbl t1
    ->  Hash
          ->  Result
                One-Time Filter: false
-(6 rows)
+               Replaces: Join on t2, t3, t4
+(7 rows)
 
 -- deduce constant-false from an EquivalenceClass
 explain (costs off)
@@ -5810,15 +5822,16 @@ select * from int8_tbl t1 left join
   (int8_tbl t2 inner join int8_tbl t3 on (t2.q1-t3.q2) = 0 and (t2.q1-t3.q2) = 1
    left join int8_tbl t4 on t2.q2 = t4.q2)
 on t1.q1 = t2.q1;
-              QUERY PLAN              
---------------------------------------
+                 QUERY PLAN                 
+--------------------------------------------
  Hash Left Join
    Hash Cond: (t1.q1 = q1)
    ->  Seq Scan on int8_tbl t1
    ->  Hash
          ->  Result
                One-Time Filter: false
-(6 rows)
+               Replaces: Join on t2, t3, t4
+(7 rows)
 
 -- pseudoconstant based on an outer-level Param
 explain (costs off)
@@ -6058,7 +6071,8 @@ select p.* from
 --------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on p
+(3 rows)
 
 select p.* from
   (parent p left join child c on (p.k = c.k)) join parent x on p.k = x.k
@@ -6075,7 +6089,8 @@ select p.* from
 --------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Join on p, x
+(3 rows)
 
 -- bug 5255: this is not optimizable by join removal
 begin;
@@ -6148,7 +6163,8 @@ SELECT q2 FROM
    ->  Result
          Output: q2, 'constant'::text
          One-Time Filter: false
-(9 rows)
+         Replaces: Scan on int8_tbl
+(10 rows)
 
 -- join removal bug #17786: check that OR conditions are cleaned up
 EXPLAIN (COSTS OFF)
@@ -6168,7 +6184,8 @@ FROM int4_tbl
                ->  Seq Scan on tenk1
                ->  Result
                      One-Time Filter: false
-(9 rows)
+                     Replaces: Scan on int8_tbl
+(10 rows)
 
 rollback;
 -- another join removal bug: we must clean up correctly when removing a PHV
@@ -6497,7 +6514,8 @@ where q1.x = q2.y;
 --------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on sj_1
+(3 rows)
 
 -- We can't use a cross-EC generated self join qual because of current logic of
 -- the generate_join_implied_equalities routine.
@@ -7321,11 +7339,12 @@ select 1 from emp1 full join
         on true
     where false) s on true
 where false;
-        QUERY PLAN        
---------------------------
+            QUERY PLAN            
+----------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Join on emp1, t1, t3
+(3 rows)
 
 select 1 from emp1 full join
     (select * from emp1 t1 join
@@ -7555,7 +7574,8 @@ where false;
  Result
    Output: 1
    One-Time Filter: false
-(3 rows)
+   Replaces: Scan on ss
+(4 rows)
 
 --
 -- Test LATERAL
@@ -8494,7 +8514,8 @@ select * from int8_tbl i8 left join lateral
    ->  Result
          Output: f1, i8.q2
          One-Time Filter: false
-(8 rows)
+         Replaces: Scan on int4_tbl
+(9 rows)
 
 explain (verbose, costs off)
 select * from int8_tbl i8 left join lateral
@@ -8508,7 +8529,8 @@ select * from int8_tbl i8 left join lateral
    ->  Result
          Output: f1, f1, i8.q2
          One-Time Filter: false
-(7 rows)
+         Replaces: Join on i1, i2
+(8 rows)
 
 -- check handling of nested appendrels inside LATERAL
 select * from
diff --git a/src/test/regress/expected/merge.out b/src/test/regress/expected/merge.out
index bcd29668297..1025bdf86d0 100644
--- a/src/test/regress/expected/merge.out
+++ b/src/test/regress/expected/merge.out
@@ -2427,7 +2427,8 @@ MERGE INTO pa_target t USING pa_source s ON t.tid = s.sid
                ->  Result
                      Output: t.tid, t.ctid
                      One-Time Filter: false
-(12 rows)
+                     Replaces: Scan on t
+(13 rows)
 
 MERGE INTO pa_target t USING pa_source s ON t.tid = s.sid
   WHEN NOT MATCHED THEN INSERT VALUES (s.sid);
diff --git a/src/test/regress/expected/partition_aggregate.out b/src/test/regress/expected/partition_aggregate.out
index 5f2c0cf5786..80b002fbdcf 100644
--- a/src/test/regress/expected/partition_aggregate.out
+++ b/src/test/regress/expected/partition_aggregate.out
@@ -146,13 +146,14 @@ SELECT c, a, count(*) FROM pagg_tab GROUP BY a, c;
 -- Test when input relation for grouping is dummy
 EXPLAIN (COSTS OFF)
 SELECT c, sum(a) FROM pagg_tab WHERE 1 = 2 GROUP BY c;
-           QUERY PLAN           
---------------------------------
+             QUERY PLAN             
+------------------------------------
  HashAggregate
    Group Key: c
    ->  Result
          One-Time Filter: false
-(4 rows)
+         Replaces: Scan on pagg_tab
+(5 rows)
 
 SELECT c, sum(a) FROM pagg_tab WHERE 1 = 2 GROUP BY c;
  c | sum 
@@ -161,12 +162,13 @@ SELECT c, sum(a) FROM pagg_tab WHERE 1 = 2 GROUP BY c;
 
 EXPLAIN (COSTS OFF)
 SELECT c, sum(a) FROM pagg_tab WHERE c = 'x' GROUP BY c;
-           QUERY PLAN           
---------------------------------
+             QUERY PLAN             
+------------------------------------
  GroupAggregate
    ->  Result
          One-Time Filter: false
-(3 rows)
+         Replaces: Scan on pagg_tab
+(4 rows)
 
 SELECT c, sum(a) FROM pagg_tab WHERE c = 'x' GROUP BY c;
  c | sum 
@@ -804,15 +806,16 @@ SELECT a.x, b.y, count(*) FROM (SELECT * FROM pagg_tab1 WHERE x < 20) a FULL JOI
 -- Empty join relation because of empty outer side, no partitionwise agg plan
 EXPLAIN (COSTS OFF)
 SELECT a.x, a.y, count(*) FROM (SELECT * FROM pagg_tab1 WHERE x = 1 AND x = 2) a LEFT JOIN pagg_tab2 b ON a.x = b.y GROUP BY a.x, a.y ORDER BY 1, 2;
-              QUERY PLAN              
---------------------------------------
+                  QUERY PLAN                  
+----------------------------------------------
  GroupAggregate
    Group Key: pagg_tab1.y
    ->  Sort
          Sort Key: pagg_tab1.y
          ->  Result
                One-Time Filter: false
-(6 rows)
+               Replaces: Join on b, pagg_tab1
+(7 rows)
 
 SELECT a.x, a.y, count(*) FROM (SELECT * FROM pagg_tab1 WHERE x = 1 AND x = 2) a LEFT JOIN pagg_tab2 b ON a.x = b.y GROUP BY a.x, a.y ORDER BY 1, 2;
  x | y | count 
diff --git a/src/test/regress/expected/partition_join.out b/src/test/regress/expected/partition_join.out
index d5368186caa..52c4ffffd81 100644
--- a/src/test/regress/expected/partition_join.out
+++ b/src/test/regress/expected/partition_join.out
@@ -1605,19 +1605,21 @@ SELECT avg(t1.a), avg(t2.b), avg(t3.a + t3.b), t1.c, t2.c, t3.c FROM plt1 t1, pl
 -- joins where one of the relations is proven empty
 EXPLAIN (COSTS OFF)
 SELECT t1.a, t1.c, t2.b, t2.c FROM prt1 t1, prt2 t2 WHERE t1.a = t2.b AND t1.a = 1 AND t1.a = 2;
-        QUERY PLAN        
---------------------------
+         QUERY PLAN         
+----------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Join on t1, t2
+(3 rows)
 
 EXPLAIN (COSTS OFF)
 SELECT t1.a, t1.c, t2.b, t2.c FROM (SELECT * FROM prt1 WHERE a = 1 AND a = 2) t1 LEFT JOIN prt2 t2 ON t1.a = t2.b;
-        QUERY PLAN        
---------------------------
+          QUERY PLAN          
+------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Join on t2, prt1
+(3 rows)
 
 EXPLAIN (COSTS OFF)
 SELECT t1.a, t1.c, t2.b, t2.c FROM (SELECT * FROM prt1 WHERE a = 1 AND a = 2) t1 RIGHT JOIN prt2 t2 ON t1.a = t2.b, prt1 t3 WHERE t2.b = t3.a;
@@ -1644,7 +1646,8 @@ SELECT t1.a, t1.c, t2.b, t2.c FROM (SELECT * FROM prt1 WHERE a = 1 AND a = 2) t1
    ->  Hash
          ->  Result
                One-Time Filter: false
-(21 rows)
+               Replaces: Scan on prt1
+(22 rows)
 
 EXPLAIN (COSTS OFF)
 SELECT t1.a, t1.c, t2.b, t2.c FROM (SELECT * FROM prt1 WHERE a = 1 AND a = 2) t1 FULL JOIN prt2 t2 ON t1.a = t2.b WHERE t2.a = 0 ORDER BY t1.a, t2.b;
@@ -1664,7 +1667,8 @@ SELECT t1.a, t1.c, t2.b, t2.c FROM (SELECT * FROM prt1 WHERE a = 1 AND a = 2) t1
          ->  Hash
                ->  Result
                      One-Time Filter: false
-(14 rows)
+                     Replaces: Scan on prt1
+(15 rows)
 
 --
 -- tests for hash partitioned tables.
@@ -2251,7 +2255,8 @@ SELECT t1.a, t1.c, t2.b, t2.c FROM (SELECT * FROM prt1_l WHERE a = 1 AND a = 2)
    ->  Hash
          ->  Result
                One-Time Filter: false
-(11 rows)
+               Replaces: Scan on prt1_l
+(12 rows)
 
 -- Test case to verify proper handling of subqueries in a partitioned delete.
 -- The weird-looking lateral join is just there to force creation of a
diff --git a/src/test/regress/expected/partition_prune.out b/src/test/regress/expected/partition_prune.out
index 0bf35260b46..6b51f39f88b 100644
--- a/src/test/regress/expected/partition_prune.out
+++ b/src/test/regress/expected/partition_prune.out
@@ -628,7 +628,8 @@ explain (costs off) select * from rlp3 where a = 20;   /* empty */
 --------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on rlp3
+(3 rows)
 
 -- redundant clauses are eliminated
 explain (costs off) select * from rlp where a > 1 and a = 10;	/* only default */
@@ -671,7 +672,8 @@ explain (costs off) select * from rlp where a = 1 and a = 3;	/* empty */
 --------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on rlp
+(3 rows)
 
 explain (costs off) select * from rlp where (a = 1 and a = 3) or (a > 1 and a = 15);
                             QUERY PLAN                             
@@ -1254,25 +1256,28 @@ select * from boolpart where a is not unknown;
 
 -- check that all partitions are pruned when faced with conflicting clauses
 explain (costs off) select * from boolpart where a is not unknown and a is unknown;
-        QUERY PLAN        
---------------------------
+          QUERY PLAN          
+------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on boolpart
+(3 rows)
 
 explain (costs off) select * from boolpart where a is false and a is unknown;
-        QUERY PLAN        
---------------------------
+          QUERY PLAN          
+------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on boolpart
+(3 rows)
 
 explain (costs off) select * from boolpart where a is true and a is unknown;
-        QUERY PLAN        
---------------------------
+          QUERY PLAN          
+------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on boolpart
+(3 rows)
 
 -- inverse boolean partitioning - a seemingly unlikely design, but we've got
 -- code for it, so we'd better test it.
@@ -1568,11 +1573,12 @@ explain (costs off) select * from coercepart where a = any ('{ab,null}');
 (2 rows)
 
 explain (costs off) select * from coercepart where a = any (null::text[]);
-        QUERY PLAN        
---------------------------
+           QUERY PLAN           
+--------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on coercepart
+(3 rows)
 
 explain (costs off) select * from coercepart where a = all ('{ab}');
                   QUERY PLAN                  
@@ -1582,25 +1588,28 @@ explain (costs off) select * from coercepart where a = all ('{ab}');
 (2 rows)
 
 explain (costs off) select * from coercepart where a = all ('{ab,bc}');
-        QUERY PLAN        
---------------------------
+           QUERY PLAN           
+--------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on coercepart
+(3 rows)
 
 explain (costs off) select * from coercepart where a = all ('{ab,null}');
-        QUERY PLAN        
---------------------------
+           QUERY PLAN           
+--------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on coercepart
+(3 rows)
 
 explain (costs off) select * from coercepart where a = all (null::text[]);
-        QUERY PLAN        
---------------------------
+           QUERY PLAN           
+--------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on coercepart
+(3 rows)
 
 drop table coercepart;
 CREATE TABLE part (a INT, b INT) PARTITION BY LIST (a);
@@ -1773,7 +1782,8 @@ explain (costs off) select * from lp where a <> 'a' and a is null;
 --------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on lp
+(3 rows)
 
 explain (costs off) select * from lp where (a <> 'a' and a <> 'd') or a is null;
                                   QUERY PLAN                                  
@@ -1866,22 +1876,24 @@ create table lparted_by_int2 (a smallint) partition by list (a);
 create table lparted_by_int2_1 partition of lparted_by_int2 for values in (1);
 create table lparted_by_int2_16384 partition of lparted_by_int2 for values in (16384);
 explain (costs off) select * from lparted_by_int2 where a = 100_000_000_000_000;
-        QUERY PLAN        
---------------------------
+             QUERY PLAN              
+-------------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on lparted_by_int2
+(3 rows)
 
 create table rparted_by_int2 (a smallint) partition by range (a);
 create table rparted_by_int2_1 partition of rparted_by_int2 for values from (1) to (10);
 create table rparted_by_int2_16384 partition of rparted_by_int2 for values from (10) to (16384);
 -- all partitions pruned
 explain (costs off) select * from rparted_by_int2 where a > 100_000_000_000_000;
-        QUERY PLAN        
---------------------------
+             QUERY PLAN              
+-------------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on rparted_by_int2
+(3 rows)
 
 create table rparted_by_int2_maxvalue partition of rparted_by_int2 for values from (16384) to (maxvalue);
 -- all partitions but rparted_by_int2_maxvalue pruned
@@ -2132,7 +2144,8 @@ explain (costs off) select * from hp where a = 1 and b = 'abcde' and
 --------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on hp
+(3 rows)
 
 --
 -- Test runtime partition pruning
@@ -3390,11 +3403,12 @@ select * from stable_qual_pruning where a < '2000-02-01'::timestamptz;
 explain (analyze, costs off, summary off, timing off, buffers off)
 select * from stable_qual_pruning
   where a = any(array['2010-02-01', '2020-01-01']::timestamp[]);
-            QUERY PLAN             
------------------------------------
+               QUERY PLAN                
+-----------------------------------------
  Result (actual rows=0.00 loops=1)
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on stable_qual_pruning
+(3 rows)
 
 explain (analyze, costs off, summary off, timing off, buffers off)
 select * from stable_qual_pruning
@@ -3642,6 +3656,7 @@ explain (analyze, costs off, summary off, timing off, buffers off) select * from
    Sort Key: ma_test.b
    InitPlan 2
      ->  Result (actual rows=1.00 loops=1)
+           Replaces: Aggregate
            InitPlan 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)
@@ -3656,7 +3671,7 @@ explain (analyze, costs off, summary off, timing off, buffers off) select * from
    ->  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)
          Index Searches: 1
-(18 rows)
+(19 rows)
 
 reset enable_seqscan;
 reset enable_sort;
@@ -3678,11 +3693,12 @@ explain (costs off) select * from pp_arrpart where a = '{1}';
 (2 rows)
 
 explain (costs off) select * from pp_arrpart where a = '{1, 2}';
-        QUERY PLAN        
---------------------------
+           QUERY PLAN           
+--------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on pp_arrpart
+(3 rows)
 
 explain (costs off) select * from pp_arrpart where a in ('{4, 5}', '{1}');
                               QUERY PLAN                              
@@ -3764,11 +3780,12 @@ explain (costs off) select * from pp_enumpart where a = 'blue';
 (2 rows)
 
 explain (costs off) select * from pp_enumpart where a = 'black';
-        QUERY PLAN        
---------------------------
+           QUERY PLAN            
+---------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on pp_enumpart
+(3 rows)
 
 drop table pp_enumpart;
 drop type pp_colors;
@@ -3785,11 +3802,12 @@ explain (costs off) select * from pp_recpart where a = '(1,1)'::pp_rectype;
 (2 rows)
 
 explain (costs off) select * from pp_recpart where a = '(1,2)'::pp_rectype;
-        QUERY PLAN        
---------------------------
+           QUERY PLAN           
+--------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on pp_recpart
+(3 rows)
 
 drop table pp_recpart;
 drop type pp_rectype;
@@ -3805,11 +3823,12 @@ explain (costs off) select * from pp_intrangepart where a = '[1,2]'::int4range;
 (2 rows)
 
 explain (costs off) select * from pp_intrangepart where a = '(1,2)'::int4range;
-        QUERY PLAN        
---------------------------
+             QUERY PLAN              
+-------------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on pp_intrangepart
+(3 rows)
 
 drop table pp_intrangepart;
 --
@@ -4125,19 +4144,21 @@ explain (costs off) update listp1 set a = 1 where a = 2;
 -- constraint exclusion enabled
 set constraint_exclusion to 'on';
 explain (costs off) select * from listp1 where a = 2;
-        QUERY PLAN        
---------------------------
+         QUERY PLAN         
+----------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on listp1
+(3 rows)
 
 explain (costs off) update listp1 set a = 1 where a = 2;
-           QUERY PLAN           
---------------------------------
+            QUERY PLAN            
+----------------------------------
  Update on listp1
    ->  Result
          One-Time Filter: false
-(3 rows)
+         Replaces: Scan on listp1
+(4 rows)
 
 reset constraint_exclusion;
 reset enable_partition_pruning;
@@ -4524,18 +4545,20 @@ create table hp_contradict_test (a int, b int) partition by hash (a part_test_in
 create table hp_contradict_test_p1 partition of hp_contradict_test for values with (modulus 2, remainder 0);
 create table hp_contradict_test_p2 partition of hp_contradict_test for values with (modulus 2, remainder 1);
 explain (costs off) select * from hp_contradict_test where a is null and a === 1 and b === 1;
-        QUERY PLAN        
---------------------------
+               QUERY PLAN               
+----------------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on hp_contradict_test
+(3 rows)
 
 explain (costs off) select * from hp_contradict_test where a === 1 and b === 1 and a is null;
-        QUERY PLAN        
---------------------------
+               QUERY PLAN               
+----------------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on hp_contradict_test
+(3 rows)
 
 drop table hp_contradict_test;
 drop operator class part_test_int4_ops2 using hash;
diff --git a/src/test/regress/expected/predicate.out b/src/test/regress/expected/predicate.out
index b79037748b7..ee8afacbf07 100644
--- a/src/test/regress/expected/predicate.out
+++ b/src/test/regress/expected/predicate.out
@@ -37,7 +37,8 @@ SELECT * FROM pred_tab t WHERE t.a IS NULL;
 --------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on t
+(3 rows)
 
 -- Ensure the IS_NULL qual is not reduced to constant-FALSE on nullable
 -- columns
@@ -78,7 +79,8 @@ SELECT * FROM pred_tab t WHERE t.a IS NULL OR t.c IS NULL;
 --------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on t
+(3 rows)
 
 -- Ensure the OR clause is not reduced to constant-FALSE when not all branches
 -- are provably false
@@ -140,7 +142,8 @@ SELECT * FROM pred_tab t1
    ->  Seq Scan on pred_tab t1
    ->  Result
          One-Time Filter: false
-(5 rows)
+         Replaces: Scan on t2
+(6 rows)
 
 -- Ensure the IS_NULL qual is not reduced to constant-FALSE when the column is
 -- nullable by an outer join
@@ -210,7 +213,8 @@ SELECT * FROM pred_tab t1
    ->  Seq Scan on pred_tab t1
    ->  Result
          One-Time Filter: false
-(5 rows)
+         Replaces: Scan on t2
+(6 rows)
 
 -- Ensure the OR clause is not reduced to constant-FALSE when a column is
 -- made nullable from an outer join
diff --git a/src/test/regress/expected/rowsecurity.out b/src/test/regress/expected/rowsecurity.out
index 1c4e37d2249..729ea4d7604 100644
--- a/src/test/regress/expected/rowsecurity.out
+++ b/src/test/regress/expected/rowsecurity.out
@@ -3607,7 +3607,8 @@ EXPLAIN (COSTS OFF) SELECT * FROM t1;
 --------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on t1
+(3 rows)
 
 SET SESSION AUTHORIZATION regress_rls_bob;
 SELECT * FROM t1;
@@ -3620,7 +3621,8 @@ EXPLAIN (COSTS OFF) SELECT * FROM t1;
 --------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on t1
+(3 rows)
 
 --
 -- COPY TO/FROM
@@ -4524,11 +4526,12 @@ SELECT * FROM rls_tbl WHERE a <<< 1000;
 (0 rows)
 
 EXPLAIN (COSTS OFF) SELECT * FROM rls_tbl WHERE a <<< 1000 or a <<< 900;
-        QUERY PLAN        
---------------------------
+         QUERY PLAN          
+-----------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on rls_tbl
+(3 rows)
 
 DROP OPERATOR <<< (int, int);
 DROP FUNCTION op_leak(int, int);
diff --git a/src/test/regress/expected/rowtypes.out b/src/test/regress/expected/rowtypes.out
index 9168979a620..328b1e142c0 100644
--- a/src/test/regress/expected/rowtypes.out
+++ b/src/test/regress/expected/rowtypes.out
@@ -1324,7 +1324,8 @@ where false;
  Result
    Output: (a).f1, (a).f2
    One-Time Filter: false
-(3 rows)
+   Replaces: Scan on ss
+(4 rows)
 
 explain (verbose, costs off)
 with cte(c) as materialized (select row(1, 2)),
@@ -1349,10 +1350,11 @@ where false;
  Result
    Output: (cte.c).f1
    One-Time Filter: false
+   Replaces: Scan on cte
    CTE cte
      ->  Result
            Output: '(1,2)'::record
-(6 rows)
+(7 rows)
 
 --
 -- Tests for component access / FieldSelect
diff --git a/src/test/regress/expected/select.out b/src/test/regress/expected/select.out
index bab0cc93ff5..d1736dac224 100644
--- a/src/test/regress/expected/select.out
+++ b/src/test/regress/expected/select.out
@@ -962,10 +962,11 @@ create table list_parted_tbl (a int,b int) partition by list (a);
 create table list_parted_tbl1 partition of list_parted_tbl
   for values in (1) partition by list(b);
 explain (costs off) select * from list_parted_tbl;
-        QUERY PLAN        
---------------------------
+             QUERY PLAN              
+-------------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on list_parted_tbl
+(3 rows)
 
 drop table list_parted_tbl;
diff --git a/src/test/regress/expected/subselect.out b/src/test/regress/expected/subselect.out
index 40d8056fcea..bd25ceabdf5 100644
--- a/src/test/regress/expected/subselect.out
+++ b/src/test/regress/expected/subselect.out
@@ -2645,12 +2645,13 @@ ON B.hundred in (SELECT min(c.hundred) FROM tenk2 C WHERE c.odd = b.odd);
                      ->  Subquery Scan on "ANY_subquery"
                            Filter: (b.hundred = "ANY_subquery".min)
                            ->  Result
+                                 Replaces: Aggregate
                                  InitPlan 1
                                    ->  Limit
                                          ->  Index Scan using tenk2_hundred on tenk2 c
                                                Index Cond: (hundred IS NOT NULL)
                                                Filter: (odd = b.odd)
-(16 rows)
+(17 rows)
 
 --
 -- Test VALUES to ARRAY (VtA) transformation
@@ -2815,7 +2816,8 @@ EXPLAIN (COSTS OFF) EXECUTE test(NULL, 3.14, NULL);
 --------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on onek
+(3 rows)
 
 EXPLAIN (COSTS OFF) EXECUTE test(NULL, 3.14, '-1.5');
                                                                      QUERY PLAN                                                                      
diff --git a/src/test/regress/expected/tsrf.out b/src/test/regress/expected/tsrf.out
index d47b5f6ec57..e73e68af971 100644
--- a/src/test/regress/expected/tsrf.out
+++ b/src/test/regress/expected/tsrf.out
@@ -92,7 +92,8 @@ SELECT unnest(ARRAY[1, 2]) FROM few WHERE false;
    Output: unnest('{1,2}'::integer[])
    ->  Result
          One-Time Filter: false
-(4 rows)
+         Replaces: Scan on few
+(5 rows)
 
 SELECT unnest(ARRAY[1, 2]) FROM few WHERE false;
  unnest 
@@ -108,7 +109,8 @@ SELECT * FROM few f1,
  Result
    Output: f1.id, f1.dataa, f1.datab, ss.unnest
    One-Time Filter: false
-(3 rows)
+   Replaces: Join on f1, ss
+(4 rows)
 
 SELECT * FROM few f1,
   (SELECT unnest(ARRAY[1,2]) FROM few f2 WHERE false OFFSET 0) ss;
-- 
2.39.3 (Apple Git-145)

v1-0002-Consider-a-Result-node-s-relids-in-ExplainPreScan.patchapplication/octet-stream; name=v1-0002-Consider-a-Result-node-s-relids-in-ExplainPreScan.patchDownload
From ea5fb810cab564dfef22e6065d05b414185f3cf1 Mon Sep 17 00:00:00 2001
From: Robert Haas <rhaas@postgresql.org>
Date: Mon, 21 Apr 2025 13:35:28 -0400
Subject: [PATCH v1 2/6] Consider a Result node's relids in ExplainPreScanNode.

Now that a Result node has a relids set, add the relids that it
carries the set accumulated by ExplainPreScanNode so that we
generate unique relation aliases for all of the referenced relations
when it calls select_rtable_names_for_explain. The effect of this
changes is that a few things get schema-qualified in the regression
test outputs that previously were not. In similar cases not involving
a Result node, we were already schema-qualifying, so this appears to
be an improvement.

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.
---
 src/backend/commands/explain.c                |  4 +++
 .../regress/expected/generated_virtual.out    |  8 +++---
 src/test/regress/expected/join.out            | 26 +++++++++----------
 src/test/regress/expected/partition_join.out  | 12 ++++-----
 4 files changed, 27 insertions(+), 23 deletions(-)

diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index bf0185e1ca1..a4bec7ac323 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -1242,6 +1242,10 @@ ExplainPreScanNode(PlanState *planstate, Bitmapset **rels_used)
 			*rels_used = bms_add_members(*rels_used,
 										 ((MergeAppend *) plan)->apprelids);
 			break;
+		case T_Result:
+			*rels_used = bms_add_members(*rels_used,
+										 ((Result *) plan)->relids);
+			break;
 		default:
 			break;
 	}
diff --git a/src/test/regress/expected/generated_virtual.out b/src/test/regress/expected/generated_virtual.out
index 1793c3f3578..53509c86433 100644
--- a/src/test/regress/expected/generated_virtual.out
+++ b/src/test/regress/expected/generated_virtual.out
@@ -1551,15 +1551,15 @@ where coalesce(t2.b, 1) = 2 or t1.a is null;
 -- Ensure that the generation expressions are wrapped into PHVs if needed
 explain (verbose, costs off)
 select t2.* from gtest32 t1 left join gtest32 t2 on false;
-                      QUERY PLAN                      
-------------------------------------------------------
+                       QUERY PLAN                        
+---------------------------------------------------------
  Nested Loop Left Join
-   Output: a, (a * 2), (20), (COALESCE(a, 100))
+   Output: t2.a, (t2.a * 2), (20), (COALESCE(t2.a, 100))
    Join Filter: false
    ->  Seq Scan on generated_virtual_tests.gtest32 t1
          Output: t1.a, t1.b, t1.c, t1.d
    ->  Result
-         Output: a, 20, COALESCE(a, 100)
+         Output: t2.a, 20, COALESCE(t2.a, 100)
          One-Time Filter: false
          Replaces: Scan on t2
 (9 rows)
diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out
index f298aa7c1dc..23913249a8f 100644
--- a/src/test/regress/expected/join.out
+++ b/src/test/regress/expected/join.out
@@ -5808,7 +5808,7 @@ on t1.q1 = t2.q1;
                  QUERY PLAN                 
 --------------------------------------------
  Hash Left Join
-   Hash Cond: (t1.q1 = q1)
+   Hash Cond: (t1.q1 = t2.q1)
    ->  Seq Scan on int8_tbl t1
    ->  Hash
          ->  Result
@@ -5825,7 +5825,7 @@ on t1.q1 = t2.q1;
                  QUERY PLAN                 
 --------------------------------------------
  Hash Left Join
-   Hash Cond: (t1.q1 = q1)
+   Hash Cond: (t1.q1 = t2.q1)
    ->  Seq Scan on int8_tbl t1
    ->  Hash
          ->  Result
@@ -6155,13 +6155,13 @@ SELECT q2 FROM
                       QUERY PLAN                      
 ------------------------------------------------------
  Nested Loop Left Join
-   Output: q2
+   Output: int8_tbl.q2
    Join Filter: NULL::boolean
    Filter: (('constant'::text) >= ('constant'::text))
    ->  Seq Scan on public.int4_tbl
          Output: int4_tbl.f1
    ->  Result
-         Output: q2, 'constant'::text
+         Output: int8_tbl.q2, 'constant'::text
          One-Time Filter: false
          Replaces: Scan on int8_tbl
 (10 rows)
@@ -6514,7 +6514,7 @@ where q1.x = q2.y;
 --------------------------
  Result
    One-Time Filter: false
-   Replaces: Scan on sj_1
+   Replaces: Scan on sj
 (3 rows)
 
 -- We can't use a cross-EC generated self join qual because of current logic of
@@ -8504,15 +8504,15 @@ select * from int4_tbl t1,
 explain (verbose, costs off)
 select * from int8_tbl i8 left join lateral
   (select *, i8.q2 from int4_tbl where false) ss on true;
-              QUERY PLAN              
---------------------------------------
+                  QUERY PLAN                  
+----------------------------------------------
  Nested Loop Left Join
-   Output: i8.q1, i8.q2, f1, (i8.q2)
+   Output: i8.q1, i8.q2, int4_tbl.f1, (i8.q2)
    Join Filter: false
    ->  Seq Scan on public.int8_tbl i8
          Output: i8.q1, i8.q2
    ->  Result
-         Output: f1, i8.q2
+         Output: int4_tbl.f1, i8.q2
          One-Time Filter: false
          Replaces: Scan on int4_tbl
 (9 rows)
@@ -8520,14 +8520,14 @@ select * from int8_tbl i8 left join lateral
 explain (verbose, costs off)
 select * from int8_tbl i8 left join lateral
   (select *, i8.q2 from int4_tbl i1, int4_tbl i2 where false) ss on true;
-               QUERY PLAN                
------------------------------------------
+                  QUERY PLAN                   
+-----------------------------------------------
  Nested Loop Left Join
-   Output: i8.q1, i8.q2, f1, f1, (i8.q2)
+   Output: i8.q1, i8.q2, i1.f1, i2.f1, (i8.q2)
    ->  Seq Scan on public.int8_tbl i8
          Output: i8.q1, i8.q2
    ->  Result
-         Output: f1, f1, i8.q2
+         Output: i1.f1, i2.f1, i8.q2
          One-Time Filter: false
          Replaces: Join on i1, i2
 (8 rows)
diff --git a/src/test/regress/expected/partition_join.out b/src/test/regress/expected/partition_join.out
index 52c4ffffd81..966b2eb6eb9 100644
--- a/src/test/regress/expected/partition_join.out
+++ b/src/test/regress/expected/partition_join.out
@@ -1626,7 +1626,7 @@ SELECT t1.a, t1.c, t2.b, t2.c FROM (SELECT * FROM prt1 WHERE a = 1 AND a = 2) t1
                     QUERY PLAN                    
 --------------------------------------------------
  Hash Left Join
-   Hash Cond: (t2.b = a)
+   Hash Cond: (t2.b = prt1.a)
    ->  Append
          ->  Hash Join
                Hash Cond: (t3_1.a = t2_1.b)
@@ -1654,9 +1654,9 @@ SELECT t1.a, t1.c, t2.b, t2.c FROM (SELECT * FROM prt1 WHERE a = 1 AND a = 2) t1
                  QUERY PLAN                 
 --------------------------------------------
  Sort
-   Sort Key: a, t2.b
+   Sort Key: prt1.a, t2.b
    ->  Hash Left Join
-         Hash Cond: (t2.b = a)
+         Hash Cond: (t2.b = prt1.a)
          ->  Append
                ->  Seq Scan on prt2_p1 t2_1
                      Filter: (a = 0)
@@ -2242,10 +2242,10 @@ SELECT COUNT(*) FROM prt1_l t1 LEFT JOIN LATERAL
 -- join with one side empty
 EXPLAIN (COSTS OFF)
 SELECT t1.a, t1.c, t2.b, t2.c FROM (SELECT * FROM prt1_l WHERE a = 1 AND a = 2) t1 RIGHT JOIN prt2_l t2 ON t1.a = t2.b AND t1.b = t2.a AND t1.c = t2.c;
-                               QUERY PLAN                                
--------------------------------------------------------------------------
+                                          QUERY PLAN                                          
+----------------------------------------------------------------------------------------------
  Hash Left Join
-   Hash Cond: ((t2.b = a) AND (t2.a = b) AND ((t2.c)::text = (c)::text))
+   Hash Cond: ((t2.b = prt1_l.a) AND (t2.a = prt1_l.b) AND ((t2.c)::text = (prt1_l.c)::text))
    ->  Append
          ->  Seq Scan on prt2_l_p1 t2_1
          ->  Seq Scan on prt2_l_p2_p1 t2_2
-- 
2.39.3 (Apple Git-145)

v1-0006-Store-information-about-elided-nodes-in-the-final.patchapplication/octet-stream; name=v1-0006-Store-information-about-elided-nodes-in-the-final.patchDownload
From d552036c71cd6e1bee9c2ec870b40d3fe64eb8a8 Mon Sep 17 00:00:00 2001
From: Robert Haas <rhaas@postgresql.org>
Date: Tue, 22 Apr 2025 14:10:19 -0400
Subject: [PATCH v1 6/6] Store information about elided nodes in the final
 plan.

When setrefs.c removes a SubqueryScan, single-child Append, or
single-child MergeAppend from the final Plan tree, the RTI which
would have been scanned by the removed node no longer appears in
the final plan (the actual range table entry is still present,
but it's no longer referenced).

That's fine for the executor, but it can create difficulties for
code that wants to deduce from the final plan what choices were
made during the planing process. For example, a traversal of a
join tree in the final plan might never encounter the RTI of one
of the relationss in the join problem, and might instead encounter
a scan of a child RTI or even one from a different subquery level.

This patch adjusts things so that each time we elide a node during
setrefs processing, we record the plan_node_id of its single surviving
child, the type of the removed node, and the RTIs that the removed
node would have scanned. This information is recorded in a separate
list that can be ignored by the executor and examined only by code
that cares about these details.

This commit also updates pg_overexplain to display these details.
---
 .../expected/pg_overexplain.out               |  4 +-
 contrib/pg_overexplain/pg_overexplain.c       | 39 ++++++++++++++
 src/backend/optimizer/plan/planner.c          |  1 +
 src/backend/optimizer/plan/setrefs.c          | 52 ++++++++++++++++++-
 src/include/nodes/pathnodes.h                 |  3 ++
 src/include/nodes/plannodes.h                 | 17 ++++++
 src/tools/pgindent/typedefs.list              |  1 +
 7 files changed, 114 insertions(+), 3 deletions(-)

diff --git a/contrib/pg_overexplain/expected/pg_overexplain.out b/contrib/pg_overexplain/expected/pg_overexplain.out
index dd8adddb4a3..0674641bec1 100644
--- a/contrib/pg_overexplain/expected/pg_overexplain.out
+++ b/contrib/pg_overexplain/expected/pg_overexplain.out
@@ -452,6 +452,8 @@ SELECT * FROM vegetables WHERE genus = 'daucus';
  Seq Scan on daucus vegetables
    Filter: (genus = 'daucus'::text)
    Scan RTI: 2
+   Elided Node Type: Append
+   Elided Node RTIs: 1
  RTI 1 (relation, inherited, in-from-clause):
    Eref: vegetables (id, name, genus)
    Relation: vegetables
@@ -465,7 +467,7 @@ SELECT * FROM vegetables WHERE genus = 'daucus';
    Relation Kind: relation
    Relation Lock Mode: AccessShareLock
  Unprunable RTIs: 1 2
-(16 rows)
+(18 rows)
 
 -- Also test a case that involves a write.
 EXPLAIN (RANGE_TABLE, COSTS OFF)
diff --git a/contrib/pg_overexplain/pg_overexplain.c b/contrib/pg_overexplain/pg_overexplain.c
index 5dc707d69e3..fa907fa472e 100644
--- a/contrib/pg_overexplain/pg_overexplain.c
+++ b/contrib/pg_overexplain/pg_overexplain.c
@@ -191,6 +191,8 @@ overexplain_per_node_hook(PlanState *planstate, List *ancestors,
 	 */
 	if (options->range_table)
 	{
+		bool		opened_elided_nodes = false;
+
 		switch (nodeTag(plan))
 		{
 			case T_SeqScan:
@@ -251,6 +253,43 @@ overexplain_per_node_hook(PlanState *planstate, List *ancestors,
 			default:
 				break;
 		}
+
+		foreach_node(ElidedNode, n, es->pstmt->elidedNodes)
+		{
+			char	   *elidednodetag;
+
+			if (n->plan_node_id != plan->plan_node_id)
+				continue;
+
+			if (!opened_elided_nodes)
+			{
+				ExplainOpenGroup("Elided Nodes", "Elided Nodes", false, es);
+				opened_elided_nodes = true;
+			}
+
+			switch (n->elided_type)
+			{
+				case T_Append:
+					elidednodetag = "Append";
+					break;
+				case T_MergeAppend:
+					elidednodetag = "MergeAppend";
+					break;
+				case T_SubqueryScan:
+					elidednodetag = "SubqueryScan";
+					break;
+				default:
+					elidednodetag = psprintf("%d", n->elided_type);
+					break;
+			}
+
+			ExplainOpenGroup("Elided Node", NULL, true, es);
+			ExplainPropertyText("Elided Node Type", elidednodetag, es);
+			overexplain_bitmapset("Elided Node RTIs", n->relids, es);
+			ExplainCloseGroup("Elided Node", NULL, true, es);
+		}
+		if (opened_elided_nodes)
+			ExplainCloseGroup("Elided Nodes", "Elided Nodes", false, es);
 	}
 }
 
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index 5dcd09e712a..9ee99fdb6bd 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -583,6 +583,7 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions,
 	result->paramExecTypes = glob->paramExecTypes;
 	/* utilityStmt should be null, but we might as well copy it */
 	result->utilityStmt = parse->utilityStmt;
+	result->elidedNodes = glob->elidedNodes;
 	result->stmt_location = parse->stmt_location;
 	result->stmt_len = parse->stmt_len;
 
diff --git a/src/backend/optimizer/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.c
index 6f0d97f3936..5ef353c1563 100644
--- a/src/backend/optimizer/plan/setrefs.c
+++ b/src/backend/optimizer/plan/setrefs.c
@@ -211,6 +211,9 @@ static List *set_windowagg_runcondition_references(PlannerInfo *root,
 												   List *runcondition,
 												   Plan *plan);
 
+static void record_elided_node(PlannerGlobal *glob, int plan_node_id,
+							   NodeTag elided_type, Bitmapset *relids);
+
 
 /*****************************************************************************
  *
@@ -1439,10 +1442,17 @@ set_subqueryscan_references(PlannerInfo *root,
 
 	if (trivial_subqueryscan(plan))
 	{
+		Index		scanrelid;
+
 		/*
 		 * We can omit the SubqueryScan node and just pull up the subplan.
 		 */
 		result = clean_up_removed_plan_level((Plan *) plan, plan->subplan);
+
+		/* Remember that we removed a SubqueryScan */
+		scanrelid = plan->scan.scanrelid + rtoffset;
+		record_elided_node(root->glob, plan->subplan->plan_node_id,
+						   T_SubqueryScan, bms_make_singleton(scanrelid));
 	}
 	else
 	{
@@ -1870,7 +1880,17 @@ set_append_references(PlannerInfo *root,
 		Plan	   *p = (Plan *) linitial(aplan->appendplans);
 
 		if (p->parallel_aware == aplan->plan.parallel_aware)
-			return clean_up_removed_plan_level((Plan *) aplan, p);
+		{
+			Plan	   *result;
+
+			result = clean_up_removed_plan_level((Plan *) aplan, p);
+
+			/* Remember that we removed an Append */
+			record_elided_node(root->glob, p->plan_node_id, T_Append,
+							   offset_relid_set(aplan->apprelids, rtoffset));
+
+			return result;
+		}
 	}
 
 	/*
@@ -1938,7 +1958,17 @@ set_mergeappend_references(PlannerInfo *root,
 		Plan	   *p = (Plan *) linitial(mplan->mergeplans);
 
 		if (p->parallel_aware == mplan->plan.parallel_aware)
-			return clean_up_removed_plan_level((Plan *) mplan, p);
+		{
+			Plan	   *result;
+
+			result = clean_up_removed_plan_level((Plan *) mplan, p);
+
+			/* Remember that we removed a MergeAppend */
+			record_elided_node(root->glob, p->plan_node_id, T_MergeAppend,
+							   offset_relid_set(mplan->apprelids, rtoffset));
+
+			return result;
+		}
 	}
 
 	/*
@@ -3753,3 +3783,21 @@ extract_query_dependencies_walker(Node *node, PlannerInfo *context)
 	return expression_tree_walker(node, extract_query_dependencies_walker,
 								  context);
 }
+
+/*
+ * Record some details about a node removed from the plan during setrefs
+ * procesing, for the benefit of code trying to reconstruct planner decisions
+ * from examination of the final plan tree.
+ */
+static void
+record_elided_node(PlannerGlobal *glob, int plan_node_id,
+				   NodeTag elided_type, Bitmapset *relids)
+{
+	ElidedNode *n = makeNode(ElidedNode);
+
+	n->plan_node_id = plan_node_id;
+	n->elided_type = elided_type;
+	n->relids = relids;
+
+	glob->elidedNodes = lappend(glob->elidedNodes, n);
+}
diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h
index 61ba04d014f..957ee4208a9 100644
--- a/src/include/nodes/pathnodes.h
+++ b/src/include/nodes/pathnodes.h
@@ -162,6 +162,9 @@ typedef struct PlannerGlobal
 	/* type OIDs for PARAM_EXEC Params */
 	List	   *paramExecTypes;
 
+	/* info about nodes elided from the plan during setrefs processing */
+	List	   *elidedNodes;
+
 	/* highest PlaceHolderVar ID assigned */
 	Index		lastPHId;
 
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index 9df11cd394a..83fb4bd8707 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -141,6 +141,9 @@ typedef struct PlannedStmt
 	/* non-null if this is utility stmt */
 	Node	   *utilityStmt;
 
+	/* info about nodes elided from the plan during setrefs processing */
+	List	   *elidedNodes;
+
 	/* statement location in source string (copied from Query) */
 	/* start location, or -1 if unknown */
 	ParseLoc	stmt_location;
@@ -1797,4 +1800,18 @@ typedef struct SubPlanRTInfo
 	bool		dummy;
 } SubPlanRTInfo;
 
+/*
+ * ElidedNode
+ *
+ * Information about nodes elided from the final plan tree: trivial subquery
+ * scans, and single-child Append and MergeAppend nodes.
+ */
+typedef struct ElidedNode
+{
+	NodeTag		type;
+	int			plan_node_id;
+	NodeTag		elided_type;
+	Bitmapset  *relids;
+} ElidedNode;
+
 #endif							/* PLANNODES_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 1c6a7252ee4..f4ae78224ab 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -4310,3 +4310,4 @@ ExplainExtensionOption
 ExplainOptionHandler
 overexplain_options
 SubPlanRTInfo
+ElidedNode
-- 
2.39.3 (Apple Git-145)

#2Tomas Vondra
tomas@vondra.me
In reply to: Robert Haas (#1)
Re: plan shape work

On 5/19/25 20:01, Robert Haas wrote:

Hi,

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.
This is a long way from something that actually looks like a usable
feature, but these are bits of infrastructure that I think will be
necessary to get to a usable feature. As a recap, my overall goal here
is to make it so that you can examine a finished plan, figure out what
decisions the planner made, and then somehow get the planner to make
those same decisions over again in a future planning cycle. Since
doing this for all types of planner decisions seems too difficult for
an initial goal, I'm focusing on scans and joins for now. A further
goal is that I want it to be possible for extensions to use this
infrastructure to implement a variety of different policies that they
might feel to be beneficial, so I'm looking to minimize the amount of
stuff that has to be done in core PostgreSQL or can only be used by
core PostgreSQL.

...

Thanks for the overview. I don't have any immediate feedback, but it
sounds like it might be related to the "making planner decisions clear"
session from the unconference ...

The basic premise of that session was about how to give users better
info about the planner decisions - why paths were selected/rejected,
etc. A simple example would be "why was the index not used", and the
possible answers include "dominated by cost by another path" or "does
not match the index keys" etc.

I wonder if this work might be useful for something like that.

regards

--
Tomas Vondra

#3Maciek Sakrejda
maciek@pganalyze.com
In reply to: Tomas Vondra (#2)
Re: plan shape work

+1, this seems like it could be very useful. A somewhat related issue
is being able to tie plan nodes back to the query text: it can be hard
to understand the planner's decisions if it's not even clear what part
of the query it's making decisions about. I'm sure this is not an easy
problem in general, but I wonder if you think that could be improved
in the course of this work, or if you have other thoughts about it.

Thanks,
Maciek

#4Robert Haas
robertmhaas@gmail.com
In reply to: Tomas Vondra (#2)
Re: plan shape work

On Tue, May 20, 2025 at 2:45 PM Tomas Vondra <tomas@vondra.me> wrote:

Thanks for the overview. I don't have any immediate feedback, but it
sounds like it might be related to the "making planner decisions clear"
session from the unconference ...

The basic premise of that session was about how to give users better
info about the planner decisions - why paths were selected/rejected,
etc. A simple example would be "why was the index not used", and the
possible answers include "dominated by cost by another path" or "does
not match the index keys" etc.

I wonder if this work might be useful for something like that.

I've been wondering that, too. There's definitely some indirect ways
in which that might be the case. For example, I think this work would
lend itself to saying "hey, try planning this query, but for that
table over there, use an index scan on this table." Then, it either
still doesn't -- meaning the index isn't usable for some reason -- or
it does and you can see the resulting plan with presumably higher cost
and maybe infer why it didn't happen. That's better than today, where
we have only very crude tools that let us do things like disable an
entire scan type for the entire query, and I think it would make it a
lot easier and less frustrating for a knowledgeable user to figure out
why things are happening.

But even though I think that would be better than today, I'm not sure
it rises to the level of actually being good, because I think it still
requires a fairly knowledgeable operator to figure things out, and you
probably have to experiment a bunch to understand the situation
instead of, say, being able to just look at the EXPLAIN plan and see
the answer. I think being able to look at the EXPLAIN plan and see the
answer, without needing a bunch of poking around, would be the ideal
scenario here.

But in some sense this is the same problem as understanding how an AI
neural network is reasoning. The answer to "why did the planner pick
plan X" is always "X was the cheapest possible plan". Ideas like "we
chose a merge join because both tables are large enough that neither
would fit into a hash table conveniently" are human explanations of
why the math had the effect that it did; they are not how the planner
actually reasons. So it's not just a matter of exposing the actual
reasoning process to the user, because the computer is not reasoning
in a way that a human would. It would have to be a matter of exposing
some kind of other information that would allow the human being to
comprehend easily what led the machine's algorithm to a certain
conclusion; and it is not obvious how to get there.

I have a sense - possibly an incorrect one - that the core of the
problem here is that the planner considers lots of very similar
alternatives. A hypothetical feature that showed the second-cheapest
plan would be all but useless, because the second-cheapest plan would
just be a very minor variation of the cheapest plan in almost all
cases. One idea that crossed my mind was to display information in
EXPLAIN about what would have happened if we'd done something really
different. For instance, suppose that at a certain level of the plan
tree we actually chose a merge join, but we also show the estimated
cost of the cheapest hash join (if any) and the cheapest nested loop
(if any) that we considered at that level. The user might be able to
draw useful conclusions based on whether those numbers were altogether
absent (i.e. that join type was not viable at all) or whether the cost
was a little higher or a lot higher than that of the path actually
chosen. For scans, you could list which indexes were believed to be
usable and perhaps what the cost would have been for the cheapest one
not actually selected; and what the cost of a sequential scan would
have been if you hadn't picked one.

I'm not sure how useful this would be, so the whole idea might
actually suck, or maybe it's sort of the right idea but needs a bunch
of refinement to really be useful. I don't have a better idea right
now, though.

If there are any notes that were taken during that unconference
session, please point me in the right direction; I was in another
session at that time but would read any available notes with interest.

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

#5Robert Haas
robertmhaas@gmail.com
In reply to: Maciek Sakrejda (#3)
Re: plan shape work

On Tue, May 20, 2025 at 3:09 PM Maciek Sakrejda <maciek@pganalyze.com> wrote:

+1, this seems like it could be very useful. A somewhat related issue
is being able to tie plan nodes back to the query text: it can be hard
to understand the planner's decisions if it's not even clear what part
of the query it's making decisions about. I'm sure this is not an easy
problem in general, but I wonder if you think that could be improved
in the course of this work, or if you have other thoughts about it.

Thanks. I don't really have any ideas about the problem you mention,
perhaps partly because I haven't experienced it too much. I mean, I
have sometimes been confused about which parts of the query go with
which parts of the EXPLAIN, but I think in my experience so far that
is mostly because either (1) both the query and the EXPLAIN output are
super long and maybe also super-wide and therefore it's hard to
correlate things by eye or (2) somebody wrote a query where they use
the same table and/or table alias over and over again in different
parts of the query and so it's hard to tell which reference goes with
which. Neither of those problems seems all that exciting to me from a
dev perspective: if you're calling everything a or x or orders or
something, maybe don't do that, and if your query is 1500 characters
long, I guess you need to budget some time to align that with the
query plan. I don't really know how much we can do here. But maybe
there are cases that I haven't seen where something better is
possible, or perhaps you have some good idea that I haven't
considered.

(If I'm honest, I do have an idea that I think might very
significantly improve the readability of EXPLAIN output. I think it
would make it much less wide in normal cases without making it much
longer. This has been percolating in my brain for a few years now and
I have the vague intention of proposing it at some point, but not
until I'm good and ready to be flamed to a well-done crisp, because
I'm quite sure there will be more than one opinion on the merits.)

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

#6Maciek Sakrejda
m.sakrejda@gmail.com
In reply to: Robert Haas (#5)
Re: plan shape work

On Wed, May 21, 2025 at 7:29 AM Robert Haas <robertmhaas@gmail.com> wrote:

On Tue, May 20, 2025 at 3:09 PM Maciek Sakrejda <maciek@pganalyze.com> wrote:

+1, this seems like it could be very useful. A somewhat related issue
is being able to tie plan nodes back to the query text: it can be hard
to understand the planner's decisions if it's not even clear what part
of the query it's making decisions about. I'm sure this is not an easy
problem in general, but I wonder if you think that could be improved
in the course of this work, or if you have other thoughts about it.

Thanks. I don't really have any ideas about the problem you mention,
perhaps partly because I haven't experienced it too much.

That may be due to your extensive experience with Postgres and EXPLAIN plans.

I mean, I
have sometimes been confused about which parts of the query go with
which parts of the EXPLAIN, but I think in my experience so far that
is mostly because either (1) both the query and the EXPLAIN output are
super long and maybe also super-wide and therefore it's hard to
correlate things by eye or (2) somebody wrote a query where they use
the same table and/or table alias over and over again in different
parts of the query and so it's hard to tell which reference goes with
which. Neither of those problems seems all that exciting to me from a
dev perspective: if you're calling everything a or x or orders or
something, maybe don't do that, and if your query is 1500 characters
long, I guess you need to budget some time to align that with the
query plan.

Fair enough, although the people trying to make sense of EXPLAIN plans
are sometimes not the same ones who are writing the queries. And
sometimes the queries are not written by people at all but by ORMs
(or—heaven help us—vibe coded). "Don't do X" is a reasonable response
to "It hurts when I do X," but it doesn't really solve the user's
problem. That said, it's hard to argue with "We don't have any good
ideas on how to improve this right now, and it's not a total dumpster
fire, so we'll focus on other work."

I don't really know how much we can do here. But maybe
there are cases that I haven't seen where something better is
possible, or perhaps you have some good idea that I haven't
considered.

No great ideas here. I thought initially that a good solution would be
to have structured EXPLAIN output include something like "Query Text
Start Index" and "Query Text End Index" fields for each node, but I
realized that this doesn't really work for multiple joins (and
probably other cases). Maybe "Query Text Indices", as a list of pairs?
But from the little I know about the planner, that seems like any sort
of tracking back to the source would be hard to implement. And it only
really solves the problem for external EXPLAIN viewers, and only ones
that put in the work to support this. I'm not sure if the problem can
be meaningfully addressed for text format, but maybe that's another
reason not to spend time on it in core.

(If I'm honest, I do have an idea that I think might very
significantly improve the readability of EXPLAIN output. I think it
would make it much less wide in normal cases without making it much
longer. This has been percolating in my brain for a few years now and
I have the vague intention of proposing it at some point, but not
until I'm good and ready to be flamed to a well-done crisp, because
I'm quite sure there will be more than one opinion on the merits.)

I'm intrigued, and happy to stand by with an extinguisher. The road to
great ideas is paved with bad ideas.

Thanks,
Maciek

#7Robert Haas
robertmhaas@gmail.com
In reply to: Maciek Sakrejda (#6)
Re: plan shape work

On Wed, May 21, 2025 at 12:03 PM Maciek Sakrejda <m.sakrejda@gmail.com> wrote:

That may be due to your extensive experience with Postgres and EXPLAIN plans.

Yes, that is very possible. All things being equal, it helps to have
done something a lot of times.

Fair enough, although the people trying to make sense of EXPLAIN plans
are sometimes not the same ones who are writing the queries. And
sometimes the queries are not written by people at all but by ORMs
(or—heaven help us—vibe coded). "Don't do X" is a reasonable response
to "It hurts when I do X," but it doesn't really solve the user's
problem. That said, it's hard to argue with "We don't have any good
ideas on how to improve this right now, and it's not a total dumpster
fire, so we'll focus on other work."

+1 to all of that.

No great ideas here. I thought initially that a good solution would be
to have structured EXPLAIN output include something like "Query Text
Start Index" and "Query Text End Index" fields for each node, but I
realized that this doesn't really work for multiple joins (and
probably other cases). Maybe "Query Text Indices", as a list of pairs?
But from the little I know about the planner, that seems like any sort
of tracking back to the source would be hard to implement. And it only
really solves the problem for external EXPLAIN viewers, and only ones
that put in the work to support this. I'm not sure if the problem can
be meaningfully addressed for text format, but maybe that's another
reason not to spend time on it in core.

I'm not gonna say you couldn't make something like that work, but it
sounds like a lot of effort for a hypothetical piece of external
visualization software that might or might not produce satisfying
results. My advice to anyone wanting to pursue this idea would be:
make a totally fake POC first. Get a sample query with at least a
moderately complex plan, get the EXPLAIN output, manually generate
whatever data you think PostgreSQL ought to be able to spit out, and
do a mock-up of an external viewer. When you're happy with the
results, show it to some other people and see if they also like it. We
can have the discussion about whether to include anything in core and
what it should be after that. I definitely would not rule out the
possibility that something like this could turn out to be really cool
-- maybe hovering over stuff and having the corresponding part of the
plan get highlighted will turn out to be awesome. But I think it might
also turn out that there are things where it's not quite clear what
you can or should usefully highlight, like target-list items, or for
example a case where the query says that a.x = b.x and b.x = c.x but
in the actual plan we use evaluate a.x = c.x, an expression not
appearing anywhere in the query text. The legwork of sorting some of
that kind of stuff out should really happen before making a feature
proposal.

I'm intrigued, and happy to stand by with an extinguisher. The road to
great ideas is paved with bad ideas.

Thanks. That proposal is a task for another day, but I appreciate the sentiment.

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

#8Andy Fan
zhihuifan1213@163.com
In reply to: Robert Haas (#1)
Re: plan shape work

Robert Haas <robertmhaas@gmail.com> writes:

Hi,

... As a recap, my overall goal here
is to make it so that you can examine a finished plan, figure out what
decisions the planner made, and then somehow get the planner to make
those same decisions over again in a future planning cycle.

I am feeling that this is similar with Oracle's outline feature, where
the final plan is examined and then a series of hints are stored and
then during the replanning of the the same query, these hints will be
applied to planner. If one of the hints is not appliable any more, like
the index is unusable, it is just ignored.

This list of elided nodes is stored in the PlannedStmt

I did a quick check on the attached patches and I can see some more
information is added into PlannedStmt. then my question are the
PlannedStmt is not avaiable during the future planning cycle, then how
does these information would be helpful on the feture planning? and I'm
strange that there are little changes on the optimizer part. Does this
patchset just a preparation work for reconstructing a same plan in
future?

outside of the portion of the tree that actually gets executed, so
that code that is doing plan tree inspection can look at it but
execution doesn't get any slower.

Thank you for sharing this!

--
Best Regards
Andy Fan

#9Andy Fan
zhihuifan1213@163.com
In reply to: Andy Fan (#8)
Re: plan shape work

Andy Fan <zhihuifan1213@163.com> writes:

This list of elided nodes is stored in the PlannedStmt

I did a quick check on the attached patches and I can see some more
information is added into PlannedStmt. then my question are the
PlannedStmt is not avaiable during the future planning cycle, then how
does these information would be helpful on the feture planning? and I'm
strange that there are little changes on the optimizer part. Does this
patchset just a preparation work for reconstructing a same plan in
future?

I'm sure it is a preliminary work for reconstructing a same plan in
future, sorry for this noise.

--
Best Regards
Andy Fan

#10Robert Haas
robertmhaas@gmail.com
In reply to: Robert Haas (#1)
7 attachment(s)
Re: plan shape work

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

Attachments:

v2-0005-Store-information-about-range-table-flattening-in.patchapplication/octet-stream; name=v2-0005-Store-information-about-range-table-flattening-in.patchDownload
From 198dacaffcc51f374b49c75983acf3c2fc13a022 Mon Sep 17 00:00:00 2001
From: Robert Haas <rhaas@postgresql.org>
Date: Fri, 21 Mar 2025 11:06:35 -0400
Subject: [PATCH v2 5/7] Store information about range-table flattening in the
 final plan.

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.  So,
this commit remembers in the final plan which part of the final range
table came from which subquery's range table.

Additionally, this commit teaches pg_overexplain'e RANGE_TABLE option
to display the subquery name for each range table entry.
---
 contrib/pg_overexplain/pg_overexplain.c | 36 +++++++++++++++++++++++++
 src/backend/optimizer/plan/planner.c    |  1 +
 src/backend/optimizer/plan/setrefs.c    | 20 ++++++++++++++
 src/include/nodes/pathnodes.h           |  3 +++
 src/include/nodes/plannodes.h           | 17 ++++++++++++
 src/tools/pgindent/typedefs.list        |  1 +
 6 files changed, 78 insertions(+)

diff --git a/contrib/pg_overexplain/pg_overexplain.c b/contrib/pg_overexplain/pg_overexplain.c
index bd70b6d9d5e..5dc707d69e3 100644
--- a/contrib/pg_overexplain/pg_overexplain.c
+++ b/contrib/pg_overexplain/pg_overexplain.c
@@ -395,6 +395,8 @@ static void
 overexplain_range_table(PlannedStmt *plannedstmt, ExplainState *es)
 {
 	Index		rti;
+	ListCell   *lc_subrtinfo = list_head(plannedstmt->subrtinfos);
+	SubPlanRTInfo *rtinfo = NULL;
 
 	/* Open group, one entry per RangeTblEntry */
 	ExplainOpenGroup("Range Table", "Range Table", false, es);
@@ -405,6 +407,18 @@ overexplain_range_table(PlannedStmt *plannedstmt, ExplainState *es)
 		RangeTblEntry *rte = rt_fetch(rti, plannedstmt->rtable);
 		char	   *kind = NULL;
 		char	   *relkind;
+		SubPlanRTInfo *next_rtinfo;
+
+		/* Advance to next SubRTInfo, if it's time. */
+		if (lc_subrtinfo != NULL)
+		{
+			next_rtinfo = lfirst(lc_subrtinfo);
+			if (rti > next_rtinfo->rtoffset)
+			{
+				rtinfo = next_rtinfo;
+				lc_subrtinfo = lnext(plannedstmt->subrtinfos, lc_subrtinfo);
+			}
+		}
 
 		/* NULL entries are possible; skip them */
 		if (rte == NULL)
@@ -469,6 +483,28 @@ overexplain_range_table(PlannedStmt *plannedstmt, ExplainState *es)
 			ExplainPropertyBool("In From Clause", rte->inFromCl, es);
 		}
 
+		/*
+		 * Indicate which subplan is the origin of which RTE. Note dummy
+		 * subplans. Here again, we crunch more onto one line in text format.
+		 */
+		if (rtinfo != NULL)
+		{
+			if (es->format == EXPLAIN_FORMAT_TEXT)
+			{
+				if (!rtinfo->dummy)
+					ExplainPropertyText("Subplan", rtinfo->plan_name, es);
+				else
+					ExplainPropertyText("Subplan",
+										psprintf("%s (dummy)",
+												 rtinfo->plan_name), es);
+			}
+			else
+			{
+				ExplainPropertyText("Subplan", rtinfo->plan_name, es);
+				ExplainPropertyBool("Subplan Is Dummy", rtinfo->dummy, es);
+			}
+		}
+
 		/* rte->alias is optional; rte->eref is requested */
 		if (rte->alias != NULL)
 			overexplain_alias("Alias", rte->alias, es);
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index b0e8d06734d..790b14382a4 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -579,6 +579,7 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions,
 	result->unprunableRelids = bms_difference(glob->allRelids,
 											  glob->prunableRelids);
 	result->permInfos = glob->finalrteperminfos;
+	result->subrtinfos = glob->subrtinfos;
 	result->resultRelations = glob->resultRelations;
 	result->appendRelations = glob->appendRelations;
 	result->subplans = glob->subplans;
diff --git a/src/backend/optimizer/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.c
index 6950eff2c5b..eef43792aeb 100644
--- a/src/backend/optimizer/plan/setrefs.c
+++ b/src/backend/optimizer/plan/setrefs.c
@@ -399,6 +399,26 @@ add_rtes_to_flat_rtable(PlannerInfo *root, bool recursing)
 	Index		rti;
 	ListCell   *lc;
 
+	/*
+	 * Record enough information to make it possible for code that looks at
+	 * the final range table to understand how it was constructed. (If
+	 * finalrtable is still NIL, then this is the very topmost PlannerInfo,
+	 * which will always have plan_name == NULL and rtoffset == 0; we omit the
+	 * degenerate list entry.)
+	 */
+	if (root->glob->finalrtable != NIL)
+	{
+		SubPlanRTInfo *rtinfo = makeNode(SubPlanRTInfo);
+
+		rtinfo->plan_name = root->plan_name;
+		rtinfo->rtoffset = list_length(root->glob->finalrtable);
+
+		/* When recursing = true, it's an unplanned or dummy subquery. */
+		rtinfo->dummy = recursing;
+
+		root->glob->subrtinfos = lappend(root->glob->subrtinfos, rtinfo);
+	}
+
 	/*
 	 * Add the query's own RTEs to the flattened rangetable.
 	 *
diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h
index e41597acb02..4f8586f6591 100644
--- a/src/include/nodes/pathnodes.h
+++ b/src/include/nodes/pathnodes.h
@@ -135,6 +135,9 @@ typedef struct PlannerGlobal
 	/* "flat" list of RTEPermissionInfos */
 	List	   *finalrteperminfos;
 
+	/* list of SubPlanRTInfo nodes */
+	List	   *subrtinfos;
+
 	/* "flat" list of PlanRowMarks */
 	List	   *finalrowmarks;
 
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index 1abcf90be43..3f2d6fafc24 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -131,6 +131,9 @@ typedef struct PlannedStmt
 	 */
 	List	   *subplans;
 
+	/* a list of SubPlanRTInfo objects */
+	List	   *subrtinfos;
+
 	/* indices of subplans that require REWIND */
 	Bitmapset  *rewindPlanIDs;
 
@@ -1801,4 +1804,18 @@ typedef enum MonotonicFunction
 	MONOTONICFUNC_BOTH = MONOTONICFUNC_INCREASING | MONOTONICFUNC_DECREASING,
 } MonotonicFunction;
 
+/*
+ * SubPlanRTInfo
+ *
+ * Information about which range table entries came from which subquery
+ * planning cycles.
+ */
+typedef struct SubPlanRTInfo
+{
+	NodeTag		type;
+	char	   *plan_name;
+	Index		rtoffset;
+	bool		dummy;
+} SubPlanRTInfo;
+
 #endif							/* PLANNODES_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index a13e8162890..8e4367dabaf 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -4347,3 +4347,4 @@ yyscan_t
 z_stream
 z_streamp
 zic_t
+SubPlanRTInfo
-- 
2.39.5 (Apple Git-154)

v2-0003-Assert-that-RTIs-of-joined-rels-are-discoverable-.patchapplication/octet-stream; name=v2-0003-Assert-that-RTIs-of-joined-rels-are-discoverable-.patchDownload
From 61c09ec7b967677f974e9c9eaa58c71e4f9c08f2 Mon Sep 17 00:00:00 2001
From: Robert Haas <rhaas@postgresql.org>
Date: Wed, 16 Apr 2025 08:32:00 -0400
Subject: [PATCH v2 3/7] Assert that RTIs of joined rels are discoverable from
 join plans.

Every RTI that appears in the joinrel's relid set should be findable
via the outer or inner plan, except for join RTIs which aren't
necessarily preserved in the final plan. This is a requirement if
we want to be able 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.

Note that this depends on the earlier commit to add a relids field to
Result nodes; without that change, a join tree involving two or more
Result nodes would be fundamentally ambiguous (and even a join tree
involving one could only be interpreted by guessing at its origin).
---
 src/backend/optimizer/plan/createplan.c | 169 ++++++++++++++++++++++++
 1 file changed, 169 insertions(+)

diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index 6f8fac85d48..c86fa5bc238 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -320,7 +320,14 @@ static ModifyTable *make_modifytable(PlannerInfo *root, Plan *subplan,
 									 int epqParam);
 static GatherMerge *create_gather_merge_plan(PlannerInfo *root,
 											 GatherMergePath *best_path);
+static void assert_join_preserves_scan_rtis(PlannerInfo *root, Path *best_path,
+											Plan *outer_plan,
+											Plan *inner_plan);
 
+#ifdef USE_ASSERT_CHECKING
+static Bitmapset *get_scanned_rtindexes(PlannerInfo *root, Plan *plan);
+static Bitmapset *remove_join_rtis(PlannerInfo *root, Bitmapset *bms);
+#endif
 
 /*
  * create_plan
@@ -4349,6 +4356,9 @@ create_nestloop_plan(PlannerInfo *root,
 
 	copy_generic_path_info(&join_plan->join.plan, &best_path->jpath.path);
 
+	assert_join_preserves_scan_rtis(root, &best_path->jpath.path, outer_plan,
+									inner_plan);
+
 	return join_plan;
 }
 
@@ -4703,6 +4713,9 @@ create_mergejoin_plan(PlannerInfo *root,
 	/* Costs of sort and material steps are included in path cost already */
 	copy_generic_path_info(&join_plan->join.plan, &best_path->jpath.path);
 
+	assert_join_preserves_scan_rtis(root, &best_path->jpath.path,
+									outer_plan, inner_plan);
+
 	return join_plan;
 }
 
@@ -4876,6 +4889,9 @@ create_hashjoin_plan(PlannerInfo *root,
 
 	copy_generic_path_info(&join_plan->join.plan, &best_path->jpath.path);
 
+	assert_join_preserves_scan_rtis(root, &best_path->jpath.path,
+									outer_plan, inner_plan);
+
 	return join_plan;
 }
 
@@ -7326,3 +7342,156 @@ is_projection_capable_plan(Plan *plan)
 	}
 	return true;
 }
+
+/*
+ * Check that the RTIs of the relations being joined at this level are
+ * properly reflected in the Plan tree.
+ *
+ * We expect to find every non-RTE_JOIN RTI from best_path->parent.relids
+ * mentioned in either the outer or inner subplan.
+ */
+static void
+assert_join_preserves_scan_rtis(PlannerInfo *root, Path *best_path,
+								Plan *outer_plan, Plan *inner_plan)
+{
+#ifdef USE_ASSERT_CHECKING
+	Bitmapset  *outer_scanrelids;
+	Bitmapset  *inner_scanrelids;
+	Bitmapset  *calculated_scanrelids;
+	Bitmapset  *filtered_joinrelids;
+
+	outer_scanrelids = get_scanned_rtindexes(root, outer_plan);
+	inner_scanrelids = get_scanned_rtindexes(root, inner_plan);
+	calculated_scanrelids = bms_union(outer_scanrelids, inner_scanrelids);
+	filtered_joinrelids = remove_join_rtis(root, best_path->parent->relids);
+
+	/* Any given scan RTI should appear on only one side or the other. */
+	Assert(!bms_overlap(inner_scanrelids, outer_scanrelids));
+
+	/*
+	 * If this assertion fails, it means that the set of range table indexes
+	 * that we found in the inner and outer path tree did not equal the set of
+	 * range table indexes that we found for this joinrel, even after
+	 * excluding RTE_JOIN range table indexes which are not expect to appear
+	 * in the plan tree.
+	 *
+	 * If this assertion fails due to the addition of a new executor node
+	 * type, you probably just need to update get_scanned_rtindexes to know
+	 * about the new node. See the header comments for that function for other
+	 * places to update at the same time.
+	 */
+	Assert(bms_equal(calculated_scanrelids, filtered_joinrelids));
+#endif
+}
+
+#ifdef USE_ASSERT_CHECKING
+/*
+ * Get the set of range table indexes that are scanned by a scan or join node,
+ * or any executor node that could appear beneath a scan or join node.
+ *
+ * We are uninterested in join RTIs here; we're only interested in which RTIs
+ * are scanned at or below a particular plan node, and only if that node can
+ * appear beneath a join.
+ *
+ * When adding new cases to this function, be sure to also update
+ * ExplainPreScanNode, ExplainNode, and overexplain_per_node_hook as
+ * appropriate.
+ */
+static Bitmapset *
+get_scanned_rtindexes(PlannerInfo *root, Plan *plan)
+{
+	switch (nodeTag(plan))
+	{
+		case T_SeqScan:
+		case T_SampleScan:
+		case T_IndexScan:
+		case T_IndexOnlyScan:
+		case T_BitmapHeapScan:
+		case T_TidScan:
+		case T_TidRangeScan:
+		case T_SubqueryScan:
+		case T_FunctionScan:
+		case T_TableFuncScan:
+		case T_ValuesScan:
+		case T_CteScan:
+		case T_NamedTuplestoreScan:
+		case T_WorkTableScan:
+			return bms_make_singleton(((Scan *) plan)->scanrelid);
+			break;
+		case T_ForeignScan:
+			return ((ForeignScan *) plan)->fs_base_relids;
+			break;
+		case T_CustomScan:
+			return ((CustomScan *) plan)->custom_relids;
+			break;
+		case T_Append:
+			return ((Append *) plan)->apprelids;
+			break;
+		case T_MergeAppend:
+			return ((MergeAppend *) plan)->apprelids;
+			break;
+		case T_Result:
+			if (plan->lefttree)
+				return get_scanned_rtindexes(root, plan->lefttree);
+			else
+				return remove_join_rtis(root, ((Result *) plan)->relids);
+			break;
+		case T_HashJoin:
+		case T_MergeJoin:
+		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;
+			}
+		case T_Sort:
+		case T_IncrementalSort:
+		case T_Unique:
+		case T_Agg:
+		case T_Hash:
+		case T_Gather:
+		case T_GatherMerge:
+		case T_Material:
+		case T_Memoize:
+			return get_scanned_rtindexes(root, plan->lefttree);
+			break;
+		default:
+			break;
+	}
+
+	return NULL;
+}
+
+/*
+ * Return a new Bitmapset containing only those range table indexes from the
+ * input set that do not reference an RTE where rtekind == RTE_JOIN.
+ */
+static Bitmapset *
+remove_join_rtis(PlannerInfo *root, Bitmapset *bms)
+{
+	int			rti = -1;
+
+	bms = bms_copy(bms);
+
+	while ((rti = bms_next_member(bms, rti)) >= 0)
+	{
+		RangeTblEntry *rte = planner_rt_fetch(rti, root);
+
+		if (rte->rtekind == RTE_JOIN)
+			bms = bms_del_member(bms, rti);
+	}
+
+	return bms;
+}
+#endif
-- 
2.39.5 (Apple Git-154)

v2-0004-Give-subplans-names-that-are-known-while-planning.patchapplication/octet-stream; name=v2-0004-Give-subplans-names-that-are-known-while-planning.patchDownload
From 31841dd040fafa5a3942dc87fe3505d365e7b092 Mon Sep 17 00:00:00 2001
From: Robert Haas <rhaas@postgresql.org>
Date: Thu, 5 Dec 2024 15:19:17 -0500
Subject: [PATCH v2 4/7] Give subplans names that are known while planning that
 subplan.

Previously, subplans were shown in EXPLAIN output identified by
a number, like "InitPlan 1", and some were identified by a name,
like "CTE foo". Now, each subplan gets a name, which for InitPlans
and SubPlans is based on the type of sublink e.g. expr_1 or any_1,
and these names are guaranteed to be unique across the whole plan.

The numerical portion of the name may be different than it was
previously, because InitPlan 1 meant the first subplan that we
finished planning (which happened to be an InitPlan). This number
couldn't be known at the time we began planning that subplan,
because the query planner might recurse into other subplans which
would then be fully planned before finishing the plan at the outer
level. These new subplan names are assigned when we *start* planning
a subplan, which allows extensions that affect planning to know the
name that will ultimately be assigned while planning is still in
progress.

Some subplans aren't shown as subplans in EXPLAIN output. This
happens when the subquery is a FROM-cluse item or a branch of a
set operation, rather than, for example, an expression that will
be transformed into something render as an InitPlan or SubPlan.
These subplans also get unique names, although those names are not
currently shown in the EXPLAIN output. This means that it's now
possible to use unique, human-readable names to refer to any
subplan within a query; only the topmost query level is nameless.
---
 .../postgres_fdw/expected/postgres_fdw.out    |  58 ++--
 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        |  83 +++--
 src/backend/optimizer/prep/prepjointree.c     |   4 +
 src/backend/optimizer/prep/prepunion.c        |   5 +-
 src/include/nodes/pathnodes.h                 |   6 +
 src/include/nodes/primnodes.h                 |   1 +
 src/include/optimizer/planner.h               |   5 +-
 src/test/regress/expected/aggregates.out      |  58 ++--
 src/test/regress/expected/create_index.out    |  14 +-
 src/test/regress/expected/groupingsets.out    |  90 +++---
 .../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 | 292 +++++++++---------
 src/test/regress/expected/portals.out         |  12 +-
 src/test/regress/expected/predicate.out       |   8 +-
 src/test/regress/expected/returning.out       |  20 +-
 src/test/regress/expected/rowsecurity.out     | 138 ++++-----
 src/test/regress/expected/rowtypes.out        |  12 +-
 src/test/regress/expected/select_parallel.out |  52 ++--
 src/test/regress/expected/sqljson.out         |   4 +-
 src/test/regress/expected/subselect.out       | 172 +++++------
 src/test/regress/expected/updatable_views.out |  48 +--
 src/test/regress/expected/update.out          |   8 +-
 src/test/regress/expected/window.out          |  10 +-
 src/test/regress/expected/with.out            |   8 +-
 34 files changed, 753 insertions(+), 623 deletions(-)

diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 18268a75180..08b3db35386 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -3190,10 +3190,10 @@ select exists(select 1 from pg_enum), sum(c1) from ft1;
                     QUERY PLAN                    
 --------------------------------------------------
  Foreign Scan
-   Output: (InitPlan 1).col1, (sum(ft1.c1))
+   Output: (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)
 
@@ -3208,8 +3208,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: (exists_1).col1, sum(ft1.c1)
+   InitPlan exists_1
      ->  Seq Scan on pg_catalog.pg_enum
    ->  Foreign Scan on public.ft1
          Output: ft1.c1
@@ -3368,15 +3368,15 @@ select distinct (select count(*) filter (where t2.c2 = 6 and t2.c1 < 10) from ft
                                                           QUERY PLAN                                                          
 ------------------------------------------------------------------------------------------------------------------------------
  Unique
-   Output: ((SubPlan 1))
+   Output: ((expr_1))
    ->  Sort
-         Output: ((SubPlan 1))
-         Sort Key: ((SubPlan 1))
+         Output: ((expr_1))
+         Sort Key: ((expr_1))
          ->  Foreign Scan
-               Output: (SubPlan 1)
+               Output: (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))
@@ -3394,14 +3394,14 @@ select distinct (select count(t1.c1) filter (where t2.c2 = 6 and t2.c1 < 10) fro
                                                                       QUERY PLAN                                                                      
 ------------------------------------------------------------------------------------------------------------------------------------------------------
  Unique
-   Output: ((SubPlan 1))
+   Output: ((expr_1))
    ->  Sort
-         Output: ((SubPlan 1))
-         Sort Key: ((SubPlan 1))
+         Output: ((expr_1))
+         Sort Key: ((expr_1))
          ->  Foreign Scan on public.ft2 t2
-               Output: (SubPlan 1)
+               Output: (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)
@@ -3433,14 +3433,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 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))
@@ -6459,9 +6459,9 @@ UPDATE ft2 AS target SET (c2, c7) = (
  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: (multiexpr_1).col1, (multiexpr_1).col2, (rescan 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"))
@@ -12151,9 +12151,9 @@ SELECT * FROM local_tbl t1 LEFT JOIN (SELECT *, (SELECT count(*) FROM async_pt W
                                        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, ((expr_1).col1)
    Join Filter: (t1.a = async_pt.a)
-   InitPlan 1
+   InitPlan expr_1
      ->  Aggregate
            Output: count(*)
            ->  Append
@@ -12165,10 +12165,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, (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, (expr_1).col1
                Remote SQL: SELECT a, b, c FROM public.base_tbl2 WHERE ((a < 3000))
 (20 rows)
 
@@ -12179,7 +12179,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)
@@ -12402,12 +12402,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 = (any_1).col1) AND ((random() > '0'::double precision) = (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 e8bd4c560dd..77dca0a54cf 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -4898,6 +4898,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
@@ -4921,8 +4922,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 65f17101591..b0e8d06734d 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
@@ -8772,3 +8777,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, 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 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 d71ed958e31..d814407ca3e 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 char *sublinktype_to_string(SubLinkType subLinkType);
 
 
 /*
@@ -172,6 +173,7 @@ make_subplan(PlannerInfo *root, Query *orig_subquery,
 	Plan	   *plan;
 	List	   *plan_params;
 	Node	   *result;
+	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, &paramIds);
 		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,31 @@ 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 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);
+}
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/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h
index 4a903d1ec18..e41597acb02 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;
 
@@ -234,6 +237,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..af50831c814 100644
--- a/src/include/optimizer/planner.h
+++ b/src/include/optimizer/planner.h
@@ -43,7 +43,7 @@ extern PlannedStmt *standard_planner(Query *parse, const char *query_string,
 									 ParamListInfo boundParams);
 
 extern PlannerInfo *subquery_planner(PlannerGlobal *glob, Query *parse,
-									 PlannerInfo *parent_root,
+									 char *plan_name, PlannerInfo *parent_root,
 									 bool hasRecursion, double tuple_fraction,
 									 SetOperationStmt *setops);
 
@@ -62,4 +62,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, 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 4aea28d71e3..a0b7cb758fd 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(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: Aggregate
-   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: Aggregate
-   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: Aggregate
-   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: Aggregate
-   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: Aggregate
-   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: Aggregate
-   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: Aggregate
-   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: Aggregate
-           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: (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: ((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: ((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: (((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: Aggregate
-   InitPlan 1
+   InitPlan minmax_1
      ->  Limit
            ->  Result
                  One-Time Filter: (100 IS NOT NULL)
@@ -1258,7 +1258,7 @@ explain (costs off)
 ---------------------------------------------------------------------------------------------
  Result
    Replaces: Aggregate
-   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: ((minmax_1).col1), ((minmax_1).col1)
          ->  Result
                Replaces: Aggregate
 (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: (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..dff9a687145 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, (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 = (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 07a37da79dd..39b7e1d2a35 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((expr_1)), ((expr_2))
+   Group Key: ((expr_2))
    ->  Sort
-         Output: ((SubPlan 2)), i1.q1
-         Sort Key: ((SubPlan 2))
+         Output: ((expr_2)), i1.q1
+         Sort Key: ((expr_2))
          ->  Seq Scan on public.int8_tbl i1
-               Output: (SubPlan 2), i1.q1
-               SubPlan 2
+               Output: (expr_2), i1.q1
+               SubPlan expr_2
                  ->  Result
                        Output: i1.q1
 (11 rows)
@@ -537,19 +537,19 @@ group by ss.x;
                  QUERY PLAN                 
 --------------------------------------------
  GroupAggregate
-   Output: (SubPlan 2), ((SubPlan 3))
-   Group Key: ((SubPlan 3))
+   Output: (expr_1), ((expr_3))
+   Group Key: ((expr_3))
    ->  Sort
-         Output: ((SubPlan 3)), i1.q1
-         Sort Key: ((SubPlan 3))
+         Output: ((expr_3)), i1.q1
+         Sort Key: ((expr_3))
          ->  Seq Scan on public.int8_tbl i1
-               Output: (SubPlan 3), i1.q1
-               SubPlan 3
+               Output: (expr_3), i1.q1
+               SubPlan expr_3
                  ->  Result
                        Output: i1.q1
-   SubPlan 2
+   SubPlan expr_1
      ->  Result
-           Output: GROUPING((SubPlan 1))
+           Output: GROUPING((expr_2))
 (14 rows)
 
 select (select grouping(ss.x))
@@ -592,7 +592,7 @@ explain (costs off)
 ------------------------------------------------------------
  Result
    Replaces: Aggregate
-   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: (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::integer END), t1.v
-   Sort Key: (CASE WHEN (GROUPING((SubPlan 2)) = 0) THEN ((SubPlan 3)) ELSE NULL::integer END) NULLS FIRST
+   Output: (GROUPING((expr_1))), ((expr_3)), (CASE WHEN (GROUPING((expr_2)) = 0) THEN ((expr_3)) ELSE NULL::integer END), t1.v
+   Sort Key: (CASE WHEN (GROUPING((expr_2)) = 0) THEN ((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::integer END, t1.v
+         Output: GROUPING((expr_1)), ((expr_3)), CASE WHEN (GROUPING((expr_2)) = 0) THEN ((expr_3)) ELSE NULL::integer END, t1.v
          Hash Key: t1.v
-         Hash Key: (SubPlan 3)
+         Hash Key: (expr_3)
          ->  Seq Scan on pg_temp.gstest5 t1
-               Output: (SubPlan 3), t1.v, t1.id
-               SubPlan 3
+               Output: (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::integer END), t1.v
-   Sort Key: (CASE WHEN (GROUPING((SubPlan 2)) = 0) THEN ((SubPlan 3)) ELSE NULL::integer END) NULLS FIRST
+   Output: (GROUPING((expr_1))), ((expr_3)), (CASE WHEN (GROUPING((expr_2)) = 0) THEN ((expr_3)) ELSE NULL::integer END), t1.v
+   Sort Key: (CASE WHEN (GROUPING((expr_2)) = 0) THEN ((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::integer END, t1.v
+         Output: GROUPING((expr_1)), ((expr_3)), CASE WHEN (GROUPING((expr_2)) = 0) THEN ((expr_3)) ELSE NULL::integer END, t1.v
          Hash Key: t1.v
-         Hash Key: (SubPlan 3)
+         Hash Key: (expr_3)
          ->  Seq Scan on pg_temp.gstest5 t1
-               Output: (SubPlan 3), t1.v, t1.id
-               SubPlan 3
+               Output: (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..92fe3527baf 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, ((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, ((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 e1225fb1add..d996fddf3bc 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: (minmax_1).col1
    Replaces: Aggregate
-   InitPlan 1
+   InitPlan minmax_1
      ->  Limit
            Output: ((1 - matest0.id))
            ->  Result
@@ -1948,7 +1948,7 @@ SELECT min(x) FROM
 --------------------------------------------------------------------
  Result
    Replaces: Aggregate
-   InitPlan 1
+   InitPlan minmax_1
      ->  Limit
            ->  Merge Append
                  Sort Key: a.unique1
@@ -1967,7 +1967,7 @@ SELECT min(y) FROM
 --------------------------------------------------------------------
  Result
    Replaces: Aggregate
-   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: (multiexpr_1).col1, (multiexpr_1).col2, (rescan 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: (multiexpr_1).col1, (multiexpr_1).col2, (rescan 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: (multiexpr_1).col1, (multiexpr_1).col2, (rescan 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: Aggregate
-   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..028ae01e307 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(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 96b5e891d97..c73fa1e2f84 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 = (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: Aggregate
-                       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 any_1).col1)))
+   SubPlan any_1
      ->  Index Only Scan using tenk1_unique2 on tenk1 b
 (4 rows)
 
@@ -3688,11 +3688,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 = (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)
@@ -4207,14 +4207,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, (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
@@ -4223,7 +4223,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, (expr_1).col1
                ->  Limit
                      Output: NULL::integer
                      ->  Result
@@ -4237,7 +4237,7 @@ where i8.q2 = 123;
                      Output: (i8.q2)
                      ->  Result
                            Output: i8.q2
-                           One-Time Filter: ((InitPlan 1).col1)
+                           One-Time Filter: ((expr_1).col1)
 (29 rows)
 
 explain (verbose, costs off)
@@ -4250,14 +4250,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, (expr_1).col1, false, (i8.q2)
+   InitPlan expr_1
      ->  Result
            Output: true
-   InitPlan 2
+   InitPlan expr_2
      ->  Result
            Output: true
    ->  Limit
@@ -4267,7 +4267,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, (expr_1).col1
                Filter: (i4.f1 = 0)
          ->  Nested Loop
                Output: i8.q1, i8.q2, (i8.q2)
@@ -4278,7 +4278,7 @@ where i8.q2 = 123;
                      Output: (i8.q2)
                      ->  Result
                            Output: i8.q2
-                           One-Time Filter: ((InitPlan 1).col1)
+                           One-Time Filter: ((expr_1).col1)
 (27 rows)
 
 -- Test proper handling of appendrel PHVs during useless-RTE removal
@@ -5739,13 +5739,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 = (any_1).col1) AND ((random() > '0'::double precision) = (any_1).col2)))
+         SubPlan any_1
            ->  Seq Scan on int8_tbl c
                  Filter: (q1 < b.unique1)
    ->  Hash
@@ -6087,7 +6087,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
@@ -6816,7 +6816,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
@@ -8843,8 +8843,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
@@ -8858,20 +8858,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 = (any_1).col1) AND ((random() > '0'::double precision) = (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: (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 = (expr_1).col1)
 (27 rows)
 
 select * from (values (0), (1)) v(id),
@@ -9583,13 +9583,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 = (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..e96769114db 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 = (expr_1)) AND ((expr_2) = (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: ((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: ((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: (((expr_1) = hjtest_1.id) AND ((expr_3) = (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: ((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: ((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 150dc1b44cf..85aada38120 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(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 c748172e98f..40e63c7a0ab 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 < (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, (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 86cecc3ed71..b8f49c8e118 100644
--- a/src/test/regress/expected/partition_prune.out
+++ b/src/test/regress/expected/partition_prune.out
@@ -1921,15 +1921,15 @@ where asptab.id > ss.b::int;
    ->  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(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(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 < (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 < (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 < (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 < (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 < (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 < (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 = (expr_1).col1) OR (a = (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 = (expr_1).col1) OR (a = (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 = (expr_1).col1) OR (a = (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 = (expr_1).col1)
+         Filter: (b = (expr_2).col1)
          ->  Bitmap Index Scan on ab_a1_b1_a_idx (never executed)
-               Index Cond: (a = (InitPlan 1).col1)
+               Index Cond: (a = (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 = (expr_1).col1)
+         Filter: (b = (expr_2).col1)
          ->  Bitmap Index Scan on ab_a1_b2_a_idx (never executed)
-               Index Cond: (a = (InitPlan 1).col1)
+               Index Cond: (a = (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 = (expr_1).col1)
+         Filter: (b = (expr_2).col1)
          ->  Bitmap Index Scan on ab_a1_b3_a_idx (never executed)
-               Index Cond: (a = (InitPlan 1).col1)
+               Index Cond: (a = (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 = (expr_1).col1)
+         Filter: (b = (expr_2).col1)
          ->  Bitmap Index Scan on ab_a2_b1_a_idx (never executed)
-               Index Cond: (a = (InitPlan 1).col1)
+               Index Cond: (a = (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 = (expr_1).col1)
+         Filter: (b = (expr_2).col1)
          ->  Bitmap Index Scan on ab_a2_b2_a_idx (never executed)
-               Index Cond: (a = (InitPlan 1).col1)
+               Index Cond: (a = (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 = (expr_1).col1)
+         Filter: (b = (expr_2).col1)
          ->  Bitmap Index Scan on ab_a2_b3_a_idx (never executed)
-               Index Cond: (a = (InitPlan 1).col1)
+               Index Cond: (a = (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 = (expr_1).col1)
+         Filter: (b = (expr_2).col1)
          ->  Bitmap Index Scan on ab_a3_b1_a_idx (never executed)
-               Index Cond: (a = (InitPlan 1).col1)
+               Index Cond: (a = (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 = (expr_1).col1)
+         Filter: (b = (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 = (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 = (expr_1).col1)
+         Filter: (b = (expr_2).col1)
          ->  Bitmap Index Scan on ab_a3_b3_a_idx (never executed)
-               Index Cond: (a = (InitPlan 1).col1)
+               Index Cond: (a = (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 = (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 = (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 = (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 = (expr_1).col1)
    ->  Seq Scan on ab_a1_b2 ab_2 (never executed)
-         Filter: (b = (InitPlan 1).col1)
+         Filter: (b = (expr_1).col1)
    ->  Seq Scan on ab_a1_b3 ab_3 (never executed)
-         Filter: (b = (InitPlan 1).col1)
+         Filter: (b = (expr_1).col1)
    ->  Seq Scan on ab_a2_b1 ab_4 (actual rows=0.00 loops=1)
-         Filter: (b = (InitPlan 1).col1)
+         Filter: (b = (expr_1).col1)
    ->  Seq Scan on ab_a2_b2 ab_5 (never executed)
-         Filter: (b = (InitPlan 1).col1)
+         Filter: (b = (expr_1).col1)
    ->  Seq Scan on ab_a2_b3 ab_6 (never executed)
-         Filter: (b = (InitPlan 1).col1)
+         Filter: (b = (expr_1).col1)
    ->  Seq Scan on ab_a3_b1 ab_7 (actual rows=0.00 loops=1)
-         Filter: (b = (InitPlan 1).col1)
+         Filter: (b = (expr_1).col1)
    ->  Seq Scan on ab_a3_b2 ab_8 (never executed)
-         Filter: (b = (InitPlan 1).col1)
+         Filter: (b = (expr_1).col1)
    ->  Seq Scan on ab_a3_b3 ab_9 (never executed)
-         Filter: (b = (InitPlan 1).col1)
+         Filter: (b = (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 = (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 = (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 = (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 = (expr_1).col1)
    ->  Seq Scan on ab_a1_b1 ab_1 (actual rows=0.00 loops=1)
-         Filter: (b = (InitPlan 1).col1)
+         Filter: (b = (expr_1).col1)
    ->  Seq Scan on ab_a1_b2 ab_2 (never executed)
-         Filter: (b = (InitPlan 1).col1)
+         Filter: (b = (expr_1).col1)
    ->  Seq Scan on ab_a1_b3 ab_3 (never executed)
-         Filter: (b = (InitPlan 1).col1)
+         Filter: (b = (expr_1).col1)
    ->  Seq Scan on ab_a2_b1 ab_4 (actual rows=0.00 loops=1)
-         Filter: (b = (InitPlan 1).col1)
+         Filter: (b = (expr_1).col1)
    ->  Seq Scan on ab_a2_b2 ab_5 (never executed)
-         Filter: (b = (InitPlan 1).col1)
+         Filter: (b = (expr_1).col1)
    ->  Seq Scan on ab_a2_b3 ab_6 (never executed)
-         Filter: (b = (InitPlan 1).col1)
+         Filter: (b = (expr_1).col1)
    ->  Seq Scan on ab_a3_b1 ab_7 (actual rows=0.00 loops=1)
-         Filter: (b = (InitPlan 1).col1)
+         Filter: (b = (expr_1).col1)
    ->  Seq Scan on ab_a3_b2 ab_8 (never executed)
-         Filter: (b = (InitPlan 1).col1)
+         Filter: (b = (expr_1).col1)
    ->  Seq Scan on ab_a3_b3 ab_9 (never executed)
-         Filter: (b = (InitPlan 1).col1)
+         Filter: (b = (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 = (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 = (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 = (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 = (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 = (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 = (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 = (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 = (expr_1).col1)
                      ->  Seq Scan on ab_a2_b2 ab_a2_2 (never executed)
-                           Filter: (b = (InitPlan 1).col1)
+                           Filter: (b = (expr_1).col1)
                      ->  Seq Scan on ab_a2_b3 ab_a2_3 (never executed)
-                           Filter: (b = (InitPlan 1).col1)
+                           Filter: (b = (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 = (expr_1).col1)
    ->  Seq Scan on listp_2_1 listp_2 (never executed)
-         Filter: (a = (InitPlan 1).col1)
+         Filter: (a = (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) < (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) < (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) < (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 = (expr_1).col1)
    ->  Seq Scan on boolp_t boolp_2 (actual rows=0.00 loops=1)
-         Filter: (a = (InitPlan 1).col1)
+         Filter: (a = (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 = (expr_1).col1)
    ->  Seq Scan on boolp_t boolp_2 (never executed)
-         Filter: (a = (InitPlan 1).col1)
+         Filter: (a = (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: Aggregate
-           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 >= (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 >= (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 >= (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 = (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 = (expr_1).col1))
    ->  Result
-         One-Time Filter: (1 = (InitPlan 1).col1)
+         One-Time Filter: (1 = (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 = (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 = (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 = (expr_1).col1))
 (10 rows)
 
 execute q (1, 1);
@@ -4113,8 +4113,8 @@ select * from listp where a = (select 2) and b <> 10;
                      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 = (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 = (expr_1).col1)
          ->  Parallel Seq Scan on listp_12_2 listp_2 (never executed)
-               Filter: (a = (InitPlan 1).col1)
+               Filter: (a = (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 = (expr_2).col1)
                ->  Parallel Seq Scan on listp_12_2 listp_2 (actual rows=N loops=N)
-                     Filter: (a = (InitPlan 2).col1)
+                     Filter: (a = (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 = (expr_1).col1)
                ->  Parallel Seq Scan on listp_12_2 listp_5 (never executed)
-                     Filter: (a = (InitPlan 1).col1)
+                     Filter: (a = (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[(expr_1).col1, (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[(expr_1).col1, (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[(expr_1).col1, (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[(expr_1).col1, (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 4ee6f4a52ea..45eaa5da61a 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(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: (exists_1).col1
+   InitPlan exists_1
      ->  Result
            One-Time Filter: false
            Replaces: Join on t3, t4, t5, t6
diff --git a/src/test/regress/expected/returning.out b/src/test/regress/expected/returning.out
index 341b689f766..3df940ee8fc 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: (expr_1), (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
@@ -581,23 +581,23 @@ UPDATE foo SET f4 = 100 WHERE f1 = 5
                           QUERY PLAN                           
 ---------------------------------------------------------------
  Update on pg_temp.foo
-   Output: (SubPlan 1), (SubPlan 2), (SubPlan 3)
+   Output: (expr_1), (expr_2), (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: (expr_1), (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 e7fe3317638..4c60c25a476 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 <= (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 <= (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 <= (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 <= (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 <= (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 <= (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 <= (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 <= (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 <= (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 <= (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 <= (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 <= (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 <= (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 <= (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 <= (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 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 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 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 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 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 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 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 328b1e142c0..878baee2d59 100644
--- a/src/test/regress/expected/rowtypes.out
+++ b/src/test/regress/expected/rowtypes.out
@@ -1249,19 +1249,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: ((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: (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..3671d261f1f 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 any_1).col1) AND (four = (hashed 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 any_1).col1)))
+   SubPlan any_1
      ->  Seq Scan on tenk2
 (4 rows)
 
@@ -346,7 +346,7 @@ explain (costs off)
                       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 = (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 ((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 = (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 = (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(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: ((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..d660049f134 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: (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 db061ee95b1..2572af229b9 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 = (rowcompare_1).col1) AND (2 = (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 = (rowcompare_1).col1) AND (2 = (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 = (all_1).col1))
+   SubPlan all_1
      ->  Materialize
-           Output: ((InitPlan 1).col1)
-           InitPlan 1
+           Output: ((expr_1).col1)
+           InitPlan expr_1
              ->  Result
                    Output: 1
            ->  Result
-                 Output: (InitPlan 1).col1
+                 Output: (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(exists_1)
+   SubPlan exists_1
      ->  Limit
            ->  Seq Scan on int4_tbl i
                  Filter: (f1 = o.f1)
@@ -1097,11 +1097,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 any_1).col1))
+   SubPlan any_1
      ->  Append
            ->  Result
                  Output: 'bar'::name
@@ -1121,11 +1121,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 = (any_1).col1))
+   SubPlan any_1
      ->  Materialize
            Output: '("(1)")'::record
            ->  Result
@@ -1164,11 +1164,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 any_1).col1))
+   SubPlan any_1
      ->  Seq Scan on inner_text
 (4 rows)
 
@@ -1185,11 +1185,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 any_1).col1) AND ((q1)::text = (hashed any_1).col1)))
+   SubPlan any_1
      ->  Seq Scan on inner_text
 (4 rows)
 
@@ -1206,11 +1206,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 ((any_1).col1 = (q1)::text))
+   SubPlan any_1
      ->  Materialize
            ->  Seq Scan on inner_text
 (5 rows)
@@ -1229,12 +1229,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 exists_2).col1)) OR (ten < 0))
+         SubPlan exists_2
            ->  Index Only Scan using tenk1_unique1 on tenk1 k
 (5 rows)
 
@@ -1254,10 +1254,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(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)
@@ -1279,20 +1279,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(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 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
@@ -1328,14 +1328,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: (expr_1).col1, (expr_2).col1
+   InitPlan expr_1
      ->  Result
            Output: now()
-   InitPlan 2
+   InitPlan expr_2
      ->  Result
            Output: now()
 (8 rows)
@@ -1343,13 +1343,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: (expr_1).col1
+         InitPlan expr_1
            ->  Result
                  Output: random()
 (7 rows)
@@ -1360,12 +1360,12 @@ explain (verbose, costs off)
                               QUERY PLAN                              
 ----------------------------------------------------------------------
  Values Scan on "*VALUES*"
-   Output: (SubPlan 1), (SubPlan 2)
-   SubPlan 1
+   Output: (expr_1), (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)
@@ -1379,8 +1379,8 @@ explain (verbose, costs off)
  Subquery Scan on ss
    Output: ss.x, ss.x
    ->  Values Scan on "*VALUES*"
-         Output: (SubPlan 1)
-         SubPlan 1
+         Output: (expr_1)
+         SubPlan expr_1
            ->  Result
                  Output: random()
                  One-Time Filter: ("*VALUES*".column1 = "*VALUES*".column1)
@@ -1400,16 +1400,16 @@ where o.ten = 0;
                                                                                          QUERY PLAN                                                                                          
 ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  Aggregate
-   Output: sum((((ANY (i.ten = (hashed SubPlan 1).col1))))::integer)
+   Output: sum((((ANY (i.ten = (hashed any_1).col1))))::integer)
    ->  Nested Loop
-         Output: ((ANY (i.ten = (hashed SubPlan 1).col1)))
+         Output: ((ANY (i.ten = (hashed 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 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)
@@ -1618,7 +1618,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)
@@ -1645,12 +1645,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 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)
@@ -2749,14 +2749,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 = (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)
@@ -2766,14 +2766,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 = (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)
@@ -2783,14 +2783,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 = (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)
@@ -2852,7 +2852,7 @@ ON B.hundred in (SELECT min(c.hundred) FROM tenk2 C WHERE c.odd = b.odd);
                            Filter: (b.hundred = "ANY_subquery".min)
                            ->  Result
                                  Replaces: Aggregate
-                                 InitPlan 1
+                                 InitPlan minmax_1
                                    ->  Limit
                                          ->  Index Scan using tenk2_hundred on tenk2 c
                                                Index Cond: (hundred IS NOT NULL)
@@ -3093,7 +3093,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)
@@ -3109,7 +3109,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
@@ -3128,7 +3128,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..66747f8af82 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)
@@ -3170,18 +3170,18 @@ EXPLAIN (costs off) INSERT INTO rw_view1 VALUES (2, 'New row 2');
                         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: ((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: (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(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(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(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(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(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(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(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(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: (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..a0aac9d4377 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: (multiexpr_1).col1, (multiexpr_1).col2, (rescan 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..55719226bef 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((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 26c88505140..4156105685f 100644
--- a/src/test/regress/expected/with.out
+++ b/src/test/regress/expected/with.out
@@ -3168,7 +3168,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
@@ -3200,7 +3200,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
@@ -3243,11 +3243,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.39.5 (Apple Git-154)

v2-0001-Keep-track-of-what-RTIs-a-Result-node-is-scanning.patchapplication/octet-stream; name=v2-0001-Keep-track-of-what-RTIs-a-Result-node-is-scanning.patchDownload
From 2b2330d6e31b72dc8e5e1bdfad06ee1f44fde98d Mon Sep 17 00:00:00 2001
From: Robert Haas <rhaas@postgresql.org>
Date: Tue, 26 Aug 2025 09:17:42 -0400
Subject: [PATCH v2 1/7] Keep track of what RTIs a Result node is scanning.

Result nodes now include an RTI set, which is only non-NULL when they
have no subplan, and is taken from the relid set of the RelOptInfo
that the Result is generating.

Using that information, EXPLAIN now emits, where relevant, a "Replaces" line
that says whether it replaced a scan, a join, or an aggregate; and in the
former two cases, which relations were involved.

Likewise, pg_overexplain's EXPLAIN (RANGE_TABLE) now displays the RTIs
stored in a Result node just as it already does for other RTI-bearing
node types.
---
 contrib/file_fdw/expected/file_fdw.out        |   1 +
 .../expected/pg_overexplain.out               |   6 +-
 contrib/pg_overexplain/pg_overexplain.c       |  12 ++
 .../postgres_fdw/expected/postgres_fdw.out    |   9 +-
 src/backend/commands/explain.c                |  95 ++++++++++++
 src/backend/optimizer/plan/createplan.c       |  93 +++++++++---
 src/backend/optimizer/plan/setrefs.c          |   2 +
 src/include/nodes/plannodes.h                 |   9 ++
 src/test/regress/expected/aggregates.out      |  51 ++++---
 src/test/regress/expected/case.out            |  21 +--
 .../regress/expected/generated_virtual.out    |   3 +-
 src/test/regress/expected/groupingsets.out    |   3 +-
 src/test/regress/expected/inherit.out         |  28 ++--
 src/test/regress/expected/join.out            | 113 ++++++++------
 src/test/regress/expected/merge.out           |   3 +-
 .../regress/expected/partition_aggregate.out  |  21 +--
 src/test/regress/expected/partition_join.out  |  23 +--
 src/test/regress/expected/partition_prune.out | 141 ++++++++++--------
 src/test/regress/expected/predicate.out       |  19 ++-
 src/test/regress/expected/rowsecurity.out     |  13 +-
 src/test/regress/expected/rowtypes.out        |   6 +-
 src/test/regress/expected/select.out          |   7 +-
 src/test/regress/expected/subselect.out       |   6 +-
 src/test/regress/expected/tsrf.out            |   6 +-
 24 files changed, 483 insertions(+), 208 deletions(-)

diff --git a/contrib/file_fdw/expected/file_fdw.out b/contrib/file_fdw/expected/file_fdw.out
index 246e3d3e566..5e710d06de0 100644
--- a/contrib/file_fdw/expected/file_fdw.out
+++ b/contrib/file_fdw/expected/file_fdw.out
@@ -323,6 +323,7 @@ SELECT explain_filter('EXPLAIN (VERBOSE, COSTS FALSE) SELECT * FROM agg_csv WHER
  Result
    Output: a, b
    One-Time Filter: false
+   Replaces: Scan on agg_csv
 
 \t off
 SELECT * FROM agg_csv WHERE a < 0;
diff --git a/contrib/pg_overexplain/expected/pg_overexplain.out b/contrib/pg_overexplain/expected/pg_overexplain.out
index 6de02323d7c..55d34666d87 100644
--- a/contrib/pg_overexplain/expected/pg_overexplain.out
+++ b/contrib/pg_overexplain/expected/pg_overexplain.out
@@ -44,9 +44,10 @@ EXPLAIN (RANGE_TABLE) SELECT 1;
                 QUERY PLAN                
 ------------------------------------------
  Result  (cost=0.00..0.01 rows=1 width=4)
+   RTIs: 1
  RTI 1 (result):
    Eref: "*RESULT*" ()
-(3 rows)
+(4 rows)
 
 -- Create a partitioned table.
 CREATE TABLE vegetables (id serial, name text, genus text)
@@ -475,6 +476,7 @@ INSERT INTO vegetables (name, genus) VALUES ('broccoflower', 'brassica');
    Nominal RTI: 1
    Exclude Relation RTI: 0
    ->  Result
+         RTIs: 2
  RTI 1 (relation):
    Eref: vegetables (id, name, genus)
    Relation: vegetables
@@ -485,5 +487,5 @@ INSERT INTO vegetables (name, genus) VALUES ('broccoflower', 'brassica');
    Eref: "*RESULT*" ()
  Unprunable RTIs: 1
  Result RTIs: 1
-(14 rows)
+(15 rows)
 
diff --git a/contrib/pg_overexplain/pg_overexplain.c b/contrib/pg_overexplain/pg_overexplain.c
index de824566f8c..bd70b6d9d5e 100644
--- a/contrib/pg_overexplain/pg_overexplain.c
+++ b/contrib/pg_overexplain/pg_overexplain.c
@@ -236,6 +236,18 @@ overexplain_per_node_hook(PlanState *planstate, List *ancestors,
 									  ((MergeAppend *) plan)->apprelids,
 									  es);
 				break;
+			case T_Result:
+
+				/*
+				 * 'relids' is only meaningful when plan->lefttree is NULL,
+				 * but if somehow it ends up set when plan->lefttree is not
+				 * NULL, print it anyway.
+				 */
+				if (plan->lefttree == NULL ||
+					((Result *) plan)->relids != NULL)
+					overexplain_bitmapset("RTIs",
+										  ((Result *) plan)->relids,
+										  es);
 			default:
 				break;
 		}
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index d3323b04676..18268a75180 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -7170,7 +7170,8 @@ EXPLAIN (VERBOSE, COSTS OFF) SELECT count(*) FROM ft1 WHERE c2 < 0;
    Output: count(*)
    ->  Result
          One-Time Filter: false
-(4 rows)
+         Replaces: Scan on ft1
+(5 rows)
 
 SELECT count(*) FROM ft1 WHERE c2 < 0;
  count 
@@ -7214,7 +7215,8 @@ EXPLAIN (VERBOSE, COSTS OFF) SELECT count(*) FROM ft1 WHERE c2 >= 0;
    Output: count(*)
    ->  Result
          One-Time Filter: false
-(4 rows)
+         Replaces: Scan on ft1
+(5 rows)
 
 SELECT count(*) FROM ft1 WHERE c2 >= 0;
  count 
@@ -8043,7 +8045,8 @@ DELETE FROM rem1 WHERE false;     -- currently can't be pushed down
    ->  Result
          Output: ctid
          One-Time Filter: false
-(5 rows)
+         Replaces: Scan on rem1
+(6 rows)
 
 -- Test with statement-level triggers
 CREATE TRIGGER trig_stmt_before
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 8345bc0264b..42723c3a150 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -147,6 +147,7 @@ static void show_buffer_usage(ExplainState *es, const BufferUsage *usage);
 static void show_wal_usage(ExplainState *es, const WalUsage *usage);
 static void show_memory_counters(ExplainState *es,
 								 const MemoryContextCounters *mem_counters);
+static void show_result_replacement_info(Result *result, ExplainState *es);
 static void ExplainIndexScanDetails(Oid indexid, ScanDirection indexorderdir,
 									ExplainState *es);
 static void ExplainScanTarget(Scan *plan, ExplainState *es);
@@ -2238,6 +2239,8 @@ ExplainNode(PlanState *planstate, List *ancestors,
 			if (plan->qual)
 				show_instrumentation_count("Rows Removed by Filter", 1,
 										   planstate, es);
+			if (plan->lefttree == NULL)
+				show_result_replacement_info(castNode(Result, plan), es);
 			break;
 		case T_ModifyTable:
 			show_modifytable_info(castNode(ModifyTableState, planstate), ancestors,
@@ -4750,6 +4753,98 @@ show_modifytable_info(ModifyTableState *mtstate, List *ancestors,
 		ExplainCloseGroup("Target Tables", "Target Tables", false, es);
 }
 
+/*
+ * Explain what a "Result" node replaced.
+ */
+static void
+show_result_replacement_info(Result *result, ExplainState *es)
+{
+	StringInfoData buf;
+	int			nrels = 0;
+	int			rti = -1;
+	bool		found_non_result = false;
+	char	   *s;
+
+	/*
+	 * If the Result node has a subplan, it didn't replace a scan, join, or
+	 * aggregate;
+	 */
+	Assert(result->plan.lefttree == NULL);
+
+	/*
+	 * If the relids set is empty, it didn't replace a scan or a join, so it
+	 * must have come from an upper rel. Currently, an aggregate seems to be
+	 * the only possibility. (We might want to add more details to the Result
+	 * node in the future to disambiguate; or to show the rels being
+	 * aggregated in the case of partitionwise aggregate.)
+	 */
+	if (bms_is_empty(result->relids))
+	{
+		ExplainPropertyText("Replaces", "Aggregate", es);
+		return;
+	}
+
+	/*
+	 * Build up a comma-separated list of user-facing names for the range
+	 * table entries in the relids set.
+	 */
+	initStringInfo(&buf);
+	while ((rti = bms_next_member(result->relids, rti)) >= 0)
+	{
+		RangeTblEntry *rte = rt_fetch(rti, es->rtable);
+		char	   *refname;
+
+		/*
+		 * add_outer_joins_to_relids will add join RTIs to the relids set of a
+		 * join; if that join is then replaced with a Result node, we may see
+		 * such RTIs here. But we want to completely ignore those here,
+		 * because "a LEFT JOIN b ON whatever" is a join between a and b, not
+		 * a join between a, b, and an unnamed join.
+		 */
+		if (rte->rtekind == RTE_JOIN)
+			continue;
+
+		/* Count the number of rels that aren't ignored completely. */
+		++nrels;
+
+		/* Work out what reference name to use and added it the string. */
+		refname = (char *) list_nth(es->rtable_names, rti - 1);
+		if (refname == NULL)
+			refname = rte->eref->aliasname;
+		if (buf.len > 0)
+			appendStringInfoString(&buf, ", ");
+		appendStringInfoString(&buf, refname);
+
+		/* Keep track of whether we see anything other than RTE_RESULT. */
+		if (rte->rtekind != RTE_RESULT)
+			found_non_result = true;
+	}
+
+	/*
+	 * If this Result node is because of a single RTE that is RTE_RESULT, it
+	 * is not really replacing anything at all, because there's no other
+	 * method for implementing a scan of such an RTE, so we don't display the
+	 * Replaces line in such cases.
+	 *
+	 * (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.)
+	 */
+	if (nrels <= 1 && !found_non_result)
+		return;
+
+	/*
+	 * We're replacing either a scan or a join, according to the number of
+	 * rels in the relids set.
+	 */
+	if (nrels > 1)
+		s = psprintf("Join on %s", buf.data);
+	else
+		s = psprintf("Scan on %s", buf.data);
+	ExplainPropertyText("Replaces", s, es);
+}
+
 /*
  * Explain the constituent plans of an Append, MergeAppend,
  * BitmapAnd, or BitmapOr node.
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index 6791cbeb416..6f8fac85d48 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -99,7 +99,8 @@ static Gather *create_gather_plan(PlannerInfo *root, GatherPath *best_path);
 static Plan *create_projection_plan(PlannerInfo *root,
 									ProjectionPath *best_path,
 									int flags);
-static Plan *inject_projection_plan(Plan *subplan, List *tlist, bool parallel_safe);
+static Plan *inject_projection_plan(Plan *subplan, List *tlist,
+									bool parallel_safe);
 static Sort *create_sort_plan(PlannerInfo *root, SortPath *best_path, int flags);
 static IncrementalSort *create_incrementalsort_plan(PlannerInfo *root,
 													IncrementalSortPath *best_path, int flags);
@@ -302,7 +303,10 @@ static SetOp *make_setop(SetOpCmd cmd, SetOpStrategy strategy,
 						 List *tlist, Plan *lefttree, Plan *righttree,
 						 List *groupList, long numGroups);
 static LockRows *make_lockrows(Plan *lefttree, List *rowMarks, int epqParam);
-static Result *make_result(List *tlist, Node *resconstantqual, Plan *subplan);
+static Result *make_gating_result(List *tlist, Node *resconstantqual,
+								  Plan *subplan);
+static Result *make_simple_result(List *tlist, Node *resconstantqual,
+								  Relids relids);
 static ProjectSet *make_project_set(List *tlist, Plan *subplan);
 static ModifyTable *make_modifytable(PlannerInfo *root, Plan *subplan,
 									 CmdType operation, bool canSetTag,
@@ -1014,6 +1018,7 @@ create_gating_plan(PlannerInfo *root, Path *path, Plan *plan,
 {
 	Plan	   *gplan;
 	Plan	   *splan;
+	Relids		relids = NULL;
 
 	Assert(gating_quals);
 
@@ -1021,7 +1026,9 @@ create_gating_plan(PlannerInfo *root, Path *path, Plan *plan,
 	 * We might have a trivial Result plan already.  Stacking one Result atop
 	 * another is silly, so if that applies, just discard the input plan.
 	 * (We're assuming its targetlist is uninteresting; it should be either
-	 * the same as the result of build_path_tlist, or a simplified version.)
+	 * the same as the result of build_path_tlist, or a simplified version.
+	 * However, we preserve the set of relids that it purports to scan and
+	 * attribute that to our replacement Result instead.)
 	 */
 	splan = plan;
 	if (IsA(plan, Result))
@@ -1030,7 +1037,10 @@ create_gating_plan(PlannerInfo *root, Path *path, Plan *plan,
 
 		if (rplan->plan.lefttree == NULL &&
 			rplan->resconstantqual == NULL)
+		{
 			splan = NULL;
+			relids = rplan->relids;
+		}
 	}
 
 	/*
@@ -1038,9 +1048,12 @@ create_gating_plan(PlannerInfo *root, Path *path, Plan *plan,
 	 * tlist; that's never a wrong choice, even if the parent node didn't ask
 	 * for CP_EXACT_TLIST.
 	 */
-	gplan = (Plan *) make_result(build_path_tlist(root, path),
-								 (Node *) gating_quals,
-								 splan);
+	if (splan == NULL)
+		gplan = (Plan *) make_simple_result(build_path_tlist(root, path),
+											(Node *) gating_quals, relids);
+	else
+		gplan = (Plan *) make_gating_result(build_path_tlist(root, path),
+											(Node *) gating_quals, splan);
 
 	/*
 	 * Notice that we don't change cost or size estimates when doing gating.
@@ -1235,10 +1248,10 @@ create_append_plan(PlannerInfo *root, AppendPath *best_path, int flags)
 		/* Generate a Result plan with constant-FALSE gating qual */
 		Plan	   *plan;
 
-		plan = (Plan *) make_result(tlist,
-									(Node *) list_make1(makeBoolConst(false,
-																	  false)),
-									NULL);
+		plan = (Plan *) make_simple_result(tlist,
+										   (Node *) list_make1(makeBoolConst(false,
+																			 false)),
+										   best_path->path.parent->relids);
 
 		copy_generic_path_info(plan, (Path *) best_path);
 
@@ -1636,7 +1649,8 @@ create_group_result_plan(PlannerInfo *root, GroupResultPath *best_path)
 	/* best_path->quals is just bare clauses */
 	quals = order_qual_clauses(root, best_path->quals);
 
-	plan = make_result(tlist, (Node *) quals, NULL);
+	plan = make_simple_result(tlist, (Node *) quals,
+							  best_path->path.parent->relids);
 
 	copy_generic_path_info(&plan->plan, (Path *) best_path);
 
@@ -1933,8 +1947,7 @@ create_projection_plan(PlannerInfo *root, ProjectionPath *best_path, int flags)
 	}
 	else
 	{
-		/* We need a Result node */
-		plan = (Plan *) make_result(tlist, NULL, subplan);
+		plan = (Plan *) make_gating_result(tlist, NULL, subplan);
 
 		copy_generic_path_info(plan, (Path *) best_path);
 	}
@@ -1958,7 +1971,7 @@ inject_projection_plan(Plan *subplan, List *tlist, bool parallel_safe)
 {
 	Plan	   *plan;
 
-	plan = (Plan *) make_result(tlist, NULL, subplan);
+	plan = (Plan *) make_gating_result(tlist, NULL, subplan);
 
 	/*
 	 * In principle, we should charge tlist eval cost plus cpu_per_tuple per
@@ -2436,7 +2449,8 @@ create_minmaxagg_plan(PlannerInfo *root, MinMaxAggPath *best_path)
 	/* Generate the output plan --- basically just a Result */
 	tlist = build_path_tlist(root, &best_path->path);
 
-	plan = make_result(tlist, (Node *) best_path->quals, NULL);
+	plan = make_simple_result(tlist, (Node *) best_path->quals,
+							  best_path->path.parent->relids);
 
 	copy_generic_path_info(&plan->plan, (Path *) best_path);
 
@@ -3887,7 +3901,8 @@ create_resultscan_plan(PlannerInfo *root, Path *best_path,
 			replace_nestloop_params(root, (Node *) scan_clauses);
 	}
 
-	scan_plan = make_result(tlist, (Node *) scan_clauses, NULL);
+	scan_plan = make_simple_result(tlist, (Node *) scan_clauses,
+								   best_path->parent->relids);
 
 	copy_generic_path_info(&scan_plan->plan, best_path);
 
@@ -6922,22 +6937,58 @@ make_limit(Plan *lefttree, Node *limitOffset, Node *limitCount,
 }
 
 /*
- * make_result
- *	  Build a Result plan node
+ * make_gating_result
+ *	  Build a Result plan node that performs projection of a subplan, and/or
+ *	  applies a one time filter (resconstantqual)
  */
 static Result *
-make_result(List *tlist,
-			Node *resconstantqual,
-			Plan *subplan)
+make_gating_result(List *tlist,
+				   Node *resconstantqual,
+				   Plan *subplan)
 {
 	Result	   *node = makeNode(Result);
 	Plan	   *plan = &node->plan;
 
+	Assert(subplan != NULL);
+
 	plan->targetlist = tlist;
 	plan->qual = NIL;
 	plan->lefttree = subplan;
 	plan->righttree = NULL;
 	node->resconstantqual = resconstantqual;
+	node->relids = NULL;
+
+	return node;
+}
+
+/*
+ * 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)
+ *
+ * 'relids' should be the relids set for this path's RelOptInfo. In essence,
+ * we're saying that this Result node generates all the tuples for that
+ * RelOptInfo. Note that the same consideration can never arise in
+ * make_gating_result(), because in that case the tuples are always coming
+ * from some subordinate node.
+ *
+ * NB: It would be nice to assert that the relids set is non-empty here,
+ * but it might be, because this could be a path for an upper rel.
+ */
+static Result *
+make_simple_result(List *tlist,
+				   Node *resconstantqual,
+				   Relids relids)
+{
+	Result	   *node = makeNode(Result);
+	Plan	   *plan = &node->plan;
+
+	plan->targetlist = tlist;
+	plan->qual = NIL;
+	plan->lefttree = NULL;
+	plan->righttree = NULL;
+	node->resconstantqual = resconstantqual;
+	node->relids = relids;
 
 	return node;
 }
diff --git a/src/backend/optimizer/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.c
index d706546f332..6950eff2c5b 100644
--- a/src/backend/optimizer/plan/setrefs.c
+++ b/src/backend/optimizer/plan/setrefs.c
@@ -1056,6 +1056,8 @@ set_plan_refs(PlannerInfo *root, Plan *plan, int rtoffset)
 				/* resconstantqual can't contain any subplan variable refs */
 				splan->resconstantqual =
 					fix_scan_expr(root, splan->resconstantqual, rtoffset, 1);
+				/* adjust the relids set */
+				splan->relids = offset_relid_set(splan->relids, rtoffset);
 			}
 			break;
 		case T_ProjectSet:
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index 29d7732d6a0..1abcf90be43 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -261,12 +261,21 @@ typedef struct Plan
  * If resconstantqual isn't NULL, it represents a one-time qualification
  * test (i.e., one that doesn't depend on any variables from the outer plan,
  * so needs to be evaluated only once).
+ *
+ * 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).
  * ----------------
  */
 typedef struct Result
 {
 	Plan		plan;
 	Node	   *resconstantqual;
+	Bitmapset  *relids;
 } Result;
 
 /* ----------------
diff --git a/src/test/regress/expected/aggregates.out b/src/test/regress/expected/aggregates.out
index 7319945ffe3..4aea28d71e3 100644
--- a/src/test/regress/expected/aggregates.out
+++ b/src/test/regress/expected/aggregates.out
@@ -959,11 +959,12 @@ explain (costs off)
                          QUERY PLAN                         
 ------------------------------------------------------------
  Result
+   Replaces: Aggregate
    InitPlan 1
      ->  Limit
            ->  Index Only Scan using tenk1_unique1 on tenk1
                  Index Cond: (unique1 IS NOT NULL)
-(5 rows)
+(6 rows)
 
 select min(unique1) from tenk1;
  min 
@@ -976,11 +977,12 @@ explain (costs off)
                              QUERY PLAN                              
 ---------------------------------------------------------------------
  Result
+   Replaces: Aggregate
    InitPlan 1
      ->  Limit
            ->  Index Only Scan Backward using tenk1_unique1 on tenk1
                  Index Cond: (unique1 IS NOT NULL)
-(5 rows)
+(6 rows)
 
 select max(unique1) from tenk1;
  max  
@@ -993,11 +995,12 @@ explain (costs off)
                                QUERY PLAN                               
 ------------------------------------------------------------------------
  Result
+   Replaces: Aggregate
    InitPlan 1
      ->  Limit
            ->  Index Only Scan Backward using tenk1_unique1 on tenk1
                  Index Cond: ((unique1 IS NOT NULL) AND (unique1 < 42))
-(5 rows)
+(6 rows)
 
 select max(unique1) from tenk1 where unique1 < 42;
  max 
@@ -1010,11 +1013,12 @@ explain (costs off)
                                QUERY PLAN                               
 ------------------------------------------------------------------------
  Result
+   Replaces: Aggregate
    InitPlan 1
      ->  Limit
            ->  Index Only Scan Backward using tenk1_unique1 on tenk1
                  Index Cond: ((unique1 IS NOT NULL) AND (unique1 > 42))
-(5 rows)
+(6 rows)
 
 select max(unique1) from tenk1 where unique1 > 42;
  max  
@@ -1033,11 +1037,12 @@ explain (costs off)
                                 QUERY PLAN                                 
 ---------------------------------------------------------------------------
  Result
+   Replaces: Aggregate
    InitPlan 1
      ->  Limit
            ->  Index Only Scan Backward using tenk1_unique1 on tenk1
                  Index Cond: ((unique1 IS NOT NULL) AND (unique1 > 42000))
-(5 rows)
+(6 rows)
 
 select max(unique1) from tenk1 where unique1 > 42000;
  max 
@@ -1052,11 +1057,12 @@ explain (costs off)
                                  QUERY PLAN                                 
 ----------------------------------------------------------------------------
  Result
+   Replaces: Aggregate
    InitPlan 1
      ->  Limit
            ->  Index Only Scan Backward using tenk1_thous_tenthous on tenk1
                  Index Cond: ((thousand = 33) AND (tenthous IS NOT NULL))
-(5 rows)
+(6 rows)
 
 select max(tenthous) from tenk1 where thousand = 33;
  max  
@@ -1069,11 +1075,12 @@ explain (costs off)
                                 QUERY PLAN                                
 --------------------------------------------------------------------------
  Result
+   Replaces: Aggregate
    InitPlan 1
      ->  Limit
            ->  Index Only Scan using tenk1_thous_tenthous on tenk1
                  Index Cond: ((thousand = 33) AND (tenthous IS NOT NULL))
-(5 rows)
+(6 rows)
 
 select min(tenthous) from tenk1 where thousand = 33;
  min 
@@ -1090,11 +1097,12 @@ explain (costs off)
  Seq Scan on int4_tbl
    SubPlan 2
      ->  Result
+           Replaces: Aggregate
            InitPlan 1
              ->  Limit
                    ->  Index Only Scan using tenk1_unique1 on tenk1
                          Index Cond: ((unique1 IS NOT NULL) AND (unique1 > int4_tbl.f1))
-(7 rows)
+(8 rows)
 
 select f1, (select min(unique1) from tenk1 where unique1 > f1) AS gt
   from int4_tbl;
@@ -1119,7 +1127,8 @@ explain (costs off)
            ->  Index Only Scan Backward using tenk1_unique2 on tenk1
                  Index Cond: (unique2 IS NOT NULL)
    ->  Result
-(7 rows)
+         Replaces: Aggregate
+(8 rows)
 
 select distinct max(unique2) from tenk1;
  max  
@@ -1138,7 +1147,8 @@ explain (costs off)
            ->  Index Only Scan Backward using tenk1_unique2 on tenk1
                  Index Cond: (unique2 IS NOT NULL)
    ->  Result
-(7 rows)
+         Replaces: Aggregate
+(8 rows)
 
 select max(unique2) from tenk1 order by 1;
  max  
@@ -1157,7 +1167,8 @@ explain (costs off)
            ->  Index Only Scan Backward using tenk1_unique2 on tenk1
                  Index Cond: (unique2 IS NOT NULL)
    ->  Result
-(7 rows)
+         Replaces: Aggregate
+(8 rows)
 
 select max(unique2) from tenk1 order by max(unique2);
  max  
@@ -1176,7 +1187,8 @@ explain (costs off)
            ->  Index Only Scan Backward using tenk1_unique2 on tenk1
                  Index Cond: (unique2 IS NOT NULL)
    ->  Result
-(7 rows)
+         Replaces: Aggregate
+(8 rows)
 
 select max(unique2) from tenk1 order by max(unique2)+1;
  max  
@@ -1196,7 +1208,8 @@ explain (costs off)
                  Index Cond: (unique2 IS NOT NULL)
    ->  ProjectSet
          ->  Result
-(8 rows)
+               Replaces: Aggregate
+(9 rows)
 
 select max(unique2), generate_series(1,3) as g from tenk1 order by g desc;
  max  | g 
@@ -1212,12 +1225,13 @@ explain (costs off)
                      QUERY PLAN                     
 ----------------------------------------------------
  Result
+   Replaces: Aggregate
    InitPlan 1
      ->  Limit
            ->  Result
                  One-Time Filter: (100 IS NOT NULL)
                  ->  Seq Scan on tenk1
-(6 rows)
+(7 rows)
 
 select max(100) from tenk1;
  max 
@@ -1243,6 +1257,7 @@ explain (costs off)
                                          QUERY PLAN                                          
 ---------------------------------------------------------------------------------------------
  Result
+   Replaces: Aggregate
    InitPlan 1
      ->  Limit
            ->  Merge Append
@@ -1265,7 +1280,7 @@ explain (costs off)
                  ->  Index Only Scan using minmaxtest2i on minmaxtest2 minmaxtest_8
                        Index Cond: (f1 IS NOT NULL)
                  ->  Index Only Scan Backward using minmaxtest3i on minmaxtest3 minmaxtest_9
-(23 rows)
+(24 rows)
 
 select min(f1), max(f1) from minmaxtest;
  min | max 
@@ -1304,7 +1319,8 @@ explain (costs off)
    ->  Sort
          Sort Key: ((InitPlan 1).col1), ((InitPlan 2).col1)
          ->  Result
-(26 rows)
+               Replaces: Aggregate
+(27 rows)
 
 select distinct min(f1), max(f1) from minmaxtest;
  min | max 
@@ -1334,7 +1350,8 @@ explain (costs off)
                    ->  Seq Scan on int4_tbl t1
                          Filter: ((f1 IS NOT NULL) AND (f1 = t0.f1))
            ->  Result
-(9 rows)
+                 Replaces: Aggregate
+(10 rows)
 
 select f1, (select distinct min(t1.f1) from int4_tbl t1 where t1.f1 = t0.f1)
 from int4_tbl t0;
diff --git a/src/test/regress/expected/case.out b/src/test/regress/expected/case.out
index efee7fc4317..d74b0dd68cc 100644
--- a/src/test/regress/expected/case.out
+++ b/src/test/regress/expected/case.out
@@ -266,27 +266,30 @@ SELECT *
 -- Tests for constant subexpression simplification
 explain (costs off)
 SELECT * FROM CASE_TBL WHERE NULLIF(1, 2) = 2;
-        QUERY PLAN        
---------------------------
+          QUERY PLAN          
+------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on case_tbl
+(3 rows)
 
 explain (costs off)
 SELECT * FROM CASE_TBL WHERE NULLIF(1, 1) IS NOT NULL;
-        QUERY PLAN        
---------------------------
+          QUERY PLAN          
+------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on case_tbl
+(3 rows)
 
 explain (costs off)
 SELECT * FROM CASE_TBL WHERE NULLIF(1, null) = 2;
-        QUERY PLAN        
---------------------------
+          QUERY PLAN          
+------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on case_tbl
+(3 rows)
 
 --
 -- Examples of updates involving tables
diff --git a/src/test/regress/expected/generated_virtual.out b/src/test/regress/expected/generated_virtual.out
index aca6347babe..8b25e77bb4d 100644
--- a/src/test/regress/expected/generated_virtual.out
+++ b/src/test/regress/expected/generated_virtual.out
@@ -1580,7 +1580,8 @@ select t2.* from gtest32 t1 left join gtest32 t2 on false;
    ->  Result
          Output: a, e, 20, COALESCE(a, 100)
          One-Time Filter: false
-(8 rows)
+         Replaces: Scan on t2
+(9 rows)
 
 select t2.* from gtest32 t1 left join gtest32 t2 on false;
  a | b | c | d | e 
diff --git a/src/test/regress/expected/groupingsets.out b/src/test/regress/expected/groupingsets.out
index 35e4cb47ebe..07a37da79dd 100644
--- a/src/test/regress/expected/groupingsets.out
+++ b/src/test/regress/expected/groupingsets.out
@@ -591,11 +591,12 @@ explain (costs off)
                          QUERY PLAN                         
 ------------------------------------------------------------
  Result
+   Replaces: Aggregate
    InitPlan 1
      ->  Limit
            ->  Index Only Scan using tenk1_unique1 on tenk1
                  Index Cond: (unique1 IS NOT NULL)
-(5 rows)
+(6 rows)
 
 -- Views with GROUPING SET queries
 CREATE VIEW gstest_view AS select a, b, grouping(a,b), sum(c), count(*), max(c)
diff --git a/src/test/regress/expected/inherit.out b/src/test/regress/expected/inherit.out
index 5b5055babdc..e1225fb1add 100644
--- a/src/test/regress/expected/inherit.out
+++ b/src/test/regress/expected/inherit.out
@@ -581,7 +581,8 @@ update some_tab set a = a + 1 where false;
    ->  Result
          Output: (some_tab.a + 1), NULL::oid, NULL::tid
          One-Time Filter: false
-(4 rows)
+         Replaces: Scan on some_tab
+(5 rows)
 
 update some_tab set a = a + 1 where false;
 explain (verbose, costs off)
@@ -593,7 +594,8 @@ update some_tab set a = a + 1 where false returning b, a;
    ->  Result
          Output: (some_tab.a + 1), NULL::oid, NULL::tid
          One-Time Filter: false
-(5 rows)
+         Replaces: Scan on some_tab
+(6 rows)
 
 update some_tab set a = a + 1 where false returning b, a;
  b | a 
@@ -700,7 +702,8 @@ explain update parted_tab set a = 2 where false;
  Update on parted_tab  (cost=0.00..0.00 rows=0 width=0)
    ->  Result  (cost=0.00..0.00 rows=0 width=10)
          One-Time Filter: false
-(3 rows)
+         Replaces: Scan on parted_tab
+(4 rows)
 
 drop table parted_tab;
 -- Check UPDATE with multi-level partitioned inherited target
@@ -1756,6 +1759,7 @@ explain (verbose, costs off) select min(1-id) from matest0;
 ---------------------------------------------------------------------------------
  Result
    Output: (InitPlan 1).col1
+   Replaces: Aggregate
    InitPlan 1
      ->  Limit
            Output: ((1 - matest0.id))
@@ -1779,7 +1783,7 @@ explain (verbose, costs off) select min(1-id) from matest0;
                        ->  Index Scan using matest3i on public.matest3 matest0_4
                              Output: matest0_4.id, (1 - matest0_4.id)
                              Index Cond: ((1 - matest0_4.id) IS NOT NULL)
-(25 rows)
+(26 rows)
 
 select min(1-id) from matest0;
  min 
@@ -1943,6 +1947,7 @@ SELECT min(x) FROM
                              QUERY PLAN                             
 --------------------------------------------------------------------
  Result
+   Replaces: Aggregate
    InitPlan 1
      ->  Limit
            ->  Merge Append
@@ -1951,7 +1956,7 @@ SELECT min(x) FROM
                        Index Cond: (unique1 IS NOT NULL)
                  ->  Index Only Scan using tenk1_unique2 on tenk1 b
                        Index Cond: (unique2 IS NOT NULL)
-(9 rows)
+(10 rows)
 
 explain (costs off)
 SELECT min(y) FROM
@@ -1961,6 +1966,7 @@ SELECT min(y) FROM
                              QUERY PLAN                             
 --------------------------------------------------------------------
  Result
+   Replaces: Aggregate
    InitPlan 1
      ->  Limit
            ->  Merge Append
@@ -1969,7 +1975,7 @@ SELECT min(y) FROM
                        Index Cond: (unique1 IS NOT NULL)
                  ->  Index Only Scan using tenk1_unique2 on tenk1 b
                        Index Cond: (unique2 IS NOT NULL)
-(9 rows)
+(10 rows)
 
 -- XXX planner doesn't recognize that index on unique2 is sufficiently sorted
 explain (costs off)
@@ -3087,11 +3093,12 @@ explain (costs off) select * from range_list_parted where a between 3 and 23 and
 
 /* Should select no rows because range partition key cannot be null */
 explain (costs off) select * from range_list_parted where a is null;
-        QUERY PLAN        
---------------------------
+              QUERY PLAN               
+---------------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on range_list_parted
+(3 rows)
 
 /* Should only select rows from the null-accepting partition */
 explain (costs off) select * from range_list_parted where b is null;
@@ -3252,6 +3259,7 @@ explain (costs off) select min(a), max(a) from parted_minmax where b = '12345';
                                            QUERY PLAN                                           
 ------------------------------------------------------------------------------------------------
  Result
+   Replaces: Aggregate
    InitPlan 1
      ->  Limit
            ->  Index Only Scan using parted_minmax1i on parted_minmax1 parted_minmax
@@ -3260,7 +3268,7 @@ explain (costs off) select min(a), max(a) from parted_minmax where b = '12345';
      ->  Limit
            ->  Index Only Scan Backward using parted_minmax1i on parted_minmax1 parted_minmax_1
                  Index Cond: ((a IS NOT NULL) AND (b = '12345'::text))
-(9 rows)
+(10 rows)
 
 select min(a), max(a) from parted_minmax where b = '12345';
  min | max 
diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out
index 98b05c94a11..76083b1ce57 100644
--- a/src/test/regress/expected/join.out
+++ b/src/test/regress/expected/join.out
@@ -2264,11 +2264,12 @@ explain (costs off)
 select aa, bb, unique1, unique1
   from tenk1 right join b_star on aa = unique1
   where bb < bb and bb is null;
-        QUERY PLAN        
---------------------------
+            QUERY PLAN             
+-----------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Join on tenk1, b_star
+(3 rows)
 
 select aa, bb, unique1, unique1
   from tenk1 right join b_star on aa = unique1
@@ -2386,11 +2387,12 @@ order by t1.unique1;
                            Index Cond: (unique1 < 10)
                SubPlan 2
                  ->  Result
+                       Replaces: Aggregate
                        InitPlan 1
                          ->  Limit
                                ->  Index Only Scan using tenk1_unique1 on tenk1
                                      Index Cond: ((unique1 IS NOT NULL) AND (unique1 = t2.unique1))
-(19 rows)
+(20 rows)
 
 -- Ensure we get the expected result
 select t1.unique1,t2.unique1 from tenk1 t1
@@ -2655,8 +2657,8 @@ select * from int8_tbl t1 left join
     (int8_tbl t2 left join int8_tbl t3 full join int8_tbl t4 on false on false)
     left join int8_tbl t5 on t2.q1 = t5.q1
 on t2.q2 = 123;
-                    QUERY PLAN                    
---------------------------------------------------
+                     QUERY PLAN                     
+----------------------------------------------------
  Nested Loop Left Join
    ->  Seq Scan on int8_tbl t1
    ->  Materialize
@@ -2668,8 +2670,9 @@ on t2.q2 = 123;
                            Filter: (q2 = 123)
                      ->  Result
                            One-Time Filter: false
+                           Replaces: Join on t3, t4
                ->  Seq Scan on int8_tbl t5
-(12 rows)
+(13 rows)
 
 explain (costs off)
 select * from int8_tbl t1
@@ -4147,6 +4150,7 @@ select * from t t1
          ->  Result
                Output: i3
                One-Time Filter: false
+               Replaces: Scan on t3
    ->  Memoize
          Output: t4.i4
          Cache Key: (1)
@@ -4154,7 +4158,7 @@ select * from t t1
          ->  Index Only Scan using t_pkey on pg_temp.t t4
                Output: t4.i4
                Index Cond: (t4.i4 > (1))
-(25 rows)
+(26 rows)
 
 explain (verbose, costs off)
 select * from
@@ -4345,7 +4349,8 @@ from int4_tbl t1
  Result
    Output: (current_database())::information_schema.sql_identifier, (c.relname)::information_schema.sql_identifier
    One-Time Filter: false
-(3 rows)
+   Replaces: Join on t1, t2, a, c, nc, t, nt, bt, nbt
+(4 rows)
 
 -- Test handling of qual pushdown to appendrel members with non-Var outputs
 explain (verbose, costs off)
@@ -4417,11 +4422,12 @@ select unique1 from tenk1, lateral f_immutable_int4(1) x where x = unique1;
 
 explain (costs off)
 select unique1 from tenk1, lateral f_immutable_int4(1) x where x in (select 17);
-        QUERY PLAN        
---------------------------
+        QUERY PLAN         
+---------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on tenk1
+(3 rows)
 
 explain (costs off)
 select unique1, x from tenk1 join f_immutable_int4(1) x on unique1 = x;
@@ -4467,11 +4473,12 @@ select unique1, x from tenk1 full join f_immutable_int4(1) x on unique1 = x;
 -- check that pullup of a const function allows further const-folding
 explain (costs off)
 select unique1 from tenk1, f_immutable_int4(1) x where x = 42;
-        QUERY PLAN        
---------------------------
+        QUERY PLAN         
+---------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on tenk1
+(3 rows)
 
 -- test inlining of immutable functions with PlaceHolderVars
 explain (costs off)
@@ -5328,7 +5335,8 @@ left join
    ->  Result
    ->  Result
          One-Time Filter: false
-(5 rows)
+         Replaces: Join on c, n
+(6 rows)
 
 -- check handling of apparently-commutable outer joins with non-commutable
 -- joins between them
@@ -5522,12 +5530,13 @@ select 1 from
      right join (select 1 as z) as ss2 on true)
   on false,
   lateral (select i4.f1, ss1.n from int8_tbl as i8 limit 1) as ss3;
-        QUERY PLAN        
---------------------------
+                  QUERY PLAN                   
+-----------------------------------------------
  Result
    Output: 1
    One-Time Filter: false
-(3 rows)
+   Replaces: Join on i4, ss3, x1, x2, *RESULT*
+(4 rows)
 
 select 1 from
   int4_tbl as i4
@@ -5556,11 +5565,12 @@ select 1 from t t1
        on false
      where t3.a = coalesce(t5.a,1)) as s2
   on true;
-        QUERY PLAN        
---------------------------
+                 QUERY PLAN                 
+--------------------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Join on t1, s1, t2, t3, t4, t5
+(3 rows)
 
 rollback;
 --
@@ -5957,14 +5967,15 @@ from int4_tbl as t1
                         inner join int8_tbl as t7 on null)
                on t5.q1 = t7.q2)
     on false;
-           QUERY PLAN           
---------------------------------
+                    QUERY PLAN                    
+--------------------------------------------------
  Nested Loop Left Join
    Join Filter: false
    ->  Seq Scan on int4_tbl t1
    ->  Result
          One-Time Filter: false
-(5 rows)
+         Replaces: Join on t2, t3, t4, t5, t7, t6
+(6 rows)
 
 -- variant with Var rather than PHV coming from t6
 explain (costs off)
@@ -5979,14 +5990,15 @@ from int4_tbl as t1
                         inner join int8_tbl as t7 on null)
                on t5.q1 = t7.q2)
     on false;
-           QUERY PLAN           
---------------------------------
+                    QUERY PLAN                    
+--------------------------------------------------
  Nested Loop Left Join
    Join Filter: false
    ->  Seq Scan on int4_tbl t1
    ->  Result
          One-Time Filter: false
-(5 rows)
+         Replaces: Join on t2, t3, t4, t5, t7, t6
+(6 rows)
 
 -- per further discussion of bug #17781
 explain (costs off)
@@ -6036,15 +6048,16 @@ select * from int8_tbl t1 left join
   (int8_tbl t2 inner join int8_tbl t3 on false
    left join int8_tbl t4 on t2.q2 = t4.q2)
 on t1.q1 = t2.q1;
-              QUERY PLAN              
---------------------------------------
+                 QUERY PLAN                 
+--------------------------------------------
  Hash Left Join
    Hash Cond: (t1.q1 = q1)
    ->  Seq Scan on int8_tbl t1
    ->  Hash
          ->  Result
                One-Time Filter: false
-(6 rows)
+               Replaces: Join on t2, t3, t4
+(7 rows)
 
 -- deduce constant-false from an EquivalenceClass
 explain (costs off)
@@ -6052,15 +6065,16 @@ select * from int8_tbl t1 left join
   (int8_tbl t2 inner join int8_tbl t3 on (t2.q1-t3.q2) = 0 and (t2.q1-t3.q2) = 1
    left join int8_tbl t4 on t2.q2 = t4.q2)
 on t1.q1 = t2.q1;
-              QUERY PLAN              
---------------------------------------
+                 QUERY PLAN                 
+--------------------------------------------
  Hash Left Join
    Hash Cond: (t1.q1 = q1)
    ->  Seq Scan on int8_tbl t1
    ->  Hash
          ->  Result
                One-Time Filter: false
-(6 rows)
+               Replaces: Join on t2, t3, t4
+(7 rows)
 
 -- pseudoconstant based on an outer-level Param
 explain (costs off)
@@ -6300,7 +6314,8 @@ select p.* from
 --------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on p
+(3 rows)
 
 select p.* from
   (parent p left join child c on (p.k = c.k)) join parent x on p.k = x.k
@@ -6317,7 +6332,8 @@ select p.* from
 --------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Join on p, x
+(3 rows)
 
 -- bug 5255: this is not optimizable by join removal
 begin;
@@ -6390,7 +6406,8 @@ SELECT q2 FROM
    ->  Result
          Output: q2, 'constant'::text
          One-Time Filter: false
-(9 rows)
+         Replaces: Scan on int8_tbl
+(10 rows)
 
 -- join removal bug #17786: check that OR conditions are cleaned up
 EXPLAIN (COSTS OFF)
@@ -6410,7 +6427,8 @@ FROM int4_tbl
                ->  Seq Scan on tenk1
                ->  Result
                      One-Time Filter: false
-(9 rows)
+                     Replaces: Scan on int8_tbl
+(10 rows)
 
 rollback;
 -- another join removal bug: we must clean up correctly when removing a PHV
@@ -6739,7 +6757,8 @@ where q1.x = q2.y;
 --------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on sj_1
+(3 rows)
 
 -- We can't use a cross-EC generated self join qual because of current logic of
 -- the generate_join_implied_equalities routine.
@@ -7563,11 +7582,12 @@ select 1 from emp1 full join
         on true
     where false) s on true
 where false;
-        QUERY PLAN        
---------------------------
+            QUERY PLAN            
+----------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Join on emp1, t1, t3
+(3 rows)
 
 select 1 from emp1 full join
     (select * from emp1 t1 join
@@ -7797,7 +7817,8 @@ where false;
  Result
    Output: 1
    One-Time Filter: false
-(3 rows)
+   Replaces: Scan on ss
+(4 rows)
 
 --
 -- Test LATERAL
@@ -8736,7 +8757,8 @@ select * from int8_tbl i8 left join lateral
    ->  Result
          Output: f1, i8.q2
          One-Time Filter: false
-(8 rows)
+         Replaces: Scan on int4_tbl
+(9 rows)
 
 explain (verbose, costs off)
 select * from int8_tbl i8 left join lateral
@@ -8750,7 +8772,8 @@ select * from int8_tbl i8 left join lateral
    ->  Result
          Output: f1, f1, i8.q2
          One-Time Filter: false
-(7 rows)
+         Replaces: Join on i1, i2
+(8 rows)
 
 -- check handling of nested appendrels inside LATERAL
 select * from
diff --git a/src/test/regress/expected/merge.out b/src/test/regress/expected/merge.out
index cf2219df754..c748172e98f 100644
--- a/src/test/regress/expected/merge.out
+++ b/src/test/regress/expected/merge.out
@@ -2427,7 +2427,8 @@ MERGE INTO pa_target t USING pa_source s ON t.tid = s.sid
                ->  Result
                      Output: t.tid, t.ctid
                      One-Time Filter: false
-(12 rows)
+                     Replaces: Scan on t
+(13 rows)
 
 MERGE INTO pa_target t USING pa_source s ON t.tid = s.sid
   WHEN NOT MATCHED THEN INSERT VALUES (s.sid);
diff --git a/src/test/regress/expected/partition_aggregate.out b/src/test/regress/expected/partition_aggregate.out
index 5f2c0cf5786..80b002fbdcf 100644
--- a/src/test/regress/expected/partition_aggregate.out
+++ b/src/test/regress/expected/partition_aggregate.out
@@ -146,13 +146,14 @@ SELECT c, a, count(*) FROM pagg_tab GROUP BY a, c;
 -- Test when input relation for grouping is dummy
 EXPLAIN (COSTS OFF)
 SELECT c, sum(a) FROM pagg_tab WHERE 1 = 2 GROUP BY c;
-           QUERY PLAN           
---------------------------------
+             QUERY PLAN             
+------------------------------------
  HashAggregate
    Group Key: c
    ->  Result
          One-Time Filter: false
-(4 rows)
+         Replaces: Scan on pagg_tab
+(5 rows)
 
 SELECT c, sum(a) FROM pagg_tab WHERE 1 = 2 GROUP BY c;
  c | sum 
@@ -161,12 +162,13 @@ SELECT c, sum(a) FROM pagg_tab WHERE 1 = 2 GROUP BY c;
 
 EXPLAIN (COSTS OFF)
 SELECT c, sum(a) FROM pagg_tab WHERE c = 'x' GROUP BY c;
-           QUERY PLAN           
---------------------------------
+             QUERY PLAN             
+------------------------------------
  GroupAggregate
    ->  Result
          One-Time Filter: false
-(3 rows)
+         Replaces: Scan on pagg_tab
+(4 rows)
 
 SELECT c, sum(a) FROM pagg_tab WHERE c = 'x' GROUP BY c;
  c | sum 
@@ -804,15 +806,16 @@ SELECT a.x, b.y, count(*) FROM (SELECT * FROM pagg_tab1 WHERE x < 20) a FULL JOI
 -- Empty join relation because of empty outer side, no partitionwise agg plan
 EXPLAIN (COSTS OFF)
 SELECT a.x, a.y, count(*) FROM (SELECT * FROM pagg_tab1 WHERE x = 1 AND x = 2) a LEFT JOIN pagg_tab2 b ON a.x = b.y GROUP BY a.x, a.y ORDER BY 1, 2;
-              QUERY PLAN              
---------------------------------------
+                  QUERY PLAN                  
+----------------------------------------------
  GroupAggregate
    Group Key: pagg_tab1.y
    ->  Sort
          Sort Key: pagg_tab1.y
          ->  Result
                One-Time Filter: false
-(6 rows)
+               Replaces: Join on b, pagg_tab1
+(7 rows)
 
 SELECT a.x, a.y, count(*) FROM (SELECT * FROM pagg_tab1 WHERE x = 1 AND x = 2) a LEFT JOIN pagg_tab2 b ON a.x = b.y GROUP BY a.x, a.y ORDER BY 1, 2;
  x | y | count 
diff --git a/src/test/regress/expected/partition_join.out b/src/test/regress/expected/partition_join.out
index 24e06845f92..0c495907eca 100644
--- a/src/test/regress/expected/partition_join.out
+++ b/src/test/regress/expected/partition_join.out
@@ -1609,19 +1609,21 @@ SELECT avg(t1.a), avg(t2.b), avg(t3.a + t3.b), t1.c, t2.c, t3.c FROM plt1 t1, pl
 -- joins where one of the relations is proven empty
 EXPLAIN (COSTS OFF)
 SELECT t1.a, t1.c, t2.b, t2.c FROM prt1 t1, prt2 t2 WHERE t1.a = t2.b AND t1.a = 1 AND t1.a = 2;
-        QUERY PLAN        
---------------------------
+         QUERY PLAN         
+----------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Join on t1, t2
+(3 rows)
 
 EXPLAIN (COSTS OFF)
 SELECT t1.a, t1.c, t2.b, t2.c FROM (SELECT * FROM prt1 WHERE a = 1 AND a = 2) t1 LEFT JOIN prt2 t2 ON t1.a = t2.b;
-        QUERY PLAN        
---------------------------
+          QUERY PLAN          
+------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Join on t2, prt1
+(3 rows)
 
 EXPLAIN (COSTS OFF)
 SELECT t1.a, t1.c, t2.b, t2.c FROM (SELECT * FROM prt1 WHERE a = 1 AND a = 2) t1 RIGHT JOIN prt2 t2 ON t1.a = t2.b, prt1 t3 WHERE t2.b = t3.a;
@@ -1648,7 +1650,8 @@ SELECT t1.a, t1.c, t2.b, t2.c FROM (SELECT * FROM prt1 WHERE a = 1 AND a = 2) t1
    ->  Hash
          ->  Result
                One-Time Filter: false
-(21 rows)
+               Replaces: Scan on prt1
+(22 rows)
 
 EXPLAIN (COSTS OFF)
 SELECT t1.a, t1.c, t2.b, t2.c FROM (SELECT * FROM prt1 WHERE a = 1 AND a = 2) t1 FULL JOIN prt2 t2 ON t1.a = t2.b WHERE t2.a = 0 ORDER BY t1.a, t2.b;
@@ -1668,7 +1671,8 @@ SELECT t1.a, t1.c, t2.b, t2.c FROM (SELECT * FROM prt1 WHERE a = 1 AND a = 2) t1
          ->  Hash
                ->  Result
                      One-Time Filter: false
-(14 rows)
+                     Replaces: Scan on prt1
+(15 rows)
 
 --
 -- tests for hash partitioned tables.
@@ -2255,7 +2259,8 @@ SELECT t1.a, t1.c, t2.b, t2.c FROM (SELECT * FROM prt1_l WHERE a = 1 AND a = 2)
    ->  Hash
          ->  Result
                One-Time Filter: false
-(11 rows)
+               Replaces: Scan on prt1_l
+(12 rows)
 
 -- Test case to verify proper handling of subqueries in a partitioned delete.
 -- The weird-looking lateral join is just there to force creation of a
diff --git a/src/test/regress/expected/partition_prune.out b/src/test/regress/expected/partition_prune.out
index d1966cd7d82..86cecc3ed71 100644
--- a/src/test/regress/expected/partition_prune.out
+++ b/src/test/regress/expected/partition_prune.out
@@ -628,7 +628,8 @@ explain (costs off) select * from rlp3 where a = 20;   /* empty */
 --------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on rlp3
+(3 rows)
 
 -- redundant clauses are eliminated
 explain (costs off) select * from rlp where a > 1 and a = 10;	/* only default */
@@ -671,7 +672,8 @@ explain (costs off) select * from rlp where a = 1 and a = 3;	/* empty */
 --------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on rlp
+(3 rows)
 
 explain (costs off) select * from rlp where (a = 1 and a = 3) or (a > 1 and a = 15);
                             QUERY PLAN                             
@@ -1254,25 +1256,28 @@ select * from boolpart where a is not unknown;
 
 -- check that all partitions are pruned when faced with conflicting clauses
 explain (costs off) select * from boolpart where a is not unknown and a is unknown;
-        QUERY PLAN        
---------------------------
+          QUERY PLAN          
+------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on boolpart
+(3 rows)
 
 explain (costs off) select * from boolpart where a is false and a is unknown;
-        QUERY PLAN        
---------------------------
+          QUERY PLAN          
+------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on boolpart
+(3 rows)
 
 explain (costs off) select * from boolpart where a is true and a is unknown;
-        QUERY PLAN        
---------------------------
+          QUERY PLAN          
+------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on boolpart
+(3 rows)
 
 -- inverse boolean partitioning - a seemingly unlikely design, but we've got
 -- code for it, so we'd better test it.
@@ -1568,11 +1573,12 @@ explain (costs off) select * from coercepart where a = any ('{ab,null}');
 (2 rows)
 
 explain (costs off) select * from coercepart where a = any (null::text[]);
-        QUERY PLAN        
---------------------------
+           QUERY PLAN           
+--------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on coercepart
+(3 rows)
 
 explain (costs off) select * from coercepart where a = all ('{ab}');
                   QUERY PLAN                  
@@ -1582,25 +1588,28 @@ explain (costs off) select * from coercepart where a = all ('{ab}');
 (2 rows)
 
 explain (costs off) select * from coercepart where a = all ('{ab,bc}');
-        QUERY PLAN        
---------------------------
+           QUERY PLAN           
+--------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on coercepart
+(3 rows)
 
 explain (costs off) select * from coercepart where a = all ('{ab,null}');
-        QUERY PLAN        
---------------------------
+           QUERY PLAN           
+--------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on coercepart
+(3 rows)
 
 explain (costs off) select * from coercepart where a = all (null::text[]);
-        QUERY PLAN        
---------------------------
+           QUERY PLAN           
+--------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on coercepart
+(3 rows)
 
 drop table coercepart;
 CREATE TABLE part (a INT, b INT) PARTITION BY LIST (a);
@@ -1773,7 +1782,8 @@ explain (costs off) select * from lp where a <> 'a' and a is null;
 --------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on lp
+(3 rows)
 
 explain (costs off) select * from lp where (a <> 'a' and a <> 'd') or a is null;
                                   QUERY PLAN                                  
@@ -1866,22 +1876,24 @@ create table lparted_by_int2 (a smallint) partition by list (a);
 create table lparted_by_int2_1 partition of lparted_by_int2 for values in (1);
 create table lparted_by_int2_16384 partition of lparted_by_int2 for values in (16384);
 explain (costs off) select * from lparted_by_int2 where a = 100_000_000_000_000;
-        QUERY PLAN        
---------------------------
+             QUERY PLAN              
+-------------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on lparted_by_int2
+(3 rows)
 
 create table rparted_by_int2 (a smallint) partition by range (a);
 create table rparted_by_int2_1 partition of rparted_by_int2 for values from (1) to (10);
 create table rparted_by_int2_16384 partition of rparted_by_int2 for values from (10) to (16384);
 -- all partitions pruned
 explain (costs off) select * from rparted_by_int2 where a > 100_000_000_000_000;
-        QUERY PLAN        
---------------------------
+             QUERY PLAN              
+-------------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on rparted_by_int2
+(3 rows)
 
 create table rparted_by_int2_maxvalue partition of rparted_by_int2 for values from (16384) to (maxvalue);
 -- all partitions but rparted_by_int2_maxvalue pruned
@@ -2132,7 +2144,8 @@ explain (costs off) select * from hp where a = 1 and b = 'abcde' and
 --------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on hp
+(3 rows)
 
 --
 -- Test runtime partition pruning
@@ -3390,11 +3403,12 @@ select * from stable_qual_pruning where a < '2000-02-01'::timestamptz;
 explain (analyze, costs off, summary off, timing off, buffers off)
 select * from stable_qual_pruning
   where a = any(array['2010-02-01', '2020-01-01']::timestamp[]);
-            QUERY PLAN             
------------------------------------
+               QUERY PLAN                
+-----------------------------------------
  Result (actual rows=0.00 loops=1)
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on stable_qual_pruning
+(3 rows)
 
 explain (analyze, costs off, summary off, timing off, buffers off)
 select * from stable_qual_pruning
@@ -3642,6 +3656,7 @@ explain (analyze, costs off, summary off, timing off, buffers off) select * from
    Sort Key: ma_test.b
    InitPlan 2
      ->  Result (actual rows=1.00 loops=1)
+           Replaces: Aggregate
            InitPlan 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)
@@ -3656,7 +3671,7 @@ explain (analyze, costs off, summary off, timing off, buffers off) select * from
    ->  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)
          Index Searches: 1
-(18 rows)
+(19 rows)
 
 reset enable_seqscan;
 reset enable_sort;
@@ -3678,11 +3693,12 @@ explain (costs off) select * from pp_arrpart where a = '{1}';
 (2 rows)
 
 explain (costs off) select * from pp_arrpart where a = '{1, 2}';
-        QUERY PLAN        
---------------------------
+           QUERY PLAN           
+--------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on pp_arrpart
+(3 rows)
 
 explain (costs off) select * from pp_arrpart where a in ('{4, 5}', '{1}');
                               QUERY PLAN                              
@@ -3764,11 +3780,12 @@ explain (costs off) select * from pp_enumpart where a = 'blue';
 (2 rows)
 
 explain (costs off) select * from pp_enumpart where a = 'black';
-        QUERY PLAN        
---------------------------
+           QUERY PLAN            
+---------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on pp_enumpart
+(3 rows)
 
 drop table pp_enumpart;
 drop type pp_colors;
@@ -3785,11 +3802,12 @@ explain (costs off) select * from pp_recpart where a = '(1,1)'::pp_rectype;
 (2 rows)
 
 explain (costs off) select * from pp_recpart where a = '(1,2)'::pp_rectype;
-        QUERY PLAN        
---------------------------
+           QUERY PLAN           
+--------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on pp_recpart
+(3 rows)
 
 drop table pp_recpart;
 drop type pp_rectype;
@@ -3805,11 +3823,12 @@ explain (costs off) select * from pp_intrangepart where a = '[1,2]'::int4range;
 (2 rows)
 
 explain (costs off) select * from pp_intrangepart where a = '(1,2)'::int4range;
-        QUERY PLAN        
---------------------------
+             QUERY PLAN              
+-------------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on pp_intrangepart
+(3 rows)
 
 drop table pp_intrangepart;
 --
@@ -4125,19 +4144,21 @@ explain (costs off) update listp1 set a = 1 where a = 2;
 -- constraint exclusion enabled
 set constraint_exclusion to 'on';
 explain (costs off) select * from listp1 where a = 2;
-        QUERY PLAN        
---------------------------
+         QUERY PLAN         
+----------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on listp1
+(3 rows)
 
 explain (costs off) update listp1 set a = 1 where a = 2;
-           QUERY PLAN           
---------------------------------
+            QUERY PLAN            
+----------------------------------
  Update on listp1
    ->  Result
          One-Time Filter: false
-(3 rows)
+         Replaces: Scan on listp1
+(4 rows)
 
 reset constraint_exclusion;
 reset enable_partition_pruning;
@@ -4524,18 +4545,20 @@ create table hp_contradict_test (a int, b int) partition by hash (a part_test_in
 create table hp_contradict_test_p1 partition of hp_contradict_test for values with (modulus 2, remainder 0);
 create table hp_contradict_test_p2 partition of hp_contradict_test for values with (modulus 2, remainder 1);
 explain (costs off) select * from hp_contradict_test where a is null and a === 1 and b === 1;
-        QUERY PLAN        
---------------------------
+               QUERY PLAN               
+----------------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on hp_contradict_test
+(3 rows)
 
 explain (costs off) select * from hp_contradict_test where a === 1 and b === 1 and a is null;
-        QUERY PLAN        
---------------------------
+               QUERY PLAN               
+----------------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on hp_contradict_test
+(3 rows)
 
 drop table hp_contradict_test;
 drop operator class part_test_int4_ops2 using hash;
diff --git a/src/test/regress/expected/predicate.out b/src/test/regress/expected/predicate.out
index 59bfe33bb1c..4ee6f4a52ea 100644
--- a/src/test/regress/expected/predicate.out
+++ b/src/test/regress/expected/predicate.out
@@ -37,7 +37,8 @@ SELECT * FROM pred_tab t WHERE t.a IS NULL;
 --------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on t
+(3 rows)
 
 -- Ensure the IS_NULL qual is not reduced to constant-FALSE on nullable
 -- columns
@@ -78,7 +79,8 @@ SELECT * FROM pred_tab t WHERE t.a IS NULL OR t.c IS NULL;
 --------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on t
+(3 rows)
 
 -- Ensure the OR clause is not reduced to constant-FALSE when not all branches
 -- are provably false
@@ -140,7 +142,8 @@ SELECT * FROM pred_tab t1
    ->  Seq Scan on pred_tab t1
    ->  Result
          One-Time Filter: false
-(5 rows)
+         Replaces: Scan on t2
+(6 rows)
 
 -- Ensure the IS_NULL qual is not reduced to constant-FALSE when the column is
 -- nullable by an outer join
@@ -210,7 +213,8 @@ SELECT * FROM pred_tab t1
    ->  Seq Scan on pred_tab t1
    ->  Result
          One-Time Filter: false
-(5 rows)
+         Replaces: Scan on t2
+(6 rows)
 
 -- Ensure the OR clause is not reduced to constant-FALSE when a column is
 -- made nullable from an outer join
@@ -267,17 +271,18 @@ SELECT * FROM pred_tab t1
     LEFT JOIN pred_tab t2 ON EXISTS
         (SELECT 1 FROM pred_tab t3, pred_tab t4, pred_tab t5, pred_tab t6
          WHERE t1.a = t3.a AND t6.a IS NULL);
-             QUERY PLAN              
--------------------------------------
+                 QUERY PLAN                 
+--------------------------------------------
  Nested Loop Left Join
    Join Filter: (InitPlan 1).col1
    InitPlan 1
      ->  Result
            One-Time Filter: false
+           Replaces: Join on t3, t4, t5, t6
    ->  Seq Scan on pred_tab t1
    ->  Materialize
          ->  Seq Scan on pred_tab t2
-(8 rows)
+(9 rows)
 
 DROP TABLE pred_tab;
 -- Validate we handle IS NULL and IS NOT NULL quals correctly with inheritance
diff --git a/src/test/regress/expected/rowsecurity.out b/src/test/regress/expected/rowsecurity.out
index 8c879509313..e7fe3317638 100644
--- a/src/test/regress/expected/rowsecurity.out
+++ b/src/test/regress/expected/rowsecurity.out
@@ -3607,7 +3607,8 @@ EXPLAIN (COSTS OFF) SELECT * FROM t1;
 --------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on t1
+(3 rows)
 
 SET SESSION AUTHORIZATION regress_rls_bob;
 SELECT * FROM t1;
@@ -3620,7 +3621,8 @@ EXPLAIN (COSTS OFF) SELECT * FROM t1;
 --------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on t1
+(3 rows)
 
 --
 -- COPY TO/FROM
@@ -4524,11 +4526,12 @@ SELECT * FROM rls_tbl WHERE a <<< 1000;
 (0 rows)
 
 EXPLAIN (COSTS OFF) SELECT * FROM rls_tbl WHERE a <<< 1000 or a <<< 900;
-        QUERY PLAN        
---------------------------
+         QUERY PLAN          
+-----------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on rls_tbl
+(3 rows)
 
 RESET SESSION AUTHORIZATION;
 CREATE TABLE rls_child_tbl () INHERITS (rls_tbl);
diff --git a/src/test/regress/expected/rowtypes.out b/src/test/regress/expected/rowtypes.out
index 9168979a620..328b1e142c0 100644
--- a/src/test/regress/expected/rowtypes.out
+++ b/src/test/regress/expected/rowtypes.out
@@ -1324,7 +1324,8 @@ where false;
  Result
    Output: (a).f1, (a).f2
    One-Time Filter: false
-(3 rows)
+   Replaces: Scan on ss
+(4 rows)
 
 explain (verbose, costs off)
 with cte(c) as materialized (select row(1, 2)),
@@ -1349,10 +1350,11 @@ where false;
  Result
    Output: (cte.c).f1
    One-Time Filter: false
+   Replaces: Scan on cte
    CTE cte
      ->  Result
            Output: '(1,2)'::record
-(6 rows)
+(7 rows)
 
 --
 -- Tests for component access / FieldSelect
diff --git a/src/test/regress/expected/select.out b/src/test/regress/expected/select.out
index bab0cc93ff5..d1736dac224 100644
--- a/src/test/regress/expected/select.out
+++ b/src/test/regress/expected/select.out
@@ -962,10 +962,11 @@ create table list_parted_tbl (a int,b int) partition by list (a);
 create table list_parted_tbl1 partition of list_parted_tbl
   for values in (1) partition by list(b);
 explain (costs off) select * from list_parted_tbl;
-        QUERY PLAN        
---------------------------
+             QUERY PLAN              
+-------------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on list_parted_tbl
+(3 rows)
 
 drop table list_parted_tbl;
diff --git a/src/test/regress/expected/subselect.out b/src/test/regress/expected/subselect.out
index 0563d0cd5a1..db061ee95b1 100644
--- a/src/test/regress/expected/subselect.out
+++ b/src/test/regress/expected/subselect.out
@@ -2851,12 +2851,13 @@ ON B.hundred in (SELECT min(c.hundred) FROM tenk2 C WHERE c.odd = b.odd);
                      ->  Subquery Scan on "ANY_subquery"
                            Filter: (b.hundred = "ANY_subquery".min)
                            ->  Result
+                                 Replaces: Aggregate
                                  InitPlan 1
                                    ->  Limit
                                          ->  Index Scan using tenk2_hundred on tenk2 c
                                                Index Cond: (hundred IS NOT NULL)
                                                Filter: (odd = b.odd)
-(16 rows)
+(17 rows)
 
 --
 -- Test VALUES to ARRAY (VtA) transformation
@@ -3020,7 +3021,8 @@ EXPLAIN (COSTS OFF) EXECUTE test(NULL, 3.14, NULL);
 --------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on onek
+(3 rows)
 
 EXPLAIN (COSTS OFF) EXECUTE test(NULL, 3.14, '-1.5');
                                                                      QUERY PLAN                                                                      
diff --git a/src/test/regress/expected/tsrf.out b/src/test/regress/expected/tsrf.out
index d47b5f6ec57..e73e68af971 100644
--- a/src/test/regress/expected/tsrf.out
+++ b/src/test/regress/expected/tsrf.out
@@ -92,7 +92,8 @@ SELECT unnest(ARRAY[1, 2]) FROM few WHERE false;
    Output: unnest('{1,2}'::integer[])
    ->  Result
          One-Time Filter: false
-(4 rows)
+         Replaces: Scan on few
+(5 rows)
 
 SELECT unnest(ARRAY[1, 2]) FROM few WHERE false;
  unnest 
@@ -108,7 +109,8 @@ SELECT * FROM few f1,
  Result
    Output: f1.id, f1.dataa, f1.datab, ss.unnest
    One-Time Filter: false
-(3 rows)
+   Replaces: Join on f1, ss
+(4 rows)
 
 SELECT * FROM few f1,
   (SELECT unnest(ARRAY[1,2]) FROM few f2 WHERE false OFFSET 0) ss;
-- 
2.39.5 (Apple Git-154)

v2-0002-Consider-a-Result-node-s-relids-in-ExplainPreScan.patchapplication/octet-stream; name=v2-0002-Consider-a-Result-node-s-relids-in-ExplainPreScan.patchDownload
From 501499399e5d9d71075be8af5df47aab4fa075fe Mon Sep 17 00:00:00 2001
From: Robert Haas <rhaas@postgresql.org>
Date: Mon, 21 Apr 2025 13:35:28 -0400
Subject: [PATCH v2 2/7] Consider a Result node's relids in ExplainPreScanNode.

Now that a Result node has a relids set, add the relids that it
carries the set accumulated by ExplainPreScanNode so that we
generate unique relation aliases for all of the referenced relations
when it calls select_rtable_names_for_explain. The effect of this
changes is that a few things get schema-qualified in the regression
test outputs that previously were not. In similar cases not involving
a Result node, we were already schema-qualifying, so this appears to
be an improvement.

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.
---
 src/backend/commands/explain.c                |  4 +++
 .../regress/expected/generated_virtual.out    |  8 ++---
 src/test/regress/expected/join.out            | 32 +++++++++----------
 src/test/regress/expected/partition_join.out  | 12 +++----
 4 files changed, 30 insertions(+), 26 deletions(-)

diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 42723c3a150..e8bd4c560dd 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -1230,6 +1230,10 @@ ExplainPreScanNode(PlanState *planstate, Bitmapset **rels_used)
 			*rels_used = bms_add_members(*rels_used,
 										 ((MergeAppend *) plan)->apprelids);
 			break;
+		case T_Result:
+			*rels_used = bms_add_members(*rels_used,
+										 ((Result *) plan)->relids);
+			break;
 		default:
 			break;
 	}
diff --git a/src/test/regress/expected/generated_virtual.out b/src/test/regress/expected/generated_virtual.out
index 8b25e77bb4d..70727623bca 100644
--- a/src/test/regress/expected/generated_virtual.out
+++ b/src/test/regress/expected/generated_virtual.out
@@ -1570,15 +1570,15 @@ where coalesce(t2.b, 1) = 2 or t1.a is null;
 -- Ensure that the generation expressions are wrapped into PHVs if needed
 explain (verbose, costs off)
 select t2.* from gtest32 t1 left join gtest32 t2 on false;
-                      QUERY PLAN                      
-------------------------------------------------------
+                          QUERY PLAN                           
+---------------------------------------------------------------
  Nested Loop Left Join
-   Output: a, (a * 2), (20), (COALESCE(a, 100)), e
+   Output: t2.a, (t2.a * 2), (20), (COALESCE(t2.a, 100)), t2.e
    Join Filter: false
    ->  Seq Scan on generated_virtual_tests.gtest32 t1
          Output: t1.a, t1.b, t1.c, t1.d, t1.e
    ->  Result
-         Output: a, e, 20, COALESCE(a, 100)
+         Output: t2.a, t2.e, 20, COALESCE(t2.a, 100)
          One-Time Filter: false
          Replaces: Scan on t2
 (9 rows)
diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out
index 76083b1ce57..96b5e891d97 100644
--- a/src/test/regress/expected/join.out
+++ b/src/test/regress/expected/join.out
@@ -4133,9 +4133,9 @@ select * from t t1
                         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)
@@ -4148,7 +4148,7 @@ select * from t t1
                      ->  Seq Scan on pg_temp.t t2
                            Output: t2.i2, 1
          ->  Result
-               Output: i3
+               Output: t3.i3
                One-Time Filter: false
                Replaces: Scan on t3
    ->  Memoize
@@ -6051,7 +6051,7 @@ on t1.q1 = t2.q1;
                  QUERY PLAN                 
 --------------------------------------------
  Hash Left Join
-   Hash Cond: (t1.q1 = q1)
+   Hash Cond: (t1.q1 = t2.q1)
    ->  Seq Scan on int8_tbl t1
    ->  Hash
          ->  Result
@@ -6068,7 +6068,7 @@ on t1.q1 = t2.q1;
                  QUERY PLAN                 
 --------------------------------------------
  Hash Left Join
-   Hash Cond: (t1.q1 = q1)
+   Hash Cond: (t1.q1 = t2.q1)
    ->  Seq Scan on int8_tbl t1
    ->  Hash
          ->  Result
@@ -6398,13 +6398,13 @@ SELECT q2 FROM
                       QUERY PLAN                      
 ------------------------------------------------------
  Nested Loop Left Join
-   Output: q2
+   Output: int8_tbl.q2
    Join Filter: NULL::boolean
    Filter: (('constant'::text) >= ('constant'::text))
    ->  Seq Scan on public.int4_tbl
          Output: int4_tbl.f1
    ->  Result
-         Output: q2, 'constant'::text
+         Output: int8_tbl.q2, 'constant'::text
          One-Time Filter: false
          Replaces: Scan on int8_tbl
 (10 rows)
@@ -6757,7 +6757,7 @@ where q1.x = q2.y;
 --------------------------
  Result
    One-Time Filter: false
-   Replaces: Scan on sj_1
+   Replaces: Scan on sj
 (3 rows)
 
 -- We can't use a cross-EC generated self join qual because of current logic of
@@ -8747,15 +8747,15 @@ select * from int4_tbl t1,
 explain (verbose, costs off)
 select * from int8_tbl i8 left join lateral
   (select *, i8.q2 from int4_tbl where false) ss on true;
-              QUERY PLAN              
---------------------------------------
+                  QUERY PLAN                  
+----------------------------------------------
  Nested Loop Left Join
-   Output: i8.q1, i8.q2, f1, (i8.q2)
+   Output: i8.q1, i8.q2, int4_tbl.f1, (i8.q2)
    Join Filter: false
    ->  Seq Scan on public.int8_tbl i8
          Output: i8.q1, i8.q2
    ->  Result
-         Output: f1, i8.q2
+         Output: int4_tbl.f1, i8.q2
          One-Time Filter: false
          Replaces: Scan on int4_tbl
 (9 rows)
@@ -8763,14 +8763,14 @@ select * from int8_tbl i8 left join lateral
 explain (verbose, costs off)
 select * from int8_tbl i8 left join lateral
   (select *, i8.q2 from int4_tbl i1, int4_tbl i2 where false) ss on true;
-               QUERY PLAN                
------------------------------------------
+                  QUERY PLAN                   
+-----------------------------------------------
  Nested Loop Left Join
-   Output: i8.q1, i8.q2, f1, f1, (i8.q2)
+   Output: i8.q1, i8.q2, i1.f1, i2.f1, (i8.q2)
    ->  Seq Scan on public.int8_tbl i8
          Output: i8.q1, i8.q2
    ->  Result
-         Output: f1, f1, i8.q2
+         Output: i1.f1, i2.f1, i8.q2
          One-Time Filter: false
          Replaces: Join on i1, i2
 (8 rows)
diff --git a/src/test/regress/expected/partition_join.out b/src/test/regress/expected/partition_join.out
index 0c495907eca..c94108eb118 100644
--- a/src/test/regress/expected/partition_join.out
+++ b/src/test/regress/expected/partition_join.out
@@ -1630,7 +1630,7 @@ SELECT t1.a, t1.c, t2.b, t2.c FROM (SELECT * FROM prt1 WHERE a = 1 AND a = 2) t1
                     QUERY PLAN                    
 --------------------------------------------------
  Hash Left Join
-   Hash Cond: (t2.b = a)
+   Hash Cond: (t2.b = prt1.a)
    ->  Append
          ->  Hash Join
                Hash Cond: (t3_1.a = t2_1.b)
@@ -1658,9 +1658,9 @@ SELECT t1.a, t1.c, t2.b, t2.c FROM (SELECT * FROM prt1 WHERE a = 1 AND a = 2) t1
                  QUERY PLAN                 
 --------------------------------------------
  Sort
-   Sort Key: a, t2.b
+   Sort Key: prt1.a, t2.b
    ->  Hash Left Join
-         Hash Cond: (t2.b = a)
+         Hash Cond: (t2.b = prt1.a)
          ->  Append
                ->  Seq Scan on prt2_p1 t2_1
                      Filter: (a = 0)
@@ -2246,10 +2246,10 @@ SELECT COUNT(*) FROM prt1_l t1 LEFT JOIN LATERAL
 -- join with one side empty
 EXPLAIN (COSTS OFF)
 SELECT t1.a, t1.c, t2.b, t2.c FROM (SELECT * FROM prt1_l WHERE a = 1 AND a = 2) t1 RIGHT JOIN prt2_l t2 ON t1.a = t2.b AND t1.b = t2.a AND t1.c = t2.c;
-                               QUERY PLAN                                
--------------------------------------------------------------------------
+                                          QUERY PLAN                                          
+----------------------------------------------------------------------------------------------
  Hash Left Join
-   Hash Cond: ((t2.b = a) AND (t2.a = b) AND ((t2.c)::text = (c)::text))
+   Hash Cond: ((t2.b = prt1_l.a) AND (t2.a = prt1_l.b) AND ((t2.c)::text = (prt1_l.c)::text))
    ->  Append
          ->  Seq Scan on prt2_l_p1 t2_1
          ->  Seq Scan on prt2_l_p2_p1 t2_2
-- 
2.39.5 (Apple Git-154)

v2-0007-Store-information-about-Append-node-consolidation.patchapplication/octet-stream; name=v2-0007-Store-information-about-Append-node-consolidation.patchDownload
From 5e7852722612318e0c92842c7f1fc8ec1d99c285 Mon Sep 17 00:00:00 2001
From: Robert Haas <rhaas@postgresql.org>
Date: Mon, 21 Jul 2025 15:49:06 -0400
Subject: [PATCH v2 7/7] Store information about Append node consolidation in
 the final plan.

When we build an AppendPath or MergeAppendPath, we sometimes pull all
child paths from a subordinate AppendPath or MergeAppendPath instead
of having one such path atop another. This results in the RTIs that
would have been associated with the subordinate path disappearing
from the final plan, making things difficult for code that wants
to scrutinize the final plan and extract information from it about
what happened during the planning process.

To avoid this, propagate the RTI sets that would have been present
in the 'apprelids' field of the subordinate Append or MergeAppend
nodes that would have been created into the surviving Append or
MergeAppend node, using a new 'child_append_relid_sets' field for
that purpose.

This commit also updates pg_overexplain to display these details.
---
 .../expected/pg_overexplain.out               |  4 +-
 contrib/pg_overexplain/pg_overexplain.c       | 56 +++++++++++
 src/backend/optimizer/path/allpaths.c         | 98 +++++++++++++++----
 src/backend/optimizer/path/joinrels.c         |  2 +-
 src/backend/optimizer/plan/createplan.c       |  2 +
 src/backend/optimizer/plan/planner.c          |  1 +
 src/backend/optimizer/prep/prepunion.c        |  5 +-
 src/backend/optimizer/util/pathnode.c         |  5 +
 src/include/nodes/pathnodes.h                 | 10 ++
 src/include/nodes/plannodes.h                 | 11 +++
 src/include/optimizer/pathnode.h              |  2 +
 11 files changed, 171 insertions(+), 25 deletions(-)

diff --git a/contrib/pg_overexplain/expected/pg_overexplain.out b/contrib/pg_overexplain/expected/pg_overexplain.out
index ca9a23ea61f..a377fb2571d 100644
--- a/contrib/pg_overexplain/expected/pg_overexplain.out
+++ b/contrib/pg_overexplain/expected/pg_overexplain.out
@@ -104,6 +104,7 @@ $$);
                Parallel Safe: true
                Plan Node ID: 2
                Append RTIs: 1
+               Child Append RTIs: none
                ->  Seq Scan on brassica vegetables_1
                      Disabled Nodes: 0
                      Parallel Safe: true
@@ -142,7 +143,7 @@ $$);
    Relation Kind: relation
    Relation Lock Mode: AccessShareLock
  Unprunable RTIs: 1 3 4
-(53 rows)
+(54 rows)
 
 -- Test a different output format.
 SELECT explain_filter($$
@@ -197,6 +198,7 @@ $$);
                <extParam>none</extParam>                            +
                <allParam>none</allParam>                            +
                <Append-RTIs>1</Append-RTIs>                         +
+               <Child-Append-RTIs>none</Child-Append-RTIs>          +
                <Subplans-Removed>0</Subplans-Removed>               +
                <Plans>                                              +
                  <Plan>                                             +
diff --git a/contrib/pg_overexplain/pg_overexplain.c b/contrib/pg_overexplain/pg_overexplain.c
index fa907fa472e..6538ffcafb0 100644
--- a/contrib/pg_overexplain/pg_overexplain.c
+++ b/contrib/pg_overexplain/pg_overexplain.c
@@ -54,6 +54,8 @@ static void overexplain_alias(const char *qlabel, Alias *alias,
 							  ExplainState *es);
 static void overexplain_bitmapset(const char *qlabel, Bitmapset *bms,
 								  ExplainState *es);
+static void overexplain_bitmapset_list(const char *qlabel, List *bms_list,
+									   ExplainState *es);
 static void overexplain_intlist(const char *qlabel, List *list,
 								ExplainState *es);
 
@@ -232,11 +234,17 @@ overexplain_per_node_hook(PlanState *planstate, List *ancestors,
 				overexplain_bitmapset("Append RTIs",
 									  ((Append *) plan)->apprelids,
 									  es);
+				overexplain_bitmapset_list("Child Append RTIs",
+										   ((Append *) plan)->child_append_relid_sets,
+										   es);
 				break;
 			case T_MergeAppend:
 				overexplain_bitmapset("Append RTIs",
 									  ((MergeAppend *) plan)->apprelids,
 									  es);
+				overexplain_bitmapset_list("Child Append RTIs",
+										   ((MergeAppend *) plan)->child_append_relid_sets,
+										   es);
 				break;
 			case T_Result:
 
@@ -815,6 +823,54 @@ overexplain_bitmapset(const char *qlabel, Bitmapset *bms, ExplainState *es)
 	pfree(buf.data);
 }
 
+/*
+ * Emit a text property describing the contents of a list of bitmapsets.
+ * If a bitmapset contains exactly 1 member, we just print an integer;
+ * otherwise, we surround the list of members by parentheses.
+ *
+ * If there are no bitmapsets in the list, we print the word "none".
+ */
+static void
+overexplain_bitmapset_list(const char *qlabel, List *bms_list,
+						   ExplainState *es)
+{
+	StringInfoData buf;
+
+	initStringInfo(&buf);
+
+	foreach_node(Bitmapset, bms, bms_list)
+	{
+		if (bms_membership(bms) == BMS_SINGLETON)
+			appendStringInfo(&buf, " %d", bms_singleton_member(bms));
+		else
+		{
+			int			x = -1;
+			bool		first = true;
+
+			appendStringInfoString(&buf, " (");
+			while ((x = bms_next_member(bms, x)) >= 0)
+			{
+				if (first)
+					first = false;
+				else
+					appendStringInfoChar(&buf, ' ');
+				appendStringInfo(&buf, "%d", x);
+			}
+			appendStringInfoChar(&buf, ')');
+		}
+	}
+
+	if (buf.len == 0)
+	{
+		ExplainPropertyText(qlabel, "none", es);
+		return;
+	}
+
+	Assert(buf.data[0] == ' ');
+	ExplainPropertyText(qlabel, buf.data + 1, es);
+	pfree(buf.data);
+}
+
 /*
  * Emit a text property describing the contents of a list of integers, OIDs,
  * or XIDs -- either a space-separated list of integer members, or the word
diff --git a/src/backend/optimizer/path/allpaths.c b/src/backend/optimizer/path/allpaths.c
index 593f5361b58..76b8c3fd7c6 100644
--- a/src/backend/optimizer/path/allpaths.c
+++ b/src/backend/optimizer/path/allpaths.c
@@ -122,8 +122,10 @@ static Path *get_cheapest_parameterized_child_path(PlannerInfo *root,
 												   Relids required_outer);
 static void accumulate_append_subpath(Path *path,
 									  List **subpaths,
-									  List **special_subpaths);
-static Path *get_singleton_append_subpath(Path *path);
+									  List **special_subpaths,
+									  List **child_append_relid_sets);
+static Path *get_singleton_append_subpath(Path *path,
+										  List **child_append_relid_sets);
 static void set_dummy_rel_pathlist(RelOptInfo *rel);
 static void set_subquery_pathlist(PlannerInfo *root, RelOptInfo *rel,
 								  Index rti, RangeTblEntry *rte);
@@ -1323,11 +1325,15 @@ add_paths_to_append_rel(PlannerInfo *root, RelOptInfo *rel,
 {
 	List	   *subpaths = NIL;
 	bool		subpaths_valid = true;
+	List	   *subpath_cars = NIL;
 	List	   *startup_subpaths = NIL;
 	bool		startup_subpaths_valid = true;
+	List	   *startup_subpath_cars = NIL;
 	List	   *partial_subpaths = NIL;
+	List	   *partial_subpath_cars = NIL;
 	List	   *pa_partial_subpaths = NIL;
 	List	   *pa_nonpartial_subpaths = NIL;
+	List	   *pa_subpath_cars = NIL;
 	bool		partial_subpaths_valid = true;
 	bool		pa_subpaths_valid;
 	List	   *all_child_pathkeys = NIL;
@@ -1360,7 +1366,7 @@ add_paths_to_append_rel(PlannerInfo *root, RelOptInfo *rel,
 		if (childrel->pathlist != NIL &&
 			childrel->cheapest_total_path->param_info == NULL)
 			accumulate_append_subpath(childrel->cheapest_total_path,
-									  &subpaths, NULL);
+									  &subpaths, NULL, &subpath_cars);
 		else
 			subpaths_valid = false;
 
@@ -1389,7 +1395,8 @@ add_paths_to_append_rel(PlannerInfo *root, RelOptInfo *rel,
 			Assert(cheapest_path->param_info == NULL);
 			accumulate_append_subpath(cheapest_path,
 									  &startup_subpaths,
-									  NULL);
+									  NULL,
+									  &startup_subpath_cars);
 		}
 		else
 			startup_subpaths_valid = false;
@@ -1400,7 +1407,8 @@ add_paths_to_append_rel(PlannerInfo *root, RelOptInfo *rel,
 		{
 			cheapest_partial_path = linitial(childrel->partial_pathlist);
 			accumulate_append_subpath(cheapest_partial_path,
-									  &partial_subpaths, NULL);
+									  &partial_subpaths, NULL,
+									  &partial_subpath_cars);
 		}
 		else
 			partial_subpaths_valid = false;
@@ -1429,7 +1437,8 @@ add_paths_to_append_rel(PlannerInfo *root, RelOptInfo *rel,
 				Assert(cheapest_partial_path != NULL);
 				accumulate_append_subpath(cheapest_partial_path,
 										  &pa_partial_subpaths,
-										  &pa_nonpartial_subpaths);
+										  &pa_nonpartial_subpaths,
+										  &pa_subpath_cars);
 			}
 			else
 			{
@@ -1448,7 +1457,8 @@ add_paths_to_append_rel(PlannerInfo *root, RelOptInfo *rel,
 				 */
 				accumulate_append_subpath(nppath,
 										  &pa_nonpartial_subpaths,
-										  NULL);
+										  NULL,
+										  &pa_subpath_cars);
 			}
 		}
 
@@ -1523,14 +1533,16 @@ add_paths_to_append_rel(PlannerInfo *root, RelOptInfo *rel,
 	 * if we have zero or one live subpath due to constraint exclusion.)
 	 */
 	if (subpaths_valid)
-		add_path(rel, (Path *) create_append_path(root, rel, subpaths, NIL,
+		add_path(rel, (Path *) create_append_path(root, rel, subpaths,
+												  NIL, subpath_cars,
 												  NIL, NULL, 0, false,
 												  -1));
 
 	/* build an AppendPath for the cheap startup paths, if valid */
 	if (startup_subpaths_valid)
 		add_path(rel, (Path *) create_append_path(root, rel, startup_subpaths,
-												  NIL, NIL, NULL, 0, false, -1));
+												  NIL, startup_subpath_cars,
+												  NIL, NULL, 0, false, -1));
 
 	/*
 	 * Consider an append of unordered, unparameterized partial paths.  Make
@@ -1571,6 +1583,7 @@ add_paths_to_append_rel(PlannerInfo *root, RelOptInfo *rel,
 
 		/* Generate a partial append path. */
 		appendpath = create_append_path(root, rel, NIL, partial_subpaths,
+										partial_subpath_cars,
 										NIL, NULL, parallel_workers,
 										enable_parallel_append,
 										-1);
@@ -1621,6 +1634,7 @@ add_paths_to_append_rel(PlannerInfo *root, RelOptInfo *rel,
 
 		appendpath = create_append_path(root, rel, pa_nonpartial_subpaths,
 										pa_partial_subpaths,
+										pa_subpath_cars,
 										NIL, NULL, parallel_workers, true,
 										partial_rows);
 		add_partial_path(rel, (Path *) appendpath);
@@ -1654,6 +1668,7 @@ add_paths_to_append_rel(PlannerInfo *root, RelOptInfo *rel,
 
 		/* Select the child paths for an Append with this parameterization */
 		subpaths = NIL;
+		subpath_cars = NIL;
 		subpaths_valid = true;
 		foreach(lcr, live_childrels)
 		{
@@ -1676,12 +1691,13 @@ add_paths_to_append_rel(PlannerInfo *root, RelOptInfo *rel,
 				subpaths_valid = false;
 				break;
 			}
-			accumulate_append_subpath(subpath, &subpaths, NULL);
+			accumulate_append_subpath(subpath, &subpaths, NULL,
+									  &subpath_cars);
 		}
 
 		if (subpaths_valid)
 			add_path(rel, (Path *)
-					 create_append_path(root, rel, subpaths, NIL,
+					 create_append_path(root, rel, subpaths, NIL, subpath_cars,
 										NIL, required_outer, 0, false,
 										-1));
 	}
@@ -1708,6 +1724,7 @@ add_paths_to_append_rel(PlannerInfo *root, RelOptInfo *rel,
 				continue;
 
 			appendpath = create_append_path(root, rel, NIL, list_make1(path),
+											list_make1(rel->relids),
 											NIL, NULL,
 											path->parallel_workers, true,
 											partial_rows);
@@ -1789,8 +1806,11 @@ generate_orderedappend_paths(PlannerInfo *root, RelOptInfo *rel,
 	{
 		List	   *pathkeys = (List *) lfirst(lcp);
 		List	   *startup_subpaths = NIL;
+		List	   *startup_subpath_cars = NIL;
 		List	   *total_subpaths = NIL;
+		List	   *total_subpath_cars = NIL;
 		List	   *fractional_subpaths = NIL;
+		List	   *fractional_subpath_cars = NIL;
 		bool		startup_neq_total = false;
 		bool		match_partition_order;
 		bool		match_partition_order_desc;
@@ -1942,16 +1962,23 @@ generate_orderedappend_paths(PlannerInfo *root, RelOptInfo *rel,
 				 * just a single subpath (and hence aren't doing anything
 				 * useful).
 				 */
-				cheapest_startup = get_singleton_append_subpath(cheapest_startup);
-				cheapest_total = get_singleton_append_subpath(cheapest_total);
+				cheapest_startup =
+					get_singleton_append_subpath(cheapest_startup,
+												 &startup_subpath_cars);
+				cheapest_total =
+					get_singleton_append_subpath(cheapest_total,
+												 &total_subpath_cars);
 
 				startup_subpaths = lappend(startup_subpaths, cheapest_startup);
 				total_subpaths = lappend(total_subpaths, cheapest_total);
 
 				if (cheapest_fractional)
 				{
-					cheapest_fractional = get_singleton_append_subpath(cheapest_fractional);
-					fractional_subpaths = lappend(fractional_subpaths, cheapest_fractional);
+					cheapest_fractional =
+						get_singleton_append_subpath(cheapest_fractional,
+													 &fractional_subpath_cars);
+					fractional_subpaths =
+						lappend(fractional_subpaths, cheapest_fractional);
 				}
 			}
 			else
@@ -1961,13 +1988,16 @@ generate_orderedappend_paths(PlannerInfo *root, RelOptInfo *rel,
 				 * child paths for the MergeAppend.
 				 */
 				accumulate_append_subpath(cheapest_startup,
-										  &startup_subpaths, NULL);
+										  &startup_subpaths, NULL,
+										  &startup_subpath_cars);
 				accumulate_append_subpath(cheapest_total,
-										  &total_subpaths, NULL);
+										  &total_subpaths, NULL,
+										  &total_subpath_cars);
 
 				if (cheapest_fractional)
 					accumulate_append_subpath(cheapest_fractional,
-											  &fractional_subpaths, NULL);
+											  &fractional_subpaths, NULL,
+											  &fractional_subpath_cars);
 			}
 		}
 
@@ -1979,6 +2009,7 @@ generate_orderedappend_paths(PlannerInfo *root, RelOptInfo *rel,
 													  rel,
 													  startup_subpaths,
 													  NIL,
+													  startup_subpath_cars,
 													  pathkeys,
 													  NULL,
 													  0,
@@ -1989,6 +2020,7 @@ generate_orderedappend_paths(PlannerInfo *root, RelOptInfo *rel,
 														  rel,
 														  total_subpaths,
 														  NIL,
+														  total_subpath_cars,
 														  pathkeys,
 														  NULL,
 														  0,
@@ -2000,6 +2032,7 @@ generate_orderedappend_paths(PlannerInfo *root, RelOptInfo *rel,
 														  rel,
 														  fractional_subpaths,
 														  NIL,
+														  fractional_subpath_cars,
 														  pathkeys,
 														  NULL,
 														  0,
@@ -2012,12 +2045,14 @@ generate_orderedappend_paths(PlannerInfo *root, RelOptInfo *rel,
 			add_path(rel, (Path *) create_merge_append_path(root,
 															rel,
 															startup_subpaths,
+															startup_subpath_cars,
 															pathkeys,
 															NULL));
 			if (startup_neq_total)
 				add_path(rel, (Path *) create_merge_append_path(root,
 																rel,
 																total_subpaths,
+																total_subpath_cars,
 																pathkeys,
 																NULL));
 
@@ -2025,6 +2060,7 @@ generate_orderedappend_paths(PlannerInfo *root, RelOptInfo *rel,
 				add_path(rel, (Path *) create_merge_append_path(root,
 																rel,
 																fractional_subpaths,
+																fractional_subpath_cars,
 																pathkeys,
 																NULL));
 		}
@@ -2127,7 +2163,8 @@ get_cheapest_parameterized_child_path(PlannerInfo *root, RelOptInfo *rel,
  * paths).
  */
 static void
-accumulate_append_subpath(Path *path, List **subpaths, List **special_subpaths)
+accumulate_append_subpath(Path *path, List **subpaths, List **special_subpaths,
+						  List **child_append_relid_sets)
 {
 	if (IsA(path, AppendPath))
 	{
@@ -2136,6 +2173,8 @@ accumulate_append_subpath(Path *path, List **subpaths, List **special_subpaths)
 		if (!apath->path.parallel_aware || apath->first_partial_path == 0)
 		{
 			*subpaths = list_concat(*subpaths, apath->subpaths);
+			*child_append_relid_sets =
+				lappend(*child_append_relid_sets, path->parent->relids);
 			return;
 		}
 		else if (special_subpaths != NULL)
@@ -2150,6 +2189,8 @@ accumulate_append_subpath(Path *path, List **subpaths, List **special_subpaths)
 												  apath->first_partial_path);
 			*special_subpaths = list_concat(*special_subpaths,
 											new_special_subpaths);
+			*child_append_relid_sets =
+				lappend(*child_append_relid_sets, path->parent->relids);
 			return;
 		}
 	}
@@ -2158,6 +2199,8 @@ accumulate_append_subpath(Path *path, List **subpaths, List **special_subpaths)
 		MergeAppendPath *mpath = (MergeAppendPath *) path;
 
 		*subpaths = list_concat(*subpaths, mpath->subpaths);
+		*child_append_relid_sets =
+			lappend(*child_append_relid_sets, path->parent->relids);
 		return;
 	}
 
@@ -2169,10 +2212,15 @@ accumulate_append_subpath(Path *path, List **subpaths, List **special_subpaths)
  *		Returns the single subpath of an Append/MergeAppend, or just
  *		return 'path' if it's not a single sub-path Append/MergeAppend.
  *
+ * As a side effect, whenever we return a single subpath rather than the
+ * original path, add the relid set for the original path to
+ * child_append_relid_sets, so that those relids don't entirely disappear
+ * from the final plan.
+ *
  * Note: 'path' must not be a parallel-aware path.
  */
 static Path *
-get_singleton_append_subpath(Path *path)
+get_singleton_append_subpath(Path *path, List **child_append_relid_sets)
 {
 	Assert(!path->parallel_aware);
 
@@ -2181,14 +2229,22 @@ get_singleton_append_subpath(Path *path)
 		AppendPath *apath = (AppendPath *) path;
 
 		if (list_length(apath->subpaths) == 1)
+		{
+			*child_append_relid_sets =
+				lappend(*child_append_relid_sets, path->parent->relids);
 			return (Path *) linitial(apath->subpaths);
+		}
 	}
 	else if (IsA(path, MergeAppendPath))
 	{
 		MergeAppendPath *mpath = (MergeAppendPath *) path;
 
 		if (list_length(mpath->subpaths) == 1)
+		{
+			*child_append_relid_sets =
+				lappend(*child_append_relid_sets, path->parent->relids);
 			return (Path *) linitial(mpath->subpaths);
+		}
 	}
 
 	return path;
@@ -2217,7 +2273,7 @@ set_dummy_rel_pathlist(RelOptInfo *rel)
 	rel->partial_pathlist = NIL;
 
 	/* Set up the dummy path */
-	add_path(rel, (Path *) create_append_path(NULL, rel, NIL, NIL,
+	add_path(rel, (Path *) create_append_path(NULL, rel, NIL, NIL, NIL,
 											  NIL, rel->lateral_relids,
 											  0, false, -1));
 
diff --git a/src/backend/optimizer/path/joinrels.c b/src/backend/optimizer/path/joinrels.c
index 535248aa525..6bddfc537d2 100644
--- a/src/backend/optimizer/path/joinrels.c
+++ b/src/backend/optimizer/path/joinrels.c
@@ -1342,7 +1342,7 @@ mark_dummy_rel(RelOptInfo *rel)
 
 	/* Set up the dummy path */
 	add_path(rel, (Path *) create_append_path(NULL, rel, NIL, NIL,
-											  NIL, rel->lateral_relids,
+											  NIL, NIL, rel->lateral_relids,
 											  0, false, -1));
 
 	/* Set or update cheapest_total_path and related fields */
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index c86fa5bc238..9f3d8c2bdb3 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -1282,6 +1282,7 @@ create_append_plan(PlannerInfo *root, AppendPath *best_path, int flags)
 	plan->plan.lefttree = NULL;
 	plan->plan.righttree = NULL;
 	plan->apprelids = rel->relids;
+	plan->child_append_relid_sets = best_path->child_append_relid_sets;
 
 	if (pathkeys != NIL)
 	{
@@ -1494,6 +1495,7 @@ create_merge_append_plan(PlannerInfo *root, MergeAppendPath *best_path,
 	plan->lefttree = NULL;
 	plan->righttree = NULL;
 	node->apprelids = rel->relids;
+	node->child_append_relid_sets = best_path->child_append_relid_sets;
 
 	/*
 	 * Compute sort column info, and adjust MergeAppend's tlist as needed.
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index 707f2054fd5..6878dce6117 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -3979,6 +3979,7 @@ create_degenerate_grouping_paths(PlannerInfo *root, RelOptInfo *input_rel,
 							   paths,
 							   NIL,
 							   NIL,
+							   NIL,
 							   NULL,
 							   0,
 							   false,
diff --git a/src/backend/optimizer/prep/prepunion.c b/src/backend/optimizer/prep/prepunion.c
index d55eb39e552..5b88514bb79 100644
--- a/src/backend/optimizer/prep/prepunion.c
+++ b/src/backend/optimizer/prep/prepunion.c
@@ -815,7 +815,7 @@ generate_union_paths(SetOperationStmt *op, PlannerInfo *root,
 	 * union child.
 	 */
 	apath = (Path *) create_append_path(root, result_rel, cheapest_pathlist,
-										NIL, NIL, NULL, 0, false, -1);
+										NIL, NIL, NIL, NULL, 0, false, -1);
 
 	/*
 	 * Estimate number of groups.  For now we just assume the output is unique
@@ -861,7 +861,7 @@ generate_union_paths(SetOperationStmt *op, PlannerInfo *root,
 
 		papath = (Path *)
 			create_append_path(root, result_rel, NIL, partial_pathlist,
-							   NIL, NULL, parallel_workers,
+							   NIL, NIL, NULL, parallel_workers,
 							   enable_parallel_append, -1);
 		gpath = (Path *)
 			create_gather_path(root, result_rel, papath,
@@ -969,6 +969,7 @@ generate_union_paths(SetOperationStmt *op, PlannerInfo *root,
 			path = (Path *) create_merge_append_path(root,
 													 result_rel,
 													 ordered_pathlist,
+													 NIL,
 													 union_pathkeys,
 													 NULL);
 
diff --git a/src/backend/optimizer/util/pathnode.c b/src/backend/optimizer/util/pathnode.c
index b0da28150d3..b4546c8842d 100644
--- a/src/backend/optimizer/util/pathnode.c
+++ b/src/backend/optimizer/util/pathnode.c
@@ -1298,6 +1298,7 @@ AppendPath *
 create_append_path(PlannerInfo *root,
 				   RelOptInfo *rel,
 				   List *subpaths, List *partial_subpaths,
+				   List *child_append_relid_sets,
 				   List *pathkeys, Relids required_outer,
 				   int parallel_workers, bool parallel_aware,
 				   double rows)
@@ -1307,6 +1308,7 @@ create_append_path(PlannerInfo *root,
 
 	Assert(!parallel_aware || parallel_workers > 0);
 
+	pathnode->child_append_relid_sets = child_append_relid_sets;
 	pathnode->path.pathtype = T_Append;
 	pathnode->path.parent = rel;
 	pathnode->path.pathtarget = rel->reltarget;
@@ -1469,6 +1471,7 @@ MergeAppendPath *
 create_merge_append_path(PlannerInfo *root,
 						 RelOptInfo *rel,
 						 List *subpaths,
+						 List *child_append_relid_sets,
 						 List *pathkeys,
 						 Relids required_outer)
 {
@@ -1484,6 +1487,7 @@ create_merge_append_path(PlannerInfo *root,
 	 */
 	Assert(bms_is_empty(rel->lateral_relids) && bms_is_empty(required_outer));
 
+	pathnode->child_append_relid_sets = child_append_relid_sets;
 	pathnode->path.pathtype = T_MergeAppend;
 	pathnode->path.parent = rel;
 	pathnode->path.pathtarget = rel->reltarget;
@@ -3951,6 +3955,7 @@ reparameterize_path(PlannerInfo *root, Path *path,
 				}
 				return (Path *)
 					create_append_path(root, rel, childpaths, partialpaths,
+									   apath->child_append_relid_sets,
 									   apath->path.pathkeys, required_outer,
 									   apath->path.parallel_workers,
 									   apath->path.parallel_aware,
diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h
index 684e02da063..6fb3a86598f 100644
--- a/src/include/nodes/pathnodes.h
+++ b/src/include/nodes/pathnodes.h
@@ -2099,6 +2099,12 @@ typedef struct CustomPath
  * For partial Append, 'subpaths' contains non-partial subpaths followed by
  * partial subpaths.
  *
+ * Whenever accumulate_append_subpath() allows us to consolidate multiple
+ * levels of Append paths are consolidated down to one, we store the RTI
+ * sets for the omitted paths in child_append_relid_sets. This is not necessary
+ * for planning or execution; we do it for the benefit of code that wants
+ * to inspect the final plan and understand how it came to be.
+ *
  * Note: it is possible for "subpaths" to contain only one, or even no,
  * elements.  These cases are optimized during create_append_plan.
  * In particular, an AppendPath with no subpaths is a "dummy" path that
@@ -2114,6 +2120,7 @@ typedef struct AppendPath
 	/* Index of first partial path in subpaths; list_length(subpaths) if none */
 	int			first_partial_path;
 	Cardinality limit_tuples;	/* hard limit on output tuples, or -1 */
+	List	   *child_append_relid_sets;
 } AppendPath;
 
 #define IS_DUMMY_APPEND(p) \
@@ -2130,12 +2137,15 @@ extern bool is_dummy_rel(RelOptInfo *rel);
 /*
  * MergeAppendPath represents a MergeAppend plan, ie, the merging of sorted
  * results from several member plans to produce similarly-sorted output.
+ *
+ * child_append_relid_sets has the same meaning here as for AppendPath.
  */
 typedef struct MergeAppendPath
 {
 	Path		path;
 	List	   *subpaths;		/* list of component Paths */
 	Cardinality limit_tuples;	/* hard limit on output tuples, or -1 */
+	List	   *child_append_relid_sets;
 } MergeAppendPath;
 
 /*
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index 497aec24876..19eceb56979 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -374,9 +374,16 @@ struct PartitionPruneInfo;		/* forward reference to struct below */
 typedef struct Append
 {
 	Plan		plan;
+
 	/* RTIs of appendrel(s) formed by this node */
 	Bitmapset  *apprelids;
+
+	/* sets of RTIs of appendrels consolidated into this node */
+	List	   *child_append_relid_sets;
+
+	/* plans to run */
 	List	   *appendplans;
+
 	/* # of asynchronous plans */
 	int			nasyncplans;
 
@@ -406,6 +413,10 @@ typedef struct MergeAppend
 	/* RTIs of appendrel(s) formed by this node */
 	Bitmapset  *apprelids;
 
+	/* sets of RTIs of appendrels consolidated into this node */
+	List	   *child_append_relid_sets;
+
+	/* plans to run */
 	List	   *mergeplans;
 
 	/* these fields are just like the sort-key info in struct Sort: */
diff --git a/src/include/optimizer/pathnode.h b/src/include/optimizer/pathnode.h
index 763cd25bb3c..5f43b7fd0cb 100644
--- a/src/include/optimizer/pathnode.h
+++ b/src/include/optimizer/pathnode.h
@@ -70,12 +70,14 @@ extern TidRangePath *create_tidrangescan_path(PlannerInfo *root,
 											  Relids required_outer);
 extern AppendPath *create_append_path(PlannerInfo *root, RelOptInfo *rel,
 									  List *subpaths, List *partial_subpaths,
+									  List *child_append_relid_sets,
 									  List *pathkeys, Relids required_outer,
 									  int parallel_workers, bool parallel_aware,
 									  double rows);
 extern MergeAppendPath *create_merge_append_path(PlannerInfo *root,
 												 RelOptInfo *rel,
 												 List *subpaths,
+												 List *child_append_relid_sets,
 												 List *pathkeys,
 												 Relids required_outer);
 extern GroupResultPath *create_group_result_path(PlannerInfo *root,
-- 
2.39.5 (Apple Git-154)

v2-0006-Store-information-about-elided-nodes-in-the-final.patchapplication/octet-stream; name=v2-0006-Store-information-about-elided-nodes-in-the-final.patchDownload
From c688df428ea72a8563073fd1050fd262b0391af8 Mon Sep 17 00:00:00 2001
From: Robert Haas <rhaas@postgresql.org>
Date: Tue, 22 Apr 2025 14:10:19 -0400
Subject: [PATCH v2 6/7] Store information about elided nodes in the final
 plan.

When setrefs.c removes a SubqueryScan, single-child Append, or
single-child MergeAppend from the final Plan tree, the RTI which
would have been scanned by the removed node no longer appears in
the final plan (the actual range table entry is still present,
but it's no longer referenced).

That's fine for the executor, but it can create difficulties for
code that wants to deduce from the final plan what choices were
made during the planing process. For example, a traversal of a
join tree in the final plan might never encounter the RTI of one
of the relationss in the join problem, and might instead encounter
a scan of a child RTI or even one from a different subquery level.

This patch adjusts things so that each time we elide a node during
setrefs processing, we record the plan_node_id of its single surviving
child, the type of the removed node, and the RTIs that the removed
node would have scanned. This information is recorded in a separate
list that can be ignored by the executor and examined only by code
that cares about these details.

This commit also updates pg_overexplain to display these details.
---
 .../expected/pg_overexplain.out               |  4 +-
 contrib/pg_overexplain/pg_overexplain.c       | 39 ++++++++++++++
 src/backend/optimizer/plan/planner.c          |  1 +
 src/backend/optimizer/plan/setrefs.c          | 52 ++++++++++++++++++-
 src/include/nodes/pathnodes.h                 |  3 ++
 src/include/nodes/plannodes.h                 | 17 ++++++
 src/tools/pgindent/typedefs.list              |  1 +
 7 files changed, 114 insertions(+), 3 deletions(-)

diff --git a/contrib/pg_overexplain/expected/pg_overexplain.out b/contrib/pg_overexplain/expected/pg_overexplain.out
index 55d34666d87..ca9a23ea61f 100644
--- a/contrib/pg_overexplain/expected/pg_overexplain.out
+++ b/contrib/pg_overexplain/expected/pg_overexplain.out
@@ -452,6 +452,8 @@ SELECT * FROM vegetables WHERE genus = 'daucus';
  Seq Scan on daucus vegetables
    Filter: (genus = 'daucus'::text)
    Scan RTI: 2
+   Elided Node Type: Append
+   Elided Node RTIs: 1
  RTI 1 (relation, inherited, in-from-clause):
    Eref: vegetables (id, name, genus)
    Relation: vegetables
@@ -465,7 +467,7 @@ SELECT * FROM vegetables WHERE genus = 'daucus';
    Relation Kind: relation
    Relation Lock Mode: AccessShareLock
  Unprunable RTIs: 1 2
-(16 rows)
+(18 rows)
 
 -- Also test a case that involves a write.
 EXPLAIN (RANGE_TABLE, COSTS OFF)
diff --git a/contrib/pg_overexplain/pg_overexplain.c b/contrib/pg_overexplain/pg_overexplain.c
index 5dc707d69e3..fa907fa472e 100644
--- a/contrib/pg_overexplain/pg_overexplain.c
+++ b/contrib/pg_overexplain/pg_overexplain.c
@@ -191,6 +191,8 @@ overexplain_per_node_hook(PlanState *planstate, List *ancestors,
 	 */
 	if (options->range_table)
 	{
+		bool		opened_elided_nodes = false;
+
 		switch (nodeTag(plan))
 		{
 			case T_SeqScan:
@@ -251,6 +253,43 @@ overexplain_per_node_hook(PlanState *planstate, List *ancestors,
 			default:
 				break;
 		}
+
+		foreach_node(ElidedNode, n, es->pstmt->elidedNodes)
+		{
+			char	   *elidednodetag;
+
+			if (n->plan_node_id != plan->plan_node_id)
+				continue;
+
+			if (!opened_elided_nodes)
+			{
+				ExplainOpenGroup("Elided Nodes", "Elided Nodes", false, es);
+				opened_elided_nodes = true;
+			}
+
+			switch (n->elided_type)
+			{
+				case T_Append:
+					elidednodetag = "Append";
+					break;
+				case T_MergeAppend:
+					elidednodetag = "MergeAppend";
+					break;
+				case T_SubqueryScan:
+					elidednodetag = "SubqueryScan";
+					break;
+				default:
+					elidednodetag = psprintf("%d", n->elided_type);
+					break;
+			}
+
+			ExplainOpenGroup("Elided Node", NULL, true, es);
+			ExplainPropertyText("Elided Node Type", elidednodetag, es);
+			overexplain_bitmapset("Elided Node RTIs", n->relids, es);
+			ExplainCloseGroup("Elided Node", NULL, true, es);
+		}
+		if (opened_elided_nodes)
+			ExplainCloseGroup("Elided Nodes", "Elided Nodes", false, es);
 	}
 }
 
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index 790b14382a4..707f2054fd5 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -590,6 +590,7 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions,
 	result->paramExecTypes = glob->paramExecTypes;
 	/* utilityStmt should be null, but we might as well copy it */
 	result->utilityStmt = parse->utilityStmt;
+	result->elidedNodes = glob->elidedNodes;
 	result->stmt_location = parse->stmt_location;
 	result->stmt_len = parse->stmt_len;
 
diff --git a/src/backend/optimizer/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.c
index eef43792aeb..5900458a0e1 100644
--- a/src/backend/optimizer/plan/setrefs.c
+++ b/src/backend/optimizer/plan/setrefs.c
@@ -211,6 +211,9 @@ static List *set_windowagg_runcondition_references(PlannerInfo *root,
 												   List *runcondition,
 												   Plan *plan);
 
+static void record_elided_node(PlannerGlobal *glob, int plan_node_id,
+							   NodeTag elided_type, Bitmapset *relids);
+
 
 /*****************************************************************************
  *
@@ -1441,10 +1444,17 @@ set_subqueryscan_references(PlannerInfo *root,
 
 	if (trivial_subqueryscan(plan))
 	{
+		Index		scanrelid;
+
 		/*
 		 * We can omit the SubqueryScan node and just pull up the subplan.
 		 */
 		result = clean_up_removed_plan_level((Plan *) plan, plan->subplan);
+
+		/* Remember that we removed a SubqueryScan */
+		scanrelid = plan->scan.scanrelid + rtoffset;
+		record_elided_node(root->glob, plan->subplan->plan_node_id,
+						   T_SubqueryScan, bms_make_singleton(scanrelid));
 	}
 	else
 	{
@@ -1872,7 +1882,17 @@ set_append_references(PlannerInfo *root,
 		Plan	   *p = (Plan *) linitial(aplan->appendplans);
 
 		if (p->parallel_aware == aplan->plan.parallel_aware)
-			return clean_up_removed_plan_level((Plan *) aplan, p);
+		{
+			Plan	   *result;
+
+			result = clean_up_removed_plan_level((Plan *) aplan, p);
+
+			/* Remember that we removed an Append */
+			record_elided_node(root->glob, p->plan_node_id, T_Append,
+							   offset_relid_set(aplan->apprelids, rtoffset));
+
+			return result;
+		}
 	}
 
 	/*
@@ -1940,7 +1960,17 @@ set_mergeappend_references(PlannerInfo *root,
 		Plan	   *p = (Plan *) linitial(mplan->mergeplans);
 
 		if (p->parallel_aware == mplan->plan.parallel_aware)
-			return clean_up_removed_plan_level((Plan *) mplan, p);
+		{
+			Plan	   *result;
+
+			result = clean_up_removed_plan_level((Plan *) mplan, p);
+
+			/* Remember that we removed a MergeAppend */
+			record_elided_node(root->glob, p->plan_node_id, T_MergeAppend,
+							   offset_relid_set(mplan->apprelids, rtoffset));
+
+			return result;
+		}
 	}
 
 	/*
@@ -3755,3 +3785,21 @@ extract_query_dependencies_walker(Node *node, PlannerInfo *context)
 	return expression_tree_walker(node, extract_query_dependencies_walker,
 								  context);
 }
+
+/*
+ * Record some details about a node removed from the plan during setrefs
+ * procesing, for the benefit of code trying to reconstruct planner decisions
+ * from examination of the final plan tree.
+ */
+static void
+record_elided_node(PlannerGlobal *glob, int plan_node_id,
+				   NodeTag elided_type, Bitmapset *relids)
+{
+	ElidedNode *n = makeNode(ElidedNode);
+
+	n->plan_node_id = plan_node_id;
+	n->elided_type = elided_type;
+	n->relids = relids;
+
+	glob->elidedNodes = lappend(glob->elidedNodes, n);
+}
diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h
index 4f8586f6591..684e02da063 100644
--- a/src/include/nodes/pathnodes.h
+++ b/src/include/nodes/pathnodes.h
@@ -159,6 +159,9 @@ typedef struct PlannerGlobal
 	/* type OIDs for PARAM_EXEC Params */
 	List	   *paramExecTypes;
 
+	/* info about nodes elided from the plan during setrefs processing */
+	List	   *elidedNodes;
+
 	/* highest PlaceHolderVar ID assigned */
 	Index		lastPHId;
 
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index 3f2d6fafc24..497aec24876 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -152,6 +152,9 @@ typedef struct PlannedStmt
 	/* non-null if this is utility stmt */
 	Node	   *utilityStmt;
 
+	/* info about nodes elided from the plan during setrefs processing */
+	List	   *elidedNodes;
+
 	/* statement location in source string (copied from Query) */
 	/* start location, or -1 if unknown */
 	ParseLoc	stmt_location;
@@ -1818,4 +1821,18 @@ typedef struct SubPlanRTInfo
 	bool		dummy;
 } SubPlanRTInfo;
 
+/*
+ * ElidedNode
+ *
+ * Information about nodes elided from the final plan tree: trivial subquery
+ * scans, and single-child Append and MergeAppend nodes.
+ */
+typedef struct ElidedNode
+{
+	NodeTag		type;
+	int			plan_node_id;
+	NodeTag		elided_type;
+	Bitmapset  *relids;
+} ElidedNode;
+
 #endif							/* PLANNODES_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8e4367dabaf..899551d5117 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -4348,3 +4348,4 @@ z_stream
 z_streamp
 zic_t
 SubPlanRTInfo
+ElidedNode
-- 
2.39.5 (Apple Git-154)

#11Bruce Momjian
bruce@momjian.us
In reply to: Robert Haas (#10)
Re: plan shape work

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.

#12Andrei Lepikhov
lepihov@gmail.com
In reply to: Robert Haas (#1)
Re: plan shape work

On 19/5/2025 20:01, Robert Haas wrote:

Hope you find this interesting. If you do, let me know what you think.Thanks for looking in this direction!

Since 2017, we designed features that should 'memorise' the experience
of previous executions, checking the PlanState tree and instrumentation
after execution.
The first dumb prototype you should know - AQO. The following, more
robust code is 'replan' [1]https://postgrespro.com/docs/enterprise/16/realtime-query-replanning. These two features utilise the core patch,
and I hope this patch can be removed after a few adjustments to the core.
In fact, 'replan' is used to stop execution in the middle (if the
time/memory usage limit is achieved), pass through the state, collect
valuable data and execute again. That's why using data for the
subsequent execution needs a matching query tree, and that data may be
irrelevant for a different set of constants.

The lessons learned during design and after some usage in real life:
1. Subplans should be uniquely identified during the planning. Like you
mentioned, two similar subplans on the same query level may be executed
differently and provide different row numbers to the 'knowledge base'. I
used the subquery_planner hack that generated a unique ID for each subplan.
2. Each node should be identified - I used a kind of signature at each
RelOptInfo node - just a hash generated by restrictinfos and underlying
signatures. This approach needs a create_plan hook to copy the signature
to the Plan nodes.
3. A great source of fluctuations is 'never executed' nodes - because
depending on the constant, the subtree may never be executed or produce
tons of tuples - I resolved it by just ignoring 'never executed'
results, no in-core changes needed.
4. A tree of paths may implement a single RelOptInfo. I have saved the
signature at the top of the Plan node for the corresponding RelOptInfo
and have never encountered any problems yet. It limits the use of
gathered data on cardinality/group number/peak memory consumption
somewhat, but not significantly.

I think the node extension fields and hooks, debated in the thread [2]/messages/by-id/CA+TgmoYxfg90rw13+JcYwn4dwSC+agw7o8-A+fA3M0fh96pg8w@mail.gmail.com,
may be enough to enable extensions to implement such a feature.

What's more, I personally prefer to have a hook that allows extension
checking of a condition during execution - it may be done inside the
ExecProcNode() by calling the node-specific hook, which an extension may
initialise in the PlanState structure before the start of execution.
Additionally, it would be beneficial to have a hook for error processing
at the top of the portal execution code - this is a key point for
managing query resources. If we need to stop and re-execute the query,
it is the only place where we can release all the resources assigned.
One more helpful thing - an option to postpone receiver sending data out
of the instance-side, even result headings. We may want to decide on the
query plan after some tuples are produced, and if something has already
been sent to the user, we can't just stop and rebuild the plan.

[1]: https://postgrespro.com/docs/enterprise/16/realtime-query-replanning
[2]: /messages/by-id/CA+TgmoYxfg90rw13+JcYwn4dwSC+agw7o8-A+fA3M0fh96pg8w@mail.gmail.com
/messages/by-id/CA+TgmoYxfg90rw13+JcYwn4dwSC+agw7o8-A+fA3M0fh96pg8w@mail.gmail.com

--
regards, Andrei Lepikhov

#13Alexandra Wang
alexandra.wang.oss@gmail.com
In reply to: Robert Haas (#10)
Re: plan shape work

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

#14Robert Haas
robertmhaas@gmail.com
In reply to: Alexandra Wang (#13)
7 attachment(s)
Re: plan shape work

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

Attachments:

v3-0003-Assert-that-RTIs-of-joined-rels-are-discoverable-.patchapplication/octet-stream; name=v3-0003-Assert-that-RTIs-of-joined-rels-are-discoverable-.patchDownload
From 0a1c5ea6286a6cc363645a31925ec487d2f3090f Mon Sep 17 00:00:00 2001
From: Robert Haas <rhaas@postgresql.org>
Date: Wed, 16 Apr 2025 08:32:00 -0400
Subject: [PATCH v3 3/7] Assert that RTIs of joined rels are discoverable from
 join plans.

Every RTI that appears in the joinrel's relid set should be findable
via the outer or inner plan, except for join RTIs which aren't
necessarily preserved in the final plan. This is a requirement if
we want to be able 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.

Note that this depends on the earlier commit to add a relids field to
Result nodes; without that change, a join tree involving two or more
Result nodes would be fundamentally ambiguous (and even a join tree
involving one could only be interpreted by guessing at its origin).
---
 src/backend/optimizer/plan/createplan.c | 168 ++++++++++++++++++++++++
 1 file changed, 168 insertions(+)

diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index 6f8fac85d48..e9237490eef 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -320,7 +320,14 @@ static ModifyTable *make_modifytable(PlannerInfo *root, Plan *subplan,
 									 int epqParam);
 static GatherMerge *create_gather_merge_plan(PlannerInfo *root,
 											 GatherMergePath *best_path);
+static void assert_join_preserves_scan_rtis(PlannerInfo *root, Path *best_path,
+											Plan *outer_plan,
+											Plan *inner_plan);
 
+#ifdef USE_ASSERT_CHECKING
+static Bitmapset *get_scanned_rtindexes(PlannerInfo *root, Plan *plan);
+static Bitmapset *remove_join_rtis(PlannerInfo *root, Bitmapset *bms);
+#endif
 
 /*
  * create_plan
@@ -4349,6 +4356,9 @@ create_nestloop_plan(PlannerInfo *root,
 
 	copy_generic_path_info(&join_plan->join.plan, &best_path->jpath.path);
 
+	assert_join_preserves_scan_rtis(root, &best_path->jpath.path, outer_plan,
+									inner_plan);
+
 	return join_plan;
 }
 
@@ -4703,6 +4713,9 @@ create_mergejoin_plan(PlannerInfo *root,
 	/* Costs of sort and material steps are included in path cost already */
 	copy_generic_path_info(&join_plan->join.plan, &best_path->jpath.path);
 
+	assert_join_preserves_scan_rtis(root, &best_path->jpath.path,
+									outer_plan, inner_plan);
+
 	return join_plan;
 }
 
@@ -4876,6 +4889,9 @@ create_hashjoin_plan(PlannerInfo *root,
 
 	copy_generic_path_info(&join_plan->join.plan, &best_path->jpath.path);
 
+	assert_join_preserves_scan_rtis(root, &best_path->jpath.path,
+									outer_plan, inner_plan);
+
 	return join_plan;
 }
 
@@ -7326,3 +7342,155 @@ is_projection_capable_plan(Plan *plan)
 	}
 	return true;
 }
+
+/*
+ * Check that the RTIs of the relations being joined at this level are
+ * properly reflected in the Plan tree.
+ *
+ * We expect to find every non-RTE_JOIN RTI from best_path->parent.relids
+ * mentioned in either the outer or inner subplan.
+ */
+static void
+assert_join_preserves_scan_rtis(PlannerInfo *root, Path *best_path,
+								Plan *outer_plan, Plan *inner_plan)
+{
+#ifdef USE_ASSERT_CHECKING
+	Bitmapset  *outer_scanrelids;
+	Bitmapset  *inner_scanrelids;
+	Bitmapset  *calculated_scanrelids;
+	Bitmapset  *filtered_joinrelids;
+
+	outer_scanrelids = get_scanned_rtindexes(root, outer_plan);
+	inner_scanrelids = get_scanned_rtindexes(root, inner_plan);
+	calculated_scanrelids = bms_union(outer_scanrelids, inner_scanrelids);
+	filtered_joinrelids = remove_join_rtis(root, best_path->parent->relids);
+
+	/* Any given scan RTI should appear on only one side or the other. */
+	Assert(!bms_overlap(inner_scanrelids, outer_scanrelids));
+
+	/*
+	 * If this assertion fails, it means that the set of range table indexes
+	 * that we found in the inner and outer path tree did not equal the set of
+	 * range table indexes that we found for this joinrel, even after
+	 * excluding RTE_JOIN range table indexes which are not expect to appear
+	 * in the plan tree.
+	 *
+	 * If this assertion fails due to the addition of a new executor node
+	 * type, you probably just need to update get_scanned_rtindexes to know
+	 * about the new node. See the header comments for that function for other
+	 * places to update at the same time.
+	 */
+	Assert(bms_equal(calculated_scanrelids, filtered_joinrelids));
+#endif
+}
+
+#ifdef USE_ASSERT_CHECKING
+/*
+ * Get the set of range table indexes that are scanned by a scan or join node,
+ * or any executor node that could appear beneath a scan or join node.
+ *
+ * We are uninterested in join RTIs here; we're only interested in which RTIs
+ * are scanned at or below a particular plan node, and only if that node can
+ * appear beneath a join.
+ *
+ * When adding new cases to this function, be sure to also update
+ * ExplainPreScanNode, ExplainNode, and overexplain_per_node_hook as
+ * appropriate.
+ */
+static Bitmapset *
+get_scanned_rtindexes(PlannerInfo *root, Plan *plan)
+{
+	switch (nodeTag(plan))
+	{
+		case T_SeqScan:
+		case T_SampleScan:
+		case T_IndexScan:
+		case T_IndexOnlyScan:
+		case T_BitmapHeapScan:
+		case T_TidScan:
+		case T_TidRangeScan:
+		case T_SubqueryScan:
+		case T_FunctionScan:
+		case T_TableFuncScan:
+		case T_ValuesScan:
+		case T_CteScan:
+		case T_NamedTuplestoreScan:
+		case T_WorkTableScan:
+			return bms_make_singleton(((Scan *) plan)->scanrelid);
+			break;
+		case T_ForeignScan:
+			return ((ForeignScan *) plan)->fs_base_relids;
+			break;
+		case T_CustomScan:
+			return ((CustomScan *) plan)->custom_relids;
+			break;
+		case T_Append:
+			return ((Append *) plan)->apprelids;
+			break;
+		case T_MergeAppend:
+			return ((MergeAppend *) plan)->apprelids;
+			break;
+		case T_Result:
+			if (plan->lefttree)
+				return get_scanned_rtindexes(root, plan->lefttree);
+			else
+				return remove_join_rtis(root, ((Result *) plan)->relids);
+			break;
+		case T_HashJoin:
+		case T_MergeJoin:
+		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);
+
+				return remove_join_rtis(root, combined_scanrelids);
+				break;
+			}
+		case T_Sort:
+		case T_IncrementalSort:
+		case T_Unique:
+		case T_Agg:
+		case T_Hash:
+		case T_Gather:
+		case T_GatherMerge:
+		case T_Material:
+		case T_Memoize:
+			return get_scanned_rtindexes(root, plan->lefttree);
+			break;
+		default:
+			break;
+	}
+
+	return NULL;
+}
+
+/*
+ * Return a new Bitmapset containing only those range table indexes from the
+ * input set that do not reference an RTE where rtekind == RTE_JOIN.
+ */
+static Bitmapset *
+remove_join_rtis(PlannerInfo *root, Bitmapset *bms)
+{
+	int			rti = -1;
+
+	bms = bms_copy(bms);
+
+	while ((rti = bms_next_member(bms, rti)) >= 0)
+	{
+		RangeTblEntry *rte = planner_rt_fetch(rti, root);
+
+		if (rte->rtekind == RTE_JOIN)
+			bms = bms_del_member(bms, rti);
+	}
+
+	return bms;
+}
+#endif
-- 
2.39.5 (Apple Git-154)

v3-0001-Keep-track-of-what-RTIs-a-Result-node-is-scanning.patchapplication/octet-stream; name=v3-0001-Keep-track-of-what-RTIs-a-Result-node-is-scanning.patchDownload
From 18cbb300717085232ad5cc3cc85b6edefd9952ef Mon Sep 17 00:00:00 2001
From: Robert Haas <rhaas@postgresql.org>
Date: Tue, 2 Sep 2025 14:12:13 -0400
Subject: [PATCH v3 1/7] Keep track of what RTIs a Result node is scanning.

Result nodes now include an RTI set, which is only non-NULL when they
have no subplan, and is taken from the relid set of the RelOptInfo
that the Result is generating.

Using that information, EXPLAIN now emits, where relevant, a "Replaces" line
that says whether it replaced a scan, a join, or an aggregate; and in the
former two cases, which relations were involved.

Likewise, pg_overexplain's EXPLAIN (RANGE_TABLE) now displays the RTIs
stored in a Result node just as it already does for other RTI-bearing
node types.
---
 contrib/file_fdw/expected/file_fdw.out        |   1 +
 .../expected/pg_overexplain.out               |   6 +-
 contrib/pg_overexplain/pg_overexplain.c       |  12 ++
 .../postgres_fdw/expected/postgres_fdw.out    |   9 +-
 src/backend/commands/explain.c                |  95 ++++++++++++
 src/backend/optimizer/plan/createplan.c       |  93 +++++++++---
 src/backend/optimizer/plan/setrefs.c          |   2 +
 src/include/nodes/plannodes.h                 |   9 ++
 src/test/regress/expected/aggregates.out      |  51 ++++---
 src/test/regress/expected/case.out            |  21 +--
 .../regress/expected/generated_virtual.out    |   3 +-
 src/test/regress/expected/groupingsets.out    |   3 +-
 src/test/regress/expected/inherit.out         |  28 ++--
 src/test/regress/expected/join.out            | 113 ++++++++------
 src/test/regress/expected/merge.out           |   3 +-
 .../regress/expected/partition_aggregate.out  |  21 +--
 src/test/regress/expected/partition_join.out  |  23 +--
 src/test/regress/expected/partition_prune.out | 141 ++++++++++--------
 src/test/regress/expected/predicate.out       |  33 ++--
 src/test/regress/expected/rowsecurity.out     |  13 +-
 src/test/regress/expected/rowtypes.out        |   6 +-
 src/test/regress/expected/select.out          |   7 +-
 src/test/regress/expected/subselect.out       |   6 +-
 src/test/regress/expected/tsrf.out            |   6 +-
 24 files changed, 491 insertions(+), 214 deletions(-)

diff --git a/contrib/file_fdw/expected/file_fdw.out b/contrib/file_fdw/expected/file_fdw.out
index 246e3d3e566..5e710d06de0 100644
--- a/contrib/file_fdw/expected/file_fdw.out
+++ b/contrib/file_fdw/expected/file_fdw.out
@@ -323,6 +323,7 @@ SELECT explain_filter('EXPLAIN (VERBOSE, COSTS FALSE) SELECT * FROM agg_csv WHER
  Result
    Output: a, b
    One-Time Filter: false
+   Replaces: Scan on agg_csv
 
 \t off
 SELECT * FROM agg_csv WHERE a < 0;
diff --git a/contrib/pg_overexplain/expected/pg_overexplain.out b/contrib/pg_overexplain/expected/pg_overexplain.out
index 6de02323d7c..55d34666d87 100644
--- a/contrib/pg_overexplain/expected/pg_overexplain.out
+++ b/contrib/pg_overexplain/expected/pg_overexplain.out
@@ -44,9 +44,10 @@ EXPLAIN (RANGE_TABLE) SELECT 1;
                 QUERY PLAN                
 ------------------------------------------
  Result  (cost=0.00..0.01 rows=1 width=4)
+   RTIs: 1
  RTI 1 (result):
    Eref: "*RESULT*" ()
-(3 rows)
+(4 rows)
 
 -- Create a partitioned table.
 CREATE TABLE vegetables (id serial, name text, genus text)
@@ -475,6 +476,7 @@ INSERT INTO vegetables (name, genus) VALUES ('broccoflower', 'brassica');
    Nominal RTI: 1
    Exclude Relation RTI: 0
    ->  Result
+         RTIs: 2
  RTI 1 (relation):
    Eref: vegetables (id, name, genus)
    Relation: vegetables
@@ -485,5 +487,5 @@ INSERT INTO vegetables (name, genus) VALUES ('broccoflower', 'brassica');
    Eref: "*RESULT*" ()
  Unprunable RTIs: 1
  Result RTIs: 1
-(14 rows)
+(15 rows)
 
diff --git a/contrib/pg_overexplain/pg_overexplain.c b/contrib/pg_overexplain/pg_overexplain.c
index de824566f8c..bd70b6d9d5e 100644
--- a/contrib/pg_overexplain/pg_overexplain.c
+++ b/contrib/pg_overexplain/pg_overexplain.c
@@ -236,6 +236,18 @@ overexplain_per_node_hook(PlanState *planstate, List *ancestors,
 									  ((MergeAppend *) plan)->apprelids,
 									  es);
 				break;
+			case T_Result:
+
+				/*
+				 * 'relids' is only meaningful when plan->lefttree is NULL,
+				 * but if somehow it ends up set when plan->lefttree is not
+				 * NULL, print it anyway.
+				 */
+				if (plan->lefttree == NULL ||
+					((Result *) plan)->relids != NULL)
+					overexplain_bitmapset("RTIs",
+										  ((Result *) plan)->relids,
+										  es);
 			default:
 				break;
 		}
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 78b8367d289..04aba992a88 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -7158,7 +7158,8 @@ EXPLAIN (VERBOSE, COSTS OFF) SELECT count(*) FROM ft1 WHERE c2 < 0;
    Output: count(*)
    ->  Result
          One-Time Filter: false
-(4 rows)
+         Replaces: Scan on ft1
+(5 rows)
 
 SELECT count(*) FROM ft1 WHERE c2 < 0;
  count 
@@ -7202,7 +7203,8 @@ EXPLAIN (VERBOSE, COSTS OFF) SELECT count(*) FROM ft1 WHERE c2 >= 0;
    Output: count(*)
    ->  Result
          One-Time Filter: false
-(4 rows)
+         Replaces: Scan on ft1
+(5 rows)
 
 SELECT count(*) FROM ft1 WHERE c2 >= 0;
  count 
@@ -8031,7 +8033,8 @@ DELETE FROM rem1 WHERE false;     -- currently can't be pushed down
    ->  Result
          Output: ctid
          One-Time Filter: false
-(5 rows)
+         Replaces: Scan on rem1
+(6 rows)
 
 -- Test with statement-level triggers
 CREATE TRIGGER trig_stmt_before
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 8345bc0264b..42723c3a150 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -147,6 +147,7 @@ static void show_buffer_usage(ExplainState *es, const BufferUsage *usage);
 static void show_wal_usage(ExplainState *es, const WalUsage *usage);
 static void show_memory_counters(ExplainState *es,
 								 const MemoryContextCounters *mem_counters);
+static void show_result_replacement_info(Result *result, ExplainState *es);
 static void ExplainIndexScanDetails(Oid indexid, ScanDirection indexorderdir,
 									ExplainState *es);
 static void ExplainScanTarget(Scan *plan, ExplainState *es);
@@ -2238,6 +2239,8 @@ ExplainNode(PlanState *planstate, List *ancestors,
 			if (plan->qual)
 				show_instrumentation_count("Rows Removed by Filter", 1,
 										   planstate, es);
+			if (plan->lefttree == NULL)
+				show_result_replacement_info(castNode(Result, plan), es);
 			break;
 		case T_ModifyTable:
 			show_modifytable_info(castNode(ModifyTableState, planstate), ancestors,
@@ -4750,6 +4753,98 @@ show_modifytable_info(ModifyTableState *mtstate, List *ancestors,
 		ExplainCloseGroup("Target Tables", "Target Tables", false, es);
 }
 
+/*
+ * Explain what a "Result" node replaced.
+ */
+static void
+show_result_replacement_info(Result *result, ExplainState *es)
+{
+	StringInfoData buf;
+	int			nrels = 0;
+	int			rti = -1;
+	bool		found_non_result = false;
+	char	   *s;
+
+	/*
+	 * If the Result node has a subplan, it didn't replace a scan, join, or
+	 * aggregate;
+	 */
+	Assert(result->plan.lefttree == NULL);
+
+	/*
+	 * If the relids set is empty, it didn't replace a scan or a join, so it
+	 * must have come from an upper rel. Currently, an aggregate seems to be
+	 * the only possibility. (We might want to add more details to the Result
+	 * node in the future to disambiguate; or to show the rels being
+	 * aggregated in the case of partitionwise aggregate.)
+	 */
+	if (bms_is_empty(result->relids))
+	{
+		ExplainPropertyText("Replaces", "Aggregate", es);
+		return;
+	}
+
+	/*
+	 * Build up a comma-separated list of user-facing names for the range
+	 * table entries in the relids set.
+	 */
+	initStringInfo(&buf);
+	while ((rti = bms_next_member(result->relids, rti)) >= 0)
+	{
+		RangeTblEntry *rte = rt_fetch(rti, es->rtable);
+		char	   *refname;
+
+		/*
+		 * add_outer_joins_to_relids will add join RTIs to the relids set of a
+		 * join; if that join is then replaced with a Result node, we may see
+		 * such RTIs here. But we want to completely ignore those here,
+		 * because "a LEFT JOIN b ON whatever" is a join between a and b, not
+		 * a join between a, b, and an unnamed join.
+		 */
+		if (rte->rtekind == RTE_JOIN)
+			continue;
+
+		/* Count the number of rels that aren't ignored completely. */
+		++nrels;
+
+		/* Work out what reference name to use and added it the string. */
+		refname = (char *) list_nth(es->rtable_names, rti - 1);
+		if (refname == NULL)
+			refname = rte->eref->aliasname;
+		if (buf.len > 0)
+			appendStringInfoString(&buf, ", ");
+		appendStringInfoString(&buf, refname);
+
+		/* Keep track of whether we see anything other than RTE_RESULT. */
+		if (rte->rtekind != RTE_RESULT)
+			found_non_result = true;
+	}
+
+	/*
+	 * If this Result node is because of a single RTE that is RTE_RESULT, it
+	 * is not really replacing anything at all, because there's no other
+	 * method for implementing a scan of such an RTE, so we don't display the
+	 * Replaces line in such cases.
+	 *
+	 * (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.)
+	 */
+	if (nrels <= 1 && !found_non_result)
+		return;
+
+	/*
+	 * We're replacing either a scan or a join, according to the number of
+	 * rels in the relids set.
+	 */
+	if (nrels > 1)
+		s = psprintf("Join on %s", buf.data);
+	else
+		s = psprintf("Scan on %s", buf.data);
+	ExplainPropertyText("Replaces", s, es);
+}
+
 /*
  * Explain the constituent plans of an Append, MergeAppend,
  * BitmapAnd, or BitmapOr node.
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index 6791cbeb416..6f8fac85d48 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -99,7 +99,8 @@ static Gather *create_gather_plan(PlannerInfo *root, GatherPath *best_path);
 static Plan *create_projection_plan(PlannerInfo *root,
 									ProjectionPath *best_path,
 									int flags);
-static Plan *inject_projection_plan(Plan *subplan, List *tlist, bool parallel_safe);
+static Plan *inject_projection_plan(Plan *subplan, List *tlist,
+									bool parallel_safe);
 static Sort *create_sort_plan(PlannerInfo *root, SortPath *best_path, int flags);
 static IncrementalSort *create_incrementalsort_plan(PlannerInfo *root,
 													IncrementalSortPath *best_path, int flags);
@@ -302,7 +303,10 @@ static SetOp *make_setop(SetOpCmd cmd, SetOpStrategy strategy,
 						 List *tlist, Plan *lefttree, Plan *righttree,
 						 List *groupList, long numGroups);
 static LockRows *make_lockrows(Plan *lefttree, List *rowMarks, int epqParam);
-static Result *make_result(List *tlist, Node *resconstantqual, Plan *subplan);
+static Result *make_gating_result(List *tlist, Node *resconstantqual,
+								  Plan *subplan);
+static Result *make_simple_result(List *tlist, Node *resconstantqual,
+								  Relids relids);
 static ProjectSet *make_project_set(List *tlist, Plan *subplan);
 static ModifyTable *make_modifytable(PlannerInfo *root, Plan *subplan,
 									 CmdType operation, bool canSetTag,
@@ -1014,6 +1018,7 @@ create_gating_plan(PlannerInfo *root, Path *path, Plan *plan,
 {
 	Plan	   *gplan;
 	Plan	   *splan;
+	Relids		relids = NULL;
 
 	Assert(gating_quals);
 
@@ -1021,7 +1026,9 @@ create_gating_plan(PlannerInfo *root, Path *path, Plan *plan,
 	 * We might have a trivial Result plan already.  Stacking one Result atop
 	 * another is silly, so if that applies, just discard the input plan.
 	 * (We're assuming its targetlist is uninteresting; it should be either
-	 * the same as the result of build_path_tlist, or a simplified version.)
+	 * the same as the result of build_path_tlist, or a simplified version.
+	 * However, we preserve the set of relids that it purports to scan and
+	 * attribute that to our replacement Result instead.)
 	 */
 	splan = plan;
 	if (IsA(plan, Result))
@@ -1030,7 +1037,10 @@ create_gating_plan(PlannerInfo *root, Path *path, Plan *plan,
 
 		if (rplan->plan.lefttree == NULL &&
 			rplan->resconstantqual == NULL)
+		{
 			splan = NULL;
+			relids = rplan->relids;
+		}
 	}
 
 	/*
@@ -1038,9 +1048,12 @@ create_gating_plan(PlannerInfo *root, Path *path, Plan *plan,
 	 * tlist; that's never a wrong choice, even if the parent node didn't ask
 	 * for CP_EXACT_TLIST.
 	 */
-	gplan = (Plan *) make_result(build_path_tlist(root, path),
-								 (Node *) gating_quals,
-								 splan);
+	if (splan == NULL)
+		gplan = (Plan *) make_simple_result(build_path_tlist(root, path),
+											(Node *) gating_quals, relids);
+	else
+		gplan = (Plan *) make_gating_result(build_path_tlist(root, path),
+											(Node *) gating_quals, splan);
 
 	/*
 	 * Notice that we don't change cost or size estimates when doing gating.
@@ -1235,10 +1248,10 @@ create_append_plan(PlannerInfo *root, AppendPath *best_path, int flags)
 		/* Generate a Result plan with constant-FALSE gating qual */
 		Plan	   *plan;
 
-		plan = (Plan *) make_result(tlist,
-									(Node *) list_make1(makeBoolConst(false,
-																	  false)),
-									NULL);
+		plan = (Plan *) make_simple_result(tlist,
+										   (Node *) list_make1(makeBoolConst(false,
+																			 false)),
+										   best_path->path.parent->relids);
 
 		copy_generic_path_info(plan, (Path *) best_path);
 
@@ -1636,7 +1649,8 @@ create_group_result_plan(PlannerInfo *root, GroupResultPath *best_path)
 	/* best_path->quals is just bare clauses */
 	quals = order_qual_clauses(root, best_path->quals);
 
-	plan = make_result(tlist, (Node *) quals, NULL);
+	plan = make_simple_result(tlist, (Node *) quals,
+							  best_path->path.parent->relids);
 
 	copy_generic_path_info(&plan->plan, (Path *) best_path);
 
@@ -1933,8 +1947,7 @@ create_projection_plan(PlannerInfo *root, ProjectionPath *best_path, int flags)
 	}
 	else
 	{
-		/* We need a Result node */
-		plan = (Plan *) make_result(tlist, NULL, subplan);
+		plan = (Plan *) make_gating_result(tlist, NULL, subplan);
 
 		copy_generic_path_info(plan, (Path *) best_path);
 	}
@@ -1958,7 +1971,7 @@ inject_projection_plan(Plan *subplan, List *tlist, bool parallel_safe)
 {
 	Plan	   *plan;
 
-	plan = (Plan *) make_result(tlist, NULL, subplan);
+	plan = (Plan *) make_gating_result(tlist, NULL, subplan);
 
 	/*
 	 * In principle, we should charge tlist eval cost plus cpu_per_tuple per
@@ -2436,7 +2449,8 @@ create_minmaxagg_plan(PlannerInfo *root, MinMaxAggPath *best_path)
 	/* Generate the output plan --- basically just a Result */
 	tlist = build_path_tlist(root, &best_path->path);
 
-	plan = make_result(tlist, (Node *) best_path->quals, NULL);
+	plan = make_simple_result(tlist, (Node *) best_path->quals,
+							  best_path->path.parent->relids);
 
 	copy_generic_path_info(&plan->plan, (Path *) best_path);
 
@@ -3887,7 +3901,8 @@ create_resultscan_plan(PlannerInfo *root, Path *best_path,
 			replace_nestloop_params(root, (Node *) scan_clauses);
 	}
 
-	scan_plan = make_result(tlist, (Node *) scan_clauses, NULL);
+	scan_plan = make_simple_result(tlist, (Node *) scan_clauses,
+								   best_path->parent->relids);
 
 	copy_generic_path_info(&scan_plan->plan, best_path);
 
@@ -6922,22 +6937,58 @@ make_limit(Plan *lefttree, Node *limitOffset, Node *limitCount,
 }
 
 /*
- * make_result
- *	  Build a Result plan node
+ * make_gating_result
+ *	  Build a Result plan node that performs projection of a subplan, and/or
+ *	  applies a one time filter (resconstantqual)
  */
 static Result *
-make_result(List *tlist,
-			Node *resconstantqual,
-			Plan *subplan)
+make_gating_result(List *tlist,
+				   Node *resconstantqual,
+				   Plan *subplan)
 {
 	Result	   *node = makeNode(Result);
 	Plan	   *plan = &node->plan;
 
+	Assert(subplan != NULL);
+
 	plan->targetlist = tlist;
 	plan->qual = NIL;
 	plan->lefttree = subplan;
 	plan->righttree = NULL;
 	node->resconstantqual = resconstantqual;
+	node->relids = NULL;
+
+	return node;
+}
+
+/*
+ * 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)
+ *
+ * 'relids' should be the relids set for this path's RelOptInfo. In essence,
+ * we're saying that this Result node generates all the tuples for that
+ * RelOptInfo. Note that the same consideration can never arise in
+ * make_gating_result(), because in that case the tuples are always coming
+ * from some subordinate node.
+ *
+ * NB: It would be nice to assert that the relids set is non-empty here,
+ * but it might be, because this could be a path for an upper rel.
+ */
+static Result *
+make_simple_result(List *tlist,
+				   Node *resconstantqual,
+				   Relids relids)
+{
+	Result	   *node = makeNode(Result);
+	Plan	   *plan = &node->plan;
+
+	plan->targetlist = tlist;
+	plan->qual = NIL;
+	plan->lefttree = NULL;
+	plan->righttree = NULL;
+	node->resconstantqual = resconstantqual;
+	node->relids = relids;
 
 	return node;
 }
diff --git a/src/backend/optimizer/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.c
index d706546f332..6950eff2c5b 100644
--- a/src/backend/optimizer/plan/setrefs.c
+++ b/src/backend/optimizer/plan/setrefs.c
@@ -1056,6 +1056,8 @@ set_plan_refs(PlannerInfo *root, Plan *plan, int rtoffset)
 				/* resconstantqual can't contain any subplan variable refs */
 				splan->resconstantqual =
 					fix_scan_expr(root, splan->resconstantqual, rtoffset, 1);
+				/* adjust the relids set */
+				splan->relids = offset_relid_set(splan->relids, rtoffset);
 			}
 			break;
 		case T_ProjectSet:
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index 29d7732d6a0..1abcf90be43 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -261,12 +261,21 @@ typedef struct Plan
  * If resconstantqual isn't NULL, it represents a one-time qualification
  * test (i.e., one that doesn't depend on any variables from the outer plan,
  * so needs to be evaluated only once).
+ *
+ * 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).
  * ----------------
  */
 typedef struct Result
 {
 	Plan		plan;
 	Node	   *resconstantqual;
+	Bitmapset  *relids;
 } Result;
 
 /* ----------------
diff --git a/src/test/regress/expected/aggregates.out b/src/test/regress/expected/aggregates.out
index c35288eecde..ce45de7e175 100644
--- a/src/test/regress/expected/aggregates.out
+++ b/src/test/regress/expected/aggregates.out
@@ -959,11 +959,12 @@ explain (costs off)
                          QUERY PLAN                         
 ------------------------------------------------------------
  Result
+   Replaces: Aggregate
    InitPlan 1
      ->  Limit
            ->  Index Only Scan using tenk1_unique1 on tenk1
                  Index Cond: (unique1 IS NOT NULL)
-(5 rows)
+(6 rows)
 
 select min(unique1) from tenk1;
  min 
@@ -976,11 +977,12 @@ explain (costs off)
                              QUERY PLAN                              
 ---------------------------------------------------------------------
  Result
+   Replaces: Aggregate
    InitPlan 1
      ->  Limit
            ->  Index Only Scan Backward using tenk1_unique1 on tenk1
                  Index Cond: (unique1 IS NOT NULL)
-(5 rows)
+(6 rows)
 
 select max(unique1) from tenk1;
  max  
@@ -993,11 +995,12 @@ explain (costs off)
                                QUERY PLAN                               
 ------------------------------------------------------------------------
  Result
+   Replaces: Aggregate
    InitPlan 1
      ->  Limit
            ->  Index Only Scan Backward using tenk1_unique1 on tenk1
                  Index Cond: ((unique1 IS NOT NULL) AND (unique1 < 42))
-(5 rows)
+(6 rows)
 
 select max(unique1) from tenk1 where unique1 < 42;
  max 
@@ -1010,11 +1013,12 @@ explain (costs off)
                                QUERY PLAN                               
 ------------------------------------------------------------------------
  Result
+   Replaces: Aggregate
    InitPlan 1
      ->  Limit
            ->  Index Only Scan Backward using tenk1_unique1 on tenk1
                  Index Cond: ((unique1 IS NOT NULL) AND (unique1 > 42))
-(5 rows)
+(6 rows)
 
 select max(unique1) from tenk1 where unique1 > 42;
  max  
@@ -1033,11 +1037,12 @@ explain (costs off)
                                 QUERY PLAN                                 
 ---------------------------------------------------------------------------
  Result
+   Replaces: Aggregate
    InitPlan 1
      ->  Limit
            ->  Index Only Scan Backward using tenk1_unique1 on tenk1
                  Index Cond: ((unique1 IS NOT NULL) AND (unique1 > 42000))
-(5 rows)
+(6 rows)
 
 select max(unique1) from tenk1 where unique1 > 42000;
  max 
@@ -1052,11 +1057,12 @@ explain (costs off)
                                  QUERY PLAN                                 
 ----------------------------------------------------------------------------
  Result
+   Replaces: Aggregate
    InitPlan 1
      ->  Limit
            ->  Index Only Scan Backward using tenk1_thous_tenthous on tenk1
                  Index Cond: ((thousand = 33) AND (tenthous IS NOT NULL))
-(5 rows)
+(6 rows)
 
 select max(tenthous) from tenk1 where thousand = 33;
  max  
@@ -1069,11 +1075,12 @@ explain (costs off)
                                 QUERY PLAN                                
 --------------------------------------------------------------------------
  Result
+   Replaces: Aggregate
    InitPlan 1
      ->  Limit
            ->  Index Only Scan using tenk1_thous_tenthous on tenk1
                  Index Cond: ((thousand = 33) AND (tenthous IS NOT NULL))
-(5 rows)
+(6 rows)
 
 select min(tenthous) from tenk1 where thousand = 33;
  min 
@@ -1090,11 +1097,12 @@ explain (costs off)
  Seq Scan on int4_tbl
    SubPlan 2
      ->  Result
+           Replaces: Aggregate
            InitPlan 1
              ->  Limit
                    ->  Index Only Scan using tenk1_unique1 on tenk1
                          Index Cond: ((unique1 IS NOT NULL) AND (unique1 > int4_tbl.f1))
-(7 rows)
+(8 rows)
 
 select f1, (select min(unique1) from tenk1 where unique1 > f1) AS gt
   from int4_tbl;
@@ -1119,7 +1127,8 @@ explain (costs off)
            ->  Index Only Scan Backward using tenk1_unique2 on tenk1
                  Index Cond: (unique2 IS NOT NULL)
    ->  Result
-(7 rows)
+         Replaces: Aggregate
+(8 rows)
 
 select distinct max(unique2) from tenk1;
  max  
@@ -1138,7 +1147,8 @@ explain (costs off)
            ->  Index Only Scan Backward using tenk1_unique2 on tenk1
                  Index Cond: (unique2 IS NOT NULL)
    ->  Result
-(7 rows)
+         Replaces: Aggregate
+(8 rows)
 
 select max(unique2) from tenk1 order by 1;
  max  
@@ -1157,7 +1167,8 @@ explain (costs off)
            ->  Index Only Scan Backward using tenk1_unique2 on tenk1
                  Index Cond: (unique2 IS NOT NULL)
    ->  Result
-(7 rows)
+         Replaces: Aggregate
+(8 rows)
 
 select max(unique2) from tenk1 order by max(unique2);
  max  
@@ -1176,7 +1187,8 @@ explain (costs off)
            ->  Index Only Scan Backward using tenk1_unique2 on tenk1
                  Index Cond: (unique2 IS NOT NULL)
    ->  Result
-(7 rows)
+         Replaces: Aggregate
+(8 rows)
 
 select max(unique2) from tenk1 order by max(unique2)+1;
  max  
@@ -1196,7 +1208,8 @@ explain (costs off)
                  Index Cond: (unique2 IS NOT NULL)
    ->  ProjectSet
          ->  Result
-(8 rows)
+               Replaces: Aggregate
+(9 rows)
 
 select max(unique2), generate_series(1,3) as g from tenk1 order by g desc;
  max  | g 
@@ -1212,12 +1225,13 @@ explain (costs off)
                      QUERY PLAN                     
 ----------------------------------------------------
  Result
+   Replaces: Aggregate
    InitPlan 1
      ->  Limit
            ->  Result
                  One-Time Filter: (100 IS NOT NULL)
                  ->  Seq Scan on tenk1
-(6 rows)
+(7 rows)
 
 select max(100) from tenk1;
  max 
@@ -1243,6 +1257,7 @@ explain (costs off)
                                          QUERY PLAN                                          
 ---------------------------------------------------------------------------------------------
  Result
+   Replaces: Aggregate
    InitPlan 1
      ->  Limit
            ->  Merge Append
@@ -1265,7 +1280,7 @@ explain (costs off)
                  ->  Index Only Scan using minmaxtest2i on minmaxtest2 minmaxtest_8
                        Index Cond: (f1 IS NOT NULL)
                  ->  Index Only Scan Backward using minmaxtest3i on minmaxtest3 minmaxtest_9
-(23 rows)
+(24 rows)
 
 select min(f1), max(f1) from minmaxtest;
  min | max 
@@ -1304,7 +1319,8 @@ explain (costs off)
    ->  Sort
          Sort Key: ((InitPlan 1).col1), ((InitPlan 2).col1)
          ->  Result
-(26 rows)
+               Replaces: Aggregate
+(27 rows)
 
 select distinct min(f1), max(f1) from minmaxtest;
  min | max 
@@ -1334,7 +1350,8 @@ explain (costs off)
                    ->  Seq Scan on int4_tbl t1
                          Filter: ((f1 IS NOT NULL) AND (f1 = t0.f1))
            ->  Result
-(9 rows)
+                 Replaces: Aggregate
+(10 rows)
 
 select f1, (select distinct min(t1.f1) from int4_tbl t1 where t1.f1 = t0.f1)
 from int4_tbl t0;
diff --git a/src/test/regress/expected/case.out b/src/test/regress/expected/case.out
index efee7fc4317..d74b0dd68cc 100644
--- a/src/test/regress/expected/case.out
+++ b/src/test/regress/expected/case.out
@@ -266,27 +266,30 @@ SELECT *
 -- Tests for constant subexpression simplification
 explain (costs off)
 SELECT * FROM CASE_TBL WHERE NULLIF(1, 2) = 2;
-        QUERY PLAN        
---------------------------
+          QUERY PLAN          
+------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on case_tbl
+(3 rows)
 
 explain (costs off)
 SELECT * FROM CASE_TBL WHERE NULLIF(1, 1) IS NOT NULL;
-        QUERY PLAN        
---------------------------
+          QUERY PLAN          
+------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on case_tbl
+(3 rows)
 
 explain (costs off)
 SELECT * FROM CASE_TBL WHERE NULLIF(1, null) = 2;
-        QUERY PLAN        
---------------------------
+          QUERY PLAN          
+------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on case_tbl
+(3 rows)
 
 --
 -- Examples of updates involving tables
diff --git a/src/test/regress/expected/generated_virtual.out b/src/test/regress/expected/generated_virtual.out
index aca6347babe..8b25e77bb4d 100644
--- a/src/test/regress/expected/generated_virtual.out
+++ b/src/test/regress/expected/generated_virtual.out
@@ -1580,7 +1580,8 @@ select t2.* from gtest32 t1 left join gtest32 t2 on false;
    ->  Result
          Output: a, e, 20, COALESCE(a, 100)
          One-Time Filter: false
-(8 rows)
+         Replaces: Scan on t2
+(9 rows)
 
 select t2.* from gtest32 t1 left join gtest32 t2 on false;
  a | b | c | d | e 
diff --git a/src/test/regress/expected/groupingsets.out b/src/test/regress/expected/groupingsets.out
index 35e4cb47ebe..07a37da79dd 100644
--- a/src/test/regress/expected/groupingsets.out
+++ b/src/test/regress/expected/groupingsets.out
@@ -591,11 +591,12 @@ explain (costs off)
                          QUERY PLAN                         
 ------------------------------------------------------------
  Result
+   Replaces: Aggregate
    InitPlan 1
      ->  Limit
            ->  Index Only Scan using tenk1_unique1 on tenk1
                  Index Cond: (unique1 IS NOT NULL)
-(5 rows)
+(6 rows)
 
 -- Views with GROUPING SET queries
 CREATE VIEW gstest_view AS select a, b, grouping(a,b), sum(c), count(*), max(c)
diff --git a/src/test/regress/expected/inherit.out b/src/test/regress/expected/inherit.out
index 5b5055babdc..e1225fb1add 100644
--- a/src/test/regress/expected/inherit.out
+++ b/src/test/regress/expected/inherit.out
@@ -581,7 +581,8 @@ update some_tab set a = a + 1 where false;
    ->  Result
          Output: (some_tab.a + 1), NULL::oid, NULL::tid
          One-Time Filter: false
-(4 rows)
+         Replaces: Scan on some_tab
+(5 rows)
 
 update some_tab set a = a + 1 where false;
 explain (verbose, costs off)
@@ -593,7 +594,8 @@ update some_tab set a = a + 1 where false returning b, a;
    ->  Result
          Output: (some_tab.a + 1), NULL::oid, NULL::tid
          One-Time Filter: false
-(5 rows)
+         Replaces: Scan on some_tab
+(6 rows)
 
 update some_tab set a = a + 1 where false returning b, a;
  b | a 
@@ -700,7 +702,8 @@ explain update parted_tab set a = 2 where false;
  Update on parted_tab  (cost=0.00..0.00 rows=0 width=0)
    ->  Result  (cost=0.00..0.00 rows=0 width=10)
          One-Time Filter: false
-(3 rows)
+         Replaces: Scan on parted_tab
+(4 rows)
 
 drop table parted_tab;
 -- Check UPDATE with multi-level partitioned inherited target
@@ -1756,6 +1759,7 @@ explain (verbose, costs off) select min(1-id) from matest0;
 ---------------------------------------------------------------------------------
  Result
    Output: (InitPlan 1).col1
+   Replaces: Aggregate
    InitPlan 1
      ->  Limit
            Output: ((1 - matest0.id))
@@ -1779,7 +1783,7 @@ explain (verbose, costs off) select min(1-id) from matest0;
                        ->  Index Scan using matest3i on public.matest3 matest0_4
                              Output: matest0_4.id, (1 - matest0_4.id)
                              Index Cond: ((1 - matest0_4.id) IS NOT NULL)
-(25 rows)
+(26 rows)
 
 select min(1-id) from matest0;
  min 
@@ -1943,6 +1947,7 @@ SELECT min(x) FROM
                              QUERY PLAN                             
 --------------------------------------------------------------------
  Result
+   Replaces: Aggregate
    InitPlan 1
      ->  Limit
            ->  Merge Append
@@ -1951,7 +1956,7 @@ SELECT min(x) FROM
                        Index Cond: (unique1 IS NOT NULL)
                  ->  Index Only Scan using tenk1_unique2 on tenk1 b
                        Index Cond: (unique2 IS NOT NULL)
-(9 rows)
+(10 rows)
 
 explain (costs off)
 SELECT min(y) FROM
@@ -1961,6 +1966,7 @@ SELECT min(y) FROM
                              QUERY PLAN                             
 --------------------------------------------------------------------
  Result
+   Replaces: Aggregate
    InitPlan 1
      ->  Limit
            ->  Merge Append
@@ -1969,7 +1975,7 @@ SELECT min(y) FROM
                        Index Cond: (unique1 IS NOT NULL)
                  ->  Index Only Scan using tenk1_unique2 on tenk1 b
                        Index Cond: (unique2 IS NOT NULL)
-(9 rows)
+(10 rows)
 
 -- XXX planner doesn't recognize that index on unique2 is sufficiently sorted
 explain (costs off)
@@ -3087,11 +3093,12 @@ explain (costs off) select * from range_list_parted where a between 3 and 23 and
 
 /* Should select no rows because range partition key cannot be null */
 explain (costs off) select * from range_list_parted where a is null;
-        QUERY PLAN        
---------------------------
+              QUERY PLAN               
+---------------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on range_list_parted
+(3 rows)
 
 /* Should only select rows from the null-accepting partition */
 explain (costs off) select * from range_list_parted where b is null;
@@ -3252,6 +3259,7 @@ explain (costs off) select min(a), max(a) from parted_minmax where b = '12345';
                                            QUERY PLAN                                           
 ------------------------------------------------------------------------------------------------
  Result
+   Replaces: Aggregate
    InitPlan 1
      ->  Limit
            ->  Index Only Scan using parted_minmax1i on parted_minmax1 parted_minmax
@@ -3260,7 +3268,7 @@ explain (costs off) select min(a), max(a) from parted_minmax where b = '12345';
      ->  Limit
            ->  Index Only Scan Backward using parted_minmax1i on parted_minmax1 parted_minmax_1
                  Index Cond: ((a IS NOT NULL) AND (b = '12345'::text))
-(9 rows)
+(10 rows)
 
 select min(a), max(a) from parted_minmax where b = '12345';
  min | max 
diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out
index 04079268b98..01ce1aa577c 100644
--- a/src/test/regress/expected/join.out
+++ b/src/test/regress/expected/join.out
@@ -2264,11 +2264,12 @@ explain (costs off)
 select aa, bb, unique1, unique1
   from tenk1 right join b_star on aa = unique1
   where bb < bb and bb is null;
-        QUERY PLAN        
---------------------------
+            QUERY PLAN             
+-----------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Join on tenk1, b_star
+(3 rows)
 
 select aa, bb, unique1, unique1
   from tenk1 right join b_star on aa = unique1
@@ -2386,11 +2387,12 @@ order by t1.unique1;
                            Index Cond: (unique1 < 10)
                SubPlan 2
                  ->  Result
+                       Replaces: Aggregate
                        InitPlan 1
                          ->  Limit
                                ->  Index Only Scan using tenk1_unique1 on tenk1
                                      Index Cond: ((unique1 IS NOT NULL) AND (unique1 = t2.unique1))
-(19 rows)
+(20 rows)
 
 -- Ensure we get the expected result
 select t1.unique1,t2.unique1 from tenk1 t1
@@ -2655,8 +2657,8 @@ select * from int8_tbl t1 left join
     (int8_tbl t2 left join int8_tbl t3 full join int8_tbl t4 on false on false)
     left join int8_tbl t5 on t2.q1 = t5.q1
 on t2.q2 = 123;
-                    QUERY PLAN                    
---------------------------------------------------
+                     QUERY PLAN                     
+----------------------------------------------------
  Nested Loop Left Join
    ->  Seq Scan on int8_tbl t1
    ->  Materialize
@@ -2668,8 +2670,9 @@ on t2.q2 = 123;
                            Filter: (q2 = 123)
                      ->  Result
                            One-Time Filter: false
+                           Replaces: Join on t3, t4
                ->  Seq Scan on int8_tbl t5
-(12 rows)
+(13 rows)
 
 explain (costs off)
 select * from int8_tbl t1
@@ -4165,6 +4168,7 @@ select * from t t1
          ->  Result
                Output: i3
                One-Time Filter: false
+               Replaces: Scan on t3
    ->  Memoize
          Output: t4.i4
          Cache Key: (1)
@@ -4172,7 +4176,7 @@ select * from t t1
          ->  Index Only Scan using t_pkey on pg_temp.t t4
                Output: t4.i4
                Index Cond: (t4.i4 > (1))
-(25 rows)
+(26 rows)
 
 explain (verbose, costs off)
 select * from
@@ -4363,7 +4367,8 @@ from int4_tbl t1
  Result
    Output: (current_database())::information_schema.sql_identifier, (c.relname)::information_schema.sql_identifier
    One-Time Filter: false
-(3 rows)
+   Replaces: Join on t1, t2, a, c, nc, t, nt, bt, nbt
+(4 rows)
 
 -- Test handling of qual pushdown to appendrel members with non-Var outputs
 explain (verbose, costs off)
@@ -4435,11 +4440,12 @@ select unique1 from tenk1, lateral f_immutable_int4(1) x where x = unique1;
 
 explain (costs off)
 select unique1 from tenk1, lateral f_immutable_int4(1) x where x in (select 17);
-        QUERY PLAN        
---------------------------
+        QUERY PLAN         
+---------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on tenk1
+(3 rows)
 
 explain (costs off)
 select unique1, x from tenk1 join f_immutable_int4(1) x on unique1 = x;
@@ -4485,11 +4491,12 @@ select unique1, x from tenk1 full join f_immutable_int4(1) x on unique1 = x;
 -- check that pullup of a const function allows further const-folding
 explain (costs off)
 select unique1 from tenk1, f_immutable_int4(1) x where x = 42;
-        QUERY PLAN        
---------------------------
+        QUERY PLAN         
+---------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on tenk1
+(3 rows)
 
 -- test inlining of immutable functions with PlaceHolderVars
 explain (costs off)
@@ -5346,7 +5353,8 @@ left join
    ->  Result
    ->  Result
          One-Time Filter: false
-(5 rows)
+         Replaces: Join on c, n
+(6 rows)
 
 -- check handling of apparently-commutable outer joins with non-commutable
 -- joins between them
@@ -5540,12 +5548,13 @@ select 1 from
      right join (select 1 as z) as ss2 on true)
   on false,
   lateral (select i4.f1, ss1.n from int8_tbl as i8 limit 1) as ss3;
-        QUERY PLAN        
---------------------------
+                  QUERY PLAN                   
+-----------------------------------------------
  Result
    Output: 1
    One-Time Filter: false
-(3 rows)
+   Replaces: Join on i4, ss3, x1, x2, *RESULT*
+(4 rows)
 
 select 1 from
   int4_tbl as i4
@@ -5574,11 +5583,12 @@ select 1 from t t1
        on false
      where t3.a = coalesce(t5.a,1)) as s2
   on true;
-        QUERY PLAN        
---------------------------
+                 QUERY PLAN                 
+--------------------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Join on t1, s1, t2, t3, t4, t5
+(3 rows)
 
 rollback;
 --
@@ -5975,14 +5985,15 @@ from int4_tbl as t1
                         inner join int8_tbl as t7 on null)
                on t5.q1 = t7.q2)
     on false;
-           QUERY PLAN           
---------------------------------
+                    QUERY PLAN                    
+--------------------------------------------------
  Nested Loop Left Join
    Join Filter: false
    ->  Seq Scan on int4_tbl t1
    ->  Result
          One-Time Filter: false
-(5 rows)
+         Replaces: Join on t2, t3, t4, t5, t7, t6
+(6 rows)
 
 -- variant with Var rather than PHV coming from t6
 explain (costs off)
@@ -5997,14 +6008,15 @@ from int4_tbl as t1
                         inner join int8_tbl as t7 on null)
                on t5.q1 = t7.q2)
     on false;
-           QUERY PLAN           
---------------------------------
+                    QUERY PLAN                    
+--------------------------------------------------
  Nested Loop Left Join
    Join Filter: false
    ->  Seq Scan on int4_tbl t1
    ->  Result
          One-Time Filter: false
-(5 rows)
+         Replaces: Join on t2, t3, t4, t5, t7, t6
+(6 rows)
 
 -- per further discussion of bug #17781
 explain (costs off)
@@ -6054,15 +6066,16 @@ select * from int8_tbl t1 left join
   (int8_tbl t2 inner join int8_tbl t3 on false
    left join int8_tbl t4 on t2.q2 = t4.q2)
 on t1.q1 = t2.q1;
-              QUERY PLAN              
---------------------------------------
+                 QUERY PLAN                 
+--------------------------------------------
  Hash Left Join
    Hash Cond: (t1.q1 = q1)
    ->  Seq Scan on int8_tbl t1
    ->  Hash
          ->  Result
                One-Time Filter: false
-(6 rows)
+               Replaces: Join on t2, t3, t4
+(7 rows)
 
 -- deduce constant-false from an EquivalenceClass
 explain (costs off)
@@ -6070,15 +6083,16 @@ select * from int8_tbl t1 left join
   (int8_tbl t2 inner join int8_tbl t3 on (t2.q1-t3.q2) = 0 and (t2.q1-t3.q2) = 1
    left join int8_tbl t4 on t2.q2 = t4.q2)
 on t1.q1 = t2.q1;
-              QUERY PLAN              
---------------------------------------
+                 QUERY PLAN                 
+--------------------------------------------
  Hash Left Join
    Hash Cond: (t1.q1 = q1)
    ->  Seq Scan on int8_tbl t1
    ->  Hash
          ->  Result
                One-Time Filter: false
-(6 rows)
+               Replaces: Join on t2, t3, t4
+(7 rows)
 
 -- pseudoconstant based on an outer-level Param
 explain (costs off)
@@ -6318,7 +6332,8 @@ select p.* from
 --------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on p
+(3 rows)
 
 select p.* from
   (parent p left join child c on (p.k = c.k)) join parent x on p.k = x.k
@@ -6335,7 +6350,8 @@ select p.* from
 --------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Join on p, x
+(3 rows)
 
 -- bug 5255: this is not optimizable by join removal
 begin;
@@ -6408,7 +6424,8 @@ SELECT q2 FROM
    ->  Result
          Output: q2, 'constant'::text
          One-Time Filter: false
-(9 rows)
+         Replaces: Scan on int8_tbl
+(10 rows)
 
 -- join removal bug #17786: check that OR conditions are cleaned up
 EXPLAIN (COSTS OFF)
@@ -6428,7 +6445,8 @@ FROM int4_tbl
                ->  Seq Scan on tenk1
                ->  Result
                      One-Time Filter: false
-(9 rows)
+                     Replaces: Scan on int8_tbl
+(10 rows)
 
 rollback;
 -- another join removal bug: we must clean up correctly when removing a PHV
@@ -6879,7 +6897,8 @@ where q1.x = q2.y;
 --------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on sj_1
+(3 rows)
 
 -- We can't use a cross-EC generated self join qual because of current logic of
 -- the generate_join_implied_equalities routine.
@@ -7703,11 +7722,12 @@ select 1 from emp1 full join
         on true
     where false) s on true
 where false;
-        QUERY PLAN        
---------------------------
+            QUERY PLAN            
+----------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Join on emp1, t1, t3
+(3 rows)
 
 select 1 from emp1 full join
     (select * from emp1 t1 join
@@ -7937,7 +7957,8 @@ where false;
  Result
    Output: 1
    One-Time Filter: false
-(3 rows)
+   Replaces: Scan on ss
+(4 rows)
 
 --
 -- Test LATERAL
@@ -8876,7 +8897,8 @@ select * from int8_tbl i8 left join lateral
    ->  Result
          Output: f1, i8.q2
          One-Time Filter: false
-(8 rows)
+         Replaces: Scan on int4_tbl
+(9 rows)
 
 explain (verbose, costs off)
 select * from int8_tbl i8 left join lateral
@@ -8890,7 +8912,8 @@ select * from int8_tbl i8 left join lateral
    ->  Result
          Output: f1, f1, i8.q2
          One-Time Filter: false
-(7 rows)
+         Replaces: Join on i1, i2
+(8 rows)
 
 -- check handling of nested appendrels inside LATERAL
 select * from
diff --git a/src/test/regress/expected/merge.out b/src/test/regress/expected/merge.out
index cf2219df754..c748172e98f 100644
--- a/src/test/regress/expected/merge.out
+++ b/src/test/regress/expected/merge.out
@@ -2427,7 +2427,8 @@ MERGE INTO pa_target t USING pa_source s ON t.tid = s.sid
                ->  Result
                      Output: t.tid, t.ctid
                      One-Time Filter: false
-(12 rows)
+                     Replaces: Scan on t
+(13 rows)
 
 MERGE INTO pa_target t USING pa_source s ON t.tid = s.sid
   WHEN NOT MATCHED THEN INSERT VALUES (s.sid);
diff --git a/src/test/regress/expected/partition_aggregate.out b/src/test/regress/expected/partition_aggregate.out
index 5f2c0cf5786..80b002fbdcf 100644
--- a/src/test/regress/expected/partition_aggregate.out
+++ b/src/test/regress/expected/partition_aggregate.out
@@ -146,13 +146,14 @@ SELECT c, a, count(*) FROM pagg_tab GROUP BY a, c;
 -- Test when input relation for grouping is dummy
 EXPLAIN (COSTS OFF)
 SELECT c, sum(a) FROM pagg_tab WHERE 1 = 2 GROUP BY c;
-           QUERY PLAN           
---------------------------------
+             QUERY PLAN             
+------------------------------------
  HashAggregate
    Group Key: c
    ->  Result
          One-Time Filter: false
-(4 rows)
+         Replaces: Scan on pagg_tab
+(5 rows)
 
 SELECT c, sum(a) FROM pagg_tab WHERE 1 = 2 GROUP BY c;
  c | sum 
@@ -161,12 +162,13 @@ SELECT c, sum(a) FROM pagg_tab WHERE 1 = 2 GROUP BY c;
 
 EXPLAIN (COSTS OFF)
 SELECT c, sum(a) FROM pagg_tab WHERE c = 'x' GROUP BY c;
-           QUERY PLAN           
---------------------------------
+             QUERY PLAN             
+------------------------------------
  GroupAggregate
    ->  Result
          One-Time Filter: false
-(3 rows)
+         Replaces: Scan on pagg_tab
+(4 rows)
 
 SELECT c, sum(a) FROM pagg_tab WHERE c = 'x' GROUP BY c;
  c | sum 
@@ -804,15 +806,16 @@ SELECT a.x, b.y, count(*) FROM (SELECT * FROM pagg_tab1 WHERE x < 20) a FULL JOI
 -- Empty join relation because of empty outer side, no partitionwise agg plan
 EXPLAIN (COSTS OFF)
 SELECT a.x, a.y, count(*) FROM (SELECT * FROM pagg_tab1 WHERE x = 1 AND x = 2) a LEFT JOIN pagg_tab2 b ON a.x = b.y GROUP BY a.x, a.y ORDER BY 1, 2;
-              QUERY PLAN              
---------------------------------------
+                  QUERY PLAN                  
+----------------------------------------------
  GroupAggregate
    Group Key: pagg_tab1.y
    ->  Sort
          Sort Key: pagg_tab1.y
          ->  Result
                One-Time Filter: false
-(6 rows)
+               Replaces: Join on b, pagg_tab1
+(7 rows)
 
 SELECT a.x, a.y, count(*) FROM (SELECT * FROM pagg_tab1 WHERE x = 1 AND x = 2) a LEFT JOIN pagg_tab2 b ON a.x = b.y GROUP BY a.x, a.y ORDER BY 1, 2;
  x | y | count 
diff --git a/src/test/regress/expected/partition_join.out b/src/test/regress/expected/partition_join.out
index 24e06845f92..0c495907eca 100644
--- a/src/test/regress/expected/partition_join.out
+++ b/src/test/regress/expected/partition_join.out
@@ -1609,19 +1609,21 @@ SELECT avg(t1.a), avg(t2.b), avg(t3.a + t3.b), t1.c, t2.c, t3.c FROM plt1 t1, pl
 -- joins where one of the relations is proven empty
 EXPLAIN (COSTS OFF)
 SELECT t1.a, t1.c, t2.b, t2.c FROM prt1 t1, prt2 t2 WHERE t1.a = t2.b AND t1.a = 1 AND t1.a = 2;
-        QUERY PLAN        
---------------------------
+         QUERY PLAN         
+----------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Join on t1, t2
+(3 rows)
 
 EXPLAIN (COSTS OFF)
 SELECT t1.a, t1.c, t2.b, t2.c FROM (SELECT * FROM prt1 WHERE a = 1 AND a = 2) t1 LEFT JOIN prt2 t2 ON t1.a = t2.b;
-        QUERY PLAN        
---------------------------
+          QUERY PLAN          
+------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Join on t2, prt1
+(3 rows)
 
 EXPLAIN (COSTS OFF)
 SELECT t1.a, t1.c, t2.b, t2.c FROM (SELECT * FROM prt1 WHERE a = 1 AND a = 2) t1 RIGHT JOIN prt2 t2 ON t1.a = t2.b, prt1 t3 WHERE t2.b = t3.a;
@@ -1648,7 +1650,8 @@ SELECT t1.a, t1.c, t2.b, t2.c FROM (SELECT * FROM prt1 WHERE a = 1 AND a = 2) t1
    ->  Hash
          ->  Result
                One-Time Filter: false
-(21 rows)
+               Replaces: Scan on prt1
+(22 rows)
 
 EXPLAIN (COSTS OFF)
 SELECT t1.a, t1.c, t2.b, t2.c FROM (SELECT * FROM prt1 WHERE a = 1 AND a = 2) t1 FULL JOIN prt2 t2 ON t1.a = t2.b WHERE t2.a = 0 ORDER BY t1.a, t2.b;
@@ -1668,7 +1671,8 @@ SELECT t1.a, t1.c, t2.b, t2.c FROM (SELECT * FROM prt1 WHERE a = 1 AND a = 2) t1
          ->  Hash
                ->  Result
                      One-Time Filter: false
-(14 rows)
+                     Replaces: Scan on prt1
+(15 rows)
 
 --
 -- tests for hash partitioned tables.
@@ -2255,7 +2259,8 @@ SELECT t1.a, t1.c, t2.b, t2.c FROM (SELECT * FROM prt1_l WHERE a = 1 AND a = 2)
    ->  Hash
          ->  Result
                One-Time Filter: false
-(11 rows)
+               Replaces: Scan on prt1_l
+(12 rows)
 
 -- Test case to verify proper handling of subqueries in a partitioned delete.
 -- The weird-looking lateral join is just there to force creation of a
diff --git a/src/test/regress/expected/partition_prune.out b/src/test/regress/expected/partition_prune.out
index d1966cd7d82..86cecc3ed71 100644
--- a/src/test/regress/expected/partition_prune.out
+++ b/src/test/regress/expected/partition_prune.out
@@ -628,7 +628,8 @@ explain (costs off) select * from rlp3 where a = 20;   /* empty */
 --------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on rlp3
+(3 rows)
 
 -- redundant clauses are eliminated
 explain (costs off) select * from rlp where a > 1 and a = 10;	/* only default */
@@ -671,7 +672,8 @@ explain (costs off) select * from rlp where a = 1 and a = 3;	/* empty */
 --------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on rlp
+(3 rows)
 
 explain (costs off) select * from rlp where (a = 1 and a = 3) or (a > 1 and a = 15);
                             QUERY PLAN                             
@@ -1254,25 +1256,28 @@ select * from boolpart where a is not unknown;
 
 -- check that all partitions are pruned when faced with conflicting clauses
 explain (costs off) select * from boolpart where a is not unknown and a is unknown;
-        QUERY PLAN        
---------------------------
+          QUERY PLAN          
+------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on boolpart
+(3 rows)
 
 explain (costs off) select * from boolpart where a is false and a is unknown;
-        QUERY PLAN        
---------------------------
+          QUERY PLAN          
+------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on boolpart
+(3 rows)
 
 explain (costs off) select * from boolpart where a is true and a is unknown;
-        QUERY PLAN        
---------------------------
+          QUERY PLAN          
+------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on boolpart
+(3 rows)
 
 -- inverse boolean partitioning - a seemingly unlikely design, but we've got
 -- code for it, so we'd better test it.
@@ -1568,11 +1573,12 @@ explain (costs off) select * from coercepart where a = any ('{ab,null}');
 (2 rows)
 
 explain (costs off) select * from coercepart where a = any (null::text[]);
-        QUERY PLAN        
---------------------------
+           QUERY PLAN           
+--------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on coercepart
+(3 rows)
 
 explain (costs off) select * from coercepart where a = all ('{ab}');
                   QUERY PLAN                  
@@ -1582,25 +1588,28 @@ explain (costs off) select * from coercepart where a = all ('{ab}');
 (2 rows)
 
 explain (costs off) select * from coercepart where a = all ('{ab,bc}');
-        QUERY PLAN        
---------------------------
+           QUERY PLAN           
+--------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on coercepart
+(3 rows)
 
 explain (costs off) select * from coercepart where a = all ('{ab,null}');
-        QUERY PLAN        
---------------------------
+           QUERY PLAN           
+--------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on coercepart
+(3 rows)
 
 explain (costs off) select * from coercepart where a = all (null::text[]);
-        QUERY PLAN        
---------------------------
+           QUERY PLAN           
+--------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on coercepart
+(3 rows)
 
 drop table coercepart;
 CREATE TABLE part (a INT, b INT) PARTITION BY LIST (a);
@@ -1773,7 +1782,8 @@ explain (costs off) select * from lp where a <> 'a' and a is null;
 --------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on lp
+(3 rows)
 
 explain (costs off) select * from lp where (a <> 'a' and a <> 'd') or a is null;
                                   QUERY PLAN                                  
@@ -1866,22 +1876,24 @@ create table lparted_by_int2 (a smallint) partition by list (a);
 create table lparted_by_int2_1 partition of lparted_by_int2 for values in (1);
 create table lparted_by_int2_16384 partition of lparted_by_int2 for values in (16384);
 explain (costs off) select * from lparted_by_int2 where a = 100_000_000_000_000;
-        QUERY PLAN        
---------------------------
+             QUERY PLAN              
+-------------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on lparted_by_int2
+(3 rows)
 
 create table rparted_by_int2 (a smallint) partition by range (a);
 create table rparted_by_int2_1 partition of rparted_by_int2 for values from (1) to (10);
 create table rparted_by_int2_16384 partition of rparted_by_int2 for values from (10) to (16384);
 -- all partitions pruned
 explain (costs off) select * from rparted_by_int2 where a > 100_000_000_000_000;
-        QUERY PLAN        
---------------------------
+             QUERY PLAN              
+-------------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on rparted_by_int2
+(3 rows)
 
 create table rparted_by_int2_maxvalue partition of rparted_by_int2 for values from (16384) to (maxvalue);
 -- all partitions but rparted_by_int2_maxvalue pruned
@@ -2132,7 +2144,8 @@ explain (costs off) select * from hp where a = 1 and b = 'abcde' and
 --------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on hp
+(3 rows)
 
 --
 -- Test runtime partition pruning
@@ -3390,11 +3403,12 @@ select * from stable_qual_pruning where a < '2000-02-01'::timestamptz;
 explain (analyze, costs off, summary off, timing off, buffers off)
 select * from stable_qual_pruning
   where a = any(array['2010-02-01', '2020-01-01']::timestamp[]);
-            QUERY PLAN             
------------------------------------
+               QUERY PLAN                
+-----------------------------------------
  Result (actual rows=0.00 loops=1)
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on stable_qual_pruning
+(3 rows)
 
 explain (analyze, costs off, summary off, timing off, buffers off)
 select * from stable_qual_pruning
@@ -3642,6 +3656,7 @@ explain (analyze, costs off, summary off, timing off, buffers off) select * from
    Sort Key: ma_test.b
    InitPlan 2
      ->  Result (actual rows=1.00 loops=1)
+           Replaces: Aggregate
            InitPlan 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)
@@ -3656,7 +3671,7 @@ explain (analyze, costs off, summary off, timing off, buffers off) select * from
    ->  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)
          Index Searches: 1
-(18 rows)
+(19 rows)
 
 reset enable_seqscan;
 reset enable_sort;
@@ -3678,11 +3693,12 @@ explain (costs off) select * from pp_arrpart where a = '{1}';
 (2 rows)
 
 explain (costs off) select * from pp_arrpart where a = '{1, 2}';
-        QUERY PLAN        
---------------------------
+           QUERY PLAN           
+--------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on pp_arrpart
+(3 rows)
 
 explain (costs off) select * from pp_arrpart where a in ('{4, 5}', '{1}');
                               QUERY PLAN                              
@@ -3764,11 +3780,12 @@ explain (costs off) select * from pp_enumpart where a = 'blue';
 (2 rows)
 
 explain (costs off) select * from pp_enumpart where a = 'black';
-        QUERY PLAN        
---------------------------
+           QUERY PLAN            
+---------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on pp_enumpart
+(3 rows)
 
 drop table pp_enumpart;
 drop type pp_colors;
@@ -3785,11 +3802,12 @@ explain (costs off) select * from pp_recpart where a = '(1,1)'::pp_rectype;
 (2 rows)
 
 explain (costs off) select * from pp_recpart where a = '(1,2)'::pp_rectype;
-        QUERY PLAN        
---------------------------
+           QUERY PLAN           
+--------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on pp_recpart
+(3 rows)
 
 drop table pp_recpart;
 drop type pp_rectype;
@@ -3805,11 +3823,12 @@ explain (costs off) select * from pp_intrangepart where a = '[1,2]'::int4range;
 (2 rows)
 
 explain (costs off) select * from pp_intrangepart where a = '(1,2)'::int4range;
-        QUERY PLAN        
---------------------------
+             QUERY PLAN              
+-------------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on pp_intrangepart
+(3 rows)
 
 drop table pp_intrangepart;
 --
@@ -4125,19 +4144,21 @@ explain (costs off) update listp1 set a = 1 where a = 2;
 -- constraint exclusion enabled
 set constraint_exclusion to 'on';
 explain (costs off) select * from listp1 where a = 2;
-        QUERY PLAN        
---------------------------
+         QUERY PLAN         
+----------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on listp1
+(3 rows)
 
 explain (costs off) update listp1 set a = 1 where a = 2;
-           QUERY PLAN           
---------------------------------
+            QUERY PLAN            
+----------------------------------
  Update on listp1
    ->  Result
          One-Time Filter: false
-(3 rows)
+         Replaces: Scan on listp1
+(4 rows)
 
 reset constraint_exclusion;
 reset enable_partition_pruning;
@@ -4524,18 +4545,20 @@ create table hp_contradict_test (a int, b int) partition by hash (a part_test_in
 create table hp_contradict_test_p1 partition of hp_contradict_test for values with (modulus 2, remainder 0);
 create table hp_contradict_test_p2 partition of hp_contradict_test for values with (modulus 2, remainder 1);
 explain (costs off) select * from hp_contradict_test where a is null and a === 1 and b === 1;
-        QUERY PLAN        
---------------------------
+               QUERY PLAN               
+----------------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on hp_contradict_test
+(3 rows)
 
 explain (costs off) select * from hp_contradict_test where a === 1 and b === 1 and a is null;
-        QUERY PLAN        
---------------------------
+               QUERY PLAN               
+----------------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on hp_contradict_test
+(3 rows)
 
 drop table hp_contradict_test;
 drop operator class part_test_int4_ops2 using hash;
diff --git a/src/test/regress/expected/predicate.out b/src/test/regress/expected/predicate.out
index 1aff0b59ff8..cd78cf11f51 100644
--- a/src/test/regress/expected/predicate.out
+++ b/src/test/regress/expected/predicate.out
@@ -37,7 +37,8 @@ SELECT * FROM pred_tab t WHERE t.a IS NULL;
 --------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on t
+(3 rows)
 
 -- Ensure the IS_NULL qual is not reduced to constant-FALSE on nullable
 -- columns
@@ -78,7 +79,8 @@ SELECT * FROM pred_tab t WHERE t.a IS NULL OR t.c IS NULL;
 --------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on t
+(3 rows)
 
 -- Ensure the OR clause is not reduced to constant-FALSE when not all branches
 -- are provably false
@@ -140,7 +142,8 @@ SELECT * FROM pred_tab t1
    ->  Seq Scan on pred_tab t1
    ->  Result
          One-Time Filter: false
-(5 rows)
+         Replaces: Scan on t2
+(6 rows)
 
 -- Ensure the IS_NULL qual is not reduced to constant-FALSE when the column is
 -- nullable by an outer join
@@ -210,7 +213,8 @@ SELECT * FROM pred_tab t1
    ->  Seq Scan on pred_tab t1
    ->  Result
          One-Time Filter: false
-(5 rows)
+         Replaces: Scan on t2
+(6 rows)
 
 -- Ensure the OR clause is not reduced to constant-FALSE when a column is
 -- made nullable from an outer join
@@ -267,17 +271,18 @@ SELECT * FROM pred_tab t1
     LEFT JOIN pred_tab t2 ON EXISTS
         (SELECT 1 FROM pred_tab t3, pred_tab t4, pred_tab t5, pred_tab t6
          WHERE t1.a = t3.a AND t6.a IS NULL);
-             QUERY PLAN              
--------------------------------------
+                 QUERY PLAN                 
+--------------------------------------------
  Nested Loop Left Join
    Join Filter: (InitPlan 1).col1
    InitPlan 1
      ->  Result
            One-Time Filter: false
+           Replaces: Join on t3, t4, t5, t6
    ->  Seq Scan on pred_tab t1
    ->  Materialize
          ->  Seq Scan on pred_tab t2
-(8 rows)
+(9 rows)
 
 DROP TABLE pred_tab;
 -- Validate we handle IS NULL and IS NOT NULL quals correctly with inheritance
@@ -418,20 +423,22 @@ SET constraint_exclusion TO ON;
 -- Ensure that we get a dummy plan
 EXPLAIN (COSTS OFF)
 SELECT * FROM pred_tab1, pred_tab2 WHERE pred_tab2.a IS NULL;
-        QUERY PLAN        
---------------------------
+                QUERY PLAN                
+------------------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Join on pred_tab1, pred_tab2
+(3 rows)
 
 -- Ensure that we get a dummy plan
 EXPLAIN (COSTS OFF)
 SELECT * FROM pred_tab2, pred_tab1 WHERE pred_tab1.a IS NULL OR pred_tab1.b < 2;
-        QUERY PLAN        
---------------------------
+                QUERY PLAN                
+------------------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Join on pred_tab2, pred_tab1
+(3 rows)
 
 RESET constraint_exclusion;
 DROP TABLE pred_tab1;
diff --git a/src/test/regress/expected/rowsecurity.out b/src/test/regress/expected/rowsecurity.out
index 8c879509313..e7fe3317638 100644
--- a/src/test/regress/expected/rowsecurity.out
+++ b/src/test/regress/expected/rowsecurity.out
@@ -3607,7 +3607,8 @@ EXPLAIN (COSTS OFF) SELECT * FROM t1;
 --------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on t1
+(3 rows)
 
 SET SESSION AUTHORIZATION regress_rls_bob;
 SELECT * FROM t1;
@@ -3620,7 +3621,8 @@ EXPLAIN (COSTS OFF) SELECT * FROM t1;
 --------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on t1
+(3 rows)
 
 --
 -- COPY TO/FROM
@@ -4524,11 +4526,12 @@ SELECT * FROM rls_tbl WHERE a <<< 1000;
 (0 rows)
 
 EXPLAIN (COSTS OFF) SELECT * FROM rls_tbl WHERE a <<< 1000 or a <<< 900;
-        QUERY PLAN        
---------------------------
+         QUERY PLAN          
+-----------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on rls_tbl
+(3 rows)
 
 RESET SESSION AUTHORIZATION;
 CREATE TABLE rls_child_tbl () INHERITS (rls_tbl);
diff --git a/src/test/regress/expected/rowtypes.out b/src/test/regress/expected/rowtypes.out
index 9168979a620..328b1e142c0 100644
--- a/src/test/regress/expected/rowtypes.out
+++ b/src/test/regress/expected/rowtypes.out
@@ -1324,7 +1324,8 @@ where false;
  Result
    Output: (a).f1, (a).f2
    One-Time Filter: false
-(3 rows)
+   Replaces: Scan on ss
+(4 rows)
 
 explain (verbose, costs off)
 with cte(c) as materialized (select row(1, 2)),
@@ -1349,10 +1350,11 @@ where false;
  Result
    Output: (cte.c).f1
    One-Time Filter: false
+   Replaces: Scan on cte
    CTE cte
      ->  Result
            Output: '(1,2)'::record
-(6 rows)
+(7 rows)
 
 --
 -- Tests for component access / FieldSelect
diff --git a/src/test/regress/expected/select.out b/src/test/regress/expected/select.out
index bab0cc93ff5..d1736dac224 100644
--- a/src/test/regress/expected/select.out
+++ b/src/test/regress/expected/select.out
@@ -962,10 +962,11 @@ create table list_parted_tbl (a int,b int) partition by list (a);
 create table list_parted_tbl1 partition of list_parted_tbl
   for values in (1) partition by list(b);
 explain (costs off) select * from list_parted_tbl;
-        QUERY PLAN        
---------------------------
+             QUERY PLAN              
+-------------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on list_parted_tbl
+(3 rows)
 
 drop table list_parted_tbl;
diff --git a/src/test/regress/expected/subselect.out b/src/test/regress/expected/subselect.out
index 0563d0cd5a1..db061ee95b1 100644
--- a/src/test/regress/expected/subselect.out
+++ b/src/test/regress/expected/subselect.out
@@ -2851,12 +2851,13 @@ ON B.hundred in (SELECT min(c.hundred) FROM tenk2 C WHERE c.odd = b.odd);
                      ->  Subquery Scan on "ANY_subquery"
                            Filter: (b.hundred = "ANY_subquery".min)
                            ->  Result
+                                 Replaces: Aggregate
                                  InitPlan 1
                                    ->  Limit
                                          ->  Index Scan using tenk2_hundred on tenk2 c
                                                Index Cond: (hundred IS NOT NULL)
                                                Filter: (odd = b.odd)
-(16 rows)
+(17 rows)
 
 --
 -- Test VALUES to ARRAY (VtA) transformation
@@ -3020,7 +3021,8 @@ EXPLAIN (COSTS OFF) EXECUTE test(NULL, 3.14, NULL);
 --------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on onek
+(3 rows)
 
 EXPLAIN (COSTS OFF) EXECUTE test(NULL, 3.14, '-1.5');
                                                                      QUERY PLAN                                                                      
diff --git a/src/test/regress/expected/tsrf.out b/src/test/regress/expected/tsrf.out
index d47b5f6ec57..e73e68af971 100644
--- a/src/test/regress/expected/tsrf.out
+++ b/src/test/regress/expected/tsrf.out
@@ -92,7 +92,8 @@ SELECT unnest(ARRAY[1, 2]) FROM few WHERE false;
    Output: unnest('{1,2}'::integer[])
    ->  Result
          One-Time Filter: false
-(4 rows)
+         Replaces: Scan on few
+(5 rows)
 
 SELECT unnest(ARRAY[1, 2]) FROM few WHERE false;
  unnest 
@@ -108,7 +109,8 @@ SELECT * FROM few f1,
  Result
    Output: f1.id, f1.dataa, f1.datab, ss.unnest
    One-Time Filter: false
-(3 rows)
+   Replaces: Join on f1, ss
+(4 rows)
 
 SELECT * FROM few f1,
   (SELECT unnest(ARRAY[1,2]) FROM few f2 WHERE false OFFSET 0) ss;
-- 
2.39.5 (Apple Git-154)

v3-0002-Consider-a-Result-node-s-relids-in-ExplainPreScan.patchapplication/octet-stream; name=v3-0002-Consider-a-Result-node-s-relids-in-ExplainPreScan.patchDownload
From 9567b8b9b323945a62217ded9478e0095387ccc1 Mon Sep 17 00:00:00 2001
From: Robert Haas <rhaas@postgresql.org>
Date: Mon, 21 Apr 2025 13:35:28 -0400
Subject: [PATCH v3 2/7] Consider a Result node's relids in ExplainPreScanNode.

Now that a Result node has a relids set, add the relids that it
carries the set accumulated by ExplainPreScanNode so that we
generate unique relation aliases for all of the referenced relations
when it calls select_rtable_names_for_explain. The effect of this
changes is that a few things get schema-qualified in the regression
test outputs that previously were not. In similar cases not involving
a Result node, we were already schema-qualifying, so this appears to
be an improvement.

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.
---
 src/backend/commands/explain.c                |  4 +++
 .../regress/expected/generated_virtual.out    |  8 ++---
 src/test/regress/expected/join.out            | 32 +++++++++----------
 src/test/regress/expected/partition_join.out  | 12 +++----
 4 files changed, 30 insertions(+), 26 deletions(-)

diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 42723c3a150..e8bd4c560dd 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -1230,6 +1230,10 @@ ExplainPreScanNode(PlanState *planstate, Bitmapset **rels_used)
 			*rels_used = bms_add_members(*rels_used,
 										 ((MergeAppend *) plan)->apprelids);
 			break;
+		case T_Result:
+			*rels_used = bms_add_members(*rels_used,
+										 ((Result *) plan)->relids);
+			break;
 		default:
 			break;
 	}
diff --git a/src/test/regress/expected/generated_virtual.out b/src/test/regress/expected/generated_virtual.out
index 8b25e77bb4d..70727623bca 100644
--- a/src/test/regress/expected/generated_virtual.out
+++ b/src/test/regress/expected/generated_virtual.out
@@ -1570,15 +1570,15 @@ where coalesce(t2.b, 1) = 2 or t1.a is null;
 -- Ensure that the generation expressions are wrapped into PHVs if needed
 explain (verbose, costs off)
 select t2.* from gtest32 t1 left join gtest32 t2 on false;
-                      QUERY PLAN                      
-------------------------------------------------------
+                          QUERY PLAN                           
+---------------------------------------------------------------
  Nested Loop Left Join
-   Output: a, (a * 2), (20), (COALESCE(a, 100)), e
+   Output: t2.a, (t2.a * 2), (20), (COALESCE(t2.a, 100)), t2.e
    Join Filter: false
    ->  Seq Scan on generated_virtual_tests.gtest32 t1
          Output: t1.a, t1.b, t1.c, t1.d, t1.e
    ->  Result
-         Output: a, e, 20, COALESCE(a, 100)
+         Output: t2.a, t2.e, 20, COALESCE(t2.a, 100)
          One-Time Filter: false
          Replaces: Scan on t2
 (9 rows)
diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out
index 01ce1aa577c..f73df838f14 100644
--- a/src/test/regress/expected/join.out
+++ b/src/test/regress/expected/join.out
@@ -4151,9 +4151,9 @@ select * from t t1
                         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)
@@ -4166,7 +4166,7 @@ select * from t t1
                      ->  Seq Scan on pg_temp.t t2
                            Output: t2.i2, 1
          ->  Result
-               Output: i3
+               Output: t3.i3
                One-Time Filter: false
                Replaces: Scan on t3
    ->  Memoize
@@ -6069,7 +6069,7 @@ on t1.q1 = t2.q1;
                  QUERY PLAN                 
 --------------------------------------------
  Hash Left Join
-   Hash Cond: (t1.q1 = q1)
+   Hash Cond: (t1.q1 = t2.q1)
    ->  Seq Scan on int8_tbl t1
    ->  Hash
          ->  Result
@@ -6086,7 +6086,7 @@ on t1.q1 = t2.q1;
                  QUERY PLAN                 
 --------------------------------------------
  Hash Left Join
-   Hash Cond: (t1.q1 = q1)
+   Hash Cond: (t1.q1 = t2.q1)
    ->  Seq Scan on int8_tbl t1
    ->  Hash
          ->  Result
@@ -6416,13 +6416,13 @@ SELECT q2 FROM
                       QUERY PLAN                      
 ------------------------------------------------------
  Nested Loop Left Join
-   Output: q2
+   Output: int8_tbl.q2
    Join Filter: NULL::boolean
    Filter: (('constant'::text) >= ('constant'::text))
    ->  Seq Scan on public.int4_tbl
          Output: int4_tbl.f1
    ->  Result
-         Output: q2, 'constant'::text
+         Output: int8_tbl.q2, 'constant'::text
          One-Time Filter: false
          Replaces: Scan on int8_tbl
 (10 rows)
@@ -6897,7 +6897,7 @@ where q1.x = q2.y;
 --------------------------
  Result
    One-Time Filter: false
-   Replaces: Scan on sj_1
+   Replaces: Scan on sj
 (3 rows)
 
 -- We can't use a cross-EC generated self join qual because of current logic of
@@ -8887,15 +8887,15 @@ select * from int4_tbl t1,
 explain (verbose, costs off)
 select * from int8_tbl i8 left join lateral
   (select *, i8.q2 from int4_tbl where false) ss on true;
-              QUERY PLAN              
---------------------------------------
+                  QUERY PLAN                  
+----------------------------------------------
  Nested Loop Left Join
-   Output: i8.q1, i8.q2, f1, (i8.q2)
+   Output: i8.q1, i8.q2, int4_tbl.f1, (i8.q2)
    Join Filter: false
    ->  Seq Scan on public.int8_tbl i8
          Output: i8.q1, i8.q2
    ->  Result
-         Output: f1, i8.q2
+         Output: int4_tbl.f1, i8.q2
          One-Time Filter: false
          Replaces: Scan on int4_tbl
 (9 rows)
@@ -8903,14 +8903,14 @@ select * from int8_tbl i8 left join lateral
 explain (verbose, costs off)
 select * from int8_tbl i8 left join lateral
   (select *, i8.q2 from int4_tbl i1, int4_tbl i2 where false) ss on true;
-               QUERY PLAN                
------------------------------------------
+                  QUERY PLAN                   
+-----------------------------------------------
  Nested Loop Left Join
-   Output: i8.q1, i8.q2, f1, f1, (i8.q2)
+   Output: i8.q1, i8.q2, i1.f1, i2.f1, (i8.q2)
    ->  Seq Scan on public.int8_tbl i8
          Output: i8.q1, i8.q2
    ->  Result
-         Output: f1, f1, i8.q2
+         Output: i1.f1, i2.f1, i8.q2
          One-Time Filter: false
          Replaces: Join on i1, i2
 (8 rows)
diff --git a/src/test/regress/expected/partition_join.out b/src/test/regress/expected/partition_join.out
index 0c495907eca..c94108eb118 100644
--- a/src/test/regress/expected/partition_join.out
+++ b/src/test/regress/expected/partition_join.out
@@ -1630,7 +1630,7 @@ SELECT t1.a, t1.c, t2.b, t2.c FROM (SELECT * FROM prt1 WHERE a = 1 AND a = 2) t1
                     QUERY PLAN                    
 --------------------------------------------------
  Hash Left Join
-   Hash Cond: (t2.b = a)
+   Hash Cond: (t2.b = prt1.a)
    ->  Append
          ->  Hash Join
                Hash Cond: (t3_1.a = t2_1.b)
@@ -1658,9 +1658,9 @@ SELECT t1.a, t1.c, t2.b, t2.c FROM (SELECT * FROM prt1 WHERE a = 1 AND a = 2) t1
                  QUERY PLAN                 
 --------------------------------------------
  Sort
-   Sort Key: a, t2.b
+   Sort Key: prt1.a, t2.b
    ->  Hash Left Join
-         Hash Cond: (t2.b = a)
+         Hash Cond: (t2.b = prt1.a)
          ->  Append
                ->  Seq Scan on prt2_p1 t2_1
                      Filter: (a = 0)
@@ -2246,10 +2246,10 @@ SELECT COUNT(*) FROM prt1_l t1 LEFT JOIN LATERAL
 -- join with one side empty
 EXPLAIN (COSTS OFF)
 SELECT t1.a, t1.c, t2.b, t2.c FROM (SELECT * FROM prt1_l WHERE a = 1 AND a = 2) t1 RIGHT JOIN prt2_l t2 ON t1.a = t2.b AND t1.b = t2.a AND t1.c = t2.c;
-                               QUERY PLAN                                
--------------------------------------------------------------------------
+                                          QUERY PLAN                                          
+----------------------------------------------------------------------------------------------
  Hash Left Join
-   Hash Cond: ((t2.b = a) AND (t2.a = b) AND ((t2.c)::text = (c)::text))
+   Hash Cond: ((t2.b = prt1_l.a) AND (t2.a = prt1_l.b) AND ((t2.c)::text = (prt1_l.c)::text))
    ->  Append
          ->  Seq Scan on prt2_l_p1 t2_1
          ->  Seq Scan on prt2_l_p2_p1 t2_2
-- 
2.39.5 (Apple Git-154)

v3-0004-Give-subplans-names-that-are-known-while-planning.patchapplication/octet-stream; name=v3-0004-Give-subplans-names-that-are-known-while-planning.patchDownload
From 167b641dd9ef52e409e4f401137406fdef66b597 Mon Sep 17 00:00:00 2001
From: Robert Haas <rhaas@postgresql.org>
Date: Thu, 5 Dec 2024 15:19:17 -0500
Subject: [PATCH v3 4/7] Give subplans names that are known while planning that
 subplan.

Previously, subplans were shown in EXPLAIN output identified by
a number, like "InitPlan 1", and some were identified by a name,
like "CTE foo". Now, each subplan gets a name, which for InitPlans
and SubPlans is based on the type of sublink e.g. expr_1 or any_1,
and these names are guaranteed to be unique across the whole plan.

The numerical portion of the name may be different than it was
previously, because InitPlan 1 meant the first subplan that we
finished planning (which happened to be an InitPlan). This number
couldn't be known at the time we began planning that subplan,
because the query planner might recurse into other subplans which
would then be fully planned before finishing the plan at the outer
level. These new subplan names are assigned when we *start* planning
a subplan, which allows extensions that affect planning to know the
name that will ultimately be assigned while planning is still in
progress.

Some subplans aren't shown as subplans in EXPLAIN output. This
happens when the subquery is a FROM-cluse item or a branch of a
set operation, rather than, for example, an expression that will
be transformed into something render as an InitPlan or SubPlan.
These subplans also get unique names, although those names are not
currently shown in the EXPLAIN output. This means that it's now
possible to use unique, human-readable names to refer to any
subplan within a query; only the topmost query level is nameless.
---
 .../postgres_fdw/expected/postgres_fdw.out    |  58 ++--
 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/include/nodes/pathnodes.h                 |   6 +
 src/include/nodes/primnodes.h                 |   1 +
 src/include/optimizer/planner.h               |   5 +-
 src/test/regress/expected/aggregates.out      |  58 ++--
 src/test/regress/expected/create_index.out    |  14 +-
 src/test/regress/expected/groupingsets.out    |  90 +++---
 .../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 | 292 +++++++++---------
 src/test/regress/expected/portals.out         |  12 +-
 src/test/regress/expected/predicate.out       |   8 +-
 src/test/regress/expected/returning.out       |  20 +-
 src/test/regress/expected/rowsecurity.out     | 138 ++++-----
 src/test/regress/expected/rowtypes.out        |  12 +-
 src/test/regress/expected/select_parallel.out |  52 ++--
 src/test/regress/expected/sqljson.out         |   4 +-
 src/test/regress/expected/subselect.out       | 172 +++++------
 src/test/regress/expected/updatable_views.out |  48 +--
 src/test/regress/expected/update.out          |   8 +-
 src/test/regress/expected/window.out          |  10 +-
 src/test/regress/expected/with.out            |   8 +-
 34 files changed, 754 insertions(+), 623 deletions(-)

diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 04aba992a88..44e169b5baa 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -3178,10 +3178,10 @@ select exists(select 1 from pg_enum), sum(c1) from ft1;
                     QUERY PLAN                    
 --------------------------------------------------
  Foreign Scan
-   Output: (InitPlan 1).col1, (sum(ft1.c1))
+   Output: (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: (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: ((expr_1))
    ->  Sort
-         Output: ((SubPlan 1))
-         Sort Key: ((SubPlan 1))
+         Output: ((expr_1))
+         Sort Key: ((expr_1))
          ->  Foreign Scan
-               Output: (SubPlan 1)
+               Output: (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: ((expr_1))
    ->  Sort
-         Output: ((SubPlan 1))
-         Sort Key: ((SubPlan 1))
+         Output: ((expr_1))
+         Sort Key: ((expr_1))
          ->  Foreign Scan on public.ft2 t2
-               Output: (SubPlan 1)
+               Output: (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 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))
@@ -6447,9 +6447,9 @@ UPDATE ft2 AS target SET (c2, c7) = (
  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: (multiexpr_1).col1, (multiexpr_1).col2, (rescan 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"))
@@ -12133,9 +12133,9 @@ SELECT * FROM local_tbl t1 LEFT JOIN (SELECT *, (SELECT count(*) FROM async_pt W
                                        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, ((expr_1).col1)
    Join Filter: (t1.a = async_pt.a)
-   InitPlan 1
+   InitPlan expr_1
      ->  Aggregate
            Output: count(*)
            ->  Append
@@ -12147,10 +12147,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, (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, (expr_1).col1
                Remote SQL: SELECT a, b, c FROM public.base_tbl2 WHERE ((a < 3000))
 (20 rows)
 
@@ -12161,7 +12161,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)
@@ -12384,12 +12384,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 = (any_1).col1) AND ((random() > '0'::double precision) = (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 e8bd4c560dd..77dca0a54cf 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -4898,6 +4898,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
@@ -4921,8 +4922,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..d19d3e4e107 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, subroot);
+
 	/*----------
 	 * 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..988bbd19ab4 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, 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 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 d71ed958e31..1aa680e9865 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 char *sublinktype_to_string(SubLinkType subLinkType);
 
 
 /*
@@ -172,6 +173,7 @@ make_subplan(PlannerInfo *root, Query *orig_subquery,
 	Plan	   *plan;
 	List	   *plan_params;
 	Node	   *result;
+	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, &paramIds);
 		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 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/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h
index 4a903d1ec18..e41597acb02 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;
 
@@ -234,6 +237,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..af50831c814 100644
--- a/src/include/optimizer/planner.h
+++ b/src/include/optimizer/planner.h
@@ -43,7 +43,7 @@ extern PlannedStmt *standard_planner(Query *parse, const char *query_string,
 									 ParamListInfo boundParams);
 
 extern PlannerInfo *subquery_planner(PlannerGlobal *glob, Query *parse,
-									 PlannerInfo *parent_root,
+									 char *plan_name, PlannerInfo *parent_root,
 									 bool hasRecursion, double tuple_fraction,
 									 SetOperationStmt *setops);
 
@@ -62,4 +62,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, 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 ce45de7e175..f4ee1d27724 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(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: Aggregate
-   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: Aggregate
-   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: Aggregate
-   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: Aggregate
-   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: Aggregate
-   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: Aggregate
-   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: Aggregate
-   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: Aggregate
-           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: (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: ((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: ((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: (((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: Aggregate
-   InitPlan 1
+   InitPlan minmax_1
      ->  Limit
            ->  Result
                  One-Time Filter: (100 IS NOT NULL)
@@ -1258,7 +1258,7 @@ explain (costs off)
 ---------------------------------------------------------------------------------------------
  Result
    Replaces: Aggregate
-   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_2
      ->  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_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 1).col1), ((InitPlan 2).col1)
+         Sort Key: ((minmax_1).col1), ((minmax_2).col1)
          ->  Result
                Replaces: Aggregate
 (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: (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..dff9a687145 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, (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 = (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 07a37da79dd..39b7e1d2a35 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((expr_1)), ((expr_2))
+   Group Key: ((expr_2))
    ->  Sort
-         Output: ((SubPlan 2)), i1.q1
-         Sort Key: ((SubPlan 2))
+         Output: ((expr_2)), i1.q1
+         Sort Key: ((expr_2))
          ->  Seq Scan on public.int8_tbl i1
-               Output: (SubPlan 2), i1.q1
-               SubPlan 2
+               Output: (expr_2), i1.q1
+               SubPlan expr_2
                  ->  Result
                        Output: i1.q1
 (11 rows)
@@ -537,19 +537,19 @@ group by ss.x;
                  QUERY PLAN                 
 --------------------------------------------
  GroupAggregate
-   Output: (SubPlan 2), ((SubPlan 3))
-   Group Key: ((SubPlan 3))
+   Output: (expr_1), ((expr_3))
+   Group Key: ((expr_3))
    ->  Sort
-         Output: ((SubPlan 3)), i1.q1
-         Sort Key: ((SubPlan 3))
+         Output: ((expr_3)), i1.q1
+         Sort Key: ((expr_3))
          ->  Seq Scan on public.int8_tbl i1
-               Output: (SubPlan 3), i1.q1
-               SubPlan 3
+               Output: (expr_3), i1.q1
+               SubPlan expr_3
                  ->  Result
                        Output: i1.q1
-   SubPlan 2
+   SubPlan expr_1
      ->  Result
-           Output: GROUPING((SubPlan 1))
+           Output: GROUPING((expr_2))
 (14 rows)
 
 select (select grouping(ss.x))
@@ -592,7 +592,7 @@ explain (costs off)
 ------------------------------------------------------------
  Result
    Replaces: Aggregate
-   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: (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::integer END), t1.v
-   Sort Key: (CASE WHEN (GROUPING((SubPlan 2)) = 0) THEN ((SubPlan 3)) ELSE NULL::integer END) NULLS FIRST
+   Output: (GROUPING((expr_1))), ((expr_3)), (CASE WHEN (GROUPING((expr_2)) = 0) THEN ((expr_3)) ELSE NULL::integer END), t1.v
+   Sort Key: (CASE WHEN (GROUPING((expr_2)) = 0) THEN ((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::integer END, t1.v
+         Output: GROUPING((expr_1)), ((expr_3)), CASE WHEN (GROUPING((expr_2)) = 0) THEN ((expr_3)) ELSE NULL::integer END, t1.v
          Hash Key: t1.v
-         Hash Key: (SubPlan 3)
+         Hash Key: (expr_3)
          ->  Seq Scan on pg_temp.gstest5 t1
-               Output: (SubPlan 3), t1.v, t1.id
-               SubPlan 3
+               Output: (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::integer END), t1.v
-   Sort Key: (CASE WHEN (GROUPING((SubPlan 2)) = 0) THEN ((SubPlan 3)) ELSE NULL::integer END) NULLS FIRST
+   Output: (GROUPING((expr_1))), ((expr_3)), (CASE WHEN (GROUPING((expr_2)) = 0) THEN ((expr_3)) ELSE NULL::integer END), t1.v
+   Sort Key: (CASE WHEN (GROUPING((expr_2)) = 0) THEN ((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::integer END, t1.v
+         Output: GROUPING((expr_1)), ((expr_3)), CASE WHEN (GROUPING((expr_2)) = 0) THEN ((expr_3)) ELSE NULL::integer END, t1.v
          Hash Key: t1.v
-         Hash Key: (SubPlan 3)
+         Hash Key: (expr_3)
          ->  Seq Scan on pg_temp.gstest5 t1
-               Output: (SubPlan 3), t1.v, t1.id
-               SubPlan 3
+               Output: (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..92fe3527baf 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, ((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, ((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 e1225fb1add..581f6859b8e 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: (minmax_1).col1
    Replaces: Aggregate
-   InitPlan 1
+   InitPlan minmax_1
      ->  Limit
            Output: ((1 - matest0.id))
            ->  Result
@@ -1948,7 +1948,7 @@ SELECT min(x) FROM
 --------------------------------------------------------------------
  Result
    Replaces: Aggregate
-   InitPlan 1
+   InitPlan minmax_1
      ->  Limit
            ->  Merge Append
                  Sort Key: a.unique1
@@ -1967,7 +1967,7 @@ SELECT min(y) FROM
 --------------------------------------------------------------------
  Result
    Replaces: Aggregate
-   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: (multiexpr_1).col1, (multiexpr_1).col2, (rescan 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: (multiexpr_1).col1, (multiexpr_1).col2, (rescan 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: (multiexpr_1).col1, (multiexpr_1).col2, (rescan 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: Aggregate
-   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_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))
diff --git a/src/test/regress/expected/insert_conflict.out b/src/test/regress/expected/insert_conflict.out
index fdd0f6c8f25..028ae01e307 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(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 f73df838f14..de290b2ff97 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 = (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: Aggregate
-                       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 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 = (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, (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, (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: ((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, (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, (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: ((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 = (any_1).col1) AND ((random() > '0'::double precision) = (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 = (any_1).col1) AND ((random() > '0'::double precision) = (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: (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 = (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 = (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..e96769114db 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 = (expr_1)) AND ((expr_2) = (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: ((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: ((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: (((expr_1) = hjtest_1.id) AND ((expr_3) = (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: ((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: ((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 150dc1b44cf..85aada38120 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(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 c748172e98f..40e63c7a0ab 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 < (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, (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 86cecc3ed71..b8f49c8e118 100644
--- a/src/test/regress/expected/partition_prune.out
+++ b/src/test/regress/expected/partition_prune.out
@@ -1921,15 +1921,15 @@ where asptab.id > ss.b::int;
    ->  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(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(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 < (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 < (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 < (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 < (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 < (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 < (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 = (expr_1).col1) OR (a = (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 = (expr_1).col1) OR (a = (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 = (expr_1).col1) OR (a = (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 = (expr_1).col1)
+         Filter: (b = (expr_2).col1)
          ->  Bitmap Index Scan on ab_a1_b1_a_idx (never executed)
-               Index Cond: (a = (InitPlan 1).col1)
+               Index Cond: (a = (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 = (expr_1).col1)
+         Filter: (b = (expr_2).col1)
          ->  Bitmap Index Scan on ab_a1_b2_a_idx (never executed)
-               Index Cond: (a = (InitPlan 1).col1)
+               Index Cond: (a = (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 = (expr_1).col1)
+         Filter: (b = (expr_2).col1)
          ->  Bitmap Index Scan on ab_a1_b3_a_idx (never executed)
-               Index Cond: (a = (InitPlan 1).col1)
+               Index Cond: (a = (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 = (expr_1).col1)
+         Filter: (b = (expr_2).col1)
          ->  Bitmap Index Scan on ab_a2_b1_a_idx (never executed)
-               Index Cond: (a = (InitPlan 1).col1)
+               Index Cond: (a = (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 = (expr_1).col1)
+         Filter: (b = (expr_2).col1)
          ->  Bitmap Index Scan on ab_a2_b2_a_idx (never executed)
-               Index Cond: (a = (InitPlan 1).col1)
+               Index Cond: (a = (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 = (expr_1).col1)
+         Filter: (b = (expr_2).col1)
          ->  Bitmap Index Scan on ab_a2_b3_a_idx (never executed)
-               Index Cond: (a = (InitPlan 1).col1)
+               Index Cond: (a = (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 = (expr_1).col1)
+         Filter: (b = (expr_2).col1)
          ->  Bitmap Index Scan on ab_a3_b1_a_idx (never executed)
-               Index Cond: (a = (InitPlan 1).col1)
+               Index Cond: (a = (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 = (expr_1).col1)
+         Filter: (b = (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 = (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 = (expr_1).col1)
+         Filter: (b = (expr_2).col1)
          ->  Bitmap Index Scan on ab_a3_b3_a_idx (never executed)
-               Index Cond: (a = (InitPlan 1).col1)
+               Index Cond: (a = (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 = (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 = (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 = (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 = (expr_1).col1)
    ->  Seq Scan on ab_a1_b2 ab_2 (never executed)
-         Filter: (b = (InitPlan 1).col1)
+         Filter: (b = (expr_1).col1)
    ->  Seq Scan on ab_a1_b3 ab_3 (never executed)
-         Filter: (b = (InitPlan 1).col1)
+         Filter: (b = (expr_1).col1)
    ->  Seq Scan on ab_a2_b1 ab_4 (actual rows=0.00 loops=1)
-         Filter: (b = (InitPlan 1).col1)
+         Filter: (b = (expr_1).col1)
    ->  Seq Scan on ab_a2_b2 ab_5 (never executed)
-         Filter: (b = (InitPlan 1).col1)
+         Filter: (b = (expr_1).col1)
    ->  Seq Scan on ab_a2_b3 ab_6 (never executed)
-         Filter: (b = (InitPlan 1).col1)
+         Filter: (b = (expr_1).col1)
    ->  Seq Scan on ab_a3_b1 ab_7 (actual rows=0.00 loops=1)
-         Filter: (b = (InitPlan 1).col1)
+         Filter: (b = (expr_1).col1)
    ->  Seq Scan on ab_a3_b2 ab_8 (never executed)
-         Filter: (b = (InitPlan 1).col1)
+         Filter: (b = (expr_1).col1)
    ->  Seq Scan on ab_a3_b3 ab_9 (never executed)
-         Filter: (b = (InitPlan 1).col1)
+         Filter: (b = (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 = (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 = (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 = (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 = (expr_1).col1)
    ->  Seq Scan on ab_a1_b1 ab_1 (actual rows=0.00 loops=1)
-         Filter: (b = (InitPlan 1).col1)
+         Filter: (b = (expr_1).col1)
    ->  Seq Scan on ab_a1_b2 ab_2 (never executed)
-         Filter: (b = (InitPlan 1).col1)
+         Filter: (b = (expr_1).col1)
    ->  Seq Scan on ab_a1_b3 ab_3 (never executed)
-         Filter: (b = (InitPlan 1).col1)
+         Filter: (b = (expr_1).col1)
    ->  Seq Scan on ab_a2_b1 ab_4 (actual rows=0.00 loops=1)
-         Filter: (b = (InitPlan 1).col1)
+         Filter: (b = (expr_1).col1)
    ->  Seq Scan on ab_a2_b2 ab_5 (never executed)
-         Filter: (b = (InitPlan 1).col1)
+         Filter: (b = (expr_1).col1)
    ->  Seq Scan on ab_a2_b3 ab_6 (never executed)
-         Filter: (b = (InitPlan 1).col1)
+         Filter: (b = (expr_1).col1)
    ->  Seq Scan on ab_a3_b1 ab_7 (actual rows=0.00 loops=1)
-         Filter: (b = (InitPlan 1).col1)
+         Filter: (b = (expr_1).col1)
    ->  Seq Scan on ab_a3_b2 ab_8 (never executed)
-         Filter: (b = (InitPlan 1).col1)
+         Filter: (b = (expr_1).col1)
    ->  Seq Scan on ab_a3_b3 ab_9 (never executed)
-         Filter: (b = (InitPlan 1).col1)
+         Filter: (b = (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 = (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 = (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 = (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 = (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 = (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 = (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 = (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 = (expr_1).col1)
                      ->  Seq Scan on ab_a2_b2 ab_a2_2 (never executed)
-                           Filter: (b = (InitPlan 1).col1)
+                           Filter: (b = (expr_1).col1)
                      ->  Seq Scan on ab_a2_b3 ab_a2_3 (never executed)
-                           Filter: (b = (InitPlan 1).col1)
+                           Filter: (b = (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 = (expr_1).col1)
    ->  Seq Scan on listp_2_1 listp_2 (never executed)
-         Filter: (a = (InitPlan 1).col1)
+         Filter: (a = (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) < (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) < (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) < (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 = (expr_1).col1)
    ->  Seq Scan on boolp_t boolp_2 (actual rows=0.00 loops=1)
-         Filter: (a = (InitPlan 1).col1)
+         Filter: (a = (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 = (expr_1).col1)
    ->  Seq Scan on boolp_t boolp_2 (never executed)
-         Filter: (a = (InitPlan 1).col1)
+         Filter: (a = (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: Aggregate
-           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 >= (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 >= (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 >= (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 = (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 = (expr_1).col1))
    ->  Result
-         One-Time Filter: (1 = (InitPlan 1).col1)
+         One-Time Filter: (1 = (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 = (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 = (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 = (expr_1).col1))
 (10 rows)
 
 execute q (1, 1);
@@ -4113,8 +4113,8 @@ select * from listp where a = (select 2) and b <> 10;
                      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 = (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 = (expr_1).col1)
          ->  Parallel Seq Scan on listp_12_2 listp_2 (never executed)
-               Filter: (a = (InitPlan 1).col1)
+               Filter: (a = (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 = (expr_2).col1)
                ->  Parallel Seq Scan on listp_12_2 listp_2 (actual rows=N loops=N)
-                     Filter: (a = (InitPlan 2).col1)
+                     Filter: (a = (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 = (expr_1).col1)
                ->  Parallel Seq Scan on listp_12_2 listp_5 (never executed)
-                     Filter: (a = (InitPlan 1).col1)
+                     Filter: (a = (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[(expr_1).col1, (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[(expr_1).col1, (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[(expr_1).col1, (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[(expr_1).col1, (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 cd78cf11f51..023585b1776 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(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: (exists_1).col1
+   InitPlan exists_1
      ->  Result
            One-Time Filter: false
            Replaces: Join on t3, t4, t5, t6
diff --git a/src/test/regress/expected/returning.out b/src/test/regress/expected/returning.out
index 341b689f766..3df940ee8fc 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: (expr_1), (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
@@ -581,23 +581,23 @@ UPDATE foo SET f4 = 100 WHERE f1 = 5
                           QUERY PLAN                           
 ---------------------------------------------------------------
  Update on pg_temp.foo
-   Output: (SubPlan 1), (SubPlan 2), (SubPlan 3)
+   Output: (expr_1), (expr_2), (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: (expr_1), (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 e7fe3317638..4c60c25a476 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 <= (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 <= (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 <= (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 <= (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 <= (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 <= (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 <= (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 <= (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 <= (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 <= (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 <= (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 <= (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 <= (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 <= (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 <= (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 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 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 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 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 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 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 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 328b1e142c0..878baee2d59 100644
--- a/src/test/regress/expected/rowtypes.out
+++ b/src/test/regress/expected/rowtypes.out
@@ -1249,19 +1249,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: ((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: (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..3671d261f1f 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 any_1).col1) AND (four = (hashed 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 any_1).col1)))
+   SubPlan any_1
      ->  Seq Scan on tenk2
 (4 rows)
 
@@ -346,7 +346,7 @@ explain (costs off)
                       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 = (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 ((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 = (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 = (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(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: ((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..d660049f134 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: (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 db061ee95b1..2572af229b9 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 = (rowcompare_1).col1) AND (2 = (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 = (rowcompare_1).col1) AND (2 = (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 = (all_1).col1))
+   SubPlan all_1
      ->  Materialize
-           Output: ((InitPlan 1).col1)
-           InitPlan 1
+           Output: ((expr_1).col1)
+           InitPlan expr_1
              ->  Result
                    Output: 1
            ->  Result
-                 Output: (InitPlan 1).col1
+                 Output: (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(exists_1)
+   SubPlan exists_1
      ->  Limit
            ->  Seq Scan on int4_tbl i
                  Filter: (f1 = o.f1)
@@ -1097,11 +1097,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 any_1).col1))
+   SubPlan any_1
      ->  Append
            ->  Result
                  Output: 'bar'::name
@@ -1121,11 +1121,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 = (any_1).col1))
+   SubPlan any_1
      ->  Materialize
            Output: '("(1)")'::record
            ->  Result
@@ -1164,11 +1164,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 any_1).col1))
+   SubPlan any_1
      ->  Seq Scan on inner_text
 (4 rows)
 
@@ -1185,11 +1185,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 any_1).col1) AND ((q1)::text = (hashed any_1).col1)))
+   SubPlan any_1
      ->  Seq Scan on inner_text
 (4 rows)
 
@@ -1206,11 +1206,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 ((any_1).col1 = (q1)::text))
+   SubPlan any_1
      ->  Materialize
            ->  Seq Scan on inner_text
 (5 rows)
@@ -1229,12 +1229,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 exists_2).col1)) OR (ten < 0))
+         SubPlan exists_2
            ->  Index Only Scan using tenk1_unique1 on tenk1 k
 (5 rows)
 
@@ -1254,10 +1254,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(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)
@@ -1279,20 +1279,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(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 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
@@ -1328,14 +1328,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: (expr_1).col1, (expr_2).col1
+   InitPlan expr_1
      ->  Result
            Output: now()
-   InitPlan 2
+   InitPlan expr_2
      ->  Result
            Output: now()
 (8 rows)
@@ -1343,13 +1343,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: (expr_1).col1
+         InitPlan expr_1
            ->  Result
                  Output: random()
 (7 rows)
@@ -1360,12 +1360,12 @@ explain (verbose, costs off)
                               QUERY PLAN                              
 ----------------------------------------------------------------------
  Values Scan on "*VALUES*"
-   Output: (SubPlan 1), (SubPlan 2)
-   SubPlan 1
+   Output: (expr_1), (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)
@@ -1379,8 +1379,8 @@ explain (verbose, costs off)
  Subquery Scan on ss
    Output: ss.x, ss.x
    ->  Values Scan on "*VALUES*"
-         Output: (SubPlan 1)
-         SubPlan 1
+         Output: (expr_1)
+         SubPlan expr_1
            ->  Result
                  Output: random()
                  One-Time Filter: ("*VALUES*".column1 = "*VALUES*".column1)
@@ -1400,16 +1400,16 @@ where o.ten = 0;
                                                                                          QUERY PLAN                                                                                          
 ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  Aggregate
-   Output: sum((((ANY (i.ten = (hashed SubPlan 1).col1))))::integer)
+   Output: sum((((ANY (i.ten = (hashed any_1).col1))))::integer)
    ->  Nested Loop
-         Output: ((ANY (i.ten = (hashed SubPlan 1).col1)))
+         Output: ((ANY (i.ten = (hashed 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 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)
@@ -1618,7 +1618,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)
@@ -1645,12 +1645,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 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)
@@ -2749,14 +2749,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 = (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)
@@ -2766,14 +2766,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 = (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)
@@ -2783,14 +2783,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 = (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)
@@ -2852,7 +2852,7 @@ ON B.hundred in (SELECT min(c.hundred) FROM tenk2 C WHERE c.odd = b.odd);
                            Filter: (b.hundred = "ANY_subquery".min)
                            ->  Result
                                  Replaces: Aggregate
-                                 InitPlan 1
+                                 InitPlan minmax_1
                                    ->  Limit
                                          ->  Index Scan using tenk2_hundred on tenk2 c
                                                Index Cond: (hundred IS NOT NULL)
@@ -3093,7 +3093,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)
@@ -3109,7 +3109,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
@@ -3128,7 +3128,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..66747f8af82 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)
@@ -3170,18 +3170,18 @@ EXPLAIN (costs off) INSERT INTO rw_view1 VALUES (2, 'New row 2');
                         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: ((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: (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(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(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(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(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(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(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(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(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: (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..a0aac9d4377 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: (multiexpr_1).col1, (multiexpr_1).col2, (rescan 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..55719226bef 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((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 26c88505140..4156105685f 100644
--- a/src/test/regress/expected/with.out
+++ b/src/test/regress/expected/with.out
@@ -3168,7 +3168,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
@@ -3200,7 +3200,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
@@ -3243,11 +3243,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.39.5 (Apple Git-154)

v3-0005-Store-information-about-range-table-flattening-in.patchapplication/octet-stream; name=v3-0005-Store-information-about-range-table-flattening-in.patchDownload
From 616190bebf3b34e3846adf5736ad8686b73e5f26 Mon Sep 17 00:00:00 2001
From: Robert Haas <rhaas@postgresql.org>
Date: Fri, 21 Mar 2025 11:06:35 -0400
Subject: [PATCH v3 5/7] Store information about range-table flattening in the
 final plan.

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.  So,
this commit remembers in the final plan which part of the final range
table came from which subquery's range table.

Additionally, this commit teaches pg_overexplain'e RANGE_TABLE option
to display the subquery name for each range table entry.
---
 contrib/pg_overexplain/pg_overexplain.c | 36 +++++++++++++++++++++++++
 src/backend/optimizer/plan/planner.c    |  1 +
 src/backend/optimizer/plan/setrefs.c    | 20 ++++++++++++++
 src/include/nodes/pathnodes.h           |  3 +++
 src/include/nodes/plannodes.h           | 17 ++++++++++++
 src/tools/pgindent/typedefs.list        |  1 +
 6 files changed, 78 insertions(+)

diff --git a/contrib/pg_overexplain/pg_overexplain.c b/contrib/pg_overexplain/pg_overexplain.c
index bd70b6d9d5e..5dc707d69e3 100644
--- a/contrib/pg_overexplain/pg_overexplain.c
+++ b/contrib/pg_overexplain/pg_overexplain.c
@@ -395,6 +395,8 @@ static void
 overexplain_range_table(PlannedStmt *plannedstmt, ExplainState *es)
 {
 	Index		rti;
+	ListCell   *lc_subrtinfo = list_head(plannedstmt->subrtinfos);
+	SubPlanRTInfo *rtinfo = NULL;
 
 	/* Open group, one entry per RangeTblEntry */
 	ExplainOpenGroup("Range Table", "Range Table", false, es);
@@ -405,6 +407,18 @@ overexplain_range_table(PlannedStmt *plannedstmt, ExplainState *es)
 		RangeTblEntry *rte = rt_fetch(rti, plannedstmt->rtable);
 		char	   *kind = NULL;
 		char	   *relkind;
+		SubPlanRTInfo *next_rtinfo;
+
+		/* Advance to next SubRTInfo, if it's time. */
+		if (lc_subrtinfo != NULL)
+		{
+			next_rtinfo = lfirst(lc_subrtinfo);
+			if (rti > next_rtinfo->rtoffset)
+			{
+				rtinfo = next_rtinfo;
+				lc_subrtinfo = lnext(plannedstmt->subrtinfos, lc_subrtinfo);
+			}
+		}
 
 		/* NULL entries are possible; skip them */
 		if (rte == NULL)
@@ -469,6 +483,28 @@ overexplain_range_table(PlannedStmt *plannedstmt, ExplainState *es)
 			ExplainPropertyBool("In From Clause", rte->inFromCl, es);
 		}
 
+		/*
+		 * Indicate which subplan is the origin of which RTE. Note dummy
+		 * subplans. Here again, we crunch more onto one line in text format.
+		 */
+		if (rtinfo != NULL)
+		{
+			if (es->format == EXPLAIN_FORMAT_TEXT)
+			{
+				if (!rtinfo->dummy)
+					ExplainPropertyText("Subplan", rtinfo->plan_name, es);
+				else
+					ExplainPropertyText("Subplan",
+										psprintf("%s (dummy)",
+												 rtinfo->plan_name), es);
+			}
+			else
+			{
+				ExplainPropertyText("Subplan", rtinfo->plan_name, es);
+				ExplainPropertyBool("Subplan Is Dummy", rtinfo->dummy, es);
+			}
+		}
+
 		/* rte->alias is optional; rte->eref is requested */
 		if (rte->alias != NULL)
 			overexplain_alias("Alias", rte->alias, es);
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index 988bbd19ab4..59ee3f319f4 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -579,6 +579,7 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions,
 	result->unprunableRelids = bms_difference(glob->allRelids,
 											  glob->prunableRelids);
 	result->permInfos = glob->finalrteperminfos;
+	result->subrtinfos = glob->subrtinfos;
 	result->resultRelations = glob->resultRelations;
 	result->appendRelations = glob->appendRelations;
 	result->subplans = glob->subplans;
diff --git a/src/backend/optimizer/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.c
index 6950eff2c5b..eef43792aeb 100644
--- a/src/backend/optimizer/plan/setrefs.c
+++ b/src/backend/optimizer/plan/setrefs.c
@@ -399,6 +399,26 @@ add_rtes_to_flat_rtable(PlannerInfo *root, bool recursing)
 	Index		rti;
 	ListCell   *lc;
 
+	/*
+	 * Record enough information to make it possible for code that looks at
+	 * the final range table to understand how it was constructed. (If
+	 * finalrtable is still NIL, then this is the very topmost PlannerInfo,
+	 * which will always have plan_name == NULL and rtoffset == 0; we omit the
+	 * degenerate list entry.)
+	 */
+	if (root->glob->finalrtable != NIL)
+	{
+		SubPlanRTInfo *rtinfo = makeNode(SubPlanRTInfo);
+
+		rtinfo->plan_name = root->plan_name;
+		rtinfo->rtoffset = list_length(root->glob->finalrtable);
+
+		/* When recursing = true, it's an unplanned or dummy subquery. */
+		rtinfo->dummy = recursing;
+
+		root->glob->subrtinfos = lappend(root->glob->subrtinfos, rtinfo);
+	}
+
 	/*
 	 * Add the query's own RTEs to the flattened rangetable.
 	 *
diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h
index e41597acb02..4f8586f6591 100644
--- a/src/include/nodes/pathnodes.h
+++ b/src/include/nodes/pathnodes.h
@@ -135,6 +135,9 @@ typedef struct PlannerGlobal
 	/* "flat" list of RTEPermissionInfos */
 	List	   *finalrteperminfos;
 
+	/* list of SubPlanRTInfo nodes */
+	List	   *subrtinfos;
+
 	/* "flat" list of PlanRowMarks */
 	List	   *finalrowmarks;
 
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index 1abcf90be43..3f2d6fafc24 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -131,6 +131,9 @@ typedef struct PlannedStmt
 	 */
 	List	   *subplans;
 
+	/* a list of SubPlanRTInfo objects */
+	List	   *subrtinfos;
+
 	/* indices of subplans that require REWIND */
 	Bitmapset  *rewindPlanIDs;
 
@@ -1801,4 +1804,18 @@ typedef enum MonotonicFunction
 	MONOTONICFUNC_BOTH = MONOTONICFUNC_INCREASING | MONOTONICFUNC_DECREASING,
 } MonotonicFunction;
 
+/*
+ * SubPlanRTInfo
+ *
+ * Information about which range table entries came from which subquery
+ * planning cycles.
+ */
+typedef struct SubPlanRTInfo
+{
+	NodeTag		type;
+	char	   *plan_name;
+	Index		rtoffset;
+	bool		dummy;
+} SubPlanRTInfo;
+
 #endif							/* PLANNODES_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index a13e8162890..8e4367dabaf 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -4347,3 +4347,4 @@ yyscan_t
 z_stream
 z_streamp
 zic_t
+SubPlanRTInfo
-- 
2.39.5 (Apple Git-154)

v3-0007-Store-information-about-Append-node-consolidation.patchapplication/octet-stream; name=v3-0007-Store-information-about-Append-node-consolidation.patchDownload
From f32f7b3ae90a20b69d977645f6606721350152f6 Mon Sep 17 00:00:00 2001
From: Robert Haas <rhaas@postgresql.org>
Date: Mon, 21 Jul 2025 15:49:06 -0400
Subject: [PATCH v3 7/7] Store information about Append node consolidation in
 the final plan.

When we build an AppendPath or MergeAppendPath, we sometimes pull all
child paths from a subordinate AppendPath or MergeAppendPath instead
of having one such path atop another. This results in the RTIs that
would have been associated with the subordinate path disappearing
from the final plan, making things difficult for code that wants
to scrutinize the final plan and extract information from it about
what happened during the planning process.

To avoid this, propagate the RTI sets that would have been present
in the 'apprelids' field of the subordinate Append or MergeAppend
nodes that would have been created into the surviving Append or
MergeAppend node, using a new 'child_append_relid_sets' field for
that purpose.

This commit also updates pg_overexplain to display these details.
---
 .../expected/pg_overexplain.out               |  4 +-
 contrib/pg_overexplain/pg_overexplain.c       | 56 +++++++++++
 src/backend/optimizer/path/allpaths.c         | 98 +++++++++++++++----
 src/backend/optimizer/path/joinrels.c         |  2 +-
 src/backend/optimizer/plan/createplan.c       |  2 +
 src/backend/optimizer/plan/planner.c          |  1 +
 src/backend/optimizer/prep/prepunion.c        |  5 +-
 src/backend/optimizer/util/pathnode.c         |  5 +
 src/include/nodes/pathnodes.h                 | 10 ++
 src/include/nodes/plannodes.h                 | 11 +++
 src/include/optimizer/pathnode.h              |  2 +
 11 files changed, 171 insertions(+), 25 deletions(-)

diff --git a/contrib/pg_overexplain/expected/pg_overexplain.out b/contrib/pg_overexplain/expected/pg_overexplain.out
index ca9a23ea61f..a377fb2571d 100644
--- a/contrib/pg_overexplain/expected/pg_overexplain.out
+++ b/contrib/pg_overexplain/expected/pg_overexplain.out
@@ -104,6 +104,7 @@ $$);
                Parallel Safe: true
                Plan Node ID: 2
                Append RTIs: 1
+               Child Append RTIs: none
                ->  Seq Scan on brassica vegetables_1
                      Disabled Nodes: 0
                      Parallel Safe: true
@@ -142,7 +143,7 @@ $$);
    Relation Kind: relation
    Relation Lock Mode: AccessShareLock
  Unprunable RTIs: 1 3 4
-(53 rows)
+(54 rows)
 
 -- Test a different output format.
 SELECT explain_filter($$
@@ -197,6 +198,7 @@ $$);
                <extParam>none</extParam>                            +
                <allParam>none</allParam>                            +
                <Append-RTIs>1</Append-RTIs>                         +
+               <Child-Append-RTIs>none</Child-Append-RTIs>          +
                <Subplans-Removed>0</Subplans-Removed>               +
                <Plans>                                              +
                  <Plan>                                             +
diff --git a/contrib/pg_overexplain/pg_overexplain.c b/contrib/pg_overexplain/pg_overexplain.c
index fa907fa472e..6538ffcafb0 100644
--- a/contrib/pg_overexplain/pg_overexplain.c
+++ b/contrib/pg_overexplain/pg_overexplain.c
@@ -54,6 +54,8 @@ static void overexplain_alias(const char *qlabel, Alias *alias,
 							  ExplainState *es);
 static void overexplain_bitmapset(const char *qlabel, Bitmapset *bms,
 								  ExplainState *es);
+static void overexplain_bitmapset_list(const char *qlabel, List *bms_list,
+									   ExplainState *es);
 static void overexplain_intlist(const char *qlabel, List *list,
 								ExplainState *es);
 
@@ -232,11 +234,17 @@ overexplain_per_node_hook(PlanState *planstate, List *ancestors,
 				overexplain_bitmapset("Append RTIs",
 									  ((Append *) plan)->apprelids,
 									  es);
+				overexplain_bitmapset_list("Child Append RTIs",
+										   ((Append *) plan)->child_append_relid_sets,
+										   es);
 				break;
 			case T_MergeAppend:
 				overexplain_bitmapset("Append RTIs",
 									  ((MergeAppend *) plan)->apprelids,
 									  es);
+				overexplain_bitmapset_list("Child Append RTIs",
+										   ((MergeAppend *) plan)->child_append_relid_sets,
+										   es);
 				break;
 			case T_Result:
 
@@ -815,6 +823,54 @@ overexplain_bitmapset(const char *qlabel, Bitmapset *bms, ExplainState *es)
 	pfree(buf.data);
 }
 
+/*
+ * Emit a text property describing the contents of a list of bitmapsets.
+ * If a bitmapset contains exactly 1 member, we just print an integer;
+ * otherwise, we surround the list of members by parentheses.
+ *
+ * If there are no bitmapsets in the list, we print the word "none".
+ */
+static void
+overexplain_bitmapset_list(const char *qlabel, List *bms_list,
+						   ExplainState *es)
+{
+	StringInfoData buf;
+
+	initStringInfo(&buf);
+
+	foreach_node(Bitmapset, bms, bms_list)
+	{
+		if (bms_membership(bms) == BMS_SINGLETON)
+			appendStringInfo(&buf, " %d", bms_singleton_member(bms));
+		else
+		{
+			int			x = -1;
+			bool		first = true;
+
+			appendStringInfoString(&buf, " (");
+			while ((x = bms_next_member(bms, x)) >= 0)
+			{
+				if (first)
+					first = false;
+				else
+					appendStringInfoChar(&buf, ' ');
+				appendStringInfo(&buf, "%d", x);
+			}
+			appendStringInfoChar(&buf, ')');
+		}
+	}
+
+	if (buf.len == 0)
+	{
+		ExplainPropertyText(qlabel, "none", es);
+		return;
+	}
+
+	Assert(buf.data[0] == ' ');
+	ExplainPropertyText(qlabel, buf.data + 1, es);
+	pfree(buf.data);
+}
+
 /*
  * Emit a text property describing the contents of a list of integers, OIDs,
  * or XIDs -- either a space-separated list of integer members, or the word
diff --git a/src/backend/optimizer/path/allpaths.c b/src/backend/optimizer/path/allpaths.c
index 593f5361b58..76b8c3fd7c6 100644
--- a/src/backend/optimizer/path/allpaths.c
+++ b/src/backend/optimizer/path/allpaths.c
@@ -122,8 +122,10 @@ static Path *get_cheapest_parameterized_child_path(PlannerInfo *root,
 												   Relids required_outer);
 static void accumulate_append_subpath(Path *path,
 									  List **subpaths,
-									  List **special_subpaths);
-static Path *get_singleton_append_subpath(Path *path);
+									  List **special_subpaths,
+									  List **child_append_relid_sets);
+static Path *get_singleton_append_subpath(Path *path,
+										  List **child_append_relid_sets);
 static void set_dummy_rel_pathlist(RelOptInfo *rel);
 static void set_subquery_pathlist(PlannerInfo *root, RelOptInfo *rel,
 								  Index rti, RangeTblEntry *rte);
@@ -1323,11 +1325,15 @@ add_paths_to_append_rel(PlannerInfo *root, RelOptInfo *rel,
 {
 	List	   *subpaths = NIL;
 	bool		subpaths_valid = true;
+	List	   *subpath_cars = NIL;
 	List	   *startup_subpaths = NIL;
 	bool		startup_subpaths_valid = true;
+	List	   *startup_subpath_cars = NIL;
 	List	   *partial_subpaths = NIL;
+	List	   *partial_subpath_cars = NIL;
 	List	   *pa_partial_subpaths = NIL;
 	List	   *pa_nonpartial_subpaths = NIL;
+	List	   *pa_subpath_cars = NIL;
 	bool		partial_subpaths_valid = true;
 	bool		pa_subpaths_valid;
 	List	   *all_child_pathkeys = NIL;
@@ -1360,7 +1366,7 @@ add_paths_to_append_rel(PlannerInfo *root, RelOptInfo *rel,
 		if (childrel->pathlist != NIL &&
 			childrel->cheapest_total_path->param_info == NULL)
 			accumulate_append_subpath(childrel->cheapest_total_path,
-									  &subpaths, NULL);
+									  &subpaths, NULL, &subpath_cars);
 		else
 			subpaths_valid = false;
 
@@ -1389,7 +1395,8 @@ add_paths_to_append_rel(PlannerInfo *root, RelOptInfo *rel,
 			Assert(cheapest_path->param_info == NULL);
 			accumulate_append_subpath(cheapest_path,
 									  &startup_subpaths,
-									  NULL);
+									  NULL,
+									  &startup_subpath_cars);
 		}
 		else
 			startup_subpaths_valid = false;
@@ -1400,7 +1407,8 @@ add_paths_to_append_rel(PlannerInfo *root, RelOptInfo *rel,
 		{
 			cheapest_partial_path = linitial(childrel->partial_pathlist);
 			accumulate_append_subpath(cheapest_partial_path,
-									  &partial_subpaths, NULL);
+									  &partial_subpaths, NULL,
+									  &partial_subpath_cars);
 		}
 		else
 			partial_subpaths_valid = false;
@@ -1429,7 +1437,8 @@ add_paths_to_append_rel(PlannerInfo *root, RelOptInfo *rel,
 				Assert(cheapest_partial_path != NULL);
 				accumulate_append_subpath(cheapest_partial_path,
 										  &pa_partial_subpaths,
-										  &pa_nonpartial_subpaths);
+										  &pa_nonpartial_subpaths,
+										  &pa_subpath_cars);
 			}
 			else
 			{
@@ -1448,7 +1457,8 @@ add_paths_to_append_rel(PlannerInfo *root, RelOptInfo *rel,
 				 */
 				accumulate_append_subpath(nppath,
 										  &pa_nonpartial_subpaths,
-										  NULL);
+										  NULL,
+										  &pa_subpath_cars);
 			}
 		}
 
@@ -1523,14 +1533,16 @@ add_paths_to_append_rel(PlannerInfo *root, RelOptInfo *rel,
 	 * if we have zero or one live subpath due to constraint exclusion.)
 	 */
 	if (subpaths_valid)
-		add_path(rel, (Path *) create_append_path(root, rel, subpaths, NIL,
+		add_path(rel, (Path *) create_append_path(root, rel, subpaths,
+												  NIL, subpath_cars,
 												  NIL, NULL, 0, false,
 												  -1));
 
 	/* build an AppendPath for the cheap startup paths, if valid */
 	if (startup_subpaths_valid)
 		add_path(rel, (Path *) create_append_path(root, rel, startup_subpaths,
-												  NIL, NIL, NULL, 0, false, -1));
+												  NIL, startup_subpath_cars,
+												  NIL, NULL, 0, false, -1));
 
 	/*
 	 * Consider an append of unordered, unparameterized partial paths.  Make
@@ -1571,6 +1583,7 @@ add_paths_to_append_rel(PlannerInfo *root, RelOptInfo *rel,
 
 		/* Generate a partial append path. */
 		appendpath = create_append_path(root, rel, NIL, partial_subpaths,
+										partial_subpath_cars,
 										NIL, NULL, parallel_workers,
 										enable_parallel_append,
 										-1);
@@ -1621,6 +1634,7 @@ add_paths_to_append_rel(PlannerInfo *root, RelOptInfo *rel,
 
 		appendpath = create_append_path(root, rel, pa_nonpartial_subpaths,
 										pa_partial_subpaths,
+										pa_subpath_cars,
 										NIL, NULL, parallel_workers, true,
 										partial_rows);
 		add_partial_path(rel, (Path *) appendpath);
@@ -1654,6 +1668,7 @@ add_paths_to_append_rel(PlannerInfo *root, RelOptInfo *rel,
 
 		/* Select the child paths for an Append with this parameterization */
 		subpaths = NIL;
+		subpath_cars = NIL;
 		subpaths_valid = true;
 		foreach(lcr, live_childrels)
 		{
@@ -1676,12 +1691,13 @@ add_paths_to_append_rel(PlannerInfo *root, RelOptInfo *rel,
 				subpaths_valid = false;
 				break;
 			}
-			accumulate_append_subpath(subpath, &subpaths, NULL);
+			accumulate_append_subpath(subpath, &subpaths, NULL,
+									  &subpath_cars);
 		}
 
 		if (subpaths_valid)
 			add_path(rel, (Path *)
-					 create_append_path(root, rel, subpaths, NIL,
+					 create_append_path(root, rel, subpaths, NIL, subpath_cars,
 										NIL, required_outer, 0, false,
 										-1));
 	}
@@ -1708,6 +1724,7 @@ add_paths_to_append_rel(PlannerInfo *root, RelOptInfo *rel,
 				continue;
 
 			appendpath = create_append_path(root, rel, NIL, list_make1(path),
+											list_make1(rel->relids),
 											NIL, NULL,
 											path->parallel_workers, true,
 											partial_rows);
@@ -1789,8 +1806,11 @@ generate_orderedappend_paths(PlannerInfo *root, RelOptInfo *rel,
 	{
 		List	   *pathkeys = (List *) lfirst(lcp);
 		List	   *startup_subpaths = NIL;
+		List	   *startup_subpath_cars = NIL;
 		List	   *total_subpaths = NIL;
+		List	   *total_subpath_cars = NIL;
 		List	   *fractional_subpaths = NIL;
+		List	   *fractional_subpath_cars = NIL;
 		bool		startup_neq_total = false;
 		bool		match_partition_order;
 		bool		match_partition_order_desc;
@@ -1942,16 +1962,23 @@ generate_orderedappend_paths(PlannerInfo *root, RelOptInfo *rel,
 				 * just a single subpath (and hence aren't doing anything
 				 * useful).
 				 */
-				cheapest_startup = get_singleton_append_subpath(cheapest_startup);
-				cheapest_total = get_singleton_append_subpath(cheapest_total);
+				cheapest_startup =
+					get_singleton_append_subpath(cheapest_startup,
+												 &startup_subpath_cars);
+				cheapest_total =
+					get_singleton_append_subpath(cheapest_total,
+												 &total_subpath_cars);
 
 				startup_subpaths = lappend(startup_subpaths, cheapest_startup);
 				total_subpaths = lappend(total_subpaths, cheapest_total);
 
 				if (cheapest_fractional)
 				{
-					cheapest_fractional = get_singleton_append_subpath(cheapest_fractional);
-					fractional_subpaths = lappend(fractional_subpaths, cheapest_fractional);
+					cheapest_fractional =
+						get_singleton_append_subpath(cheapest_fractional,
+													 &fractional_subpath_cars);
+					fractional_subpaths =
+						lappend(fractional_subpaths, cheapest_fractional);
 				}
 			}
 			else
@@ -1961,13 +1988,16 @@ generate_orderedappend_paths(PlannerInfo *root, RelOptInfo *rel,
 				 * child paths for the MergeAppend.
 				 */
 				accumulate_append_subpath(cheapest_startup,
-										  &startup_subpaths, NULL);
+										  &startup_subpaths, NULL,
+										  &startup_subpath_cars);
 				accumulate_append_subpath(cheapest_total,
-										  &total_subpaths, NULL);
+										  &total_subpaths, NULL,
+										  &total_subpath_cars);
 
 				if (cheapest_fractional)
 					accumulate_append_subpath(cheapest_fractional,
-											  &fractional_subpaths, NULL);
+											  &fractional_subpaths, NULL,
+											  &fractional_subpath_cars);
 			}
 		}
 
@@ -1979,6 +2009,7 @@ generate_orderedappend_paths(PlannerInfo *root, RelOptInfo *rel,
 													  rel,
 													  startup_subpaths,
 													  NIL,
+													  startup_subpath_cars,
 													  pathkeys,
 													  NULL,
 													  0,
@@ -1989,6 +2020,7 @@ generate_orderedappend_paths(PlannerInfo *root, RelOptInfo *rel,
 														  rel,
 														  total_subpaths,
 														  NIL,
+														  total_subpath_cars,
 														  pathkeys,
 														  NULL,
 														  0,
@@ -2000,6 +2032,7 @@ generate_orderedappend_paths(PlannerInfo *root, RelOptInfo *rel,
 														  rel,
 														  fractional_subpaths,
 														  NIL,
+														  fractional_subpath_cars,
 														  pathkeys,
 														  NULL,
 														  0,
@@ -2012,12 +2045,14 @@ generate_orderedappend_paths(PlannerInfo *root, RelOptInfo *rel,
 			add_path(rel, (Path *) create_merge_append_path(root,
 															rel,
 															startup_subpaths,
+															startup_subpath_cars,
 															pathkeys,
 															NULL));
 			if (startup_neq_total)
 				add_path(rel, (Path *) create_merge_append_path(root,
 																rel,
 																total_subpaths,
+																total_subpath_cars,
 																pathkeys,
 																NULL));
 
@@ -2025,6 +2060,7 @@ generate_orderedappend_paths(PlannerInfo *root, RelOptInfo *rel,
 				add_path(rel, (Path *) create_merge_append_path(root,
 																rel,
 																fractional_subpaths,
+																fractional_subpath_cars,
 																pathkeys,
 																NULL));
 		}
@@ -2127,7 +2163,8 @@ get_cheapest_parameterized_child_path(PlannerInfo *root, RelOptInfo *rel,
  * paths).
  */
 static void
-accumulate_append_subpath(Path *path, List **subpaths, List **special_subpaths)
+accumulate_append_subpath(Path *path, List **subpaths, List **special_subpaths,
+						  List **child_append_relid_sets)
 {
 	if (IsA(path, AppendPath))
 	{
@@ -2136,6 +2173,8 @@ accumulate_append_subpath(Path *path, List **subpaths, List **special_subpaths)
 		if (!apath->path.parallel_aware || apath->first_partial_path == 0)
 		{
 			*subpaths = list_concat(*subpaths, apath->subpaths);
+			*child_append_relid_sets =
+				lappend(*child_append_relid_sets, path->parent->relids);
 			return;
 		}
 		else if (special_subpaths != NULL)
@@ -2150,6 +2189,8 @@ accumulate_append_subpath(Path *path, List **subpaths, List **special_subpaths)
 												  apath->first_partial_path);
 			*special_subpaths = list_concat(*special_subpaths,
 											new_special_subpaths);
+			*child_append_relid_sets =
+				lappend(*child_append_relid_sets, path->parent->relids);
 			return;
 		}
 	}
@@ -2158,6 +2199,8 @@ accumulate_append_subpath(Path *path, List **subpaths, List **special_subpaths)
 		MergeAppendPath *mpath = (MergeAppendPath *) path;
 
 		*subpaths = list_concat(*subpaths, mpath->subpaths);
+		*child_append_relid_sets =
+			lappend(*child_append_relid_sets, path->parent->relids);
 		return;
 	}
 
@@ -2169,10 +2212,15 @@ accumulate_append_subpath(Path *path, List **subpaths, List **special_subpaths)
  *		Returns the single subpath of an Append/MergeAppend, or just
  *		return 'path' if it's not a single sub-path Append/MergeAppend.
  *
+ * As a side effect, whenever we return a single subpath rather than the
+ * original path, add the relid set for the original path to
+ * child_append_relid_sets, so that those relids don't entirely disappear
+ * from the final plan.
+ *
  * Note: 'path' must not be a parallel-aware path.
  */
 static Path *
-get_singleton_append_subpath(Path *path)
+get_singleton_append_subpath(Path *path, List **child_append_relid_sets)
 {
 	Assert(!path->parallel_aware);
 
@@ -2181,14 +2229,22 @@ get_singleton_append_subpath(Path *path)
 		AppendPath *apath = (AppendPath *) path;
 
 		if (list_length(apath->subpaths) == 1)
+		{
+			*child_append_relid_sets =
+				lappend(*child_append_relid_sets, path->parent->relids);
 			return (Path *) linitial(apath->subpaths);
+		}
 	}
 	else if (IsA(path, MergeAppendPath))
 	{
 		MergeAppendPath *mpath = (MergeAppendPath *) path;
 
 		if (list_length(mpath->subpaths) == 1)
+		{
+			*child_append_relid_sets =
+				lappend(*child_append_relid_sets, path->parent->relids);
 			return (Path *) linitial(mpath->subpaths);
+		}
 	}
 
 	return path;
@@ -2217,7 +2273,7 @@ set_dummy_rel_pathlist(RelOptInfo *rel)
 	rel->partial_pathlist = NIL;
 
 	/* Set up the dummy path */
-	add_path(rel, (Path *) create_append_path(NULL, rel, NIL, NIL,
+	add_path(rel, (Path *) create_append_path(NULL, rel, NIL, NIL, NIL,
 											  NIL, rel->lateral_relids,
 											  0, false, -1));
 
diff --git a/src/backend/optimizer/path/joinrels.c b/src/backend/optimizer/path/joinrels.c
index 535248aa525..6bddfc537d2 100644
--- a/src/backend/optimizer/path/joinrels.c
+++ b/src/backend/optimizer/path/joinrels.c
@@ -1342,7 +1342,7 @@ mark_dummy_rel(RelOptInfo *rel)
 
 	/* Set up the dummy path */
 	add_path(rel, (Path *) create_append_path(NULL, rel, NIL, NIL,
-											  NIL, rel->lateral_relids,
+											  NIL, NIL, rel->lateral_relids,
 											  0, false, -1));
 
 	/* Set or update cheapest_total_path and related fields */
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index e9237490eef..f4be0300bb6 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -1282,6 +1282,7 @@ create_append_plan(PlannerInfo *root, AppendPath *best_path, int flags)
 	plan->plan.lefttree = NULL;
 	plan->plan.righttree = NULL;
 	plan->apprelids = rel->relids;
+	plan->child_append_relid_sets = best_path->child_append_relid_sets;
 
 	if (pathkeys != NIL)
 	{
@@ -1494,6 +1495,7 @@ create_merge_append_plan(PlannerInfo *root, MergeAppendPath *best_path,
 	plan->lefttree = NULL;
 	plan->righttree = NULL;
 	node->apprelids = rel->relids;
+	node->child_append_relid_sets = best_path->child_append_relid_sets;
 
 	/*
 	 * Compute sort column info, and adjust MergeAppend's tlist as needed.
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index fef57d84bb3..9678734d34c 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -3979,6 +3979,7 @@ create_degenerate_grouping_paths(PlannerInfo *root, RelOptInfo *input_rel,
 							   paths,
 							   NIL,
 							   NIL,
+							   NIL,
 							   NULL,
 							   0,
 							   false,
diff --git a/src/backend/optimizer/prep/prepunion.c b/src/backend/optimizer/prep/prepunion.c
index d55eb39e552..5b88514bb79 100644
--- a/src/backend/optimizer/prep/prepunion.c
+++ b/src/backend/optimizer/prep/prepunion.c
@@ -815,7 +815,7 @@ generate_union_paths(SetOperationStmt *op, PlannerInfo *root,
 	 * union child.
 	 */
 	apath = (Path *) create_append_path(root, result_rel, cheapest_pathlist,
-										NIL, NIL, NULL, 0, false, -1);
+										NIL, NIL, NIL, NULL, 0, false, -1);
 
 	/*
 	 * Estimate number of groups.  For now we just assume the output is unique
@@ -861,7 +861,7 @@ generate_union_paths(SetOperationStmt *op, PlannerInfo *root,
 
 		papath = (Path *)
 			create_append_path(root, result_rel, NIL, partial_pathlist,
-							   NIL, NULL, parallel_workers,
+							   NIL, NIL, NULL, parallel_workers,
 							   enable_parallel_append, -1);
 		gpath = (Path *)
 			create_gather_path(root, result_rel, papath,
@@ -969,6 +969,7 @@ generate_union_paths(SetOperationStmt *op, PlannerInfo *root,
 			path = (Path *) create_merge_append_path(root,
 													 result_rel,
 													 ordered_pathlist,
+													 NIL,
 													 union_pathkeys,
 													 NULL);
 
diff --git a/src/backend/optimizer/util/pathnode.c b/src/backend/optimizer/util/pathnode.c
index b0da28150d3..b4546c8842d 100644
--- a/src/backend/optimizer/util/pathnode.c
+++ b/src/backend/optimizer/util/pathnode.c
@@ -1298,6 +1298,7 @@ AppendPath *
 create_append_path(PlannerInfo *root,
 				   RelOptInfo *rel,
 				   List *subpaths, List *partial_subpaths,
+				   List *child_append_relid_sets,
 				   List *pathkeys, Relids required_outer,
 				   int parallel_workers, bool parallel_aware,
 				   double rows)
@@ -1307,6 +1308,7 @@ create_append_path(PlannerInfo *root,
 
 	Assert(!parallel_aware || parallel_workers > 0);
 
+	pathnode->child_append_relid_sets = child_append_relid_sets;
 	pathnode->path.pathtype = T_Append;
 	pathnode->path.parent = rel;
 	pathnode->path.pathtarget = rel->reltarget;
@@ -1469,6 +1471,7 @@ MergeAppendPath *
 create_merge_append_path(PlannerInfo *root,
 						 RelOptInfo *rel,
 						 List *subpaths,
+						 List *child_append_relid_sets,
 						 List *pathkeys,
 						 Relids required_outer)
 {
@@ -1484,6 +1487,7 @@ create_merge_append_path(PlannerInfo *root,
 	 */
 	Assert(bms_is_empty(rel->lateral_relids) && bms_is_empty(required_outer));
 
+	pathnode->child_append_relid_sets = child_append_relid_sets;
 	pathnode->path.pathtype = T_MergeAppend;
 	pathnode->path.parent = rel;
 	pathnode->path.pathtarget = rel->reltarget;
@@ -3951,6 +3955,7 @@ reparameterize_path(PlannerInfo *root, Path *path,
 				}
 				return (Path *)
 					create_append_path(root, rel, childpaths, partialpaths,
+									   apath->child_append_relid_sets,
 									   apath->path.pathkeys, required_outer,
 									   apath->path.parallel_workers,
 									   apath->path.parallel_aware,
diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h
index 684e02da063..6fb3a86598f 100644
--- a/src/include/nodes/pathnodes.h
+++ b/src/include/nodes/pathnodes.h
@@ -2099,6 +2099,12 @@ typedef struct CustomPath
  * For partial Append, 'subpaths' contains non-partial subpaths followed by
  * partial subpaths.
  *
+ * Whenever accumulate_append_subpath() allows us to consolidate multiple
+ * levels of Append paths are consolidated down to one, we store the RTI
+ * sets for the omitted paths in child_append_relid_sets. This is not necessary
+ * for planning or execution; we do it for the benefit of code that wants
+ * to inspect the final plan and understand how it came to be.
+ *
  * Note: it is possible for "subpaths" to contain only one, or even no,
  * elements.  These cases are optimized during create_append_plan.
  * In particular, an AppendPath with no subpaths is a "dummy" path that
@@ -2114,6 +2120,7 @@ typedef struct AppendPath
 	/* Index of first partial path in subpaths; list_length(subpaths) if none */
 	int			first_partial_path;
 	Cardinality limit_tuples;	/* hard limit on output tuples, or -1 */
+	List	   *child_append_relid_sets;
 } AppendPath;
 
 #define IS_DUMMY_APPEND(p) \
@@ -2130,12 +2137,15 @@ extern bool is_dummy_rel(RelOptInfo *rel);
 /*
  * MergeAppendPath represents a MergeAppend plan, ie, the merging of sorted
  * results from several member plans to produce similarly-sorted output.
+ *
+ * child_append_relid_sets has the same meaning here as for AppendPath.
  */
 typedef struct MergeAppendPath
 {
 	Path		path;
 	List	   *subpaths;		/* list of component Paths */
 	Cardinality limit_tuples;	/* hard limit on output tuples, or -1 */
+	List	   *child_append_relid_sets;
 } MergeAppendPath;
 
 /*
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index 497aec24876..19eceb56979 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -374,9 +374,16 @@ struct PartitionPruneInfo;		/* forward reference to struct below */
 typedef struct Append
 {
 	Plan		plan;
+
 	/* RTIs of appendrel(s) formed by this node */
 	Bitmapset  *apprelids;
+
+	/* sets of RTIs of appendrels consolidated into this node */
+	List	   *child_append_relid_sets;
+
+	/* plans to run */
 	List	   *appendplans;
+
 	/* # of asynchronous plans */
 	int			nasyncplans;
 
@@ -406,6 +413,10 @@ typedef struct MergeAppend
 	/* RTIs of appendrel(s) formed by this node */
 	Bitmapset  *apprelids;
 
+	/* sets of RTIs of appendrels consolidated into this node */
+	List	   *child_append_relid_sets;
+
+	/* plans to run */
 	List	   *mergeplans;
 
 	/* these fields are just like the sort-key info in struct Sort: */
diff --git a/src/include/optimizer/pathnode.h b/src/include/optimizer/pathnode.h
index 763cd25bb3c..5f43b7fd0cb 100644
--- a/src/include/optimizer/pathnode.h
+++ b/src/include/optimizer/pathnode.h
@@ -70,12 +70,14 @@ extern TidRangePath *create_tidrangescan_path(PlannerInfo *root,
 											  Relids required_outer);
 extern AppendPath *create_append_path(PlannerInfo *root, RelOptInfo *rel,
 									  List *subpaths, List *partial_subpaths,
+									  List *child_append_relid_sets,
 									  List *pathkeys, Relids required_outer,
 									  int parallel_workers, bool parallel_aware,
 									  double rows);
 extern MergeAppendPath *create_merge_append_path(PlannerInfo *root,
 												 RelOptInfo *rel,
 												 List *subpaths,
+												 List *child_append_relid_sets,
 												 List *pathkeys,
 												 Relids required_outer);
 extern GroupResultPath *create_group_result_path(PlannerInfo *root,
-- 
2.39.5 (Apple Git-154)

v3-0006-Store-information-about-elided-nodes-in-the-final.patchapplication/octet-stream; name=v3-0006-Store-information-about-elided-nodes-in-the-final.patchDownload
From 8c79bb726e43ca828779c30e9d244780834403e0 Mon Sep 17 00:00:00 2001
From: Robert Haas <rhaas@postgresql.org>
Date: Tue, 22 Apr 2025 14:10:19 -0400
Subject: [PATCH v3 6/7] Store information about elided nodes in the final
 plan.

When setrefs.c removes a SubqueryScan, single-child Append, or
single-child MergeAppend from the final Plan tree, the RTI which
would have been scanned by the removed node no longer appears in
the final plan (the actual range table entry is still present,
but it's no longer referenced).

That's fine for the executor, but it can create difficulties for
code that wants to deduce from the final plan what choices were
made during the planing process. For example, a traversal of a
join tree in the final plan might never encounter the RTI of one
of the relationss in the join problem, and might instead encounter
a scan of a child RTI or even one from a different subquery level.

This patch adjusts things so that each time we elide a node during
setrefs processing, we record the plan_node_id of its single surviving
child, the type of the removed node, and the RTIs that the removed
node would have scanned. This information is recorded in a separate
list that can be ignored by the executor and examined only by code
that cares about these details.

This commit also updates pg_overexplain to display these details.
---
 .../expected/pg_overexplain.out               |  4 +-
 contrib/pg_overexplain/pg_overexplain.c       | 39 ++++++++++++++
 src/backend/optimizer/plan/planner.c          |  1 +
 src/backend/optimizer/plan/setrefs.c          | 52 ++++++++++++++++++-
 src/include/nodes/pathnodes.h                 |  3 ++
 src/include/nodes/plannodes.h                 | 17 ++++++
 src/tools/pgindent/typedefs.list              |  1 +
 7 files changed, 114 insertions(+), 3 deletions(-)

diff --git a/contrib/pg_overexplain/expected/pg_overexplain.out b/contrib/pg_overexplain/expected/pg_overexplain.out
index 55d34666d87..ca9a23ea61f 100644
--- a/contrib/pg_overexplain/expected/pg_overexplain.out
+++ b/contrib/pg_overexplain/expected/pg_overexplain.out
@@ -452,6 +452,8 @@ SELECT * FROM vegetables WHERE genus = 'daucus';
  Seq Scan on daucus vegetables
    Filter: (genus = 'daucus'::text)
    Scan RTI: 2
+   Elided Node Type: Append
+   Elided Node RTIs: 1
  RTI 1 (relation, inherited, in-from-clause):
    Eref: vegetables (id, name, genus)
    Relation: vegetables
@@ -465,7 +467,7 @@ SELECT * FROM vegetables WHERE genus = 'daucus';
    Relation Kind: relation
    Relation Lock Mode: AccessShareLock
  Unprunable RTIs: 1 2
-(16 rows)
+(18 rows)
 
 -- Also test a case that involves a write.
 EXPLAIN (RANGE_TABLE, COSTS OFF)
diff --git a/contrib/pg_overexplain/pg_overexplain.c b/contrib/pg_overexplain/pg_overexplain.c
index 5dc707d69e3..fa907fa472e 100644
--- a/contrib/pg_overexplain/pg_overexplain.c
+++ b/contrib/pg_overexplain/pg_overexplain.c
@@ -191,6 +191,8 @@ overexplain_per_node_hook(PlanState *planstate, List *ancestors,
 	 */
 	if (options->range_table)
 	{
+		bool		opened_elided_nodes = false;
+
 		switch (nodeTag(plan))
 		{
 			case T_SeqScan:
@@ -251,6 +253,43 @@ overexplain_per_node_hook(PlanState *planstate, List *ancestors,
 			default:
 				break;
 		}
+
+		foreach_node(ElidedNode, n, es->pstmt->elidedNodes)
+		{
+			char	   *elidednodetag;
+
+			if (n->plan_node_id != plan->plan_node_id)
+				continue;
+
+			if (!opened_elided_nodes)
+			{
+				ExplainOpenGroup("Elided Nodes", "Elided Nodes", false, es);
+				opened_elided_nodes = true;
+			}
+
+			switch (n->elided_type)
+			{
+				case T_Append:
+					elidednodetag = "Append";
+					break;
+				case T_MergeAppend:
+					elidednodetag = "MergeAppend";
+					break;
+				case T_SubqueryScan:
+					elidednodetag = "SubqueryScan";
+					break;
+				default:
+					elidednodetag = psprintf("%d", n->elided_type);
+					break;
+			}
+
+			ExplainOpenGroup("Elided Node", NULL, true, es);
+			ExplainPropertyText("Elided Node Type", elidednodetag, es);
+			overexplain_bitmapset("Elided Node RTIs", n->relids, es);
+			ExplainCloseGroup("Elided Node", NULL, true, es);
+		}
+		if (opened_elided_nodes)
+			ExplainCloseGroup("Elided Nodes", "Elided Nodes", false, es);
 	}
 }
 
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index 59ee3f319f4..fef57d84bb3 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -590,6 +590,7 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions,
 	result->paramExecTypes = glob->paramExecTypes;
 	/* utilityStmt should be null, but we might as well copy it */
 	result->utilityStmt = parse->utilityStmt;
+	result->elidedNodes = glob->elidedNodes;
 	result->stmt_location = parse->stmt_location;
 	result->stmt_len = parse->stmt_len;
 
diff --git a/src/backend/optimizer/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.c
index eef43792aeb..5900458a0e1 100644
--- a/src/backend/optimizer/plan/setrefs.c
+++ b/src/backend/optimizer/plan/setrefs.c
@@ -211,6 +211,9 @@ static List *set_windowagg_runcondition_references(PlannerInfo *root,
 												   List *runcondition,
 												   Plan *plan);
 
+static void record_elided_node(PlannerGlobal *glob, int plan_node_id,
+							   NodeTag elided_type, Bitmapset *relids);
+
 
 /*****************************************************************************
  *
@@ -1441,10 +1444,17 @@ set_subqueryscan_references(PlannerInfo *root,
 
 	if (trivial_subqueryscan(plan))
 	{
+		Index		scanrelid;
+
 		/*
 		 * We can omit the SubqueryScan node and just pull up the subplan.
 		 */
 		result = clean_up_removed_plan_level((Plan *) plan, plan->subplan);
+
+		/* Remember that we removed a SubqueryScan */
+		scanrelid = plan->scan.scanrelid + rtoffset;
+		record_elided_node(root->glob, plan->subplan->plan_node_id,
+						   T_SubqueryScan, bms_make_singleton(scanrelid));
 	}
 	else
 	{
@@ -1872,7 +1882,17 @@ set_append_references(PlannerInfo *root,
 		Plan	   *p = (Plan *) linitial(aplan->appendplans);
 
 		if (p->parallel_aware == aplan->plan.parallel_aware)
-			return clean_up_removed_plan_level((Plan *) aplan, p);
+		{
+			Plan	   *result;
+
+			result = clean_up_removed_plan_level((Plan *) aplan, p);
+
+			/* Remember that we removed an Append */
+			record_elided_node(root->glob, p->plan_node_id, T_Append,
+							   offset_relid_set(aplan->apprelids, rtoffset));
+
+			return result;
+		}
 	}
 
 	/*
@@ -1940,7 +1960,17 @@ set_mergeappend_references(PlannerInfo *root,
 		Plan	   *p = (Plan *) linitial(mplan->mergeplans);
 
 		if (p->parallel_aware == mplan->plan.parallel_aware)
-			return clean_up_removed_plan_level((Plan *) mplan, p);
+		{
+			Plan	   *result;
+
+			result = clean_up_removed_plan_level((Plan *) mplan, p);
+
+			/* Remember that we removed a MergeAppend */
+			record_elided_node(root->glob, p->plan_node_id, T_MergeAppend,
+							   offset_relid_set(mplan->apprelids, rtoffset));
+
+			return result;
+		}
 	}
 
 	/*
@@ -3755,3 +3785,21 @@ extract_query_dependencies_walker(Node *node, PlannerInfo *context)
 	return expression_tree_walker(node, extract_query_dependencies_walker,
 								  context);
 }
+
+/*
+ * Record some details about a node removed from the plan during setrefs
+ * procesing, for the benefit of code trying to reconstruct planner decisions
+ * from examination of the final plan tree.
+ */
+static void
+record_elided_node(PlannerGlobal *glob, int plan_node_id,
+				   NodeTag elided_type, Bitmapset *relids)
+{
+	ElidedNode *n = makeNode(ElidedNode);
+
+	n->plan_node_id = plan_node_id;
+	n->elided_type = elided_type;
+	n->relids = relids;
+
+	glob->elidedNodes = lappend(glob->elidedNodes, n);
+}
diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h
index 4f8586f6591..684e02da063 100644
--- a/src/include/nodes/pathnodes.h
+++ b/src/include/nodes/pathnodes.h
@@ -159,6 +159,9 @@ typedef struct PlannerGlobal
 	/* type OIDs for PARAM_EXEC Params */
 	List	   *paramExecTypes;
 
+	/* info about nodes elided from the plan during setrefs processing */
+	List	   *elidedNodes;
+
 	/* highest PlaceHolderVar ID assigned */
 	Index		lastPHId;
 
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index 3f2d6fafc24..497aec24876 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -152,6 +152,9 @@ typedef struct PlannedStmt
 	/* non-null if this is utility stmt */
 	Node	   *utilityStmt;
 
+	/* info about nodes elided from the plan during setrefs processing */
+	List	   *elidedNodes;
+
 	/* statement location in source string (copied from Query) */
 	/* start location, or -1 if unknown */
 	ParseLoc	stmt_location;
@@ -1818,4 +1821,18 @@ typedef struct SubPlanRTInfo
 	bool		dummy;
 } SubPlanRTInfo;
 
+/*
+ * ElidedNode
+ *
+ * Information about nodes elided from the final plan tree: trivial subquery
+ * scans, and single-child Append and MergeAppend nodes.
+ */
+typedef struct ElidedNode
+{
+	NodeTag		type;
+	int			plan_node_id;
+	NodeTag		elided_type;
+	Bitmapset  *relids;
+} ElidedNode;
+
 #endif							/* PLANNODES_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8e4367dabaf..899551d5117 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -4348,3 +4348,4 @@ z_stream
 z_streamp
 zic_t
 SubPlanRTInfo
+ElidedNode
-- 
2.39.5 (Apple Git-154)

#15Richard Guo
guofenglinux@gmail.com
In reply to: Robert Haas (#14)
Re: plan shape work

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

#16Robert Haas
robertmhaas@gmail.com
In reply to: Richard Guo (#15)
Re: plan shape work

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

#17Tom Lane
tgl@sss.pgh.pa.us
In reply to: Robert Haas (#16)
Re: plan shape work

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

#18Robert Haas
robertmhaas@gmail.com
In reply to: Tom Lane (#17)
Re: plan shape work

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

#19Robert Haas
robertmhaas@gmail.com
In reply to: Robert Haas (#18)
7 attachment(s)
Re: plan shape work

On Fri, Sep 5, 2025 at 12:40 PM Robert Haas <robertmhaas@gmail.com> wrote:

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

Here's the result. Seem to work OK.

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

Attachments:

v4-0001-Keep-track-of-what-RTIs-a-Result-node-is-scanning.patchapplication/octet-stream; name=v4-0001-Keep-track-of-what-RTIs-a-Result-node-is-scanning.patchDownload
From 18cbb300717085232ad5cc3cc85b6edefd9952ef Mon Sep 17 00:00:00 2001
From: Robert Haas <rhaas@postgresql.org>
Date: Tue, 2 Sep 2025 14:12:13 -0400
Subject: [PATCH v4 1/7] Keep track of what RTIs a Result node is scanning.

Result nodes now include an RTI set, which is only non-NULL when they
have no subplan, and is taken from the relid set of the RelOptInfo
that the Result is generating.

Using that information, EXPLAIN now emits, where relevant, a "Replaces" line
that says whether it replaced a scan, a join, or an aggregate; and in the
former two cases, which relations were involved.

Likewise, pg_overexplain's EXPLAIN (RANGE_TABLE) now displays the RTIs
stored in a Result node just as it already does for other RTI-bearing
node types.
---
 contrib/file_fdw/expected/file_fdw.out        |   1 +
 .../expected/pg_overexplain.out               |   6 +-
 contrib/pg_overexplain/pg_overexplain.c       |  12 ++
 .../postgres_fdw/expected/postgres_fdw.out    |   9 +-
 src/backend/commands/explain.c                |  95 ++++++++++++
 src/backend/optimizer/plan/createplan.c       |  93 +++++++++---
 src/backend/optimizer/plan/setrefs.c          |   2 +
 src/include/nodes/plannodes.h                 |   9 ++
 src/test/regress/expected/aggregates.out      |  51 ++++---
 src/test/regress/expected/case.out            |  21 +--
 .../regress/expected/generated_virtual.out    |   3 +-
 src/test/regress/expected/groupingsets.out    |   3 +-
 src/test/regress/expected/inherit.out         |  28 ++--
 src/test/regress/expected/join.out            | 113 ++++++++------
 src/test/regress/expected/merge.out           |   3 +-
 .../regress/expected/partition_aggregate.out  |  21 +--
 src/test/regress/expected/partition_join.out  |  23 +--
 src/test/regress/expected/partition_prune.out | 141 ++++++++++--------
 src/test/regress/expected/predicate.out       |  33 ++--
 src/test/regress/expected/rowsecurity.out     |  13 +-
 src/test/regress/expected/rowtypes.out        |   6 +-
 src/test/regress/expected/select.out          |   7 +-
 src/test/regress/expected/subselect.out       |   6 +-
 src/test/regress/expected/tsrf.out            |   6 +-
 24 files changed, 491 insertions(+), 214 deletions(-)

diff --git a/contrib/file_fdw/expected/file_fdw.out b/contrib/file_fdw/expected/file_fdw.out
index 246e3d3e566..5e710d06de0 100644
--- a/contrib/file_fdw/expected/file_fdw.out
+++ b/contrib/file_fdw/expected/file_fdw.out
@@ -323,6 +323,7 @@ SELECT explain_filter('EXPLAIN (VERBOSE, COSTS FALSE) SELECT * FROM agg_csv WHER
  Result
    Output: a, b
    One-Time Filter: false
+   Replaces: Scan on agg_csv
 
 \t off
 SELECT * FROM agg_csv WHERE a < 0;
diff --git a/contrib/pg_overexplain/expected/pg_overexplain.out b/contrib/pg_overexplain/expected/pg_overexplain.out
index 6de02323d7c..55d34666d87 100644
--- a/contrib/pg_overexplain/expected/pg_overexplain.out
+++ b/contrib/pg_overexplain/expected/pg_overexplain.out
@@ -44,9 +44,10 @@ EXPLAIN (RANGE_TABLE) SELECT 1;
                 QUERY PLAN                
 ------------------------------------------
  Result  (cost=0.00..0.01 rows=1 width=4)
+   RTIs: 1
  RTI 1 (result):
    Eref: "*RESULT*" ()
-(3 rows)
+(4 rows)
 
 -- Create a partitioned table.
 CREATE TABLE vegetables (id serial, name text, genus text)
@@ -475,6 +476,7 @@ INSERT INTO vegetables (name, genus) VALUES ('broccoflower', 'brassica');
    Nominal RTI: 1
    Exclude Relation RTI: 0
    ->  Result
+         RTIs: 2
  RTI 1 (relation):
    Eref: vegetables (id, name, genus)
    Relation: vegetables
@@ -485,5 +487,5 @@ INSERT INTO vegetables (name, genus) VALUES ('broccoflower', 'brassica');
    Eref: "*RESULT*" ()
  Unprunable RTIs: 1
  Result RTIs: 1
-(14 rows)
+(15 rows)
 
diff --git a/contrib/pg_overexplain/pg_overexplain.c b/contrib/pg_overexplain/pg_overexplain.c
index de824566f8c..bd70b6d9d5e 100644
--- a/contrib/pg_overexplain/pg_overexplain.c
+++ b/contrib/pg_overexplain/pg_overexplain.c
@@ -236,6 +236,18 @@ overexplain_per_node_hook(PlanState *planstate, List *ancestors,
 									  ((MergeAppend *) plan)->apprelids,
 									  es);
 				break;
+			case T_Result:
+
+				/*
+				 * 'relids' is only meaningful when plan->lefttree is NULL,
+				 * but if somehow it ends up set when plan->lefttree is not
+				 * NULL, print it anyway.
+				 */
+				if (plan->lefttree == NULL ||
+					((Result *) plan)->relids != NULL)
+					overexplain_bitmapset("RTIs",
+										  ((Result *) plan)->relids,
+										  es);
 			default:
 				break;
 		}
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 78b8367d289..04aba992a88 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -7158,7 +7158,8 @@ EXPLAIN (VERBOSE, COSTS OFF) SELECT count(*) FROM ft1 WHERE c2 < 0;
    Output: count(*)
    ->  Result
          One-Time Filter: false
-(4 rows)
+         Replaces: Scan on ft1
+(5 rows)
 
 SELECT count(*) FROM ft1 WHERE c2 < 0;
  count 
@@ -7202,7 +7203,8 @@ EXPLAIN (VERBOSE, COSTS OFF) SELECT count(*) FROM ft1 WHERE c2 >= 0;
    Output: count(*)
    ->  Result
          One-Time Filter: false
-(4 rows)
+         Replaces: Scan on ft1
+(5 rows)
 
 SELECT count(*) FROM ft1 WHERE c2 >= 0;
  count 
@@ -8031,7 +8033,8 @@ DELETE FROM rem1 WHERE false;     -- currently can't be pushed down
    ->  Result
          Output: ctid
          One-Time Filter: false
-(5 rows)
+         Replaces: Scan on rem1
+(6 rows)
 
 -- Test with statement-level triggers
 CREATE TRIGGER trig_stmt_before
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 8345bc0264b..42723c3a150 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -147,6 +147,7 @@ static void show_buffer_usage(ExplainState *es, const BufferUsage *usage);
 static void show_wal_usage(ExplainState *es, const WalUsage *usage);
 static void show_memory_counters(ExplainState *es,
 								 const MemoryContextCounters *mem_counters);
+static void show_result_replacement_info(Result *result, ExplainState *es);
 static void ExplainIndexScanDetails(Oid indexid, ScanDirection indexorderdir,
 									ExplainState *es);
 static void ExplainScanTarget(Scan *plan, ExplainState *es);
@@ -2238,6 +2239,8 @@ ExplainNode(PlanState *planstate, List *ancestors,
 			if (plan->qual)
 				show_instrumentation_count("Rows Removed by Filter", 1,
 										   planstate, es);
+			if (plan->lefttree == NULL)
+				show_result_replacement_info(castNode(Result, plan), es);
 			break;
 		case T_ModifyTable:
 			show_modifytable_info(castNode(ModifyTableState, planstate), ancestors,
@@ -4750,6 +4753,98 @@ show_modifytable_info(ModifyTableState *mtstate, List *ancestors,
 		ExplainCloseGroup("Target Tables", "Target Tables", false, es);
 }
 
+/*
+ * Explain what a "Result" node replaced.
+ */
+static void
+show_result_replacement_info(Result *result, ExplainState *es)
+{
+	StringInfoData buf;
+	int			nrels = 0;
+	int			rti = -1;
+	bool		found_non_result = false;
+	char	   *s;
+
+	/*
+	 * If the Result node has a subplan, it didn't replace a scan, join, or
+	 * aggregate;
+	 */
+	Assert(result->plan.lefttree == NULL);
+
+	/*
+	 * If the relids set is empty, it didn't replace a scan or a join, so it
+	 * must have come from an upper rel. Currently, an aggregate seems to be
+	 * the only possibility. (We might want to add more details to the Result
+	 * node in the future to disambiguate; or to show the rels being
+	 * aggregated in the case of partitionwise aggregate.)
+	 */
+	if (bms_is_empty(result->relids))
+	{
+		ExplainPropertyText("Replaces", "Aggregate", es);
+		return;
+	}
+
+	/*
+	 * Build up a comma-separated list of user-facing names for the range
+	 * table entries in the relids set.
+	 */
+	initStringInfo(&buf);
+	while ((rti = bms_next_member(result->relids, rti)) >= 0)
+	{
+		RangeTblEntry *rte = rt_fetch(rti, es->rtable);
+		char	   *refname;
+
+		/*
+		 * add_outer_joins_to_relids will add join RTIs to the relids set of a
+		 * join; if that join is then replaced with a Result node, we may see
+		 * such RTIs here. But we want to completely ignore those here,
+		 * because "a LEFT JOIN b ON whatever" is a join between a and b, not
+		 * a join between a, b, and an unnamed join.
+		 */
+		if (rte->rtekind == RTE_JOIN)
+			continue;
+
+		/* Count the number of rels that aren't ignored completely. */
+		++nrels;
+
+		/* Work out what reference name to use and added it the string. */
+		refname = (char *) list_nth(es->rtable_names, rti - 1);
+		if (refname == NULL)
+			refname = rte->eref->aliasname;
+		if (buf.len > 0)
+			appendStringInfoString(&buf, ", ");
+		appendStringInfoString(&buf, refname);
+
+		/* Keep track of whether we see anything other than RTE_RESULT. */
+		if (rte->rtekind != RTE_RESULT)
+			found_non_result = true;
+	}
+
+	/*
+	 * If this Result node is because of a single RTE that is RTE_RESULT, it
+	 * is not really replacing anything at all, because there's no other
+	 * method for implementing a scan of such an RTE, so we don't display the
+	 * Replaces line in such cases.
+	 *
+	 * (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.)
+	 */
+	if (nrels <= 1 && !found_non_result)
+		return;
+
+	/*
+	 * We're replacing either a scan or a join, according to the number of
+	 * rels in the relids set.
+	 */
+	if (nrels > 1)
+		s = psprintf("Join on %s", buf.data);
+	else
+		s = psprintf("Scan on %s", buf.data);
+	ExplainPropertyText("Replaces", s, es);
+}
+
 /*
  * Explain the constituent plans of an Append, MergeAppend,
  * BitmapAnd, or BitmapOr node.
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index 6791cbeb416..6f8fac85d48 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -99,7 +99,8 @@ static Gather *create_gather_plan(PlannerInfo *root, GatherPath *best_path);
 static Plan *create_projection_plan(PlannerInfo *root,
 									ProjectionPath *best_path,
 									int flags);
-static Plan *inject_projection_plan(Plan *subplan, List *tlist, bool parallel_safe);
+static Plan *inject_projection_plan(Plan *subplan, List *tlist,
+									bool parallel_safe);
 static Sort *create_sort_plan(PlannerInfo *root, SortPath *best_path, int flags);
 static IncrementalSort *create_incrementalsort_plan(PlannerInfo *root,
 													IncrementalSortPath *best_path, int flags);
@@ -302,7 +303,10 @@ static SetOp *make_setop(SetOpCmd cmd, SetOpStrategy strategy,
 						 List *tlist, Plan *lefttree, Plan *righttree,
 						 List *groupList, long numGroups);
 static LockRows *make_lockrows(Plan *lefttree, List *rowMarks, int epqParam);
-static Result *make_result(List *tlist, Node *resconstantqual, Plan *subplan);
+static Result *make_gating_result(List *tlist, Node *resconstantqual,
+								  Plan *subplan);
+static Result *make_simple_result(List *tlist, Node *resconstantqual,
+								  Relids relids);
 static ProjectSet *make_project_set(List *tlist, Plan *subplan);
 static ModifyTable *make_modifytable(PlannerInfo *root, Plan *subplan,
 									 CmdType operation, bool canSetTag,
@@ -1014,6 +1018,7 @@ create_gating_plan(PlannerInfo *root, Path *path, Plan *plan,
 {
 	Plan	   *gplan;
 	Plan	   *splan;
+	Relids		relids = NULL;
 
 	Assert(gating_quals);
 
@@ -1021,7 +1026,9 @@ create_gating_plan(PlannerInfo *root, Path *path, Plan *plan,
 	 * We might have a trivial Result plan already.  Stacking one Result atop
 	 * another is silly, so if that applies, just discard the input plan.
 	 * (We're assuming its targetlist is uninteresting; it should be either
-	 * the same as the result of build_path_tlist, or a simplified version.)
+	 * the same as the result of build_path_tlist, or a simplified version.
+	 * However, we preserve the set of relids that it purports to scan and
+	 * attribute that to our replacement Result instead.)
 	 */
 	splan = plan;
 	if (IsA(plan, Result))
@@ -1030,7 +1037,10 @@ create_gating_plan(PlannerInfo *root, Path *path, Plan *plan,
 
 		if (rplan->plan.lefttree == NULL &&
 			rplan->resconstantqual == NULL)
+		{
 			splan = NULL;
+			relids = rplan->relids;
+		}
 	}
 
 	/*
@@ -1038,9 +1048,12 @@ create_gating_plan(PlannerInfo *root, Path *path, Plan *plan,
 	 * tlist; that's never a wrong choice, even if the parent node didn't ask
 	 * for CP_EXACT_TLIST.
 	 */
-	gplan = (Plan *) make_result(build_path_tlist(root, path),
-								 (Node *) gating_quals,
-								 splan);
+	if (splan == NULL)
+		gplan = (Plan *) make_simple_result(build_path_tlist(root, path),
+											(Node *) gating_quals, relids);
+	else
+		gplan = (Plan *) make_gating_result(build_path_tlist(root, path),
+											(Node *) gating_quals, splan);
 
 	/*
 	 * Notice that we don't change cost or size estimates when doing gating.
@@ -1235,10 +1248,10 @@ create_append_plan(PlannerInfo *root, AppendPath *best_path, int flags)
 		/* Generate a Result plan with constant-FALSE gating qual */
 		Plan	   *plan;
 
-		plan = (Plan *) make_result(tlist,
-									(Node *) list_make1(makeBoolConst(false,
-																	  false)),
-									NULL);
+		plan = (Plan *) make_simple_result(tlist,
+										   (Node *) list_make1(makeBoolConst(false,
+																			 false)),
+										   best_path->path.parent->relids);
 
 		copy_generic_path_info(plan, (Path *) best_path);
 
@@ -1636,7 +1649,8 @@ create_group_result_plan(PlannerInfo *root, GroupResultPath *best_path)
 	/* best_path->quals is just bare clauses */
 	quals = order_qual_clauses(root, best_path->quals);
 
-	plan = make_result(tlist, (Node *) quals, NULL);
+	plan = make_simple_result(tlist, (Node *) quals,
+							  best_path->path.parent->relids);
 
 	copy_generic_path_info(&plan->plan, (Path *) best_path);
 
@@ -1933,8 +1947,7 @@ create_projection_plan(PlannerInfo *root, ProjectionPath *best_path, int flags)
 	}
 	else
 	{
-		/* We need a Result node */
-		plan = (Plan *) make_result(tlist, NULL, subplan);
+		plan = (Plan *) make_gating_result(tlist, NULL, subplan);
 
 		copy_generic_path_info(plan, (Path *) best_path);
 	}
@@ -1958,7 +1971,7 @@ inject_projection_plan(Plan *subplan, List *tlist, bool parallel_safe)
 {
 	Plan	   *plan;
 
-	plan = (Plan *) make_result(tlist, NULL, subplan);
+	plan = (Plan *) make_gating_result(tlist, NULL, subplan);
 
 	/*
 	 * In principle, we should charge tlist eval cost plus cpu_per_tuple per
@@ -2436,7 +2449,8 @@ create_minmaxagg_plan(PlannerInfo *root, MinMaxAggPath *best_path)
 	/* Generate the output plan --- basically just a Result */
 	tlist = build_path_tlist(root, &best_path->path);
 
-	plan = make_result(tlist, (Node *) best_path->quals, NULL);
+	plan = make_simple_result(tlist, (Node *) best_path->quals,
+							  best_path->path.parent->relids);
 
 	copy_generic_path_info(&plan->plan, (Path *) best_path);
 
@@ -3887,7 +3901,8 @@ create_resultscan_plan(PlannerInfo *root, Path *best_path,
 			replace_nestloop_params(root, (Node *) scan_clauses);
 	}
 
-	scan_plan = make_result(tlist, (Node *) scan_clauses, NULL);
+	scan_plan = make_simple_result(tlist, (Node *) scan_clauses,
+								   best_path->parent->relids);
 
 	copy_generic_path_info(&scan_plan->plan, best_path);
 
@@ -6922,22 +6937,58 @@ make_limit(Plan *lefttree, Node *limitOffset, Node *limitCount,
 }
 
 /*
- * make_result
- *	  Build a Result plan node
+ * make_gating_result
+ *	  Build a Result plan node that performs projection of a subplan, and/or
+ *	  applies a one time filter (resconstantqual)
  */
 static Result *
-make_result(List *tlist,
-			Node *resconstantqual,
-			Plan *subplan)
+make_gating_result(List *tlist,
+				   Node *resconstantqual,
+				   Plan *subplan)
 {
 	Result	   *node = makeNode(Result);
 	Plan	   *plan = &node->plan;
 
+	Assert(subplan != NULL);
+
 	plan->targetlist = tlist;
 	plan->qual = NIL;
 	plan->lefttree = subplan;
 	plan->righttree = NULL;
 	node->resconstantqual = resconstantqual;
+	node->relids = NULL;
+
+	return node;
+}
+
+/*
+ * 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)
+ *
+ * 'relids' should be the relids set for this path's RelOptInfo. In essence,
+ * we're saying that this Result node generates all the tuples for that
+ * RelOptInfo. Note that the same consideration can never arise in
+ * make_gating_result(), because in that case the tuples are always coming
+ * from some subordinate node.
+ *
+ * NB: It would be nice to assert that the relids set is non-empty here,
+ * but it might be, because this could be a path for an upper rel.
+ */
+static Result *
+make_simple_result(List *tlist,
+				   Node *resconstantqual,
+				   Relids relids)
+{
+	Result	   *node = makeNode(Result);
+	Plan	   *plan = &node->plan;
+
+	plan->targetlist = tlist;
+	plan->qual = NIL;
+	plan->lefttree = NULL;
+	plan->righttree = NULL;
+	node->resconstantqual = resconstantqual;
+	node->relids = relids;
 
 	return node;
 }
diff --git a/src/backend/optimizer/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.c
index d706546f332..6950eff2c5b 100644
--- a/src/backend/optimizer/plan/setrefs.c
+++ b/src/backend/optimizer/plan/setrefs.c
@@ -1056,6 +1056,8 @@ set_plan_refs(PlannerInfo *root, Plan *plan, int rtoffset)
 				/* resconstantqual can't contain any subplan variable refs */
 				splan->resconstantqual =
 					fix_scan_expr(root, splan->resconstantqual, rtoffset, 1);
+				/* adjust the relids set */
+				splan->relids = offset_relid_set(splan->relids, rtoffset);
 			}
 			break;
 		case T_ProjectSet:
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index 29d7732d6a0..1abcf90be43 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -261,12 +261,21 @@ typedef struct Plan
  * If resconstantqual isn't NULL, it represents a one-time qualification
  * test (i.e., one that doesn't depend on any variables from the outer plan,
  * so needs to be evaluated only once).
+ *
+ * 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).
  * ----------------
  */
 typedef struct Result
 {
 	Plan		plan;
 	Node	   *resconstantqual;
+	Bitmapset  *relids;
 } Result;
 
 /* ----------------
diff --git a/src/test/regress/expected/aggregates.out b/src/test/regress/expected/aggregates.out
index c35288eecde..ce45de7e175 100644
--- a/src/test/regress/expected/aggregates.out
+++ b/src/test/regress/expected/aggregates.out
@@ -959,11 +959,12 @@ explain (costs off)
                          QUERY PLAN                         
 ------------------------------------------------------------
  Result
+   Replaces: Aggregate
    InitPlan 1
      ->  Limit
            ->  Index Only Scan using tenk1_unique1 on tenk1
                  Index Cond: (unique1 IS NOT NULL)
-(5 rows)
+(6 rows)
 
 select min(unique1) from tenk1;
  min 
@@ -976,11 +977,12 @@ explain (costs off)
                              QUERY PLAN                              
 ---------------------------------------------------------------------
  Result
+   Replaces: Aggregate
    InitPlan 1
      ->  Limit
            ->  Index Only Scan Backward using tenk1_unique1 on tenk1
                  Index Cond: (unique1 IS NOT NULL)
-(5 rows)
+(6 rows)
 
 select max(unique1) from tenk1;
  max  
@@ -993,11 +995,12 @@ explain (costs off)
                                QUERY PLAN                               
 ------------------------------------------------------------------------
  Result
+   Replaces: Aggregate
    InitPlan 1
      ->  Limit
            ->  Index Only Scan Backward using tenk1_unique1 on tenk1
                  Index Cond: ((unique1 IS NOT NULL) AND (unique1 < 42))
-(5 rows)
+(6 rows)
 
 select max(unique1) from tenk1 where unique1 < 42;
  max 
@@ -1010,11 +1013,12 @@ explain (costs off)
                                QUERY PLAN                               
 ------------------------------------------------------------------------
  Result
+   Replaces: Aggregate
    InitPlan 1
      ->  Limit
            ->  Index Only Scan Backward using tenk1_unique1 on tenk1
                  Index Cond: ((unique1 IS NOT NULL) AND (unique1 > 42))
-(5 rows)
+(6 rows)
 
 select max(unique1) from tenk1 where unique1 > 42;
  max  
@@ -1033,11 +1037,12 @@ explain (costs off)
                                 QUERY PLAN                                 
 ---------------------------------------------------------------------------
  Result
+   Replaces: Aggregate
    InitPlan 1
      ->  Limit
            ->  Index Only Scan Backward using tenk1_unique1 on tenk1
                  Index Cond: ((unique1 IS NOT NULL) AND (unique1 > 42000))
-(5 rows)
+(6 rows)
 
 select max(unique1) from tenk1 where unique1 > 42000;
  max 
@@ -1052,11 +1057,12 @@ explain (costs off)
                                  QUERY PLAN                                 
 ----------------------------------------------------------------------------
  Result
+   Replaces: Aggregate
    InitPlan 1
      ->  Limit
            ->  Index Only Scan Backward using tenk1_thous_tenthous on tenk1
                  Index Cond: ((thousand = 33) AND (tenthous IS NOT NULL))
-(5 rows)
+(6 rows)
 
 select max(tenthous) from tenk1 where thousand = 33;
  max  
@@ -1069,11 +1075,12 @@ explain (costs off)
                                 QUERY PLAN                                
 --------------------------------------------------------------------------
  Result
+   Replaces: Aggregate
    InitPlan 1
      ->  Limit
            ->  Index Only Scan using tenk1_thous_tenthous on tenk1
                  Index Cond: ((thousand = 33) AND (tenthous IS NOT NULL))
-(5 rows)
+(6 rows)
 
 select min(tenthous) from tenk1 where thousand = 33;
  min 
@@ -1090,11 +1097,12 @@ explain (costs off)
  Seq Scan on int4_tbl
    SubPlan 2
      ->  Result
+           Replaces: Aggregate
            InitPlan 1
              ->  Limit
                    ->  Index Only Scan using tenk1_unique1 on tenk1
                          Index Cond: ((unique1 IS NOT NULL) AND (unique1 > int4_tbl.f1))
-(7 rows)
+(8 rows)
 
 select f1, (select min(unique1) from tenk1 where unique1 > f1) AS gt
   from int4_tbl;
@@ -1119,7 +1127,8 @@ explain (costs off)
            ->  Index Only Scan Backward using tenk1_unique2 on tenk1
                  Index Cond: (unique2 IS NOT NULL)
    ->  Result
-(7 rows)
+         Replaces: Aggregate
+(8 rows)
 
 select distinct max(unique2) from tenk1;
  max  
@@ -1138,7 +1147,8 @@ explain (costs off)
            ->  Index Only Scan Backward using tenk1_unique2 on tenk1
                  Index Cond: (unique2 IS NOT NULL)
    ->  Result
-(7 rows)
+         Replaces: Aggregate
+(8 rows)
 
 select max(unique2) from tenk1 order by 1;
  max  
@@ -1157,7 +1167,8 @@ explain (costs off)
            ->  Index Only Scan Backward using tenk1_unique2 on tenk1
                  Index Cond: (unique2 IS NOT NULL)
    ->  Result
-(7 rows)
+         Replaces: Aggregate
+(8 rows)
 
 select max(unique2) from tenk1 order by max(unique2);
  max  
@@ -1176,7 +1187,8 @@ explain (costs off)
            ->  Index Only Scan Backward using tenk1_unique2 on tenk1
                  Index Cond: (unique2 IS NOT NULL)
    ->  Result
-(7 rows)
+         Replaces: Aggregate
+(8 rows)
 
 select max(unique2) from tenk1 order by max(unique2)+1;
  max  
@@ -1196,7 +1208,8 @@ explain (costs off)
                  Index Cond: (unique2 IS NOT NULL)
    ->  ProjectSet
          ->  Result
-(8 rows)
+               Replaces: Aggregate
+(9 rows)
 
 select max(unique2), generate_series(1,3) as g from tenk1 order by g desc;
  max  | g 
@@ -1212,12 +1225,13 @@ explain (costs off)
                      QUERY PLAN                     
 ----------------------------------------------------
  Result
+   Replaces: Aggregate
    InitPlan 1
      ->  Limit
            ->  Result
                  One-Time Filter: (100 IS NOT NULL)
                  ->  Seq Scan on tenk1
-(6 rows)
+(7 rows)
 
 select max(100) from tenk1;
  max 
@@ -1243,6 +1257,7 @@ explain (costs off)
                                          QUERY PLAN                                          
 ---------------------------------------------------------------------------------------------
  Result
+   Replaces: Aggregate
    InitPlan 1
      ->  Limit
            ->  Merge Append
@@ -1265,7 +1280,7 @@ explain (costs off)
                  ->  Index Only Scan using minmaxtest2i on minmaxtest2 minmaxtest_8
                        Index Cond: (f1 IS NOT NULL)
                  ->  Index Only Scan Backward using minmaxtest3i on minmaxtest3 minmaxtest_9
-(23 rows)
+(24 rows)
 
 select min(f1), max(f1) from minmaxtest;
  min | max 
@@ -1304,7 +1319,8 @@ explain (costs off)
    ->  Sort
          Sort Key: ((InitPlan 1).col1), ((InitPlan 2).col1)
          ->  Result
-(26 rows)
+               Replaces: Aggregate
+(27 rows)
 
 select distinct min(f1), max(f1) from minmaxtest;
  min | max 
@@ -1334,7 +1350,8 @@ explain (costs off)
                    ->  Seq Scan on int4_tbl t1
                          Filter: ((f1 IS NOT NULL) AND (f1 = t0.f1))
            ->  Result
-(9 rows)
+                 Replaces: Aggregate
+(10 rows)
 
 select f1, (select distinct min(t1.f1) from int4_tbl t1 where t1.f1 = t0.f1)
 from int4_tbl t0;
diff --git a/src/test/regress/expected/case.out b/src/test/regress/expected/case.out
index efee7fc4317..d74b0dd68cc 100644
--- a/src/test/regress/expected/case.out
+++ b/src/test/regress/expected/case.out
@@ -266,27 +266,30 @@ SELECT *
 -- Tests for constant subexpression simplification
 explain (costs off)
 SELECT * FROM CASE_TBL WHERE NULLIF(1, 2) = 2;
-        QUERY PLAN        
---------------------------
+          QUERY PLAN          
+------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on case_tbl
+(3 rows)
 
 explain (costs off)
 SELECT * FROM CASE_TBL WHERE NULLIF(1, 1) IS NOT NULL;
-        QUERY PLAN        
---------------------------
+          QUERY PLAN          
+------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on case_tbl
+(3 rows)
 
 explain (costs off)
 SELECT * FROM CASE_TBL WHERE NULLIF(1, null) = 2;
-        QUERY PLAN        
---------------------------
+          QUERY PLAN          
+------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on case_tbl
+(3 rows)
 
 --
 -- Examples of updates involving tables
diff --git a/src/test/regress/expected/generated_virtual.out b/src/test/regress/expected/generated_virtual.out
index aca6347babe..8b25e77bb4d 100644
--- a/src/test/regress/expected/generated_virtual.out
+++ b/src/test/regress/expected/generated_virtual.out
@@ -1580,7 +1580,8 @@ select t2.* from gtest32 t1 left join gtest32 t2 on false;
    ->  Result
          Output: a, e, 20, COALESCE(a, 100)
          One-Time Filter: false
-(8 rows)
+         Replaces: Scan on t2
+(9 rows)
 
 select t2.* from gtest32 t1 left join gtest32 t2 on false;
  a | b | c | d | e 
diff --git a/src/test/regress/expected/groupingsets.out b/src/test/regress/expected/groupingsets.out
index 35e4cb47ebe..07a37da79dd 100644
--- a/src/test/regress/expected/groupingsets.out
+++ b/src/test/regress/expected/groupingsets.out
@@ -591,11 +591,12 @@ explain (costs off)
                          QUERY PLAN                         
 ------------------------------------------------------------
  Result
+   Replaces: Aggregate
    InitPlan 1
      ->  Limit
            ->  Index Only Scan using tenk1_unique1 on tenk1
                  Index Cond: (unique1 IS NOT NULL)
-(5 rows)
+(6 rows)
 
 -- Views with GROUPING SET queries
 CREATE VIEW gstest_view AS select a, b, grouping(a,b), sum(c), count(*), max(c)
diff --git a/src/test/regress/expected/inherit.out b/src/test/regress/expected/inherit.out
index 5b5055babdc..e1225fb1add 100644
--- a/src/test/regress/expected/inherit.out
+++ b/src/test/regress/expected/inherit.out
@@ -581,7 +581,8 @@ update some_tab set a = a + 1 where false;
    ->  Result
          Output: (some_tab.a + 1), NULL::oid, NULL::tid
          One-Time Filter: false
-(4 rows)
+         Replaces: Scan on some_tab
+(5 rows)
 
 update some_tab set a = a + 1 where false;
 explain (verbose, costs off)
@@ -593,7 +594,8 @@ update some_tab set a = a + 1 where false returning b, a;
    ->  Result
          Output: (some_tab.a + 1), NULL::oid, NULL::tid
          One-Time Filter: false
-(5 rows)
+         Replaces: Scan on some_tab
+(6 rows)
 
 update some_tab set a = a + 1 where false returning b, a;
  b | a 
@@ -700,7 +702,8 @@ explain update parted_tab set a = 2 where false;
  Update on parted_tab  (cost=0.00..0.00 rows=0 width=0)
    ->  Result  (cost=0.00..0.00 rows=0 width=10)
          One-Time Filter: false
-(3 rows)
+         Replaces: Scan on parted_tab
+(4 rows)
 
 drop table parted_tab;
 -- Check UPDATE with multi-level partitioned inherited target
@@ -1756,6 +1759,7 @@ explain (verbose, costs off) select min(1-id) from matest0;
 ---------------------------------------------------------------------------------
  Result
    Output: (InitPlan 1).col1
+   Replaces: Aggregate
    InitPlan 1
      ->  Limit
            Output: ((1 - matest0.id))
@@ -1779,7 +1783,7 @@ explain (verbose, costs off) select min(1-id) from matest0;
                        ->  Index Scan using matest3i on public.matest3 matest0_4
                              Output: matest0_4.id, (1 - matest0_4.id)
                              Index Cond: ((1 - matest0_4.id) IS NOT NULL)
-(25 rows)
+(26 rows)
 
 select min(1-id) from matest0;
  min 
@@ -1943,6 +1947,7 @@ SELECT min(x) FROM
                              QUERY PLAN                             
 --------------------------------------------------------------------
  Result
+   Replaces: Aggregate
    InitPlan 1
      ->  Limit
            ->  Merge Append
@@ -1951,7 +1956,7 @@ SELECT min(x) FROM
                        Index Cond: (unique1 IS NOT NULL)
                  ->  Index Only Scan using tenk1_unique2 on tenk1 b
                        Index Cond: (unique2 IS NOT NULL)
-(9 rows)
+(10 rows)
 
 explain (costs off)
 SELECT min(y) FROM
@@ -1961,6 +1966,7 @@ SELECT min(y) FROM
                              QUERY PLAN                             
 --------------------------------------------------------------------
  Result
+   Replaces: Aggregate
    InitPlan 1
      ->  Limit
            ->  Merge Append
@@ -1969,7 +1975,7 @@ SELECT min(y) FROM
                        Index Cond: (unique1 IS NOT NULL)
                  ->  Index Only Scan using tenk1_unique2 on tenk1 b
                        Index Cond: (unique2 IS NOT NULL)
-(9 rows)
+(10 rows)
 
 -- XXX planner doesn't recognize that index on unique2 is sufficiently sorted
 explain (costs off)
@@ -3087,11 +3093,12 @@ explain (costs off) select * from range_list_parted where a between 3 and 23 and
 
 /* Should select no rows because range partition key cannot be null */
 explain (costs off) select * from range_list_parted where a is null;
-        QUERY PLAN        
---------------------------
+              QUERY PLAN               
+---------------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on range_list_parted
+(3 rows)
 
 /* Should only select rows from the null-accepting partition */
 explain (costs off) select * from range_list_parted where b is null;
@@ -3252,6 +3259,7 @@ explain (costs off) select min(a), max(a) from parted_minmax where b = '12345';
                                            QUERY PLAN                                           
 ------------------------------------------------------------------------------------------------
  Result
+   Replaces: Aggregate
    InitPlan 1
      ->  Limit
            ->  Index Only Scan using parted_minmax1i on parted_minmax1 parted_minmax
@@ -3260,7 +3268,7 @@ explain (costs off) select min(a), max(a) from parted_minmax where b = '12345';
      ->  Limit
            ->  Index Only Scan Backward using parted_minmax1i on parted_minmax1 parted_minmax_1
                  Index Cond: ((a IS NOT NULL) AND (b = '12345'::text))
-(9 rows)
+(10 rows)
 
 select min(a), max(a) from parted_minmax where b = '12345';
  min | max 
diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out
index 04079268b98..01ce1aa577c 100644
--- a/src/test/regress/expected/join.out
+++ b/src/test/regress/expected/join.out
@@ -2264,11 +2264,12 @@ explain (costs off)
 select aa, bb, unique1, unique1
   from tenk1 right join b_star on aa = unique1
   where bb < bb and bb is null;
-        QUERY PLAN        
---------------------------
+            QUERY PLAN             
+-----------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Join on tenk1, b_star
+(3 rows)
 
 select aa, bb, unique1, unique1
   from tenk1 right join b_star on aa = unique1
@@ -2386,11 +2387,12 @@ order by t1.unique1;
                            Index Cond: (unique1 < 10)
                SubPlan 2
                  ->  Result
+                       Replaces: Aggregate
                        InitPlan 1
                          ->  Limit
                                ->  Index Only Scan using tenk1_unique1 on tenk1
                                      Index Cond: ((unique1 IS NOT NULL) AND (unique1 = t2.unique1))
-(19 rows)
+(20 rows)
 
 -- Ensure we get the expected result
 select t1.unique1,t2.unique1 from tenk1 t1
@@ -2655,8 +2657,8 @@ select * from int8_tbl t1 left join
     (int8_tbl t2 left join int8_tbl t3 full join int8_tbl t4 on false on false)
     left join int8_tbl t5 on t2.q1 = t5.q1
 on t2.q2 = 123;
-                    QUERY PLAN                    
---------------------------------------------------
+                     QUERY PLAN                     
+----------------------------------------------------
  Nested Loop Left Join
    ->  Seq Scan on int8_tbl t1
    ->  Materialize
@@ -2668,8 +2670,9 @@ on t2.q2 = 123;
                            Filter: (q2 = 123)
                      ->  Result
                            One-Time Filter: false
+                           Replaces: Join on t3, t4
                ->  Seq Scan on int8_tbl t5
-(12 rows)
+(13 rows)
 
 explain (costs off)
 select * from int8_tbl t1
@@ -4165,6 +4168,7 @@ select * from t t1
          ->  Result
                Output: i3
                One-Time Filter: false
+               Replaces: Scan on t3
    ->  Memoize
          Output: t4.i4
          Cache Key: (1)
@@ -4172,7 +4176,7 @@ select * from t t1
          ->  Index Only Scan using t_pkey on pg_temp.t t4
                Output: t4.i4
                Index Cond: (t4.i4 > (1))
-(25 rows)
+(26 rows)
 
 explain (verbose, costs off)
 select * from
@@ -4363,7 +4367,8 @@ from int4_tbl t1
  Result
    Output: (current_database())::information_schema.sql_identifier, (c.relname)::information_schema.sql_identifier
    One-Time Filter: false
-(3 rows)
+   Replaces: Join on t1, t2, a, c, nc, t, nt, bt, nbt
+(4 rows)
 
 -- Test handling of qual pushdown to appendrel members with non-Var outputs
 explain (verbose, costs off)
@@ -4435,11 +4440,12 @@ select unique1 from tenk1, lateral f_immutable_int4(1) x where x = unique1;
 
 explain (costs off)
 select unique1 from tenk1, lateral f_immutable_int4(1) x where x in (select 17);
-        QUERY PLAN        
---------------------------
+        QUERY PLAN         
+---------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on tenk1
+(3 rows)
 
 explain (costs off)
 select unique1, x from tenk1 join f_immutable_int4(1) x on unique1 = x;
@@ -4485,11 +4491,12 @@ select unique1, x from tenk1 full join f_immutable_int4(1) x on unique1 = x;
 -- check that pullup of a const function allows further const-folding
 explain (costs off)
 select unique1 from tenk1, f_immutable_int4(1) x where x = 42;
-        QUERY PLAN        
---------------------------
+        QUERY PLAN         
+---------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on tenk1
+(3 rows)
 
 -- test inlining of immutable functions with PlaceHolderVars
 explain (costs off)
@@ -5346,7 +5353,8 @@ left join
    ->  Result
    ->  Result
          One-Time Filter: false
-(5 rows)
+         Replaces: Join on c, n
+(6 rows)
 
 -- check handling of apparently-commutable outer joins with non-commutable
 -- joins between them
@@ -5540,12 +5548,13 @@ select 1 from
      right join (select 1 as z) as ss2 on true)
   on false,
   lateral (select i4.f1, ss1.n from int8_tbl as i8 limit 1) as ss3;
-        QUERY PLAN        
---------------------------
+                  QUERY PLAN                   
+-----------------------------------------------
  Result
    Output: 1
    One-Time Filter: false
-(3 rows)
+   Replaces: Join on i4, ss3, x1, x2, *RESULT*
+(4 rows)
 
 select 1 from
   int4_tbl as i4
@@ -5574,11 +5583,12 @@ select 1 from t t1
        on false
      where t3.a = coalesce(t5.a,1)) as s2
   on true;
-        QUERY PLAN        
---------------------------
+                 QUERY PLAN                 
+--------------------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Join on t1, s1, t2, t3, t4, t5
+(3 rows)
 
 rollback;
 --
@@ -5975,14 +5985,15 @@ from int4_tbl as t1
                         inner join int8_tbl as t7 on null)
                on t5.q1 = t7.q2)
     on false;
-           QUERY PLAN           
---------------------------------
+                    QUERY PLAN                    
+--------------------------------------------------
  Nested Loop Left Join
    Join Filter: false
    ->  Seq Scan on int4_tbl t1
    ->  Result
          One-Time Filter: false
-(5 rows)
+         Replaces: Join on t2, t3, t4, t5, t7, t6
+(6 rows)
 
 -- variant with Var rather than PHV coming from t6
 explain (costs off)
@@ -5997,14 +6008,15 @@ from int4_tbl as t1
                         inner join int8_tbl as t7 on null)
                on t5.q1 = t7.q2)
     on false;
-           QUERY PLAN           
---------------------------------
+                    QUERY PLAN                    
+--------------------------------------------------
  Nested Loop Left Join
    Join Filter: false
    ->  Seq Scan on int4_tbl t1
    ->  Result
          One-Time Filter: false
-(5 rows)
+         Replaces: Join on t2, t3, t4, t5, t7, t6
+(6 rows)
 
 -- per further discussion of bug #17781
 explain (costs off)
@@ -6054,15 +6066,16 @@ select * from int8_tbl t1 left join
   (int8_tbl t2 inner join int8_tbl t3 on false
    left join int8_tbl t4 on t2.q2 = t4.q2)
 on t1.q1 = t2.q1;
-              QUERY PLAN              
---------------------------------------
+                 QUERY PLAN                 
+--------------------------------------------
  Hash Left Join
    Hash Cond: (t1.q1 = q1)
    ->  Seq Scan on int8_tbl t1
    ->  Hash
          ->  Result
                One-Time Filter: false
-(6 rows)
+               Replaces: Join on t2, t3, t4
+(7 rows)
 
 -- deduce constant-false from an EquivalenceClass
 explain (costs off)
@@ -6070,15 +6083,16 @@ select * from int8_tbl t1 left join
   (int8_tbl t2 inner join int8_tbl t3 on (t2.q1-t3.q2) = 0 and (t2.q1-t3.q2) = 1
    left join int8_tbl t4 on t2.q2 = t4.q2)
 on t1.q1 = t2.q1;
-              QUERY PLAN              
---------------------------------------
+                 QUERY PLAN                 
+--------------------------------------------
  Hash Left Join
    Hash Cond: (t1.q1 = q1)
    ->  Seq Scan on int8_tbl t1
    ->  Hash
          ->  Result
                One-Time Filter: false
-(6 rows)
+               Replaces: Join on t2, t3, t4
+(7 rows)
 
 -- pseudoconstant based on an outer-level Param
 explain (costs off)
@@ -6318,7 +6332,8 @@ select p.* from
 --------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on p
+(3 rows)
 
 select p.* from
   (parent p left join child c on (p.k = c.k)) join parent x on p.k = x.k
@@ -6335,7 +6350,8 @@ select p.* from
 --------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Join on p, x
+(3 rows)
 
 -- bug 5255: this is not optimizable by join removal
 begin;
@@ -6408,7 +6424,8 @@ SELECT q2 FROM
    ->  Result
          Output: q2, 'constant'::text
          One-Time Filter: false
-(9 rows)
+         Replaces: Scan on int8_tbl
+(10 rows)
 
 -- join removal bug #17786: check that OR conditions are cleaned up
 EXPLAIN (COSTS OFF)
@@ -6428,7 +6445,8 @@ FROM int4_tbl
                ->  Seq Scan on tenk1
                ->  Result
                      One-Time Filter: false
-(9 rows)
+                     Replaces: Scan on int8_tbl
+(10 rows)
 
 rollback;
 -- another join removal bug: we must clean up correctly when removing a PHV
@@ -6879,7 +6897,8 @@ where q1.x = q2.y;
 --------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on sj_1
+(3 rows)
 
 -- We can't use a cross-EC generated self join qual because of current logic of
 -- the generate_join_implied_equalities routine.
@@ -7703,11 +7722,12 @@ select 1 from emp1 full join
         on true
     where false) s on true
 where false;
-        QUERY PLAN        
---------------------------
+            QUERY PLAN            
+----------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Join on emp1, t1, t3
+(3 rows)
 
 select 1 from emp1 full join
     (select * from emp1 t1 join
@@ -7937,7 +7957,8 @@ where false;
  Result
    Output: 1
    One-Time Filter: false
-(3 rows)
+   Replaces: Scan on ss
+(4 rows)
 
 --
 -- Test LATERAL
@@ -8876,7 +8897,8 @@ select * from int8_tbl i8 left join lateral
    ->  Result
          Output: f1, i8.q2
          One-Time Filter: false
-(8 rows)
+         Replaces: Scan on int4_tbl
+(9 rows)
 
 explain (verbose, costs off)
 select * from int8_tbl i8 left join lateral
@@ -8890,7 +8912,8 @@ select * from int8_tbl i8 left join lateral
    ->  Result
          Output: f1, f1, i8.q2
          One-Time Filter: false
-(7 rows)
+         Replaces: Join on i1, i2
+(8 rows)
 
 -- check handling of nested appendrels inside LATERAL
 select * from
diff --git a/src/test/regress/expected/merge.out b/src/test/regress/expected/merge.out
index cf2219df754..c748172e98f 100644
--- a/src/test/regress/expected/merge.out
+++ b/src/test/regress/expected/merge.out
@@ -2427,7 +2427,8 @@ MERGE INTO pa_target t USING pa_source s ON t.tid = s.sid
                ->  Result
                      Output: t.tid, t.ctid
                      One-Time Filter: false
-(12 rows)
+                     Replaces: Scan on t
+(13 rows)
 
 MERGE INTO pa_target t USING pa_source s ON t.tid = s.sid
   WHEN NOT MATCHED THEN INSERT VALUES (s.sid);
diff --git a/src/test/regress/expected/partition_aggregate.out b/src/test/regress/expected/partition_aggregate.out
index 5f2c0cf5786..80b002fbdcf 100644
--- a/src/test/regress/expected/partition_aggregate.out
+++ b/src/test/regress/expected/partition_aggregate.out
@@ -146,13 +146,14 @@ SELECT c, a, count(*) FROM pagg_tab GROUP BY a, c;
 -- Test when input relation for grouping is dummy
 EXPLAIN (COSTS OFF)
 SELECT c, sum(a) FROM pagg_tab WHERE 1 = 2 GROUP BY c;
-           QUERY PLAN           
---------------------------------
+             QUERY PLAN             
+------------------------------------
  HashAggregate
    Group Key: c
    ->  Result
          One-Time Filter: false
-(4 rows)
+         Replaces: Scan on pagg_tab
+(5 rows)
 
 SELECT c, sum(a) FROM pagg_tab WHERE 1 = 2 GROUP BY c;
  c | sum 
@@ -161,12 +162,13 @@ SELECT c, sum(a) FROM pagg_tab WHERE 1 = 2 GROUP BY c;
 
 EXPLAIN (COSTS OFF)
 SELECT c, sum(a) FROM pagg_tab WHERE c = 'x' GROUP BY c;
-           QUERY PLAN           
---------------------------------
+             QUERY PLAN             
+------------------------------------
  GroupAggregate
    ->  Result
          One-Time Filter: false
-(3 rows)
+         Replaces: Scan on pagg_tab
+(4 rows)
 
 SELECT c, sum(a) FROM pagg_tab WHERE c = 'x' GROUP BY c;
  c | sum 
@@ -804,15 +806,16 @@ SELECT a.x, b.y, count(*) FROM (SELECT * FROM pagg_tab1 WHERE x < 20) a FULL JOI
 -- Empty join relation because of empty outer side, no partitionwise agg plan
 EXPLAIN (COSTS OFF)
 SELECT a.x, a.y, count(*) FROM (SELECT * FROM pagg_tab1 WHERE x = 1 AND x = 2) a LEFT JOIN pagg_tab2 b ON a.x = b.y GROUP BY a.x, a.y ORDER BY 1, 2;
-              QUERY PLAN              
---------------------------------------
+                  QUERY PLAN                  
+----------------------------------------------
  GroupAggregate
    Group Key: pagg_tab1.y
    ->  Sort
          Sort Key: pagg_tab1.y
          ->  Result
                One-Time Filter: false
-(6 rows)
+               Replaces: Join on b, pagg_tab1
+(7 rows)
 
 SELECT a.x, a.y, count(*) FROM (SELECT * FROM pagg_tab1 WHERE x = 1 AND x = 2) a LEFT JOIN pagg_tab2 b ON a.x = b.y GROUP BY a.x, a.y ORDER BY 1, 2;
  x | y | count 
diff --git a/src/test/regress/expected/partition_join.out b/src/test/regress/expected/partition_join.out
index 24e06845f92..0c495907eca 100644
--- a/src/test/regress/expected/partition_join.out
+++ b/src/test/regress/expected/partition_join.out
@@ -1609,19 +1609,21 @@ SELECT avg(t1.a), avg(t2.b), avg(t3.a + t3.b), t1.c, t2.c, t3.c FROM plt1 t1, pl
 -- joins where one of the relations is proven empty
 EXPLAIN (COSTS OFF)
 SELECT t1.a, t1.c, t2.b, t2.c FROM prt1 t1, prt2 t2 WHERE t1.a = t2.b AND t1.a = 1 AND t1.a = 2;
-        QUERY PLAN        
---------------------------
+         QUERY PLAN         
+----------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Join on t1, t2
+(3 rows)
 
 EXPLAIN (COSTS OFF)
 SELECT t1.a, t1.c, t2.b, t2.c FROM (SELECT * FROM prt1 WHERE a = 1 AND a = 2) t1 LEFT JOIN prt2 t2 ON t1.a = t2.b;
-        QUERY PLAN        
---------------------------
+          QUERY PLAN          
+------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Join on t2, prt1
+(3 rows)
 
 EXPLAIN (COSTS OFF)
 SELECT t1.a, t1.c, t2.b, t2.c FROM (SELECT * FROM prt1 WHERE a = 1 AND a = 2) t1 RIGHT JOIN prt2 t2 ON t1.a = t2.b, prt1 t3 WHERE t2.b = t3.a;
@@ -1648,7 +1650,8 @@ SELECT t1.a, t1.c, t2.b, t2.c FROM (SELECT * FROM prt1 WHERE a = 1 AND a = 2) t1
    ->  Hash
          ->  Result
                One-Time Filter: false
-(21 rows)
+               Replaces: Scan on prt1
+(22 rows)
 
 EXPLAIN (COSTS OFF)
 SELECT t1.a, t1.c, t2.b, t2.c FROM (SELECT * FROM prt1 WHERE a = 1 AND a = 2) t1 FULL JOIN prt2 t2 ON t1.a = t2.b WHERE t2.a = 0 ORDER BY t1.a, t2.b;
@@ -1668,7 +1671,8 @@ SELECT t1.a, t1.c, t2.b, t2.c FROM (SELECT * FROM prt1 WHERE a = 1 AND a = 2) t1
          ->  Hash
                ->  Result
                      One-Time Filter: false
-(14 rows)
+                     Replaces: Scan on prt1
+(15 rows)
 
 --
 -- tests for hash partitioned tables.
@@ -2255,7 +2259,8 @@ SELECT t1.a, t1.c, t2.b, t2.c FROM (SELECT * FROM prt1_l WHERE a = 1 AND a = 2)
    ->  Hash
          ->  Result
                One-Time Filter: false
-(11 rows)
+               Replaces: Scan on prt1_l
+(12 rows)
 
 -- Test case to verify proper handling of subqueries in a partitioned delete.
 -- The weird-looking lateral join is just there to force creation of a
diff --git a/src/test/regress/expected/partition_prune.out b/src/test/regress/expected/partition_prune.out
index d1966cd7d82..86cecc3ed71 100644
--- a/src/test/regress/expected/partition_prune.out
+++ b/src/test/regress/expected/partition_prune.out
@@ -628,7 +628,8 @@ explain (costs off) select * from rlp3 where a = 20;   /* empty */
 --------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on rlp3
+(3 rows)
 
 -- redundant clauses are eliminated
 explain (costs off) select * from rlp where a > 1 and a = 10;	/* only default */
@@ -671,7 +672,8 @@ explain (costs off) select * from rlp where a = 1 and a = 3;	/* empty */
 --------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on rlp
+(3 rows)
 
 explain (costs off) select * from rlp where (a = 1 and a = 3) or (a > 1 and a = 15);
                             QUERY PLAN                             
@@ -1254,25 +1256,28 @@ select * from boolpart where a is not unknown;
 
 -- check that all partitions are pruned when faced with conflicting clauses
 explain (costs off) select * from boolpart where a is not unknown and a is unknown;
-        QUERY PLAN        
---------------------------
+          QUERY PLAN          
+------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on boolpart
+(3 rows)
 
 explain (costs off) select * from boolpart where a is false and a is unknown;
-        QUERY PLAN        
---------------------------
+          QUERY PLAN          
+------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on boolpart
+(3 rows)
 
 explain (costs off) select * from boolpart where a is true and a is unknown;
-        QUERY PLAN        
---------------------------
+          QUERY PLAN          
+------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on boolpart
+(3 rows)
 
 -- inverse boolean partitioning - a seemingly unlikely design, but we've got
 -- code for it, so we'd better test it.
@@ -1568,11 +1573,12 @@ explain (costs off) select * from coercepart where a = any ('{ab,null}');
 (2 rows)
 
 explain (costs off) select * from coercepart where a = any (null::text[]);
-        QUERY PLAN        
---------------------------
+           QUERY PLAN           
+--------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on coercepart
+(3 rows)
 
 explain (costs off) select * from coercepart where a = all ('{ab}');
                   QUERY PLAN                  
@@ -1582,25 +1588,28 @@ explain (costs off) select * from coercepart where a = all ('{ab}');
 (2 rows)
 
 explain (costs off) select * from coercepart where a = all ('{ab,bc}');
-        QUERY PLAN        
---------------------------
+           QUERY PLAN           
+--------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on coercepart
+(3 rows)
 
 explain (costs off) select * from coercepart where a = all ('{ab,null}');
-        QUERY PLAN        
---------------------------
+           QUERY PLAN           
+--------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on coercepart
+(3 rows)
 
 explain (costs off) select * from coercepart where a = all (null::text[]);
-        QUERY PLAN        
---------------------------
+           QUERY PLAN           
+--------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on coercepart
+(3 rows)
 
 drop table coercepart;
 CREATE TABLE part (a INT, b INT) PARTITION BY LIST (a);
@@ -1773,7 +1782,8 @@ explain (costs off) select * from lp where a <> 'a' and a is null;
 --------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on lp
+(3 rows)
 
 explain (costs off) select * from lp where (a <> 'a' and a <> 'd') or a is null;
                                   QUERY PLAN                                  
@@ -1866,22 +1876,24 @@ create table lparted_by_int2 (a smallint) partition by list (a);
 create table lparted_by_int2_1 partition of lparted_by_int2 for values in (1);
 create table lparted_by_int2_16384 partition of lparted_by_int2 for values in (16384);
 explain (costs off) select * from lparted_by_int2 where a = 100_000_000_000_000;
-        QUERY PLAN        
---------------------------
+             QUERY PLAN              
+-------------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on lparted_by_int2
+(3 rows)
 
 create table rparted_by_int2 (a smallint) partition by range (a);
 create table rparted_by_int2_1 partition of rparted_by_int2 for values from (1) to (10);
 create table rparted_by_int2_16384 partition of rparted_by_int2 for values from (10) to (16384);
 -- all partitions pruned
 explain (costs off) select * from rparted_by_int2 where a > 100_000_000_000_000;
-        QUERY PLAN        
---------------------------
+             QUERY PLAN              
+-------------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on rparted_by_int2
+(3 rows)
 
 create table rparted_by_int2_maxvalue partition of rparted_by_int2 for values from (16384) to (maxvalue);
 -- all partitions but rparted_by_int2_maxvalue pruned
@@ -2132,7 +2144,8 @@ explain (costs off) select * from hp where a = 1 and b = 'abcde' and
 --------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on hp
+(3 rows)
 
 --
 -- Test runtime partition pruning
@@ -3390,11 +3403,12 @@ select * from stable_qual_pruning where a < '2000-02-01'::timestamptz;
 explain (analyze, costs off, summary off, timing off, buffers off)
 select * from stable_qual_pruning
   where a = any(array['2010-02-01', '2020-01-01']::timestamp[]);
-            QUERY PLAN             
------------------------------------
+               QUERY PLAN                
+-----------------------------------------
  Result (actual rows=0.00 loops=1)
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on stable_qual_pruning
+(3 rows)
 
 explain (analyze, costs off, summary off, timing off, buffers off)
 select * from stable_qual_pruning
@@ -3642,6 +3656,7 @@ explain (analyze, costs off, summary off, timing off, buffers off) select * from
    Sort Key: ma_test.b
    InitPlan 2
      ->  Result (actual rows=1.00 loops=1)
+           Replaces: Aggregate
            InitPlan 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)
@@ -3656,7 +3671,7 @@ explain (analyze, costs off, summary off, timing off, buffers off) select * from
    ->  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)
          Index Searches: 1
-(18 rows)
+(19 rows)
 
 reset enable_seqscan;
 reset enable_sort;
@@ -3678,11 +3693,12 @@ explain (costs off) select * from pp_arrpart where a = '{1}';
 (2 rows)
 
 explain (costs off) select * from pp_arrpart where a = '{1, 2}';
-        QUERY PLAN        
---------------------------
+           QUERY PLAN           
+--------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on pp_arrpart
+(3 rows)
 
 explain (costs off) select * from pp_arrpart where a in ('{4, 5}', '{1}');
                               QUERY PLAN                              
@@ -3764,11 +3780,12 @@ explain (costs off) select * from pp_enumpart where a = 'blue';
 (2 rows)
 
 explain (costs off) select * from pp_enumpart where a = 'black';
-        QUERY PLAN        
---------------------------
+           QUERY PLAN            
+---------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on pp_enumpart
+(3 rows)
 
 drop table pp_enumpart;
 drop type pp_colors;
@@ -3785,11 +3802,12 @@ explain (costs off) select * from pp_recpart where a = '(1,1)'::pp_rectype;
 (2 rows)
 
 explain (costs off) select * from pp_recpart where a = '(1,2)'::pp_rectype;
-        QUERY PLAN        
---------------------------
+           QUERY PLAN           
+--------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on pp_recpart
+(3 rows)
 
 drop table pp_recpart;
 drop type pp_rectype;
@@ -3805,11 +3823,12 @@ explain (costs off) select * from pp_intrangepart where a = '[1,2]'::int4range;
 (2 rows)
 
 explain (costs off) select * from pp_intrangepart where a = '(1,2)'::int4range;
-        QUERY PLAN        
---------------------------
+             QUERY PLAN              
+-------------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on pp_intrangepart
+(3 rows)
 
 drop table pp_intrangepart;
 --
@@ -4125,19 +4144,21 @@ explain (costs off) update listp1 set a = 1 where a = 2;
 -- constraint exclusion enabled
 set constraint_exclusion to 'on';
 explain (costs off) select * from listp1 where a = 2;
-        QUERY PLAN        
---------------------------
+         QUERY PLAN         
+----------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on listp1
+(3 rows)
 
 explain (costs off) update listp1 set a = 1 where a = 2;
-           QUERY PLAN           
---------------------------------
+            QUERY PLAN            
+----------------------------------
  Update on listp1
    ->  Result
          One-Time Filter: false
-(3 rows)
+         Replaces: Scan on listp1
+(4 rows)
 
 reset constraint_exclusion;
 reset enable_partition_pruning;
@@ -4524,18 +4545,20 @@ create table hp_contradict_test (a int, b int) partition by hash (a part_test_in
 create table hp_contradict_test_p1 partition of hp_contradict_test for values with (modulus 2, remainder 0);
 create table hp_contradict_test_p2 partition of hp_contradict_test for values with (modulus 2, remainder 1);
 explain (costs off) select * from hp_contradict_test where a is null and a === 1 and b === 1;
-        QUERY PLAN        
---------------------------
+               QUERY PLAN               
+----------------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on hp_contradict_test
+(3 rows)
 
 explain (costs off) select * from hp_contradict_test where a === 1 and b === 1 and a is null;
-        QUERY PLAN        
---------------------------
+               QUERY PLAN               
+----------------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on hp_contradict_test
+(3 rows)
 
 drop table hp_contradict_test;
 drop operator class part_test_int4_ops2 using hash;
diff --git a/src/test/regress/expected/predicate.out b/src/test/regress/expected/predicate.out
index 1aff0b59ff8..cd78cf11f51 100644
--- a/src/test/regress/expected/predicate.out
+++ b/src/test/regress/expected/predicate.out
@@ -37,7 +37,8 @@ SELECT * FROM pred_tab t WHERE t.a IS NULL;
 --------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on t
+(3 rows)
 
 -- Ensure the IS_NULL qual is not reduced to constant-FALSE on nullable
 -- columns
@@ -78,7 +79,8 @@ SELECT * FROM pred_tab t WHERE t.a IS NULL OR t.c IS NULL;
 --------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on t
+(3 rows)
 
 -- Ensure the OR clause is not reduced to constant-FALSE when not all branches
 -- are provably false
@@ -140,7 +142,8 @@ SELECT * FROM pred_tab t1
    ->  Seq Scan on pred_tab t1
    ->  Result
          One-Time Filter: false
-(5 rows)
+         Replaces: Scan on t2
+(6 rows)
 
 -- Ensure the IS_NULL qual is not reduced to constant-FALSE when the column is
 -- nullable by an outer join
@@ -210,7 +213,8 @@ SELECT * FROM pred_tab t1
    ->  Seq Scan on pred_tab t1
    ->  Result
          One-Time Filter: false
-(5 rows)
+         Replaces: Scan on t2
+(6 rows)
 
 -- Ensure the OR clause is not reduced to constant-FALSE when a column is
 -- made nullable from an outer join
@@ -267,17 +271,18 @@ SELECT * FROM pred_tab t1
     LEFT JOIN pred_tab t2 ON EXISTS
         (SELECT 1 FROM pred_tab t3, pred_tab t4, pred_tab t5, pred_tab t6
          WHERE t1.a = t3.a AND t6.a IS NULL);
-             QUERY PLAN              
--------------------------------------
+                 QUERY PLAN                 
+--------------------------------------------
  Nested Loop Left Join
    Join Filter: (InitPlan 1).col1
    InitPlan 1
      ->  Result
            One-Time Filter: false
+           Replaces: Join on t3, t4, t5, t6
    ->  Seq Scan on pred_tab t1
    ->  Materialize
          ->  Seq Scan on pred_tab t2
-(8 rows)
+(9 rows)
 
 DROP TABLE pred_tab;
 -- Validate we handle IS NULL and IS NOT NULL quals correctly with inheritance
@@ -418,20 +423,22 @@ SET constraint_exclusion TO ON;
 -- Ensure that we get a dummy plan
 EXPLAIN (COSTS OFF)
 SELECT * FROM pred_tab1, pred_tab2 WHERE pred_tab2.a IS NULL;
-        QUERY PLAN        
---------------------------
+                QUERY PLAN                
+------------------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Join on pred_tab1, pred_tab2
+(3 rows)
 
 -- Ensure that we get a dummy plan
 EXPLAIN (COSTS OFF)
 SELECT * FROM pred_tab2, pred_tab1 WHERE pred_tab1.a IS NULL OR pred_tab1.b < 2;
-        QUERY PLAN        
---------------------------
+                QUERY PLAN                
+------------------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Join on pred_tab2, pred_tab1
+(3 rows)
 
 RESET constraint_exclusion;
 DROP TABLE pred_tab1;
diff --git a/src/test/regress/expected/rowsecurity.out b/src/test/regress/expected/rowsecurity.out
index 8c879509313..e7fe3317638 100644
--- a/src/test/regress/expected/rowsecurity.out
+++ b/src/test/regress/expected/rowsecurity.out
@@ -3607,7 +3607,8 @@ EXPLAIN (COSTS OFF) SELECT * FROM t1;
 --------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on t1
+(3 rows)
 
 SET SESSION AUTHORIZATION regress_rls_bob;
 SELECT * FROM t1;
@@ -3620,7 +3621,8 @@ EXPLAIN (COSTS OFF) SELECT * FROM t1;
 --------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on t1
+(3 rows)
 
 --
 -- COPY TO/FROM
@@ -4524,11 +4526,12 @@ SELECT * FROM rls_tbl WHERE a <<< 1000;
 (0 rows)
 
 EXPLAIN (COSTS OFF) SELECT * FROM rls_tbl WHERE a <<< 1000 or a <<< 900;
-        QUERY PLAN        
---------------------------
+         QUERY PLAN          
+-----------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on rls_tbl
+(3 rows)
 
 RESET SESSION AUTHORIZATION;
 CREATE TABLE rls_child_tbl () INHERITS (rls_tbl);
diff --git a/src/test/regress/expected/rowtypes.out b/src/test/regress/expected/rowtypes.out
index 9168979a620..328b1e142c0 100644
--- a/src/test/regress/expected/rowtypes.out
+++ b/src/test/regress/expected/rowtypes.out
@@ -1324,7 +1324,8 @@ where false;
  Result
    Output: (a).f1, (a).f2
    One-Time Filter: false
-(3 rows)
+   Replaces: Scan on ss
+(4 rows)
 
 explain (verbose, costs off)
 with cte(c) as materialized (select row(1, 2)),
@@ -1349,10 +1350,11 @@ where false;
  Result
    Output: (cte.c).f1
    One-Time Filter: false
+   Replaces: Scan on cte
    CTE cte
      ->  Result
            Output: '(1,2)'::record
-(6 rows)
+(7 rows)
 
 --
 -- Tests for component access / FieldSelect
diff --git a/src/test/regress/expected/select.out b/src/test/regress/expected/select.out
index bab0cc93ff5..d1736dac224 100644
--- a/src/test/regress/expected/select.out
+++ b/src/test/regress/expected/select.out
@@ -962,10 +962,11 @@ create table list_parted_tbl (a int,b int) partition by list (a);
 create table list_parted_tbl1 partition of list_parted_tbl
   for values in (1) partition by list(b);
 explain (costs off) select * from list_parted_tbl;
-        QUERY PLAN        
---------------------------
+             QUERY PLAN              
+-------------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on list_parted_tbl
+(3 rows)
 
 drop table list_parted_tbl;
diff --git a/src/test/regress/expected/subselect.out b/src/test/regress/expected/subselect.out
index 0563d0cd5a1..db061ee95b1 100644
--- a/src/test/regress/expected/subselect.out
+++ b/src/test/regress/expected/subselect.out
@@ -2851,12 +2851,13 @@ ON B.hundred in (SELECT min(c.hundred) FROM tenk2 C WHERE c.odd = b.odd);
                      ->  Subquery Scan on "ANY_subquery"
                            Filter: (b.hundred = "ANY_subquery".min)
                            ->  Result
+                                 Replaces: Aggregate
                                  InitPlan 1
                                    ->  Limit
                                          ->  Index Scan using tenk2_hundred on tenk2 c
                                                Index Cond: (hundred IS NOT NULL)
                                                Filter: (odd = b.odd)
-(16 rows)
+(17 rows)
 
 --
 -- Test VALUES to ARRAY (VtA) transformation
@@ -3020,7 +3021,8 @@ EXPLAIN (COSTS OFF) EXECUTE test(NULL, 3.14, NULL);
 --------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on onek
+(3 rows)
 
 EXPLAIN (COSTS OFF) EXECUTE test(NULL, 3.14, '-1.5');
                                                                      QUERY PLAN                                                                      
diff --git a/src/test/regress/expected/tsrf.out b/src/test/regress/expected/tsrf.out
index d47b5f6ec57..e73e68af971 100644
--- a/src/test/regress/expected/tsrf.out
+++ b/src/test/regress/expected/tsrf.out
@@ -92,7 +92,8 @@ SELECT unnest(ARRAY[1, 2]) FROM few WHERE false;
    Output: unnest('{1,2}'::integer[])
    ->  Result
          One-Time Filter: false
-(4 rows)
+         Replaces: Scan on few
+(5 rows)
 
 SELECT unnest(ARRAY[1, 2]) FROM few WHERE false;
  unnest 
@@ -108,7 +109,8 @@ SELECT * FROM few f1,
  Result
    Output: f1.id, f1.dataa, f1.datab, ss.unnest
    One-Time Filter: false
-(3 rows)
+   Replaces: Join on f1, ss
+(4 rows)
 
 SELECT * FROM few f1,
   (SELECT unnest(ARRAY[1,2]) FROM few f2 WHERE false OFFSET 0) ss;
-- 
2.39.5 (Apple Git-154)

v4-0003-Assert-that-RTIs-of-joined-rels-are-discoverable-.patchapplication/octet-stream; name=v4-0003-Assert-that-RTIs-of-joined-rels-are-discoverable-.patchDownload
From e9b369dfae28dd62a5c50898a5f84c0942701d87 Mon Sep 17 00:00:00 2001
From: Robert Haas <rhaas@postgresql.org>
Date: Fri, 5 Sep 2025 13:26:57 -0400
Subject: [PATCH v4 3/7] Assert that RTIs of joined rels are discoverable from
 join plans.

Every RTI that appears in the joinrel's relid set should be findable
via the outer or inner plan, except for join RTIs which aren't
necessarily preserved in the final plan. This is a requirement if
we want to be able 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.

Note that this depends on the earlier commit to add a relids field to
Result nodes; without that change, a join tree involving two or more
Result nodes would be fundamentally ambiguous (and even a join tree
involving one could only be interpreted by guessing at its origin).
---
 src/backend/optimizer/plan/createplan.c | 153 ++++++++++++++++++++++++
 1 file changed, 153 insertions(+)

diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index 6f8fac85d48..1f33f0d97f9 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -320,7 +320,13 @@ static ModifyTable *make_modifytable(PlannerInfo *root, Plan *subplan,
 									 int epqParam);
 static GatherMerge *create_gather_merge_plan(PlannerInfo *root,
 											 GatherMergePath *best_path);
+static void assert_join_preserves_scan_rtis(PlannerInfo *root, Path *best_path,
+											Plan *outer_plan,
+											Plan *inner_plan);
 
+#ifdef USE_ASSERT_CHECKING
+static Bitmapset *get_scanned_rtindexes(PlannerInfo *root, Plan *plan);
+#endif
 
 /*
  * create_plan
@@ -4349,6 +4355,9 @@ create_nestloop_plan(PlannerInfo *root,
 
 	copy_generic_path_info(&join_plan->join.plan, &best_path->jpath.path);
 
+	assert_join_preserves_scan_rtis(root, &best_path->jpath.path, outer_plan,
+									inner_plan);
+
 	return join_plan;
 }
 
@@ -4703,6 +4712,9 @@ create_mergejoin_plan(PlannerInfo *root,
 	/* Costs of sort and material steps are included in path cost already */
 	copy_generic_path_info(&join_plan->join.plan, &best_path->jpath.path);
 
+	assert_join_preserves_scan_rtis(root, &best_path->jpath.path,
+									outer_plan, inner_plan);
+
 	return join_plan;
 }
 
@@ -4876,6 +4888,9 @@ create_hashjoin_plan(PlannerInfo *root,
 
 	copy_generic_path_info(&join_plan->join.plan, &best_path->jpath.path);
 
+	assert_join_preserves_scan_rtis(root, &best_path->jpath.path,
+									outer_plan, inner_plan);
+
 	return join_plan;
 }
 
@@ -7326,3 +7341,141 @@ is_projection_capable_plan(Plan *plan)
 	}
 	return true;
 }
+
+/*
+ * Check that the RTIs of the relations being joined at this level are
+ * properly reflected in the Plan tree.
+ *
+ * We expect to find every non-RTE_JOIN RTI from best_path->parent.relids
+ * mentioned in either the outer or inner subplan.
+ */
+static void
+assert_join_preserves_scan_rtis(PlannerInfo *root, Path *best_path,
+								Plan *outer_plan, Plan *inner_plan)
+{
+#ifdef USE_ASSERT_CHECKING
+	Bitmapset  *outer_scanrelids;
+	Bitmapset  *inner_scanrelids;
+	Bitmapset  *combined_scanrelids;
+	Bitmapset  *joinrelids;
+
+	/* Find inner and outer RTI sets and combine them. */
+	outer_scanrelids = get_scanned_rtindexes(root, outer_plan);
+	inner_scanrelids = get_scanned_rtindexes(root, inner_plan);
+	combined_scanrelids = bms_union(outer_scanrelids, inner_scanrelids);
+
+	/* Remove join RTIs. Inner join RTIs won't appear here. */
+	combined_scanrelids = bms_difference(combined_scanrelids,
+										 root->outer_join_rels);
+
+	/* Similarly, remove join RTIs from the join's RTI set. */
+	joinrelids = bms_difference(best_path->parent->relids,
+								root->outer_join_rels);
+
+	/* Any given scan RTI should appear on only one side or the other. */
+	Assert(!bms_overlap(inner_scanrelids, outer_scanrelids));
+
+	/*
+	 * If this assertion fails, it means that the set of range table indexes
+	 * that we found in the inner and outer path tree did not equal the set of
+	 * range table indexes that we found for this joinrel, even after
+	 * excluding RTE_JOIN range table indexes which are not expect to appear
+	 * in the plan tree.
+	 *
+	 * If this assertion fails due to the addition of a new executor node
+	 * type, you probably just need to update get_scanned_rtindexes to know
+	 * about the new node. See the header comments for that function for other
+	 * places to update at the same time.
+	 */
+	Assert(bms_equal(combined_scanrelids, joinrelids));
+#endif
+}
+
+#ifdef USE_ASSERT_CHECKING
+/*
+ * Get the set of range table indexes that are scanned by a scan or join node,
+ * or any executor node that could appear beneath a scan or join node.
+ *
+ * We are uninterested in join RTIs here; we're only interested in which RTIs
+ * are scanned at or below a particular plan node, and only if that node can
+ * appear beneath a join.
+ *
+ * When adding new cases to this function, be sure to also update
+ * ExplainPreScanNode, ExplainNode, and overexplain_per_node_hook as
+ * appropriate.
+ */
+static Bitmapset *
+get_scanned_rtindexes(PlannerInfo *root, Plan *plan)
+{
+	switch (nodeTag(plan))
+	{
+		case T_SeqScan:
+		case T_SampleScan:
+		case T_IndexScan:
+		case T_IndexOnlyScan:
+		case T_BitmapHeapScan:
+		case T_TidScan:
+		case T_TidRangeScan:
+		case T_SubqueryScan:
+		case T_FunctionScan:
+		case T_TableFuncScan:
+		case T_ValuesScan:
+		case T_CteScan:
+		case T_NamedTuplestoreScan:
+		case T_WorkTableScan:
+			return bms_make_singleton(((Scan *) plan)->scanrelid);
+			break;
+		case T_ForeignScan:
+			return ((ForeignScan *) plan)->fs_base_relids;
+			break;
+		case T_CustomScan:
+			return ((CustomScan *) plan)->custom_relids;
+			break;
+		case T_Append:
+			return ((Append *) plan)->apprelids;
+			break;
+		case T_MergeAppend:
+			return ((MergeAppend *) plan)->apprelids;
+			break;
+		case T_Result:
+			if (plan->lefttree)
+				return get_scanned_rtindexes(root, plan->lefttree);
+			else
+				return ((Result *) plan)->relids;
+			break;
+		case T_HashJoin:
+		case T_MergeJoin:
+		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);
+
+				return combined_scanrelids;
+				break;
+			}
+		case T_Sort:
+		case T_IncrementalSort:
+		case T_Unique:
+		case T_Agg:
+		case T_Hash:
+		case T_Gather:
+		case T_GatherMerge:
+		case T_Material:
+		case T_Memoize:
+			return get_scanned_rtindexes(root, plan->lefttree);
+			break;
+		default:
+			break;
+	}
+
+	return NULL;
+}
+#endif
-- 
2.39.5 (Apple Git-154)

v4-0005-Store-information-about-range-table-flattening-in.patchapplication/octet-stream; name=v4-0005-Store-information-about-range-table-flattening-in.patchDownload
From 04401d05a725656e2f779091d143cc89723585f8 Mon Sep 17 00:00:00 2001
From: Robert Haas <rhaas@postgresql.org>
Date: Fri, 21 Mar 2025 11:06:35 -0400
Subject: [PATCH v4 5/7] Store information about range-table flattening in the
 final plan.

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.  So,
this commit remembers in the final plan which part of the final range
table came from which subquery's range table.

Additionally, this commit teaches pg_overexplain'e RANGE_TABLE option
to display the subquery name for each range table entry.
---
 contrib/pg_overexplain/pg_overexplain.c | 36 +++++++++++++++++++++++++
 src/backend/optimizer/plan/planner.c    |  1 +
 src/backend/optimizer/plan/setrefs.c    | 20 ++++++++++++++
 src/include/nodes/pathnodes.h           |  3 +++
 src/include/nodes/plannodes.h           | 17 ++++++++++++
 src/tools/pgindent/typedefs.list        |  1 +
 6 files changed, 78 insertions(+)

diff --git a/contrib/pg_overexplain/pg_overexplain.c b/contrib/pg_overexplain/pg_overexplain.c
index bd70b6d9d5e..5dc707d69e3 100644
--- a/contrib/pg_overexplain/pg_overexplain.c
+++ b/contrib/pg_overexplain/pg_overexplain.c
@@ -395,6 +395,8 @@ static void
 overexplain_range_table(PlannedStmt *plannedstmt, ExplainState *es)
 {
 	Index		rti;
+	ListCell   *lc_subrtinfo = list_head(plannedstmt->subrtinfos);
+	SubPlanRTInfo *rtinfo = NULL;
 
 	/* Open group, one entry per RangeTblEntry */
 	ExplainOpenGroup("Range Table", "Range Table", false, es);
@@ -405,6 +407,18 @@ overexplain_range_table(PlannedStmt *plannedstmt, ExplainState *es)
 		RangeTblEntry *rte = rt_fetch(rti, plannedstmt->rtable);
 		char	   *kind = NULL;
 		char	   *relkind;
+		SubPlanRTInfo *next_rtinfo;
+
+		/* Advance to next SubRTInfo, if it's time. */
+		if (lc_subrtinfo != NULL)
+		{
+			next_rtinfo = lfirst(lc_subrtinfo);
+			if (rti > next_rtinfo->rtoffset)
+			{
+				rtinfo = next_rtinfo;
+				lc_subrtinfo = lnext(plannedstmt->subrtinfos, lc_subrtinfo);
+			}
+		}
 
 		/* NULL entries are possible; skip them */
 		if (rte == NULL)
@@ -469,6 +483,28 @@ overexplain_range_table(PlannedStmt *plannedstmt, ExplainState *es)
 			ExplainPropertyBool("In From Clause", rte->inFromCl, es);
 		}
 
+		/*
+		 * Indicate which subplan is the origin of which RTE. Note dummy
+		 * subplans. Here again, we crunch more onto one line in text format.
+		 */
+		if (rtinfo != NULL)
+		{
+			if (es->format == EXPLAIN_FORMAT_TEXT)
+			{
+				if (!rtinfo->dummy)
+					ExplainPropertyText("Subplan", rtinfo->plan_name, es);
+				else
+					ExplainPropertyText("Subplan",
+										psprintf("%s (dummy)",
+												 rtinfo->plan_name), es);
+			}
+			else
+			{
+				ExplainPropertyText("Subplan", rtinfo->plan_name, es);
+				ExplainPropertyBool("Subplan Is Dummy", rtinfo->dummy, es);
+			}
+		}
+
 		/* rte->alias is optional; rte->eref is requested */
 		if (rte->alias != NULL)
 			overexplain_alias("Alias", rte->alias, es);
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index 988bbd19ab4..59ee3f319f4 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -579,6 +579,7 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions,
 	result->unprunableRelids = bms_difference(glob->allRelids,
 											  glob->prunableRelids);
 	result->permInfos = glob->finalrteperminfos;
+	result->subrtinfos = glob->subrtinfos;
 	result->resultRelations = glob->resultRelations;
 	result->appendRelations = glob->appendRelations;
 	result->subplans = glob->subplans;
diff --git a/src/backend/optimizer/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.c
index 6950eff2c5b..eef43792aeb 100644
--- a/src/backend/optimizer/plan/setrefs.c
+++ b/src/backend/optimizer/plan/setrefs.c
@@ -399,6 +399,26 @@ add_rtes_to_flat_rtable(PlannerInfo *root, bool recursing)
 	Index		rti;
 	ListCell   *lc;
 
+	/*
+	 * Record enough information to make it possible for code that looks at
+	 * the final range table to understand how it was constructed. (If
+	 * finalrtable is still NIL, then this is the very topmost PlannerInfo,
+	 * which will always have plan_name == NULL and rtoffset == 0; we omit the
+	 * degenerate list entry.)
+	 */
+	if (root->glob->finalrtable != NIL)
+	{
+		SubPlanRTInfo *rtinfo = makeNode(SubPlanRTInfo);
+
+		rtinfo->plan_name = root->plan_name;
+		rtinfo->rtoffset = list_length(root->glob->finalrtable);
+
+		/* When recursing = true, it's an unplanned or dummy subquery. */
+		rtinfo->dummy = recursing;
+
+		root->glob->subrtinfos = lappend(root->glob->subrtinfos, rtinfo);
+	}
+
 	/*
 	 * Add the query's own RTEs to the flattened rangetable.
 	 *
diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h
index e41597acb02..4f8586f6591 100644
--- a/src/include/nodes/pathnodes.h
+++ b/src/include/nodes/pathnodes.h
@@ -135,6 +135,9 @@ typedef struct PlannerGlobal
 	/* "flat" list of RTEPermissionInfos */
 	List	   *finalrteperminfos;
 
+	/* list of SubPlanRTInfo nodes */
+	List	   *subrtinfos;
+
 	/* "flat" list of PlanRowMarks */
 	List	   *finalrowmarks;
 
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index 1abcf90be43..3f2d6fafc24 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -131,6 +131,9 @@ typedef struct PlannedStmt
 	 */
 	List	   *subplans;
 
+	/* a list of SubPlanRTInfo objects */
+	List	   *subrtinfos;
+
 	/* indices of subplans that require REWIND */
 	Bitmapset  *rewindPlanIDs;
 
@@ -1801,4 +1804,18 @@ typedef enum MonotonicFunction
 	MONOTONICFUNC_BOTH = MONOTONICFUNC_INCREASING | MONOTONICFUNC_DECREASING,
 } MonotonicFunction;
 
+/*
+ * SubPlanRTInfo
+ *
+ * Information about which range table entries came from which subquery
+ * planning cycles.
+ */
+typedef struct SubPlanRTInfo
+{
+	NodeTag		type;
+	char	   *plan_name;
+	Index		rtoffset;
+	bool		dummy;
+} SubPlanRTInfo;
+
 #endif							/* PLANNODES_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index a13e8162890..8e4367dabaf 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -4347,3 +4347,4 @@ yyscan_t
 z_stream
 z_streamp
 zic_t
+SubPlanRTInfo
-- 
2.39.5 (Apple Git-154)

v4-0002-Consider-a-Result-node-s-relids-in-ExplainPreScan.patchapplication/octet-stream; name=v4-0002-Consider-a-Result-node-s-relids-in-ExplainPreScan.patchDownload
From 9567b8b9b323945a62217ded9478e0095387ccc1 Mon Sep 17 00:00:00 2001
From: Robert Haas <rhaas@postgresql.org>
Date: Mon, 21 Apr 2025 13:35:28 -0400
Subject: [PATCH v4 2/7] Consider a Result node's relids in ExplainPreScanNode.

Now that a Result node has a relids set, add the relids that it
carries the set accumulated by ExplainPreScanNode so that we
generate unique relation aliases for all of the referenced relations
when it calls select_rtable_names_for_explain. The effect of this
changes is that a few things get schema-qualified in the regression
test outputs that previously were not. In similar cases not involving
a Result node, we were already schema-qualifying, so this appears to
be an improvement.

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.
---
 src/backend/commands/explain.c                |  4 +++
 .../regress/expected/generated_virtual.out    |  8 ++---
 src/test/regress/expected/join.out            | 32 +++++++++----------
 src/test/regress/expected/partition_join.out  | 12 +++----
 4 files changed, 30 insertions(+), 26 deletions(-)

diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 42723c3a150..e8bd4c560dd 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -1230,6 +1230,10 @@ ExplainPreScanNode(PlanState *planstate, Bitmapset **rels_used)
 			*rels_used = bms_add_members(*rels_used,
 										 ((MergeAppend *) plan)->apprelids);
 			break;
+		case T_Result:
+			*rels_used = bms_add_members(*rels_used,
+										 ((Result *) plan)->relids);
+			break;
 		default:
 			break;
 	}
diff --git a/src/test/regress/expected/generated_virtual.out b/src/test/regress/expected/generated_virtual.out
index 8b25e77bb4d..70727623bca 100644
--- a/src/test/regress/expected/generated_virtual.out
+++ b/src/test/regress/expected/generated_virtual.out
@@ -1570,15 +1570,15 @@ where coalesce(t2.b, 1) = 2 or t1.a is null;
 -- Ensure that the generation expressions are wrapped into PHVs if needed
 explain (verbose, costs off)
 select t2.* from gtest32 t1 left join gtest32 t2 on false;
-                      QUERY PLAN                      
-------------------------------------------------------
+                          QUERY PLAN                           
+---------------------------------------------------------------
  Nested Loop Left Join
-   Output: a, (a * 2), (20), (COALESCE(a, 100)), e
+   Output: t2.a, (t2.a * 2), (20), (COALESCE(t2.a, 100)), t2.e
    Join Filter: false
    ->  Seq Scan on generated_virtual_tests.gtest32 t1
          Output: t1.a, t1.b, t1.c, t1.d, t1.e
    ->  Result
-         Output: a, e, 20, COALESCE(a, 100)
+         Output: t2.a, t2.e, 20, COALESCE(t2.a, 100)
          One-Time Filter: false
          Replaces: Scan on t2
 (9 rows)
diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out
index 01ce1aa577c..f73df838f14 100644
--- a/src/test/regress/expected/join.out
+++ b/src/test/regress/expected/join.out
@@ -4151,9 +4151,9 @@ select * from t t1
                         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)
@@ -4166,7 +4166,7 @@ select * from t t1
                      ->  Seq Scan on pg_temp.t t2
                            Output: t2.i2, 1
          ->  Result
-               Output: i3
+               Output: t3.i3
                One-Time Filter: false
                Replaces: Scan on t3
    ->  Memoize
@@ -6069,7 +6069,7 @@ on t1.q1 = t2.q1;
                  QUERY PLAN                 
 --------------------------------------------
  Hash Left Join
-   Hash Cond: (t1.q1 = q1)
+   Hash Cond: (t1.q1 = t2.q1)
    ->  Seq Scan on int8_tbl t1
    ->  Hash
          ->  Result
@@ -6086,7 +6086,7 @@ on t1.q1 = t2.q1;
                  QUERY PLAN                 
 --------------------------------------------
  Hash Left Join
-   Hash Cond: (t1.q1 = q1)
+   Hash Cond: (t1.q1 = t2.q1)
    ->  Seq Scan on int8_tbl t1
    ->  Hash
          ->  Result
@@ -6416,13 +6416,13 @@ SELECT q2 FROM
                       QUERY PLAN                      
 ------------------------------------------------------
  Nested Loop Left Join
-   Output: q2
+   Output: int8_tbl.q2
    Join Filter: NULL::boolean
    Filter: (('constant'::text) >= ('constant'::text))
    ->  Seq Scan on public.int4_tbl
          Output: int4_tbl.f1
    ->  Result
-         Output: q2, 'constant'::text
+         Output: int8_tbl.q2, 'constant'::text
          One-Time Filter: false
          Replaces: Scan on int8_tbl
 (10 rows)
@@ -6897,7 +6897,7 @@ where q1.x = q2.y;
 --------------------------
  Result
    One-Time Filter: false
-   Replaces: Scan on sj_1
+   Replaces: Scan on sj
 (3 rows)
 
 -- We can't use a cross-EC generated self join qual because of current logic of
@@ -8887,15 +8887,15 @@ select * from int4_tbl t1,
 explain (verbose, costs off)
 select * from int8_tbl i8 left join lateral
   (select *, i8.q2 from int4_tbl where false) ss on true;
-              QUERY PLAN              
---------------------------------------
+                  QUERY PLAN                  
+----------------------------------------------
  Nested Loop Left Join
-   Output: i8.q1, i8.q2, f1, (i8.q2)
+   Output: i8.q1, i8.q2, int4_tbl.f1, (i8.q2)
    Join Filter: false
    ->  Seq Scan on public.int8_tbl i8
          Output: i8.q1, i8.q2
    ->  Result
-         Output: f1, i8.q2
+         Output: int4_tbl.f1, i8.q2
          One-Time Filter: false
          Replaces: Scan on int4_tbl
 (9 rows)
@@ -8903,14 +8903,14 @@ select * from int8_tbl i8 left join lateral
 explain (verbose, costs off)
 select * from int8_tbl i8 left join lateral
   (select *, i8.q2 from int4_tbl i1, int4_tbl i2 where false) ss on true;
-               QUERY PLAN                
------------------------------------------
+                  QUERY PLAN                   
+-----------------------------------------------
  Nested Loop Left Join
-   Output: i8.q1, i8.q2, f1, f1, (i8.q2)
+   Output: i8.q1, i8.q2, i1.f1, i2.f1, (i8.q2)
    ->  Seq Scan on public.int8_tbl i8
          Output: i8.q1, i8.q2
    ->  Result
-         Output: f1, f1, i8.q2
+         Output: i1.f1, i2.f1, i8.q2
          One-Time Filter: false
          Replaces: Join on i1, i2
 (8 rows)
diff --git a/src/test/regress/expected/partition_join.out b/src/test/regress/expected/partition_join.out
index 0c495907eca..c94108eb118 100644
--- a/src/test/regress/expected/partition_join.out
+++ b/src/test/regress/expected/partition_join.out
@@ -1630,7 +1630,7 @@ SELECT t1.a, t1.c, t2.b, t2.c FROM (SELECT * FROM prt1 WHERE a = 1 AND a = 2) t1
                     QUERY PLAN                    
 --------------------------------------------------
  Hash Left Join
-   Hash Cond: (t2.b = a)
+   Hash Cond: (t2.b = prt1.a)
    ->  Append
          ->  Hash Join
                Hash Cond: (t3_1.a = t2_1.b)
@@ -1658,9 +1658,9 @@ SELECT t1.a, t1.c, t2.b, t2.c FROM (SELECT * FROM prt1 WHERE a = 1 AND a = 2) t1
                  QUERY PLAN                 
 --------------------------------------------
  Sort
-   Sort Key: a, t2.b
+   Sort Key: prt1.a, t2.b
    ->  Hash Left Join
-         Hash Cond: (t2.b = a)
+         Hash Cond: (t2.b = prt1.a)
          ->  Append
                ->  Seq Scan on prt2_p1 t2_1
                      Filter: (a = 0)
@@ -2246,10 +2246,10 @@ SELECT COUNT(*) FROM prt1_l t1 LEFT JOIN LATERAL
 -- join with one side empty
 EXPLAIN (COSTS OFF)
 SELECT t1.a, t1.c, t2.b, t2.c FROM (SELECT * FROM prt1_l WHERE a = 1 AND a = 2) t1 RIGHT JOIN prt2_l t2 ON t1.a = t2.b AND t1.b = t2.a AND t1.c = t2.c;
-                               QUERY PLAN                                
--------------------------------------------------------------------------
+                                          QUERY PLAN                                          
+----------------------------------------------------------------------------------------------
  Hash Left Join
-   Hash Cond: ((t2.b = a) AND (t2.a = b) AND ((t2.c)::text = (c)::text))
+   Hash Cond: ((t2.b = prt1_l.a) AND (t2.a = prt1_l.b) AND ((t2.c)::text = (prt1_l.c)::text))
    ->  Append
          ->  Seq Scan on prt2_l_p1 t2_1
          ->  Seq Scan on prt2_l_p2_p1 t2_2
-- 
2.39.5 (Apple Git-154)

v4-0004-Give-subplans-names-that-are-known-while-planning.patchapplication/octet-stream; name=v4-0004-Give-subplans-names-that-are-known-while-planning.patchDownload
From 4fca914fac6e036662c526aa150f28c51f9a6dd8 Mon Sep 17 00:00:00 2001
From: Robert Haas <rhaas@postgresql.org>
Date: Thu, 5 Dec 2024 15:19:17 -0500
Subject: [PATCH v4 4/7] Give subplans names that are known while planning that
 subplan.

Previously, subplans were shown in EXPLAIN output identified by
a number, like "InitPlan 1", and some were identified by a name,
like "CTE foo". Now, each subplan gets a name, which for InitPlans
and SubPlans is based on the type of sublink e.g. expr_1 or any_1,
and these names are guaranteed to be unique across the whole plan.

The numerical portion of the name may be different than it was
previously, because InitPlan 1 meant the first subplan that we
finished planning (which happened to be an InitPlan). This number
couldn't be known at the time we began planning that subplan,
because the query planner might recurse into other subplans which
would then be fully planned before finishing the plan at the outer
level. These new subplan names are assigned when we *start* planning
a subplan, which allows extensions that affect planning to know the
name that will ultimately be assigned while planning is still in
progress.

Some subplans aren't shown as subplans in EXPLAIN output. This
happens when the subquery is a FROM-cluse item or a branch of a
set operation, rather than, for example, an expression that will
be transformed into something render as an InitPlan or SubPlan.
These subplans also get unique names, although those names are not
currently shown in the EXPLAIN output. This means that it's now
possible to use unique, human-readable names to refer to any
subplan within a query; only the topmost query level is nameless.
---
 .../postgres_fdw/expected/postgres_fdw.out    |  58 ++--
 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/include/nodes/pathnodes.h                 |   6 +
 src/include/nodes/primnodes.h                 |   1 +
 src/include/optimizer/planner.h               |   5 +-
 src/test/regress/expected/aggregates.out      |  58 ++--
 src/test/regress/expected/create_index.out    |  14 +-
 src/test/regress/expected/groupingsets.out    |  90 +++---
 .../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 | 292 +++++++++---------
 src/test/regress/expected/portals.out         |  12 +-
 src/test/regress/expected/predicate.out       |   8 +-
 src/test/regress/expected/returning.out       |  20 +-
 src/test/regress/expected/rowsecurity.out     | 138 ++++-----
 src/test/regress/expected/rowtypes.out        |  12 +-
 src/test/regress/expected/select_parallel.out |  52 ++--
 src/test/regress/expected/sqljson.out         |   4 +-
 src/test/regress/expected/subselect.out       | 172 +++++------
 src/test/regress/expected/updatable_views.out |  48 +--
 src/test/regress/expected/update.out          |   8 +-
 src/test/regress/expected/window.out          |  10 +-
 src/test/regress/expected/with.out            |   8 +-
 34 files changed, 754 insertions(+), 623 deletions(-)

diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 04aba992a88..44e169b5baa 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -3178,10 +3178,10 @@ select exists(select 1 from pg_enum), sum(c1) from ft1;
                     QUERY PLAN                    
 --------------------------------------------------
  Foreign Scan
-   Output: (InitPlan 1).col1, (sum(ft1.c1))
+   Output: (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: (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: ((expr_1))
    ->  Sort
-         Output: ((SubPlan 1))
-         Sort Key: ((SubPlan 1))
+         Output: ((expr_1))
+         Sort Key: ((expr_1))
          ->  Foreign Scan
-               Output: (SubPlan 1)
+               Output: (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: ((expr_1))
    ->  Sort
-         Output: ((SubPlan 1))
-         Sort Key: ((SubPlan 1))
+         Output: ((expr_1))
+         Sort Key: ((expr_1))
          ->  Foreign Scan on public.ft2 t2
-               Output: (SubPlan 1)
+               Output: (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 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))
@@ -6447,9 +6447,9 @@ UPDATE ft2 AS target SET (c2, c7) = (
  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: (multiexpr_1).col1, (multiexpr_1).col2, (rescan 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"))
@@ -12133,9 +12133,9 @@ SELECT * FROM local_tbl t1 LEFT JOIN (SELECT *, (SELECT count(*) FROM async_pt W
                                        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, ((expr_1).col1)
    Join Filter: (t1.a = async_pt.a)
-   InitPlan 1
+   InitPlan expr_1
      ->  Aggregate
            Output: count(*)
            ->  Append
@@ -12147,10 +12147,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, (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, (expr_1).col1
                Remote SQL: SELECT a, b, c FROM public.base_tbl2 WHERE ((a < 3000))
 (20 rows)
 
@@ -12161,7 +12161,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)
@@ -12384,12 +12384,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 = (any_1).col1) AND ((random() > '0'::double precision) = (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 e8bd4c560dd..77dca0a54cf 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -4898,6 +4898,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
@@ -4921,8 +4922,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..d19d3e4e107 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, subroot);
+
 	/*----------
 	 * 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..988bbd19ab4 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, 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 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 d71ed958e31..1aa680e9865 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 char *sublinktype_to_string(SubLinkType subLinkType);
 
 
 /*
@@ -172,6 +173,7 @@ make_subplan(PlannerInfo *root, Query *orig_subquery,
 	Plan	   *plan;
 	List	   *plan_params;
 	Node	   *result;
+	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, &paramIds);
 		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 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/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h
index 4a903d1ec18..e41597acb02 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;
 
@@ -234,6 +237,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..af50831c814 100644
--- a/src/include/optimizer/planner.h
+++ b/src/include/optimizer/planner.h
@@ -43,7 +43,7 @@ extern PlannedStmt *standard_planner(Query *parse, const char *query_string,
 									 ParamListInfo boundParams);
 
 extern PlannerInfo *subquery_planner(PlannerGlobal *glob, Query *parse,
-									 PlannerInfo *parent_root,
+									 char *plan_name, PlannerInfo *parent_root,
 									 bool hasRecursion, double tuple_fraction,
 									 SetOperationStmt *setops);
 
@@ -62,4 +62,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, 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 ce45de7e175..f4ee1d27724 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(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: Aggregate
-   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: Aggregate
-   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: Aggregate
-   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: Aggregate
-   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: Aggregate
-   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: Aggregate
-   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: Aggregate
-   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: Aggregate
-           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: (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: ((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: ((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: (((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: Aggregate
-   InitPlan 1
+   InitPlan minmax_1
      ->  Limit
            ->  Result
                  One-Time Filter: (100 IS NOT NULL)
@@ -1258,7 +1258,7 @@ explain (costs off)
 ---------------------------------------------------------------------------------------------
  Result
    Replaces: Aggregate
-   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_2
      ->  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_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 1).col1), ((InitPlan 2).col1)
+         Sort Key: ((minmax_1).col1), ((minmax_2).col1)
          ->  Result
                Replaces: Aggregate
 (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: (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..dff9a687145 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, (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 = (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 07a37da79dd..39b7e1d2a35 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((expr_1)), ((expr_2))
+   Group Key: ((expr_2))
    ->  Sort
-         Output: ((SubPlan 2)), i1.q1
-         Sort Key: ((SubPlan 2))
+         Output: ((expr_2)), i1.q1
+         Sort Key: ((expr_2))
          ->  Seq Scan on public.int8_tbl i1
-               Output: (SubPlan 2), i1.q1
-               SubPlan 2
+               Output: (expr_2), i1.q1
+               SubPlan expr_2
                  ->  Result
                        Output: i1.q1
 (11 rows)
@@ -537,19 +537,19 @@ group by ss.x;
                  QUERY PLAN                 
 --------------------------------------------
  GroupAggregate
-   Output: (SubPlan 2), ((SubPlan 3))
-   Group Key: ((SubPlan 3))
+   Output: (expr_1), ((expr_3))
+   Group Key: ((expr_3))
    ->  Sort
-         Output: ((SubPlan 3)), i1.q1
-         Sort Key: ((SubPlan 3))
+         Output: ((expr_3)), i1.q1
+         Sort Key: ((expr_3))
          ->  Seq Scan on public.int8_tbl i1
-               Output: (SubPlan 3), i1.q1
-               SubPlan 3
+               Output: (expr_3), i1.q1
+               SubPlan expr_3
                  ->  Result
                        Output: i1.q1
-   SubPlan 2
+   SubPlan expr_1
      ->  Result
-           Output: GROUPING((SubPlan 1))
+           Output: GROUPING((expr_2))
 (14 rows)
 
 select (select grouping(ss.x))
@@ -592,7 +592,7 @@ explain (costs off)
 ------------------------------------------------------------
  Result
    Replaces: Aggregate
-   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: (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::integer END), t1.v
-   Sort Key: (CASE WHEN (GROUPING((SubPlan 2)) = 0) THEN ((SubPlan 3)) ELSE NULL::integer END) NULLS FIRST
+   Output: (GROUPING((expr_1))), ((expr_3)), (CASE WHEN (GROUPING((expr_2)) = 0) THEN ((expr_3)) ELSE NULL::integer END), t1.v
+   Sort Key: (CASE WHEN (GROUPING((expr_2)) = 0) THEN ((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::integer END, t1.v
+         Output: GROUPING((expr_1)), ((expr_3)), CASE WHEN (GROUPING((expr_2)) = 0) THEN ((expr_3)) ELSE NULL::integer END, t1.v
          Hash Key: t1.v
-         Hash Key: (SubPlan 3)
+         Hash Key: (expr_3)
          ->  Seq Scan on pg_temp.gstest5 t1
-               Output: (SubPlan 3), t1.v, t1.id
-               SubPlan 3
+               Output: (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::integer END), t1.v
-   Sort Key: (CASE WHEN (GROUPING((SubPlan 2)) = 0) THEN ((SubPlan 3)) ELSE NULL::integer END) NULLS FIRST
+   Output: (GROUPING((expr_1))), ((expr_3)), (CASE WHEN (GROUPING((expr_2)) = 0) THEN ((expr_3)) ELSE NULL::integer END), t1.v
+   Sort Key: (CASE WHEN (GROUPING((expr_2)) = 0) THEN ((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::integer END, t1.v
+         Output: GROUPING((expr_1)), ((expr_3)), CASE WHEN (GROUPING((expr_2)) = 0) THEN ((expr_3)) ELSE NULL::integer END, t1.v
          Hash Key: t1.v
-         Hash Key: (SubPlan 3)
+         Hash Key: (expr_3)
          ->  Seq Scan on pg_temp.gstest5 t1
-               Output: (SubPlan 3), t1.v, t1.id
-               SubPlan 3
+               Output: (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..92fe3527baf 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, ((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, ((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 e1225fb1add..581f6859b8e 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: (minmax_1).col1
    Replaces: Aggregate
-   InitPlan 1
+   InitPlan minmax_1
      ->  Limit
            Output: ((1 - matest0.id))
            ->  Result
@@ -1948,7 +1948,7 @@ SELECT min(x) FROM
 --------------------------------------------------------------------
  Result
    Replaces: Aggregate
-   InitPlan 1
+   InitPlan minmax_1
      ->  Limit
            ->  Merge Append
                  Sort Key: a.unique1
@@ -1967,7 +1967,7 @@ SELECT min(y) FROM
 --------------------------------------------------------------------
  Result
    Replaces: Aggregate
-   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: (multiexpr_1).col1, (multiexpr_1).col2, (rescan 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: (multiexpr_1).col1, (multiexpr_1).col2, (rescan 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: (multiexpr_1).col1, (multiexpr_1).col2, (rescan 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: Aggregate
-   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_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))
diff --git a/src/test/regress/expected/insert_conflict.out b/src/test/regress/expected/insert_conflict.out
index fdd0f6c8f25..028ae01e307 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(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 f73df838f14..de290b2ff97 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 = (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: Aggregate
-                       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 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 = (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, (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, (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: ((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, (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, (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: ((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 = (any_1).col1) AND ((random() > '0'::double precision) = (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 = (any_1).col1) AND ((random() > '0'::double precision) = (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: (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 = (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 = (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..e96769114db 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 = (expr_1)) AND ((expr_2) = (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: ((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: ((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: (((expr_1) = hjtest_1.id) AND ((expr_3) = (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: ((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: ((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 150dc1b44cf..85aada38120 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(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 c748172e98f..40e63c7a0ab 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 < (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, (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 86cecc3ed71..b8f49c8e118 100644
--- a/src/test/regress/expected/partition_prune.out
+++ b/src/test/regress/expected/partition_prune.out
@@ -1921,15 +1921,15 @@ where asptab.id > ss.b::int;
    ->  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(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(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 < (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 < (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 < (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 < (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 < (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 < (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 = (expr_1).col1) OR (a = (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 = (expr_1).col1) OR (a = (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 = (expr_1).col1) OR (a = (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 = (expr_1).col1)
+         Filter: (b = (expr_2).col1)
          ->  Bitmap Index Scan on ab_a1_b1_a_idx (never executed)
-               Index Cond: (a = (InitPlan 1).col1)
+               Index Cond: (a = (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 = (expr_1).col1)
+         Filter: (b = (expr_2).col1)
          ->  Bitmap Index Scan on ab_a1_b2_a_idx (never executed)
-               Index Cond: (a = (InitPlan 1).col1)
+               Index Cond: (a = (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 = (expr_1).col1)
+         Filter: (b = (expr_2).col1)
          ->  Bitmap Index Scan on ab_a1_b3_a_idx (never executed)
-               Index Cond: (a = (InitPlan 1).col1)
+               Index Cond: (a = (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 = (expr_1).col1)
+         Filter: (b = (expr_2).col1)
          ->  Bitmap Index Scan on ab_a2_b1_a_idx (never executed)
-               Index Cond: (a = (InitPlan 1).col1)
+               Index Cond: (a = (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 = (expr_1).col1)
+         Filter: (b = (expr_2).col1)
          ->  Bitmap Index Scan on ab_a2_b2_a_idx (never executed)
-               Index Cond: (a = (InitPlan 1).col1)
+               Index Cond: (a = (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 = (expr_1).col1)
+         Filter: (b = (expr_2).col1)
          ->  Bitmap Index Scan on ab_a2_b3_a_idx (never executed)
-               Index Cond: (a = (InitPlan 1).col1)
+               Index Cond: (a = (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 = (expr_1).col1)
+         Filter: (b = (expr_2).col1)
          ->  Bitmap Index Scan on ab_a3_b1_a_idx (never executed)
-               Index Cond: (a = (InitPlan 1).col1)
+               Index Cond: (a = (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 = (expr_1).col1)
+         Filter: (b = (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 = (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 = (expr_1).col1)
+         Filter: (b = (expr_2).col1)
          ->  Bitmap Index Scan on ab_a3_b3_a_idx (never executed)
-               Index Cond: (a = (InitPlan 1).col1)
+               Index Cond: (a = (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 = (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 = (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 = (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 = (expr_1).col1)
    ->  Seq Scan on ab_a1_b2 ab_2 (never executed)
-         Filter: (b = (InitPlan 1).col1)
+         Filter: (b = (expr_1).col1)
    ->  Seq Scan on ab_a1_b3 ab_3 (never executed)
-         Filter: (b = (InitPlan 1).col1)
+         Filter: (b = (expr_1).col1)
    ->  Seq Scan on ab_a2_b1 ab_4 (actual rows=0.00 loops=1)
-         Filter: (b = (InitPlan 1).col1)
+         Filter: (b = (expr_1).col1)
    ->  Seq Scan on ab_a2_b2 ab_5 (never executed)
-         Filter: (b = (InitPlan 1).col1)
+         Filter: (b = (expr_1).col1)
    ->  Seq Scan on ab_a2_b3 ab_6 (never executed)
-         Filter: (b = (InitPlan 1).col1)
+         Filter: (b = (expr_1).col1)
    ->  Seq Scan on ab_a3_b1 ab_7 (actual rows=0.00 loops=1)
-         Filter: (b = (InitPlan 1).col1)
+         Filter: (b = (expr_1).col1)
    ->  Seq Scan on ab_a3_b2 ab_8 (never executed)
-         Filter: (b = (InitPlan 1).col1)
+         Filter: (b = (expr_1).col1)
    ->  Seq Scan on ab_a3_b3 ab_9 (never executed)
-         Filter: (b = (InitPlan 1).col1)
+         Filter: (b = (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 = (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 = (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 = (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 = (expr_1).col1)
    ->  Seq Scan on ab_a1_b1 ab_1 (actual rows=0.00 loops=1)
-         Filter: (b = (InitPlan 1).col1)
+         Filter: (b = (expr_1).col1)
    ->  Seq Scan on ab_a1_b2 ab_2 (never executed)
-         Filter: (b = (InitPlan 1).col1)
+         Filter: (b = (expr_1).col1)
    ->  Seq Scan on ab_a1_b3 ab_3 (never executed)
-         Filter: (b = (InitPlan 1).col1)
+         Filter: (b = (expr_1).col1)
    ->  Seq Scan on ab_a2_b1 ab_4 (actual rows=0.00 loops=1)
-         Filter: (b = (InitPlan 1).col1)
+         Filter: (b = (expr_1).col1)
    ->  Seq Scan on ab_a2_b2 ab_5 (never executed)
-         Filter: (b = (InitPlan 1).col1)
+         Filter: (b = (expr_1).col1)
    ->  Seq Scan on ab_a2_b3 ab_6 (never executed)
-         Filter: (b = (InitPlan 1).col1)
+         Filter: (b = (expr_1).col1)
    ->  Seq Scan on ab_a3_b1 ab_7 (actual rows=0.00 loops=1)
-         Filter: (b = (InitPlan 1).col1)
+         Filter: (b = (expr_1).col1)
    ->  Seq Scan on ab_a3_b2 ab_8 (never executed)
-         Filter: (b = (InitPlan 1).col1)
+         Filter: (b = (expr_1).col1)
    ->  Seq Scan on ab_a3_b3 ab_9 (never executed)
-         Filter: (b = (InitPlan 1).col1)
+         Filter: (b = (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 = (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 = (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 = (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 = (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 = (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 = (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 = (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 = (expr_1).col1)
                      ->  Seq Scan on ab_a2_b2 ab_a2_2 (never executed)
-                           Filter: (b = (InitPlan 1).col1)
+                           Filter: (b = (expr_1).col1)
                      ->  Seq Scan on ab_a2_b3 ab_a2_3 (never executed)
-                           Filter: (b = (InitPlan 1).col1)
+                           Filter: (b = (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 = (expr_1).col1)
    ->  Seq Scan on listp_2_1 listp_2 (never executed)
-         Filter: (a = (InitPlan 1).col1)
+         Filter: (a = (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) < (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) < (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) < (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 = (expr_1).col1)
    ->  Seq Scan on boolp_t boolp_2 (actual rows=0.00 loops=1)
-         Filter: (a = (InitPlan 1).col1)
+         Filter: (a = (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 = (expr_1).col1)
    ->  Seq Scan on boolp_t boolp_2 (never executed)
-         Filter: (a = (InitPlan 1).col1)
+         Filter: (a = (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: Aggregate
-           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 >= (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 >= (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 >= (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 = (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 = (expr_1).col1))
    ->  Result
-         One-Time Filter: (1 = (InitPlan 1).col1)
+         One-Time Filter: (1 = (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 = (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 = (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 = (expr_1).col1))
 (10 rows)
 
 execute q (1, 1);
@@ -4113,8 +4113,8 @@ select * from listp where a = (select 2) and b <> 10;
                      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 = (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 = (expr_1).col1)
          ->  Parallel Seq Scan on listp_12_2 listp_2 (never executed)
-               Filter: (a = (InitPlan 1).col1)
+               Filter: (a = (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 = (expr_2).col1)
                ->  Parallel Seq Scan on listp_12_2 listp_2 (actual rows=N loops=N)
-                     Filter: (a = (InitPlan 2).col1)
+                     Filter: (a = (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 = (expr_1).col1)
                ->  Parallel Seq Scan on listp_12_2 listp_5 (never executed)
-                     Filter: (a = (InitPlan 1).col1)
+                     Filter: (a = (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[(expr_1).col1, (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[(expr_1).col1, (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[(expr_1).col1, (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[(expr_1).col1, (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 cd78cf11f51..023585b1776 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(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: (exists_1).col1
+   InitPlan exists_1
      ->  Result
            One-Time Filter: false
            Replaces: Join on t3, t4, t5, t6
diff --git a/src/test/regress/expected/returning.out b/src/test/regress/expected/returning.out
index 341b689f766..3df940ee8fc 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: (expr_1), (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
@@ -581,23 +581,23 @@ UPDATE foo SET f4 = 100 WHERE f1 = 5
                           QUERY PLAN                           
 ---------------------------------------------------------------
  Update on pg_temp.foo
-   Output: (SubPlan 1), (SubPlan 2), (SubPlan 3)
+   Output: (expr_1), (expr_2), (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: (expr_1), (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 e7fe3317638..4c60c25a476 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 <= (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 <= (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 <= (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 <= (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 <= (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 <= (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 <= (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 <= (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 <= (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 <= (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 <= (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 <= (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 <= (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 <= (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 <= (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 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 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 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 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 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 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 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 328b1e142c0..878baee2d59 100644
--- a/src/test/regress/expected/rowtypes.out
+++ b/src/test/regress/expected/rowtypes.out
@@ -1249,19 +1249,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: ((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: (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..3671d261f1f 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 any_1).col1) AND (four = (hashed 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 any_1).col1)))
+   SubPlan any_1
      ->  Seq Scan on tenk2
 (4 rows)
 
@@ -346,7 +346,7 @@ explain (costs off)
                       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 = (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 ((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 = (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 = (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(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: ((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..d660049f134 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: (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 db061ee95b1..2572af229b9 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 = (rowcompare_1).col1) AND (2 = (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 = (rowcompare_1).col1) AND (2 = (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 = (all_1).col1))
+   SubPlan all_1
      ->  Materialize
-           Output: ((InitPlan 1).col1)
-           InitPlan 1
+           Output: ((expr_1).col1)
+           InitPlan expr_1
              ->  Result
                    Output: 1
            ->  Result
-                 Output: (InitPlan 1).col1
+                 Output: (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(exists_1)
+   SubPlan exists_1
      ->  Limit
            ->  Seq Scan on int4_tbl i
                  Filter: (f1 = o.f1)
@@ -1097,11 +1097,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 any_1).col1))
+   SubPlan any_1
      ->  Append
            ->  Result
                  Output: 'bar'::name
@@ -1121,11 +1121,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 = (any_1).col1))
+   SubPlan any_1
      ->  Materialize
            Output: '("(1)")'::record
            ->  Result
@@ -1164,11 +1164,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 any_1).col1))
+   SubPlan any_1
      ->  Seq Scan on inner_text
 (4 rows)
 
@@ -1185,11 +1185,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 any_1).col1) AND ((q1)::text = (hashed any_1).col1)))
+   SubPlan any_1
      ->  Seq Scan on inner_text
 (4 rows)
 
@@ -1206,11 +1206,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 ((any_1).col1 = (q1)::text))
+   SubPlan any_1
      ->  Materialize
            ->  Seq Scan on inner_text
 (5 rows)
@@ -1229,12 +1229,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 exists_2).col1)) OR (ten < 0))
+         SubPlan exists_2
            ->  Index Only Scan using tenk1_unique1 on tenk1 k
 (5 rows)
 
@@ -1254,10 +1254,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(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)
@@ -1279,20 +1279,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(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 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
@@ -1328,14 +1328,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: (expr_1).col1, (expr_2).col1
+   InitPlan expr_1
      ->  Result
            Output: now()
-   InitPlan 2
+   InitPlan expr_2
      ->  Result
            Output: now()
 (8 rows)
@@ -1343,13 +1343,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: (expr_1).col1
+         InitPlan expr_1
            ->  Result
                  Output: random()
 (7 rows)
@@ -1360,12 +1360,12 @@ explain (verbose, costs off)
                               QUERY PLAN                              
 ----------------------------------------------------------------------
  Values Scan on "*VALUES*"
-   Output: (SubPlan 1), (SubPlan 2)
-   SubPlan 1
+   Output: (expr_1), (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)
@@ -1379,8 +1379,8 @@ explain (verbose, costs off)
  Subquery Scan on ss
    Output: ss.x, ss.x
    ->  Values Scan on "*VALUES*"
-         Output: (SubPlan 1)
-         SubPlan 1
+         Output: (expr_1)
+         SubPlan expr_1
            ->  Result
                  Output: random()
                  One-Time Filter: ("*VALUES*".column1 = "*VALUES*".column1)
@@ -1400,16 +1400,16 @@ where o.ten = 0;
                                                                                          QUERY PLAN                                                                                          
 ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  Aggregate
-   Output: sum((((ANY (i.ten = (hashed SubPlan 1).col1))))::integer)
+   Output: sum((((ANY (i.ten = (hashed any_1).col1))))::integer)
    ->  Nested Loop
-         Output: ((ANY (i.ten = (hashed SubPlan 1).col1)))
+         Output: ((ANY (i.ten = (hashed 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 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)
@@ -1618,7 +1618,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)
@@ -1645,12 +1645,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 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)
@@ -2749,14 +2749,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 = (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)
@@ -2766,14 +2766,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 = (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)
@@ -2783,14 +2783,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 = (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)
@@ -2852,7 +2852,7 @@ ON B.hundred in (SELECT min(c.hundred) FROM tenk2 C WHERE c.odd = b.odd);
                            Filter: (b.hundred = "ANY_subquery".min)
                            ->  Result
                                  Replaces: Aggregate
-                                 InitPlan 1
+                                 InitPlan minmax_1
                                    ->  Limit
                                          ->  Index Scan using tenk2_hundred on tenk2 c
                                                Index Cond: (hundred IS NOT NULL)
@@ -3093,7 +3093,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)
@@ -3109,7 +3109,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
@@ -3128,7 +3128,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..66747f8af82 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)
@@ -3170,18 +3170,18 @@ EXPLAIN (costs off) INSERT INTO rw_view1 VALUES (2, 'New row 2');
                         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: ((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: (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(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(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(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(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(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(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(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(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: (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..a0aac9d4377 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: (multiexpr_1).col1, (multiexpr_1).col2, (rescan 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..55719226bef 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((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 26c88505140..4156105685f 100644
--- a/src/test/regress/expected/with.out
+++ b/src/test/regress/expected/with.out
@@ -3168,7 +3168,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
@@ -3200,7 +3200,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
@@ -3243,11 +3243,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.39.5 (Apple Git-154)

v4-0006-Store-information-about-elided-nodes-in-the-final.patchapplication/octet-stream; name=v4-0006-Store-information-about-elided-nodes-in-the-final.patchDownload
From 6bae20f299e0787d350f213d573153a112b2213f Mon Sep 17 00:00:00 2001
From: Robert Haas <rhaas@postgresql.org>
Date: Tue, 22 Apr 2025 14:10:19 -0400
Subject: [PATCH v4 6/7] Store information about elided nodes in the final
 plan.

When setrefs.c removes a SubqueryScan, single-child Append, or
single-child MergeAppend from the final Plan tree, the RTI which
would have been scanned by the removed node no longer appears in
the final plan (the actual range table entry is still present,
but it's no longer referenced).

That's fine for the executor, but it can create difficulties for
code that wants to deduce from the final plan what choices were
made during the planing process. For example, a traversal of a
join tree in the final plan might never encounter the RTI of one
of the relationss in the join problem, and might instead encounter
a scan of a child RTI or even one from a different subquery level.

This patch adjusts things so that each time we elide a node during
setrefs processing, we record the plan_node_id of its single surviving
child, the type of the removed node, and the RTIs that the removed
node would have scanned. This information is recorded in a separate
list that can be ignored by the executor and examined only by code
that cares about these details.

This commit also updates pg_overexplain to display these details.
---
 .../expected/pg_overexplain.out               |  4 +-
 contrib/pg_overexplain/pg_overexplain.c       | 39 ++++++++++++++
 src/backend/optimizer/plan/planner.c          |  1 +
 src/backend/optimizer/plan/setrefs.c          | 52 ++++++++++++++++++-
 src/include/nodes/pathnodes.h                 |  3 ++
 src/include/nodes/plannodes.h                 | 17 ++++++
 src/tools/pgindent/typedefs.list              |  1 +
 7 files changed, 114 insertions(+), 3 deletions(-)

diff --git a/contrib/pg_overexplain/expected/pg_overexplain.out b/contrib/pg_overexplain/expected/pg_overexplain.out
index 55d34666d87..ca9a23ea61f 100644
--- a/contrib/pg_overexplain/expected/pg_overexplain.out
+++ b/contrib/pg_overexplain/expected/pg_overexplain.out
@@ -452,6 +452,8 @@ SELECT * FROM vegetables WHERE genus = 'daucus';
  Seq Scan on daucus vegetables
    Filter: (genus = 'daucus'::text)
    Scan RTI: 2
+   Elided Node Type: Append
+   Elided Node RTIs: 1
  RTI 1 (relation, inherited, in-from-clause):
    Eref: vegetables (id, name, genus)
    Relation: vegetables
@@ -465,7 +467,7 @@ SELECT * FROM vegetables WHERE genus = 'daucus';
    Relation Kind: relation
    Relation Lock Mode: AccessShareLock
  Unprunable RTIs: 1 2
-(16 rows)
+(18 rows)
 
 -- Also test a case that involves a write.
 EXPLAIN (RANGE_TABLE, COSTS OFF)
diff --git a/contrib/pg_overexplain/pg_overexplain.c b/contrib/pg_overexplain/pg_overexplain.c
index 5dc707d69e3..fa907fa472e 100644
--- a/contrib/pg_overexplain/pg_overexplain.c
+++ b/contrib/pg_overexplain/pg_overexplain.c
@@ -191,6 +191,8 @@ overexplain_per_node_hook(PlanState *planstate, List *ancestors,
 	 */
 	if (options->range_table)
 	{
+		bool		opened_elided_nodes = false;
+
 		switch (nodeTag(plan))
 		{
 			case T_SeqScan:
@@ -251,6 +253,43 @@ overexplain_per_node_hook(PlanState *planstate, List *ancestors,
 			default:
 				break;
 		}
+
+		foreach_node(ElidedNode, n, es->pstmt->elidedNodes)
+		{
+			char	   *elidednodetag;
+
+			if (n->plan_node_id != plan->plan_node_id)
+				continue;
+
+			if (!opened_elided_nodes)
+			{
+				ExplainOpenGroup("Elided Nodes", "Elided Nodes", false, es);
+				opened_elided_nodes = true;
+			}
+
+			switch (n->elided_type)
+			{
+				case T_Append:
+					elidednodetag = "Append";
+					break;
+				case T_MergeAppend:
+					elidednodetag = "MergeAppend";
+					break;
+				case T_SubqueryScan:
+					elidednodetag = "SubqueryScan";
+					break;
+				default:
+					elidednodetag = psprintf("%d", n->elided_type);
+					break;
+			}
+
+			ExplainOpenGroup("Elided Node", NULL, true, es);
+			ExplainPropertyText("Elided Node Type", elidednodetag, es);
+			overexplain_bitmapset("Elided Node RTIs", n->relids, es);
+			ExplainCloseGroup("Elided Node", NULL, true, es);
+		}
+		if (opened_elided_nodes)
+			ExplainCloseGroup("Elided Nodes", "Elided Nodes", false, es);
 	}
 }
 
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index 59ee3f319f4..fef57d84bb3 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -590,6 +590,7 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions,
 	result->paramExecTypes = glob->paramExecTypes;
 	/* utilityStmt should be null, but we might as well copy it */
 	result->utilityStmt = parse->utilityStmt;
+	result->elidedNodes = glob->elidedNodes;
 	result->stmt_location = parse->stmt_location;
 	result->stmt_len = parse->stmt_len;
 
diff --git a/src/backend/optimizer/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.c
index eef43792aeb..5900458a0e1 100644
--- a/src/backend/optimizer/plan/setrefs.c
+++ b/src/backend/optimizer/plan/setrefs.c
@@ -211,6 +211,9 @@ static List *set_windowagg_runcondition_references(PlannerInfo *root,
 												   List *runcondition,
 												   Plan *plan);
 
+static void record_elided_node(PlannerGlobal *glob, int plan_node_id,
+							   NodeTag elided_type, Bitmapset *relids);
+
 
 /*****************************************************************************
  *
@@ -1441,10 +1444,17 @@ set_subqueryscan_references(PlannerInfo *root,
 
 	if (trivial_subqueryscan(plan))
 	{
+		Index		scanrelid;
+
 		/*
 		 * We can omit the SubqueryScan node and just pull up the subplan.
 		 */
 		result = clean_up_removed_plan_level((Plan *) plan, plan->subplan);
+
+		/* Remember that we removed a SubqueryScan */
+		scanrelid = plan->scan.scanrelid + rtoffset;
+		record_elided_node(root->glob, plan->subplan->plan_node_id,
+						   T_SubqueryScan, bms_make_singleton(scanrelid));
 	}
 	else
 	{
@@ -1872,7 +1882,17 @@ set_append_references(PlannerInfo *root,
 		Plan	   *p = (Plan *) linitial(aplan->appendplans);
 
 		if (p->parallel_aware == aplan->plan.parallel_aware)
-			return clean_up_removed_plan_level((Plan *) aplan, p);
+		{
+			Plan	   *result;
+
+			result = clean_up_removed_plan_level((Plan *) aplan, p);
+
+			/* Remember that we removed an Append */
+			record_elided_node(root->glob, p->plan_node_id, T_Append,
+							   offset_relid_set(aplan->apprelids, rtoffset));
+
+			return result;
+		}
 	}
 
 	/*
@@ -1940,7 +1960,17 @@ set_mergeappend_references(PlannerInfo *root,
 		Plan	   *p = (Plan *) linitial(mplan->mergeplans);
 
 		if (p->parallel_aware == mplan->plan.parallel_aware)
-			return clean_up_removed_plan_level((Plan *) mplan, p);
+		{
+			Plan	   *result;
+
+			result = clean_up_removed_plan_level((Plan *) mplan, p);
+
+			/* Remember that we removed a MergeAppend */
+			record_elided_node(root->glob, p->plan_node_id, T_MergeAppend,
+							   offset_relid_set(mplan->apprelids, rtoffset));
+
+			return result;
+		}
 	}
 
 	/*
@@ -3755,3 +3785,21 @@ extract_query_dependencies_walker(Node *node, PlannerInfo *context)
 	return expression_tree_walker(node, extract_query_dependencies_walker,
 								  context);
 }
+
+/*
+ * Record some details about a node removed from the plan during setrefs
+ * procesing, for the benefit of code trying to reconstruct planner decisions
+ * from examination of the final plan tree.
+ */
+static void
+record_elided_node(PlannerGlobal *glob, int plan_node_id,
+				   NodeTag elided_type, Bitmapset *relids)
+{
+	ElidedNode *n = makeNode(ElidedNode);
+
+	n->plan_node_id = plan_node_id;
+	n->elided_type = elided_type;
+	n->relids = relids;
+
+	glob->elidedNodes = lappend(glob->elidedNodes, n);
+}
diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h
index 4f8586f6591..684e02da063 100644
--- a/src/include/nodes/pathnodes.h
+++ b/src/include/nodes/pathnodes.h
@@ -159,6 +159,9 @@ typedef struct PlannerGlobal
 	/* type OIDs for PARAM_EXEC Params */
 	List	   *paramExecTypes;
 
+	/* info about nodes elided from the plan during setrefs processing */
+	List	   *elidedNodes;
+
 	/* highest PlaceHolderVar ID assigned */
 	Index		lastPHId;
 
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index 3f2d6fafc24..497aec24876 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -152,6 +152,9 @@ typedef struct PlannedStmt
 	/* non-null if this is utility stmt */
 	Node	   *utilityStmt;
 
+	/* info about nodes elided from the plan during setrefs processing */
+	List	   *elidedNodes;
+
 	/* statement location in source string (copied from Query) */
 	/* start location, or -1 if unknown */
 	ParseLoc	stmt_location;
@@ -1818,4 +1821,18 @@ typedef struct SubPlanRTInfo
 	bool		dummy;
 } SubPlanRTInfo;
 
+/*
+ * ElidedNode
+ *
+ * Information about nodes elided from the final plan tree: trivial subquery
+ * scans, and single-child Append and MergeAppend nodes.
+ */
+typedef struct ElidedNode
+{
+	NodeTag		type;
+	int			plan_node_id;
+	NodeTag		elided_type;
+	Bitmapset  *relids;
+} ElidedNode;
+
 #endif							/* PLANNODES_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8e4367dabaf..899551d5117 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -4348,3 +4348,4 @@ z_stream
 z_streamp
 zic_t
 SubPlanRTInfo
+ElidedNode
-- 
2.39.5 (Apple Git-154)

v4-0007-Store-information-about-Append-node-consolidation.patchapplication/octet-stream; name=v4-0007-Store-information-about-Append-node-consolidation.patchDownload
From a68a9a731b39974b98125f530c48ebb826f98a39 Mon Sep 17 00:00:00 2001
From: Robert Haas <rhaas@postgresql.org>
Date: Mon, 21 Jul 2025 15:49:06 -0400
Subject: [PATCH v4 7/7] Store information about Append node consolidation in
 the final plan.

When we build an AppendPath or MergeAppendPath, we sometimes pull all
child paths from a subordinate AppendPath or MergeAppendPath instead
of having one such path atop another. This results in the RTIs that
would have been associated with the subordinate path disappearing
from the final plan, making things difficult for code that wants
to scrutinize the final plan and extract information from it about
what happened during the planning process.

To avoid this, propagate the RTI sets that would have been present
in the 'apprelids' field of the subordinate Append or MergeAppend
nodes that would have been created into the surviving Append or
MergeAppend node, using a new 'child_append_relid_sets' field for
that purpose.

This commit also updates pg_overexplain to display these details.
---
 .../expected/pg_overexplain.out               |  4 +-
 contrib/pg_overexplain/pg_overexplain.c       | 56 +++++++++++
 src/backend/optimizer/path/allpaths.c         | 98 +++++++++++++++----
 src/backend/optimizer/path/joinrels.c         |  2 +-
 src/backend/optimizer/plan/createplan.c       |  2 +
 src/backend/optimizer/plan/planner.c          |  1 +
 src/backend/optimizer/prep/prepunion.c        |  5 +-
 src/backend/optimizer/util/pathnode.c         |  5 +
 src/include/nodes/pathnodes.h                 | 10 ++
 src/include/nodes/plannodes.h                 | 11 +++
 src/include/optimizer/pathnode.h              |  2 +
 11 files changed, 171 insertions(+), 25 deletions(-)

diff --git a/contrib/pg_overexplain/expected/pg_overexplain.out b/contrib/pg_overexplain/expected/pg_overexplain.out
index ca9a23ea61f..a377fb2571d 100644
--- a/contrib/pg_overexplain/expected/pg_overexplain.out
+++ b/contrib/pg_overexplain/expected/pg_overexplain.out
@@ -104,6 +104,7 @@ $$);
                Parallel Safe: true
                Plan Node ID: 2
                Append RTIs: 1
+               Child Append RTIs: none
                ->  Seq Scan on brassica vegetables_1
                      Disabled Nodes: 0
                      Parallel Safe: true
@@ -142,7 +143,7 @@ $$);
    Relation Kind: relation
    Relation Lock Mode: AccessShareLock
  Unprunable RTIs: 1 3 4
-(53 rows)
+(54 rows)
 
 -- Test a different output format.
 SELECT explain_filter($$
@@ -197,6 +198,7 @@ $$);
                <extParam>none</extParam>                            +
                <allParam>none</allParam>                            +
                <Append-RTIs>1</Append-RTIs>                         +
+               <Child-Append-RTIs>none</Child-Append-RTIs>          +
                <Subplans-Removed>0</Subplans-Removed>               +
                <Plans>                                              +
                  <Plan>                                             +
diff --git a/contrib/pg_overexplain/pg_overexplain.c b/contrib/pg_overexplain/pg_overexplain.c
index fa907fa472e..6538ffcafb0 100644
--- a/contrib/pg_overexplain/pg_overexplain.c
+++ b/contrib/pg_overexplain/pg_overexplain.c
@@ -54,6 +54,8 @@ static void overexplain_alias(const char *qlabel, Alias *alias,
 							  ExplainState *es);
 static void overexplain_bitmapset(const char *qlabel, Bitmapset *bms,
 								  ExplainState *es);
+static void overexplain_bitmapset_list(const char *qlabel, List *bms_list,
+									   ExplainState *es);
 static void overexplain_intlist(const char *qlabel, List *list,
 								ExplainState *es);
 
@@ -232,11 +234,17 @@ overexplain_per_node_hook(PlanState *planstate, List *ancestors,
 				overexplain_bitmapset("Append RTIs",
 									  ((Append *) plan)->apprelids,
 									  es);
+				overexplain_bitmapset_list("Child Append RTIs",
+										   ((Append *) plan)->child_append_relid_sets,
+										   es);
 				break;
 			case T_MergeAppend:
 				overexplain_bitmapset("Append RTIs",
 									  ((MergeAppend *) plan)->apprelids,
 									  es);
+				overexplain_bitmapset_list("Child Append RTIs",
+										   ((MergeAppend *) plan)->child_append_relid_sets,
+										   es);
 				break;
 			case T_Result:
 
@@ -815,6 +823,54 @@ overexplain_bitmapset(const char *qlabel, Bitmapset *bms, ExplainState *es)
 	pfree(buf.data);
 }
 
+/*
+ * Emit a text property describing the contents of a list of bitmapsets.
+ * If a bitmapset contains exactly 1 member, we just print an integer;
+ * otherwise, we surround the list of members by parentheses.
+ *
+ * If there are no bitmapsets in the list, we print the word "none".
+ */
+static void
+overexplain_bitmapset_list(const char *qlabel, List *bms_list,
+						   ExplainState *es)
+{
+	StringInfoData buf;
+
+	initStringInfo(&buf);
+
+	foreach_node(Bitmapset, bms, bms_list)
+	{
+		if (bms_membership(bms) == BMS_SINGLETON)
+			appendStringInfo(&buf, " %d", bms_singleton_member(bms));
+		else
+		{
+			int			x = -1;
+			bool		first = true;
+
+			appendStringInfoString(&buf, " (");
+			while ((x = bms_next_member(bms, x)) >= 0)
+			{
+				if (first)
+					first = false;
+				else
+					appendStringInfoChar(&buf, ' ');
+				appendStringInfo(&buf, "%d", x);
+			}
+			appendStringInfoChar(&buf, ')');
+		}
+	}
+
+	if (buf.len == 0)
+	{
+		ExplainPropertyText(qlabel, "none", es);
+		return;
+	}
+
+	Assert(buf.data[0] == ' ');
+	ExplainPropertyText(qlabel, buf.data + 1, es);
+	pfree(buf.data);
+}
+
 /*
  * Emit a text property describing the contents of a list of integers, OIDs,
  * or XIDs -- either a space-separated list of integer members, or the word
diff --git a/src/backend/optimizer/path/allpaths.c b/src/backend/optimizer/path/allpaths.c
index 593f5361b58..76b8c3fd7c6 100644
--- a/src/backend/optimizer/path/allpaths.c
+++ b/src/backend/optimizer/path/allpaths.c
@@ -122,8 +122,10 @@ static Path *get_cheapest_parameterized_child_path(PlannerInfo *root,
 												   Relids required_outer);
 static void accumulate_append_subpath(Path *path,
 									  List **subpaths,
-									  List **special_subpaths);
-static Path *get_singleton_append_subpath(Path *path);
+									  List **special_subpaths,
+									  List **child_append_relid_sets);
+static Path *get_singleton_append_subpath(Path *path,
+										  List **child_append_relid_sets);
 static void set_dummy_rel_pathlist(RelOptInfo *rel);
 static void set_subquery_pathlist(PlannerInfo *root, RelOptInfo *rel,
 								  Index rti, RangeTblEntry *rte);
@@ -1323,11 +1325,15 @@ add_paths_to_append_rel(PlannerInfo *root, RelOptInfo *rel,
 {
 	List	   *subpaths = NIL;
 	bool		subpaths_valid = true;
+	List	   *subpath_cars = NIL;
 	List	   *startup_subpaths = NIL;
 	bool		startup_subpaths_valid = true;
+	List	   *startup_subpath_cars = NIL;
 	List	   *partial_subpaths = NIL;
+	List	   *partial_subpath_cars = NIL;
 	List	   *pa_partial_subpaths = NIL;
 	List	   *pa_nonpartial_subpaths = NIL;
+	List	   *pa_subpath_cars = NIL;
 	bool		partial_subpaths_valid = true;
 	bool		pa_subpaths_valid;
 	List	   *all_child_pathkeys = NIL;
@@ -1360,7 +1366,7 @@ add_paths_to_append_rel(PlannerInfo *root, RelOptInfo *rel,
 		if (childrel->pathlist != NIL &&
 			childrel->cheapest_total_path->param_info == NULL)
 			accumulate_append_subpath(childrel->cheapest_total_path,
-									  &subpaths, NULL);
+									  &subpaths, NULL, &subpath_cars);
 		else
 			subpaths_valid = false;
 
@@ -1389,7 +1395,8 @@ add_paths_to_append_rel(PlannerInfo *root, RelOptInfo *rel,
 			Assert(cheapest_path->param_info == NULL);
 			accumulate_append_subpath(cheapest_path,
 									  &startup_subpaths,
-									  NULL);
+									  NULL,
+									  &startup_subpath_cars);
 		}
 		else
 			startup_subpaths_valid = false;
@@ -1400,7 +1407,8 @@ add_paths_to_append_rel(PlannerInfo *root, RelOptInfo *rel,
 		{
 			cheapest_partial_path = linitial(childrel->partial_pathlist);
 			accumulate_append_subpath(cheapest_partial_path,
-									  &partial_subpaths, NULL);
+									  &partial_subpaths, NULL,
+									  &partial_subpath_cars);
 		}
 		else
 			partial_subpaths_valid = false;
@@ -1429,7 +1437,8 @@ add_paths_to_append_rel(PlannerInfo *root, RelOptInfo *rel,
 				Assert(cheapest_partial_path != NULL);
 				accumulate_append_subpath(cheapest_partial_path,
 										  &pa_partial_subpaths,
-										  &pa_nonpartial_subpaths);
+										  &pa_nonpartial_subpaths,
+										  &pa_subpath_cars);
 			}
 			else
 			{
@@ -1448,7 +1457,8 @@ add_paths_to_append_rel(PlannerInfo *root, RelOptInfo *rel,
 				 */
 				accumulate_append_subpath(nppath,
 										  &pa_nonpartial_subpaths,
-										  NULL);
+										  NULL,
+										  &pa_subpath_cars);
 			}
 		}
 
@@ -1523,14 +1533,16 @@ add_paths_to_append_rel(PlannerInfo *root, RelOptInfo *rel,
 	 * if we have zero or one live subpath due to constraint exclusion.)
 	 */
 	if (subpaths_valid)
-		add_path(rel, (Path *) create_append_path(root, rel, subpaths, NIL,
+		add_path(rel, (Path *) create_append_path(root, rel, subpaths,
+												  NIL, subpath_cars,
 												  NIL, NULL, 0, false,
 												  -1));
 
 	/* build an AppendPath for the cheap startup paths, if valid */
 	if (startup_subpaths_valid)
 		add_path(rel, (Path *) create_append_path(root, rel, startup_subpaths,
-												  NIL, NIL, NULL, 0, false, -1));
+												  NIL, startup_subpath_cars,
+												  NIL, NULL, 0, false, -1));
 
 	/*
 	 * Consider an append of unordered, unparameterized partial paths.  Make
@@ -1571,6 +1583,7 @@ add_paths_to_append_rel(PlannerInfo *root, RelOptInfo *rel,
 
 		/* Generate a partial append path. */
 		appendpath = create_append_path(root, rel, NIL, partial_subpaths,
+										partial_subpath_cars,
 										NIL, NULL, parallel_workers,
 										enable_parallel_append,
 										-1);
@@ -1621,6 +1634,7 @@ add_paths_to_append_rel(PlannerInfo *root, RelOptInfo *rel,
 
 		appendpath = create_append_path(root, rel, pa_nonpartial_subpaths,
 										pa_partial_subpaths,
+										pa_subpath_cars,
 										NIL, NULL, parallel_workers, true,
 										partial_rows);
 		add_partial_path(rel, (Path *) appendpath);
@@ -1654,6 +1668,7 @@ add_paths_to_append_rel(PlannerInfo *root, RelOptInfo *rel,
 
 		/* Select the child paths for an Append with this parameterization */
 		subpaths = NIL;
+		subpath_cars = NIL;
 		subpaths_valid = true;
 		foreach(lcr, live_childrels)
 		{
@@ -1676,12 +1691,13 @@ add_paths_to_append_rel(PlannerInfo *root, RelOptInfo *rel,
 				subpaths_valid = false;
 				break;
 			}
-			accumulate_append_subpath(subpath, &subpaths, NULL);
+			accumulate_append_subpath(subpath, &subpaths, NULL,
+									  &subpath_cars);
 		}
 
 		if (subpaths_valid)
 			add_path(rel, (Path *)
-					 create_append_path(root, rel, subpaths, NIL,
+					 create_append_path(root, rel, subpaths, NIL, subpath_cars,
 										NIL, required_outer, 0, false,
 										-1));
 	}
@@ -1708,6 +1724,7 @@ add_paths_to_append_rel(PlannerInfo *root, RelOptInfo *rel,
 				continue;
 
 			appendpath = create_append_path(root, rel, NIL, list_make1(path),
+											list_make1(rel->relids),
 											NIL, NULL,
 											path->parallel_workers, true,
 											partial_rows);
@@ -1789,8 +1806,11 @@ generate_orderedappend_paths(PlannerInfo *root, RelOptInfo *rel,
 	{
 		List	   *pathkeys = (List *) lfirst(lcp);
 		List	   *startup_subpaths = NIL;
+		List	   *startup_subpath_cars = NIL;
 		List	   *total_subpaths = NIL;
+		List	   *total_subpath_cars = NIL;
 		List	   *fractional_subpaths = NIL;
+		List	   *fractional_subpath_cars = NIL;
 		bool		startup_neq_total = false;
 		bool		match_partition_order;
 		bool		match_partition_order_desc;
@@ -1942,16 +1962,23 @@ generate_orderedappend_paths(PlannerInfo *root, RelOptInfo *rel,
 				 * just a single subpath (and hence aren't doing anything
 				 * useful).
 				 */
-				cheapest_startup = get_singleton_append_subpath(cheapest_startup);
-				cheapest_total = get_singleton_append_subpath(cheapest_total);
+				cheapest_startup =
+					get_singleton_append_subpath(cheapest_startup,
+												 &startup_subpath_cars);
+				cheapest_total =
+					get_singleton_append_subpath(cheapest_total,
+												 &total_subpath_cars);
 
 				startup_subpaths = lappend(startup_subpaths, cheapest_startup);
 				total_subpaths = lappend(total_subpaths, cheapest_total);
 
 				if (cheapest_fractional)
 				{
-					cheapest_fractional = get_singleton_append_subpath(cheapest_fractional);
-					fractional_subpaths = lappend(fractional_subpaths, cheapest_fractional);
+					cheapest_fractional =
+						get_singleton_append_subpath(cheapest_fractional,
+													 &fractional_subpath_cars);
+					fractional_subpaths =
+						lappend(fractional_subpaths, cheapest_fractional);
 				}
 			}
 			else
@@ -1961,13 +1988,16 @@ generate_orderedappend_paths(PlannerInfo *root, RelOptInfo *rel,
 				 * child paths for the MergeAppend.
 				 */
 				accumulate_append_subpath(cheapest_startup,
-										  &startup_subpaths, NULL);
+										  &startup_subpaths, NULL,
+										  &startup_subpath_cars);
 				accumulate_append_subpath(cheapest_total,
-										  &total_subpaths, NULL);
+										  &total_subpaths, NULL,
+										  &total_subpath_cars);
 
 				if (cheapest_fractional)
 					accumulate_append_subpath(cheapest_fractional,
-											  &fractional_subpaths, NULL);
+											  &fractional_subpaths, NULL,
+											  &fractional_subpath_cars);
 			}
 		}
 
@@ -1979,6 +2009,7 @@ generate_orderedappend_paths(PlannerInfo *root, RelOptInfo *rel,
 													  rel,
 													  startup_subpaths,
 													  NIL,
+													  startup_subpath_cars,
 													  pathkeys,
 													  NULL,
 													  0,
@@ -1989,6 +2020,7 @@ generate_orderedappend_paths(PlannerInfo *root, RelOptInfo *rel,
 														  rel,
 														  total_subpaths,
 														  NIL,
+														  total_subpath_cars,
 														  pathkeys,
 														  NULL,
 														  0,
@@ -2000,6 +2032,7 @@ generate_orderedappend_paths(PlannerInfo *root, RelOptInfo *rel,
 														  rel,
 														  fractional_subpaths,
 														  NIL,
+														  fractional_subpath_cars,
 														  pathkeys,
 														  NULL,
 														  0,
@@ -2012,12 +2045,14 @@ generate_orderedappend_paths(PlannerInfo *root, RelOptInfo *rel,
 			add_path(rel, (Path *) create_merge_append_path(root,
 															rel,
 															startup_subpaths,
+															startup_subpath_cars,
 															pathkeys,
 															NULL));
 			if (startup_neq_total)
 				add_path(rel, (Path *) create_merge_append_path(root,
 																rel,
 																total_subpaths,
+																total_subpath_cars,
 																pathkeys,
 																NULL));
 
@@ -2025,6 +2060,7 @@ generate_orderedappend_paths(PlannerInfo *root, RelOptInfo *rel,
 				add_path(rel, (Path *) create_merge_append_path(root,
 																rel,
 																fractional_subpaths,
+																fractional_subpath_cars,
 																pathkeys,
 																NULL));
 		}
@@ -2127,7 +2163,8 @@ get_cheapest_parameterized_child_path(PlannerInfo *root, RelOptInfo *rel,
  * paths).
  */
 static void
-accumulate_append_subpath(Path *path, List **subpaths, List **special_subpaths)
+accumulate_append_subpath(Path *path, List **subpaths, List **special_subpaths,
+						  List **child_append_relid_sets)
 {
 	if (IsA(path, AppendPath))
 	{
@@ -2136,6 +2173,8 @@ accumulate_append_subpath(Path *path, List **subpaths, List **special_subpaths)
 		if (!apath->path.parallel_aware || apath->first_partial_path == 0)
 		{
 			*subpaths = list_concat(*subpaths, apath->subpaths);
+			*child_append_relid_sets =
+				lappend(*child_append_relid_sets, path->parent->relids);
 			return;
 		}
 		else if (special_subpaths != NULL)
@@ -2150,6 +2189,8 @@ accumulate_append_subpath(Path *path, List **subpaths, List **special_subpaths)
 												  apath->first_partial_path);
 			*special_subpaths = list_concat(*special_subpaths,
 											new_special_subpaths);
+			*child_append_relid_sets =
+				lappend(*child_append_relid_sets, path->parent->relids);
 			return;
 		}
 	}
@@ -2158,6 +2199,8 @@ accumulate_append_subpath(Path *path, List **subpaths, List **special_subpaths)
 		MergeAppendPath *mpath = (MergeAppendPath *) path;
 
 		*subpaths = list_concat(*subpaths, mpath->subpaths);
+		*child_append_relid_sets =
+			lappend(*child_append_relid_sets, path->parent->relids);
 		return;
 	}
 
@@ -2169,10 +2212,15 @@ accumulate_append_subpath(Path *path, List **subpaths, List **special_subpaths)
  *		Returns the single subpath of an Append/MergeAppend, or just
  *		return 'path' if it's not a single sub-path Append/MergeAppend.
  *
+ * As a side effect, whenever we return a single subpath rather than the
+ * original path, add the relid set for the original path to
+ * child_append_relid_sets, so that those relids don't entirely disappear
+ * from the final plan.
+ *
  * Note: 'path' must not be a parallel-aware path.
  */
 static Path *
-get_singleton_append_subpath(Path *path)
+get_singleton_append_subpath(Path *path, List **child_append_relid_sets)
 {
 	Assert(!path->parallel_aware);
 
@@ -2181,14 +2229,22 @@ get_singleton_append_subpath(Path *path)
 		AppendPath *apath = (AppendPath *) path;
 
 		if (list_length(apath->subpaths) == 1)
+		{
+			*child_append_relid_sets =
+				lappend(*child_append_relid_sets, path->parent->relids);
 			return (Path *) linitial(apath->subpaths);
+		}
 	}
 	else if (IsA(path, MergeAppendPath))
 	{
 		MergeAppendPath *mpath = (MergeAppendPath *) path;
 
 		if (list_length(mpath->subpaths) == 1)
+		{
+			*child_append_relid_sets =
+				lappend(*child_append_relid_sets, path->parent->relids);
 			return (Path *) linitial(mpath->subpaths);
+		}
 	}
 
 	return path;
@@ -2217,7 +2273,7 @@ set_dummy_rel_pathlist(RelOptInfo *rel)
 	rel->partial_pathlist = NIL;
 
 	/* Set up the dummy path */
-	add_path(rel, (Path *) create_append_path(NULL, rel, NIL, NIL,
+	add_path(rel, (Path *) create_append_path(NULL, rel, NIL, NIL, NIL,
 											  NIL, rel->lateral_relids,
 											  0, false, -1));
 
diff --git a/src/backend/optimizer/path/joinrels.c b/src/backend/optimizer/path/joinrels.c
index 535248aa525..6bddfc537d2 100644
--- a/src/backend/optimizer/path/joinrels.c
+++ b/src/backend/optimizer/path/joinrels.c
@@ -1342,7 +1342,7 @@ mark_dummy_rel(RelOptInfo *rel)
 
 	/* Set up the dummy path */
 	add_path(rel, (Path *) create_append_path(NULL, rel, NIL, NIL,
-											  NIL, rel->lateral_relids,
+											  NIL, NIL, rel->lateral_relids,
 											  0, false, -1));
 
 	/* Set or update cheapest_total_path and related fields */
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index 1f33f0d97f9..3fe560944a1 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -1281,6 +1281,7 @@ create_append_plan(PlannerInfo *root, AppendPath *best_path, int flags)
 	plan->plan.lefttree = NULL;
 	plan->plan.righttree = NULL;
 	plan->apprelids = rel->relids;
+	plan->child_append_relid_sets = best_path->child_append_relid_sets;
 
 	if (pathkeys != NIL)
 	{
@@ -1493,6 +1494,7 @@ create_merge_append_plan(PlannerInfo *root, MergeAppendPath *best_path,
 	plan->lefttree = NULL;
 	plan->righttree = NULL;
 	node->apprelids = rel->relids;
+	node->child_append_relid_sets = best_path->child_append_relid_sets;
 
 	/*
 	 * Compute sort column info, and adjust MergeAppend's tlist as needed.
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index fef57d84bb3..9678734d34c 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -3979,6 +3979,7 @@ create_degenerate_grouping_paths(PlannerInfo *root, RelOptInfo *input_rel,
 							   paths,
 							   NIL,
 							   NIL,
+							   NIL,
 							   NULL,
 							   0,
 							   false,
diff --git a/src/backend/optimizer/prep/prepunion.c b/src/backend/optimizer/prep/prepunion.c
index d55eb39e552..5b88514bb79 100644
--- a/src/backend/optimizer/prep/prepunion.c
+++ b/src/backend/optimizer/prep/prepunion.c
@@ -815,7 +815,7 @@ generate_union_paths(SetOperationStmt *op, PlannerInfo *root,
 	 * union child.
 	 */
 	apath = (Path *) create_append_path(root, result_rel, cheapest_pathlist,
-										NIL, NIL, NULL, 0, false, -1);
+										NIL, NIL, NIL, NULL, 0, false, -1);
 
 	/*
 	 * Estimate number of groups.  For now we just assume the output is unique
@@ -861,7 +861,7 @@ generate_union_paths(SetOperationStmt *op, PlannerInfo *root,
 
 		papath = (Path *)
 			create_append_path(root, result_rel, NIL, partial_pathlist,
-							   NIL, NULL, parallel_workers,
+							   NIL, NIL, NULL, parallel_workers,
 							   enable_parallel_append, -1);
 		gpath = (Path *)
 			create_gather_path(root, result_rel, papath,
@@ -969,6 +969,7 @@ generate_union_paths(SetOperationStmt *op, PlannerInfo *root,
 			path = (Path *) create_merge_append_path(root,
 													 result_rel,
 													 ordered_pathlist,
+													 NIL,
 													 union_pathkeys,
 													 NULL);
 
diff --git a/src/backend/optimizer/util/pathnode.c b/src/backend/optimizer/util/pathnode.c
index b0da28150d3..b4546c8842d 100644
--- a/src/backend/optimizer/util/pathnode.c
+++ b/src/backend/optimizer/util/pathnode.c
@@ -1298,6 +1298,7 @@ AppendPath *
 create_append_path(PlannerInfo *root,
 				   RelOptInfo *rel,
 				   List *subpaths, List *partial_subpaths,
+				   List *child_append_relid_sets,
 				   List *pathkeys, Relids required_outer,
 				   int parallel_workers, bool parallel_aware,
 				   double rows)
@@ -1307,6 +1308,7 @@ create_append_path(PlannerInfo *root,
 
 	Assert(!parallel_aware || parallel_workers > 0);
 
+	pathnode->child_append_relid_sets = child_append_relid_sets;
 	pathnode->path.pathtype = T_Append;
 	pathnode->path.parent = rel;
 	pathnode->path.pathtarget = rel->reltarget;
@@ -1469,6 +1471,7 @@ MergeAppendPath *
 create_merge_append_path(PlannerInfo *root,
 						 RelOptInfo *rel,
 						 List *subpaths,
+						 List *child_append_relid_sets,
 						 List *pathkeys,
 						 Relids required_outer)
 {
@@ -1484,6 +1487,7 @@ create_merge_append_path(PlannerInfo *root,
 	 */
 	Assert(bms_is_empty(rel->lateral_relids) && bms_is_empty(required_outer));
 
+	pathnode->child_append_relid_sets = child_append_relid_sets;
 	pathnode->path.pathtype = T_MergeAppend;
 	pathnode->path.parent = rel;
 	pathnode->path.pathtarget = rel->reltarget;
@@ -3951,6 +3955,7 @@ reparameterize_path(PlannerInfo *root, Path *path,
 				}
 				return (Path *)
 					create_append_path(root, rel, childpaths, partialpaths,
+									   apath->child_append_relid_sets,
 									   apath->path.pathkeys, required_outer,
 									   apath->path.parallel_workers,
 									   apath->path.parallel_aware,
diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h
index 684e02da063..6fb3a86598f 100644
--- a/src/include/nodes/pathnodes.h
+++ b/src/include/nodes/pathnodes.h
@@ -2099,6 +2099,12 @@ typedef struct CustomPath
  * For partial Append, 'subpaths' contains non-partial subpaths followed by
  * partial subpaths.
  *
+ * Whenever accumulate_append_subpath() allows us to consolidate multiple
+ * levels of Append paths are consolidated down to one, we store the RTI
+ * sets for the omitted paths in child_append_relid_sets. This is not necessary
+ * for planning or execution; we do it for the benefit of code that wants
+ * to inspect the final plan and understand how it came to be.
+ *
  * Note: it is possible for "subpaths" to contain only one, or even no,
  * elements.  These cases are optimized during create_append_plan.
  * In particular, an AppendPath with no subpaths is a "dummy" path that
@@ -2114,6 +2120,7 @@ typedef struct AppendPath
 	/* Index of first partial path in subpaths; list_length(subpaths) if none */
 	int			first_partial_path;
 	Cardinality limit_tuples;	/* hard limit on output tuples, or -1 */
+	List	   *child_append_relid_sets;
 } AppendPath;
 
 #define IS_DUMMY_APPEND(p) \
@@ -2130,12 +2137,15 @@ extern bool is_dummy_rel(RelOptInfo *rel);
 /*
  * MergeAppendPath represents a MergeAppend plan, ie, the merging of sorted
  * results from several member plans to produce similarly-sorted output.
+ *
+ * child_append_relid_sets has the same meaning here as for AppendPath.
  */
 typedef struct MergeAppendPath
 {
 	Path		path;
 	List	   *subpaths;		/* list of component Paths */
 	Cardinality limit_tuples;	/* hard limit on output tuples, or -1 */
+	List	   *child_append_relid_sets;
 } MergeAppendPath;
 
 /*
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index 497aec24876..19eceb56979 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -374,9 +374,16 @@ struct PartitionPruneInfo;		/* forward reference to struct below */
 typedef struct Append
 {
 	Plan		plan;
+
 	/* RTIs of appendrel(s) formed by this node */
 	Bitmapset  *apprelids;
+
+	/* sets of RTIs of appendrels consolidated into this node */
+	List	   *child_append_relid_sets;
+
+	/* plans to run */
 	List	   *appendplans;
+
 	/* # of asynchronous plans */
 	int			nasyncplans;
 
@@ -406,6 +413,10 @@ typedef struct MergeAppend
 	/* RTIs of appendrel(s) formed by this node */
 	Bitmapset  *apprelids;
 
+	/* sets of RTIs of appendrels consolidated into this node */
+	List	   *child_append_relid_sets;
+
+	/* plans to run */
 	List	   *mergeplans;
 
 	/* these fields are just like the sort-key info in struct Sort: */
diff --git a/src/include/optimizer/pathnode.h b/src/include/optimizer/pathnode.h
index 763cd25bb3c..5f43b7fd0cb 100644
--- a/src/include/optimizer/pathnode.h
+++ b/src/include/optimizer/pathnode.h
@@ -70,12 +70,14 @@ extern TidRangePath *create_tidrangescan_path(PlannerInfo *root,
 											  Relids required_outer);
 extern AppendPath *create_append_path(PlannerInfo *root, RelOptInfo *rel,
 									  List *subpaths, List *partial_subpaths,
+									  List *child_append_relid_sets,
 									  List *pathkeys, Relids required_outer,
 									  int parallel_workers, bool parallel_aware,
 									  double rows);
 extern MergeAppendPath *create_merge_append_path(PlannerInfo *root,
 												 RelOptInfo *rel,
 												 List *subpaths,
+												 List *child_append_relid_sets,
 												 List *pathkeys,
 												 Relids required_outer);
 extern GroupResultPath *create_group_result_path(PlannerInfo *root,
-- 
2.39.5 (Apple Git-154)

#20Dilip Kumar
dilipbalaut@gmail.com
In reply to: Robert Haas (#4)
Re: plan shape work

On Wed, May 21, 2025 at 7:29 PM Robert Haas <robertmhaas@gmail.com> wrote:

On Tue, May 20, 2025 at 2:45 PM Tomas Vondra <tomas@vondra.me> wrote:

I have a sense - possibly an incorrect one - that the core of the
problem here is that the planner considers lots of very similar
alternatives. A hypothetical feature that showed the second-cheapest
plan would be all but useless, because the second-cheapest plan would
just be a very minor variation of the cheapest plan in almost all
cases. One idea that crossed my mind was to display information in
EXPLAIN about what would have happened if we'd done something really
different. For instance, suppose that at a certain level of the plan
tree we actually chose a merge join, but we also show the estimated
cost of the cheapest hash join (if any) and the cheapest nested loop
(if any) that we considered at that level. The user might be able to
draw useful conclusions based on whether those numbers were altogether
absent (i.e. that join type was not viable at all) or whether the cost
was a little higher or a lot higher than that of the path actually
chosen. For scans, you could list which indexes were believed to be
usable and perhaps what the cost would have been for the cheapest one
not actually selected; and what the cost of a sequential scan would
have been if you hadn't picked one.

I'm not sure how useful this would be, so the whole idea might
actually suck, or maybe it's sort of the right idea but needs a bunch
of refinement to really be useful. I don't have a better idea right
now, though.

Having detailed information on the costs of alternative join
methods/scan method, even when a different method is chosen, would be
valuable information. For example, if a merge join is selected for
tables t1 and t2 in a subquery, showing the estimated costs for both a
hash join and a nested loop join would provide a more complete picture
of the planner's decision-making process.

And I believe, this information would be particularly useful if the
cost of a non-selected plan, such as a nested loop join, is very close
to the cost of the chosen merge join. In such cases, a database
administrator or query optimizer could use this insight to manually
override the planner's choice and opt for the nested loop join for
specific tables in a subquery. This level of detail would empower
users to fine-tune query performance and explore alternative execution
strategies.

IIUC, one of the goal of this work is where operator can say I want to
use this scan method while scanning a particular table in a particular
subquery, that means if the planner can give the information about non
selected paths as well then it would be really helpful in making this
process more smooth otherwise without much information on what path
got rejected its very hard to provide hints.

--
Regards,
Dilip Kumar
Google

#21Richard Guo
guofenglinux@gmail.com
In reply to: Tom Lane (#17)
Re: plan shape work

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

#22Robert Haas
robertmhaas@gmail.com
In reply to: Richard Guo (#21)
Re: plan shape work

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

#23Richard Guo
guofenglinux@gmail.com
In reply to: Robert Haas (#22)
Re: plan shape work

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

#24Robert Haas
robertmhaas@gmail.com
In reply to: Richard Guo (#23)
Re: plan shape work

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

#25Tom Lane
tgl@sss.pgh.pa.us
In reply to: Robert Haas (#24)
Re: plan shape work

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

#26Robert Haas
robertmhaas@gmail.com
In reply to: Tom Lane (#25)
Re: plan shape work

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

#27Robert Haas
robertmhaas@gmail.com
In reply to: Robert Haas (#26)
Re: plan shape work

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

#28Tom Lane
tgl@sss.pgh.pa.us
In reply to: Robert Haas (#27)
Re: plan shape work

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

#29Richard Guo
guofenglinux@gmail.com
In reply to: Robert Haas (#24)
Re: plan shape work

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

#30Robert Haas
robertmhaas@gmail.com
In reply to: Richard Guo (#29)
Re: plan shape work

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

#31Robert Haas
robertmhaas@gmail.com
In reply to: Tom Lane (#28)
Re: plan shape work

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

#32Tom Lane
tgl@sss.pgh.pa.us
In reply to: Richard Guo (#29)
Re: plan shape work

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

#33Robert Haas
robertmhaas@gmail.com
In reply to: Tom Lane (#32)
Re: plan shape work

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

#34Robert Haas
robertmhaas@gmail.com
In reply to: Robert Haas (#33)
3 attachment(s)
Re: plan shape work

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

Attachments:

v5-0003-Ensure-that-all-joinrel-RTIs-are-discoverable-fro.patchapplication/octet-stream; name=v5-0003-Ensure-that-all-joinrel-RTIs-are-discoverable-fro.patchDownload
From 3626956eada2104c0495bab53878986589e2377c Mon Sep 17 00:00:00 2001
From: Robert Haas <rhaas@postgresql.org>
Date: Fri, 5 Sep 2025 13:26:57 -0400
Subject: [PATCH v5 3/3] 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.

We also now assert when constructing a Join plan that we can find all of
the joinrel's RTIs and no others in the plan tree -- either associated
with a node below the outer or inner side of the join, or in the
'ojrelids' field of the Join itself. Any RTIs appearing in this last
place must be of type RTE_JOIN; scanned relations need to be associated
with underlying scan nodes.

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.

Note that this depends on the earlier commit to add a relids field to
Result nodes; without that change, a join tree involving two or more
Result nodes would be fundamentally ambiguous (and even a join tree
involving one could only be interpreted by guessing at its origin).
---
 .../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       | 200 +++++++++++++++++-
 src/include/nodes/plannodes.h                 |   2 +
 5 files changed, 272 insertions(+), 5 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..415d9e9dbab 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 86477906807..6998d56d22a 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -161,6 +161,7 @@ static CustomScan *create_customscan_plan(PlannerInfo *root,
 static NestLoop *create_nestloop_plan(PlannerInfo *root, NestPath *best_path);
 static MergeJoin *create_mergejoin_plan(PlannerInfo *root, MergePath *best_path);
 static HashJoin *create_hashjoin_plan(PlannerInfo *root, HashPath *best_path);
+static Bitmapset *compute_join_ojrelids(PlannerInfo *root, JoinPath *jpath);
 static Node *replace_nestloop_params(PlannerInfo *root, Node *expr);
 static Node *replace_nestloop_params_mutator(Node *node, PlannerInfo *root);
 static void fix_indexqual_references(PlannerInfo *root, IndexPath *index_path,
@@ -232,6 +233,7 @@ static BitmapOr *make_bitmap_or(List *bitmapplans);
 static NestLoop *make_nestloop(List *tlist,
 							   List *joinclauses, List *otherclauses, List *nestParams,
 							   Plan *lefttree, Plan *righttree,
+							   Bitmapset *ojrelids,
 							   JoinType jointype, bool inner_unique);
 static HashJoin *make_hashjoin(List *tlist,
 							   List *joinclauses, List *otherclauses,
@@ -239,6 +241,7 @@ static HashJoin *make_hashjoin(List *tlist,
 							   List *hashoperators, List *hashcollations,
 							   List *hashkeys,
 							   Plan *lefttree, Plan *righttree,
+							   Bitmapset *ojrelids,
 							   JoinType jointype, bool inner_unique);
 static Hash *make_hash(Plan *lefttree,
 					   List *hashkeys,
@@ -253,6 +256,7 @@ static MergeJoin *make_mergejoin(List *tlist,
 								 bool *mergereversals,
 								 bool *mergenullsfirst,
 								 Plan *lefttree, Plan *righttree,
+								 Bitmapset *ojrelids,
 								 JoinType jointype, bool inner_unique,
 								 bool skip_mark_restore);
 static Sort *make_sort(Plan *lefttree, int numCols,
@@ -320,7 +324,15 @@ static ModifyTable *make_modifytable(PlannerInfo *root, Plan *subplan,
 									 int epqParam);
 static GatherMerge *create_gather_merge_plan(PlannerInfo *root,
 											 GatherMergePath *best_path);
-
+static void assert_join_preserves_rtis(PlannerInfo *root,
+									   RelOptInfo *rel,
+									   Plan *outer_plan,
+									   Plan *inner_plan,
+									   Bitmapset *ojrelids);
+
+#ifdef USE_ASSERT_CHECKING
+static Bitmapset *get_plan_rtis(PlannerInfo *root, Plan *plan);
+#endif
 
 /*
  * create_plan
@@ -4335,11 +4347,16 @@ create_nestloop_plan(PlannerInfo *root,
 							  nestParams,
 							  outer_plan,
 							  inner_plan,
+							  compute_join_ojrelids(root, &best_path->jpath),
 							  best_path->jpath.jointype,
 							  best_path->jpath.inner_unique);
 
 	copy_generic_path_info(&join_plan->join.plan, &best_path->jpath.path);
 
+	assert_join_preserves_rtis(root, best_path->jpath.path.parent,
+							   outer_plan, inner_plan,
+							   join_plan->join.ojrelids);
+
 	return join_plan;
 }
 
@@ -4687,6 +4704,7 @@ create_mergejoin_plan(PlannerInfo *root,
 							   mergenullsfirst,
 							   outer_plan,
 							   inner_plan,
+							   compute_join_ojrelids(root, &best_path->jpath),
 							   best_path->jpath.jointype,
 							   best_path->jpath.inner_unique,
 							   best_path->skip_mark_restore);
@@ -4694,6 +4712,10 @@ create_mergejoin_plan(PlannerInfo *root,
 	/* Costs of sort and material steps are included in path cost already */
 	copy_generic_path_info(&join_plan->join.plan, &best_path->jpath.path);
 
+	assert_join_preserves_rtis(root, best_path->jpath.path.parent,
+							   outer_plan, inner_plan,
+							   join_plan->join.ojrelids);
+
 	return join_plan;
 }
 
@@ -4862,14 +4884,71 @@ create_hashjoin_plan(PlannerInfo *root,
 							  outer_hashkeys,
 							  outer_plan,
 							  (Plan *) hash_plan,
+							  compute_join_ojrelids(root, &best_path->jpath),
 							  best_path->jpath.jointype,
 							  best_path->jpath.inner_unique);
 
 	copy_generic_path_info(&join_plan->join.plan, &best_path->jpath.path);
 
+	assert_join_preserves_rtis(root, best_path->jpath.path.parent,
+							   outer_plan, inner_plan,
+							   join_plan->join.ojrelids);
+
 	return join_plan;
 }
 
+/*
+ * compute_join_ojrelids
+ *	  Determine the set of outer joins completed by this join.
+ *
+ * See add_outer_joins_to_relids for related logic. When we execute joins
+ * in syntactic order, this will compute a 1-item RTI set for outer joins
+ * and the empty set for inner joins. When we rearrange the join order using
+ * outer-join identity 3, the outer join isn't fully calculated until the
+ * commuted join has also been computed, so an upper outer join can complete
+ * multiple outer joins while a lower one completes none.
+ */
+static Bitmapset *
+compute_join_ojrelids(PlannerInfo *root, JoinPath *jpath)
+{
+	Bitmapset  *ojrelids;
+
+	ojrelids = jpath->path.parent->relids;
+	ojrelids = bms_difference(ojrelids, jpath->outerjoinpath->parent->relids);
+	ojrelids = bms_difference(ojrelids, jpath->innerjoinpath->parent->relids);
+
+#ifdef USE_ASSERT_CHECKING
+	switch (jpath->jointype)
+	{
+		case JOIN_INNER:
+			/* Inner joins should never complete outer joins. */
+			Assert(ojrelids == NULL);
+			break;
+		case JOIN_FULL:
+			/* Full outer joins cannot be commuted. */
+			Assert(bms_membership(ojrelids) == BMS_SINGLETON);
+			break;
+		default:
+			/* Other types of joins can be rearranged. */
+			break;
+	}
+
+	/* Any RTIs in the ojrelids set should be of type RTE_JOIN. */
+	if (ojrelids != NULL)
+	{
+		int rti = -1;
+
+		while ((rti = bms_next_member(ojrelids, rti)) >= 0)
+		{
+			RangeTblEntry *rte = planner_rt_fetch(rti, root);
+
+			Assert(rte->rtekind == RTE_JOIN);
+		}
+	}
+#endif
+
+	return ojrelids;
+}
 
 /*****************************************************************************
  *
@@ -5934,6 +6013,7 @@ make_nestloop(List *tlist,
 			  List *nestParams,
 			  Plan *lefttree,
 			  Plan *righttree,
+			  Bitmapset *ojrelids,
 			  JoinType jointype,
 			  bool inner_unique)
 {
@@ -5947,6 +6027,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;
@@ -5962,6 +6043,7 @@ make_hashjoin(List *tlist,
 			  List *hashkeys,
 			  Plan *lefttree,
 			  Plan *righttree,
+			  Bitmapset *ojrelids,
 			  JoinType jointype,
 			  bool inner_unique)
 {
@@ -5979,6 +6061,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;
 }
@@ -6017,6 +6100,7 @@ make_mergejoin(List *tlist,
 			   bool *mergenullsfirst,
 			   Plan *lefttree,
 			   Plan *righttree,
+			   Bitmapset *ojrelids,
 			   JoinType jointype,
 			   bool inner_unique,
 			   bool skip_mark_restore)
@@ -6037,6 +6121,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;
 }
@@ -7316,3 +7401,116 @@ is_projection_capable_plan(Plan *plan)
 	}
 	return true;
 }
+
+/*
+ * Check that every joinrel RTI appears in the inner or outer plan or in this
+ * Join's ojrelids set.
+ */
+static void
+assert_join_preserves_rtis(PlannerInfo *root, RelOptInfo *rel,
+						   Plan *outer_plan, Plan *inner_plan,
+						   Bitmapset *ojrelids)
+{
+#ifdef USE_ASSERT_CHECKING
+	Bitmapset  *outerrelids;
+	Bitmapset  *innerrelids;
+	Bitmapset  *joinrelids;
+
+	/* Find outer and inner relid sets. */
+	outerrelids = get_plan_rtis(root, outer_plan);
+	innerrelids = get_plan_rtis(root, inner_plan);
+
+	/* Any given scan RTI should appear in just one set. */
+	Assert(!bms_overlap(innerrelids, outerrelids));
+	Assert(!bms_overlap(outerrelids, ojrelids));
+	Assert(!bms_overlap(innerrelids, ojrelids));
+
+	/* Combine all three sets and check that all RTIs were preserved. */
+	joinrelids = bms_union(ojrelids, bms_union(innerrelids, outerrelids));
+	Assert(bms_equal(joinrelids, rel->relids));
+#endif
+}
+
+#ifdef USE_ASSERT_CHECKING
+/*
+ * Get the set of range table indexes for a scan or join node, or any executor
+ * node that could appear beneath a scan or join node.
+ *
+ * We're only interested in RTIs from within the same subquery, so we do not
+ * attempt to look through T_SubqueryScan here.
+ *
+ * When adding new cases to this function, be sure to also update
+ * ExplainPreScanNode, ExplainNode, and overexplain_per_node_hook as
+ * appropriate.
+ */
+static Bitmapset *
+get_plan_rtis(PlannerInfo *root, Plan *plan)
+{
+	switch (nodeTag(plan))
+	{
+		case T_SeqScan:
+		case T_SampleScan:
+		case T_IndexScan:
+		case T_IndexOnlyScan:
+		case T_BitmapHeapScan:
+		case T_TidScan:
+		case T_TidRangeScan:
+		case T_SubqueryScan:
+		case T_FunctionScan:
+		case T_TableFuncScan:
+		case T_ValuesScan:
+		case T_CteScan:
+		case T_NamedTuplestoreScan:
+		case T_WorkTableScan:
+			return bms_make_singleton(((Scan *) plan)->scanrelid);
+			break;
+		case T_ForeignScan:
+			return ((ForeignScan *) plan)->fs_relids;
+			break;
+		case T_CustomScan:
+			return ((CustomScan *) plan)->custom_relids;
+			break;
+		case T_Append:
+			return ((Append *) plan)->apprelids;
+			break;
+		case T_MergeAppend:
+			return ((MergeAppend *) plan)->apprelids;
+			break;
+		case T_Result:
+			if (plan->lefttree)
+				return get_plan_rtis(root, plan->lefttree);
+			else
+				return ((Result *) plan)->relids;
+			break;
+		case T_HashJoin:
+		case T_MergeJoin:
+		case T_NestLoop:
+			{
+				Bitmapset  *outerrelids;
+				Bitmapset  *innerrelids;
+
+				outerrelids = get_plan_rtis(root, plan->lefttree);
+				innerrelids = get_plan_rtis(root, plan->righttree);
+
+				return bms_union(bms_union(outerrelids, innerrelids),
+								 ((Join *) plan)->ojrelids);
+				break;
+			}
+		case T_Sort:
+		case T_IncrementalSort:
+		case T_Unique:
+		case T_Agg:
+		case T_Hash:
+		case T_Gather:
+		case T_GatherMerge:
+		case T_Material:
+		case T_Memoize:
+			return get_plan_rtis(root, plan->lefttree);
+			break;
+		default:
+			break;
+	}
+
+	return NULL;
+}
+#endif
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index 3908847e3bf..1d41e7b5f2e 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -940,6 +940,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;
@@ -964,6 +965,7 @@ typedef struct Join
 	bool		inner_unique;
 	/* JOIN quals (in addition to plan.qual) */
 	List	   *joinqual;
+	Bitmapset  *ojrelids;
 } Join;
 
 /* ----------------
-- 
2.39.5 (Apple Git-154)

v5-0001-Keep-track-of-what-RTIs-a-Result-node-is-scanning.patchapplication/octet-stream; name=v5-0001-Keep-track-of-what-RTIs-a-Result-node-is-scanning.patchDownload
From 8585f4c763e844038c9be0cd3cbba3f094ff793d Mon Sep 17 00:00:00 2001
From: Robert Haas <rhaas@postgresql.org>
Date: Tue, 2 Sep 2025 14:12:13 -0400
Subject: [PATCH v5 1/3] Keep track of what RTIs a Result node is scanning.

Result nodes now include an RTI set, which is only non-NULL when they
have no subplan, and is taken from the relid set of the RelOptInfo
that the Result is generating. They also now include a result_reason,
which tells us something about why the Result node was inserted.

Using that information, EXPLAIN now emits, where relevant, a "Replaces" line
that says whether it replaced a scan, a join, or an aggregate; and where
relevant, which relations were involved.

Likewise, pg_overexplain's EXPLAIN (RANGE_TABLE) now displays the RTIs
stored in a Result node just as it already does for other RTI-bearing
node types.
---
 contrib/file_fdw/expected/file_fdw.out        |   1 +
 .../expected/pg_overexplain.out               |   6 +-
 contrib/pg_overexplain/pg_overexplain.c       |  12 ++
 .../postgres_fdw/expected/postgres_fdw.out    |   9 +-
 src/backend/commands/explain.c                | 109 ++++++++++++++
 src/backend/optimizer/plan/createplan.c       | 117 ++++++++++-----
 src/backend/optimizer/plan/setrefs.c          |   2 +
 src/include/nodes/plannodes.h                 |  24 +++
 src/test/regress/expected/aggregates.out      |  51 ++++---
 src/test/regress/expected/case.out            |  21 +--
 .../regress/expected/generated_virtual.out    |   3 +-
 src/test/regress/expected/groupingsets.out    |   3 +-
 src/test/regress/expected/inherit.out         |  28 ++--
 src/test/regress/expected/join.out            | 113 ++++++++------
 src/test/regress/expected/merge.out           |   3 +-
 .../regress/expected/partition_aggregate.out  |  21 +--
 src/test/regress/expected/partition_join.out  |  23 +--
 src/test/regress/expected/partition_prune.out | 141 ++++++++++--------
 src/test/regress/expected/predicate.out       |  33 ++--
 src/test/regress/expected/rowsecurity.out     |  13 +-
 src/test/regress/expected/rowtypes.out        |   6 +-
 src/test/regress/expected/select.out          |   7 +-
 src/test/regress/expected/subselect.out       |   6 +-
 src/test/regress/expected/tsrf.out            |   6 +-
 24 files changed, 527 insertions(+), 231 deletions(-)

diff --git a/contrib/file_fdw/expected/file_fdw.out b/contrib/file_fdw/expected/file_fdw.out
index 246e3d3e566..5e710d06de0 100644
--- a/contrib/file_fdw/expected/file_fdw.out
+++ b/contrib/file_fdw/expected/file_fdw.out
@@ -323,6 +323,7 @@ SELECT explain_filter('EXPLAIN (VERBOSE, COSTS FALSE) SELECT * FROM agg_csv WHER
  Result
    Output: a, b
    One-Time Filter: false
+   Replaces: Scan on agg_csv
 
 \t off
 SELECT * FROM agg_csv WHERE a < 0;
diff --git a/contrib/pg_overexplain/expected/pg_overexplain.out b/contrib/pg_overexplain/expected/pg_overexplain.out
index 6de02323d7c..55d34666d87 100644
--- a/contrib/pg_overexplain/expected/pg_overexplain.out
+++ b/contrib/pg_overexplain/expected/pg_overexplain.out
@@ -44,9 +44,10 @@ EXPLAIN (RANGE_TABLE) SELECT 1;
                 QUERY PLAN                
 ------------------------------------------
  Result  (cost=0.00..0.01 rows=1 width=4)
+   RTIs: 1
  RTI 1 (result):
    Eref: "*RESULT*" ()
-(3 rows)
+(4 rows)
 
 -- Create a partitioned table.
 CREATE TABLE vegetables (id serial, name text, genus text)
@@ -475,6 +476,7 @@ INSERT INTO vegetables (name, genus) VALUES ('broccoflower', 'brassica');
    Nominal RTI: 1
    Exclude Relation RTI: 0
    ->  Result
+         RTIs: 2
  RTI 1 (relation):
    Eref: vegetables (id, name, genus)
    Relation: vegetables
@@ -485,5 +487,5 @@ INSERT INTO vegetables (name, genus) VALUES ('broccoflower', 'brassica');
    Eref: "*RESULT*" ()
  Unprunable RTIs: 1
  Result RTIs: 1
-(14 rows)
+(15 rows)
 
diff --git a/contrib/pg_overexplain/pg_overexplain.c b/contrib/pg_overexplain/pg_overexplain.c
index de824566f8c..bd70b6d9d5e 100644
--- a/contrib/pg_overexplain/pg_overexplain.c
+++ b/contrib/pg_overexplain/pg_overexplain.c
@@ -236,6 +236,18 @@ overexplain_per_node_hook(PlanState *planstate, List *ancestors,
 									  ((MergeAppend *) plan)->apprelids,
 									  es);
 				break;
+			case T_Result:
+
+				/*
+				 * 'relids' is only meaningful when plan->lefttree is NULL,
+				 * but if somehow it ends up set when plan->lefttree is not
+				 * NULL, print it anyway.
+				 */
+				if (plan->lefttree == NULL ||
+					((Result *) plan)->relids != NULL)
+					overexplain_bitmapset("RTIs",
+										  ((Result *) plan)->relids,
+										  es);
 			default:
 				break;
 		}
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 18d727d7790..b89805ec265 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -7158,7 +7158,8 @@ EXPLAIN (VERBOSE, COSTS OFF) SELECT count(*) FROM ft1 WHERE c2 < 0;
    Output: count(*)
    ->  Result
          One-Time Filter: false
-(4 rows)
+         Replaces: Scan on ft1
+(5 rows)
 
 SELECT count(*) FROM ft1 WHERE c2 < 0;
  count 
@@ -7202,7 +7203,8 @@ EXPLAIN (VERBOSE, COSTS OFF) SELECT count(*) FROM ft1 WHERE c2 >= 0;
    Output: count(*)
    ->  Result
          One-Time Filter: false
-(4 rows)
+         Replaces: Scan on ft1
+(5 rows)
 
 SELECT count(*) FROM ft1 WHERE c2 >= 0;
  count 
@@ -8031,7 +8033,8 @@ DELETE FROM rem1 WHERE false;     -- currently can't be pushed down
    ->  Result
          Output: ctid
          One-Time Filter: false
-(5 rows)
+         Replaces: Scan on rem1
+(6 rows)
 
 -- Test with statement-level triggers
 CREATE TRIGGER trig_stmt_before
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 8345bc0264b..affa5c8fa8a 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -147,6 +147,7 @@ static void show_buffer_usage(ExplainState *es, const BufferUsage *usage);
 static void show_wal_usage(ExplainState *es, const WalUsage *usage);
 static void show_memory_counters(ExplainState *es,
 								 const MemoryContextCounters *mem_counters);
+static void show_result_replacement_info(Result *result, ExplainState *es);
 static void ExplainIndexScanDetails(Oid indexid, ScanDirection indexorderdir,
 									ExplainState *es);
 static void ExplainScanTarget(Scan *plan, ExplainState *es);
@@ -2238,6 +2239,8 @@ ExplainNode(PlanState *planstate, List *ancestors,
 			if (plan->qual)
 				show_instrumentation_count("Rows Removed by Filter", 1,
 										   planstate, es);
+			if (plan->lefttree == NULL)
+				show_result_replacement_info(castNode(Result, plan), es);
 			break;
 		case T_ModifyTable:
 			show_modifytable_info(castNode(ModifyTableState, planstate), ancestors,
@@ -4750,6 +4753,112 @@ show_modifytable_info(ModifyTableState *mtstate, List *ancestors,
 		ExplainCloseGroup("Target Tables", "Target Tables", false, es);
 }
 
+/*
+ * Explain what a "Result" node replaced.
+ */
+static void
+show_result_replacement_info(Result *result, ExplainState *es)
+{
+	StringInfoData buf;
+	int			nrels = 0;
+	int			rti = -1;
+	bool		found_non_result = false;
+	char	   *replacement_type;
+
+	/* We print nothing extra for a gating result node. */
+	Assert(result->result_type != RESULT_TYPE_GATING);
+
+	/*
+	 * If the Result node has a subplan, it didn't replace a scan, join, or
+	 * aggregate;
+	 */
+	Assert(result->plan.lefttree == NULL);
+
+	switch (result->result_type)
+	{
+		case RESULT_TYPE_GATING:
+			replacement_type = "Gating";
+			break;
+		case RESULT_TYPE_SCAN:
+			replacement_type = "Scan";
+			break;
+		case RESULT_TYPE_JOIN:
+			replacement_type = "Join";
+			break;
+		case RESULT_TYPE_UPPER:
+			/* a small white lie */
+			replacement_type = "Aggregate";
+			break;
+		case RESULT_TYPE_MINMAX:
+			replacement_type = "MinMaxAggregate";
+			break;
+	}
+
+	/*
+	 * Build up a comma-separated list of user-facing names for the range
+	 * table entries in the relids set.
+	 */
+	initStringInfo(&buf);
+	while ((rti = bms_next_member(result->relids, rti)) >= 0)
+	{
+		RangeTblEntry *rte = rt_fetch(rti, es->rtable);
+		char	   *refname;
+
+		/*
+		 * add_outer_joins_to_relids will add join RTIs to the relids set of a
+		 * join; if that join is then replaced with a Result node, we may see
+		 * such RTIs here. But we want to completely ignore those here,
+		 * because "a LEFT JOIN b ON whatever" is a join between a and b, not
+		 * a join between a, b, and an unnamed join.
+		 */
+		if (rte->rtekind == RTE_JOIN)
+			continue;
+
+		/* Count the number of rels that aren't ignored completely. */
+		++nrels;
+
+		/* Work out what reference name to use and added it the string. */
+		refname = (char *) list_nth(es->rtable_names, rti - 1);
+		if (refname == NULL)
+			refname = rte->eref->aliasname;
+		if (buf.len > 0)
+			appendStringInfoString(&buf, ", ");
+		appendStringInfoString(&buf, refname);
+
+		/* Keep track of whether we see anything other than RTE_RESULT. */
+		if (rte->rtekind != RTE_RESULT)
+			found_non_result = true;
+	}
+
+	/*
+	 * If this Result node is because of a single RTE that is RTE_RESULT, it
+	 * is not really replacing anything at all, because there's no other
+	 * method for implementing a scan of such an RTE, so we don't display the
+	 * Replaces line in such cases.
+	 *
+	 * (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.)
+	 */
+	if (nrels <= 1 && !found_non_result &&
+		result->result_type == RESULT_TYPE_SCAN)
+		return;
+
+	/*
+	 * 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);
+	}
+}
+
 /*
  * Explain the constituent plans of an Append, MergeAppend,
  * BitmapAnd, or BitmapOr node.
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index 6791cbeb416..86477906807 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -99,7 +99,8 @@ static Gather *create_gather_plan(PlannerInfo *root, GatherPath *best_path);
 static Plan *create_projection_plan(PlannerInfo *root,
 									ProjectionPath *best_path,
 									int flags);
-static Plan *inject_projection_plan(Plan *subplan, List *tlist, bool parallel_safe);
+static Plan *inject_projection_plan(Plan *subplan, List *tlist,
+									bool parallel_safe);
 static Sort *create_sort_plan(PlannerInfo *root, SortPath *best_path, int flags);
 static IncrementalSort *create_incrementalsort_plan(PlannerInfo *root,
 													IncrementalSortPath *best_path, int flags);
@@ -302,7 +303,10 @@ static SetOp *make_setop(SetOpCmd cmd, SetOpStrategy strategy,
 						 List *tlist, Plan *lefttree, Plan *righttree,
 						 List *groupList, long numGroups);
 static LockRows *make_lockrows(Plan *lefttree, List *rowMarks, int epqParam);
-static Result *make_result(List *tlist, Node *resconstantqual, Plan *subplan);
+static Result *make_gating_result(List *tlist, Node *resconstantqual,
+								  Plan *subplan);
+static Result *make_simple_result(List *tlist, Node *resconstantqual,
+								  RelOptInfo *rel);
 static ProjectSet *make_project_set(List *tlist, Plan *subplan);
 static ModifyTable *make_modifytable(PlannerInfo *root, Plan *subplan,
 									 CmdType operation, bool canSetTag,
@@ -1012,36 +1016,36 @@ static Plan *
 create_gating_plan(PlannerInfo *root, Path *path, Plan *plan,
 				   List *gating_quals)
 {
-	Plan	   *gplan;
-	Plan	   *splan;
+	Result	   *gplan;
 
 	Assert(gating_quals);
 
 	/*
-	 * We might have a trivial Result plan already.  Stacking one Result atop
-	 * another is silly, so if that applies, just discard the input plan.
+	 * Since we need a Result node anyway, always return the path's requested
+	 * tlist; that's never a wrong choice, even if the parent node didn't ask
+	 * for CP_EXACT_TLIST.
+	 */
+	gplan = make_gating_result(build_path_tlist(root, path),
+							   (Node *) gating_quals, plan);
+
+	/*
+	 * We might have had a trivial Result plan already.  Stacking one Result
+	 * atop another is silly, so if that applies, just discard the input plan.
 	 * (We're assuming its targetlist is uninteresting; it should be either
-	 * the same as the result of build_path_tlist, or a simplified version.)
+	 * the same as the result of build_path_tlist, or a simplified version.
+	 * However, we preserve the set of relids that it purports to scan and
+	 * attribute that to our replacement Result instead, and likewise for the
+	 * result_type.)
 	 */
-	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;
 	}
 
-	/*
-	 * Since we need a Result node anyway, always return the path's requested
-	 * tlist; that's never a wrong choice, even if the parent node didn't ask
-	 * for CP_EXACT_TLIST.
-	 */
-	gplan = (Plan *) make_result(build_path_tlist(root, path),
-								 (Node *) gating_quals,
-								 splan);
-
 	/*
 	 * Notice that we don't change cost or size estimates when doing gating.
 	 * The costs of qual eval were already included in the subplan's cost.
@@ -1054,12 +1058,12 @@ create_gating_plan(PlannerInfo *root, Path *path, Plan *plan,
 	 * in most cases we have only a very bad idea of the probability of the
 	 * gating qual being true.
 	 */
-	copy_plan_costsize(gplan, plan);
+	copy_plan_costsize(&gplan->plan, plan);
 
 	/* Gating quals could be unsafe, so better use the Path's safety flag */
-	gplan->parallel_safe = path->parallel_safe;
+	gplan->plan.parallel_safe = path->parallel_safe;
 
-	return gplan;
+	return &gplan->plan;
 }
 
 /*
@@ -1235,10 +1239,10 @@ create_append_plan(PlannerInfo *root, AppendPath *best_path, int flags)
 		/* Generate a Result plan with constant-FALSE gating qual */
 		Plan	   *plan;
 
-		plan = (Plan *) make_result(tlist,
-									(Node *) list_make1(makeBoolConst(false,
-																	  false)),
-									NULL);
+		plan = (Plan *) make_simple_result(tlist,
+										   (Node *) list_make1(makeBoolConst(false,
+																			 false)),
+										   best_path->path.parent);
 
 		copy_generic_path_info(plan, (Path *) best_path);
 
@@ -1636,7 +1640,7 @@ create_group_result_plan(PlannerInfo *root, GroupResultPath *best_path)
 	/* best_path->quals is just bare clauses */
 	quals = order_qual_clauses(root, best_path->quals);
 
-	plan = make_result(tlist, (Node *) quals, NULL);
+	plan = make_simple_result(tlist, (Node *) quals, best_path->path.parent);
 
 	copy_generic_path_info(&plan->plan, (Path *) best_path);
 
@@ -1933,8 +1937,7 @@ create_projection_plan(PlannerInfo *root, ProjectionPath *best_path, int flags)
 	}
 	else
 	{
-		/* We need a Result node */
-		plan = (Plan *) make_result(tlist, NULL, subplan);
+		plan = (Plan *) make_gating_result(tlist, NULL, subplan);
 
 		copy_generic_path_info(plan, (Path *) best_path);
 	}
@@ -1958,7 +1961,7 @@ inject_projection_plan(Plan *subplan, List *tlist, bool parallel_safe)
 {
 	Plan	   *plan;
 
-	plan = (Plan *) make_result(tlist, NULL, subplan);
+	plan = (Plan *) make_gating_result(tlist, NULL, subplan);
 
 	/*
 	 * In principle, we should charge tlist eval cost plus cpu_per_tuple per
@@ -2436,7 +2439,9 @@ create_minmaxagg_plan(PlannerInfo *root, MinMaxAggPath *best_path)
 	/* Generate the output plan --- basically just a Result */
 	tlist = build_path_tlist(root, &best_path->path);
 
-	plan = make_result(tlist, (Node *) best_path->quals, NULL);
+	plan = make_simple_result(tlist, (Node *) best_path->quals,
+							  best_path->path.parent);
+	plan->result_type = RESULT_TYPE_MINMAX;
 
 	copy_generic_path_info(&plan->plan, (Path *) best_path);
 
@@ -3887,7 +3892,8 @@ create_resultscan_plan(PlannerInfo *root, Path *best_path,
 			replace_nestloop_params(root, (Node *) scan_clauses);
 	}
 
-	scan_plan = make_result(tlist, (Node *) scan_clauses, NULL);
+	scan_plan = make_simple_result(tlist, (Node *) scan_clauses,
+								   best_path->parent);
 
 	copy_generic_path_info(&scan_plan->plan, best_path);
 
@@ -6922,22 +6928,57 @@ make_limit(Plan *lefttree, Node *limitOffset, Node *limitCount,
 }
 
 /*
- * make_result
- *	  Build a Result plan node
+ * make_gating_result
+ *	  Build a Result plan node that performs projection of a subplan, and/or
+ *	  applies a one time filter (resconstantqual)
  */
 static Result *
-make_result(List *tlist,
-			Node *resconstantqual,
-			Plan *subplan)
+make_gating_result(List *tlist,
+				   Node *resconstantqual,
+				   Plan *subplan)
 {
 	Result	   *node = makeNode(Result);
 	Plan	   *plan = &node->plan;
 
+	Assert(subplan != NULL);
+
 	plan->targetlist = tlist;
 	plan->qual = NIL;
 	plan->lefttree = subplan;
 	plan->righttree = NULL;
+	node->result_type = RESULT_TYPE_GATING;
+	node->resconstantqual = resconstantqual;
+	node->relids = NULL;
+
+	return node;
+}
+
+/*
+ * 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)
+ *
+ * 'rel' should be this path's RelOptInfo. In essence, we're saying that this
+ * Result node generates all the tuples for that RelOptInfo. Note that the same
+ * consideration can never arise in make_gating_result(), because in that case
+ * the tuples are always coming from some subordinate node.
+ */
+static Result *
+make_simple_result(List *tlist,
+				   Node *resconstantqual,
+				   RelOptInfo *rel)
+{
+	Result	   *node = makeNode(Result);
+	Plan	   *plan = &node->plan;
+
+	plan->targetlist = tlist;
+	plan->qual = NIL;
+	plan->lefttree = NULL;
+	plan->righttree = NULL;
+	node->result_type = IS_UPPER_REL(rel) ? RESULT_TYPE_UPPER :
+		IS_JOIN_REL(rel) ? RESULT_TYPE_JOIN : RESULT_TYPE_SCAN;
 	node->resconstantqual = resconstantqual;
+	node->relids = rel->relids;
 
 	return node;
 }
diff --git a/src/backend/optimizer/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.c
index d706546f332..6950eff2c5b 100644
--- a/src/backend/optimizer/plan/setrefs.c
+++ b/src/backend/optimizer/plan/setrefs.c
@@ -1056,6 +1056,8 @@ set_plan_refs(PlannerInfo *root, Plan *plan, int rtoffset)
 				/* resconstantqual can't contain any subplan variable refs */
 				splan->resconstantqual =
 					fix_scan_expr(root, splan->resconstantqual, rtoffset, 1);
+				/* adjust the relids set */
+				splan->relids = offset_relid_set(splan->relids, rtoffset);
 			}
 			break;
 		case T_ProjectSet:
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index 29d7732d6a0..3908847e3bf 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -252,6 +252,20 @@ typedef struct Plan
 #define outerPlan(node)			(((Plan *)(node))->lefttree)
 
 
+/* ----------------
+ *	 ResultType -
+ *		Classification of Result nodes
+ * ----------------
+ */
+typedef enum ResultType
+{
+	RESULT_TYPE_GATING,			/* project or one-time-filter outer plan */
+	RESULT_TYPE_SCAN,			/* replace empty scan */
+	RESULT_TYPE_JOIN,			/* replace empty join */
+	RESULT_TYPE_UPPER,			/* replace degenerate upper rel */
+	RESULT_TYPE_MINMAX			/* implement minmax aggregate */
+} ResultType;
+
 /* ----------------
  *	 Result node -
  *		If no outer plan, evaluate a variable-free targetlist.
@@ -261,12 +275,22 @@ typedef struct Plan
  * If resconstantqual isn't NULL, it represents a one-time qualification
  * test (i.e., one that doesn't depend on any variables from the outer plan,
  * so needs to be evaluated only once).
+ *
+ * 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).
  * ----------------
  */
 typedef struct Result
 {
 	Plan		plan;
+	ResultType	result_type;
 	Node	   *resconstantqual;
+	Bitmapset  *relids;
 } Result;
 
 /* ----------------
diff --git a/src/test/regress/expected/aggregates.out b/src/test/regress/expected/aggregates.out
index c35288eecde..1f24f6ffd1f 100644
--- a/src/test/regress/expected/aggregates.out
+++ b/src/test/regress/expected/aggregates.out
@@ -959,11 +959,12 @@ explain (costs off)
                          QUERY PLAN                         
 ------------------------------------------------------------
  Result
+   Replaces: MinMaxAggregate
    InitPlan 1
      ->  Limit
            ->  Index Only Scan using tenk1_unique1 on tenk1
                  Index Cond: (unique1 IS NOT NULL)
-(5 rows)
+(6 rows)
 
 select min(unique1) from tenk1;
  min 
@@ -976,11 +977,12 @@ explain (costs off)
                              QUERY PLAN                              
 ---------------------------------------------------------------------
  Result
+   Replaces: MinMaxAggregate
    InitPlan 1
      ->  Limit
            ->  Index Only Scan Backward using tenk1_unique1 on tenk1
                  Index Cond: (unique1 IS NOT NULL)
-(5 rows)
+(6 rows)
 
 select max(unique1) from tenk1;
  max  
@@ -993,11 +995,12 @@ explain (costs off)
                                QUERY PLAN                               
 ------------------------------------------------------------------------
  Result
+   Replaces: MinMaxAggregate
    InitPlan 1
      ->  Limit
            ->  Index Only Scan Backward using tenk1_unique1 on tenk1
                  Index Cond: ((unique1 IS NOT NULL) AND (unique1 < 42))
-(5 rows)
+(6 rows)
 
 select max(unique1) from tenk1 where unique1 < 42;
  max 
@@ -1010,11 +1013,12 @@ explain (costs off)
                                QUERY PLAN                               
 ------------------------------------------------------------------------
  Result
+   Replaces: MinMaxAggregate
    InitPlan 1
      ->  Limit
            ->  Index Only Scan Backward using tenk1_unique1 on tenk1
                  Index Cond: ((unique1 IS NOT NULL) AND (unique1 > 42))
-(5 rows)
+(6 rows)
 
 select max(unique1) from tenk1 where unique1 > 42;
  max  
@@ -1033,11 +1037,12 @@ explain (costs off)
                                 QUERY PLAN                                 
 ---------------------------------------------------------------------------
  Result
+   Replaces: MinMaxAggregate
    InitPlan 1
      ->  Limit
            ->  Index Only Scan Backward using tenk1_unique1 on tenk1
                  Index Cond: ((unique1 IS NOT NULL) AND (unique1 > 42000))
-(5 rows)
+(6 rows)
 
 select max(unique1) from tenk1 where unique1 > 42000;
  max 
@@ -1052,11 +1057,12 @@ explain (costs off)
                                  QUERY PLAN                                 
 ----------------------------------------------------------------------------
  Result
+   Replaces: MinMaxAggregate
    InitPlan 1
      ->  Limit
            ->  Index Only Scan Backward using tenk1_thous_tenthous on tenk1
                  Index Cond: ((thousand = 33) AND (tenthous IS NOT NULL))
-(5 rows)
+(6 rows)
 
 select max(tenthous) from tenk1 where thousand = 33;
  max  
@@ -1069,11 +1075,12 @@ explain (costs off)
                                 QUERY PLAN                                
 --------------------------------------------------------------------------
  Result
+   Replaces: MinMaxAggregate
    InitPlan 1
      ->  Limit
            ->  Index Only Scan using tenk1_thous_tenthous on tenk1
                  Index Cond: ((thousand = 33) AND (tenthous IS NOT NULL))
-(5 rows)
+(6 rows)
 
 select min(tenthous) from tenk1 where thousand = 33;
  min 
@@ -1090,11 +1097,12 @@ explain (costs off)
  Seq Scan on int4_tbl
    SubPlan 2
      ->  Result
+           Replaces: MinMaxAggregate
            InitPlan 1
              ->  Limit
                    ->  Index Only Scan using tenk1_unique1 on tenk1
                          Index Cond: ((unique1 IS NOT NULL) AND (unique1 > int4_tbl.f1))
-(7 rows)
+(8 rows)
 
 select f1, (select min(unique1) from tenk1 where unique1 > f1) AS gt
   from int4_tbl;
@@ -1119,7 +1127,8 @@ explain (costs off)
            ->  Index Only Scan Backward using tenk1_unique2 on tenk1
                  Index Cond: (unique2 IS NOT NULL)
    ->  Result
-(7 rows)
+         Replaces: MinMaxAggregate
+(8 rows)
 
 select distinct max(unique2) from tenk1;
  max  
@@ -1138,7 +1147,8 @@ explain (costs off)
            ->  Index Only Scan Backward using tenk1_unique2 on tenk1
                  Index Cond: (unique2 IS NOT NULL)
    ->  Result
-(7 rows)
+         Replaces: MinMaxAggregate
+(8 rows)
 
 select max(unique2) from tenk1 order by 1;
  max  
@@ -1157,7 +1167,8 @@ explain (costs off)
            ->  Index Only Scan Backward using tenk1_unique2 on tenk1
                  Index Cond: (unique2 IS NOT NULL)
    ->  Result
-(7 rows)
+         Replaces: MinMaxAggregate
+(8 rows)
 
 select max(unique2) from tenk1 order by max(unique2);
  max  
@@ -1176,7 +1187,8 @@ explain (costs off)
            ->  Index Only Scan Backward using tenk1_unique2 on tenk1
                  Index Cond: (unique2 IS NOT NULL)
    ->  Result
-(7 rows)
+         Replaces: MinMaxAggregate
+(8 rows)
 
 select max(unique2) from tenk1 order by max(unique2)+1;
  max  
@@ -1196,7 +1208,8 @@ explain (costs off)
                  Index Cond: (unique2 IS NOT NULL)
    ->  ProjectSet
          ->  Result
-(8 rows)
+               Replaces: MinMaxAggregate
+(9 rows)
 
 select max(unique2), generate_series(1,3) as g from tenk1 order by g desc;
  max  | g 
@@ -1212,12 +1225,13 @@ explain (costs off)
                      QUERY PLAN                     
 ----------------------------------------------------
  Result
+   Replaces: MinMaxAggregate
    InitPlan 1
      ->  Limit
            ->  Result
                  One-Time Filter: (100 IS NOT NULL)
                  ->  Seq Scan on tenk1
-(6 rows)
+(7 rows)
 
 select max(100) from tenk1;
  max 
@@ -1243,6 +1257,7 @@ explain (costs off)
                                          QUERY PLAN                                          
 ---------------------------------------------------------------------------------------------
  Result
+   Replaces: MinMaxAggregate
    InitPlan 1
      ->  Limit
            ->  Merge Append
@@ -1265,7 +1280,7 @@ explain (costs off)
                  ->  Index Only Scan using minmaxtest2i on minmaxtest2 minmaxtest_8
                        Index Cond: (f1 IS NOT NULL)
                  ->  Index Only Scan Backward using minmaxtest3i on minmaxtest3 minmaxtest_9
-(23 rows)
+(24 rows)
 
 select min(f1), max(f1) from minmaxtest;
  min | max 
@@ -1304,7 +1319,8 @@ explain (costs off)
    ->  Sort
          Sort Key: ((InitPlan 1).col1), ((InitPlan 2).col1)
          ->  Result
-(26 rows)
+               Replaces: MinMaxAggregate
+(27 rows)
 
 select distinct min(f1), max(f1) from minmaxtest;
  min | max 
@@ -1334,7 +1350,8 @@ explain (costs off)
                    ->  Seq Scan on int4_tbl t1
                          Filter: ((f1 IS NOT NULL) AND (f1 = t0.f1))
            ->  Result
-(9 rows)
+                 Replaces: MinMaxAggregate
+(10 rows)
 
 select f1, (select distinct min(t1.f1) from int4_tbl t1 where t1.f1 = t0.f1)
 from int4_tbl t0;
diff --git a/src/test/regress/expected/case.out b/src/test/regress/expected/case.out
index efee7fc4317..d74b0dd68cc 100644
--- a/src/test/regress/expected/case.out
+++ b/src/test/regress/expected/case.out
@@ -266,27 +266,30 @@ SELECT *
 -- Tests for constant subexpression simplification
 explain (costs off)
 SELECT * FROM CASE_TBL WHERE NULLIF(1, 2) = 2;
-        QUERY PLAN        
---------------------------
+          QUERY PLAN          
+------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on case_tbl
+(3 rows)
 
 explain (costs off)
 SELECT * FROM CASE_TBL WHERE NULLIF(1, 1) IS NOT NULL;
-        QUERY PLAN        
---------------------------
+          QUERY PLAN          
+------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on case_tbl
+(3 rows)
 
 explain (costs off)
 SELECT * FROM CASE_TBL WHERE NULLIF(1, null) = 2;
-        QUERY PLAN        
---------------------------
+          QUERY PLAN          
+------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on case_tbl
+(3 rows)
 
 --
 -- Examples of updates involving tables
diff --git a/src/test/regress/expected/generated_virtual.out b/src/test/regress/expected/generated_virtual.out
index aca6347babe..8b25e77bb4d 100644
--- a/src/test/regress/expected/generated_virtual.out
+++ b/src/test/regress/expected/generated_virtual.out
@@ -1580,7 +1580,8 @@ select t2.* from gtest32 t1 left join gtest32 t2 on false;
    ->  Result
          Output: a, e, 20, COALESCE(a, 100)
          One-Time Filter: false
-(8 rows)
+         Replaces: Scan on t2
+(9 rows)
 
 select t2.* from gtest32 t1 left join gtest32 t2 on false;
  a | b | c | d | e 
diff --git a/src/test/regress/expected/groupingsets.out b/src/test/regress/expected/groupingsets.out
index 35e4cb47ebe..210bbe307a7 100644
--- a/src/test/regress/expected/groupingsets.out
+++ b/src/test/regress/expected/groupingsets.out
@@ -591,11 +591,12 @@ explain (costs off)
                          QUERY PLAN                         
 ------------------------------------------------------------
  Result
+   Replaces: MinMaxAggregate
    InitPlan 1
      ->  Limit
            ->  Index Only Scan using tenk1_unique1 on tenk1
                  Index Cond: (unique1 IS NOT NULL)
-(5 rows)
+(6 rows)
 
 -- Views with GROUPING SET queries
 CREATE VIEW gstest_view AS select a, b, grouping(a,b), sum(c), count(*), max(c)
diff --git a/src/test/regress/expected/inherit.out b/src/test/regress/expected/inherit.out
index 5b5055babdc..589792284e1 100644
--- a/src/test/regress/expected/inherit.out
+++ b/src/test/regress/expected/inherit.out
@@ -581,7 +581,8 @@ update some_tab set a = a + 1 where false;
    ->  Result
          Output: (some_tab.a + 1), NULL::oid, NULL::tid
          One-Time Filter: false
-(4 rows)
+         Replaces: Scan on some_tab
+(5 rows)
 
 update some_tab set a = a + 1 where false;
 explain (verbose, costs off)
@@ -593,7 +594,8 @@ update some_tab set a = a + 1 where false returning b, a;
    ->  Result
          Output: (some_tab.a + 1), NULL::oid, NULL::tid
          One-Time Filter: false
-(5 rows)
+         Replaces: Scan on some_tab
+(6 rows)
 
 update some_tab set a = a + 1 where false returning b, a;
  b | a 
@@ -700,7 +702,8 @@ explain update parted_tab set a = 2 where false;
  Update on parted_tab  (cost=0.00..0.00 rows=0 width=0)
    ->  Result  (cost=0.00..0.00 rows=0 width=10)
          One-Time Filter: false
-(3 rows)
+         Replaces: Scan on parted_tab
+(4 rows)
 
 drop table parted_tab;
 -- Check UPDATE with multi-level partitioned inherited target
@@ -1756,6 +1759,7 @@ explain (verbose, costs off) select min(1-id) from matest0;
 ---------------------------------------------------------------------------------
  Result
    Output: (InitPlan 1).col1
+   Replaces: MinMaxAggregate
    InitPlan 1
      ->  Limit
            Output: ((1 - matest0.id))
@@ -1779,7 +1783,7 @@ explain (verbose, costs off) select min(1-id) from matest0;
                        ->  Index Scan using matest3i on public.matest3 matest0_4
                              Output: matest0_4.id, (1 - matest0_4.id)
                              Index Cond: ((1 - matest0_4.id) IS NOT NULL)
-(25 rows)
+(26 rows)
 
 select min(1-id) from matest0;
  min 
@@ -1943,6 +1947,7 @@ SELECT min(x) FROM
                              QUERY PLAN                             
 --------------------------------------------------------------------
  Result
+   Replaces: MinMaxAggregate
    InitPlan 1
      ->  Limit
            ->  Merge Append
@@ -1951,7 +1956,7 @@ SELECT min(x) FROM
                        Index Cond: (unique1 IS NOT NULL)
                  ->  Index Only Scan using tenk1_unique2 on tenk1 b
                        Index Cond: (unique2 IS NOT NULL)
-(9 rows)
+(10 rows)
 
 explain (costs off)
 SELECT min(y) FROM
@@ -1961,6 +1966,7 @@ SELECT min(y) FROM
                              QUERY PLAN                             
 --------------------------------------------------------------------
  Result
+   Replaces: MinMaxAggregate
    InitPlan 1
      ->  Limit
            ->  Merge Append
@@ -1969,7 +1975,7 @@ SELECT min(y) FROM
                        Index Cond: (unique1 IS NOT NULL)
                  ->  Index Only Scan using tenk1_unique2 on tenk1 b
                        Index Cond: (unique2 IS NOT NULL)
-(9 rows)
+(10 rows)
 
 -- XXX planner doesn't recognize that index on unique2 is sufficiently sorted
 explain (costs off)
@@ -3087,11 +3093,12 @@ explain (costs off) select * from range_list_parted where a between 3 and 23 and
 
 /* Should select no rows because range partition key cannot be null */
 explain (costs off) select * from range_list_parted where a is null;
-        QUERY PLAN        
---------------------------
+              QUERY PLAN               
+---------------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on range_list_parted
+(3 rows)
 
 /* Should only select rows from the null-accepting partition */
 explain (costs off) select * from range_list_parted where b is null;
@@ -3252,6 +3259,7 @@ explain (costs off) select min(a), max(a) from parted_minmax where b = '12345';
                                            QUERY PLAN                                           
 ------------------------------------------------------------------------------------------------
  Result
+   Replaces: MinMaxAggregate
    InitPlan 1
      ->  Limit
            ->  Index Only Scan using parted_minmax1i on parted_minmax1 parted_minmax
@@ -3260,7 +3268,7 @@ explain (costs off) select min(a), max(a) from parted_minmax where b = '12345';
      ->  Limit
            ->  Index Only Scan Backward using parted_minmax1i on parted_minmax1 parted_minmax_1
                  Index Cond: ((a IS NOT NULL) AND (b = '12345'::text))
-(9 rows)
+(10 rows)
 
 select min(a), max(a) from parted_minmax where b = '12345';
  min | max 
diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out
index 04079268b98..88af557a45c 100644
--- a/src/test/regress/expected/join.out
+++ b/src/test/regress/expected/join.out
@@ -2264,11 +2264,12 @@ explain (costs off)
 select aa, bb, unique1, unique1
   from tenk1 right join b_star on aa = unique1
   where bb < bb and bb is null;
-        QUERY PLAN        
---------------------------
+            QUERY PLAN             
+-----------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Join on tenk1, b_star
+(3 rows)
 
 select aa, bb, unique1, unique1
   from tenk1 right join b_star on aa = unique1
@@ -2386,11 +2387,12 @@ order by t1.unique1;
                            Index Cond: (unique1 < 10)
                SubPlan 2
                  ->  Result
+                       Replaces: MinMaxAggregate
                        InitPlan 1
                          ->  Limit
                                ->  Index Only Scan using tenk1_unique1 on tenk1
                                      Index Cond: ((unique1 IS NOT NULL) AND (unique1 = t2.unique1))
-(19 rows)
+(20 rows)
 
 -- Ensure we get the expected result
 select t1.unique1,t2.unique1 from tenk1 t1
@@ -2655,8 +2657,8 @@ select * from int8_tbl t1 left join
     (int8_tbl t2 left join int8_tbl t3 full join int8_tbl t4 on false on false)
     left join int8_tbl t5 on t2.q1 = t5.q1
 on t2.q2 = 123;
-                    QUERY PLAN                    
---------------------------------------------------
+                     QUERY PLAN                     
+----------------------------------------------------
  Nested Loop Left Join
    ->  Seq Scan on int8_tbl t1
    ->  Materialize
@@ -2668,8 +2670,9 @@ on t2.q2 = 123;
                            Filter: (q2 = 123)
                      ->  Result
                            One-Time Filter: false
+                           Replaces: Join on t3, t4
                ->  Seq Scan on int8_tbl t5
-(12 rows)
+(13 rows)
 
 explain (costs off)
 select * from int8_tbl t1
@@ -4165,6 +4168,7 @@ select * from t t1
          ->  Result
                Output: i3
                One-Time Filter: false
+               Replaces: Scan on t3
    ->  Memoize
          Output: t4.i4
          Cache Key: (1)
@@ -4172,7 +4176,7 @@ select * from t t1
          ->  Index Only Scan using t_pkey on pg_temp.t t4
                Output: t4.i4
                Index Cond: (t4.i4 > (1))
-(25 rows)
+(26 rows)
 
 explain (verbose, costs off)
 select * from
@@ -4363,7 +4367,8 @@ from int4_tbl t1
  Result
    Output: (current_database())::information_schema.sql_identifier, (c.relname)::information_schema.sql_identifier
    One-Time Filter: false
-(3 rows)
+   Replaces: Join on t1, t2, a, c, nc, t, nt, bt, nbt
+(4 rows)
 
 -- Test handling of qual pushdown to appendrel members with non-Var outputs
 explain (verbose, costs off)
@@ -4435,11 +4440,12 @@ select unique1 from tenk1, lateral f_immutable_int4(1) x where x = unique1;
 
 explain (costs off)
 select unique1 from tenk1, lateral f_immutable_int4(1) x where x in (select 17);
-        QUERY PLAN        
---------------------------
+        QUERY PLAN         
+---------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on tenk1
+(3 rows)
 
 explain (costs off)
 select unique1, x from tenk1 join f_immutable_int4(1) x on unique1 = x;
@@ -4485,11 +4491,12 @@ select unique1, x from tenk1 full join f_immutable_int4(1) x on unique1 = x;
 -- check that pullup of a const function allows further const-folding
 explain (costs off)
 select unique1 from tenk1, f_immutable_int4(1) x where x = 42;
-        QUERY PLAN        
---------------------------
+        QUERY PLAN         
+---------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on tenk1
+(3 rows)
 
 -- test inlining of immutable functions with PlaceHolderVars
 explain (costs off)
@@ -5346,7 +5353,8 @@ left join
    ->  Result
    ->  Result
          One-Time Filter: false
-(5 rows)
+         Replaces: Join on c, n
+(6 rows)
 
 -- check handling of apparently-commutable outer joins with non-commutable
 -- joins between them
@@ -5540,12 +5548,13 @@ select 1 from
      right join (select 1 as z) as ss2 on true)
   on false,
   lateral (select i4.f1, ss1.n from int8_tbl as i8 limit 1) as ss3;
-        QUERY PLAN        
---------------------------
+                  QUERY PLAN                   
+-----------------------------------------------
  Result
    Output: 1
    One-Time Filter: false
-(3 rows)
+   Replaces: Join on i4, ss3, x1, x2, *RESULT*
+(4 rows)
 
 select 1 from
   int4_tbl as i4
@@ -5574,11 +5583,12 @@ select 1 from t t1
        on false
      where t3.a = coalesce(t5.a,1)) as s2
   on true;
-        QUERY PLAN        
---------------------------
+                 QUERY PLAN                 
+--------------------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Join on t1, s1, t2, t3, t4, t5
+(3 rows)
 
 rollback;
 --
@@ -5975,14 +5985,15 @@ from int4_tbl as t1
                         inner join int8_tbl as t7 on null)
                on t5.q1 = t7.q2)
     on false;
-           QUERY PLAN           
---------------------------------
+                    QUERY PLAN                    
+--------------------------------------------------
  Nested Loop Left Join
    Join Filter: false
    ->  Seq Scan on int4_tbl t1
    ->  Result
          One-Time Filter: false
-(5 rows)
+         Replaces: Join on t2, t3, t4, t5, t7, t6
+(6 rows)
 
 -- variant with Var rather than PHV coming from t6
 explain (costs off)
@@ -5997,14 +6008,15 @@ from int4_tbl as t1
                         inner join int8_tbl as t7 on null)
                on t5.q1 = t7.q2)
     on false;
-           QUERY PLAN           
---------------------------------
+                    QUERY PLAN                    
+--------------------------------------------------
  Nested Loop Left Join
    Join Filter: false
    ->  Seq Scan on int4_tbl t1
    ->  Result
          One-Time Filter: false
-(5 rows)
+         Replaces: Join on t2, t3, t4, t5, t7, t6
+(6 rows)
 
 -- per further discussion of bug #17781
 explain (costs off)
@@ -6054,15 +6066,16 @@ select * from int8_tbl t1 left join
   (int8_tbl t2 inner join int8_tbl t3 on false
    left join int8_tbl t4 on t2.q2 = t4.q2)
 on t1.q1 = t2.q1;
-              QUERY PLAN              
---------------------------------------
+                 QUERY PLAN                 
+--------------------------------------------
  Hash Left Join
    Hash Cond: (t1.q1 = q1)
    ->  Seq Scan on int8_tbl t1
    ->  Hash
          ->  Result
                One-Time Filter: false
-(6 rows)
+               Replaces: Join on t2, t3, t4
+(7 rows)
 
 -- deduce constant-false from an EquivalenceClass
 explain (costs off)
@@ -6070,15 +6083,16 @@ select * from int8_tbl t1 left join
   (int8_tbl t2 inner join int8_tbl t3 on (t2.q1-t3.q2) = 0 and (t2.q1-t3.q2) = 1
    left join int8_tbl t4 on t2.q2 = t4.q2)
 on t1.q1 = t2.q1;
-              QUERY PLAN              
---------------------------------------
+                 QUERY PLAN                 
+--------------------------------------------
  Hash Left Join
    Hash Cond: (t1.q1 = q1)
    ->  Seq Scan on int8_tbl t1
    ->  Hash
          ->  Result
                One-Time Filter: false
-(6 rows)
+               Replaces: Join on t2, t3, t4
+(7 rows)
 
 -- pseudoconstant based on an outer-level Param
 explain (costs off)
@@ -6318,7 +6332,8 @@ select p.* from
 --------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on p
+(3 rows)
 
 select p.* from
   (parent p left join child c on (p.k = c.k)) join parent x on p.k = x.k
@@ -6335,7 +6350,8 @@ select p.* from
 --------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Join on p, x
+(3 rows)
 
 -- bug 5255: this is not optimizable by join removal
 begin;
@@ -6408,7 +6424,8 @@ SELECT q2 FROM
    ->  Result
          Output: q2, 'constant'::text
          One-Time Filter: false
-(9 rows)
+         Replaces: Scan on int8_tbl
+(10 rows)
 
 -- join removal bug #17786: check that OR conditions are cleaned up
 EXPLAIN (COSTS OFF)
@@ -6428,7 +6445,8 @@ FROM int4_tbl
                ->  Seq Scan on tenk1
                ->  Result
                      One-Time Filter: false
-(9 rows)
+                     Replaces: Scan on int8_tbl
+(10 rows)
 
 rollback;
 -- another join removal bug: we must clean up correctly when removing a PHV
@@ -6879,7 +6897,8 @@ where q1.x = q2.y;
 --------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on sj_1
+(3 rows)
 
 -- We can't use a cross-EC generated self join qual because of current logic of
 -- the generate_join_implied_equalities routine.
@@ -7703,11 +7722,12 @@ select 1 from emp1 full join
         on true
     where false) s on true
 where false;
-        QUERY PLAN        
---------------------------
+            QUERY PLAN            
+----------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Join on emp1, t1, t3
+(3 rows)
 
 select 1 from emp1 full join
     (select * from emp1 t1 join
@@ -7937,7 +7957,8 @@ where false;
  Result
    Output: 1
    One-Time Filter: false
-(3 rows)
+   Replaces: Scan on ss
+(4 rows)
 
 --
 -- Test LATERAL
@@ -8876,7 +8897,8 @@ select * from int8_tbl i8 left join lateral
    ->  Result
          Output: f1, i8.q2
          One-Time Filter: false
-(8 rows)
+         Replaces: Scan on int4_tbl
+(9 rows)
 
 explain (verbose, costs off)
 select * from int8_tbl i8 left join lateral
@@ -8890,7 +8912,8 @@ select * from int8_tbl i8 left join lateral
    ->  Result
          Output: f1, f1, i8.q2
          One-Time Filter: false
-(7 rows)
+         Replaces: Join on i1, i2
+(8 rows)
 
 -- check handling of nested appendrels inside LATERAL
 select * from
diff --git a/src/test/regress/expected/merge.out b/src/test/regress/expected/merge.out
index cf2219df754..c748172e98f 100644
--- a/src/test/regress/expected/merge.out
+++ b/src/test/regress/expected/merge.out
@@ -2427,7 +2427,8 @@ MERGE INTO pa_target t USING pa_source s ON t.tid = s.sid
                ->  Result
                      Output: t.tid, t.ctid
                      One-Time Filter: false
-(12 rows)
+                     Replaces: Scan on t
+(13 rows)
 
 MERGE INTO pa_target t USING pa_source s ON t.tid = s.sid
   WHEN NOT MATCHED THEN INSERT VALUES (s.sid);
diff --git a/src/test/regress/expected/partition_aggregate.out b/src/test/regress/expected/partition_aggregate.out
index 5f2c0cf5786..80b002fbdcf 100644
--- a/src/test/regress/expected/partition_aggregate.out
+++ b/src/test/regress/expected/partition_aggregate.out
@@ -146,13 +146,14 @@ SELECT c, a, count(*) FROM pagg_tab GROUP BY a, c;
 -- Test when input relation for grouping is dummy
 EXPLAIN (COSTS OFF)
 SELECT c, sum(a) FROM pagg_tab WHERE 1 = 2 GROUP BY c;
-           QUERY PLAN           
---------------------------------
+             QUERY PLAN             
+------------------------------------
  HashAggregate
    Group Key: c
    ->  Result
          One-Time Filter: false
-(4 rows)
+         Replaces: Scan on pagg_tab
+(5 rows)
 
 SELECT c, sum(a) FROM pagg_tab WHERE 1 = 2 GROUP BY c;
  c | sum 
@@ -161,12 +162,13 @@ SELECT c, sum(a) FROM pagg_tab WHERE 1 = 2 GROUP BY c;
 
 EXPLAIN (COSTS OFF)
 SELECT c, sum(a) FROM pagg_tab WHERE c = 'x' GROUP BY c;
-           QUERY PLAN           
---------------------------------
+             QUERY PLAN             
+------------------------------------
  GroupAggregate
    ->  Result
          One-Time Filter: false
-(3 rows)
+         Replaces: Scan on pagg_tab
+(4 rows)
 
 SELECT c, sum(a) FROM pagg_tab WHERE c = 'x' GROUP BY c;
  c | sum 
@@ -804,15 +806,16 @@ SELECT a.x, b.y, count(*) FROM (SELECT * FROM pagg_tab1 WHERE x < 20) a FULL JOI
 -- Empty join relation because of empty outer side, no partitionwise agg plan
 EXPLAIN (COSTS OFF)
 SELECT a.x, a.y, count(*) FROM (SELECT * FROM pagg_tab1 WHERE x = 1 AND x = 2) a LEFT JOIN pagg_tab2 b ON a.x = b.y GROUP BY a.x, a.y ORDER BY 1, 2;
-              QUERY PLAN              
---------------------------------------
+                  QUERY PLAN                  
+----------------------------------------------
  GroupAggregate
    Group Key: pagg_tab1.y
    ->  Sort
          Sort Key: pagg_tab1.y
          ->  Result
                One-Time Filter: false
-(6 rows)
+               Replaces: Join on b, pagg_tab1
+(7 rows)
 
 SELECT a.x, a.y, count(*) FROM (SELECT * FROM pagg_tab1 WHERE x = 1 AND x = 2) a LEFT JOIN pagg_tab2 b ON a.x = b.y GROUP BY a.x, a.y ORDER BY 1, 2;
  x | y | count 
diff --git a/src/test/regress/expected/partition_join.out b/src/test/regress/expected/partition_join.out
index 24e06845f92..0c495907eca 100644
--- a/src/test/regress/expected/partition_join.out
+++ b/src/test/regress/expected/partition_join.out
@@ -1609,19 +1609,21 @@ SELECT avg(t1.a), avg(t2.b), avg(t3.a + t3.b), t1.c, t2.c, t3.c FROM plt1 t1, pl
 -- joins where one of the relations is proven empty
 EXPLAIN (COSTS OFF)
 SELECT t1.a, t1.c, t2.b, t2.c FROM prt1 t1, prt2 t2 WHERE t1.a = t2.b AND t1.a = 1 AND t1.a = 2;
-        QUERY PLAN        
---------------------------
+         QUERY PLAN         
+----------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Join on t1, t2
+(3 rows)
 
 EXPLAIN (COSTS OFF)
 SELECT t1.a, t1.c, t2.b, t2.c FROM (SELECT * FROM prt1 WHERE a = 1 AND a = 2) t1 LEFT JOIN prt2 t2 ON t1.a = t2.b;
-        QUERY PLAN        
---------------------------
+          QUERY PLAN          
+------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Join on t2, prt1
+(3 rows)
 
 EXPLAIN (COSTS OFF)
 SELECT t1.a, t1.c, t2.b, t2.c FROM (SELECT * FROM prt1 WHERE a = 1 AND a = 2) t1 RIGHT JOIN prt2 t2 ON t1.a = t2.b, prt1 t3 WHERE t2.b = t3.a;
@@ -1648,7 +1650,8 @@ SELECT t1.a, t1.c, t2.b, t2.c FROM (SELECT * FROM prt1 WHERE a = 1 AND a = 2) t1
    ->  Hash
          ->  Result
                One-Time Filter: false
-(21 rows)
+               Replaces: Scan on prt1
+(22 rows)
 
 EXPLAIN (COSTS OFF)
 SELECT t1.a, t1.c, t2.b, t2.c FROM (SELECT * FROM prt1 WHERE a = 1 AND a = 2) t1 FULL JOIN prt2 t2 ON t1.a = t2.b WHERE t2.a = 0 ORDER BY t1.a, t2.b;
@@ -1668,7 +1671,8 @@ SELECT t1.a, t1.c, t2.b, t2.c FROM (SELECT * FROM prt1 WHERE a = 1 AND a = 2) t1
          ->  Hash
                ->  Result
                      One-Time Filter: false
-(14 rows)
+                     Replaces: Scan on prt1
+(15 rows)
 
 --
 -- tests for hash partitioned tables.
@@ -2255,7 +2259,8 @@ SELECT t1.a, t1.c, t2.b, t2.c FROM (SELECT * FROM prt1_l WHERE a = 1 AND a = 2)
    ->  Hash
          ->  Result
                One-Time Filter: false
-(11 rows)
+               Replaces: Scan on prt1_l
+(12 rows)
 
 -- Test case to verify proper handling of subqueries in a partitioned delete.
 -- The weird-looking lateral join is just there to force creation of a
diff --git a/src/test/regress/expected/partition_prune.out b/src/test/regress/expected/partition_prune.out
index 68ecd951809..cdd712fe51c 100644
--- a/src/test/regress/expected/partition_prune.out
+++ b/src/test/regress/expected/partition_prune.out
@@ -628,7 +628,8 @@ explain (costs off) select * from rlp3 where a = 20;   /* empty */
 --------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on rlp3
+(3 rows)
 
 -- redundant clauses are eliminated
 explain (costs off) select * from rlp where a > 1 and a = 10;	/* only default */
@@ -671,7 +672,8 @@ explain (costs off) select * from rlp where a = 1 and a = 3;	/* empty */
 --------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on rlp
+(3 rows)
 
 explain (costs off) select * from rlp where (a = 1 and a = 3) or (a > 1 and a = 15);
                             QUERY PLAN                             
@@ -1254,25 +1256,28 @@ select * from boolpart where a is not unknown;
 
 -- check that all partitions are pruned when faced with conflicting clauses
 explain (costs off) select * from boolpart where a is not unknown and a is unknown;
-        QUERY PLAN        
---------------------------
+          QUERY PLAN          
+------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on boolpart
+(3 rows)
 
 explain (costs off) select * from boolpart where a is false and a is unknown;
-        QUERY PLAN        
---------------------------
+          QUERY PLAN          
+------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on boolpart
+(3 rows)
 
 explain (costs off) select * from boolpart where a is true and a is unknown;
-        QUERY PLAN        
---------------------------
+          QUERY PLAN          
+------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on boolpart
+(3 rows)
 
 -- inverse boolean partitioning - a seemingly unlikely design, but we've got
 -- code for it, so we'd better test it.
@@ -1568,11 +1573,12 @@ explain (costs off) select * from coercepart where a = any ('{ab,null}');
 (2 rows)
 
 explain (costs off) select * from coercepart where a = any (null::text[]);
-        QUERY PLAN        
---------------------------
+           QUERY PLAN           
+--------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on coercepart
+(3 rows)
 
 explain (costs off) select * from coercepart where a = all ('{ab}');
                   QUERY PLAN                  
@@ -1582,25 +1588,28 @@ explain (costs off) select * from coercepart where a = all ('{ab}');
 (2 rows)
 
 explain (costs off) select * from coercepart where a = all ('{ab,bc}');
-        QUERY PLAN        
---------------------------
+           QUERY PLAN           
+--------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on coercepart
+(3 rows)
 
 explain (costs off) select * from coercepart where a = all ('{ab,null}');
-        QUERY PLAN        
---------------------------
+           QUERY PLAN           
+--------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on coercepart
+(3 rows)
 
 explain (costs off) select * from coercepart where a = all (null::text[]);
-        QUERY PLAN        
---------------------------
+           QUERY PLAN           
+--------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on coercepart
+(3 rows)
 
 drop table coercepart;
 CREATE TABLE part (a INT, b INT) PARTITION BY LIST (a);
@@ -1773,7 +1782,8 @@ explain (costs off) select * from lp where a <> 'a' and a is null;
 --------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on lp
+(3 rows)
 
 explain (costs off) select * from lp where (a <> 'a' and a <> 'd') or a is null;
                                   QUERY PLAN                                  
@@ -1866,22 +1876,24 @@ create table lparted_by_int2 (a smallint) partition by list (a);
 create table lparted_by_int2_1 partition of lparted_by_int2 for values in (1);
 create table lparted_by_int2_16384 partition of lparted_by_int2 for values in (16384);
 explain (costs off) select * from lparted_by_int2 where a = 100_000_000_000_000;
-        QUERY PLAN        
---------------------------
+             QUERY PLAN              
+-------------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on lparted_by_int2
+(3 rows)
 
 create table rparted_by_int2 (a smallint) partition by range (a);
 create table rparted_by_int2_1 partition of rparted_by_int2 for values from (1) to (10);
 create table rparted_by_int2_16384 partition of rparted_by_int2 for values from (10) to (16384);
 -- all partitions pruned
 explain (costs off) select * from rparted_by_int2 where a > 100_000_000_000_000;
-        QUERY PLAN        
---------------------------
+             QUERY PLAN              
+-------------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on rparted_by_int2
+(3 rows)
 
 create table rparted_by_int2_maxvalue partition of rparted_by_int2 for values from (16384) to (maxvalue);
 -- all partitions but rparted_by_int2_maxvalue pruned
@@ -2132,7 +2144,8 @@ explain (costs off) select * from hp where a = 1 and b = 'abcde' and
 --------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on hp
+(3 rows)
 
 --
 -- Test runtime partition pruning
@@ -3390,11 +3403,12 @@ select * from stable_qual_pruning where a < '2000-02-01'::timestamptz;
 explain (analyze, costs off, summary off, timing off, buffers off)
 select * from stable_qual_pruning
   where a = any(array['2010-02-01', '2020-01-01']::timestamp[]);
-            QUERY PLAN             
------------------------------------
+               QUERY PLAN                
+-----------------------------------------
  Result (actual rows=0.00 loops=1)
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on stable_qual_pruning
+(3 rows)
 
 explain (analyze, costs off, summary off, timing off, buffers off)
 select * from stable_qual_pruning
@@ -3642,6 +3656,7 @@ explain (analyze, costs off, summary off, timing off, buffers off) select * from
    Sort Key: ma_test.b
    InitPlan 2
      ->  Result (actual rows=1.00 loops=1)
+           Replaces: MinMaxAggregate
            InitPlan 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)
@@ -3656,7 +3671,7 @@ explain (analyze, costs off, summary off, timing off, buffers off) select * from
    ->  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)
          Index Searches: 1
-(18 rows)
+(19 rows)
 
 reset enable_seqscan;
 reset enable_sort;
@@ -3678,11 +3693,12 @@ explain (costs off) select * from pp_arrpart where a = '{1}';
 (2 rows)
 
 explain (costs off) select * from pp_arrpart where a = '{1, 2}';
-        QUERY PLAN        
---------------------------
+           QUERY PLAN           
+--------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on pp_arrpart
+(3 rows)
 
 explain (costs off) select * from pp_arrpart where a in ('{4, 5}', '{1}');
                               QUERY PLAN                              
@@ -3764,11 +3780,12 @@ explain (costs off) select * from pp_enumpart where a = 'blue';
 (2 rows)
 
 explain (costs off) select * from pp_enumpart where a = 'black';
-        QUERY PLAN        
---------------------------
+           QUERY PLAN            
+---------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on pp_enumpart
+(3 rows)
 
 drop table pp_enumpart;
 drop type pp_colors;
@@ -3785,11 +3802,12 @@ explain (costs off) select * from pp_recpart where a = '(1,1)'::pp_rectype;
 (2 rows)
 
 explain (costs off) select * from pp_recpart where a = '(1,2)'::pp_rectype;
-        QUERY PLAN        
---------------------------
+           QUERY PLAN           
+--------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on pp_recpart
+(3 rows)
 
 drop table pp_recpart;
 drop type pp_rectype;
@@ -3805,11 +3823,12 @@ explain (costs off) select * from pp_intrangepart where a = '[1,2]'::int4range;
 (2 rows)
 
 explain (costs off) select * from pp_intrangepart where a = '(1,2)'::int4range;
-        QUERY PLAN        
---------------------------
+             QUERY PLAN              
+-------------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on pp_intrangepart
+(3 rows)
 
 drop table pp_intrangepart;
 --
@@ -4125,19 +4144,21 @@ explain (costs off) update listp1 set a = 1 where a = 2;
 -- constraint exclusion enabled
 set constraint_exclusion to 'on';
 explain (costs off) select * from listp1 where a = 2;
-        QUERY PLAN        
---------------------------
+         QUERY PLAN         
+----------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on listp1
+(3 rows)
 
 explain (costs off) update listp1 set a = 1 where a = 2;
-           QUERY PLAN           
---------------------------------
+            QUERY PLAN            
+----------------------------------
  Update on listp1
    ->  Result
          One-Time Filter: false
-(3 rows)
+         Replaces: Scan on listp1
+(4 rows)
 
 reset constraint_exclusion;
 reset enable_partition_pruning;
@@ -4524,18 +4545,20 @@ create table hp_contradict_test (a int, b int) partition by hash (a part_test_in
 create table hp_contradict_test_p1 partition of hp_contradict_test for values with (modulus 2, remainder 0);
 create table hp_contradict_test_p2 partition of hp_contradict_test for values with (modulus 2, remainder 1);
 explain (costs off) select * from hp_contradict_test where a is null and a === 1 and b === 1;
-        QUERY PLAN        
---------------------------
+               QUERY PLAN               
+----------------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on hp_contradict_test
+(3 rows)
 
 explain (costs off) select * from hp_contradict_test where a === 1 and b === 1 and a is null;
-        QUERY PLAN        
---------------------------
+               QUERY PLAN               
+----------------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on hp_contradict_test
+(3 rows)
 
 drop table hp_contradict_test;
 drop operator class part_test_int4_ops2 using hash;
diff --git a/src/test/regress/expected/predicate.out b/src/test/regress/expected/predicate.out
index 1aff0b59ff8..cd78cf11f51 100644
--- a/src/test/regress/expected/predicate.out
+++ b/src/test/regress/expected/predicate.out
@@ -37,7 +37,8 @@ SELECT * FROM pred_tab t WHERE t.a IS NULL;
 --------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on t
+(3 rows)
 
 -- Ensure the IS_NULL qual is not reduced to constant-FALSE on nullable
 -- columns
@@ -78,7 +79,8 @@ SELECT * FROM pred_tab t WHERE t.a IS NULL OR t.c IS NULL;
 --------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on t
+(3 rows)
 
 -- Ensure the OR clause is not reduced to constant-FALSE when not all branches
 -- are provably false
@@ -140,7 +142,8 @@ SELECT * FROM pred_tab t1
    ->  Seq Scan on pred_tab t1
    ->  Result
          One-Time Filter: false
-(5 rows)
+         Replaces: Scan on t2
+(6 rows)
 
 -- Ensure the IS_NULL qual is not reduced to constant-FALSE when the column is
 -- nullable by an outer join
@@ -210,7 +213,8 @@ SELECT * FROM pred_tab t1
    ->  Seq Scan on pred_tab t1
    ->  Result
          One-Time Filter: false
-(5 rows)
+         Replaces: Scan on t2
+(6 rows)
 
 -- Ensure the OR clause is not reduced to constant-FALSE when a column is
 -- made nullable from an outer join
@@ -267,17 +271,18 @@ SELECT * FROM pred_tab t1
     LEFT JOIN pred_tab t2 ON EXISTS
         (SELECT 1 FROM pred_tab t3, pred_tab t4, pred_tab t5, pred_tab t6
          WHERE t1.a = t3.a AND t6.a IS NULL);
-             QUERY PLAN              
--------------------------------------
+                 QUERY PLAN                 
+--------------------------------------------
  Nested Loop Left Join
    Join Filter: (InitPlan 1).col1
    InitPlan 1
      ->  Result
            One-Time Filter: false
+           Replaces: Join on t3, t4, t5, t6
    ->  Seq Scan on pred_tab t1
    ->  Materialize
          ->  Seq Scan on pred_tab t2
-(8 rows)
+(9 rows)
 
 DROP TABLE pred_tab;
 -- Validate we handle IS NULL and IS NOT NULL quals correctly with inheritance
@@ -418,20 +423,22 @@ SET constraint_exclusion TO ON;
 -- Ensure that we get a dummy plan
 EXPLAIN (COSTS OFF)
 SELECT * FROM pred_tab1, pred_tab2 WHERE pred_tab2.a IS NULL;
-        QUERY PLAN        
---------------------------
+                QUERY PLAN                
+------------------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Join on pred_tab1, pred_tab2
+(3 rows)
 
 -- Ensure that we get a dummy plan
 EXPLAIN (COSTS OFF)
 SELECT * FROM pred_tab2, pred_tab1 WHERE pred_tab1.a IS NULL OR pred_tab1.b < 2;
-        QUERY PLAN        
---------------------------
+                QUERY PLAN                
+------------------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Join on pred_tab2, pred_tab1
+(3 rows)
 
 RESET constraint_exclusion;
 DROP TABLE pred_tab1;
diff --git a/src/test/regress/expected/rowsecurity.out b/src/test/regress/expected/rowsecurity.out
index 8c879509313..e7fe3317638 100644
--- a/src/test/regress/expected/rowsecurity.out
+++ b/src/test/regress/expected/rowsecurity.out
@@ -3607,7 +3607,8 @@ EXPLAIN (COSTS OFF) SELECT * FROM t1;
 --------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on t1
+(3 rows)
 
 SET SESSION AUTHORIZATION regress_rls_bob;
 SELECT * FROM t1;
@@ -3620,7 +3621,8 @@ EXPLAIN (COSTS OFF) SELECT * FROM t1;
 --------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on t1
+(3 rows)
 
 --
 -- COPY TO/FROM
@@ -4524,11 +4526,12 @@ SELECT * FROM rls_tbl WHERE a <<< 1000;
 (0 rows)
 
 EXPLAIN (COSTS OFF) SELECT * FROM rls_tbl WHERE a <<< 1000 or a <<< 900;
-        QUERY PLAN        
---------------------------
+         QUERY PLAN          
+-----------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on rls_tbl
+(3 rows)
 
 RESET SESSION AUTHORIZATION;
 CREATE TABLE rls_child_tbl () INHERITS (rls_tbl);
diff --git a/src/test/regress/expected/rowtypes.out b/src/test/regress/expected/rowtypes.out
index 9168979a620..328b1e142c0 100644
--- a/src/test/regress/expected/rowtypes.out
+++ b/src/test/regress/expected/rowtypes.out
@@ -1324,7 +1324,8 @@ where false;
  Result
    Output: (a).f1, (a).f2
    One-Time Filter: false
-(3 rows)
+   Replaces: Scan on ss
+(4 rows)
 
 explain (verbose, costs off)
 with cte(c) as materialized (select row(1, 2)),
@@ -1349,10 +1350,11 @@ where false;
  Result
    Output: (cte.c).f1
    One-Time Filter: false
+   Replaces: Scan on cte
    CTE cte
      ->  Result
            Output: '(1,2)'::record
-(6 rows)
+(7 rows)
 
 --
 -- Tests for component access / FieldSelect
diff --git a/src/test/regress/expected/select.out b/src/test/regress/expected/select.out
index bab0cc93ff5..d1736dac224 100644
--- a/src/test/regress/expected/select.out
+++ b/src/test/regress/expected/select.out
@@ -962,10 +962,11 @@ create table list_parted_tbl (a int,b int) partition by list (a);
 create table list_parted_tbl1 partition of list_parted_tbl
   for values in (1) partition by list(b);
 explain (costs off) select * from list_parted_tbl;
-        QUERY PLAN        
---------------------------
+             QUERY PLAN              
+-------------------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on list_parted_tbl
+(3 rows)
 
 drop table list_parted_tbl;
diff --git a/src/test/regress/expected/subselect.out b/src/test/regress/expected/subselect.out
index 7a1c216a0b1..4a12a0adbbe 100644
--- a/src/test/regress/expected/subselect.out
+++ b/src/test/regress/expected/subselect.out
@@ -2870,12 +2870,13 @@ ON B.hundred in (SELECT min(c.hundred) FROM tenk2 C WHERE c.odd = b.odd);
                      ->  Subquery Scan on unnamed_subquery
                            Filter: (b.hundred = unnamed_subquery.min)
                            ->  Result
+                                 Replaces: MinMaxAggregate
                                  InitPlan 1
                                    ->  Limit
                                          ->  Index Scan using tenk2_hundred on tenk2 c
                                                Index Cond: (hundred IS NOT NULL)
                                                Filter: (odd = b.odd)
-(16 rows)
+(17 rows)
 
 --
 -- Test VALUES to ARRAY (VtA) transformation
@@ -3039,7 +3040,8 @@ EXPLAIN (COSTS OFF) EXECUTE test(NULL, 3.14, NULL);
 --------------------------
  Result
    One-Time Filter: false
-(2 rows)
+   Replaces: Scan on onek
+(3 rows)
 
 EXPLAIN (COSTS OFF) EXECUTE test(NULL, 3.14, '-1.5');
                                                                      QUERY PLAN                                                                      
diff --git a/src/test/regress/expected/tsrf.out b/src/test/regress/expected/tsrf.out
index d47b5f6ec57..e73e68af971 100644
--- a/src/test/regress/expected/tsrf.out
+++ b/src/test/regress/expected/tsrf.out
@@ -92,7 +92,8 @@ SELECT unnest(ARRAY[1, 2]) FROM few WHERE false;
    Output: unnest('{1,2}'::integer[])
    ->  Result
          One-Time Filter: false
-(4 rows)
+         Replaces: Scan on few
+(5 rows)
 
 SELECT unnest(ARRAY[1, 2]) FROM few WHERE false;
  unnest 
@@ -108,7 +109,8 @@ SELECT * FROM few f1,
  Result
    Output: f1.id, f1.dataa, f1.datab, ss.unnest
    One-Time Filter: false
-(3 rows)
+   Replaces: Join on f1, ss
+(4 rows)
 
 SELECT * FROM few f1,
   (SELECT unnest(ARRAY[1,2]) FROM few f2 WHERE false OFFSET 0) ss;
-- 
2.39.5 (Apple Git-154)

v5-0002-Consider-a-Result-node-s-relids-in-ExplainPreScan.patchapplication/octet-stream; name=v5-0002-Consider-a-Result-node-s-relids-in-ExplainPreScan.patchDownload
From a1309619a40ea839c8c78d99ba7ea21603cd05e0 Mon Sep 17 00:00:00 2001
From: Robert Haas <rhaas@postgresql.org>
Date: Mon, 21 Apr 2025 13:35:28 -0400
Subject: [PATCH v5 2/3] Consider a Result node's relids in ExplainPreScanNode.

Now that a Result node has a relids set, add the relids that it
carries the set accumulated by ExplainPreScanNode so that we
generate unique relation aliases for all of the referenced relations
when it calls select_rtable_names_for_explain. The effect of this
changes is that a few things get schema-qualified in the regression
test outputs that previously were not. In similar cases not involving
a Result node, we were already schema-qualifying, so this appears to
be an improvement.

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.
---
 src/backend/commands/explain.c                |  4 +++
 .../regress/expected/generated_virtual.out    |  8 ++---
 src/test/regress/expected/join.out            | 32 +++++++++----------
 src/test/regress/expected/partition_join.out  | 12 +++----
 4 files changed, 30 insertions(+), 26 deletions(-)

diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index affa5c8fa8a..b56ff332e53 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -1230,6 +1230,10 @@ ExplainPreScanNode(PlanState *planstate, Bitmapset **rels_used)
 			*rels_used = bms_add_members(*rels_used,
 										 ((MergeAppend *) plan)->apprelids);
 			break;
+		case T_Result:
+			*rels_used = bms_add_members(*rels_used,
+										 ((Result *) plan)->relids);
+			break;
 		default:
 			break;
 	}
diff --git a/src/test/regress/expected/generated_virtual.out b/src/test/regress/expected/generated_virtual.out
index 8b25e77bb4d..70727623bca 100644
--- a/src/test/regress/expected/generated_virtual.out
+++ b/src/test/regress/expected/generated_virtual.out
@@ -1570,15 +1570,15 @@ where coalesce(t2.b, 1) = 2 or t1.a is null;
 -- Ensure that the generation expressions are wrapped into PHVs if needed
 explain (verbose, costs off)
 select t2.* from gtest32 t1 left join gtest32 t2 on false;
-                      QUERY PLAN                      
-------------------------------------------------------
+                          QUERY PLAN                           
+---------------------------------------------------------------
  Nested Loop Left Join
-   Output: a, (a * 2), (20), (COALESCE(a, 100)), e
+   Output: t2.a, (t2.a * 2), (20), (COALESCE(t2.a, 100)), t2.e
    Join Filter: false
    ->  Seq Scan on generated_virtual_tests.gtest32 t1
          Output: t1.a, t1.b, t1.c, t1.d, t1.e
    ->  Result
-         Output: a, e, 20, COALESCE(a, 100)
+         Output: t2.a, t2.e, 20, COALESCE(t2.a, 100)
          One-Time Filter: false
          Replaces: Scan on t2
 (9 rows)
diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out
index 88af557a45c..2d33d31c6d2 100644
--- a/src/test/regress/expected/join.out
+++ b/src/test/regress/expected/join.out
@@ -4151,9 +4151,9 @@ select * from t t1
                         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)
@@ -4166,7 +4166,7 @@ select * from t t1
                      ->  Seq Scan on pg_temp.t t2
                            Output: t2.i2, 1
          ->  Result
-               Output: i3
+               Output: t3.i3
                One-Time Filter: false
                Replaces: Scan on t3
    ->  Memoize
@@ -6069,7 +6069,7 @@ on t1.q1 = t2.q1;
                  QUERY PLAN                 
 --------------------------------------------
  Hash Left Join
-   Hash Cond: (t1.q1 = q1)
+   Hash Cond: (t1.q1 = t2.q1)
    ->  Seq Scan on int8_tbl t1
    ->  Hash
          ->  Result
@@ -6086,7 +6086,7 @@ on t1.q1 = t2.q1;
                  QUERY PLAN                 
 --------------------------------------------
  Hash Left Join
-   Hash Cond: (t1.q1 = q1)
+   Hash Cond: (t1.q1 = t2.q1)
    ->  Seq Scan on int8_tbl t1
    ->  Hash
          ->  Result
@@ -6416,13 +6416,13 @@ SELECT q2 FROM
                       QUERY PLAN                      
 ------------------------------------------------------
  Nested Loop Left Join
-   Output: q2
+   Output: int8_tbl.q2
    Join Filter: NULL::boolean
    Filter: (('constant'::text) >= ('constant'::text))
    ->  Seq Scan on public.int4_tbl
          Output: int4_tbl.f1
    ->  Result
-         Output: q2, 'constant'::text
+         Output: int8_tbl.q2, 'constant'::text
          One-Time Filter: false
          Replaces: Scan on int8_tbl
 (10 rows)
@@ -6897,7 +6897,7 @@ where q1.x = q2.y;
 --------------------------
  Result
    One-Time Filter: false
-   Replaces: Scan on sj_1
+   Replaces: Scan on sj
 (3 rows)
 
 -- We can't use a cross-EC generated self join qual because of current logic of
@@ -8887,15 +8887,15 @@ select * from int4_tbl t1,
 explain (verbose, costs off)
 select * from int8_tbl i8 left join lateral
   (select *, i8.q2 from int4_tbl where false) ss on true;
-              QUERY PLAN              
---------------------------------------
+                  QUERY PLAN                  
+----------------------------------------------
  Nested Loop Left Join
-   Output: i8.q1, i8.q2, f1, (i8.q2)
+   Output: i8.q1, i8.q2, int4_tbl.f1, (i8.q2)
    Join Filter: false
    ->  Seq Scan on public.int8_tbl i8
          Output: i8.q1, i8.q2
    ->  Result
-         Output: f1, i8.q2
+         Output: int4_tbl.f1, i8.q2
          One-Time Filter: false
          Replaces: Scan on int4_tbl
 (9 rows)
@@ -8903,14 +8903,14 @@ select * from int8_tbl i8 left join lateral
 explain (verbose, costs off)
 select * from int8_tbl i8 left join lateral
   (select *, i8.q2 from int4_tbl i1, int4_tbl i2 where false) ss on true;
-               QUERY PLAN                
------------------------------------------
+                  QUERY PLAN                   
+-----------------------------------------------
  Nested Loop Left Join
-   Output: i8.q1, i8.q2, f1, f1, (i8.q2)
+   Output: i8.q1, i8.q2, i1.f1, i2.f1, (i8.q2)
    ->  Seq Scan on public.int8_tbl i8
          Output: i8.q1, i8.q2
    ->  Result
-         Output: f1, f1, i8.q2
+         Output: i1.f1, i2.f1, i8.q2
          One-Time Filter: false
          Replaces: Join on i1, i2
 (8 rows)
diff --git a/src/test/regress/expected/partition_join.out b/src/test/regress/expected/partition_join.out
index 0c495907eca..c94108eb118 100644
--- a/src/test/regress/expected/partition_join.out
+++ b/src/test/regress/expected/partition_join.out
@@ -1630,7 +1630,7 @@ SELECT t1.a, t1.c, t2.b, t2.c FROM (SELECT * FROM prt1 WHERE a = 1 AND a = 2) t1
                     QUERY PLAN                    
 --------------------------------------------------
  Hash Left Join
-   Hash Cond: (t2.b = a)
+   Hash Cond: (t2.b = prt1.a)
    ->  Append
          ->  Hash Join
                Hash Cond: (t3_1.a = t2_1.b)
@@ -1658,9 +1658,9 @@ SELECT t1.a, t1.c, t2.b, t2.c FROM (SELECT * FROM prt1 WHERE a = 1 AND a = 2) t1
                  QUERY PLAN                 
 --------------------------------------------
  Sort
-   Sort Key: a, t2.b
+   Sort Key: prt1.a, t2.b
    ->  Hash Left Join
-         Hash Cond: (t2.b = a)
+         Hash Cond: (t2.b = prt1.a)
          ->  Append
                ->  Seq Scan on prt2_p1 t2_1
                      Filter: (a = 0)
@@ -2246,10 +2246,10 @@ SELECT COUNT(*) FROM prt1_l t1 LEFT JOIN LATERAL
 -- join with one side empty
 EXPLAIN (COSTS OFF)
 SELECT t1.a, t1.c, t2.b, t2.c FROM (SELECT * FROM prt1_l WHERE a = 1 AND a = 2) t1 RIGHT JOIN prt2_l t2 ON t1.a = t2.b AND t1.b = t2.a AND t1.c = t2.c;
-                               QUERY PLAN                                
--------------------------------------------------------------------------
+                                          QUERY PLAN                                          
+----------------------------------------------------------------------------------------------
  Hash Left Join
-   Hash Cond: ((t2.b = a) AND (t2.a = b) AND ((t2.c)::text = (c)::text))
+   Hash Cond: ((t2.b = prt1_l.a) AND (t2.a = prt1_l.b) AND ((t2.c)::text = (prt1_l.c)::text))
    ->  Append
          ->  Seq Scan on prt2_l_p1 t2_1
          ->  Seq Scan on prt2_l_p2_p1 t2_2
-- 
2.39.5 (Apple Git-154)

#35Tom Lane
tgl@sss.pgh.pa.us
In reply to: Robert Haas (#33)
Re: plan shape work

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

#36Robert Haas
robertmhaas@gmail.com
In reply to: Tom Lane (#35)
Re: plan shape work

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

#37Tom Lane
tgl@sss.pgh.pa.us
In reply to: Robert Haas (#36)
Re: plan shape work

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

#38Robert Haas
robertmhaas@gmail.com
In reply to: Tom Lane (#37)
Re: plan shape work

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

#39Tom Lane
tgl@sss.pgh.pa.us
In reply to: Robert Haas (#38)
Re: plan shape work

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

#40Robert Haas
robertmhaas@gmail.com
In reply to: Tom Lane (#39)
Re: plan shape work

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

#41Richard Guo
guofenglinux@gmail.com
In reply to: Tom Lane (#35)
Re: plan shape work

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

#42Richard Guo
guofenglinux@gmail.com
In reply to: Robert Haas (#38)
Re: plan shape work

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

#43Tom Lane
tgl@sss.pgh.pa.us
In reply to: Tom Lane (#39)
Re: plan shape work

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

#44Robert Haas
robertmhaas@gmail.com
In reply to: Tom Lane (#43)
Re: plan shape work

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

#45Tom Lane
tgl@sss.pgh.pa.us
In reply to: Robert Haas (#44)
Re: plan shape work

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

#46Robert Haas
robertmhaas@gmail.com
In reply to: Tom Lane (#45)
Re: plan shape work

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

#47Tom Lane
tgl@sss.pgh.pa.us
In reply to: Robert Haas (#46)
Re: plan shape work

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

#48Robert Haas
robertmhaas@gmail.com
In reply to: Tom Lane (#45)
3 attachment(s)
Re: plan shape work

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

Attachments:

v6-0002-Consider-a-Result-node-s-relids-in-ExplainPreScan.patchapplication/octet-stream; name=v6-0002-Consider-a-Result-node-s-relids-in-ExplainPreScan.patchDownload
From 1396e626b1dd768c59990321b20943648e8a933f Mon Sep 17 00:00:00 2001
From: Robert Haas <rhaas@postgresql.org>
Date: Tue, 16 Sep 2025 09:56:35 -0400
Subject: [PATCH v6 2/3] Consider a Result node's relids in ExplainPreScanNode.

Now that a Result node has a relids set, add the relids that it
carries the set accumulated by ExplainPreScanNode so that we
generate unique relation aliases for all of the referenced relations
when it calls select_rtable_names_for_explain. The effect of this
changes is that a few things get schema-qualified in the regression
test outputs that previously were not. In similar cases not involving
a Result node, we were already schema-qualifying, so this appears to
be an improvement.

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.
---
 src/backend/commands/explain.c                |  4 +++
 .../regress/expected/generated_virtual.out    |  8 ++---
 src/test/regress/expected/join.out            | 32 +++++++++----------
 src/test/regress/expected/partition_join.out  | 12 +++----
 src/test/regress/expected/subselect.out       |  8 ++---
 5 files changed, 34 insertions(+), 30 deletions(-)

diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 0ab1813728a..c5c958bbada 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -1230,6 +1230,10 @@ ExplainPreScanNode(PlanState *planstate, Bitmapset **rels_used)
 			*rels_used = bms_add_members(*rels_used,
 										 ((MergeAppend *) plan)->apprelids);
 			break;
+		case T_Result:
+			*rels_used = bms_add_members(*rels_used,
+										 ((Result *) plan)->relids);
+			break;
 		default:
 			break;
 	}
diff --git a/src/test/regress/expected/generated_virtual.out b/src/test/regress/expected/generated_virtual.out
index b48e6bdbf9d..c861bd36c5a 100644
--- a/src/test/regress/expected/generated_virtual.out
+++ b/src/test/regress/expected/generated_virtual.out
@@ -1570,15 +1570,15 @@ where coalesce(t2.b, 1) = 2 or t1.a is null;
 -- Ensure that the generation expressions are wrapped into PHVs if needed
 explain (verbose, costs off)
 select t2.* from gtest32 t1 left join gtest32 t2 on false;
-                      QUERY PLAN                      
-------------------------------------------------------
+                          QUERY PLAN                           
+---------------------------------------------------------------
  Nested Loop Left Join
-   Output: a, (a * 2), (20), (COALESCE(a, 100)), e
+   Output: t2.a, (t2.a * 2), (20), (COALESCE(t2.a, 100)), t2.e
    Join Filter: false
    ->  Seq Scan on generated_virtual_tests.gtest32 t1
          Output: t1.a, t1.b, t1.c, t1.d, t1.e
    ->  Result
-         Output: a, e, 20, COALESCE(a, 100)
+         Output: t2.a, t2.e, 20, COALESCE(t2.a, 100)
          Replaces: Scan on t2
          One-Time Filter: false
 (9 rows)
diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out
index 30baa9dc51a..cd37f549b5a 100644
--- a/src/test/regress/expected/join.out
+++ b/src/test/regress/expected/join.out
@@ -4151,9 +4151,9 @@ select * from t t1
                         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)
@@ -4166,7 +4166,7 @@ select * from t t1
                      ->  Seq Scan on pg_temp.t t2
                            Output: t2.i2, 1
          ->  Result
-               Output: i3
+               Output: t3.i3
                Replaces: Scan on t3
                One-Time Filter: false
    ->  Memoize
@@ -6069,7 +6069,7 @@ on t1.q1 = t2.q1;
                  QUERY PLAN                 
 --------------------------------------------
  Hash Left Join
-   Hash Cond: (t1.q1 = q1)
+   Hash Cond: (t1.q1 = t2.q1)
    ->  Seq Scan on int8_tbl t1
    ->  Hash
          ->  Result
@@ -6086,7 +6086,7 @@ on t1.q1 = t2.q1;
                  QUERY PLAN                 
 --------------------------------------------
  Hash Left Join
-   Hash Cond: (t1.q1 = q1)
+   Hash Cond: (t1.q1 = t2.q1)
    ->  Seq Scan on int8_tbl t1
    ->  Hash
          ->  Result
@@ -6416,13 +6416,13 @@ SELECT q2 FROM
                       QUERY PLAN                      
 ------------------------------------------------------
  Nested Loop Left Join
-   Output: q2
+   Output: int8_tbl.q2
    Join Filter: NULL::boolean
    Filter: (('constant'::text) >= ('constant'::text))
    ->  Seq Scan on public.int4_tbl
          Output: int4_tbl.f1
    ->  Result
-         Output: q2, 'constant'::text
+         Output: int8_tbl.q2, 'constant'::text
          Replaces: Scan on int8_tbl
          One-Time Filter: false
 (10 rows)
@@ -6896,7 +6896,7 @@ where q1.x = q2.y;
         QUERY PLAN        
 --------------------------
  Result
-   Replaces: Scan on sj_1
+   Replaces: Scan on sj
    One-Time Filter: false
 (3 rows)
 
@@ -8887,15 +8887,15 @@ select * from int4_tbl t1,
 explain (verbose, costs off)
 select * from int8_tbl i8 left join lateral
   (select *, i8.q2 from int4_tbl where false) ss on true;
-              QUERY PLAN              
---------------------------------------
+                  QUERY PLAN                  
+----------------------------------------------
  Nested Loop Left Join
-   Output: i8.q1, i8.q2, f1, (i8.q2)
+   Output: i8.q1, i8.q2, int4_tbl.f1, (i8.q2)
    Join Filter: false
    ->  Seq Scan on public.int8_tbl i8
          Output: i8.q1, i8.q2
    ->  Result
-         Output: f1, i8.q2
+         Output: int4_tbl.f1, i8.q2
          Replaces: Scan on int4_tbl
          One-Time Filter: false
 (9 rows)
@@ -8903,14 +8903,14 @@ select * from int8_tbl i8 left join lateral
 explain (verbose, costs off)
 select * from int8_tbl i8 left join lateral
   (select *, i8.q2 from int4_tbl i1, int4_tbl i2 where false) ss on true;
-               QUERY PLAN                
------------------------------------------
+                  QUERY PLAN                   
+-----------------------------------------------
  Nested Loop Left Join
-   Output: i8.q1, i8.q2, f1, f1, (i8.q2)
+   Output: i8.q1, i8.q2, i1.f1, i2.f1, (i8.q2)
    ->  Seq Scan on public.int8_tbl i8
          Output: i8.q1, i8.q2
    ->  Result
-         Output: f1, f1, i8.q2
+         Output: i1.f1, i2.f1, i8.q2
          Replaces: Join on i1, i2
          One-Time Filter: false
 (8 rows)
diff --git a/src/test/regress/expected/partition_join.out b/src/test/regress/expected/partition_join.out
index 13fda3f32ef..713828be335 100644
--- a/src/test/regress/expected/partition_join.out
+++ b/src/test/regress/expected/partition_join.out
@@ -1630,7 +1630,7 @@ SELECT t1.a, t1.c, t2.b, t2.c FROM (SELECT * FROM prt1 WHERE a = 1 AND a = 2) t1
                     QUERY PLAN                    
 --------------------------------------------------
  Hash Left Join
-   Hash Cond: (t2.b = a)
+   Hash Cond: (t2.b = prt1.a)
    ->  Append
          ->  Hash Join
                Hash Cond: (t3_1.a = t2_1.b)
@@ -1658,9 +1658,9 @@ SELECT t1.a, t1.c, t2.b, t2.c FROM (SELECT * FROM prt1 WHERE a = 1 AND a = 2) t1
                  QUERY PLAN                 
 --------------------------------------------
  Sort
-   Sort Key: a, t2.b
+   Sort Key: prt1.a, t2.b
    ->  Hash Left Join
-         Hash Cond: (t2.b = a)
+         Hash Cond: (t2.b = prt1.a)
          ->  Append
                ->  Seq Scan on prt2_p1 t2_1
                      Filter: (a = 0)
@@ -2246,10 +2246,10 @@ SELECT COUNT(*) FROM prt1_l t1 LEFT JOIN LATERAL
 -- join with one side empty
 EXPLAIN (COSTS OFF)
 SELECT t1.a, t1.c, t2.b, t2.c FROM (SELECT * FROM prt1_l WHERE a = 1 AND a = 2) t1 RIGHT JOIN prt2_l t2 ON t1.a = t2.b AND t1.b = t2.a AND t1.c = t2.c;
-                               QUERY PLAN                                
--------------------------------------------------------------------------
+                                          QUERY PLAN                                          
+----------------------------------------------------------------------------------------------
  Hash Left Join
-   Hash Cond: ((t2.b = a) AND (t2.a = b) AND ((t2.c)::text = (c)::text))
+   Hash Cond: ((t2.b = prt1_l.a) AND (t2.a = prt1_l.b) AND ((t2.c)::text = (prt1_l.c)::text))
    ->  Append
          ->  Seq Scan on prt2_l_p1 t2_1
          ->  Seq Scan on prt2_l_p2_p1 t2_2
diff --git a/src/test/regress/expected/subselect.out b/src/test/regress/expected/subselect.out
index 065ae550918..0c64098f38d 100644
--- a/src/test/regress/expected/subselect.out
+++ b/src/test/regress/expected/subselect.out
@@ -1997,15 +1997,15 @@ create temp table json_tab (a int);
 insert into json_tab values (1);
 explain (verbose, costs off)
 select * from json_tab t1 left join (select json_array(1, a) from json_tab t2) s on false;
-                    QUERY PLAN                     
----------------------------------------------------
+                      QUERY PLAN                      
+------------------------------------------------------
  Nested Loop Left Join
-   Output: t1.a, (JSON_ARRAY(1, a RETURNING json))
+   Output: t1.a, (JSON_ARRAY(1, t2.a RETURNING json))
    Join Filter: false
    ->  Seq Scan on pg_temp.json_tab t1
          Output: t1.a
    ->  Result
-         Output: JSON_ARRAY(1, a RETURNING json)
+         Output: JSON_ARRAY(1, t2.a RETURNING json)
          Replaces: Scan on t2
          One-Time Filter: false
 (9 rows)
-- 
2.39.5 (Apple Git-154)

v6-0001-Keep-track-of-what-RTIs-a-Result-node-is-scanning.patchapplication/octet-stream; name=v6-0001-Keep-track-of-what-RTIs-a-Result-node-is-scanning.patchDownload
From cf39c9ca76e40b48abad55d1c51c6707265b58af Mon Sep 17 00:00:00 2001
From: Robert Haas <rhaas@postgresql.org>
Date: Tue, 16 Sep 2025 09:32:15 -0400
Subject: [PATCH v6 1/3] Keep track of what RTIs a Result node is scanning.

Result nodes now include an RTI set, which is only non-NULL when they
have no subplan, and is taken from the relid set of the RelOptInfo
that the Result is generating. They also now include a result_reason,
which tells us something about why the Result node was inserted.

Using that information, EXPLAIN now emits, where relevant, a "Replaces"
line describing the origin of the Result node.

Likewise, pg_overexplain's EXPLAIN (RANGE_TABLE) now displays the RTIs
stored in a Result node just as it already does for other RTI-bearing
node types.
---
 contrib/file_fdw/expected/file_fdw.out        |   1 +
 .../expected/pg_overexplain.out               |   6 +-
 contrib/pg_overexplain/pg_overexplain.c       |  12 ++
 .../postgres_fdw/expected/postgres_fdw.out    |   9 +-
 src/backend/commands/explain.c                |  98 ++++++++++++
 src/backend/optimizer/plan/createplan.c       | 117 ++++++++++-----
 src/backend/optimizer/plan/setrefs.c          |   2 +
 src/include/nodes/plannodes.h                 |  22 +++
 src/test/regress/expected/aggregates.out      |  51 ++++---
 src/test/regress/expected/case.out            |  21 +--
 src/test/regress/expected/explain.out         |   8 +
 .../regress/expected/generated_virtual.out    |  17 ++-
 src/test/regress/expected/groupingsets.out    |   3 +-
 src/test/regress/expected/inherit.out         |  28 ++--
 src/test/regress/expected/join.out            | 113 ++++++++------
 src/test/regress/expected/merge.out           |   3 +-
 .../regress/expected/partition_aggregate.out  |  21 +--
 src/test/regress/expected/partition_join.out  |  23 +--
 src/test/regress/expected/partition_prune.out | 141 ++++++++++--------
 src/test/regress/expected/predicate.out       |  33 ++--
 src/test/regress/expected/rowsecurity.out     |  13 +-
 src/test/regress/expected/rowtypes.out        |   6 +-
 src/test/regress/expected/select.out          |   7 +-
 src/test/regress/expected/subselect.out       |   9 +-
 src/test/regress/expected/tsrf.out            |   6 +-
 src/test/regress/sql/explain.sql              |   1 +
 src/tools/pgindent/typedefs.list              |   1 +
 27 files changed, 534 insertions(+), 238 deletions(-)

diff --git a/contrib/file_fdw/expected/file_fdw.out b/contrib/file_fdw/expected/file_fdw.out
index 246e3d3e566..5121e27dce5 100644
--- a/contrib/file_fdw/expected/file_fdw.out
+++ b/contrib/file_fdw/expected/file_fdw.out
@@ -322,6 +322,7 @@ SET constraint_exclusion = 'on';
 SELECT explain_filter('EXPLAIN (VERBOSE, COSTS FALSE) SELECT * FROM agg_csv WHERE a < 0');
  Result
    Output: a, b
+   Replaces: Scan on agg_csv
    One-Time Filter: false
 
 \t off
diff --git a/contrib/pg_overexplain/expected/pg_overexplain.out b/contrib/pg_overexplain/expected/pg_overexplain.out
index 6de02323d7c..55d34666d87 100644
--- a/contrib/pg_overexplain/expected/pg_overexplain.out
+++ b/contrib/pg_overexplain/expected/pg_overexplain.out
@@ -44,9 +44,10 @@ EXPLAIN (RANGE_TABLE) SELECT 1;
                 QUERY PLAN                
 ------------------------------------------
  Result  (cost=0.00..0.01 rows=1 width=4)
+   RTIs: 1
  RTI 1 (result):
    Eref: "*RESULT*" ()
-(3 rows)
+(4 rows)
 
 -- Create a partitioned table.
 CREATE TABLE vegetables (id serial, name text, genus text)
@@ -475,6 +476,7 @@ INSERT INTO vegetables (name, genus) VALUES ('broccoflower', 'brassica');
    Nominal RTI: 1
    Exclude Relation RTI: 0
    ->  Result
+         RTIs: 2
  RTI 1 (relation):
    Eref: vegetables (id, name, genus)
    Relation: vegetables
@@ -485,5 +487,5 @@ INSERT INTO vegetables (name, genus) VALUES ('broccoflower', 'brassica');
    Eref: "*RESULT*" ()
  Unprunable RTIs: 1
  Result RTIs: 1
-(14 rows)
+(15 rows)
 
diff --git a/contrib/pg_overexplain/pg_overexplain.c b/contrib/pg_overexplain/pg_overexplain.c
index de824566f8c..bd70b6d9d5e 100644
--- a/contrib/pg_overexplain/pg_overexplain.c
+++ b/contrib/pg_overexplain/pg_overexplain.c
@@ -236,6 +236,18 @@ overexplain_per_node_hook(PlanState *planstate, List *ancestors,
 									  ((MergeAppend *) plan)->apprelids,
 									  es);
 				break;
+			case T_Result:
+
+				/*
+				 * 'relids' is only meaningful when plan->lefttree is NULL,
+				 * but if somehow it ends up set when plan->lefttree is not
+				 * NULL, print it anyway.
+				 */
+				if (plan->lefttree == NULL ||
+					((Result *) plan)->relids != NULL)
+					overexplain_bitmapset("RTIs",
+										  ((Result *) plan)->relids,
+										  es);
 			default:
 				break;
 		}
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 18d727d7790..7aa61cec5da 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -7157,8 +7157,9 @@ EXPLAIN (VERBOSE, COSTS OFF) SELECT count(*) FROM ft1 WHERE c2 < 0;
  Aggregate
    Output: count(*)
    ->  Result
+         Replaces: Scan on ft1
          One-Time Filter: false
-(4 rows)
+(5 rows)
 
 SELECT count(*) FROM ft1 WHERE c2 < 0;
  count 
@@ -7201,8 +7202,9 @@ EXPLAIN (VERBOSE, COSTS OFF) SELECT count(*) FROM ft1 WHERE c2 >= 0;
  Aggregate
    Output: count(*)
    ->  Result
+         Replaces: Scan on ft1
          One-Time Filter: false
-(4 rows)
+(5 rows)
 
 SELECT count(*) FROM ft1 WHERE c2 >= 0;
  count 
@@ -8030,8 +8032,9 @@ DELETE FROM rem1 WHERE false;     -- currently can't be pushed down
    Remote SQL: DELETE FROM public.loc1 WHERE ctid = $1
    ->  Result
          Output: ctid
+         Replaces: Scan on rem1
          One-Time Filter: false
-(5 rows)
+(6 rows)
 
 -- Test with statement-level triggers
 CREATE TRIGGER trig_stmt_before
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 8345bc0264b..0ab1813728a 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -147,6 +147,7 @@ static void show_buffer_usage(ExplainState *es, const BufferUsage *usage);
 static void show_wal_usage(ExplainState *es, const WalUsage *usage);
 static void show_memory_counters(ExplainState *es,
 								 const MemoryContextCounters *mem_counters);
+static void show_result_replacement_info(Result *result, ExplainState *es);
 static void ExplainIndexScanDetails(Oid indexid, ScanDirection indexorderdir,
 									ExplainState *es);
 static void ExplainScanTarget(Scan *plan, ExplainState *es);
@@ -2232,6 +2233,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
 								   ancestors, es);
 			break;
 		case T_Result:
+			show_result_replacement_info(castNode(Result, plan), es);
 			show_upper_qual((List *) ((Result *) plan)->resconstantqual,
 							"One-Time Filter", planstate, ancestors, es);
 			show_upper_qual(plan->qual, "Filter", planstate, ancestors, es);
@@ -4750,6 +4752,102 @@ show_modifytable_info(ModifyTableState *mtstate, List *ancestors,
 		ExplainCloseGroup("Target Tables", "Target Tables", false, es);
 }
 
+/*
+ * Explain what a "Result" node replaced.
+ */
+static void
+show_result_replacement_info(Result *result, ExplainState *es)
+{
+	StringInfoData buf;
+	int			nrels = 0;
+	int			rti = -1;
+	bool		found_non_result = false;
+	char	   *replacement_type;
+
+	/* If the Result node has a subplan, it didn't replace anything. */
+	if (result->plan.lefttree != NULL)
+		return;
+
+	/* Gating result nodes should have a subplan, and we don't. */
+	Assert(result->result_type != RESULT_TYPE_GATING);
+
+	switch (result->result_type)
+	{
+		case RESULT_TYPE_GATING:
+			replacement_type = "Gating";
+			break;
+		case RESULT_TYPE_SCAN:
+			replacement_type = "Scan";
+			break;
+		case RESULT_TYPE_JOIN:
+			replacement_type = "Join";
+			break;
+		case RESULT_TYPE_UPPER:
+			/* a small white lie */
+			replacement_type = "Aggregate";
+			break;
+		case RESULT_TYPE_MINMAX:
+			replacement_type = "MinMaxAggregate";
+			break;
+	}
+
+	/*
+	 * Build up a comma-separated list of user-facing names for the range
+	 * table entries in the relids set.
+	 */
+	initStringInfo(&buf);
+	while ((rti = bms_next_member(result->relids, rti)) >= 0)
+	{
+		RangeTblEntry *rte = rt_fetch(rti, es->rtable);
+		char	   *refname;
+
+		/*
+		 * add_outer_joins_to_relids will add join RTIs to the relids set of a
+		 * join; if that join is then replaced with a Result node, we may see
+		 * such RTIs here. But we want to completely ignore those here,
+		 * because "a LEFT JOIN b ON whatever" is a join between a and b, not
+		 * a join between a, b, and an unnamed join.
+		 */
+		if (rte->rtekind == RTE_JOIN)
+			continue;
+
+		/* Count the number of rels that aren't ignored completely. */
+		++nrels;
+
+		/* Work out what reference name to use and add it to the string. */
+		refname = (char *) list_nth(es->rtable_names, rti - 1);
+		if (refname == NULL)
+			refname = rte->eref->aliasname;
+		if (buf.len > 0)
+			appendStringInfoString(&buf, ", ");
+		appendStringInfoString(&buf, refname);
+
+		/* Keep track of whether we see anything other than RTE_RESULT. */
+		if (rte->rtekind != RTE_RESULT)
+			found_non_result = true;
+	}
+
+	/*
+	 * If this Result node is because of a single RTE that is RTE_RESULT, it
+	 * is not really replacing anything at all, because there's no other
+	 * method for implementing a scan of such an RTE, so we don't display the
+	 * Replaces line in such cases.
+	 */
+	if (nrels <= 1 && !found_non_result &&
+		result->result_type == RESULT_TYPE_SCAN)
+		return;
+
+	/* Say what we replaced, with list of rels if available. */
+	if (buf.len == 0)
+		ExplainPropertyText("Replaces", replacement_type, es);
+	else
+	{
+		char	   *s = psprintf("%s on %s", replacement_type, buf.data);
+
+		ExplainPropertyText("Replaces", s, es);
+	}
+}
+
 /*
  * Explain the constituent plans of an Append, MergeAppend,
  * BitmapAnd, or BitmapOr node.
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index 6791cbeb416..c9dba7ff346 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -99,7 +99,8 @@ static Gather *create_gather_plan(PlannerInfo *root, GatherPath *best_path);
 static Plan *create_projection_plan(PlannerInfo *root,
 									ProjectionPath *best_path,
 									int flags);
-static Plan *inject_projection_plan(Plan *subplan, List *tlist, bool parallel_safe);
+static Plan *inject_projection_plan(Plan *subplan, List *tlist,
+									bool parallel_safe);
 static Sort *create_sort_plan(PlannerInfo *root, SortPath *best_path, int flags);
 static IncrementalSort *create_incrementalsort_plan(PlannerInfo *root,
 													IncrementalSortPath *best_path, int flags);
@@ -302,7 +303,10 @@ static SetOp *make_setop(SetOpCmd cmd, SetOpStrategy strategy,
 						 List *tlist, Plan *lefttree, Plan *righttree,
 						 List *groupList, long numGroups);
 static LockRows *make_lockrows(Plan *lefttree, List *rowMarks, int epqParam);
-static Result *make_result(List *tlist, Node *resconstantqual, Plan *subplan);
+static Result *make_gating_result(List *tlist, Node *resconstantqual,
+								  Plan *subplan);
+static Result *make_one_row_result(List *tlist, Node *resconstantqual,
+								   RelOptInfo *rel);
 static ProjectSet *make_project_set(List *tlist, Plan *subplan);
 static ModifyTable *make_modifytable(PlannerInfo *root, Plan *subplan,
 									 CmdType operation, bool canSetTag,
@@ -1012,36 +1016,36 @@ static Plan *
 create_gating_plan(PlannerInfo *root, Path *path, Plan *plan,
 				   List *gating_quals)
 {
-	Plan	   *gplan;
-	Plan	   *splan;
+	Result	   *gplan;
 
 	Assert(gating_quals);
 
 	/*
-	 * We might have a trivial Result plan already.  Stacking one Result atop
-	 * another is silly, so if that applies, just discard the input plan.
+	 * Since we need a Result node anyway, always return the path's requested
+	 * tlist; that's never a wrong choice, even if the parent node didn't ask
+	 * for CP_EXACT_TLIST.
+	 */
+	gplan = make_gating_result(build_path_tlist(root, path),
+							   (Node *) gating_quals, plan);
+
+	/*
+	 * We might have had a trivial Result plan already.  Stacking one Result
+	 * atop another is silly, so if that applies, just discard the input plan.
 	 * (We're assuming its targetlist is uninteresting; it should be either
-	 * the same as the result of build_path_tlist, or a simplified version.)
+	 * the same as the result of build_path_tlist, or a simplified version.
+	 * However, we preserve the set of relids that it purports to scan and
+	 * attribute that to our replacement Result instead, and likewise for the
+	 * result_type.)
 	 */
-	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;
 	}
 
-	/*
-	 * Since we need a Result node anyway, always return the path's requested
-	 * tlist; that's never a wrong choice, even if the parent node didn't ask
-	 * for CP_EXACT_TLIST.
-	 */
-	gplan = (Plan *) make_result(build_path_tlist(root, path),
-								 (Node *) gating_quals,
-								 splan);
-
 	/*
 	 * Notice that we don't change cost or size estimates when doing gating.
 	 * The costs of qual eval were already included in the subplan's cost.
@@ -1054,12 +1058,12 @@ create_gating_plan(PlannerInfo *root, Path *path, Plan *plan,
 	 * in most cases we have only a very bad idea of the probability of the
 	 * gating qual being true.
 	 */
-	copy_plan_costsize(gplan, plan);
+	copy_plan_costsize(&gplan->plan, plan);
 
 	/* Gating quals could be unsafe, so better use the Path's safety flag */
-	gplan->parallel_safe = path->parallel_safe;
+	gplan->plan.parallel_safe = path->parallel_safe;
 
-	return gplan;
+	return &gplan->plan;
 }
 
 /*
@@ -1235,10 +1239,10 @@ create_append_plan(PlannerInfo *root, AppendPath *best_path, int flags)
 		/* Generate a Result plan with constant-FALSE gating qual */
 		Plan	   *plan;
 
-		plan = (Plan *) make_result(tlist,
-									(Node *) list_make1(makeBoolConst(false,
-																	  false)),
-									NULL);
+		plan = (Plan *) make_one_row_result(tlist,
+											(Node *) list_make1(makeBoolConst(false,
+																			  false)),
+											best_path->path.parent);
 
 		copy_generic_path_info(plan, (Path *) best_path);
 
@@ -1636,7 +1640,7 @@ create_group_result_plan(PlannerInfo *root, GroupResultPath *best_path)
 	/* best_path->quals is just bare clauses */
 	quals = order_qual_clauses(root, best_path->quals);
 
-	plan = make_result(tlist, (Node *) quals, NULL);
+	plan = make_one_row_result(tlist, (Node *) quals, best_path->path.parent);
 
 	copy_generic_path_info(&plan->plan, (Path *) best_path);
 
@@ -1933,8 +1937,7 @@ create_projection_plan(PlannerInfo *root, ProjectionPath *best_path, int flags)
 	}
 	else
 	{
-		/* We need a Result node */
-		plan = (Plan *) make_result(tlist, NULL, subplan);
+		plan = (Plan *) make_gating_result(tlist, NULL, subplan);
 
 		copy_generic_path_info(plan, (Path *) best_path);
 	}
@@ -1958,7 +1961,7 @@ inject_projection_plan(Plan *subplan, List *tlist, bool parallel_safe)
 {
 	Plan	   *plan;
 
-	plan = (Plan *) make_result(tlist, NULL, subplan);
+	plan = (Plan *) make_gating_result(tlist, NULL, subplan);
 
 	/*
 	 * In principle, we should charge tlist eval cost plus cpu_per_tuple per
@@ -2436,7 +2439,9 @@ create_minmaxagg_plan(PlannerInfo *root, MinMaxAggPath *best_path)
 	/* Generate the output plan --- basically just a Result */
 	tlist = build_path_tlist(root, &best_path->path);
 
-	plan = make_result(tlist, (Node *) best_path->quals, NULL);
+	plan = make_one_row_result(tlist, (Node *) best_path->quals,
+							   best_path->path.parent);
+	plan->result_type = RESULT_TYPE_MINMAX;
 
 	copy_generic_path_info(&plan->plan, (Path *) best_path);
 
@@ -3887,7 +3892,8 @@ create_resultscan_plan(PlannerInfo *root, Path *best_path,
 			replace_nestloop_params(root, (Node *) scan_clauses);
 	}
 
-	scan_plan = make_result(tlist, (Node *) scan_clauses, NULL);
+	scan_plan = make_one_row_result(tlist, (Node *) scan_clauses,
+									best_path->parent);
 
 	copy_generic_path_info(&scan_plan->plan, best_path);
 
@@ -6922,22 +6928,57 @@ make_limit(Plan *lefttree, Node *limitOffset, Node *limitCount,
 }
 
 /*
- * make_result
- *	  Build a Result plan node
+ * make_gating_result
+ *	  Build a Result plan node that performs projection of a subplan, and/or
+ *	  applies a one time filter (resconstantqual)
  */
 static Result *
-make_result(List *tlist,
-			Node *resconstantqual,
-			Plan *subplan)
+make_gating_result(List *tlist,
+				   Node *resconstantqual,
+				   Plan *subplan)
 {
 	Result	   *node = makeNode(Result);
 	Plan	   *plan = &node->plan;
 
+	Assert(subplan != NULL);
+
 	plan->targetlist = tlist;
 	plan->qual = NIL;
 	plan->lefttree = subplan;
 	plan->righttree = NULL;
+	node->result_type = RESULT_TYPE_GATING;
+	node->resconstantqual = resconstantqual;
+	node->relids = NULL;
+
+	return node;
+}
+
+/*
+ * make_one_row_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)
+ *
+ * 'rel' should be this path's RelOptInfo. In essence, we're saying that this
+ * Result node generates all the tuples for that RelOptInfo. Note that the same
+ * consideration can never arise in make_gating_result(), because in that case
+ * the tuples are always coming from some subordinate node.
+ */
+static Result *
+make_one_row_result(List *tlist,
+					Node *resconstantqual,
+					RelOptInfo *rel)
+{
+	Result	   *node = makeNode(Result);
+	Plan	   *plan = &node->plan;
+
+	plan->targetlist = tlist;
+	plan->qual = NIL;
+	plan->lefttree = NULL;
+	plan->righttree = NULL;
+	node->result_type = IS_UPPER_REL(rel) ? RESULT_TYPE_UPPER :
+		IS_JOIN_REL(rel) ? RESULT_TYPE_JOIN : RESULT_TYPE_SCAN;
 	node->resconstantqual = resconstantqual;
+	node->relids = rel->relids;
 
 	return node;
 }
diff --git a/src/backend/optimizer/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.c
index d706546f332..6950eff2c5b 100644
--- a/src/backend/optimizer/plan/setrefs.c
+++ b/src/backend/optimizer/plan/setrefs.c
@@ -1056,6 +1056,8 @@ set_plan_refs(PlannerInfo *root, Plan *plan, int rtoffset)
 				/* resconstantqual can't contain any subplan variable refs */
 				splan->resconstantqual =
 					fix_scan_expr(root, splan->resconstantqual, rtoffset, 1);
+				/* adjust the relids set */
+				splan->relids = offset_relid_set(splan->relids, rtoffset);
 			}
 			break;
 		case T_ProjectSet:
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index 29d7732d6a0..3d196f5078e 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -252,6 +252,20 @@ typedef struct Plan
 #define outerPlan(node)			(((Plan *)(node))->lefttree)
 
 
+/* ----------------
+ *	 ResultType -
+ *		Classification of Result nodes
+ * ----------------
+ */
+typedef enum ResultType
+{
+	RESULT_TYPE_GATING,			/* project or one-time-filter outer plan */
+	RESULT_TYPE_SCAN,			/* replace empty scan */
+	RESULT_TYPE_JOIN,			/* replace empty join */
+	RESULT_TYPE_UPPER,			/* replace degenerate upper rel */
+	RESULT_TYPE_MINMAX			/* implement minmax aggregate */
+} ResultType;
+
 /* ----------------
  *	 Result node -
  *		If no outer plan, evaluate a variable-free targetlist.
@@ -261,12 +275,20 @@ typedef struct Plan
  * If resconstantqual isn't NULL, it represents a one-time qualification
  * test (i.e., one that doesn't depend on any variables from the outer plan,
  * so needs to be evaluated only once).
+ *
+ * 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.
  * ----------------
  */
 typedef struct Result
 {
 	Plan		plan;
+	ResultType	result_type;
 	Node	   *resconstantqual;
+	Bitmapset  *relids;
 } Result;
 
 /* ----------------
diff --git a/src/test/regress/expected/aggregates.out b/src/test/regress/expected/aggregates.out
index c35288eecde..1f24f6ffd1f 100644
--- a/src/test/regress/expected/aggregates.out
+++ b/src/test/regress/expected/aggregates.out
@@ -959,11 +959,12 @@ explain (costs off)
                          QUERY PLAN                         
 ------------------------------------------------------------
  Result
+   Replaces: MinMaxAggregate
    InitPlan 1
      ->  Limit
            ->  Index Only Scan using tenk1_unique1 on tenk1
                  Index Cond: (unique1 IS NOT NULL)
-(5 rows)
+(6 rows)
 
 select min(unique1) from tenk1;
  min 
@@ -976,11 +977,12 @@ explain (costs off)
                              QUERY PLAN                              
 ---------------------------------------------------------------------
  Result
+   Replaces: MinMaxAggregate
    InitPlan 1
      ->  Limit
            ->  Index Only Scan Backward using tenk1_unique1 on tenk1
                  Index Cond: (unique1 IS NOT NULL)
-(5 rows)
+(6 rows)
 
 select max(unique1) from tenk1;
  max  
@@ -993,11 +995,12 @@ explain (costs off)
                                QUERY PLAN                               
 ------------------------------------------------------------------------
  Result
+   Replaces: MinMaxAggregate
    InitPlan 1
      ->  Limit
            ->  Index Only Scan Backward using tenk1_unique1 on tenk1
                  Index Cond: ((unique1 IS NOT NULL) AND (unique1 < 42))
-(5 rows)
+(6 rows)
 
 select max(unique1) from tenk1 where unique1 < 42;
  max 
@@ -1010,11 +1013,12 @@ explain (costs off)
                                QUERY PLAN                               
 ------------------------------------------------------------------------
  Result
+   Replaces: MinMaxAggregate
    InitPlan 1
      ->  Limit
            ->  Index Only Scan Backward using tenk1_unique1 on tenk1
                  Index Cond: ((unique1 IS NOT NULL) AND (unique1 > 42))
-(5 rows)
+(6 rows)
 
 select max(unique1) from tenk1 where unique1 > 42;
  max  
@@ -1033,11 +1037,12 @@ explain (costs off)
                                 QUERY PLAN                                 
 ---------------------------------------------------------------------------
  Result
+   Replaces: MinMaxAggregate
    InitPlan 1
      ->  Limit
            ->  Index Only Scan Backward using tenk1_unique1 on tenk1
                  Index Cond: ((unique1 IS NOT NULL) AND (unique1 > 42000))
-(5 rows)
+(6 rows)
 
 select max(unique1) from tenk1 where unique1 > 42000;
  max 
@@ -1052,11 +1057,12 @@ explain (costs off)
                                  QUERY PLAN                                 
 ----------------------------------------------------------------------------
  Result
+   Replaces: MinMaxAggregate
    InitPlan 1
      ->  Limit
            ->  Index Only Scan Backward using tenk1_thous_tenthous on tenk1
                  Index Cond: ((thousand = 33) AND (tenthous IS NOT NULL))
-(5 rows)
+(6 rows)
 
 select max(tenthous) from tenk1 where thousand = 33;
  max  
@@ -1069,11 +1075,12 @@ explain (costs off)
                                 QUERY PLAN                                
 --------------------------------------------------------------------------
  Result
+   Replaces: MinMaxAggregate
    InitPlan 1
      ->  Limit
            ->  Index Only Scan using tenk1_thous_tenthous on tenk1
                  Index Cond: ((thousand = 33) AND (tenthous IS NOT NULL))
-(5 rows)
+(6 rows)
 
 select min(tenthous) from tenk1 where thousand = 33;
  min 
@@ -1090,11 +1097,12 @@ explain (costs off)
  Seq Scan on int4_tbl
    SubPlan 2
      ->  Result
+           Replaces: MinMaxAggregate
            InitPlan 1
              ->  Limit
                    ->  Index Only Scan using tenk1_unique1 on tenk1
                          Index Cond: ((unique1 IS NOT NULL) AND (unique1 > int4_tbl.f1))
-(7 rows)
+(8 rows)
 
 select f1, (select min(unique1) from tenk1 where unique1 > f1) AS gt
   from int4_tbl;
@@ -1119,7 +1127,8 @@ explain (costs off)
            ->  Index Only Scan Backward using tenk1_unique2 on tenk1
                  Index Cond: (unique2 IS NOT NULL)
    ->  Result
-(7 rows)
+         Replaces: MinMaxAggregate
+(8 rows)
 
 select distinct max(unique2) from tenk1;
  max  
@@ -1138,7 +1147,8 @@ explain (costs off)
            ->  Index Only Scan Backward using tenk1_unique2 on tenk1
                  Index Cond: (unique2 IS NOT NULL)
    ->  Result
-(7 rows)
+         Replaces: MinMaxAggregate
+(8 rows)
 
 select max(unique2) from tenk1 order by 1;
  max  
@@ -1157,7 +1167,8 @@ explain (costs off)
            ->  Index Only Scan Backward using tenk1_unique2 on tenk1
                  Index Cond: (unique2 IS NOT NULL)
    ->  Result
-(7 rows)
+         Replaces: MinMaxAggregate
+(8 rows)
 
 select max(unique2) from tenk1 order by max(unique2);
  max  
@@ -1176,7 +1187,8 @@ explain (costs off)
            ->  Index Only Scan Backward using tenk1_unique2 on tenk1
                  Index Cond: (unique2 IS NOT NULL)
    ->  Result
-(7 rows)
+         Replaces: MinMaxAggregate
+(8 rows)
 
 select max(unique2) from tenk1 order by max(unique2)+1;
  max  
@@ -1196,7 +1208,8 @@ explain (costs off)
                  Index Cond: (unique2 IS NOT NULL)
    ->  ProjectSet
          ->  Result
-(8 rows)
+               Replaces: MinMaxAggregate
+(9 rows)
 
 select max(unique2), generate_series(1,3) as g from tenk1 order by g desc;
  max  | g 
@@ -1212,12 +1225,13 @@ explain (costs off)
                      QUERY PLAN                     
 ----------------------------------------------------
  Result
+   Replaces: MinMaxAggregate
    InitPlan 1
      ->  Limit
            ->  Result
                  One-Time Filter: (100 IS NOT NULL)
                  ->  Seq Scan on tenk1
-(6 rows)
+(7 rows)
 
 select max(100) from tenk1;
  max 
@@ -1243,6 +1257,7 @@ explain (costs off)
                                          QUERY PLAN                                          
 ---------------------------------------------------------------------------------------------
  Result
+   Replaces: MinMaxAggregate
    InitPlan 1
      ->  Limit
            ->  Merge Append
@@ -1265,7 +1280,7 @@ explain (costs off)
                  ->  Index Only Scan using minmaxtest2i on minmaxtest2 minmaxtest_8
                        Index Cond: (f1 IS NOT NULL)
                  ->  Index Only Scan Backward using minmaxtest3i on minmaxtest3 minmaxtest_9
-(23 rows)
+(24 rows)
 
 select min(f1), max(f1) from minmaxtest;
  min | max 
@@ -1304,7 +1319,8 @@ explain (costs off)
    ->  Sort
          Sort Key: ((InitPlan 1).col1), ((InitPlan 2).col1)
          ->  Result
-(26 rows)
+               Replaces: MinMaxAggregate
+(27 rows)
 
 select distinct min(f1), max(f1) from minmaxtest;
  min | max 
@@ -1334,7 +1350,8 @@ explain (costs off)
                    ->  Seq Scan on int4_tbl t1
                          Filter: ((f1 IS NOT NULL) AND (f1 = t0.f1))
            ->  Result
-(9 rows)
+                 Replaces: MinMaxAggregate
+(10 rows)
 
 select f1, (select distinct min(t1.f1) from int4_tbl t1 where t1.f1 = t0.f1)
 from int4_tbl t0;
diff --git a/src/test/regress/expected/case.out b/src/test/regress/expected/case.out
index efee7fc4317..4c77f7e3961 100644
--- a/src/test/regress/expected/case.out
+++ b/src/test/regress/expected/case.out
@@ -266,27 +266,30 @@ SELECT *
 -- Tests for constant subexpression simplification
 explain (costs off)
 SELECT * FROM CASE_TBL WHERE NULLIF(1, 2) = 2;
-        QUERY PLAN        
---------------------------
+          QUERY PLAN          
+------------------------------
  Result
+   Replaces: Scan on case_tbl
    One-Time Filter: false
-(2 rows)
+(3 rows)
 
 explain (costs off)
 SELECT * FROM CASE_TBL WHERE NULLIF(1, 1) IS NOT NULL;
-        QUERY PLAN        
---------------------------
+          QUERY PLAN          
+------------------------------
  Result
+   Replaces: Scan on case_tbl
    One-Time Filter: false
-(2 rows)
+(3 rows)
 
 explain (costs off)
 SELECT * FROM CASE_TBL WHERE NULLIF(1, null) = 2;
-        QUERY PLAN        
---------------------------
+          QUERY PLAN          
+------------------------------
  Result
+   Replaces: Scan on case_tbl
    One-Time Filter: false
-(2 rows)
+(3 rows)
 
 --
 -- Examples of updates involving tables
diff --git a/src/test/regress/expected/explain.out b/src/test/regress/expected/explain.out
index c53bf9c8aa3..7c1f26b182c 100644
--- a/src/test/regress/expected/explain.out
+++ b/src/test/regress/expected/explain.out
@@ -54,6 +54,14 @@ set jit = off;
 -- enabled.
 set track_io_timing = off;
 -- Simple cases
+explain (costs off) select 1 as a, 2 as b having false;
+        QUERY PLAN        
+--------------------------
+ Result
+   Replaces: Aggregate
+   One-Time Filter: false
+(3 rows)
+
 select explain_filter('explain select * from int8_tbl i8');
                      explain_filter                      
 ---------------------------------------------------------
diff --git a/src/test/regress/expected/generated_virtual.out b/src/test/regress/expected/generated_virtual.out
index d8645192351..b48e6bdbf9d 100644
--- a/src/test/regress/expected/generated_virtual.out
+++ b/src/test/regress/expected/generated_virtual.out
@@ -1579,8 +1579,9 @@ select t2.* from gtest32 t1 left join gtest32 t2 on false;
          Output: t1.a, t1.b, t1.c, t1.d, t1.e
    ->  Result
          Output: a, e, 20, COALESCE(a, 100)
+         Replaces: Scan on t2
          One-Time Filter: false
-(8 rows)
+(9 rows)
 
 select t2.* from gtest32 t1 left join gtest32 t2 on false;
  a | b | c | d | e 
@@ -1642,20 +1643,22 @@ set constraint_exclusion to on;
 -- should get a dummy Result, not a seq scan
 explain (costs off)
 select * from gtest33 where b < 10;
-        QUERY PLAN        
---------------------------
+         QUERY PLAN          
+-----------------------------
  Result
+   Replaces: Scan on gtest33
    One-Time Filter: false
-(2 rows)
+(3 rows)
 
 -- should get a dummy Result, not a seq scan
 explain (costs off)
 select * from gtest33 where b is null;
-        QUERY PLAN        
---------------------------
+         QUERY PLAN          
+-----------------------------
  Result
+   Replaces: Scan on gtest33
    One-Time Filter: false
-(2 rows)
+(3 rows)
 
 reset constraint_exclusion;
 drop table gtest33;
diff --git a/src/test/regress/expected/groupingsets.out b/src/test/regress/expected/groupingsets.out
index 35e4cb47ebe..210bbe307a7 100644
--- a/src/test/regress/expected/groupingsets.out
+++ b/src/test/regress/expected/groupingsets.out
@@ -591,11 +591,12 @@ explain (costs off)
                          QUERY PLAN                         
 ------------------------------------------------------------
  Result
+   Replaces: MinMaxAggregate
    InitPlan 1
      ->  Limit
            ->  Index Only Scan using tenk1_unique1 on tenk1
                  Index Cond: (unique1 IS NOT NULL)
-(5 rows)
+(6 rows)
 
 -- Views with GROUPING SET queries
 CREATE VIEW gstest_view AS select a, b, grouping(a,b), sum(c), count(*), max(c)
diff --git a/src/test/regress/expected/inherit.out b/src/test/regress/expected/inherit.out
index 5b5055babdc..031dd87424a 100644
--- a/src/test/regress/expected/inherit.out
+++ b/src/test/regress/expected/inherit.out
@@ -580,8 +580,9 @@ update some_tab set a = a + 1 where false;
  Update on public.some_tab
    ->  Result
          Output: (some_tab.a + 1), NULL::oid, NULL::tid
+         Replaces: Scan on some_tab
          One-Time Filter: false
-(4 rows)
+(5 rows)
 
 update some_tab set a = a + 1 where false;
 explain (verbose, costs off)
@@ -592,8 +593,9 @@ update some_tab set a = a + 1 where false returning b, a;
    Output: some_tab.b, some_tab.a
    ->  Result
          Output: (some_tab.a + 1), NULL::oid, NULL::tid
+         Replaces: Scan on some_tab
          One-Time Filter: false
-(5 rows)
+(6 rows)
 
 update some_tab set a = a + 1 where false returning b, a;
  b | a 
@@ -699,8 +701,9 @@ explain update parted_tab set a = 2 where false;
 --------------------------------------------------------
  Update on parted_tab  (cost=0.00..0.00 rows=0 width=0)
    ->  Result  (cost=0.00..0.00 rows=0 width=10)
+         Replaces: Scan on parted_tab
          One-Time Filter: false
-(3 rows)
+(4 rows)
 
 drop table parted_tab;
 -- Check UPDATE with multi-level partitioned inherited target
@@ -1756,6 +1759,7 @@ explain (verbose, costs off) select min(1-id) from matest0;
 ---------------------------------------------------------------------------------
  Result
    Output: (InitPlan 1).col1
+   Replaces: MinMaxAggregate
    InitPlan 1
      ->  Limit
            Output: ((1 - matest0.id))
@@ -1779,7 +1783,7 @@ explain (verbose, costs off) select min(1-id) from matest0;
                        ->  Index Scan using matest3i on public.matest3 matest0_4
                              Output: matest0_4.id, (1 - matest0_4.id)
                              Index Cond: ((1 - matest0_4.id) IS NOT NULL)
-(25 rows)
+(26 rows)
 
 select min(1-id) from matest0;
  min 
@@ -1943,6 +1947,7 @@ SELECT min(x) FROM
                              QUERY PLAN                             
 --------------------------------------------------------------------
  Result
+   Replaces: MinMaxAggregate
    InitPlan 1
      ->  Limit
            ->  Merge Append
@@ -1951,7 +1956,7 @@ SELECT min(x) FROM
                        Index Cond: (unique1 IS NOT NULL)
                  ->  Index Only Scan using tenk1_unique2 on tenk1 b
                        Index Cond: (unique2 IS NOT NULL)
-(9 rows)
+(10 rows)
 
 explain (costs off)
 SELECT min(y) FROM
@@ -1961,6 +1966,7 @@ SELECT min(y) FROM
                              QUERY PLAN                             
 --------------------------------------------------------------------
  Result
+   Replaces: MinMaxAggregate
    InitPlan 1
      ->  Limit
            ->  Merge Append
@@ -1969,7 +1975,7 @@ SELECT min(y) FROM
                        Index Cond: (unique1 IS NOT NULL)
                  ->  Index Only Scan using tenk1_unique2 on tenk1 b
                        Index Cond: (unique2 IS NOT NULL)
-(9 rows)
+(10 rows)
 
 -- XXX planner doesn't recognize that index on unique2 is sufficiently sorted
 explain (costs off)
@@ -3087,11 +3093,12 @@ explain (costs off) select * from range_list_parted where a between 3 and 23 and
 
 /* Should select no rows because range partition key cannot be null */
 explain (costs off) select * from range_list_parted where a is null;
-        QUERY PLAN        
---------------------------
+              QUERY PLAN               
+---------------------------------------
  Result
+   Replaces: Scan on range_list_parted
    One-Time Filter: false
-(2 rows)
+(3 rows)
 
 /* Should only select rows from the null-accepting partition */
 explain (costs off) select * from range_list_parted where b is null;
@@ -3252,6 +3259,7 @@ explain (costs off) select min(a), max(a) from parted_minmax where b = '12345';
                                            QUERY PLAN                                           
 ------------------------------------------------------------------------------------------------
  Result
+   Replaces: MinMaxAggregate
    InitPlan 1
      ->  Limit
            ->  Index Only Scan using parted_minmax1i on parted_minmax1 parted_minmax
@@ -3260,7 +3268,7 @@ explain (costs off) select min(a), max(a) from parted_minmax where b = '12345';
      ->  Limit
            ->  Index Only Scan Backward using parted_minmax1i on parted_minmax1 parted_minmax_1
                  Index Cond: ((a IS NOT NULL) AND (b = '12345'::text))
-(9 rows)
+(10 rows)
 
 select min(a), max(a) from parted_minmax where b = '12345';
  min | max 
diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out
index 04079268b98..30baa9dc51a 100644
--- a/src/test/regress/expected/join.out
+++ b/src/test/regress/expected/join.out
@@ -2264,11 +2264,12 @@ explain (costs off)
 select aa, bb, unique1, unique1
   from tenk1 right join b_star on aa = unique1
   where bb < bb and bb is null;
-        QUERY PLAN        
---------------------------
+            QUERY PLAN             
+-----------------------------------
  Result
+   Replaces: Join on tenk1, b_star
    One-Time Filter: false
-(2 rows)
+(3 rows)
 
 select aa, bb, unique1, unique1
   from tenk1 right join b_star on aa = unique1
@@ -2386,11 +2387,12 @@ order by t1.unique1;
                            Index Cond: (unique1 < 10)
                SubPlan 2
                  ->  Result
+                       Replaces: MinMaxAggregate
                        InitPlan 1
                          ->  Limit
                                ->  Index Only Scan using tenk1_unique1 on tenk1
                                      Index Cond: ((unique1 IS NOT NULL) AND (unique1 = t2.unique1))
-(19 rows)
+(20 rows)
 
 -- Ensure we get the expected result
 select t1.unique1,t2.unique1 from tenk1 t1
@@ -2655,8 +2657,8 @@ select * from int8_tbl t1 left join
     (int8_tbl t2 left join int8_tbl t3 full join int8_tbl t4 on false on false)
     left join int8_tbl t5 on t2.q1 = t5.q1
 on t2.q2 = 123;
-                    QUERY PLAN                    
---------------------------------------------------
+                     QUERY PLAN                     
+----------------------------------------------------
  Nested Loop Left Join
    ->  Seq Scan on int8_tbl t1
    ->  Materialize
@@ -2667,9 +2669,10 @@ on t2.q2 = 123;
                      ->  Seq Scan on int8_tbl t2
                            Filter: (q2 = 123)
                      ->  Result
+                           Replaces: Join on t3, t4
                            One-Time Filter: false
                ->  Seq Scan on int8_tbl t5
-(12 rows)
+(13 rows)
 
 explain (costs off)
 select * from int8_tbl t1
@@ -4164,6 +4167,7 @@ select * from t t1
                            Output: t2.i2, 1
          ->  Result
                Output: i3
+               Replaces: Scan on t3
                One-Time Filter: false
    ->  Memoize
          Output: t4.i4
@@ -4172,7 +4176,7 @@ select * from t t1
          ->  Index Only Scan using t_pkey on pg_temp.t t4
                Output: t4.i4
                Index Cond: (t4.i4 > (1))
-(25 rows)
+(26 rows)
 
 explain (verbose, costs off)
 select * from
@@ -4362,8 +4366,9 @@ from int4_tbl t1
 -------------------------------------------------------------------------------------------------------------------
  Result
    Output: (current_database())::information_schema.sql_identifier, (c.relname)::information_schema.sql_identifier
+   Replaces: Join on t1, t2, a, c, nc, t, nt, bt, nbt
    One-Time Filter: false
-(3 rows)
+(4 rows)
 
 -- Test handling of qual pushdown to appendrel members with non-Var outputs
 explain (verbose, costs off)
@@ -4435,11 +4440,12 @@ select unique1 from tenk1, lateral f_immutable_int4(1) x where x = unique1;
 
 explain (costs off)
 select unique1 from tenk1, lateral f_immutable_int4(1) x where x in (select 17);
-        QUERY PLAN        
---------------------------
+        QUERY PLAN         
+---------------------------
  Result
+   Replaces: Scan on tenk1
    One-Time Filter: false
-(2 rows)
+(3 rows)
 
 explain (costs off)
 select unique1, x from tenk1 join f_immutable_int4(1) x on unique1 = x;
@@ -4485,11 +4491,12 @@ select unique1, x from tenk1 full join f_immutable_int4(1) x on unique1 = x;
 -- check that pullup of a const function allows further const-folding
 explain (costs off)
 select unique1 from tenk1, f_immutable_int4(1) x where x = 42;
-        QUERY PLAN        
---------------------------
+        QUERY PLAN         
+---------------------------
  Result
+   Replaces: Scan on tenk1
    One-Time Filter: false
-(2 rows)
+(3 rows)
 
 -- test inlining of immutable functions with PlaceHolderVars
 explain (costs off)
@@ -5345,8 +5352,9 @@ left join
    Join Filter: false
    ->  Result
    ->  Result
+         Replaces: Join on c, n
          One-Time Filter: false
-(5 rows)
+(6 rows)
 
 -- check handling of apparently-commutable outer joins with non-commutable
 -- joins between them
@@ -5540,12 +5548,13 @@ select 1 from
      right join (select 1 as z) as ss2 on true)
   on false,
   lateral (select i4.f1, ss1.n from int8_tbl as i8 limit 1) as ss3;
-        QUERY PLAN        
---------------------------
+                  QUERY PLAN                   
+-----------------------------------------------
  Result
    Output: 1
+   Replaces: Join on i4, ss3, x1, x2, *RESULT*
    One-Time Filter: false
-(3 rows)
+(4 rows)
 
 select 1 from
   int4_tbl as i4
@@ -5574,11 +5583,12 @@ select 1 from t t1
        on false
      where t3.a = coalesce(t5.a,1)) as s2
   on true;
-        QUERY PLAN        
---------------------------
+                 QUERY PLAN                 
+--------------------------------------------
  Result
+   Replaces: Join on t1, s1, t2, t3, t4, t5
    One-Time Filter: false
-(2 rows)
+(3 rows)
 
 rollback;
 --
@@ -5975,14 +5985,15 @@ from int4_tbl as t1
                         inner join int8_tbl as t7 on null)
                on t5.q1 = t7.q2)
     on false;
-           QUERY PLAN           
---------------------------------
+                    QUERY PLAN                    
+--------------------------------------------------
  Nested Loop Left Join
    Join Filter: false
    ->  Seq Scan on int4_tbl t1
    ->  Result
+         Replaces: Join on t2, t3, t4, t5, t7, t6
          One-Time Filter: false
-(5 rows)
+(6 rows)
 
 -- variant with Var rather than PHV coming from t6
 explain (costs off)
@@ -5997,14 +6008,15 @@ from int4_tbl as t1
                         inner join int8_tbl as t7 on null)
                on t5.q1 = t7.q2)
     on false;
-           QUERY PLAN           
---------------------------------
+                    QUERY PLAN                    
+--------------------------------------------------
  Nested Loop Left Join
    Join Filter: false
    ->  Seq Scan on int4_tbl t1
    ->  Result
+         Replaces: Join on t2, t3, t4, t5, t7, t6
          One-Time Filter: false
-(5 rows)
+(6 rows)
 
 -- per further discussion of bug #17781
 explain (costs off)
@@ -6054,15 +6066,16 @@ select * from int8_tbl t1 left join
   (int8_tbl t2 inner join int8_tbl t3 on false
    left join int8_tbl t4 on t2.q2 = t4.q2)
 on t1.q1 = t2.q1;
-              QUERY PLAN              
---------------------------------------
+                 QUERY PLAN                 
+--------------------------------------------
  Hash Left Join
    Hash Cond: (t1.q1 = q1)
    ->  Seq Scan on int8_tbl t1
    ->  Hash
          ->  Result
+               Replaces: Join on t2, t3, t4
                One-Time Filter: false
-(6 rows)
+(7 rows)
 
 -- deduce constant-false from an EquivalenceClass
 explain (costs off)
@@ -6070,15 +6083,16 @@ select * from int8_tbl t1 left join
   (int8_tbl t2 inner join int8_tbl t3 on (t2.q1-t3.q2) = 0 and (t2.q1-t3.q2) = 1
    left join int8_tbl t4 on t2.q2 = t4.q2)
 on t1.q1 = t2.q1;
-              QUERY PLAN              
---------------------------------------
+                 QUERY PLAN                 
+--------------------------------------------
  Hash Left Join
    Hash Cond: (t1.q1 = q1)
    ->  Seq Scan on int8_tbl t1
    ->  Hash
          ->  Result
+               Replaces: Join on t2, t3, t4
                One-Time Filter: false
-(6 rows)
+(7 rows)
 
 -- pseudoconstant based on an outer-level Param
 explain (costs off)
@@ -6317,8 +6331,9 @@ select p.* from
         QUERY PLAN        
 --------------------------
  Result
+   Replaces: Scan on p
    One-Time Filter: false
-(2 rows)
+(3 rows)
 
 select p.* from
   (parent p left join child c on (p.k = c.k)) join parent x on p.k = x.k
@@ -6334,8 +6349,9 @@ select p.* from
         QUERY PLAN        
 --------------------------
  Result
+   Replaces: Join on p, x
    One-Time Filter: false
-(2 rows)
+(3 rows)
 
 -- bug 5255: this is not optimizable by join removal
 begin;
@@ -6407,8 +6423,9 @@ SELECT q2 FROM
          Output: int4_tbl.f1
    ->  Result
          Output: q2, 'constant'::text
+         Replaces: Scan on int8_tbl
          One-Time Filter: false
-(9 rows)
+(10 rows)
 
 -- join removal bug #17786: check that OR conditions are cleaned up
 EXPLAIN (COSTS OFF)
@@ -6427,8 +6444,9 @@ FROM int4_tbl
                Filter: ((tenk1.unique1 = (42)) OR (tenk1.unique2 = (42)))
                ->  Seq Scan on tenk1
                ->  Result
+                     Replaces: Scan on int8_tbl
                      One-Time Filter: false
-(9 rows)
+(10 rows)
 
 rollback;
 -- another join removal bug: we must clean up correctly when removing a PHV
@@ -6878,8 +6896,9 @@ where q1.x = q2.y;
         QUERY PLAN        
 --------------------------
  Result
+   Replaces: Scan on sj_1
    One-Time Filter: false
-(2 rows)
+(3 rows)
 
 -- We can't use a cross-EC generated self join qual because of current logic of
 -- the generate_join_implied_equalities routine.
@@ -7703,11 +7722,12 @@ select 1 from emp1 full join
         on true
     where false) s on true
 where false;
-        QUERY PLAN        
---------------------------
+            QUERY PLAN            
+----------------------------------
  Result
+   Replaces: Join on emp1, t1, t3
    One-Time Filter: false
-(2 rows)
+(3 rows)
 
 select 1 from emp1 full join
     (select * from emp1 t1 join
@@ -7936,8 +7956,9 @@ where false;
 --------------------------
  Result
    Output: 1
+   Replaces: Scan on ss
    One-Time Filter: false
-(3 rows)
+(4 rows)
 
 --
 -- Test LATERAL
@@ -8875,8 +8896,9 @@ select * from int8_tbl i8 left join lateral
          Output: i8.q1, i8.q2
    ->  Result
          Output: f1, i8.q2
+         Replaces: Scan on int4_tbl
          One-Time Filter: false
-(8 rows)
+(9 rows)
 
 explain (verbose, costs off)
 select * from int8_tbl i8 left join lateral
@@ -8889,8 +8911,9 @@ select * from int8_tbl i8 left join lateral
          Output: i8.q1, i8.q2
    ->  Result
          Output: f1, f1, i8.q2
+         Replaces: Join on i1, i2
          One-Time Filter: false
-(7 rows)
+(8 rows)
 
 -- check handling of nested appendrels inside LATERAL
 select * from
diff --git a/src/test/regress/expected/merge.out b/src/test/regress/expected/merge.out
index cf2219df754..44df626c40c 100644
--- a/src/test/regress/expected/merge.out
+++ b/src/test/regress/expected/merge.out
@@ -2426,8 +2426,9 @@ MERGE INTO pa_target t USING pa_source s ON t.tid = s.sid
                Output: t.tid, t.ctid
                ->  Result
                      Output: t.tid, t.ctid
+                     Replaces: Scan on t
                      One-Time Filter: false
-(12 rows)
+(13 rows)
 
 MERGE INTO pa_target t USING pa_source s ON t.tid = s.sid
   WHEN NOT MATCHED THEN INSERT VALUES (s.sid);
diff --git a/src/test/regress/expected/partition_aggregate.out b/src/test/regress/expected/partition_aggregate.out
index 5f2c0cf5786..cb12bf53719 100644
--- a/src/test/regress/expected/partition_aggregate.out
+++ b/src/test/regress/expected/partition_aggregate.out
@@ -146,13 +146,14 @@ SELECT c, a, count(*) FROM pagg_tab GROUP BY a, c;
 -- Test when input relation for grouping is dummy
 EXPLAIN (COSTS OFF)
 SELECT c, sum(a) FROM pagg_tab WHERE 1 = 2 GROUP BY c;
-           QUERY PLAN           
---------------------------------
+             QUERY PLAN             
+------------------------------------
  HashAggregate
    Group Key: c
    ->  Result
+         Replaces: Scan on pagg_tab
          One-Time Filter: false
-(4 rows)
+(5 rows)
 
 SELECT c, sum(a) FROM pagg_tab WHERE 1 = 2 GROUP BY c;
  c | sum 
@@ -161,12 +162,13 @@ SELECT c, sum(a) FROM pagg_tab WHERE 1 = 2 GROUP BY c;
 
 EXPLAIN (COSTS OFF)
 SELECT c, sum(a) FROM pagg_tab WHERE c = 'x' GROUP BY c;
-           QUERY PLAN           
---------------------------------
+             QUERY PLAN             
+------------------------------------
  GroupAggregate
    ->  Result
+         Replaces: Scan on pagg_tab
          One-Time Filter: false
-(3 rows)
+(4 rows)
 
 SELECT c, sum(a) FROM pagg_tab WHERE c = 'x' GROUP BY c;
  c | sum 
@@ -804,15 +806,16 @@ SELECT a.x, b.y, count(*) FROM (SELECT * FROM pagg_tab1 WHERE x < 20) a FULL JOI
 -- Empty join relation because of empty outer side, no partitionwise agg plan
 EXPLAIN (COSTS OFF)
 SELECT a.x, a.y, count(*) FROM (SELECT * FROM pagg_tab1 WHERE x = 1 AND x = 2) a LEFT JOIN pagg_tab2 b ON a.x = b.y GROUP BY a.x, a.y ORDER BY 1, 2;
-              QUERY PLAN              
---------------------------------------
+                  QUERY PLAN                  
+----------------------------------------------
  GroupAggregate
    Group Key: pagg_tab1.y
    ->  Sort
          Sort Key: pagg_tab1.y
          ->  Result
+               Replaces: Join on b, pagg_tab1
                One-Time Filter: false
-(6 rows)
+(7 rows)
 
 SELECT a.x, a.y, count(*) FROM (SELECT * FROM pagg_tab1 WHERE x = 1 AND x = 2) a LEFT JOIN pagg_tab2 b ON a.x = b.y GROUP BY a.x, a.y ORDER BY 1, 2;
  x | y | count 
diff --git a/src/test/regress/expected/partition_join.out b/src/test/regress/expected/partition_join.out
index 24e06845f92..13fda3f32ef 100644
--- a/src/test/regress/expected/partition_join.out
+++ b/src/test/regress/expected/partition_join.out
@@ -1609,19 +1609,21 @@ SELECT avg(t1.a), avg(t2.b), avg(t3.a + t3.b), t1.c, t2.c, t3.c FROM plt1 t1, pl
 -- joins where one of the relations is proven empty
 EXPLAIN (COSTS OFF)
 SELECT t1.a, t1.c, t2.b, t2.c FROM prt1 t1, prt2 t2 WHERE t1.a = t2.b AND t1.a = 1 AND t1.a = 2;
-        QUERY PLAN        
---------------------------
+         QUERY PLAN         
+----------------------------
  Result
+   Replaces: Join on t1, t2
    One-Time Filter: false
-(2 rows)
+(3 rows)
 
 EXPLAIN (COSTS OFF)
 SELECT t1.a, t1.c, t2.b, t2.c FROM (SELECT * FROM prt1 WHERE a = 1 AND a = 2) t1 LEFT JOIN prt2 t2 ON t1.a = t2.b;
-        QUERY PLAN        
---------------------------
+          QUERY PLAN          
+------------------------------
  Result
+   Replaces: Join on t2, prt1
    One-Time Filter: false
-(2 rows)
+(3 rows)
 
 EXPLAIN (COSTS OFF)
 SELECT t1.a, t1.c, t2.b, t2.c FROM (SELECT * FROM prt1 WHERE a = 1 AND a = 2) t1 RIGHT JOIN prt2 t2 ON t1.a = t2.b, prt1 t3 WHERE t2.b = t3.a;
@@ -1647,8 +1649,9 @@ SELECT t1.a, t1.c, t2.b, t2.c FROM (SELECT * FROM prt1 WHERE a = 1 AND a = 2) t1
                      ->  Seq Scan on prt2_p3 t2_3
    ->  Hash
          ->  Result
+               Replaces: Scan on prt1
                One-Time Filter: false
-(21 rows)
+(22 rows)
 
 EXPLAIN (COSTS OFF)
 SELECT t1.a, t1.c, t2.b, t2.c FROM (SELECT * FROM prt1 WHERE a = 1 AND a = 2) t1 FULL JOIN prt2 t2 ON t1.a = t2.b WHERE t2.a = 0 ORDER BY t1.a, t2.b;
@@ -1667,8 +1670,9 @@ SELECT t1.a, t1.c, t2.b, t2.c FROM (SELECT * FROM prt1 WHERE a = 1 AND a = 2) t1
                      Filter: (a = 0)
          ->  Hash
                ->  Result
+                     Replaces: Scan on prt1
                      One-Time Filter: false
-(14 rows)
+(15 rows)
 
 --
 -- tests for hash partitioned tables.
@@ -2254,8 +2258,9 @@ SELECT t1.a, t1.c, t2.b, t2.c FROM (SELECT * FROM prt1_l WHERE a = 1 AND a = 2)
          ->  Seq Scan on prt2_l_p3_p2 t2_5
    ->  Hash
          ->  Result
+               Replaces: Scan on prt1_l
                One-Time Filter: false
-(11 rows)
+(12 rows)
 
 -- Test case to verify proper handling of subqueries in a partitioned delete.
 -- The weird-looking lateral join is just there to force creation of a
diff --git a/src/test/regress/expected/partition_prune.out b/src/test/regress/expected/partition_prune.out
index 68ecd951809..7499cdb2cdf 100644
--- a/src/test/regress/expected/partition_prune.out
+++ b/src/test/regress/expected/partition_prune.out
@@ -627,8 +627,9 @@ explain (costs off) select * from rlp3 where a = 20;   /* empty */
         QUERY PLAN        
 --------------------------
  Result
+   Replaces: Scan on rlp3
    One-Time Filter: false
-(2 rows)
+(3 rows)
 
 -- redundant clauses are eliminated
 explain (costs off) select * from rlp where a > 1 and a = 10;	/* only default */
@@ -670,8 +671,9 @@ explain (costs off) select * from rlp where a = 1 and a = 3;	/* empty */
         QUERY PLAN        
 --------------------------
  Result
+   Replaces: Scan on rlp
    One-Time Filter: false
-(2 rows)
+(3 rows)
 
 explain (costs off) select * from rlp where (a = 1 and a = 3) or (a > 1 and a = 15);
                             QUERY PLAN                             
@@ -1254,25 +1256,28 @@ select * from boolpart where a is not unknown;
 
 -- check that all partitions are pruned when faced with conflicting clauses
 explain (costs off) select * from boolpart where a is not unknown and a is unknown;
-        QUERY PLAN        
---------------------------
+          QUERY PLAN          
+------------------------------
  Result
+   Replaces: Scan on boolpart
    One-Time Filter: false
-(2 rows)
+(3 rows)
 
 explain (costs off) select * from boolpart where a is false and a is unknown;
-        QUERY PLAN        
---------------------------
+          QUERY PLAN          
+------------------------------
  Result
+   Replaces: Scan on boolpart
    One-Time Filter: false
-(2 rows)
+(3 rows)
 
 explain (costs off) select * from boolpart where a is true and a is unknown;
-        QUERY PLAN        
---------------------------
+          QUERY PLAN          
+------------------------------
  Result
+   Replaces: Scan on boolpart
    One-Time Filter: false
-(2 rows)
+(3 rows)
 
 -- inverse boolean partitioning - a seemingly unlikely design, but we've got
 -- code for it, so we'd better test it.
@@ -1568,11 +1573,12 @@ explain (costs off) select * from coercepart where a = any ('{ab,null}');
 (2 rows)
 
 explain (costs off) select * from coercepart where a = any (null::text[]);
-        QUERY PLAN        
---------------------------
+           QUERY PLAN           
+--------------------------------
  Result
+   Replaces: Scan on coercepart
    One-Time Filter: false
-(2 rows)
+(3 rows)
 
 explain (costs off) select * from coercepart where a = all ('{ab}');
                   QUERY PLAN                  
@@ -1582,25 +1588,28 @@ explain (costs off) select * from coercepart where a = all ('{ab}');
 (2 rows)
 
 explain (costs off) select * from coercepart where a = all ('{ab,bc}');
-        QUERY PLAN        
---------------------------
+           QUERY PLAN           
+--------------------------------
  Result
+   Replaces: Scan on coercepart
    One-Time Filter: false
-(2 rows)
+(3 rows)
 
 explain (costs off) select * from coercepart where a = all ('{ab,null}');
-        QUERY PLAN        
---------------------------
+           QUERY PLAN           
+--------------------------------
  Result
+   Replaces: Scan on coercepart
    One-Time Filter: false
-(2 rows)
+(3 rows)
 
 explain (costs off) select * from coercepart where a = all (null::text[]);
-        QUERY PLAN        
---------------------------
+           QUERY PLAN           
+--------------------------------
  Result
+   Replaces: Scan on coercepart
    One-Time Filter: false
-(2 rows)
+(3 rows)
 
 drop table coercepart;
 CREATE TABLE part (a INT, b INT) PARTITION BY LIST (a);
@@ -1772,8 +1781,9 @@ explain (costs off) select * from lp where a <> 'a' and a is null;
         QUERY PLAN        
 --------------------------
  Result
+   Replaces: Scan on lp
    One-Time Filter: false
-(2 rows)
+(3 rows)
 
 explain (costs off) select * from lp where (a <> 'a' and a <> 'd') or a is null;
                                   QUERY PLAN                                  
@@ -1866,22 +1876,24 @@ create table lparted_by_int2 (a smallint) partition by list (a);
 create table lparted_by_int2_1 partition of lparted_by_int2 for values in (1);
 create table lparted_by_int2_16384 partition of lparted_by_int2 for values in (16384);
 explain (costs off) select * from lparted_by_int2 where a = 100_000_000_000_000;
-        QUERY PLAN        
---------------------------
+             QUERY PLAN              
+-------------------------------------
  Result
+   Replaces: Scan on lparted_by_int2
    One-Time Filter: false
-(2 rows)
+(3 rows)
 
 create table rparted_by_int2 (a smallint) partition by range (a);
 create table rparted_by_int2_1 partition of rparted_by_int2 for values from (1) to (10);
 create table rparted_by_int2_16384 partition of rparted_by_int2 for values from (10) to (16384);
 -- all partitions pruned
 explain (costs off) select * from rparted_by_int2 where a > 100_000_000_000_000;
-        QUERY PLAN        
---------------------------
+             QUERY PLAN              
+-------------------------------------
  Result
+   Replaces: Scan on rparted_by_int2
    One-Time Filter: false
-(2 rows)
+(3 rows)
 
 create table rparted_by_int2_maxvalue partition of rparted_by_int2 for values from (16384) to (maxvalue);
 -- all partitions but rparted_by_int2_maxvalue pruned
@@ -2131,8 +2143,9 @@ explain (costs off) select * from hp where a = 1 and b = 'abcde' and
         QUERY PLAN        
 --------------------------
  Result
+   Replaces: Scan on hp
    One-Time Filter: false
-(2 rows)
+(3 rows)
 
 --
 -- Test runtime partition pruning
@@ -3390,11 +3403,12 @@ select * from stable_qual_pruning where a < '2000-02-01'::timestamptz;
 explain (analyze, costs off, summary off, timing off, buffers off)
 select * from stable_qual_pruning
   where a = any(array['2010-02-01', '2020-01-01']::timestamp[]);
-            QUERY PLAN             
------------------------------------
+               QUERY PLAN                
+-----------------------------------------
  Result (actual rows=0.00 loops=1)
+   Replaces: Scan on stable_qual_pruning
    One-Time Filter: false
-(2 rows)
+(3 rows)
 
 explain (analyze, costs off, summary off, timing off, buffers off)
 select * from stable_qual_pruning
@@ -3642,6 +3656,7 @@ explain (analyze, costs off, summary off, timing off, buffers off) select * from
    Sort Key: ma_test.b
    InitPlan 2
      ->  Result (actual rows=1.00 loops=1)
+           Replaces: MinMaxAggregate
            InitPlan 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)
@@ -3656,7 +3671,7 @@ explain (analyze, costs off, summary off, timing off, buffers off) select * from
    ->  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)
          Index Searches: 1
-(18 rows)
+(19 rows)
 
 reset enable_seqscan;
 reset enable_sort;
@@ -3678,11 +3693,12 @@ explain (costs off) select * from pp_arrpart where a = '{1}';
 (2 rows)
 
 explain (costs off) select * from pp_arrpart where a = '{1, 2}';
-        QUERY PLAN        
---------------------------
+           QUERY PLAN           
+--------------------------------
  Result
+   Replaces: Scan on pp_arrpart
    One-Time Filter: false
-(2 rows)
+(3 rows)
 
 explain (costs off) select * from pp_arrpart where a in ('{4, 5}', '{1}');
                               QUERY PLAN                              
@@ -3764,11 +3780,12 @@ explain (costs off) select * from pp_enumpart where a = 'blue';
 (2 rows)
 
 explain (costs off) select * from pp_enumpart where a = 'black';
-        QUERY PLAN        
---------------------------
+           QUERY PLAN            
+---------------------------------
  Result
+   Replaces: Scan on pp_enumpart
    One-Time Filter: false
-(2 rows)
+(3 rows)
 
 drop table pp_enumpart;
 drop type pp_colors;
@@ -3785,11 +3802,12 @@ explain (costs off) select * from pp_recpart where a = '(1,1)'::pp_rectype;
 (2 rows)
 
 explain (costs off) select * from pp_recpart where a = '(1,2)'::pp_rectype;
-        QUERY PLAN        
---------------------------
+           QUERY PLAN           
+--------------------------------
  Result
+   Replaces: Scan on pp_recpart
    One-Time Filter: false
-(2 rows)
+(3 rows)
 
 drop table pp_recpart;
 drop type pp_rectype;
@@ -3805,11 +3823,12 @@ explain (costs off) select * from pp_intrangepart where a = '[1,2]'::int4range;
 (2 rows)
 
 explain (costs off) select * from pp_intrangepart where a = '(1,2)'::int4range;
-        QUERY PLAN        
---------------------------
+             QUERY PLAN              
+-------------------------------------
  Result
+   Replaces: Scan on pp_intrangepart
    One-Time Filter: false
-(2 rows)
+(3 rows)
 
 drop table pp_intrangepart;
 --
@@ -4125,19 +4144,21 @@ explain (costs off) update listp1 set a = 1 where a = 2;
 -- constraint exclusion enabled
 set constraint_exclusion to 'on';
 explain (costs off) select * from listp1 where a = 2;
-        QUERY PLAN        
---------------------------
+         QUERY PLAN         
+----------------------------
  Result
+   Replaces: Scan on listp1
    One-Time Filter: false
-(2 rows)
+(3 rows)
 
 explain (costs off) update listp1 set a = 1 where a = 2;
-           QUERY PLAN           
---------------------------------
+            QUERY PLAN            
+----------------------------------
  Update on listp1
    ->  Result
+         Replaces: Scan on listp1
          One-Time Filter: false
-(3 rows)
+(4 rows)
 
 reset constraint_exclusion;
 reset enable_partition_pruning;
@@ -4524,18 +4545,20 @@ create table hp_contradict_test (a int, b int) partition by hash (a part_test_in
 create table hp_contradict_test_p1 partition of hp_contradict_test for values with (modulus 2, remainder 0);
 create table hp_contradict_test_p2 partition of hp_contradict_test for values with (modulus 2, remainder 1);
 explain (costs off) select * from hp_contradict_test where a is null and a === 1 and b === 1;
-        QUERY PLAN        
---------------------------
+               QUERY PLAN               
+----------------------------------------
  Result
+   Replaces: Scan on hp_contradict_test
    One-Time Filter: false
-(2 rows)
+(3 rows)
 
 explain (costs off) select * from hp_contradict_test where a === 1 and b === 1 and a is null;
-        QUERY PLAN        
---------------------------
+               QUERY PLAN               
+----------------------------------------
  Result
+   Replaces: Scan on hp_contradict_test
    One-Time Filter: false
-(2 rows)
+(3 rows)
 
 drop table hp_contradict_test;
 drop operator class part_test_int4_ops2 using hash;
diff --git a/src/test/regress/expected/predicate.out b/src/test/regress/expected/predicate.out
index 1aff0b59ff8..304b6868b90 100644
--- a/src/test/regress/expected/predicate.out
+++ b/src/test/regress/expected/predicate.out
@@ -36,8 +36,9 @@ SELECT * FROM pred_tab t WHERE t.a IS NULL;
         QUERY PLAN        
 --------------------------
  Result
+   Replaces: Scan on t
    One-Time Filter: false
-(2 rows)
+(3 rows)
 
 -- Ensure the IS_NULL qual is not reduced to constant-FALSE on nullable
 -- columns
@@ -77,8 +78,9 @@ SELECT * FROM pred_tab t WHERE t.a IS NULL OR t.c IS NULL;
         QUERY PLAN        
 --------------------------
  Result
+   Replaces: Scan on t
    One-Time Filter: false
-(2 rows)
+(3 rows)
 
 -- Ensure the OR clause is not reduced to constant-FALSE when not all branches
 -- are provably false
@@ -139,8 +141,9 @@ SELECT * FROM pred_tab t1
    Join Filter: false
    ->  Seq Scan on pred_tab t1
    ->  Result
+         Replaces: Scan on t2
          One-Time Filter: false
-(5 rows)
+(6 rows)
 
 -- Ensure the IS_NULL qual is not reduced to constant-FALSE when the column is
 -- nullable by an outer join
@@ -209,8 +212,9 @@ SELECT * FROM pred_tab t1
    Join Filter: false
    ->  Seq Scan on pred_tab t1
    ->  Result
+         Replaces: Scan on t2
          One-Time Filter: false
-(5 rows)
+(6 rows)
 
 -- Ensure the OR clause is not reduced to constant-FALSE when a column is
 -- made nullable from an outer join
@@ -267,17 +271,18 @@ SELECT * FROM pred_tab t1
     LEFT JOIN pred_tab t2 ON EXISTS
         (SELECT 1 FROM pred_tab t3, pred_tab t4, pred_tab t5, pred_tab t6
          WHERE t1.a = t3.a AND t6.a IS NULL);
-             QUERY PLAN              
--------------------------------------
+                 QUERY PLAN                 
+--------------------------------------------
  Nested Loop Left Join
    Join Filter: (InitPlan 1).col1
    InitPlan 1
      ->  Result
+           Replaces: Join on t3, t4, t5, t6
            One-Time Filter: false
    ->  Seq Scan on pred_tab t1
    ->  Materialize
          ->  Seq Scan on pred_tab t2
-(8 rows)
+(9 rows)
 
 DROP TABLE pred_tab;
 -- Validate we handle IS NULL and IS NOT NULL quals correctly with inheritance
@@ -418,20 +423,22 @@ SET constraint_exclusion TO ON;
 -- Ensure that we get a dummy plan
 EXPLAIN (COSTS OFF)
 SELECT * FROM pred_tab1, pred_tab2 WHERE pred_tab2.a IS NULL;
-        QUERY PLAN        
---------------------------
+                QUERY PLAN                
+------------------------------------------
  Result
+   Replaces: Join on pred_tab1, pred_tab2
    One-Time Filter: false
-(2 rows)
+(3 rows)
 
 -- Ensure that we get a dummy plan
 EXPLAIN (COSTS OFF)
 SELECT * FROM pred_tab2, pred_tab1 WHERE pred_tab1.a IS NULL OR pred_tab1.b < 2;
-        QUERY PLAN        
---------------------------
+                QUERY PLAN                
+------------------------------------------
  Result
+   Replaces: Join on pred_tab2, pred_tab1
    One-Time Filter: false
-(2 rows)
+(3 rows)
 
 RESET constraint_exclusion;
 DROP TABLE pred_tab1;
diff --git a/src/test/regress/expected/rowsecurity.out b/src/test/regress/expected/rowsecurity.out
index 8c879509313..7153ebba521 100644
--- a/src/test/regress/expected/rowsecurity.out
+++ b/src/test/regress/expected/rowsecurity.out
@@ -3606,8 +3606,9 @@ EXPLAIN (COSTS OFF) SELECT * FROM t1;
         QUERY PLAN        
 --------------------------
  Result
+   Replaces: Scan on t1
    One-Time Filter: false
-(2 rows)
+(3 rows)
 
 SET SESSION AUTHORIZATION regress_rls_bob;
 SELECT * FROM t1;
@@ -3619,8 +3620,9 @@ EXPLAIN (COSTS OFF) SELECT * FROM t1;
         QUERY PLAN        
 --------------------------
  Result
+   Replaces: Scan on t1
    One-Time Filter: false
-(2 rows)
+(3 rows)
 
 --
 -- COPY TO/FROM
@@ -4524,11 +4526,12 @@ SELECT * FROM rls_tbl WHERE a <<< 1000;
 (0 rows)
 
 EXPLAIN (COSTS OFF) SELECT * FROM rls_tbl WHERE a <<< 1000 or a <<< 900;
-        QUERY PLAN        
---------------------------
+         QUERY PLAN          
+-----------------------------
  Result
+   Replaces: Scan on rls_tbl
    One-Time Filter: false
-(2 rows)
+(3 rows)
 
 RESET SESSION AUTHORIZATION;
 CREATE TABLE rls_child_tbl () INHERITS (rls_tbl);
diff --git a/src/test/regress/expected/rowtypes.out b/src/test/regress/expected/rowtypes.out
index 9168979a620..40b94a82ad9 100644
--- a/src/test/regress/expected/rowtypes.out
+++ b/src/test/regress/expected/rowtypes.out
@@ -1323,8 +1323,9 @@ where false;
 --------------------------
  Result
    Output: (a).f1, (a).f2
+   Replaces: Scan on ss
    One-Time Filter: false
-(3 rows)
+(4 rows)
 
 explain (verbose, costs off)
 with cte(c) as materialized (select row(1, 2)),
@@ -1348,11 +1349,12 @@ where false;
 -----------------------------------
  Result
    Output: (cte.c).f1
+   Replaces: Scan on cte
    One-Time Filter: false
    CTE cte
      ->  Result
            Output: '(1,2)'::record
-(6 rows)
+(7 rows)
 
 --
 -- Tests for component access / FieldSelect
diff --git a/src/test/regress/expected/select.out b/src/test/regress/expected/select.out
index bab0cc93ff5..2cdb78d1e9a 100644
--- a/src/test/regress/expected/select.out
+++ b/src/test/regress/expected/select.out
@@ -962,10 +962,11 @@ create table list_parted_tbl (a int,b int) partition by list (a);
 create table list_parted_tbl1 partition of list_parted_tbl
   for values in (1) partition by list(b);
 explain (costs off) select * from list_parted_tbl;
-        QUERY PLAN        
---------------------------
+             QUERY PLAN              
+-------------------------------------
  Result
+   Replaces: Scan on list_parted_tbl
    One-Time Filter: false
-(2 rows)
+(3 rows)
 
 drop table list_parted_tbl;
diff --git a/src/test/regress/expected/subselect.out b/src/test/regress/expected/subselect.out
index 307e5ca1f3d..065ae550918 100644
--- a/src/test/regress/expected/subselect.out
+++ b/src/test/regress/expected/subselect.out
@@ -2006,8 +2006,9 @@ select * from json_tab t1 left join (select json_array(1, a) from json_tab t2) s
          Output: t1.a
    ->  Result
          Output: JSON_ARRAY(1, a RETURNING json)
+         Replaces: Scan on t2
          One-Time Filter: false
-(8 rows)
+(9 rows)
 
 select * from json_tab t1 left join (select json_array(1, a) from json_tab t2) s on false;
  a | json_array 
@@ -2898,12 +2899,13 @@ ON B.hundred in (SELECT min(c.hundred) FROM tenk2 C WHERE c.odd = b.odd);
                      ->  Subquery Scan on unnamed_subquery
                            Filter: (b.hundred = unnamed_subquery.min)
                            ->  Result
+                                 Replaces: MinMaxAggregate
                                  InitPlan 1
                                    ->  Limit
                                          ->  Index Scan using tenk2_hundred on tenk2 c
                                                Index Cond: (hundred IS NOT NULL)
                                                Filter: (odd = b.odd)
-(16 rows)
+(17 rows)
 
 --
 -- Test VALUES to ARRAY (VtA) transformation
@@ -3066,8 +3068,9 @@ EXPLAIN (COSTS OFF) EXECUTE test(NULL, 3.14, NULL);
         QUERY PLAN        
 --------------------------
  Result
+   Replaces: Scan on onek
    One-Time Filter: false
-(2 rows)
+(3 rows)
 
 EXPLAIN (COSTS OFF) EXECUTE test(NULL, 3.14, '-1.5');
                                                                      QUERY PLAN                                                                      
diff --git a/src/test/regress/expected/tsrf.out b/src/test/regress/expected/tsrf.out
index d47b5f6ec57..c4f7b187f5b 100644
--- a/src/test/regress/expected/tsrf.out
+++ b/src/test/regress/expected/tsrf.out
@@ -91,8 +91,9 @@ SELECT unnest(ARRAY[1, 2]) FROM few WHERE false;
  ProjectSet
    Output: unnest('{1,2}'::integer[])
    ->  Result
+         Replaces: Scan on few
          One-Time Filter: false
-(4 rows)
+(5 rows)
 
 SELECT unnest(ARRAY[1, 2]) FROM few WHERE false;
  unnest 
@@ -107,8 +108,9 @@ SELECT * FROM few f1,
 ------------------------------------------------
  Result
    Output: f1.id, f1.dataa, f1.datab, ss.unnest
+   Replaces: Join on f1, ss
    One-Time Filter: false
-(3 rows)
+(4 rows)
 
 SELECT * FROM few f1,
   (SELECT unnest(ARRAY[1,2]) FROM few f2 WHERE false OFFSET 0) ss;
diff --git a/src/test/regress/sql/explain.sql b/src/test/regress/sql/explain.sql
index 7842d25ded3..ebdab42604b 100644
--- a/src/test/regress/sql/explain.sql
+++ b/src/test/regress/sql/explain.sql
@@ -61,6 +61,7 @@ set track_io_timing = off;
 
 -- Simple cases
 
+explain (costs off) select 1 as a, 2 as b having false;
 select explain_filter('explain select * from int8_tbl i8');
 select explain_filter('explain (analyze, buffers off) select * from int8_tbl i8');
 select explain_filter('explain (analyze, buffers off, verbose) select * from int8_tbl i8');
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index e90af5b2ad3..3c80d49b67e 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2568,6 +2568,7 @@ RestrictInfo
 Result
 ResultRelInfo
 ResultState
+ResultType
 RetainDeadTuplesData
 RetainDeadTuplesPhase
 ReturnSetInfo
-- 
2.39.5 (Apple Git-154)

v6-0003-Ensure-that-all-joinrel-RTIs-are-discoverable-fro.patchapplication/octet-stream; name=v6-0003-Ensure-that-all-joinrel-RTIs-are-discoverable-fro.patchDownload
From 111e9bdc610f0e79da8c78c34b91dd3d2fadf1a1 Mon Sep 17 00:00:00 2001
From: Robert Haas <rhaas@postgresql.org>
Date: Tue, 16 Sep 2025 10:05:02 -0400
Subject: [PATCH v6 3/3] 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.

We also now assert when constructing a Join plan that we can find all of
the joinrel's RTIs and no others in the plan tree -- either associated
with a node below the outer or inner side of the join, or in the
'ojrelids' field of the Join itself. Any RTIs appearing in this last
place must be of type RTE_JOIN; scanned relations need to be associated
with underlying scan nodes.

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.

Note that this depends on the earlier commit to add a relids field to
Result nodes; without that change, a join tree involving two or more
Result nodes would be fundamentally ambiguous (and even a join tree
involving one could only be interpreted by guessing at its origin).
---
 .../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       | 200 +++++++++++++++++-
 src/include/nodes/plannodes.h                 |   2 +
 5 files changed, 272 insertions(+), 5 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..6fa782d7c58 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -161,6 +161,7 @@ static CustomScan *create_customscan_plan(PlannerInfo *root,
 static NestLoop *create_nestloop_plan(PlannerInfo *root, NestPath *best_path);
 static MergeJoin *create_mergejoin_plan(PlannerInfo *root, MergePath *best_path);
 static HashJoin *create_hashjoin_plan(PlannerInfo *root, HashPath *best_path);
+static Bitmapset *compute_join_ojrelids(PlannerInfo *root, JoinPath *jpath);
 static Node *replace_nestloop_params(PlannerInfo *root, Node *expr);
 static Node *replace_nestloop_params_mutator(Node *node, PlannerInfo *root);
 static void fix_indexqual_references(PlannerInfo *root, IndexPath *index_path,
@@ -232,6 +233,7 @@ static BitmapOr *make_bitmap_or(List *bitmapplans);
 static NestLoop *make_nestloop(List *tlist,
 							   List *joinclauses, List *otherclauses, List *nestParams,
 							   Plan *lefttree, Plan *righttree,
+							   Bitmapset *ojrelids,
 							   JoinType jointype, bool inner_unique);
 static HashJoin *make_hashjoin(List *tlist,
 							   List *joinclauses, List *otherclauses,
@@ -239,6 +241,7 @@ static HashJoin *make_hashjoin(List *tlist,
 							   List *hashoperators, List *hashcollations,
 							   List *hashkeys,
 							   Plan *lefttree, Plan *righttree,
+							   Bitmapset *ojrelids,
 							   JoinType jointype, bool inner_unique);
 static Hash *make_hash(Plan *lefttree,
 					   List *hashkeys,
@@ -253,6 +256,7 @@ static MergeJoin *make_mergejoin(List *tlist,
 								 bool *mergereversals,
 								 bool *mergenullsfirst,
 								 Plan *lefttree, Plan *righttree,
+								 Bitmapset *ojrelids,
 								 JoinType jointype, bool inner_unique,
 								 bool skip_mark_restore);
 static Sort *make_sort(Plan *lefttree, int numCols,
@@ -320,7 +324,15 @@ static ModifyTable *make_modifytable(PlannerInfo *root, Plan *subplan,
 									 int epqParam);
 static GatherMerge *create_gather_merge_plan(PlannerInfo *root,
 											 GatherMergePath *best_path);
-
+static void assert_join_preserves_rtis(PlannerInfo *root,
+									   RelOptInfo *rel,
+									   Plan *outer_plan,
+									   Plan *inner_plan,
+									   Bitmapset *ojrelids);
+
+#ifdef USE_ASSERT_CHECKING
+static Bitmapset *get_plan_rtis(PlannerInfo *root, Plan *plan);
+#endif
 
 /*
  * create_plan
@@ -4335,11 +4347,16 @@ create_nestloop_plan(PlannerInfo *root,
 							  nestParams,
 							  outer_plan,
 							  inner_plan,
+							  compute_join_ojrelids(root, &best_path->jpath),
 							  best_path->jpath.jointype,
 							  best_path->jpath.inner_unique);
 
 	copy_generic_path_info(&join_plan->join.plan, &best_path->jpath.path);
 
+	assert_join_preserves_rtis(root, best_path->jpath.path.parent,
+							   outer_plan, inner_plan,
+							   join_plan->join.ojrelids);
+
 	return join_plan;
 }
 
@@ -4687,6 +4704,7 @@ create_mergejoin_plan(PlannerInfo *root,
 							   mergenullsfirst,
 							   outer_plan,
 							   inner_plan,
+							   compute_join_ojrelids(root, &best_path->jpath),
 							   best_path->jpath.jointype,
 							   best_path->jpath.inner_unique,
 							   best_path->skip_mark_restore);
@@ -4694,6 +4712,10 @@ create_mergejoin_plan(PlannerInfo *root,
 	/* Costs of sort and material steps are included in path cost already */
 	copy_generic_path_info(&join_plan->join.plan, &best_path->jpath.path);
 
+	assert_join_preserves_rtis(root, best_path->jpath.path.parent,
+							   outer_plan, inner_plan,
+							   join_plan->join.ojrelids);
+
 	return join_plan;
 }
 
@@ -4862,14 +4884,71 @@ create_hashjoin_plan(PlannerInfo *root,
 							  outer_hashkeys,
 							  outer_plan,
 							  (Plan *) hash_plan,
+							  compute_join_ojrelids(root, &best_path->jpath),
 							  best_path->jpath.jointype,
 							  best_path->jpath.inner_unique);
 
 	copy_generic_path_info(&join_plan->join.plan, &best_path->jpath.path);
 
+	assert_join_preserves_rtis(root, best_path->jpath.path.parent,
+							   outer_plan, inner_plan,
+							   join_plan->join.ojrelids);
+
 	return join_plan;
 }
 
+/*
+ * compute_join_ojrelids
+ *	  Determine the set of outer joins completed by this join.
+ *
+ * See add_outer_joins_to_relids for related logic. When we execute joins
+ * in syntactic order, this will compute a 1-item RTI set for outer joins
+ * and the empty set for inner joins. When we rearrange the join order using
+ * outer-join identity 3, the outer join isn't fully calculated until the
+ * commuted join has also been computed, so an upper outer join can complete
+ * multiple outer joins while a lower one completes none.
+ */
+static Bitmapset *
+compute_join_ojrelids(PlannerInfo *root, JoinPath *jpath)
+{
+	Bitmapset  *ojrelids;
+
+	ojrelids = jpath->path.parent->relids;
+	ojrelids = bms_difference(ojrelids, jpath->outerjoinpath->parent->relids);
+	ojrelids = bms_difference(ojrelids, jpath->innerjoinpath->parent->relids);
+
+#ifdef USE_ASSERT_CHECKING
+	switch (jpath->jointype)
+	{
+		case JOIN_INNER:
+			/* Inner joins should never complete outer joins. */
+			Assert(ojrelids == NULL);
+			break;
+		case JOIN_FULL:
+			/* Full outer joins cannot be commuted. */
+			Assert(bms_membership(ojrelids) == BMS_SINGLETON);
+			break;
+		default:
+			/* Other types of joins can be rearranged. */
+			break;
+	}
+
+	/* Any RTIs in the ojrelids set should be of type RTE_JOIN. */
+	if (ojrelids != NULL)
+	{
+		int			rti = -1;
+
+		while ((rti = bms_next_member(ojrelids, rti)) >= 0)
+		{
+			RangeTblEntry *rte = planner_rt_fetch(rti, root);
+
+			Assert(rte->rtekind == RTE_JOIN);
+		}
+	}
+#endif
+
+	return ojrelids;
+}
 
 /*****************************************************************************
  *
@@ -5934,6 +6013,7 @@ make_nestloop(List *tlist,
 			  List *nestParams,
 			  Plan *lefttree,
 			  Plan *righttree,
+			  Bitmapset *ojrelids,
 			  JoinType jointype,
 			  bool inner_unique)
 {
@@ -5947,6 +6027,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;
@@ -5962,6 +6043,7 @@ make_hashjoin(List *tlist,
 			  List *hashkeys,
 			  Plan *lefttree,
 			  Plan *righttree,
+			  Bitmapset *ojrelids,
 			  JoinType jointype,
 			  bool inner_unique)
 {
@@ -5979,6 +6061,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;
 }
@@ -6017,6 +6100,7 @@ make_mergejoin(List *tlist,
 			   bool *mergenullsfirst,
 			   Plan *lefttree,
 			   Plan *righttree,
+			   Bitmapset *ojrelids,
 			   JoinType jointype,
 			   bool inner_unique,
 			   bool skip_mark_restore)
@@ -6037,6 +6121,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;
 }
@@ -7316,3 +7401,116 @@ is_projection_capable_plan(Plan *plan)
 	}
 	return true;
 }
+
+/*
+ * Check that every joinrel RTI appears in the inner or outer plan or in this
+ * Join's ojrelids set.
+ */
+static void
+assert_join_preserves_rtis(PlannerInfo *root, RelOptInfo *rel,
+						   Plan *outer_plan, Plan *inner_plan,
+						   Bitmapset *ojrelids)
+{
+#ifdef USE_ASSERT_CHECKING
+	Bitmapset  *outerrelids;
+	Bitmapset  *innerrelids;
+	Bitmapset  *joinrelids;
+
+	/* Find outer and inner relid sets. */
+	outerrelids = get_plan_rtis(root, outer_plan);
+	innerrelids = get_plan_rtis(root, inner_plan);
+
+	/* Any given scan RTI should appear in just one set. */
+	Assert(!bms_overlap(innerrelids, outerrelids));
+	Assert(!bms_overlap(outerrelids, ojrelids));
+	Assert(!bms_overlap(innerrelids, ojrelids));
+
+	/* Combine all three sets and check that all RTIs were preserved. */
+	joinrelids = bms_union(ojrelids, bms_union(innerrelids, outerrelids));
+	Assert(bms_equal(joinrelids, rel->relids));
+#endif
+}
+
+#ifdef USE_ASSERT_CHECKING
+/*
+ * Get the set of range table indexes for a scan or join node, or any executor
+ * node that could appear beneath a scan or join node.
+ *
+ * We're only interested in RTIs from within the same subquery, so we do not
+ * attempt to look through T_SubqueryScan here.
+ *
+ * When adding new cases to this function, be sure to also update
+ * ExplainPreScanNode, ExplainNode, and overexplain_per_node_hook as
+ * appropriate.
+ */
+static Bitmapset *
+get_plan_rtis(PlannerInfo *root, Plan *plan)
+{
+	switch (nodeTag(plan))
+	{
+		case T_SeqScan:
+		case T_SampleScan:
+		case T_IndexScan:
+		case T_IndexOnlyScan:
+		case T_BitmapHeapScan:
+		case T_TidScan:
+		case T_TidRangeScan:
+		case T_SubqueryScan:
+		case T_FunctionScan:
+		case T_TableFuncScan:
+		case T_ValuesScan:
+		case T_CteScan:
+		case T_NamedTuplestoreScan:
+		case T_WorkTableScan:
+			return bms_make_singleton(((Scan *) plan)->scanrelid);
+			break;
+		case T_ForeignScan:
+			return ((ForeignScan *) plan)->fs_relids;
+			break;
+		case T_CustomScan:
+			return ((CustomScan *) plan)->custom_relids;
+			break;
+		case T_Append:
+			return ((Append *) plan)->apprelids;
+			break;
+		case T_MergeAppend:
+			return ((MergeAppend *) plan)->apprelids;
+			break;
+		case T_Result:
+			if (plan->lefttree)
+				return get_plan_rtis(root, plan->lefttree);
+			else
+				return ((Result *) plan)->relids;
+			break;
+		case T_HashJoin:
+		case T_MergeJoin:
+		case T_NestLoop:
+			{
+				Bitmapset  *outerrelids;
+				Bitmapset  *innerrelids;
+
+				outerrelids = get_plan_rtis(root, plan->lefttree);
+				innerrelids = get_plan_rtis(root, plan->righttree);
+
+				return bms_union(bms_union(outerrelids, innerrelids),
+								 ((Join *) plan)->ojrelids);
+				break;
+			}
+		case T_Sort:
+		case T_IncrementalSort:
+		case T_Unique:
+		case T_Agg:
+		case T_Hash:
+		case T_Gather:
+		case T_GatherMerge:
+		case T_Material:
+		case T_Memoize:
+			return get_plan_rtis(root, plan->lefttree);
+			break;
+		default:
+			break;
+	}
+
+	return NULL;
+}
+#endif
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.39.5 (Apple Git-154)

#49Robert Haas
robertmhaas@gmail.com
In reply to: Robert Haas (#48)
Re: plan shape work

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

#50Robert Haas
robertmhaas@gmail.com
In reply to: Robert Haas (#49)
7 attachment(s)
Re: plan shape work

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

Attachments:

v7-0002-Consider-a-Result-node-s-relids-in-ExplainPreScan.patchapplication/octet-stream; name=v7-0002-Consider-a-Result-node-s-relids-in-ExplainPreScan.patchDownload
From 0e416ecc8507a4a16e5646cf3365931e77fc3a33 Mon Sep 17 00:00:00 2001
From: Robert Haas <rhaas@postgresql.org>
Date: Tue, 16 Sep 2025 09:56:35 -0400
Subject: [PATCH v7 2/7] Consider a Result node's relids in ExplainPreScanNode.

Now that a Result node has a relids set, add the relids that it
carries the set accumulated by ExplainPreScanNode so that we
generate unique relation aliases for all of the referenced relations
when it calls select_rtable_names_for_explain. The effect of this
changes is that a few things get schema-qualified in the regression
test outputs that previously were not. In similar cases not involving
a Result node, we were already schema-qualifying, so this appears to
be an improvement.

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.
---
 src/backend/commands/explain.c                |  4 +++
 .../regress/expected/generated_virtual.out    |  8 ++---
 src/test/regress/expected/join.out            | 32 +++++++++----------
 src/test/regress/expected/partition_join.out  | 12 +++----
 src/test/regress/expected/subselect.out       |  8 ++---
 5 files changed, 34 insertions(+), 30 deletions(-)

diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 1de666905c8..207f86f1d39 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -1230,6 +1230,10 @@ ExplainPreScanNode(PlanState *planstate, Bitmapset **rels_used)
 			*rels_used = bms_add_members(*rels_used,
 										 ((MergeAppend *) plan)->apprelids);
 			break;
+		case T_Result:
+			*rels_used = bms_add_members(*rels_used,
+										 ((Result *) plan)->relids);
+			break;
 		default:
 			break;
 	}
diff --git a/src/test/regress/expected/generated_virtual.out b/src/test/regress/expected/generated_virtual.out
index b48e6bdbf9d..c861bd36c5a 100644
--- a/src/test/regress/expected/generated_virtual.out
+++ b/src/test/regress/expected/generated_virtual.out
@@ -1570,15 +1570,15 @@ where coalesce(t2.b, 1) = 2 or t1.a is null;
 -- Ensure that the generation expressions are wrapped into PHVs if needed
 explain (verbose, costs off)
 select t2.* from gtest32 t1 left join gtest32 t2 on false;
-                      QUERY PLAN                      
-------------------------------------------------------
+                          QUERY PLAN                           
+---------------------------------------------------------------
  Nested Loop Left Join
-   Output: a, (a * 2), (20), (COALESCE(a, 100)), e
+   Output: t2.a, (t2.a * 2), (20), (COALESCE(t2.a, 100)), t2.e
    Join Filter: false
    ->  Seq Scan on generated_virtual_tests.gtest32 t1
          Output: t1.a, t1.b, t1.c, t1.d, t1.e
    ->  Result
-         Output: a, e, 20, COALESCE(a, 100)
+         Output: t2.a, t2.e, 20, COALESCE(t2.a, 100)
          Replaces: Scan on t2
          One-Time Filter: false
 (9 rows)
diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out
index 30baa9dc51a..cd37f549b5a 100644
--- a/src/test/regress/expected/join.out
+++ b/src/test/regress/expected/join.out
@@ -4151,9 +4151,9 @@ select * from t t1
                         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)
@@ -4166,7 +4166,7 @@ select * from t t1
                      ->  Seq Scan on pg_temp.t t2
                            Output: t2.i2, 1
          ->  Result
-               Output: i3
+               Output: t3.i3
                Replaces: Scan on t3
                One-Time Filter: false
    ->  Memoize
@@ -6069,7 +6069,7 @@ on t1.q1 = t2.q1;
                  QUERY PLAN                 
 --------------------------------------------
  Hash Left Join
-   Hash Cond: (t1.q1 = q1)
+   Hash Cond: (t1.q1 = t2.q1)
    ->  Seq Scan on int8_tbl t1
    ->  Hash
          ->  Result
@@ -6086,7 +6086,7 @@ on t1.q1 = t2.q1;
                  QUERY PLAN                 
 --------------------------------------------
  Hash Left Join
-   Hash Cond: (t1.q1 = q1)
+   Hash Cond: (t1.q1 = t2.q1)
    ->  Seq Scan on int8_tbl t1
    ->  Hash
          ->  Result
@@ -6416,13 +6416,13 @@ SELECT q2 FROM
                       QUERY PLAN                      
 ------------------------------------------------------
  Nested Loop Left Join
-   Output: q2
+   Output: int8_tbl.q2
    Join Filter: NULL::boolean
    Filter: (('constant'::text) >= ('constant'::text))
    ->  Seq Scan on public.int4_tbl
          Output: int4_tbl.f1
    ->  Result
-         Output: q2, 'constant'::text
+         Output: int8_tbl.q2, 'constant'::text
          Replaces: Scan on int8_tbl
          One-Time Filter: false
 (10 rows)
@@ -6896,7 +6896,7 @@ where q1.x = q2.y;
         QUERY PLAN        
 --------------------------
  Result
-   Replaces: Scan on sj_1
+   Replaces: Scan on sj
    One-Time Filter: false
 (3 rows)
 
@@ -8887,15 +8887,15 @@ select * from int4_tbl t1,
 explain (verbose, costs off)
 select * from int8_tbl i8 left join lateral
   (select *, i8.q2 from int4_tbl where false) ss on true;
-              QUERY PLAN              
---------------------------------------
+                  QUERY PLAN                  
+----------------------------------------------
  Nested Loop Left Join
-   Output: i8.q1, i8.q2, f1, (i8.q2)
+   Output: i8.q1, i8.q2, int4_tbl.f1, (i8.q2)
    Join Filter: false
    ->  Seq Scan on public.int8_tbl i8
          Output: i8.q1, i8.q2
    ->  Result
-         Output: f1, i8.q2
+         Output: int4_tbl.f1, i8.q2
          Replaces: Scan on int4_tbl
          One-Time Filter: false
 (9 rows)
@@ -8903,14 +8903,14 @@ select * from int8_tbl i8 left join lateral
 explain (verbose, costs off)
 select * from int8_tbl i8 left join lateral
   (select *, i8.q2 from int4_tbl i1, int4_tbl i2 where false) ss on true;
-               QUERY PLAN                
------------------------------------------
+                  QUERY PLAN                   
+-----------------------------------------------
  Nested Loop Left Join
-   Output: i8.q1, i8.q2, f1, f1, (i8.q2)
+   Output: i8.q1, i8.q2, i1.f1, i2.f1, (i8.q2)
    ->  Seq Scan on public.int8_tbl i8
          Output: i8.q1, i8.q2
    ->  Result
-         Output: f1, f1, i8.q2
+         Output: i1.f1, i2.f1, i8.q2
          Replaces: Join on i1, i2
          One-Time Filter: false
 (8 rows)
diff --git a/src/test/regress/expected/partition_join.out b/src/test/regress/expected/partition_join.out
index 13fda3f32ef..713828be335 100644
--- a/src/test/regress/expected/partition_join.out
+++ b/src/test/regress/expected/partition_join.out
@@ -1630,7 +1630,7 @@ SELECT t1.a, t1.c, t2.b, t2.c FROM (SELECT * FROM prt1 WHERE a = 1 AND a = 2) t1
                     QUERY PLAN                    
 --------------------------------------------------
  Hash Left Join
-   Hash Cond: (t2.b = a)
+   Hash Cond: (t2.b = prt1.a)
    ->  Append
          ->  Hash Join
                Hash Cond: (t3_1.a = t2_1.b)
@@ -1658,9 +1658,9 @@ SELECT t1.a, t1.c, t2.b, t2.c FROM (SELECT * FROM prt1 WHERE a = 1 AND a = 2) t1
                  QUERY PLAN                 
 --------------------------------------------
  Sort
-   Sort Key: a, t2.b
+   Sort Key: prt1.a, t2.b
    ->  Hash Left Join
-         Hash Cond: (t2.b = a)
+         Hash Cond: (t2.b = prt1.a)
          ->  Append
                ->  Seq Scan on prt2_p1 t2_1
                      Filter: (a = 0)
@@ -2246,10 +2246,10 @@ SELECT COUNT(*) FROM prt1_l t1 LEFT JOIN LATERAL
 -- join with one side empty
 EXPLAIN (COSTS OFF)
 SELECT t1.a, t1.c, t2.b, t2.c FROM (SELECT * FROM prt1_l WHERE a = 1 AND a = 2) t1 RIGHT JOIN prt2_l t2 ON t1.a = t2.b AND t1.b = t2.a AND t1.c = t2.c;
-                               QUERY PLAN                                
--------------------------------------------------------------------------
+                                          QUERY PLAN                                          
+----------------------------------------------------------------------------------------------
  Hash Left Join
-   Hash Cond: ((t2.b = a) AND (t2.a = b) AND ((t2.c)::text = (c)::text))
+   Hash Cond: ((t2.b = prt1_l.a) AND (t2.a = prt1_l.b) AND ((t2.c)::text = (prt1_l.c)::text))
    ->  Append
          ->  Seq Scan on prt2_l_p1 t2_1
          ->  Seq Scan on prt2_l_p2_p1 t2_2
diff --git a/src/test/regress/expected/subselect.out b/src/test/regress/expected/subselect.out
index b91ff5e8fe9..47b2af7b2e1 100644
--- a/src/test/regress/expected/subselect.out
+++ b/src/test/regress/expected/subselect.out
@@ -1998,15 +1998,15 @@ create temp table json_tab (a int);
 insert into json_tab values (1);
 explain (verbose, costs off)
 select * from json_tab t1 left join (select json_array(1, a) from json_tab t2) s on false;
-                    QUERY PLAN                     
----------------------------------------------------
+                      QUERY PLAN                      
+------------------------------------------------------
  Nested Loop Left Join
-   Output: t1.a, (JSON_ARRAY(1, a RETURNING json))
+   Output: t1.a, (JSON_ARRAY(1, t2.a RETURNING json))
    Join Filter: false
    ->  Seq Scan on pg_temp.json_tab t1
          Output: t1.a
    ->  Result
-         Output: JSON_ARRAY(1, a RETURNING json)
+         Output: JSON_ARRAY(1, t2.a RETURNING json)
          Replaces: Scan on t2
          One-Time Filter: false
 (9 rows)
-- 
2.39.5 (Apple Git-154)

v7-0001-Keep-track-of-what-RTIs-a-Result-node-is-scanning.patchapplication/octet-stream; name=v7-0001-Keep-track-of-what-RTIs-a-Result-node-is-scanning.patchDownload
From 53bc8142c0aca568601445614268912103475ba2 Mon Sep 17 00:00:00 2001
From: Robert Haas <rhaas@postgresql.org>
Date: Tue, 16 Sep 2025 09:32:15 -0400
Subject: [PATCH v7 1/7] Keep track of what RTIs a Result node is scanning.

Result nodes now include an RTI set, which is only non-NULL when they
have no subplan, and is taken from the relid set of the RelOptInfo
that the Result is generating. They also now include a result_reason,
which tells us something about why the Result node was inserted.

Using that information, EXPLAIN now emits, where relevant, a "Replaces"
line describing the origin of the Result node.

Likewise, pg_overexplain's EXPLAIN (RANGE_TABLE) now displays the RTIs
stored in a Result node just as it already does for other RTI-bearing
node types.
---
 contrib/file_fdw/expected/file_fdw.out        |   1 +
 .../expected/pg_overexplain.out               |   6 +-
 contrib/pg_overexplain/pg_overexplain.c       |  12 ++
 .../postgres_fdw/expected/postgres_fdw.out    |   9 +-
 src/backend/commands/explain.c                |  98 ++++++++++++
 src/backend/optimizer/plan/createplan.c       | 117 ++++++++++-----
 src/backend/optimizer/plan/setrefs.c          |   2 +
 src/include/nodes/plannodes.h                 |  22 +++
 src/test/regress/expected/aggregates.out      |  51 ++++---
 src/test/regress/expected/case.out            |  21 +--
 src/test/regress/expected/explain.out         |   8 +
 .../regress/expected/generated_virtual.out    |  17 ++-
 src/test/regress/expected/groupingsets.out    |   3 +-
 src/test/regress/expected/inherit.out         |  28 ++--
 src/test/regress/expected/join.out            | 113 ++++++++------
 src/test/regress/expected/merge.out           |   3 +-
 .../regress/expected/partition_aggregate.out  |  21 +--
 src/test/regress/expected/partition_join.out  |  23 +--
 src/test/regress/expected/partition_prune.out | 141 ++++++++++--------
 src/test/regress/expected/predicate.out       |  33 ++--
 src/test/regress/expected/rowsecurity.out     |  13 +-
 src/test/regress/expected/rowtypes.out        |   6 +-
 src/test/regress/expected/select.out          |   7 +-
 src/test/regress/expected/subselect.out       |   9 +-
 src/test/regress/expected/tsrf.out            |   6 +-
 src/test/regress/sql/explain.sql              |   1 +
 src/tools/pgindent/typedefs.list              |   1 +
 27 files changed, 534 insertions(+), 238 deletions(-)

diff --git a/contrib/file_fdw/expected/file_fdw.out b/contrib/file_fdw/expected/file_fdw.out
index 246e3d3e566..5121e27dce5 100644
--- a/contrib/file_fdw/expected/file_fdw.out
+++ b/contrib/file_fdw/expected/file_fdw.out
@@ -322,6 +322,7 @@ SET constraint_exclusion = 'on';
 SELECT explain_filter('EXPLAIN (VERBOSE, COSTS FALSE) SELECT * FROM agg_csv WHERE a < 0');
  Result
    Output: a, b
+   Replaces: Scan on agg_csv
    One-Time Filter: false
 
 \t off
diff --git a/contrib/pg_overexplain/expected/pg_overexplain.out b/contrib/pg_overexplain/expected/pg_overexplain.out
index 6de02323d7c..55d34666d87 100644
--- a/contrib/pg_overexplain/expected/pg_overexplain.out
+++ b/contrib/pg_overexplain/expected/pg_overexplain.out
@@ -44,9 +44,10 @@ EXPLAIN (RANGE_TABLE) SELECT 1;
                 QUERY PLAN                
 ------------------------------------------
  Result  (cost=0.00..0.01 rows=1 width=4)
+   RTIs: 1
  RTI 1 (result):
    Eref: "*RESULT*" ()
-(3 rows)
+(4 rows)
 
 -- Create a partitioned table.
 CREATE TABLE vegetables (id serial, name text, genus text)
@@ -475,6 +476,7 @@ INSERT INTO vegetables (name, genus) VALUES ('broccoflower', 'brassica');
    Nominal RTI: 1
    Exclude Relation RTI: 0
    ->  Result
+         RTIs: 2
  RTI 1 (relation):
    Eref: vegetables (id, name, genus)
    Relation: vegetables
@@ -485,5 +487,5 @@ INSERT INTO vegetables (name, genus) VALUES ('broccoflower', 'brassica');
    Eref: "*RESULT*" ()
  Unprunable RTIs: 1
  Result RTIs: 1
-(14 rows)
+(15 rows)
 
diff --git a/contrib/pg_overexplain/pg_overexplain.c b/contrib/pg_overexplain/pg_overexplain.c
index de824566f8c..bd70b6d9d5e 100644
--- a/contrib/pg_overexplain/pg_overexplain.c
+++ b/contrib/pg_overexplain/pg_overexplain.c
@@ -236,6 +236,18 @@ overexplain_per_node_hook(PlanState *planstate, List *ancestors,
 									  ((MergeAppend *) plan)->apprelids,
 									  es);
 				break;
+			case T_Result:
+
+				/*
+				 * 'relids' is only meaningful when plan->lefttree is NULL,
+				 * but if somehow it ends up set when plan->lefttree is not
+				 * NULL, print it anyway.
+				 */
+				if (plan->lefttree == NULL ||
+					((Result *) plan)->relids != NULL)
+					overexplain_bitmapset("RTIs",
+										  ((Result *) plan)->relids,
+										  es);
 			default:
 				break;
 		}
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index cb74c87d1aa..6dc04e916dc 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -7159,8 +7159,9 @@ EXPLAIN (VERBOSE, COSTS OFF) SELECT count(*) FROM ft1 WHERE c2 < 0;
  Aggregate
    Output: count(*)
    ->  Result
+         Replaces: Scan on ft1
          One-Time Filter: false
-(4 rows)
+(5 rows)
 
 SELECT count(*) FROM ft1 WHERE c2 < 0;
  count 
@@ -7203,8 +7204,9 @@ EXPLAIN (VERBOSE, COSTS OFF) SELECT count(*) FROM ft1 WHERE c2 >= 0;
  Aggregate
    Output: count(*)
    ->  Result
+         Replaces: Scan on ft1
          One-Time Filter: false
-(4 rows)
+(5 rows)
 
 SELECT count(*) FROM ft1 WHERE c2 >= 0;
  count 
@@ -8032,8 +8034,9 @@ DELETE FROM rem1 WHERE false;     -- currently can't be pushed down
    Remote SQL: DELETE FROM public.loc1 WHERE ctid = $1
    ->  Result
          Output: ctid
+         Replaces: Scan on rem1
          One-Time Filter: false
-(5 rows)
+(6 rows)
 
 -- Test with statement-level triggers
 CREATE TRIGGER trig_stmt_before
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 8345bc0264b..1de666905c8 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -147,6 +147,7 @@ static void show_buffer_usage(ExplainState *es, const BufferUsage *usage);
 static void show_wal_usage(ExplainState *es, const WalUsage *usage);
 static void show_memory_counters(ExplainState *es,
 								 const MemoryContextCounters *mem_counters);
+static void show_result_replacement_info(Result *result, ExplainState *es);
 static void ExplainIndexScanDetails(Oid indexid, ScanDirection indexorderdir,
 									ExplainState *es);
 static void ExplainScanTarget(Scan *plan, ExplainState *es);
@@ -2232,6 +2233,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
 								   ancestors, es);
 			break;
 		case T_Result:
+			show_result_replacement_info(castNode(Result, plan), es);
 			show_upper_qual((List *) ((Result *) plan)->resconstantqual,
 							"One-Time Filter", planstate, ancestors, es);
 			show_upper_qual(plan->qual, "Filter", planstate, ancestors, es);
@@ -4750,6 +4752,102 @@ show_modifytable_info(ModifyTableState *mtstate, List *ancestors,
 		ExplainCloseGroup("Target Tables", "Target Tables", false, es);
 }
 
+/*
+ * Explain what a "Result" node replaced.
+ */
+static void
+show_result_replacement_info(Result *result, ExplainState *es)
+{
+	StringInfoData buf;
+	int			nrels = 0;
+	int			rti = -1;
+	bool		found_non_result = false;
+	char	   *replacement_type = "???";
+
+	/* If the Result node has a subplan, it didn't replace anything. */
+	if (result->plan.lefttree != NULL)
+		return;
+
+	/* Gating result nodes should have a subplan, and we don't. */
+	Assert(result->result_type != RESULT_TYPE_GATING);
+
+	switch (result->result_type)
+	{
+		case RESULT_TYPE_GATING:
+			replacement_type = "Gating";
+			break;
+		case RESULT_TYPE_SCAN:
+			replacement_type = "Scan";
+			break;
+		case RESULT_TYPE_JOIN:
+			replacement_type = "Join";
+			break;
+		case RESULT_TYPE_UPPER:
+			/* a small white lie */
+			replacement_type = "Aggregate";
+			break;
+		case RESULT_TYPE_MINMAX:
+			replacement_type = "MinMaxAggregate";
+			break;
+	}
+
+	/*
+	 * Build up a comma-separated list of user-facing names for the range
+	 * table entries in the relids set.
+	 */
+	initStringInfo(&buf);
+	while ((rti = bms_next_member(result->relids, rti)) >= 0)
+	{
+		RangeTblEntry *rte = rt_fetch(rti, es->rtable);
+		char	   *refname;
+
+		/*
+		 * add_outer_joins_to_relids will add join RTIs to the relids set of a
+		 * join; if that join is then replaced with a Result node, we may see
+		 * such RTIs here. But we want to completely ignore those here,
+		 * because "a LEFT JOIN b ON whatever" is a join between a and b, not
+		 * a join between a, b, and an unnamed join.
+		 */
+		if (rte->rtekind == RTE_JOIN)
+			continue;
+
+		/* Count the number of rels that aren't ignored completely. */
+		++nrels;
+
+		/* Work out what reference name to use and add it to the string. */
+		refname = (char *) list_nth(es->rtable_names, rti - 1);
+		if (refname == NULL)
+			refname = rte->eref->aliasname;
+		if (buf.len > 0)
+			appendStringInfoString(&buf, ", ");
+		appendStringInfoString(&buf, refname);
+
+		/* Keep track of whether we see anything other than RTE_RESULT. */
+		if (rte->rtekind != RTE_RESULT)
+			found_non_result = true;
+	}
+
+	/*
+	 * If this Result node is because of a single RTE that is RTE_RESULT, it
+	 * is not really replacing anything at all, because there's no other
+	 * method for implementing a scan of such an RTE, so we don't display the
+	 * Replaces line in such cases.
+	 */
+	if (nrels <= 1 && !found_non_result &&
+		result->result_type == RESULT_TYPE_SCAN)
+		return;
+
+	/* Say what we replaced, with list of rels if available. */
+	if (buf.len == 0)
+		ExplainPropertyText("Replaces", replacement_type, es);
+	else
+	{
+		char	   *s = psprintf("%s on %s", replacement_type, buf.data);
+
+		ExplainPropertyText("Replaces", s, es);
+	}
+}
+
 /*
  * Explain the constituent plans of an Append, MergeAppend,
  * BitmapAnd, or BitmapOr node.
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index 6791cbeb416..c9dba7ff346 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -99,7 +99,8 @@ static Gather *create_gather_plan(PlannerInfo *root, GatherPath *best_path);
 static Plan *create_projection_plan(PlannerInfo *root,
 									ProjectionPath *best_path,
 									int flags);
-static Plan *inject_projection_plan(Plan *subplan, List *tlist, bool parallel_safe);
+static Plan *inject_projection_plan(Plan *subplan, List *tlist,
+									bool parallel_safe);
 static Sort *create_sort_plan(PlannerInfo *root, SortPath *best_path, int flags);
 static IncrementalSort *create_incrementalsort_plan(PlannerInfo *root,
 													IncrementalSortPath *best_path, int flags);
@@ -302,7 +303,10 @@ static SetOp *make_setop(SetOpCmd cmd, SetOpStrategy strategy,
 						 List *tlist, Plan *lefttree, Plan *righttree,
 						 List *groupList, long numGroups);
 static LockRows *make_lockrows(Plan *lefttree, List *rowMarks, int epqParam);
-static Result *make_result(List *tlist, Node *resconstantqual, Plan *subplan);
+static Result *make_gating_result(List *tlist, Node *resconstantqual,
+								  Plan *subplan);
+static Result *make_one_row_result(List *tlist, Node *resconstantqual,
+								   RelOptInfo *rel);
 static ProjectSet *make_project_set(List *tlist, Plan *subplan);
 static ModifyTable *make_modifytable(PlannerInfo *root, Plan *subplan,
 									 CmdType operation, bool canSetTag,
@@ -1012,36 +1016,36 @@ static Plan *
 create_gating_plan(PlannerInfo *root, Path *path, Plan *plan,
 				   List *gating_quals)
 {
-	Plan	   *gplan;
-	Plan	   *splan;
+	Result	   *gplan;
 
 	Assert(gating_quals);
 
 	/*
-	 * We might have a trivial Result plan already.  Stacking one Result atop
-	 * another is silly, so if that applies, just discard the input plan.
+	 * Since we need a Result node anyway, always return the path's requested
+	 * tlist; that's never a wrong choice, even if the parent node didn't ask
+	 * for CP_EXACT_TLIST.
+	 */
+	gplan = make_gating_result(build_path_tlist(root, path),
+							   (Node *) gating_quals, plan);
+
+	/*
+	 * We might have had a trivial Result plan already.  Stacking one Result
+	 * atop another is silly, so if that applies, just discard the input plan.
 	 * (We're assuming its targetlist is uninteresting; it should be either
-	 * the same as the result of build_path_tlist, or a simplified version.)
+	 * the same as the result of build_path_tlist, or a simplified version.
+	 * However, we preserve the set of relids that it purports to scan and
+	 * attribute that to our replacement Result instead, and likewise for the
+	 * result_type.)
 	 */
-	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;
 	}
 
-	/*
-	 * Since we need a Result node anyway, always return the path's requested
-	 * tlist; that's never a wrong choice, even if the parent node didn't ask
-	 * for CP_EXACT_TLIST.
-	 */
-	gplan = (Plan *) make_result(build_path_tlist(root, path),
-								 (Node *) gating_quals,
-								 splan);
-
 	/*
 	 * Notice that we don't change cost or size estimates when doing gating.
 	 * The costs of qual eval were already included in the subplan's cost.
@@ -1054,12 +1058,12 @@ create_gating_plan(PlannerInfo *root, Path *path, Plan *plan,
 	 * in most cases we have only a very bad idea of the probability of the
 	 * gating qual being true.
 	 */
-	copy_plan_costsize(gplan, plan);
+	copy_plan_costsize(&gplan->plan, plan);
 
 	/* Gating quals could be unsafe, so better use the Path's safety flag */
-	gplan->parallel_safe = path->parallel_safe;
+	gplan->plan.parallel_safe = path->parallel_safe;
 
-	return gplan;
+	return &gplan->plan;
 }
 
 /*
@@ -1235,10 +1239,10 @@ create_append_plan(PlannerInfo *root, AppendPath *best_path, int flags)
 		/* Generate a Result plan with constant-FALSE gating qual */
 		Plan	   *plan;
 
-		plan = (Plan *) make_result(tlist,
-									(Node *) list_make1(makeBoolConst(false,
-																	  false)),
-									NULL);
+		plan = (Plan *) make_one_row_result(tlist,
+											(Node *) list_make1(makeBoolConst(false,
+																			  false)),
+											best_path->path.parent);
 
 		copy_generic_path_info(plan, (Path *) best_path);
 
@@ -1636,7 +1640,7 @@ create_group_result_plan(PlannerInfo *root, GroupResultPath *best_path)
 	/* best_path->quals is just bare clauses */
 	quals = order_qual_clauses(root, best_path->quals);
 
-	plan = make_result(tlist, (Node *) quals, NULL);
+	plan = make_one_row_result(tlist, (Node *) quals, best_path->path.parent);
 
 	copy_generic_path_info(&plan->plan, (Path *) best_path);
 
@@ -1933,8 +1937,7 @@ create_projection_plan(PlannerInfo *root, ProjectionPath *best_path, int flags)
 	}
 	else
 	{
-		/* We need a Result node */
-		plan = (Plan *) make_result(tlist, NULL, subplan);
+		plan = (Plan *) make_gating_result(tlist, NULL, subplan);
 
 		copy_generic_path_info(plan, (Path *) best_path);
 	}
@@ -1958,7 +1961,7 @@ inject_projection_plan(Plan *subplan, List *tlist, bool parallel_safe)
 {
 	Plan	   *plan;
 
-	plan = (Plan *) make_result(tlist, NULL, subplan);
+	plan = (Plan *) make_gating_result(tlist, NULL, subplan);
 
 	/*
 	 * In principle, we should charge tlist eval cost plus cpu_per_tuple per
@@ -2436,7 +2439,9 @@ create_minmaxagg_plan(PlannerInfo *root, MinMaxAggPath *best_path)
 	/* Generate the output plan --- basically just a Result */
 	tlist = build_path_tlist(root, &best_path->path);
 
-	plan = make_result(tlist, (Node *) best_path->quals, NULL);
+	plan = make_one_row_result(tlist, (Node *) best_path->quals,
+							   best_path->path.parent);
+	plan->result_type = RESULT_TYPE_MINMAX;
 
 	copy_generic_path_info(&plan->plan, (Path *) best_path);
 
@@ -3887,7 +3892,8 @@ create_resultscan_plan(PlannerInfo *root, Path *best_path,
 			replace_nestloop_params(root, (Node *) scan_clauses);
 	}
 
-	scan_plan = make_result(tlist, (Node *) scan_clauses, NULL);
+	scan_plan = make_one_row_result(tlist, (Node *) scan_clauses,
+									best_path->parent);
 
 	copy_generic_path_info(&scan_plan->plan, best_path);
 
@@ -6922,22 +6928,57 @@ make_limit(Plan *lefttree, Node *limitOffset, Node *limitCount,
 }
 
 /*
- * make_result
- *	  Build a Result plan node
+ * make_gating_result
+ *	  Build a Result plan node that performs projection of a subplan, and/or
+ *	  applies a one time filter (resconstantqual)
  */
 static Result *
-make_result(List *tlist,
-			Node *resconstantqual,
-			Plan *subplan)
+make_gating_result(List *tlist,
+				   Node *resconstantqual,
+				   Plan *subplan)
 {
 	Result	   *node = makeNode(Result);
 	Plan	   *plan = &node->plan;
 
+	Assert(subplan != NULL);
+
 	plan->targetlist = tlist;
 	plan->qual = NIL;
 	plan->lefttree = subplan;
 	plan->righttree = NULL;
+	node->result_type = RESULT_TYPE_GATING;
+	node->resconstantqual = resconstantqual;
+	node->relids = NULL;
+
+	return node;
+}
+
+/*
+ * make_one_row_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)
+ *
+ * 'rel' should be this path's RelOptInfo. In essence, we're saying that this
+ * Result node generates all the tuples for that RelOptInfo. Note that the same
+ * consideration can never arise in make_gating_result(), because in that case
+ * the tuples are always coming from some subordinate node.
+ */
+static Result *
+make_one_row_result(List *tlist,
+					Node *resconstantqual,
+					RelOptInfo *rel)
+{
+	Result	   *node = makeNode(Result);
+	Plan	   *plan = &node->plan;
+
+	plan->targetlist = tlist;
+	plan->qual = NIL;
+	plan->lefttree = NULL;
+	plan->righttree = NULL;
+	node->result_type = IS_UPPER_REL(rel) ? RESULT_TYPE_UPPER :
+		IS_JOIN_REL(rel) ? RESULT_TYPE_JOIN : RESULT_TYPE_SCAN;
 	node->resconstantqual = resconstantqual;
+	node->relids = rel->relids;
 
 	return node;
 }
diff --git a/src/backend/optimizer/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.c
index d706546f332..6950eff2c5b 100644
--- a/src/backend/optimizer/plan/setrefs.c
+++ b/src/backend/optimizer/plan/setrefs.c
@@ -1056,6 +1056,8 @@ set_plan_refs(PlannerInfo *root, Plan *plan, int rtoffset)
 				/* resconstantqual can't contain any subplan variable refs */
 				splan->resconstantqual =
 					fix_scan_expr(root, splan->resconstantqual, rtoffset, 1);
+				/* adjust the relids set */
+				splan->relids = offset_relid_set(splan->relids, rtoffset);
 			}
 			break;
 		case T_ProjectSet:
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index 29d7732d6a0..3d196f5078e 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -252,6 +252,20 @@ typedef struct Plan
 #define outerPlan(node)			(((Plan *)(node))->lefttree)
 
 
+/* ----------------
+ *	 ResultType -
+ *		Classification of Result nodes
+ * ----------------
+ */
+typedef enum ResultType
+{
+	RESULT_TYPE_GATING,			/* project or one-time-filter outer plan */
+	RESULT_TYPE_SCAN,			/* replace empty scan */
+	RESULT_TYPE_JOIN,			/* replace empty join */
+	RESULT_TYPE_UPPER,			/* replace degenerate upper rel */
+	RESULT_TYPE_MINMAX			/* implement minmax aggregate */
+} ResultType;
+
 /* ----------------
  *	 Result node -
  *		If no outer plan, evaluate a variable-free targetlist.
@@ -261,12 +275,20 @@ typedef struct Plan
  * If resconstantqual isn't NULL, it represents a one-time qualification
  * test (i.e., one that doesn't depend on any variables from the outer plan,
  * so needs to be evaluated only once).
+ *
+ * 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.
  * ----------------
  */
 typedef struct Result
 {
 	Plan		plan;
+	ResultType	result_type;
 	Node	   *resconstantqual;
+	Bitmapset  *relids;
 } Result;
 
 /* ----------------
diff --git a/src/test/regress/expected/aggregates.out b/src/test/regress/expected/aggregates.out
index c35288eecde..1f24f6ffd1f 100644
--- a/src/test/regress/expected/aggregates.out
+++ b/src/test/regress/expected/aggregates.out
@@ -959,11 +959,12 @@ explain (costs off)
                          QUERY PLAN                         
 ------------------------------------------------------------
  Result
+   Replaces: MinMaxAggregate
    InitPlan 1
      ->  Limit
            ->  Index Only Scan using tenk1_unique1 on tenk1
                  Index Cond: (unique1 IS NOT NULL)
-(5 rows)
+(6 rows)
 
 select min(unique1) from tenk1;
  min 
@@ -976,11 +977,12 @@ explain (costs off)
                              QUERY PLAN                              
 ---------------------------------------------------------------------
  Result
+   Replaces: MinMaxAggregate
    InitPlan 1
      ->  Limit
            ->  Index Only Scan Backward using tenk1_unique1 on tenk1
                  Index Cond: (unique1 IS NOT NULL)
-(5 rows)
+(6 rows)
 
 select max(unique1) from tenk1;
  max  
@@ -993,11 +995,12 @@ explain (costs off)
                                QUERY PLAN                               
 ------------------------------------------------------------------------
  Result
+   Replaces: MinMaxAggregate
    InitPlan 1
      ->  Limit
            ->  Index Only Scan Backward using tenk1_unique1 on tenk1
                  Index Cond: ((unique1 IS NOT NULL) AND (unique1 < 42))
-(5 rows)
+(6 rows)
 
 select max(unique1) from tenk1 where unique1 < 42;
  max 
@@ -1010,11 +1013,12 @@ explain (costs off)
                                QUERY PLAN                               
 ------------------------------------------------------------------------
  Result
+   Replaces: MinMaxAggregate
    InitPlan 1
      ->  Limit
            ->  Index Only Scan Backward using tenk1_unique1 on tenk1
                  Index Cond: ((unique1 IS NOT NULL) AND (unique1 > 42))
-(5 rows)
+(6 rows)
 
 select max(unique1) from tenk1 where unique1 > 42;
  max  
@@ -1033,11 +1037,12 @@ explain (costs off)
                                 QUERY PLAN                                 
 ---------------------------------------------------------------------------
  Result
+   Replaces: MinMaxAggregate
    InitPlan 1
      ->  Limit
            ->  Index Only Scan Backward using tenk1_unique1 on tenk1
                  Index Cond: ((unique1 IS NOT NULL) AND (unique1 > 42000))
-(5 rows)
+(6 rows)
 
 select max(unique1) from tenk1 where unique1 > 42000;
  max 
@@ -1052,11 +1057,12 @@ explain (costs off)
                                  QUERY PLAN                                 
 ----------------------------------------------------------------------------
  Result
+   Replaces: MinMaxAggregate
    InitPlan 1
      ->  Limit
            ->  Index Only Scan Backward using tenk1_thous_tenthous on tenk1
                  Index Cond: ((thousand = 33) AND (tenthous IS NOT NULL))
-(5 rows)
+(6 rows)
 
 select max(tenthous) from tenk1 where thousand = 33;
  max  
@@ -1069,11 +1075,12 @@ explain (costs off)
                                 QUERY PLAN                                
 --------------------------------------------------------------------------
  Result
+   Replaces: MinMaxAggregate
    InitPlan 1
      ->  Limit
            ->  Index Only Scan using tenk1_thous_tenthous on tenk1
                  Index Cond: ((thousand = 33) AND (tenthous IS NOT NULL))
-(5 rows)
+(6 rows)
 
 select min(tenthous) from tenk1 where thousand = 33;
  min 
@@ -1090,11 +1097,12 @@ explain (costs off)
  Seq Scan on int4_tbl
    SubPlan 2
      ->  Result
+           Replaces: MinMaxAggregate
            InitPlan 1
              ->  Limit
                    ->  Index Only Scan using tenk1_unique1 on tenk1
                          Index Cond: ((unique1 IS NOT NULL) AND (unique1 > int4_tbl.f1))
-(7 rows)
+(8 rows)
 
 select f1, (select min(unique1) from tenk1 where unique1 > f1) AS gt
   from int4_tbl;
@@ -1119,7 +1127,8 @@ explain (costs off)
            ->  Index Only Scan Backward using tenk1_unique2 on tenk1
                  Index Cond: (unique2 IS NOT NULL)
    ->  Result
-(7 rows)
+         Replaces: MinMaxAggregate
+(8 rows)
 
 select distinct max(unique2) from tenk1;
  max  
@@ -1138,7 +1147,8 @@ explain (costs off)
            ->  Index Only Scan Backward using tenk1_unique2 on tenk1
                  Index Cond: (unique2 IS NOT NULL)
    ->  Result
-(7 rows)
+         Replaces: MinMaxAggregate
+(8 rows)
 
 select max(unique2) from tenk1 order by 1;
  max  
@@ -1157,7 +1167,8 @@ explain (costs off)
            ->  Index Only Scan Backward using tenk1_unique2 on tenk1
                  Index Cond: (unique2 IS NOT NULL)
    ->  Result
-(7 rows)
+         Replaces: MinMaxAggregate
+(8 rows)
 
 select max(unique2) from tenk1 order by max(unique2);
  max  
@@ -1176,7 +1187,8 @@ explain (costs off)
            ->  Index Only Scan Backward using tenk1_unique2 on tenk1
                  Index Cond: (unique2 IS NOT NULL)
    ->  Result
-(7 rows)
+         Replaces: MinMaxAggregate
+(8 rows)
 
 select max(unique2) from tenk1 order by max(unique2)+1;
  max  
@@ -1196,7 +1208,8 @@ explain (costs off)
                  Index Cond: (unique2 IS NOT NULL)
    ->  ProjectSet
          ->  Result
-(8 rows)
+               Replaces: MinMaxAggregate
+(9 rows)
 
 select max(unique2), generate_series(1,3) as g from tenk1 order by g desc;
  max  | g 
@@ -1212,12 +1225,13 @@ explain (costs off)
                      QUERY PLAN                     
 ----------------------------------------------------
  Result
+   Replaces: MinMaxAggregate
    InitPlan 1
      ->  Limit
            ->  Result
                  One-Time Filter: (100 IS NOT NULL)
                  ->  Seq Scan on tenk1
-(6 rows)
+(7 rows)
 
 select max(100) from tenk1;
  max 
@@ -1243,6 +1257,7 @@ explain (costs off)
                                          QUERY PLAN                                          
 ---------------------------------------------------------------------------------------------
  Result
+   Replaces: MinMaxAggregate
    InitPlan 1
      ->  Limit
            ->  Merge Append
@@ -1265,7 +1280,7 @@ explain (costs off)
                  ->  Index Only Scan using minmaxtest2i on minmaxtest2 minmaxtest_8
                        Index Cond: (f1 IS NOT NULL)
                  ->  Index Only Scan Backward using minmaxtest3i on minmaxtest3 minmaxtest_9
-(23 rows)
+(24 rows)
 
 select min(f1), max(f1) from minmaxtest;
  min | max 
@@ -1304,7 +1319,8 @@ explain (costs off)
    ->  Sort
          Sort Key: ((InitPlan 1).col1), ((InitPlan 2).col1)
          ->  Result
-(26 rows)
+               Replaces: MinMaxAggregate
+(27 rows)
 
 select distinct min(f1), max(f1) from minmaxtest;
  min | max 
@@ -1334,7 +1350,8 @@ explain (costs off)
                    ->  Seq Scan on int4_tbl t1
                          Filter: ((f1 IS NOT NULL) AND (f1 = t0.f1))
            ->  Result
-(9 rows)
+                 Replaces: MinMaxAggregate
+(10 rows)
 
 select f1, (select distinct min(t1.f1) from int4_tbl t1 where t1.f1 = t0.f1)
 from int4_tbl t0;
diff --git a/src/test/regress/expected/case.out b/src/test/regress/expected/case.out
index efee7fc4317..4c77f7e3961 100644
--- a/src/test/regress/expected/case.out
+++ b/src/test/regress/expected/case.out
@@ -266,27 +266,30 @@ SELECT *
 -- Tests for constant subexpression simplification
 explain (costs off)
 SELECT * FROM CASE_TBL WHERE NULLIF(1, 2) = 2;
-        QUERY PLAN        
---------------------------
+          QUERY PLAN          
+------------------------------
  Result
+   Replaces: Scan on case_tbl
    One-Time Filter: false
-(2 rows)
+(3 rows)
 
 explain (costs off)
 SELECT * FROM CASE_TBL WHERE NULLIF(1, 1) IS NOT NULL;
-        QUERY PLAN        
---------------------------
+          QUERY PLAN          
+------------------------------
  Result
+   Replaces: Scan on case_tbl
    One-Time Filter: false
-(2 rows)
+(3 rows)
 
 explain (costs off)
 SELECT * FROM CASE_TBL WHERE NULLIF(1, null) = 2;
-        QUERY PLAN        
---------------------------
+          QUERY PLAN          
+------------------------------
  Result
+   Replaces: Scan on case_tbl
    One-Time Filter: false
-(2 rows)
+(3 rows)
 
 --
 -- Examples of updates involving tables
diff --git a/src/test/regress/expected/explain.out b/src/test/regress/expected/explain.out
index c53bf9c8aa3..7c1f26b182c 100644
--- a/src/test/regress/expected/explain.out
+++ b/src/test/regress/expected/explain.out
@@ -54,6 +54,14 @@ set jit = off;
 -- enabled.
 set track_io_timing = off;
 -- Simple cases
+explain (costs off) select 1 as a, 2 as b having false;
+        QUERY PLAN        
+--------------------------
+ Result
+   Replaces: Aggregate
+   One-Time Filter: false
+(3 rows)
+
 select explain_filter('explain select * from int8_tbl i8');
                      explain_filter                      
 ---------------------------------------------------------
diff --git a/src/test/regress/expected/generated_virtual.out b/src/test/regress/expected/generated_virtual.out
index d8645192351..b48e6bdbf9d 100644
--- a/src/test/regress/expected/generated_virtual.out
+++ b/src/test/regress/expected/generated_virtual.out
@@ -1579,8 +1579,9 @@ select t2.* from gtest32 t1 left join gtest32 t2 on false;
          Output: t1.a, t1.b, t1.c, t1.d, t1.e
    ->  Result
          Output: a, e, 20, COALESCE(a, 100)
+         Replaces: Scan on t2
          One-Time Filter: false
-(8 rows)
+(9 rows)
 
 select t2.* from gtest32 t1 left join gtest32 t2 on false;
  a | b | c | d | e 
@@ -1642,20 +1643,22 @@ set constraint_exclusion to on;
 -- should get a dummy Result, not a seq scan
 explain (costs off)
 select * from gtest33 where b < 10;
-        QUERY PLAN        
---------------------------
+         QUERY PLAN          
+-----------------------------
  Result
+   Replaces: Scan on gtest33
    One-Time Filter: false
-(2 rows)
+(3 rows)
 
 -- should get a dummy Result, not a seq scan
 explain (costs off)
 select * from gtest33 where b is null;
-        QUERY PLAN        
---------------------------
+         QUERY PLAN          
+-----------------------------
  Result
+   Replaces: Scan on gtest33
    One-Time Filter: false
-(2 rows)
+(3 rows)
 
 reset constraint_exclusion;
 drop table gtest33;
diff --git a/src/test/regress/expected/groupingsets.out b/src/test/regress/expected/groupingsets.out
index 35e4cb47ebe..210bbe307a7 100644
--- a/src/test/regress/expected/groupingsets.out
+++ b/src/test/regress/expected/groupingsets.out
@@ -591,11 +591,12 @@ explain (costs off)
                          QUERY PLAN                         
 ------------------------------------------------------------
  Result
+   Replaces: MinMaxAggregate
    InitPlan 1
      ->  Limit
            ->  Index Only Scan using tenk1_unique1 on tenk1
                  Index Cond: (unique1 IS NOT NULL)
-(5 rows)
+(6 rows)
 
 -- Views with GROUPING SET queries
 CREATE VIEW gstest_view AS select a, b, grouping(a,b), sum(c), count(*), max(c)
diff --git a/src/test/regress/expected/inherit.out b/src/test/regress/expected/inherit.out
index 5b5055babdc..031dd87424a 100644
--- a/src/test/regress/expected/inherit.out
+++ b/src/test/regress/expected/inherit.out
@@ -580,8 +580,9 @@ update some_tab set a = a + 1 where false;
  Update on public.some_tab
    ->  Result
          Output: (some_tab.a + 1), NULL::oid, NULL::tid
+         Replaces: Scan on some_tab
          One-Time Filter: false
-(4 rows)
+(5 rows)
 
 update some_tab set a = a + 1 where false;
 explain (verbose, costs off)
@@ -592,8 +593,9 @@ update some_tab set a = a + 1 where false returning b, a;
    Output: some_tab.b, some_tab.a
    ->  Result
          Output: (some_tab.a + 1), NULL::oid, NULL::tid
+         Replaces: Scan on some_tab
          One-Time Filter: false
-(5 rows)
+(6 rows)
 
 update some_tab set a = a + 1 where false returning b, a;
  b | a 
@@ -699,8 +701,9 @@ explain update parted_tab set a = 2 where false;
 --------------------------------------------------------
  Update on parted_tab  (cost=0.00..0.00 rows=0 width=0)
    ->  Result  (cost=0.00..0.00 rows=0 width=10)
+         Replaces: Scan on parted_tab
          One-Time Filter: false
-(3 rows)
+(4 rows)
 
 drop table parted_tab;
 -- Check UPDATE with multi-level partitioned inherited target
@@ -1756,6 +1759,7 @@ explain (verbose, costs off) select min(1-id) from matest0;
 ---------------------------------------------------------------------------------
  Result
    Output: (InitPlan 1).col1
+   Replaces: MinMaxAggregate
    InitPlan 1
      ->  Limit
            Output: ((1 - matest0.id))
@@ -1779,7 +1783,7 @@ explain (verbose, costs off) select min(1-id) from matest0;
                        ->  Index Scan using matest3i on public.matest3 matest0_4
                              Output: matest0_4.id, (1 - matest0_4.id)
                              Index Cond: ((1 - matest0_4.id) IS NOT NULL)
-(25 rows)
+(26 rows)
 
 select min(1-id) from matest0;
  min 
@@ -1943,6 +1947,7 @@ SELECT min(x) FROM
                              QUERY PLAN                             
 --------------------------------------------------------------------
  Result
+   Replaces: MinMaxAggregate
    InitPlan 1
      ->  Limit
            ->  Merge Append
@@ -1951,7 +1956,7 @@ SELECT min(x) FROM
                        Index Cond: (unique1 IS NOT NULL)
                  ->  Index Only Scan using tenk1_unique2 on tenk1 b
                        Index Cond: (unique2 IS NOT NULL)
-(9 rows)
+(10 rows)
 
 explain (costs off)
 SELECT min(y) FROM
@@ -1961,6 +1966,7 @@ SELECT min(y) FROM
                              QUERY PLAN                             
 --------------------------------------------------------------------
  Result
+   Replaces: MinMaxAggregate
    InitPlan 1
      ->  Limit
            ->  Merge Append
@@ -1969,7 +1975,7 @@ SELECT min(y) FROM
                        Index Cond: (unique1 IS NOT NULL)
                  ->  Index Only Scan using tenk1_unique2 on tenk1 b
                        Index Cond: (unique2 IS NOT NULL)
-(9 rows)
+(10 rows)
 
 -- XXX planner doesn't recognize that index on unique2 is sufficiently sorted
 explain (costs off)
@@ -3087,11 +3093,12 @@ explain (costs off) select * from range_list_parted where a between 3 and 23 and
 
 /* Should select no rows because range partition key cannot be null */
 explain (costs off) select * from range_list_parted where a is null;
-        QUERY PLAN        
---------------------------
+              QUERY PLAN               
+---------------------------------------
  Result
+   Replaces: Scan on range_list_parted
    One-Time Filter: false
-(2 rows)
+(3 rows)
 
 /* Should only select rows from the null-accepting partition */
 explain (costs off) select * from range_list_parted where b is null;
@@ -3252,6 +3259,7 @@ explain (costs off) select min(a), max(a) from parted_minmax where b = '12345';
                                            QUERY PLAN                                           
 ------------------------------------------------------------------------------------------------
  Result
+   Replaces: MinMaxAggregate
    InitPlan 1
      ->  Limit
            ->  Index Only Scan using parted_minmax1i on parted_minmax1 parted_minmax
@@ -3260,7 +3268,7 @@ explain (costs off) select min(a), max(a) from parted_minmax where b = '12345';
      ->  Limit
            ->  Index Only Scan Backward using parted_minmax1i on parted_minmax1 parted_minmax_1
                  Index Cond: ((a IS NOT NULL) AND (b = '12345'::text))
-(9 rows)
+(10 rows)
 
 select min(a), max(a) from parted_minmax where b = '12345';
  min | max 
diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out
index 04079268b98..30baa9dc51a 100644
--- a/src/test/regress/expected/join.out
+++ b/src/test/regress/expected/join.out
@@ -2264,11 +2264,12 @@ explain (costs off)
 select aa, bb, unique1, unique1
   from tenk1 right join b_star on aa = unique1
   where bb < bb and bb is null;
-        QUERY PLAN        
---------------------------
+            QUERY PLAN             
+-----------------------------------
  Result
+   Replaces: Join on tenk1, b_star
    One-Time Filter: false
-(2 rows)
+(3 rows)
 
 select aa, bb, unique1, unique1
   from tenk1 right join b_star on aa = unique1
@@ -2386,11 +2387,12 @@ order by t1.unique1;
                            Index Cond: (unique1 < 10)
                SubPlan 2
                  ->  Result
+                       Replaces: MinMaxAggregate
                        InitPlan 1
                          ->  Limit
                                ->  Index Only Scan using tenk1_unique1 on tenk1
                                      Index Cond: ((unique1 IS NOT NULL) AND (unique1 = t2.unique1))
-(19 rows)
+(20 rows)
 
 -- Ensure we get the expected result
 select t1.unique1,t2.unique1 from tenk1 t1
@@ -2655,8 +2657,8 @@ select * from int8_tbl t1 left join
     (int8_tbl t2 left join int8_tbl t3 full join int8_tbl t4 on false on false)
     left join int8_tbl t5 on t2.q1 = t5.q1
 on t2.q2 = 123;
-                    QUERY PLAN                    
---------------------------------------------------
+                     QUERY PLAN                     
+----------------------------------------------------
  Nested Loop Left Join
    ->  Seq Scan on int8_tbl t1
    ->  Materialize
@@ -2667,9 +2669,10 @@ on t2.q2 = 123;
                      ->  Seq Scan on int8_tbl t2
                            Filter: (q2 = 123)
                      ->  Result
+                           Replaces: Join on t3, t4
                            One-Time Filter: false
                ->  Seq Scan on int8_tbl t5
-(12 rows)
+(13 rows)
 
 explain (costs off)
 select * from int8_tbl t1
@@ -4164,6 +4167,7 @@ select * from t t1
                            Output: t2.i2, 1
          ->  Result
                Output: i3
+               Replaces: Scan on t3
                One-Time Filter: false
    ->  Memoize
          Output: t4.i4
@@ -4172,7 +4176,7 @@ select * from t t1
          ->  Index Only Scan using t_pkey on pg_temp.t t4
                Output: t4.i4
                Index Cond: (t4.i4 > (1))
-(25 rows)
+(26 rows)
 
 explain (verbose, costs off)
 select * from
@@ -4362,8 +4366,9 @@ from int4_tbl t1
 -------------------------------------------------------------------------------------------------------------------
  Result
    Output: (current_database())::information_schema.sql_identifier, (c.relname)::information_schema.sql_identifier
+   Replaces: Join on t1, t2, a, c, nc, t, nt, bt, nbt
    One-Time Filter: false
-(3 rows)
+(4 rows)
 
 -- Test handling of qual pushdown to appendrel members with non-Var outputs
 explain (verbose, costs off)
@@ -4435,11 +4440,12 @@ select unique1 from tenk1, lateral f_immutable_int4(1) x where x = unique1;
 
 explain (costs off)
 select unique1 from tenk1, lateral f_immutable_int4(1) x where x in (select 17);
-        QUERY PLAN        
---------------------------
+        QUERY PLAN         
+---------------------------
  Result
+   Replaces: Scan on tenk1
    One-Time Filter: false
-(2 rows)
+(3 rows)
 
 explain (costs off)
 select unique1, x from tenk1 join f_immutable_int4(1) x on unique1 = x;
@@ -4485,11 +4491,12 @@ select unique1, x from tenk1 full join f_immutable_int4(1) x on unique1 = x;
 -- check that pullup of a const function allows further const-folding
 explain (costs off)
 select unique1 from tenk1, f_immutable_int4(1) x where x = 42;
-        QUERY PLAN        
---------------------------
+        QUERY PLAN         
+---------------------------
  Result
+   Replaces: Scan on tenk1
    One-Time Filter: false
-(2 rows)
+(3 rows)
 
 -- test inlining of immutable functions with PlaceHolderVars
 explain (costs off)
@@ -5345,8 +5352,9 @@ left join
    Join Filter: false
    ->  Result
    ->  Result
+         Replaces: Join on c, n
          One-Time Filter: false
-(5 rows)
+(6 rows)
 
 -- check handling of apparently-commutable outer joins with non-commutable
 -- joins between them
@@ -5540,12 +5548,13 @@ select 1 from
      right join (select 1 as z) as ss2 on true)
   on false,
   lateral (select i4.f1, ss1.n from int8_tbl as i8 limit 1) as ss3;
-        QUERY PLAN        
---------------------------
+                  QUERY PLAN                   
+-----------------------------------------------
  Result
    Output: 1
+   Replaces: Join on i4, ss3, x1, x2, *RESULT*
    One-Time Filter: false
-(3 rows)
+(4 rows)
 
 select 1 from
   int4_tbl as i4
@@ -5574,11 +5583,12 @@ select 1 from t t1
        on false
      where t3.a = coalesce(t5.a,1)) as s2
   on true;
-        QUERY PLAN        
---------------------------
+                 QUERY PLAN                 
+--------------------------------------------
  Result
+   Replaces: Join on t1, s1, t2, t3, t4, t5
    One-Time Filter: false
-(2 rows)
+(3 rows)
 
 rollback;
 --
@@ -5975,14 +5985,15 @@ from int4_tbl as t1
                         inner join int8_tbl as t7 on null)
                on t5.q1 = t7.q2)
     on false;
-           QUERY PLAN           
---------------------------------
+                    QUERY PLAN                    
+--------------------------------------------------
  Nested Loop Left Join
    Join Filter: false
    ->  Seq Scan on int4_tbl t1
    ->  Result
+         Replaces: Join on t2, t3, t4, t5, t7, t6
          One-Time Filter: false
-(5 rows)
+(6 rows)
 
 -- variant with Var rather than PHV coming from t6
 explain (costs off)
@@ -5997,14 +6008,15 @@ from int4_tbl as t1
                         inner join int8_tbl as t7 on null)
                on t5.q1 = t7.q2)
     on false;
-           QUERY PLAN           
---------------------------------
+                    QUERY PLAN                    
+--------------------------------------------------
  Nested Loop Left Join
    Join Filter: false
    ->  Seq Scan on int4_tbl t1
    ->  Result
+         Replaces: Join on t2, t3, t4, t5, t7, t6
          One-Time Filter: false
-(5 rows)
+(6 rows)
 
 -- per further discussion of bug #17781
 explain (costs off)
@@ -6054,15 +6066,16 @@ select * from int8_tbl t1 left join
   (int8_tbl t2 inner join int8_tbl t3 on false
    left join int8_tbl t4 on t2.q2 = t4.q2)
 on t1.q1 = t2.q1;
-              QUERY PLAN              
---------------------------------------
+                 QUERY PLAN                 
+--------------------------------------------
  Hash Left Join
    Hash Cond: (t1.q1 = q1)
    ->  Seq Scan on int8_tbl t1
    ->  Hash
          ->  Result
+               Replaces: Join on t2, t3, t4
                One-Time Filter: false
-(6 rows)
+(7 rows)
 
 -- deduce constant-false from an EquivalenceClass
 explain (costs off)
@@ -6070,15 +6083,16 @@ select * from int8_tbl t1 left join
   (int8_tbl t2 inner join int8_tbl t3 on (t2.q1-t3.q2) = 0 and (t2.q1-t3.q2) = 1
    left join int8_tbl t4 on t2.q2 = t4.q2)
 on t1.q1 = t2.q1;
-              QUERY PLAN              
---------------------------------------
+                 QUERY PLAN                 
+--------------------------------------------
  Hash Left Join
    Hash Cond: (t1.q1 = q1)
    ->  Seq Scan on int8_tbl t1
    ->  Hash
          ->  Result
+               Replaces: Join on t2, t3, t4
                One-Time Filter: false
-(6 rows)
+(7 rows)
 
 -- pseudoconstant based on an outer-level Param
 explain (costs off)
@@ -6317,8 +6331,9 @@ select p.* from
         QUERY PLAN        
 --------------------------
  Result
+   Replaces: Scan on p
    One-Time Filter: false
-(2 rows)
+(3 rows)
 
 select p.* from
   (parent p left join child c on (p.k = c.k)) join parent x on p.k = x.k
@@ -6334,8 +6349,9 @@ select p.* from
         QUERY PLAN        
 --------------------------
  Result
+   Replaces: Join on p, x
    One-Time Filter: false
-(2 rows)
+(3 rows)
 
 -- bug 5255: this is not optimizable by join removal
 begin;
@@ -6407,8 +6423,9 @@ SELECT q2 FROM
          Output: int4_tbl.f1
    ->  Result
          Output: q2, 'constant'::text
+         Replaces: Scan on int8_tbl
          One-Time Filter: false
-(9 rows)
+(10 rows)
 
 -- join removal bug #17786: check that OR conditions are cleaned up
 EXPLAIN (COSTS OFF)
@@ -6427,8 +6444,9 @@ FROM int4_tbl
                Filter: ((tenk1.unique1 = (42)) OR (tenk1.unique2 = (42)))
                ->  Seq Scan on tenk1
                ->  Result
+                     Replaces: Scan on int8_tbl
                      One-Time Filter: false
-(9 rows)
+(10 rows)
 
 rollback;
 -- another join removal bug: we must clean up correctly when removing a PHV
@@ -6878,8 +6896,9 @@ where q1.x = q2.y;
         QUERY PLAN        
 --------------------------
  Result
+   Replaces: Scan on sj_1
    One-Time Filter: false
-(2 rows)
+(3 rows)
 
 -- We can't use a cross-EC generated self join qual because of current logic of
 -- the generate_join_implied_equalities routine.
@@ -7703,11 +7722,12 @@ select 1 from emp1 full join
         on true
     where false) s on true
 where false;
-        QUERY PLAN        
---------------------------
+            QUERY PLAN            
+----------------------------------
  Result
+   Replaces: Join on emp1, t1, t3
    One-Time Filter: false
-(2 rows)
+(3 rows)
 
 select 1 from emp1 full join
     (select * from emp1 t1 join
@@ -7936,8 +7956,9 @@ where false;
 --------------------------
  Result
    Output: 1
+   Replaces: Scan on ss
    One-Time Filter: false
-(3 rows)
+(4 rows)
 
 --
 -- Test LATERAL
@@ -8875,8 +8896,9 @@ select * from int8_tbl i8 left join lateral
          Output: i8.q1, i8.q2
    ->  Result
          Output: f1, i8.q2
+         Replaces: Scan on int4_tbl
          One-Time Filter: false
-(8 rows)
+(9 rows)
 
 explain (verbose, costs off)
 select * from int8_tbl i8 left join lateral
@@ -8889,8 +8911,9 @@ select * from int8_tbl i8 left join lateral
          Output: i8.q1, i8.q2
    ->  Result
          Output: f1, f1, i8.q2
+         Replaces: Join on i1, i2
          One-Time Filter: false
-(7 rows)
+(8 rows)
 
 -- check handling of nested appendrels inside LATERAL
 select * from
diff --git a/src/test/regress/expected/merge.out b/src/test/regress/expected/merge.out
index cf2219df754..44df626c40c 100644
--- a/src/test/regress/expected/merge.out
+++ b/src/test/regress/expected/merge.out
@@ -2426,8 +2426,9 @@ MERGE INTO pa_target t USING pa_source s ON t.tid = s.sid
                Output: t.tid, t.ctid
                ->  Result
                      Output: t.tid, t.ctid
+                     Replaces: Scan on t
                      One-Time Filter: false
-(12 rows)
+(13 rows)
 
 MERGE INTO pa_target t USING pa_source s ON t.tid = s.sid
   WHEN NOT MATCHED THEN INSERT VALUES (s.sid);
diff --git a/src/test/regress/expected/partition_aggregate.out b/src/test/regress/expected/partition_aggregate.out
index 5f2c0cf5786..cb12bf53719 100644
--- a/src/test/regress/expected/partition_aggregate.out
+++ b/src/test/regress/expected/partition_aggregate.out
@@ -146,13 +146,14 @@ SELECT c, a, count(*) FROM pagg_tab GROUP BY a, c;
 -- Test when input relation for grouping is dummy
 EXPLAIN (COSTS OFF)
 SELECT c, sum(a) FROM pagg_tab WHERE 1 = 2 GROUP BY c;
-           QUERY PLAN           
---------------------------------
+             QUERY PLAN             
+------------------------------------
  HashAggregate
    Group Key: c
    ->  Result
+         Replaces: Scan on pagg_tab
          One-Time Filter: false
-(4 rows)
+(5 rows)
 
 SELECT c, sum(a) FROM pagg_tab WHERE 1 = 2 GROUP BY c;
  c | sum 
@@ -161,12 +162,13 @@ SELECT c, sum(a) FROM pagg_tab WHERE 1 = 2 GROUP BY c;
 
 EXPLAIN (COSTS OFF)
 SELECT c, sum(a) FROM pagg_tab WHERE c = 'x' GROUP BY c;
-           QUERY PLAN           
---------------------------------
+             QUERY PLAN             
+------------------------------------
  GroupAggregate
    ->  Result
+         Replaces: Scan on pagg_tab
          One-Time Filter: false
-(3 rows)
+(4 rows)
 
 SELECT c, sum(a) FROM pagg_tab WHERE c = 'x' GROUP BY c;
  c | sum 
@@ -804,15 +806,16 @@ SELECT a.x, b.y, count(*) FROM (SELECT * FROM pagg_tab1 WHERE x < 20) a FULL JOI
 -- Empty join relation because of empty outer side, no partitionwise agg plan
 EXPLAIN (COSTS OFF)
 SELECT a.x, a.y, count(*) FROM (SELECT * FROM pagg_tab1 WHERE x = 1 AND x = 2) a LEFT JOIN pagg_tab2 b ON a.x = b.y GROUP BY a.x, a.y ORDER BY 1, 2;
-              QUERY PLAN              
---------------------------------------
+                  QUERY PLAN                  
+----------------------------------------------
  GroupAggregate
    Group Key: pagg_tab1.y
    ->  Sort
          Sort Key: pagg_tab1.y
          ->  Result
+               Replaces: Join on b, pagg_tab1
                One-Time Filter: false
-(6 rows)
+(7 rows)
 
 SELECT a.x, a.y, count(*) FROM (SELECT * FROM pagg_tab1 WHERE x = 1 AND x = 2) a LEFT JOIN pagg_tab2 b ON a.x = b.y GROUP BY a.x, a.y ORDER BY 1, 2;
  x | y | count 
diff --git a/src/test/regress/expected/partition_join.out b/src/test/regress/expected/partition_join.out
index 24e06845f92..13fda3f32ef 100644
--- a/src/test/regress/expected/partition_join.out
+++ b/src/test/regress/expected/partition_join.out
@@ -1609,19 +1609,21 @@ SELECT avg(t1.a), avg(t2.b), avg(t3.a + t3.b), t1.c, t2.c, t3.c FROM plt1 t1, pl
 -- joins where one of the relations is proven empty
 EXPLAIN (COSTS OFF)
 SELECT t1.a, t1.c, t2.b, t2.c FROM prt1 t1, prt2 t2 WHERE t1.a = t2.b AND t1.a = 1 AND t1.a = 2;
-        QUERY PLAN        
---------------------------
+         QUERY PLAN         
+----------------------------
  Result
+   Replaces: Join on t1, t2
    One-Time Filter: false
-(2 rows)
+(3 rows)
 
 EXPLAIN (COSTS OFF)
 SELECT t1.a, t1.c, t2.b, t2.c FROM (SELECT * FROM prt1 WHERE a = 1 AND a = 2) t1 LEFT JOIN prt2 t2 ON t1.a = t2.b;
-        QUERY PLAN        
---------------------------
+          QUERY PLAN          
+------------------------------
  Result
+   Replaces: Join on t2, prt1
    One-Time Filter: false
-(2 rows)
+(3 rows)
 
 EXPLAIN (COSTS OFF)
 SELECT t1.a, t1.c, t2.b, t2.c FROM (SELECT * FROM prt1 WHERE a = 1 AND a = 2) t1 RIGHT JOIN prt2 t2 ON t1.a = t2.b, prt1 t3 WHERE t2.b = t3.a;
@@ -1647,8 +1649,9 @@ SELECT t1.a, t1.c, t2.b, t2.c FROM (SELECT * FROM prt1 WHERE a = 1 AND a = 2) t1
                      ->  Seq Scan on prt2_p3 t2_3
    ->  Hash
          ->  Result
+               Replaces: Scan on prt1
                One-Time Filter: false
-(21 rows)
+(22 rows)
 
 EXPLAIN (COSTS OFF)
 SELECT t1.a, t1.c, t2.b, t2.c FROM (SELECT * FROM prt1 WHERE a = 1 AND a = 2) t1 FULL JOIN prt2 t2 ON t1.a = t2.b WHERE t2.a = 0 ORDER BY t1.a, t2.b;
@@ -1667,8 +1670,9 @@ SELECT t1.a, t1.c, t2.b, t2.c FROM (SELECT * FROM prt1 WHERE a = 1 AND a = 2) t1
                      Filter: (a = 0)
          ->  Hash
                ->  Result
+                     Replaces: Scan on prt1
                      One-Time Filter: false
-(14 rows)
+(15 rows)
 
 --
 -- tests for hash partitioned tables.
@@ -2254,8 +2258,9 @@ SELECT t1.a, t1.c, t2.b, t2.c FROM (SELECT * FROM prt1_l WHERE a = 1 AND a = 2)
          ->  Seq Scan on prt2_l_p3_p2 t2_5
    ->  Hash
          ->  Result
+               Replaces: Scan on prt1_l
                One-Time Filter: false
-(11 rows)
+(12 rows)
 
 -- Test case to verify proper handling of subqueries in a partitioned delete.
 -- The weird-looking lateral join is just there to force creation of a
diff --git a/src/test/regress/expected/partition_prune.out b/src/test/regress/expected/partition_prune.out
index 68ecd951809..7499cdb2cdf 100644
--- a/src/test/regress/expected/partition_prune.out
+++ b/src/test/regress/expected/partition_prune.out
@@ -627,8 +627,9 @@ explain (costs off) select * from rlp3 where a = 20;   /* empty */
         QUERY PLAN        
 --------------------------
  Result
+   Replaces: Scan on rlp3
    One-Time Filter: false
-(2 rows)
+(3 rows)
 
 -- redundant clauses are eliminated
 explain (costs off) select * from rlp where a > 1 and a = 10;	/* only default */
@@ -670,8 +671,9 @@ explain (costs off) select * from rlp where a = 1 and a = 3;	/* empty */
         QUERY PLAN        
 --------------------------
  Result
+   Replaces: Scan on rlp
    One-Time Filter: false
-(2 rows)
+(3 rows)
 
 explain (costs off) select * from rlp where (a = 1 and a = 3) or (a > 1 and a = 15);
                             QUERY PLAN                             
@@ -1254,25 +1256,28 @@ select * from boolpart where a is not unknown;
 
 -- check that all partitions are pruned when faced with conflicting clauses
 explain (costs off) select * from boolpart where a is not unknown and a is unknown;
-        QUERY PLAN        
---------------------------
+          QUERY PLAN          
+------------------------------
  Result
+   Replaces: Scan on boolpart
    One-Time Filter: false
-(2 rows)
+(3 rows)
 
 explain (costs off) select * from boolpart where a is false and a is unknown;
-        QUERY PLAN        
---------------------------
+          QUERY PLAN          
+------------------------------
  Result
+   Replaces: Scan on boolpart
    One-Time Filter: false
-(2 rows)
+(3 rows)
 
 explain (costs off) select * from boolpart where a is true and a is unknown;
-        QUERY PLAN        
---------------------------
+          QUERY PLAN          
+------------------------------
  Result
+   Replaces: Scan on boolpart
    One-Time Filter: false
-(2 rows)
+(3 rows)
 
 -- inverse boolean partitioning - a seemingly unlikely design, but we've got
 -- code for it, so we'd better test it.
@@ -1568,11 +1573,12 @@ explain (costs off) select * from coercepart where a = any ('{ab,null}');
 (2 rows)
 
 explain (costs off) select * from coercepart where a = any (null::text[]);
-        QUERY PLAN        
---------------------------
+           QUERY PLAN           
+--------------------------------
  Result
+   Replaces: Scan on coercepart
    One-Time Filter: false
-(2 rows)
+(3 rows)
 
 explain (costs off) select * from coercepart where a = all ('{ab}');
                   QUERY PLAN                  
@@ -1582,25 +1588,28 @@ explain (costs off) select * from coercepart where a = all ('{ab}');
 (2 rows)
 
 explain (costs off) select * from coercepart where a = all ('{ab,bc}');
-        QUERY PLAN        
---------------------------
+           QUERY PLAN           
+--------------------------------
  Result
+   Replaces: Scan on coercepart
    One-Time Filter: false
-(2 rows)
+(3 rows)
 
 explain (costs off) select * from coercepart where a = all ('{ab,null}');
-        QUERY PLAN        
---------------------------
+           QUERY PLAN           
+--------------------------------
  Result
+   Replaces: Scan on coercepart
    One-Time Filter: false
-(2 rows)
+(3 rows)
 
 explain (costs off) select * from coercepart where a = all (null::text[]);
-        QUERY PLAN        
---------------------------
+           QUERY PLAN           
+--------------------------------
  Result
+   Replaces: Scan on coercepart
    One-Time Filter: false
-(2 rows)
+(3 rows)
 
 drop table coercepart;
 CREATE TABLE part (a INT, b INT) PARTITION BY LIST (a);
@@ -1772,8 +1781,9 @@ explain (costs off) select * from lp where a <> 'a' and a is null;
         QUERY PLAN        
 --------------------------
  Result
+   Replaces: Scan on lp
    One-Time Filter: false
-(2 rows)
+(3 rows)
 
 explain (costs off) select * from lp where (a <> 'a' and a <> 'd') or a is null;
                                   QUERY PLAN                                  
@@ -1866,22 +1876,24 @@ create table lparted_by_int2 (a smallint) partition by list (a);
 create table lparted_by_int2_1 partition of lparted_by_int2 for values in (1);
 create table lparted_by_int2_16384 partition of lparted_by_int2 for values in (16384);
 explain (costs off) select * from lparted_by_int2 where a = 100_000_000_000_000;
-        QUERY PLAN        
---------------------------
+             QUERY PLAN              
+-------------------------------------
  Result
+   Replaces: Scan on lparted_by_int2
    One-Time Filter: false
-(2 rows)
+(3 rows)
 
 create table rparted_by_int2 (a smallint) partition by range (a);
 create table rparted_by_int2_1 partition of rparted_by_int2 for values from (1) to (10);
 create table rparted_by_int2_16384 partition of rparted_by_int2 for values from (10) to (16384);
 -- all partitions pruned
 explain (costs off) select * from rparted_by_int2 where a > 100_000_000_000_000;
-        QUERY PLAN        
---------------------------
+             QUERY PLAN              
+-------------------------------------
  Result
+   Replaces: Scan on rparted_by_int2
    One-Time Filter: false
-(2 rows)
+(3 rows)
 
 create table rparted_by_int2_maxvalue partition of rparted_by_int2 for values from (16384) to (maxvalue);
 -- all partitions but rparted_by_int2_maxvalue pruned
@@ -2131,8 +2143,9 @@ explain (costs off) select * from hp where a = 1 and b = 'abcde' and
         QUERY PLAN        
 --------------------------
  Result
+   Replaces: Scan on hp
    One-Time Filter: false
-(2 rows)
+(3 rows)
 
 --
 -- Test runtime partition pruning
@@ -3390,11 +3403,12 @@ select * from stable_qual_pruning where a < '2000-02-01'::timestamptz;
 explain (analyze, costs off, summary off, timing off, buffers off)
 select * from stable_qual_pruning
   where a = any(array['2010-02-01', '2020-01-01']::timestamp[]);
-            QUERY PLAN             
------------------------------------
+               QUERY PLAN                
+-----------------------------------------
  Result (actual rows=0.00 loops=1)
+   Replaces: Scan on stable_qual_pruning
    One-Time Filter: false
-(2 rows)
+(3 rows)
 
 explain (analyze, costs off, summary off, timing off, buffers off)
 select * from stable_qual_pruning
@@ -3642,6 +3656,7 @@ explain (analyze, costs off, summary off, timing off, buffers off) select * from
    Sort Key: ma_test.b
    InitPlan 2
      ->  Result (actual rows=1.00 loops=1)
+           Replaces: MinMaxAggregate
            InitPlan 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)
@@ -3656,7 +3671,7 @@ explain (analyze, costs off, summary off, timing off, buffers off) select * from
    ->  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)
          Index Searches: 1
-(18 rows)
+(19 rows)
 
 reset enable_seqscan;
 reset enable_sort;
@@ -3678,11 +3693,12 @@ explain (costs off) select * from pp_arrpart where a = '{1}';
 (2 rows)
 
 explain (costs off) select * from pp_arrpart where a = '{1, 2}';
-        QUERY PLAN        
---------------------------
+           QUERY PLAN           
+--------------------------------
  Result
+   Replaces: Scan on pp_arrpart
    One-Time Filter: false
-(2 rows)
+(3 rows)
 
 explain (costs off) select * from pp_arrpart where a in ('{4, 5}', '{1}');
                               QUERY PLAN                              
@@ -3764,11 +3780,12 @@ explain (costs off) select * from pp_enumpart where a = 'blue';
 (2 rows)
 
 explain (costs off) select * from pp_enumpart where a = 'black';
-        QUERY PLAN        
---------------------------
+           QUERY PLAN            
+---------------------------------
  Result
+   Replaces: Scan on pp_enumpart
    One-Time Filter: false
-(2 rows)
+(3 rows)
 
 drop table pp_enumpart;
 drop type pp_colors;
@@ -3785,11 +3802,12 @@ explain (costs off) select * from pp_recpart where a = '(1,1)'::pp_rectype;
 (2 rows)
 
 explain (costs off) select * from pp_recpart where a = '(1,2)'::pp_rectype;
-        QUERY PLAN        
---------------------------
+           QUERY PLAN           
+--------------------------------
  Result
+   Replaces: Scan on pp_recpart
    One-Time Filter: false
-(2 rows)
+(3 rows)
 
 drop table pp_recpart;
 drop type pp_rectype;
@@ -3805,11 +3823,12 @@ explain (costs off) select * from pp_intrangepart where a = '[1,2]'::int4range;
 (2 rows)
 
 explain (costs off) select * from pp_intrangepart where a = '(1,2)'::int4range;
-        QUERY PLAN        
---------------------------
+             QUERY PLAN              
+-------------------------------------
  Result
+   Replaces: Scan on pp_intrangepart
    One-Time Filter: false
-(2 rows)
+(3 rows)
 
 drop table pp_intrangepart;
 --
@@ -4125,19 +4144,21 @@ explain (costs off) update listp1 set a = 1 where a = 2;
 -- constraint exclusion enabled
 set constraint_exclusion to 'on';
 explain (costs off) select * from listp1 where a = 2;
-        QUERY PLAN        
---------------------------
+         QUERY PLAN         
+----------------------------
  Result
+   Replaces: Scan on listp1
    One-Time Filter: false
-(2 rows)
+(3 rows)
 
 explain (costs off) update listp1 set a = 1 where a = 2;
-           QUERY PLAN           
---------------------------------
+            QUERY PLAN            
+----------------------------------
  Update on listp1
    ->  Result
+         Replaces: Scan on listp1
          One-Time Filter: false
-(3 rows)
+(4 rows)
 
 reset constraint_exclusion;
 reset enable_partition_pruning;
@@ -4524,18 +4545,20 @@ create table hp_contradict_test (a int, b int) partition by hash (a part_test_in
 create table hp_contradict_test_p1 partition of hp_contradict_test for values with (modulus 2, remainder 0);
 create table hp_contradict_test_p2 partition of hp_contradict_test for values with (modulus 2, remainder 1);
 explain (costs off) select * from hp_contradict_test where a is null and a === 1 and b === 1;
-        QUERY PLAN        
---------------------------
+               QUERY PLAN               
+----------------------------------------
  Result
+   Replaces: Scan on hp_contradict_test
    One-Time Filter: false
-(2 rows)
+(3 rows)
 
 explain (costs off) select * from hp_contradict_test where a === 1 and b === 1 and a is null;
-        QUERY PLAN        
---------------------------
+               QUERY PLAN               
+----------------------------------------
  Result
+   Replaces: Scan on hp_contradict_test
    One-Time Filter: false
-(2 rows)
+(3 rows)
 
 drop table hp_contradict_test;
 drop operator class part_test_int4_ops2 using hash;
diff --git a/src/test/regress/expected/predicate.out b/src/test/regress/expected/predicate.out
index 1aff0b59ff8..304b6868b90 100644
--- a/src/test/regress/expected/predicate.out
+++ b/src/test/regress/expected/predicate.out
@@ -36,8 +36,9 @@ SELECT * FROM pred_tab t WHERE t.a IS NULL;
         QUERY PLAN        
 --------------------------
  Result
+   Replaces: Scan on t
    One-Time Filter: false
-(2 rows)
+(3 rows)
 
 -- Ensure the IS_NULL qual is not reduced to constant-FALSE on nullable
 -- columns
@@ -77,8 +78,9 @@ SELECT * FROM pred_tab t WHERE t.a IS NULL OR t.c IS NULL;
         QUERY PLAN        
 --------------------------
  Result
+   Replaces: Scan on t
    One-Time Filter: false
-(2 rows)
+(3 rows)
 
 -- Ensure the OR clause is not reduced to constant-FALSE when not all branches
 -- are provably false
@@ -139,8 +141,9 @@ SELECT * FROM pred_tab t1
    Join Filter: false
    ->  Seq Scan on pred_tab t1
    ->  Result
+         Replaces: Scan on t2
          One-Time Filter: false
-(5 rows)
+(6 rows)
 
 -- Ensure the IS_NULL qual is not reduced to constant-FALSE when the column is
 -- nullable by an outer join
@@ -209,8 +212,9 @@ SELECT * FROM pred_tab t1
    Join Filter: false
    ->  Seq Scan on pred_tab t1
    ->  Result
+         Replaces: Scan on t2
          One-Time Filter: false
-(5 rows)
+(6 rows)
 
 -- Ensure the OR clause is not reduced to constant-FALSE when a column is
 -- made nullable from an outer join
@@ -267,17 +271,18 @@ SELECT * FROM pred_tab t1
     LEFT JOIN pred_tab t2 ON EXISTS
         (SELECT 1 FROM pred_tab t3, pred_tab t4, pred_tab t5, pred_tab t6
          WHERE t1.a = t3.a AND t6.a IS NULL);
-             QUERY PLAN              
--------------------------------------
+                 QUERY PLAN                 
+--------------------------------------------
  Nested Loop Left Join
    Join Filter: (InitPlan 1).col1
    InitPlan 1
      ->  Result
+           Replaces: Join on t3, t4, t5, t6
            One-Time Filter: false
    ->  Seq Scan on pred_tab t1
    ->  Materialize
          ->  Seq Scan on pred_tab t2
-(8 rows)
+(9 rows)
 
 DROP TABLE pred_tab;
 -- Validate we handle IS NULL and IS NOT NULL quals correctly with inheritance
@@ -418,20 +423,22 @@ SET constraint_exclusion TO ON;
 -- Ensure that we get a dummy plan
 EXPLAIN (COSTS OFF)
 SELECT * FROM pred_tab1, pred_tab2 WHERE pred_tab2.a IS NULL;
-        QUERY PLAN        
---------------------------
+                QUERY PLAN                
+------------------------------------------
  Result
+   Replaces: Join on pred_tab1, pred_tab2
    One-Time Filter: false
-(2 rows)
+(3 rows)
 
 -- Ensure that we get a dummy plan
 EXPLAIN (COSTS OFF)
 SELECT * FROM pred_tab2, pred_tab1 WHERE pred_tab1.a IS NULL OR pred_tab1.b < 2;
-        QUERY PLAN        
---------------------------
+                QUERY PLAN                
+------------------------------------------
  Result
+   Replaces: Join on pred_tab2, pred_tab1
    One-Time Filter: false
-(2 rows)
+(3 rows)
 
 RESET constraint_exclusion;
 DROP TABLE pred_tab1;
diff --git a/src/test/regress/expected/rowsecurity.out b/src/test/regress/expected/rowsecurity.out
index 8c879509313..7153ebba521 100644
--- a/src/test/regress/expected/rowsecurity.out
+++ b/src/test/regress/expected/rowsecurity.out
@@ -3606,8 +3606,9 @@ EXPLAIN (COSTS OFF) SELECT * FROM t1;
         QUERY PLAN        
 --------------------------
  Result
+   Replaces: Scan on t1
    One-Time Filter: false
-(2 rows)
+(3 rows)
 
 SET SESSION AUTHORIZATION regress_rls_bob;
 SELECT * FROM t1;
@@ -3619,8 +3620,9 @@ EXPLAIN (COSTS OFF) SELECT * FROM t1;
         QUERY PLAN        
 --------------------------
  Result
+   Replaces: Scan on t1
    One-Time Filter: false
-(2 rows)
+(3 rows)
 
 --
 -- COPY TO/FROM
@@ -4524,11 +4526,12 @@ SELECT * FROM rls_tbl WHERE a <<< 1000;
 (0 rows)
 
 EXPLAIN (COSTS OFF) SELECT * FROM rls_tbl WHERE a <<< 1000 or a <<< 900;
-        QUERY PLAN        
---------------------------
+         QUERY PLAN          
+-----------------------------
  Result
+   Replaces: Scan on rls_tbl
    One-Time Filter: false
-(2 rows)
+(3 rows)
 
 RESET SESSION AUTHORIZATION;
 CREATE TABLE rls_child_tbl () INHERITS (rls_tbl);
diff --git a/src/test/regress/expected/rowtypes.out b/src/test/regress/expected/rowtypes.out
index d84122881af..dd52d96d50f 100644
--- a/src/test/regress/expected/rowtypes.out
+++ b/src/test/regress/expected/rowtypes.out
@@ -1325,8 +1325,9 @@ where false;
 --------------------------
  Result
    Output: (a).f1, (a).f2
+   Replaces: Scan on ss
    One-Time Filter: false
-(3 rows)
+(4 rows)
 
 explain (verbose, costs off)
 with cte(c) as materialized (select row(1, 2)),
@@ -1350,11 +1351,12 @@ where false;
 -----------------------------------
  Result
    Output: (cte.c).f1
+   Replaces: Scan on cte
    One-Time Filter: false
    CTE cte
      ->  Result
            Output: '(1,2)'::record
-(6 rows)
+(7 rows)
 
 --
 -- Tests for component access / FieldSelect
diff --git a/src/test/regress/expected/select.out b/src/test/regress/expected/select.out
index bab0cc93ff5..2cdb78d1e9a 100644
--- a/src/test/regress/expected/select.out
+++ b/src/test/regress/expected/select.out
@@ -962,10 +962,11 @@ create table list_parted_tbl (a int,b int) partition by list (a);
 create table list_parted_tbl1 partition of list_parted_tbl
   for values in (1) partition by list(b);
 explain (costs off) select * from list_parted_tbl;
-        QUERY PLAN        
---------------------------
+             QUERY PLAN              
+-------------------------------------
  Result
+   Replaces: Scan on list_parted_tbl
    One-Time Filter: false
-(2 rows)
+(3 rows)
 
 drop table list_parted_tbl;
diff --git a/src/test/regress/expected/subselect.out b/src/test/regress/expected/subselect.out
index 76ffb2b9027..b91ff5e8fe9 100644
--- a/src/test/regress/expected/subselect.out
+++ b/src/test/regress/expected/subselect.out
@@ -2007,8 +2007,9 @@ select * from json_tab t1 left join (select json_array(1, a) from json_tab t2) s
          Output: t1.a
    ->  Result
          Output: JSON_ARRAY(1, a RETURNING json)
+         Replaces: Scan on t2
          One-Time Filter: false
-(8 rows)
+(9 rows)
 
 select * from json_tab t1 left join (select json_array(1, a) from json_tab t2) s on false;
  a | json_array 
@@ -2899,12 +2900,13 @@ ON B.hundred in (SELECT min(c.hundred) FROM tenk2 C WHERE c.odd = b.odd);
                      ->  Subquery Scan on unnamed_subquery
                            Filter: (b.hundred = unnamed_subquery.min)
                            ->  Result
+                                 Replaces: MinMaxAggregate
                                  InitPlan 1
                                    ->  Limit
                                          ->  Index Scan using tenk2_hundred on tenk2 c
                                                Index Cond: (hundred IS NOT NULL)
                                                Filter: (odd = b.odd)
-(16 rows)
+(17 rows)
 
 --
 -- Test VALUES to ARRAY (VtA) transformation
@@ -3067,8 +3069,9 @@ EXPLAIN (COSTS OFF) EXECUTE test(NULL, 3.14, NULL);
         QUERY PLAN        
 --------------------------
  Result
+   Replaces: Scan on onek
    One-Time Filter: false
-(2 rows)
+(3 rows)
 
 EXPLAIN (COSTS OFF) EXECUTE test(NULL, 3.14, '-1.5');
                                                                      QUERY PLAN                                                                      
diff --git a/src/test/regress/expected/tsrf.out b/src/test/regress/expected/tsrf.out
index d47b5f6ec57..c4f7b187f5b 100644
--- a/src/test/regress/expected/tsrf.out
+++ b/src/test/regress/expected/tsrf.out
@@ -91,8 +91,9 @@ SELECT unnest(ARRAY[1, 2]) FROM few WHERE false;
  ProjectSet
    Output: unnest('{1,2}'::integer[])
    ->  Result
+         Replaces: Scan on few
          One-Time Filter: false
-(4 rows)
+(5 rows)
 
 SELECT unnest(ARRAY[1, 2]) FROM few WHERE false;
  unnest 
@@ -107,8 +108,9 @@ SELECT * FROM few f1,
 ------------------------------------------------
  Result
    Output: f1.id, f1.dataa, f1.datab, ss.unnest
+   Replaces: Join on f1, ss
    One-Time Filter: false
-(3 rows)
+(4 rows)
 
 SELECT * FROM few f1,
   (SELECT unnest(ARRAY[1,2]) FROM few f2 WHERE false OFFSET 0) ss;
diff --git a/src/test/regress/sql/explain.sql b/src/test/regress/sql/explain.sql
index 7842d25ded3..ebdab42604b 100644
--- a/src/test/regress/sql/explain.sql
+++ b/src/test/regress/sql/explain.sql
@@ -61,6 +61,7 @@ set track_io_timing = off;
 
 -- Simple cases
 
+explain (costs off) select 1 as a, 2 as b having false;
 select explain_filter('explain select * from int8_tbl i8');
 select explain_filter('explain (analyze, buffers off) select * from int8_tbl i8');
 select explain_filter('explain (analyze, buffers off, verbose) select * from int8_tbl i8');
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index e90af5b2ad3..3c80d49b67e 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2568,6 +2568,7 @@ RestrictInfo
 Result
 ResultRelInfo
 ResultState
+ResultType
 RetainDeadTuplesData
 RetainDeadTuplesPhase
 ReturnSetInfo
-- 
2.39.5 (Apple Git-154)

v7-0003-Ensure-that-all-joinrel-RTIs-are-discoverable-fro.patchapplication/octet-stream; name=v7-0003-Ensure-that-all-joinrel-RTIs-are-discoverable-fro.patchDownload
From a2ebae8e37bbdb5b533e9d200cab9fbdb38519d0 Mon Sep 17 00:00:00 2001
From: Robert Haas <rhaas@postgresql.org>
Date: Tue, 16 Sep 2025 10:05:02 -0400
Subject: [PATCH v7 3/7] 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.

We also now assert when constructing a Join plan that we can find all of
the joinrel's RTIs and no others in the plan tree -- either associated
with a node below the outer or inner side of the join, or in the
'ojrelids' field of the Join itself. Any RTIs appearing in this last
place must be of type RTE_JOIN; scanned relations need to be associated
with underlying scan nodes.

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.

Note that this depends on the earlier commit to add a relids field to
Result nodes; without that change, a join tree involving two or more
Result nodes would be fundamentally ambiguous (and even a join tree
involving one could only be interpreted by guessing at its origin).
---
 .../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       | 200 +++++++++++++++++-
 src/include/nodes/plannodes.h                 |   2 +
 5 files changed, 272 insertions(+), 5 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..6fa782d7c58 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -161,6 +161,7 @@ static CustomScan *create_customscan_plan(PlannerInfo *root,
 static NestLoop *create_nestloop_plan(PlannerInfo *root, NestPath *best_path);
 static MergeJoin *create_mergejoin_plan(PlannerInfo *root, MergePath *best_path);
 static HashJoin *create_hashjoin_plan(PlannerInfo *root, HashPath *best_path);
+static Bitmapset *compute_join_ojrelids(PlannerInfo *root, JoinPath *jpath);
 static Node *replace_nestloop_params(PlannerInfo *root, Node *expr);
 static Node *replace_nestloop_params_mutator(Node *node, PlannerInfo *root);
 static void fix_indexqual_references(PlannerInfo *root, IndexPath *index_path,
@@ -232,6 +233,7 @@ static BitmapOr *make_bitmap_or(List *bitmapplans);
 static NestLoop *make_nestloop(List *tlist,
 							   List *joinclauses, List *otherclauses, List *nestParams,
 							   Plan *lefttree, Plan *righttree,
+							   Bitmapset *ojrelids,
 							   JoinType jointype, bool inner_unique);
 static HashJoin *make_hashjoin(List *tlist,
 							   List *joinclauses, List *otherclauses,
@@ -239,6 +241,7 @@ static HashJoin *make_hashjoin(List *tlist,
 							   List *hashoperators, List *hashcollations,
 							   List *hashkeys,
 							   Plan *lefttree, Plan *righttree,
+							   Bitmapset *ojrelids,
 							   JoinType jointype, bool inner_unique);
 static Hash *make_hash(Plan *lefttree,
 					   List *hashkeys,
@@ -253,6 +256,7 @@ static MergeJoin *make_mergejoin(List *tlist,
 								 bool *mergereversals,
 								 bool *mergenullsfirst,
 								 Plan *lefttree, Plan *righttree,
+								 Bitmapset *ojrelids,
 								 JoinType jointype, bool inner_unique,
 								 bool skip_mark_restore);
 static Sort *make_sort(Plan *lefttree, int numCols,
@@ -320,7 +324,15 @@ static ModifyTable *make_modifytable(PlannerInfo *root, Plan *subplan,
 									 int epqParam);
 static GatherMerge *create_gather_merge_plan(PlannerInfo *root,
 											 GatherMergePath *best_path);
-
+static void assert_join_preserves_rtis(PlannerInfo *root,
+									   RelOptInfo *rel,
+									   Plan *outer_plan,
+									   Plan *inner_plan,
+									   Bitmapset *ojrelids);
+
+#ifdef USE_ASSERT_CHECKING
+static Bitmapset *get_plan_rtis(PlannerInfo *root, Plan *plan);
+#endif
 
 /*
  * create_plan
@@ -4335,11 +4347,16 @@ create_nestloop_plan(PlannerInfo *root,
 							  nestParams,
 							  outer_plan,
 							  inner_plan,
+							  compute_join_ojrelids(root, &best_path->jpath),
 							  best_path->jpath.jointype,
 							  best_path->jpath.inner_unique);
 
 	copy_generic_path_info(&join_plan->join.plan, &best_path->jpath.path);
 
+	assert_join_preserves_rtis(root, best_path->jpath.path.parent,
+							   outer_plan, inner_plan,
+							   join_plan->join.ojrelids);
+
 	return join_plan;
 }
 
@@ -4687,6 +4704,7 @@ create_mergejoin_plan(PlannerInfo *root,
 							   mergenullsfirst,
 							   outer_plan,
 							   inner_plan,
+							   compute_join_ojrelids(root, &best_path->jpath),
 							   best_path->jpath.jointype,
 							   best_path->jpath.inner_unique,
 							   best_path->skip_mark_restore);
@@ -4694,6 +4712,10 @@ create_mergejoin_plan(PlannerInfo *root,
 	/* Costs of sort and material steps are included in path cost already */
 	copy_generic_path_info(&join_plan->join.plan, &best_path->jpath.path);
 
+	assert_join_preserves_rtis(root, best_path->jpath.path.parent,
+							   outer_plan, inner_plan,
+							   join_plan->join.ojrelids);
+
 	return join_plan;
 }
 
@@ -4862,14 +4884,71 @@ create_hashjoin_plan(PlannerInfo *root,
 							  outer_hashkeys,
 							  outer_plan,
 							  (Plan *) hash_plan,
+							  compute_join_ojrelids(root, &best_path->jpath),
 							  best_path->jpath.jointype,
 							  best_path->jpath.inner_unique);
 
 	copy_generic_path_info(&join_plan->join.plan, &best_path->jpath.path);
 
+	assert_join_preserves_rtis(root, best_path->jpath.path.parent,
+							   outer_plan, inner_plan,
+							   join_plan->join.ojrelids);
+
 	return join_plan;
 }
 
+/*
+ * compute_join_ojrelids
+ *	  Determine the set of outer joins completed by this join.
+ *
+ * See add_outer_joins_to_relids for related logic. When we execute joins
+ * in syntactic order, this will compute a 1-item RTI set for outer joins
+ * and the empty set for inner joins. When we rearrange the join order using
+ * outer-join identity 3, the outer join isn't fully calculated until the
+ * commuted join has also been computed, so an upper outer join can complete
+ * multiple outer joins while a lower one completes none.
+ */
+static Bitmapset *
+compute_join_ojrelids(PlannerInfo *root, JoinPath *jpath)
+{
+	Bitmapset  *ojrelids;
+
+	ojrelids = jpath->path.parent->relids;
+	ojrelids = bms_difference(ojrelids, jpath->outerjoinpath->parent->relids);
+	ojrelids = bms_difference(ojrelids, jpath->innerjoinpath->parent->relids);
+
+#ifdef USE_ASSERT_CHECKING
+	switch (jpath->jointype)
+	{
+		case JOIN_INNER:
+			/* Inner joins should never complete outer joins. */
+			Assert(ojrelids == NULL);
+			break;
+		case JOIN_FULL:
+			/* Full outer joins cannot be commuted. */
+			Assert(bms_membership(ojrelids) == BMS_SINGLETON);
+			break;
+		default:
+			/* Other types of joins can be rearranged. */
+			break;
+	}
+
+	/* Any RTIs in the ojrelids set should be of type RTE_JOIN. */
+	if (ojrelids != NULL)
+	{
+		int			rti = -1;
+
+		while ((rti = bms_next_member(ojrelids, rti)) >= 0)
+		{
+			RangeTblEntry *rte = planner_rt_fetch(rti, root);
+
+			Assert(rte->rtekind == RTE_JOIN);
+		}
+	}
+#endif
+
+	return ojrelids;
+}
 
 /*****************************************************************************
  *
@@ -5934,6 +6013,7 @@ make_nestloop(List *tlist,
 			  List *nestParams,
 			  Plan *lefttree,
 			  Plan *righttree,
+			  Bitmapset *ojrelids,
 			  JoinType jointype,
 			  bool inner_unique)
 {
@@ -5947,6 +6027,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;
@@ -5962,6 +6043,7 @@ make_hashjoin(List *tlist,
 			  List *hashkeys,
 			  Plan *lefttree,
 			  Plan *righttree,
+			  Bitmapset *ojrelids,
 			  JoinType jointype,
 			  bool inner_unique)
 {
@@ -5979,6 +6061,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;
 }
@@ -6017,6 +6100,7 @@ make_mergejoin(List *tlist,
 			   bool *mergenullsfirst,
 			   Plan *lefttree,
 			   Plan *righttree,
+			   Bitmapset *ojrelids,
 			   JoinType jointype,
 			   bool inner_unique,
 			   bool skip_mark_restore)
@@ -6037,6 +6121,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;
 }
@@ -7316,3 +7401,116 @@ is_projection_capable_plan(Plan *plan)
 	}
 	return true;
 }
+
+/*
+ * Check that every joinrel RTI appears in the inner or outer plan or in this
+ * Join's ojrelids set.
+ */
+static void
+assert_join_preserves_rtis(PlannerInfo *root, RelOptInfo *rel,
+						   Plan *outer_plan, Plan *inner_plan,
+						   Bitmapset *ojrelids)
+{
+#ifdef USE_ASSERT_CHECKING
+	Bitmapset  *outerrelids;
+	Bitmapset  *innerrelids;
+	Bitmapset  *joinrelids;
+
+	/* Find outer and inner relid sets. */
+	outerrelids = get_plan_rtis(root, outer_plan);
+	innerrelids = get_plan_rtis(root, inner_plan);
+
+	/* Any given scan RTI should appear in just one set. */
+	Assert(!bms_overlap(innerrelids, outerrelids));
+	Assert(!bms_overlap(outerrelids, ojrelids));
+	Assert(!bms_overlap(innerrelids, ojrelids));
+
+	/* Combine all three sets and check that all RTIs were preserved. */
+	joinrelids = bms_union(ojrelids, bms_union(innerrelids, outerrelids));
+	Assert(bms_equal(joinrelids, rel->relids));
+#endif
+}
+
+#ifdef USE_ASSERT_CHECKING
+/*
+ * Get the set of range table indexes for a scan or join node, or any executor
+ * node that could appear beneath a scan or join node.
+ *
+ * We're only interested in RTIs from within the same subquery, so we do not
+ * attempt to look through T_SubqueryScan here.
+ *
+ * When adding new cases to this function, be sure to also update
+ * ExplainPreScanNode, ExplainNode, and overexplain_per_node_hook as
+ * appropriate.
+ */
+static Bitmapset *
+get_plan_rtis(PlannerInfo *root, Plan *plan)
+{
+	switch (nodeTag(plan))
+	{
+		case T_SeqScan:
+		case T_SampleScan:
+		case T_IndexScan:
+		case T_IndexOnlyScan:
+		case T_BitmapHeapScan:
+		case T_TidScan:
+		case T_TidRangeScan:
+		case T_SubqueryScan:
+		case T_FunctionScan:
+		case T_TableFuncScan:
+		case T_ValuesScan:
+		case T_CteScan:
+		case T_NamedTuplestoreScan:
+		case T_WorkTableScan:
+			return bms_make_singleton(((Scan *) plan)->scanrelid);
+			break;
+		case T_ForeignScan:
+			return ((ForeignScan *) plan)->fs_relids;
+			break;
+		case T_CustomScan:
+			return ((CustomScan *) plan)->custom_relids;
+			break;
+		case T_Append:
+			return ((Append *) plan)->apprelids;
+			break;
+		case T_MergeAppend:
+			return ((MergeAppend *) plan)->apprelids;
+			break;
+		case T_Result:
+			if (plan->lefttree)
+				return get_plan_rtis(root, plan->lefttree);
+			else
+				return ((Result *) plan)->relids;
+			break;
+		case T_HashJoin:
+		case T_MergeJoin:
+		case T_NestLoop:
+			{
+				Bitmapset  *outerrelids;
+				Bitmapset  *innerrelids;
+
+				outerrelids = get_plan_rtis(root, plan->lefttree);
+				innerrelids = get_plan_rtis(root, plan->righttree);
+
+				return bms_union(bms_union(outerrelids, innerrelids),
+								 ((Join *) plan)->ojrelids);
+				break;
+			}
+		case T_Sort:
+		case T_IncrementalSort:
+		case T_Unique:
+		case T_Agg:
+		case T_Hash:
+		case T_Gather:
+		case T_GatherMerge:
+		case T_Material:
+		case T_Memoize:
+			return get_plan_rtis(root, plan->lefttree);
+			break;
+		default:
+			break;
+	}
+
+	return NULL;
+}
+#endif
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.39.5 (Apple Git-154)

v7-0004-Give-subplans-names-that-are-known-while-planning.patchapplication/octet-stream; name=v7-0004-Give-subplans-names-that-are-known-while-planning.patchDownload
From e356fdcd9b451d26bd7d0d660245d9f2c5e96772 Mon Sep 17 00:00:00 2001
From: Robert Haas <rhaas@postgresql.org>
Date: Thu, 5 Dec 2024 15:19:17 -0500
Subject: [PATCH v7 4/7] Give subplans names that are known while planning that
 subplan.

Previously, subplans were shown in EXPLAIN output identified by
a number, like "InitPlan 1", and some were identified by a name,
like "CTE foo". Now, each subplan gets a name, which for InitPlans
and SubPlans is based on the type of sublink e.g. expr_1 or any_1,
and these names are guaranteed to be unique across the whole plan.

The numerical portion of the name may be different than it was
previously, because InitPlan 1 meant the first subplan that we
finished planning (which happened to be an InitPlan). This number
couldn't be known at the time we began planning that subplan,
because the query planner might recurse into other subplans which
would then be fully planned before finishing the plan at the outer
level. These new subplan names are assigned when we *start* planning
a subplan, which allows extensions that affect planning to know the
name that will ultimately be assigned while planning is still in
progress.

Some subplans aren't shown as subplans in EXPLAIN output. This
happens when the subquery is a FROM-cluse item or a branch of a
set operation, rather than, for example, an expression that will
be transformed into something render as an InitPlan or SubPlan.
These subplans also get unique names, although those names are not
currently shown in the EXPLAIN output. This means that it's now
possible to use unique, human-readable names to refer to any
subplan within a query; only the topmost query level is nameless.
---
 .../postgres_fdw/expected/postgres_fdw.out    |  58 ++--
 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        |  83 +++--
 src/backend/optimizer/prep/prepjointree.c     |   4 +
 src/backend/optimizer/prep/prepunion.c        |   5 +-
 src/include/nodes/pathnodes.h                 |   6 +
 src/include/nodes/primnodes.h                 |   1 +
 src/include/optimizer/planner.h               |   5 +-
 src/test/regress/expected/aggregates.out      |  58 ++--
 src/test/regress/expected/create_index.out    |  14 +-
 src/test/regress/expected/groupingsets.out    |  90 +++---
 .../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 | 292 +++++++++---------
 src/test/regress/expected/portals.out         |  12 +-
 src/test/regress/expected/predicate.out       |   8 +-
 src/test/regress/expected/returning.out       |  20 +-
 src/test/regress/expected/rowsecurity.out     | 138 ++++-----
 src/test/regress/expected/rowtypes.out        |  12 +-
 src/test/regress/expected/select_parallel.out |  52 ++--
 src/test/regress/expected/sqljson.out         |   4 +-
 src/test/regress/expected/subselect.out       | 174 +++++------
 src/test/regress/expected/updatable_views.out |  48 +--
 src/test/regress/expected/update.out          |   8 +-
 src/test/regress/expected/window.out          |  10 +-
 src/test/regress/expected/with.out            |  20 +-
 34 files changed, 760 insertions(+), 630 deletions(-)

diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 6dc04e916dc..d673acdf6fa 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -3178,10 +3178,10 @@ select exists(select 1 from pg_enum), sum(c1) from ft1;
                     QUERY PLAN                    
 --------------------------------------------------
  Foreign Scan
-   Output: (InitPlan 1).col1, (sum(ft1.c1))
+   Output: (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: (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: ((expr_1))
    ->  Sort
-         Output: ((SubPlan 1))
-         Sort Key: ((SubPlan 1))
+         Output: ((expr_1))
+         Sort Key: ((expr_1))
          ->  Foreign Scan
-               Output: (SubPlan 1)
+               Output: (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: ((expr_1))
    ->  Sort
-         Output: ((SubPlan 1))
-         Sort Key: ((SubPlan 1))
+         Output: ((expr_1))
+         Sort Key: ((expr_1))
          ->  Foreign Scan on public.ft2 t2
-               Output: (SubPlan 1)
+               Output: (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 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))
@@ -6449,9 +6449,9 @@ UPDATE ft2 AS target SET (c2, c7) = (
  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: (multiexpr_1).col1, (multiexpr_1).col2, (rescan 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"))
@@ -12135,9 +12135,9 @@ SELECT * FROM local_tbl t1 LEFT JOIN (SELECT *, (SELECT count(*) FROM async_pt W
                                        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, ((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, (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, (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 = (any_1).col1) AND ((random() > '0'::double precision) = (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..988bbd19ab4 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, 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 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..22848df76d2 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 char *sublinktype_to_string(SubLinkType subLinkType);
 
 
 /*
@@ -172,6 +173,7 @@ make_subplan(PlannerInfo *root, Query *orig_subquery,
 	Plan	   *plan;
 	List	   *plan_params;
 	Node	   *result;
+	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, &paramIds);
 		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,31 @@ 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 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);
+}
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/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..af50831c814 100644
--- a/src/include/optimizer/planner.h
+++ b/src/include/optimizer/planner.h
@@ -43,7 +43,7 @@ extern PlannedStmt *standard_planner(Query *parse, const char *query_string,
 									 ParamListInfo boundParams);
 
 extern PlannerInfo *subquery_planner(PlannerGlobal *glob, Query *parse,
-									 PlannerInfo *parent_root,
+									 char *plan_name, PlannerInfo *parent_root,
 									 bool hasRecursion, double tuple_fraction,
 									 SetOperationStmt *setops);
 
@@ -62,4 +62,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, 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..825e95fa59c 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(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: (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: ((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: ((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: (((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: ((minmax_1).col1), ((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: (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..dff9a687145 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, (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 = (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..68c4b86ffb4 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((expr_1)), ((expr_2))
+   Group Key: ((expr_2))
    ->  Sort
-         Output: ((SubPlan 2)), i1.q1
-         Sort Key: ((SubPlan 2))
+         Output: ((expr_2)), i1.q1
+         Sort Key: ((expr_2))
          ->  Seq Scan on public.int8_tbl i1
-               Output: (SubPlan 2), i1.q1
-               SubPlan 2
+               Output: (expr_2), i1.q1
+               SubPlan expr_2
                  ->  Result
                        Output: i1.q1
 (11 rows)
@@ -537,19 +537,19 @@ group by ss.x;
                  QUERY PLAN                 
 --------------------------------------------
  GroupAggregate
-   Output: (SubPlan 2), ((SubPlan 3))
-   Group Key: ((SubPlan 3))
+   Output: (expr_1), ((expr_3))
+   Group Key: ((expr_3))
    ->  Sort
-         Output: ((SubPlan 3)), i1.q1
-         Sort Key: ((SubPlan 3))
+         Output: ((expr_3)), i1.q1
+         Sort Key: ((expr_3))
          ->  Seq Scan on public.int8_tbl i1
-               Output: (SubPlan 3), i1.q1
-               SubPlan 3
+               Output: (expr_3), i1.q1
+               SubPlan expr_3
                  ->  Result
                        Output: i1.q1
-   SubPlan 2
+   SubPlan expr_1
      ->  Result
-           Output: GROUPING((SubPlan 1))
+           Output: GROUPING((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: (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::integer END), t1.v
-   Sort Key: (CASE WHEN (GROUPING((SubPlan 2)) = 0) THEN ((SubPlan 3)) ELSE NULL::integer END) NULLS FIRST
+   Output: (GROUPING((expr_1))), ((expr_3)), (CASE WHEN (GROUPING((expr_2)) = 0) THEN ((expr_3)) ELSE NULL::integer END), t1.v
+   Sort Key: (CASE WHEN (GROUPING((expr_2)) = 0) THEN ((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::integer END, t1.v
+         Output: GROUPING((expr_1)), ((expr_3)), CASE WHEN (GROUPING((expr_2)) = 0) THEN ((expr_3)) ELSE NULL::integer END, t1.v
          Hash Key: t1.v
-         Hash Key: (SubPlan 3)
+         Hash Key: (expr_3)
          ->  Seq Scan on pg_temp.gstest5 t1
-               Output: (SubPlan 3), t1.v, t1.id
-               SubPlan 3
+               Output: (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::integer END), t1.v
-   Sort Key: (CASE WHEN (GROUPING((SubPlan 2)) = 0) THEN ((SubPlan 3)) ELSE NULL::integer END) NULLS FIRST
+   Output: (GROUPING((expr_1))), ((expr_3)), (CASE WHEN (GROUPING((expr_2)) = 0) THEN ((expr_3)) ELSE NULL::integer END), t1.v
+   Sort Key: (CASE WHEN (GROUPING((expr_2)) = 0) THEN ((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::integer END, t1.v
+         Output: GROUPING((expr_1)), ((expr_3)), CASE WHEN (GROUPING((expr_2)) = 0) THEN ((expr_3)) ELSE NULL::integer END, t1.v
          Hash Key: t1.v
-         Hash Key: (SubPlan 3)
+         Hash Key: (expr_3)
          ->  Seq Scan on pg_temp.gstest5 t1
-               Output: (SubPlan 3), t1.v, t1.id
-               SubPlan 3
+               Output: (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..92fe3527baf 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, ((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, ((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..babb7a96af8 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: (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: (multiexpr_1).col1, (multiexpr_1).col2, (rescan 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: (multiexpr_1).col1, (multiexpr_1).col2, (rescan 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: (multiexpr_1).col1, (multiexpr_1).col2, (rescan 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..028ae01e307 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(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..070322656a5 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 = (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 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 = (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, (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, (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: ((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, (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, (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: ((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 = (any_1).col1) AND ((random() > '0'::double precision) = (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 = (any_1).col1) AND ((random() > '0'::double precision) = (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: (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 = (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 = (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..e96769114db 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 = (expr_1)) AND ((expr_2) = (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: ((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: ((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: (((expr_1) = hjtest_1.id) AND ((expr_3) = (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: ((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: ((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..38d4cabf8c1 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(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..dcb5a066ad0 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 < (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, (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..6ae3c2d724f 100644
--- a/src/test/regress/expected/partition_prune.out
+++ b/src/test/regress/expected/partition_prune.out
@@ -1921,15 +1921,15 @@ where asptab.id > ss.b::int;
    ->  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(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(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 < (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 < (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 < (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 < (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 < (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 < (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 = (expr_1).col1) OR (a = (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 = (expr_1).col1) OR (a = (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 = (expr_1).col1) OR (a = (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 = (expr_1).col1)
+         Filter: (b = (expr_2).col1)
          ->  Bitmap Index Scan on ab_a1_b1_a_idx (never executed)
-               Index Cond: (a = (InitPlan 1).col1)
+               Index Cond: (a = (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 = (expr_1).col1)
+         Filter: (b = (expr_2).col1)
          ->  Bitmap Index Scan on ab_a1_b2_a_idx (never executed)
-               Index Cond: (a = (InitPlan 1).col1)
+               Index Cond: (a = (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 = (expr_1).col1)
+         Filter: (b = (expr_2).col1)
          ->  Bitmap Index Scan on ab_a1_b3_a_idx (never executed)
-               Index Cond: (a = (InitPlan 1).col1)
+               Index Cond: (a = (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 = (expr_1).col1)
+         Filter: (b = (expr_2).col1)
          ->  Bitmap Index Scan on ab_a2_b1_a_idx (never executed)
-               Index Cond: (a = (InitPlan 1).col1)
+               Index Cond: (a = (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 = (expr_1).col1)
+         Filter: (b = (expr_2).col1)
          ->  Bitmap Index Scan on ab_a2_b2_a_idx (never executed)
-               Index Cond: (a = (InitPlan 1).col1)
+               Index Cond: (a = (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 = (expr_1).col1)
+         Filter: (b = (expr_2).col1)
          ->  Bitmap Index Scan on ab_a2_b3_a_idx (never executed)
-               Index Cond: (a = (InitPlan 1).col1)
+               Index Cond: (a = (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 = (expr_1).col1)
+         Filter: (b = (expr_2).col1)
          ->  Bitmap Index Scan on ab_a3_b1_a_idx (never executed)
-               Index Cond: (a = (InitPlan 1).col1)
+               Index Cond: (a = (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 = (expr_1).col1)
+         Filter: (b = (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 = (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 = (expr_1).col1)
+         Filter: (b = (expr_2).col1)
          ->  Bitmap Index Scan on ab_a3_b3_a_idx (never executed)
-               Index Cond: (a = (InitPlan 1).col1)
+               Index Cond: (a = (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 = (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 = (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 = (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 = (expr_1).col1)
    ->  Seq Scan on ab_a1_b2 ab_2 (never executed)
-         Filter: (b = (InitPlan 1).col1)
+         Filter: (b = (expr_1).col1)
    ->  Seq Scan on ab_a1_b3 ab_3 (never executed)
-         Filter: (b = (InitPlan 1).col1)
+         Filter: (b = (expr_1).col1)
    ->  Seq Scan on ab_a2_b1 ab_4 (actual rows=0.00 loops=1)
-         Filter: (b = (InitPlan 1).col1)
+         Filter: (b = (expr_1).col1)
    ->  Seq Scan on ab_a2_b2 ab_5 (never executed)
-         Filter: (b = (InitPlan 1).col1)
+         Filter: (b = (expr_1).col1)
    ->  Seq Scan on ab_a2_b3 ab_6 (never executed)
-         Filter: (b = (InitPlan 1).col1)
+         Filter: (b = (expr_1).col1)
    ->  Seq Scan on ab_a3_b1 ab_7 (actual rows=0.00 loops=1)
-         Filter: (b = (InitPlan 1).col1)
+         Filter: (b = (expr_1).col1)
    ->  Seq Scan on ab_a3_b2 ab_8 (never executed)
-         Filter: (b = (InitPlan 1).col1)
+         Filter: (b = (expr_1).col1)
    ->  Seq Scan on ab_a3_b3 ab_9 (never executed)
-         Filter: (b = (InitPlan 1).col1)
+         Filter: (b = (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 = (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 = (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 = (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 = (expr_1).col1)
    ->  Seq Scan on ab_a1_b1 ab_1 (actual rows=0.00 loops=1)
-         Filter: (b = (InitPlan 1).col1)
+         Filter: (b = (expr_1).col1)
    ->  Seq Scan on ab_a1_b2 ab_2 (never executed)
-         Filter: (b = (InitPlan 1).col1)
+         Filter: (b = (expr_1).col1)
    ->  Seq Scan on ab_a1_b3 ab_3 (never executed)
-         Filter: (b = (InitPlan 1).col1)
+         Filter: (b = (expr_1).col1)
    ->  Seq Scan on ab_a2_b1 ab_4 (actual rows=0.00 loops=1)
-         Filter: (b = (InitPlan 1).col1)
+         Filter: (b = (expr_1).col1)
    ->  Seq Scan on ab_a2_b2 ab_5 (never executed)
-         Filter: (b = (InitPlan 1).col1)
+         Filter: (b = (expr_1).col1)
    ->  Seq Scan on ab_a2_b3 ab_6 (never executed)
-         Filter: (b = (InitPlan 1).col1)
+         Filter: (b = (expr_1).col1)
    ->  Seq Scan on ab_a3_b1 ab_7 (actual rows=0.00 loops=1)
-         Filter: (b = (InitPlan 1).col1)
+         Filter: (b = (expr_1).col1)
    ->  Seq Scan on ab_a3_b2 ab_8 (never executed)
-         Filter: (b = (InitPlan 1).col1)
+         Filter: (b = (expr_1).col1)
    ->  Seq Scan on ab_a3_b3 ab_9 (never executed)
-         Filter: (b = (InitPlan 1).col1)
+         Filter: (b = (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 = (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 = (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 = (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 = (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 = (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 = (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 = (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 = (expr_1).col1)
                      ->  Seq Scan on ab_a2_b2 ab_a2_2 (never executed)
-                           Filter: (b = (InitPlan 1).col1)
+                           Filter: (b = (expr_1).col1)
                      ->  Seq Scan on ab_a2_b3 ab_a2_3 (never executed)
-                           Filter: (b = (InitPlan 1).col1)
+                           Filter: (b = (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 = (expr_1).col1)
    ->  Seq Scan on listp_2_1 listp_2 (never executed)
-         Filter: (a = (InitPlan 1).col1)
+         Filter: (a = (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) < (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) < (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) < (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 = (expr_1).col1)
    ->  Seq Scan on boolp_t boolp_2 (actual rows=0.00 loops=1)
-         Filter: (a = (InitPlan 1).col1)
+         Filter: (a = (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 = (expr_1).col1)
    ->  Seq Scan on boolp_t boolp_2 (never executed)
-         Filter: (a = (InitPlan 1).col1)
+         Filter: (a = (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 >= (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 >= (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 >= (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 = (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 = (expr_1).col1))
    ->  Result
-         One-Time Filter: (1 = (InitPlan 1).col1)
+         One-Time Filter: (1 = (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 = (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 = (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 = (expr_1).col1))
 (10 rows)
 
 execute q (1, 1);
@@ -4113,8 +4113,8 @@ select * from listp where a = (select 2) and b <> 10;
                      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 = (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 = (expr_1).col1)
          ->  Parallel Seq Scan on listp_12_2 listp_2 (never executed)
-               Filter: (a = (InitPlan 1).col1)
+               Filter: (a = (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 = (expr_2).col1)
                ->  Parallel Seq Scan on listp_12_2 listp_2 (actual rows=N loops=N)
-                     Filter: (a = (InitPlan 2).col1)
+                     Filter: (a = (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 = (expr_1).col1)
                ->  Parallel Seq Scan on listp_12_2 listp_5 (never executed)
-                     Filter: (a = (InitPlan 1).col1)
+                     Filter: (a = (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[(expr_1).col1, (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[(expr_1).col1, (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[(expr_1).col1, (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[(expr_1).col1, (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..4b5971efb75 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(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: (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..3df940ee8fc 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: (expr_1), (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
@@ -581,23 +581,23 @@ UPDATE foo SET f4 = 100 WHERE f1 = 5
                           QUERY PLAN                           
 ---------------------------------------------------------------
  Update on pg_temp.foo
-   Output: (SubPlan 1), (SubPlan 2), (SubPlan 3)
+   Output: (expr_1), (expr_2), (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: (expr_1), (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..ca81b1cf1ff 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 <= (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 <= (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 <= (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 <= (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 <= (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 <= (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 <= (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 <= (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 <= (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 <= (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 <= (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 <= (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 <= (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 <= (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 <= (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 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 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 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 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 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 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 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..74bc64c0e47 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: ((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: (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..3671d261f1f 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 any_1).col1) AND (four = (hashed 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 any_1).col1)))
+   SubPlan any_1
      ->  Seq Scan on tenk2
 (4 rows)
 
@@ -346,7 +346,7 @@ explain (costs off)
                       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 = (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 ((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 = (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 = (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(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: ((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..d660049f134 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: (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..a9d0882013e 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 = (rowcompare_1).col1) AND (2 = (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 = (rowcompare_1).col1) AND (2 = (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 = (all_1).col1))
+   SubPlan all_1
      ->  Materialize
-           Output: ((InitPlan 1).col1)
-           InitPlan 1
+           Output: ((expr_1).col1)
+           InitPlan expr_1
              ->  Result
                    Output: 1
            ->  Result
-                 Output: (InitPlan 1).col1
+                 Output: (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(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 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 = (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 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 any_1).col1) AND ((q1)::text = (hashed 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 ((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 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(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(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 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: (expr_1).col1, (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: (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: (expr_1), (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: (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 any_1).col1))))::integer)
    ->  Nested Loop
-         Output: ((ANY (i.ten = (hashed SubPlan 1).col1)))
+         Output: ((ANY (i.ten = (hashed 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 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 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 = (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 = (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 = (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..66747f8af82 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)
@@ -3170,18 +3170,18 @@ EXPLAIN (costs off) INSERT INTO rw_view1 VALUES (2, 'New row 2');
                         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: ((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: (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(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(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(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(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(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(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(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(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: (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..a0aac9d4377 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: (multiexpr_1).col1, (multiexpr_1).col2, (rescan 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..55719226bef 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((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..700f3b6ef85 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, (expr_1)
+   SubPlan expr_1
      ->  Aggregate
-           Output: count((InitPlan 1).col1)
-           InitPlan 1
+           Output: count((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.39.5 (Apple Git-154)

v7-0005-Store-information-about-range-table-flattening-in.patchapplication/octet-stream; name=v7-0005-Store-information-about-range-table-flattening-in.patchDownload
From 8776ed3dd48f6a6d94b175048d99c3d72e551802 Mon Sep 17 00:00:00 2001
From: Robert Haas <rhaas@postgresql.org>
Date: Fri, 21 Mar 2025 11:06:35 -0400
Subject: [PATCH v7 5/7] Store information about range-table flattening in the
 final plan.

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.  So,
this commit remembers in the final plan which part of the final range
table came from which subquery's range table.

Additionally, this commit teaches pg_overexplain'e RANGE_TABLE option
to display the subquery name for each range table entry.
---
 contrib/pg_overexplain/pg_overexplain.c | 36 +++++++++++++++++++++++++
 src/backend/optimizer/plan/planner.c    |  1 +
 src/backend/optimizer/plan/setrefs.c    | 20 ++++++++++++++
 src/include/nodes/pathnodes.h           |  3 +++
 src/include/nodes/plannodes.h           | 17 ++++++++++++
 src/tools/pgindent/typedefs.list        |  1 +
 6 files changed, 78 insertions(+)

diff --git a/contrib/pg_overexplain/pg_overexplain.c b/contrib/pg_overexplain/pg_overexplain.c
index 92cfd8af2eb..d1d4e2f51e2 100644
--- a/contrib/pg_overexplain/pg_overexplain.c
+++ b/contrib/pg_overexplain/pg_overexplain.c
@@ -416,6 +416,8 @@ static void
 overexplain_range_table(PlannedStmt *plannedstmt, ExplainState *es)
 {
 	Index		rti;
+	ListCell   *lc_subrtinfo = list_head(plannedstmt->subrtinfos);
+	SubPlanRTInfo *rtinfo = NULL;
 
 	/* Open group, one entry per RangeTblEntry */
 	ExplainOpenGroup("Range Table", "Range Table", false, es);
@@ -426,6 +428,18 @@ overexplain_range_table(PlannedStmt *plannedstmt, ExplainState *es)
 		RangeTblEntry *rte = rt_fetch(rti, plannedstmt->rtable);
 		char	   *kind = NULL;
 		char	   *relkind;
+		SubPlanRTInfo *next_rtinfo;
+
+		/* Advance to next SubRTInfo, if it's time. */
+		if (lc_subrtinfo != NULL)
+		{
+			next_rtinfo = lfirst(lc_subrtinfo);
+			if (rti > next_rtinfo->rtoffset)
+			{
+				rtinfo = next_rtinfo;
+				lc_subrtinfo = lnext(plannedstmt->subrtinfos, lc_subrtinfo);
+			}
+		}
 
 		/* NULL entries are possible; skip them */
 		if (rte == NULL)
@@ -490,6 +504,28 @@ overexplain_range_table(PlannedStmt *plannedstmt, ExplainState *es)
 			ExplainPropertyBool("In From Clause", rte->inFromCl, es);
 		}
 
+		/*
+		 * Indicate which subplan is the origin of which RTE. Note dummy
+		 * subplans. Here again, we crunch more onto one line in text format.
+		 */
+		if (rtinfo != NULL)
+		{
+			if (es->format == EXPLAIN_FORMAT_TEXT)
+			{
+				if (!rtinfo->dummy)
+					ExplainPropertyText("Subplan", rtinfo->plan_name, es);
+				else
+					ExplainPropertyText("Subplan",
+										psprintf("%s (dummy)",
+												 rtinfo->plan_name), es);
+			}
+			else
+			{
+				ExplainPropertyText("Subplan", rtinfo->plan_name, es);
+				ExplainPropertyBool("Subplan Is Dummy", rtinfo->dummy, es);
+			}
+		}
+
 		/* rte->alias is optional; rte->eref is requested */
 		if (rte->alias != NULL)
 			overexplain_alias("Alias", rte->alias, es);
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index 988bbd19ab4..59ee3f319f4 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -579,6 +579,7 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions,
 	result->unprunableRelids = bms_difference(glob->allRelids,
 											  glob->prunableRelids);
 	result->permInfos = glob->finalrteperminfos;
+	result->subrtinfos = glob->subrtinfos;
 	result->resultRelations = glob->resultRelations;
 	result->appendRelations = glob->appendRelations;
 	result->subplans = glob->subplans;
diff --git a/src/backend/optimizer/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.c
index 6950eff2c5b..eef43792aeb 100644
--- a/src/backend/optimizer/plan/setrefs.c
+++ b/src/backend/optimizer/plan/setrefs.c
@@ -399,6 +399,26 @@ add_rtes_to_flat_rtable(PlannerInfo *root, bool recursing)
 	Index		rti;
 	ListCell   *lc;
 
+	/*
+	 * Record enough information to make it possible for code that looks at
+	 * the final range table to understand how it was constructed. (If
+	 * finalrtable is still NIL, then this is the very topmost PlannerInfo,
+	 * which will always have plan_name == NULL and rtoffset == 0; we omit the
+	 * degenerate list entry.)
+	 */
+	if (root->glob->finalrtable != NIL)
+	{
+		SubPlanRTInfo *rtinfo = makeNode(SubPlanRTInfo);
+
+		rtinfo->plan_name = root->plan_name;
+		rtinfo->rtoffset = list_length(root->glob->finalrtable);
+
+		/* When recursing = true, it's an unplanned or dummy subquery. */
+		rtinfo->dummy = recursing;
+
+		root->glob->subrtinfos = lappend(root->glob->subrtinfos, rtinfo);
+	}
+
 	/*
 	 * Add the query's own RTEs to the flattened rangetable.
 	 *
diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h
index a341b01a1e1..7f1ed030cb2 100644
--- a/src/include/nodes/pathnodes.h
+++ b/src/include/nodes/pathnodes.h
@@ -135,6 +135,9 @@ typedef struct PlannerGlobal
 	/* "flat" list of RTEPermissionInfos */
 	List	   *finalrteperminfos;
 
+	/* list of SubPlanRTInfo nodes */
+	List	   *subrtinfos;
+
 	/* "flat" list of PlanRowMarks */
 	List	   *finalrowmarks;
 
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index 16f3f5a7925..6ed08716b39 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -131,6 +131,9 @@ typedef struct PlannedStmt
 	 */
 	List	   *subplans;
 
+	/* a list of SubPlanRTInfo objects */
+	List	   *subrtinfos;
+
 	/* indices of subplans that require REWIND */
 	Bitmapset  *rewindPlanIDs;
 
@@ -1816,4 +1819,18 @@ typedef enum MonotonicFunction
 	MONOTONICFUNC_BOTH = MONOTONICFUNC_INCREASING | MONOTONICFUNC_DECREASING,
 } MonotonicFunction;
 
+/*
+ * SubPlanRTInfo
+ *
+ * Information about which range table entries came from which subquery
+ * planning cycles.
+ */
+typedef struct SubPlanRTInfo
+{
+	NodeTag		type;
+	char	   *plan_name;
+	Index		rtoffset;
+	bool		dummy;
+} SubPlanRTInfo;
+
 #endif							/* PLANNODES_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 3c80d49b67e..993688870db 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -4343,3 +4343,4 @@ yyscan_t
 z_stream
 z_streamp
 zic_t
+SubPlanRTInfo
-- 
2.39.5 (Apple Git-154)

v7-0007-Store-information-about-Append-node-consolidation.patchapplication/octet-stream; name=v7-0007-Store-information-about-Append-node-consolidation.patchDownload
From 4e4c6a92ef5ebc8c0873eb56575c8f757f353378 Mon Sep 17 00:00:00 2001
From: Robert Haas <rhaas@postgresql.org>
Date: Mon, 21 Jul 2025 15:49:06 -0400
Subject: [PATCH v7 7/7] Store information about Append node consolidation in
 the final plan.

When we build an AppendPath or MergeAppendPath, we sometimes pull all
child paths from a subordinate AppendPath or MergeAppendPath instead
of having one such path atop another. This results in the RTIs that
would have been associated with the subordinate path disappearing
from the final plan, making things difficult for code that wants
to scrutinize the final plan and extract information from it about
what happened during the planning process.

To avoid this, propagate the RTI sets that would have been present
in the 'apprelids' field of the subordinate Append or MergeAppend
nodes that would have been created into the surviving Append or
MergeAppend node, using a new 'child_append_relid_sets' field for
that purpose.

This commit also updates pg_overexplain to display these details.
---
 .../expected/pg_overexplain.out               |  4 +-
 contrib/pg_overexplain/pg_overexplain.c       | 56 +++++++++++
 src/backend/optimizer/path/allpaths.c         | 98 +++++++++++++++----
 src/backend/optimizer/path/joinrels.c         |  2 +-
 src/backend/optimizer/plan/createplan.c       |  2 +
 src/backend/optimizer/plan/planner.c          |  1 +
 src/backend/optimizer/prep/prepunion.c        |  5 +-
 src/backend/optimizer/util/pathnode.c         |  5 +
 src/include/nodes/pathnodes.h                 | 10 ++
 src/include/nodes/plannodes.h                 | 11 +++
 src/include/optimizer/pathnode.h              |  2 +
 11 files changed, 171 insertions(+), 25 deletions(-)

diff --git a/contrib/pg_overexplain/expected/pg_overexplain.out b/contrib/pg_overexplain/expected/pg_overexplain.out
index 3c3c65dbb48..303ae101860 100644
--- a/contrib/pg_overexplain/expected/pg_overexplain.out
+++ b/contrib/pg_overexplain/expected/pg_overexplain.out
@@ -104,6 +104,7 @@ $$);
                Parallel Safe: true
                Plan Node ID: 2
                Append RTIs: 1
+               Child Append RTIs: none
                ->  Seq Scan on brassica vegetables_1
                      Disabled Nodes: 0
                      Parallel Safe: true
@@ -142,7 +143,7 @@ $$);
    Relation Kind: relation
    Relation Lock Mode: AccessShareLock
  Unprunable RTIs: 1 3 4
-(53 rows)
+(54 rows)
 
 -- Test a different output format.
 SELECT explain_filter($$
@@ -197,6 +198,7 @@ $$);
                <extParam>none</extParam>                            +
                <allParam>none</allParam>                            +
                <Append-RTIs>1</Append-RTIs>                         +
+               <Child-Append-RTIs>none</Child-Append-RTIs>          +
                <Subplans-Removed>0</Subplans-Removed>               +
                <Plans>                                              +
                  <Plan>                                             +
diff --git a/contrib/pg_overexplain/pg_overexplain.c b/contrib/pg_overexplain/pg_overexplain.c
index 3edbe3d1f80..59164b792a4 100644
--- a/contrib/pg_overexplain/pg_overexplain.c
+++ b/contrib/pg_overexplain/pg_overexplain.c
@@ -54,6 +54,8 @@ static void overexplain_alias(const char *qlabel, Alias *alias,
 							  ExplainState *es);
 static void overexplain_bitmapset(const char *qlabel, Bitmapset *bms,
 								  ExplainState *es);
+static void overexplain_bitmapset_list(const char *qlabel, List *bms_list,
+									   ExplainState *es);
 static void overexplain_intlist(const char *qlabel, List *list,
 								ExplainState *es);
 
@@ -232,11 +234,17 @@ overexplain_per_node_hook(PlanState *planstate, List *ancestors,
 				overexplain_bitmapset("Append RTIs",
 									  ((Append *) plan)->apprelids,
 									  es);
+				overexplain_bitmapset_list("Child Append RTIs",
+										   ((Append *) plan)->child_append_relid_sets,
+										   es);
 				break;
 			case T_MergeAppend:
 				overexplain_bitmapset("Append RTIs",
 									  ((MergeAppend *) plan)->apprelids,
 									  es);
+				overexplain_bitmapset_list("Child Append RTIs",
+										   ((MergeAppend *) plan)->child_append_relid_sets,
+										   es);
 				break;
 			case T_Result:
 
@@ -836,6 +844,54 @@ overexplain_bitmapset(const char *qlabel, Bitmapset *bms, ExplainState *es)
 	pfree(buf.data);
 }
 
+/*
+ * Emit a text property describing the contents of a list of bitmapsets.
+ * If a bitmapset contains exactly 1 member, we just print an integer;
+ * otherwise, we surround the list of members by parentheses.
+ *
+ * If there are no bitmapsets in the list, we print the word "none".
+ */
+static void
+overexplain_bitmapset_list(const char *qlabel, List *bms_list,
+						   ExplainState *es)
+{
+	StringInfoData buf;
+
+	initStringInfo(&buf);
+
+	foreach_node(Bitmapset, bms, bms_list)
+	{
+		if (bms_membership(bms) == BMS_SINGLETON)
+			appendStringInfo(&buf, " %d", bms_singleton_member(bms));
+		else
+		{
+			int			x = -1;
+			bool		first = true;
+
+			appendStringInfoString(&buf, " (");
+			while ((x = bms_next_member(bms, x)) >= 0)
+			{
+				if (first)
+					first = false;
+				else
+					appendStringInfoChar(&buf, ' ');
+				appendStringInfo(&buf, "%d", x);
+			}
+			appendStringInfoChar(&buf, ')');
+		}
+	}
+
+	if (buf.len == 0)
+	{
+		ExplainPropertyText(qlabel, "none", es);
+		return;
+	}
+
+	Assert(buf.data[0] == ' ');
+	ExplainPropertyText(qlabel, buf.data + 1, es);
+	pfree(buf.data);
+}
+
 /*
  * Emit a text property describing the contents of a list of integers, OIDs,
  * or XIDs -- either a space-separated list of integer members, or the word
diff --git a/src/backend/optimizer/path/allpaths.c b/src/backend/optimizer/path/allpaths.c
index 593f5361b58..76b8c3fd7c6 100644
--- a/src/backend/optimizer/path/allpaths.c
+++ b/src/backend/optimizer/path/allpaths.c
@@ -122,8 +122,10 @@ static Path *get_cheapest_parameterized_child_path(PlannerInfo *root,
 												   Relids required_outer);
 static void accumulate_append_subpath(Path *path,
 									  List **subpaths,
-									  List **special_subpaths);
-static Path *get_singleton_append_subpath(Path *path);
+									  List **special_subpaths,
+									  List **child_append_relid_sets);
+static Path *get_singleton_append_subpath(Path *path,
+										  List **child_append_relid_sets);
 static void set_dummy_rel_pathlist(RelOptInfo *rel);
 static void set_subquery_pathlist(PlannerInfo *root, RelOptInfo *rel,
 								  Index rti, RangeTblEntry *rte);
@@ -1323,11 +1325,15 @@ add_paths_to_append_rel(PlannerInfo *root, RelOptInfo *rel,
 {
 	List	   *subpaths = NIL;
 	bool		subpaths_valid = true;
+	List	   *subpath_cars = NIL;
 	List	   *startup_subpaths = NIL;
 	bool		startup_subpaths_valid = true;
+	List	   *startup_subpath_cars = NIL;
 	List	   *partial_subpaths = NIL;
+	List	   *partial_subpath_cars = NIL;
 	List	   *pa_partial_subpaths = NIL;
 	List	   *pa_nonpartial_subpaths = NIL;
+	List	   *pa_subpath_cars = NIL;
 	bool		partial_subpaths_valid = true;
 	bool		pa_subpaths_valid;
 	List	   *all_child_pathkeys = NIL;
@@ -1360,7 +1366,7 @@ add_paths_to_append_rel(PlannerInfo *root, RelOptInfo *rel,
 		if (childrel->pathlist != NIL &&
 			childrel->cheapest_total_path->param_info == NULL)
 			accumulate_append_subpath(childrel->cheapest_total_path,
-									  &subpaths, NULL);
+									  &subpaths, NULL, &subpath_cars);
 		else
 			subpaths_valid = false;
 
@@ -1389,7 +1395,8 @@ add_paths_to_append_rel(PlannerInfo *root, RelOptInfo *rel,
 			Assert(cheapest_path->param_info == NULL);
 			accumulate_append_subpath(cheapest_path,
 									  &startup_subpaths,
-									  NULL);
+									  NULL,
+									  &startup_subpath_cars);
 		}
 		else
 			startup_subpaths_valid = false;
@@ -1400,7 +1407,8 @@ add_paths_to_append_rel(PlannerInfo *root, RelOptInfo *rel,
 		{
 			cheapest_partial_path = linitial(childrel->partial_pathlist);
 			accumulate_append_subpath(cheapest_partial_path,
-									  &partial_subpaths, NULL);
+									  &partial_subpaths, NULL,
+									  &partial_subpath_cars);
 		}
 		else
 			partial_subpaths_valid = false;
@@ -1429,7 +1437,8 @@ add_paths_to_append_rel(PlannerInfo *root, RelOptInfo *rel,
 				Assert(cheapest_partial_path != NULL);
 				accumulate_append_subpath(cheapest_partial_path,
 										  &pa_partial_subpaths,
-										  &pa_nonpartial_subpaths);
+										  &pa_nonpartial_subpaths,
+										  &pa_subpath_cars);
 			}
 			else
 			{
@@ -1448,7 +1457,8 @@ add_paths_to_append_rel(PlannerInfo *root, RelOptInfo *rel,
 				 */
 				accumulate_append_subpath(nppath,
 										  &pa_nonpartial_subpaths,
-										  NULL);
+										  NULL,
+										  &pa_subpath_cars);
 			}
 		}
 
@@ -1523,14 +1533,16 @@ add_paths_to_append_rel(PlannerInfo *root, RelOptInfo *rel,
 	 * if we have zero or one live subpath due to constraint exclusion.)
 	 */
 	if (subpaths_valid)
-		add_path(rel, (Path *) create_append_path(root, rel, subpaths, NIL,
+		add_path(rel, (Path *) create_append_path(root, rel, subpaths,
+												  NIL, subpath_cars,
 												  NIL, NULL, 0, false,
 												  -1));
 
 	/* build an AppendPath for the cheap startup paths, if valid */
 	if (startup_subpaths_valid)
 		add_path(rel, (Path *) create_append_path(root, rel, startup_subpaths,
-												  NIL, NIL, NULL, 0, false, -1));
+												  NIL, startup_subpath_cars,
+												  NIL, NULL, 0, false, -1));
 
 	/*
 	 * Consider an append of unordered, unparameterized partial paths.  Make
@@ -1571,6 +1583,7 @@ add_paths_to_append_rel(PlannerInfo *root, RelOptInfo *rel,
 
 		/* Generate a partial append path. */
 		appendpath = create_append_path(root, rel, NIL, partial_subpaths,
+										partial_subpath_cars,
 										NIL, NULL, parallel_workers,
 										enable_parallel_append,
 										-1);
@@ -1621,6 +1634,7 @@ add_paths_to_append_rel(PlannerInfo *root, RelOptInfo *rel,
 
 		appendpath = create_append_path(root, rel, pa_nonpartial_subpaths,
 										pa_partial_subpaths,
+										pa_subpath_cars,
 										NIL, NULL, parallel_workers, true,
 										partial_rows);
 		add_partial_path(rel, (Path *) appendpath);
@@ -1654,6 +1668,7 @@ add_paths_to_append_rel(PlannerInfo *root, RelOptInfo *rel,
 
 		/* Select the child paths for an Append with this parameterization */
 		subpaths = NIL;
+		subpath_cars = NIL;
 		subpaths_valid = true;
 		foreach(lcr, live_childrels)
 		{
@@ -1676,12 +1691,13 @@ add_paths_to_append_rel(PlannerInfo *root, RelOptInfo *rel,
 				subpaths_valid = false;
 				break;
 			}
-			accumulate_append_subpath(subpath, &subpaths, NULL);
+			accumulate_append_subpath(subpath, &subpaths, NULL,
+									  &subpath_cars);
 		}
 
 		if (subpaths_valid)
 			add_path(rel, (Path *)
-					 create_append_path(root, rel, subpaths, NIL,
+					 create_append_path(root, rel, subpaths, NIL, subpath_cars,
 										NIL, required_outer, 0, false,
 										-1));
 	}
@@ -1708,6 +1724,7 @@ add_paths_to_append_rel(PlannerInfo *root, RelOptInfo *rel,
 				continue;
 
 			appendpath = create_append_path(root, rel, NIL, list_make1(path),
+											list_make1(rel->relids),
 											NIL, NULL,
 											path->parallel_workers, true,
 											partial_rows);
@@ -1789,8 +1806,11 @@ generate_orderedappend_paths(PlannerInfo *root, RelOptInfo *rel,
 	{
 		List	   *pathkeys = (List *) lfirst(lcp);
 		List	   *startup_subpaths = NIL;
+		List	   *startup_subpath_cars = NIL;
 		List	   *total_subpaths = NIL;
+		List	   *total_subpath_cars = NIL;
 		List	   *fractional_subpaths = NIL;
+		List	   *fractional_subpath_cars = NIL;
 		bool		startup_neq_total = false;
 		bool		match_partition_order;
 		bool		match_partition_order_desc;
@@ -1942,16 +1962,23 @@ generate_orderedappend_paths(PlannerInfo *root, RelOptInfo *rel,
 				 * just a single subpath (and hence aren't doing anything
 				 * useful).
 				 */
-				cheapest_startup = get_singleton_append_subpath(cheapest_startup);
-				cheapest_total = get_singleton_append_subpath(cheapest_total);
+				cheapest_startup =
+					get_singleton_append_subpath(cheapest_startup,
+												 &startup_subpath_cars);
+				cheapest_total =
+					get_singleton_append_subpath(cheapest_total,
+												 &total_subpath_cars);
 
 				startup_subpaths = lappend(startup_subpaths, cheapest_startup);
 				total_subpaths = lappend(total_subpaths, cheapest_total);
 
 				if (cheapest_fractional)
 				{
-					cheapest_fractional = get_singleton_append_subpath(cheapest_fractional);
-					fractional_subpaths = lappend(fractional_subpaths, cheapest_fractional);
+					cheapest_fractional =
+						get_singleton_append_subpath(cheapest_fractional,
+													 &fractional_subpath_cars);
+					fractional_subpaths =
+						lappend(fractional_subpaths, cheapest_fractional);
 				}
 			}
 			else
@@ -1961,13 +1988,16 @@ generate_orderedappend_paths(PlannerInfo *root, RelOptInfo *rel,
 				 * child paths for the MergeAppend.
 				 */
 				accumulate_append_subpath(cheapest_startup,
-										  &startup_subpaths, NULL);
+										  &startup_subpaths, NULL,
+										  &startup_subpath_cars);
 				accumulate_append_subpath(cheapest_total,
-										  &total_subpaths, NULL);
+										  &total_subpaths, NULL,
+										  &total_subpath_cars);
 
 				if (cheapest_fractional)
 					accumulate_append_subpath(cheapest_fractional,
-											  &fractional_subpaths, NULL);
+											  &fractional_subpaths, NULL,
+											  &fractional_subpath_cars);
 			}
 		}
 
@@ -1979,6 +2009,7 @@ generate_orderedappend_paths(PlannerInfo *root, RelOptInfo *rel,
 													  rel,
 													  startup_subpaths,
 													  NIL,
+													  startup_subpath_cars,
 													  pathkeys,
 													  NULL,
 													  0,
@@ -1989,6 +2020,7 @@ generate_orderedappend_paths(PlannerInfo *root, RelOptInfo *rel,
 														  rel,
 														  total_subpaths,
 														  NIL,
+														  total_subpath_cars,
 														  pathkeys,
 														  NULL,
 														  0,
@@ -2000,6 +2032,7 @@ generate_orderedappend_paths(PlannerInfo *root, RelOptInfo *rel,
 														  rel,
 														  fractional_subpaths,
 														  NIL,
+														  fractional_subpath_cars,
 														  pathkeys,
 														  NULL,
 														  0,
@@ -2012,12 +2045,14 @@ generate_orderedappend_paths(PlannerInfo *root, RelOptInfo *rel,
 			add_path(rel, (Path *) create_merge_append_path(root,
 															rel,
 															startup_subpaths,
+															startup_subpath_cars,
 															pathkeys,
 															NULL));
 			if (startup_neq_total)
 				add_path(rel, (Path *) create_merge_append_path(root,
 																rel,
 																total_subpaths,
+																total_subpath_cars,
 																pathkeys,
 																NULL));
 
@@ -2025,6 +2060,7 @@ generate_orderedappend_paths(PlannerInfo *root, RelOptInfo *rel,
 				add_path(rel, (Path *) create_merge_append_path(root,
 																rel,
 																fractional_subpaths,
+																fractional_subpath_cars,
 																pathkeys,
 																NULL));
 		}
@@ -2127,7 +2163,8 @@ get_cheapest_parameterized_child_path(PlannerInfo *root, RelOptInfo *rel,
  * paths).
  */
 static void
-accumulate_append_subpath(Path *path, List **subpaths, List **special_subpaths)
+accumulate_append_subpath(Path *path, List **subpaths, List **special_subpaths,
+						  List **child_append_relid_sets)
 {
 	if (IsA(path, AppendPath))
 	{
@@ -2136,6 +2173,8 @@ accumulate_append_subpath(Path *path, List **subpaths, List **special_subpaths)
 		if (!apath->path.parallel_aware || apath->first_partial_path == 0)
 		{
 			*subpaths = list_concat(*subpaths, apath->subpaths);
+			*child_append_relid_sets =
+				lappend(*child_append_relid_sets, path->parent->relids);
 			return;
 		}
 		else if (special_subpaths != NULL)
@@ -2150,6 +2189,8 @@ accumulate_append_subpath(Path *path, List **subpaths, List **special_subpaths)
 												  apath->first_partial_path);
 			*special_subpaths = list_concat(*special_subpaths,
 											new_special_subpaths);
+			*child_append_relid_sets =
+				lappend(*child_append_relid_sets, path->parent->relids);
 			return;
 		}
 	}
@@ -2158,6 +2199,8 @@ accumulate_append_subpath(Path *path, List **subpaths, List **special_subpaths)
 		MergeAppendPath *mpath = (MergeAppendPath *) path;
 
 		*subpaths = list_concat(*subpaths, mpath->subpaths);
+		*child_append_relid_sets =
+			lappend(*child_append_relid_sets, path->parent->relids);
 		return;
 	}
 
@@ -2169,10 +2212,15 @@ accumulate_append_subpath(Path *path, List **subpaths, List **special_subpaths)
  *		Returns the single subpath of an Append/MergeAppend, or just
  *		return 'path' if it's not a single sub-path Append/MergeAppend.
  *
+ * As a side effect, whenever we return a single subpath rather than the
+ * original path, add the relid set for the original path to
+ * child_append_relid_sets, so that those relids don't entirely disappear
+ * from the final plan.
+ *
  * Note: 'path' must not be a parallel-aware path.
  */
 static Path *
-get_singleton_append_subpath(Path *path)
+get_singleton_append_subpath(Path *path, List **child_append_relid_sets)
 {
 	Assert(!path->parallel_aware);
 
@@ -2181,14 +2229,22 @@ get_singleton_append_subpath(Path *path)
 		AppendPath *apath = (AppendPath *) path;
 
 		if (list_length(apath->subpaths) == 1)
+		{
+			*child_append_relid_sets =
+				lappend(*child_append_relid_sets, path->parent->relids);
 			return (Path *) linitial(apath->subpaths);
+		}
 	}
 	else if (IsA(path, MergeAppendPath))
 	{
 		MergeAppendPath *mpath = (MergeAppendPath *) path;
 
 		if (list_length(mpath->subpaths) == 1)
+		{
+			*child_append_relid_sets =
+				lappend(*child_append_relid_sets, path->parent->relids);
 			return (Path *) linitial(mpath->subpaths);
+		}
 	}
 
 	return path;
@@ -2217,7 +2273,7 @@ set_dummy_rel_pathlist(RelOptInfo *rel)
 	rel->partial_pathlist = NIL;
 
 	/* Set up the dummy path */
-	add_path(rel, (Path *) create_append_path(NULL, rel, NIL, NIL,
+	add_path(rel, (Path *) create_append_path(NULL, rel, NIL, NIL, NIL,
 											  NIL, rel->lateral_relids,
 											  0, false, -1));
 
diff --git a/src/backend/optimizer/path/joinrels.c b/src/backend/optimizer/path/joinrels.c
index 535248aa525..6bddfc537d2 100644
--- a/src/backend/optimizer/path/joinrels.c
+++ b/src/backend/optimizer/path/joinrels.c
@@ -1342,7 +1342,7 @@ mark_dummy_rel(RelOptInfo *rel)
 
 	/* Set up the dummy path */
 	add_path(rel, (Path *) create_append_path(NULL, rel, NIL, NIL,
-											  NIL, rel->lateral_relids,
+											  NIL, NIL, rel->lateral_relids,
 											  0, false, -1));
 
 	/* Set or update cheapest_total_path and related fields */
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index 6fa782d7c58..6a8c7293999 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -1278,6 +1278,7 @@ create_append_plan(PlannerInfo *root, AppendPath *best_path, int flags)
 	plan->plan.lefttree = NULL;
 	plan->plan.righttree = NULL;
 	plan->apprelids = rel->relids;
+	plan->child_append_relid_sets = best_path->child_append_relid_sets;
 
 	if (pathkeys != NIL)
 	{
@@ -1490,6 +1491,7 @@ create_merge_append_plan(PlannerInfo *root, MergeAppendPath *best_path,
 	plan->lefttree = NULL;
 	plan->righttree = NULL;
 	node->apprelids = rel->relids;
+	node->child_append_relid_sets = best_path->child_append_relid_sets;
 
 	/*
 	 * Compute sort column info, and adjust MergeAppend's tlist as needed.
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index fef57d84bb3..9678734d34c 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -3979,6 +3979,7 @@ create_degenerate_grouping_paths(PlannerInfo *root, RelOptInfo *input_rel,
 							   paths,
 							   NIL,
 							   NIL,
+							   NIL,
 							   NULL,
 							   0,
 							   false,
diff --git a/src/backend/optimizer/prep/prepunion.c b/src/backend/optimizer/prep/prepunion.c
index d55eb39e552..5b88514bb79 100644
--- a/src/backend/optimizer/prep/prepunion.c
+++ b/src/backend/optimizer/prep/prepunion.c
@@ -815,7 +815,7 @@ generate_union_paths(SetOperationStmt *op, PlannerInfo *root,
 	 * union child.
 	 */
 	apath = (Path *) create_append_path(root, result_rel, cheapest_pathlist,
-										NIL, NIL, NULL, 0, false, -1);
+										NIL, NIL, NIL, NULL, 0, false, -1);
 
 	/*
 	 * Estimate number of groups.  For now we just assume the output is unique
@@ -861,7 +861,7 @@ generate_union_paths(SetOperationStmt *op, PlannerInfo *root,
 
 		papath = (Path *)
 			create_append_path(root, result_rel, NIL, partial_pathlist,
-							   NIL, NULL, parallel_workers,
+							   NIL, NIL, NULL, parallel_workers,
 							   enable_parallel_append, -1);
 		gpath = (Path *)
 			create_gather_path(root, result_rel, papath,
@@ -969,6 +969,7 @@ generate_union_paths(SetOperationStmt *op, PlannerInfo *root,
 			path = (Path *) create_merge_append_path(root,
 													 result_rel,
 													 ordered_pathlist,
+													 NIL,
 													 union_pathkeys,
 													 NULL);
 
diff --git a/src/backend/optimizer/util/pathnode.c b/src/backend/optimizer/util/pathnode.c
index b0da28150d3..b4546c8842d 100644
--- a/src/backend/optimizer/util/pathnode.c
+++ b/src/backend/optimizer/util/pathnode.c
@@ -1298,6 +1298,7 @@ AppendPath *
 create_append_path(PlannerInfo *root,
 				   RelOptInfo *rel,
 				   List *subpaths, List *partial_subpaths,
+				   List *child_append_relid_sets,
 				   List *pathkeys, Relids required_outer,
 				   int parallel_workers, bool parallel_aware,
 				   double rows)
@@ -1307,6 +1308,7 @@ create_append_path(PlannerInfo *root,
 
 	Assert(!parallel_aware || parallel_workers > 0);
 
+	pathnode->child_append_relid_sets = child_append_relid_sets;
 	pathnode->path.pathtype = T_Append;
 	pathnode->path.parent = rel;
 	pathnode->path.pathtarget = rel->reltarget;
@@ -1469,6 +1471,7 @@ MergeAppendPath *
 create_merge_append_path(PlannerInfo *root,
 						 RelOptInfo *rel,
 						 List *subpaths,
+						 List *child_append_relid_sets,
 						 List *pathkeys,
 						 Relids required_outer)
 {
@@ -1484,6 +1487,7 @@ create_merge_append_path(PlannerInfo *root,
 	 */
 	Assert(bms_is_empty(rel->lateral_relids) && bms_is_empty(required_outer));
 
+	pathnode->child_append_relid_sets = child_append_relid_sets;
 	pathnode->path.pathtype = T_MergeAppend;
 	pathnode->path.parent = rel;
 	pathnode->path.pathtarget = rel->reltarget;
@@ -3951,6 +3955,7 @@ reparameterize_path(PlannerInfo *root, Path *path,
 				}
 				return (Path *)
 					create_append_path(root, rel, childpaths, partialpaths,
+									   apath->child_append_relid_sets,
 									   apath->path.pathkeys, required_outer,
 									   apath->path.parallel_workers,
 									   apath->path.parallel_aware,
diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h
index aea7f97b83b..2d2cc55af43 100644
--- a/src/include/nodes/pathnodes.h
+++ b/src/include/nodes/pathnodes.h
@@ -2089,6 +2089,12 @@ typedef struct CustomPath
  * For partial Append, 'subpaths' contains non-partial subpaths followed by
  * partial subpaths.
  *
+ * Whenever accumulate_append_subpath() allows us to consolidate multiple
+ * levels of Append paths are consolidated down to one, we store the RTI
+ * sets for the omitted paths in child_append_relid_sets. This is not necessary
+ * for planning or execution; we do it for the benefit of code that wants
+ * to inspect the final plan and understand how it came to be.
+ *
  * Note: it is possible for "subpaths" to contain only one, or even no,
  * elements.  These cases are optimized during create_append_plan.
  * In particular, an AppendPath with no subpaths is a "dummy" path that
@@ -2104,6 +2110,7 @@ typedef struct AppendPath
 	/* Index of first partial path in subpaths; list_length(subpaths) if none */
 	int			first_partial_path;
 	Cardinality limit_tuples;	/* hard limit on output tuples, or -1 */
+	List	   *child_append_relid_sets;
 } AppendPath;
 
 #define IS_DUMMY_APPEND(p) \
@@ -2120,12 +2127,15 @@ extern bool is_dummy_rel(RelOptInfo *rel);
 /*
  * MergeAppendPath represents a MergeAppend plan, ie, the merging of sorted
  * results from several member plans to produce similarly-sorted output.
+ *
+ * child_append_relid_sets has the same meaning here as for AppendPath.
  */
 typedef struct MergeAppendPath
 {
 	Path		path;
 	List	   *subpaths;		/* list of component Paths */
 	Cardinality limit_tuples;	/* hard limit on output tuples, or -1 */
+	List	   *child_append_relid_sets;
 } MergeAppendPath;
 
 /*
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index 45c5037b7c7..8fbbd6cb443 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -387,9 +387,16 @@ struct PartitionPruneInfo;		/* forward reference to struct below */
 typedef struct Append
 {
 	Plan		plan;
+
 	/* RTIs of appendrel(s) formed by this node */
 	Bitmapset  *apprelids;
+
+	/* sets of RTIs of appendrels consolidated into this node */
+	List	   *child_append_relid_sets;
+
+	/* plans to run */
 	List	   *appendplans;
+
 	/* # of asynchronous plans */
 	int			nasyncplans;
 
@@ -419,6 +426,10 @@ typedef struct MergeAppend
 	/* RTIs of appendrel(s) formed by this node */
 	Bitmapset  *apprelids;
 
+	/* sets of RTIs of appendrels consolidated into this node */
+	List	   *child_append_relid_sets;
+
+	/* plans to run */
 	List	   *mergeplans;
 
 	/* these fields are just like the sort-key info in struct Sort: */
diff --git a/src/include/optimizer/pathnode.h b/src/include/optimizer/pathnode.h
index 763cd25bb3c..5f43b7fd0cb 100644
--- a/src/include/optimizer/pathnode.h
+++ b/src/include/optimizer/pathnode.h
@@ -70,12 +70,14 @@ extern TidRangePath *create_tidrangescan_path(PlannerInfo *root,
 											  Relids required_outer);
 extern AppendPath *create_append_path(PlannerInfo *root, RelOptInfo *rel,
 									  List *subpaths, List *partial_subpaths,
+									  List *child_append_relid_sets,
 									  List *pathkeys, Relids required_outer,
 									  int parallel_workers, bool parallel_aware,
 									  double rows);
 extern MergeAppendPath *create_merge_append_path(PlannerInfo *root,
 												 RelOptInfo *rel,
 												 List *subpaths,
+												 List *child_append_relid_sets,
 												 List *pathkeys,
 												 Relids required_outer);
 extern GroupResultPath *create_group_result_path(PlannerInfo *root,
-- 
2.39.5 (Apple Git-154)

v7-0006-Store-information-about-elided-nodes-in-the-final.patchapplication/octet-stream; name=v7-0006-Store-information-about-elided-nodes-in-the-final.patchDownload
From f4079ab4b6df0d438eeb34009391ff5b7e6b2782 Mon Sep 17 00:00:00 2001
From: Robert Haas <rhaas@postgresql.org>
Date: Tue, 22 Apr 2025 14:10:19 -0400
Subject: [PATCH v7 6/7] Store information about elided nodes in the final
 plan.

When setrefs.c removes a SubqueryScan, single-child Append, or
single-child MergeAppend from the final Plan tree, the RTI which
would have been scanned by the removed node no longer appears in
the final plan (the actual range table entry is still present,
but it's no longer referenced).

That's fine for the executor, but it can create difficulties for
code that wants to deduce from the final plan what choices were
made during the planing process. For example, a traversal of a
join tree in the final plan might never encounter the RTI of one
of the relationss in the join problem, and might instead encounter
a scan of a child RTI or even one from a different subquery level.

This patch adjusts things so that each time we elide a node during
setrefs processing, we record the plan_node_id of its single surviving
child, the type of the removed node, and the RTIs that the removed
node would have scanned. This information is recorded in a separate
list that can be ignored by the executor and examined only by code
that cares about these details.

This commit also updates pg_overexplain to display these details.
---
 .../expected/pg_overexplain.out               |  4 +-
 contrib/pg_overexplain/pg_overexplain.c       | 39 ++++++++++++++
 src/backend/optimizer/plan/planner.c          |  1 +
 src/backend/optimizer/plan/setrefs.c          | 52 ++++++++++++++++++-
 src/include/nodes/pathnodes.h                 |  3 ++
 src/include/nodes/plannodes.h                 | 17 ++++++
 src/tools/pgindent/typedefs.list              |  1 +
 7 files changed, 114 insertions(+), 3 deletions(-)

diff --git a/contrib/pg_overexplain/expected/pg_overexplain.out b/contrib/pg_overexplain/expected/pg_overexplain.out
index 57c997e8b32..3c3c65dbb48 100644
--- a/contrib/pg_overexplain/expected/pg_overexplain.out
+++ b/contrib/pg_overexplain/expected/pg_overexplain.out
@@ -488,6 +488,8 @@ SELECT * FROM vegetables WHERE genus = 'daucus';
  Seq Scan on daucus vegetables
    Filter: (genus = 'daucus'::text)
    Scan RTI: 2
+   Elided Node Type: Append
+   Elided Node RTIs: 1
  RTI 1 (relation, inherited, in-from-clause):
    Eref: vegetables (id, name, genus)
    Relation: vegetables
@@ -501,7 +503,7 @@ SELECT * FROM vegetables WHERE genus = 'daucus';
    Relation Kind: relation
    Relation Lock Mode: AccessShareLock
  Unprunable RTIs: 1 2
-(16 rows)
+(18 rows)
 
 -- Also test a case that involves a write.
 EXPLAIN (RANGE_TABLE, COSTS OFF)
diff --git a/contrib/pg_overexplain/pg_overexplain.c b/contrib/pg_overexplain/pg_overexplain.c
index d1d4e2f51e2..3edbe3d1f80 100644
--- a/contrib/pg_overexplain/pg_overexplain.c
+++ b/contrib/pg_overexplain/pg_overexplain.c
@@ -191,6 +191,8 @@ overexplain_per_node_hook(PlanState *planstate, List *ancestors,
 	 */
 	if (options->range_table)
 	{
+		bool		opened_elided_nodes = false;
+
 		switch (nodeTag(plan))
 		{
 			case T_SeqScan:
@@ -272,6 +274,43 @@ overexplain_per_node_hook(PlanState *planstate, List *ancestors,
 			default:
 				break;
 		}
+
+		foreach_node(ElidedNode, n, es->pstmt->elidedNodes)
+		{
+			char	   *elidednodetag;
+
+			if (n->plan_node_id != plan->plan_node_id)
+				continue;
+
+			if (!opened_elided_nodes)
+			{
+				ExplainOpenGroup("Elided Nodes", "Elided Nodes", false, es);
+				opened_elided_nodes = true;
+			}
+
+			switch (n->elided_type)
+			{
+				case T_Append:
+					elidednodetag = "Append";
+					break;
+				case T_MergeAppend:
+					elidednodetag = "MergeAppend";
+					break;
+				case T_SubqueryScan:
+					elidednodetag = "SubqueryScan";
+					break;
+				default:
+					elidednodetag = psprintf("%d", n->elided_type);
+					break;
+			}
+
+			ExplainOpenGroup("Elided Node", NULL, true, es);
+			ExplainPropertyText("Elided Node Type", elidednodetag, es);
+			overexplain_bitmapset("Elided Node RTIs", n->relids, es);
+			ExplainCloseGroup("Elided Node", NULL, true, es);
+		}
+		if (opened_elided_nodes)
+			ExplainCloseGroup("Elided Nodes", "Elided Nodes", false, es);
 	}
 }
 
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index 59ee3f319f4..fef57d84bb3 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -590,6 +590,7 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions,
 	result->paramExecTypes = glob->paramExecTypes;
 	/* utilityStmt should be null, but we might as well copy it */
 	result->utilityStmt = parse->utilityStmt;
+	result->elidedNodes = glob->elidedNodes;
 	result->stmt_location = parse->stmt_location;
 	result->stmt_len = parse->stmt_len;
 
diff --git a/src/backend/optimizer/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.c
index eef43792aeb..5900458a0e1 100644
--- a/src/backend/optimizer/plan/setrefs.c
+++ b/src/backend/optimizer/plan/setrefs.c
@@ -211,6 +211,9 @@ static List *set_windowagg_runcondition_references(PlannerInfo *root,
 												   List *runcondition,
 												   Plan *plan);
 
+static void record_elided_node(PlannerGlobal *glob, int plan_node_id,
+							   NodeTag elided_type, Bitmapset *relids);
+
 
 /*****************************************************************************
  *
@@ -1441,10 +1444,17 @@ set_subqueryscan_references(PlannerInfo *root,
 
 	if (trivial_subqueryscan(plan))
 	{
+		Index		scanrelid;
+
 		/*
 		 * We can omit the SubqueryScan node and just pull up the subplan.
 		 */
 		result = clean_up_removed_plan_level((Plan *) plan, plan->subplan);
+
+		/* Remember that we removed a SubqueryScan */
+		scanrelid = plan->scan.scanrelid + rtoffset;
+		record_elided_node(root->glob, plan->subplan->plan_node_id,
+						   T_SubqueryScan, bms_make_singleton(scanrelid));
 	}
 	else
 	{
@@ -1872,7 +1882,17 @@ set_append_references(PlannerInfo *root,
 		Plan	   *p = (Plan *) linitial(aplan->appendplans);
 
 		if (p->parallel_aware == aplan->plan.parallel_aware)
-			return clean_up_removed_plan_level((Plan *) aplan, p);
+		{
+			Plan	   *result;
+
+			result = clean_up_removed_plan_level((Plan *) aplan, p);
+
+			/* Remember that we removed an Append */
+			record_elided_node(root->glob, p->plan_node_id, T_Append,
+							   offset_relid_set(aplan->apprelids, rtoffset));
+
+			return result;
+		}
 	}
 
 	/*
@@ -1940,7 +1960,17 @@ set_mergeappend_references(PlannerInfo *root,
 		Plan	   *p = (Plan *) linitial(mplan->mergeplans);
 
 		if (p->parallel_aware == mplan->plan.parallel_aware)
-			return clean_up_removed_plan_level((Plan *) mplan, p);
+		{
+			Plan	   *result;
+
+			result = clean_up_removed_plan_level((Plan *) mplan, p);
+
+			/* Remember that we removed a MergeAppend */
+			record_elided_node(root->glob, p->plan_node_id, T_MergeAppend,
+							   offset_relid_set(mplan->apprelids, rtoffset));
+
+			return result;
+		}
 	}
 
 	/*
@@ -3755,3 +3785,21 @@ extract_query_dependencies_walker(Node *node, PlannerInfo *context)
 	return expression_tree_walker(node, extract_query_dependencies_walker,
 								  context);
 }
+
+/*
+ * Record some details about a node removed from the plan during setrefs
+ * procesing, for the benefit of code trying to reconstruct planner decisions
+ * from examination of the final plan tree.
+ */
+static void
+record_elided_node(PlannerGlobal *glob, int plan_node_id,
+				   NodeTag elided_type, Bitmapset *relids)
+{
+	ElidedNode *n = makeNode(ElidedNode);
+
+	n->plan_node_id = plan_node_id;
+	n->elided_type = elided_type;
+	n->relids = relids;
+
+	glob->elidedNodes = lappend(glob->elidedNodes, n);
+}
diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h
index 7f1ed030cb2..aea7f97b83b 100644
--- a/src/include/nodes/pathnodes.h
+++ b/src/include/nodes/pathnodes.h
@@ -159,6 +159,9 @@ typedef struct PlannerGlobal
 	/* type OIDs for PARAM_EXEC Params */
 	List	   *paramExecTypes;
 
+	/* info about nodes elided from the plan during setrefs processing */
+	List	   *elidedNodes;
+
 	/* highest PlaceHolderVar ID assigned */
 	Index		lastPHId;
 
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index 6ed08716b39..45c5037b7c7 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -152,6 +152,9 @@ typedef struct PlannedStmt
 	/* non-null if this is utility stmt */
 	Node	   *utilityStmt;
 
+	/* info about nodes elided from the plan during setrefs processing */
+	List	   *elidedNodes;
+
 	/* statement location in source string (copied from Query) */
 	/* start location, or -1 if unknown */
 	ParseLoc	stmt_location;
@@ -1833,4 +1836,18 @@ typedef struct SubPlanRTInfo
 	bool		dummy;
 } SubPlanRTInfo;
 
+/*
+ * ElidedNode
+ *
+ * Information about nodes elided from the final plan tree: trivial subquery
+ * scans, and single-child Append and MergeAppend nodes.
+ */
+typedef struct ElidedNode
+{
+	NodeTag		type;
+	int			plan_node_id;
+	NodeTag		elided_type;
+	Bitmapset  *relids;
+} ElidedNode;
+
 #endif							/* PLANNODES_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 993688870db..b9f71bc5271 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -4344,3 +4344,4 @@ z_stream
 z_streamp
 zic_t
 SubPlanRTInfo
+ElidedNode
-- 
2.39.5 (Apple Git-154)

#51Junwang Zhao
zhjwpku@gmail.com
In reply to: Robert Haas (#50)
Re: plan shape work

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

#52Junwang Zhao
zhjwpku@gmail.com
In reply to: Junwang Zhao (#51)
Re: plan shape work

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

#53Robert Haas
robertmhaas@gmail.com
In reply to: Junwang Zhao (#52)
Re: plan shape work

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

#54Tom Lane
tgl@sss.pgh.pa.us
In reply to: Robert Haas (#50)
Re: plan shape work

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

#55Robert Haas
robertmhaas@gmail.com
In reply to: Tom Lane (#54)
Re: plan shape work

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

#56Tom Lane
tgl@sss.pgh.pa.us
In reply to: Robert Haas (#55)
1 attachment(s)
Re: plan shape work

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

Attachments:

v8-0001-Ensure-that-all-joinrel-RTIs-are-discoverable-fro.patchtext/x-diff; charset=us-ascii; name*0=v8-0001-Ensure-that-all-joinrel-RTIs-are-discoverable-fro.p; name*1=atchDownload
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

#57Robert Haas
robertmhaas@gmail.com
In reply to: Tom Lane (#56)
Re: plan shape work

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

#58Robert Haas
robertmhaas@gmail.com
In reply to: Robert Haas (#57)
4 attachment(s)
Re: plan shape work

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

Attachments:

v9-0002-Store-information-about-range-table-flattening-in.patchapplication/octet-stream; name=v9-0002-Store-information-about-range-table-flattening-in.patchDownload
From 9039237030f03be9484ec6d734421aeea8ef4e2b Mon Sep 17 00:00:00 2001
From: Robert Haas <rhaas@postgresql.org>
Date: Wed, 24 Sep 2025 10:06:59 -0400
Subject: [PATCH v9 2/4] Store information about range-table flattening in the
 final plan.

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.  So,
this commit remembers in the final plan which part of the final range
table came from which subquery's range table.

Additionally, this commit teaches pg_overexplain'e RANGE_TABLE option
to display the subquery name for each range table entry.
---
 contrib/pg_overexplain/pg_overexplain.c | 36 +++++++++++++++++++++++++
 src/backend/optimizer/plan/planner.c    |  1 +
 src/backend/optimizer/plan/setrefs.c    | 20 ++++++++++++++
 src/include/nodes/pathnodes.h           |  3 +++
 src/include/nodes/plannodes.h           | 17 ++++++++++++
 src/tools/pgindent/typedefs.list        |  1 +
 6 files changed, 78 insertions(+)

diff --git a/contrib/pg_overexplain/pg_overexplain.c b/contrib/pg_overexplain/pg_overexplain.c
index bd70b6d9d5e..5dc707d69e3 100644
--- a/contrib/pg_overexplain/pg_overexplain.c
+++ b/contrib/pg_overexplain/pg_overexplain.c
@@ -395,6 +395,8 @@ static void
 overexplain_range_table(PlannedStmt *plannedstmt, ExplainState *es)
 {
 	Index		rti;
+	ListCell   *lc_subrtinfo = list_head(plannedstmt->subrtinfos);
+	SubPlanRTInfo *rtinfo = NULL;
 
 	/* Open group, one entry per RangeTblEntry */
 	ExplainOpenGroup("Range Table", "Range Table", false, es);
@@ -405,6 +407,18 @@ overexplain_range_table(PlannedStmt *plannedstmt, ExplainState *es)
 		RangeTblEntry *rte = rt_fetch(rti, plannedstmt->rtable);
 		char	   *kind = NULL;
 		char	   *relkind;
+		SubPlanRTInfo *next_rtinfo;
+
+		/* Advance to next SubRTInfo, if it's time. */
+		if (lc_subrtinfo != NULL)
+		{
+			next_rtinfo = lfirst(lc_subrtinfo);
+			if (rti > next_rtinfo->rtoffset)
+			{
+				rtinfo = next_rtinfo;
+				lc_subrtinfo = lnext(plannedstmt->subrtinfos, lc_subrtinfo);
+			}
+		}
 
 		/* NULL entries are possible; skip them */
 		if (rte == NULL)
@@ -469,6 +483,28 @@ overexplain_range_table(PlannedStmt *plannedstmt, ExplainState *es)
 			ExplainPropertyBool("In From Clause", rte->inFromCl, es);
 		}
 
+		/*
+		 * Indicate which subplan is the origin of which RTE. Note dummy
+		 * subplans. Here again, we crunch more onto one line in text format.
+		 */
+		if (rtinfo != NULL)
+		{
+			if (es->format == EXPLAIN_FORMAT_TEXT)
+			{
+				if (!rtinfo->dummy)
+					ExplainPropertyText("Subplan", rtinfo->plan_name, es);
+				else
+					ExplainPropertyText("Subplan",
+										psprintf("%s (dummy)",
+												 rtinfo->plan_name), es);
+			}
+			else
+			{
+				ExplainPropertyText("Subplan", rtinfo->plan_name, es);
+				ExplainPropertyBool("Subplan Is Dummy", rtinfo->dummy, es);
+			}
+		}
+
 		/* rte->alias is optional; rte->eref is requested */
 		if (rte->alias != NULL)
 			overexplain_alias("Alias", rte->alias, es);
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index fafc5627fc7..d47c2915b3a 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -579,6 +579,7 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions,
 	result->unprunableRelids = bms_difference(glob->allRelids,
 											  glob->prunableRelids);
 	result->permInfos = glob->finalrteperminfos;
+	result->subrtinfos = glob->subrtinfos;
 	result->resultRelations = glob->resultRelations;
 	result->appendRelations = glob->appendRelations;
 	result->subplans = glob->subplans;
diff --git a/src/backend/optimizer/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.c
index 6950eff2c5b..eef43792aeb 100644
--- a/src/backend/optimizer/plan/setrefs.c
+++ b/src/backend/optimizer/plan/setrefs.c
@@ -399,6 +399,26 @@ add_rtes_to_flat_rtable(PlannerInfo *root, bool recursing)
 	Index		rti;
 	ListCell   *lc;
 
+	/*
+	 * Record enough information to make it possible for code that looks at
+	 * the final range table to understand how it was constructed. (If
+	 * finalrtable is still NIL, then this is the very topmost PlannerInfo,
+	 * which will always have plan_name == NULL and rtoffset == 0; we omit the
+	 * degenerate list entry.)
+	 */
+	if (root->glob->finalrtable != NIL)
+	{
+		SubPlanRTInfo *rtinfo = makeNode(SubPlanRTInfo);
+
+		rtinfo->plan_name = root->plan_name;
+		rtinfo->rtoffset = list_length(root->glob->finalrtable);
+
+		/* When recursing = true, it's an unplanned or dummy subquery. */
+		rtinfo->dummy = recursing;
+
+		root->glob->subrtinfos = lappend(root->glob->subrtinfos, rtinfo);
+	}
+
 	/*
 	 * Add the query's own RTEs to the flattened rangetable.
 	 *
diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h
index 6bf51fcefb9..e14cbd383fc 100644
--- a/src/include/nodes/pathnodes.h
+++ b/src/include/nodes/pathnodes.h
@@ -135,6 +135,9 @@ typedef struct PlannerGlobal
 	/* "flat" list of RTEPermissionInfos */
 	List	   *finalrteperminfos;
 
+	/* list of SubPlanRTInfo nodes */
+	List	   *subrtinfos;
+
 	/* "flat" list of PlanRowMarks */
 	List	   *finalrowmarks;
 
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index 3d196f5078e..a54f42524b0 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -131,6 +131,9 @@ typedef struct PlannedStmt
 	 */
 	List	   *subplans;
 
+	/* a list of SubPlanRTInfo objects */
+	List	   *subrtinfos;
+
 	/* indices of subplans that require REWIND */
 	Bitmapset  *rewindPlanIDs;
 
@@ -1814,4 +1817,18 @@ typedef enum MonotonicFunction
 	MONOTONICFUNC_BOTH = MONOTONICFUNC_INCREASING | MONOTONICFUNC_DECREASING,
 } MonotonicFunction;
 
+/*
+ * SubPlanRTInfo
+ *
+ * Information about which range table entries came from which subquery
+ * planning cycles.
+ */
+typedef struct SubPlanRTInfo
+{
+	NodeTag		type;
+	const char *plan_name;
+	Index		rtoffset;
+	bool		dummy;
+} SubPlanRTInfo;
+
 #endif							/* PLANNODES_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 3c80d49b67e..993688870db 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -4343,3 +4343,4 @@ yyscan_t
 z_stream
 z_streamp
 zic_t
+SubPlanRTInfo
-- 
2.39.5 (Apple Git-154)

v9-0004-Store-information-about-Append-node-consolidation.patchapplication/octet-stream; name=v9-0004-Store-information-about-Append-node-consolidation.patchDownload
From 419920680cf71cdf0834fcee804c7a6941085feb Mon Sep 17 00:00:00 2001
From: Robert Haas <rhaas@postgresql.org>
Date: Mon, 21 Jul 2025 15:49:06 -0400
Subject: [PATCH v9 4/4] Store information about Append node consolidation in
 the final plan.

When we build an AppendPath or MergeAppendPath, we sometimes pull all
child paths from a subordinate AppendPath or MergeAppendPath instead
of having one such path atop another. This results in the RTIs that
would have been associated with the subordinate path disappearing
from the final plan, making things difficult for code that wants
to scrutinize the final plan and extract information from it about
what happened during the planning process.

To avoid this, propagate the RTI sets that would have been present
in the 'apprelids' field of the subordinate Append or MergeAppend
nodes that would have been created into the surviving Append or
MergeAppend node, using a new 'child_append_relid_sets' field for
that purpose.

This commit also updates pg_overexplain to display these details.
---
 .../expected/pg_overexplain.out               |  4 +-
 contrib/pg_overexplain/pg_overexplain.c       | 56 +++++++++++
 src/backend/optimizer/path/allpaths.c         | 98 +++++++++++++++----
 src/backend/optimizer/path/joinrels.c         |  2 +-
 src/backend/optimizer/plan/createplan.c       |  2 +
 src/backend/optimizer/plan/planner.c          |  1 +
 src/backend/optimizer/prep/prepunion.c        |  5 +-
 src/backend/optimizer/util/pathnode.c         |  5 +
 src/include/nodes/pathnodes.h                 | 10 ++
 src/include/nodes/plannodes.h                 | 11 +++
 src/include/optimizer/pathnode.h              |  2 +
 11 files changed, 171 insertions(+), 25 deletions(-)

diff --git a/contrib/pg_overexplain/expected/pg_overexplain.out b/contrib/pg_overexplain/expected/pg_overexplain.out
index ca9a23ea61f..a377fb2571d 100644
--- a/contrib/pg_overexplain/expected/pg_overexplain.out
+++ b/contrib/pg_overexplain/expected/pg_overexplain.out
@@ -104,6 +104,7 @@ $$);
                Parallel Safe: true
                Plan Node ID: 2
                Append RTIs: 1
+               Child Append RTIs: none
                ->  Seq Scan on brassica vegetables_1
                      Disabled Nodes: 0
                      Parallel Safe: true
@@ -142,7 +143,7 @@ $$);
    Relation Kind: relation
    Relation Lock Mode: AccessShareLock
  Unprunable RTIs: 1 3 4
-(53 rows)
+(54 rows)
 
 -- Test a different output format.
 SELECT explain_filter($$
@@ -197,6 +198,7 @@ $$);
                <extParam>none</extParam>                            +
                <allParam>none</allParam>                            +
                <Append-RTIs>1</Append-RTIs>                         +
+               <Child-Append-RTIs>none</Child-Append-RTIs>          +
                <Subplans-Removed>0</Subplans-Removed>               +
                <Plans>                                              +
                  <Plan>                                             +
diff --git a/contrib/pg_overexplain/pg_overexplain.c b/contrib/pg_overexplain/pg_overexplain.c
index fa907fa472e..6538ffcafb0 100644
--- a/contrib/pg_overexplain/pg_overexplain.c
+++ b/contrib/pg_overexplain/pg_overexplain.c
@@ -54,6 +54,8 @@ static void overexplain_alias(const char *qlabel, Alias *alias,
 							  ExplainState *es);
 static void overexplain_bitmapset(const char *qlabel, Bitmapset *bms,
 								  ExplainState *es);
+static void overexplain_bitmapset_list(const char *qlabel, List *bms_list,
+									   ExplainState *es);
 static void overexplain_intlist(const char *qlabel, List *list,
 								ExplainState *es);
 
@@ -232,11 +234,17 @@ overexplain_per_node_hook(PlanState *planstate, List *ancestors,
 				overexplain_bitmapset("Append RTIs",
 									  ((Append *) plan)->apprelids,
 									  es);
+				overexplain_bitmapset_list("Child Append RTIs",
+										   ((Append *) plan)->child_append_relid_sets,
+										   es);
 				break;
 			case T_MergeAppend:
 				overexplain_bitmapset("Append RTIs",
 									  ((MergeAppend *) plan)->apprelids,
 									  es);
+				overexplain_bitmapset_list("Child Append RTIs",
+										   ((MergeAppend *) plan)->child_append_relid_sets,
+										   es);
 				break;
 			case T_Result:
 
@@ -815,6 +823,54 @@ overexplain_bitmapset(const char *qlabel, Bitmapset *bms, ExplainState *es)
 	pfree(buf.data);
 }
 
+/*
+ * Emit a text property describing the contents of a list of bitmapsets.
+ * If a bitmapset contains exactly 1 member, we just print an integer;
+ * otherwise, we surround the list of members by parentheses.
+ *
+ * If there are no bitmapsets in the list, we print the word "none".
+ */
+static void
+overexplain_bitmapset_list(const char *qlabel, List *bms_list,
+						   ExplainState *es)
+{
+	StringInfoData buf;
+
+	initStringInfo(&buf);
+
+	foreach_node(Bitmapset, bms, bms_list)
+	{
+		if (bms_membership(bms) == BMS_SINGLETON)
+			appendStringInfo(&buf, " %d", bms_singleton_member(bms));
+		else
+		{
+			int			x = -1;
+			bool		first = true;
+
+			appendStringInfoString(&buf, " (");
+			while ((x = bms_next_member(bms, x)) >= 0)
+			{
+				if (first)
+					first = false;
+				else
+					appendStringInfoChar(&buf, ' ');
+				appendStringInfo(&buf, "%d", x);
+			}
+			appendStringInfoChar(&buf, ')');
+		}
+	}
+
+	if (buf.len == 0)
+	{
+		ExplainPropertyText(qlabel, "none", es);
+		return;
+	}
+
+	Assert(buf.data[0] == ' ');
+	ExplainPropertyText(qlabel, buf.data + 1, es);
+	pfree(buf.data);
+}
+
 /*
  * Emit a text property describing the contents of a list of integers, OIDs,
  * or XIDs -- either a space-separated list of integer members, or the word
diff --git a/src/backend/optimizer/path/allpaths.c b/src/backend/optimizer/path/allpaths.c
index ffc21a6afeb..bf29e348ee6 100644
--- a/src/backend/optimizer/path/allpaths.c
+++ b/src/backend/optimizer/path/allpaths.c
@@ -122,8 +122,10 @@ static Path *get_cheapest_parameterized_child_path(PlannerInfo *root,
 												   Relids required_outer);
 static void accumulate_append_subpath(Path *path,
 									  List **subpaths,
-									  List **special_subpaths);
-static Path *get_singleton_append_subpath(Path *path);
+									  List **special_subpaths,
+									  List **child_append_relid_sets);
+static Path *get_singleton_append_subpath(Path *path,
+										  List **child_append_relid_sets);
 static void set_dummy_rel_pathlist(RelOptInfo *rel);
 static void set_subquery_pathlist(PlannerInfo *root, RelOptInfo *rel,
 								  Index rti, RangeTblEntry *rte);
@@ -1323,11 +1325,15 @@ add_paths_to_append_rel(PlannerInfo *root, RelOptInfo *rel,
 {
 	List	   *subpaths = NIL;
 	bool		subpaths_valid = true;
+	List	   *subpath_cars = NIL;
 	List	   *startup_subpaths = NIL;
 	bool		startup_subpaths_valid = true;
+	List	   *startup_subpath_cars = NIL;
 	List	   *partial_subpaths = NIL;
+	List	   *partial_subpath_cars = NIL;
 	List	   *pa_partial_subpaths = NIL;
 	List	   *pa_nonpartial_subpaths = NIL;
+	List	   *pa_subpath_cars = NIL;
 	bool		partial_subpaths_valid = true;
 	bool		pa_subpaths_valid;
 	List	   *all_child_pathkeys = NIL;
@@ -1360,7 +1366,7 @@ add_paths_to_append_rel(PlannerInfo *root, RelOptInfo *rel,
 		if (childrel->pathlist != NIL &&
 			childrel->cheapest_total_path->param_info == NULL)
 			accumulate_append_subpath(childrel->cheapest_total_path,
-									  &subpaths, NULL);
+									  &subpaths, NULL, &subpath_cars);
 		else
 			subpaths_valid = false;
 
@@ -1389,7 +1395,8 @@ add_paths_to_append_rel(PlannerInfo *root, RelOptInfo *rel,
 			Assert(cheapest_path->param_info == NULL);
 			accumulate_append_subpath(cheapest_path,
 									  &startup_subpaths,
-									  NULL);
+									  NULL,
+									  &startup_subpath_cars);
 		}
 		else
 			startup_subpaths_valid = false;
@@ -1400,7 +1407,8 @@ add_paths_to_append_rel(PlannerInfo *root, RelOptInfo *rel,
 		{
 			cheapest_partial_path = linitial(childrel->partial_pathlist);
 			accumulate_append_subpath(cheapest_partial_path,
-									  &partial_subpaths, NULL);
+									  &partial_subpaths, NULL,
+									  &partial_subpath_cars);
 		}
 		else
 			partial_subpaths_valid = false;
@@ -1429,7 +1437,8 @@ add_paths_to_append_rel(PlannerInfo *root, RelOptInfo *rel,
 				Assert(cheapest_partial_path != NULL);
 				accumulate_append_subpath(cheapest_partial_path,
 										  &pa_partial_subpaths,
-										  &pa_nonpartial_subpaths);
+										  &pa_nonpartial_subpaths,
+										  &pa_subpath_cars);
 			}
 			else
 			{
@@ -1448,7 +1457,8 @@ add_paths_to_append_rel(PlannerInfo *root, RelOptInfo *rel,
 				 */
 				accumulate_append_subpath(nppath,
 										  &pa_nonpartial_subpaths,
-										  NULL);
+										  NULL,
+										  &pa_subpath_cars);
 			}
 		}
 
@@ -1523,14 +1533,16 @@ add_paths_to_append_rel(PlannerInfo *root, RelOptInfo *rel,
 	 * if we have zero or one live subpath due to constraint exclusion.)
 	 */
 	if (subpaths_valid)
-		add_path(rel, (Path *) create_append_path(root, rel, subpaths, NIL,
+		add_path(rel, (Path *) create_append_path(root, rel, subpaths,
+												  NIL, subpath_cars,
 												  NIL, NULL, 0, false,
 												  -1));
 
 	/* build an AppendPath for the cheap startup paths, if valid */
 	if (startup_subpaths_valid)
 		add_path(rel, (Path *) create_append_path(root, rel, startup_subpaths,
-												  NIL, NIL, NULL, 0, false, -1));
+												  NIL, startup_subpath_cars,
+												  NIL, NULL, 0, false, -1));
 
 	/*
 	 * Consider an append of unordered, unparameterized partial paths.  Make
@@ -1571,6 +1583,7 @@ add_paths_to_append_rel(PlannerInfo *root, RelOptInfo *rel,
 
 		/* Generate a partial append path. */
 		appendpath = create_append_path(root, rel, NIL, partial_subpaths,
+										partial_subpath_cars,
 										NIL, NULL, parallel_workers,
 										enable_parallel_append,
 										-1);
@@ -1621,6 +1634,7 @@ add_paths_to_append_rel(PlannerInfo *root, RelOptInfo *rel,
 
 		appendpath = create_append_path(root, rel, pa_nonpartial_subpaths,
 										pa_partial_subpaths,
+										pa_subpath_cars,
 										NIL, NULL, parallel_workers, true,
 										partial_rows);
 		add_partial_path(rel, (Path *) appendpath);
@@ -1654,6 +1668,7 @@ add_paths_to_append_rel(PlannerInfo *root, RelOptInfo *rel,
 
 		/* Select the child paths for an Append with this parameterization */
 		subpaths = NIL;
+		subpath_cars = NIL;
 		subpaths_valid = true;
 		foreach(lcr, live_childrels)
 		{
@@ -1676,12 +1691,13 @@ add_paths_to_append_rel(PlannerInfo *root, RelOptInfo *rel,
 				subpaths_valid = false;
 				break;
 			}
-			accumulate_append_subpath(subpath, &subpaths, NULL);
+			accumulate_append_subpath(subpath, &subpaths, NULL,
+									  &subpath_cars);
 		}
 
 		if (subpaths_valid)
 			add_path(rel, (Path *)
-					 create_append_path(root, rel, subpaths, NIL,
+					 create_append_path(root, rel, subpaths, NIL, subpath_cars,
 										NIL, required_outer, 0, false,
 										-1));
 	}
@@ -1708,6 +1724,7 @@ add_paths_to_append_rel(PlannerInfo *root, RelOptInfo *rel,
 				continue;
 
 			appendpath = create_append_path(root, rel, NIL, list_make1(path),
+											list_make1(rel->relids),
 											NIL, NULL,
 											path->parallel_workers, true,
 											partial_rows);
@@ -1789,8 +1806,11 @@ generate_orderedappend_paths(PlannerInfo *root, RelOptInfo *rel,
 	{
 		List	   *pathkeys = (List *) lfirst(lcp);
 		List	   *startup_subpaths = NIL;
+		List	   *startup_subpath_cars = NIL;
 		List	   *total_subpaths = NIL;
+		List	   *total_subpath_cars = NIL;
 		List	   *fractional_subpaths = NIL;
+		List	   *fractional_subpath_cars = NIL;
 		bool		startup_neq_total = false;
 		bool		match_partition_order;
 		bool		match_partition_order_desc;
@@ -1942,16 +1962,23 @@ generate_orderedappend_paths(PlannerInfo *root, RelOptInfo *rel,
 				 * just a single subpath (and hence aren't doing anything
 				 * useful).
 				 */
-				cheapest_startup = get_singleton_append_subpath(cheapest_startup);
-				cheapest_total = get_singleton_append_subpath(cheapest_total);
+				cheapest_startup =
+					get_singleton_append_subpath(cheapest_startup,
+												 &startup_subpath_cars);
+				cheapest_total =
+					get_singleton_append_subpath(cheapest_total,
+												 &total_subpath_cars);
 
 				startup_subpaths = lappend(startup_subpaths, cheapest_startup);
 				total_subpaths = lappend(total_subpaths, cheapest_total);
 
 				if (cheapest_fractional)
 				{
-					cheapest_fractional = get_singleton_append_subpath(cheapest_fractional);
-					fractional_subpaths = lappend(fractional_subpaths, cheapest_fractional);
+					cheapest_fractional =
+						get_singleton_append_subpath(cheapest_fractional,
+													 &fractional_subpath_cars);
+					fractional_subpaths =
+						lappend(fractional_subpaths, cheapest_fractional);
 				}
 			}
 			else
@@ -1961,13 +1988,16 @@ generate_orderedappend_paths(PlannerInfo *root, RelOptInfo *rel,
 				 * child paths for the MergeAppend.
 				 */
 				accumulate_append_subpath(cheapest_startup,
-										  &startup_subpaths, NULL);
+										  &startup_subpaths, NULL,
+										  &startup_subpath_cars);
 				accumulate_append_subpath(cheapest_total,
-										  &total_subpaths, NULL);
+										  &total_subpaths, NULL,
+										  &total_subpath_cars);
 
 				if (cheapest_fractional)
 					accumulate_append_subpath(cheapest_fractional,
-											  &fractional_subpaths, NULL);
+											  &fractional_subpaths, NULL,
+											  &fractional_subpath_cars);
 			}
 		}
 
@@ -1979,6 +2009,7 @@ generate_orderedappend_paths(PlannerInfo *root, RelOptInfo *rel,
 													  rel,
 													  startup_subpaths,
 													  NIL,
+													  startup_subpath_cars,
 													  pathkeys,
 													  NULL,
 													  0,
@@ -1989,6 +2020,7 @@ generate_orderedappend_paths(PlannerInfo *root, RelOptInfo *rel,
 														  rel,
 														  total_subpaths,
 														  NIL,
+														  total_subpath_cars,
 														  pathkeys,
 														  NULL,
 														  0,
@@ -2000,6 +2032,7 @@ generate_orderedappend_paths(PlannerInfo *root, RelOptInfo *rel,
 														  rel,
 														  fractional_subpaths,
 														  NIL,
+														  fractional_subpath_cars,
 														  pathkeys,
 														  NULL,
 														  0,
@@ -2012,12 +2045,14 @@ generate_orderedappend_paths(PlannerInfo *root, RelOptInfo *rel,
 			add_path(rel, (Path *) create_merge_append_path(root,
 															rel,
 															startup_subpaths,
+															startup_subpath_cars,
 															pathkeys,
 															NULL));
 			if (startup_neq_total)
 				add_path(rel, (Path *) create_merge_append_path(root,
 																rel,
 																total_subpaths,
+																total_subpath_cars,
 																pathkeys,
 																NULL));
 
@@ -2025,6 +2060,7 @@ generate_orderedappend_paths(PlannerInfo *root, RelOptInfo *rel,
 				add_path(rel, (Path *) create_merge_append_path(root,
 																rel,
 																fractional_subpaths,
+																fractional_subpath_cars,
 																pathkeys,
 																NULL));
 		}
@@ -2127,7 +2163,8 @@ get_cheapest_parameterized_child_path(PlannerInfo *root, RelOptInfo *rel,
  * paths).
  */
 static void
-accumulate_append_subpath(Path *path, List **subpaths, List **special_subpaths)
+accumulate_append_subpath(Path *path, List **subpaths, List **special_subpaths,
+						  List **child_append_relid_sets)
 {
 	if (IsA(path, AppendPath))
 	{
@@ -2136,6 +2173,8 @@ accumulate_append_subpath(Path *path, List **subpaths, List **special_subpaths)
 		if (!apath->path.parallel_aware || apath->first_partial_path == 0)
 		{
 			*subpaths = list_concat(*subpaths, apath->subpaths);
+			*child_append_relid_sets =
+				lappend(*child_append_relid_sets, path->parent->relids);
 			return;
 		}
 		else if (special_subpaths != NULL)
@@ -2150,6 +2189,8 @@ accumulate_append_subpath(Path *path, List **subpaths, List **special_subpaths)
 												  apath->first_partial_path);
 			*special_subpaths = list_concat(*special_subpaths,
 											new_special_subpaths);
+			*child_append_relid_sets =
+				lappend(*child_append_relid_sets, path->parent->relids);
 			return;
 		}
 	}
@@ -2158,6 +2199,8 @@ accumulate_append_subpath(Path *path, List **subpaths, List **special_subpaths)
 		MergeAppendPath *mpath = (MergeAppendPath *) path;
 
 		*subpaths = list_concat(*subpaths, mpath->subpaths);
+		*child_append_relid_sets =
+			lappend(*child_append_relid_sets, path->parent->relids);
 		return;
 	}
 
@@ -2169,10 +2212,15 @@ accumulate_append_subpath(Path *path, List **subpaths, List **special_subpaths)
  *		Returns the single subpath of an Append/MergeAppend, or just
  *		return 'path' if it's not a single sub-path Append/MergeAppend.
  *
+ * As a side effect, whenever we return a single subpath rather than the
+ * original path, add the relid set for the original path to
+ * child_append_relid_sets, so that those relids don't entirely disappear
+ * from the final plan.
+ *
  * Note: 'path' must not be a parallel-aware path.
  */
 static Path *
-get_singleton_append_subpath(Path *path)
+get_singleton_append_subpath(Path *path, List **child_append_relid_sets)
 {
 	Assert(!path->parallel_aware);
 
@@ -2181,14 +2229,22 @@ get_singleton_append_subpath(Path *path)
 		AppendPath *apath = (AppendPath *) path;
 
 		if (list_length(apath->subpaths) == 1)
+		{
+			*child_append_relid_sets =
+				lappend(*child_append_relid_sets, path->parent->relids);
 			return (Path *) linitial(apath->subpaths);
+		}
 	}
 	else if (IsA(path, MergeAppendPath))
 	{
 		MergeAppendPath *mpath = (MergeAppendPath *) path;
 
 		if (list_length(mpath->subpaths) == 1)
+		{
+			*child_append_relid_sets =
+				lappend(*child_append_relid_sets, path->parent->relids);
 			return (Path *) linitial(mpath->subpaths);
+		}
 	}
 
 	return path;
@@ -2217,7 +2273,7 @@ set_dummy_rel_pathlist(RelOptInfo *rel)
 	rel->partial_pathlist = NIL;
 
 	/* Set up the dummy path */
-	add_path(rel, (Path *) create_append_path(NULL, rel, NIL, NIL,
+	add_path(rel, (Path *) create_append_path(NULL, rel, NIL, NIL, NIL,
 											  NIL, rel->lateral_relids,
 											  0, false, -1));
 
diff --git a/src/backend/optimizer/path/joinrels.c b/src/backend/optimizer/path/joinrels.c
index 535248aa525..6bddfc537d2 100644
--- a/src/backend/optimizer/path/joinrels.c
+++ b/src/backend/optimizer/path/joinrels.c
@@ -1342,7 +1342,7 @@ mark_dummy_rel(RelOptInfo *rel)
 
 	/* Set up the dummy path */
 	add_path(rel, (Path *) create_append_path(NULL, rel, NIL, NIL,
-											  NIL, rel->lateral_relids,
+											  NIL, NIL, rel->lateral_relids,
 											  0, false, -1));
 
 	/* Set or update cheapest_total_path and related fields */
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index c9dba7ff346..71d0fe00133 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -1266,6 +1266,7 @@ create_append_plan(PlannerInfo *root, AppendPath *best_path, int flags)
 	plan->plan.lefttree = NULL;
 	plan->plan.righttree = NULL;
 	plan->apprelids = rel->relids;
+	plan->child_append_relid_sets = best_path->child_append_relid_sets;
 
 	if (pathkeys != NIL)
 	{
@@ -1478,6 +1479,7 @@ create_merge_append_plan(PlannerInfo *root, MergeAppendPath *best_path,
 	plan->lefttree = NULL;
 	plan->righttree = NULL;
 	node->apprelids = rel->relids;
+	node->child_append_relid_sets = best_path->child_append_relid_sets;
 
 	/*
 	 * Compute sort column info, and adjust MergeAppend's tlist as needed.
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index d977cbc92ad..12788989a8f 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -3979,6 +3979,7 @@ create_degenerate_grouping_paths(PlannerInfo *root, RelOptInfo *input_rel,
 							   paths,
 							   NIL,
 							   NIL,
+							   NIL,
 							   NULL,
 							   0,
 							   false,
diff --git a/src/backend/optimizer/prep/prepunion.c b/src/backend/optimizer/prep/prepunion.c
index 598877c4f9d..505282c054f 100644
--- a/src/backend/optimizer/prep/prepunion.c
+++ b/src/backend/optimizer/prep/prepunion.c
@@ -815,7 +815,7 @@ generate_union_paths(SetOperationStmt *op, PlannerInfo *root,
 	 * union child.
 	 */
 	apath = (Path *) create_append_path(root, result_rel, cheapest_pathlist,
-										NIL, NIL, NULL, 0, false, -1);
+										NIL, NIL, NIL, NULL, 0, false, -1);
 
 	/*
 	 * Estimate number of groups.  For now we just assume the output is unique
@@ -861,7 +861,7 @@ generate_union_paths(SetOperationStmt *op, PlannerInfo *root,
 
 		papath = (Path *)
 			create_append_path(root, result_rel, NIL, partial_pathlist,
-							   NIL, NULL, parallel_workers,
+							   NIL, NIL, NULL, parallel_workers,
 							   enable_parallel_append, -1);
 		gpath = (Path *)
 			create_gather_path(root, result_rel, papath,
@@ -969,6 +969,7 @@ generate_union_paths(SetOperationStmt *op, PlannerInfo *root,
 			path = (Path *) create_merge_append_path(root,
 													 result_rel,
 													 ordered_pathlist,
+													 NIL,
 													 union_pathkeys,
 													 NULL);
 
diff --git a/src/backend/optimizer/util/pathnode.c b/src/backend/optimizer/util/pathnode.c
index b0da28150d3..b4546c8842d 100644
--- a/src/backend/optimizer/util/pathnode.c
+++ b/src/backend/optimizer/util/pathnode.c
@@ -1298,6 +1298,7 @@ AppendPath *
 create_append_path(PlannerInfo *root,
 				   RelOptInfo *rel,
 				   List *subpaths, List *partial_subpaths,
+				   List *child_append_relid_sets,
 				   List *pathkeys, Relids required_outer,
 				   int parallel_workers, bool parallel_aware,
 				   double rows)
@@ -1307,6 +1308,7 @@ create_append_path(PlannerInfo *root,
 
 	Assert(!parallel_aware || parallel_workers > 0);
 
+	pathnode->child_append_relid_sets = child_append_relid_sets;
 	pathnode->path.pathtype = T_Append;
 	pathnode->path.parent = rel;
 	pathnode->path.pathtarget = rel->reltarget;
@@ -1469,6 +1471,7 @@ MergeAppendPath *
 create_merge_append_path(PlannerInfo *root,
 						 RelOptInfo *rel,
 						 List *subpaths,
+						 List *child_append_relid_sets,
 						 List *pathkeys,
 						 Relids required_outer)
 {
@@ -1484,6 +1487,7 @@ create_merge_append_path(PlannerInfo *root,
 	 */
 	Assert(bms_is_empty(rel->lateral_relids) && bms_is_empty(required_outer));
 
+	pathnode->child_append_relid_sets = child_append_relid_sets;
 	pathnode->path.pathtype = T_MergeAppend;
 	pathnode->path.parent = rel;
 	pathnode->path.pathtarget = rel->reltarget;
@@ -3951,6 +3955,7 @@ reparameterize_path(PlannerInfo *root, Path *path,
 				}
 				return (Path *)
 					create_append_path(root, rel, childpaths, partialpaths,
+									   apath->child_append_relid_sets,
 									   apath->path.pathkeys, required_outer,
 									   apath->path.parallel_workers,
 									   apath->path.parallel_aware,
diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h
index 3bff0763f7b..83e5ad35995 100644
--- a/src/include/nodes/pathnodes.h
+++ b/src/include/nodes/pathnodes.h
@@ -2089,6 +2089,12 @@ typedef struct CustomPath
  * For partial Append, 'subpaths' contains non-partial subpaths followed by
  * partial subpaths.
  *
+ * Whenever accumulate_append_subpath() allows us to consolidate multiple
+ * levels of Append paths are consolidated down to one, we store the RTI
+ * sets for the omitted paths in child_append_relid_sets. This is not necessary
+ * for planning or execution; we do it for the benefit of code that wants
+ * to inspect the final plan and understand how it came to be.
+ *
  * Note: it is possible for "subpaths" to contain only one, or even no,
  * elements.  These cases are optimized during create_append_plan.
  * In particular, an AppendPath with no subpaths is a "dummy" path that
@@ -2104,6 +2110,7 @@ typedef struct AppendPath
 	/* Index of first partial path in subpaths; list_length(subpaths) if none */
 	int			first_partial_path;
 	Cardinality limit_tuples;	/* hard limit on output tuples, or -1 */
+	List	   *child_append_relid_sets;
 } AppendPath;
 
 #define IS_DUMMY_APPEND(p) \
@@ -2120,12 +2127,15 @@ extern bool is_dummy_rel(RelOptInfo *rel);
 /*
  * MergeAppendPath represents a MergeAppend plan, ie, the merging of sorted
  * results from several member plans to produce similarly-sorted output.
+ *
+ * child_append_relid_sets has the same meaning here as for AppendPath.
  */
 typedef struct MergeAppendPath
 {
 	Path		path;
 	List	   *subpaths;		/* list of component Paths */
 	Cardinality limit_tuples;	/* hard limit on output tuples, or -1 */
+	List	   *child_append_relid_sets;
 } MergeAppendPath;
 
 /*
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index 218f8f1ad6d..c5b823e79b6 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -387,9 +387,16 @@ struct PartitionPruneInfo;		/* forward reference to struct below */
 typedef struct Append
 {
 	Plan		plan;
+
 	/* RTIs of appendrel(s) formed by this node */
 	Bitmapset  *apprelids;
+
+	/* sets of RTIs of appendrels consolidated into this node */
+	List	   *child_append_relid_sets;
+
+	/* plans to run */
 	List	   *appendplans;
+
 	/* # of asynchronous plans */
 	int			nasyncplans;
 
@@ -419,6 +426,10 @@ typedef struct MergeAppend
 	/* RTIs of appendrel(s) formed by this node */
 	Bitmapset  *apprelids;
 
+	/* sets of RTIs of appendrels consolidated into this node */
+	List	   *child_append_relid_sets;
+
+	/* plans to run */
 	List	   *mergeplans;
 
 	/* these fields are just like the sort-key info in struct Sort: */
diff --git a/src/include/optimizer/pathnode.h b/src/include/optimizer/pathnode.h
index 763cd25bb3c..5f43b7fd0cb 100644
--- a/src/include/optimizer/pathnode.h
+++ b/src/include/optimizer/pathnode.h
@@ -70,12 +70,14 @@ extern TidRangePath *create_tidrangescan_path(PlannerInfo *root,
 											  Relids required_outer);
 extern AppendPath *create_append_path(PlannerInfo *root, RelOptInfo *rel,
 									  List *subpaths, List *partial_subpaths,
+									  List *child_append_relid_sets,
 									  List *pathkeys, Relids required_outer,
 									  int parallel_workers, bool parallel_aware,
 									  double rows);
 extern MergeAppendPath *create_merge_append_path(PlannerInfo *root,
 												 RelOptInfo *rel,
 												 List *subpaths,
+												 List *child_append_relid_sets,
 												 List *pathkeys,
 												 Relids required_outer);
 extern GroupResultPath *create_group_result_path(PlannerInfo *root,
-- 
2.39.5 (Apple Git-154)

v9-0001-Assign-each-subquery-a-unique-name-prior-to-plann.patchapplication/octet-stream; name=v9-0001-Assign-each-subquery-a-unique-name-prior-to-plann.patchDownload
From cf1dc523bded1925c77db3f72a726efe89e0aa05 Mon Sep 17 00:00:00 2001
From: Robert Haas <rhaas@postgresql.org>
Date: Wed, 24 Sep 2025 10:05:17 -0400
Subject: [PATCH v9 1/4] Assign each subquery a unique name prior to planning
 it.

Previously, subqueries were given names only after they were planned,
which makes it difficult to use information from a previous execution of
the query to guide future planning. If, for example, you knew something
about how you want "InitPlan 2" to be planned, you won't know whether
the subquery you're currently planning will end up being "InitPlan 2"
until after you've finished planning it, by which point it's too late to
use the information that you had.

To fix this, assign each subplan a unique name before we begin planning
it. To improve consistency, use textual names for all subplans, rather
than, as we did previously, a mix of numbers (such as "InitPlan 1") and
names (such as "CTE foo"), and make sure that the same name is never
assigned more than once.

We adopt the somewhat arbitrary convention of using the type of sublink
to set the plan name; for example, a query that previously had two
expression sublinks shown as InitPlan 2 and InitPlan 1 will now end up
named expr_1 and expr_2. Because names are assigned before rather than
after planning, some of the regression test outputs show the numerical
part of the name switching positions: what was previously SubPlan 2 was
actually the first one encountered, but we finished planning it later.

We assign names even to subqueries that aren't shown as such within the
EXPLAIN output. These include subqueries that are a FROM clause item or
a branch of a set operation, rather than something that will be turned
into an InitPlan or SubPlan. The purpose of this is to make sure that,
below the topmost query level, there's always a name for each subquery
that is stable from one planning cycle to the next (assuming no changes
to the query or the database schema).
---
 .../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                 |   3 +-
 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, 808 insertions(+), 653 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..ffc21a6afeb 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;
+	const 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..fafc5627fc7 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, const 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.
+ */
+const 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 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..a8637efd392 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, &paramIds);
 		if (subquery)
 		{
+			const 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..598877c4f9d 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;
+		const 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..6bf51fcefb9 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 */
+	const 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..dacd079e56e 100644
--- a/src/include/nodes/primnodes.h
+++ b/src/include/nodes/primnodes.h
@@ -1088,13 +1088,14 @@ typedef struct SubPlan
 	/* Identification of the Plan tree to use: */
 	int			plan_id;		/* Index (from 1) in PlannedStmt.subplans */
 	/* Identification of the SubPlan for EXPLAIN and debugging purposes: */
-	char	   *plan_name;		/* A name assigned during planning */
+	const char *plan_name;		/* A name assigned during planning */
 	/* Extra data useful for determining subplan's output type: */
 	Oid			firstColType;	/* Type of first column of subplan result */
 	int32		firstColTypmod; /* Typmod of first column of subplan result */
 	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..e8abb1969a8 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,
+									 const 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 const 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::integer END), 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 ((SubPlan expr_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::integer END, t1.v
+         Output: GROUPING((SubPlan expr_1)), ((SubPlan expr_3)), CASE WHEN (GROUPING((SubPlan expr_2)) = 0) THEN ((SubPlan expr_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::integer END), 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 ((SubPlan expr_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::integer END, t1.v
+         Output: GROUPING((SubPlan expr_1)), ((SubPlan expr_3)), CASE WHEN (GROUPING((SubPlan expr_2)) = 0) THEN ((SubPlan expr_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.39.5 (Apple Git-154)

v9-0003-Store-information-about-elided-nodes-in-the-final.patchapplication/octet-stream; name=v9-0003-Store-information-about-elided-nodes-in-the-final.patchDownload
From e8e329c64a2eda3a664d6e7e7ffd94ce3c3a2e59 Mon Sep 17 00:00:00 2001
From: Robert Haas <rhaas@postgresql.org>
Date: Tue, 22 Apr 2025 14:10:19 -0400
Subject: [PATCH v9 3/4] Store information about elided nodes in the final
 plan.

When setrefs.c removes a SubqueryScan, single-child Append, or
single-child MergeAppend from the final Plan tree, the RTI which
would have been scanned by the removed node no longer appears in
the final plan (the actual range table entry is still present,
but it's no longer referenced).

That's fine for the executor, but it can create difficulties for
code that wants to deduce from the final plan what choices were
made during the planing process. For example, a traversal of a
join tree in the final plan might never encounter the RTI of one
of the relationss in the join problem, and might instead encounter
a scan of a child RTI or even one from a different subquery level.

This patch adjusts things so that each time we elide a node during
setrefs processing, we record the plan_node_id of its single surviving
child, the type of the removed node, and the RTIs that the removed
node would have scanned. This information is recorded in a separate
list that can be ignored by the executor and examined only by code
that cares about these details.

This commit also updates pg_overexplain to display these details.
---
 .../expected/pg_overexplain.out               |  4 +-
 contrib/pg_overexplain/pg_overexplain.c       | 39 ++++++++++++++
 src/backend/optimizer/plan/planner.c          |  1 +
 src/backend/optimizer/plan/setrefs.c          | 52 ++++++++++++++++++-
 src/include/nodes/pathnodes.h                 |  3 ++
 src/include/nodes/plannodes.h                 | 17 ++++++
 src/tools/pgindent/typedefs.list              |  1 +
 7 files changed, 114 insertions(+), 3 deletions(-)

diff --git a/contrib/pg_overexplain/expected/pg_overexplain.out b/contrib/pg_overexplain/expected/pg_overexplain.out
index 55d34666d87..ca9a23ea61f 100644
--- a/contrib/pg_overexplain/expected/pg_overexplain.out
+++ b/contrib/pg_overexplain/expected/pg_overexplain.out
@@ -452,6 +452,8 @@ SELECT * FROM vegetables WHERE genus = 'daucus';
  Seq Scan on daucus vegetables
    Filter: (genus = 'daucus'::text)
    Scan RTI: 2
+   Elided Node Type: Append
+   Elided Node RTIs: 1
  RTI 1 (relation, inherited, in-from-clause):
    Eref: vegetables (id, name, genus)
    Relation: vegetables
@@ -465,7 +467,7 @@ SELECT * FROM vegetables WHERE genus = 'daucus';
    Relation Kind: relation
    Relation Lock Mode: AccessShareLock
  Unprunable RTIs: 1 2
-(16 rows)
+(18 rows)
 
 -- Also test a case that involves a write.
 EXPLAIN (RANGE_TABLE, COSTS OFF)
diff --git a/contrib/pg_overexplain/pg_overexplain.c b/contrib/pg_overexplain/pg_overexplain.c
index 5dc707d69e3..fa907fa472e 100644
--- a/contrib/pg_overexplain/pg_overexplain.c
+++ b/contrib/pg_overexplain/pg_overexplain.c
@@ -191,6 +191,8 @@ overexplain_per_node_hook(PlanState *planstate, List *ancestors,
 	 */
 	if (options->range_table)
 	{
+		bool		opened_elided_nodes = false;
+
 		switch (nodeTag(plan))
 		{
 			case T_SeqScan:
@@ -251,6 +253,43 @@ overexplain_per_node_hook(PlanState *planstate, List *ancestors,
 			default:
 				break;
 		}
+
+		foreach_node(ElidedNode, n, es->pstmt->elidedNodes)
+		{
+			char	   *elidednodetag;
+
+			if (n->plan_node_id != plan->plan_node_id)
+				continue;
+
+			if (!opened_elided_nodes)
+			{
+				ExplainOpenGroup("Elided Nodes", "Elided Nodes", false, es);
+				opened_elided_nodes = true;
+			}
+
+			switch (n->elided_type)
+			{
+				case T_Append:
+					elidednodetag = "Append";
+					break;
+				case T_MergeAppend:
+					elidednodetag = "MergeAppend";
+					break;
+				case T_SubqueryScan:
+					elidednodetag = "SubqueryScan";
+					break;
+				default:
+					elidednodetag = psprintf("%d", n->elided_type);
+					break;
+			}
+
+			ExplainOpenGroup("Elided Node", NULL, true, es);
+			ExplainPropertyText("Elided Node Type", elidednodetag, es);
+			overexplain_bitmapset("Elided Node RTIs", n->relids, es);
+			ExplainCloseGroup("Elided Node", NULL, true, es);
+		}
+		if (opened_elided_nodes)
+			ExplainCloseGroup("Elided Nodes", "Elided Nodes", false, es);
 	}
 }
 
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index d47c2915b3a..d977cbc92ad 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -590,6 +590,7 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions,
 	result->paramExecTypes = glob->paramExecTypes;
 	/* utilityStmt should be null, but we might as well copy it */
 	result->utilityStmt = parse->utilityStmt;
+	result->elidedNodes = glob->elidedNodes;
 	result->stmt_location = parse->stmt_location;
 	result->stmt_len = parse->stmt_len;
 
diff --git a/src/backend/optimizer/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.c
index eef43792aeb..5900458a0e1 100644
--- a/src/backend/optimizer/plan/setrefs.c
+++ b/src/backend/optimizer/plan/setrefs.c
@@ -211,6 +211,9 @@ static List *set_windowagg_runcondition_references(PlannerInfo *root,
 												   List *runcondition,
 												   Plan *plan);
 
+static void record_elided_node(PlannerGlobal *glob, int plan_node_id,
+							   NodeTag elided_type, Bitmapset *relids);
+
 
 /*****************************************************************************
  *
@@ -1441,10 +1444,17 @@ set_subqueryscan_references(PlannerInfo *root,
 
 	if (trivial_subqueryscan(plan))
 	{
+		Index		scanrelid;
+
 		/*
 		 * We can omit the SubqueryScan node and just pull up the subplan.
 		 */
 		result = clean_up_removed_plan_level((Plan *) plan, plan->subplan);
+
+		/* Remember that we removed a SubqueryScan */
+		scanrelid = plan->scan.scanrelid + rtoffset;
+		record_elided_node(root->glob, plan->subplan->plan_node_id,
+						   T_SubqueryScan, bms_make_singleton(scanrelid));
 	}
 	else
 	{
@@ -1872,7 +1882,17 @@ set_append_references(PlannerInfo *root,
 		Plan	   *p = (Plan *) linitial(aplan->appendplans);
 
 		if (p->parallel_aware == aplan->plan.parallel_aware)
-			return clean_up_removed_plan_level((Plan *) aplan, p);
+		{
+			Plan	   *result;
+
+			result = clean_up_removed_plan_level((Plan *) aplan, p);
+
+			/* Remember that we removed an Append */
+			record_elided_node(root->glob, p->plan_node_id, T_Append,
+							   offset_relid_set(aplan->apprelids, rtoffset));
+
+			return result;
+		}
 	}
 
 	/*
@@ -1940,7 +1960,17 @@ set_mergeappend_references(PlannerInfo *root,
 		Plan	   *p = (Plan *) linitial(mplan->mergeplans);
 
 		if (p->parallel_aware == mplan->plan.parallel_aware)
-			return clean_up_removed_plan_level((Plan *) mplan, p);
+		{
+			Plan	   *result;
+
+			result = clean_up_removed_plan_level((Plan *) mplan, p);
+
+			/* Remember that we removed a MergeAppend */
+			record_elided_node(root->glob, p->plan_node_id, T_MergeAppend,
+							   offset_relid_set(mplan->apprelids, rtoffset));
+
+			return result;
+		}
 	}
 
 	/*
@@ -3755,3 +3785,21 @@ extract_query_dependencies_walker(Node *node, PlannerInfo *context)
 	return expression_tree_walker(node, extract_query_dependencies_walker,
 								  context);
 }
+
+/*
+ * Record some details about a node removed from the plan during setrefs
+ * procesing, for the benefit of code trying to reconstruct planner decisions
+ * from examination of the final plan tree.
+ */
+static void
+record_elided_node(PlannerGlobal *glob, int plan_node_id,
+				   NodeTag elided_type, Bitmapset *relids)
+{
+	ElidedNode *n = makeNode(ElidedNode);
+
+	n->plan_node_id = plan_node_id;
+	n->elided_type = elided_type;
+	n->relids = relids;
+
+	glob->elidedNodes = lappend(glob->elidedNodes, n);
+}
diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h
index e14cbd383fc..3bff0763f7b 100644
--- a/src/include/nodes/pathnodes.h
+++ b/src/include/nodes/pathnodes.h
@@ -159,6 +159,9 @@ typedef struct PlannerGlobal
 	/* type OIDs for PARAM_EXEC Params */
 	List	   *paramExecTypes;
 
+	/* info about nodes elided from the plan during setrefs processing */
+	List	   *elidedNodes;
+
 	/* highest PlaceHolderVar ID assigned */
 	Index		lastPHId;
 
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index a54f42524b0..218f8f1ad6d 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -152,6 +152,9 @@ typedef struct PlannedStmt
 	/* non-null if this is utility stmt */
 	Node	   *utilityStmt;
 
+	/* info about nodes elided from the plan during setrefs processing */
+	List	   *elidedNodes;
+
 	/* statement location in source string (copied from Query) */
 	/* start location, or -1 if unknown */
 	ParseLoc	stmt_location;
@@ -1831,4 +1834,18 @@ typedef struct SubPlanRTInfo
 	bool		dummy;
 } SubPlanRTInfo;
 
+/*
+ * ElidedNode
+ *
+ * Information about nodes elided from the final plan tree: trivial subquery
+ * scans, and single-child Append and MergeAppend nodes.
+ */
+typedef struct ElidedNode
+{
+	NodeTag		type;
+	int			plan_node_id;
+	NodeTag		elided_type;
+	Bitmapset  *relids;
+} ElidedNode;
+
 #endif							/* PLANNODES_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 993688870db..b9f71bc5271 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -4344,3 +4344,4 @@ z_stream
 z_streamp
 zic_t
 SubPlanRTInfo
+ElidedNode
-- 
2.39.5 (Apple Git-154)

#59Tom Lane
tgl@sss.pgh.pa.us
In reply to: Robert Haas (#58)
2 attachment(s)
Re: plan shape work

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

Attachments:

v10-0001-Assign-each-subquery-a-unique-name-prior-to-plan.patchtext/x-diff; charset=us-ascii; name*0=v10-0001-Assign-each-subquery-a-unique-name-prior-to-plan.p; name*1=atchDownload
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, &paramIds);
 		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::integer END), 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 ((SubPlan expr_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::integer END, t1.v
+         Output: GROUPING((SubPlan expr_1)), ((SubPlan expr_3)), CASE WHEN (GROUPING((SubPlan expr_2)) = 0) THEN ((SubPlan expr_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::integer END), 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 ((SubPlan expr_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::integer END, t1.v
+         Output: GROUPING((SubPlan expr_1)), ((SubPlan expr_3)), CASE WHEN (GROUPING((SubPlan expr_2)) = 0) THEN ((SubPlan expr_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

v10-0002-Use-a-list-of-subplan-names-instead-of-a-list-of.patchtext/x-diff; charset=us-ascii; name*0=v10-0002-Use-a-list-of-subplan-names-instead-of-a-list-of.p; name*1=atchDownload
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

#60Robert Haas
robertmhaas@gmail.com
In reply to: Tom Lane (#59)
Re: plan shape work

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

#61Tom Lane
tgl@sss.pgh.pa.us
In reply to: Robert Haas (#60)
Re: plan shape work

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

#62Alexandra Wang
alexandra.wang.oss@gmail.com
In reply to: Tom Lane (#61)
Re: plan shape work

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

#63Robert Haas
robertmhaas@gmail.com
In reply to: Alexandra Wang (#62)
Re: plan shape work

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

#64Tom Lane
tgl@sss.pgh.pa.us
In reply to: Robert Haas (#63)
Re: plan shape work

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

#65Richard Guo
guofenglinux@gmail.com
In reply to: Tom Lane (#64)
Re: plan shape work

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

#66Tom Lane
tgl@sss.pgh.pa.us
In reply to: Richard Guo (#65)
Re: plan shape work

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

#67Richard Guo
guofenglinux@gmail.com
In reply to: Tom Lane (#66)
Re: plan shape work

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

#68Tom Lane
tgl@sss.pgh.pa.us
In reply to: Richard Guo (#67)
Re: plan shape work

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

#69Robert Haas
robertmhaas@gmail.com
In reply to: Richard Guo (#67)
Re: plan shape work

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

#70Richard Guo
guofenglinux@gmail.com
In reply to: Tom Lane (#68)
Re: plan shape work

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

#71Tom Lane
tgl@sss.pgh.pa.us
In reply to: Richard Guo (#70)
Re: plan shape work

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

#72Richard Guo
guofenglinux@gmail.com
In reply to: Tom Lane (#71)
1 attachment(s)
Re: plan shape work

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

Attachments:

PoC-choose_plan_name.patchapplication/octet-stream; name=PoC-choose_plan_name.patchDownload
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index 3b130e724f7..a112b4ac119 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -331,6 +331,7 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions,
 	glob->subplans = NIL;
 	glob->subpaths = NIL;
 	glob->subroots = NIL;
+	glob->subplanNames = NIL;
 	glob->rewindPlanIDs = NULL;
 	glob->finalrtable = NIL;
 	glob->allRelids = NULL;
@@ -8837,6 +8838,39 @@ create_partial_unique_paths(PlannerInfo *root, RelOptInfo *input_rel,
 	}
 }
 
+/*
+ * 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)
+{
+	size_t		base_len = strlen(name);
+	int			max_suffix = always_number ? 0 : -1;
+	char	   *canon_name;
+
+	foreach_ptr(char, subplan_name, glob->subplanNames)
+	{
+		char	   *last_underscore = strrchr(subplan_name, '_');
+
+		if ((last_underscore - subplan_name) == base_len &&
+			strncmp(subplan_name, name, base_len) == 0)
+		{
+			int			suffix_num = atoi(last_underscore + 1);
+
+			if (suffix_num > max_suffix)
+				max_suffix = suffix_num;
+		}
+	}
+
+	canon_name = psprintf("%s_%u", name, max_suffix + 1);
+	glob->subplanNames = lappend(glob->subplanNames, canon_name);
+
+	return (max_suffix == -1) ? pstrdup(name) : canon_name;
+}
+
+#if 0
 /*
  * Choose a unique name for some subroot.
  *
@@ -8903,3 +8937,4 @@ choose_plan_name(PlannerGlobal *glob, const char *name, bool always_number)
 		pfree(proposed_name);
 	}
 }
+#endif
#73Robert Haas
robertmhaas@gmail.com
In reply to: Richard Guo (#72)
Re: plan shape work

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

#74Tom Lane
tgl@sss.pgh.pa.us
In reply to: Robert Haas (#73)
Re: plan shape work

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

#75Richard Guo
guofenglinux@gmail.com
In reply to: Robert Haas (#73)
Re: plan shape work

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

#76Robert Haas
robertmhaas@gmail.com
In reply to: Richard Guo (#75)
Re: plan shape work

On Mon, Sep 29, 2025 at 9:59 PM Richard Guo <guofenglinux@gmail.com> wrote:

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 studied your patch in more detail today and I find that you are
correct. It doesn't do what I thought it did. However, it also doesn't
guarantee uniqueness. Here is an example:

robert.haas=# explain select (select random() limit 1), (select
random() limit 2) from (select * from pg_class limit 1) expr_1;
WARNING: choose_plan_name for name "expr" returns "expr_1"
WARNING: choose_plan_name for name "expr" returns "expr_2"
WARNING: choose_plan_name for name "expr_1" returns "expr_1"
QUERY PLAN
-------------------------------------------------------------------------
Subquery Scan on expr_1 (cost=0.03..0.08 rows=1 width=16)
InitPlan expr_1
-> Limit (cost=0.00..0.01 rows=1 width=8)
-> Result (cost=0.00..0.01 rows=1 width=8)
InitPlan expr_2
-> Limit (cost=0.00..0.01 rows=1 width=8)
-> Result (cost=0.00..0.01 rows=1 width=8)
-> Limit (cost=0.00..0.04 rows=1 width=240)
-> Seq Scan on pg_class (cost=0.00..18.16 rows=416 width=240)
(9 rows)

When I originally looked at the patch, I believed that the use of
atoi() without sanity checking was going to cause the sorts of
problems I was mentioning. That was wrong, since you never let a
user-specified name leak directly into the list, but only names to
which your code has added a numeric suffix. But that also means that
you don't get conflicts between names that, in reality, should
conflict.

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

#77Robert Haas
robertmhaas@gmail.com
In reply to: Robert Haas (#76)
1 attachment(s)
Re: plan shape work

I see that Richard's PoC last patch confused cfbot. Here's a new
version of just the patch proposed for commit for CfBot testing.

Attachments:

v11-0001-Assign-each-subquery-a-unique-name-prior-to-plan.patchapplication/octet-stream; name=v11-0001-Assign-each-subquery-a-unique-name-prior-to-plan.patchDownload
From 8de19d0f42c84392389250aa265b5ef45ca358b8 Mon Sep 17 00:00:00 2001
From: Robert Haas <rhaas@postgresql.org>
Date: Fri, 26 Sep 2025 10:45:18 -0400
Subject: [PATCH v11] Assign each subquery a unique name prior to planning it.

Previously, subqueries were given names only after they were planned,
which makes it difficult to use information from a previous execution of
the query to guide future planning. If, for example, you knew something
about how you want "InitPlan 2" to be planned, you won't know whether
the subquery you're currently planning will end up being "InitPlan 2"
until after you've finished planning it, by which point it's too late to
use the information that you had.

To fix this, assign each subplan a unique name before we begin planning
it. To improve consistency, use textual names for all subplans, rather
than, as we did previously, a mix of numbers (such as "InitPlan 1") and
names (such as "CTE foo"), and make sure that the same name is never
assigned more than once.

We adopt the somewhat arbitrary convention of using the type of sublink
to set the plan name; for example, a query that previously had two
expression sublinks shown as InitPlan 2 and InitPlan 1 will now end up
named expr_1 and expr_2. Because names are assigned before rather than
after planning, some of the regression test outputs show the numerical
part of the name switching positions: what was previously SubPlan 2 was
actually the first one encountered, but we finished planning it later.

We assign names even to subqueries that aren't shown as such within the
EXPLAIN output. These include subqueries that are a FROM clause item or
a branch of a set operation, rather than something that will be turned
into an InitPlan or SubPlan. The purpose of this is to make sure that,
below the topmost query level, there's always a name for each subquery
that is stable from one planning cycle to the next (assuming no changes
to the query or the database schema).

Author: Robert Haas <rhaas@postgresql.org>
Co-authored-by: Tom Lane <tgl@sss.pgh.pa.us>
Reviewed-by: Alexandra Wang <alexandra.wang.oss@gmail.com>
Reviewed-by: Richard Guo <guofenglinux@gmail.com>
Reviewed-by: Tom Lane <tgl@sss.pgh.pa.us>
Reviewed-by: Junwang Zhao <zhjwpku@gmail.com>
Discussion: http://postgr.es/m/3641043.1758751399@sss.pgh.pa.us
---
 .../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          |   3 +
 src/backend/optimizer/plan/planner.c          |  78 ++++-
 src/backend/optimizer/plan/subselect.c        |  87 +++--
 src/backend/optimizer/prep/prepjointree.c     |   1 +
 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, 810 insertions(+), 653 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 d7ff36d89be..1f82239b4e0 100644
--- a/src/backend/optimizer/path/allpaths.c
+++ b/src/backend/optimizer/path/allpaths.c
@@ -2529,6 +2529,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
@@ -2671,8 +2672,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..a2ac58d246e 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;
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index 41bd8353430..3b130e724f7 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);
@@ -630,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.
@@ -656,9 +658,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 +675,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;
@@ -8833,3 +8836,70 @@ create_partial_unique_paths(PlannerInfo *root, RelOptInfo *input_rel,
 								  sjinfo, unique_rel);
 	}
 }
+
+/*
+ * 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)
+{
+	unsigned	n;
+
+	/*
+	 * 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_ptr(char, subplan_name, glob->subplanNames)
+		{
+			if (strcmp(subplan_name, name) == 0)
+			{
+				found = true;
+				break;
+			}
+		}
+
+		if (!found)
+		{
+			/* pstrdup here is just to avoid cast-away-const */
+			char	   *chosen_name = pstrdup(name);
+
+			glob->subplanNames = lappend(glob->subplanNames, chosen_name);
+			return chosen_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_ptr(char, subplan_name, glob->subplanNames)
+		{
+			if (strcmp(subplan_name, proposed_name) == 0)
+			{
+				found = true;
+				break;
+			}
+		}
+
+		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 fae18548e07..14192a13236 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, &paramIds);
 		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,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, 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
@@ -989,10 +992,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 +1043,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 +3186,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 +3203,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..563be151a4d 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;
diff --git a/src/backend/optimizer/prep/prepunion.c b/src/backend/optimizer/prep/prepunion.c
index 547dbd53540..8c33425cd23 100644
--- a/src/backend/optimizer/prep/prepunion.c
+++ b/src/backend/optimizer/prep/prepunion.c
@@ -232,6 +232,7 @@ recurse_set_operations(Node *setOp, PlannerInfo *root,
 		PlannerInfo *subroot;
 		List	   *tlist;
 		bool		trivial_tlist;
+		char	   *plan_name;
 
 		Assert(subquery != NULL);
 
@@ -246,7 +247,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 21663af6979..050eef97a4c 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -8751,8 +8751,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;
@@ -9589,11 +9597,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;
@@ -9613,11 +9629,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..7ee9a7a68d8 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);
 
+	/* 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;
 
@@ -228,6 +231,9 @@ struct PlannerInfo
 	/* NULL at outermost Query */
 	PlannerInfo *parent_root pg_node_attr(read_write_ignore);
 
+	/* Subplan name for EXPLAIN and debugging purposes (NULL at top level) */
+	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 e9d8bf74145..1b4436f2ff6 100644
--- a/src/include/nodes/primnodes.h
+++ b/src/include/nodes/primnodes.h
@@ -1108,6 +1108,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 035f9a78206..bc83a6e188e 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_2
      ->  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_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 1).col1), ((InitPlan 2).col1)
+         Sort Key: ((InitPlan minmax_1).col1), ((InitPlan minmax_2).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::integer END), 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 ((SubPlan expr_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::integer END, t1.v
+         Output: GROUPING((SubPlan expr_1)), ((SubPlan expr_3)), CASE WHEN (GROUPING((SubPlan expr_2)) = 0) THEN ((SubPlan expr_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::integer END), 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 ((SubPlan expr_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::integer END, t1.v
+         Output: GROUPING((SubPlan expr_1)), ((SubPlan expr_3)), CASE WHEN (GROUPING((SubPlan expr_2)) = 0) THEN ((SubPlan expr_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..0490a746555 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_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))
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 a595fa28ce1..a2867f477f0 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.50.1 (Apple Git-155)

#78Richard Guo
guofenglinux@gmail.com
In reply to: Robert Haas (#77)
Re: plan shape work

On Tue, Oct 7, 2025 at 4:37 AM Robert Haas <robertmhaas@gmail.com> wrote:

I see that Richard's PoC last patch confused cfbot. Here's a new
version of just the patch proposed for commit for CfBot testing.

Does it make sense to explicitly initialize glob->subplanNames in
standard_planner()? I understand this might seem pointless since
makeNode() zeroes all fields by default, but subplanNames is currently
the only field in PlannerGlobal that isn't explicitly initialized. I
previously committed a patch (2c0ed86d3) to ensure all PlannerGlobal
fields are explicitly initialized, and I'd prefer to maintain that
consistency.

I actually suggested the same in [1]/messages/by-id/CAMbWs4-ysLvZiWp=w5=+noCMdX9FHFrrc0Wuk-TcUz1RDmEbkQ@mail.gmail.com (the last paragraph), but it
seems to have been overlooked.

[1]: /messages/by-id/CAMbWs4-ysLvZiWp=w5=+noCMdX9FHFrrc0Wuk-TcUz1RDmEbkQ@mail.gmail.com

- Richard

#79Tom Lane
tgl@sss.pgh.pa.us
In reply to: Richard Guo (#78)
Re: plan shape work

Richard Guo <guofenglinux@gmail.com> writes:

Does it make sense to explicitly initialize glob->subplanNames in
standard_planner()? I understand this might seem pointless since
makeNode() zeroes all fields by default, but subplanNames is currently
the only field in PlannerGlobal that isn't explicitly initialized. I
previously committed a patch (2c0ed86d3) to ensure all PlannerGlobal
fields are explicitly initialized, and I'd prefer to maintain that
consistency.

We don't really have consensus on that point, I fear. I like the
initialize-em-all-explicitly approach, but some other senior hackers
think it's useless verbiage.

My argument for doing it explicitly is that when adding a new field
to a struct, one frequently searches for existing references to a
nearby field. Without initialize-em-all, this risks missing places
where you need to initialize your new field. If you'd only set it
to zero, then fine ... but what if that particular place needs some
other initial value? So I think omitting initializations-to-zero
risks future bugs of omission. Some other folk don't find that
argument very compelling, though.

regards, tom lane

#80Robert Haas
robertmhaas@gmail.com
In reply to: Tom Lane (#79)
Re: plan shape work

On Wed, Oct 8, 2025 at 11:04 PM Tom Lane <tgl@sss.pgh.pa.us> wrote:

We don't really have consensus on that point, I fear. I like the
initialize-em-all-explicitly approach, but some other senior hackers
think it's useless verbiage.

I'm in the "useless verbiage" camp.

My argument for doing it explicitly is that when adding a new field
to a struct, one frequently searches for existing references to a
nearby field. Without initialize-em-all, this risks missing places
where you need to initialize your new field. If you'd only set it
to zero, then fine ... but what if that particular place needs some
other initial value? So I think omitting initializations-to-zero
risks future bugs of omission. Some other folk don't find that
argument very compelling, though.

This problem does not typically happen for me because it is my habit
to start by grepping for makeNode(Whatever) -- or for relevant palloc
calls, for non-Node types -- at the very start of my research into any
topic, and only later to look into specific fields.

There might be a consistency argument for doing this in this case, if
other nearby fields are doing the same thing, and if one of you wants
to go and make it so I have much better things to do than spend time
complaining about it. But any code I write is likely to rely on
makeNode() or palloc0() to zero fields wherever relying on such a
thing is convenient, unless somebody forces me to do otherwise in a
particular case.

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

#81Robert Haas
robertmhaas@gmail.com
In reply to: Tom Lane (#59)
Re: plan shape work

On Wed, Sep 24, 2025 at 6:03 PM Tom Lane <tgl@sss.pgh.pa.us> wrote:

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.

Today, I discovered a disadvantage of the change from allroots to
subplanNames. The concern that you raise about transient roots still
seems entirely valid to me. However, the allroots list - if correctly
constructed to exclude such transient roots - seems to me to have
potential utility for extensions that subplanNames doesn't. What I
wanted to do was use planner_shutdown_hook to copy some information
from each PlannerInfo to the extension_state field of the PlannedStmt
and, without allroots, there's no easy way to find all of them.

I do think there are other ways to solve that problem. For instance,
we could add a hook that's called when we invoke subquery_planner,
similar to the way that planner_hook can be used to intercept calls to
planner(). The new hook could then do whatever it likes at the end of
each call to subquery_planner(). That has the disadvantage of making
each call to subquery_planner() a little more expensive, but it might
turn out that such a hook has other utility.

For what I was trying to do today, I can probably even solve it using
set_join_pathlist_hook. I wanted to capture the relid sets of all
baserels and joinrels where rel->unique_rel is non-NULL, and I think
that I could do that by having set_join_pathlist_hook add notice when
save_jointype is JOIN_UNIQUE_INNER or JOIN_UNIQUE_OUTER, and then add
innerrel or outerrel as appropriate to a list hanging off an object
attached using Get/SetPlannerGlobalExtensionState, making sure not to
add the same one more than once. But that only works in this specific
case, and it's pretty indirect even for that.

So I'm wondering if we ought to step back and rethink a bit.
subplanNames ensures that we assign a different name to every
PlannerInfo, but it doesn't give you any help finding the PlannerInfo
given the name, or enumerating all of the PlannerInfo objects that
exist. If we went back to allroots, and just fixed the problem of
temporary entries creeping into the list, the core code wouldn't
really notice the difference, but I think extensions would have an
easier time.

Thoughts?

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

#82Robert Haas
robertmhaas@gmail.com
In reply to: Robert Haas (#81)
2 attachment(s)
Re: plan shape work

On Mon, Dec 1, 2025 at 3:31 PM Robert Haas <robertmhaas@gmail.com> wrote:

So I'm wondering if we ought to step back and rethink a bit.
subplanNames ensures that we assign a different name to every
PlannerInfo, but it doesn't give you any help finding the PlannerInfo
given the name, or enumerating all of the PlannerInfo objects that
exist. If we went back to allroots, and just fixed the problem of
temporary entries creeping into the list, the core code wouldn't
really notice the difference, but I think extensions would have an
easier time.

Thoughts?

Here's a couple of patches to bring back allroots. The first one adds
allroots, makes choose_plan_name() use it, and adds assertions that
the contents of allroots and subplanNames correspond. The second one
removes subplanNames. I imagine that these would be collapsed for
commit, but testing 0001 without 0002 seems useful for peace of mind.

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

Attachments:

v1-0002-Remove-glob-subplanNames.patchapplication/octet-stream; name=v1-0002-Remove-glob-subplanNames.patchDownload
From e72960487aa269f3d400a7b6149eb22c08a2ebef Mon Sep 17 00:00:00 2001
From: Robert Haas <rhaas@postgresql.org>
Date: Mon, 8 Dec 2025 13:55:55 -0500
Subject: [PATCH v1 2/2] Remove glob->subplanNames.

glob->allroots is sufficient; we don't need both.
---
 src/backend/optimizer/plan/planner.c | 18 ------------------
 src/include/nodes/pathnodes.h        |  3 ---
 2 files changed, 21 deletions(-)

diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index 47b32b20e1a..ccf1aad98c0 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -8963,26 +8963,12 @@ create_partial_unique_paths(PlannerInfo *root, RelOptInfo *input_rel,
 
 /*
  * 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)
 {
 	unsigned	n;
 
-#ifdef USE_ASSERT_CHECKING
-	Assert(list_length(glob->allroots) == list_length(glob->subplanNames) + 1);
-	foreach_ptr(char, subplan_name, glob->subplanNames)
-	{
-		PlannerInfo *subroot;
-
-		subroot = list_nth(glob->allroots,
-						   foreach_current_index(subplan_name) + 1);
-		Assert(strcmp(subroot->plan_name, subplan_name) == 0);
-	}
-#endif
-
 	/*
 	 * 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
@@ -9007,7 +8993,6 @@ choose_plan_name(PlannerGlobal *glob, const char *name, bool always_number)
 			/* pstrdup here is just to avoid cast-away-const */
 			char	   *chosen_name = pstrdup(name);
 
-			glob->subplanNames = lappend(glob->subplanNames, chosen_name);
 			return chosen_name;
 		}
 	}
@@ -9033,10 +9018,7 @@ 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/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h
index 2707ca50019..103186a2f97 100644
--- a/src/include/nodes/pathnodes.h
+++ b/src/include/nodes/pathnodes.h
@@ -113,9 +113,6 @@ typedef struct PlannerGlobal
 	/* every non-transient PlannerInfo (superset of subroots) */
 	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;
 
-- 
2.51.0

v1-0001-Add-a-list-of-all-PlannerInfo-roots-to-PlannerGlo.patchapplication/octet-stream; name=v1-0001-Add-a-list-of-all-PlannerInfo-roots-to-PlannerGlo.patchDownload
From 31f4e33142227bd31355aa2042ae0823937a36bd Mon Sep 17 00:00:00 2001
From: Robert Haas <rhaas@postgresql.org>
Date: Mon, 8 Dec 2025 13:46:43 -0500
Subject: [PATCH v1 1/2] Add a list of all PlannerInfo "roots" to
 PlannerGlobal.

This can serve the same purpose as glob->subplanNames, namely, to make
sure that we don't assign the same name to more than one subplan, but it
also can be used to locate a given subplan name, which is something that
glob->subplanNames cannot do.

This commit does not remove glob->subplanNames, but it ensures that
glob->allroots and glob->subplanNames have equivalent contents at the
key point in the code, namely, the start of choose_plan_name.
---
 src/backend/optimizer/plan/planagg.c      |  1 +
 src/backend/optimizer/plan/planner.c      | 50 +++++++++++++++++++++--
 src/backend/optimizer/prep/prepjointree.c |  7 ++++
 src/include/nodes/pathnodes.h             |  3 ++
 src/include/optimizer/planner.h           |  1 +
 5 files changed, 58 insertions(+), 4 deletions(-)

diff --git a/src/backend/optimizer/plan/planagg.c b/src/backend/optimizer/plan/planagg.c
index a2ac58d246e..f138f372e7d 100644
--- a/src/backend/optimizer/plan/planagg.c
+++ b/src/backend/optimizer/plan/planagg.c
@@ -341,6 +341,7 @@ build_minmax_path(PlannerInfo *root, MinMaxAggInfo *mminfo,
 	subroot->query_level++;
 	subroot->parent_root = root;
 	subroot->plan_name = choose_plan_name(root->glob, "minmax", true);
+	remember_plannerinfo(subroot);
 
 	/* reset subplan-related stuff */
 	subroot->plan_params = NIL;
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index fd77334e5fd..47b32b20e1a 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -745,6 +745,9 @@ subquery_planner(PlannerGlobal *glob, Query *parse, char *plan_name,
 		root->wt_param_id = -1;
 	root->non_recursive_path = NULL;
 
+	/* Add this PlanenrInfo to the PlannerGlobal's list */
+	remember_plannerinfo(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
@@ -8968,6 +8971,18 @@ choose_plan_name(PlannerGlobal *glob, const char *name, bool always_number)
 {
 	unsigned	n;
 
+#ifdef USE_ASSERT_CHECKING
+	Assert(list_length(glob->allroots) == list_length(glob->subplanNames) + 1);
+	foreach_ptr(char, subplan_name, glob->subplanNames)
+	{
+		PlannerInfo *subroot;
+
+		subroot = list_nth(glob->allroots,
+						   foreach_current_index(subplan_name) + 1);
+		Assert(strcmp(subroot->plan_name, subplan_name) == 0);
+	}
+#endif
+
 	/*
 	 * 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
@@ -8977,9 +8992,10 @@ choose_plan_name(PlannerGlobal *glob, const char *name, bool always_number)
 	{
 		bool		found = false;
 
-		foreach_ptr(char, subplan_name, glob->subplanNames)
+		foreach_ptr(PlannerInfo, subroot, glob->allroots)
 		{
-			if (strcmp(subplan_name, name) == 0)
+			if (subroot->plan_name != NULL &&
+				strcmp(subroot->plan_name, name) == 0)
 			{
 				found = true;
 				break;
@@ -9006,9 +9022,10 @@ choose_plan_name(PlannerGlobal *glob, const char *name, bool always_number)
 		char	   *proposed_name = psprintf("%s_%u", name, n);
 		bool		found = false;
 
-		foreach_ptr(char, subplan_name, glob->subplanNames)
+		foreach_ptr(PlannerInfo, subroot, glob->allroots)
 		{
-			if (strcmp(subplan_name, proposed_name) == 0)
+			if (subroot->plan_name != NULL &&
+				strcmp(subroot->plan_name, proposed_name) == 0)
 			{
 				found = true;
 				break;
@@ -9024,3 +9041,28 @@ choose_plan_name(PlannerGlobal *glob, const char *name, bool always_number)
 		pfree(proposed_name);
 	}
 }
+
+/*
+ * Add a new PlannerInfo to the PlannerGlobal's list of all subroots.
+ */
+void
+remember_plannerinfo(PlannerInfo *newroot)
+{
+	PlannerGlobal *glob = newroot->glob;
+
+#ifdef USE_ASSERT_CHECKING
+	/* Only the first PlannerInfo should be nameless. */
+	Assert(newroot->plan_name != NULL || glob->allroots == NIL);
+
+	/* PlannerInfo names should not be duplicated. */
+	foreach_node(PlannerInfo, root, glob->allroots)
+	{
+		if (root->plan_name == NULL)
+			continue;
+		Assert(strcmp(root->plan_name, newroot->plan_name) != 0);
+	}
+#endif
+
+	/* Add new PlannerInfo to list. */
+	glob->allroots = lappend(glob->allroots, newroot);
+}
diff --git a/src/backend/optimizer/prep/prepjointree.c b/src/backend/optimizer/prep/prepjointree.c
index 7581695647d..86e1abe8524 100644
--- a/src/backend/optimizer/prep/prepjointree.c
+++ b/src/backend/optimizer/prep/prepjointree.c
@@ -1350,6 +1350,13 @@ pull_up_simple_subquery(PlannerInfo *root, Node *jtnode, RangeTblEntry *rte,
 	/*
 	 * Create a PlannerInfo data structure for this subquery.
 	 *
+	 * Unlike subquery_planner, the subroot we create here is only transient.
+	 * It inherits properties such as the query_level, plan_name, and
+	 * parent_root from the supplied root, rather than becoming a new query
+	 * level. Whether we succeed or fail in pulling up the subquery, this
+	 * subroot won't survive long-term and shouldn't be linked into any
+	 * long-lived planner data structures.
+	 *
 	 * NOTE: the next few steps should match the first processing in
 	 * subquery_planner().  Can we refactor to avoid code duplication, or
 	 * would that just make things uglier?
diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h
index 46a8655621d..2707ca50019 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 non-transient PlannerInfo (superset of subroots) */
+	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);
 
diff --git a/src/include/optimizer/planner.h b/src/include/optimizer/planner.h
index 55d9b7940aa..76a7b7e9a2a 100644
--- a/src/include/optimizer/planner.h
+++ b/src/include/optimizer/planner.h
@@ -82,5 +82,6 @@ extern RelOptInfo *create_unique_paths(PlannerInfo *root, RelOptInfo *rel,
 
 extern char *choose_plan_name(PlannerGlobal *glob, const char *name,
 							  bool always_number);
+extern void remember_plannerinfo(PlannerInfo *newroot);
 
 #endif							/* PLANNER_H */
-- 
2.51.0