更新:通过在 EF6 中添加 InExpression,处理 Enumerable.Contains 的性能显着提高。这个答案中的分析很棒,但自 2013 年以来基本上已经过时了。
在实体框架中使用Contains其实很慢。确实,它转换为 SQL 中的IN 子句,并且 SQL 查询本身执行得很快。但问题和性能瓶颈在于将 LINQ 查询转换为 SQL。将要创建的表达式树扩展为一长串OR 连接,因为没有代表IN 的本机表达式。创建 SQL 时,许多ORs 的表达式被识别并折叠回SQL IN 子句中。
这并不意味着使用Contains 比在ids 集合(您的第一个选项)中为每个元素发出一个查询更糟糕。它可能仍然更好 - 至少对于不太大的收藏。但对于大型收藏品来说,这真的很糟糕。我记得前段时间我测试了一个包含大约 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。
编辑
以下是一些测量结果:
我在一个有 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;
}
测试 1
var result = context.Set<MyEntity>()
.Where(e => ids.Contains(e.ID))
.ToList();
结果 -> 毫秒 = 85.5 秒
测试 2
var result = context.Set<MyEntity>().AsNoTracking()
.Where(e => ids.Contains(e.ID))
.ToList();
结果 -> 毫秒 = 84.5 秒
AsNoTracking 的这种微小效果非常不寻常。说明瓶颈不是对象物化(也不是如下所示的SQL)。
对于这两个测试,可以在 SQL Profiler 中看到 SQL 查询很晚才到达数据库。 (我没有准确测量,但它晚于 70 秒。)显然,将这个 LINQ 查询翻译成 SQL 是非常昂贵的。
测试 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();
结果 -> 毫秒 = 5.1 秒
测试 4
// same as Test 3 but this time including AsNoTracking
var result = context.Set<MyEntity>().SqlQuery(sql).AsNoTracking().ToList();
结果 -> 毫秒 = 3.8 秒
这一次禁用跟踪的效果更加明显。
测试 5
// same as Test 3 but this time using Database.SqlQuery
var result = context.Database.SqlQuery<MyEntity>(sql).ToList();
结果 -> 毫秒 = 3.7 秒
我的理解是context.Database.SqlQuery<MyEntity>(sql)和context.Set<MyEntity>().SqlQuery(sql).AsNoTracking()是一样的,所以预计Test 4和Test 5没有区别。
(由于随机 id 选择后可能出现重复,结果集的长度并不总是相同,但总是在 19600 和 19640 个元素之间。)
编辑 2
测试 6
即使 20000 次往返数据库也比使用 Contains 更快:
var result = new List<MyEntity>();
foreach (var id in ids)
result.Add(context.Set<MyEntity>().SingleOrDefault(e => e.ID == id));
结果 -> 毫秒 = 73.6 秒
请注意,我使用了SingleOrDefault 而不是Find。对Find 使用相同的代码非常慢(几分钟后我取消了测试),因为Find 在内部调用DetectChanges。禁用自动更改检测 (context.Configuration.AutoDetectChangesEnabled = false) 导致与 SingleOrDefault 大致相同的性能。使用AsNoTracking 可以将时间缩短一到两秒。
测试是在同一台机器上使用数据库客户端(控制台应用程序)和数据库服务器完成的。由于多次往返,使用“远程”数据库的最后一个结果可能会变得更糟。