2015-11-03 36 views
3

我正在玩一个基本的表达式树优化器来构建查询计划。在解析树时,我可以根据我可以分配给每个操作的权重来决定如何构建它。详尽搜索/生成表达式树的每个组合

如果我有一个简单的树,有2个关于如何执行动作的选择,我希望能够生成树的两个变体,然后可以比较每个的权重以查看哪些是最高效。

例如,下面的代码会允许我来构建表达式树的两个变化加入操作:一个带有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); 
    } 
} 
+0

不确定访问者是否是最好的方式。 VisitJoin必须返回它生成的多个变体。所有呼叫者必须支持多种变体,并且他们自己可能会生成多种变体无论如何,加入计划通常不是以这种简单的穷举方式完成的,因为时间复杂性将呈指数级增长。 – usr

+0

这个连接只是我能想到的一个非常简单的例子中最简单的例子。我使用表达式树来生成不同的计划 - 并且需要一种生成每个可能结果的方法(无论它是用于Join还是别的)。所以问题是如何生成不同版本的树,可以在生成期间生成访问节点。这是我可以仔细检查,我选择的查询计划是最好的,通过检查一个详尽的搜索... – Jack

+0

我想你可以使它的工作,如果你让访问者的方法返回IEnumerable 而不是表达式。 – usr

回答

2

所以下手,我们将创建一个访问者,这将只是帮助我们从一个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)); 
} 
+0

这是一个非常好的解决方案,因为它不需要改变访问者返回序列而不是单身。但是,尽管事实上每次加入只能创建一次替代品,它又能如何工作呢?似乎必须为每个替换操作创建替代方案,因为子树每次都因儿童替换而不同。更换后取决于子树。 – usr

+0

@us你吓了我一秒,但修复其实很简单。通过让'ReplaceAll'访问被替换的节点,它将最终遍历NestLoop或Merge表达式的整个树,并根据提供的映射替换所有嵌套的'JoinExpression'元素,所以需要改变的就是添加在返回之前查找结果的'base.VIsit'调用。 – Servy

+0

很酷。希望看到一些令人信服的代码单元测试,尽管:)我认为是一个彻底的测试的好例子。 – usr

1

你需要一成不变的树木肯定。

创建一个类:

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); 
    } 
} 

反正一个详尽的搜索可以迅速爆炸在你的脸上,我想你知道这一点。 声明:我只是在这个编辑器中输入了它,它甚至不会编译,但你应该能够明白。

+0

JoinOptionsExpression是一个黑客攻击。最好马上收藏。除此之外,这是正确的方法,我在评论中主张。 – usr

+0

@usr:是的,JoinOptionsExpression是一种将一个节点中所有可能的子节点分组的方法。对我来说,这将允许更好的调试概述,有一棵树和所有选项。 – MBoros

+0

@MBoros,很好的解决方案 - 你完全理解我的要求并提供了一个可行的解决方案。我不把这个标记为答案的唯一原因是JoinOptionsExpression类 - 我不想要一个新的表达式类型,他的唯一目的是处理连接的详尽搜索,特别是因为我需要一些用于不同表达式类型的数据,没有这些可以解决整体问题。虽然很好的答案。 – Jack