From 5c968dd818f0e3d3efde41919a0612963c7a2262 Mon Sep 17 00:00:00 2001 From: William Bernbaum Date: Tue, 25 Aug 2026 19:33:44 -0700 Subject: [PATCH 4/4] semijoin-v1-patch-d Co-authored-by: Cursor --- src/backend/optimizer/README | 4 + src/backend/optimizer/plan/analyzejoins.c | 13 ++ .../regress/expected/semijoin_conversion.out | 130 ++++++++++++++++++ src/test/regress/sql/semijoin_conversion.sql | 60 ++++++++ 4 files changed, 207 insertions(+) diff --git a/src/backend/optimizer/README b/src/backend/optimizer/README index 4efc4414419..579a4fdceeb 100644 --- a/src/backend/optimizer/README +++ b/src/backend/optimizer/README @@ -313,6 +313,10 @@ as one righthand side, but with a selective lefthand side and no index, in a handful of adversarial cases, the transformation could cost more than preventing the fanout would save. +A table, a subquery, a function scan and a VALUES list can all be lifted. The +last two build their own rows from expressions, so a volatile expression rules +one out: unique-ification changes how often the rows are built. + enable_semijoin_conversion controls the transformation. Only queries whose joins are all plain inner joins are considered, and lateral references and PlaceHolderVars rule a query out, since either may need a column from a diff --git a/src/backend/optimizer/plan/analyzejoins.c b/src/backend/optimizer/plan/analyzejoins.c index 1517a2d68ff..0f353ce4359 100644 --- a/src/backend/optimizer/plan/analyzejoins.c +++ b/src/backend/optimizer/plan/analyzejoins.c @@ -1176,6 +1176,19 @@ convert_joins_to_semijoins(PlannerInfo *root, List *joinlist) case RTE_SUBQUERY: break; + /* + * These build their own rows, and unique-ification changes + * how often that happens. + */ + case RTE_FUNCTION: + if (contain_volatile_functions((Node *) rte->functions)) + continue; + break; + case RTE_VALUES: + if (contain_volatile_functions((Node *) rte->values_lists)) + continue; + break; + default: continue; } diff --git a/src/test/regress/expected/semijoin_conversion.out b/src/test/regress/expected/semijoin_conversion.out index 29875be96de..31f08e52b31 100644 --- a/src/test/regress/expected/semijoin_conversion.out +++ b/src/test/regress/expected/semijoin_conversion.out @@ -393,6 +393,98 @@ SELECT DISTINCT d.id, d.payload -> Seq Scan on sjc_driver d (12 rows) +-- +-- Relation kinds other than tables and subqueries +-- +-- A VALUES list restricting which drivers survive is no different from a table +-- restricting them, and duplicates in the list fan the driver out the same way. +EXPLAIN (COSTS OFF) +SELECT DISTINCT d.id, d.payload + FROM sjc_driver d JOIN (VALUES (1), (1), (2), (2)) v(id) ON v.id = d.grp; + QUERY PLAN +------------------------------------------------- + HashAggregate + Group Key: d.id, d.payload + -> Hash Semi Join + Hash Cond: (d.grp = "*VALUES*".column1) + -> Seq Scan on sjc_driver d + -> Hash + -> Values Scan on "*VALUES*" +(7 rows) + +-- Same for a set-returning function, which is how a client-supplied key set +-- often arrives. +EXPLAIN (COSTS OFF) +SELECT DISTINCT d.id, d.payload + FROM sjc_driver d JOIN generate_series(1, 4) g(id) ON g.id = d.grp; + QUERY PLAN +------------------------------------------------------ + HashAggregate + Group Key: d.id, d.payload + -> Hash Semi Join + Hash Cond: (d.grp = g.id) + -> Seq Scan on sjc_driver d + -> Hash + -> Function Scan on generate_series g +(7 rows) + +-- The function scan and the table join to each other, so the two form one +-- group and the group is declined. +EXPLAIN (COSTS OFF) +SELECT DISTINCT d.id, d.payload + FROM sjc_driver d + JOIN generate_series(1, 4) g(id) ON true + JOIN sjc_filter f ON f.driver_id = d.id AND f.id % 4 = g.id + WHERE f.flag; + QUERY PLAN +-------------------------------------------------------------------- + Unique + -> Sort + Sort Key: d.id, d.payload + -> Nested Loop + -> Hash Join + Hash Cond: ((f.id % 4) = g.id) + -> Seq Scan on sjc_filter f + Filter: flag + -> Hash + -> Function Scan on generate_series g + -> Index Scan using sjc_driver_pkey on sjc_driver d + Index Cond: (id = f.driver_id) +(12 rows) + +-- A volatile function builds fresh rows on every read, and unique-ification +-- would change how often the read happens. +EXPLAIN (COSTS OFF) +SELECT DISTINCT d.id, d.payload + FROM sjc_driver d + JOIN generate_series(1, (random() * 4)::int) g(id) ON g.id = d.grp; + QUERY PLAN +------------------------------------------------ + HashAggregate + Group Key: d.id, d.payload + -> Hash Join + Hash Cond: (g.id = d.grp) + -> Function Scan on generate_series g + -> Hash + -> Seq Scan on sjc_driver d +(7 rows) + +-- Likewise a VALUES list whose entries are volatile. +EXPLAIN (COSTS OFF) +SELECT DISTINCT d.id, d.payload + FROM sjc_driver d + JOIN (VALUES ((random() * 4)::int), (2)) v(id) ON v.id = d.grp; + QUERY PLAN +------------------------------------------------- + HashAggregate + Group Key: d.id, d.payload + -> Hash Join + Hash Cond: (d.grp = "*VALUES*".column1) + -> Seq Scan on sjc_driver d + -> Hash + -> Values Scan on "*VALUES*" +(7 rows) + -- -- A key set filtering a chain of to-many joins -- @@ -639,6 +731,25 @@ SELECT count(*), sum(id) FROM ( 30 | 630 (1 row) +SELECT count(*), sum(id) FROM ( + SELECT DISTINCT d.id + FROM sjc_driver d + JOIN generate_series(1, 4) g(id) ON true + JOIN sjc_filter f ON f.driver_id = d.id AND f.id % 4 = g.id + WHERE f.flag) s; + count | sum +-------+----- + 30 | 630 +(1 row) + +SELECT count(*), sum(id) FROM ( + SELECT DISTINCT d.id + FROM sjc_driver d JOIN (VALUES (1), (1), (2), (2)) v(id) ON v.id = d.grp) s; + count | sum +-------+----- + 16 | 304 +(1 row) + SELECT count(DISTINCT c.id) FROM sjc_customer c LEFT JOIN sjc_order o ON o.customer_id = c.id @@ -708,6 +819,25 @@ SELECT count(*), sum(id) FROM ( 30 | 630 (1 row) +SELECT count(*), sum(id) FROM ( + SELECT DISTINCT d.id + FROM sjc_driver d + JOIN generate_series(1, 4) g(id) ON true + JOIN sjc_filter f ON f.driver_id = d.id AND f.id % 4 = g.id + WHERE f.flag) s; + count | sum +-------+----- + 30 | 630 +(1 row) + +SELECT count(*), sum(id) FROM ( + SELECT DISTINCT d.id + FROM sjc_driver d JOIN (VALUES (1), (1), (2), (2)) v(id) ON v.id = d.grp) s; + count | sum +-------+----- + 16 | 304 +(1 row) + SELECT count(DISTINCT c.id) FROM sjc_customer c LEFT JOIN sjc_order o ON o.customer_id = c.id diff --git a/src/test/regress/sql/semijoin_conversion.sql b/src/test/regress/sql/semijoin_conversion.sql index 37ce12e776a..97d03d77cba 100644 --- a/src/test/regress/sql/semijoin_conversion.sql +++ b/src/test/regress/sql/semijoin_conversion.sql @@ -175,6 +175,44 @@ SELECT DISTINCT d.id, d.payload JOIN sjc_uniq2 u2 ON u2.unique_id = u.id WHERE u2.id < 30; +-- +-- Relation kinds other than tables and subqueries +-- + +-- A VALUES list restricting which drivers survive is no different from a table +-- restricting them, and duplicates in the list fan the driver out the same way. +EXPLAIN (COSTS OFF) +SELECT DISTINCT d.id, d.payload + FROM sjc_driver d JOIN (VALUES (1), (1), (2), (2)) v(id) ON v.id = d.grp; + +-- Same for a set-returning function, which is how a client-supplied key set +-- often arrives. +EXPLAIN (COSTS OFF) +SELECT DISTINCT d.id, d.payload + FROM sjc_driver d JOIN generate_series(1, 4) g(id) ON g.id = d.grp; + +-- The function scan and the table join to each other, so the two form one +-- group and the group is declined. +EXPLAIN (COSTS OFF) +SELECT DISTINCT d.id, d.payload + FROM sjc_driver d + JOIN generate_series(1, 4) g(id) ON true + JOIN sjc_filter f ON f.driver_id = d.id AND f.id % 4 = g.id + WHERE f.flag; + +-- A volatile function builds fresh rows on every read, and unique-ification +-- would change how often the read happens. +EXPLAIN (COSTS OFF) +SELECT DISTINCT d.id, d.payload + FROM sjc_driver d + JOIN generate_series(1, (random() * 4)::int) g(id) ON g.id = d.grp; + +-- Likewise a VALUES list whose entries are volatile. +EXPLAIN (COSTS OFF) +SELECT DISTINCT d.id, d.payload + FROM sjc_driver d + JOIN (VALUES ((random() * 4)::int), (2)) v(id) ON v.id = d.grp; + -- -- A key set filtering a chain of to-many joins -- @@ -311,6 +349,17 @@ SELECT count(*), sum(id) FROM ( JOIN sjc_deep e ON e.filter_id = f.id WHERE f.flag) s; +SELECT count(*), sum(id) FROM ( + SELECT DISTINCT d.id + FROM sjc_driver d + JOIN generate_series(1, 4) g(id) ON true + JOIN sjc_filter f ON f.driver_id = d.id AND f.id % 4 = g.id + WHERE f.flag) s; + +SELECT count(*), sum(id) FROM ( + SELECT DISTINCT d.id + FROM sjc_driver d JOIN (VALUES (1), (1), (2), (2)) v(id) ON v.id = d.grp) s; + SELECT count(DISTINCT c.id) FROM sjc_customer c LEFT JOIN sjc_order o ON o.customer_id = c.id @@ -357,6 +406,17 @@ SELECT count(*), sum(id) FROM ( JOIN sjc_deep e ON e.filter_id = f.id WHERE f.flag) s; +SELECT count(*), sum(id) FROM ( + SELECT DISTINCT d.id + FROM sjc_driver d + JOIN generate_series(1, 4) g(id) ON true + JOIN sjc_filter f ON f.driver_id = d.id AND f.id % 4 = g.id + WHERE f.flag) s; + +SELECT count(*), sum(id) FROM ( + SELECT DISTINCT d.id + FROM sjc_driver d JOIN (VALUES (1), (1), (2), (2)) v(id) ON v.id = d.grp) s; + SELECT count(DISTINCT c.id) FROM sjc_customer c LEFT JOIN sjc_order o ON o.customer_id = c.id