暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

OceanBase1.0 BIT/ENUM/SET数据类型支持(技术长文)

MySQL数据类型十分丰富且为弱类型,这就给兼容MySQL带来了很多困难,但是OBer没有放弃过100%兼容MySQL的目标。这次我们攻克的是BIT/ENUM/SET类型,这样我们就为业务端同学提供了更加丰富的类型选择。对于三种数据类型的介绍,可以参考MySQL文档:BIT,  ENUM, SET.

行为分析

  想要兼容MySQL首要任务就是调研清楚其行为表现,然而弄清其行为并非一件易事。MySQL中基本所有的DML和DDL均会使用数据类型,进一步说只要有表达式的地方就会有数据类型,而同一种数据类型在不同表达式中的表现又不同。在调研阶段,我们通常会采用下述几种方式:

  • 文档参考

  官网文档是MySQL最权威的指南,所有行为也都应该与其文档中描述的一致(在文档不出错的前提下)。

  • 脚本分析

  对于文档中没有明确说明的,我们则会自行编写测试脚本来分析MySQL的行为;在完成代码的编写后,这些测试脚本又可用于测试的回归,可谓一举两得。

  • 源码分析

  而对于通过测试用例仍旧无法归纳的行为,我们会进一步深入到MySQL的代码层面来分析问题。如果此时我们仍旧无法得到满意的结论,则会给MySQL报个BUG一探究竟。

举例说明 1

  这里我找一个简而不易的地方给大家介绍一下我们的调研过程:创建ENUM列时DEFAULT VALUE的指定。通过阅读MySQL文档,我们仅能得到一条相关信息:定义列时,如果COLUMN允许为NULL且DEFAULT VALUE没有指定,则DEFAULT VALUE为NULL值;如果COLUMN定义为NOT NULL且DEFAULT VALUE没有指定,则DEFAULT VALUE为MEMBERS中的第一个值。
  然而下面这些内容是文档中没有交代清楚的,需要我们自行调研:

  • 合法Literal

  这点我们不能武断的推测合法的Literal为字符串,因为前期的调研中我们已经发现ENUM类型在不同的上下文中既可以有Numeric表现又可以有String表现。通过脚本调研和MySQL的Parser分析,得到的结论是其合法值为String Literals,Hexadecimal Literals和Bit-Value Literals,其他类型的Literal均会报错。

mysql> create table t1(c1 set('a','b','c') default 0b1100001);
Query OK, 0 rows affected (0.01 sec)

mysql> create table t2(c1 set('a','b','c') default 0x61);
Query OK, 0 rows affected (0.01 sec)

mysql> create table t3(c1 set('a','b','c') default 'a');
Query OK, 0 rows affected (0.01 sec)
  • Charset & Collation

  DEFAULT VALUE的Literal可以单独设置其Charset和Collation,当其与COLUMN定义中的Charset和Collation不同时又会怎样呢? 这里以Collation为例进行说明。按照常规的理解,当COLUMN和DEFAULT VALUE的Collation冲突时,应该以COLUMN的为准。但我们测试MySQL时得到的结果却是这样的:

mysql> create table t1(c1 set('a','b','c') collate utf8mb4_bin default 'A' collate utf8mb4_general_ci);
Query OK, 0 rows affected (0.01 sec)

mysql> create table t2(c1 set('a','b','c') collate utf8mb4_general_ci default 'A' collate utf8mb4_bin);
ERROR 1067 (42000): Invalid default value for 'c1'

  带着迷惑与不解,我们给MySQL提了一个BUG,并成功的被确认。因此,这里我们将以COLUMN的Collation为准去校验DEFAULT VALUE。

  • NOT NULL

  文档中已经说明当COLUMN指定为NOT NULL且没有指定DEFAULT VALUE时,其DEAFULT VALUE为MEMBERS中的第一个值。然而我们测试的结果却有些不同 : 当使用default函数时,竟然报DEFAULT VALUE不存在。

mysql> create table def_enum(c1 enum('a','b','c') NOT NULL, c2 int default 99);
Query OK, 0 rows affected (0.01 sec)

mysql> insert into def_enum(c2)values(1);
Query OK, 1 row affected (0.00 sec)

mysql> insert into def_enum(c1, c2) values(default, 1);
Query OK, 1 row affected (0.00 sec)

mysql> insert into def_enum(c1, c2) values(default(c1), 1);
ERROR 1364 (HY000): Field 'c1' doesn't have a default value

  文档中对DEFAULT函数的描述为

Returns the default value for a table column. An error results if the column has no default value.

  然而上述的表现却与文档中的描述有出入,带着疑问给MySQL再次报了一个BUG,同样得到了确认。我们也将按照同一的行为来编写代码。

  • 合法值校验

  ENUM DEFAULT VALUE的一个特殊之处在于其需要检查DEFAULT VALUE是否为一个有效值,这个过程需要与MEMBER进行比较,这里又要考虑空串、结尾空格等情况。具体的行为不再赘述,其规则与向ENUM列插入数据时的判断基本类似,又略有不同。然而对于SET类型的检查又要复杂一些,因为其又涉及到SET MEMBER可能会重复的问题,最终通过MySQL的代码分析调研清了其行为表现。

举例说明 2

  第二个找一个MySQL问题比较严重的例子:Subquery输出列中含有ENUM类型时,其输出类型应该是什么?

  首先我们通过MySQL客户端的--column-type-info选项来分析简单查询的返回结果,由下面的结果可知,得到的是一个STRING TYPE。

mysql> create table t1(c1 enum('a','b','c'));
Query OK, 0 rows affected (0.02 sec)

mysql> insert into t1 values(1);
Query OK, 1 row affected (0.00 sec)

mysql> select * from t1;
Field   1:  `c1`Catalog:    `def`Database:   `test`Table:      `t1`Org_table:  `t1`
Type:       STRING//NOTE!!! string type
Collation:  utf8_general_ci (33)
Length:     3
Max_length: 1
Decimals:   0
Flags:      ENUM+------+| c1   |
+------+| a    |
+------+1 row in set (0.00 sec)

  但是在Subquery中又是怎样的表现呢?我们猜测其表现应该是STRING TYPE或ENUM TYPE。然而在实际的测试过程中,MySQL的表现并不一致:一些地方表现出的是STRING TYPE;一些地方表现出的是ENUM TYPE。详情可查看我们为MySQL报的BUG:#83454, #83452。也就是说,当前MySQL并没有将Subquery中Enum列的输出类型统一,显然这是无法接受的。

  对于OceanBase我们并不是毫无底线的去兼容MySQL,我们的原则是要保证正确性和一致性,对于上述问题就需要我们自己来决定其应该采用何种行为更加合理。摆在我们面前的有两个选择:ENUM在Subquery输出为STRING类型或ENUM类型。
  如果选择输出为String,这种方式最易于实现(后文会加以说明),然而却隐藏着正确性问题。例如存在两张表,其定义如下:

OceanBase (root@oceanbase)> create table t1(c1 enum('a','b','c'));Query OK, 0 rows affected (0.23 sec)

OceanBase (root@oceanbase)> create table t2(c1 int);
Query OK, 0 rows affected (0.18 sec)

  对于select * from t2 where c1 > any (select c1 from t1)在开启和关闭ob_enable_transformation系统变量时的执行计划并不相同:

改写模块开启

| ======================================================|ID|OPERATOR                  |NAME|EST. ROWS|COST   |
------------------------------------------------------|0 |NESTED-LOOP LEFT SEMI JOIN|    |1000     |1358256|
|1 | TABLE SCAN               |t2  |1000     |1356   ||2 | TABLE SCAN               |t1  |333      |1356   |
======================================================Outputs & filters:
-------------------------------------  0 - output([t2.c1]), filter(nil),      conds(nil), nl_params_([t2.c1])  1 - output([t2.c1]), filter(nil),      access([t2.c1]), partitions(p0)  2 - output([t1.c1]), filter([? > t1.c1]) // NOTE!!!      access([t1.c1]), partitions(p0) |

改写模块关闭

| =========================================|ID|OPERATOR      |NAME|EST. ROWS|COST  |
-----------------------------------------|0 |SUBPLAN FILTER|    |500      |679356|
|1 | TABLE SCAN   |t2  |500      |1356  ||2 | TABLE SCAN   |t1  |1000     |1356  |
=========================================Outputs & filters:
-------------------------------------  0 - output([t2.c1]), filter([t2.c1 > ANY(subquery(1))]),// NOTE!!!      exec_params_(nil), onetime_exprs_(nil), init_plan_idxs_([1])  1 - output([t2.c1]), filter(nil),      access([t2.c1]), partitions(p0)  2 - output([t1.c1]), filter(nil),      access([t1.c1]), partitions(p0)

  值得注意的是如果进行了改写filter为filter([? > t1.c1]),t1表的ENUM列将会使用ENUM类型产生比较;如果没有改写filter为filter([t2.c1 > ANY(subquery(1))]),若subquery的输出为String类型,也就是使用String TYpe与t2.c1进行比较,显而易见这样将导致在发生改写和不发生改写时,产生不同的结果。这样的结果肯定我们不能忍受的,因此我们这里约定Enum类型在Subquery中的输出仍旧我Enum类型。可是这又将会给实现带来一定的难度,这一部分留在后面的功能实现再进行分析。

MySQL相关BUG
  在调研MySQL BIT/ENUM/SET类型行为期间,我们共发现了8个MySQL相关BUG:#84055, #83859, #83454, #83452, #83312, #83031, #83146 ,#83320。MySQL的BUG肯定不仅如此,我们的目的也不是为了发现多少MySQL的问题,而是对外提供更加一致、更加合理的数据类型表现。

功能实现

  数据类型的实现会贯穿整个SQL引擎和存储引擎,这里我们也不赘述具体的实现细节,而是对此三种类型实现的特殊之处加以说明:

多态表现

  BIT/ENUM/SET类型的特殊之处在于其会根据表达式的上下文选择Numer Value或String Value参与运算。对于实现上述三种数据类型,我们的期望是不更改存储的代码,完全复用当前存储层的数据存储机制。在存储层我们使用已有的ObIntStoreType类型进行存储,这样就需要我们在SQL层来完成上述表达计算时的特殊行为。以下实现说明均以ENUM类型为例,SET与之类似,BIT有所不同。

  OceanBase的SQL引擎的Deduce Type流程用来分析表达式的result type和calc type,也就是说在表达式的分析阶段我们已经能够推断出该数据类型应该以什么的样值来参与运算;因此我们的做法是:如果在Deduce Type过程中推断出ENUM类型的calc type是String,我们会在ENUM Value外面套一个to_str表达式来完成Enum向String Value的转换。

  上面看似完美的解决方案在实际调研过程中又遇到了问题,因为有的表达式会需要一个类型的不同value表现形式。典型的如in 表达式:c1 in (1, 'a')。对于这个问题我们提出多个解决方案,最后选择了一个与当前框架最为契合的方式:在处理上述表达式时,我们会在ENUM Value外面套一个to_enum_inner的表达式,该表达式的结果为一个同时含有String Value和Numeric value的内部类型。上面的例子即变为to_enum_inner(c1) in (1, 'a')。这样在表达式计算时,可以根据相应的需求去取相应的值。

  上述的方法已经能够解决所有表达式计算的情况,但同样会带来两个问题:1.每一个表达式均需要确保其在deduce type流程中calc_type被设置且设置正确(以前的表达式计算框架中并没有强依赖这一点),因此我们就不得不对OceanBase当前的上百个函数进行逐个的梳理与校验;2.添加to_str或to_enum_inner表达式会导致计算时多走了一次表达式计算流程,或多或少会带来一些性能消耗,接下来的工作中我们会努力将这部分的消耗降为最低。

查询处理

  查询存在三种基本的形式:1.简单查询;2.子查询;3.集合操作。这三种情况ENUM列的输出类型并不相同。

  • 简单查询 :ENUM类型在简单查询中返回的结果为String Type;

  • 子查询 :MySQL对子查询处理存在一致性问题,OB中约定子查询中ENUM列输出仍旧为Enum类型。

  • 集合 :而对于集合操作,结果类型则与参与集合操作的类型息息相关。

  为了区分上面的三种情况,SQL引擎的RESOLVER模块会判断select_stmt所处的上下文。

  • 如果是简单查询,则会在target_list外面套一个to_str表达式,保证输出为String Type;

  • 如果是子查询,则需要进一步判断子查询所处的表达式上下文,判断出其所需要的类型,然后选择是否在target_list外套一层表达式(如果此处我们简单的规定子查询的输出为String Type,就可以避免表达式的处理流程,但是这样会引起不一致的问题);

  • 对于集合操作,首先需要推导出集合的结果类型,再根据结果类型决定是否对ENUM的targe_list做处理。

插入处理

  对于插入操作ENUM类型同样需要特殊处理,因为当输入为ENUM类型时我们需要考虑ENUM对外使用String Value还是Numer Value。下面举几个例子做简要说明。

  表定义如下:

create table t1(c1 int, c2 enum('a','b','c') default 'b', c3 set('e','f','g'));create table t2(c1 varchar(20));

  SQL1,2,3都是取ENUM的DEFAULT值插入到表中,然而由于DEFAULT的指定方式不同,代码中又将以不同的方式进行创建;由于使用的是DEFAULT VALUE,此时需要取ENUM的Numeric Value。

SQL1: insert into t1 (c1, c3)values(1,'e');
SQL2: insert into t1 values(1, default, 'e');
SQL3: insert into t1 values(1, default(c2), 'e');

  SQL4使用c2列的DEFAULT VALUE插入到c3列中,由于c3是SET类型,需要在第二个default函数外面在套一层to_str表达式完成ENUM到STRING的转换。

SQL4:insert into t1 values(1, default(c2), default(c2));

  SQL5使用c2列的当前值插入到c3列中,由于c3是SET类型,首先将c2转换为column_conv表达式来获取c2的value,然后在其外面套一个to_str表达式完成ENUM到STRING的转换。

SQL5:insert into t1 values(1, 'a', c2);

  SQL6将t1表的查询结果插入到t2表中,由于t2.c1是varchar,需要在select_stmt的target_list中套一个to_str表达式完成ENUM到STRING的转换。

SQL6:insert into t2 select c2 from t1;

  上面仅是对几种INSERT情况的简单描述,具体到细节又会涉及多种问题,这里就不一一赘述。

未来展望

  我们的目标100%兼容MySQL,然而并不是毫无原则的兼容,当遇到MySQL不合理的地方时我们会选择性的放弃兼容,正确性和一致性永远都是第一位的。对MySQL数据类型的兼容仍在继续,以后我们还将支持大对象和JSON等类型;我们将会提供更加合理、一致和稳定的数据类型体系,敬请期待。

「喜欢这篇文章,您的关注和赞赏是给作者最好的鼓励」
关注作者
【版权声明】本文为墨天轮用户原创内容,转载时必须标注文章的来源(墨天轮),文章链接,文章作者等基本信息,否则作者和墨天轮有权追究责任。如果您发现墨天轮中有涉嫌抄袭或者侵权的内容,欢迎发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论