【SQLite】WALモードとは?読み書きを両立させる仕組み

SQL

こんにちは、かつコーチです。

前回はインデックスによる読み取り最適化を扱いました。

今回は、SQLiteの同時アクセス問題を大きく緩和するWALモード(Write-Ahead Logging)を、ジャーナリングの仕組みを前提知識として扱っていきます。

同時アクセスの記事で触れた「書き込み中は読み取りも待たされる」という制約は、WALモードによってかなり改善できます。

WALモードの仕組み

デフォルトのロールバックジャーナルとの違い

SQLiteのデフォルトであるロールバックジャーナルモードでは、書き込み時に変更前のデータを別ファイル(-journal)に退避してから本体ファイルを直接書き換えます。

この方式では、書き込み用のEXCLUSIVEロックを取得している間、読み取り用のSHAREDロックすら取得できません。

一方WALモードは、本体ファイルを直接書き換えるのではなく、変更内容を-walという別ファイルに追記していく方式です。

PRAGMA journal_mode = WAL;
ls -lh
# app.sqlite3
# app.sqlite3-wal   ← 変更内容が追記されていくファイル
# app.sqlite3-shm    ← 複数プロセス間でWALの状態を共有するための共有メモリファイル

読み取り側は「本体ファイル+まだチェックポイントされていないWALファイルの内容」を組み合わせて参照するため、書き込みが進行中でも本体ファイルには手を加えず、読み取りをブロックせずに済みます。

読み取りと書き込みが同時に成立する理由

WALモードの最大のメリットは、1つの書き込みトランザクションと、複数の読み取りトランザクションが同時に成立することです。

項目ロールバックジャーナルモードWALモード
書き込み中の読み取りブロックされる可能(書き込み開始前のスナップショットを読む)
同時に書き込めるプロセス数1(当然WALでも同じ)1
追加で生成されるファイル-journal(一時的)-wal, -shm(常時)
ネットワークファイルシステムでの利用比較的安全非推奨(ロック機構が正しく動作しない場合がある)

書き込みは相変わらず1プロセスしか同時に行えませんが、「書き込み中でも読み取りは止まらない」という点が、Webアプリケーションのような読み取り中心のワークロードにおいて非常に大きな差になります。

WALモードの使い方

モードの切り替えとチェックポイント

WALモードへの切り替えは、PRAGMA文1行で完結します。

PRAGMA journal_mode = WAL;
-- => wal

この設定は接続ごとではなく、データベースファイル自体に記録されるため、一度設定すればアプリを再起動しても維持されます。

WALファイルは書き込みのたびに肥大化していきますが、一定のタイミングで本体ファイルに反映(チェックポイント)され、WALファイルの内容がクリアされます。

-- 手動でチェックポイントを実行する
PRAGMA wal_checkpoint(TRUNCATE);

通常はSQLiteが自動でチェックポイントを実行してくれるため、明示的な実行は必須ではありませんが、バッチ処理の直後などWALファイルの肥大化が気になるタイミングで手動実行することもあります。

WALモードが向くケース・向かないケース

WALモードは万能ではなく、向き不向きがあります。

  • 向いているケース:Webアプリのバックエンド、読み取りが頻繁に発生するローカルアプリ、キャッシュエンジン的な用途
  • 向かないケース:ネットワークドライブ・NFS上にDBファイルを置く構成、非常に頻繁な書き込みが集中しファイルI/Oがボトルネックになる構成

WALモードはロック機構の一部をファイルシステムの共有メモリマッピングに依存しているため、ネットワークファイルシステム上では正しく動作しないことが公式ドキュメントでも明記されています。

クラウド上のネットワークストレージにSQLiteファイルを置く構成を検討している場合は、この制約を事前に確認しておく必要があります。

つまずきやすいポイント:WALファイルが肥大化し続ける

チェックポイントが実行されずファイルサイズが膨らむ

❌ Before:長時間接続を張りっぱなしにしてチェックポイントを妨げる

# 読み取り専用の接続を長時間開きっぱなしにする
conn = sqlite3.connect("app.sqlite3")
cursor = conn.execute("SELECT * FROM logs")
# この接続を閉じないまま、別プロセスが書き込みを続ける

私がログ収集用のバッチでWALモードを使い始めた際、監視ツール用に張っていた読み取り専用の接続を閉じ忘れていたことがありました。

その結果、自動チェックポイントが実行されずWALファイルがどんどん肥大化し、数日後には本体ファイルより-walファイルの方が大きくなっているという事態に気づきました。

WALモードでは、実行中の読み取りトランザクションがWALファイルの一部を参照し続けている限り、その部分は本体ファイルにチェックポイントできない仕組みになっています。

✅ After:接続のライフサイクルを短く保ち、定期的にチェックポイントを確認する

# 必要な処理が終わったら都度接続を閉じる
with sqlite3.connect("app.sqlite3") as conn:
    rows = conn.execute("SELECT * FROM logs").fetchall()
# withブロックを抜けるタイミングで接続がクローズされる
-- WALファイルのサイズや未チェックポイント分のページ数を確認する
PRAGMA wal_checkpoint;

読み取り接続を必要な範囲に限定して閉じる運用に変えてからは、WALファイルが異常に肥大化する問題は解消しました。

長時間動作するプロセスでWALモードを使う場合は、接続の寿命を意識することが重要です。

応用:synchronousプラグマとの組み合わせ

WALモードは、PRAGMA synchronousの設定と組み合わせることで、さらに性能特性を調整できます。

-- WALモードでは NORMAL でも十分な耐障害性が確保されるとされる
PRAGMA synchronous = NORMAL;

デフォルトのFULLはディスクへの同期を厳密に行う分、書き込み性能に制約が出ますが、WALモードとNORMALの組み合わせは、クラッシュ耐性を保ちながら書き込み性能を上げる定番の構成として広く使われています。

パフォーマンスと耐障害性のどちらを優先するかは、アプリケーションの要件に応じて判断してください。

まとめ

この記事のポイント

  • WALモードは変更内容を-walファイルに追記する方式で、書き込み中でも読み取りをブロックしない
  • PRAGMA journal_mode = WAL一行で切り替えられ、設定はファイルに永続化される
  • チェックポイントで本体ファイルに反映される。長時間の読み取り接続は肥大化の原因になる
  • ネットワークファイルシステム上ではWALモードは非推奨

次に読むべき記事

WALモードで運用の耐久性が上がったところで、次はSQLiteの運用に欠かせないバックアップとVACUUMコマンドを見ていきましょう。

→ 次の記事:SQLiteのバックアップとVACUUMコマンド

タグ: SQLite, 上級者向け, パフォーマンス

タイトルとURLをコピーしました