Scalable包含针对SQL后端的LINQ方法

时间:2014-07-02 14:47:25

标签: performance linq entity-framework scalability

我正在寻找一种以可扩展方式执行Contains()语句的优雅方式。在我提出实际问题之前,请允许我给出一些背景知识。

IN声明

在Entity Framework和LINQ to SQL中,Contains语句被翻译为SQL IN语句。例如,从这个声明:

var ids = Enumerable.Range(1,10);
var courses = Courses.Where(c => ids.Contains(c.CourseID)).ToList();

实体框架将生成

SELECT 
    [Extent1].[CourseID] AS [CourseID], 
    [Extent1].[Title] AS [Title], 
    [Extent1].[Credits] AS [Credits], 
    [Extent1].[DepartmentID] AS [DepartmentID]
    FROM [dbo].[Course] AS [Extent1]
    WHERE [Extent1].[CourseID] IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

不幸的是,In语句不可扩展。根据{{​​3}}:

  

在IN子句中包含极大数量的值(数千个)会消耗资源并返回错误8623或8632

与耗尽资源或超出表达式限制有关。

之前发生这些错误时,随着项目数量的增加,IN语句变得越来越慢。我无法找到有关其增长率的文档,但它可以很好地执行数千个项目,但除此之外它变得非常缓慢。 (基于SQL Server经验)。

可扩展

我们不能总是避免这种说法。具有源数据的JOIN通常会表现得更好,但这只有在源数据位于同一上下文中时才有可能。在这里,我正在处理来自断开连接的场景中的客户端的数据。所以我一直在寻找可扩展的解决方案。一个令人满意的方法是将操作切成块:

var courses = ids.ToChunks(1000)
                 .Select(chunk => Courses.Where(c => chunk.Contains(c.CourseID)))
                 .SelectMany(x => x).ToList();

(其中ToChunksMSDN小扩展方法)。

这将以1000块的形式执行查询,这些查询都表现得非常好。用例如将运行5000个项目,5个查询,这些查询可能比具有5000个项目的一个查询更快。

但不是DRY

但是我当然不希望在我的代码中分散这个构造。我正在寻找一种扩展方法,通过该方法可以将任何IQueryable<T>转换为厚实的执行语句。理想情况下是这样的:

var courses = Courses.Where(c => ids.Contains(c.CourseID))
              .AsChunky(1000)
              .ToList();

但也许这个

var courses = Courses.ChunkyContains(c => c.CourseID, ids, 1000)
              .ToList();

我第一次给出了后一种解决方案:

public static IEnumerable<TEntity> ChunkyContains<TEntity, TContains>(
    this IQueryable<TEntity> query, 
    Expression<Func<TEntity,TContains>> match, 
    IEnumerable<TContains> containList, 
    int chunkSize = 500)
{
    return containList.ToChunks(chunkSize)
               .Select (chunk => query.Where(x => chunk.Contains(match)))
               .SelectMany(x => x);
}

显然,部分x => chunk.Contains(match)无法编译。但我不知道如何将match表达式操纵为Contains表达式。

也许有人可以帮我解决这个问题。当然,我愿意接受其他方法来使这个语句可扩展。

5 个答案:

答案 0 :(得分:8)

我在一个月之前用一种不同的方法解决了这个问题。也许这对你来说也是一个很好的解决方案。

我不希望我的解决方案更改查询本身。所以ids.ChunkContains(p.Id)或特殊的WhereContains方法是不可行的。该解决方案还应该能够将Contains与另一个过滤器结合使用,以及多次使用相同的集合。

db.TestEntities.Where(p => (ids.Contains(p.Id) || ids.Contains(p.ParentId)) && p.Name.StartsWith("Test"))

所以我尝试将逻辑封装在一个特殊的ToList方法中,该方法可以重写表达式,以便以块的形式查询指定的集合。

var ids = Enumerable.Range(1, 11);
var result = db.TestEntities.Where(p => Ids.Contains(p.Id) && p.Name.StartsWith ("Test"))
                                .ToChunkedList(ids,4);

为了重写表达式树,我在查询中发现了来自本地集合的所有Contains方法调用,并带有帮助类的视图。

private class ContainsExpression
{
    public ContainsExpression(MethodCallExpression methodCall)
    {
        this.MethodCall = methodCall;
    }

    public MethodCallExpression MethodCall { get; private set; }

    public object GetValue()
    {
        var parent = MethodCall.Object ?? MethodCall.Arguments.FirstOrDefault();
        return Expression.Lambda<Func<object>>(parent).Compile()();
    }

    public bool IsLocalList()
    {
        Expression parent = MethodCall.Object ?? MethodCall.Arguments.FirstOrDefault();
        while (parent != null) {
            if (parent is ConstantExpression)
                return true;
            var member = parent as MemberExpression;
            if (member != null) {
                parent = member.Expression;
            } else {
                parent = null;
            }
        }
        return false;
    }
}

private class FindExpressionVisitor<T> : ExpressionVisitor where T : Expression
{
    public List<T> FoundItems { get; private set; }

    public FindExpressionVisitor()
    {
        this.FoundItems = new List<T>();
    }

    public override Expression Visit(Expression node)
    {
        var found = node as T;
        if (found != null) {
            this.FoundItems.Add(found);
        }
        return base.Visit(node);
    }
}

public static List<T> ToChunkedList<T, TValue>(this IQueryable<T> query, IEnumerable<TValue> list, int chunkSize)
{
    var finder = new FindExpressionVisitor<MethodCallExpression>();
    finder.Visit(query.Expression);
    var methodCalls = finder.FoundItems.Where(p => p.Method.Name == "Contains").Select(p => new ContainsExpression(p)).Where(p => p.IsLocalList()).ToList();
    var localLists = methodCalls.Where(p => p.GetValue() == list).ToList();

如果在查询表达式中找到了在ToChunkedList方法中传递的本地集合,我将对原始列表的Contains调用替换为包含一个批处理的id的临时列表的新调用。

if (localLists.Any()) {
    var result = new List<T>();
    var valueList = new List<TValue>();

    var containsMethod = typeof(Enumerable).GetMethods(BindingFlags.Static | BindingFlags.Public)
                        .Single(p => p.Name == "Contains" && p.GetParameters().Count() == 2)
                        .MakeGenericMethod(typeof(TValue));

    var queryExpression = query.Expression;

    foreach (var item in localLists) {
        var parameter = new List<Expression>();
        parameter.Add(Expression.Constant(valueList));
        if (item.MethodCall.Object == null) {
            parameter.AddRange(item.MethodCall.Arguments.Skip(1));
        } else {
            parameter.AddRange(item.MethodCall.Arguments);
        }

        var call = Expression.Call(containsMethod, parameter.ToArray());

        var replacer = new ExpressionReplacer(item.MethodCall,call);

        queryExpression = replacer.Visit(queryExpression);
    }

    var chunkQuery = query.Provider.CreateQuery<T>(queryExpression);


    for (int i = 0; i < Math.Ceiling((decimal)list.Count() / chunkSize); i++) {
        valueList.Clear();
        valueList.AddRange(list.Skip(i * chunkSize).Take(chunkSize));

        result.AddRange(chunkQuery.ToList());
    }
    return result;
}
// if the collection was not found return query.ToList()
return query.ToList();

Expression Replacer:

private class ExpressionReplacer : ExpressionVisitor {

    private Expression find, replace;

    public ExpressionReplacer(Expression find, Expression replace)
    {
        this.find = find;
        this.replace = replace;
    }

    public override Expression Visit(Expression node)
    {
        if (node == this.find)
            return this.replace;

        return base.Visit(node);
    }
}

答案 1 :(得分:1)

Linqkit救援!可能是一种直接做到这一点的更好的方式,但这似乎工作得很好,并且很清楚正在做什么。添加的内容为AsExpandable(),可让您使用Invoke扩展名。

using LinqKit;

public static IEnumerable<TEntity> ChunkyContains<TEntity, TContains>(
    this IQueryable<TEntity> query, 
    Expression<Func<TEntity,TContains>> match, 
    IEnumerable<TContains> containList, 
    int chunkSize = 500)
{
    return containList
            .ToChunks(chunkSize)
            .Select (chunk => query.AsExpandable()
                                   .Where(x => chunk.Contains(match.Invoke(x))))
            .SelectMany(x => x);
}

您可能还想这样做:

containsList.Distinct()
            .ToChunks(chunkSize)

...或类似的东西,如果出现这种情况,你不会得到重复的结果:

query.ChunkyContains(x => x.Id, new List<int> { 1, 1 }, 1);

答案 2 :(得分:1)

另一种方法是以这种方式构建谓词(当然,应该改进一些部分,只是提出想法)。

public static Expression<Func<TEntity, bool>> ContainsPredicate<TEntity, TContains>(this IEnumerable<TContains> chunk, Expression<Func<TEntity, TContains>> match)
        {
            return Expression.Lambda<Func<TEntity, bool>>(Expression.Call(
                typeof (Enumerable),
                "Contains",
                new[]
                {
                    typeof (TContains)
                },
                Expression.Constant(chunk, typeof(IEnumerable<TContains>)), match.Body),
                match.Parameters);
        }

你可以在你的ChunkContains方法中调用

return containList.ToChunks(chunkSize)
               .Select(chunk => query.Where(ContainsPredicate(chunk, match)))
               .SelectMany(x => x);

答案 3 :(得分:0)

有没有办法让你用两个数据上下文来解决这个问题?

两个底层数据库是否有任何方式可以相互通信。例如,在SQL Server中,您可以创建链接服务器,以允许您对两个表运行查询,就好像它们位于同一个数据库中一样。

根据数据更改的频率,您可以设置ETL过程以将ID(和相关数据)导入数据库。

答案 4 :(得分:0)

使用带有表值参数的存储过程也可以正常工作。实际上你在表/视图和表值参数之间的存储过程中编写了一个联合。

https://docs.microsoft.com/en-us/dotnet/framework/data/adonet/sql/table-valued-parameters