Константа лямбда-выражения 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 ответа:
Теория #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
vsnvarchar
соответственно - 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" }