[英]Rewriting a LINQ Expression query to enable caching SQL Execution Plan
在閱讀有關實體框架性能的文章時,我發現了這條信息:
其次,問題[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();
這允許實體框架知道這些是變量,並且生成的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
語法不起作用。 我在下面回答了這個問題。
經過大量的反復試驗,我們發現您仍然可以通過稍微改變傳入方式來強制Entity Framework將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毫秒)
讓我回顧一下。
你正在構建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");
等等
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.