【问题标题】:Why is .Contains slow? Most efficient way to get multiple entities by primary key?为什么 .Contains 慢?通过主键获取多个实体的最有效方法?
【发布时间】:2011-12-27 18:59:07
【问题描述】:

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

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?

}

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

【问题讨论】:

标签: c# .net entity-framework linq entity-framework-4.1


【解决方案1】:

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

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

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

【讨论】:

  • 谢谢,你认为这是要走的路吗?还是有其他方法?
  • @Tim:这绝对是要走的路。只要你在 EF 4+ 上,你就可以使用它,并获得一个单一的 fetch 到 db...
  • 我认为这是要走的路——我不知道有更好的方法。我会看看我是否可以在某处找到记录的大小限制。如果存在,您可以将 id 划分为适当大小的块。如果已知您的 id 集合很小,您甚至可以忽略该问题。我相信限制是 512 或 1024...
  • 嗯,在任何地方都找不到记录的上限。也许我弄错了,它只适用于 Linq2SQL。我会投票支持任何记录(可能缺少)该上限的人:-)
  • EF 没有限制,因为它将其转换为非参数化的 IN 表达式 (column IN (val1, val2, val3, ...)),而 LINQ-to-SQL 参数化所有 IN 值 (column IN (@p1, @p2, @p3, ...)),因此您点击2100 参数限制很快。
【解决方案2】:

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

在实体框架中使用Contains其实很慢。确实,它转换为 SQL 中的IN 子句,并且 SQL 查询本身执行得很快。但问题和性能瓶颈在于将 LINQ 查询转换为 SQL。将要创建的表达式树扩展为一长串OR 连接,因为没有代表IN 的本机表达式。创建 SQL 时,许多ORs 的表达式被识别并折叠回SQL IN 子句中。

这并不意味着使用Contains 比在ids 集合(您的第一个选项)中为每个元素发出一个查询更糟糕。它可能仍然更好 - 至少对于不太大的收藏。但对于大型收藏品来说,这真的很糟糕。我记得前段时间我测试了一个包含大约 12.000 个元素的 Contains 查询,尽管 SQL 中的查询在不到一秒的时间内执行,但它仍然有效但花了大约一分钟。

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

此方法以及将Contains 与实体框架一起使用的限制在此处显示和解释:

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

在这种情况下,原始 SQL 命令可能会执行得最好,这意味着您调用 dbContext.Database.SqlQuery&lt;Image&gt;(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 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&lt;MyEntity&gt;(sql)context.Set&lt;MyEntity&gt;().SqlQuery(sql).AsNoTracking()是一样的,所以预计Test 4和Test 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 可以将时间缩短一到两秒。

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

【讨论】:

  • 我开始认为,理想情况下,从性能的角度来看,这种类型的查询是应该避免的。
  • @Tom:我做了一些测试,请参阅我的编辑。
  • @Tom:我已经完成了第 6 次测试,请参阅我的编辑 2。是的,首先避免这种情况似乎是一个好策略。但有时您可能只需要这样的查询并且无法绕过它。我的结论更多:有时使用原始 SQL 是有意义的,甚至出于性能原因,对于像实体框架这样的 ORM 是必须的。
  • @Tom:现在为你的问题 +1,我忘了。因为我学到了很多东西,它帮助我发现了自己的应用程序中的严重性能瓶颈。谢谢你的问题:)
  • 很好的答案和研究。一个超级小注释,但您可以在测试 3 中像这样在一行中构建 ID 列表:string values = string.Join(", ", ids);
【解决方案3】:

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

return db.PERSON.Find(id);

而不是:

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

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

【讨论】:

【解决方案4】:

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

这不是一个漂亮的方法,但对于大型列表来说,它非常有表现力。

【讨论】:

  • 如果多个用户使用不同的选择集访问相同的代码块,这将如何影响[可能来自网络浏览器]
  • @Sebastian 实际上它可能是一团糟,但在您的情况下,您可以在表名或其他标识符中连接会话 ID,例如 string tempTableName = "@Temp_" + HttpContext.Session.SessionID;
【解决方案5】:

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

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

【讨论】:

  • 抱歉,这没有意义。 Images.toArray()整个 Images 表拉入内存。你不能这么认真。此外,尚不清楚第一行在那里做什么。
【解决方案6】:
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();

【讨论】:

    猜你喜欢
    • 2017-11-16
    • 1970-01-01
    • 2011-10-08
    • 2011-06-23
    • 1970-01-01
    • 2021-09-28
    • 2011-07-03
    • 2021-09-16
    • 1970-01-01
    相关资源
    最近更新 更多