-
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 INTO
if 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