导读
云溪数据库的 SQL 引擎蕴含连贯、编译、缓存、分布式日志和分布式执行五大服务组件,实现了多集群多节点协同的高效计算,大大晋升了用户的查问效率。
为了进一步晋升 SQL 引擎的性能,研发团队结合实际业务需要,在原有架构的根底上,针对 SQL 引擎的编译服务、执行服务、算法等方面进行了一系列深度定制化的优化改良工作。本文将这些改良工作逐个开展介绍。
云溪数据库针对 SQL 引擎的优化改良
1. 编译服务优化
1.1 类型、性能、语法兼容
随着日益增多的场景须要,云溪数据库陆续欠缺了对 PostgreSQL、Oracle、MySQL 语法、类型、函数的兼容。
1.2 打算优化
1.2.1 直方图
云溪数据库还扩大了统计信息性能,除了表行数,表中列的 Distinct 值(某一列的惟一值总共有多少条),还额定引入了直方图。为 CBO 的优化提供了更多的根据。统计信息获取的简略流程如下:
对每个 range 进行抽样,用蓄水池算法生成样本汇合,而后用样本进行各种统计信息的预估,将后果通过写入函数 writeResults,写进零碎表 system.table_statistics 中。
1.2.2 执行打算治理
云溪数据库扩大了对执行打算的治理,包含执行打算绑定、主动捕捉绑定、自动更新绑定等。执行打算绑定性能使得能够在不批改 SQL 语句的状况下抉择指定的执行打算。用户通过绑定执行打算,能够将打算存入云溪数据库中,下次再执行解析后打算雷同的 SQL 语句时,只有取出之前存入的打算即可,省去了构建打算的工夫。云溪数据库还会智能地主动捕捉执行频率较多的并且用户之前没有手动为其创立绑定的 SQL 语句,在后盾主动为其创立打算绑定。
因为表数据的变动,如:数据变动、数据结构变动、统计信息变动,可能会导致之前绑定的执行打算执行效率升高,云溪数据库将自动检测执行工夫,将绑定好的执行打算进行优化,为用户进步复合以后数据场景的更高的执行效率。
2. 执行优化
2.1 矢量算子
云溪数据库还引入了矢量算子,相比基于 Goetz Graefe 论文的“火山”模型,“矢量”模型在计算行数显著大于列数的场景下,性能会有极大的晋升。
从原理上讲,这是用一系列专门针对数据类型和计算的特定编译循环代替了通用的相似于解释器的 SQL 表达式评估器,因而计算机能够间断执行许多更简略的工作,大大节俭了反复的计算所须要的工夫。配合团队开发的列式存储,查问性能还将有进一步的晋升。
目前矢量算子反对的类型有:Array、BIT、BOOL、BYTES、COLLATE、DATE、DECIMAL、INET、INT、INTERVAL、JSONB、SERIAL、TIME、TIMESTAMP、TIMESTAMPTZ、UUID、FLOAT、STRING 等。
目前云溪数据库反对的矢量算子有:Noop、TableReader、Distinct、Ordinality、Hashjoiner、MergeJoiner、Sorter、Windower 等。
举例来说,请思考一个蕴含三列的 People 表:Id,Name 和 Age。在火山模型中,每个数据行由每个算子解决一次 —— 一种逐行执行办法。相比之下,在矢量化执行引擎中,咱们一次传递了无限大小的面向列数据的批处理。咱们应用一组列,而不是应用元组数组的数据结构,其中每一列都是特定数据类型的数组。在该示例中,分批解决将由一个 Id 的整数数组,一个 Name 的字节数组和 Age 的整数数组组成。下图显示了两个模型中数据布局之间的区别:
火山模型
矢量模型
SELECT Name, (Age – 30) * 50 AS Bonus FROM People WHERE Age > 30;
这样的语句查问,在火山模型中,顶级用户向 Project 算子申请一行,该申请被流传到底层的 Scan 算子。扫描从键值存储中读取一行,并将其传递给 Select,Select 将查看该行是否通过了 Age> 30 的谓词。如果该行通过了查看,则将其返回给 Project 算子以计算 Bonus = (Age – 30) * 50 作为最终输入。
火山模型流程图
一次解决一行,对于每一行,咱们都在调用一个齐全通用的标量表达式的过滤器!表达式能够是任何货色:乘法,除法,相等查看或内置函数,甚至能够是一长串这样的货色。因为这种通用性,计算机在每一行上都有很多工作要做——必须在甚至无奈执行任何工作之前查看表达式的含意。与编译后的语言相比,这种计算形式与解释型语言同样麻烦。
在矢量化模型中,咱们采纳不同的形式。每个矢量化算子背地的原理是在执行期间不容许任何自由度或运行时抉择。这意味着对于工作,数据类型和属性的任意组合,应该由一个专门的算子来负责这项工作。对于示例查问,用户从算子链中申请一批。每个算子都向其子级申请一批,执行其特定工作,而后将一批返回给其父级。
矢量模型流程图
为了对此进行可视化,请思考由 SelectIntGreaterThanInt 解决的 People batch。该算子将抉择所有大于 30 的 Age 值。这个新的 sel_age batch 而后传递到 ProjectSubIntInt 算子,该算子执行简略的减法运算以生成 tmp batch。最初,将这个 tmp batch 传递给 ProjectMultIntInt 算子,该算子将计算最终 Bonus =(Age-30)* 50 值。
矢量模型流程图
2.2 并行优化
在云溪数据库的开发过程中也对算子进行了优化,进步了运算效率。
2.2.1 tablereader 并行
Tablereader 通过拆分 Span 进行并行的 baRequest 下发读取数据,返回的数据封装进 baResponse 外面,放入管道由 tablereader 进行并行处理。
tablereader 的并行分为以下几步:
Step1:Span 拆分,逻辑打算实现后会生成一个 Span ALL(索引、主键查问除外),Span ALL 会依据 table 的 range 边界拆分从成多个范畴更小的 Span,每个 Span 会取得相应的 range 信息,依据 rangeID 能够获得对应 range 的正本信息,再依据正本抉择策略(就近抉择、随机抉择、lease holder(默认)),获取到对应正本的 nodeID,再将该 Span 放入一个 Map 构造(Map[nodeID][]Span)中;
当 tablereader 下发到了对应节点后,再将 Spans 进行平均调配进 tablereader 的各个 worker fethcer 当中进行并行的数据读取:
Step2:BatchRequest 下发,对应节点的 tablereader 的每个 fetcher 的 spans 的每一个 span 会封装为一个 ScanRequest 申请,多个 ScanRequest 申请封装进一个 BatchRequest(BacthRequest 申请中 header 信息能够指定一次申请返回的最大 kv 数目),该 BacthRequest 通过散发层逻辑后下发至对应节点的对应 Store 进行数据查问,返回的数据封装为 BatchResponse,蕴含多个对应的 ScanResponse,将 ScanResponse 的 kv 数据放入 channel 中,再由每个 fetcher 绑定的 worker 进行 kv 解码以及后续的解决:
Step3:数据返回,每个 fetcher 的 worker 协程解决 (通过 filter 或 render) 完每行 kv 数据后都会放入一个 buffer 当中(默认 buffer 缓存 <= 64 行),每个 worker 每实现一个 buffer 会将该 buffer 放入 tablereader 的后果管道中,提供 NextRow 和 NextChunk 两类接口供下层算子调用:
2.2.2 hashjoin 优化
原有 hashjoin 流程图如下:
原有执行流程存在如下问题:
· 单点流程是串行化执行,导致取出 outer 表的一行数据须要期待正在进行的 hashjoin 计算实现。
· hashjoin 计算只由一个协程执行,数据量大的时候比拟耗时。
通过优化后 hashjoin 由 3 个局部实现:
- Main thread:结构 hash 表;启动 Outer Fecther 和 Join Workers;从 join Woker 拿取计算结果,返回至下层;期待所有的 join worker 完结,更新状态为计算实现。
- Outer Fetcher(协程):循环读取 Outer 表每一行数据,将读取的数据通过 channel 传递给 Join Woker 进行计算;告诉 Join Wokers Outer 表读取实现。
- Join Workers(协程):将 Outer Fetcher 发送来的数据进行 hash join 计算;将计算结果通过 channel 发送至 Main thread。
优化前后比照剖析:
· 设结构 inner(storedSide)一侧的 hash 表工夫为 t1
· 设读取一条 outer(otherSide)数据工夫为 t2
· 设执行一轮 hashjoin 工夫为 t3
· 设 outer(otherSide)表有 m 条数据
执行完 hashjoin:
优化前耗时≈t1+mt2+mt3
优化后耗时≈t1+(m/n)*t3
Δt≈(m(n-1))/n t3+m*t2
预期:随着 outer 表数据增多和 join worker 协程数减少,实践上优化越显著。
经优化后有如下劣势:
- 计算读取拆散:将读取 outer 表和 hash join 计算拆散,使得读取 outer 表下一行数据不用再期待上一个 hash join 计算实现。
- 并行计算:启用多个 join worker 参加 hash join 计算,进步了并行度。