实体框架:Count() 在大型 DbSet 和复杂的 WHERE 子句上非常慢

问题描述

我需要使用相对复杂的表达式作为 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;
}