关于c#:LINQ查询子句的顺序是否会影响Entity Framework的性能?

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万行就能重现该问题。在那种情况下,将触发优化器中的一个弱点:由于条件复杂,它无法识别出它可以对Colour进行查找。在第一个查询中,优化器确实会识别出模式并创建索引查找。

这没有语义上的原因。即使在NULL上搜索也可以在索引上搜索。这是优化器中的弱点/错误。这是两个计划:

enter

1
2
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都不满意。两种产品都应固定。 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"部分在补偿三值逻辑时不太常见:

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

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

    1
    2
    3
    (x = y) --> NULL
    (x IS NULL AND y IS NULL) --> false
    NULL OR false --> 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永远不会受到过滤顺序的影响?