💨

DB設計のアプローチ:マスタ・トランザクション vs イミュータブルデータモデル

に公開
2026/03/29

はじめに

この記事では、マスタ・トランザクション型の設計と、イミュータブルデータモデルの違いを整理します。

知りたいことは、たいてい次の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