dotnet / dotnet/efcore

Ternary false-expression does not take ternary condition into account, resulting in slow execution on SQL Server

Open
#36,293 2 comments 0 reactions 0 assignees View on GitHub
area-perf area-query customer-reported
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

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.