Performance baselines
Reproducible size, startup, memory, and throughput measurements against the pinned DuckDB CLI.
This document preserves development-host measurements from v0.1.0 through v0.2.1. They are historical, hardware-specific evidence rather than current release guarantees. Re-run the committed harness before making a comparison against a newer sqrail, DuckDB, compiler, or machine.
This report separates a comparative frontend benchmark from a development sanity check. It is not a query-engine superiority claim: both compared executables contain the same DuckDB source revision.
Measured on 2026-07-26:
| Component | Value |
|---|---|
| Host | Apple M2 Pro, 12 CPU cores, 32 GiB RAM |
| OS | macOS 26.5.1, arm64 |
| Compiler | Apple Clang 21.0.0 |
| CMake | 4.4.0 |
| DuckDB | v1.5.5 |
| sqrail | 0.1.0 |
Executable
| Metric | Result |
|---|---|
| Unstripped build executable | 43,310,344 bytes |
| Stripped release executable | 35,711,416 bytes |
Stripped gzip -9 size |
11,760,406 bytes |
| Dynamic dependencies | system libc++ and libSystem only |
sqrail --version wall time |
approximately 0.01 s |
sqrail --version maximum RSS |
2,932,736 bytes |
sqrail versus the pinned DuckDB CLI
The committed harness built both frontends from DuckDB commit
d8cdaa33fda8df955cc76ef58a280f68f4cd43fa. The
deterministic dataset contains 1,000,000 fact rows, 100,000
dimension rows, a 49,668,679-byte Parquet fact file, and a
65,666,371-byte CSV fact file.
Each timed value is the mean of five warm-cache runs after one warmup, with a 512 MB DuckDB memory limit and two threads. Peak RSS is one separate profiled run. All result pairs had identical row counts and logical checksums.
| Workload | DuckDB CLI mean | sqrail mean | DuckDB CLI RSS | sqrail RSS |
|---|---|---|---|---|
| selective Parquet scan | 38.87 ms | 38.13 ms | 30.9 MiB | 28.4 MiB |
| high-cardinality aggregate | 50.78 ms | 50.86 ms | 49.5 MiB | 44.1 MiB |
| fact-dimension join | 45.53 ms | 40.87 ms | 39.4 MiB | 40.7 MiB |
| partitioned window | 120.44 ms | 113.28 ms | 71.7 MiB | 83.0 MiB |
| CSV to Parquet | 341.25 ms | 327.42 ms | 163.9 MiB | 149.6 MiB |
At this scale, sqrail adds no measurable systematic runtime penalty. Individual differences should not be interpreted as engine improvements: the query engine, plans, data, settings, and output checks are the same, while the frontends have different startup and command-processing paths. The window case also shows why the project reports memory rather than assuming the smaller interface always uses less.
Reproduce with the benchmark instructions.
One-million-row write sanity check
Command:
sqrail run \
--memory 64MB \
--threads 2 \
-o million.parquet \
'SELECT i, i % 100 AS bucket, md5(i::VARCHAR) AS digest
FROM range(1000000) t(i)'
Single observed run:
| Metric | Result |
|---|---|
| Wall time | 0.27 s |
| Maximum RSS | 54,886,400 bytes |
| Output size | approximately 35 MiB |
| Verified rows | 1,000,000 |
| Verified distinct buckets | 100 |
The dataset is generated rather than scanned, and filesystem caching was not controlled. This result only verifies that direct Parquet output works within the stated 64 MB DuckDB memory limit on the development host.
v0.2.0 host-tuned development results
These measurements use the same host and DuckDB revision as the
v0.1.0 baseline. The portable comparison binary uses a conventional
Release build. The host-tuned binary additionally applies ThinLTO
and -mcpu=native; it is not portable and is never used
for release archives.
| Workload | Portable mean | Host-tuned mean | Elapsed reduction |
|---|---|---|---|
| 1M-row Parquet grouped aggregate, 15 runs | 105.0 ms | 93.9 ms | 10.5% |
| 1M-row generated strict JSONL, 10 runs | 612.8 ms | 574.4 ms | 6.3% |
The grouped aggregate used two threads. Both cohorts were warm-cache measurements; the comparison combines native code generation and ThinLTO and does not isolate their individual contributions.
An explicit out-of-core run then fully sorted 10,000,000 Parquet rows with a 128 MiB DuckDB memory limit, two threads, and a 2 GiB spill cap:
| Metric | Observed value |
|---|---|
| Input Parquet | 496,596,101 bytes |
| Harness wall time | 2.123 s |
| Maximum process RSS | 276,168,704 bytes |
| Sampled peak spill | 215,154,688 bytes |
| Output Parquet | 121,696,159 bytes |
| Verified rows | 10,000,000 |
The spill sampler runs every 10 ms, so its maximum is a lower bound.
DuckDB’s memory setting limits its buffer manager rather than total
process RSS. The committed run-out-of-core.sh harness
refuses to replace results and validates the output row count and
logical checksum.
v0.2.1 strict JSON streaming optimization
The expanded matrix exposed a frontend bottleneck that query-engine
tuning cannot fix: converting 10,000,000 Parquet rows to strict
JSONL spent most of its time materializing one DuckDB
Value and one std::string per row. The
revised path reads DuckDB’s unified vector buffer directly and
allocates only for rows that contain a non-finite token requiring
RFC 8259 normalization.
The before and after binaries used the same Release/IPO build tree,
Apple Clang 21, DuckDB commit d8cdaa33f, 512 MB memory
limit, two threads, and an 8 GiB explicit spill cap. Each result is
the mean of five runs after one warmup.
| 10M-row Parquet-to-JSONL path | Mean | Relative to old sqrail |
|---|---|---|
DuckDB CLI COPY |
2.207 s | 3.35x faster |
| sqrail before vector fast path | 7.398 s | baseline |
| sqrail after vector fast path | 2.728 s | 2.71x faster |
All three outputs contained 10,000,000 rows and the same logical
checksum 13911955137382488891. The optimized sqrail
path remains 23.6% slower than DuckDB’s direct JSON
COPY; that comparison is not semantics-equivalent when
data contains NaN or infinities, because DuckDB emits bare
non-standard tokens while sqrail maps them to null.
Unit and smoke tests verify the strict conversion for scalar and
nested non-finite values.