DynamoDB の GSI は内部的にどう保存されるか
は、テーブルへ戻るポインタではありません。それは 別個の、内部で管理されるテーブル — 独自のパーティション、独自のキースキーマ、独自の キャパシティ — で、DynamoDB が書き込みをそこへ非同期にコピーすることで同期に保ちます。
SQL から来ると、インデックスは同じ物理テーブルにボルト留めされた B ツリーで、同じ トランザクション内で更新されます。GSI はその両方の前提を破り、ほとんどすべての GSI の 驚きは、その 1 つの事実にたどり着きます。
DynamoDB の GSI はどう保存されるのか?
DynamoDB の GSI は、ベーステーブルへのポインタとしてではなく、別個の内部で管理されるテーブル — 独自のパーティション、キースキーマ、キャパシティ — として保存されます。DynamoDB は各書き込みをインデックスへ非同期にコピーし、GSI キー、ベーステーブルのキー、そして 属性だけを保存します。
- GSI はそれ自身のテーブル。 ベーステーブルのではなく、GSI のパーティションキーで キー付けされた、完全に独立したパーティション空間を持ちます。
- 書き込みは非同期に複製される。 あなたの書き込みはまずベーステーブルにコミットされ、 その後 DynamoDB がバックグラウンドのパスで各 GSI へファンアウトします。
- 射影された属性だけが保存される。 インデックスは GSI キー、ベースキー、そしてあなたが 射影した属性だけを保持します — それ以外は何も。
- GSI キーは一意である必要はない。 複数のベースアイテムが 1 つの GSI パーティション/ ソートキーを共有できます。ベースの が、それらを区別に保つ タイブレーカーです。
1 つのベースアイテムから始める
SaaS の 監査ログ を取ります。ワークスペース内のすべての特権的なアクションが不変の
イベントになります。ベーステーブル WorkspaceEvents は、あるワークスペースのすべての
イベントが 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 はあるワークスペースのイベントを時系列順で返します。それは「この
ワークスペースのタイムラインを見せて」を完璧に提供します。
それ以外は何も提供しません。「USR#kp はすべてのワークスペースで何をしたか?」は問えません
— actorId はキーではないので、ベーステーブルでそれに答える唯一の方法はフルの
Scan です。それこそ、GSI が追加するために存在するアクセス
パターンです。
GSI を追加して 2 つ目のテーブルが現れるのを見る
同じイベントを、それを実行した者で再パーティション分けする GSI ByActor を定義します。
ByActor (GSI)
partition key = actorId ("USR#kp")
sort key = EventSK ("TS#2026-06-23T14:02:11Z")
DynamoDB は今や 2 つ目の物理構造を維持します。同じ論理イベントが 2 回 保存されます —
一度はベーステーブルの WS#orbit-9 パーティションに、そしてもう一度は GSI の USR#kp
パーティションに。
| actorId | EventSK | EventPK | verb |
|---|---|---|---|
| USR#kp | TS#2026-06-23T14:02:11Z | WS#orbit-9 | 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 なら欠けたカラムのためにヒープへ結合し 戻します。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 の書き込み増幅(課金)
GSI に射影するすべてのベーステーブル書き込みは、us-east-1 オンデマンドで ベース WCU + インデックス WCU がかかります。ALL 射影の 1 KB アイテムは通常合計 約 2 WCU — テーブル行に1、インデックスコピーに1 — を課金します。KEYS_ONLY はインデックス書き込みを小さくし、ALL はストレージと書き込み増幅を倍にします。アイテムサイズと射影は 料金計算機 でモデルしてください。
落とし穴と次のステップ
- 射影されていない属性が返ってくると期待しない。 GSI
Queryはインデックスが保存する ものだけを返します。完全なアイテムが必要なら、それを射影するか、一緒に運ばれるキーで ベーステーブルから取得しましょう。 - GSI キーを一意として扱わない。
Queryがキーごとに複数のアイテムを返すことを想定 しましょう。ベースのプライマリキーだけが唯一の本当のアイデンティティです。 - それに供給した書き込みの直後に GSI を読まない。 非同期パスは、インデックスがまだ あなたの書き込みを表示していないことを意味します — read-your-own-writes が必要なときは ベーステーブルを読みましょう。
- GSI のキャパシティを意図的にサイジングする。 それは読み取りでは独立し、書き込みでは 隠れた依存関係です。
すべての勝負どころは、あなたのパターンを提供するキーの形を選ぶことです — シングルテーブル設計 は 1 つの GSI を多数のパターンにまたいで オーバーロードします。GSI と LSI は、代わりにローカルインデックスが 合うのはいつかを扱います。
GSI の KeyConditionExpression を DynamoDB 式ビルダー
で組み立ててプレビューし、それから DynoTable を試して インデックスの射影された
属性を調べ、書き込みが自分のテーブルで GSI に複製される様子を見てみましょう。