在Entity Framework LINQ查询中使用IEnumerable.Contains时如何避免查询计划重新编译?

时间:2014-08-10 11:58:36

标签: performance linq entity-framework linq-to-entities

我使用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 进行查询的同时避免重新编译?

2 个答案:

答案 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;
        }
    }
}