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

Posted

技术标签:

【中文标题】与 SSMS 相比,实体框架中的查询时间极慢【英文标题】:Extremely slow query times in Entity Framework compared to SSMS 【发布时间】:2021-11-13 12:44:57 【问题描述】:

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

Entity Framework 正在生成以下查询(在 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

此查询从可能的 55 行中返回 10 行,所以没有说大数字或其他任何内容。

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

希望有人可以建议在哪里看。

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

【问题讨论】:

TL;DR 可能是参数嗅探。见Slow in the Application, Fast in SSMS?。 您可以使用您的查询创建一个存储过程并从 EF 调用它,它会快得多。 更好地显示 LINQ 查询。 或者问题出在连接...或者您的 linq 查询在 sql 中做不到...我们需要您的 linq 查询,就像 Svyatoslav 说的... 通常的疑点:延迟加载(n+1 加载)。再次:显示 LINQ 查询 + 类模型。现在不可能再说什么更有帮助的了。 【参考方案1】:

这里的主要问题是您已声明此“查询”在 EF 中花费超过 30 秒,在 SSMS 中花费不到 1 秒,但您没有提供的是 EF 编译执行的 SQL

您要求我们将 苹果 与橙子的 idea 进行比较... 我们确实需要至少查看已编译的 SQL,但 C#/Linq 代码也会有所帮助。它不必编译,但它会展示您在其中操作的一些上下文。

tldr

这不太可能与 EF 本身有关,而与您正在执行的代码中的模式和您的特定查询有关。 对于这样一个小而简单的查询,根本不应该使用延迟加载,之后我们谈论的关于 EF 性能的通常怀疑对于这个小数据集也不应该显着衡量。 从所提供的少量信息中我们只能说您的 EF 查询与您预期的 SQL 不匹配,因此我们应该从那里开始并确保您的 EF 查询正在编译您所使用的查询的合理近似值期待。

如果一切都失败了,只需使用Raw SQL Queries 并继续。

虽然使用像 EF 这样的 ORM 确实存在一些开销,但对于像这样的简单查询,我们应该讨论几 毫秒,但其他任何事情都表明您的 EF Linq 查询要么是错误的,要么是写得很糟糕。

如果您使用延迟加载,请注意哪些代码行会导致来自服务器的新查询,而不是使用内存中的数据。延迟加载可能很强大,但在少数情况下它是有意义的。使用投影是一个不错的选择,但您应该考虑完全禁用延迟加载并切换到急切加载始终。如果您不确定,请尝试禁用数据上下文的延迟加载功能,您会很快发现您的代码是否依赖于延迟功能,因为它可能会在运行时失败。

如果只有一个执行点,那么您应该能够捕获原始 SQL为往返时间计时。发布您用来计时执行的代码,原始的SQL 和时间请。

如果单个执行点需要 30 秒才能加载,则可能存在 冷启动 问题,即您可能在查询之前执行了一些进程,但对您的框架没有更多了解,这很容易调试的示例是首先通过一个简单的调用来启动数据库连接,以返回所有 OpsDocuments 记录的计数,然后执行您的查询。

列过多或奇怪的数据类型比较等其他性能问题在这里并不适用。您可以肯定地优化此查询,但如果使用 10 行和少于 50 列,即使是非常慢的 PC 也应该能够在几毫秒内将此结果读入 EF 图。

如果您已经消除了延迟加载,并且您捕获的由 EF 生成的 SQL 查询在 SSMS 中执行时闪电般,但在应用程序运行时却非常慢,那么锁定“可能”是一个问题.

验证锁定是否存在问题的一种简单方法是在应用程序等待响应时查询数据库中当前正在执行的查询,如果等待时间确实为 30 秒,那么您将有足够的时间在等待时在 SSMS 中执行以下操作。

作为奖励,这将证明查询是否正在运行

Declare @Identifier Char(1) = '~'
SELECT r.session_id, r.status,
       st.TEXT AS batch_text,
       qp.query_plan AS 'XML Plan',
       r.start_time,
       r.status,
       r.total_elapsed_time, r.blocking_session_id, r.wait_type, r.wait_time, r.open_transaction_count, r.open_resultset_count
FROM sys.dm_exec_requests AS r
     CROSS APPLY sys.dm_exec_sql_text(r.sql_handle) AS st
     CROSS APPLY sys.dm_exec_query_plan(r.plan_handle) AS qp
WHERE st.TEXT NOT LIKE 'Declare @Identifier Char(1) = ''~''%'
ORDER BY cpu_time DESC;

【讨论】:

【参考方案2】:

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

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

在一些看似无关的更改之后,这可能突然需要更长的时间:新关系被延迟加载。 这方面的一个例子是这个数据被序列化并且一个新的关系被添加到一个或多个现在被延迟加载命中绊倒的实体。 (通常可以通过在页面加载前运行后看到额外的查询来证明)

导致此问题花费的时间超出预期的其他原因:

    DbContext 跟踪的实体过多。 DbContext 跟踪的实体越多,在拼凑来自 Linq 查询的结果时它必须经过的引用就越多。一些团队希望 EF 缓存类似于 NHibernate 的实例,这将提高性能。通常情况正好相反,它跟踪的实体越多,获得结果所需的时间就越长。 并发读取和锁定。如果表没有被有效地索引,与测试/调试相比,当系统在生产中运行时,这可能是一个杀手。虽然这通常会影响行数和/或用户数非常大的系统。

在解决 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();

使用:

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();

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

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

【讨论】:

以上是关于与 SSMS 相比,实体框架中的查询时间极慢的主要内容,如果未能解决你的问题,请参考以下文章

.net 实体框架与 LinqToSql 相比如何?

改善使用实体框架时的搜索功能延迟

首先将可更新视图与实体框架代码一起使用

实体框架查询——加载数据优化

实体框架 + LINQ 缓慢与字符串查询速度?

从一个 id 更新实体框架中的多行