Skip to content

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

@mycroes

Description

@mycroes

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

See above

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

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions