一、问题概述
ORACLE数据库在存储数据的时候,有时候会存在这样一种现象,一张表里的数据,既存在UTF8字符的,也存在GBK字符的,同时还有可能存在乱码数据。
NLS_CHARACTERSET是数据库字符集,NLS_NCHAR_CHARACTERSET是国家字符集,NLS_LANG 是 Oracle 数据库客户端的一个环境变量。
这种问题在于ORACLE对于规定了一种字符集后,对于插入的数据并没有强校验(garbage-in–garbage-out)。但是对于PG系的数据库,数据库的字符集在最开始创建数据库的时候就指定了,而且一般情况下其中的表数据受字符集的严格校验(SQL_ASCII除外)。
这也就导致了,原本是什么类型的字符正常可以导入到对应的字符集的库里,而原本直接存入ORACLE库里的和表原本的字符集不匹配的其他类型的字符可能无法转换,此外乱码数据可能直接不能转换到PG系的库里。(ORACLE数据库可以存储乱码数据,PG系数据库有严格校验)
一些常见场景如下:
1)如果恰巧数据库的字符集也是UTF8, 那么Oracle就不作任何转换直接插入到数据中.
2)如果指定NLS_LANG是utf8, 但是输入的却是zhs16gbk的编码, 那么Oracle也会不作任何转换, 将ZHS16GBK的字符编码直接存入数据库
3)如果数据库的字符是AL32UTF8, 您指定NLS_LANG为ZHS16GBK, 但是, 您真正输入的是UTF8的字符, 那么,Oracle会把您输入的UTF8字符当作ZHS16GBK字符转换为UTF8存入数据库. 这种情况会出现乱码。
4)工具(putty/securecrt等等各种SSH客户端等等)设置的字符集可能导致乱码或者编码转换,客户端字符集 NLS_LANG 和 个人工具显示的字符集应该一致。
二、问题处理方案
1、使用SQL_ASCII字符集的数据库
如果不考虑中文显示以及乱码显示,可以直接在目标端建一个SQL_ASCII字符集的数据库,那么所有的数据均可以导入到新的PG系的数据库里,这样就不会有编码转换。这个设置基本不用来声明所使用的编码,因为此声明会忽略编码。在大多数情况下,如果使用了任何非ASCII数据,那么我们不建议使用SQL_ASCII,因为openGauss/MogDB无法转换或者校验非ASCII字符。
2.根据原库的编码,创建相同字符集编码的数据库
ORACLE在创建数据库的时候,需要指定字符集。虽然上述的一些原因导致ORACLE在指定了一种字符集的数据库后,还可能存在其他类型的字符和乱码数据,但这明显是不符合规范的,因为抛开其他类型的数据,单单乱码数据就不能算作有效数据,而且其他类型的数据,属于违反了最初的规划,也不能算作正常的数据。
因为不同的字符类型,对应存储的不同的字节长度是不一样的,例如一个汉字,在UTF8里存储为3位字节,而在GBK里存储为2个字节。
比如一个“你”字,用UTF-8存储在ORACLE数据里,是占用3个字节,是“xe4bda0”
而用GBK存储在ORACLE数据里,是占用2个字节,是“xc4e3”
所以长度是不一致的,在它转换到PG系的数据库里时,如果不能分辨出对应的是哪种类型的字符的话,只根据固定的一种类型的字符集的话,会存在部分数据的编码无法转换,在使用MTK(云和恩墨的迁移工具)迁移数据的时候,可能类似于这种的报错。
对于这种报错,因为本身在原库写入的时候就存在问题,如果数据库层大量修正的话,会耗费很大精力,因此,建议规范处理流程,仅根据数据库的字符集迁移“正确”的数据,其余的有问题的数据,可以在迁移过程找到对应的位置,或者进一步判断出该类数据原本应该属于什么样的字符集,然后这些问题数据让应用自行修正。
(1)查询存储的编码
在遇到问题数据的时候,我们可以根据其他正常的列,定位到对应的数据行,使用ORACLE的dump函数,查看其在ORACLE数据库里存储的编码格式
select dump(sms_content,1016) code,sms_content from tab_xxx where mo_id=‘123123123132312’
CODE SMS_CONTENT
-------------------------------------------------- ----------------------------------------------------------------------------------------------------
Typ=1 Len=64 CharacterSet=ZHS16GBK: 78,20,20,20,20 x :(; "“ ¥ ”€“"
,20,20,20,20,20,20,20,20,20,3a,28,a3,bb,20,20,20,2
0,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,
22,a1,b0,20,20,20,20,20,20,20,20,20,20,20,a3,a4,20
,20,20,20,a1,b1,80,a1,b0,22
(2)根据函数或者工具检验出真实的字符编码
然后可以把有问题的数据对应的编码,根据不同位数进行转换,看转换的数据是否是正常可读的数据,原本最开始查询的数据不用额外关注,因为可能受本地终端字符集的影响,可能本身存储的是正常的,例如,我查询到了问题列的数据导出的结果是如下。
取出部分编码,进行转换,"xe59089e69e97"根据GBK按照两位字节的形式进行转换,得到的是“鍚夋灄”,而根据UTF-8的类型进行转换,得到的是“吉林”。因此原本这条数据本身应该是UTF-8的字符集。
可以使用相关函数进行转换,也可以使用相关在线工具,例如:https://www.toolhelper.cn/EncodeDecode/EncodeDecode
postgres=# select convert_from(decode('e59089e69e97','hex')::bytea,'utf-8');
convert_from
--------------
吉林
(1 row)
而本身这条数据正确显示的时候,应该为
根据此类方法,以及对业务数据的了解,可以分辨出原本数据应该属于的字符集类型和非乱码的数据。
(3)MTK迁移过程需要处理的部分
手动使用COPY处理
因为MTK迁移的过程可以指定每批次导入数据的条数,在导入数据存在此类字符集编码问题的时候,我们一般会带上igErrorData参数,会把有问题的整个批次的数据全部回滚,而其余数据继续入库。
这个参数会在回滚那个问题批次数据的同时,把这个批次的数据记录到错误文件里,产生csv文件.
需要注意的是,这个csv是MTK分的迁移批次,每个批次的数据条数是一致的,可能在这一个批次里,仅存在一条问题数据,但它会把整个批次的数据全部放到csv里。可以先自行查看csv筛出问题数据,或者定位问题数据。
这个时候,我们关注的是这个csv里的正常数据,这个csv可以使用copy命令进行绕入。问题数据也可以暂时性先倒入,但是我们不关注因为字符集问题导致的查询乱码现象。
后续可以使用openGauss/MogDB的copy带有的COMPATIBLE_ILLEGAL_CHARS参数,这个参数允许导入非法字符容错参数。此语法仅对COPY FROM导入有效。但是在导入这些存在问题的数据的时候,存在一个规则,即对于’ ’,容错后转换为空格,其他非法字符,容错后转换为问号。这部分数据即我们上文提到的属于原库就错误写入的,后续应该让业务修正。
MTK自动处理
MTK(v2.9.4)新增的compatibleIlLegaLChars选项决定了mogdb/openGauss使用MTK迁移过程里,copy是否带COMPATIBLE_ILLEGAL_CHARS选项。
如果开启后,则会直接进行入上述的转换方式,写入数据,当然问题数据需要业务修正。(一般建议手动先产生错误的数据的csv后,定位有问题数据的位置后,再重新做一次自动处理的迁移)