我是否误解了LINQ to SQL中的.AsEnumerable()?

66

考虑以下代码:

var query = db.Table
              .Where(t => SomeCondition(t))
              .AsEnumerable();

int recordCount = query.Count();
int totalSomeNumber = query.Sum();
decimal average = query.Average();

假设 query 运行时间非常长。我需要获取记录计数、返回的总 SomeNumber 数量,并在最后求取平均值。根据我的阅读,我认为 .AsEnumerable() 将使用 LINQ-to-SQL 执行查询,然后对 CountSumAverage 使用 LINQ-to-Objects 。然而,在 LINQPad 中执行时,我发现相同的查询被运行了三次。如果我将 .AsEnumerable() 替换为 .ToList(),则只查询一次。
我是否错过了关于 AsEnumerable 的某些内容/操作?

3
了解 AsEnumerable() 行为的一个非常有用的问题。 - LCJ
6个回答

85

调用 AsEnumerable() 不会执行查询,只有枚举它才会。

IQueryable 是让 LINQ to SQL 发挥其魔力的接口。 IQueryable 实现了 IEnumerable,因此当您调用 AsEnumerable() 时,您正在更改从那时起调用的扩展方法,即从 IQueryable 方法更改为 IEnumerable 方法(在这种特定情况下从 LINQ to SQL 更改为 LINQ to Objects)。但是,您并没有执行实际的查询,只是全面地更改了它将如何执行。

要强制执行查询,必须调用 ToList()


53
认为“什么都没有发生”是错误的。虽然调用 AsEnumerable 不会在调用时立即评估查询,但它绝对会产生影响。查询中随后调用的任何内容都将使用 LINQ to objects 进行评估,因此您不能组合附加元素到查询中(例如另一个 WhereOrderBy),这些元素将成为 SQL 语句的一部分。 - Adam Robinson
1
这是一个很好的例子,说明了这个功能的作用和为什么你想要使用它:假设你正在构建一个查询,并且第一个块以 OrderBy(...) 结尾,那么类型现在就是 IOrderedEnumerable,所以在后面你可以继续追加 ThenBy(...),甚至更晚一些时候,你可以说 return originalQuery.AsEnumerable() 将其转换回常规的 IEnumerable - The Muffin Man
2
我更喜欢使用ToArray,因为它可以完成相同的任务,除非你特别需要List<T>的实现。 - Rush Frisby
3
ToList() 更快,因此除非您的对象存在很长时间,否则请使用 ToList 而不是 ToArray,请参见 https://dev59.com/B3NA5IYBdhLWcg3wEZeT#16323412。 - flindeberg
@flindeberg 显然,如果在之前就知道计数,它们的速度是相同的。请参阅Scott Rippey的评论:https://dev59.com/B3NA5IYBdhLWcg3wEZeT#1106012 - David Klempfner
1
@Backwards_Dave 嗯,是的,我不反对这个观点。似乎我要反驳的那个陈述现在已经被删除了,但一般情况下,分配比所需更多的内存(即 ToList())比确保恰好足够的内存(即 ToArray())更快。我的论点是,如果你不知道该使用哪一个,请使用 ToList(),因为它的抽象程度更高,而且你不会受到性能惩罚。 - flindeberg

20

是的。所有的AsEnumerable做的就是导致Count, Sum,和Average函数在客户端执行(也就是说,它会将整个结果集带回到客户端,然后客户端会执行这些聚合操作,而不是创建COUNT()SUM()AVG()语句在SQL中)。


1
但是楼主的观点是他假设你所说的是正确的,但实证测试表明它并不是真的。 - James Curran
-1 这完全是不正确的。IQueryable 实现了 IEnumerable,因此对 AsEnumerable 的调用是无操作的,并不会强制执行查询。 - Justin Niessner
14
@James,Justin:你们误解了。我从未说过AsEnumerable()会导致查询被评估,我是说添加它的唯一作用就是在聚合函数被评估,它们将在客户端上执行(整个结果集将在客户端枚举,并且计算聚合函数),而不是被翻译成SQL语句。 - Adam Robinson
需要注意的是,如果你使用了 IOrderedEnumerable(已经通过某种类型的 OrderBy 完成了第一部分查询),你可能想要使用 AsEnumerable() - The Muffin Man
1
@JustinNiessner,那个评论完全是错误的,静态类型转换怎么可能是NOP呢?它改变了整个执行方案...这非常重要,因为LINQ是作为扩展方法(即静态类型)构建的,而不是继承(动态/运行时类型)。 - flindeberg

6

Justin Niessner的回答非常完美。

我只是想在这里引用一下MSDN的解释:.NET Language-Integrated Query for Relational Data

与ToList()和ToArray()不同,AsEnumerable()操作符不会导致查询执行。它仍然是延迟执行的。AsEnumerable()操作符仅仅改变了查询的静态类型,将IQueryable转换为IEnumerable,欺骗编译器将其余的查询视为本地执行。

一旦是LINQ to Objects,我们就可以应用对象的方法(例如ToString())。这是关于LINQ经常被问到的一个问题的解释 - 为什么LINQ to Entities无法识别方法'System.String ToString()?

根据ASENUMERABLE - codeblog.jonskeetAsEnumerable可能会很方便。
最后,还请参阅这个相关问题:Returning IEnumerable vs. IQueryable

3

好的,你走上了正确的道路。问题在于一个 IQueryable(在 AsEnumerable 调用之前的语句)也是一个 IEnumerable,所以这个调用实际上是无效的。它需要将其强制转换为特定的内存数据结构(例如 ToList()),以强制执行查询。


1
我认为ToList会强制Linq从数据库中获取记录。当您执行后续计算时,它们将针对内存中的对象而不是涉及数据库进行操作。
将返回类型保留为Enumerable意味着在代码执行计算之前不会获取数据。我猜这样做的结果是数据库被访问了三次——每个计算一次,数据不会保存到内存中。

1

只是想再澄清一下:

我认为根据我的阅读,.AsEnumerable()将使用LINQ-to-SQL执行查询

它不会立即执行查询,正如Justin's answer所解释的那样。它只会在稍后被实现(访问数据库)。

相反,当我在LINQPad中执行此操作时,我看到相同的查询运行了三次。

是的,请注意所有三个查询都完全相同,基本上将给定条件的所有行提取到内存中,然后在本地计算计数/总和/平均值。

如果我用.ToList()替换.AsEnumerable(),它只会被查询一次。

但仍然将所有数据加载到内存中,优点是现在只运行一次。

如果性能改进是一个问题,只需删除.AsEnumerable(),然后计数/总和/平均值将正确转换为它们的SQL对应项。这样做将运行三个查询(如果有满足条件的索引,则可能更快),但内存占用要少得多。


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