需求编号 |
需求名称 |
特性描述 |
备注 |
1 |
兼容bit(n)数据类型 |
对于bit(n)类型,支持插入比n长度短的二进制字符串 |
openGauss当前必须插入n为二进制字符串,否则校验不通过报错 |
2 |
兼容tinyint(n)数据类型 |
兼容tinyint(n)类型,n无实际作用,忽略即可 |
当前openGauss的tinyint默认为无符号,而MySQL的tinyint默认为有符号,本次特性不对此进行修改 |
3 |
兼容smallint(n)数据类型 |
兼容smallint(n)类型,n无实际作用,忽略即可 |
|
4 |
兼容mediumint/mediumint(n)数据类型 |
内部使用int4实现,同时n无实际作用,忽略即可 |
MySQL的mediumint占3个字节,取值范围为-8388608~8388607,目前openGauss没有此种大小的整形,使用int4替代,实际取值范围比MySQL的更大 |
5 |
兼容int(n)数据类型 |
兼容int(n)类型,n无实际作用,忽略即可 |
|
6 |
兼容bigint(n)数据类型 |
兼容bigint(n)类型,n无实际作用,忽略即可 |
|
7 |
兼容char/varchar/nvarchar/text字符数据类型 |
对于字符串尾部的空格,存储时不截断,比较时会截断。即'a'='a '为真 |
openGauss中,对于字符串尾部的空格,存储时不截断,比较时也不截断 |
# 2.需求场景分析
## 2.1特性需求来源与价值概述
本特性需求只要兼容常用的MySQL数据类型,使得客户将MySQL数据库迁移至openGauss时,能更少的修改应用,更符合以前的使用习惯,降低客户从MySQL迁移到openGauss的成本。
## 2.2特性场景分析
用户在通过gsql等openGauss客户端/驱动连接openGauss后,在建表等语句中,可以使用本特性兼容的数据类型,且表现和MySQL 5.7基本一致。
## 2.3特性影响分析
### 2.3.1硬件限制
无
### 2.3.2 技术限制
本特性基于b_sql_plugin插件开发,如需使用相关特性,需要加载b_sql_plugin插件。
### 2.3.3 对License的影响分析
本特性的代码基于openGauss b_sql_plugin插件开发,特性代码没有参考MySQL,没有引入新的三方件,对License无影响。
### 2.3.4对系统性能规格的影响分析
1. 对于tinyint、smallint、mediumint、int、bigint等数据类型,只涉及建表语句时的兼容,对性能无影响。
2. 对于bit、字符数据类型,涉及建表、插入时的合法性校验、where/join过滤条件对比,预计在建表、插入时的合法性校验这两个场景性能无劣化。在where/join过滤条件对比字符数据类型时,由于需要先截断尾部的空格再对比,预计在部分场景下性能会差于原始场景。
### 2.3.5对系统可靠性规格的影响分析
不涉及
### 2.3.6对系统兼容性的影响分析
前向兼容分析:
1. bit(n),修改前,插入的数据长度必须为n,修改后,插入数据的长度可小于n。
2. tinyint(n)。无影响。
3. smallint(n)。无影响。
4. mediumint。原来mediumint不是关键字,改为数据类型后,mediumint将作为col_name_keyword(非保留关键字,但是不能是函数或类型)
5. int(n)。无影响。
6. bigint(n)。无影响。
8. char/varchar/nvarchar/text字符类型。主要影响join/where条件下过滤出来的数据,原来'a' = 'a '为false,兼容性后为true。
### 2.3.7与其他重大特性的交互性,冲突性的影响分析
无
## 2.4 同类社区/商用软件实现方案分析
无
# 3.特性/功能实现原理(可分解出来多个Use Case)
## 3.1目标
本特性的数据类型在建表、插入数据、join/where场景下,基本表现同MySQL 5.7.
## 3.2总体方案
对于tinyint(n)、smallint(n)、int(n)、bigint(n),仅需兼容(n),修改gram.y即可,在语法层兼容(n),但是对实际内容进行忽略,使xx(n)等价于xx。
对于mediumnint,新增mediumnint作为col_name_keyword类型关键字,并将其转换成int4数据类型。
对于字符数据类型的尾部空格字符截断问题,需要修改 <=、>=、=操作符涉及的内部函数,在对字符串进行比较前,去除尾部空格。
# 4.Use Case一实现
## 4.1设计思路
对于tinyint(n)、smallint(n)、int(n)、bigint(n),在gram.y中直接增加可选的opt_type_modifiers即可:
```
| SMALLINT opt_type_modifiers
{
$$ = SystemTypeName("int2");
$$->location = @1;
}
```
对于mediumnint,新增mediumnint作为col_name_keyword类型关键字,并将mediumint放到Numeric类型下,和其他数字类型放到一起。
```
PG_KEYWORD("mediumint", MEDIUMINT, COL_NAME_KEYWORD)
```
```
| MEDIUMINT opt_type_modifiers
{
$$ = SystemTypeName("int4");
$$->location = @1;
}
```
对于字符数据类型的尾部空格字符截断问题。目前openGauss对比两个字符数据类型是否相同是通过先判断两个字符串长度是否相等,然后再通过 memcmp 比较内容是否相同。根据这个原则,如果想在对比时截断尾部空格,只需要在计算字符串长度时,不将尾部的空格算入即可。
原算法:
```C
len1 = toast_raw_datum_size(arg1);
len2 = toast_raw_datum_size(arg2);
if (len1 != len2) {
result = false;
} else {
text* targ1 = DatumGetTextPP(arg1);
text* targ2 = DatumGetTextPP(arg2);
result = (memcmp(VARDATA_ANY(targ1), VARDATA_ANY(targ2), len1 - VARHDRSZ) == 0);
}
```
新算法:
```C
len1 = toast_raw_datum_size(arg1) - VARHDRSZ;
len2 = toast_raw_datum_size(arg2) - VARHDRSZ;
if (DB_IS_CMPT(B_FORMAT)) {
targ1 = DatumGetTextPP(arg1);
targ2 = DatumGetTextPP(arg2);
parg1 = VARDATA_ANY(targ1);
parg2 = VARDATA_ANY(targ2);
len1 = StrLenWithoutTailBlank(parg1, len1);
len2 = StrLenWithoutTailBlank(parg2, len2);
}
if (len1 != len2) {
result = false;
} else {
if (targ1 == NULL) {
targ1 = DatumGetTextPP(arg1);
parg1 = VARDATA_ANY(targ1);
}
if (targ2 == NULL) {
targ2 = DatumGetTextPP(arg2);
parg2 = VARDATA_ANY(targ2);
}
result = (memcmp(parg1, parg2, len1) == 0);
}
/* Calculate the length without tail blank */
static inline Size StrLenWithoutTailBlank(char *str, Size realLen)
{
Size len = realLen;
str = str + realLen - 1;
while (realLen > 0 && *str == ' ') {
len--;
str--;
}
return len;
}
```
需要注意是,相比于原来的text_cmp,对于性能最主要的影响是对比时需要先将text detoast。因为字符串的长度是可以通过toast_raw_datum_size方法直接获得,不需要将内容先detoast,只有当两个text长度相同时,才会将内容detoast并通过memcmp进行对比。
而兼容方案中,为了能在对比时忽略尾部空格,只能先将text detoast,并遍历尾部的内容以确定不含尾部空格时的字符长度,预计对于本身长度就不相等的字符串场景下,对比耗时将增加,因为多了detoast的耗时。
另外此修改同时影响其他使用text_cmp做对比的数据类型,包括char/varchar/varchar2/nvarchar2/text/clob。MySQL的尾部空格截断也同样适用于其他字符数据类型,包括char/varchar/nvarchar/text。可使用如下SQL在MySQL上测试:
```SQL
create table t_ch(a char(10), b varchar(10), c nvarchar(10), d text);
insert into t_ch values('a ','a ','a ','a ');
insert into t_ch values('a','a','a','a');
select * from t_ch where a='a ';
select * from t_ch where a='a ';
select * from t_ch where a='a';
```
性能测试对比:
```SQL
---建表
create table t1(a text);
---插入数据,55条,每条长200MB
insert into t1 values (lpad('a',209715200,'c'));
---执行查询
select * from t1 where a='a ';
```
对于原始openGauss,对比是否相等时先判断长度,无需将数据detoast,所以对于这种场景,查询速度很快:
```SQL
openGauss=# select length(a) from t1 limit 1;
length
-----------
209715200
(1 row)
Time: 1889.678 ms
openGauss=# select count(*) from t1;
count
-------
55
(1 row)
Time: 0.479 ms
openGauss=# select * from t1 where a='a ';
a
---
(0 rows)
Time: 0.361 ms
```
修改后,由于需要对每条数据都先detoast,查询速度明显降低:
```SQL
b_test=# select length(a) from t1 limit 1;
length
-----------
209715200
(1 row)
Time: 2459.771 ms
b_test=# select count(*) from t1;
count
-------
55
(1 row)
Time: 0.562 ms
b_test=# select * from t1 where a='a ';
a
---
(0 rows)
Time: 29416.276 ms
```
MySQL的情况:
```SQL
MySQL [pengjiong]> select length(a) from t1 limit 1;
+-----------+
| length(a) |
+-----------+
| 209715200 |
+-----------+
1 row in set (0.17 sec)
MySQL [pengjiong]> select count(*) from t1;
+----------+
| count(*) |
+----------+
| 55 |
+----------+
1 row in set (0.00 sec)
MySQL [pengjiong]> select * from t1 where a='a ';
Empty set (11.32 sec)
```
MySQL的查询情况比openGauss快,猜测应该是由于openGauss是将大的text压缩后存到toast中,而MySQL好像没有类似功能,所以对比时,openGauss多了一个detoast的消耗。
由于此种做法对于长字符串(特别是触发了toast存储的字符串)的比较会带来较大的性能损耗,需要判断是否有必要做这个兼容。
由于本身openGauss自带的压缩+toast功能,导致从toast获取数据+解压的操作比MySQL更耗时,所以在上述有toast+压缩的场景下,性能损耗很大。尝试测试下没有压缩+toast的场景,性能是否有劣化。
通过测试,发现当text的长度 > 2004 时,会触发压缩,如果压缩后的长度仍超过 2004,触发toast。(相关代码: heap_prepare_insert -> toast_insert_or_update)
测试在不需要压缩&toast的场景下,测试性能。用例如下:
```SQL
---建表
create table t1(a text);
---插入数据,插入102400条相同的数据
insert into t1 values (lpad('a',2004,'c'));
---查看表大小
---openGauss
b_test=# select pg_size_pretty(pg_table_size('t1'));
pg_size_pretty
----------------
200 MB
(1 row)
---MySQL
mysql> select concat(round(sum(DATA_LENGTH/1024/1024),2),'MB') as data from TABLES where table_name='t1';
+----------+
| data |
+----------+
| 229.78MB |
+----------+
1 row in set (0.02 sec)
```
使用 ```select * from t1 where a='a '; ``` 连续测试查询性能,耗时如下: