为什么实体框架在计算总和时会出现性能问题

时间:2019-11-08 11:09:20

标签: c# performance entity-framework linq

我在C#应用程序中使用Entity Framework,并且在使用延迟加载。在计算元素集合中某个属性的总和时遇到性能问题。让我用我的代码的简化版本进行说明:

public decimal GetPortfolioValue(Guid portfolioId) {

    var portfolio = DbContext.Portfolios.FirstOrDefault( x => x.Id.Equals( portfolioId ) );
    if (portfolio == null) return 0m;

    return portfolio.Items
        .Where( i =>
            i.Status == ItemStatus.Listed
            &&
            _activateStatuses.Contains( i.Category.Status )
        )
        .Sum( i => i.Amount );
} 

因此,我想获取我所有具有特定状态且其父项也具有特定状态的商品的价值。

记录由EF生成的查询时,我看到它首先是在获取我的Portfolio(很好)。然后,它将执行查询以加载属于该投资组合的所有Item实体。然后,它开始为每个Category一一读取所有Item实体。因此,如果我有一个包含100个项目的投资组合(每个项目都有一个类别),它实际上会执行100个SELECT ... FROM categories WHERE id = ...查询。

因此,似乎只是在获取所有信息,将其存储在内存中,然后计算总和。为什么它不在我的表之间做简单的联接并像这样计算呢?

我不希望执行102个查询来计算100个项目的总和,而是希望遵循以下原则:

SELECT
    i.id, i.amount 
FROM
    items i 
    INNER JOIN categories c ON c.id = i.category_id
WHERE
    i.portfolio_id = @portfolioId
    AND
    i.status = 'listed'
    AND
    c.status IN ('active', 'pending', ...);

然后可以在其上计算总和(如果它不能直接在查询中使用SUM)。

问题是什么?除了编写纯ADO查询而不是使用Entity Framework之外,如何提高性能?

为完整起见,这是我的EF实体:

public class ItemConfiguration : EntityTypeConfiguration<Item> {
   ToTable("items");
   ...
   HasRequired(p => p.Portfolio);
}

public class CategoryConfiguration : EntityTypeConfiguration<Category> {
    ToTable("categories");
    ...
    HasMany(c => c.Products).WithRequired(p => p.Category);
}

根据评论进行编辑:

我认为这并不重要,但是_activeStatuses是枚举列表。

private CategoryStatus[] _activeStatuses = new[] { CategoryStatus.Active, ... };

但是可能更重要的是,我忽略了数据库中的状态是字符串(“活动”,“待定”,...),但是我将它们映射到应用程序中使用的枚举。这就是为什么EF无法对其进行评估?实际的代码是:

... && _activateStatuses.Contains(CategoryStatusMapper.MapToEnum(i.Category.Status)) ...

EDIT2

实际上,映射是问题的重要组成部分,但查询本身似乎是最大的问题。为什么这两个查询之间的性能差异如此之大?

// Slow query
var portfolio = DbContext.Portfolios.FirstOrDefault(p => p.Id.Equals(portfolioId));
var value = portfolio.Items.Where(i => i.Status == ItemStatusConstants.Listed && 
                _activeStatuses.Contains(i.Category.Status))
                .Select(i => i.Amount).Sum();

// Fast query
var value = DbContext.Portfolios.Where(p => p.Id.Equals(portfolioId))
                .SelectMany(p => p.Items.Where(i => 
                    i.Status == ItemStatusConstants.Listed &&
                    _activeStatuses.Contains(i.Category.Status)))
                    .Select(i => i.Amount).Sum();

第一个查询执行很多小型SQL查询,而第二个查询只是将所有内容组合到一个更大的查询中。我希望甚至第一个查询都可以运行一个查询来获得投资组合的价值。

2 个答案:

答案 0 :(得分:1)

调用portfolio.Items会在Items中延迟加载集合,然后 执行随后的调用,包括WhereSum表达式。另请参见Loading Related Entities article

您需要直接在DbContext表达式上执行调用,Sum表达式可以在数据库服务器端进行评估。

var portfolio = DbContext.Portfolios
    .Where(x => x.Id.Equals(portfolioId))
    .SelectMany(x => x.Items.Where(i => i.Status == ItemStatus.Listed && _activateStatuses.Contains( i.Category.Status )).Select(i => i.Amount))
    .Sum();

您还必须为_activateStatuses实例使用适当的类型,因为所包含的值必须与数据库中保留的类型匹配。如果数据库保留字符串值,那么您需要传递字符串值列表。

var _activateStatuses = new string[] {"Active", "etc"};

您可以使用Linq表达式将枚举转换为它们的字符串代表。


注释

  • 我建议您关闭DbContext类型的延迟加载。一旦这样做,您将开始在运行时通过Exception捕获此类问题,然后可以编写更多性能代码。
  • 如果没有找到投资组合,我没有进行错误检查,但是您可以相应地扩展此代码。

答案 1 :(得分:1)

是的11 Tom 2 Tom 20 Ben 20 Tom 21 Ben 无法转换为SQL,从而迫使其在.Net中运行CategoryStatusMapper.MapToEnum。除了将状态映射到枚举外,Where应该包含枚举中的整数值列表,因此不需要映射。

_activeStatuses

使包含成为

private int[] _activeStatuses = new[] { (int)CategoryStatus.Active, ... };

并且都可以转换为SQL

UPDATE

假设... && _activateStatuses.Contains(i.Category.Status) ... 是数据库中的字符串,然后

i.Category.Status