中級読了 3 分

DynamoDB のスパースインデックス

スパースインデックスとは、そのキー属性を持つアイテムだけを保持するセカンダリインデックスです — そのため、巨大なテーブルの小さくホットなサブセットが、それ自体で事前フィルタリングされた、すぐにクエリできる コレクションになります。

数百万行あるのに、一日中実行するクエリが触れるのはごくわずかなスライスです。オープンなサポートチケット、 未払いの請求書、レビュー対象にフラグが立ったアカウントなど。

そのスライスをフィルタリングしても、依然としてテーブル全体をスキャンし、すべての読み取り分を請求されます。 スパースインデックスは、インデックスそのものを小さくします。

DynamoDB のスパースインデックスとは何か?

スパースインデックスとは、そのキー属性を持つアイテムだけを保持するセカンダリインデックスです。DynamoDB は そのキーを持たないアイテムをスキップするので、欲しいアイテムだけが書き込むキー — オープンなチケット、未払いの 請求書 — を考案すると、インデックスはまさにそのサブセットになります。すると、クエリはそれだけを読み取ります。 フィルターも、無駄な読み取りキャパシティもありません。

  • セカンダリインデックスは、そのキーを持つアイテムだけをインデックスする。 アイテムにキーを付けなければ、 それはインデックスに決して入りません — プレースホルダーも、null の行もありません。
  • だから、欲しいアイテムだけが持つキーを考案する。 クエリするアイテムにはそれを書き込み、残りには取り除きます。 インデックスはまさにそのサブセットになります。
  • クエリはサブセットだけを読み、フィルターはなし。 そのサイズは、テーブルの総数ではなく、小さくホットな セットを追随します。
  • てこは空白化ではなく REMOVE 空文字列は有効なインデックスキーではありません — DynamoDB は書き込み全体を ValidationException で拒否します — なので、属性を削除しなければなりません。

問題: フィルタリングは読み取りを節約しない

SQL から来ると、WHERE 句が作業を絞り込むと思い込みます。DynamoDB の FilterExpression はそうしません。 それはアイテムが読み取られたに動くのであって、前ではありません。

AWS デベロッパーガイド によれば、「Query はフィルター式の有無に関わらず、同じ量の読み取りキャパシティを消費する」 — 調べたすべての アイテムに対して支払い、それから一致しないものを捨てるのです。

ですから、500万件のチケットのうち50件がオープンなら、フィルター付きの Query/Scan はその50件を渡すために 数百万件を読み通します。

それがあらゆる「なぜ私のスキャンはこんなに高コストなのか」というスレッドの背後にある落とし穴です。 query 対 scan に完全なコストの全体像があります。

スパースインデックスは、インデックスそのものを小さくすることでそれを回避します。

スパースさはどのように機能するか

セカンダリインデックスは、インデックスのキー属性を実際に持つアイテムだけをインデックスします

スパースインデックスに関する AWS のドキュメント がこれを明言しています。DynamoDB はアイテムがインデックスのキー属性を持つ場合にのみ、そのアイテムを セカンダリインデックスに書き込むので、めったに設定されない属性に対するインデックスは自然と小さいままです。

アイテムで GSI のパーティションキー(またはソートキー)を欠くと、DynamoDB はそれをインデックスに単に 書き込みません。プレースホルダーも、null の行もありません — アイテムは不在です。

その「デフォルトでの不在」がトリックのすべてです。すべての アイテムが持つ status 属性をインデックス してはいけません。クエリしたいアイテムだけがそもそも持つ属性を考案しましょう。

すると、インデックスはまさにそれらのアイテムだけのきれいなリストになり、それに対する Query はそれらだけを 読み取ります — フィルターも、無駄なキャパシティもありません。

インデックスに供給するベーステーブルを思い描きましょう。キーを持つアイテムだけが向こう側へ渡ります。

キーを削除キーを削除スパース GSI(オープンのみ)オープンオープンベーステーブル(全アイテム)オープン: キーありオープン: キーありクローズ: キーなしクローズ: キーなし

キーを持つ(オープンな)アイテムだけがインデックスに複製されます。クローズされたアイテムは決して入りません。

これは シングルテーブル設計 と同じキー整形の考え方です。キーは特定のアクセス パターンのために構築する道具であって、データの忠実な鏡ではありません。

実例: 「オープンなチケットだけ」

サポートチケットのテーブルを取り上げます。ベーステーブルは、ID でチケットを取得し、あるカスタマーのチケットを 一覧するためにキー付けされています。

PKSKattributes
TICKET#a91fDETAILsubject, body, priority, openState
CUSTOMER#88TICKET#a91fsubject, priority, openState

テーブルの生涯にわたって、ほとんどのチケットは最終的にクローズされます。しかし、エージェントが一日中 叩くダッシュボードのクエリは「すべてのオープンなチケットを古い順に見せて」です — 数百万件の中に隠れた 数百行です。

スパースインデックスの一手はこうです。パーティションキー openBucket とソートキー openedAt を持つ を定義し、オープンなチケットにだけ openBucket を書き込みます。チケットが作成されたときに 設定し、チケットが解決されたときに REMOVE します。

PKSKopenBucketopenedAt
TICKET#a91fDETAILOPEN2026-06-23T09:14:00Z← open: in the index
TICKET#b02cDETAILOPEN2026-06-22T16:40:00Z← open: in the index
TICKET#77deDETAIL(absent)2026-05-30T11:02:00Z← closed: NOT in the index

チケット a91fb02copenBucket を持つので、GSI に住みます。チケット 77de は解決され openBucket が取り除かれたので、静かに脱落しました。ダッシュボードは今や1つの安価なクエリです。

Query  IndexName = "open-tickets-index"
KeyConditionExpression: openBucket = "OPEN"
ScanIndexForward: true        # oldest first

これはオープンなチケットだけを読み取ります。チケットがクローズされるにつれ、インデックスはひとりでに縮みます — そのサイズは総数ではなく、オープンな 個体数を追随します。

ここでは1つの静的なパーティション値("OPEN")で十分です。まさにセットが小さいままだからです。巨大な オープンセットならシャーディングされたパーティションキーが必要でしょうが、「小さいサブセット」のインデックスは、 1つの値が正しい選択となるまさにその場所です。

それを機能させる遷移は、1つの です — チケットが解決されたときに属性を取り除きます。

その REMOVE 句と、読み取り側の型付きキー条件は、ExpressionAttributeNames:val プレースホルダーを 自分で手組みする代わりに、DynamoDB 式ビルダー で試作しましょう。

DynoTable でやってみる

スパースインデックスの難しいところは読み取りではありません — どのアイテムがインデックスに入り、どれが静かに 脱落したか を見ることです。

DynoTable では、テーブルビューをセカンダリインデックスに切り替えて、まさに埋まったサブセットを見ることが できます。ですから、解決されたチケットが古いキーを引きずったまま残るのではなく、本当に open-tickets-index を離れたことを確認できます。

DynoTable で、サポートチケットのテーブルをそのオープンチケット用スパース GSI 越しに表示し、openBucket キーを持つアイテムだけを示した様子。
DynoTable で、サポートチケットのテーブルをそのオープンチケット用スパース GSI 越しに表示し、openBucket キーを持つアイテムだけを示した様子。

落とし穴と次のステップ

いくつか気をつけることがあります。

  • キーを空白化するのではなく、取り除く。 空文字列は有効なインデックスキーではありません — openBucket = "" を書き込むと ValidationException で失敗するので、アイテムはそれではインデックスされません。 インデックスからアイテムを外すには、属性を REMOVE しなければなりません。
  • インデックスは GSI は非同期に更新されるので、たった今解決されたチケットが 短時間まだ現れることがあります — GSI 読み取りは 結果整合性のみをサポートします。 「このチケットは今オープンか」の判断でそれを信用してはいけません。
  • 属性に気を配る。 インデックスに対する Query は、そこに射影された属性だけを 返します。ダッシュボードに subject と priority が必要なら、それらを射影しましょう — さもなければ完全な ベースアイテムのために追加の GetItem を支払います。
  • GSI も LSI もスパースにできる — てこは同じです。インデックスしたくないアイテムでは、インデックスの ソートキーを省きます。ただし、通常は GSI のほうが適します。テーブル作成後に追加でき、独自のキースキーマと キャパシティを与えられるからです。GSI 対 LSI がトレードオフを分解します。

スパースインデックスは、このモデルにおける最も古いアイデアの1つです。元の 2007年の Amazon Dynamo 論文 は、既知の大量アクセスパターンに安価に応えることを中心にストアを構築しました。

スパースインデックスはまさにそれです。よくあるクエリが不要なものを一切読まないように、キーを整形するのです。

実際に構築して調べるには、DynoTable をダウンロード してテーブルに向け、データビューを スパース GSI に切り替えましょう — アイテムがインデックスキーを得たり失ったりするにつれてサブセットが 更新される様子を眺めてください。

更新日