DynamoDBの設計パターンで性能を引き出す:スケーラブルなアプリ構築の実践

DynamoDBを最適に活用するには、単にテーブルを作成するだけでは不十分です。この記事では、パーティションキー設計、ソートキー活用、GSI(Global Secondary Index)戦略など、実務で即座に応用できる5つの設計パターンと、ハマりやすいポイントの回避方法を解説します。

DynamoDBの性能は設計で決まる理由

DynamoDBはAWSの完全マネージドNoSQLデータベースで、スケーラビリティとパフォーマンスが特徴です。しかし、設計が不適切だと以下の問題が生じます:

  • ホットパーティションによるスロットリング
  • 予期しない高額な課金
  • クエリパフォーマンスの急激な低下
  • スケーリング戦略の失敗

DynamoDBはプロビジョニングキャパシティまたはオンデマンドプライシングのいずれかで運用しますが、どちらでも設計の良さが直結します。

パターン1:パーティションキー設計で均等分散を実現

ホットパーティション問題の回避

DynamoDBはパーティションキーのハッシュ値に基づいてデータを分散させます。パーティションキーの選択が悪いと、特定のキー値にアクセスが集中し、スロットリングが発生します。

❌ 悪い例:ユーザーIDが1〜1000の範囲で、IDが若いユーザーほどアクティブな場合、user_id を直接パーティションキーにするとID 1-100に負荷が集中します。

✅ 良い例:ユーザーIDの前に環境識別子やテナントIDを付与するか、複合キー戦略を採用します。

// テーブル作成時の設定例(AWS SDK for JavaScript)
const params = {
  TableName: 'Users',
  KeySchema: [
    { AttributeName: 'tenant_id', KeyType: 'HASH' },    // パーティションキー
    { AttributeName: 'user_id', KeyType: 'RANGE' }      // ソートキー
  ],
  AttributeDefinitions: [
    { AttributeName: 'tenant_id', AttributeType: 'S' },
    { AttributeName: 'user_id', AttributeType: 'S' }
  ],
  BillingMode: 'PAY_PER_REQUEST'  // オンデマンドプライシング
};

dynamodb.createTable(params, (err, data) => {
  if (err) console.log('Error:', err);
  else console.log('Table created:', data);
});
  

均等分散のためのベストプラクティス

  • 高カーディナリティを選択:可能な値の種類が多いフィールドをパーティションキーにする
  • 複合キーを活用:テナントID + ユーザーIDのような組み合わせで、アクセスパターンに合わせて分散させる
  • ランダム化を検討:タイムスタンプやUUIDの一部を含めるケースもある

パターン2:ソートキーを活用した効率的なクエリ

範囲クエリの設計

ソートキーを適切に設計すると、単一キーの取得(GetItem)ではなく、効率的なquery操作が可能になります。

// テーブル設計例:注文履歴の管理
const params = {
  TableName: 'Orders',
  KeySchema: [
    { AttributeName: 'customer_id', KeyType: 'HASH' },
    { AttributeName: 'order_date#order_id', KeyType: 'RANGE' }
  ]
};

// クエリ例:特定顧客の過去30日分の注文を取得
const queryParams = {
  TableName: 'Orders',
  KeyConditionExpression: 'customer_id = :cid AND order_date#order_id BETWEEN :start AND :end',
  ExpressionAttributeValues: {
    ':cid': 'CUST#12345',
    ':start': '2025-01-01#',
    ':end': '2025-01-31#ZZZ'  // ZZZはソートキーの大小比較で最後になるようにしている
  }
};

dynamodb.query(queryParams, (err, data) => {
  if (err) console.log('Error:', err);
  else console.log('Orders:', data.Items);
});
  

ソートキー設計の注意点

  • 日付を含める場合は「YYYY-MM-DD」形式で辞書順ソートと時間順ソートを一致させる
  • 複数の値を結合する場合は、区切り文字(#など)を明示的に使用して可視性を上げる
  • SparseIndex を活用する場合、ソートキーが存在しないデータはインデックスに含まれないことを理解する

パターン3:GSI設計で複数のアクセスパターンに対応

複数のクエリ方式を同一テーブルで実現

DynamoDBは単一の主キー構造を持つため、異なるアクセスパターンに対応するにはGSI(Global Secondary Index)が必須です。

// テーブル設計例:商品検索
const params = {
  TableName: 'Products',
  KeySchema: [
    { AttributeName: 'product_id', KeyType: 'HASH' }
  ],
  GlobalSecondaryIndexes: [
    {
      IndexName: 'category_popularity_index',
      KeySchema: [
        { AttributeName: 'category', KeyType: 'HASH' },
        { AttributeName: 'popularity_score', KeyType: 'RANGE' }
      ],
      Projection: {
        ProjectionType: 'INCLUDE',
        NonKeyAttributes: ['product_name', 'price', 'image_url']
      },
      BillingMode: 'PAY_PER_REQUEST'
    },
    {
      IndexName: 'brand_price_index',
      KeySchema: [
        { AttributeName: 'brand', KeyType: 'HASH' },
        { AttributeName: 'price', KeyType: 'RANGE' }
      ],
      Projection: {
        ProjectionType: 'ALL'  // すべての属性を返す場合
      },
      BillingMode: 'PAY_PER_REQUEST'
    }
  ]
};

// GSIを使ったクエリ例:カテゴリ内で人気度が高い商品を取得
const queryParams = {
  TableName: 'Products',
  IndexName: 'category_popularity_index',
  KeyConditionExpression: 'category = :cat AND popularity_score > :score',
  ExpressionAttributeValues: {
    ':cat': 'Electronics',
    ':score': 80
  },
  ScanIndexForward: false  // 降順(人気が高い順)
};

dynamodb.query(queryParams, (err, data) => {
  if (err) console.log('Error:', err);
  else console.log('Popular products:', data.Items);
});
  

GSI設計の重要なポイント

  • ProjectionType を検討:ALL は安全だが課金が増加、INCLUDE は必要な属性のみ指定して効率化、KEYS_ONLY は最小限の属性のみ
  • GSIのスロットリングは独立:メインテーブルがスロットルされていなくても、GSIだけスロットルされることがある
  • GSIは最大10個:アクセスパターンが多すぎる場合、クエリロジックの見直しを検討する
  • スパースインデックスの活用:特定の属性が存在するレコードのみインデックスしたい場合、その属性を主キーに含める

パターン4:オンデマンドプライシングとプロビジョニングの使い分け

コスト最適化の判断基準

DynamoDBは2つの課金モデルから選択できます。設計段階で決定することは極めて重要です。

// テーブル作成時に課金モデルを選択
// オンデマンドプライシング:トラフィックが予測不可能な場合
const onDemandParams = {
  TableName: 'EventLog',
  KeySchema: [
    { AttributeName: 'event_id', KeyType: 'HASH' }
  ],
  AttributeDefinitions: [
    { AttributeName: 'event_id', AttributeType: 'S' }
  ],
  BillingMode: 'PAY_PER_REQUEST'
};

// プロビジョニング:トラフィックが安定している場合
const provisionedParams = {
  TableName: 'UserProfile',
  KeySchema: [
    { AttributeName: 'user_id', KeyType: 'HASH' }
  ],
  AttributeDefinitions: [
    { AttributeName: 'user_id', AttributeType: 'S' }
  ],
  BillingMode: 'PROVISIONED',
  ProvisionedThroughput: {
    ReadCapacityUnits: 100,
    WriteCapacityUnits: 50
  }
};
  
モデル 向いている用途 向かない用途
オンデマンド トラフィックが不安定、突発的なスパイク、開発段階 一貫して高トラフィック(コスト増加の可能性)
プロビジョニング 安定したトラフィック、コスト予測可能、高スケール トラフィックが不規則、オートスケーリング設定必須

パターン5:シングルテーブル設計で複雑なアクセスパターンに対応

1つのテーブルで複数のエンティティを管理

最近のベストプラクティスとして、複数のエンティティ型を1つのテーブルに格納する「シングルテーブル設計」が注目されています。これにより、複雑なクエリやトランザクション処理が簡潔になります。

// シングルテーブル設計の例:SaaS プラットフォーム
const singleTableParams = {
  TableName: 'SaaSPlatform',
  KeySchema: [
    { AttributeName: 'PK', KeyType: 'HASH' },
    { AttributeName: 'SK', KeyType: 'RANGE' }
  ],
  AttributeDefinitions: [
    { AttributeName: 'PK', AttributeType: 'S' },
    { AttributeName: 'SK', AttributeType: 'S' },
    { AttributeName: 'GSI1PK', AttributeType: 'S' },
    { AttributeName: 'GSI1SK', AttributeType: 'S' }
  ],
  GlobalSecondaryIndexes: [
    {
      IndexName: 'GSI1',
      KeySchema: [
        { AttributeName: 'GSI1PK', KeyType: 'HASH' },
        { AttributeName: 'GSI1SK', KeyType: 'RANGE' }
      ],
      Projection: { ProjectionType: 'ALL' },
      BillingMode: 'PAY_PER_REQUEST'
    }
  ],
  BillingMode: 'PAY_PER_REQUEST'
};

// データ構造例
// 組織マスタ:PK = "ORG#org123", SK = "METADATA"
// ユーザー:PK = "ORG#org123", SK = "USER#user456"
// 請求書:PK = "ORG#org123", SK = "INVOICE#inv789"
// インデックス活用:GSI1PK = "USER#user456", GSI1SK = "INVOICE#inv789" で、ユーザーが作成した請求書を検索

const putItemParams = {
  TableName: 'SaaSPlatform',
  Item: {
    PK: { S: 'ORG#org123' },
    SK: { S: 'USER#user456' },
    GSI1PK: { S: 'USER#user456' },
    GSI1SK: { S: 'CREATED_DATE#2025-01-15' },
    email: { S: 'user@example.com' },
    name: { S: 'John Doe' },
    created_at: { N: '1705276800' },
    entity_type: { S: 'USER' }
  }
};

dynamodb.putItem(putItemParams, (err, data) => {
  if (err) console
    
K
AWS・Python・生成AIを専門とするソフトウェアエンジニア。AI・クラウド・開発ワークフローの実践ガイドを執筆しています。詳しく見る →