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

k4aesqcs  于 2022-12-06  发布在  其他
关注(0)|答案(4)|浏览(373)

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

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?

}

我意识到我可以做一些性能测试来进行比较,但我想知道是否有一种比这两种方法更好的方法,并且正在寻找这两种查询之间的差异的一些启示,如果有的话,一旦它们被“翻译”。

rhfm7lfc

rhfm7lfc1#

    • UPDATE:在EF6中添加InExpression后,处理Enumerable. Contains的性能显著提高。此答案中的分析很棒,但自2013年以来已基本过时。**

在Entity Framework中使用Contains实际上是非常慢的。确实,它转换成SQL中的IN子句,并且SQL查询本身执行得很快。但是问题和性能瓶颈是在从LINQ查询到SQL的转换中。将创建的表达式树扩展为OR串联的长链,因为没有表示IN。当创建SQL时,这个包含许多OR的表达式被识别并折叠回SQL IN子句。
这并不意味着使用Contains比对ids集合中的每个元素发出一个查询更糟糕(你的第一个选择)。它可能更好-至少对于不太大的集合。但是对于大的集合,它真的很糟糕。我记得我以前测试过一个Contains查询,大约12。000个元素,虽然SQL中的查询在不到一秒的时间内执行,但它仍然工作了大约一分钟。
可能值得测试多次往返数据库的组合的性能,每次往返的Contains表达式中的元素数较少。
此方法以及将Contains与Entity Framework一起使用的限制将在此处显示和解释:
为什么Contains()操作符会如此显著地降低Entity Framework的性能?
在这种情况下,原始SQL命令可能会执行得最好,这意味着您调用dbContext.Database.SqlQuery<Image>(sqlString)dbContext.Images.SqlQuery(sqlString),其中sqlString是@Rune的答案中显示的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;
}
    • 测试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事件探查器中看到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之间预期没有差异。
(The由于随机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在内部调用DetectChanges。(context.Configuration.AutoDetectChangesEnabled = false)的性能与SingleOrDefault大致相同。使用AsNoTracking可减少一到两秒的时间。
测试是在同一台机器上的数据库客户端(控制台应用程序)和数据库服务器上进行的。由于多次往返,最后的结果可能会在"远程"数据库上变得更糟。

dgtucam1

dgtucam12#

第二个选项肯定比第一个选项好。第一个选项将导致对数据库的ids.Length查询,而第二个选项可以在SQL查询中使用'IN'操作符。它基本上会将您的LINQ查询转换为类似以下SQL的内容:

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

其中value1,value2等是你的id变量的值。但是要注意,我认为用这种方法可以序列化到一个查询中的值的数量可能有一个上限。我会看看我是否能找到一些文档...

kcrjzv8t

kcrjzv8t3#

嗯,最近我遇到了一个类似的问题,我发现最好的方法是在临时表中插入列表,然后进行连接。

private List<Foo> GetFoos(IEnumerable<long> ids)
{
    var sb = new StringBuilder();
    sb.Append("DECLARE @Temp TABLE (Id bigint 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();
}

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

3pmvbmvn

3pmvbmvn4#

使用toArray()将List转换为Array可提高性能。可以通过以下方式执行此操作:

ids.Select(id => Images.Find(id));     
    return Images.toArray().Where( im => ids.Contains(im.Id));

相关问题