Oracle基本数据类型存储格式浅析(一)——字符类型

 

Oracle基本数据类型存储格式浅析(一)——字符类型

 

这一篇主要说明字符类型的存储格式主要包括char、varchar2和long等几种类型。

 

查看当前用户:

SQL> show user

USER is "LSF"

 

建立一个测试表:

SQL> create table test_char  

  2  (char_col char(10),

  3   varchar_col varchar2(10),

  4   long_col long

  5  );

 

Table created.

 

插入测试数据:

SQL> insert into test_char values('abc','123',',fd');

 

1 row created.

 

SQL> commit;

 

Commit complete.

 

查看rowid:

SQL> select rowid from test_char;

 

ROWID

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

AAAHw5AALAAAAKdAAA

 

ROWID是数据的详细地址,通过rowid,oracle可以快速的定位某行具体的数据的位置。

ROWID可以分为物理rowid和逻辑rowid两种。普通的堆表中的rowid是物理rowid,索引组织表(IOT)的rowid是逻辑rowid。oracle提供了一种urowid的数据类型,同时支持物理和逻辑rowid。这里主要关注物理rowid

物理rowid又分为扩展rowid(extended rowid)和限制rowid(restricted rowid)两种格式。限制rowid主要是oracle7以前的rowid格式,现在已经不再使用,保留该类型只是为了兼容性。所以本文的提到物理 rowid一般是指扩展rowid格式。

 

Rowid的显示形式:

我们从rowid伪列里select出来的rowid是基于base64编码,一共有18位,分为4部分:

OOOOOOFFFBBBBBBRRR

其中:

OOOOOO:六位表示data object id,根据object id可以确定segment。

FFF:三位表示相对文件号。根据该相对文件号可以得到绝对文件号,从而确定datafile。

BBBBBB:六位表示data block number,这里的data block number是相对于datafile的编号,而不是相对于tablespace的编号。

RRR:三位表示row number。

Oracle提供了dbms_rowid来进行rowid的一些转换计算。

SQL> select dbms_rowid.rowid_object(rowid) obj#,

  2  dbms_rowid.rowid_relative_fno(rowid) rfile#,

  3  dbms_rowid.rowid_block_number(rowid) block#,

  4  dbms_rowid.rowid_row_number(rowid) row#,

  5  dbms_rowid.rowid_to_absolute_fno(rowid,'LSF','TEST_CHAR') file#

  6  from test_char;

 

      OBJ#     RFILE#     BLOCK#       ROW#      FILE#

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

     31801         11        669          0         11

 

如何从rowid计算得到obj#,rfile#,block#,row#

rowid是base64编码的,用A~Z a~z 0~9 + /共64个字符表示。A表示0,B表示1,……,a表示26,……,0表示52,……,+表示62,/表示63可以将其看做一个64进制的数。

所以,

ROWID

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

AAAGbEAAHAAAAB8AAA

 

obj#=AAAGbE=6*64^2+27*64+4=26308

rfile#=AAH=7

block#=AAAAB8=64+60=124

row#=AAA=0

 

我们的例子中

ROWID

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

AAAHw5AALAAAAKdAAA

obj#=AAAHw5=7*64^2+48*64+57=31801

rfile#=AAL=11

block#=AAAAKd=669

row#=AAA=0

 

如何从obj#,rfile#,block#,row#计算得到rowid

实际上就是将十进制数转化成64进制数,当然,从二进制转化的规则比较简单点。

将二进制数从右到左,6个bit一组,然后将这6个bit组转成10进制数,就是A~Z a~z 0~9 + /这64个字符的位置(从0开始),替换成base64的字符即可。

obj#=26308=110 011011 000100=6 27 4=G b E,补足成6位base64编码,左边填0,也就是A,结果为AAAGbE

rfile#=7=111=7=H,补足成3位,得到AAH

block#=124=1 111100=1 60=B 8,补足成6位,得到AAAAB8

row#=0,3位AAA

合起来就是AAAGbEAAHAAAAB8AAA

 

我们的例子中

ROWID

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

AAAHw5AALAAAAKdAAA

obj#=AAAHw5=7*64^2+48*64+57=31801

rfile#=AAL=11

block#=AAAAKd=669

row#=AAA=0

 

obj#=31801= 111 110000 111001=7 48 57=H w 5,补足成6位,得到AAAHw5

rfile#=11=1011=11=L,补足成3位,得到AAL

block#=669= 1010 011101=10 29=Kd,补足成6位,得到AAAAKd

row#=0,三位AAA

合起来就是AAAHw5AALAAAAKdAAA

 

Rowid的内部存储格式

虽然我们从rowid伪列中select出来的rowid是以base64字符显示的,但在oracle内部存储的时候还是以原值的二进制表示的。一个扩展 rowid采用10个byte来存储,共80bit,其中obj#32bit,rfile#10bit,block#22bit,row#16bit。所以相对文件号不能超过1023,也就是一个表空间的数据文件不能超过1023个(不存在文件号为0的文件),一个datafile只能有2^22=4M个 block,一个block中不能超过2^16=64K行数据。而一个数据库内不能有超过2^32=4G个object。

SQL> select dump(rowid,16) from test;

DUMP(ROWID,16)

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

Typ=69 Len=10: 0,0,66,c4,1,c0,0,7c,0,0

00000000 00000000 01100110 11000100 00000001 11000000 00000000 01111100 00000000 00000000

最右边16bit为row#=00000000 00000000=0

接下来22bit为block#=000000 00000000 01111100=124

接下来10bit为rfile#=00000001 11=7

接下来32bit为obj#=00000000 00000000 01100110 11000100=26308

 

我们的例子中

SQL> select dump(rowid,16) from test_char;

 

DUMP(ROWID,16)

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

Typ=69 Len=10: 0,0,7c,39,2,c0,2,9d,0,0

00000000 00000000 01111100 00111001 00000010 11000000 00000010 10011101 00000000 00000000

最右边16bit为row#=00000000 00000000=0

接下来22bit为block#=000000 00000010 10011101=669

接下来10bit为file#=00000010 11=11

接下来32bit为obj#=00000000 00000000 01111100 00111001=31801

 

Index中存储的rowid

a.普通B-tree索引

SQL> create index ix_test on test(id);

Index created.

SQL> select file_id,block_id from dba_extents where segment_name='IX_TEST' and owner=user;

FILE_ID BLOCK_ID

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

7 129

---由于是assm表空间,去掉3个block的头

SQL> alter system dump datafile 1 block 132;

System altered.

得到trace文件内容如下(省略无关内容):

row#0[8024] flag: -----, lock: 0

col 0; len 2; (2): c1 02 ---索引键数据ID=1

col 1; len 6; (6): 01 c0 00 7c 00 00 ---对应的rowid记录

----- end of leaf block dump -----

End dump data blocks tsn: 7 file#: 7 minblk 132 maxblk 132

普通索引中保存的rowid是不包括obj#的,但是分区表的global index是包括obj#的,这是因为分区表包括多个segment,每个segment可能在不同的datafile中,根据表的obj#就无法确定该索引键对应的rowid(rfile#确定不了)。

01 c0 00 7c 00 00 转化为二进制 000000001 11000000 00000000 01111100 00000000 00000000

右边8bit row#=0

接下来22bit block#=000000 00000000 01111100=124

接下来10bit rfile#=000000001 11=7

b.唯一索引

SQL> drop index ix_test;

Index dropped.

SQL> create unique index ix_test on test(id);

Index created.

SQL> select file_id,block_id from dba_extents where segment_name='IX_TEST' and wner=user;

FILE_ID BLOCK_ID

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

7 129

SQL> alter system dump datafile 1 block 132;

System altered.

得到trace文件内容如下:

row#0[8025] flag: -----, lock: 0, data:(6): 01 c0 00 7c 00 00 ---对应的rowid记录

col 0; len 2; (2): c1 02 ---索引键数据ID=1

----- end of leaf block dump -----

End dump data blocks tsn: 7 file#: 7 minblk 132 maxblk 132

得到rowid为 01 c0 00 7c 00 00,具体的转换计算和前面的一样,就不重复了。

 

下面继续我们上面的实验:

根据rowid的定义规则,第7~9位是表示的是数据文件,L表示11,而10~15位表示的是在这个数据文件中的第几个BLOCK,Kd表示669。(rowid编码相当于64进制。用A~Z a~z 0~9 + /共64个字符表示。A表示0,B表示1,……,a表示26,……,0表示52,……,+表示62,/表示63。)

 

我们根据计算的结果去dump这个block。

SQL> @trace.sql                             

 

Session altered.

 

 

Session altered.

SQL> alter system dump datafile 11 block 669;

 

System altered.

SQL> @gettracefilename.sql

 

RTRIM(C.VALUE,'/')||'/'||D.INSTANCE_NAME||'_ORA_'||LTRIM(TO_CHAR(A.SPID))||'.TRC'

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

/u01/app/oracle/admin/orcl/udump/orcl_ora_2758.trc

 

打开产生的trace文件:

Start dump data blocks tsn: 12 file#: 11 minblk 669 maxblk 669

buffer tsn: 12 rdba: 0x02c0029d (11/669)

scn: 0x0000.0005ee3e seq: 0x01 flg: 0x06 tail: 0xee3e0601

frmt: 0x02 chkval: 0x82c5 type: 0x06=trans data

Block header dump:  0x02c0029d

 Object id on Block? Y

 seg/obj: 0x7c39  csc: 0x00.5ee3c  itc: 2  flg: E  typ: 1 - DATA

     brn: 0  bdba: 0x2c00289 ver: 0x01

     inc: 0  exflg: 0

 

 Itl           Xid                  Uba         Flag  Lck        Scn/Fsc

0x01   0x0004.002.0000014c  0x008000bf.0053.4d  --U-    1  fsc 0x0000.0005ee3e

0x02   0x0000.000.00000000  0x00000000.0000.00  ----    0  fsc 0x0000.00000000

 

data_block_dump,data header at 0xc6b3e64

===============

tsiz: 0x1f98

hsiz: 0x14

pbl: 0x0c6b3e64

bdba: 0x02c0029d

     76543210

flag=--------

ntab=1

nrow=1

frre=-1

fsbo=0x14

fseo=0x1f82

avsp=0x1f6e

tosp=0x1f6e

0xe:pti[0]      nrow=1  offs=0

0x12:pri[0]     offs=0x1f82

block_row_dump:

tab 0, row 0, @0x1f82

tl: 22 fb: --H-FL-- lb: 0x1  cc: 3

col  0: [10]  61 62 63 20 20 20 20 20 20 20

col  1: [ 3]  31 32 33

col  2: [ 3]  2c 66 64

end_of_block_dump

End dump data blocks tsn: 12 file#: 11 minblk 669 maxblk 669

 

观察dump出来的结果,可以发现以下几点:

1.对于每个字段,除了保存字段的值以外,还会保存当前字段中数据的长度。而且,oracle显然没有把字段的长度定义或类型定义保存在block中,这些信息保存在oracle的数据字典里面。

2.根据dump的结果,可以清楚的看到,字符类型在数据库中是以ascii格式存储的。

SQL> select chr(to_number('61','xx')) from dual;

 

CHR(TO_N

--------

a

3.char类型为定长格式,存储的时候会在字符串后面填补空格,而varchar2和long类型都是变长的。

SQL> select dump(char_col,16) d_char from test_char;

 

D_CHAR

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

Typ=96 Len=10: 61,62,63,20,20,20,20,20,20,20

SQL> select dump(varchar_col,16) d_varchar2 from test_char;

 

D_VARCHAR2

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

Typ=1 Len=3: 31,32,33

SQL> select dump(long_col,16) d_varchar2 from test_char;

select dump(long_col,16) d_varchar2 from test_char

            *

ERROR at line 1:

ORA-00997: illegal use of LONG datatype

 

由于DUMP不支持LONG类型,因此我们使用了alter system dump block的方式,通过比较两种方式得到的结果,发现DUMP()函数不但方便,结果清晰,而且指出了进行DUMP的数据类型,在以后的例子中,除非必要的情况,否则都会采用DUMP()函数的方式进行说明。

 

下面看一下插入中文的情况,首先看一下数据库的字符集

SecureCRT设置会话选项->字符编码->UTF-8

$ export NLS_LANG=.AL32UTF8

SQL> COL VALUE FOR A30

SQL> COL PARAMETER A30

SQL> select * from nls_database_parameters where parameter='NLS_CHARACTERSET';

 

PARAMETER                      VALUE

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

NLS_CHARACTERSET               AL32UTF8

SQL> select * from nls_database_parameters where parameter='NLS_NCHAR_CHARACTERSET';

 

PARAMETER                      VALUE

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

NLS_NCHAR_CHARACTERSET         AL16UTF16

SQL> select * from nls_database_parameters;

 

PARAMETER                      VALUE

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

NLS_LANGUAGE                   AMERICAN

NLS_TERRITORY                  AMERICA

NLS_CURRENCY                   $

NLS_ISO_CURRENCY               AMERICA

NLS_NUMERIC_CHARACTERS         .,

NLS_CHARACTERSET               AL32UTF8

NLS_CALENDAR                   GREGORIAN

NLS_DATE_FORMAT                DD-MON-RR

NLS_DATE_LANGUAGE              AMERICAN

NLS_SORT                       BINARY

NLS_TIME_FORMAT                HH.MI.SSXFF AM

 

PARAMETER                      VALUE

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

NLS_TIMESTAMP_FORMAT           DD-MON-RR HH.MI.SSXFF AM

NLS_TIME_TZ_FORMAT             HH.MI.SSXFF AM TZR

NLS_TIMESTAMP_TZ_FORMAT        DD-MON-RR HH.MI.SSXFF AM TZR

NLS_DUAL_CURRENCY              $

NLS_COMP                       BINARY

NLS_LENGTH_SEMANTICS           BYTE

NLS_NCHAR_CONV_EXCP            FALSE

NLS_NCHAR_CHARACTERSET         AL16UTF16

NLS_RDBMS_VERSION              9.2.0.4.0

 

20 rows selected.

 

以SYS用户

SQL> SELECT NAME,VALUE$ FROM SYS.PROPS$ WHERE NAME='NLS_CHARACTERSET';

也可以查看数据库的字符集

SQL> select userenv('language') from dual;

 

USERENV('LANGUAGE')

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

AMERICAN_AMERICA.AL32UTF8

 

Dump 文件的第2和第3个字节记录了dump文件的字符集

如果dump文件相对较小,用UltraEdit打开(16进制方式),看第2第3个字节的内容,如0354,然后用以下SQL查出它对应的字符集。

如果dmp文件很大,比如有2G以上(这也是最常见的情况),用文本编辑器打开很慢或者完全打不开,可以用以下命令(在unix主机上):

cat exp.dmp |od -x|head -1|awk '{print $2 $3}'|cut -c 3-6

然后用上述SQL也可以得到它对应的字符集。

 

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

 

NLS_CHARSET_NAME(TO_NUMBER('0354','XXXX'

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

ZHS16GBK

 

SQL> insert into test_char values('定长','变长',null);

 

1 row created.

SQL> select dump(char_col,16) d_char from test_char;

 

D_CHAR

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

Typ=96 Len=10: 61,62,63,20,20,20,20,20,20,20

Typ=96 Len=10: e5,ae,9a,e9,95,bf,20,20,20,20

 

SQL> select dump(varchar_col,16) d_varchar2 from test_char;

 

D_VARCHAR2

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

Typ=1 Len=3: 31,32,33

Typ=1 Len=6: e5,8f,98,e9,95,bf

根据dump结果,可以清楚的看出,普通英文字符和标点用一个字节表示,而中文字符或中文标点需要三个字节来表示。

 

下面,对比一下nchar和nvarchar2与char、varchar2类型有什么不同。

SQL> create table test_nchar(nchar_col nchar(10), nvarchar_col nvarchar2(10));

 

Table created.

 

SQL> insert into test_nchar values ('nchar定长','nvarchar变长');

 

1 row created.

从这里已经可以看出一些不同了,如果按照刚才中文的计算方法,'nvarchar变长'的长度是8+2*3=14已经超过了数据类型定义的大小,可是为什么插入成功了?--可能有错

还是dump一下看看结果吧。

SQL> select dump(nchar_col,16) from test_nchar;

 

DUMP(NCHAR_COL,16)

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

Typ=96 Len=20: 0,6e,0,63,0,68,0,61,0,72,5b,9a,95,7f,0,20,0,20,0,20

 

SQL> select dump(nvarchar_col,16) from test_nchar;

 

DUMP(NVARCHAR_COL,16)

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

Typ=1 Len=20: 0,6e,0,76,0,61,0,72,0,63,0,68,0,61,0,72,53,d8,95,7f

这下就明白了,虽然仍然是采用ascii码存储,但是nchar使用的AL16UTF16字符集,编码长度变为2个字节。这样中文使用两个字节,对于可以用一个字节就表示的英文字符,采用了高位补0的方式凑足2位,这样,对于采用AL16UTF16字符集的nchar类型,无论中文还是英文都用2位字符表示。因此'nvarchar变长'的长度是10,并没有超过数据类型的限制。

 

补充:

nchar(size)的定义:

CREATE TABLE tab1 (col1 NCHAR(30));

This statement creates a column with maximum character length of 30. The maximum byte length is the multiple of the maximum character length and the maximum number of bytes in each character.

size是以字符character为定义,而不是byte;同理nvarchar;

查看tom的chapter 12 Datatypes章节有这个介绍。

 

一个英文字母是一个字符,一个中文汉字是几个字符呢?我们知道,os字符集是gbk环境下,一个中文汉字是双字节字符,但它算做几个字符与其数据库字符集有关。如果数据库字符集使用单字节US7ASCII,则一个中文汉字是二个字符;如果数据库字符集使用双字节字符集ZHS16GBK,则一个中文汉字被视为一个字符。有关这一点可以使用 Oracle的函数substr得到证明。

使用US7ASCⅡ字符集时:

select substr(‘南京航空航天大学’,1,2) from dual;

语句执行结果返回‘南’。

使用ZHS16GBK字符集时:

select substr(‘南京航空航天大学’,1,2) from dual;

语句执行结果返回‘南京’。

使用AL32UTF8字符集时:

select substr('南京航空航天大学',1,2) from dual;

语句执行结果返回‘南京’。

SQL> select substr('南京航空航天大学',1,2) from dual;

 

SUBSTR

------

南京

 

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值