实体框架: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 类),因此不应发生延迟加载。

如何进一步优化此查询以加快处理速度?

主要用例是显示具有多个过滤条件的索引页面,但该函数还用于根据服务类中的其他操作的需要向ParcelOrderstable 写入通用查询,这可能是一个坏主意,导致由懒惰导致的非常复杂的查询,可能会成为未来的问题。 该计数稍后用于分页,实际显示的记录数(例如 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&lt;ParcelOrder,bool&gt;),这将在您应该调用IQueryable.Count(Expression&lt;Func&lt;ParcelOrder,bool&gt;&gt;) 的地方强制进行客户端评估。这里:

    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 子句上非常慢的主要内容,如果未能解决你的问题,请参考以下文章

实体框架和 DbSet

在实体框架中,DbSet.Local 保持不同步

实体框架和DbSet

我可以在选择时让我的实体框架DbSet调用我的表值函数吗?

FakeItEasy DbSet / IQueryable<T> - 实体框架 6

IDENTITY_INSERT ON不被实体框架DBSet.Add方法所尊重