[PATCH v1] Batch B-tree TIDs when building a bitmap

Started by Tomás Senart1 day ago1 messageshackers
Beta feature

Hackorum builds and tests every patch posted to the lists, not only commitfest submissions. This is Hackorum's own CI rather than the PostgreSQL project's, and it is still under testing - please report anything that looks wrong.

appliessuccessCI history

You can run a PostgreSQL built from this patch straight from Docker, with no checkout and no build:

docker run --rm -p 5432:5432 ghcr.io/hackorum-dev/postgres-patch:t253857
psql -h localhost -U postgres

Built from patchset v1 (message #1), September 20, 2026 at 01:48 PM.

Every patchset is also pushed to a branch of our PostgreSQL fork, so you can check out the same tree CI built. Without a PostgreSQL checkout:

git clone --branch t253857_1 https://github.com/hackorum-dev/postgres.git

In a checkout you already have, add the fork once:

git remote add hackorum https://github.com/hackorum-dev/postgres.git

then, for this patchset and every later one:

git fetch hackorum t253857_1 && git checkout t253857_1

Patchset v1 (message #1) is on t253857_1

Jump to latest
#1Tomás Senart
ts@perfloop.ai

Hi,

I would like review of this B-tree bitmap-scan patch. Perfloop's agent
found and implemented it; the patch and submission packet used AI
assistance. I am the contact.

btgetbitmap calls tbm_add_tuples once per matching TID. The patch batches
saved leaf-page spans to reuse that API's within-call heap-block cache [1]https://github.com/postgres/postgres/commit/f5ae3ba4828ece02bae2d16b4cbce847fbcea850.

The first TID keeps the scalar path. Remaining spans use a buffer with
less than 8 KiB of payload for 8 KiB pages, allocated once per primitive
scan and reused. All-singleton scans avoid allocation. No API, disk format,
planner rule, or TID order changes.

One-, two-, and eight-span tests found no clear target benefit from larger
batches. Eight helped the largest scattered control by 0.97%; one uses
less memory.

On one Linux/x86-64 GCP VM, ten paired runs per query showed:

10M rows, 3M matches: 29.937 ms paired saving (14.2%).
2M rows, 200K matches: 1.766 ms paired saving (13.6%).

These are whole-run mean transaction times from single-client pgbench,
with forced bitmap plans. Copy and allocation costs are included.

Nine controls, including scattered and single-row queries, passed a
predeclared 5% practical-loss ceiling. This does not prove zero regression.

All 240 core regression tests passed. Full test counts, skips, build
limits, paired intervals, and reproduction instructions are attached.
The scripts do not require Perfloop.

Base: master 0c5d6269614e107d1d2d669f82f63f7e232b30c9 (20devel).

The v34 Index Prefetching patch [2]/messages/by-id/attachment/203026/v34-0003-Add-amgetbatch-interface-and-adopt-it-in-nbtree.patch changes the same loop but still passes
one TID per bitmap call. We have not tested the changes together.

I would welcome review of the batching boundary, small-scan path, and
other workload cases.

Thanks,
Tomás

[1]: https://github.com/postgres/postgres/commit/f5ae3ba4828ece02bae2d16b4cbce847fbcea850
[2]: /messages/by-id/attachment/203026/v34-0003-Add-amgetbatch-interface-and-adopt-it-in-nbtree.patch

Attachments:

t253857_1
v1-0001-Batch-B-tree-TIDs-when-building-a-bitmap.patchtext/x-patch; charset=US-ASCII; name=v1-0001-Batch-B-tree-TIDs-when-building-a-bitmap.patchDownload+96-19
results.mdtext/markdown; charset=UTF-8; name=results.mdDownload
reproduce.tar.gzapplication/gzip; name=reproduce.tar.gzDownload