简体   繁体   English

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

[英]Entity Framework: Count() very slow on large DbSet and complex WHERE clause

I need to perform a count operation on this Entity Framework (EF6) data set using a relatively complex expression as a WHERE clause and expecting it to return about 100k records.我需要使用相对复杂的表达式作为WHERE子句对此实体框架 (EF6) 数据集执行计数操作,并期望它返回大约 100k 条记录。

The count operation is obviously where records are materialized and therefore the slowest of operations to take place.计数操作显然是记录被物化的地方,因此是最慢的操作。 The count operation is taking about 10 seconds in our production environment which is unacceptable.在我们的生产环境中,计数操作大约需要 10 秒,这是不可接受的。

Note that the operation is performed on the DbSet directly (db being the Context class), so no lazy loading should be taking place.请注意,该操作是直接在 DbSet 上执行的(db 是 Context 类),因此不应发生延迟加载。

How can I further optimize this query in order to speed up the process?如何进一步优化此查询以加快流程?

The main use case is displaying an index page with multiple filter criteria but the the function is also used for writing generic queries to the ParcelOrders table as required for other operations in the service classes which might be a bad idea resulting in very complex queries resulting from laziness and might potentially be a future problem.主要用例是显示具有多个过滤条件的索引页面,但 function 也用于根据服务类中的其他操作的需要将通用查询写入ParcelOrders表,这可能是一个坏主意,导致非常复杂的查询懒惰,可能会成为未来的问题。 The count is later used for pagination, and a much smaller number of records (eg 500) is actually displayed.该计数稍后用于分页,实际显示的记录数(例如 500 条)要少得多。 This is a database-first project using SQL Server.这是一个使用 SQL 服务器的数据库优先项目。

ParcelOrderSearchModel is a C#-class that serves to encapsualte query parameters and is used exclusively by service classes in order to call the GetMatchingOrders function. ParcelOrderSearchModel是一个 C# 类,用于封装查询参数,仅由服务类使用,以便调用GetMatchingOrders function。 Note that on the majority of calls, the majority of the parameters of ParcelOrderSearchModel will be null.请注意,在大多数调用中, ParcelOrderSearchModel的大部分参数将为 null。

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;
}

If you can possibly avoid it, don't count for pagination.如果您可以避免它,请不要计入分页。 Just return the first page.只需返回第一页。 It's always expensive to count and adds little to the user experience.计数总是很昂贵,而且对用户体验的影响很小。

And in any case you're building the dynamic search wrong.无论如何,您构建的动态搜索都是错误的。

You're calling IEnumerable.Count(Func<ParcelOrder,bool>) , which will force client-side evaluation where you should be calling IQueryable.Count(Expression<Func<ParcelOrder,bool>>) .您正在调用IEnumerable.Count(Func<ParcelOrder,bool>) ,这将强制客户端评估您应该调用IQueryable.Count(Expression<Func<ParcelOrder,bool>>) Here:这里:

    Func<ParcelOrder, bool> completeExpression = order => userPrivilegeValidation(order) && criteriaMatchValidation(order);
    searchModel.PaginationTotalCount = db.ParcelOrder.Count(completeExpression);

But there's a simpler, better pattern for this in EF: just conditionally add criteria to your IQueryable.但是在 EF 中有一个更简单、更好的模式:只需有条件地将条件添加到您的 IQueryable。

eg put a method on your DbContext like this:例如,在您的 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;
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM