From 2970fbefc048001cfa108f5a41d809ebae6ad280 Mon Sep 17 00:00:00 2001 From: Alena Rybakina Date: Fri, 30 Oct 2026 18:49:50 +0300 Subject: [PATCH 1/2] Preserve SubPlans when folding ORCA Result projections Keep a Result node when replacing its child's target list would drop a SubPlan reference after Motion slices have been allocated. Build the proposed projection on copies so rejecting the fold leaves the child expressions intact. Add regression coverage for EXISTS and NOT EXISTS with nullable outer joins, LIMIT/OFFSET, and multiple unused SubPlans. --- src/backend/optimizer/plan/orca.c | 64 +++++- .../regress/expected/orca_motion_subplan.out | 191 ++++++++++++++++++ src/test/regress/greenplum_schedule | 1 + src/test/regress/sql/orca_motion_subplan.sql | 102 ++++++++++ 4 files changed, 352 insertions(+), 6 deletions(-) create mode 100644 src/test/regress/expected/orca_motion_subplan.out create mode 100644 src/test/regress/sql/orca_motion_subplan.sql diff --git a/src/backend/optimizer/plan/orca.c b/src/backend/optimizer/plan/orca.c index a083fcda6c7..ae6faa213aa 100644 --- a/src/backend/optimizer/plan/orca.c +++ b/src/backend/optimizer/plan/orca.c @@ -34,6 +34,7 @@ #include "optimizer/planner.h" #include "optimizer/tlist.h" #include "optimizer/transform.h" +#include "optimizer/walkers.h" #include "parser/parse_collate.h" #include "parser/parsetree.h" #include "rewrite/rewriteManip.h" @@ -52,6 +53,7 @@ extern PlannedStmt * GPOPTOptimizedPlan(Query *parse, bool *had_unexpected_failu static Plan *remove_redundant_results(PlannerInfo *root, Plan *plan); static Node *remove_redundant_results_mutator(Node *node, void *); static bool can_replace_tlist(Plan *plan); +static bool preserves_subplans(List *old_tlist, List *new_tlist); static Node *push_down_expr_mutator(Node *node, List *child_tlist); /* @@ -453,11 +455,12 @@ remove_redundant_results_mutator(Node *node, void *ctx) !expression_returns_set((Node *) result_plan->plan.targetlist) && can_replace_tlist(child_plan)) { - List *tlist = result_plan->plan.targetlist; + List *tlist = copyObject(result_plan->plan.targetlist); ListCell *lc; child_plan = (Plan *) remove_redundant_results_mutator((Node *) child_plan, ctx); + result_plan->plan.lefttree = child_plan; foreach(lc, tlist) { @@ -467,6 +470,24 @@ remove_redundant_results_mutator(Node *node, void *ctx) child_plan->targetlist); } + /* + * The slice table has already been built. Dropping a SubPlan + * reference here can leave its Motion slices without an executable + * plan tree. Keep the Result unless all child SubPlans survive. + */ + if (!preserves_subplans(child_plan->targetlist, tlist)) + { + /* + * The child was already processed above. Process the Result's + * target list here so that SubPlans retained by the Result are + * processed too, without descending into the child a second time. + */ + result_plan->plan.targetlist = (List *) + remove_redundant_results_mutator( + (Node *) result_plan->plan.targetlist, ctx); + return node; + } + child_plan->targetlist = tlist; child_plan->flow = result_plan->plan.flow; @@ -480,6 +501,34 @@ remove_redundant_results_mutator(Node *node, void *ctx) true); } +/* Allow projection folding when it retains every referenced SubPlan. */ +static bool +preserves_subplans(List *old_tlist, List *new_tlist) +{ + List *old_subplans; + List *new_subplans; + ListCell *lc; + Bitmapset *remaining = NULL; + bool preserved; + + if (!contain_subplans((Node *) old_tlist)) + return true; + + old_subplans = extract_nodes_expression((Node *) old_tlist, T_SubPlan, false); + new_subplans = new_tlist ? + extract_nodes_expression((Node *) new_tlist, T_SubPlan, false) : NIL; + foreach(lc, old_subplans) + remaining = bms_add_member(remaining, ((SubPlan *) lfirst(lc))->plan_id); + foreach(lc, new_subplans) + remaining = bms_del_member(remaining, ((SubPlan *) lfirst(lc))->plan_id); + + preserved = bms_is_empty(remaining); + bms_free(remaining); + list_free(old_subplans); + list_free(new_subplans); + return preserved; +} + /* * Can the target list of a Plan node safely be replaced? */ @@ -541,20 +590,23 @@ push_down_expr_mutator(Node *node, List *child_tlist) { TargetEntry *child_tle = (TargetEntry *) list_nth(child_tlist, var->varattno - 1); + Expr *expr = copyObject(child_tle->expr); + + /* Do not change the child if the proposed replacement is rejected. */ // The const expr pertatining to a column in a child result node // has const.consttypmod set as default value. // correct typmod can be found at var.vartypmod. // const.consttypmod value needs to be fixed before replacing var with const. - if (IsA(child_tle->expr, Const)) + if (IsA(expr, Const)) { - ((Const *) child_tle->expr)->consttypmod = ((Var *) node)->vartypmod; + ((Const *) expr)->consttypmod = var->vartypmod; } - else if (IsA(child_tle->expr, Var)) + else if (IsA(expr, Var)) { - ((Var *) child_tle->expr)->vartypmod = ((Var *) node)->vartypmod; + ((Var *) expr)->vartypmod = var->vartypmod; } - return (Node *) child_tle->expr; + return (Node *) expr; } } return expression_tree_mutator(node, push_down_expr_mutator, child_tlist); diff --git a/src/test/regress/expected/orca_motion_subplan.out b/src/test/regress/expected/orca_motion_subplan.out new file mode 100644 index 00000000000..f98d5403705 --- /dev/null +++ b/src/test/regress/expected/orca_motion_subplan.out @@ -0,0 +1,191 @@ +-- Result projection folding must not orphan a SubPlan's Motion slices. +SET optimizer = on; +CREATE TEMP TABLE motion_l(id int) DISTRIBUTED BY (id); +CREATE TEMP TABLE motion_r(id int, lid int, k int) DISTRIBUTED BY (id); +CREATE TEMP TABLE motion_s(id int, k int) DISTRIBUTED BY (id); +INSERT INTO motion_l VALUES (1), (2), (3), (4); +INSERT INTO motion_r VALUES (10,1,7), (11,1,NULL), (12,3,8), (13,5,NULL); +INSERT INTO motion_s VALUES (100,7), (101,7), (102,8), (103,NULL), (104,9); +ANALYZE motion_l; +ANALYZE motion_r; +ANALYZE motion_s; +-- The count SubPlan is unused by EXISTS, but its slice has already been +-- allocated. EXPLAIN must still reach it after Result projection folding. +EXPLAIN (COSTS OFF) +SELECT l.id, r.id +FROM motion_l l LEFT JOIN motion_r r ON l.id = r.lid +WHERE EXISTS (SELECT (SELECT count(*) FROM motion_s) + FROM motion_s s WHERE s.k IS NOT DISTINCT FROM r.k LIMIT 1); + QUERY PLAN +-------------------------------------------------------------------------------- + Result + Filter: (SubPlan 2) + -> Gather Motion 3:1 (slice1; segments: 3) + -> Hash Left Join + Hash Cond: (l.id = r.lid) + -> Seq Scan on motion_l l + -> Hash + -> Redistribute Motion 3:3 (slice2; segments: 3) + Hash Key: r.lid + -> Seq Scan on motion_r r + SubPlan 2 + -> Result + -> Result + -> Limit + -> Result + Filter: (NOT (s.k IS DISTINCT FROM r.k)) + -> Materialize + -> Gather Motion 3:1 (slice3; segments: 3) + -> Seq Scan on motion_s s + SubPlan 1 + -> Aggregate + -> Materialize + -> Gather Motion 3:1 (slice4; segments: 3) + -> Seq Scan on motion_s + Optimizer: GPORCA +(25 rows) + +SET optimizer = off; +SELECT l.id, r.id +FROM motion_l l LEFT JOIN motion_r r ON l.id = r.lid +WHERE EXISTS (SELECT (SELECT count(*) FROM motion_s) + FROM motion_s s WHERE s.k IS NOT DISTINCT FROM r.k LIMIT 1) +ORDER BY 1, 2; + id | id +----+---- + 1 | 10 + 1 | 11 + 2 | + 3 | 12 + 4 | +(5 rows) + +SET optimizer = on; +SELECT l.id, r.id +FROM motion_l l LEFT JOIN motion_r r ON l.id = r.lid +WHERE EXISTS (SELECT (SELECT count(*) FROM motion_s) + FROM motion_s s WHERE s.k IS NOT DISTINCT FROM r.k LIMIT 1) +ORDER BY 1, 2; + id | id +----+---- + 1 | 10 + 1 | 11 + 2 | + 3 | 12 + 4 | +(5 rows) + +-- NOT EXISTS and a positive OFFSET: only k = 7 has a second matching row. +-- Include both real NULL keys and NULL-extended rows from a FULL JOIN. +SET optimizer = off; +SELECT l.id, r.id +FROM motion_l l FULL JOIN motion_r r ON l.id = r.lid +WHERE NOT EXISTS (SELECT (SELECT count(*) FROM motion_s) + FROM motion_s s WHERE s.k = r.k LIMIT 1 OFFSET 1) +ORDER BY 1, 2; + id | id +----+---- + 1 | 11 + 2 | + 3 | 12 + 4 | + | 13 +(5 rows) + +SET optimizer = on; +SELECT l.id, r.id +FROM motion_l l FULL JOIN motion_r r ON l.id = r.lid +WHERE NOT EXISTS (SELECT (SELECT count(*) FROM motion_s) + FROM motion_s s WHERE s.k = r.k LIMIT 1 OFFSET 1) +ORDER BY 1, 2; + id | id +----+---- + 1 | 11 + 2 | + 3 | 12 + 4 | + | 13 +(5 rows) + +-- A positive OFFSET must also preserve a nonempty EXISTS result. +SET optimizer = off; +SELECT l.id, r.id +FROM motion_l l FULL JOIN motion_r r ON l.id = r.lid +WHERE EXISTS (SELECT (SELECT count(*) FROM motion_s) + FROM motion_s s WHERE r.id IS NULL LIMIT 2 OFFSET 2) +ORDER BY 1, 2; + id | id +----+---- + 2 | + 4 | +(2 rows) + +SET optimizer = on; +SELECT l.id, r.id +FROM motion_l l FULL JOIN motion_r r ON l.id = r.lid +WHERE EXISTS (SELECT (SELECT count(*) FROM motion_s) + FROM motion_s s WHERE r.id IS NULL LIMIT 2 OFFSET 2) +ORDER BY 1, 2; + id | id +----+---- + 2 | + 4 | +(2 rows) + +-- LIMIT 0 still makes EXISTS false, even with a scalar aggregate target. +SET optimizer = off; +SELECT l.id, r.id +FROM motion_l l LEFT JOIN motion_r r ON l.id = r.lid +WHERE EXISTS (SELECT (SELECT count(*) FROM motion_s) + FROM motion_s s WHERE s.k IS NOT DISTINCT FROM r.k LIMIT 0) +ORDER BY 1, 2; + id | id +----+---- +(0 rows) + +SET optimizer = on; +SELECT l.id, r.id +FROM motion_l l LEFT JOIN motion_r r ON l.id = r.lid +WHERE EXISTS (SELECT (SELECT count(*) FROM motion_s) + FROM motion_s s WHERE s.k IS NOT DISTINCT FROM r.k LIMIT 0) +ORDER BY 1, 2; + id | id +----+---- +(0 rows) + +-- Keep every unused SubPlan when the discarded projection owns several. +-- NULL LIMIT/OFFSET are neutral; matching NULL keys must remain visible. +SET optimizer = off; +SELECT l.id, r.id +FROM motion_l l RIGHT JOIN motion_r r ON l.id = r.lid +WHERE EXISTS (SELECT (SELECT count(*) FROM motion_s), + (SELECT max(id) FROM motion_s) + FROM motion_s s WHERE s.k IS NOT DISTINCT FROM r.k + LIMIT NULL OFFSET NULL) +ORDER BY 1, 2; + id | id +----+---- + 1 | 10 + 1 | 11 + 3 | 12 + | 13 +(4 rows) + +SET optimizer = on; +SELECT l.id, r.id +FROM motion_l l RIGHT JOIN motion_r r ON l.id = r.lid +WHERE EXISTS (SELECT (SELECT count(*) FROM motion_s), + (SELECT max(id) FROM motion_s) + FROM motion_s s WHERE s.k IS NOT DISTINCT FROM r.k + LIMIT NULL OFFSET NULL) +ORDER BY 1, 2; + id | id +----+---- + 1 | 10 + 1 | 11 + 3 | 12 + | 13 +(4 rows) + +DROP TABLE motion_l, motion_r, motion_s; +RESET optimizer; diff --git a/src/test/regress/greenplum_schedule b/src/test/regress/greenplum_schedule index 882af76f9a5..01ea9ceb9d9 100755 --- a/src/test/regress/greenplum_schedule +++ b/src/test/regress/greenplum_schedule @@ -214,6 +214,7 @@ test: rpt rpt_joins rpt_tpch rpt_returning # temp tables test: bfv_cte test: bfv_joins bfv_subquery bfv_planner bfv_legacy bfv_temp bfv_dml +test: orca_motion_subplan # test tpcds query 04 test: tpcds_q04 diff --git a/src/test/regress/sql/orca_motion_subplan.sql b/src/test/regress/sql/orca_motion_subplan.sql new file mode 100644 index 00000000000..81204e3f11c --- /dev/null +++ b/src/test/regress/sql/orca_motion_subplan.sql @@ -0,0 +1,102 @@ +-- Result projection folding must not orphan a SubPlan's Motion slices. +SET optimizer = on; +CREATE TEMP TABLE motion_l(id int) DISTRIBUTED BY (id); +CREATE TEMP TABLE motion_r(id int, lid int, k int) DISTRIBUTED BY (id); +CREATE TEMP TABLE motion_s(id int, k int) DISTRIBUTED BY (id); +INSERT INTO motion_l VALUES (1), (2), (3), (4); +INSERT INTO motion_r VALUES (10,1,7), (11,1,NULL), (12,3,8), (13,5,NULL); +INSERT INTO motion_s VALUES (100,7), (101,7), (102,8), (103,NULL), (104,9); +ANALYZE motion_l; +ANALYZE motion_r; +ANALYZE motion_s; + +-- The count SubPlan is unused by EXISTS, but its slice has already been +-- allocated. EXPLAIN must still reach it after Result projection folding. +EXPLAIN (COSTS OFF) +SELECT l.id, r.id +FROM motion_l l LEFT JOIN motion_r r ON l.id = r.lid +WHERE EXISTS (SELECT (SELECT count(*) FROM motion_s) + FROM motion_s s WHERE s.k IS NOT DISTINCT FROM r.k LIMIT 1); + +SET optimizer = off; +SELECT l.id, r.id +FROM motion_l l LEFT JOIN motion_r r ON l.id = r.lid +WHERE EXISTS (SELECT (SELECT count(*) FROM motion_s) + FROM motion_s s WHERE s.k IS NOT DISTINCT FROM r.k LIMIT 1) +ORDER BY 1, 2; + +SET optimizer = on; +SELECT l.id, r.id +FROM motion_l l LEFT JOIN motion_r r ON l.id = r.lid +WHERE EXISTS (SELECT (SELECT count(*) FROM motion_s) + FROM motion_s s WHERE s.k IS NOT DISTINCT FROM r.k LIMIT 1) +ORDER BY 1, 2; + +-- NOT EXISTS and a positive OFFSET: only k = 7 has a second matching row. +-- Include both real NULL keys and NULL-extended rows from a FULL JOIN. +SET optimizer = off; +SELECT l.id, r.id +FROM motion_l l FULL JOIN motion_r r ON l.id = r.lid +WHERE NOT EXISTS (SELECT (SELECT count(*) FROM motion_s) + FROM motion_s s WHERE s.k = r.k LIMIT 1 OFFSET 1) +ORDER BY 1, 2; + +SET optimizer = on; +SELECT l.id, r.id +FROM motion_l l FULL JOIN motion_r r ON l.id = r.lid +WHERE NOT EXISTS (SELECT (SELECT count(*) FROM motion_s) + FROM motion_s s WHERE s.k = r.k LIMIT 1 OFFSET 1) +ORDER BY 1, 2; + +-- A positive OFFSET must also preserve a nonempty EXISTS result. +SET optimizer = off; +SELECT l.id, r.id +FROM motion_l l FULL JOIN motion_r r ON l.id = r.lid +WHERE EXISTS (SELECT (SELECT count(*) FROM motion_s) + FROM motion_s s WHERE r.id IS NULL LIMIT 2 OFFSET 2) +ORDER BY 1, 2; + +SET optimizer = on; +SELECT l.id, r.id +FROM motion_l l FULL JOIN motion_r r ON l.id = r.lid +WHERE EXISTS (SELECT (SELECT count(*) FROM motion_s) + FROM motion_s s WHERE r.id IS NULL LIMIT 2 OFFSET 2) +ORDER BY 1, 2; + +-- LIMIT 0 still makes EXISTS false, even with a scalar aggregate target. +SET optimizer = off; +SELECT l.id, r.id +FROM motion_l l LEFT JOIN motion_r r ON l.id = r.lid +WHERE EXISTS (SELECT (SELECT count(*) FROM motion_s) + FROM motion_s s WHERE s.k IS NOT DISTINCT FROM r.k LIMIT 0) +ORDER BY 1, 2; + +SET optimizer = on; +SELECT l.id, r.id +FROM motion_l l LEFT JOIN motion_r r ON l.id = r.lid +WHERE EXISTS (SELECT (SELECT count(*) FROM motion_s) + FROM motion_s s WHERE s.k IS NOT DISTINCT FROM r.k LIMIT 0) +ORDER BY 1, 2; + +-- Keep every unused SubPlan when the discarded projection owns several. +-- NULL LIMIT/OFFSET are neutral; matching NULL keys must remain visible. +SET optimizer = off; +SELECT l.id, r.id +FROM motion_l l RIGHT JOIN motion_r r ON l.id = r.lid +WHERE EXISTS (SELECT (SELECT count(*) FROM motion_s), + (SELECT max(id) FROM motion_s) + FROM motion_s s WHERE s.k IS NOT DISTINCT FROM r.k + LIMIT NULL OFFSET NULL) +ORDER BY 1, 2; + +SET optimizer = on; +SELECT l.id, r.id +FROM motion_l l RIGHT JOIN motion_r r ON l.id = r.lid +WHERE EXISTS (SELECT (SELECT count(*) FROM motion_s), + (SELECT max(id) FROM motion_s) + FROM motion_s s WHERE s.k IS NOT DISTINCT FROM r.k + LIMIT NULL OFFSET NULL) +ORDER BY 1, 2; + +DROP TABLE motion_l, motion_r, motion_s; +RESET optimizer; From c88b7ad46408b0c87d8ebd1337361089e35ae1d0 Mon Sep 17 00:00:00 2001 From: Alena Rybakina Date: Fri, 30 Oct 2026 18:59:36 +0300 Subject: [PATCH 2/2] Avoid scalar correlated Apply for multirow EXISTS inputs An outer Apply introduced below grouping for an existential or quantified subquery can have a multirow inner input. If decorrelation fails, implementing it as a scalar SubPlan raises a spurious cardinality violation before the grouping can run. Only generate that scalar correlated alternative when the inner input is proven to return at most one row. Keep the correlated alternatives generated from the original subquery. Add regression coverage for nullable FULL JOIN correlations, LIMIT/OFFSET, quantified subqueries, and genuine scalar cardinality errors. --- .../include/gpopt/xforms/CXformApply2Join.h | 15 + .../expected/orca_exists_cardinality.out | 296 ++++++++++++++++++ src/test/regress/greenplum_schedule | 2 +- .../regress/sql/orca_exists_cardinality.sql | 156 +++++++++ 4 files changed, 468 insertions(+), 1 deletion(-) create mode 100644 src/test/regress/expected/orca_exists_cardinality.out create mode 100644 src/test/regress/sql/orca_exists_cardinality.sql diff --git a/src/backend/gporca/libgpopt/include/gpopt/xforms/CXformApply2Join.h b/src/backend/gporca/libgpopt/include/gpopt/xforms/CXformApply2Join.h index 8243e0657e5..3bae78ec3e8 100644 --- a/src/backend/gporca/libgpopt/include/gpopt/xforms/CXformApply2Join.h +++ b/src/backend/gporca/libgpopt/include/gpopt/xforms/CXformApply2Join.h @@ -49,6 +49,21 @@ class CXformApply2Join : public CXformExploration COperator::EOperatorId op_id = pexprApply->Pop()->Eopid(); + // An outer Apply introduced below a grouping operation for EXISTS or + // a quantified subquery can have a multi-row inner input. If + // decorrelation fails, it cannot be implemented as a scalar SubPlan: + // the grouping above it does not enforce scalar cardinality below it. + // Keep scalar correlated execution only when the inner input is + // proven to return at most one row. SubqueryUnnest also generates a + // correlated alternative directly from the original subquery. + if (COperator::EopLogicalLeftOuterApply == op_id && + COperator::EopScalarSubquery == + CLogicalApply::PopConvert(pexprApply->Pop())->EopidOriginSubq() && + 1 < (*pexprApply)[1]->DeriveMaxCard().Ull()) + { + return false; + } + // consider only Inner/Outer/Left (Anti) Semi Apply here, // correlated left anti semi apply (with ALL/NOT-IN semantics) can only be generated by SubqueryHandler return COperator::EopLogicalInnerApply == op_id || diff --git a/src/test/regress/expected/orca_exists_cardinality.out b/src/test/regress/expected/orca_exists_cardinality.out new file mode 100644 index 00000000000..dbe43854254 --- /dev/null +++ b/src/test/regress/expected/orca_exists_cardinality.out @@ -0,0 +1,296 @@ +-- An EXISTS input can return many rows even when its target is a scalar +-- aggregate subquery. Failed decorrelation must not make that input scalar. +SET optimizer = on; +CREATE TEMP TABLE card_l(id int) DISTRIBUTED BY (id); +CREATE TEMP TABLE card_r(id int, lid int, k int) DISTRIBUTED BY (id); +CREATE TEMP TABLE card_s(id int, k int) DISTRIBUTED BY (id); +INSERT INTO card_l VALUES (1), (2), (3), (4); +INSERT INTO card_r VALUES (10,1,7), (11,1,NULL), (12,3,8), (13,5,NULL); +INSERT INTO card_s VALUES (100,7), (101,7), (102,8), (103,NULL), (104,9); +ANALYZE card_l; +ANALYZE card_r; +ANALYZE card_s; +-- The FULL JOIN includes actual NULLs and NULL-extended rows on both sides. +-- There are two inner matches for k = 7. NULL LIMIT/OFFSET are neutral. +EXPLAIN (COSTS OFF) +SELECT l.id, r.id, + EXISTS (SELECT (SELECT count(*) FROM card_s) + FROM card_s s WHERE s.k = r.k LIMIT NULL OFFSET NULL) +FROM card_l l FULL JOIN card_r r ON l.id = r.lid; + QUERY PLAN +-------------------------------------------------------------------------------- + Result + -> Gather Motion 3:1 (slice1; segments: 3) + -> Hash Full Join + Hash Cond: (r.lid = l.id) + -> Redistribute Motion 3:3 (slice2; segments: 3) + Hash Key: r.lid + -> Seq Scan on card_r r + -> Hash + -> Seq Scan on card_l l + SubPlan 2 + -> Result + -> Result + -> Limit + -> Result + Filter: (s.k = r.k) + -> Materialize + -> Gather Motion 3:1 (slice3; segments: 3) + -> Seq Scan on card_s s + SubPlan 1 + -> Aggregate + -> Materialize + -> Gather Motion 3:1 (slice4; segments: 3) + -> Seq Scan on card_s + Optimizer: GPORCA +(24 rows) + +SET optimizer = off; +SELECT l.id, r.id, + EXISTS (SELECT (SELECT count(*) FROM card_s) + FROM card_s s WHERE s.k = r.k LIMIT NULL OFFSET NULL) +FROM card_l l FULL JOIN card_r r ON l.id = r.lid +ORDER BY 1, 2; + id | id | exists +----+----+-------- + 1 | 10 | t + 1 | 11 | f + 2 | | f + 3 | 12 | t + 4 | | f + | 13 | f +(6 rows) + +SET optimizer = on; +SELECT l.id, r.id, + EXISTS (SELECT (SELECT count(*) FROM card_s) + FROM card_s s WHERE s.k = r.k LIMIT NULL OFFSET NULL) +FROM card_l l FULL JOIN card_r r ON l.id = r.lid +ORDER BY 1, 2; + id | id | exists +----+----+-------- + 1 | 10 | t + 1 | 11 | f + 2 | | f + 3 | 12 | t + 4 | | f + | 13 | f +(6 rows) + +SET optimizer = off; +SELECT l.id, r.id, + EXISTS (SELECT (SELECT count(*) FROM card_s) + FROM card_s s WHERE s.k IS NOT DISTINCT FROM r.k + LIMIT NULL OFFSET NULL) +FROM card_l l FULL JOIN card_r r ON l.id = r.lid +ORDER BY 1, 2; + id | id | exists +----+----+-------- + 1 | 10 | t + 1 | 11 | t + 2 | | t + 3 | 12 | t + 4 | | t + | 13 | t +(6 rows) + +SET optimizer = on; +SELECT l.id, r.id, + EXISTS (SELECT (SELECT count(*) FROM card_s) + FROM card_s s WHERE s.k IS NOT DISTINCT FROM r.k + LIMIT NULL OFFSET NULL) +FROM card_l l FULL JOIN card_r r ON l.id = r.lid +ORDER BY 1, 2; + id | id | exists +----+----+-------- + 1 | 10 | t + 1 | 11 | t + 2 | | t + 3 | 12 | t + 4 | | t + | 13 | t +(6 rows) + +-- A nullable outer reference can select all five inner rows. After OFFSET +-- there are still two rows, which is valid for EXISTS and NOT EXISTS. +SET optimizer = off; +SELECT l.id, r.id, + EXISTS (SELECT (SELECT count(*) FROM card_s) + FROM card_s s WHERE r.id IS NULL LIMIT 2 OFFSET 2), + NOT EXISTS (SELECT (SELECT count(*) FROM card_s) + FROM card_s s WHERE r.id IS NULL LIMIT 2 OFFSET 2) +FROM card_l l FULL JOIN card_r r ON l.id = r.lid +ORDER BY 1, 2; + id | id | exists | ?column? +----+----+--------+---------- + 1 | 10 | f | t + 1 | 11 | f | t + 2 | | t | f + 3 | 12 | f | t + 4 | | t | f + | 13 | f | t +(6 rows) + +SET optimizer = on; +SELECT l.id, r.id, + EXISTS (SELECT (SELECT count(*) FROM card_s) + FROM card_s s WHERE r.id IS NULL LIMIT 2 OFFSET 2), + NOT EXISTS (SELECT (SELECT count(*) FROM card_s) + FROM card_s s WHERE r.id IS NULL LIMIT 2 OFFSET 2) +FROM card_l l FULL JOIN card_r r ON l.id = r.lid +ORDER BY 1, 2; + id | id | exists | ?column? +----+----+--------+---------- + 1 | 10 | f | t + 1 | 11 | f | t + 2 | | t | f + 3 | 12 | f | t + 4 | | t | f + | 13 | f | t +(6 rows) + +-- Preserve empty inputs and the boundary where OFFSET removes the last row. +SET optimizer = off; +SELECT l.id, r.id, + EXISTS (SELECT (SELECT count(*) FROM card_s) + FROM card_s s WHERE s.k = r.k LIMIT 0), + EXISTS (SELECT (SELECT count(*) FROM card_s) + FROM card_s s WHERE s.k = r.k LIMIT 1 OFFSET 1) +FROM card_l l FULL JOIN card_r r ON l.id = r.lid +ORDER BY 1, 2; + id | id | exists | exists +----+----+--------+-------- + 1 | 10 | f | t + 1 | 11 | f | f + 2 | | f | f + 3 | 12 | f | f + 4 | | f | f + | 13 | f | f +(6 rows) + +SET optimizer = on; +SELECT l.id, r.id, + EXISTS (SELECT (SELECT count(*) FROM card_s) + FROM card_s s WHERE s.k = r.k LIMIT 0), + EXISTS (SELECT (SELECT count(*) FROM card_s) + FROM card_s s WHERE s.k = r.k LIMIT 1 OFFSET 1) +FROM card_l l FULL JOIN card_r r ON l.id = r.lid +ORDER BY 1, 2; + id | id | exists | exists +----+----+--------+-------- + 1 | 10 | f | t + 1 | 11 | f | f + 2 | | f | f + 3 | 12 | f | f + 4 | | f | f + | 13 | f | f +(6 rows) + +-- Quantified subqueries share the outer-Apply transformation. Preserve +-- their multirow behavior, including UNKNOWN for the matching NULL key. +SET optimizer = off; +SELECT r.id, + r.k = ANY (SELECT s.k FROM card_s s + WHERE s.k IS NOT DISTINCT FROM r.k LIMIT NULL OFFSET NULL), + r.k = ALL (SELECT s.k FROM card_s s + WHERE s.k IS NOT DISTINCT FROM r.k LIMIT NULL OFFSET NULL) +FROM card_r r ORDER BY 1; + id | ?column? | ?column? +----+----------+---------- + 10 | t | t + 11 | | + 12 | t | t + 13 | | +(4 rows) + +SET optimizer = on; +SELECT r.id, + r.k = ANY (SELECT s.k FROM card_s s + WHERE s.k IS NOT DISTINCT FROM r.k LIMIT NULL OFFSET NULL), + r.k = ALL (SELECT s.k FROM card_s s + WHERE s.k IS NOT DISTINCT FROM r.k LIMIT NULL OFFSET NULL) +FROM card_r r ORDER BY 1; + id | ?column? | ?column? +----+----------+---------- + 10 | t | t + 11 | | + 12 | t | t + 13 | | +(4 rows) + +-- Genuine scalar subqueries with at most one row remain valid, including +-- an aggregate over empty input and a correlated top-1 query. +SET optimizer = off; +SELECT r.id, + (SELECT count(*) FROM card_s s WHERE s.k = r.k), + (SELECT s.id FROM card_s s WHERE s.k = r.k ORDER BY s.id LIMIT 1) +FROM card_r r ORDER BY 1; + id | count | id +----+-------+----- + 10 | 2 | 100 + 11 | 0 | + 12 | 1 | 102 + 13 | 0 | +(4 rows) + +SET optimizer = on; +SELECT r.id, + (SELECT count(*) FROM card_s s WHERE s.k = r.k), + (SELECT s.id FROM card_s s WHERE s.k = r.k ORDER BY s.id LIMIT 1) +FROM card_r r ORDER BY 1; + id | count | id +----+-------+----- + 10 | 2 | 100 + 11 | 0 | + 12 | 1 | 102 + 13 | 0 | +(4 rows) + +-- An aggregate at the EXISTS level still produces one row for empty input. +SET optimizer = off; +SELECT r.id, EXISTS (SELECT count(*) FROM card_s s WHERE s.k = -1) +FROM card_r r ORDER BY 1; + id | exists +----+-------- + 10 | t + 11 | t + 12 | t + 13 | t +(4 rows) + +SET optimizer = on; +SELECT r.id, EXISTS (SELECT count(*) FROM card_s s WHERE s.k = -1) +FROM card_r r ORDER BY 1; + id | exists +----+-------- + 10 | t + 11 | t + 12 | t + 13 | t +(4 rows) + +-- Do not suppress a real scalar cardinality violation. +SET optimizer = off; +DO $$ +BEGIN + PERFORM (SELECT s.id FROM card_s s WHERE s.k = r.k LIMIT 2) + FROM card_r r WHERE r.id = 10; + RAISE EXCEPTION 'missing scalar cardinality violation'; +EXCEPTION WHEN cardinality_violation THEN + RAISE NOTICE 'expected scalar cardinality violation'; +END +$$; +NOTICE: expected scalar cardinality violation +SET optimizer = on; +DO $$ +BEGIN + PERFORM (SELECT s.id FROM card_s s WHERE s.k = r.k LIMIT 2) + FROM card_r r WHERE r.id = 10; + RAISE EXCEPTION 'missing scalar cardinality violation'; +EXCEPTION WHEN cardinality_violation THEN + RAISE NOTICE 'expected scalar cardinality violation'; +END +$$; +NOTICE: expected scalar cardinality violation +DROP TABLE card_l, card_r, card_s; +RESET optimizer; diff --git a/src/test/regress/greenplum_schedule b/src/test/regress/greenplum_schedule index 01ea9ceb9d9..27eac1319fb 100755 --- a/src/test/regress/greenplum_schedule +++ b/src/test/regress/greenplum_schedule @@ -214,7 +214,7 @@ test: rpt rpt_joins rpt_tpch rpt_returning # temp tables test: bfv_cte test: bfv_joins bfv_subquery bfv_planner bfv_legacy bfv_temp bfv_dml -test: orca_motion_subplan +test: orca_motion_subplan orca_exists_cardinality # test tpcds query 04 test: tpcds_q04 diff --git a/src/test/regress/sql/orca_exists_cardinality.sql b/src/test/regress/sql/orca_exists_cardinality.sql new file mode 100644 index 00000000000..fbd039d4eca --- /dev/null +++ b/src/test/regress/sql/orca_exists_cardinality.sql @@ -0,0 +1,156 @@ +-- An EXISTS input can return many rows even when its target is a scalar +-- aggregate subquery. Failed decorrelation must not make that input scalar. +SET optimizer = on; +CREATE TEMP TABLE card_l(id int) DISTRIBUTED BY (id); +CREATE TEMP TABLE card_r(id int, lid int, k int) DISTRIBUTED BY (id); +CREATE TEMP TABLE card_s(id int, k int) DISTRIBUTED BY (id); +INSERT INTO card_l VALUES (1), (2), (3), (4); +INSERT INTO card_r VALUES (10,1,7), (11,1,NULL), (12,3,8), (13,5,NULL); +INSERT INTO card_s VALUES (100,7), (101,7), (102,8), (103,NULL), (104,9); +ANALYZE card_l; +ANALYZE card_r; +ANALYZE card_s; + +-- The FULL JOIN includes actual NULLs and NULL-extended rows on both sides. +-- There are two inner matches for k = 7. NULL LIMIT/OFFSET are neutral. +EXPLAIN (COSTS OFF) +SELECT l.id, r.id, + EXISTS (SELECT (SELECT count(*) FROM card_s) + FROM card_s s WHERE s.k = r.k LIMIT NULL OFFSET NULL) +FROM card_l l FULL JOIN card_r r ON l.id = r.lid; + +SET optimizer = off; +SELECT l.id, r.id, + EXISTS (SELECT (SELECT count(*) FROM card_s) + FROM card_s s WHERE s.k = r.k LIMIT NULL OFFSET NULL) +FROM card_l l FULL JOIN card_r r ON l.id = r.lid +ORDER BY 1, 2; + +SET optimizer = on; +SELECT l.id, r.id, + EXISTS (SELECT (SELECT count(*) FROM card_s) + FROM card_s s WHERE s.k = r.k LIMIT NULL OFFSET NULL) +FROM card_l l FULL JOIN card_r r ON l.id = r.lid +ORDER BY 1, 2; + +SET optimizer = off; +SELECT l.id, r.id, + EXISTS (SELECT (SELECT count(*) FROM card_s) + FROM card_s s WHERE s.k IS NOT DISTINCT FROM r.k + LIMIT NULL OFFSET NULL) +FROM card_l l FULL JOIN card_r r ON l.id = r.lid +ORDER BY 1, 2; + +SET optimizer = on; +SELECT l.id, r.id, + EXISTS (SELECT (SELECT count(*) FROM card_s) + FROM card_s s WHERE s.k IS NOT DISTINCT FROM r.k + LIMIT NULL OFFSET NULL) +FROM card_l l FULL JOIN card_r r ON l.id = r.lid +ORDER BY 1, 2; + +-- A nullable outer reference can select all five inner rows. After OFFSET +-- there are still two rows, which is valid for EXISTS and NOT EXISTS. +SET optimizer = off; +SELECT l.id, r.id, + EXISTS (SELECT (SELECT count(*) FROM card_s) + FROM card_s s WHERE r.id IS NULL LIMIT 2 OFFSET 2), + NOT EXISTS (SELECT (SELECT count(*) FROM card_s) + FROM card_s s WHERE r.id IS NULL LIMIT 2 OFFSET 2) +FROM card_l l FULL JOIN card_r r ON l.id = r.lid +ORDER BY 1, 2; + +SET optimizer = on; +SELECT l.id, r.id, + EXISTS (SELECT (SELECT count(*) FROM card_s) + FROM card_s s WHERE r.id IS NULL LIMIT 2 OFFSET 2), + NOT EXISTS (SELECT (SELECT count(*) FROM card_s) + FROM card_s s WHERE r.id IS NULL LIMIT 2 OFFSET 2) +FROM card_l l FULL JOIN card_r r ON l.id = r.lid +ORDER BY 1, 2; + +-- Preserve empty inputs and the boundary where OFFSET removes the last row. +SET optimizer = off; +SELECT l.id, r.id, + EXISTS (SELECT (SELECT count(*) FROM card_s) + FROM card_s s WHERE s.k = r.k LIMIT 0), + EXISTS (SELECT (SELECT count(*) FROM card_s) + FROM card_s s WHERE s.k = r.k LIMIT 1 OFFSET 1) +FROM card_l l FULL JOIN card_r r ON l.id = r.lid +ORDER BY 1, 2; + +SET optimizer = on; +SELECT l.id, r.id, + EXISTS (SELECT (SELECT count(*) FROM card_s) + FROM card_s s WHERE s.k = r.k LIMIT 0), + EXISTS (SELECT (SELECT count(*) FROM card_s) + FROM card_s s WHERE s.k = r.k LIMIT 1 OFFSET 1) +FROM card_l l FULL JOIN card_r r ON l.id = r.lid +ORDER BY 1, 2; + +-- Quantified subqueries share the outer-Apply transformation. Preserve +-- their multirow behavior, including UNKNOWN for the matching NULL key. +SET optimizer = off; +SELECT r.id, + r.k = ANY (SELECT s.k FROM card_s s + WHERE s.k IS NOT DISTINCT FROM r.k LIMIT NULL OFFSET NULL), + r.k = ALL (SELECT s.k FROM card_s s + WHERE s.k IS NOT DISTINCT FROM r.k LIMIT NULL OFFSET NULL) +FROM card_r r ORDER BY 1; + +SET optimizer = on; +SELECT r.id, + r.k = ANY (SELECT s.k FROM card_s s + WHERE s.k IS NOT DISTINCT FROM r.k LIMIT NULL OFFSET NULL), + r.k = ALL (SELECT s.k FROM card_s s + WHERE s.k IS NOT DISTINCT FROM r.k LIMIT NULL OFFSET NULL) +FROM card_r r ORDER BY 1; + +-- Genuine scalar subqueries with at most one row remain valid, including +-- an aggregate over empty input and a correlated top-1 query. +SET optimizer = off; +SELECT r.id, + (SELECT count(*) FROM card_s s WHERE s.k = r.k), + (SELECT s.id FROM card_s s WHERE s.k = r.k ORDER BY s.id LIMIT 1) +FROM card_r r ORDER BY 1; + +SET optimizer = on; +SELECT r.id, + (SELECT count(*) FROM card_s s WHERE s.k = r.k), + (SELECT s.id FROM card_s s WHERE s.k = r.k ORDER BY s.id LIMIT 1) +FROM card_r r ORDER BY 1; + +-- An aggregate at the EXISTS level still produces one row for empty input. +SET optimizer = off; +SELECT r.id, EXISTS (SELECT count(*) FROM card_s s WHERE s.k = -1) +FROM card_r r ORDER BY 1; + +SET optimizer = on; +SELECT r.id, EXISTS (SELECT count(*) FROM card_s s WHERE s.k = -1) +FROM card_r r ORDER BY 1; + +-- Do not suppress a real scalar cardinality violation. +SET optimizer = off; +DO $$ +BEGIN + PERFORM (SELECT s.id FROM card_s s WHERE s.k = r.k LIMIT 2) + FROM card_r r WHERE r.id = 10; + RAISE EXCEPTION 'missing scalar cardinality violation'; +EXCEPTION WHEN cardinality_violation THEN + RAISE NOTICE 'expected scalar cardinality violation'; +END +$$; + +SET optimizer = on; +DO $$ +BEGIN + PERFORM (SELECT s.id FROM card_s s WHERE s.k = r.k LIMIT 2) + FROM card_r r WHERE r.id = 10; + RAISE EXCEPTION 'missing scalar cardinality violation'; +EXCEPTION WHEN cardinality_violation THEN + RAISE NOTICE 'expected scalar cardinality violation'; +END +$$; + +DROP TABLE card_l, card_r, card_s; +RESET optimizer;