-
Notifications
You must be signed in to change notification settings - Fork 3.3k
Description
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.ActivePalletIdThis 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
See aboveStack 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