-
Notifications
You must be signed in to change notification settings - Fork 3.3k
Description
We currently always wrap changes in a transaction - but that isn't necessary when only a single change is involved. Here's a comparison of a single change with and without a transaction:
| Method | DatabaseType | Mean | Error | StdDev | Median | Ratio | RatioSD |
|---|---|---|---|---|---|---|---|
| UpdateWithTransaction | Postgres | 1,171.1 us | 20.53 us | 19.20 us | 1,172.1 us | 1.00 | 0.00 |
| UpdateWithoutTransaction | Postgres | 998.9 us | 19.17 us | 16.99 us | 999.4 us | 0.85 | 0.02 |
| UpdateWithoutTransaction_SetImplicitTransaction | Postgres | NA | NA | NA | NA | ? | ? |
| Query | Postgres | 202.0 us | 5.40 us | 15.93 us | 206.9 us | 0.18 | 0.01 |
So the difference on PostgreSQL is 14.6%. Note that since unneeded roundtrips are eliminated (2 on Npgsql, 3 on SQL Server and most other providers), the perf gain increases as server latency increases (the above 14.6% are against localhost, so it's a minimum figure).
On SQL Server (localhost) the gain is higher, 27.8%.
Note that when an external transaction already exists, we still need to create a savepoint, even if there's only one change, since databases behave quite differently when a failure occurs during transaction, and a rollback to a savepoint is necessary in at least some cases (e.g. PostgreSQL).
Benchmark code
BenchmarkRunner.Run<Benchmark>();
public class Benchmark
{
private DbConnection _connection;
private DbCommand _command;
[Params(DatabaseType.Postgres, DatabaseType.SqlServer)]
public DatabaseType DatabaseType { get; set; }
private async Task Setup()
{
_connection = DatabaseType == DatabaseType.Postgres
? new NpgsqlConnection("Host=localhost;Username=test;Password=test;Max Auto Prepare=10")
: new SqlConnection("Server=localhost;Database=test;User=SA;Password=Abcd5678;Connect Timeout=60;ConnectRetryCount=0;Trust Server Certificate=true");
await _connection.OpenAsync();
using var cmd = _connection.CreateCommand();
cmd.CommandText = @"
DROP TABLE IF EXISTS data;
CREATE TABLE data (id int PRIMARY KEY, num INT);
INSERT INTO data (id, num) VALUES (1, 1)";
await cmd.ExecuteNonQueryAsync();
}
[GlobalSetup(Targets = new[] { nameof(UpdateWithTransaction), nameof(UpdateWithoutTransaction)})]
public async Task Setup_Update()
{
await Setup();
_command = _connection.CreateCommand();
_command.CommandText = "UPDATE data SET num = num + 1";
}
[GlobalSetup(Target = nameof(UpdateWithoutTransaction_SetImplicitTransaction))]
public async Task Setup_UpdateWithoutTransaction_SetImplicitTransaction()
{
if (DatabaseType != DatabaseType.SqlServer)
throw new NotSupportedException("Benchmark relevant only for SQL Server");
await Setup();
_command = _connection.CreateCommand();
_command.CommandText = "SET IMPLICIT_TRANSACTIONS OFF; UPDATE data SET num = num + 1";
}
[GlobalSetup(Target = nameof(Query))]
public async Task Setup_Query()
{
await Setup();
_command = _connection.CreateCommand();
_command.CommandText = "SELECT num FROM data WHERE id = 1";
}
[Benchmark(Baseline = true)]
public async Task UpdateWithTransaction()
{
var tx = _connection.BeginTransaction();
_command.Transaction = tx;
await _command.ExecuteNonQueryAsync();
tx.Commit();
}
[Benchmark]
public async Task UpdateWithoutTransaction()
{
await _command.ExecuteNonQueryAsync();
}
[Benchmark]
public async Task UpdateWithoutTransaction_SetImplicitTransaction()
{
await _command.ExecuteNonQueryAsync();
}
[Benchmark]
public async Task Query()
{
await _command.ExecuteNonQueryAsync();
}
}
public enum DatabaseType
{
Postgres,
SqlServer
}