这是一篇非常不错的文章, 原文:
最近在看dbca发布的mydul的源码,自己不是太懂java,看起来也有点费力, 但对oracle还是有好多不懂。在源码中提到这篇文章我就跟过来了学习学习!
oracle基本数据类型存储格式浅析(一)—— 字符类型
前一阵看完文档,对oracle的基本数据类型的存储格式有了一些了解,最近有做了一些测试进行了验证。
打算整理总结一下,这一篇主要说明字符类型的存储格式。主要包括char、varchar2和long等几种类型。
sql> create table test_char(char_col char(10), varchar_col varchar2(10), long_col long);
表已创建。
sql> insert into test_char values ('abc', '123', ',fd');
已创建 1 行。
sql> commit;
提交完成。
sql> select rowid from test_char;
rowid
------------------
aaab3laafaaaaagaaa
根据rowid的定义规则, 第7~9位是表示的是数据文件, f表示5, 而10~15位表示的是在这个数据文件中的第几个block,g表示32。(rowid编码相当于64进制。用a~z a~z 0~9 / 共64个字符表示。a表示0, b表示1, ……, a表示26,……,0表示52,……, 表示62,/表示63。)
我们根据计算的结果去dump这个block。
sql> alter system dump datafile 5 block 32;
系统已更改。
打开产生的trace文件:
data_block_dump, data header at 0x3421064
===============
tsiz: 0x1f98
hsiz: 0x14
pbl: 0x03421064
bdba: 0x01400020
76543210
flag=--------
ntab=1
nrow=1
frre=-1
fsbo=0x14
fseo=0x1f82
avsp=0x1f6e
tosp=0x1f6e
0xeti[0] nrow=1 offs=0
0x12ri[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: 5 file#: 5 minblk 32 maxblk 32
观察dump出来的结果,可以发现以下几点:
1. 对于每个字段,除了保存字段的值以外,还会保存当前字段中数据的长度。而且,oracle显然没有把字段的长度定义或类型定义保存在block中,这些信息保存在oracle的数据字典里面。
2. 根据dump的结果,可以清楚的看到,字符类型在数据库中是以ascii格式存储的。
sql> select chr(to_number('61', 'xx')) from dual;
ch
--
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 位于第 1 行:
ora-00997: 非法使用 long 数据类型
由于dump不支持long类型,因此我们使用了alter system dump block 的方式,通过比较两种方式得到的结果,发现dump()函数不但方便,结果清晰,而且指出了进行dump的数据类型,在以后的例子中,除非必要的情况,否则都会采用dump()函数的方式进行说明。
下面看一下插入中文的情况,首先看一下数据库的字符集
sql> select name, value$ from sys.props$ where name like '%characterset%';
name value$
------------------------------ ------------------------------
nls_characterset zhs16gbk
nls_nchar_characterset al16utf16
sql> insert into test_char values ('定长', '变长', null);
已创建 1 行。
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: b6,a8,b3,a4,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
typ=1 len=4: b1,e4,b3,a4
根据dump结果,可以清楚的看出,普通英文字符和标点用一个字节表示,而中文字符或中文标点需要两个字节来表示。
下面,对比一下nchar和nvarchar2与char、varchar2类型有什么不同。
sql> create table test_nchar (nchar_col nchar(10), nvarchar_col nvarchar2(10));
表已创建。
sql> insert into test_nchar values ('nchar定长', 'nvarchar变长');
已创建 1 行。
从这里已经可以看出一些不同了,如果按照刚才中文的计算方法,'nvarchar变长'的长度是8 2*2=12已经超过了数据类型定义的大小,可是为什么插入成功了?
还是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,并没有超过数据类型的限制。
==================================================
oracle基本数据类型存储格式浅析(二) —— 数字类型
这篇文章主要描述number类型的数据和如何在数据库中存储的。
oracle的number类型最多由三个部分构成,这三个部分分别是最高位表示位、数据部分、符号位。其中负数包含符号位,正数不会包括符号位。另外,数值0比较特殊,它只包含一个数值最高位表示位80,没有数据部分。
正数的最高位表示位大于80,负数的最高位表示位小于80。其中一个正数的最高位是个位的话,则最高位表示位为c1,百位、万位依次为c2、c3,百分位、万分为依次为c0、bf。一个负数的最高位为个位的话,最高位表示位为3e,百位、万位依次为3d、3c,百分位、万分位依次为3f、40。
数据部分每一位都表示2位数。这个两位数可能是从0到99,如果是数据本身是正数,则分别用二进制的1到64表示,如果数据本身是负数,则使用二进制65到2表示。
符号位用66表示。
上面的这些是我通过dump结果总结出来的,对于上面提到的这些关系常数,oracle之所以这样选择是有道理的,我们后面根据例子也可以推导出来,而且会进一步说明为什么会采用这种方式表示。这里列出的意思是使大家先对number类型数据有一个大概的了解。
下面我们通过一个例子详细说明:
sql> create table test_number (number_col number);
表已创建。
sql> insert into test_number values (0);
已创建 1 行。
sql> insert into test_number values (1);
已创建 1 行。
sql> insert into test_number values (2);
已创建 1 行。
sql> insert into test_number values (25);
已创建 1 行。
sql> insert into test_number values (123);
已创建 1 行。
sql> insert into test_number values (4100);
已创建 1 行。
sql> insert into test_number values (132004078);
已创建 1 行。
sql> insert into test_number values (2.01);
已创建 1 行。
sql> insert into test_number values (0.3);
已创建 1 行。
sql> insert into test_number values (0.00000125);
已创建 1 行。
sql> insert into test_number values (115.200003);
已创建 1 行。
sql> insert into test_number values (-1);
已创建 1 行。
sql> insert into test_number values (-5);
已创建 1 行。
sql> insert into test_number values (-20032);
已创建 1 行。
sql> insert into test_number values (-234.432);
已创建 1 行。
sql> commit;
提交完成。
sql> col d_number format a50
sql> select number_col, dump(number_col, 16) d_number from test_number;
number_col d_number
---------- --------------------------------------------------
0 typ=2 len=1: 80
1 typ=2 len=2: c1,2
2 typ=2 len=2: c1,3
25 typ=2 len=2: c1,1a
123 typ=2 len=3: c2,2,18
4100 typ=2 len=2: c2,2a
132004078 typ=2 len=6: c5,2,21,1,29,4f
2.01 typ=2 len=3: c1,3,2
.3 typ=2 len=2: c0,1f
.00000125 typ=2 len=3: be,2,1a
115.200003 typ=2 len=6: c2,2,10,15,1,4
-1 typ=2 len=3: 3e,64,66
-5 typ=2 len=3: 3e,60,66
-20032 typ=2 len=5: 3c,63,65,45,66
-234.432 typ=2 len=6: 3d,63,43,3a,51,66
已选择15行。
下面根据例子得到的结果,对每行进行说明。首先说明两点基本的。dump函数返回的type=2表示dump的数据类型是number,length=n表示数值在数据库中存储的长度是n。
1. dump(0)的结果是0x80,在前面已经提到,0只有高位表示位,没有数据位。由于0的特殊,既不属于正数,也不属于负数,因此使用高位表示位用80表示就足够了,不会和其它数据冲突,oracle出于节省空间的考虑将后面数据部分省掉了。但是为什么oracle选择0x80表示0呢? 我们知道正数和负数互为相反数,每个正数都有一个对应的负数。因此如果我们要使用编码表示数值,则表示正数和负数的编码应该各占一半,这样才能保证使oracle表示数据范围是合理的。而0x80的二进制编码是1000 0000,正好是一个字节编码最大值的一半,因此,oracle选择0x80来表示0,是十分有道理的。
2. dump(1)的结果是0xc102,0xc1表示了最高位个位,0x2表示数值是1。首先,oracle为什么用c1表示个位呢? 其实,道理和刚才的差不多。采用科学计数法,任何一个实数s都可以描述为a.b×10n,a表示整数部分,b表示小数部分,而n表示10的指数部分。当s大于1时,n大于等于0,s小于1时,n小于0。也就是说,采用指数的方式表示,n大于0和n小于0的情况各占一半左右时,oracle所表示的范围最广。因此,oracle选择了c1表示个位是最高位的情况。
sql> select to_char(round(to_number('81', 'xxx') (to_number('ff', 'xxx') - to_number('81', 'xxx') 1)/2), 'xx') from dual;
to_
---
c1
为什么oracle使用0x2表示1,而不直接使用0x1表示1呢?oracle每个字节表示2位数,因此对于这个2位数,出现的可能是0~99共100种可能,问题出在0这里。oracle底层是用c语言实现的,我们知道二进制0在c语言中用作字符串终结符,oracle为了避免这个问题,因此使用了0x1表示0,并依次类推,使用0x64表示99。
3. dump(2)的结果是0xc103。
4. dump(25)的结果是0xc11a。前面提到,数据部分是以2位为最小单位保存的。因此对于25来说,最高位表示位仍然是个位,个位上的值是25,根据上面推出的规则,25在存储为0xc11a。
sql> select to_char(25 1, 'xx') from dual;
to_
---
1a
5. dump(123)的结果是0xc20218。由于123最高为是百位,所以最高位表示位为0xc2,百位上是1,用0x02表示,个位上是23,用0x18表示。
6. dump(4100)的结果是0xc22a。注意一点,如果数字最后数位上如果是0,oracle出于节省空间的考虑不会存储。比如:4100只保存百位上的41,12000000只保存百位位上的12,512000只保存万位上的51和百位上的20。
7. dump(132004078)的结果是0xc5022101294f。最高位是亿位,因此用0xc5表示,亿位上是1用0x02表示,百位位上是32用0x21表示,万位上是0用0x01表示,百位上是40用0x29表示,个位上78用0x4f表示。
注意:中间数位上的0不能省略。
8. dump(2.01)的结果是0xc10302。最高位是个位用0xc1表示,个位上是2用0x03表示,百分位上是1用0x02表示。
注意:个位下面一位是百分位不是十分位。
9. dump(0.3)的结果是0xc01f。最高位是百分位,使用0xc0表示,百分位上是30用0x1f表示。
10. dump(0.00000125)的结果是0xbe021a。最高位是百万分位,用0xbe表示,最高位上的1用0x02表示,25用0x1a表示。
11. dump(115.200003)的结果是0xc20210150104。
12. dump(-1)的结果是0x3e6466。最高位个位,用0x3e表示,64表示个位上是1,66是符号位,表示这个数是负数。
负数和正数互为相反数,负数的最高位表示位和它对应的相反数的最高位相加的值是ff。1的最高位表示位是c1,-1的最高位表示位是3e。负数中1用64表示。负数中的数值和它相反数的数据相加是0x66,也就是符号位。正数1用0x02表示,负数1用0x64表示,二者相加是0x66。负数多个一个标识位,用0x66表示。由于正数的表示范围是0x01到0x64,负数的表示范围是0x65到0x02。因此,不会在表示数字时出现的0x66表示。
13. dump(-5)的结果是0x3e6066。0x3e表示最高位是个位,0x60表示个位上是5,0x66是符号标识位。0x3e加0xc1是0xff。0x60加0x06的结果是0x66。
14. dump(-20032)的结果是0x3c63654566。最高位是万位,正数的万位是0xc3,因此负数的万位是0x3c。万位上是2,正数用0x03表示,负数为0x63,百位上是0,正数用0x01表示,负数使用0x65表示,个位上是32,正数用0x21表示,负数使用0x45表示。0x66是负数表示位。
15. dump(-234.432)的结果是0x3d63433a5166。
根据oracle的存储特性,还可以推出oracle的number类型的取值范围。
oracle的concept上是这样描述的:
the following numbers can be stored in a number column:
positive numbers in the range 1 x 10^-130 to 9.99...9 x 10^125 with up to 38 significant digits.
negative numbers from -1 x 10^-130 to 9.99...99 x 10^125 with up to 38 significant digits.
zero.
下面来推导出取值范围。
来看符号位,0xc1表示个位。
sql> select to_number('ff', 'xxx') - to_number('c1', 'xxx') from dual;
to_number('ff','xxx')-to_number('c1','xxx')
-------------------------------------------
62
由于oracle是两位、两位存储的,因此最高位相当于62×2=124,而且最高位上最大值是99,因此正数的最大值为9.999……×10^125。
sql> select to_number('c1', 'xxx') - to_number('80', 'xxx') from dual;
to_number('c1','xxx')-to_number('80','xxx')
-------------------------------------------
65
最高位相当于65×2=130,因此正数的最小值为1×10^-130。
负数和正数在各使用了一半的编码,因此具有相同的极值范围。
==============================================================
oracle基本数据类型存储格式浅析(三) —— 日期类型(一)
这篇文章描述date类型的数据在oracle中是以何种格式存放的。
下面通过一个例子进行说明。
sql> create table test_date (date_col date);
表已创建。
sql> insert into test_date values (to_date('2000-1-1 0:0:0', 'yyyy-mm-dd hh24:mi:ss'));
已创建 1 行。
sql> insert into test_date values (to_date('1-1-1 0:0:0', 'yyyy-mm-dd hh24:mi:ss'));
已创建 1 行。
sql> insert into test_date values (to_date('-1-1-1 0:0:0', 'syyyy-mm-dd hh24:mi:ss'));
已创建 1 行。
sql> insert into test_date values (to_date('-101-1-1 0:0:0', 'syyyy-mm-dd hh24:mi:ss'));
已创建 1 行。
sql> insert into test_date values (to_date('-4712-1-1 0:0:0', 'syyyy-mm-dd hh24:mi:ss'));
已创建 1 行。
sql> insert into test_date values (to_date('9999-12-31 23:59:59', 'syyyy-mm-dd hh24:mi:ss'));
已创建 1 行。
sql> insert into test_date values (sysdate);
已创建 1 行。
sql> insert into test_date values (to_date('-4713-1-1 0:0:0', 'syyyy-mm-dd hh24:mi:ss'));
insert into test_date values (to_date('-4713-1-1 0:0:0', 'syyyy-mm-dd hh24:mi:ss'))
*
error 位于第 1 行:
ora-01841: (全)年度值必须介于 -4713 和 9999 之间,且不为 0
sql> insert into test_date values (to_date('0000-1-1 0:0:0', 'yyyy-mm-dd hh24:mi:ss'));
insert into test_date values (to_date('0000-1-1 0:0:0', 'yyyy-mm-dd hh24:mi:ss'))
*
error 位于第 1 行:
ora-01841: (全)年度值必须介于 -4713 和 9999 之间,且不为 0
sql> col dump_date format a80
sql> select to_char(date_col, 'syyyy-mm-dd hh24:mi:ss'), dump(date_col) dump_date from test_date;
to_char(date_col,'sy dump_date
-------------------- ---------------------------------------
2000-01-01 00:00:00 typ=12 len=7: 120,100,1,1,1,1,1
0001-01-01 00:00:00 typ=12 len=7: 100,101,1,1,1,1,1
-0001-01-01 00:00:00 typ=12 len=7: 100,99,1,1,1,1,1
-0101-01-01 00:00:00 typ=12 len=7: 99,99,1,1,1,1,1
-4712-01-01 00:00:00 typ=12 len=7: 53,88,1,1,1,1,1
9999-12-31 23:59:59 typ=12 len=7: 199,199,12,31,24,60,60
2004-12-15 13:56:19 typ=12 len=7: 120,104,12,15,14,57,20
已选择7行。
通过最后两条语句已经可以看出oracle的date类型的取值范围是公元前4712年1月1日至公元9999年12月31日。而且根据日期的特定,要不然是公元1年,要不然是公元前1年,不会出现0年的情况。
日期类型长度是7,7个字节分别表示世纪、年、月、日、时、分和秒。
由于不会出现0的情况,月和日都是按照原值存储的,月的范围是1~12,日的范围是1~31。
由于时、分、秒都会出现0的情况,因此存储时采用原值加1的方式。0时保存为1,13时保存为14,23时保存为24。分和秒的情况与小时类似。小时的范围是0~23,在数据库中以1~24保存。分和秒的范围都是0~59,在数据库中以1~60保存。
年和世纪的情况相对比较复杂,可分为公元前和公元后两种情况。由于最小的世纪的值是-47(公元前4712年),最大值是99(公元9999年)。为了避免负数的产生,oracle把世纪加100保存在数据库中。公元2000年,世纪保存为120,公元9999年,世纪保存为199,公元前101年,世纪保存为99(100 (-1)),公元前4712年,世纪保存为53(100 (-47))。
注意,对于公元前1年,虽然已经是公元前了,但是表示世纪的前两位的值仍然是0,因此,这时的保存的世纪的值仍然是100。世纪的范围是-47~99,保存的值是53~199。
年的保存与世纪的保存方式类似,也把年的值加上100进行保存。对于公元2000年,年保持为100,公元1年保存为101,公元2004年保存为104,公元9999年保存为199,公元前1年,保存为99(100 (-1)),公元前101年,保存为99(100 (-1)),公元前4712年保存为88(100 (-12))。对于公元前的年,保存的值总是小于等于100,对于公元后的年,保存的值总是大于等于100。年的范围是0~99,保存的值是1~199。
注意:一般的世纪,都包含了100年,而对于0世纪,由于包含公元前和公元后两部分且不包含0年,因此包含了198年。
==================================================
oracle基本数据类型存储格式浅析(三)——日期类型(二)
这篇文章描述timestamp类型的数据在oracle中是以何种格式存放的。
下面通过一个例子进行说明。
sql> create table test_time (col_time timestamp);
表已创建。
sql> insert into test_time values (to_timestamp('0001-1-1 0:0:0.0', 'syyyy-mm-dd hh24:mi:ss.ff'));
已创建 1 行。
sql> insert into test_time values (to_timestamp('2000-1-1 0:0:0.0', 'syyyy-mm-dd hh24:mi:ss.ff'));
已创建 1 行。
sql> insert into test_time values (to_timestamp('9999-12-31 23:59:59.999999', 'syyyy-mm-dd hh24:mi:ss.ff'));
已创建 1 行。
sql> insert into test_time values (to_timestamp('-0001-1-1 0:0:0.0', 'syyyy-mm-dd hh24:mi:ss.ff'));
已创建 1 行。
sql> insert into test_time values (to_timestamp('-0100-3-4 13:2:3.234015', 'syyyy-mm-dd hh24:mi:ss.ff'));
已创建 1 行。
sql> insert into test_time values (systimestamp);
已创建 1 行。
sql> insert into test_time values (to_timestamp('2000-1-1 0:0:0.123456789', 'syyyy-mm-dd hh24:mi:ss.ff9'));
已创建 1 行。
sql> commit;
提交完成。
sql> select to_char(col_time, 'syyyy-mm-dd hh24:mi:ss.ff9') time, dump(col_time) dump_time from test_time;
time dump_time
------------------------------ ----------------------------------------------------
0001-01-01 00:00:00.000000000 typ=180 len=7: 100,101,1,1,1,1,1
2000-01-01 00:00:00.000000000 typ=180 len=7: 120,100,1,1,1,1,1
9999-12-31 23:59:59.999999000 typ=180 len=11: 199,199,12,31,24,60,60,59,154,198,24
-0001-01-01 00:00:00.000000000 typ=180 len=7: 100,99,1,1,1,1,1
-0100-03-04 13:02:03.234015000 typ=180 len=11: 99,100,3,4,14,3,4,13,242,201,24
2004-12-15 16:14:52.738000000 typ=180 len=11: 120,104,12,15,17,15,53,43,252,252,128
2000-01-01 00:00:00.123457000 typ=180 len=11: 120,100,1,1,1,1,1,7,91,205,232
已选择7行。
与date类型对比可以发现,对于timestamp类型,如果不包含微秒信息或者微秒值为0,那么存储结果和date完全相同。当微秒值为0时,oracle为了节省空间,不会保存微秒信息。
如果毫秒值不为0,oracle把微秒值当作一个9位数的数字来保存。
比如999999000,保存为59,154,198,24。234015000保存为13,242,201,24。
sql> select to_char(999999000, 'xxxxxxxxxx') from dual;
to_char(999
-----------
3b9ac618
sql> select to_number('3b', 'xxx') one, to_number('9a', 'xxx') two, to_number('c6', 'xxx') three, to_number('18', 'xxx') four from dual;
one two three four
---------- ---------- ---------- ----------
59 154 198 24
sql> select to_char(234015000, 'xxxxxxxx') from dual;
to_char(2
---------
df2c918
sql> select to_number('d', 'xxx') one, to_number('f2', 'xxx') two, to_number('c9', 'xxx') three, to_number('18', 'xxx') four from dual;
one two three four
---------- ---------- ---------- ----------
13 242 201 24
另外,注意一点,不指定精度的情况下,timestamp默认取6位。长度超过6位,会四舍五入到6位。如果希望保存9位的timestamp,必须明确指定精度。
sql> alter table test_time modify (col_time timestamp(9));
表已更改。
sql> insert into test_time values (to_timestamp('2000-1-1 0:0:0.123456789', 'syyyy-mm-dd hh24:mi:ss.ff9'));
已创建 1 行。
sql> select to_char(col_time, 'syyyy-mm-dd hh24:mi:ss.ff9') time, dump(col_time) dump_time from test_time;
time dump_time
------------------------------ ---------------------------------------------------
0001-01-01 00:00:00.000000000 typ=180 len=7: 100,101,1,1,1,1,1
2000-01-01 00:00:00.000000000 typ=180 len=7: 120,100,1,1,1,1,1
9999-12-31 23:59:59.999999000 typ=180 len=11: 199,199,12,31,24,60,60,59,154,198,24
-0001-01-01 00:00:00.000000000 typ=180 len=7: 100,99,1,1,1,1,1
-0100-03-04 13:02:03.234015000 typ=180 len=11: 99,100,3,4,14,3,4,13,242,201,24
2004-12-15 16:14:52.738000000 typ=180 len=11: 120,104,12,15,17,15,53,43,252,252,128
2000-01-01 00:00:00.123457000 typ=180 len=11: 120,100,1,1,1,1,1,7,91,205,232
2000-01-01 00:00:00.123456789 typ=180 len=11: 120,100,1,1,1,1,1,7,91,205,21
已选择8行。
============================================================
oracle基本数据类型存储格式浅析(三) —— 日期类型(三)
如果直接在sql语句中对sysdate或由to_date函数生成日期进行dump操作,会发现得到的结果与dump数据库中保存的日期的结果不一样。
sql> truncate table test_date;
表已截掉。
sql> insert into test_date values (to_date('2004-12-17 16:42:42', 'syyyy-mm-dd hh24:mi:ss'));
已创建 1 行。
sql> col dump_date format a65
sql> select to_char(date_col, 'syyyy-mm-dd hh24:mi:ss') dat, dump(date_col) dump_date from test_date;
dat dump_date
-------------------- ---------------------------------------------------------
2004-12-17 16:42:42 typ=12 len=7: 120,104,12,17,17,43,43
sql> select to_char(to_date('2004-12-17 16:42:42', 'syyyy-mm-dd hh24:mi:ss'), 'syyyy-mm-dd hh24:mi:ss') dat,
dump(to_date('2004-12-17 16:42:42', 'syyyy-mm-dd hh24:mi:ss')) dump_date from dual;
dat dump_date
-------------------- ---------------------------------------------------------
2004-12-17 16:42:42 typ=13 len=8: 212,7,12,17,16,42,42,0
存储在数据库中的date类型是12,而直接在sql中使用的date类型是13。而且二者的长度以及表示方式都不相同。这两种类型的不同指出主要体现在两点:
一、 时、分、秒的表示不同。
二、 世纪和年的表示不同。
sql中使用date的时分秒没有采用加1存储方式,而且原值存储。
sql中使用date没有采用世纪、年的方式保持,而是采用了按数值保存的方式。第一位表示低位,第二位表示高位。低位表示最大的值是255。如上面的例子中,212 7×256=2004。
sql> select to_char(to_date('-2004-12-17 16:42:42', 'syyyy-mm-dd hh24:mi:ss'), 'syyyy-mm-dd hh24:mi:ss') dat,
2 dump(to_date('-2004-12-17 16:42:42', 'syyyy-mm-dd hh24:mi:ss')) dump_date from dual;
dat dump_date
-------------------- ---------------------------------------------------
-2004-12-17 16:42:42 typ=13 len=8: 44,248,12,17,16,42,42,0
sql> select dump(to_date('-1-1-1', 'syyyy-mm-dd')) from dual;
dump(to_date('-1-1-1','syyyy-mm-d
---------------------------------
typ=13 len=8: 255,255,1,1,0,0,0,0
对于公元前的日期,oracle从255,255开始保存。公元前的年的保存的值和对应的公元后的年的值相加的和是256,255。如上例中的公元2004年和公元前2004年的值相加:212 44=256,7 248=255。
sql中date类型最后还包括一个0,似乎目前没有使用。
========================================================
oracle基本数据类型存储格式浅析(四) —— rowid类型(一)
oracle的rowid用来唯一标识表中的一条记录,是这条数据在数据库中存放的物理地址。
oracle的rowid分为两种:物理rowid和逻辑rowid。索引组织表使用逻辑rowid,其他类型的表使用物理rowid。其中物理rowid在oracle的8版本中进行了扩展,oracle7及以下版本使用约束rowid,oracle8及以上版本使用扩展rowid。本文描述物理扩展rowid,由于约束rowid仅仅是为了兼容早期版本,因此不做讨论。
sql> create table test_rowid (id number, row_id rowid);
表已创建。
sql> insert into test_rowid values (1, null);
已创建 1 行。
sql> update test_rowid set row_id = rowid where id = 1;
已更新 1 行。
sql> commit;
提交完成。
sql> select rowid, row_id from test_rowid;
rowid row_id
------------------ ------------------
aaabnraagaaaacwaaa aaabnraagaaaacwaaa
oracle的物理扩展rowid有18位,每位采用64位编码,分别用a~z、a~z、0~9、 、/ 共64个字符表示。a表示0,b表示1,……z表示25,a表示26,……z表示51,0表示52,……,9表示61, 表示62,/表示63。
rowid具体划分可以分为4部分。
1. oooooo:前6位表示data object number,将起转化位数字后匹配dba_objects中的data_object_id,可以确定表信息。
如上面例子中的data object number是aaabnr,转化位数字是1×64×64 39×64 17。
sql> select owner, object_name from dba_objects
2 where data_object_id = 1*64*64 39*64 17;
owner object_name
------------------------------ -----------------------------
yangtk test_rowid
2. fff:第7到9位表示相对表空间的数据文件号。
上面的例子中是aag,表示数据文件6。
sql> select file_name, tablespace_name from dba_data_files where relative_fno = 6;
file_name tablespace_name
--------------------------------------------- ---------------
e:oracleoradatatestyangtk01.dbf yangtk
3. bbbbbb:第10到15位表示这条记录在数据文件中的第几个block中。
上面的例子是aaaacw,转化位数字是2×64+22,表示这条记录在数据文件中的第150个block。
4. rrr:最后3位表示这条记录是block中的第几条记录。
上面的例子是aaa,表示第0条记录(总是从0开始计数)。
sql> alter system dump datafile 6 block 150;
系统已更改。
sql> select row_id, dump(row_id, 16) dump_rowid from test_rowid;
row_id dump_rowid
------------------ -------------------------------------------------
aaabnraagaaaacwaaa typ=69 len=10: 0,0,19,d1,1,80,0,96,0,0
找到对应的dump文件,可以发现类型的信息
*** 2004-12-21 17:58:26.000
*** session id13.91) 2004-12-21 17:58:26.000
start dump data blocks tsn: 6 file#: 6 minblk 150 maxblk 150
buffer tsn: 6 rdba: 0x01800096 (6/150)
scn: 0x0000.2e389c16 seq: 0x01 flg: 0x06 tail: 0x9c160601
frmt: 0x02 chkval: 0xc97d type: 0x06=trans data
block header dump: 0x01800096
object id on block? y
seg/obj: 0x19d1 csc: 0x00.2e389c0f itc: 2 flg: o typ: 1 - data
fsl: 0 fnx: 0x0 ver: 0x01
itl xid uba flag lck scn/fsc
0x01 0x0003.009.00000057 0x0080004b.0042.56 --u- 1 fsc 0x0000.2e389c16
0x02 0x0000.000.00000000 0x00000000.0000.00 ---- 0 fsc 0x0000.00000000
data_block_dump,data header at 0x651105c
===============
tsiz: 0x3fa0
hsiz: 0x14
pbl: 0x0651105c
bdba: 0x01800096
76543210
flag=--------
ntab=1
nrow=1
frre=-1
fsbo=0x14
fseo=0x3f89
avsp=0x3f7b
tosp=0x3f7b
0xeti[0] nrow=1 offs=0
0x12ri[0] offs=0x3f89
block_row_dump:
tab 0, row 0, @0x3f89
tl: 17 fb: --h-fl-- lb: 0x1 cc: 2
col 0: [ 2] c1 02
col 1: [10] 00 00 19 d1 01 80 00 96 00 00
end_of_block_dump
end dump data blocks tsn: 6 file#: 6 minblk 150 maxblk 150
有时需要查看表的dump信息,但是很难准确定位表中数据开始于哪个block,根据rowid中包含的信息就可以方便的找到起始block。
下面简单描述一下rowid类型是如何存储的。
sql> select row_id, dump(row_id, 16) dump_rowid from test_rowid;
row_id dump_rowid
------------------ -------------------------------------------------
aaabnraagaaaacwaaa typ=69 len=10: 0,0,19,d1,1,80,0,96,0,0
前4位表示rowid的前6位,也就是data_object_id信息。数据以数值的格式保存。
sql> select to_number('19d1', 'xxxxxx') from dual;
to_number('19d1','xxxxxx')
--------------------------
6609
sql> select 1*64*64 39*64 17 from dual;
1*64*64 39*64 17
----------------
6609
这里存在一个问题,根据rowid的取值范围,object_data_id最大的值是64的6次方,而根据dump,oracle只用了4位保存,因此取值范围是256的4次方。
sql> set numwid 12
sql> select power(64, 6), power(256, 4), power(64, 6)/power(256, 4) from dual;
power(64,6) power(256,4) power(64,6)/power(256,4)
------------ ------------ ------------------------
68719476736 4294967296 16
可见,object_data_id的最大值是4294967296,当超过这个值时会出现重复的情况。(当然,现实中不大可能)。
后面4位比较特殊,是数据文件号和block数的“和”值构成。
数据文件的数值乘64后保存在5、6位上。
sql> select to_number('0180', 'xxxx') from dual;
to_number('0180','xxxx')
------------------------
384
sql> select 6*64 from dual;
6*64
------------
384
同时,6位block的值,也保存在这4位上,并与数据文件转存结果相加。仍然是以数字格式存放。
sql> select to_number('96', 'xxx') from dual;
to_number('96','xxx')
---------------------
150
sql> select 2*64 22 from dual;
2*64 22
----------
150
由于采用两位保存数据文件的值,且最小单位是64,因此,rowid中可以保存的数据文件数是1024,超过1024会造成rowid的重复。
sql> select 256*256/64 from dual;
256*256/64
----------
1024
由于block的值和数据文件共用这4位,因此block的第3位最大值应小于64,这样才能保证rowid的不重复。因此block值的最大值应该是4194304。
sql> select 64*256*256 from dual;
64*256*256
----------
4194304
最后两位保存block中记录的值。这个值的最大值是65536。
sql> select 256*256 from dual;
256*256
----------
65536
================================================
oracle基本数据类型存储格式浅析(四)—— rowid类型(二)
oracle的文档上没有介绍逻辑rowid的编码规则,而且通过dump的结果也很难反推出编码规则。因此,本文只简单讨论一下逻辑rowid的存储。
下面来看例子。
sql> create table test_index (id number primary key, name varchar2(20)) organization index;
表已创建。
sql> insert into test_index values (1, 'a');
已创建 1 行。
sql> commit;
提交完成。
sql> col dump_rowid format a60
sql> select rowid, dump(rowid) dump_rowid from test_index;
rowid dump_rowid
--------------------------- ----------------------------------------
*bafab4wcwql typ=208 len=10: 2,4,1,64,7,140,2,193,2,254
逻辑rowid的dump结果前两位都是2和4,最后一位都是254,(我还没有发现其他的情况),由于逻辑rowid和主键的值有关,所以长度是不定的,因此应该是用来表示开始和结束的。
第3、4位和物理rowid一样,表示的是相对表空间的数据文件号乘以64的值。
第5、6位表示这条记录在数据文件的第几个block中。
从第7位开始到dump结果的倒数第二位,表示主键的值。首先是主键中第一个字段的长度,这里是2,然后是主键的值,由于是number类型,因此193,2表示数值1。如果是多个字段组成的主键,第一个字段之后是第二个字段的长度,然后是第二个字段的值……。
sql> select (1*256 64)/64 from dual;
(1*256 64)/64
-------------
5
sql> select 7*256 140 from dual;
7*256 140
----------
1932
sql> alter system dump datafile 5 block 1932;
系统已更改。
找到相应的dump文件,可以发现刚才插入的记录。
dump file fracleadmintest4udumptest4_ora_3828.trc
thu dec 23 00:17:53 2004
oracle v9.2.0.4.0 - production vsnsta=0
vsnsql=12 vsnxtr=3
windows 2000 version 5.1 service pack 1, cpu type 586
oracle9i enterprise edition release 9.2.0.4.0 - production
with the partitioning, oracle label security, olap and oracle data mining options
jserver release 9.2.0.4.0 - production
windows 2000 version 5.1 service pack 1, cpu type 586
instance name: test4
redo thread mounted by this instance: 1
oracle process number: 9
windows thread id: 3828, image: oracle.exe
*** 2004-12-23 00:17:53.361
*** session id8.82) 2004-12-23 00:17:53.301
start dump data blocks tsn: 5 file#: 5 minblk 1932 maxblk 1932
buffer tsn: 5 rdba: 0x0140078c (5/1932)
scn: 0x0000.00e9f122 seq: 0x01 flg: 0x02 tail: 0xf1220601
frmt: 0x02 chkval: 0x0000 type: 0x06=trans data
block header dump: 0x0140078c
object id on block? y
seg/obj: 0x1e48 csc: 0x00.e9f113 itc: 2 flg: e typ: 2 - index
brn: 0 bdba: 0x1400789 ver: 0x01
inc: 0 exflg: 0
itl xid uba flag lck scn/fsc
0x01 0x0000.000.00000000 0x00000000.0000.00 ---- 0 fsc 0x0000.00000000
0x02 0x0005.008.000000e7 0x00800226.005c.24 --u- 1 fsc 0x0000.00e9f122
leaf block dump
===============
header address 71963236=0x44a1264
kdxcolev 0
kdxcolev flags = - - -
kdxcolok 0
kdxcoopc 0x90: opcode=0: iot flags=i-- is converted=y
kdxconco 1
kdxcosdc 0
kdxconro 1
kdxcofbo 38=0x26
kdxcofeo 8026=0x1f5a
kdxcoavs 7988
kdxlespl 0
kdxlende 0
kdxlenxt 0=0x0
kdxleprv 0=0x0
kdxledsz 0
kdxlebksz 8036
row#0[8026] flag: k----, lock: 2
col 0; len 2; (2): c1 02
tl: 5 fb: --h-fl-- lb: 0x0 cc: 1
col 0: [ 1]
dump of memory from 0x044a31c7 to 0x044a31c8
44a31c0 61010100 [...a]
----- end of leaf block dump -----
end dump data blocks tsn: 5 file#: 5 minblk 1932 maxblk 1932
可以看到,根据dump结果的3、4、5、6位可以定位记录的物理位置。
需要注意的是,索引组织表以主键的顺序存储数据,因此插入、更新和删除数据都可能造成一条记录的物理位置发生变化,这时通过rowid中的datafile和block的信息可能就无法正确定位到记录的物理位置。当根据逻辑rowid访问索引组织表时,首先会根据datafile和block信息去找到相应的block,检查数据是否在这个block中,如果不在,就通过逻辑rowid中的主键信息去通过索引扫描,找到这条记录。这就是oracle文档在提到的physical guess。
下面看一个由字符串和日期组成联合主键的例子。
sql> create table test_index2 (id char(4), time date,
2 constraint pk_test_index2 primary key (id, time)) organization index;
表已创建。
sql> insert into test_index2 values ('1', sysdate);
已创建 1 行。
sql> col dump_rowid format a75
sql> select rowid, dump(rowid) dump_rowid from test_index2;
rowid dump_rowid
---------------------------- ------------------------------------------------------------------
*bafab5qemsagiad4aawxasmt/g typ=208 len=20: 2,4,1,64,7,148,4,49,32,32,32,7,120,104,12,23,1,35,19,254
可以看出,第7位是字段id的长度4,然后是字符串1和三个空格的ascii码,这是字符串的存储格式,后面跟着的7是字段time长度,后面七位是日期的存储格式。在逻辑rowid中,数值、字符和日期类型的存储格式都和它们本身的存储格式一致,这里不在赘述。
一般情况下,使用一位来表示长度,但是如果长度超过了127(16进制dump的结果是7f),则长度开始用两位表示。第一位以8开头,这个8只是标识位,表明长度字段现在由两位来表示。例如长度128表示位8080,而支持的最大值3800表示为8ed8。
====================================================
oracle基本数据类型存储格式浅析(五) —— raw类型
和其他数据类型相比,raw类型的存储显得直观多了,它和select时数据展示的值完全一样。(select时是按照16进制展示的)
sql> create table test_raw (id number, raw_date raw(10));
表已创建。
sql> insert into test_raw values (1, hextoraw('ff'));
已创建 1 行。
sql> drop table test_raw;
表已丢弃。
sql> create table test_raw (raw_col raw(10));
表已创建。
sql> insert into test_raw values (hextoraw('ff'));
已创建 1 行。
sql> insert into test_raw values (hextoraw('0'));
已创建 1 行。
sql> insert into test_raw values (hextoraw('23fc'));
已创建 1 行。
sql> insert into test_raw values (hextoraw('fffffffffff'));
已创建 1 行。
sql> insert into test_raw values (hextoraw('ffffffffffffffffffff'));
已创建 1 行。
sql> insert into test_raw values (utl_raw.cast_to_raw('051'));
已创建 1 行。
sql> select raw_col, dump(raw_col, 16) dump_raw from test_raw;
raw_col dump_raw
-------------------- -----------------------------------------------
ff typ=23 len=1: ff
00 typ=23 len=1: 0
23fc typ=23 len=2: 23,fc
0fffffffffff typ=23 len=6: f,ff,ff,ff,ff,ff
ffffffffffffffffffff typ=23 len=10: ff,ff,ff,ff,ff,ff,ff,ff,ff,ff
303531 typ=23 len=3: 30,35,31
已选择6行。
raw类型的存储很简单,对比字段的查询结果和dump的结果就一目了然了。
需要注意的是,两种转化为raw的函数之间的差别。当使用hextoraw时,会把字符串中数据当作16进制数。而使用utl_raw.cast_to_raw时,直接把字符串中每个字符的ascii码存放到raw类型的字段中。
sql> insert into test_raw values ('gg');
insert into test_raw values ('gg')
*
error 位于第 1 行:
ora-01465: 无效的十六进制数字
sql> insert into test_raw values (hextoraw('gg'));
insert into test_raw values (hextoraw('gg'))
*
error 位于第 1 行:
ora-01465: 无效的十六进制数字
sql> insert into test_raw values (utl_raw.cast_to_raw('gg'));
已创建 1 行。
sql> select raw_col, dump(raw_col, 16) dump_raw from test_raw;
raw_col dump_raw
-------------------- ----------------------------------------------
ff typ=23 len=1: ff
00 typ=23 len=1: 0
23fc typ=23 len=2: 23,fc
6767 typ=23 len=2: 67,67
0fffffffffff typ=23 len=6: f,ff,ff,ff,ff,ff
ffffffffffffffffffff typ=23 len=10: ff,ff,ff,ff,ff,ff,ff,ff,ff,ff
303531 typ=23 len=3: 30,35,31
已选择7行。
=========================================
oracle基本数据类型存储格式浅析(三) —— 日期类型(四)
本文对timestamp with local time zone和timestamp with time zone类型的存储格式进行简单的说明。
sql> create table test_timestamp(time1 timestamp(9), time2 timestamp(6) with local time zone,
2 time3 timestamp(4) with time zone);
表已创建。
sql> insert into test_timestamp values (systimestamp, systimestamp, systimestamp);
已创建 1 行。
sql> select * from test_timestamp;
time1
----------------------------------------------------
time2
----------------------------------------------------
time3
----------------------------------------------------
11-1月 -05 11.08.15.027000000 下午
11-1月 -05 11.08.15.027000 下午
11-1月 -05 11.08.15.0270 下午 08:00
sql> select dump(time1, 16), dump(time2, 16), dump(time3, 16) from test_timestamp;
dump(time1,16)
-------------------------------------------------------------
dump(time2,16)
-------------------------------------------------------------
dump(time3,16)
-------------------------------------------------------------
typ=180 len=11: 78,69,1,b,18,9,10,1,9b,fc,c0
typ=231 len=11: 78,69,1,b,18,9,10,1,9b,fc,c0
typ=181 len=13: 78,69,1,b,10,9,10,1,9b,fc,c0,1c,3c
可以发现,如果客户端和数据库中的时区是一致的,那么timestamp和timestamp with local time zone存储的数据是完全一样的。
timestamp with time zone则略有不同,它保存的是0时区的时间,和所处的时区信息。
修改客户端主机的时区,由东8区( 8区)改为0时区。
sql> insert into test_timestamp values (systimestamp, systimestamp, systimestamp);
已创建 1 行。
修改客户端主机的时区,改为西5区(-5时区)。
sql> insert into test_timestamp values (systimestamp, systimestamp, systimestamp);
已创建 1 行。
修改客户端主机的时区,改为西12区(-12时区)。
sql> insert into test_timestamp values (systimestamp, systimestamp, systimestamp);
已创建 1 行。
修改客户端主机的时区,改为东13区( 13时区)。
sql> insert into test_timestamp values (systimestamp, systimestamp, systimestamp);
已创建 1 行。
修改客户端主机的时区,改为西3.5区(-3.5时区)。
sql> insert into test_timestamp values (systimestamp, systimestamp, systimestamp);
已创建 1 行。
修改客户端主机的时区,改为东9.5区( 9.5时区)。
sql> insert into test_timestamp values (systimestamp, systimestamp, systimestamp);
已创建 1 行。
sql> commit;
提交完成。
修改客户端主机的时区,改回东8区( 8时区)。
sql> select * from test_timestamp;
time1
-----------------------------------------------
time2
-----------------------------------------------
time3
-----------------------------------------------
11-1月 -05 11.08.15.027000000 下午
11-1月 -05 11.08.15.027000 下午
11-1月 -05 11.08.15.0270 下午 08:00
11-1月 -05 03.11.43.746000000 下午
11-1月 -05 11.11.43.746000 下午
11-1月 -05 03.11.43.7460 下午 00:00
11-1月 -05 10.14.08.987000000 上午
11-1月 -05 11.14.08.987000 下午
11-1月 -05 10.14.08.9870 上午 -05:00
11-1月 -05 03.15.01.732000000 上午
11-1月 -05 11.15.01.732000 下午
11-1月 -05 03.15.01.7320 上午 -12:00
12-1月 -05 04.20.21.522000000 上午
11-1月 -05 11.20.21.522000 下午
12-1月 -05 04.20.21.5220 上午 13:00
11-1月 -05 02.15.16.567000000 下午
12-1月 -05 01.45.16.567000 上午
11-1月 -05 02.15.16.5670 下午 -03:30
12-1月 -05 03.16.54.992000000 上午
12-1月 -05 01.46.54.992000 上午
12-1月 -05 03.16.54.9920 上午 09:30
已选择7行。
sql> select dump(time1, 16), dump(time2, 16), dump(time3, 16) from test_timestamp;
dump(time1,16)
-------------------------------------------------------------
dump(time2,16)
-------------------------------------------------------------
dump(time3,16)
-------------------------------------------------------------
typ=180 len=11: 78,69,1,b,18,9,10,1,9b,fc,c0
typ=231 len=11: 78,69,1,b,18,9,10,1,9b,fc,c0
typ=181 len=13: 78,69,1,b,10,9,10,1,9b,fc,c0,1c,3c
typ=180 len=11: 78,69,1,b,10,c,2c,2c,77,e,80
typ=231 len=11: 78,69,1,b,18,c,2c,2c,77,e,80
typ=181 len=13: 78,69,1,b,10,c,2c,2c,77,e,80,14,3c
typ=180 len=11: 78,69,1,b,b,f,9,3a,d4,6c,c0
typ=231 len=11: 78,69,1,b,18,f,9,3a,d4,6c,c0
typ=181 len=13: 78,69,1,b,10,f,9,3a,d4,6c,c0,f,3c
typ=180 len=11: 78,69,1,b,4,10,2,2b,a1,6f,0
typ=231 len=11: 78,69,1,b,18,10,2,2b,a1,6f,0
typ=181 len=13: 78,69,1,b,10,10,2,2b,a1,6f,0,8,3c
typ=180 len=11: 78,69,1,c,5,15,16,1f,1d,16,80
typ=231 len=11: 78,69,1,b,18,15,16,1f,1d,16,80
typ=181 len=13: 78,69,1,b,10,15,16,1f,1d,16,80,21,3c
typ=180 len=11: 78,69,1,b,f,10,11,21,cb,bb,c0
typ=231 len=11: 78,69,1,c,2,2e,11,21,cb,bb,c0
typ=181 len=13: 78,69,1,b,12,2e,11,21,cb,bb,c0,11,1e
typ=180 len=11: 78,69,1,c,4,11,37,3b,20,b8,0
typ=231 len=11: 78,69,1,c,2,2f,37,3b,20,b8,0
typ=181 len=13: 78,69,1,b,12,2f,37,3b,20,b8,0,1d,5a
sql> select to_number('1c', 'xxx'), to_number('3c', 'xxx') from dual;
to_number('1c','xxx') to_number('3c','xxx')
--------------------- ---------------------
28 60
sql> select to_number('14', 'xxx'), to_number('3c', 'xxx'), to_number('143c', 'xxxxxxx') from dual;
to_number('14','xxx') to_number('3c','xxx')
--------------------- ---------------------
20 60
sql> select to_number('3c', 'xxx') , to_number('1e', 'xxx'), to_number('5a', 'xxx') from dual;
to_number('3c','xxx') to_number('1e','xxx') to_number('5a','xxx')
--------------------- --------------------- -------------------
60 30 90
可以看出,修改时区会导致系统timestamp时间发生变化,但是对于timestamp with local time zone类型,总是将系统的时间转化到数据库服务器上时区的时间进行存储。
timestamp with time zone保存的是当前时间转化到0时区的对应的时间,并通过最后两位来保存时区信息。
第一位表示时区的小时部分。0时区用0x14表示。东n区在这个基础上加n,西n区在这个基础上减n。我们所处的东8区表示为0x1c。西5区表示为0xf。
第二位表示时区的分钟部分。标准是0x3c,即60分钟。对于东时区的半区,在这个基础上加上30分钟,如果是西时区,则减去30分钟。
阅读(2977) | 评论(0) | 转发(0) |