From 0c91d1d8d22bdd4a116a8d5c6860c32b22c5610a Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas@vondra.me>
Date: Fri, 19 Jun 2026 22:15:49 +0200
Subject: [PATCH v1 4/6] Fast-path out for small joins

Try calculating the weak upper bound N! * C(N-1). If it's smaller than
the budget, we can skip the DPccp estimation entirely, because it can't
possibly end up with a higher estimate.

The N! * C(N-1) grows pretty quick, but for small-ish joins it can be
below reasonable budgets. For example, budget=10k allows N=5, and with
budget=100k this works for N=6.

Disabled by default, to allow testing / benchmarking of small joins.
---
 contrib/join_hardness/join_hardness.c | 69 +++++++++++++++++++++++++--
 1 file changed, 64 insertions(+), 5 deletions(-)

diff --git a/contrib/join_hardness/join_hardness.c b/contrib/join_hardness/join_hardness.c
index 728215ec76f..be0e4acd3f1 100644
--- a/contrib/join_hardness/join_hardness.c
+++ b/contrib/join_hardness/join_hardness.c
@@ -67,6 +67,7 @@ PG_MODULE_MAGIC_EXT(
 /* GUCs */
 static bool	join_hardness_enabled = false;
 static bool	join_hardness_split = false;
+static bool	join_hardness_fast = false;
 static int	join_hardness_max_effort = 10000;
 
 static join_search_hook_type prev_join_search_hook = NULL;
@@ -107,6 +108,7 @@ 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 Bitmapset *neighborhood(JoinEnumState *state, Bitmapset *s, Bitmapset *x);
 static void on_connected_subgraph(JoinEnumState *state, Bitmapset *s1);
 
@@ -156,6 +158,17 @@ _PG_init(void)
 							 NULL,
 							 NULL);
 
+	DefineCustomBoolVariable("join_hardness.fast",
+							 "allow fast-path for small joins",
+							 NULL,
+							 &join_hardness_fast,
+							 false,
+							 PGC_SUSET,
+							 0,
+							 NULL,
+							 NULL,
+							 NULL);
+
 	DefineCustomIntVariable("join_hardness.threshold",
 							"maximum hardness before a join is considered too hard.",
 							NULL,
@@ -198,6 +211,8 @@ estimate_join_search_effort(PlannerInfo *root, List *initial_rels,
 	JoinGraph   *graph;
 	int			n = list_length(initial_rels);
 	int			i;
+	int64		max_complexity;
+
 	TimestampTz	ts_start,
 				ts_end;
 	uint64		microsec;
@@ -209,12 +224,13 @@ estimate_join_search_effort(PlannerInfo *root, List *initial_rels,
 		return 0;
 
 	/*
-	 * XXX We could check the simple n! * C(n-1) formula estimating the
-	 * number of join orderings, and return immediately if that's below
-	 * budget, so that we don't need to do anything for really small
-	 * problems, right? For small problems we know even need to do the
-	 * enumeration. There simply can't be enough orderings.
+	 * Calculate the (very loose) upper boundary from the number of relations.
+	 * This grows very fast - at 6 tables it's ~30k, at 8 it's already ~17M.
+	 * But for very small joins it allows us to do without the join graph.
 	 */
+	max_complexity = join_ordering_upper_bound(n, budget);
+	if (max_complexity < budget)
+		return max_complexity;
 
 	/* build the adjacency matrix representing the graph */
 	graph = build_join_graph(root, initial_rels);
@@ -550,6 +566,49 @@ join_search_hardness_hook(PlannerInfo *root, int levels_needed, List *initial_re
 		return standard_join_search(root, levels_needed, initial_rels);
 }
 
+/*
+ * join_ordering_upper_bound
+ *		Compute n! * C(n-1), saturated at "budget".
+ *
+ * This is the maximum number of binary join orderings for n base rels, where
+ * C(k) is the k-th Catalan number.  The value is an upper bound independent of
+ * join graph shape, so if it is below the caller's budget then no DPccp search
+ * is needed to prove the budget cannot be reached.
+ */
+static int64
+join_ordering_upper_bound(int n, int64 budget)
+{
+	int64		result = 1.0;
+	int			i;
+
+	/* disable fast-path (useful for testing the full estimation logic) */
+	if (!join_hardness_fast)
+		return budget;
+
+	/* Compute n!, stopping once it reaches the only threshold we care about. */
+	for (i = 2; i <= n; i++)
+	{
+		if ((budget != 0) && (result * i >= budget))
+			return budget;
+
+		result *= i;
+	}
+
+	/* Compute C(n-1) by recurrence: C_k = C_{k-1} * 2(2k-1)/(k+1). */
+	for (i = 1; i < n; i++)
+	{
+		double		factor = 2.0 * (2.0 * (double) i - 1.0) /
+			(double) (i + 1);
+
+		if ((budget != 0) && (result >= budget / factor))
+			return budget;
+
+		result *= factor;
+	}
+
+	return result;
+}
+
 /*
  * build_join_graph
  *		Populate "graph" with one vertex per initial rel and an edge between
-- 
2.54.0

