Conditions

Measured 11 August 2026
Engine WitDatabase, default mode
Compared against SQLite (Microsoft.Data.Sqlite), LiteDB
Machine Ryzen 9 5950X, 32 logical cores
Runtime .NET 10.0.8
Harness BenchmarkDotNet 0.15.8, --job short --inProcess, MemoryDiagnoser on
Source Benchmarks/OutWit.Database.Benchmarks

Before anything was timed, every benchmark body ran once under a verification pass that compares what the three engines return. All compared operations agree.

Two full passes were taken on an otherwise idle machine and averaged. Of 31 paired operations, 25 repeated to within 5%. Rows whose two passes disagreed by more than 10% are excluded and named as such.

These figures replace a set measured on 2 August 2026, and the write path is slower than that earlier baseline by half again. The cause has not been found. The control that makes it a finding rather than a suspicion is that SQLite and LiteDB read what they read in August on the same rows, the same harness and the same machine.

All times are milliseconds. Lower is better.

The shape of the result

One number decides most of it: how many rows cross the boundary in a single call.

SQLite runs in native code, and every call into it pays a fixed cost for the transition. WitDatabase is managed code and pays no such toll, but it handles each row more slowly than C does. So the two engines trade places depending on how the work is divided.

  • One row per call, many calls. WitDatabase is ahead by an order of magnitude.
  • Tens of rows per call. The two are close.
  • Hundreds or thousands of rows per call. SQLite is ahead, and the gap widens with the count.

A service that fetches a record, updates a row, or writes an entry lives on the left of that crossover. A report that scans a table lives on the right.

Against SQLite

Both engines parse the SQL, plan it, enforce the schema and maintain the indexes, so these rows compare the same work.

Many small calls

Operation WitDatabase SQLite
Point query by key, ×100 0.255 4.856
Seek on a UNIQUE index, ×100 (5,000 rows) 0.518 5.090
Sequential reads, ×100 0.353 5.024

One call over many rows

Operation WitDatabase SQLite
SELECT … LIMIT 100 0.121 0.072
Full scan, 1,000 rows 0.798 0.176
Projection, 1,000 rows 0.827 0.146
GROUP BY, 1,000 rows 1.011 0.220
ORDER BY, 1,000 rows 1.659 0.228
Index range scan (BETWEEN), 5,000 rows 0.933 0.204
Seek, non-unique, ×20 (5,000 rows) 9.333 2.569
Composite index, 5,000 rows 5.024 0.923

Writes

Operation WitDatabase SQLite
Mixed transaction (insert, update, select) 4.848 6.957
100 INSERTs in one transaction 5.068 6.822
UPDATE by key, in a transaction 5.161 6.878
INSERT … RETURNING 5.165 7.299
100 INSERTs without a transaction 301.2 925 to 1233

SQLite's autocommit reading moved by a third between passes and is given as a range.

Joins

Operation WitDatabase SQLite
INNER JOIN, 2 tables 0.175 0.074
LEFT JOIN 0.166 0.076
INNER JOIN, 4 tables 0.480 0.091

The gap grows with each table added. An equi-join on INNER or LEFT can be answered by a hash join; everything else is nested loops. See limitations.

Against LiteDB

LiteDB is the only other database written entirely in managed .NET, which makes it the comparison to draw for anyone who needs the same thing WitDatabase provides.

Operation WitDatabase LiteDB
Point query by key, ×100 0.255 1.269
Seek on a UNIQUE index, ×100 0.518 1.949
Sequential reads, ×100 0.353 7.589
Full scan, 1,000 rows 0.798 1.395
Projection, 1,000 rows 0.827 1.406
GROUP BY, 1,000 rows 1.011 1.499
ORDER BY, 1,000 rows 1.659 1.781
Index range scan (BETWEEN), 5,000 rows 0.933 4.582
SELECT … LIMIT 100 0.121 0.147
Mixed transaction 4.848 7.330
Seek, non-unique, ×20 8.125 (LiteDB) 9.333 (WitDatabase)
Composite index, 5,000 rows 5.024 3.662
INNER JOIN, 4 tables 0.480 0.176

Inserts are not on this list

LiteDB inserts a document by appending it. WitDatabase parses the statement, plans it, checks the values against a schema and its constraints, and carries the row through every secondary index under snapshot isolation. On the timing LiteDB is several times faster, and the number describes which engine does less work rather than which one is quicker at the same work.

For a write comparison on equal terms, use the SQLite table above.

What is excluded, and why

COUNT(*). WitDatabase answers it from a per-table counter and reads 0.001 ms against LiteDB's 0.231 and SQLite's 0.057. The other two engines count rows. Publishing that as a 230-fold speed result would compare a lookup against a scan. It is a real property of the engine and it belongs in the documentation, but not in a speed table.

Three write rows whose two passes disagreed by more than 10%: UPDATE by indexed column, bulk UPDATE, and transaction rollback.

Storage engines

Everything above uses the default B+Tree store. The LSM store measures level with it when multi-version concurrency control is switched off and far behind it when MVCC is on, which is the default. See storage engines.

Reproducing this

bash
dotnet run -c Release --project Benchmarks/OutWit.Database.Benchmarks -- verify
dotnet run -c Release --project Benchmarks/OutWit.Database.Benchmarks -- --filter "*"

Run verify first. It executes every benchmark body once and compares what WitDatabase, SQLite and LiteDB return; a timing comparison between engines that do not compute the same answer is not a measurement. Take two passes on an idle machine, and treat a row whose passes disagree by more than 10% as a machine too busy to measure on.