【问题标题】:Exhaustive search / generate each combination of an expression tree穷举搜索/生成表达式树的每个组合
【发布时间】:2015-11-03 13:42:11
【问题描述】:

我正在使用基本的表达式树优化器来构建查询计划。在解析树时,我可以决定如何“最好”地构造它,这取决于我可以分配给每个操作的权重。

如果我有一个简单的树,有两个关于如何执行操作的选择,我希望能够生成树的两种变体,然后可以比较每个变体的权重,看看什么是最有效的.

例如,下面的代码将允许我构造表达式树连接操作的两种变体:一种带有MergeJoinExpression,另一种带有NestedLoopJoinExpression

class Customer
{
        public int Id { get; set; }
}
class Orders
{
        public int Id { get; set; }
        public int CustomerId { get; set; }
}

class MergeJoinExpresion : JoinExpression
{
}

class NestLoopJoinExpresion : JoinExpression
{
}

class Visitor : ExpressionVisitor
{
    public List<Expression> GetPlans(Expression expr)
    {
        // ???
    }

    override VisitJoin(JoinExpression join)
    {
        // For this join, I can return the following (trite example)
        // return MergeJoinExpresion
        // return NestLoopJoinExpresion

        return base.VisitJoin(join);
    }
}

我如何构造一个方法来生成树的每个变体并将它们返回给我?

class Program
{
        static void Main(string[] args)
        {
             var query = from c in customers
                        join o in orders on c.Id equals o.CustomerId
                        select new
                        {
                            CustomerId = c.Id,
                            OrderId = o.Id
                        };


            var plans = new Visitor().GetPlans(query);
        }
}

谁能告诉我如何修改VisitorGetPlans 方法来生成这些变体?

编辑 - 类似:

class Visitor : ExpressionVisitor
{
    private List<Expression> exprs = new List<Expression>();

    public List<Expression> GetPlans(Expression expr)
    {
        Visit(expr);    
        return exprs;
    }

    override VisitJoin(JoinExpression join)
    {
        // For this join, I can return the following (trite example)
        // return MergeJoinExpresion
        // return NestLoopJoinExpresion      
        var choices = new Expression[] { MergeJoinExpresion.Create(join), NestLoopJoinExpresion.Create(join) };

        foreach(var choice in choices)
        {
             var cloned = Cloner.Clone(choice);
             var newTree = base.VisitJoin(cloned);
             exprs.Add(newTree);
        }

        return base.VisitJoin(join);
    }
}

【问题讨论】:

  • 不确定访问者是否是最好的方法。 VisitJoin 必须返回它生成的多个变体。所有调用者都必须支持多个变体,并且它们自己可能会生成多个变体。无论如何,加入计划通常不会以这种简单详尽的方式完成,因为时间复杂度是指数级的。
  • 连接只是我在一个非常简单的例子中能想到的最简单的例子。我正在使用表达式树来生成不同的计划 - 并且需要一种生成每个可能结果的方法(无论是用于联接还是其他)。所以问题是如何生成可以在期间生成的不同版本的树访问节点。这样我就可以通过检查详尽的搜索来仔细检查我选择的查询计划是不是最好的......
  • 我猜你可以让它工作,如果你让访问者方法返回 IEnumerable 而不是 Expression。
  • 我认为这不是一个好主意。访问者必须符合基本访问者(并且只返回表达式)。需要发生的事情(尽管我在这里遇到了一个心理障碍)是 - 计算出可以做出多少变化并为每个变化构建一个新树。
  • 您还需要更改“基本访问者”。没有办法通过一个表达式来引导多个表达式。您不仅需要一个连接节点上的变体,还需要变体本身的变体。顺便说一句,如果您编写递归方法来遍历树,这将是简单而明显的。返回值将是一个 IEnumerable。唯一的问题是如何调整访问者模式来做到这一点。

标签: c# tree expression expression-trees visitor-pattern


【解决方案1】:

首先我们将创建一个访问者,它可以帮助我们从Expression 中提取JoinExpression 对象列表:

internal class FindJoinsVisitor : ExpressionVisitor
{
    private List<JoinExpression> expressions = new List<JoinExpression>();
    protected override Expression VisitJoin(JoinExpression join)
    {
        expressions.Add(join);
        return base.VisitJoin(join);
    }
    public IEnumerable<JoinExpression> JoinExpressions
    {
        get
        {
            return expressions;
        }
    }
}
public static IEnumerable<JoinExpression> FindJoins(
    this Expression expression)
{
    var visitor = new FindJoinsVisitor();
    visitor.Visit(expression);
    return visitor.JoinExpressions;
}

接下来我们将使用以下方法(取自this blog post)来获取序列序列的笛卡尔积:

static IEnumerable<IEnumerable<T>> CartesianProduct<T>(
    this IEnumerable<IEnumerable<T>> sequences) 
{ 
    IEnumerable<IEnumerable<T>> emptyProduct = new[] { Enumerable.Empty<T>() }; 
    return sequences.Aggregate( 
        emptyProduct, 
        (accumulator, sequence) => 
            from accseq in accumulator 
            from item in sequence 
            select accseq.Concat(new[] {item})); 
}

接下来,我们将创建一个访问者,它接受一系列表达式对,并将该对中第一个表达式的所有实例替换为第二个:

internal class ReplaceVisitor : ExpressionVisitor
{
    private readonly Dictionary<Expression, Expression> lookup;
    public ReplaceVisitor(Dictionary<Expression, Expression> pairsToReplace)
    {
        lookup = pairsToReplace;
    }
    public override Expression Visit(Expression node)
    {
        if(lookup.ContainsKey(node))
            return base.Visit(lookup[node]);
        else
            return base.Visit(node);
    }
}

public static Expression ReplaceAll(this Expression expression,
    Dictionary<Expression, Expression> pairsToReplace)
{
    return new ReplaceVisitor(pairsToReplace).Visit(expression);
}

public static Expression ReplaceAll(this Expression expression,
    IEnumerable<Tuple<Expression, Expression>> pairsToReplace)
{
    var lookup = pairsToReplace.ToDictionary(pair => pair.Item1, pair => pair.Item2);
    return new ReplaceVisitor(lookup).Visit(expression);
}

最后,我们通过查找表达式中的所有连接表达式将所有内容放在一起,将它们投影到一系列对中,其中JoinExpression 是对中的第一项,第二项是每个可能的替换值。从那里我们可以使用它的笛卡尔积来获得表达式替换对的所有组合。最后,我们可以将每个替换组合投影到表达式中,该表达式实际上是替换原始表达式中的所有这些对:

public static IEnumerable<Expression> AllJoinCombinations(Expression expression)
{
    var combinations = expression.FindJoins()
        .Select(join => new Tuple<Expression, Expression>[]
        {
            Tuple.Create<Expression, Expression>(join, new NestLoopJoinExpresion(join)), 
            Tuple.Create<Expression, Expression>(join, new MergeJoinExpresion(join)),
        })
        .CartesianProduct();

    return combinations.Select(combination => expression.ReplaceAll(combination));
}

【讨论】:

  • 这是一个非常好的解决方案,因为它不需要更改访问者以返回序列而不是单例。但是,尽管对于每个连接,您只创建一次替代方案,但它如何工作?似乎必须为每个替换操作创建备选方案,因为子树每次都因子替换而不同。替换依赖于替换后的子子树。
  • @usr 你吓了我一秒,但修复其实很简单。通过让ReplaceAll 访问被替换的节点,它将最终遍历 NestLoop 或 Merge 表达式的整个树,并根据提供的映射替换所有嵌套的 JoinExpression 元素,因此所有需要更改的只是添加在返回查找结果之前调用base.VIsit
  • 酷。不过,希望看到对此代码进行一些令人信服的单元测试 :) 我认为这是详尽测试的好案例。
  • @Servy - 理想的解决方案,您完全理解我的要求,并提供了一个优雅的解决方案,我可以修改和扩展它来解决我的问题......很棒的东西
【解决方案2】:

你肯定需要不可变的树。

创建一个类:

class JoinOptionsExpression: JoinExpression {
    public IEnumerable<JoinExpression> Options {get; private set;}
    private JoinOptionsExpression(){}
    public static JoinOptionsExpression Create(IEnumerable<JoinExpression> options){
        return new JoinOptionsExpression{Options = options.ToList().AsReadOnly()}; // you can improve this probably
    }
}

然后在您的 VisitJoin 方法中返回选项,并返回所有选项:

private List<Dictionary<JoinOptionsExpression,int>> selections = new List<Dictionary<JoinOptionsExpression,int>>{new Dictionary<JoinOptionsExpression,int>()};
override VisitJoin(JoinExpression join)
{
    var choices = new Expression[] { MergeJoinExpresion.Create(join), NestLoopJoinExpresion.Create(join) };
    List<Expression> exprs = new List<Expression>();
    foreach(var choice in choices)
    {
         var cloned = Cloner.Clone(choice);
         var newTree = base.VisitJoin(cloned);
         exprs.Add(newTree);
    }
    var result = JoinOptionsExpression.Create(exprs);
    // now add all choices
    if (exprs.Count > 0)
        foreach (selection in selections.ToList()) // to make sure your don't modify during enumeration, you can improve this too
        {
            selection.Add(result, 0);
            for (i=1; i<exprs.Count; i++)
            {
                var copy= new Dictionary<JoinOptionsExpression, int>(selection);
                copy[result] = i;
                selections.Add(copy);
            }
        }
    return result;
}

然后您将需要第二个访问者,它源自框架访问者,并且没有其他原因,只需提取您的选项:

class OptionsExtractor:ExpressionVisitor
{
    public IEnumerable<Expression> Extract(Expression expression, List<Dictionary<JoinOptionsExpression,int>> selections)
    {
        foreach(var selection in selections)
        {
            currentSelections = selection;
            yield return Visit(expression);
        }
    }
    private Dictionary<JoinOptionsExpression,int> currentSelections;
    override Expression Visit(Expression node)
    {
        var opts = node as JoinOptionsExpression;
        if (opts != null)
            return base.Visit(opts.Options.ElementAt(currentSelections[opts]);
        else
            return base.Visit(node);
    }
}

无论如何,详尽的搜索很快就会在您的脸上爆炸,我想您知道这一点。 免责声明:我只是在这个编辑器中输入了这个,它甚至可能无法编译,但你应该能够明白。

【讨论】:

  • JoinOptionsExpression 是一个 hack。最好立即通过一个集合。除此之外,这是正确的方法,我在 cmets 中提倡它。
  • @usr:是的,JoinOptionsExpression 是一种将所有可能的子节点分组到一个节点中的技巧。对我来说,这将允许更好的调试概述,一棵树包含所有选项。
  • @MBoros,很好的解决方案 - 你完全理解我的要求并提供了一个可行的解决方案。我没有将其标记为答案的唯一原因是 JoinOptionsExpression 类 - 我不想要一个新的表达式类型,其唯一目的是处理对连接的详尽搜索,特别是因为我需要其中一些用于不同的表达式类型,并且可以在没有这些的情况下解决整体问题。不过答案很好。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-11-18
  • 2023-04-08
  • 2016-08-12
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多