From 7f51d1ca7cd0c8bb14eb2e5e87b2e53bbc75a6f0 Mon Sep 17 00:00:00 2001 From: Rui Zhao 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