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/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_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/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..27eac1319fb 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 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; 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;