From 82a9b29f30f6a2a5cf39429862082dba039eb37d Mon Sep 17 00:00:00 2001 From: William Bernbaum Date: Wed, 26 Aug 2026 13:14:58 -0700 Subject: [PATCH v1 10/16] Give the existence check a driver to probe with Where a join lies entirely within the relations excluded from the tlist and the HAVING qual, add the parameterization it needs. --- src/backend/optimizer/path/joinpath.c | 13 ++ src/backend/optimizer/plan/initsplan.c | 33 ++++ src/backend/optimizer/plan/planmain.c | 1 + src/include/nodes/pathnodes.h | 3 + src/test/regress/expected/eager_aggregate.out | 162 ++++++++++++++++++ src/test/regress/sql/eager_aggregate.sql | 102 +++++++++++ 6 files changed, 314 insertions(+) diff --git a/src/backend/optimizer/path/joinpath.c b/src/backend/optimizer/path/joinpath.c index dfd08e7aeb1..c7f1abfd6f0 100644 --- a/src/backend/optimizer/path/joinpath.c +++ b/src/backend/optimizer/path/joinpath.c @@ -300,6 +300,19 @@ add_paths_to_joinrel(PlannerInfo *root, sjinfo2->min_lefthand)); } + /* + * The rules above parameterize a join where a SpecialJoinInfo constrains + * the join order. A join folded into an existence check is probed once + * per row of whatever drives it, and the join order leaves that shape + * optional. Add the parameterization it needs, from any relation outside + * the join. + */ + if (root->filter_only_rels != NULL && + bms_is_subset(joinrelids, root->filter_only_rels)) + extra.param_source_rels = + bms_join(extra.param_source_rels, + bms_difference(root->all_baserels, joinrelids)); + /* * However, when a LATERAL subquery is involved, there will simply not be * any paths for the joinrel that aren't parameterized by whatever the diff --git a/src/backend/optimizer/plan/initsplan.c b/src/backend/optimizer/plan/initsplan.c index f21946813c7..d7ff206c1d3 100644 --- a/src/backend/optimizer/plan/initsplan.c +++ b/src/backend/optimizer/plan/initsplan.c @@ -118,6 +118,7 @@ static SpecialJoinInfo *make_outerjoininfo(PlannerInfo *root, Relids inner_join_rels, JoinType jointype, Index ojrelid, List *clause); +static Relids find_filter_only_rels(PlannerInfo *root); static void compute_semijoin_info(PlannerInfo *root, SpecialJoinInfo *sjinfo, List *clause); static void deconstruct_distribute_oj_quals(PlannerInfo *root, @@ -771,6 +772,38 @@ setup_eager_aggregation(PlannerInfo *root) * Collect grouping expressions that appear in grouping clauses. */ create_grouping_expr_infos(root); + + root->filter_only_rels = find_filter_only_rels(root); +} + +/* + * find_filter_only_rels + * Base relations that only decide which rows survive. + * + * Excluded from the tlist and the HAVING qual. The query can only tell + * whether a match exists, so a join of these may be folded into an existence + * check. This set only filters which paths get built. + */ +static Relids +find_filter_only_rels(PlannerInfo *root) +{ + Relids observable = NULL; + ListCell *lc; + + foreach(lc, root->processed_tlist) + { + TargetEntry *te = lfirst_node(TargetEntry, lc); + + observable = bms_add_members(observable, + pull_varnos(root, (Node *) te->expr)); + } + + if (root->parse->havingQual) + observable = bms_add_members(observable, + pull_varnos(root, + root->parse->havingQual)); + + return bms_difference(root->all_baserels, observable); } /* diff --git a/src/backend/optimizer/plan/planmain.c b/src/backend/optimizer/plan/planmain.c index 407b89219e1..7ebe8d966ed 100644 --- a/src/backend/optimizer/plan/planmain.c +++ b/src/backend/optimizer/plan/planmain.c @@ -79,6 +79,7 @@ query_planner(PlannerInfo *root, root->agg_clause_list = NIL; root->group_expr_list = NIL; root->eager_group_clause = NIL; + root->filter_only_rels = NULL; root->tlist_vars = NIL; root->fkey_list = NIL; root->initial_rels = NIL; diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h index 158d689c377..e899c3bec37 100644 --- a/src/include/nodes/pathnodes.h +++ b/src/include/nodes/pathnodes.h @@ -504,6 +504,9 @@ struct PlannerInfo /* the SortGroupClauses the grouping expressions were derived from */ List *eager_group_clause; + /* base rels supplying nothing the query outputs, or NULL if none */ + Relids filter_only_rels; + /* list of plain Vars contained in targetlist and havingQual */ List *tlist_vars; diff --git a/src/test/regress/expected/eager_aggregate.out b/src/test/regress/expected/eager_aggregate.out index 96b28cb9b61..a77234e353b 100644 --- a/src/test/regress/expected/eager_aggregate.out +++ b/src/test/regress/expected/eager_aggregate.out @@ -2391,3 +2391,165 @@ RESET enable_eager_aggregate; DROP TABLE eager_distinct_c1; DROP TABLE eager_distinct_c2; DROP TABLE eager_distinct_c3; +-- +-- Test that a relation the query can only test for a match is folded into an +-- existence check, and that this never happens where the matches are counted +-- +CREATE TABLE eager_semi_d (id int PRIMARY KEY, k numeric); +CREATE TABLE eager_semi_f1 (id int PRIMARY KEY, d_id int); +CREATE TABLE eager_semi_f2 (id int PRIMARY KEY, f1_id int, flag bool); +INSERT INTO eager_semi_d SELECT i, i::numeric FROM generate_series(1, 100) i; +INSERT INTO eager_semi_f1 + SELECT i, ((i - 1) / 10) + 1 FROM generate_series(1, 1000) i; +INSERT INTO eager_semi_f2 + SELECT i, ((i - 1) / 10) + 1, i % 2 = 0 FROM generate_series(1, 10000) i; +CREATE INDEX ON eager_semi_f1 (d_id); +CREATE INDEX ON eager_semi_f2 (f1_id); +ANALYZE eager_semi_d; +ANALYZE eager_semi_f1; +ANALYZE eager_semi_f2; +-- Neither f1 nor f2 reaches the output, so the join may stop at the first match +EXPLAIN (COSTS OFF) +SELECT DISTINCT d.id + FROM eager_semi_d d + JOIN eager_semi_f1 f1 ON f1.d_id = d.id + JOIN eager_semi_f2 f2 ON f2.f1_id = f1.id + WHERE f2.flag; + QUERY PLAN +-------------------------------------------------------------------------------- + HashAggregate + Group Key: d.id + -> Nested Loop Semi Join + -> Seq Scan on eager_semi_d d + -> Nested Loop + -> Index Scan using eager_semi_f1_d_id_idx on eager_semi_f1 f1 + Index Cond: (d_id = d.id) + -> Index Scan using eager_semi_f2_f1_id_idx on eager_semi_f2 f2 + Index Cond: (f1_id = f1.id) + Filter: flag +(10 rows) + +SELECT count(*), sum(id) FROM ( + SELECT DISTINCT d.id + FROM eager_semi_d d + JOIN eager_semi_f1 f1 ON f1.d_id = d.id + JOIN eager_semi_f2 f2 ON f2.f1_id = f1.id + WHERE f2.flag) s; + count | sum +-------+------ + 100 | 5050 +(1 row) + +-- Once a column of f1 reaches the output, the matches are observable +EXPLAIN (COSTS OFF) +SELECT DISTINCT d.id, f1.id + FROM eager_semi_d d + JOIN eager_semi_f1 f1 ON f1.d_id = d.id; + QUERY PLAN +---------------------------------------------- + HashAggregate + Group Key: d.id, f1.id + -> Hash Join + Hash Cond: (f1.d_id = d.id) + -> Seq Scan on eager_semi_f1 f1 + -> Hash + -> Seq Scan on eager_semi_d d +(7 rows) + +-- An aggregate counts the matches, so every one of them must be produced +EXPLAIN (COSTS OFF) +SELECT d.id, count(*) + FROM eager_semi_d d + JOIN eager_semi_f1 f1 ON f1.d_id = d.id + GROUP BY d.id; + QUERY PLAN +------------------------------------------------ + Finalize HashAggregate + Group Key: d.id + -> Hash Join + Hash Cond: (f1.d_id = d.id) + -> Partial HashAggregate + Group Key: f1.d_id + -> Seq Scan on eager_semi_f1 f1 + -> Hash + -> Seq Scan on eager_semi_d d +(9 rows) + +SELECT count(*), sum(c) FROM ( + SELECT d.id, count(*) AS c + FROM eager_semi_d d + JOIN eager_semi_f1 f1 ON f1.d_id = d.id + GROUP BY d.id) s; + count | sum +-------+------ + 100 | 1000 +(1 row) + +-- The same holds when the aggregate names no column of the counted relation +SELECT count(*), sum(c) FROM ( + SELECT d.id, count(*) AS c + FROM eager_semi_d d + JOIN eager_semi_f1 f1 ON f1.d_id = d.id + JOIN eager_semi_f2 f2 ON f2.f1_id = f1.id + GROUP BY d.id) s; + count | sum +-------+------- + 100 | 10000 +(1 row) + +-- Equality does not imply image equality for numeric, so the grouping key +-- rules out eager aggregation altogether +EXPLAIN (COSTS OFF) +SELECT DISTINCT d.k + FROM eager_semi_d d + JOIN eager_semi_f1 f1 ON f1.d_id = d.id; + QUERY PLAN +---------------------------------------------- + HashAggregate + Group Key: d.k + -> Hash Join + Hash Cond: (f1.d_id = d.id) + -> Seq Scan on eager_semi_f1 f1 + -> Hash + -> Seq Scan on eager_semi_d d +(7 rows) + +-- An outer join above the existence check still sees the rows it must +SELECT count(*), sum(id) FROM ( + SELECT DISTINCT d.id + FROM eager_semi_d d + LEFT JOIN eager_semi_f1 f1 ON f1.d_id = d.id + JOIN eager_semi_f2 f2 ON f2.f1_id = f1.id + WHERE f2.flag) s; + count | sum +-------+------ + 100 | 5050 +(1 row) + +SET enable_eager_aggregate TO off; +SELECT count(*), sum(id) FROM ( + SELECT DISTINCT d.id + FROM eager_semi_d d + JOIN eager_semi_f1 f1 ON f1.d_id = d.id + JOIN eager_semi_f2 f2 ON f2.f1_id = f1.id + WHERE f2.flag) s; + count | sum +-------+------ + 100 | 5050 +(1 row) + +SELECT count(*), sum(id) FROM ( + SELECT DISTINCT d.id + FROM eager_semi_d d + LEFT JOIN eager_semi_f1 f1 ON f1.d_id = d.id + JOIN eager_semi_f2 f2 ON f2.f1_id = f1.id + WHERE f2.flag) s; + count | sum +-------+------ + 100 | 5050 +(1 row) + +RESET enable_eager_aggregate; +DROP TABLE eager_semi_d; +DROP TABLE eager_semi_f1; +DROP TABLE eager_semi_f2; diff --git a/src/test/regress/sql/eager_aggregate.sql b/src/test/regress/sql/eager_aggregate.sql index 8e4111dddf7..32650fa945c 100644 --- a/src/test/regress/sql/eager_aggregate.sql +++ b/src/test/regress/sql/eager_aggregate.sql @@ -643,3 +643,105 @@ RESET enable_eager_aggregate; DROP TABLE eager_distinct_c1; DROP TABLE eager_distinct_c2; DROP TABLE eager_distinct_c3; + + +-- +-- Test that a relation the query can only test for a match is folded into an +-- existence check, and that this never happens where the matches are counted +-- + +CREATE TABLE eager_semi_d (id int PRIMARY KEY, k numeric); +CREATE TABLE eager_semi_f1 (id int PRIMARY KEY, d_id int); +CREATE TABLE eager_semi_f2 (id int PRIMARY KEY, f1_id int, flag bool); + +INSERT INTO eager_semi_d SELECT i, i::numeric FROM generate_series(1, 100) i; +INSERT INTO eager_semi_f1 + SELECT i, ((i - 1) / 10) + 1 FROM generate_series(1, 1000) i; +INSERT INTO eager_semi_f2 + SELECT i, ((i - 1) / 10) + 1, i % 2 = 0 FROM generate_series(1, 10000) i; + +CREATE INDEX ON eager_semi_f1 (d_id); +CREATE INDEX ON eager_semi_f2 (f1_id); + +ANALYZE eager_semi_d; +ANALYZE eager_semi_f1; +ANALYZE eager_semi_f2; + +-- Neither f1 nor f2 reaches the output, so the join may stop at the first match +EXPLAIN (COSTS OFF) +SELECT DISTINCT d.id + FROM eager_semi_d d + JOIN eager_semi_f1 f1 ON f1.d_id = d.id + JOIN eager_semi_f2 f2 ON f2.f1_id = f1.id + WHERE f2.flag; + +SELECT count(*), sum(id) FROM ( + SELECT DISTINCT d.id + FROM eager_semi_d d + JOIN eager_semi_f1 f1 ON f1.d_id = d.id + JOIN eager_semi_f2 f2 ON f2.f1_id = f1.id + WHERE f2.flag) s; + +-- Once a column of f1 reaches the output, the matches are observable +EXPLAIN (COSTS OFF) +SELECT DISTINCT d.id, f1.id + FROM eager_semi_d d + JOIN eager_semi_f1 f1 ON f1.d_id = d.id; + +-- An aggregate counts the matches, so every one of them must be produced +EXPLAIN (COSTS OFF) +SELECT d.id, count(*) + FROM eager_semi_d d + JOIN eager_semi_f1 f1 ON f1.d_id = d.id + GROUP BY d.id; + +SELECT count(*), sum(c) FROM ( + SELECT d.id, count(*) AS c + FROM eager_semi_d d + JOIN eager_semi_f1 f1 ON f1.d_id = d.id + GROUP BY d.id) s; + +-- The same holds when the aggregate names no column of the counted relation +SELECT count(*), sum(c) FROM ( + SELECT d.id, count(*) AS c + FROM eager_semi_d d + JOIN eager_semi_f1 f1 ON f1.d_id = d.id + JOIN eager_semi_f2 f2 ON f2.f1_id = f1.id + GROUP BY d.id) s; + +-- Equality does not imply image equality for numeric, so the grouping key +-- rules out eager aggregation altogether +EXPLAIN (COSTS OFF) +SELECT DISTINCT d.k + FROM eager_semi_d d + JOIN eager_semi_f1 f1 ON f1.d_id = d.id; + +-- An outer join above the existence check still sees the rows it must +SELECT count(*), sum(id) FROM ( + SELECT DISTINCT d.id + FROM eager_semi_d d + LEFT JOIN eager_semi_f1 f1 ON f1.d_id = d.id + JOIN eager_semi_f2 f2 ON f2.f1_id = f1.id + WHERE f2.flag) s; + +SET enable_eager_aggregate TO off; + +SELECT count(*), sum(id) FROM ( + SELECT DISTINCT d.id + FROM eager_semi_d d + JOIN eager_semi_f1 f1 ON f1.d_id = d.id + JOIN eager_semi_f2 f2 ON f2.f1_id = f1.id + WHERE f2.flag) s; + +SELECT count(*), sum(id) FROM ( + SELECT DISTINCT d.id + FROM eager_semi_d d + LEFT JOIN eager_semi_f1 f1 ON f1.d_id = d.id + JOIN eager_semi_f2 f2 ON f2.f1_id = f1.id + WHERE f2.flag) s; + +RESET enable_eager_aggregate; + +DROP TABLE eager_semi_d; +DROP TABLE eager_semi_f1; +DROP TABLE eager_semi_f2;