为什么Entity Framework的AsEnumerable()从服务器下载所有数据?

时间:2015-08-05 15:42:14

标签: linq entity-framework

使用AsEnumerable()时EF下载所有结果行的解释是什么?

我的意思是这段代码:

context.Logs.AsEnumerable().Where(x => x.Id % 2 == 0).Take(100).ToList();

会在将任何行传递给Where()方法之前从表中下载所有行,并且表中可能有数百万行。

我希望它能够下载的内容足以收集满足Id % 2 == 0条件的100行(最可能只有200行)。

无法使用Read() SqlDataReader方法使用普通ADO.NET按需加载行,并节省时间和带宽吗?

我认为由于某种原因它不能起作用,我希望听到一个支持该设计决定的好论据。

注意:这是一个完全做作的例子,我通常知道你不应该这样使用EF,但我在一些现有的代码中发现了这一点,并且我的假设被证明是错误的。

7 个答案:

答案 0 :(得分:7)

简答:不同行为的原因是,当您直接使用IQueryable时,可以为整个LINQ查询形成单个SQL查询;但是当你使用IEnumerable时,必须加载整个数据表。

答案很长:请考虑以下代码。

context.Logs.Where(x => x.Id % 2 == 0)

context.Logs的类型为IQueryable<Log>IQueryable<Log>.WhereExpression<Func<Log, bool>>作为谓词。 Expression表示抽象语法树;也就是说,它不仅仅是您可以运行的代码。可以把它想象成在运行时在内存中表示,如下所示:

Lambda (=>)
  Parameters
    Variable: x
  Body
    Equals (==)
      Modulo (%)
        PropertyAccess (.)
          Variable: x
          Property: Id
        Constant: 2
      Constant: 0

LINQ-to-Entities引擎可以使用context.Logs.Where(x => x.Id % 2 == 0)并将其机械转换为SQL查询,如下所示:

SELECT *
FROM "Logs"
WHERE "Logs"."Id" % 2 = 0;

如果将代码更改为context.Logs.Where(x => x.Id % 2 == 0).Take(100),则SQL查询将变为如下所示:

SELECT *
FROM "Logs"
WHERE "Logs"."Id" % 2 = 0
LIMIT 100;

这完全是因为IQueryable上的LINQ扩展方法使用的是Expression,而不仅仅是Func

现在考虑context.Logs.AsEnumerable().Where(x => x.Id % 2 == 0)IEnumerable<Log>.Where扩展方法将Func<Log, bool>作为谓词。那只是可运行的代码。无法分析确定其结构;它不能用于形成SQL查询。

答案 1 :(得分:3)

Entity Framework和Linq使用延迟加载。这意味着(除其他外)他们不会运行查询,直到他们需要枚举结果:例如使用ToList()AsEnumerable(),或者结果是否用作枚举器(在{中)例如{1}}。

相反,它使用谓词构建查询,并返回foreach个对象,以便在实际返回结果之前进一步“预过滤”结果。例如,您可以找到更多信息here。实体框架实际上将根据您传递的谓词构建SQL查询。

在你的例子中:

IQueryable

从上下文中的Logs表中,它获取所有内容,返回带有结果的context.Logs.AsEnumerable().Where(x => x.Id % 2 == 0).Take(100).ToList(); ,然后过滤结果,获取前100个,然后将结果列为IEnumerable

另一方面,只需删除List即可解决问题:

AsEnumerable

这里它将在结果上构建一个查询/过滤器,然后只有执行context.Logs.Where(x => x.Id % 2 == 0).Take(100).ToList(); 后才能查询数据库。

这也意味着您可以动态构建复杂查询,而无需在数据库上实际运行它直到结束,例如:

ToList()

更新

正如你在评论中提到的那样,你似乎已经知道我刚写的内容,并且只是在寻找原因。

它甚至更简单:因为var logs = context.Logs.Where(a); // first filter if (something) { logs = logs.Where(b); // second filter } var results = logs.Take(100).ToList(); // only here is the query actually executed 将结果转换为另一种类型(在这种情况下为AsEnumerableIQueryable<T>),它必须首先转换所有结果行,因此它具有首先获取数据。在这种情况下,它基本上是IEnumerable<T>

答案 2 :(得分:1)

显然,您了解为什么最好避免在问题中使用AsEnumerable()

此外,其他一些答案非常清楚 为什么 调用AsEnumerable()会改变执行和读取查询的方式。简而言之,这是因为您正在调用IEnumrable<T>扩展方法而不是IQueryable<T>扩展方法,后者允许您在执行 之前组合谓词 数据库中的查询。

但是,我仍然认为这不能回答你的实际问题,这是一个合理的问题。你说(强调我的):

  

我的意思是这段代码:

     

context.Logs.AsEnumerable().Where(x => x.Id % 2 == 0).Take(100).ToList();

     在将任何行传递给Where()方法之前,

将从表中下载 所有行 ,并且表中可能有数百万行。

我的问题是:是什么让你得出这样的结论?

我认为,因为您使用的是IEnumrable<T>而不是IQueryable<T>,所以在数据库中执行的查询确实很简单:

select * from logs

...没有任何谓词,与使用IQueryable<T>调用WhereTake时发生的情况不同。

但是,AsEnumerable()方法调用 在此时获取所有行,正如其他答案所暗示的那样。实际上,这是AsEnumerable()调用的实现:

public static IEnumerable<TSource> AsEnumerable<TSource>(this IEnumerable<TSource> source)
{
    return source;
}

那里没有进攻。实际上,即使对IEnumerable<T>.Where()IEnumerable<T>.Take()的调用实际上也不会在那一刻开始获取任何行。他们只是设置包装IEnumerable s,它将在迭代结果时过滤结果。结果的获取和迭代实际上只在调用ToList()时开始。

所以当你说:

  

使用Read() SqlDataReader 1,000,000方法,使用普通ADO.NET,可以不按需要加载行,并节省时间和带宽吗?

...再次,我的问题是:不是已经这样做了吗?

如果您的表格有100行,我仍然希望您的代码段只能获取满足Where条件的static void Main(string[] args) { var list = PretendImAOneMillionRecordTable().Where(i => i < 500).Take(10).ToList(); } private static IEnumerable<int> PretendImAOneMillionRecordTable() { for (int i = 0; i < 1000000; i++) { Console.WriteLine("fetching {0}", i); yield return i; } } 行,然后停止提取行。

为证明这一点,请尝试运行以下小程序:

fetching 0
fetching 1
fetching 2
fetching 3
fetching 4
fetching 5
fetching 6
fetching 7
fetching 8
fetching 9

...当我运行它时,我只得到以下10行输出:

1,000,000

它不会遍历整个Where()“行”,即使我在Take()上链接IEnumerable<T>SqlDataReader.Read()次。

现在,您必须记住,对于您的小EF代码段,如果您使用非常小的表进行测试,它实际上可能会获取 all 如果所有行都符合SqlConnection.PacketSize的值,则一次显示行。这个是正常的。每次调用AsEnumerable()时,它都不会一次只提取一行。为了减少网络呼叫往返次数,它将始终尝试一次获取一批行。我想知道这是否是您观察到的,这误导您认为IQueryable导致 所有 行从表中获取。

即使你发现你的例子表现不如你想象的那么糟糕,但这不是不使用IQueryable的理由。使用<?xml version="1.0"?> <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> <Employee> <FIRSTNAME /> </Employee> </xs:schema> 构建更复杂的数据库查询几乎总能提供更好的性能,因为您可以从数据库索引等中受益,从而更有效地获取结果。

答案 3 :(得分:0)

AsEnumerable()急切加载DbSet<T> Logs

你可能想要像

这样的东西
context.Logs.Where(x => x.Id % 2 == 0).AsEnumerable();

这里的想法是,在从数据库中实际加载谓词过滤器之前,你应该对该集合应用谓词过滤器。

EF支持LINQ世界令人印象深刻的子集。它会将您漂亮的LINQ查询转换为幕后的SQL表达式。

答案 4 :(得分:0)

我之前遇到过这种情况。 在调用linq函数之前,不会执行context命令,因为您已完成

context.Logs.AsEnumerable() 

它假设你已经完成了查询,因此编译它并返回所有行。 如果您将其更改为:

context.Logs.Where(x => x.Id % 2 == 0).AsEnumerable() 

它将编译一个SQL语句,该语句只能获取id为模块2的行。 同样,如果你做了

context.Logs.Where(x => x.Id % 2 == 0).Take(100).ToList();

这将创建一个能够获得前100名的陈述......

我希望有所帮助。

答案 5 :(得分:0)

LinQ to Entities在进入枚举之前有一个由所有Linq方法组成的商店表达式。

当您使用AsEnumerable()然后使用Where()时:

context.Logs.Where(...).AsEnumerable()

Where()知道前一个链调用有一个商店表达式,所以他将它的谓词附加到It来进行延迟加载。

如果你调用它,那么调用Where的重载是不同的:

context.Logs.AsEnumerable().Where(...)

这里Where()只知道他以前的方法是枚举(它可以是任何类型的&#34;可枚举的&#34;集合),并且他可以应用他的条件的唯一方法是迭代集合DbSet类的IEnumerable实现,它必须首先从数据库中检索记录。

答案 6 :(得分:0)

我认为你不应该使用它:

context.Logs.AsEnumerable().Where(x => x.Id % 2 == 0).Take(100).ToList();

正确的做事方式是:

context.Logs.AsQueryable().Where(x => x.Id % 2 == 0).Take(100).ToList();

回答这里的解释: