0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【DB用語の歯がゆさ 第1回】ACIDの「C(一貫性)」、説明できますか?

0
Posted at

株式会社Good Labでエンジニアをしている コータロー です。
日々、Java・SQL・Gitなどの技術情報や、新人エンジニア向けの学習ノウハウ、
AI活用についての情報を発信しています。

Good Labについて気になった方は、コーポレートサイトもぜひご覧ください。
コーポレートサイト

このシリーズについて

「知ってはいる。けど、人に説明しろと言われると詰まる」——そんな歯がゆいDB用語を、1記事1用語・図解中心・5分で読める分量で解消していくシリーズです。

第1回は ACID。特に C(一貫性) を狙い撃ちします。

まず自己診断

次の質問に、口に出して答えてみてください。

  1. ACIDの A は? →「原子性。全部やるか、全部やらないか」……たぶん言えます
  2. ACIDの D は? →「永続性。コミットしたら消えない」……これも言えます
  3. 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で触れた「ダイヤル」の中身を図解します。

参考


@kotaro_ai_lab
AI活用や開発効率化について発信しています。フォローお気軽にどうぞ!

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?