实体框架 - 计数性能

16
我有一个关于Entity Framework性能的小问题。
类似这样的东西
using (MyContext context = new MyContext())
{
    Document DocObject = context.Document.Find(_id);
    int GroupCount = context.Document.Where(w=>w.Group == DocObject.Group).ToList().Count();
}

在我的数据库中(约30k个数据集),需要大约2秒钟,而这个则需要更长时间。

using (MyContext context = new MyContext())
{
    Document DocObject = context.Document.Find(_id);
    int GroupCount = context.Document.Where(w=>w.Group == DocObject.Group).Count();
}

需要 0.02 秒。

当我筛选出 10 个文件需要等待 20 秒时,我检查了我的代码,并将其更改为在 Count() 之前不使用 ToList()

有任何想法为什么这行代码加上 ToList() 后需要 2 秒钟?

5个回答

27
调用ToList()然后调用Count()会:
  • 对你的数据库执行整个SELECT FROM WHERE
  • 然后将所有结果实体化为.Net对象
  • 创建一个包含所有结果的新List<T>对象
  • 返回刚刚创建的.Net列表的Count属性的结果
针对IQueryable调用Count()会:
  • 对你的数据库执行SELECT COUNT FROM WHERE
  • 返回一个Int32,表示行数
显然,如果你只关心项目的数量(而不是项目本身),那么就不应该首先调用ToList(),因为它会浪费大量资源。

4
是的,ToList()会评估结果(从数据库检索对象),如果您不使用ToList(),则不会从数据库中检索对象。
Linq-To-Entities默认使用延迟加载。
它的工作原理类似于这样; 当您使用Linq-To-Entities查询底层DB连接时,您将获得一个代理对象,您可以在其上执行许多操作(其中之一是计数)。这意味着您不会立即从DB获取所有数据,而是在评估时从DB检索对象。评估对象的一种方法是使用ToList()。
也许您应该阅读此文档

1
这个答案是不正确的。问题与延迟加载或延迟执行无关;两个查询都将被“急切地”执行:第一个由于ToList(),第二个由于Count()区别在于第二个查询被优化为一个SELECT COUNT(*)查询,这样会快得多。 - InBetween
这是语义学问题。我从未声明Count()不会急切地执行,但从Where方法返回的对象将是IQuerable <T>类型的代理对象,而ToList()将执行完整选择并急切地加载整个对象。在两个示例中都执行了Count(),在后者中它在IQueryable <T>上执行,在第一个示例中它在评估的List <T>上执行。性能损失的原因绝对是由于执行了完整的SELECT而不是SELECT COUNT(*),这是使用ToList急切地加载对象的结果。 - Marcus
我不想就这个问题展开讨论。事实是第二种解决方案更快,因为 EF 会将 DB 查询优化为 SELECT COUNT(*) 查询,这一点在你的回答中没有提到。你只是提到了延迟加载、延迟执行和 IQuerable<T>,这与问题无关。在第二个选项中,Count() 也会立即执行查询并加载“对象”。不同之处在于查询已经在查询级别上进行了优化,因此返回一个单一值。 - InBetween
这不是“优化” - 这是一个不同的查询,为什么是不同的查询?因为在第一个示例中,ToList方法将急切地加载整个对象(执行完整的SELECT),而在后者中则不会(对Count()执行SELECT COUNT(*))。 - Marcus
当然这是一种优化,还能是啥?代码不够优化,但框架理解这种常见情况并为你优化查询到 SELECT COUNT (*) ,否则,为了计数对象,它首先必须执行查询,就像使用 ToList() 一样。为了计数,你需要遍历整个数据,否则你怎么做呢? Count() 急切地执行任何查询,就像 ToList() 一样。如果你还不确定,只需使用内存中的对象进行调试即可。 - InBetween
我知道Count()会“急切地执行”,我从未说过其他的。好的,深入核心;你同意通过ToList加载整个对象吗? 你同意ToList().Count()和Count()会产生不同的查询结果吗? 你同意Where()将返回IQueryable而ToList()将返回完整的List吗?现在,请回到我的回复中阅读。 - Marcus

4

因为ToList()会查询整个对象的数据库(可以说是SELECT *),然后您将在内存中使用包含所有记录的列表上的Count(),而如果您在IQueryable上使用Count()(而不是在List上使用),EF将其转换为一个简单的SELECT COUNT(*) SQL查询。


2

您的第一个查询并没有完全转换为SQL——当您调用.ToList().Count()时,您基本上是在说“下载所有内容,将其实体化为POCO并调用名为Count()的扩展方法”,这当然需要一些时间。

然而,您的第二个查询被转换为类似于select count(*) from Documents where GroupId = @DocObjectGroup的东西,这样执行起来要快得多,并且您不需要实体化任何内容,只需简单的标量即可。


1
使用扩展方法Enumerable.ToList()将从IEnumerable<T>源集合构造一个新的List对象,这意味着执行ToList()会带来相关成本。

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