Performance 可伸缩包含针对SQL后端的LINQ方法

Performance 可伸缩包含针对SQL后端的LINQ方法,performance,linq,entity-framework,scalability,Performance,Linq,Entity Framework,Scalability,我正在寻找一种优雅的方式,以可伸缩的方式执行语句。在我回答实际问题之前,请允许我提供一些背景资料 语句中的 在实体框架和LINQ to SQL中,Contains语句被转换为SQLIn语句。例如,从这句话中: var-id=Enumerable.Range(1,10); var courses=courses.Where(c=>id.Contains(c.CourseID)).ToList(); 实体框架将生成 选择 [Extent1][CourseID]作为[CourseID], [Exte

我正在寻找一种优雅的方式,以可伸缩的方式执行语句。在我回答实际问题之前,请允许我提供一些背景资料

语句中的

在实体框架和LINQ to SQL中,
Contains
语句被转换为SQL
In
语句。例如,从这句话中:

var-id=Enumerable.Range(1,10);
var courses=courses.Where(c=>id.Contains(c.CourseID)).ToList();
实体框架将生成

选择
[Extent1][CourseID]作为[CourseID],
[Extent1].[Title]作为[Title],
[Extent1]。[Credits]作为[Credits],
[Extent1][DepartmentID]作为[DepartmentID]
从[dbo].[Course]到[Extent1]
其中[Extent1].[CourseID]在(1,2,3,4,5,6,7,8,9,10)中
不幸的是,
语句中的
是不可伸缩的。根据:

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

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

但是在这些错误发生之前,
中的
语句会随着项目数量的增加而变得越来越慢。我找不到关于其增长率的文档,但它在数千项中表现良好,但除此之外,它的速度会显著减慢。(基于SQL Server经验)

可伸缩 我们不能总是避免这种说法。使用源数据进行
连接
通常会表现得更好,但这只有在源数据位于相同上下文中时才可能实现。在这里,我处理的是来自断开连接场景中的客户端的数据。因此,我一直在寻找一个可扩展的解决方案。结果证明,一种令人满意的方法是将操作分块进行:

var courses = ids.ToChunks(1000)
                 .Select(chunk => Courses.Where(c => chunk.Contains(c.CourseID)))
                 .SelectMany(x => x).ToList();
(其中,
ToChunks
是小扩展方法)

这将以1000个块的形式执行查询,所有这些块的性能都足够好。例如,对于5000个项目,5个查询将一起运行,这可能比一个包含5000个项目的查询运行得更快

但不是干的 但是我当然不想把这个结构散布在我的代码中。我正在寻找一种扩展方法,通过这种方法,任何
IQueryable
都可以转换为一个执行语句。理想情况下是这样的:

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);
}
公共静态IEnumerable ChunkyContains(
这是一个易懂的问题,
表情匹配,
IEnumerable容器列表,
int chunkSize=500)
{
返回containList.ToChunks(chunkSize)
.Select(chunk=>query.Where(x=>chunk.Contains(match)))
.SelectMany(x=>x);
}
显然,部分
x=>chunk.Contains(match)
没有编译。但是我不知道如何将
匹配
表达式操纵成
包含
表达式

也许有人能帮我解决这个问题。当然,我也愿意采用其他方法使这句话具有可扩展性。

来拯救我!这可能是一种更好的直接实现方法,但这似乎效果不错,并且非常清楚正在做什么。添加的内容是
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);
}
…或类似的内容,以便在发生这种情况时不会得到重复的结果:

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

另一种方法是以这种方式构建谓词(当然,有些部分需要改进,只是给出了想法)


一个月前,我用一种稍微不同的方法解决了这个问题。也许这对你来说也是个好办法

我不希望我的解决方案改变查询本身。因此,一个ids.ChunkContains(p.Id)或一个特殊的WhereContains方法是不可行的。解决方案还应该能够将包含与另一个筛选器组合,并多次使用同一集合

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);
为了重写表达式树,我在带有帮助类的视图的查询中发现all 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();

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

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


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

使用带有表值参数的存储过程也可以很好地工作。实际上,您在存储过程中的表/视图和表值参数之间编写了一个连接


这是一项伟大的工作!你应该在Github或Codeplex上共享它。这是最接近我所认为的“理想”的,所以我把它标记为答案。唯一感觉有点不自然的部分是必须在
ToChunkedList
方法中再次传递列表,但我不认为这可以避免。多次使用
包含的
的能力非常出色。1。不,这次不是。2.不是一个真正的解决方案,因为这是关于同步逻辑的,它会将问题转移到ETL过程。我可以理解ETL是否不是您的选择,但#2是一个真正的解决方案。迁移到ETL的唯一一件事是将数据从一个数据库传输到另一个数据库。真正的解决方案总是这样。出于好奇,你的数据来源是什么?我遇到了同样的问题。如何使初始解决方案(ToChunks)运行异步?
db.TestEntities.Where(p => (ids.Contains(p.Id) || ids.Contains(p.ParentId)) && p.Name.StartsWith("Test"))
var ids = Enumerable.Range(1, 11);
var result = db.TestEntities.Where(p => Ids.Contains(p.Id) && p.Name.StartsWith ("Test"))
                                .ToChunkedList(ids,4);
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();
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();
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);
    }
}