Заставить Entity Framework использовать параметризацию SQL для лучшего повторного использования кэша процедур SQL - PullRequest
20 голосов
/ 09 февраля 2012

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, а не на клиенте. Этот вопрос относится к параметризации этих значений.

Ответы [ 2 ]

24 голосов
/ 11 марта 2012

Обновление: Методы расширения Skip и Take, которые принимают параметры лямбды, описанные ниже, являются частью Entity Framework начиная с версии 6 и далее.Вы можете воспользоваться ими, импортировав пространство имен System.Data.Entity в свой код.

Обычно LINQ to Entities преобразует константы как константы и переменные, передаваемые в запрос, в параметры.

Проблема заключается в том, что версии Queryable Skip и Take принимают простые целочисленные параметры, а не лямбда-выражения, поэтому, хотя LINQ to Entities может видеть передаваемые вами значения, он не может видеть тот факт, что вы использовали переменную для передачиих (другими словами, методы, такие как Skip и Take, не имеют доступа к закрытию метода).

Это влияет не только на параметризацию в LINQ to 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 голосов
/ 09 февраля 2012

Методы Skip и Top из ObjectQuery<T> могут быть параметризованы. Есть пример на MSDN .

Я сделал то же самое в модели моего собственного, и в профилировщике SQL Server были показаны детали

SELECT TOP (@limit)

и

WHERE [Extent1].[row_number] > @skip

Так что да. Это может быть сделано. И я согласен с другими, что это ценное замечание, которое вы сделали здесь.

...