Performance¶
This page tracks the latest verified local benchmark snapshot rather than an old best-case run. Benchmarks are meant to be reproducible, not magical; always rerun them on your own workload and hardware.
Latest Verified Snapshot¶
- Snapshot date / commit: 2026-08-07,
08af3b2onmain - System: macOS 26.6, Apple arm (10 cores), 32 GB RAM
- Stack: Python 3.12.2, ApexBase 1.28.0, SQLite 3.46.0, DuckDB 1.1.3, PyArrow 23.0.1
- Dataset: 1,000,000 rows x 5 columns (
name,age,score,city,category) - Vector dataset: 1,000,000 vectors x dim=128,
k=10, batch size 10 queries - Method: 2 warmup iterations + 5 timed iterations
- Layout: the default benchmark entrypoint tracks the README public scoreboard: 77 fair cross-engine tabular metrics (45 OLAP + 32 OLTP, including the five table-operation metrics added in v1.28.0) plus six competitive vector metrics. Apex-only vector metrics and other extended diagnostics live in
benchmarks/bench_vs_sqlite_duckdb_extended.py. - Fairness rule: only the default fair OLAP/OLTP cross-engine tables count toward the win/loss summary. Vector similarity uses a separate dataset and its own ApexBase-vs-DuckDB scoreboard.
Scoreboard¶
| Scope | Metrics | Apex wins | Ties | Slower |
|---|---|---|---|---|
| Default fair (OLAP + OLTP) | 77 | 75 | 0 | 2 |
| OLAP fair | 45 | 45 | 0 | 0 |
| OLTP fair | 32 | 30 | 0 | 2 |
| Vector similarity (ApexBase vs DuckDB) | 6 | 6 | 0 | 0 |
Total verified: 83 metrics (77 tabular + 6 vector). The two slower tabular
metrics are Table DROP and Table CREATE+DROP cycle against SQLite's
empty-table operations; ApexBase remains faster than DuckDB on both (see
Table Ops).
Stock SQLite is not ranked in the vector table because the built-in sqlite3
used here has no native vector distance/top-k functions in this harness.
Representative OLAP Gaps¶
| Metric | ApexBase | SQLite | DuckDB | Gap to best other |
|---|---|---|---|---|
| COUNT(*) | 0.087 ms | 7.83 ms | 0.528 ms | 6.1x faster vs DuckDB |
| SELECT * LIMIT 100 (warm cache) | 0.071 ms | 0.120 ms | 0.235 ms | 1.7x faster vs SQLite |
| Filtered LIMIT 100 (age>30) | 0.016 ms | 0.122 ms | 0.415 ms | 7.6x faster vs SQLite |
| GROUP BY city (10 groups) | 1.52 ms | 355.31 ms | 2.92 ms | 1.9x faster vs DuckDB |
| Temp Table (CSV) Query (filter+agg) | 0.429 ms | N/A | 0.668 ms | 1.6x faster vs DuckDB |
Representative OLTP Gaps¶
| Metric | ApexBase | SQLite | DuckDB | Gap to best other |
|---|---|---|---|---|
| Bulk Insert (N rows; default fair) | 225.19 ms | 1.01 s | 178.57 s | 4.5x faster vs SQLite |
| Point Lookup (SQL by ID) | 2.92 us | 4.38 us | 3.09 ms | 1.5x faster vs SQLite |
| Retrieve Many (SQL, 100 IDs) | 0.195 ms | 0.435 ms | 5.49 ms | 2.2x faster vs SQLite |
| FTS Index Build (name,city,category) | 1.50 ms | 1.51 s | 1.32 s | 880x faster vs DuckDB |
| FTS Search ('Electronics') | 5.38 ms | 29.49 ms | 23.06 ms | 4.3x faster vs DuckDB |
Table Ops (new in v1.28.0)¶
v1.28.0 added five cross-engine table-operation metrics to the fair OLTP
scoreboard: Table CREATE (1 col), Table DROP, Table CREATE+DROP cycle,
List tables (10), and ALTER TABLE ADD COLUMN. ApexBase wins 3 of 5; the two
losses are against SQLite's minimal empty-table DDL path, while ApexBase stays
ahead of DuckDB on every one of them.
| Metric | ApexBase | SQLite | DuckDB | Gap |
|---|---|---|---|---|
| Table CREATE (1 col) | 0.012 ms | 0.037 ms | 0.139 ms | 3.1x faster vs SQLite |
| Table DROP | 0.049 ms | 0.030 ms | 0.142 ms | 1.6x slower vs SQLite; 2.9x faster vs DuckDB |
| Table CREATE+DROP cycle | 0.107 ms | 0.069 ms | 0.346 ms | 1.5x slower vs SQLite; 3.2x faster vs DuckDB |
| List tables (10) | 5.96 us | 0.031 ms | 2.71 ms | 5.2x faster vs SQLite |
| ALTER TABLE ADD COLUMN | 0.057 ms | 0.074 ms | 0.128 ms | 1.3x faster vs SQLite |
Representative Vector Gaps¶
SQLite is excluded here because stock sqlite3 in this harness has no native vector distance/top-k support.
Single-query rows compare one materialized TopK result from each engine. Batch rows compare ApexBase's batch_topk_distance() with ten DuckDB single-query SQL calls over the same deterministic query batch; every DuckDB result is materialized before the next query runs.
| Metric | ApexBase | DuckDB | Gap to DuckDB |
|---|---|---|---|
| TopK L2 | 7.73 ms | 32.50 ms | 4.2x faster |
| TopK Cosine | 7.87 ms | 43.32 ms | 5.5x faster |
| TopK Dot | 7.61 ms | 30.39 ms | 4.0x faster |
| Batch TopK L2 (10 queries) | 59.37 ms | 357.23 ms | 6.0x faster |
| Batch TopK Cosine (10 queries) | 70.25 ms | 480.35 ms | 6.8x faster |
| Batch TopK Dot (10 queries) | 59.35 ms | 475.29 ms | 8.0x faster |
Hot-Path Latency Snapshot¶
The tables below come from the extended diagnostics profile
(benchmarks/bench_vs_sqlite_duckdb_extended.py) and are not part of the fair
scoreboard. They answer a different question: how fast is the already-loaded
hot path, and what happens when durability or transaction semantics are made
explicit?
Snapshot: 200,000 rows, 2 warmup iterations + 3 timed iterations, median per-call hot-path latency, recorded 2026-08-07.
Default Microbenchmarks¶
| Metric | ApexBase | SQLite | DuckDB | Gap to best other |
|---|---|---|---|---|
| COUNT(*) (direct API) | 4.0 us | 1.259 ms | 0.145 ms | 36x faster vs DuckDB |
| Point lookup (projected SQL) | 3.0 us | 3.0 us | 1.691 ms | tied with SQLite |
| Retrieve 100 IDs (projected SQL) | 0.034 ms | 0.112 ms | 4.532 ms | 3.3x faster vs SQLite |
| Insert 1 row (default fair) | 0.012 ms | 0.015 ms | 0.275 ms | 1.3x faster vs SQLite |
| UPDATE by ID | 3.0 us | 5.0 us | 0.491 ms | 1.7x faster vs SQLite |
| DELETE missing ID | 0.4 us | 4.0 us | 0.385 ms | 10x faster vs SQLite |
Durable Fair Microbenchmarks¶
| Metric | ApexBase | SQLite | DuckDB | Gap to best other |
|---|---|---|---|---|
| Insert 1 row (durable fair) | 0.167 ms | 0.121 ms | 33.185 ms | 1.4x slower vs SQLite; 199x faster vs DuckDB |
| UPDATE by ID (durable fair) | 4.0 us | 7.0 us | 4.479 ms | 1.8x faster vs SQLite |
Transaction Fair Microbenchmarks¶
| Metric | ApexBase | SQLite | DuckDB | Gap to best other |
|---|---|---|---|---|
| TXN empty (BEGIN+COMMIT; durable sync) | 2.0 us | 5.0 us | 0.177 ms | 2.5x faster vs SQLite |
| TXN read COUNT(*) (COMMIT; durable sync) | 0.023 ms | 1.345 ms | 0.320 ms | 13.9x faster vs DuckDB |
| TXN backlog string miss (COMMIT; 1500 preseed; durable sync) | 0.063 ms | 9.088 ms | 0.412 ms | 6.5x faster vs DuckDB |
| TXN backlog COUNT(*) (COMMIT; 1500 preseed; durable sync) | 0.024 ms | 1.459 ms | 0.312 ms | 13.0x faster vs DuckDB |
| TXN backlog INSERT+read-own-name (COMMIT; 1500 preseed; durable sync) | 0.374 ms | 9.929 ms | 36.439 ms | 26.5x faster vs SQLite |
OLAP Throughput (Q/s)¶
The Q/s harness runs a mixed analytical read profile (COUNT(*), two GROUP BY
scans, and a filtered LIMIT 100, materialized to Python rows) on a freshly
loaded copy of the dataset table (200,000 rows, 2026-08-07, extended profile).
| Engine | Single thread Q/s | 4 threads Q/s |
|---|---|---|
| ApexBase | 9,216.3 | 15,068.8 |
| SQLite | 34.8 | 124.5 |
| DuckDB | 878.4 | 2,633.6 |
Measurement notes:
- The harness re-creates each engine on a clean loaded table before sampling (the same mechanism the OLTP microbenchmark sections use), so the profile measures steady-state read throughput rather than a table left in a delta-heavy state by the calibrated single-row DML microbenchmarks.
- The earlier published throughput snapshot (ApexBase ~123,700 Q/s single-threaded) came from a harness that created fresh engines but never loaded the dataset, so it measured empty-table query overhead and is not comparable to the current real-data numbers.
OLTP Write Visibility¶
ApexBase exposes two fast single-row append paths, and the benchmark keeps them out of the fair scoreboard because their visibility rules are Apex-specific:
- Memtable OLTP is the default fast single-row path for schema-stable
store({...})calls withdurability="fast". The writing client can read the row immediately, managed clients in the same Python process share the storage instance, andflush()/close()persists pending rows. - Buffered OLTP is explicit: call
begin_buffered_writes(), issue many single-rowstore({...})calls, then callflush_buffered_writes()orend_buffered_writes(flush=True). Buffered rows are not visible until flushed.
That separation is deliberate: the fair tables compare committed cross-engine behavior, while Apex-only write modes remain visible as diagnostics instead of being mixed into the competitive summary.
Reproduce¶
Use the same command as the snapshot above:
Add --skip-vector if you want a tabular-only rerun without the separate vector module.
Run python benchmarks/bench_vs_sqlite_duckdb_extended.py --rows 200000 --warmup 2 --iterations 3 for the file-format, materialization, Q/s, microbenchmark, durable, transaction, buffered/memtable, and full vector diagnostics. Add --output path.json to keep a machine-readable report of any run.
Out-of-core file comparison¶
Use the focused harness to compare ApexBase and DuckDB against the exact same generated CSV or Parquet source:
python benchmarks/bench_out_of_core_import.py --rows 1000000 --format csv
python benchmarks/bench_out_of_core_import.py --rows 1000000 --format parquet
Each engine runs in an isolated process. The report separates direct file query
time, disk-backed table materialization time, repeated native-table query time,
incremental peak RSS, and storage size. Results and filtered row counts are
cross-checked before ratios are printed. DuckDB defaults to a 1GB memory limit
and an explicit spill directory; change it with --memory-limit 512MB. Increase
--rows until the generated source exceeds physical memory for a true
out-of-core stress run. Ratios are reported as ApexBase divided by DuckDB, so a
value below 1.0x is better for ApexBase.
On the same Apple Silicon development machine, the focused 1,000,000-row Parquet run (21 measured queries after 5 warmups) produced this verification snapshot:
| Metric | ApexBase | DuckDB | ApexBase / DuckDB |
|---|---|---|---|
| Direct filtered Parquet count | 1.35 ms | 9.75 ms | 0.138x |
| Disk-backed materialization | 0.131 s | 0.278 s | 0.469x |
| Filter + GROUP BY + COUNT/AVG | 1.175 ms | 3.862 ms | 0.304x |
| Incremental peak RSS | 77.8 MB | 126.9 MB | 0.613x |
| Native storage size | 9.5 MiB | 6.5 MiB | 1.454x |
The native storage result first decreased from 33.9 MiB to 27.4 MiB by
selecting string dictionary encoding from its actual serialized size instead
of a periodic cardinality sample. One-, two-, or four-byte string dictionary
indices reduced it further to 22.6 MiB. Temp materializations now omit the
physical _id array when a row group's IDs are contiguous, reconstructing IDs
from min_id only in .apex_tmp files; this keeps mutable-table storage and
DML paths on their established format. Lossless low-cardinality Float64
dictionaries then reduce this workload to 9.5 MiB. The float encoding is used
only for at least 32K rows when sampling shows useful repetition and its
serialized size is below 70% of plain storage; high-cardinality data retains
the original encoding. The fused range-filter aggregation evaluates each
dictionary value once and scans compact row indices without materializing a
full float vector. Nullable inputs and more complex SQL shapes deliberately
fall back to the general executor.
Blob storage has a focused Lance comparison harness:
The script measures write throughput, non-blob projection scans, descriptor metadata reads, random full blob reads, random range reads, and projected blob materialization. It uses Lance Blob helpers when the installed Lance package exposes them, and reports unavailable or fallback modes explicitly.
For a larger stress run, increase --rows to 1000000.