From 31b4ea9a28b8e1a67f61168ac8d5e7c46a341931 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas@vondra.me>
Date: Fri, 19 Jun 2026 22:16:06 +0200
Subject: [PATCH v1 5/6] Correctly handle clause-less joins

The basic DPccp enumeration works for connected graphs, with a path
between arbitrary vertices. Make it work for clause-less joins too, by
decomposing the graph into components, computing an estimate for each
component, and then evaluating ways to join the components.
---
 contrib/join_hardness/join_hardness.c | 125 +++++++++++++++++++++++++-
 1 file changed, 122 insertions(+), 3 deletions(-)

diff --git a/contrib/join_hardness/join_hardness.c b/contrib/join_hardness/join_hardness.c
index be0e4acd3f1..66e90823492 100644
--- a/contrib/join_hardness/join_hardness.c
+++ b/contrib/join_hardness/join_hardness.c
@@ -109,6 +109,8 @@ typedef struct JoinEnumState
 static JoinGraph *build_join_graph(PlannerInfo *root, List *initial_rels);
 static void free_join_graph(JoinGraph *graph);
 static int64 join_ordering_upper_bound(int n, int64 budget);
+static int	count_join_graph_components(JoinGraph *graph);
+static int64 clauseless_combination_effort(int c, int64 budget);
 static Bitmapset *neighborhood(JoinEnumState *state, Bitmapset *s, Bitmapset *x);
 static void on_connected_subgraph(JoinEnumState *state, Bitmapset *s1);
 
@@ -212,6 +214,7 @@ estimate_join_search_effort(PlannerInfo *root, List *initial_rels,
 	int			n = list_length(initial_rels);
 	int			i;
 	int64		max_complexity;
+	int64		result;
 
 	TimestampTz	ts_start,
 				ts_end;
@@ -272,6 +275,27 @@ estimate_join_search_effort(PlannerInfo *root, List *initial_rels,
 		bms_free(x);
 	}
 
+	/*
+	 * DPccp counts only the csg-cmp pairs reachable through join-graph edges,
+	 * so it has not accounted for the clauseless Cartesian-product joins that
+	 * standard_join_search() must perform to glue together the connected
+	 * components of a disconnected graph.  Add that term on top of the
+	 * per-component pair count (it is zero for a connected graph), keeping the
+	 * total capped at the budget.  If the walk aborted, the work already
+	 * exceeded the budget and the answer is simply the budget.
+	 */
+	if (state.aborted)
+		result = budget;
+	else
+	{
+		int			components = count_join_graph_components(graph);
+		int64		cross = clauseless_combination_effort(components, budget);
+
+		result = state.ccp + cross;
+		if ((budget != 0) && (result > budget))
+			result = budget;
+	}
+
 	free_join_graph(graph);
 
 	ts_end = GetCurrentTimestamp();
@@ -281,10 +305,12 @@ estimate_join_search_effort(PlannerInfo *root, List *initial_rels,
 	 * If aborted, return the budget (to show we aborted), otherwise return
 	 * the number of ccp pairs.
 	 */
-	elog(WARNING, "estimate_join_search_effort: rels %d aborted %d ccp " INT64_FORMAT " budget " INT64_FORMAT " timing " INT64_FORMAT " us",
-		 n, state.aborted, state.ccp, budget, microsec);
+	elog(WARNING, "estimate_join_search_effort: rels %d aborted %d "
+				  "result " INT64_FORMAT " ccp " INT64_FORMAT
+				  " budget " INT64_FORMAT " timing " INT64_FORMAT " us",
+		 n, state.aborted, result, state.ccp, budget, microsec);
 
-	return (state.aborted ? budget : state.ccp);
+	return result;
 }
 
 /*
@@ -670,6 +696,99 @@ free_join_graph(JoinGraph *graph)
 	pfree(graph);
 }
 
+/*
+ * count_join_graph_components
+ *		Return the number of connected components of the join graph.
+ *
+ * A connected graph has a single component; each additional component is a set
+ * of rels that share no join clause or join-order restriction with the rest of
+ * the query and can therefore only be combined with the others by a Cartesian
+ * product.  We find the components with a simple flood fill.
+ */
+static int
+count_join_graph_components(JoinGraph *graph)
+{
+	Bitmapset  *seen = NULL;
+	int			components = 0;
+	int			i;
+
+	for (i = 0; i < graph->n; i++)
+	{
+		Bitmapset  *stack;
+		int			v;
+
+		if (bms_is_member(i, seen))
+			continue;
+
+		/* Start a new component and flood fill it from vertex i. */
+		components++;
+		seen = bms_add_member(seen, i);
+		stack = bms_make_singleton(i);
+
+		while ((v = bms_next_member(stack, -1)) >= 0)
+		{
+			int			w = -1;
+
+			stack = bms_del_member(stack, v);
+			while ((w = bms_next_member(graph->neighbors[v], w)) >= 0)
+			{
+				if (!bms_is_member(w, seen))
+				{
+					seen = bms_add_member(seen, w);
+					stack = bms_add_member(stack, w);
+				}
+			}
+		}
+
+		bms_free(stack);
+	}
+
+	bms_free(seen);
+
+	return components;
+}
+
+/*
+ * clauseless_combination_effort
+ *		Estimate the make_join_rel() pairs forced by combining "c" connected
+ *		components purely through Cartesian products, saturated at "budget".
+ *
+ * standard_join_search() builds up each connected component using that
+ * component's join clauses, but when the join graph splits into several
+ * components it has to glue them together with clauseless (Cartesian-product)
+ * joins.  Treating each fully-built component as an atomic unit, the
+ * left-/right-sided clauseless joins that join_search_one_level() generates
+ * amount to
+ *
+ *		sum_{L=2..c} C(c, L-1) * (c-L+1)  =  c * (2^(c-1) - 1)
+ *
+ * candidate pairs, where C(n, k) is the binomial coefficient.  (Bushy
+ * clauseless joins are deliberately skipped by the join search, so this
+ * left-/right-sided count is the relevant figure.)  DPccp never traverses
+ * these cross-component edges, so the term is added on top of the
+ * per-component #ccp; otherwise a disconnected join graph would be
+ * mis-estimated as essentially free.  The result is zero for a connected graph
+ * (c <= 1).  The accumulation saturates at "budget" so it cannot overflow.
+ */
+static int64
+clauseless_combination_effort(int c, int64 budget)
+{
+	double		pow2 = 1.0;		/* will hold 2^(c-1) */
+	int			i;
+
+	if (c <= 1)
+		return 0.0;
+
+	for (i = 1; i <= c - 1; i++)
+	{
+		pow2 *= 2.0;
+		if ((budget != 0) && ((double) c * (pow2 - 1.0) >= budget))
+			return budget;
+	}
+
+	return (double) c * (pow2 - 1.0);
+}
+
 /*
  * neighborhood
  *		Return the open neighborhood of vertex set "s".
-- 
2.54.0

