メインコンテンツまでスキップ

CQRS(Command Query Responsibility Segregation)

書き込みと読み取りを分離する

CQRS は「書き込み(Command)」と「読み取り(Query)」で異なるモデル・ストレージを使うアーキテクチャパターンです。


なぜ分離するのか

書き込みと読み取りでは、データに求める性質が異なります。

┌─────────────────────────────────────────────────────────────┐
│ 書き込みと読み取りの要件の違い │
├────────────────────┬────────────────────────────────────────┤
│ 書き込み │ 読み取り │
├────────────────────┼────────────────────────────────────────┤
│ 1件ずつ正確に │ 大量データをまとめて集計 │
│ トランザクション保証│ 速度優先(多少の遅延OK) │
│ 整合性が最重要 │ 柔軟なクエリ(アドホック分析) │
│ 正規化されたモデル │ 非正規化・事前集計されたモデル │
│ 行指向が適切 │ 列指向が適切 │
│ │ │
│ → PostgreSQL │ → ClickHouse, Elasticsearch 等 │
└────────────────────┴────────────────────────────────────────┘

1つのDBで両方を賄おうとすると、どちらかが犠牲になる。
→ 分ければ、それぞれ最適なストレージ・モデルを使える。

アーキテクチャ

従来(単一モデル)

┌─────────────────────────────────────────────┐
│ アプリケーション │
│ │
│ 書き込み ──┐ │
│ ├──→ 同じDB、同じテーブル │
│ 読み取り ──┘ 同じモデル │
│ │
│ ┌───────────────────────────────────────┐ │
│ │ PostgreSQL │ │
│ │ events テーブル │ │
│ │ ├── INSERT (書き込み) │ │
│ │ └── SELECT COUNT GROUP BY (読み取り) │ │
│ │ → 数億行になると遅い │ │
│ └───────────────────────────────────────┘ │
└─────────────────────────────────────────────┘

CQRS(分離モデル)

┌─────────────────────────────────────────────────────────────┐
│ アプリケーション │
│ │
│ ┌─ Command 側 ─────────────────────────────────────────┐ │
│ │ │ │
│ │ 「書く」ための最適なモデル │ │
│ │ ┌───────────────────────────────┐ │ │
│ │ │ PostgreSQL │ │ │
│ │ │ ・INSERT events │ │ │
│ │ │ ・トランザクション保証 │ │ │
│ │ │ ・正規化されたスキーマ │ │ │
│ │ └──────────────┬────────────────┘ │ │
│ │ │ │ │
│ └──────────────────┼────────────────────────────────────┘ │
│ │ イベント伝搬 │
│ │ (CDC / Message Queue / Pub-Sub) │
│ │ │
│ ┌─ Query 側 ────────┼──────────────────────────────────┐ │
│ │ ▼ │ │
│ │ 「読む」ための最適なモデル │ │
│ │ ┌───────────────────────────────┐ │ │
│ │ │ ClickHouse │ │ │
│ │ │ ・COUNT / GROUP BY が爆速 │ │ │
│ │ │ ・列指向、圧縮 │ │ │
│ │ │ ・非正規化OK │ │ │
│ │ └───────────────────────────────┘ │ │
│ │ │ │
│ └────────────────────────────────────────────────────────┘ │
│ │
└───────────────────────────────────────────────────────────────┘

イベント伝搬の方法

Command 側で書いたデータを Query 側に届ける方法。

┌─────────────────────────────────────────────────────────────┐
│ イベント伝搬の選択肢 │
│ │
│ ① CDC(Change Data Capture) │
│ ┌──────────┐ WAL ┌────────┐ ┌────────────┐ │
│ │PostgreSQL│ ────→ │ PeerDB │ ────→ │ ClickHouse │ │
│ └──────────┘ └────────┘ └────────────┘ │
│ ・アプリ変更不要 │
│ ・DBレベルで完結 │
│ ・遅延: 数秒 │
│ │
│ ② メッセージキュー │
│ ┌──────┐ ┌───────┐ ┌────────────┐ │
│ │ App │ → │ Kafka │ → │ ClickHouse │ │
│ └──────┘ └───────┘ └────────────┘ │
│ ・アプリがイベントを明示的に publish │
│ ・複数の購読者に同時配信可能 │
│ ・遅延: ミリ秒〜秒 │
│ │
│ ③ アプリ内 Pub/Sub │
│ ┌──────────────────────────────────────┐ │
│ │ App │ │
│ │ EventPublisher → EventListener │ │
│ │ ├→ 統計更新 │ │
│ │ ├→ 通知送信 │ │
│ │ └→ 監査ログ │ │
│ └──────────────────────────────────────┘ │
│ ・最もシンプル(Spring の @EventListener 等) │
│ ・同一プロセス内 │
│ ・遅延: なし │
│ │
└─────────────────────────────────────────────────────────────┘

結果整合性(Eventual Consistency)

CQRS では Command 側と Query 側のデータに遅延が生まれます。

時刻 0:00  ユーザーがログイン
時刻 0:00 PostgreSQL に INSERT(Command 側は即座に反映)
時刻 0:03 CDC で ClickHouse に到達(Query 側は3秒遅延)

→ 0:00〜0:03 の間、Command 側と Query 側で状態が異なる
→ これが「結果整合性」

結果整合性が許容できるケース

ケース遅延許容度
ダッシュボード数秒〜数分「今日のログイン数」が3秒古くてもOK
月次レポート数時間翌朝見れれば十分
監査ログ検索数秒直後に検索する人は少ない

結果整合性が許容できないケース

ケース理由対応
ログイン直後の「自分のセッション一覧」自分の操作がすぐ反映されないと混乱Command 側のDBから読む
残高確認古い残高で決済されると困るCommand 側のDBから読む

CQRS は「全ての読み取り」を分離するのではなく、「分離して得する読み取り」だけを分離するのが現実的。


CQRS を採用すべきとき・すべきでないとき

┌─────────────────────────────────────────────────────────────┐
│ │
│ ✅ 採用すべきとき: │
│ ├── 書き込みと読み取りの負荷特性が大きく異なる │
│ │ (大量INSERTと大量集計が同じDBで競合する) │
│ ├── 読み取りのクエリパターンが多様 │
│ │ (アドホック分析、複数のダッシュボード) │
│ ├── 読み取りに特化したストレージが有効 │
│ │ (列指向DB、全文検索エンジン) │
│ └── 結果整合性が許容できる読み取りがある │
│ │
│ ❌ 採用すべきでないとき: │
│ ├── 読み書きの負荷が小さく、1つのDBで十分 │
│ ├── 強整合性が全ての読み取りで必要 │
│ ├── チームが小さく、運用コストを吸収できない │
│ └── 書き込み直後の読み取りが主要ユースケース │
│ │
└─────────────────────────────────────────────────────────────┘

まとめ

┌────────────────────────────────────────────────────────────┐
│ CQRS の本質 │
├────────────────────────────────────────────────────────────┤
│ │
│ 「書き込みと読み取りで最適な形が違うなら、分ければいい」 │
│ │
│ ・Command: トランザクション保証、正規化、行指向DB │
│ ・Query: 高速集計、非正規化OK、列指向DB │
│ ・イベント伝搬で両者を接続(CDC, MQ, Pub/Sub) │
│ ・結果整合性を受け入れる範囲を見極める │
│ │
│ DDD の用語として紹介されることが多いが、 │
│ 本質はデータアーキテクチャの設計判断。 │
│ │
└────────────────────────────────────────────────────────────┘

次のステップ

  • 実現パターン: CDC, メッセージキュー, イベントストアの具体的な選択