ひとことで言うと#
データの書き込み(Command)と読み取り(Query)を別々のモデルで処理するアーキテクチャパターン。それぞれに最適化されたデータストアやモデルを使うことで、複雑なシステムのスケーラビリティと保守性を向上させる。
押さえておきたい用語#
- コマンド(Command)
- データを変更する操作のこと。「注文を作成する」「サブスクリプションを解約する」など、状態を変える意図を表現する。戻り値は原則として持たない。
- クエリ(Query)
- データを読み取る操作のこと。「商品一覧を取得する」「注文履歴を検索する」など、状態を変えずに情報を返す。
- プロジェクション(Projection)
- コマンド側の変更を受けて読み取り用に最適化されたビューを構築する処理のこと。イベントを購読して非正規化されたリードモデルを更新する。
- 結果整合性(Eventual Consistency)
- 書き込みと読み取りが即座には一致せず、一定時間後に整合する状態のこと。非同期同期のCQRSでは避けられないトレードオフ。
CQRSの全体像#
こんな悩みに効く#
- 読み取りと書き込みの負荷特性が大きく異なり、片方がボトルネックになっている
- 読み取り用のクエリが複雑で、書き込み用のデータモデルと合わない
- 単一のモデルで読み書き両方を最適化しようとして、どちらも中途半端になっている
基本の使い方#
データを変更する操作に特化したモデルを作る。
- ビジネスルールの検証と整合性の保証に集中する
- ドメインモデル(DDD)との相性が良い
- コマンドは「何をしたいか」を表現する(例: PlaceOrder, CancelSubscription)
ポイント: コマンドモデルはビジネスロジックの正しさを保証することに専念する。
データを表示・検索する操作に特化したモデルを作る。
- 画面表示に最適化された非正規化されたビュー
- 複雑なJOINを避け、1クエリで必要なデータを取得できる設計
- 読み取り専用なので、ビジネスルールの検証は不要
ポイント: クエリモデルは「UIが必要とするデータの形」に合わせて設計する。
書き込みモデルの変更を読み取りモデルに反映する仕組みを作る。
- 同期的: コマンド実行時に読み取りモデルも更新(シンプルだが結合度が高い)
- 非同期的: イベントを発行し、読み取りモデルが非同期に更新(スケーラブルだが結果整合性)
- イベントソーシングとの組み合わせが強力
ポイント: 非同期の場合、「書き込んだ直後に読み取ると古いデータが返る」ことをUIで対処する必要がある。
システム全体ではなく、特定のドメインからCQRSを適用する。
- 読み書きの負荷差が大きいドメインから始める
- メトリクス(レイテンシ、スループット)で効果を定量評価する
- 過剰適用を避け、シンプルなCRUDで十分な箇所には使わない
ポイント: CQRSは万能薬ではない。複雑さに見合うメリットがある場所にだけ適用する。
具体例#
課題: 商品カタログの読み取り(ユーザーの商品一覧・検索)と書き込み(管理者の商品登録・更新)で要件が全く違う。読み取りは高スループット・低レイテンシが求められ、書き込みは複雑なバリデーションが必要。
コマンドモデル: PostgreSQLに正規化された商品データ。在庫確認、価格計算、カテゴリ整合性チェックなどのビジネスルールを適用して書き込み。
クエリモデル: Elasticsearchに非正規化された商品ビュー。商品名、価格、カテゴリ、画像URL、レビュー平均点がすべて1ドキュメントに含まれる。
同期: 商品更新時にProductUpdatedイベントを発行し、非同期でElasticsearchのインデックスを更新。遅延は通常1秒以内。
結果: 商品検索のレイテンシがp99 800ms→50msに改善。管理画面のバリデーションロジックも読み取りの都合に引きずられなくなり、開発速度が向上。
課題: 分析ダッシュボードの集計クエリ(GROUP BY + JOIN 5テーブル)が重く、レスポンスに8秒かかる。この集計がメインDBのCPUを圧迫し、書き込み性能にも影響。
CQRS適用:
- Command側: トランザクションデータをメインDBに通常通り書き込み
- Query側: 日次バッチと差分更新で集計済みテーブル(マテリアライズドビュー)を構築。ClickHouseに格納
結果: ダッシュボードのレスポンスが8秒→200ms。メインDBのCPU使用率が70%→45%に低下。書き込みパフォーマンスも20%改善。
課題: チケット販売開始時、読み取り(空席確認)のトラフィックが書き込み(予約確定)の100倍に達する。単一DBではスケーリングの限界。
CQRS適用:
- Command側: 予約確定処理は正規化DB + 排他ロックで整合性を保証。レプリカ2台
- Query側: Redis上に非正規化された空席情報を保持。レプリカ10台で読み取りをスケールアウト
- 同期: 予約確定時にイベント発行→Redis更新(遅延50ms以内)
結果: 販売開始時に秒間50,000リクエストの読み取りを安定処理。書き込み側はビジネスルールに集中し、予約の二重確保バグがゼロに。
やりがちな失敗パターン#
- 単純なCRUDにCQRSを適用する — 読み書きの要件に差がないシステムにCQRSを導入すると、複雑さが増すだけでメリットがない。負荷特性やモデルの複雑さに差がある場合にのみ適用すること
- 結果整合性をユーザーに説明しない — 非同期同期の場合、「今登録した商品が検索に出てこない」とユーザーが混乱する。UIで「反映まで少しお待ちください」と明示すること
- 同期メカニズムの障害を想定しない — イベントの欠損や順序逆転でクエリモデルが不整合になる。イベントの冪等処理と再構築(リプロジェクション)の仕組みを用意すること
- Command側とQuery側でチームを分断する — 書き込みチームと読み取りチームを完全に分離すると、同期メカニズムの責任が曖昧になる。ドメイン単位でチームを組み、Command/Query両方をオーナーシップする
まとめ#
CQRSは読み取りと書き込みを分離し、それぞれに最適化されたモデルを持たせるパターン。読み書きの負荷特性や複雑さが異なるドメインで効果を発揮する。ただし複雑さが増すため、「本当にCQRSが必要か?」を常に自問しながら、適切な場所にだけ適用しよう。