首页 > 解决方案 > 为什么实体框架在计算总和时会出现性能问题

问题描述

我在 C# 应用程序中使用实体框架,并且正在使用延迟加载。在计算元素集合中的属性总和时,我遇到了性能问题。让我用我的代码的简化版本来说明它:

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 查询而不是使用实体框架之外,还有什么问题可以提高性能?

为了完整起见,这里是我的 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)) ...

编辑2

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

// 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 查询,而第二个查询只是将所有内容组合成一个更大的查询。我希望即使是第一个查询也能运行一个查询来获取投资组合价值。

标签: c#performanceentity-frameworklinq

解决方案


调用portfolio.Items它会延迟加载集合,Items然后执行后续调用,包括WhereandSum表达式。另请参阅加载相关实体文章

您需要直接在可以评估数据库服务器端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 类型的延迟加载。一旦你这样做了,你就会开始在运行时通过异常来捕捉这样的问题,然后可以编写更高性能的代码。
  • 如果没有找到投资组合,我没有包括错误检查,但您可以相应地扩展此代码。

推荐阅读