Repository navigation
btree: interpolation search for rowid seeks using parent-page knots - #2311
Draft
timohuovinen wants to merge 1 commit into
Draft
timohuovinen wants to merge 1 commit into
timohuovinen wants to merge 1 commit into
Conversation
sqlite3BtreeTableMoveto() binary-searches every page and ignores the key
values. Rowids are integers and usually close to evenly spaced within a
page, and the search already knows each child's key range when it
descends: the interior cell pointing at the child holds the child's
largest rowid and its left neighbour bounds the child from below.
Use those bounds ("knots") to choose where the existing search loop
probes: first the interpolated slot, then steps of 1, 2, 4, ... cells
away from it until intKey is bracketed, then the usual (lwr+upr)/2. The
loop's own < and > branches record the nearest keys on each side, which
become the knots of the child; a page that lacks a knot (the root and the
left/right edge of the tree) probes its first or last cell to get one.
Only the probe position changes. Any probe inside [lwr,upr] keeps the
binary-search invariant, so results are identical to binary search on
well-formed pages and the worst case stays O(log n); stale separators
only cost probes. An assert() checks that every probe stays inside the
bracket. Applies to pages with at least LIBSQL_INTERP_MIN_CELLS (16)
cells, not to BTREE_APPEND seeks. No file-format or API change.
-DLIBSQL_OMIT_INTERP_SEEK compiles it out; builds with
SQLITE_OMIT_FLOATING_POINT leave it out automatically.
Cells compared per seek (1M rows, 4 KiB pages) fall from 19.2 to 7.1 for
dense rowids and to 11.1 for random 64-bit rowids. Rowid point lookups,
range seeks, joins and inserts are 8-37% faster across six key
distributions; speedtest1's rowid-seeking tests are 2-28% faster and the
other tests run the same instruction count.
The same change is applied to both bundled amalgamations. Adds
test/interpseek.test and documents the options in
doc/libsql_extensions.md.
timohuovinen
force-pushed
the
interp-seek-rowid
branch
from
November 8, 2026 20:58
fc0c1d3 to
12db963
Compare
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #2310
What
sqlite3BtreeTableMoveto()binary-searches every page of a rowid b-tree and ignores the key values. Rowids are integers and usually close to evenly spaced within a page. When the search descends into a child page, it already knows that child's key range: the interior cell pointing at the child holds the child's largest rowid, and the cell to its left bounds it from below.This PR uses those two bounds ("knots") to decide where the existing search loop probes:
(lwr+upr)/2.The loop's existing
<and>branches record the nearest keys on each side, and those become the knots of the child it descends into. A page that lacks a knot (the root, and pages on the left or right edge of the tree) reads its first or last cell to get one.[lwr,upr]keeps the binary-search invariant. On well-formed pages the result is identical, and the worst case is still O(log n). Stale separator keys only cost extra probes. A newassert()checks that every probe stays inside the bracket.LIBSQL_INTERP_MIN_CELLS).BTREE_APPENDseeks.-DLIBSQL_OMIT_INTERP_SEEKcompiles it out. Builds withSQLITE_OMIT_FLOATING_POINTleave it out automatically.src/btree.c: +103 lines, all insidesqlite3BtreeTableMoveto()plus a small inline helper.test/interpseek.test.doc/libsql_extensions.md.Benchmarks
Measured on a Ryzen 7 9700X under WSL2 with gcc 14.2
-O2, pinned to one core.Cells compared per seek (1M rows, 4 KiB pages, 200k random lookups; this count is deterministic):
Micro-benchmark (1M rows, 4 KiB pages, 1M operations per test, 5 rounds). The table shows the time change against current libSQL. The patch is faster in all 30 cells and in 150 of 150 paired samples; the geomean time ratio is 0.768.
On the skewed distributions, interpolation needs about 3% more probes, but they land next to the guess, in the same cache lines, so lookups are still faster.
speedtest1 (user-mode CPU cycles from hardware counters).
Code-layout effects on this machine reached ±6% per test, even for tests that never run this code. To remove them, base and patch were each linked with 6 different random function orders (
lld --shuffle-sections). "Wins" counts the (base layout, patch layout) pairs, out of 36, in which the patch was faster.--size 100file--size 500 --memdbCorrectness
testrunner.tcl veryquick, 1,208 files: 0 errors out of 328,573 tests.make teststeps:srctree-checkandsourcetestpass.fuzztestreports 0 errors out of 46,388 in fuzzcheck and 0 crashes in 339 sessionfuzz cases.SQLITE_DEBUGwithLIBSQL_INTERP_MIN_CELLS=2, so it runs on almost every page, with the bracket assertion on):fuzztest: 0 errors and 0 crashes.veryquick: 1 error out of 328,844, inaltercorrupt-1.1. That test opens a deliberately corrupt database, and the forced build reports the corruption aserror in index … orphan indexinstead of the expecteddatabase disk image is malformed. On a corrupt page the keys aren't sorted, so a different probe order can find a different symptom first.test/interpseek.test(61 tests, about 7 s):INTEGER PRIMARY KEYtable (dense, gaps, random, skewed, negative, INT64 edges) and into a mirror table that can only be scanned.=,<,<=,>,>=andBETWEENat page sizes 512 and 4096.integrity_check.LIBSQL_OMIT_INTERP_SEEK, the forced debug build, and an ASan+UBSan build (-fno-sanitize-recover=all).cargo test -p libsql --test integration_testspasses 31 tests (1 ignored by default).cargo test -p libsql --features encryption --test encryption, which builds the SQLite3MultipleCiphers amalgamation, passes.Notes
--enable-wasm-runtime-dynamicandmake rusttestwasm(no WasmEdge locally), and the fullcargo nextest runworkspace.