Richard Guo <[email protected]> writes:
> On Tue, Jul 21, 2026 at 3:52 AM Tom Lane <[email protected]> wrote:
>> I have a modest proposal to make instead: let's nuke all that logic
>> from orbit.  Revise analyzejoins.c so that it does join removals
>> working strictly on the jointree representation, which is simpler
>> and far more stable than any of the planner's derived data.  Then,
>> if we successfully removed any joins, throw away all the derived
>> data and loop back around within query_planner() to redo
>> deconstruct_jointree and all the rest of it.

> I prototyped this proposal to see how it would look in code, mainly
> copying how remove_useless_result_rtes does the removal from the join
> tree.  See attached PoC.  Please note that this is far from
> review-ready, and it only covers outer-join removal, skipping
> self-join elimination for now, and it lacks badly proper comments and
> test cases.

I spent some time hacking on this with the aid of Claude Code, and
arrived at what seems a complete patch.  It's a net deletion of
over 900 lines of code, and the newly-added code is mostly very
straightforward recursions over the jointree.

I had Claude do some performance testing, and the only case that got
noticeably slower was removal of multiple self-joins (about 10%
planning time slowdown for removal of 8 self-joins).  I'm not super
concerned about that; it doesn't seem like such cases would be common.

What I'm pretty unclear on is whether we want to risk back-patching
this.  It's a big change, and I can't honestly promise that it doesn't
bring some new bugs.  Still, it fixes one known bug and very likely
some not-yet-known ones.  Perhaps a reasonable choice would be to
back-patch to v18 where self-join elimination came in, because I still
have very little faith in that code.

                        regards, tom lane

From 3c3015f5c51d8501ac9f3bb79c4d83901519da04 Mon Sep 17 00:00:00 2001
From: Tom Lane <[email protected]>
Date: Sat, 25 Jul 2026 10:46:29 -0400
Subject: [PATCH v2 1/3] Postpone initialization of
 all_result_relids/leaf_result_relids.

Nothing actually pays attention to these PlannerInfo fields
before add_other_rels_to_query(), since they're not really
useful until that function has finished filling them in.
By delaying their setup until just before we call that function,
we can remove them from the set of things that join removal
has to worry about.  (It was already not doing anything with
them, but now we do not need an argument why that's okay.)

This is just some minor refactoring before the main patch
to deal with bug #19560.

Bug: #19560
Reported-by: Orestis Markou <[email protected]>
Author: Tom Lane <[email protected]>
Discussion: https://postgr.es/m/[email protected]
---
 src/backend/optimizer/plan/analyzejoins.c | 11 -----------
 src/backend/optimizer/plan/planmain.c     | 17 +++++++++++++++++
 src/backend/optimizer/plan/planner.c      | 18 ++----------------
 3 files changed, 19 insertions(+), 27 deletions(-)

diff --git a/src/backend/optimizer/plan/analyzejoins.c b/src/backend/optimizer/plan/analyzejoins.c
index 881950e5264..e1af49d3dea 100644
--- a/src/backend/optimizer/plan/analyzejoins.c
+++ b/src/backend/optimizer/plan/analyzejoins.c
@@ -2178,17 +2178,6 @@ remove_self_join_rel(PlannerInfo *root, PlanRowMark *kmark, PlanRowMark *rmark,
 	ChangeVarNodesExtended((Node *) root->processed_tlist, toRemove->relid,
 						   toKeep->relid, 0, replace_relid_callback);
 
-	/*
-	 * No need to touch all_result_relids or leaf_result_relids: at this point
-	 * those sets contain only parse->resultRelation; inheritance children
-	 * have not been added yet; that happens later in add_other_rels_to_query.
-	 * And remove_self_joins_recurse rejects parse->resultRelation as an SJE
-	 * candidate to preserve the EPQ mechanism.  So toRemove->relid cannot be
-	 * a member.
-	 */
-	Assert(!bms_is_member(toRemove->relid, root->all_result_relids));
-	Assert(!bms_is_member(toRemove->relid, root->leaf_result_relids));
-
 	/*
 	 * There may be references to the rel in root->fkey_list, but if so,
 	 * match_foreign_keys_to_quals() will get rid of them.
diff --git a/src/backend/optimizer/plan/planmain.c b/src/backend/optimizer/plan/planmain.c
index 02495e22e24..c123a397c1a 100644
--- a/src/backend/optimizer/plan/planmain.c
+++ b/src/backend/optimizer/plan/planmain.c
@@ -28,6 +28,7 @@
 #include "optimizer/paths.h"
 #include "optimizer/placeholder.h"
 #include "optimizer/planmain.h"
+#include "parser/parsetree.h"
 
 
 /*
@@ -274,6 +275,22 @@ query_planner(PlannerInfo *root,
 	 */
 	setup_eager_aggregation(root);
 
+	/*
+	 * If there's a result relation, initialize all_result_relids to include
+	 * it; and if we've verified that it is non-inheriting, mark it as a leaf
+	 * target.  add_other_rels_to_query() will expand these sets if the result
+	 * relation has children.
+	 */
+	if (parse->resultRelation)
+	{
+		RangeTblEntry *rte = rt_fetch(parse->resultRelation, parse->rtable);
+
+		root->all_result_relids = bms_make_singleton(parse->resultRelation);
+		if (!rte->inh)
+			root->leaf_result_relids =
+				bms_make_singleton(parse->resultRelation);
+	}
+
 	/*
 	 * Now expand appendrels by adding "otherrels" for their children.  We
 	 * delay this to the end so that we have as much information as possible
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index 3225185d16f..16ad18babaa 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -803,9 +803,8 @@ subquery_planner(PlannerGlobal *glob, Query *parse, char *plan_name,
 	root->eq_classes = NIL;
 	root->ec_merging_done = false;
 	root->last_rinfo_serial = 0;
-	root->all_result_relids =
-		parse->resultRelation ? bms_make_singleton(parse->resultRelation) : NULL;
-	root->leaf_result_relids = NULL;	/* we'll find out leaf-ness later */
+	root->all_result_relids = NULL;
+	root->leaf_result_relids = NULL;
 	root->append_rel_list = NIL;
 	root->row_identity_vars = NIL;
 	root->rowMarks = NIL;
@@ -976,19 +975,6 @@ subquery_planner(PlannerGlobal *glob, Query *parse, char *plan_name,
 											list_length(rte->securityQuals));
 	}
 
-	/*
-	 * If we have now verified that the query target relation is
-	 * non-inheriting, mark it as a leaf target.
-	 */
-	if (parse->resultRelation)
-	{
-		RangeTblEntry *rte = rt_fetch(parse->resultRelation, parse->rtable);
-
-		if (!rte->inh)
-			root->leaf_result_relids =
-				bms_make_singleton(parse->resultRelation);
-	}
-
 	/*
 	 * This would be a convenient time to check access permissions for all
 	 * relations mentioned in the query, since it would be better to fail now,
-- 
2.52.0

From e06e5d1fa86c66133036e5ea59e0483d5b51624f Mon Sep 17 00:00:00 2001
From: Tom Lane <[email protected]>
Date: Sat, 25 Jul 2026 11:34:55 -0400
Subject: [PATCH v2 2/3] Perform join removal by editing the query's jointree.

analyzejoins.c decided which joins could be dropped by consulting the
planner's derived data structures, but then implemented the removal
by updating those structures in-place.  That is a lot of fiddly work,
and nothing keeps it in step with the rest of the planner:
remove_leftjoinrel_from_query only bothered to update "parts of the
planner's data structures that will actually be consulted later", with
no good way to know what those are.  Bug #19560 is one consequence.
Removing a join there leaves an EquivalenceClass that now gives rise to
a base restriction clause, but base restriction clauses have already
been generated and nothing reconsiders them, so the WHERE condition
disappears from the plan and we return wrong answers.  The self-join
elimination code has the same design and the same type of hazard.

Instead, do the removals by editing root->parse->jointree, which is a
far simpler and more stable representation than the derived data, and
then have query_planner() discard everything it computed from the
jointree and derive it over again.

reduce_unique_semijoins() gets the same treatment: rather than deleting
the semijoin's SpecialJoinInfo and relying on the jointree not being
consulted again, it now changes the JoinExpr's jointype to JOIN_INNER.

Some plans change in the join regression test.  Qual evaluation order
shifts in a few cases, because the conditions now reach later planning
in jointree order rather than in whatever order the removal code
re-distributed them.  A few plans improve, since the rebuilt relation
targetlists no longer carry columns that only a removed join needed.
We also detect a constant-false filter condition whose test used to
carry a FIXME label.  One plan gets marginally worse, because the old
code recomputed attr_needed from equivalence classes after a join
removal; that is more accurate than what deconstruct_jointree()
derives from the original clauses, but we no longer do that.  Making
that recomputation happen anyway could be worth doing, but it should
be considered independently and perhaps implemented differently.

This leaves a good deal of dead code behind, which the next commit
removes; the separation is made to make each diff a little more
readable.  As of this patch there are -Wunused-function warnings
about several no-longer-used functions in analyzejoins.c.

Bug: #19560
Reported-by: Orestis Markou <[email protected]>
Author: Tom Lane <[email protected]>
Discussion: https://postgr.es/m/[email protected]
---
 src/backend/optimizer/plan/analyzejoins.c | 833 +++++++++++++++-------
 src/backend/optimizer/plan/initsplan.c    |   6 +-
 src/backend/optimizer/plan/planmain.c     |  67 +-
 src/include/optimizer/planmain.h          |   6 +-
 src/test/regress/expected/join.out        | 101 ++-
 src/test/regress/sql/join.sql             |  40 +-
 6 files changed, 755 insertions(+), 298 deletions(-)

diff --git a/src/backend/optimizer/plan/analyzejoins.c b/src/backend/optimizer/plan/analyzejoins.c
index e1af49d3dea..ef355ec1fe1 100644
--- a/src/backend/optimizer/plan/analyzejoins.c
+++ b/src/backend/optimizer/plan/analyzejoins.c
@@ -7,9 +7,14 @@
  * certain optimizations cannot be performed at that stage for lack of
  * detailed information about the query.  The routines here are invoked
  * after initsplan.c has done its work, and can do additional join removal
- * and simplification steps based on the information extracted.  The penalty
- * is that we have to work harder to clean up after ourselves when we modify
- * the query, since the derived data structures have to be updated too.
+ * and simplification steps based on the information extracted.
+ *
+ * Although the decisions about what can be removed are made using the
+ * planner's derived data structures, the removals themselves are implemented
+ * by editing the query's jointree, which is a far simpler and more stable
+ * representation.  We make no attempt to update the derived data structures
+ * to match; instead, query_planner() throws them all away and recomputes them
+ * whenever we report having removed something.
  *
  * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
  * Portions Copyright (c) 1994, Regents of the University of California
@@ -23,6 +28,7 @@
 #include "postgres.h"
 
 #include "catalog/pg_class.h"
+#include "nodes/makefuncs.h"
 #include "nodes/nodeFuncs.h"
 #include "optimizer/joininfo.h"
 #include "optimizer/optimizer.h"
@@ -30,6 +36,7 @@
 #include "optimizer/paths.h"
 #include "optimizer/placeholder.h"
 #include "optimizer/planmain.h"
+#include "optimizer/prep.h"
 #include "optimizer/restrictinfo.h"
 #include "parser/parse_agg.h"
 #include "rewrite/rewriteManip.h"
@@ -69,6 +76,11 @@ static void remove_rel_from_restrictinfo_phvs(RestrictInfo *rinfo,
 static Node *remove_rel_from_phvs(Node *node, int relid, int ojrelid);
 static Node *remove_rel_from_phvs_mutator(Node *node, Relids removable);
 static List *remove_rel_from_joinlist(List *joinlist, int relid, int *nremoved);
+static Node *remove_join_from_jointree(Node *jtnode, int ojrelid,
+									   int *nremoved);
+static void remove_rels_from_query_tree(PlannerInfo *root,
+										Relids removed_relids);
+static bool reduce_semijoin_in_jointree(Node *jtnode, Relids syn_righthand);
 static bool rel_supports_distinctness(PlannerInfo *root, RelOptInfo *rel);
 static bool rel_is_distinct_for(PlannerInfo *root, RelOptInfo *rel,
 								List *clause_list, List **extra_clauses);
@@ -80,6 +92,13 @@ static bool is_innerrel_unique_for(PlannerInfo *root,
 								   JoinType jointype,
 								   List *restrictlist,
 								   List **extra_clauses);
+static Node *remove_rel_from_jointree(Node *jtnode, int relid,
+									  Node **orphan_quals, int *nremoved);
+static Node *merge_quals(Node *quals1, Node *quals2);
+static void fixup_selfjoin_jointree(PlannerInfo *root, Node *jtnode,
+									int relid);
+static List *fixup_selfjoin_quals(PlannerInfo *root, List *quals, int relid);
+static Node *replace_selfjoin_qual(Node *qual);
 static int	self_join_candidates_cmp(const void *a, const void *b);
 static bool replace_relid_callback(Node *node,
 								   ChangeVarNodes_context *context);
@@ -88,21 +107,22 @@ static bool replace_relid_callback(Node *node,
 /*
  * remove_useless_joins
  *		Check for relations that don't actually need to be joined at all,
- *		and remove them from the query.
+ *		and remove them from the query's jointree.
  *
- * We are passed the current joinlist and return the updated list.  Other
- * data structures that have to be updated are accessible via "root".
+ * Returns true if we removed anything.  In that case the caller must discard
+ * everything it has derived from the jointree and compute it over again,
+ * since we don't try to update any of that here.
  */
-List *
-remove_useless_joins(PlannerInfo *root, List *joinlist)
+bool
+remove_useless_joins(PlannerInfo *root)
 {
+	Relids		removed_relids = NULL;
 	ListCell   *lc;
 
 	/*
 	 * We are only interested in relations that are left-joined to, so we can
 	 * scan the join_info_list to find them easily.
 	 */
-restart:
 	foreach(lc, root->join_info_list)
 	{
 		SpecialJoinInfo *sjinfo = (SpecialJoinInfo *) lfirst(lc);
@@ -114,37 +134,59 @@ restart:
 			continue;
 
 		/*
-		 * Currently, join_is_removable can only succeed when the sjinfo's
-		 * righthand is a single baserel.  Remove that rel from the query and
-		 * joinlist.
+		 * join_is_removable insists that the join's syntactic righthand side
+		 * be a single baserel, so we can implement the removal by dropping
+		 * the JoinExpr and everything below its righthand side.
 		 */
-		innerrelid = bms_singleton_member(sjinfo->min_righthand);
+		innerrelid = bms_singleton_member(sjinfo->syn_righthand);
 
-		remove_leftjoinrel_from_query(root, innerrelid, sjinfo);
+		/*
+		 * The removed rel had better not be one that the rest of the planner
+		 * expects to find.  It can't be, really: a result relation or a
+		 * row-marked relation has junk columns in the query targetlist, and
+		 * join_is_removable would have seen those as uses of the rel from
+		 * "relation 0".  But let's check.
+		 */
+		Assert(innerrelid != root->parse->resultRelation);
+#ifdef USE_ASSERT_CHECKING
+		foreach_node(PlanRowMark, rc, root->rowMarks)
+			Assert(rc->rti != innerrelid && rc->prti != innerrelid);
+#endif
 
-		/* We verify that exactly one reference gets removed from joinlist */
+		/* We verify that exactly one JoinExpr gets removed */
 		nremoved = 0;
-		joinlist = remove_rel_from_joinlist(joinlist, innerrelid, &nremoved);
+		root->parse->jointree = (FromExpr *)
+			remove_join_from_jointree((Node *) root->parse->jointree,
+									  sjinfo->ojrelid, &nremoved);
 		if (nremoved != 1)
-			elog(ERROR, "failed to find relation %d in joinlist", innerrelid);
+			elog(ERROR, "failed to find join %d in jointree", sjinfo->ojrelid);
 
-		/*
-		 * We can delete this SpecialJoinInfo from the list too, since it's no
-		 * longer of interest.  (Since we'll restart the foreach loop
-		 * immediately, we don't bother with foreach_delete_current.)
-		 */
-		root->join_info_list = list_delete_cell(root->join_info_list, lc);
+		/* Track all the relids we've removed, for use below */
+		removed_relids = bms_add_member(removed_relids, innerrelid);
+		removed_relids = bms_add_member(removed_relids, sjinfo->ojrelid);
 
 		/*
-		 * Restart the scan.  This is necessary to ensure we find all
-		 * removable joins independently of ordering of the join_info_list
-		 * (note that removal of attr_needed bits may make a join appear
-		 * removable that did not before).
+		 * It's okay to keep scanning join_info_list for more removable joins,
+		 * even though the data that join_is_removable consults is now
+		 * slightly out of date.  Removing a join can only delete attr_needed
+		 * bits and join clauses, and any attr_needed bit or join clause that
+		 * mentions the removed rel above its own join level would have
+		 * prevented that rel from being removable.  So what remains to be
+		 * examined is unchanged by what we just did.
+		 *
+		 * The converse doesn't hold: dropping a join can make some other join
+		 * removable that didn't look so before.  That's why our caller loops
+		 * until we report finding nothing more to remove.
 		 */
-		goto restart;
 	}
 
-	return joinlist;
+	if (bms_is_empty(removed_relids))
+		return false;
+
+	/* Clean up the traces that the removed rels have left elsewhere */
+	remove_rels_from_query_tree(root, removed_relids);
+
+	return true;
 }
 
 /*
@@ -176,8 +218,15 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 	if (sjinfo->jointype != JOIN_LEFT)
 		return false;
 
-	if (!bms_get_singleton_member(sjinfo->min_righthand, &innerrelid))
+	/*
+	 * We test the syntactic righthand side, not min_righthand, because the
+	 * removal is done by deleting the whole righthand subtree of the join.
+	 * (min_righthand can be a singleton when syn_righthand is not, but in
+	 * such a case the attr_needed tests below would reject the join anyway.)
+	 */
+	if (!bms_get_singleton_member(sjinfo->syn_righthand, &innerrelid))
 		return false;
+	Assert(bms_equal(sjinfo->min_righthand, sjinfo->syn_righthand));
 
 	/*
 	 * Never try to eliminate a left join to the query result rel.  Although
@@ -1025,6 +1074,91 @@ remove_rel_from_joinlist(List *joinlist, int relid, int *nremoved)
 }
 
 
+/*
+ * remove_join_from_jointree
+ *		Delete the JoinExpr with the given RT index, along with everything
+ *		below its righthand side, from the query's jointree.
+ *
+ * The JoinExpr is replaced by its lefthand input.  Its ON conditions can just
+ * be dropped: since this is a left join, they could only have determined
+ * which righthand rows join to a given lefthand row, and there are no
+ * righthand rows anymore.
+ *
+ * *nremoved is incremented by the number of JoinExprs removed (there should
+ * be exactly one, but the caller checks that).
+ */
+static Node *
+remove_join_from_jointree(Node *jtnode, int ojrelid, int *nremoved)
+{
+	if (jtnode == NULL)
+		return NULL;
+	if (IsA(jtnode, RangeTblRef))
+	{
+		/* nothing to do here */
+	}
+	else if (IsA(jtnode, FromExpr))
+	{
+		FromExpr   *f = (FromExpr *) jtnode;
+		ListCell   *l;
+
+		foreach(l, f->fromlist)
+			lfirst(l) = remove_join_from_jointree((Node *) lfirst(l),
+												  ojrelid, nremoved);
+	}
+	else if (IsA(jtnode, JoinExpr))
+	{
+		JoinExpr   *j = (JoinExpr *) jtnode;
+
+		if (j->rtindex == ojrelid)
+		{
+			(*nremoved)++;
+			return j->larg;
+		}
+		j->larg = remove_join_from_jointree(j->larg, ojrelid, nremoved);
+		j->rarg = remove_join_from_jointree(j->rarg, ojrelid, nremoved);
+	}
+	else
+		elog(ERROR, "unrecognized jointree node type: %d",
+			 (int) nodeTag(jtnode));
+
+	return jtnode;
+}
+
+/*
+ * remove_rels_from_query_tree
+ *		Delete all remaining references to the given relids from the query.
+ *
+ * Having removed some relations and outer joins from the jointree, we must
+ * get rid of any references to them that are left behind elsewhere.  There
+ * should be no ordinary Vars of a removed relation left, but the relids can
+ * still appear in the nullingrels sets of surviving Vars and PlaceHolderVars,
+ * and in the phrels sets of PlaceHolderVars.  ChangeVarNodes knows how to
+ * strip a relid out of all of those.
+ */
+static void
+remove_rels_from_query_tree(PlannerInfo *root, Relids removed_relids)
+{
+	int			relid = -1;
+
+	while ((relid = bms_next_member(removed_relids, relid)) >= 0)
+	{
+		/* Pass -1 for new_index to get the removal behavior */
+		ChangeVarNodes((Node *) root->parse, relid, -1, 0);
+
+		/*
+		 * processed_tlist shares some but not all of its nodes with
+		 * parse->targetList, so it has to be processed separately.  (That's
+		 * harmless: ChangeVarNodes works in-place, and removing a relid that
+		 * isn't there is idempotent.)
+		 */
+		ChangeVarNodes((Node *) root->processed_tlist, relid, -1, 0);
+
+		/* There could be references in the append_rel_list, too */
+		if (root->append_rel_list != NIL)
+			ChangeVarNodes((Node *) root->append_rel_list, relid, -1, 0);
+	}
+}
+
 /*
  * reduce_unique_semijoins
  *		Check for semijoins that can be simplified to plain inner joins
@@ -1033,14 +1167,13 @@ remove_rel_from_joinlist(List *joinlist, int relid, int *nremoved)
  * Ideally this would happen during reduce_outer_joins, but we don't have
  * enough information at that point.
  *
- * To perform the strength reduction when applicable, we need only delete
- * the semijoin's SpecialJoinInfo from root->join_info_list.  (We don't
- * bother fixing the join type attributed to it in the query jointree,
- * since that won't be consulted again.)
+ * Like the join removal cases, we do this on the query's jointree, so
+ * returning true means the caller must recompute the derived data.
  */
-void
+bool
 reduce_unique_semijoins(PlannerInfo *root)
 {
+	bool		changed = false;
 	ListCell   *lc;
 
 	/*
@@ -1097,9 +1230,65 @@ reduce_unique_semijoins(PlannerInfo *root)
 								JOIN_SEMI, restrictlist, true))
 			continue;
 
-		/* OK, remove the SpecialJoinInfo from the list. */
-		root->join_info_list = foreach_delete_current(root->join_info_list, lc);
+		/* OK, reduce the join to a plain inner join in the jointree. */
+		if (!reduce_semijoin_in_jointree((Node *) root->parse->jointree,
+										 sjinfo->syn_righthand))
+			elog(ERROR, "failed to find semijoin in jointree");
+		changed = true;
+	}
+
+	return changed;
+}
+
+/*
+ * reduce_semijoin_in_jointree
+ *		Find the JoinExpr for the semijoin with the given syntactic righthand
+ *		side, and turn it into an inner join.
+ *
+ * Semijoins have no RT index of their own, so we have to identify the one
+ * we want by the set of relids on its righthand side.
+ */
+static bool
+reduce_semijoin_in_jointree(Node *jtnode, Relids syn_righthand)
+{
+	if (jtnode == NULL)
+		return false;
+	if (IsA(jtnode, RangeTblRef))
+	{
+		/* nothing to do here */
+	}
+	else if (IsA(jtnode, FromExpr))
+	{
+		FromExpr   *f = (FromExpr *) jtnode;
+		ListCell   *l;
+
+		foreach(l, f->fromlist)
+		{
+			if (reduce_semijoin_in_jointree((Node *) lfirst(l), syn_righthand))
+				return true;
+		}
 	}
+	else if (IsA(jtnode, JoinExpr))
+	{
+		JoinExpr   *j = (JoinExpr *) jtnode;
+
+		if (j->jointype == JOIN_SEMI &&
+			bms_equal(get_relids_in_jointree(j->rarg, true, false),
+					  syn_righthand))
+		{
+			j->jointype = JOIN_INNER;
+			return true;
+		}
+		if (reduce_semijoin_in_jointree(j->larg, syn_righthand))
+			return true;
+		if (reduce_semijoin_in_jointree(j->rarg, syn_righthand))
+			return true;
+	}
+	else
+		elog(ERROR, "unrecognized jointree node type: %d",
+			 (int) nodeTag(jtnode));
+
+	return false;
 }
 
 
@@ -2014,130 +2203,64 @@ replace_relid_callback(Node *node, ChangeVarNodes_context *context)
 }
 
 /*
- * Remove a relation after we have proven that it participates only in an
- * unneeded unique self-join.
- *
- * Replace any links in planner info structures.
- *
- * Transfer join and restriction clauses from the removed relation to the
- * remaining one. We change the Vars of the clause to point to the
- * remaining relation instead of the removed one. The clauses that require
- * a subset of joinrelids become restriction clauses of the remaining
- * relation, and others remain join clauses. We append them to
- * baserestrictinfo and joininfo, respectively, trying not to introduce
- * duplicates.
- *
- * We also have to process the 'joinclauses' list here, because it
- * contains EC-derived join clauses which must become filter clauses. It
- * is not enough to just correct the ECs because the EC-derived
- * restrictions are generated before join removal (see
- * generate_base_implied_equalities).
- *
- * NOTE: Remember to keep the code in sync with PlannerInfo to be sure all
- * cached relids and relid bitmapsets can be correctly cleaned during the
- * self-join elimination procedure.
+ * Remove the toRemove relation after we have proven that it participates only
+ * in an unneeded unique self-join with toKeep.
+ *
+ * The removal is done by deleting the relation's RangeTblRef from the
+ * jointree and then pointing everything that referenced it at the relation we
+ * are keeping.  All the conditions that were attached to the removed relation
+ * thereby become conditions on the remaining one, which is what we want:
+ * we've proven that the two relations select the same rows.
+ *
+ * kmark and rmark are the PlanRowMarks (if any) for the kept and removed
+ * relations.  We could re-locate those, but the caller already found them.
  */
 static void
-remove_self_join_rel(PlannerInfo *root, PlanRowMark *kmark, PlanRowMark *rmark,
+remove_self_join_rel(PlannerInfo *root,
 					 RelOptInfo *toKeep, RelOptInfo *toRemove,
-					 List *restrictlist)
+					 PlanRowMark *kmark, PlanRowMark *rmark)
 {
-	List	   *joininfos;
-	ListCell   *lc;
-	int			i;
-	List	   *jinfo_candidates = NIL;
-	List	   *binfo_candidates = NIL;
+	Node	   *orphan_quals = NULL;
+	int			nremoved = 0;
 
 	Assert(toKeep->relid > 0);
 	Assert(toRemove->relid > 0);
 
-	/*
-	 * Replace the index of the removing table with the keeping one. The
-	 * technique of removing/distributing restrictinfo is used here to attach
-	 * just appeared (for keeping relation) join clauses and avoid adding
-	 * duplicates of those that already exist in the joininfo list.
-	 */
-	joininfos = list_copy(toRemove->joininfo);
-	foreach_node(RestrictInfo, rinfo, joininfos)
-	{
-		remove_join_clause_from_rels(root, rinfo, rinfo->required_relids);
-		ChangeVarNodesExtended((Node *) rinfo, toRemove->relid, toKeep->relid,
-							   0, replace_relid_callback);
-
-		if (bms_membership(rinfo->required_relids) == BMS_MULTIPLE)
-			jinfo_candidates = lappend(jinfo_candidates, rinfo);
-		else
-			binfo_candidates = lappend(binfo_candidates, rinfo);
-	}
+	/* We verify that exactly one reference gets removed from the jointree */
+	root->parse->jointree = (FromExpr *)
+		remove_rel_from_jointree((Node *) root->parse->jointree,
+								 toRemove->relid,
+								 &orphan_quals, &nremoved);
+	if (nremoved != 1)
+		elog(ERROR, "failed to find relation %d in jointree", toRemove->relid);
+	/* The topmost FromExpr can't have gone away, so nothing can be orphaned */
+	Assert(root->parse->jointree != NULL);
+	Assert(orphan_quals == NULL);
 
 	/*
-	 * Concatenate restrictlist to the list of base restrictions of the
-	 * removing table just to simplify the replacement procedure: all of them
-	 * weren't connected to any keeping relations and need to be added to some
-	 * rels.
+	 * Replace all references to the removed relation.  Note that this must
+	 * happen after the jointree surgery, else we'd not be able to tell the
+	 * two relations' RangeTblRefs apart.
 	 */
-	toRemove->baserestrictinfo = list_concat(toRemove->baserestrictinfo,
-											 restrictlist);
-	foreach_node(RestrictInfo, rinfo, toRemove->baserestrictinfo)
-	{
-		ChangeVarNodesExtended((Node *) rinfo, toRemove->relid, toKeep->relid,
-							   0, replace_relid_callback);
-
-		if (bms_membership(rinfo->required_relids) == BMS_MULTIPLE)
-			jinfo_candidates = lappend(jinfo_candidates, rinfo);
-		else
-			binfo_candidates = lappend(binfo_candidates, rinfo);
-	}
-
-	/*
-	 * Now, add all non-redundant clauses to the keeping relation.
-	 */
-	add_non_redundant_clauses(root, binfo_candidates,
-							  &toKeep->baserestrictinfo, toRemove->relid);
-	add_non_redundant_clauses(root, jinfo_candidates,
-							  &toKeep->joininfo, toRemove->relid);
-
-	list_free(binfo_candidates);
-	list_free(jinfo_candidates);
-
-	/*
-	 * Arrange equivalence classes, mentioned removing a table, with the
-	 * keeping one: varno of removing table should be replaced in members and
-	 * sources lists. Also, remove duplicated elements if this replacement
-	 * procedure created them.
-	 */
-	i = -1;
-	while ((i = bms_next_member(toRemove->eclass_indexes, i)) >= 0)
-	{
-		EquivalenceClass *ec = (EquivalenceClass *) list_nth(root->eq_classes, i);
-
-		update_eclasses(ec, toRemove->relid, toKeep->relid);
-		toKeep->eclass_indexes = bms_add_member(toKeep->eclass_indexes, i);
-	}
+	ChangeVarNodes((Node *) root->parse, toRemove->relid, toKeep->relid, 0);
 
 	/*
-	 * Transfer the targetlist and attr_needed flags.
+	 * processed_tlist shares some but not all of its nodes with
+	 * parse->targetList, so it has to be processed separately.  (That's
+	 * harmless: ChangeVarNodes works in-place, and the second visit to a
+	 * shared node finds nothing to change.)
 	 */
+	ChangeVarNodes((Node *) root->processed_tlist, toRemove->relid,
+				   toKeep->relid, 0);
 
-	foreach(lc, toRemove->reltarget->exprs)
-	{
-		Node	   *node = lfirst(lc);
-
-		ChangeVarNodesExtended(node, toRemove->relid, toKeep->relid, 0,
-							   replace_relid_callback);
-		if (!list_member(toKeep->reltarget->exprs, node))
-			toKeep->reltarget->exprs = lappend(toKeep->reltarget->exprs, node);
-	}
+	/* There could be references in the append_rel_list, too */
+	if (root->append_rel_list != NIL)
+		ChangeVarNodes((Node *) root->append_rel_list, toRemove->relid,
+					   toKeep->relid, 0);
 
-	for (i = toKeep->min_attr; i <= toKeep->max_attr; i++)
-	{
-		int			attno = i - toKeep->min_attr;
-
-		toRemove->attr_needed[attno] = adjust_relid_set(toRemove->attr_needed[attno],
-														toRemove->relid, toKeep->relid);
-		toKeep->attr_needed[attno] = bms_add_members(toKeep->attr_needed[attno],
-													 toRemove->attr_needed[attno]);
-	}
+	/* Clean up the quals that the substitution has messed with */
+	fixup_selfjoin_jointree(root, (Node *) root->parse->jointree,
+							toKeep->relid);
 
 	/*
 	 * If the removed relation has a row mark, transfer it to the remaining
@@ -2157,52 +2280,270 @@ remove_self_join_rel(PlannerInfo *root, PlanRowMark *kmark, PlanRowMark *rmark,
 		}
 		else
 		{
-			/* Shouldn't have inheritance children here. */
+			/* Shouldn't have inheritance children yet. */
 			Assert(rmark->rti == rmark->prti);
 
 			rmark->rti = rmark->prti = toKeep->relid;
 		}
 	}
+}
 
-	/*
-	 * Replace varno in all the query structures, except nodes RangeTblRef
-	 * otherwise later remove_rel_from_joinlist will yield errors.
-	 */
-	ChangeVarNodesExtended((Node *) root->parse, toRemove->relid, toKeep->relid,
-						   0, replace_relid_callback);
+/*
+ * remove_rel_from_jointree
+ *		Delete the RangeTblRef for the given relation from the query's
+ *		jointree.
+ *
+ * This is used for self-join elimination, where the removed relation's
+ * qual conditions must all be preserved (they will be transposed onto the
+ * remaining relation afterwards).  Hence, if dropping the RangeTblRef leaves
+ * a JoinExpr or FromExpr with nothing under it, we can't simply drop that
+ * node; we hand its quals back to the caller in *orphan_quals, to be merged
+ * into the nearest enclosing node that still has some content.  That's a
+ * valid transformation only for inner joins, but a jointree node can't become
+ * empty at an outer join here: remove_self_joins_one_group() insists that the
+ * two relations be on the same side of every outer join, so the relation we
+ * are keeping would have to be in the emptied subtree too.
+ *
+ * *nremoved is incremented by the number of RangeTblRefs removed (there
+ * should be exactly one, but the caller checks that).
+ */
+static Node *
+remove_rel_from_jointree(Node *jtnode, int relid,
+						 Node **orphan_quals, int *nremoved)
+{
+	if (jtnode == NULL)
+		return NULL;
+	if (IsA(jtnode, RangeTblRef))
+	{
+		RangeTblRef *rtr = (RangeTblRef *) jtnode;
 
-	/* Replace links in the planner info */
-	remove_rel_from_query(root, toRemove->relid, toKeep->relid, NULL, NULL);
+		if (rtr->rtindex == relid)
+		{
+			(*nremoved)++;
+			return NULL;
+		}
+	}
+	else if (IsA(jtnode, FromExpr))
+	{
+		FromExpr   *f = (FromExpr *) jtnode;
+		List	   *newfromlist = NIL;
+		Node	   *sub_orphans = NULL;
+		ListCell   *l;
 
-	/* Replace varno in the fully-processed targetlist */
-	ChangeVarNodesExtended((Node *) root->processed_tlist, toRemove->relid,
-						   toKeep->relid, 0, replace_relid_callback);
+		foreach(l, f->fromlist)
+		{
+			Node	   *newchild;
 
-	/*
-	 * There may be references to the rel in root->fkey_list, but if so,
-	 * match_foreign_keys_to_quals() will get rid of them.
-	 */
+			newchild = remove_rel_from_jointree((Node *) lfirst(l), relid,
+												&sub_orphans, nremoved);
+			if (newchild != NULL)
+				newfromlist = lappend(newfromlist, newchild);
+		}
+		f->fromlist = newfromlist;
+		f->quals = merge_quals(sub_orphans, f->quals);
+		if (newfromlist == NIL)
+		{
+			/* Nothing left here, so pass our quals up to the parent */
+			*orphan_quals = merge_quals(f->quals, *orphan_quals);
+			return NULL;
+		}
+	}
+	else if (IsA(jtnode, JoinExpr))
+	{
+		JoinExpr   *j = (JoinExpr *) jtnode;
+		Node	   *sub_orphans = NULL;
+
+		j->larg = remove_rel_from_jointree(j->larg, relid,
+										   &sub_orphans, nremoved);
+		j->rarg = remove_rel_from_jointree(j->rarg, relid,
+										   &sub_orphans, nremoved);
+		if (j->larg == NULL || j->rarg == NULL)
+		{
+			Node	   *surviving = (j->larg != NULL) ? j->larg : j->rarg;
+			Node	   *quals = merge_quals(sub_orphans, j->quals);
 
-	/*
-	 * Finally, remove the rel from the baserel array to prevent it from being
-	 * referenced again.  (We can't do this earlier because
-	 * remove_join_clause_from_rels will touch it.)
-	 */
-	root->simple_rel_array[toRemove->relid] = NULL;
-	root->simple_rte_array[toRemove->relid] = NULL;
+			/* As explained above, this can only happen for an inner join */
+			Assert(j->jointype == JOIN_INNER);
+			/* We can't have removed both children */
+			Assert(surviving != NULL);
+
+			/*
+			 * Replace the join by a FromExpr, so that the surviving side's
+			 * rows are still filtered by the join's conditions.
+			 */
+			return (Node *) makeFromExpr(list_make1(surviving), quals);
+		}
+		/* A subtree that survives never hands any quals back to us */
+		Assert(sub_orphans == NULL);
+	}
+	else
+		elog(ERROR, "unrecognized jointree node type: %d",
+			 (int) nodeTag(jtnode));
+
+	return jtnode;
+}
+
+/*
+ * merge_quals
+ *		Combine two jointree qual conditions.
+ *
+ * quals1 should be the quals from the lower of the two jointree levels,
+ * so that those quals get applied first.
+ *
+ * Jointree quals have been through preprocess_expression() by now, so each
+ * one is either NULL or an implicitly-ANDed List.
+ */
+static Node *
+merge_quals(Node *quals1, Node *quals2)
+{
+	if (quals1 == NULL)
+		return quals2;
+	if (quals2 == NULL)
+		return quals1;
+	return (Node *) list_concat(castNode(List, quals1),
+								castNode(List, quals2));
+}
+
+/*
+ * fixup_selfjoin_jointree
+ *		Clean up the query's jointree quals after self-join elimination has
+ *		merged one relation into another.  (relid is the kept relation.)
+ *
+ * See fixup_selfjoin_quals() for what needs fixing.
+ */
+static void
+fixup_selfjoin_jointree(PlannerInfo *root, Node *jtnode, int relid)
+{
+	if (jtnode == NULL)
+		return;
+	if (IsA(jtnode, RangeTblRef))
+	{
+		/* nothing to do here */
+	}
+	else if (IsA(jtnode, FromExpr))
+	{
+		FromExpr   *f = (FromExpr *) jtnode;
+		ListCell   *l;
+
+		foreach(l, f->fromlist)
+			fixup_selfjoin_jointree(root, (Node *) lfirst(l), relid);
+		f->quals = (Node *) fixup_selfjoin_quals(root,
+												 castNode(List, f->quals),
+												 relid);
+	}
+	else if (IsA(jtnode, JoinExpr))
+	{
+		JoinExpr   *j = (JoinExpr *) jtnode;
+
+		fixup_selfjoin_jointree(root, j->larg, relid);
+		fixup_selfjoin_jointree(root, j->rarg, relid);
+		j->quals = (Node *) fixup_selfjoin_quals(root,
+												 castNode(List, j->quals),
+												 relid);
+	}
+	else
+		elog(ERROR, "unrecognized jointree node type: %d",
+			 (int) nodeTag(jtnode));
+}
+
+/*
+ * fixup_selfjoin_quals
+ *		Clean up one qual list after self-join elimination.
+ *
+ * Two things need fixing here.  First, a join clause such as "t1.a = t2.a"
+ * has turned into "t1.a = t1.a".  For a strict mergejoinable operator that
+ * means "t1.a IS NOT NULL", and we should make the substitution, for two
+ * reasons:
+ * 1. It will typically result in better selectivity estimates.
+ * 2. EquivalenceClass processing is likely to make the substitution
+ *    if we don't.  While not directly harmful, we'd then fail to
+ *    recognize it as a duplicate of a user-written "t1.a IS NOT NULL"
+ *    clause, again leading to bad selectivity estimates.
+ * Second, conditions that were written against the two relations separately
+ * may now be identical, and we don't want to apply the same condition twice
+ * (much less double-count its selectivity).
+ *
+ * We only touch the top-level conjuncts of the list.  There, turning a NULL
+ * result into FALSE makes no difference, whereas below a NOT it would,
+ * invalidating the IS NOT NULL substitution.  EquivalenceClass processing
+ * will not be applied to sub-clauses, and cleaning up duplicates in them
+ * seems like more trouble than it's worth.  Also, we only consider clauses
+ * that mention the relation we merged into, so that we don't change the
+ * treatment of anything we didn't touch.
+ *
+ * Since this is not a correctness issue but just an optimization opportunity,
+ * we likewise don't worry about recognizing duplicates that appear in
+ * different qual lists.
+ */
+static List *
+fixup_selfjoin_quals(PlannerInfo *root, List *quals, int relid)
+{
+	List	   *result = NIL;
+	ListCell   *l;
 
-	/* And nuke the RelOptInfo, just in case there's another access path. */
-	pfree(toRemove);
+	foreach(l, quals)
+	{
+		Node	   *qual = (Node *) lfirst(l);
 
+		if (bms_is_member(relid, pull_varnos(root, qual)))
+		{
+			qual = replace_selfjoin_qual(qual);
+			/* Drop it if the substitution has made it a duplicate */
+			if (list_member(result, qual))
+				continue;
+		}
+		result = lappend(result, qual);
+	}
+
+	return result;
+}
+
+/*
+ * replace_selfjoin_qual
+ *		Replace one "X = X" qual by "X IS NOT NULL", if it is one.
+ */
+static Node *
+replace_selfjoin_qual(Node *qual)
+{
+	OpExpr	   *opexpr;
+	Node	   *leftop;
+	Node	   *rightop;
+	NullTest   *ntest;
+
+	/* See if it looks like "X op X" */
+	if (!is_opclause(qual))
+		return qual;
+	opexpr = (OpExpr *) qual;
+	if (list_length(opexpr->args) != 2)
+		return qual;
+	leftop = get_leftop((Expr *) opexpr);
+	rightop = get_rightop((Expr *) opexpr);
+	if (!equal(leftop, rightop))
+		return qual;
 
 	/*
-	 * Now repeat construction of attr_needed bits coming from all other
-	 * sources.
+	 * The operator must be strict and behave like btree equality, else we
+	 * can't conclude that it yields true for any non-null input.  And the
+	 * input had better not be volatile, else the two evaluations might not
+	 * agree.  If either condition doesn't hold, the clause is not a candidate
+	 * to be an equivalence, so we needn't worry about it getting replaced by
+	 * equivclass.c.
 	 */
-	rebuild_placeholder_attr_needed(root);
-	rebuild_joinclause_attr_needed(root);
-	rebuild_eclass_attr_needed(root);
-	rebuild_lateral_attr_needed(root);
+	set_opfuncid(opexpr);
+	if (!func_strict(opexpr->opfuncid))
+		return qual;
+	if (!op_mergejoinable(opexpr->opno, exprType(leftop)))
+		return qual;
+	if (contain_volatile_functions(leftop))
+		return qual;
+
+	/* OK, replace it */
+	ntest = makeNode(NullTest);
+	ntest->arg = (Expr *) leftop;
+	ntest->nulltesttype = IS_NOT_NULL;
+	ntest->argisrow = false;	/* correct even if composite arg */
+	ntest->location = -1;
+	return (Node *) ntest;
 }
 
 /*
@@ -2227,7 +2568,12 @@ split_selfjoin_quals(PlannerInfo *root, List *joinquals, List **selfjoinquals,
 		Node	   *leftexpr;
 		Node	   *rightexpr;
 
-		/* In general, clause looks like F(arg1) = G(arg2) */
+		/*
+		 * Since the given joinquals all came from
+		 * generate_join_implied_equalities, they ought to look like equality
+		 * operators on single-relation expressions.  But let's check that.
+		 * Anything that doesn't look like that can be dumped into ojoinquals.
+		 */
 		if (!rinfo->mergeopfamilies ||
 			bms_num_members(rinfo->clause_relids) != 2 ||
 			bms_membership(rinfo->left_relids) != BMS_SINGLETON ||
@@ -2340,15 +2686,16 @@ match_unique_clauses(PlannerInfo *root, RelOptInfo *outer, List *uclauses,
 }
 
 /*
- * Find and remove unique self-joins in a group of base relations that have
+ * Find and remove one unique self-join in a group of base relations that have
  * the same Oid.
  *
- * Returns a set of relids that were removed.
+ * Returns true if we removed a relation.  We stop at the first removal,
+ * because it invalidates all the derived information that the tests here
+ * depend on; our caller will come back for more once that's been rebuilt.
  */
-static Relids
+static bool
 remove_self_joins_one_group(PlannerInfo *root, Relids relids)
 {
-	Relids		result = NULL;
 	int			k;				/* Index of kept relation */
 	int			r = -1;			/* Index of removed relation */
 
@@ -2356,8 +2703,8 @@ remove_self_joins_one_group(PlannerInfo *root, Relids relids)
 	{
 		RelOptInfo *rrel = root->simple_rel_array[r];
 
+		/* k iterates over the relids after r */
 		k = r;
-
 		while ((k = bms_next_member(relids, k)) > 0)
 		{
 			Relids		joinrelids = NULL;
@@ -2377,8 +2724,8 @@ remove_self_joins_one_group(PlannerInfo *root, Relids relids)
 
 			/*
 			 * It is impossible to eliminate the join of two relations if they
-			 * belong to different rules of order. Otherwise, the planner
-			 * can't find any variants of the correct query plan.
+			 * are not on the same side of every outer join.  Otherwise, the
+			 * planner can't find any variants of the correct query plan.
 			 */
 			foreach(lc, root->join_info_list)
 			{
@@ -2492,29 +2839,26 @@ remove_self_joins_one_group(PlannerInfo *root, Relids relids)
 			if (!match_unique_clauses(root, rrel, uclauses, krel->relid))
 				continue;
 
-			/*
-			 * Remove rrel RelOptInfo from the planner structures and the
-			 * corresponding row mark.
-			 */
-			remove_self_join_rel(root, kmark, rmark, krel, rrel, restrictlist);
-
-			result = bms_add_member(result, r);
+			/* OK, remove rrel from the query, and report that we did so */
+			remove_self_join_rel(root, krel, rrel, kmark, rmark);
 
-			/* We have removed the outer relation, try the next one. */
-			break;
+			return true;
 		}
 	}
 
-	return result;
+	return false;
 }
 
 /*
- * Gather indexes of base relations from the joinlist and try to eliminate self
- * joins.
+ * Gather indexes of base relations from the joinlist and try to eliminate one
+ * self join.
+ *
+ * Returns true if we removed a relation.
  */
-static Relids
-remove_self_joins_recurse(PlannerInfo *root, List *joinlist, Relids toRemove)
+static bool
+remove_self_joins_recurse(PlannerInfo *root, List *joinlist)
 {
+	bool		removed = false;
 	ListCell   *jl;
 	Relids		relids = NULL;
 	SelfJoinCandidate *candidates = NULL;
@@ -2536,7 +2880,7 @@ remove_self_joins_recurse(PlannerInfo *root, List *joinlist, Relids toRemove)
 			 * We only consider ordinary relations as candidates to be
 			 * removed, and these relations should not have TABLESAMPLE
 			 * clauses specified.  Removing a relation with TABLESAMPLE clause
-			 * could potentially change the syntax of the query. Because of
+			 * could potentially change the semantics of the query. Because of
 			 * UPDATE/DELETE EPQ mechanism, currently Query->resultRelation or
 			 * Query->mergeTargetRelation associated rel cannot be eliminated.
 			 */
@@ -2552,9 +2896,9 @@ remove_self_joins_recurse(PlannerInfo *root, List *joinlist, Relids toRemove)
 		}
 		else if (IsA(jlnode, List))
 		{
-			/* Recursively go inside the sub-joinlist */
-			toRemove = remove_self_joins_recurse(root, (List *) jlnode,
-												 toRemove);
+			/* Recursively consider SJE within the sub-joinlist */
+			if (remove_self_joins_recurse(root, (List *) jlnode))
+				return true;
 		}
 		else
 			elog(ERROR, "unrecognized joinlist node type: %d",
@@ -2565,7 +2909,7 @@ remove_self_joins_recurse(PlannerInfo *root, List *joinlist, Relids toRemove)
 
 	/* Need at least two relations for the join */
 	if (numRels < 2)
-		return toRemove;
+		return false;
 
 	/*
 	 * In order to find relations with the same oid we first build an array of
@@ -2586,8 +2930,7 @@ remove_self_joins_recurse(PlannerInfo *root, List *joinlist, Relids toRemove)
 
 	/*
 	 * Iteratively form a group of relation indexes with the same oid and
-	 * launch the routine that detects self-joins in this group and removes
-	 * excessive range table entries.
+	 * launch the routine that detects a self-join in this group.
 	 *
 	 * At the end of the iteration, exclude the group from the overall relids
 	 * list. So each next iteration of the cycle will involve less and less
@@ -2598,11 +2941,10 @@ remove_self_joins_recurse(PlannerInfo *root, List *joinlist, Relids toRemove)
 	{
 		if (j == numRels || candidates[j].reloid != candidates[i].reloid)
 		{
-			if (j - i >= 2)
+			if (j - i >= 2 && !removed)
 			{
 				/* Create a group of relation indexes with the same oid */
 				Relids		group = NULL;
-				Relids		removed;
 
 				while (i < j)
 				{
@@ -2612,34 +2954,29 @@ remove_self_joins_recurse(PlannerInfo *root, List *joinlist, Relids toRemove)
 				relids = bms_del_members(relids, group);
 
 				/*
-				 * Try to remove self-joins from a group of identical entries.
-				 * Make the next attempt iteratively - if something is deleted
-				 * from a group, changes in clauses and equivalence classes
-				 * can give us a chance to find more candidates.
+				 * Try to remove a self-join from a group of identical
+				 * entries.  We stop as soon as we succeed, since the
+				 * information we based the decision on is then out of date;
+				 * our caller will start over from scratch.
 				 */
-				do
-				{
-					Assert(!bms_overlap(group, toRemove));
-					removed = remove_self_joins_one_group(root, group);
-					toRemove = bms_add_members(toRemove, removed);
-					group = bms_del_members(group, removed);
-				} while (!bms_is_empty(removed) &&
-						 bms_membership(group) == BMS_MULTIPLE);
-				bms_free(removed);
+				removed = remove_self_joins_one_group(root, group);
 				bms_free(group);
 			}
 			else
 			{
-				/* Single relation, just remove it from the set */
-				relids = bms_del_member(relids, candidates[i].relid);
-				i = j;
+				/* Nothing to do with this group, just drop it from the set */
+				while (i < j)
+				{
+					relids = bms_del_member(relids, candidates[i].relid);
+					i++;
+				}
 			}
 		}
 	}
 
 	Assert(bms_is_empty(relids));
 
-	return toRemove;
+	return removed;
 }
 
 /*
@@ -2681,45 +3018,25 @@ self_join_candidates_cmp(const void *a, const void *b)
  * go over each set with the same Oid, and consider each pair of relations
  * in this set.
  *
- * To remove the join, we mark one of the participating relations as dead
- * and rewrite all references to it to point to the remaining relation.
- * This includes modifying RestrictInfos, EquivalenceClasses, and
- * EquivalenceMembers. We also have to modify the row marks. The join clauses
- * of the removed relation become either restriction or join clauses, based on
- * whether they reference any relations not participating in the removed join.
+ * To remove the join, we delete one of the participating relations from the
+ * query's jointree and rewrite all references to it to point to the remaining
+ * relation.  We also have to modify their row marks.
+ *
+ * 'joinlist' is the top-level joinlist of the query; we use it to identify
+ * groups of relations that could be joined to each other.
  *
- * 'joinlist' is the top-level joinlist of the query. If it has any
- * references to the removed relations, we update them to point to the
- * remaining ones.
+ * We remove at most one self-join per call, and return true if we did so.
+ * The caller must then recompute everything that was derived from the
+ * jointree before calling here again.
  */
-List *
+bool
 remove_useless_self_joins(PlannerInfo *root, List *joinlist)
 {
-	Relids		toRemove = NULL;
-	int			relid = -1;
-
+	/* Skip if SJE is disabled, or if the joinlist has less than 2 members. */
 	if (!enable_self_join_elimination || joinlist == NIL ||
 		(list_length(joinlist) == 1 && !IsA(linitial(joinlist), List)))
-		return joinlist;
-
-	/*
-	 * Merge pairs of relations participated in self-join. Remove unnecessary
-	 * range table entries.
-	 */
-	toRemove = remove_self_joins_recurse(root, joinlist, toRemove);
-
-	if (unlikely(toRemove != NULL))
-	{
-		/* At the end, remove orphaned relation links */
-		while ((relid = bms_next_member(toRemove, relid)) >= 0)
-		{
-			int			nremoved = 0;
-
-			joinlist = remove_rel_from_joinlist(joinlist, relid, &nremoved);
-			if (nremoved != 1)
-				elog(ERROR, "failed to find relation %d in joinlist", relid);
-		}
-	}
+		return false;
 
-	return joinlist;
+	/* Try to merge one pair of relations participating in a self-join. */
+	return remove_self_joins_recurse(root, joinlist);
 }
diff --git a/src/backend/optimizer/plan/initsplan.c b/src/backend/optimizer/plan/initsplan.c
index b38422c47a4..7b6d7249c66 100644
--- a/src/backend/optimizer/plan/initsplan.c
+++ b/src/backend/optimizer/plan/initsplan.c
@@ -4044,9 +4044,9 @@ match_foreign_keys_to_quals(PlannerInfo *root)
 
 		/*
 		 * Either relid might identify a rel that is in the query's rtable but
-		 * isn't referenced by the jointree, or has been removed by join
-		 * removal, so that it won't have a RelOptInfo.  Hence don't use
-		 * find_base_rel() here.  We can ignore such FKs.
+		 * isn't referenced by the jointree (typically because it's been
+		 * removed by join removal), so that it won't have a RelOptInfo. Hence
+		 * don't use find_base_rel() here.  We can ignore such FKs.
 		 */
 		if (fkinfo->con_relid >= root->simple_rel_array_size ||
 			fkinfo->ref_relid >= root->simple_rel_array_size)
diff --git a/src/backend/optimizer/plan/planmain.c b/src/backend/optimizer/plan/planmain.c
index c123a397c1a..298affe6bca 100644
--- a/src/backend/optimizer/plan/planmain.c
+++ b/src/backend/optimizer/plan/planmain.c
@@ -55,33 +55,71 @@ RelOptInfo *
 query_planner(PlannerInfo *root,
 			  query_pathkeys_callback qp_callback, void *qp_extra)
 {
-	Query	   *parse = root->parse;
+	Query	   *parse;
 	List	   *joinlist;
 	RelOptInfo *final_rel;
 
 	/*
-	 * Init planner lists to empty.
+	 * The join simplification steps below work by modifying parse->jointree,
+	 * and they make no attempt to update the information we derive from it.
+	 * So whenever one of them succeeds, we must throw away all that derived
+	 * information and recompute it from scratch, which we do by looping back
+	 * to "restart".  We cannot loop indefinitely, because each successful
+	 * simplification either deletes a base relation from the jointree or
+	 * turns a semijoin into an inner join, and neither of those can be undone
+	 * by a later pass.
 	 *
-	 * NOTE: append_rel_list was set up by subquery_planner, so do not touch
-	 * here.
+	 * These initial Asserts check that the state at entry is not too complex
+	 * for the code below to restore.  There mustn't be any EquivalenceClasses
+	 * yet, and we should have only the top-level JoinDomain.
 	 */
+	Assert(root->eq_classes == NIL);
+	Assert(list_length(root->join_domains) == 1);
+
+restart:
+	parse = root->parse;
+
+	/*
+	 * Initialize information derived from the jointree to empty.
+	 *
+	 * It's critical that this reset every field that the steps below will
+	 * fill in, since we may be going around this loop more than once.
+	 *
+	 * NOTE: append_rel_list was created earlier, so do not clear it here;
+	 * rowMarks ditto.  Join simplification must update those if necessary.
+	 */
+	root->all_baserels = NULL;
+	root->outer_join_rels = NULL;
+	root->all_query_rels = NULL;
 	root->join_rel_list = NIL;
 	root->join_rel_hash = NULL;
 	root->join_rel_level = NULL;
 	root->join_cur_level = 0;
+	root->eq_classes = NIL;
+	root->ec_merging_done = false;
 	root->canon_pathkeys = NIL;
 	root->left_join_clauses = NIL;
 	root->right_join_clauses = NIL;
 	root->full_join_clauses = NIL;
 	root->join_info_list = NIL;
+	root->last_rinfo_serial = 0;
 	root->placeholder_list = NIL;
 	root->placeholder_array = NULL;
 	root->placeholder_array_size = 0;
+	root->placeholdersFrozen = false;
 	root->agg_clause_list = NIL;
 	root->group_expr_list = NIL;
 	root->tlist_vars = NIL;
 	root->fkey_list = NIL;
 	root->initial_rels = NIL;
+	root->hasPseudoConstantQuals = false;
+
+	/*
+	 * We don't want to delete the top-level join domain, but get rid of other
+	 * ones so as to reset the list to initial state.  deconstruct_jointree
+	 * will take care of (re)computing the top level's jd_relids.
+	 */
+	root->join_domains = list_truncate(root->join_domains, 1);
 
 	/*
 	 * Set up arrays for accessing base relations and AppendRelInfos.
@@ -228,19 +266,30 @@ query_planner(PlannerInfo *root,
 	 * Remove any useless outer joins.  Ideally this would be done during
 	 * jointree preprocessing, but the necessary information isn't available
 	 * until we've built baserel data structures and classified qual clauses.
+	 * If we remove a join, loop back to the top and redo what we did so far.
 	 */
-	joinlist = remove_useless_joins(root, joinlist);
+	if (remove_useless_joins(root))
+		goto restart;
 
 	/*
 	 * Also, reduce any semijoins with unique inner rels to plain inner joins.
-	 * Likewise, this can't be done until now for lack of needed info.
+	 * Likewise, this can't be done until now for lack of needed info, and we
+	 * must loop around if we find any simplifications.
+	 */
+	if (reduce_unique_semijoins(root))
+		goto restart;
+
+	/*
+	 * Remove self joins on a unique column.  Again, this couldn't be done any
+	 * earlier, and we must loop around if we find anything to remove.
 	 */
-	reduce_unique_semijoins(root);
+	if (remove_useless_self_joins(root, joinlist))
+		goto restart;
 
 	/*
-	 * Remove self joins on a unique column.
+	 * No more join simplifications apply, so we're done looping.  Code below
+	 * this point does not need to be able to restart.
 	 */
-	joinlist = remove_useless_self_joins(root, joinlist);
 
 	/*
 	 * Now distribute "placeholders" to base rels as needed.  This has to be
diff --git a/src/include/optimizer/planmain.h b/src/include/optimizer/planmain.h
index 71c043a25e8..85301d912f0 100644
--- a/src/include/optimizer/planmain.h
+++ b/src/include/optimizer/planmain.h
@@ -108,8 +108,8 @@ extern void match_foreign_keys_to_quals(PlannerInfo *root);
 /*
  * prototypes for plan/analyzejoins.c
  */
-extern List *remove_useless_joins(PlannerInfo *root, List *joinlist);
-extern void reduce_unique_semijoins(PlannerInfo *root);
+extern bool remove_useless_joins(PlannerInfo *root);
+extern bool reduce_unique_semijoins(PlannerInfo *root);
 extern bool query_supports_distinctness(Query *query);
 extern bool query_is_distinct_for(Query *query, List *distinct_cols);
 extern bool innerrel_is_unique(PlannerInfo *root,
@@ -119,7 +119,7 @@ extern bool innerrel_is_unique_ext(PlannerInfo *root, Relids joinrelids,
 								   Relids outerrelids, RelOptInfo *innerrel,
 								   JoinType jointype, List *restrictlist,
 								   bool force_cache, List **extra_clauses);
-extern List *remove_useless_self_joins(PlannerInfo *root, List *joinlist);
+extern bool remove_useless_self_joins(PlannerInfo *root, List *joinlist);
 
 /*
  * prototypes for plan/setrefs.c
diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out
index 19e2cca548b..254b203aeba 100644
--- a/src/test/regress/expected/join.out
+++ b/src/test/regress/expected/join.out
@@ -6890,6 +6890,38 @@ where t1.a = s.c;
 ----------
 (0 rows)
 
+rollback;
+-- join removal bug #19560: removing a join can leave an EquivalenceClass that
+-- now yields a base restriction clause, so we must redo equivalence
+-- processing from scratch
+begin;
+create temp table items (id text, owner text);
+create temp table follows (item_id text, user_id text,
+                           unique (user_id, item_id));
+insert into items values ('item1', 'alice');
+explain (costs off)
+with viewer as (select 'bob' as id)
+select count(*) from items
+  left join follows on follows.item_id = items.id and follows.user_id = 'bob'
+  left join viewer on true
+where items.owner = viewer.id;
+              QUERY PLAN               
+---------------------------------------
+ Aggregate
+   ->  Seq Scan on items
+         Filter: (owner = 'bob'::text)
+(3 rows)
+
+with viewer as (select 'bob' as id)
+select count(*) from items
+  left join follows on follows.item_id = items.id and follows.user_id = 'bob'
+  left join viewer on true
+where items.owner = viewer.id;
+ count 
+-------
+     0
+(1 row)
+
 rollback;
 -- check handling of semijoins after join removal: we must suppress
 -- unique-ification of known-constant values
@@ -6915,17 +6947,17 @@ where exists (select 1 from t t4
                Output: t1.a
                Index Cond: (t1.a = 1)
          ->  HashAggregate
-               Output: t5.a
+               Output: t4.a, t5.a
                Group Key: t5.a
                ->  Hash Join
-                     Output: t5.a
+                     Output: t4.a, t5.a
                      Hash Cond: (t6.b = t4.b)
                      ->  Seq Scan on pg_temp.t t6
                            Output: t6.a, t6.b
                      ->  Hash
-                           Output: t4.b, t5.b, t5.a
+                           Output: t4.b, t4.a, t5.b, t5.a
                            ->  Hash Join
-                                 Output: t4.b, t5.b, t5.a
+                                 Output: t4.b, t4.a, t5.b, t5.a
                                  Inner Unique: true
                                  Hash Cond: (t5.b = t4.b)
                                  ->  Seq Scan on pg_temp.t t5
@@ -7357,7 +7389,7 @@ on q1.ax = q2.a;
  Nested Loop Left Join
    Join Filter: (t2.a = t4.a)
    ->  Seq Scan on sj t2
-         Filter: ((b IS NULL) AND (a IS NOT NULL) AND ((c * c) = (c + 2)))
+         Filter: ((a IS NOT NULL) AND (b IS NULL) AND ((c * c) = (c + 2)))
    ->  Seq Scan on sj t4
          Filter: (c IS NOT NULL)
 (6 rows)
@@ -7440,9 +7472,9 @@ select t1.a from sj t1 where t1.b in (
    ->  Seq Scan on public.sj t1
          Output: t1.a, t1.b, t1.c
    ->  Materialize
-         Output: t3.c, t3.b
+         Output: t3.b
          ->  Seq Scan on public.sj t3
-               Output: t3.c, t3.b
+               Output: t3.b
                Filter: (t3.c IS NOT NULL)
 (10 rows)
 
@@ -7776,15 +7808,15 @@ explain (costs off) select * from sj p join sj q on p.a = q.a
          ->  Seq Scan on sj r
 (6 rows)
 
--- FIXME this constant false filter doesn't look good. Should we merge
--- equivalence classes?
+-- Check that we detect constant-false condition after merging ECs.
 explain (costs off)
 select * from sj p, sj q where p.a = q.a and p.b = 1 and q.b = 2;
-                     QUERY PLAN                      
------------------------------------------------------
- Seq Scan on sj q
-   Filter: ((a IS NOT NULL) AND (b = 2) AND (b = 1))
-(2 rows)
+        QUERY PLAN        
+--------------------------
+ Result
+   Replaces: Scan on q
+   One-Time Filter: false
+(3 rows)
 
 -- Check that attr_needed is updated correctly after self-join removal. In this
 -- test, the join of j1 with j2 is removed. k1.b is required at either j1 or j2.
@@ -7920,11 +7952,9 @@ where s1.x = 1;
    ->  Seq Scan on public.emp1 t1
          Output: t1.id, t1.code
    ->  Materialize
-         Output: t3.id
          ->  Seq Scan on public.emp1 t3
-               Output: t3.id
                Filter: (1 = 1)
-(9 rows)
+(7 rows)
 
 -- Check that PHVs do not impose any constraints on removing self joins
 explain (verbose, costs off)
@@ -7974,14 +8004,14 @@ SELECT 1 FROM tbl_phv t1 LEFT JOIN
     (SELECT y FROM tbl_phv tr) t4
   ON t4.y = t3.y
 ON true WHERE t3.extra IS NOT NULL AND t3.x = t1.x % 2;
-                       QUERY PLAN                        
----------------------------------------------------------
+                          QUERY PLAN                          
+--------------------------------------------------------------
  Nested Loop
    Output: 1
    ->  Seq Scan on public.tbl_phv t1
          Output: t1.x, t1.y
-   ->  Index Scan using tbl_phv_idx on public.tbl_phv tr
-         Output: tr.x, tr.y
+   ->  Index Only Scan using tbl_phv_idx on public.tbl_phv tr
+         Output: tr.x
          Index Cond: (tr.x = (t1.x % 2))
          Filter: (1 IS NOT NULL)
 (8 rows)
@@ -8162,7 +8192,7 @@ where t1.b = t2.b and t2.a = 3 and t1.a = 3
 ---------------------------------------------------------------------------------------------
  Seq Scan on public.sl t2
    Output: t2.a, t2.b, t2.c, t2.a, t2.b, t2.c
-   Filter: ((t2.c IS NOT NULL) AND (t2.b IS NOT NULL) AND (t2.a IS NOT NULL) AND (t2.a = 3))
+   Filter: ((t2.b IS NOT NULL) AND (t2.c IS NOT NULL) AND (t2.a IS NOT NULL) AND (t2.a = 3))
 (3 rows)
 
 -- Join qual isn't mergejoinable, but inner is unique.
@@ -8204,10 +8234,35 @@ SELECT 1 AS c1 FROM sl sl1 LEFT JOIN (sl AS sl2 NATURAL JOIN sl AS sl3)
    ->  Nested Loop Left Join
          Join Filter: sl3.bool_col
          ->  Seq Scan on sl sl3
-               Filter: (bool_col AND (a IS NOT NULL) AND (b IS NOT NULL) AND (c IS NOT NULL) AND (bool_col IS NOT NULL))
+               Filter: ((a IS NOT NULL) AND (b IS NOT NULL) AND (c IS NOT NULL) AND (bool_col IS NOT NULL) AND bool_col)
          ->  Seq Scan on sl sl4
 (7 rows)
 
+-- Check that quals of a jointree node that becomes empty when the self-join
+-- is removed are not lost, and that they don't migrate above an outer join
+EXPLAIN (COSTS OFF)
+SELECT s.a FROM (SELECT * FROM sl WHERE c IS NOT NULL) s, sl t
+WHERE t.a = s.a AND t.b = s.b;
+                             QUERY PLAN                              
+---------------------------------------------------------------------
+ Seq Scan on sl
+   Filter: ((c IS NOT NULL) AND (a IS NOT NULL) AND (b IS NOT NULL))
+(2 rows)
+
+EXPLAIN (COSTS OFF)
+SELECT t1.a, ss.a FROM sl t1
+  LEFT JOIN (SELECT s.a FROM (SELECT * FROM sl WHERE c IS NOT NULL) s
+                             JOIN sl t ON t.a = s.a AND t.b = s.b) ss
+    ON ss.a = t1.a;
+                                QUERY PLAN                                 
+---------------------------------------------------------------------------
+ Nested Loop Left Join
+   Join Filter: (sl.a = t1.a)
+   ->  Seq Scan on sl t1
+   ->  Seq Scan on sl
+         Filter: ((c IS NOT NULL) AND (a IS NOT NULL) AND (b IS NOT NULL))
+(5 rows)
+
 -- Check optimization disabling if it will violate special join conditions.
 -- Two identical joined relations satisfies self join removal conditions but
 -- stay in different special join infos.
diff --git a/src/test/regress/sql/join.sql b/src/test/regress/sql/join.sql
index 85aed7bf704..afa96daecb5 100644
--- a/src/test/regress/sql/join.sql
+++ b/src/test/regress/sql/join.sql
@@ -2601,6 +2601,31 @@ where t1.a = s.c;
 
 rollback;
 
+-- join removal bug #19560: removing a join can leave an EquivalenceClass that
+-- now yields a base restriction clause, so we must redo equivalence
+-- processing from scratch
+begin;
+
+create temp table items (id text, owner text);
+create temp table follows (item_id text, user_id text,
+                           unique (user_id, item_id));
+insert into items values ('item1', 'alice');
+
+explain (costs off)
+with viewer as (select 'bob' as id)
+select count(*) from items
+  left join follows on follows.item_id = items.id and follows.user_id = 'bob'
+  left join viewer on true
+where items.owner = viewer.id;
+
+with viewer as (select 'bob' as id)
+select count(*) from items
+  left join follows on follows.item_id = items.id and follows.user_id = 'bob'
+  left join viewer on true
+where items.owner = viewer.id;
+
+rollback;
+
 -- check handling of semijoins after join removal: we must suppress
 -- unique-ification of known-constant values
 begin;
@@ -3015,8 +3040,7 @@ select 1 from (select y.* from sj x, sj y where x.a = y.a) q,
 explain (costs off) select * from sj p join sj q on p.a = q.a
   left join sj r on p.a + q.a = r.a;
 
--- FIXME this constant false filter doesn't look good. Should we merge
--- equivalence classes?
+-- Check that we detect constant-false condition after merging ECs.
 explain (costs off)
 select * from sj p, sj q where p.a = q.a and p.b = 1 and q.b = 2;
 
@@ -3214,6 +3238,18 @@ EXPLAIN (COSTS OFF)
 SELECT 1 AS c1 FROM sl sl1 LEFT JOIN (sl AS sl2 NATURAL JOIN sl AS sl3)
   ON sl2.bool_col LEFT JOIN sl AS sl4 ON sl2.bool_col;
 
+-- Check that quals of a jointree node that becomes empty when the self-join
+-- is removed are not lost, and that they don't migrate above an outer join
+EXPLAIN (COSTS OFF)
+SELECT s.a FROM (SELECT * FROM sl WHERE c IS NOT NULL) s, sl t
+WHERE t.a = s.a AND t.b = s.b;
+
+EXPLAIN (COSTS OFF)
+SELECT t1.a, ss.a FROM sl t1
+  LEFT JOIN (SELECT s.a FROM (SELECT * FROM sl WHERE c IS NOT NULL) s
+                             JOIN sl t ON t.a = s.a AND t.b = s.b) ss
+    ON ss.a = t1.a;
+
 -- Check optimization disabling if it will violate special join conditions.
 -- Two identical joined relations satisfies self join removal conditions but
 -- stay in different special join infos.
-- 
2.52.0

From a3d6ff0731aea4461b641486f645095efedfa91e Mon Sep 17 00:00:00 2001
From: Tom Lane <[email protected]>
Date: Sat, 25 Jul 2026 12:32:46 -0400
Subject: [PATCH v2 3/3] Remove dead code that's no longer needed for join
 removal.

This is mostly a straightforward exercise in deleting unreferenced
functions, but a couple of points are worth noting.

While replace_relid_callback() was still reached in the previous
patch, it did nothing since we no longer invoked it on any trees
containing RangeTblRef or RestrictInfo nodes.  So we can delete it
and make the two remaining users use ChangeVarNodes instead of
ChangeVarNodesExtended.  Then, ChangeVarNodesExtended is unused
and we can remove that too, along with the exposed definition
of ChangeVarNodes_context (basically reverting that area of the
code to what it was in v17).

ec_clear_derived_clauses, innerrel_is_unique_ext, adjust_relid_set are
now static since they're no longer called from outside their modules.

Bug: #19560
Reported-by: Orestis Markou <[email protected]>
Author: Tom Lane <[email protected]>
Discussion: https://postgr.es/m/[email protected]
---
 src/backend/optimizer/path/equivclass.c   |   51 +-
 src/backend/optimizer/plan/analyzejoins.c | 1037 +--------------------
 src/backend/optimizer/plan/initsplan.c    |  179 +---
 src/backend/optimizer/util/joininfo.c     |   36 -
 src/backend/optimizer/util/placeholder.c  |   27 -
 src/backend/rewrite/rewriteManip.c        |   65 +-
 src/include/optimizer/joininfo.h          |    3 -
 src/include/optimizer/paths.h             |    2 -
 src/include/optimizer/placeholder.h       |    1 -
 src/include/optimizer/planmain.h          |    8 -
 src/include/rewrite/rewriteManip.h        |   19 -
 src/tools/pgindent/typedefs.list          |    1 -
 12 files changed, 30 insertions(+), 1399 deletions(-)

diff --git a/src/backend/optimizer/path/equivclass.c b/src/backend/optimizer/path/equivclass.c
index e3697df51a2..66fe1c24a58 100644
--- a/src/backend/optimizer/path/equivclass.c
+++ b/src/backend/optimizer/path/equivclass.c
@@ -89,6 +89,7 @@ static void ec_build_derives_hash(PlannerInfo *root, EquivalenceClass *ec);
 static void ec_add_derived_clauses(EquivalenceClass *ec, List *clauses);
 static void ec_add_derived_clause(EquivalenceClass *ec, RestrictInfo *clause);
 static void ec_add_clause_to_derives_hash(EquivalenceClass *ec, RestrictInfo *rinfo);
+static void ec_clear_derived_clauses(EquivalenceClass *ec);
 static RestrictInfo *ec_search_clause_for_ems(PlannerInfo *root, EquivalenceClass *ec,
 											  EquivalenceMember *leftem,
 											  EquivalenceMember *rightem,
@@ -1452,8 +1453,7 @@ generate_base_implied_equalities_no_const(PlannerInfo *root,
 	 * For the moment we force all the Vars to be available at all join nodes
 	 * for this eclass.  Perhaps this could be improved by doing some
 	 * pre-analysis of which members we prefer to join, but it's no worse than
-	 * what happened in the pre-8.3 code.  (Note: rebuild_eclass_attr_needed
-	 * needs to match this code.)
+	 * what happened in the pre-8.3 code.
 	 */
 	foreach(lc, ec->ec_members)
 	{
@@ -2560,51 +2560,6 @@ reconsider_full_join_clause(PlannerInfo *root, OuterJoinClauseInfo *ojcinfo)
 	return false;				/* failed to make any deduction */
 }
 
-/*
- * rebuild_eclass_attr_needed
- *	  Put back attr_needed bits for Vars/PHVs needed for join eclasses.
- *
- * This is used to rebuild attr_needed/ph_needed sets after removal of a
- * useless outer join.  It should match what
- * generate_base_implied_equalities_no_const did, except that we call
- * add_vars_to_attr_needed not add_vars_to_targetlist.
- */
-void
-rebuild_eclass_attr_needed(PlannerInfo *root)
-{
-	ListCell   *lc;
-
-	foreach(lc, root->eq_classes)
-	{
-		EquivalenceClass *ec = (EquivalenceClass *) lfirst(lc);
-
-		/*
-		 * We don't expect any EC child members to exist at this point. Ensure
-		 * that's the case, otherwise, we might be getting asked to do
-		 * something this function hasn't been coded for.
-		 */
-		Assert(ec->ec_childmembers == NULL);
-
-		/* Need do anything only for a multi-member, no-const EC. */
-		if (list_length(ec->ec_members) > 1 && !ec->ec_has_const)
-		{
-			ListCell   *lc2;
-
-			foreach(lc2, ec->ec_members)
-			{
-				EquivalenceMember *cur_em = (EquivalenceMember *) lfirst(lc2);
-				List	   *vars = pull_var_clause((Node *) cur_em->em_expr,
-												   PVC_RECURSE_AGGREGATES |
-												   PVC_RECURSE_WINDOWFUNCS |
-												   PVC_INCLUDE_PLACEHOLDERS);
-
-				add_vars_to_attr_needed(root, vars, ec->ec_relids);
-				list_free(vars);
-			}
-		}
-	}
-}
-
 /*
  * find_join_domain
  *	  Find the highest JoinDomain enclosed within the given relid set.
@@ -3826,7 +3781,7 @@ ec_add_clause_to_derives_hash(EquivalenceClass *ec, RestrictInfo *rinfo)
  * when thousands of partitions are involved, so we free it as well -- even
  * though we do not typically free lists.
  */
-void
+static void
 ec_clear_derived_clauses(EquivalenceClass *ec)
 {
 	list_free(ec->ec_derives_list);
diff --git a/src/backend/optimizer/plan/analyzejoins.c b/src/backend/optimizer/plan/analyzejoins.c
index ef355ec1fe1..a038b898bdb 100644
--- a/src/backend/optimizer/plan/analyzejoins.c
+++ b/src/backend/optimizer/plan/analyzejoins.c
@@ -30,11 +30,9 @@
 #include "catalog/pg_class.h"
 #include "nodes/makefuncs.h"
 #include "nodes/nodeFuncs.h"
-#include "optimizer/joininfo.h"
 #include "optimizer/optimizer.h"
 #include "optimizer/pathnode.h"
 #include "optimizer/paths.h"
-#include "optimizer/placeholder.h"
 #include "optimizer/planmain.h"
 #include "optimizer/prep.h"
 #include "optimizer/restrictinfo.h"
@@ -62,20 +60,6 @@ bool		enable_self_join_elimination;
 
 /* local functions */
 static bool join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo);
-static void remove_leftjoinrel_from_query(PlannerInfo *root, int relid,
-										  SpecialJoinInfo *sjinfo);
-static void remove_rel_from_query(PlannerInfo *root, int relid,
-								  int subst, SpecialJoinInfo *sjinfo,
-								  Relids joinrelids);
-static void remove_rel_from_restrictinfo(RestrictInfo *rinfo,
-										 int relid, int ojrelid);
-static void remove_rel_from_eclass(PlannerInfo *root, EquivalenceClass *ec,
-								   int relid, int ojrelid);
-static void remove_rel_from_restrictinfo_phvs(RestrictInfo *rinfo,
-											  int relid, int ojrelid);
-static Node *remove_rel_from_phvs(Node *node, int relid, int ojrelid);
-static Node *remove_rel_from_phvs_mutator(Node *node, Relids removable);
-static List *remove_rel_from_joinlist(List *joinlist, int relid, int *nremoved);
 static Node *remove_join_from_jointree(Node *jtnode, int ojrelid,
 									   int *nremoved);
 static void remove_rels_from_query_tree(PlannerInfo *root,
@@ -85,6 +69,14 @@ static bool rel_supports_distinctness(PlannerInfo *root, RelOptInfo *rel);
 static bool rel_is_distinct_for(PlannerInfo *root, RelOptInfo *rel,
 								List *clause_list, List **extra_clauses);
 static DistinctColInfo *distinct_col_search(int colno, List *distinct_cols);
+static bool innerrel_is_unique_ext(PlannerInfo *root,
+								   Relids joinrelids,
+								   Relids outerrelids,
+								   RelOptInfo *innerrel,
+								   JoinType jointype,
+								   List *restrictlist,
+								   bool force_cache,
+								   List **extra_clauses);
 static bool is_innerrel_unique_for(PlannerInfo *root,
 								   Relids joinrelids,
 								   Relids outerrelids,
@@ -100,8 +92,6 @@ static void fixup_selfjoin_jointree(PlannerInfo *root, Node *jtnode,
 static List *fixup_selfjoin_quals(PlannerInfo *root, List *quals, int relid);
 static Node *replace_selfjoin_qual(Node *qual);
 static int	self_join_candidates_cmp(const void *a, const void *b);
-static bool replace_relid_callback(Node *node,
-								   ChangeVarNodes_context *context);
 
 
 /*
@@ -367,713 +357,6 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 	return false;
 }
 
-/*
- * Remove the target relid and references to the target join from the
- * planner's data structures, having determined that there is no need
- * to include them in the query.
- *
- * We are not terribly thorough here.  We only bother to update parts of
- * the planner's data structures that will actually be consulted later.
- */
-static void
-remove_leftjoinrel_from_query(PlannerInfo *root, int relid,
-							  SpecialJoinInfo *sjinfo)
-{
-	RelOptInfo *rel = find_base_rel(root, relid);
-	int			ojrelid = sjinfo->ojrelid;
-	Relids		joinrelids;
-	Relids		join_plus_commute;
-	List	   *joininfos;
-	ListCell   *l;
-
-	/* Compute the relid set for the join we are considering */
-	joinrelids = bms_union(sjinfo->min_lefthand, sjinfo->min_righthand);
-	Assert(ojrelid != 0);
-	joinrelids = bms_add_member(joinrelids, ojrelid);
-
-	remove_rel_from_query(root, relid, -1, sjinfo, joinrelids);
-
-	/*
-	 * Remove any joinquals referencing the rel from the joininfo lists.
-	 *
-	 * In some cases, a joinqual has to be put back after deleting its
-	 * reference to the target rel.  This can occur for pseudoconstant and
-	 * outerjoin-delayed quals, which can get marked as requiring the rel in
-	 * order to force them to be evaluated at or above the join.  We can't
-	 * just discard them, though.  Only quals that logically belonged to the
-	 * outer join being discarded should be removed from the query.
-	 *
-	 * We might encounter a qual that is a clone of a deletable qual with some
-	 * outer-join relids added (see deconstruct_distribute_oj_quals).  To
-	 * ensure we get rid of such clones as well, add the relids of all OJs
-	 * commutable with this one to the set we test against for
-	 * pushed-down-ness.
-	 */
-	join_plus_commute = bms_union(joinrelids,
-								  sjinfo->commute_above_r);
-	join_plus_commute = bms_add_members(join_plus_commute,
-										sjinfo->commute_below_l);
-
-	/*
-	 * We must make a copy of the rel's old joininfo list before starting the
-	 * loop, because otherwise remove_join_clause_from_rels would destroy the
-	 * list while we're scanning it.
-	 */
-	joininfos = list_copy(rel->joininfo);
-	foreach(l, joininfos)
-	{
-		RestrictInfo *rinfo = (RestrictInfo *) lfirst(l);
-
-		remove_join_clause_from_rels(root, rinfo, rinfo->required_relids);
-
-		if (RINFO_IS_PUSHED_DOWN(rinfo, join_plus_commute))
-		{
-			/*
-			 * There might be references to relid or ojrelid in the
-			 * RestrictInfo's relid sets, as a consequence of PHVs having had
-			 * ph_eval_at sets that include those.  We already checked above
-			 * that any such PHV is safe (and updated its ph_eval_at), so we
-			 * can just drop those references.
-			 */
-			remove_rel_from_restrictinfo(rinfo, relid, ojrelid);
-
-			/*
-			 * Cross-check that the clause itself does not reference the
-			 * target rel or join.
-			 */
-#ifdef USE_ASSERT_CHECKING
-			{
-				Relids		clause_varnos = pull_varnos(root,
-														(Node *) rinfo->clause);
-
-				Assert(!bms_is_member(relid, clause_varnos));
-				Assert(!bms_is_member(ojrelid, clause_varnos));
-			}
-#endif
-			/* Now throw it back into the joininfo lists */
-			distribute_restrictinfo_to_rels(root, rinfo);
-		}
-	}
-
-	/*
-	 * There may be references to the rel in root->fkey_list, but if so,
-	 * match_foreign_keys_to_quals() will get rid of them.
-	 */
-
-	/*
-	 * Now remove the rel from the baserel array to prevent it from being
-	 * referenced again.  (We can't do this earlier because
-	 * remove_join_clause_from_rels will touch it.)
-	 */
-	root->simple_rel_array[relid] = NULL;
-	root->simple_rte_array[relid] = NULL;
-
-	/* And nuke the RelOptInfo, just in case there's another access path */
-	pfree(rel);
-
-	/*
-	 * Now repeat construction of attr_needed bits coming from all other
-	 * sources.
-	 */
-	rebuild_placeholder_attr_needed(root);
-	rebuild_joinclause_attr_needed(root);
-	rebuild_eclass_attr_needed(root);
-	rebuild_lateral_attr_needed(root);
-}
-
-/*
- * Remove the target relid and references to the target join from the
- * planner's data structures, having determined that there is no need
- * to include them in the query.  Optionally replace references to the
- * removed relid with subst if this is a self-join removal.
- *
- * This function serves as the common infrastructure for left-join removal
- * and self-join elimination.  It is intentionally scoped to update only the
- * shared planner data structures that are universally affected by relation
- * removal.  Each specific caller remains responsible for updating any
- * remaining data structures required by its unique removal logic.
- *
- * The specific type of removal being performed is dictated by the combination
- * of the sjinfo and subst parameters.  A non-NULL sjinfo indicates left-join
- * removal.  When sjinfo is NULL, a positive subst value indicates self-join
- * elimination (where references are replaced with subst).
- */
-static void
-remove_rel_from_query(PlannerInfo *root, int relid,
-					  int subst, SpecialJoinInfo *sjinfo,
-					  Relids joinrelids)
-{
-	int			ojrelid = sjinfo ? sjinfo->ojrelid : 0;
-	Index		rti;
-	ListCell   *l;
-	bool		is_outer_join = (sjinfo != NULL);
-	bool		is_self_join = (!is_outer_join && subst > 0);
-	Bitmapset  *seen_serials = NULL;
-
-	Assert(is_outer_join || is_self_join);
-	Assert(!is_outer_join || ojrelid > 0);
-	Assert(!is_outer_join || joinrelids != NULL);
-
-	/*
-	 * Update all_baserels and related relid sets.
-	 */
-	root->all_baserels = adjust_relid_set(root->all_baserels, relid, subst);
-	root->all_query_rels = adjust_relid_set(root->all_query_rels, relid, subst);
-
-	if (is_outer_join)
-	{
-		root->outer_join_rels = bms_del_member(root->outer_join_rels, ojrelid);
-		root->all_query_rels = bms_del_member(root->all_query_rels, ojrelid);
-	}
-
-	/*
-	 * Likewise remove references from SpecialJoinInfo data structures.
-	 *
-	 * This is relevant in case the relation we're deleting is part of the
-	 * relid sets of special joins: those sets have to be adjusted.  If we are
-	 * removing an outer join, the RHS of the target outer join will be made
-	 * empty here, but that's OK since the caller will delete that
-	 * SpecialJoinInfo entirely.
-	 */
-	foreach(l, root->join_info_list)
-	{
-		SpecialJoinInfo *sjinf = (SpecialJoinInfo *) lfirst(l);
-
-		/*
-		 * initsplan.c is fairly cavalier about allowing SpecialJoinInfos'
-		 * lefthand/righthand relid sets to be shared with other data
-		 * structures.  Ensure that we don't modify the original relid sets.
-		 * (The commute_xxx sets are always per-SpecialJoinInfo though.)
-		 */
-		sjinf->min_lefthand = bms_copy(sjinf->min_lefthand);
-		sjinf->min_righthand = bms_copy(sjinf->min_righthand);
-		sjinf->syn_lefthand = bms_copy(sjinf->syn_lefthand);
-		sjinf->syn_righthand = bms_copy(sjinf->syn_righthand);
-
-		/* Now adjust relid bit in the sets: */
-		sjinf->min_lefthand = adjust_relid_set(sjinf->min_lefthand, relid, subst);
-		sjinf->min_righthand = adjust_relid_set(sjinf->min_righthand, relid, subst);
-		sjinf->syn_lefthand = adjust_relid_set(sjinf->syn_lefthand, relid, subst);
-		sjinf->syn_righthand = adjust_relid_set(sjinf->syn_righthand, relid, subst);
-
-		if (is_outer_join)
-		{
-			/* Remove ojrelid bit from the sets: */
-			sjinf->min_lefthand = bms_del_member(sjinf->min_lefthand, ojrelid);
-			sjinf->min_righthand = bms_del_member(sjinf->min_righthand, ojrelid);
-			sjinf->syn_lefthand = bms_del_member(sjinf->syn_lefthand, ojrelid);
-			sjinf->syn_righthand = bms_del_member(sjinf->syn_righthand, ojrelid);
-			/* relid cannot appear in these fields, but ojrelid can: */
-			sjinf->commute_above_l = bms_del_member(sjinf->commute_above_l, ojrelid);
-			sjinf->commute_above_r = bms_del_member(sjinf->commute_above_r, ojrelid);
-			sjinf->commute_below_l = bms_del_member(sjinf->commute_below_l, ojrelid);
-			sjinf->commute_below_r = bms_del_member(sjinf->commute_below_r, ojrelid);
-		}
-		else
-		{
-			/*
-			 * For self-join removal, replace relid references in
-			 * semi_rhs_exprs.
-			 */
-			ChangeVarNodesExtended((Node *) sjinf->semi_rhs_exprs, relid, subst,
-								   0, replace_relid_callback);
-		}
-	}
-
-	/*
-	 * Likewise remove references from PlaceHolderVar data structures,
-	 * removing any no-longer-needed placeholders entirely.  We only remove
-	 * PHVs for left-join removal.  With self-join elimination, PHVs already
-	 * get moved to the remaining relation, where they might still be needed.
-	 * It might also happen that we skip the removal of some PHVs that could
-	 * be removed.  However, the overhead of extra PHVs is small compared to
-	 * the complexity of analysis needed to remove them.
-	 *
-	 * Removal is a bit trickier than it might seem: we can remove PHVs that
-	 * are used at the target rel and/or in the join qual, but not those that
-	 * are used at join partner rels or above the join.  It's not that easy to
-	 * distinguish PHVs used at partner rels from those used in the join qual,
-	 * since they will both have ph_needed sets that are subsets of
-	 * joinrelids.  However, a PHV used at a partner rel could not have the
-	 * target rel in ph_eval_at, so we check that while deciding whether to
-	 * remove or just update the PHV.  There is no corresponding test in
-	 * join_is_removable because it doesn't need to distinguish those cases.
-	 */
-	foreach(l, root->placeholder_list)
-	{
-		PlaceHolderInfo *phinfo = (PlaceHolderInfo *) lfirst(l);
-
-		Assert(!is_outer_join || !bms_is_member(relid, phinfo->ph_lateral));
-
-		if (is_outer_join &&
-			bms_is_subset(phinfo->ph_needed, joinrelids) &&
-			bms_is_member(relid, phinfo->ph_eval_at) &&
-			!bms_is_member(ojrelid, phinfo->ph_eval_at))
-		{
-			root->placeholder_list = foreach_delete_current(root->placeholder_list,
-															l);
-			root->placeholder_array[phinfo->phid] = NULL;
-		}
-		else
-		{
-			PlaceHolderVar *phv = phinfo->ph_var;
-
-			phinfo->ph_eval_at = adjust_relid_set(phinfo->ph_eval_at, relid, subst);
-			if (is_outer_join)
-				phinfo->ph_eval_at = bms_del_member(phinfo->ph_eval_at, ojrelid);
-			Assert(!bms_is_empty(phinfo->ph_eval_at));	/* checked previously */
-
-			/* Reduce ph_needed to contain only "relation 0"; see below */
-			if (bms_is_member(0, phinfo->ph_needed))
-				phinfo->ph_needed = bms_make_singleton(0);
-			else
-				phinfo->ph_needed = NULL;
-
-			phv->phrels = adjust_relid_set(phv->phrels, relid, subst);
-			if (is_outer_join)
-				phv->phrels = bms_del_member(phv->phrels, ojrelid);
-			Assert(!bms_is_empty(phv->phrels));
-
-			/*
-			 * For self-join removal, update Var nodes within the PHV's
-			 * expression to reference the replacement relid, and adjust
-			 * ph_lateral for the relid substitution.  (For left-join removal,
-			 * we're removing rather than replacing, and any surviving PHV
-			 * shouldn't reference the removed rel in its expression.  Also,
-			 * relid can't appear in ph_lateral for outer joins.)
-			 */
-			if (is_self_join)
-			{
-				ChangeVarNodesExtended((Node *) phv->phexpr, relid, subst, 0,
-									   replace_relid_callback);
-				phinfo->ph_lateral = adjust_relid_set(phinfo->ph_lateral, relid, subst);
-
-				/*
-				 * ph_lateral might contain rels mentioned in ph_eval_at after
-				 * the replacement, remove them.
-				 */
-				phinfo->ph_lateral = bms_difference(phinfo->ph_lateral, phinfo->ph_eval_at);
-				/* ph_lateral might or might not be empty */
-			}
-
-			Assert(phv->phnullingrels == NULL); /* no need to adjust */
-		}
-	}
-
-	/*
-	 * Likewise remove references from EquivalenceClasses.
-	 *
-	 * For self-join removal, the caller has already updated the
-	 * EquivalenceClasses, so we can skip this step.
-	 */
-	if (is_outer_join)
-	{
-		foreach(l, root->eq_classes)
-		{
-			EquivalenceClass *ec = (EquivalenceClass *) lfirst(l);
-
-			remove_rel_from_eclass(root, ec, relid, ojrelid);
-		}
-	}
-
-	/*
-	 * Finally, we must prepare for the caller to recompute per-Var
-	 * attr_needed and per-PlaceHolderVar ph_needed relid sets.  These have to
-	 * be known accurately, else we may fail to remove other now-removable
-	 * joins.  Because the caller removes the join clause(s) associated with
-	 * the removed join, Vars that were formerly needed may no longer be.
-	 *
-	 * The actual reconstruction of these relid sets is performed by the
-	 * specific caller.  Here, we simply clear out the existing attr_needed
-	 * sets (we already did this above for ph_needed) to ensure they are
-	 * rebuilt from scratch.  We can cheat to one small extent: we can avoid
-	 * re-examining the targetlist and HAVING qual by preserving "relation 0"
-	 * bits from the existing relid sets.  This is safe because we'd never
-	 * remove such references.
-	 *
-	 * Additionally, if we are performing self-join elimination, we must
-	 * replace references to the removed relid with subst within the
-	 * lateral_vars lists.
-	 *
-	 * Also, for left-join removal, we strip the removed rel and join from any
-	 * PlaceHolderVar embedded in the surviving rels' restriction clauses and
-	 * join clauses; we needn't bother with the rel being removed, nor when
-	 * the query has no PlaceHolderVars.
-	 */
-	for (rti = 1; rti < root->simple_rel_array_size; rti++)
-	{
-		RelOptInfo *otherrel = root->simple_rel_array[rti];
-		int			attroff;
-
-		/* there may be empty slots corresponding to non-baserel RTEs */
-		if (otherrel == NULL)
-			continue;
-
-		Assert(otherrel->relid == rti); /* sanity check on array */
-
-		for (attroff = otherrel->max_attr - otherrel->min_attr;
-			 attroff >= 0;
-			 attroff--)
-		{
-			if (bms_is_member(0, otherrel->attr_needed[attroff]))
-				otherrel->attr_needed[attroff] = bms_make_singleton(0);
-			else
-				otherrel->attr_needed[attroff] = NULL;
-		}
-
-		if (is_self_join)
-			ChangeVarNodesExtended((Node *) otherrel->lateral_vars, relid,
-								   subst, 0, replace_relid_callback);
-
-		if (is_outer_join && rti != relid && root->glob->lastPHId != 0)
-		{
-			foreach_node(RestrictInfo, rinfo, otherrel->baserestrictinfo)
-				remove_rel_from_restrictinfo_phvs(rinfo, relid, ojrelid);
-
-			/*
-			 * Join clauses need the same treatment, but there's no value in
-			 * processing any join clause more than once.  So it's slightly
-			 * annoying that we have to find them via the per-base-relation
-			 * joininfo lists.  Avoid duplicate processing by tracking the
-			 * rinfo_serial numbers of join clauses we've already seen.  (This
-			 * doesn't work for is_clone clauses, so we must waste effort on
-			 * them.)
-			 */
-			foreach_node(RestrictInfo, rinfo, otherrel->joininfo)
-			{
-				if (!rinfo->is_clone)	/* else serial number is not unique */
-				{
-					if (bms_is_member(rinfo->rinfo_serial, seen_serials))
-						continue;	/* saw it already */
-					seen_serials = bms_add_member(seen_serials,
-												  rinfo->rinfo_serial);
-				}
-				remove_rel_from_restrictinfo_phvs(rinfo, relid, ojrelid);
-			}
-		}
-	}
-}
-
-/*
- * Remove any references to relid or ojrelid from the RestrictInfo.
- *
- * We only bother to clean out bits in the RestrictInfo's various relid sets,
- * not nullingrel bits in contained Vars and PHVs.  (This might have to be
- * improved sometime.)  However, if the RestrictInfo contains an OR clause
- * we have to also clean up the sub-clauses.
- */
-static void
-remove_rel_from_restrictinfo(RestrictInfo *rinfo, int relid, int ojrelid)
-{
-	/*
-	 * initsplan.c is fairly cavalier about allowing RestrictInfos to share
-	 * relid sets with other RestrictInfos, and SpecialJoinInfos too.  Make
-	 * sure this RestrictInfo has its own relid sets before we modify them.
-	 * (In present usage, clause_relids is probably not shared, but
-	 * required_relids could be; let's not assume anything.)
-	 */
-	rinfo->clause_relids = bms_copy(rinfo->clause_relids);
-	rinfo->clause_relids = bms_del_member(rinfo->clause_relids, relid);
-	rinfo->clause_relids = bms_del_member(rinfo->clause_relids, ojrelid);
-	/* Likewise for required_relids */
-	rinfo->required_relids = bms_copy(rinfo->required_relids);
-	rinfo->required_relids = bms_del_member(rinfo->required_relids, relid);
-	rinfo->required_relids = bms_del_member(rinfo->required_relids, ojrelid);
-	/* Likewise for incompatible_relids */
-	rinfo->incompatible_relids = bms_copy(rinfo->incompatible_relids);
-	rinfo->incompatible_relids = bms_del_member(rinfo->incompatible_relids, relid);
-	rinfo->incompatible_relids = bms_del_member(rinfo->incompatible_relids, ojrelid);
-	/* Likewise for outer_relids */
-	rinfo->outer_relids = bms_copy(rinfo->outer_relids);
-	rinfo->outer_relids = bms_del_member(rinfo->outer_relids, relid);
-	rinfo->outer_relids = bms_del_member(rinfo->outer_relids, ojrelid);
-	/* Likewise for left_relids */
-	rinfo->left_relids = bms_copy(rinfo->left_relids);
-	rinfo->left_relids = bms_del_member(rinfo->left_relids, relid);
-	rinfo->left_relids = bms_del_member(rinfo->left_relids, ojrelid);
-	/* Likewise for right_relids */
-	rinfo->right_relids = bms_copy(rinfo->right_relids);
-	rinfo->right_relids = bms_del_member(rinfo->right_relids, relid);
-	rinfo->right_relids = bms_del_member(rinfo->right_relids, ojrelid);
-
-	/* If it's an OR, recurse to clean up sub-clauses */
-	if (restriction_is_or_clause(rinfo))
-	{
-		ListCell   *lc;
-
-		Assert(is_orclause(rinfo->orclause));
-		foreach(lc, ((BoolExpr *) rinfo->orclause)->args)
-		{
-			Node	   *orarg = (Node *) lfirst(lc);
-
-			/* OR arguments should be ANDs or sub-RestrictInfos */
-			if (is_andclause(orarg))
-			{
-				List	   *andargs = ((BoolExpr *) orarg)->args;
-				ListCell   *lc2;
-
-				foreach(lc2, andargs)
-				{
-					RestrictInfo *rinfo2 = lfirst_node(RestrictInfo, lc2);
-
-					remove_rel_from_restrictinfo(rinfo2, relid, ojrelid);
-				}
-			}
-			else
-			{
-				RestrictInfo *rinfo2 = castNode(RestrictInfo, orarg);
-
-				remove_rel_from_restrictinfo(rinfo2, relid, ojrelid);
-			}
-		}
-	}
-}
-
-/*
- * Remove any references to relid or ojrelid from the EquivalenceClass.
- *
- * We fix the EC and EM relid sets to ensure that implied join equalities will
- * be generated at the appropriate join level(s).  We also strip the removed
- * rel from PlaceHolderVars embedded in member expressions; a member's
- * em_relids reflects ph_eval_at rather than the PHV's phrels, so the latter
- * can still mention the removed rel even when em_relids does not.  Like
- * remove_rel_from_restrictinfo, we don't bother with nullingrel bits in
- * contained plain Vars.
- */
-static void
-remove_rel_from_eclass(PlannerInfo *root, EquivalenceClass *ec,
-					   int relid, int ojrelid)
-{
-	ListCell   *lc;
-
-	/*
-	 * Strip the removed rel/join from PlaceHolderVars in member expressions.
-	 * This is needed even when the EC's relids don't mention the removed rel.
-	 * Plain Vars and Consts can't contain a PlaceHolderVar, so skip them.
-	 */
-	if (root->glob->lastPHId != 0)
-	{
-		foreach_node(EquivalenceMember, em, ec->ec_members)
-		{
-			if (!IsA(em->em_expr, Var) && !IsA(em->em_expr, Const))
-				em->em_expr = (Expr *)
-					remove_rel_from_phvs((Node *) em->em_expr, relid, ojrelid);
-		}
-	}
-
-	if (!bms_is_member(relid, ec->ec_relids) &&
-		!bms_is_member(ojrelid, ec->ec_relids))
-		return;
-
-	/* Fix up the EC's overall relids */
-	ec->ec_relids = bms_del_member(ec->ec_relids, relid);
-	ec->ec_relids = bms_del_member(ec->ec_relids, ojrelid);
-
-	/*
-	 * We don't expect any EC child members to exist at this point.  Ensure
-	 * that's the case, otherwise, we might be getting asked to do something
-	 * this function hasn't been coded for.
-	 */
-	Assert(ec->ec_childmembers == NULL);
-
-	/*
-	 * Fix up the member expressions.  Any non-const member that ends with
-	 * empty em_relids must be a Var or PHV of the removed relation.  We don't
-	 * need it anymore, so we can drop it.
-	 */
-	foreach(lc, ec->ec_members)
-	{
-		EquivalenceMember *cur_em = (EquivalenceMember *) lfirst(lc);
-
-		if (bms_is_member(relid, cur_em->em_relids) ||
-			bms_is_member(ojrelid, cur_em->em_relids))
-		{
-			Assert(!cur_em->em_is_const);
-			/* em_relids is likely to be shared with some RestrictInfo */
-			cur_em->em_relids = bms_copy(cur_em->em_relids);
-			cur_em->em_relids = bms_del_member(cur_em->em_relids, relid);
-			cur_em->em_relids = bms_del_member(cur_em->em_relids, ojrelid);
-			if (bms_is_empty(cur_em->em_relids))
-				ec->ec_members = foreach_delete_current(ec->ec_members, lc);
-		}
-	}
-
-	/* Fix up the source clauses, in case we can re-use them later */
-	foreach(lc, ec->ec_sources)
-	{
-		RestrictInfo *rinfo = (RestrictInfo *) lfirst(lc);
-
-		remove_rel_from_restrictinfo(rinfo, relid, ojrelid);
-	}
-
-	/*
-	 * Rather than expend code on fixing up any already-derived clauses, just
-	 * drop them.  (At this point, any such clauses would be base restriction
-	 * clauses, which we'd not need anymore anyway.)
-	 */
-	ec_clear_derived_clauses(ec);
-}
-
-/*
- * Remove any references to relid or ojrelid from the PlaceHolderVars embedded
- * in a RestrictInfo's clause.
- *
- * If it's an OR clause, we must also fix up the orclause, which is a parallel
- * representation built from its own sub-RestrictInfos.  We recurse into the
- * sub-clauses for that, mirroring remove_rel_from_restrictinfo.
- */
-static void
-remove_rel_from_restrictinfo_phvs(RestrictInfo *rinfo, int relid, int ojrelid)
-{
-	rinfo->clause = (Expr *)
-		remove_rel_from_phvs((Node *) rinfo->clause, relid, ojrelid);
-
-	/* If it's an OR, recurse to clean up sub-clauses */
-	if (restriction_is_or_clause(rinfo))
-	{
-		ListCell   *lc;
-
-		Assert(is_orclause(rinfo->orclause));
-		foreach(lc, ((BoolExpr *) rinfo->orclause)->args)
-		{
-			Node	   *orarg = (Node *) lfirst(lc);
-
-			/* OR arguments should be ANDs or sub-RestrictInfos */
-			if (is_andclause(orarg))
-			{
-				List	   *andargs = ((BoolExpr *) orarg)->args;
-				ListCell   *lc2;
-
-				foreach(lc2, andargs)
-				{
-					RestrictInfo *rinfo2 = lfirst_node(RestrictInfo, lc2);
-
-					remove_rel_from_restrictinfo_phvs(rinfo2, relid, ojrelid);
-				}
-			}
-			else
-			{
-				RestrictInfo *rinfo2 = castNode(RestrictInfo, orarg);
-
-				remove_rel_from_restrictinfo_phvs(rinfo2, relid, ojrelid);
-			}
-		}
-	}
-}
-
-/*
- * Remove any references to the specified RT index(es) from the phrels (and
- * phnullingrels) of every PlaceHolderVar in the given expression.
- *
- * remove_rel_from_query() fixes up the relid sets of RestrictInfos and
- * EquivalenceMembers, but not the PlaceHolderVars embedded in their
- * expressions.  That's normally fine, but such an expression may later be
- * translated for an appendrel child and have its relids recomputed by
- * pull_varnos().  A leftover removed relid in phrels would then make
- * pull_varnos() reference a nonexistent rel, so we strip it here to match the
- * canonical PlaceHolderVar.
- */
-static Node *
-remove_rel_from_phvs(Node *node, int relid, int ojrelid)
-{
-	Relids		removable = bms_add_member(bms_make_singleton(relid), ojrelid);
-
-	return remove_rel_from_phvs_mutator(node, removable);
-}
-
-static Node *
-remove_rel_from_phvs_mutator(Node *node, Relids removable)
-{
-	if (node == NULL)
-		return NULL;
-	if (IsA(node, PlaceHolderVar))
-	{
-		PlaceHolderVar *phv = (PlaceHolderVar *) node;
-		Relids		newphrels;
-
-		/* Upper-level PlaceHolderVars should be long gone at this point */
-		Assert(phv->phlevelsup == 0);
-
-		/* Copy the PlaceHolderVar and mutate what's below ... */
-		phv = (PlaceHolderVar *)
-			expression_tree_mutator(node,
-									remove_rel_from_phvs_mutator,
-									removable);
-
-		/*
-		 * ... then strip the removed rels from its relid sets.
-		 *
-		 * If stripping would empty phrels, the PHV is evaluated only at the
-		 * removed relation(s); it then belongs to an EquivalenceMember that
-		 * the caller drops immediately afterwards.  Leave such a PHV
-		 * untouched rather than build one with empty phrels, which the rest
-		 * of the planner assumes never occurs.
-		 */
-		newphrels = bms_difference(phv->phrels, removable);
-		if (!bms_is_empty(newphrels))
-		{
-			phv->phrels = newphrels;
-			phv->phnullingrels = bms_difference(phv->phnullingrels,
-												removable);
-		}
-
-		return (Node *) phv;
-	}
-	return expression_tree_mutator(node,
-								   remove_rel_from_phvs_mutator,
-								   removable);
-}
-
-/*
- * Remove any occurrences of the target relid from a joinlist structure.
- *
- * It's easiest to build a whole new list structure, so we handle it that
- * way.  Efficiency is not a big deal here.
- *
- * *nremoved is incremented by the number of occurrences removed (there
- * should be exactly one, but the caller checks that).
- */
-static List *
-remove_rel_from_joinlist(List *joinlist, int relid, int *nremoved)
-{
-	List	   *result = NIL;
-	ListCell   *jl;
-
-	foreach(jl, joinlist)
-	{
-		Node	   *jlnode = (Node *) lfirst(jl);
-
-		if (IsA(jlnode, RangeTblRef))
-		{
-			int			varno = ((RangeTblRef *) jlnode)->rtindex;
-
-			if (varno == relid)
-				(*nremoved)++;
-			else
-				result = lappend(result, jlnode);
-		}
-		else if (IsA(jlnode, List))
-		{
-			/* Recurse to handle subproblem */
-			List	   *sublist;
-
-			sublist = remove_rel_from_joinlist((List *) jlnode,
-											   relid, nremoved);
-			/* Avoid including empty sub-lists in the result */
-			if (sublist)
-				result = lappend(result, sublist);
-		}
-		else
-		{
-			elog(ERROR, "unrecognized joinlist node type: %d",
-				 (int) nodeTag(jlnode));
-		}
-	}
-
-	return result;
-}
-
-
 /*
  * remove_join_from_jointree
  *		Delete the JoinExpr with the given RT index, along with everything
@@ -1731,7 +1014,7 @@ innerrel_is_unique(PlannerInfo *root,
  * A non-NULL extra_clauses indicates that we're checking for self-join and
  * correspondingly dealing with filtered clauses.
  */
-bool
+static bool
 innerrel_is_unique_ext(PlannerInfo *root,
 					   Relids joinrelids,
 					   Relids outerrelids,
@@ -1910,298 +1193,6 @@ is_innerrel_unique_for(PlannerInfo *root,
 	return rel_is_distinct_for(root, innerrel, clause_list, extra_clauses);
 }
 
-/*
- * Update EC members to point to the remaining relation instead of the removed
- * one, removing duplicates.
- *
- * Restriction clauses for base relations are already distributed to
- * the respective baserestrictinfo lists (see
- * generate_implied_equalities_for_column). The above code has already processed
- * this list and updated these clauses to reference the remaining
- * relation, so that we can skip them here based on their relids.
- *
- * Likewise, we have already processed the join clauses that join the
- * removed relation to the remaining one.
- *
- * Finally, there might be join clauses tying the removed relation to
- * some third relation.  We can't just delete the source clauses and
- * regenerate them from the EC because the corresponding equality
- * operators might be missing (see the handling of ec_broken).
- * Therefore, we will update the references in the source clauses.
- *
- * Derived clauses can be generated again, so it is simpler just to
- * delete them.
- */
-static void
-update_eclasses(EquivalenceClass *ec, int from, int to)
-{
-	List	   *new_members = NIL;
-	List	   *new_sources = NIL;
-
-	/*
-	 * We don't expect any EC child members to exist at this point.  Ensure
-	 * that's the case, otherwise, we might be getting asked to do something
-	 * this function hasn't been coded for.
-	 */
-	Assert(ec->ec_childmembers == NULL);
-
-	foreach_node(EquivalenceMember, em, ec->ec_members)
-	{
-		bool		is_redundant = false;
-
-		if (!bms_is_member(from, em->em_relids))
-		{
-			new_members = lappend(new_members, em);
-			continue;
-		}
-
-		em->em_relids = adjust_relid_set(em->em_relids, from, to);
-		em->em_jdomain->jd_relids = adjust_relid_set(em->em_jdomain->jd_relids, from, to);
-
-		/* We only process inner joins */
-		ChangeVarNodesExtended((Node *) em->em_expr, from, to, 0,
-							   replace_relid_callback);
-
-		foreach_node(EquivalenceMember, other, new_members)
-		{
-			if (!equal(em->em_relids, other->em_relids))
-				continue;
-
-			if (equal(em->em_expr, other->em_expr))
-			{
-				is_redundant = true;
-				break;
-			}
-		}
-
-		if (!is_redundant)
-			new_members = lappend(new_members, em);
-	}
-
-	list_free(ec->ec_members);
-	ec->ec_members = new_members;
-
-	ec_clear_derived_clauses(ec);
-
-	/* Update EC source expressions */
-	foreach_node(RestrictInfo, rinfo, ec->ec_sources)
-	{
-		bool		is_redundant = false;
-
-		if (!bms_is_member(from, rinfo->required_relids))
-		{
-			new_sources = lappend(new_sources, rinfo);
-			continue;
-		}
-
-		ChangeVarNodesExtended((Node *) rinfo, from, to, 0,
-							   replace_relid_callback);
-
-		/*
-		 * After switching the clause to the remaining relation, check it for
-		 * redundancy with existing ones. We don't have to check for
-		 * redundancy with derived clauses, because we've just deleted them.
-		 */
-		foreach_node(RestrictInfo, other, new_sources)
-		{
-			if (!equal(rinfo->clause_relids, other->clause_relids))
-				continue;
-
-			if (equal(rinfo->clause, other->clause))
-			{
-				is_redundant = true;
-				break;
-			}
-		}
-
-		if (!is_redundant)
-			new_sources = lappend(new_sources, rinfo);
-	}
-
-	list_free(ec->ec_sources);
-	ec->ec_sources = new_sources;
-	ec->ec_relids = adjust_relid_set(ec->ec_relids, from, to);
-}
-
-/*
- * "Logically" compares two RestrictInfo's ignoring the 'rinfo_serial' field,
- * which makes almost every RestrictInfo unique.  This type of comparison is
- * useful when removing duplicates while moving RestrictInfo's from removed
- * relation to remaining relation during self-join elimination.
- *
- * XXX: In the future, we might remove the 'rinfo_serial' field completely and
- * get rid of this function.
- */
-static bool
-restrict_infos_logically_equal(RestrictInfo *a, RestrictInfo *b)
-{
-	int			saved_rinfo_serial = a->rinfo_serial;
-	bool		result;
-
-	a->rinfo_serial = b->rinfo_serial;
-	result = equal(a, b);
-	a->rinfo_serial = saved_rinfo_serial;
-
-	return result;
-}
-
-/*
- * This function adds all non-redundant clauses to the keeping relation
- * during self-join elimination.  That is a contradictory operation. On the
- * one hand, we reduce the length of the `restrict` lists, which can
- * impact planning or executing time.  Additionally, we improve the
- * accuracy of cardinality estimation.  On the other hand, it is one more
- * place that can make planning time much longer in specific cases.  It
- * would have been better to avoid calling the equal() function here, but
- * it's the only way to detect duplicated inequality expressions.
- *
- * (*keep_rinfo_list) is given by pointer because it might be altered by
- * distribute_restrictinfo_to_rels().
- */
-static void
-add_non_redundant_clauses(PlannerInfo *root,
-						  List *rinfo_candidates,
-						  List **keep_rinfo_list,
-						  Index removed_relid)
-{
-	foreach_node(RestrictInfo, rinfo, rinfo_candidates)
-	{
-		bool		is_redundant = false;
-
-		Assert(!bms_is_member(removed_relid, rinfo->required_relids));
-
-		foreach_node(RestrictInfo, src, (*keep_rinfo_list))
-		{
-			if (!bms_equal(src->clause_relids, rinfo->clause_relids))
-				/* Can't compare trivially different clauses */
-				continue;
-
-			if (src == rinfo ||
-				(rinfo->parent_ec != NULL &&
-				 src->parent_ec == rinfo->parent_ec) ||
-				restrict_infos_logically_equal(rinfo, src))
-			{
-				is_redundant = true;
-				break;
-			}
-		}
-		if (!is_redundant)
-			distribute_restrictinfo_to_rels(root, rinfo);
-	}
-}
-
-/*
- * A custom callback for ChangeVarNodesExtended() providing Self-join
- * elimination (SJE) related functionality
- *
- * SJE needs to skip the RangeTblRef node type.  During SJE's last
- * step, remove_rel_from_joinlist() removes remaining RangeTblRefs
- * with target relid.  If ChangeVarNodes() replaces the target relid
- * before, remove_rel_from_joinlist() would fail to identify the nodes
- * to delete.
- *
- * SJE also needs to change the relids within RestrictInfo's.
- */
-static bool
-replace_relid_callback(Node *node, ChangeVarNodes_context *context)
-{
-	if (IsA(node, RangeTblRef))
-	{
-		return true;
-	}
-	else if (IsA(node, RestrictInfo))
-	{
-		RestrictInfo *rinfo = (RestrictInfo *) node;
-		int			relid = -1;
-		bool		is_req_equal =
-			(rinfo->required_relids == rinfo->clause_relids);
-		bool		clause_relids_is_multiple =
-			(bms_membership(rinfo->clause_relids) == BMS_MULTIPLE);
-
-		/*
-		 * Recurse down into clauses if the target relation is present in
-		 * clause_relids or required_relids.  We must check required_relids
-		 * because the relation not present in clause_relids might still be
-		 * present somewhere in orclause.
-		 */
-		if (bms_is_member(context->rt_index, rinfo->clause_relids) ||
-			bms_is_member(context->rt_index, rinfo->required_relids))
-		{
-			Relids		new_clause_relids;
-
-			ChangeVarNodesWalkExpression((Node *) rinfo->clause, context);
-			ChangeVarNodesWalkExpression((Node *) rinfo->orclause, context);
-
-			new_clause_relids = adjust_relid_set(rinfo->clause_relids,
-												 context->rt_index,
-												 context->new_index);
-
-			/*
-			 * Incrementally adjust num_base_rels based on the change of
-			 * clause_relids, which could contain both base relids and
-			 * outer-join relids.  This operation is legal until we remove
-			 * only baserels.
-			 */
-			rinfo->num_base_rels -= bms_num_members(rinfo->clause_relids) -
-				bms_num_members(new_clause_relids);
-
-			rinfo->clause_relids = new_clause_relids;
-			rinfo->left_relids =
-				adjust_relid_set(rinfo->left_relids, context->rt_index, context->new_index);
-			rinfo->right_relids =
-				adjust_relid_set(rinfo->right_relids, context->rt_index, context->new_index);
-		}
-
-		if (is_req_equal)
-			rinfo->required_relids = rinfo->clause_relids;
-		else
-			rinfo->required_relids =
-				adjust_relid_set(rinfo->required_relids, context->rt_index, context->new_index);
-
-		rinfo->outer_relids =
-			adjust_relid_set(rinfo->outer_relids, context->rt_index, context->new_index);
-		rinfo->incompatible_relids =
-			adjust_relid_set(rinfo->incompatible_relids, context->rt_index, context->new_index);
-
-		if (rinfo->mergeopfamilies &&
-			bms_get_singleton_member(rinfo->clause_relids, &relid) &&
-			clause_relids_is_multiple &&
-			relid == context->new_index && IsA(rinfo->clause, OpExpr))
-		{
-			Expr	   *leftOp;
-			Expr	   *rightOp;
-
-			leftOp = (Expr *) get_leftop(rinfo->clause);
-			rightOp = (Expr *) get_rightop(rinfo->clause);
-
-			/*
-			 * For self-join elimination, changing varnos could transform
-			 * "t1.a = t2.a" into "t1.a = t1.a".  That is always true as long
-			 * as "t1.a" is not null.  We use equal() to check for such a
-			 * case, and then we replace the qual with a check for not null
-			 * (NullTest).
-			 */
-			if (leftOp != NULL && equal(leftOp, rightOp))
-			{
-				NullTest   *ntest = makeNode(NullTest);
-
-				ntest->arg = leftOp;
-				ntest->nulltesttype = IS_NOT_NULL;
-				ntest->argisrow = false;
-				ntest->location = -1;
-				rinfo->clause = (Expr *) ntest;
-				rinfo->mergeopfamilies = NIL;
-				rinfo->left_em = NULL;
-				rinfo->right_em = NULL;
-			}
-			Assert(rinfo->orclause == NULL);
-		}
-		return true;
-	}
-
-	return false;
-}
-
 /*
  * Remove the toRemove relation after we have proven that it participates only
  * in an unneeded unique self-join with toKeep.
@@ -2604,10 +1595,9 @@ split_selfjoin_quals(PlannerInfo *root, List *joinquals, List **selfjoinquals,
 		 * when we have cast of the same var to different (but compatible)
 		 * types.
 		 */
-		ChangeVarNodesExtended(rightexpr,
-							   bms_singleton_member(rinfo->right_relids),
-							   bms_singleton_member(rinfo->left_relids), 0,
-							   replace_relid_callback);
+		ChangeVarNodes(rightexpr,
+					   bms_singleton_member(rinfo->right_relids),
+					   bms_singleton_member(rinfo->left_relids), 0);
 
 		if (equal(leftexpr, rightexpr))
 			sjoinquals = lappend(sjoinquals, rinfo);
@@ -2643,8 +1633,7 @@ match_unique_clauses(PlannerInfo *root, RelOptInfo *outer, List *uclauses,
 			   bms_is_empty(rinfo->right_relids));
 
 		clause = (Expr *) copyObject(rinfo->clause);
-		ChangeVarNodesExtended((Node *) clause, relid, outer->relid, 0,
-							   replace_relid_callback);
+		ChangeVarNodes((Node *) clause, relid, outer->relid, 0);
 
 		iclause = bms_is_empty(rinfo->left_relids) ? get_rightop(clause) :
 			get_leftop(clause);
diff --git a/src/backend/optimizer/plan/initsplan.c b/src/backend/optimizer/plan/initsplan.c
index 7b6d7249c66..d8a5c242eef 100644
--- a/src/backend/optimizer/plan/initsplan.c
+++ b/src/backend/optimizer/plan/initsplan.c
@@ -295,8 +295,6 @@ build_base_rel_tlists(PlannerInfo *root, List *final_tlist)
  *	  have a single owning relation; we keep their attr_needed info in
  *	  root->placeholder_list instead.  Find or create the associated
  *	  PlaceHolderInfo entry, and update its ph_needed.
- *
- *	  See also add_vars_to_attr_needed.
  */
 void
 add_vars_to_targetlist(PlannerInfo *root, List *vars,
@@ -350,63 +348,6 @@ add_vars_to_targetlist(PlannerInfo *root, List *vars,
 	}
 }
 
-/*
- * add_vars_to_attr_needed
- *	  This does a subset of what add_vars_to_targetlist does: it just
- *	  updates attr_needed for Vars and ph_needed for PlaceHolderVars.
- *	  We assume the Vars are already in their relations' targetlists.
- *
- *	  This is used to rebuild attr_needed/ph_needed sets after removal
- *	  of a useless outer join.  The removed join clause might have been
- *	  the only upper-level use of some other relation's Var, in which
- *	  case we can reduce that Var's attr_needed and thereby possibly
- *	  open the door to further join removals.  But we can't tell that
- *	  without tedious reconstruction of the attr_needed data.
- *
- *	  Note that if a Var's attr_needed is successfully reduced to empty,
- *	  it will still be in the relation's targetlist even though we do
- *	  not really need the scan plan node to emit it.  The extra plan
- *	  inefficiency seems tiny enough to not be worth spending planner
- *	  cycles to get rid of it.
- */
-void
-add_vars_to_attr_needed(PlannerInfo *root, List *vars,
-						Relids where_needed)
-{
-	ListCell   *temp;
-
-	Assert(!bms_is_empty(where_needed));
-
-	foreach(temp, vars)
-	{
-		Node	   *node = (Node *) lfirst(temp);
-
-		if (IsA(node, Var))
-		{
-			Var		   *var = (Var *) node;
-			RelOptInfo *rel = find_base_rel(root, var->varno);
-			int			attno = var->varattno;
-
-			if (bms_is_subset(where_needed, rel->relids))
-				continue;
-			Assert(attno >= rel->min_attr && attno <= rel->max_attr);
-			attno -= rel->min_attr;
-			rel->attr_needed[attno] = bms_add_members(rel->attr_needed[attno],
-													  where_needed);
-		}
-		else if (IsA(node, PlaceHolderVar))
-		{
-			PlaceHolderVar *phv = (PlaceHolderVar *) node;
-			PlaceHolderInfo *phinfo = find_placeholder_info(root, phv);
-
-			phinfo->ph_needed = bms_add_members(phinfo->ph_needed,
-												where_needed);
-		}
-		else
-			elog(ERROR, "unrecognized node type: %d", (int) nodeTag(node));
-	}
-}
-
 /*****************************************************************************
  *
  *	  GROUP BY
@@ -1226,54 +1167,10 @@ extract_lateral_references(PlannerInfo *root, RelOptInfo *brel, Index rtindex)
 	 */
 	add_vars_to_targetlist(root, newvars, where_needed);
 
-	/*
-	 * Remember the lateral references for rebuild_lateral_attr_needed and
-	 * create_lateral_join_info.
-	 */
+	/* Remember the lateral references for create_lateral_join_info */
 	brel->lateral_vars = newvars;
 }
 
-/*
- * rebuild_lateral_attr_needed
- *	  Put back attr_needed bits for Vars/PHVs needed for lateral references.
- *
- * This is used to rebuild attr_needed/ph_needed sets after removal of a
- * useless outer join.  It should match what find_lateral_references did,
- * except that we call add_vars_to_attr_needed not add_vars_to_targetlist.
- */
-void
-rebuild_lateral_attr_needed(PlannerInfo *root)
-{
-	Index		rti;
-
-	/* We need do nothing if the query contains no LATERAL RTEs */
-	if (!root->hasLateralRTEs)
-		return;
-
-	/* Examine the same baserels that find_lateral_references did */
-	for (rti = 1; rti < root->simple_rel_array_size; rti++)
-	{
-		RelOptInfo *brel = root->simple_rel_array[rti];
-		Relids		where_needed;
-
-		if (brel == NULL)
-			continue;
-		if (brel->reloptkind != RELOPT_BASEREL)
-			continue;
-
-		/*
-		 * We don't need to repeat all of extract_lateral_references, since it
-		 * kindly saved the extracted Vars/PHVs in lateral_vars.
-		 */
-		if (brel->lateral_vars == NIL)
-			continue;
-
-		where_needed = bms_make_singleton(rti);
-
-		add_vars_to_attr_needed(root, brel->lateral_vars, where_needed);
-	}
-}
-
 /*
  * create_lateral_join_info
  *	  Fill in the per-base-relation direct_lateral_relids, lateral_relids
@@ -3227,9 +3124,6 @@ distribute_qual_to_rels(PlannerInfo *root, Node *clause,
 	 * var propagation is ensured by making ojscope include input rels from
 	 * both sides of the join.
 	 *
-	 * See also rebuild_joinclause_attr_needed, which has to partially repeat
-	 * this work after removal of an outer join.
-	 *
 	 * Note: if the clause gets absorbed into an EquivalenceClass then this
 	 * may be unnecessary, but for now we have to do it to cover the case
 	 * where the EC becomes ec_broken and we end up reinserting the original
@@ -3803,11 +3697,6 @@ process_implied_equality(PlannerInfo *root,
 	 * some of the Vars could have missed having that done because they only
 	 * appeared in single-relation clauses originally.  So do it here for
 	 * safety.
-	 *
-	 * See also rebuild_joinclause_attr_needed, which has to partially repeat
-	 * this work after removal of an outer join.  (Since we will put this
-	 * clause into the joininfo lists, that function needn't do any extra work
-	 * to find it.)
 	 */
 	if (bms_membership(relids) == BMS_MULTIPLE)
 	{
@@ -3949,72 +3838,6 @@ get_join_domain_min_rels(PlannerInfo *root, Relids domain_relids)
 }
 
 
-/*
- * rebuild_joinclause_attr_needed
- *	  Put back attr_needed bits for Vars/PHVs needed for join clauses.
- *
- * This is used to rebuild attr_needed/ph_needed sets after removal of a
- * useless outer join.  It should match what distribute_qual_to_rels did,
- * except that we call add_vars_to_attr_needed not add_vars_to_targetlist.
- */
-void
-rebuild_joinclause_attr_needed(PlannerInfo *root)
-{
-	/*
-	 * We must examine all join clauses, but there's no value in processing
-	 * any join clause more than once.  So it's slightly annoying that we have
-	 * to find them via the per-base-relation joininfo lists.  Avoid duplicate
-	 * processing by tracking the rinfo_serial numbers of join clauses we've
-	 * already seen.  (This doesn't work for is_clone clauses, so we must
-	 * waste effort on them.)
-	 */
-	Bitmapset  *seen_serials = NULL;
-	Index		rti;
-
-	/* Scan all baserels for join clauses */
-	for (rti = 1; rti < root->simple_rel_array_size; rti++)
-	{
-		RelOptInfo *brel = root->simple_rel_array[rti];
-		ListCell   *lc;
-
-		if (brel == NULL)
-			continue;
-		if (brel->reloptkind != RELOPT_BASEREL)
-			continue;
-
-		foreach(lc, brel->joininfo)
-		{
-			RestrictInfo *rinfo = (RestrictInfo *) lfirst(lc);
-			Relids		relids = rinfo->required_relids;
-
-			if (!rinfo->is_clone)	/* else serial number is not unique */
-			{
-				if (bms_is_member(rinfo->rinfo_serial, seen_serials))
-					continue;	/* saw it already */
-				seen_serials = bms_add_member(seen_serials,
-											  rinfo->rinfo_serial);
-			}
-
-			if (bms_membership(relids) == BMS_MULTIPLE)
-			{
-				List	   *vars = pull_var_clause((Node *) rinfo->clause,
-												   PVC_RECURSE_AGGREGATES |
-												   PVC_RECURSE_WINDOWFUNCS |
-												   PVC_INCLUDE_PLACEHOLDERS);
-				Relids		where_needed;
-
-				if (rinfo->is_clone)
-					where_needed = bms_intersect(relids, root->all_baserels);
-				else
-					where_needed = relids;
-				add_vars_to_attr_needed(root, vars, where_needed);
-				list_free(vars);
-			}
-		}
-	}
-}
-
-
 /*
  * match_foreign_keys_to_quals
  *		Match foreign-key constraints to equivalence classes and join quals
diff --git a/src/backend/optimizer/util/joininfo.c b/src/backend/optimizer/util/joininfo.c
index ef2f054c39e..f75e35f22ae 100644
--- a/src/backend/optimizer/util/joininfo.c
+++ b/src/backend/optimizer/util/joininfo.c
@@ -145,39 +145,3 @@ add_join_clause_to_rels(PlannerInfo *root,
 		rel->joininfo = lappend(rel->joininfo, restrictinfo);
 	}
 }
-
-/*
- * remove_join_clause_from_rels
- *	  Delete 'restrictinfo' from all the joininfo lists it is in
- *
- * This reverses the effect of add_join_clause_to_rels.  It's used when we
- * discover that a relation need not be joined at all.
- *
- * 'restrictinfo' describes the join clause
- * 'join_relids' is the set of relations participating in the join clause
- *				 (some of these could be outer joins)
- */
-void
-remove_join_clause_from_rels(PlannerInfo *root,
-							 RestrictInfo *restrictinfo,
-							 Relids join_relids)
-{
-	int			cur_relid;
-
-	cur_relid = -1;
-	while ((cur_relid = bms_next_member(join_relids, cur_relid)) >= 0)
-	{
-		RelOptInfo *rel = find_base_rel_ignore_join(root, cur_relid);
-
-		/* We would only have added the clause to baserels */
-		if (rel == NULL)
-			continue;
-
-		/*
-		 * Remove the restrictinfo from the list.  Pointer comparison is
-		 * sufficient.
-		 */
-		Assert(list_member_ptr(rel->joininfo, restrictinfo));
-		rel->joininfo = list_delete_ptr(rel->joininfo, restrictinfo);
-	}
-}
diff --git a/src/backend/optimizer/util/placeholder.c b/src/backend/optimizer/util/placeholder.c
index dd9b11885af..07171f84f61 100644
--- a/src/backend/optimizer/util/placeholder.c
+++ b/src/backend/optimizer/util/placeholder.c
@@ -316,33 +316,6 @@ fix_placeholder_input_needed_levels(PlannerInfo *root)
 	}
 }
 
-/*
- * rebuild_placeholder_attr_needed
- *	  Put back attr_needed bits for Vars/PHVs needed in PlaceHolderVars.
- *
- * This is used to rebuild attr_needed/ph_needed sets after removal of a
- * useless outer join.  It should match what
- * fix_placeholder_input_needed_levels did, except that we call
- * add_vars_to_attr_needed not add_vars_to_targetlist.
- */
-void
-rebuild_placeholder_attr_needed(PlannerInfo *root)
-{
-	ListCell   *lc;
-
-	foreach(lc, root->placeholder_list)
-	{
-		PlaceHolderInfo *phinfo = (PlaceHolderInfo *) lfirst(lc);
-		List	   *vars = pull_var_clause((Node *) phinfo->ph_var->phexpr,
-										   PVC_RECURSE_AGGREGATES |
-										   PVC_RECURSE_WINDOWFUNCS |
-										   PVC_INCLUDE_PLACEHOLDERS);
-
-		add_vars_to_attr_needed(root, vars, phinfo->ph_eval_at);
-		list_free(vars);
-	}
-}
-
 /*
  * add_placeholders_to_base_rels
  *		Add any required PlaceHolderVars to base rels' targetlists.
diff --git a/src/backend/rewrite/rewriteManip.c b/src/backend/rewrite/rewriteManip.c
index 9c9d1cad33b..330ec596162 100644
--- a/src/backend/rewrite/rewriteManip.c
+++ b/src/backend/rewrite/rewriteManip.c
@@ -64,6 +64,7 @@ static bool contain_windowfuncs_walker(Node *node, void *context);
 static bool locate_windowfunc_walker(Node *node,
 									 locate_windowfunc_context *context);
 static bool checkExprHasSubLink_walker(Node *node, void *context);
+static Relids adjust_relid_set(Relids relids, int oldrelid, int newrelid);
 static Node *add_nulling_relids_mutator(Node *node,
 										add_nulling_relids_context *context);
 static Node *remove_nulling_relids_mutator(Node *node,
@@ -528,23 +529,27 @@ OffsetVarNodes(Node *node, int offset, int sublevels_up)
  *
  * Find all Var nodes in the given tree belonging to a specific relation
  * (identified by sublevels_up and rt_index), and change their varno fields
- * to 'new_index'.  The varnosyn fields are changed too.  Also, adjust other
- * nodes that contain rangetable indexes, such as RangeTblRef and JoinExpr.
+ * to 'new_index' (see adjust_relid_set for the exact change behavior).
+ * The varnosyn fields are changed too.  Also adjust other nodes that
+ * contain rangetable indexes, such as RangeTblRef and JoinExpr.
  *
  * NOTE: although this has the form of a walker, we cheat and modify the
  * nodes in-place.  The given expression tree should have been copied
  * earlier to ensure that no unwanted side-effects occur!
  */
 
+typedef struct
+{
+	int			rt_index;
+	int			new_index;
+	int			sublevels_up;
+} ChangeVarNodes_context;
+
 static bool
 ChangeVarNodes_walker(Node *node, ChangeVarNodes_context *context)
 {
 	if (node == NULL)
 		return false;
-
-	if (context->callback && context->callback(node, context))
-		return false;
-
 	if (IsA(node, Var))
 	{
 		Var		   *var = (Var *) node;
@@ -649,28 +654,14 @@ ChangeVarNodes_walker(Node *node, ChangeVarNodes_context *context)
 	return expression_tree_walker(node, ChangeVarNodes_walker, context);
 }
 
-/*
- * ChangeVarNodesExtended - similar to ChangeVarNodes, but with an additional
- *							'callback' param
- *
- * ChangeVarNodes changes a given node and all of its underlying nodes.  This
- * version of function additionally takes a callback, which has a chance to
- * process a node before ChangeVarNodes_walker.  A callback returns a boolean
- * value indicating if the given node should be skipped from further processing
- * by ChangeVarNodes_walker.  The callback is called only for expressions and
- * other children nodes of a Query processed by a walker.  Initial processing
- * of the root Query node doesn't invoke the callback.
- */
 void
-ChangeVarNodesExtended(Node *node, int rt_index, int new_index,
-					   int sublevels_up, ChangeVarNodes_callback callback)
+ChangeVarNodes(Node *node, int rt_index, int new_index, int sublevels_up)
 {
 	ChangeVarNodes_context context;
 
 	context.rt_index = rt_index;
 	context.new_index = new_index;
 	context.sublevels_up = sublevels_up;
-	context.callback = callback;
 
 	/*
 	 * Must be prepared to start with a Query or a bare expression tree; if
@@ -717,36 +708,6 @@ ChangeVarNodesExtended(Node *node, int rt_index, int new_index,
 		ChangeVarNodes_walker(node, &context);
 }
 
-void
-ChangeVarNodes(Node *node, int rt_index, int new_index, int sublevels_up)
-{
-	ChangeVarNodesExtended(node, rt_index, new_index, sublevels_up, NULL);
-}
-
-/*
- * ChangeVarNodesWalkExpression - process subexpression within a callback
- *								  function passed to ChangeVarNodesExtended.
- *
- * This is intended to be used by a callback that needs to recursively
- * process subexpressions of some node being visited by an outer
- * ChangeVarNodesExtended call, instead of relying on ChangeVarNodes_walker's
- * default recursion.  We invoke ChangeVarNodes_walker directly rather than
- * via expression_tree_walker, because expression_tree_walker only visits
- * child nodes and would fail to process the passed node itself --
- * for example, a bare Var node would not get its varno adjusted.
- *
- * Because this calls ChangeVarNodes_walker directly, if the passed node is
- * a Query, it will be treated as a sub-Query: sublevels_up is incremented
- * before recursing into it, and Query-level fields (resultRelation,
- * mergeTargetRelation, rowMarks, etc.) will not be adjusted.  Do not apply
- * this to a top-level Query node; use ChangeVarNodesExtended for that.
- */
-bool
-ChangeVarNodesWalkExpression(Node *node, ChangeVarNodes_context *context)
-{
-	return ChangeVarNodes_walker(node, context);
-}
-
 /*
  * adjust_relid_set - substitute newrelid for oldrelid in a Relid set
  *
@@ -756,7 +717,7 @@ ChangeVarNodesWalkExpression(Node *node, ChangeVarNodes_context *context)
  * a special varno, this function does nothing.  When newrelid is a special
  * varno, this function behaves as delete.
  */
-Relids
+static Relids
 adjust_relid_set(Relids relids, int oldrelid, int newrelid)
 {
 	if (!IS_SPECIAL_VARNO(oldrelid) && bms_is_member(oldrelid, relids))
diff --git a/src/include/optimizer/joininfo.h b/src/include/optimizer/joininfo.h
index 117195fbcfa..aa26cc670f9 100644
--- a/src/include/optimizer/joininfo.h
+++ b/src/include/optimizer/joininfo.h
@@ -23,8 +23,5 @@ extern bool have_relevant_joinclause(PlannerInfo *root,
 extern void add_join_clause_to_rels(PlannerInfo *root,
 									RestrictInfo *restrictinfo,
 									Relids join_relids);
-extern void remove_join_clause_from_rels(PlannerInfo *root,
-										 RestrictInfo *restrictinfo,
-										 Relids join_relids);
 
 #endif							/* JOININFO_H */
diff --git a/src/include/optimizer/paths.h b/src/include/optimizer/paths.h
index 17f2099ec3b..051e38c1189 100644
--- a/src/include/optimizer/paths.h
+++ b/src/include/optimizer/paths.h
@@ -137,7 +137,6 @@ extern bool process_equivalence(PlannerInfo *root,
 extern Expr *canonicalize_ec_expression(Expr *expr,
 										Oid req_type, Oid req_collation);
 extern void reconsider_outer_join_clauses(PlannerInfo *root);
-extern void rebuild_eclass_attr_needed(PlannerInfo *root);
 extern EquivalenceClass *get_eclass_for_sort_expr(PlannerInfo *root,
 												  Expr *expr,
 												  List *opfamilies,
@@ -208,7 +207,6 @@ extern bool eclass_useful_for_merging(PlannerInfo *root,
 extern bool is_redundant_derived_clause(RestrictInfo *rinfo, List *clauselist);
 extern bool is_redundant_with_indexclauses(RestrictInfo *rinfo,
 										   List *indexclauses);
-extern void ec_clear_derived_clauses(EquivalenceClass *ec);
 
 /*
  * pathkeys.c
diff --git a/src/include/optimizer/placeholder.h b/src/include/optimizer/placeholder.h
index 60798281090..2099075e670 100644
--- a/src/include/optimizer/placeholder.h
+++ b/src/include/optimizer/placeholder.h
@@ -23,7 +23,6 @@ extern PlaceHolderInfo *find_placeholder_info(PlannerInfo *root,
 											  PlaceHolderVar *phv);
 extern void find_placeholders_in_jointree(PlannerInfo *root);
 extern void fix_placeholder_input_needed_levels(PlannerInfo *root);
-extern void rebuild_placeholder_attr_needed(PlannerInfo *root);
 extern void add_placeholders_to_base_rels(PlannerInfo *root);
 extern void add_placeholders_to_joinrel(PlannerInfo *root, RelOptInfo *joinrel,
 										RelOptInfo *outer_rel, RelOptInfo *inner_rel,
diff --git a/src/include/optimizer/planmain.h b/src/include/optimizer/planmain.h
index 85301d912f0..f7e22f5989d 100644
--- a/src/include/optimizer/planmain.h
+++ b/src/include/optimizer/planmain.h
@@ -73,12 +73,9 @@ extern void add_other_rels_to_query(PlannerInfo *root);
 extern void build_base_rel_tlists(PlannerInfo *root, List *final_tlist);
 extern void add_vars_to_targetlist(PlannerInfo *root, List *vars,
 								   Relids where_needed);
-extern void add_vars_to_attr_needed(PlannerInfo *root, List *vars,
-									Relids where_needed);
 extern void remove_useless_groupby_columns(PlannerInfo *root);
 extern void setup_eager_aggregation(PlannerInfo *root);
 extern void find_lateral_references(PlannerInfo *root);
-extern void rebuild_lateral_attr_needed(PlannerInfo *root);
 extern void create_lateral_join_info(PlannerInfo *root);
 extern List *deconstruct_jointree(PlannerInfo *root);
 extern bool restriction_is_always_true(PlannerInfo *root,
@@ -102,7 +99,6 @@ extern RestrictInfo *build_implied_join_equality(PlannerInfo *root,
 												 Expr *item2,
 												 Relids qualscope,
 												 Index security_level);
-extern void rebuild_joinclause_attr_needed(PlannerInfo *root);
 extern void match_foreign_keys_to_quals(PlannerInfo *root);
 
 /*
@@ -115,10 +111,6 @@ extern bool query_is_distinct_for(Query *query, List *distinct_cols);
 extern bool innerrel_is_unique(PlannerInfo *root,
 							   Relids joinrelids, Relids outerrelids, RelOptInfo *innerrel,
 							   JoinType jointype, List *restrictlist, bool force_cache);
-extern bool innerrel_is_unique_ext(PlannerInfo *root, Relids joinrelids,
-								   Relids outerrelids, RelOptInfo *innerrel,
-								   JoinType jointype, List *restrictlist,
-								   bool force_cache, List **extra_clauses);
 extern bool remove_useless_self_joins(PlannerInfo *root, List *joinlist);
 
 /*
diff --git a/src/include/rewrite/rewriteManip.h b/src/include/rewrite/rewriteManip.h
index 9d234cb8741..2d15d8f8f95 100644
--- a/src/include/rewrite/rewriteManip.h
+++ b/src/include/rewrite/rewriteManip.h
@@ -41,30 +41,11 @@ typedef enum ReplaceVarsNoMatchOption
 	REPLACEVARS_SUBSTITUTE_NULL,	/* replace with a NULL Const */
 } ReplaceVarsNoMatchOption;
 
-typedef struct ChangeVarNodes_context ChangeVarNodes_context;
-
-typedef bool (*ChangeVarNodes_callback) (Node *node,
-										 ChangeVarNodes_context *arg);
-
-struct ChangeVarNodes_context
-{
-	int			rt_index;
-	int			new_index;
-	int			sublevels_up;
-	ChangeVarNodes_callback callback;
-};
-
-pg_nodiscard extern Relids adjust_relid_set(Relids relids, int oldrelid, int newrelid);
 extern void CombineRangeTables(List **dst_rtable, List **dst_perminfos,
 							   List *src_rtable, List *src_perminfos);
 extern void OffsetVarNodes(Node *node, int offset, int sublevels_up);
 extern void ChangeVarNodes(Node *node, int rt_index, int new_index,
 						   int sublevels_up);
-extern void ChangeVarNodesExtended(Node *node, int rt_index, int new_index,
-								   int sublevels_up,
-								   ChangeVarNodes_callback callback);
-extern bool ChangeVarNodesWalkExpression(Node *node,
-										 ChangeVarNodes_context *context);
 extern void IncrementVarSublevelsUp(Node *node, int delta_sublevels_up,
 									int min_sublevels_up);
 extern void IncrementVarSublevelsUp_rtable(List *rtable,
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 56c1f997f88..41ceb9b160d 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -431,7 +431,6 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
-ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
 CheckPoint
-- 
2.52.0

Reply via email to