この記事はRust+SvelteKit+CDKでRSS要約アプリを作ってみる Advent Calendar 2025の4日目の記事になります。
また、筆者が属している株式会社野村総合研究所のアドベントカレンダーもあるので、ぜひ購読ください。
DynamoDBのGSIを考える
あまりしっかりDynamoDBを触ってこなかったので、筆者はDynamoDBのGSI(グローバルセカンダリインデックス)をあまりしっかり理解できていません。AWSの資格勉強の過程で、「後から作成することができる」「全項目をスキャンするより効率的にアイテムを探索できる」ということぐらいしか分かっていませんでした。
今回、Webアプリを作る上でDynamoDBを採用したため、真剣にGSIについて考慮する必要がありました。というか、キーについて全然不勉強だったのでちんぷんかんぷんです。
もちろん、スキャンしてクライアント側でフィルタすることもできなくはないですが、技術習得を目的に開発に取り組んでいるため、ちゃんと勉強してGSIを設定することにします。
GSIのベストプラクティスを調べてみる
パーティションキーとソートキー
まずはDynamoDBにおけるキーについて調べてみます。DynamoDBのテーブルを作成するときにはパーティションキーとソートキー(任意)を設定できます。パーティションキーはアイテムを分散配置するためのキーです(いわゆるカーディナリティが高いものが望ましい)。ソートキーは同じパーティションキーの中でソートするときに用いられ、一つのテーブルの中のアイテムは、パーティションキーとソートキーの組み合わせで一意に定められる必要があります。
ソートキーの設定は任意です。もしパーティションキーのみ設定した場合は、パーティションキーのみで一意に定められる必要があります。
キーを使ったテーブルのインデックスには大きく2種類存在します。
ローカルセカンダリインデックス(LSI)
パーティションキーを共有しつつ、別のソートキーを設定できる機能です。テーブル作成時のみ設定で、上限は5個です。
グローバルセカンダリインデックス(GSI)
パーティションキーとソートキーを自由に設定できます。
え、じゃあGSIはLSIの上位互換じゃない?
前述の説明だけを読むと、より自由度が高く後からでも作成できるGSIの方が便利なように思えます。LSIの存在意義はなんなんでしょうか。
LSIはパーティションキーを共有すると書きました。すなわち元のテーブルと同じパーティション内に別のソートキーでインデックスを作成します。そのため、下記のメリットを享受できます。
- 強整合性を保つことができる
- LSIはパーティションが同じであるので、元のテーブルに加わった変更が即座に反映されるため、強整合性が確保されます。GSIは別のパーティションになるため、結果整合性が確保されます。
- コストが安い
- パーティションが別々なので、その分のストレージや書き込み処理も増え、それはコスト増に繋がります。
パーティションを共有するが故のメリットがあるということですね。ちなみに、同じパーティション内に作成するためにテーブル作成時にしか設定できないという仕組みらしいです。
どう使い分けるのがいいのか
ChatGPTに聞いたところ、強整合性が重要なのであればLSIを、そうでないのであればGSIをお勧めされました。
「GSIの方が大まかに機能や性能がLSIより優れている」というよりは、「テーブル作成時にすべて決めなければならない&最大5つまでという制約に縛られずに済む」という理由のようですし、個人的にもその通りかな、と思いました。
今回のWebアプリでも、そこまで強整合性は必要ないのでGSIを採用します。
記事を取得するタイミング
GSIはデータを効率よく取得するための設定ですので、まずはどのようなタイミングで何を目的にデータを取得するのか、そのユースケースを整理する必要があります。
今回のアプリケーションでDBとやりとりする場面は以下のタイミングです。
-
Collector(RSS収集関数)
- RSSフィードから取得した記事を格納する(PUT)
- Processor(記事処理関数)
- SQSメッセージのIDを元に記事を参照する(GET)
- AI要約を追加した記事を格納する(PUT)
- Generator(静的サイト作成ジョブ)
- 対象日付ごとに該当する記事を取得する(GET)
GSIはデータの取得するときに役立つものなので、今回は以下のようなユースケースに絞って考える必要があります。
- SQSメッセージから記事IDを抜きだし、対応する記事を単一取得する
- ジョブ起動時に、対象日付ごとに、その日付に作成された記事を取得する
このうち、前者のIDに紐づけた単一取得は単に一意のものを取るだけなのでGSIは不要です。そのため、今回のWebアプリでGSIが必要になるのは後者の日付ごとに取得する部分になります。
CDKでGSIを設定する
それでは日付ごとに取得するためのGSIをCDKで設定してみます。今回はパーティションキーとして日付項目(本アプリではdate)を設定します(ソートキーはidにしていますが、深い意味はありません)。
articleDb.addGlobalSecondaryIndex({
indexName: "ByDate",
partitionKey: {
name: "date",
type: cdk.aws_dynamodb.AttributeType.STRING,
},
sortKey: { name: "id", type: cdk.aws_dynamodb.AttributeType.STRING },
projectionType: cdk.aws_dynamodb.ProjectionType.ALL,
});
projectionTypeってなんだ?
この記事を書くにあたって改めてコードを読んだところ、projectionTypeについて理解していないことに気づきました(AIの提案をそのまま受け入れていました)。
これは、GSIで確保されるストレージに対して、元テーブルから「どの項目をプロジェクション(投影)するか」を選択する設定になります。
例えば、↑のCDKではALLを設定しているので、元テーブルのすべての項目がGSIにプロジェクションされます。利点として、GSIで取得したデータに丸ごと元データが含まれているので、データの参照や加工に手間がかからないことが挙げられます。その代わり、不要な項目も含まれるためストレージ効率は低くなります。
KEYS_ONLYにすると、元テーブルの主キーとGSIで設定したパーティションキーやソートキーのみがプロジェクションされます。最低限のキーのみなので効率はいいですが、キー以外の項目を取得したい場合は、キーを元に別途元テーブルにクエリを投げる必要があります。
INCLUDEを使うと、明示的に開発者がどの項目を含めるかを指定できます。個人的には、ちゃんと計画立ててこの設定を使うのが一番効率がいいのかな。。。という印象でした。