Feature benchmarks
Snapshot: 2026-10-17 · Release: 1.0.9-rc2 (branch 1.0.9-rc3, HEAD 881bf497) · Provider: SQLite
A per-feature comparison that answers "how fast is this query shape, and what does it allocate" instead of "which library wins an end-to-end request". The benchmarks page measures whole scenarios and the prepared/compiled path; this page splits the query surface into tiers and compares every row per query, with Nextorm on its implicit plan cache only.
- No prepared path. Every Nextorm number comes from the implicit plan cache (
B_Nextorm_Cached_*) or a direct terminal (WriteJson/WriteCsv). NoPrepare()/A_*Nextorm arm is used. The prepared path is omitted on purpose: Nextorm leads it unconditionally, winning on every feature (Category A on the benchmarks page), so the only contest left is the cached path this report covers. - One table per feature group. Columns are the libraries; rows are scenarios. There is no Category A/B split inside a table.
- Per query. Each cell is
mean time / allocated. Category B methods execute 10 queries per invocation, so their BenchmarkDotNetMean/Allocatedare divided by 10; Tier-1 methods execute one query per invocation. Tier-5 cells are one query plus serialization of 10,000 rows. - Marks.
**time**is the fastest time in the row;**alloc**is the smallest allocation; a fully bold cell is both. - Noise.
ShortRun+InProcessEmitToolchainis exploratory: treat differences below ~20 % as noise and read the full reports before drawing conclusions.
Method
- Machine: AMD Ryzen 7 5800HS with Radeon Graphics (8 logical / 4 physical cores), .NET 10.0.12,
BenchmarkDotNet 0.15.8 (
ShortRun+InProcessEmitToolchain), SQLite throughMicrosoft.Data.Sqlite; the database lives on tmpfs. - Nextorm: implicit plan cache for the Category B–derived groups, and the cached Tier-1 arms
(
B_Nextorm_Cached_*,B_Nextorm_ToDataReader). NoPrepare()and no compiled-query arm. - Dapper: raw SQL with its cached IL. Dapper has no prepared or compiled form, so its raw-SQL arm is the only Dapper path and is used unchanged.
- linq2db and EF Core: regular, non-compiled APIs (
B_Linq2Db_*,B_EFCore_*). NoCompiledQuery/Compile()arms. - Missing arms: EF Core has no equivalent in this suite for CTE, recursive CTE, window functions or the
synchronous raw reader; those cells are
—. - Raw reader: synchronous, shared non-retaining sink, zero materialization; EF Core is excluded by design because it has no LINQ raw-reader counterpart.
- JSON/CSV: the competitor arms are the closest equivalent — materialize then serialize manually — whereas Nextorm writes directly. These two rows are useful, but they are not a shared-terminal comparison.
Source reports (BenchmarkDotNet output committed under benchmarks/BenchmarkDotNet.Artifacts/results/):
Tier-1 — query-shape scenarios
Projection, SQL-side aggregates, ordered paging, DTO streaming and a zero-materialization raw reader.
Nextorm is fastest on 6 of 8 scenarios and allocates least on 5; Dapper wins Count, and the raw reader is
1.19× behind linq2db (1.17× behind Dapper).
| Scenario | Nextorm | Dapper | linq2db | EF Core |
|---|---|---|---|---|
| Projection → DTO | 4.047 ms / 1.30 MB | 7.286 ms / 1.70 MB | 5.958 ms / 1.48 MB | 9.881 ms / 3.62 MB |
| Count | 18.47 μs / 3.73 KB | 17.07 μs / 944 B | 24.37 μs / 2.80 KB | 47.10 μs / 9.84 KB |
| Sum | 286.23 μs / 4.13 KB | 297.01 μs / 954 B | 305.35 μs / 3.41 KB | 345.24 μs / 10.72 KB |
| GroupBy → Count | 1.276 ms / 10.33 KB | 1.290 ms / 12.04 KB | 1.416 ms / 11.27 KB | 1.397 ms / 41.94 KB |
| Paging — offset 5,000 / limit 100 | 101.31 μs / 22.62 KB | 103.67 μs / 18.15 KB | 140.91 μs / 22.65 KB | 185.84 μs / 56.90 KB |
| Streaming — buffered DTO | 4.044 ms / 1.30 MB | 7.102 ms / 1.70 MB | 6.053 ms / 1.48 MB | 8.838 ms / 3.62 MB |
| Streaming — unbuffered DTO | 4.059 ms / 1.22 MB | 4.886 ms / 1.45 MB | 4.522 ms / 1.23 MB | 5.166 ms / 3.37 MB |
| Raw reader — synchronous | 4.677 ms / 947.98 KB | 3.996 ms / 942.77 KB | 3.942 ms / 943.00 KB | — |
What this says. Nextorm's wins are the wide projection and DTO streaming (0.55–0.57× Dapper), Sum
(0.96×), GroupBy (0.99×) and paging (0.98×). Count is 1.08× behind Dapper. On the raw reader all three
libraries are within 1.19× on time and within 0.6 % on allocation.
Tier-2 — relational shapes
Dapper is fastest on 5 of 6 and allocates least on all 6; Nextorm wins only SELECT DISTINCT
(15.53 vs 18.41 μs, 0.84×). Nextorm trails Dapper by 1.16–1.75× on the rest.
| Scenario | Nextorm | Dapper | linq2db | EF Core |
|---|---|---|---|---|
| CTE | 32.72 μs / 10.89 KB | 19.83 μs / 1.57 KB | 56.89 μs / 9.06 KB | — |
| Recursive CTE | 35.33 μs / 13.31 KB | 24.44 μs / 1.53 KB | 60.41 μs / 8.39 KB | — |
| 4-table join | 32.96 μs / 10.67 KB | 25.03 μs / 2.02 KB | 65.38 μs / 14.10 KB | 114.20 μs / 24.69 KB |
| LEFT JOIN | 23.03 μs / 6.16 KB | 19.83 μs / 2.22 KB | 69.12 μs / 8.60 KB | 94.80 μs / 20.27 KB |
| SELECT DISTINCT | 15.53 μs / 3.80 KB | 18.41 μs / 1.56 KB | 30.44 μs / 3.91 KB | 64.19 μs / 12.20 KB |
| IN-list — inline | 31.65 μs / 7.76 KB | 18.12 μs / 1.27 KB | 69.44 μs / 18.91 KB | 112.63 μs / 22.61 KB |
The captured IN-list variants add 24.2 μs and 12.6 KB per query to the inline arm; that is a Nextorm cache-lookup property, so the table keeps the inline arm for the cross-library row.
Tier-3 — set operations
Dapper wins both by a small time margin and by 4.4–4.9× on allocation.
| Scenario | Nextorm | Dapper | linq2db | EF Core |
|---|---|---|---|---|
| EXCEPT | 17.15 μs / 6.38 KB | 16.96 μs / 1.44 KB | 38.10 μs / 5.05 KB | 72.82 μs / 15.31 KB |
| INTERSECT | 16.79 μs / 6.36 KB | 15.60 μs / 1.29 KB | 36.77 μs / 5.01 KB | 71.94 μs / 14.38 KB |
EXCEPT is 1.01× and INTERSECT is 1.08× behind Dapper; both stay 2.2–2.4× ahead of linq2db on time.
Tier-4 — scalar and expression features
Nextorm wins CASE WHEN (0.93×) and String.Contains (0.82×); Dapper wins ToUpper (1.10×), UDF (1.09×)
and both window scenarios (1.04× and 1.11×), and allocates least on all six. Splitting Contains/ToUpper
and ROW_NUMBER/SUM OVER avoids mixing different operations in one “string” or “window” row.
| Scenario | Nextorm | Dapper | linq2db | EF Core |
|---|---|---|---|---|
| CASE WHEN | 13.04 μs / 3.22 KB | 14.00 μs / 1.34 KB | 25.05 μs / 3.80 KB | 56.78 μs / 11.95 KB |
| String.Contains | 14.97 μs / 4.20 KB | 18.18 μs / 1.72 KB | 30.95 μs / 4.78 KB | 69.55 μs / 14.01 KB |
| ToUpper | 15.29 μs / 4.20 KB | 13.93 μs / 1.28 KB | 47.75 μs / 6.50 KB | 66.26 μs / 13.27 KB |
| UDF | 15.53 μs / 4.29 KB | 14.29 μs / 1.28 KB | 48.46 μs / 6.50 KB | 66.25 μs / 13.27 KB |
| Window — ROW_NUMBER | 31.55 μs / 5.03 KB | 30.46 μs / 1.54 KB | 62.11 μs / 5.99 KB | — |
| Window — SUM OVER | 33.03 μs / 5.46 KB | 29.75 μs / 1.55 KB | 62.28 μs / 5.82 KB | — |
Tier-5 — serialization output
Nextorm allocates least on JSON but is 1.10–1.13× behind Dapper/linq2db on time; CSV is fastest and allocates least. The competitor arms materialize then serialize manually, so these are closest-equivalent rows, not a shared terminal.
| Scenario | Nextorm | Dapper | linq2db | EF Core |
|---|---|---|---|---|
| JSON array — DTO (10,000 rows) | 10.352 ms / 435.64 KB | 9.416 ms / 1.24 MB | 9.150 ms / 963.56 KB | 11.303 ms / 3.08 MB |
| CSV (10,000 rows) | 11.076 ms / 1.70 MB | 13.291 ms / 4.75 MB | 13.385 ms / 4.22 MB | 14.364 ms / 6.36 MB |
The JSON row uses WriteJson_Array_Dto, the same DTO payload as the competitor arms (the narrower
WriteJson_Array_Scalar is faster but writes only the key). A specialized Nextorm string-projection CSV
path is faster still (4.029 ms / 949.67 KB), but it covers a narrower shape and is not the general row
above.
Verdict
Across the 22 like-for-like query rows, Nextorm is fastest on 9, Dapper on 12 and linq2db on 1; allocations
lead on 5, 17 and 0 respectively. Dapper remains the warm-path baseline to beat. Nextorm's wins are
concentrated in projection/streaming/aggregates, SELECT DISTINCT, CASE WHEN and String.Contains; the
relational shapes, set operations and most scalar/expression rows still trail. On the two serialization
rows, CSV is a Nextorm win on both metrics; JSON allocates least but is 1.10–1.13× behind on time. Both
serialization rows compare a direct writer with materialize-then-serialize arms, so read them with that
caveat.
This report intentionally says nothing about the prepared path: Nextorm leads it unconditionally, winning on every feature (Category A on the dated benchmarks page), so there is nothing to compare against competitors — they only get a contest on the cached path. It also does not measure startup or NativeAOT: the zero-config AOT reference and its JIT/AOT numbers are in the same benchmarks page.
Optimization follow-ups
The cases where this snapshot is behind linq2db are tracked in the rc3 milestone: #243 (Count/Sum allocations), #244 (CTE/recursive CTE allocations), #245 (EXCEPT/INTERSECT allocations), #246 (raw reader) and #247 (JSON streaming).
T246 status (2026-10-17, rv2): GAP / incomplete — qualification exhausted, no qualified final result, empty
arm unmeasured. The canonical out-of-process oracle gives empty-arm allocation 7.86–7.94×, not 2.31×;
2.31× is a historical in-process ShortRun figure from a different regime and the two are not comparable (no
conversion factor between jobs). No PASS is claimed and no final ratios are published.
Reproducing
# No prepared arms are used by this report.
for filter in "*SqliteBenchmarkFeaturesFairCached*" "*SqliteBenchmarkProjection*" \
"*SqliteBenchmarkAggregates*" "*SqliteBenchmarkPaging*" "*SqliteBenchmarkStreaming*" \
"*SqliteBenchmarkWriteJson*" "*SqliteBenchmarkCsv*"; do
dotnet run --project benchmarks/nextorm.benchmark -c Release -- --filter "$filter"
done
Each linked report is the BenchmarkDotNet output committed under
benchmarks/BenchmarkDotNet.Artifacts/results/. Set NEXTORM_BENCH_FULL=1 to run the full out-of-process
job instead of ShortRun.