導入Pythonスクリプトが単体では完璧に動作しているとします。しかし、マルチスレッドを追加したり、DB BrowserのようなGUIでデータベースを開いたりしてスケールアップした途端、sqlite3.OperationalError: database is lockedという厄介なエラーに直面することがあります。
これは通常、あるプロセスがデータベースに書き込みを行っている最中に、別のプロセスがそのトランザクションが終わる前に書き込み(あるいは時には読み取り)を試みた場合に発生します。SQLiteは非常に軽量ですが、デフォルトのロックメカニズムはかなり厳格です。
発生原因SQLiteは「シングルライター、マルチリーダー(1つの書き込み、複数の読み取り)」という設計思想に従っています。複数の同時読み取りは問題なく処理できますが、書き込みは一度に1つしか許可されません。書き込み操作が始まると、SQLiteはファイルに「RESERVED」ロックをかけます。そのロックが有効な間に別の接続がデータを変更しようとすると、エンジンは即座に、あるいは非常に短いデフォルトのタイムアウトの後に database is locked エラーを返します。
クイックフィックス:接続タイムアウトを増やす一時的なロックを処理する最も手っ取り早い方法は、ロックが解除されるまでもう少し長く待つようにSQLiteに指示することです。標準のPython sqlite3 接続のデフォルトは5.0秒のタイムアウトです。並行性の高い環境では、5秒は瞬く間に過ぎてしまいます。
import sqlite3
# 他のプロセスの終了を待つためにタイムアウトを20秒に増やす
conn = sqlite3.connect('my_database.db', timeout=20)
cursor = conn.cursor()
これでロックの発生自体を防げるわけではありませんが、アプリケーションの耐障害性は向上します。スクリプトが即座にクラッシュする代わりに、最大20秒間、内部で操作を再試行するようになります。
恒久的な対策:WALモードを有効にするアプリケーションで頻繁に読み書きが発生する場合、デフォルトの「rollback journal」モードがボトルネックになっている可能性があります。Write-Ahead Logging (WAL) モードに切り替えると、状況が劇的に改善されます。書き込み中であっても読み取りを継続できるため、競合が大幅に減少します。
データベースを初期化する際に、このコマンドを一度実行してください:
import sqlite3
conn = sqlite3.connect('my_database.db')
# 並行性を高めるためにWALモードを有効にする
conn.execute('PRAGMA journal_mode=WAL;')
cursor = conn.cursor()
cursor.execute("INSERT INTO logs (message) VALUES ('test')")
conn.commit()
conn.close()
WALモードは永続的です。一度有効にすると、明示的に戻さない限りデータベースはこのモードを維持します。接続がアクティブな間、.db ファイルの横に -wal と -shm という2つのテンポラリファイルが表示されます。これらは正常な動作であり、このモードが機能するために必要なものです。
適切なトランザクション管理完了していない処理が原因でエラーになることはよくあります。スクリプトが BEGIN をトリガーしたものの COMMIT や ROLLBACK に到達しない場合、データベースは無期限にロックされたままになります。これは、例外が発生して接続が適切にクリーンアップされない際によく起こります。
コンテキストマネージャを使用するのが、トランザクションを確実に終了させる最もクリーンな方法です。
import sqlite3
def update_data(val):
try:
# 'with' ステートメントは成功時に自動的にコミット、エラー時にロールバックを行う
with sqlite3.connect('my_database.db', timeout=20) as conn:
conn.execute('PRAGMA journal_mode=WAL;')
conn.execute("UPDATE settings SET value = ? WHERE id = 1", (val,))
except sqlite3.OperationalError as e:
print(f"Database error: {e}")
update_data("active")
外部ロックの確認問題がコード以外にある場合もあります。DBeaverやDB Browser for SQLiteなどのツールを開いている場合、手動で行った編集によって未コミットのトランザクションが保持されている可能性があります。GUIツールで「変更を書き込む(Write Changes)」または「コミット(Commit)」をクリックしたか確認してください。
- Windows: リソース モニターを使用して、
.dbファイルに関連付けられたハンドルを検索します。- Linux/macOS:lsofを実行して、どのプロセスがファイルを占有しているかを特定します:``` lsof | grep my_database.db
## 検証手順修正を確認するには、このスクリプトを使用して高負荷環境をシミュレートします。このスクリプトは、同じファイルに対して同時に書き込みを試みる5つのスレッドを並行して起動します:
import sqlite3 import threading import time
def worker(thread_id): try: conn = sqlite3.connect('test.db', timeout=10) conn.execute('PRAGMA journal_mode=WAL;') conn.execute("INSERT INTO test_table VALUES (?)", (thread_id,)) time.sleep(0.5) # 重い書き込み操作をシミュレート conn.commit() conn.close() print(f"Thread {thread_id} successful") except sqlite3.OperationalError as e: print(f"Thread {thread_id} failed: {e}")
テーブルを初期化
conn = sqlite3.connect('test.db') conn.execute("CREATE TABLE IF NOT EXISTS test_table (id INTEGER)") conn.close()
5つのスレッドを起動
threads = [threading.Thread(target=worker, args=(i,)) for i in range(5)] for t in threads: t.start() for t in threads: t.join()
すべてのスレッドが成功を報告すれば、実装は本番レベルの並行アクセスに耐えられるほど堅牢になっています。

