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
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.