目次を表示する

DB 設計の軸 2026 ─ ドメイン駆動と特性駆動の二つの流派を行き来する 19 章

OLTP の本質 ─ MVCC・WAL・行指向

OLTP の本質 ─ MVCC・WAL・行指向

OLTP(Online Transaction Processing)は、過去 30 年の業務システムの主役だった。今でも多くのアプリケーションのバックエンドは OLTP DB(PostgreSQL / MySQL / SQL Server)に乗っている。だがその「当たり前さ」の裏で、OLTP DB は何をどう支えているのかは意外と知られていない。

この章では PostgreSQL を主役に、OLTP の本質を実装から見る。

OLTP の定義

Kleppmann『DDIA』第 3 章 の定義:

OLTP: user-facing で、各リクエストは少数のレコードを触り、書き込みと読み込みが両方ある。大量の同時リクエストを低レイテンシで捌く。

第 3 章で出した 6 軸で言うと:

  • read/write: 半々〜read 寄り
  • レイテンシ: ms オーダー
  • 整合性: Snapshot 〜 Strong
  • データ形状: 行
  • 寿命: Hot + Warm
  • スケール: Vertical + Read replica

「大量の小さなトランザクションを、ms 内に、整合性を保って捌く」── これが OLTP の責務。これを支えるのが、MVCC・WAL・行指向ストレージの 3 点セット。

MVCC ─ 読み書きを並列化する仕組み

MVCC(Multi-Version Concurrency Control)は、PostgreSQL の心臓部。アイデアは単純:

行を上書きせず、新しいバージョンを書く

例:

-- 初期状態
INSERT INTO accounts (id, balance) VALUES (1, 100);

-- セッション A で更新(コミット前)
BEGIN;
UPDATE accounts SET balance = 90 WHERE id = 1;
-- まだ COMMIT していない

-- セッション B が同時に SELECT する
SELECT balance FROM accounts WHERE id = 1;
-- → 100 を返す(A のコミット前の値)

UPDATE は既存の行を上書きせず、新しい行(tuple)を作って、古い行を “obsolete” マークにする。各 tuple には xmin(作成トランザクション ID)xmax(削除トランザクション ID) が刻まれていて、Snapshot を取った時点で見えるべき tuple だけが返される。

sequenceDiagram
  participant A as Session A
  participant DB as PostgreSQL
  participant B as Session B

  Note over DB: tuple v1<br/>xmin=100, xmax=null<br/>balance=100

  A->>DB: BEGIN UPDATE balance=90
  Note over DB: tuple v1: xmin=100, xmax=200<br/>tuple v2: xmin=200, xmax=null<br/>balance=90

  B->>DB: SELECT balance
  Note over DB: B の Snapshot は<br/>v1 だけ見える
  DB-->>B: 100

  A->>DB: COMMIT
  Note over DB: v2 が visible に

これで 読み手は書き手をブロックしない、書き手は読み手をブロックしない。OLTP のスループットを支える基本原理。

MVCC の代償 ─ Dead Tuples と VACUUM

MVCC は無料ではない。

問題:古い tuple は「使われない」が、ディスクには残る。これを dead tuple と呼ぶ。放置すると:

  1. テーブルサイズが膨らむ(bloat
  2. インデックスに古いポインタが残り、スキャンコストが上がる
  3. メモリ常駐率が下がる

これを掃除するのが VACUUM。autovacuum で自動的に走るが、設定が悪いと bloat が雪だるま式に増える ── これが PostgreSQL 運用の頻出トラブルの一つ。

graph LR
  A[UPDATE 多発] --> B[dead tuple 蓄積]
  B --> C{VACUUM が追いつくか?}
  C -->|Yes| D[健全]
  C -->|No| E[bloat / 性能低下]
  E --> F[VACUUM FULL or 再構築]

  style E fill:#ffe1e1
  style D fill:#e1ffe1

VACUUM は dead tuple のスペースを 再利用可能 にするが、ディスク領域そのものは縮小しない。縮小したいなら VACUUM FULL(テーブルロック)か pg_repack

HOT updates ─ 同一ページ内最適化

PostgreSQL には HOT (Heap Only Tuple) updates という最適化がある。インデックスされていない列だけを更新する場合、同じページ内で新 tuple を作り、インデックスを更新しない。これは bloat 削減に大きく効く。

-- インデックス: (id)
CREATE INDEX idx_accounts_id ON accounts(id);

-- balance はインデックスなし → HOT update が効く
UPDATE accounts SET balance = balance + 10 WHERE id = 1;

-- email にインデックスがあると HOT update は効かない
CREATE INDEX idx_accounts_email ON accounts(email);
UPDATE accounts SET email = '[email protected]' WHERE id = 1;

つまり何にインデックスを張るかは、書き込みパフォーマンスに直結する。これは設計編(第 5 章)で詳しく扱う。

WAL ─ 永続性と耐障害性の根拠

ACID の D(Durability) を支えるのが WAL(Write-Ahead Logging)。

データを変更する前に、変更内容をログに書く

sequenceDiagram
  participant A as Session A
  participant Mem as Memory (shared_buffers)
  participant WAL as WAL (disk)
  participant Heap as Heap (disk)

  A->>Mem: UPDATE accounts SET balance=90
  Mem->>WAL: WAL レコード書き込み
  WAL-->>A: COMMIT 可能(fsync 後)
  Note over A: ここで「コミット完了」と<br/>応答できる

  Mem-)Heap: バックグラウンドで遅延書き込み

ポイント:

  • WAL は append-only。シーケンシャル書き込みでディスクに優しい
  • データファイル(heap)への書き込みは遅延できる
  • クラッシュ時は WAL を replay することで状態を復元できる

PostgreSQL の COMMIT が成立する条件は「WAL レコードが fsync された」こと。データファイルへの反映はもっと後で良い。この遅延が OLTP のスループットを支える

WAL は 論理的な意味でも重要。WAL はそのまま 論理レプリケーションCDC (Change Data Capture) の源にもなる。Debezium のような CDC ツールは WAL を読んで他システムにストリームする。OLTP DB が外の世界に流れていく出口でもある(第 12 章 Stream と第 17 章 Polyglot で再登場)。

行指向ストレージ ─ 単一行を素早く取り出す

OLTP は 少数のレコードを触るワークロード。これに最適なのが 行指向

PostgreSQL の Heap(行指向):
ページ 1: [row1: id=1, name=Alice, balance=100] [row2: id=2, name=Bob, balance=200] ...
ページ 2: [row3: id=3, name=Carol, balance=300] ...

id=1 の行を取得」というクエリは、インデックスから 1 ページを読むだけで完了する。

これに対して列指向は:

列指向の物理配置:
id 列:      [1, 2, 3, ...]
name 列:    [Alice, Bob, Carol, ...]
balance 列: [100, 200, 300, ...]

id=1 の行を取得」しようとすると、3 つの場所からデータを引っ張ってきて合体する必要がある。OLTP には不向き。

逆に「全行の balance の合計」なら、列指向の方が圧倒的に速い ── これが OLAP の話で、第 6 章で扱う。

OLTP の本質を 3 つで言うなら

graph TB
  A[MVCC] -->|読み書き並列| Z[OLTP の<br/>高スループット]
  B[WAL] -->|永続性 + 遅延書き込み| Z
  C[行指向] -->|単一行高速取得| Z

  style Z fill:#e1ffe1

この 3 つが揃うことで、OLTP DB は「多くの同時セッション」を「ms 以内」で「整合性を保ったまま」捌ける。だがその裏には dead tuple、bloat、VACUUM の運用、WAL の管理、HOT update の前提といった 設計判断が控えている。

この章の要点

  • OLTP = user-facing、少数行、ms レイテンシ、半々の read/write 比率
  • MVCC = 上書きせず新しい tuple。読み書き並列の根拠
  • 代償: dead tuple / bloat / VACUUM。HOT update が緩和
  • WAL = 永続性の根拠、append-only、論理レプリ・CDC の源
  • 行指向 = 単一行高速取得。OLTP に最適、OLAP には不向き

次章への問いかけ

MVCC は読み書き並列を可能にするが、dead tuple という代償を生んだ。HOT update を効かせるには インデックス戦略 が要る。WAL は他システムへの 出口にもなる。

こうした特性は、設計の意思決定として実務に降りてくる。次章で OLTP 設計の 5 つの判断 ── Aggregate 境界 / Hot row / Index / 進化 / 非正規化 ── を具体に扱う。