Entity Framework Sum() 性能问题

3

我在使用以下实体框架查询时遇到性能问题:

using (MyEntities context = new MyEntities())
{
    return context.Companies
                  .Single(c => c.CompanyId == company.CompanyId)
                  .DataFile.Sum(d => d.FileSize);
}

在SQL profiler中进行跟踪时,我看到以下SQL命令:
exec sp_executesql N'SELECT 
[Extent1].[DataFileID] AS [DataFileID], 
[Extent1].[LocalFileName] AS [LocalFileName], 
[Extent1].[ServerFileName] AS [ServerFileName], 
[Extent1].[DateUploaded] AS [DateUploaded], 
[Extent1].[FileSize] AS [FileSize], 
[Extent1].[CompanyID] AS [CompanyID]
FROM [dbo].[DataFile] AS [Extent1]
WHERE [Extent1].[CompanyID] = @EntityKeyValue1',N'@EntityKeyValue1 int',@EntityKeyValue1=16

据我所见,所有数据文件行(超过10,000行)都被返回到内存中,然后进行Sum()计算。

编辑:

根据Patryk的建议,我已将查询更改为以下内容:

using (MyEntities context = new MyEntities())
{
    return context.Companies
                  .Where(c => c.CompanyId == company.CompanyId)
                  .Select(x => x.DataFiles.Sum(d => d.FileSize))
                  .Single();
}

SQL跟踪看起来像这样:

SELECT TOP (2) 
(
    SELECT 
        SUM([Extent2].[FileSize]) AS [A1]
    FROM 
        [dbo].[DataFile] AS [Extent2]
    WHERE 
        [Extent1].[CompanyId] = [Extent2].[CompanyID]
) AS [C1]
FROM 
    [dbo].[Company] AS [Extent1]
WHERE 
    [Extent1].[CompanyId] = 16

这已经好多了,但实际上我只想要像这样简单快速的东西:
SELECT SUM(FileSize) FROM DataFile WHERE CompanyId = 16

也许稍微改变一下查询语句,使其看起来更像SQL会有帮助:context.Companies.Where(c => ...).Select(x => x.DataFile.Sum(d => d.FileSize)).Single()。这样表达式访问器可能会更容易处理,生成的SQL也可能更好;如果没有帮助,您可以在Select之后手动调用AsEnumerable,看看是否有所改变。 - Patryk Ćwiek
@PatrykĆwiek 优化正确,但是推理不合理。问题归结为懒加载。 - Aron
如果你想要“确切的查询”,那么只需使用context.Companies.SqlQuery("...") - 我怀疑这两个查询之间的性能差异并不显著。 - D Stanley
在这种特定情况下,是的,在调用“Single”之后,我们会收到一个惰性加载的集合。往往情况下,当您将LINQ查询重构为更类似于SQL的形式时,可以避免许多问题 - 我们不能忘记它在某个时候会被转换为SQL。 - Patryk Ćwiek
在我看来,这里的问题在于微软。他们有一百万种方法可以使.Single()/产生一个懒惰且Linq感知的返回,但他们没有这样做。 - Aron
2个回答

5
首先...Entity Framework可能已经有所改进,因为根据所有帐户,表达式.Single(c => c.CompanyId == company.CompanyId)应该失败,因为Entity Framework应该在Expression.Constant<Company>上失败。我怀疑您实际上已经混淆了代码列表。
这个问题出现的原因是由于.Single(Expression)的工作方式。与大多数Linq IQueryable<T>扩展方法不同,它立即评估。
using (MyEntities context = new MyEntities())
{
    return context.Companies
                  .Single(c => c.CompanyId == company.CompanyId)
                  .DataFile.Sum(d => d.FileSize);
}

等同于

using (MyEntities context = new MyEntities())
{
    Company company = context.Companies.Single(c => c.CompanyId == company.CompanyId);
    List<DataFile> dataFiles = company.DataFile
    return dataFiles.Sum(d => d.FileSize);
}

为了更好地解释,这里有几点原因导致性能糟糕。
首先,.Single() 强制执行查询,返回你需要的 Company(虽然你不需要),如果幸运的话,EF 可能会聪明地从缓存中拉取数据。
其次,第二行检索了该公司的所有 DataFiles(因为 List<T> 没有任何 Entity Framework 代码。这意味着它必须拉取整个列表。
然后第三部分执行 .Sum()。但是,如果你查看实际的 .Sum() 实现,它实际上是 IEnumerable.Sum(),与 Entity Framework 没有任何关系。签名完全不同。
适用于 ELinq 的方法是 IQueryable<T>.Sum<T,TValue>(Expression<Func<T,TValue>> projection),而 Linq to Object 的方法是 IEnumerable<T>.Sum<T,TValue>(Func<T.TValue> projection) 总结一下:
简而言之,想要理解 LinqToEF 的起点和终点需要一些时间。你代码能够正常工作的唯一原因是 EF 惰性加载。但我建议在出现性能问题时关闭 EF 惰性加载,因为它经常隐藏了对 Linq 的误解。

0

选择语句实际上应该只返回查询定义的内容,

select * from Extend1 where CompanyID = 16

意思是,它应该只返回CompanyID = 16的所有行。

说实话,我不知道Entity Framework的行为如何,但是如果你使用nhibernate进行查询,例如使用linq查询.First(p=>p.Id==16)将执行Select Top(1)。

也许这篇文章可以帮助您优化生成的查询:强制实体框架使用SQL参数化以获得更好的SQL proc缓存重用


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接