From bec3a7b2dbf965003149d7766b4c0af534b1692c Mon Sep 17 00:00:00 2001 From: Haibo Yan Date: Tue, 27 Oct 2026 14:30:40 -0700 Subject: [PATCH] Prevent lateral dependency cycles in self-join elimination Self-join elimination (remove_useless_self_joins) simplifies queries by removing redundant inner- and semi-joins when a unique key ensures that at most one matching row exists. However, when the query contains LATERAL references or PlaceHolderVars whose evaluation depends on lateral inputs, merging two relations can turn an acyclic lateral dependency chain (such as rel1 -> rel2 -> rel3) into a cycle if rel1 and rel3 are combined. When such an unsafe merge occurs, later planning stages fail. In builds with assertions enabled, query_planner() triggers an assertion failure in initsplan.c: TRAP: failed Assert("!bms_is_member(rti, lateral_relids)") In non-assertion builds, the cycle prevents the join search from forming valid join paths, resulting in planner errors such as: ERROR: failed to build any 2-way joins To fix this, introduce self_join_creates_lateral_cycle() to check whether merging candidate relations would create a cyclic lateral dependency in the transitive closure of lateral_relids. This check simulates the variable substitution in PlaceHolderInfos and recomputes their evaluation levels so indirect dependencies mediated by PlaceHolderVars are detected. When a safe merge is accepted, remove_self_join_rel() updates root->all_baserels, each baserel's lateral_vars, and active PlaceHolderInfos in place so subsequent candidate pairs evaluated in the same pass observe the updated relations. Add regression tests in join.sql covering: - Direct and indirect lateral dependency chains that must not be merged; - Original outer-join and reduced function-scan reproducers; - Safe PlaceHolderVar evaluation-site expansions; - Multi-candidate same-group and cross-group restart scenarios; - Nested PlaceHolderVars where a safe merge occurs and updates state read by a later candidate; - Safe self-join controls and empty-input counterparts with deterministic ordering. --- src/backend/optimizer/plan/analyzejoins.c | 277 +++++++++++++++- src/test/regress/expected/join.out | 384 ++++++++++++++++++++++ src/test/regress/sql/join.sql | 181 ++++++++++ 3 files changed, 838 insertions(+), 4 deletions(-) diff --git a/src/backend/optimizer/plan/analyzejoins.c b/src/backend/optimizer/plan/analyzejoins.c index 7aad91fd34e..1482e5c8cb7 100644 --- a/src/backend/optimizer/plan/analyzejoins.c +++ b/src/backend/optimizer/plan/analyzejoins.c @@ -33,6 +33,7 @@ #include "optimizer/optimizer.h" #include "optimizer/pathnode.h" #include "optimizer/paths.h" +#include "optimizer/placeholder.h" #include "optimizer/planmain.h" #include "optimizer/prep.h" #include "optimizer/restrictinfo.h" @@ -95,6 +96,10 @@ static void fixup_selfjoin_jointree(PlannerInfo *root, Node *jtnode, int relid, Node **hoist_quals, bool *found_relid); static List *fixup_selfjoin_quals(PlannerInfo *root, List *quals, int relid); static Node *replace_selfjoin_qual(Node *qual); +static void update_placeholder_eval_levels(PlannerInfo *root); +static bool self_join_creates_lateral_cycle(PlannerInfo *root, + RelOptInfo *toKeep, + RelOptInfo *toRemove); static int self_join_candidates_cmp(const void *a, const void *b); @@ -1310,6 +1315,34 @@ remove_self_join_rel(PlannerInfo *root, rmark->rti = rmark->prti = toKeep->relid; } } + + /* + * Update all_baserels, baserel lateral_vars, and PlaceHolderInfos so that + * any subsequent self-join checks in the same pass see the updated + * relation references. + */ + root->all_baserels = bms_del_member(root->all_baserels, toRemove->relid); + if (root->hasLateralRTEs) + { + int rti = -1; + ListCell *lc; + + while ((rti = bms_next_member(root->all_baserels, rti)) > 0) + { + RelOptInfo *brel = root->simple_rel_array[rti]; + + ChangeVarNodes((Node *) brel->lateral_vars, + toRemove->relid, toKeep->relid, 0); + } + foreach(lc, root->placeholder_list) + { + PlaceHolderInfo *phinfo = (PlaceHolderInfo *) lfirst(lc); + + ChangeVarNodes((Node *) phinfo->ph_var, + toRemove->relid, toKeep->relid, 0); + } + update_placeholder_eval_levels(root); + } } /* @@ -1758,6 +1791,238 @@ match_unique_clauses(PlannerInfo *root, RelOptInfo *outer, List *uclauses, return true; } +/* + * Recompute ph_eval_at and ph_lateral for all PlaceHolderInfos in + * root->placeholder_list from their current ph_var expressions. + * + * We clear and repopulate placeholder_array in placeholder_list order so that + * if one PlaceHolderVar references another, pull_varnos() sees the same + * placeholder_array state as find_placeholder_info() will see after + * query_planner() restarts. + */ +static void +update_placeholder_eval_levels(PlannerInfo *root) +{ + ListCell *lc; + + foreach(lc, root->placeholder_list) + { + PlaceHolderInfo *phinfo = (PlaceHolderInfo *) lfirst(lc); + + if (phinfo->phid < root->placeholder_array_size) + root->placeholder_array[phinfo->phid] = NULL; + } + + foreach(lc, root->placeholder_list) + { + PlaceHolderInfo *phinfo = (PlaceHolderInfo *) lfirst(lc); + PlaceHolderVar *phv = phinfo->ph_var; + Relids rels_used; + + rels_used = pull_varnos(root, (Node *) phv->phexpr); + phinfo->ph_lateral = bms_difference(rels_used, phv->phrels); + 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)); + } + if (phinfo->phid < root->placeholder_array_size) + root->placeholder_array[phinfo->phid] = phinfo; + } +} + +/* + * Check whether merging toRemove into toKeep would create a lateral + * dependency cycle (that is, a relation that laterally depends on itself, + * either directly or indirectly through other relations or PlaceHolderVars). + * + * Self-join elimination can turn a valid lateral dependency chain such as + * rel1 -> rel2 -> rel3 into a cycle if rel1 and rel3 are merged. Check for + * that by computing the per-baserel lateral_relids that + * create_lateral_join_info() will derive after the merge, and testing whether + * the transitive closure contains any self-dependency. + */ +static bool +self_join_creates_lateral_cycle(PlannerInfo *root, + RelOptInfo *toKeep, RelOptInfo *toRemove) +{ + typedef struct + { + PlaceHolderVar *ph_var; + Relids ph_eval_at; + Relids ph_lateral; + } SavedPlaceHolderInfo; + + int k = toKeep->relid; + int r = toRemove->relid; + Relids baserels; + Relids *lateral_relids; + SavedPlaceHolderInfo *saved_ph = NULL; + bool found_laterals = false; + bool has_cycle = false; + int num_ph; + int i; + int rti; + ListCell *lc; + + /* Nothing to do if the query has no LATERAL RTEs */ + if (!root->hasLateralRTEs) + return false; + + baserels = bms_difference(root->all_baserels, toRemove->relids); + lateral_relids = palloc0_array(Relids, root->simple_rel_array_size); + + /* + * Temporarily update each PlaceHolderInfo's ph_var, ph_eval_at, and + * ph_lateral to reflect replacing r with k, matching what + * find_placeholder_info() will compute after query_planner() restarts. + */ + num_ph = list_length(root->placeholder_list); + if (num_ph > 0) + { + saved_ph = palloc_array(SavedPlaceHolderInfo, num_ph); + i = 0; + foreach(lc, root->placeholder_list) + { + PlaceHolderInfo *phinfo = (PlaceHolderInfo *) lfirst(lc); + + saved_ph[i].ph_var = phinfo->ph_var; + saved_ph[i].ph_eval_at = phinfo->ph_eval_at; + saved_ph[i].ph_lateral = phinfo->ph_lateral; + i++; + + phinfo->ph_var = copyObject(phinfo->ph_var); + ChangeVarNodes((Node *) phinfo->ph_var, r, k, 0); + } + update_placeholder_eval_levels(root); + } + + /* + * Collect direct lateral dependencies from each remaining baserel's + * lateral_vars, translating references to r into k. + */ + rti = -1; + while ((rti = bms_next_member(baserels, rti)) > 0) + { + RelOptInfo *brel = root->simple_rel_array[rti]; + + Assert(brel != NULL && brel->reloptkind == RELOPT_BASEREL); + + foreach(lc, brel->lateral_vars) + { + Node *node = (Node *) lfirst(lc); + + if (IsA(node, Var)) + { + Var *var = (Var *) node; + int varno = (var->varno == r) ? k : var->varno; + + found_laterals = true; + lateral_relids[rti] = bms_add_member(lateral_relids[rti], + varno); + } + else if (IsA(node, PlaceHolderVar)) + { + PlaceHolderVar *phv = (PlaceHolderVar *) node; + PlaceHolderInfo *phinfo = find_placeholder_info(root, phv); + + found_laterals = true; + lateral_relids[rti] = bms_add_members(lateral_relids[rti], + phinfo->ph_eval_at); + } + else + Assert(false); + } + } + + /* + * Collect lateral dependencies from PlaceHolderVars, marking each baserel + * in ph_eval_at as laterally dependent on the baserels in ph_lateral. + */ + foreach(lc, root->placeholder_list) + { + PlaceHolderInfo *phinfo = (PlaceHolderInfo *) lfirst(lc); + Relids eval_at = phinfo->ph_eval_at; + Relids lateral_refs; + int varno; + + if (phinfo->ph_lateral == NULL) + continue; + + lateral_refs = bms_intersect(phinfo->ph_lateral, baserels); + if (bms_is_empty(lateral_refs)) + continue; + + found_laterals = true; + + varno = -1; + while ((varno = bms_next_member(eval_at, varno)) >= 0) + { + if (bms_is_member(varno, baserels)) + lateral_relids[varno] = bms_add_members(lateral_relids[varno], + lateral_refs); + } + } + + /* + * Compute the transitive closure of lateral_relids (Warshall's algorithm) + * and check whether any baserel ends up laterally dependent on itself. + */ + if (found_laterals) + { + rti = -1; + while ((rti = bms_next_member(baserels, rti)) > 0) + { + Relids outer_lateral_relids = lateral_relids[rti]; + int rti2; + + if (outer_lateral_relids == NULL) + continue; + + rti2 = -1; + while ((rti2 = bms_next_member(baserels, rti2)) > 0) + { + if (bms_is_member(rti, lateral_relids[rti2])) + lateral_relids[rti2] = bms_add_members(lateral_relids[rti2], + outer_lateral_relids); + } + } + + rti = -1; + while ((rti = bms_next_member(baserels, rti)) > 0) + { + if (bms_is_member(rti, lateral_relids[rti])) + { + has_cycle = true; + break; + } + } + } + + /* Restore PlaceHolderInfos to their pre-check state */ + if (num_ph > 0) + { + i = 0; + foreach(lc, root->placeholder_list) + { + PlaceHolderInfo *phinfo = (PlaceHolderInfo *) lfirst(lc); + + phinfo->ph_var = saved_ph[i].ph_var; + phinfo->ph_eval_at = saved_ph[i].ph_eval_at; + phinfo->ph_lateral = saved_ph[i].ph_lateral; + i++; + } + pfree(saved_ph); + } + + pfree(lateral_relids); + bms_free(baserels); + + return has_cycle; +} + /* * Find and remove unique self-joins in a group of base relations that have * the same Oid. @@ -1860,10 +2125,6 @@ remove_self_joins_one_group(PlannerInfo *root, Relids relids) joinrelids = bms_add_member(joinrelids, r); joinrelids = bms_add_member(joinrelids, k); - /* - * PHVs should not impose any constraints on removing self-joins. - */ - /* * At this stage, joininfo lists of inner and outer can contain * only clauses required for a superior outer join that can't @@ -1922,6 +2183,14 @@ remove_self_joins_one_group(PlannerInfo *root, Relids relids) if (!match_unique_clauses(root, rrel, uclauses, krel->relid)) continue; + /* + * Reject the merge if combining rrel and krel would turn a chain + * of lateral dependencies (possibly involving PlaceHolderVars) + * into a cycle. + */ + if (self_join_creates_lateral_cycle(root, krel, rrel)) + continue; + /* OK, remove rrel from the query */ remove_self_join_rel(root, krel, rrel, kmark, rmark); removed = true; diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out index 1e6aa380257..85512da96e4 100644 --- a/src/test/regress/expected/join.out +++ b/src/test/regress/expected/join.out @@ -9197,6 +9197,390 @@ where t1.a = s.a; Filter: (a IS NOT NULL) (2 rows) +explain (costs off) +select s.y +from sj x, + lateral (select coalesce(x.a, 0) from sj c where c.a = x.a) s(y) +group by cube (1) +order by 1; + QUERY PLAN +--------------------------------------- + Sort + Sort Key: (COALESCE(c.a, 0)) + -> MixedAggregate + Hash Key: COALESCE(c.a, 0) + Group Key: () + -> Seq Scan on sj c + Filter: (a IS NOT NULL) +(7 rows) + +select s.y +from sj x, + lateral (select coalesce(x.a, 0) from sj c where c.a = x.a) s(y) +group by cube (1) +order by 1; + y +--- + 1 + 2 + +(3 rows) + +-- Ensure that SJE does not turn an indirect lateral dependency chain +-- (c -> b -> x) into a cycle +explain (costs off) +select s.b +from sj x, + abs(x.a) b, + lateral (select b.b from sj c where c.a = x.a) s +group by cube (1) +order by 1; + QUERY PLAN +------------------------------------------------ + Sort + Sort Key: (b.b) + -> MixedAggregate + Hash Key: (b.b) + Group Key: () + -> Nested Loop + -> Nested Loop + -> Seq Scan on sj x + -> Function Scan on abs b + -> Memoize + Cache Key: x.a, b.b + Cache Mode: binary + -> Seq Scan on sj c + Filter: (x.a = a) +(14 rows) + +select s.b +from sj x, + abs(x.a) b, + lateral (select b.b from sj c where c.a = x.a) s +group by cube (1) +order by 1; + b +--- + 1 + 2 + +(3 rows) + +explain (costs off) +select s.a +from sj x, + lateral (select x.a offset 0) b, + lateral (select b.a from sj c where c.a = x.a) s +group by cube (1) +order by 1; + QUERY PLAN +--------------------------------------------- + Sort + Sort Key: ((x.a)) + -> MixedAggregate + Hash Key: ((x.a)) + Group Key: () + -> Nested Loop + -> Nested Loop + -> Seq Scan on sj x + -> Result + -> Memoize + Cache Key: x.a, (x.a) + Cache Mode: binary + -> Seq Scan on sj c + Filter: (x.a = a) +(14 rows) + +select s.a +from sj x, + lateral (select x.a offset 0) b, + lateral (select b.a from sj c where c.a = x.a) s +group by cube (1) +order by 1; + a +--- + 1 + 2 + +(3 rows) + +-- Original outer-join reproducer +explain (costs off) +select s.y +from sj d left join + (sj x cross join lateral (select x.a as v offset 0) b + cross join lateral (select coalesce(b.v, 0) from sj c where c.a = x.a) s(y)) + on true +order by 1; + QUERY PLAN +-------------------------------------------------------------- + Sort + Sort Key: (COALESCE((x.a), 0)) + -> Nested Loop Left Join + -> Seq Scan on sj d + -> Materialize + -> Nested Loop + -> Nested Loop + -> Seq Scan on sj x + -> Result + -> Memoize + Cache Key: x.a, COALESCE((x.a), 0) + Cache Mode: binary + -> Seq Scan on sj c + Filter: (x.a = a) +(14 rows) + +select s.y +from sj d left join + (sj x cross join lateral (select x.a as v offset 0) b + cross join lateral (select coalesce(b.v, 0) from sj c where c.a = x.a) s(y)) + on true +order by 1; + y +--- + 1 + 1 + 1 + 2 + 2 + 2 +(6 rows) + +-- Empty table input counterpart: GROUP BY CUBE returns a single NULL row +create table sj_empty (a int primary key); +explain (costs off) +select s.b +from sj_empty x, + abs(x.a) b, + lateral (select b.b from sj_empty c where c.a = x.a) s +group by cube (1) +order by 1; + QUERY PLAN +--------------------------------------------------------------------- + Sort + Sort Key: (b.b) + -> MixedAggregate + Hash Key: (b.b) + Group Key: () + -> Nested Loop + -> Nested Loop + -> Seq Scan on sj_empty x + -> Function Scan on abs b + -> Index Only Scan using sj_empty_pkey on sj_empty c + Index Cond: (a = x.a) +(11 rows) + +select s.b +from sj_empty x, + abs(x.a) b, + lateral (select b.b from sj_empty c where c.a = x.a) s +group by cube (1) +order by 1; + b +--- + +(1 row) + +drop table sj_empty; +-- Likewise when a join-scoped PHV only acquires c in ph_eval_at after merging +explain (costs off) +select s.y +from sj x, + abs(x.a) b, + lateral (select x.a + d.a + b.b as y from sj c, sj d where c.a = x.a and d.a = x.a) s +group by cube (1) +order by 1; + QUERY PLAN +------------------------------------------------ + Sort + Sort Key: (((x.a + d.a) + b.b)) + -> MixedAggregate + Hash Key: (((x.a + d.a) + b.b)) + Group Key: () + -> Nested Loop + -> Nested Loop + -> Seq Scan on sj x + -> Function Scan on abs b + -> Memoize + Cache Key: x.a, b.b + Cache Mode: binary + -> Seq Scan on sj d + Filter: (x.a = a) +(14 rows) + +select s.y +from sj x, + abs(x.a) b, + lateral (select x.a + d.a + b.b as y from sj c, sj d where c.a = x.a and d.a = x.a) s +group by cube (1) +order by 1; + y +--- + 3 + 6 + +(3 rows) + +-- 3-way self-join where one pair (x, c1) can be merged safely, while the +-- remaining pair (c1, c2) would create a lateral cycle and must be kept +explain (costs off) +select s.b +from sj x, + sj c1, + abs(c1.a) b, + lateral (select b.b from sj c2 where c2.a = x.a) s +where c1.a = x.a +group by cube (1) +order by 1; + QUERY PLAN +--------------------------------------------------- + Sort + Sort Key: (b.b) + -> MixedAggregate + Hash Key: (b.b) + Group Key: () + -> Nested Loop + -> Nested Loop + -> Seq Scan on sj c1 + Filter: (a IS NOT NULL) + -> Function Scan on abs b + -> Seq Scan on sj c2 + Filter: (c1.a = a) +(12 rows) + +select s.b +from sj x, + sj c1, + abs(c1.a) b, + lateral (select b.b from sj c2 where c2.a = x.a) s +where c1.a = x.a +group by cube (1) +order by 1; + b +--- + 1 + 2 + +(3 rows) + +-- Two self-join pairs across separate candidate groups (sj and sj2), +-- where merging the first pair is safe, but merging both would form a cycle +create table sj2 (a int primary key); +insert into sj2 values (1), (2); +analyze sj2; +explain (costs off) +select s1.b1 + s2.b2 as sum_b +from sj x1, + sj2 x2, + abs(x2.a) b1, + abs(x1.a) b2, + lateral (select b1.b1 from sj c1 where c1.a = x1.a) s1, + lateral (select b2.b2 from sj2 c2 where c2.a = x2.a) s2 +group by cube (1) +order by 1; + QUERY PLAN +--------------------------------------------------------------- + Sort + Sort Key: (((b1.b1) + (b2.b2))) + -> MixedAggregate + Hash Key: ((b1.b1) + (b2.b2)) + Group Key: () + -> Nested Loop + -> Nested Loop + -> Nested Loop + -> Nested Loop + -> Seq Scan on sj2 x2 + -> Function Scan on abs b1 + -> Memoize + Cache Key: b1.b1 + Cache Mode: binary + -> Seq Scan on sj c1 + Filter: (a IS NOT NULL) + -> Function Scan on abs b2 + -> Memoize + Cache Key: x2.a, b2.b2 + Cache Mode: binary + -> Seq Scan on sj2 c2 + Filter: (x2.a = a) +(22 rows) + +select s1.b1 + s2.b2 as sum_b +from sj x1, + sj2 x2, + abs(x2.a) b1, + abs(x1.a) b2, + lateral (select b1.b1 from sj c1 where c1.a = x1.a) s1, + lateral (select b2.b2 from sj2 c2 where c2.a = x2.a) s2 +group by cube (1) +order by 1; + sum_b +------- + 2 + 3 + 4 + +(4 rows) + +-- Safe self-join merge modifying nested PlaceHolderVars, read by a later candidate +explain (costs off) +select s2.y +from (sj x1 + left join lateral (select x1.a + 1 as v1) ph1 on true + left join lateral (select ph1.v1 + 10 as v2) ph2 on true), + sj c1, + sj2 x2, + abs(x2.a) b2, + lateral ( + select ph2.v2 + c2.a + b2.b2 as y + from sj2 c2 + where c2.a = x2.a + ) s2 +where c1.a = x1.a +group by cube (1) +order by 1; + QUERY PLAN +------------------------------------------------------------ + Sort + Sort Key: ((((((c1.a + 1) + 10)) + c2.a) + b2.b2)) + -> MixedAggregate + Hash Key: ((((((c1.a + 1) + 10)) + c2.a) + b2.b2)) + Group Key: () + -> Nested Loop + -> Seq Scan on sj c1 + Filter: (a IS NOT NULL) + -> Nested Loop + -> Nested Loop + -> Seq Scan on sj2 x2 + -> Function Scan on abs b2 + -> Seq Scan on sj2 c2 + Filter: (x2.a = a) +(14 rows) + +select s2.y +from (sj x1 + left join lateral (select x1.a + 1 as v1) ph1 on true + left join lateral (select ph1.v1 + 10 as v2) ph2 on true), + sj c1, + sj2 x2, + abs(x2.a) b2, + lateral ( + select ph2.v2 + c2.a + b2.b2 as y + from sj2 c2 + where c2.a = x2.a + ) s2 +where c1.a = x1.a +group by cube (1) +order by 1; + y +---- + 14 + 15 + 16 + 17 + +(5 rows) + +drop table sj2; -- Degenerated case. explain (costs off) select * from diff --git a/src/test/regress/sql/join.sql b/src/test/regress/sql/join.sql index 6264f23e081..24095516422 100644 --- a/src/test/regress/sql/join.sql +++ b/src/test/regress/sql/join.sql @@ -3517,6 +3517,187 @@ select * from sj t1 on true where t1.a = s.a; +explain (costs off) +select s.y +from sj x, + lateral (select coalesce(x.a, 0) from sj c where c.a = x.a) s(y) +group by cube (1) +order by 1; + +select s.y +from sj x, + lateral (select coalesce(x.a, 0) from sj c where c.a = x.a) s(y) +group by cube (1) +order by 1; + +-- Ensure that SJE does not turn an indirect lateral dependency chain +-- (c -> b -> x) into a cycle +explain (costs off) +select s.b +from sj x, + abs(x.a) b, + lateral (select b.b from sj c where c.a = x.a) s +group by cube (1) +order by 1; + +select s.b +from sj x, + abs(x.a) b, + lateral (select b.b from sj c where c.a = x.a) s +group by cube (1) +order by 1; + +explain (costs off) +select s.a +from sj x, + lateral (select x.a offset 0) b, + lateral (select b.a from sj c where c.a = x.a) s +group by cube (1) +order by 1; + +select s.a +from sj x, + lateral (select x.a offset 0) b, + lateral (select b.a from sj c where c.a = x.a) s +group by cube (1) +order by 1; + +-- Original outer-join reproducer +explain (costs off) +select s.y +from sj d left join + (sj x cross join lateral (select x.a as v offset 0) b + cross join lateral (select coalesce(b.v, 0) from sj c where c.a = x.a) s(y)) + on true +order by 1; + +select s.y +from sj d left join + (sj x cross join lateral (select x.a as v offset 0) b + cross join lateral (select coalesce(b.v, 0) from sj c where c.a = x.a) s(y)) + on true +order by 1; + +-- Empty table input counterpart: GROUP BY CUBE returns a single NULL row +create table sj_empty (a int primary key); + +explain (costs off) +select s.b +from sj_empty x, + abs(x.a) b, + lateral (select b.b from sj_empty c where c.a = x.a) s +group by cube (1) +order by 1; + +select s.b +from sj_empty x, + abs(x.a) b, + lateral (select b.b from sj_empty c where c.a = x.a) s +group by cube (1) +order by 1; + +drop table sj_empty; + +-- Likewise when a join-scoped PHV only acquires c in ph_eval_at after merging +explain (costs off) +select s.y +from sj x, + abs(x.a) b, + lateral (select x.a + d.a + b.b as y from sj c, sj d where c.a = x.a and d.a = x.a) s +group by cube (1) +order by 1; + +select s.y +from sj x, + abs(x.a) b, + lateral (select x.a + d.a + b.b as y from sj c, sj d where c.a = x.a and d.a = x.a) s +group by cube (1) +order by 1; + +-- 3-way self-join where one pair (x, c1) can be merged safely, while the +-- remaining pair (c1, c2) would create a lateral cycle and must be kept +explain (costs off) +select s.b +from sj x, + sj c1, + abs(c1.a) b, + lateral (select b.b from sj c2 where c2.a = x.a) s +where c1.a = x.a +group by cube (1) +order by 1; + +select s.b +from sj x, + sj c1, + abs(c1.a) b, + lateral (select b.b from sj c2 where c2.a = x.a) s +where c1.a = x.a +group by cube (1) +order by 1; + +-- Two self-join pairs across separate candidate groups (sj and sj2), +-- where merging the first pair is safe, but merging both would form a cycle +create table sj2 (a int primary key); +insert into sj2 values (1), (2); +analyze sj2; + +explain (costs off) +select s1.b1 + s2.b2 as sum_b +from sj x1, + sj2 x2, + abs(x2.a) b1, + abs(x1.a) b2, + lateral (select b1.b1 from sj c1 where c1.a = x1.a) s1, + lateral (select b2.b2 from sj2 c2 where c2.a = x2.a) s2 +group by cube (1) +order by 1; + +select s1.b1 + s2.b2 as sum_b +from sj x1, + sj2 x2, + abs(x2.a) b1, + abs(x1.a) b2, + lateral (select b1.b1 from sj c1 where c1.a = x1.a) s1, + lateral (select b2.b2 from sj2 c2 where c2.a = x2.a) s2 +group by cube (1) +order by 1; + +-- Safe self-join merge modifying nested PlaceHolderVars, read by a later candidate +explain (costs off) +select s2.y +from (sj x1 + left join lateral (select x1.a + 1 as v1) ph1 on true + left join lateral (select ph1.v1 + 10 as v2) ph2 on true), + sj c1, + sj2 x2, + abs(x2.a) b2, + lateral ( + select ph2.v2 + c2.a + b2.b2 as y + from sj2 c2 + where c2.a = x2.a + ) s2 +where c1.a = x1.a +group by cube (1) +order by 1; + +select s2.y +from (sj x1 + left join lateral (select x1.a + 1 as v1) ph1 on true + left join lateral (select ph1.v1 + 10 as v2) ph2 on true), + sj c1, + sj2 x2, + abs(x2.a) b2, + lateral ( + select ph2.v2 + c2.a + b2.b2 as y + from sj2 c2 + where c2.a = x2.a + ) s2 +where c1.a = x1.a +group by cube (1) +order by 1; + +drop table sj2; + -- Degenerated case. explain (costs off) select * from -- 2.56.0