ストーリー
NoSQLの4大カテゴリ
| カテゴリ | 代表的なDB | データモデル | 主なユースケース |
|---|---|---|---|
| ドキュメント | MongoDB, DynamoDB | JSON/BSON ドキュメント | CMS、ユーザープロフィール |
| Key-Value | Redis, 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 | 転置インデックス、スコアリング |
| トランザクション処理 | PostgreSQL | ACID保証が必要な場合 |
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-Value | TTL、Sorted Set等の構造を活用 |
| カラムファミリー | クエリファーストでテーブル設計 |
| グラフDB | ノードとエッジで複雑な関係性を表現 |
チェックリスト
- 4種類のNoSQLカテゴリと適切なユースケースを説明できる
- ドキュメントDBの埋め込み vs 参照の判断基準を理解した
- DynamoDB シングルテーブル設計の考え方を理解した
- Polyglot Persistence の概念を理解した
次のステップへ
次はイベントソーシングとCQRSパターンを学び、イベント中心のデータモデリングを理解します。
推定読了時間: 30分