【问题标题】:C# lambda expression constant vs stringC# lambda 表达式常量与字符串
【发布时间】:2016-04-04 22:39:49
【问题描述】:

如果我运行这个表达式,有人可以解释为什么:

const string testValue = "ABC"; 
return NameDbContext.MasterNames
    .Where(m => m.Names.Any(n => n.LastName == testValue))
    .ToList();

我得到了预期的结果,但是如果我使用 testValue 作为变量运行相同的结果,它会失败:

string testValue = "ABC"; 
return NameDbContext.MasterNames
    .Where(m => m.Names.Any(n => n.LastName == testValue))
    .ToList();

这似乎只发生在string 上。与int 类似的代码在将testValue 作为变量或常量都可以正常工作。

我怀疑这是由于字符串的对象性质。如果是这种情况,我怎么能用变量调用这个表达式(我不知道编译时testValue 的值)。

谢谢。

编辑:

此查询针对大型 oracle 数据表(> 600 万行)运行。使用常量时,它会立即返回正确的结果集。使用变量运行时,似乎where 的应用效率非常低(返回需要一分钟以上)。

EDIT2:

在我看到的数据库中跟踪查询:

使用常量调用时:

SELECT *
  FROM (SELECT   "Filter2"."MALPHA_KEY" AS "MALPHA_KEY"
      FROM (SELECT "Extent1"."MALPHA_KEY" AS "MALPHA_KEY",
          ROW_NUMBER () OVER (ORDER BY "Extent1"."MALPHA_KEY" ASC)
                                                              AS "row_number"
                    FROM "RMS"."ALPHA_MASTER_NAME" "Extent1"
                   WHERE (EXISTS (
                             SELECT 1 AS "C1"
                               FROM "RMS"."ALPHA" "Extent2"
                              WHERE (    ("Extent1"."MALPHA_KEY" =
                                                        "Extent2"."MALPHA_KEY"
                                         )
                                     AND ('ABC' = "Extent2"."LAST_NAME")
                                    ))
                         )) "Filter2"
           WHERE ("Filter2"."row_number" > 0)
        ORDER BY "Filter2"."MALPHA_KEY" ASC)
 WHERE (ROWNUM <= (50))

使用变量调用时:

SELECT *
  FROM (SELECT   "Project2"."MALPHA_KEY" AS "MALPHA_KEY"
            FROM (SELECT "Project2"."MALPHA_KEY" AS "MALPHA_KEY",
                         ROW_NUMBER () OVER (ORDER BY "Project2"."MALPHA_KEY" ASC)
                                                              AS "row_number"
                    FROM (SELECT "Extent1"."MALPHA_KEY" AS "MALPHA_KEY"
                            FROM "RMS"."ALPHA_MASTER_NAME" "Extent1"
                           WHERE (EXISTS (
                                     SELECT 1 AS "C1"
                                       FROM "RMS"."ALPHA" "Extent2"
                                      WHERE (    ("Extent1"."MALPHA_KEY" =
                                                        "Extent2"."MALPHA_KEY"
                                                 )
                                             AND (   ("Extent2"."LAST_NAME" =
                                                                   :p__linq__0
                                                     )
                                                  OR (    ("Extent2"."LAST_NAME" IS NULL
                                                          )
                                                      AND (:p__linq__0 IS NULL
                                                          )
                                                     )
                                                 )
                                            ))
                                 )) "Project2") "Project2"
           WHERE ("Project2"."row_number" > 0)
        ORDER BY "Project2"."MALPHA_KEY" ASC)
 WHERE (ROWNUM <= (50))

注意 where 语句的区别(除了使用变量)它测试 NULL 相等性

    AND (   ("Extent2"."LAST_NAME" = :p__linq__0
        )
   OR (    ("Extent2"."LAST_NAME" IS NULL )
   AND (:p__linq__0 IS NULL )  )  )

对 NULL 的测试导致全表扫描...

【问题讨论】:

  • 试试((n => n.LastName.Equals(testValue)),看看有没有区别。
  • “失败”是什么意思?
  • 请解释什么失败,你得到什么样的错误?
  • @Bibi,看起来不错,我们正在取得进展。首先要尝试:DbContext.Configuration.UseDatabaseNullSemantics = true 将导致第二个查询生成更接近“const”版本的 WHERE 子句。如果之后还是很慢,很可能是参数数据类型不匹配(:p__linq__0)。
  • @Bibi,这正是打开 UseDatabaseNullSemantics 所做的事情:它强制使用直接的 WHERE xxx = @param 子句,而无需额外的空检查。

标签: c# linq lambda where-clause


【解决方案1】:

理论#1

如果您已经测试了生成的查询并确定它实际上是导致全表扫描的参数空检查,那么修复非常简单:

NameDbContext.Configuration.UseDatabaseNullSemantics = true;

这将产生一个简化的WHERE 子句:

WHERE "Extent2"."LAST_NAME" = :p__linq__0

显然,您需要考虑这将对使用NameDbContext 的其他查询产生的影响。

或者,您可以使用@IanMercer 的高度娱乐性解决方案并执行表达式树节点替换以获得所需的WHERE 子句。我希望最终结果是相似的,尽管我不确定 Oracle 是否足够聪明,可以在没有显式参数化的情况下生成可重用的查询计划,这可能会导致一些重新编译开销。

理论 #2

根据个人经验(尽管使用 SQL Server,但由于一般概念是相同的,我认为这可以适用于您的情况),绕过索引可能还有另一个原因,那就是您的 LAST_NAME 之间的类型不匹配列和:p__linq__0 参数。在我的场景中,数据库中的列是非 unicode,但 EF 生成的参数是 unicode(varcharnvarchar 分别 - unicode 是 EF 的默认值),因此无法进行索引查找。

【讨论】:

  • Kirill,理论 #1:在我的情况下设置 NameDbContext.Configuration.UseDatabaseNullSemantics = true; 效果很好。我需要对应用程序其余部分的影响进行进一步调查,但感谢您让我走上正轨。
【解决方案2】:

解决此问题的一种方法是创建一个简单的ExpressionVisitor,它使用部分应用程序将现有表达式上的参数重写为常量值。

例如,我创建表达式,然后对它们应用一个值(仅在运行时知道):

 Expression<Func<int, int, bool>> expr = (a, b) => a < b;
 var applied = expr.Apply(input.FirstMonth);

这是我使用的(许多)Apply 方法之一(每种方法采用不同数量的参数):

/// <summary>
/// Partially apply a value to an expression
/// </summary>
public static Expression<Func<U, bool>> Apply<T, U>(this Expression<Func<T, U, bool>> input,
    T value)
{
   var swap = new ExpressionSubstitute(input.Parameters[0],
       Expression.Constant(value));
   var lambda = Expression.Lambda<Func<U, bool>>(
       swap.Visit(input.Body), 
       input.Parameters[1]);
   return lambda;
}


class ExpressionSubstitute : System.Linq.Expressions.ExpressionVisitor
{
    private readonly Expression from, to;
    public ExpressionSubstitute(Expression from, Expression to)
    {
        this.from = from;
        this.to = to;
    }

    public override Expression Visit(Expression node)
    {
        if (node == from) return to;
        return base.Visit(node);
    }
}

【讨论】:

    【解决方案3】:

    创建 linq 查询时,实际上是在构建表达式树。在您的示例中,您有两个表达式树来构建查询:

    Expression<Func<Name, bool>> exp1 = name => name.LastName == testValue;
    Expression<Func<MasterName, bool>> exp2 = masterName => masterName.Names.Any(exp1);
    var result = NameDbContext.MasterNames.Where(exp2).ToList();
    

    来自这个答案Local variable and expression trees

    捕获局部变量实际上是通过将局部变量“提升”到编译器生成的类的实例变量中来执行的。 C# 编译器在适当的时候创建额外类的新实例,并将对局部变量的任何访问更改为对相关实例中实例变量的访问。

    因此,表达式树需要成为实例中的字段访问 - 并且实例本身是通过 ConstantExpression 提供的。

    创建表达式树的最简单方法通常是在 lambda 表达式中创建类似的东西,然后在 Reflector 中查看生成的代码,将优化级别调低,这样 Reflector 就不会将其转换回 lambda 表达式.

    如果我定义一个本地变量string testValue = "ABC";,调试视图将输出:

    .Lambda #Lambda1<System.Func`2[ConsoleApp.Program+Name,System.Boolean]>(ConsoleApp.Program+Name $name)
    {
        $name.LastName == .Constant<ConsoleApp.Program+<>c__DisplayClass0_0>(ConsoleApp.Program+<>c__DisplayClass0_0).testValue
    }
    

    现在如果我定义一个常量const string testValue = "ABC";,调试视图将输出:

    .Lambda #Lambda1<System.Func`2[ConsoleApp.Program+Name,System.Boolean]>(ConsoleApp.Program+Name $name)
    {
        $name.LastName == "ABC"
    }
    

    【讨论】:

    • 这似乎没有完全回答这个问题,而且:你为什么要在一个简单的表达式上调用Compile(绕过所有IQueryable&lt;T&gt; 好东西并强制满载桌子)?
    • 这解释了为什么局部变量和常量有不同的行为。我刚刚拆分了这个查询,以便更容易理解。无论如何,我不会强迫一张桌子满载...
    • 你会把你的房子押在最后一句话上吗?我会重申,DbSet.Where(expr)DbSet.Where(expr.Compile()) 会导致完全不同的行为。
    • 我没有房子^^谢谢你,我什至不知道这个^^。我会编辑我的帖子。
    猜你喜欢
    • 1970-01-01
    • 2016-10-07
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-07-02
    • 1970-01-01
    • 2017-06-14
    相关资源
    最近更新 更多