更新3:根据this announcement,EF团队在EF6 alpha 2中已经解决了这个问题。
更新2:我已经提出修复此问题的建议。投票赞成,go here。
考虑一个带有一个非常简单的表的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列表。
答案 0 :(得分:65)
更新:通过在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远远超过我们预期客户的元素数量将传递给Enumerable.Contains。也就是说,我知道这是一个烦恼,表达式树的操作会使你的特定场景中的东西太贵。
我和我们的一位开发人员讨论了这个问题,我们相信将来我们可以通过增加对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);
}
}
}
}
希望这有帮助!
答案 1 :(得分:24)
如果您发现性能问题阻碍了您,请不要花费多少时间来解决它,因为您很可能不会成功,您必须直接与MS沟通(如果您有高级支持)这需要很长时间。
如果出现性能问题,请使用变通方法和解决方法,EF表示直接SQL。这没什么不好的。全球的想法,使用EF =不再使用SQL是一个谎言。您有SQL Server 2008 R2所以:
Include
逻辑SqlDataReader
获取结果并构建实体如果性能对您至关重要,您将无法找到更好的解决方案。 EF无法映射和执行此过程,因为当前版本不支持表值参数或多个结果集。
答案 2 :(得分:9)
我们能够通过添加中间表并从需要使用Contains子句的LINQ查询连接该表来解决EF Contains问题。通过这种方法我们得到了惊人的结果。我们有一个大的EF模型,并且在预编译EF查询时不允许使用“Contains”,对于使用“Contains”子句的查询,我们的性能非常差。
概述:
在SQL Server中创建一个表 - 例如HelperForContainsOfIntType
HelperID
Guid
数据类型和ReferenceID
int
数据类型列。根据需要使用不同数据类型的ReferenceID创建不同的表。
为HelperForContainsOfIntType
和EF模型中的其他此类表创建实体/实体集。根据需要为不同的数据类型创建不同的Entity / EntitySet。
在.NET代码中创建一个辅助方法,该方法接受IEnumerable<int>
的输入并返回Guid
。此方法生成新的Guid
,并将IEnumerable<int>
中的值与HelperForContainsOfIntType
一起插入生成的Guid
。接下来,该方法将新生成的Guid
返回给调用者。要快速插入HelperForContainsOfIntType
表,请创建一个存储过程,该过程接受值列表的输入并进行插入。见Table-Valued Parameters in SQL Server 2008 (ADO.NET)。为不同的数据类型创建不同的帮助程序,或创建一个通用的帮助程序方法来处理不同的数据类型。
创建一个EF编译的查询,类似于下面的内容:
static Func<MyEntities, Guid, IEnumerable<Customer>> _selectCustomers =
CompiledQuery.Compile(
(MyEntities db, Guid containsHelperID) =>
from cust in db.Customers
join x in db.HelperForContainsOfIntType on cust.CustomerID equals x.ReferenceID where x.HelperID == containsHelperID
select cust
);
使用要在Contains
子句中使用的值调用helper方法,并获取要在查询中使用的Guid
。例如:
var containsHelperID = dbHelper.InsertIntoHelperForContainsOfIntType(new int[] { 1, 2, 3 });
var result = _selectCustomers(_dbContext, containsHelperID).ToList();
答案 3 :(得分:5)
编辑我的原始答案 - 根据实体的复杂程度,可能有一种解决方法。如果您知道EF生成的sql以填充您的实体,则可以使用DbContext.Database.SqlQuery直接执行它。在EF 4中,我认为你可以使用ObjectContext.ExecuteStoreQuery,但我没有尝试过。
例如,使用下面原始答案中的代码使用StringBuilder
生成sql语句,我能够执行以下操作
var rows = db.Database.SqlQuery<Main>(sql).ToArray();
总时间从大约26秒到0.5秒。
我会是第一个说它丑陋的人,希望有一个更好的解决方案。
经过一番思考之后,我意识到如果你使用联接来过滤你的结果,EF就不必构建那么长的id列表。这可能很复杂,具体取决于并发查询的数量,但我相信您可以使用用户ID或会话ID来隔离它们。
为了测试这一点,我创建了一个Target
表,其架构与Main
相同。然后,我使用StringBuilder
创建INSERT
命令,以1,000个批量填充Target
表,因为这是SQL Server在单个INSERT
中接受的最多。直接执行sql语句要比通过EF(大约0.3秒对2.5秒)快得多,我相信也可以,因为表模式不应该改变。
最后,使用join
进行选择会导致更简单的查询,并在不到0.5秒的时间内执行。
ExecuteStoreCommand("DELETE Target");
var ids = Main.Select(a => a.Id).ToArray();
var sb = new StringBuilder();
for (int i = 0; i < 10; i++)
{
sb.Append("INSERT INTO Target(Id) VALUES (");
for (int j = 1; j <= 1000; j++)
{
if (j > 1)
{
sb.Append(",(");
}
sb.Append(i * 1000 + j);
sb.Append(")");
}
ExecuteStoreCommand(sb.ToString());
sb.Clear();
}
var rows = (from m in Main
join t in Target on m.Id equals t.Id
select m).ToArray();
rows.Length.Dump();
由EF为连接生成的sql:
SELECT
[Extent1].[Id] AS [Id]
FROM [dbo].[Main] AS [Extent1]
INNER JOIN [dbo].[Target] AS [Extent2] ON [Extent1].[Id] = [Extent2].[Id]
(原始答案)
这不是一个答案,但我想分享一些额外的信息,而且评论太长了。我能够重现你的结果,还有一些其他的东西需要补充:
SQL事件探查器显示执行第一个查询(Main.Select
)和第二个Main.Where
查询之间的延迟,因此我怀疑问题是生成并发送该大小的查询(48,980字节) )。
但是,在T-SQL中动态构建相同的sql语句需要不到1秒的时间,并从ids
语句中获取Main.Select
,构建相同的sql语句并使用{{执行它花了0.112秒,这包括将内容写入控制台的时间。
此时,我怀疑EF在构建查询时为10,000 SqlCommand
中的每一个进行了一些分析/处理。希望我能提供明确的答案和解决方案:(。
这是我在SSMS和LINQPad中尝试过的代码(请不要过于苛刻地批评,我急于尝试离开工作):
ids
declare @sql nvarchar(max)
set @sql = 'SELECT
[Extent1].[Id] AS [Id]
FROM [dbo].[Main] AS [Extent1]
WHERE [Extent1].[Id] IN ('
declare @count int = 0
while @count < 10000
begin
if @count > 0 set @sql = @sql + ','
set @count = @count + 1
set @sql = @sql + cast(@count as nvarchar)
end
set @sql = @sql + ')'
exec(@sql)
答案 4 :(得分:5)
我对实体框架并不熟悉,但如果您执行以下操作,性能会更好吗?
而不是:
var ids = Main.Select(a => a.Id).ToArray();
var rows = Main.Where (a => ids.Contains(a.Id)).ToArray();
这个怎么样(假设ID是一个int):
var ids = new HashSet<int>(Main.Select(a => a.Id));
var rows = Main.Where (a => ids.Contains(a.Id)).ToArray();
答案 5 :(得分:3)
已在实体框架6 Alpha 2上修复:http://entityframework.codeplex.com/SourceControl/changeset/a7b70f69e551
http://blogs.msdn.com/b/adonet/archive/2012/12/10/ef6-alpha-2-available-on-nuget.aspx
答案 6 :(得分:2)
包含的可缓存替代?
这只是我的位置,所以我在实体框架功能建议链接中添加了两个便士。
问题肯定是在生成SQL时。我有一个客户端,查询生成的数据是4秒,但执行时间为0.1秒。
我注意到在使用dynamic LINQ和OR时,sql生成的时间也一样长,但它生成的内容可能是缓存。因此,当再次执行它时,它降至0.2秒。
请注意,仍然会生成SQL in。
如果您可以忍受最初的命中,那么您需要考虑的其他因素,您的数组计数变化不大,并且运行查询很多。 (在LINQ Pad中测试)
答案 7 :(得分:1)
问题在于Entity Framework的SQL生成。如果其中一个参数是列表,则无法缓存查询。
要让EF缓存您的查询,您可以将列表转换为字符串并对字符串执行.Contains。
因此,例如,由于EF可以缓存查询,因此此代码运行得更快:
var ids = Main.Select(a => a.Id).ToArray();
var idsString = "|" + String.Join("|", ids) + "|";
var rows = Main
.Where (a => idsString.Contains("|" + a.Id + "|"))
.ToArray();
当生成此查询时,它可能会使用Like而不是In生成,因此它会加速您的C#,但它可能会降低您的SQL速度。在我的情况下,我没有注意到我的SQL执行性能下降,并且C#的运行速度明显加快。