From 30bfe08c1c78e9eb177103cd545a3944fd24b6af Mon Sep 17 00:00:00 2001 From: Perfloop Agent Date: Sat, 19 Sep 2026 01:01:30 +0200 Subject: [PATCH] Batch B-tree TIDs when building a bitmap Pass each saved leaf-page span to tbm_add_tuples in one call. This lets the existing within-call heap-block cache serve adjacent TIDs. Copy the TIDs because BTScanPosItem does not store them as a contiguous array. Keep the first TID on the scalar path. Use a separate non-inlined helper only when another saved item is available. Allocate one bounded buffer per primitive scan and reuse it for its remaining leaf-page spans. Keep scan order, tuple counts, recheck behavior, and scalar-array progression unchanged. Add regression coverage for duplicate keys in a scalar-array scan and for a small span with both scalar and batched TIDs. --- src/backend/access/nbtree/nbtree.c | 86 +++++++++++++++++++------ src/test/regress/expected/bitmapops.out | 19 ++++++ src/test/regress/sql/bitmapops.sql | 9 +++ 3 files changed, 96 insertions(+), 18 deletions(-) diff --git a/src/backend/access/nbtree/nbtree.c b/src/backend/access/nbtree/nbtree.c index 0abdd7b49f..7196b38378 100644 --- a/src/backend/access/nbtree/nbtree.c +++ b/src/backend/access/nbtree/nbtree.c @@ -284,6 +284,58 @@ btgettuple(IndexScanDesc scan, ScanDirection dir) return res; } +/* + * _btgetbitmap_batch() -- add the rest of a primitive scan in batches + * + * The caller has found a saved leaf-page span with more than one item. + * Keeping the batch state in this helper leaves the common singleton path in + * btgetbitmap with the same local state as the original scalar loop. + */ +static pg_noinline int64 +_btgetbitmap_batch(IndexScanDesc scan, TIDBitmap *tbm) +{ + BTScanOpaque so = (BTScanOpaque) scan->opaque; + int64 ntids = 0; + ItemPointerData *heapTids; + int firstItem; + int lastItem; + int nitems; + + heapTids = palloc_array(ItemPointerData, MaxTIDsPerBTreePage); + firstItem = so->currPos.itemIndex; + lastItem = so->currPos.lastItem; + nitems = lastItem - firstItem + 1; + + for (;;) + { + /* + * The heap TIDs are not contiguous in BTScanPosItem, so copy them + * into a contiguous array before adding them to the bitmap. Pass the + * saved leaf-page span in one call so tbm_add_tuples can reuse its + * current-block lookup cache within the span. + */ + for (int i = 0; i < nitems; i++) + heapTids[i] = so->currPos.items[firstItem + i].heapTid; + tbm_add_tuples(tbm, heapTids, nitems, false); + ntids += nitems; + + /* + * Mark the current page consumed before letting _bt_next move to the + * next one. + */ + so->currPos.itemIndex = lastItem; + if (!_bt_next(scan, ForwardScanDirection)) + break; + + firstItem = so->currPos.itemIndex; + lastItem = so->currPos.lastItem; + nitems = lastItem - firstItem + 1; + } + + pfree(heapTids); + return ntids; +} + /* * btgetbitmap() -- gets all matching tuples, and adds them to a bitmap */ @@ -292,7 +344,6 @@ btgetbitmap(IndexScanDesc scan, TIDBitmap *tbm) { BTScanOpaque so = (BTScanOpaque) scan->opaque; int64 ntids = 0; - ItemPointer heapTid; Assert(scan->heapRelation == NULL); @@ -302,28 +353,27 @@ btgetbitmap(IndexScanDesc scan, TIDBitmap *tbm) /* Fetch the first page & tuple */ if (_bt_first(scan, ForwardScanDirection)) { - /* Save tuple ID, and continue scanning */ - heapTid = &scan->xs_heaptid; - tbm_add_tuples(tbm, heapTid, 1, false); - ntids++; - 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) + /* Keep the current tuple on the original scalar path. */ + tbm_add_tuples(tbm, &scan->xs_heaptid, 1, false); + ntids++; + + ++so->currPos.itemIndex; + if (so->currPos.itemIndex <= so->currPos.lastItem) { - /* let _bt_next do the heavy lifting */ - if (!_bt_next(scan, ForwardScanDirection)) - break; + /* + * A later item in the current span is available, so the + * helper can consume the rest of this primitive scan in + * batches. + */ + ntids += _btgetbitmap_batch(scan, tbm); + break; } - /* Save tuple ID, and continue scanning */ - heapTid = &so->currPos.items[so->currPos.itemIndex].heapTid; - tbm_add_tuples(tbm, heapTid, 1, false); - ntids++; + /* let _bt_next do the heavy lifting */ + if (!_bt_next(scan, ForwardScanDirection)) + break; } } /* Now see if we need another primitive index scan */ diff --git a/src/test/regress/expected/bitmapops.out b/src/test/regress/expected/bitmapops.out index 64068e0469..948d8e3d81 100644 --- a/src/test/regress/expected/bitmapops.out +++ b/src/test/regress/expected/bitmapops.out @@ -44,5 +44,24 @@ SELECT count(*) FROM bmscantest WHERE a = 1 OR b = 1; 2485 (1 row) +-- Test a scalar-array B-tree bitmap scan, including duplicate index keys. +SELECT count(*) FROM bmscantest WHERE a IN (1, 2, 3); + count +------- + 3963 +(1 row) + +-- Two matches on one leaf page must include the scalar and batched TIDs. +CREATE TABLE bmscan_small (a int); +INSERT INTO bmscan_small VALUES (1), (2), (3); +CREATE INDEX bmscan_small_idx ON bmscan_small(a); +SELECT a FROM bmscan_small WHERE a BETWEEN 1 AND 2 ORDER BY a; + a +--- + 1 + 2 +(2 rows) + +DROP TABLE bmscan_small; -- clean up DROP TABLE bmscantest; diff --git a/src/test/regress/sql/bitmapops.sql b/src/test/regress/sql/bitmapops.sql index 1b175f6ff9..a56b7ea871 100644 --- a/src/test/regress/sql/bitmapops.sql +++ b/src/test/regress/sql/bitmapops.sql @@ -42,6 +42,15 @@ SELECT count(*) FROM bmscantest WHERE a = 1 AND b = 1; -- Test bitmap-or. SELECT count(*) FROM bmscantest WHERE a = 1 OR b = 1; +-- Test a scalar-array B-tree bitmap scan, including duplicate index keys. +SELECT count(*) FROM bmscantest WHERE a IN (1, 2, 3); + +-- Two matches on one leaf page must include the scalar and batched TIDs. +CREATE TABLE bmscan_small (a int); +INSERT INTO bmscan_small VALUES (1), (2), (3); +CREATE INDEX bmscan_small_idx ON bmscan_small(a); +SELECT a FROM bmscan_small WHERE a BETWEEN 1 AND 2 ORDER BY a; +DROP TABLE bmscan_small; -- clean up DROP TABLE bmscantest; base-commit: 0c5d6269614e107d1d2d669f82f63f7e232b30c9 -- 2.50.0