部署到IIS8时,EF 6与EF 5相对性能问题

时间:2014-03-17 18:31:03

标签: c# sql-server performance linq entity-framework

我有一个使用EF 6的MVC 4应用程序。从EF 5升级到EF 6后,我注意到我的一个linq-entities查询存在性能问题。起初我很兴奋,因为在我的开发盒中,我注意到从EF 5到EF 6的改进了50%。这个查询返回了大约73,000条记录。使用Activity Monitor,Recent Expensive Queries拦截在生产服务器上运行的SQL,此时间也包含在下表中。一旦DB被预热,以下数字:

开发:64位操作系统,SS 2012,2核,6 GB RAM,IIS Express。

EF 5 ~30 sec
EF 6 ~15 sec
SQL ~26 sec

生产:64位操作系统,SS 2012,32内核,32 GB内存,IIS8。

EF 5 ~8 sec
EF 6 ~4 minutes
SQL ~6 sec.

我已经包含了这些规范,只是为了了解相对性能应该是什么。因此,当我在我的开发环境中使用EF 6时,当我向生产服务器发布一个巨大的性能问题时,我得到了性能提升。如果不完全相同,数据库是相似的。所有索引都已重建,SQL查询似乎也表明没有理由怀疑数据库是否有问题。应用程序池是.Net 4.0正在生产中。开发和生产服务器都安装了.Net 4.5。我不知道下一步要检查什么,或者如何调试这个问题,关于做什么或如何进一步调试的任何想法?

更新 使用SQL Server Profiler发现EF5和EF6产生的TSQL略有不同。 TSQL差异如下:

EF5: LEFT OUTER JOIN [dbo].[Pins] AS [Extent9] ON [Extent1].[PinId] = [Extent9].[PinID]
EF6: INNER JOIN [dbo].[Pins] AS [Extent9] ON [Extent1].[PinId] = [Extent9].[PinID]

来自EF6的相同TSQL也会执行不同的操作,具体取决于执行TSQL的服务器/数据库。检查EF6& amp;的查询计划后慢速数据库(生产服务器SS build 11.0.3000企业版)与相同的实例(测试服务器SS build 11.0.3128开发人员版)相比,此计划执行所有扫描并且没有搜索,其中有一些搜索可以产生差异。挂钟时间>生产4分钟,小型测试服务器12秒。 EF将这些查询放入sp_executesql proc,截获的sp_executesql proc用于上述时序。在开发服务器上执行时,我不会使用EF5或EF6生成的代码获得慢时间(错误的查询计划)。同样奇怪的是,如果我从sp_executesql中删除TSQL并在生产服务器上运行它,则查询会很快执行(6秒)。总之,对于缓慢的执行计划,需要做三件事:

1. Execute on production server build 11.0.3000
2. Use Inner Join with Pins table (EF6 generated code).
3. Execute TSQL inside of sp_executesql.

测试环境是使用我的生产数据备份创建的,两台服务器上的数据完全相同。可以创建备份和恢复数据库修复了数据的一些问题吗?我还没有尝试删除实例并在生产服务器上进行恢复,因为我想知道在删除和恢复实例之前问题是什么,以防万一它确实解决了问题。我尝试使用以下TSQL刷新缓存

select DB_ID() 
DBCC Flushprocindb(database_Id)
and 
DBCC FREEPROCCACHE(plan_handle)

使用上面的刷新不会影响查询计划。有什么建议接下来要尝试什么?

以下是linq查询:

    result =
    (
    from p1 in context.CookSales

    join l2 in context.CookSaleStatus on new { ID = p1.PinId, YEAR = year1 } equals new { ID = l2.PinId, YEAR = l2.StatusYear } into list2
    from p3 in list2.DefaultIfEmpty()
    join l3 in context.CookSaleStatus on new { ID = p1.PinId, YEAR = year2 } equals new { ID = l3.PinId, YEAR = l3.StatusYear } into list3
    from p4 in list3.DefaultIfEmpty()
    join l4 in context.CookSaleStatus on new { ID = p1.PinId, YEAR = year3 } equals new { ID = l4.PinId, YEAR = l4.StatusYear } into list4
    from p5 in list4.DefaultIfEmpty()
    join l10 in context.CookSaleStatus on new { ID = p1.PinId, YEAR = year4 } equals new { ID = l10.PinId, YEAR = l10.StatusYear } into list10
    from p11 in list10.DefaultIfEmpty()

    join l5 in context.ILCookAssessors on p1.PinId equals l5.PinID into list5
    from p6 in list5.DefaultIfEmpty()
    join l7 in context.ILCookPropertyTaxes on new { ID = p1.PinId } equals new { ID = l7.PinID } into list7
    from p8 in list7.DefaultIfEmpty()

    join l13 in context.WatchLists on p1.PinId equals l13.PinId into list13
    from p14 in list13.DefaultIfEmpty()

    join l14 in context.Pins on p1.PinId equals l14.PinID into list14
    from p15 in list14.DefaultIfEmpty()
    orderby p1.Volume, p1.PIN
    where p1.SaleYear == userSettings.SaleYear 
    where ((p1.PinId == pinId) || (pinId == null))
    select new SaleView
    {
        id = p1.id,
        PinId = p1.PinId,
        Paid = p1.Paid == "P" ? "Paid" : p1.Paid,
        Volume = p1.Volume,
        PinText = p15.PinText,
        PinTextF = p15.PinTextF,
        ImageFile = p15.FnaImage.TaxBodyImageFile,
        SaleYear = p1.SaleYear,
        YearForSale = p1.YearForSale,
        Unpaid = p1.DelinquentAmount,
        Taxes = p1.TotalTaxAmount,
        TroubleTicket = p1.TroubleTicket,
        Tag1 = p1.Tag1,
        Tag2 = p1.Tag2,
        HasBuildingPermit = p1.Pin1.BuildingPermitGeos.Any(p => p.PinId == p1.PinId),
        BidRate = p1.BidRate,
        WinningBid = p1.WinningBid,
        WinningBidderNumber = p1.BidderNumber,
        WinningBidderName = p1.BidderName,
        TaxpayerName = p1.TaxpayerName,
        PropertyAddress = SqlFunctions.StringConvert((double?)p1.TaxpayerPropertyHouse) + " " + p1.TaxpayerPropertyDirection + " "
                        + p1.TaxpayerPropertyStreet
                        + " " + p1.TaxpayerPropertySuffix +
                        System.Environment.NewLine + (p1.TaxpayerPropertyCity ?? "") + ", " + (p1.TaxpayerPropertyState ?? "") +
                        " " + (p1.TaxpayerPropertyZip ?? ""),
        MailingAddress = (p1.TaxpayerName ?? "") + System.Environment.NewLine + (p1.TaxpayerMailingAddress ?? "") +
                        System.Environment.NewLine + (p1.TaxpayerMailingCity ?? "") + ", " + (p1.TaxpayerMailingState ?? "") +
                        " " + (p1.TaxpayerMailingZip ?? ""),
        Status1 = p3.Status.Equals("Clear") ? null : p3.Status,
        Status2 = p4.Status.Equals("Clear") ? null : p4.Status,
        Status3 = p5.Status.Equals("Clear") ? null : p5.Status,
        Status4 = p11.Status.Equals("Clear") ? null : p11.Status,
        Township = p6.Township,
        AssessorLastUpdate = p6.LastUpdate,
        Age = p6.Age,
        LandSquareFootage = p6.LandSquareFootage,
        BuildingSquareFootage = p6.BuildingSquareFootage,
        CurrLand = p6.CurrLand,
        CurrBldg = p6.CurrBldg,
        CurrTotal = p6.CurrTotal,
        PriorLand = p6.PriorLand,
        PriorBldg = p6.PriorBldg,
        PriorTotal = p6.PriorTotal,
        ClassDescription = p6.ClassDescription,
        Class = p1.Classification == null ? p6.Class.Trim() : p1.Classification,
        TaxCode = p6.TaxCode,
        Usage = p6.Usage,

        Status0 = (p8.CurrentTaxYear != null && p8.CurrentTaxYearPaidAmount == 0) ? "Paid" : null, 
        LastTaxYearPaidAmount = p8.LastTaxYearPaidAmount,
        NoteStatus = p15.PinNotes.Any(p => p.PinId == p15.PinID),
        EntryComment = p1.EntryComment,
        IsInScavenger = p14.IsInScavenger ?? false,
        IsInTbs = p14.IsInTbs ?? false,
        RedeemVts = (p3.Redeemed == "VTS" || p4.Redeemed == "VTS" || p5.Redeemed == "VTS" || p11.Redeemed == "VTS") ? true : false,
        FivePercenter = (p3.FivePercenter || p4.FivePercenter || p5.FivePercenter || p11.FivePercenter) ? true : false,
    }
    ).ToList();

使用此查询生成的SQL似乎是合理的。 (我没有把它包括在内,因为当我将它粘贴在其中时,它没有格式化并且难以阅读。)

1 个答案:

答案 0 :(得分:9)

在研究这个问题时,我发现了一些我不知道的关于SQL Server的东西。对于某些人来说,这可能是常识,但对我而言,这不是。以下是我的整体亮点。

  1. EF对所有查询使用动态sql,特别是sp_exectutesql()。 sp_executesql()执行动态SQL,如果删除此SQL并在SSMS中作为adhoc查询执行,则不希望获得相同的性能结果。如果您遇到这些类型的问题,我会强烈建议您阅读here和参考文件this
  2. 在某些条件下,EF5会产生与EF6不同的动态SQL。
  3. 将linq优化为实体很困难,因为根据硬件可能会得到不同的结果,这在参考文献中进行了解释。我最初的目标是在升级到EF6时优化linq查询。我注意到使用导航属性改进了我的开发和测试服务器的性能,但在生产中将其杀死。
  4. 在所有环境中,具有可接受性能的最终结果是连接和导航属性的组合。最后如果我使用了所有导航属性,它从一开始就会更好用。使用的连接键来自错误的表,当您编写即席SQL时,它并不重要但它必须用于动态SQL。如果我使用导航,就不会有任何错误的关键。但是,最佳性能是使用一个连接和其余的导航属性。生成的动态SQL对于所有场景都非常相似,但是当使用导航属性时,SQL Server查询计划优化器会获得更好的线索(这是猜测)。
  5. linq的关键部分改变了:

                    from p1 in context.CookSales
                    join p15 in context.Pins on p1.PinId equals p15.PinID
                    where p1.SaleYear == userSettings.SaleYear
                    where ((p1.PinId == pinId) || (pinId == null))
                    orderby p1.Volume, p1.PIN
                    select new SaleView bla bla
    

    Pins表包含PinId的主键,而所有其他表都包含PinId作为外键。将引脚保持为连接而不是导航属性可以提高性能。