- 
                Notifications
    
You must be signed in to change notification settings  - Fork 3.3k
 
Closed
SQL Server: Optimize SQL Server OUTPUT clause usage when retrieving database-generated values #27372
Feature
Copy link
Milestone
Description
When inserting an entity with database-generated columns, we currently generate the following (as long as there's no IDENTITY column):
DECLARE @inserted0 TABLE ([Id] int);
INSERT INTO [Blogs] ([Name]) OUTPUT INSERTED.[Id] INTO @inserted0 VALUES (@p0);
SELECT [i].[Id] FROM @inserted0 i;This could be simplified into this:
INSERT INTO [Blogs] ([Name]) OUTPUT INSERTED.[Id] VALUES (@p0);The roundabout through the inserted0 TVP is probably because the OUTPUT clause won't work if there's a trigger defined, unless it's an OUTPUT INTO (??) (@AndriySvyryd it this right, any more context?).
Unfortunately, using OUTPUT INTO instead of OUTPUT adds a lot of overhead:
// * Summary *
BenchmarkDotNet=v0.13.0, OS=ubuntu 21.10
Intel Xeon W-2133 CPU 3.60GHz, 1 CPU, 12 logical and 6 physical cores
.NET SDK=6.0.101
  [Host]     : .NET 6.0.1 (6.0.121.56705), X64 RyuJIT
  DefaultJob : .NET 6.0.1 (6.0.121.56705), X64 RyuJIT
|     Method |     Mean |     Error |    StdDev | Ratio | RatioSD |
|----------- |---------:|----------:|----------:|------:|--------:|
|   NoOutput | 2.119 ms | 0.0423 ms | 0.0396 ms |  0.39 |    0.01 |
|     Output | 1.926 ms | 0.0382 ms | 0.0497 ms |  0.36 |    0.02 |
| OutputInto | 5.365 ms | 0.1068 ms | 0.2083 ms |  1.00 |    0.00 |
That's over 3ms just for passing through a TVP! Also, mysteriously the version with no OUTPUT clause at all performs worse...
Remarks:
- We could default to using 
OUTPUT, and switch back toOUTPUT INTOif the user tells us the table has triggers (via metadata). - Note that we have Do not track after SaveChanges() #9118 for not retrieving anything; this would make this optimization a bit less valuable.
 - If the table has any IDENTITY column, OUTPUT isn't used at all.
 
Benchmark code
BenchmarkRunner.Run<SequenceBenchmark>();
public class SequenceBenchmark
{
    const string ConnectionString = "Server=localhost;Database=test;User=SA;Password=Abcd5678;Connect Timeout=60;ConnectRetryCount=0;Encrypt=false";
    private SqlConnection _connection;
    [GlobalSetup]
    public async Task Setup()
    {
        _connection = new SqlConnection(ConnectionString);
        await _connection.OpenAsync();
        await using var cmd = new SqlCommand(@"
DROP TABLE IF EXISTS [Foo];
DROP SEQUENCE IF EXISTS [FooSeq];
CREATE SEQUENCE [FooSeq] AS int START WITH 1 INCREMENT BY 1 NO MINVALUE NO MAXVALUE NO CYCLE;
CREATE TABLE [Foo] (
    [Id] int PRIMARY KEY NOT NULL DEFAULT (NEXT VALUE FOR FooSeq),
    [BAR] int
);", _connection);
        await cmd.ExecuteNonQueryAsync();
    }
    [Benchmark]
    public async Task NoOutput()
    {
        await using var cmd = new SqlCommand("INSERT INTO [Foo] ([Bar]) VALUES (8)", _connection);
        _ = await cmd.ExecuteScalarAsync();
    }
    [Benchmark]
    public async Task Output()
    {
        await using var cmd = new SqlCommand("INSERT INTO [Foo] ([Bar]) OUTPUT INSERTED.[Id] VALUES (8)", _connection);
        _ = await cmd.ExecuteScalarAsync();
    }
    [Benchmark(Baseline = true)]
    public async Task OutputInto()
    {
        await using var cmd = new SqlCommand(@"DECLARE @inserted TABLE ([Id] int);
INSERT INTO [Foo] ([Bar]) OUTPUT INSERTED.[Id] INTO @inserted VALUES (8);
SELECT [i].[Id] FROM @inserted i;
", _connection);
        _ = await cmd.ExecuteScalarAsync();
    }
    [GlobalCleanup]
    public ValueTask Cleanup()
        => _connection.DisposeAsync();
}umbersar