StarRocks Join Reorder 源码解析

news2024/11/21 2:28:37

导读:欢迎来到 StarRocks 源码解析系列文章,我们将为你全方位揭晓 StarRocks 背后的技术原理和实践细节,助你逐步了解这款明星开源数据库产品。 本期 StarRocks 技术内幕将介绍 Join Reorder 算法如何找到最优解的原理。

背景介绍

多表 Join 是现实业务场景中很常见的需求,其执行效率和 Join 的执行顺序息息相关,比如两表 t1 Join t2 就有 t1t2 t2 t1 两种方式(Join 满足交换律),三表 t1 Join t2 Join t3 由于 Join 满足结合律,可以 t1 和 t2 先做Join,再和 t3 Join,即(t1 t2) ⨝ t3, 也可以先做 t2 和 t3 的 Join,再和 t1 做 Join,即 t1 ⨝ (t2t3)。

如上图所示,Table A 和 B 的 Join 会生成较大的 Join 中间结果集,使用 Join Reorder 算法优化后,结果集缩小显著。Join 的执行顺序和执行方式对查询性能的结果影响非常明显,部分场景下甚至能带来数量级的差异,因此优化器选择出好的 Join 顺序尤为重要。

整体流程

刚才提到 Join 是满足交换和结合律的,因此通过 Join 的交换结合,可以拓展出所有的 Join 顺序。在 “StarRocks 优化器代码导读”中我们介绍过,StarRocks 优化器使用 Memo 进行空间搜索,通过使用 transform rule 来完成 GroupExpression 的转换,使用 JoinCommutativity 和 JoinAssociativity 两个 rule 完成 Join 的交换和结合。先来看张图,可以帮助你理解:

上图罗列了三表 A、B、C 所有可能的 Join 顺序,这些在 Memo 中表示成不同的 GroupExpression,并记录在同一个 Group 中。注意,这些 Join 顺序不同的 GroupExpression 是逻辑等价的。

理论上我们可以使用 Join 的交换结合枚举出所有的 Plan,通过计算每个 Plan 的 Cost,从而选出代价最小的 Plan。但实际上随着 Join 节点的增多,优化器的搜索空间会成指数级放大。如下图所示:

​随着 Join 节点的增多,优化器将无法枚举出所有 Plan。另一方面,优化器需要在有限时间内给出最优解,因此我们需要使用高效的 Join Reorder 算法来决定 Join 的顺序。 在无法枚举所有的 Join 顺序时,StarRocks 使用了贪心和动态规划两种算法来决定多表 Join 的顺序,具体策略是:

  1. Plan 中 Join 节点小于等于 4(可通过 session 变量 cbo_max_reorder_node_use_exhaustive 修改)个时,使用枚举的方法决定Join顺序。

  2. Plan 中 Join 节点大于 4,小于等于 10(可通过 session 变量 cbo_max_reorder_node_use_dp 修改)个 Join 节点时,使用动态规划和贪心算法决定 Join 顺序。

  3. Plan中 Join 节点大于 10个的时候,使用贪心算法决定 Join 的顺序。

  4. 如果 Plan 中的 Scan 节点包含未知的列统计信息,将只生成默认的左深树。

枚举的方法可以通过 Cascades 框架估算出分布式计划的代价,DP 和贪心算法生成的 Plan 则需要通过 Memo 的 Property Enforce 实现,因此所有通过贪心和 DP 生成的 Plan Tree 都需要 Copy In Memo,参与后续搜索空间的拓展并计算分布式 Plan 的 Cost。

DP 和贪心算法选择出的 Join Order 是单机最优 Plan,为了尽量找到最优的分布式 Plan,StarRocks 在Join 个数不超过cbo_max_reorder_node_use_dp 时,会同时保留 DP 和贪心的 Plan,且贪心算法也会保留 Cost 最小的 10 个 Plan,为后续找到“最优”分布式 Plan 提供更多的可能性。 接下来,我们将详细介绍相关代码。

代码导读

Join 交换结合

Join 的交换和结合律的使用,是基于 Cascades 优化框架实现的,因此在 StarRocks 中只需要实现对应的 Transform Rule,就可以完成 Join 的交换和结合。 通过 Join 的交换和结合,可以找到所有逻辑等价的 GroupExpression。Join 的交换通过 JoinCommutativityRule 完成,逻辑比较简单,就是将孩子的左右孩子节点互换。 需要注意的是,并不是只有 Inner Join 和 Cross Join 才可以进行互换,Outer Join 和 SemiJoin 同样可以。JoinCommutativityRule 中通过一个 Map 记录了 Join Type 发生交换时的改变,代码如下:

private static final Map<JoinOperator, JoinOperator> Join_COMMUTATIVITY_MAP =
        ImmutableMap.<JoinOperator, JoinOperator>builder()
                .put(JoinOperator.LEFT_ANTI_Join, JoinOperator.RIGHT_ANTI_Join)
                .put(JoinOperator.RIGHT_ANTI_Join, JoinOperator.LEFT_ANTI_Join)
                .put(JoinOperator.LEFT_SEMI_Join, JoinOperator.RIGHT_SEMI_Join)
                .put(JoinOperator.RIGHT_SEMI_Join, JoinOperator.LEFT_SEMI_Join)
                .put(JoinOperator.LEFT_OUTER_Join, JoinOperator.RIGHT_OUTER_Join)
                .put(JoinOperator.RIGHT_OUTER_Join, JoinOperator.LEFT_OUTER_Join)
                .put(JoinOperator.INNER_Join, JoinOperator.INNER_Join)
                .put(JoinOperator.CROSS_Join, JoinOperator.CROSS_Join)
                .put(JoinOperator.FULL_OUTER_Join, JoinOperator.FULL_OUTER_Join)
                .build();

例如 left outer Join 的孩子节点交换时,需要从 left outer 转换成 right outer。代码如下:

       left outer Join             right outer Join
       /           \      =>        /            \
      A             B              B              A

Join 的结合通过 JoinAssociativityRule 完成,主要逻辑可以用下面的图表示,Join 顺序的改变在Plan 中的改变就是树的形状变化。当然,在生成新的 OptExression 的过程中,也需要考虑 predicate 和 project 的重新分配。StarRocks 为 Join On 条件中包含表达式的结合转换做了支持,例如 SQL:

Select C.v4 from A Join B on A.v1 = B.v2 Join C on B.v2+1=C.v4 and B.v3 = C.V5

Join 上的 predicate 使用的列需要在孩子节点中包含,对于有 project 的进行表达式计算的,也需要考虑将其放在合适的 Join 上。 例如下图中,须将 B.v3 = C.v5 放在新生成的 Join 节点上,如果 Table B 和 Table C 之间没有等值的谓词连接条件,StarRocks 会禁止转换,避免生成 CrossJoin 节点Project 节点也需要在新生成的 Join 节点上重新计算,保证向上输出上层 Join 节点所需的 Column

MultiJoinNode

为了加速多表(StarRocks 中为多于 4 表)Join reorder 的处理,StarRocks 中使用了 MultiJoinNode 来表示多张表的 Join。可以化简为以下代码:

public class MultiJoinNode {
    // Atom: A child of the Multi Join. This could be a table or some
    // other operator like a group by or a full outer Join.
    private final LinkedHashSet<OptExpression> atoms;
    private final List<ScalarOperator> predicates;
    private Map<ColumnRefOperator, ScalarOperator> expressionMap;

    public MultiJoinNode(LinkedHashSet<OptExpression> atoms, List<ScalarOperator> predicates,
                         Map<ColumnRefOperator, ScalarOperator> expressionMap) {
        this.atoms = atoms;
        this.predicates = predicates;
        this.expressionMap = expressionMap;
    }
 }

将多个 InnerJoin/CrossJoin 节点转换成 MultiJoinNode,其中需要 reorder 的孩子节点都表示成atoms。如下图所示,table A、B、C、D 就是各个 atom,所有的谓词记录在 predicates 中,后续算法将基于 MultiJoinNode 对 atom 进行重新组合,以产生合适的顺序。

左深树

如下图所示,左侧为左深树,右侧为稠密树。在单机/单任务数据库上,只需考虑左深树就可完成 Join 重排。当无法获取表的列统计信息时,无法准确估算 Join 的中间结果集,因此 StarRocks 选择了只生成左深树。

左深树的生成是由 JoinReorderLeftDeep 类完成的,逻辑比较简单。StarRocks 的 HashJoin 都是右表 build,左表 probe,期望生成的 Plan 中右表应该是小表,此方法中将需要 reorder 的 atom 按照 row count 从大到小进行排序,树最深的节点是代价最高的节点,这种 reorder 方法在没有列统计时也可以获得较为不错的表现。

动态规划

StarRocks 使用的是 DPsub 算法,通过将 atoms 划分成不同的 Partitions,递归计算子 Patition 的 bestPlan,并记录在 bestPlanMemo 中,从而实现规避重复计算。

例如,下图中的 [A,B] 就可以直接从 bestPlanMemo 中得到,而 [C,D] 则需要进一步递归分别得到 [C] 和 [D] 的 Best Plan,再计算 Join order [C,D] 的 Cost,并将 Cost 最小的 Join order 插入到 BestPlanMemo 中。最终包含所有 atom 的 Partition 即为 DP 算法选出的最佳 Plan。

这种 Bottom-Up 的 DP 算法可以有效处理稠密树空间枚举的问题,并能够利用动态规划来解决中间结果重复计算的问题,但由于需要枚举出所有子 Partition 的 Best Plan,atoms 过大时会导致优化时间太长,因此我们默认 10 个 Join 以内采用此方法。

贪心算法

如图,为贪心算法的实现:

在贪心算法的实现中,StarRocks 将 Join 分为了多个 Level。第一层就是每个 atom,从第一层中选择 Row Count 最小 atom 的和其他的 atom Join 生成 Join level 2。

其他 level 类比,Level K 从 Level K-1 中选择 Row Count 最小的,Join 其他的 atom,计算出 Level K 输出的 Row Count。当 K 等于 atom 个数时,算法结束,选出的即为最终“最优”的 Join Order。

这种方法的实现比较简单,贪心算法的原理也比较好理解,但不足之处在于只能用来构建左深树,且第一个 atom 的选择会对 Join Order 产生比较大的影响,容易陷入局部最优的问题。为了缓解这一问题,StarRocks 会启发式地生成 K 个 join 顺序,每个 Join 顺序选择的第一个 atom 都不一样,并将这些生成的 Plan 都 Copy In Memo,参与后续的 cost 计算,并从中选择 cost 最低的 plan。

总结

本文主要介绍了 StarRocks 中使用的 Join Reorder 算法和其基本原理。依据 Join 节点的个数不同,我们选用不同的 Join Reorder 算法,较少时用枚举法,10 个以内 Join 节点使用 DP 和贪心算法,超过 10 个时只使用贪心算法。通过对多种算法的使用,StarRocks 可以在 Join 较少时迅速找到最优解,在 Join 较多时也能在相对较短的时间内产生效果不错的 Plan。 此外,为避免 Join Reorder 后的 Plan 只是单机最优,StarRocks 中还保留了多个算法产生 Join Order,以尽可能在 Memo 中找到分布式的最优解。

本期 StarRocks 源码解析到这就结束了,好学的你肯定学会了一些新东西,又产生了一些新困惑,不妨留言评论或者加入我们的社区一起交流(StarRocks 小助手微信号)。下一篇 StarRocks 源码解析,我们将为你带来 StarRocks 统计信息和 Cost 估算。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/110285.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

vue之watchEffect

在Options API中&#xff0c;我们可以通过watch选项来侦听data或者props的数据变化&#xff0c;当数据变化时执行某一些操作。 在Composition API中&#xff0c;我们可以使用watchEffect和watch来完成响应式数据的侦听。 watchEffect用于自动收集响应式数据的依赖&#xff0c;需…

Jmeter实现websocket协议接口测试

一&#xff0e;为了方便使用&#xff0c;首先将jmeter设置成中文&#xff0c;有两种方法&#xff1a; 1.在Jmeter界面进行设置&#xff0c;Options ->Choose Language ->Chinese(Simplified)&#xff0c;这种方法在关闭jmeter重启后又会恢复成默认的英文&#xff0c;如果…

学习Python编程好找工作吗?

说起编程语言&#xff0c;不少人都会推荐学习Python&#xff0c;但很多人对Python不太了解&#xff0c;所以比较好奇“学习Python编程是否好找工作”&#xff0c;关于这个问题&#xff0c;小编通过下文为大家详细解答一下。 从市场情况来讲&#xff0c;Python职位比较多&…

阿里内部进阶学习SpringBoot+Vue全栈开发实战文档

前言 Spring 作为一个轻量级的容器&#xff0c;在JavaEE开发中得到了广泛的应用&#xff0c;但是Spring 的配置烦琐臃肿&#xff0c;在和各种第三方框架进行整合时代码量都非常大&#xff0c;并且整合的代码大多是重复的&#xff0c;为了使开发者能够快速上手Spring&#xff0…

105.(leaflet之家)leaflet态势标绘-聚集地修改

地图之家总目录(订阅之前请先查看该博客) 地图之家:cesium+leaflet+echart+地图数据+地图工具等相关内容的介绍 文章末尾处提供保证可运行完整代码包,运行如有问题,可“私信”博主。 效果如下所示: 下面献上完整代码,代码重要位置会做相应解释 <!DOCTYPE html>…

Python函数总结

在Python中&#xff0c;函数是一个带有名字的代码块&#xff0c;可以被反复调用。函数可以帮助你组织和重用代码&#xff0c;使你的程序更整洁&#xff0c;更易于维护。本文将会深入探索Python的秘密 目录 定义函数 自定义函数 内置函数 函数式方程 高阶函数 函数标注 …

Linux5.4.0内存分配器核心代码解析

理论 伙伴系统 核心代码解析 __rmqueue_smallest函数 在zone的free_list上进行搜索,找到符合migratetype的、大小为order的空闲页面块 static __always_inline struct page *__rmqueue_smallest(struct zone *zone, unsigned int order,int migratetype

【TypeScript】常用类型声明详情概述

目录 TypeScript常用类型 类型注解 TS类型概述 原始类型 数组类型 对象类型 函数类型 类型别名 接口 元组 字面量类型 枚举 any类型 typeof操作符 类型推论 类型断言 TypeScript常用类型 TypeScript是JS的超集&#xff0c;TS提供了JS的所有功能&#xff0c;并额…

【Linux】第五部分 网络配置

【Linux】第五部分 网络配置 文章目录【Linux】第五部分 网络配置5. 网络配置5.1 对vmware网络连接的三种模式探讨&#xff0c;Bridged&#xff08;桥接模式&#xff09;、NAT&#xff08;网络地址转换模式&#xff09;、Host-Only&#xff08;仅主机模式&#xff09;Bridged&a…

车企数据治理的障碍是什么?如何解决?

​在全行业数字化转型的浪潮下&#xff0c;底层技术的发展与行业之间的碰撞&#xff0c;正在成为变革的巨大力量&#xff0c;汽车行业也是如此。汽车行业的“数字化转型”是利用新的技术驱动行业的创新与发展&#xff0c;改善用户体验、重构商业模式、降本增效&#xff0c;而这…

跟着pink老师学习第二天的学习总结(1)

1.CSS初始化 <style>/* 清除浏览器默认格式 */*{margin:0;padding:0}/* 斜体文字不倾斜 */em,i{font-style:normal}/* 去掉li的小圆点 */li{list-style:none}img{/* 照顾低版本浏览器,如果图片外面包含链接,会出现边框 */border:0;/* 取消图片底部与边框产生的缝隙 */ve…

element plus + vue3表单第一次数据未清空的bug问题解决

使用框架&#xff1a;element Plus vue3 场景描述&#xff1a; 场景一&#xff1a; 表单的添加和修改功能&#xff0c;公用同一个弹框&#xff0c;点击修改后&#xff0c;点击添加表单显示的是上次修改的数据。 场景二&#xff1a; 点击修改&#xff0c;数据回显到表单&…

谁能主宰智能驾驶赛道?「芯片+感知」是第一主角

得「感知」者&#xff0c;得天下。 这句话依然适用于当今的自动驾驶赛道&#xff0c;实际上从Mobileye开始&#xff0c;到特斯拉&#xff0c;都是如此。尤其是智能化变革的快速演进&#xff0c;对于下游车企来说&#xff0c;具备规控的自研能力&#xff08;更多考虑到系统的体验…

【栈与队列】——栈的实现及应用

目录概念栈的实现初始化栈入栈出栈获取栈顶元素获取栈中有效元素个数判断栈是否为空栈的销毁栈的应用概念 栈 栈是一种特殊的线性表&#xff0c;其只允许在固定的一端进行插入和删除元素操作。进行数据插入和删除操作的一端称为栈顶&#xff0c;另一端称为栈底栈中的数据元素遵…

python+pyqt5+mysql设计图书管理系统(5)- 普通用户图书管理界面

前面已经实现了图书管理系统中的一部分功能,今天就在完整最后一点内容,使用pyqt5设计出detail_stu.ui文档,然后使用PyUIC转为detail_stu.py文档,就得到了设计好的界面文档的代码。然后再逐一实现界面上对应的功能。 设计的界面如下: 1.菜单栏选项功能实现 菜单栏-登录选…

物流批量查询,如何筛选出物流发往时间大于12小时的单号

小编分享一个方法批量查询物流信息&#xff0c;并分析揽收到发往的时间差大于12小时的单号&#xff0c;有需要的朋友可以接着往下看&#xff0c;希望能给大家带来帮助。 第一步&#xff0c;运行【快递批量查询高手】在主界面中的任意空白处【右键】选择添加单号。 第二步&#…

box-shadow阴影的妙用-笔记

box-shadow: 0 0 4px 0 #ff0000; 注意阴影的这个颜色要和边框的颜色一致&#xff0c;就能出这种效果

【车载开发系列】UDS诊断---链接控制服务($0x87)

【车载开发系列】UDS诊断—链接控制服务&#xff08;$0x87&#xff09; 诊断---链接控制服务&#xff08;$0x87&#xff09;【车载开发系列】UDS诊断---链接控制服务&#xff08;$0x87&#xff09;一.概念定义二.应用场景三.报文格式1&#xff09;报文请求2&#xff09;肯定响应…

【LeetCode每日一题】——152.乘积最大子数组

文章目录一【题目类别】二【题目难度】三【题目编号】四【题目描述】五【题目示例】六【解题思路】七【题目提示】八【时间频度】九【代码实现】十【提交结果】一【题目类别】 动态规划 二【题目难度】 中等 三【题目编号】 152.乘积最大子数组 四【题目描述】 给你一个…

文件操作:文件的使用打开关闭与读写(顺序读写)

1.为什么使用文件 我们前面学习结构体时&#xff0c;写了通讯录的程序&#xff0c;当通讯录运行起来的时候&#xff0c;可以给通讯录中增加、删除数据&#xff0c;此时数据是存放在内存中&#xff0c;当程序退出的时候&#xff0c;通讯录中的数据自然就不存在了&#xff0c;等下…