我正在寻找一种优雅的方式来以可伸缩的方式执行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();
(其中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
表达式。
也许有人能帮助我使这个解决方案发挥作用。当然,我也愿意用其他方法使这个声明具有可扩展性。
5条答案
按热度按时间5t7ly7z51#
一个月前,我已经用一种稍微不同的方法解决了这个问题。也许这对你来说也是一个很好的解决方案。
我不希望我的解决方案改变查询本身。因此ids.ChunkContains(p.Id)或特殊的WhereContains方法是不可行的。此外,解决方案应该能够将Contains与另一个过滤器组合,以及多次使用同一集合。
因此,我尝试将逻辑封装在一个特殊的ToList方法中,该方法可以为要按块查询的指定集合重写Expression。
为了重写表达式树,我在查询中发现了所有来自本地集合的Contains Method调用,并使用了视图帮助类。
如果在查询表达式中找到了ToChunkedList方法中传递的本地集合,我会将对原始列表的Contains调用替换为对包含一个批次的id的临时列表的新调用。
表达式替换器:
g2ieeal72#
请允许我提供一个替代大块的方法。
在 predicate 中涉及
Contains
的技术适用于:*常量值列表(非易失性)。
*小值列表。
如果您的本地数据具有这两个特征,
Contains
将非常有用,因为这些小的值集将在最终的SQL查询中硬编码。当你的值列表有熵时,问题就开始了(非常量)。在撰写本文时,实体框架(经典和核心)不要试图以任何方式参数化这些值,这会强制SQL Server在每次看到查询中的新值组合时生成查询计划。此操作开销很大,并且会因查询的整体复杂性而加剧(例如,许多表、列表中的许多值等)。
Chunky方法仍然存在SQL Server查询计划高速缓存污染问题,因为它不对查询进行参数化,只是将创建大型执行计划的成本转移到更易于SQL Server计算(和丢弃)的较小执行计划中,此外,每个块都增加了到数据库的额外往返,这增加了解析查询所需的时间。
EF Core的高效解决方案
🎉 新!QueryableValues
EF6 Edition
has arrived!对于EF Core,请继续阅读下面的内容。如果能够以SQL Server友好的方式在查询中组合本地数据,那不是很好吗?输入QueryableValues。
我设计这个库有两个主要目标:
它有一个灵活的API,允许您组合由
IEnumerable<T>
提供的本地数据,并返回一个IQueryable<T>
;就像它是你的DbContext
的另一个实体一样使用它(真的),例如:您还可以组合复杂类型!
不用说,所提供的
IEnumerable<T>
只在查询被物化时(而不是之前)被枚举,在这方面保持EF Core的相同行为。如何工作?
QueryableValues会在内部建立参数化查询,并以SQL Server本身可识别的序列化格式提供您的值。这可让您的查询只需要到数据库的一次往返行程就能解析,并避免在后续执行时建立新的查询计划(因为它的参数化性质)。
有用的链接
QueryableValues根据MIT许可证分发
ibrsph3r3#
Linqkit来拯救我们!直接执行可能是一种更好的方法,但这似乎工作得很好,并且使正在执行的操作非常清楚。添加了
AsExpandable()
,它允许您使用Invoke
扩展。您可能还需要执行以下操作:
......或类似的内容,这样在发生以下情况时不会得到重复的结果:
qltillow4#
另一种方法是以这种方式构建 predicate (当然,有些部分应该改进,只是给出了想法)。
您可以在ChunkContains方法中调用它
rlcwz9us5#
使用具有表值参数的存储过程也可以很好地工作。实际上,您在存储过程中在表/视图和表值参数之间编写了一个连接。
https://learn.microsoft.com/en-us/dotnet/framework/data/adonet/sql/table-valued-parameters