LESSON 30分

ストーリー

佐藤CTO
RDBだけでは全てのデータ要件を満たせない
佐藤CTO
NoSQLは”銀の弾丸”ではないが、適切な場面で使えば劇的な改善をもたらす。重要なのは、どのNoSQLカテゴリがどのアクセスパターンに最適かを見極める力だ

NoSQLの4大カテゴリ

カテゴリ代表的なDBデータモデル主なユースケース
ドキュメントMongoDB, DynamoDBJSON/BSON ドキュメントCMS、ユーザープロフィール
Key-ValueRedis, Memcachedキーと値のペアキャッシュ、セッション管理
カラムファミリーCassandra, HBaseカラム指向ストレージ時系列データ、IoT
グラフNeo4j, Neptuneノード + エッジソーシャルネットワーク、推薦

ドキュメントモデリング

埋め込み vs 参照

// パターン1: 埋め込み(Embedding)
// 一緒に読み取られるデータを1ドキュメントに
interface OrderDocument {
  _id: string;
  customerId: string;
  orderDate: Date;
  status: 'pending' | 'confirmed' | 'shipped' | 'delivered';
  // 注文アイテムを埋め込み(1:Few の関係)
  items: Array<{
    productId: string;
    productName: string;  // 非正規化(読み取り最適化)
    quantity: number;
    unitPrice: number;
  }>;
  // 配送先を埋め込み(1:1 の関係)
  shippingAddress: {
    postalCode: string;
    prefecture: string;
    city: string;
    line1: string;
  };
  totalAmount: number;
}

// パターン2: 参照(Referencing)
// 独立して更新されるデータは参照
interface CustomerDocument {
  _id: string;
  name: string;
  email: string;
  // 注文は参照のみ(1:Many、上限なし)
  // orderIds: string[] ← これは避ける(無限に増える)
}

// 代わりに注文側で customerId を持つ
interface OrderRef {
  _id: string;
  customerId: string;  // 参照
  // ...
}

埋め込み vs 参照の判断基準

基準埋め込み参照
カーディナリティ1
、1
1
、Many
読み取りパターン常に一緒に取得個別に取得
更新頻度親と一緒に更新独立して頻繁に更新
データサイズドキュメント16MB以内大きくなりがち

DynamoDB のモデリング(シングルテーブル設計)

// DynamoDB シングルテーブル設計
// PK(パーティションキー)+ SK(ソートキー)でアクセスパターンを表現

interface DynamoDBItem {
  PK: string;    // パーティションキー
  SK: string;    // ソートキー
  GSI1PK?: string;  // Global Secondary Index
  GSI1SK?: string;
  [key: string]: unknown;
}

// アクセスパターンに基づいた設計
const items: DynamoDBItem[] = [
  // 顧客情報
  {
    PK: 'CUSTOMER#C001',
    SK: 'PROFILE',
    name: '田中太郎',
    email: 'tanaka@example.com',
    GSI1PK: 'EMAIL#tanaka@example.com',
    GSI1SK: 'CUSTOMER#C001',
  },
  // 顧客の注文一覧(新しい順)
  {
    PK: 'CUSTOMER#C001',
    SK: 'ORDER#2024-01-15#ORD001',
    orderId: 'ORD001',
    status: 'delivered',
    totalAmount: 15000,
    GSI1PK: 'ORDER#ORD001',
    GSI1SK: 'CUSTOMER#C001',
  },
  // 注文の詳細アイテム
  {
    PK: 'ORDER#ORD001',
    SK: 'ITEM#001',
    productId: 'PROD-A',
    productName: 'ワイヤレスイヤホン',
    quantity: 1,
    unitPrice: 15000,
  },
];

// クエリパターン
// 1. 顧客プロフィール取得: PK=CUSTOMER#C001, SK=PROFILE
// 2. 顧客の注文一覧: PK=CUSTOMER#C001, SK begins_with ORDER#
// 3. 注文詳細取得: PK=ORDER#ORD001, SK begins_with ITEM#
// 4. メールで顧客検索: GSI1PK=EMAIL#tanaka@example.com

Key-Value モデリング

// Redis を使ったモデリングパターン

import Redis from 'ioredis';
const redis = new Redis();

// パターン1: セッション管理(Hash)
async function setSession(sessionId: string, userId: string): Promise<void> {
  await redis.hset(`session:${sessionId}`, {
    userId,
    loginAt: new Date().toISOString(),
    lastAccess: new Date().toISOString(),
  });
  await redis.expire(`session:${sessionId}`, 3600); // 1時間TTL
}

// パターン2: ランキング(Sorted Set)
async function updateRanking(
  productId: string,
  salesCount: number
): Promise<void> {
  await redis.zadd('ranking:daily', salesCount, productId);
}

async function getTopProducts(limit: number): Promise<string[]> {
  return redis.zrevrange('ranking:daily', 0, limit - 1);
}

// パターン3: レート制限(String + TTL)
async function checkRateLimit(
  userId: string,
  maxRequests: number,
  windowSeconds: number
): Promise<boolean> {
  const key = `ratelimit:${userId}`;
  const current = await redis.incr(key);
  if (current === 1) {
    await redis.expire(key, windowSeconds);
  }
  return current <= maxRequests;
}

カラムファミリーモデリング

# Cassandra のモデリング: クエリファーストアプローチ
# 「何を聞きたいか」からテーブルを設計する

# アクセスパターン1: 特定センサーの直近データ
CREATE_SENSOR_READINGS = """
CREATE TABLE IF NOT EXISTS sensor_readings (
    sensor_id    TEXT,
    reading_time TIMESTAMP,
    temperature  DOUBLE,
    humidity     DOUBLE,
    pressure     DOUBLE,
    PRIMARY KEY (sensor_id, reading_time)
) WITH CLUSTERING ORDER BY (reading_time DESC);
"""

# アクセスパターン2: 日付別の全センサーデータ
CREATE_DAILY_READINGS = """
CREATE TABLE IF NOT EXISTS daily_sensor_readings (
    date         DATE,
    sensor_id    TEXT,
    reading_time TIMESTAMP,
    temperature  DOUBLE,
    humidity     DOUBLE,
    PRIMARY KEY ((date), sensor_id, reading_time)
) WITH CLUSTERING ORDER BY (sensor_id ASC, reading_time DESC);
"""

# Cassandraの鉄則: 1クエリ = 1テーブル
# JOINは使えない → アクセスパターンごとにテーブルを作る
# データの重複はOK → ストレージは安い、レイテンシが重要

グラフモデリング

-- Neo4j (Cypher) でのソーシャルグラフモデリング

-- ノード作成
CREATE (u1:User {id: 'U001', name: '田中太郎', department: 'Engineering'})
CREATE (u2:User {id: 'U002', name: '佐藤花子', department: 'Product'})
CREATE (p1:Product {id: 'P001', name: 'データ分析基盤', category: 'Internal Tool'})

-- リレーション作成
CREATE (u1)-[:FOLLOWS {since: date('2024-01-01')}]->(u2)
CREATE (u1)-[:PURCHASED {date: date('2024-06-15'), amount: 50000}]->(p1)
CREATE (u2)-[:REVIEWED {rating: 5, comment: '素晴らしい'}]->(p1)

-- クエリ: 友人が購入した商品を推薦
MATCH (me:User {id: 'U001'})-[:FOLLOWS]->(friend)-[:PURCHASED]->(product)
WHERE NOT (me)-[:PURCHASED]->(product)
RETURN product.name, COUNT(friend) AS friendCount
ORDER BY friendCount DESC
LIMIT 10;

-- クエリ: 最短経路(6次の隔たり)
MATCH path = shortestPath(
  (a:User {id: 'U001'})-[:FOLLOWS*..6]-(b:User {id: 'U100'})
)
RETURN path, length(path) AS distance;

NoSQL選定マトリクス

ユースケース推奨DB理由
商品カタログMongoDB / DynamoDB柔軟なスキーマ、階層データ
セッション/キャッシュRedis超低レイテンシ、TTL対応
IoT時系列データCassandra / TimescaleDB高スループット書き込み
ソーシャルグラフNeo4j関係性の深いトラバーサル
全文検索Elasticsearch転置インデックス、スコアリング
トランザクション処理PostgreSQLACID保証が必要な場合
Polyglot Persistence の考え方

現代のアーキテクチャでは、1つのシステムで複数のデータベースを使い分ける Polyglot Persistence が一般的です。

graph TD
    App["アプリケーション"]
    App --> O & S & Ses & R

    O["注文管理<br/>PostgreSQL<br/>(ACID)"]
    S["商品検索<br/>Elasticsearch<br/>(全文検索)"]
    Ses["セッション<br/>Redis<br/>(高速)"]
    R["推薦<br/>Neo4j<br/>(グラフ)"]

    classDef app fill:#1e40af,stroke:#1e40af,color:#fff,font-weight:bold
    classDef db fill:#dbeafe,stroke:#3b82f6
    class App app
    class O,S,Ses,R db

各データストアの強みを活かしつつ、データ同期の戦略(CDC、イベント駆動)を別途設計する必要があります。


まとめ

ポイント内容
ドキュメントDB埋め込み vs 参照をカーディナリティで判断
Key-ValueTTL、Sorted Set等の構造を活用
カラムファミリークエリファーストでテーブル設計
グラフDBノードとエッジで複雑な関係性を表現

チェックリスト

  • 4種類のNoSQLカテゴリと適切なユースケースを説明できる
  • ドキュメントDBの埋め込み vs 参照の判断基準を理解した
  • DynamoDB シングルテーブル設計の考え方を理解した
  • Polyglot Persistence の概念を理解した

次のステップへ

次はイベントソーシングとCQRSパターンを学び、イベント中心のデータモデリングを理解します。


推定読了時間: 30分