Vegas Engineering by FORBIS 記事一覧
← 記事一覧へ戻る
Architecture ・ 第4回

書きと読みを、分ける — CQRS と射影パイプラインで両立させる"速い検索"と"堅い更新"

更新は文書指向で堅く、検索は正規化データで速く。2つのモデルを射影でつなぐ設計。

Vegas 開発チーム ・ 2026-07-03 ・ 読了 7分

PROBLEM1つのモデルで両方は最適化できない

通販基幹エンジンでは、1件の注文が「明細・配送先・決済・履歴」といった多くの要素を入れ子で抱えます。この業務レコードを更新するときは、関連する要素をまとめて一貫性を保ちたい。ところが同じデータを「顧客名で検索」「期間で集計」「一覧で並べ替え」したいとき、入れ子構造のままではフィルタも結合も重くなります。更新に最適な形と、検索・集計に最適な形は、そもそも別物なのです。

APPROACH書き込みと読み取りでモデルを分ける

CQRS(Command Query Responsibility Segregation)は、この矛盾を「モデルを2つ持つ」ことで解きます。書き込み側と読み取り側で、データの持ち方そのものを分離するのです。

PROJECTIONイベントで射影して同期する

2つのモデルは、変更イベントを介してつなぎます。書き込みストアで業務レコードが更新されると、その変更がイベントとして流れ、射影(プロジェクション)処理が読み取りストアの正規化テーブルへ反映します。読み取り側はあくまで検索・集計のための「作られた形」であり、真実は常に書き込み側にあります。

更新要求 command 書き込みストア 文書DB / 入れ子 射影 event → projection 読み取りストア リレーショナル / 正規化 検索 集計
図:更新は文書DBへ書き、イベント経由で正規化DBへ射影して検索・集計に供する

CONTRAST同じ業務データ、2つの表現

同じ注文データが、書き込み側では入れ子ドキュメントとして、読み取り側では複数テーブルに分解された形として、それぞれの目的に合わせて表現されます。

文書DB(1ドキュメント) order { id, customer, items:[ … ], shipping:{ … } payment:{ … } } リレーショナルDB(正規化) orders id, customer order_items order_id, sku 検索・集計用インデックス
図:入れ子の1ドキュメントが、検索に適した複数テーブルへと展開される

TRADE-OFF速さの代償は結果整合と運用コスト

この設計の利点は明快です。更新は文書単位で堅牢に、検索・集計は正規化データで軽快に、両方を取れる。大量の注文が積み上がっても検索が重くならず、書き込み側と読み取り側を独立にスケールできます。

一方で代償もあります。射影には遅延があり、書き込み直後に読み取り側へ反映されない結果整合を受け入れる必要があります。さらに、2つのスキーマを二重管理し、射影パイプラインを運用し続けるコストもかかります。だからこそ、直後の整合が絶対に必要な画面(たとえば決済確定の確認)は、読み取り側を待たず書き込みストアから直接読む、といった使い分けが要になります。

read_side/search_orders.sqlSQL
-- 読み取りストア:正規化テーブルへのテンプレート検索
SELECT o.id, o.customer, o.ordered_at, SUM(i.amount) AS total
FROM orders o
JOIN order_items i ON i.order_id = o.id
WHERE o.customer LIKE :keyword
  AND o.ordered_at BETWEEN :from AND :to
GROUP BY o.id, o.customer, o.ordered_at
ORDER BY o.ordered_at DESC
LIMIT :size OFFSET :offset;
TAKEAWAY

更新の堅さと検索の速さは、1つのモデルでは両立しにくい。書き込みは文書指向で整合を守り、読み取りは正規化して速く引く。両者をイベント射影でつなげば両取りできるが、結果整合と二重スキーマの運用コストが対価になる。整合が要る画面は書き込み側から直接読む使い分けを設計に織り込むこと。