简介: Insert类的SQL语句的流程可初略分为:解析、校验、优化器、执行器、物理执行(GalaxyEngine执行)。本文将以一条简单的Insert语句通过调试的方式进行解读。

在阅读本文之前,强烈建议先阅读《PolarDB-X源码解读系列:SQL 的一生》,能够了解一条SQL的执行流程,也能知道GalaxySQL(CN)的各个组件,然后再阅读本文,了解Insert的具体实现过程,加深各个组件的理解。
Insert类的SQL语句的流程可初略分为:解析、校验、优化器、执行器、物理执行(GalaxyEngine执行)。本文将以一条简单的Insert语句通过调试的方式进行解读。
建表语句:

#一个简单的PolarDB-X中的分库分表sbtest

CREATE TABLE `sbtest` (

`id` int(11) NOT NULL AUTO_INCREMENT,

`k` int(11) NOT NULL DEFAULT '0',

`c` char(120) NOT NULL DEFAULT '',

`pad` char(60) NOT NULL DEFAULT '',

PRIMARY KEY (`id`)

)dbpartition by hash(`id`) tbpartition by hash(`id`) tbpartitions 2;

#调试语句

insert into sbtest(id) values(100);

解析

连接上PolarDB-X后,执行一条Insert语句insert into sbtest(id) values(100);PolarDB-X接收到该字符串语句后,开始执行该SQL,可见TConnection#executeSQL:

准备执行该SQL语句,ExecutionContext会保留该Sql执行的参数、配置、等上下文信息,该变量会一直陪伴该Sql经过解析、校验、优化器、执行器,直到下发给GalaxyEngine(DN)。
PolarDB-X执行该SQL时,需要先获取执行计划,可见代码TConnection#executeQuery:

ExecutionPlan plan = Planner.getInstance().plan(sql, executionContext);

为了避免执行同一条SQL每次都要解析、校验、优化器等操作,PolarDB-X内置了PlanCache,会在PlanCache中获取该SQL的执行计划,当然,并不是根据纯字符串SQL来进行缓存,而是生成SqlParameterized,如下图所示(Planner#plan),真正缓存的是sql模板,该类中的sql变量:INSERT INTO sbtest (id)\nVALUES (?),可适用于类似的语句,代表可填入的值,不同的值都是同一类SQL语句。

如果PlanCache找不到的话,需要生成新的执行计划,具体代码见PlanCache#getFromCache:

先将字符串通过FastsqlParser解析成抽象语法树,检查有没有语法错误等,生成SqlNode,本条SQL是Insert语句,解析成SqlInsert类,然后继续根据抽象语法树获取执行计划,具体SqlInsert内容为:

简单解释几个变量:

  • keywords:关键字,例如:Insert Ignore语句会加Ignore关键字,代表该语句特征;
  • source:数据来源,插入数据的来源,这里是values,如果是 Insert ... Select语句,则是select语句;
  • updateList:修改信息,例如:Insert ... ON DUPLICATE KEY 语句会把修改信息保存在该变量;

至此,完成了字符串SQL语句到SqlNode的转变,即完成了解析部分。

校验

校验过程即检查SqlNode的语义是否正确,例如表是否存在、列是否存在、类型是否正确等等,具体入口在Planner#getPlan函数中:

SqlNode validatedNode = converter.validate(ast);

便是验证该SQL的有效性,PolarDB-X沿用了Apache Calcite框架,validate的实现也是类似的大框架,包含Scope和Namespace两个概念,在此基础上进行验证,SqlInsert类型的验证入口在SqlValidatorImpl#validateInsert(SqlInsert insert)中:

...

final SqlValidatorNamespace targetNamespace = getNamespace(insert);

validateNamespace(targetNamespace, unknownType);

...

final SqlNode source = insert.getSource();

if (source instanceof SqlSelect) {

final SqlSelect sqlSelect = (SqlSelect) source;

validateSelect(sqlSelect, targetRowType);

} else {

final SqlValidatorScope scope = scopes.get(source);

validateQuery(source, scope, targetRowType);

}

...

大体流程检查两个部分:首先,检查insert into sbtest语句是否正确;然后检查SqlInsert.source部分是否有效。本条SQL是Values,所以检查Values是否有效,如果是Insert ...Select语句,source是SqlSelect,需要检查Select语句是否有效。没有报错,则说明SQL语句语义没有错误,校验通过,可以发现还是SqlInsert:

优化器

在经过优化器之前,还需要将SqlNode(SqlInsert)转成RelNode,大体含义就是将sql语法树转成关系表达式,入口在Planner#getPlan:

RelNode relNode = converter.toRel(validatedNode, plannerContext);

具体转换过程在SqlConverter#toRel:

...

final SqlToRelConverter sqlToRelConverter = new TddlSqlToRelConverter(...);

RelRoot root = sqlToRelConverter.convertQuery(validatedNode, false, true);

...

TddlSqlToRelConverter类是PolarDB-X的转换器,继承Calcite的SqlToRelConverter类,转换SqlInsert的执行过程在TddlSqlToRelConverter#convertInsert(SqlInsert call):

RelNode relNode = super.convertInsert(call);

if (relNode instanceof TableModify) {

...

}

可以发现, 会调用SqlToRelConverter#convertInsert,在该方法中,会将SqlInsert转成LogicalTableModify,该类的内容如下:

可以注意到几个变量:operation:操作类型;input:输入来源,本条sql是values;
PolarDB-X内部还有新的自己的RelNode,所以还会把RelNode再转成自己定义的RelNode,入口在Planner#getPlan:

ToDrdsRelVisitor toDrdsRelVisitor = new ToDrdsRelVisitor(validatedNode, plannerContext);

RelNode drdsRelNode = relNode.accept(toDrdsRelVisitor);

转换过程在ToDrdsRelVisitor#visit(RelNode other):

if ((other instanceof LogicalTableModify)) {

...

if (operation == TableModify.Operation.INSERT || ...) {

LogicalInsert logicalInsert = new LogicalInsert(modify);

...

}

}

Insert类型会转成LogicalInsert,就是PolarDB-X内部的RelNode,执行也是基于该类,LogicalInsert的内容如下(还有部分变量不在截图中):

大多数变量和LogicalTableModify一样,新增了像PolarDB-X特有的gsi相关变量等等。
然后便是经过优化器阶段,优化器执行过程代码在Planner#sqlRewriteAndPlanEnumerate:

private RelNode sqlRewriteAndPlanEnumerate(RelNode input, PlannerContext plannerContext) {

CalcitePlanOptimizerTrace.getOptimizerTracer().get().addSnapshot("Start", input, plannerContext);

//RBO优化

RelNode logicalOutput = optimizeBySqlWriter(input, plannerContext);

CalcitePlanOptimizerTrace.getOptimizerTracer().get()

.addSnapshot("PlanEnumerate", logicalOutput, plannerContext);

//CBO优化

RelNode bestPlan = optimizeByPlanEnumerator(logicalOutput, plannerContext);

// finally we should clear the planner to release memory

bestPlan.getCluster().getPlanner().clear();

bestPlan.getCluster().invalidateMetadataQuery();

return bestPlan;

}

Insert的优化器主要在RBO过程,定义了一些规则,CBO规则对Insert几乎没有改变。可以重点关注RBO的OptimizeLogicalInsertRule规则,会根据GMS(PolarDB-X的元数据管理)的信息来判断该SQL的执行计划,可能会将LogicalInsert转变成其它的RelNode去执行,方便区分不同的SQL执行方式,首先会确定该SQL的执行策略,主要分为三种:

public enum ExecutionStrategy {

/**

* Foreach row, exists only one target partition.

* Pushdown origin statement, with function call not pushable (like sequence call) replaced by RexCallParam.

* Typical for single table and partitioned table without gsi.

*/

PUSHDOWN,

/**

* Foreach row, might exists more than one target partition.

* Pushdown origin statement, with nondeterministic function call replaced by RexCallParam.

* Typical for broadcast table.

*/

DETERMINISTIC_PUSHDOWN,

/**

* Foreach row, might exists more than one target partition, and data in different target partitions might be different.

* Select then execute, with all function call replaced by RexCallParam.

* Typical for table with gsi or table are doing scale out.

*/

LOGICAL;

};

由于本条SQL较为简单,策略是PUSHDOWN,处理过程也比较简单,然后生成InsertWriter,该类负责生成下发到DN的SQL语句,保存在LogicalInsert中,OptimizeLogicalInsertRule处理规则较为细节,感兴趣的可以自行查看onMatch方法。
经过优化器后,还是LogicalInsert类的RelNode,至此,意味着优化器执行完毕。
最终会生成执行计划,在PlanCache#getFromCache,见下图(图中非全部变量):

ExecutionPlan.plan就是执行计划,可以发现是LogicalInsert,对于简单的Insert,PolarDB-X还会改写执行计划,代码在PlanCache#getFromCache:

BuildFinalPlanVisitor visitor = new BuildFinalPlanVisitor(executionPlan.getAst(), plannerContext);

executionPlan = executionPlan.copy(executionPlan.getPlan().accept(visitor));

insert into sbtest(id) values(100);语句执行BuildFinalPlanVisitor#buildNewPlanForInsert(LogicalInsert logicalInsert, ExecutionContext ec),因为该Insert语句比较简单,只有一个values,包含拆分键和auto_increment列,只需要根据拆分键就能确定下发到DN的哪一个分片,在CN端无需更多操作,所以会简化执行计划,在BuildFinalPlanVisitor#buildSingleTableInsert转成SingleTableOperation,并保存了分库分表规则,最终的执行计划如下:

执行计划变成SingleTableOperation,至此,执行计划生成完毕。

执行器

SQL语句生成执行计划后,将由执行器进行执行,执行入口在TConnection#executeQuery:

ResultCursor resultCursor = executor.execute(plan, executionContext);

然后会由ExecutorHelper#execute方法执行ExecutionPlan.plan,也就是前面的SingleTableOperation,执行策略有CURSOR、TP_LOCAL、AP_LOCAL、MPP,Insert类型基本都是走CURSOR,接着根据执行计划拿对应的Handler进行处理,具体可查看CommandHandlerFactoryMyImp类,例如:SingleTableOperation是MySingleTableModifyHandler,LogicalInsert是LogicalInsertHandler。会在对应的Handler里面进行执行,一般会返回一个Cursor,Cursor里面会调用真正的执行过程,调用Cursor.next便会获取结果,Insert语句的结果是affect Rows,本条SQL会创建一个
MyPhyTableModifyCursor,入口在MySingleTableModifyHandler#handleInner:

...

MyPhyTableModifyCursor modifyCursor = (MyPhyTableModifyCursor) repo.getCursorFactory().repoCursor(executionContext, logicalPlan);

...

affectRows = modifyCursor.batchUpdate();

...

根据ExecutionContext和SingleTableOperation创建一个MyPhyTableModifyCursor,然后直接执行:

public int[] batchUpdate() {

try {

return handler.executeUpdate(this.plan);

} catch (SQLException e) {

throw GeneralUtil.nestedException(e);

}

}

这里的this.plan就是SingleTableOperation,handler是PolarDB-X的CN与DN间交互的MyJdbcHandler,可以认为是执行物理计划的handler,会根据plan生成真正的物理SQL,下发到DN执行。
由于这条SQL较为简单,CN不需要过多处理,再举一例Insert语句:insert into sbtest(k) values(101),(102);
经过优化器后,该语句的执行计划是LogicalInsert,如下图:

可以发现sqlTemplate为INSERT\nINTO ? (id,k)\nVALUES(?, ?),表名可能要换成物理表名,同时增加了一列id,因为该列是auto_increment,会有一个全局的sequence表来记录该列的值,才能保证全局唯一,插入的values的参数保留在ExecutionContext的params中,如下图:

id列的值会在真正生成物理执行计划的时候才会去获取,LogicalInsert计划适用LogicalInsertHandler来执行,执行过程:

public Cursor handle(RelNode logicalPlan, ExecutionContext executionContext){

...

LogicalInsert logicalInsert = (LogicalInsert) logicalPlan;

...

if (!logicalInsert.isSourceSelect()) {

affectRows = doExecute(logicalInsert, executionContext, handlerParams);

} else {

affectRows = selectForInsert(logicalInsert, executionContext, handlerParams);

}

...

}

会根据来源是否是Select语句选择不同的执行方式,具体执行过程在LogicalInsertHandler#executeInsert,如下:

...

//生成主表的物理执行计划

final InsertWriter primaryWriter = logicalInsert.getPrimaryInsertWriter();

List inputs = primaryWriter.getInput(executionContext);

...

//如果有GSI,生成GSI表的物理执行计划

final List gsiWriters = logicalInsert.getGsiInsertWriters();

gsiWriters.stream().map(gsiWriter -> gsiWriter.getInput(executionContext))...;

...

//执行所有物理执行计划

final int totalAffectRows = executePhysicalPlan(allPhyPlan, executionContext, schemaName, isBroadcast);

...

主表生成物理执行计划过程中,会先获取id的值,由于id也是拆分键,所以两个values会根据拆分键定位到不同的物理分库分表上,会生成有两个物理执行计划,如下:


其中dbIndex是物理库名,tableNames是物理表名,param保存了这条slqTemplate的参数值,填充上就是完整的SQL,然后执行所有物理执行计划,就完成了该SQL的执行。

物理执行

PolarDB-X中CN与DN的交互都在MyJdbcHandler中,以SingleTableOperation为例,看看具体交互过程:

public int[] executeUpdate(BaseQueryOperation phyTableModify) throws SQLException {

...

//获取物理执行计划的库名和参数

Pair> dbIndexAndParam =

phyTableModify.getDbIndexAndParam(executionContext.getParams() == null ? null : executionContext.getParams()

.getCurrentParameter(), executionContext);

...

//根据库名获取连接

connection = getPhyConnection(transaction, rw, groupName);

...

//根据参数组成字符串SQL

String sql = buildSql(sqlAndParam.sql, executionContext);

...

//根据连接创建prepareStatement

ps = prepareStatement(sql, connection, executionContext, isInsert, false);

...

//设置参数

ParameterMethod.setParameters(ps, sqlAndParam.param);

...

//执行

affectRow = ((PreparedStatement) ps).executeUpdate();

...

}

将物理执行计划发送到DN执行,执行完成后,根据affectRow返回到执行器,最终会把结果返回给用户,至此,一条完整SQL就执行完成。

小结

本文通过调试简单的Insert语句,介绍了PolarDB-X在解析、校验、优化器、执行器对Insert语句的处理,当然,Insert语句也有很多特殊的用法,本文并没有一一概述,感兴趣的同学可以在相应代码处进行查看。

本文为阿里云原创内容,未经允许不得转载。

PolarDB-X源码解读系列:DML之Insert流程的更多相关文章

  1. Alamofire源码解读系列(二)之错误处理(AFError)

    本篇主要讲解Alamofire中错误的处理机制 前言 在开发中,往往最容易被忽略的内容就是对错误的处理.有经验的开发者,能够对自己写的每行代码负责,而且非常清楚自己写的代码在什么时候会出现异常,这样就 ...

  2. Alamofire源码解读系列(四)之参数编码(ParameterEncoding)

    本篇讲解参数编码的内容 前言 我们在开发中发的每一个请求都是通过URLRequest来进行封装的,可以通过一个URL生成URLRequest.那么如果我有一个参数字典,这个参数字典又是如何从客户端传递 ...

  3. Alamofire源码解读系列(三)之通知处理(Notification)

    本篇讲解swift中通知的用法 前言 通知作为传递事件和数据的载体,在使用中是不受限制的.由于忘记移除某个通知的监听,会造成很多潜在的问题,这些问题在测试中是很难被发现的.但这不是我们这篇文章探讨的主 ...

  4. Alamofire源码解读系列(五)之结果封装(Result)

    本篇讲解Result的封装 前言 有时候,我们会根据现实中的事物来对程序中的某个业务关系进行抽象,这句话很难理解.在Alamofire中,使用Response来描述请求后的结果.我们都知道Alamof ...

  5. Alamofire源码解读系列(六)之Task代理(TaskDelegate)

    本篇介绍Task代理(TaskDelegate.swift) 前言 我相信可能有80%的同学使用AFNetworking或者Alamofire处理网络事件,并且这两个框架都提供了丰富的功能,我也相信很 ...

  6. Alamofire源码解读系列(七)之网络监控(NetworkReachabilityManager)

    Alamofire源码解读系列(七)之网络监控(NetworkReachabilityManager) 本篇主要讲解iOS开发中的网络监控 前言 在开发中,有时候我们需要获取这些信息: 手机是否联网 ...

  7. Alamofire源码解读系列(八)之安全策略(ServerTrustPolicy)

    本篇主要讲解Alamofire中安全验证代码 前言 作为开发人员,理解HTTPS的原理和应用算是一项基本技能.HTTPS目前来说是非常安全的,但仍然有大量的公司还在使用HTTP.其实HTTPS也并不是 ...

  8. Alamofire源码解读系列(九)之响应封装(Response)

    本篇主要带来Alamofire中Response的解读 前言 在每篇文章的前言部分,我都会把我认为的本篇最重要的内容提前讲一下.我更想同大家分享这些顶级框架在设计和编码层次究竟有哪些过人的地方?当然, ...

  9. Alamofire源码解读系列(十)之序列化(ResponseSerialization)

    本篇主要讲解Alamofire中如何把服务器返回的数据序列化 前言 和前边的文章不同, 在这一篇中,我想从程序的设计层次上解读ResponseSerialization这个文件.更直观的去探讨该功能是 ...

  10. Alamofire源码解读系列(十一)之多表单(MultipartFormData)

    本篇讲解跟上传数据相关的多表单 前言 我相信应该有不少的开发者不明白多表单是怎么一回事,然而事实上,多表单确实很简单.试想一下,如果有多个不同类型的文件(png/txt/mp3/pdf等等)需要上传给 ...

随机推荐

  1. awk第一天

    awk第一天 1.用awk 打印整个test.txt (以下操作都是用awk工具实现,针对test.txt) awk '{print}' test.txt [root@master ~]# awk ' ...

  2. 云游长江大桥,3DCAT实时云渲染助力打造沉浸化数字文旅平台

    南京长江大桥是中国第一座自主设计建造的双层公路铁路桥,也是世界上最早的双层公路铁路桥之一.它不仅是一座桥梁,更是一座历史文化的见证者和传承者.它见证了中国人民的智慧和奋斗,承载了中国社会的变迁和发展. ...

  3. java方法的内存及练习

    方法的内存 一.方法调用的基本内存原理: Java内存分配 栈: 方法运行时使用的内存方法进栈运行,运行完毕就出栈 堆: newl出来的,都在堆内存中开辟了一个小空间 方法区: 存储可以运行的clas ...

  4. HTML(html结构、标签导读 、路径))

    HTML第一天 我们接下来是进行的网页开发网页的相关概念: 什么是网页? 什么是HTML? 网页的形成? 一 什么是网页: 1.网站是指在因特网上根据一定的规则,使用 HTML 等制作的用于展示特定内 ...

  5. Python解压当天创建的ZIP文件到指定的文件夹中

    1. 首先,导入了需要使用的模块: - `zipfile`:用于处理ZIP文件的模块. - `os`:提供了与操作系统交互的功能. - `shutil`:用于高级文件操作,例如移动文件等. - `da ...

  6. 工具推荐-personal kanban

    工具推荐 -- personal kanban 看板工具 在项目中接触到项目管理工具pingcode中含有看板工具 但是实际使用时一般一周才看一下项目进度 这个看板的参与度实际上很低 为了将日常的工作 ...

  7. 在idea/webstorm等terminal运行命令报错:Command rejected by the operating system没有权限【已解决】

    在idea/webstorm等编译器terminal窗口运行命令报错:Command rejected by the operating system没有权限[已解决] 1.修改terminal窗口 ...

  8. 成长计划校园极客秀|基于OpenHarmony的智能阳台

    前言 本文由OpenAtom OpenHarmony(以下简称"OpenHarmony")开源开发者成长计划活动的参与者李建涛提供,详细阐述了由搭载OpenHarmony系统的拓维 ...

  9. 如何在 Python 中执行 MySQL 结果限制和分页查询

    Python MySQL 限制结果 限制结果数量 示例 1: 获取您自己的 Python 服务器 选择 "customers" 表中的前 5 条记录: import mysql.c ...

  10. Linux忘记root密码修改方法

    1. 再开机启动的时候按键盘上的"e"键会出现如下界面.再次按"e"键进行选择相应的内核. 2. 选择相应的内核,再次按"e". 3.经过第 ...