我有一个使用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似乎是合理的。 (我没有把它包括在内,因为当我将它粘贴在其中时,它没有格式化并且难以阅读。)
答案 0 :(得分:9)
在研究这个问题时,我发现了一些我不知道的关于SQL Server的东西。对于某些人来说,这可能是常识,但对我而言,这不是。以下是我的整体亮点。
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作为外键。将引脚保持为连接而不是导航属性可以提高性能。