问题描述
我需要使用相对复杂的表达式作为 WHERE
子句对这个实体框架 (EF6) 数据集执行计数操作,并期望它返回大约 10 万条记录。
计数操作显然是记录物化的地方,因此是最慢的操作。在我们的生产环境中,计数操作需要大约 10 秒,这是不可接受的。
请注意,操作是直接在 DbSet 上执行的(db 是 Context 类),因此不应发生延迟加载。
如何进一步优化此查询以加快流程?
主要用例是显示具有多个过滤条件的索引页面,但该函数还用于将通用查询写入 ParcelOrders
表,这是服务类中其他操作所需的,这可能是一个坏主意导致由于懒惰而导致非常复杂的查询,并且可能成为未来的问题。
计数稍后用于分页,实际显示的记录数量要少得多(例如 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;
}
解决方法
如果您可以避免它,请不要计入分页。只需返回第一页。计算总是很昂贵,而且对用户体验几乎没有任何帮助。
无论如何,您构建的动态搜索都是错误的。
您正在调用 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;
}