和oracle字符集相关的参数是nls_lang。NLS_LANG的格式是:language_territory.client_charset,如AMERICAN_AMERICA.ZHS16GBK,那么第一位AMERICAN表示语言,第二位AMERICA表示日期和数字格式,第三位ZHS16GBK表示字符集。影响数据库和客户端的其实是第三部分。
通常情况下,数据库字符集不轻易修改,如果要修改,一般可以简单采用下面两种方法可行。:
1 2 |
1. 如果需要修改字符集,通常需要导出数据库数据,重建数据库,再导入数据库数据的方式来转换。 2. 通过ALTER DATABASE CHARACTER SET|[INTERNAL_USE] <new_characterset>语句修改字符集,但创建数据库后修改字符集是有限制的,只有新的字符集是当前字符集的超集时才能修改数据库字符集,例如UTF8是US7ASCII的超集,修改数据库字符集可使用ALTER DATABASE CHARACTER SET UTF8。 |
另外,我们还可以用csscan/csalter,和DMU(The Database Migration Assistant for Unicode)来实现数据库的字符集转换。
csscan/csalter适用在10g和11.1,在11.2之后就desupport,12c之后,唯一的字符集转换工具就是DMU。我们以DMU为例,进行一次数据集的转换。我们将原来的zhs16gbk的数据库字符集,转成al32utf8的字符集。
DMU的介绍可见The Database Migration Assistant for Unicode (DMU) Tool (Doc ID 1272374.1),且在这个文档中,也说明了可以在这里下载DMU。
关于DMU的known issue,可以参考Tips For and Known Issues With The Database Migration Assistant for Unicode (DMU) Tool version 2.1 (Doc ID 2018250.1)
初始的情况:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 |
[oracle11g@testdb2 ~]$ env |grep -i lang LANG=en_US.UTF-8 [oracle11g@testdb2 ~]$ [oracle11g@testdb2 ~]$ export NLS_LANG=AMERICAN_AMERICA.ZHS16GBK [oracle11g@testdb2 ~]$ sqlplus test/test SQL*Plus: Release 11.2.0.4.0 Production on Mon Jun 20 15:27:58 2016 Copyright (c) 1982, 2013, Oracle. All rights reserved. Connected to: Oracle Database 11g Enterprise Edition Release 11.2.0.4.0 - 64bit Production With the Partitioning, Oracle Label Security, OLAP, Data Mining and Real Application Testing options SQL> select * from nls_database_parameters where parameter in ('NLS_LANGUAGE','NLS_TERRITORY','NLS_CHARACTERSET','NLS_NCHAR_CHARACTERSET'); PARAMETER VALUE ------------------------------ ---------------------------------------- NLS_LANGUAGE AMERICAN NLS_TERRITORY AMERICA NLS_CHARACTERSET ZHS16GBK NLS_NCHAR_CHARACTERSET AL16UTF16 SQL> SQL> create table cs_test(a varchar2(4)); SQL> insert into cs_test values('1234'); SQL> insert into cs_test values('abcd'); SQL> insert into cs_test values('回家'); SQL> insert into cs_test values('高兴'); SQL> insert into cs_test values('好玩'); SQL> commit; SQL> select dump(a,1017) from cs_test; DUMP(A,1017) -------------------------------------------------------------------------------- Typ=1 Len=4 CharacterSet=ZHS16GBK: 1,2,3,4 Typ=1 Len=4 CharacterSet=ZHS16GBK: a,b,c,d Typ=1 Len=4 CharacterSet=ZHS16GBK: bb,d8,bc,d2 Typ=1 Len=4 CharacterSet=ZHS16GBK: b8,df,d0,cb Typ=1 Len=4 CharacterSet=ZHS16GBK: ba,c3,cd,e6 SQL> SQL> select dump(a,1016) from cs_test; DUMP(A,1016) -------------------------------------------------------------------------------- Typ=1 Len=4 CharacterSet=ZHS16GBK: 31,32,33,34 Typ=1 Len=4 CharacterSet=ZHS16GBK: 61,62,63,64 Typ=1 Len=4 CharacterSet=ZHS16GBK: bb,d8,bc,d2 Typ=1 Len=4 CharacterSet=ZHS16GBK: b8,df,d0,cb Typ=1 Len=4 CharacterSet=ZHS16GBK: ba,c3,cd,e6 SQL> insert into cs_test select * from cs_test; --插入若干次。变成一张有500多万数据,64M的表。 SQL>commit; SQL> select segment_name,(bytes)/1024/1024 size_mb from dba_segments where segment_name='CS_TEST'; SEGMENT_NAME SIZE_MB ------------------------------ ---------- CS_TEST 64 SQL> select count(*) from cs_test; COUNT(*) ---------- 5242880 SQL> --再创建CS_TEST2表,字段类型比CS_TEST的大,是varchar2(20),而不是varchar2(4),然后灌入CS_TEST的数据: SQL> create table CS_TEST2 (b varchar2(20)); SQL> insert into CS_TEST2 select * from CS_TEST; SQL> commit; --再创建CS_TEST3表,也是varchar2(4),一会用bulk cleansing的方式清洗: SQL> create table CS_TEST3 as select * from CS_TEST; --在test2下再建立一个clob字段的表,检查看看是否会影响clob字段 SQL> conn test2/test2 Connected. SQL> SQL> create table t ( x int, y clob ); SQL> create or replace procedure p( p_x in int, p_new_text in varchar2 ) 2 as 3 begin 4 insert into t values ( p_x, p_new_text ); 5 end; 6 / Procedure created. SQL> exec p(1, rpad('*',32000,'*') ); SQL> exec p(1, rpad('我',32000,'他')); SQL> exec p(1, rpad('我',32002,'他')); SQL> exec p(1, rpad('我',32200,'他')); SQL> select x, dbms_lob.getlength(y) from t; X DBMS_LOB.GETLENGTH(Y) ---------- --------------------- 1 32000 1 16000 1 16001 1 16100 SQL> --再建一个cs_test_normal,是char类型。 SQL> create table cs_test_normal as select 'a' as myname from dual connect by level <=100; Table created. SQL> desc cs_test_normal Name Null? Type ----------------------------------------- -------- ---------------------------- MYNAME CHAR(1) SQL> |
下面,我们就用DMU来进行字符集的转换。DMU目前只是提供了GUI界面,没有CLI界面,所以下面我们都用截图表示(每个截图可点击放大)。
注1,使用DMU,必须在数据库中安装XDB组件,不然会报错:
注2,我们需要先运行?/rdbms/admin/prvtdumi.plb,不然会报错如下:
注3,建立存放DMU repository的表空间:
1 2 3 4 5 |
SQL> create tablespace tbs_csmig datafile '/u01/ora11g/app/oracle/oradata/dmutest/csmig.dbf' size 200m autoextend on; Tablespace created. SQL> |
注4,由于修改字符集是逻辑的修改,可以在备份数据库的情况下,创建一个回滚点,以便在修改失败的情况下,迅速恢复。
1 2 3 4 5 |
SQL> create restore point before_cs_change guarantee flashback database; Restore point created. SQL> |
点击test connection,测试连接是否成功。如果成功,显示如下:
可以看到,如果扫描到有问题的schema,左边会显示一个感叹号。
要查看scan报告,可以点击database scan report。
expand all之后,可以看到我的cs_test和cs_test3有问题。因为这2个表的a字段都是varchar2(4),里面包含两个汉字的行,zhs16gbk是可以满足的,但是转成al32utf8之后,2个汉字会占据6个字符,varchar2(4)就无法容纳了。在这里,我们对于两个表采用不同的修改方式:第一个表用手工清洗,第二个表我们用bulk cleansing放在批量的方式中处理。
我们开始对第一个表进行手工清洗。点击cleansing editor。
然后我们进行修改,点击schedule的modify,这样就能在最后convert阶段修改,而不是当前就修改了。
好了,手工清洗的,我只举了一个例子,由于如果很多表需要修改的话,手工清洗会很麻烦,我们可以采用批量清洗的方式。现在我们就点击bulk cleansing,开始批量清洗。
选择修改成character length semantics,就原来是varchar2(4),会修改成varchar2(4 char)。即按照实际使用的字符计算。
同样也选择schedule,即在convert阶段进行这些操作。
清洗步骤完成后,再次扫描一次database,确认所有的要素都会在convert时被修复。
请注意看conversion step。列出来了convert的各个步骤,我们看到第一个步骤是修改一些参数和禁用一些trigger。以防止在转换过程中触发内部操作。
第五步,是将刚刚bulk cleansing的字段进行修改,并且将在第一步中修改的参数复原。
注意,在右上角,我们可以修改一些convert时的参数。如点击点一个conversion parameters:
我们可以选择并发度(就是第二步的update的语句的并发度),另外还有convert的进程数等等。
也可以看到转换的方式。比如选择只需要convert的行进行转换。
选择完之后,回到原来的界面,可以看到第二步的语句中已经有变化,后面多了where的条件。
点击右边中间的convert按钮,开始正式转换。会提示你最后一次scan是什么时间。
转换结束,要求你断开DMU,重启数据库(因为还在restricted模式)。
转换过程容易踩到的坑是:更新字段set column_a=SYS_OP_CSCONV(column_a,’AL32UTF8′)的时候,只能采用parallel dml的方式,而不能采用分批提交的方式。这就容易造成一个大的事物,占用大量的undo。而且万一更新失败,回滚undo也需要很长时间。我曾经尝试更新1亿多行记录的大表,发现回滚段占据了快10G,撑爆了磁盘空间。而在4个并发的情况下,花了半个多小时也没有完成。
转换后:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
SQL> select * from nls_database_parameters where parameter in ('NLS_LANGUAGE','NLS_TERRITORY','NLS_CHARACTERSET','NLS_NCHAR_CHARACTERSET'); PARAMETER VALUE ------------------------------ ------------------------------ NLS_LANGUAGE AMERICAN NLS_TERRITORY AMERICA NLS_CHARACTERSET AL32UTF8 NLS_NCHAR_CHARACTERSET AL16UTF16 SQL> SQL> desc cs_test Name Null? Type ----------------------------------------------------- -------- ------------------------------------ A VARCHAR2(4 CHAR) SQL> SQL> select dump(a,1016) from cs_test where rownum<=4; DUMP(A,1016) ---------------------------------------------------------------------------------------------------- Typ=1 Len=4 CharacterSet=AL32UTF8: 31,32,33,34 Typ=1 Len=4 CharacterSet=AL32UTF8: 61,62,63,64 Typ=1 Len=6 CharacterSet=AL32UTF8: e5,9b,9e,e5,ae,b6 Typ=1 Len=6 CharacterSet=AL32UTF8: e9,ab,98,e5,85,b4 SQL> select dump(a,1017) from cs_test where rownum<=4; DUMP(A,1017) ---------------------------------------------------------------------------------------------------- Typ=1 Len=4 CharacterSet=AL32UTF8: 1,2,3,4 Typ=1 Len=4 CharacterSet=AL32UTF8: a,b,c,d Typ=1 Len=6 CharacterSet=AL32UTF8: e5,9b,9e,e5,ae,b6 Typ=1 Len=6 CharacterSet=AL32UTF8: e9,ab,98,e5,85,b4 SQL> |
下面我们来看一下在新数据库中的字符是否显示正确。
注,如果看不到正确的数据,可能和以下因素有关。
(1)客户端操作系统不支持显示中文。
(2)Oracle客户端工具不支持显示中文。
(3)Oracle客户端有相关设置(比如NLS_LANG)不正确。
(4)存储在数据库中的数据已经是不正确的数据。
我的客户端是SecureCRT,操作系统是win7,操作系统支持中文。客户端也支持中文,客户端的nls_lang是继承操作系统,由于下面的设置是default:
所以就继承了OS的编码,操作系统的page code是936,即gbk,所以要设置客户端的nls_lang为gbk。
如果不设置为gbk,就犯了上面的第三条,就会报错。见下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 |
windows环境字符集: C:\Users\jijihe>chcp Active code page: 936 即GBK的字符集 而我的SecureCRT由于是设置default是继承操作系统的,所以也是GBK。要正确显示数据库中的字符,需要也设置成gbk SecureCRT登录数据库后: [oracle11g@testdb2 ~]$ env |grep -i lang LANG=en_US.UTF-8 [oracle11g@testdb2 ~]$ [oracle11g@testdb2 ~]$ 如果设置成al32utf8,此时就会乱码: [oracle11g@testdb2 ~]$ export NLS_LANG=american_america.al32utf8 [oracle11g@testdb2 ~]$ [oracle11g@testdb2 ~]$ [oracle11g@testdb2 ~]$ sqlplus "/ as sysdba" SQL*Plus: Release 11.2.0.4.0 Production on Wed Jun 22 18:12:18 2016 Copyright (c) 1982, 2013, Oracle. All rights reserved. Connected to: Oracle Database 11g Enterprise Edition Release 11.2.0.4.0 - 64bit Production With the Partitioning, Oracle Label Security, OLAP, Data Mining and Real Application Testing options SQL> select * from nls_database_parameters where parameter in ('NLS_LANGUAGE','NLS_TERRITORY','NLS_CHARACTERSET','NLS_NCHAR_CHARACTERSET') SQL> / PARAMETER VALUE ------------------------------ ------------------------------ NLS_LANGUAGE AMERICAN NLS_TERRITORY AMERICA NLS_CHARACTERSET AL32UTF8 NLS_NCHAR_CHARACTERSET AL16UTF16 SQL> SQL> select a,dump(a,1016) from test.cs_test where rownum<=5 SQL> / A DUMP(A,1016) ------ -------------------------------------------------------------------------------- 1234 Typ=1 Len=4 CharacterSet=AL32UTF8: 31,32,33,34 abcd Typ=1 Len=4 CharacterSet=AL32UTF8: 61,62,63,64 鍥炲 Typ=1 Len=6 CharacterSet=AL32UTF8: e5,9b,9e,e5,ae,b6 <<<<<<<< 乱码 楂樺叴 Typ=1 Len=6 CharacterSet=AL32UTF8: e9,ab,98,e5,85,b4 <<<<<<<< 乱码 濂界帺 Typ=1 Len=6 CharacterSet=AL32UTF8: e5,a5,bd,e7,8e,a9 <<<<<<<< 乱码 SQL> 如果设置成gbk,才能正确显示: [oracle11g@testdb2 ~]$ export NLS_LANG=american_america.zhs16gbk [oracle11g@testdb2 ~]$ sqlplus "/ as sysdba" SQL*Plus: Release 11.2.0.4.0 Production on Wed Jun 22 18:13:56 2016 Copyright (c) 1982, 2013, Oracle. All rights reserved. Connected to: Oracle Database 11g Enterprise Edition Release 11.2.0.4.0 - 64bit Production With the Partitioning, Oracle Label Security, OLAP, Data Mining and Real Application Testing options SQL> col dump(a,1016) for a80 SQL> select a,dump(a,1016) from test.cs_test where rownum<=5 SQL> / A DUMP(A,1016) ------------ -------------------------------------------------------------------------------- 1234 Typ=1 Len=4 CharacterSet=AL32UTF8: 31,32,33,34 abcd Typ=1 Len=4 CharacterSet=AL32UTF8: 61,62,63,64 回家 Typ=1 Len=6 CharacterSet=AL32UTF8: e5,9b,9e,e5,ae,b6 高兴 Typ=1 Len=6 CharacterSet=AL32UTF8: e9,ab,98,e5,85,b4 好玩 Typ=1 Len=6 CharacterSet=AL32UTF8: e5,a5,bd,e7,8e,a9 SQL> |
总体来说nls_lang的作用是告诉oracle数据库服务器,当前的客户端用的是哪个字符集,是否需要转码。
设置nls_lang要看客户端(或者说工具端)的设置。有些客户端自己包含字符集(如adobe的一些产品,如ebs的产品,如peoplesoft的产品),有些客户端是继承操作系统的字符集,有些客户端是包含多个字符集,可以选择。要数据库服务器能正确转码,客户端登录数据库前,需要将nls_lang设置成客户端自己的字符集或者继承的字符集。
网上说要设置客户端的nls_lang和数据库端的字符集一样,并不是一种准确的说法。Oracle在文档中也提到这种做法NOT ALWAYS correct:
1 2 |
* Setting the NLS_LANG to the characterset of the database (NLS_CHARACTERSET) MAY be correct but IS NOT ALWAYS correct. Please DO NOT assume that NLS_LANG needs to be ALWAYS the same as the database characterset. THIS IS NOT TRUE. * The characterset defined with the NLS_LANG parameter does NOT CHANGE your client's characterset, it is used to let Oracle know what characterset you are USING on the client side, so Oracle can do the proper conversion between the client's encoding and the NLS_CHARACTERSET. |
参考:『Oracle实例解析:编码与字符集』或者The correct NLS_LANG in a Windows Environment [ID 179133.1]和The correct NLS_LANG setting in Unix Environments [ID 264157.1]
注:本文基于How to Migrate a WE8ISO8859P1 DB to AL32UTF8 using DMU 1.2 an example (Doc ID 1546507.1)进行的转换测试。