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

2022-01-30 15:01:50 标签 c#sql-serverentity-framework-core

我继承了一个代码库,我有一个奇怪的问题与实体框架核心v3。1。19。

实体框架生成以下查询(SQL Server分析器中)和它的近30秒运行相同的代码运行时(同样来自分析器)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

这个查询返回10行从一个可能的55,所以我们不谈论大的数字或任何东西。

起初,我认为这可能是数据类型问题的转换,但检查所有的数据类型,他们都是正确的,因为问题是显示在profiler,我假设这是一个SQL问题,而不是具体的实体框架。然而,在运行分析器时,我没有发现两者之间的任何区别,除了EF的那个花了30倍的时间。

希望有人能告诉我们去哪里找。

编辑:感谢评论中所有的建议。至于Linq和可复制的例子,它将是棘手的,因为这个项目的代码基是一些奇怪的自制自动生成系统。你给它一个带有大量自定义属性的ViewModel,它试图为你做所有的事情(这么多抽象层),所以它很难找到任何东西。

听起来我得把这些重写成更有限的控制器了。

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

看看SQL,这是一个跨4个表的急加载查询OPSDocuments TourClients TourLanguages and 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)来帮助避免系统老化时出现的意外,因为行数会随着时间增长而导致性能下降。

###这里的主要问题是,你已经声明这个“查询”在EF中花费了超过30秒,在SSMS中少于1秒,但你没有提供的是EF已编译执行的SQL

你是在让我们把苹果和橙子的概念相比较……

我们确实需要将编译后的SQL作为最小值,但c# / Linq代码也会有所帮助。它不需要编译,但它将演示您正在操作的一些上下文。

tldr

这与EF本身的关系不大,更多的是与您正在执行的代码中的模式和特定的查询有关。

对于这样一个小而简单的查询,延迟加载不应该在任何情况下使用,因为我们讨论的EF性能在这个小数据集上也不应该被显著衡量。我们只能说,从提供的小信息是,您的EF查询不匹配您预期的SQL,所以我们应该从那里开始,并确保您的EF查询是编译一个合理的近似查询,你所期望的。

如果所有这些都失败了,只需使用原始SQL查询并继续。

虽然使用像EF这样的ORM和一个简单的查询会有一些固有的开销,但我们应该讨论几毫秒的时间,其他任何事情都表明你的EF Linq查询要么是错误的,要么写得很糟糕。

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

如果只有一个执行点,那么您应该能够捕获原始SQL并对往返进行计时。

请发布您用来计时执行原始SQL和时间的代码。

如果一次执行点需要30秒加载之后可能会有一个冷启动的问题,你可能有一些流程执行查询泵前了解更多有关你的框架一个简单的例子来调试启动数据库连接首先用一个简单的调用返回所有OpsDocuments记录的计数执行查询。

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

如果你已经消除了惰性加载,你捕获的SQL查询生成的EF在SSMS执行时闪电般的快,但从应用程序运行时非常慢,那么锁定“可能”是一个问题。

一个简单的方法来验证如果锁是一个问题是为当前查询数据库执行查询在您的应用程序是等待响应等待时间是否真的30秒你就会有足够的时间来执行以下在ssm当你等待。

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

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;
阅读全文

▼ 版权说明

相关文章也很精彩
推荐内容
更多标签
相关热门
全站排行
随便看看

错说 cuoshuo.com —— 程序员的报错记录

部分内容根据CC版权协议转载;网站内容仅供参考,生产环境使用务必查阅官方文档

辽ICP备19011660号-5

×

扫码关注公众号:职场神器
发送: 1
获取永久解锁本站全部文章的验证码