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

在阅读有关entity framework性能的文章时,我发现了这条信息:

其次,问题[SQL Server不会重用执行计划]首先发生因为(由于实现细节)将int传递给Skip()和Take()方法,Entity Framework无法查看是否它们传递的绝对值如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(); 

这允许entity framework知道这些是变量,并且生成的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> ( Expression.Equal( Expression.Property( itemParameter, prop.Name ), Expression.Constant(convertedId) ), new[] { itemParameter } ); return Get().Where(whereExpression); 

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

 WHERE [Extent1].[Id] = 1234 

和:

 WHERE [Extent1].[Id] = 1235 

和:

 WHERE [Extent1].[Id] = 1236 

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

经过大量的反复试验,我们发现您仍然可以通过稍微改变传递方式来强制Entity Framework将convertedId识别为参数:

 .... var convObj = new { id = convertedId }; var rightExp = Expression.Convert(Expression.Property(Expression.Constant(convObj), "id"), convertedId.GetType()); var whereExpression = Expression.Lambda> ( Expression.Equal( Expression.Property( itemParameter, prop.Name ), rightExp ), new[] { itemParameter } ); return Get().Where(whereExpression); 

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

 WHERE [Extent1].[Id] = @p__linq__0 

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

让我回顾一下。

你正在建造这样的Expression>

 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>(body, item); 

问题是什么应该用于right ,以使EF不将其视为常数。

显然原始价值就像

 var right = Expression.Convert(Expression.Constant(convertedId), left.Type); 

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

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

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

或者Tuple实例(有点冗长,但消除了Expression.Convert

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

等等