【问题标题】:EF6 and EFCore context generate different SQL scripts causing performance issue in EFCoreEF6 和 EF Core 上下文生成不同的 SQL 脚本,导致 EF Core 出现性能问题
【发布时间】:2021-11-16 23:41:00
【问题描述】:

我最近一直在将旧解决方案从 EF6 升级到 EFCore,将 Net48 升级到 Net5.0。

所以在迁移到 EFCore 之后,我注意到一些在 EF6 中执行良好的查询现在在 EFCore 中是 SQL 超时。

我将旧的 EF6 和 EFCore DbContext 连接都导入到 LINQPad 中,并调查了生成的 SQL。

这些是我的实体:

[Table("Asset")]
public class Asset
{
    public Asset()
    {
        MonitoringLogs = new HashSet<MonitoringLog>();
    }

    [Key]
    public int AssetId { get; set; }

    public virtual ICollection<MonitoringLog> MonitoringLogs { get; set; }

}
[Table("MonitoringLog")]
public class MonitoringLog
{
    [Key]
    public int MonitoringLogId { get; set; }

    public DateTime LogUTCDateTime { get; set; }

    public string OtherProperty { get; set; }

    [ForeignKey(nameof(Asset))]
    public int AssetId { get; set; }
    public virtual Asset Asset { get; set; }

}

这是我的 LINQ 查询:

 this.Assets.SelectMany(r => r.MonitoringLogs.OrderByDescending(t => t.LogUTCDateTime).Take(1)).Dump();

在 EFCore 中生成的 SQL 是:

SELECT [t0].[MonitoringLogId], [t0].[AssetId], [t0].[LogUTCDateTime], [t0].[OtherProperty]
FROM [Asset] AS [a]
INNER JOIN (
    SELECT [t].[MonitoringLogId], [t].[AssetId], [t].[LogUTCDateTime], [t].[OtherProperty]
    FROM (
        SELECT [m].[MonitoringLogId], [m].[AssetId], [m].[LogUTCDateTime], [m].[OtherProperty], ROW_NUMBER() OVER(PARTITION BY [m].[AssetId] ORDER BY [m].[LogUTCDateTime] DESC) AS [row]
        FROM [MonitoringLog] AS [m]
    ) AS [t]
    WHERE [t].[row] <= 1
) AS [t0] ON [a].[AssetId] = [t0].[AssetId]
GO

它似乎生成了一个ROW_NUMBER(),其分区为AssetId。然后过滤掉带有RowNumber == 1 的记录。现在我不明白为什么当TOP (1) ORDER BY LogUTCDateTime 可以解决这个问题时这一切都是必要的。

执行计划:https://www.brentozar.com/pastetheplan/?id=H1a3J6-dY

而EF6中生成的SQL是:

SELECT 
    [Limit1].[MonitoringLogId] AS [MonitoringLogId], 
    [Limit1].[LogUTCDateTime] AS [LogUTCDateTime], 
    [Limit1].[OtherProperty] AS [OtherProperty], 
    [Limit1].[AssetId] AS [AssetId]
    FROM  [dbo].[Asset] AS [Extent1]
    CROSS APPLY  (SELECT TOP (1) [Project1].[MonitoringLogId] AS [MonitoringLogId], [Project1].[LogUTCDateTime] AS [LogUTCDateTime], [Project1].[OtherProperty] AS [OtherProperty], [Project1].[AssetId] AS [AssetId]
        FROM ( SELECT 
            [Extent2].[MonitoringLogId] AS [MonitoringLogId], 
            [Extent2].[LogUTCDateTime] AS [LogUTCDateTime], 
            [Extent2].[OtherProperty] AS [OtherProperty], 
            [Extent2].[AssetId] AS [AssetId]
            FROM [dbo].[MonitoringLog] AS [Extent2]
            WHERE [Extent1].[AssetId] = [Extent2].[AssetId]
        )  AS [Project1]
        ORDER BY [Project1].[LogUTCDateTime] DESC ) AS [Limit1]

执行计划:https://www.brentozar.com/pastetheplan/?id=SJLQxTbdY

对于小数据集,对查询没有太大影响 - 但对于大数据集,这会导致 SQL 超时。

这是一个包含 EF6 和 EFCore 上下文的 Repo,该 Repo 还包含用于生成 DB/Tables 并创建一些示例数据的 SQL 脚本:

https://github.com/mdawood1991/EFIssue

注意:以上示例是从我的大型解决方案中提取的示例。

【问题讨论】:

    标签: entity-framework entity-framework-core


    【解决方案1】:

    这似乎是 EF Core 的一个已知问题:https://github.com/dotnet/efcore/issues/17936

    普遍接受的解决方法似乎是在内部条件中添加一个ToArray(),以强制 EF 在不使用行计数器的情况下更改生成:

    this.Assets.SelectMany(r => r.MonitoringLogs
         .OrderByDescending(t => t.LogUTCDateTime)
         .Take(1)
         .ToArray()
         .FirstOrDefault()).Dump();
    

    【讨论】:

    • 谢谢 - 这似乎比默认生成的 SQL 更好。希望它在下一个版本中得到修复,这会对像这样扫描整个表的性能产生重大影响。
    猜你喜欢
    • 1970-01-01
    • 2021-09-25
    • 2021-10-05
    • 2020-06-12
    • 1970-01-01
    • 2016-11-21
    • 2017-12-16
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多