ORA-01578 データブロック障害からの復旧方法

ハードウェアRAID5でバッチリ、と思っていたらコントローラがヘンになって、Oracleさんから見たときに論理的な整合性が失われた場合など、

ORA-01578 Oracleデータ・ブロックに障害が発生しました(ファイル番号6、ブロック番号9178)

とかなんとかが出てあるテーブルにアクセス出来なくなることがある。これは名前の通り あるデータファイル(表領域)に含まれるデータブロックのうち、部分的に障害が発生していることを表している。こうなるとその表の一部分にアクセスできないということが起こるのだが、「一部分にのみ」というところがポイントになり、案外気づきにくいこともある。障害範囲のブロックにアクセスしない限りはこのエラーメッセージは出力されない(もちろんそのほうが好都合だが*1 )。

また、破損したブロックが「表(テーブル)」なのか「索引(インデックス)」なのかによっても挙動が異なるようだ

  • 表のブロックが破損している場合、そのブロックに含まれるレコードにアクセスしようとするとエラーでストップする
    • WHERE句などによってなんらかの形でそのブロックにアクセスしなければ発生しない
    • TABLE ACCESS FULL などの実行計画の場合に COUNT(*) などすると全ブロックにアクセスする(かな?)のでどうしても発生する
  • 索引のブロックが破損している場合、そのリーフブロックにアクセスしようとするとストップする
    • つまり、実際に存在しないレコードを検索する問い合わせがあるとして、インデックスが使用される場合、大小比較の際に特定のリーフに触れるとエラーが出る
    • WHERE A>40 などという簡単な問い合わせの場合でも、 A列に対する索引の一部が破損しているとき、エラーになることもある、という意味

索引のほうはあくまで B*Tree索引での話になるが、この挙動はツリーそのものに対する最低限の理解がないと謎が深まると思う。存在しないレコードへの問い合わせでエラーが出たり出なかったりする、というのは「データブロックが破損」という響きが直接想像するものとはやや矛盾があると感じる人もいるだろうから。

さて、このORA-01578が出たらどうするのか、という話。いろいろ方法があるのだけれども、ケーススタディ的に、あくまで私の場合の実施例を以下に示しておこうと思う。すべてのケースに適用できるわけではないと思うが、何かの足しになれば幸いだ。

まず、DBMS_REPAIRパッケージを使えるようにした

DBMS_REPAIRという標準のPL/SQLパッケージがあり、データブロックの破損の回復に使えるものが詰めこまれている。これを使うのだが、諸々の回復に使うには準備がいる。以下のようなSQLSQL Worksheet(SQLDeveloper)とかSQL*Plusで実行すると良い。ちなみに SYSTEMでログインしていてもエラーになったので SYSDBA権限で SYSユーザーでログインする必要があった。これはハマるかもしれないポイントなので明記しておく。

BEGIN
  DBMS_REPAIR.ADMIN_TABLES (
    TABLE_NAME => 'REPAIR_TABLE',
    TABLE_TYPE => dbms_repair.repair_table,
    ACTION => dbms_repair.create_action,
    TABLESPACE => ' <<このREPAIR_TABLEという表を格納したい表領域名>> ');
END;

ついでにこっちも準備。しかし使わなかったんだけど。

BEGIN
  DBMS_REPAIR.ADMIN_TABLES (
    TABLE_NAME => 'ORPHAN_KEY_TABLE',
    TABLE_TYPE => dbms_repair.orphan_table,
    ACTION => dbms_repair.create_action,
    TABLESPACE => '<<このORPHAN_KEY_TABLEという表を格納したい表領域名>> ');
END;

障害範囲を特定する

次に、障害範囲をDBMS_REPAIRに発見させる。ここでもハマったのだが、表に対する検出と、索引に対する検出は別々に行う必要があった。ひょっとすると何かいい方法があるのかもしれないが。

まずは、表に対しての検出。ORA-01578のエラーメッセージからは「どの表がおかしい」とは出ないので、結局全てのテーブルについて実施することになるだろう。USER_TABLESなどのディクショナリから表名の一覧を引っ張ってきてスクリプトかなにかでSQLを吐き出させると簡単だ。

SET SERVEROUTPUT ON
DECLARE num_corrupt INT;
BEGIN
  num_corrupt := 0;
  DBMS_REPAIR.CHECK_OBJECT(
    SCHEMA_NAME => '対象表のスキーマ名',
    OBJECT_NAME => '対象表名',
    REPAIR_TABLE_NAME => 'REPAIR_TABLE',  
    CORRUPT_COUNT => num_corrupt);
  DBMS_OUTPUT.PUT_LINE('number corrupt: ' || TO_CHAR(num_corrupt) );
END;
/

これを実行すると、「ORA-01578を発生させうる表」については ゼロ以外の数値が出力される。だが、別にDBMSの出力を食い入るように見つめる必要はなくて、REPAIR_TABLE表の中にどのテーブルのどのブロックが破損しているかがINSERTされていくので、その表をあとで見るとよい。ちなみに、INSERTされていくので、何度も実施するとREPAIR_TABLEにどんどん行がたまる。作業前にクリアしておくと良いかもしれない。

さて、ここでゼロが出てもその表が安全であるとはいえない。その表が索引を持っている場合はその索引もチェックする必要がある

SET SERVEROUTPUT ON
DECLARE num_corrupt INT;
BEGIN
  num_corrupt := 0;
  DBMS_REPAIR.CHECK_OBJECT(
    SCHEMA_NAME => '対象表/索引のスキーマ名',
    OBJECT_NAME => '対象索引名',
    OBJECT_TYPE => dbms_repair.index_object,
    REPAIR_TABLE_NAME => 'REPAIR_TABLE',  
    CORRUPT_COUNT => num_corrupt);
  DBMS_OUTPUT.PUT_LINE('number corrupt: ' || TO_CHAR(num_corrupt) );
END;
/

ここでゼロ以外が出力された索引を使う問い合わせは失敗するし、おそらくINSERT/UPDATE/DELETEなどでも索引の当該ブロックへのアクセスがあるようなケースでは失敗するだろう。なので索引が破損しているときは修復の対象となる。

対象となる表、索引をリストアップしたら次へ進む。

復旧可能なデータは自力で復旧する

更新されない、所謂マスタ表の類は、元データがあるならそっちから復旧するほうが安全確実だ。このへんの説明は割愛する。なんというか、障害でなくても普通のことなので。

表の破損に対してFIX_CORRUPT_BLOCKS を使う

表のデータブロックが破損している場合に、かつその表に対するチェックが 先述のCHECK_OBJECTによって実施され、REPAIR_TABLEに格納されている場合に、FIX_CORRUPT_BLOCKSプロシージャを使って復旧できる。かもしれないらしい。ちなみに私の場合はこれだけでは不完全で復旧できなかった。この話は後述する。

DECLARE num_fix INT;
BEGIN
  num_fix := 0;
  DBMS_REPAIR.FIX_CORRUPT_BLOCKS (
    SCHEMA_NAME => 'スキーマ名',
    OBJECT_NAME => '表名',
    OBJECT_TYPE => dbms_repair.table_object,
    REPAIR_TABLE_NAME => 'REPAIR_TABLE',
    FIX_COUNT => num_fix);
  DBMS_OUTPUT.PUT_LINE('num fix: ' || TO_CHAR(num_fix));
END;

num_fix: にゼロ以外の値が出力されると修復されている、かもしれない。ためしに全件アクセスしてみるといい。
ゼロの値が出たり、アクセスしてみるとやっぱりエラーになる場合は次に進む。

表の破損に対して SKIP_CORRUPT_BLOCKS を使う

で、その破損ブロックへのアクセスを停止させることで表自体へのコントロールを取り戻すことができる。もちろんこれはデータを失う可能性がある。
いかにも失いそうなものだが、どういうわけか私が実施したときにはデータは特に失われなかった。そういうこともあるのかもしれない(空のデータブロックへ問い合わせでアクセスすることがあるのだろうか...? よくわからないが、問い合わせでエラーになるブロックをスキップしたらデータが失われない、のはなんとなく不思議だ)。

BEGIN
  DBMS_REPAIR.SKIP_CORRUPT_BLOCKS (
    SCHEMA_NAME => 'スキーマ名',
    OBJECT_NAME => 'テーブル名',
    OBJECT_TYPE => dbms_repair.table_object,
    FLAGS => dbms_repair.skip_flag);
END;

私の場合は、ここまででテーブルへは全件アクセスできたし、データも失われなかった。バンザイだ。しかし管理者マニュアルによれば、この方法ではデータが失われる可能性があるはず(仕組みからしても)なので、注意。

さて、索引が破損している場合は、私はこんな方法を取った。破損ブロックだけを隠すよりもおそらく楽。

索引の破損に対して 索引の再生成を実施する

表自体のデータが失われていないのであれば、索引は再生成してしまえばいい。データの件数にもよるが私の場合は2000万件程度のテーブル相手だったが、再作成することにした。結果論になるが数分で終わったのでリスク回避としては妥当な選択だったかなと思っている。まあこのへんは状況によるのでなんとも。

まず普通に試す

ALTER INDEX hogehoge REBUILD;

いけるかと思うと

SQL Error: ORA-01578: Oracleデータ・ブロックに障害が発生しました(ファイル番号4、ブロック番号6954)
ORA-01110: データ・ファイル4: 'なんとかかんとか'
01578. 00000 -  "ORACLE data block corrupted (file # %s, block # %s)"
*Cause:    The data block indicated was corrupted, mostly due to software
           errors.
*Action:   Try to restore the segment containing the block indicated. This
           may involve dropping the segment and recreating it. If there
           is a trace file, report the errors in it to your ORACLE
           representative.

とか言われた。REBUILDは結局当該領域へのアクセスを伴うようだ。

じゃあ作り直せばいい。

DROP INDEX hogehoge;

しかし、この場合もいろいろあり得る。

ORA-02429: 一意キーまたは主キーの保持に使用される索引は削除できません。
ORA-02273: この一意/主キーは外部キーによって参照されています。

まあとりあえずこのへんはよくあると思う。これらへの対処方法は説明を割愛する。言われたとおりの対処をすればいいので、例えばALTER TABLE DISABLE CONSTRAINTのお世話になってみるなどすればよいと思う。

とにかく、問題の索引を DROP してしまう。そして

CREATE INDEX hogehoge ON hoge(fuga) TABLESPACE...

などと、その索引を作成したときと同じSQLを実行すれば修復完了だ。作り直しただけなので当たり前だ。勢いでやってしまうと TABLESPACE句などはコロッと忘れてしまうので注意したい。

修復後のチェック

修復後のチェックとして全件問い合わせなどしてみると良い。ちなみに、EXPコマンドでスキーマをフルダンプする場合、索引ブロックに障害があってもエラーが発生しなかったので、EXPをチェック代わりにするのはやめたほうがいい。
なにかエラーが再現できる問い合わせを準備しておいて、修復後に発生しなくなった、などとするほうがベター。

以上がORA-01578への対処の事例。検索してみたけどあまり出てこなかったので、無知と恥を晒すようだが書いておいた。誰かの役に立てば幸い。

*1:ブロック別にアクセスできなくなるだけで表単位でのアクセスは確保してくれているOracle DBは比較的優秀だと思う。