<p><em>Or: how we stopped losing to databases older than most of our engineers, and learned to benchmark honestly along the way.</em></p><p>There's a quiet assumption baked into a lot of database folklore: row stores are for transactions, and if you want analytics — big <code>GROUP BY</code>s, <code>SUM</code>s over millions of rows — you reach for a columnar engine. MySQL and PostgreSQL will do it, sure, but slowly, and everyone knows it.</p><p>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.</p><p>This release is the story of getting there — and it has a twist about honesty in the middle that we think is worth telling.</p><h2>The result first</h2><p>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):</p><pre><code class="language-text">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&gt;500)  53.5 ms    55.5            229.8
</code></pre><p>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.</p><p>But getting an honest version of that table taught us something.</p><h2>The honesty detour</h2><p>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×.</p><p>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.</p><p>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 — <code>gh workflow run benchmark.yml</code> — and anyone can reproduce the numbers above. No laptop, no nested VM, no hand-waving.</p><p>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.</p><h2>How we did it</h2><p>Three ideas did most of the work.</p><h3>1. Vectorized (columnar) aggregation inside a row store</h3><p>The slow part of <code>SELECT SUM(amount), AVG(amount), MIN(amount), MAX(amount) FROM events</code> isn't the math — it's boxing every value into an enum and dispatching each aggregate per row, a million times over.</p><p>So for aggregate-only queries over numeric columns, we now decode a batch of rows into a plain, contiguous <code>f64</code> array, and aggregate with tight loops the compiler happily auto-vectorizes:</p><pre><code class="language-sql">-- one scan, columns pulled into f64 arrays, SIMD-friendly reduction
SELECT SUM(amount), AVG(amount), MIN(amount), MAX(amount) FROM events;
</code></pre><p>That <code>SUM</code>/<code>AVG</code>/<code>MIN</code>/<code>MAX</code> query went from ~17 ms to ~12 ms locally, and lands at 36.6 ms on 1M rows — beating PostgreSQL's 55.5.</p><h3>2. A compiled filter predicate</h3><p>Here's a subtle one. When you write:</p><pre><code class="language-sql">SELECT category, SUM(amount) FROM events WHERE amount &gt; 500 GROUP BY category;
</code></pre><p>...the naïve way to evaluate <code>amount &gt; 500</code> is to walk the expression tree for every row and — worse — look up the column <code>amount</code> by name each time (a case-insensitive scan of the schema). A million rows, a million redundant name lookups.</p><p>Now we compile simple filters once: resolve <code>amount</code> to a column index, recognize the comparison, and evaluate it with a native <code>f64</code> compare per row. Anything we don't recognize (an <code>OR</code>, a <code>LIKE</code>, 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.</p><h3>3. COUNT(*) without reading the rows</h3><p><code>SELECT COUNT(*) FROM events</code> 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:</p><pre><code class="language-sql">SELECT COUNT(*) FROM events;   -- ~8 ms on 1M rows locally (was ~24 ms)
</code></pre><p>There's also a new knob, <code>ELYRASQL_AGG_WORKERS</code>, 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 <code>min(cores, 4)</code>.</p><h2>It's still a database you'd actually use</h2><p>None of this is a special "analytics mode." It's the same MySQL-compatible server, the same single <code>*.edb</code> file, the same wire protocol your existing tools and drivers already speak. You point <code>mysql</code>, DBeaver, <code>sqlx</code>, or your ORM at it and run normal SQL:</p><pre><code class="language-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;
</code></pre><p>On the core transactional workloads it's competitive too: full-scan <code>COUNT</code> and <code>GROUP BY</code> 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.</p><h2>Being honest about the rest</h2><p>Two things we're not going to oversell:</p><ul><li><p><strong>Point queries.</strong> 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.</p></li><li><p><strong>Tiny-batch inserts.</strong> 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.</p></li></ul><p>We keep <code>docs/limitations.md</code> current for exactly this reason. A benchmark you can trust is one that tells you where it loses, too.</p><h2>Try it</h2><pre><code class="language-bash">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
</code></pre><p>Or grab a static binary (x86_64 / aarch64) from the 0.9.6 release. Then run the benchmark yourself — on your own hardware, honestly.</p><p>ElyraSQL 0.9.6 is out now. One file. MySQL on the wire. And, it turns out, faster than PostgreSQL at analytics. 🦀</p>