高手云集,群英荟萃。
2022年3月19日 CSDN 数据库沙龙上,众多技术专家汇集在数据库线上 Meetup 直播现场,来自 OceanBase 技术专家田逸飞(花名:义博)为大家精彩讲述了 OceanBase SQL 的一生。
本次分享从一条 SQL 被数据库接收开始,一直到执行结束,讲述了数据库中的整体执行流程,以及了解 OceanBase 如何让同一类 SQL 共享执行计划,通过这条SQL的一生,帮助大家更好的了解和掌握 SQL 执行流程和计划缓存两大模块。
一、SQL 执行流程
1.SQL 执行流程介绍
从图中我们可以看到,当用户从 SQL 发送到 OBServer 后,会先由 OBServer 对其进行快速参数化,参数化后的 SQL 进入 Plan Cache 尝试命中计划缓存。当找到一个可以使用的计划,则直接将计划交由 SQL 的执行引擎去执行,并将执行完成后的结果返回给用户;如果没有找到可以使用的计划,则会重新为此 SQL 生成计划,完整地执行 SQL 的Parser、Resolver、Transformer、Optimizer、Code Generator 解析流程,然后生成一个可用的物理执行计划,并交由执行引擎执行,同时此计划会被加入到计划缓存,以便后续的 SQL 重新使用,详见下图。
2.查询改写
如下图所示,流程中的 Parser 主要负责语法词法的解析,它会将用户输入的 SQL 基于 lax 和 yacc 生成一个 Parse Node Tree,如下图右侧所示,它将用户的 SQL 拆成了一个树状结构,同时做了一些语法解析。
3.语义分析
Resolver 负责对 Parse Node Tree 做语义的分析,主要包括语句的解析、中缀表达式的生成、表达式的类型推导等,并最终将其转化成 OceanBase 在代码中更易于操作的数据结构
4.查询改写
Transformer 会在保证 SQL 执行结果相同并且正确的情况下对 SQL 做等价变换。目前在 OceanBase 中存在两类改写。
- 基于规则的改写,这一类改写总是会把 SQL 向好的方向改写,比如 join 连接消除改写
- 基于代价的改写,这一类改写发生后 SQL 的性能可能变得更好或者更差。对于这一类的改写,会分别计算发生改写 SQL 和不发生改写的代价。在比较之后,对于那些发生改写后代价低时 OceanBase 选择改写;而发生改写 SQL 后代价更高的情况,则不去选择改写。
5. 查询优化
在 Transformer 做完等价改写之后,将改写后的 SQL移交至 Optimizer ,也就是查询优化模块中,由 Optimizer 继续生成逻辑计划。Optimizer 会对用户的 SQL 生成一组可选的逻辑计划,从这些所有的逻辑计划中选出一个优化器所认为的最优计划,详见上图。
在这个过程中,Optimizer 会结合代价模型和统计信息,做基表路径、连接顺序、连接算法的选择以及分布式计划的生成等。并且在做出这些选择的过程中,Optimizer 还会有一些基于规则的剪枝和 Skyline 剪枝的优化,通过这些剪枝规则减少优化过程中路径的数量,降低优化的复杂度,从而提升优化速度。
6 .代码生成
在 Optimizer 模块完成后,便得到了 SQL 的逻辑计划,而这个逻辑计划被移交 Code Generator 做代码生成,因为此时的逻辑执行计划不能被直接执行。所以 Code Generator 需要将逻辑的执行计划翻译,生成执行引擎可识别、可执行的物理执行计划。也就是说,Code Generator在这里所做的事情其实就是单纯的对逻辑执行计划做一个转换,包括将逻辑执行计划中的逻辑算子转换成物理算子。
经过 Code Generator 这个步骤,我们得到可以实际执行的物理执行计划,而这个物理执行计划也会被交由 Executor ,也就是我们的执行引擎去执行。
7. 执行引擎
在 OceanBase 中的执行引擎使用的是火山模型。火山模型在数据库中非常常见,是经典的数据执行模型。除此之外,OceanBase 的执行引擎还提供了并行执行框架,比如说当 CPU 资源容量足够多时,用户可通过开启并行机制来提高 SQL 的执行性能。
二、计划缓存
在将计划缓存之前,先介绍一下计划缓存的两种匹配模式:
Foece 模式和 Exact 模式
1.Force 模式
这是 OceanBase 默认的匹配模式。 Force 模式下,首先会对 SQL 里面的常量进行参数化,再对参数化后的计划进行匹配。什么是参数化呢?如上图右侧 SQL 语句, select c1,c2 from t1 where c1=1,这里的1 是一个常量。参数化就是将常量替换成一个通配符“?”。所以参数化之后,这条 SQL 就变成了 select c1,c2 from t1 where c1=?。
参数化之后,文本与此条 SQL 一样的 SQL 都可以共享一个计划。比如这里 where c1=1、c1=2、c1=3的语句都可以共用同一个执行计划。但它的不足之处在于 SQL 命中的计划可能并不是一个最优的执行计划。
依然以上述 SQL 为例,假设对于 t1 中,c1=1 的值占了1%,c1=0 的值占了99%,即表里的c1 只有 1 和 0 两个取值。
假设一开始计划缓存是空,先来了一个 c1=1 的 SQL,就会用这条 SQL 去生成计划。如果c1=1 的过滤性非常好的,则会选一个走 c1 索引的计划,相当于在 c1 的索引上做一次扫描,然后做索引回表,并将计划缓存到计划缓存中。但是如果之后来了一个 c1=0 的 SQL,那么它就会命中刚刚生成的计划走索引扫描。但是对于 c1=0的条件,索引其实不是最优的,
因为它不能过滤掉大量数据。索引扫描完成后它还有 99% 的数据要做索引回表。在这种场景下,索引往往不如基表扫描快。
2.Exact 模式
Exact模式下,做计划匹配的时候不会做参数化,而是直接用原始的 SQL 去做匹配,匹配的时候要求 SQL 的文本完全相同。比如大小写、空格的数量以及参数都要相同才能共享计划。
它的优点在于不同的参数都能选到自己最优的计划。不足之处也非常明显——计划的命中率非常低。比如通过主键去查找表里某一行具体的数据,主键在表中的每一个值都是不同的,也就意味着它们都无法共享计划,每一个主键值都要重新生成一次计划,这就导致计划的命中率非常低。而且大量的计划会导致计划缓存的内存会大幅膨胀。
上图展示了一些计划共享的例子。
第一组 SQL 参数化之后都会变成 select c1,c2 from t1 where c1=? and c2 =?。它们参数化后的 SQL 文本是相同的,因此可以共享计划。
第二组 SQL 参数化之后变成了select c1,c2 from t1 where c1=? order by ?。
但实际上它们的语义是不一样的,第一条 order by 1,意味着要对 select 里的第一列做排序,而第二条意味着要对 s
elect 里的第二列做排序。如果它们共享计划,两个结果一定会出现问题。
为了防止这样错误的共享计划,OceanBase引入了额外的约束机制。比如对于第一条 SQL ,要求 order by=1 的时候才能使用它生成的计划,同理对于第二条 SQL 会要求 order by=2 的时候才能使用它生成的计划。
第三组 SQL 参数化之后会变成 select c1,c2 from t1 where c1=? and ? = ?。而它在 OceanBase 中也是无法共享计划的。因为1 = 1是一个恒 true的条件,而 1 = 2 是一个恒 false 的条件。对于这种恒 true、恒 false 的条件,OceanBase 在改写优化的过程中会对其做一些特殊处理,简化这些计划的生成过程和生成的计划。一旦不同的恒 true、恒 false 条件共享了计划,也会导致一些结果错误。
对于上述场景, OceanBase 也会抽取出约束。比如对于第一条 SQL,它会要求第二个参数等于第三个参数的计算结果是 true 的时候才能共享这条 SQL 产生生成的计划;对于第二条 SQL,它会要求第二个参数等于第三个参数的计算结果是 false 的时候才能共享第二条 SQL 产生的计划。
第四组 SQL 的问题在于 select 的大小写不同,因此也不能共享计划。
3.SQL 执行计划获取过程
这里的图片内容跟第一部分几乎完全相同。对于一个 SQL 请求,OceanBase 会对这条 SQL 做一次快速参数化,并用参数化后的 SQL 到计划缓存中匹配执行计划。这里存在能匹配到计划和匹配不到计划两种情况。如果匹配到就去执行;而匹配不到则将走完一个完整 SQL 执行流程,先在 Parser 中做词法语法解析。具体流程是:SQL 先通过 Parser 做语法词法解析,然后做一次普通参数化,最后再做剩下的 Resolver、Transformer、Optimizer 等一系列过程。
4.快速参数化和普通参数化区别
快速参数化
快速参数化的功能:通过语法分析快速将 SQL 中常量值替换为通配符的问号。
快速参数化只依赖词法分析即可直接基于 SQL 文本做参数化,对 Parser 的依赖小,参数化效率高。
在参数化的过程中 OceanBase 会记录当前 SQL 的参数信息,用于计划匹配时检查是否满足约束。
普通参数化
普通参数化是基于 Parser 的结果进行参数化的过程。在 Parser 之后我们已经得到了 SQL 完整的语法词法树,也就获得了 SQL 的语义信息。在这个基础上我们做参数化可以进行一些额外的操作,例如约束的抽取。当我们对上图中的语法词法树进行参数化时,会发现 order by 后是一个常量,这个时候我们就会抽取约束,要求 order by 后的参数必须为1时才能共享这条 SQL 生成的计划。
5.计划更新
生成的物理执行计划并非一成不变,在某个时间点生成的计划只能表示,
一条 SQL 在特定时间点最优的计划。但在数据库中,表的结构、表中数据量、表中数据值的分布都可能会发生变化,在各种因素的不断变化中,不能保证在某一个时间点生成的计划永远是最优的。所以计划一定需要在合适的时机更新,从而保证这个计划相对来说是最优计划。
OceanBase 计划更新场景
- 执行计划依赖对象的 Scheme 发生变化
比如数据表T1,在T1上新建一个索引,建立索引的动作会导致T1表 schema 的版本号被推高,而当检测到schema 版本号推高时,计划缓存中所有涉及到T1表的计划全部失效。如果此时一条新的 SQL 完全依赖这个数据表T1 ,就会重新生成计划。因此这时新计划极可能用新建索引做出更好的优化。
- 统计信息发生改变
当统计信息发生变化时,可以利用更新的统计信息重新生成计划。比如当列的直方图、NDV值、最大最小值更新时,优化器可以使用这些新的统计信息去生成更好的计划。
- 执行反馈需要更新计划
通过执行反馈发现当前的执行计划,相比于它生成计划时的性能更差。这时即可将此计划完全淘汰,重新生成计划。
以上就是关于在 OceanBase 中 SQL 执行流程和计划缓存的所有内容。非常期待和欢迎大家到 OceanBase 开源社区参与讨论、交流和互动,请扫描以下二维码,快速与我们联系。