株式会社Good Labでエンジニアをしている コータロー です。
日々、Java・SQL・Gitなどの技術情報や、新人エンジニア向けの学習ノウハウ、
AI活用についての情報を発信しています。
Good Labについて気になった方は、コーポレートサイトもぜひご覧ください。
▶コーポレートサイト
このシリーズについて
「知ってはいる。けど、人に説明しろと言われると詰まる」——そんな歯がゆいDB用語を、1記事1用語・図解中心・5分で読める分量で解消していくシリーズです。
第1回は ACID。特に C(一貫性) を狙い撃ちします。
まず自己診断
次の質問に、口に出して答えてみてください。
- ACIDの A は? →「原子性。全部やるか、全部やらないか」……たぶん言えます
- ACIDの D は? →「永続性。コミットしたら消えない」……これも言えます
- ACIDの C は? →「一貫性。ええと……データが壊れないこと?」
3で詰まったら、この記事の対象読者です。 実は「データが壊れない」はA・I・Dの説明にもなってしまう、いちばん曖昧な答えです。
ACIDの全体像
ACIDは、トランザクション(=ひとまとまりの処理)が満たすべき4つの性質の頭文字です。
以下、口座間送金(AさんからBさんへ800円)を題材に1つずつ見ていきます。SQLの実験はすべて SQLite 3.51.0(macOS標準の sqlite3)で実行した実測結果です。
A:原子性 —— 「途中で終わらせない」
送金は「引く」と「足す」の2ステップ。片方だけ成功すると、お金が消えます。
原子性とは、この赤い中間状態を外から観測させない・そこで終わらせないという保証です。
ここが落とし穴:エラー=自動ロールバックではない
「エラーが出たら勝手に巻き戻る」と思っていると事故ります。実際に試します。2本目を「足す」ではなく「引く」と書き間違えた、というありがちなバグを想定します(残高500から800は引けないので CHECK 制約に引っかかります)。
CREATE TABLE accounts (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
balance INTEGER NOT NULL CHECK (balance >= 0)
);
INSERT INTO accounts VALUES (1, 'A', 1000), (2, 'B', 500);
BEGIN;
UPDATE accounts SET balance = balance - 800 WHERE id = 1;
UPDATE accounts SET balance = balance - 800 WHERE id = 2; -- 本当は + のはず。残高500なのでCHECK違反
SELECT * FROM accounts; -- ← エラーの直後、まだROLLBACKしていない
実行結果:
Runtime error near line 12: CHECK constraint failed: balance >= 0 (19)
id name balance
-- ---- -------
1 A 200 ← 1本目のUPDATEは生きたまま
2 B 500
2本目が失敗しても、1本目の変更は残っています。SQLiteの既定の競合解決は ABORT で、これは「失敗したそのSQL文の変更だけを取り消し、トランザクションは開いたまま継続する」という動作だからです(MySQL・PostgreSQLでも「文が1つ失敗=即座に全体が巻き戻る」とは限りません。PostgreSQLはエラー後アボート状態になり、以降の文を拒否します)。
つまり 原子性は「アプリが ROLLBACK を実行して初めて完成する」。ここで ROLLBACK; を送ると、期待どおり A:1000 / B:500 に戻ります。
実務上の含意:例外を握りつぶして
ROLLBACKを呼ばないコードは、原子性を自分で壊しています。フレームワークの@Transactional等が何を契機にロールバックするかは必ず確認を。
C:一貫性 —— 本題
よくある説明と、その物足りなさ
「一貫性とは、データが壊れないこと」
これでは A(原子性)や D(永続性)と区別がつきません。もう一段だけ正確にします。
一貫性とは「トランザクションは、DBを"ルールを満たした状態"から"別のルールを満たした状態"へ移す」という保証です。途中でルールを破るのは構いません。終わったときに破っていなければいい。
A・C・D を1文で対比すると差がはっきりします。
| 性質 | 守っているもの | 反対語 |
|---|---|---|
| A:原子性 | 処理のまとまり | 中途半端に適用された状態 |
| C:一貫性 | データのルール | ルール違反のデータ |
| D:永続性 | 結果の寿命 | コミット後に消える |
そして最大のポイント:DBが守る「ルール」とは何か
ここが歯がゆさの正体です。DBは、あなたが宣言した制約しか守ってくれません。
「合計残高は変わらない」というルールを守るのは、DBではなくアプリケーションです。DBが担保するのは、宣言済みの制約が COMMIT 時点で破られていないこと。ACIDのCは、DBとアプリの責任分界点を示す言葉なのです。
実験:宣言しなければ、不整合は普通に入る
SQLiteには「外部キー制約はデフォルトで無効」という有名な仕様があります(後方互換性のため。SQLite固有の話で、MySQL/InnoDB や PostgreSQL では宣言すれば有効です)。これがCの本質をよく表しています。
CREATE TABLE accounts (id INTEGER PRIMARY KEY, name TEXT NOT NULL);
CREATE TABLE transfers (
id INTEGER PRIMARY KEY,
account_id INTEGER NOT NULL REFERENCES accounts(id) -- 外部キーを宣言している
);
INSERT INTO accounts VALUES (1, 'A');
PRAGMA foreign_keys; -- 現在の設定を確認
INSERT INTO transfers VALUES (100, 999); -- 存在しない口座999への参照
SELECT * FROM transfers;
実行結果:
foreign_keys
------------
0 ← 無効になっている
id account_id
--- ----------
100 999 ← 存在しない口座を参照する行が、エラーも出ずに入った
同じDBに対し PRAGMA foreign_keys = ON; を付けてから同じINSERTを流すと:
Runtime error near line 2: FOREIGN KEY constraint failed (19)
書いてあるSQLは同じ。違うのは「制約を有効にしたか」だけ。 一貫性は勝手に湧いてくる性質ではなく、設計者が制約として宣言し、有効化して初めて手に入る、という良い実例です。
I:独立性 —— 「同時に走っても邪魔しない」
2つのトランザクションが同じ行を触ったとき、互いの途中経過が見えないようにする性質です。
ただし独立性は **「あるかないか」ではなく「どこまで効かせるかを選ぶダイヤル」**です。これがトランザクション分離レベル(READ COMMITTED、REPEATABLE READ など)で、性能と引き換えに緩められます。
分離レベルは本シリーズ第2回で図解します。
D:永続性 —— 「COMMITしたら消えない」
COMMIT が成功を返した時点で、その変更は電源断・プロセスクラッシュを越えて残ります。
実験してみます。COMMIT した行と、COMMIT せずにセッションを終了した行を比べます。
CREATE TABLE t (v TEXT);
BEGIN;
INSERT INTO t VALUES ('committed');
COMMIT;
BEGIN;
INSERT INTO t VALUES ('not committed');
-- ここでセッション終了(COMMITしていない)
別プロセスで開き直すと:
committed
COMMIT済みの1行だけが残りました。「COMMITが返ってきた」=「残る」の境界線が、そのまま永続性の定義です。
まとめ:1行で説明するなら
| 言えるようになりたい説明 | |
|---|---|
| Atomicity | 全部やるか、全部やらないか。ただしROLLBACKを呼ぶのはアプリの責任 |
| Consistency | 宣言した制約を破った状態でトランザクションを終わらせない。守る範囲は設計者が決める |
| Isolation | 同時実行しても互いの中間状態が見えない。強さは分離レベルで選ぶ |
| Durability | COMMITが成功を返したら、電源が落ちても残る |
Cを聞かれたら、こう答えれば詰まりません。
「トランザクションの前後で、DBが制約を満たした状態であることの保証です。ただしDBが守るのは
NOT NULLや外部キーのように宣言された制約だけで、『合計残高が変わらない』のような業務ルールはアプリ側の責任。だからCは4つの中で唯一、アプリと分担する性質なんです」
シリーズ予定
「分かったつもりで説明できない」用語を、以下の順で1本ずつ潰していきます。
| 回 | テーマ | 歯がゆさポイント |
|---|---|---|
| 1 | ACID(本記事) | Cだけ説明できない |
| 2 | トランザクション分離レベル | READ COMMITTEDで結局何が読める? |
| 3 | ロックとデッドロック | 誰が何をどこまでロックしている? |
| 4 | スキーマ | 文脈ごとに指すものが違う |
| 5 | 正規化 | 第3正規形まで、の「まで」って何? |
| 6 | インデックス | 貼ったのに効かないのはなぜ? |
| 7 | 実行計画 | EXPLAINの出力、どこを見る? |
| 8 | N+1問題 | なぜ「1回」で済まないのか |
| 9 | コネクションプール | プールサイズは何を基準に決める? |
| 10 | ビューとマテリアライズドビュー | どっちをいつ使う? |
| 11 | レプリケーションとラグ | 書いた直後に読めないのはなぜ? |
次回は 第2回:トランザクション分離レベル。本記事のIで触れた「ダイヤル」の中身を図解します。
参考
- SQLite - Distinctive Features: Transactional
- SQLite - SQLite Foreign Key Support
- SQLite - ON CONFLICT clause(既定の競合解決 ABORT の挙動)
- MySQL 8.4 リファレンスマニュアル - InnoDB and the ACID Model
- PostgreSQL ドキュメント - Transactions
- PostgreSQL ドキュメント - Constraints
@kotaro_ai_lab
AI活用や開発効率化について発信しています。フォローお気軽にどうぞ!