Skip to content

feat(bench): DuckDB driver, Group B only (sub-phase 9.5)#106

Merged
joaoh82 merged 1 commit into
mainfrom
bench-9.5-duckdb
May 7, 2026
Merged

feat(bench): DuckDB driver, Group B only (sub-phase 9.5)#106
joaoh82 merged 1 commit into
mainfrom
bench-9.5-duckdb

Conversation

@joaoh82

@joaoh82 joaoh82 commented May 7, 2026

Copy link
Copy Markdown
Owner

Summary

Sub-phase 9.5 of docs/benchmarks-plan.md. Builds on 9.1–9.4 (#102 / #103 / #104 / #105). Adds the optional duckdb-rs driver, feature-gated and wired into Group B (W7 / W8 / W9) only per the plan's viability section.

Feature What it does
make bench unchanged — SQLRite + SQLite (lean profile, no DuckDB dep)
make bench-duckdb new — same suite + DuckDB on Group B
cargo build -p sqlrite-benchmarks unchanged (no DuckDB dep pulled)
cargo build -p sqlrite-benchmarks --features duckdb builds with libduckdb (~50 MB compiled)

Smoke-run numbers (M1 Pro)

Workload SQLRite SQLite (WAL+NORMAL) DuckDB (default)
W7 SUM (1M rows) ~111 ms ~31 ms ~378 µs (~80× faster than SQLite)
W8 GROUP BY card-10 ~204 ms ~460 ms ~634 µs (~720× faster than SQLite)
W8 GROUP BY card-1k ~1.50 s ~242 ms ~701 µs (~345× faster than SQLite)
W8 GROUP BY card-100k skipped (SQLR-19) ~241 ms ~20.4 ms (~12× faster than SQLite)
W9 JOIN (10k×10k) ~32 s ~2.3 µs ~500 µs (~220× slower than SQLite)

Two findings the plan predicted:

  1. Vectorized columnar wins big on aggregate / GROUP BY workloads. W7 / W8 are exactly the shape DuckDB was built for, and it's ~80–720× faster than the same query on SQLite's tuned WAL+NORMAL profile. That's the "what does a different storage model look like?" sister number from the plan's "Why include DuckDB?" rationale.

  2. DuckDB is much slower than SQLite on per-PK-probe single-row JOIN (W9). ~220× slower. Per-PK-probe + single-row OLTP is SQLite's home turf — DuckDB's analytical optimizer pays much higher per-query overhead. Plan flagged this as "apples-to-oranges"; the directional measurement is the point.

Engineering quirks worth knowing

  • DuckDB's Statement::column_count is only valid AFTER query execution (it reads from the result struct, not the parse tree). The first version of the driver crashed with The statement was not executed yet; fixed by pulling cols from the first Row via Row::as_ref().column_count().
  • DuckDB widens SUM(BIGINT) to HugeInt(i128) defensively even when the result fits in i64. The driver downcasts with a range check; bails loudly on genuine overflow.
  • No prepare_cached on duckdb-rs (the C API doesn't have an LRU). Every query reprepares — honest for the comparison since the parser is part of what we're measuring.

Test plan

  • cargo fmt --all -- --check clean
  • cargo clippy -p sqlrite-benchmarks --all-targets clean (default features)
  • cargo clippy -p sqlrite-benchmarks --features duckdb --all-targets clean
  • Full CI-equivalent: cargo build / test / doc --workspace --exclude {desktop,python,nodejs,benchmarks} green (CI never sees the bench crate; this verifies nothing in the engine broke)
  • cargo test -p sqlrite-benchmarks (default features) passes — 5 tests
  • cargo bench -p sqlrite-benchmarks --features duckdb -- 'duckdb' runs all five DuckDB cells (W7 / W8×3 / W9) end-to-end

Follow-ups still open from prior sub-phases

  • SQLR-21 — Phase 8.1 FTS overflow chaining (W11/W12 corpus cap)
  • SQLR-20 — W9 JOIN inner-side index pushdown (SQLRite)
  • SQLR-19 — W8 GROUP BY high-cardinality blowup (SQLRite)
  • SQLR-18 — W4 single-row INSERT gap (SQLRite)
  • SQLR-17 — desktop-build CI hang (only act if recurs)

What's left

9.6 — Reporting + first published run. scripts/compare.py to render any two JSONs into a Markdown diff table, the first official pinned-host JSON committed under benchmarks/results/, and the canonical docs/benchmarks.md reference + top-level README "Benchmarks" section.

References

🤖 Generated with Claude Code

Adds the optional `duckdb-rs` driver to the bench suite, wired into
W7 / W8 / W9 (Group B only per the plan's viability section —
DuckDB's columnar / vectorized model is meaningful on read-only
SELECTs, aggregates, GROUP BY, and indexed range scans on read-
only data; misleading on per-row INSERTs / UPDATEs / OLTP-shape
PK probes).

Feature-gated: `cargo build -p sqlrite-benchmarks` stays lean and
fast as before. `make bench-duckdb` (and `cargo bench --features
duckdb`) pulls the heavy `libduckdb` cdylib (~50 MB compiled) and
the duckdb-rs crate into the build.

Driver shape mirrors the SQLite driver — open / execute /
execute_with_params / query_one / query_all. Two engine-specific
quirks worth knowing:

  - DuckDB's `Statement::column_count` is only valid AFTER query
    execution (it reads from the result struct, not the parse
    tree). The driver pulls `cols` from the first `Row` via
    `Row::as_ref().column_count()` instead — same numeric answer,
    correctly post-execution.
  - DuckDB widens `SUM(BIGINT)` to `HugeInt(i128)` defensively
    even when the result fits in i64. The driver downcasts with a
    range check; bails loudly on a genuine overflow.

Plan-doc + CLAUDE.md + bench README updated to reflect the new
driver. Makefile gains `make bench-duckdb`. The `duckdb` feature
declaration was empty in 9.1's Cargo.toml (placeholder for this
phase); now wired up properly.

Smoke run (M1 Pro, criterion --warm-up-time 1 --measurement-time 1
--sample-size 10):

  W7  SUM(v) over 1M rows
      sqlrite ~111 ms / sqlite ~31 ms / duckdb ~378 us
      DuckDB ~80x faster than SQLite — vectorized columnar wins.

  W8 GROUP BY at three cardinalities (10/1k/100k)
      duckdb 634 us / 701 us / 20.4 ms
      DuckDB 720x / 345x / 12x faster than SQLite. SQLRite × 100k
      stays skipped (SQLR-19).

  W9  INNER JOIN, per-PK probe (10k×10k)
      duckdb ~500 us
      ~220x SLOWER than SQLite — per-PK-probe single-row JOIN is
      OLTP territory; DuckDB's analytical optimizer pays much
      higher per-query overhead. Plan flagged this as
      "apples-to-oranges"; the directional measurement is the
      point.

Verification:
  - cargo fmt --all -- --check clean
  - cargo clippy -p sqlrite-benchmarks --all-targets clean
  - cargo clippy -p sqlrite-benchmarks --features duckdb --all-targets clean
  - Full CI-equivalent build / test / doc green
    (--exclude desktop / python / nodejs / benchmarks)
  - cargo test -p sqlrite-benchmarks (default features) passes
  - Smoke `cargo bench --features duckdb -- 'duckdb'` runs all
    five DuckDB cells (W7 / W8×3 / W9) end-to-end

Refs: SQLR-16 (parent execution), Q4 (DuckDB inclusion gating).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@joaoh82 joaoh82 merged commit 9ffd55a into main May 7, 2026
15 checks passed
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