在开发中,数据的变更与维护工作一般较频繁。当我们执行数据库的DML操作时,必须谨慎考虑变更对数据可能产生的后果,以及变更是否能够顺利执行。若出现意外数据丢失、操作失误或语法错误等情况,我们必须迅速将数据库恢复到变更之前的状态,以确保数据的一致性和完整性。然而,回滚操作通常需要开发人员手动编写回滚SQL脚本,这不仅繁琐复杂,而且极易出错。
为了解决这个问题,ODC(OceanBase Developer Center)V4.2.0支持在执行数据库变更任务时自动生成备份回滚 SQL 以提高开发效率、减少错误率,保证数据的一致性和完整性。当单条误操作 SQL 受影响的数据量在10万以内时,您可使用该方法进行数据恢复。
也可以在工单「任务详情」中的「任务流程」中点击「下载备份回滚方案」以下载该文件。
例如,针对 「SQL内容」为以下的数据库变更任务:
- update t2 set c2=11 where c1=1;
- update tab2 set c2=11 where id=1;
生成的「备份回滚方案」示例如下:
- /*
- [SQL]:
- update t2 set c2=11 where c1=1
-
- [QUERY SQL]:
- SELECT t2.* FROM t2 WHERE c1=1;
-
- */
- REPLACE INTO `jingtian_test`.`t2` VALUES (1,11,1);
-
- /*
- [SQL]:
- update tab2 set c2=11 where id=1
-
- [ERROR MESSAGE]:
- It is not supported to generate rollback plan for tables without primary key or unique key
- */
包含以下几部分内容:
[ERROR MESSAGE]
中。下面是 ODC 根据一条 SQL 生成对应的回滚 SQL 的流程图:
接下来详解介绍每一个流程的具体实现。
SQL 解析可以理解为对 SQL 进行建模,将原始 SQL 语句转化为一个 SQL 对象。我们根据 SQL 的词法和语法文件可以生成 SQL 的词法分析器和语法分析器。词法分析阶段将 SQL 语句拆解成 Token 序列,并识别出关键词、标识、常量等,而语法分析阶段基于词法分析的结果,构造出一棵抽象语法数。例如一条 SQL:
UPDATE tab1, tab2 SET tab1.c2 = 100, tab2.c2=200 WHERE tab1.c1 = tab2.c1;
生成对应的抽象语法树如下图:
通过遍历语法树将其转化为一个 SQL 对象:
可以看到,这个 SQL 对象实际上是将 SQL 语句结构化了,对 SQL 的各个部分都定义了相应的抽象,比如针对 where 的查询条件我们定义成了 CompoundExpression
对象。通过将 SQL 语句解析成对象,我们就可以围绕这个对象编写生成对应回滚 SQL 的处理逻辑。
SQL 校验阶段就是基于解析出来的 SQL statement 对象去做一些校验的逻辑:
- -- 查询主键
- SHOW INDEX FROM table_name WHERE Non_unique = 0 and Key_name='PRIMARY';
-
- -- 查询唯一建
- SHOW INDEX FROM table_name WHERE Non_unique = 0;
而针对 OB ORACLE 模式,我们直接采用 ROWID 作为每一张变更表的唯一建,因此不需要有校验变更表是否具有主键或者唯一键的逻辑。
UPDATE t1, t2 set col1=2222, col2=333 WHERE t1.c1=t2.c1;
由于 table_references 中涉及多个表,但是 set column_name=... 时没有指明该column_name 的表名,导致我们仅根据这一条 SQL 无法直接确认变更的字段属于哪个表,因此该 SQL 不符合能够生成回滚 SQL 的语句。这种情况只需稍做修改,指明列所属的表就可以支持生成回滚 SQL:
UPDATE t1, t2 set t1.col1=2222, t2.col2=333 WHERE t1.c1=t2.c1;
如何基于一条 SQL 生成对应的查询原始数据的 SQL 呢?其实核心逻辑就是基于解析出的 SQL 对象和 SQL 语句格式去获取我们需要的信息进而拼接出查询 SQL。拿 UPDATE 语句为例,以下是 OB MYSQL 的 UPDATE 语句的格式:
- UPDATE [IGNORE] table_references
- SET update_asgn_list
- [WHERE where_condition]
- [ORDER BY order_list]
- [LIMIT row_count];
-
- table_references:
- tbl_name [PARTITION (partition_name,...)] [, ...]
-
- update_asgn_list:
- column_name = expr [, ...]
-
- order_list:
- column_name [ASC|DESC] [, column_name [ASC|DESC]...]
简单示例
比如基于以下 SQL:
UPDATE tab1, tab2 SET tab1.c2 = 100, tab2.c2=200 WHERE tab1.c1 = tab2.c1;
通过 SQL 解析后基于 SQL 对象我们可以获取该 SQL Statement 的 table_references、update_asgn_list 和 where_condition,通过 update_asgn_list 我们可以知道该 SQL 涉及的变更表有2个:tab1 和 tab2,进而我们就可以针对每个变更表生成的查询 SQL :
- SELECT * FROM tab1 WHERE tab1.c1 = tab2.c1;
-
- SELECT * FROM tab2 WHERE tab1.c1 = tab2.c1;
当然以上的示例 SQL 是一个最简单的例子,复杂一点的场景比如涉及 多表 join 和 子查询 的场景又该怎么生成对应的查询 SQL 呢?其实不管 SQL 多复杂我们的核心都是基于 SQL Statement 去拼接出查询 SQL。
考虑如下涉及多表 join 的 SQL:
UPDATE tab1 v1 inner join tab2 v2 inner join tab3 v3 on v1.c1 = v2.c1 and v1.c1=v3.c1 SET v1.c2=200, v2.c2=200;
针对这条 SQL 我们通过解析 table_references、 update_asgn_list 从而拼接的查询 SQL 如下:
- SELECT v1.* FROM tab1 v1 inner join tab2 v2 inner join tab3 v3 on v1.c1 = v2.c1 and v1.c1=v3.c1;
-
- SELECT v2.* FROM tab1 v1 inner join tab2 v2 inner join tab3 v3 on v1.c1 = v2.c1 and v1.c1=v3.c1;
获取到基于原始 SQL 生成的查询 SQL 后,我们就可以执行查询 SQL 来获取原始数据了。在查询数据的时候可能会遇上这个问题:一次查询出的数据过大,导致内存溢出怎么办?针对这个问题传统的处理方式也有很多,比如通过 limit 分页查询,但是这种方式在我们的场景下不适用,比如如果原始 SQL 就带了 limit 那么这种方式就失效了。ODC 采用根据表的主键或唯一键(优先采用主键)拼接批量查询 SQL 的方式来解决这个问题。
OB MYSQL 模式
比如我们通过查询 SQL 可以获取到表 tab1 的主键列为 c1,如果原始的查询 SQL 如下:
SELECT tab1.* FROM tab1 WHERE c3 > 1;
我们先查询出所有变更行数据的主键值:
SELECT tab1.c1 FROM tab1 WHERE c3 > 1;
通过以上 SQL 我们就可以查询出变更的总行数以及每一行的主键值,若总变更行数为 1500 且设置的批量查询的大小为 1000,也就是每次查询最多获取 1000 行的数据,那么基于以上查询 SQL 得到的批量查询 SQL 为:
- SELECT tab1.* FROM tab1 WHERE c3 > 1 AND c1 IN (1,2,3, ..., 1000);
-
- SELECT tab1.* FROM tab1 WHERE c3 > 1 AND c1 IN (1001,1002,1003, ..., 1500);
这样以来就可以做到批量查询原始变更数据了。由于增加的 where 条件也是走索引的,因此也不存在性能瓶颈问题。
OB ORACLE 模式
针对 ORACLE 模式我们直接利用 ROWID 来拼接批量查询 SQL。比如原始 SQL;
SELECT tab1.* FROM tab1 WHERE c3 > 1;
我们先查询出所有变更行数据的 ROWID 值:
SELECT ROWID FROM tab1 WHERE c3 > 1;
那么基于以上查询 SQL 得到的批量查询 SQL 为:
SELECT tab1.* FROM tab1 WHERE c3 > 1 AND ROWID IN (...);
通过以上处理逻辑,我们已经获得了原始变更数据,那么接下来就是最后一步基于变更行数据生成回滚 SQL 了。
OB MYSQL 模式
针对 OB MYSQL 模式,UPDATE 语句生成的对应回滚 SQL 为 REPLACE INTO 语句,REPLACE INTO 用于实时覆盖写入数据。写入数据时,会先根据主键或唯一键判断待写入的数据是否已经存在于表中,并根据判断结果选择不同的方式写入数据:
这里要求变更表必须有主键或者是唯一索引,否则 replace into 会直接插入数据(等效于 INSERT INTO),这可能会导致表中出现重复的数据。
最终 ODC 生成的完整的备份回滚 SQL 文件如下所示:
- /*
- [SQL]:
- update tab set col2=2 where col=1;
-
- [QUERY SQL]:
- SELECT tab.* FROM tab WHERE col=1;
-
- */
- REPLACE INTO `schema_name`.`tab` VALUES (1,1,1);
OB ORACLE 模式
针对 OB ORACLE 模式,会先生成 DELTE 语句来删除变更数据,然后生成 INSERT INTO 语句插入原始的变更前的数据,从而避免插入数据时存在主键或者唯一键的冲突。
- /*
- [SQL]:
- UPDATE TAB1 set c2=2 where c1=1
-
- [QUERY SQL]:
- SELECT TAB1.* FROM TAB1 WHERE c1=1;
-
- */
- DELETE FROM TAB1 WHERE c1=1;
- INSERT INTO "TEST"."TAB1" VALUES (1,1);
针对 DELETE 语句,生成的回滚 SQL 为 INSERT INTO 语句。例如针对以下变更 SQL:
delete from tab where col=1;
最终生成的备份回滚文件内容如下:
- /*
- [SQL]:
- delete from tab where col=1;
-
- [QUERY SQL]:
- SELECT tab.* FROM tab WHERE col=1;
-
- */
- INSERT INTO `schema_`.`tab` VALUES (1,1,1);
感谢您的阅读~ODC 已于 2023 年 DTCC 大会上正式宣布开源。ODC 希望通过开源,与社区共同打造一款帮助 Dev 和 DBA 真正实现“ONE team”的企业级数据库协同开发工具。如果你对这个开源项目感兴趣,欢迎以任何形式参与 ODC 社区。无论是贡献代码、提出问题、分享见解,或者仅仅是表达对这个开源项目的喜爱与支持,都将成为推动 ODC 项目发展的动力。
↓ 前往 GitHub,深入了解 ODC 的更多细节