From 7f51d1ca7cd0c8bb14eb2e5e87b2e53bbc75a6f0 Mon Sep 17 00:00:00 2001
From: Rui Zhao <zhaorui126@gmail.com>
Date: Tue, 11 Aug 2026 14:43:01 +0800
Subject: [PATCH] Derive the safe quals' non-nullable Vars once per jointree
 node

v2 shares one SafeQualsInfo among the SubLinks at a jointree node, but the
quals it hands to the derivation are every safe qual at or below that node.
A NOT IN at level k therefore runs flatten_join_alias_vars over the quals of
all k levels below it, and flatten_join_alias_vars copies what it walks,
sub-selects included.  With a NOT IN in each of N join ON clauses that is
N(N+1)/2 copies of a qual instead of N, and nothing frees them until planning
is done.

Link each node's SafeQualsInfo to its children instead of copying their quals
into it, and compute the non-nullable Vars bottom-up, caching per node.  A
node's set is the union of its own qual's and its children's, which is what
find_nonnullable_vars returns for all of them at once anyway, since it takes
the union across an implicit-AND list.  Each qual is then flattened at most
once no matter how many nodes above it ask, and the derivation stays lazy:
nothing is computed unless some NOT IN actually needs it.

No behavior change: the same set of Vars is proven non-null, so the same
conversions happen.
---
 src/backend/optimizer/plan/subselect.c    |  68 +++++++++----
 src/backend/optimizer/prep/prepjointree.c | 118 +++++++++++++---------
 src/include/optimizer/subselect.h         |  12 ++-
 3 files changed, 130 insertions(+), 68 deletions(-)

diff --git a/src/backend/optimizer/plan/subselect.c b/src/backend/optimizer/plan/subselect.c
index 595a6ceac5e..08d860a6533 100644
--- a/src/backend/optimizer/plan/subselect.c
+++ b/src/backend/optimizer/plan/subselect.c
@@ -94,6 +94,8 @@ static void inline_cte(PlannerInfo *root, CommonTableExpr *cte);
 static bool inline_cte_walker(Node *node, inline_cte_walker_context *context);
 static bool sublink_testexpr_is_not_nullable(PlannerInfo *root, SubLink *sublink,
 											 SafeQualsInfo *sqinfo);
+static List *safe_quals_nonnullable_vars(PlannerInfo *root,
+										 SafeQualsInfo *sqinfo);
 static bool simplify_EXISTS_query(PlannerInfo *root, Query *query);
 static Query *convert_EXISTS_to_ANY(PlannerInfo *root, Query *subselect,
 									Node **testexpr, List **paramIds);
@@ -1609,33 +1611,17 @@ sublink_testexpr_is_not_nullable(PlannerInfo *root, SubLink *sublink,
 		 * SafeQualsInfo among all the SubLinks at a jointree node, so we do
 		 * this once for all of them.
 		 *
-		 * Note that the quals must be run through flatten_join_alias_vars,
-		 * just as the outer expressions were above, so that the Vars match
-		 * up.
-		 *
 		 * Note also that we can only prove things about this query's own
 		 * Vars.
 		 */
-		if (sqinfo != NULL && sqinfo->quals != NIL &&
+		if (sqinfo != NULL &&
 			IsA(expr, Var) && ((Var *) expr)->varlevelsup == 0)
 		{
 			Var		   *var = (Var *) expr;
 
-			if (!sqinfo->computed)
-			{
-				List	   *flat_quals;
-
-				flat_quals = (List *)
-					flatten_join_alias_vars(root, root->parse,
-											(Node *) sqinfo->quals);
-				sqinfo->nonnullable_vars =
-					find_nonnullable_vars((Node *) flat_quals);
-				sqinfo->computed = true;
-			}
-
 			if (mbms_is_member(var->varno,
 							   var->varattno - FirstLowInvalidHeapAttributeNumber,
-							   sqinfo->nonnullable_vars))
+							   safe_quals_nonnullable_vars(root, sqinfo)))
 				continue;
 		}
 
@@ -1646,6 +1632,52 @@ sublink_testexpr_is_not_nullable(PlannerInfo *root, SubLink *sublink,
 	return true;
 }
 
+/*
+ * safe_quals_nonnullable_vars: the Vars sqinfo's quals force non-null
+ *
+ * The result is the union of what this jointree node's own qual forces
+ * non-null and what the nodes below it do.  That is the same set
+ * find_nonnullable_vars would return for all of those quals at once, since it
+ * takes the union across an implicit-AND list, but computing it per node lets
+ * us flatten each qual exactly once no matter how many nodes above it ask.
+ *
+ * The quals must be run through flatten_join_alias_vars, just as the outer
+ * expressions in sublink_testexpr_is_not_nullable were, so that the Vars match
+ * up.
+ *
+ * Each node's set is computed on first use and cached, so the SubLinks at one
+ * node, and the nodes above it, share the work.
+ */
+static List *
+safe_quals_nonnullable_vars(PlannerInfo *root, SafeQualsInfo *sqinfo)
+{
+	/* Since this function recurses, it could be driven to stack overflow. */
+	check_stack_depth();
+
+	if (!sqinfo->computed)
+	{
+		List	   *vars = NIL;
+
+		foreach_ptr(SafeQualsInfo, child, sqinfo->children)
+			vars = mbms_add_members(vars,
+									safe_quals_nonnullable_vars(root, child));
+
+		if (sqinfo->quals != NULL)
+		{
+			Node	   *flat_quals;
+
+			flat_quals = flatten_join_alias_vars(root, root->parse,
+												 sqinfo->quals);
+			vars = mbms_add_members(vars, find_nonnullable_vars(flat_quals));
+		}
+
+		sqinfo->nonnullable_vars = vars;
+		sqinfo->computed = true;
+	}
+
+	return sqinfo->nonnullable_vars;
+}
+
 /*
  * convert_EXISTS_sublink_to_join: try to convert an EXISTS SubLink to a join
  *
diff --git a/src/backend/optimizer/prep/prepjointree.c b/src/backend/optimizer/prep/prepjointree.c
index 97ccd294bf4..61179764926 100644
--- a/src/backend/optimizer/prep/prepjointree.c
+++ b/src/backend/optimizer/prep/prepjointree.c
@@ -109,8 +109,11 @@ static Query *expand_virtual_generated_columns(PlannerInfo *root, Query *parse,
 											   RangeTblEntry *rte, int rt_index,
 											   Relation relation);
 static Node *pull_up_sublinks_jointree_recurse(PlannerInfo *root, Node *jtnode,
-											   Relids *relids, List **safe_quals);
-static void init_safe_quals_info(SafeQualsInfo *sqinfo, List *quals);
+											   Relids *relids,
+											   SafeQualsInfo **safe_quals);
+static SafeQualsInfo *make_safe_quals_info(Node *quals, List *children);
+static List *safe_quals_children(SafeQualsInfo *child1,
+								 SafeQualsInfo *child2);
 static Node *pull_up_sublinks_qual_recurse(PlannerInfo *root, Node *node,
 										   Node **jtlink1, Relids available_rels1,
 										   Node **jtlink2, Relids available_rels2,
@@ -675,7 +678,7 @@ pull_up_sublinks(PlannerInfo *root)
 {
 	Node	   *jtnode;
 	Relids		relids;
-	List	   *safe_quals;
+	SafeQualsInfo *safe_quals;
 
 	/* Begin recursion through the jointree */
 	jtnode = pull_up_sublinks_jointree_recurse(root,
@@ -698,13 +701,16 @@ pull_up_sublinks(PlannerInfo *root)
  * In addition to returning the possibly-modified jointree node, we return
  * a relids set of the contained rels into *relids.
  *
- * We also return into *safe_quals the WHERE/ON quals at or below this node
- * that constrain rels not below the nullable side of any outer join, which
- * can be used to prove an expression evaluated at our level non-nullable.
+ * We also return into *safe_quals a SafeQualsInfo describing the WHERE/ON
+ * quals at or below this node that constrain rels not below the nullable side
+ * of any outer join, which can be used to prove an expression evaluated at our
+ * level non-nullable, or NULL if there are none.  The nodes are linked to
+ * their children rather than having each level accumulate a flat qual list,
+ * so that a qual is flattened at most once however many levels above it ask.
  */
 static Node *
 pull_up_sublinks_jointree_recurse(PlannerInfo *root, Node *jtnode,
-								  Relids *relids, List **safe_quals)
+								  Relids *relids, SafeQualsInfo **safe_quals)
 {
 	/* Since this function recurses, it could be driven to stack overflow. */
 	check_stack_depth();
@@ -712,14 +718,14 @@ pull_up_sublinks_jointree_recurse(PlannerInfo *root, Node *jtnode,
 	if (jtnode == NULL)
 	{
 		*relids = NULL;
-		*safe_quals = NIL;
+		*safe_quals = NULL;
 	}
 	else if (IsA(jtnode, RangeTblRef))
 	{
 		int			varno = ((RangeTblRef *) jtnode)->rtindex;
 
 		*relids = bms_make_singleton(varno);
-		*safe_quals = NIL;
+		*safe_quals = NULL;
 		/* jtnode is returned unmodified */
 	}
 	else if (IsA(jtnode, FromExpr))
@@ -727,8 +733,8 @@ pull_up_sublinks_jointree_recurse(PlannerInfo *root, Node *jtnode,
 		FromExpr   *f = (FromExpr *) jtnode;
 		List	   *newfromlist = NIL;
 		Relids		frelids = NULL;
-		List	   *fsafequals = NIL;
-		SafeQualsInfo sqinfo;
+		List	   *childinfos = NIL;
+		SafeQualsInfo *sqinfo;
 		FromExpr   *newf;
 		Node	   *jtlink;
 		ListCell   *l;
@@ -741,7 +747,7 @@ pull_up_sublinks_jointree_recurse(PlannerInfo *root, Node *jtnode,
 		{
 			Node	   *newchild;
 			Relids		childrelids;
-			List	   *childsafequals;
+			SafeQualsInfo *childsafequals;
 
 			newchild = pull_up_sublinks_jointree_recurse(root,
 														 lfirst(l),
@@ -749,22 +755,21 @@ pull_up_sublinks_jointree_recurse(PlannerInfo *root, Node *jtnode,
 														 &childsafequals);
 			newfromlist = lappend(newfromlist, newchild);
 			frelids = bms_join(frelids, childrelids);
-			fsafequals = list_concat(fsafequals, childsafequals);
+			if (childsafequals)
+				childinfos = lappend(childinfos, childsafequals);
 		}
 		/* This level's WHERE quals are safe to use as well. */
-		if (f->quals)
-			fsafequals = lappend(fsafequals, f->quals);
+		sqinfo = make_safe_quals_info(f->quals, childinfos);
 
 		/* Build the replacement FromExpr; no quals yet */
 		newf = makeFromExpr(newfromlist, NULL);
 		/* Set up a link representing the rebuilt jointree */
 		jtlink = (Node *) newf;
 		/* Now process qual --- all children are available for use */
-		init_safe_quals_info(&sqinfo, fsafequals);
 		newf->quals = pull_up_sublinks_qual_recurse(root, f->quals,
 													&jtlink, frelids,
 													NULL, NULL,
-													&sqinfo);
+													sqinfo);
 
 		/*
 		 * Note that the result will be either newf, or a stack of JoinExprs
@@ -776,7 +781,7 @@ pull_up_sublinks_jointree_recurse(PlannerInfo *root, Node *jtnode,
 		 * outputs anyway.
 		 */
 		*relids = frelids;
-		*safe_quals = fsafequals;
+		*safe_quals = sqinfo;
 		jtnode = jtlink;
 	}
 	else if (IsA(jtnode, JoinExpr))
@@ -784,10 +789,9 @@ pull_up_sublinks_jointree_recurse(PlannerInfo *root, Node *jtnode,
 		JoinExpr   *j;
 		Relids		leftrelids;
 		Relids		rightrelids;
-		List	   *leftsafequals;
-		List	   *rightsafequals;
-		List	   *passquals;
-		SafeQualsInfo sqinfo;
+		SafeQualsInfo *leftsafequals;
+		SafeQualsInfo *rightsafequals;
+		SafeQualsInfo *sqinfo;
 		Node	   *jtlink;
 
 		/*
@@ -827,19 +831,17 @@ pull_up_sublinks_jointree_recurse(PlannerInfo *root, Node *jtnode,
 				 * quals constrain its output, so all of them are safe here
 				 * and above.
 				 */
-				passquals = list_concat(leftsafequals, rightsafequals);
-				if (j->quals)
-					passquals = lappend(passquals, j->quals);
-
-				init_safe_quals_info(&sqinfo, passquals);
+				sqinfo = make_safe_quals_info(j->quals,
+											  safe_quals_children(leftsafequals,
+																  rightsafequals));
 				j->quals = pull_up_sublinks_qual_recurse(root, j->quals,
 														 &jtlink,
 														 bms_union(leftrelids,
 																   rightrelids),
 														 NULL, NULL,
-														 &sqinfo);
+														 sqinfo);
 
-				*safe_quals = passquals;
+				*safe_quals = sqinfo;
 				break;
 			case JOIN_LEFT:
 
@@ -850,35 +852,31 @@ pull_up_sublinks_jointree_recurse(PlannerInfo *root, Node *jtnode,
 				 * safe for this node's own quals, since a sublink pulled up
 				 * here goes into the right side.
 				 */
-				passquals = rightsafequals;
-				if (j->quals)
-					passquals = lappend(passquals, j->quals);
-
-				init_safe_quals_info(&sqinfo, passquals);
+				sqinfo = make_safe_quals_info(j->quals,
+											  safe_quals_children(rightsafequals,
+																  NULL));
 				j->quals = pull_up_sublinks_qual_recurse(root, j->quals,
 														 &j->rarg,
 														 rightrelids,
 														 NULL, NULL,
-														 &sqinfo);
+														 sqinfo);
 
 				*safe_quals = leftsafequals;
 				break;
 			case JOIN_FULL:
 				/* can't do anything with full-join quals */
-				*safe_quals = NIL;
+				*safe_quals = NULL;
 				break;
 			case JOIN_RIGHT:
 				/* Mirror of the JOIN_LEFT case */
-				passquals = leftsafequals;
-				if (j->quals)
-					passquals = lappend(passquals, j->quals);
-
-				init_safe_quals_info(&sqinfo, passquals);
+				sqinfo = make_safe_quals_info(j->quals,
+											  safe_quals_children(leftsafequals,
+																  NULL));
 				j->quals = pull_up_sublinks_qual_recurse(root, j->quals,
 														 &j->larg,
 														 leftrelids,
 														 NULL, NULL,
-														 &sqinfo);
+														 sqinfo);
 
 				*safe_quals = rightsafequals;
 				break;
@@ -909,13 +907,41 @@ pull_up_sublinks_jointree_recurse(PlannerInfo *root, Node *jtnode,
 
 /*
  * Set up a jointree node's safe quals for convert_ANY_sublink_to_join.
+ *
+ * quals is the node's own qual clause, children the SafeQualsInfos of the
+ * nodes below it whose quals also apply here.  Returns NULL if there is
+ * nothing to say about this node.
  */
-static void
-init_safe_quals_info(SafeQualsInfo *sqinfo, List *quals)
+static SafeQualsInfo *
+make_safe_quals_info(Node *quals, List *children)
 {
+	SafeQualsInfo *sqinfo;
+
+	if (quals == NULL && children == NIL)
+		return NULL;
+
+	sqinfo = palloc_object(SafeQualsInfo);
 	sqinfo->quals = quals;
+	sqinfo->children = children;
 	sqinfo->nonnullable_vars = NIL;
 	sqinfo->computed = false;
+	return sqinfo;
+}
+
+/*
+ * Build a children list for make_safe_quals_info, dropping the empty ones.
+ * Pass NULL for child2 where only one child's quals apply.
+ */
+static List *
+safe_quals_children(SafeQualsInfo *child1, SafeQualsInfo *child2)
+{
+	List	   *result = NIL;
+
+	if (child1)
+		result = lappend(result, child1);
+	if (child2)
+		result = lappend(result, child2);
+	return result;
 }
 
 /*
@@ -954,7 +980,7 @@ pull_up_sublinks_qual_recurse(PlannerInfo *root, Node *node,
 		SubLink    *sublink = (SubLink *) node;
 		JoinExpr   *j;
 		Relids		child_rels;
-		List	   *child_safequals;
+		SafeQualsInfo *child_safequals;
 
 		/* Is it a convertible ANY or EXISTS clause? */
 		if (sublink->subLinkType == ANY_SUBLINK)
@@ -1095,7 +1121,7 @@ pull_up_sublinks_qual_recurse(PlannerInfo *root, Node *node,
 		SubLink    *sublink = (SubLink *) get_notclausearg((Expr *) node);
 		JoinExpr   *j;
 		Relids		child_rels;
-		List	   *child_safequals;
+		SafeQualsInfo *child_safequals;
 
 		if (sublink && IsA(sublink, SubLink))
 		{
diff --git a/src/include/optimizer/subselect.h b/src/include/optimizer/subselect.h
index 4481b75b65e..8fa3c39e963 100644
--- a/src/include/optimizer/subselect.h
+++ b/src/include/optimizer/subselect.h
@@ -18,13 +18,17 @@
 
 /*
  * The quals that are guaranteed to filter the rows on which the SubLinks at
- * one jointree node are evaluated, and the Vars they force non-null.  The
- * latter is computed on first use and then shared by every SubLink at that
- * node.
+ * one jointree node are evaluated, and the Vars they force non-null.
+ *
+ * quals is the qual clause of the jointree node itself; children are the
+ * SafeQualsInfos of the nodes below it whose quals also apply here.  The Var
+ * set is computed on first use and then shared by every SubLink at that node,
+ * and by the nodes above it.
  */
 typedef struct SafeQualsInfo
 {
-	List	   *quals;			/* qual clauses, with implicit-AND semantics */
+	Node	   *quals;			/* this node's own WHERE or ON qual, or NULL */
+	List	   *children;		/* child SafeQualsInfos whose quals apply */
 	List	   *nonnullable_vars;	/* multibitmapset; valid if computed */
 	bool		computed;		/* have we computed nonnullable_vars yet? */
 } SafeQualsInfo;
-- 
2.43.7

