DUL工具是Oracle数据库挽救数据的最后手段,你用到DUL的时候,大部分情况下,数据库已经不能启动了,甚至有些数据文件已经损坏了。那么DUL又是怎样在这些极端的情况下把数据导出来的呢?下面我们就来一步步的分析它的工作原理。如果你想自己开发一个类似的工具,这篇文章也会告诉你有那些工作要做,该怎样去做。

Oracle数据库实际上是一堆数据的集合,数据存储在表中,通过一些软件来管理这些数据,其中读取数据只是这些功能中的一小部分。在这些数据中最重要的就是用户数据,它们通常保存在数据文件中,按照一定的格式存储。这些数据怎样解释成我们看到的样子,这就需要元数据的帮忙了,通常我们把元数据叫做数据字典。下面我们先来看看数据字典是什么样子。

数据字典

Oracle的数据字典也是由一些表组成的。其中最主要的有obj$,tab$,col$这三张表,obj$表中指定了对象的名称,对象ID,对象的数据ID等,当然也指定了对象的属主ID。tab$表指定了表的一些属性,最主要的是它指定了表开始的位置,在哪个数据文件中,从哪个块开始。col$表指定了表的列属性,包括列的名称,列ID,列在段中的ID,列的类型,长度等等,有了col$中的信息,Oracle就能解释存储在数据块中的表的格式了。

表在数据文件中的位置

上面我们说过,tab$表中有两个字段指定了表开始的位置,一个叫FILE#,指示表在哪个数据文件中,另一个叫BLOCK#,指示表从哪个块开始。这个开始的块叫段头块,里面包含了一个个extent地址范围,叫做extent map,extent是由连续的数据块构成的。有了这个extent map,就可以从这些块中读取数据了,这些块就是表的数据块。如果一个表非常大,段头块并不能包含所有的extent,那怎么办呢?Oracle会在这个块中指定下一个extent map的块地址,直到所有的extent map都列举完毕。

有了上面的知识,我们就可以从数据文件中读取表的数据了。在开始之前,好像还有一点问题,怎样从数据文件中读取数据字典表呢?数据字典也是表,表的段头位置又是从tab$中得到的,这时我们还没有读到tab$的数据,好像陷入死循环了。别急,Oracle在启动的时候会遇到跟我们一样的问题,它怎么来解决呢?原来Oracle启动时,先在内存中创建一个叫做bootstrap$的表,这个表中存储了一些建表语句,其中就包括了上面提到的obj$,tab$和col$,有趣的是,在每个建表语句的后面,还指示了这个表的段头块位置,那么这下就方便了,直接到这个位置找到extent map,遍历所有的extent map找到属于这个表的数据块,解析数据块中的内容,就可以得到数据字典的信息了。
看到这里,你还有点困惑,那么说明你思考的深入,是的,bootstrap$表的开始位置在哪里呢?它保存在1号数据文件的1号块中,这个块包含文件头信息,里面有个叫root dba的字段,包含的地址就是bootstrap$表的段头块地址。

数据块

数据块中包含了表中的数据,它也是有一定结构的,开始是块头信息,事务信息,下面是ITL,ITL大小是固定的,叫做事务槽,块中包含几个事务槽,在事务信息中指定。再后面就是数据头信息,紧接着是表目录(table directory)信息,后面是行目录(row directory),行目录指定了每行数据的位置。再后面就是行数据了,行数据是从块的底部往上来存储的,所以在行目录和真正的数据之间可能会有一部分空闲的空间。数据块的结构比较复杂,好在Oracle有一个工具叫做bbed,可以打开一个数据块,它详细定义了这些数据结构,包含数据结构的各个字段,可以方便的看到数据存储的细节。

long数据类型

LONG类型的数据一般比较长,很容易造成行连接,当然如果一个表创建时字段过多,也会造成行连接,就是说一行数据分布在了两个或多个数据块之间,这时怎么办呢?Oracle在每行数据开始都有一个叫做fb的字段,指示数据是否连接到了下一个块,如果到了下一个块,那么就会出现一个叫做nrid的字段,用来指示后面的数据连接到了哪里,这是一个地址,代表了在哪一个块的哪个偏移量。如果下一个块还没有完全容纳这一行数据,那么会有下一个nrid,一直连接下去,直到数据行结束。

lob数据类型

LOB是大对象数据类型,是为了替代LONG类型引入的,当数据量比较小时,它存储在表的块内,如果数据比较大,就存储在表外的一个段中,这个段叫做LOB段。LOB数据在LOB段中的位置,由一个叫做定位器的字段来指定,英文名称叫做Lob Locator,这个定位器存储在表的数据块中,这样读到LOB字段时,就可以通过定位器找到LOB数据。

lob index

其实,LOB的存储是相当复杂的,默认的情况下,为了方便存储,LOB列在表的数据块中,不仅存储了定位器,还存储了一些LOB数据块的地址,通过这些地址把LOB数据读出来。但是这些存储的地址个数是有限制的,这取决于表数据块中LOB信息的长度,默认情况下最多是12个,如果超出了,就要用到定位器了,定位器不能直接找到LOB段的块位置,实际上他是LOB index的一个键值,通过这个键,在LOB索引中找到一系列的LOB块的地址,通过这些地址把LOB数据读出来。

SecureFile

上面谈到的LOB存储格式叫做BasicFile LOB,从11g开始,Oracle引入了一种新的LOB存储格式,叫做SecureFile LOB。它几乎把LOB index取消了,而是把LOB的块地址直接放在了LOB段的头块中,通过头块中的地址可以直接读取LOB数据。当然如果LOB数据量很大很大,头块也放不下这么多地址,那怎么办呢?Oracle在头块中设置了四个地址,分别叫做dba0,dba1,dba2,dba3。这是一个四级的内部树结构,dba0相当于一个叶子节点,管理了很多LOB数据块地址,当dba0满了,就会出现dba1,是dba0的上级节点,它又管理了很多类似dba0的叶子,每个叶子节点块都包含了很多LOB数据块的地址,dba1满了,就会出现dba2节点,类推上去,到了dba3时,能管理的数据量已经远超过了LOB数据量的最大限制,这样所有的LOB数据都能通过这个结构遍历读取了。

recyclebin

如果你删除了表,从11g开始默认情况下并没有真正删除,而是把表名改变了,原来的表名存储在了一个叫回收站的表中,如果你改变了主意,还可以通过命令恢复回来,对误删了表是一个好消息。由于它与普通表没有区别,所以通过上面的知识,我们就能恢复回来。

truncate table

如果一个表被截断了,那可能你就真的访问不了原来的数据了,如果现在后悔了,也只能去撞墙了。用我们前面介绍的方法能不能找回数据呢?找到表的段头块,dump出来看看,你会发现段头块中的extent map已经被清除了,这就没法通过extent map把数据遍历出来了。办法总是有的,数据不是都存储在数据文件中吗?那我们把所有数据文件中的块都扫描一遍,把跟这个表的ID一致的那些块都找出来,然后从这些块中把数据都分析出来,不就可以了吗?只是花费的时间多一些,并且要保证不能遗漏了数据文件,实践证明还是可以把数据读出来的。

drop table

有了上面截断表的经验,删除的表也就好处理了。段头块的改变几乎与截断表是一样的。与截断表不同的是,你要先把数据字典中删除的记录恢复出来,obj$,tab$,col$表中关于这个表的记录都被删除了,那么怎样恢复呢?记得前面我们提到过在每行数据前面都有一个叫做fb的字段,其实Oracle并没有把这条数据清除掉,只是在fb字段上做了一个标记,去除这个标记,这些记录就都恢复了。下面再把数据文件扫描一遍,找到属于这个表的块,就能把数据恢复了。

数据字典损坏

最严重的情况就是数据文件中有部分已经损坏了,那么就不能保证完全恢复数据了。那么首先还是要尝试读取数据字典,Oracle对基础的数据字典表存储的段头块位置都是固定的,找一个相同版本的数据库,从bootstrap$表中查找到数据字典的段头位置,或者从tab$中找到段头位置,然后尝试从这些地方导出数据字典,如果能导出数据字典,剩下的工作就跟前面一样了。
最坏的情况就是系统表空间的数据文件丢失或者严重损坏,已经无法导出数据字典了,那么这时怎么办呢?那么只有通过数据来重建数据字典了,还是把所有数据文件扫描一遍,记录段头块的位置,每个段头块会对应一个表或一个分区,这样表的段头位置就找到了,接下来要做的就是重建col$中的字段,主要是数据类型,长度等。有些数据类型的长度是固定的,比如日期类型,时间戳类型,很好猜测。数字类型也有自己的特点比较好确定,剩下的就是字符类型了,大部分猜测不到的都可以先当做字符类型处理。然后根据重建的数据字典导出一部分数据,这时就要通过人工比对,把字段类型确定清楚,然后就可以导出数据了。