Evidence sqrail docs

Markdown

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.