实体框架:Count() 在大型 DbSet 和复杂的 WHERE 子句上非常慢
Posted
技术标签:
【中文标题】实体框架:Count() 在大型 DbSet 和复杂的 WHERE 子句上非常慢【英文标题】:Entity Framework: Count() very slow on large DbSet and complex WHERE clause 【发布时间】:2021-05-17 09:06:29 【问题描述】:我需要使用相对复杂的表达式作为WHERE
子句对此实体框架 (EF6) 数据集执行计数操作,并期望它返回大约 100k 条记录。
计数操作显然是记录物化的地方,因此是最慢的操作。在我们的生产环境中,计数操作大约需要 10 秒,这是不可接受的。
请注意,操作是直接在 DbSet 上执行的(db 是 Context 类),因此不应发生延迟加载。
如何进一步优化此查询以加快处理速度?
主要用例是显示具有多个过滤条件的索引页面,但该函数还用于根据服务类中的其他操作的需要向ParcelOrders
table 写入通用查询,这可能是一个坏主意,导致由懒惰导致的非常复杂的查询,可能会成为未来的问题。
该计数稍后用于分页,实际显示的记录数(例如 500 条)要少得多。这是一个使用 SQL Server 的数据库优先项目。
ParcelOrderSearchModel
是一个 C# 类,用于封装查询参数,并由服务类专门用于调用 GetMatchingOrders
函数。
请注意,在大多数调用中,ParcelOrderSearchModel
的大部分参数将为空。
public List<ParcelOrderDto> GetMatchingOrders(ParcelOrderSearchModel searchModel)
// cryptic id known --> allow public access without login
if (String.IsNullOrEmpty(searchModel.KeyApplicationUserId) && searchModel.ExactKey_CrypticID == null)
throw new UnableToCheckPrivilegesException();
Func<ParcelOrder, bool> userPrivilegeValidation = (x => false);
if (searchModel.ExactKey_CrypticID != null)
userPrivilegeValidation = (x => true);
else if (searchModel.KeyApplicationUserId != null)
userPrivilegeValidation = privilegeService.UserPrivilegeValdationExpression(searchModel.KeyApplicationUserId);
var criteriaMatchValidation = CriteriaMatchValidationExpression(searchModel);
var parcelOrdersWithNoteHistoryPoints = db.HistoryPoint.Where(hp => hp.Type == (int)HistoryPointType.Note)
.Select(hp => hp.ParcelOrderID)
.Distinct();
Func<ParcelOrder, bool> completeExpression = order => userPrivilegeValidation(order) && criteriaMatchValidation(order);
searchModel.PaginationTotalCount = db.ParcelOrder.Count(completeExpression);
// todo: use this count for pagination
public Func<ParcelOrder, bool> CriteriaMatchValidationExpression(ParcelOrderSearchModel searchModel)
Func<ParcelOrder, bool> expression =
po => po.ID == 1;
expression =
po =>
(searchModel.KeyUploadID == null || po.UploadID == searchModel.KeyUploadID)
&& (searchModel.KeyCustomerID == null || po.CustomerID == searchModel.KeyCustomerID)
&& (searchModel.KeyContainingVendorProvidedId == null || (po.VendorProvidedID != null && searchModel.KeyContainingVendorProvidedId.Contains(po.VendorProvidedID)))
&& (searchModel.ExactKeyReferenceNumber == null || (po.CustomerID + "-" + po.ReferenceNumber) == searchModel.ExactKeyReferenceNumber)
&& (searchModel.ExactKey_CrypticID == null || po.CrypticID == searchModel.ExactKey_CrypticID)
&& (searchModel.ContainsKey_ReferenceNumber == null || (po.CustomerID + "-" + po.ReferenceNumber).Contains(searchModel.ContainsKey_ReferenceNumber))
&& (searchModel.OrKey_Referencenumber_ConsignmentID == null ||
((po.CustomerID + "-" + po.ReferenceNumber).Contains(searchModel.OrKey_Referencenumber_ConsignmentID)
|| (po.VendorProvidedID != null && po.VendorProvidedID.Contains(searchModel.OrKey_Referencenumber_ConsignmentID))))
&& (searchModel.KeyClientName == null || po.Parcel.Name.ToUpper().Contains(searchModel.KeyClientName.ToUpper()))
&& (searchModel.KeyCountries == null || searchModel.KeyCountries.Contains(po.Parcel.City.Country))
&& (searchModel.KeyOrderStates == null || searchModel.KeyOrderStates.Contains(po.State.Value))
&& (searchModel.KeyFromDateRegisteredToOTS == null || po.DateRegisteredToOTS > searchModel.KeyFromDateRegisteredToOTS)
&& (searchModel.KeyToDateRegisteredToOTS == null || po.DateRegisteredToOTS < searchModel.KeyToDateRegisteredToOTS)
&& (searchModel.KeyFromDateDeliveredToVendor == null || po.DateRegisteredToVendor > searchModel.KeyFromDateDeliveredToVendor)
&& (searchModel.KeyToDateDeliveredToVendor == null || po.DateRegisteredToVendor < searchModel.KeyToDateDeliveredToVendor);
return expression;
public Func<ParcelOrder, bool> UserPrivilegeValdationExpression(string userId)
var roles = GetRolesForUser(userId);
Func<ParcelOrder, bool> expression =
po => po.ID == 1;
if (roles != null)
if (roles.Contains("ParcelAdministrator"))
expression =
po => true;
else if (roles.Contains("RegionalAdministrator"))
var user = db.AspNetUsers.First(u => u.Id == userId);
if (user.RegionalAdministrator != null)
expression =
po => po.HubID == user.RegionalAdministrator.HubID;
else if (roles.Contains("Customer"))
var customerID = db.AspNetUsers.First(u => u.Id == userId).CustomerID;
expression =
po => po.CustomerID == customerID;
else
expression =
po => false;
return expression;
【问题讨论】:
.Count
应该一直到数据库,因此您需要优化它。鉴于如此复杂的谓词(不是真正可索引的,除非您能找到一个好的过滤列,因此无论您做什么都会很慢)和如此大量的行,我首先会质疑是否需要计数,特别是如果你正在分页。但是请务必通过pastetheplan.com 分享查询计划,也许我们可以做点什么
请注意,这在技术上不是关于 EF 的讨论,EF 只是让您更容易表达复杂的查询要求。这个问题有其他解决方案,例如编写优化的 SQL 查询以返回结果(绕过 EF)、使用外部 NO-SQL 索引服务(类似于 Azure 搜索服务)、物化视图(或您自己的编码索引)。 Azure 搜索在这方面非常出色,但如果您可以将内容重新构造成一种在数据更改时简化搜索过程的表单,您可以获得更好的性能结果。
重申@Charlieface 知道总计数对于分页本身并不是那么有用,您真正需要的是第一个或当前页面结果。最坏的情况,异步加载总数......这种类型的数据场景正是 Azure 搜索等外部服务旨在为您解决的问题。
ParcelOrderSearchModel
是表、查询、函数还是视图?
ParcelOrderSearchModel
根本不是数据库实体。它是一个用于封装查询参数的 C# 类,服务类专门使用它来调用 GetMatchingOrders
函数。需要计数以计算可用页数,因为客户端需要在输出中显示的记录总数。
【参考方案1】:
如果您可以避免它,请不要计入分页。只需返回第一页。计数总是很昂贵,而且对用户体验的影响很小。
无论如何,您构建的动态搜索都是错误的。
您正在调用IEnumerable.Count(Func<ParcelOrder,bool>)
,这将在您应该调用IQueryable.Count(Expression<Func<ParcelOrder,bool>>)
的地方强制进行客户端评估。这里:
Func<ParcelOrder, bool> completeExpression = order => userPrivilegeValidation(order) && criteriaMatchValidation(order);
searchModel.PaginationTotalCount = db.ParcelOrder.Count(completeExpression);
但在 EF 中有一个更简单、更好的模式:只需有条件地将条件添加到您的 IQueryable。
例如,像这样在 DbContext 上放置一个方法:
public IQueryable<ParcelOrder> SearchParcels(ParcelOrderSearchModel searchModel)
var q = this.ParcelOrders();
if (searchModel.KeyUploadID != null)
q = q.Where( po => po.UploadID == searchModel.KeyUploadID );
if (searchModel.KeyCustomerID != null)
q = q.Where( po.CustomerID == searchModel.KeyCustomerID );
//. . .
return q;
【讨论】:
修复这个明显的错误(在IQueryable
而不是ÌEnumerable
上调用Count()
)速度提高了十倍以上,为我们解决了问题。
有条件地设置条件,考虑到条件一般是独占的,可以节省很多查询。这意味着会有不同的缓存查询计划,但在这种情况下通常是可以的。针对不同类型查找的单独计划将看到更简单的长期维护以上是关于实体框架:Count() 在大型 DbSet 和复杂的 WHERE 子句上非常慢的主要内容,如果未能解决你的问题,请参考以下文章