C#lambda表达式常量vs字符串

时间:2016-04-04 22:39:49

标签: c# linq lambda where-clause

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

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的测试导致全表扫描......

3 个答案:

答案 0 :(得分:3)

理论#1

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

NameDbContext.Configuration.UseDatabaseNullSemantics = true;

这将导致简化的WHERE子句:

WHERE "Extent2"."LAST_NAME" = :p__linq__0

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

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

理论#2

从个人经验来看(虽然使用SQL Server,但由于一般概念是相同的,我认为这可以适用于您的情况),绕过索引可能还有另一个原因,那就是{{1}之间的类型不匹配列和LAST_NAME参数。在我的场景中,数据库中的列是非unicode,但EF生成的参数分别是unicode(:p__linq__0 vs varchar - unicode是EF的默认值,使索引不可能。< / p>

答案 1 :(得分:1)

解决此问题的一种方法是创建一个简单的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);
    }
}

答案 2 :(得分:0)

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

options(stringsAsFactors=F)
library(stringr)
library(stringi)
library(data.table)
library(rbenchmark)

#generate some sample data
numDat <- 3e3 #*100
numKey <- 35  #*100
numRep <- 30e3/numDat
set.seed(1)
df <- data.frame(CleanText=stri_rand_lipsum(numDat))
allwords <- unlist(unique(stri_split_regex(gsub("[[:punct:]]", "", df$CleanText), "\\s+")))
negative <- sample(allwords, numKey)

#using stri_extract_all_regex and data.table
myFunc <- function() {
    strparts <- stri_extract_all_regex(df$CleanText, '\\S+')
    dt <- data.table(Key=unlist(strparts, use.names=FALSE),
                     ID=rep(1:numDat, sapply(strparts, length)),
                     key="Key")
    negDT <- data.table(Key=negative, key="Key")
    ans <<- dt[negDT][,.(negativeWords=paste(unique(Key), collapse=", ")),by="ID"][order(ID),]
}

#OP's original function
OPfunc <- function() {
    df$negativeWords <<- sapply(str_extract_all(df$CleanText, '\\S+'), function(x) paste(unique(x[x %in% negative]), collapse = ', '))
}

#benchmarking
benchmark(opfunc=OPfunc(), myfunc=myFunc(), replications=numRep)

#word sets are identical except for ordering of negative words
tail(ans)
tail(df$negativeWords)

从这个答案Local variable and expression trees

  

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

     

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

     

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

如果我定义了本地变量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(); ,则调试视图将输出:

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";