Oracle全球支持(即Globalization Support)允许我们使用本地语言和格式来存储和检索数据。通过全球支持,Oracle可以支持多种语言及字符集,得以展示数据库的强大魅力。
由于不同语言及字符集的共同存储存在设置上具有一定的复杂性,字符集一度成为普遍困扰大家的一个主要问题。
本文试图就一些常见问题进行探讨,希望可以把一些实际经验共享给大家!
1. 字符集的基本知识
如果从头说起,字符集最早的编码方案来自于与ASCII.
这也是我们最常见的编码方式。该方案起源于1960年代初期,最初是美国国会图书馆制定用来作为美国图书馆界书目交换的共同标准,最后完善成为美国的国家标准ASCII(American Standard Codefor Information Interchange),之后进一步演变成世界性的计算机字符编码标准ISO646(其全名为7-bit coded character set for information interchange)。成为计算机编码方案的基础。
Oracle数据库最早支持的编码方案也就是US7ASCII.
但是我们知道,英文字符一般是以一个字节来存储的,7位的编码方案最多只能代表128个字符;经过扩展的8位的编码方案也只能代表256个字符,这远远不能满足计算机发展的需要,对于亚洲国家复杂的字符存储需要更多的码位,于是各种编码方案随之而生。
为了容纳全世界各种语言的所有字符和符号,解决不同编码之间的兼容和转换问题,1991年元月,10多家公司共同出资,组建Unicode协会,随后Unicode编码产生了。
Unicode协会的口号是: 给每个字符提供了一个唯一的数字,不论是什么平台,不论是什么程序,不论什么语言。
最初Unicode编码使用2-Byte(16bit)来进行编码,但是最多只能容纳65536个字符,仍然不够使用,后来进行了扩充,也就是Unicode3.1标准,增加了额外的补充字符定义,现在Unicode4.0标准已经发布,具体可以参考Unicode官方站点:
www.unicode.org
Unicode编码方案主要有三个实施标准:
UTF-8
USC-2
UTF-16
Oracle从7.2开始支持UTF-8编码,提供Unicode编码支持。
按照各种标准的含义,Oracle推荐,如果你的数据库需要存放不同语言的不同符号和字符,建议使用Unicode编码方案。诚然,Unicode方案可以表示更多的字符,但是由于多位的存储,需要额外的存储空间和网络传输,所以选择最适合的数据库字符集仍然需要慎重考虑。
2. 数据库的字符集
一旦你的字符集选定了,数据库中能够存储的字符就受到了限制,所以你选择的字符集的应该可以容纳所有你将用到字符。
常见的中文字符集有:
ZHS16CGB231280 CGB2312-80 16-bit Simplified Chinese MB, ASCIIZHS16GBK GBK 16-bit Simplified Chinese MB, ASCII, UDC
其中GB2312码是中华人民共和国国家汉字信息交换用编码,全称《信息交换用汉字编码字符集--基本集》,由国家标准总局发布,
1981年5月1日实施,通行于大陆。新加坡等地也使用此编码。
GBK编码是1995年12月颁布的指导性规范。
GBK与国家标准 GB 2312-80 信息处理交换码所对应的、事实上的内码标准兼容;同时,在字汇一级支持 ISO/IEC 10646-1 和
GB 13000-1 的全部中日韩 (CJK) 汉字(20902字)。包含了更多的编码。
但是我们说,ZHS16GBK 并非是ZHS16CGB231280的严格超集(虽然后者的汉字在前者中都存在,但是同样的编码在不同两个
字符集中可能表达不同的汉字),所以在做数据库字符转换时仍然需要特别注意。
Oracle的字符集命名遵循以下命名规则:
需要说明的是,有些字符集命名违背了这个规范,Oracle8/Oralce8i中的UTF-8是第一个打破这个命名规范的字符集。
我们可以看到一类字符集以 AL开头,如:
AL16UTF16
其中 AL代表 ALL,指适用于所有语言(All Languages),按照这个标准当年UTF-8本应被命名为AL24UTF8。
3. 字符集的更改
数据库创建以后,如果需要修改字符集,通常需要重建数据库,通过导入导出的方式来转换。
我们也可以通过以下方式更改
ALTER DATABASE CHARACTER SET
注意:修改数据库字符集时必须谨慎,修改之前一定要为数据库备份。由于不能回退这项操作,因此可能会造成数据丢失或者损坏。
这是最简单的转换字符集的方式,但并不总是有效。
这个命令在Oracle8时被引入Oracle,这个操作在本质上并不转换任何数据库字符,只是简单的更新数据库中所有跟字符集相关的信息。
这意味着,你只能在新字符集是旧字符集严格超集的情况下使用这种方式转换。
所谓超集是指:
当前字符集中的每一个字符在新字符集中都可以表示,并使用同样的代码点
比如很多字符集都是US7ASCII的严格超集。
如果不是超集,将获得以下错误:
SQL> ALTER DATABASE CHARACTER SET ZHS16CGB231280;ALTER DATABASE CHARACTER SET ZHS16CGB231280*ERROR at line 1:ORA-12712: new character set must be a superset of old character set
下面我们来看一个测试(以下测试在Oracle9.2.0下进行,Oracle9i较Oracle8i在编码方面有较大改变,在Oracle8i中,测试结果可能略有不同):
SQL> select name,value$ from props$ where name like ’%NLS%’;
NAME VALUE$
------------------------------ ------------------------------
NLS_LANGUAGE AMERICAN
NLS_TERRITORY AMERICA
NLS_CURRENCY $
NLS_ISO_CURRENCY AMERICA
NLS_NUMERIC_CHARACTERS .,
NLS_CHARACTERSET US7ASCII
NLS_CALENDAR GREGORIAN
NLS_DATE_FORMAT DD-MON-RR
NLS_DATE_LANGUAGE AMERICAN
……………….
NLS_NCHAR_CHARACTERSET AL16UTF16
NLS_RDBMS_VERSION 9.2.0.4.0
20 rows selected.
SQL> select name,dump(name) from eygle.test;
NAME DUMP(NAME)
------------------------------------------------------
测试 Typ=1 Len=4: 178,226,202,212
Test Typ=1 Len=4: 116,101,115,116
2 rows selected.
转换字符集,数据库应该在RESTRICTED模式下进行.
c:\>sqlplus "/ as sysdba"
SQL*Plus: Release 9.2.0.4.0 - Production on Sat Nov 1 10:52:30 2003
Copyright (c) 1982, 2002, Oracle Corporation. All rights reserved.
Connected to:
Oracle9i Enterprise Edition Release 9.2.0.4.0 - Production
With the Partitioning, Oracle Label Security, OLAP and Oracle Data Mining options
JServer Release 9.2.0.4.0 - Production
SQL> shutdown immediate
Database closed.
Database dismounted.
ORACLE instance shut down.
SQL> STARTUP MOUNT;
ORACLE instance started.
Total System Global Area 76619308 bytes
Fixed Size 454188 bytes
Variable Size 58720256 bytes
Database Buffers 16777216 bytes
Redo Buffers 667648 bytes
Database mounted.
SQL> ALTER SESSION SET SQL_TRACE=TRUE;
Session altered.
SQL> ALTER SYSTEM ENABLE RESTRICTED SESSION;
System altered.
SQL> ALTER SYSTEM SET JOB_QUEUE_PROCESSES=0;
System altered.
SQL> ALTER SYSTEM SET AQ_TM_PROCESSES=0;
System altered.
SQL> ALTER DATABASE OPEN;
Database altered.
SQL> set linesize 120
SQL> ALTER DATABASE CHARACTER SET ZHS16GBK;
ALTER DATABASE CHARACTER SET ZHS16GBK
*
ERROR at line 1:
ORA-12721: operation cannot execute when other sessions are active
SQL> ALTER DATABASE CHARACTER SET ZHS16GBK;
ALTER DATABASE CHARACTER SET ZHS16GBK
*
ERROR at line 1:
ORA-12716: Cannot ALTER DATABASE CHARACTER SET when CLOB data exists
在Oracle9i中,如果数据库存在CLOB类型字段,那么就不允许对字符集进行转换
SQL>
这时候,我们可以去查看alert<sid>.log日志文件,看CLOB字段存在于哪些表上:
ALTER DATABASE CHARACTER SET ZHS16GBK SYS.METASTYLESHEET (STYLESHEET) - CLOB populatedORA-12716 signalled during: ALTER DATABASE CHARACTER SET ZHS16GBK...
对于不同情况,Oracle提供不同的解决方案,如果是用户数据表,一般我们可以把包含CLOB字段的表导出,然后drop掉相关对象,
转换后再导入数据库;对于系统表,可以按照以下方式处理:
SQL> truncate table Metastylesheet;
Table truncated.
然后可以继续进行转换!
SQL> ALTER SESSION SET SQL_TRACE=TRUE;
Session altered.
SQL> ALTER DATABASE CHARACTER SET ZHS16GBK;
Database altered.
SQL> ALTER SESSION SET SQL_TRACE=FALSE;
Session altered.
在9.2.0中,转换完成以后,可以通过运行catmet.sql脚本来重建Metastylesheet表:
SQL> @?/rdbms/admin/catmet.sql
转换后的数据:
SQL> select name,value$ from props$ where name like ’%NLS%’;
NAME VALUE$
------------------------------ ------------------------------
NLS_LANGUAGE AMERICAN
NLS_TERRITORY AMERICA
NLS_CURRENCY $
NLS_ISO_CURRENCY AMERICA
NLS_NUMERIC_CHARACTERS .,
NLS_CHARACTERSET ZHS16GBK
…..
NLS_NCHAR_CHARACTERSET AL16UTF16
NLS_RDBMS_VERSION 9.2.0.4.0
20 rows selected.
SQL> select * from eygle.test;
NAME
------------------------------
测试
test
2 rows selected.
提示:
通过设置sql_trace,我们可以跟踪很多数据库的后台操作,这个工具是DBA常用的“利器”之一。
我们简单看一下数据库更改字符集时的后台处理,我提取了主要的更新部分。
通过以下跟踪过程,我们看到数据库在更改字符集的时候,主要更新了12张数据字典表,修改了数据库的原数据,这也证实了我们以前的说法:
这个更改字符集的操作在本质上并不转换任何数据库字符,只是简单的更新数据库中所有跟字符集相关的信息。
update col$ set charsetid = :1
where
charsetform = :2
update argument$ set charsetid = :1
where
charsetform = :2
update collection$ set charsetid = :1
where
charsetform = :2
update attribute$ set charsetid = :1
where
charsetform = :2
update parameter$ set charsetid = :1
where
charsetform = :2
update result$ set charsetid = :1
where
charsetform = :2
update partcol$ set spare1 = :1
where
charsetform = :2
update subpartcol$ set spare1 = :1
where
charsetform = :2
update props$ set value$ = :1
where
name = :2
update "SYS"."KOTAD$" set SYS_NC_ROWINFO$ = :1
where
SYS_NC_OID$ = :2
update seq$ set increment$=:2,minvalue=:3,maxvalue=:4,cycle#=:5,order$=:6,
cache=:7,highwater=:8,audit$=:9,flags=:10
where
obj#=:1
update kopm$ set metadata = :1, length = :2
where
name=’DB_FDO’
在这里我们顺便纠正一个由来以及的错误方法.
经常可以在网上看到这样的更改字符集的方法:
1)用SYS用户名登陆ORACLE。
2)查看字符集内容
SQL>SELECT * FROM PROPS$;
3)修改字符集
SQL> update props$ set value$=’新字符集’ where name=’NLS_CHARACTERSET’
4) COMMIT;
我们看到很多人在这个问题上遇到了惨痛的教训,使用这种方式更改字符集,如果你的value$值输入了不正确的字符集,在8i中那么你
的数据库可能会无法启动,这种情况是非常严重的,有时候你必须从备份中进行恢复;如果是在9i中,可以重新启动数据库后再修改回正
确的字符集。但是我们仍然不建议使用这种方式进行任何数据库修改,这是一种极其危险的操作。
实际上当我们更新了字符集,数据库启动时会根据数据库的字符集自动的来修改控制文件的字符集,如果字符集可以识别,更新控制文
件字符集等于数据库字符集;如果字符集不可识别,那么控制文件字符集更新为US7ASCII.
通过更新props$表的方式修改字符集,在Oracle7之后就不应该被使用.
以下是我的测试结果,但是严禁一切不备份的修改研究,即使是对测试库的。
SQL> update props$ set value$=’EYGLE’ where name=’NLS_CHARACTERSET’;
1 row updated.
SQL> commit;
Commit complete.
SQL> select name,value$ from props$ where name like ’%NLS%’;
NAME VALUE$
------------------------------ -----------------------------------
NLS_LANGUAGE AMERICAN
NLS_TERRITORY AMERICA
NLS_CURRENCY $
NLS_ISO_CURRENCY AMERICA
NLS_NUMERIC_CHARACTERS .,
NLS_CHARACTERSET EYGLE
NLS_CALENDAR GREGORIAN
NLS_DATE_FORMAT DD-MON-RR
NLS_DATE_LANGUAGE AMERICAN
….
NLS_NCHAR_CHARACTERSET ZHS16GBK
NLS_RDBMS_VERSION 8.1.7.1.1
18 rows selected.
重新启动数据库,发现alert.log文件中记录如下操作:
Mon Nov 03 16:11:35 2003
Updating character set in controlfile to US7ASCII
Completed: ALTER DATABASE OPEN
启动数据库后恢复字符集设置:
SQL> update props$ set value$=’ZHS16GBK’ where name=’NLS_CHARACTERSET’;
1 row updated.
SQL> commit;
Commit complete.
SQL> select name,value$ from props$ where name like ’%NLS%’;
NAME VALUE$
------------------------------ -----------------------------------
NLS_LANGUAGE AMERICAN
NLS_TERRITORY AMERICA
NLS_CURRENCY $
NLS_ISO_CURRENCY AMERICA
NLS_NUMERIC_CHARACTERS .,
NLS_CHARACTERSET ZHS16GBK
NLS_CALENDAR GREGORIAN
NLS_DATE_FORMAT DD-MON-RR
NLS_DATE_LANGUAGE AMERICAN
………
NLS_COMP BINARY
NLS_NCHAR_CHARACTERSET ZHS16GBK
NLS_RDBMS_VERSION 8.1.7.1.1
18 rows selected.
重新启动数据库后,发现控制文件的字符集被更新:
Mon Nov 03 16:21:41 2003
Updating character set in controlfile to ZHS16GBK
Completed: ALTER DATABASE OPEN
理解了字符集调整的内部操作以后,我们可以轻易的指出,以上的方法是不正确的,通过前面 ” ALTER DATABASE CHARACTER SET” 方式更改字
符集时,Oracle至少需要更改12张数据字典表,而这种直接更新props$表的方式只完成了其中十二分之一的工作,潜在的完整性隐患是可想而知的。
所以,更改字符集尽量要使用正常的途径。
4. 导入导出及转换
导入导出是我们常用的一个数据迁移及转化工具,因其导出文件具有平台无关性,所以在跨平台迁移中,最为常用。
在导出操作时,非常重要的是客户端的字符集设置,也就是客户端的NLS_LANG设置。
NLS_LANG参数由以下部分组成:
NLS_LANG=<Language>_<Territory>.<Clients Characterset>
NLS_LANG各部分含义如下:
LANGUAGE指定:
-Oracle消息使用的语言
-日期中月份和日显示
TERRITORY指定
-货币和数字格式
-地区和计算星期及日期的习惯
CHARACTERSET:
-控制客户端应用程序使用的字符集
通常设置或者等于客户端(如Windows)代码页
或者对于unicode应用设置为UTF8
在Windows上查看当前系统的代码页可以使用chcp命令:
E:\>chcp
活动的代码页: 936
代码页936也就是中文字符集 GBK,在Microsoft的官方站点上,我们可以遭到关于936代码页的具体编码规则,请参考以下链接:
http://www.microsoft.com/globaldev/reference/dbcs/936.htm
我们看一个简单的测试,来了解一下这几个参数的作用:
E:\>set NLS_LANG=SIMPLIFIED CHINESE_CHINA.ZHS16GBK
E:\>sqlplus "/ as sysdba"
SQL*Plus: Release 9.2.0.4.0 - Production on 星期六 11月 1 22:51:59 2003
Copyright (c) 1982, 2002, Oracle Corporation. All rights reserved.
连接到:
Oracle9i Enterprise Edition Release 9.2.0.4.0 - Production
With the Partitioning, Oracle Label Security, OLAP and Oracle Data Mining options
JServer Release 9.2.0.4.0 - Production
SQL> select sysdate from dual;
SYSDATE
----------
01-11月-03
已选择 1 行。
SQL> exit
从Oracle9i Enterprise Edition Release 9.2.0.4.0 - Production
With the Partitioning, Oracle Label Security, OLAP and Oracle Data Mining options
JServer Release 9.2.0.4.0 - Production中断开
E:\>set NLS_LANG=AMERICAN_AMERICA.ZHS16GBK
E:\>sqlplus "/ as sysdba"
SQL*Plus: Release 9.2.0.4.0 - Production on Sat Nov 1 22:52:24 2003
Copyright (c) 1982, 2002, Oracle Corporation. All rights reserved.
Connected to:
Oracle9i Enterprise Edition Release 9.2.0.4.0 - Production
With the Partitioning, Oracle Label Security, OLAP and Oracle Data Mining options
JServer Release 9.2.0.4.0 - Production
SQL> select sysdate from dual;
SYSDATE
---------
01-NOV-03
1 row selected.
SQL>
查看客户端NLS_LANG设置可以使用以下方法:
Windows使用:
echo %NLS_LANG%
如:
E:\>echo %NLS_LANG%
AMERICAN_AMERICA.ZHS16GBK
Unix使用:
env|grep NLS_LANG
如:
/opt/oracle>env|grep NLS_LANG
NLS_LANG=AMERICAN_CHINA.ZHS16GBK
Windows客户端设置,可以在注册表中更改NLS_LANG,具体键值位于:
HKEY_LOCAL_MACHINE\SOFTWARE\ORACLE\HOMExx\
xx指存在多个ORACLE_HOME时系统编号。
导入和导出是客户端产品,同SQL*PLUS和Oralce Forms一样,因此,使用EXP/IMP工具将按照NLS_LANG定义的方式转换字符集。
导出使用的字符集将会记录在导出文件中,当文件导入时,将会检查导出时使用的字符集设置,如果这个字符集不同于导入客户端的NLS_LANG
设置,字符集将根据导入客户端NLS_LANG设置进行转换,如果必要,在数据插入数据库之前会进行进一步转换。
通常在导出时最好把客户端字符集设置得和数据库端相同,这样可以避免在导出时发生不必要的数据转换,导出文件将和数据库具有相同的字符集。
即使将来会把导出文件导入到不同字符集的数据库中,这样做也可以把转换延缓至导入时刻。
当进行数据导入时,主要存在以下两种情况:
1.源数据库和目标数据库具有相同字符集设置
这时,只需要设置NLS_LANG等于数据库字符集即可导入(前提是,导出使用的是和源数据库相同字符集,即三者相同)
2.源数据库和目标数据库字符集不同
如果我们导出时候使用的NLS_LANG是和源数据库相同的字符集,那么导入时就可以设置客户端NLS_LANG等于导出时使用的字符集,这
样转换只发生在数据库端,而且只发生一次。
例如:
如果进行从WE8MSWIN1252到UTF8的转换
1)使用NLS_LANG=AMERICAN_AMERICA.WE8MSWIN1252导出数据库。
这时创建的导出文件包含WE8MSWIN1252的数据
2)导入时使用NLS_LANG=AMERICAN_AMERICA.WE8MSWIN1252
这时转换仅发生在insert数据到UTF8的数据库中。
以上假设的转换只在目标数据库字符集是源数据库字符集的超集时才能转换。如果不同,一般就需要进行一些特殊的处理。
我们简单看一下导入的转换过程(以Oracle8i为例):
1.确定导出数据库字符集环境
通过读取导出文件头,可以获得导出文件的字符集设置
2.确定导入session的字符集,即导入Session使用的NLS_LANG环境变量
3.IMP读取导出文件
读取导出文件字符集ID,和导入进程的NLS_LANG进行比较
4.如果导出文件字符集和导入Session字符集相同,那么在这一步骤内就不需要转换
如果不同,就需要把数据转换为导入Session使用的字符集。
然而这种转换只能在单byte字符集之间进行。
我们看一个测试:
E:\nls2>set NLS_LANG=AMERICAN_AMERICA.US7ASCII
设置导入session NLS_LANG为US7ASCII
E:\nls2>e:\oracle\ora8i\bin\imp eygle/eygle file=Sus7ascii-Cus7ascii-exp817.dmp fromuser=eygle touser=eygle tables=test
这个导出文件是从US7ASCII数据库导出,导出客户端NLS_LANG也是US7ASCII
Import: Release 8.1.7.1.1 - Production on Fri Nov 7 00:59:22 2003
(c) Copyright 2000 Oracle Corporation. All rights reserved.
Connected to: Oracle8i Enterprise Edition Release 8.1.7.1.1 - Production
With the Partitioning option
JServer Release 8.1.7.1.1 - Production
这时导入,在DMP文件和NLS_LANG之间不需要进行字符集转换。
Export file created by EXPORT:V08.01.07 via conventional path
import done in US7ASCII character set and ZHS16GBK NCHAR character set
import server uses ZHS16GBK character set (possible charset conversion)
export server uses UTF8 NCHAR character set (possible ncharset conversion)
. . importing table "TEST" 2 rows imported
Import terminated successfully without warnings.
5.对于多Byte字符集的导入(如:UTF8)
需要设置导入Session字符集和导出字符集相同
否则就会遇到:IMP-16 "Required character set conversion (type %lu to %lu) not supported" 错误。
:
E:\nls2>set NLS_LANG=AMERICAN_AMERICA.ZHS16GBK
导入Session字符集设置为ZHS16GBK
导入US7ASCII的导出文件
E:\nls2>e:\oracle\ora8i\bin\imp eygle/eygle file=Sus7ascii-Cus7ascii-exp817.dmp fromuser=eygle touser=eygle
Import: Release 8.1.7.1.1 - Production on Fri Nov 7 00:38:55 2003
(c) Copyright 2000 Oracle Corporation. All rights reserved.
Connected to: Oracle8i Enterprise Edition Release 8.1.7.1.1 - Production
With the Partitioning option
JServer Release 8.1.7.1.1 - Production
IMP-00016: required character set conversion (type 1 to 852) not supported
IMP-00000: Import terminated unsuccessfully
在从导出文件US7ASCII到导入 NLS_LANG设置为ZHS16GBK的过程中,不支持单Byte字符集向多Byte转换,报出以上错误。
6.导入Session字符集应该是导出字符集的超级,否则,专有的字符将难以正确转换。
7.当数据转换为导入Session字符集设置以后,如果导入Session字符集不同于导入数据库字符集,这时还需要最后一步转换,这要求导入数据库字符
集是导入session字符集的超级,否则某些专有字符将不能正常转换。
我们继续看上面的两个过程,这里有这样两个原则:
1.如果NLS_LANG的设置和数据库相同,那么数据(在传输过程中当然是2进制码)不经过转换就直接插入数据库中。
2.如果NLS_LANG的设置和数据库不同,那么数据需要转换后才能插入数据库中。
我们再回头来看上面的第一个例子:
:
Export file created by EXPORT:V08.01.07 via conventional path
import done in US7ASCII character set and ZHS16GBK NCHAR character set
import server uses ZHS16GBK character set (possible charset conversion)
export server uses UTF8 NCHAR character set (possible ncharset conversion)
. . importing table "TEST" 2 rows imported
Import terminated successfully without warnings.
这时候经过第一步转换后的数据,US7ASCII到ZHS16GBK丢失首位,原样插入数据库,我们看到这时数据库中存放的就是错误的字符(在后面部分我们做了详细的转换):
E:\nls2>sqlplus eygle/eygle
SQL*Plus: Release 9.2.0.4.0 - Production on Fri Nov 7 00:35:39 2003
Copyright (c) 1982, 2002, Oracle Corporation. All rights reserved.
Connected to:
Oracle8i Enterprise Edition Release 8.1.7.1.1 - Production
With the Partitioning option
JServer Release 8.1.7.1.1 - Production
SQL> select * from test;
NAME
--------------------
2bJT
test
在Oracle9i中,以上情况略有不同。