Skip to content

Perf regression (V9): Generated SQL from ternary expression on nullable property causes slow execution on SQL Server #36291

@mycroes

Description

@mycroes

Bug description

While testing a complex query in templated form to find the most performant query I noticed EF Core 8 would produce a very suboptimal query with a ternary on a null-condition, while the not-null ternary was behaving fine. This resulted in the not-null ternary executing in about ~6s, while the null-conditioned ternary would run in ~33s.

Upgrading the codebase to EF Core 9 (in order to file this bug report) I noticed the not-null conditioned ternary had regressed to the same performance as the null conditioned ternary due to changes in the generated SQL.

Your code

I'm using the following predicate as part of the template:

r => Template.SupplyOrderId != null
    ? r.SourcePallet.SupplyOrderId == Template.SupplyOrderId
    : r.SourcePallet.Id == Template.ActivePalletId

In EF Core 8, this resulted in the following SQL (subquery only):

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 p3.ActivePalletId is int?, s.SupplyOrderId is int? as well.

in EF Core 9, the subquery is as follows:

SELECT [w].[NetWeight], 1 AS [Key]
FROM [WasteRegistrations] AS [w]
INNER JOIN [SourcePallet] AS [s4] ON [w].[SourcePalletId] = [s4].[Id]
WHERE CASE
    WHEN [s].[SupplyOrderId] IS NOT NULL THEN ~CAST([s4].[SupplyOrderId] ^ [s].[SupplyOrderId] AS bit)
    ELSE ~CAST([s4].[Id] ^ [p3].[ActivePalletId] AS bit)
END = CAST(1 AS bit)

The EF8 query results in a filter on the SourcePallet table, which in turn requires a Clustered Index Seek on the WasteRegistrations table to find the accompanying waste registrations. With EF9 however, the subquery causes a CLustered Index Scan, because (my assumption) it can no longer digest what source it should use to match the s4.SupplyOrderId part of the query. I also have my doubts how this behavior will affect filtered indexes, but I think it renders them moot due to the way the query is now built.

Stack traces

NA.

Verbose output

NA.

EF Core version

9.0.6

Database provider

Microsoft.EntityFrameworkCore.SqlServer

Target framework

.NET 8.0

Operating system

Windows 11

IDE

Visual Studio 2022 17.4

Metadata

Metadata

Assignees

Type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions