From 331e8a35118699cb6d4076aa65ddbfa44a905665 Mon Sep 17 00:00:00 2001
From: Tom Lane <tgl@sss.pgh.pa.us>
Date: Thu, 13 Nov 2025 16:55:43 -0500
Subject: [PATCH v5 5/5] Use hashing to avoid O(N^2) matching work in
 eqjoinsel.

Use a simplehash hash table if there are enough MCVs and the
join operator has associated hash functions.  The threshold
for switching to hash mode perhaps could use more research.
---
 src/backend/utils/adt/selfuncs.c | 245 ++++++++++++++++++++++++++++++-
 src/tools/pgindent/typedefs.list |   3 +
 2 files changed, 243 insertions(+), 5 deletions(-)

diff --git a/src/backend/utils/adt/selfuncs.c b/src/backend/utils/adt/selfuncs.c
index b4ed12a3737..a68d5423874 100644
--- a/src/backend/utils/adt/selfuncs.c
+++ b/src/backend/utils/adt/selfuncs.c
@@ -143,12 +143,47 @@
 
 #define DEFAULT_PAGE_CPU_MULTIPLIER 50.0
 
+/*
+ * In production builds, switch to hash-based MCV matching when lists are
+ * large enough to amortize hash setup cost.  In debug builds, we use a
+ * smaller threshold so that the regression tests cover both paths well.
+ */
+#ifndef USE_ASSERT_CHECKING
+#define EQJOINSEL_MCV_HASH_THRESHOLD 100
+#else
+#define EQJOINSEL_MCV_HASH_THRESHOLD 10
+#endif
+
+/* Entries in the simplehash hash table used by eqjoinsel_find_matches */
+typedef struct McvHashEntry
+{
+	Datum		value;			/* the value represented by this entry */
+	int			index;			/* its index in the relevant AttStatsSlot */
+	uint32		hash;			/* hash code for the Datum */
+	char		status;			/* status code used by simplehash.h */
+} McvHashEntry;
+
+/* private_data for the simplehash hash table */
+typedef struct McvHashContext
+{
+	FunctionCallInfo equal_fcinfo;	/* the equality join operator */
+	FunctionCallInfo hash_fcinfo;	/* the hash function to use */
+	bool		op_is_reversed; /* equality compares hash type to probe type */
+	bool		insert_mode;	/* doing inserts or lookups? */
+	bool		hash_typbyval;	/* typbyval of hashed data type */
+	int16		hash_typlen;	/* typlen of hashed data type */
+} McvHashContext;
+
+/* forward reference */
+typedef struct McvHashTable_hash McvHashTable_hash;
+
 /* Hooks for plugins to get control when we ask for stats */
 get_relation_stats_hook_type get_relation_stats_hook = NULL;
 get_index_stats_hook_type get_index_stats_hook = NULL;
 
 static double eqsel_internal(PG_FUNCTION_ARGS, bool negate);
 static double eqjoinsel_inner(FmgrInfo *eqproc, Oid collation,
+							  Oid hashLeft, Oid hashRight,
 							  VariableStatData *vardata1, VariableStatData *vardata2,
 							  double nd1, double nd2,
 							  bool isdefault1, bool isdefault2,
@@ -157,6 +192,7 @@ static double eqjoinsel_inner(FmgrInfo *eqproc, Oid collation,
 							  bool have_mcvs1, bool have_mcvs2,
 							  bool *hasmatch1, bool *hasmatch2);
 static double eqjoinsel_semi(FmgrInfo *eqproc, Oid collation,
+							 Oid hashLeft, Oid hashRight,
 							 bool op_is_reversed,
 							 VariableStatData *vardata1, VariableStatData *vardata2,
 							 double nd1, double nd2,
@@ -167,11 +203,14 @@ static double eqjoinsel_semi(FmgrInfo *eqproc, Oid collation,
 							 bool *hasmatch1, bool *hasmatch2,
 							 RelOptInfo *inner_rel);
 static void eqjoinsel_find_matches(FmgrInfo *eqproc, Oid collation,
+								   Oid hashLeft, Oid hashRight,
 								   bool op_is_reversed,
 								   AttStatsSlot *sslot1, AttStatsSlot *sslot2,
 								   int nvalues1, int nvalues2,
 								   bool *hasmatch1, bool *hasmatch2,
 								   int *p_nmatches, double *p_matchprodfreq);
+static uint32 hash_mcv(McvHashTable_hash *tab, Datum key);
+static bool mcvs_equal(McvHashTable_hash *tab, Datum key0, Datum key1);
 static bool estimate_multivariate_ndistinct(PlannerInfo *root,
 											RelOptInfo *rel, List **varinfos, double *ndistinct);
 static bool convert_to_scalar(Datum value, Oid valuetypid, Oid collid,
@@ -227,6 +266,20 @@ static RelOptInfo *find_join_input_rel(PlannerInfo *root, Relids relids);
 static double btcost_correlation(IndexOptInfo *index,
 								 VariableStatData *vardata);
 
+/* Define support routines for MCV hash tables */
+#define SH_PREFIX				McvHashTable
+#define SH_ELEMENT_TYPE			McvHashEntry
+#define SH_KEY_TYPE				Datum
+#define SH_KEY					value
+#define SH_HASH_KEY(tab,key)	hash_mcv(tab, key)
+#define SH_EQUAL(tab,key0,key1)	mcvs_equal(tab, key0, key1)
+#define SH_SCOPE				static inline
+#define SH_STORE_HASH
+#define SH_GET_HASH(tab,ent)	(ent)->hash
+#define SH_DEFINE
+#define SH_DECLARE
+#include "lib/simplehash.h"
+
 
 /*
  *		eqsel			- Selectivity of "=" for any data types.
@@ -2314,6 +2367,8 @@ eqjoinsel(PG_FUNCTION_ARGS)
 	bool		isdefault2;
 	Oid			opfuncoid;
 	FmgrInfo	eqproc;
+	Oid			hashLeft = InvalidOid;
+	Oid			hashRight = InvalidOid;
 	AttStatsSlot sslot1;
 	AttStatsSlot sslot2;
 	Form_pg_statistic stats1 = NULL;
@@ -2378,12 +2433,20 @@ eqjoinsel(PG_FUNCTION_ARGS)
 		fmgr_info(opfuncoid, &eqproc);
 		hasmatch1 = (bool *) palloc0(sslot1.nvalues * sizeof(bool));
 		hasmatch2 = (bool *) palloc0(sslot2.nvalues * sizeof(bool));
+
+		/*
+		 * If the MCV lists are long enough to justify hashing, try to look up
+		 * hash functions for the join operator.  XXX should this be Max()?
+		 */
+		if (Min(sslot1.nvalues, sslot2.nvalues) >= EQJOINSEL_MCV_HASH_THRESHOLD)
+			(void) get_op_hash_functions(operator, &hashLeft, &hashRight);
 	}
 	else
 		memset(&eqproc, 0, sizeof(eqproc)); /* silence uninit-var warnings */
 
 	/* We need to compute the inner-join selectivity in all cases */
 	selec_inner = eqjoinsel_inner(&eqproc, collation,
+								  hashLeft, hashRight,
 								  &vardata1, &vardata2,
 								  nd1, nd2,
 								  isdefault1, isdefault2,
@@ -2412,6 +2475,7 @@ eqjoinsel(PG_FUNCTION_ARGS)
 
 			if (!join_is_reversed)
 				selec = eqjoinsel_semi(&eqproc, collation,
+									   hashLeft, hashRight,
 									   false,
 									   &vardata1, &vardata2,
 									   nd1, nd2,
@@ -2423,6 +2487,7 @@ eqjoinsel(PG_FUNCTION_ARGS)
 									   inner_rel);
 			else
 				selec = eqjoinsel_semi(&eqproc, collation,
+									   hashLeft, hashRight,
 									   true,
 									   &vardata2, &vardata1,
 									   nd2, nd1,
@@ -2481,6 +2546,7 @@ eqjoinsel(PG_FUNCTION_ARGS)
  */
 static double
 eqjoinsel_inner(FmgrInfo *eqproc, Oid collation,
+				Oid hashLeft, Oid hashRight,
 				VariableStatData *vardata1, VariableStatData *vardata2,
 				double nd1, double nd2,
 				bool isdefault1, bool isdefault2,
@@ -2521,6 +2587,7 @@ eqjoinsel_inner(FmgrInfo *eqproc, Oid collation,
 
 		/* Fill the match arrays */
 		eqjoinsel_find_matches(eqproc, collation,
+							   hashLeft, hashRight,
 							   false,
 							   sslot1, sslot2,
 							   sslot1->nvalues, sslot2->nvalues,
@@ -2635,6 +2702,7 @@ eqjoinsel_inner(FmgrInfo *eqproc, Oid collation,
  */
 static double
 eqjoinsel_semi(FmgrInfo *eqproc, Oid collation,
+			   Oid hashLeft, Oid hashRight,
 			   bool op_is_reversed,
 			   VariableStatData *vardata1, VariableStatData *vardata2,
 			   double nd1, double nd2,
@@ -2725,6 +2793,7 @@ eqjoinsel_semi(FmgrInfo *eqproc, Oid collation,
 			memset(hasmatch2, 0, clamped_nvalues2 * sizeof(bool));
 			/* Re-fill the match arrays */
 			eqjoinsel_find_matches(eqproc, collation,
+								   hashLeft, hashRight,
 								   op_is_reversed,
 								   sslot1, sslot2,
 								   sslot1->nvalues, clamped_nvalues2,
@@ -2799,6 +2868,8 @@ eqjoinsel_semi(FmgrInfo *eqproc, Oid collation,
  * Inputs:
  *	eqproc: FmgrInfo for equality function to use (might be reversed)
  *	collation: OID of collation to use
+ *	hashLeft, hashRight: OIDs of hash functions associated with equality op,
+ *		or InvalidOid if we're not to use hashing
  *	op_is_reversed: indicates that eqproc compares right type to left type
  *	sslot1, sslot2: MCV values for the lefthand and righthand inputs
  *	nvalues1, nvalues2: number of values to be considered (can be less than
@@ -2810,6 +2881,9 @@ eqjoinsel_semi(FmgrInfo *eqproc, Oid collation,
  *	*p_matchprodfreq: receives sum(sslot1->numbers[i] * sslot2->numbers[j])
  *		for matching MCVs
  *
+ * Note that hashLeft is for the eqproc's left-hand input type, hashRight
+ * for its right, regardless of op_is_reversed.
+ *
  * Note we assume that each MCV will match at most one member of the other
  * MCV list.  If the operator isn't really equality, there could be multiple
  * matches --- but we don't look for them, both for speed and because the
@@ -2817,6 +2891,7 @@ eqjoinsel_semi(FmgrInfo *eqproc, Oid collation,
  */
 static void
 eqjoinsel_find_matches(FmgrInfo *eqproc, Oid collation,
+					   Oid hashLeft, Oid hashRight,
 					   bool op_is_reversed,
 					   AttStatsSlot *sslot1, AttStatsSlot *sslot2,
 					   int nvalues1, int nvalues2,
@@ -2837,12 +2912,111 @@ eqjoinsel_find_matches(FmgrInfo *eqproc, Oid collation,
 	fcinfo->args[0].isnull = false;
 	fcinfo->args[1].isnull = false;
 
-	/*
-	 * The reason for this extra level of braces will become apparent later.
-	 * For now, it just prevents having to re-indent this chunk of code moved
-	 * from eqjoinsel_inner.
-	 */
+	if (OidIsValid(hashLeft) && OidIsValid(hashRight))
+	{
+		/* Use a hash table to speed up the matching */
+		LOCAL_FCINFO(hash_fcinfo, 1);
+		FmgrInfo	hash_proc;
+		McvHashContext hashContext;
+		McvHashTable_hash *hashTable;
+		AttStatsSlot *statsProbe;
+		AttStatsSlot *statsHash;
+		bool	   *hasMatchProbe;
+		bool	   *hasMatchHash;
+		int			nvaluesProbe;
+		int			nvaluesHash;
+
+		/* Make sure we build the hash table on the smaller array. */
+		if (sslot1->nvalues >= sslot2->nvalues)
+		{
+			statsProbe = sslot1;
+			statsHash = sslot2;
+			hasMatchProbe = hasmatch1;
+			hasMatchHash = hasmatch2;
+			nvaluesProbe = nvalues1;
+			nvaluesHash = nvalues2;
+		}
+		else
+		{
+			/* We'll have to reverse the direction of use of the operator. */
+			op_is_reversed = !op_is_reversed;
+			statsProbe = sslot2;
+			statsHash = sslot1;
+			hasMatchProbe = hasmatch2;
+			hasMatchHash = hasmatch1;
+			nvaluesProbe = nvalues2;
+			nvaluesHash = nvalues1;
+		}
+
+		/*
+		 * Build the hash table on the smaller array, using the appropriate
+		 * hash function for its data type.
+		 */
+		fmgr_info(op_is_reversed ? hashLeft : hashRight, &hash_proc);
+		InitFunctionCallInfoData(*hash_fcinfo, &hash_proc, 1, collation,
+								 NULL, NULL);
+		hash_fcinfo->args[0].isnull = false;
+
+		hashContext.equal_fcinfo = fcinfo;
+		hashContext.hash_fcinfo = hash_fcinfo;
+		hashContext.op_is_reversed = op_is_reversed;
+		hashContext.insert_mode = true;
+		get_typlenbyval(statsHash->valuetype,
+						&hashContext.hash_typlen,
+						&hashContext.hash_typbyval);
+
+		hashTable = McvHashTable_create(CurrentMemoryContext,
+										nvaluesHash,
+										&hashContext);
+
+		for (int i = 0; i < nvaluesHash; i++)
+		{
+			bool		found = false;
+			McvHashEntry *entry = McvHashTable_insert(hashTable,
+													  statsHash->values[i],
+													  &found);
+
+			Assert(!found);		/* XXX seems possibly unsafe */
+			entry->index = i;
+		}
+
+		/*
+		 * Prepare to probe the hash table.  If the probe values are of a
+		 * different data type, then we need to change hash functions.  (This
+		 * code relies on the assumption that since we defined SH_STORE_HASH,
+		 * simplehash.h will never need to compute hash values for existing
+		 * hash table entries.)
+		 */
+		hashContext.insert_mode = false;
+		if (hashLeft != hashRight)
+		{
+			fmgr_info(op_is_reversed ? hashRight : hashLeft, &hash_proc);
+			/* Resetting hash_fcinfo is probably unnecessary, but be safe */
+			InitFunctionCallInfoData(*hash_fcinfo, &hash_proc, 1, collation,
+									 NULL, NULL);
+			hash_fcinfo->args[0].isnull = false;
+		}
+
+		/* Look up each probe value in turn. */
+		for (int i = 0; i < nvaluesProbe; i++)
+		{
+			McvHashEntry *entry = McvHashTable_lookup(hashTable,
+													  statsProbe->values[i]);
+
+			/* As in the other code path, skip already-matched hash entries */
+			if (entry != NULL && !hasMatchHash[entry->index])
+			{
+				hasMatchHash[entry->index] = hasMatchProbe[i] = true;
+				nmatches++;
+				matchprodfreq += statsHash->numbers[entry->index] * statsProbe->numbers[i];
+			}
+		}
+
+		McvHashTable_destroy(hashTable);
+	}
+	else
 	{
+		/* We're not to use hashing, so do it the O(N^2) way */
 		int			index1,
 					index2;
 
@@ -2886,6 +3060,67 @@ eqjoinsel_find_matches(FmgrInfo *eqproc, Oid collation,
 	*p_matchprodfreq = matchprodfreq;
 }
 
+/*
+ * Support functions for the hash tables used by eqjoinsel_find_matches
+ */
+static uint32
+hash_mcv(McvHashTable_hash *tab, Datum key)
+{
+	McvHashContext *context = (McvHashContext *) tab->private_data;
+	FunctionCallInfo fcinfo = context->hash_fcinfo;
+	Datum		fresult;
+
+	fcinfo->args[0].value = key;
+	fcinfo->isnull = false;
+	fresult = FunctionCallInvoke(fcinfo);
+	Assert(!fcinfo->isnull);
+	return DatumGetUInt32(fresult);
+}
+
+static bool
+mcvs_equal(McvHashTable_hash *tab, Datum key0, Datum key1)
+{
+	McvHashContext *context = (McvHashContext *) tab->private_data;
+
+	if (context->insert_mode)
+	{
+		/*
+		 * During the insertion step, any comparisons will be between two
+		 * Datums of the hash table's data type, so if the given operator is
+		 * cross-type it will be the wrong thing to use.  Fortunately, we can
+		 * use datum_image_eq instead.  The MCV values should all be distinct
+		 * anyway, so it's mostly pro-forma to compare them at all.
+		 */
+		return datum_image_eq(key0, key1,
+							  context->hash_typbyval, context->hash_typlen);
+	}
+	else
+	{
+		FunctionCallInfo fcinfo = context->equal_fcinfo;
+		Datum		fresult;
+
+		/*
+		 * Apply the operator the correct way around.  Although simplehash.h
+		 * doesn't document this explicitly, during lookups key0 is from the
+		 * hash table while key1 is the probe value, so we should compare them
+		 * in that order only if op_is_reversed.
+		 */
+		if (context->op_is_reversed)
+		{
+			fcinfo->args[0].value = key0;
+			fcinfo->args[1].value = key1;
+		}
+		else
+		{
+			fcinfo->args[0].value = key1;
+			fcinfo->args[1].value = key0;
+		}
+		fcinfo->isnull = false;
+		fresult = FunctionCallInvoke(fcinfo);
+		return (!fcinfo->isnull && DatumGetBool(fresult));
+	}
+}
+
 /*
  *		neqjoinsel		- Join selectivity of "!="
  */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 23bce72ae64..ac5c6ba9833 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1666,6 +1666,9 @@ ManyTestResourceKind
 Material
 MaterialPath
 MaterialState
+McvHashContext
+McvHashEntry
+McvHashTable_hash
 MdPathStr
 MdfdVec
 Memoize
-- 
2.43.7

