HoloDb evidence

HoloDb benchmarks

On a 10,000,000-invoice ledger joined to 200,000 customers, the dashboard rollups read in microseconds in the embedded engine, and every result is identical across the engines. The queries where HoloDb is slower are listed beside the wins.

  • Engine alone, without the optional plane
  • Losses shown

Headline

queries where HoloDb embedded beats SQLite
15 of 16
wins, ties and losses against DuckDB
13 / 2 / 1
wins, ties and losses against SQL Server
15 / 0 / 1

Where it is slower

Every loss in this record, stated once

How it was measured

Stated once, for every figure on this page
Workload
A billing business's invoice ledger (a normalised invoices fact of 10,000,000 rows joined to a customers dimension of 200,000 rows) with sixteen queries: five rollups, a single-invoice lookup, five single-table scans and five joins to the customer dimension. The plane sections use three constructed workloads: a live dashboard, ingest while querying, and multi-tenant slices and joins.
Engines
SQLite (in memory, the external reference, always listed first); DuckDB (in process, in memory); SQL Server 2022 LocalDB (tables in tempdb on disk); HoloDb embedded (in process); HoloDb server (networked over TLS on the loopback interface); HoloDb engine: the same database without a plane. HoloDb plane: a database with the optional Pleroma plane attached.
Check
Before any query is timed, its full result set is normalised and compared across the engines. Every timed result is identical, including on the queries where HoloDb is slower. After each plane workload the plane is compared with a rebuild from the columns and found equal.
Statistic
Medians of repeated calls through each engine's typed client API, execute plus consume every row. A cell whose own two timing loops disagree by more than 25 percent is marked noisy, and no ratio is drawn from it. A difference under 1.15x counts as a tie.
Machine
One 16-core laptop CPU (Intel Core i9-11950H). The billing comparison ran with no other benchmark or heavy application running. The plane showcase ran with ordinary desktop applications and the SQL Server service open.
Memory and logging
Every HoloDb figure is an in-memory database with no write-ahead log; SQLite and DuckDB ran in memory and SQL Server used tempdb on disk. The single-row write tables therefore compare the engine alone against in-memory SQLite and do not measure durable commits: a database opened with a write-ahead log flushes each committed statement to disk before returning, which adds one flush per commit, set by the disk and not measured here. Bulk-load rates compare unlike modes: typed arrays into memory, an appender, inserts in one transaction, and a bulk copy into tempdb.
Ties
Fresh process means the first statements of a newly started process, which is what an application feels after it starts. The cost of just-in-time compilation is included.

Billing ledger against SQLite, DuckDB and SQL Server

The headline results below are the engine's own: the optional Pleroma plane was not attached in these comparisons. The ledger has 200,000 customers, about 50 invoices each, over 24 months, four regions and five statuses, with money held as integer cents.

Bulk load, rows per second: DuckDB 1,159,279; HoloDb server 1,051,473; SQLite 726,050; SQL Server 693,231; HoloDb embedded 679,271.

In process, in memory: HoloDb embedded against DuckDB and SQLite (median milliseconds), drawn on a logarithmic scale. HoloDb embedded against DuckDB. invoices by status 0.02 ms revenue by region 0.02 ms total invoiced 0.00 ms revenue by month 0.04 ms invoice count 0.00 ms look up one invoice 0.02 ms revenue by segment (JOIN) 14.71 ms top named customers (JOIN) 132.35 ms revenue by segment and status (JOIN) 36.17 ms overdue outstanding 6.12 ms revenue by region and status 35.58 ms top customers by revenue 89.87 ms largest invoices 2.42 ms big-ticket invoices (over $5k) 2.49 ms enterprise revenue (JOIN) 8.97 ms revenue by signup cohort (JOIN) 14.92 mslogarithmic scale, shorter is faster

DuckDBHoloDb embeddedwhere the other engine is ahead

In process, in memory: HoloDb embedded against DuckDB and SQLite (median milliseconds)
QuerySQLiteDuckDBHoloDb embeddedResult
invoices by status3,719.6641.630.02HoloDb, more than 1,600x
revenue by region3,492.7034.330.02HoloDb, more than 1,300x
total invoiced414.542.560.00HoloDb, more than 510x
revenue by month3,584.015.480.04HoloDb, more than 120x
invoice count13.900.680.00HoloDb, more than 130x
look up one invoice0.010.600.02SQLite ahead, 0.01 ms
revenue by segment (JOIN)6,527.9749.0114.71HoloDb 3.3x
top named customers (JOIN)8,944.31291.33132.35HoloDb 2.2x
revenue by segment and status (JOIN)10,685.4177.5436.17HoloDb 2.1x
overdue outstanding440.8011.936.12HoloDb 1.9x
revenue by region and status7,495.5258.0935.58HoloDb 1.6x
top customers by revenue4,346.30129.6189.87HoloDb 1.4x
largest invoices573.943.282.42HoloDb 1.4x
big-ticket invoices (over $5k)382.922.572.49tie (1.03x)
enterprise revenue (JOIN)3,047.438.688.97tie (DuckDB 1.03x)
revenue by signup cohort (JOIN)6,469.1611.9614.92DuckDB 1.25x
Drawn from the table, which holds every value.
Networked: HoloDb server against SQL Server and SQLite (median milliseconds), drawn on a logarithmic scale. HoloDb server against SQL Server. revenue by region 0.19 ms invoices by status 0.20 ms revenue by month 0.35 ms total invoiced 0.25 ms invoice count 0.17 ms largest invoices 2.74 ms revenue by signup cohort (JOIN) 13.88 ms enterprise revenue (JOIN) 8.55 ms revenue by segment (JOIN) 14.65 ms revenue by segment and status (JOIN) 34.35 ms big-ticket invoices (over $5k) 3.06 ms overdue outstanding 5.97 ms revenue by region and status 34.46 ms top customers by revenue 87.17 ms top named customers (JOIN) 127.51 ms look up one invoice 0.18 mslogarithmic scale, shorter is faster

SQL ServerHoloDb serverwhere the other engine is ahead

Networked: HoloDb server against SQL Server and SQLite (median milliseconds)
QuerySQLiteSQL ServerHoloDb serverResult
revenue by region3,492.702,088.590.19HoloDb, about 11,000x
invoices by status3,719.662,178.580.20HoloDb, about 10,900x
revenue by month3,584.011,485.330.35HoloDb, about 4,200x
total invoiced414.54943.630.25HoloDb, about 3,800x
invoice count13.90470.330.17HoloDb, about 2,800x
largest invoices573.941,472.472.74HoloDb, about 540x
revenue by signup cohort (JOIN)6,469.162,562.8413.88HoloDb, about 185x
enterprise revenue (JOIN)3,047.431,550.708.55HoloDb, about 180x
revenue by segment (JOIN)6,527.972,626.8414.65HoloDb, about 180x
revenue by segment and status (JOIN)10,685.415,089.7034.35HoloDb, about 150x
big-ticket invoices (over $5k)382.92397.233.06HoloDb, about 130x
overdue outstanding440.80588.615.97HoloDb, about 100x
revenue by region and status7,495.522,792.4434.46HoloDb, about 81x
top customers by revenue4,346.302,426.1087.17HoloDb, about 28x
top named customers (JOIN)8,944.312,616.03127.51HoloDb, about 21x
look up one invoice0.010.070.18SQL Server, 2.6x
Drawn from the table, which holds every value.

HoloDb embedded is faster than SQLite on 15 of the 16 queries, from 48x (top customers by revenue) to more than 2,700x on the rollups. Against DuckDB, counting a difference under 1.15x as a tie, it is faster on 13 queries, tied on 2 and slower on 1. The rollups and the lookup read a maintained total or the primary-key index, so they take microseconds and do not grow with the ledger. The benchmark prints two decimals, so those cells are shown at that resolution and the ratios are lower bounds.

HoloDb server is faster than SQL Server on 15 of the 16 queries, including all five joins by 21x to 185x. Both are networked services paying a client and server round trip. The server figures are medians of five calls, so they include the first-call cost of the network path: a longer loop through the same client measured the rollups and the lookup at a steady 0.06 to 0.10 ms, and a point query pays a fixed 0.07 ms of client, TLS, socket and result encoding that the same query does not pay embedded (1 to 3 microseconds).

Total invoiced across 10,000,000 invoices (SELECT SUM(amount_cents)): SQLite 414.54 ms, DuckDB 2.56 ms, SQL Server 943.63 ms, HoloDb under 0.01 ms, because a maintained total is read instead of 10 million rows.

The optional plane on the billing ledger

The plane is opt-in per table and was not on in the comparison above. This is a separate run of the same workload with a plane attached to the invoices table: dimensions region (4 values), status (5) and month (24), a 10-bit joint, and the measure amount_cents. Building it for 10,000,000 rows took 741 ms (749 ms on the server's database) and its cells take 34,528 bytes. All 16 results were again identical across the engines.

Plane off and plane on, median milliseconds
QuerySQLiteDuckDBHoloDb, plane offHoloDb, plane onPlane vs off
overdue outstanding429.3011.435.930.01about 600x faster
revenue by region and status7,381.3456.6632.300.18about 180x faster

Through the server the same two queries take 6.30 ms and 34.58 ms with the plane off and 0.11 ms and 0.45 ms with it on (SQL Server: 611.59 ms and 2,898.13 ms).

What the plane serves: filters and multi-column GROUP BY over its dimension columns. What it declines, and why: the big-ticket count (the filter is on a measure, not a dimension), top customers (200,000 distinct customer ids, far over the 1,024-value dimension cap), the five joins (200,000 join keys against a limit of 4,095) and the lookup and largest-invoices queries (no aggregate). The five rollups are left to the engine on purpose, because it reads a maintained total there in microseconds and the plane is slower. Per database, 12 calls were served by the plane and 42 declined.

The cells the plane does not touch run the identical engine path in both columns and differed by 1 to 13 percent typically and by up to 33 percent (big-ticket: 3.01 ms off, 2.26 ms on), so differences of that size are noise.

Plane on constructed workloads: dashboards, ingest and tenants

These workloads were built to suit the plane and are measured separately from the engine-only comparisons above. What they share: the filter combinations cannot be pre-indexed because nobody knows in advance which dimensions a person will cut by, the answers must be exact, and the table keeps changing while it is queried.

A live dashboard: 10,000,000 events and 32 ad-hoc slices over six small-domain dimensions (a 14-bit joint) and the measures. Building the plane took 1,759 ms and its cells take 920,248 bytes. The slices are scalar filters (W), group-bys with and without a filter (G), HAVING (H), COUNT DISTINCT and MIN and MAX with a filter (D), queries outside the plane (X) and whole-table queries the engine already answers from a maintained total.

Plane against the engine and against the best other engine, by class of slice
Class (queries)Plane vs engine, medianPlane vs best of SQLite, DuckDB, SQL Server, median
W scalar slices (9)121.40x191.5x
G group by (7)20.90x27.4x
H having (2)24.57x42.2x
D distinct, min/max with a filter (3)18.42x36.0x
X outside the plane (7)0.97x (tie)1.5x
First call of each query, median milliseconds
QueryDuckDBSQL ServerHoloDb engineHoloDb plane
WHERE region = 'north' AND device = 'mobile', COUNT and SUM25.185827.21818.5260.153
GROUP BY region, device, COUNT and SUM56.7123,012.04743.2222.068
GROUP BY region, channel HAVING COUNT(*) > ...67.1303,047.37443.8152.110
COUNT(DISTINCT channel), COUNT(DISTINCT device) with a WHERE37.5062,336.28050.1890.803
(outside the plane: filter on user_id, a column that is not a dimension)12.139486.46611.30011.484

The best other engine on the W, G, H and D slices is DuckDB in every row shown. Three of the nine W cells are noisy, because the engine's own two timing loops disagreed by more than 25 percent; the class median is as printed.

The first call of each query, which pays any one-time work, is also faster with the plane: the median first-call speed-up over the engine is 16.99x for W, 10.90x for G, 13.97x for H and 10.17x for D.

The queries the plane declines or leaves to the engine (the X class: a filter on a non-dimension column, COUNT(DISTINCT user_id), a sum of a non-measure column, a top-10 by user, a filter on a measure, a row list and a point lookup by event id) run the engine's own path and tie with it: the seven X queries read 0.95x to 1.04x warm. On the first call five of the seven were slower with the plane attached, at 0.42x to 0.84x of the engine's first call; these are single samples.

Ingest while querying

A 2,000,000-row base table, then 20 rounds of a 10,000-row bulk load, 200 single-row INSERTs and an eight-query dashboard panel, then 300 trickle pairs (one INSERT followed by one group-by query). The panel's results matched in every round.

Per round, median milliseconds
Per round (ms)SQLiteDuckDBSQL ServerHoloDb engineHoloDb plane
panel (8 queries)3,794.93112.303,057.90137.9011.53
round total (bulk load, INSERTs, panel)3,816.72239.883,109.90149.4725.84
trickle pair (INSERT plus query)760.78416.088419.6107.6341.090

The plane makes the panel 11.95x faster than the engine, the round total 5.78x faster and the trickle pair 7.00x faster. Through the server the round total is 179.34 ms for the engine and 45.65 ms with the plane.

The first panel query after each ingest gains only 1.66x (2.304 ms against 3.835 ms), while the other seven gain 2.87x to 319.60x. The likely cause is the flush of the plane's pending changes that the first query after an ingest triggers; the flush was not timed separately. DuckDB takes 6.288 ms on that query, so the plane is still 2.7x ahead of it.

Multi-tenant slices and joins

Five million events spread over six tenant tables (2,000,000 down to 150,000 rows) and a 2,000-product dimension table. A filter with a COUNT and SUM on one tenant is a median 11.70x faster than the engine and 25.0x faster than the best other engine. The largest tenant reads 0.171 ms with the plane, 4.263 ms for the engine, 6.312 ms for DuckDB and 195.195 ms for SQL Server.

The same join repeated, median milliseconds
QueryDuckDBSQL ServerHoloDb engineHoloDb plane, embeddedHoloDb plane, server
join COUNT, largest tenant (2,000,000 rows)1.749307.0390.3800.0020.078
join COUNT and two SUMs, largest tenant2.906477.0432.7160.0030.079
join COUNT, smallest tenant (150,000 rows)0.94723.6780.1100.0020.071
join COUNT and two SUMs, smallest tenant1.39237.1580.9990.0030.073

The join in these tests is one statement repeated: tenant events joined to the products table on the product key (about 2,000 keys), with COUNT(*) alone or with two SUMs. The plane serves a repeat from a memo of the exact answer, valid until either of the two tables is written, so the very low figures are for a repeated identical statement with no write in between. A memo hit takes 0.001 to 0.003 ms embedded and 0.071 to 0.084 ms through the server.

Joins with a WHERE or a GROUP BY on the dimension table are not served by the plane at all; they run the engine's path and tie with it (0.93x to 1.02x).

The plane against the engine at 1,000,000 rows

Seventeen query classes on a 1,000,000-row table, with the engine alone, the engine with the plane, and SQLite. The control query (one the plane declines, so both sides run the same engine path) read 0.81, inside the band of 0.8 to 1.25. The join rows use a very small dimension table: 40 distinct join keys for the two INT joins and 4 for the TEXT join, far below the 2,000 product keys of the tenant workload and the 200,000 join keys of the billing ledger, so they say little about joins over many keys.

Seventeen query classes at 1,000,000 rows
Query classSQLiteHoloDb engineHoloDb planePlane vs engine
COUNT(*) whole table417.4 us1.6 us3.3 usslower (0.48x)
SUM(qty) whole table40.25 ms1.8 us3.8 usslower (0.47x)
GROUP BY 1 column288.46 ms4.4 us10.2 usslower (0.43x)
GROUP BY 2 columns484.34 ms2.88 ms2.40 mstie (1.20x)
WHERE = on 1 column48.11 ms1.28 ms3.8 us336x faster
WHERE BETWEEN52.23 ms620.0 us9.2 usnoisy, no ratio
WHERE on 3 columns (AND/IN)65.19 ms1.63 ms144.2 us11x faster
WHERE LIKE ... OR61.37 ms1.24 ms37.2 us33x faster
GROUP BY with WHERE126.79 ms3.10 ms2.32 ms1.34x faster
COUNT DISTINCT whole table81.41 ms2.1 us1.7 ustie (1.24x)
COUNT DISTINCT with WHERE71.71 ms1.91 ms630.7 us3.0x faster
MIN/MAX of an INT column51.34 ms1.8 us3.4 usslower (0.53x)
Range count44.39 ms343.4 us2.7 us127x faster
Join COUNT69.68 ms3.75 ms15.9 usnoisy, no ratio
Join SUM (both sides)108.23 ms4.92 ms85.5 us58x faster
Join on a TEXT key327.89 ms37.36 ms8.0 us4,670x faster
Query outside the plane (control)48.55 ms938.1 us1.16 ms (declined, engine path)tie (0.81x)

The engine alone is faster than SQLite on every class, from 9x (the TEXT join) to more than 250x on COUNT(*) and more than 20,000x on SUM, single-column GROUP BY, MIN and MAX and COUNT DISTINCT over the whole table. The plane is faster than SQLite on every class it serves. Against the engine it wins where there is a real scan or join to avoid. Outcome counts for the plane against the engine: 7 classes at least 2x faster, 3 ties, 1 between 1.25x and 2x, 4 slower, 2 noisy.

At 100,000 rows the engine is faster than the plane on seven of the 17 classes: the whole-table classes, the single-column GROUP BY and MIN and MAX (0.33x to 0.51x), the two-column GROUP BY (0.26x), GROUP BY with a WHERE (0.41x) and COUNT DISTINCT with a WHERE (0.58x). At 300,000 rows the plane is faster on every filter and join (24x to 1,887x) and on the two-column GROUP BY (1.77x). Default routing sends GROUP BY with a WHERE to the engine below 140,000 rows.

The plane's cost when attached, against the engine alone: single-row INSERT 1.4x to 1.9x, UPDATE 1.6x to 1.8x (these cells are microseconds and move with the order the two arms run in); batch load of the whole table 1.93 s with the plane against 1.80 s without at 1,000,000 rows; rebuilding the plane from the columns 131 ms; a heap of 139.7 bytes per row with the plane against 135.2 without; the plane's cells are a flat 0.92 MB joint slice whatever the row count. One INSERT followed by a two-column GROUP BY costs the plane a floor of roughly 3 ms, which the engine beats on small tables and loses to on large ones.

The TEXT-key join is faster than SQLite in the engine alone (39.98 ms against 322.17 ms at 1,000,000 rows, rows identical). It requires both join columns to be TEXT; a join between an INT column and a TEXT column takes the general path and is much slower.

Single-row writes and transactions, engine alone against SQLite

Autocommit statements on a table of the benchmark shape (9 columns), 205 statements per cell with the first 5 dropped, median, each size in its own newly started process.

Single-row statements, fresh process
StatementRowsSQLiteHoloDb engineEngine vs SQLite
single-row INSERT100,0005.2 us7.7 us1.5x
single-row INSERT1,000,0004.3 us7.0 us1.6x
single-row UPDATE100,0005.5 us11.6 us2.1x
single-row UPDATE1,000,0005.3 us10.5 us2.0x
single-row DELETE100,0004.8 us8.3 us1.7x
single-row DELETE1,000,0004.3 us7.9 us1.8x
point SELECT by key100,0004.6 us3.8 ustie (0.8x, within noise)
point SELECT by key1,000,0004.0 us4.0 ustie (1.0x)
Transactions on a 1,000,000-row table
OperationSQLiteHoloDb engine
BEGIN3.2 us0.8 us
1,000 single-row UPDATEs inside BEGIN, then COMMIT6.44 ms28.18 ms
the same, then ROLLBACK6.54 ms33.35 ms

BEGIN costs the same at any table size because rows are journaled as they change instead of the table being copied, and ROLLBACK replays the journal. The transaction figures are an upper bound for the engine, measured before a later compilation setting was added, and were not re-measured; the 100,000-row figures follow the same pattern.

Dashboard reads during live ingest, and opening a database

Whole-table counts and sums, and GROUP BY on a column with up to 64 distinct values, are served from a snapshot republished after every write, without taking the engine's write lock. In one run, 2,000,000 events were ingested in 1,000-row batches at 1.16 million events a second while 4 concurrent readers queried: median read 0.009 to 0.09 ms, p99 0.8 ms (group by region), 1.1 ms (sum) and 6.6 ms (count), worst single read 6.7 ms.

Opening a database of 1,000,000 rows (a 43 MB log) takes about 2.0 s by replaying its write-ahead log (1.95 to 2.05 s across two runs) and about 1.1 s from a checkpoint (1.09 to 1.11 s), with the loaded state checked correct and a heap of about 110 bytes per row, in a newly started process. This is the engine's own recovery and checkpoint; it does not use the plane or the page store.

Checks behind the figures

Every release is measured against the previous one before it ships: each benchmark cell is run twice on each build, and a cell counts as a regression only if it is worse than 25 percent (or the baseline's own spread) in both pairs. The last such check covered the 10-million-invoice comparison, the plane benchmark at 100,000, 300,000 and 1,000,000 rows, single-row writes and transactions, and opening a 1,000,000-row database from its log and from a checkpoint, and found no cell slower.

Queries the plane declines or leaves to the engine stay within noise: of the 16 queries that run the same path in both arms, none fell outside a band of 0.8x to 1.25x, with class medians of 0.97 and 0.99. A deliberately wrong plane (the measure column shuffled at 1,000,000 rows) was caught on every query it changes and matched on every query it does not: 0 failures of 32.

The aggregate plane returns row-for-row the same answers as the engine and SQLite across the parity suite (2,853 checks at 2,000 rows, and the suite also passes at 5,000 and 100,000 rows), including a random insert, update, delete, rollback and reopen log, and sums past 64 bits. Nine deliberate mutations (a dropped carry, a missed delete, a stale join limb and others) each turned the suite red. Sums past 64 bits are never wrapped: the plane raises an error and PleromaSumExact returns the exact 128-bit total.

The page store passes 72 of 72 checks: exactly-once replay after a crash, a torn journal tail discarded, restore points that rewind without recounting, and repair of a lost page from its parity group. Eight deliberate mutations and a tiny-cache case each turned the suite red.

Atomicity and crash recovery are covered by a 48-check suite (a failing log append is undone in full, a reopen from the log reproduces the committed state) and a 58-check transaction suite plus about 600 randomised comparisons against SQLite; three deliberate mutations each turned it red.

Limits

  • One run per size on one machine, and constructed workloads for the plane sections: one synthetic table (9 columns, four small-domain dimensions, a 14-bit joint). The plane serves only the columns you attach, lives in RAM and is not persisted.
  • In the engine alone, GROUP BY with a WHERE is about 3 ms at 1,000,000 rows because the filter and the grouping are separate passes. The dense-code GROUP BY covers INT and TEXT columns whose combined distinct count is at most 65,536, and anything else uses a slower string-key path. REAL aggregates over a filter use the older scan.
  • The first DDL statement inside a transaction takes a snapshot of every in-memory table, so it costs time proportional to the data. After a ROLLBACK of deleted rows the table holds the same rows but not necessarily in the same internal order.
  • The plane does not help the billing joins (200,000 join keys against the limit of 4,095) or the top-customers query. A plane join over fewer keys is served, as in the tenant workload.
  • Page-store throughput is not reported here.

Related