我在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查询,而第二个查询只是将所有内容组合到一个更大的查询中。我希望甚至第一个查询都可以运行一个查询来获得投资组合的价值。
答案 0 :(得分:1)
调用portfolio.Items
会在Items
中延迟加载集合,然后 执行随后的调用,包括Where
和Sum
表达式。另请参见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表达式将枚举转换为它们的字符串代表。
答案 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