經(jīng)??吹揭恍┡笥褑?wèn)ORACLE字符集方面的問(wèn)題,我想以迭代的方式來(lái)介紹一下。
第一次迭代:掌握字符集方面的基本概念。
有些朋友可能會(huì)認(rèn)為這是多此一舉,但實(shí)際上正是由于對(duì)相關(guān)基本概念把握不清,才導(dǎo)致了諸多問(wèn)題和疑問(wèn)。
首先是字符集的概念。
我們知道,電子計(jì)算機(jī)最初是用來(lái)進(jìn)行科學(xué)計(jì)算的(所以叫做“計(jì)算機(jī)”),但隨著技術(shù)的發(fā)展,還需要計(jì)算機(jī)進(jìn)行其它方面的應(yīng)用處理。這就要求計(jì)算機(jī)不僅能處理數(shù)值,還能處理諸如文字、特殊符號(hào)等其它信息,而計(jì)算機(jī)本身能直接處理的只有數(shù)值信息,所以就要求對(duì)這些文字、符號(hào)信息進(jìn)行數(shù)值編碼,最初的字符集是我們都非常熟悉的ASCII,它是用7個(gè)二進(jìn)制位來(lái)表示128個(gè)字符,而后來(lái)隨著不同國(guó)家、組織的需要,出現(xiàn)了許許多多的字符集,如表示西歐字符的ISO8859系列的字符集,表示漢字的GB2312-80、GBK等字符集。
字符集的實(shí)質(zhì)就是對(duì)一組特定的符號(hào),分別賦予不同的數(shù)值編碼,以便于計(jì)算機(jī)的處理。
字符集之間的轉(zhuǎn)換。字符集多了,就會(huì)帶來(lái)一個(gè)問(wèn)題,比如一個(gè)字符,在某一字符集中被編碼為一個(gè)數(shù)值,而在另一個(gè)字符集中被編碼為另一個(gè)數(shù)值,比如我來(lái)創(chuàng)造兩個(gè)字符集demo_charset1與demo_charset2,在demo_charset1中,我規(guī)定了三個(gè)符號(hào)的編碼為:A(0001),B(0010),?(1111);而在demo_charset2中,我也規(guī)定了三個(gè)符號(hào)的編碼為:A(1001),C(1011),?(1111),這時(shí)我接到一個(gè)任務(wù),要編寫一個(gè)程序,負(fù)責(zé)在demo_charset1與demo_charset2之間進(jìn)行轉(zhuǎn)換。由于知道兩個(gè)字符集的編碼規(guī)則,對(duì)于demo_charset1中的0001,在轉(zhuǎn)換為demo_charset2時(shí),要將其編碼改為1001;對(duì)于demo_charset1中的1111,轉(zhuǎn)換為demo_charset2時(shí),其數(shù)值不變;而對(duì)于demo_charset1中的0010,其對(duì)應(yīng)的字符為B,但在demo_charset2沒(méi)有對(duì)應(yīng)的字符,所以從理論上無(wú)法轉(zhuǎn)換,對(duì)于所有這類無(wú)法轉(zhuǎn)換的情況,我們可以將它們統(tǒng)一轉(zhuǎn)換為目標(biāo)字符集中的一個(gè)特殊字符(稱為“替換字符”),比如在這里我們可以將?作為替換字符,所以B就轉(zhuǎn)換為了?,出現(xiàn)了信息的丟失;同樣道理,將demo_charset2的C字符轉(zhuǎn)換到demo_charset1時(shí),也會(huì)出現(xiàn)信息丟失。
所以說(shuō),在字符集轉(zhuǎn)換過(guò)程中,如果源字符集中的某個(gè)字符在目標(biāo)字符集中沒(méi)有定義,將會(huì)出現(xiàn)信息丟失。
數(shù)據(jù)庫(kù)字符集的選擇。
我們?cè)趧?chuàng)建數(shù)據(jù)庫(kù)時(shí),需要考慮的一個(gè)問(wèn)題就是選擇什么字符集與國(guó)家字符集(通過(guò)create database中的CHARACTER SET與NATIONAL CHARACTER SET子句指定)??紤]這個(gè)問(wèn)題,我們必須要清楚數(shù)據(jù)庫(kù)中都需要存儲(chǔ)什么數(shù)據(jù),如果只需要存儲(chǔ)英文信息,那么選擇US7ASCII作為字符集就可以;但是如果要存儲(chǔ)中文,那么我們就需要選擇能夠支持中文的字符集(如ZHS16GBK);如果需要存儲(chǔ)多國(guó)語(yǔ)言文字,那就要選擇UTF8了。
數(shù)據(jù)庫(kù)字符集的確定,實(shí)際上說(shuō)明這個(gè)數(shù)據(jù)庫(kù)所能處理的字符的集合及其編碼方式,由于字符集選定后再進(jìn)行更改會(huì)有諸多的限制,所以在數(shù)據(jù)庫(kù)創(chuàng)建時(shí)一定要考慮清楚后再選擇。
而我們?cè)S多朋友在創(chuàng)建數(shù)據(jù)庫(kù)時(shí),不考慮清楚,往往選擇一個(gè)默認(rèn)的字符集,如WE8ISO8859P1或US7ASCII,而這兩個(gè)字符集都沒(méi)有漢字編碼,所以用這種字符集存儲(chǔ)漢字信息從原則上說(shuō)就是錯(cuò)誤的。雖然在有些時(shí)候選用這種字符集好象也能正常使用,但它會(huì)給數(shù)據(jù)庫(kù)的使用與維護(hù)帶來(lái)一系列的麻煩,在后面的迭代過(guò)程中我們將深入分析。
客戶端的字符集。
有過(guò)一些Oracle使用經(jīng)驗(yàn)的朋友,大多會(huì)知道通過(guò)NLS_LANG來(lái)設(shè)置客戶端的情況,NLS_LANG由以下部分組成:NLS_LANG=<Language>_<Territory>.<Clients Characterset>,其中第三部分<Clients Characterset>的本意就是用來(lái)指明客戶端操作系統(tǒng)缺省使用的字符集。所以按正規(guī)的用法,NLS_LANG應(yīng)該按照客戶端機(jī)器的實(shí)際情況進(jìn)行配置,尤其對(duì)于字符集一項(xiàng)更是如此,這樣Oracle就能夠在最大程度上實(shí)現(xiàn)數(shù)據(jù)庫(kù)字符集與客戶端字符集的自動(dòng)轉(zhuǎn)換(當(dāng)然是如果需要轉(zhuǎn)換的話)。
總結(jié)一下第一次迭代的重點(diǎn):
字符集:將特定的符號(hào)集編碼為計(jì)算機(jī)能夠處理的數(shù)值;
字符集間的轉(zhuǎn)換:對(duì)于在源字符集與目標(biāo)字符集都存在的符號(hào),理論上轉(zhuǎn)換將不會(huì)產(chǎn)生信息丟失;而對(duì)于在源字符集中存在而在目標(biāo)字符集中不存在的符號(hào),理論上轉(zhuǎn)換將會(huì)產(chǎn)生信息丟失;
數(shù)據(jù)庫(kù)字符集:選擇能夠包含所有將要存儲(chǔ)的信息符號(hào)的字符集;
客戶端字符集設(shè)置:指明客戶端操作系統(tǒng)缺省使用的字符集。
第二次迭代:通過(guò)實(shí)例加深對(duì)基本概念的理解
下面我將引用網(wǎng)友tellin在ITPUB上發(fā)表的“CHARACTER SET研究及疑問(wèn)”帖子,該朋友在帖子中列舉了他做的相關(guān)實(shí)驗(yàn),并對(duì)實(shí)驗(yàn)結(jié)果提出了一些疑問(wèn),我將對(duì)他的實(shí)驗(yàn)結(jié)果進(jìn)行分析,并回答他的疑問(wèn)。
實(shí)驗(yàn)結(jié)果分析一
quote:
最初由 tellin 發(fā)布
設(shè)置客戶端字符集為US7ASCII
D:\>SET NLS_LANG=AMERICAN_AMERICA.US7ASCII
查看服務(wù)器字符集為US7ASCII
SQL> SELECT * FROM NLS_DATABASE_PARAMETERS;
PARAMETER VALUE
------------------------------ ----------------------------------------
NLS_CHARACTERSET US7ASCII
建立測(cè)試表
SQL> CREATE TABLE TEST (R1 VARCHAR2(10));
Table created.
插入數(shù)據(jù)
SQL> INSERT INTO TEST VALUES('東北');
1 row created.
SQL> SELECT * FROM TEST;
R1
----------
東北
SQL> EXIT
這一部分的實(shí)驗(yàn)數(shù)據(jù)的存取與顯示都正確,好象沒(méi)什么問(wèn)題,但實(shí)際上卻隱藏著很大的隱患。
首先,要將漢字存入數(shù)據(jù)庫(kù),而將數(shù)據(jù)庫(kù)字符集設(shè)置為US7ASCII是不合適的。US7ASCII字符集只定義了128個(gè)符號(hào),并不支持漢字。另外,由于在SQL*PLUS中能夠輸入中文,操作系統(tǒng)缺省應(yīng)該是支持中文的,但在NLS_LANG中的字符集設(shè)置為US7ASCII,顯然也是不正確的,它沒(méi)有反映客戶端的實(shí)際情況。
但實(shí)際顯示卻是正確的,這主要是因?yàn)镺racle檢查數(shù)據(jù)庫(kù)與客戶端的字符集設(shè)置是同樣的,那么數(shù)據(jù)在客戶與數(shù)據(jù)庫(kù)之間的存取過(guò)程中將不發(fā)生任何轉(zhuǎn)換。具體地說(shuō),在客戶端輸入“東北”,“東”的漢字的編碼為182(10110110)、171(10101011),“北”漢字的編碼為177(10110001)、177(10110001),它們將不做任何變化的存入數(shù)據(jù)庫(kù)中,但是這實(shí)際上導(dǎo)致了數(shù)據(jù)庫(kù)標(biāo)識(shí)的字符集與實(shí)際存入的內(nèi)容是不相符的,從某種意義上講,這也是一種不一致性,也是一種錯(cuò)誤。而在SELECT的過(guò)程中,Oracle同樣檢查發(fā)現(xiàn)數(shù)據(jù)庫(kù)與客戶端的字符集設(shè)置是相同的,所以它也將存入的內(nèi)容原封不動(dòng)地傳送到客戶端,而客戶端操作系統(tǒng)識(shí)別出這是漢字編碼所以能夠正確顯示。
在這個(gè)例子中,數(shù)據(jù)庫(kù)與客戶端的設(shè)置都有問(wèn)題,但卻好象起到了“負(fù)負(fù)得正”的效果,從應(yīng)用的角度看倒好象沒(méi)問(wèn)題。但這里面卻存在著極大的隱患,比如在應(yīng)用length或substr等字符串函數(shù)時(shí),就可能得到意外的結(jié)果。另外,如果遇到導(dǎo)入/導(dǎo)出(import /export)將會(huì)遇到更大的麻煩。有些朋友在這方面做了大量的測(cè)試,如eygle研究了“源數(shù)據(jù)庫(kù)字符集為US7ASCII,導(dǎo)出文件字符集為US7ASCII或ZHS16GBK,目標(biāo)數(shù)據(jù)庫(kù)字符集為ZHS16GBK”的情況,他得出的結(jié)論是 “如果的是在Oracle92中,我們發(fā)現(xiàn)對(duì)于這種情況,不論怎樣處理,這個(gè)導(dǎo)出文件都無(wú)法正確導(dǎo)入到Oracle9i數(shù)據(jù)庫(kù)中”、“對(duì)于這種情況,我們可以通過(guò)使用Oracle8i的導(dǎo)出工具,設(shè)置導(dǎo)出字符集為US7ASCII,導(dǎo)出后修改第二、三字符,修改 0001 為0354,這樣就可以將US7ASCII字符集的數(shù)據(jù)正確導(dǎo)入到ZHS16GBK的數(shù)據(jù)庫(kù)中”。我想對(duì)于這些結(jié)論,這樣理解可能更合適一些:由于ZHS16GBK字符集是US7ASCII的超級(jí),所以如果按正常操作,這種轉(zhuǎn)換應(yīng)該沒(méi)有問(wèn)題;但出現(xiàn)問(wèn)題的本質(zhì)是我們讓本應(yīng)只存儲(chǔ)英文字符的US7ASCII數(shù)據(jù)庫(kù),非常規(guī)地存儲(chǔ)了中文信息,那么在轉(zhuǎn)化過(guò)程中出現(xiàn)錯(cuò)誤或麻煩就沒(méi)什么奇怪的了,不出麻煩倒是有些奇怪了。
所以說(shuō)要避免這種情況,就是要在建立數(shù)據(jù)庫(kù)時(shí)選擇合適的字符集,不讓標(biāo)簽(數(shù)據(jù)庫(kù)的字符集設(shè)置)與實(shí)際(數(shù)據(jù)庫(kù)中實(shí)際存儲(chǔ)的信息)不符的情況發(fā)生。
實(shí)驗(yàn)結(jié)果分析二
quote:
[ 更改客戶端字符集為ZHS16GBK
D:\>SET NLS_LANG=AMERICAN_AMERICA.ZHS16GBK
D:\>SQLPLUS "/ AS SYSDBA"
無(wú)法正常顯示數(shù)據(jù)
SQL> SELECT * FROM TEST;
R1
--------------------
6+11
疑問(wèn)1:ZHS16GBK為US7ASCII的超集,為什么在ZHS16GBK環(huán)境下無(wú)法正常顯示
這主要是因?yàn)镺racle檢查發(fā)現(xiàn)數(shù)據(jù)庫(kù)設(shè)置的字符集與客戶端配置字符集不同,它將對(duì)數(shù)據(jù)進(jìn)行字符集的轉(zhuǎn)換。數(shù)據(jù)庫(kù)中實(shí)際存放的數(shù)據(jù)為182(10110110)、171(10101011)、177(10110001)、177(10110001),由于數(shù)據(jù)庫(kù)字符集設(shè)置為US7ASCII,它是一個(gè)7bit的字符集,存儲(chǔ)在8bit的字節(jié)中,則Oracle忽略各字節(jié)的最高bit,則182(10110110)就變成了54(0110110),在ZHS16GBK中代表數(shù)字符號(hào)“6”(當(dāng)然在其它字符集中也是“6”),同樣過(guò)程也發(fā)生在其它3個(gè)字節(jié),這樣“東北”就變成了“6+11”。
實(shí)驗(yàn)結(jié)果分析三
quote:
最初由 tellin 發(fā)布
用ZHS16GBK插入數(shù)據(jù)
SQL> INSERT INTO TEST VALUES('東北');
1 row created.
SQL> SELECT * FROM TEST;
R1
--------------------
6+11
??
SQL> EXIT
當(dāng)客戶端字符集設(shè)置為ZHS16GBK后向數(shù)據(jù)庫(kù)插入“東北”,Oracle檢查發(fā)現(xiàn)數(shù)據(jù)庫(kù)設(shè)置的字符集為US7ASCII與客戶端不一致,需要進(jìn)行轉(zhuǎn)換,但字符集ZHS16GBK中的“東北”兩字在US7ASCII中沒(méi)有對(duì)應(yīng)的字符,所以O(shè)racle用統(tǒng)一的“替換字符”插入數(shù)據(jù)庫(kù),在這里為“?”,編碼為63(00111111),這時(shí),輸入的信息實(shí)際上已經(jīng)丟失,不管字符集設(shè)置如何改變(如下面引用的實(shí)驗(yàn)結(jié)果),第二行SELECT出來(lái)的結(jié)果也都是兩個(gè)“?”號(hào)(注意是2個(gè),而不是4個(gè))。
quote:
更改客戶端字符集為US7ASCII
D:\>SET NLS_LANG=AMERICAN_AMERICA.US7ASCII
D:\>SQLPLUS "/ AS SYSDBA"
無(wú)法顯示用ZHS16GBK插入的字符集,但可以顯示用US7ASCII插入的字符集
SQL> SELECT * FROM TEST;
R1
----------
東北
??
更改服務(wù)器字符集為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的字符集,但無(wú)法顯示用ZHS16GBK插入的數(shù)據(jù),說(shuō)明用ZHS16GBK插入的數(shù)據(jù)為亂碼。
SQL> SELECT * FROM TEST;
R1
--------------------
東北
??
需要指出的是,通過(guò)“update props$ set value$='ZHS16GBK' WHERE NAME='NLS_CHARACTERSET';”來(lái)修改數(shù)據(jù)庫(kù)字符集是非常規(guī)作法,很可能引起問(wèn)題,在這里只是原文引用網(wǎng)友的實(shí)驗(yàn)結(jié)果。
?
實(shí)驗(yàn)結(jié)果分析四
quote:
SQL> INSERT INTO TEST VALUES('東北');
1 row created.
SQL> SELECT * FROM TEST;
R1
--------------------
東北
??
東北
SQL> EXIT
由于此時(shí)數(shù)據(jù)庫(kù)與客戶端的字符集設(shè)置均為ZHS16GBK,所以不會(huì)發(fā)生字符集的轉(zhuǎn)換,第一行與第三行數(shù)據(jù)顯示正確,而第二行由于存儲(chǔ)的數(shù)據(jù)就是63(00111111),所以顯示的是“?”號(hào)。
quote:
更改客戶端字符集為US7ASCII
D:\>SET NLS_LANG=AMERICAN_AMERICA.US7ASCII
D:\>SQLPLUS "/ AS SYSDBA"
無(wú)法顯示數(shù)據(jù)
SQL> SELECT * FROM TEST;
R1
----------
??
??
??
疑問(wèn)2:第一行數(shù)據(jù)是用US7ASCII環(huán)境插入的,為何無(wú)法正常顯示?
將客戶端字符集設(shè)置改為US7ASCII后進(jìn)行SELECT,Oracle檢查發(fā)現(xiàn)數(shù)據(jù)庫(kù)設(shè)置的字符集為ZHS16GBK,數(shù)據(jù)需要進(jìn)行字符集轉(zhuǎn)換,而第一行與第三行的漢字“東”與“北”在客戶端字符集US7ASCII中沒(méi)有對(duì)應(yīng)字符,所以轉(zhuǎn)換為“替換字符”(“?”),而第二行數(shù)據(jù)在數(shù)據(jù)庫(kù)中存的本來(lái)就是兩個(gè)“?”號(hào),所以雖然在客戶端顯示的三行都是兩個(gè)“?”號(hào),但在數(shù)據(jù)庫(kù)中存儲(chǔ)的內(nèi)容卻是不同的。
實(shí)驗(yàn)結(jié)果分析五
quote:
SQL> INSERT INTO TEST VALUES('東北');
1 row created.
SQL> EXIT
更改客戶端字符集為ZHS16GBK
D:\>SET NLS_LANG=AMERICAN_AMERICA.ZHS16GBK
D:\>SQLPLUS "/ AS SYSDBA"
無(wú)法顯示用US7ASCII插入的字符集,但可以顯示用ZHS16GBK插入的字符集
SQL> SELECT * FROM TEST;
R1
--------------------
東北
??
東北
6+11
SQL>
疑問(wèn)3:US7ASCII為ZHS16GBK的子集,為何在US7ASCII環(huán)境下插入的數(shù)據(jù)無(wú)法顯示? [/B]
在客戶端字符集設(shè)置為US7ASCII時(shí),向字符集為ZHS16GBK的數(shù)據(jù)庫(kù)中插入“東北”,需要進(jìn)行字符轉(zhuǎn)換,“東北”的ZHS16GBK編碼為182(10110110)、171(10101011)與177(10110001)、177(10110001),由于US7ASCII為7bit編碼,Oracle將這兩個(gè)漢字當(dāng)作四個(gè)字符,并忽略各字節(jié)的最高位,從而存入數(shù)據(jù)庫(kù)的編碼就變成了54(00110110)、43(00101011)與49(00110001)、49(00110001),也就是“6+11”,原始信息被改變了。這時(shí),將客戶端字符集設(shè)置為ZHS16GBK再進(jìn)行SELECT,數(shù)據(jù)庫(kù)中的信息不需要改變傳到客戶端,第一、三行由于存入的信息沒(méi)有改變能顯示“東北”,而第二、四行由于插入數(shù)據(jù)時(shí)信息改變,所以不能顯示原有信息了。
分析了這么多的內(nèi)容,但實(shí)際上總結(jié)起來(lái)也很簡(jiǎn)單
分析了這么多的內(nèi)容,但實(shí)際上總結(jié)起來(lái)也很簡(jiǎn)單,要想在字符集方面少些錯(cuò)誤與麻煩,需要堅(jiān)持兩條基本原則:
在數(shù)據(jù)庫(kù)端:選擇需要的字符集(通過(guò)create database中的CHARACTER SET與NATIONAL CHARACTER SET子句指定);
在客戶端:設(shè)置操作系統(tǒng)實(shí)際使用的字符集(通過(guò)環(huán)境變量NLS_LANG設(shè)置)。