oracle資料庫特種恢復技術(三)—轉換篇

jinqibingl發表於2012-10-04

oracle資料庫特種恢復技術(三)—轉換篇  

       oracle資料庫特種恢復技術(三)—轉換篇


作者:謝浩
Oracle資料庫特種恢復技術(一)—原理篇連結:http://www.itpub.net/thread-1507760-1-1.html
oracle資料庫特種恢復技術(二)—塊內篇連結:http://www.itpub.net/thread-1507766-1-1.html


      繼續上一次的內容。
       儲存的資料最常見的三種型別date、number、varchar2。
一、varchar2型別
       Varchar2型別是oracle提供的一種可變長度字元型別,所謂可變長度就是說定義一個長度為4000的varchar2型別欄位,如果實際存放在該欄位中的字元不足4000,那麼實際佔用的物理空間是和實際存放的長度相同的,不會有空間浪費的情況,所帶來的缺點就是需要為每行的每個欄位單獨記錄長度,略微增加處理開銷。
       Varchar2定義的長度,是隻儲存的最大位元組數,而一個varchar2型別欄位能存放的最大字元數量,根據資料庫不同的編碼方式有所不同。最常用的兩種字符集zhs16gbk和al32utf8。
       zhs16gbk是gbk編碼的一種儲存實現方式,是一種定長字符集,每個字元固定用一或兩個位元組儲存,具體為ascii碼對應的字元在zhs16gbk中佔一個位元組,其他字元佔用兩個位元組。儲存方式就是直接存放gbk編碼表中的編碼。這裡有一個有趣的現象,在各種字元編碼中字元都不是連續的,有些值是空缺的,沒有對應任何字元,這其實是為了避讓各個作業系統、程式語言、協議等的關鍵字。
       al32utf8是unicode字元編碼的一種實現。al32utf8採用變長度儲存,具體為:
1、0000到007f直接的字元(對應ascii字元)採用一個位元組儲存
2、0080到07ff直接的字元用兩個位元組儲存
3、0800到ffff之間的字元用三個位元組儲存
4、ffff之後的的字元用四個位元組儲存
      需要說明的是,常說的utf-8,習慣上是指al24utf8,與al32utf8的區別是al24utf8最多使用三個位元組,ffff之後的字元在儲存上被拆分成兩個字元儲存,對於ffff之前的字元al24utf8和al32utf8是完全相同的,也就是說al32utf8是al24utf8的嚴格超集,al24utf8是al32utf8的真子集。al32utf8的儲存方式為:

1、             如果是一個需要一個位元組儲存的字元(0000~007f),那麼這個位元組的第一個bit位固定為0。

2、             如果是一個需要兩個位元組儲存的字元(0080~07ff),那麼其中第一個位元組的前三個bit位固定為110,第二個位元組的前兩個bit位固定為10。

3、             如果是一個需要三個位元組儲存的字元(0800~ffff),那麼其中第一個位元組的前四個bit為固定為1110,第二、三個位元組的前兩個bit位古亭為10。

4、             如果是一個需要四個位元組儲存的字元(10000~1fffff),那麼其中第一個位元組的前五個bit為固定為11110,第二、三、四個位元組的前兩個bit位古亭為10。

      其實al32utf8只是utf-8編碼方式中的一種,現有編碼方式最大已經達到al48utf8。
      有些書籍說zhs16gbk是al32utf8的真子集,所以zhs16gbk的庫匯出的資料,可以完美匯入al32utf8的庫,這是不正確的,在oracle online document裡詳細記錄的oracle支援的所有字符集之間的subset-superset關係,這部分內容位於book Globalization Support Guide中。
      根據文件中記載zhs16gbk不是任何一個字符集的真子集(幾乎所有字符集都是US7ASCII的嚴格超集)。所以經常被採用的修改匯出檔案第二、第三位元組的方式,其實是不嚴謹的,因為兩種字符集對同一個字元的儲存方式完全不同。之所以能匯入成功,和系統平臺字符集、oracle內部的自動轉換都有關係。嚴格的做法是編寫程式,逐個讀出匯出檔案中的字元,並查詢字符集編碼對照表,轉換為目標端字符集。
      這裡有一個使用oracle online document的小心得,每次搜尋到文件中的一個章節,在頁面的最上方一行都會顯示該章節對應的book名稱,book名稱的前兩個字母可以幫助在index頁面中快速定位各種資料,比如常用的book SQL Reference記錄了oracle中所有語句的語法,Book PL/SQL Packages and Types Reference記錄了oracle自帶的所有工具包以及其中過程、函式的使用方法,book Database Installation Guide記錄了oracle在所有作業系統平臺上的詳細安裝步驟,Book Reference記錄了oracle所有引數的含義等等。
      zhs16gbk與al32utf8作為國內在oracle中使用最廣泛的字符集,分別代表了定長和變長兩種編碼儲存格式。兩者各有各自的優缺點。
            定長字符集的優點:儲存轉換簡便(直接儲存字元編碼);al32utf8儲存cjk統漢字佔用三個位元組,zhs16gbk只需用兩個位元組,節省了空間。
            定長字符集的缺點:確定字元與字元之間的邊界困難,要確定儲存中各個字元,只能從檔案頭逐個順序分割,這就是常見的文件中一個字出現亂碼,則後面連續很多字都變成亂碼,直到出現一個ascii字元才能恢復正常。
            變長字符集的優點:確定字元與字元之間的邊界容易,每個byte的前導bit位就能確定該位元組是否是一個字元的首位元組,如果一個字元的儲存出現錯誤,不會拖累其後的字元。
            變長字符集的缺點:對於常用的cjk來講空間使用率不高,轉換比較費事。
      瞭解兩種編碼的原理後,進行實際驗證。以漢字“浩”為例,以下為擷取的unicode、gbk編碼轉換表中“浩”字對應的行:
  

unicode十六進位制

  
  

unicode十進位制

  
  

gbk十六進位制

  
  

gbk十進位制

  
  

區位碼

  
  

漢字

  
  

6d69

  
  

28009

  
  

bac6

  
  

47814

  
  

2638

  
  

  






分別在定義為al32utf8字符集和zhs16gbk字符集的庫上dump“浩”字的儲存方式。
SQL> select dump('浩',1016) from dual;
DUMP('浩',1016)
--------------------------------------------
Typ=96 Len=3 CharacterSet=AL32UTF8: e6,b5,a9
select dump('浩',1016) from dual;
DUMP('浩',1016)
-----------------------------------------
Typ=96 Len=2 CharacterSet=ZHS16GBK: ba,c6

      首先觀察ZHS16GBK字符集的儲存格式ba,c6就是gbk編碼的原始碼。再觀察AL32UTF8字符集的儲存格式e6,b5,a9,將其轉換為二進位制為11100110,10110101,10101001,按上文所述編碼方式,擷取第一個位元組0110,第二個位元組110101,第三個位元組101001,拼接為0110110101101001再轉換為16進製為6D69,與編碼表中的unicode編碼相符合。

對於英文,由於ZHS16GBK和AL32UTF8都是US7ASCII的嚴格超集,因此儲存格式完全相同:

SQL> selectdump('abcdefghigklmnopqrstuvwxyz0123456789',1016) from dual;

DUMP('ABCDEFGHIGKLMNOPQRSTUVWXYZ0123456789',1016)

------------------------------------------------------------------------------------------------------------------------------------------------

Typ=96 Len=36 CharacterSet=ZHS16GBK:61,62,63,64,65,66,67,68,69,67,6b,6c,6d,6e,6f,70,71,72,73,74,75,76,77,78,79,7a,30,31,32,33,34,35,36,37,38,39

SQL> selectdump('abcdefghigklmnopqrstuvwxyz0123456789',1016) from dual;

DUMP('ABCDEFGHIGKLMNOPQRSTUVWXYZ0123456789',1016)

--------------------------------------------------------------------------------

Typ=96 Len=36CharacterSet=AL32UTF8: 61,62,63,64,65,66,67,68,69,67,6b,6c,6d,6e,6

f,70,71,72,73,74,75,76,77,78,79,7a,30,31,32,33,34,35,36,37,38,39

需要注意的是,oracle中有char、varchar和nchar、nvarchar兩套變數,對應的分別是資料庫中NLS_CHARACTERSET和NLS_NCHAR_CHARACTERSET兩個字符集設定。

二、number型別

Number型別在oracle中的儲存分三部分:

1、  指數部分

2、  數值部分

3、  符號部分

      數字0在oracle中只有指數部分一個位元組,且該位元組固定為80,oracle內部認定指數位大於80的為正數,小於80的為負數。
      正數的指數部分需要減去193再使用,負數的指數部分需要被62減再使用。
      正數的數值部分,每個位元組存放兩位數,且是按十進位制儲存,需要減去1再使用(儲存時加1是為了避讓0x00這個字串結尾符號),負數的數值部分每個位元組存放兩位數,且是按十進位制儲存,需要被101減再使用。
負數的後面跟一個位元組0x66表示負數。
進行實際驗證,以sys.bootstrap$為例,該表第一行資料如下:
SQL> select * from (select * from sys.bootstrap$ a order bya.obj#) where rownum < 2;
     LINE#       OBJ# SQL_TEXT
---------- ------------------------------------------------------------------------------------------
        -1         -1 8.0.0.0.0
在dd輸出中,找到該行物理儲存資料如下:
2C 01 03 03 3E 64 66 03 3E 64 66 09 38 2E 30 2E 302E 30 2E 30   
      按前兩次介紹,其中的3E 64 66是第一個欄位的值,將其轉換為十進位制為62  100  102,首先判斷第一個欄位62小於80且最後一個欄位為102(數值部分只表示1~99,不可能出現超過99的位元組)判斷該數值符號為負,負數的指數需要被62減,62-62=0,負數的數值需要按位元組被101減,101-100=1。最後得出:-1*100^0=-1,與查詢結果相符合。
再看一個正數。
SQL> select * from (select a.*,rownum rn from sys.bootstrap$ a order bya.obj#) where rn=3;
     LINE#       OBJ# SQL_TEXT                                                                       RN
---------- ----------------------------------------------------------------------------------------------------
        20         20 CREATE TABLE ICOL$("OBJ#"NUMBER NOT NULL,"BO#" NUMBER NOT NULL,"COL#" NUMBER NO          3
找到改行資料如下:

00001dd9h: 2C 0103 02 C1 15 02 C1 15 FE 79 01 43 52 45 41 ; ,...?.??.CREA

00001de9h: 54 4520 54 41 42 4C 45 20 49 43 4F 4C 24 28 22 ; TE TABLE ICOL$("

00001df9h: 4F 424A 23 22 20 4E 55 4D 42 45 52 20 4E 4F 54 ; OBJ#" NUMBER NOT

  。。。。。。。。。。。。省略。。。。。。。。。。。。。。。
      其第一個欄位為C1 15,轉換為十進位制193  21,由於第一位元組大於80,因此為正數,正數的指數為減去192後使用,193-193=0,正數的數值部分-1後使用21-1=20。最後得出:20*100^0=20,與查詢結果相符合。
      掌握number型別的儲存結構後,再分析一下這種儲存結構的細節。
      按oracle對Number型別的儲存格式計算number型別的取值範圍,number型別最多佔21位元組儲存,最大正數時第一個位元組為0xfe(ff和null衝突),後續19個位元組(最後一個位元組是符號位元組)全部是0x64(二進位制的100),99*(100^62+100^61+100^60……..+100^(62-19)就是oracle能表示的最大數值。最小正數時第一個位元組為0x81,第二個位元組為0x02,最小值為1*100^-65。如果超出表示範圍會出現符號錯誤的現象,比如,一個小數,精確到小數點後66位,那麼在儲存上這個數字的指數字節為x-193=-66,則x=127對應十六進位制的0x7f,小於0x80,那麼oracle會判斷該數值為負數。
insert into xh_num
values
  ('k',0.0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001,  length('0.0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001'),
   '',
   sysdate,
  '0.0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001');
這裡,將一個超高精度的小數分別按數值和字元插入表中,再查詢出來:

SQL>selecta.num,a.num_char from xh_num a where a.dummy = 'k';

-.000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000099 0.0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001
       從結果集中可以看到,數值的絕對值已經變化,而且由於精度太高,造成指數字節太小(小於0x80),因此被oracle錯誤的判斷為一個負數。其實在任何一種程式語言乃至作業系統中對於大長度數字的儲存和計算都是一個比較困難的領域,而且數值的儲存、顯示、轉換等要面臨多種環節的多個問題,這也就是在進行資料庫設計時一個非常重要的原則:除了需要參與運算的欄位,其他一律不要用數值型。
      Oracle之所以要在負數後加一個位元組102,和oracle比較兩數大小的方式有關的,oracle在比較兩個數的大小時,先比較指數位,大於0x80的為正數,必然大於指數位小於0x80的負數。如果指數位相同,那麼取左起第一個位元組對應的數值,較小的被判為較小數值,如果相同,在比較左起第二個位元組,以此類推。按此規則如果兩個數的符號恰巧都為“-”,那麼又有兩種情況:1、如果兩數長度相同,由於負數在儲存上的值越大實際值就越小(實際值要被101減)那麼可以得到正常結果2、如果兩數長度不同,就會出現如下現象:
SQL> select dump(-100),dump(-112) from dual;
DUMP(-100)      DUMP(-112)
----------------------- --------------------------
Typ=2 Len=3: 61,100,102 Typ=2 Len=4:61,100,89,102
      這裡可以看到-112對應的儲存值前兩位和-100對應的儲存值完全相同,如果沒有最後一個102,那麼按上面所述的比較規則,-100沒有位元組可以和-112的第三個位元組的89相對應,那麼就形成了89與0比較的情形,由於89>0,因此出現-112>-100的錯誤結果。而加入最後一個位元組的102以後,89<102因此可以得出正確的結果。選擇102為符號位是由於不管是整數還是負數,都不會出現值為102的位。
三、date型別
       Date型別固定佔7個位元組儲存,第一到地球分別為:世紀(53~199)-年(100~199)-月(1~12)-日(1~31)-時(0~23)-分(0~59)-秒(0~59)。月日直接儲存對應數值,世紀和年採用的是+100儲存的方式,如1999年:世紀為19,儲存為119,年份為99,儲存為199。時分秒都採用+1儲存的方式。
SQL> select dump(a.da),to_char(a.da,'yyyy-mm-dd hh24:mi:ss') fromxh_num a;
DUMP(A.DA)                                                             TO_CHAR(A.DA,'YYYY-MM-DDHH24:M
--------------------------------------------------------------------------------------------------------------
Typ=12 Len=7: 120,111,10,11,1,1,1            2011-10-11 00:00:00                                                                  
Typ=12 Len=7: 120,111,10,25,1,1,1            2011-10-25 00:00:00                                                            
Typ=12 Len=7: 120,111,10,24,1,1,1           2011-10-24 00:00:00

2011-10-1100:00:00,世紀兩位20儲存時加100為120,年份兩位11加100儲存為111,兩位月份10直接儲存,兩位日期11直接儲存,時、分、秒分別加1後儲存。


      對三種最常用的資料型別進行分析後,下一步就是要用一個實際的實驗對整個流程進行簡單貫通。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/9606200/viewspace-745679/,如需轉載,請註明出處,否則將追究法律責任。

相關文章