06. データベース理論 - ACID と BASE と CAP定理

読了時間: 50-70分(移動中に分割して読むと丁度よい) 想定読者: 「MySQL も MongoDB もなんとなく触ったことあるけど、何が違うのか言語化できない人」 ゴール: RDBMS と NoSQL の選び分け、ACID / BASE / CAP の3点セットを人に説明できる 手は動かさない: スマホ片手に読むだけでOK

この記事で持って帰れること

  • データベース1970年代からの進化を「なぜそうなったか」の物語として語れる
  • ACID 4要素を、銀行口座の振込の例で人に説明できる
  • トランザクション分離レベル4段階と、それぞれが防ぐ異常現象を語れる
  • MVCC が現代 DB の心臓部だと理解し、PostgreSQL の VACUUM トラブルを予期できる
  • NoSQL の4大カテゴリと CAP / PACELC を踏まえ、「とりあえず Mongo」の罠を回避できる

前回 (05_OSの基礎) との繋がり

前回は OS の役割 (プロセス、メモリ、ファイル) の話だった。データベースは結局のところ「OS が提供するファイルシステムの上に、賢い構造を被せたもの」。WAL は OS の fsync を、MVCC は OS のメモリ管理を、ロックは OS のシステムコールを使って動いている。今回はその「賢い構造」を覗き込む話。


大前提: なぜ「データベース理論」を読むのか

あなたが書いた Web アプリは、ユーザー情報をどこかに保存している。多くの場合 MySQL や PostgreSQL、もしくは MongoDB、Firestore あたり。SQL を叩けば動くし、ORM を使えば SQL すら書かない。それで「動く」。

なのに、なぜ「データベース理論」なんてものが必要なのか。

答えは、動いてるアプリはいつか必ず壊れるから

具体的にはこういう日が来る:

  • ユーザーが10万人を超えた途端、ページが重くなった
  • 銀行残高が「マイナス10円」になった、しかも処理は成功扱いだった
  • 2台のサーバーで同じユーザーIDが同時発行され、片方が消えた
  • バックアップから戻したら、最後の30分の注文が全部消えていた

これらは全部「データベース理論を知らなかった代償」。ACID とは何か、分離レベルとは何か、CAP 定理が言っていることは何か。これを知らずに「とりあえず MySQL」「とりあえず MongoDB」とやると、上のような事故が必ず起きる。

逆に言うと、データベース理論は「今後 30 年、技術が変わっても陳腐化しない知識」。フレームワークは流行り廃りがあるが、トランザクションの ACID は1970年代から本質が変わっていない。一度ちゃんと理解しておくと、新しい DB が出てきても「これは結局 CAP のどこを犠牲にしてるんだ?」で読み解ける。

この記事の楽しみ方

数式はほぼ出てこない。出てくる例は「銀行口座」「在庫」「掲示板」みたいな日常的な話。 一気に読まなくていい。電車で1セクション、待ち時間で1セクション、と分けて読めるサイズに切ってある。 「ふーん」で読み流していい。後でカリキュラム本編で MySQL や PostgreSQL を実際に触る時、「あ、あの記事で読んだやつだ」と思い出せれば勝ち。


第1章: データベースの歴史 - 「データを保存する」の進化

1-1. そもそも、データベースが無かった時代

最初期のコンピュータには「データベース」という概念は無かった。プログラムが直接ファイルを読み書きしていた。「顧客リスト.txt」みたいなテキストファイルを開いて、自分でパースして、書き換えて、保存する。

これで何が困るかというと:

  • 形式が固まらない: 「氏名,年齢,住所」の順で書いてある前提で全プログラムが書かれている。誰かが順番を変えると全部壊れる
  • 同時アクセスで壊れる: AさんとBさんが同時にファイルを開いて、別々の編集をして、両方保存すると後勝ちで片方の編集が消える
  • 検索が遅い: 「年齢が30以上の人」を探すには全行を上から読むしかない

つまり、「データの構造」と「同時アクセス制御」と「検索の高速化」、この3つを誰かがまとめて面倒見てくれる仕組みが要る。それがデータベース管理システム (DBMS) という発想。

1-2. 階層型データベース (1960年代)

最初の DBMS は IBM が1960年代に作った IMS (Information Management System)。これは 階層型 といって、データを木構造で表現する。

会社
├─ 部署A
│  ├─ 社員1
│  └─ 社員2
└─ 部署B
   ├─ 社員3
   └─ 社員4

ファイルツリーみたいな構造。これで「会社 → 部署 → 社員」と上から辿れる。アポロ計画のロケット部品管理にも使われた由緒正しい DB。

階層型の致命的な弱点

「社員から見て、所属する部署」は簡単に辿れる。でも「同じスキルを持つ社員を全社で横断検索」は超苦手。木構造は「上下関係」しか表現できないから、「横の関係」がからむと一気に詰む。

リレーション(関係)が複雑な現実世界とは合わなかった。

1-3. ネットワーク型データベース (1970年代前半)

次に出てきたのが ネットワーク型。CODASYL という業界団体が標準化した。これは「データ同士がポインタで繋がりまくる」モデル。

階層型の「木」が「グラフ」になったイメージ。横の繋がりも表現できるようになった。が、これも問題があった。

ネットワーク型のつらさ

プログラマがデータを取りに行く時、ポインタを自分で辿る必要があった

「Aさんの部署の、同じプロジェクトの、別のチームの、リーダーの名前」

こういう問い合わせを、ポインタを5段階手繰って書く。コードが超複雑。データ構造が変わるたびに全プログラムが壊れる。

階層型もネットワーク型も「データ構造とプログラムが密結合している」のが根本問題だった。これを綺麗に解決した人が現れる。

1-4. リレーショナルモデルの登場 - エドガー・コッド (1970)

1970年、IBM の研究員 エドガー・F・コッド (Edgar F. Codd) が論文 “A Relational Model of Data for Large Shared Data Banks” を発表する。

彼の主張はシンプル:

「データは 表 (テーブル) で表現せよ。表と表は 共通の値 (キー) で関係を作れ。プログラマはデータの物理配置を意識する必要がない」

これが リレーショナルモデル。今あなたが MySQL や PostgreSQL で使ってる SQL の根本。

社員テーブル              部署テーブル
+----+-------+--------+    +----+--------+
| id | name  | dep_id |    | id | name   |
+----+-------+--------+    +----+--------+
| 1  | 田中  | 10     |    | 10 | 開発   |
| 2  | 山田  | 20     |    | 20 | 営業   |
+----+-------+--------+    +----+--------+
        ↑                        ↑
        └──── dep_id で繋がる ───┘

社員テーブルの dep_id と部署テーブルの id が一致してたら、それが「所属関係」。ポインタじゃない、値そのもの。だから物理配置がどうなっていようと、SQL は同じように書ける。

コッドの何が天才だったか

彼が偉大なのは「数学的にきちんと定義した」点。関係代数 (relational algebra) という数学的な土台を作った。これがあるから、SQL のオプティマイザは「クエリを別の同等な形に書き換えても結果が同じ」と保証して、自動で高速化できる。

ちなみに彼は IBM 社内で長年認められず、後年やっと評価された。「営業しないと正しいことも認められない」というエンジニア教訓でもある。

1-5. 正規化はなぜ必要か

リレーショナルモデルとセットで出てきたのが 正規化 (normalization)。これは「テーブルを適切に分割して、データの重複と矛盾を防ぐ」設計手法。

例えば、こんな表があったとする:

+----+-------+------+--------+
| id | name  | 部署 | 部長   |
+----+-------+------+--------+
| 1  | 田中  | 開発 | 鈴木   |
| 2  | 山田  | 営業 | 佐藤   |
| 3  | 高橋  | 開発 | 鈴木   |
| 4  | 伊藤  | 開発 | 鈴木   |
+----+-------+------+--------+

開発部の部長が「鈴木」と何度も書かれている。何が問題か:

  1. 更新の手間: 部長が交代したら、開発部の全行を更新する必要がある
  2. 更新の矛盾: 一部だけ更新し忘れて「同じ開発部なのに部長が違う」状態が生まれる
  3. 削除の異常: 開発部の社員が全員辞めると、「開発部の部長は鈴木」という情報自体が消える

正規化の本質

「同じ情報を一箇所だけに書く」原則。重複を排除すれば、更新の矛盾が起きない。

上の例を正規化すると、社員テーブルと部署テーブルに分かれる:

社員: id, name, dep_id
部署: id, name, leader_id

開発部の部長が交代したら、部署テーブルの1行を更新するだけ。

正規化の段階: 第1正規形 → 第2正規形 → 第3正規形 → ボイス・コッド正規形 (BCNF) → 第4 → 第5。実務では「第3正規形まで一旦やる」が標準的。

正規化と非正規化のトレードオフ

正規化を進めすぎると、データ取得時に大量の JOIN が要る → 遅くなる。

なので実務では「普段は正規化、性能の必要な箇所だけ意図的に冗長化 (非正規化)」する。例えばランキング表示は SQL で集計し直さず、集計済みカラムを別途持つ、など。

「とりあえず JSON カラムに突っ込む」設計は、最初は楽だけど後で必ず後悔する。これは Day1 のアンチパターンとして覚えておくといい。

1-6. SQL の標準化と RDBMS の隆盛 (1980-2000)

1979年に Oracle が世界初の商用 RDBMS を出した。1989年に PostgreSQL の元になる Postgres が、1995年に MySQL が登場。1986年には SQL が ANSI で標準化される。

これでデータベースの世界はほぼ統一された。20年以上、ほとんどの Web サービスは「RDBMS + SQL」で作られた。MySQL や PostgreSQL が動いていない Web 会社を探す方が難しかった。

この時代の代表的な RDBMS

  • Oracle Database: エンタープライズ向けの王様。銀行・通信・大企業で使われる。ライセンス高額
  • MySQL: オープンソース、Web 向け。LAMP スタック (Linux + Apache + MySQL + PHP) の M
  • PostgreSQL: 「最も標準準拠で堅実な OSS RDBMS」。本記事の MVCC の話で再登場する
  • SQL Server: Microsoft 製、Windows サーバーと相性が良い
  • SQLite: 単一ファイルで動く軽量 RDBMS。スマホアプリ内、組み込み、テスト用途で世界一使われているデータベース

第2章: ACID - トランザクションの4つの約束

2-1. トランザクションとは何か

データベースで一番重要な概念。トランザクション (transaction) とは「いくつかの処理を、ひとまとまりとして扱う」仕組み。

定番の例: 銀行の振込。

1. A の口座から 1000円を引く
2. B の口座に 1000円を足す

これが2ステップに分かれている。もし1で停電が起きて、2が実行されないと、世界から1000円が消える。これを防ぐには「1と2は一緒に成功するか、一緒に失敗するか、どちらかしかありえない」という保証が要る。これがトランザクション。

BEGIN;
UPDATE accounts SET balance = balance - 1000 WHERE id = 'A';
UPDATE accounts SET balance = balance + 1000 WHERE id = 'B';
COMMIT;

BEGIN から COMMIT までが1つのトランザクション。途中で何があってもこのブロック単位で扱われる。

このトランザクションが守るべき性質を4つの頭文字で表現したのが ACID

↓ ACID 4要素の関係(トランザクションの4つの約束)↓

mindmap
  root((ACID))
    A: Atomicity<br/>原子性
      全部成功 or 全部失敗
      中途半端は許さない
      ROLLBACK で巻き戻し
    C: Consistency<br/>一貫性
      整合性制約を守る
      外部キー / CHECK
      アプリと DB の合作
    I: Isolation<br/>分離性
      同時実行でも干渉しない
      4段階の分離レベル
      ロック / MVCC で実現
    D: Durability<br/>永続性
      COMMIT したら消えない
      WAL で実現
      fsync で確実にディスクへ

2-2. A - Atomicity (原子性)

「ひとまとまりは、全部成功するか、全部失敗するか」

「原子」というのは「これ以上分割できない最小単位」という意味。トランザクションを原子のように分割不能なものとして扱う。

途中で停電・サーバークラッシュ・プログラムのバグで例外発生、どんな理由であれ、中途半端な状態を残さない。失敗したら全部 ROLLBACK されて、トランザクション開始前の状態に戻る。

Atomicity が無いとどうなるか(実害ベース)

実際に起きた事故: 2012年、Knight Capital 社のトレーディングシステムで、株式注文処理が「途中で止まる」事故が発生。45分間で4億6千万ドル (約500億円) の損失。会社は実質倒産。

原因は新旧コードの混在で、一部のトランザクション保証が崩れていた。Atomicity が崩れると、本気で会社が潰れる。

2-3. C - Consistency (一貫性)

「トランザクションの前後で、データのルールは守られる」

「データのルール」とは:

  • balance >= 0 (残高はマイナスにならない)
  • user_id が users テーブルに存在する (外部キー制約)
  • メールアドレスはユニーク

これらの制約を、トランザクション完了時には必ず満たしている。途中ではルール違反でも構わない (内部状態として)。終わった時に守られていればOK。

Consistency はちょっと曖昧

実は ACID の中で C だけは他の3つと毛色が違う。A, I, D は「DB がやってくれること」だが、C は「アプリ開発者と DB の合作」。

例えば「残高がマイナスになってはいけない」というルールは、CHECK 制約として書くこともできるし、アプリ側で if 文で守ることもできる。どちらにせよ「ルール違反の状態が残らない」よう保証するのが C。

2-4. I - Isolation (分離性)

「同時に走っているトランザクションは、お互いに干渉しない」

ここが一番奥が深い。次の章で深掘りする。

イメージとしては「自分のトランザクション中は、他人のトランザクションが無いように振る舞ってほしい」。1人で使っているかのように見せたい。

でも、複数人が同時に同じテーブルを触ったら、完全に分離するのは性能上きつい。どこまで分離するかで「分離レベル」が決まる。

2-5. D - Durability (永続性)

「COMMIT したら、その内容は二度と消えない」

COMMIT が成功した瞬間、その変更はディスクなど永続ストレージに書き込まれていて、サーバーが停電で落ちても、再起動後に必ず復元される。

Durability の裏側 - WAL (Write-Ahead Logging)

永続性を実現する技術が WAL (Write-Ahead Log) = 先行書き込みログ。

やってることは:

  1. データを変更する前に、「これからこういう変更をする」というログをディスクに書く
  2. ログがディスクに書けたら、メモリ上のデータを変更
  3. メモリ上のデータは後でゆっくりディスクに反映

もし途中で電源が落ちても、ログさえ残っていれば「やり直し」または「やり直しの取り消し」ができる。PostgreSQL も MySQL InnoDB も、ファイルシステム (ext4, ZFS) も、全部 WAL を使っている。

ちなみに fsync() というシステムコールが「本当にディスクに書いた?」を OS に強制確認するもの。これを呼ばないと、OS のページキャッシュに溜まっていて、停電で消える。多くの DB の COMMIT は内部で fsync を呼んでいる。

Durability を破ったアプリの末路

性能を上げたいからと、fsync を無効化したり、innodb_flush_log_at_trx_commit = 0 のような設定にしたまま本番運用 → サーバー再起動で直近1秒の取引が消える。

「COMMIT 返したけど消えてた」は、最悪のバグ。ユーザーには成功と伝えたのに、データが無い。これを Phantom Commit と呼ぶ。


第3章: 分離レベル - Isolation の奥深い世界

ACID の I (Isolation) を本気で扱う章。ここを知ってるかどうかで「DB 触れる人」と「DB 設計できる人」の差が出る。

3-1. なぜ分離レベルが必要か

複数のトランザクションが同時に走ると、いろんな「困った現象」が起きる。

具体的な困りごと:

  • Dirty Read (ダーティリード): まだコミットされてない他人の変更を読んでしまう
  • Non-Repeatable Read (反復不能読み取り): 同じ行を2回読んだら、値が変わっていた
  • Phantom Read (ファントムリード): 同じ条件で2回検索したら、行の数が違った

これらを防ぐには、トランザクション同士を「分離」する必要がある。でも、完全に分離すると性能が落ちる。

そこで、SQL 標準は 4段階の分離レベル を定義している。

レベルDirty ReadNon-Repeatable ReadPhantom Read
READ UNCOMMITTED起きる起きる起きる
READ COMMITTED防ぐ起きる起きる
REPEATABLE READ防ぐ防ぐ起きる
SERIALIZABLE防ぐ防ぐ防ぐ

上から下に行くほど厳密、でも遅い。デフォルトは DBMS によって違う:

  • PostgreSQL: READ COMMITTED
  • MySQL InnoDB: REPEATABLE READ
  • SQL Server: READ COMMITTED
  • Oracle: READ COMMITTED

↓ トランザクション分離レベル4段階の関係(防げる現象の包含関係)↓

classDiagram
    class READ_UNCOMMITTED {
        +Dirty Read を許す
        +Non-Repeatable を許す
        +Phantom を許す
        +最も緩い / 最速
    }
    class READ_COMMITTED {
        +Dirty Read を防ぐ
        +Non-Repeatable を許す
        +Phantom を許す
        +PG/Oracle/SQLServer デフォルト
    }
    class REPEATABLE_READ {
        +Dirty Read を防ぐ
        +Non-Repeatable を防ぐ
        +Phantom を許す (標準)
        +MySQL InnoDB デフォルト
    }
    class SERIALIZABLE {
        +全ての異常を防ぐ
        +直列実行と等価
        +最も厳密 / 最遅
    }
    READ_UNCOMMITTED <|-- READ_COMMITTED : より厳密
    READ_COMMITTED <|-- REPEATABLE_READ : より厳密
    REPEATABLE_READ <|-- SERIALIZABLE : より厳密

3-2. Dirty Read を例で理解する

シナリオ: A さんが B さんに 1000円振込中。途中で C さんが残高を見る。

時刻 トランザクション1 (振込)            トランザクション2 (残高確認)
T1   BEGIN
T2   UPDATE A.balance -= 1000
T3                                       SELECT balance FROM A
T4                                       → READ UNCOMMITTED なら、まだ COMMIT してない値が読める
T5   ROLLBACK (停電など)

T4 で C さんは「A の残高は減った」と認識した。でも T5 で実際にはロールバックされる。Cさんが見た値は「幻」。これが Dirty Read

Dirty Read を許す READ UNCOMMITTED の使い所

ほぼ無い。「とにかく最新を読みたい、整合性は気にしない」レポーティング用途くらい。実務でこの分離レベルを使うことはほぼ無い。

「使ってる」と思って実は使ってない、というケースが多い。MySQL や PostgreSQL は READ UNCOMMITTED を指定しても実質 READ COMMITTED 相当に動くこともある。

3-3. Non-Repeatable Read を例で理解する

シナリオ: 月次レポートを集計中、他のユーザーが値を更新する

時刻 トランザクション1 (集計)             トランザクション2 (更新)
T1   BEGIN
T2   SELECT sum(price) FROM sales        → 100,000
T3                                        UPDATE sales SET price=...
T4                                        COMMIT
T5   SELECT sum(price) FROM sales        → 105,000 ← 違う!
T6   COMMIT

同じトランザクションの中で同じクエリを2回投げたのに、結果が違う。これが Non-Repeatable Read (反復不能読み取り)。

これを防ぐのが REPEATABLE READ。トランザクション開始時のスナップショットを保持して、同じトランザクション中は「あの時点」のデータを読み続ける。

3-4. Phantom Read を例で理解する

Non-Repeatable Read は「既存の行の値が変わる」現象。それに対して Phantom Read は「新しい行が追加 or 削除されて、検索結果の行数が変わる」現象。

時刻 トランザクション1                    トランザクション2
T1   BEGIN
T2   SELECT count(*) FROM orders         → 100
     WHERE status='pending'
T3                                        INSERT INTO orders ... status='pending'
T4                                        COMMIT
T5   SELECT count(*) FROM orders         → 101 ← 1行増えた!
     WHERE status='pending'
T6   COMMIT

同じ条件で count したのに数字が違う。これが Phantom Read (幻影読み取り)。

これを防ぐのが SERIALIZABLE。事実上「トランザクションを直列実行する」と同じ結果を保証する最強モード。が、当然性能は一番悪い。

MySQL InnoDB の REPEATABLE READ は実は Phantom も防ぐ

標準的には REPEATABLE READ は Phantom Read を許す。でも MySQL InnoDB は ギャップロック という独自機構で、REPEATABLE READ でも Phantom も防いでいる。なので MySQL の REPEATABLE READ は事実上 SERIALIZABLE に近い。

このため、PostgreSQL と MySQL で「REPEATABLE READ」の意味が微妙に違う。DBMS を乗り換える時の落とし穴。

分離レベルを上げれば良い、というものではない

強い分離レベルは「ロック」や「再試行」のコストが大きい。

  • READ COMMITTED: 普通の業務システム、Web アプリの大多数はこれで十分
  • REPEATABLE READ: 一連の集計を整合性とって取りたい時。MySQL のデフォルト
  • SERIALIZABLE: 金融、在庫の厳密管理など、絶対に矛盾が許されない処理

全部 SERIALIZABLE にすると、複数トランザクションが衝突して大量の Serialization Failure (再実行要求) が起きる。性能はガタ落ち。

3-5. 異常系の追加メンバー: Lost Update, Write Skew

SQL 標準の3つの異常 (Dirty / Non-Repeatable / Phantom) 以外にも、実務で気をつけるべき現象がある。

Lost Update (失われた更新):

時刻 トランザクション1                    トランザクション2
T1   read x = 100
T2                                        read x = 100
T3   x = x + 10
T4                                        x = x + 20
T5   write x = 110
T6                                        write x = 120 ← T1 の更新が消える

両方とも「+10」「+20」を意図していたが、結果は 130 ではなく 120 になっている。T1 の +10 が消えた。

Lost Update を防ぐ常套手段

  1. SELECT … FOR UPDATE で行ロックを取る:
BEGIN;
SELECT x FROM t WHERE id=1 FOR UPDATE;
-- ロック中、他のトランザクションは待つ
UPDATE t SET x = x + 10 WHERE id=1;
COMMIT;
  1. アトミックな更新 にする:
UPDATE t SET x = x + 10 WHERE id=1;

SELECT してから計算して UPDATE、ではなく、最初から「+10」を SQL に書く。

  1. 楽観的ロック: バージョン番号や updated_at を付けて、書き込み時に「読んだ時と同じか」を確認 (CAS 操作)

Web アプリのアンチパターンとして、フォームで「現在の在庫数を画面表示 → ユーザーが減らす → サーバーが UPDATE」をやると、画面を2タブ開いた瞬間に Lost Update が起きる。


第4章: ロック・デッドロック・MVCC

4-1. ロックの2大分類

DB が分離性を実現する基本道具が ロック (Lock)

  • 共有ロック (Shared Lock, S Lock): 読み取り用。複数同時にOK
  • 排他ロック (Exclusive Lock, X Lock): 書き込み用。1つだけ
T1:  read → S
T2:  read → S   ← 両方OK
T3:  write → X  ← T1, T2 のロックが解けるまで待つ

ロックを取る対象は「行単位」「ページ単位」「テーブル単位」と粒度がある。細かいほど並行性は上がるが、ロック管理のオーバーヘッドが増える。

4-2. デッドロック - 互いに待ち続ける

時刻 T1                T2
1    Lock(A)
2                      Lock(B)
3    Lock(B) で待機
4                      Lock(A) で待機
                       ↑↓ 永遠に解けない

T1 は A を持ったまま B を待ち、T2 は B を持ったまま A を待つ。これが デッドロック

DB は「サイクル」を検知して、片方のトランザクションを 強制ロールバック する。アプリ側にはエラーが返る。アプリは適切に再試行する必要がある。

↓ ロックとデッドロック(2 トランザクションの相互ブロック)↓

sequenceDiagram
    participant T1 as トランザクション1
    participant A as 行 A (ロック資源)
    participant B as 行 B (ロック資源)
    participant T2 as トランザクション2
    T1->>A: Lock(A) 取得 OK
    T2->>B: Lock(B) 取得 OK
    T1->>B: Lock(B) 要求
    Note over T1,B: T2 が解放するまで待機
    T2->>A: Lock(A) 要求
    Note over T2,A: T1 が解放するまで待機
    Note over T1,T2: 互いに待ち続ける = デッドロック
    Note over T1,T2: DB がサイクルを検知
    Note over T2: T2 を強制 ROLLBACK
    T2-->>T2: エラー: deadlock detected
    T1->>B: Lock(B) 取得 OK (B が解放された)
    T1->>T1: COMMIT 完了

デッドロックを誘発するアンチパターン

-- ユーザーA: 注文1 → 注文2 の順で更新
UPDATE orders SET ... WHERE id=1;
UPDATE orders SET ... WHERE id=2;
 
-- ユーザーB: 注文2 → 注文1 の順で更新 (逆順)
UPDATE orders SET ... WHERE id=2;
UPDATE orders SET ... WHERE id=1;

同じ行に対して、トランザクションごとにアクセス順序が違うとデッドロックが起きる。

対策: 同じ順序で行を触る (例えば常に ID 昇順)。これだけでデッドロックは劇的に減る。

楽観的ロック vs 悲観的ロック

  • 悲観的ロック (Pessimistic Lock): 「衝突するに違いない」と思って先にロックを取る。SELECT ... FOR UPDATE の世界
  • 楽観的ロック (Optimistic Lock): 「衝突しないだろう」と思って、書き込み時に「読んだ時と変わってないか」確認。バージョン番号や updated_at を使う

一般的に:

  • 競合が多い (例: 在庫1個を100人が奪い合う) → 悲観的ロック
  • 競合が少ない (例: 各ユーザーが自分のプロフィールを編集) → 楽観的ロック

Rails の ActiveRecord は両方サポートしている。

4-3. MVCC - ロックを使わずに並行制御

ここがモダンな DB の心臓部。

MVCC (Multi-Version Concurrency Control) = 「複数バージョン同時実行制御」。

考え方はシンプル: データを上書きせず、新しいバージョンとして追記する。読む側は「自分のトランザクション開始時点のバージョン」を読む。書く側は「新しいバージョン」を作る。

時刻 トランザクション1 (読み)         トランザクション2 (書き)
T1   BEGIN  (この時点のスナップショット取得)
T2   SELECT x → 100                   UPDATE x = 200
T3                                    COMMIT
T4   SELECT x → 100 ← まだスナップショット時点の値
T5   COMMIT

T1 開始時点では x = 100。T2 で T2 が更新するが、T1 が見るのは「自分の開始時点」の値なので 100 のまま。これで Non-Repeatable Read が防げる。読み取りはロックを取らないので、書き込みと衝突しない。

↓ MVCC の仕組み(複数バージョンの管理)↓

flowchart TB
    subgraph V1["バージョン1 (古い)"]
        D1["x = 100<br/>created_by: T0<br/>deleted_by: T2"]
    end
    subgraph V2["バージョン2 (新しい)"]
        D2["x = 200<br/>created_by: T2<br/>deleted_by: -"]
    end
    subgraph V3["バージョン3 (最新)"]
        D3["x = 300<br/>created_by: T5<br/>deleted_by: -"]
    end
    subgraph Readers["読み取り側 (スナップショット時点)"]
        R1["T1 (T0 時点)<br/>→ V1 を読む"]
        R3["T3 (T2 時点)<br/>→ V2 を読む"]
        R6["T6 (T5 時点)<br/>→ V3 を読む"]
    end
    subgraph Cleanup["古いバージョンの掃除"]
        VAC["VACUUM (PG)<br/>Undo Log (MySQL)"]
    end
    R1 -->|ロック取らず読む| V1
    R3 -->|ロック取らず読む| V2
    R6 -->|ロック取らず読む| V3
    V1 -. もう参照されない .-> VAC

MVCC のメリットとデメリット

メリット:

  • 読み取りはロックを取らないので、並行性が高い
  • スナップショット分離が自然に実現できる
  • 長時間の集計クエリも、後ろから更新が来ても安心

デメリット:

  • 古いバージョンの行が DB 内に残る → 定期的に掃除 (VACUUM) が必要
  • ストレージを余分に使う
  • 「最新の値」を読むには明示的に lock を取らないといけない場面がある

PostgreSQL と MySQL InnoDB の MVCC

  • PostgreSQL: 完全に MVCC。古いタプル (行のバージョン) を残し、VACUUM で掃除する。VACUUM が遅れると Bloating (テーブル肥大化) を起こす
  • MySQL InnoDB: MVCC + Undo Log。古いバージョンは Undo ログから再構築する。WAL とは別に Undo ログを持つ

どちらも「読み取りはロックフリー、書き込みは行ロック」が基本。これが現代の高速 DB の根幹。

VACUUM を放置すると本番が燃える

PostgreSQL の VACUUM は、長く動いているトランザクションがあると進まない。サービスの裏で長時間 SELECT を打ち続けるレポート系の処理が動いていると、いつまでも古いバージョンが消えず、テーブルが2倍3倍に膨れ上がる。

「サービスは動いてるけど、なぜか毎日重くなっていく」という症状は、9割 VACUUM 関連。pg_stat_user_tablesn_dead_tup を監視するのが基本。

これは MySQL章 Lv5: MVCC 内部実装 でも詳しく扱う想定。


第5章: NoSQL の登場 - なぜ RDBMS だけじゃダメだったか

5-1. 2000年代後半、Web 規模が変わった

20世紀末まで、データベースは「1台の高性能サーバー」で動かすのが普通だった。Oracle や DB2 のような商用 RDBMS は「スケールアップ」、つまり「より高い CPU、より大きいメモリ、より速いディスク」で処理を捌くことを前提にしていた。

ところが2000年代半ば、Google・Amazon・Facebook が直面した問題は、これでは解けなかった。

  • Google: 全インターネットのページをインデックスする (数百億ページ)
  • Amazon: ブラックフライデーで一瞬に10万人が買い物カートに商品を入れる
  • Facebook: 10億人のユーザーのタイムラインを毎秒生成する

1台のスーパーコンピュータでは絶対に無理。何千台もの普通のサーバーで分散処理する しかない。これが「スケールアウト」。

ところが、RDBMS をスケールアウトするのは死ぬほど難しい。理由は ACID。特に Consistency と Isolation を「複数台」で守ろうとすると、ネットワーク越しの調整 (分散トランザクション) が要る。これがクソ遅い。

そこで彼らは考えた。「ACID を緩めれば、もっとシンプルに分散できるんじゃね?」

これが NoSQL の出発点。

5-2. NoSQL の4大カテゴリ

NoSQL は「SQL じゃない」というだけのザックリしたカテゴリ。中身は大きく4種類:

タイプ代表特徴
Key-ValueRedis, DynamoDB, Memcachedキーから値を引くだけ。超高速
ドキュメントMongoDB, CouchbaseJSON ドキュメント単位で保存
カラムファミリCassandra, HBase列を集めて保存。書き込み高速
グラフNeo4j, Amazon Neptuneノードとエッジで関係を表現

それぞれ向き不向きが激しい。「NoSQL = MongoDB」と思いがちだが、目的が違うと適切な選択も違う。

それぞれのユースケース

  • Redis: セッション保存、キャッシュ、リアルタイムランキング (Sorted Set)、Pub/Sub。「メモリで高速にキーから値を引く」が必要な全シーン
  • DynamoDB: AWS のフルマネージド KVS。スケールアウト前提のサーバーレス Web の定番
  • MongoDB: スキーマが頻繁に変わるアプリ、JSON 形式が自然に合うログ・イベント系
  • Cassandra: 大量の書き込み (IoT センサーデータ、メッセージング)、地理分散
  • Neo4j: SNS の人間関係、推薦エンジン、不正検知

5-3. NoSQL を「使うべき理由」と「使うべきでない理由」

NoSQL を選ぶ正当な理由

  1. スケール要求が極端 - 数億 QPS、PB 級データ
  2. データ構造が固まらない - スキーマレスで開発スピード重視
  3. 特殊なアクセスパターン - グラフトラバース、地理空間検索
  4. キャッシュ・セッション用途 - Redis のような高速 KVS で良い

NoSQL を選んで失敗するパターン

  • 「JOIN しないから速いはず」 で MongoDB を選ぶ → アプリ側で JOIN 相当の処理を書く羽目になり、結局遅い
  • 「スキーマレスだから自由」 で MongoDB → 数年後、ドキュメントの形式がバラバラで誰も触れなくなる
  • 「RDBMS は時代遅れ」 という思い込みで NoSQL → トランザクションが必要な業務処理で壊滅

教訓: 「あなたの要件」が「ACID を捨てていい性質」を持っているかどうか。これが NoSQL 選定の唯一の判断軸。多くの Web アプリは ACID が必要な業務処理が中心で、NoSQL の出番は限定的。


第6章: BASE と CAP 定理 - 分散時代の3点セット

6-1. BASE - ACID の対立概念

NoSQL の世界では、ACID の代わりに BASE という性質が語られる。

  • Basically Available: 基本的に使える (一部のノードが落ちてもサービス継続)
  • Soft state: 状態は時間とともに変わる可能性がある
  • Eventually consistent: 最終的には整合性がとれる

ACID が「今、絶対に正しい」を保証するのに対し、BASE は「最終的に正しくなる、それまでは多少ズレてもいい」という姿勢。

BASE の本質

例えば、Amazon の在庫表示。商品ページを見て「残り3個」と書いてあったが、買おうとしたら「在庫切れ」になることがある。これは BASE 的な動き。

各データセンターのキャッシュが完全に同期されてなくて、「ある場所では3個と表示、別の場所では0個」みたいな状態が一時的に発生している。最終的には全部0個に揃うが、それまでは多少ズレる。

これを「結果整合性 (Eventual Consistency)」と呼ぶ。ユーザー体験上、「絶対同じ瞬間に同じ値」を保証するより、「99.99%の可用性」が大事なケース。

↓ BASE と Eventually Consistent の流れ(書き込み伝播の様子)↓

sequenceDiagram
    participant User as ユーザー
    participant N1 as ノード1 (受付)
    participant N2 as ノード2 (レプリカ)
    participant N3 as ノード3 (レプリカ)
    participant Other as 別ユーザー
    User->>N1: 書き込み (x=200)
    N1->>N1: ローカルに反映
    N1-->>User: OK 即返却 (Basically Available)
    Note over N1,N3: 非同期で伝播 (Soft state)
    N1->>N2: レプリケーション (x=200)
    Other->>N3: 読み取り (x=?)
    N3-->>Other: x=100 を返す<br/>(まだ古い値 / 一時的なズレ)
    N1->>N3: レプリケーション (x=200)
    N3->>N3: 反映完了
    Other->>N3: 再度読み取り
    N3-->>Other: x=200 (Eventually Consistent)
    Note over User,Other: 最終的に全ノードで整合

BASE = 雑、ではない

BASE は「整合性を捨てる」のではなく、「いつ整合するかを緩める」設計。

銀行の振込で BASE をやったら大事故。BASE が許されるのは「少しのズレが許容される」用途のみ。

SNS のフォロワー数、いいね数、商品の在庫表示の「だいたい正しい」が許される世界に向く。

6-2. CAP 定理 - 分散システムの3つ巴

2000年、UC Berkeley の エリック・ブリュワー (Eric Brewer) が提唱した定理。

分散システムでは、以下の3つを同時に満たすことはできない:

  • C (Consistency): すべてのノードが、同じ時刻に同じデータを見る
  • A (Availability): 一部のノードが落ちても、リクエストには応答する
  • P (Partition tolerance): ネットワーク分断 (一部のノード間で通信不能) が起きても動く

これを CAP 定理 と呼ぶ。

なぜ3つ同時は無理かというと、「ネットワーク分断 (P) が必ず起きる現実」では、「整合性 (C)」と「可用性 (A)」のどちらかを諦めるしかないから。

ネットワーク分断が起きた時:
- 「全ノードが同じ値を返す」(C) を守るなら → 分断側のノードはエラーを返す (A を捨てる)
- 「常に応答する」(A) を守るなら → 古い値を返す可能性がある (C を捨てる)

P は「現実世界では避けられない」ので、実質的には C と A のどちらを取るか という選択になる。

↓ CAP 定理の三角関係(CP / AP / CA の選択)↓

flowchart TB
    CAP{"CAP 定理<br/>3つ同時は不可能"}
    C["C: Consistency<br/>全ノードが同じ値"]
    A["A: Availability<br/>常に応答する"]
    P["P: Partition tolerance<br/>分断に耐える"]
    CAP --- C
    CAP --- A
    CAP --- P
    CP["CP 系<br/>(A を捨てる)"]
    AP["AP 系<br/>(C を捨てる)"]
    CA["CA 系<br/>(P を捨てる = 分散しない)"]
    C -.選択.-> CP
    P -.選択.-> CP
    A -.選択.-> AP
    P -.選択.-> AP
    C -.選択.-> CA
    A -.選択.-> CA
    CP --> CP_EX["MongoDB(旧) / HBase<br/>ZooKeeper / etcd"]
    AP --> AP_EX["Cassandra / DynamoDB<br/>CouchDB"]
    CA --> CA_EX["1台 RDBMS<br/>(分散システムでは非現実的)"]

6-3. CAP の3つの分類

分類何を捨てる
CPA (可用性) を捨てるMongoDB (旧), HBase, ZooKeeper, etcd
APC (整合性) を捨てるCassandra, DynamoDB (デフォルト), CouchDB
CAP (分断耐性) を捨てる1台で動く RDBMS (分散しない)

CA は実質「分散しない」と同義

CA は「ネットワーク分断は起きない前提」だから、分散システムとして成立しない。「同一データセンター内、超高品質ネットワーク」みたいな特殊環境でしか想定できない。

一般的な分散システム議論では「CP か AP か」の二択。

CAP は誤解されやすい

CAP は「常に C, A, P から2つを選ぶ」のではない。「分断が起きた時に、C か A のどちらかを諦めることになる」 が正確。

分断が起きていない通常時は、両方守れる。「分断時にどちらを優先するか」を設計時に決める必要がある。

6-4. PACELC - CAP の拡張

CAP は「分断時の挙動」しか議論していない。実際は分断が起きてない通常時にも「整合性 vs 遅延」のトレードオフがある。

これを補強したのが PACELC (Daniel Abadi, 2010):

  • P (Partition) の時: A (可用性) と C (整合性) のどちらを取るか
  • E (Else) 通常時: L (遅延を低く保つ) と C (整合性を高く保つ) のどちらを取るか
システムPACELC
Dynamo / CassandraAPEL (低遅延優先)
MongoDB (旧)CPEC (整合性優先)
Google SpannerCPEC (整合性優先、ただし遅延あり)
Amazon DynamoDBAPEL (低遅延優先)

PACELC の方が現実に即している。「分断時はどうか、通常時はどうか」を独立に設計できる。

6-5. 結果整合性の実例 - DynamoDB と Cassandra

Amazon DynamoDB は、Amazon が自社の年末セール向けに作った内部システム (Dynamo) を製品化したもの。論文 “Dynamo: Amazon’s Highly Available Key-value Store” (2007) は分散システム研究の必読論文。

DynamoDB は「書き込みは絶対に止めない」設計。複数のレプリカに書き込むが、過半数が成功したら OK を返す。読み取り時、レプリカごとに違う値が返ってきたら、最新と思しき値を選ぶ (Last-Write-Wins などのルール)。

DynamoDB の「Eventually Consistent Read」

DynamoDB は読み取り API に2種類ある:

  • Eventually Consistent Read (デフォルト): 安い、速い、でも古い値を返すことがある
  • Strongly Consistent Read: 倍のコスト、レプリカ間で同期取ってから返す

多くの場合、デフォルトの結果整合性で十分。「常に最新」が要る箇所だけ Strongly を指定する。

Apache Cassandra は Facebook が作って OSS 化した。チャットメッセージ、Netflix の視聴履歴、Instagram のフィードなど、超大規模書き込みワークロードで採用されている。

Cassandra のキャッチコピーは「Always-on」。1ノードや1データセンターが死んでも止まらない。整合性はチューニング可能で、CONSISTENCY レベルを ONE (1ノード書ければOK) から ALL (全レプリカ書けるまで待つ) まで選べる。

結果整合性の心構え

結果整合性のシステムを使うアプリは、「古い値が返ってくることがある」前提で設計する必要がある。

「いいねボタンを押した瞬間、表示が反映されてない」「フォロワー数が一瞬戻った」みたいな現象は、結果整合性の現れ。ユーザーは多くの場合許容するが、「残高表示」「注文確定」など重要な操作には絶対に使えない。


第7章: インデックスの仕組み - なぜ全カラムに張らないのか

7-1. インデックスとは

DB の検索を高速化する仕組み。本の巻末の索引と同じ発想。「特定の値を持つ行はどこ?」を、O(N) ではなく O(log N) で見つけられる。

-- インデックスがない場合
SELECT * FROM users WHERE email = 'foo@example.com';
-- → 全ユーザーを上から読む (Full Table Scan)
 
-- インデックスがある場合
CREATE INDEX idx_users_email ON users(email);
SELECT * FROM users WHERE email = 'foo@example.com';
-- → email の B-tree を辿って一発でヒット

100万行のテーブルなら、Full Scan が数百ms かかるところ、B-tree インデックスなら数ms。

7-2. B-tree インデックス - 王道

ほとんどの RDBMS のデフォルトインデックスは B-tree (実装的には B+tree)。これは「バランス取れた木」で、各ノードに複数のキーが入る。

                [50]
               /    \
          [20,40]  [70,90]
          / | \    / | \
        ... ... ... 葉のレベルにデータ

B-tree の特徴:

  • どの値を探しても深さがほぼ同じ (バランス木)
  • 範囲検索が高速 (WHERE age BETWEEN 20 AND 30)
  • ソート済みなので ORDER BY も楽

↓ B-tree インデックスの構造(実際は B+tree で葉同士が連結)↓

graph TD
    Root["[50]<br/>ルートノード"]
    N1["[20, 40]<br/>内部ノード"]
    N2["[70, 90]<br/>内部ノード"]
    L1["[10, 15]<br/>葉 → 行ポインタ"]
    L2["[25, 30, 35]<br/>葉 → 行ポインタ"]
    L3["[42, 45]<br/>葉 → 行ポインタ"]
    L4["[55, 60, 65]<br/>葉 → 行ポインタ"]
    L5["[75, 80]<br/>葉 → 行ポインタ"]
    L6["[92, 95, 99]<br/>葉 → 行ポインタ"]
    Root -->|< 50| N1
    Root -->|>= 50| N2
    N1 -->|< 20| L1
    N1 -->|20-40| L2
    N1 -->|>= 40| L3
    N2 -->|< 70| L4
    N2 -->|70-90| L5
    N2 -->|>= 90| L6
    L1 -. 葉同士は連結 .-> L2
    L2 -. 範囲スキャン用 .-> L3
    L3 -. 葉同士は連結 .-> L4
    L4 -. 範囲スキャン用 .-> L5
    L5 -. 葉同士は連結 .-> L6

B+tree と B-tree の違い (補足)

厳密には DB が使うのは B-tree ではなく B+tree。B+tree は「葉ノードだけにデータがあり、葉同士がリンクで繋がっている」構造。これにより範囲検索が爆速になる。

多くの記事や教科書では「B-tree インデックス」と表記するが、実装は B+tree。マニアな人向けの豆知識。

7-3. ハッシュインデックス

ハッシュ関数で値からアドレスを計算する。等価検索 (=) には B-tree より速いが、範囲検索 (> < BETWEEN) ができない。

PostgreSQL や MySQL InnoDB では使えるが、用途が限定的。Redis のような KVS の内部でよく使われている。

7-4. なぜ「全カラムに張る」のはダメか

インデックスは魔法ではない。コストがある。

全カラムにインデックスを張った時の災い

  1. 書き込みが激遅になる: INSERT / UPDATE / DELETE のたびに、関連する全インデックスを更新する必要がある。インデックスが10本あれば、書き込みコストは10倍以上
  2. ストレージを食う: インデックスは別途容量を取る。テーブル本体より大きくなることも珍しくない
  3. オプティマイザが混乱する: インデックスが多すぎると「どれを使うか」の判断にミスが出る。意図しないインデックスが使われて遅くなる

インデックスは「よく使うクエリのパターン」に絞って張る。「念のため」は本当に大体間違い。

インデックスを張るべき場面

  • WHERE 句で頻繁に絞り込むカラム: WHERE user_id = ?user_id
  • JOIN のキー: 外部キーは大体 INDEX 必要
  • ORDER BY や GROUP BY のキー
  • ユニーク制約: 自動的にインデックスが張られる (PRIMARY KEY, UNIQUE)

一方で、カーディナリティが低い (例: gender のように2-3種類しか値が無いカラム) は B-tree インデックスを張っても効果が薄い。

7-5. 複合インデックスと「左端一致」

複数カラムを組み合わせたインデックスを 複合インデックス (composite index) と呼ぶ。

CREATE INDEX idx_users_company_dept ON users(company_id, dept_id);

これは内部的に「company_id で並べた中で、さらに dept_id で並べた木」になる。重要なのは 左端一致の原則:

  • WHERE company_id = 1 → 効く
  • WHERE company_id = 1 AND dept_id = 5 → 効く
  • WHERE dept_id = 5効かない (左端 company_id を指定してないので、木を辿れない)

複合インデックスの落とし穴

「とりあえずカラム3つで複合インデックス張った、片方しか WHERE 書いてないけど大丈夫やろ」 → ダメ。左端から指定が要る。

インデックスを張る順序は WHERE で絞り込みが強いカラムを左に が基本。

詳細は MySQL章 Lv3: インデックス設計 でガッツリやる予定。


第8章: 結局、RDBMS と NoSQL どっち選ぶ?(2026年の判断軸)

8-1. デフォルトは RDBMS でいい

身も蓋もない結論だが、「迷ったら RDBMS (PostgreSQL or MySQL) を選ぶ」 が正解。

理由:

  1. 大体のアプリで ACID は欲しい: 業務処理、課金、ユーザー管理、ほぼ全部が ACID 要件
  2. JOIN が便利: 正規化されたデータを柔軟に組み合わせて取れる
  3. SQL が共通言語: チームメンバーが理解できる、ツール群が充実 (BI, 監視, バックアップ)
  4. スケール限界が想像より遠い: 1台の PostgreSQL でも、適切に設計すれば月間1000万 PV くらいは捌ける

Web アプリの95%は、PostgreSQL 1台で間に合う

よくある誤解: 「サービスが流行ったら DB がボトルネックになるから、最初から MongoDB にしておこう」

現実: ボトルネックになるレベルまでサービスが伸びる確率自体が低い。仮にそうなっても、リードレプリカやキャッシュ層を足すなどの正攻法で延命できる。最初から NoSQL で複雑性を背負うコストの方が圧倒的に大きい。

8-2. NoSQL を選ぶべき具体的な場面

NoSQL を選ぶ正当な動機

  • Redis: キャッシュ・セッション・リアルタイムランキング・Pub/Sub。これは RDBMS の代替ではなく補完
  • DynamoDB / Firestore: AWS / GCP のサーバーレス構成と相性が良い。アクセスパターンが完全に固定で、JOIN が要らない場合
  • Elasticsearch / OpenSearch: 全文検索、ログ集約、可視化
  • Cassandra: 数万 TPS の書き込みが要る、地理分散が要る
  • Neo4j: 関係性そのものを扱う (SNS, 推薦, 不正検知)
  • MongoDB: スキーマが頻繁に変わるドキュメント型データ、ログイベント、CMS 系

ポイントは「RDBMS を主とし、必要な箇所だけ NoSQL を併用する」が現代の主流。1つだけで完結しない。これを Polyglot Persistence (多言語永続化) と呼ぶ。

よくある実構成

  • メインの業務 DB: PostgreSQL
  • キャッシュ・セッション: Redis
  • 全文検索: Elasticsearch
  • ログ集約: CloudWatch Logs / Datadog
  • ファイル: S3

「1個の DB で全部やる」より、得意分野ごとに分ける方が、結果的に運用が楽。

8-3. NewSQL という新潮流

2010年代後半から、「RDBMS の便利さ (ACID, SQL) + NoSQL のスケール性」を両立しようとする NewSQL が登場している。

製品提供特徴
Google SpannerGoogleグローバル分散、ACID, SQL。原子時計で時刻同期
Amazon AuroraAWSMySQL / PostgreSQL 互換でスケーラブル
CockroachDBCockroach LabsSpanner クローン的、OSS
TiDBPingCAPMySQL 互換でスケールアウト可能

これらは「分散しても ACID を諦めない」設計。CAP 定理を真っ向から覆そうとしている。実際は「多少の遅延を犠牲にして整合性を取る」(PACELC で言う EC) なので、CAP 定理自体は破れていない。

2026年時点での現実

NewSQL は素晴らしいが、運用コスト・複雑性は依然として高い。「自社で本当に必要か」を冷静に判断する必要がある。Spanner 級のスケールが要るのは、世界でも数百社くらい。

多くの場合、PostgreSQL + リードレプリカ + Redis で十分。


まとめ: あなたが今後 DB を見る時に思い出してほしいこと

この記事のエッセンス

  1. データベースは「データ構造 + 同時アクセス + 検索高速化」を肩代わりする道具 - これが無いとファイル直接編集の世界

  2. コッドのリレーショナルモデルは50年経っても色褪せない - 「数学的に綺麗な土台」の重要さ

  3. ACID は1970年代に固まった、今も DB の基本 - Atomicity, Consistency, Isolation, Durability

  4. 分離レベル4段階は、性能 vs 整合性のトレードオフ - READ UNCOMMITTED / READ COMMITTED / REPEATABLE READ / SERIALIZABLE

  5. MVCC は現代 DB の心臓部 - 読み取りはロックフリー、書き込みは行ロック

  6. NoSQL は「ACID を緩めてスケールアウトする」発想 - 4大カテゴリ (KV, Document, Column, Graph)

  7. BASE と CAP は分散時代の3点セット - Basically Available, Soft state, Eventually consistent

  8. CAP 定理は「分断時に C か A か」の選択 - P は現実では避けられない

  9. PACELC は CAP の現実拡張 - 通常時のトレードオフ (遅延 vs 整合性) も考える

  10. インデックスは魔法じゃない、コストがある - 書き込み遅延、ストレージ、オプティマイザ混乱

  11. 迷ったら RDBMS。NoSQL は補完として併用 - これが2026年の現実解

あなたが明日から少し違う目で見られること

  • 「とりあえず MongoDB」と言うエンジニアに対して、「ACID 要らないアクセスパターン?」と聞ける
  • MySQL のデフォルト分離レベルが REPEATABLE READ で、PostgreSQL は READ COMMITTED と知っていて、移行時の挙動差を予期できる
  • 「インデックス張れば速くなる」を盲信せず、「書き込み比率」と「カーディナリティ」を考えられる
  • 「Cassandra と DynamoDB と PostgreSQL の違いは何?」と聞かれて、CAP と PACELC の言葉で説明できる
  • 障害時に「結果整合性のシステムだから、データ反映の遅延が来てる」と仮説が立てられる

考えてみよう

  1. もしあなたが新規プロジェクトで「来年100万ユーザー突破するかも」と言われたら、最初の DB に何を選ぶ?理由は?
  2. 「決済システムで Cassandra を使う」設計をレビューに出されたら、何を質問する?
  3. CAP 定理は「現実では成り立つ」が、その制約をどうやって緩めるか考えてみよう (NewSQL の発想)

さらに深掘りするなら

  • 書籍: 『データ指向アプリケーションデザイン』(Martin Kleppmann) - 邦題「DDIA」と呼ばれる名著。本記事の内容を10倍深く扱う。エンジニアの教養書として必読
  • 書籍: 『SQL アンチパターン』(Bill Karwin) - DB 設計の失敗事例集。「やりがちなアンチパターン」を笑いながら学べる
  • 書籍: 『達人に学ぶ DB 設計徹底指南書』(ミック) - 日本語で読める正規化と設計の本
  • 論文: Codd “A Relational Model of Data” (1970) - 関係モデルの原典
  • 論文: “Dynamo: Amazon’s Highly Available Key-value Store” (2007) - DynamoDB の原典、結果整合性の入門に最適
  • 論文: Brewer “Towards Robust Distributed Systems” (2000) - CAP 定理のキーノート
  • 論文: Abadi “Consistency Tradeoffs in Modern Distributed Database System Design” (2012) - PACELC 提唱論文
  • ドキュメント: PostgreSQL Documentation の “Concurrency Control” 章
  • ドキュメント: MySQL Reference Manual の “InnoDB Locking and Transaction Model” 章
  • 動画: MIT 6.824 Distributed Systems (YouTube 公開) - 分散システムの王道講義
  • OSS: PostgreSQL, MySQL InnoDB のソースコード - 現代 DB のリファレンス実装

カリキュラム本編で関連する章

  • MySQL 章 - トランザクション、ロック、MVCC、インデックスを実際に試す本気編
  • セキュリティ章 - SQL injection、最小権限の原則、暗号化カラム
  • インフラ章 - RDS / Aurora の運用、バックアップ、リードレプリカ、フェイルオーバー
  • Go 章 - database/sql パッケージ、トランザクション制御、コネクションプール

次の読み物

07_分散システム.md をまだ読んでいなければ次に。今回出てきた「CAP 定理」「結果整合性」をもっと一般化した、分散システム全般の話。

DB の話は「データ」を分散する話だったが、分散システム全般では「計算」「時刻」「合意」をどう分散させるかが主題になる。CAP 定理の元になった FLP 不可能性、Paxos / Raft、リーダー選出、ベクトルクロックなど、分散時代のエンジニア教養がまるごと詰まっている。次もスマホで気楽にどうぞ。

07. 分散システム入門 - 「一貫性」の難しさ