DynamoDB LSI vs GSI: ユースケースで選ぶセカンダリインデックス完全ガイド

DynamoDBで『このアイテムを注文日でも検索したい』と思った瞬間、テーブル設計を見直す羽目になった経験は少なくないはずだ。プライマリキー以外の属性でクエリを実行したいとき、LSI(ローカルセカンダリインデックス)とGSI(グローバルセカンダリインデックス)のどちらを選ぶかは、後から変更できない制約も絡むため、設計段階での判断が特に重要になる。

TL;DR: LSI vs GSI 早見表

項目LSI(ローカルセカンダリインデックス)GSI(グローバルセカンダリインデックス)
パーティションキーベーステーブルと同一任意の属性を指定可能
ソートキー異なる属性を指定任意の属性を指定可能(省略可)
作成タイミングテーブル作成時のみテーブル作成後でも追加可能
整合性モデル強整合性読み取り可能結果整合性のみ
ストレージ制限パーティションあたり10GB上限制限なし(テーブル全体と同様)
スループットベーステーブルと共有独立したキャパシティ設定が可能
テーブルあたりの上限最大5個最大20個(デフォルト)

DynamoDB セカンダリインデックスの仕組み

DynamoDBのセカンダリインデックスは、ベーステーブルのデータを別のキースキーマで参照できるようにした、内部的には独立したデータ構造だ。インデックスへの書き込みはDynamoDBが自動的に管理する非同期プロセスで、ベーステーブルへの書き込みが成功した後にインデックスへ反映される。LSIの場合はこの反映が同一パーティション内で行われるため強整合性読み取りをサポートできるが、GSIは非同期レプリケーションのため結果整合性のみとなる。

インデックスに射影(Projection)する属性の選択も重要だ。KEYS_ONLYINCLUDEALLの3種類があり、射影する属性が多いほどストレージコストと書き込みコストが増加する。クエリで必要な属性だけを射影するのが基本的な最適化方針になる。

sequenceDiagram participant App as アプリケーション participant DDB as DynamoDB
ベーステーブル participant LSI as LSI participant GSI as GSI App->>DDB: PutItem / UpdateItem DDB-->>App: 成功レスポンス DDB->>LSI: 同期更新(同一パーティション内) Note over DDB,LSI: 強整合性読み取り可能 DDB-->>GSI: 非同期レプリケーション Note over DDB,GSI: 結果整合性のみ App->>LSI: Query(consistent-read 可) App->>GSI: Query(結果整合性)
  1. ベーステーブルへの書き込み: アプリケーションがPutItem/UpdateItemを実行する。
  2. LSI同期: 同一パーティション内でインデックスが更新されるため、強整合性読み取りが可能。
  3. GSI非同期レプリケーション: ベーステーブルの書き込み成功後、GSIへの反映は非同期で行われる。この短い遅延ウィンドウ中はGSIから古いデータが返る可能性がある。
  4. クエリ実行: LSIクエリはベーステーブルと同じパーティションキーを使用。GSIクエリは任意のパーティションキーで全テーブルを横断できる。

LSI(ローカルセカンダリインデックス)の詳細

LSIは『同じパーティションキーで、別のソートキーでも並べ替えたい』という要件に応える。例えば、userIdをパーティションキーとするユーザーの注文テーブルで、デフォルトのソートキーがorderIdだとする。注文日時(orderDate)でも範囲クエリしたい場合、LSIでorderDateをソートキーとするインデックスを追加できる。

LSIをライブラリの棚に例えると、同じ棚(パーティション)の本を著者順に並べ直したもの。別の棚を参照するわけではなく、同じ棚の中での別の並び順だ。

最も重要な制約: LSIはテーブル作成後に追加・削除できない。また、LSIを持つテーブルでは、同一パーティションキー値を持つすべてのアイテムの合計サイズが10GBを超えることができない。この制限はLSIなしのテーブルには適用されない。

LSI作成例(テーブル作成時)

aws dynamodb create-table \
  --table-name Orders \
  --attribute-definitions \
    AttributeName=userId,AttributeType=S \
    AttributeName=orderId,AttributeType=S \
    AttributeName=orderDate,AttributeType=S \
  --key-schema \
    AttributeName=userId,KeyType=HASH \
    AttributeName=orderId,KeyType=RANGE \
  --local-secondary-indexes \
    '[{
      "IndexName": "OrderDateIndex",
      "KeySchema": [
        {"AttributeName": "userId", "KeyType": "HASH"},
        {"AttributeName": "orderDate", "KeyType": "RANGE"}
      ],
      "Projection": {
        "ProjectionType": "INCLUDE",
        "NonKeyAttributes": ["status", "totalAmount"]
      }
    }]' \
  --billing-mode PAY_PER_REQUEST \
  --region us-east-1

LSIを使ったクエリ

aws dynamodb query \
  --table-name Orders \
  --index-name OrderDateIndex \
  --key-condition-expression 'userId = :uid AND orderDate BETWEEN :start AND :end' \
  --expression-attribute-values \
    '{":uid": {"S": "user-001"}, ":start": {"S": "2024-01-01"}, ":end": {"S": "2024-03-31"}}' \
  --consistent-read \
  --region us-east-1

--consistent-readフラグはLSIでのみ有効だ。GSIに対して同じフラグを指定するとエラーになる。

GSI(グローバルセカンダリインデックス)の詳細

GSIはパーティションキー自体を変えられる。これにより、テーブル全体を別のアクセスパターンで横断的に検索できる。先ほどの注文テーブルで、status(処理中/完了/キャンセル)をパーティションキーとするGSIを作れば、全ユーザーの処理中注文を一度のクエリで取得できる。

GSIはテーブル作成後でも追加・削除できる。ただし、GSIのバックフィル(既存データのインデックス構築)には時間がかかり、その間テーブルのスループットに影響が出る場合がある。大規模テーブルへのGSI追加はオフピーク時に実施するのが現実的な運用判断だ。

GSI作成例(既存テーブルへの追加)

aws dynamodb update-table \
  --table-name Orders \
  --attribute-definitions \
    AttributeName=status,AttributeType=S \
    AttributeName=orderDate,AttributeType=S \
  --global-secondary-index-updates \
    '[{
      "Create": {
        "IndexName": "StatusDateIndex",
        "KeySchema": [
          {"AttributeName": "status", "KeyType": "HASH"},
          {"AttributeName": "orderDate", "KeyType": "RANGE"}
        ],
        "Projection": {
          "ProjectionType": "INCLUDE",
          "NonKeyAttributes": ["userId", "totalAmount"]
        }
      }
    }]' \
  --region us-east-1

GSIを使ったクエリ

aws dynamodb query \
  --table-name Orders \
  --index-name StatusDateIndex \
  --key-condition-expression '#s = :status AND orderDate >= :since' \
  --expression-attribute-names '{"#s": "status"}' \
  --expression-attribute-values \
    '{":status": {"S": "PROCESSING"}, ":since": {"S": "2024-01-01"}}' \
  --region us-east-1

statusはDynamoDBの予約語ではないが、将来の安全性のため#sのような式属性名を使う習慣をつけておくと良い。

GSIのスパースインデックス活用

GSIの強力なパターンの一つがスパースインデックスだ。GSIのキー属性がアイテムに存在しない場合、そのアイテムはGSIに含まれない。例えばisUrgentという属性を持つ注文だけをGSIに含めることで、緊急注文のみを効率的にクエリできる。全アイテムをスキャンする代わりに、GSIのサイズ自体を小さく保てる。

graph TD A["アイテム書き込み"] --> B{"isUrgent属性
が存在するか?"} B -- "Yes" --> C["GSIに含まれる
(スパースインデックス)"] B -- "No" --> D["GSIに含まれない"] C --> E["緊急注文クエリで
直接ヒット"] D --> F["GSIクエリでは
返されない"]
  1. 通常注文: isUrgent属性なし → GSIに含まれない。
  2. 緊急注文: isUrgent = true → GSIに含まれる。
  3. クエリ効率: GSIには緊急注文のみ存在するため、フィルタリングなしで直接取得できる。

LSI vs GSI: どちらを選ぶべきか

graph TD Start(["別属性でクエリしたい"]) --> Q1{"パーティションキーを
変える必要があるか?"} Q1 -- "No
同じPKで別ソートキー" --> Q2{"強整合性が
必要か?"} Q1 -- "Yes
別PKでテーブル横断" --> GSI["GSI を選択"] Q2 -- "Yes" --> Q3{"テーブルは
まだ作成前か?"} Q2 -- "No" --> Q4{"テーブルは
まだ作成前か?"} Q3 -- "Yes" --> LSI["LSI を選択"] Q3 -- "No" --> Redesign["テーブル再設計が必要
LSIは後から追加不可"] Q4 -- "Yes" --> LSI2["LSI を選択
(後からGSIも追加可)"] Q4 -- "No" --> GSI2["GSI を選択"]

判断の出発点は『パーティションキーを変える必要があるか』だ。同じパーティションキーの中で別の並び順が必要なだけならLSI。別のパーティションキーでテーブルを横断したいならGSI一択になる。

強整合性が必要な場合はLSIしか選択肢がない。ただし、LSIはテーブル作成後に追加できないため、将来のアクセスパターンを事前に予測できない場合はGSIの方が柔軟性が高い。

実際の障害パターン: GSIのスロットリングを見逃した話

本番環境でよくあるのが、ベーステーブルのメトリクスは正常なのにアプリケーションが断続的にProvisionedThroughputExceededExceptionを返すケースだ。最初はアプリ側のリトライロジックを疑い、次にベーステーブルのRCU/WCUを確認する。どちらも問題なさそうに見える。

実際の原因はGSIのキャパシティ不足だった。プロビジョニングモードのテーブルでは、GSIのキャパシティはベーステーブルとは独立して設定する必要がある。GSIへの書き込みはベーステーブルへの書き込みと別にWCUを消費するため、ベーステーブルのWCUが十分でもGSIがスロットリングされる。

GSIのキャパシティを確認するには:

aws dynamodb describe-table \
  --table-name Orders \
  --query 'Table.GlobalSecondaryIndexes[*].{IndexName:IndexName,ReadCapacity:ProvisionedThroughput.ReadCapacityUnits,WriteCapacity:ProvisionedThroughput.WriteCapacityUnits}' \
  --region us-east-1

CloudWatchでGSI単位のスロットリングを確認する場合は、ConsumedWriteCapacityUnitsWriteThrottleEventsメトリクスをインデックス名でディメンションフィルタリングして確認する。ベーステーブルのメトリクスとGSIのメトリクスは別々に発行されるため、ダッシュボードにGSI単位のメトリクスを含めていないと見逃しやすい。

PAY_PER_REQUESTモードではこのGSIキャパシティ管理は不要になるが、コストトレードオフは別途評価が必要だ。

IAMポリシー: インデックスへのアクセス制御

GSIとLSIへのクエリはベーステーブルへのアクセス権とは別に制御できる。インデックスのARN形式は以下の通り:

arn:aws:dynamodb:us-east-1:123456789012:table/Orders/index/StatusDateIndex
🔽 インデックスへの読み取り専用アクセスポリシー例(クリックで展開)
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "dynamodb:Query"
      ],
      "Resource": [
        "arn:aws:dynamodb:us-east-1:123456789012:table/Orders/index/StatusDateIndex"
      ]
    },
    {
      "Effect": "Allow",
      "Action": [
        "dynamodb:GetItem",
        "dynamodb:Query"
      ],
      "Resource": [
        "arn:aws:dynamodb:us-east-1:123456789012:table/Orders"
      ]
    }
  ]
}

インデックスへのQuery権限だけを付与しても、ベーステーブルへのGetItem権限がないとインデックスから返されたキーでアイテムを取得できない点に注意。射影に必要な属性がすべてインデックスに含まれている場合は、ベーステーブルへのアクセスなしでインデックスのみのクエリが完結する。

LSI vs GSI: DynamoDBセカンダリインデックスのまとめと次のステップ

LSIとGSIの選択は、アクセスパターンの設計と切り離せない。LSIはパーティション内の別ソートキーが必要かつ強整合性が求められる場合に適しているが、テーブル作成後の変更不可という制約がある。GSIはアクセスパターンの柔軟性が高く、後から追加できるが、結果整合性とキャパシティの独立管理を理解した上で使う必要がある。

設計の出発点として、まずアクセスパターンをリストアップし、各パターンに必要なパーティションキーとソートキーを明確にすることを推奨する。DynamoDBのテーブル設計はリレーショナルDBとは根本的に異なり、クエリパターン先行で設計するアプローチが基本だ。

用語集

用語説明
パーティションキー(HASH)DynamoDBがデータを物理的に分散させるためのキー。同一パーティションキー値のアイテムは同じパーティションに格納される。
ソートキー(RANGE)同一パーティション内でアイテムをソートするためのキー。範囲クエリや前方一致クエリに使用できる。
射影(Projection)インデックスにコピーする属性のセット。KEYS_ONLY / INCLUDE / ALL から選択する。
スパースインデックスGSIのキー属性が存在するアイテムのみがインデックスに含まれる設計パターン。インデックスサイズを最小化できる。
結果整合性書き込み直後の読み取りで最新データが返らない可能性がある整合性モデル。GSIは常にこのモデルを使用する。

Related Posts

コメント

このブログの人気の投稿

SQS Visibility Timeoutの完全解説:メッセージ二重処理の原因と対策

EC2 SSH接続タイムアウトの原因と修正方法 — セキュリティグループのインバウンドルール完全ガイド

S3パブリックアクセス拒否の原因と解決策:バケットレベルの「Block Public Access」が優先される仕組み