diff options
author | Tom Lane <tgl@sss.pgh.pa.us> | 2013-08-19 13:19:25 -0400 |
---|---|---|
committer | Tom Lane <tgl@sss.pgh.pa.us> | 2013-08-19 13:19:41 -0400 |
commit | c64de21e9625acad57e2caf8f22435e1617fb1ce (patch) | |
tree | 0304ca222f842e04c1c72c598d103feeae4de5e9 /src/backend/optimizer/prep | |
parent | 78e1220104227c86b4b49d0fc123db7fa596d43d (diff) |
Fix qual-clause-misplacement issues with pulled-up LATERAL subqueries.
In an example such as
SELECT * FROM
i LEFT JOIN LATERAL (SELECT * FROM j WHERE i.n = j.n) j ON true;
it is safe to pull up the LATERAL subquery into its parent, but we must
then treat the "i.n = j.n" clause as a qual clause of the LEFT JOIN. The
previous coding in deconstruct_recurse mistakenly labeled the clause as
"is_pushed_down", resulting in wrong semantics if the clause were applied
at the join node, as per an example submitted awhile ago by Jeremy Evans.
To fix, postpone processing of such clauses until we return back up to
the appropriate recursion depth in deconstruct_recurse.
In addition, tighten the is-safe-to-pull-up checks in is_simple_subquery;
we previously missed the possibility that the LATERAL subquery might itself
contain an outer join that makes lateral references in lower quals unsafe.
A regression test case equivalent to Jeremy's example was already in my
commit of yesterday, but was giving the wrong results because of this
bug. This patch fixes the expected output for that, and also adds a
test case for the second problem.
Diffstat (limited to 'src/backend/optimizer/prep')
-rw-r--r-- | src/backend/optimizer/prep/prepjointree.c | 146 |
1 files changed, 130 insertions, 16 deletions
diff --git a/src/backend/optimizer/prep/prepjointree.c b/src/backend/optimizer/prep/prepjointree.c index 875baefbd39..c742cc9542b 100644 --- a/src/backend/optimizer/prep/prepjointree.c +++ b/src/backend/optimizer/prep/prepjointree.c @@ -84,6 +84,8 @@ static bool is_simple_union_all(Query *subquery); static bool is_simple_union_all_recurse(Node *setOp, Query *setOpQuery, List *colTypes); static bool is_safe_append_member(Query *subquery); +static bool jointree_contains_lateral_outer_refs(Node *jtnode, bool restricted, + Relids safe_upper_varnos); static void replace_vars_in_jointree(Node *jtnode, pullup_replace_vars_context *context, JoinExpr *lowest_nulling_outer_join); @@ -1303,20 +1305,58 @@ is_simple_subquery(Query *subquery, RangeTblEntry *rte, return false; /* - * If the subquery is LATERAL, and we're below any outer join, and the - * subquery contains lateral references to rels outside the outer join, - * don't pull up. Doing so would risk creating outer-join quals that - * contain references to rels outside the outer join, which is a semantic - * mess that doesn't seem worth addressing at the moment. + * If the subquery is LATERAL, check for pullup restrictions from that. */ - if (rte->lateral && lowest_outer_join != NULL) + if (rte->lateral) { - Relids lvarnos = pull_varnos_of_level((Node *) subquery, 1); - Relids jvarnos = get_relids_in_jointree((Node *) lowest_outer_join, - true); + bool restricted; + Relids safe_upper_varnos; - if (!bms_is_subset(lvarnos, jvarnos)) + /* + * The subquery's WHERE and JOIN/ON quals mustn't contain any lateral + * references to rels outside a higher outer join (including the case + * where the outer join is within the subquery itself). In such a + * case, pulling up would result in a situation where we need to + * postpone quals from below an outer join to above it, which is + * probably completely wrong and in any case is a complication that + * doesn't seem worth addressing at the moment. + */ + if (lowest_outer_join != NULL) + { + restricted = true; + safe_upper_varnos = get_relids_in_jointree((Node *) lowest_outer_join, + true); + } + else + { + restricted = false; + safe_upper_varnos = NULL; /* doesn't matter */ + } + + if (jointree_contains_lateral_outer_refs((Node *) subquery->jointree, + restricted, safe_upper_varnos)) return false; + + /* + * If there's an outer join above the LATERAL subquery, also disallow + * pullup if the subquery's targetlist has any references to rels + * outside the outer join, since these might get pulled into quals + * above the subquery (but in or below the outer join) and then lead + * to qual-postponement issues similar to the case checked for above. + * (We wouldn't need to prevent pullup if no such references appear in + * outer-query quals, but we don't have enough info here to check + * that. Also, maybe this restriction could be removed if we forced + * such refs to be wrapped in PlaceHolderVars, even when they're below + * the nearest outer join? But it's a pretty hokey usage, so not + * clear this is worth sweating over.) + */ + if (lowest_outer_join != NULL) + { + Relids lvarnos = pull_varnos_of_level((Node *) subquery->targetList, 1); + + if (!bms_is_subset(lvarnos, safe_upper_varnos)) + return false; + } } /* @@ -1344,12 +1384,12 @@ is_simple_subquery(Query *subquery, RangeTblEntry *rte, * correctly generate a Result plan for a jointree that's totally empty, * but we can't cope with an empty FromExpr appearing lower down in a * jointree: we identify join rels via baserelid sets, so we couldn't - * distinguish a join containing such a FromExpr from one without it. - * This would for example break the PlaceHolderVar mechanism, since we'd - * have no way to identify where to evaluate a PHV coming out of the - * subquery. Not worth working hard on this, just to collapse - * SubqueryScan/Result into Result; especially since the SubqueryScan can - * often be optimized away by setrefs.c anyway. + * distinguish a join containing such a FromExpr from one without it. This + * would for example break the PlaceHolderVar mechanism, since we'd have + * no way to identify where to evaluate a PHV coming out of the subquery. + * Not worth working hard on this, just to collapse SubqueryScan/Result + * into Result; especially since the SubqueryScan can often be optimized + * away by setrefs.c anyway. */ if (subquery->jointree->fromlist == NIL) return false; @@ -1467,6 +1507,80 @@ is_safe_append_member(Query *subquery) } /* + * jointree_contains_lateral_outer_refs + * Check for disallowed lateral references in a jointree's quals + * + * If restricted is false, all level-1 Vars are allowed (but we still must + * search the jointree, since it might contain outer joins below which there + * will be restrictions). If restricted is true, return TRUE when any qual + * in the jointree contains level-1 Vars coming from outside the rels listed + * in safe_upper_varnos. + */ +static bool +jointree_contains_lateral_outer_refs(Node *jtnode, bool restricted, + Relids safe_upper_varnos) +{ + if (jtnode == NULL) + return false; + if (IsA(jtnode, RangeTblRef)) + return false; + else if (IsA(jtnode, FromExpr)) + { + FromExpr *f = (FromExpr *) jtnode; + ListCell *l; + + /* First, recurse to check child joins */ + foreach(l, f->fromlist) + { + if (jointree_contains_lateral_outer_refs(lfirst(l), + restricted, + safe_upper_varnos)) + return true; + } + + /* Then check the top-level quals */ + if (restricted && + !bms_is_subset(pull_varnos_of_level(f->quals, 1), + safe_upper_varnos)) + return true; + } + else if (IsA(jtnode, JoinExpr)) + { + JoinExpr *j = (JoinExpr *) jtnode; + + /* + * If this is an outer join, we mustn't allow any upper lateral + * references in or below it. + */ + if (j->jointype != JOIN_INNER) + { + restricted = true; + safe_upper_varnos = NULL; + } + + /* Check the child joins */ + if (jointree_contains_lateral_outer_refs(j->larg, + restricted, + safe_upper_varnos)) + return true; + if (jointree_contains_lateral_outer_refs(j->rarg, + restricted, + safe_upper_varnos)) + return true; + + /* Check the JOIN's qual clauses */ + if (restricted && + !bms_is_subset(pull_varnos_of_level(j->quals, 1), + safe_upper_varnos)) + return true; + } + else + elog(ERROR, "unrecognized node type: %d", + (int) nodeTag(jtnode)); + return false; +} + +/* * Helper routine for pull_up_subqueries: do pullup_replace_vars on every * expression in the jointree, without changing the jointree structure itself. * Ugly, but there's no other way... |