Skip to content

Index engine work: in-memory mode, compact keys, B-tree housekeeping ​

Runs: Chromium 153, 5 reps each. Medians are shown. A verdict is given only when the min–max ranges do not overlap. All engines returned identical results in every run.

  • CPU-only (smaltdb :memory: vs SQLite :memory:):
    • before: bench/results/2026-11-01-chromium-2eb08b9-cpu.json;
    • after: bench/results/2026-11-01-chromium-859fa58-cpu.json.
  • OPFS (smaltdb vs the faster of SQLite WAL / TRUNCATE):
    • before: bench/results/2026-11-01-chromium-9f1f951.json;
    • after: bench/results/2026-11-01-chromium-859fa58.json.

What changed ​

  1. In-memory database mode (ADR 0006): no file, WAL or page checksums. Rollback uses undo images held in reusable slabs. The browser bench has smaltdb-mem / sqlite-mem for a CPU-only view.
  2. Compact index keys (format 2, ADR 0006): numbers take 2–4 bytes instead of 11, plus a rowid suffix with a length byte. 200k-row indexes: a(b) 766 → 397 pages, a(d,c) 1,040 → 455, a(lower(e)) 636 → 478.
  3. CREATE INDEX packs entries into one buffer and sorts them with a native numeric sort on (4-byte prefix, position). It then builds the B-tree bottom-up (BTree.bulkLoadKeys) instead of inserting key by key.
  4. UPDATE skips indexes whose columns did not change and reuses encoders instead of copying keys.

CPU-only (in memory, both engines) ​

Workloadsmaltdb beforesmaltdb afterSQLiteAfter vs SQLite
CREATE INDEX, 1 column145 ms78 ms40 ms0.51× slower
CREATE INDEX, 2 columns185 ms81 ms81 msno clear difference
CREATE INDEX on lower(e)280 ms151 ms66 ms0.44× slower
Equality lookups ×10k94 ms70 ms79 ms1.12× faster
Range lookups ×1k730 ms688 ms508 ms0.74× slower
Composite lookups ×1k39.5 ms29.0 ms31.0 msno clear difference
Expression lookups ×1k122 ms121 ms180 ms1.49× faster
UPDATE indexed column, 10k rows93 ms85 ms28.5 ms0.33× slower
DELETE 10k rows54 ms46.5 ms25.6 ms0.55× slower
Bulk insert 100k, 3 indexes579 ms479 ms685 ms1.43× faster

OPFS (smaltdb vs best crash-safe SQLite) ​

Workloadsmaltdb beforesmaltdb afterBest SQLiteAfter vs SQLite
CREATE INDEX, 1 column196 ms92 ms180 ms1.96× faster
CREATE INDEX, 2 columns279 ms130 ms284 ms2.18× faster
CREATE INDEX on lower(e)338 ms179 ms216 ms1.21× faster
Equality lookups ×10k88.8 ms69.9 ms84.6 ms1.21× faster
Range lookups ×1k696 ms721 ms514 ms0.71× slower
Composite lookups ×1k38.8 ms29.3 ms30.1 msno clear difference
Expression lookups ×1k118 ms112 ms179 ms1.60× faster
UPDATE indexed column, 10k rows236 ms229 ms1,325 ms5.8× faster
DELETE 10k rows197 ms168 ms668 ms4.0× faster
Bulk insert 100k, 3 indexes756 ms607 ms1,105 ms1.82× faster
Load join tables (612k rows, 2 indexes)1,959 ms1,415 ms2,495 msfaster

The non-index workloads (OLTP, analytic, joins) are unchanged within run-to-run noise.

Still behind SQLite, CPU-only ​

  • UPDATE (3×) and DELETE (1.8×). The cost is spread across several places:

    • UPDATE runs in two passes: collect rowids, then re-read each row by rowid;
    • every index and table operation searches down from the root;
    • fully packed pages are compacted on insert after a delete (no free-space list in pages);
    • undo images are copied for each modified page.

    Candidates: a leaf-position hint for consecutive operations on the same leaf; a one-pass UPDATE for statements that cannot see their own changes; free-space lists in pages.

  • Range lookups (0.74×). Fetching each table row by rowid costs a full descent. Candidates: the same leaf hint, or sorting the rowids before fetching for unordered aggregates.

  • Expression CREATE INDEX (0.44×). Each row is fully decoded and the expression runs through the generic evaluator.

  • Table size. Rows are 2.3× SQLite's because integers take a fixed 8 bytes. This is deferred to the columnar milestone, as recommended.

  • Index entry overhead. Each entry still carries a 7-byte cell header and a 2-byte slot.

Round 2: sorted rowid fetch, format 3, faster CREATE INDEX ​

Changes:

  1. Sorted rowid fetch. An index scan whose row order cannot be observed sorts its rowids in batches of 4,096 and walks the table forward.
  2. Format 3 (ADR 0007): varint leaf cell headers (2 bytes for an index entry instead of 7) and per-page free-block lists.
  3. CREATE INDEX. A radix sort on key prefixes, and expression indexes decode only the columns they read.

Sizes (200k rows): table 1,427 → 1,307 pages; a(b) 397 → 275 (SQLite 263); a(d, c) 455 → 332 (SQLite 463); a(lower(e)) 478 → 355 (SQLite 286).

Runs:

  • bench/results/2026-11-01-chromium-4feec71-cpu.json (CPU-only);
  • bench/results/2026-11-01-chromium-4feec71.json (OPFS).

Each is compared with the round-1 runs above.

CPU-only (both engines in memory) ​

WorkloadRound 1Round 2SQLiteRound 2 vs SQLite
CREATE INDEX, 1 column78 ms73 ms41 ms0.56× slower
CREATE INDEX, 2 columns81 ms60 ms81 ms1.34× faster
CREATE INDEX on lower(e)151 ms120 ms69 ms0.57× slower
Equality lookups ×10k70 ms73 ms78 msno clear difference
Range lookups ×1k688 ms495 ms534 msno clear difference (was 0.74×)
Composite lookups ×1k29.0 ms28.7 ms29.6 msno clear difference
Expression lookups ×1k121 ms114 ms182 ms1.60× faster
UPDATE indexed column, 10k rows85 ms73 ms28 ms0.38× slower
DELETE 10k rows46.5 ms46.8 ms26.3 ms0.56× slower
Bulk insert 100k, 3 indexes479 ms460 ms664 ms1.44× faster

OPFS (smaltdb vs the faster crash-safe SQLite mode) ​

WorkloadRound 1Round 2Best SQLiteRound 2 vs SQLite
CREATE INDEX, 1 column92 ms78 ms163 ms2.10× faster
CREATE INDEX, 2 columns130 ms102 ms302 ms2.96× faster
CREATE INDEX on lower(e)179 ms139 ms196 ms1.41× faster
Equality lookups ×10k70 ms74 ms78 msno clear difference
Range lookups ×1k721 ms484 ms520 msno clear difference (was 0.71×)
Composite lookups ×1k29.3 ms29.7 ms31.1 msno clear difference
Expression lookups ×1k112 ms126 ms181 ms1.44× faster
UPDATE indexed column, 10k rows229 ms213 ms1,331 ms6.2× faster
DELETE 10k rows168 ms166 ms654 ms3.9× faster
Bulk insert 100k, 3 indexes607 ms612 ms1,118 ms1.83× faster

On OPFS, smaltdb is now at least even with SQLite on every index workload. Non-index workloads are unchanged within noise.

Still behind, CPU-only ​

  • UPDATE (0.38×) and DELETE (0.56×): two passes, a root-to-leaf search per operation, and undo copies of modified pages.
  • 1-column and expression CREATE INDEX (about 0.56×): the cost is now spread across the scan loop, the bulk build and the key encoding.

MIT License