【问题标题】:Why is EF generating SQL queries with unnecessary null-checks?为什么 EF 生成带有不必要的空检查的 SQL 查询?
【发布时间】:2016-07-18 09:50:05
【问题描述】:

我遇到了一个问题,即 EF 在搜索字符串字段时会创建糟糕的查询。它产生了一个懒惰程序员风格的查询,包含强制扫描整个索引的空检查。

考虑以下查询。

  1. 查询 1

    var p1 = "x";
    var r1 = ctx.Set<E>().FirstOrDefault(
                            subject =>
                                p1.Equals(subject.StringField));
    
  2. 查询 2

    const string p2 = "x";
    var r2 = ctx.Set<E>().FirstOrDefault(
                            subject =>
                                p2.Equals(subject.StringField));
    

查询 1 产生

WHERE (('x' = [Extent2].[StringField]) OR (('x' IS NULL) AND ([Extent2].[StringField] IS NULL))) 

并在 4 秒内执行

查询 2 产生

WHERE (N'x' = [Extent2].[StringField]) 

并在 2 毫秒内执行

有没有人知道任何变通方法? (no参数不能是const,因为它是由用户输入的,但不能为null。)

N.B 分析时,两个查询均由 EF 使用 sp_executesql 准备;因为如果它们刚刚被执行,查询优化器将否定 OR 'x' IS NULL 检查。

【问题讨论】:

标签: c# sql-server entity-framework


【解决方案1】:

设置UseDatabaseNullSemantics = true;

  • UseDatabaseNullSemantics == true 时,(operand1 == operand2) 将被翻译为:

    WHERE operand1 = operand2
    
  • UseDatabaseNullSemantics == false时,(operand1 == operand2)会被翻译为:

    WHERE
        (
            (operand1 = operand2)
            AND
            (NOT (operand1 IS NULL OR operand2 IS NULL))
        )
        OR
        (
            (operand1 IS NULL)
            AND
            (operand2 IS NULL)
        )
    

This is documented by Microsoft:

获取或设置一个值,该值指示在比较两个操作数时是否显示数据库空语义,这两个操作数都可能为空。默认值为 false。

您可以在DbContext 子类构造函数中设置它,如下所示:

public class MyContext : DbContext
{
    public MyContext()
    {
        this.Configuration.UseDatabaseNullSemantics = true;
    }
}

或者您也可以像下面的代码示例一样从外部将此设置设置为您的dbContext 实例,从我的角度来看(请参阅@GertArnold 评论),此方法会更好,因为它不会更改默认值数据库行为或配置):

myDbContext.Configuration.UseDatabaseNullSemantics = true;

【讨论】:

  • 但这也表明空检查不是“不必要的”。当var p1null 时,它们可以防止意外行为。这就是为什么 UseDatabaseNullSemantics 默认为 false 的原因,所以你得到(预期的).Net 空语义。
  • @GertArnold 是的,对于那些特殊的用例,他可以从外部放置它!我也喜欢 myDbContext.Configuration.UseDatabaseNullSemantics = true;其余的保留默认配置,我将更新答案
  • @GertArnold 在一般情况下,上下文标志确定是使用 SQL 还是 C# 空值比较规则。由于 OP 的 operand1 是参数并且不能为空,因此那些空比较是不必要的;在这种情况下,SQL 空值比较的工作方式相同。这是预期的结果。
  • ef core 呢?
【解决方案2】:

您可以通过在 StringField 属性上添加 [Required] 来解决此问题

public class Test
{
    [Key]
    public int Id { get; set; }
    [Required]
    public string Bar{ get; set; }
    public string Foo { get; set; }

}


 string p1 = "x";
 var query1 = new Context().Tests.Where(F => p1.Equals(F.Bar));

 var query2 = new Context().Tests.Where(F => p1.Equals(F.Foo));

这是查询1

{选择 [Extent1].[Id] AS [Id], [Extent1].[Bar] AS [Bar], [Extent1].[Foo] AS [Foo] FROM [dbo].[Tests] AS [Extent1] WHERE @p__linq__0 = [Extent1].[Bar]}

这是查询2

{选择 [Extent1].[Id] AS [Id], [Extent1].[Bar] AS [Bar], [Extent1].[Foo] AS [Foo] FROM [dbo].[Tests] AS [Extent1] WHERE (@p__linq__0 = [Extent1].[Foo]) OR ((@p__linq__0 IS NULL) AND ([Extent1].[Bar2] IS NULL))}

【讨论】:

  • 该字段可以为空,但查询永远不会搜索空值。
  • 我也相信 ef 应该根据参数是否为空来生成两个可能的字符串查询。不是一个懒惰的人,它把它留给 SQL Server 让我想起了开发人员在 SP 中编写类似的东西,因为他们懒得写两个 SP
  • @Mark 但是如果您有一个包含 20 个参数的查询,每个参数都可以为空并且用户可以搜索任何组合,查询构建器将为每个空/非空参数组合创建一个单独的查询- 总共最多 2^20 个查询。这可能会杀死您的语句缓存并降低性能。这是准备好的语句之前的问题之一,当时人们通过字符串连接编写所有查询......
  • 如果缓存被频繁循环,则为真。但它会降低性能超过 2000 倍吗?因为这就是我通过让它输出共享查询所看到的。此外,IMO 并体验到用户不太可能真正通过 20 个可空字段搜索单个表。我认为数据库在 TNF 中不正确。
  • 这看起来像代码优先和不同的“使用”命名空间。否则,Required 是 ASP.Net 显示属性,与生成的 SQL 无关。但如果代码优先是一个选项,这是一个很好的建议。
【解决方案3】:

我的一位同事刚刚找到了一个非常好的解决方案。因为我已经发现使用常量会产生正确的 SQL。我们想知道是否可以将表达式中的变量换成常量;事实证明你可以。我相信这种方法比在 DB 上下文中更改 null 设置的侵入性更小。

public class Foo_test : EntityContextIntegrationSpec
        {

            private static string _foo = null;

            private static DataConnection _result;

            private Because _of = () => _result = EntityContext.Set<E>().Where(StringMatch<E>(x => x.StringField));

            private static Expression<Func<TSource, bool>> StringMatch<TSource>(Expression<Func<TSource, string>> prop)
            {
                var body = Expression.Equal(prop.Body, Expression.Constant(_foo));
                return Expression.Lambda<Func<TSource,bool>>(body, prop.Parameters[0]);                
            }

            [Test] public void Test() => _result.ShouldNotBeNull();
        }

【讨论】:

  • 虽然这确实会生成更好的 SQL,但实际执行时间要差得多。我相信这有两个原因。 1-构建表达式的开销。 2-这会导致查询计划不可缓存,这实际上给每次调用增加了更多的数据库开销,而不是进行空检查的额外工作
  • 在我们的用例中,情况并非如此。执行时间比空检查快几个数量级。请参阅问题中所述的统计数据。
  • 在生成的 SQL 中,使用此代码会导致在 SQL 中使用常量值,而不是参数化查询。不幸的是,这意味着更改值会在 SQL 中产生一个新常量,这意味着必须再次编译和计划查询。对于经常使用不同值运行的较小查询,这似乎很快超过了额外空检查的成本。
猜你喜欢
  • 1970-01-01
  • 2017-12-16
  • 1970-01-01
  • 2012-12-26
  • 2011-06-17
  • 1970-01-01
  • 2022-01-11
  • 2020-12-07
  • 2021-08-16
相关资源
最近更新 更多