NoSQL をもう一度ちゃんと整理する — SQL との対比で理解する使い所とアンチパターン
はじめに — 「とりあえず NoSQL」の危うさ
「スケールしそう」「JSON そのまま入る」「速い」というふわっとしたイメージで NoSQL が選ばれる場面は多いです。ところが運用に入ると、ホットパーティションで詰まる、クエリがうまく書けない、データ修正に数日かかる、といった形で跳ね返ってきます。
本記事は、特定ベンダーの紹介ではなく NoSQL 全般を SQL(RDBMS)との対比で整理 し、さらに モデリング例・パーティションキー設計・整合性の寄せ方・併用パターン・選定観点 まで踏み込むことで、「整理記事」ではなく 設計判断に使える記事 にすることを目指します。
扱う軸は次のとおりです。
- NoSQL とは何か(非リレーショナルの本質)
- SQL との違い(6 軸比較)
- 4 タイプ(Key-Value / Document / Wide-Column / Graph)
- 具体モデリング例: 注文データを SQL と Document でどう持つか
- NoSQL で最初に壊れる設計: パーティションキー
- 有効シナリオとアンチパターン
- NoSQL で整合性をどう寄せるか(Saga / Outbox / 冪等性)
- 二者択一ではない併用パターン
- 製品選定の観点と判断フロー
主張を先に書きます。NoSQL は "SQL の上位互換" ではなく、トレードオフの違う別の選択肢です。この前提が崩れると、設計判断の多くが歪みます。
NoSQL とは何か(非リレーショナルの本質)
NoSQL は "Not Only SQL" の略で、リレーショナルモデル(行と列の固定スキーマ、外部キー、JOIN、ACID)をあえて捨てる代わりに別の何かを取る データベースの総称です。得るものは主に 3 つ。
- 柔軟なスキーマ: 事前定義なし、ドキュメントごとに属性が違ってもよい
- 水平スケール: ノード追加でスループットと容量を伸ばす
- 分散環境下の可用性: 一部ノード障害でも読み書きを続けられる
このトレードオフは CAP 定理(Consistency / Availability / Partition tolerance のうち、ネットワーク分断下で同時に成立するのは 2 つまで)でも語られます。歴史的には RDBMS = CP 寄り、NoSQL = AP 寄りと整理されがちですが、現代の NoSQL は設定で強整合性も選べる ものが多く、「NoSQL = 結果整合のみ」は単純化しすぎです。
整合性モデルの略語は押さえておきます。
- ACID(RDBMS で一般的): Atomicity / Consistency / Isolation / Durability。一連の操作がまとめて成功するか、まとめて失敗することを保証する
- BASE(多くの NoSQL の既定): Basically Available / Soft state / Eventually consistent。可用性とスケールを優先し、結果整合でよいという考え方
銀行の振込のように「途中で止まると困る」仕事は ACID 向き、アクセスログのように「最終的に揃っていれば十分」な仕事は BASE で十分、というのが基本の感覚です。
SQL との比較(6 軸)
| 軸 | SQL (RDBMS) | NoSQL |
|---|---|---|
| データモデル | 行と列のテーブル | Key-Value / Document / Wide-Column / Graph |
| スキーマ | 事前定義、厳格 | 動的 / 柔軟 / スキーマレス |
| 整合性 | ACID(強整合) | BASE が基本、設定次第で強整合も |
| スケーリング | 基本は垂直(Scale Up) | 基本は水平(Scale Out) |
| クエリ | 標準 SQL、複雑な JOIN / 集計 | 製品ごとに独自、JOIN は苦手または非対応 |
| 関係性 | 外部キー + 正規化 | デノーマライズ + 埋め込み |
重要な 3 軸を掘り下げます。
スキーマ: 柔軟さの代償は「アプリ側でスキーマを守る責務」
RDBMS ではスキーマ違反は DB が蹴ります。NoSQL では通ってしまいます。DB が面倒を見ていた型や必須項目のチェックを、アプリ側のバリデーション層で負う ことになります。短期的には楽ですが、チームが増えてコードベースが広がると一貫性の担保が難しくなります。
整合性とスケール: 両立はタダではない
まず、垂直スケール(Scale Up) と 水平スケール(Scale Out) を分けて捉えると理解しやすいです。
- 垂直スケール: 1 台の DB サーバーをより大きなマシンに置き換える。CPU / RAM / SSD を増やして性能を上げる
- 水平スケール: データを複数ノードに分散し、ノード数を増やして容量とスループットを伸ばす
RDBMS でも読み取り専用レプリカによる read scale はよく使われますが、読み書きをまたいだ水平スケール になると事情が変わります。どの行をどのサーバーに置くかを決める シャーディング(sharding) が必要になり、アプリケーションやミドルウェア側でルーティング、再分散、クロスシャード JOIN / トランザクションを面倒見る必要が出てきます。つまり、RDBMS でも scale out は可能ですが、標準の強みだけで自然に実現できるわけではない ということです。
一方 NoSQL は、最初から「データを分けて複数ノードに載せる」前提 で設計されていることが多いです。たとえば Cosmos DB のような分散 DB では、アイテムはまず 論理パーティション に分かれ、その論理パーティション群が 物理パーティション に配置されます。物理パーティションにデータ量とスループットが分散されるので、ノードやパーティションが増えるほど全体の処理能力を伸ばしやすくなります。
ここで用語を 1 回そろえておきます。本記事で単に「パーティション」と書く場合、基本的にはアプリケーション設計で意識する「論理パーティション」 を指します。一方で、物理パーティション は DB エンジン内部の実装単位で、実際にスループットやストレージが配分される先です。読者が設計で直接決めるのはパーティションキーであり、そこから結果として論理パーティションが生まれ、さらに内部で物理パーティションへ配置される、という順番で読むと混乱しにくいです。
ただし、ここで大事なのは 「ノードを増やせば自動で速くなる」のではなく、「負荷をうまく散らせるキー設計をしたときに初めて水平スケールが効く」 という点です。大量のリクエストが同じキーに集中すれば、そのキーが載っている 1 つの論理パーティション、ひいては 1 つの物理パーティションだけが熱くなります。これが ホットパーティション です。
要するに、NoSQL の水平スケールは 「分散 DB だから勝手に伸びる」 ではなく、「キーを軸にデータとリクエストを均等に散らせると伸びる」 という性質です。ただしその代償として、分散ノードをまたぐ強整合トランザクションは高コスト、または制限つきです。
JOIN と関係性: 多 JOIN を移植してはいけない
RDBMS の強みは JOIN を伴う複雑クエリです。NoSQL ではこれをそのまま移植せず、一緒に使うものを一緒に保存する(デノーマライズ / 埋め込み)方向に倒します。関連データをアプリ側で都度寄せ集めると、RDBMS なら 1 本の SQL になる処理が N+1 API 呼び出しに化けます。
NoSQL の 4 タイプと向き不向き
NoSQL はひとくくりに語れません。「NoSQL を選ぶ」ではなく「どのタイプを選ぶ」 が本当の問いです。
Key-Value ストア
- 代表例: Redis、DynamoDB、Memcached
- 得意: キー 1 発の超高速 lookup、キャッシュ、セッション、設定値
- 苦手: 範囲検索、属性フィルタ、集計
「このキーに対する値をくれ」が 9 割の世界で輝きます。「年齢 16〜24 で好物がワッフルで、直近 24 時間にログインしたユーザーを全員くれ」と言われると詰みます。
Document(ドキュメント)
- 代表例: MongoDB、CouchDB、Cosmos DB(NoSQL API)
- 得意: 半構造化データ、アプリのオブジェクトをそのまま保存、アジャイルに属性が増える領域
- 苦手: 多テーブル JOIN、強い関係整合性
ドメインオブジェクトを JSON 的に保存し、"一緒に読むもの"を 1 ドキュメントに凝集 する設計に向きます。
Wide-Column(列指向 / Column-Family)
- 代表例: Cassandra、HBase、Bigtable
- 得意: 書き込みスループット、時系列、巨大ログ
- 苦手: アドホックな絞り込み、JOIN、トランザクション
「書き込みが毎秒数万〜数十万で、クエリパターンは決まっている」領域が主戦場です。
:bulb: 「列指向」という語の注意
同じ「列指向」という訳語で呼ばれる分析特化のカラムナー DB(BigQuery、Redshift、ClickHouse など)とは別物です。NoSQL の Wide-Column は 行キーごとに列ファミリを持つ分散 KV 寄りの構造 で、OLTP 的な書き込みを得意とします。分析特化のカラムナー DB は 列単位で列値を連続格納し、OLAP の集計スキャンを高速化 するのが目的で、設計思想も用途も異なります。
Graph
- 代表例: Neo4j、Neptune、Cosmos DB(Gremlin API)
- 得意: 関係の深さを辿る探索(SNS の友達の友達、推薦、不正検知、権限グラフ)
- 苦手: 大量のバルクスキャン、ヘビーな集計
「データそのものより関係に価値がある」ケース向けです。RDBMS で再帰 JOIN を大量に書き始めたら、Graph DB 検討のサインです。
比較表
| 特性 | Key-Value | Document | Wide-Column | Graph |
|---|---|---|---|---|
| データモデル | キーと値 | JSON 風ドキュメント | 行キー + 列ファミリ | ノードと辺 |
| 得意 | キー直取り | 半構造化の凝集 | 書き込み・時系列 | 関係の探索 |
| スキーマ | 事実上なし | 柔軟 | 半構造化 | スキーマレス |
| 代表例 | Redis / DynamoDB | MongoDB / Cosmos DB | Cassandra / HBase | Neo4j / Neptune |
具体モデリング例: 注文データを SQL と Document でどう持つか
抽象論だけでは掴みにくいので、「EC サイトの注文」という定番の題材で RDBMS と Document をモデリング比較します。
RDBMS のモデル(正規化)
customers(id, name, email)
products(id, name, price, stock)
orders(id, customer_id, ordered_at, total)
order_items(id, order_id, product_id, qty, unit_price)
- 注文一覧画面は JOIN 1 本で組み立てられる
- 商品マスタ更新(価格変更、名称変更)は
productsの 1 行を直すだけ - 在庫の整合性、外部キー制約は DB が守る
Document のモデル(埋め込み)
{
"_id": "order_001",
"orderedAt": "2026-04-18T10:00:00Z",
"customer": {
"customerId": "cust_123",
"name": "Alice",
"email": "alice@example.com"
},
"items": [
{
"productId": "prod_001",
"name": "Mechanical Keyboard",
"unitPrice": 14900,
"qty": 1
},
{
"productId": "prod_002",
"name": "USB-C Cable",
"unitPrice": 1290,
"qty": 2
}
],
"total": 17480
}
- 注文詳細画面はこのドキュメント 1 本を読むだけで完結
-
items[].nameやunitPriceは 注文時点のスナップショット。後から商品マスタの価格が変わっても注文側は動かない(そもそも履歴として動いてはいけない) -
productId/customerIdは参照として残す。商品の詳細が必要なら別 collection を引く
どこまで埋め込み、どこから分けるか
判断軸は 3 つ。
| 軸 | 埋め込み | 参照(分ける) |
|---|---|---|
| 件数 | 有界(1 注文あたり高々数十件) | 無限成長(1 商品に紐づく全注文、1 投稿の全コメント) |
| 変化頻度 | ほぼ不変(スナップショット的) | 頻繁に独立で更新される |
| 読み方 | ほぼ一緒に読む | 単独で検索・更新する |
この記事の注文例では、items は注文あたり件数が有界で、しかも「注文時点のスナップショット」として残したいため埋め込み。一方で customer は 「現在のメール」も欲しい場面がある ならフルに埋め込まず、customerId + 最小スナップショットに留めるのが無難です。
よくある失敗
- 商品を丸ごと埋め込む: 商品マスタ更新のたびに全注文を書き換える羽目になる
- 配送ステータス履歴を 1 注文に無限追記: ドキュメントサイズ上限(MongoDB は 16MB など)に近づいてインデックスが劣化する。上限を超えたら書き込み失敗
- 何でも分けてしまう: RDBMS のクセで全部参照にすると、Document のメリット(読み取り 1 発、ローカルな原子更新)を失う
シナリオで学ぶ: NoSQL が有効なパターン
4 タイプそれぞれに典型シナリオを 1 本。
A: セッション / ユーザー設定を爆速で取得したい
- 要件: ユーザー ID から設定値を毎リクエスト取得、ミリ秒未満、書き込みは稀、数千万ユーザー
- SQL: 可能だが、RDBMS をキャッシュ代わりに酷使。接続プールも逼迫しがち
- NoSQL: Key-Value が最適
B: EC サイトの商品カタログ(属性が商品ごとにバラバラ)
- 要件: 家電、衣類、食品が同じサイトに並ぶ。属性がカテゴリで全然違う
- SQL: EAV(Entity-Attribute-Value)にねじ込むとクエリが汚くなる
- NoSQL: Document。商品ごとに属性セットが違う JSON を入れる
C: IoT / アプリログの時系列大量書き込み
- 要件: 数十万デバイスから毎秒メトリクス、読みは「デバイス X の直近 N 時間」中心
- SQL: 書き込みがボトルネック、パーティショニング運用が重い
- NoSQL: Wide-Column。デバイス ID + タイムスタンプをキーに
D: フォロー関係の「友達の友達」/ 不正検知
- 要件: 関係を数ホップ辿る。例: 3 ホップ以内に共通の送金相手がいるか
- SQL: 再帰 CTE や多段 JOIN。ホップ数が増えるほど壊れる
- NoSQL: Graph。グラフトラバーサルで書ける
NoSQL で最初に壊れる設計: パーティションキーとアクセスパターン
NoSQL で本当に事故るのはここです。章 1 つ使って深掘りする価値がある ポイントです。
まず、パーティションキーとは何か
パーティションキーは、「どのデータを同じまとまりとして保存し、どのノードへ送るか」を決める値 です。RDBMS の主キーが「行を一意に識別する」ためのものだとすると、NoSQL のパーティションキーは 「データ配置と負荷分散のための軸」 という性格が強いです。
たとえば注文データを考えます。
-
customerIdをキーにすると、ある顧客の注文は同じ論理パーティションに集まりやすい -
orderStatusをキーにすると、PENDINGやSHIPPEDのような少数の値に全注文が集中しやすい -
createdAtをそのままキーにすると、今この瞬間の書き込みがごく一部に偏りやすい
NoSQL 製品の多くは、まず パーティションキー値ごとの論理パーティション を作り、それを内部で物理パーティションやノードに割り当てます。つまり、水平スケールの最小単位は「1 レコード」ではなく「キー値ごとのまとまり」 です。
この区別は大事なので、言い換えておきます。
- 論理パーティション: 同じパーティションキー値を持つデータのまとまり。アプリ設計・クエリ・トランザクション境界に効く
- 物理パーティション: DB が内部的に保持する実体。スループットや容量の配分先であり、複数の論理パーティションが載ることがある
イメージを 3 行で描くと、関係はこんな感じです。
論理パーティション A(user_1) ─┐
論理パーティション B(user_2) ─┼─> 物理パーティション X
論理パーティション C(user_3) ─┘
つまり、アプリから見えるのは「キー値ごとの論理パーティション」 であり、DB 内部では複数の論理パーティションが 1 つの物理パーティションに載る ことがあります。
したがって、「複数パーティションにまたがる」 という表現には 2 通りあります。
- 複数論理パーティションにまたがる: キー値が複数に散る。アプリ視点ではこちらが重要
- 複数物理パーティションにまたがる: 内部的に複数の実体へ処理が飛ぶ。性能や RU / レイテンシに効く
多くの実務判断では、まず 「その操作は 1 つの論理パーティションで完結するか」 を見るのがコツです。そこが崩れると、内部で複数物理パーティションへ広がる可能性も高くなります。
このため、パーティションキーは少なくとも次の 4 つを同時に左右します。
- データの置き場所: どの論理パーティション / 物理ノードに置かれるか
- スループットの使われ方: どこに read / write が集中するか
- クエリ効率: 1 つの論理パーティションで完結するか、複数の論理パーティションをまたぐか
- トランザクション範囲: 同一論理パーティション内だけ原子的に扱える製品が多い
つまりパーティションキーは、単なる「検索条件の 1 つ」ではなく、性能・コスト・整合性の境界線を決める設計項目 です。ここを軽く決めると、後続のインデックスやクエリ最適化では取り返しにくくなります。
水平スケールとキーはどうつながるのか
水平スケールをもう少し具体的に言うと、多数の論理パーティションが複数の物理パーティションへ分散配置され、各物理パーティションが並列に仕事をする状態 です。NoSQL が強いのは、この分散前提の構造を最初から持っている点にあります。
ただし、アプリケーションから見える入口はたいてい パーティションキー です。内部ではハッシュやレンジなどで物理配置が決まりますが、出発点はあくまでキー値です。
- キーの値の種類が多い(high cardinality)
- かつアクセスが偏りすぎない
- かつ主要なクエリがそのキーを含む
この 3 つを満たすと、データ量とリクエスト量が複数の論理パーティションへ散り、結果として複数の物理パーティションも活用されやすくなり、scale out が効きやすくなります。
逆に、キーが悪いとこうなります。
- 分散できない: 値の種類が少なく、少数の論理パーティションにしか割れない
- 偏る: 一部キーに書き込みが集中してホットパーティション化する
- 読めても高い: キーを含まないクエリが多く、クロスパーティション検索になる
- 増やしても伸びない: ノードを追加しても、熱いキーの載った場所だけが先に限界に達する
ここが重要です。水平スケールのボトルネックは、マシン台数ではなく「最も熱いキー」になる ことが多いです。全体で 10 ノードあっても、1 つのキーに 8 割の書き込みが集中していれば、そのキーに対応する論理パーティション、ひいてはそれを載せている物理パーティションが先に詰まります。クラスタ全体では空きがあっても、アプリから見ると「DB が遅い」「429 / throttling が出る」になります。実務ではこの挙動がかなりいやらしいです。空いているのに詰まる、DB 界の満員電車です。
なぜパーティションキーが最重要か
NoSQL の水平スケールは「キーで分散する」ことが前提です。キーを間違えるとノードが均等に使われず、特定ノードだけが過負荷になります。そして厄介なことに、パーティションキーは事実上変更不可です。後から変えるにはデータ全再配置、もしくはテーブル作り直しが必要になります。
重要性をもう少し分解すると、次の理由があります。
- 性能を決めるから: point read で済むか、複数の論理パーティションをまたぐ query になるかが変わる
- スケール上限を決めるから: キー分布が悪いと、クラスタ全体ではなく 1 論理パーティション / 1 物理パーティション周辺の上限に先にぶつかる
- コストを決めるから: 複数論理パーティションにまたがる query や再試行が増えると RU / WCU / レイテンシが悪化する
- 整合性の範囲を決めるから: 「同一キー内だけトランザクション可」の製品では、ドメイン境界に直結する
- 後戻りしづらいから: インデックス追加より重く、データ移行や二重書きが絡みやすい
物理的な制約もあります。たとえば DynamoDB は物理パーティション相当の単位で 10GB / 読み 3000RU / 書き 1000WU、Cosmos DB は論理パーティションあたり 20GB が上限です。つまり製品ごとに「どの層のパーティション上限か」は違うので、「1 パーティション上限」とだけ覚えるのは危険です。「無限に書ける」わけではありません。
悪いキー
-
isActive(boolean, 低 cardinality): 2 つ程度の論理パーティションにしか分かれない。事実上スケールしない -
country(偏りが大きい): 日本ユーザーが大半のサービスで、日本に対応する論理パーティションへ集中しやすい -
created_at(時刻そのまま): 今の時間帯に対応する論理パーティションへ書き込みが集中する典型的ホットパーティション。過去側の論理パーティションは書き込まれずコールドになり、プロビジョンスループットも無駄 -
status/type/category: 取りうる値が数個しかなく、低 cardinality。特定の値に偏ると不均衡
少しマシなキー
-
customerId/userId: cardinality が高く、均等分散しやすい - ただしヘビーユーザーが数人いると、その人に対応する論理パーティションだけ熱くなる問題は残る
- マルチテナント SaaS で
tenantIdにすると、特定テナントが大きすぎる と 20GB 上限に刺さる
よくある妥協案(複合キー / ハッシュサフィックス)
完璧なキーは普通ない、と前提した上で。
-
userId#yyyyMM: ユーザー単位で時間で切る。サイズを抑えつつ、ユーザー単位の読みを維持 -
tenantId#userId: テナントで分散しつつ、テナント内は user で分ける -
customerId#shardNo(0〜N-1 のランダム): 書き込みを N シャードに撒く(write sharding)。代わりに読み取りは N 倍のファンアウト になる -
createdAt_bucket + randomSuffix: 時系列でもホットを避けたい場合
どれも「完璧」ではなく、書きと読みの間でどのトレードオフを取るか という話です。
原則
- 最初に アクセスパターン(どう読み、どう書くか)を洗い出す
- その上で 均等分散するキー を選ぶ
- 書き読みの両方を想定する(片方だけ最適化すると、もう片方が破綻する)
- 必要なら複合キーで時間やテナントを織り込む
- 本番投入前に キー分布の偏り を実データで確認する
チェック観点をさらに実務寄りに言い換えると、次の問いに Yes で答えられるかを見ます。
- このキーは 十分に種類が多いか
- 上位 1% のキーに トラフィックが集中しすぎないか
- 主要な read パターンは キーを指定して 1 つの論理パーティションに絞れるか
- 将来データが増えても、1 キーあたりのサイズ上限 に早く達しないか
- トランザクションでまとめたい単位と、キーのまとまり は一致しているか
パーティションキーは「後で直せばいい」ものではありません。最初の設計で決まる と思ったほうが安全です。
シナリオで学ぶ: NoSQL のアンチパターン
経験上ハマりやすいものを 6 つ挙げます。
アンチパターン 1: 銀行口座の振込のような多エンティティトランザクション
「A から引き、B に足す」を同時に成立させるか、同時に失敗させるかしか許されない処理は、ACID の atomicity と consistency の典型で、RDBMS が得意な領域です。NoSQL でも multi-document ACID を持つ製品はありますが、「できる」と「得意」は違います。
アンチパターン 2: 多表 JOIN 前提のレポート / ダッシュボード
「顧客 × 注文 × 明細 × 商品」を JOIN して集計するレポートは SQL の独壇場です。NoSQL に移すと アプリ側で JOIN を書き直す ことになり、往復回数が増えてレイテンシもコストも悪化します。分析用途なら、DWH や分析 DB を横に置くほうが筋が通ります(アンチパターン 6 とも関連)。
アンチパターン 3: スキーマレスを盾にした雑ドキュメント設計
「スキーマを持たなくていい」ではなく「スキーマを DB に守らせない」にすぎません。
- Unbounded arrays: 1 ドキュメント内に無制限に伸びる配列(投稿に対する全コメント埋め込みなど)。ドキュメントサイズ上限やインデックス性能に刺さる
- Bloated documents: ドキュメントが肥大化し、関係ないフィールドも毎回読み出す羽目に
アンチパターン 4: ホットパーティション(章 7 の復習)
パーティションキーを country や生の created_at にして、特定ノードだけが熱くなる失敗。章 7 のとおり、キー設計を真面目にやる 以外に根本対処はありません。
アンチパターン 5: クエリパターンを決めずに設計を始める
RDBMS は後から WHERE やインデックスを足せば何とかなることが多いですが、NoSQL はアクセスパターン駆動設計が原則です。「どう問い合わせるか」が決まっていないなら、NoSQL は早計です(ここは危険と言い換えても同じ意味)。
アンチパターン 6: 運用 DB と分析 DB を兼ねようとする
よくある失敗。運用トラフィックを捌いている NoSQL 本体に、社内レポートやダッシュボードのクエリをそのままぶつける。結果として、オンライン処理のレイテンシが悪化し、プロビジョンスループットも食い潰します。
対処: 分析用途は素直に DWH / 分析 DB / 読み取り専用レプリカに分離する。CDC(Change Data Capture)やストリーミングで別系統へ流すのが定番です。「1 つの DB で全部」を目指さない。
NoSQL で整合性をどう寄せるか
「強整合が要るなら RDBMS」で終わらせず、NoSQL 寄りの世界で整合性をどう扱うかを軽く触れておきます。単なる比較記事から設計記事に上げる ための肝です。
冪等性(Idempotency)
同じリクエストを 2 回処理しても結果が同じになる設計。分散環境ではネットワーク的にリトライが発生するのが前提なので、冪等性は前提スキル です。
実装例:
- リクエスト ID(clientRequestId)を保持し、すでに処理済みなら前回結果を返す
- 「追加」ではなく「set」「upsert」ベースで書く
- カウンタのインクリメントは「既にインクリメント済みか」を記録する
リトライと DLQ(Dead Letter Queue)
結果整合は「いつか揃う」前提。揃わないイベントをどう扱うかも設計に含めます。
- 一定回数までリトライ
- 超えたら DLQ に退避して人が見る / 自動リトライする
- タイムアウトは短めに、リトライは冪等前提で
Saga パターン
多サービス / 多エンティティにまたがる処理を、ローカルトランザクションと補償トランザクションの連鎖 で実現するパターン。2PC(2 相コミット)を避けたい分散システムで広く使われます。
- オーケストレーション型: 中央のコーディネータが各ステップを順に呼ぶ。監視しやすいが、コーディネータがボトルネック
- コレオグラフィ型: 各サービスがイベントを発行して反応する。スケールするが、全体の流れが見えづらい
例: 注文確定で「在庫確保 → 決済 → 出荷予約」と進め、決済で失敗したら補償として「在庫を戻す」を実行する。
Outbox パターン
「DB 書き込みとメッセージ送信を同時に成立させる」という dual-write 問題への定番解です。
- 業務データの更新と同じトランザクションで
outboxテーブルにイベントを書く - 別プロセス(ポーラーや CDC)が
outboxを読んでメッセージブローカに配信 - 配信済みフラグを立てる
これにより、DB 書き込みは成功したがメッセージが飛ばなかった(逆もまた然り)という不整合を防げます。
まとめ
NoSQL を選ぶなら、「ACID 一発で済ませる」ではなく 「結果整合を前提に、冪等性とイベント連携で揃える」思考 に切り替える必要があります。逆に、この思考を組織がまだ持っていない段階で NoSQL に飛びつくと、アンチパターンに一直線です。
実務では二者択一ではない: RDBMS と NoSQL の併用パターン
現実のシステムは「どちらか」ではなく「組み合わせ」です。このように役割ごとに適切なストアを選ぶ発想を Polyglot Persistence(ポリグロット永続化) と呼びます。
パターン A: 本体 RDBMS + キャッシュに Key-Value
最も普通の構成。RDBMS の前に Redis 等を置き、読み取りの大半をキャッシュで返す。TTL やキー設計の手間はあるが、効果が大きい。
パターン B: 本体 RDBMS + 検索は全文検索エンジン
キーワード検索、日本語の形態素解析、ファセット検索などは RDBMS の LIKE では限界がある。OpenSearch / Elasticsearch 相当に非同期で流す。検索インデックスは結果整合でよい ケースが多い。
パターン C: トランザクションは RDBMS、イベント / ログは Wide-Column
監査ログや操作ログ、イベントストリームは書き込み量が桁違い。これらは Wide-Column 系に逃がし、RDBMS はビジネスの整合性が要る部分 に集中させる。
パターン D: マスタは RDBMS、プロファイル / 設定は Document
ユーザープロファイルや設定は「人によって属性がバラバラ」「頻繁に増減する」性質があり Document 向き。請求や契約情報は RDBMS。
パターン E: 運用と分析を分離
運用 DB(RDBMS でも NoSQL でも)から、CDC やストリーミングで DWH / 分析 DB に流す。ダッシュボードや重集計はそちらで。運用の性能を守る ための基本形。
注意点
- データの二重管理 は必ず発生する。どちらが master of record かを決める
- 整合性の責任 を Saga / Outbox / CDC のどれで取るかを明示する
- 運用対象が増える。運用負荷と恩恵を天秤にかける
製品選定の観点
「4 タイプのどれか」だけで終わらせず、実務の選定では次の軸も必ず見るべきです。
| 観点 | 見るべきこと |
|---|---|
| トランザクション範囲 | 単一キーのみ / 同一論理パーティション内 / 論理パーティションまたぎ / マルチドキュメント。要件に合うか |
| セカンダリインデックス | 有無、強整合 or 結果整合、インデックスあたりの課金、更新時の書き込み増幅 |
| 整合性レベル | 強整合 / 有界陳腐化 / セッション / 結果整合 など、選べる粒度 |
| TTL | 自動失効が使えるか。ログやセッションで効く |
| バックアップ / PITR | Point-in-Time Restore の粒度と保持期間 |
| リージョン冗長 | マルチリージョン書き込みの可否、コンフリクト解決の挙動(LWW / カスタム) |
| 課金モデル | プロビジョンド / オンデマンド、ストレージ、RU / WCU などの単位、読み書きの非対称性 |
| 運用負荷 | マネージド度、オートスケールの滑らかさ、監視の自動連携 |
| エコシステム | ドライバ、言語サポート、マイグレーションツール、運用ツール |
| パーティション上限 | 論理パーティション上限と物理パーティション上限を分けて確認する。例: Cosmos DB は論理パーティション 20GB、物理パーティション側にも throughput / storage 上限がある |
プロダクト選定は タイプ選定で 5 割、これらの運用観点で残り 5 割 が決まると思っていいです。
判断フロー / チェックリスト
迷ったら上から順に。最初に Yes になったところが答えです。
- クエリパターンがまだ固まっていない → RDBMS で始める(プロトタイプ含む。早計に NoSQL に飛びつくと後悔しやすい)
- 多エンティティにまたがる強整合なトランザクションが中核要件 → RDBMS
- キー 1 発の高速アクセスが 9 割以上 → Key-Value
- スキーマが頻繁に揺れる、または要素ごとに属性がバラバラ → Document
- 書き込みスループット優先で、時系列 / ログ的データ → Wide-Column
- データの価値が「関係」そのもの → Graph
- どれにも強く当てはまらない → RDBMS(NoSQL を選ぶ積極的理由がない)
そして大事なこととして、単体ではなく複数組み合わせ(Polyglot Persistence)の選択肢も常に残してください。実務では多くがハイブリッドです。
まとめ
- NoSQL は SQL の上位互換ではなく、トレードオフの違う別の選択肢
- 4 タイプは得意領域が違う。「NoSQL を選ぶ」ではなく「どのタイプを選ぶ」
- 注文データのような題材では、埋め込みと参照の境界 を判断軸(有界性 / 変化頻度 / 読み方)で決める
- パーティションキーは最初の設計で決まる。後からの修正は極めて重い
- 整合性は「諦める」ではなく、冪等性 / Saga / Outbox で寄せる
- 現実は二者択一ではなく適材適所の併用(Polyglot Persistence)
- 製品選定は タイプ + 運用観点 の両輪
- 迷ったら RDBMS から始め、根拠のある領域だけ NoSQL に切り出すのが安全
- 「何を問い合わせるか」が決まっていない段階で NoSQL を選ぶのは 早計 / 危険
参考資料
全体・比較
- AWS — Relational vs Nonrelational Databases
- AWS — What is Database Sharding?
- MongoDB — Relational Vs. Non-Relational Databases
- GeeksforGeeks — Types of NoSQL Databases
- insightsoftware — Relational vs. Non-Relational Databases
- InterSystems — NoSQL Databases Explained
- ByteHouse — SQL and NoSQL Databases
- Rivery — Relational vs NoSQL
- Stack Overflow — Column Family vs Key-Value vs Document vs Graph
モデリング(注文・埋め込み vs 参照)
- Mingo — Embedding vs Referencing in MongoDB
- MongoDB Docs — Model Embedded One-to-Many
- OneUptime — Embedding vs Referencing
- GeeksforGeeks — Embedded vs Referenced
パーティションキー / アンチパターン
- DynamoDB — Best practices for partition keys
- Azure Cosmos DB — Partitioning and horizontal scaling
- OneUptime — Effective Partition Key Strategy in Cosmos DB
- Medium — Partitioning in NoSQL databases
- ScyllaDB — NoSQL Data Modeling Mistakes
- MongoDB Docs — Schema Design Anti-Patterns
Discussion