InsertAll, UpdateAll, UpsertAll and SaveAll send their rows to the database together, instead of making a
round trip for each row. There's nothing to change in your code:
db.InsertAll(orders);
db.UpdateAll(orders);
db.UpsertAll(orders);
db.SaveAll(orders);
await db.InsertAllAsync(orders);
Each row still has its own statement, with the same SQL, db params, filters and rules
as before. They're sent with an ADO.NET DbBatch, in a transaction, so either all the rows are written or none are.
Supported drivers​
Rows are sent together when the ADO.NET driver supports batches:
| Driver | OrmLite package | Batched |
|---|---|---|
| Npgsql | ServiceStack.OrmLite.PostgreSQL |
Yes |
| Microsoft.Data.SqlClient | ServiceStack.OrmLite.SqlServer.Data |
Yes |
| MySqlConnector | ServiceStack.OrmLite.MySqlConnector |
Yes |
| System.Data.SqlClient | ServiceStack.OrmLite.SqlServer |
No |
| MySql.Data | ServiceStack.OrmLite.MySql |
No |
| Microsoft.Data.Sqlite | ServiceStack.OrmLite.Sqlite.Data |
No |
| System.Data.SQLite | ServiceStack.OrmLite.Sqlite |
No |
With the other drivers, and on .NET Framework, a statement is sent for each row as before. SQLite runs in your App's process, so it has no round trips to save.
How much faster​
The time to write rows to a database on the same machine, which is when a round trip costs the least:
| Rows | A statement for each row | Sent together | Faster | |
|---|---|---|---|---|
| InsertAll, PostgreSQL | 100 | 6.4 ms | 1.5 ms | 4.3x |
| 1,000 | 65.0 ms | 13.1 ms | 4.9x | |
| InsertAll, SQL Server | 100 | 12.9 ms | 1.7 ms | 7.8x |
| 1,000 | 133.2 ms | 15.6 ms | 8.5x | |
| InsertAll, MariaDB | 100 | 5.4 ms | 1.7 ms | 3.3x |
| 1,000 | 57.5 ms | 15.8 ms | 3.6x | |
| UpdateAll, PostgreSQL | 100 | 13.4 ms | 7.4 ms | 1.8x |
| 1,000 | 86.6 ms | 23.1 ms | 3.7x | |
| UpdateAll, SQL Server | 100 | 18.0 ms | 7.3 ms | 2.5x |
| 1,000 | 130.5 ms | 14.8 ms | 8.8x | |
| UpdateAll, MariaDB | 100 | 6.1 ms | 1.8 ms | 3.4x |
| 1,000 | 65.5 ms | 16.6 ms | 3.9x |
Measured with BenchmarkDotNet on .NET 10, in a transaction. Updates include the time to commit, which is most of the time to update a few rows in PostgreSQL and SQL Server. Run them with:
cd ServiceStack.OrmLite/tests/ServiceStack.OrmLite.Tests.Benchmarks
dotnet run -c Release -- --filter '*DbBatch*'
The further your database is from your App, the more each round trip costs and the more that's saved.
Configure​
Rows are sent in batches of 1000 statements. Change it, or send a statement for each row, on the dialect:
services.AddOrmLite(options => options.UsePostgres(connectionString, dialect => {
dialect.BatchSize = 500; // statements sent together, 1000 by default
dialect.UseDbBatch = false; // send a statement for each row
}));
Rows that are sent on their own​
Some rows need a result from the database before the next row can be sent:
| API | Sent on their own |
|---|---|
InsertAll |
Rows of tables with values that are returned by the insert, e.g. [ReturnOnInsert] columns |
UpdateAll |
Rows with a RowVersion on MySqlConnector, which doesn't return the rows that each statement updated |
UpsertAll |
New rows of tables with an [AutoIncrement] Primary Key, whose id is read back. All rows of tables with a RowVersion or [ReturnOnInsert] columns, and on connections with filters or rules |
SaveAll |
New rows of tables with an [AutoIncrement] Primary Key, whose id is read back. All rows of tables with a RowVersion |
Rows are always written in the order they're in, so the rows before a row that's sent on its own are sent first.
A statement is also sent for each row when they're observed as they're run, with OrmLiteConfig.ResultsFilter, e.g.
CaptureSqlFilter, or the dialect's OnBeforeExecuteNonQuery and OnAfterExecuteNonQuery.
Things to be aware of​
- Filters are run for each row:
OrmLiteConfig.InsertFilter,UpdateFilterandBeforeExecFilter, acommandFilterand SQL logging all get each row's statement before it's sent - Errors are thrown when a batch is sent, not as each row is added, so a row that fails is reported after the
rows before it in its batch have been sent.
OrmLiteConfig.ExceptionFiltergets the last statement of the batch - Roll back when a write fails in your own transaction: SQL Server runs the statements after one that fails, so
some of the rows are written. The
*AllAPIs roll back their own transaction, do the same when they're called in yours - Stale row versions fail
UpdateAllwith anOptimisticConcurrencyExceptionafter its batch is sent, and none of its rows are updated - For thousands of rows BulkInsert and BulkUpsert are faster, which use each database's bulk loader instead of a statement for each row