上級読了 3 分

DynamoDB の GSI は内部的にどう保存されるか

Global Secondary Index は、テーブルへ戻るポインタではありません。それは 別の、内部的に管理されたテーブル です — 独自のパーティション、独自のキースキーマ、独自のキャパシティを持ち — DynamoDB は書き込みを非同期にコピーすることで同期を保ちます。

SQL から来ると、インデックスは同じ物理テーブルにボルト留めされた B-tree で、同じトランザクション内で更新されます。GSI はその両方の前提を破り、ほぼすべての GSI の意外性はその1つの事実に遡ります。

DynamoDB の GSI はどう保存されるか?

DynamoDB の GSI は、別の、内部的に管理されたテーブルとして保存されます — 独自のパーティション、キースキーマ、キャパシティを持ち、ベーステーブルへのポインタではありません。DynamoDB は各書き込みをインデックスへ非同期にコピーし、GSI キー、ベーステーブルのキー、そして射影された属性だけを保存します。

  • GSI はそれ自身のテーブル。 ベーステーブルではなく、GSI のパーティションキーをキーとする、完全に独立したパーティション空間を持つ。
  • 書き込みは非同期にレプリケートされる。 書き込みはまずベーステーブルにコミットされ、その後 DynamoDB がバックグラウンド経路で各 GSI へファンアウトする。
  • 射影された属性だけが保存される。 インデックスは GSI キー、ベースキー、そしてあなたが射影した属性を保持する — それ以外はない。
  • GSI キーは一意である必要はない。 複数のベースアイテムが1つの GSI パーティション/ソートキーを共有しうる。ベース主キーがそれらを区別するタイブレーカーだ。

1つのベースアイテムから始める

SaaS の 監査ログ を考えます。ワークスペース内のすべての特権アクションが不変のイベントになります。ベーステーブル WorkspaceEvents は、1つのワークスペースのすべてのイベントが1つのアイテムコレクションに、時刻順で存在するようにキー付けされます。

WorkspaceEvents (base table)
EventPKEventSKactorIdverbtargetRef
WS#orbit-9TS#2026-06-23T14:02:11ZUSR#kpROLE_GRANTEDUSR#mara

EventPK = "WS#orbit-9" がワークスペースで分割し、EventSK は ISO タイムスタンプなので Query が1つのワークスペースのイベントを時系列順に返します。それは「このワークスペースのタイムラインを見せて」に完璧に応えます。

それ以外には何にも応えません。「USR#kp はすべてのワークスペースで何をしたか?」は問えません — actorId はキーではないので、ベーステーブルでそれに答える唯一の方法はフルScanです。それが GSI が追加するために存在するアクセスパターンです。

GSI を追加して2つ目のテーブルが現れるのを見る

同じイベントを、それを実行した人で再分割する GSI ByActor を定義します。

ByActor (GSI)
GSI1PK = actorId   ("USR#kp")
GSI1SK = EventSK   ("TS#2026-06-23T14:02:11Z")

DynamoDB は今や2つ目の物理構造を維持します。同じ論理イベントが 2回 保存されます — 一度はベーステーブルの WS#orbit-9 パーティションに、もう一度は GSI の USR#kp パーティションに。

ByActor (GSI) — その独自のパーティション空間
GSI1PKGSI1SKEventPKEventSKverb
USR#kpTS#2026-06-23T14:02:11ZWS#orbit-9TS#2026-06-23T14:02:11ZROLE_GRANTED

何が一緒に乗ってきたかに注目してください — ベーステーブルのキーEventPKEventSK)がすべての GSI アイテムに自動的に保存されます。それが、GSI のヒットが完全なアイテムへ指し戻せる理由であり — そしてKEYS_ONLYインデックスでもストレージがかかる理由です。

GSI に実際に存在するもの

インデックスはアイテム全体を コピーしません。各 GSI エントリはちょうど3つのものを保持し、あなたが制御できるのは3つ目だけです。

GSI に保存されるものどこから来るか任意?
GSI パーティション + ソートキーGSI キーとして指定した属性いいえ
ベーステーブルのキーすべてのベースアイテムからコピーいいえ
射影された属性あなたの Projection の選択はい

ProjectionKEYS_ONLYINCLUDE(指定したリスト)、または ALL です。GSI への Query は、インデックスにある属性しか返せません。

射影されていないものを要求すると、DynamoDB はそれを透過的に取りに行か — そのフィールドには何も返ってきません。(AWS GSI ドキュメント

それはリレーショナルの罠の逆です。SQL なら欠けた列のためにヒープへ join し戻ります。GSI は決してそうしません。射影が契約のすべてです。

書き込みがインデックスに届く仕組み

レプリケーションこそ、SQL の直感を最も強く破る部分です。ベース書き込みとそのインデックス更新は 1つのアトミックな操作ではありません

PutItem すると、DynamoDB はベーステーブルへ永続的にコミットし、書き込みを確認応答し、その後 で各 GSI を更新するバックグラウンド経路へ変更を伝播させます。確認応答はインデックスを待ちません。

監査の書き込みについて、イベントの順序を上から下へ示します。

PutItemWS#orbit-9 イベントベースパーティションにコミット200 OKを呼び出し元へ非同期経路:GSI キーを抽出ByActorパーティション USR#kpへルーティング射影された属性を書き込み

呼び出し元はステップ3で 200 OK を受け取ります — ステップ4から6が終わる前に — なので、その隙間に ByActor への Query は、ちょうど新しいイベントを取りこぼしうるのです。

その非同期性はバグではなく設計です — それは 2007 年のAmazon Dynamo 論文の系譜で、同期的な整合性より可用性を選びました。完全な帰結はGSI が結果整合性である理由にあります。

GSI キーは一意キーではない

SQL では、非一意なセカンダリインデックスがデフォルトで、一意なものはオプトインする制約です。GSI は逆です — 一意性の保証は 一切、決してありません。

衝突するタイムスタンプの同じアクターからの2つの監査イベントは、同じ GSI1PK GSI1SK を共有します。DynamoDB は両方を保存します — 常に一緒に運ばれるベーステーブルの主キーによって内部的に区別します。

なので、1つのアクターの1つの瞬間に対する GSI の Query は、正当に複数のアイテムを返しうります。SQL の一意インデックスが与えるようにキーごとに1行を仮定していたなら、それが地雷です。

インデックスをクエリするとき、DynamoDB 式ビルダーは名前と値を正しくエスケープして KeyConditionExpression を書きます — たとえば、あるカットオフ以降の1アクターにマッチさせる場合。

KeyConditionExpression: "#a = :actor AND #ts > :since"
ExpressionAttributeNames:  { "#a": "actorId", "#ts": "EventSK" }
ExpressionAttributeValues: {
  ":actor": { "S": "USR#kp" },
  ":since": { "S": "TS#2026-06-01T00:00:00Z" }
}

キャパシティはテーブルではなくインデックスとともにある

GSI はそれ自身のテーブルなので、ベーステーブルとは別に課金・スロットルされる 独自の 読み取り・書き込みキャパシティを持ちます。ByActor からの読み取りは GSI の読み取りユニットを消費し、テーブルのものは決して消費しません。

噛みつくのは逆の結合です — すべてのベーステーブルの書き込みはインデックスにも書き込み、GSI がそれを吸収できないと、ベース書き込みに背圧をかけます。そのメカニズムは専用のガイドを持ちます — GSI がベーステーブルの書き込みをスロットルするとき

これは、GSI のパーティションキーがベーステーブルのものと同じくらい重要な理由でもあります。低カーディナリティの GSI キーは、ベース書き込みが完璧に分散していても書き込みを1つのインデックスパーティションに固めます — 再キー付けによって自分で作ったホットパーティションです。

落とし穴と次のステップ

  • 射影されていない属性が返ると期待しない。 GSI の Query はインデックスが保存するものだけを返す。完全なアイテムが必要なら、射影するか、一緒に運ばれたキーでベーステーブルから取得する。
  • GSI キーを一意として扱わない。 Query がキーごとに複数のアイテムを返すことを見込む。ベース主キーが唯一の本当の識別子だ。
  • GSI を、それに供給した書き込みの直後に読まない。 非同期経路は、インデックスがまだ書き込みを見せないことを意味する — read-your-own-writes が必要なときはベーステーブルを読む。
  • GSI のキャパシティを意図的にサイジングする。 読み取りでは独立し、書き込みでは隠れた依存になる。

ゲーム全体は、パターンに応える形のキーを選ぶことです — シングルテーブル設計は1つの GSI を多くのパターンにまたいでオーバーロードします。GSI と LSIは、代わりにローカルインデックスが合うのはいつかを扱います。

DynamoDB 式ビルダーで GSI の KeyConditionExpression を構築してプレビューし、DynoTable を試してインデックスの射影された属性を調べ、自分のテーブルで書き込みが GSI へレプリケートされる様子を見ましょう。

更新日