通过主键选择多个实体的最有效方法是什么?
public IEnumerable<Models.Image> GetImagesById(IEnumerable<int> ids) { //return ids.Select(id => Images.Find(id)); //is this cool? return Images.Where( im => ids.Contains(im.Id)); //is this better, worse or the same? //is there a (better) third way? }
我意识到我可以进行一些性能测试以进行比较,但是我想知道实际上是否有比这两种方法更好的方法,并且我想对这两个查询(如果有)之间的区别(如果有)有所启发“翻译”。
更新:通过在EF6中添加InExpression,处理Enumerable.Contains的性能得到了显着提高。 自2013年以来,此答案中的分析很棒,但已经过时了。
使用Contains实体框架其实是很慢的。的确,它可以转换为INSQL中的子句,并且SQL查询本身可以快速执行。但是问题和性能瓶颈在于从LINQ查询到SQL的转换。将要创建的表达式树被扩展为一长串OR串联,因为没有原生表达式表示IN。创建SQL时,OR可以识别出许多s的表达式并将其折叠回SQL IN子句中。
Contains
IN
OR
这并不意味着使用Contains比在ids集合中的每个元素上发出一个查询(您的第一个选择)更糟糕。它可能仍然更好- 至少对于不是太大的收藏。但是对于大型收藏来说,这确实很糟糕。我记得我曾经测试过一个Contains包含大约12.000个元素的查询,该查询可以工作,但是花费了大约一分钟,即使SQL中的查询在不到一秒钟的时间内执行了。
ids
可能需要测试多次往返数据库的组合的性能,并且Contains每次往返表达式中的元素数量较少。
Contains此处显示并说明了这种方法以及与Entity Framework一起使用的局限性:
为什么Contains()运算符会如此大幅度降低Entity Framework的性能?
在这种情况下,原始SQL命令可能会发挥最佳性能,这意味着您将调用@Rune答案中显示的SQL dbContext.Database.SqlQuery<Image>(sqlString)或dbContext.Images.SqlQuery(sqlString)在哪里sqlString。
dbContext.Database.SqlQuery<Image>(sqlString)
dbContext.Images.SqlQuery(sqlString)
sqlString
编辑
以下是一些测量:
我在具有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)。
AsNoTracking
对于这两个测试,可以在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(),因此在测试4和测试5之间没有预期的区别。
context.Database.SqlQuery<MyEntity>(sql)
context.Set<MyEntity>().SqlQuery(sql).AsNoTracking()
(由于随机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可以将时间减少一到两秒。
SingleOrDefault
Find
DetectChanges
context.Configuration.AutoDetectChangesEnabled = false
测试是在同一台计算机上使用数据库客户端(控制台应用程序)和数据库服务器完成的。由于有许多往返,使用“远程”数据库的最后结果可能会变得更糟。