⚖️

DynamoDB ProjectionExpressionの設計パターン:パフォーマンスと保守性のトレードオフ

に公開

DynamoDBを使った開発で、ProjectionExpressionをどう設計するか悩んだことはありませんか?

複数のAPIが同じテーブルを参照する際、「各APIに最適化したクエリを書くべきか」「共通のクエリで統一するべきか」という判断に迫られることがあります。

この記事では、実際の開発で遭遇するこのトレードオフについて、具体例を交えて整理してみます。

問題設定

2つのAPIが同じDynamoDBテーブルを参照し、部分集合的な属性要件を持つケースを考えてみましょう。

// API A: 基本情報のみ必要
// 必要属性: userId, createdAt, monthlyUsage (3属性)

// API B: 詳細分析用
// 必要属性: userId, createdAt, monthlyUsage, userStatus, planType (5属性)

この場合、どのような設計パターンが考えられるでしょうか?

2つのアプローチ

パターン1: 分離アプローチ(パフォーマンス重視)

各APIに最適化されたクエリを作成するアプローチです。

// API A用:軽量なクエリ
async getBasicUserInfo(userId: string) {
  const command = new QueryCommand({
    TableName: 'Users',
    ProjectionExpression: 'userId, createdAt, monthlyUsage',
    // ... その他のパラメータ
  });
  return await client.send(command);
}

// API B用:詳細なクエリ
async getDetailedUserInfo(userId: string) {
  const command = new QueryCommand({
    TableName: 'Users',
    ProjectionExpression: 'userId, createdAt, monthlyUsage, userStatus, planType',
    // ... その他のパラメータ
  });
  return await client.send(command);
}

メリット

  • パフォーマンスの最適化: データ転送量を最適化
  • 責任の明確化: 各APIの要件が明示的

デメリット

  • 保守コスト: 変更時に複数箇所の修正が必要
  • テスト負荷: それぞれのクエリに対するテストが必要

パターン2: 統一アプローチ(保守性重視)

和集合の属性を取得する共通クエリを使用するアプローチです。

// 共通のクエリメソッド
async getUserData(userId: string) {
  const command = new QueryCommand({
    TableName: 'Users',
    // 両APIで必要な属性の和集合
    ProjectionExpression: 'userId, createdAt, monthlyUsage, userStatus, planType',
    // ... その他のパラメータ
  });
  return await client.send(command);
}

// 各APIはこの共通メソッドを使用
export const apiA = async (userId: string) => {
  const data = await getUserData(userId);
  // 必要な属性のみ使用(余分な属性は無視)
  return data.map(item => ({
    userId: item.userId,
    createdAt: item.createdAt,
    monthlyUsage: item.monthlyUsage
  }));
};

export const apiB = async (userId: string) => {
  const data = await getUserData(userId);
  // 全属性を使用
  return data;
};

メリット・デメリット

パターン1の逆

判断基準

このトレードオフをどう判断すべきでしょうか?以下の要因を考慮して決めることをお勧めします。

分離アプローチを選ぶべき場合

パフォーマンスが重要な場合

  • 大容量データ: 不要な属性のデータサイズが大きい
  • 高頻度API: 一方のAPIが非常に頻繁に呼ばれる
  • ネットワーク制約: 帯域幅が限られた環境

// 例:商品テーブル
// API A(一覧): id, name, price (1KB)
// API B(詳細): id, name, price, description, images (10KB)
//
// API Aが毎秒1000リクエスト、API Bが毎秒10リクエストの場合
// 統一アプローチだと 9KB × 1000req/s = 9MB/s の無駄な転送

統一アプローチを選ぶべき場合

保守性が重要な場合

  • 高い重複率: 共通属性が多い
  • 開発効率重視: 少人数チームや短期開発
  • 小容量データ: 余分なデータのコストが小さい
  • 要件変更頻度: 属性要件が頻繁に変わる

// 例:ユーザーテーブル
// API A(プロフィール): id, name, email, avatar (2KB)
// API B(設定画面): id, name, email, avatar, preferences (2.5KB)
//
// 重複率80%、追加0.5KBの差は許容範囲
// → 統一アプローチが適切

実践的なガイドライン

実際の開発では、以下の順序で検討することをお勧めします:

ステップ1: デフォルトは統一

// まず統一アプローチで実装
async getUserData(userId: string) {
  // 必要な属性の和集合で実装
}

理由:

  • 開発速度が早い
  • 要件が固まるまでの変更に強い
  • 過度な最適化を避けられる

ステップ2: パフォーマンス測定

Google Chromeのデベロッパーツール等でAPIのレスポンス速度を測定します。

ステップ3: 必要に応じて分離

パフォーマンス問題が確認されたら分離を検討します。

// 高頻度APIは最適化
async getBasicUserInfo(userId: string) {
  // 最小限の属性のみ
}

// 低頻度APIは従来通り
async getDetailedUserInfo(userId: string) {
  // 詳細な属性を含む
}

まとめ

DynamoDBのProjectionExpressionの設計は、技術的制約とビジネス要件のバランスを取るエンジニアリング判断の典型例です。

迷った時の指針

  1. 開発初期: 統一アプローチで素早く実装
  2. パフォーマンス測定: 実際のデータでパフォーマンスを確認
  3. 最適化判断: 必要に応じて分離アプローチに移行

完璧な設計を最初から目指すよりも、段階的に改善していくアプローチが実践的で効果的です。

プロジェクトの成長と要件の変化に応じて、柔軟に見直していきましょう!

Discussion