From 5de889401caa5937873656198e64e6ab29111ca1 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas@vondra.me>
Date: Fri, 17 Jul 2026 23:02:51 +0200
Subject: [PATCH v6 8/9] Add GUCs for limiting join enumaration

Replace BLOOM_MAX_BUILD_RELIDS and BLOOM_MAX_BUILD_SETS with
user-defined GUCs, to allow easier experimentation.
---
 src/backend/optimizer/path/costsize.c         | 16 ++++++++
 src/backend/optimizer/path/joinrels.c         | 37 ++++++++-----------
 src/backend/utils/misc/guc_parameters.dat     | 22 +++++++++++
 src/backend/utils/misc/postgresql.conf.sample |  2 +
 src/include/optimizer/cost.h                  |  2 +
 5 files changed, 58 insertions(+), 21 deletions(-)

diff --git a/src/backend/optimizer/path/costsize.c b/src/backend/optimizer/path/costsize.c
index a29e63e8738..f650c7c3d19 100644
--- a/src/backend/optimizer/path/costsize.c
+++ b/src/backend/optimizer/path/costsize.c
@@ -184,6 +184,22 @@ double		bloom_filter_pushdown_threshold = 0.3;
  */
 int			bloom_filter_pushdown_max = 3;
 
+/*
+ * Maximum number of base relations allowed in an enumerated Bloom-filter build
+ * side.  Bloom filters over larger joins are unlikely to be worthwhile and
+ * enumerating them would inflate planning time, so we keep this small.  This
+ * bounds the *size* of each candidate build side, which is distinct from
+ * bloom_filter_pushdown_max that bounds how many interesting filters are
+ * ultimately kept per probe relation.
+ */
+int			bloom_filter_pushdown_max_build_relids = 3;
+
+/*
+ * Overall cap on the number of enumerated build-side relid sets, as a safety
+ * valve against pathological join graphs.
+ */
+int			bloom_filter_pushdown_max_build_sets = 100;
+
 typedef struct
 {
 	PlannerInfo *root;
diff --git a/src/backend/optimizer/path/joinrels.c b/src/backend/optimizer/path/joinrels.c
index c73f3e69a7c..8faba70664b 100644
--- a/src/backend/optimizer/path/joinrels.c
+++ b/src/backend/optimizer/path/joinrels.c
@@ -899,23 +899,6 @@ simple_have_relevant_joinclause(PlannerInfo *root, Relids relids1,
 	return false;
 }
 
-/*
- * Maximum number of base relations allowed in an enumerated Bloom-filter
- * build side.  Bloom filters over larger joins are unlikely to be worthwhile
- * and enumerating them would inflate planning time, so we keep this small.
- *
- * This bounds the *size* of each candidate build side, which is distinct from
- * the bloom_filter_pushdown_max GUC that bounds how many interesting filters
- * are ultimately kept per probe relation.
- */
-#define BLOOM_MAX_BUILD_RELIDS	3
-
-/*
- * Overall cap on the number of enumerated build-side relid sets, as a safety
- * valve against pathological join graphs.
- */
-#define BLOOM_MAX_BUILD_SETS	100
-
 /*
  * enumerate_bloom_filter_build_relids
  *	  Enumerate the legal join relations (and single base relations) that
@@ -970,14 +953,15 @@ simple_have_relevant_joinclause(PlannerInfo *root, Relids relids1,
 List *
 enumerate_bloom_filter_build_relids(PlannerInfo *root)
 {
-	List	   *levels[BLOOM_MAX_BUILD_RELIDS + 1];
+	List	  **levels;
 	List	   *result = NIL;
 	int			nbaserels = 0;
 	int			level;
 	int			i;
 
 	/* bail out if filter pushdown disabled */
-	if (!enable_hashjoin_bloom || bloom_filter_pushdown_max <= 0)
+	if (!enable_hashjoin_bloom || bloom_filter_pushdown_max <= 0 ||
+		bloom_filter_pushdown_max_build_relids <= 0)
 		return NIL;
 
 	/* Return the cached answer if we've already computed it. */
@@ -987,6 +971,12 @@ enumerate_bloom_filter_build_relids(PlannerInfo *root)
 	root->bloom_build_relids_valid = true;
 	root->bloom_build_relids = NIL;
 
+	/*
+	 * Join levels, indexed by number of relids (starts at 1 for baserels,
+	 * same as in standard_join_search).
+	 */
+	levels = palloc0_array(List *, (bloom_filter_pushdown_max_build_relids + 1));
+
 	/* Level 1: each base relation on its own. */
 	levels[1] = NIL;
 	for (i = 1; i < root->simple_rel_array_size; i++)
@@ -1023,7 +1013,7 @@ enumerate_bloom_filter_build_relids(PlannerInfo *root)
 	result = list_copy(levels[1]);
 
 	/* Build higher levels by adding one base relation at a time. */
-	for (level = 2; level <= BLOOM_MAX_BUILD_RELIDS; level++)
+	for (level = 2; level <= bloom_filter_pushdown_max_build_relids; level++)
 	{
 		ListCell   *lc;
 
@@ -1080,7 +1070,12 @@ enumerate_bloom_filter_build_relids(PlannerInfo *root)
 				levels[level] = lappend(levels[level], joinrelids);
 				result = lappend(result, joinrelids);
 
-				if (list_length(result) >= BLOOM_MAX_BUILD_SETS)
+				/*
+				 * Stop generating joins once we hit the maximum allowed
+				 * number of join relids (to keep the planning effort under
+				 * control).
+				 */
+				if (list_length(result) >= bloom_filter_pushdown_max_build_sets)
 				{
 					root->bloom_build_relids = result;
 					return result;
diff --git a/src/backend/utils/misc/guc_parameters.dat b/src/backend/utils/misc/guc_parameters.dat
index aacadc8de40..3ac75bc4b38 100644
--- a/src/backend/utils/misc/guc_parameters.dat
+++ b/src/backend/utils/misc/guc_parameters.dat
@@ -388,6 +388,27 @@
   boot_val => '3',
   min => '0',
   max => '10',
+  flags => 'GUC_EXPLAIN',
+},
+
+{ name => 'bloom_filter_pushdown_max_build_relids', type => 'int', context => 'PGC_USERSET', group => 'QUERY_TUNING_OTHER',
+  short_desc => 'Maximum number of base relations in an enumerated hash join bloom filter build side.',
+  long_desc => 'Bounds the size of each candidate build side the planner enumerates when looking for interesting pushed-down bloom filters.',
+  variable => 'bloom_filter_pushdown_max_build_relids',
+  boot_val => '3',
+  min => '1',
+  max => '100',
+  flags => 'GUC_EXPLAIN',
+},
+
+{ name => 'bloom_filter_pushdown_max_build_sets', type => 'int', context => 'PGC_USERSET', group => 'QUERY_TUNING_OTHER',
+  short_desc => 'Maximum number of enumerated hash join bloom filter build-side relation sets.',
+  long_desc => 'Overall cap on the number of build-side relation sets the planner enumerates, as a safety valve against pathological join graphs.',
+  variable => 'bloom_filter_pushdown_max_build_sets',
+  boot_val => '100',
+  min => '1',
+  max => 'INT_MAX',
+  flags => 'GUC_EXPLAIN',
 },
 
 { name => 'bloom_filter_pushdown_threshold', type => 'real', context => 'PGC_USERSET', group => 'QUERY_TUNING_OTHER',
@@ -398,6 +419,7 @@
   boot_val => '0.3',
   min => '0.0',
   max => '1.0',
+  flags => 'GUC_EXPLAIN',
 },
 
 { name => 'bonjour', type => 'bool', context => 'PGC_POSTMASTER', group => 'CONN_AUTH_SETTINGS',
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index 3247ed46e6e..9080e9e0784 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -488,6 +488,8 @@
 
 #bloom_filter_pushdown_max = 3          # range 0-10
 #bloom_filter_pushdown_threshold = 0.3  # range 0.0-1.0
+#bloom_filter_pushdown_max_build_relids = 3      # range 1-100
+#bloom_filter_pushdown_max_build_sets = 100      # range 1-INT_MAX
 #default_statistics_target = 100        # range 1-10000
 #constraint_exclusion = partition       # on, off, or partition
 #cursor_tuple_fraction = 0.1            # range 0.0-1.0
diff --git a/src/include/optimizer/cost.h b/src/include/optimizer/cost.h
index 2047f211c4c..9daa6ded3f8 100644
--- a/src/include/optimizer/cost.h
+++ b/src/include/optimizer/cost.h
@@ -74,6 +74,8 @@ extern PGDLLIMPORT bool enable_presorted_aggregate;
 extern PGDLLIMPORT bool enable_async_append;
 extern PGDLLIMPORT double bloom_filter_pushdown_threshold;
 extern PGDLLIMPORT int bloom_filter_pushdown_max;
+extern PGDLLIMPORT int bloom_filter_pushdown_max_build_relids;
+extern PGDLLIMPORT int bloom_filter_pushdown_max_build_sets;
 extern PGDLLIMPORT int constraint_exclusion;
 
 extern double index_pages_fetched(double tuples_fetched, BlockNumber pages,
-- 
2.55.0

