From 304f3e4090cf16e379d00cc03ee7b074e7841f9d Mon Sep 17 00:00:00 2001 From: David Geier Date: Wed, 7 Oct 2026 16:26:52 +0200 Subject: [PATCH v2] Batch TIDBitmap insertions in index scans Change the B-tree, hash, GiST, GIN, SP-GiST, and bloom index access methods to collect matching TIDs into arrays and pass them to tbm_add_tuples() in batches, rather than adding each TID individually. tbm_add_tuples() reuses the TIDBitmap page entry while processing consecutive TIDs from the same heap page. Larger batches therefore reduce repeated hash table lookups and function call overhead. Access methods that can return different recheck values use separate batches for exact TIDs and TIDs requiring rechecks. Access methods whose result streams are not bounded by an index page use fixed-size batches that are flushed when full. Also simplify the control flow in btgetbitmap() by using a for loop with _bt_first() and _bt_next(). --- contrib/bloom/blscan.c | 14 ++++++++++- src/backend/access/gin/ginget.c | 39 ++++++++++++++++++++++++++--- src/backend/access/gist/gistget.c | 23 +++++++++++++++-- src/backend/access/hash/hash.c | 23 ++++++++++------- src/backend/access/nbtree/nbtree.c | 37 +++++++-------------------- src/backend/access/spgist/spgscan.c | 16 +++++++++++- src/include/access/spgist_private.h | 5 ++++ 7 files changed, 113 insertions(+), 44 deletions(-) diff --git a/contrib/bloom/blscan.c b/contrib/bloom/blscan.c index 1a0e42021ec..ba6de0aaea5 100644 --- a/contrib/bloom/blscan.c +++ b/contrib/bloom/blscan.c @@ -20,6 +20,8 @@ #include "storage/bufmgr.h" #include "storage/read_stream.h" +#define BLOOM_TBM_BATCH_SIZE 256 + /* * Begin scan of bloom index. */ @@ -153,6 +155,8 @@ blgetbitmap(IndexScanDesc scan, TIDBitmap *tbm) { OffsetNumber offset, maxOffset = BloomPageGetMaxOffset(page); + ItemPointerData tids[BLOOM_TBM_BATCH_SIZE]; + int ntids_batch = 0; for (offset = 1; offset <= maxOffset; offset++) { @@ -172,10 +176,18 @@ blgetbitmap(IndexScanDesc scan, TIDBitmap *tbm) /* Add matching tuples to bitmap */ if (res) { - tbm_add_tuples(tbm, &itup->heapPtr, 1, true); + if (ntids_batch == BLOOM_TBM_BATCH_SIZE) + { + tbm_add_tuples(tbm, tids, ntids_batch, true); + ntids_batch = 0; + } + + tids[ntids_batch++] = itup->heapPtr; ntids++; } } + + tbm_add_tuples(tbm, tids, ntids_batch, true); } UnlockReleaseBuffer(buffer); diff --git a/src/backend/access/gin/ginget.c b/src/backend/access/gin/ginget.c index 2bcb32ca3d0..2c32a9e82e1 100644 --- a/src/backend/access/gin/ginget.c +++ b/src/backend/access/gin/ginget.c @@ -26,6 +26,8 @@ /* GUC parameter */ int GinFuzzySearchLimit = 0; +#define GIN_TBM_BATCH_SIZE 256 + typedef struct pendingPosition { Buffer pendingBuffer; @@ -1842,6 +1844,8 @@ scanPendingInsert(IndexScanDesc scan, TIDBitmap *tbm, int64 *ntids) Buffer metabuffer = ReadBuffer(scan->indexRelation, GIN_METAPAGE_BLKNO); Page page; BlockNumber blkno; + ItemPointerData tids[2][GIN_TBM_BATCH_SIZE]; + int ntids_batch[2] = {0, 0}; *ntids = 0; @@ -1913,11 +1917,22 @@ scanPendingInsert(IndexScanDesc scan, TIDBitmap *tbm, int64 *ntids) if (match) { - tbm_add_tuples(tbm, &pos.item, 1, recheck); + const int b = recheck ? 1 : 0; + + if (ntids_batch[b] == GIN_TBM_BATCH_SIZE) + { + tbm_add_tuples(tbm, tids[b], ntids_batch[b], recheck); + ntids_batch[b] = 0; + } + + tids[b][ntids_batch[b]++] = pos.item; (*ntids)++; } } + for (int i = 0; i < 2; i++) + tbm_add_tuples(tbm, tids[i], ntids_batch[i], i == 1); + pfree(pos.hasMatchKey); } @@ -1931,6 +1946,8 @@ gingetbitmap(IndexScanDesc scan, TIDBitmap *tbm) int64 ntids; ItemPointerData iptr; bool recheck; + ItemPointerData tids[2][GIN_TBM_BATCH_SIZE]; + int ntids_batch[2] = {0, 0}; /* * Set up the scan keys, and check for unsatisfiable query. @@ -1969,11 +1986,27 @@ gingetbitmap(IndexScanDesc scan, TIDBitmap *tbm) break; if (ItemPointerIsLossyPage(&iptr)) + { tbm_add_page(tbm, ItemPointerGetBlockNumber(&iptr)); + ntids++; + } else - tbm_add_tuples(tbm, &iptr, 1, recheck); - ntids++; + { + const int b = recheck ? 1 : 0; + + if (ntids_batch[b] == GIN_TBM_BATCH_SIZE) + { + tbm_add_tuples(tbm, tids[b], ntids_batch[b], recheck); + ntids_batch[b] = 0; + } + + tids[b][ntids_batch[b]++] = iptr; + ntids++; + } } + for (int i = 0; i < 2; i++) + tbm_add_tuples(tbm, tids[i], ntids_batch[i], i == 1); + return ntids; } diff --git a/src/backend/access/gist/gistget.c b/src/backend/access/gist/gistget.c index 111c310f9df..23f77d6841a 100644 --- a/src/backend/access/gist/gistget.c +++ b/src/backend/access/gist/gistget.c @@ -348,6 +348,10 @@ gistScanPage(IndexScanDesc scan, GISTSearchItem *pageItem, OffsetNumber maxoff; OffsetNumber i; MemoryContext oldcxt; + ItemPointerData tids_match[MaxIndexTuplesPerPage]; + ItemPointerData tids_recheck[MaxIndexTuplesPerPage]; + int ntids_match = 0; + int ntids_recheck = 0; Assert(!GISTSearchItemIsHeap(*pageItem)); @@ -464,9 +468,15 @@ gistScanPage(IndexScanDesc scan, GISTSearchItem *pageItem, { /* * getbitmap scan, so just push heap tuple TIDs into the bitmap - * without worrying about ordering + * without worrying about ordering. Because tbm_add_tuples + * takes a single recheck flag, we split matching TIDs into two + * per-page batches based on the recheck value. */ - tbm_add_tuples(tbm, &it->t_tid, 1, recheck); + if (recheck) + tids_recheck[ntids_recheck++] = it->t_tid; + else + tids_match[ntids_match++] = it->t_tid; + (*ntids)++; } else if (scan->numberOfOrderBys == 0 && GistPageIsLeaf(page)) @@ -543,6 +553,15 @@ gistScanPage(IndexScanDesc scan, GISTSearchItem *pageItem, } } + /* + * For a getbitmap scan, flush all matching TIDs from this page into the TIDBitmap. + */ + if (tbm) + { + tbm_add_tuples(tbm, tids_match, ntids_match, false); + tbm_add_tuples(tbm, tids_recheck, ntids_recheck, true); + } + UnlockReleaseBuffer(buffer); } diff --git a/src/backend/access/hash/hash.c b/src/backend/access/hash/hash.c index b2e34d2d45e..1cc0b2fdc8b 100644 --- a/src/backend/access/hash/hash.c +++ b/src/backend/access/hash/hash.c @@ -356,21 +356,26 @@ hashgetbitmap(IndexScanDesc scan, TIDBitmap *tbm) HashScanOpaque so = (HashScanOpaque) scan->opaque; bool res; int64 ntids = 0; - HashScanPosItem *currItem; res = _hash_first(scan, ForwardScanDirection); + /* + * Each iteration of this loop reads one index page worth of matching + * TIDs (already collected into so->currPos.items by _hash_first/ + * _hash_next) and adds them to the TIDBitmap in a single batch. Dead + * index entries are handled by _hash_first/_hash_next whenever + * scan->ignore_killed_tuples is true, so there is nothing else to do. + */ while (res) { - currItem = &so->currPos.items[so->currPos.itemIndex]; + ItemPointerData tids[MaxIndexTuplesPerPage]; + int ntids_page = 0; - /* - * _hash_first and _hash_next handle eliminate dead index entries - * whenever scan->ignore_killed_tuples is true. Therefore, there's - * nothing to do here except add the results to the TIDBitmap. - */ - tbm_add_tuples(tbm, &(currItem->heapTid), 1, true); - ntids++; + while (so->currPos.itemIndex <= so->currPos.lastItem) + tids[ntids_page++] = so->currPos.items[so->currPos.itemIndex++].heapTid; + + tbm_add_tuples(tbm, tids, ntids_page, true); + ntids += ntids_page; res = _hash_next(scan, ForwardScanDirection); } diff --git a/src/backend/access/nbtree/nbtree.c b/src/backend/access/nbtree/nbtree.c index 0abdd7b49f5..90192b789cc 100644 --- a/src/backend/access/nbtree/nbtree.c +++ b/src/backend/access/nbtree/nbtree.c @@ -291,45 +291,26 @@ int64 btgetbitmap(IndexScanDesc scan, TIDBitmap *tbm) { BTScanOpaque so = (BTScanOpaque) scan->opaque; - int64 ntids = 0; - ItemPointer heapTid; + int64 ntids_total = 0; Assert(scan->heapRelation == NULL); - /* Each loop iteration performs another primitive index scan */ do { - /* Fetch the first page & tuple */ - if (_bt_first(scan, ForwardScanDirection)) + for (bool more = _bt_first(scan, ForwardScanDirection); more; more =_bt_next(scan, ForwardScanDirection)) { - /* Save tuple ID, and continue scanning */ - heapTid = &scan->xs_heaptid; - tbm_add_tuples(tbm, heapTid, 1, false); - ntids++; + ItemPointerData tids[MaxTIDsPerBTreePage]; + int ntids_page = 0; - for (;;) - { - /* - * Advance to next tuple within page. This is the same as the - * easy case in _bt_next(). - */ - if (++so->currPos.itemIndex > so->currPos.lastItem) - { - /* let _bt_next do the heavy lifting */ - if (!_bt_next(scan, ForwardScanDirection)) - break; - } + while (so->currPos.itemIndex <= so->currPos.lastItem) + tids[ntids_page++] = so->currPos.items[so->currPos.itemIndex++].heapTid; - /* Save tuple ID, and continue scanning */ - heapTid = &so->currPos.items[so->currPos.itemIndex].heapTid; - tbm_add_tuples(tbm, heapTid, 1, false); - ntids++; - } + tbm_add_tuples(tbm, tids, ntids_page, false); + ntids_total += ntids_page; } - /* Now see if we need another primitive index scan */ } while (so->numArrayKeys && _bt_start_prim_scan(scan)); - return ntids; + return ntids_total; } /* diff --git a/src/backend/access/spgist/spgscan.c b/src/backend/access/spgist/spgscan.c index 2cc5f06f5d7..8ec4ef86b05 100644 --- a/src/backend/access/spgist/spgscan.c +++ b/src/backend/access/spgist/spgscan.c @@ -928,8 +928,17 @@ storeBitmap(SpGistScanOpaque so, ItemPointer heapPtr, SpGistLeafTuple leafTuple, bool recheck, bool recheckDistances, double *distances) { + const int b = recheck ? 1 : 0; + Assert(!recheckDistances && !distances); - tbm_add_tuples(so->tbm, heapPtr, 1, recheck); + + if (so->ntbmTids[b] == SPGIST_TBM_BATCH_SIZE) + { + tbm_add_tuples(so->tbm, so->tbmTids[b], so->ntbmTids[b], recheck); + so->ntbmTids[b] = 0; + } + + so->tbmTids[b][so->ntbmTids[b]++] = *heapPtr; so->ntids++; } @@ -943,9 +952,14 @@ spggetbitmap(IndexScanDesc scan, TIDBitmap *tbm) so->tbm = tbm; so->ntids = 0; + so->ntbmTids[0] = 0; + so->ntbmTids[1] = 0; spgWalk(scan->indexRelation, so, true, storeBitmap); + for (int i = 0; i < 2; i++) + tbm_add_tuples(so->tbm, so->tbmTids[i], so->ntbmTids[i], i == 1); + return so->ntids; } diff --git a/src/include/access/spgist_private.h b/src/include/access/spgist_private.h index ec6d6f5f74d..42766341889 100644 --- a/src/include/access/spgist_private.h +++ b/src/include/access/spgist_private.h @@ -22,6 +22,8 @@ #include "utils/geo_decls.h" #include "utils/relcache.h" +#define SPGIST_TBM_BATCH_SIZE 256 + typedef struct SpGistOptions { @@ -221,6 +223,9 @@ typedef struct SpGistScanOpaqueData TIDBitmap *tbm; /* bitmap being filled */ int64 ntids; /* number of TIDs passed to bitmap */ + ItemPointerData tbmTids[2][SPGIST_TBM_BATCH_SIZE]; + int ntbmTids[2]; /* number of buffered TIDs */ + /* These fields are only used in amgettuple scans: */ bool want_itup; /* are we reconstructing tuples? */ TupleDesc reconTupDesc; /* if so, descriptor for reconstructed tuples */ -- 2.55.0