oracle数据库特种恢复技术(三)—转换篇

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/,如需转载,请注明出处,否则将追究法律责任。

转载于:http://blog.itpub.net/9606200/viewspace-745679/

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值