インフラエンジニアです。
何気なく値が変わっていなくてもデータベースの値を同じ値でアップデートすると何が起るのか調べたらめっちゃホラーだったので共有します。
ジュニアアプリケーションエンジニア向けの内容です。
1. インフラ・DBエンジン層で起きること
値が変わらない更新(No-op Update)であっても、RDBMS内部では物理的な書き込みプロセスがトリガーされます。
ストレージI/OとWAL(Write Ahead Logging)
多くのRDBMS(MySQL, PostgreSQL等)では、データの変更前に WAL(先行書き込みログ) への記録が行われます。値が同一であっても、UPDATEクエリが発行された時点でログへのシーケンシャルな書き出しが発生し、ディスクI/Oのリソースを消費します。
MVCC(多版型並行制御)への影響
特にPostgreSQLのようなMVCCを採用しているデータベースでは顕著です。
- 物理的な行の追加: MVCCでは、既存の行を書き換えるのではなく、新しいバージョンの行を作成します。値が同じでも「新しい行」が作成され、古い行は「デッドタプル」となります。
- テーブルの肥大化(Bloat): 空更新を繰り返すと、有効なデータ量は変わらないのに物理的なファイルサイズだけが増大し、フルスキャンやインデックスのパフォーマンスが低下します。
ロック競合とデッドロック
- 排他ロック(Exclusive Lock): UPDATE文は対象行に排他ロックをかけます。値の変更がなくても、コミットされるまで他のセッションによるその行への更新はブロックされます。
- インデックスへの波及: 更新対象のカラムにインデックスが貼られている場合、値が変わらなくてもインデックスエントリーの更新が発生し、インデックスツリーのロック競合を引き起こす可能性があります。
インフラ的にはバキュームしても効かないなくらいの違和感で済むのでここまでなら言うほどホラーではないです。問題はここからです。
2. アプリケーション・データ整合性層で起きること
ここでの最大の問題は「データの先祖返り」です。これは並行制御の不備による ロスト・アップデート(Lost Update) に分類されます。
ロスト・アップデート(消失した更新)のメカニズム
- Read-Modify-Writeアンチパターン: アプリケーションが「一度SELECTしてメモリに展開し、一部を書き換えて全体をUPDATEする」という設計の場合に発生します。
- クリティカルセクションの破壊: * セッションAが 値:X を読み込む。
- セッションBが 値:X を読み込む。
- セッションAが 値:Y に更新(コミット)。
- セッションBが「自分は変更していないからXのままだ」と判断せず、保持していた 値:X で全カラム上書きUPDATEを実行。
- 結果: セッションAの更新(Y)が、Bの「古いX」によって上書きされ、消失します。
べき等性(Idempotency)と副作用
UPDATE処理自体は、数学的には特定の状態に収束させる「べき等」な操作に見えますが、並行環境下では「他者の状態変化を無視する」という副作用を孕みます。
実務でやっちまったら背筋が凍りますね。
3. 回避策:技術的アプローチ
記事の結びとして、以下の技術的対策を提示するのが一般的です。
-
Dirty Checking (差分検知):
ORM(HibernateやActive Recordなど)が備える、メモリ上のオブジェクトの状態とDBから取得時の状態を比較し、変更があったカラムのみを UPDATE 文に含める仕組み。これにより、無関係なカラムの「先祖返り」を防ぎます。 -
Optimistic Concurrency Control (楽観的並行制御):
WHERE 句に更新前のバージョン番号や最終更新日時を含める手法です。
UPDATE table SET value = 'X', version = 2 WHERE id = 1 AND version = 1;もし他者が先に更新していれば、version=1 の行が見つからず更新件数0件となり、アプリケーション側で衝突を検知できます。
以上、何気なく調べた内容がホラーだった件でした。