更新3:根据此公告,EF团队已在EF6 alpha 2中解决了此问题。
更新2:我已经提出了解决此问题的建议。要投票,请转到此处。
考虑一个带有一个非常简单的表的SQL数据库。
CREATE TABLE Main (Id INT PRIMARY KEY)
我用10,000条记录填充表。
WITH Numbers AS ( SELECT 1 AS Id UNION ALL SELECT Id + 1 AS Id FROM Numbers WHERE Id <= 10000 ) INSERT Main (Id) SELECT Id FROM Numbers OPTION (MAXRECURSION 0)
我为该表构建EF模型,并在LINQPad中运行以下查询(我使用的是“ C#语句”模式,因此LINQPad不会自动创建转储)。
var rows = Main .ToArray();
执行时间约为0.07秒。现在,我添加了Contains运算符并重新运行查询。
var ids = Main.Select(a => a.Id).ToArray(); var rows = Main .Where (a => ids.Contains(a.Id)) .ToArray();
这种情况下的执行时间为 20.14秒 (慢288倍)!
最初,我怀疑为查询发出的T-SQL需要花费更长的时间才能执行,因此我尝试将其从LINQPad的SQL窗格中剪切和粘贴到SQL Server Management Studio中。
SET NOCOUNT ON SET STATISTICS TIME ON SELECT [Extent1].[Id] AS [Id] FROM [dbo].[Primary] AS [Extent1] WHERE [Extent1].[Id] IN (1,2,3,4,5,6,7,8,...
结果是
SQL Server Execution Times: CPU time = 0 ms, elapsed time = 88 ms.
接下来,我怀疑是LINQPad引起了问题,但是无论是在LINQPad还是在控制台应用程序中运行,其性能都是相同的。
因此,看来问题出在实体框架内。
我在这里做错什么了吗?这是代码中时间紧迫的部分,因此我可以做些什么来提高性能吗?
我正在使用Entity Framework 4.1和Sql Server 2008 R2。
更新1:
在下面的讨论中,存在一些有关是否在EF构建初始查询时或在解析回接收到的数据时发生延迟的问题。为了对此进行测试,我运行了以下代码,
var ids = Main.Select(a => a.Id).ToArray(); var rows = (ObjectQuery<MainRow>) Main .Where (a => ids.Contains(a.Id)); var sql = rows.ToTraceString();
这将迫使EF生成查询而不对数据库执行查询。结果是此代码需要大约20秒的时间才能运行,因此似乎几乎所有时间都花在了构建初始查询上。
然后把CompiledQuery救出来?并不是那么快… CompiledQuery要求传递到查询中的参数必须是基本类型(int,string,float等)。它不接受数组或IEnumerable,所以我不能将其用于ID列表。
更新:通过在EF6中添加InExpression,处理Enumerable.Contains的性能得到了显着提高。 不再需要此答案中描述的方法。
没错,大多数时间都花在处理查询的翻译上。EF的提供程序模型当前不包含表示IN子句的表达式,因此ADO.NET提供程序无法原生支持IN。取而代之的是,Enumerable.Contains的实现将其转换为OR表达式的树,即对于C#中看起来像这样的东西:
new []{1, 2, 3, 4}.Contains(i)
…我们将生成一个DbExpression树,可以这样表示:
((1 = @i) OR (2 = @i)) OR ((3 = @i) OR (4 = @i))
(必须平衡表达式树,因为如果我们在单个长脊柱上具有所有OR,那么表达式访问者将有更多机会遇到堆栈溢出(是的,我们实际上在测试中就达到了))
稍后我们将这样的树发送给ADO.NET提供程序,该提供程序可以识别这种模式并将其减少为SQL生成期间的IN子句。
当我们增加对EF4中包含的Enumerable.Contains的支持时,我们认为这样做是很必要的,而不必在提供程序模型中引入对IN表达式的支持,说实话,10,000远远超过了我们预期客户将传递给的元素数量可枚举。就是说,我知道这很烦人,并且在特定情况下对表达式树的操作使事情变得太昂贵了。
我与一位开发人员讨论了此问题,我们相信将来我们可以通过为IN添加一流的支持来更改实现。我将确保将其添加到我们的待办事项列表中,但是鉴于我们还有很多其他改进要做,因此我无法保证何时才能实现。
对于该线程中已经建议的解决方法,我将添加以下内容:
考虑创建一种方法来平衡数据库往返次数与传递给Contains的元素数量之间的平衡。例如,在我自己的测试中,我观察到针对SQL Server的本地实例进行计算和执行时,具有100个元素的查询需要1/60秒的时间。如果您以这样的方式编写查询,即使用100个不同的ID集执行100个查询将为您提供与10,000个元素相同的查询结果,那么您可以在大约1.67秒而不是18秒的时间内得到结果。
根据查询和数据库连接的延迟,不同的块大小应更好地工作。对于某些查询,即,如果传递的序列重复,或者在嵌套条件下使用Enumerable.Contains,则可能会在结果中获取重复元素。
这是一个代码片段(很抱歉,如果用于将输入切成块的代码看起来太复杂了。实现相同功能的方法比较简单,但是我试图提出一种模式,该模式可以保留序列和我在LINQ中找不到类似的东西,所以我可能超出了这个部分:)):
用法:
var list = context.GetMainItems(ids).ToList();
上下文或存储库的方法:
public partial class ContainsTestEntities { public IEnumerable<Main> GetMainItems(IEnumerable<int> ids, int chunkSize = 100) { foreach (var chunk in ids.Chunk(chunkSize)) { var q = this.MainItems.Where(a => chunk.Contains(a.Id)); foreach (var item in q) { yield return item; } } } }
分割可枚举序列的扩展方法:
public static class EnumerableSlicing { private class Status { public bool EndOfSequence; } private static IEnumerable<T> TakeOnEnumerator<T>(IEnumerator<T> enumerator, int count, Status status) { while (--count > 0 && (enumerator.MoveNext() || !(status.EndOfSequence = true))) { yield return enumerator.Current; } } public static IEnumerable<IEnumerable<T>> Chunk<T>(this IEnumerable<T> items, int chunkSize) { if (chunkSize < 1) { throw new ArgumentException("Chunks should not be smaller than 1 element"); } var status = new Status { EndOfSequence = false }; using (var enumerator = items.GetEnumerator()) { while (!status.EndOfSequence) { yield return TakeOnEnumerator(enumerator, chunkSize, status); } } } }
希望这可以帮助!