# ARX.SDK on SQL Server: benchmarks

A living summary of the SQL Server benchmarks: how ARX compares with raw Microsoft.Data.SqlClient, Dapper and EF Core.
**Update it whenever a benchmark is rerun or an operation is added.**

| | |
|---|---|
| Last updated | 2026-11-09: first full run of every operation (108 benchmarks), then Delete rerun after the JSON-list change |
| Operations measured | Create, CreateMultiple, Update, UpdateMultiple, Delete, DeleteMultiple, Retrieve, RetrieveMultiple (rows, a page with the total), Execute (a procedure with result rows and OUTPUT values, a scalar function) |
| Machine | Intel Core i7-8750H, Windows 11, .NET 10.0.14, SQL Server 2022 (16.0.4225.2, `mcr.microsoft.com/mssql/server:2022-latest`) in Docker Desktop, Microsoft.Data.SqlClient 7.1.1, BenchmarkDotNet 0.15.8, job Standard (8 iterations, 3 warm-ups) |
| Compared with | Raw SqlClient (hand-written, typed parameters), Dapper 2.1.89, EF Core 10 (`Microsoft.EntityFrameworkCore.SqlServer` 10.0.0), `SqlBulkCopy` |
| Reports | The full run: results/2026-11-09-full-run/. Delete after the change: [results/2026-11-09-delete-after-json-list.md](#report-2026-11-09-delete-after-json-list). The IN-list probe: [results/probes/in-list-vs-openjson.md](#report-probes-in-list-vs-openjson). After READ COMMITTED on writes (E16): [Update](#report-2026-11-09-update-after-read-committed), [Delete](#report-2026-11-09-delete-after-read-committed) (ARX and raw only). |

**In one line:** ARX is level with hand-written SqlClient for single-row work, level with the best hand-written form for
many-row writes (and far ahead of the usual one and of EF Core), level with Dapper and EF Core for reads, and ahead of
everyone for deleting lists of 100 to 1,000 ids. Raw typed readers are faster than ARX's dictionaries for large reads.

Read the ratios, not the absolute times, which depend on the machine. Differences under about 10% on a single run are
inside the noise here. One run of each benchmark so far: treat close calls as level.

## Every operation at a glance (2026-11-09)

Mean times. "Usual" is the hand-written form most code uses; "best" is the fastest hand-written form found.

| Operation | **ARX** | Raw SqlClient | Dapper | EF Core | ARX vs raw |
|---|---|---|---|---|---|
| Create, one row | **2.65 ms** | 2.57 ms | 2.58 ms | 3.83 ms | level (1.03×) |
| CreateMultiple, 100 rows, ids back | **6.28 ms** | usual 24.5 ms, best 8.18 ms | – | 24.0 ms | 3.9× faster than usual, 0.77× best |
| CreateMultiple, 1,000 rows, ids back | **35.5 ms** | usual 437 ms, best 31.8 ms | – | 286 ms | 12× faster than usual, 1.12× best |
| CreateMultiple, 10,000 rows, ids back | **339 ms** | usual 4.43 s, best 345 ms | – | 1.85 s | level with best |
| CreateMultiple, 10,000 rows, no ids needed | **365 ms** (still returns ids) | `SqlBulkCopy` 111 ms | one statement a row: 27.2 s | – | 3.3× `SqlBulkCopy` |
| Update, one row | **2.67–2.86 ms** | 2.47–2.78 ms | 2.76 ms | 3.72 ms | 0.96–1.16× (two runs) |
| UpdateMultiple, 100 rows | **6.23 ms** | 5.73 ms | 273 ms | 25.0 ms | 1.09× |
| UpdateMultiple, 1,000 rows | **27.8 ms** | 23.2 ms | 2.52 s | 262 ms | 1.20× |
| UpdateMultiple, 10,000 rows | **269–279 ms** | 219–261 ms | 25.7 s | 1.96 s | 1.07–1.23× (two runs) |
| Delete, one row | **4.38–4.74 ms** | 4.20–4.66 ms | 4.14 ms | 12.4 ms | 0.96–1.12× (three runs) |
| DeleteMultiple, 100 ids | **4.87 ms** | IN list 5.89 ms | 6.12 ms | 15.1 ms | 0.83× |
| DeleteMultiple, 1,000 ids | **10.5–11.5 ms** | IN list 180 ms | 174 ms | 264 ms | 16× faster |
| DeleteMultiple, 10,000 ids | **52.3 ms** | OPENJSON 48.5 ms | 2.27 s | 61.4 ms | level |
| Retrieve, one row by id | **1.60 ms** | 1.47 ms | 1.44 ms | 2.12 ms | 1.09× |
| RetrieveMultiple, 100 rows | **1.88 ms** | 1.97 ms | 2.76 ms | 2.83 ms | level |
| RetrieveMultiple, 1,000 rows | **5.82 ms** | 4.01 ms | 4.97 ms | 5.15 ms | 1.45× (1.17× Dapper) |
| RetrieveMultiple, 10,000 rows | **62.8 ms** | typed 34.3 ms | 64.4 ms | 61.0 ms | 1.83× typed; level with Dapper and EF Core |
| A page of 50 with the total | **1.76–2.88 ms** | 2.68–2.73 ms | – | – | level |
| Execute: a procedure, 1 row + OUTPUT values | **3.24 ms** | 3.25 ms | 2.96 ms | no way to read OUTPUT values | level |
| Execute: a procedure, 1,000 rows | **4.78 ms** | 4.06 ms | 4.04 ms | – | 1.18× |
| Execute: a scalar function | **1.47–1.81 ms** | 1.43–1.51 ms | – | – | 1.03–1.20× |

"Usual" CreateMultiple is a multi-row `INSERT … OUTPUT` of up to 2,000 parameters a statement (ids unordered); "best"
is a hand-written `MERGE` from one `OPENJSON` parameter returning ids in list order, which is what ARX sends for groups
of 20 rows or more. The Dapper CreateMultiple and UpdateMultiple columns are `ExecuteAsync(sql, list)`, one statement
per row: what Dapper does with a list.

## What the numbers say

- **Single-row calls are level** with raw SqlClient and Dapper (0.96–1.16× across the runs), and 1.3–2.6× faster than EF Core. Getting
  Create there needed the plain `OUTPUT` change: the table variable cost about 2 ms a call.
- **Many-row writes are level with the best hand-written SQL** (0.77–1.20×): the JSON row source sends one parameter for
  any number of rows, so SQL Server compiles one plan and there is no 2,100-parameter limit to split around. Against
  the usual hand-written SQL (multi-row VALUES) ARX is 4–13× faster, and 4–9× faster than EF Core.
- **`SqlBulkCopy` is 3.3× faster for 10,000 rows when ids aren't needed.** ARX always returns ids; a bulk path without
  them would be a new option.
- **Lists of ids:** SQL Server's cost for an IN list climbs faster than the list (a SELECT by 1,000 ids is 28 ms as an
  IN list, 5 ms as one OPENJSON parameter, with a cached plan). ARX sends lists from 50 values as OPENJSON, so it beats
  every IN-list approach from 100 ids up (16× at 1,000).
- **Reads are level with Dapper and EF Core.** A raw typed reader is faster for large results (1.8× at 10,000 rows):
  ARX builds a dictionary per row, as on PostgreSQL, and allocates 2.3× what the raw reader does.
