From 33d1c09d41faa35e6a10a1fe55cd09d069b85e53 Mon Sep 17 00:00:00 2001
From: Matthias van de Meent <boekewurm+postgres@gmail.com>
Date: Sun, 29 Oct 2023 21:39:23 +0100
Subject: [PATCH v14 2/7] btree: optimize _bt_moveright using downlink's right
 separator

Due to the inner workings of the btree, it is extremely likely that the
right separator of the btree page's downlink on the parent page matches
the downlinked page's HIKEY: Only when the page was split after we
accessed its parent page (and this split wasn't completed yet) the HIKEY
will not match.

So, instead of doing _bt_compare in _bt_moveright, we can store the
right separator key of the downlink, and check if it matches the HIKEY of
the linked page. If they match, we don't have to call the relatively
expensive _bt_compare, which allows us to reuse the _bt_compare result of
the right separator key.

This reduces the average number of all-attribute _bt_compare operations
we need to do on a page from 3 to 2, thus further increasing performance
of indexes that have expensive compare operations.
---
 src/backend/access/nbtree/README      | 25 +++++++-
 src/backend/access/nbtree/nbtsearch.c | 83 +++++++++++++++++++++++----
 2 files changed, 96 insertions(+), 12 deletions(-)

diff --git a/src/backend/access/nbtree/README b/src/backend/access/nbtree/README
index 61601aca61..0aeee328f1 100644
--- a/src/backend/access/nbtree/README
+++ b/src/backend/access/nbtree/README
@@ -933,7 +933,30 @@ that has no "wasteful" prefix that we can skip.
 With the above optimizations, dynamic prefix truncation improves the worst
 case complexity of indexing from O(tree_height * natts * log(tups_per_page))
 to O(tree_height * (3*natts + log(tups_per_page))) attribute compare
-operations, which can improve performance significantly.
+operations, which can improve performance significantly.  If we also take
+the downlink right separator/HIKEY moveright optimization into account, that
+is further reduced to O(tree_height * (2*natts + log(tups_per_page)))
+attribute compares, which further improves performance.
+
+Notes about the right separator/HIKEY moveright optimization
+------------------------------------------------------------
+
+Page splits consistently cause the HIKEY of the split page to be inserted
+into the parent page as the right separator of the page's downlink (or,
+as the split page's new right sibling's left link), with the only
+difference between the HIKEY and the separator being the contents of the
+IndexTupleData struct of these tuples: the payloads are bit-identical.
+This allows us to reuse the _bt_compare result of the right separator key
+for the downlinked page's HIKEY, if we can determine that those are indeed
+bit-identical: Concurrent page splits and deletions may have caused the
+downlinked page to get a different HIKEY, which could have a different
+_bt_compare result.  To make this work, in _bt_search we cache the
+current downlink's right separator key, to then in the _bt_moveright
+phase of the layer below use memcmp() to validate our assumptions about
+the HIKEY matching the downlink's right separator key.  Only if the
+assumption is proven wrong (memcmp(HIKEY, parent_rsep) != 0), we call
+_bt_compare(), otherwise we can be certain that the parent page's result
+is unchanged, i.e. that _bt_compare would return "<".
 
 Notes about deduplication
 -------------------------
diff --git a/src/backend/access/nbtree/nbtsearch.c b/src/backend/access/nbtree/nbtsearch.c
index 71fd658d15..6dac8169a5 100644
--- a/src/backend/access/nbtree/nbtsearch.c
+++ b/src/backend/access/nbtree/nbtsearch.c
@@ -26,10 +26,12 @@
 
 
 static void _bt_drop_lock_and_maybe_pin(IndexScanDesc scan, BTScanPos sp);
-static OffsetNumber _bt_binsrch(Relation rel, BTScanInsert key, Buffer buf);
+static OffsetNumber _bt_binsrch(Relation rel, BTScanInsert key, Buffer buf,
+								AttrNumber *rsepcmpcol);
 static Buffer _bt_moveright(Relation rel, Relation heaprel, BTScanInsert key,
 							Buffer buf, bool forupdate, BTStack stack,
-							int access, AttrNumber *prefix);
+							int access, AttrNumber *prefix,
+							IndexTuple rightsep);
 static int	_bt_binsrch_posting(BTScanInsert key, Page page,
 								OffsetNumber offnum);
 static bool _bt_readpage(IndexScanDesc scan, ScanDirection dir,
@@ -101,6 +103,12 @@ _bt_search(Relation rel, Relation heaprel, BTScanInsert key, Buffer *bufP,
 {
 	BTStack		stack_in = NULL;
 	int			page_access = BT_READ;
+	union {
+		char	data[MAXALIGN_DOWN(BLCKSZ / 3)];
+		IndexTupleData tuple;
+		double	_align;
+	}			rsepbuf;
+	IndexTuple	rightsep = NULL;
 	AttrNumber	highkeyprefix = 1;
 
 	/* heaprel must be set whenever _bt_allocbuf is reachable */
@@ -138,7 +146,7 @@ _bt_search(Relation rel, Relation heaprel, BTScanInsert key, Buffer *bufP,
 		 * opportunity to finish splits of internal pages too.
 		 */
 		*bufP = _bt_moveright(rel, heaprel, key, *bufP, (access == BT_WRITE),
-							  stack_in, page_access, &highkeyprefix);
+							  stack_in, page_access, &highkeyprefix, rightsep);
 
 		/* if this is a leaf page, we're done */
 		page = BufferGetPage(*bufP);
@@ -150,12 +158,31 @@ _bt_search(Relation rel, Relation heaprel, BTScanInsert key, Buffer *bufP,
 		 * Find the appropriate pivot tuple on this page.  Its downlink points
 		 * to the child page that we're about to descend to.
 		 */
-		offnum = _bt_binsrch(rel, key, *bufP);
+		offnum = _bt_binsrch(rel, key, *bufP, &highkeyprefix);
 		itemid = PageGetItemId(page, offnum);
 		itup = (IndexTuple) PageGetItem(page, itemid);
 		Assert(BTreeTupleIsPivot(itup) || !key->heapkeyspace);
 		child = BTreeTupleGetDownLink(itup);
 
+		if (offnum < PageGetMaxOffsetNumber(page))
+		{
+			ItemId	rightsepitem = PageGetItemId(page, offnum + 1);
+			IndexTuple pagerightsep = (IndexTuple) PageGetItem(page, rightsepitem);
+			memcpy(rsepbuf.data, pagerightsep, ItemIdGetLength(rightsepitem));
+			rightsep = &rsepbuf.tuple;
+		}
+		else if (!P_RIGHTMOST(opaque))
+		{
+			/*
+			 * The rightmost data tuple on inner page has P_HIKEY as its right
+			 * separator.
+			 */
+			ItemId	rightsepitem = PageGetItemId(page, P_HIKEY);
+			IndexTuple pagerightsep = (IndexTuple) PageGetItem(page, rightsepitem);
+			memcpy(rsepbuf.data, pagerightsep, ItemIdGetLength(rightsepitem));
+			rightsep = &rsepbuf.tuple;
+		}
+
 		/*
 		 * We need to save the location of the pivot tuple we chose in a new
 		 * stack entry for this page/level.  If caller ends up splitting a
@@ -201,7 +228,7 @@ _bt_search(Relation rel, Relation heaprel, BTScanInsert key, Buffer *bufP,
 		 * move right to its new sibling.  Do that.
 		 */
 		*bufP = _bt_moveright(rel, heaprel, key, *bufP, true, stack_in, BT_WRITE,
-							  &highkeyprefix);
+							  &highkeyprefix, rightsep);
 	}
 
 	return stack_in;
@@ -248,7 +275,8 @@ _bt_moveright(Relation rel,
 			  bool forupdate,
 			  BTStack stack,
 			  int access,
-			  AttrNumber *prefix)
+			  AttrNumber *prefix,
+			  IndexTuple rightsep)
 {
 	Page		page;
 	BTPageOpaque opaque;
@@ -327,8 +355,30 @@ _bt_moveright(Relation rel,
 			continue;
 		}
 
-		if (P_IGNORE(opaque) ||
-			_bt_compare(rel, key, page, P_HIKEY, &prefixatt) >= cmpval)
+		if (P_IGNORE(opaque))
+		{
+			buf = _bt_relandgetbuf(rel, buf, opaque->btpo_next, access);
+			continue;
+		}
+
+		if (PointerIsValid(rightsep))
+		{
+			ItemId	hikeyid = PageGetItemId(page, P_HIKEY);
+			IndexTuple highkey = (IndexTuple) PageGetItem(page, hikeyid);
+
+			if (ItemIdGetLength(hikeyid) == IndexTupleSize(rightsep) &&
+				memcmp(&highkey[1], &rightsep[1],
+					   IndexTupleSize(rightsep) - sizeof(IndexTupleData)) == 0)
+			{
+				/*
+				 * *comparecol contains the _bt_compare(... &comparecol)
+				 * result for rightsep, no need to update it
+				 */
+				break;
+			}
+		}
+
+		if (_bt_compare(rel, key, page, P_HIKEY, &prefixatt) >= cmpval)
 		{
 			/* set the compare column when breaking out of the loop */
 			buf = _bt_relandgetbuf(rel, buf, opaque->btpo_next, access);
@@ -372,7 +422,8 @@ _bt_moveright(Relation rel,
 static OffsetNumber
 _bt_binsrch(Relation rel,
 			BTScanInsert key,
-			Buffer buf)
+			Buffer buf,
+			AttrNumber *rsepcmpcol)
 {
 	Page		page;
 	BTPageOpaque opaque;
@@ -385,7 +436,7 @@ _bt_binsrch(Relation rel,
 	 * "highkeyprefix" is the value for this page's high key (if any) or 1
 	 * (no established shared prefix)
 	 */
-	AttrNumber	highkeyprefix = 1,
+	AttrNumber	highkeyprefix = *rsepcmpcol,
 				lowkeyprefix = 1;
 
 	page = BufferGetPage(buf);
@@ -407,7 +458,11 @@ _bt_binsrch(Relation rel,
 	 * never empty (an internal page must have children).
 	 */
 	if (unlikely(high < low))
+	{
+		/* Prefixes don't make much sense in empty pages, so just reset it */
+		*rsepcmpcol = 1;
 		return low;
+	}
 
 	/*
 	 * Binary search to find the first key on the page >= scan key, or first
@@ -450,6 +505,8 @@ _bt_binsrch(Relation rel,
 		}
 	}
 
+	*rsepcmpcol = highkeyprefix;
+
 	/*
 	 * At this point we have high == low, but be careful: they could point
 	 * past the last slot on the page.
@@ -1482,7 +1539,11 @@ _bt_first(IndexScanDesc scan, ScanDirection dir)
 	_bt_initialize_more_data(so, dir);
 
 	/* position to the precise item on the page */
-	offnum = _bt_binsrch(rel, &inskey, buf);
+	{
+		/* bt_search doesn't  */
+		AttrNumber prefix = 1;
+		offnum = _bt_binsrch(rel, &inskey, buf, &prefix);
+	}
 
 	/*
 	 * If nextkey = false, we are positioned at the first item >= scan key, or
-- 
2.40.1

