为什么。包容很慢? 通过主键获取多个实体的最有效方法?
什么是通过主键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中的查询执行时间不到一秒钟。
对于每次往返的Contains
expression式中使用较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(); }
这不是一个漂亮的方式,但对于大型列表,它是非常高性能的。