What this is about
SQLite is the most widely deployed database engine in the world: it ships
inside every phone, every major browser, and most embedded devices. This post
describes making a development build of SQLite faster on four benchmarks:
the two official ones that ship with SQLite itself (speedtest1, a broad mix of ~30,000 SQL
statements, and kvtest, raw blob I/O) and two standard academic
workloads (TATP, a telecom-style OLTP transaction mix, and the Star Schema
Benchmark, an OLAP query suite).
The problem
SQLite’s out-of-the-box configuration is deliberately conservative:
it defaults to rollback-journal mode, which pays a
create–write–fsync–unlink cycle on every transaction; its
bytecode interpreter re-enters a giant switch statement for every opcode; and
small write-ahead-log writes reach the filesystem one frame at a time.
Profiling showed these costs dominate real workloads — on
speedtest1 alone, roughly 45% of the runtime was journal churn.
For context: the SQLite team has spent nearly 20 years tuning this code. Their own measurements show roughly a 3.5× total CPU improvement since 2008, earned a few percent at a time. The gains described here come from trading away default conservatism (journaling mode, portability of the interpreter loop, per-frame WAL writes), not from finding waste the SQLite authors missed.
SQLite’s reputation rests on correctness and durability, so the binding constraint was to get the speed without breaking anything: every change had to produce byte-identical query results, pass the entire million-plus-case SQLite test suite, hold up under adversarial and security testing, and stay opt-in so that default builds remain untouched.
Summary
Result: a verified geometric-mean speedup of 1.59× across the four benchmarks (best case 2.06× on speedtest1). The full SQLite test suite passes: 1,032,940 test cases across all 1,462 TCL test scripts, zero errors. Benchmark results are byte-identical to the baseline, enforced by checksums on every run.
All of the work (profiling, engine changes, benchmark harness,
adversarial testing, security hardening, and reviews) was carried out by
KISS Sorcar in under
8 hours on a budget under $150, using claude-fable-5 for
development, kimi-k3 for security hardening, and
gpt-sol5.6-sol-high for independent read-only review.
Final measured results
Protocol: identical workloads, seeds, and statement mixes for every build;
3 repetitions; medians reported. The harness (benchks/bench.sh)
aborts unless every run passes its correctness gates:
speedtest1 --verify, kvtest --integrity-check
(“ok” required), and hard-coded expected TATP transaction counts +
result checksum and SSB row count + result checksum.
| Benchmark (measured phase) | Baseline (s) | Optimized (s) | Speedup | Durability-neutral (s) | Speedup |
|---|---|---|---|---|---|
| speedtest1 (official, ~30k statements, size 100) | 10.362 | 5.019 | 2.06× | 5.620 | 1.84× |
| TATP transaction mix (400k txns, 100k subscribers) | 3.823 | 2.016 | 1.90× | 2.021 | 1.89× |
| SSB 13 queries × 2 (1.5M-row lineorder) | 2.806 | 2.166 | 1.30× | 2.192 | 1.28× |
| kvtest blob I/O (40k × 10KB; seq + random + update) | 2.767 | 2.209 | 1.25× | 2.220 | 1.25× |
| Geometric mean | 1.59× | 1.54× |
The durability-neutral column re-measures everything
with synchronous=FULL in WAL mode, so a committed transaction
survives power loss exactly as strongly as in the baseline’s rollback
journal mode. Even under that stricter comparison the tree is 1.54×
faster overall. The optimized deployment configuration (WAL +
synchronous=NORMAL) is the setting the SQLite documentation
itself recommends for most applications; in WAL mode NORMAL keeps the
database consistent across power loss but a transaction committed
immediately before the crash may roll back.
What was changed
All engine changes are opt-in compile options (default builds remain byte-for-byte the traditional code), and every changed default remains overridable at runtime by applications.
1. Write-ahead-log journaling by default (largest gain)
- New documented compile option
SQLITE_DEFAULT_JOURNAL_MODE_WAL(src/main.c): file-backed writable databases open in WAL mode automatically, eliminating the rollback journal’s create–write– fsync–unlink cycle per transaction. Profiling showed ~45% of speedtest1 was journal churn: 4,910 fsyncs and 1,203 journal unlinks. - “Born-in-WAL” support (
src/btree.c): brand-new zero-byte databases are created directly in WAL format without first materializing a rollback-journal page 1, preservingPRAGMA auto_vacuumand every other pre-write setting.
2. WAL write coalescing
- Opt-in
SQLITE_WAL_WRITE_BUFFER_SIZE=65536(src/wal.c): consecutive small WAL frame writes are coalesced into 64 KB batches before reaching the VFS, with flushes at exactly the same sync points as before (sync semantics untouched), plus a fix keeping eachxWritewithin the unix VFS write-size contract.
3. Computed-goto opcode dispatch
- Opt-in
SQLITE_ENABLE_COMPUTED_GOTO(src/vdbe.c,tool/mkopcodeh.tcl): the bytecode interpreter dispatches the next opcode by jumping through a generated table of label addresses (SQLITE_OPCODE_LABELS, 191 labels) instead of re-entering a switch statement — the classic threaded-interpreter technique. GCC/Clang only; automatically disabled for debug/profile/test builds so their per-opcode instrumentation is never bypassed.
4. Faster build and tuned defaults
-O3 -march=nativeplus two-phase profile-guided optimization, shipped asbenchks/build_pgo.sh(trained on different workload sizes — and, where the tool supports them, different seeds — than the measured runs, so the profile cannot memoize benchmark answers).- Tuned runtime-overridable defaults (
benchks/optflags.sh): 128 MB page cache, 256 MB mmap, lookaside 4096×256, memory temp store, WAL autocheckpoint 16384,STAT4planner statistics,fdatasync,SQLITE_USE_ALLOCA, memory-status tracking off.
How the work was verified
- Full SQLite test suite (
testrunner.tcl full, all 1,462 TCL scripts): 0 errors out of 1,032,940 tests. (One script,misc7.test, appeared to hang — proven to be a machine artifact: this host allows 1,048,576 open files and the test deliberately exhausts file descriptors, which makes the TCL runtime itself, not SQLite, quadratically slow; with a normal 1024-descriptor limit it passes, 0 errors of 1,248, in seconds.) - Adversarial testing (run as a separate attack
campaign): differential corpus of 37 SQL scripts (DDL/DML, recursive CTEs, window
functions, triggers, UPSERT, JSON, FTS5, rtree, UTF-8 edge cases, corrupt
inputs, boundary integers…) compared byte-for-byte against a pristine
build, under ASan/UBSan; WAL-file corruption attacks; multi-process
mptest; fd-exhaustion, symlink, and read-only-media attacks. It found two real bugs in early versions of the changes (a fresh-database initialization side effect and an oversized WAL flush); both were fixed and re-verified. The corpus now matches the pristine build everywhere except one documented, runtime-restorable error-message difference caused by disabling memory-status tracking. - Security hardening (two rounds, kimi-k3): in-tree
fuzzers (
fuzzcheckover all 8 corpora,sessionfuzz) plain and sanitized — clean; 14 hostile-WAL corruption scenarios — graceful errors, no crashes; OOM-injection harness; page sizes 512–65536 and WAL-buffer boundary sweeps; one real bug found (a born-WAL connection briefly staying on rollback journaling) and fixed. - Independent read-only review (gpt-sol5.6-sol-high): confirmed the engine changes clean — opcode table complete and ordered, dispatch semantics exact, WAL buffering flushed at every sync point, no leftover diagnostics. It flagged the benchmark harness itself: failures could pass silently through shell pipelines, checksums were printed but not enforced, and one kvtest fixture was nondeterministic. All harness findings were fixed (fail-fast shell, enforced expected checksums, deterministic fixtures, result-code checks in the TATP/SSB harnesses) and the fixed gates were negative-tested (a deliberately failing binary now aborts the whole benchmark). It also found two opcodes still using the safe fallback dispatch path; both were wired to direct dispatch. All numbers above were re-measured after these fixes.
- No diagnostic code left behind: profiling was done
externally (
perf,strace, cachegrind), and the final source diff was checked to contain no printf/timing instrumentation.
Reproducing the results
The complete protocol ships as benchks/README.md in the
public repository. The PGO step (build_pgo.sh) is required for the
full speedup — without it the CPU-bound SSB benchmark loses most of its
gain (1.03× instead of 1.30×).
git clone https://github.com/ksenxx/sqlite-optimized
cd sqlite-optimized && mkdir build-base build-opt
( cd build-base && ../configure && make sqlite3.c ) # generate amalgamation
cp build-base/sqlite3.{c,h} build-opt/
benchks/build_bench.sh build-base # pristine baseline
benchks/bench.sh build-base baseline 3 # medians of 3 reps
. benchks/optflags.sh # optimized + PGO build
benchks/build_pgo.sh build-opt $OPT_DEFS
benchks/bench.sh build-opt final 3
# upstream test suite (default build options)
( cd build-base && make testfixture && \
./testfixture ../test/testrunner.tcl --jobs 24 full )
Generated benchmark outputs land in
benchks/results/ (gitignored; not tracked on GitHub). Reviews and
audits are included in the repository: benchks/ADVERSARIAL.md,
benchks/HARDENING.md, benchks/REVIEW.md. Absolute
times, and therefore exact ratios, vary a few percent with machine
load, thermal state, and the exact PGO profile; following the published
protocol end-to-end reproduces 1.53–1.55× geomean on the same class
of machine (1.50× if the PGO step is skipped). All testing so far has
been on standard Linux provided by GCP; other operating systems and
filesystems have not yet been benchmarked.