c# - 为什么实体框架在计算总和时会出现性能问题
问题描述
我在 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 查询,而第二个查询只是将所有内容组合成一个更大的查询。我希望即使是第一个查询也能运行一个查询来获取投资组合价值。
解决方案
调用portfolio.Items
它会延迟加载集合,Items
然后执行后续调用,包括Where
andSum
表达式。另请参阅加载相关实体文章。
您需要直接在可以评估数据库服务器端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 类型的延迟加载。一旦你这样做了,你就会开始在运行时通过异常来捕捉这样的问题,然后可以编写更高性能的代码。
- 如果没有找到投资组合,我没有包括错误检查,但您可以相应地扩展此代码。
推荐阅读
- python - 如何使用 lambda 函数根据其他列分配值,同时对多个值使用“或”以获得相同的结果?
- git - 如何让 GitHub 历史记录显示我从其他人那里克隆的代码的历史记录
- css - 在 CSS 4.0 中替代 ms-flex-preferred-size
- elasticsearch - 如何从现有索引在 ElasticSearch 中创建重复索引?
- python - 如何让 conda list 命令仅打印名称列?
- python - 我如何检查邮件内容是否被引用打印编码?
- reactjs - 使用渲染选项自动完成材料未找到酶中的查找元素具有类名
- html - Bootstrap4 图像未显示在屏幕上
- pandas - 如何在数据框的表格中换行(转换为 .png)
- windows - Jenkins git 身份验证在 Windows 上使用正确的凭据失败