Force Entity Framework использовать SQL-параметризацию для лучшего повторного использования кэша SQL proc
Entity Framework всегда использует константы в сгенерированном SQL для значений, предоставляемых Skip()
и Take()
.
В приведенном ниже упрощенном примере:
int x = 10;
int y = 10;
var stuff = context.Users
.OrderBy(u => u.Id)
.Skip(x)
.Take(y)
.Select(u => u.Id)
.ToList();
x = 20;
var stuff2 = context.Users
.OrderBy(u => u.Id)
.Skip(x)
.Take(y)
.Select(u => u.Id)
.ToList();
приведенный выше код генерирует следующие SQL-запросы:
SELECT TOP (10)
[Extent1].[Id] AS [Id]
FROM ( SELECT [Extent1].[Id] AS [Id], row_number() OVER (ORDER BY [Extent1].[Id] ASC) AS [row_number]
FROM [dbo].[User] AS [Extent1]
) AS [Extent1]
WHERE [Extent1].[row_number] > 10
ORDER BY [Extent1].[Id] ASC
SELECT TOP (10)
[Extent1].[Id] AS [Id]
FROM ( SELECT [Extent1].[Id] AS [Id], row_number() OVER (ORDER BY [Extent1].[Id] ASC) AS [row_number]
FROM [dbo].[User] AS [Extent1]
) AS [Extent1]
WHERE [Extent1].[row_number] > 20
ORDER BY [Extent1].[Id] ASC
Результат в 2 Adhoc-планах, добавленных в кэш-память SQL, с 1 использованием каждого.
То, что я хотел бы сделать, - это параметризовать логику Skip()
и Take()
, чтобы генерировались следующие SQL-запросы:
EXEC sp_executesql N'SELECT TOP (@p__linq__0)
[Extent1].[Id] AS [Id]
FROM ( SELECT [Extent1].[Id] AS [Id], row_number() OVER (ORDER BY [Extent1].[Id] ASC) AS [row_number]
FROM [dbo].[User] AS [Extent1]
) AS [Extent1]
WHERE [Extent1].[row_number] > @p__linq__1
ORDER BY [Extent1].[Id] ASC',N'@p__linq__0 int,@p__linq__1 int',@p__linq__0=10,@p__linq__1=10
EXEC sp_executesql N'SELECT TOP (@p__linq__0)
[Extent1].[Id] AS [Id]
FROM ( SELECT [Extent1].[Id] AS [Id], row_number() OVER (ORDER BY [Extent1].[Id] ASC) AS [row_number]
FROM [dbo].[User] AS [Extent1]
) AS [Extent1]
WHERE [Extent1].[row_number] > @p__linq__1
ORDER BY [Extent1].[Id] ASC',N'@p__linq__0 int,@p__linq__1 int',@p__linq__0=10,@p__linq__1=20
В результате получается 1 подготовленный план, добавленный в кэш-память SQL с двумя типами использования.
У меня есть довольно сложные запросы, и я испытываю значительные накладные расходы (на стороне SQL Server) при первом запуске и гораздо более быстрое выполнение при последующих запусках (поскольку он может использовать кеш плана). Обратите внимание, что эти более продвинутые запросы уже используют sp_executesql, поскольку другие значения параметризуются, поэтому меня не интересует этот аспект.
Первый набор запросов, сгенерированный выше, в основном означает, что любая логика разбиения на страницы создаст новую запись в кеше плана для каждой страницы, раздувая кеш и требуя накладных расходов на формирование плана для каждой страницы.
Могу ли я заставить Entity Framework параметризовать значения? Я заметил для других значений, например. в Where
, иногда он параметризует значения, а иногда использует константы.
Я полностью выхожу на обед? Есть ли какая-то причина, по которой существующее поведение Entity Framework лучше, чем поведение, которое я желаю?
Edit:
В случае, если это уместно, я должен упомянуть, что я использую Entity Framework 4.2.
Изменить 2:
Этот вопрос не является дубликатом Entity Framework/Linq to SQL: Skip и Take, который просто спрашивает, как обеспечить выполнение Skip
и Take
в SQL вместо этого на клиенте. Этот вопрос относится к параметризации этих значений.
Ответы
Ответ 1
Обновление: методы прокрутки и принятия, которые принимают параметры lambda, описанные ниже, являются частью Entity Framework с версии 6 и далее. Вы можете воспользоваться ими, импортировав пространство имен System.Data.Entity в свой код.
В общем случае LINQ to Entities переводит константы в качестве констант и переменных, переданных в запрос в параметры.
Проблема заключается в том, что Queryable версии Skip и Take принимают простые целочисленные параметры, а не лямбда-выражения, поэтому, когда LINQ to Entities может видеть значения, которые вы передаете, он не может видеть, что вы использовали переменную для их передачи (в другие слова, такие методы, как Skip и Take, не имеют доступа к закрытию метода).
Это не только влияет на параметризацию в LINQ на Entities, но и на то, что если вы передаете переменную в запрос LINQ, последнее значение переменной используется каждый раз, когда вы повторно выполняете запрос. Например, что-то вроде этого работает для Where, но не для Skip или Take:
var letter = "";
var q = from db.Beattles.Where(p => p.Name.StartsWith(letter));
letter = "p";
var beattle1 = q.First(); // Returns Paul
letter = "j";
var beattle2 = q.First(); // Returns John
Обратите внимание, что эта же особенность также влияет на ElementAt, но эта в настоящее время не поддерживается LINQ to Entities.
Вот трюк, который вы можете использовать для принудительной параметризации Skip и Take и в то же время заставить их вести себя как другие операторы запросов:
public static class PagingExtensions
{
private static readonly MethodInfo SkipMethodInfo =
typeof(Queryable).GetMethod("Skip");
public static IQueryable<TSource> Skip<TSource>(
this IQueryable<TSource> source,
Expression<Func<int>> countAccessor)
{
return Parameterize(SkipMethodInfo, source, countAccessor);
}
private static readonly MethodInfo TakeMethodInfo =
typeof(Queryable).GetMethod("Take");
public static IQueryable<TSource> Take<TSource>(
this IQueryable<TSource> source,
Expression<Func<int>> countAccessor)
{
return Parameterize(TakeMethodInfo, source, countAccessor);
}
private static IQueryable<TSource> Parameterize<TSource, TParameter>(
MethodInfo methodInfo,
IQueryable<TSource> source,
Expression<Func<TParameter>> parameterAccessor)
{
if (source == null)
throw new ArgumentNullException("source");
if (parameterAccessor == null)
throw new ArgumentNullException("parameterAccessor");
return source.Provider.CreateQuery<TSource>(
Expression.Call(
null,
methodInfo.MakeGenericMethod(new[] { typeof(TSource) }),
new[] { source.Expression, parameterAccessor.Body }));
}
}
В приведенном выше классе определяются новые перегрузки Skip и Take, которые ожидают лямбда-выражения и могут, следовательно, захватывать переменные. Использование таких методов приведет к тому, что переменные будут переведены в параметры с помощью LINQ to Entities:
int x = 10;
int y = 10;
var query = context.Users.OrderBy(u => u.Id).Skip(() => x).Take(() => y);
var result1 = query.ToList();
x = 20;
var result2 = query.ToList();
Надеюсь, что это поможет.
Ответ 2
Методы Skip
и Top
of ObjectQuery<T>
могут быть параметризованы. Пример: MSDN.
Я сделал аналогичную вещь в модели моего собственного и sql-сервера профилировщика показал части
SELECT TOP (@limit)
и
WHERE [Extent1].[row_number] > @skip
Итак, да. Это может быть сделано. И я согласен с другими, что это ценное замечание вы сделали здесь.