Константа лямбда-выражения C# vs строка


Может кто-нибудь объяснить, почему, если я использую это выражение:

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 (> 6 миллионов строк). При использовании константы она сразу же возвращается с соответствующим набором результатов. При работе с переменной, кажется, что 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 (помимо использования переменной), которое он проверяет на нулевое равенство
    AND (   ("Extent2"."LAST_NAME" = :p__linq__0
        )
   OR (    ("Extent2"."LAST_NAME" IS NULL )
   AND (:p__linq__0 IS NULL )  )  )

Проверка на нуль приводит к полному сканированию таблицы...

3 4

3 ответа:

Теория #1

Если вы протестировали сгенерированный запрос и определили, что это, по сути, проверка аргумента null, которая вызывает полное сканирование таблицы, то исправление довольно простое:

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 (varchar vs nvarchar соответственно - unicode по умолчанию для EF), что делает невозможным поиск индекса.

Один из способов решить эту проблему-создать простой ExpressionVisitor, который переписывает параметр существующего выражения в постоянное значение с помощью частичного приложения.

Например, я создаю выражения и затем применяю к ним значение (известное только во время выполнения):

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

И вот один из (многих) методов применения, которые я использую (каждый принимает разное количество аргументов):

/// <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);
    }
}

Когда вы создаете запросы 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();

Из этого ответа локальные переменные и деревья выражений :

Захват локальной переменной фактически выполняется путем "поднятия" локальной переменной в переменную экземпляра класса, созданного компилятором. Компилятор C# создает новый экземпляр дополнительного класса в соответствующее время и изменяет любой доступ к локальной переменной в доступ переменной экземпляра в соответствующем экземпляре.

Таким образом, дерево выражений должно быть доступом к полю внутри экземпляра-и сам экземпляр предоставляется через ConstantExpression.

Самый простой подход к созданию деревьев выражений обычно заключается в том, чтобы создать нечто подобное в лямбда-выражении, а затем посмотреть на сгенерированный код в Reflector, понизив уровень оптимизации, чтобы Reflector не работал. преобразуйте его обратно в лямбда-выражения.

Если я определяю локальную переменную 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"
}