From fa0225107dd0f79976841ce2c9bd41c26fd2ded9 Mon Sep 17 00:00:00 2001
From: Richard Guo <guofenglinux@gmail.com>
Date: Wed, 19 Aug 2026 09:44:11 +0900
Subject: [PATCH v1] Collect quals for outer-join reduction on demand

To prove that a forced-null Var is non-nullable in every matching row,
reduce_outer_joins() uses the strict quals that hold for every row a
subtree emits.  The first pass currently gathers those quals into each
subtree's state, so that the second pass need not re-walk the jointree
at decision time.

That duplicates find_subquery_safe_quals(), which already performs
this traversal for query_outputs_are_not_nullable(), and it charges
every query containing an outer join for a list build at every
FromExpr and every inner or semi join, whether or not any upper qual
turns out to force a Var null.  Since each level copies what its
children collected, the space required grows quadratically with the
depth of nested inner joins.  What it buys is small even then: the
walk it avoids is cheap enough to be called on demand.

So rename find_subquery_safe_quals() to find_safe_quals(), export it,
and drop safe_quals from the first pass's state in favor of the
subtree's jointree node.  The second pass calls find_safe_quals() at
the point where it needs the proof.

Also stop treating a semijoin there like a left join: semijoins are
reachable from the new caller, and a semijoin emits only matched
left-hand rows, so its ON quals do hold for every output row.
---
 src/backend/optimizer/prep/prepjointree.c | 61 +++++------------------
 src/backend/optimizer/util/clauses.c      | 54 ++++++++++++--------
 src/include/optimizer/clauses.h           |  1 +
 3 files changed, 45 insertions(+), 71 deletions(-)

diff --git a/src/backend/optimizer/prep/prepjointree.c b/src/backend/optimizer/prep/prepjointree.c
index 9d9483c08bb..5931501e470 100644
--- a/src/backend/optimizer/prep/prepjointree.c
+++ b/src/backend/optimizer/prep/prepjointree.c
@@ -90,11 +90,7 @@ typedef struct reduce_outer_joins_pass1_state
 	bool		contains_outer; /* does subtree contain outer join(s)? */
 	Relids		nullable_rels;	/* base relids that are nullable within this
 								 * subtree */
-	List	   *safe_quals;		/* quals (implicit-AND) that are applied to
-								 * every output row of this subtree, and so
-								 * can be used to prove non-nullability of its
-								 * outputs.  May be shared with a child
-								 * state's list; don't modify in place. */
+	Node	   *jtnode;			/* the jointree node this state describes */
 	List	   *sub_states;		/* List of states for subtree components */
 } reduce_outer_joins_pass1_state;
 
@@ -3354,7 +3350,7 @@ reduce_outer_joins_pass1(Node *jtnode)
 	result->relids = NULL;
 	result->contains_outer = false;
 	result->nullable_rels = NULL;
-	result->safe_quals = NIL;
+	result->jtnode = jtnode;
 	result->sub_states = NIL;
 
 	if (jtnode == NULL)
@@ -3380,15 +3376,8 @@ reduce_outer_joins_pass1(Node *jtnode)
 			result->contains_outer |= sub_state->contains_outer;
 			result->nullable_rels = bms_add_members(result->nullable_rels,
 													sub_state->nullable_rels);
-			/* All of a FROM item's safe quals are safe at this level too */
-			result->safe_quals = list_concat(result->safe_quals,
-											 sub_state->safe_quals);
 			result->sub_states = lappend(result->sub_states, sub_state);
 		}
-		/* ... and so are this FromExpr's own WHERE quals */
-		if (f->quals)
-			result->safe_quals = list_concat(result->safe_quals,
-											 (List *) f->quals);
 	}
 	else if (IsA(jtnode, JoinExpr))
 	{
@@ -3411,54 +3400,27 @@ reduce_outer_joins_pass1(Node *jtnode)
 		{
 			case JOIN_INNER:
 			case JOIN_SEMI:
-
-				/*
-				 * No new nullability; propagate state from children.  Both
-				 * children's quals, plus our own ON quals, hold for every
-				 * output row.  (At a semijoin the RHS's quals are included
-				 * too, harmlessly, since nothing above can reference its
-				 * Vars.)
-				 */
+				/* No new nullability; propagate state from children */
 				result->contains_outer = left_state->contains_outer ||
 					right_state->contains_outer;
 				result->nullable_rels = bms_union(left_state->nullable_rels,
 												  right_state->nullable_rels);
-				result->safe_quals = list_concat_copy(left_state->safe_quals,
-													  right_state->safe_quals);
-				if (j->quals)
-					result->safe_quals = list_concat(result->safe_quals,
-													 (List *) j->quals);
 				break;
 			case JOIN_LEFT:
 			case JOIN_ANTI:
-
-				/*
-				 * RHS is nullable; LHS keeps existing status.  A
-				 * null-extended row satisfies neither the ON quals nor the
-				 * RHS's own quals, so only the LHS's quals survive.
-				 */
+				/* RHS is nullable; LHS keeps existing status */
 				result->contains_outer = true;
 				result->nullable_rels = bms_union(left_state->nullable_rels,
 												  right_state->relids);
-				result->safe_quals = left_state->safe_quals;
 				break;
 			case JOIN_RIGHT:
-
-				/*
-				 * LHS is nullable; RHS keeps existing status.  Symmetrically,
-				 * only the RHS's quals survive.
-				 */
+				/* LHS is nullable; RHS keeps existing status */
 				result->contains_outer = true;
 				result->nullable_rels = bms_union(left_state->relids,
 												  right_state->nullable_rels);
-				result->safe_quals = right_state->safe_quals;
 				break;
 			case JOIN_FULL:
-
-				/*
-				 * Both sides are nullable, so no qual is guaranteed to hold
-				 * for every output row; safe_quals stays NIL.
-				 */
+				/* Both sides are nullable */
 				result->contains_outer = true;
 				result->nullable_rels = bms_union(left_state->relids,
 												  right_state->relids);
@@ -3926,9 +3888,9 @@ forced_null_var_is_attnotnull(PlannerInfo *root, List *forced_null_vars,
  *		actually non-nullable in every row that the given subtree emits.
  *
  * We prove non-nullness from quals that hold for every such row: the subtree's
- * collected safe_quals, plus any "extra_quals" the caller knows also constrain
- * the Var, or a NOT NULL table constraint (excluding Vars nullable due to
- * lower-level outer joins).
+ * safe quals, plus any "extra_quals" the caller knows also constrain the Var,
+ * or a NOT NULL table constraint (excluding Vars nullable due to lower-level
+ * outer joins).
  *
  * A whole-row Var in "forced_null_vars" requires, in any matching row, every
  * column of its relation to be NULL, so it is refuted by proving any one of
@@ -3941,12 +3903,13 @@ forced_null_var_is_nonnullable(PlannerInfo *root, List *forced_null_vars,
 							   reduce_outer_joins_pass1_state *state,
 							   List *extra_quals)
 {
-	List	   *all_quals;
+	List	   *all_quals = NIL;
 	List	   *nonnullable_vars;
 	int			wholerow_attno = 0 - FirstLowInvalidHeapAttributeNumber;
 	int			varno = -1;
 
-	all_quals = list_concat_copy(state->safe_quals, extra_quals);
+	find_safe_quals(state->jtnode, &all_quals);
+	all_quals = list_concat(all_quals, extra_quals);
 	nonnullable_vars = find_nonnullable_vars((Node *) all_quals);
 
 	/*
diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c
index e7f14677583..8da4ed617b5 100644
--- a/src/backend/optimizer/util/clauses.c
+++ b/src/backend/optimizer/util/clauses.c
@@ -126,7 +126,6 @@ static bool contain_context_dependent_node_walker(Node *node, int *flags);
 static bool contain_leaked_vars_walker(Node *node, void *context);
 static Relids find_nonnullable_rels_walker(Node *node, bool top_level);
 static List *find_nonnullable_vars_walker(Node *node, bool top_level);
-static void find_subquery_safe_quals(Node *jtnode, List **safe_quals);
 static bool is_strict_saop(ScalarArrayOpExpr *expr, bool falseOK);
 static bool convert_saop_to_hashed_saop_walker(Node *node, void *context);
 static bool grouping_conflict_walker(Node *node, grouping_walker_ctx *ctx);
@@ -2165,9 +2164,9 @@ query_outputs_are_not_nullable(Query *query)
 		 * can wrap join alias Vars.
 		 *
 		 * We must also apply flatten_join_alias_vars to the quals extracted
-		 * by find_subquery_safe_quals.  We do not need to apply
-		 * flatten_group_exprs to these quals, though, because grouping Vars
-		 * cannot appear in jointree quals.
+		 * by find_safe_quals.  We do not need to apply flatten_group_exprs to
+		 * these quals, though, because grouping Vars cannot appear in
+		 * jointree quals.
 		 */
 
 		/*
@@ -2208,7 +2207,7 @@ query_outputs_are_not_nullable(Query *query)
 			 */
 			if (!computed_nonnullable_vars)
 			{
-				find_subquery_safe_quals((Node *) query->jointree, &safe_quals);
+				find_safe_quals((Node *) query->jointree, &safe_quals);
 				safe_quals = (List *)
 					flatten_join_alias_vars(NULL, query, (Node *) safe_quals);
 				nonnullable_vars = find_nonnullable_vars((Node *) safe_quals);
@@ -2231,18 +2230,24 @@ query_outputs_are_not_nullable(Query *query)
 }
 
 /*
- * find_subquery_safe_quals
+ * find_safe_quals
  *		Traverse jointree to locate quals on non-outerjoined-rels.
  *
  * We locate all WHERE and JOIN/ON quals that constrain the rels that are not
  * below the nullable side of any outer join, and add them to the *safe_quals
- * list (forming a list with implicit-AND semantics).  These quals can be used
- * to prove non-nullability of the subquery's outputs.
+ * list (forming a list with implicit-AND semantics).  These quals hold for
+ * every row the jointree emits, so they can be used to prove non-nullability
+ * of its outputs.
+ *
+ * The caller may pass a whole jointree or any subtree of one, with quals
+ * either raw or already preprocessed into implicit-AND lists.  The result
+ * therefore may contain both bare expressions and nested lists, which
+ * find_nonnullable_vars() reads as implicit-AND in either case.
  *
  * Top-level caller must initialize *safe_quals to NIL.
  */
-static void
-find_subquery_safe_quals(Node *jtnode, List **safe_quals)
+void
+find_safe_quals(Node *jtnode, List **safe_quals)
 {
 	if (jtnode == NULL)
 		return;
@@ -2257,7 +2262,7 @@ find_subquery_safe_quals(Node *jtnode, List **safe_quals)
 
 		/* All elements of the FROM list are allowable */
 		foreach_ptr(Node, child_node, f->fromlist)
-			find_subquery_safe_quals(child_node, safe_quals);
+			find_safe_quals(child_node, safe_quals);
 		/* ... and its WHERE quals are too */
 		if (f->quals)
 			*safe_quals = lappend(*safe_quals, f->quals);
@@ -2269,30 +2274,35 @@ find_subquery_safe_quals(Node *jtnode, List **safe_quals)
 		switch (j->jointype)
 		{
 			case JOIN_INNER:
-				/* visit both children */
-				find_subquery_safe_quals(j->larg, safe_quals);
-				find_subquery_safe_quals(j->rarg, safe_quals);
-				/* and grab the ON quals too */
+			case JOIN_SEMI:
+
+				/*
+				 * Visit both children, and grab the ON quals too.  A semijoin
+				 * emits only matched left-hand rows, so its quals hold for
+				 * every output row as well.  (Its right-hand side's quals are
+				 * collected too; that's harmless, since nothing above can
+				 * reference that side's Vars.)
+				 */
+				find_safe_quals(j->larg, safe_quals);
+				find_safe_quals(j->rarg, safe_quals);
 				if (j->quals)
 					*safe_quals = lappend(*safe_quals, j->quals);
 				break;
 
 			case JOIN_LEFT:
-			case JOIN_SEMI:
 			case JOIN_ANTI:
 
 				/*
 				 * Only the left input is possibly non-nullable; furthermore,
-				 * the quals of this join don't constrain the left input.
-				 * Note: we probably can't see SEMI or ANTI joins at this
-				 * point, but if we do, we can treat them like LEFT joins.
+				 * the quals of this join don't constrain the left input,
+				 * since unmatched rows are emitted null-extended.
 				 */
-				find_subquery_safe_quals(j->larg, safe_quals);
+				find_safe_quals(j->larg, safe_quals);
 				break;
 
 			case JOIN_RIGHT:
-				/* Reverse of the above case */
-				find_subquery_safe_quals(j->rarg, safe_quals);
+				/* Reverse of the JOIN_LEFT case */
+				find_safe_quals(j->rarg, safe_quals);
 				break;
 
 			case JOIN_FULL:
diff --git a/src/include/optimizer/clauses.h b/src/include/optimizer/clauses.h
index 0e5a7b07404..e929789762b 100644
--- a/src/include/optimizer/clauses.h
+++ b/src/include/optimizer/clauses.h
@@ -52,6 +52,7 @@ extern Relids find_nonnullable_rels(Node *clause);
 extern List *find_nonnullable_vars(Node *clause);
 extern List *find_forced_null_vars(Node *node);
 extern Var *find_forced_null_var(Node *node);
+extern void find_safe_quals(Node *jtnode, List **safe_quals);
 extern bool query_outputs_are_not_nullable(Query *query);
 
 extern bool is_pseudo_constant_clause(Node *clause);
-- 
2.37.1 (Apple Git-137.1)

