postgreSQL和openGauss的connector支持update/delete的实现
1 相关背景
1.1 需求
当前 openLooKeng postgreSQL connector 不支持对数据表的 update 和 delete 操作,同时,openGauss 和 postgreSql 同源,因此本文拟在 openLooKeng 框架的基础上,开发 postgreSql/openGauss connector 支 持 update/delete 操作的代码。
1.2 openLooKeng 语句执行流程
一条 SQL 语句的执行的整体流程如下图所示:
这里面主要涉及到两种类型的节点,分别为 Coordinator 和 Worker 节点。
Coordinator 节点是用来解析语句,执行计划分析和管理 openLooKeng 的 Worker 节点,同时 Coordinator 跟踪每个 Work 的活动情况并协调查询语句的执行。Coordinator 为每个查询建立模型,模型包含多个 Stage,每个 Stage 再转为 Task 分发到不同的 Worker 上执行。
Worker 是负责执行任务和处理数据。Worker 从 Connector 获取数据。Worker 之间会交换中间数据。Coordinator 是负责从 Worker 获取结果并返回最终结果给 Client。
具体步骤为:
客户端通过协议发送一条查询语句给 openLooKeng 集群的 Coordinator;
Coordinator 接手到客户端传送到的查询语句,对语句进行解析、生成查询执行计划,根据查询执行计划一次生成 sqlQueryExecution->sqlStageExecution->HttpRemoteTask;
Coordinator 将每个 task 分发到所需要处理的数据所在的 Worker 节点上执行;
Task 通过 Connector 从数据源中读取数据,并执行 Source Stage 的 task;
处于下游的 Stage 中的 task 会读取上游 Stage 产生的输出结果,并在该 Stage 的每个 task 所处的 Worker 的内存中进行后续处理;
Coordinator 从发布的 task 后,持续地从 Singe Stage 中的 task 获取结果,并将结果缓存到 buffer 中,知道查询结束;
Client 从提交查询之后,会持续地从 Coordinator 获取本次查询的计算结果,知道获得所有的计算结果。
1.3 openLooKeng 内核调用
openLooKeng 的多源查询能力是通过 Connector 机制来实现的。其中 MySQL、postgreSQL 等 Connector 是主要是通过 presto-base-jdbc 中的代码来实现对 SQL 等数据源的读写。
其中openLooKeng-spi中主要定义了一些公共接口,供 openLooKeng-main 中的代码进行调用。
openLooKeng-base-jdbc是数据库连接器的公共模块,对 openLooKeng-main 进行了实现和补充。其代码经过编译后,会对应加载到 mysql-plugin 等插件中,实现 mysql 对数据源的访问功能。base-jdbc 本身自己不会编译单独的插件。
通过断点调试,对 openLooKeng 接受一条 update SQL 语句的函数栈调用流程进行梳理总结,如下图所示。
结合源码分析,发现一条 update 语句在 connector jdbc 中的执行流程为beginUpdate()->getConnection()->buildUpdateSql()->setUpdateSql()->finishUpdate()。在这个过程中需要一个字段作为行标识符来表示数据行。
其中buildUpdateSql将 openLooKeng 传入的 update SQL 语句转换为 connector 更新时的预编译语句,这里用到获取的行标识符作为 where 的筛选条件;setUpdateSql作用是以 where 条件对数据行进行更新。
因此若想要实现 postgreSQL conncetor 的 update/delete table 功能,我们需要重写上述函数即可完成。
2 代码实现
2.1 方案设计及论证
通过前期的调研,共形成了以主键为标识符和以 ctid 字段为标识符的两种开发方案。
方案一:以主键为标识符
主键的定义为:表中经常有一个列或列的组合,其值能唯一地标识表中的每一行。由定义可知,主键对应到一张数据表中会设计到多种场景,分别为:没有主键、单字段主键以及多字段组合成的复合主键,因此需要考虑的情况较多。
方案二:以 ctid 字段为标识符
ctid 表示数据行在它所处的表内的物理位置,ctid 字段的类型是 tid。尽管 ctid 可以快速定位数据行,每次 vacuum full 之后,数据行在块内的物理位置就会移动,即 ctid 会发生变化,所以 ctid 不能作为长期的行标识符。因此如果要以 ctid 字段为标识符进行代码开发,需首先验证 ctid 在同一事务中进行 update/delete 操作是否会发生变化。
2.2 ctid 在同一事务中的变化情况
为了验证 ctid 字段能否用于 postgreSQL 的 update/delete 操作,我们分别在单线程和多线程的情况下去验证 ctid 在同一事务和不同事务的变化情况。
单线程
通过 jdbc 模拟了单线程,在线程中对数据表进行 update 操作,分别查询 update 前后的信息,观察 ctid 的变化情况。
autocommit=false 时,结果显示 ctid 未发生变化:
autocommit=true 时,结果显示 ctid 发生变化:
多线程
同理,通过 jdbc 模拟了两个线程,在两个线程中分别对数据表进行 update 操作,分别查询 update 前后的信息,观察 ctid 的变化情况。
autocommit=false 时,结果显示 ctid 未发生变化:
autocommit=true 时,结果显示 ctid 发生变化:
由验证实验结果来看,在同一事务中,update 未提交前或者回滚之后 ctid 是不变的。
2.3 开发方案选择
综合对比两种方案,其优缺点对比如下:
方案 | 优点 | 缺点 |
---|---|---|
主键 | 1、每一数据行的标识符能够始终保持不变。 | 1、场景复杂,需考虑数据表无主键、单字段主键及多字段复合主键等多种情况; 2、不太适合现有 openLooKeng 下推框架。 |
ctid 字段 | 1、ctid 性质类似与 rowid,代码开发可借鉴性较高。 | 1、需考虑在同一事务中对数据行进行 update/delete。 |
综上所述,本文拟采用以 ctid 字段为行标识符进行代码开发。
2.4 ctid 字段为行标识符的代码开发
autocommit 设置为 false,即保证能在同一事务中进行操作,ctid 是可以保证唯一性的。而 PostgreSql 的继承关系如下图,同时 OpenGaussClient 也继承于 BasePostgreSqlClient
因此我们的主要开发重点在于重写beginUpdate()-> getConnection()-> buildUpdateSql-> setUpdateSql ->finishUpdate(),其代码实现主要放在 BasePostgreSqlClient
getUpdateRowIdColumnHandle用于获取行标识符
beginUpdate()
getConnection 重写 BaseJdbcClient 该方法,用于将只读模式设为 false,以便于进行更新操作
buildUpdateSql将 openLooKeng 传入的 update SQL 语句转换为 oracle 更新时的预编译语句,其内部调用关系为
buildUpdateSql()
buildRemoteSchemaTableName()
getColumnNameFromDataSource()
getColumnNameMap()
其中 buildRemoteSchemaTableName()获取数据表名,getColumnNameFromDataSource()用于从源数据中获取要更新的字段名,getColumnNameMap()用于构建字段名的映射关系,将原始字段统一转换为小写字母。具体实现如下图所示
setUpdateSql是以 where 条件对数据行进行更新,其内部调用关系为
setUpdateSql()
setStatement()
其中 setStatement()用于对需要更新的字段根据数据类型进行赋值。具体实现如下图所示:
到此,我们完成了 postgreSQL connector 的 update 功能代码,而 delete 功能代码与此具有很大的相似性,此处不做展开讲述,具体可参考https://gitee.com/openlookeng/hetu-core/pulls/1380 。
3 功能展示
此处我们简单演示 postgreSQL/openGauss connector 的 update/delete 特性。首先需对代码进行编译,运行**mvn clean install -DskipTests -T 1C 即可,编译完成之后,拷贝 hetu-server-1.5.0-SNAPSHOT 文件夹及 hetu-cli-1.50-SNAPSHOT-executable.jar 包。紧接着相关配置可参考社区文档。
在 web 界面输入 ip 及端口号,显示
数据表已经建好,具体实例为
针对 update,执行语句及结果包括
功能点 | SQL | 结果 |
---|---|---|
boolean 表达式 | update infomation set name = ‘Kitty’, age = 26, birthday = date’1995-08-16’ where id = 1; update infomation set name = ‘Jane’, sex = ‘2’ where score = ‘good’; update infomation set score = ‘qualified’ where salary = 10000.00; update infomationa set age = age + 1, birthday = date’1998-08-16’ where age = 22; update infomation setT salary = 36000 where birthday = date’1996-08-14’; update infomation set score = ‘bad’, birthday = date’1995-10-16’ where name = ‘Jane’; | success |
子查询 | update infomation set age=age+1 where salary=(select avg(salary) from data); update infomation set salary=salary+1000 where age in (select age from data where birthday between date’1993-09-01’ and date’1997-08-31’); | success |
针对 delete,执行语句及结果包括
功能点 | SQL | 结果 |
---|---|---|
boolean 表达式 | delete from infomation where name = ‘Bob’; delete from infomation where birthday = date’1996-08-14’; delete from infomation where salary = 15000; | success |
子查询 | delete from infomation where salary<(select avg(salary) from data); delete from infomation set salary=salary+1000 where age in (select age from data where birthday between date’1993-09-01’ and date’1997-08-31’); | success |