From 6bf7f9f5d1c8d42ec09c68e75e6aefb4c1bd1734 Mon Sep 17 00:00:00 2001 From: Alena Rybakina Date: Tue, 6 Oct 2026 14:15:12 +0300 Subject: [PATCH 1/2] Honor forceDistRandom when pulling up UNION ALL subqueries The UNION ALL pull-up path does not respect forceDistRandom, unlike ordinary subquery pull-up. This can bypass the planning logic that enforces execution on segments and produce incorrect results. Prevent UNION ALL pull-up when forceDistRandom is set, preserving the required execution location through the existing subquery planning path. --- src/backend/optimizer/prep/prepjointree.c | 4 +- src/test/regress/expected/gpdist.out | 50 +++++++++++++++++++ .../regress/expected/gpdist_optimizer.out | 50 +++++++++++++++++++ src/test/regress/sql/gpdist.sql | 30 +++++++++++ 4 files changed, 133 insertions(+), 1 deletion(-) diff --git a/src/backend/optimizer/prep/prepjointree.c b/src/backend/optimizer/prep/prepjointree.c index 3b9b4f39bb0..dfea14bcfbe 100644 --- a/src/backend/optimizer/prep/prepjointree.c +++ b/src/backend/optimizer/prep/prepjointree.c @@ -934,7 +934,8 @@ pull_up_subqueries_recurse(PlannerInfo *root, Node *jtnode, /* * Alternatively, is it a simple UNION ALL subquery? If so, flatten - * into an "append relation". + * into an "append relation". As above, keep gp_dist_random subqueries + * intact so set_subquery_pathlist can enforce segment execution. * * It's safe to do this regardless of whether this query is itself an * appendrel member. (If you're thinking we should try to flatten the @@ -942,6 +943,7 @@ pull_up_subqueries_recurse(PlannerInfo *root, Node *jtnode, * in set_append_rel_pathlist, not here.) */ if (rte->rtekind == RTE_SUBQUERY && + !rte->forceDistRandom && is_simple_union_all(rte->subquery)) return pull_up_simple_union_all(root, jtnode, rte); diff --git a/src/test/regress/expected/gpdist.out b/src/test/regress/expected/gpdist.out index dbe017b1f59..d86857eb040 100644 --- a/src/test/regress/expected/gpdist.out +++ b/src/test/regress/expected/gpdist.out @@ -771,3 +771,53 @@ SELECT * FROM gp_dist_random('"gp.dist.random.schema".gp_dist_random_table_with_ DROP SCHEMA "gp.dist.random.schema" CASCADE; NOTICE: drop cascades to table "gp.dist.random.schema".gp_dist_random_table_with_schema +-- gp_dist_random must preserve segment execution when a view is a UNION ALL. +BEGIN; +SET LOCAL optimizer = off; +CREATE TEMP VIEW gdr_union AS + SELECT gp_execution_segment() AS seg, 1 AS branch FROM gp_id + UNION ALL + SELECT gp_execution_segment(), 2 FROM gp_id; +SELECT branch, + bool_and(seg >= 0) AS on_segments, + count(*) = (SELECT count(*) FROM gp_segment_configuration + WHERE role = 'p' AND content >= 0) + AND count(*) = count(DISTINCT seg) AS once_per_segment + FROM gp_dist_random('gdr_union') + GROUP BY branch + ORDER BY branch; + branch | on_segments | once_per_segment +--------+-------------+------------------ + 1 | t | t + 2 | t | t +(2 rows) + +-- Ordinary access still executes on the coordinator. +SELECT count(*) = 2 AS two_rows, bool_and(seg = -1) AS on_coordinator + FROM gdr_union; + two_rows | on_coordinator +----------+---------------- + t | t +(1 row) + +-- A nested view with a filter must also execute on the segments. +CREATE TEMP VIEW gdr_union_filtered AS + SELECT * FROM gdr_union WHERE branch = 2; +SELECT bool_and(seg >= 0) AS on_segments, + count(*) = (SELECT count(*) FROM gp_segment_configuration + WHERE role = 'p' AND content >= 0) + AND count(*) = count(DISTINCT seg) AS once_per_segment + FROM gp_dist_random('gdr_union_filtered'); + on_segments | once_per_segment +-------------+------------------ + t | t +(1 row) + +-- An impossible filter must remain empty. +SELECT count(*) FROM gp_dist_random('gdr_union') WHERE branch = 3; + count +------- + 0 +(1 row) + +ROLLBACK; diff --git a/src/test/regress/expected/gpdist_optimizer.out b/src/test/regress/expected/gpdist_optimizer.out index e50698247f4..9025dba258e 100644 --- a/src/test/regress/expected/gpdist_optimizer.out +++ b/src/test/regress/expected/gpdist_optimizer.out @@ -775,3 +775,53 @@ SELECT * FROM gp_dist_random('"gp.dist.random.schema".gp_dist_random_table_with_ DROP SCHEMA "gp.dist.random.schema" CASCADE; NOTICE: drop cascades to table "gp.dist.random.schema".gp_dist_random_table_with_schema +-- gp_dist_random must preserve segment execution when a view is a UNION ALL. +BEGIN; +SET LOCAL optimizer = off; +CREATE TEMP VIEW gdr_union AS + SELECT gp_execution_segment() AS seg, 1 AS branch FROM gp_id + UNION ALL + SELECT gp_execution_segment(), 2 FROM gp_id; +SELECT branch, + bool_and(seg >= 0) AS on_segments, + count(*) = (SELECT count(*) FROM gp_segment_configuration + WHERE role = 'p' AND content >= 0) + AND count(*) = count(DISTINCT seg) AS once_per_segment + FROM gp_dist_random('gdr_union') + GROUP BY branch + ORDER BY branch; + branch | on_segments | once_per_segment +--------+-------------+------------------ + 1 | t | t + 2 | t | t +(2 rows) + +-- Ordinary access still executes on the coordinator. +SELECT count(*) = 2 AS two_rows, bool_and(seg = -1) AS on_coordinator + FROM gdr_union; + two_rows | on_coordinator +----------+---------------- + t | t +(1 row) + +-- A nested view with a filter must also execute on the segments. +CREATE TEMP VIEW gdr_union_filtered AS + SELECT * FROM gdr_union WHERE branch = 2; +SELECT bool_and(seg >= 0) AS on_segments, + count(*) = (SELECT count(*) FROM gp_segment_configuration + WHERE role = 'p' AND content >= 0) + AND count(*) = count(DISTINCT seg) AS once_per_segment + FROM gp_dist_random('gdr_union_filtered'); + on_segments | once_per_segment +-------------+------------------ + t | t +(1 row) + +-- An impossible filter must remain empty. +SELECT count(*) FROM gp_dist_random('gdr_union') WHERE branch = 3; + count +------- + 0 +(1 row) + +ROLLBACK; diff --git a/src/test/regress/sql/gpdist.sql b/src/test/regress/sql/gpdist.sql index 5465ebce0a5..a0ce5e66bb4 100644 --- a/src/test/regress/sql/gpdist.sql +++ b/src/test/regress/sql/gpdist.sql @@ -535,3 +535,33 @@ CREATE TABLE "gp.dist.random.schema".gp_dist_random_table_with_schema AS SELECT * FROM gp_dist_random('"gp_dist_random_table"'); SELECT * FROM gp_dist_random('"gp.dist.random.schema".gp_dist_random_table_with_schema'); DROP SCHEMA "gp.dist.random.schema" CASCADE; + +-- gp_dist_random must preserve segment execution when a view is a UNION ALL. +BEGIN; +SET LOCAL optimizer = off; +CREATE TEMP VIEW gdr_union AS + SELECT gp_execution_segment() AS seg, 1 AS branch FROM gp_id + UNION ALL + SELECT gp_execution_segment(), 2 FROM gp_id; +SELECT branch, + bool_and(seg >= 0) AS on_segments, + count(*) = (SELECT count(*) FROM gp_segment_configuration + WHERE role = 'p' AND content >= 0) + AND count(*) = count(DISTINCT seg) AS once_per_segment + FROM gp_dist_random('gdr_union') + GROUP BY branch + ORDER BY branch; +-- Ordinary access still executes on the coordinator. +SELECT count(*) = 2 AS two_rows, bool_and(seg = -1) AS on_coordinator + FROM gdr_union; +-- A nested view with a filter must also execute on the segments. +CREATE TEMP VIEW gdr_union_filtered AS + SELECT * FROM gdr_union WHERE branch = 2; +SELECT bool_and(seg >= 0) AS on_segments, + count(*) = (SELECT count(*) FROM gp_segment_configuration + WHERE role = 'p' AND content >= 0) + AND count(*) = count(DISTINCT seg) AS once_per_segment + FROM gp_dist_random('gdr_union_filtered'); +-- An impossible filter must remain empty. +SELECT count(*) FROM gp_dist_random('gdr_union') WHERE branch = 3; +ROLLBACK; From 9ff840e2cf93abbf5d03ea75f1a1484b310c3069 Mon Sep 17 00:00:00 2001 From: Alena Rybakina Date: Tue, 6 Oct 2026 14:33:53 +0300 Subject: [PATCH 2/2] Compare forceDistRandom when checking range table entry equality RangeTblEntry equality ignores forceDistRandom, allowing ordinary and forced-distributed scans to compare equal. OR simplification can then eliminate a semantically different EXISTS subquery and produce a result that depends on operand order. Compare forceDistRandom in _equalRangeTblEntry() so expression equality preserves the required execution location. Add regression coverage for ordinary and distributed EXISTS expressions, both OR orders, and AND controls. --- src/backend/nodes/equalfuncs.c | 1 + src/test/regress/expected/gpdist.out | 46 +++++++++++++++++++ .../regress/expected/gpdist_optimizer.out | 46 +++++++++++++++++++ src/test/regress/sql/gpdist.sql | 24 ++++++++++ 4 files changed, 117 insertions(+) diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c index 9981c2fd023..78713eefd2d 100644 --- a/src/backend/nodes/equalfuncs.c +++ b/src/backend/nodes/equalfuncs.c @@ -3177,6 +3177,7 @@ _equalRangeTblEntry(const RangeTblEntry *a, const RangeTblEntry *b) COMPARE_BITMAPSET_FIELD(updatedCols); COMPARE_BITMAPSET_FIELD(extraUpdatedCols); COMPARE_NODE_FIELD(securityQuals); + COMPARE_SCALAR_FIELD(forceDistRandom); return true; } diff --git a/src/test/regress/expected/gpdist.out b/src/test/regress/expected/gpdist.out index d86857eb040..53809098cf0 100644 --- a/src/test/regress/expected/gpdist.out +++ b/src/test/regress/expected/gpdist.out @@ -821,3 +821,49 @@ SELECT count(*) FROM gp_dist_random('gdr_union') WHERE branch = 3; (1 row) ROLLBACK; +-- RTE equality must distinguish ordinary scans from gp_dist_random scans. +-- ONLY makes the inheritance flag identical, leaving forceDistRandom different. +BEGIN; +SET LOCAL optimizer = off; +SELECT EXISTS (SELECT 1 FROM ONLY gp_id WHERE gp_execution_segment() >= 0) + AS on_coordinator, + EXISTS (SELECT 1 FROM gp_dist_random('gp_id') WHERE gp_execution_segment() >= 0) + AS on_segments; + on_coordinator | on_segments +----------------+------------- + f | t +(1 row) + +-- Both OR orders must retain the distributed subquery and return one row. +SELECT 1 AS result + WHERE EXISTS (SELECT 1 FROM ONLY gp_id WHERE gp_execution_segment() >= 0) + OR EXISTS (SELECT 1 FROM gp_dist_random('gp_id') WHERE gp_execution_segment() >= 0); + result +-------- + 1 +(1 row) + +SELECT 1 AS result + WHERE EXISTS (SELECT 1 FROM gp_dist_random('gp_id') WHERE gp_execution_segment() >= 0) + OR EXISTS (SELECT 1 FROM ONLY gp_id WHERE gp_execution_segment() >= 0); + result +-------- + 1 +(1 row) + +-- Both AND orders must retain the coordinator subquery and return no rows. +SELECT 1 AS result + WHERE EXISTS (SELECT 1 FROM ONLY gp_id WHERE gp_execution_segment() >= 0) + AND EXISTS (SELECT 1 FROM gp_dist_random('gp_id') WHERE gp_execution_segment() >= 0); + result +-------- +(0 rows) + +SELECT 1 AS result + WHERE EXISTS (SELECT 1 FROM gp_dist_random('gp_id') WHERE gp_execution_segment() >= 0) + AND EXISTS (SELECT 1 FROM ONLY gp_id WHERE gp_execution_segment() >= 0); + result +-------- +(0 rows) + +ROLLBACK; diff --git a/src/test/regress/expected/gpdist_optimizer.out b/src/test/regress/expected/gpdist_optimizer.out index 9025dba258e..2e8e5eb5aaf 100644 --- a/src/test/regress/expected/gpdist_optimizer.out +++ b/src/test/regress/expected/gpdist_optimizer.out @@ -825,3 +825,49 @@ SELECT count(*) FROM gp_dist_random('gdr_union') WHERE branch = 3; (1 row) ROLLBACK; +-- RTE equality must distinguish ordinary scans from gp_dist_random scans. +-- ONLY makes the inheritance flag identical, leaving forceDistRandom different. +BEGIN; +SET LOCAL optimizer = off; +SELECT EXISTS (SELECT 1 FROM ONLY gp_id WHERE gp_execution_segment() >= 0) + AS on_coordinator, + EXISTS (SELECT 1 FROM gp_dist_random('gp_id') WHERE gp_execution_segment() >= 0) + AS on_segments; + on_coordinator | on_segments +----------------+------------- + f | t +(1 row) + +-- Both OR orders must retain the distributed subquery and return one row. +SELECT 1 AS result + WHERE EXISTS (SELECT 1 FROM ONLY gp_id WHERE gp_execution_segment() >= 0) + OR EXISTS (SELECT 1 FROM gp_dist_random('gp_id') WHERE gp_execution_segment() >= 0); + result +-------- + 1 +(1 row) + +SELECT 1 AS result + WHERE EXISTS (SELECT 1 FROM gp_dist_random('gp_id') WHERE gp_execution_segment() >= 0) + OR EXISTS (SELECT 1 FROM ONLY gp_id WHERE gp_execution_segment() >= 0); + result +-------- + 1 +(1 row) + +-- Both AND orders must retain the coordinator subquery and return no rows. +SELECT 1 AS result + WHERE EXISTS (SELECT 1 FROM ONLY gp_id WHERE gp_execution_segment() >= 0) + AND EXISTS (SELECT 1 FROM gp_dist_random('gp_id') WHERE gp_execution_segment() >= 0); + result +-------- +(0 rows) + +SELECT 1 AS result + WHERE EXISTS (SELECT 1 FROM gp_dist_random('gp_id') WHERE gp_execution_segment() >= 0) + AND EXISTS (SELECT 1 FROM ONLY gp_id WHERE gp_execution_segment() >= 0); + result +-------- +(0 rows) + +ROLLBACK; diff --git a/src/test/regress/sql/gpdist.sql b/src/test/regress/sql/gpdist.sql index a0ce5e66bb4..aa5406c6043 100644 --- a/src/test/regress/sql/gpdist.sql +++ b/src/test/regress/sql/gpdist.sql @@ -565,3 +565,27 @@ SELECT bool_and(seg >= 0) AS on_segments, -- An impossible filter must remain empty. SELECT count(*) FROM gp_dist_random('gdr_union') WHERE branch = 3; ROLLBACK; + +-- RTE equality must distinguish ordinary scans from gp_dist_random scans. +-- ONLY makes the inheritance flag identical, leaving forceDistRandom different. +BEGIN; +SET LOCAL optimizer = off; +SELECT EXISTS (SELECT 1 FROM ONLY gp_id WHERE gp_execution_segment() >= 0) + AS on_coordinator, + EXISTS (SELECT 1 FROM gp_dist_random('gp_id') WHERE gp_execution_segment() >= 0) + AS on_segments; +-- Both OR orders must retain the distributed subquery and return one row. +SELECT 1 AS result + WHERE EXISTS (SELECT 1 FROM ONLY gp_id WHERE gp_execution_segment() >= 0) + OR EXISTS (SELECT 1 FROM gp_dist_random('gp_id') WHERE gp_execution_segment() >= 0); +SELECT 1 AS result + WHERE EXISTS (SELECT 1 FROM gp_dist_random('gp_id') WHERE gp_execution_segment() >= 0) + OR EXISTS (SELECT 1 FROM ONLY gp_id WHERE gp_execution_segment() >= 0); +-- Both AND orders must retain the coordinator subquery and return no rows. +SELECT 1 AS result + WHERE EXISTS (SELECT 1 FROM ONLY gp_id WHERE gp_execution_segment() >= 0) + AND EXISTS (SELECT 1 FROM gp_dist_random('gp_id') WHERE gp_execution_segment() >= 0); +SELECT 1 AS result + WHERE EXISTS (SELECT 1 FROM gp_dist_random('gp_id') WHERE gp_execution_segment() >= 0) + AND EXISTS (SELECT 1 FROM ONLY gp_id WHERE gp_execution_segment() >= 0); +ROLLBACK;