为什么.Contains慢?通过主键获取多个实体的最有效方法?

Tom*_*Tom 57 .net c# linq entity-framework entity-framework-4.1

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

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?

}
Run Code Online (Sandbox Code Playgroud)

我意识到我可以做一些性能测试来比较,但我想知道是否实际上有比两者更好的方法,并且我正在寻找一些启示,这两个查询之间的区别是,如果有的话,一旦它们一直存在"翻译".

Sla*_*uma 131

更新:通过在EF6中添加InExpression,处理Enumerable.Contains的性能得到了显着提升.这个答案的分析很好,但自2013年以来基本上已经过时了.

使用Contains实体框架其实是很慢的.确实,它转换为INSQL中的一个子句,并且SQL查询本身可以快速执行.但问题和性能瓶颈在于从LINQ查询到SQL的转换.将创建的表达式树被扩展为一个长OR连接链,因为没有表示一个的本地表达式IN.创建SQL时,OR会识别许多s的表达式并将其折叠回SQL IN子句.

这并不意味着使用Contains比在ids集合中每个元素发出一个查询(第一个选项)更糟糕.它可能还是更好 - 至少对于不太大的集合.但对于大型系列来说,这真的很糟糕.我记得我前段时间测试了一个Contains大约12.000个元素的查询但是工作了大约一分钟,即使SQL中的查询执行时间不到一秒钟.

Contains对于每次往返,在表达式中使用较少数量的元素来测试多次往返组合对数据库的性能可能是值得的.

此方法以及使用ContainsEntity Framework 的限制在此处显示和解释:

为什么Contains()运算符会如此显着地降低Entity Framework的性能?

在这种情况下,原始SQL命令可能会表现最佳,这意味着您可以调用@ Rune的答案中显示的SQL dbContext.Database.SqlQuery<Image>(sqlString)dbContext.Images.SqlQuery(sqlString)其中sqlString的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;
}
Run Code Online (Sandbox Code Playgroud)

测试1

var result = context.Set<MyEntity>()
    .Where(e => ids.Contains(e.ID))
    .ToList();
Run Code Online (Sandbox Code Playgroud)

结果 - > msec = 85.5秒

测试2

var result = context.Set<MyEntity>().AsNoTracking()
    .Where(e => ids.Contains(e.ID))
    .ToList();
Run Code Online (Sandbox Code Playgroud)

结果 - > msec = 84.5秒

这种微小的效果AsNoTracking非常不寻常.它表明瓶颈不是对象实现(而不是如下所示的SQL).

对于这两个测试,可以在SQL事件探查器中看到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();
Run Code Online (Sandbox Code Playgroud)

结果 - > msec = 5.1秒

测试4

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

结果 - > msec = 3.8秒

这次禁用跟踪的效果更加明显.

测试5

// same as Test 3 but this time using Database.SqlQuery
var result = context.Database.SqlQuery<MyEntity>(sql).ToList();
Run Code Online (Sandbox Code Playgroud)

结果 - > msec = 3.7秒

我的理解是context.Database.SqlQuery<MyEntity>(sql)相同的context.Set<MyEntity>().SqlQuery(sql).AsNoTracking(),因此测试4和测试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));
Run Code Online (Sandbox Code Playgroud)

结果 - > msec = 73.6秒

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

测试是在同一台机器上使用数据库客户端(控制台应用程序)和数据库服务器完成的.由于许多往返,最后的结果可能会因"远程"数据库而变得更糟.

  • 很棒的答案和研究.一个超级小调,但你可以在一行中构建一个ID列表,如下所示:string values = string.Join(",",ids); (3认同)

归档时间:

查看次数:

34975 次

最近记录:

7 年,1 月 前