#08 SQLをちゃんと理解する

トランザクションとACID

トランザクションとは

トランザクション? 複数のDB操作をひとまとまりとして扱う仕組み。途中でエラーが起きたら全部元に戻す(ロールバック)ことで、データの整合性を保つ。 とは、複数のSQL操作をひとまとまりの処理として扱う仕組みです。「全部成功するか、全部なかったことにするか」を保証します。

典型的な例として銀行振込を考えましょう。

-- A の口座から B の口座へ 10,000円 振込
UPDATE accounts SET balance = balance - 10000 WHERE id = 'A';
UPDATE accounts SET balance = balance + 10000 WHERE id = 'B';

1行目が成功して2行目が失敗した場合、Aの残高だけが減って消えてしまいます。トランザクションを使えば、どちらかが失敗したとき両方を取り消すことができます。


BEGIN / COMMIT / ROLLBACK

-- トランザクションの基本構文
BEGIN;  -- または START TRANSACTION;

UPDATE accounts SET balance = balance - 10000 WHERE id = 'A';
UPDATE accounts SET balance = balance + 10000 WHERE id = 'B';

COMMIT;   -- 全操作を確定する
-- または
ROLLBACK; -- 全操作をなかったことにする

アプリケーション側での典型的な処理フロー:

BEGIN;
-- 操作1
UPDATE accounts SET balance = balance - 10000 WHERE id = 'A';
-- 操作2
UPDATE accounts SET balance = balance + 10000 WHERE id = 'B';
-- 問題なければコミット
COMMIT;
-- エラーが発生した場合
-- ROLLBACK;

SAVEPOINT

トランザクション内に「セーブポイント」を設け、部分的なロールバックが可能です。

BEGIN;
INSERT INTO orders VALUES (...);
SAVEPOINT after_order;  -- セーブポイントを設定

INSERT INTO order_items VALUES (...);
-- order_itemsだけを取り消したい場合
ROLLBACK TO after_order;  -- セーブポイントまで戻る

COMMIT;  -- ordersのINSERTだけ確定

ACID特性

ACID特性? トランザクションが満たすべき4つの性質。Atomicity(原子性・全部成功か全部失敗)・Consistency(一貫性)・Isolation(分離性・他のトランザクションの影響を受けない)・Durability(永続性)。 とは、トランザクションが保証する4つの特性の頭文字です。

ACID — トランザクションの4特性 A Atomicity 原子性 全部成功か 全部失敗のどちらか 途中の状態はない 振込: 引落と入金は 必ずセットで完了 C Consistency 一貫性 トランザクション前後で 整合性を保つ 制約違反ではCOMMIT不可 外部キー・NOT NULL・ UNIQUE 制約を維持 I Isolation 独立性 他トランザクションの 中途状態が見えない 分離レベルで制御 READ COMMITTED~ SERIALIZABLE D Durability 永続性 COMMITしたデータは 障害でも失われない WAL・ディスク書込 Write-Ahead Log で クラッシュ後も復元可 トランザクションの流れ BEGIN UPDATE / INSERT / DELETE ... 成功? COMMIT YES ROLLBACK NO 変更が確定・永続化 変更がすべて取り消し SAVEPOINT で部分ロールバックも可能 SAVEPOINT sp1; ... ROLLBACK TO sp1; セーブポイントまで戻り、それ以前は維持 複雑なトランザクションの途中状態を保存できる
図1: ACID 4特性の概要と意味

A: Atomicity(原子性)

トランザクション内のすべての操作は「全部成功」か「全部失敗」のどちらかになります。途中の状態はありません。

C: Consistency(一貫性)

トランザクションの前後でデータベースは整合性のある状態を保ちます。外部キー制約、NOT NULL制約などがすべて満たされた状態でしかCOMMITできません。

I: Isolation(独立性)

同時に複数のトランザクションが実行されても、互いの中途状態が見えないようにします(どの程度遮断するかは分離レベルで設定します)。

D: Durability(永続性)

COMMITしたデータはハードウェア障害が起きても失われません。ディスクへの書き込みやWAL(Write-Ahead Log)によって保証されます。


分離レベルと発生する問題

複数のトランザクションが同時に実行されると、分離が不十分な場合に以下の問題が発生します。

ダーティーリード

まだコミットされていない別トランザクションの変更が見えてしまう。

T1: UPDATE accounts SET balance = 50000 WHERE id = 'A';
T2: SELECT balance FROM accounts WHERE id = 'A';  -- 50000が見える(まだT1はCOMMITしていない)
T1: ROLLBACK;  -- T2が見たデータは存在しなかったことになる

ノンリピータブルリード

同一トランザクション内で同じ行を2回読んだとき、別トランザクションのCOMMITにより値が変わる。

T1: SELECT balance FROM accounts WHERE id = 'A';  -- 100000
T2: UPDATE accounts SET balance = 80000 WHERE id = 'A'; COMMIT;
T1: SELECT balance FROM accounts WHERE id = 'A';  -- 80000(変わった!)

ファントムリード

同一トランザクション内で同じ条件で検索したとき、別トランザクションのINSERT/DELETEにより行数が変わる。

T1: SELECT COUNT(*) FROM orders WHERE date = '2025-01-01';  -- 10件
T2: INSERT INTO orders (...) VALUES (...); COMMIT;  -- 同じ日付で追加
T1: SELECT COUNT(*) FROM orders WHERE date = '2025-01-01';  -- 11件(幽霊行が出現)

4段階の分離レベル

分離レベル? トランザクション同士がどの程度互いの変更を見えるようにするかの設定。READ UNCOMMITTED・READ COMMITTED・REPEATABLE READ(MySQLデフォルト)・SERIALIZABLE の4段階がある。 によって、どの問題を防ぐかが決まります。

分離レベルと発生する問題 分離レベル ダーティーリード ノンリピータブル ファントムリード READ UNCOMMITTED コミット前のデータも読める 発生 発生 発生 READ COMMITTED コミット済みのみ読める(多くのDBのデフォルト) 防止 発生 発生 REPEATABLE READ 同じ行は同じ値で読める(MySQLデフォルト) 防止 防止 発生* SERIALIZABLE 最も厳格。直列実行と同等の結果を保証 防止 防止 防止 *MySQLのInnoDBはネクストキーロックにより REPEATABLE READ でもファントムリードをほぼ防止 各問題の説明 ダーティーリード: 未コミットの変更が見える ノンリピータブル: 同行を再読したら値が変わる ファントムリード: 再検索で行数が変わる (他TxがINSERT/DELETEした幽霊行が出現) 分離レベルのトレードオフ 分離レベルが高い ← 安全・正確 分離レベルが高い → 同時実行性が低下 多くのWebアプリはREPEATABLE READで十分 金融系など強整合性が必要な場面ではSERIALIZABLE
図2: 分離レベルと発生する問題の対応表
分離レベルダーティーリードノンリピータブルファントムリード
READ UNCOMMITTED発生発生発生
READ COMMITTED防止発生発生
REPEATABLE READ防止防止発生
SERIALIZABLE防止防止防止
-- 分離レベルの設定(MySQL)
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;

-- 分離レベルの設定(PostgreSQL)
BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;

MySQLのInnoDBはデフォルトが REPEATABLE READ で、ネクストキーロックによりファントムリードも多くの場合防止されます。多くのアプリケーションではこのデフォルトで十分です。


デッドロック

2つのトランザクションが互いに相手のロックを待ち続ける状態をデッドロックと言います。

T1: accounts の id=A をロック
T2: accounts の id=B をロック
T1: id=B をロックしようとする → T2 が解放するまで待機
T2: id=A をロックしようとする → T1 が解放するまで待機
→ 互いに待ち続けてデッドロック

DBMSはデッドロックを検出すると、一方のトランザクションを強制ロールバックします。アプリケーション側はデッドロックエラーを受け取ったらリトライする実装が必要です。

デッドロックの回避策

-- 対策1: 常に同じ順番でテーブル・行をロックする
-- 悪い例:
-- T1: id=A → id=B の順でロック
-- T2: id=B → id=A の順でロック  ← 順序が逆でデッドロック発生

-- 良い例:
-- T1: id=A → id=B の順でロック
-- T2: id=A → id=B の順でロック  ← 常に同じ順序

-- 対策2: SELECT ... FOR UPDATE で先にロックを取得
BEGIN;
SELECT * FROM accounts WHERE id IN ('A', 'B') ORDER BY id FOR UPDATE;
-- ORDER BY id で常に同じ順序でロック
UPDATE accounts SET balance = balance - 10000 WHERE id = 'A';
UPDATE accounts SET balance = balance + 10000 WHERE id = 'B';
COMMIT;

-- 対策3: トランザクションを短くする(ロック保持時間を最小化)

まとめ

  • トランザクションは複数の操作を「全部成功か全部失敗」にまとめる仕組み
  • BEGIN / COMMIT / ROLLBACK でトランザクションを制御する
  • ACID: 原子性・一貫性・独立性・永続性の4特性
  • 分離不足でダーティーリード・ノンリピータブルリード・ファントムリードが発生
  • 分離レベルは高いほど安全だが同時実行性能が下がる
  • デッドロックはロック順序の統一とトランザクションを短くすることで回避

次回はこのシリーズ最終回として インデックス? テーブルの検索を高速化するためのデータ構造。本の索引のように、特定の列の値から行の位置をすばやく特定できる。検索は速くなるが、書き込みはやや遅くなる。 と実行計画の基礎を解説します。