DuckDB extension: datoms in DuckDB (1.11) vs a SQLite file (1.10.3)

Same dataset (benchmarks/scale gen.py), same extension calls, one client, release builds, DuckDB v1.5.6 Python client, EC2 c7i.8xlarge (32 vCPU, Xeon 8488C, 61 GiB). 1.10.3’s extension keeps its datoms in a SQLite file (run with an in-memory DuckDB); 1.11’s keeps them in tables of a persistent DuckDB database. Driver: ab.sh (SC=xs|s); tables: abtab.py. Correctness is checked separately (tests-harness differential test vs SQLite: 0 disagreements), so these are timings only.

xs (~0.1M datoms)

scenario (p50 ms, 1 client) 1.10.3: datoms in a SQLite file 1.11: datoms in DuckDB new / old
point_lookup 0.58 2.79 4.78
ref_traversal 2.62 6.80 2.60
aggregate 3.02 3.68 1.22
predicate_scan 3.47 9.97 2.87
pull 0.55 2.98 5.43
as_of 8.77 19.98 2.28
since 1.26 2.34 1.85
input_bindings 0.92 4.90 5.33
bulk load (s) 2.3 5.7 2.51
single-datom transact (tx/s) 186 66 0.36
store size (MB) 21 16 0.74

s (~1M datoms)

scenario (p50 ms, 1 client) 1.10.3: datoms in a SQLite file 1.11: datoms in DuckDB new / old
point_lookup 0.60 3.23 5.40
ref_traversal 22.39 10.37 0.46
aggregate 30.15 5.53 0.18
predicate_scan 28.89 40.43 1.40
pull 0.56 3.83 6.85
as_of 88.57 25.46 0.29
since 5.25 2.73 0.52
input_bindings 1.12 8.78 7.82
bulk load (s) 177.4 62.7 0.35
single-datom transact (tx/s) 182 54 0.29
store size (MB) 213 157 0.74

Reading it

  • Faster on DuckDB at 1M datoms: queries that touch many datoms. Aggregates 5.5x, as-of 3.5x, ref traversal 2.2x, since 1.9x, and bulk load 2.8x. The store is a quarter smaller. These get better with scale, because DuckDB scans columns and the SQLite store probes its indexes row by row.
  • Slower on DuckDB: point lookups, pull and :in bindings, by about 3-8 ms each, and single-datom transactions (~55 tx/s vs ~180). These are per-statement costs. Each call runs several statements (a schema-generation check, the query, and for pull the attribute fetch), and DuckDB plans and runs each in ~0.2-1 ms, where SQLite answers an indexed lookup in microseconds. DuckDB can’t index the value column (a UNION) and doesn’t use its ART indexes for these joins, so a value lookup is a scan of the attribute’s rows.
  • Two optimizations got it here, both measured at xs:
    • Caching each store’s schema per thread (per-call overhead 4.4 -> 2.1 ms).
    • Plain typed copies of the value (v_i/v_d/v_s) for filters, joins and comparisons. Ref traversal went 12.1 -> 6.8 ms; on the UNION alone, DuckDB took 16 ms for a join that takes 3.3 ms on plain columns.
  • Not done (possible next steps): batch a call’s statements; a connection pool so calls run concurrently (they are serialized on one connection today); clustering datoms by (a, e) for zone-map pruning (2.6 vs 3.3 ms on the ref traversal probe).