Skip to content

Commit 579baa9

Browse files
authored
8.0 breaking change note on parameterized collection perf regression (#4887)
See dotnet/efcore#32394 (comment)
1 parent e578fcf commit 579baa9

File tree

1 file changed

+93
-12
lines changed

1 file changed

+93
-12
lines changed

entity-framework/core/what-is-new/ef-core-8.0/breaking-changes.md

Lines changed: 93 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ EF Core 8 targets .NET 8. Applications targeting older .NET, .NET Core, and .NET
2222
| **Breaking change** | **Impact** |
2323
|:--------------------------------------------------------------------------------------------------------------|------------|
2424
| [`Contains` in LINQ queries may stop working on older SQL Server versions](#sqlserver-contains-compatibility) | High |
25+
| [Possible query performance regressions around `Contains` in LINQ queries](#contains-perf-regression) | High |
2526
| [Enums in JSON are stored as ints instead of strings by default](#enums-as-ints) | High |
2627
| [SQL Server `date` and `time` now scaffold to .NET `DateOnly` and `TimeOnly`](#sqlserver-date-time-only) | Medium |
2728
| [Boolean columns with a database generated value are no longer scaffolded as nullable](#scaffold-bools) | Medium |
@@ -47,17 +48,79 @@ EF Core 8 targets .NET 8. Applications targeting older .NET, .NET Core, and .NET
4748

4849
#### Old behavior
4950

50-
Previously, when the `Contains` operator was used in LINQ queries with a parameterized value list, EF generated SQL that was inefficient but worked on all SQL Server versions.
51+
EF had specialized support for LINQ queries using `Contains` operator over a parameterized value list:
52+
53+
```c#
54+
var names = new[] { "Blog1", "Blog2" };
55+
56+
var blogs = await context.Blogs
57+
.Where(b => names.Contains(b.Name))
58+
.ToArrayAsync();
59+
```
60+
61+
Before EF Core 8.0, EF inserted the parameterized values as constants into the SQL:
62+
63+
```sql
64+
SELECT [b].[Id], [b].[Name]
65+
FROM [Blogs] AS [b]
66+
WHERE [b].[Name] IN (N'Blog1', N'Blog2')
67+
```
5168

5269
#### New behavior
5370

54-
Starting with EF Core 8.0, EF now generates SQL that is more efficient, but is unsupported on SQL Server 2014 and below.
71+
Starting with EF Core 8.0, EF now generates SQL that is more efficient in many cases, but is unsupported on SQL Server 2014 and below:
72+
73+
```sql
74+
SELECT [b].[Id], [b].[Name]
75+
FROM [Blogs] AS [b]
76+
WHERE [b].[Name] IN (
77+
SELECT [n].[value]
78+
FROM OPENJSON(@__names_0) WITH ([value] nvarchar(max) '$') AS [n]
79+
)
80+
```
5581

5682
Note that newer SQL Server versions may be configured with an older [compatibility level](/sql/t-sql/statements/alter-database-transact-sql-compatibility-level), also making them incompatible with the new SQL. This can also occur with an Azure SQL database which was migrated from a previous on-premises SQL Server instance, carrying over the old compatibility level.
5783

5884
#### Why
5985

60-
The previous SQL generated by EF Core for `Contains` inserted the parameterized values as constants in the SQL. For example, the following LINQ query:
86+
The insertion of constant values into the SQL creates many performance problems, defeating query plan caching and causing unneeded evictions of other queries. The new EF Core 8.0 translation uses the SQL Server [`OPENJSON`](/sql/t-sql/functions/openjson-transact-sql) function to instead transfer the values as a JSON array. This solves the performance issues inherent in the previous technique; however, the `OPENJSON` function is unavailable in SQL Server 2014 and below.
87+
88+
For more information about this change, [see this blog post](https://devblogs.microsoft.com/dotnet/announcing-ef8-preview-4/).
89+
90+
#### Mitigations
91+
92+
If your database is SQL Server 2016 (13.x) or newer, or if you're using Azure SQL, check the configured compatibility level of your database via the following command:
93+
94+
```sql
95+
SELECT name, compatibility_level FROM sys.databases;
96+
```
97+
98+
If the compatibility level is below 130 (SQL Server 2016), consider modifying it to a newer value ([documentation](/sql/t-sql/statements/alter-database-transact-sql-compatibility-level#best-practices-for-upgrading-database-compatibility-leve)).
99+
100+
Otherwise, if your database version really is older than SQL Server 2016, or is set to an old compatibility level which you cannot change for some reason, you can configure EF to revert to the older, pre-8.0 SQL. If you're using EF 9, you can use the newly-introduced <xref:Microsoft.EntityFrameworkCore.Infrastructure.RelationalDbContextOptionsBuilder%602.TranslateParameterizedCollectionsToConstants%2A>:
101+
102+
```c#
103+
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
104+
=> optionsBuilder.UseSqlServer("<CONNECTION STRING>", o => o.TranslateParameterizedCollectionsToConstants())
105+
```
106+
107+
If you're using EF 8, you can achieve the same effect when using SQL Server by configuring EF's SQL compatibility level:
108+
109+
```c#
110+
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
111+
=> optionsBuilder
112+
.UseSqlServer(@"<CONNECTION STRING>", o => o.UseCompatibilityLevel(120));
113+
```
114+
115+
<a name="contains-perf-regression"></a>
116+
117+
### Possible query performance regressions around `Contains` in LINQ queries
118+
119+
[Tracking Issue #32394](https://github.com/dotnet/efcore/issues/32394)
120+
121+
#### Old behavior
122+
123+
EF had specialized support for LINQ queries using `Contains` operator over a parameterized value list:
61124

62125
```c#
63126
var names = new[] { "Blog1", "Blog2" };
@@ -67,36 +130,54 @@ var blogs = await context.Blogs
67130
.ToArrayAsync();
68131
```
69132

70-
... would be translated to the following SQL:
133+
Before EF Core 8.0, EF inserted the parameterized values as constants into the SQL:
71134

72135
```sql
73136
SELECT [b].[Id], [b].[Name]
74137
FROM [Blogs] AS [b]
75138
WHERE [b].[Name] IN (N'Blog1', N'Blog2')
76139
```
77140

78-
Such insertion of constant values into the SQL creates many performance problems, defeating query plan caching and causing unneeded evictions of other queries. The new EF Core 8.0 translation uses the SQL Server [`OPENJSON`](/sql/t-sql/functions/openjson-transact-sql) function to instead transfer the values as a JSON array. This solves the performance issues inherent in the previous technique; however, the `OPENJSON` function is unavailable in SQL Server 2014 and below.
141+
#### New behavior
79142

80-
For more information about this change, [see this blog post](https://devblogs.microsoft.com/dotnet/announcing-ef8-preview-4/).
143+
Starting with EF Core 8.0, EF now generates the following:
144+
145+
```sql
146+
SELECT [b].[Id], [b].[Name]
147+
FROM [Blogs] AS [b]
148+
WHERE [b].[Name] IN (
149+
SELECT [n].[value]
150+
FROM OPENJSON(@__names_0) WITH ([value] nvarchar(max) '$') AS [n]
151+
)
152+
```
153+
154+
However, after the release of EF 8 it turned out that while the new SQL is more efficient for most cases, it can be dramatically less efficient in a minority of cases, even causing query timeouts in some cases
81155

82156
#### Mitigations
83157

84-
If your database is SQL Server 2016 (13.x) or newer, or if you're using Azure SQL, check the configured compatibility level of your database via the following command:
158+
If you're using EF 9, you can use the newly-introduced <xref:Microsoft.EntityFrameworkCore.Infrastructure.RelationalDbContextOptionsBuilder%602.TranslateParameterizedCollectionsToConstants%2A> to revert the `Contains` translation for all queries back to the pre-8.0 behavior:
85159

86-
```sql
87-
SELECT name, compatibility_level FROM sys.databases;
160+
```c#
161+
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
162+
=> optionsBuilder.UseSqlServer("<CONNECTION STRING>", o => o.TranslateParameterizedCollectionsToConstants())
88163
```
89164

90-
If the compatibility level is below 130 (SQL Server 2016), consider modifying it to a newer value ([documentation](/sql/t-sql/statements/alter-database-transact-sql-compatibility-level#best-practices-for-upgrading-database-compatibility-leve)).
91-
92-
Otherwise, if your database version really is older than SQL Server 2016, or is set to an old compatibility level which you cannot change for some reason, configure EF Core to revert to the older, less efficient SQL as follows:
165+
If you're using EF 8, you can achieve the same effect when using SQL Server by configuring EF's SQL compatibility level:
93166

94167
```c#
95168
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
96169
=> optionsBuilder
97170
.UseSqlServer(@"<CONNECTION STRING>", o => o.UseCompatibilityLevel(120));
98171
```
99172

173+
Finally, you can control the translation on a query-by-query basis using <xref:Microsoft.EntityFrameworkCore.EF.Constant%2A?displayProperty=nameWithType> as follows:
174+
175+
```c#
176+
var blogs = await context.Blogs
177+
.Where(b => EF.Constant(names).Contains(b.Name))
178+
.ToArrayAsync();
179+
```
180+
100181
<a name="enums-as-ints"></a>
101182

102183
### Enums in JSON are stored as ints instead of strings by default

0 commit comments

Comments
 (0)