From 1722ece7f754b136d481737dc05c18695d039c32 Mon Sep 17 00:00:00 2001
From: Khoa Nguyen <kdnguyen9.oss@gmail.com>
Date: Wed, 9 Sep 2026 20:04:19 -0700
Subject: [PATCH] Add hash_build_sort_mode developer GUC to force the hash
 build sort path.

hashbuild() sorts index tuples by bucket number only when the initial
index size exceeds maintenance_work_mem or the number of usable buffers,
whichever is less.  Measuring whether that sort is actually useless on
small indexes requires running both paths at a fixed row count and a
fixed sort memory budget, which the existing knobs cannot do:
maintenance_work_mem selects the path and sizes the sort at the same
time.

Add a developer-only enum GUC, hash_build_sort_mode, with values auto
(default, historical behavior), on, and off.  Marked DEVELOPER_OPTIONS
and GUC_NOT_IN_SAMPLE.

Benchmarking test only; not intended for submission.
---
 src/backend/access/hash/hash.c            | 44 ++++++++++++++++++++++-
 src/backend/utils/misc/guc_parameters.dat |  9 +++++
 src/backend/utils/misc/guc_tables.c       |  8 +++++
 src/include/access/hash.h                 | 16 +++++++++
 4 files changed, 76 insertions(+), 1 deletion(-)

diff --git a/src/backend/access/hash/hash.c b/src/backend/access/hash/hash.c
index b2e34d2..a88fcfb 100644
--- a/src/backend/access/hash/hash.c
+++ b/src/backend/access/hash/hash.c
@@ -61,6 +61,13 @@ static BlockNumber hash_bulkdelete_read_stream_cb(ReadStream *stream,
 												  void *callback_private_data,
 												  void *per_buffer_data);
 
+/*
+ * Developer GUC: override the automatic choice of whether hashbuild() sorts
+ * index tuples by bucket number.  Defaults to HASH_BUILD_SORT_AUTO, which
+ * preserves the historical behavior.  See hashbuild().
+ */
+int			hash_build_sort_mode = HASH_BUILD_SORT_AUTO;
+
 
 /*
  * Hash handler function: return IndexAmRoutine with access method parameters
@@ -140,6 +147,7 @@ hashbuild(Relation heap, Relation index, IndexInfo *indexInfo)
 	double		allvisfrac;
 	uint32		num_buckets;
 	Size		sort_threshold;
+	bool		do_sort;
 	HashBuildState buildstate;
 
 	/*
@@ -173,6 +181,12 @@ hashbuild(Relation heap, Relation index, IndexInfo *indexInfo)
 	 * NOTE: this test will need adjustment if a bucket is ever different from
 	 * one page.  Also, "initial index size" accounting does not include the
 	 * metapage, nor the first bitmap page.
+	 *
+	 * The hash_build_sort_mode developer GUC can override this decision, so
+	 * that the sorted and unsorted paths can be compared at a fixed row count
+	 * and a fixed maintenance_work_mem.  Without it, the only way to reach the
+	 * unsorted path is to raise maintenance_work_mem, which also changes how
+	 * much memory the sort itself gets, confounding the comparison.
 	 */
 	sort_threshold = (maintenance_work_mem * (Size) 1024) / BLCKSZ;
 	if (index->rd_rel->relpersistence != RELPERSISTENCE_TEMP)
@@ -180,7 +194,21 @@ hashbuild(Relation heap, Relation index, IndexInfo *indexInfo)
 	else
 		sort_threshold = Min(sort_threshold, NLocBuffer);
 
-	if (num_buckets >= sort_threshold)
+	switch (hash_build_sort_mode)
+	{
+		case HASH_BUILD_SORT_ON:
+			do_sort = true;
+			break;
+		case HASH_BUILD_SORT_OFF:
+			do_sort = false;
+			break;
+		case HASH_BUILD_SORT_AUTO:
+		default:
+			do_sort = (num_buckets >= sort_threshold);
+			break;
+	}
+
+	if (do_sort)
 		buildstate.spool = _h_spoolinit(heap, index, num_buckets);
 	else
 		buildstate.spool = NULL;
diff --git a/src/backend/utils/misc/guc_parameters.dat b/src/backend/utils/misc/guc_parameters.dat
index 3c5e16a..21b9934 100644
--- a/src/backend/utils/misc/guc_parameters.dat
+++ b/src/backend/utils/misc/guc_parameters.dat
@@ -1217,6 +1217,15 @@
   boot_val => 'false',
 },
 
+{ name => 'hash_build_sort_mode', type => 'enum', context => 'PGC_USERSET', group => 'DEVELOPER_OPTIONS',
+  short_desc => 'Forces or disables the sort step during hash index builds.',
+  long_desc => 'The default, auto, sorts only when the initial index size exceeds "maintenance_work_mem" or the number of usable buffers, whichever is less.',
+  flags => 'GUC_NOT_IN_SAMPLE',
+  variable => 'hash_build_sort_mode',
+  boot_val => 'HASH_BUILD_SORT_AUTO',
+  options => 'hash_build_sort_mode_options',
+},
+
 { name => 'hash_mem_multiplier', type => 'real', context => 'PGC_USERSET', group => 'RESOURCES_MEM',
   short_desc => 'Multiple of "work_mem" to use for hash tables.',
   flags => 'GUC_EXPLAIN',
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index c6d9b2a..3385a82 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -31,6 +31,7 @@
 
 #include "access/commit_ts.h"
 #include "access/gin.h"
+#include "access/hash.h"
 #include "access/slru.h"
 #include "access/toast_compression.h"
 #include "access/twophase.h"
@@ -520,6 +521,13 @@ static const struct config_enum_entry data_checksums_options[] = {
 	{NULL, 0, false}
 };
 
+static const struct config_enum_entry hash_build_sort_mode_options[] = {
+	{"auto", HASH_BUILD_SORT_AUTO, false},
+	{"on", HASH_BUILD_SORT_ON, false},
+	{"off", HASH_BUILD_SORT_OFF, false},
+	{NULL, 0, false}
+};
+
 /*
  * Options for enum values stored in other modules
  */
diff --git a/src/include/access/hash.h b/src/include/access/hash.h
index a8702f0..c965c4e 100644
--- a/src/include/access/hash.h
+++ b/src/include/access/hash.h
@@ -358,6 +358,22 @@ typedef struct HashOptions
 #define HASHNProcs				3
 
 
+/*
+ * Values for the hash_build_sort_mode developer GUC, which overrides the
+ * automatic choice of whether hashbuild() sorts index tuples by bucket
+ * number.  HASH_BUILD_SORT_AUTO must be the boot value: it leaves the
+ * historical maintenance_work_mem/NBuffers test in force.
+ */
+typedef enum HashBuildSortMode
+{
+	HASH_BUILD_SORT_AUTO = 0,
+	HASH_BUILD_SORT_ON,
+	HASH_BUILD_SORT_OFF,
+} HashBuildSortMode;
+
+extern PGDLLIMPORT int hash_build_sort_mode;
+
+
 /* public routines */
 
 extern IndexBuildResult *hashbuild(Relation heap, Relation index,
-- 
2.39.5

