目次を表示する

DB 設計の軸 2026 ─ ドメイン駆動と特性駆動の二つの流派を行き来する 19 章

KV / Document NoSQL の本質 ─ Partition と Eventual Consistency

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 発言

Houlihan 自身の 2024 年のツイート

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 の判断を扱う。