为什么。包容很慢? 通过主键获取多个实体的最有效方法?

什么是通过主键select多个实体最有效的方法?

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? } 

我意识到我可以做一些性能testing来进行比较,但是我想知道是否有比这两个更好的方法,并且正在寻找一些关于这两个查询之间的区别是什么的启发,如果有的话,一旦他们“翻译”。

在entity framework中使用Contains其实非常慢。 确实,它在SQL中转化为IN子句,并且SQL查询本身的执行速度很快。 但是问题和性能瓶颈在于从LINQ查询到SQL的转换。 将被创build的expression式树被扩展为OR连接的长链,因为没有表示IN本地expression式。 当创buildSQL时,这个OR的expression式被识别并折叠回SQL IN子句中。

这并不意味着使用Contains比在您的ids集合(第一个选项)中为每个元素发出一个查询更糟糕。 这可能还是更好 – 至less对于不太大的集合。 但是对于大集合来说,这真的很糟糕。 我记得前一段时间我testing了一个Contains大约12.000个元素的Contains查询,但是却花了一分钟左右的时间,尽pipeSQL中的查询执行时间不到一秒钟。

对于每次往返的Containsexpression式中使用较less数量的元素来testing多次往返组合的性能可能是值得的。

这种方法以及使用Contains with Entity Framework的限制在此处显示和解释:

为什么Contains()运算符会显着降低entity framework的性能?

原始的SQL命令可能会在这种情况下performance得最好,这意味着你调用了dbContext.Database.SqlQuery<Image>(sqlString)dbContext.Images.SqlQuery(sqlString) ,其中sqlString是@ Rune的答案中显示的SQL。

编辑

这里有一些测量:

我在550000条logging和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; } 

testing1

 var result = context.Set<MyEntity>() .Where(e => ids.Contains(e.ID)) .ToList(); 

结果 – > 毫秒= 85.5秒

testing2

 var result = context.Set<MyEntity>().AsNoTracking() .Where(e => ids.Contains(e.ID)) .ToList(); 

结果 – > msec = 84.5秒

AsNoTracking这个小小的影响是非常不寻常的。 这表明瓶颈不是对象实现(而不是如下所示的SQL)。

对于这两个testing,可以在SQL Profiler中看到SQL查询到达数据库的时间很晚。 (我没有准确的测量,但是超过了70秒。)很显然,将这​​个LINQ查询翻译成SQL是非常昂贵的。

testing3

 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(); 

结果 – > msec = 5.1秒

testing4

 // same as Test 3 but this time including AsNoTracking var result = context.Set<MyEntity>().SqlQuery(sql).AsNoTracking().ToList(); 

结果 – > 毫秒= 3.8秒

这次禁用跟踪的效果更为明显。

testing5

 // 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() ,因此testing4和testing5之间没有区别。

(结果集的长度并不总是相同的,因为在随机IDselect后可能有重复,但总是在19600和19640之间)。

编辑2

testing6

即使20000往返数据库也比使用Contains

 var result = new List<MyEntity>(); foreach (var id in ids) result.Add(context.Set<MyEntity>().SingleOrDefault(e => e.ID == id)); 

结果 – > msec = 73.6秒

请注意,我使用SingleOrDefault而不是Find 。 在Find使用相同的代码非常慢(我在几分钟后取消了testing),因为Find DetectChanges内部调用DetectChanges 。 禁用自动更改检测( context.Configuration.AutoDetectChangesEnabled = false )将导致与SingleOrDefault大致相同的性能。 使用AsNoTracking将时间减less一两秒钟。

数据库客户端(控制台应用程序)和数据库服务器在同一台机器上完成了testing。 由于多次往返,“远程”数据库的最后结果可能会变得更糟。

第二种select肯定比第一种更好。 第一个选项将导致对数据库的ids.Length查询,而第二个选项可以在SQL查询中使用'IN'运算符。 它将基本上把你的LINQ查询变成如下的SQL:

 SELECT * FROM ImagesTable WHERE id IN (value1,value2,...) 

其中value1,value2等是您的idsvariables的值。 但请注意,我认为可以通过这种方式将序列化到查询中的值的数量设置为上限。 我会看看如果我能find一些文件…

我正在使用entity framework6.1,发现使用你的代码 ,更好地使用:

 return db.PERSON.Find(id); 

而不是:

 return db.PERSONA.FirstOrDefault(x => x.ID == id); 

Find()与FirstOrDefault的性能是对此的一些想法。

最近有一个类似的问题,我发现最好的方法是在一个临时表中插入包含的列表并进行连接。

 private List<Foo> GetFoos(IEnumerable<long> ids) { var sb = new StringBuilder(); sb.Append("DECLARE @Temp TABLE (Id bitint PRIMARY KEY)\n"); foreach (var id in ids) { sb.Append("INSERT INTO @Temp VALUES ('"); sb.Append(id); sb.Append("')\n"); } sb.Append("SELECT f.* FROM [dbo].[Foo] f inner join @Temp t on f.Id = t.Id"); return this.context.Database.SqlQuery<Foo>(sb.ToString()).ToList(); } 

这不是一个漂亮的方式,但对于大型列表,它是非常高性能的。