diff options
author | Tom Lane <tgl@sss.pgh.pa.us> | 2023-05-17 11:13:52 -0400 |
---|---|---|
committer | Tom Lane <tgl@sss.pgh.pa.us> | 2023-05-17 11:14:04 -0400 |
commit | 9df8f903eb6758be5a19e66cdf77e922e9329c31 (patch) | |
tree | b2726b13ad6c830593a71fd6b1c5ac317a8ef62b /src/backend/optimizer/util/relnode.c | |
parent | 867be9c0738bef591544d39985f886b7d8e99bf0 (diff) | |
download | postgresql-9df8f903eb6758be5a19e66cdf77e922e9329c31.tar.gz postgresql-9df8f903eb6758be5a19e66cdf77e922e9329c31.zip |
Fix some issues with improper placement of outer join clauses.
After applying outer-join identity 3 in the forward direction,
it was possible for the planner to mistakenly apply a qual clause
from above the two outer joins at the now-lower join level.
This can give the wrong answer, since a value that would get nulled
by the now-upper join might not yet be null.
To fix, when we perform such a transformation, consider that the
now-lower join hasn't really completed the outer join it's nominally
responsible for and thus its relid set should not include that OJ's
relid (nor should its output Vars have that nullingrel bit set).
Instead we add those bits when the now-upper join is performed.
The existing rules for qual placement then suffice to prevent
higher qual clauses from dropping below the now-upper join.
There are a few complications from needing to consider transitive
closures in case multiple pushdowns have happened, but all in all
it's not a very complex patch.
This is all new logic (from 2489d76c4) so no need to back-patch.
The added test cases all have the same results as in v15.
Tom Lane and Richard Guo
Discussion: https://postgr.es/m/0b819232-4b50-f245-1c7d-c8c61bf41827@postgrespro.ru
Diffstat (limited to 'src/backend/optimizer/util/relnode.c')
-rw-r--r-- | src/backend/optimizer/util/relnode.c | 71 |
1 files changed, 53 insertions, 18 deletions
diff --git a/src/backend/optimizer/util/relnode.c b/src/backend/optimizer/util/relnode.c index 68fd0335952..8d5f7c5e8dd 100644 --- a/src/backend/optimizer/util/relnode.c +++ b/src/backend/optimizer/util/relnode.c @@ -43,6 +43,7 @@ typedef struct JoinHashEntry static void build_joinrel_tlist(PlannerInfo *root, RelOptInfo *joinrel, RelOptInfo *input_rel, SpecialJoinInfo *sjinfo, + List *pushed_down_joins, bool can_null); static List *build_joinrel_restrictlist(PlannerInfo *root, RelOptInfo *joinrel, @@ -632,6 +633,7 @@ add_join_rel(PlannerInfo *root, RelOptInfo *joinrel) * 'outer_rel' and 'inner_rel' are relation nodes for the relations to be * joined * 'sjinfo': join context info + * 'pushed_down_joins': any pushed-down outer joins that are now completed * 'restrictlist_ptr': result variable. If not NULL, *restrictlist_ptr * receives the list of RestrictInfo nodes that apply to this * particular pair of joinable relations. @@ -645,6 +647,7 @@ build_join_rel(PlannerInfo *root, RelOptInfo *outer_rel, RelOptInfo *inner_rel, SpecialJoinInfo *sjinfo, + List *pushed_down_joins, List **restrictlist_ptr) { RelOptInfo *joinrel; @@ -757,9 +760,9 @@ build_join_rel(PlannerInfo *root, * and inner rels we first try to build it from. But the contents should * be the same regardless. */ - build_joinrel_tlist(root, joinrel, outer_rel, sjinfo, + build_joinrel_tlist(root, joinrel, outer_rel, sjinfo, pushed_down_joins, (sjinfo->jointype == JOIN_FULL)); - build_joinrel_tlist(root, joinrel, inner_rel, sjinfo, + build_joinrel_tlist(root, joinrel, inner_rel, sjinfo, pushed_down_joins, (sjinfo->jointype != JOIN_INNER)); add_placeholders_to_joinrel(root, joinrel, outer_rel, inner_rel, sjinfo); @@ -870,8 +873,8 @@ build_child_join_rel(PlannerInfo *root, RelOptInfo *outer_rel, joinrel->reloptkind = RELOPT_OTHER_JOINREL; joinrel->relids = bms_union(outer_rel->relids, inner_rel->relids); - if (sjinfo->ojrelid != 0) - joinrel->relids = bms_add_member(joinrel->relids, sjinfo->ojrelid); + joinrel->relids = add_outer_joins_to_relids(root, joinrel->relids, sjinfo, + NULL); joinrel->rows = 0; /* cheap startup cost is interesting iff not all tuples to be retrieved */ joinrel->consider_startup = (root->tuple_fraction > 0); @@ -1042,22 +1045,32 @@ min_join_parameterization(PlannerInfo *root, * from this input relation. If so, we will (normally) add the join's relid * to the nulling bitmaps of Vars and PHVs bubbled up from the input. * - * When forming an outer join's target list, special handling is needed - * in case the outer join was commuted with another one per outer join - * identity 3 (see optimizer/README). We must take steps to ensure that - * the output Vars have the same nulling bitmaps that they would if the - * two joins had been done in syntactic order; else they won't match Vars - * appearing higher in the query tree. We need to do two things: + * When forming an outer join's target list, special handling is needed in + * case the outer join was commuted with another one per outer join identity 3 + * (see optimizer/README). We must take steps to ensure that the output Vars + * have the same nulling bitmaps that they would if the two joins had been + * done in syntactic order; else they won't match Vars appearing higher in + * the query tree. An exception to the match-the-syntactic-order rule is + * that when an outer join is pushed down into another one's RHS per identity + * 3, we can't mark its Vars as nulled until the now-upper outer join is also + * completed. So we need to do three things: * - * First, we add the outer join's relid to the nulling bitmap only if the Var - * or PHV actually comes from within the syntactically nullable side(s) of the - * outer join. This takes care of the possibility that we have transformed + * First, we add the outer join's relid to the nulling bitmap only if the + * outer join has been completely performed and the Var or PHV actually + * comes from within the syntactically nullable side(s) of the outer join. + * This takes care of the possibility that we have transformed * (A leftjoin B on (Pab)) leftjoin C on (Pbc) * to * A leftjoin (B leftjoin C on (Pbc)) on (Pab) - * Here the now-upper A/B join must not mark C columns as nulled by itself. + * Here the pushed-down B/C join cannot mark C columns as nulled yet, + * while the now-upper A/B join must not mark C columns as nulled by itself. * - * Second, any relid in sjinfo->commute_above_r that is already part of + * Second, perform the same operation for each SpecialJoinInfo listed in + * pushed_down_joins (which, in this example, would be the B/C join when + * we are at the now-upper A/B join). This allows the now-upper join to + * complete the marking of "C" Vars that now have fully valid values. + * + * Third, any relid in sjinfo->commute_above_r that is already part of * the joinrel is added to the nulling bitmaps of nullable Vars and PHVs. * This takes care of the reverse case where we implement * A leftjoin (B leftjoin C on (Pbc)) on (Pab) @@ -1071,10 +1084,12 @@ static void build_joinrel_tlist(PlannerInfo *root, RelOptInfo *joinrel, RelOptInfo *input_rel, SpecialJoinInfo *sjinfo, + List *pushed_down_joins, bool can_null) { Relids relids = joinrel->relids; ListCell *vars; + ListCell *lc; foreach(vars, input_rel->reltarget->exprs) { @@ -1101,11 +1116,21 @@ build_joinrel_tlist(PlannerInfo *root, RelOptInfo *joinrel, phv = copyObject(phv); /* See comments above to understand this logic */ if (sjinfo->ojrelid != 0 && + bms_is_member(sjinfo->ojrelid, relids) && (bms_is_subset(phv->phrels, sjinfo->syn_righthand) || (sjinfo->jointype == JOIN_FULL && bms_is_subset(phv->phrels, sjinfo->syn_lefthand)))) phv->phnullingrels = bms_add_member(phv->phnullingrels, sjinfo->ojrelid); + foreach(lc, pushed_down_joins) + { + SpecialJoinInfo *othersj = (SpecialJoinInfo *) lfirst(lc); + + Assert(bms_is_member(othersj->ojrelid, relids)); + if (bms_is_subset(phv->phrels, othersj->syn_righthand)) + phv->phnullingrels = bms_add_member(phv->phnullingrels, + othersj->ojrelid); + } phv->phnullingrels = bms_join(phv->phnullingrels, bms_intersect(sjinfo->commute_above_r, @@ -1166,11 +1191,21 @@ build_joinrel_tlist(PlannerInfo *root, RelOptInfo *joinrel, var = copyObject(var); /* See comments above to understand this logic */ if (sjinfo->ojrelid != 0 && + bms_is_member(sjinfo->ojrelid, relids) && (bms_is_member(var->varno, sjinfo->syn_righthand) || (sjinfo->jointype == JOIN_FULL && bms_is_member(var->varno, sjinfo->syn_lefthand)))) var->varnullingrels = bms_add_member(var->varnullingrels, sjinfo->ojrelid); + foreach(lc, pushed_down_joins) + { + SpecialJoinInfo *othersj = (SpecialJoinInfo *) lfirst(lc); + + Assert(bms_is_member(othersj->ojrelid, relids)); + if (bms_is_member(var->varno, othersj->syn_righthand)) + var->varnullingrels = bms_add_member(var->varnullingrels, + othersj->ojrelid); + } var->varnullingrels = bms_join(var->varnullingrels, bms_intersect(sjinfo->commute_above_r, @@ -1259,7 +1294,7 @@ build_joinrel_restrictlist(PlannerInfo *root, joinrel->relids, outer_rel->relids, inner_rel, - sjinfo->ojrelid)); + sjinfo)); return result; } @@ -1543,7 +1578,7 @@ get_baserel_parampathinfo(PlannerInfo *root, RelOptInfo *baserel, joinrelids, required_outer, baserel, - 0)); + NULL)); /* Compute set of serial numbers of the enforced clauses */ pserials = NULL; @@ -1666,7 +1701,7 @@ get_joinrel_parampathinfo(PlannerInfo *root, RelOptInfo *joinrel, join_and_req, required_outer, joinrel, - 0); + NULL); /* We only want ones that aren't movable to lower levels */ dropped_ecs = NIL; foreach(lc, eclauses) |