DynamoDB のフィルタリング戦略
DynamoDB における「フィルタリング」は、同じ言葉をまとった4つの異なるものを意味します。3つはデータを読み取られて課金される前に絞り、1つ — Filter という名のもの — は後に絞ります。どれがどれかを知ることが、スキルのほとんどです。
DynamoDB のフィルタリングはどう動くのか?
DynamoDB にはフィルタリングの方法が4つあり、課金された後に実行されるのはそのうち1つだけです。パーティションキーはパーティションを選び、ソートキーはスライスを絞り、スパースインデックスは属性の存在によって絞ります — この3つはいずれも計測される前に読み取りコストを削ります。FilterExpression は読み取りの後に実行されるので、レスポンスは縮めますが、請求額は決して縮めません。
- パーティションキーは最も安価なフィルタです: パーティションを選ぶので、テーブルの残りに決して触れません。
- ソートキーは
begins_with、between、<、>でパーティション内を絞ります — これも課金前で、これも安価です。 - スパースインデックスは不在によって絞ります: アイテムはインデックス対象の属性を持つ場合にのみインデックスに現れるので、インデックスそのものが絞られた集合です。
FilterExpressionは罠です: DynamoDB が読み取りを計測した後に実行されるので、レスポンスのサイズは削りますが、請求額は決して削りません。
例を設定する
製品カタログ。1つのテーブル、パーティションキー PK、ソートキー SK:
PK = "DEPT#kitchen" SK = "PROD#00194"
すべての製品はさらに price、inStock(ブール値)、clearanceAt(unix タイムスタンプ、クリアランス対象とマークされたアイテムにのみ存在)を持ちます。1つの部門のアイテムはパーティションを共有し、製品 ID でソートされます。
4つのアクセスパターンが欲しいとします。それぞれが異なるフィルタリング戦略にマッピングされ — どれか1つでも選択を誤れば、永久に料金を払う Scan になります。
パーティションキーでフィルタする
「kitchen のすべての製品をください。」パーティションキーがこれに直接答えます:
Query PK = "DEPT#kitchen"
DynamoDB は正確に1つのパーティションを読みます。テーブル内の他のものには触れず、課金もされません。これは重要な意味で無料である唯一のフィルタです — それが Query と Scan の違いです。
SQL の出身者には、これは逆向きに感じます: インデックスをスキャンする WHERE department = 'kitchen' はなく、ただパーティションを名指しします。名指しできないなら、それはクエリの問題ではなく、モデリングの問題です。
ソートキーでフィルタする
「PROD#00100 以降の kitchen 製品をください。」ソートキーはパーティション内を絞り、しかも読み取りが計測される前にそうします:
Query PK = "DEPT#kitchen" AND SK between "PROD#00100" AND "PROD#00200"
ソートキー条件は意図的に限定されています: =、<、<=、>、>=、between、begins_with。OR も任意の述語もありません。
その制約こそが、読み取りをターゲットに保つものです — DynamoDB はパーティション全体ではなく、連続したスライスを辿ります。
ここでのレバーはソートキーをどうエンコードするかです。パターンが「価格帯ごと」なら、PROD#<id> のソートキーは役に立ちません — 価格をキーに焼き込むことになります。
それはソートキー戦略の決定で、クエリ時ではなく設計時に行われます。
スパースインデックスでフィルタする
「現在クリアランス中のものをすべてください。」ほとんどの製品はそうではないので、その数少ないものを見つけるためにカタログを読みたくはありません。
スパースインデックスは不在によってこれを解決します。グローバルセカンダリインデックスは、そのアイテムがインデックスのキー属性の両方を持つ場合にのみ、そのアイテムを含みます。
GSI のパーティションキーを clearanceAt(クリアランスアイテムにのみ存在)に設定すると、インデックスはそれ以外を何も保持しません。
AWS はこれを明言しています: GSI は「インデックス対象の属性を持つアイテムのみを含む」ので、キー属性が欠けているアイテムは単に伝播されません(AWS — スパースインデックスを活用する)。
これでクエリはクリアランスアイテムだけを読み、それらの分だけ課金されます:
Query ON ClearanceIndex GSI_PK = "CLEARANCE" (sorted by clearanceAt)
フィルタはデータを書き込んだときに起こりました — そもそも clearanceAt を設定するかどうかを選ぶことによって。インデックスが絞られた集合です。どのインデックス型が合うかは GSI と LSI の比較を参照してください。
FilterExpression でフィルタする
「在庫のある kitchen 製品をください。」inStock はキー属性ではないので、FilterExpression に手を伸ばします:
Query PK = "DEPT#kitchen"
Filter inStock = true
ここが罠です。DynamoDB は kitchen パーティションのすべてのアイテムを読み、それらすべてのキャパシティを計測し、それから在庫切れのものを落とします。
公式のルール: フィルタ式は「Query が完了した後、結果が返される前に適用され」、「追加の読み取りキャパシティユニットを消費しない」 — あなたはすでにフル読み取りの分を払っています(AWS — Query のフィルタ式)。
だから kitchen に1万個の製品があり、12個が在庫ありなら、1万個を読む分を払います。レスポンスは小さいですが、請求額はそうではありません。FilterExpression は回線を渡るペイロードを縮めますが、読み取りは決して縮めません。
もう1つの、より鋭い刃があります: ページネーションはフィルタリングの前に計測されます。1ページは1 MB の読み取られたアイテムであって、1 MB の一致ではありません。
フィルタは LastEvaluatedKey がセットされた空のページを返すことがあります — DynamoDB はフルの1メガバイトを読み、何も一致せず、空の配列を渡したのです。ページングを続け、すべての空のページの分を払います。
その式 — 名前、値、そして正しい予約語のエスケープ — を DynamoDB Expression Builder で構築すれば、#inStock/:val のプレースホルダが一発で正しくなります。
4つを比較する
| いつフィルタするか | 読み取りコストを削る? | 述語の表現力 | セットアップのコスト | |
|---|---|---|---|---|
| パーティションキー | 読み取り前 | はい — 1パーティション | 等価のみ | 無料(キーそのもの) |
| ソートキー | 読み取り前 | はい — スライス | 範囲 / begins_with | ソートキー設計 |
| スパースインデックス | 読み取り前 | はい — インデックスのみ | 属性の存在 | 追加の GSI + 書き込みコスト |
| **FilterExpression | 読み取り後 | いいえ** | ほぼ任意の条件 | 無し |
表を上から下へ読んでください: 述語の表現力は上がり、コスト制御は下がります。FilterExpression が何でも正確に表現できるのは、それがすでに読まれたアイテムに対して実行されるからです — それがまさに、お金を節約できない理由でもあります。
DynoTable で見る
フィルタ付きで Query を実行すると、読み取られたアイテムと返されたアイテムの差がすべての物語です。DynoTable は消費キャパシティを結果件数の隣に表示します — だからパーティション全体をこっそり読んでいるフィルタが、月々の請求書に隠れるのではなく、可視になります。
フィルタが答えられない本物のクロスアイテムの問い — 「部門ごとの平均価格」、「在庫のある製品とそのレビューの結合」 — には、DynoTable の SQL Workbench が、テーブル全体の Scan にコンパイルする代わりに、有界の結果セットに対してクライアント側で GROUP BY、JOIN、集計を実行します。
落とし穴と次のステップ
FilterExpressionを主要なアクセスパスとして使わない。 あるパターンが一般的なら、それをキーかスパースインデックスにモデル化しましょう。フィルタは最後のわずかな絞り込みのためのもので、大部分のためではありません。- 空のページに注意。 フィルタ付きクエリは、何も返さずに長くページングし得ます。
LastEvaluatedKeyを尊重しましょう。空のページが「完了」を意味すると思い込まないこと。 - スパースインデックスは無料ではない。 そこに着地するすべてのアイテムについて、書き込みキャパシティとストレージのコストがかかります — 属性が稀なら安価ですが、そうでなければそれほどでもありません。
フィルタ付きの読み取りが実際にいくらかかるかをキャパシティ計算ツールで見積もり、DynoTable を試して、自分のテーブルで消費キャパシティを返却行数と並べて観察しましょう。