# 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).