デフォルトモードの限界
SQLite はデフォルトで ロールバックジャーナル を使います。書き込みが発生すると、SQLite は元のページを -journal ファイルにコピーし、メインのデータベースを書き換えて、コミット時にジャーナルを削除します。途中でプロセスがクラッシュしても、ジャーナルを逆順に適用すれば中途半端な変更を取り消せる、という仕組みです。
シンプルで安全な方式ですが、ひとつ厄介な性質があります。書き込み側と読み込み側が同じファイルを取り合うのです。ライターがデータベースのロックを握っている間は、リーダーは新しいトランザクションを開始できません。逆にリーダーが動いている間は、ライターが待たされます。リクエストが少し並ぶ Web サーバーのような、ある程度トラフィックのあるアプリだと、SQLITE_BUSY エラーが想像以上に早く飛んできます。
これを解決してくれるのが WAL モードです。
SQLite WAL モードの仕組み
WAL(Write-Ahead Logging)は発想を逆転させたモードです。メインのデータベースファイルを直接書き換えるのではなく、コミット済みのページを -wal という拡張子の別ファイルに 追記 していきます。リーダーは引き続きメインファイルを読みつつ、必要なページの新しいバージョンが WAL にないかも確認する、という流れになります。
これによって、ライター 1 つと任意の数のリーダーが同時に動けるようになります。各リーダーはトランザクション開始時点の一貫したスナップショットを見ながら、ライターはリーダーが見ているデータには触れずに WAL へ追記していく、というわけです。
このプラグマを実行するだけで、データベースがWALモードに切り替わります。設定はファイルヘッダーに保存されるため永続化され、以降のすべての接続で自動的にWALモードが使われます。接続ごとに毎回実行する必要はなく、データベースを初期化するとき(あるいはマイグレーションスクリプトの中)で一度だけ流せば十分です。
このプラグマは切り替え後のモードを返します。WAL が返ってくれば成功です。それ以外が返ってきた場合は、ファイルシステムが共有メモリをサポートしていない可能性が高いです(詳細は後述)。
WALモードの有効化と確認
現在のジャーナルモードはいつでも確認できます。
1回目の呼び出しでWALを有効化し、新しいモードを返します。2回目の呼び出し(=なし)は現在のモードを問い合わせるだけです。これ以降、書き込みなどのアクティビティがあると、messages.dbがあるディレクトリには messages.db、messages.db-wal、messages.db-shm の3つのファイルが現れます。後ろの2つは、接続が開いているかどうかによって出現したり消えたりします。
-wal ファイルと -shm ファイルの役割
WALモードでは2つの追加ファイルが付いてきます。それぞれの役割を押さえておきましょう。
-walはコミット済みだがまだ本体DBにマージされていないトランザクションを保持します。書き込みが発生するたびに大きくなり、チェックポイントのタイミングで縮小(またはリセット)されます。-shmは共有メモリファイルです。WALへのインデックスとして機能し、どのページがどこにあるかをすべての接続が把握できるようにします。これがあるおかげで、クエリのたびにWAL全体を走査する必要がありません。
ここから導かれる実用的な注意点として、WALモードのデータベースを .db ファイルだけコピーしてバックアップしてはいけません。最新データは -wal の中にあり、これを含めないコピーは古い状態か、最悪の場合壊れたものになります。書き込み中の接続がない状態で3ファイルすべてをコピーするか、もっと安全な方法としてSQLiteのバックアップAPI(次章で扱います)を使ってください。
書き込みは1つ、読み込みは複数:sqliteの並行処理
WALを使っても、書き込みを並列に走らせられるわけではありません。SQLiteは依然として書き込みを直列化し、ある時点で書き込みロックを保持できるトランザクションは1つだけです。WALで変わったのは、書き込みが読み込みをブロックせず、読み込みも書き込みをブロックしないという点です。
そのため、WALで動かしている典型的なWebアプリは次のような挙動になります。
- 読み込み中心のエンドポイントは競合せずに並列実行できる。
- 書き込みエンドポイントは互いに少しキューイングされるが、読み込みは止めない。
- 長時間走る読み込み(分析クエリやエクスポートなど)が書き込み側を待たせることもない。
ただし、2つの接続が同時に書き込もうとすると、後発の方は SQLITE_BUSY を受け取ります。対処法としては、適切なビジータイムアウトを設定するのが定番です。SQLiteに「諦める前に少し待ってね」と伝えるイメージです。
busy_timeout=5000 は「ロックがかかっていたら、エラーを出すまで最大5秒は待つ」という設定です。WALと組み合わせることで、実際のアプリで起こるロック競合のほとんどはこれでさばけます。BEGIN IMMEDIATE を使うと、最初の書き込み時ではなくトランザクション開始時に書き込みロックを取得するため、複数のコネクションが同時に書き込もうとしたときに発生しがちなアップグレード型のデッドロックを避けられます。
チェックポイント:WALを本体に書き戻す
WALファイルは無限に大きくなれるわけではありません。チェックポイント とは、WALにたまったコミット済みのページをメインのデータベースファイルに書き戻し、WALをリセットする処理のことです。
SQLiteは、WALがおよそ1000ページを超えたタイミングで自動的にチェックポイントを実行します(これがデフォルトの wal_autocheckpoint の挙動です)。ほとんどのアプリではこのままで問題ありません。閾値をチューニングしたい、あるいは手動でチェックポイントを走らせたい場合は次のようにします:
wal_checkpoint プラグマにはモードを指定します。
PASSIVE— リーダーやライターを邪魔せず、できる範囲でチェックポイントを実行します。デフォルトの挙動です。FULL— 書き込み中のトランザクションが終わるのを待ってから、コミット済みの内容をすべてチェックポイントします。RESTART—FULLの動作に加えて、新しいリーダーが古い WAL を使えないようにブロックします。TRUNCATE—RESTARTの動作に加えて、WAL ファイルのサイズを 0 バイトまで切り詰めます。
サーバー用途であれば、これを手動で呼ぶ必要はまずありません。ただし、デスクトップアプリを配布していて、終了時にファイルサイズをきれいに保ちたい場合は、最後の接続を閉じる前に TRUNCATE チェックポイントを実行しておくのは理にかなった習慣です。
WAL モードと相性のいいプラグマ
WAL を有効化するだけでも十分効果はありますが、本番運用ではいくつかの設定を組み合わせて使うのが定番です。
ざっと押さえておきたいポイントです。
synchronous=NORMALは WAL モードと組み合わせる定番設定です。アプリのクラッシュや OS のクラッシュには耐えられ、データベースの整合性も保たれます。直近のトランザクションが失われる可能性があるのは、運悪く電源断が起きた瞬間くらいです。デフォルトのFULLの方が安全ですが、体感できるほど遅くなります。busy_timeoutは前のセクションで説明したとおりです。foreign_keys=ONは WAL とは関係ありませんが、どの接続でも必ず設定しておきたい項目です。SQLite は後方互換性のため、外部キー制約をデフォルトでオフにしているからです。
これらは接続ごとの設定です(ただし journal_mode だけは永続化されます)。アプリ側で接続を開いた直後に実行するのがおすすめです。
WAL モードが向かないケース
WAL はデフォルトで推奨される設定ですが、いくつか例外もあります。
- ネットワークファイルシステム。 WAL はデータベースにアクセスするプロセス間で共有メモリ(
mmap)を使う仕組みです。NFS や SMB などのプロトコルではこれが安定して動きません。データベースをネットワーク共有に置く場合はロールバックジャーナルを使うか、そもそも SQLite をネットワーク共有に置かないのが無難です。 - 読み取り専用メディア。 WAL は
-walファイルと-shmファイルへの書き込みが必要です。CD-ROM のようなメディア上のデータベースでは、書き込みを伴わないジャーナルモードを使うか、mode=roで読み取り専用として開く必要があります。 - 同時読み取りのないシングルライターのバッチ処理。 WAL にしてもデメリットはありませんが、特に得るものもありません。デフォルトのロールバックジャーナルで十分です。
それ以外、つまり Web バックエンド、デスクトップアプリ、モバイルアプリ、ローカルストレージを持つ組み込みデバイスといった 95% のアプリケーションでは、WAL が正解です。
実戦向けの設定例
本番環境の SQLite でよく見かける構成を、そのまま実行できる PRAGMA にまとめたものがこちらです。
temp_store=MEMORY を指定すると、一時テーブルやインデックスがディスクではなくメモリ上に保持されます。メモリに余裕があるなら、タダで得られるちょっとしたパフォーマンス向上です。
これらをアプリのDB初期化処理の中で接続時にまとめて設定しておけば、SQLiteベースのアプリが同時アクセス下でうまく振る舞うために必要なポイントは、ほぼ押さえたことになります。
次回:バックアップとリストア
データベースに -wal や -shm といった付属ファイルが生まれた今、ファイルをそのままコピーするだけでは安全なバックアップとは言えません。次の章では、稼働中のSQLiteデータベースを正しくバックアップする方法を扱います。.backup コマンド、オンラインバックアップAPI、そしてアプリを止めずに整合性のあるスナップショットを取る方法について見ていきましょう。
よくある質問
SQLiteのWALモードとは?
WALは write-ahead logging(ライトアヘッドログ)の略です。従来のように本体のDBファイルを直接更新してロールバックジャーナルで巻き戻す方式ではなく、変更内容をいったん別ファイルの-walに追記しておき、あとでまとめて本体に反映します。最大のメリットは並行性で、リーダーとライターが互いをブロックせずに同時に動けるようになります。
SQLiteでWALモードを有効にするには?
PRAGMA journal_mode=WAL;を一度実行するだけでOKです。この設定はDBファイルのヘッダーに書き込まれて永続化されるので、以降の接続では自動的にWALが使われます。接続ごとに毎回設定する必要はありません。成功すると新しいモード(wal)が返ってきます。
WALモードなら書き込みも並列にできる?
いいえ。書き込みはあくまで直列化されます。書き込みロックを保持できるのは常に1つのライターだけです。WALで変わるのは「リーダーがライターをブロックしない/ライターもリーダーをブロックしない」という点で、実際のアプリではこの部分こそがボトルネックだったケースがほとんどです。
-walファイルと-shmファイルって何?
-walファイルと-shmファイルって何?-walファイルにはコミット済みでまだ本体DBに反映されていない変更が入っています。-shmファイルは小さな共有メモリ用のインデックスで、各接続がWAL内のページを高速に探すために使います。どちらも自動的に作り直されますが、DBをコピーするときはこれらも一緒にコピーするか、バックアップAPIを使う必要があります。