Skip to content

Stop wrapping single changes in transactions where possible #27439

@roji

Description

@roji

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
}

Metadata

Metadata

Assignees

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions