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

时间:2011-11-12 20:42:41

标签: c# .net entity-framework linq 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?

}

我意识到我可以做一些性能测试来进行比较,但我想知道实际上是否有比两者更好的方法,并且我正在寻找一些关于这两个查询之间的区别(如果有的话)的启示他们被“翻译”了。

6 个答案:

答案 0 :(得分:129)

更新:通过在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与实体框架一起使用的限制:

Why does the Contains() operator degrade Entity Framework's performance so dramatically?

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

结果 - &gt; 毫秒= 85.5秒

测试2

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

结果 - &gt; 毫秒= 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();

结果 - &gt; 毫秒= 5.1秒

测试4

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

结果 - &gt; 毫秒= 3.8秒

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

测试5

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

结果 - &gt; 毫秒= 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));

结果 - &gt; 毫秒= 73.6秒

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

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

答案 1 :(得分:4)

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

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

其中value1,value2等是ids变量的值。但请注意,我认为以这种方式可以序列化为查询的值的数量可能存在上限。我会看看能不能找到一些文件...

答案 2 :(得分:0)

我正在使用Entity Framework 6.1,并发现使用您的code,最好使用:

return db.PERSON.Find(id);

而不是:

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

Performance of Find() vs. FirstOrDefault对此有一些想法。

答案 3 :(得分:0)

Weel,最近有一个类似的问题,我找到的最好的方法是在临时表中插入包含列表,然后进行连接。

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

这不是一个很好的方式,但对于大型列表来说,它非常高效。

答案 4 :(得分:0)

string q;
            q = @"DECLARE @flds TABLE(CmpFldDtlCod int)
            INSERT @flds
            SELECT CmpFldDtlCod FROM CmpFldDtlTb (NOLOCK) WHERE CmpCod = " + cmpCod + @" AND AprvUsrCod IS NULL
            SELECT p.PlcyCod,p.CmpFldDtlCod,f.MapFldDtlCod,f.CmpFldDtlNam,u.PrntCmpDocNo,p.InsNam,p.Prm,p.Cptl,p.IsuDte
            FROM dbo.PlcyTb p (NOLOCK)
            JOIN dbo.PlcyUnqTb u (NOLOCK) ON p.PlcyUnqCod = u.PlcyUnqCod
            JOIN CmpFldDtlTb f (NOLOCK) ON f.CmpFldDtlCod = p.CmpFldDtlCod
            WHERE p.CmpCod = " + cmpCod + " AND p.CmpFldDtlCod IN(SELECT CmpFldDtlCod FROM @flds)";

            lstResult = db.Database.SqlQuery<View_LastPlcyFldNotAprv>(q).ToList();

答案 5 :(得分:-1)

使用toArray()将List转换为数组可提高性能。你可以这样做:

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