EXERCISE 60分

ストーリー

新しいプロジェクトが立ち上がった。高橋アーキテクトが設計を任せてくれた。

「今回は4つのシナリオがある。それぞれ最適なデータストアを選び、モデルを設計してくれ。選定理由も明確に言語化するんだ。間違った選択をしても構わないが、根拠なき選択だけは許さない」


ミッション概要

項目内容
目標4つのシナリオに対して最適なデータストアとモデルを設計
評価基準データストアの選定理由、モデルの適切さ、アクセスパターンの考慮
制限時間60分

Mission 1: リアルタイムチャットアプリ

要件

  • 1対1チャットとグループチャット(最大100人)
  • メッセージの送受信は1秒以内のレイテンシ
  • 既読状態の管理
  • メッセージ検索機能
  • 過去メッセージの無限スクロール
  • 1日100万メッセージの書き込み

課題: データストアの選定、メッセージとチャットルームのデータモデルを設計せよ。

ヒント

リアルタイム性が求められる部分とメッセージの永続化で異なるストアを使い分けることを検討しよう。

回答例

選定:

  • メッセージ永続化: MongoDB(柔軟なスキーマ、大量書き込み)
  • リアルタイム配信: Redis Pub/Sub(低レイテンシ通知)
  • 既読管理: Redis Hash(高頻度更新)

MongoDBモデル:

// チャットルーム
{
  _id: ObjectId("room1"),
  type: "group",              // "direct" | "group"
  name: "開発チーム",
  members: [
    { userId: "user1", joinedAt: ISODate("2024-01-01"), role: "admin" },
    { userId: "user2", joinedAt: ISODate("2024-01-02"), role: "member" }
  ],
  lastMessage: {
    body: "了解です!",
    senderId: "user2",
    sentAt: ISODate("2024-03-15T10:30:00Z")
  },
  createdAt: ISODate("2024-01-01")
}

// メッセージ
{
  _id: ObjectId("msg1"),
  roomId: ObjectId("room1"),
  senderId: "user1",
  body: "ミーティング開始します",
  type: "text",               // "text" | "image" | "file"
  sentAt: ISODate("2024-03-15T10:00:00Z")
}

// インデックス
db.messages.createIndex({ roomId: 1, sentAt: -1 });
db.messages.createIndex({ body: "text" });  // 全文検索
db.rooms.createIndex({ "members.userId": 1 });

Redis(既読管理):

HSET read:room1:user1 lastRead "2024-03-15T10:30:00Z"
HSET read:room1:user2 lastRead "2024-03-15T10:25:00Z"

選定理由: メッセージは構造が柔軟(テキスト、画像、ファイルなど)でJOINが少なく、大量書き込みが必要なためMongoDB。リアルタイム性はRedis Pub/Subで担保。


Mission 2: ECサイトの商品カタログ

要件

  • 10万点の商品、50カテゴリ
  • カテゴリごとに属性が異なる(服: サイズ/色、PC: CPU/メモリ/ストレージ)
  • 属性によるフィルター検索
  • 商品ページは秒間1万リクエスト
  • 在庫管理と注文処理もある

課題: 商品カタログと在庫・注文をどのデータストアで管理するか設計せよ。

ヒント

商品カタログと注文処理で求められる特性が異なることに注目しよう。

回答例

選定:

  • 商品カタログ: MongoDB(柔軟な属性スキーマ)+ Redis(キャッシュ)
  • 在庫・注文: PostgreSQL(トランザクション必須)

MongoDBモデル(商品カタログ):

// 商品(カテゴリによって属性が異なる)
{
  _id: ObjectId("prod1"),
  name: "TypeScript入門Tシャツ",
  category: "clothing",
  price: 3500,
  images: ["/img/tshirt1.jpg", "/img/tshirt2.jpg"],
  attributes: {
    size: ["S", "M", "L", "XL"],
    color: ["white", "black", "navy"],
    material: "cotton"
  },
  description: "...",
  rating: { average: 4.5, count: 128 }
}

{
  _id: ObjectId("prod2"),
  name: "ゲーミングPC X1",
  category: "electronics",
  price: 250000,
  images: ["/img/pc1.jpg"],
  attributes: {
    cpu: "Intel i9-14900K",
    memory: "64GB DDR5",
    storage: "2TB NVMe SSD",
    gpu: "RTX 4090"
  },
  description: "...",
  rating: { average: 4.8, count: 42 }
}

// インデックス
db.products.createIndex({ category: 1, price: 1 });
db.products.createIndex({ "attributes.size": 1 });
db.products.createIndex({ "attributes.cpu": 1 });

PostgreSQL(在庫・注文):

CREATE TABLE inventory (
  product_id VARCHAR(24) PRIMARY KEY,
  stock INT NOT NULL CHECK (stock >= 0),
  reserved INT NOT NULL DEFAULT 0
);

CREATE TABLE orders (
  id SERIAL PRIMARY KEY,
  user_id INT NOT NULL,
  status VARCHAR(20) NOT NULL DEFAULT 'pending',
  total DECIMAL(12,2) NOT NULL,
  created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

選定理由: 商品属性がカテゴリにより異なるためMongoDBが適切。在庫と注文はACIDトランザクションが必須なためPostgreSQL。


Mission 3: ソーシャルグラフ

要件

  • 100万ユーザーのフォロー関係
  • 「友達の友達」の推薦機能
  • 「共通のフォロワー」の表示
  • フォロー数・フォロワー数のリアルタイム表示

課題: フォロー関係と推薦機能のデータモデルを設計せよ。

ヒント

「友達の友達」のような多段の関係探索に強いデータストアを検討しよう。

回答例

選定:

  • フォロー関係・推薦: Neo4j(グラフ探索に特化)
  • フォロー数カウンター: Redis(高頻度更新)

Neo4jモデル:

// ノード: ユーザー
CREATE (u:User {id: "user1", name: "田中太郎"})
CREATE (u:User {id: "user2", name: "佐藤花子"})

// リレーション: フォロー
CREATE (u1)-[:FOLLOWS {since: datetime("2024-01-15")}]->(u2)

// フォロワー一覧
MATCH (u:User {id: "user1"})<-[:FOLLOWS]-(follower)
RETURN follower.name

// 友達の友達(推薦)
MATCH (me:User {id: "user1"})-[:FOLLOWS]->(friend)-[:FOLLOWS]->(suggestion)
WHERE NOT (me)-[:FOLLOWS]->(suggestion) AND suggestion <> me
RETURN suggestion.name, COUNT(friend) AS mutual_friends
ORDER BY mutual_friends DESC
LIMIT 10

// 共通のフォロワー
MATCH (u1:User {id: "user1"})<-[:FOLLOWS]-(common)-[:FOLLOWS]->(u2:User {id: "user2"})
RETURN common.name

Redis(カウンター):

HSET user:user1:counts followers 1500 following 300
HINCRBY user:user1:counts followers 1  // フォローされた

選定理由: 「友達の友達」のような多段の関係トラバーサルはグラフDBが圧倒的に高速(RDBでは再帰CTE + 大量JOINが必要)。カウンターは高頻度更新のためRedis。


Mission 4: IoTセンサーデータ

要件

  • 1万台のセンサーが10秒ごとにデータ送信
  • データ項目: デバイスID、温度、湿度、気圧、タイムスタンプ
  • 直近1時間のリアルタイムダッシュボード
  • 日次・月次の集計レポート
  • データは3年間保持

課題: センサーデータの格納と集計のデータモデルを設計せよ。

ヒント

大量の時系列データの書き込みに強く、集計が効率的なデータストアを検討しよう。

回答例

選定:

  • センサーデータ: TimescaleDB(PostgreSQL拡張、時系列特化)or Cassandra
  • リアルタイムダッシュボード: Redis(直近データのキャッシュ)

TimescaleDBモデル:

-- ハイパーテーブル(自動パーティショニング)
CREATE TABLE sensor_readings (
  time TIMESTAMPTZ NOT NULL,
  device_id VARCHAR(50) NOT NULL,
  temperature DECIMAL(5,2),
  humidity DECIMAL(5,2),
  pressure DECIMAL(7,2)
);

SELECT create_hypertable('sensor_readings', 'time');

-- インデックス
CREATE INDEX idx_readings_device_time
  ON sensor_readings(device_id, time DESC);

-- 連続集計(自動集計)
CREATE MATERIALIZED VIEW hourly_stats
WITH (timescaledb.continuous) AS
SELECT
  time_bucket('1 hour', time) AS bucket,
  device_id,
  AVG(temperature) AS avg_temp,
  MAX(temperature) AS max_temp,
  MIN(temperature) AS min_temp,
  AVG(humidity) AS avg_humidity
FROM sensor_readings
GROUP BY bucket, device_id;

-- データ保持ポリシー(3年)
SELECT add_retention_policy('sensor_readings', INTERVAL '3 years');

Redis(リアルタイム):

// 最新データをRedisに保持
await redis.hset(`sensor:device001:latest`, {
  temperature: '25.3',
  humidity: '60.1',
  pressure: '1013.2',
  timestamp: new Date().toISOString(),
});
await redis.expire(`sensor:device001:latest`, 120);

選定理由: 時系列データの大量書き込みと時間範囲での集計クエリに特化したTimescaleDBが最適。直近データはRedisで低レイテンシアクセス。書き込み規模が極めて大きい場合はCassandraも選択肢。

書き込み規模:

  • 1万台 x 6回/分 = 6万回/分 = 1000回/秒
  • 1日: 8,640万行
  • 3年: 約946億行

達成チェックリスト

  • Mission 1: チャットアプリの設計でMongoDBとRedisの使い分けができた
  • Mission 2: ECサイトでRDBとNoSQLの併用を設計できた
  • Mission 3: グラフDBの適用場面を理解し設計できた
  • Mission 4: 時系列データに適したストアを選定し設計できた

推定所要時間: 60分