与 SSMS 相比,实体框架中的查询时间极慢

nuk*_*eem 5 c# sql-server entity-framework-core

我继承了一个代码库,但在 Entity Framework Core v3.1.19 上遇到了一个奇怪的问题。

实体框架正在生成以下查询(如 SQL Server Profiler 中所示),并且运行需要近 30 秒,而在 SSMS 中运行相同的代码(再次从分析器中获取)需要 1 秒(这是一个示例,但整个站点运行)从数据库获取数据时速度非常慢)。

exec sp_executesql N'SELECT [t].[Id], [t].[AccrualLink], [t].[BidId], [t].[BidId1], [t].[Cancelled], [t].[ClientId], [t].[CreatedUtc], [t].[CreatorUserId], [t].[Date], [t].[DeletedUtc], [t].[DeleterUserId], [t].[EmergencyContact], [t].[EmergencyName], [t].[EmergencyPhone], [t].[EndDate], [t].[FinalizerId], [t].[Guid], [t].[Invoiced], [t].[IsDeleted], [t].[Notes], [t].[OfficeId], [t].[PONumber], [t].[PlannerId], [t].[PortAgencyAgentEmail], [t].[PortAgencyAgentName], [t].[PortAgencyAgentPhone], [t].[PortAgencyId], [t].[PortAgentId], [t].[PortId], [t].[PortType], [t].[PositionNote], [t].[ProposalLink], [t].[ServiceId], [t].[ShipId], [t].[ShorexAssistantEmail], [t].[ShorexAssistantName], [t].[ShorexAssistantPhone], [t].[ShorexManagerEmail], [t].[ShorexManagerName], [t].[ShorexManagerPhone], [t].[ShuttleBus], [t].[ShuttleBusEmail], [t].[ShuttleBusName], [t].[ShuttleBusPhone], [t].[ShuttleBusServiceProvided], [t].[TouristInformationBus], [t].[TouristInformationEmail], [t].[TouristInformationName], [t].[TouristInformationPhone], [t].[TouristInformationServiceProvided], [t].[UpdatedUtc], [t].[UpdaterUserId], [t].[Water], [t].[WaterDetails], [t0].[Id], [t0].[CreatedUtc], [t0].[CreatorUserId], [t0].[DeletedUtc], [t0].[DeleterUserId], [t0].[Guid], [t0].[IsDeleted], [t0].[LanguageId], [t0].[Logo], [t0].[Name], [t0].[Notes], [t0].[OldId], [t0].[PaymentTerms], [t0].[Pricing], [t0].[Services], [t0].[Status], [t0].[UpdatedUtc], [t0].[UpdaterUserId], [t1].[Id], [t1].[CreatedUtc], [t1].[CreatorUserId], [t1].[DeletedUtc], [t1].[DeleterUserId], [t1].[Guid], [t1].[IsDeleted], [t1].[Name], [t1].[OldId], [t1].[UpdatedUtc], [t1].[UpdaterUserId], [s].[Id], [s].[CreatedUtc], [s].[CreatorUserId], [s].[DeletedUtc], [s].[DeleterUserId], [s].[Guid], [s].[IsDeleted], [s].[Name], [s].[Pax], [s].[UpdatedUtc], [s].[UpdaterUserId]
FROM (
    SELECT [o].[Id], [o].[AccrualLink], [o].[BidId], [o].[BidId1], [o].[Cancelled], [o].[ClientId], [o].[CreatedUtc], [o].[CreatorUserId], [o].[Date], [o].[DeletedUtc], [o].[DeleterUserId], [o].[EmergencyContact], [o].[EmergencyName], [o].[EmergencyPhone], [o].[EndDate], [o].[FinalizerId], [o].[Guid], [o].[Invoiced], [o].[IsDeleted], [o].[Notes], [o].[OfficeId], [o].[PONumber], [o].[PlannerId], [o].[PortAgencyAgentEmail], [o].[PortAgencyAgentName], [o].[PortAgencyAgentPhone], [o].[PortAgencyId], [o].[PortAgentId], [o].[PortId], [o].[PortType], [o].[PositionNote], [o].[ProposalLink], [o].[ServiceId], [o].[ShipId], [o].[ShorexAssistantEmail], [o].[ShorexAssistantName], [o].[ShorexAssistantPhone], [o].[ShorexManagerEmail], [o].[ShorexManagerName], [o].[ShorexManagerPhone], [o].[ShuttleBus], [o].[ShuttleBusEmail], [o].[ShuttleBusName], [o].[ShuttleBusPhone], [o].[ShuttleBusServiceProvided], [o].[TouristInformationBus], [o].[TouristInformationEmail], [o].[TouristInformationName], [o].[TouristInformationPhone], [o].[TouristInformationServiceProvided], [o].[UpdatedUtc], [o].[UpdaterUserId], [o].[Water], [o].[WaterDetails]
    FROM [OpsDocuments] AS [o]
    WHERE ([o].[IsDeleted] <> CAST(1 AS bit)) AND ((CASE
        WHEN [o].[Cancelled] = CAST(0 AS bit) THEN CAST(1 AS bit)
        ELSE CAST(0 AS bit)
    END & CASE
        WHEN [o].[Invoiced] = CAST(0 AS bit) THEN CAST(1 AS bit)
        ELSE CAST(0 AS bit)
    END) = CAST(1 AS bit))
    ORDER BY [o].[Date]
    OFFSET @__p_0 ROWS FETCH NEXT @__p_1 ROWS ONLY
) AS [t]
LEFT JOIN [TourClients] AS [t0] ON [t].[ClientId] = [t0].[Id]
LEFT JOIN [TourLanguages] AS [t1] ON [t0].[LanguageId] = [t1].[Id]
LEFT JOIN [Ships] AS [s] ON [t].[ShipId] = [s].[Id]
ORDER BY [t].[Date]',N'@__p_0 int,@__p_1 int',@__p_0=0,@__p_1=10
Run Code Online (Sandbox Code Playgroud)

此查询从可能的 55 行中返回 10 行,因此我们并不是在谈论大数字或任何其他内容。

起初我认为这可能是转换时的数据类型问题,但检查了所有数据类型,它们都是正确的,并且由于问题显示在探查器中,我假设这是一个 SQL 问题,而不是特定的实体框架。然而,在分析器中运行时,我找不到两者之间的任何区别,除了 EF 的运行时间长了 30 倍。

希望有人能建议去哪里看。

编辑:感谢评论中的所有建议。至于 Linq 和可重现的示例,这将很棘手,因为该项目的代码库是一些奇怪的自制自动生成系统。你给它一个带有大量自定义属性的 ViewModel,它会尝试为你做所有事情(如此多的抽象层),因此很难找到任何东西。听起来我将不得不开始将它们重写为更有限的控制器。

Ste*_* Py 0

EF 总是比原始 SQL 花费更长的时间,因为 EF 必须为查询中返回的每个实体具体化跟踪实体。

查看 SQL,这是跨 4 个表、OPSDocuments、TourClients、TourLanguages 和 Ships 的急切加载查询。

在一些看似无关的更改之后,这可能会突然花费更长的时间,原因是:新关系被延迟加载。一个例子是,该数据正在被序列化,并且新的关系已被添加到一个或多个实体,而这些实体现在正被延迟加载命中所触发。(通常通过在页面加载之前运行此查询后看到出现额外查询来证明)

导致此过程花费的时间比应有的时间长的其他原因:

  1. DbContext 正在跟踪太多实体。DbContext 跟踪的实体越多,在将 Linq 查询的结果拼凑在一起时必须经历的引用就越多。一些团队期望 EF 缓存类似于 NHibernate 的实例,这将提高性能。通常情况恰恰相反,跟踪的实体越多,获得结果所需的时间就越长。
  2. 并发读取和锁定。如果表没有有效地建立索引,那么与测试/调试相比,当系统在生产中运行时,这可能会成为一个杀手。通常,这会影响具有大量行和/或用户数的系统。

在使用 EF 解决性能问题时,我能提供的最佳一般建议是尽可能利用投影。这可以帮助您优化查询并识别反映您提取数据的最大容量场景的有用索引,并避免未来因关系变化而导致的陷阱,从而导致 Select n+1 延迟加载命中潜入系统。

例如,代替:

var results = context.OpsDocuments
    .Include(x => x.TourClient)
    .ThenInclude(x => x.TourLanguage)
    .Include(x => x.Ship)
    .OrderBy(x => x.Date)
    .ToList();
Run Code Online (Sandbox Code Playgroud)

使用:

var results = context.OpsDocuments
    .Select(x => new TourSummaryViewModel
    {
        DocumentId = x.DocumentId,
        ClientId = x.Client.Id,
        ClientName = x.Client.Name,
        Language = x.Client.Language.Name,
        ShipName = x.Ship.Name,
        Date = x.Date
    }).OrderBy(x => x.Date)
    .ToList();
Run Code Online (Sandbox Code Playgroud)

...视图模型仅反映实体图中所需的详细信息。这可以保护您免受视图/使用者不需要的引入关系的影响(除非您将它们添加到Select),并且如果运行得相当好,生成的查询可以帮助识别有用的索引以提高性能。(根据实际数据库使用而不是猜测调整索引)

我还建议所有像这样的查询对返回的最大行数实施限制器。(使用Take)有助于避免系统老化时出现意外情况,即行数随着时间的推移而增加,从而导致性能随着时间的推移而下降。