【问题标题】:IQueryable vs IEnumerableIQueryable 与 IEnumerable
【发布时间】:2020-04-28 09:04:20
【问题描述】:

考虑以下片段:

IQueryable<Person> pQ = from p in db.People select p;
IEnumerable<Person> pE = pQ;

Console.WriteLine(pQ.Count());
Console.WriteLine(pE.Count());

两者都给出相同的结果,但如果您跟踪生成的 SQL,IQueryable 版本使用COUNT 发出查询,而IEnumerable 版本只发出SELECT,拉入所有行并执行在内存中计数,这可能是非常低效的。这同样适用于其他方法,例如 Sum() 和 Average(),并且在 EF6 和 EF Core 3 中都会出现。

原因是Count() 是一个扩展方法,因此绑定到变量的静态类型(第一种情况下为IQueryable,第二种情况下为IEnumerable)而不是动态类型(两者都相同)情况,因为它是同一个对象)。

这是一个非常讨厌的问题,这意味着在这种情况下通常最好避免使用IEnumerable。但是,有一个解决方法:

Console.WriteLine(pE.AsQueryable().Count());

这意味着SQL COUNT 已发出。

(注意:转换为IQueryable 也可以在这里工作,但不是一般情况下 - 如果IEnumerable 纯粹是指内存中的数据,那么转换会失败,但AsQueryable() 会简单地将对象包围在一个中性的包装器。)

我的问题是:既然这个问题很容易被忽视并且会导致这种低效的行为,为什么对AsQueryable() 的调用不简单地嵌入到扩展方法的IEnumerable 实现中?

【问题讨论】:

  • IEnumerable 扩展方法的功能与IQueryable 版本不同。这在Count 中并不明显,但请考虑Where。在IEnumerable 中,这需要一个谓词(返回布尔值)lambda 并使用它来过滤流。在 IQueryable 中,这需要一个表示 lambda 的 Expression 树并将其转换为 SQL。到方法执行时,参数已经是错误的类型,无法转换。本质上,您的代码所做的是将IQueryable 转换为IEnumerable,这与调用执行查询的AsEnumerable 相同。

标签: entity-framework linq-to-entities extension-methods


【解决方案1】:

我的问题是:既然这个问题很容易被忽视并且会导致这种低效的行为,为什么对AsQueryable() 的调用不简单地嵌入到扩展方法的IEnumerable 实现中? em>

原因是您并不总是想调用IQueryable 扩展方法。

如果源的类型实现IQueryable&lt;T&gt;, AsQueryable&lt;TElement&gt;(IEnumerable&lt;TElement&gt;) 直接返回。 否则,它返回一个IQueryable&lt;T&gt;,它通过以下方式执行查询 在Enumerable 中调用等效的查询运算符方法,而不是 Queryable中的那些。

如果您想具体化查询,您可以调用返回IEnumerable&lt;T&gt; 的AsEnumerable 方法或使用对IEnumerable&lt;T&gt; 变量的赋值(如您所做的那样),然后调用Count 方法将再次调用AsQueryable方法将返回原始IQueryable&lt;T&gt; 对象并为IQueryable&lt;T&gt; 执行Count。

问题在于IQueryable 使用QueryProvider 将给定的表达式树转换为实际查询,并且并非每个表达式树都可以转换为可能导致错误的有效查询。有许多查询提供程序不够成熟/不够成熟,在这种情况下它只是抛出异常。我认为这甚至比性能下降还要糟糕。此外 Entity Framework Core 团队认为,由于某些查询被部分翻译,其余部分在本地评估(幸运的是 EFCore 不会引发异常)。

如果我可以建议您如何处理这个可能被忽视的问题,我会编写 roslyn 分析器,它会调查代码并查找 IEnumerable&lt;T&gt; enumerable = queryable; 分配并在发现问题时发出警告。 GitHub 上可能已经实现了一些,因为您可能不是唯一处理它的人。

【讨论】:

  • 我不同意抛出异常比性能下降更糟糕。异常告诉您存在问题并迫使您解决它。糟糕的表现会被忽视。
  • 问题通常出在查询提供程序实现中,另一个版本可能会解决它。如果抛出异常,您将解决它,并且您永远不会调查在未来版本中是否已解决。如果你解决它,你将需要大多数查询去实现。所以,我还是会毫无例外地翻译。
  • 我不明白这有什么意义。如果您在 IEnumerable 上调用扩展方法,那么您几乎完全绕过了查询提供程序(除了基本的 SELECT),所以您一开始就永远不会意识到提供程序中存在错误,更不用说是已修复。
  • 有一件事是应该导致异常的错误。另一件事是查询提供程序无法翻译(在某个时间点/版本),这不应该导致异常。有意义吗?
  • 没有。因为 IEnumerable 的使用完全绕过了查询提供程序。它的能力无关紧要。
猜你喜欢
  • 2011-06-09
  • 2014-11-09
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-08-18
  • 1970-01-01
相关资源
最近更新 更多