正確的答案是,普通字符串轉換發生在客戶端(具體來說是由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 27D6 D027 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 273F 3F27 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 27D6 D027 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 273F A3 BF27 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 27D6 D027 1.values.(1,’..’
000000C0 2C 27D6 D027 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 0054 0301 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 00D0 0702 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 02D6…………….
00000110 D0024E 2D08 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設置進行轉換。
下面我們討論一下關於導入導出(exp/imp)的問題。這個問題是這樣的:
使用imp導入數據後,發現數據是正確的,沒有亂碼,但是表和列上的注釋(comments)、中文列名、Procedure/Package裡面的中文全部變成了亂碼。
網上很少有文章討論到這一點,其實exp/imp與通常執行SQL引起的字符集轉換有一些不同。這得從dmp文件的格式說起。
先看看下面的測試:
vIEw plaincopy to clipboardprint?
SQL> create table t1 ( a number,b varchar2(100));
SQL> insert into t1 values (123456,'aaaaaa');
SQL> insert into t1 values (67890,'中中中中');
SQL> commit;
SQL> comment on table t1 is '測試表';
SQL> create table t1 ( a number,b varchar2(100));
SQL> insert into t1 values (123456,'aaaaaa');
SQL> insert into t1 values (67890,'中中中中');
SQL> commit;
SQL> comment on table t1 is '測試表';
現在將NLS_LANG設置為AMERICAN_AMERICA.ZHS16GBK,導出T1表,然後看看導出的dmp文件中的數據:
000008f0h: 22 54 31 22 0A 43 52 45 41 54 45 20 54 41 42 4C ; “T1″.CREATE TABL
00000900h: 45 20 22 54 31 22 20 28 22 41 22 20 4E 55 4D 42 ; E “T1″ (”A” NUMB
00000910h: 45 52 2C 20 22 42 22 20 56 41 52 43 48 41 52 32 ; ER, “B” VARCHAR2
00000920h: 28 31 30 30 29 29 20 20 50 43 54 46 52 45 45 20 ; (100)) PCTFREE
00000930h: 31 30 20 50 43 54 55 53 45 44 20 34 30 20 49 4E ; 10 PCTUSED 40 IN
00000940h: 49 54 52 41 4E 53 20 31 20 4D 41 58 54 52 41 4E ; ITRANS 1 MAXTRAN
00000950h: 53 20 32 35 35 20 53 54 4F 52 41 47 45 28 49 4E ; S 255 STORAGE(IN
00000960h: 49 54 49 41 4C 20 31 30 34 38 35 37 36 20 46 52 ; ITIAL 1048576 FR
00000970h: 45 45 4C 49 53 54 53 20 31 20 46 52 45 45 4C 49 ; EELISTS 1 FREELI
00000980h: 53 54 20 47 52 4F 55 50 53 20 31 29 20 54 41 42 ; ST GROUPS 1) TAB
00000990h: 4C 45 53 50 41 43 45 20 22 54 45 53 54 5F 38 4B ; LESPACE “TEST_8K
000009a0h: 22 20 4C 4F 47 47 49 4E 47 20 4E 4F 43 4F 4D 50 ; ” LOGGING NOCOMP
000009b0h: 52 45 53 53 0A 49 4E 53 45 52 54 20 49 4E 54 4F ; RESS.INSERT INTO
000009c0h: 20 22 54 31 22 20 28 22 41 22 2C 20 22 42 22 29 ; “T1″ (”A”, “B”)
000009d0h: 20 56 41 4C 55 45 53 20 28 3A 31 2C 20 3A 32 29 ; VALUES (:1, :2)
000009e0h: 0A 02 00 02 00 16 00 01 00 64 00 54 03 01 00 00 ; ………d.T….
000009f0h: 00 00 00 04 00 C3 0D 23 39 06 00 61 61 61 61 61 ; …..?#9..aaaaa
00000a00h: 61 00 00 04 00 C3 07 4F 5B 08 00 D6 D0 D6 D0 D6 ; a….?O[..中中?
00000a10h: D0 D6 D0 00 00 FF FF 0A 43 4F 4D 4D 45 4E 54 20 ; 兄?.