DB設計のアプローチ:マスタ・トランザクション vs イミュータブルデータモデル
はじめに
この記事では、マスタ・トランザクション型の設計と、イミュータブルデータモデルの違いを整理します。
知りたいことは、たいてい次の3つです。
- 現在値中心の設計にするべきか
- 変更履歴を事実として積み上げる設計にするべきか
- 実務ではどこで両者を使い分けるべきか
結論から言うと、多くの業務システムでは二者択一ではありません。
現在値を持つテーブルと、変更履歴やイベントを持つテーブルを役割分担して併用することが多いです。
その前提を踏まえたうえで、それぞれの設計が何を得意とし、どこで複雑さを持つのかを見ていきます。
率直な結論
まず結論を先にまとめます。
- 現在の状態を素早く読みたいなら、マスタ・トランザクション型が扱いやすいです
- 変更の事実そのものに意味があるなら、イミュータブル型が向いています
- 監査、再計算、履歴追跡が重要なら、イベントを残す価値が高いです
- 一覧検索やAPI応答性能が重要なら、現在値テーブルを別に持つ方が安定します
- 実務では「現在値テーブル + 履歴テーブル」の併用が中心です
つまり、比較の軸は流行ではありません。
何を主な読み取り対象にするか、どの時点の事実を残したいか、です。
まず違いを整理する
マスタ・トランザクション型は、現在の管理対象と履歴を分けて持つ考え方です。
- マスタ: 現在有効な情報や基準情報
- トランザクション: 発生した処理や変更履歴
例えば、受注システムなら次のようになります。
- orders: 現在の注文状態
- order_status_histories: 状態変更履歴
- payments: 入金トランザクション
一方、イミュータブルデータモデルは、更新で上書きするよりも、起きた事実を追加で残すことを重視します。
- 注文が作成された
- 支払いが完了した
- 出荷された
- キャンセルされた
このとき中心になるのは「現在どうなっているか」より、「何が起きたか」です。
ただし、ここで誤解しやすい点があります。
イミュータブル型でも、実務では現在値を高速参照するためのテーブルやスナップショットを別に持つことが多いです。
そのため、実際の設計判断は次の比較になります。
- 現在値を主に読むか
- 発生イベントを主に読むか
- 現在値をイベントから毎回組み立てるか
- 現在値を別テーブルに投影して持つか
マスタ・トランザクション型が向いている場面
マスタ・トランザクション型は、現在値中心の業務で扱いやすいです。
例えば次のようなケースです。
- 顧客マスタ
- 商品マスタ
- 契約の現在状態
- 在庫の現在数量
- ユーザーの現在設定
この型の強みは、読み取りが単純になることです。
一覧表示、検索、参照APIでは、今の値をそのまま引ける方が実装しやすくなります。
単純化した例です。
CREATE TABLE orders (
id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
customer_id BIGINT NOT NULL,
current_status VARCHAR(20) NOT NULL,
total_amount DECIMAL(10, 2) NOT NULL,
updated_at DATETIME NOT NULL
);
CREATE TABLE order_status_histories (
id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
order_id BIGINT NOT NULL,
from_status VARCHAR(20) NOT NULL,
to_status VARCHAR(20) NOT NULL,
changed_at DATETIME NOT NULL,
FOREIGN KEY (order_id) REFERENCES orders(id)
);
この形なら、注文一覧は orders を見れば足ります。
履歴が必要なときだけ order_status_histories を参照できます。
向いている理由は次のとおりです。
- 参照クエリを単純にしやすい
- 現在値の検索条件を書きやすい
- 業務画面の一覧や詳細を作りやすい
- イベント再生なしで現在値を取得できる
一方で、注意点もあります。
- 現在値の更新ロジックが複雑になりやすい
- 履歴の粒度が後付けになりやすい
- なぜ今の状態になったかが履歴不足で見えなくなることがある
現在値を UPDATE で維持する設計で、どこまで状態管理を明示すべきかは、UPDATE設計を破綻させないDB状態遷移図の作り方でも整理しています。
イミュータブル型が向いている場面
イミュータブル型は、変更履歴そのものが業務上重要なときに強いです。
例えば次のようなケースです。
- 会計仕訳
- ポイント増減履歴
- 監査ログ
- ワークフローの承認イベント
- 外部連携の受信イベント
この型では、更新より追加が中心になります。
CREATE TABLE order_events (
id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
order_id BIGINT NOT NULL,
event_type VARCHAR(30) NOT NULL,
occurred_at DATETIME NOT NULL,
payload_json JSON NOT NULL,
FOREIGN KEY (order_id) REFERENCES orders(id)
);
この設計の利点は、何が起きたかを失いにくいことです。
- 監査しやすい
- 後から経緯を追いやすい
- 再計算や再投影の材料を残せる
- 更新競合を避けやすい場面がある
ただし、イミュータブル型にもコストがあります。
- 現在値をそのまま引けないことがある
- 一覧や検索のクエリが重くなりやすい
- イベント設計が粗いと後で読み解けなくなる
- 保持データ量が増えやすい
特に重要なのは、イベントを残せば自動的に設計が良くなるわけではないことです。
イベント名、ペイロード、順序保証、重複処理、再実行方針まで決めないと、単に読みづらい履歴テーブルになることがあります。
実務では併用が多い
実務で多いのは、次の形です。
- events系テーブルで事実を残す
- current系テーブルで現在値を持つ
例えば次の構成です。
CREATE TABLE order_events (
id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
order_id BIGINT NOT NULL,
event_type VARCHAR(30) NOT NULL,
occurred_at DATETIME NOT NULL,
payload_json JSON NOT NULL,
FOREIGN KEY (order_id) REFERENCES orders(id)
);
CREATE TABLE orders (
id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
customer_id BIGINT NOT NULL,
current_status VARCHAR(20) NOT NULL,
total_amount DECIMAL(10, 2) NOT NULL,
updated_at DATETIME NOT NULL
);
この形にすると、役割を分けやすくなります。
- 事実を残す: order_events
- 速く読む: orders
これはイミュータブル型を採用しつつ、読み取り性能と運用性を確保するための現実的な形です。
前提として、eventsからordersへどう反映するかは設計が必要です。
- 同一トランザクションで更新するか
- 非同期で投影するか
- 投影遅延を許容できるか
ここを曖昧にすると、履歴は正しいが現在値が遅れる、または現在値はあるが再計算できない、といった問題が起きます。
どちらを選ぶかの判断軸
判断軸は、次のように整理すると考えやすいです。
現在値の参照が中心か
画面一覧、検索API、管理画面など、今の状態を高速に見たいなら、現在値テーブルを中心にした方が安定します。
変更履歴が主役か
監査、追跡、原因調査、再集計が重要なら、イベントや履歴を事実として残す価値が高いです。
再構築が必要か
将来的に「過去時点の状態を再現したい」「集計ロジックを作り直したい」がありうるなら、イミュータブル寄りの設計が効きます。
運用で耐えられるか
イベント投影の失敗監視、再実行、順序保証まで運用できないなら、無理に複雑なイベント中心設計へ寄せない方が安全です。
よくある誤解
よくある誤解は次の2つです。
イミュータブルなら常に優れている
そうではありません。
読み取り性能、運用負荷、設計難易度は上がりやすいです。
変更履歴が重要でない領域まで全面採用すると、複雑さが先に立ちます。
マスタ・トランザクションは古い設計である
これも違います。
現在値中心の業務では今でも有効です。
問題なのは古いか新しいかではなく、履歴の必要性と読み取り要件に合っているかです。
使い分けの目安
迷ったら、次のように考えると判断しやすいです。
- 最新状態だけを正しく持てればよい: マスタ・トランザクション寄り
- 起きた出来事を失いたくない: イミュータブル寄り
- 両方必要: 現在値テーブルとイベントテーブルを併用
最初からすべてをイベント化する必要はありません。
一方で、履歴が重要な業務を上書き更新だけで済ませると、後で追跡できなくなります。
まとめ
マスタ・トランザクション型とイミュータブル型の違いは、どちらが正しいかではありません。
何を中心に保存し、何を主に読み、どこに複雑さを置くかの違いです。
実務では次の整理が有効です。
- 現在値を速く読みたいなら現在値テーブルを持つ
- 起きた事実を残したいならイベントや履歴を持つ
- その両方が必要なら併用する
設計で重要なのは、流行の用語を採用することではありません。
業務上重要な事実と、運用上必要な現在値を分けて考えることです。
Discussion