From 82a9b29f30f6a2a5cf39429862082dba039eb37d Mon Sep 17 00:00:00 2001
From: William Bernbaum <wbernbaum@dwdev.com>
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;
