エラーと、それが本当に伝えていること
データベースを開こうとすると、クライアントがぴたりと止まります。
Error: database disk image is malformed
同じ状態は、C の API では SQLITE_CORRUPT として、 ラッパーでは SqliteException: SQLite Error 11 として、sqlite3 シェルからはそっけない「malformed」として 表面化します。言い回しがどうであれ、エンジンはファイル内のポインタを たどり、正しいページではありえない何かに行き着いたので、責任を持てない バイトを渡すのではなく処理を止めたのです。
その厳格さこそが、ファイルが実際より悪く見えることの多い理由です。 1 つの損傷したページがテーブル全体を読めなくする一方で、他のすべての ページは無傷でそこにあります。復旧の仕事は、ページを直接読み取って 生き残ったものを救い出すことであり、それはまず SQLite ファイルとは 何なのかを知ることから始まります。
B ツリーのデータベースにとって「malformed」とは何か
SQLite のデータベースは、固定サイズのページ(多くは 各 4096 バイト)に分割された 1 つのファイルです。最初のページは ヘッダーとスキーマを保持します。各テーブルとインデックスはB ツリーです。別のページを指すページが枝分かれして いく構造で、末端は実際の行を収めるリーフページになります。行は ページの中にセルとして存在し、自分自身の長さ、 列の型、そして値を持ち歩きます。
B ツリー内の 1 つの malformed なページだけで、クエリからはテーブル全体が読めなくなります。周囲のページは完全にデコード可能なままなのにです。
「Malformed」は、ポインタがデータと辻褄が合わなくなったときに SQLite が 口にする言葉です。枝ページが正しい B ツリーページでない子を指している、 セルがページに収まりきらないほど長いペイロードを主張している、 フリーリストがループしている、あるいはページの型バイトが何の意味も 持たない、といった状態です。SQLite はこれらのどれ 1 つでも、ファイル 全体を破損と宣言する根拠として扱います。実際には損傷はたいてい局所的であるにもかかわらずです。不良ページと、その 1 つの B ツリーの中でそれより下流にあるものは疑わしいものの、ファイルの残りは ありふれた、デコード可能なデータなのです。
SQLite ファイルは実際にどう壊れるのか
SQLite は堅牢で、現実世界の破損のほとんどは、不安定なエンジンではなく ひと握りのメカニズムにさかのぼります。どれに当たったかが、何を取り戻せると 期待してよいかを左右します。
書き込み中の電源喪失やクラッシュ
マシンの電源が落ちた、プロセスが強制終了された、コンテナが破棄された、 といった瞬間に進行中だった書き込みは、ページを更新途中のまま残すことが あります。ロールバックジャーナルと WAL のモードはこれを乗り切るよう 設計されていますが、それはジャーナルやログ、そしてその fsyncの保証もまた生き残った場合に限られます。フラッシュについて嘘をつく ストレージ上では、その安全網は失われ、引き裂かれたページが残ります。
-wal ファイルの欠落や不一致
WAL モードでは、最新のコミット済みデータは、まだメインのデータベースに 畳み込まれていない -wal という補助ファイルに存在する ことがあります。.sqlite だけを単独でコピーすれば、それらの コミットを取り残してしまいます。別の時点の -wal と組み合わせ れば、事態はさらに悪くなります。どちらにしても破損として読み取られます。 2 つの半分がもはや同じデータベースを記述していないからです。
WAL を伴わずに稼働中のデータベースをコピーする
別のプロセスがまさに書き込んでいるデータベースを素朴にコピーすると、 一貫性のないスナップショットを捉えてしまいます。コピーはあるページを トランザクションの前に、別のページをその後に取り込みます。コピーした 時点では問題なさそうに見え、別の場所で開いた瞬間に失敗します。稼働中の データベースには、生のコピーではなく SQLite のバックアップ API やVACUUM INTO を使ってください。
本来いてはならない 2 人の書き手
SQLite はファイルロックを通じてアクセスを調整します。ロックが信頼できない ネットワーク共有にファイルを置いたり、別々のロック前提を持つ 2 つの プロセスに同時に書き込ませたりすると、更新が入り混じり、単一の書き手なら 決して生み出さない状態になります。ネットワークファイルシステムは常習犯で、 だからこそ SQLite のドキュメントはそれらに対して警告しているのです。
.recover 方式の救出は実際にどう動くのか
反射的に手が伸びるのは PRAGMA integrity_check です。実行する 価値はありますが、それが何をするのかははっきりさせておきましょう。それは 損傷を報告するだけで、何かを直すわけでも、行を取り出すわけでも ありません。そのためには、sqlite3 シェルの.recover コマンドが取る方法が必要で、それは通常のクエリとは まったく違う動き方をします。
救出は B ツリーのポインタを信頼する代わりに、ファイルをページごとに走査します。テーブルのリーフに見える ページごとに、各セルを直接読み取って行をデコードします。rowid、列の シリアル型、そして値です。デコードできた行はすべて、再構築できたスキーマ から作られた、まっさらで空のデータベースへ再挿入されます。 損傷したページは飛ばされ、読めるページは、それらを索引していたツリーが 生き残ったかどうかに関わらず行を差し出します。
元のテーブルを特定できない行も捨てられはしません。慣例的にlost_and_found と名付けられたテーブルに、由来したページと セルをキーとして着地するので、手作業で調べて並べ直せます。出力はきれいに 開ける新しいファイルで、元のファイルでデコード可能だった行をすべて 保持しています。
それが正直な上限です。救出はディスク上で生き残ったものを復旧します。 不良ページ上の行や、一度もパースされなかった下流のページ上の行は 再構築されません。SQLite ファイルにはそれらを再生成するための冗長性が ないので、どこでもデコードできない行は単に失われています。復旧は生き 残ったデータの周りにクリーンなデータベースを組み直すのであって、存在 しなかったデータをでっち上げることは決してありません。
私たちが知る限り、これまでクライアント側のブラウザツールでこれを提供した ものはありません。この種の救出には、コマンドラインとローカルの SQLite ビルドが必要で、ファイルをまずサーバーに置くことができない、あるいは 置くべきでない人には選択肢になりませんでした。ページ走査をブラウザ内で 走らせることが、その隔たりを埋めます。同じ手法で、アップロードなしです。
復旧した出力は常に疑ってかかる
救出したデータベースはきれいに開けてクエリも通るので、それを決定版として 扱いたくなります。抗ってください。ページ走査は、健全なデータベースが課す 制約なしに生のバイトをデコードするので、出力には確認しておくべき癖が 伴います。
- よみがえる削除。 削除した行は、その領域が再利用される まではページ上に残り続けます。救出は生きている行と、削除されたが 上書きされていない行とを区別できないので、削除した記録が再び現れる ことがあります。
- 型のずれ。 各値はシリアル型とともに格納されています。 想定外の型でデコードされると、整数が blob として戻ってきたり、数値の 精度がずれたりします。型が重要な列は抜き取り確認しましょう。
- lost_and_found の孤児。
lost_and_foundの 行は、元のテーブルも信頼できる順序も持たずにやってきます。それらを 本来の場所へ振り分けるには、スキーマとデータについてのあなた自身の 知識が必要になります。 - 破られた不変条件。 外部キー、一意制約、トリガーは 再挿入の間バイパスされるので、復旧したデータベースには、元のスキーマ なら拒否したであろう組み合わせが入り込むことがあります。
復旧したデータベースは、判決ではなく質の高い手がかりとして扱ってください。 下流の何かがそれに依存する前に、正しいとわかっている記録と照らし合わせて 検証しましょう。
兄弟分の -wal ファイルの扱い方
データベース本体が損傷していると結論づける前に、その隣に-wal ファイル(そしてその連れの -shm)がないか 探してください。WAL モードでは、最新のコミット済みトランザクションが、 まだメインファイルへチェックポイントされずにそのログの中に居座っている ことがあります。それらなしで復旧すると、最も気にかけている、まさに 最新のデータを取りこぼします。
三点セットを一緒に保ってください。同じ瞬間にコピーした.sqlite、-wal、そして -shm です。 健全な SQLite がそれらを一緒に開くと、ログが適用され、結局のところ データベースが一貫していると判明することもあり、その場合は救出は 不要です。本当に破損しているときは、WAL も読む救出が、そこに保持された コミット済みページをデコードできます。逆のリスクもあります。別の データベースの -wal や、古いセッションの残り物である-wal は、間違ったファイルと組み合わさると破損をでっち 上げます。補助ファイルの出所が不確かなら、データベースを単独で救出して 比較してください。
よくある質問
「database disk image is malformed」とはどういう意味ですか?
SQLite がファイル内のポインタをたどった先に、正しいデータベースページではありえないものを見つけた、という意味です。ページの末尾を越えて指す B ツリーのセル、意味をなさない型バイトを持つページ、ループするフリーリスト、あるいはヘッダーと食い違うペイロード長を持つ行などです。SQLite は構造を信頼できなくなった瞬間に SQLITE_CORRUPT を発します。これは症状の名前であって原因ではなく、何行が影響を受けているかは教えてくれません。
壊れた SQLite データベースは復旧できますか?
たいていはかなりの割合が、はい、復旧できます。各テーブルとインデックスは固定サイズのページに分散した B ツリーであり、1 つの不良ページが他のページまで壊すことはめったにありません。.recover 方式の救出は生のページを走査し、解釈できるセルをすべてデコードして、それらの行を新しいデータベースへ再挿入します。加えて、元のテーブルを特定できなかった行は lost_and_found テーブルに集めます。
復旧は元のファイルに対して実行すべきですか?
唯一のコピーに対しては決してやめてください。まずバイト単位でそのままのコピーを取り、そこから救出します。そうすれば試みが失敗しても何も失いません。データベースの隣に -wal や -shm ファイルがあれば、それらもコピーしてください。最新のコミット済み変更を保持している可能性があるからです。救出はまったく新しい出力ファイルを書き出し、入力には手を触れませんが、それでも予備のコピーは安上がりな保険です。
復旧したデータが正しくないように見えることがあるのはなぜですか?
救出は、健全なデータベースが課す検査を一切行わず、各セルに物理的に存在するバイトをそのままデコードするからです。削除した行が再び現れたり、値が誤った型で戻ってきたり、孤立した行が信頼できる順序を持たないまま lost_and_found に着地したりします。復旧した出力は常に疑ってかかるべきです。頭から信じ込むのではなく、正しいとわかっている記録と照らし合わせて検証してください。