CQRS(コマンドクエリ責務分離)

英語名 Command Query Responsibility Segregation
読み方 シーキューアールエス
難易度
所要時間 設計に1〜2週間
提唱者 グレッグ・ヤング(2010年)
目次

ひとことで言うと
#

データの書き込み(Command)と読み取り(Query)を別々のモデルで処理するアーキテクチャパターン。それぞれに最適化されたデータストアやモデルを使うことで、複雑なシステムのスケーラビリティと保守性を向上させる。

押さえておきたい用語
#

押さえておきたい用語
コマンド(Command)
データを変更する操作のこと。「注文を作成する」「サブスクリプションを解約する」など、状態を変える意図を表現する。戻り値は原則として持たない。
クエリ(Query)
データを読み取る操作のこと。「商品一覧を取得する」「注文履歴を検索する」など、状態を変えずに情報を返す。
プロジェクション(Projection)
コマンド側の変更を受けて読み取り用に最適化されたビューを構築する処理のこと。イベントを購読して非正規化されたリードモデルを更新する。
結果整合性(Eventual Consistency)
書き込みと読み取りが即座には一致せず、一定時間後に整合する状態のこと。非同期同期のCQRSでは避けられないトレードオフ。

CQRSの全体像
#

CQRS:書き込みと読み取りを分離する構造
Command(書き込み側)ビジネスルールの検証とデータの整合性を保証正規化されたDB(PostgreSQL等)ドメインモデル(DDD)と高相性PlaceOrder, CancelSubscriptionUpdateProfile, ApplyDiscountQuery(読み取り側)UIに最適化された非正規化されたビュー検索用DB(Elasticsearch等)1クエリで必要データを全取得商品一覧, 注文履歴検索ダッシュボード, レポート同期メカニズムCommand側の変更をイベントで発行Query側がプロジェクションで非同期更新結果整合性:書き込み直後は古いデータが返る可能性あり
CQRS導入の進め方
1
Command設計
書き込み専用モデルでビジネスルールを保証
2
Query設計
UIに最適化された非正規化ビューを構築
3
同期構築
イベント経由の非同期同期メカニズムを実装
段階的導入
負荷差が大きいドメインから効果を検証

こんな悩みに効く
#

  • 読み取りと書き込みの負荷特性が大きく異なり、片方がボトルネックになっている
  • 読み取り用のクエリが複雑で、書き込み用のデータモデルと合わない
  • 単一のモデルで読み書き両方を最適化しようとして、どちらも中途半端になっている

基本の使い方
#

ステップ1: コマンド(書き込み)モデルを設計する

データを変更する操作に特化したモデルを作る。

  • ビジネスルールの検証と整合性の保証に集中する
  • ドメインモデル(DDD)との相性が良い
  • コマンドは「何をしたいか」を表現する(例: PlaceOrder, CancelSubscription)

ポイント: コマンドモデルはビジネスロジックの正しさを保証することに専念する。

ステップ2: クエリ(読み取り)モデルを設計する

データを表示・検索する操作に特化したモデルを作る。

  • 画面表示に最適化された非正規化されたビュー
  • 複雑なJOINを避け、1クエリで必要なデータを取得できる設計
  • 読み取り専用なので、ビジネスルールの検証は不要

ポイント: クエリモデルは「UIが必要とするデータの形」に合わせて設計する。

ステップ3: コマンドとクエリの同期メカニズムを構築する

書き込みモデルの変更を読み取りモデルに反映する仕組みを作る。

  • 同期的: コマンド実行時に読み取りモデルも更新(シンプルだが結合度が高い)
  • 非同期的: イベントを発行し、読み取りモデルが非同期に更新(スケーラブルだが結果整合性)
  • イベントソーシングとの組み合わせが強力

ポイント: 非同期の場合、「書き込んだ直後に読み取ると古いデータが返る」ことをUIで対処する必要がある。

ステップ4: 段階的に導入し、効果を検証する

システム全体ではなく、特定のドメインからCQRSを適用する。

  • 読み書きの負荷差が大きいドメインから始める
  • メトリクス(レイテンシ、スループット)で効果を定量評価する
  • 過剰適用を避け、シンプルなCRUDで十分な箇所には使わない

ポイント: CQRSは万能薬ではない。複雑さに見合うメリットがある場所にだけ適用する。

具体例
#

例1:ECサイトの商品カタログで検索レイテンシをp99 800ms→50msに改善する

課題: 商品カタログの読み取り(ユーザーの商品一覧・検索)と書き込み(管理者の商品登録・更新)で要件が全く違う。読み取りは高スループット・低レイテンシが求められ、書き込みは複雑なバリデーションが必要。

コマンドモデル: PostgreSQLに正規化された商品データ。在庫確認、価格計算、カテゴリ整合性チェックなどのビジネスルールを適用して書き込み。

クエリモデル: Elasticsearchに非正規化された商品ビュー。商品名、価格、カテゴリ、画像URL、レビュー平均点がすべて1ドキュメントに含まれる。

同期: 商品更新時にProductUpdatedイベントを発行し、非同期でElasticsearchのインデックスを更新。遅延は通常1秒以内

結果: 商品検索のレイテンシがp99 800ms→50msに改善。管理画面のバリデーションロジックも読み取りの都合に引きずられなくなり、開発速度が向上

例2:SaaSダッシュボードのレポート生成を独立させる

課題: 分析ダッシュボードの集計クエリ(GROUP BY + JOIN 5テーブル)が重く、レスポンスに8秒かかる。この集計がメインDBのCPUを圧迫し、書き込み性能にも影響。

CQRS適用:

  • Command側: トランザクションデータをメインDBに通常通り書き込み
  • Query側: 日次バッチと差分更新で集計済みテーブル(マテリアライズドビュー)を構築。ClickHouseに格納

結果: ダッシュボードのレスポンスが8秒→200ms。メインDBのCPU使用率が70%→45%に低下。書き込みパフォーマンスも20%改善

例3:チケット予約システムで読み書きを独立スケールさせる

課題: チケット販売開始時、読み取り(空席確認)のトラフィックが書き込み(予約確定)の100倍に達する。単一DBではスケーリングの限界。

CQRS適用:

  • Command側: 予約確定処理は正規化DB + 排他ロックで整合性を保証。レプリカ2台
  • Query側: Redis上に非正規化された空席情報を保持。レプリカ10台で読み取りをスケールアウト
  • 同期: 予約確定時にイベント発行→Redis更新(遅延50ms以内

結果: 販売開始時に秒間50,000リクエストの読み取りを安定処理。書き込み側はビジネスルールに集中し、予約の二重確保バグがゼロに。

やりがちな失敗パターン
#

  1. 単純なCRUDにCQRSを適用する — 読み書きの要件に差がないシステムにCQRSを導入すると、複雑さが増すだけでメリットがない。負荷特性やモデルの複雑さに差がある場合にのみ適用すること
  2. 結果整合性をユーザーに説明しない — 非同期同期の場合、「今登録した商品が検索に出てこない」とユーザーが混乱する。UIで「反映まで少しお待ちください」と明示すること
  3. 同期メカニズムの障害を想定しない — イベントの欠損や順序逆転でクエリモデルが不整合になる。イベントの冪等処理と再構築(リプロジェクション)の仕組みを用意すること
  4. Command側とQuery側でチームを分断する — 書き込みチームと読み取りチームを完全に分離すると、同期メカニズムの責任が曖昧になる。ドメイン単位でチームを組み、Command/Query両方をオーナーシップする

まとめ
#

CQRSは読み取りと書き込みを分離し、それぞれに最適化されたモデルを持たせるパターン。読み書きの負荷特性や複雑さが異なるドメインで効果を発揮する。ただし複雑さが増すため、「本当にCQRSが必要か?」を常に自問しながら、適切な場所にだけ適用しよう