《云原生一站式数据库技术与实践》——二、云原生数据仓库AnalyticDB MySQL高性能存储引擎(2) https://developer.aliyun.com/article/1231658?groupCode=aliyundb
Replace语句被拆分为两步:先删除,再append。数据从Raft command 发出后,会先同步消费,再apply到table engine。table engine 消费完成之后,用户JDBC的insert into value 会立即返回,用户得到“返回成功”。此后即可对步骤③已经apply的数据进行查询。
但是,Replace 的实际流程为先删除后写入,因此需要进行一定的优化,防止用户的查询跳变。
如上图左下方所示,Client(1)是写入client,当前已经写到第199 条,下一步可能要做replace。Client(2)为读client,要读第100 条数据。当前,第100 条数据还未被删除,因此可以查询得到。
如果要执行replace,则会先将第100 条标记为删除,无法再得到查询结果。为了避免该种情况,此时会将第100 条数据标记在RowIdMap 进行删除屏蔽,在第②和③步之间保证查询没有问题。等append 即③完成之后,整个replace 执行完毕,200 条数据均已经存在,查询结果将返回新数据。
以上设计实现了replace的原子性保证。
针对下推到存储的计算,我们也进行了一些优化。
首先,DFP(dynamic filter pushdown)。Hash Join 有小表和大表,小表往往会被build 成hashtable,大表用于扫描。我们对其进行了优化,优化前提为小表非常小(或过滤之后非常小)且大表有索引。将hashtable 变成了另一种执行模式,将小表传输到大表侧,变成in 的算子进行下推,可以直接做二级分析裁剪;其次,得益于精确的索引,单个in 的索引开销只需在几十毫秒以内,节省了扫描大表的开销,性能也有了提高。
Hash Join 的另一优化为local index join。优化的前提条件为具有比较良好的建模,做Join 时的两个表使用了同一级分区键进行一级分区,保证它们分布时是对齐的。比如一个用户表和订单表同时按照user ID 进行一级分区,同时他们基于user ID 进行Join。基于以上前提,可以数据完全不走网络,小表在本地直接利用大表的索引进行Index find 找到命中的行,直接实现Local InnerJoin。
数据文件是典型的 RC File 的实现。Column Entry 记录在Meta file 里,包含列级统计信息,包含行数有多少个null、最大值、最小值等元信息。
Block entry 记录数据Block 的基础元信息,包括 min、max 以及offset。如果没有定义索引进行精确查找,则会通过min-max 进行粗糙集过滤,进而判断是否需要读数据block。如果需要读,则再通过offset找到具体的数据block。
在单条记录远超常规大小的前提下,对该字段的Block 进行批量加载很容易导致系统OOM。因此,这种情况下,超长字段会存储到独立的数据文件并为每条记录存在一条toast offset。最小的IO 单元由block自动退化为value。
我们支持多种索引,此外还有分区裁剪等策略来减少以计算量。在索引的选择上,支持目前列级的索引。用户仅需对每一列选择是否单独建索引,无需感知索引类型,能够根据用户的数据类型和数据特征自动化构建索引。此外为了使用户使用简单,也无需构建任何组合索引。
对于任何查询,ADB 都会将存储侧复杂查询拆分为不同的查询路径。如上图以id=123 and city in 'hangzhou'为例,存储引擎会先对两个查询条件在索引内进行独立查询,此后将结果集进行取交;如果为not,则取差。最终进行多路归并,取到结果集,即为最终的查询结果,无需构建组合索引。
该方案存在的主要问题为某些索引命中率非常高导致索引效率比较低,因此存储引擎内部也实现了一条基于代价的执行计划选择器自适应选择是否使用索引。
实时数据主要面向写优化,历史数据主要面向读优化,build 能够将这两种数据进行合并产生新的历史数据。合并过程会进行面向查询的建模,包括分区、排序、构建索引、收集统计信息,是CPU 密集且IO 密集型的操作。
《云原生一站式数据库技术与实践》——二、云原生数据仓库AnalyticDB MySQL高性能存储引擎(4) https://developer.aliyun.com/article/1231656?groupCode=aliyundb