DynamoDB の射影式 (Projection Expression)
射影式は DynamoDB の SELECT col1, col2 です。カンマ区切りの名の
リストで、GetItem、Query、Scan にアイテム全体ではなくそれらの属性だけを返すよう
伝えます。
DynamoDB の射影式は読み取りコストを削減しますか?
いいえ。ProjectionExpression はレスポンスのペイロードを削りますが、課金される読み取り
キャパシティは削りません。DynamoDB はストレージからアイテム全体を読み取り、そのディスク上の
サイズでを計量し、それから指定しなかった属性を出力の途中で落とします。
実際に読み取りコストを削るには、代わりにカバリングを使います。
- 削るのはペイロードであって、読み取りコストではない。 DynamoDB はストレージから
アイテム全体を読み取って (そして課金して)、それから指定しなかった属性を出力の途中で
落とします。
ProjectionExpressionはネットワークの最適化であって、キャパシティの最適化 ではありません。 - 公開サブセットを取得する方法。 呼び出し元が見てよい少数の属性を指定します。残りは テーブルを決して離れません。
- 予約語になりうるものには
#nameプレースホルダを使う。 式の中の素の属性名は、 DynamoDB の約 570 の予約語と衝突し、リクエストを失敗させます。 - 本当の読み取り節約には、代わりにカバリングインデックスを使う。 必要な列だけを射影する は、それ自身の (より小さい) サイズで読まれます。
実際に何を節約するのか
SQL から来ると、SELECT a, b は SELECT * より少なくスキャンすると思い込むでしょう。
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つのフィールドが必要です。アイテム全体を取得すると、
emailAddress、lastLoginIp、internalRiskScore が、それらを決して見るべきでない
コンテキストへ渡ってしまいます。公開サブセットだけを指定しましょう。
GetItem PK = "PROFILE#ada" SK = "PROFILE#ada"
ProjectionExpression: displayName, avatarUrl, bio
レスポンスは3つの属性を運びます。非公開のものはテーブルに留まります——到着後にアプリが _フィルターして_落とすのではなく、そもそもレスポンスに一切シリアライズされないのです。 それがセキュリティ上の勝利であり、秘密がいったん境界を越えてしまうと取り返しがつかない、 まさにそのものです。
このリクエストそのもの——名前、プレースホルダ、SDK 呼び出し——を
DynamoDB 式ビルダーで組み立ててコピーできます。
ProjectionExpression と ExpressionAttributeNames のマップをあなたのために生成します。
下のプリセットでフィールドを追加または削除して、ProjectionExpression が変わる様子を
見てみましょう——指定した属性だけが返ります。
# プレースホルダで予約語をエスケープする
きれいな射影が吹き飛ぶのがここです。DynamoDB は長い語のリストを予約しています——name、
status、comment、size、timestamp、その他数百。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_ONLY、INCLUDE、ALL を選びます。3 KEYS_ONLY や狭い INCLUDE
のインデックスはアイテムあたり物理的に小さいので、それに対する Query はその小さいサイズ
で計量されます。
それがカバリングインデックスです。クエリはインデックスだけで完全に答えられ、ベース テーブルへの往復はありません。ホットな読み取りパターンが、大きなアイテムから常に少数の 属性しか必要としないときに使いましょう。
ProjectionExpression | カバリング GSI | |
|---|---|---|
| ペイロードを削る | はい | はい |
| 読み取りコストを削る | いいえ | はい — インデックスのサイズで読む |
| 追加のストレージ | なし | 射影したフィールドの2つ目のコピー |
| 追加の書き込みコスト | なし | 書き込みがインデックスに伝播する |
| 最適な用途 | 非公開フィールドを隠す。小さな勝利 | 大きなアイテムから少数のフィールドをホットに読む |
トレードオフは正直です。インデックスは、読み取りキャパシティを節約するために、ストレージと
書き込みキャパシティを費やします。重いアイテムから薄いスライスを頻繁に読むなら見合います。
一度きりの GetItem を削るためには見合いません。インデックスの型を選ぶには
GSI vs LSIを、ホットパスに載せる前に
GSI の読み取りが古くなりうるときを参照して
ください。
落とし穴と次のステップ
- 請求が小さくなると期待しない。 射影だけでは RCU は決して変わりません。数字が動か なかったなら、それはバグではなく文書化された挙動です。
- 予約語をプレースホルダ化する。 式の中の素の
nameやstatusはリクエストを 失敗させます——#でマッピングしましょう。 - 常にキー属性を含める。 ペイロードへの追加はごくわずかで、アイテムをページングしたり 再取得したりできるようにします。
- カバリングインデックスに手を伸ばすのは、ホットなパターンが大きなアイテムから少数の フィールドを読むときだけ。まず書き込み/ストレージのコストを天秤にかけましょう。
ProjectionExpression とその属性名マップを式ビルダー
で組み立て、DynoTable を試して自分のテーブルに対してこれらの射影を実行し、
レスポンスが縮む様子を見てみましょう。
- AWS DynamoDB デベロッパーガイド、Using projection expressions in DynamoDB — 読み取りキャパシティは、いかなる
ProjectionExpressionが適用される前のアイテムサイズに基づく。https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.ProjectionExpressions.html ↩ - AWS DynamoDB デベロッパーガイド、Reserved Words in DynamoDB。https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/ReservedWords.html ↩
- AWS DynamoDB デベロッパーガイド、Attribute Projections (
KEYS_ONLY/INCLUDE/ALL)。https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/GSI.html ↩