使用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,但我在一些现有的代码中发现了这一点,并且我的假设被证明是错误的。
答案 0 :(得分:7)
简答:不同行为的原因是,当您直接使用IQueryable
时,可以为整个LINQ查询形成单个SQL查询;但是当你使用IEnumerable
时,必须加载整个数据表。
答案很长:请考虑以下代码。
context.Logs.Where(x => x.Id % 2 == 0)
context.Logs
的类型为IQueryable<Log>
。 IQueryable<Log>.Where
以Expression<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
将结果转换为另一种类型(在这种情况下为AsEnumerable
到IQueryable<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>
调用Where
和Take
时发生的情况不同。
但是,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();
回答这里的解释: