KV / Document NoSQL の本質 ─ Partition と Eventual Consistency
OLTP は RDB の縄張り、OLAP は列指向の縄張り。だが現代の Web スケールアプリケーションには、その間にもう一つの縄張りがある。KV / Document NoSQL ── DynamoDB / Cosmos DB / MongoDB の世界。
この章では DynamoDB を主役に、NoSQL の本質を見る。
NoSQL は何を解決したのか
NoSQL の登場(2009 年頃の “movement”)は、RDB が単一ノードで限界に当たったことへの反応だった。
- Web のトラフィックが秒間万 req を超え、単一 RDB のリプリケーションでは捌けない
- グローバルなアプリケーションでは、地理的レイテンシを無視できない
- Schema-less な「とりあえず JSON で保存」が必要な場面が増えた
これらに「強整合性を捨てて、分散を取る」ことで応えたのが NoSQL。
第 3 章の 6 軸で見ると:
- read/write: 半々〜write 寄り
- レイテンシ: ms(ホットパス)
- 整合性: Causal / Eventual(Strong は要件次第で選べる)
- データ形状: KV / Document
- 寿命: Hot + Warm
- スケール: Sharding(auto)
「強整合性をデフォルトにしない」「スケールアウトをデフォルトにする」── ここが RDB との一番の違い。
Partition ─ 分散の根本原理
DynamoDB の心臓は Partition Key(PK)。
// DynamoDB の item 構造
{
PK: 'USER#123', // Partition Key
SK: 'ORDER#456', // Sort Key(オプション)
amount: 100,
createdAt: '2026-05-09T10:00:00Z',
}
PK の hash でデータが 物理ノードに分散される。同じ PK を持つ item は同一ノードに集まる。これが「一緒にアクセスされるものを一緒に置く」(Houlihan)の物理的な実装。
graph TB
C[Client] --> R[Router]
R -->|hash PK %| N1[Node 1<br/>USER#1, USER#5, ...]
R -->|hash PK %| N2[Node 2<br/>USER#2, USER#7, ...]
R -->|hash PK %| N3[Node 3<br/>USER#3, USER#9, ...]
style R fill:#e1f5ff
Sort Key で範囲クエリ
PK 内では Sort Key(SK)で範囲クエリができる。
// USER#123 の最新 10 件の注文を取る
await ddb.query({
TableName: 'app',
KeyConditionExpression: 'PK = :pk AND begins_with(SK, :sk)',
ExpressionAttributeValues: {
':pk': 'USER#123',
':sk': 'ORDER#',
},
Limit: 10,
ScanIndexForward: false,
});
PK + SK の組み合わせで、1:N の関係を表現する。SK にタイムスタンプを入れれば「最新の N 件」が取れる。これが NoSQL の典型的な設計パターン。
Hot Partition の罠
PK の選び方を間違えると、1 つのノードに書き込みが集中する。
// ❌ 全 user の今日のイベントが同じ PK
{
PK: 'EVENT#2026-05-09', // 全リクエストがここに殺到
SK: 'USER#123#TS#...',
}
DynamoDB の各 partition にはスループット上限(3000 RCU / 1000 WCU per partition)。これを超えると throttle される。
対策:
// ✅ shard suffix で分散
{
PK: 'EVENT#2026-05-09#SHARD#7', // 0-9 で分散
SK: 'USER#123#TS#...',
}
// 読み出し時は 10 partition を query して merge
これは設計編(次章)の主題の一つ。
Eventual Consistency ─ Strong を捨てる代償と恩恵
DynamoDB のデフォルトは Eventual Consistent Read。書き込み直後に読むと、古い値が返ることがある。
await ddb.put({ TableName: 'app', Item: { PK: 'USER#123', name: 'Alice' } });
// 直後の読み出し
const result = await ddb.get({ TableName: 'app', Key: { PK: 'USER#123' } });
// → 'Alice' が返るかもしれないし、古い値(または item なし)かもしれない
理由:DynamoDB は内部で 複数 AZ にレプリケーションしており、書き込みが全 AZ に伝播する前に読み出しが起きると、古い値を返す可能性がある。
Strong Consistent Read の選択
ConsistentRead: true を指定すると、強整合性が保証される:
const result = await ddb.get({
TableName: 'app',
Key: { PK: 'USER#123' },
ConsistentRead: true,
});
// → 必ず最新の値
コスト: RCU を 2 倍消費、レイテンシが上がる、AZ 障害時に読めなくなる。
判断: ほとんどの read は eventual で良い。ユーザー自身が直前に書いた直後に読み返すような場面(アバター更新後のプロフィール表示など)でだけ Strong を使う。
Conditional Write
整合性のもう一つの形:条件付き書き込み。
// 残高が 100 以上なら 100 を引く(ロックなしの atomic)
await ddb.update({
TableName: 'app',
Key: { PK: 'ACCOUNT#123' },
UpdateExpression: 'SET balance = balance - :amount',
ConditionExpression: 'balance >= :amount',
ExpressionAttributeValues: { ':amount': 100 },
});
ロックを取らずに 一貫性ある単一 item の更新 ができる。これが CAS(Compare-And-Swap)の DynamoDB 版。
Trans (TransactWriteItems)
複数 item の atomic 更新は TransactWriteItems で 最大 25 件まで。
await ddb.transactWrite({
TransactItems: [
{ Update: { /* item 1 */ } },
{ Update: { /* item 2 */ } },
{ Put: { /* item 3 */ } },
],
});
25 件の制約は、Aggregate 設計に影響する(前章までに何度か触れた)。複数 Aggregate を atomic 更新したい場合、25 件以内に収まるか、別の戦略(Saga 等)が必要。
Single Table Design と「2024 年の deprecated 発言」
NoSQL を語る上で避けて通れないのが Single Table Design (STD)。Houlihan が DynamoDB チームで生み出した設計パターン:
1 つのテーブルに、複数の異なる種類の item を全部入れる
// 同じテーブルに User、Order、Product が混在
{ PK: 'USER#123', SK: 'METADATA', name: 'Alice' }
{ PK: 'USER#123', SK: 'ORDER#456', amount: 100 }
{ PK: 'USER#123', SK: 'ORDER#789', amount: 200 }
{ PK: 'PRODUCT#abc', SK: 'METADATA', name: 'Widget' }
PK = USER#123 で query すると、その user の全 item(メタデータ + 注文)が取れる。1 回のリクエストで関連データをまとめて取得できる。
2024 年の Houlihan 発言
2024 年の今、Single Table Design の動機の多くは解決済み。25 個の GSI、on-demand pricing、Index allocation の自動化で、Index Overloading を強引に行う必要がなくなった。STD は今や deprecated と考えるべき。
つまり「シングルテーブルにする」というパターン自体は古びた。だが核心は不変:
What is accessed together should be stored together.
「アクセスパターンに従って schema を設計する」という流儀は今でも生きている。シングルテーブルかマルチテーブルかは結果であって、起点ではない。
NoSQL の本質を 3 つで言うなら
graph TB
A[Partition Key で分散] -->|無限スケール| Z[Web スケールの<br/>NoSQL]
B[Eventual Consistency] -->|低レイテンシ| Z
C[アクセスパターン駆動] -->|クエリ最適化| Z
style Z fill:#e1ffe1
これらが揃うことで、RDB が苦手な write スループット・地理分散・自動スケールを実現する。代償は Strong consistency をデフォルトで失うこと、スキーマレスゆえの設計の難しさ。
ドメイン駆動との緊張
NoSQL は最も ドメイン駆動と緊張する DB タイプ。
| 観点 | DDD の流儀 | NoSQL の流儀 |
|---|---|---|
| 起点 | Aggregate | アクセスパターン |
| 整合性 | Aggregate 単位 | Item 単位 |
| Repository | 抽象化可能 | 抽象化が難しい |
| 進化 | リファクタ可能 | アクセスパターン変更で再設計 |
DynamoDB で Repository パターンを綺麗に適用しようとすると、しばしば NoSQL の利点を捨てることになる。これは第 16 章「共通基盤」と第 18 章「両派を行き来する」で深掘りする。
この章の要点
- NoSQL は強整合性を捨てて分散・スケールを取った
- Partition Key が分散の根本。Hot Partition を避ける設計が必須
- Eventual Consistency がデフォルト、Strong は明示選択
- Conditional Write / TransactWriteItems で限定的な atomic 操作
- Single Table Design は 2024 年に Houlihan 自身が deprecated 発言。だがアクセスパターン駆動の核心は不変
- DDD と最も緊張する DB タイプ
次章への問いかけ
「シングルテーブルが deprecated になっても残る教え」── 一緒にアクセスされるものは一緒に置く。
次章で NoSQL の設計編。アクセスパターン列挙・Hot partition 回避・GSI / LSI の選択・schema vs index の判断を扱う。