因为工作中用到calcite做SQL query engine,所以对calcite的源代码做了一些研究,其中VolcanoPlanner是非常重要的一个模块,本文对最近的一些学习做一个整理。
术语说明
1. rel node (RelNode) : calcite中用于表示一个logical plan的数据结构。
2. rel set (RelSet) :具有相同语义的rel node的集合。
3. rel subset (RelSubSet) :具有相同语义和相同物理属性的RelNode的集合,一个rel subset必然属于某个rel set.
4. RelOptRule : 优化规则,VolcanoPlanner会利用一些列的规则来优化rel node.
5. RelOptRuleOperand :优化规则的操作数,这些操作数会被用于匹配rel node,只有匹配上了才能apply相应的规则。
6. VolcanoRuleCall :表示对RelOptRule的一次调用,包含相关的RelNode参数。
VolcanoPlanner
Apache Calcite[1]的optimizer是基于经典的volcano/cascades框架构建的,其中RelOptPlanner是calcite的query optimizer,负责基于给定的rules和cost model将relational expression转换成语义相同但cost最小的relational expression.
在calcite中,RelOptPlanner是一个接口,其相关类图如下:
注意,上图中只列出了部分成员方法/变量。RelOptPlanner接口在calcite中有VolcanoPlanner和HepPlanner两个实现类。HepPlanner是RelOptPlanner接口的一种heuristic implementation,因为没有深入研究,这里暂不讨论。VolcanoPlanner使用动态规划(dynamic programming)算法对查询中的relational expression有选择地进行转换,最终得到最优的逻辑查询计划(在calcite中用rel node表示)。下文会基于源码对VolcanoPlanner的工作原理和流程做一个简单梳理。
使用RelOptPlanner的sample code
这里选择calcite中的RuleSetProgram的run方法作为示例代码,看看RelOptPlanner是怎么被使用的:
1. 和经典的volcano/cascades框架一样,calcite的volcano planner也是基于规则和代价模型的,所以通常都会调用addRule方法添加一系列规则到planner中。
2. 优化器需要有个起点,setRoot方法就是用于设置优化工作的起始RelNode, 下文会介绍到,setRoot方法不仅仅是设置了一个root,还触发了一系列的初始化工作。
3. 最终优化完的RelNode是通过findBestExp方法获取到的,这个方法背后也是触发了一系列的函数调用,最终得到优化结果。
VolcanoPlanner的主要成员变量/函数
说明:
1. root用于存储根节点。注意,root是RelSubSet类型,而不是RelNode类型,root是一个集合,其包含一个或多个rel node,findBestExp方法会从root中构建出cheapest的rel node作为最终结果。
2. classOperands存储了RelNode实现类到RelOptRuleOperand的一对多映射。VolcanoPlanner.addRule和VolcanoPlanner.onNewClass会向classOperands中添加元素。
3. ruleSet存储了所有添加的规则。
4. root的值最初由setRoot方法调用registerImpl获取并设置。
5. registerImpl的作用是把给定的rel node注册到当前VolcanoPlanner(添加到mapRel2Subset中,并关联相应的RelSet和RelSubSet对象),并通过fireRules触发classOperands中匹配该rel node的所有rules.
6. registerImpl除了注册给定rel node外,还会注册给定rel node所有子rel node. 通过这种注册机制,一个rel node及其子结点都会被记录在VolcanoPlanner的数据结构中,并通过RelSet,RelSubSet存储了它们之间的语义和物理属性的关系,而RelSubSet又存储了最优rel node(优劣的判断是基于cost model进行的,本文暂不涉及cost model的内容). 这里其实就是保存了动态规划算法中的子问题的解。
7. ruleQueue存储的是对rule的调用(VolcanoRuleCall)。fireRules会把rules封装成VolcanoRuleCall添加到ruleQueue当中。
8. findBest方法会从ruleQueue中获取VolcanoRuleCall并apply相应的rule对rel node进行转换,然后又通过registerImpl将新得到的rel node注册到VolcanoPlanner中,如此循环执行,直到满足退出条件或已穷尽所有rule call. 最后通过root.buildCheapestPlan方法得到并返回最优的rel node,这里其实就是动态规划中的通过子问题的解构建原问题的解。
VolcanoPlanner Planning过程中的函数调用
基于上文所述及上图所示,我们可以简单总结一下VolcanoPlanner的工作过程:
1. 用户调用VolcanoPlanner.addRules将一些列规则添加到ruleSet中,并将相应映射添加到classOperands.
2. 用户调用VolcanoPlanner.setRoot将要优化的rel node注册到planner中:
a. 这个过程会递归注册所有的子rel node,如果中间发现了新的rel node实现类,也会添加到classOperands映射。
b. 注册rel node的同时会生成rel node相关的rule call,并将其添加到planner的ruleQueue中。
3. 用户调用VolcanoPlanner.findBestExp循环地从ruleQueue中获取rule call并将相应rule应用到rel node进行转换,转换后得到的rel node又会被注册到planner中去。如此循环执行,直到满足退出条件或穷尽rule call. 最后构建最优的rel node.
说明
1. Calcite源码版本:1.21.0
2. 水平有限,如有错误,望读者支出
Reference
[1] Apache Calcite: A Foundational Framework for Optimized Query Processing Over Heterogeneous Data Sources
[2] The Cascades Framework for Query Optimization
[3] The Volcano Optimizer Generator : Extensibility and Efficient Search