From fa0225107dd0f79976841ce2c9bd41c26fd2ded9 Mon Sep 17 00:00:00 2001 From: Richard Guo 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)