diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c index 1c93e0c5ac3..8713eabc646 100644 --- a/contrib/postgres_fdw/postgres_fdw.c +++ b/contrib/postgres_fdw/postgres_fdw.c @@ -540,7 +540,6 @@ postgresGetForeignPaths(PlannerInfo *root, { PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) baserel->fdw_private; ForeignPath *path; - Relids lateral_referencers; List *join_quals; Relids required_outer; double rows; @@ -579,34 +578,13 @@ postgresGetForeignPaths(PlannerInfo *root, * consider combinations of clauses, probably. */ - /* - * If there are any rels that have LATERAL references to this one, we - * cannot use join quals referencing them as remote quals for this one, - * since such rels would have to be on the inside not the outside of a - * nestloop join relative to this one. Create a Relids set listing all - * such rels, for use in checks of potential join clauses. - */ - lateral_referencers = NULL; - foreach(lc, root->lateral_info_list) - { - LateralJoinInfo *ljinfo = (LateralJoinInfo *) lfirst(lc); - - if (bms_is_member(baserel->relid, ljinfo->lateral_lhs)) - lateral_referencers = bms_add_member(lateral_referencers, - ljinfo->lateral_rhs); - } - /* Scan the rel's join clauses */ foreach(lc, baserel->joininfo) { RestrictInfo *rinfo = (RestrictInfo *) lfirst(lc); /* Check if clause can be moved to this rel */ - if (!join_clause_is_movable_to(rinfo, baserel->relid)) - continue; - - /* Not useful if it conflicts with any LATERAL references */ - if (bms_overlap(rinfo->clause_relids, lateral_referencers)) + if (!join_clause_is_movable_to(rinfo, baserel)) continue; /* See if it is safe to send to remote */ @@ -667,7 +645,7 @@ postgresGetForeignPaths(PlannerInfo *root, baserel, ec_member_matches_foreign, (void *) &arg, - lateral_referencers); + baserel->lateral_referencers); /* Done if there are no more expressions in the foreign rel */ if (arg.current == NULL) @@ -682,12 +660,9 @@ postgresGetForeignPaths(PlannerInfo *root, RestrictInfo *rinfo = (RestrictInfo *) lfirst(lc); /* Check if clause can be moved to this rel */ - if (!join_clause_is_movable_to(rinfo, baserel->relid)) + if (!join_clause_is_movable_to(rinfo, baserel)) continue; - /* Shouldn't conflict with any LATERAL references */ - Assert(!bms_overlap(rinfo->clause_relids, lateral_referencers)); - /* See if it is safe to send to remote */ if (!is_foreign_expr(root, baserel, rinfo->clause)) continue; diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c index 525e3fa2d19..6b20e317323 100644 --- a/src/backend/nodes/copyfuncs.c +++ b/src/backend/nodes/copyfuncs.c @@ -1917,8 +1917,8 @@ _copyLateralJoinInfo(const LateralJoinInfo *from) { LateralJoinInfo *newnode = makeNode(LateralJoinInfo); - COPY_SCALAR_FIELD(lateral_rhs); COPY_BITMAPSET_FIELD(lateral_lhs); + COPY_BITMAPSET_FIELD(lateral_rhs); return newnode; } @@ -1952,6 +1952,7 @@ _copyPlaceHolderInfo(const PlaceHolderInfo *from) COPY_SCALAR_FIELD(phid); COPY_NODE_FIELD(ph_var); COPY_BITMAPSET_FIELD(ph_eval_at); + COPY_BITMAPSET_FIELD(ph_lateral); COPY_BITMAPSET_FIELD(ph_needed); COPY_SCALAR_FIELD(ph_width); diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c index 379cbc0c203..b49e1e731de 100644 --- a/src/backend/nodes/equalfuncs.c +++ b/src/backend/nodes/equalfuncs.c @@ -761,15 +761,19 @@ _equalPlaceHolderVar(const PlaceHolderVar *a, const PlaceHolderVar *b) /* * We intentionally do not compare phexpr. Two PlaceHolderVars with the * same ID and levelsup should be considered equal even if the contained - * expressions have managed to mutate to different states. One way in - * which that can happen is that initplan sublinks would get replaced by - * differently-numbered Params when sublink folding is done. (The end - * result of such a situation would be some unreferenced initplans, which - * is annoying but not really a problem.) + * expressions have managed to mutate to different states. This will + * happen during final plan construction when there are nested PHVs, since + * the inner PHV will get replaced by a Param in some copies of the outer + * PHV. Another way in which it can happen is that initplan sublinks + * could get replaced by differently-numbered Params when sublink folding + * is done. (The end result of such a situation would be some + * unreferenced initplans, which is annoying but not really a problem.) On + * the same reasoning, there is no need to examine phrels. * * COMPARE_NODE_FIELD(phexpr); + * + * COMPARE_BITMAPSET_FIELD(phrels); */ - COMPARE_BITMAPSET_FIELD(phrels); COMPARE_SCALAR_FIELD(phid); COMPARE_SCALAR_FIELD(phlevelsup); @@ -794,8 +798,8 @@ _equalSpecialJoinInfo(const SpecialJoinInfo *a, const SpecialJoinInfo *b) static bool _equalLateralJoinInfo(const LateralJoinInfo *a, const LateralJoinInfo *b) { - COMPARE_SCALAR_FIELD(lateral_rhs); COMPARE_BITMAPSET_FIELD(lateral_lhs); + COMPARE_BITMAPSET_FIELD(lateral_rhs); return true; } @@ -817,8 +821,9 @@ static bool _equalPlaceHolderInfo(const PlaceHolderInfo *a, const PlaceHolderInfo *b) { COMPARE_SCALAR_FIELD(phid); - COMPARE_NODE_FIELD(ph_var); + COMPARE_NODE_FIELD(ph_var); /* should be redundant */ COMPARE_BITMAPSET_FIELD(ph_eval_at); + COMPARE_BITMAPSET_FIELD(ph_lateral); COMPARE_BITMAPSET_FIELD(ph_needed); COMPARE_SCALAR_FIELD(ph_width); diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 67aeb7e65c3..f6e211429c3 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -1752,6 +1752,7 @@ _outRelOptInfo(StringInfo str, const RelOptInfo *node) WRITE_INT_FIELD(max_attr); WRITE_NODE_FIELD(lateral_vars); WRITE_BITMAPSET_FIELD(lateral_relids); + WRITE_BITMAPSET_FIELD(lateral_referencers); WRITE_NODE_FIELD(indexlist); WRITE_UINT_FIELD(pages); WRITE_FLOAT_FIELD(tuples, "%.0f"); @@ -1909,8 +1910,8 @@ _outLateralJoinInfo(StringInfo str, const LateralJoinInfo *node) { WRITE_NODE_TYPE("LATERALJOININFO"); - WRITE_UINT_FIELD(lateral_rhs); WRITE_BITMAPSET_FIELD(lateral_lhs); + WRITE_BITMAPSET_FIELD(lateral_rhs); } static void @@ -1934,6 +1935,7 @@ _outPlaceHolderInfo(StringInfo str, const PlaceHolderInfo *node) WRITE_UINT_FIELD(phid); WRITE_NODE_FIELD(ph_var); WRITE_BITMAPSET_FIELD(ph_eval_at); + WRITE_BITMAPSET_FIELD(ph_lateral); WRITE_BITMAPSET_FIELD(ph_needed); WRITE_INT_FIELD(ph_width); } diff --git a/src/backend/optimizer/README b/src/backend/optimizer/README index 751766fb9dd..a8b014843a1 100644 --- a/src/backend/optimizer/README +++ b/src/backend/optimizer/README @@ -802,5 +802,19 @@ parameterized paths still apply, though; in particular, each such path is still expected to enforce any join clauses that can be pushed down to it, so that all paths of the same parameterization have the same rowcount. +We also allow LATERAL subqueries to be flattened (pulled up into the parent +query) by the optimizer, but only when they don't contain any lateral +references to relations outside the lowest outer join that can null the +LATERAL subquery. This restriction prevents lateral references from being +introduced into outer-join qualifications, which would create semantic +confusion. Note that even with this restriction, pullup of a LATERAL +subquery can result in creating PlaceHolderVars that contain lateral +references to relations outside their syntactic scope. We still evaluate +such PHVs at their syntactic location or lower, but the presence of such a +PHV in the quals or targetlist of a plan node requires that node to appear +on the inside of a nestloop join relative to the rel(s) supplying the +lateral reference. (Perhaps now that that stuff works, we could relax the +pullup restriction?) + -- bjm & tgl diff --git a/src/backend/optimizer/path/allpaths.c b/src/backend/optimizer/path/allpaths.c index 4b8a73d60f4..bfd3809a007 100644 --- a/src/backend/optimizer/path/allpaths.c +++ b/src/backend/optimizer/path/allpaths.c @@ -386,8 +386,7 @@ set_plain_rel_pathlist(PlannerInfo *root, RelOptInfo *rel, RangeTblEntry *rte) /* * We don't support pushing join clauses into the quals of a seqscan, but * it could still have required parameterization due to LATERAL refs in - * its tlist. (That can only happen if the seqscan is on a relation - * pulled up out of a UNION ALL appendrel.) + * its tlist. */ required_outer = rel->lateral_relids; @@ -550,8 +549,8 @@ set_append_rel_size(PlannerInfo *root, RelOptInfo *rel, * Note: the resulting childrel->reltargetlist may contain arbitrary * expressions, which otherwise would not occur in a reltargetlist. * Code that might be looking at an appendrel child must cope with - * such. Note in particular that "arbitrary expression" can include - * "Var belonging to another relation", due to LATERAL references. + * such. (Normally, a reltargetlist would only include Vars and + * PlaceHolderVars.) */ childrel->joininfo = (List *) adjust_appendrel_attrs(root, @@ -1355,8 +1354,7 @@ set_cte_pathlist(PlannerInfo *root, RelOptInfo *rel, RangeTblEntry *rte) /* * We don't support pushing join clauses into the quals of a CTE scan, but * it could still have required parameterization due to LATERAL refs in - * its tlist. (That can only happen if the CTE scan is on a relation - * pulled up out of a UNION ALL appendrel.) + * its tlist. */ required_outer = rel->lateral_relids; @@ -1408,10 +1406,8 @@ set_worktable_pathlist(PlannerInfo *root, RelOptInfo *rel, RangeTblEntry *rte) /* * We don't support pushing join clauses into the quals of a worktable * scan, but it could still have required parameterization due to LATERAL - * refs in its tlist. (That can only happen if the worktable scan is on a - * relation pulled up out of a UNION ALL appendrel. I'm not sure this is - * actually possible given the restrictions on recursive references, but - * it's easy enough to support.) + * refs in its tlist. (I'm not sure this is actually possible given the + * restrictions on recursive references, but it's easy enough to support.) */ required_outer = rel->lateral_relids; diff --git a/src/backend/optimizer/path/costsize.c b/src/backend/optimizer/path/costsize.c index 3507f18007e..a2cc6979594 100644 --- a/src/backend/optimizer/path/costsize.c +++ b/src/backend/optimizer/path/costsize.c @@ -3935,10 +3935,9 @@ set_rel_width(PlannerInfo *root, RelOptInfo *rel) /* * Ordinarily, a Var in a rel's reltargetlist must belong to that rel; - * but there are corner cases involving LATERAL references in - * appendrel members where that isn't so (see set_append_rel_size()). - * If the Var has the wrong varno, fall through to the generic case - * (it doesn't seem worth the trouble to be any smarter). + * but there are corner cases involving LATERAL references where that + * isn't so. If the Var has the wrong varno, fall through to the + * generic case (it doesn't seem worth the trouble to be any smarter). */ if (IsA(node, Var) && ((Var *) node)->varno == rel->relid) diff --git a/src/backend/optimizer/path/indxpath.c b/src/backend/optimizer/path/indxpath.c index 65eb344cde4..606734a1221 100644 --- a/src/backend/optimizer/path/indxpath.c +++ b/src/backend/optimizer/path/indxpath.c @@ -141,12 +141,10 @@ static void match_restriction_clauses_to_index(RelOptInfo *rel, IndexClauseSet *clauseset); static void match_join_clauses_to_index(PlannerInfo *root, RelOptInfo *rel, IndexOptInfo *index, - Relids lateral_referencers, IndexClauseSet *clauseset, List **joinorclauses); static void match_eclass_clauses_to_index(PlannerInfo *root, IndexOptInfo *index, - Relids lateral_referencers, IndexClauseSet *clauseset); static void match_clauses_to_index(IndexOptInfo *index, List *clauses, @@ -220,14 +218,14 @@ static Const *string_to_const(const char *str, Oid datatype); * * Note: check_partial_indexes() must have been run previously for this rel. * - * Note: in corner cases involving LATERAL appendrel children, it's possible - * that rel->lateral_relids is nonempty. Currently, we include lateral_relids - * into the parameterization reported for each path, but don't take it into - * account otherwise. The fact that any such rels *must* be available as - * parameter sources perhaps should influence our choices of index quals ... - * but for now, it doesn't seem worth troubling over. In particular, comments - * below about "unparameterized" paths should be read as meaning - * "unparameterized so far as the indexquals are concerned". + * Note: in cases involving LATERAL references in the relation's tlist, it's + * possible that rel->lateral_relids is nonempty. Currently, we include + * lateral_relids into the parameterization reported for each path, but don't + * take it into account otherwise. The fact that any such rels *must* be + * available as parameter sources perhaps should influence our choices of + * index quals ... but for now, it doesn't seem worth troubling over. + * In particular, comments below about "unparameterized" paths should be read + * as meaning "unparameterized so far as the indexquals are concerned". */ void create_index_paths(PlannerInfo *root, RelOptInfo *rel) @@ -236,7 +234,6 @@ create_index_paths(PlannerInfo *root, RelOptInfo *rel) List *bitindexpaths; List *bitjoinpaths; List *joinorclauses; - Relids lateral_referencers; IndexClauseSet rclauseset; IndexClauseSet jclauseset; IndexClauseSet eclauseset; @@ -246,23 +243,6 @@ create_index_paths(PlannerInfo *root, RelOptInfo *rel) if (rel->indexlist == NIL) return; - /* - * If there are any rels that have LATERAL references to this one, we - * cannot use join quals referencing them as index quals for this one, - * since such rels would have to be on the inside not the outside of a - * nestloop join relative to this one. Create a Relids set listing all - * such rels, for use in checks of potential join clauses. - */ - lateral_referencers = NULL; - foreach(lc, root->lateral_info_list) - { - LateralJoinInfo *ljinfo = (LateralJoinInfo *) lfirst(lc); - - if (bms_is_member(rel->relid, ljinfo->lateral_lhs)) - lateral_referencers = bms_add_member(lateral_referencers, - ljinfo->lateral_rhs); - } - /* Bitmap paths are collected and then dealt with at the end */ bitindexpaths = bitjoinpaths = joinorclauses = NIL; @@ -303,7 +283,7 @@ create_index_paths(PlannerInfo *root, RelOptInfo *rel) * EquivalenceClasses. Also, collect join OR clauses for later. */ MemSet(&jclauseset, 0, sizeof(jclauseset)); - match_join_clauses_to_index(root, rel, index, lateral_referencers, + match_join_clauses_to_index(root, rel, index, &jclauseset, &joinorclauses); /* @@ -311,7 +291,7 @@ create_index_paths(PlannerInfo *root, RelOptInfo *rel) * the index. */ MemSet(&eclauseset, 0, sizeof(eclauseset)); - match_eclass_clauses_to_index(root, index, lateral_referencers, + match_eclass_clauses_to_index(root, index, &eclauseset); /* @@ -1957,7 +1937,6 @@ match_restriction_clauses_to_index(RelOptInfo *rel, IndexOptInfo *index, static void match_join_clauses_to_index(PlannerInfo *root, RelOptInfo *rel, IndexOptInfo *index, - Relids lateral_referencers, IndexClauseSet *clauseset, List **joinorclauses) { @@ -1969,11 +1948,7 @@ match_join_clauses_to_index(PlannerInfo *root, RestrictInfo *rinfo = (RestrictInfo *) lfirst(lc); /* Check if clause can be moved to this rel */ - if (!join_clause_is_movable_to(rinfo, rel->relid)) - continue; - - /* Not useful if it conflicts with any LATERAL references */ - if (bms_overlap(rinfo->clause_relids, lateral_referencers)) + if (!join_clause_is_movable_to(rinfo, rel)) continue; /* Potentially usable, so see if it matches the index or is an OR */ @@ -1991,7 +1966,6 @@ match_join_clauses_to_index(PlannerInfo *root, */ static void match_eclass_clauses_to_index(PlannerInfo *root, IndexOptInfo *index, - Relids lateral_referencers, IndexClauseSet *clauseset) { int indexcol; @@ -2012,7 +1986,7 @@ match_eclass_clauses_to_index(PlannerInfo *root, IndexOptInfo *index, index->rel, ec_member_matches_indexcol, (void *) &arg, - lateral_referencers); + index->rel->lateral_referencers); /* * We have to check whether the results actually do match the index, @@ -2644,7 +2618,7 @@ check_partial_indexes(PlannerInfo *root, RelOptInfo *rel) RestrictInfo *rinfo = (RestrictInfo *) lfirst(lc); /* Check if clause can be moved to this rel */ - if (!join_clause_is_movable_to(rinfo, rel->relid)) + if (!join_clause_is_movable_to(rinfo, rel)) continue; clauselist = lappend(clauselist, rinfo); diff --git a/src/backend/optimizer/path/joinpath.c b/src/backend/optimizer/path/joinpath.c index d6050a616c7..5b477e52d3f 100644 --- a/src/backend/optimizer/path/joinpath.c +++ b/src/backend/optimizer/path/joinpath.c @@ -29,19 +29,19 @@ static void sort_inner_and_outer(PlannerInfo *root, RelOptInfo *joinrel, RelOptInfo *outerrel, RelOptInfo *innerrel, List *restrictlist, List *mergeclause_list, JoinType jointype, SpecialJoinInfo *sjinfo, - Relids param_source_rels); + Relids param_source_rels, Relids extra_lateral_rels); static void match_unsorted_outer(PlannerInfo *root, RelOptInfo *joinrel, RelOptInfo *outerrel, RelOptInfo *innerrel, List *restrictlist, List *mergeclause_list, JoinType jointype, SpecialJoinInfo *sjinfo, SemiAntiJoinFactors *semifactors, - Relids param_source_rels); + Relids param_source_rels, Relids extra_lateral_rels); static void hash_inner_and_outer(PlannerInfo *root, RelOptInfo *joinrel, RelOptInfo *outerrel, RelOptInfo *innerrel, List *restrictlist, JoinType jointype, SpecialJoinInfo *sjinfo, SemiAntiJoinFactors *semifactors, - Relids param_source_rels); + Relids param_source_rels, Relids extra_lateral_rels); static List *select_mergejoin_clauses(PlannerInfo *root, RelOptInfo *joinrel, RelOptInfo *outerrel, @@ -87,6 +87,7 @@ add_paths_to_joinrel(PlannerInfo *root, bool mergejoin_allowed = true; SemiAntiJoinFactors semifactors; Relids param_source_rels = NULL; + Relids extra_lateral_rels = NULL; ListCell *lc; /* @@ -162,12 +163,49 @@ add_paths_to_joinrel(PlannerInfo *root, { LateralJoinInfo *ljinfo = (LateralJoinInfo *) lfirst(lc); - if (bms_is_member(ljinfo->lateral_rhs, joinrel->relids)) + if (bms_is_subset(ljinfo->lateral_rhs, joinrel->relids)) param_source_rels = bms_join(param_source_rels, bms_difference(ljinfo->lateral_lhs, joinrel->relids)); } + /* + * Another issue created by LATERAL references is that PlaceHolderVars + * that need to be computed at this join level might contain lateral + * references to rels not in the join, meaning that the paths for the join + * would need to be marked as parameterized by those rels, independently + * of all other considerations. Set extra_lateral_rels to the set of such + * rels. This will not affect our decisions as to which paths to + * generate; we merely add these rels to their required_outer sets. + */ + foreach(lc, root->placeholder_list) + { + PlaceHolderInfo *phinfo = (PlaceHolderInfo *) lfirst(lc); + + /* PHVs without lateral refs can be skipped over quickly */ + if (phinfo->ph_lateral == NULL) + continue; + /* Is it due to be evaluated at this join, and not in either input? */ + if (bms_is_subset(phinfo->ph_eval_at, joinrel->relids) && + !bms_is_subset(phinfo->ph_eval_at, outerrel->relids) && + !bms_is_subset(phinfo->ph_eval_at, innerrel->relids)) + { + /* Yes, remember its lateral rels */ + extra_lateral_rels = bms_add_members(extra_lateral_rels, + phinfo->ph_lateral); + } + } + + /* + * Make sure extra_lateral_rels doesn't list anything within the join, and + * that it's NULL if empty. (This allows us to use bms_add_members to add + * it to required_outer below, while preserving the property that + * required_outer is exactly NULL if empty.) + */ + extra_lateral_rels = bms_del_members(extra_lateral_rels, joinrel->relids); + if (bms_is_empty(extra_lateral_rels)) + extra_lateral_rels = NULL; + /* * 1. Consider mergejoin paths where both relations must be explicitly * sorted. Skip this if we can't mergejoin. @@ -175,7 +213,8 @@ add_paths_to_joinrel(PlannerInfo *root, if (mergejoin_allowed) sort_inner_and_outer(root, joinrel, outerrel, innerrel, restrictlist, mergeclause_list, jointype, - sjinfo, param_source_rels); + sjinfo, + param_source_rels, extra_lateral_rels); /* * 2. Consider paths where the outer relation need not be explicitly @@ -187,7 +226,8 @@ add_paths_to_joinrel(PlannerInfo *root, if (mergejoin_allowed) match_unsorted_outer(root, joinrel, outerrel, innerrel, restrictlist, mergeclause_list, jointype, - sjinfo, &semifactors, param_source_rels); + sjinfo, &semifactors, + param_source_rels, extra_lateral_rels); #ifdef NOT_USED @@ -205,7 +245,8 @@ add_paths_to_joinrel(PlannerInfo *root, if (mergejoin_allowed) match_unsorted_inner(root, joinrel, outerrel, innerrel, restrictlist, mergeclause_list, jointype, - sjinfo, &semifactors, param_source_rels); + sjinfo, &semifactors, + param_source_rels, extra_lateral_rels); #endif /* @@ -216,7 +257,8 @@ add_paths_to_joinrel(PlannerInfo *root, if (enable_hashjoin || jointype == JOIN_FULL) hash_inner_and_outer(root, joinrel, outerrel, innerrel, restrictlist, jointype, - sjinfo, &semifactors, param_source_rels); + sjinfo, &semifactors, + param_source_rels, extra_lateral_rels); } /* @@ -231,6 +273,7 @@ try_nestloop_path(PlannerInfo *root, SpecialJoinInfo *sjinfo, SemiAntiJoinFactors *semifactors, Relids param_source_rels, + Relids extra_lateral_rels, Path *outer_path, Path *inner_path, List *restrict_clauses, @@ -253,6 +296,12 @@ try_nestloop_path(PlannerInfo *root, return; } + /* + * Independently of that, add parameterization needed for any + * PlaceHolderVars that need to be computed at the join. + */ + required_outer = bms_add_members(required_outer, extra_lateral_rels); + /* * Do a precheck to quickly eliminate obviously-inferior paths. We * calculate a cheap lower bound on the path's cost and then use @@ -301,6 +350,7 @@ try_mergejoin_path(PlannerInfo *root, JoinType jointype, SpecialJoinInfo *sjinfo, Relids param_source_rels, + Relids extra_lateral_rels, Path *outer_path, Path *inner_path, List *restrict_clauses, @@ -326,6 +376,12 @@ try_mergejoin_path(PlannerInfo *root, return; } + /* + * Independently of that, add parameterization needed for any + * PlaceHolderVars that need to be computed at the join. + */ + required_outer = bms_add_members(required_outer, extra_lateral_rels); + /* * If the given paths are already well enough ordered, we can skip doing * an explicit sort. @@ -383,6 +439,7 @@ try_hashjoin_path(PlannerInfo *root, SpecialJoinInfo *sjinfo, SemiAntiJoinFactors *semifactors, Relids param_source_rels, + Relids extra_lateral_rels, Path *outer_path, Path *inner_path, List *restrict_clauses, @@ -405,6 +462,12 @@ try_hashjoin_path(PlannerInfo *root, return; } + /* + * Independently of that, add parameterization needed for any + * PlaceHolderVars that need to be computed at the join. + */ + required_outer = bms_add_members(required_outer, extra_lateral_rels); + /* * See comments in try_nestloop_path(). Also note that hashjoin paths * never have any output pathkeys, per comments in create_hashjoin_path. @@ -483,6 +546,7 @@ clause_sides_match_join(RestrictInfo *rinfo, RelOptInfo *outerrel, * 'jointype' is the type of join to do * 'sjinfo' is extra info about the join for selectivity estimation * 'param_source_rels' are OK targets for parameterization of result paths + * 'extra_lateral_rels' are additional parameterization for result paths */ static void sort_inner_and_outer(PlannerInfo *root, @@ -493,7 +557,8 @@ sort_inner_and_outer(PlannerInfo *root, List *mergeclause_list, JoinType jointype, SpecialJoinInfo *sjinfo, - Relids param_source_rels) + Relids param_source_rels, + Relids extra_lateral_rels) { Path *outer_path; Path *inner_path; @@ -623,6 +688,7 @@ sort_inner_and_outer(PlannerInfo *root, jointype, sjinfo, param_source_rels, + extra_lateral_rels, outer_path, inner_path, restrictlist, @@ -668,6 +734,7 @@ sort_inner_and_outer(PlannerInfo *root, * 'sjinfo' is extra info about the join for selectivity estimation * 'semifactors' contains valid data if jointype is SEMI or ANTI * 'param_source_rels' are OK targets for parameterization of result paths + * 'extra_lateral_rels' are additional parameterization for result paths */ static void match_unsorted_outer(PlannerInfo *root, @@ -679,7 +746,8 @@ match_unsorted_outer(PlannerInfo *root, JoinType jointype, SpecialJoinInfo *sjinfo, SemiAntiJoinFactors *semifactors, - Relids param_source_rels) + Relids param_source_rels, + Relids extra_lateral_rels) { JoinType save_jointype = jointype; bool nestjoinOK; @@ -809,6 +877,7 @@ match_unsorted_outer(PlannerInfo *root, sjinfo, semifactors, param_source_rels, + extra_lateral_rels, outerpath, inner_cheapest_total, restrictlist, @@ -834,6 +903,7 @@ match_unsorted_outer(PlannerInfo *root, sjinfo, semifactors, param_source_rels, + extra_lateral_rels, outerpath, innerpath, restrictlist, @@ -848,6 +918,7 @@ match_unsorted_outer(PlannerInfo *root, sjinfo, semifactors, param_source_rels, + extra_lateral_rels, outerpath, matpath, restrictlist, @@ -903,6 +974,7 @@ match_unsorted_outer(PlannerInfo *root, jointype, sjinfo, param_source_rels, + extra_lateral_rels, outerpath, inner_cheapest_total, restrictlist, @@ -1001,6 +1073,7 @@ match_unsorted_outer(PlannerInfo *root, jointype, sjinfo, param_source_rels, + extra_lateral_rels, outerpath, innerpath, restrictlist, @@ -1046,6 +1119,7 @@ match_unsorted_outer(PlannerInfo *root, jointype, sjinfo, param_source_rels, + extra_lateral_rels, outerpath, innerpath, restrictlist, @@ -1080,6 +1154,7 @@ match_unsorted_outer(PlannerInfo *root, * 'sjinfo' is extra info about the join for selectivity estimation * 'semifactors' contains valid data if jointype is SEMI or ANTI * 'param_source_rels' are OK targets for parameterization of result paths + * 'extra_lateral_rels' are additional parameterization for result paths */ static void hash_inner_and_outer(PlannerInfo *root, @@ -1090,7 +1165,8 @@ hash_inner_and_outer(PlannerInfo *root, JoinType jointype, SpecialJoinInfo *sjinfo, SemiAntiJoinFactors *semifactors, - Relids param_source_rels) + Relids param_source_rels, + Relids extra_lateral_rels) { bool isouterjoin = IS_OUTER_JOIN(jointype); List *hashclauses; @@ -1164,6 +1240,7 @@ hash_inner_and_outer(PlannerInfo *root, sjinfo, semifactors, param_source_rels, + extra_lateral_rels, cheapest_total_outer, cheapest_total_inner, restrictlist, @@ -1183,6 +1260,7 @@ hash_inner_and_outer(PlannerInfo *root, sjinfo, semifactors, param_source_rels, + extra_lateral_rels, cheapest_total_outer, cheapest_total_inner, restrictlist, @@ -1195,6 +1273,7 @@ hash_inner_and_outer(PlannerInfo *root, sjinfo, semifactors, param_source_rels, + extra_lateral_rels, cheapest_startup_outer, cheapest_total_inner, restrictlist, @@ -1219,6 +1298,7 @@ hash_inner_and_outer(PlannerInfo *root, sjinfo, semifactors, param_source_rels, + extra_lateral_rels, cheapest_startup_outer, cheapest_total_inner, restrictlist, @@ -1256,6 +1336,7 @@ hash_inner_and_outer(PlannerInfo *root, sjinfo, semifactors, param_source_rels, + extra_lateral_rels, outerpath, innerpath, restrictlist, diff --git a/src/backend/optimizer/path/joinrels.c b/src/backend/optimizer/path/joinrels.c index 819498a4281..d627f9e130c 100644 --- a/src/backend/optimizer/path/joinrels.c +++ b/src/backend/optimizer/path/joinrels.c @@ -526,7 +526,7 @@ join_is_legal(PlannerInfo *root, RelOptInfo *rel1, RelOptInfo *rel2, { LateralJoinInfo *ljinfo = (LateralJoinInfo *) lfirst(l); - if (bms_is_member(ljinfo->lateral_rhs, rel2->relids) && + if (bms_is_subset(ljinfo->lateral_rhs, rel2->relids) && bms_overlap(ljinfo->lateral_lhs, rel1->relids)) { /* has to be implemented as nestloop with rel1 on left */ @@ -539,7 +539,7 @@ join_is_legal(PlannerInfo *root, RelOptInfo *rel1, RelOptInfo *rel2, (reversed || match_sjinfo->jointype == JOIN_FULL)) return false; /* not implementable as nestloop */ } - if (bms_is_member(ljinfo->lateral_rhs, rel1->relids) && + if (bms_is_subset(ljinfo->lateral_rhs, rel1->relids) && bms_overlap(ljinfo->lateral_lhs, rel2->relids)) { /* has to be implemented as nestloop with rel2 on left */ @@ -829,10 +829,10 @@ have_join_order_restriction(PlannerInfo *root, { LateralJoinInfo *ljinfo = (LateralJoinInfo *) lfirst(l); - if (bms_is_member(ljinfo->lateral_rhs, rel2->relids) && + if (bms_is_subset(ljinfo->lateral_rhs, rel2->relids) && bms_overlap(ljinfo->lateral_lhs, rel1->relids)) return true; - if (bms_is_member(ljinfo->lateral_rhs, rel1->relids) && + if (bms_is_subset(ljinfo->lateral_rhs, rel1->relids) && bms_overlap(ljinfo->lateral_lhs, rel2->relids)) return true; } @@ -928,7 +928,7 @@ has_join_restriction(PlannerInfo *root, RelOptInfo *rel) { LateralJoinInfo *ljinfo = (LateralJoinInfo *) lfirst(l); - if (bms_is_member(ljinfo->lateral_rhs, rel->relids) || + if (bms_is_subset(ljinfo->lateral_rhs, rel->relids) || bms_overlap(ljinfo->lateral_lhs, rel->relids)) return true; } diff --git a/src/backend/optimizer/path/orindxpath.c b/src/backend/optimizer/path/orindxpath.c index e51e5c08b52..16f29d350fd 100644 --- a/src/backend/optimizer/path/orindxpath.c +++ b/src/backend/optimizer/path/orindxpath.c @@ -103,7 +103,7 @@ create_or_index_quals(PlannerInfo *root, RelOptInfo *rel) RestrictInfo *rinfo = (RestrictInfo *) lfirst(i); if (restriction_is_or_clause(rinfo) && - join_clause_is_movable_to(rinfo, rel->relid)) + join_clause_is_movable_to(rinfo, rel)) { /* * Use the generate_bitmap_or_paths() machinery to estimate the diff --git a/src/backend/optimizer/path/tidpath.c b/src/backend/optimizer/path/tidpath.c index f49d0f96d04..256856da35e 100644 --- a/src/backend/optimizer/path/tidpath.c +++ b/src/backend/optimizer/path/tidpath.c @@ -255,8 +255,7 @@ create_tidscan_paths(PlannerInfo *root, RelOptInfo *rel) /* * We don't support pushing join clauses into the quals of a tidscan, but * it could still have required parameterization due to LATERAL refs in - * its tlist. (That can only happen if the tidscan is on a relation - * pulled up out of a UNION ALL appendrel.) + * its tlist. */ required_outer = rel->lateral_relids; diff --git a/src/backend/optimizer/plan/analyzejoins.c b/src/backend/optimizer/plan/analyzejoins.c index daab355b1d3..2271a7c35e0 100644 --- a/src/backend/optimizer/plan/analyzejoins.c +++ b/src/backend/optimizer/plan/analyzejoins.c @@ -202,7 +202,9 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo) * that will be used above the join. We only need to fail if such a PHV * actually references some inner-rel attributes; but the correct check * for that is relatively expensive, so we first check against ph_eval_at, - * which must mention the inner rel if the PHV uses any inner-rel attrs. + * which must mention the inner rel if the PHV uses any inner-rel attrs as + * non-lateral references. Note that if the PHV's syntactic scope is just + * the inner rel, we can't drop the rel even if the PHV is variable-free. */ foreach(l, root->placeholder_list) { @@ -210,9 +212,13 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo) if (bms_is_subset(phinfo->ph_needed, joinrelids)) continue; /* PHV is not used above the join */ + if (bms_overlap(phinfo->ph_lateral, innerrel->relids)) + return false; /* it references innerrel laterally */ if (!bms_overlap(phinfo->ph_eval_at, innerrel->relids)) continue; /* it definitely doesn't reference innerrel */ - if (bms_overlap(pull_varnos((Node *) phinfo->ph_var), + if (bms_is_subset(phinfo->ph_eval_at, innerrel->relids)) + return false; /* there isn't any other place to eval PHV */ + if (bms_overlap(pull_varnos((Node *) phinfo->ph_var->phexpr), innerrel->relids)) return false; /* it does reference innerrel */ } @@ -355,7 +361,7 @@ remove_rel_from_query(PlannerInfo *root, int relid, Relids joinrelids) * Likewise remove references from LateralJoinInfo data structures. * * If we are deleting a LATERAL subquery, we can forget its - * LateralJoinInfo altogether. Otherwise, make sure the target is not + * LateralJoinInfos altogether. Otherwise, make sure the target is not * included in any lateral_lhs set. (It probably can't be, since that * should have precluded deciding to remove it; but let's cope anyway.) */ @@ -364,29 +370,27 @@ remove_rel_from_query(PlannerInfo *root, int relid, Relids joinrelids) LateralJoinInfo *ljinfo = (LateralJoinInfo *) lfirst(l); nextl = lnext(l); - if (ljinfo->lateral_rhs == relid) + ljinfo->lateral_rhs = bms_del_member(ljinfo->lateral_rhs, relid); + if (bms_is_empty(ljinfo->lateral_rhs)) root->lateral_info_list = list_delete_ptr(root->lateral_info_list, ljinfo); else + { ljinfo->lateral_lhs = bms_del_member(ljinfo->lateral_lhs, relid); + Assert(!bms_is_empty(ljinfo->lateral_lhs)); + } } /* * Likewise remove references from PlaceHolderVar data structures. - * - * Here we have a special case: if a PHV's eval_at set is just the target - * relid, we want to leave it that way instead of reducing it to the empty - * set. An empty eval_at set would confuse later processing since it - * would match every possible eval placement. */ foreach(l, root->placeholder_list) { PlaceHolderInfo *phinfo = (PlaceHolderInfo *) lfirst(l); phinfo->ph_eval_at = bms_del_member(phinfo->ph_eval_at, relid); - if (bms_is_empty(phinfo->ph_eval_at)) /* oops, belay that */ - phinfo->ph_eval_at = bms_add_member(phinfo->ph_eval_at, relid); - + Assert(!bms_is_empty(phinfo->ph_eval_at)); + Assert(!bms_is_member(relid, phinfo->ph_lateral)); phinfo->ph_needed = bms_del_member(phinfo->ph_needed, relid); } diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c index 52bab79007e..c501737a267 100644 --- a/src/backend/optimizer/plan/createplan.c +++ b/src/backend/optimizer/plan/createplan.c @@ -44,9 +44,9 @@ static Plan *create_plan_recurse(PlannerInfo *root, Path *best_path); static Plan *create_scan_plan(PlannerInfo *root, Path *best_path); -static List *build_relation_tlist(RelOptInfo *rel); +static List *build_path_tlist(PlannerInfo *root, Path *path); static bool use_physical_tlist(PlannerInfo *root, RelOptInfo *rel); -static void disuse_physical_tlist(Plan *plan, Path *path); +static void disuse_physical_tlist(PlannerInfo *root, Plan *plan, Path *path); static Plan *create_gating_plan(PlannerInfo *root, Plan *plan, List *quals); static Plan *create_join_plan(PlannerInfo *root, JoinPath *best_path); static Plan *create_append_plan(PlannerInfo *root, AppendPath *best_path); @@ -305,21 +305,12 @@ create_scan_plan(PlannerInfo *root, Path *best_path) tlist = build_physical_tlist(root, rel); /* if fail because of dropped cols, use regular method */ if (tlist == NIL) - tlist = build_relation_tlist(rel); + tlist = build_path_tlist(root, best_path); } } else { - tlist = build_relation_tlist(rel); - - /* - * If it's a parameterized otherrel, there might be lateral references - * in the tlist, which need to be replaced with Params. This cannot - * happen for regular baserels, though. Note use_physical_tlist() - * always fails for otherrels, so we don't need to check this above. - */ - if (rel->reloptkind != RELOPT_BASEREL && best_path->param_info) - tlist = (List *) replace_nestloop_params(root, (Node *) tlist); + tlist = build_path_tlist(root, best_path); } /* @@ -439,11 +430,12 @@ create_scan_plan(PlannerInfo *root, Path *best_path) } /* - * Build a target list (ie, a list of TargetEntry) for a relation. + * Build a target list (ie, a list of TargetEntry) for the Path's output. */ static List * -build_relation_tlist(RelOptInfo *rel) +build_path_tlist(PlannerInfo *root, Path *path) { + RelOptInfo *rel = path->parent; List *tlist = NIL; int resno = 1; ListCell *v; @@ -453,6 +445,15 @@ build_relation_tlist(RelOptInfo *rel) /* Do we really need to copy here? Not sure */ Node *node = (Node *) copyObject(lfirst(v)); + /* + * If it's a parameterized path, there might be lateral references in + * the tlist, which need to be replaced with Params. There's no need + * to remake the TargetEntry nodes, so apply this to each list item + * separately. + */ + if (path->param_info) + node = replace_nestloop_params(root, node); + tlist = lappend(tlist, makeTargetEntry((Expr *) node, resno, NULL, @@ -528,7 +529,7 @@ use_physical_tlist(PlannerInfo *root, RelOptInfo *rel) * and Material nodes want this, so they don't have to store useless columns. */ static void -disuse_physical_tlist(Plan *plan, Path *path) +disuse_physical_tlist(PlannerInfo *root, Plan *plan, Path *path) { /* Only need to undo it for path types handled by create_scan_plan() */ switch (path->pathtype) @@ -544,7 +545,7 @@ disuse_physical_tlist(Plan *plan, Path *path) case T_CteScan: case T_WorkTableScan: case T_ForeignScan: - plan->targetlist = build_relation_tlist(path->parent); + plan->targetlist = build_path_tlist(root, path); break; default: break; @@ -678,7 +679,7 @@ static Plan * create_append_plan(PlannerInfo *root, AppendPath *best_path) { Append *plan; - List *tlist = build_relation_tlist(best_path->path.parent); + List *tlist = build_path_tlist(root, &best_path->path); List *subplans = NIL; ListCell *subpaths; @@ -733,7 +734,7 @@ create_merge_append_plan(PlannerInfo *root, MergeAppendPath *best_path) { MergeAppend *node = makeNode(MergeAppend); Plan *plan = &node->plan; - List *tlist = build_relation_tlist(best_path->path.parent); + List *tlist = build_path_tlist(root, &best_path->path); List *pathkeys = best_path->path.pathkeys; List *subplans = NIL; ListCell *subpaths; @@ -862,7 +863,7 @@ create_material_plan(PlannerInfo *root, MaterialPath *best_path) subplan = create_plan_recurse(root, best_path->subpath); /* We don't want any excess columns in the materialized tuples */ - disuse_physical_tlist(subplan, best_path->subpath); + disuse_physical_tlist(root, subplan, best_path->subpath); plan = make_material(subplan); @@ -911,7 +912,7 @@ create_unique_plan(PlannerInfo *root, UniquePath *best_path) * should be left as-is if we don't need to add any expressions; but if we * do have to add expressions, then a projection step will be needed at * runtime anyway, so we may as well remove unneeded items. Therefore - * newtlist starts from build_relation_tlist() not just a copy of the + * newtlist starts from build_path_tlist() not just a copy of the * subplan's tlist; and we don't install it into the subplan unless we are * sorting or stuff has to be added. */ @@ -919,7 +920,7 @@ create_unique_plan(PlannerInfo *root, UniquePath *best_path) uniq_exprs = best_path->uniq_exprs; /* initialize modified subplan tlist as just the "required" vars */ - newtlist = build_relation_tlist(best_path->path.parent); + newtlist = build_path_tlist(root, &best_path->path); nextresno = list_length(newtlist) + 1; newitems = false; @@ -1009,7 +1010,7 @@ create_unique_plan(PlannerInfo *root, UniquePath *best_path) * subplan tlist. */ plan = (Plan *) make_agg(root, - build_relation_tlist(best_path->path.parent), + build_path_tlist(root, &best_path->path), NIL, AGG_HASHED, NULL, @@ -2028,7 +2029,7 @@ create_nestloop_plan(PlannerInfo *root, Plan *inner_plan) { NestLoop *join_plan; - List *tlist = build_relation_tlist(best_path->path.parent); + List *tlist = build_path_tlist(root, &best_path->path); List *joinrestrictclauses = best_path->joinrestrictinfo; List *joinclauses; List *otherclauses; @@ -2118,7 +2119,7 @@ create_mergejoin_plan(PlannerInfo *root, Plan *outer_plan, Plan *inner_plan) { - List *tlist = build_relation_tlist(best_path->jpath.path.parent); + List *tlist = build_path_tlist(root, &best_path->jpath.path); List *joinclauses; List *otherclauses; List *mergeclauses; @@ -2186,7 +2187,7 @@ create_mergejoin_plan(PlannerInfo *root, */ if (best_path->outersortkeys) { - disuse_physical_tlist(outer_plan, best_path->jpath.outerjoinpath); + disuse_physical_tlist(root, outer_plan, best_path->jpath.outerjoinpath); outer_plan = (Plan *) make_sort_from_pathkeys(root, outer_plan, @@ -2199,7 +2200,7 @@ create_mergejoin_plan(PlannerInfo *root, if (best_path->innersortkeys) { - disuse_physical_tlist(inner_plan, best_path->jpath.innerjoinpath); + disuse_physical_tlist(root, inner_plan, best_path->jpath.innerjoinpath); inner_plan = (Plan *) make_sort_from_pathkeys(root, inner_plan, @@ -2413,7 +2414,7 @@ create_hashjoin_plan(PlannerInfo *root, Plan *outer_plan, Plan *inner_plan) { - List *tlist = build_relation_tlist(best_path->jpath.path.parent); + List *tlist = build_path_tlist(root, &best_path->jpath.path); List *joinclauses; List *otherclauses; List *hashclauses; @@ -2470,11 +2471,11 @@ create_hashjoin_plan(PlannerInfo *root, best_path->jpath.outerjoinpath->parent->relids); /* We don't want any excess columns in the hashed tuples */ - disuse_physical_tlist(inner_plan, best_path->jpath.innerjoinpath); + disuse_physical_tlist(root, inner_plan, best_path->jpath.innerjoinpath); /* If we expect batching, suppress excess columns in outer tuples too */ if (best_path->num_batches > 1) - disuse_physical_tlist(outer_plan, best_path->jpath.outerjoinpath); + disuse_physical_tlist(root, outer_plan, best_path->jpath.outerjoinpath); /* * If there is a single join clause and we can identify the outer variable @@ -2604,16 +2605,37 @@ replace_nestloop_params_mutator(Node *node, PlannerInfo *root) Assert(phv->phlevelsup == 0); /* - * If not to be replaced, just return the PlaceHolderVar unmodified. - * We use bms_overlap as a cheap/quick test to see if the PHV might be - * evaluated in the outer rels, and then grab its PlaceHolderInfo to - * tell for sure. + * Check whether we need to replace the PHV. We use bms_overlap as a + * cheap/quick test to see if the PHV might be evaluated in the outer + * rels, and then grab its PlaceHolderInfo to tell for sure. */ - if (!bms_overlap(phv->phrels, root->curOuterRels)) - return node; - if (!bms_is_subset(find_placeholder_info(root, phv, false)->ph_eval_at, - root->curOuterRels)) - return node; + if (!bms_overlap(phv->phrels, root->curOuterRels) || + !bms_is_subset(find_placeholder_info(root, phv, false)->ph_eval_at, + root->curOuterRels)) + { + /* + * We can't replace the whole PHV, but we might still need to + * replace Vars or PHVs within its expression, in case it ends up + * actually getting evaluated here. (It might get evaluated in + * this plan node, or some child node; in the latter case we don't + * really need to process the expression here, but we haven't got + * enough info to tell if that's the case.) Flat-copy the PHV + * node and then recurse on its expression. + * + * Note that after doing this, we might have different + * representations of the contents of the same PHV in different + * parts of the plan tree. This is OK because equal() will just + * match on phid/phlevelsup, so setrefs.c will still recognize an + * upper-level reference to a lower-level copy of the same PHV. + */ + PlaceHolderVar *newphv = makeNode(PlaceHolderVar); + + memcpy(newphv, phv, sizeof(PlaceHolderVar)); + newphv->phexpr = (Expr *) + replace_nestloop_params_mutator((Node *) phv->phexpr, + root); + return (Node *) newphv; + } /* Create a Param representing the PlaceHolderVar */ param = assign_nestloop_param_placeholdervar(root, phv); /* Is this param already listed in root->curOuterParams? */ diff --git a/src/backend/optimizer/plan/initsplan.c b/src/backend/optimizer/plan/initsplan.c index 07c4dddd24e..98f601cdede 100644 --- a/src/backend/optimizer/plan/initsplan.c +++ b/src/backend/optimizer/plan/initsplan.c @@ -38,7 +38,7 @@ int join_collapse_limit; static void extract_lateral_references(PlannerInfo *root, RelOptInfo *brel, Index rtindex); -static void add_lateral_info(PlannerInfo *root, Index rhs, Relids lhs); +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); @@ -177,6 +177,8 @@ add_vars_to_targetlist(PlannerInfo *root, List *vars, RelOptInfo *rel = find_base_rel(root, var->varno); int attno = var->varattno; + if (bms_is_subset(where_needed, rel->relids)) + continue; Assert(attno >= rel->min_attr && attno <= rel->max_attr); attno -= rel->min_attr; if (rel->attr_needed[attno] == NULL) @@ -221,6 +223,12 @@ add_vars_to_targetlist(PlannerInfo *root, List *vars, * ensure that the Vars/PHVs propagate up to the nestloop join level; this * means setting suitable where_needed values for them. * + * Note that this only deals with lateral references in unflattened LATERAL + * subqueries. When we flatten a LATERAL subquery, its lateral references + * become plain Vars in the parent query, but they may have to be wrapped in + * PlaceHolderVars if they need to be forced NULL by outer joins that don't + * also null the LATERAL subquery. That's all handled elsewhere. + * * This has to run before deconstruct_jointree, since it might result in * creation of PlaceHolderInfos. */ @@ -250,16 +258,18 @@ find_lateral_references(PlannerInfo *root) * This bit is less obvious than it might look. We ignore appendrel * otherrels and consider only their parent baserels. In a case where * a LATERAL-containing UNION ALL subquery was pulled up, it is the - * otherrels that are actually going to be in the plan. However, we - * want to mark all their lateral references as needed by the parent, + * otherrel that is actually going to be in the plan. However, we + * want to mark all its lateral references as needed by the parent, * because it is the parent's relid that will be used for join * planning purposes. And the parent's RTE will contain all the - * lateral references we need to know, since the pulled-up members are - * nothing but copies of parts of the original RTE's subquery. We - * could visit the children instead and transform their references - * back to the parent's relid, but it would be much more complicated - * for no real gain. (Important here is that the child members have - * not yet received any processing beyond being pulled up.) + * lateral references we need to know, since the pulled-up member is + * nothing but a copy of parts of the original RTE's subquery. We + * could visit the parent's children instead and transform their + * references back to the parent's relid, but it would be much more + * complicated for no real gain. (Important here is that the child + * members have not yet received any processing beyond being pulled + * up.) Similarly, in appendrels created by inheritance expansion, + * it's sufficient to look at the parent relation. */ /* ignore RTEs that are "other rels" */ @@ -346,7 +356,10 @@ extract_lateral_references(PlannerInfo *root, RelOptInfo *brel, Index rtindex) */ where_needed = bms_make_singleton(rtindex); - /* Push the Vars into their source relations' targetlists */ + /* + * Push Vars into their source relations' targetlists, and PHVs into + * root->placeholder_list. + */ add_vars_to_targetlist(root, newvars, where_needed, true); /* Remember the lateral references for create_lateral_join_info */ @@ -355,16 +368,20 @@ extract_lateral_references(PlannerInfo *root, RelOptInfo *brel, Index rtindex) /* * create_lateral_join_info - * For each LATERAL subquery, create LateralJoinInfo(s) and add them to - * root->lateral_info_list, and fill in the per-rel lateral_relids sets. + * For each unflattened LATERAL subquery, create LateralJoinInfo(s) and add + * them to root->lateral_info_list, and fill in the per-rel lateral_relids + * and lateral_referencers sets. Also generate LateralJoinInfo(s) to + * represent any lateral references within PlaceHolderVars (this part deals + * with the effects of flattened LATERAL subqueries). * * This has to run after deconstruct_jointree, because we need to know the - * final ph_eval_at values for referenced PlaceHolderVars. + * final ph_eval_at values for PlaceHolderVars. */ void create_lateral_join_info(PlannerInfo *root) { Index rti; + ListCell *lc; /* We need do nothing if the query contains no LATERAL RTEs */ if (!root->hasLateralRTEs) @@ -377,7 +394,6 @@ create_lateral_join_info(PlannerInfo *root) { RelOptInfo *brel = root->simple_rel_array[rti]; Relids lateral_relids; - ListCell *lc; /* there may be empty slots corresponding to non-baserel RTEs */ if (brel == NULL) @@ -400,7 +416,8 @@ create_lateral_join_info(PlannerInfo *root) { Var *var = (Var *) node; - add_lateral_info(root, rti, bms_make_singleton(var->varno)); + add_lateral_info(root, bms_make_singleton(var->varno), + brel->relids); lateral_relids = bms_add_member(lateral_relids, var->varno); } @@ -410,7 +427,7 @@ create_lateral_join_info(PlannerInfo *root) PlaceHolderInfo *phinfo = find_placeholder_info(root, phv, false); - add_lateral_info(root, rti, bms_copy(phinfo->ph_eval_at)); + add_lateral_info(root, phinfo->ph_eval_at, brel->relids); lateral_relids = bms_add_members(lateral_relids, phinfo->ph_eval_at); } @@ -422,15 +439,100 @@ create_lateral_join_info(PlannerInfo *root) if (bms_is_empty(lateral_relids)) continue; /* ensure lateral_relids is NULL if empty */ brel->lateral_relids = lateral_relids; + } + + /* + * Now check for lateral references within PlaceHolderVars, and make + * LateralJoinInfos describing each such reference. Unlike references in + * unflattened LATERAL RTEs, the referencing location could be a join. + */ + foreach(lc, root->placeholder_list) + { + PlaceHolderInfo *phinfo = (PlaceHolderInfo *) lfirst(lc); + Relids eval_at = phinfo->ph_eval_at; + + if (phinfo->ph_lateral != NULL) + { + List *vars = pull_var_clause((Node *) phinfo->ph_var->phexpr, + PVC_RECURSE_AGGREGATES, + PVC_INCLUDE_PLACEHOLDERS); + ListCell *lc2; + + foreach(lc2, vars) + { + Node *node = (Node *) lfirst(lc2); + + if (IsA(node, Var)) + { + Var *var = (Var *) node; + + if (!bms_is_member(var->varno, eval_at)) + add_lateral_info(root, + bms_make_singleton(var->varno), + eval_at); + } + else if (IsA(node, PlaceHolderVar)) + { + PlaceHolderVar *other_phv = (PlaceHolderVar *) node; + PlaceHolderInfo *other_phi; + + other_phi = find_placeholder_info(root, other_phv, + false); + if (!bms_is_subset(other_phi->ph_eval_at, eval_at)) + add_lateral_info(root, other_phi->ph_eval_at, eval_at); + } + else + Assert(false); + } + + list_free(vars); + } + } + + /* If we found no lateral references, we're done. */ + if (root->lateral_info_list == NIL) + return; + + /* + * Now that we've identified all lateral references, make a second pass in + * which we mark each baserel with the set of relids of rels that + * reference it laterally (essentially, the inverse mapping of + * lateral_relids). We'll need this for join_clause_is_movable_to(). + * + * Also, propagate lateral_relids and lateral_referencers from appendrel + * parent rels to their child rels. We intentionally give each child rel + * the same minimum parameterization, even though it's quite possible that + * some don't reference all the lateral rels. This is because any append + * path for the parent will have to have the same parameterization for + * every child anyway, and there's no value in forcing extra + * reparameterize_path() calls. Similarly, a lateral reference to the + * parent prevents use of otherwise-movable join rels for each child. + */ + for (rti = 1; rti < root->simple_rel_array_size; rti++) + { + RelOptInfo *brel = root->simple_rel_array[rti]; + Relids lateral_referencers; + + if (brel == NULL) + continue; + if (brel->reloptkind != RELOPT_BASEREL) + continue; + + /* Compute lateral_referencers using the finished lateral_info_list */ + lateral_referencers = NULL; + foreach(lc, root->lateral_info_list) + { + LateralJoinInfo *ljinfo = (LateralJoinInfo *) lfirst(lc); + + if (bms_is_member(brel->relid, ljinfo->lateral_lhs)) + lateral_referencers = bms_add_members(lateral_referencers, + ljinfo->lateral_rhs); + } + brel->lateral_referencers = lateral_referencers; /* - * If it's an appendrel parent, copy its lateral_relids to each child - * rel. We intentionally give each child rel the same minimum - * parameterization, even though it's quite possible that some don't - * reference all the lateral rels. This is because any append path - * for the parent will have to have the same parameterization for - * every child anyway, and there's no value in forcing extra - * reparameterize_path() calls. + * If it's an appendrel parent, copy its lateral_relids and + * lateral_referencers to each child rel. */ if (root->simple_rte_array[rti]->inh) { @@ -444,7 +546,9 @@ create_lateral_join_info(PlannerInfo *root) childrel = root->simple_rel_array[appinfo->child_relid]; Assert(childrel->reloptkind == RELOPT_OTHER_MEMBER_REL); Assert(childrel->lateral_relids == NULL); - childrel->lateral_relids = lateral_relids; + childrel->lateral_relids = brel->lateral_relids; + Assert(childrel->lateral_referencers == NULL); + childrel->lateral_referencers = brel->lateral_referencers; } } } @@ -454,38 +558,39 @@ create_lateral_join_info(PlannerInfo *root) * add_lateral_info * Add a LateralJoinInfo to root->lateral_info_list, if needed * - * We suppress redundant list entries. The passed lhs set must be freshly - * made; we free it if not used in a new list entry. + * We suppress redundant list entries. The passed Relids are copied if saved. */ static void -add_lateral_info(PlannerInfo *root, Index rhs, Relids lhs) +add_lateral_info(PlannerInfo *root, Relids lhs, Relids rhs) { LateralJoinInfo *ljinfo; - ListCell *l; + ListCell *lc; - Assert(!bms_is_member(rhs, lhs)); + /* Sanity-check the input */ + Assert(!bms_is_empty(lhs)); + Assert(!bms_is_empty(rhs)); + Assert(!bms_overlap(lhs, rhs)); /* - * If an existing list member has the same RHS and an LHS that is a subset - * of the new one, it's redundant, but we don't trouble to get rid of it. - * The only case that is really worth worrying about is identical entries, - * and we handle that well enough with this simple logic. + * The input is redundant if it has the same RHS and an LHS that is a + * subset of an existing entry's. If an existing entry has the same RHS + * and an LHS that is a subset of the new one, it's redundant, but we + * don't trouble to get rid of it. The only case that is really worth + * worrying about is identical entries, and we handle that well enough + * with this simple logic. */ - foreach(l, root->lateral_info_list) + foreach(lc, root->lateral_info_list) { - ljinfo = (LateralJoinInfo *) lfirst(l); - if (rhs == ljinfo->lateral_rhs && + ljinfo = (LateralJoinInfo *) lfirst(lc); + if (bms_equal(rhs, ljinfo->lateral_rhs) && bms_is_subset(lhs, ljinfo->lateral_lhs)) - { - bms_free(lhs); return; - } } /* Not there, so make a new entry */ ljinfo = makeNode(LateralJoinInfo); - ljinfo->lateral_rhs = rhs; - ljinfo->lateral_lhs = lhs; + ljinfo->lateral_lhs = bms_copy(lhs); + ljinfo->lateral_rhs = bms_copy(rhs); root->lateral_info_list = lappend(root->lateral_info_list, ljinfo); } diff --git a/src/backend/optimizer/plan/planmain.c b/src/backend/optimizer/plan/planmain.c index 42a98945a38..284929f125e 100644 --- a/src/backend/optimizer/plan/planmain.c +++ b/src/backend/optimizer/plan/planmain.c @@ -175,12 +175,6 @@ query_planner(PlannerInfo *root, List *tlist, joinlist = deconstruct_jointree(root); - /* - * Create the LateralJoinInfo list now that we have finalized - * PlaceHolderVar eval levels. - */ - create_lateral_join_info(root); - /* * Reconsider any postponed outer-join quals now that we have built up * equivalence classes. (This could result in further additions or @@ -225,6 +219,13 @@ query_planner(PlannerInfo *root, List *tlist, */ add_placeholders_to_base_rels(root); + /* + * Create the LateralJoinInfo list now that we have finalized + * PlaceHolderVar eval levels and made any necessary additions to the + * lateral_vars lists for lateral references within PlaceHolderVars. + */ + create_lateral_join_info(root); + /* * We should now have size estimates for every actual table involved in * the query, and we also know which if any have been deleted from the diff --git a/src/backend/optimizer/prep/prepjointree.c b/src/backend/optimizer/prep/prepjointree.c index 52842931ec5..1178b0fc996 100644 --- a/src/backend/optimizer/prep/prepjointree.c +++ b/src/backend/optimizer/prep/prepjointree.c @@ -41,6 +41,8 @@ typedef struct pullup_replace_vars_context PlannerInfo *root; List *targetlist; /* tlist of subquery being pulled up */ RangeTblEntry *target_rte; /* RTE of subquery */ + Relids relids; /* relids within subquery, as numbered after + * pullup (set only if target_rte->lateral) */ bool *outer_hasSubLinks; /* -> outer query's hasSubLinks */ int varno; /* varno of subquery */ bool need_phvs; /* do we need PlaceHolderVars? */ @@ -884,14 +886,19 @@ pull_up_simple_subquery(PlannerInfo *root, Node *jtnode, RangeTblEntry *rte, /* * The subquery's targetlist items are now in the appropriate form to * insert into the top query, but if we are under an outer join then - * non-nullable items may have to be turned into PlaceHolderVars. If we - * are dealing with an appendrel member then anything that's not a simple - * Var has to be turned into a PlaceHolderVar. Set up appropriate context - * data for pullup_replace_vars. + * non-nullable items and lateral references may have to be turned into + * PlaceHolderVars. If we are dealing with an appendrel member then + * anything that's not a simple Var has to be turned into a + * PlaceHolderVar. Set up required context data for pullup_replace_vars. */ rvcontext.root = root; rvcontext.targetlist = subquery->targetList; rvcontext.target_rte = rte; + if (rte->lateral) + rvcontext.relids = get_relids_in_jointree((Node *) subquery->jointree, + true); + else /* won't need relids */ + rvcontext.relids = NULL; rvcontext.outer_hasSubLinks = &parse->hasSubLinks; rvcontext.varno = varno; rvcontext.need_phvs = (lowest_nulling_outer_join != NULL || @@ -1674,8 +1681,18 @@ pullup_replace_vars_callback(Var *var, if (newnode && IsA(newnode, Var) && ((Var *) newnode)->varlevelsup == 0) { - /* Simple Vars always escape being wrapped */ - wrap = false; + /* + * Simple Vars always escape being wrapped, unless they are + * lateral references to something outside the subquery being + * pulled up. (Even then, we could omit the PlaceHolderVar if + * the referenced rel is under the same lowest outer join, but + * it doesn't seem worth the trouble to check that.) + */ + if (rcon->target_rte->lateral && + !bms_is_member(((Var *) newnode)->varno, rcon->relids)) + wrap = true; + else + wrap = false; } else if (newnode && IsA(newnode, PlaceHolderVar) && ((PlaceHolderVar *) newnode)->phlevelsup == 0) @@ -1691,9 +1708,10 @@ pullup_replace_vars_callback(Var *var, else { /* - * If it contains a Var of current level, and does not contain - * any non-strict constructs, then it's certainly nullable so - * we don't need to insert a PlaceHolderVar. + * If it contains a Var of the subquery being pulled up, and + * does not contain any non-strict constructs, then it's + * certainly nullable so we don't need to insert a + * PlaceHolderVar. * * This analysis could be tighter: in particular, a non-strict * construct hidden within a lower-level PlaceHolderVar is not @@ -1702,8 +1720,14 @@ pullup_replace_vars_callback(Var *var, * * Note: in future maybe we should insert a PlaceHolderVar * anyway, if the tlist item is expensive to evaluate? + * + * For a LATERAL subquery, we have to check the actual var + * membership of the node, but if it's non-lateral then any + * level-zero var must belong to the subquery. */ - if (contain_vars_of_level((Node *) newnode, 0) && + if ((rcon->target_rte->lateral ? + bms_overlap(pull_varnos((Node *) newnode), rcon->relids) : + contain_vars_of_level((Node *) newnode, 0)) && !contain_nonstrict_functions((Node *) newnode)) { /* No wrap needed */ diff --git a/src/backend/optimizer/util/placeholder.c b/src/backend/optimizer/util/placeholder.c index da92497689b..5049ba1c5a9 100644 --- a/src/backend/optimizer/util/placeholder.c +++ b/src/backend/optimizer/util/placeholder.c @@ -69,6 +69,7 @@ find_placeholder_info(PlannerInfo *root, PlaceHolderVar *phv, bool create_new_ph) { PlaceHolderInfo *phinfo; + Relids rels_used; ListCell *lc; /* if this ever isn't true, we'd need to be able to look in parent lists */ @@ -89,8 +90,24 @@ find_placeholder_info(PlannerInfo *root, PlaceHolderVar *phv, phinfo->phid = phv->phid; phinfo->ph_var = copyObject(phv); - /* initialize ph_eval_at as the set of contained relids */ - phinfo->ph_eval_at = pull_varnos((Node *) phv); + + /* + * Any referenced rels that are outside the PHV's syntactic scope are + * LATERAL references, which should be included in ph_lateral but not in + * ph_eval_at. If no referenced rels are within the syntactic scope, + * force evaluation at the syntactic location. + */ + rels_used = pull_varnos((Node *) phv->phexpr); + phinfo->ph_lateral = bms_difference(rels_used, phv->phrels); + if (bms_is_empty(phinfo->ph_lateral)) + phinfo->ph_lateral = NULL; /* make it exactly NULL if empty */ + phinfo->ph_eval_at = bms_int_members(rels_used, phv->phrels); + /* If no contained vars, force evaluation at syntactic location */ + if (bms_is_empty(phinfo->ph_eval_at)) + { + phinfo->ph_eval_at = bms_copy(phv->phrels); + Assert(!bms_is_empty(phinfo->ph_eval_at)); + } /* ph_eval_at may change later, see update_placeholder_eval_levels */ phinfo->ph_needed = NULL; /* initially it's unused */ /* for the moment, estimate width using just the datatype info */ @@ -115,6 +132,12 @@ find_placeholder_info(PlannerInfo *root, PlaceHolderVar *phv, * * We don't need to look at the targetlist because build_base_rel_tlists() * will already have made entries for any PHVs in the tlist. + * + * This is called before we begin deconstruct_jointree. Once we begin + * deconstruct_jointree, all active placeholders must be present in + * root->placeholder_list, because make_outerjoininfo and + * update_placeholder_eval_levels require this info to be available + * while we crawl up the join tree. */ void find_placeholders_in_jointree(PlannerInfo *root) @@ -219,7 +242,7 @@ find_placeholders_in_expr(PlannerInfo *root, Node *expr) * The initial eval_at level set by find_placeholder_info was the set of * rels used in the placeholder's expression (or the whole subselect below * the placeholder's syntactic location, if the expr is variable-free). - * If the subselect contains any outer joins that can null any of those rels, + * If the query contains any outer joins that can null any of those rels, * we must delay evaluation to above those joins. * * We repeat this operation each time we add another outer join to @@ -299,6 +322,9 @@ update_placeholder_eval_levels(PlannerInfo *root, SpecialJoinInfo *new_sjinfo) } } while (found_some); + /* Can't move the PHV's eval_at level to above its syntactic level */ + Assert(bms_is_subset(eval_at, syn_level)); + phinfo->ph_eval_at = eval_at; } } @@ -309,11 +335,14 @@ update_placeholder_eval_levels(PlannerInfo *root, SpecialJoinInfo *new_sjinfo) * * This is called after we've finished determining the eval_at levels for * all placeholders. We need to make sure that all vars and placeholders - * needed to evaluate each placeholder will be available at the join level - * where the evaluation will be done. Note that this loop can have - * side-effects on the ph_needed sets of other PlaceHolderInfos; that's okay - * because we don't examine ph_needed here, so there are no ordering issues - * to worry about. + * needed to evaluate each placeholder will be available at the scan or join + * level where the evaluation will be done. (It might seem that scan-level + * evaluations aren't interesting, but that's not so: a LATERAL reference + * within a placeholder's expression needs to cause the referenced var or + * placeholder to be marked as needed in the scan where it's evaluated.) + * Note that this loop can have side-effects on the ph_needed sets of other + * PlaceHolderInfos; that's okay because we don't examine ph_needed here, so + * there are no ordering issues to worry about. */ void fix_placeholder_input_needed_levels(PlannerInfo *root) @@ -323,27 +352,23 @@ fix_placeholder_input_needed_levels(PlannerInfo *root) foreach(lc, root->placeholder_list) { PlaceHolderInfo *phinfo = (PlaceHolderInfo *) lfirst(lc); - Relids eval_at = phinfo->ph_eval_at; + List *vars = pull_var_clause((Node *) phinfo->ph_var->phexpr, + PVC_RECURSE_AGGREGATES, + PVC_INCLUDE_PLACEHOLDERS); - /* No work unless it'll be evaluated above baserel level */ - if (bms_membership(eval_at) == BMS_MULTIPLE) - { - List *vars = pull_var_clause((Node *) phinfo->ph_var->phexpr, - PVC_RECURSE_AGGREGATES, - PVC_INCLUDE_PLACEHOLDERS); - - add_vars_to_targetlist(root, vars, eval_at, false); - list_free(vars); - } + add_vars_to_targetlist(root, vars, phinfo->ph_eval_at, false); + list_free(vars); } } /* * add_placeholders_to_base_rels - * Add any required PlaceHolderVars to base rels' targetlists. + * Add any required PlaceHolderVars to base rels' targetlists, and + * update lateral_vars lists for lateral references contained in them. * * If any placeholder can be computed at a base rel and is needed above it, - * add it to that rel's targetlist. This might look like it could be merged + * add it to that rel's targetlist, and add any lateral references it requires + * to the rel's lateral_vars list. This might look like it could be merged * with fix_placeholder_input_needed_levels, but it must be separate because * join removal happens in between, and can change the ph_eval_at sets. There * is essentially the same logic in add_placeholders_to_joinrel, but we can't @@ -359,14 +384,52 @@ add_placeholders_to_base_rels(PlannerInfo *root) PlaceHolderInfo *phinfo = (PlaceHolderInfo *) lfirst(lc); Relids eval_at = phinfo->ph_eval_at; - if (bms_membership(eval_at) == BMS_SINGLETON && - bms_nonempty_difference(phinfo->ph_needed, eval_at)) + if (bms_membership(eval_at) == BMS_SINGLETON) { int varno = bms_singleton_member(eval_at); RelOptInfo *rel = find_base_rel(root, varno); - rel->reltargetlist = lappend(rel->reltargetlist, - copyObject(phinfo->ph_var)); + /* add it to reltargetlist if needed above the rel scan level */ + if (bms_nonempty_difference(phinfo->ph_needed, eval_at)) + rel->reltargetlist = lappend(rel->reltargetlist, + copyObject(phinfo->ph_var)); + /* if there are lateral refs in it, add them to lateral_vars */ + if (phinfo->ph_lateral != NULL) + { + List *vars = pull_var_clause((Node *) phinfo->ph_var->phexpr, + PVC_RECURSE_AGGREGATES, + PVC_INCLUDE_PLACEHOLDERS); + ListCell *lc2; + + foreach(lc2, vars) + { + Node *node = (Node *) lfirst(lc2); + + if (IsA(node, Var)) + { + Var *var = (Var *) node; + + if (var->varno != varno) + rel->lateral_vars = lappend(rel->lateral_vars, + var); + } + else if (IsA(node, PlaceHolderVar)) + { + PlaceHolderVar *other_phv = (PlaceHolderVar *) node; + PlaceHolderInfo *other_phi; + + other_phi = find_placeholder_info(root, other_phv, + false); + if (!bms_is_subset(other_phi->ph_eval_at, eval_at)) + rel->lateral_vars = lappend(rel->lateral_vars, + other_phv); + } + else + Assert(false); + } + + list_free(vars); + } } } } diff --git a/src/backend/optimizer/util/relnode.c b/src/backend/optimizer/util/relnode.c index 8ee5671a551..b6265b31675 100644 --- a/src/backend/optimizer/util/relnode.c +++ b/src/backend/optimizer/util/relnode.c @@ -113,6 +113,7 @@ build_simple_rel(PlannerInfo *root, int relid, RelOptKind reloptkind) /* min_attr, max_attr, attr_needed, attr_widths are set below */ rel->lateral_vars = NIL; rel->lateral_relids = NULL; + rel->lateral_referencers = NULL; rel->indexlist = NIL; rel->pages = 0; rel->tuples = 0; @@ -374,6 +375,7 @@ build_join_rel(PlannerInfo *root, joinrel->attr_widths = NULL; joinrel->lateral_vars = NIL; joinrel->lateral_relids = NULL; + joinrel->lateral_referencers = NULL; joinrel->indexlist = NIL; joinrel->pages = 0; joinrel->tuples = 0; diff --git a/src/backend/optimizer/util/restrictinfo.c b/src/backend/optimizer/util/restrictinfo.c index cd7109b687b..33b029b0e4e 100644 --- a/src/backend/optimizer/util/restrictinfo.c +++ b/src/backend/optimizer/util/restrictinfo.c @@ -651,24 +651,32 @@ extract_actual_join_clauses(List *restrictinfo_list, * outer join, as that would change the results (rows would be suppressed * rather than being null-extended). * - * And the target relation must not be in the clause's nullable_relids, i.e., + * Also the target relation must not be in the clause's nullable_relids, i.e., * there must not be an outer join below the clause that would null the Vars * coming from the target relation. Otherwise the clause might give results * different from what it would give at its normal semantic level. + * + * Also, the join clause must not use any relations that have LATERAL + * references to the target relation, since we could not put such rels on + * the outer side of a nestloop with the target relation. */ bool -join_clause_is_movable_to(RestrictInfo *rinfo, Index baserelid) +join_clause_is_movable_to(RestrictInfo *rinfo, RelOptInfo *baserel) { /* Clause must physically reference target rel */ - if (!bms_is_member(baserelid, rinfo->clause_relids)) + if (!bms_is_member(baserel->relid, rinfo->clause_relids)) return false; /* Cannot move an outer-join clause into the join's outer side */ - if (bms_is_member(baserelid, rinfo->outer_relids)) + if (bms_is_member(baserel->relid, rinfo->outer_relids)) return false; /* Target rel must not be nullable below the clause */ - if (bms_is_member(baserelid, rinfo->nullable_relids)) + if (bms_is_member(baserel->relid, rinfo->nullable_relids)) + return false; + + /* Clause must not use any rels with LATERAL references to this rel */ + if (bms_overlap(baserel->lateral_referencers, rinfo->clause_relids)) return false; return true; @@ -695,6 +703,11 @@ join_clause_is_movable_to(RestrictInfo *rinfo, Index baserelid) * not pushing the clause into its outer-join outer side, nor down into * a lower outer join's inner side. * + * There's no check here equivalent to join_clause_is_movable_to's test on + * lateral_relids. We assume the caller wouldn't be inquiring unless it'd + * verified that the proposed outer rels don't have lateral references to + * the current rel(s). + * * Note: get_joinrel_parampathinfo depends on the fact that if * current_and_outer is NULL, this function will always return false * (since one or the other of the first two tests must fail). diff --git a/src/backend/optimizer/util/var.c b/src/backend/optimizer/util/var.c index 5f736ad6c40..4a3d5c8408e 100644 --- a/src/backend/optimizer/util/var.c +++ b/src/backend/optimizer/util/var.c @@ -161,8 +161,13 @@ pull_varnos_walker(Node *node, pull_varnos_context *context) if (IsA(node, PlaceHolderVar)) { /* - * Normally, we can just take the varnos in the contained expression. - * But if it is variable-free, use the PHV's syntactic relids. + * A PlaceHolderVar acts as a variable of its syntactic scope, or + * lower than that if it references only a subset of the rels in its + * syntactic scope. It might also contain lateral references, but we + * should ignore such references when computing the set of varnos in + * an expression tree. Also, if the PHV contains no variables within + * its syntactic scope, it will be forced to be evaluated exactly at + * the syntactic scope, so take that as the relid set. */ PlaceHolderVar *phv = (PlaceHolderVar *) node; pull_varnos_context subcontext; @@ -170,12 +175,15 @@ pull_varnos_walker(Node *node, pull_varnos_context *context) subcontext.varnos = NULL; subcontext.sublevels_up = context->sublevels_up; (void) pull_varnos_walker((Node *) phv->phexpr, &subcontext); - - if (bms_is_empty(subcontext.varnos) && - phv->phlevelsup == context->sublevels_up) - context->varnos = bms_add_members(context->varnos, phv->phrels); - else - context->varnos = bms_join(context->varnos, subcontext.varnos); + if (phv->phlevelsup == context->sublevels_up) + { + subcontext.varnos = bms_int_members(subcontext.varnos, + phv->phrels); + if (bms_is_empty(subcontext.varnos)) + context->varnos = bms_add_members(context->varnos, + phv->phrels); + } + context->varnos = bms_join(context->varnos, subcontext.varnos); return false; } if (IsA(node, Query)) diff --git a/src/include/nodes/relation.h b/src/include/nodes/relation.h index b611e0b905a..a2853fbf044 100644 --- a/src/include/nodes/relation.h +++ b/src/include/nodes/relation.h @@ -339,6 +339,7 @@ typedef struct PlannerInfo * Vars and PlaceHolderVars) * lateral_relids - required outer rels for LATERAL, as a Relids set * (for child rels this can be more than lateral_vars) + * lateral_referencers - relids of rels that reference this one laterally * indexlist - list of IndexOptInfo nodes for relation's indexes * (always NIL if it's not a table) * pages - number of disk pages in relation (zero if not a table) @@ -432,6 +433,7 @@ typedef struct RelOptInfo int32 *attr_widths; /* array indexed [min_attr .. max_attr] */ List *lateral_vars; /* LATERAL Vars and PHVs referenced by rel */ Relids lateral_relids; /* minimum parameterization of rel */ + Relids lateral_referencers; /* rels that reference me laterally */ List *indexlist; /* list of IndexOptInfo */ BlockNumber pages; /* size estimates derived from pg_class */ double tuples; @@ -1344,30 +1346,38 @@ typedef struct SpecialJoinInfo /* * "Lateral join" info. * - * Lateral references in subqueries constrain the join order in a way that's - * somewhat like outer joins, though different in detail. We construct one or - * more LateralJoinInfos for each RTE with lateral references, and add them to - * the PlannerInfo node's lateral_info_list. + * Lateral references constrain the join order in a way that's somewhat like + * outer joins, though different in detail. We construct a LateralJoinInfo + * for each lateral cross-reference, placing them in the PlannerInfo node's + * lateral_info_list. * - * lateral_rhs is the relid of a baserel with lateral references, and - * lateral_lhs is a set of relids of baserels it references, all of which - * must be present on the LHS to compute a parameter needed by the RHS. - * Typically, lateral_lhs is a singleton, but it can include multiple rels - * if the RHS references a PlaceHolderVar with a multi-rel ph_eval_at level. - * We disallow joining to only part of the LHS in such cases, since that would - * result in a join tree with no convenient place to compute the PHV. + * For unflattened LATERAL RTEs, we generate LateralJoinInfo(s) in which + * lateral_rhs is the relid of the LATERAL baserel, and lateral_lhs is a set + * of relids of baserels it references, all of which must be present on the + * LHS to compute a parameter needed by the RHS. Typically, lateral_lhs is + * a singleton, but it can include multiple rels if the RHS references a + * PlaceHolderVar with a multi-rel ph_eval_at level. We disallow joining to + * only part of the LHS in such cases, since that would result in a join tree + * with no convenient place to compute the PHV. * * When an appendrel contains lateral references (eg "LATERAL (SELECT x.col1 * UNION ALL SELECT y.col2)"), the LateralJoinInfos reference the parent * baserel not the member otherrels, since it is the parent relid that is * considered for joining purposes. + * + * If any LATERAL RTEs were flattened into the parent query, it is possible + * that the query now contains PlaceHolderVars containing lateral references, + * representing expressions that need to be evaluated at particular spots in + * the jointree but contain lateral references to Vars from elsewhere. These + * give rise to LateralJoinInfos in which lateral_rhs is the evaluation point + * of a PlaceHolderVar and lateral_lhs is the set of lateral rels it needs. */ typedef struct LateralJoinInfo { NodeTag type; - Index lateral_rhs; /* a baserel containing lateral refs */ - Relids lateral_lhs; /* some base relids it references */ + Relids lateral_lhs; /* rels needed to compute a lateral value */ + Relids lateral_rhs; /* rel where lateral value is needed */ } LateralJoinInfo; /* @@ -1465,6 +1475,10 @@ typedef struct AppendRelInfo * then allow it to bubble up like a Var until the ph_needed join level. * ph_needed has the same definition as attr_needed for a regular Var. * + * The PlaceHolderVar's expression might contain LATERAL references to vars + * coming from outside its syntactic scope. If so, those rels are *not* + * included in ph_eval_at, but they are recorded in ph_lateral. + * * Notice that when ph_eval_at is a join rather than a single baserel, the * PlaceHolderInfo may create constraints on join order: the ph_eval_at join * has to be formed below any outer joins that should null the PlaceHolderVar. @@ -1481,6 +1495,7 @@ typedef struct PlaceHolderInfo Index phid; /* ID for PH (unique within planner run) */ PlaceHolderVar *ph_var; /* copy of PlaceHolderVar tree */ Relids ph_eval_at; /* lowest level we can evaluate value at */ + Relids ph_lateral; /* relids of contained lateral refs, if any */ Relids ph_needed; /* highest level the value is needed at */ int32 ph_width; /* estimated attribute width */ } PlaceHolderInfo; diff --git a/src/include/optimizer/restrictinfo.h b/src/include/optimizer/restrictinfo.h index db0ba71ed96..47b19691c29 100644 --- a/src/include/optimizer/restrictinfo.h +++ b/src/include/optimizer/restrictinfo.h @@ -41,7 +41,7 @@ extern List *extract_actual_clauses(List *restrictinfo_list, extern void extract_actual_join_clauses(List *restrictinfo_list, List **joinquals, List **otherquals); -extern bool join_clause_is_movable_to(RestrictInfo *rinfo, Index baserelid); +extern bool join_clause_is_movable_to(RestrictInfo *rinfo, RelOptInfo *baserel); extern bool join_clause_is_movable_into(RestrictInfo *rinfo, Relids currentrelids, Relids current_and_outer); diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out index 814ddd80469..fc3e1688068 100644 --- a/src/test/regress/expected/join.out +++ b/src/test/regress/expected/join.out @@ -3577,6 +3577,195 @@ select v.* from -4567890123456789 | (20 rows) +explain (verbose, costs off) +select * from + int8_tbl a left join + lateral (select *, a.q2 as x from int8_tbl b) ss on a.q2 = ss.q1; + QUERY PLAN +------------------------------------------ + Nested Loop Left Join + Output: a.q1, a.q2, b.q1, b.q2, (a.q2) + -> Seq Scan on public.int8_tbl a + Output: a.q1, a.q2 + -> Seq Scan on public.int8_tbl b + Output: b.q1, b.q2, a.q2 + Filter: (a.q2 = b.q1) +(7 rows) + +select * from + int8_tbl a left join + lateral (select *, a.q2 as x from int8_tbl b) ss on a.q2 = ss.q1; + q1 | q2 | q1 | q2 | x +------------------+-------------------+------------------+-------------------+------------------ + 123 | 456 | | | + 123 | 4567890123456789 | 4567890123456789 | 123 | 4567890123456789 + 123 | 4567890123456789 | 4567890123456789 | 4567890123456789 | 4567890123456789 + 123 | 4567890123456789 | 4567890123456789 | -4567890123456789 | 4567890123456789 + 4567890123456789 | 123 | 123 | 456 | 123 + 4567890123456789 | 123 | 123 | 4567890123456789 | 123 + 4567890123456789 | 4567890123456789 | 4567890123456789 | 123 | 4567890123456789 + 4567890123456789 | 4567890123456789 | 4567890123456789 | 4567890123456789 | 4567890123456789 + 4567890123456789 | 4567890123456789 | 4567890123456789 | -4567890123456789 | 4567890123456789 + 4567890123456789 | -4567890123456789 | | | +(10 rows) + +explain (verbose, costs off) +select * from + int8_tbl a left join + lateral (select *, coalesce(a.q2, 42) as x from int8_tbl b) ss on a.q2 = ss.q1; + QUERY PLAN +---------------------------------------------------------------- + Nested Loop Left Join + Output: a.q1, a.q2, b.q1, b.q2, (COALESCE(a.q2, 42::bigint)) + -> Seq Scan on public.int8_tbl a + Output: a.q1, a.q2 + -> Seq Scan on public.int8_tbl b + Output: b.q1, b.q2, COALESCE(a.q2, 42::bigint) + Filter: (a.q2 = b.q1) +(7 rows) + +select * from + int8_tbl a left join + lateral (select *, coalesce(a.q2, 42) as x from int8_tbl b) ss on a.q2 = ss.q1; + q1 | q2 | q1 | q2 | x +------------------+-------------------+------------------+-------------------+------------------ + 123 | 456 | | | + 123 | 4567890123456789 | 4567890123456789 | 123 | 4567890123456789 + 123 | 4567890123456789 | 4567890123456789 | 4567890123456789 | 4567890123456789 + 123 | 4567890123456789 | 4567890123456789 | -4567890123456789 | 4567890123456789 + 4567890123456789 | 123 | 123 | 456 | 123 + 4567890123456789 | 123 | 123 | 4567890123456789 | 123 + 4567890123456789 | 4567890123456789 | 4567890123456789 | 123 | 4567890123456789 + 4567890123456789 | 4567890123456789 | 4567890123456789 | 4567890123456789 | 4567890123456789 + 4567890123456789 | 4567890123456789 | 4567890123456789 | -4567890123456789 | 4567890123456789 + 4567890123456789 | -4567890123456789 | | | +(10 rows) + +-- lateral can result in join conditions appearing below their +-- real semantic level +explain (verbose, costs off) +select * from int4_tbl i left join + lateral (select * from int2_tbl j where i.f1 = j.f1) k on true; + QUERY PLAN +------------------------------------------- + Nested Loop Left Join + Output: i.f1, j.f1 + Filter: (i.f1 = j.f1) + -> Seq Scan on public.int4_tbl i + Output: i.f1 + -> Materialize + Output: j.f1 + -> Seq Scan on public.int2_tbl j + Output: j.f1 +(9 rows) + +select * from int4_tbl i left join + lateral (select * from int2_tbl j where i.f1 = j.f1) k on true; + f1 | f1 +----+---- + 0 | 0 +(1 row) + +explain (verbose, costs off) +select * from int4_tbl i left join + lateral (select coalesce(i) from int2_tbl j where i.f1 = j.f1) k on true; + QUERY PLAN +------------------------------------- + Nested Loop Left Join + Output: i.f1, (COALESCE(i.*)) + -> Seq Scan on public.int4_tbl i + Output: i.f1, i.* + -> Seq Scan on public.int2_tbl j + Output: j.f1, COALESCE(i.*) + Filter: (i.f1 = j.f1) +(7 rows) + +select * from int4_tbl i left join + lateral (select coalesce(i) from int2_tbl j where i.f1 = j.f1) k on true; + f1 | coalesce +-------------+---------- + 0 | (0) + 123456 | + -123456 | + 2147483647 | + -2147483647 | +(5 rows) + +-- lateral reference in a PlaceHolderVar evaluated at join level +explain (verbose, costs off) +select * from + int8_tbl a left join lateral + (select b.q1 as bq1, c.q1 as cq1, least(a.q1,b.q1,c.q1) from + int8_tbl b cross join int8_tbl c) ss + on a.q2 = ss.bq1; + QUERY PLAN +------------------------------------------------------------- + Nested Loop Left Join + Output: a.q1, a.q2, b.q1, c.q1, (LEAST(a.q1, b.q1, c.q1)) + -> Seq Scan on public.int8_tbl a + Output: a.q1, a.q2 + -> Nested Loop + Output: b.q1, c.q1, LEAST(a.q1, b.q1, c.q1) + Join Filter: (a.q2 = b.q1) + -> Seq Scan on public.int8_tbl b + Output: b.q1, b.q2 + -> Materialize + Output: c.q1 + -> Seq Scan on public.int8_tbl c + Output: c.q1 +(13 rows) + +select * from + int8_tbl a left join lateral + (select b.q1 as bq1, c.q1 as cq1, least(a.q1,b.q1,c.q1) from + int8_tbl b cross join int8_tbl c) ss + on a.q2 = ss.bq1; + q1 | q2 | bq1 | cq1 | least +------------------+-------------------+------------------+------------------+------------------ + 123 | 456 | | | + 123 | 4567890123456789 | 4567890123456789 | 123 | 123 + 123 | 4567890123456789 | 4567890123456789 | 123 | 123 + 123 | 4567890123456789 | 4567890123456789 | 4567890123456789 | 123 + 123 | 4567890123456789 | 4567890123456789 | 4567890123456789 | 123 + 123 | 4567890123456789 | 4567890123456789 | 4567890123456789 | 123 + 123 | 4567890123456789 | 4567890123456789 | 123 | 123 + 123 | 4567890123456789 | 4567890123456789 | 123 | 123 + 123 | 4567890123456789 | 4567890123456789 | 4567890123456789 | 123 + 123 | 4567890123456789 | 4567890123456789 | 4567890123456789 | 123 + 123 | 4567890123456789 | 4567890123456789 | 4567890123456789 | 123 + 123 | 4567890123456789 | 4567890123456789 | 123 | 123 + 123 | 4567890123456789 | 4567890123456789 | 123 | 123 + 123 | 4567890123456789 | 4567890123456789 | 4567890123456789 | 123 + 123 | 4567890123456789 | 4567890123456789 | 4567890123456789 | 123 + 123 | 4567890123456789 | 4567890123456789 | 4567890123456789 | 123 + 4567890123456789 | 123 | 123 | 123 | 123 + 4567890123456789 | 123 | 123 | 123 | 123 + 4567890123456789 | 123 | 123 | 4567890123456789 | 123 + 4567890123456789 | 123 | 123 | 4567890123456789 | 123 + 4567890123456789 | 123 | 123 | 4567890123456789 | 123 + 4567890123456789 | 123 | 123 | 123 | 123 + 4567890123456789 | 123 | 123 | 123 | 123 + 4567890123456789 | 123 | 123 | 4567890123456789 | 123 + 4567890123456789 | 123 | 123 | 4567890123456789 | 123 + 4567890123456789 | 123 | 123 | 4567890123456789 | 123 + 4567890123456789 | 4567890123456789 | 4567890123456789 | 123 | 123 + 4567890123456789 | 4567890123456789 | 4567890123456789 | 123 | 123 + 4567890123456789 | 4567890123456789 | 4567890123456789 | 4567890123456789 | 4567890123456789 + 4567890123456789 | 4567890123456789 | 4567890123456789 | 4567890123456789 | 4567890123456789 + 4567890123456789 | 4567890123456789 | 4567890123456789 | 4567890123456789 | 4567890123456789 + 4567890123456789 | 4567890123456789 | 4567890123456789 | 123 | 123 + 4567890123456789 | 4567890123456789 | 4567890123456789 | 123 | 123 + 4567890123456789 | 4567890123456789 | 4567890123456789 | 4567890123456789 | 4567890123456789 + 4567890123456789 | 4567890123456789 | 4567890123456789 | 4567890123456789 | 4567890123456789 + 4567890123456789 | 4567890123456789 | 4567890123456789 | 4567890123456789 | 4567890123456789 + 4567890123456789 | 4567890123456789 | 4567890123456789 | 123 | 123 + 4567890123456789 | 4567890123456789 | 4567890123456789 | 123 | 123 + 4567890123456789 | 4567890123456789 | 4567890123456789 | 4567890123456789 | 4567890123456789 + 4567890123456789 | 4567890123456789 | 4567890123456789 | 4567890123456789 | 4567890123456789 + 4567890123456789 | 4567890123456789 | 4567890123456789 | 4567890123456789 | 4567890123456789 + 4567890123456789 | -4567890123456789 | | | +(42 rows) + -- case requiring nested PlaceHolderVars explain (verbose, costs off) select * from @@ -3595,7 +3784,7 @@ select * from Output: c.q1, c.q2, a.q1, a.q2, b.q1, d.q1, (COALESCE(b.q2, 42::bigint)), (COALESCE((COALESCE(b.q2, 42::bigint)), d.q2)) Hash Cond: (d.q1 = c.q2) -> Nested Loop - Output: a.q1, a.q2, b.q1, d.q1, (COALESCE(b.q2, 42::bigint)), COALESCE((COALESCE(b.q2, 42::bigint)), d.q2) + Output: a.q1, a.q2, b.q1, d.q1, (COALESCE(b.q2, 42::bigint)), (COALESCE((COALESCE(b.q2, 42::bigint)), d.q2)) -> Hash Left Join Output: a.q1, a.q2, b.q1, (COALESCE(b.q2, 42::bigint)) Hash Cond: (a.q2 = b.q1) @@ -3605,17 +3794,15 @@ select * from Output: b.q1, (COALESCE(b.q2, 42::bigint)) -> Seq Scan on public.int8_tbl b Output: b.q1, COALESCE(b.q2, 42::bigint) - -> Materialize - Output: d.q1, d.q2 - -> Seq Scan on public.int8_tbl d - Output: d.q1, d.q2 + -> Seq Scan on public.int8_tbl d + Output: d.q1, COALESCE((COALESCE(b.q2, 42::bigint)), d.q2) -> Hash Output: c.q1, c.q2 -> Seq Scan on public.int8_tbl c Output: c.q1, c.q2 -> Result Output: (COALESCE((COALESCE(b.q2, 42::bigint)), d.q2)) -(26 rows) +(24 rows) -- case that breaks the old ph_may_need optimization explain (verbose, costs off) @@ -3629,45 +3816,43 @@ select c.*,a.*,ss1.q1,ss2.q1,ss3.* from lateral (select q1, coalesce(ss1.x,q2) as y from int8_tbl d) ss2 ) on c.q2 = ss2.q1, lateral (select * from int4_tbl i where ss2.y > f1) ss3; - QUERY PLAN -------------------------------------------------------------------------------------------- - Hash Right Join + QUERY PLAN +--------------------------------------------------------------------------------------------------------- + Nested Loop Output: c.q1, c.q2, a.q1, a.q2, b.q1, d.q1, i.f1 - Hash Cond: (d.q1 = c.q2) - Filter: ((COALESCE((COALESCE(b.q2, (b2.f1)::bigint)), d.q2)) > i.f1) - -> Nested Loop - Output: a.q1, a.q2, b.q1, d.q1, COALESCE((COALESCE(b.q2, (b2.f1)::bigint)), d.q2) - -> Hash Right Join - Output: a.q1, a.q2, b.q1, (COALESCE(b.q2, (b2.f1)::bigint)) - Hash Cond: (b.q1 = a.q2) - -> Nested Loop - Output: b.q1, COALESCE(b.q2, (b2.f1)::bigint) - Join Filter: (b.q1 < b2.f1) - -> Seq Scan on public.int8_tbl b - Output: b.q1, b.q2 - -> Materialize - Output: b2.f1 - -> Seq Scan on public.int4_tbl b2 - Output: b2.f1 - -> Hash - Output: a.q1, a.q2 - -> Seq Scan on public.int8_tbl a - Output: a.q1, a.q2 - -> Materialize - Output: d.q1, d.q2 - -> Seq Scan on public.int8_tbl d - Output: d.q1, d.q2 - -> Hash - Output: c.q1, c.q2, i.f1 + Join Filter: ((COALESCE((COALESCE(b.q2, (b2.f1)::bigint)), d.q2)) > i.f1) + -> Hash Right Join + Output: c.q1, c.q2, a.q1, a.q2, b.q1, d.q1, (COALESCE((COALESCE(b.q2, (b2.f1)::bigint)), d.q2)) + Hash Cond: (d.q1 = c.q2) -> Nested Loop - Output: c.q1, c.q2, i.f1 + Output: a.q1, a.q2, b.q1, d.q1, (COALESCE((COALESCE(b.q2, (b2.f1)::bigint)), d.q2)) + -> Hash Right Join + Output: a.q1, a.q2, b.q1, (COALESCE(b.q2, (b2.f1)::bigint)) + Hash Cond: (b.q1 = a.q2) + -> Nested Loop + Output: b.q1, COALESCE(b.q2, (b2.f1)::bigint) + Join Filter: (b.q1 < b2.f1) + -> Seq Scan on public.int8_tbl b + Output: b.q1, b.q2 + -> Materialize + Output: b2.f1 + -> Seq Scan on public.int4_tbl b2 + Output: b2.f1 + -> Hash + Output: a.q1, a.q2 + -> Seq Scan on public.int8_tbl a + Output: a.q1, a.q2 + -> Seq Scan on public.int8_tbl d + Output: d.q1, COALESCE((COALESCE(b.q2, (b2.f1)::bigint)), d.q2) + -> Hash + Output: c.q1, c.q2 -> Seq Scan on public.int8_tbl c Output: c.q1, c.q2 - -> Materialize - Output: i.f1 - -> Seq Scan on public.int4_tbl i - Output: i.f1 -(36 rows) + -> Materialize + Output: i.f1 + -> Seq Scan on public.int4_tbl i + Output: i.f1 +(34 rows) -- test some error cases where LATERAL should have been used but wasn't select f1,g from int4_tbl a, (select f1 as g) ss; diff --git a/src/test/regress/sql/join.sql b/src/test/regress/sql/join.sql index 7ec2cbeea4b..36853ddce49 100644 --- a/src/test/regress/sql/join.sql +++ b/src/test/regress/sql/join.sql @@ -995,6 +995,47 @@ select v.* from left join int4_tbl z on z.f1 = x.q2, lateral (select x.q1,y.q1 from dual union all select x.q2,y.q2 from dual) v(vx,vy); +explain (verbose, costs off) +select * from + int8_tbl a left join + lateral (select *, a.q2 as x from int8_tbl b) ss on a.q2 = ss.q1; +select * from + int8_tbl a left join + lateral (select *, a.q2 as x from int8_tbl b) ss on a.q2 = ss.q1; +explain (verbose, costs off) +select * from + int8_tbl a left join + lateral (select *, coalesce(a.q2, 42) as x from int8_tbl b) ss on a.q2 = ss.q1; +select * from + int8_tbl a left join + lateral (select *, coalesce(a.q2, 42) as x from int8_tbl b) ss on a.q2 = ss.q1; + +-- lateral can result in join conditions appearing below their +-- real semantic level +explain (verbose, costs off) +select * from int4_tbl i left join + lateral (select * from int2_tbl j where i.f1 = j.f1) k on true; +select * from int4_tbl i left join + lateral (select * from int2_tbl j where i.f1 = j.f1) k on true; +explain (verbose, costs off) +select * from int4_tbl i left join + lateral (select coalesce(i) from int2_tbl j where i.f1 = j.f1) k on true; +select * from int4_tbl i left join + lateral (select coalesce(i) from int2_tbl j where i.f1 = j.f1) k on true; + +-- lateral reference in a PlaceHolderVar evaluated at join level +explain (verbose, costs off) +select * from + int8_tbl a left join lateral + (select b.q1 as bq1, c.q1 as cq1, least(a.q1,b.q1,c.q1) from + int8_tbl b cross join int8_tbl c) ss + on a.q2 = ss.bq1; +select * from + int8_tbl a left join lateral + (select b.q1 as bq1, c.q1 as cq1, least(a.q1,b.q1,c.q1) from + int8_tbl b cross join int8_tbl c) ss + on a.q2 = ss.bq1; + -- case requiring nested PlaceHolderVars explain (verbose, costs off) select * from