Ternary false-expression does not take ternary condition into account, resulting in slow execution on SQL Server
- Dominant language
- C#
- Stars
- 14.8k
- Forks
- 3.4k
- PR merge metrics
- PR metrics pending
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:
```csharp
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:
```sql
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:
```csharp
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:
```sql
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
```csharp
See above
```
### Stack traces
```text
```
### Verbose output
```text
```
### 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
Contributor guide
Assessment
This issue has not been assessed yet.