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
语句被转换为SQLIn
语句。例如,从这句话中:
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);
}
}