ElyraSQL 0.9.6: teaching a single-file database to win at analytics
ElyraSQL is a single-file, MySQL-compatible database in Rust — a row store. In 0.9.6 it beats PostgreSQL 17 and MySQL 8.4 on every OLAP query we threw at it, thanks to vectorized aggregation, a compiled filter predicate, and a COUNT(*) that never reads a row. Plus a hard-won lesson about benchmarking honestly.
Or: how we stopped losing to databases older than most of our engineers, and learned to benchmark honestly along the way.
There's a quiet assumption baked into a lot of database folklore: row stores are for transactions, and if you want analytics — big GROUP BYs, SUMs over millions of rows — you reach for a columnar engine. MySQL and PostgreSQL will do it, sure, but slowly, and everyone knows it.
We didn't love that assumption. ElyraSQL is a single-file, MySQL-compatible database written in Rust. It's a row store. And it's built in 2026 — so when we sat down to profile it against MySQL 8.4 and PostgreSQL 17 on analytical queries, "respectable for a young project" wasn't the bar. The bar was: win.
This release is the story of getting there — and it has a twist about honesty in the middle that we think is worth telling.
The result first
Here's ElyraSQL 0.9.6 against MySQL 8.4 and PostgreSQL 17, 1,000,000 rows, all three engines on the same native Linux host (lower is better):
Query ElyraSQL PostgreSQL 17 MySQL 8.4
--------------------------------------- --------- -------------- ---------
COUNT(*) 27.6 ms 29.0 24.0
Global aggregation (SUM/AVG/MIN/MAX) 36.6 ms 55.5 162.6
GROUP BY (100 groups) 63.6 ms 92.1 314.1
GROUP BY + top-10 (10k groups) 93.4 ms 113.9 343.0
Filtered aggregation (WHERE amount>500) 53.5 ms 55.5 229.8
ElyraSQL is the fastest of the three on every OLAP query, and 2–5× ahead of MySQL. A single-file row store, beating PostgreSQL at aggregation. That's the headline.
But getting an honest version of that table taught us something.
The honesty detour
Our first benchmarks ran on a laptop, inside a container VM (OrbStack on an Apple Silicon Mac). And there, ElyraSQL lost to PostgreSQL on the same queries — by 1.5–2×.
We could have shrugged and said "it's a young project." Instead we dug in, and found the culprit: the laptop hypervisor was systematically penalizing ElyraSQL's parallel, memory-mapped scans. Four database engines fighting over a virtualized 16 cores, with nested-VM overhead landing hardest on the engine that leans on parallelism. It wasn't measuring ElyraSQL — it was measuring the VM.
So we did the boring, correct thing: we wrote a CI benchmark that runs on a real, native Linux host (a GitHub Actions runner), with MySQL and PostgreSQL as service containers on the same machine. One command — gh workflow run benchmark.yml — and anyone can reproduce the numbers above. No laptop, no nested VM, no hand-waving.
The lesson we keep relearning: the benchmark environment is part of the benchmark. If your numbers come from a shared laptop VM, say so. We now publish the native ones, and they're the fair ones.
How we did it
Three ideas did most of the work.
1. Vectorized (columnar) aggregation inside a row store
The slow part of SELECT SUM(amount), AVG(amount), MIN(amount), MAX(amount) FROM events isn't the math — it's boxing every value into an enum and dispatching each aggregate per row, a million times over.
So for aggregate-only queries over numeric columns, we now decode a batch of rows into a plain, contiguous f64 array, and aggregate with tight loops the compiler happily auto-vectorizes:
-- one scan, columns pulled into f64 arrays, SIMD-friendly reduction
SELECT SUM(amount), AVG(amount), MIN(amount), MAX(amount) FROM events;
That SUM/AVG/MIN/MAX query went from ~17 ms to ~12 ms locally, and lands at 36.6 ms on 1M rows — beating PostgreSQL's 55.5.
2. A compiled filter predicate
Here's a subtle one. When you write:
SELECT category, SUM(amount) FROM events WHERE amount > 500 GROUP BY category;
...the naïve way to evaluate amount > 500 is to walk the expression tree for every row and — worse — look up the column amount by name each time (a case-insensitive scan of the schema). A million rows, a million redundant name lookups.
Now we compile simple filters once: resolve amount to a column index, recognize the comparison, and evaluate it with a native f64 compare per row. Anything we don't recognize (an OR, a LIKE, a function) falls back to the full interpreter, so nothing changes semantically. Filtered aggregation dropped from ~87 ms to 53.5 ms — from behind PostgreSQL to ahead of it. And because the filter also prunes rows, the filtered query is now faster than the unfiltered one, exactly as it should be.
3. COUNT(*) without reading the rows
SELECT COUNT(*) FROM events doesn't need any column data — just the number of rows. We now count keys across parallel clustered ranges without decoding a single value, and seed the result directly:
SELECT COUNT(*) FROM events; -- ~8 ms on 1M rows locally (was ~24 ms)
There's also a new knob, ELYRASQL_AGG_WORKERS, because we learned the hard way that aggregation is memory-bandwidth bound: past about four parallel readers, you're just adding coordination overhead. The default caps at min(cores, 4).
It's still a database you'd actually use
None of this is a special "analytics mode." It's the same MySQL-compatible server, the same single *.edb file, the same wire protocol your existing tools and drivers already speak. You point mysql, DBeaver, sqlx, or your ORM at it and run normal SQL:
CREATE TABLE events (id BIGINT PRIMARY KEY, user_id BIGINT, category BIGINT, amount BIGINT);
-- ... load a million rows ...
SELECT user_id, SUM(amount) AS spend
FROM events
GROUP BY user_id
ORDER BY spend DESC
LIMIT 10;
On the core transactional workloads it's competitive too: full-scan COUNT and GROUP BY lead the field (10.4 ms and 12.9 ms on 200k rows), point lookups and joins are sub-millisecond, and bulk ingest hits ~351k rows/s at realistic batch sizes.
Being honest about the rest
Two things we're not going to oversell:
Point queries. PostgreSQL keeps a small edge on sub-millisecond PK lookups and range scans — decades of tuple-format and planner refinement. We're within a hair (0.28 ms vs 0.20 ms), and it's already well under a millisecond, but it's their nose in front.
Tiny-batch inserts. ElyraSQL's storage is a crash-safe copy-on-write B-tree with no separate write-ahead log, so a 2,000-row autocommit batch flushes more than a WAL append would. At real bulk-load sizes we pull ahead; at tiny batches we trail. A WAL is a possible future project.
We keep docs/limitations.md current for exactly this reason. A benchmark you can trust is one that tells you where it loses, too.
Try it
docker run -d --name elyrasql -p 3307:3307
-e ELYRASQL_USER=root -e ELYRASQL_PASSWORD=secret
ghcr.io/kwhorne/elyrasql:0.9.6
mysql -h 127.0.0.1 -P 3307 -u root -psecret
Or grab a static binary (x86_64 / aarch64) from the 0.9.6 release. Then run the benchmark yourself — on your own hardware, honestly.
ElyraSQL 0.9.6 is out now. One file. MySQL on the wire. And, it turns out, faster than PostgreSQL at analytics. 🦀