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つのアイテムコレクションに、時刻順で存在するようにキー付けされます。
| EventPK | EventSK | actorId | verb | targetRef |
|---|---|---|---|---|
| WS#orbit-9 | TS#2026-06-23T14:02:11Z | USR#kp | ROLE_GRANTED | USR#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 パーティションに。
| GSI1PK | GSI1SK | EventPK | EventSK | verb |
|---|---|---|---|---|
| USR#kp | TS#2026-06-23T14:02:11Z | WS#orbit-9 | TS#2026-06-23T14:02:11Z | ROLE_GRANTED |
何が一緒に乗ってきたかに注目してください — ベーステーブルのキー(EventPK、EventSK)がすべての GSI アイテムに自動的に保存されます。それが、GSI のヒットが完全なアイテムへ指し戻せる理由であり — そしてKEYS_ONLYインデックスでもストレージがかかる理由です。
GSI に実際に存在するもの
インデックスはアイテム全体を コピーしません。各 GSI エントリはちょうど3つのものを保持し、あなたが制御できるのは3つ目だけです。
| GSI に保存されるもの | どこから来るか | 任意? |
|---|---|---|
| GSI パーティション + ソートキー | GSI キーとして指定した属性 | いいえ |
| ベーステーブルのキー | すべてのベースアイテムからコピー | いいえ |
| 射影された属性 | あなたの Projection の選択 | はい |
Projection は KEYS_ONLY、INCLUDE(指定したリスト)、または ALL です。GSI への Query は、インデックスにある属性しか返せません。
射影されていないものを要求すると、DynamoDB はそれを透過的に取りに行か ず — そのフィールドには何も返ってきません。(AWS GSI ドキュメント)
それはリレーショナルの罠の逆です。SQL なら欠けた列のためにヒープへ join し戻ります。GSI は決してそうしません。射影が契約のすべてです。
書き込みがインデックスに届く仕組み
レプリケーションこそ、SQL の直感を最も強く破る部分です。ベース書き込みとそのインデックス更新は 1つのアトミックな操作ではありません。
PutItem すると、DynamoDB はベーステーブルへ永続的にコミットし、書き込みを確認応答し、その後 で各 GSI を更新するバックグラウンド経路へ変更を伝播させます。確認応答はインデックスを待ちません。
監査の書き込みについて、イベントの順序を上から下へ示します。
呼び出し元はステップ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 へレプリケートされる様子を見ましょう。