-
Notifications
You must be signed in to change notification settings - Fork 3.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Stop wrapping single changes in transactions where possible #27439
Comments
Note: I still need to do some due dilligence that dropping single-statement transactions has no effect across databases. |
See #6342. |
@ajcvickers thanks... I updated the issue with the benchmark code - note that this was a pure ADO.NET benchmark (though such gains are very likely to show through with EF too). |
@roji Very possible things have changed since 2016. Also, that was SQL Server only. Also, I thought we added an option to DbContext to opt out of transaction creation, but I can't find it. So either we didn't do it, we removed it, or my long-term memory is corrupted. |
Ah, but that disables all transactions, regardless of the number of changes, right? That's quite different. BTW am surprised that disabling the transaction speeds things up - usually it's the opposite. Interesting. Maybe removing the transaction was faster... with just one update... |
Note: IMPLICIT_TRANSACTIONS is indeed a problem. Although this is an obscure feature, if it happens to be on, then changes aren't actually committed until the next COMMIT (see code sample below). We could mitigate this by prepending IMPLICIT_TRANSACTION demoawait using var conn = new SqlConnection("Server=localhost;Database=test;User=SA;Password=Abcd5678;Connect Timeout=60;ConnectRetryCount=0;Encrypt=false");
await conn.OpenAsync();
using var cmd = new SqlCommand(@"DROP TABLE IF EXISTS data; CREATE TABLE data (id int PRIMARY KEY, num INT)", conn);
await cmd.ExecuteNonQueryAsync();
// Regular (IMPLICIT_TRANSACTIONS is OFF)
cmd.CommandText = "INSERT INTO data (id, num) VALUES (1, 1)";
await cmd.ExecuteNonQueryAsync();
await conn.CloseAsync();
await conn.OpenAsync();
cmd.CommandText = "SELECT COUNT(*) FROM data";
Console.WriteLine("Rows1: " + await cmd.ExecuteScalarAsync()); // 1
// IMPLICIT_TRANSACTIONS is ON
cmd.CommandText = "SET IMPLICIT_TRANSACTIONS ON";
await cmd.ExecuteNonQueryAsync();
cmd.CommandText = "INSERT INTO data (id, num) VALUES (2, 2)";
await cmd.ExecuteNonQueryAsync();
await conn.CloseAsync();
await conn.OpenAsync();
cmd.CommandText = "SELECT COUNT(*) FROM data";
Console.WriteLine("Rows2: " + await cmd.ExecuteScalarAsync()); // 1 |
SQLite does autocommit (the sane thing) by default (docs), and I don't see a way to disable that (good). MySQL has the same situation as SQL Server (docs): while it does autocommit by default, you can do So:
|
Note: updated the benchmark above with a scenario where we prepend |
Note by @AndriySvyryd: this may cause us to prefer a single-statement for insert when returning generated values (#27372). For example, today for single-updates which involve identity columns, we do INSERT+SELECT: INSERT INTO [Blogs] ([Name])
VALUES (@p0);
SELECT [Id], [Foo]
FROM [Blogs]
WHERE @@ROWCOUNT = 1 AND [Id] = scope_identity(); However, it's not certain that these two statements actually need to be wrapped in a transaction. Because we use read commited isolation (the default), concurrent updates between the INSERT and the SELECT are already possible (see #27446). Since |
Design decisions:
|
Even the documentation for |
@GSPP the docs do say that, but they also say that when In any case, my logic here is that if we drop the transaction without handling |
Closes #27439 Closes #27507 Co-authored-by: Andriy Svyryd <[email protected]>
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:
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
The text was updated successfully, but these errors were encountered: