Skip to content

fix: expose PRAGMA wal_autocheckpoint tuning for bulk-insert-heavy SQLite workloads#251

Merged
pdlug merged 1 commit into
mainfrom
fix/sqlite-bulk-load-checkpoint
Jul 13, 2026
Merged

fix: expose PRAGMA wal_autocheckpoint tuning for bulk-insert-heavy SQLite workloads#251
pdlug merged 1 commit into
mainfrom
fix/sqlite-bulk-load-checkpoint

Conversation

@pdlug

@pdlug pdlug commented Jul 11, 2026

Copy link
Copy Markdown
Contributor
  • Adds walAutocheckpointPages to createLocalSqliteBackend's pragmas option (PRAGMA wal_autocheckpoint), following the exact pattern already established by cacheSizeKib/mmapSizeBytes: defaults to undefined (SQLite's own built-in default untouched), so existing callers are unaffected. Values are runtime-validated as integers from 0 through 2,147,483,647, matching SQLite's signed 32-bit C API; oversized values are rejected instead of being silently interpreted by SQLite as 0 (which disables automatic checkpointing).
  • Root-caused via the SF10 LDBC SNB benchmark (packages/benchmarks): a real SF10 run showed SQLite taking ~9 hours to bulk-load ~30M rows — ~4.85x slower than Postgres running the exact same bulkInsert code path in-process with no network round trip, and worse-than-linear relative to SF1 (SQLite's load time grew ~13.4x for a ~10.65x row-count increase; Postgres grew ~9.6x, essentially linear).
  • A local repro (real bulkCreate() calls, 100K/500K/2M synthetic rows, real SNB Comment schema/indexes) confirmed the cause: SQLite's default wal_autocheckpoint (1,000 pages, ~4MiB) checkpoints often enough that each checkpoint pays an increasing cost as the database file grows over a bulk load — flushing WAL frames into a B-tree that's larger, and less page-cache-resident, than the one before it.
  • Raising wal_autocheckpoint cut the 2M-row case's wall-clock time by over 50% at the best-performing value, plateauing (then regressing — an oversized WAL has its own costs) around 50,000-100,000 pages.

…Lite workloads

SQLite's default checkpoints WAL back into the main database file every
~4MiB (1,000 pages). Fine for a normal read/write mix, but a large bulk
load pays increasingly expensive checkpoints as the file grows over the
course of the load — each checkpoint flushes WAL frames into a B-tree
that's larger, and less page-cache-resident, than the one before it.

Root-caused via the SF10 LDBC SNB benchmark (packages/benchmarks): a
~9-hour SQLite bulk load, ~4.85x slower than Postgres running the exact
same bulkInsert code path in-process with no network round trip. A local
repro (real bulkCreate() calls, 100K/500K/2M synthetic rows) confirmed
per-row insert cost degrades as the table grows and isolated the cause
to wal_autocheckpoint's untuned default; raising it cut a 2M-row load's
wall-clock time by over 50% at the scale tested, plateauing around
50,000-100,000 pages before regressing again at very large values (an
oversized WAL has its own costs).

Adds walAutocheckpointPages to LocalSqlitePragmaOptions, following the
exact pattern already established by cacheSizeKib/mmapSizeBytes:
defaults to undefined (SQLite's own default untouched), validated as a
non-negative safe integer (0 disables automatic checkpointing entirely,
for callers that would rather run one explicit PRAGMA wal_checkpoint
after a bulk load finishes).
@pdlug pdlug force-pushed the fix/sqlite-bulk-load-checkpoint branch from 283e1c2 to b515b67 Compare July 13, 2026 15:19
@pdlug pdlug merged commit f23f7a5 into main Jul 13, 2026
13 checks passed
@pdlug pdlug deleted the fix/sqlite-bulk-load-checkpoint branch July 13, 2026 16:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant