What's the most efficient way to select multiple entities by primary key?

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?

}

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

推荐答案

UPDATE: With the addition of InExpression in EF6, the performance of processing Enumerable.Contains improved dramatically. The analysis in this answer is great but largely obsolete since 2013.

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

这并不意味着使用Contains比对ids集合中的每个元素发出一个查询更糟糕(您的第一个 Select ).它可能仍然更好-至少对于不太大的Collection 品来说是这样.但对于大型Collection 品来说,这真的很糟糕.我记得不久前我测试过一个包含大约12.000个元素的Contains%的查询,它可以工作,但花费了大约一分钟的时间,即使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.

Edit

以下是一些测量数据:

我在一个包含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;
}

Test 1

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

Result -> msec = 85.5 sec

Test 2

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

Result -> msec = 84.5 sec

AsNoTracking的这种微小效应是非常不寻常的.这表明瓶颈不是对象materialized (也不是如下所示的SQL).

对于这两个测试,可以在SQL Profiler中看到SQL查询很晚到达数据库.(我没有精确测量,但超过了70秒.)显然,将这个LINQ查询转换为SQL非常昂贵.

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

Result -> msec = 5.1 sec

Test 4

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

Result -> msec = 3.8 sec

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

Test 5

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

Result -> msec = 3.7 sec

我的理解是context.Database.SqlQuery<MyEntity>(sql)context.Set<MyEntity>().SqlQuery(sql).AsNoTracking()是一样的,所以测试4和测试5之间没有预期的差异.

(由于随机id Select 后可能出现重复,结果集的长度并不总是相同的,但它始终在19600到19640个元素之间.)

Edit 2

Test 6

即使是20000次往返数据库也比使用Contains次要快:

var result = new List<MyEntity>();
foreach (var id in ids)
    result.Add(context.Set<MyEntity>().SingleOrDefault(e => e.ID == id));

Result -> msec = 73.6 sec

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

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

.net相关问答推荐

使用CLR将数据从Excel导入SQL Server时出错

Dotnet 反射:使用 F# 中的out参数调用 MethodInfo 上的调用

使用 Powershell TOM 在 SSAS 表格中创建分区

如何找到windows服务exe路径

图像 UriSource 和数据绑定

为什么我得到 411 Length required 错误?

C# 的部分类是糟糕的设计吗?

找不到 Microsoft.Office.Interop Visual Studio

重新启动(回收)应用程序池

如何在 C# 中直接执行 SQL 查询?

在 .NET (C#) 中本地存储数据的最佳方式

String.Format - 它是如何工作的以及如何实现自定义格式字符串

是否有 Linq 方法可以将单个项目添加到 IEnumerable

Find() 和 First() 抛出异常,如何改为返回 null?

log4net的正确使用方法(记录器命名)

如何从文件中删除单个属性(例如只读)?

是否可以判断对象是否已附加到实体框架中的数据上下文?

为什么 !0 是 Microsoft 中间语言 (MSIL) 中的一种类型?

Windows 服务在哪个目录中运行?

嵌套捕获组如何在正则表达式中编号?