重写LINQ表达式查询以启用缓存SQL执行计划

时间:2016-01-17 23:32:27

标签: c# sql-server entity-framework linq

在阅读Entity Framework performance上的文章时,我偶然发现了这条信息:

  

其次,问题 [SQL Server不会重用执行计划] 首先发生,因为(由于实现细节)将int传递给Skip()和Take( )方法,实体框架无法查看它们是否传递了绝对值,如Take(100),或者像Take(resultsPerPage)这样的变量,因此它不知道该值是否应该参数化。

建议的解决方案是改变这种代码形式:

var schools = db.Schools
    .OrderBy(s => s.PostalZipCode)
    .Skip(model.Page * model.ResultsPerPage)
    .Take(model.ResultsPerPage)
    .ToList();

进入这种风格:

int resultsToSkip = model.Page * model.ResultsPerPage;
var schools = db.Schools
    .OrderBy(s => s.PostalZipCode)
    .Skip(() => resultsToSkip) //must pre-calculate this value
    .Take(() => model.ResultsPerPage)
    .ToList();

这允许实体框架知道这些是变量,并且生成的SQL应该被参数化,这反过来允许重用执行计划。

我们的应用程序中有一些代码以相同的方式使用变量,但是我们必须在运行时构建Expression,因为事先不知道类型。

以下是它的样子:

var convertedId = typeof(T).GetConvertedIdValue(id);
var prop = GetIdProperty(typeof(T));

var itemParameter = Expression.Parameter(typeof(T), "item");
var whereExpression = Expression.Lambda<Func<T, bool>>
    (
    Expression.Equal(
        Expression.Property(
            itemParameter,
            prop.Name
            ),
        Expression.Constant(convertedId)
        ),
    new[] { itemParameter }
    );

return Get<T>().Where(whereExpression);

问题是使用Expression.Constant(convertedId)会导致将常量插入到生成的SQL中。这会导致SQL更改为您查找的每个新项目,这将停止任何执行计划缓存:

WHERE [Extent1].[Id] = 1234

WHERE [Extent1].[Id] = 1235

WHERE [Extent1].[Id] = 1236

那么问题是如何以强制生成SQL的参数化的方式使用Expression构建? () => convertedId语法不起作用。我在下面回答了这个问题。

2 个答案:

答案 0 :(得分:5)

经过大量的反复试验后,我们发现您仍然可以强制实体框架将convertedId识别为参数,只需稍微改变传递方式:

....

var convObj = new
{
    id = convertedId
};
var rightExp = Expression.Convert(Expression.Property(Expression.Constant(convObj), "id"), convertedId.GetType());

var whereExpression = Expression.Lambda<Func<T, bool>>
    (
    Expression.Equal(
        Expression.Property(
            itemParameter,
            prop.Name
            ),
        rightExp
        ),
    new[] { itemParameter }
    );

return Get<T>().Where(whereExpression);

这导致生成的SQL对任何给定的id使用相同的参数(和代码):

WHERE [Extent1].[Id] = @p__linq__0 

我们正在处理的查询需要很长时间来生成执行计划,因此我们看到访问新ID的执行时间显着减少(从3~4秒减少到~300毫秒)

答案 1 :(得分:2)

让我回顾一下。

您正在构建Expression<Func<T, bool>>这样的

var item = Expression.Parameter(typeof(T), "item");
var left = Expression.Property(item, idPropertyName);
Expression right = ...;
var body = Expression.Equal(left, right);
var predicate = Expression.Lambda<Func<T, bool>>(body, item);

问题是right应该使用什么才能使EF不将其视为常量。

显然像

这样的原始价值
var right = Expression.Convert(Expression.Constant(convertedId), left.Type);

不起作用,因此解决方案是提供某个类实例的属性。你通过使用匿名类型解决了它,但当然还有很多其他方法可以做到这一点。

例如,使用闭包(就像你没有手动创建表达式那样)

Expression<Func<object>> closure = () => convertedId;
var right = Expresion.Convert(closure.Body, left.Type);

Tuple<T>实例(有点详细,但删除了Expression.Convert

var tuple = Activator.CreateInstance(
    typeof(Tuple<>).MakeGenericType(left.Type), convertedId);
var right = Expression.Property(Expression.Constant(tuple), "Item1");

等。