RBO
不想看文章直接访问https://github.com/yuqi1129/schema/tree/master/mysql-protocol
(Java版本的Mysql)、https://github.com/yuqi1129/calcite-test,这里有关于Calcite RBO使用具体用例
1. 简介
RBO(Rule based optimization) 基于规则的优化。是指采用指定的等价关系代数表达式规则进行优化。所谓的等价关系代数表达式是指能产生同一结果的不同的关系表达式,比如说
示列中 左图中先进行Join然后再作Filter, 而右图中先分别对Join左右两边作Filter,然后再作Filter。一般情况下右图中的执行计划会优于左边,因为先做Filter会显著减少join的数据,提高性能。
上面所示的列子在Calcite中称为FilterJoinRule
,而现实中还有很多类似的基本等价关系代数优化如:
- Join Order, Join commute
- OutJoin to InnerJoin
- 列裁剪、常量折叠
- 投影合并、投影过滤合并、过滤合并等
- 子查询转SemiJoin
目前在Calcite中存在两种类型的Rule, 一种Rule其作用为上面所介绍, 主要作用是等价关系代数改写,另一种是Convention改写,所谓的Convention指的是约定拥有相同的Convention的RelNode遵循着同一转化规则,比如说Calcite定义了Convention.Spark, Convention.MySQL, 这些有着同一Convention的RelNode能够在对应的场景下进行关系代数转化,关于Convention, 后面会详细会有章节进行详细说明
2. RBO 怎么书写
在Calcite中,创建一个Rule只需要以下步骤
- 确定改Rule所需要作用的RelNode及其输入
- 继承
RelOptRule
或者ConverterRule
类 - 实现对应的onMatch()方法或convert方法
下面为一个很简单的Rule
public class ProjectMergeRule extends RelOptRule {
public static final ProjectMergeRule INSTANCE =
new ProjectMergeRule(true, RelFactories.LOGICAL_BUILDER);
//~ Instance fields --------------------------------------------------------
/** Whether to always merge projects. */
private final boolean force;
//~ Constructors -----------------------------------------------------------
/**
* Creates a ProjectMergeRule, specifying whether to always merge projects.
*
* @param force Whether to always merge projects
*/
public ProjectMergeRule(boolean force, RelBuilderFactory relBuilderFactory) {
super(
// 第一个RelNode的类型,
operand(Project.class,
// 第二个RelNode类型, 第三个RelNode类型为any(), 任意
operand(Project.class, any())),
relBuilderFactory,
"ProjectMergeRule" + (force ? ":force_mode" : ""));
this.force = force;
}
public void onMatch(RelOptRuleCall call) {
final Project topProject = call.rel(0);
final Project bottomProject = call.rel(1);
final RelBuilder relBuilder = call.builder();
// If one or both projects are permutations, short-circuit the complex logic
// of building a RexProgram.
final Permutation topPermutation = topProject.getPermutation();
if (topPermutation != null) {
if (topPermutation.isIdentity()) {
// Let ProjectRemoveRule handle this.
return;
}
final Permutation bottomPermutation = bottomProject.getPermutation();
if (bottomPermutation != null) {
if (bottomPermutation.isIdentity()) {
// Let ProjectRemoveRule handle this.
return;
}
final Permutation product = topPermutation.product(bottomPermutation);
relBuilder.push(bottomProject.getInput());
relBuilder.project(relBuilder.fields(product),
topProject.getRowType().getFieldNames());
call.transformTo(relBuilder.build());
return;
}
}
// If we're not in force mode and the two projects reference identical
// inputs, then return and let ProjectRemoveRule replace the projects.
if (!force) {
if (RexUtil.isIdentity(topProject.getProjects(),
topProject.getInput().getRowType())) {
return;
}
}
final List<RexNode> newProjects =
RelOptUtil.pushPastProject(topProject.getProjects(), bottomProject);
final RelNode input = bottomProject.getInput();
if (RexUtil.isIdentity(newProjects, input.getRowType())) {
if (force
|| input.getRowType().getFieldNames()
.equals(topProject.getRowType().getFieldNames())) {
call.transformTo(input);
return;
}
}
// replace the two projects with a combined projection
relBuilder.push(bottomProject.getInput());
relBuilder.project(newProjects, topProject.getRowType().getFieldNames());
call.transformTo(relBuilder.build());
}
}
只要一个RelNode 包含有连续两个Project, 就会使用这个Rule进行Projec合并
一个完整的例子见代码
4. RBO 执行过程
Calcite中核心代码有
//HepPlanner.java
public void setRoot(RelNode rel) {
root = addRelToGraph(rel);
dumpGraph();
}
public RelNode findBestExp() {
assert root != null;
executeProgram(mainProgram);
// Get rid of everything except what's in the final plan.
collectGarbage();
return buildFinalPlan(root);
}
setRoot
主要作用是将输入的RelNode 转化成有向无环图形式
在右图中,每一个HepRelVertex封装左图中的一个RelNode, 并把其输入关系映射成对应的边,最后检查右图是否有环, 其算法为:
- 统计每一个HepRelVertex的出度,如上图中LogicalProject的出度为0, LogicalFilter出度为1。
- 循环以下步骤
- 找到一个出度为0的HepRelVertex A
- 将所有以A为target的HepRelVertex 的出度减1
- 循环1, 直到所有的HepRelVertex都遍历完成
如果有环的话,肯定有HepRelVertex无法通过以上方法遍历完
findBestExp()
为主要的执行步骤,其主要方法为:
private void executeProgram(HepProgram program) {
HepProgram savedProgram = currentProgram;
currentProgram = program;
/*注意这个方法,初始化的时候定义一个Rule最多Match次数和match顺序
match 顺序只要指的是Rule在RelNode图中匹配顺序
ARBITRARY 随意顺序
BOTTOM_UP 从底到顶
TOP_DOWN 从顶到底
DEPTH_FIRST 深度优先(就是图的深度优先遍历)
*/
currentProgram.initialize(program == mainProgram);
for (HepInstruction instruction : currentProgram.instructions) {
//对Rule一个一个地执行匹配
instruction.execute(this);
...
}
currentProgram = savedProgram;
}
最终调用
private void applyRules(
Collection<RelOptRule> rules,
boolean forceConversions) {
...
boolean fixedPoint;
do {
//这里会根据上面定入的matcher顺序获取HepRelVertex相应的迭代器进行遍历访问
Iterator<HepRelVertex> iter = getGraphIterator(root);
fixedPoint = true;
while (iter.hasNext()) {
HepRelVertex vertex = iter.next();
for (RelOptRule rule : rules) {
HepRelVertex newVertex =
applyRule(rule, vertex, forceConversions);
}
}
} while (!fixedPoint);
}
applyRule()
函数主要步骤是
- 根据当前Rule, 确定所匹配的RelNode List, 如ProjectMergeRule就会得到连续两个Project的 RelNode List, 而FilterJoin 会得到一个RelNode List 包函数有 Filter和Join(顺序相关)
- 构造
HepRuleCall
实例, 该实例负责传入1中得到RelNode List - 触发Rule的onMatch()方法,此方法就是实现等价转化的主要方法
可见Calcite对RBO的实现封装的很好,用户几乎不用知道RBO实现细节, 只需要关心关系表达式的具体实现即可,大大简化了使用门槛。
5. 若干RBO实现分析
5.1 ReduceExpressionsRule
ReduceExpressionsRule
这个Rule是用来折叠常量表达式的,比如说
select id from test where 1 + 2 > 3;
表达式 1 + 2 可以直接被折叠成常量3, 然后整个SQL可以折叠成一个空值的LogicalValues, 而ReduceExpressionsRule
是通过Codegen方法实现上述计算
最终会调用如下代码:
public void reduce(RexBuilder rexBuilder, List<RexNode> constExps,
List<RexNode> reducedValues) {
//CodeGen Java代码并编译,关于Calcite的CodeGen体系,后面会专门介绍这一块
final String code = compile(rexBuilder, constExps,
(list, index, storageType) -> {
throw new UnsupportedOperationException();
});
final RexExecutable executable = new RexExecutable(code, constExps);
executable.setDataContext(dataContext);
//执行并得到最终的结果
executable.reduce(rexBuilder, constExps, reducedValues);
}
5. 2 ConverterRule
class EnumerableSortRule extends ConverterRule {
EnumerableSortRule() {
super(Sort.class, Convention.NONE, EnumerableConvention.INSTANCE,
"EnumerableSortRule");
}
public RelNode convert(RelNode rel) {
final Sort sort = (Sort) rel;
if (sort.offset != null || sort.fetch != null) {
return null;
}
final RelNode input = sort.getInput();
return EnumerableSort.create(
convert(
input,
//将原来的Convention转化成EnumerableConvention
input.getTraitSet().replace(EnumerableConvention.INSTANCE)),
sort.getCollation(),
null,
null);
}
}
ConverterRule 主要作用是将RelNode 从一个Convention转化成另一个Convention, 关于Convention的作用,我会专门用细说。
6. 总结
RBO 总的来说并不复杂,基本上利用已有的关系代码进行等价转化,这是现在数据库的执行计划优化基础,然后,你也可以看到RBO只是机械的利用规则并没有考虑实际场景下数据量、数据分布等对整个SQL执行的影响,比如说Join的结合就没有考虑到应当先结合筛选率高和数据量比较大表。CBO(
Cost Based Optimization) 就是用来优化此类场景,关于CBO介绍,请见下文