针对SQL后端的LINQ的可扩展包含方法

vs91vp4v  于 2022-12-06  发布在  其他
关注(0)|答案(5)|浏览(176)

我正在寻找一种优雅的方式来以可伸缩的方式执行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语句是不可伸缩的。
在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();

(其中ToChunksthis的小扩展方法)。
这将以1000个数据块为单位执行查询,这些数据块的性能都很好。例如,对于5000个项目,将运行5个查询,这可能比一个具有5000个项目的查询更快。

但不干燥

当然,我并不想把这个构造分散到我的代码中,我正在寻找一种扩展方法,通过它可以将任何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表达式。
也许有人能帮助我使这个解决方案发挥作用。当然,我也愿意用其他方法使这个声明具有可扩展性。

5t7ly7z5

5t7ly7z51#

一个月前,我已经用一种稍微不同的方法解决了这个问题。也许这对你来说也是一个很好的解决方案。
我不希望我的解决方案改变查询本身。因此ids.ChunkContains(p.Id)或特殊的WhereContains方法是不可行的。此外,解决方案应该能够将Contains与另一个过滤器组合,以及多次使用同一集合。

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

因此,我尝试将逻辑封装在一个特殊的ToList方法中,该方法可以为要按块查询的指定集合重写Expression。

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

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

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();

表达式替换器:

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);
    }
}
g2ieeal7

g2ieeal72#

请允许我提供一个替代大块的方法。
在 predicate 中涉及Contains的技术适用于:

*常量值列表(非易失性)。
*值列表。

如果您的本地数据具有这两个特征,Contains将非常有用,因为这些小的值集将在最终的SQL查询中硬编码。
当你的值列表有熵时,问题就开始了(非常量)。在撰写本文时,实体框架(经典和核心)不要试图以任何方式参数化这些值,这会强制SQL Server在每次看到查询中的新值组合时生成查询计划。此操作开销很大,并且会因查询的整体复杂性而加剧(例如,许多表、列表中的许多值等)。
Chunky方法仍然存在SQL Server查询计划高速缓存污染问题,因为它不对查询进行参数化,只是将创建大型执行计划的成本转移到更易于SQL Server计算(和丢弃)的较小执行计划中,此外,每个块都增加了到数据库的额外往返,这增加了解析查询所需的时间。

EF Core的高效解决方案

🎉 新!QueryableValues EF6 Edition has arrived!对于EF Core,请继续阅读下面的内容。

如果能够以SQL Server友好的方式在查询中组合本地数据,那不是很好吗?输入QueryableValues
我设计这个库有两个主要目标:

  • 它必须解决SQL Server的查询计划缓存污染问题
  • 一定要快!

它有一个灵活的API,允许您组合由IEnumerable<T>提供的本地数据,并返回一个IQueryable<T>;就像它是你的DbContext的另一个实体一样使用它(真的),例如:

// Sample values.
IEnumerable<int> values = Enumerable.Range(1, 1000);

// Using a Join (query syntax).
var query1 = 
    from e in dbContext.MyEntities
    join v in dbContext.AsQueryableValues(values) on e.Id equals v 
    select new
    {
        e.Id,
        e.Name
    };

// Using Contains (method syntax)
var query2 = dbContext.MyEntities
    .Where(e => dbContext.AsQueryableValues(values).Contains(e.Id))
    .Select(e => new
    {
        e.Id,
        e.Name
    });

您还可以组合复杂类型!
不用说,所提供的IEnumerable<T>只在查询被物化时(而不是之前)被枚举,在这方面保持EF Core的相同行为。

如何工作?

QueryableValues会在内部建立参数化查询,并以SQL Server本身可识别的序列化格式提供您的值。这可让您的查询只需要到数据库的一次往返行程就能解析,并避免在后续执行时建立新的查询计划(因为它的参数化性质)。

有用的链接

  • Nuget Package
  • GitHub储存库
  • 基准测试
  • SQL Server缓存污染问题

QueryableValues根据MIT许可证分发

ibrsph3r

ibrsph3r3#

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);
qltillow

qltillow4#

另一种方法是以这种方式构建 predicate (当然,有些部分应该改进,只是给出了想法)。

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);
rlcwz9us

rlcwz9us5#

使用具有表值参数的存储过程也可以很好地工作。实际上,您在存储过程中在表/视图和表值参数之间编写了一个连接。
https://learn.microsoft.com/en-us/dotnet/framework/data/adonet/sql/table-valued-parameters

相关问题