执行计划

在 OceanBase 中,所有的 DML 语句都会生成对应的执行计划来进行数据的读写操作,每一种 DML 语句都会生成一种对应的 DML 算子。DML 算子负责读取数据,并通过存储层提供的数据写入接口将 DML 语句涉及到的数据写入存储引擎中。对于 INSERT/REPLACE 语句而言,由于其不用读取表中的已有数据,因此,INSERT 语句的执行计划相对简单,其执行计划为简单的 EXPR VALUES+INSERT OP 算子构成。而对于 UPDATE 或者 DELETE 语句而言,优化器会通过代价模型对 WHERE 条件进行访问路径的选择,或者 ORDER BY 数据顺序的选择,详细选择方式请参考访问路径章节。

INSERT 执行计划示例

UPDATE 执行计划示例

DELETE 执行计划示例

OceanBase 是一个分布式关系数据库,所有的表都可以通过一定的分区方式扩展成分区表,不同的分区可以被打散存储在不同的物理位置。
如下例子为单机部署的环境,有两条 UPDATE 语句。第一条显然是单分区的更新,第二条语句虽然是多分区更新,但是不存在分区之间进行数据交叉访问,并且分区的物理位置都在一台机器上,所以第二条 UPDATE 语句也能执行成功,进一步展开第二条 UPDATE 语句的计划可以看出,该语句确实更新了多个分区,但是分区之间各自独立更新,没有数据交叉访问。

OceanBase2.x 版本支持全局时间戳服务,在开启全局时间戳服务的情况下,事务支持单语句跨物理位置的多分区写操作。因此,在 OceanBase2.x 版本中,DML 语句支持跨分区的读写操作。由于分区信息跟数据密切相关,因此只有在执行阶段能够读取到具体数据才能确定分区信息,所以在优化器生成执行计划阶段,DML 语句只能判断是否进行分区写操作,而真实的分区写操作只能等到执行阶段通过具体数据生成执行信息,通过数据 shuffle 出具体的分区信息然后生成分区写任务,多分区写操作通过 RPC 机制来维护,而对于小规模的数据写操作而言,RPC 是其性能的主要消耗,因此,在 OceanBase2.x 版本中,为了节省 RPC 次数,执行计划的 RPC 操作会按物理的 Server 进行聚合。

事务管理

OceanBase 提供 READ-COMMITTED 隔离级别的事务管理,所有写入 OceanBase 数据库的数据都必须满足事务的 ACID 约束,因此,在每一条 DML 语句的执行过程中,也伴随着事务对写入数据的控制。

DML 语句的执行和事务相关的交互包括 start_stmt、start_participant、end_participant、end_stmt 四个步骤:

  1. 在一条 DML 语句执行前需要开启 start_stmt ,告诉事务控制层,该DML 语句的写操作将被开启。start_stmt 需要收集该 DML 的分区信息,用来区分事务的类型。如果事务涉及到一个分区,即为本地事务;如果涉及到多个分区,则该事务需要进入分布式事务流程。对于单分区的 DML 来说涉及到的分区信息在 start_stmt 阶段就已经确定,对于跨分区 DML 而言,由于分区信息是在执行过程中动态确定的,因此,事务层还提供了 extend_stmt 接口用来动态的增加该语句参与事务的分区信息;
  2. 如果该分区发生了写失败操作,end_participant 会回滚该分区的写入脏数据;
  3. end_stmt 会回滚该 DML 语句写入的所有数据,如果 DML 语句执行成功,end_stmt 会记录该语句的 save point,当该事务内后续的其它语句写失败的时候,save point 用来决定回滚的起始位置。

DML 操作的表对象每一列都有相关的约束性定义,例如列的 NOT NULL 约束,UNIQUE KEY 约束等。在 DML语句最终写入数据前,需要对列的 NOT NULL、UNIQUE KEY 约束进行检查,同时还要对写入数据进行类型转换,保证写入数据的类型同 Schema 中列定义的类型保持一致。当约束性检查失败,需要回滚该 DML 语句写入的脏数据,NOT NULL 检查和类型转换通过 SQL 层生成的 COLUMN_CONVERT 表达式来完成,执行计划会为 DML语句写入表中的每一列都添加该表达式。在执行算子中,数据以行的形式被流式的迭代,在迭代过程中,COLUMN_CONVERT 表达式被计算,即可完成相应的类型转换和约束性检查,而 UNIQUE KEY 约束的检查是在存储层的 data buffer 中完成。

锁管理

加锁时机

加锁顺序

如果写数据的表中含有索引,DML 操作还需要级联的同步更新数据表中的数据和索引表中的数据。当并发 DML 操作存在的时候,如果不规定数据表和索引更新和加锁的先后顺序,实现层面上将极其容易发生死锁,为了规避实现上的死锁,这个时候,就涉及到加锁顺序的约定:

  • 如果 DML 涉及的表对象只含有 LOCAL INDEX,由于 LOCAL INDEX 同数据表的存储位置是绑定在一起的,为了避免并发带来的实现上的死锁,我们规定必须先在主表上加锁,如果是普通的 LOCAL 索引,因为其主键包含数据表主键,因此没必要再对普通的 LOCAL 索引再加锁,而对于 UNIQUE 索引,由于其主键不包含数据表主键,因此对于UNIQUE 索引的更新,当数据表主键加锁更新后还需要再对 UNIQUE 索引主键单独加锁并更新。
  • 如果 DML 涉及的表对象含有 GLOBAL INDEX,如果 GLOBAL INDEX 和数据表都是非分区表,那么其存储位置是相互绑定的,其本质和 LOCAL INDEX 是一样的,DML 对于其的处理也和 LOCAL INDEX 一致。
  • 如果 GLOBAL INDEX 或者数据表是分区表,GLOBAL INDEX 和数据表的存储位置就是是完全独立的,因此从数据存储层来看,GLOBAL INDEX 是一张独立的表,在 DML 更新数据表和 GLOBAL INDEX 的时候,由于物理位置的隔离,产生RPC交互也是无法避免的,权衡了 RPC 次数带来的性能损失和加锁死锁的概率后,规定数据表和 GLOBAL INDEX 的加锁顺序为:

  • 如果 DML 涉及到的数据表和 GLOBAL INDEX 都分布在不同的 server 上,将不再约定 server 间的先后顺序,各个 server 并行执行,但是在每个 Server 内部如果同时存在更新数据表和 GLOBAL INDEX,依然会先对数据表加锁,在对 GLOBAL INDEX 加锁,这是出于尽力避免死锁的考虑。

免死锁,对于 GLOBAL INDEX 的 DML 操作,用户应当尽量保证 DML 操作涉及到的数据表中的数据位于同一个 server 上。