From 70b61d63ab50109efeb51663ebd24cf52a51409f Mon Sep 17 00:00:00 2001 From: Yao Feng Date: Tue, 29 Sep 2026 09:09:20 +0000 Subject: [PATCH 1/2] Refactor B-tree specialized search architecture to follow SortSupport model Refactor the specialized in-page search facility into a clean, two-tier architecture aligned with the SortSupport design. This establishes the search engine (nbtsearch.c) as the single source of truth for in-page search control flow, interval reduction, and tree consistency invariants, while reducing operator class support to lightweight, stateless primitive callbacks. This eliminates code duplication across search paths and provides an extensible foundation for adding specialized searches to additional data types. --- src/backend/access/nbtree/nbtcompare.c | 209 ++++----------------- src/backend/access/nbtree/nbtsearch.c | 242 +++++++++++++++---------- src/backend/access/nbtree/nbtutils.c | 8 +- src/include/access/nbtree.h | 32 ++-- 4 files changed, 202 insertions(+), 289 deletions(-) diff --git a/src/backend/access/nbtree/nbtcompare.c b/src/backend/access/nbtree/nbtcompare.c index 672d6ee70c6..9685ecad9e3 100644 --- a/src/backend/access/nbtree/nbtcompare.c +++ b/src/backend/access/nbtree/nbtcompare.c @@ -206,201 +206,71 @@ btint4cmp(PG_FUNCTION_ARGS) } static inline bool -btint4tuplevalue(IndexTuple itup, int32 *value) +bt_tuple_value_check(IndexTuple itup) { - if (IndexTupleHasNulls(itup)) + if (IndexTupleHasNulls(itup) || + (BTreeTupleIsPivot(itup) && + (ItemPointerGetOffsetNumberNoCheck(&itup->t_tid) & BT_OFFSET_MASK) == 0)) 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) +btint4tuplevalue(IndexTuple itup, int32 *value) { - IndexTuple itup; + if (!bt_tuple_value_check(itup)) + return false; - itup = (IndexTuple) PageGetItem(page, PageGetItemId(page, offnum)); - return btint4tuplevalue(itup, value); + /* int4 is the first and only key, so its offset is fixed. */ + memcpy(value, (char *) itup + sizeof(IndexTupleData), sizeof(*value)); + return true; } static bool -btint4pagecmp(Relation rel, BTScanInsert key, Page page, OffsetNumber offnum, - int32 *result) +btint4pagecmp(BTScanInsert key, IndexTuple itup, int32 *result) { - 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; - Datum datum; - bool isnull; - - itup = (IndexTuple) PageGetItem(page, itemid); if (likely(btint4tuplevalue(itup, &value))) { - datum = Int32GetDatum(value); - isnull = false; - } - else - datum = index_getattr(itup, 1, tupdesc, &isnull); - - if (isnull) - *result = (skey->sk_flags & SK_BT_NULLS_FIRST) ? 1 : -1; - else if (skey->sk_flags & SK_BT_DESC) - *result = pg_cmp_s32(DatumGetInt32(datum), search); - else - *result = pg_cmp_s32(search, DatumGetInt32(datum)); - - if (*result != 0) - return true; - - heapTid = BTreeTupleGetHeapTID(itup); - if (key->scantid == NULL) - { - if (!key->backward && heapTid == NULL && key->heapkeyspace) - *result = 1; - return true; - } - - if (heapTid == NULL) - { - *result = 1; + if (skey->sk_flags & SK_BT_DESC) + *result = pg_cmp_s32(value, search); + else + *result = pg_cmp_s32(search, value); return true; } - *result = ItemPointerCompare(key->scantid, heapTid); - if (*result > 0 && BTreeTupleIsPosting(itup)) - { - *result = ItemPointerCompare(key->scantid, - BTreeTupleGetMaxHeapTID(itup)); - if (*result <= 0) - *result = 0; - } - - return true; -} - -/* Nonincremental searches don't need a cached strict upper bound. */ -static inline bool -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); - int32 result; - bool advance; - - if (!btint4pagecmp(rel, key, page, mid, &result)) - return false; - - advance = result >= cmpval; - low = advance ? mid + 1 : low; - high = advance ? high : mid; - } - - *resultoff = low; - return true; + return false; } -/* Single-column page binary search for int4 opclasses. */ +/* + * Interpolation support for int4 single-column page search. + */ static bool -btint4binsearch(Relation rel, BTScanInsert key, Page page, - OffsetNumber low, OffsetNumber high, int32 cmpval, - OffsetNumber *resultoff, OffsetNumber *strictresult) -{ - OffsetNumber stricthigh = high; - - if (strictresult == NULL) - return btint4binsearch_uncached(rel, key, page, low, high, cmpval, - resultoff); - - while (high > low) - { - OffsetNumber mid = low + ((high - low) / 2); - int32 result; +btint4interpolate(IndexTuple itup_low, IndexTuple itup_high, + Datum target, bool desc, int nslots, int *offset) +{ + int32 lowval; + int32 highval; + int32 search; + int64 span; + int64 delta; + + if (!btint4tuplevalue(itup_low, &lowval) || + !btint4tuplevalue(itup_high, &highval) || + lowval == highval) + return false; - if (!btint4pagecmp(rel, key, page, mid, &result)) - return false; - if (unlikely(result == 0 && key->scantid != NULL)) - return false; + search = DatumGetInt32(target); + span = desc ? (int64) lowval - highval : (int64) highval - lowval; + delta = desc ? (int64) lowval - search : (int64) search - lowval; - if (result >= cmpval) - low = mid + 1; - else - { - high = mid; - if (result != 0) - stricthigh = high; - } - } + if (span <= 0 || delta < 0 || delta > span) + return false; - *resultoff = low; - *strictresult = stricthigh; + *offset = (int) ((delta * (int64) nslots) / span); return true; } @@ -411,7 +281,7 @@ btint4binsearchsupport(PG_FUNCTION_ARGS) (BTBinSearchSupportData *) PG_GETARG_POINTER(0); support->compare_tuple = btint4pagecmp; - support->binary_search = btint4binsearch; + support->interpolate = btint4interpolate; PG_RETURN_VOID(); } @@ -492,6 +362,7 @@ btint8sortsupport(PG_FUNCTION_ARGS) PG_RETURN_VOID(); } + static Datum int8_decrement(Relation rel, Datum existing, bool *underflow) { diff --git a/src/backend/access/nbtree/nbtsearch.c b/src/backend/access/nbtree/nbtsearch.c index 93845d42acb..c51838999ca 100644 --- a/src/backend/access/nbtree/nbtsearch.c +++ b/src/backend/access/nbtree/nbtsearch.c @@ -33,6 +33,14 @@ static Buffer _bt_moveright(Relation rel, Relation heaprel, BTScanInsert key, Buffer buf, bool forupdate, BTStack stack, int access); static OffsetNumber _bt_binsrch(Relation rel, BTScanInsert key, Buffer buf); +static bool _bt_binsrch_probe(Relation rel, BTScanInsert key, Page page, + OffsetNumber *low, OffsetNumber *high, + int32 cmpval, OffsetNumber *resultoff); +static inline OffsetNumber _bt_binsrch_loop(Relation rel, BTScanInsert key, + Page page, OffsetNumber low, + OffsetNumber high, int32 cmpval, + OffsetNumber *stricthigh_out, + BTInsertState insertstate); static int _bt_binsrch_posting(BTScanInsert key, Page page, OffsetNumber offnum); static inline void _bt_returnitem(IndexScanDesc scan, BTScanOpaque so); @@ -341,10 +349,118 @@ _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. + */ +static bool +_bt_binsrch_probe(Relation rel, BTScanInsert key, Page page, + OffsetNumber *low, OffsetNumber *high, int32 cmpval, + OffsetNumber *resultoff) +{ + IndexTuple itup_low; + IndexTuple itup_high; + int nslots; + int offset; + OffsetNumber probe; + OffsetNumber neighbor; + int32 result; + bool advance; + bool neighbor_advance; + bool desc; + + if (*high - *low < 32 || key->interpolate == NULL) + return false; + + itup_low = (IndexTuple) PageGetItem(page, PageGetItemId(page, *low)); + itup_high = (IndexTuple) PageGetItem(page, PageGetItemId(page, *high - 1)); + + nslots = *high - *low - 1; + desc = (key->scankeys[0].sk_flags & SK_BT_DESC) != 0; + + if (!key->interpolate(itup_low, itup_high, + key->scankeys[0].sk_argument, + desc, nslots, &offset)) + return false; + + probe = *low + offset; + + result = _bt_compare(rel, key, page, probe); + advance = result >= cmpval; + + if ((advance && OffsetNumberNext(probe) == *high) || + (!advance && probe == *low)) + { + *resultoff = advance ? *high : *low; + return true; + } + + neighbor = advance ? OffsetNumberNext(probe) : OffsetNumberPrev(probe); + result = _bt_compare(rel, key, page, neighbor); + neighbor_advance = result >= cmpval; + + if (advance != neighbor_advance) + { + *resultoff = advance ? neighbor : probe; + return true; + } + + if (advance) + *low = OffsetNumberNext(neighbor); + else + *high = probe; + + return false; +} + +/* + * _bt_binsrch_loop -- binary search loop on a page. * - * NB: Keep opclass binary_search callbacks in sync with changes to the - * search semantics here or in _bt_binsrch_insert(). + * Finds the first slot in [low, high) where _bt_compare() < cmpval. + * If insertstate is non-NULL, also maintains stricthigh and postingoff. */ +static inline OffsetNumber +_bt_binsrch_loop(Relation rel, BTScanInsert key, Page page, + OffsetNumber low, OffsetNumber high, int32 cmpval, + OffsetNumber *stricthigh_out, BTInsertState insertstate) +{ + OffsetNumber stricthigh = high; + + while (high > low) + { + OffsetNumber mid = low + ((high - low) / 2); + int32 result; + + result = _bt_compare(rel, key, page, mid); + + if (result >= cmpval) + low = mid + 1; + else + { + high = mid; + if (result != 0) + stricthigh = high; + } + + if (insertstate != NULL && unlikely(result == 0 && key->scantid != NULL)) + { + if (insertstate->postingoff != 0) + ereport(ERROR, + (errcode(ERRCODE_INDEX_CORRUPTED), + errmsg_internal("table tid from new index tuple (%u,%u) cannot find insert offset between offsets %u and %u of block %u in index \"%s\"", + ItemPointerGetBlockNumber(key->scantid), + ItemPointerGetOffsetNumber(key->scantid), + low, stricthigh, + BufferGetBlockNumber(insertstate->buf), + RelationGetRelationName(rel)))); + + insertstate->postingoff = _bt_binsrch_posting(key, page, mid); + } + } + + if (stricthigh_out != NULL) + *stricthigh_out = stricthigh; + + return low; +} + static OffsetNumber _bt_binsrch(Relation rel, BTScanInsert key, @@ -354,8 +470,7 @@ _bt_binsrch(Relation rel, BTPageOpaque opaque; OffsetNumber low, high; - int32 result, - cmpval; + int32 cmpval; page = BufferGetPage(buf); opaque = BTPageGetOpaque(page); @@ -395,40 +510,29 @@ _bt_binsrch(Relation rel, cmpval = key->nextkey ? 0 : 1; /* select comparison value */ /* - * A specialized search may skip the minus-infinity first item on an - * internal page. It can decline at run time, in which case use the - * generic comparison loop from its original bounds. + * Try interpolation probe to find or narrow the search interval. + * On internal pages, skip the minus-infinity first item. */ - if (key->binary_search != NULL) + if (key->interpolate != NULL) { - OffsetNumber resultoff; OffsetNumber searchlow = low; + OffsetNumber resultoff; if (!P_ISLEAF(opaque)) searchlow++; - if (key->binary_search(rel, key, page, searchlow, high, cmpval, - &resultoff, NULL)) + if (_bt_binsrch_probe(rel, key, page, &searchlow, &high, cmpval, + &resultoff)) { low = resultoff; goto search_done; } - } - while (high > low) - { - OffsetNumber mid = low + ((high - low) / 2); - - /* We have low <= mid < high, so mid points at a real slot */ - - result = _bt_compare(rel, key, page, mid); - - if (result >= cmpval) - low = mid + 1; - else - high = mid; + low = searchlow; } + low = _bt_binsrch_loop(rel, key, page, low, high, cmpval, NULL, NULL); + search_done: /* @@ -499,8 +603,6 @@ search_done: * determine which existing heap TID will need to be replaced by a posting * list split). * - * NB: Keep opclass binary_search callbacks in sync with changes to the - * search semantics here or in _bt_binsrch(). */ OffsetNumber _bt_binsrch_insert(Relation rel, BTInsertState insertstate) @@ -511,8 +613,7 @@ _bt_binsrch_insert(Relation rel, BTInsertState insertstate) OffsetNumber low, high, stricthigh; - int32 result, - cmpval; + int32 cmpval; page = BufferGetPage(insertstate->buf); opaque = BTPageGetOpaque(page); @@ -556,73 +657,11 @@ _bt_binsrch_insert(Relation rel, BTInsertState insertstate) */ if (!insertstate->bounds_valid) high++; /* establish the loop invariant for high */ - stricthigh = high; /* high initially strictly higher */ cmpval = 1; /* !nextkey comparison value */ - /* - * Let an opclass complete the search using a specialized implementation. - * It may decline at run time, in which case the generic loop starts from - * the original bounds. - */ - if (!insertstate->bounds_valid && key->binary_search != NULL) - { - OffsetNumber keylow; - OffsetNumber keystricthigh; - - if (key->binary_search(rel, key, page, low, high, cmpval, - &keylow, &keystricthigh)) - { - low = keylow; - high = keylow; - stricthigh = keystricthigh; - } - } - - while (high > low) - { - OffsetNumber mid = low + ((high - low) / 2); - - /* We have low <= mid < high, so mid points at a real slot */ - - result = _bt_compare(rel, key, page, mid); - - if (result >= cmpval) - low = mid + 1; - else - { - high = mid; - if (result != 0) - stricthigh = high; - } - - /* - * If tuple at offset located by binary search is a posting list whose - * TID range overlaps with caller's scantid, perform posting list - * binary search to set postingoff for caller. Caller must split the - * posting list when postingoff is set. This should happen - * infrequently. - */ - if (unlikely(result == 0 && key->scantid != NULL)) - { - /* - * postingoff should never be set more than once per leaf page - * binary search. That would mean that there are duplicate table - * TIDs in the index, which is never okay. Check for that here. - */ - if (insertstate->postingoff != 0) - ereport(ERROR, - (errcode(ERRCODE_INDEX_CORRUPTED), - errmsg_internal("table tid from new index tuple (%u,%u) cannot find insert offset between offsets %u and %u of block %u in index \"%s\"", - ItemPointerGetBlockNumber(key->scantid), - ItemPointerGetOffsetNumber(key->scantid), - low, stricthigh, - BufferGetBlockNumber(insertstate->buf), - RelationGetRelationName(rel)))); - - insertstate->postingoff = _bt_binsrch_posting(key, page, mid); - } - } + low = _bt_binsrch_loop(rel, key, page, low, high, cmpval, + &stricthigh, insertstate); /* * On a leaf page, a binary search always returns the first key >= scan @@ -733,9 +772,6 @@ _bt_binsrch_posting(BTScanInsert key, Page page, OffsetNumber offnum) * that isn't relied upon. This allows us to implement the Lehman and * Yao convention that the first down-link pointer is before the first * key. See backend/access/nbtree/README for details. - * - * NB: Keep opclass compare_tuple callbacks in sync with changes to the - * comparison semantics here. *---------- */ int32 @@ -764,13 +800,17 @@ _bt_compare(Relation rel, if (!P_ISLEAF(opaque) && offnum == P_FIRSTDATAKEY(opaque)) return 1; - if (key->compare_tuple != NULL && - key->compare_tuple(rel, key, page, offnum, &result)) - return result; - itup = (IndexTuple) PageGetItem(page, PageGetItemId(page, offnum)); ntupatts = BTreeTupleGetNAtts(itup, rel); + if (key->compare_tuple != NULL && + key->compare_tuple(key, itup, &result)) + { + if (result != 0) + return result; + goto tiebreak_tid; + } + /* * The scan key is set up with the attribute number associated with each * term in the key. It is important that, if the index is multi-key, the @@ -836,6 +876,8 @@ _bt_compare(Relation rel, scankey++; } +tiebreak_tid: + /* * All non-truncated attributes (other than heap TID) were found to be * equal. Treat truncated attributes as minus infinity when scankey has a @@ -1501,7 +1543,7 @@ _bt_first(IndexScanDesc scan, ScanDirection dir) inskey.scantid = NULL; inskey.keysz = keysz; inskey.compare_tuple = NULL; - inskey.binary_search = NULL; + inskey.interpolate = NULL; _bt_setup_binsearch(rel, &inskey); switch (strat_total) { diff --git a/src/backend/access/nbtree/nbtutils.c b/src/backend/access/nbtree/nbtutils.c index 68bcb26a8a2..3d9add1bfd1 100644 --- a/src/backend/access/nbtree/nbtutils.c +++ b/src/backend/access/nbtree/nbtutils.c @@ -96,7 +96,7 @@ _bt_mkscankey(Relation rel, IndexTuple itup) key->backward = false; /* usual case, required by btinsert */ key->keysz = Min(indnkeyatts, tupnatts); key->compare_tuple = NULL; - key->binary_search = NULL; + key->interpolate = NULL; key->scantid = key->heapkeyspace && itup ? BTreeTupleGetHeapTID(itup) : NULL; skey = key->scankeys; @@ -161,7 +161,7 @@ _bt_setup_binsearch(Relation rel, BTScanInsert key) ScanKey skey; key->compare_tuple = NULL; - key->binary_search = NULL; + key->interpolate = NULL; if (IndexRelationGetNumberOfKeyAttributes(rel) != 1 || key->keysz != 1) return; @@ -178,7 +178,7 @@ _bt_setup_binsearch(Relation rel, BTScanInsert key) if (support != NULL) { key->compare_tuple = support->compare_tuple; - key->binary_search = support->binary_search; + key->interpolate = support->interpolate; return; } @@ -199,7 +199,7 @@ _bt_setup_binsearch(Relation rel, BTScanInsert key) procinfo->fn_extra = support; } key->compare_tuple = support->compare_tuple; - key->binary_search = support->binary_search; + key->interpolate = support->interpolate; } /* diff --git a/src/include/access/nbtree.h b/src/include/access/nbtree.h index 9b4b419a398..edb76c30183 100644 --- a/src/include/access/nbtree.h +++ b/src/include/access/nbtree.h @@ -801,32 +801,32 @@ struct BTScanInsertData; /* * Optional callbacks installed by a BTBINSEARCH_PROC support function. * - * compare_tuple has the same result semantics as _bt_compare(). It is never - * asked to compare the minus-infinity tuple at the start of an internal page. - * binary_search searches the half-open interval [low, high), using cmpval in - * the same way as _bt_binsrch(). When requested, stricthigh must be set to a - * bound known to compare strictly greater than the insertion scankey. + * compare_tuple compares an insertion scankey to a tuple on a page. + * It has the same result semantics as _bt_compare() for attribute 1, + * leaving heap TID tiebreaking to _bt_compare(). + * + * interpolate calculates the slot offset estimate between itup_low and + * itup_high using linear interpolation. * * A callback may return false without changing its output arguments, making * nbtree repeat the operation using the generic comparator. */ -typedef bool (*BTCompareTupleFunction) (Relation rel, - struct BTScanInsertData *key, - Page page, OffsetNumber offnum, +typedef bool (*BTCompareTupleFunction) (struct BTScanInsertData *key, + IndexTuple itup, int32 *result); -typedef bool (*BTBinSearchFunction) (Relation rel, - struct BTScanInsertData *key, - Page page, OffsetNumber low, - OffsetNumber high, int32 cmpval, - OffsetNumber *result, - OffsetNumber *stricthigh); +typedef bool (*BTInterpolateFunction) (IndexTuple itup_low, + IndexTuple itup_high, + Datum target, + bool desc, + int nslots, + int *offset); typedef struct BTBinSearchSupportData { /* Support functions set either or both callbacks. */ BTCompareTupleFunction compare_tuple; - BTBinSearchFunction binary_search; + BTInterpolateFunction interpolate; } BTBinSearchSupportData; typedef struct BTScanInsertData @@ -839,7 +839,7 @@ typedef struct BTScanInsertData ItemPointer scantid; /* tiebreaker for scankeys */ int keysz; /* Size of scankeys array */ BTCompareTupleFunction compare_tuple; - BTBinSearchFunction binary_search; + BTInterpolateFunction interpolate; ScanKeyData scankeys[INDEX_MAX_KEYS]; /* Must appear last */ } BTScanInsertData; -- 2.53.0