对Oracle数据库的字符集问题的资料收集,受益匪浅

我对字符集一直比较头大,在一开始使用jsp的时候就发现不能使用中文。当时在网上搜了搜,什么UTF8,Unicode,GB2312,GBK之类的,看了半天都不懂。现在使用的pentaho本身是国外的东西,对英文支持比较好,小老板要求我界面上不能有一个英文字母,这下就要了我的命了。发现字符集这个问题躲是躲不掉的,还是静下心来好好研究研究字符集吧。
----------------------------------------------------------------------------
Oracle数据库字符集问题解析

经常看到一些朋友问ORACLE字符集方面的问题,我想以迭代的方式来介绍一下。

第一次迭代:掌握字符集方面的基本概念。 
有些朋友可能会认为这是多此一举,但实际上正是由于对相关基本概念把握不清,才导致了诸多问题和疑问。
首先是字符集的概念。
我们知道,电子计算机最初是用来进行科学计算的(所以叫做“计算机”),但随着技术的发展,还需要计算机进行其它方面的应用处理。这就要求计算机不仅能处理数值,还能处理诸如文字、特殊符号等其它信息,而计算机本身能直接处理的只有数值信息,所以就要求对这些文字、符号信息进行数值编码,最初的字符集是我们都非常熟悉的ASCII,它是用7个二进制位来表示128个字符,而后来随着不同国家、组织的需要,出现了许许多多的字符集,如表示西欧字符的ISO8859系列的字符集,表示汉字的GB2312-80、GBK等字符集。
字符集的实质就是对一组特定的符号,分别赋予不同的数值编码,以便于计算机的处理。
字符集之间的转换。字符集多了,就会带来一个问题,比如一个字符,在某一字符集中被编码为一个数值,而在另一个字符集中被编码为另一个数值,比如我来创造两个字符集demo_charset1与demo_charset2,在demo_charset1中,我规定了三个符号的编码为:A(0001),B(0010),?(1111);而在demo_charset2中,我也规定了三个符号的编码为:A(1001),C(1011),?(1111),这时我接到一个任务,要编写一个程序,负责在demo_charset1与demo_charset2之间进行转换。由于知道两个字符集的编码规则,对于demo_charset1中的0001,在转换为demo_charset2时,要将其编码改为1001;对于demo_charset1中的1111,转换为demo_charset2时,其数值不变;而对于demo_charset1中的0010,其对应的字符为B,但在demo_charset2没有对应的字符,所以从理论上无法转换,对于所有这类无法转换的情况,我们可以将它们统一转换为目标字符集中的一个特殊字符(称为“替换字符”),比如在这里我们可以将?作为替换字符,所以B就转换为了?,出现了信息的丢失;同样道理,将demo_charset2的C字符转换到demo_charset1时,也会出现信息丢失。
所以说,在字符集转换过程中,如果源字符集中的某个字符在目标字符集中没有定义,将会出现信息丢失。
数据库字符集的选择。
我们在创建数据库时,需要考虑的一个问题就是选择什么字符集与国家字符集(通过create database中的CHARACTER SET与NATIONAL CHARACTER SET子句指定)。考虑这个问题,我们必须要清楚数据库中都需要存储什么数据,如果只需要存储英文信息,那么选择US7ASCII作为字符集就可以;但是如果要存储中文,那么我们就需要选择能够支持中文的字符集(如ZHS16GBK);如果需要存储多国语言文字,那就要选择UTF8了。
数据库字符集的确定,实际上说明这个数据库所能处理的字符的集合及其编码方式,由于字符集选定后再进行更改会有诸多的限制,所以在数据库创建时一定要考虑清楚后再选择。
而我们许多朋友在创建数据库时,不考虑清楚,往往选择一个默认的字符集,如WE8ISO8859P1或US7ASCII,而这两个字符集都没有汉字编码,所以 用这种字符集存储汉字信息从原则上说就是错误的。虽然在有些时候选用这种字符集好象也能正常使用,但它会给数据库的使用与维护带来一系列的麻烦,在后面的迭代过程中我们将深入分析。
客户端的字符集。
有过一些Oracle使用经验的朋友,大多会知道通过NLS_LANG来设置客户端的情况,NLS_LANG由以下部分组成:NLS_LANG=<Language>_<Territory>.<Clients Characterset>,其中第三部分<Clients Characterset>的本意就是用来指明客户端操作系统缺省使用的字符集。所以按正规的用法,NLS_LANG应该按照客户端机器的实际情况进行配置,尤其对于字符集一项更是如此,这样Oracle就能够在最大程度上实现数据库字符集与客户端字符集的自动转换(当然是如果需要转换的话)。

总结一下第一次迭代的重点:

字符集:将特定的符号集编码为计算机能够处理的数值;
字符集间的转换:对于在源字符集与目标字符集都存在的符号,理论上转换将不会产生信息丢失;而对于在源字符集中存在而在目标字符集中不存在的符号,理论上转换将会产生信息丢失;
数据库字符集:选择能够包含所有将要存储的信息符号的字符集;
客户端字符集设置:指明客户端操作系统缺省使用的字符集。

第二次迭代:通过实例加深对基本概念的理解

下面我将引用网友tellin在ITPUB上发表的“CHARACTER SET研究及疑问”帖子,该朋友在帖子中列举了他做的相关实验,并对实验结果提出了一些疑问,我将对他的实验结果进行分析,并回答他的疑问。
实验结果分析一

QUOTE:
最初由 tellin 发布
设置客户端字符集为US7ASCII 
D:\>SET NLS_LANG=AMERICAN_AMERICA.US7ASCII
查看服务器字符集为US7ASCII 
SQL> SELECT * FROM NLS_DATABASE_PARAMETERS;
PARAMETER                      VALUE
------------------------------ ----------------------------------------
NLS_CHARACTERSET                US7ASCII 

 建立测试表
SQL> CREATE TABLE TEST (R1 VARCHAR2(10));

Table created.

 插入数据
SQL> INSERT INTO TEST VALUES('东北');

1 row created.

SQL> SELECT * FROM TEST;

R1
----------
东北

SQL> EXIT



这一部分的实验数据的存取与显示都正确,好象没什么问题,但实际上却隐藏着很大的隐患。
首先,要将汉字存入数据库,而将数据库字符集设置为US7ASCII是不合适的。US7ASCII字符集只定义了128个符号,并不支持汉字。另外,由于在SQL*PLUS中能够输入中文,操作系统缺省应该是支持中文的,但在NLS_LANG中的字符集设置为US7ASCII,显然也是不正确的,它没有反映客户端的实际情况。
但实际显示却是正确的,这主要是因为Oracle检查数据库与客户端的字符集设置是同样的,那么数据在客户与数据库之间的存取过程中将不发生任何转换。具体地说,在客户端输入“东北”,“东”的汉字的编码为182(10110110)、171(10101011),“北”汉字的编码为177(10110001)、177(10110001),它们将不做任何变化的存入数据库中,但是这实际上导致了 数据库标识的字符集与实际存入的内容是不相符的,从某种意义上讲,这也是一种不一致性,也是一种错误。而在SELECT的过程中,Oracle同样检查发现数据库与客户端的字符集设置是相同的,所以它也将存入的内容原封不动地传送到客户端,而客户端操作系统识别出这是汉字编码所以能够正确显示。
在这个例子中,数据库与客户端的设置都有问题,但却好象起到了“负负得正”的效果,从应用的角度看倒好象没问题。但这里面却存在着极大的隐患,比如在应用length或substr等字符串函数时,就可能得到意外的结果。另外,如果遇到导入/导出(import /export)将会遇到更大的麻烦。有些朋友在这方面做了大量的测试,如eygle研究了“源数据库字符集为US7ASCII,导出文件字符集为US7ASCII或ZHS16GBK,目标数据库字符集为ZHS16GBK”的情况,他得出的结论是 “如果的是在Oracle92中,我们发现对于这种情况,不论怎样处理,这个导出文件都无法正确导入到Oracle9i数据库中”、“对于这种情况,我们可以通过使用Oracle8i的导出工具,设置导出字符集为US7ASCII,导出后修改第二、三字符,修改 0001 为0354,这样就可以将US7ASCII字符集的数据正确导入到ZHS16GBK的数据库中”。我想对于这些结论,这样理解可能更合适一些: 由于ZHS16GBK字符集是US7ASCII的超级,所以如果按正常操作,这种转换应该没有问题;但出现问题的本质是我们让本应只存储英文字符的US7ASCII数据库,非常规地存储了中文信息,那么在转化过程中出现错误或麻烦就没什么奇怪的了,不出麻烦倒是有些奇怪了。
所以说要避免这种情况,就是要在建立数据库时选择合适的字符集,不让标签(数据库的字符集设置)与实际(数据库中实际存储的信息)不符的情况发生。

实验结果分析二



QUOTE:
更改客户端字符集为ZHS16GBK
D:\>SET NLS_LANG=AMERICAN_AMERICA.ZHS16GBK

D:\>SQLPLUS "/ AS SYSDBA"

无法正常显示数据

SQL> SELECT * FROM TEST;

R1
--------------------
6+11

疑问1:ZHS16GBK为US7ASCII的超集,为什么在ZHS16GBK环境下无法正常显示

这主要是因为Oracle检查发现数据库设置的字符集与客户端配置字符集不同,它将对数据进行字符集的转换。数据库中实际存放的数据为182(10110110)、171(10101011)、177(10110001)、177(10110001),由于数据库字符集设置为US7ASCII,它是一个7bit的字符集,存储在8bit的字节中,则Oracle忽略各字节的最高bit,则182(10110110)就变成了54(0110110),在ZHS16GBK中代表数字符号“6”(当然在其它字符集中也是“6”),同样过程也发生在其它3个字节,这样“东北”就变成了“6+11”。
实验结果分析三



QUOTE:
最初由 tellin 发布
用ZHS16GBK插入数据
SQL> INSERT INTO TEST VALUES('东北');

1 row created.

SQL> SELECT * FROM TEST;

R1
--------------------
6+11
??

SQL> EXIT

当客户端字符集设置为ZHS16GBK后向数据库插入“东北”,Oracle检查发现数据库设置的字符集为US7ASCII与客户端不一致,需要进行转换,但字符集ZHS16GBK中的“东北”两字在US7ASCII中没有对应的字符,所以Oracle用统一的“替换字符”插入数据库,在这里为“?”,编码为63(00111111),这时,输入的信息实际上已经丢失,不管字符集设置如何改变(如下面引用的实验结果),第二行SELECT出来的结果也都是两个“?”号(注意是2个,而不是4个)。

QUOTE:
更改客户端字符集为US7ASCII 
D:\>SET NLS_LANG=AMERICAN_AMERICA.US7ASCII

D:\>SQLPLUS "/ AS SYSDBA"

无法显示用ZHS16GBK插入的字符集,但可以显示用US7ASCII插入的字符集
SQL> SELECT * FROM TEST;

R1
----------
东北
??


更改服务器字符集为ZHS16GBK
SQL> update props$ set value$='ZHS16GBK' WHERE NAME='NLS_CHARACTERSET';

1 row updated.

SQL> COMMIT;

更改客户端字符集为ZHS16GBK
D:\>SET NLS_LANG=AMERICAN_AMERICA.ZHS16GBK

D:\>SQLPLUS "/ AS SYSDBA"

可以显示以前US7ASCII的字符集,但无法显示用ZHS16GBK插入的数据,说明用ZHS16GBK插入的数据为乱码。

SQL> SELECT * FROM TEST;

R1
--------------------
东北
??

需要指出的是,通过“update props$ set value$='ZHS16GBK' WHERE NAME='NLS_CHARACTERSET';”来修改数据库字符集是非常规作法,很可能引起问题,在这里只是原文引用网友的实验结果。

实验结果分析四



QUOTE:
SQL> INSERT INTO TEST VALUES('东北');

1 row created.

SQL> SELECT * FROM TEST;

R1
--------------------
东北
??
东北

SQL> EXIT

由于此时数据库与客户端的字符集设置均为ZHS16GBK,所以不会发生字符集的转换,第一行与第三行数据显示正确,而第二行由于存储的数据就是63(00111111),所以显示的是“?”号。

QUOTE:
更改客户端字符集为US7ASCII

D:\>SET NLS_LANG=AMERICAN_AMERICA.US7ASCII

D:\>SQLPLUS "/ AS SYSDBA"

无法显示数据

SQL> SELECT * FROM TEST;

R1
----------
??
??
??

疑问2:第一行数据是用US7ASCII环境插入的,为何无法正常显示?

将客户端字符集设置改为US7ASCII后进行SELECT,Oracle检查发现数据库设置的字符集为ZHS16GBK,数据需要进行字符集转换,而第一行与第三行的汉字“东”与“北”在客户端字符集US7ASCII中没有对应字符,所以转换为“替换字符”(“?”),而第二行数据在数据库中存的本来就是两个“?”号,所以虽然在客户端显示的三行都是两个“?”号,但在数据库中存储的内容却是不同的。

实验结果分析五



QUOTE:
SQL> INSERT INTO TEST VALUES('东北');

1 row created.

SQL> EXIT
更改客户端字符集为ZHS16GBK
D:\>SET NLS_LANG=AMERICAN_AMERICA.ZHS16GBK


D:\>SQLPLUS "/ AS SYSDBA"

无法显示用US7ASCII插入的字符集,但可以显示用ZHS16GBK插入的字符集
SQL> SELECT * FROM TEST;

R1
--------------------
东北
??
东北
6+11

SQL>
疑问3:US7ASCII为ZHS16GBK的子集,为何在US7ASCII环境下插入的数据无法显示?

在客户端字符集设置为US7ASCII时,向字符集为ZHS16GBK的数据库中插入“东北”,需要进行字符转换,“东北”的ZHS16GBK编码为182(10110110)、171(10101011)与177(10110001)、177(10110001),由于US7ASCII为7bit编码,Oracle将这两个汉字当作四个字符,并忽略各字节的最高位,从而存入数据库的编码就变成了54(00110110)、43(00101011)与49(00110001)、49(00110001),也就是“6+11”,原始信息被改变了。这时,将客户端字符集设置为ZHS16GBK再进行SELECT,数据库中的信息不需要改变传到客户端,第一、三行由于存入的信息没有改变能显示“东北”,而第二、四行由于插入数据时信息改变,所以不能显示原有信息了。

分析了这么多的内容,但实际上总结起来也很简单

分析了这么多的内容,但实际上总结起来也很简单,要想在字符集方面少些错误与麻烦,需要坚持两条基本原则:
在数据库端:选择需要的字符集(通过create database中的CHARACTER SET与NATIONAL CHARACTER SET子句指定);
在客户端:设置操作系统实际使用的字符集(通过环境变量NLS_LANG设置)。
再论字符集

原文见 我的blog  http://space.itpub.net/?69924
字符集是一个老生常谈的问题了。论坛中很多贴子探讨过这个问题,这个问题的引起,绝大部分是因为“乱码”。而乱码是由于客户端与服务器的字符集的不同进行字符集转换而引起的。不过很多贴子提到了转换,却没有提到这个转换是在哪个阶段和哪里发生的?是在服务器向块里写入数据的时候吗?在客户端还是在服务器端?

正确的答案是,普通字符串转换发生在客户端(具体来说是由OCI LIBRARY完成的),国家字符串经过两次转换,第一次发生在客户端,第二次发生在服务器端。下面做个测试:

连接到:
Oracle Database 10g Enterprise Edition Release 10.2.0.1.0 - Production
With the Partitioning, Real Application Clusters, OLAP and Data Mining options

SQL> select * from nls_database_parameters where parameter like '%CHARACTERSET%';

PARAMETER                      VALUE
------------------------------ ------------------------------
NLS_CHARACTERSET               ZHS16GBK
NLS_NCHAR_CHARACTERSET         AL16UTF16

SQL> create table t1(a varchar2(100));

表已创建。

SQL>

SQL> insert into t1 values ('中');

已创建 1 行。

SQL>

在本次连接中,我没有设置NLS_LANG变量。则客户端字符集为操作系统的缺省字符集ZHS16GBK。通过捕获网络包,可以发现客户端传送给客户端的数据(不能上传图片,郁闷):

00000090  00 00 00 00 00 00 00 00 00 00 00 28 DB 00 01 1C ...........(....
000000A0  69 6E 73 65 72 74 20 69 6E 74 6F 20 74 31 20 76 insert.into.t1.v
000000B0  61 6C 75 65 73 20 28 27  D6 D0 27 29 01 00 00 00 alues.('..')....
000000C0  01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................

注意红色的部分,16进制D6 D0正是“中”字的GBK编码。(关于怎么获取汉字的各种编码,暂且略过,如有需要再交流)

现在我们退出SQLPLUS,设置环境变量NLS_LANG:

SQL> rollback;

回退已完成。

SQL> exit
从 Oracle Database 10g Enterprise Edition Release 10.2.0.1.0 - Production
With the Partitioning, Real Application Clusters, OLAP and Data Mining options
断开

C:\Documents and Settings\Administrator>set nls_lang=american_america.us7ascii

C:\Documents and Settings\Administrator>sqlplus test/test@dmdb

SQL*Plus: Release 10.2.0.1.0 - Production on Mon Jan 28 00:48:41 2008

Copyright (c) 1982, 2005, Oracle.  All rights reserved.


Connected to:
Oracle Database 10g Enterprise Edition Release 10.2.0.1.0 - Production
With the Partitioning, Real Application Clusters, OLAP and Data Mining options

SQL> insert into t1 values ('中');

1 row created.

抓获的网络包发现,在SQL提交给服务器之前已经转换了。OCI库认为提交过来的编码是US7ASCII,因此要将转换为服务器端的ZHS16GBK编码,然而“中”的编码即16进制D6 D0并不是有效的US7ASCII编码,所以ORACLE OCI就转为了转省值3F3F(US7ASCII是单字节字符集,会认为“中”字是两个字符,因此为有两个3F) 这就是“??”号的由来。

00000090  00 00 00 00 00 00 00 00 00 00 00 C8 1D FF 00 1C ................
000000A0  69 6E 73 65 72 74 20 69 6E 74 6F 20 74 31 20 76 insert.into.t1.v
000000B0  61 6C 75 65 73 20 28 27  3F 3F 27 29 01 00 00 00 alues.('??')....
000000C0  01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................


我们再看看将客户端NLS_LANG设置为simplified chinese_china.zhs16cgb231280会发生什么:

SQL> insert into t1 values ('中');

已创建 1 行。

00000090  00 00 00 00 00 00 00 00 00 00 00 00 EC 01 01 1C ................
000000A0  69 6E 73 65 72 74 20 69 6E 74 6F 20 74 31 20 76 insert.into.t1.v
000000B0  61 6C 75 65 73 20 28 27  D6 D0 27 29 01 00 00 00 alues.('..')....
000000C0  01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................

嗯,这里仍然是D6 D0,我们知道ZHS16GBK近似于ZHS16CGB231280超级。“中”对两种字符集来说,都是同一个编码。
看看我们使用生僻字会发生什么:

SQL> insert  into t1 values ('喫');
ERROR:
ORA-01756: 引号内的字符串没有正确结束

居然没有捕获到这个INSERT INTO语句提交到服务器的网络吧。由于在客户端要将“喫”字从ZHS16GB231280转换为ZHS16GBK,但这个字并不是一个有效的GB2312编码的字。但为什么出现了ORA-01756?转换过程认为“喫”字是GB2312编码,而操作系统传过来的编码是16进制86 CB,GB2312的编码,每个字节都是大于A1,因此认为第1个字节是一个8位的单字符,下一个字节大于A1,因此转换过程就将CB和下一个字节“'”合起来成为一个GB2312的双字节字符,因此就造成了这个错误信息。然而下面的语句是可以通过的:

SQL> insert into t1 values ('喫1');

已创建 1 行。

抓获的网络包却发现是下面的结果:

00000090  00 00 00 00 00 00 00 00 00 00 00 10 EC 01 01 1D ................
000000A0  69 6E 73 65 72 74 20 69 6E 74 6F 20 74 31 20 76 insert.into.t1.v
000000B0  61 6C 75 65 73 20 28 27  3F A3 BF 27 29 01 00 00 alues.('?..')...
000000C0  00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................

验证了上面的观点。第1字节被作为一个单字节字符转换,但是也不能转换为GBK字符,因此就转为了3F,但后面的两个字节仍然不是有效的GBK编码,就转为了A3 BF(全角的“?”)

下面将讨论国家字符集的转换。

再论字符集(二)

上一篇讲到普通字符串的转换,本篇将讲到国家字符集字符串的转换:

客户端的NLS_LANG为默认值,即ZHS16GBK:

SQL> create table t1 ( id number ,aa varchar2(20),bb nvarchar2(20));

表已创建。

SQL> insert into t1 values (1,'中','中');

已创建 1 行。

捕获的网络包如下:

00000090  00 00 00 00 00 00 EA 4E DB 00 AC 0D DC 00 00 00 .......N........
000000A0  00 00 23 69 6E 73 65 72 74 20 69 6E 74 6F 20 74 ..#insert.into.t
000000B0  31 20 76 61 6C 75 65 73 20 28 31 2C 27  D6 D0 27 1.values.(1,'..'
000000C0  2C 27  D6 D0 27 29 01 00 00 00 01 00 00 00 00 00 ,'..')..........

SQL> select dump(aa) aa,dump(bb) bb from t1;

AA                             BB
------------------------------ ------------------------------
Typ=1 Len=2: 214,208           Typ=1 Len=2: 78,45

客户端发送给数据库的SQL语句,两个“中”字均为D6 D0,但服务器对NVARCHAR2类似的列作了转换,将其从ZHS16GBK编码转换为AL16UTF16,转换后的结果为10进制78,45,即16进制的4E  2D

因此对于国家字符集,客户端在提交SQL时实际并不区分是否国家字符集,统一将SQL中的字符转换为数据库字符集,服务器端再将国家字符集的列,从数据集字符集转换为国家字符集。因此,我们可以设想,如果数据库字符集与国家字符集不兼容,会发生什么?或者说是从数据库字符集转换为国家字符集是不是也会出现问题?我们用另一个数据库测试一下:

SQL> select * from nls_database_parameters where parameter like '%CHARACTERSET%'
;

PARAMETER                      VALUE
------------------------------ ------------------------------
NLS_CHARACTERSET               US7ASCII
NLS_NCHAR_CHARACTERSET         AL16UTF16

将客户端的NLS_LANG设置为AMERICAN_AMERICA.US7ASCII

SQL> create table t1 (id number,aa varchar2(20),bb nvarchar2(20));

SQL> insert into t1 values (1,'中','中');

1 row created.

SQL> select dump(aa) aa,dump(bb) bb from t1;

AA                             BB
------------------------------ ------------------------------
Typ=1 Len=2: 214,208           Typ=1 Len=4: 0,86,0,80

注意看这里dump出的结果,与前一个库dump出的结果,aa列是一样的,而bb列dump出来变成了10进制的0,86,0,80。我们看看这个值是怎么来的:
1.客户端NLS_LANG与数据库字符集相同,因此在客户端并没对SQL中的字符进行转换。
2.服务器在执行SQL时,将bb列的值从数据库字符集编码(10进制214,208)转换为AL16UTF16编码(这种编码每个字符为固定的两字节)。由于数据库字符集为单字节字符集,在转换时认为是两个字符,同时US7ASCII字符的高位应该为0,而214-128=86,208-128=80.因此转换后其结果就为字符串“VP"了:

SQL> select * from t1;

ID AA                   BB
---------- -------------------- --------------------
1 中                   VP

因此,如果选择了错误的数据库字符集,虽然可以通过设置NLS_LANG将客户端字符集设置为与服务器字符集一致,但国家字符集却有可能不能正常地从数据库字符集转换为国家字符集。

下面要讨论的是数据查询时和数据导出时的字符集转换。

再论字符集(三)

前文主要讲到的是执行DML的字符集转换,下面再讨论检索数据时的字符集转换,还是先看测试:

先将NLS_LANG设置为默认值ZHS16GBK

SQL> insert into t1 values (1,'中','中');

已创建 1 行。

SQL> commit;

提交完成。

SQL> select * from t1;

ID AA                   BB
---------- -------------------- ----------------------------------------
1 中                   中

从抓取的网络包中找到返回的数据:

00000030                    01 3D 00 00 06 00 00 00 00 00       .=........
00000040  10 17 3A 08 C0 CA 9B 07 F7 10 15 1A EA 23 F7 68 ..:..........#.h
00000050  DD 85 78 6C 01 1C 0D 22 36 52 00 00 00 03 00 00 ..xl..."6R......
00000060  00 39 02 00 00 81 16 00 00 00 00 00 00 00 00 00 .9..............
00000070  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 ................
00000080  02 02 00 00 00 02 49 44 00 00 00 00 00 00 00 00 ......ID........
00000090  01 80 00 00 14 00 00 00 00 00 00 00 00 00 00 00 ................
000000A0  00 00 00 00 00 00  54 03 01 14 00 00 00 01 02 02 ......T.........
000000B0  00 00 00 02 41 41 00 00 00 00 00 00 00 00 01 80 ....AA..........
000000C0  00 00 28 00 00 00 00 00 00 00 00 10 00 00 00 00 ..(.............
000000D0  00 00 00 00  D0 07 02 14 00 00 00 01 02 02 00 00 ................
000000E0  00 02 42 42 00 00 00 00 00 00 00 00 07 00 00 00 ..BB............
000000F0  07 78 6C 01 1C 0D 22 36 06 02 03 00 00 00 01 00 .xl..."6........
00000100  00 00 00 00 00 00 00 00 00 00 07 02 C1 02 02  D6 ................
00000110  D0 
02  4E 2D 08 06 00 F2 DF 02 00 00 00 00 00 02 ..N-............
00000120  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00000130  00 00 00 04 01 00 00 00 01 00 00 00 00 00 00 00 ................
00000140  00 00 02 00 0E 00 03 00 00 00 00 00 07 28 00 00 .............(..
00000150  04 00 00 16 00 00 00 01 00 00 00 00 00 00 2C 00 ..............,.
00000160  00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00000170  00 00 00                                        ...             

上面展示的是返回的数据。红色分别为AA列和BB列的字符集ID:

SQL> select nls_charset_name(to_number('0354','xxxx')) from dual;

NLS_CHARSET_NAME(TO_NUMBER('0354','XXXX'
----------------------------------------
ZHS16GBK

SQL> select nls_charset_name(to_number('07D0','xxxx')) from dual;

NLS_CHARSET_NAME(TO_NUMBER('07D0','XXXX'
----------------------------------------
AL16UTF16

蓝色部分是列数据,D6 D0为ZHS16GBK编码的“中”,而4E 2D为AL16UTF16编码的“中”字,数据原样从数据库中返回。这两个不同的编码,最后显示的结果均为“中”字。由于数据库字符集ZHS16GBK与客户端相同,客户端没有对数据作转换,而国家字符集的“中”字,要转换为ZHS16GBK,再最终由客户端程序(SQLPLUS)显示出来。

下面把NLS_LANG设置为AMERICAN_AMERICA.US7ASCII,再进行同样的测试,发现,返回的网络包是一样,即服务器端返回的数据是一样的,并没有因为NLS_LANG的不同而不同,因此转换仍然是发生在客户端。在这次测试中,将服务器返回的数据,转换成US7ASCII编码,出现了乱码,显示为?号

再将NLS_LANG设置为AMERICAN_AMERICA.UTF8,看看返回的结果

SQL> select * from t1;

ID AA                   BB
---------- -------------------- --------------------
1 涓?                  涓


这次是出现了将“中”字转换成了其他汉字。为什么是转成了这个“涓”字,在此不在细述。

下面把NLS_LANG设置为AMERICAN_AMERICAN.UTF8,但增加了一个环境变量NLS_NCHAR=ZHS16GBK

SQL> select * from t1;

ID AA                   BB
---------- -------------------- --------------------
1 涓?                  中

在本次测试中,字符集为国家字符集AL16UTF16的列BB显示了正确的结果。这说明客户端OCI库在转换时,对国家字符集是根据NLS_NCHAR进行转换的,在这个测试中NLS_NCHAR为ZHS16GBK,将AL16UTF16编码正确地转换到了ZHS16GBK编码。

再作一个测试,将NLS_LANG设置为AMERICAN_AMERICA.ZHS16GBK,将NLS_NCHAR设置为AL16UTF16

SQL> select * from t1;

ID AA                   BB
---------- -------------------- -----------
1 中                   N-
由于NLS_NCHAR与国家字符集相同,因此对国家字集符的列没有作转换,直接返回。“中”字的AL16UTF16的编码为 4E 2D,在客户端操作系统中,正好是英文字符“N”和“-”的编码

结论:

在客户端向服务器端提交SQL语句时,客户端根据NLS_LANG和服务器数据库字符集,对SQL中的字符进行转换处理。如果NLS_LANG设置的字符集与服务器数据库字符集相同,不作转换,否则要转换成服务器端字符符。如果有国家字符集,客户端不作处理,由服务器端再将其转换为国家字符集。

在查询数据时,服务器端原服务器端的编码返回数据,由客户端根据返回的元数据中的字符集与NLS_LANG和NLS_NCHAR的设置进行比较。如果NLS_NCHAR没有设置,则其默认值为NLS_LANG中的字符集设置。如果数据中的字符集与客户端设置一致,不进行转换,否则要进行转换。国家字符集的转换根据NLS_NCHAR设置进行转换。

根据这个结论,再推断出EXPORT和IMPORT时的字符集转换行为:

在EXPORT时,EXP程序本身也是一个普通的客户端程序,因此在执行导出时也会按NLS_LANG和NLS_NCHAR的设置进行字符集转换。然后在DMP文件记录导出时客户端的字符集。

在IMPORT时,如果DMP文件记录的字符集与客户端字符集不一样,需要将其数据转换为客户端的字符集,然后在导入到库中时,由ORACLE的客户端OCI库按前述规则,根据NLS_LANG和服务器端字符集的比较,进行了转换。

你可能感兴趣的:(oracle数据库)