Bug description
EF Core 8, only, but pending outcome of #36291.
When the ternary condition contains a == null check, the else branch doesn't eliminate null values in the else expression, resulting in queries that will scan more data than required when executed against SQL Server.
As part of a query template to optimize performance for a specific query I have the following C# predicate, which translates and works as expected:
r => Template.SupplyOrderId != null
? r.SourcePallet.SupplyOrderId == Template.SupplyOrderId
: r.SourcePallet.Id == Template.ActivePalletId
This predicate translates to the where clause of the following SQL subquery:
SELECT [w].[NetWeight], 1 AS [Key]
FROM [WasteRegistrations] AS [w]
INNER JOIN [SourcePallet] AS [s3] ON [w].[SourcePalletId] = [s3].[Id]
WHERE CASE
WHEN [s].[SupplyOrderId] IS NOT NULL THEN CASE
WHEN [s3].[SupplyOrderId] = [s].[SupplyOrderId] AND [s3].[SupplyOrderId] IS NOT NULL THEN CAST(1 AS bit)
ELSE CAST(0 AS bit)
END
ELSE CASE
WHEN [s3].[Id] = [p3].[ActivePalletId] AND [p3].[ActivePalletId] IS NOT NULL THEN CAST(1 AS bit)
ELSE CAST(0 AS bit)
END
END = CAST(1 AS bit)
Here s.SupplyOrderId is of type int?, just as well as p3.ActivePalletId is of type int?.
If we however invert the ternary conditions and swap the expressions, we get the following expression:
r => Template.SupplyOrderId == null
? r.SourcePallet.Id == Template.ActivePalletId
: r.SourcePallet.SupplyOrderId == Template.SupplyOrderId.Value);
This predicate translates to the where clause of the following SQL subquery:
SELECT [w].[NetWeight], 1 AS [Key]
FROM [WasteRegistrations] AS [w]
INNER JOIN [SourcePallet] AS [s3] ON [w].[SourcePalletId] = [s3].[Id]
WHERE CASE
WHEN [s].[SupplyOrderId] IS NULL THEN CASE
WHEN [s3].[Id] = [p3].[ActivePalletId] AND [p3].[ActivePalletId] IS NOT NULL THEN CAST(1 AS bit)
ELSE CAST(0 AS bit)
END
ELSE CASE
WHEN ([s3].[SupplyOrderId] = [s].[SupplyOrderId] AND [s3].[SupplyOrderId] IS NOT NULL AND [s].[SupplyOrderId] IS NOT NULL) OR ([s3].[SupplyOrderId] IS NULL AND [s].[SupplyOrderId] IS NULL) THEN CAST(1 AS bit)
ELSE CAST(0 AS bit)
END
END = CAST(1 AS bit)
The obvious flaw in this query is OR ([s3].[SupplyOrderId] IS NULL AND [s].[SupplyOrderId] IS NULL, because that's already excluded by this branch being the ELSE CASE of WHEN [s].[SupplyOrderId] IS NULL. Unfortunately SQL Server isn't able to optimize this out of the query, resulting in very inefficient data access. SourcePallet gets a Clustered Index Scan while there's more approriate indexes to use, this in turn causes a Clustered Index Scan against the WasteRegistration table (which in my case is a lot larger) instead of a more optimal Clustered Index Seek. Of course the outcome is specific to my data structures, nonetheless the OR part can be eliminated resolving this specific issue,
Your code
Stack traces
Verbose output
EF Core version
8.0.11
Database provider
Microsoft.EntityFrameworkCore.SqlServer
Target framework
.NET 8.0
Operating system
Windows 11
IDE
Visual Studio 2022 17.4
Bug description
EF Core 8, only, but pending outcome of #36291.
When the ternary condition contains a
== nullcheck, the else branch doesn't eliminate null values in the else expression, resulting in queries that will scan more data than required when executed against SQL Server.As part of a query template to optimize performance for a specific query I have the following C# predicate, which translates and works as expected:
This predicate translates to the where clause of the following SQL subquery:
Here
s.SupplyOrderIdis of typeint?, just as well asp3.ActivePalletIdis of typeint?.If we however invert the ternary conditions and swap the expressions, we get the following expression:
This predicate translates to the where clause of the following SQL subquery:
The obvious flaw in this query is
OR ([s3].[SupplyOrderId] IS NULL AND [s].[SupplyOrderId] IS NULL, because that's already excluded by this branch being theELSE CASEofWHEN [s].[SupplyOrderId] IS NULL. Unfortunately SQL Server isn't able to optimize this out of the query, resulting in very inefficient data access.SourcePalletgets a Clustered Index Scan while there's more approriate indexes to use, this in turn causes a Clustered Index Scan against theWasteRegistrationtable (which in my case is a lot larger) instead of a more optimalClustered Index Seek. Of course the outcome is specific to my data structures, nonetheless theORpart can be eliminated resolving this specific issue,Your code
Stack traces
Verbose output
EF Core version
8.0.11
Database provider
Microsoft.EntityFrameworkCore.SqlServer
Target framework
.NET 8.0
Operating system
Windows 11
IDE
Visual Studio 2022 17.4