LINQ 查询子句的顺序是不是会影响实体框架的性能?
Posted
技术标签:
【中文标题】LINQ 查询子句的顺序是不是会影响实体框架的性能?【英文标题】:Should the order of LINQ query clauses affect Entity Framework performance?LINQ 查询子句的顺序是否会影响实体框架的性能? 【发布时间】:2013-06-26 14:55:08 【问题描述】:我正在使用实体框架(代码优先)并且发现我在 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<Widget> 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
为颜色和尺寸各添加一个索引,然后查询:
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'
像我一样,您可能还会发现,如果您重新运行虚拟数据插入以使现在有 200 万行,问题就会消失。
【问题讨论】:
对这些列使用 nvarchar 似乎是一个糟糕的选择。 我希望生成的 SQL 中的顺序也颠倒过来。如果您直接在数据库上运行查询而不使用实体框架,那么顺序是否重要? 检查输出的 TSQL。检查查询计划。通常,查询优化器会找到最好的,但在某些情况下,TSQL 的顺序会影响查询计划,但对于 2 个简单的地方,这让我感到惊讶。 但重要的是 LINQ 解释。检查来自 LINQ 的 TSQL。 就赏金而言,很难比@PaulWhite 更可信 【参考方案1】:问题的核心不是“为什么顺序对 LINQ 很重要?”。 LINQ 只是按字面翻译而不重新排序。真正的问题是“为什么两个 SQL 查询的性能不同?”。
我只能通过插入 100k 行来重现该问题。在这种情况下,优化器中的一个弱点被触发:由于复杂的条件,它无法识别它可以在Colour
上进行搜索。在第一个查询中,优化器确实识别了该模式并创建了一个索引查找。
没有语义上的原因。即使在NULL
上搜索时,也可以在索引上搜索。这是优化器中的一个弱点/错误。以下是两个计划:
EF 在这里尝试提供帮助,因为它假定列和过滤器变量都可以为空。在这种情况下,它会尝试为您提供匹配项(根据 C# 语义,这是正确的)。
我尝试通过添加以下过滤器来撤消它:
Colour IS NOT NULL AND @p__linq__0 IS NOT NULL
AND Size IS NOT NULL AND @p__linq__1 IS NOT NULL
希望优化器现在使用这些知识来简化复杂的 EF 过滤器表达式。它没有做到这一点。如果这可行,则可以将相同的过滤器添加到 EF 查询中,从而提供简单的修复。
以下是我推荐的修复方法,您应该尝试它们:
-
使数据库中的数据库列不为空
在 EF 数据模型中使列不为空,希望这样可以防止 EF 创建复杂的过滤条件
创建索引:
Colour, Size
和/或Size, Colour
。他们也消除了他们的问题。
确保过滤以正确的顺序完成并留下代码注释
尝试使用INTERSECT
/Queryable.Intersect
组合过滤器。这通常会导致不同的计划形状。
创建一个执行过滤的内联表值函数。 EF 可以将这样的函数用作更大查询的一部分
下拉到原始 SQL
使用计划指南更改计划
所有这些都是变通方法,而不是根本原因修复。
最后我对这里的 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]
【讨论】:
Usr,非常感谢您抽出宝贵时间查看此问题并提供了如此出色的答案。这正是我所希望的。我很高兴我认为自己浪费在解决原始问题上的时间让我接受了有价值的教育。我迟迟没有回来以你的方式发送赏金,我看到你的答案在此期间被 Paul White 慷慨地增加了。保罗非常感谢您的学习贡献,如果有可能奖励您更多的赏金,我打算这样做。再次感谢大家。【参考方案2】:注意:在其他人已经提供了普遍正确的答案很久之后,我才遇到这个问题。我决定将此作为单独的答案发布,只是因为我认为解决方法可能会有所帮助,并且因为您可能希望更好地了解 EF 以这种方式运行的原因。
简答:解决此问题的最佳方法是在您的 DbContext 实例上设置此标志:
context.Configuration.UseDatabaseNullSemantics = true;
当您这样做时,所有额外的空检查都将消失,如果受此问题影响,您的查询应该会执行得更快。
长答案: 此线程中的其他人是正确的,在 EF6 中我们默认引入了额外的空值检查术语以补偿数据库中空值比较语义之间的差异 (three-valued logic)和标准的内存空比较。这样做的目的是满足以下非常受欢迎的要求:
Incorrect handling of null variables in 'where' clause
Paul White 也是正确的,在以下表达式中,“与非”部分在补偿三值逻辑时不太常见:
((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] Reduce the expression tree for complex queries in case of C# null comparison semantics
【讨论】:
context.Configuration.UseDatabaseNullSemantics = true 应该是默认值而不是例外。这会导致相当简单的查询经常忽略索引并在生产环境中导致死锁和超时。另一个典型的例子,微软的程序员就是不明白。您永远不会随意更改预期的默认功能!【参考方案3】:Linq-to-SQL 将为您的 Linq 代码生成等效的 SQL 查询。这意味着它将按照您指定的顺序进行过滤。如果不运行它进行测试,它真的无法知道哪个会更快。
无论哪种方式,您的第一次过滤都将在整个数据集上运行,因此会很慢。不过……
如果您首先过滤稀有条件,那么它可以将整个表格缩减为一小部分结果。然后您的第二个过滤器只有一小部分需要处理,这不会花费很长时间。 如果先对常见条件进行过滤,那么后面留下的数据集还是比较大的。因此,第二次过滤对大量数据进行操作,因此需要的时间稍长。所以,稀有优先意味着慢+快,而常见优先意味着慢+慢。 Linq-to-SQL 为您优化这种区别的唯一方法是首先进行查询以检查这两个条件中哪一个更罕见,但这意味着生成的 SQL 每次运行时都会有所不同(并且因此无法缓存以加快速度)或者比您在 Linq 中编写的内容要复杂得多(Linq-to-SQL 设计人员不希望这样做,可能是因为它会使调试成为用户的噩梦)。
不过,没有什么可以阻止您自己进行此优化;预先添加一个查询来计数,看看两个过滤器中的哪一个会为第二个过滤器生成较小的结果集。对于小型数据库,几乎在所有情况下这都会变慢,因为您要进行整个额外的查询,但是如果您的数据库足够大并且您的检查查询很聪明,它可能最终平均会更快。此外,无论您拥有多少条件 B 对象,都可以计算出条件 A 必须有多少才能更快,然后只计算条件 A,这将有助于使检查查询更快。
【讨论】:
自从我大约一个月前写这篇文章以来,这个问题已经发生了很大的变化,但我保留了答案,因为它涵盖了一些可能对未来访问者有用的信息。跨度> 【参考方案4】:在调整 SQL 查询时,过滤结果的顺序当然很重要。为什么希望 Linq-to-SQL 永远不会受到过滤顺序的影响?
【讨论】:
通常假设您编写查询的方式不会影响性能,至少在简单的情况下是这样,因为优化器会重新排序内容。如果您认为情况并非如此,则应详细说明。 -1 @usr 你自己说的......“至少在简单的情况下”......你通过你对你的陈述缺乏信心来证明我的观点。 我的意思是,您的回答虽然正确,但并没有增加对问题的任何洞察力。 @usr wow...我只是向上滚动并查看了问题...当我提交答案时(大约一个月前),问题完全不同。我同意我的回答现在并不是特别有用,但我不同意您的反对意见……因此,我赞成您的回答! +1以上是关于LINQ 查询子句的顺序是不是会影响实体框架的性能?的主要内容,如果未能解决你的问题,请参考以下文章