int join_collapse_limit;
+/* Elements of the postponed_qual_list used during deconstruct_recurse */
+typedef struct PostponedQual
+{
+ Node *qual; /* a qual clause waiting to be processed */
+ Relids relids; /* the set of baserels it references */
+} PostponedQual;
+
+
static void extract_lateral_references(PlannerInfo *root, RelOptInfo *brel,
Index rtindex);
static void add_lateral_info(PlannerInfo *root, Relids lhs, Relids rhs);
static List *deconstruct_recurse(PlannerInfo *root, Node *jtnode,
bool below_outer_join,
- Relids *qualscope, Relids *inner_join_rels);
+ Relids *qualscope, Relids *inner_join_rels,
+ List **postponed_qual_list);
static SpecialJoinInfo *make_outerjoininfo(PlannerInfo *root,
Relids left_rels, Relids right_rels,
Relids inner_join_rels,
Relids qualscope,
Relids ojscope,
Relids outerjoin_nonnullable,
- Relids deduced_nullable_relids);
+ Relids deduced_nullable_relids,
+ List **postponed_qual_list);
static bool check_outerjoin_delay(PlannerInfo *root, Relids *relids_p,
Relids *nullable_relids_p, bool is_pushed_down);
static bool check_equivalence_delay(PlannerInfo *root,
List *
deconstruct_jointree(PlannerInfo *root)
{
+ List *result;
Relids qualscope;
Relids inner_join_rels;
+ List *postponed_qual_list = NIL;
/* Start recursion at top of jointree */
Assert(root->parse->jointree != NULL &&
IsA(root->parse->jointree, FromExpr));
- return deconstruct_recurse(root, (Node *) root->parse->jointree, false,
- &qualscope, &inner_join_rels);
+ result = deconstruct_recurse(root, (Node *) root->parse->jointree, false,
+ &qualscope, &inner_join_rels,
+ &postponed_qual_list);
+
+ /* Shouldn't be any leftover quals */
+ Assert(postponed_qual_list == NIL);
+
+ return result;
}
/*
* *inner_join_rels gets the set of base Relids syntactically included in
* inner joins appearing at or below this jointree node (do not modify
* or free this, either)
+ * *postponed_qual_list is a list of PostponedQual structs, which we can
+ * add quals to if they turn out to belong to a higher join level
* Return value is the appropriate joinlist for this jointree node
*
* In addition, entries will be added to root->join_info_list for outer joins.
*/
static List *
deconstruct_recurse(PlannerInfo *root, Node *jtnode, bool below_outer_join,
- Relids *qualscope, Relids *inner_join_rels)
+ Relids *qualscope, Relids *inner_join_rels,
+ List **postponed_qual_list)
{
List *joinlist;
else if (IsA(jtnode, FromExpr))
{
FromExpr *f = (FromExpr *) jtnode;
+ List *child_postponed_quals = NIL;
int remaining;
ListCell *l;
sub_joinlist = deconstruct_recurse(root, lfirst(l),
below_outer_join,
&sub_qualscope,
- inner_join_rels);
+ inner_join_rels,
+ &child_postponed_quals);
*qualscope = bms_add_members(*qualscope, sub_qualscope);
sub_members = list_length(sub_joinlist);
remaining--;
if (list_length(f->fromlist) > 1)
*inner_join_rels = *qualscope;
+ /*
+ * Try to process any quals postponed by children. If they need
+ * further postponement, add them to my output postponed_qual_list.
+ */
+ foreach(l, child_postponed_quals)
+ {
+ PostponedQual *pq = (PostponedQual *) lfirst(l);
+
+ if (bms_is_subset(pq->relids, *qualscope))
+ distribute_qual_to_rels(root, pq->qual,
+ false, below_outer_join, JOIN_INNER,
+ *qualscope, NULL, NULL, NULL,
+ NULL);
+ else
+ *postponed_qual_list = lappend(*postponed_qual_list, pq);
+ }
+
/*
* Now process the top-level quals.
*/
distribute_qual_to_rels(root, qual,
false, below_outer_join, JOIN_INNER,
- *qualscope, NULL, NULL, NULL);
+ *qualscope, NULL, NULL, NULL,
+ postponed_qual_list);
}
}
else if (IsA(jtnode, JoinExpr))
{
JoinExpr *j = (JoinExpr *) jtnode;
+ List *child_postponed_quals = NIL;
Relids leftids,
rightids,
left_inners,
case JOIN_INNER:
leftjoinlist = deconstruct_recurse(root, j->larg,
below_outer_join,
- &leftids, &left_inners);
+ &leftids, &left_inners,
+ &child_postponed_quals);
rightjoinlist = deconstruct_recurse(root, j->rarg,
below_outer_join,
- &rightids, &right_inners);
+ &rightids, &right_inners,
+ &child_postponed_quals);
*qualscope = bms_union(leftids, rightids);
*inner_join_rels = *qualscope;
/* Inner join adds no restrictions for quals */
case JOIN_ANTI:
leftjoinlist = deconstruct_recurse(root, j->larg,
below_outer_join,
- &leftids, &left_inners);
+ &leftids, &left_inners,
+ &child_postponed_quals);
rightjoinlist = deconstruct_recurse(root, j->rarg,
true,
- &rightids, &right_inners);
+ &rightids, &right_inners,
+ &child_postponed_quals);
*qualscope = bms_union(leftids, rightids);
*inner_join_rels = bms_union(left_inners, right_inners);
nonnullable_rels = leftids;
case JOIN_SEMI:
leftjoinlist = deconstruct_recurse(root, j->larg,
below_outer_join,
- &leftids, &left_inners);
+ &leftids, &left_inners,
+ &child_postponed_quals);
rightjoinlist = deconstruct_recurse(root, j->rarg,
below_outer_join,
- &rightids, &right_inners);
+ &rightids, &right_inners,
+ &child_postponed_quals);
*qualscope = bms_union(leftids, rightids);
*inner_join_rels = bms_union(left_inners, right_inners);
/* Semi join adds no restrictions for quals */
case JOIN_FULL:
leftjoinlist = deconstruct_recurse(root, j->larg,
true,
- &leftids, &left_inners);
+ &leftids, &left_inners,
+ &child_postponed_quals);
rightjoinlist = deconstruct_recurse(root, j->rarg,
true,
- &rightids, &right_inners);
+ &rightids, &right_inners,
+ &child_postponed_quals);
*qualscope = bms_union(leftids, rightids);
*inner_join_rels = bms_union(left_inners, right_inners);
/* each side is both outer and inner */
ojscope = NULL;
}
- /* Process the qual clauses */
+ /*
+ * Try to process any quals postponed by children. If they need
+ * further postponement, add them to my output postponed_qual_list.
+ */
+ foreach(l, child_postponed_quals)
+ {
+ PostponedQual *pq = (PostponedQual *) lfirst(l);
+
+ if (bms_is_subset(pq->relids, *qualscope))
+ distribute_qual_to_rels(root, pq->qual,
+ false, below_outer_join, j->jointype,
+ *qualscope,
+ ojscope, nonnullable_rels, NULL,
+ NULL);
+ else
+ {
+ /*
+ * We should not be postponing any quals past an outer join.
+ * If this Assert fires, pull_up_subqueries() messed up.
+ */
+ Assert(j->jointype == JOIN_INNER);
+ *postponed_qual_list = lappend(*postponed_qual_list, pq);
+ }
+ }
+
+ /* Process the JOIN's qual clauses */
foreach(l, (List *) j->quals)
{
Node *qual = (Node *) lfirst(l);
distribute_qual_to_rels(root, qual,
false, below_outer_join, j->jointype,
*qualscope,
- ojscope, nonnullable_rels, NULL);
+ ojscope, nonnullable_rels, NULL,
+ postponed_qual_list);
}
/* Now we can add the SpecialJoinInfo to join_info_list */
* the appropriate list for each rel. Alternatively, if the clause uses a
* mergejoinable operator and is not delayed by outer-join rules, enter
* the left- and right-side expressions into the query's list of
- * EquivalenceClasses.
+ * EquivalenceClasses. Alternatively, if the clause needs to be treated
+ * as belonging to a higher join level, just add it to postponed_qual_list.
*
* 'clause': the qual clause to be distributed
* 'is_deduced': TRUE if the qual came from implied-equality deduction
* equal qualscope)
* 'deduced_nullable_relids': if is_deduced is TRUE, the nullable relids to
* impute to the clause; otherwise NULL
+ * 'postponed_qual_list': list of PostponedQual structs, which we can add
+ * this qual to if it turns out to belong to a higher join level.
+ * Can be NULL if caller knows postponement is impossible.
*
* 'qualscope' identifies what level of JOIN the qual came from syntactically.
* 'ojscope' is needed if we decide to force the qual up to the outer-join
Relids qualscope,
Relids ojscope,
Relids outerjoin_nonnullable,
- Relids deduced_nullable_relids)
+ Relids deduced_nullable_relids,
+ List **postponed_qual_list)
{
Relids relids;
bool is_pushed_down;
relids = pull_varnos(clause);
/*
- * Normally relids is a subset of qualscope, and we like to check that
- * here as a crosscheck on the parser and rewriter. That need not be the
- * case when there are LATERAL RTEs, however: the clause could contain
- * references to rels outside its syntactic scope as a consequence of
- * pull-up of such references from a LATERAL subquery below it. So, only
- * check if the query contains no LATERAL RTEs.
- *
- * However, if it's an outer-join clause, we always insist that relids be
- * a subset of ojscope. This is safe because is_simple_subquery()
- * disallows pullup of LATERAL subqueries that could cause the restriction
- * to be violated.
+ * In ordinary SQL, a WHERE or JOIN/ON clause can't reference any rels
+ * that aren't within its syntactic scope; however, if we pulled up a
+ * LATERAL subquery then we might find such references in quals that have
+ * been pulled up. We need to treat such quals as belonging to the join
+ * level that includes every rel they reference. Although we could make
+ * pull_up_subqueries() place such quals correctly to begin with, it's
+ * easier to handle it here. When we find a clause that contains Vars
+ * outside its syntactic scope, we add it to the postponed-quals list, and
+ * process it once we've recursed back up to the appropriate join level.
+ */
+ if (!bms_is_subset(relids, qualscope))
+ {
+ PostponedQual *pq = (PostponedQual *) palloc(sizeof(PostponedQual));
+
+ Assert(root->hasLateralRTEs); /* shouldn't happen otherwise */
+ Assert(jointype == JOIN_INNER); /* mustn't postpone past outer join */
+ Assert(!is_deduced); /* shouldn't be deduced, either */
+ pq->qual = clause;
+ pq->relids = relids;
+ *postponed_qual_list = lappend(*postponed_qual_list, pq);
+ return;
+ }
+
+ /*
+ * If it's an outer-join clause, also check that relids is a subset of
+ * ojscope. (This should not fail if the syntactic scope check passed.)
*/
- if (!root->hasLateralRTEs && !bms_is_subset(relids, qualscope))
- elog(ERROR, "JOIN qualification cannot refer to other relations");
if (ojscope && !bms_is_subset(relids, ojscope))
elog(ERROR, "JOIN qualification cannot refer to other relations");
*/
distribute_qual_to_rels(root, (Node *) clause,
true, below_outer_join, JOIN_INNER,
- qualscope, NULL, NULL, nullable_relids);
+ qualscope, NULL, NULL, nullable_relids,
+ NULL);
}
/*
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);
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;
+ }
}
/*
return false;
/*
- * Hack: don't try to pull up a subquery with an empty jointree.
- * query_planner() will correctly generate a Result plan for a jointree
- * that's totally empty, but I don't think the right things happen if an
- * empty FromExpr appears lower down in a jointree. It would pose a
- * problem for the PlaceHolderVar mechanism too, 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.
+ * Don't pull up a subquery with an empty jointree. query_planner() will
+ * 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.
*/
if (subquery->jointree->fromlist == NIL)
return false;
return true;
}
+/*
+ * 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.