From cb065e674f3435275651e289588f3c034426b22b Mon Sep 17 00:00:00 2001 From: David Geier Date: Wed, 7 Oct 2026 16:26:52 +0200 Subject: [PATCH v4] 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 | 26 ++++++++++++------- src/backend/access/nbtree/nbtree.c | 40 +++++++++-------------------- src/backend/access/spgist/spgscan.c | 16 +++++++++++- src/include/access/spgist_private.h | 5 ++++ 7 files changed, 119 insertions(+), 44 deletions(-) diff --git a/contrib/bloom/blscan.c b/contrib/bloom/blscan.c index 1a0e42021ec..d22584ae173 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. */ @@ -80,10 +82,12 @@ blgetbitmap(IndexScanDesc scan, TIDBitmap *tbm) BlockNumber blkno, npages; int i; + int ntids_batch = 0; BufferAccessStrategy bas; BloomScanOpaque so = (BloomScanOpaque) scan->opaque; BlockRangeReadStreamPrivate p; ReadStream *stream; + ItemPointerData tids[BLOOM_TBM_BATCH_SIZE]; if (so->sign == NULL) { @@ -172,7 +176,13 @@ 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++; } } @@ -182,6 +192,8 @@ blgetbitmap(IndexScanDesc scan, TIDBitmap *tbm) CHECK_FOR_INTERRUPTS(); } + tbm_add_tuples(tbm, tids, ntids_batch, true); + Assert(read_stream_next_buffer(stream, NULL) == InvalidBuffer); read_stream_end(stream); FreeAccessStrategy(bas); 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 63846c10840..3b8ad50bec1 100644 --- a/src/backend/access/hash/hash.c +++ b/src/backend/access/hash/hash.c @@ -356,21 +356,29 @@ 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; + const int lastItem = so->currPos.lastItem; - /* - * _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++; + for (int i = so->currPos.firstItem; i <= lastItem; i++) + tids[ntids_page++] = so->currPos.items[i].heapTid; + + /* _bt_next() advanced to the next page if itemIndex is past lastItem. */ + so->currPos.itemIndex = lastItem + 1; + 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..9fcb8d7d652 100644 --- a/src/backend/access/nbtree/nbtree.c +++ b/src/backend/access/nbtree/nbtree.c @@ -291,45 +291,29 @@ 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; + const int lastItem = so->currPos.lastItem; - 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; - } + for (int i = so->currPos.firstItem; i <= lastItem; i++) + tids[ntids_page++] = so->currPos.items[i].heapTid; - /* Save tuple ID, and continue scanning */ - heapTid = &so->currPos.items[so->currPos.itemIndex].heapTid; - tbm_add_tuples(tbm, heapTid, 1, false); - ntids++; - } + /* _bt_next() advanced to the next page if itemIndex is past lastItem. */ + so->currPos.itemIndex = lastItem + 1; + 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