LINQ查询子句的顺序是否会影响entity framework的性能?

我正在使用Entity Framework(代码优先),并且在我的LINQ查询中查找指定子句的顺序会对性能产生巨大影响,例如:

using (var db = new MyDbContext()) { var mySize = "medium"; var myColour = "vermilion"; var list1 = db.Widgets.Where(x => x.Colour == myColour && x.Size == mySize).ToList(); var list2 = db.Widgets.Where(x => x.Size == mySize && x.Colour == myColour).ToList(); } 

如果(罕见)颜色子句在(通用)大小子句之前,它的速度很快,但反过来它的速度要慢一些。 该表有几百万行,所讨论的两个字段是nvarchar(50),因此没有标准化,但它们都是索引的。 这些字段以代码优先方式指定,如下所示:

  [StringLength(50)] public string Colour { get; set; } [StringLength(50)] public string Size { get; set; } 

我真的应该在我的LINQ查询中担心这些事情,我认为那是数据库的工作吗?

系统规格如下:

  • Visual Studio 2010
  • .NET 4
  • EntityFramework 6.0.0-beta1
  • SQL Server 2008 R2 Web(64位)

更新:

对于任何惩罚的贪婪,效果可以如下复制。 这个问题似乎对许多因素非常敏感,所以请关注其中某些因素的人为性质:

通过nuget安装EntityFramework 6.0.0-beta1,然后生成代码第一个样式:

 public class Widget { [Key] public int WidgetId { get; set; } [StringLength(50)] public string Size { get; set; } [StringLength(50)] public string Colour { get; set; } } 

 public class MyDbContext : DbContext { public MyDbContext() : base("DefaultConnection") { } public DbSet Widgets { get; set; } } 

使用以下SQL生成虚拟数据:


 insert into gadget (Size, Colour) select RND1 + ' is the name is this size' as Size, RND2 + ' is the name of this colour' as Colour from (Select top 1000000 CAST(abs(Checksum(NewId())) % 100 as varchar) As RND1, CAST(abs(Checksum(NewId())) % 10000 as varchar) As RND2 from master..spt_values t1 cross join master..spt_values t2) t3 

为Color和Size添加一个索引,然后查询:


 string mySize = "99 is the name is this size"; string myColour = "9999 is the name of this colour"; using (var db = new WebDbContext()) { var list1= db.Widgets.Where(x => x.Colour == myColour && x.Size == mySize).ToList(); } using (var db = new WebDbContext()) { var list2 = db.Widgets.Where(x => x.Size == mySize && x.Colour == myColour).ToList(); } 

该问题似乎与生成的SQL中的NULL比较的钝性集合有关,如下所示。

 exec sp_executesql N'SELECT [Extent1].[WidgetId] AS [WidgetId], [Extent1].[Size] AS [Size], [Extent1].[Colour] AS [Colour] FROM [dbo].[Widget] AS [Extent1] WHERE ((([Extent1].[Size] = @p__linq__0) AND ( NOT ([Extent1].[Size] IS NULL OR @p__linq__0 IS NULL))) OR (([Extent1].[Size] IS NULL) AND (@p__linq__0 IS NULL))) AND ((([Extent1].[Colour] = @p__linq__1) AND ( NOT ([Extent1].[Colour] IS NULL OR @p__linq__1 IS NULL))) OR (([Extent1].[Colour] IS NULL) AND (@p__linq__1 IS NULL)))',N'@p__linq__0 nvarchar(4000),@p__linq__1 nvarchar(4000)', @p__linq__0=N'99 is the name is this size', @p__linq__1=N'9999 is the name of this colour' go 

将LINQ中的相等运算符更改为StartWith()会使问题消失,将两个字段中的任何一个更改为在数据库中不可为空也是如此。

我绝望了!

更新2:

对于任何赏金猎人的一些帮助,该问题可以在干净的数据库中在SQL Server 2008 R2 Web(64位)上重现,如下所示:

 CREATE TABLE [dbo].[Widget]( [WidgetId] [int] IDENTITY(1,1) NOT NULL, [Size] [nvarchar](50) NULL, [Colour] [nvarchar](50) NULL, CONSTRAINT [PK_dbo.Widget] PRIMARY KEY CLUSTERED ( [WidgetId] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] ) ON [PRIMARY] GO CREATE NONCLUSTERED INDEX IX_Widget_Size ON dbo.Widget ( Size ) WITH( STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] GO CREATE NONCLUSTERED INDEX IX_Widget_Colour ON dbo.Widget ( Colour ) WITH( STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] GO insert into Widget (Size, Colour) select RND1 + ' is the name is this size' as Size, RND2 + ' is the name of this colour' as Colour from (Select top 1000000 CAST(abs(Checksum(NewId())) % 100 as varchar) As RND1, CAST(abs(Checksum(NewId())) % 10000 as varchar) As RND2 from master..spt_values t1 cross join master..spt_values t2) t3 GO 

然后比较以下两个查询的相对性能(您可能需要调整参数测试值以获得一个返回几行的查询,以便观察效果,​​即第二个查询ID慢得多)。

 exec sp_executesql N'SELECT [Extent1].[WidgetId] AS [WidgetId], [Extent1].[Size] AS [Size], [Extent1].[Colour] AS [Colour] FROM [dbo].[Widget] AS [Extent1] WHERE ((([Extent1].[Colour] = @p__linq__0) AND ( NOT ([Extent1].[Colour] IS NULL OR @p__linq__0 IS NULL))) OR (([Extent1].[Colour] IS NULL) AND (@p__linq__0 IS NULL))) AND ((([Extent1].[Size] = @p__linq__1) AND ( NOT ([Extent1].[Size] IS NULL OR @p__linq__1 IS NULL))) OR (([Extent1].[Size] IS NULL) AND (@p__linq__1 IS NULL)))', N'@p__linq__0 nvarchar(4000),@p__linq__1 nvarchar(4000)', @p__linq__0=N'9999 is the name of this colour', @p__linq__1=N'99 is the name is this size' go exec sp_executesql N'SELECT [Extent1].[WidgetId] AS [WidgetId], [Extent1].[Size] AS [Size], [Extent1].[Colour] AS [Colour] FROM [dbo].[Widget] AS [Extent1] WHERE ((([Extent1].[Size] = @p__linq__0) AND ( NOT ([Extent1].[Size] IS NULL OR @p__linq__0 IS NULL))) OR (([Extent1].[Size] IS NULL) AND (@p__linq__0 IS NULL))) AND ((([Extent1].[Colour] = @p__linq__1) AND ( NOT ([Extent1].[Colour] IS NULL OR @p__linq__1 IS NULL))) OR (([Extent1].[Colour] IS NULL) AND (@p__linq__1 IS NULL)))', N'@p__linq__0 nvarchar(4000),@p__linq__1 nvarchar(4000)', @p__linq__0=N'99 is the name is this size', @p__linq__1=N'9999 is the name of this colour' 

您也可以像我一样找到,如果重新运行虚拟数据插入,以便现在有两百万行,问题就会消失。

问题的核心不是“为什么订单与LINQ有关?”。 LINQ只是按字面翻译而无需重新排序。 真正的问题是“为什么两个SQL查询具有不同的性能?”。

我只能插入100k行来重现问题。 在这种情况下,优化器中的弱点被触发:由于复杂的条件,它无法识别它可以对Colour进行搜索。 在第一个查询中,优化器确实识别模式并创建索引搜索。

这应该是没有语义原因的。 即使在寻找NULL时,也可以寻找索引。 这是优化器中的弱点/错误。 以下是两个计划:

在此处输入图像描述

EF尝试在这里提供帮助,因为它假定列和filter变量都可以为空。 在这种情况下,它会尝试给你一个匹配(根据C#语义是正确的)。

我尝试通过添加以下filter来撤消它:

 Colour IS NOT NULL AND @p__linq__0 IS NOT NULL AND Size IS NOT NULL AND @p__linq__1 IS NOT NULL 

希望优化器现在使用该知识来简化复杂的EFfilter表达式。 它没有设法这样做。 如果这有效,可以将相同的filter添加到EF查询中,从而提供简单的修复。

以下是我建议按照您应该尝试的顺序修复:

  1. 使数据库列在数据库中不为空
  2. 在EF数据模型中使列不为空,希望这会阻止EF创建复杂的过滤条件
  3. 创建索引: Colour, Size和/或Size, Colour 。 他们也删除了他们的问题。
  4. 确保按正确的顺序完成过滤并留下代码注释
  5. 尝试使用INTERSECT / Queryable.Intersect来组合filter。 这通常会导致不同的计划形状。
  6. 创建一个用于执行过滤的内联表值函数。 EF可以将此类函数用作更大查询的一部分
  7. 下拉到原始SQL
  8. 使用计划指南更改计划

所有这些都是解决方法,而不是根本原因修复。

最后,我对SQL Server和EF都不满意。 两种产品都应该是固定的。 唉,他们可能不会,你也不能等待。

以下是索引脚本:

 CREATE NONCLUSTERED INDEX IX_Widget_Colour_Size ON dbo.Widget ( Colour, Size ) WITH( STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] CREATE NONCLUSTERED INDEX IX_Widget_Size_Colour ON dbo.Widget ( Size, Colour ) WITH( STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] 

很难知道这个问题是否是由运行EF beta引起的,但是从SQL Server查询优化器的角度来看,为简单的LINQ查询生成的SQL出乎意料地无用。

用于处理可空的相等比较的常见SQL构造确实具有特定的SQL Server查询优化器支持。 一个(众多)兼容的SQL查询forms是:

 (x = y) OR (x IS NULL AND y IS NULL) 

EF生成的SQL改为遵循以下模式:

 ((x = y) AND NOT (x IS NULL OR y IS NULL)) OR (x IS NULL AND y IS NULL) 

额外的AND NOT子句是多余的; 但它只是不透明,以避免匹配关键的查询优化器简化。 具有讽刺意味的是,查询优化器中的许多简化规则纯粹是为了支持生成的代码。

如果EF生成了与第一个SQL表单匹配的代码(例如):

 SELECT W.WidgetId, W.Size, W.Colour FROM dbo.Widget AS W WHERE ( (W.Size = @p__linq__0) OR (W.Size IS NULL AND @p__linq__0 IS NULL) ) AND ( (W.Colour = @p__linq__1) OR (W.Colour IS NULL AND @p__linq__1 IS NULL) ); 

…执行计划将是一个简单的索引交集,在每次搜索时使用单个NULL -aware相等谓词:

指数交叉计划

如果WHERE子句中主谓词集的顺序颠倒,则会生成相同的有效计划。

在这种情况下,通过将数据库中的列和/或EF代码声明为NOT NULL (假设数据实际上不包含NULLs )可以避免问题,但这并不会改变简单的可空列比较应该是的事实。处理得更好。

如果EF和SQL Server团队在此协同工作,则EF将以更优化器友好的forms生成代码,或者将此语法的特定支持添加到SQL Server优化器中。 请注意,在SQL中编写相同的逻辑需求总会有更多的方法,而不是通用数据库优化器可以(或应该)预期的方法。 首先,它不可能预测每一种可能的结构,而另一种情况则是你不喜欢计划编译时间。

我应该提到最终的SQL Serverfunction,因为代码生成器通常似乎不支持它是一种遗憾。 在使用OPTION (RECOMPILE)查询提示在每次执行时标记生成的查询以进行重新编译的情况下,查询优化器可以使用参数嵌入优化来为特定参数值生成一次性查询计划。

这是一个强大的function,能够在优化之前完全删除或重写查询的不必要部分。 例如,只需将查询提示添加到原始生成的SQL:

 SELECT [Extent1].[WidgetId] AS [WidgetId], [Extent1].[Size] AS [Size], [Extent1].[Colour] AS [Colour] FROM [dbo].[Widget] AS [Extent1] WHERE ((([Extent1].[Size] = @p__linq__0) AND ( NOT ([Extent1].[Size] IS NULL OR @p__linq__0 IS NULL))) OR (([Extent1].[Size] IS NULL) AND (@p__linq__0 IS NULL))) AND ((([Extent1].[Colour] = @p__linq__1) AND ( NOT ([Extent1].[Colour] IS NULL OR @p__linq__1 IS NULL))) OR (([Extent1].[Colour] IS NULL) AND (@p__linq__1 IS NULL))) OPTION (RECOMPILE); 

…根据特定参数值生成最佳计划:

重新编译计划

每次执行都要为计划编制付出代价,但改进的计划质量往往会多次回报这个成本。 如果一个好的计划敏感地依赖于传递的参数值,这可能是一种非常有效的技术。

注意:在其他人已经提供了一般正确的答案后很久就遇到了这个问题。 我决定将此作为单独的答案发布,因为我认为解决方法可能会有所帮助,并且因为您可能希望更好地了解EF的行为方式。

简短回答:此问题的最佳解决方法是在DbContext实例上设置此标志:

 context.Configuration.UseDatabaseNullSemantics = true; 

当您执行此操作时,所有额外的空值检查将消失,如果受此问题的影响,您的查询应该执行得更快。

答案很长:这个线程中的其他人是正确的,在EF6中我们默认引入了额外的空检查项,以补偿数据库中的空比较语义( 三值逻辑 )和标准的内存空比较之间的差异。 这样做的目的是满足以下非常受欢迎的要求:

‘where’子句中的空变量处理不正确

Paul White也是正确的,在下面的表达式中,’AND NOT’部分在补偿三值逻辑方面不常见:

 ((x = y) AND NOT (x IS NULL OR y IS NULL)) OR (x IS NULL AND y IS NULL) 

在一般情况下,该额外条件是必要的,以防止整个表达式的结果为NULL,例如假设x = 1且y = NULL。 然后

 (x = y) --> NULL (x IS NULL AND y IS NULL) --> false NULL OR false --> NULL 

如果比较表达式在查询表达式的组合中稍后被否定,则NULL和false之间的区别很重要,例如:

 NOT (false) --> true NOT (NULL) --> NULL 

确实,我们可以将智能添加到EF以确定何时不需要这个额外的术语(例如,如果我们知道表达式在查询的谓词中没有被否定)并且优化它在查询之外。

顺便说一下,我们在codeplex中的以下EF错误中跟踪此问题:

[Performance]在C#null比较语义的情况下,减少复杂查询的表达式树

Linq-to-SQL将为您的Linq代码生成等效的SQL查询。 这意味着它将按您指定的顺序进行过滤。 如果没有运行它来测试它,它真的没有办法知道哪个会更快。

无论哪种方式,您的第一个过滤将在整个数据集上运行,因此会很慢。 然而…

  • 如果您首先过滤罕见情况,那么它可以将整个表格缩小到一小组结果。 然后你的第二个过滤只有一小部分可以处理,这不需要很长时间。
  • 如果首先过滤常见条件,那么之后留下的数据集仍然非常大。 因此,第二次过滤对大量数据进行操作,因此需要更长的时间。

因此,罕见的首先意味着慢+快,而普通的首先意味着慢+慢。 Linq-to-SQL为您优化这种区别的唯一方法是首先进行查询以检查这两个条件中的哪一个是罕见的,但这意味着每次运行时生成的SQL都会有所不同(和因此无法缓存以加快速度)或者比你在Linq中编写的内容要复杂得多(Linq-to-SQL设计者不想要,可能是因为它可能使调试成为用户的噩梦)。

没有什么可以阻止你自己做这个优化; 事先添加一个查询来计算并查看两个filter中的哪一个将为第二个filter生成较小的结果集。 对于小型数据库,几乎在所有情况下这都会变慢,因为您正在进行一个额外的查询,但如果您的数据库足够大并且您的检查查询很聪明,那么它的平均速度可能会更快。 此外,有可能计算出条件A必须有多少才能更快,无论你有多少条件B对象,然后只计算条件A,这将有助于使检查查询更快。

在调优SQL查询时,您过滤结果的顺序肯定很重要。为什么您希望Linq-to-SQL永远不会受到过滤顺序的影响?