信创-TDSQL迁移GaussDB实战之数据迁移和表结构变换
在某公司的信创改造任务中,前期因为公司采购的TDSQL在实际的生产过程中存在一些性能问题,随着后期的采购转变为GaussDB(虽然非功能和性能问题也是有一些),因此在目前已经上线的项目中来对现有的TDSQL部分进行替代,在本次文章中介绍TDSQL迁移GaussDB的数据迁移和表结构变换部分。
GaussDB提供了ugo和drs两个工具,可以完成大部分的异构数据库之间的表结构变换和数据迁移任务,但是TDSQL这款封装了MySQL的产品因为其保护性目前暂不支持使用ugo和drs因此需要开发人员来采用其他方式完成任务。
下面部分将开始介绍TDSQL(MySQL)与GaussDB for PG的不同之处和数据迁移的方案。
1 数据类型
1.1 数值类型
1.1.1 整数类型
MySQL |
GaussDB PG |
||||||
名称 |
描述 |
存储空间 |
范围 |
名称 |
描述 |
存储空间 |
范围 |
BIT |
对于bit来说有bit(m),顾名思义m就是bit位数,当m为8的时候,就表示一个字节。不可以保存负数。 |
与m值有关 |
M指示每个值的位数,从1到64。默认为1,如果M被省略了。 |
|
|
|
|
BOOL |
对于Boolean来说,存储的值也就是0和1。在MySQL中已经将bool类型转换成了tinyint(1)。 |
1个字节 |
零的值被认为是假的。非零值被认为是真的。 |
|
|
|
|
TINYINT |
对于tinyint而言,在建表的时候如果使用tinyint(m)的格式,由于tinyint的大小为1字节们也就是m的最大值为255,如果超过255的话,则会报错Display width out of range for column `XXX` (max = 255)。
|
1个字节 |
对于无符号的数的存储范围是0~255,而对于有符号数的存储范围为-128~127。 |
TINYINT |
微整数,别名为 INT1。 |
1字节 |
0 ~ 255 |
SMALLINT |
Smallint可以存储2个字节,也就是16位的数据。 |
2个字节 |
对于无符号数的存储范围是0~65535,而对于有符号数的存储范围是-32768~32767。
|
SMALLINT |
小范围整数,别名为 INT2。 |
2字节 |
-32,768 ~ +32,767 |
MEDIUMINT |
Mediumint可以存储3个字节的数据,也就是24位的数据。 |
3个字节 |
可以存储无符号数的范围是0~16777216,而对于无符号数的存储范围是-8388608~8388607。 |
|
|
|
|
INT/INTEGER |
Int可以存储4个字节的数据,也就是32位数据. |
4个字节 |
可以存储无符号数据的范围为0~4294967295,而对无符号数的存储范围是-2147483648~2147483647。 |
INTEGER/BINARY_IN TEGER |
常用的整数,别名为 INT4。 |
4字节 |
-2,147,483,648 ~ +2,147,483,647 |
BIGINT |
Bigint可以存储8个字节的数据,也就是64位的数据。 |
8个字节 |
可以存储无符号数的范围是0~18446744073709551615,而对无符号数的存储范围是-9223372036854775808~9223372036854775807。已经很大了,在实际的工程应用中使用bigint的场景也是有很多。 |
BIGINT |
大范围的整数,别名 为INT8。 |
8字节 |
-9,223,372,036,854,775,808 ~ +9,223,372,036,854,775,807 |
|
|
|
|
int16 |
十六字节的大范围整 数,目前不支持用户 用于建表等使用。 |
16字节 |
-170,141,183,460,469,231,731,68 7,303,715,884,105,728 ~ +170,141,183,460,469,231,731,68 7,303,715,884,105,727 |
1.1.2 任意精度类型
MySQL |
GaussDB PG |
||||||
名称 |
描述 |
存储空间 |
范围 |
名称 |
描述 |
存储空间 |
范围 |
DECIMAL、NUMERIC |
Decimal(M,D)大小不确定,M表示指定长度,就是小数位数(精度)的总数,D表示小数点(标度)后的位数。如果D是0,则设置没有小数点或分数部分。M最大65,D最大30。如果D被省略,默认是0。如果M被省略,默认是10。 |
|
标准SQL要求DECIMAL(5,2)可以用五位数和两个小数来存储任何值,因此可以将这些值存储在salary列范 围-999.99到999.99 |
NUMERIC[ (p[,s])], DECIMAL[( p[,s])] |
精度p取值范围为 [1,1000],标度s取 值范围为[0,p]。 说明 p为总位数,s为小 数位数。 |
用户声明精度。每四 位(十进制位)占用 两个字节,然后在整 个数据上加上八个字 节的额外开销。 |
未指定精度的情况下, 小数点前最大131,072 位,小数点后最大 16,383位。 |
|
|
|
|
NUMBER[( p[,s])] |
NUMERIC类型的 别名。 |
用户声明精度。每四 位(十进制位)占用 两个字节,然后在整 个数据上加上八个字 节的额外开销。 |
未指定精度的情况下, 小数点前最大131,072 位,小数点后最大 16,383位。 |
1.1.3 序列整型
MySQL |
GaussDB PG |
||||||
名称 |
描述 |
存储空间 |
范围 |
名称 |
描述 |
存储空间 |
范围 |
AUTO_INCREMENT |
MySQL本身没有内建的序列类型,但可以使用AUTO_INCREMENT属性来模拟序列的行为。 |
|
|
|
|
|
|
|
|
|
|
SMALLSERIAL |
二字节序列整型 |
2字节 |
-32,768 ~ +32,767 |
|
|
|
|
SERIAL |
四字节序列整型 |
4字节 |
-2,147,483,648 ~ +2,147,483,647 |
|
|
|
|
BIGSERIAL |
八字节序列整型 |
8字节 |
-9,223,372,036,854,775,808 ~ +9,223,372,036,854,775,807 |
|
|
|
|
LARGESERIAL |
默认插入十六字节 序列整形,实际数 值类型和numeric 相同。 |
变长类 型,每四 位(十进 制位)占 用两个字 节,然后 在整个数 据上加上 八个字节 的额外开 销。 |
小数点前最大131,072位,小数 点后最大16,383位。 |
|
|
|
|
说明:SMALLSERIAL,SERIAL,BIGSERIAL和LARGESERIAL类型不是真正的类型,只是为在表中设置唯一标识做的概念上的便利。因此,创建一个整数字段,并且把它的缺省数值安排为从一个序列发生器读取。应用了一个NOT NULL约束以确保NULL不会被插入。在大多数情况下用户可能还希望附加一个UNIQUE或PRIMARY KEY约束避免意外地插入重复的数值,但这个不是自动的。最后,将序列发生器从属于那个字段,这样当该字段或表被删除的时候也一并删除它。目前只支持在创建表时候指定SERIAL列,不可以在已有的表中,增加SERIAL列。另外临时表也不支持创建SERIAL列。因为SERIAL不是真正的类型,也不可以将表中存在的列类型转化为SERIAL。 |
1.1.4 浮点类型
MySQL |
GaussDB PG |
||||||
名称 |
描述 |
存储空间 |
范围 |
名称 |
描述 |
存储空间 |
范围 |
FLOAT |
|
Float为单精度4个字节 |
可以保证6位精度 |
|
|
|
|
DOUBLE |
|
Double为双精度8个字节 |
可以保证16位精度 |
|
|
|
|
DECIMAL |
Decimal(M,D)大小不确定,M表示指定长度,就是小数位数(精度)的总数,D表示小数点(标度)后的位数。如果D是0,则设置没有小数点或分数部分。M最大65,D最大30。如果D被省略,默认是0。如果M被省略,默认是10。 |
|
|
|
|
|
|
|
|
|
|
REAL, FLOAT4 |
单精度浮点数,不 精准。 |
4字节 |
-3.402E+38~3.402E +38,6位十进制数字精度。 |
|
|
|
|
DOUBLE PRECISION , FLOAT8 |
双精度浮点数,不 精准。 |
8字节 |
-1.79E+308~1.79E +308,15位十进制数字 精度。 |
|
|
|
|
FLOAT[(p)] |
浮点数,不精准。 精度p取值范围为 [1,53]。 说明 p为精度,表示二进 制总位数。 |
4字节或8字节 |
根据精度p不同选择 REAL或DOUBLE PRECISION作为内部表 示。如不指定精度,内 部用DOUBLE PRECISION表示。 |
|
|
|
|
BINARY_D OUBLE |
是DOUBLE PRECISION的别 名。 |
8字节 |
-1.79E+308~1.79E +308,15位十进制数字 精度。 |
|
|
|
|
DEC[(p[,s])] |
精度p取值范围为 [1,1000],标度s取 值范围为[0,p]。 说明 p为总位数,s为小 数位位数。 |
用户声明精度。每四 位(十进制位)占用 两个字节,然后在整 个数据上加上八个字 节的额外开销。 |
未指定精度的情况下, 小数点前最大131,072 位,小数点后最大 16,383位。 |
|
|
|
|
INTEGER[(p[,s])] |
精度p取值范围为 [1,1000],标度s取 值范围为[0,p]。 |
用户声明精度。每四 位(十进制位)占用 两个字节,然后在整 个数据上加上八个字 节的额外开销。 |
|
说明:同样的数据4.125,由于我们选择的是(5,2),就会导致出现float和double的结果是4.12,而decimal的结果是4.13,也就是说float和double与decimal之间所采用的数据截断和进位的方式不同。这是因为float和double使用的是四舍六入五成双。对于四舍六入五成双,就是5以下舍弃5以上进位,如果需要处理数据为5的时候,需要看5后面是否还有不为0的任何数字,如果有,则直接进位,如果没有,需要看5前面的数字,如果是奇数则进位,如果是偶数则将5舍掉,也就是4.125取两位小数在float和double取2位小数的情况下的结果为4.12,而decimal是4.13。 |
|
|
|
|
1.2 字符类型
MySQL |
GaussDB PG |
||||
名称 |
描述 |
存储空间 |
名称 |
描述 |
存储空间 |
CHAR |
显示的是字符,固定长度字符串最长255字符。 |
|
CHAR(n) CHARACTER(n) NCHAR(n) |
定长字符串,不足补空 格。n是指字节长度,如 不带精度n,默认精度为 1。 |
最大为10MB。 |
VARCHAR |
显示的是字符,0~65535字节,可变长度字符串,最大65535字节。字符和字节有很大区别,对于字节而言,不同的编码方式字符占用的字节长度不同。Utf8编码最大21844字符,1~3个字节用于记录大小,对于utf8格式,3个字节表示1个字符,所以有(65535 - 3) / 3 = 21844。 |
|
VARCHAR(n) CHARACTER VARYING(n) |
变长字符串。PG兼容模式 下,n是字符长度。其他 兼容模式下,n是指字节 长度。 |
最大为10MB。 |
|
|
|
VARCHAR2(n) |
变长字符串。n是指字节长度。 是VARCHAR(n)类型的别名,为兼容Oracle类型特设。 存储类型VARCHAR。 |
最大为10MB。 |
|
|
|
NVARCHAR2(n) |
变长字符串。n是指字符 长度。 |
最大为10MB。 |
TINYBLOB |
在MySQL中,BLOB类型实际是个系统类型系列(TinyBlob、Blob、MediumBlob、LongBlob),除了在存储的最大信息量上不同外,他们是等同的。不过在MySQL中一般是不用Blob来存储大型对象的,可以使用MongoDB来进行代替。 |
最大255B |
|
|
|
BLOB |
|
最大65535B |
BLOB |
二进制大对象 目前BLOB支持的外部存取 接口仅为: DBE_LOB.GET_LENGTH DBE_LOB.READ DBE_LOB.WRITE DBE_LOB.WRITE_APPEND DBE_LOB.COPY DBE_LOB.ERASE 说明: 列存不支持BLOB类型 |
最大为32TB(即35184372088832字 节)。 |
MEDIUMBLOB |
|
最大 16777215B
|
|
|
|
LONGBLOB |
|
最大4294967295B |
|
|
|
TINYTEXT |
|
最大255B |
|
|
|
TEXT |
|
最大65535B |
TEXT |
变长字符串 |
最大为1GB-1,但还需要考虑到列描述头信息的大小,以及列所在元组的大小限制(也小于1GB-1),因此TEXT类型最大大小可能小于1GB-1 |
MEDIUMTEXT |
|
最大16777215B |
|
|
|
LONGTEXT |
|
最大4294967295B |
|
|
|
|
|
|
CLOB |
文本大对象。是TEXT类型 的别名。 |
最大为32TB-1,但还需要考虑到列描述头信息的大小,以及列所在元组的大小限制(也小于32TB-1),因此CLOB类型最大大小可能小于32TB-1。 |
|
|
|
RAW |
变长的十六进制类型。 说明: 列存不支持RAW类型, RAW(n),n是指字节长度建议值,不会用于校验输入raw类型的字节长度。 |
4字节加上实际的十六进制字符串。最 大为1GB-8203字节(即1073733621字 节)。 |
|
|
|
BYTEA |
变长的二进制字符串 |
4字节加上实际的二进制字符串。最大 为1GB-8203字节(即1073733621字 节)。 |
1.3 日期类型
MySQL |
GaussDB PG |
||||
名称 |
描述 |
存储空间 |
名称 |
描述 |
存储空间 |
DATE |
日期,yyyy-mm-dd |
|
DATE |
日期。 说明 A兼容性下,数据库将空字符串作为 NULL处理,数据类型DATE会被替换为 TIMESTAMP(0) WITHOUT TIME ZONE。 |
4字节(兼容模式A 下存储空间大小为8 字节) |
TIME |
时间,hh:mm:ss。
|
|
TIME [(p)] [WITHOUT TIME ZONE] |
只用于一日内时间。 p表示小数点后的精度,取值范围为 0~6。 |
8字节 |
|
|
|
TIME [(p)] [WITH TIME ZONE] |
只用于一日内时间,带时区。 p表示小数点后的精度,取值范围为 0~6。 |
12字节 |
DATETIME |
日期和时间的组合,yyyy-mm-dd hh:mm:ss。 |
8字节 |
|
|
|
|
|
|
SMALLDATETIME |
日期和时间,不带时区。 精确到分钟,秒位大于等于30秒进一 位。 |
8字节 |
TIMESTAMP |
Yyyymmddhhmmss。 |
4字节 |
TIMESTAMP[(p)] [WITHOUT TIME ZONE] |
日期和时间。 p表示小数点后的精度,取值范围为 0~6。 |
8字节 |
|
|
|
TIMESTAMP[(p)] [WITH TIME ZONE] |
日期和时间,带时区。TIMESTAMP 的别名为TIMESTAMPTZ。 p表示小数点后的精度,取值范围为 0~6。 |
8字节 |
|
|
|
INTERVAL DAY (l) TO SECOND (p) |
时间间隔,X天X小时X分X秒。 l:天数的精度,取值范围为 0~6。兼容性考虑,目前未实现具 体功能。 p:秒数的精度,取值范围为 0~6。小数末尾的零不显示。 |
16字节 |
|
|
|
INTERVAL [FIELDS] [ (p) ] |
时间间隔。 fields:可以是YEAR,MONTH, DAY,HOUR,MINUTE, SECOND,DAY TO HOUR,DAY TO MINUTE,DAY TO SECOND,HOUR TO MINUTE, HOUR TO SECOND,MINUTE TO SECOND。 p:秒数的精度,取值范围为 0~6,且fields为SECOND,DAY TO SECOND,HOUR TO SECOND或MINUTE TO SECOND 时,参数p才有效。小数末尾的零 不显示。 |
12字节 |
|
|
|
reltime |
相对时间间隔。格式为: X years X mons X days XX:XX:XX。 采用儒略历计时,规定一年为365.25 天,一个月为30天,计算输入值对应 的相对时间间隔。 |
4字节 |
|
|
|
abstime |
日期和时间。格式为: YYYY-MM-DD hh:mm:ss+timezone 取值范围为1901-12-13 20:45:53 GMT~2038-01-18 23:59:59 GMT, 精度为秒。 |
4字节 |
YEAR |
年份yyyy。 |
|
|
|
|
说明:Datatime用8个字节来保存数据,其取值范围为1000-01-01 00:00:00 ~ 9999-12-31 23:59:59。其跟时区无关。Timestamp用4个字节来保存数据,其取值范围为1970-01-01 00:00:00 UTC ~ 2038-01-19 03:14:07,timestamp和时区有关。优先推荐使用datatime类型保存日期和时间,可以保存的时间范围更大一些。注意事项:在给时间字段设置默认值时,建议不要设置成0000-00-00 00:00:00,不然查询表时可能会因为转换不了,而直接报错。 |
|
|
2 数据库操作
2.1 DDL
2.1.1 建库建表
1)MySQL:
CREATE DATABASE [IF NOT EXISTS] db_name [create_specification[,create_specification]…]
create_specification:
1-1)CHARACTER_SET:
指定数据库采用的字符集,如果不能指定字符集,默认是utf8。
1-2)COLLATE:
指定数据库字符集的校对规则,常用的是utf8_bin(区分大小写)和utf8_general_ci(不区分大小写),注意默认使用的是utf8_general_ci。
虽然默认的是utf8,但是在实际的工程应用中都会建议使用utf8mb4。在所占字节数上utf8占3个字节,utf8mb4占4个字节。显然utf8mb4是要比utf8能存储更多的信息。
在MySQL中的utf8实际上不是真正的UTF-8。因为MySQL中的utf8编码只支持每个字符最多三个字节,而真正的UTF-8是每个字符最多四个字节。在MySQL的utf8编码中,中文是3个字节,其他数据、英文和符号占1个字节。
但是emoji符号占4个字节,一些较为复杂的文字、繁体字也是4个字节。所以会导致以上这些写入失败,因此需要改为utf8mb4。MySQL的utf8mb4才是真正的UTF-8。MySQL的utf8是一种专属的编码,它能够编码的Unicode字符并不多。
所有在使用utf8的MySQL和MariaDB用户都应该改用utf8mb4,不要再使用utf8。
1-3)创建数据表:
创建数据表的语句如下:
CREATE TABLE IF NOT EXISTS table_name( Filed1 datatype, Filed2 datatype, Filed3 datatype ) character set 字符集 collate 校对规则 engine 存储引擎
Filed:指定列名
Datatype:指定列类型(字段类型)
Character set:如不指定则为数据库字符集
Collate:如不指定则为所在数据库校对规则
Engine:存储引擎,前面大篇幅内容介绍过的,默认使用InnoDB,还可以选择MyISAM和Memory等等。
当然对于一个实际的建表任务而言,还会有AUTO_INCREMENT、ROW_FORMAT等参数来控制表的构建。
2)GaussDB PG:
建库:
CREATE DATABASE database_name [ [ WITH ] { [ OWNER [=] user_name ] | [ TEMPLATE [=] template ] | [ ENCODING [=] encoding ] | [ LC_COLLATE [=] lc_collate ] | [ LC_CTYPE [=] lc_ctype ] | [ DBCOMPATIBILITY [=] compatibilty_type ] | [ TABLESPACE [=] tablespace_name ] | [ CONNECTION LIMIT [=] connlimit ] | [ DBTIMEZONE [=] time_zone ]}[...] ];
2-1)database_name:
数据库名称。
取值范围:字符串,要符合标识符的命名规范。
2-2)OWNER [ = ] user_name:
数据库所有者。缺省时,新数据库的所有者是当前用户。
取值范围:已存在的用户名。
2-3)TEMPLATE [ = ] template:
模板名。即从哪个模板创建新数据库。GaussDB采用从模板数据库复制的方式来
创建新的数据库。初始时,GaussDB包含两个模板数据库template0、
template1,以及一个默认的用户数据库postgres。
取值范围:仅template0。
2-4)ENCODING [ = ] encoding:
指定数据库使用的字符编码,可以是字符串(如'SQL_ASCII')、整数编号。
不指定时,默认使用模版数据库的编码。模板数据库template0和template1的编
码默认与操作系统环境相关。template1不允许修改字符编码,因此若要变更编
码,请使用template0创建数据库。
常用取值:GBK、UTF8、Latin1、GB18030等。
2-5)LC_COLLATE [ = ] lc_collate:
指定新数据库使用的字符集。例如,通过lc_collate = 'zh_CN.gbk'设定该参数。
该参数的使用会影响到对字符串的排序顺序(如使用ORDER BY执行,以及在文本
列上使用索引的顺序)。默认是使用模板数据库的排序顺序。
取值范围:操作系统支持的字符集。
注:
ENCODING、LC_COLLATE和LC_CTYPE可以根据如下命令查询:
select pg_encoding_to_char(collencoding) as encoding,collname,collcollate,collctype from pg_collation;
2-6)LC_CTYPE [ = ] lc_ctype:
指定新数据库使用的字符分类。例如,通过lc_ctype = 'zh_CN.gbk'设定该参数。
该参数的使用会影响到字符的分类,如大写、小写和数字。默认是使用模板数据
库的字符分类。
取值范围:操作系统支持的字符分类。
2-7)DBCOMPATIBILITY [ = ] compatibility_type:
指定兼容的数据库的类型,默认兼容O。
取值范围:A、B、C、PG。分别表示兼容O、MY、TD和POSTGRES。
A兼容性下,数据库将空字符串作为NULL处理,数据类型DATE会被替换为
TIMESTAMP(0) WITHOUT TIME ZONE。
将字符串转换成整数类型时,如果输入不合法,B兼容性会将输入转换为0,而其它兼
容性则会报错。
PG兼容性下,CHAR和VARCHAR以字符为计数单位,其它兼容性以字节为计数单位。
例如,对于UTF-8字符集,CHAR(3)在PG兼容性下能存放3个中文字符,而在其它兼容
性下只能存放1个中文字符。
2-8)TABLESPACE [ = ] tablespace_name:
指定数据库对应的表空间。
取值范围:已存在表空间名。
2-9)CONNECTION LIMIT [ = ] connlimit
数据库可以接受的并发连接数。
2-10)特别说明:
在GaussDB for PG中,Encoding 是编码规则,collate是基于这个编码规则中对于字符的排序,而 CTYPE 是什么,Ctype是针对字符的大小写比对起作用的配置。
可以在设计索引的时候,指定不同的字符集。
在默认的情况下建表:
可以创建不同字符集的索引:
C.UTF-8 是一种语言环境设置,主要用于确保程序在处理文本时使用 UTF-8 编码。它与其他语言环境(如 en_US.UTF-8 和 zh_CN.UTF-8)有一些区别。
语言环境的区别:
C.UTF-8 是 POSIX 标准兼容的默认语言环境,严格支持 ASCII 字符,并扩展支持基本的 UTF-8。
en_US.UTF-8 是美式英语的 UTF-8 语言环境,支持英文及其他拉丁字母语种。
zh_CN.UTF-8 是中文的 UTF-8 语言环境,主要支持汉字、拼音、符号以及少量英文单词。
使用场景:
C.UTF-8 适用于需要严格遵循 POSIX 标准的场景,通常用于服务器端。
en_US.UTF-8 适用于以英语为主的应用程序和系统。
zh_CN.UTF-8 适用于中文应用程序和系统,显示中文更为兼容。
GaussDB数据库设计规范:
禁止使用postgres数据库,必须为业务创建Database。
创建Database必须设置字符集编码(ENCODING)、兼容性(DBCOMPATIBILITY)。
ENCODING:设置为UTF8;
DBCOMPATIBILITY:需配置为兼容Oracle语法,集中式版本需将参数配置为A,分布式版本需将参数配置为ORA,参考建库语句如下:
版本 |
参考建库语句 |
集中式 |
CREATE DATABASE test2 ENCODING='UTF8' lc_collate='C' lc_ctype='C' DBCOMPATIBILITY='A' TEMPLATE template0; |
分布式 |
CREATE DATABASE test2 ENCODING='UTF8' DBCOMPATIBILITY='ORA' TEMPLATE template0; |
建议一个数据库实例内应用自定义的Database数量不超过3个。
建议使用Schema进行业务隔离。
建表:
数据表设计规范
必须使用行存及ASTORE引擎;
建表语句参考:
CREATE TABLE tbl_name .... WITH (orientation=row,storage_type = astore);
必须定义Range分区表的上边界为Maxvalue。
删除、切割、合并、清空、交换分区的操作会使分区Global索引失效,建议申明UPDATE GLOBAL INDEX子句更新索引。
说明:UPDATE GLOBAL INDEX的更新策略为异步更新,可以避免在分区操作并UPDATE GLOBAL INDEX时,产生无法使用索引的问题。
对于ASTORE和USTORE:
Ustore 存储引擎,又名In-place Update存储引擎(原地更新),是openGauss 内核新增的一种存储模式。此前的版本使用的行存储引擎是Append (追加更新)模式。相比于Append Update(追加更新)行存储引擎,Ustore存储引擎可以提高数据页面内更新的HOT UPDATE的垃圾回收效率,有更新元组后存储空间占用的问题。Append Update和 In-place Update是两种不同的存储引擎策略,适用场景有所不同。
2.1.2 索引
1)MYSQL:
单张表的索引尽量控制在5个以内
单个索引的字段原则上不超过5个
表索引总长度不超过3072字节(系统限制)
组合索引的顺序要斟酌,以下需综合考虑
常用的筛选条件字段放在前面
选择性(基数)高的字段放在前面
尽可能地使用数据原生顺序从而避免额外的排序操作,需要经常排序的字段可加到索引中,字段顺序和最常用的排序一致
对于较长的字符串类型字段,建议使用前缀索引
合理合并索引,避免冗余,例如:
组合索引的最左前缀部分,不再创建相同的索引,比如(a,b)和(a),应该去掉(a)
主键字段(a),不再创建(a,b)这样的组合索引
创建索引:
索引的类型:
键索引:主键自动的为主索引(类型primary key)。
唯一索引:UNIQUE。
普通索引:INDEX。
全文索引:FULLTEXT,适用于MyISAM存储引擎。对于全文索引在实际开发中的考虑,要使用Solr或ElasticSearch。
添加索引:
CREATE [UNIQUE] INDEX index_name ON tbl_name (col_name[(length)][ASC|DESC],…);
或
ALTER TABLE tbl_name ADD INDEX [index_name] (index_col_name,…);
2)GaussDB:
合理设计组合索引,避免冗余。
禁止使用外键。
禁止索引总字符串长度超过200(单个索引、组合索引)。
关于NULL约束:
明确不存在NULL 值的字段必须添加NOT NULL 约束
用于WHERE 条件过滤和关联的字段都建议设置NOT NULL 约束
索引(包括单列索引和复合索引)字段建议添加NOT NULL约束
建议给数据表建立业务主键。
说明:使用DRS进行数据迁移或数据同步时,目标数据表上必须要有主键。
创建索引时,如果有联机事务,必须添加CONCURRENTLY参数。
说明:创建索引时,一般会阻塞其他DML对该索引所依赖表的访问,添加CONCURRENTLY参数将不阻塞其他DML操作。
在表上创建索引:
CREATE [ UNIQUE ] INDEX [ CONCURRENTLY ] [ [schema_name.]index_name ] ON table_name [ USING method ] ({ { column_name [ ( length ) ] | ( expression ) } [ COLLATE collation ] [ opclass ] [ ASC | DESC ] [ NULLS { FIRST | LAST } ] }[, ...] ) [ INCLUDE ( column_name [, ...] )] [ WITH ( {storage_parameter = value} [, ... ] ) ] [ TABLESPACE tablespace_name ] [ WHERE predicate ];
在分区表上创建索引:
CREATE [ UNIQUE ] INDEX [ CONCURRENTLY ] [ [schema_name.]index_name ] ON table_name [ USING method ] ( {{ column_name [ ( length ) ] | ( expression ) } [ COLLATE collation ] [ opclass ] [ ASC | DESC ] [ NULLS LAST ] }[, ...] ) [ LOCAL [ ( { PARTITION index_partition_name | SUBPARTITION index_subpartition_name [ TABLESPACE index_partition_tablespace ] } [, ...] ) ] | GLOBAL ] [ INCLUDE ( column_name [, ...] )] [ WITH ( { storage_parameter = value } [, ...] ) ] [ TABLESPACE tablespace_name ] [ WHERE predicate ];
重点参数说明:
UNIQUE:
创建唯一性索引,每次添加数据时检测表中是否有重复值。如果插入或更新的值
会引起重复的记录时,将导致一个错误。
目前只有B-tree及UBtree索引支持唯一索引。
CONCURRENTLY:
以不阻塞DML的方式创建索引(加ShareUpdateExclusiveLock锁)。创建索引
时,一般会阻塞其他语句对该索引所依赖表的访问。指定此关键字,可以实现创
建过程中不阻塞DML。
此选项只能指定一个索引的名称。
普通CREATE INDEX命令可以在事务内执行,但是CREATE INDEX
CONCURRENTLY不可以在事务内执行。
列存表不支持CONCURRENTLY方式创建索引。对于临时表,支持使用
CONCURRENTLY关键字创建索引,但是实际创建过程中,采用的是阻塞式的
创建方式,因为没有其他会话会并发访问临时表,并且阻塞式创建成本更
低。
创建索引时指定此关键字,Astore需要执行先后两次对该表的全表扫描来完成build,
第一次扫描的时候创建索引,不阻塞读写操作;第二次扫描的时候合并更新第一次扫描
到目前为止发生的变更;Ustore只需全表扫描一次来完成索引创建。
2.1.3 分区
1)MySQL:
建议只使用RANGE、LIST和HASH最基本的分区方式;分区字段的类型请参照下面的介绍。分区表支持的分区类型:
1-1)RANGE
CREATE TABLE tbl_name .... PARTITION BY RANGE(column) ( PARTITION partition_name1 VALUES LESS THAN (value), …, PARTITION partition_nameN VALUES LESS THAN MAXVALUE)
语法说明:
支持指定一列的值进行RANGE分区。
分区键须为主键的一部分
建议分区键的类型为整型或时间类型
使用时间进行RANGE分区,均支持年、月、日函数。
注:若增加分区,需要人工干预。
实际操作:
创建分区表:
DROP TABLE if exists orders_1; CREATE TABLE orders_1 ( id INT PRIMARY KEY, order_date CHAR(4) , customer_id INT, product_name VARCHAR(255) ) PARTITION BY RANGE (order_date) ( PARTITION p1 VALUES LESS THAN (2021), PARTITION p2 VALUES LESS THAN (2022), PARTITION p3 VALUES LESS THAN (2023), PARTITION P4 VALUES LESS THAN(MAXVALUE));
查询分区信息:
select relname,parttype,parentid,boundaries from pg_partition where parentid in (SELECT parentid FROM pg_partition where relname='orders_1');
修改分区表:
ALTER TABLE orders_1 DROP PARTITION P4; ALTER TABLE orders_1 DROP PARTITION P5; ALTER TABLE orders_1 ADD PARTITION P4 VALUES LESS THAN (2024); ALTER TABLE orders_1 ADD PARTITION P6 VALUES LESS THAN (2025); ALTER TABLE orders_1 ADD PARTITION P4 VALUES LESS THAN (2024); ALTER TABLE orders_1 ADD PARTITION P6 VALUES LESS THAN (2025); ALTER TABLE orders_1 ADD PARTITION P10 VALUES LESS THAN (MAXVALUE); ALTER TABLE orders_1 RENAME PARTITION P10 TO P5;
注意:
GaussDB for PG中不存在类似MySQL的REORGANIZE PARTITION语法用于重新组织表的分区,需要先删除旧分区,然后添加一个新的分区,覆盖旧分区的范围。
1-2)LIST
CREATE TABLE tbl_name .... PARTITION BY LIST(column) ( PARTITION partition_name1 VALUES IN (value)或 PARTITION partition_name1 VALUES (value), …)
语法说明:
只支持按指定一列的值进行LIST分区。
分区键须为主键的一部分
建议分区键的类型为整型或时间类型
原因:MySQL、集中式TDSQL、分布式TDSQL单表如果采用PARTITION BY LIST,那么VALUES中的值只支持整型。如果希望支持字符类型,需要使用PARTITION BY LIST COLUMNS,而Oracle不兼容该语法,因此将该语法作为TDSQL独有语法,不建议使用。
采用LIST分区,程序需要保证不允许有LIST分区之外的值插入。
1-3)HASH
CREATE TABLE tbl_name .... PARTITION BY HASH(column) PARTITIONS num
语法说明:
只支持按指定一列的值进行HASH分区
分区键需要是主键的一部分
支持整型和时间类型(支持年、月、日函数)
分布式TDSQL中的分表不支持HASH二级分区
2)GaussDB:
GaussDB 分布式range、list分布表主键要求必须包含分布列。
当表中的数据量很大时,应当对表进行分区,一般需要遵循以下原则:
【建议】使用具有明显区间性的字段进行分区,比如日期、区域等字段上建立分区。
【建议】分区名称应当体现分区的数据特征。例如,关键字+区间特征。
【建议】将分区上边界的分区值定义为MAXVALUE,以防止可能出现的数据溢出。
分区方式 |
描述 |
Range |
表数据通过范围进行分区。 |
Interval |
表数据通过范围进行分区,超出范围的会自动根据间隔创建新的分区。 |
List |
表数据通过指定列按照具体值进行分区。 |
Hash |
表数据通过Hash散列方式进行分区。 |
典型的分区表定义如下:
创建Range分区表:
CREATE TABLE staffS_p1 ( staff_ID NUMBER(6) not null, FIRST_NAME VARCHAR2(20), LAST_NAME VARCHAR2(25), EMAIL VARCHAR2(25), PHONE_NUMBER VARCHAR2(20), HIRE_DATE DATE, employment_ID VARCHAR2(10), SALARY NUMBER(8,2), COMMISSION_PCT NUMBER(4,2), MANAGER_ID NUMBER(6), section_ID NUMBER(4) ) PARTITION BY RANGE (HIRE_DATE) ( PARTITION HIRE_19950501 VALUES LESS THAN ('1995-05-01 00:00:00'), PARTITION HIRE_19950502 VALUES LESS THAN ('1995-05-02 00:00:00'), PARTITION HIRE_maxvalue VALUES LESS THAN (MAXVALUE) );
创建Interval分区表,初始两个分区,插入分区范围外的数据会自动新增分区:
CREATE TABLE sales (prod_id NUMBER(6), cust_id NUMBER, time_id DATE, channel_id CHAR(1), promo_id NUMBER(6), quantity_sold NUMBER(3), amount_sold NUMBER(10,2) ) PARTITION BY RANGE (time_id) INTERVAL('1 day') ( PARTITION p1 VALUES LESS THAN ('2019-02-01 00:00:00'), PARTITION p2 VALUES LESS THAN ('2019-02-02 00:00:00') );
创建List分区表:
CREATE TABLE test_list (col1 int, col2 int) partition by list(col1) ( partition p1 values (2000), partition p2 values (3000), partition p3 values (4000), partition p4 values (5000) );
创建Hash分区表:
CREATE TABLE test_hash (col1 int, col2 int) partition by hash(col1) ( partition p1, partition p2 );
2.1.4 修改表
1)MySQL:
修改列的类型时优先使用MODIFY关键字,示例:
ALTER TABLE test_table MODIFY a VARCHAR (11);
TDSQL对表的修改绝大部分操作都会重构数据,可能对业务造成影响。为减少这种影响,必须把对表的多次ALTER操作合并为一次操作。
例如,要给表t增加一个字段b,同时给已有的字段aa建立索引, 通常的做法分为两步:
ALTER TABLE tt ADD COLUMN b VARCHAR(10); ALTER TABLE tt ADD INDEX idx_aa(aa);
正确的做法:
ALTER TABLE tt ADD COLUMN b VARCHAR(10),ADD INDEX idx_aa(aa);
2)Gauss DB:
修改表,包括修改表的定义、重命名表、重命名表中指定的列、重命名表的约束、设
置表的所属模式、添加/更新多个列、打开/关闭行访问控制开关。
修改表的定义:
ALTER TABLE [ IF EXISTS ] { table_name [*] | ONLY table_name | ONLY ( table_name ) } action [, ... ];
其中具体表操作action可以是以下子句之一:
column_clause | ADD table_constraint [ NOT VALID ] | ADD table_constraint_using_index | VALIDATE CONSTRAINT constraint_name | DROP CONSTRAINT [ IF EXISTS ] constraint_name [ RESTRICT | CASCADE ] | CLUSTER ON index_name
等。
2.2 DML
2.2.1 SELECT
1)MySQL:
1. 查询非重复项使用DISTINCT,禁用QNIQUE
2. 尽量避免子查询
3. 聚合操作
1) 先缩小范围再做聚合等操作,以减少CPU和IO
2) 尽量避免使用类似COUNT(*)的全表扫描查询,从设计上考虑另外用一张表存这个计数值
4. 注意字段类型,避免类型转换,类型转换除了会增加CPU消耗,如果转换失败,还会导致索引失效
5. 尽量避免让数据库做算术运算,交给应用层来做,例如:
SELECT a FROM tbl WHERE id*10=100;
6. 尽量使用UNION ALL,而不是UNION,UNION会做去重和排序
7. WHERE子句使用的原则
1) 尽量使用索引,尤其是主键,尽量简单,尽量匹配到更少的行
2) 即使有索引,WHERE子句匹配的行数也不要超过表的30%,否则效率仍很低
3) 多使用等值操作符,少使用非等值操作符,非等值操作符通常会导致索引失效
8. OR条件大于3个:
1) 不同字段的,使用UNION ALL代替
2) 相同字段的,用IN代替
9. 尽量使用WHERE子句代替HAVING子句,例如:
SELECT id,COUNT(*) FROM tbl GROUP BY id HAVING age>=30;
应该替换为:
SELECT id,COUNT(*) FROM tbl WHERE age>30 GROUP BY id;
10. 一个表的ORDER BY和GROUP BY的组合都不应该超过3种,否则从业务逻辑考虑进行优化或者分成多张表
11. 表关联
1) 数据分库分表后尽量避免跨库表关联
2) 对于大表,建议不多于2张表做关联;对于小表(如配置信息类的)容许超过2张表以上的关联
3) 关联列上要求必须有索引
JOIN/LEFT JOIN/RIGHT JOIN,选择满足应用场景的关联类型
2)Gauss DB:
1.带有LIMIT的分页查询语句中必须带有ORDER BY 保证有序。
2.避免对大字段(如VARCHAR(2000))执行ORDER BY、DISTINCT、GROUP BY、UNION等会引起排序的操作。此类操作将消耗大量的CPU和内存资源,执行效率低下。
3.ORDER BY子句进行排序时,建议显式指定排序方式(ASC/DESC),NULL的排序方式(NULL FIRST/NULL LAST)。
4.禁止使用LOCK TABLE 语句加锁。
说明:LOCK TABLE提供多种锁级别,但如果对数据库原理和业务理解不足,误用表锁可能触发死锁,导致集群不可用。
5.不建议超过3张表或视图进行关联(特别是full join)。
6.建议使用连接操作符“||”替换concat函数进行字符串连接。
说明:concat函数的输出跟data type有关,生成执行计划时不能提前计算结果值,导致查询性能严重劣化。
7.建议使用CURRENT_DATE、CURRENT_TIME、CURRENT_TIMESTAMP(n)代替now函数获取当前时间。
说明:now函数生成的执行计划无法下推,导致查询性能严重劣化。
8.当in(val1, val2, val3…)表达式中字段较多时,建议使用in (values(va11), (val2),(val3)…)语句进行替换。
说明:优化器会自动把in约束转换为非关联子查询,从而提升查询性能。
9.若join列上的NULL值较多,则可以加上is not null过滤条件,以提前过滤数据,提高join效率。
10.多表关联查询时, 建议对表添加使用别名,保证语句逻辑清晰,便于维护。
11.子查询深度不建议超过2层。
2.2.2 INSERT
1)MySQL:
1. 多条INSERT语句要合并成一条批量提交,建议一次不要超过1000行数据
2. 禁止使用INSERT INTO tbl(),必须显示指明字段,示例:
INSERT INTO tbl_name(a,b,c) VALUES(1,2,3);
2)Gauss DB:
1.在批量数据入库之后,或者数据增量达到一定阈值后,建议对表进行analyze操作,防止统计信息不准确而导致的执行计划劣化。
2.INSERT ON DUPLICATE KEY UPDATE 不支持对主键或唯一约束的列上执行UPDATE。
说明:INSERT ON DUPLICATE KEY UPDATE的语义是对唯一约束冲突的行进行更新,这个过程中不应对约束的值进行更新。
3.不建议对存在多个唯一约束的表执行INSERT ON DUPLICATE KEY UPDATE。
说明:当存在多个唯一约束时,会默认检查所有的唯一约束条件,只要任何一个约束存在冲突,就会对冲突行进行更新,即可能更新多条记录,与业务预期不相符。业务应给予更加明确的插入更新条件。
4.对于批量插入的情况,建议使用executeBatch 执行INSERT INTO VALUES (?),执行效率将高于执行多条INSERT INTO VALUES(?)或INSERT INTO VALUES(?),...,(?)。
5.批量插入数据量不宜过大,建议单次批量数据条数不超过1000。
2.2.3 UPDATE
1)MySQL:
1. 必须带WHERE条件,最好使用主键
2. 控制一次性更新的数据记录,建议在1万条记录以内,以避免引起事务持有锁的时间过长,甚至可能因为单个事务的binlog太大导致强同步阻塞
3. 不使用不确定值的函数,比如RAND ()和NOW()
4. 禁止在UPDATE语句中,将“,”写成AND,例如:
UPDATE tbl SET fid=fid+1000, gid=gid+1000 WHERE id > 2;
如果写成
UPDATE tbl SET fid=fid+1000 AND gid=gid+1000 WHERE id > 2;
此时“fid+1000 AND gid=gid+1000”将作为值赋给fid,且没有Warning。
2)GaussDB:
1.不建议UPDATE 语句中直接使用LIMIT,应使用WHERE 条件明确需要更新的目标行。
2.禁止在UPDATE 更新多个列时,被更新列同样是更新源。
说明:同时更新多列,且更新源相同,在不同的数据库下行为不同,避免带来兼容性问题。
UPDATE table SET col1 = col2, col3 = col1 WHERE col1 = 1;
该语句在Oracle中,col3的值为原col1的值;而MySQL中,col3的值为col2的值(因为col2的值被赋予给了col1)。
3.UPDATE 语句中禁止使用ORDER BY、GROUP BY 子句,避免不必要的排序。
4.有主键/索引的表,更新时WHERE条件应结合主键/索引。
5.对于频繁更新的表,建议应用使用VACUUM进行清理。
2.2.4 DELETE
1)MySQL:
1. 必须带WHERE条件,最好使用主键
2. 控制一次性删除的数据记录,建议在1万条记录以内,以避免引起事务持有锁的时间过长,甚至可能因为单个事务的binlog太大导致强同步阻塞
3. 不使用不确定值的函数,比如RAND ()和NOW()
2)GaussDB:
1.不建议DELETE 语句中使用LIMIT。应使用WHERE条件明确需要删除的目标行。
2.DELETE 有主键或索引的表,WHERE 条件应结合主键或索引,提高执行效率。
3.DELETE语句中禁止使用ORDER BY、GROUP BY子句,避免不必要的排序。
2.2.5 WHERE
1)MySQL:
2)Gauss DB:
1.关于NULL
1)、查询条件中与NULL做比较时,必须使用IS NULL或IS NOT NULL
2)、在关联字段不存在NULL值的情况下,使用(not) exist代替(not) in
2.查询条件中不建议对索引字段使用“!= ”比较符,避免索引失效。
3.查询条件的索引字段上不建议NOT IN。
4.如果过滤条件只有OR表达式,建议将OR表达式转化为UNION ALL以提升性能。使用OR的SQL语句经常无法优化,导致执行速度慢。
5.exists/not exists子查询中含大表的查询条件时,建议增加/*+ no_expand*/提示
说明:优化器可能会自动将exists/not exists子查询展开,一旦子查询中含有大表,优化器则无法找到最优执行计划,从而变成慢SQL
6.禁止在SELECT 目标列中使用子查询,子查询作为过滤条件会使索引失效,从而导致全表扫描。
如:select (select ...),t2.c2 from t2 WHERE ...
3 其他注意点
3.1 MySQL和GaussDB不兼容语法处理建议
以下是在GaussDB PG和MySQL之间的一些主要的SQL语法不兼容性。这个列表包含了一些主要的不同之处,但并不是全部的不兼容性。在进行迁移之前,建议深入对比和测试以确保兼容性。
MySQL |
GaussDB |
备注 |
AUTO_INCREMENT |
SERIAL |
序列生成器 |
SHOW TABLES |
\dt |
查看所有表 |
SHOW DATABASES |
\l |
查看所有数据库 |
LIMIT n OFFSET m |
LIMIT n OFFSET m 或 FETCH FIRST n ROWS ONLY OFFSET m |
分页查询 |
DESCRIBE table or DESC table |
\d table |
描述表结构 |
CONCAT(str1, str2, ...) |
str1 || str2 |
拼接字符串 |
SUBSTRING(str, pos, len) |
SUBSTRING(str FROM pos FOR len) |
子字符串 |
LENGTH(str) |
LENGTH(str)或CHAR_LENGTH(str) |
字符串长度 |
CURDATE() |
CURRENT_DATE |
当前日期 |
NOW() |
CURRENT_TIMESTAMP |
当前事件 |
RAND() |
RANDOM() |
生成随机数 |
IFNULL(exp, replace_exp) |
COALESCE(exp, replace_exp) |
判断表达式是否为空 |
ISNULL(exp) |
exp IS NULL |
判断表达式是否为空 |
`作为标识符引用符 |
“作为标识符引用符 |
标识符引用符 |
存储过程使用BEGIN和END |
存储过程使用$$ |
存储过程定义 |
TRUNCATE TABLE不可在事务中 |
TRUNCATE TABLE可在事务中 |
清空表数据 |
REPALCE INTO |
INSERT ... ON CONFLICT DO UPDATE |
插入冲突更新 |
REGEXP |
~ |
正则表达式搜索 |
CREATE DATABASE 不支持所有者 |
CREATE TABLE支持所有者 |
创建数据库 |
ENUM类型 |
无对应,可用CHECK约束代替 |
类型不一致 |
SET类型 |
无对应,可用ARRAY类型代替 |
类型不一致 |
YEAR类型 |
无对应,可用INTERVAL YEAR或DATE类型代替 |
类型不一致 |
UNSIGNED类型 |
无对应,需注意数值范围 |
类型不一致 |
ZEROFILL类型 |
无对应,需在应用层处理 |
类型不一致 |
SHOW CREATE TABLE |
pg_dump -t table -s |
查看表创建语句 |
ENGINE = InnoDB |
无对应 |
存储引擎 |
CHARSET = utf8mb4 |
ENCODING = UTF8 |
字符集 |
COLLATE = utf8mb4_bin |
COLLATE “C” |
排序规则 |
分区表 |
分区表 |
分区定义语法不同 |
CREATE USER |
CREATE ROLE |
创建用户 |
REVOKE ALL PRIVILEGES |
REVOKE ALL |
撤销权限 |
GRANT SELECT ON *.* |
GRANT SELECT ON ALL TABLES IN SCHEMA |
授予权限 |
/*!50003 CREATE */ |
CREATE OR REPLACE |
创建或替换 |
LOCK TABLES |
LOCK TABLE |
锁表 |
UNLOCK TABLES |
COMMIT |
解锁 |
KILL QUERY |
CANCEL |
取消查询 |
SHOW PROCESSLIST |
SELECT * FROM pg_stat_activity |
查看进程列表 |
LOAD DATA INFILE |
COPY |
数据导入 |
SELECT INTO OUTFILE |
COPY TO |
数据导出 |
RENAME COLUMN |
ALTER COLUMN RENAME |
重命名列 |
RENAME INDEX |
ALTER INDEX RENAME |
重命名索引 |
RENAME DATABASE |
无对应 |
重命名数据库 |
ALTER DATABASE不支持修改所有者 |
ALTER DATABASE支持修改所有者 |
修改数据库所有者 |
ALTER DATABASE支持修改字符集和排序规则 |
ALTER DATABASE 不支持修改字符集和排序规则 |
修改数据库字符集和排序规则 |
ALTER TABLE支持一次修改多个列 |
ALTER TABLE一次只能修改一个列 |
修改表列 |
ALTER TABLE支持一次添加多个索引 |
ALTER TABLE一次只能添加一个索引 |
添加表索引 |
ALTER TABLE支持一次删除多个索引 |
ALTER TABLE一次只能删除一个索引 |
删除表索引 |
ALTER TABLE支持修改存储引擎 |
ALTER TABLE不支持修改存储引擎 |
修改表存储引擎 |
ALTER TABLE支持修改字符集和排序规则 |
ALTER TABLE不支持修改字符集和排序规则 |
修改表字符集和排序规则 |
ALTER TABLE支持修改自增值 |
ALTER TABLE不支持修改自增值 |
修改表自增值 |
3.2 语法差异
描述 |
MySQL |
GaussDB PG |
插入并存在时更新 |
REPLACE INTO或INSERT ... ON DUPLICATE KEY UPDATE ... |
INSERT...ON CONFLICT DO UPDATE... |
使用LIMIT进行分页查询 |
SELECT ... LIMIT offset,count |
SELECT ... LIMIT count OFFSET offset |
单行插入返回插入的ID |
INSERT INTO ...;SELECT LAST_INSERT_ID() |
INSERT INTO ... RETURNING id |
单行更新返回更新的行 |
UPDATE...;SELECT...; |
UPDATE ... RETURNING *; |
单行删除返回删除的行 |
DELETE...;SELECT...; |
DELETE...RETURNING*; |
随机获取一行记录 |
SELECT...ORDER BY RAND() LIMIT 1; |
SELECT...ORDER BY RANDOM() LIMIT 1; |
索引字段查询优化 |
SELECT * FROM table USE INDEX(index) WHERE column = value; |
SET enable_seqscan TO OFF;SELECT * FROM table WHERE column = value;SET enable_seqscan TO ON; |
使用全文索引搜索 |
MATCH(column) AGAINST(‘text’) |
to_tsvector(column) @@ to_tsquery(‘text’) |
查询特定范围的行 |
SELECT * FROM table LIMIT x,y |
SELECT * FROM table LIMIT y OFFSET x |
使用存储过程 |
CALL procedure_name() |
SELECT * FROM procedure_name() |
事务处理 |
START TRANSACTION; COMMIT; ROLLBACK; |
BEGIN; COMMIT; ROLLBACK; |
创建临时表 |
CREATE TEMPORARY TABLE table_name |
CREATE TEMP TABLE table_name |
MySQL使用反引号(``)引用表名和列名 |
SELECT column FROM table |
SELECT “column” FROM “table” |
时间和日期函数 |
DATE(),NOW(),YEAR(),MONTH(), DAY(),HOUR(),MINUTE(), SECOND() |
CURRENT_DATE, CURRENT_TIME, DATE_PART(‘year’, column), DATE_PAR column), DATE_PART(‘day’, column), DATE_PART(‘hour’, column), DATE_PART column), DATE_PART(‘second’, column) |
字符串连接函数 |
CONCAT(column1, column2) |
column1 || column2 |
对分组的限制 |
SELECT … FROM … GROUP BY … WITH ROLLUP |
SELECT … FROM … GROUP BY … ROLLUP(…) |
对NULL的处理 |
SELECT IFNULL(column, 0) FROM table |
SELECT COALESCE(column, 0) FROM table |
判断是否为空 |
SELECT column IS NULL FROM table |
SELECT column IS NULL FROM table |
日期加减操作 |
SELECT DATE_ADD(date, INTERVAL 1 DAY) FROM table |
SELECT date + INTERVAL ‘1 day’ FROM table |
创建自增主键 |
CREATE TABLE table(id INT AUTO_INCREMENT, PRIMARY KEY(id)) |
CREATE TABLE table(id SERIAL PRIMARY KEY) |
使用正则表达式匹配数据 |
SELECT column REGEXP ‘pattern’ FROM table |
SELECT column ~ ‘pattern’ FROM table |
计算平均值 |
SELECT AVG(column) FROM table |
SELECT AVG(column) FROM table |
计算最大值 |
SELECT MAX(column) FROM table |
SELECT MAX(column) FROM table |
计算最小值 |
SELECT MIN(column) FROM table |
SELECT MIN(column) FROM table |
计算总和 |
SELECT SUM(column) FROM table |
SELECT SUM(column) FROM table |
计算记录数 |
SELECT COUNT(column) FROM table |
SELECT COUNT(column) FROM table |
使用内置数学函数 |
SELECT ABS(column), CEIL(column), FLOOR(column), ROUND(column) FROM table |
SELECT ABS(column), CEILING(column), FLOOR(column), ROUND(column) FROM table |
使用内置字符串函数 |
SELECT CHAR_LENGTH(column), LOWER(column), UPPER(column), REVERSE(column) FROM table |
SELECT LENGTH(column), LOWER(column), UPPER(column), REVERSE(column) FROM table |
使用内置日期函数 |
SELECT CURRENT_DATE(), CURRENT_TIME(), DATE_FORMAT(date, ‘format’) FROM table |
SELECT CURRENT_DATE, CURRENT_TIME, TO_CHAR(date, ‘format’) FROM table |
使用内置转换函数 |
SELECT CAST(column AS type) FROM table |
SELECT CAST(column AS type) FROM table |
4 数据迁移案例
4.1 登陆GaussDB服务器
登陆服务器,在开发环境登陆XXX.XXX.XXX.XXX这个高斯节点。
例如使用MobaXterm。打开软件,连接该节点XXX.XXX.XXX.XXX。
输入密码进行登录。
然后,输入命令cd /opt/bin。
然后运行脚本,输入命令source gsql_env.sh。
然后登录数据库,输入命令gsql -h XXX.XXX.XXX.XXX -d postgres -U root -p 8000 -W password。就可以进行相关的操作了。
4.2 创建数据库
在gaussdb目录下执行CREATE DATABASE XXXX TEMPLATE template0 ENCODING='UTF8' DBCOMPATIBILITY='A';来创建XXXX数据库。
然后在gaussdb目录下执行\l,来查看建好的数据库信息。
4.3 建表
执行命令\c XXXX来选择使用XXXX库,输入密码可以登录。
然后进行建表操作,执行如下样例建表语句。
CREATE TABLE IF NOT EXISTS tctatomtype ( atom_type char(8) NOT NULL, ap_code char(4) DEFAULT NULL, at_type_name varchar(256) DEFAULT NULL, at_type_desc varchar(1024) DEFAULT NULL, at_type_param1 varchar(64) DEFAULT NULL, at_type_param2 varchar(64) DEFAULT NULL, at_type_param3 varchar(64) DEFAULT NULL, at_type_param4 varchar(64) DEFAULT NULL, at_type_param5 varchar(64) DEFAULT NULL, at_type_param6 varchar(64) DEFAULT NULL, at_type_param7 varchar(64) DEFAULT NULL, at_type_param8 varchar(64) DEFAULT NULL, at_type_param9 varchar(64) DEFAULT NULL, at_type_param10 varchar(64) DEFAULT NULL, PRIMARY KEY(atom_type) ) WITH (orientation=row,storage_type = USTORE);
建表完成之后,在XXXX目录下执行\dt 来查看数据库里的表信息。
还可以在XXXX目录下执行\d tctatomtype来查看tctatomtype的建表信息。
4.4 使用DataX进行迁移
4.4.1 安装DataX
配置示例:从stream读取数据并打印到控制台
1)创建配置文件(json格式):
通过命令查看配置模板:python datax.py -r {YOUR_READER} -w {YOUR_WRITER}
例如:python datax.py -r streamreader -w streamwriter
得到配置模板:
根据模板配置来配置脚本:
{ "job": { "content": [ { "reader": { "name": "streamreader", "parameter": { "sliceRecordCount": 10, "column": [ { "type": "long", "value": "10" }, { "type": "string", "value": "hello, 你好,世界-DataX" } ] } }, "writer": { "name": "streamwriter", "parameter": { "encoding": "UTF-8", "print": true } } } ], "setting": { "speed": { "channel": "5" } } } }
将该脚本上传至/XXXX/datax/job下,并执行chmod +x stream2stream.json,赋予执行权限。
2)启动DataX:
在/XXXX/datax/bin目录下来执行如下命令:
python datax.py /xxxx/datax/job/stream2stream.json
可以看到运行结果:
可以看到完成数据迁移。
4.4.2 DataX内部类型与MySQL和GaussDB的关系
DataX数据类型 |
MySQL表数据类型 |
GaussDB For PG表数据类型 |
long |
int,tinyint,smallint,mediumint,int,bigint,year |
bigint,bigserial,integer,smallint,serial |
double |
float,double,decimal |
double precision,money, numeric,real |
string |
nvarchar,char,tinytext,text,mediumtext,longtext |
nvarchar,char,text,bit,inet |
boolean |
bit,bool |
bool |
date |
Date,datetime,timestamp,time |
date,time,timestamp |
bytes |
tinyblob,mediumblob,blob,longblob,varbinary |
bytea |
4.4.3 创建校验数据表
创建一个check_data表来对数据量做校验来对迁移时间做记录。
CREATE TABLE IF NOT EXISTS check_data ( table_name VARCHAR(200) NOT NULL, mysql_num BIGINT DEFAULT NULL, pgsql_num BIGINT DEFAULT NULL, start_time TIMESTAMP DEFAULT NULL, end_time TIMESTAMP DEFAULT NULL, sql_state VARCHAR(4) DEFAULT NULL, PRIMARY KEY(table_name) ) WITH (orientation=row,storage_type = USTORE);
4.4.4 实际数据迁移测试
1)表结构信息:
选择XXXX里的某张配置表来进行数据迁移实验。选择控制交易类型的TCTATOMTYPE表来进行测试。
在TDSQL中,查看TCTATOMTYPE表的建表:
CREATE TABLE `tctatomtype` ( `atom_type` char(2) COLLATE utf8_bin NOT NULL, `ap_code` char(1) COLLATE utf8_bin DEFAULT NULL, `at_type_name` varchar(64) COLLATE utf8_bin DEFAULT NULL, `at_type_desc` varchar(256) COLLATE utf8_bin DEFAULT NULL, `at_type_param1` varchar(16) COLLATE utf8_bin DEFAULT NULL, `at_type_param2` varchar(16) COLLATE utf8_bin DEFAULT NULL, `at_type_param3` varchar(16) COLLATE utf8_bin DEFAULT NULL, `at_type_param4` varchar(16) COLLATE utf8_bin DEFAULT NULL, `at_type_param5` varchar(16) COLLATE utf8_bin DEFAULT NULL, `at_type_param6` varchar(16) COLLATE utf8_bin DEFAULT NULL, `at_type_param7` varchar(16) COLLATE utf8_bin DEFAULT NULL, `at_type_param8` varchar(16) COLLATE utf8_bin DEFAULT NULL, `at_type_param9` varchar(16) COLLATE utf8_bin DEFAULT NULL, `at_type_param10` varchar(16) COLLATE utf8_bin DEFAULT NULL, PRIMARY KEY (`atom_type`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin;
对应的在GaussDB中的建表:
CREATE TABLE IF NOT EXISTS tctatomtype ( atom_type char(8) NOT NULL, ap_code char(4) DEFAULT NULL, at_type_name varchar(256) DEFAULT NULL, at_type_desc varchar(1024) DEFAULT NULL, at_type_param1 varchar(64) DEFAULT NULL, at_type_param2 varchar(64) DEFAULT NULL, at_type_param3 varchar(64) DEFAULT NULL, at_type_param4 varchar(64) DEFAULT NULL, at_type_param5 varchar(64) DEFAULT NULL, at_type_param6 varchar(64) DEFAULT NULL, at_type_param7 varchar(64) DEFAULT NULL, at_type_param8 varchar(64) DEFAULT NULL, at_type_param9 varchar(64) DEFAULT NULL, at_type_param10 varchar(64) DEFAULT NULL, PRIMARY KEY(atom_type) ) WITH (orientation=row,storage_type = USTORE);
共计14个字段。
2)迁移脚本准备:
在/XXXX/datax/bin目录下执行命令python datax.py -r mysqlreader -w postgresqlwriter。
得到TDSQL迁移GaussDB的模板。
{ "job": { "content": [ { "reader": { "name": "mysqlreader", "parameter": { "column": [], "connection": [ { "jdbcUrl": [], "table": [] } ], "password": "", "username": "", "where": "" } }, "writer": { "name": "postgresqlwriter", "parameter": { "column": [], "connection": [ { "jdbcUrl": "", "table": [] } ], "password": "", "postSql": [], "preSql": [], "username": "" } } } ], "setting": { "speed": { "channel": "" } } } }
3)准备校验数据表:
在GaussDB中出创建一张表来记录数据迁移的信息和两个数据库的数据校验。
CREATE TABLE IF NOT EXISTS check_data ( table_name VARCHAR(200) NOT NULL, mysql_num BIGINT DEFAULT NULL, start_time TIMESTAMPTZ[6] DEFAULT NULL, end_time TIMESTAMPTZ[6] DEFAULT NULL, sql_state VARCHAR(4) DEFAULT NULL, PRIMARY KEY(table_name) ) WITH (orientation=row,storage_type = USTORE);
4)填写迁移脚本:
按照要迁移的数据库和数据表信息来完成迁移脚本的填写。
命名为mysql2postgresql.json。
{ "job": { "content": [ { "reader": { "name": "mysqlreader", "parameter": { "column": ["atom_type","ap_code","at_type_name","at_type_desc","at_type_param1", "at_type_param2","at_type_param3","at_type_param4","at_type_param5", "at_type_param6","at_type_param7","at_type_param8","at_type_param9", "at_type_param10"], "connection": [ { "jdbcUrl": ["jdbc:mysql://XXXX/XXXX"], "table": ["tctatomtype"] } ], "password": "XXXXXXXX", "username": "XXXX" } }, "writer": { "name": "postgresqlwriter", "parameter": { "column": ["atom_type","ap_code","at_type_name","at_type_desc","at_type_param1", "at_type_param2","at_type_param3","at_type_param4","at_type_param5", "at_type_param6","at_type_param7","at_type_param8","at_type_param9", "at_type_param10"], "connection": [ { "jdbcUrl": "jdbc:postgresql://XXXXX/XXXX", "table": ["tctatomtype"] } ], "password": "XXXXX", "postSql": ["update check_data set pgsql_num = (select count(1) from tctatomtype),end_time = current_timestamp,sql_state = '2' where table_name = 'tctatomtype'"], "preSql": ["update check_data set start_time = current_timestamp,sql_state = '1' where table_name = 'tctatomtype'"], "username": "root" } } } ], "setting": { "speed": { "channel": "1" } } } }
然后上传至/XXXX/datax/job路径下。
5)执行迁移数据脚本:
在/XXXX/datax/bin路径下执行命令:python datax.py /XXXX/datax/job/mysql2postgresql.json
但是可以看到会有报错,如下所示:
错误信息为:org.postgresql.util.PSQLException: The authentication type 10 is not supported. Check that you have configured the pg_hba.conf file to include the client's IP address or subnet, and that it is using an authentication scheme supported by the driver.]
这个报错的原因是postgresql驱动版本低不匹配,需要更新驱动,但是由于是使用GaussDB,因此不能使用原生的postgresql驱动来进行替换升级。登陆GaussDB官网,下载dws_8.1.x_jdbc_driver.zip这个压缩包,解压后找到gsjdbc4.jar,来替换/baas/datax/plugin/writer/postgresqlwriter/libs中的postgresql-9.3-1102-jdbc4.jar。
最终结果如下:
替换前:
共计14个字段。
2)迁移脚本准备:
在/XXXX/datax/bin目录下执行命令python datax.py -r mysqlreader -w postgresqlwriter。
得到TDSQL迁移GaussDB的模板。
{ "job": { "content": [ { "reader": { "name": "mysqlreader", "parameter": { "column": [], "connection": [ { "jdbcUrl": [], "table": [] } ], "password": "", "username": "", "where": "" } }, "writer": { "name": "postgresqlwriter", "parameter": { "column": [], "connection": [ { "jdbcUrl": "", "table": [] } ], "password": "", "postSql": [], "preSql": [], "username": "" } } } ], "setting": { "speed": { "channel": "" } } }
3)准备校验数据表:
在GaussDB中出创建一张表来记录数据迁移的信息和两个数据库的数据校验。
CREATE TABLE IF NOT EXISTS check_data ( table_name VARCHAR(200) NOT NULL, mysql_num BIGINT DEFAULT NULL, start_time TIMESTAMPTZ[6] DEFAULT NULL, end_time TIMESTAMPTZ[6] DEFAULT NULL, sql_state VARCHAR(4) DEFAULT NULL, PRIMARY KEY(table_name) ) WITH (orientation=row,storage_type = USTORE);
4)填写迁移脚本:
按照要迁移的数据库和数据表信息来完成迁移脚本的填写。
命名为mysql2postgresql.json。
{ "job": { "content": [ { "reader": { "name": "mysqlreader", "parameter": { "column": ["atom_type","ap_code","at_type_name","at_type_desc","at_type_param1", "at_type_param2","at_type_param3","at_type_param4","at_type_param5", "at_type_param6","at_type_param7","at_type_param8","at_type_param9", "at_type_param10"], "connection": [ { "jdbcUrl": ["jdbc:mysql://XXXX/XXXX"], "table": ["tctatomtype"] } ], "password": "XXXX", "username": "XXXX" } }, "writer": { "name": "postgresqlwriter", "parameter": { "column": ["atom_type","ap_code","at_type_name","at_type_desc","at_type_param1", "at_type_param2","at_type_param3","at_type_param4","at_type_param5", "at_type_param6","at_type_param7","at_type_param8","at_type_param9", "at_type_param10"], "connection": [ { "jdbcUrl": "jdbc:postgresql://XXXX/XXXX", "table": ["tctatomtype"] } ], "password": "XXXX", "postSql": ["update check_data set pgsql_num = (select count(1) from tctatomtype),end_time = current_timestamp,sql_state = '2' where table_name = 'tctatomtype'"], "preSql": ["update check_data set start_time = current_timestamp,sql_state = '1' where table_name = 'tctatomtype'"], "username": "root" } } } ], "setting": { "speed": { "channel": "1" } } } }
然后上传至/XXXX/datax/job路径下。
5)执行迁移数据脚本:
在/xxxx/datax/bin路径下执行命令:python datax.py /xxxx/datax/job/mysql2postgresql.json
但是可以看到会有报错,如下所示:
错误信息为:org.postgresql.util.PSQLException: The authentication type 10 is not supported. Check that you have configured the pg_hba.conf file to include the client's IP address or subnet, and that it is using an authentication scheme supported by the driver.]
这个报错的原因是postgresql驱动版本低不匹配,需要更新驱动,但是由于是使用GaussDB,因此不能使用原生的postgresql驱动来进行替换升级。登陆GaussDB官网,下载dws_8.1.x_jdbc_driver.zip这个压缩包,解压后找到gsjdbc4.jar,来替换/baas/datax/plugin/writer/postgresqlwriter/libs中的postgresql-9.3-1102-jdbc4.jar。
最终结果如下:
替换前:
替换后:
然后再在/XXXX/datax/bin路径下执行命令:python datax.py /XXXX/datax/job/mysql2postgresql.json,可以看到数据完成迁移。
数据查看:
check_data表,TDSQL和GaussDB数据量一致:
校对后迁移结果与在TDSQL上的表一致。
4.4.5数据迁移前准备
在check_data表中有table_name和mysql_num字段,需要在数据迁移之前来填充。
这块也使用DataX来完成,执行的脚本为check_data.json。
具体如下:
{ "job": { "setting": { "speed": { "channel": 1 } }, "content": [{ "reader": { "name": "mysqlreader", "parameter": { "username": "baas", "password": "XXXX", "connection": [ { "querySql": [ "select 'tctatomtype' as table_name,count(1) as mysql_num from tctatomtype"], "jdbcUrl": [ "jdbc:mysql://XXXX/XXXX" ] } ] } }, "writer": { "name": "postgresqlwriter", "parameter": { "column": ["table_name","mysql_num"], "connection": [{ "jdbcUrl": "jdbc:postgresql://XXXX/XXXX", "table": ["check_data"] }], "preSql":["delete from check_data"], "password": "XXXX", "username": "root" } } }] } }
在/XXXX/datax/bin目录下执行python datax.py /XXXX/datax/job/check_data.json,迁移成功之后可以看到:
5 数据迁移实战
本次数据迁移工作是从TDSQL(MySQL)迁移到GaussDB for PG,总共704张表。
5.1 DDL
5.1.1字段
TDSQL中使用的字段和一些关键字与GaussDB for PG的对应关系:
序号 |
TDSQL字段或关键字 |
GaussDB for PG相应替代 |
备注 |
1 |
bigint |
bigint |
GaussDB for PG中bigint为有符号数,存储范围为-9,223,372,036,854,775,808 ~ +9,223,372,036,854,775,807。 TDSQL中bigint可以存储无符号数的范围是0~18446744073709551615,而对有符号数的存储范围是-9223372036854775808~9223372036854775807。 |
2 |
varchar |
varchar |
GaussDB for PG中varchar为字节,TDSQL中varchar为字符。 需要进行相应的换算。 |
3 |
timestamp |
timestamp |
GaussDB for PG中,timestamp类型用于存储带有日期和时间部分的值,其范围是公元0001年01月01日 00:00:00.000000至公元9999年12月31日 23:59:59.999999 TDSQL中timestamp存储时间范围为1970-01-01 00:00:00 UTC ~ 2038-01-19 03:14:07。 建议对TDSQL中的timestamp进行替换。 |
4 |
longblob |
blob |
GaussDB for PG中,blob最大为32TB(即35184372088832字节)。 TDSQL中,longblob最大4294967295B,约4GB。 GaussDB for PG的blob可以包含TDSQL的longblob。 |
5 |
tinyint |
tinyint |
GaussDB for PG中tinyint为无符号数,存储范围为0 ~ 255。 TDSQL中tinyint对于无符号的数的存储范围是0~255,而对于有符号数的存储范围为-128~127。 |
6 |
int |
integer |
GaussDB for PG中integer为有符号数,存储范围为-2,147,483,648 ~ +2,147,483,647。 TDSQL中tinyint可以存储无符号数据的范围为0~4294967295,而对无符号数的存储范围是-2147483648~2147483647。 需要判断GaussDB for PG中integer作为有符号数的存储范围是否够,如果不够需要换bigint。 |
7 |
datetime |
timestamp |
GaussDB for PG中timestamp类型用于存储带有日期和时间部分的值,其范围是公元0001年01月01日 00:00:00.000000至公元9999年12月31日 23:59:59.999999。 TDSQL中datetime取值范围为1000-01-01 00:00:00 ~ 9999-12-31 23:59:59。其跟时区无关。 |
8 |
double |
dec[(p[,s])]或double precision , float8 |
GaussDB for PG中DEC[(p[,s])]精度p取值范围为 [1,1000],标度s取值范围为[0,p]。说明:p为总位数,s为小数位位数。未指定精度的情况下, 小数点前最大131,072位,小数点后最大16,383位。 TDSQL中double为双精度8个字节,可以保证16位精度。Double类型不精确,double使用的是四舍六入五成双,可能会产生一些问题。 但是如果不确定使用DEC是否正确,使用DOUBLE PRECISION,FLOAT8,但是GaussDB for PG中 DOUBLE PRECISION,FLOAT8与TDSQL中的double不相同,不存在四舍六入五成双。 |
9 |
char |
char(n) |
GaussDB for PG中char(n)定长字符串,不足补空 格。n是指字节长度,如不带精度n,默认精度为 1。最大为10MB。 TDSQL中char显示的是字符,固定长度字符串最长255字符。 |
10 |
decimal |
numeric[(p[,s])], decimal[(p[,s])] |
GaussDB for PG中numeric精度p取值范围为 [1,1000],标度s取值范围为[0,p]。说明:p为总位数,s为小数位数。 TDSQL中decimal(M,D)大小不确定,M表示指定长度,就是小数位数(精度)的总数,D表示小数点(标度)后的位数。如果D是0,则设置没有小数点或分数部分。M最大65,D最大30。如果D被省略,默认是0。如果M被省略,默认是10。 |
11 |
blob |
blob |
GaussDB for PG中,blob最大为32TB(即35184372088832字节)。 TDSQL中,blob最大最大65535B。 |
12 |
mediumtext |
text |
GaussDB for PG中,text最大为1GB-1,但还需要考虑到列描述头信息的大小,以及列所在元组的大小限制(也小于1GB-1),因此TEXT类型最大大小可能小于1GB-1。 TDSQL中mediumtext最大65535B。 |
13 |
longtext |
clob |
GaussDB for PG中,clob最大为32TB-1,但还需要考虑到列描述头信息的大小,以及列所在元组的大小限制(也小于32TB-1),因此CLOB类型最大大小可能小于32TB-1。 TDSQL中longtext最大4294967295B。 |
14 |
date |
date或timestamp without time zone |
GaussDB for PG中,A兼容性下,数据库将空字符串作为NULL处理,数据类型DATE会被替换为 TIMESTAMP(0) WITHOUT TIME ZONE。 TDSQL中格式为日期,yyyy-mm-dd。 如果要将TDSQL中的date字段值插入到GaussDB For PG中,会存储为 yyyy-mm-dd 00:00:00的格式,在使用的时候,需要做substring(dt, 0, 11)截断处理。 |
15 |
text |
text |
GaussDB for PG中,text最大为1GB-1,但还需要考虑到列描述头信息的大小,以及列所在元组的大小限制(也小于1GB-1),因此TEXT类型最大大小可能小于1GB-1。 TDSQL中,text最大65535B。 |
16 |
smallint |
smallint |
GaussDB for PG中,smallint取值范围为-32,768 ~ +32,767。 TDSQL中,smallint对于无符号数的存储范围是0~65535,而对于有符号数的存储范围是-32768~32767。 需要判断GaussDB for PG中smallint作为有符号数的存储范围是否够,如果不够需要换integer。 |
17 |
int unsigned |
integer |
GaussDB for PG中integer为有符号数,存储范围为-2,147,483,648 ~ +2,147,483,647。 TDSQL中int可以存储无符号数据的范围为0~4294967295,而对无符号数的存储范围是-2147483648~2147483647。 需要判断GaussDB for PG中integer作为有符号数的存储范围是否够,如果不够需要换bigint。 |
18 |
bigint unsigned |
bigint |
GaussDB for PG中bigint为有符号数,存储范围为-9,223,372,036,854,775,808 ~ +9,223,372,036,854,775,807。 TDSQL中bigint可以存储无符号数的范围是0~18446744073709551615,而对有符号数的存储范围是-9223372036854775808~9223372036854775807。 |
5.1.2建表
其他关键字:
序号 |
TDSQL字段或关键字 |
GaussDB for PG相应替代 |
备注 |
1 |
not null |
not null |
无区别 |
2 |
comment 'column' |
comment on column table_name.column_name is 'column'; |
字段备注 GaussDB for PG中的comment使用在建表完成之后,用单独的语句来完成备注,并且一次只能完成一个字段的备注,多个字段需要分别执行来完成。 TDSQL中comment跟在建表中字段之后使用。 |
3 |
comment='table' |
comment on table table_name is 'table'; |
表备注 GaussDB for PG中的comment使用在建表完成之后,用单独的语句来完成备注。 TDSQL中comment跟在建表语句之后使用。 |
4 |
auto_increment |
serial |
GaussDB for PG中的serial不建议使用,需要使用uuid等来替换。 |
5 |
collate |
默认在建库时使用lc_collate='C' lc_ctype='C' |
GaussDB for PG中的排序是lc_collate和lc_ctype配合使用的。 |
6 |
default null |
default null |
无区别 |
7 |
primary key |
primary key |
GaussDB for PG中的primary key可以是如下格式: create table test3 (a text primary key); create table test4 (a text, primary key(a)); |
8 |
key |
无 |
通过在表定义之后使用 CREATE INDEX 语句来实现 -- 创建表 CREATE TABLE example_table ( id SERIAL PRIMARY KEY, name VARCHAR(100), age INT ); -- 创建索引 CREATE INDEX idx_example_table_name ON example_table(name); 在这个例子中,我们首先创建了一个名为 example_table 的表,其中包含一个 id 列(作为主键,自动递增),一个 name 列和一个 age 列。然后,我们为 name 列创建了一个索引,索引名为 idx_example_table_name。 虽然 PostgreSQL 不支持直接在 CREATE TABLE 语句中嵌入 CREATE INDEX 语句,但你可以使用事务或脚本将这两步操作组合在一起,以确保它们在逻辑上是一起的,但是这种方式是在公司不允许的。 |
9 |
engine |
无 |
GaussDB for PG不选择存储引擎。 |
10 |
default charset |
encoding |
GaussDB for PG中encoding='UTF8'替代TDSQL的DEFAULT CHARSET=utf8mb4 |
11 |
partition by range |
partition by range |
无区别 |
12 |
to_days() |
无 |
GaussDB for PG中不存在对应的函数,需要自建函数来实现,但是在实际的开发过程中不允许使用自建函数,因为不通用。 |
13 |
charset |
encoding |
GaussDB for PG中encoding='UTF8'替代TDSQL的charset=utf8mb4 |
注:构建按日分区的表的方式
常见的分区方案有范围分区(Range Partitioning)、间隔分区(Interval Partitioning)、哈希分区(Hash Partitioning)、列表分区(List Partitioning)、数值分区(Value Partition)等。目前行存表支持范围分区、间隔分区、哈希分区、列表分区,列存表仅支持范围分区。
范围分区策略:根据分区键值将记录映射到已创建的某个分区上,如果可以映射到已
创建的某一分区上,则把记录插入到对应的分区上,否则给出报错和提示信息。这是最常用的分区策略。
间隔分区是一种特殊的范围分区,相比范围分区,新增间隔值定义,当插入记录找不
到匹配的分区时,可以根据间隔值自动创建分区。
间隔分区只支持基于表的一列分区,并且该列只支持TIMESTAMP[(p)] [WITHOUT TIME ZONE]、TIMESTAMP[(p)] [WITH TIME ZONE]、DATE数据类型。
由于GaussDB for PG不存在TDSQL中的to_days函数,因此建立分区时可以采用如下格式:
drop table if exists sales; create table if not exists sales ( sale_id SERIAL PRIMARY KEY, sale_date DATE NOT NULL) partition by range(sale_date) ( partition p_20190201 VALUES LESS THAN ('2019-02-01'), partition p_20190202 VALUES LESS THAN ('2019-02-02'), partition p_20190203 VALUES LESS THAN ('2019-02-03'), partition p_DEF values less than ('9999-12-31') )
直接使用日期来进行日期划分。注意,对于用于需要进行分区的字段赋成Date或timestamp,同时在批量程序中也需要做相应的修改。
5.1.3表结构转换
由于TDSQL迁移GaussDB无法使用华为提供的表结构转换工具,因此需要自行编写程序来实现相应的功能,总体是实现5.1.1和5.1.2部分的相关内容。
表结构转换程序的输入是TDSQL环境下的建表DDL语句,输出是GaussDB for PG环境下的建表DDL语句。
POM配置:
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>org.example</groupId> <artifactId>tdsql2guass</artifactId> <version>1.0-SNAPSHOT</version> <properties> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.29</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.15</version> </dependency> </dependencies> </project>
连接TDSQL数据库:
ConnectTdsql.java:
public class ConnectTdsql { private static Connection conn = null; private static Statement stsm = null; // 注册驱动程序 static { try { Class.forName("com.mysql.cj.jdbc.Driver"); } catch (Exception e) { e.printStackTrace(); System.out.println("获取数据库连接对象出错"); } } // 获取连接对象,该方法返回一个连接 public static Connection getConnection(String url, String user, String password) { // 创建连接对象 try { conn = DriverManager.getConnection(url, user, password); System.out.println("数据库已经连接好了!"); } catch (SQLException e) { e.printStackTrace(); throw new RuntimeException(e); } return conn; } // 释放资源,断开连接 public static void close(Connection conn, Statement stsm) { if (null != stsm) { try { stsm.close(); } catch (SQLException e) { e.printStackTrace(); throw new RuntimeException(e); } } if (null != conn) { try { conn.close(); } catch (SQLException e) { e.printStackTrace(); throw new RuntimeException(e); } } } // 释放资源,断开连接,重载 public static void close(Connection conn, Statement stsm, ResultSet rs) { if (null != rs) { try { rs.close(); } catch (SQLException e1) { e1.printStackTrace(); throw new RuntimeException(e1); } } if (null != stsm) { try { stsm.close(); } catch (SQLException e) { e.printStackTrace(); throw new RuntimeException(e); } } if (null != conn) { try { conn.close(); } catch (SQLException e) { e.printStackTrace(); throw new RuntimeException(e); } } } }
获取TDSQL中表的DDL:
public class GetTableNameAndDDL { public static ArrayList getTableName(String url, String user, String password, Connection conn, Statement stsm, String dataBaseName) { ArrayList<String> tableName = new ArrayList<>(); ResultSet rs = null; int counter = 0; try { // 调用工具类连接对象 conn = ConnectTdsql.getConnection(url, user, password); // 创建statement对象 stsm = conn.createStatement(); // 准备sql语句 String sql = "SELECT TABLE_NAME, TABLE_SCHEMA FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = '" + dataBaseName + "';"; // 执行sql语句,返回的是ResultSet对象 rs = stsm.executeQuery(sql); // 遍历查询结果 while (rs.next()) { StringBuilder sb = new StringBuilder(); // 根据表情况来定制 String table_name = rs.getString("TABLE_NAME"); String table_schema = rs.getString("TABLE_SCHEMA"); sb = sb.append(table_schema).append(".").append(table_name); String table_all_name = sb.toString(); tableName.add(table_all_name); ++counter; } System.out.println("数据查询完毕!" + "共有 " + counter + " 张表"); } catch (Exception e) { e.printStackTrace(); throw new RuntimeException(e); } finally { // 调用工具类关闭连接 ConnectTdsql.close(conn, stsm, rs); } return tableName; } public static void getTableDDL(String url, String user, String password, Connection conn, Statement stsm, String dataBaseName, String folderPath) { ResultSet rs = null; try { // 获取所有的表名 ArrayList<String> allTableName = getTableName(url, user, password, conn, stsm, dataBaseName); // 获取建表信息 // 调用工具类连接对象 conn = ConnectTdsql.getConnection(url, user, password); // 创建statement对象 stsm = conn.createStatement(); // 创建文件夹 makeFolder(folderPath); // 准备sql语句 for (String tableName : allTableName) { String sql = "SHOW CREATE TABLE " + tableName; rs = stsm.executeQuery(sql); if (null != rs) { if (rs.next()) { String table_ddl = rs.getString("Create Table"); stringToFile(folderPath, tableName, table_ddl); } } } } catch (Exception e) { e.printStackTrace(); throw new RuntimeException(e); } finally { // 调用工具类关闭连接 ConnectTdsql.close(conn, stsm, rs); } } public static void stringToFile(String folderPath, String fileName, String tableDDLInfo) { try { String filePath = folderPath + fileName + ".sql"; // 创建一个File文件,表示要保存的文件 File file = new File(filePath); // 创建一个BufferedWriter对象 BufferedWriter writer = new BufferedWriter(new FileWriter(file)); // 将String字符串写入文件 writer.write(tableDDLInfo); // 关闭BufferedWriter对象 writer.close(); System.out.println("存放 " + fileName + " 建表信息的文件已经建好!"); } catch (IOException e) { e.printStackTrace(); throw new RuntimeException(e); } } public static void makeFolder(String folderPath) { File folder = new File(folderPath); if (!folder.exists()) { folder.mkdirs(); } System.out.println("存放建表信息的文件夹已经建好!"); } }
TDSQL的DDL结构转为GaussDB的DDL结构:
public class GetDDLAndTransform { public static void getAllFile(File fileInput, ArrayList<File> allFileList) { // 获取文件列表 File[] fileList = fileInput.listFiles(); assert fileList != null; for (File file : fileList) { allFileList.add(file); } } public static void getFileDDLInfoAndTransform(String folderPath) { File dir = new File(folderPath); ArrayList<File> allFileList = new ArrayList<>(); // 判断文件夹是否存在 if(!dir.exists()) { System.out.println("需要查询文件的文件夹不存在!"); return; } getAllFile(dir, allFileList); for (File file : allFileList) { String ddlInfo = readFileByPath(file); System.out.println(ddlInfo); // 处理建表转换的主逻辑 /* * 这里是实现本次任务的主要部分,执行tdsqlDDL2GaussDBDDL,然后输出到文件中 */ System.out.println(tdsqlDDL2GaussDBDDL(file)); } System.out.println("该文件夹下共有 " + allFileList.size() + " 个文件"); } public static String readFileByPath(File fileName) { StringBuilder result = new StringBuilder(); try { FileInputStream fileInputStream = new FileInputStream(fileName); InputStreamReader inputStreamReader = new InputStreamReader(fileInputStream); BufferedReader bufferedReader = new BufferedReader(inputStreamReader); String line = null; while ((line = bufferedReader.readLine()) != null) { result.append(line).append("\n"); } bufferedReader.close(); } catch (Exception e) { e.printStackTrace(); } return result.toString(); } /* * 针对具体的TDSQL到GaussDB for PG的DDL转换任务,设计如下: * 使用3个HashMap来存储相关信息,定义为HashMap<String, ArrayList<String>> * 1)第1个HashMap存储字段信息,key为字段名,value为关于该字段的相关设定 * 2)第2个HashMap存储索引信息,key为索引名,value为关于该索引的相关类型 * 3)第3个HashMap存储分区信息,key为分区键信息,value为关于该分区的规则和数量信息 * 将TDSQL中相关表信息拆解到上述数据结构中,然后读取出来按照GaussDB for PG的相关规则进行DDL重构 */ public static String tdsqlDDL2GaussDBDDL(File tdsqlFileName) { // 存储GaussDB的建表信息 StringBuilder gaussDDL = new StringBuilder(); // 存储GaussDB的分区信息 StringBuilder gaussDDLPart = new StringBuilder(); // 存储GaussDB的索引信息 StringBuilder gaussDDLIndex = new StringBuilder(); // 存储表信息 LinkedHashMap<String, String> tableInfo = new LinkedHashMap<>(); // 存储列信息 LinkedHashMap<String, String> columnInfo = new LinkedHashMap<>(); // 存储索引信息 LinkedHashMap<String, String> indexInfo = new LinkedHashMap<>(); // 存储分区信息 LinkedHashMap<String, String> partitionInfo = new LinkedHashMap<>(); // 存储comment信息 ArrayList<String> commentInfo = new ArrayList<>(); try { FileInputStream fileInputStream = new FileInputStream(tdsqlFileName); InputStreamReader inputStreamReader = new InputStreamReader(fileInputStream); BufferedReader bufferedReader = new BufferedReader(inputStreamReader); String line = null; // 提取完毕之后使用Map里的数据来进行重构 while ((line = bufferedReader.readLine()) != null) { // 获取表名 if (checkTableNameInfo(line)) { tableInfo = getTableNameInfo(line); // 获取列相关 // 重构GaussDB建表 // 1)首先获取表名 String gs_table_name = tableInfo.get("table_name"); String gs_create_table_name = "CREATE TABLE IF NOT EXISTS " + gs_table_name + " ("; gaussDDL.append(gs_create_table_name).append("\r\n"); } else if (checkColumnNameInfo(line)) { columnInfo = getColumnNameInfo(line); // 关于列信息的构建需要字段的规则映射 /* * 需要补充映射规则 */ // 列名之前需要缩进 gaussDDL.append("\t"); Iterator<Map.Entry<String, String>> iterator = columnInfo.entrySet().iterator(); String gs_column_name = null; String gs_comment = null; String gs_not_null = null; String gs_default_null = null; String gs_auto_increment = null; String gs_column_type = null; String tdsql_column_type = null; // 遍历结果提取信息进行拼接 while (iterator.hasNext()) { Map.Entry<String, String> entry = iterator.next(); String key = entry.getKey(); if ("column_name" == key) { gs_column_name = entry.getValue(); } else if ("comment" == key) { gs_comment = entry.getValue(); // comment信息存储起来 commentInfo.add(gs_comment); } else if ("not_null" == key) { gs_not_null = entry.getValue(); } else if ("default_null" == key) { gs_default_null = entry.getValue(); } else if ("auto_increment" == key) { gs_auto_increment = entry.getValue(); } else if ("column_type" == key) { tdsql_column_type = entry.getValue(); } } // 列名 gaussDDL.append(gs_column_name.replace("`","")); gaussDDL.append(" "); // 判断是否为auto_increment,如果为auto_increment在Gauss中需要用uuid来替代,字段类型需要使用varchar,此时跳过字段类型的转换 if (gs_auto_increment == "1") { gs_column_type = "varchar(256)"; gaussDDL.append(gs_column_type); gaussDDL.append(" "); } else { // 字段类型,这里需要维护一个映射表,如果还涉及到char和varchar,需要将数字进行转换 if (null != tdsql_column_type) { gs_column_type = tdsqlColumnConvert2Gauss(tdsql_column_type); gaussDDL.append(gs_column_type); gaussDDL.append(" "); } } // 是否为not null if (gs_not_null == "1") { gaussDDL.append("NOT NULL"); gaussDDL.append(" "); } // 是否为default null if (gs_default_null == "1") { gaussDDL.append("DEFAULT NULL"); gaussDDL.append(" "); } gaussDDL.append(",").append("\r\n"); // 获取索引相关 } else if (checkIndexNameInfo(line)) { indexInfo = getIndexNameInfo(line); // 先添加主键,普通索引的添加方式不同于主键 if (indexInfo.containsKey("primary_key")) { gaussDDL.append("\t"); gaussDDL.append("PRIMARY KEY("); gaussDDL.append(indexInfo.get("primary_key").replace("`","")); gaussDDL.append(")").append("\r\n"); } if (!indexInfo.isEmpty()) { Iterator<Map.Entry<String, String>> iterator = indexInfo.entrySet().iterator(); while (iterator.hasNext()) { Map.Entry<String, String> entry = iterator.next(); String key = entry.getKey(); if ("primary_key" != key) { gaussDDLIndex.append("CREATE INDEX IF NOT EXISTS ").append(entry.getKey().replace("`","")).append(" ON ").append(tableInfo.get("table_name")).append("(").append(entry.getValue().replace("`","")).append(");").append("\r\n"); } } } // 到主键完成,高斯中的新表已经构建完成 // 获取分区相关 } else if (checkPartitionNameInfo(line)) { partitionInfo = getPartitionNameInfo(line); // 重构的时候需要注意,使用时期的字段需要时Date if (!partitionInfo.isEmpty()) { Iterator<Map.Entry<String, String>> iterator = partitionInfo.entrySet().iterator(); while (iterator.hasNext()) { Map.Entry<String, String> entry = iterator.next(); String key = entry.getKey(); if ("PARTITION" == key) { gaussDDLPart.append("PARTITION BY RANGE(").append(entry.getValue()).append(")").append("\r\n"); gaussDDLPart.append("(").append("\r\n"); } else if ("P_DEF".equals(key)) { gaussDDLPart.append("\t").append("PARTITION ").append(key).append(" VALUES LESS THAN (").append(entry.getValue()).append(")").append("\r\n"); gaussDDLPart.append(");"); } else if (key != "PARTITION" && key != "P_DEF") { gaussDDLPart.append("\t").append("PARTITION ").append(key).append(" VALUES LESS THAN (").append(entry.getValue()).append(")").append(",").append("\r\n"); } } } } // 还需要获取关于表的comment,这个就节后来做吧,tableInfo作为传入变量 } gaussDDL.append(") WITH (orientation=row,storage_type = USTORE)").append("\r\n"); if (gaussDDLPart.toString().trim().length() == 0) { gaussDDL.append(";").append("\r\n"); } else { gaussDDL.append(gaussDDLPart.toString()).append("\r\n"); } gaussDDL.append(gaussDDLIndex.toString()).append("\r\n"); } catch (Exception e) { e.printStackTrace(); } return gaussDDL.toString(); } public static boolean checkTableNameInfo(String line) { boolean result = false; try { String regex = "CREATE TABLE"; Pattern pattern = Pattern.compile(regex, Pattern.CASE_INSENSITIVE); Matcher matcher = pattern.matcher(line); if (matcher.find()) { System.out.println("表名信息匹配成功,开始获取表名!"); result = true; } } catch (Exception e) { e.printStackTrace(); } return result; } public static LinkedHashMap<String, String> getTableNameInfo(String line) { LinkedHashMap<String, String> tableHashMap = new LinkedHashMap<>(); String tableName = null; try { String splitRole = "`"; String[] parts = line.split(splitRole); int partsLength = parts.length; if (partsLength >= 2) { tableName = parts[partsLength - 2]; } tableHashMap.put("table_name", tableName); } catch (Exception e){ e.printStackTrace(); } return tableHashMap; } public static boolean checkColumnNameInfo(String line) { boolean result = false; try { String[] notColumnKeyWorlds = {"CREATE TABLE", "KEY", "ENGINE=InnoDB", "PARTITION"}; boolean auxiliary = true; for (String keyworld : notColumnKeyWorlds) { Pattern pattern = Pattern.compile(keyworld, Pattern.CASE_INSENSITIVE); Matcher matcher = pattern.matcher(line); // 如果匹配到关键字,则代表不属于列定义行,需要同时满足 auxiliary = auxiliary & (!matcher.find()); } if (auxiliary == true) { result = true; } } catch (Exception e) { e.printStackTrace(); } return result; } public static LinkedHashMap<String, String> getColumnNameInfo(String line) { LinkedHashMap<String, String> columnHashMap = new LinkedHashMap<>(); String beforeKeyword = null; String afterKeyword = null; String modifiedString = null; String columnName = null; String columnType = null; /* * 1)key为‘column_name’,value为相应的字段名,如果没有为0 * 2)key为’column_type‘,value为该数据类型和数值,如果没有为0 * 3)key为关键字‘not null',value为1表示存在,value为0表示不存在 * 4)key为关键字’default null‘,value为1表示存在,value为0表示不存在 * 5)key为关键字’comment‘,value为具体的内容,如果没有的话为0 * 6)key为关键字’auto_increment',value为1表示存在,value为0表示不存在(这部分使用uuid来替代,在程序中来进行改造) */ try { // 首先将输入的字符串都转换为小写 String input = line.toLowerCase().trim(); // 首先提取出comment前后的内容,防止comment中的内容对具体的拆分产生影响,如果comment中继续包含comment关键字需要更加细致的分析 String commentKeyWord = "comment"; int resultCommentKeyWord = -1; resultCommentKeyWord = keywordOccurrence(input, commentKeyWord); if (resultCommentKeyWord != -1) { // 如果存在comment关键字 beforeKeyword = input.substring(0, resultCommentKeyWord); int startIndexAfterKeyword = resultCommentKeyWord + commentKeyWord.length(); if (startIndexAfterKeyword <= input.length()) { afterKeyword = input.substring(startIndexAfterKeyword); if (afterKeyword.endsWith(",")) { modifiedString = afterKeyword.substring(0, afterKeyword.length() - 1); } columnHashMap.put("comment", modifiedString); } // 后面的部分围绕beforeKeyword来开展 // 1)列名关键字: columnName = extraceColumnName(beforeKeyword); if (isNotBlank(columnName)) { columnHashMap.put("column_name", columnName); } else { columnHashMap.put("column_name", "0"); } // 2)not null关键字 int resultNotNullKeyWord = -1; String notNullKeyWord = "not null"; resultNotNullKeyWord = keywordOccurrence(beforeKeyword, notNullKeyWord); if (resultNotNullKeyWord != -1) { columnHashMap.put("not_null", "1"); } else { columnHashMap.put("not_null", "0"); } // 3)default null关键字 int resultDefualtNullKeyWord = -1; String defaultNullKeyWord = "default null"; resultDefualtNullKeyWord = keywordOccurrence(beforeKeyword, defaultNullKeyWord); if (resultDefualtNullKeyWord != -1) { columnHashMap.put("default_null", "1"); } else { columnHashMap.put("default_null", "0"); } // 4)auto_increment关键字 int resultAutoIncrementKeyWord = -1; String autoIncrementKeyWord = "auto_increment"; resultAutoIncrementKeyWord = keywordOccurrence(beforeKeyword, autoIncrementKeyWord); if (resultAutoIncrementKeyWord != -1) { columnHashMap.put("auto_increment", "1"); } else { columnHashMap.put("auto_increment", "0"); } // 5)column_type关键字 // 导出的标准SQL,column_type会紧跟在columnName之后,处于第二的位置,将beforeKeyword拆分之后找到第二个位置就好 String regex = " "; String[] types = beforeKeyword.trim().split(regex); if (types.length >= 1) { columnType = types[1]; columnHashMap.put("column_type", columnType); } else { columnHashMap.put("column_type", "0"); } } else { // 如果resultCommentKeyWord表示-1,则代表不存在comment关键字 columnHashMap.put("comment", "0"); // 后面的部分围绕beforeKeyword来开展 // 1)列名关键字: columnName = extraceColumnName(input); if (isNotBlank(columnName)) { columnHashMap.put("column_name", columnName); } else { columnHashMap.put("column_name", "0"); } // 2)not null关键字 int resultNotNullKeyWord = -1; String notNullKeyWord = "not null"; resultNotNullKeyWord = keywordOccurrence(input, notNullKeyWord); if (resultNotNullKeyWord != -1) { columnHashMap.put("not_null", "1"); } else { columnHashMap.put("not_null", "0"); } // 3)default null关键字 int resultDefualtNullKeyWord = -1; String defaultNullKeyWord = "default null"; resultDefualtNullKeyWord = keywordOccurrence(input, defaultNullKeyWord); if (resultDefualtNullKeyWord != -1) { columnHashMap.put("default_null", "1"); } else { columnHashMap.put("default_null", "0"); } // 4)auto_increment关键字 int resultAutoIncrementKeyWord = -1; String autoIncrementKeyWord = "auto_increment"; resultAutoIncrementKeyWord = keywordOccurrence(input, autoIncrementKeyWord); if (resultAutoIncrementKeyWord != -1) { columnHashMap.put("auto_increment", "1"); } else { columnHashMap.put("auto_increment", "0"); } // 5)column_type关键字 // 导出的标准SQL,column_type会紧跟在columnName之后,处于第二的位置,将beforeKeyword拆分之后找到第二个位置就好 String regex = " "; String[] types = input.trim().split(regex); if (types.length >= 1) { columnType = types[1]; columnHashMap.put("column_type", columnType); } else { columnHashMap.put("column_type", "0"); } } } catch (Exception e) { e.printStackTrace(); } return columnHashMap; } public static int keywordOccurrence(String input, String keyword) { int firstOccurrentIndex = -1; int count = 0; int currentIndex = 0; while ((currentIndex = input.indexOf(keyword.toLowerCase(), currentIndex)) != -1) { count++; if (firstOccurrentIndex == -1) { firstOccurrentIndex = currentIndex; } currentIndex += keyword.length(); } return firstOccurrentIndex; } public static String extraceColumnName(String input) { String result = null; String regex = "`([^`]*)`"; Pattern pattern = Pattern.compile(regex); Matcher matcher = pattern.matcher(input); while (matcher.find()) { result = matcher.group(0); } return result; } public static boolean isNotBlank(String s){ if(s != null && !s.isEmpty()){ return true; } return false; } public static boolean checkIndexNameInfo(String line) { boolean result = false; try { String regex = "KEY"; Pattern pattern = Pattern.compile(regex, Pattern.CASE_INSENSITIVE); Matcher matcher = pattern.matcher(line); if (matcher.find()) { result = true; } } catch (Exception e) { e.printStackTrace(); } return result; } public static LinkedHashMap<String, String> getIndexNameInfo(String line) { LinkedHashMap<String, String> indexHashMap = new LinkedHashMap<>(); try { // 判断主键 String regex_prim_key = "PRIMARY KEY"; Pattern pattern = Pattern.compile(regex_prim_key, Pattern.CASE_INSENSITIVE); Matcher matcher = pattern.matcher(line); if (matcher.find()) { String regex = "\\(([^)]+)\\)"; Pattern pattern_prim_key = Pattern.compile(regex, Pattern.CASE_INSENSITIVE); Matcher matcher_prim_key = pattern_prim_key.matcher(line); while (matcher_prim_key.find()) { String prim_key_column = matcher_prim_key.group(1); indexHashMap.put("primary_key", prim_key_column); } } else { // 判断普通索引 String regex_key = "KEY"; Pattern pattern1 = Pattern.compile(regex_key, Pattern.CASE_INSENSITIVE); Matcher matcher1 = pattern1.matcher(line); if (matcher1.find()) { String split_key = " "; String[] indexs = line.trim().split(split_key); String index_name = indexs[1]; String index_info = null; String regex_index = "\\(([^)]+)\\)"; Pattern pattern_key = Pattern.compile(regex_index, Pattern.CASE_INSENSITIVE); Matcher matcher_key = pattern_key.matcher(line); while (matcher_key.find()) { String index_column = matcher_key.group(1); indexHashMap.put(index_name, index_column); } } } } catch (Exception e) { e.printStackTrace(); } return indexHashMap; } public static boolean checkPartitionNameInfo(String line) { boolean result = false; try { String regex = "PARTITION"; Pattern pattern = Pattern.compile(regex, Pattern.CASE_INSENSITIVE); Matcher matcher = pattern.matcher(line); if (matcher.find()) { result = true; } } catch (Exception e) { e.printStackTrace(); } return result; } public static LinkedHashMap<String, String> getPartitionNameInfo(String line) { StringBuilder sb = new StringBuilder(); LinkedHashMap<String, String> partitionHashMap = new LinkedHashMap<>(); String part_key_column = null; String part_info = null; String part_info_detail = null; try { // 判断分区键 String regex_part_key = "PARTITION BY RANGE"; Pattern pattern = Pattern.compile(regex_part_key, Pattern.CASE_INSENSITIVE); Matcher matcher = pattern.matcher(line); if (matcher.find()) { String regex = "to_days\\(([^)]+)\\)"; Pattern pattern_part_key = Pattern.compile(regex, Pattern.CASE_INSENSITIVE); Matcher matcher_part_key = pattern_part_key.matcher(line); while (matcher_part_key.find()) { part_key_column = matcher_part_key.group(1); partitionHashMap.put("PARTITION", part_key_column); } } else { // 判断分区信息 String regex_part_info = "VALUES LESS THAN"; Pattern pattern1 = Pattern.compile(regex_part_info, Pattern.CASE_INSENSITIVE); Matcher matcher1 = pattern1.matcher(line); if (matcher1.find()) { String split_key = " "; String[] indexs = line.trim().split(split_key); part_info = indexs[1]; part_info_detail = convertDateFormat(part_info); sb.append("'").append(part_info_detail).append("'"); partitionHashMap.put(part_info, sb.toString()); } } } catch (Exception e) { e.printStackTrace(); } return partitionHashMap; } public static String convertDateFormat(String input) { String dataPart = input.replace("P_", ""); String result = null; SimpleDateFormat inputFormat = new SimpleDateFormat("yyyyMMdd"); SimpleDateFormat outputFormat = new SimpleDateFormat("yyyy-MM-dd"); try { if (isNumberic(dataPart)) { Date date = inputFormat.parse(dataPart); Calendar calendar = Calendar.getInstance(); calendar.setTime(date); calendar.add(calendar.DAY_OF_MONTH, 1); Date tomorrow = calendar.getTime(); result = outputFormat.format(tomorrow); } else { result = "9999-12-31"; } } catch (ParseException e) { e.printStackTrace(); return null; } return result; } public static boolean isNumberic(String str) { return str != null && str.matches("\\d+"); } public static String tdsqlColumnConvert2Gauss(String str) { // 映射表,key为TDSQL字段类型,value为GaussDB字段类型 HashMap<String, String> columnMap = new HashMap<>(); // 向HashMap中添加一些键值对,用作后面column_info的转换 columnMap.put("bigint", "bigint"); columnMap.put("varchar", "varchar"); columnMap.put("timestamp", "timestamp"); columnMap.put("longblob", "blob"); columnMap.put("tinyint", "tinyint"); columnMap.put("int", "integer"); columnMap.put("datetime", "timestamp"); columnMap.put("double", "float8"); columnMap.put("char", "char"); columnMap.put("decimal", "decimal"); columnMap.put("blob", "blob"); columnMap.put("mediumtext", "text"); columnMap.put("longtext", "clob"); columnMap.put("date", "date"); columnMap.put("text", "text"); columnMap.put("smallint", "smallint"); columnMap.put("int unsigned", "integer"); columnMap.put("bigint unsigned", "bigint"); // 存储结果 StringBuilder result = new StringBuilder(); String column_digit = null; String column_info = null; String column_info_gs = null; boolean isDecimal = false; boolean isVar2char = false; // 1)第一步:判断输入的字符串中是否含有数字,如果有就提取出来 boolean containsDigit = containsDigit(str); if (containsDigit) { // 这里主要是针对varchar、char和decimal的情况 // 如果包含数字且包含逗号","的情况 boolean containsComma = containsComma(str); if (containsComma) { column_info = extractNonBracketContent(str); column_digit = extractContentInString(str); isDecimal = true; } else { // 在这之前需要去掉字段里的括号 String input = str.replace("(","").replace(")",""); // 数字部分 column_digit = extractNumbers(input); // 字段类型 column_info = extractNoDigitPart(input); isVar2char = true; } } else { column_info = str; } // 2)第二步:创建一个字段类型映射HashMap if (columnMap.containsKey(column_info)) { column_info_gs = columnMap.get(column_info); } // 3)第三步:拼接字段 if (null != column_digit) { if (isDecimal) { result = result.append(column_info_gs).append(column_digit); } else if (isVar2char) { int num = Integer.parseInt(column_digit); if (column_info_gs == "varchar" || column_info_gs == "char") { num = num * 4; } // num = num * 4; String var2CharDigit = Integer.toString(num); result = result.append(column_info_gs).append("(").append(var2CharDigit).append(")"); } } else { result = result.append(column_info_gs); } return result.toString(); } public static boolean containsDigit(String input) { Pattern pattern = Pattern.compile("\\d"); Matcher matcher = pattern.matcher(input); return matcher.find(); } public static boolean containsComma(String input) { return input.contains(","); } public static String extractNumbers(String input) { Pattern pattern = Pattern.compile("\\d+"); Matcher matcher = pattern.matcher(input); StringBuilder result = new StringBuilder(); while (matcher.find()) { result.append(matcher.group()).append(" "); } if (result.length() > 0) { result.setLength(result.length() - 1); } return result.toString(); } public static String extractNoDigitPart(String str) { return str.replaceAll("\\d", ""); } public static String extractContentInString(String str) { String result = null; String pattern = "\\((.*?)\\)"; Pattern r = Pattern.compile(pattern); Matcher m = r.matcher(str); if (m.find()) { result = m.group(0); } return result; } public static String extractNonBracketContent(String str) { String result = null; String pattern = "^[^(]*"; Pattern r = Pattern.compile(pattern); Matcher m = r.matcher(str); if (m.find()) { result = m.group(); } return result; } }
5.1.4 表结构转换结果
以有多字段、多索引和多分区的TDSQL表转换为例:
TDSQL的DDL结构:
CREATE TABLE `baas_mq_tran_monitor` ( `msg_id` varchar(40) COLLATE utf8mb4_bin NOT NULL COMMENT '消息ID', `global_serno` varchar(40) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '全局流水号', `credit_type` varchar(20) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '征信类型', `send_time` datetime NOT NULL COMMENT '发送时间', `receive_time` datetime DEFAULT NULL COMMENT '接收时间', PRIMARY KEY (`msg_id`,`send_time`), KEY `idx_global_serno_1` (`global_serno`), KEY `idx_sendTime_1` (`send_time`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='mq交易监控表' /*!50100 PARTITION BY RANGE (TO_DAYS(send_time)) (PARTITION P_20250121 VALUES LESS THAN (739638) ENGINE = InnoDB, PARTITION P_20250122 VALUES LESS THAN (739639) ENGINE = InnoDB, PARTITION P_20250123 VALUES LESS THAN (739640) ENGINE = InnoDB, PARTITION P_20250124 VALUES LESS THAN (739641) ENGINE = InnoDB, PARTITION P_20250125 VALUES LESS THAN (739642) ENGINE = InnoDB, PARTITION P_20250126 VALUES LESS THAN (739643) ENGINE = InnoDB, PARTITION P_20250127 VALUES LESS THAN (739644) ENGINE = InnoDB, PARTITION P_20250128 VALUES LESS THAN (739645) ENGINE = InnoDB, PARTITION P_20250129 VALUES LESS THAN (739646) ENGINE = InnoDB, PARTITION P_20250130 VALUES LESS THAN (739647) ENGINE = InnoDB, PARTITION P_20250131 VALUES LESS THAN (739648) ENGINE = InnoDB, PARTITION P_20250201 VALUES LESS THAN (739649) ENGINE = InnoDB, PARTITION P_20250202 VALUES LESS THAN (739650) ENGINE = InnoDB, PARTITION P_20250203 VALUES LESS THAN (739651) ENGINE = InnoDB, PARTITION P_20250204 VALUES LESS THAN (739652) ENGINE = InnoDB, PARTITION P_20250205 VALUES LESS THAN (739653) ENGINE = InnoDB, PARTITION P_20250206 VALUES LESS THAN (739654) ENGINE = InnoDB, PARTITION P_20250207 VALUES LESS THAN (739655) ENGINE = InnoDB, PARTITION P_DEF VALUES LESS THAN MAXVALUE ENGINE = InnoDB) */
转换为GaussDB的DDL结构:
CREATE TABLE IF NOT EXISTS baas_mq_tran_monitor ( msg_id varchar(160) NOT NULL , global_serno varchar(160) DEFAULT NULL , credit_type varchar(80) DEFAULT NULL , send_time timestamp NOT NULL , receive_time timestamp DEFAULT NULL , PRIMARY KEY(msg_id,send_time) ) WITH (orientation=row,storage_type = USTORE) PARTITION BY RANGE(send_time) ( PARTITION P_20250121 VALUES LESS THAN ('2025-01-22'), PARTITION P_20250122 VALUES LESS THAN ('2025-01-23'), PARTITION P_20250123 VALUES LESS THAN ('2025-01-24'), PARTITION P_20250124 VALUES LESS THAN ('2025-01-25'), PARTITION P_20250125 VALUES LESS THAN ('2025-01-26'), PARTITION P_20250126 VALUES LESS THAN ('2025-01-27'), PARTITION P_20250127 VALUES LESS THAN ('2025-01-28'), PARTITION P_20250128 VALUES LESS THAN ('2025-01-29'), PARTITION P_20250129 VALUES LESS THAN ('2025-01-30'), PARTITION P_20250130 VALUES LESS THAN ('2025-01-31'), PARTITION P_20250131 VALUES LESS THAN ('2025-02-01'), PARTITION P_20250201 VALUES LESS THAN ('2025-02-02'), PARTITION P_20250202 VALUES LESS THAN ('2025-02-03'), PARTITION P_20250203 VALUES LESS THAN ('2025-02-04'), PARTITION P_20250204 VALUES LESS THAN ('2025-02-05'), PARTITION P_20250205 VALUES LESS THAN ('2025-02-06'), PARTITION P_20250206 VALUES LESS THAN ('2025-02-07'), PARTITION P_20250207 VALUES LESS THAN ('2025-02-08'), PARTITION P_DEF VALUES LESS THAN ('9999-12-31') ); CREATE INDEX IF NOT EXISTS idx_global_serno_1 ON baas_mq_tran_monitor(global_serno); CREATE INDEX IF NOT EXISTS idx_sendTime_1 ON baas_mq_tran_monitor(send_time);
测试完毕无误。
欢迎大家讨论交流。