From 0e07ad2850d2c0b1ea65649a80e38ca937b72876 Mon Sep 17 00:00:00 2001
From: Andrey Borodin <amborodin@acm.org>
Date: Tue, 4 Aug 2026 22:45:57 +0500
Subject: [PATCH v1 2/2] Use interpolation for specialized int4 page searches

Even after avoiding fmgr and tuple deformation, binary search must read
several tuples from each B-tree page.  Fixed-width integer keys often allow
the likely boundary to be found directly from the first and last page keys.

For sufficiently large searches that do not need cached insertion bounds,
estimate the boundary by linear interpolation and verify it against the
adjacent tuple.  Accept the estimate only when those two comparisons prove
the exact lower or upper bound.  Otherwise continue with the specialized
binary search using the bound established by the probe.  This preserves
correctness for skewed keys, duplicates, NULLs, and both ascending and
descending indexes without making the key distribution part of the contract.

Add regression coverage for all five strategies, including duplicates,
large gaps, int4 limits, and descending indexes.
---
 src/backend/access/nbtree/nbtcompare.c    | 87 +++++++++++++++++++++--
 src/test/regress/expected/btree_index.out | 57 ++++++++++++++-
 src/test/regress/sql/btree_index.sql      | 49 ++++++++++++-
 3 files changed, 182 insertions(+), 11 deletions(-)

diff --git a/src/backend/access/nbtree/nbtcompare.c b/src/backend/access/nbtree/nbtcompare.c
index 895b42c8f00..a0bd8431338 100644
--- a/src/backend/access/nbtree/nbtcompare.c
+++ b/src/backend/access/nbtree/nbtcompare.c
@@ -205,6 +205,26 @@ btint4cmp(PG_FUNCTION_ARGS)
 		PG_RETURN_INT32(A_LESS_THAN_B);
 }
 
+static inline bool
+btint4tuplevalue(IndexTuple itup, int32 *value)
+{
+	if (IndexTupleHasNulls(itup))
+		return false;
+
+	/* int4 is the first and only key, so its offset is fixed. */
+	memcpy(value, (char *) itup + sizeof(IndexTupleData), sizeof(*value));
+	return true;
+}
+
+static inline bool
+btint4pagevalue(Page page, OffsetNumber offnum, int32 *value)
+{
+	IndexTuple	itup;
+
+	itup = (IndexTuple) PageGetItem(page, PageGetItemId(page, offnum));
+	return btint4tuplevalue(itup, value);
+}
+
 static bool
 btint4pagecmp(Relation rel, BTScanInsert key, Page page, OffsetNumber offnum,
 			  int32 *result)
@@ -212,6 +232,7 @@ btint4pagecmp(Relation rel, BTScanInsert key, Page page, OffsetNumber offnum,
 	TupleDesc	tupdesc = RelationGetDescr(rel);
 	ScanKey		skey = key->scankeys;
 	int32		search = DatumGetInt32(skey->sk_argument);
+	int32		value;
 	ItemId		itemid = PageGetItemId(page, offnum);
 	IndexTuple	itup;
 	ItemPointer heapTid;
@@ -220,12 +241,8 @@ btint4pagecmp(Relation rel, BTScanInsert key, Page page, OffsetNumber offnum,
 
 	itup = (IndexTuple) PageGetItem(page, itemid);
 
-	if (likely(!IndexTupleHasNulls(itup)))
+	if (likely(btint4tuplevalue(itup, &value)))
 	{
-		int32		value;
-
-		/* int4 is the first and only key, so its offset is fixed. */
-		memcpy(&value, (char *) itup + sizeof(IndexTupleData), sizeof(value));
 		datum = Int32GetDatum(value);
 		isnull = false;
 	}
@@ -274,6 +291,64 @@ btint4binsearch_uncached(Relation rel, BTScanInsert key, Page page,
 						 OffsetNumber low, OffsetNumber high, int32 cmpval,
 						 OffsetNumber *resultoff)
 {
+	/*
+	 * Interpolation can find the boundary with at most four key reads.  Avoid
+	 * its division overhead when binary search is already about that short.
+	 */
+	if (high - low >= 32)
+	{
+		int32		lowval;
+		int32		highval;
+
+		if (btint4pagevalue(page, low, &lowval) &&
+			btint4pagevalue(page, high - 1, &highval) &&
+			lowval != highval)
+		{
+			int32		search = DatumGetInt32(key->scankeys->sk_argument);
+			bool		desc = key->scankeys->sk_flags & SK_BT_DESC;
+			int64		span = desc ? (int64) lowval - highval :
+				(int64) highval - lowval;
+			int64		delta = desc ? (int64) lowval - search :
+				(int64) search - lowval;
+
+			if (span > 0 && delta >= 0 && delta <= span)
+			{
+				OffsetNumber probe = low +
+					(delta * (high - low - 1)) / span;
+				OffsetNumber neighbor;
+				int32		result;
+				bool		advance;
+				bool		neighbor_advance;
+
+				if (!btint4pagecmp(rel, key, page, probe, &result))
+					return false;
+				advance = result >= cmpval;
+				if ((advance && OffsetNumberNext(probe) == high) ||
+					(!advance && probe == low))
+				{
+					*resultoff = advance ? high : low;
+					return true;
+				}
+
+				neighbor = advance ? OffsetNumberNext(probe) :
+					OffsetNumberPrev(probe);
+				if (!btint4pagecmp(rel, key, page, neighbor, &result))
+					return false;
+				neighbor_advance = result >= cmpval;
+				if (advance != neighbor_advance)
+				{
+					*resultoff = advance ? neighbor : probe;
+					return true;
+				}
+
+				if (advance)
+					low = OffsetNumberNext(neighbor);
+				else
+					high = probe;
+			}
+		}
+	}
+
 	while (high > low)
 	{
 		OffsetNumber mid = low + ((high - low) / 2);
@@ -282,8 +357,6 @@ btint4binsearch_uncached(Relation rel, BTScanInsert key, Page page,
 
 		if (!btint4pagecmp(rel, key, page, mid, &result))
 			return false;
-		if (unlikely(result == 0 && key->scantid != NULL))
-			return false;
 
 		advance = result >= cmpval;
 		low = advance ? mid + 1 : low;
diff --git a/src/test/regress/expected/btree_index.out b/src/test/regress/expected/btree_index.out
index 6bfc63e1a5c..5e21b1a70cd 100644
--- a/src/test/regress/expected/btree_index.out
+++ b/src/test/regress/expected/btree_index.out
@@ -908,11 +908,15 @@ CREATE TABLE btree_binsearch_control (k int4, v int4);
 INSERT INTO btree_binsearch_fast
 SELECT CASE WHEN g % 101 = 0 THEN NULL ELSE g % 257 - 128 END, g
 FROM generate_series(1, 20000) g;
+INSERT INTO btree_binsearch_fast
+SELECT g * 1000000, -10000 + g FROM generate_series(-2000, 2000) g;
+INSERT INTO btree_binsearch_fast VALUES
+  ('-2147483648', -20001), (2147483647, -20002);
 INSERT INTO btree_binsearch_control SELECT * FROM btree_binsearch_fast;
 CREATE INDEX btree_binsearch_fast_idx ON btree_binsearch_fast
-  (k DESC NULLS FIRST) WITH (deduplicate_items = on);
+  (k) WITH (deduplicate_items = on);
 CREATE INDEX btree_binsearch_control_idx ON btree_binsearch_control
-  (k btree_no_binsearch_ops DESC NULLS FIRST);
+  (k btree_no_binsearch_ops);
 -- Exercise insertion into a multilevel index, including duplicate keys.
 INSERT INTO btree_binsearch_fast
 SELECT g % 31 - 15, 20000 + g FROM generate_series(1, 2000) g;
@@ -957,6 +961,55 @@ SELECT
  t
 (1 row)
 
+-- Compare exact boundary positions across duplicates, gaps, and int4 limits.
+WITH q(k) AS
+  (SELECT g::int4 FROM generate_series(-2000000000::bigint,
+                                       2000000000::bigint, 100000000) g
+   UNION ALL VALUES ('-2147483648'), (-129), (-128), (-1), (0), (1),
+                    (128), (129), (2147483647))
+SELECT bool_and(
+  ARRAY(SELECT v FROM btree_binsearch_fast
+        WHERE btree_binsearch_fast.k = q.k ORDER BY v) =
+  ARRAY(SELECT v FROM btree_binsearch_control
+        WHERE btree_binsearch_control.k = q.k ORDER BY v) AND
+  ARRAY(SELECT v FROM btree_binsearch_fast
+        WHERE btree_binsearch_fast.k >= q.k ORDER BY k, v LIMIT 10) =
+  ARRAY(SELECT v FROM btree_binsearch_control
+        WHERE btree_binsearch_control.k >= q.k ORDER BY k, v LIMIT 10) AND
+  ARRAY(SELECT v FROM btree_binsearch_fast
+        WHERE btree_binsearch_fast.k > q.k ORDER BY k, v LIMIT 10) =
+  ARRAY(SELECT v FROM btree_binsearch_control
+        WHERE btree_binsearch_control.k > q.k ORDER BY k, v LIMIT 10) AND
+  ARRAY(SELECT v FROM btree_binsearch_fast
+        WHERE btree_binsearch_fast.k <= q.k ORDER BY k DESC, v LIMIT 10) =
+  ARRAY(SELECT v FROM btree_binsearch_control
+        WHERE btree_binsearch_control.k <= q.k ORDER BY k DESC, v LIMIT 10) AND
+  ARRAY(SELECT v FROM btree_binsearch_fast
+        WHERE btree_binsearch_fast.k < q.k ORDER BY k DESC, v LIMIT 10) =
+  ARRAY(SELECT v FROM btree_binsearch_control
+        WHERE btree_binsearch_control.k < q.k ORDER BY k DESC, v LIMIT 10))
+FROM q;
+ bool_and 
+----------
+ t
+(1 row)
+
+-- Exercise the same boundary rules with descending physical key order.
+DROP INDEX btree_binsearch_fast_idx, btree_binsearch_control_idx;
+CREATE INDEX btree_binsearch_fast_idx ON btree_binsearch_fast
+  (k DESC NULLS FIRST);
+CREATE INDEX btree_binsearch_control_idx ON btree_binsearch_control
+  (k btree_no_binsearch_ops DESC NULLS FIRST);
+SELECT
+  (SELECT array_agg(v ORDER BY v) FROM btree_binsearch_fast
+   WHERE k >= -128 AND k <= 128) =
+  (SELECT array_agg(v ORDER BY v) FROM btree_binsearch_control
+   WHERE k >= -128 AND k <= 128);
+ ?column? 
+----------
+ t
+(1 row)
+
 -- Check both scan directions across many leaf pages.
 SELECT
   (SELECT array_agg(k) FROM
diff --git a/src/test/regress/sql/btree_index.sql b/src/test/regress/sql/btree_index.sql
index b9104dace4b..9e3ddc5b71e 100644
--- a/src/test/regress/sql/btree_index.sql
+++ b/src/test/regress/sql/btree_index.sql
@@ -540,11 +540,15 @@ CREATE TABLE btree_binsearch_control (k int4, v int4);
 INSERT INTO btree_binsearch_fast
 SELECT CASE WHEN g % 101 = 0 THEN NULL ELSE g % 257 - 128 END, g
 FROM generate_series(1, 20000) g;
+INSERT INTO btree_binsearch_fast
+SELECT g * 1000000, -10000 + g FROM generate_series(-2000, 2000) g;
+INSERT INTO btree_binsearch_fast VALUES
+  ('-2147483648', -20001), (2147483647, -20002);
 INSERT INTO btree_binsearch_control SELECT * FROM btree_binsearch_fast;
 CREATE INDEX btree_binsearch_fast_idx ON btree_binsearch_fast
-  (k DESC NULLS FIRST) WITH (deduplicate_items = on);
+  (k) WITH (deduplicate_items = on);
 CREATE INDEX btree_binsearch_control_idx ON btree_binsearch_control
-  (k btree_no_binsearch_ops DESC NULLS FIRST);
+  (k btree_no_binsearch_ops);
 
 -- Exercise insertion into a multilevel index, including duplicate keys.
 INSERT INTO btree_binsearch_fast
@@ -573,6 +577,47 @@ SELECT
   (SELECT array_agg(v ORDER BY v) FROM btree_binsearch_control
    WHERE k >= -10 AND k < 10);
 
+-- Compare exact boundary positions across duplicates, gaps, and int4 limits.
+WITH q(k) AS
+  (SELECT g::int4 FROM generate_series(-2000000000::bigint,
+                                       2000000000::bigint, 100000000) g
+   UNION ALL VALUES ('-2147483648'), (-129), (-128), (-1), (0), (1),
+                    (128), (129), (2147483647))
+SELECT bool_and(
+  ARRAY(SELECT v FROM btree_binsearch_fast
+        WHERE btree_binsearch_fast.k = q.k ORDER BY v) =
+  ARRAY(SELECT v FROM btree_binsearch_control
+        WHERE btree_binsearch_control.k = q.k ORDER BY v) AND
+  ARRAY(SELECT v FROM btree_binsearch_fast
+        WHERE btree_binsearch_fast.k >= q.k ORDER BY k, v LIMIT 10) =
+  ARRAY(SELECT v FROM btree_binsearch_control
+        WHERE btree_binsearch_control.k >= q.k ORDER BY k, v LIMIT 10) AND
+  ARRAY(SELECT v FROM btree_binsearch_fast
+        WHERE btree_binsearch_fast.k > q.k ORDER BY k, v LIMIT 10) =
+  ARRAY(SELECT v FROM btree_binsearch_control
+        WHERE btree_binsearch_control.k > q.k ORDER BY k, v LIMIT 10) AND
+  ARRAY(SELECT v FROM btree_binsearch_fast
+        WHERE btree_binsearch_fast.k <= q.k ORDER BY k DESC, v LIMIT 10) =
+  ARRAY(SELECT v FROM btree_binsearch_control
+        WHERE btree_binsearch_control.k <= q.k ORDER BY k DESC, v LIMIT 10) AND
+  ARRAY(SELECT v FROM btree_binsearch_fast
+        WHERE btree_binsearch_fast.k < q.k ORDER BY k DESC, v LIMIT 10) =
+  ARRAY(SELECT v FROM btree_binsearch_control
+        WHERE btree_binsearch_control.k < q.k ORDER BY k DESC, v LIMIT 10))
+FROM q;
+
+-- Exercise the same boundary rules with descending physical key order.
+DROP INDEX btree_binsearch_fast_idx, btree_binsearch_control_idx;
+CREATE INDEX btree_binsearch_fast_idx ON btree_binsearch_fast
+  (k DESC NULLS FIRST);
+CREATE INDEX btree_binsearch_control_idx ON btree_binsearch_control
+  (k btree_no_binsearch_ops DESC NULLS FIRST);
+SELECT
+  (SELECT array_agg(v ORDER BY v) FROM btree_binsearch_fast
+   WHERE k >= -128 AND k <= 128) =
+  (SELECT array_agg(v ORDER BY v) FROM btree_binsearch_control
+   WHERE k >= -128 AND k <= 128);
+
 -- Check both scan directions across many leaf pages.
 SELECT
   (SELECT array_agg(k) FROM
-- 
2.50.1 (Apple Git-155)

