From b39431e6e321fa2e6b00261bccf2b4132d3441af Mon Sep 17 00:00:00 2001 From: Alena Rybakina Date: Thu, 24 Sep 2026 18:41:51 +0300 Subject: [PATCH v8] Allow pulling up EXISTS sublinks with correlated JOIN/ON clauses convert_EXISTS_sublink_to_join() gave up if anything but the WHERE clause of the EXISTS sub-select referred to the parent query, so a query like SELECT * FROM ta WHERE EXISTS (SELECT 1 FROM tb JOIN tc ON tc.id = tb.id AND tb.aval = ta.id); was executed with a correlated SubPlan instead of a semijoin. Now the JOIN/ON clauses that refer to the parent query are moved to the WHERE clause of the sub-select, which becomes the semijoin or antijoin qual. This is only done for the quals of inner joins that are not below the nullable side of an outer join, since moving any other qual above its join could change the result. If the ON clause of an outer join, or a qual below the nullable side of an outer join, refers to the parent query, or a qual to be moved is volatile, the sublink is not pulled up. Join quals that don't refer to the parent query stay where they are. Author: Alena Rybakina Reviewed-by: Ranier Vilela Reviewed-by: Ilia Evdokimov Reviewed-by: Peter Petrov Tested-by: Solaimurugan Vellaipandiyan Discussion: https://postgr.es/m/0686b354-57dc-46e9-b756-21661c656c10@yandex.ru --- src/backend/optimizer/plan/subselect.c | 157 ++++++++-- src/test/regress/expected/subselect.out | 372 ++++++++++++++++++++++++ src/test/regress/sql/subselect.sql | 171 +++++++++++ 3 files changed, 680 insertions(+), 20 deletions(-) diff --git a/src/backend/optimizer/plan/subselect.c b/src/backend/optimizer/plan/subselect.c index 5760b616813..c72eaf2cbd7 100644 --- a/src/backend/optimizer/plan/subselect.c +++ b/src/backend/optimizer/plan/subselect.c @@ -93,6 +93,8 @@ static void inline_cte(PlannerInfo *root, CommonTableExpr *cte); static bool inline_cte_walker(Node *node, inline_cte_walker_context *context); static bool sublink_testexpr_is_not_nullable(PlannerInfo *root, SubLink *sublink); static bool simplify_EXISTS_query(PlannerInfo *root, Query *query); +static bool collect_correlated_join_quals(Node *jtnode, bool nullable_side, + List **hoist_nodes); static Query *convert_EXISTS_to_ANY(PlannerInfo *root, Query *subselect, Node **testexpr, List **paramIds); static Node *replace_correlation_vars_mutator(Node *node, PlannerInfo *root); @@ -1593,6 +1595,10 @@ sublink_testexpr_is_not_nullable(PlannerInfo *root, SubLink *sublink) * convert_EXISTS_sublink_to_join: try to convert an EXISTS SubLink to a join * * The API of this function is identical to convert_ANY_sublink_to_join's. + * + * References to the parent query are allowed in the WHERE clause of the + * sub-select and in the JOIN/ON clauses that can be moved to the WHERE + * clause without changing the result; see collect_correlated_join_quals(). */ JoinExpr * convert_EXISTS_sublink_to_join(PlannerInfo *root, SubLink *sublink, @@ -1602,6 +1608,9 @@ convert_EXISTS_sublink_to_join(PlannerInfo *root, SubLink *sublink, Query *parse = root->parse; Query *subselect = (Query *) sublink->subselect; Node *whereClause; + FromExpr *jointree; + List *hoist_nodes = NIL; + ListCell *lc; PlannerInfo subroot; int rtoffset; int varno; @@ -1637,31 +1646,29 @@ convert_EXISTS_sublink_to_join(PlannerInfo *root, SubLink *sublink, return NULL; /* - * Separate out the WHERE clause. (We could theoretically also remove - * top-level plain JOIN/ON clauses, but it's probably not worth the - * trouble.) - */ - whereClause = subselect->jointree->quals; - subselect->jointree->quals = NULL; - - /* - * The rest of the sub-select must not refer to any Vars of the parent - * query. (Vars of higher levels should be okay, though.) + * Only the jointree (the WHERE clause and the JOIN/ON clauses) may refer + * to Vars of the parent query; which of them can be moved to the join + * qual is checked below. The rest of the sub-select must not refer to + * any Vars of the parent query. (Vars of higher levels should be okay, + * though.) */ + jointree = subselect->jointree; + subselect->jointree = NULL; if (contain_vars_of_level((Node *) subselect, 1)) return NULL; + subselect->jointree = jointree; /* - * On the other hand, the WHERE clause must contain some Vars of the - * parent query, else it's not gonna be a join. + * On the other hand, the jointree must contain some Vars of the parent + * query, else it's not gonna be a join. */ - if (!contain_vars_of_level(whereClause, 1)) + if (!contain_vars_of_level((Node *) jointree, 1)) return NULL; /* * We don't risk optimizing if the WHERE clause is volatile, either. */ - if (contain_volatile_functions(whereClause)) + if (contain_volatile_functions(jointree->quals)) return NULL; /* @@ -1675,24 +1682,46 @@ convert_EXISTS_sublink_to_join(PlannerInfo *root, SubLink *sublink, * Note: we construct up an entirely dummy PlannerInfo for use here. This * is fine because only the "glob" and "parse" links will be used in this * case. - * - * Note: we temporarily assign back the WHERE clause so that any virtual - * generated column references within it can be expanded. It should be - * separated out again afterward. */ MemSet(&subroot, 0, sizeof(subroot)); subroot.type = T_PlannerInfo; subroot.glob = root->glob; subroot.parse = subselect; - subselect->jointree->quals = whereClause; subselect = preprocess_relation_rtes(&subroot); /* - * Now separate out the WHERE clause again. + * Now separate out the WHERE clause. */ whereClause = subselect->jointree->quals; subselect->jointree->quals = NULL; + /* + * Find the JOIN/ON clauses that refer to the parent query, and move them + * to the WHERE clause. Fail if some of them can't be moved. + */ + if (!collect_correlated_join_quals((Node *) subselect->jointree, false, + &hoist_nodes)) + return NULL; + + foreach(lc, hoist_nodes) + { + Node *node = (Node *) lfirst(lc); + + if (IsA(node, JoinExpr)) + { + whereClause = make_and_qual(whereClause, ((JoinExpr *) node)->quals); + ((JoinExpr *) node)->quals = NULL; + } + else + { + whereClause = make_and_qual(whereClause, ((FromExpr *) node)->quals); + ((FromExpr *) node)->quals = NULL; + } + } + + /* Now the rest of the sub-select must not refer to the parent query */ + Assert(!contain_vars_of_level((Node *) subselect, 1)); + /* * The subquery must have a nonempty jointree, but we can make it so. */ @@ -1777,6 +1806,94 @@ convert_EXISTS_sublink_to_join(PlannerInfo *root, SubLink *sublink, return result; } +/* + * collect_correlated_join_quals: find the quals of an EXISTS sub-select's + * jointree that refer to the parent query + * + * convert_EXISTS_sublink_to_join() moves such quals to the WHERE clause of + * the sub-select, which then becomes the semijoin or antijoin qual. That is + * only safe for the quals of inner joins (and of FROM lists) that are not + * below the nullable side of an outer join. The ON clause of an outer join + * doesn't filter out rows of its non-nullable side, and a qual below the + * nullable side of an outer join only decides which rows are null-extended, + * so evaluating either of them above the join could change the result. + * + * nullable_side is true if jtnode is below the nullable side of some outer + * join. JoinExpr and FromExpr nodes whose quals are to be moved are appended + * to *hoist_nodes. Returns false if some quals referring to the parent query + * can't be moved, or are volatile. + */ +static bool +collect_correlated_join_quals(Node *jtnode, bool nullable_side, + List **hoist_nodes) +{ + if (jtnode == NULL || IsA(jtnode, RangeTblRef)) + return true; + + if (IsA(jtnode, FromExpr)) + { + FromExpr *f = (FromExpr *) jtnode; + ListCell *lc; + + if (contain_vars_of_level(f->quals, 1)) + { + if (nullable_side || contain_volatile_functions(f->quals)) + return false; + *hoist_nodes = lappend(*hoist_nodes, f); + } + + foreach(lc, f->fromlist) + { + if (!collect_correlated_join_quals(lfirst(lc), nullable_side, + hoist_nodes)) + return false; + } + } + else if (IsA(jtnode, JoinExpr)) + { + JoinExpr *j = (JoinExpr *) jtnode; + bool larg_nullable = nullable_side; + bool rarg_nullable = nullable_side; + + switch (j->jointype) + { + case JOIN_INNER: + break; + case JOIN_LEFT: + rarg_nullable = true; + break; + case JOIN_RIGHT: + larg_nullable = true; + break; + case JOIN_FULL: + larg_nullable = true; + rarg_nullable = true; + break; + default: + /* semijoins and antijoins aren't expected here */ + return false; + } + + if (contain_vars_of_level(j->quals, 1)) + { + if (j->jointype != JOIN_INNER || nullable_side || + contain_volatile_functions(j->quals)) + return false; + *hoist_nodes = lappend(*hoist_nodes, j); + } + + if (!collect_correlated_join_quals(j->larg, larg_nullable, + hoist_nodes) || + !collect_correlated_join_quals(j->rarg, rarg_nullable, + hoist_nodes)) + return false; + } + else + elog(ERROR, "unrecognized node type: %d", (int) nodeTag(jtnode)); + + return true; +} + /* * simplify_EXISTS_query: remove any useless stuff in an EXISTS's subquery * diff --git a/src/test/regress/expected/subselect.out b/src/test/regress/expected/subselect.out index cf295d56507..62c3d034274 100644 --- a/src/test/regress/expected/subselect.out +++ b/src/test/regress/expected/subselect.out @@ -1042,6 +1042,378 @@ where exists ( where road.name = ss.f1 ); rollback; -- +-- Tests for pulling up EXISTS sublinks whose JOIN/ON clauses refer to the +-- parent query +-- +CREATE TEMP TABLE td (id int); +INSERT INTO td SELECT g FROM generate_series(1, 100) g; +ANALYZE td; +-- Pull-up: correlated ON clause of an inner join +EXPLAIN (COSTS OFF) +SELECT 1 +FROM ta +WHERE EXISTS ( + SELECT 1 + FROM tb + JOIN tc ON ta.id = tc.id + AND ta.id = tb.id +); + QUERY PLAN +------------------------------------ + Hash Right Semi Join + Hash Cond: (tc.id = ta.id) + -> Hash Join + Hash Cond: (tb.id = tc.id) + -> Seq Scan on tb + -> Hash + -> Seq Scan on tc + -> Hash + -> Seq Scan on ta +(9 rows) + +-- Pull-up: the correlated ON clause refers only to the parent query +EXPLAIN (COSTS OFF) +SELECT 1 +FROM ta +JOIN tb ON true +WHERE EXISTS ( + SELECT 1 + FROM tb tb1 + JOIN tc ON ta.id = tb.id +); + QUERY PLAN +------------------------------------ + Nested Loop Semi Join + -> Hash Join + Hash Cond: (ta.id = tb.id) + -> Seq Scan on ta + -> Hash + -> Seq Scan on tb + -> Nested Loop + -> Seq Scan on tb tb1 + -> Materialize + -> Seq Scan on tc +(10 rows) + +-- Pull-up: correlated ON clause of a join nested in another join +EXPLAIN (COSTS OFF) +SELECT ta.* +FROM ta +WHERE EXISTS ( + SELECT 1 + FROM tb + JOIN tc ON tc.id = tb.id + AND tb.id = ta.id + JOIN td ON td.id = tc.id +); + QUERY PLAN +------------------------------------------------------- + Nested Loop + Join Filter: (ta.id = td.id) + -> HashAggregate + Group Key: tb.id + -> Nested Loop + Join Filter: (tc.id = td.id) + -> Hash Join + Hash Cond: (tb.id = td.id) + -> Seq Scan on tb + -> Hash + -> Seq Scan on td + -> Index Only Scan using tc_pkey on tc + Index Cond: (id = tb.id) + -> Index Scan using ta_pkey on ta + Index Cond: (id = tc.id) +(15 rows) + +-- Pull-up: correlated ON clause together with an EXISTS in the WHERE clause +EXPLAIN (COSTS OFF) +SELECT 1 +FROM ta ta1 +WHERE EXISTS ( + SELECT 1 + FROM ta + JOIN tb ON ta.id = ta1.id + AND ta1.val = 1 + WHERE EXISTS ( + SELECT 1 + FROM ta ta2 + WHERE ta2.id = ta1.id + ) +); + QUERY PLAN +------------------------------------------------- + Nested Loop Semi Join + -> Seq Scan on ta ta2 + Filter: (val = 1) + -> Nested Loop + -> Index Only Scan using ta_pkey on ta + Index Cond: (id = ta2.id) + -> Seq Scan on tb +(7 rows) + +-- Pull-up: the moved ON clause contains an EXISTS sublink +EXPLAIN (COSTS OFF) +SELECT ta.id +FROM ta +WHERE EXISTS ( + SELECT 1 + FROM tb + JOIN tc ON tc.id = ta.id + AND EXISTS ( + SELECT 1 + FROM td + WHERE td.id = ta.id + ) +); + QUERY PLAN +------------------------------------------------- + Nested Loop Semi Join + -> Hash Semi Join + Hash Cond: (ta.id = td.id) + -> Seq Scan on ta + -> Hash + -> Seq Scan on td + -> Nested Loop + -> Index Only Scan using tc_pkey on tc + Index Cond: (id = td.id) + -> Seq Scan on tb +(10 rows) + +EXPLAIN (COSTS OFF) +SELECT ta.id +FROM ta +WHERE EXISTS ( + SELECT 1 + FROM tb + JOIN tc ON tc.id = ta.id + AND EXISTS ( + SELECT 1 + FROM td + WHERE tb.id = ta.id + ) +); + QUERY PLAN +-------------------------------------------- + Hash Right Semi Join + Hash Cond: (tc.id = ta.id) + Join Filter: EXISTS(SubPlan exists_1) + -> Nested Loop + -> Seq Scan on tb + -> Materialize + -> Seq Scan on tc + -> Hash + -> Seq Scan on ta + SubPlan exists_1 + -> Result + One-Time Filter: (tb.id = ta.id) + -> Seq Scan on td +(13 rows) + +-- No pull-up: the ON clause of an outer join refers to the parent query +EXPLAIN (COSTS OFF) +SELECT 1 +FROM ta +WHERE EXISTS ( + SELECT 1 + FROM tb + RIGHT JOIN tc ON ta.id = tc.id +); + QUERY PLAN +---------------------------------------- + Seq Scan on ta + Filter: EXISTS(SubPlan exists_1) + SubPlan exists_1 + -> Nested Loop Left Join + Join Filter: (ta.id = tc.id) + -> Seq Scan on tc + -> Materialize + -> Seq Scan on tb +(8 rows) + +DROP TABLE td; +-- Check the results of queries where only some of the JOIN/ON clauses can be +-- moved up +CREATE TEMP TABLE ex_o (x int, y int); +CREATE TEMP TABLE ex_a (x int, y int); +CREATE TEMP TABLE ex_b (x int, y int); +CREATE TEMP TABLE ex_c (x int, y int); +INSERT INTO ex_o VALUES (1, 1), (2, 2), (3, 3); +INSERT INTO ex_a VALUES (1, 10), (5, 50); +INSERT INTO ex_b VALUES (7, 70); +INSERT INTO ex_c VALUES (100, 1); +ANALYZE ex_o, ex_a, ex_b, ex_c; +-- No pull-up: the ON clause of a LEFT JOIN refers to the parent query +EXPLAIN (COSTS OFF) +SELECT * FROM ex_o o +WHERE EXISTS (SELECT 1 FROM ex_a a LEFT JOIN ex_b b ON a.x = o.x); + QUERY PLAN +---------------------------------------- + Seq Scan on ex_o o + Filter: EXISTS(SubPlan exists_1) + SubPlan exists_1 + -> Nested Loop Left Join + Join Filter: (a.x = o.x) + -> Seq Scan on ex_a a + -> Materialize + -> Seq Scan on ex_b b +(8 rows) + +SELECT * FROM ex_o o +WHERE EXISTS (SELECT 1 FROM ex_a a LEFT JOIN ex_b b ON a.x = o.x) +ORDER BY 1; + x | y +---+--- + 1 | 1 + 2 | 2 + 3 | 3 +(3 rows) + +-- No pull-up: correlated inner join below the nullable side of a LEFT JOIN +EXPLAIN (COSTS OFF) +SELECT * FROM ex_o o +WHERE EXISTS (SELECT 1 FROM ex_a a + LEFT JOIN (ex_b b JOIN ex_c c ON b.x = c.x AND c.y = o.y) + ON a.x = b.x); + QUERY PLAN +------------------------------------------------ + Seq Scan on ex_o o + Filter: EXISTS(SubPlan exists_1) + SubPlan exists_1 + -> Nested Loop Left Join + Join Filter: (a.x = b.x) + -> Seq Scan on ex_a a + -> Materialize + -> Nested Loop + Join Filter: (b.x = c.x) + -> Seq Scan on ex_b b + -> Seq Scan on ex_c c + Filter: (y = o.y) +(12 rows) + +SELECT * FROM ex_o o +WHERE EXISTS (SELECT 1 FROM ex_a a + LEFT JOIN (ex_b b JOIN ex_c c ON b.x = c.x AND c.y = o.y) + ON a.x = b.x) +ORDER BY 1; + x | y +---+--- + 1 | 1 + 2 | 2 + 3 | 3 +(3 rows) + +-- No pull-up: correlated inner join below a FULL JOIN +EXPLAIN (COSTS OFF) +SELECT * FROM ex_o o +WHERE EXISTS (SELECT 1 FROM (ex_a a JOIN ex_c c ON a.x = o.x) + FULL JOIN ex_b b ON a.x = b.x); + QUERY PLAN +----------------------------------------- + Seq Scan on ex_o o + Filter: EXISTS(SubPlan exists_1) + SubPlan exists_1 + -> Hash Full Join + Hash Cond: (a.x = b.x) + -> Nested Loop + -> Seq Scan on ex_a a + Filter: (x = o.x) + -> Seq Scan on ex_c c + -> Hash + -> Seq Scan on ex_b b +(11 rows) + +-- Pull-up: correlated inner join above a LEFT JOIN; the uncorrelated ON +-- clause of the LEFT JOIN must be kept +EXPLAIN (COSTS OFF) +SELECT * FROM ex_o o +WHERE EXISTS (SELECT 1 FROM ex_a a LEFT JOIN ex_b b ON a.x = b.x + JOIN ex_c c ON b.y = c.y AND c.y = o.y); + QUERY PLAN +---------------------------------------------- + Nested Loop Semi Join + Join Filter: (o.y = b.y) + -> Seq Scan on ex_o o + -> Materialize + -> Nested Loop + Join Filter: (b.y = c.y) + -> Nested Loop + Join Filter: (a.x = b.x) + -> Seq Scan on ex_b b + -> Seq Scan on ex_a a + -> Seq Scan on ex_c c +(11 rows) + +SELECT * FROM ex_o o +WHERE EXISTS (SELECT 1 FROM ex_a a LEFT JOIN ex_b b ON a.x = b.x + JOIN ex_c c ON b.y = c.y AND c.y = o.y) +ORDER BY 1; + x | y +---+--- +(0 rows) + +-- Pull-up: correlated inner join on the non-nullable side of a LEFT JOIN +EXPLAIN (COSTS OFF) +SELECT * FROM ex_o o +WHERE EXISTS (SELECT 1 FROM (ex_a a JOIN ex_c c ON a.x = o.x) + LEFT JOIN ex_b b ON a.x = b.x); + QUERY PLAN +-------------------------------------------------- + Nested Loop Semi Join + Join Filter: (o.x = a.x) + -> Seq Scan on ex_o o + -> Materialize + -> Nested Loop + -> Seq Scan on ex_c c + -> Nested Loop Left Join + Join Filter: (a.x = b.x) + -> Seq Scan on ex_a a + -> Materialize + -> Seq Scan on ex_b b +(11 rows) + +SELECT * FROM ex_o o +WHERE EXISTS (SELECT 1 FROM (ex_a a JOIN ex_c c ON a.x = o.x) + LEFT JOIN ex_b b ON a.x = b.x) +ORDER BY 1; + x | y +---+--- + 1 | 1 +(1 row) + +-- Pull-up: the WHERE clause may refer to the nullable side of a LEFT JOIN +EXPLAIN (COSTS OFF) +SELECT * FROM ex_o o +WHERE EXISTS (SELECT 1 FROM ex_a a LEFT JOIN ex_b b ON a.x = b.x + WHERE b.y = o.y); + QUERY PLAN +---------------------------------------- + Nested Loop Semi Join + Join Filter: (o.y = b.y) + -> Seq Scan on ex_o o + -> Materialize + -> Nested Loop + Join Filter: (a.x = b.x) + -> Seq Scan on ex_b b + -> Seq Scan on ex_a a +(8 rows) + +-- No pull-up: volatile correlated ON clause +EXPLAIN (COSTS OFF) +SELECT * FROM ex_o o +WHERE EXISTS (SELECT 1 FROM ex_a a JOIN ex_b b ON a.x = o.x AND random() > 0.5); + QUERY PLAN +------------------------------------------------------------- + Seq Scan on ex_o o + Filter: EXISTS(SubPlan exists_1) + SubPlan exists_1 + -> Nested Loop + Join Filter: (random() > '0.5'::double precision) + -> Seq Scan on ex_a a + Filter: (x = o.x) + -> Seq Scan on ex_b b +(8 rows) + +DROP TABLE ex_o, ex_a, ex_b, ex_c; -- Test case for sublinks pushed down into subselects via join alias expansion -- select diff --git a/src/test/regress/sql/subselect.sql b/src/test/regress/sql/subselect.sql index 07438694f6e..cf08004e40a 100644 --- a/src/test/regress/sql/subselect.sql +++ b/src/test/regress/sql/subselect.sql @@ -529,6 +529,177 @@ where exists ( rollback; -- +-- Tests for pulling up EXISTS sublinks whose JOIN/ON clauses refer to the +-- parent query +-- + +CREATE TEMP TABLE td (id int); +INSERT INTO td SELECT g FROM generate_series(1, 100) g; +ANALYZE td; + +-- Pull-up: correlated ON clause of an inner join +EXPLAIN (COSTS OFF) +SELECT 1 +FROM ta +WHERE EXISTS ( + SELECT 1 + FROM tb + JOIN tc ON ta.id = tc.id + AND ta.id = tb.id +); + +-- Pull-up: the correlated ON clause refers only to the parent query +EXPLAIN (COSTS OFF) +SELECT 1 +FROM ta +JOIN tb ON true +WHERE EXISTS ( + SELECT 1 + FROM tb tb1 + JOIN tc ON ta.id = tb.id +); + +-- Pull-up: correlated ON clause of a join nested in another join +EXPLAIN (COSTS OFF) +SELECT ta.* +FROM ta +WHERE EXISTS ( + SELECT 1 + FROM tb + JOIN tc ON tc.id = tb.id + AND tb.id = ta.id + JOIN td ON td.id = tc.id +); + +-- Pull-up: correlated ON clause together with an EXISTS in the WHERE clause +EXPLAIN (COSTS OFF) +SELECT 1 +FROM ta ta1 +WHERE EXISTS ( + SELECT 1 + FROM ta + JOIN tb ON ta.id = ta1.id + AND ta1.val = 1 + WHERE EXISTS ( + SELECT 1 + FROM ta ta2 + WHERE ta2.id = ta1.id + ) +); + +-- Pull-up: the moved ON clause contains an EXISTS sublink +EXPLAIN (COSTS OFF) +SELECT ta.id +FROM ta +WHERE EXISTS ( + SELECT 1 + FROM tb + JOIN tc ON tc.id = ta.id + AND EXISTS ( + SELECT 1 + FROM td + WHERE td.id = ta.id + ) +); + +EXPLAIN (COSTS OFF) +SELECT ta.id +FROM ta +WHERE EXISTS ( + SELECT 1 + FROM tb + JOIN tc ON tc.id = ta.id + AND EXISTS ( + SELECT 1 + FROM td + WHERE tb.id = ta.id + ) +); + +-- No pull-up: the ON clause of an outer join refers to the parent query +EXPLAIN (COSTS OFF) +SELECT 1 +FROM ta +WHERE EXISTS ( + SELECT 1 + FROM tb + RIGHT JOIN tc ON ta.id = tc.id +); + +DROP TABLE td; + +-- Check the results of queries where only some of the JOIN/ON clauses can be +-- moved up +CREATE TEMP TABLE ex_o (x int, y int); +CREATE TEMP TABLE ex_a (x int, y int); +CREATE TEMP TABLE ex_b (x int, y int); +CREATE TEMP TABLE ex_c (x int, y int); +INSERT INTO ex_o VALUES (1, 1), (2, 2), (3, 3); +INSERT INTO ex_a VALUES (1, 10), (5, 50); +INSERT INTO ex_b VALUES (7, 70); +INSERT INTO ex_c VALUES (100, 1); +ANALYZE ex_o, ex_a, ex_b, ex_c; + +-- No pull-up: the ON clause of a LEFT JOIN refers to the parent query +EXPLAIN (COSTS OFF) +SELECT * FROM ex_o o +WHERE EXISTS (SELECT 1 FROM ex_a a LEFT JOIN ex_b b ON a.x = o.x); +SELECT * FROM ex_o o +WHERE EXISTS (SELECT 1 FROM ex_a a LEFT JOIN ex_b b ON a.x = o.x) +ORDER BY 1; + +-- No pull-up: correlated inner join below the nullable side of a LEFT JOIN +EXPLAIN (COSTS OFF) +SELECT * FROM ex_o o +WHERE EXISTS (SELECT 1 FROM ex_a a + LEFT JOIN (ex_b b JOIN ex_c c ON b.x = c.x AND c.y = o.y) + ON a.x = b.x); +SELECT * FROM ex_o o +WHERE EXISTS (SELECT 1 FROM ex_a a + LEFT JOIN (ex_b b JOIN ex_c c ON b.x = c.x AND c.y = o.y) + ON a.x = b.x) +ORDER BY 1; + +-- No pull-up: correlated inner join below a FULL JOIN +EXPLAIN (COSTS OFF) +SELECT * FROM ex_o o +WHERE EXISTS (SELECT 1 FROM (ex_a a JOIN ex_c c ON a.x = o.x) + FULL JOIN ex_b b ON a.x = b.x); + +-- Pull-up: correlated inner join above a LEFT JOIN; the uncorrelated ON +-- clause of the LEFT JOIN must be kept +EXPLAIN (COSTS OFF) +SELECT * FROM ex_o o +WHERE EXISTS (SELECT 1 FROM ex_a a LEFT JOIN ex_b b ON a.x = b.x + JOIN ex_c c ON b.y = c.y AND c.y = o.y); +SELECT * FROM ex_o o +WHERE EXISTS (SELECT 1 FROM ex_a a LEFT JOIN ex_b b ON a.x = b.x + JOIN ex_c c ON b.y = c.y AND c.y = o.y) +ORDER BY 1; + +-- Pull-up: correlated inner join on the non-nullable side of a LEFT JOIN +EXPLAIN (COSTS OFF) +SELECT * FROM ex_o o +WHERE EXISTS (SELECT 1 FROM (ex_a a JOIN ex_c c ON a.x = o.x) + LEFT JOIN ex_b b ON a.x = b.x); +SELECT * FROM ex_o o +WHERE EXISTS (SELECT 1 FROM (ex_a a JOIN ex_c c ON a.x = o.x) + LEFT JOIN ex_b b ON a.x = b.x) +ORDER BY 1; + +-- Pull-up: the WHERE clause may refer to the nullable side of a LEFT JOIN +EXPLAIN (COSTS OFF) +SELECT * FROM ex_o o +WHERE EXISTS (SELECT 1 FROM ex_a a LEFT JOIN ex_b b ON a.x = b.x + WHERE b.y = o.y); + +-- No pull-up: volatile correlated ON clause +EXPLAIN (COSTS OFF) +SELECT * FROM ex_o o +WHERE EXISTS (SELECT 1 FROM ex_a a JOIN ex_b b ON a.x = o.x AND random() > 0.5); + +DROP TABLE ex_o, ex_a, ex_b, ex_c; + -- Test case for sublinks pushed down into subselects via join alias expansion -- -- 2.50.1 (Apple Git-155)