针对 SQL 后端的 LINQ 的可扩展包含方法
Posted
技术标签:
【中文标题】针对 SQL 后端的 LINQ 的可扩展包含方法【英文标题】:Scalable Contains method for LINQ against a SQL backend 【发布时间】:2014-08-23 10:15:15 【问题描述】:我正在寻找一种优雅的方式来以可扩展的方式执行 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
语句不可扩展。根据MSDN:
在 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();
(其中ToChunks
是this 小扩展方法)。
这会以 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
表达式。
也许有人可以帮我解决这个问题。当然,我愿意接受其他方法来使这种说法具有可扩展性。
【问题讨论】:
我遇到了同样的问题。如何使您的初始解决方案 (ToChunks) 异步运行? EF-core 用户:考虑使用this answer,而不是最高票数。 【参考方案1】:一个月前,我用一种稍微不同的方法解决了这个问题。也许这对您来说也是一个很好的解决方案。
我不希望我的解决方案更改查询本身。所以 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);
【讨论】:
这是一部很棒的作品!你应该在 Github 或 Codeplex 上分享它。它最接近我认为的“理想”,因此我将其标记为答案。唯一感觉有点不自然的部分是必须在ToChunkedList
方法中再次通过列表,但我不知道如何避免这种情况。多次使用Contains
的能力非常棒。【参考方案2】:
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);
【讨论】:
【参考方案3】:另一种方法是用这种方式构建谓词(当然,有些部分应该改进,只是给出想法)。
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);
【讨论】:
【参考方案4】:请允许我提供一种替代 Chunky 方法的方法。
在谓词中涉及Contains
的技术适用于:
如果您的本地数据具有这两个特征,Contains
将非常有用,因为这些小值集将在最终 SQL 查询中被硬编码。
当您的值列表具有熵(非常量)时,问题就开始了。在撰写本文时,实体框架(经典和核心)不会尝试以任何方式参数化这些值,这会强制 SQL Server 每次在查询中看到新的值组合时生成查询计划。此操作代价高昂,并且会因查询的整体复杂性而加剧(例如,许多表、列表中的许多值等)。
Chunky 方法仍然受到 SQL Server query plan cache pollution problem 的影响,因为它没有对查询进行参数化,它只是将创建大型执行计划的成本转移到更容易被 SQL Server 计算(和丢弃)的更小的执行计划中,此外,每个块都会向数据库添加额外的往返行程,这会增加解析查询所需的时间。
一种高效的解决方案(目前适用于 EF Core)
如果能以一种对 SQL Server 友好的方式在查询中组合本地数据不是很好吗?输入QueryableValues。
我设计这个库有两个主要目标:
必须解决 SQL Server 的查询计划缓存污染问题✅ 必须是fast!⚡它有一个灵活的 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
);
你也可以compose complex types!
不用说,提供的IEnumerable<T>
仅在您的查询实现时(而不是之前)枚举,在这方面保留了 EF Core 的相同行为。
它是如何工作的?
QueryableValues 在内部创建参数化查询,并以 SQL Server 本身可以理解的序列化格式提供您的值。这样一来,您的查询就可以通过单次往返数据库来解决,并避免由于其参数化性质而在后续执行中创建新的查询计划。
有用的链接
Nuget Package GitHub Repository Benchmarks SQL Server Cache Pollution ProblemQueryableValues 在 MIT 许可下分发
【讨论】:
这基本上是一个仅链接的答案。如果链接断开,则答案将失去其价值。甚至指向 Stack Overflow 答案的链接也可能会中断(例如,如果用户删除了问题)。除此之外,这里没有回答我的问题。 也就是说,这将是this question 的答案,因此您可以考虑在此处添加答案,但请根据具体情况调整您的答案并显示可以解决问题的代码。 @Gert 我不确定我的答案的格式,因为这个问题与不同的用例重叠,我想避免使用链接重复。我会听取您的建议并在此处和其他问题中定制更好的答案。感谢您的反馈。 @GertArnold 我对我的答案进行了更改。我希望它现在为这个问题提供更多价值。 是的,这样更好。尽管它不是解决原始问题的方法,但它可能有助于防止附带损害。值得研究。【参考方案5】:使用带有表值参数的存储过程也可以很好地工作。您实际上在表/视图和表值参数之间的存储过程中编写了一个联合。
https://docs.microsoft.com/en-us/dotnet/framework/data/adonet/sql/table-valued-parameters
【讨论】:
以上是关于针对 SQL 后端的 LINQ 的可扩展包含方法的主要内容,如果未能解决你的问题,请参考以下文章