一尘不染

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

c#

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

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?

}

我意识到我可以进行一些性能测试以进行比较,但是我想知道实际上是否有比这两种方法更好的方法,并且我想对这两个查询(如果有)之间的区别(如果有)有所启发“翻译”。


阅读 207

收藏
2020-05-19

共1个答案

一尘不染

更新:通过在EF6中添加InExpression,处理Enumerable.Contains的性能得到了显着提高。
自2013年以来,此答案中的分析很棒,但已经过时了。

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

这并不意味着使用Contains比在ids集合中的每个元素上发出一个查询(您的第一个选择)更糟糕。它可能仍然更好-
至少对于不是太大的收藏。但是对于大型收藏来说,这确实很糟糕。我记得我曾经测试过一个Contains包含大约12.000个元素的查询,该查询可以工作,但是花费了大约一分钟,即使SQL中的查询在不到一秒钟的时间内执行了。

可能需要测试多次往返数据库的组合的性能,并且Contains每次往返表达式中的元素数量较少。

Contains此处显示并说明了这种方法以及与Entity Framework一起使用的局限性:

为什么Contains()运算符会如此大幅度降低Entity
Framework的性能?

在这种情况下,原始SQL命令可能会发挥最佳性能,这意味着您将调用@Rune答案中显示的SQL
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)。

对于这两个测试,可以在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之间没有预期的区别。

(由于随机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可以将时间减少一到两秒。

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

2020-05-19