SQLiteはなぜ「壊れない」のか——WALモードとアトミックコミットの仕組みを徹底解説
はじめに——なぜSQLiteは電源が落ちても壊れないのか?
「Raspberry Piで動いていたロガーから、うっかり電源ケーブルを抜いてしまった」。home labを運用している方なら、そんな瞬間が一度はあるでしょう。心臓に悪い数秒の後、SQLiteのデータベースファイルを開いてみると——なぜか壊れていない。前回コミット済みのデータはそのまま残り、途中まで書き込みだったトランザクションは跡形もなく消えています。これは幸運ではなく、設計された動作です。
SQLiteは公式サイトで「高信頼性(high-reliability)」を掲げる組込みデータベースです。スマホの連絡先やブラウザの履歴をはじめ、多くの組込み機器で採用されていますが、その普及の大きな理由の一つが、このクラッシュ安全性——電源断やOSクラッシュ、プロセス強制終了が起きてもデータベースファイルが論理的に破損しない性質です(sqlite.org)。
その正体は、大きく2つの仕組みです。1つ目はアトミックコミット。トランザクション内のすべての変更が「すべて適用される」か「1つも適用されない」かのどちらかだけに限定する技術です。ディスクはセクタ単位で、遅延され、並べ替えられる形で書き込まれるため、真の「同時書き込み」は物理的に不可能。SQLiteは書き込み順序を注意深く制御し、中断されても中途半端な状態が残らないようにしています(Atomic Commit In SQLite)。2つ目がその実装であるロールバックジャーナル、そして2010年のバージョン3.7.0で追加されたWAL(Write-Ahead Logging)モードです(Write-Ahead Logging)。
本記事では、古典的なロールバックジャーナルの仕組み → WALモードの考え方と制約 → モード比較と選択基準 → 実際の切り替え手順、の順に、公式ドキュメントに基づいて図解で追っていきます。
一点だけ、最初に断っておきます。「壊れない」はトランザックションの原子性の話であり、バックアップが不要とか、ビットエラーやストレージ故障に無敵という意味ではありません。メディア故障への対策はOSとハードウェア、そしてバックアップの責任です(sqlite.org)。この「何を保証し、何を保証しないか」を正確に理解することが、信頼性を「魔法」ではなく「設計」として使いこなす第一歩になります。
アトミックコミットとは?——「全か無か」を実現する錯覚の技術
「アトミックコミット」とは、1つのトランザクション内のすべての変更が適用されるか、まったく適用されないかのどちらかだけを保証する仕組みです。途中で「半分だけ書き込まれた」という中間状態が存在しないように見せることが、SQLiteのクラッシュ安全性の核心です(Atomic Commit In SQLite §1)。
しかし、これは錯覚です。現実のハードウェアには「複数の場所への書き込みを同時・瞬時に行う」能力はありません。ディスクはセクタ単位(かつては512バイト、最近は4096バイトが一般的)で書き込まれ、OSは書き込みを遅延させたり並べ替えたりします。SQLiteがしているのは、この「同時書き込みできない」ハードウェアの上で、あたかも変更が一瞬で同時に適用されたかのように振る舞わせる工夫なのです。
SQLiteが置くハードウェア仮定
SQLiteは公式ドキュメントで、次のような前提を置いて設計されていると明かしています(同 §2)。
- セクタ書き込みは原子的でないが線形である:書き込みはセクタの端から端へ進む
- ファイル削除は原子的である:電源喪失後、ファイルは完全に存在するか完全に存在しない
- fsync()は正しく動く:要所でOSキャッシュをディスクへ書き出す指示が守られる
- powersafe overwrite:セクタ内の書き込みはそのセクタ外に影響しない(バージョン3.7.9以降デフォルトで仮定)
つまり「壊れない」という信頼性は、SQLite単体の力ではなく、OSとハードウェアがこの仮定を満たしていることが前提です。公式ドキュメントには「一部のWindows/Linuxではfsyncが壊れている可能性があり、その場合コミット途中の電源断で破損しうる。SQLite側では対策不能」とも正直に記されています。信頼性の土台の最下層はOSにある、という点は押さえておきましょう。
「全か無か」の動きを図で理解する
コミットの瞬間に何が起こるか、概念図で示します。
flowchart TD
A["BEGIN トランザクション"] --> B{"COMMIT / クラッシュ?"}
B -->|コミット完了| C["すべての変更が適用される"]
B -->|途中でクラッシュ| D["何も起きていない状態に戻る"]
C --> E["中間状態は存在しない"]
D --> E
コミットが完了すればすべての変更が反映され、途中で電源が落ちれば何も起きていなかった状態に戻る。「一部だけ適用された」という状態は利用者からは観測されません。この「全か無か(all-or-nothing)」こそがアトミックコミットの約束です。
なお、守られるのはあくまでトランザクションの原子性です。ストレージのビットエラーの検出・訂正はハードウェアとOSの責任であり、SQLite自体は冗長性を持ちません(同 §2)。「壊れない=バックアップが不要」という意味ではない点は、後のセクションでも繰り返し確認します。
この原子性を、具体的にどういう手順で実現するのか。次のセクションでは、デフォルトのロールバックジャーナル(DELETEモード)の動きをステップごとに見ていきます。
ロールバックジャーナルの仕組み——デフォルトのDELETEモードはどう動く?
前節で見たとおり、SQLiteは書き込みを「一瞬で同時に」終わらせることはできません。そこで古典的なDELETEモード(デフォルトのジャーナルモード)では、変更前の内容を先に別ファイルに退避してから、DB本体を書き換えるという順序で「全か無か」を実現します。
具体的なコミット手順を追ってみましょう(sqlite.org/atomiccommit.html §3)。
- 共有ロックを取得してデータベースを読み込む
- 予約ロックを取得(書き込みの宣言。他プロセスの読み取りは継続可能)
- 変更対象ページの変更前の内容をロールバックジャーナルに退避
- ユーザー空間でページを変更
- ジャーナルをfsyncしてディスクへ完全に書き出す(多くのプラットフォームで2回)——ここがクラッシュ安全性の要
- pendingロック→排他ロックに昇格し、変更済みページをDB本体へ書き換えてfsync
- ジャーナルを削除——この削除が完了した瞬間がコミット成立の合図
つまり「ジャーナルが存在する間はまだコミットされていない、削除されたらコミット済み」というただ一つの判定基準が、原子性を生み出しています。
クラッシュ時の復旧——「ホットジャーナル」
もし手順6の途中で電源が落ちたらどうなるでしょうか。DBファイルは中途半端に書き換わっていますが、ジャーナルには変更前の内容が完全に残っています。次回オープン時、SQLiteはこの「ホットジャーナル」を検出して再生(プレイバック)し、データベースを元の状態へ復元します。これが「電源が落ちても壊れない」仕組みの古典的実装です。
sequenceDiagram
participant App as アプリ
participant DB as DBファイル
participant J as ロールバックジャーナル
App->>DB: 共有ロック+読み取り
App->>DB: 予約ロック
App->>J: 変更前ページを退避
App->>J: fsync(耐久化)
App->>DB: 排他ロック→変更を書き込み→fsync
App->>J: ジャーナル削除=COMMIT
Note over J,DB: クラッシュ時: ジャーナルが残っていれば再生して復元
弱点——書き込み中に読み取りがブロックされる
この方式の弱点は2つあります。第一に、排他ロック中は他プロセスの読み取りすらブロックされること(sqlite.org/lockingv3.html)。第二に、コミットごとにジャーナルとDB本体の両方をfsyncするため書き込みが2回発生し、fsyncの回数も多いことです。この制約を解決するために生まれたのが、次節で解説するWALモードです。
(本節の内容はSQLite公式ドキュメント Atomic Commit In SQLite に基づいています。)
WALモードの基本——どうやって読み書きを同時にできるのか?
ロールバックジャーナルには「書き込み中は読み取りがブロックされる」という弱点がありました。SQLite 3.7.0(2010年リリース)で追加されたWAL(Write-Ahead Logging)モードは、ここを発想の転換で解決します(sqlite.org/wal.html)。
発想の反転:変更はDB本体に書かず、WALに追記する
DELETEモードは「変更前の内容をジャーナルに退避してからDB本体を書き換える」方式でした。WALモードはその逆です。元の内容はデータベース本体に残したまま、変更内容を専用の -wal ファイルの末尾に追記します。コミットとは、このWALファイルへのコミットレコード追記だけで完了します。DB本体には一切触れないため、書き込みが1回で済み(ロールバック方式は2回)、しかもシーケンシャルな追記書き込みになる——これが「ほとんどのシナリオで大幅に高速」という公式の謳い文句の正体です。fsyncの回数も減り、fsync実装が怪しいシステムでも問題に強いとされています。
読み取りの仕組み:end markによる一貫スナップショット
リーダーはトランザクション開始時に、WAL内の「最後の有効コミットレコードの位置(end mark)」を記録します。ページを読むときは、まずWAL内のend mark以前にある最新コピーを探し、なければDB本体から読み取ります。つまりリーダーは「自分が開始した時点」の一貫したスナップショットを見続けられるのです。ライターはWALのさらに後ろに追記していくだけなので、リーダーとライターが互いにブロックし合いません。ただしWALファイルは1つなので、同時に書けるライターは1プロセスだけです。
wal-index(-shm):高速検索と「同一ホスト限定」の理由
WALが大きくなると「どのページがどこにあるか」の検索がボトルネックになります。そこで -shm ファイル内の共有メモリ上にwal-indexという索引を持ち、WAL内ページを高速に特定します。共有メモリを使う以上、すべてのプロセスが同一ホスト上になければなりません。これが「NFS/SMBなどのネットワークファイルシステムではWALが動作しない」という公式制約の理由です(FAQでも後述します)。
flowchart LR
W["ライター(1つ)"] -->|"変更を追記"| WAL["-wal ファイル"]
R1["リーダーA"] -->|"end mark以前を参照"| WAL
R2["リーダーB"] -->|"end mark以前を参照"| WAL
R1 --> DB["DBファイル(元の内容)"]
R2 --> DB
WAL -->|"チェックポイントで書き戻し"| DB
WAL -.->|"-shm: wal-index(共有メモリ)"| R1
チェックポイント:WALの内容をDB本体へ書き戻す
追記し続けるだけではWALが無限に成長してしまいます。そこでチェックポイントという操作がWALの内容をDB本体へ書き戻します。リーダーと並行して実行できますが、いずれかのリーダーのend markを超えるページで停止するため、長時間の読み取りトランザクションはチェックポイントの進行を妨げ得ます。デフォルトではWALが1000ページに達した時点のCOMMIT時、および最後の接続が閉じるときに自動実行(PASSIVE)されます。手動実行や閾値調整も可能で、詳細は実践ステップのセクションで扱います。WALが大きくなると読み取りコストがサイズに比例して増えるため、チェックポイントでWALを小さく保つことが運用のポイントです。
利点と制約を公式ドキュメントベースで整理
利点は、(1)ほとんどのシナリオで高速、(2)読み書きの同時実行、(3)シーケンシャルなディスクI/O、(4)fsync回数の削減、の4点。制約は、(1)同一ホスト限定・NFS不可、(2)WALモード中はpage_sizeを変更できない、(3)読み取り専用オープンに条件がある(-shmへの書き込み権限等)、(4)-wal/-shmファイルが準恒久的に存在する、(5)巨大トランザクションが不得手——約100MB超ではロールバックジャーナルの方が速く、1GB超ではディスクフルエラーの恐れがあります(いずれもsqlite.org/wal.htmlによる)。数十MB以上のバッチ投入ではロールバックモードの検討が無難です。
なおWALモードは他のジャーナルモードと異なり設定が永続的で、DBファイルに記録されます。一度 PRAGMA journal_mode=WAL; を実行すれば、アプリを変更せずに再起動するだけで移行完了です。
出典: Write-Ahead Logging — SQLite公式ドキュメント (https://www.sqlite.org/wal.html)、Atomic Commit In SQLite (https://www.sqlite.org/atomiccommit.html)
WALとジャーナルモードの比較表——どのモードを選べばいい?
ここまでロールバックジャーナルとWALモードの仕組みを見てきました。実際の運用では、PRAGMA journal_mode で6種類のモードから選択することになります。まず全体を俯瞰する比較表で整理しましょう(出典:SQLite公式 PRAGMA journal_mode)。
journal_mode 6種の比較表
| 項目 | DELETE(デフォルト) | TRUNCATE | PERSIST | MEMORY | OFF | WAL |
|---|---|---|---|---|---|---|
| クラッシュ安全性 | ○ | ○ | ○ | × | × | ○ |
| 読み書きの同時実行 | × | × | × | × | × | ○ |
| 書き込み速度 | 標準 | やや速い | やや速い | 速い | 速い | 高速 |
| ネットワークFS | ○ | ○ | ○ | ○ | ○ | × |
| 設定の永続性 | 都度 | 都度 | 都度 | 都度 | 都度 | 永続 |
| 主な用途 | 汎用・互換重視 | 組込み | フラッシュ | 一時データ | 破棄可データ | 同時アクセス・高速化 |
各モードの違いを簡単に補足します。
- DELETE: コミット後にジャーナルファイルを削除する標準動作。クラッシュ時はホットジャーナルを再生して復元します。
- TRUNCATE: 削除ではなく0バイトへの切り詰め。ファイルシステムによっては削除より高速です。
- PERSIST: ジャーナルヘッダを無効化してファイルを残す。削除コストの回避とフラッシュメディアへの書き込み削減に有効です。
- MEMORY: ジャーナルをメモリに置くため高速ですが、クラッシュ時に破損の恐れがあります。
- OFF: ジャーナル自体を作らず、ロールバック不可能。クラッシュするとデータベースが壊れる可能性があります。
- WAL: 変更をWALファイルに追記する先行書き込みログ。読み書きの同時実行と高速な書き込みを両立します。
WAL vs ロールバック(DELETE)の詳細比較
多くの場面で選択肢は「デフォルトのDELETEのままか、WALへ移行するか」に絞られます。公式ドキュメントに基づいて両者を詳しく比べましょう。
| 観点 | ロールバック(DELETE) | WAL |
|---|---|---|
| 書き込み回数 | 2回(ジャーナル+DB本体) | 1回(WALへの追記のみ) |
| fsync回数 | 多い(ジャーナルと本体の両方) | 少ない |
| 同時性 | 書き込み中は読み取りブロック | リーダーとライターが相互に非ブロック |
| I/Oパターン | ランダムアクセス中心 | シーケンシャル |
| 巨大トランザクション | 数十MB以上でも安定 | 約100MB超ではDELETEが速く、1GB超ではディスクフルの恐れ |
| 追加ファイル | ジャーナル(コミット後に削除) | -wal / -shm が準恒久的に存在 |
| ネットワークFS | 動作可能 | 動作不可(共有メモリのため同一ホスト限定) |
選択基準のまとめ
- ローカル配置+複数プロセスから同時アクセス → WALが第一候補。リーダーがライターをブロックしない恩恵が大きい
- NFS/SMB上にDBを置きたい → WALは使えないためDELETE系で運用するか、ローカル配置+バックアップ方式に切り替える
- 巨大なバッチトランザクション(数十MB超) → ロールバックモードが有利
- フラッシュメディア(SDカード等)での書き込み負荷軽減 → WALのシーケンシャルI/Oと少ないfsyncが有利。PERSISTも選択肢
なお、WALモードだけは設定が永続的で、DBファイル自体に記録されます。一度 PRAGMA journal_mode=WAL; を実行すれば、以降のオープンでもWALモードが維持される点も実運用では重要な違いです(TRUNCATEなどは再オープンでDELETEに戻ります)。切り替えの具体的な手順は次のセクションで解説します。
実践ステップ——WALモードへの切り替えとチューニング方法
ここまで仕組みを理解したら、次は実際に自分のデータベースをWALモードへ移行してみましょう。手順は5つ。いずれもSQLite公式ドキュメント(sqlite.org/wal.html、pragma.html)に基づいています。
ステップ1: WALモードへの切り替え
CLIから一度実行するだけです。
$ sqlite3 mydb.db
sqlite> PRAGMA journal_mode=WAL;
wal
戻り値が wal と表示されれば成功です。WALモードは他のジャーナルモードと異なり永続的で、設定がDBファイル自体に記録されます。つまり、アプリケーションのコードを一切変更せず、再起動するだけで移行が完了します。VFSが共有メモリ機構をサポートしていない場合は元のモードのまま返るので、必ず戻り値を確認しましょう。
ステップ2: synchronous設定の判断
PRAGMA synchronous=NORMAL;
デフォルトの FULL は毎コミットでWALをsyncして最高の耐久性を得ますが、NORMAL はこのsyncを省略して高速化します。重要なのは、公式ドキュメントによれば、NORMALでもチェックポイント時にはsyncされるため、システム前提(fsyncなど)が満たされていれば電源断でデータベースが破損することはないという点です。失われるのは「直近のコミットがロールバックされ得る」という耐久性だけ。Raspberry Piなどのエッジデバイスでは、この組み合わせがしばしば採用されます。
ステップ3: チェックポイント管理
WALがデフォルトで1000ページに達すると、COMMIT時に自動チェックポイント(PASSIVE)が走ります。書き込み量が多い場合は閾値を調整できます。
PRAGMA wal_autocheckpoint=2000; -- 閾値を変更
PRAGMA wal_checkpoint(TRUNCATE); -- 手動実行+WALを0バイトに
読み取り性能重視ならチェックポイントを頻繁に、書き込み性能重視なら頻度を下げる、というトレードオフです。
ステップ4: WALファイルの監視
-wal と -shm ファイルがなかなか消えない場合、長時間続いている読み取りトランザクションがチェックポイントの進行を妨げている疑いがあります。リーダーのend markより先へは書き戻せないためです。全接続が閉じれば自動チェックポイント+削除されるので、まずは接続の滞留を確認しましょう。
ステップ5: NFS/SMBで使いたい場合の回避策
WALは共有メモリを使うため、ネットワークファイルシステム(NFS/SMB)上では動作しません。この場合は、DBはローカルに置き、必要に応じてバックアップやレプリケーションでNASへ転送する設計に切り替えるのが正解です。
まずは PRAGMA journal_mode=WAL; と PRAGMA synchronous=NORMAL; の2行から試してみてください。
ケーススタディ——home labとエッジデバイスでの活用
ここまで解説してきた仕組みを、実際のhome labやエッジデバイスの現場に当てはめてみましょう。公式ドキュメント(sqlite.org/wal.html、atomiccommit.html)の記述と照らし合わせながら、4つの典型的なケースを見ていきます。
ケース1: Raspberry Pi + SDカードでのセンサーロギング。 温度や湿度などのセンサーデータを数分おきに書き込む構成では、WALモードと PRAGMA synchronous=NORMAL; の組み合わせが定番です。WALは書き込みが1回で済み(ロールバックジャーナル方式は2回)、ディスクI/Oがシーケンシャルになり、fsyncの回数も少なくなります。ランダム書き込みと fsyncの頻発はフラッシュメモリへの負荷要因とされるため、この「シーケンシャル・低fsync」という特性はSDカードやeMMCとの相性が良いと期待できます(SQLite公式: https://www.sqlite.org/wal.html)。
ケース2: NAS上にデータベースを置きたい。 「ラズパイのストレージが心もとないから、DBファイルだけNASのNFS/SMB共有に置こう」——これは代表的なハマりポイントです。WALモードはwal-index(-shmファイル内の共有メモリ)を使うため、全プロセスが同一ホスト上になければならず、ネットワークファイルシステム上では動作しません(同)。代替設計は2つ。NFS上ではロールバックジャーナル(DELETE系モード)を使うか、あるいはDBはローカルに置いて定期バックアップだけをNASに飛ばす構成です。後者の方がトラブルが少ない構成になりやすいでしょう。
ケース3: 複数プロセスから参照するhome labダッシュボード。 家電制御の記録を書き込む常駐プロセスと、グラフ表示のために数秒ごとに読み込むダッシュボードが同じDBを触る状況を考えます。ロールバックモードでは書き込み中の排他ロックが読み取りをブロックしますが、WALなら「リーダーはライターをブロックせず、ライターもリーダーをブロックしない」ため、画面の更新が止まるようなことがありません。単一ホスト・複数プロセスという条件が満たされていれば、WALが最適解です(同)。
ケース4: 数十MB超の大量バッチ投入。 月次まとめ登録のような巨大トランザクションはWALの不得手な領域です。公式ドキュメントによれば、約100MBを超えるトランザクションではロールバックジャーナルの方が速く、1GB超ではディスクフルエラーで失敗する可能性すらあります(同)。バッチ処理時だけロールバックモードに戻す、という使い分けが現実的な判断です。
最後に電源断時の挙動を正直にまとめておきます。OSとハードウェアが前述の前提(fsyncの正常動作など)を満たしている限り、いずれのジャーナルモードでもデータベース自体が論理的に破損することはありません。ただし synchronous=NORMAL では、電源断の瞬間に直近のコミットがロールバックされる可能性があります。センサーログなら「最後の1件が失われるだけ」で許容範囲でしょう。破損しないことと、1件も失わないことは別物です——この区別を設計判断の土台にしてください。
(参考: SQLite公式ドキュメント https://www.sqlite.org/wal.html / https://www.sqlite.org/atomiccommit.html)
よくある質問(FAQ)
ここでは、WALモードへの移行や運用で特に検索の多い疑問に、SQLite公式ドキュメント(sqlite.org/wal.html、atomiccommit.html)に基づいて回答します。
WALモードにすると -wal と -shm ファイルがずっと残っている。壊れた?
壊れていません。正常な挙動です。WALモードのデータベースでは、-wal(先行書き込みログ)と -shm(wal-index用の共有メモリ)ファイルが「準恒久的に」存在する設計です(公式: WALの特性)。ただし、そのデータベースへの最後の接続が閉じるとき、自動チェックポイントが実行されてWALの内容がDB本体へ書き戻され、-wal/-shm は削除されます。残り続けている場合は、どこかのプロセスが接続を開いたままになっていないか確認しましょう。
ネットワークドライブ(NFS/SMB)でWALモードは使える?
使えません。WALモードは -shm ファイル内の共有メモリ(wal-index)を使ってWAL内のページを高速に特定する仕組みのため、データベースにアクセスするすべてのプロセスが同一ホスト上になければなりません(公式: WALの制約)。NFSやSMBなどのネットワークファイルシステム上では、プロセス間でこの共有メモリを共有できないため動作しません。NAS上にDBを置きたい場合は、ロールバックジャーナルモード(DELETE)を使うか、ローカルにDBを置いてバックアップだけをNASに送る設計が定石です。
synchronous=NORMAL にするとデータが壊れる?
壊れません。ただし直近のコミットが失われる可能性はあります。WALモードで synchronous=NORMAL を指定すると、毎コミット時のWALへのsyncが省略され、その分高速になります(公式: PRAGMA synchronous)。電源断が起きた場合、syncされていない直近のトランザクションはロールバックされることがありますが、公式ドキュメントによれば、チェックポイント時にはsyncされるため、データベース自体が破損することはありません。「破損しない(原子性は保たれる)」と「最後の1コミットまで確実に永続化される(耐久性)」は別の概念であり、NORMALは後者を一部諦めるトレードオフの選択です。Raspberry Piなどのエッジデバイスでは、この構成でフラッシュへの書き込み負荷を抑える運用も見られます。
WALファイルが巨大化して読み込みが遅い。どう対処する?
2段階で対処します。まず、長時間続いている読み取りトランザクションがないか確認してください。チェックポイントはリーダーと並行して実行できますが、いずれかのリーダーが記録したend mark(読み取り開始時点のコミット位置)を超えるページで停止するため、放置された読み取りトランザクションがWALの縮小を妨げることがあります(公式: チェックポイント)。次に、PRAGMA wal_checkpoint(TRUNCATE); を手動実行してWALファイルを切り詰めます。WALが大きくなると読み取り性能はサイズに比例して低下するため、読み取りが重い運用ではチェックポイント頻度を上げる(PRAGMA wal_autocheckpoint の閾値を下げる)チューニングが有効です。
アプリケーションを変更せずにWALモードへ移行できる?
できます。WALモードは他のジャーナルモード(TRUNCATEやPERSISTは再オープンでDELETEに戻る)と異なり、設定が永続的です。一度 PRAGMA journal_mode=WAL; を実行するとモード情報がDBファイル自体に記録され、以降の再オープンでもWALモードが維持されます(公式: 永続性)。具体的には、sqlite3 CLIで対象DBに対してこのPRAGMAを一度実行し、戻り値が wal になったことを確認してアプリケーションを再起動するだけで移行完了です。アプリケーション側のコード変更は不要で、いずれか1つの接続で設定すれば同じDBファイルへの全接続に適用されます。
WALモードはどんな場合に向かない?
主に次の3つです(公式: WALの欠点・制約)。
- ネットワークファイルシステム(NFS/SMB)上での運用 — 共有メモリの前提が崩れるため動作しません(前述のFAQ参照)。
- 巨大なトランザクション — WALは追記型のため大きな変更ほどWALが肥大化します。目安として約100MB超のトランザクションではロールバックジャーナルの方が速く、1GB超ではディスクフルエラーで失敗するおそれがあります。数十MB以上のバッチ投入ではロールバックモードへの切り替えを検討してください。
- page_sizeの変更が必要な場合 — WALモード中はページサイズを変更できません(VACUUMやバックアップAPI経由でも不可)。変更するには一度ロールバックモードへ戻す必要があります。
このほか、読み取り専用メディアでの配布では -shm への書き込み権限の問題があるため、配布前にチェックポイント済みのクリーンな単一ファイルにしておくのが定石です。
まとめ——「壊れない」は魔法ではなく設計である
ここまで見てきたとおり、SQLiteが電源断やクラッシュで「壊れない」のは、魔法でも偶然でもなく、入念に設計された仕組みの結果です。その核心は2つ。1つはアトミックコミット——トランザクションの変更は「すべて適用される」か「1つも適用されない」かのどちらかで、中途半端な状態がディスクに残らないよう、fsyncのタイミングまで含めて制御されています。もう1つはジャーナルによるクラッシュ回復——ロールバックジャーナルは変更前の内容を退避してクラッシュ時に書き戻し、WALは変更を追記型ログとして蓄えることで、どちらもOSとハードウェアの前提が満たされる限り「コミット済みのデータを守る」ことを保証します。
WALモードはその延長線上で、シーケンシャルI/Oと少ないfsyncによる高速書き込みに加え、リーダーがライターをブロックしない同時実行性を実現しました。ただし代償もあります。すべてのプロセスが同一ホストにいる必要がある(NFS/SMB上では使えない)、チェックポイントの運用を意識する必要がある、巨大トランザクションには不向き、といった制約です。
モード選択の基準はシンプルです。ローカルディスクで同時アクセスがあるならWAL、NFS上に置くならロールバックジャーナル、数十MB級の巨大トランザクションならロールバック。そして忘れてはいけないのが、WALは「壊れない」の対価としてsynchronous設定との組み合わせで耐久性と速度のバランスを取れることです。
まずは手元のデータベースで PRAGMA journal_mode=WAL; と PRAGMA synchronous=NORMAL; を試してみてください。一度実行するだけで永続的にWALに切り替わる手軽さを実感できるはずです。home labやエッジデバイスでの実践例については、こちらの関連記事でも継続的に紹介しています。SQLiteの信頼性は、仕組みを知れば知るほど「よく作られている」と実感できる好例です。