UPDATE: With the addition of InExpression in EF6, the performance of processing Enumerable.Contains improved dramatically. The analysis in this answer is great but largely obsolete since 2013.
在实体框架中使用Contains
实际上非常慢.的确,它在SQL中被转换为IN
子句,并且SQL查询本身执行得很快.但是问题和性能瓶颈在于从LINQ查询到SQL的转换.将要创建的表达式树被扩展为OR
个连接的长链,因为没有表示IN
的本机表达式.当SQL被创建时,许多OR
的表达式被识别并折叠回SQL IN
子句中.
这并不意味着使用Contains
比对ids
集合中的每个元素发出一个查询更糟糕(您的第一个 Select ).它可能仍然更好-至少对于不太大的Collection 品来说是这样.但对于大型Collection 品来说,这真的很糟糕.我记得不久前我测试过一个包含大约12.000个元素的Contains
%的查询,它可以工作,但花费了大约一分钟的时间,即使SQL中的查询在不到一秒的时间内执行.
在每次往返的Contains
个表达式中使用较少的元素来测试数据库的多个往返组合的性能可能是值得的.
这里展示并解释了这种方法以及将Contains
与实体框架结合使用的局限性:
Why does the Contains() operator degrade Entity Framework's performance so dramatically?
在这种情况下,原始SQL命令可能会表现最好,这意味着您可以调用dbContext.Database.SqlQuery<Image>(sqlString)
或dbContext.Images.SqlQuery(sqlString)
,其中sqlString
是@Rune的答案中显示的SQL.
Edit
以下是一些测量数据:
我在一个包含550000条记录和11列(ID从1开始,没有空格)的表上执行了此操作,并随机选取了20000个ID:
using (var context = new MyDbContext())
{
Random rand = new Random();
var ids = new List<int>();
for (int i = 0; i < 20000; i++)
ids.Add(rand.Next(550000));
Stopwatch watch = new Stopwatch();
watch.Start();
// here are the code snippets from below
watch.Stop();
var msec = watch.ElapsedMilliseconds;
}
Test 1
var result = context.Set<MyEntity>()
.Where(e => ids.Contains(e.ID))
.ToList();
Result -> msec = 85.5 sec
Test 2个
var result = context.Set<MyEntity>().AsNoTracking()
.Where(e => ids.Contains(e.ID))
.ToList();
Result -> msec = 84.5 sec
AsNoTracking
的这种微小效应是非常不寻常的.这表明瓶颈不是对象materialized (也不是如下所示的SQL).
对于这两个测试,可以在SQL Profiler中看到SQL查询很晚到达数据库.(我没有精确测量,但超过了70秒.)显然,将这个LINQ查询转换为SQL非常昂贵.
Test 3
var values = new StringBuilder();
values.AppendFormat("{0}", ids[0]);
for (int i = 1; i < ids.Count; i++)
values.AppendFormat(", {0}", ids[i]);
var sql = string.Format(
"SELECT * FROM [MyDb].[dbo].[MyEntities] WHERE [ID] IN ({0})",
values);
var result = context.Set<MyEntity>().SqlQuery(sql).ToList();
Result -> msec = 5.1 sec
Test 4
// same as Test 3 but this time including AsNoTracking
var result = context.Set<MyEntity>().SqlQuery(sql).AsNoTracking().ToList();
Result -> msec = 3.8 sec
这一次,禁用跟踪的效果更加明显.
Test 5个
// same as Test 3 but this time using Database.SqlQuery
var result = context.Database.SqlQuery<MyEntity>(sql).ToList();
Result -> msec = 3.7 sec
我的理解是context.Database.SqlQuery<MyEntity>(sql)
和context.Set<MyEntity>().SqlQuery(sql).AsNoTracking()
是一样的,所以测试4和测试5之间没有预期的差异.
(由于随机id Select 后可能出现重复,结果集的长度并不总是相同的,但它始终在19600到19640个元素之间.)
Edit 2个
Test 6个
即使是20000次往返数据库也比使用Contains
次要快:
var result = new List<MyEntity>();
foreach (var id in ids)
result.Add(context.Set<MyEntity>().SingleOrDefault(e => e.ID == id));
Result -> msec = 73.6 sec
请注意,我使用了SingleOrDefault
而不是Find
.对Find
使用相同的代码非常慢(几分钟后我取消了测试),因为Find
在内部调用DetectChanges
.禁用自动变化检测(context.Configuration.AutoDetectChangesEnabled = false
)将导致与SingleOrDefault
大致相同的性能.使用AsNoTracking
可以减少一到两秒的时间.
在同一台机器上使用数据库客户端(控制台应用程序)和数据库服务器进行测试.最后一个结果可能会因"远程"数据库的多次往返而变得更糟.