我使用Entity Framework(v6.1.1)执行以下LINQ查询:
private IList<Customer> GetFullCustomers(IEnumerable<int> customersIds)
{
IQueryable<Customer> fullCustomerQuery = GetFullQuery();
return fullCustomerQuery.Where(c => customersIds.Contains(c.Id)).ToList();
}
此查询被翻译成相当不错的SQL:
SELECT
[Extent1].[Id] AS [Id],
[Extent1].[FirstName] AS [FirstName]
-- ...
FROM [dbo].[Customer] AS [Extent1]
WHERE [Extent1].[Id] IN (1, 2, 3, 5)
但是,我在查询编译阶段获得了非常显着的性能影响。主叫:
ELinqQueryState.GetExecutionPlan(MergeOption? forMergeOption)
每次请求需要约50%的时间。深入研究结果表明,每次通过不同的 customersIds 时,都会重新编译查询。 根据{{3}},这是一种预期的行为,因为在查询中使用的IEnumerable被认为是volatile,并且是缓存的SQL的一部分。这就是为什么SQL对于 customersIds 的每个不同组合都不同的原因,它总是有不同的哈希值,用于从缓存中获取编译查询。
现在的问题是:如何在使用多个 customersIds 进行查询的同时避免重新编译?
答案 0 :(得分:23)
这是一个很好的问题。首先,这里有一些解决方法(它们都需要更改查询):
第一个解决方法
这个可能有点显而易见,遗憾的是一般不适用:如果您需要传递给Enumerable.Contains
的项目的选择已经存在于数据库的表中,您可以编写一个调用{{ 1}}在谓词中设置的相应实体上,而不是先将项目放入内存。对数据库中的数据进行Enumerable.Contains
调用应该会产生某种可以缓存的基于JOIN的查询。例如。假设Customers和SelectedCustomers之间没有导航属性,您应该能够编写如下查询:
Enumerable.Contains
在这种情况下,Any的查询语法稍微简单一些:
var q = db.Customers.Where(c =>
db.SelectedCustomers.Select(s => s.Id).Contains(c.Id));
如果您还没有存储在数据库中的必要选择数据,您可能不需要存储它的开销,因此您应该考虑下一个解决方法。
第二种解决方法
如果您事先知道列表中的元素数量相对可管理,则可以使用OR-ed等式比较树替换var q = db.Customers.Where(c =>
db.SelectedCustomers.Any(s => s.Id == c.Id));
,例如:
Enumerable.Contains
这应该生成一个可以缓存的参数化查询。如果列表的大小因查询而异,则应为每个列表大小生成不同的缓存条目。或者,您可以使用具有固定大小的列表,并传递一些您知道永远不会与值参数匹配的标记值,例如0,-1。为了在运行时基于列表以编程方式生成此类谓词表达式,您可能需要考虑使用类似PredicateBuilder的内容。
潜在的解决方案及其挑战
一方面,在当前版本的EF中,显式使用CompiledQuery支持缓存此类查询所需的更改将非常复杂。关键原因是传递给var list = new [] {1,2,3};
var q = db.Customers.Where(c =>
list[0] == c.Id ||
list[1] == c.Id ||
list[2] == c.Id );
方法的IEnumerable<T>
中的元素必须转换为我们生成的特定翻译的查询的结构部分,例如:
Enumerable.Contains
可枚举的“列表”在C#/ LINQ中看起来像一个简单的变量,但它需要转换为这样的查询(为简洁起见,简化了):
var list = new [] {1,2,3};
var q = db.Customers.Where(c => list.Contains(c.Id)).ToList();
如果列表更改为new [] {5,4,3,2,1},我们将不得不再次生成SQL查询!
SELECT * FROM Customers WHERE Id IN(1,2,3)
作为一种潜在的解决方案,我们已经讨论过将生成的SQL查询保留为某种特殊占位符,例如:存储在只是说
的查询缓存中SELECT * FROM Customers WHERE Id IN(5,4,3,2,1)
在执行时,我们可以从缓存中选择此SQL并使用实际值完成SQL生成。另一种选择是在目标数据库可以支持列表的情况下利用表值参数。第一个选项可能只适用于常量值,后者需要一个支持特殊功能的数据库。在EF中实现这两者都非常复杂。
自动编译查询
另一方面,对于自动编译查询(与显式CompiledQuery相反),问题变得有点人为:在这种情况下,我们在初始LINQ转换后计算查询缓存键,因此传递的任何SELECT * FROM Customers WHERE Id IN(<place holder>)
参数都应该已经扩展到DbExpression节点:EF5中的OR-ed相等比较树,通常是EF6中的单个DbInExpression节点。由于查询树已经为IEnumerable<T>
的源参数中的每个不同元素组合包含了一个不同的表达式(因此对于每个不同的输出SQL查询),因此可以缓存查询。
然而,即使在EF6中,即使在自动编译的查询案例中也不会缓存这些查询。关键原因是我们期望列表中元素的可变性很高(这与列表的可变大小有关,但是由于我们通常不参数化出现的值这一事实也会加剧这一点。作为查询的常量,因此常量列表将被转换为SQL中的常量文字),因此对Enumerable.Contains
的查询进行足够的调用会产生相当大的缓存污染。
我们已经考虑了alternative solutions to this as well,但我们还没有实施。所以我的结论是,在大多数情况下,如果按照我的说法,你知道列表中的元素数量将会很小并且易于管理(否则你将面临performance issues)。
希望这有帮助!
答案 1 :(得分:0)
我遇到了确切的挑战。这是我在IQueryables的扩展方法中针对字符串或long类型解决此问题的方法。
为了限制缓存污染,我们使用n个m个(可配置)参数(例如1 * m,2 * m等)来创建相同的查询。因此,如果设置为15,则为0。查询计划将具有15、30、45等个参数,具体取决于包含中的元素数量(我们事先不知道,但可能少于100个),如果最大的包含是,则将查询计划的数量限制为3小于或等于45。
其余参数由一个占位符值填充(我们知道),该值在数据库中不存在。在这种情况下,“-1”
结果查询部分;
... WHERE [Filter1].[SomeProperty] IN (@p__linq__0,@p__linq__1, (...) ,@p__linq__19)
... @p__linq__0='SomeSearchText1',@p__linq__1='SomeSearchText2',@p__linq__2='-1',
(...) ,@p__linq__19='-1'
用法:
ICollection<string> searchtexts = .....ToList();
//or
//ICollection<long> searchIds = .....ToList();
//this is the setting that is relevant for the resulting multitude of possible queryplans
int itemsPerSet = 15;
IQueryable<MyEntity> myEntities = (from c in dbContext.MyEntities
select c)
.WhereContains(d => d.SomeProperty, searchtexts, "-1", itemsPerSet);
扩展方法:
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
namespace MyCompany.Something.Extensions
{
public static class IQueryableExtensions
{
public static IQueryable<T> WhereContains<T, U>(this IQueryable<T> source, Expression<Func<T,U>> propertySelector, ICollection<U> identifiers, U placeholderThatDoesNotExistsAsValue, int cacheLevel)
{
if(!(propertySelector.Body is MemberExpression))
{
throw new ArgumentException("propertySelector must be a MemberExpression", nameof(propertySelector));
}
var propertyExpression = propertySelector.Body as MemberExpression;
var propertyName = propertyExpression.Member.Name;
return WhereContains(source, propertyName, identifiers, placeholderThatDoesNotExistsAsValue, cacheLevel);
}
public static IQueryable<T> WhereContains<T, U>(this IQueryable<T> source, string propertyName, ICollection<U> identifiers, U placeholderThatDoesNotExistsAsValue, int cacheLevel)
{
return source.Where(ContainsPredicateBuilder<T, U>(identifiers, propertyName, placeholderThatDoesNotExistsAsValue, cacheLevel));
}
public static Expression<Func<T, bool>> ContainsPredicateBuilder<T,U>(ICollection<U> ids, string propertyName, U placeholderValue, int cacheLevel = 20)
{
if(cacheLevel < 1)
{
throw new ArgumentException("cacheLevel must be greater than or equal to 1", nameof(cacheLevel));
}
Expression<Func<T, bool>> predicate;
var propertyIsNullable = Nullable.GetUnderlyingType(typeof(T).GetProperty(propertyName).PropertyType) != null;
// fill a list of cachableLevel number of parameters for the property, equal the selected items and padded with the placeholder value to fill the list.
Expression finalExpression = Expression.Constant(false);
var parameter = Expression.Parameter(typeof(T), "x");
/* factor makes sure that this query part contains a multitude of m parameters (i.e. 20, 40, 60, ...),
* so the number of query plans is limited even if lots of users have more than m items selected */
int factor = Math.Max(1, (int)Math.Ceiling((double)ids.Count / cacheLevel));
for (var i = 0; i < factor * cacheLevel; i++)
{
U id = placeholderValue;
if (i < ids.Count)
{
id = ids.ElementAt(i);
}
var temp = new { id };
var constant = Expression.Constant(temp);
var field = Expression.Property(constant, "id");
var member = Expression.Property(parameter, propertyName);
if (propertyIsNullable)
{
member = Expression.Property(member, "Value");
}
var expression = Expression.Equal(member, field);
finalExpression = Expression.OrElse(finalExpression, expression);
}
predicate = Expression.Lambda<Func<T, bool>>(finalExpression, parameter);
return predicate;
}
}
}