共计 2815 个字符,预计需要花费 8 分钟才能阅读完成。
导读 | DDL 是数据定义语言,用于定义和管理 SQL 数据库中的所有对象的语言,常用的命令有:create,drop,alter 等。 |
DDL 是数据定义语言,用于定义和管理 SQL 数据库中的所有对象的语言,常用的命令有:create,drop,alter 等。通常来说,浪潮云溪数据库 DDL 语句的流程主要分为四个部分,分别是逻辑计划生成,物理计划生成,计划执行和 schemachange。本文主要介绍逻辑计划生成,物理计划生成和计划执行。
逻辑计划生成主要是生成一个 planNode 逻辑计划节点,每一条 SQL 都会有自己的 planNode,以 create table 为例,该 SQL 生成的 planNode 中包含有 CREATETABLE statement 信息例如表名,有该表所属数据库的信息,还有该表的列的信息等。planNode 生成后将其记录到 planner 的 curplan(curplan 有当前计划的属性,包含抽象语法树,planNode,子查询计划等)里,主要流程如下图所示。
以 create table 语句为例,构建逻辑计划主要是进行 memo 的构建以及 RBO 和 CBO 优化(memo 是用来存储查询计划森林的一种数据结构)。首先会初始化一个优化计划的上下文 optPlanningCtx,里面会初始化优化器并记录是否使用 memo cache 进行 memo 的缓存复用,而这也是 DDL 与 DML 语句之间的区别,DDL 语句不会复用 memo。
构建 memo 会调用 builder 的 build () 方法,对于不同的 DDL 语句,会调用不同的方法去构建逻辑计划。Create table 语句会调用 buildCreateTable, 构建 outScope,主要是生成语句的表达式 expr 记录在 outScope 里,最终记录到 memo 里面。而对于其他的 DDL 语句则会调用 tryBuildOpaque,然后通过 ConstructOpaqueDDL 函数构建 memo 的 expr。
Memo 构建完以后就是进行优化,通过 Optmize () 函数进入进行 RBO 和 CBO 优化,而 DDL 语句不会进行优化,这也是与 DML 语句的区别,DML 会将优化完的 memo 加入缓存。然后就是进行逻辑计划 planNode 的构建,DDL 语句中,create 语句会执行 buildCreateTable 进行 ConstructCreateTable 构建 createTableNode,而 drop 和 alter 语句则会执行 buildOpaque,通过 ConstructOpaque 返回所对应的 planNode,最后将构建好的 planNode 封装到 planTop 里面,并且如果是 DDL 语句会在 planTop 的 flags 里面置 planFlagIsDDL。
逻辑计划生成后,就会根据逻辑计划生成物理计划,主要的流程如下图所示。
在生成物理计划之前会先判断语句是否需要分布式执行,DDL 不会进行分布式执行,会生成一个本地的 PlanningCtx(PlanningCtx 包含在单个查询的整个规划过程中使用和更新的数据)。如果 SQL 语句有子句的话,则会调用 PlanAndRunSubqueries 函数先执行子句。DDL 语句会进入到 wrapPlan 进行物理计划的生成。在 wrapPlan 中会深度优先遍历 planNode 树,找到第一个支持 DistSQL processor 的 planNode 然后在该 planNode 上递归 DistSQL 优化,如果有等效的 DistSQL 处理器调用 createPlanForNode 生成物理计划。
函数首先判断当前 planNode 的类型,调用对应的函数为 planNode 创建物理计划 (如 indexJoinNode 会调用 createPlanForIndexJoin),DDL 则会递归调用 wrapPlan 将 planNode 包起来继续处理 (wrapPlan 只包装节点本身而不包括孩子节点)。然后,调用 shouldPlanTestMetadata 函数判断是否需要进行元数据处理,如果需要则添加相关信息)。然后再进行创建 planNodeToRowSource(该结构体),如果被包裹的 planNode 是 flow 中的第一个 node,且语句类型是 RowsAffected(返回受影响行的计数的语句)则可以使用 fast path,planNode 子树若支持 DistSQL 优化会在被优化后连接到 wrapper。返回 wrapper 后,为物理计划添加 LocalProcessor 和 LocalProcessorIndexes 的元素,LocalProcessors 数组包含了所有的 planNodeToRowSource,即被包裹的 planNode,记录物理计划的 ResultTypes。
每当添加新的 PhysicalPlan 时,都需要覆写 ResultRouters,我们将只需要一个 result router,由于 local processor 不是分布式的,确保 p.ResultRouters 只有一个元素,最后填充计划的 endpoints 就进入执行流程。
执行过程的主要流程如下图所示。
StartExec 是具体执行 planNode 的方法,根据构建的物理计划对表描述符进行操作。StartExec 会先对 commend 遍历(以 alter table 为例,commend 是一个表修改操作的切片),在遍历中获取集群版本信息,检查在当前版本中是否支持添加列的类型,若支持则为 true,反之报错;接着按照计划中的列定义信息生成列描述符,若添加的列是主键或含有唯一性约束,还会生成索引描述符,将这些描述符会包装成一个个 mutation 并添加到表描述符的 Mutations 字段里;然后根据表、mutation 等信息创建一个 job,job 也是通过系统表 system.jobs 进行维护,这个 job 是用来触发及跟踪 schema change 执行,接着将含有 mutations 的表描述通过 batch 更新到系统表 “descriptor” 里。
执行流程中最主要的结构体就是 batch,表描述符会存在 batch 里面,通过 writeTableDescToBatch 函数,在该函数里面,首先判断这个表如果不是一个新的表,需要将表的 version+1,然后会验证表描述符格式是否良好,调用 addUncommittedTable 函数,这允许事务在绕过表租赁机制的情况下查看自己的修改,最后是将表描述符的 KV 写入到 batch 里面。batch 构建完以后就执行 batchRequest。
在执行流程结束后,如果需要数据回填的话,则进入到 online schema change 流程回填数据然后写入系统表完成执行流程,如果不需要的话,则在执行算子的时候就将数据写入到系统表中完成执行流程。