Force Entity Framework使用SQL参数化来更好地重用SQL proc缓存

时间:2012-02-08 21:05:40

标签: c# .net sql-server entity-framework entity-framework-4

实体框架似乎总是在生成的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 proc缓存中,每个计划使用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 proc缓存中,有2次使用。

我有一些相当复杂的查询,并且在第一次运行时遇到了很大的开销(在SQL Server端),并且在后续运行时执行得更快(因为它可以使用计划缓存)。请注意,这些更高级的查询已经使用sp_executesql,因为其他值已参数化,因此我并不关心这方面。

上面生成的第一组查询基本上意味着任何分页逻辑都会在每个页面的计划缓存中创建一个新条目,膨胀缓存并要求每个页面产生计划生成开销。

我可以强制Entity Framework参数化值吗?我注意到其他值,例如在Where子句中,有时它会对值进行参数化,有时它会使用常量。

我完全出去吃午饭吗?有没有理由为什么实体框架的现有行为比我想要的行为更好?

修改 如果它是相关的,我应该提到我正在使用Entity Framework 4.2。

编辑2: 这个问题不是Entity Framework/Linq to SQL: Skip & Take的重复,它只是询问如何确保SkipTake在SQL中而不是在客户端上执行。这个问题与参数化这些值有关。

2 个答案:

答案 0 :(得分:24)

更新: 采用下面描述的lambda参数的Skip and Take扩展方法是版本6及更高版本的实体框架的一部分。您可以通过在代码中导入System.Data.Entity命名空间来利用它们。

通常,LINQ to Entities将常量转换为传递给查询的常量和变量为参数。

问题是Skip和Take的Queryable版本接受简单的整数参数而不接受lambda表达式,因此当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的新重载,它们期望一个lambda表达式,因此可以捕获变量。使用这样的方法将导致变量被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();

希望这有帮助。

答案 1 :(得分:2)

Skip的方法TopObjectQuery<T>可以进行参数化。 MSDN有一个例子。

我在自己的模型中做了类似的事情,并且sql server profiler显示了部分

SELECT TOP (@limit)

WHERE [Extent1].[row_number] > @skip

所以,是的。可以办到。我同意其他人的意见,这是你在这里做出的一个有价值的观察。