我有以下通用可查询(可能已经应用了选项):
IQueryable<TEntity> queryable = DBSet<TEntity>.AsQueryable();
然后有Provider
类看起来像这样:
public class Provider<TEntity>
{
public Expression<Func<TEntity, bool>> Condition { get; set; }
[...]
}
可以按以下方式为每个实例定义Condition
:
Condition = entity => entity.Id == 3;
现在,我想选择至少由Provider
的一个实体满足Condition
的所有DBSet
个实例:
List<Provider> providers = [...];
var matchingProviders = providers.Where(provider => queryable.Any(provider.Condition))
问题:我正在开始查询列表中的每个Provider
实例。我宁愿使用单个查询来获得相同的结果。由于性能可疑,此主题尤其重要。如何使用Linq
语句或Expression Trees
语句在单个查询中获得相同的结果并提高效果?
答案 0 :(得分:5)
有趣的挑战。我看到的唯一方法是构建动态SELECT TOP 1 0 FROM Table WHERE Condition[0]
UNION ALL
SELECT TOP 1 1 FROM Table WHERE Condition[1]
...
UNION ALL
SELECT TOP 1 N-1 FROM Table WHERE Condition[N-1]
查询,如下所示:
var parameter = Expression.Parameter(typeof(TEntity), "e");
var indexQuery = providers
.Select((provider, index) => queryable
.Where(provider.Condition)
.Take(1)
.Select(Expression.Lambda<Func<TEntity, int>>(Expression.Constant(index), parameter)))
.Aggregate(Queryable.Concat);
var indexes = indexQuery.ToList();
var matchingProviders = indexes.Select(index => providers[index]);
然后使用返回的数字作为索引来获取匹配的提供者。
这样的事情:
Expression
请注意,我可以在不使用Select
类的情况下构建查询,方法是将上面的.Select(_ => index)
替换为
Active
但这会为每个索引引入不必要的SQL查询参数。
答案 1 :(得分:4)
这是我心中想到的另一个(疯狂的)想法。请注意,与我之前的答案类似,它并不能保证更好的性能(事实上它可能更糟)。它只是提供了一种通过单个SQL查询来执行您所要求的方法。
这里我们将创建一个返回单个string
的查询,其长度为N,由'0'和'1'组成,'1'表示匹配(类似于字符串位数组)。该查询将使用我最喜欢的组,通过常量技术动态构建如下:
var matchInfo = queryable
.GroupBy(e => 1)
.Select(g =>
(g.Max(Condition[0] ? "1" : "0")) +
(g.Max(Condition[1] ? "1" : "0")) +
...
(g.Max(Condition[N-1] ? "1" : "0")))
.FirstOrDefault() ?? "";
以下是代码:
var group = Expression.Parameter(typeof(IGrouping<int, TEntity>), "g");
var concatArgs = providers.Select(provider => Expression.Call(
typeof(Enumerable), "Max", new[] { typeof(TEntity), typeof(string) },
group, Expression.Lambda(
Expression.Condition(
provider.Condition.Body, Expression.Constant("1"), Expression.Constant("0")),
provider.Condition.Parameters)));
var concatCall = Expression.Call(
typeof(string).GetMethod("Concat", new[] { typeof(string[]) }),
Expression.NewArrayInit(typeof(string), concatArgs));
var selector = Expression.Lambda<Func<IGrouping<int, TEntity>, string>>(concatCall, group);
var matchInfo = queryable
.GroupBy(e => 1)
.Select(selector)
.FirstOrDefault() ?? "";
var matchingProviders = matchInfo.Zip(providers,
(match, provider) => match == '1' ? provider : null)
.Where(provider => provider != null)
.ToList();
享受:)
PS 在我看来,此查询将以恒定速度运行(关于条件的数量和类型,即可以被认为是最好的 O(N),最差和平均情况,其中 N 是表中记录的数量),因为数据库必须始终执行全表扫描。知道什么是实际表现仍然很有趣,但最有可能做这样的事情并不值得努力。
更新:关于赏金和更新后的要求:
查找快速查询仅读取表格的记录,如果已满足所有条件则结束查询
没有满足这两个条件的标准SQL构造(甚至不谈论LINQ查询转换)。允许像EXISTS
这样的早期结束的构造可以用于单个条件,因此当针对多个条件执行时,将违反仅读取一次表记录的第一个规则。虽然在这个答案中使用聚合的构造满足第一个规则,但是为了产生聚合值,它们必须读取所有记录,因此不能提前退出。
很快,没有任何查询可以满足这两个要求。那么 fast 部分,它实际上取决于数据的大小以及条件的数量和类型,表索引等,所以对于所有情况,根本没有“最佳”的通用解决方案。
答案 2 :(得分:3)
基于@Ivan的这个Post我创建了一个在某些情况下稍微快一点的表达式。
它使用Any
代替Max
来获得所需的结果。
var group = Expression.Parameter(typeof(IGrouping<int, TEntity>), "g");
var anyMethod = typeof(Enumerable)
.GetMethods()
.First(m => m.Name == "Any" && m.GetParameters()
.Count() == 2)
.MakeGenericMethod(typeof(TEntity));
var concatArgs = Providers.Select(provider =>
Expression.Call(anyMethod, group,
Expression.Lambda(provider.Condition.Body, provider.Condition.Parameters)));
var convertExpression = concatArgs.Select(concat =>
Expression.Condition(concat, Expression.Constant("1"), Expression.Constant("0")));
var concatCall = Expression.Call(
typeof(string).GetMethod("Concat", new[] { typeof(string[]) }),
Expression.NewArrayInit(typeof(string), convertExpression));
var selector = Expression.Lambda<Func<IGrouping<int, TEntity>, string>>(concatCall, group);
var matchInfo = queryable
.GroupBy(e => 1)
.Select(selector)
.First();
var MatchingProviders = matchInfo.Zip(Providers,
(match, provider) => match == '1' ? provider : null)
.Where(provider => provider != null)
.ToList();
答案 3 :(得分:1)
我在这里尝试的方法是创建Conditions
并将它们嵌套到一个Expression
中。如果符合Conditions
之一,我们会获得Provider
的索引。
private static Expression NestedExpression(
IEnumerable<Expression<Func<TEntity, bool>>> expressions,
int startIndex = 0)
{
var range = expressions.ToList();
range.RemoveRange(0, startIndex);
if (range.Count == 0)
return Expression.Constant(-1);
return Expression.Condition(
range[0].Body,
Expression.Constant(startIndex),
NestedExpression(expressions, ++startIndex));
}
由于Expressions
仍然可以使用不同的ParameterExpressions
,我们需要ExpressionVisitor
来重写这些内容:
private class PredicateRewriterVisitor : ExpressionVisitor
{
private readonly ParameterExpression _parameterExpression;
public PredicateRewriterVisitor(ParameterExpression parameterExpression)
{
_parameterExpression = parameterExpression;
}
protected override Expression VisitParameter(ParameterExpression node)
{
return _parameterExpression;
}
}
对于重写,我们只需要调用此方法:
private static Expression<Func<T, bool>> Rewrite<T>(
Expression<Func<T, bool>> exp,
ParameterExpression parameterExpression)
{
var newExpression = new PredicateRewriterVisitor(parameterExpression).Visit(exp);
return (Expression<Func<T, bool>>)newExpression;
}
查询本身和Provider
实例的选择如下:
var parameterExpression = Expression.Parameter(typeof(TEntity), "src");
var conditions = Providers.Select(provider =>
Rewrite(provider.Condition, parameterExpression)
);
var nestedExpression = NestedExpression(conditions);
var lambda = Expression.Lambda<Func<TEntity, int>>(nestedExpression, parameterExpression);
var matchInfo = queryable.Select(lambda).Distinct();
var MatchingProviders = Providers.Where((provider, index) => matchInfo.Contains(index));
注意:另一个选项并不是很快
答案 4 :(得分:1)
以下是与表达式无关的问题的另一种观点。
由于主要目标是提高性能,如果尝试使用单个查询生成结果没有帮助,我们可以尝试通过并行执行原始多查询解决方案来提高速度。
由于它确实是LINQ to Objects查询(内部执行多个EF查询),理论上应该通过插入PLINQ来将其转换为AsParallel
查询这(不工作):
var matchingProviders = providers
.AsParallel()
.Where(provider => queryable.Any(provider.Condition))
.ToList();
然而,事实证明EF DbContext
不适合多线程访问,而上述只是生成运行时错误。所以我不得不使用允许我们提供本地状态的TPL重载之一来Parallel.ForEach
,我曾经在执行期间分配了几个DbContext
实例。
最终的工作代码如下所示:
var matchingProviders = new List<Provider<TEntity>>();
Parallel.ForEach(providers,
() => new
{
context = new MyDbContext(),
matchingProviders = new List<Provider<TEntity>>()
},
(provider, state, data) =>
{
if (data.context.Set<TEntity>().Any(provider.Condition))
data.matchingProviders.Add(provider);
return data;
},
data =>
{
data.context.Dispose();
if (data.matchingProviders.Count > 0)
{
lock (matchingProviders)
matchingProviders.AddRange(data.matchingProviders);
}
}
);
如果你有一个多核CPU(现在很正常)和一个好的数据库服务器,这应该可以为你提供所需的改进。