初級読了 2 分

DynamoDB の射影式 (Projection Expression)

射影式は DynamoDB の SELECT col1, col2 です。カンマ区切りの名の リストで、GetItemQueryScan にアイテム全体ではなくそれらの属性だけを返すよう 伝えます。

DynamoDB の射影式は読み取りコストを削減しますか?

いいえ。ProjectionExpression はレスポンスのペイロードを削りますが、課金される読み取り キャパシティは削りません。DynamoDB はストレージからアイテム全体を読み取り、そのディスク上の サイズでを計量し、それから指定しなかった属性を出力の途中で落とします。 実際に読み取りコストを削るには、代わりにカバリングを使います。

  • 削るのはペイロードであって、読み取りコストではない。 DynamoDB はストレージから アイテム全体を読み取って (そして課金して)、それから指定しなかった属性を出力の途中で 落とします。ProjectionExpression はネットワークの最適化であって、キャパシティの最適化 ではありません。
  • 公開サブセットを取得する方法。 呼び出し元が見てよい少数の属性を指定します。残りは テーブルを決して離れません。
  • 予約語になりうるものには #name プレースホルダを使う。 式の中の素の属性名は、 DynamoDB の約 570 の予約語と衝突し、リクエストを失敗させます。
  • 本当の読み取り節約には、代わりにカバリングインデックスを使う。 必要な列だけを射影する は、それ自身の (より小さい) サイズで読まれます。

実際に何を節約するのか

SQL から来ると、SELECT a, bSELECT * より少なくスキャンすると思い込むでしょう。 DynamoDB ではその直感は誤りです。読み取りのキャパシティユニット は、ディスク上のアイテムのサイズから計算され、次の 4 KB に切り上げられます——射影が 適用される前にです。AWS は明言しています。ProjectionExpression はリクエストが消費する 読み取りキャパシティを変えません。1

ですから射影が節約するのは2つのことで、どちらも実在しますが、どちらも読み取りの下流です。

  • ワイヤー上のバイト。 6 KB のアイテムが2つの小さな属性として返されれば、レスポンスは ちっぽけです。数百のアイテムを返す Query では、それがすぐに積み上がります。
  • クライアント側の作業。 デシリアライズするものが少なく、メモリに保持するものが少なく、 うっかりログや API レスポンスに漏らすものが少なくなります。

節約しないのは RCU です。それが落とし穴です。人々は請求を削るために射影に手を伸ばし、 変化がないのを見て、DynamoDB が壊れていると結論づけます。壊れていません——測ったてこが 間違っていたのです。

公開ユーザープロフィールを射影する

ユーザーディレクトリを運営しているとします。各プロフィールは1つのアイテムで、ハンドルで 人を取得できるようにキーイングされています。

PK = "PROFILE#ada"      (partition key)
SK = "PROFILE#ada"      (sort key — single-item collection)

そのアイテムは太っています。アカウントの公開の顔に加えて、非公開で運用上の属性の山を 運びます。

{
  "PK": "PROFILE#ada",
  "SK": "PROFILE#ada",
  "displayName": "Ada L.",
  "avatarUrl": "https://cdn.example.com/u/ada.png",
  "bio": "Builds things.",
  "emailAddress": "ada@example.com",
  "passwordResetToken": "…",
  "billingCustomerId": "cus_…",
  "lastLoginIp": "…",
  "internalRiskScore": 0.02
}

公開プロフィールカードには3つのフィールドが必要です。アイテム全体を取得すると、 emailAddresslastLoginIpinternalRiskScore が、それらを決して見るべきでない コンテキストへ渡ってしまいます。公開サブセットだけを指定しましょう。

GetItem  PK = "PROFILE#ada"  SK = "PROFILE#ada"
ProjectionExpression: displayName, avatarUrl, bio

レスポンスは3つの属性を運びます。非公開のものはテーブルに留まります——到着後にアプリが _フィルターして_落とすのではなく、そもそもレスポンスに一切シリアライズされないのです。 それがセキュリティ上の勝利であり、秘密がいったん境界を越えてしまうと取り返しがつかない、 まさにそのものです。

このリクエストそのもの——名前、プレースホルダ、SDK 呼び出し——を DynamoDB 式ビルダーで組み立ててコピーできます。 ProjectionExpressionExpressionAttributeNames のマップをあなたのために生成します。

下のプリセットでフィールドを追加または削除して、ProjectionExpression が変わる様子を 見てみましょう——指定した属性だけが返ります。

リクエストを構築
生成されたコード
new QueryCommand({
  "TableName": "AuditLog",
  "KeyConditionExpression": "#hashKey = :hashKeyValue AND begins_with(#rangeKey, :rangeKeyValue)",
  "ProjectionExpression": "#proj0, #proj1, #proj2",
  "ExpressionAttributeNames": {
    "#hashKey": "pk",
    "#rangeKey": "sk",
    "#proj0": "action",
    "#proj1": "actor",
    "#proj2": "createdAt"
  },
  "ExpressionAttributeValues": {
    ":hashKeyValue": {
      "S": "TENANT#acme"
    },
    ":rangeKeyValue": {
      "S": "EVENT#"
    }
  }
})

# プレースホルダで予約語をエスケープする

きれいな射影が吹き飛ぶのがここです。DynamoDB は長い語のリストを予約しています——namestatuscommentsizetimestamp、その他数百。2 射影しようとしている 属性がそのうちの1つなら、式の中の素の名前は拒否されます。

プロフィールに status 属性 ("active""suspended") もあるとします。これは失敗します。

ProjectionExpression   displayName, status

status は予約語です。修正は式属性名 (expression attribute name) です——# を 前置したプレースホルダを実際の名前にマッピングします。

ProjectionExpression       displayName, #s
ExpressionAttributeNames   { "#s": "status" }

同じ仕組みがネストされた属性にも届きます。マップから単一のフィールドを、あるいはリストの 1要素を引き出すには、ドキュメントパス構文を使います——そして各セグメントを プレースホルダ化します。どれも予約語かもしれないからです。

ProjectionExpression       #addr.#city, tags[0]
ExpressionAttributeNames   { "#addr": "address", "#city": "city" }

実践的なルール:すべてをプレースホルダ化しましょう。約 570 の予約語のどれを踏んでいるか 覚えておく必要が一切なくなり、式はどちらにしても同じように読めます。そして、どの名前が 実際に問題なのかを知りたいなら、予約語チェッカーに 貼り付けましょう — 衝突を指摘し、ExpressionAttributeNames のエイリアスマップを出力します。

カバリングインデックスが射影に勝るとき

本当に読み取りコストを削る必要があるなら——ペイロードだけでなく——てこは、読み取る属性 だけを射影するグローバルセカンダリインデックスです。GSI はデータの別コピーです。その 射影に KEYS_ONLYINCLUDEALL を選びます。3 KEYS_ONLY や狭い INCLUDE のインデックスはアイテムあたり物理的に小さいので、それに対する Query はその小さいサイズ で計量されます。

それがカバリングインデックスです。クエリはインデックスだけで完全に答えられ、ベース テーブルへの往復はありません。ホットな読み取りパターンが、大きなアイテムから常に少数の 属性しか必要としないときに使いましょう。

ProjectionExpressionカバリング GSI
ペイロードを削るはいはい
読み取りコストを削るいいえはい — インデックスのサイズで読む
追加のストレージなし射影したフィールドの2つ目のコピー
追加の書き込みコストなし書き込みがインデックスに伝播する
最適な用途非公開フィールドを隠す。小さな勝利大きなアイテムから少数のフィールドをホットに読む

トレードオフは正直です。インデックスは、読み取りキャパシティを節約するために、ストレージと 書き込みキャパシティを費やします。重いアイテムから薄いスライスを頻繁に読むなら見合います。 一度きりの GetItem を削るためには見合いません。インデックスの型を選ぶには GSI vs LSIを、ホットパスに載せる前に GSI の読み取りが古くなりうるときを参照して ください。

落とし穴と次のステップ

  • 請求が小さくなると期待しない。 射影だけでは RCU は決して変わりません。数字が動か なかったなら、それはバグではなく文書化された挙動です。
  • 予約語をプレースホルダ化する。 式の中の素の namestatus はリクエストを 失敗させます——# でマッピングしましょう。
  • 常にキー属性を含める。 ペイロードへの追加はごくわずかで、アイテムをページングしたり 再取得したりできるようにします。
  • カバリングインデックスに手を伸ばすのは、ホットなパターンが大きなアイテムから少数の フィールドを読むときだけ。まず書き込み/ストレージのコストを天秤にかけましょう。

ProjectionExpression とその属性名マップを式ビルダー で組み立て、DynoTable を試して自分のテーブルに対してこれらの射影を実行し、 レスポンスが縮む様子を見てみましょう。


  1. AWS DynamoDB デベロッパーガイド、Using projection expressions in DynamoDB — 読み取りキャパシティは、いかなる ProjectionExpression が適用される前のアイテムサイズに基づく。https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.ProjectionExpressions.html
  2. AWS DynamoDB デベロッパーガイド、Reserved Words in DynamoDBhttps://docs.aws.amazon.com/amazondynamodb/latest/developerguide/ReservedWords.html
  3. AWS DynamoDB デベロッパーガイド、Attribute Projections (KEYS_ONLY / INCLUDE / ALL)。https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/GSI.html

更新日