技术开发 频道

使用DBMS_REPAIR包修复损坏数据块

  2、恢复数据

  使用DBMS_REPAIR包的目的不仅是为了使表重新可以访问,而且使用这个包还能在一定程度上恢复被因坏块而无法读取的数据。

  由于坏块产生在表上,因此索引是可以访问,所有被索引的列的数据都可以恢复。遗憾的是,Oracle的文档并没有给出恢复的方法,我查询了METALINK和ASKTOM也没有找到相应的答案,所以,恢复的工作只能靠自己摸索进行。这部分的内容完全是建立在我对Oracle数据类型理解的基础上的,虽然我已经对我的程序进行过测试,但是由于没有文档可以参考,而且测试环境相对比较单一,因此,我并不能确保我提供的包和函数一定没有错误。如果想将这种方法应用的正式系统中,请首先做好备份工作。

  言归正传,在上面的步骤中,使用DUMP_ORPHAN_KEYS过程保存了坏块中的索引键值,下面就通过这些保存的键值来进行数据的恢复。

  先看一下ORPHAN_KEY_TABLE的表结构:

SQL> DESC ORPHAN_KEY_TABLE 名称 是否为空? 类型 -------------------------------------- -------- -------------- SCHEMA_NAME NOT NULL VARCHAR2(30) INDEX_NAME NOT NULL VARCHAR2(30) IPART_NAME VARCHAR2(30) INDEX_ID NOT NULL NUMBER TABLE_NAME NOT NULL VARCHAR2(30) PART_NAME VARCHAR2(30) TABLE_ID NOT NULL NUMBER KEYROWID NOT NULL ROWID KEY NOT NULL ROWID DUMP_TIMESTAMP NOT NULL DATE

  由于字段名基本上都是自解释的,这里就不再过多描述了,需要说明的是KEYROWID和KEY两个字段。

  KEYROWID存放的是该索引键值对应的表中的ROWID,而KEY保存的就是索引的键值。

  但是查询KEY的值发现,并非像想象中一样,存放的是1、2、3……或ALL_TABLES、ACCESS$……等值,而是以逻辑ROWID方式存放的。

SQL> SELECT KEY FROM ORPHAN_KEY_TABLE WHERE INDEX_NAME = 'IND_TEST_ID' AND ROWNUM = 1; KEY --------------------------------------------------------------- *BAAAAAACwQL+ SQL> SELECT KEY FROM ORPHAN_KEY_TABLE WHERE INDEX_NAME = 'IND_TEST_NAME' AND ROWNUM = 1; KEY --------------------------------------------------------------- *BAAAAAAHQUNDRVNTJP4

  如何将逻辑ROWID转化为NUMBER或VARCHAR2类型呢?Oracle的文档并没有找到相应的解决方法。

  于是抱着尝试的想法对这个字段DUMP一下。

SQL> SELECT DUMP(KEY) FROM ORPHAN_KEY_TABLE WHERE INDEX_NAME = 'IND_TEST_ID' AND ROWNUM < 6; DUMP(KEY) ---------------------------------------------------------------- Typ=208 Len=10: 2,4,0,0,0,0,2,193,2,254 Typ=208 Len=10: 2,4,0,0,0,0,2,193,3,254 Typ=208 Len=10: 2,4,0,0,0,0,2,193,4,254 Typ=208 Len=10: 2,4,0,0,0,0,2,193,5,254 Typ=208 Len=10: 2,4,0,0,0,0,2,193,6,254

  这回看到希望了。还记得上面修改数据文件时123的编码吗?是不是和第一个查询中的结果很相似?

  2,4,0,0,0,0前几位是不变的,最后一位254也是不变的。中间的部分就是有意义的数字了。其中第一个2表示长度是2,193表示最高位是个位,2表示最高位上的值是1,也就是说,第一个键值是数字1。(可以参考我对逻辑ROWID的存储格式进行分析的帖子)

SQL> SELECT DUMP(1) FROM DUAL; DUMP(1) ------------------ Typ=2 Len=2: 193,2

  Oracle把数据在文件中的存储格式保存在ROWID字段中了。根据这个假设,我们看看字符串是不是以同样方式存储的。

SQL> SELECT DUMP(KEY) FROM ORPHAN_KEY_TABLE WHERE INDEX_NAME = 'IND_TEST_NAME' AND ROWNUM < 6; DUMP(KEY) ---------------------------------------------------------------- Typ=208 Len=15: 2,4,0,0,0,0,7,65,67,67,69,83,83,36,254 Typ=208 Len=17: 2,4,0,0,0,0,9,65,71,71,88,77,76,73,77,80,254 Typ=208 Len=23: 2,4,0,0,0,0,15,65,71,71,88,77,76,73,78,80,85,84,84,89,80,69,254 Typ=208 Len=22: 2,4,0,0,0,0,14,65,76,76,95,65,76,76,95,84,65,66,76,69,83,254 Typ=208 Len=17: 2,4,0,0,0,0,9,65,76,76,95,65,80,80,76,89,254

  显然7代表长度,后面跟着的明显就是ASCII编码。

SQL> SELECT CHR(65)||CHR(67)||CHR(67)||CHR(69)||CHR(83)||CHR(83)||CHR(36) FROM DUAL; CHR(65) ------- ACCESS$

  知道这个规律,就可以着手进行恢复了。我写了一个包,用来恢复各种数据类型的数据。

SQL> CREATE OR REPLACE PACKAGE P_DUMPROWID 2 AS 3 --内部函数,由于去掉DUMP结果中的开头描述部分 4 FUNCTION F_GET_VALUE(P_STR IN VARCHAR2) RETURN VARCHAR2; 5 PRAGMA RESTRICT_REFERENCES (F_GET_VALUE, WNDS, WNPS, RNDS, RNPS); 6 7 --整理DUMP结果中16进制数,一位数的情况前面补0,并过滤逗号 8 FUNCTION F_ADD_PREFIX_ZERO (P_STR IN VARCHAR2, P_POSITION IN NUMBER) RETURN VARCHAR2; 9 PRAGMA RESTRICT_REFERENCES (F_ADD_PREFIX_ZERO, WNDS, WNPS, RNDS, RNPS); 10 11 --根据输入ROWID、方案名、索引名和索引中列的位置,将ROWID中的列的值提取出来 12 FUNCTION F_DUMP_FROM_ROWID 13 ( 14 P_DUMP_ROWID IN UROWID, 15 P_OWNER IN VARCHAR2, 16 P_INDEX_NAME IN VARCHAR2, 17 P_COLUMN_POSITION IN NUMBER DEFAULT 1 18 ) 19 RETURN VARCHAR2; 20 PRAGMA RESTRICT_REFERENCES (F_DUMP_FROM_ROWID, WNDS, WNPS); 21 22 END P_DUMPROWID; 23 / 程序包已创建。 SQL> CREATE OR REPLACE PACKAGE BODY P_DUMPROWID AS 2 3 FUNCTION F_GET_VALUE(P_STR IN VARCHAR2) RETURN VARCHAR2 AS 4 BEGIN 5 RETURN (SUBSTR(P_STR, INSTR(P_STR, ':') + 2)); 6 END F_GET_VALUE; 7 8 FUNCTION F_ADD_PREFIX_ZERO (P_STR IN VARCHAR2, P_POSITION IN NUMBER) RETURN VARCHAR2 9 AS 10 V_STR VARCHAR2(30000) := P_STR; 11 V_POSITION NUMBER := P_POSITION; 12 V_STR_PART VARCHAR2(2); 13 V_RETURN VARCHAR2(30000); 14 BEGIN 15 WHILE (V_POSITION != 0) LOOP 16 V_STR_PART := SUBSTR(V_STR, 1, V_POSITION - 1); 17 V_STR := SUBSTR(V_STR, V_POSITION + 1); 18 19 IF V_POSITION = 2 THEN 20 V_RETURN := V_RETURN || '0' || V_STR_PART; 21 ELSIF V_POSITION = 3 THEN 22 V_RETURN := V_RETURN || V_STR_PART; 23 ELSE 24 RAISE_APPLICATION_ERROR(-20002, 'DUMP ERROR CHECK THE INPUT ROWID'); 25 END IF; 26 27 V_POSITION := INSTR(V_STR, ','); 28 END LOOP; 29 RETURN REPLACE(V_RETURN , ','); 30 END F_ADD_PREFIX_ZERO; 31 32 FUNCTION F_DUMP_FROM_ROWID 33 ( 34 P_DUMP_ROWID IN UROWID, 35 P_OWNER IN VARCHAR2, 36 P_INDEX_NAME IN VARCHAR2, 37 P_COLUMN_POSITION IN NUMBER DEFAULT 1 38 ) 39 RETURN VARCHAR2 AS 40 V_COLUMN_TYPE DBA_TAB_COLUMNS.DATA_TYPE%TYPE; 41 42 V_LENGTH_STR VARCHAR2(10); 43 V_LENGTH NUMBER DEFAULT 7; 44 V_DUMP_ROWID VARCHAR2(30000); 45 46 V_DATE_STR VARCHAR2(100); 47 TYPE T_DATE IS TABLE OF NUMBER INDEX BY BINARY_INTEGER; 48 V_DATE T_DATE; 49 50 BEGIN 51 52 --根据SCHEMA、INDEX_NAME和COLUMN_NAME得到COLUMN的数据类型 53 SELECT T.DATA_TYPE 54 INTO V_COLUMN_TYPE 55 FROM DBA_IND_COLUMNS I, DBA_TAB_COLUMNS T 56 WHERE I.TABLE_NAME = T.TABLE_NAME 57 AND I.TABLE_OWNER = T.OWNER 58 AND I.COLUMN_NAME = T.COLUMN_NAME 59 AND I.TABLE_OWNER = P_OWNER 60 AND I.INDEX_NAME = P_INDEX_NAME 61 AND I.COLUMN_POSITION = P_COLUMN_POSITION; 62 63 --根据COLUMN在索引中的位置,循环找到这个COLUMN对应的ROWID 64 FOR I IN 1..P_COLUMN_POSITION LOOP 65 66 --如果COLUMN的长度超过127,即表示长度的位超过7f,则开始用两位来存储长度,其中第一位以8开始。 67 SELECT F_GET_VALUE(DUMP(P_DUMP_ROWID, 16, V_LENGTH, 1)) INTO V_LENGTH_STR FROM DUAL; 68 IF SUBSTR(V_LENGTH_STR, 1, 1) = '8' THEN 69 SELECT SUBSTR(F_GET_VALUE(DUMP(P_DUMP_ROWID, 16, V_LENGTH, 2)), 2) INTO V_LENGTH_STR FROM DUAL; 70 V_LENGTH_STR := TO_CHAR(TO_NUMBER(REPLACE(V_LENGTH_STR, ','), 'XXXX')); 71 SELECT F_GET_VALUE(DUMP(P_DUMP_ROWID, 16, V_LENGTH + 2, TO_NUMBER(V_LENGTH_STR))) INTO V_DUMP_ROWID 72 FROM DUAL; 73 V_LENGTH := V_LENGTH + TO_NUMBER(V_LENGTH_STR) + 2; 74 ELSE 75 V_LENGTH_STR := TO_CHAR(TO_NUMBER(V_LENGTH_STR, 'XXX')); 76 SELECT F_GET_VALUE(DUMP(P_DUMP_ROWID, 16, V_LENGTH + 1, TO_NUMBER(V_LENGTH_STR))) INTO V_DUMP_ROWID 77 FROM DUAL; 78 V_LENGTH := V_LENGTH + TO_NUMBER(V_LENGTH_STR) + 1; 79 END IF; 80 END LOOP; 81 82 IF V_COLUMN_TYPE = 'VARCHAR2' OR V_COLUMN_TYPE = 'CHAR' THEN 83 84 V_DUMP_ROWID :=F_ADD_PREFIX_ZERO(V_DUMP_ROWID || ',', INSTR(V_DUMP_ROWID, ',')); 85 86 RETURN(UTL_RAW.CAST_TO_VARCHAR2(V_DUMP_ROWID)); 87 ELSIF V_COLUMN_TYPE = 'NUMBER' THEN 88 89 V_DUMP_ROWID :=F_ADD_PREFIX_ZERO(V_DUMP_ROWID || ',', INSTR(V_DUMP_ROWID, ',')); 90 91 RETURN(TO_CHAR(UTL_RAW.CAST_TO_NUMBER(V_DUMP_ROWID))); 92 ELSIF V_COLUMN_TYPE = 'DATE' THEN 93 94 V_DUMP_ROWID := ',' || V_DUMP_ROWID || ','; 95 96 FOR I IN 1..7 LOOP 97 V_DATE(I) := TO_NUMBER(SUBSTR(V_DUMP_ROWID, INSTR(V_DUMP_ROWID, ',', 1, I) + 1, 98 INSTR(V_DUMP_ROWID, ',', 1, I + 1) - INSTR(V_DUMP_ROWID, ',', 1, I) - 1), 'XXX'); 99 END LOOP; 100 101 V_DATE(1) := V_DATE(1) - 100; 102 V_DATE(2) := V_DATE(2) - 100; 103 104 IF ((V_DATE(1) < 0) OR (V_DATE(2) < 0)) THEN 105 V_DATE_STR := '-' || LTRIM(TO_CHAR(ABS(V_DATE(1)), '00')) || LTRIM(TO_CHAR(ABS(V_DATE(2)), '00')); 106 ELSE 107 V_DATE_STR := LTRIM(TO_CHAR(ABS(V_DATE(1)), '00')) || LTRIM(TO_CHAR(ABS(V_DATE(2)),'00')); 108 END IF; 109 110 V_DATE_STR := V_DATE_STR || '-' || TO_CHAR(V_DATE(3)) || '-' || TO_CHAR(V_DATE(4)) || ' ' || 111 TO_CHAR(V_DATE(5) - 1) || ':' || TO_CHAR(V_DATE(6) - 1) || ':' || TO_CHAR(V_DATE(7) - 1); 112 RETURN (V_DATE_STR); 113 ELSIF V_COLUMN_TYPE LIKE 'TIMESTAMP(_)' THEN 114 115 V_DUMP_ROWID := ',' || V_DUMP_ROWID || ','; 116 117 FOR I IN 1..11 LOOP 118 V_DATE(I) := TO_NUMBER(SUBSTR(V_DUMP_ROWID, INSTR(V_DUMP_ROWID, ',', 1, I) + 1, 119 INSTR(V_DUMP_ROWID, ',', 1, I + 1) - INSTR(V_DUMP_ROWID, ',', 1, I) - 1), 'XXX'); 120 END LOOP; 121 122 V_DATE(1) := V_DATE(1) - 100; 123 V_DATE(2) := V_DATE(2) - 100; 124 125 IF ((V_DATE(1) < 0) OR (V_DATE(2) < 0)) THEN 126 V_DATE_STR := '-' || LTRIM(TO_CHAR(ABS(V_DATE(1)), '00')) || LTRIM(TO_CHAR(ABS(V_DATE(2)), '00')); 127 ELSE 128 V_DATE_STR := LTRIM(TO_CHAR(ABS(V_DATE(1)), '00')) || LTRIM(TO_CHAR(ABS(V_DATE(2)),'00')); 129 END IF; 130 131 V_DATE_STR := V_DATE_STR || '-' || TO_CHAR(V_DATE(3)) || '-' || TO_CHAR(V_DATE(4)) || ' ' || 132 TO_CHAR(V_DATE(5) - 1) || ':' || TO_CHAR(V_DATE(6) - 1) || ':' || TO_CHAR(V_DATE(7) - 1) || '.' || 133 SUBSTR(TO_CHAR(V_DATE(8) * POWER(256, 3) + V_DATE(9) * POWER(256, 2) + V_DATE(10) * 256 + V_DATE(11)), 134 1, TO_NUMBER(SUBSTR(V_COLUMN_TYPE, 11, 1))); 135 RETURN (V_DATE_STR); 136 ELSIF V_COLUMN_TYPE = 'RAW' THEN 137 138 V_DUMP_ROWID :=F_ADD_PREFIX_ZERO(V_DUMP_ROWID || ',', INSTR(V_DUMP_ROWID, ',')); 139 140 RETURN(V_DUMP_ROWID); 141 ELSIF V_COLUMN_TYPE = 'ROWID' THEN 142 143 V_DUMP_ROWID :=F_ADD_PREFIX_ZERO(V_DUMP_ROWID || ',', INSTR(V_DUMP_ROWID, ',')); 144 145 RETURN (DBMS_ROWID.ROWID_CREATE( 146 1, 147 TO_NUMBER(SUBSTR(V_DUMP_ROWID, 1, 8), 'XXXXXXXXXXX'), 148 TRUNC(TO_NUMBER(SUBSTR(V_DUMP_ROWID, 9, 4), 'XXXXXX')/64), 149 TO_NUMBER(MOD(TO_NUMBER(SUBSTR(V_DUMP_ROWID, 9, 4), 'XXXXXX'), 64) || 150 TO_NUMBER(SUBSTR(V_DUMP_ROWID, 13, 4), 'XXXXXXXXXXX')), 151 TO_NUMBER(SUBSTR(V_DUMP_ROWID, 17, 4), 'XXXXXX'))); 152 ELSE 153 RAISE_APPLICATION_ERROR(-20001, 'TYPE NOT VALID OR CAN''T TRANSALTE ' || V_COLUMN_TYPE || ' TYPE'); 154 END IF; 155 EXCEPTION 156 WHEN NO_DATA_FOUND THEN 157 RAISE_APPLICATION_ERROR(-20003, 158 'INVALID SCHEMA_NAME OR INVALID INDEX_NAME OR INVALID COLUMN_NAME OR INVALID COLUMN_POSITION'); 159 END F_DUMP_FROM_ROWID; 160 161 END P_DUMPROWID; 162 / 程序包主体已创建。

  这个包主要对外提供一个函数F_DUMP_FROM_ROWID,另外两个函数是为了包中内部调用的。这个函数的输入参数分别是逻辑ROWID,表所在的SCHEMA,索引名称和抽取的列在索引中的位置。前三个参数分别由ORPHAN_KEY_TABLE表的KEY、SCHEMA_NAME和INDEX_NAME三个字段的值提供,最后一个参数的默认值为1,对于单列索引或复合索引的第一个字段的抽取,可以省略这个参数。

  由于dbms_repair包一般是由DBA执行的,因此一般把这个包也建立在SYS帐户中,如果用普通用户建立,需要对DBA_TAB_COLUMNS和DBA_IND_COLUMNS的单独授权。下面测试一下函数的功能。

SQL> SELECT P_DUMPROWID.F_DUMP_FROM_ROWID(KEY, SCHEMA_NAME, INDEX_NAME) DUMP FROM ORPHAN_KEY_TABLE 2 WHERE INDEX_NAME = 'IND_TEST_ID' AND ROWNUM < 6; DUMP ------------------------------------------- 1 2 3 4 5 SQL> SELECT P_DUMPROWID.F_DUMP_FROM_ROWID(KEY, SCHEMA_NAME, INDEX_NAME, 1) DUMP FROM ORPHAN_KEY_TABLE 2 WHERE INDEX_NAME = 'IND_TEST_NAME' AND ROWNUM < 6; DUMP ------------------------------------------- ACCESS$ AGGXMLIMP AGGXMLINPUTTYPE ALL_ALL_TABLES ALL_APPLY

  好了,剩下的事情就简单了。我们将ORPHAN_KEY_TABLE表中的记录转变后,重新插入到TEST表中即可。

SQL> INSERT INTO YANGTK.TEST (ID, NAME) 2 SELECT 3 P_DUMPROWID.F_DUMP_FROM_ROWID(A.KEY, A.SCHEMA_NAME, A.INDEX_NAME), 4 P_DUMPROWID.F_DUMP_FROM_ROWID(B.KEY, B.SCHEMA_NAME, B.INDEX_NAME) 5 FROM ORPHAN_KEY_TABLE A, ORPHAN_KEY_TABLE B 6 WHERE A.KEYROWID = B.KEYROWID 7 AND A.INDEX_NAME = 'IND_TEST_ID' 8 AND B.INDEX_NAME = 'IND_TEST_NAME'; 已创建549行。 SQL> SELECT * FROM YANGTK.TEST WHERE ID = 1; ID NAME ---------- ------------------------------ 1 ACCESS$ SQL> SELECT * FROM YANGTK.TEST WHERE ID = 123; ID NAME ---------- ------------------------------ 123 ALL_REPCONFLICT SQL> COMMIT;

  至此,已经成功的恢复了数据。

0
相关文章