Should the order of LINQ query clauses affect Entity Framework performance?
我正在使用Entity Framework(代码优先),并发现我在LINQ查询中指定子句的顺序会对性能产生巨大影响,因此例如:
1 2 3 4 5 6 7 | 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(); } |
(稀有)color子句位于(common)size子句之前,它很快,但是反过来又慢了几个数量级。该表有两百万行,所讨论的两个字段为nvarchar(50),因此未进行规范化,但它们均已索引。这些字段以代码优先方式指定,如下所示:
1 2 3 4 5 | [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,然后使用以下方式生成代码优先样式:
1 2 3 4 5 6 7 8 9 10 11 | public class Widget { [Key] public int WidgetId { get; set; } [StringLength(50)] public string Size { get; set; } [StringLength(50)] public string Colour { get; set; } } |
1 2 3 4 5 6 7 8 9 | public class MyDbContext : DbContext { public MyDbContext() : base("DefaultConnection") { } public DbSet<Widget> Widgets { get; set; } } |
使用以下SQL生成虚拟数据:
1 2 3 4 5 6 7 | 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 |
为颜色和大小各添加一个索引,然后使用以下查询:
1 2 3 4 5 6 7 8 9 10 | 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比较的钝集合有关,如下所示。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | 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位)上的干净数据库中重现此问题,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | 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慢得多)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | 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' |
与我一样,您可能还会发现,如果您重新运行虚拟数据插入以使现在有200万行,问题就会消失。
问题的核心不是"为什么订单对LINQ至关重要?"。 LINQ只是按字面意思翻译而无需重新排序。真正的问题是"为什么两个SQL查询的性能不同?"。
我仅通过插入10万行就能重现该问题。在那种情况下,将触发优化器中的一个弱点:由于条件复杂,它无法识别出它可以对
这没有语义上的原因。即使在
EF在这里尝试提供帮助,因为它假定列和过滤器变量都可以为null。在这种情况下,它会尝试为您提供匹配项(根据C#语义这是正确的选择)。
我尝试通过添加以下过滤器来撤消该操作:
希望优化器现在使用该知识来简化复杂的EF过滤器表达式。它没有这样做。如果此方法有效,则可以将相同的过滤器添加到EF查询中,以提供简单的解决方法。
以下是我按照应尝试的顺序推荐的修复程序:
所有这些都是解决方法,而不是根本原因修复。
最后,我对这里的SQL Server和EF都不满意。两种产品都应固定。 las,他们可能不会,您也等不及了。
以下是索引脚本:
1 2 3 4 5 6 7 8 | 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如此行事的原因有更好的了解。
简短答案:此问题的最佳解决方法是在DbContext实例上设置此标志:
1 | context.Configuration.UseDatabaseNullSemantics = true; |
执行此操作后,所有多余的null检查都将消失,并且如果受此问题的影响,您的查询应会执行得更快。
长答案:这个线程中的其他人都说得对,在EF6中,我们默认引入了额外的空检查项,以补偿数据库中空比较(三值逻辑)与标准内存中空比较的??语义之间的差异比较。目的是满足以下非常受欢迎的要求:
\\'where \\'子句中对空变量的错误处理
保罗·怀特(Paul White)也很正确,在下面的表达式中," AND AND"部分在补偿三值逻辑时不太常见:
在一般情况下,需要额外的条件以防止整个表达式的结果为NULL,例如假设x = 1且y = NULL。然后
如果比较表达式在查询表达式的组成部分的后面取反,则NULL和false之间的区别很重要,例如:
1 2 | NOT (false) --> true NOT (NULL) --> NULL |
我们的确可以将smarts添加到EF中,以找出何时不需要此多余的术语(例如,如果我们知道查询的谓词中没有否定表达式)并对其进行优化查询。
顺便说一下,我们正在代码复合体的以下EF错误中跟踪此问题:
[性能]在C#空比较语义的情况下,减少复杂查询的表达式树
Linq-to-SQL将为您的Linq代码生成等效的SQL查询。这意味着它将按照您指定的顺序进行过滤。如果没有运行测试,它真的没有办法知道哪个会更快。
无论哪种方式,您的第一次过滤都将在整个数据集上进行,因此会很慢。但是...
- 如果您首先根据稀有条件进行过滤,则可以将整个表格缩减为一小组结果。然后,您的第二个过滤操作只有一小部分可以进行,不需要很长时间。
- 如果先对常见条件进行过滤,那么之后剩下的数据集仍然很大。因此,第二个过滤对大量数据进行操作,因此需要更长的时间。
因此,稀有优先意味着慢速快,而通用优先意味着慢速慢。 Linq-to-SQL为您优化这种区别的唯一方法是,首先进行查询以检查这两个条件中哪一个比较少见,但这意味着生成的SQL每次运行都会有所不同(并且因此无法被缓存以加快速度),或者比您在Linq中编写的内容(Linq-to-SQL设计人员所不希望的)要复杂得多,这可能是因为它会使用户调试成为噩梦)。
没有什么可以阻止您自己进行此优化;预先添加一个查询以进行计数,并查看两个过滤器中的哪个将产生较小的结果集,以供第二个过滤器使用。对于小型数据库,这几乎在每种情况下都会变慢,因为您要进行额外的查询,但是如果数据库足够大且检查查询很聪明,则平均结果可能会更快。同样,可能有可能要计算出条件A多少个条件才能使其更快,而不管您有多少条件B对象,然后只计算条件A,这将有助于使检查查询更快。
在调整SQL查询时,过滤结果的顺序当然很重要。为什么您希望Linq-to-SQL永远不会受到过滤顺序的影响?