v14-0001-Refactor-nbtree-insertion-scankeys-HEIKKI-comments.patch

text/x-patch

Filename: v14-0001-Refactor-nbtree-insertion-scankeys-HEIKKI-comments.patch
Type: text/x-patch
Part: 0
Message: Re: Making all nbtree entries unique by having heap TIDs participate in comparisons

Patch

Format: unified
Series: patch v14-0001
File+
src/backend/access/nbtree/nbtinsert.c 142 58
src/backend/access/nbtree/nbtsearch.c 10 9
src/backend/access/nbtree/nbtutils.c 2 3
src/backend/access/nbtree/README 3 0
src/include/access/nbtree.h 9 4
diff --git a/src/backend/access/nbtree/README b/src/backend/access/nbtree/README
index 3680e69b89a..eb4df2ebbe6 100644
--- a/src/backend/access/nbtree/README
+++ b/src/backend/access/nbtree/README
@@ -609,6 +609,9 @@ original search scankey is consulted as each index entry is sequentially
 scanned to decide whether to return the entry and whether the scan can
 stop (see _bt_checkkeys()).
 
+HEIKKI: The above probably needs some updating, now that we have a
+separate BTScanInsert struct to represent an insertion scan key.
+
 We use term "pivot" index tuples to distinguish tuples which don't point
 to heap tuples, but rather used for tree navigation.  Pivot tuples includes
 all tuples on non-leaf pages and high keys on leaf pages.  Note that pivot
diff --git a/src/backend/access/nbtree/nbtinsert.c b/src/backend/access/nbtree/nbtinsert.c
index b3fbba276dd..2a2d6576060 100644
--- a/src/backend/access/nbtree/nbtinsert.c
+++ b/src/backend/access/nbtree/nbtinsert.c
@@ -97,9 +97,12 @@ static void _bt_vacuum_one_page(Relation rel, Buffer buffer, Relation heapRel);
  *		will allow duplicates.  Otherwise (UNIQUE_CHECK_YES or
  *		UNIQUE_CHECK_EXISTING) it will throw error for a duplicate.
  *		For UNIQUE_CHECK_EXISTING we merely run the duplicate check, and
- *		don't actually insert.  If rel is a unique index, then every call
- *		here is a checkingunique call (i.e. every call does a duplicate
- *		check, though perhaps only a tentative check).
+ *		don't actually insert.
+
+HEIKKI: 'checkingunique' is a local variable in the function. Seems a bit
+weird to talk about it in the function comment. I didn't understand what
+the point of adding this sentence was, so I removed it.
+
  *
  *		The result value is only significant for UNIQUE_CHECK_PARTIAL:
  *		it must be true if the entry is known unique, else false.
@@ -285,9 +288,10 @@ top:
 		CheckForSerializableConflictIn(rel, NULL, buf);
 
 		/*
-		 * Do the insertion.  Note that itup_key contains mutable state used
-		 * by _bt_check_unique to help _bt_findinsertloc avoid repeating its
-		 * binary search.  !checkingunique case must start own binary search.
+		 * Do the insertion.  Note that itup_key contains state filled in by
+		 * _bt_check_unique to help _bt_findinsertloc avoid repeating its
+		 * binary search.  !checkingunique case must start its own binary
+		 * search.
 		 */
 		newitemoff = _bt_findinsertloc(rel, itup_key, &buf, checkingunique,
 									   itup, stack, heapRel);
@@ -311,10 +315,6 @@ top:
 /*
  *	_bt_check_unique() -- Check for violation of unique index constraint
  *
- * Sets state in itup_key sufficient for later _bt_findinsertloc() call to
- * reuse most of the work of our initial binary search to find conflicting
- * tuples.
- *
  * Returns InvalidTransactionId if there is no conflict, else an xact ID
  * we must wait for to see if it commits a conflicting tuple.   If an actual
  * conflict is detected, no return --- just ereport().  If an xact ID is
@@ -326,6 +326,10 @@ top:
  * InvalidTransactionId because we don't want to wait.  In this case we
  * set *is_unique to false if there is a potential conflict, and the
  * core code must redo the uniqueness check later.
+ *
+ * As a side-effect, sets state in itup_key that can later be used by
+ * _bt_findinsertloc() to reuse most of the binary search work we do
+ * here.
  */
 static TransactionId
 _bt_check_unique(Relation rel, BTScanInsert itup_key,
@@ -352,8 +356,8 @@ _bt_check_unique(Relation rel, BTScanInsert itup_key,
 	maxoff = PageGetMaxOffsetNumber(page);
 
 	/*
-	 * Save binary search bounds.  Note that this is also used within
-	 * _bt_findinsertloc() later.
+	 * Save binary search bounds.  We use them in the fastpath below, but
+	 * also in the _bt_findinsertloc() call later.
 	 */
 	itup_key->savebinsrch = true;
 	offset = _bt_binsrch(rel, itup_key, buf);
@@ -375,16 +379,16 @@ _bt_check_unique(Relation rel, BTScanInsert itup_key,
 		if (offset <= maxoff)
 		{
 			/*
-			 * Fastpath: _bt_binsrch() search bounds can be used to limit our
-			 * consideration to items that are definitely duplicates in most
-			 * cases (though not when original page is empty, or when initial
-			 * offset is past the end of the original page, which may indicate
-			 * that we'll have to examine a second or subsequent page).
+			 * Fastpath: In most cases, we can use _bt_binsrch search bounds
+			 * to limit our consideration to items that are definitely
+			 * duplicates.  This fastpath doesn't apply, when the original
+			 * page is empty, or when initial offset is past the end of the
+			 * original page, which may indicate that we need to examine a
+			 * second or subsequent page.
 			 *
 			 * Note that this optimization avoids calling _bt_isequal()
-			 * entirely when there are no duplicates, provided initial offset
-			 * isn't past end of the initial page (and provided page has at
-			 * least one item).
+			 * entirely when there are no duplicates, as long as the location
+			 * where the key would belong to is not at the end of the page.
 			 */
 			if (nbuf == InvalidBuffer && offset == itup_key->stricthigh)
 			{
@@ -588,6 +592,17 @@ _bt_check_unique(Relation rel, BTScanInsert itup_key,
 			if (P_RIGHTMOST(opaque))
 				break;
 			highkeycmp = _bt_compare(rel, itup_key, page, P_HIKEY);
+
+			/*
+			 * HEIKKI: This assertion might fire if the user-defined opclass
+			 * is broken. It's just an assertion, so maybe that's ok. With a
+			 * broken opclass, it's obviously "garbage in, garbage out", but
+			 * we should try to behave sanely anyway. I don't remember what
+			 * our general policy on that is; should we assert, elog(ERROR),
+			 * or continue silently in that case? An elog(ERROR) or
+			 * elog(WARNING) would feel best to me, but I don't remember what
+			 * we usually do.
+			 */
 			Assert(highkeycmp <= 0);
 			if (highkeycmp != 0)
 				break;
@@ -644,30 +659,55 @@ _bt_check_unique(Relation rel, BTScanInsert itup_key,
  *		Once we have chosen the page to put the key on, we'll insert it before
  *		any existing equal keys because of the way _bt_binsrch() works.
  *
- *		_bt_check_unique() callers arrange for their insertion scan key to
- *		save the progress of the last binary search performed.  No additional
- *		binary search comparisons occur in the common case where there was no
- *		existing duplicate tuple, though we may occasionally still not be able
- *		to reuse their work for our own reasons.  Even when there are garbage
- *		duplicates, very few binary search comparisons will be performed
- *		without being strictly necessary.
+ *		_bt_check_unique() saves the progress of the binary search it
+ *		performs, in the insertion scan key.  In the common case that there
+ *		were no duplicates, we don't need to do any additional binary search
+ *		comparisons here.  Though occasionally, we may still not be able to
+ *		reuse the saved state for our own reasons. Even when there are garbage
+ *		duplicates, we do very few binary search comparisons that are not
+ *		strictly necessary.
  *
- *		The caller should hold an exclusive lock on *bufptr in all cases.  On
- *		exit,  bufptr points to the chosen insert location in all cases.  If
- *		we have to move right, the lock and pin on the original page will be
- *		released, and the new page returned to the caller is exclusively
- *		locked instead.  In any case, we return the offset that caller should
- *		use to insert into the buffer pointed to by bufptr on return.
+HEIKKI:
+
+Should we mention explicitly that this binary-search reuse is only applicable
+if unique checks were performed? It's kind of implied by the fact that it's
+_bt_check_unique() that saves the state, but perhaps we should be more clear
+about it.
+
+What is a "garbage duplicate"? Same as a "dead duplicate"?
+
+The last sentence, about garbage duplicates, seems really vague. Why do we
+ever do any comparisons that are not strictly necessary? Perhaps it's best to
+just remove that last sentence.
+
+ *
+ *		On entry, *bufptr points to the first legal page where the new tuple
+ *		could be inserted.  The caller must hold an exclusive lock on *bufptr.
+ *
+ *		On exit, *bufptr points to the chosen insertion page, and the offset
+ *		within that page is returned.  If _bt_findinsertloc decides to move
+ *		right, the lock and pin on the original page is released, and the new
+ *		page returned to the caller is exclusively locked instead.
  *
  *		This is also where opportunistic microvacuuming of LP_DEAD tuples
  *		occurs.  It has to happen here, since it may invalidate a
  *		_bt_check_unique() caller's cached binary search work.
+
+HEIKKI: I don't buy the argument that microvacuuming has to happen here. You
+could easily imagine a separate function that does microvacuuming, and resets
+(or even updates) the binary-search cache in the insertion key. I agree this
+is a convenient place to do it, though.
+
  */
 static OffsetNumber
 _bt_findinsertloc(Relation rel,
 				  BTScanInsert itup_key,
 				  Buffer *bufptr,
 				  bool checkingunique,
+/* HEIKKI:
+Do we need 'checkunique' as an argument? If unique checks were not
+performed, the insertion key will simply not have saved state.
+*/
 				  IndexTuple newtup,
 				  BTStack stack,
 				  Relation heapRel)
@@ -706,6 +746,30 @@ _bt_findinsertloc(Relation rel,
 				 errtableconstraint(heapRel,
 									RelationGetRelationName(rel))));
 
+	/* HEIKKI: I liked this comment that we used to have here, before this patch: */
+	/*----------
+	 * If we will need to split the page to put the item on this page,
+	 * check whether we can put the tuple somewhere to the right,
+	 * instead.  Keep scanning right until we
+	 *		(a) find a page with enough free space,
+	 *		(b) reach the last page where the tuple can legally go, or
+	 *		(c) get tired of searching.
+	 * (c) is not flippant; it is important because if there are many
+	 * pages' worth of equal keys, it's better to split one of the early
+	 * pages than to scan all the way to the end of the run of equal keys
+	 * on every insert.  We implement "get tired" as a random choice,
+	 * since stopping after scanning a fixed number of pages wouldn't work
+	 * well (we'd never reach the right-hand side of previously split
+	 * pages).  Currently the probability of moving right is set at 0.99,
+	 * which may seem too high to change the behavior much, but it does an
+	 * excellent job of preventing O(N^2) behavior with many equal keys.
+	 *----------
+	 */
+	/* HEIKKI: Maybe it's not relevant with the later patches, but at least
+	 * with just this first patch, it's still valid. I noticed that the
+	 * comment is now in _bt_useduplicatepage, it seems a bit out-of-place
+	 * there. */
+
 	Assert(P_ISLEAF(lpageop) && !P_INCOMPLETE_SPLIT(lpageop));
 	for (;;)
 	{
@@ -714,29 +778,35 @@ _bt_findinsertloc(Relation rel,
 		BlockNumber rblkno;
 
 		/*
-		 * The checkingunique (restorebinsrch) case may well have established
-		 * bounds within _bt_check_unique()'s binary search that preclude the
-		 * need for a further high key check.  This fastpath isn't used when
-		 * there are no items on the existing page (other than high key), or
-		 * when it looks like the new item belongs last on the page, but it
-		 * might go on a later page instead.
+		 * An earlier _bt_check_unique() call may well have saved bounds that
+		 * we can use to skip the high key check.  This fastpath cannot be
+		 * used when there are no items on the existing page (other than the
+		 * high key), or when it looks like the new item belongs on the page
+		 * but it might go on a later page instead.
 		 */
 		if (restorebinsrch && itup_key->low <= itup_key->stricthigh &&
 			itup_key->stricthigh <= PageGetMaxOffsetNumber(page))
 			break;
 
+		/*
+		 * If this is the last page that the tuple can legally go to, stop
+		 * here.
+		 */
 		if (P_RIGHTMOST(lpageop))
 			break;
 		cmpval = _bt_compare(rel, itup_key, page, P_HIKEY);
+		if (cmpval != 0)
+			break;
 
 		/*
-		 * May have to handle case where there is a choice of which page to
-		 * place new tuple on, and we must balance space utilization as best
-		 * we can.
+		 * Otherwise, we have a choice to insert here, or move right to a
+		 * later page.  Try to balance space utilization the best we can.
 		 */
-		if (cmpval != 0 || _bt_useduplicatepage(rel, heapRel, buf,
-												&restorebinsrch, itemsz))
+		if (_bt_useduplicatepage(rel, heapRel, buf, &restorebinsrch, itemsz))
+		{
+			/* decided to insert here */
 			break;
+		}
 
 		/*
 		 * step right to next non-dead page
@@ -786,9 +856,17 @@ _bt_findinsertloc(Relation rel,
 		   _bt_compare(rel, itup_key, page, P_HIKEY) <= 0);
 
 	/*
-	 * Perform microvacuuming of the page we're about to insert tuple on if it
-	 * looks like it has LP_DEAD items.  Only microvacuum when it's likely to
-	 * forestall a page split, though.
+	 * If the page we're about to insert to doesn't have enough room for the
+	 * new tuple, we will have to split it.  If it looks like the page has
+	 * LP_DEAD items, try to remove them, in hope of making room for the new
+	 * item and avoiding the split.
+
+HEIKKI: In some scenarios, if the BTP_HAS_GARBAGE flag is falsely set, we would
+try to microvacuum the page twice: first in _bt_useduplicatepage, and second
+time here. That's because _bt_vacuum_one_page() doesn't clear the flag, if
+there are in fact no LP_DEAD items. That's probably insignificant and not worth
+worrying about, but I thought I'd mention it.
+
 	 */
 	if (P_HAS_GARBAGE(lpageop) && PageGetFreeSpace(page) < itemsz)
 	{
@@ -814,15 +892,15 @@ _bt_findinsertloc(Relation rel,
 /*
  *	_bt_useduplicatepage() -- Settle for this page of duplicates?
  *
- *		This function handles the question of whether or not an insertion
- *		of a duplicate into a pg_upgrade'd !heapkeyspace index should
- *		insert on the page contained in buf when a choice must be made.
- *		Preemptive microvacuuming is performed here when that could allow
- *		caller to insert on to the page in buf.
+ *		If we have the choice to insert to current page, or to some later
+ *		later page to the right, this function decides what to do.
  *
- *		Returns true if caller should proceed with insert on buf's page.
- *		Otherwise, caller should move on to the page to the right (caller
- *		must always be able to still move right following call here).
+ *		If the current page doesn't have enough free space for the new
+ *		tuple, we "microvacuum" the page, removing LP_DEAD items, in
+ *		hope that it will make enough room.
+ *
+ *		Returns true if caller should proceed with insert on the current
+ *		page.  Otherwise, caller should move on to the page to the right.
  */
 static bool
 _bt_useduplicatepage(Relation rel, Relation heapRel, Buffer buf,
@@ -838,6 +916,10 @@ _bt_useduplicatepage(Relation rel, Relation heapRel, Buffer buf,
 	if (PageGetFreeSpace(page) >= itemsz)
 		return true;
 
+	/*
+	 * Before considering moving right, see if we can obtain enough space by
+	 * erasing LP_DEAD items.
+	 */
 	if (P_HAS_GARBAGE(lpageop))
 	{
 		_bt_vacuum_one_page(rel, buf, heapRel);
@@ -1275,9 +1357,11 @@ _bt_split(Relation rel, Buffer buf, Buffer cbuf, OffsetNumber firstright,
 	 * If the page we're splitting is not the rightmost page at its level in
 	 * the tree, then the first entry on the page is the high key for the
 	 * page.  We need to copy that to the right half.  Otherwise (meaning the
-	 * rightmost page case), all the items on the right half will be user data
-	 * (there is no existing high key that needs to be relocated to the new
-	 * right page).
+	 * rightmost page case), all the items on the right half will be user
+	 * data.
+	 *
+HEIKKI: I don't think the comment change you made here was needed or
+helpful, so I reverted it.
 	 */
 	rightoff = P_HIKEY;
 
diff --git a/src/backend/access/nbtree/nbtsearch.c b/src/backend/access/nbtree/nbtsearch.c
index 7940297305d..f27148eb27d 100644
--- a/src/backend/access/nbtree/nbtsearch.c
+++ b/src/backend/access/nbtree/nbtsearch.c
@@ -162,8 +162,8 @@ _bt_search(Relation rel, BTScanInsert key, Buffer *bufP, int access,
 		new_stack->bts_parent = stack_in;
 
 		/*
-		 * Page level 1 is lowest non-leaf page level prior to leaves.  So,
-		 * if we're on the level 1 and asked to lock leaf page in write mode,
+		 * Page level 1 is lowest non-leaf page level prior to leaves.  So, if
+		 * we're on the level 1 and asked to lock leaf page in write mode,
 		 * then lock next page in write mode, because it must be a leaf.
 		 */
 		if (opaque->btpo.level == 1 && access == BT_WRITE)
@@ -333,13 +333,14 @@ _bt_moveright(Relation rel,
  *
  * This procedure is not responsible for walking right, it just examines
  * the given page.  _bt_binsrch() has no lock or refcount side effects
- * on the buffer.  When key.savebinsrch is set, modifies mutable fields
- * of insertion scan key, so that a subsequent call where caller sets
- * key.savebinsrch can reuse the low and strict high bound of original
- * binary search.  Callers that use these fields directly must be
- * prepared for the case where stricthigh isn't on the same page (it
- * exceeds maxoff for the page), and the case where there are no items
- * on the page (high < low).
+ * on the buffer.
+ *
+ * When key.savebinsrch is set, modifies mutable fields of insertion scan
+ * key, so that a subsequent call where caller sets key.restorebinsrch can
+ * reuse the low and strict high bound of original binary search.  Callers
+ * that use these fields directly must be prepared for the case where
+ * stricthigh isn't on the same page (it exceeds maxoff for the page), and
+ * the case where there are no items on the page (high < low).
  */
 OffsetNumber
 _bt_binsrch(Relation rel,
diff --git a/src/backend/access/nbtree/nbtutils.c b/src/backend/access/nbtree/nbtutils.c
index e010bcdcfa9..3daf5829f82 100644
--- a/src/backend/access/nbtree/nbtutils.c
+++ b/src/backend/access/nbtree/nbtutils.c
@@ -103,9 +103,8 @@ _bt_mkscankey(Relation rel, IndexTuple itup)
 		procinfo = index_getprocinfo(rel, i + 1, BTORDER_PROC);
 
 		/*
-		 * Key arguments built when caller provides no tuple are defensively
-		 * represented as NULL values, though they should still not
-		 * participate in comparisons.
+		 * If the caller provides no tuple, the key arguments should never be
+		 * used.  Set them to NULL, anyway, to be defensive.
 		 */
 		if (i < tupnatts)
 			arg = index_getattr(itup, i + 1, itupdesc, &null);
diff --git a/src/include/access/nbtree.h b/src/include/access/nbtree.h
index dc2eafb5665..45899454bba 100644
--- a/src/include/access/nbtree.h
+++ b/src/include/access/nbtree.h
@@ -323,8 +323,7 @@ typedef BTStackData *BTStack;
  * BTScanInsert is the btree-private state needed to find an initial position
  * for an indexscan, or to insert new tuples -- an "insertion scankey" (not to
  * be confused with a search scankey).  It's used to descend a B-Tree using
- * _bt_search.  For details on its mutable state, see _bt_binsrch and
- * _bt_findinsertloc.
+ * _bt_search.
  *
  * When nextkey is false (the usual case), _bt_search and _bt_binsrch will
  * locate the first item >= scankey.  When nextkey is true, they will locate
@@ -334,9 +333,14 @@ typedef BTStackData *BTStack;
  *
  * scankeys is an array of scan key entries for attributes that are compared.
  * During insertion, there must be a scan key for every attribute, but when
- * starting a regular index scan some can be omitted.  The array is used as a
+ * starting a regular index scan, some can be omitted.  The array is used as a
  * flexible array member, though it's sized in a way that makes it possible to
  * use stack allocations.  See nbtree/README for full details.
+
+HEIKKI: I don't see anything in the README about stack allocations. What
+exactly does the README reference refer to? No code seems to actually allocate
+this in the stack, so we don't really need that.
+
  */
 
 typedef struct BTScanInsertData
@@ -344,7 +348,8 @@ typedef struct BTScanInsertData
 	/*
 	 * Mutable state used by _bt_binsrch to inexpensively repeat a binary
 	 * search on the leaf level.  Only used for insertions where
-	 * _bt_check_unique is called.
+	 * _bt_check_unique is called.  See _bt_binsrch and _bt_findinsertloc
+	 * for details.
 	 */
 	bool		savebinsrch;
 	bool		restorebinsrch;