Skip to main content
Performance

Fast because of what it doesn't do

ConjureDB is fast for structural reasons, not tricks: compilation removes runtime parsing and planning; declared indexes turn the lists you'd loop over every frame into O(1) or O(log n) lookups; the query hot path avoids heap allocations; and reactive views apply deltas instead of recomputing. The numbers below follow from that — and we tell you exactly how they were measured.

56 / 56production queries ≥50× vs SQLite
108×PK lookup
661×leaderboard top-N
0queries slower than SQLite
Where the speed comes from

Four structural reasons

Nothing is interpreted

Queries are compiled to C# at build time. There is no parser or planner on the hot path at runtime.

Indexes you declare

A matching index turns a full scan into a direct lookup or a bounded range walk — chosen by the optimizer.

Allocation-lean paths

Struct storage, ref returns and pooled buffers keep query evaluation off the heap; only the result collection is materialized, so per-query GC stays low.

Deltas, not recomputes

Incremental view maintenance updates reactive results by the size of the change, not the size of the table.

The verdict

LINQ vs SQLite vs ConjureDB

The same queries, the same 50,000-row data, three ways to run them: the hand-written LINQ a developer reaches for first, embedded SQLite, and ConjureDB. Here is how they stack up.

Typical query time — geometric mean across 35 real-work cases · bar length ∝ time (lower is better)
Hand-written LINQ
37.3 ms519× slower
SQLite
17.2 ms239× slower
ConjureDB
71.8 µsbaseline
~519×faster than hand-written LINQ (geomean)
~239×faster than SQLite (geomean)
0 BConjureDB allocations on 26 of 57 cases
31/57cases where naive LINQ is slower than SQLite

Fair by construction. Those geomeans deliberately set aside 22 cases that resolve to a sub-microsecond index probe (primary- and foreign-key lookups, top-N off a sorted index) — there ConjureDB does almost no work and the ratio is effectively unbounded, which would flatter the average. Counting all 57 cases it is ~497× vs SQLite and ~3,400× vs LINQ; the numbers above are the conservative slice that still does real scanning, joining and aggregation.

It’s index hits, not magic. The advantage is a declared index turning a full O(n) — or O(n·m) — scan into an O(1)/O(log n) probe. Hand-written LINQ over a plain List<T> has no index, so it walks everything; on correlated queries that is quadratic, which is why naive LINQ lands slower than SQLite in 31 of 57 cases — SQLite at least has a planner and indexes.

Allocations tell the same story. ConjureDB’s best variant allocates 0 B on 26 of 57 cases — it streams from indexes into caller-owned buffers. The naive LINQ churns a geomean of 2.3 MB per query (1007.4 MB across the suite), all of it pressure on the GC — which on a frame budget is its own tax.

And this is the floor, not the ceiling. These are one-shot evaluations — ConjureDB runs the query against its indexes on every call, same as SQLite and the LINQ, and with no incremental view maintenance. For hot, repeatedly-read queries it can go further with IVM: instead of re-evaluating, it applies O(changes) deltas as data is written and reads just return the maintained result. That moves cost to write time — proportional to what changed, not the dataset, and off the synchronous read path — a deliberate read/write trade these benchmarks don’t use.

The coverage

Every query shape, measured

The verdict above isn't one cherry-picked query. It holds across a governed taxonomy of 12 query families57 BenchmarkDotNet cases vs SQLite at 50,000 players, spanning filters, lookups, joins, aggregation, windows, semi/anti/exists, set operations, outer joins, distinct and sort/pagination. All 56 production-tier cases run ≥50×, none slower; the weakest is 54.81×. Every row carries all three baselines side by side — open one for the ConjureDB, LINQ and SQL form and the per-engine timings. Browse by family:

Filter scan and streaming projection

2 cases · 120×–8,807× vs SQLite

Three baselines, same 50,000-row dataset. Select a query for its ConjureDB, LINQ and SQL form.

QueryLINQSQLiteConjureDBvs SQLiteAlloc

Methodology: ConjureDB and SQLite figures are from the project's authoritative all-case BenchmarkDotNet proof (57 cases) vs SQLite, Release, .NET 8, 50,000-row datasets; the best-measured ConjureDB variant is shown per query. The LINQ baseline is measured separately on the identical dataset and query parameters (in-process timing). These are measurements of one configuration — not a guarantee. Your game is different: schema, hardware and generated plan all matter, so benchmark your own workload. Raw benchmark data ships with the source.

A different yardstick

And against a plain Dictionary

SQLite and LINQ are query baselines. For raw collection operations the honest comparison is a Dictionary — and ConjureDB is honest both ways.

vs a plain Dictionary — raw reads

50,000 items, fair fight: the Dictionary gets preset capacity and does none of the work — no transactions, no indexes, no change tracking, no replay-safe commands. ConjureDB still wins primary-key lookups by using a dense-id index and runs level on a contiguous scan.

OperationWhat it doesDictionaryConjureDBResult
SearchLook up 50k items by id (dense-id index = a direct array hit)753 µs56.0 µs13× faster
IterateScan all 50k — a flat contiguous span87.1 µs88.4 µs≈ parity

vs a plain Dictionary — CRUD writes

5,000,000 operations, median shown. The known-size Dictionary column uses preset capacity; the default-capacity column shows the common missed-capacity case for insert-like operations where resize/rehash happens inside the measured work.

OperationWhat it doesDictionary known sizeDictionary defaultConjureDBResult vs known size
Add — batchedInsert 5M with bulk Add(T[]) in one transaction22.5 ms33.5 ms28.5 ms1.3× slower
Add — explicit tx / 10 callsInsert 5M via BeginTransaction + 10 Add calls + Commit22.5 ms33.5 ms122 ms5.4× slower
Add — bulk API / 10 rowsInsert 5M via BeginTransaction + Add(T[10]) + Commit22.5 ms33.5 ms60.3 ms2.7× slower
Add — direct 1 / commitInsert 5M via the direct single-row commit API22.5 ms33.5 ms23.8 ms≈ parity
Add — generated mutationInsert 5M through the generated command/mutation API22.5 ms33.5 ms27.2 ms1.2× slower
Update — direct 1 / commitUpdate 5M existing rows through the direct single-row commit API15.0 ms15.8 ms≈ parity
Update — generated mutationUpdate 5M existing rows through the generated command/mutation API15.0 ms21.8 ms1.5× slower
Upsert — update existingUpsert 5M existing rows through the generated command/mutation API14.9 ms22.8 ms1.5× slower
Upsert — insert missUpsert 5M missing rows through the generated command/mutation API22.3 ms34.0 ms38.2 ms1.7× slower
Remove — batchedDelete 5M with bulk Remove(int[]) in one transaction16.0 ms18.0 ms1.1× slower
Remove — explicit tx / 10 callsDelete 5M via BeginTransaction + 10 Remove calls + Commit16.0 ms97.8 ms6.1× slower
Remove — bulk API / 10 rowsDelete 5M via BeginTransaction + Remove(int[10]) + Commit16.0 ms47.0 ms2.9× slower
Remove — direct 1 / commitDelete 5M via the direct single-row commit API16.0 ms19.8 ms1.2× slower
Remove — generated mutationDelete 5M through the generated command/mutation API16.0 ms19.7 ms1.2× slower
The other lane

And a no-JIT lane for config-delivered queries

Everything above is the compiled lane: queries you know at build time, lowered to C# with nothing to interpret at runtime. But some queries ship later — as content or config — and on iOS and Unity/IL2CPP you can't JIT or generate code at runtime to run them. ConjureDB runs those on a portable interpreter: a pre-verified bytecode executed with no JIT, no codegen and no reflection. It's a deliberate second lane, not a fallback — and it stays fast and allocation-lean.

4.2–50×faster than SQLite across 23 relational shapes
23 / 23relational scenarios faster than SQLite
~18×indexed range top-N vs portable full scan
0 Ballocation on a keyed point lookup

No JIT, no codegen

Config-delivered queries ship as a pre-verified bytecode and run through a register-based interpreter — no runtime IL emission, no reflection — so they work on iOS and Unity/IL2CPP where a JIT cannot.

Allocation-light runtime

Pooled hashtables and arena-backed key/accumulator buffers keep allocation tiny: 0 bytes for a point lookup, and no current 23-scenario vs-SQLite corpus case above 152 bytes per query.

Index-backed access

A primary-key probe is constant-time at about 49 ns in the current vs-SQLite harness, and secondary-index plans use the same ordered accessors as the compiled path. Indexed range top-N is about 18× faster than the portable full-scan variant.

Parity, not approximation

The interpreter produces bit-identical results to the AOT C# lane — same rows, ordering and null semantics — continuously verified by differential and SQL-parity tests.

Every operator, measured against SQLite

The fair fight: SQLite is itself a bytecode interpreter, so this is interpreter vs interpreter. Each of the 23 shapes runs the same query over the same 1000/1000/500-row Players/Orders/Products corpus data, with matching indexes for each scenario and prepared statements — and the no-JIT lane wins all 23, from 4.2–50×, while keeping point lookups at 0 B and the heaviest current 23-scenario corpus case at 152 B.

Query shapeWhat it doesSQLiteConjureDBvs SQLiteAllocated
Point lookupFetch one row by primary key634 ns49 ns13× faster0 B
Indexed range top-NUse score index for range filter + top 107.59 µs433 ns18× faster40 B
Indexed stock top-NUse stock index for range filter + top 102.54 µs525 ns4.8× faster40 B
Indexed score lookupUse score index for equality lookup22.9 µs1.51 µs15× faster40 B
Sort top-NSort by price, take 1017.8 µs2.57 µs6.9× faster152 B
DistinctDistinct values of one column31.0 µs2.98 µs10× faster40 B
Multi-column distinctDistinct over two columns165 µs4.28 µs38× faster96 B
Hash aggregateCount rows grouped by a column110 µs4.28 µs26× faster96 B
Indexed scan · filter · projectUse level index, then project three columns174 µs6.26 µs28× faster40 B
Set unionUnion two result sets (dedup)126 µs6.30 µs20× faster97 B
Set intersectIntersect two result sets (dedup)128 µs6.34 µs20× faster97 B
Score lookupScan/filter rows by score equality37.8 µs6.60 µs5.7× faster40 B
Scan · filter · projectFilter a table, project three columns160 µs7.60 µs21× faster41 B
Range top-NFilter, sort by score, take 1033.4 µs7.81 µs4.3× faster152 B
Multi-key aggregateCount + sum grouped by two columns301 µs9.30 µs32× faster97 B
Aggregate · double keySum grouped by a floating-point column471 µs9.69 µs49× faster42 B
Window · row_numberrow_number() over a sorted partition490 µs11.5 µs43× faster129 B
Product stock top-NFull-scan filter, sort by stock, take 1048.3 µs11.5 µs4.2× faster152 B
Multi-aggregateCount/sum/min/max grouped by a column152 µs12.7 µs12× faster40 B
Window · running sumRunning SUM() over an ordered frame772 µs15.5 µs50× faster99 B
Aggregate · string keyCount grouped by a string column404 µs15.9 µs25× faster42 B
Hash joinInner-join two tables on a key381 µs17.8 µs21× faster3 B
Complex expressionArithmetic projection, then filter on the result242 µs25.6 µs9.4× faster147 B

Some of the win is structural: an in-process engine on your own heap skips the managed↔native marshaling SQLite pays per query — which is exactly why you'd embed one on a client. And the compiled C# lane above is still the throughput ceiling; keep your hottest, build-time-known queries there. The portable lane's job is to run the queries the compiled lane structurally can't, on platforms a JIT can't — and to still beat SQLite doing it. The v1 superinstruction pass is measured separately: fused opcode hit scenarios improve+6.5% geomean, with the rest of the corpus flat.

Methodology: Apple M4 Max · .NET 8 · single thread, BenchmarkDotNet, IterationCount=8 after 4 warmups with BenchmarkDotNet's reported outlier filtering. Both engines run the same query over the same 1000/1000/500-row Players/Orders/Products corpus data with a primary-key index plus matching secondary indexes for index-aware scenarios, prepared statements, and the full result materialized; SQLite is in-memory with fast pragmas (synchronous=OFF, temp_store=MEMORY). “vs SQLite” is SQLite's mean ÷ ConjureDB's mean. One configuration — not a guarantee. Benchmark your own workload.

Speed you can audit

The fastest path is the one with nothing dynamic in it. See how the compiler gets there.