【问题标题】:Rewriting a LINQ Expression query to enable caching SQL Execution Plan重写 LINQ 表达式查询以启用缓存 SQL 执行计划
【发布时间】:2016-01-17 23:32:26
【问题描述】:

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

其次,问题[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 应该被参数化,这反过来又允许执行计划被重用。

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

这是它过去的样子:

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 进行参数化?() =&gt; convertedId 语法将不起作用。我已经在下面回答了。

【问题讨论】:

  • 我不明白,这里的问题是什么?
  • 问题是在使用 Expression.Constant 时如何转换上面的代码以生成参数化 SQL,因为 () =&gt; convertedId 语法不起作用。
  • 我已更新主帖以明确包含该问题。

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


【解决方案1】:

经过大量试验和错误,我们发现您仍然可以通过稍微更改传入方式来强制实体框架将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 毫秒)

【讨论】:

  • 没错,skip/take 示例只是一种更简单的方法来突出使用常量值而不是参数化值生成 SQL 的潜在问题。
【解决方案2】:

让我回顾一下。

你正在像这样构建Expression&lt;Func&lt;T, bool&gt;&gt;

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&lt;T&gt; 实例(有点冗长,但消除了Expression.Convert

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

等等。

【讨论】:

  • 不错!我非常喜欢闭包解决方案
猜你喜欢
  • 1970-01-01
  • 2012-02-18
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-04-30
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多