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 と呼ぶ。放置すると:
- テーブルサイズが膨らむ(bloat)
- インデックスに古いポインタが残り、スキャンコストが上がる
- メモリ常駐率が下がる
これを掃除するのが 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 / 進化 / 非正規化 ── を具体に扱う。