DynamoDB の GSI と LSI
グローバルセカンダリインデックス(GSI)もローカルセカンダリインデックス(LSI)も、テーブルのキーではない属性で Query できるようにします。どちらが必要かは、どのキーを変えるのかで決まります。
DynamoDB の GSI と LSI の違いは何ですか?
グローバルセカンダリインデックスは独自のパーティションキーと任意のソートキーを持ち、独自のキャパシティを得て、いつでも追加できます。ただし結果整合性のある読み取りしか提供しません。
ローカルセカンダリインデックスはテーブルのパーティションキーを異なるソートキーと組み合わせて保ち、強い整合性のある読み取りをサポートし、テーブルのキャパシティを共有しますが、テーブルと同時に作成する必要があります。
効いてくる違い
| GSI | LSI | |
|---|---|---|
| パーティションキー | 任意の スカラー (S/N/B) | テーブルと 同じ |
| ソートキー | 任意のスカラー (S/N/B)、省略可 | 任意のスカラー (S/N/B) |
| キー属性 | パーティションキー最大 4 + ソートキー 4 | ソートキー属性はちょうど 1 つ |
| 作成のタイミング | いつでも | テーブル作成時のみ |
| 整合性 | 結果整合性のみ | 強い整合性が利用可能 |
| キャパシティ | 独自 | テーブルと共有 |
| 書き込み伝播 | 非同期(結果整合性) | 同期 |
| テーブルあたり最大 | 20(デフォルトクォータ) | 5 |
| 10 GB のアイテムコレクション上限 | なし | あり(パーティションキーごと) |
4 + 4 の行は新しいものです。2025 年 11 月以降、GSI のキーは 1 つの連結文字列ではなく、型付きの複数属性から組み立てられます。LSI のソートキーは今もちょうど 1 つの属性です。
経験則
- 異なるパーティションキー が必要(例: チケットを
accountではなくstatusで引く)? なら GSI が必要です。LSI は再パーティションできません。 - 同じパーティション内での 2 つ目のソート順 が必要で、前もって決めておき、強い整合性のある読み取りが欲しい? なら LSI が合います。LSI はテーブルの正確なパーティションキーを保ち、異なるソートキーだけを差し替えます。
選択は 1 つの問いに収束します。どのキーを変えるのか、です。
異なるパーティションキーは GSI を強制します。同じパーティションで異なるソートキー、これが LSI の合う唯一のケースです。
GSI が通常のデフォルトです。後から追加でき、独自のキャパシティでスケールし、10 GB の上限がありません。AWS は、アイテムコレクションが 10 GB を超えて育つ可能性があるなら GSI を勧めています。単一の GSI のキーをオーバーロードして複数のパターンに使いましょう。シングルテーブル設計を参照してください。
Scan を潰すために GSI を追加するなら、それが 独自 の読み取り/書き込みキャパシティを持つことを忘れないでください。その追加コストは料金計算ツールで見積もりましょう。
実例:優先度順のオープンチケット
サポートデスクを運営しているとします。チケットは PK = TEAM#7、SK = TICKET#8842 の下にあります。2 つのアクセスパターンが競合します。
- ID で 1 件のチケットを取得: ベーステーブルのキーに対する
GetItem。 - チームのオープンチケットを優先度の高い順に一覧: チケット ID とは異なるソート順が必要です。
パターン 2 は SK だけのベーステーブル Query にはできません。ソートキーは優先度ではなくチケット ID だからです。選択肢はこうです。
| アプローチ | キー | 一覧の整合性 | 合う場面 |
|---|---|---|---|
GSI1PK = TEAM#7、GSI1SK = P#1#TICKET#8842 の GSI、オープンの間だけ設定 | 新しいパーティション + ソート | 結果整合性 | ステータスが頻繁に変わる。ローンチ後にインデックスを追加する |
同じ PK で LSI1SK = P#1#TICKET#8842 の LSI、オープンの間だけ設定 | 同じパーティション、新しいソート | 強い整合性 | テーブル作成時にインデックスを計画済み。ステータスの読み取りが新鮮でなければならない |
ベースの Query でフィルタし、コード側で優先度順にソート | ベースのキーのみ | 強い整合性 | 小さなパーティションのみ。フィルタは読んだパーティション全体を課金する |
インデックスキーをオープンなチケットにだけ書けば、どちらのインデックスもスパースになります。クローズしたチケットは決して入りません。GSI では、P#1#TICKET#8842 という文字列を組み立てる代わりに、priority と ticketId を 2 つの型付きソートキー属性にすることもできます。
エージェントが数秒おきに更新する忙しいキューなら、GSI の結果整合性はたいてい許容できます。API が返る前にステータス変更を反映しなければならない金融の元帳の行なら、読み取りはベーステーブルか LSI から提供しましょう。
書き込み増幅
更新がインデックス書き込みを消費するのは、そのインデックスが格納するものに触れたときだけです。射影された属性を変えれば 1 回、インデックスキーの値を変えれば 2 回(古いエントリを削除し、新しいものを put する)、インデックスがそれを格納していなければ 0 回です。
GSI1SK と LSI1SK の両方に入っている priority を変える更新を考えます。テーブルにはそれを格納しない GSI2 もあります。これは 2 + 2 + 0 = 4 回のインデックス書き込みに、ベースの書き込みが加わります。
1 KB を超えるインデックスエントリは、1 回の書き込みにつき 1 書き込みユニットより多くかかります。インデックスの概要がこれを月額で見積もっています。
LSI の書き込みはベースアイテムと同じパーティションに着地します。LSI はパーティションキーを共有するからです。各 LSI がそのパーティションに書き込みコストを上乗せするので、忙しいパーティションキーはパーティションあたりの上限に早く達します。
GSI は書き込みを独自のパーティションに移しますが、サイズ不足やホットな GSI はやはりベーステーブルの書き込みをスロットリングします。なぜ GSI がベーステーブルの書き込みをスロットリングするのかを参照してください。
10 GB の LSI アイテムコレクション上限
LSI のあるテーブルでは、AWS は各パーティションキー値を 10 GB に制限し、ベーステーブルのアイテムとその LSI エントリを合わせて数えます。LSI のないテーブルにはこの上限はなく、GSI も対象外です。1 つの TEAM#7 の下に数百万のチケットを抱えるテナントは、この上限にぶつかりえます。
上限に達すると、DynamoDB はコレクションを大きくする書き込みを ItemCollectionSizeLimitExceededException で拒否することがあります。縮める書き込みは引き続き成功します。
PutItem、UpdateItem、DeleteItem、BatchWriteItem に ReturnItemCollectionMetrics: "SIZE" を渡せば、近づいているのが分かります。
SizeEstimateRangeGB が 10 より下のしきい値(AWS は 8 GB を勧めています)を超えたらアラートを出しましょう。回復するには、そのコレクションのアイテムを削除または削減するか、履歴を新しいテーブルや S3 に移します。際限なく育つコレクションには、最初から GSI を使いましょう。
設計時の選び方
| 問い | GSI 寄り | LSI 寄り |
|---|---|---|
| 異なるパーティションキーが必要か? | はい | いいえ、LSI にはできない |
| 読み取りは強い整合性でなければならないか? | いいえ、GSI は結果整合性 | はい |
| テーブル作成後にインデックスを追加するか? | はい | いいえ、テーブル作成時のみ |
| アイテムコレクションが ~10 GB を超えうるか? | はい、上限なし | LSI では危険 |
| 書き込みスケールを分離したいか? | はい | いいえ、テーブルの WCU を共有 |
稼働中のテーブルがあって LSI が欲しい? 追加はできません。結果整合性の読み取りで構わないなら、テーブルのパーティションキーと新しいソートキーを持つ GSI を追加します(GlobalSecondaryIndexUpdates を指定した UpdateTable)。
強い整合性の読み取りが必要なら、LSI 付きの新しいテーブルを CreateTable し、アイテムをコピーして(並列 Scan と BatchWriteItem、切り替え中はデュアルライト)、読み取り側を切り替えます。Import from S3 は役に立ちません。それが作るテーブルは GSI は持てても LSI は持てないからです。
複数のアクセスパターンを 1 つの GSI にオーバーロードするときは、まずシングルテーブル設計ツールでキーのテンプレートを描いてみましょう。どの読み取りがインデックスを共有するかを示し、バックフィルに費用を払う前にサポートされないパターンを警告してくれます。
コミットする前にインデックスを調べる
DynoTable で、ベーステーブルに対してフィルタを実行し、次にフィルタービルダーのインデックスピッカーを各 GSI に切り替えてもう一度実行します。
GSI の結果に無い属性は、その射影に含まれていません。
候補となる GSI の KeyConditionExpression は DynamoDB 式ビルダーで構築し、それからクエリビルダーでページネーション込みの完全なプログラムを出力して、ステージングのテストに貼り付けましょう。
この選択の先にある 3 つのガイド
- GSI の結果整合性のある読み取り: 書き込んだばかりのアイテムが、なぜ一瞬 GSI のクエリから抜け落ちるのか。
- インデックスの射影: 各インデックスが物理的に何を保存するか。
- スパースインデックス: マーカー属性を持つアイテムだけをインデックスして GSI を小さく保つ。
DynoTable を試して、設計にコミットする前に自分のテーブルで各インデックスのキーと射影を調べましょう。