社内向けの受発注システムや請求書発行ツールを、軽量なPythonスクリプトとSQLiteで組んでいる方に向けた内容です。依存パッケージを増やさず、単一ファイルのデータベースで業務アプリを運用する構成は手軽に見えます。ただしSQLite(サーバーを立てずファイル1つで動くデータベースエンジン)のデフォルト設定のまま使うと、同時アクセスが増えた瞬間にロック待ちが頻発する落とし穴があります。
何が起きるか
SQLiteを標準設定のまま使っている業務アプリで、複数人が同時に書き込み処理を実行すると「database is locked」というエラーが出ることがあります。
注文登録と在庫更新が同時に走る、請求書発行と入金確認の処理が重なる、といった場面で発生しやすい症状です。
影響範囲は地味ながら深刻です。エラーが出たリクエストは処理が失敗し、再実行の仕組みがなければデータが欠落します。
さらに厄介なのは、開発環境や少人数のテストでは絶対に再現しないという点です。利用者が3人から10人に増えた瞬間に初めて表面化し、原因の切り分けに時間がかかります。
なぜ起きるか
原因はSQLiteの「ジャーナルモード」という仕組みにあります。これはデータベースが書き込み中にクラッシュしても整合性を保つための記録方式です。
SQLiteのデフォルトは「rollback journal」というモードです。この方式では、誰かが書き込みを行っている間、データベースファイル全体がロックされます。
ロックされている間は、他の接続からの読み取りも書き込みも待たされます。読み取り専用の処理まで巻き込まれて待機するのが、rollback journalの大きな弱点です。
接続のタイムアウト設定が短い、あるいは未設定の場合、待機中にタイムアウトしてエラーになります。Pythonのsqlite3モジュールはデフォルトのタイムアウトが5秒程度と短めなので、書き込みが集中する時間帯にはすぐに限界が来ます。
これは「SQLiteが遅いから」ではありません。アーキテクチャの前提として、複数プロセス・複数スレッドからの同時書き込みを想定した排他制御の設計が、アプリ側で行われていないことが根本原因です。
自分のプロジェクトが該当するか確認する方法
まず、使っているSQLiteのジャーナルモードを確認します。Pythonから接続して次のコマンドを実行してください。
import sqlite3
conn = sqlite3.connect("your_database.db")
mode = conn.execute("PRAGMA journal_mode;").fetchone()
print(mode)結果が('delete',)や('truncate',)と表示された場合、デフォルトのrollback journalのままです。('wal',)と表示されれば、すでにWALモードが有効になっています。
次に、該当アプリのコード内でsqlite3.connect()を呼んでいる箇所を検索してください。
grep -rn "sqlite3.connect" --include="*.py" .PRAGMA journal_mode = WAL;という文字列が接続直後に実行されているかどうかを確認します。見当たらなければ未対応の状態です。
あわせて確認したいのが同時アクセスの実態です。Webアプリであれば複数ワーカープロセスで動いているか、バッチ処理と同時にUIからの書き込みが発生し得るかをチェックしてください。単一プロセス・単一スレッドで常に動くツールであれば、優先度は下がります。
対策の手順
対策はシンプルで、接続直後に3つのPRAGMA文(SQLiteの動作設定を変更する命令)を実行するだけです。
import sqlite3
def connect(db_path: str) -> sqlite3.Connection:
conn = sqlite3.connect(db_path, timeout=15.0)
conn.row_factory = sqlite3.Row
conn.execute("PRAGMA journal_mode = WAL;")
conn.execute("PRAGMA foreign_keys = ON;")
conn.execute("PRAGMA synchronous = NORMAL;")
return connそれぞれの設定が何をしているか分解します。
journal_mode = WAL: Write-Ahead Logging(書き込み内容を別ファイルに先行記録する方式)に切り替える設定。読み取りが書き込みをブロックしなくなるforeign_keys = ON: 外部キー制約を有効化する設定。親子関係のあるテーブルで不整合なデータ削除を防ぐsynchronous = NORMAL: ディスクへの同期頻度を調整する設定。WALモードと組み合わせることで性能とデータ保護のバランスを取るtimeout=15.0: 書き込みロックが解放されるまで最大何秒待つかの設定。デフォルトの5秒より余裕を持たせる
WALモードに切り替えると、データベースファイルと同じディレクトリに-walと-shmという補助ファイルが作られます。これは正常な動作です。
バックアップを取る際は、本体ファイルだけでなくこの2つの補助ファイルも含めてコピーするか、sqlite3コマンドラインツールの.backupコマンドを使う必要があります。ファイルコピーだけだと未反映のデータが欠落する可能性があるため注意してください。
もう1点、接続ごとにPRAGMA文を実行する設計が面倒であれば、トランザクションの扱いも合わせて見直すとよい箇所があります。Pythonのsqlite3ではwith conn:構文を使うことで、BEGIN・COMMIT・ROLLBACKを自動化できます。書き込み処理のたびに手動でコミットを書いていないか確認してください。
まとめ
SQLiteは依存パッケージゼロで動く手軽さが魅力ですが、デフォルト設定のまま本番運用に入ると同時書き込みの壁にぶつかります。
まず確認すべきはPRAGMA journal_mode;の出力です。wal以外であれば対策が必要と判断できます。
対策は接続処理にjournal_mode = WAL・foreign_keys = ON・synchronous = NORMALの3行を加えるだけで完結します。コードの変更量は小さいものの、効果は同時アクセス時の安定性に直結します。
バックアップ運用も-wal・-shmファイルを含めた方式に見直しておくと、将来のトラブルを一つ減らせます。小さな設定変更ですが、業務アプリの可用性を左右する判断材料として覚えておく価値があります。