DynamoDB のフィルタリング戦略
DynamoDB での「フィルタリング」は、同じ言葉をまとった 4 つの異なるものを意味します。
3 つはデータが読み取られ課金される 前 に絞り込み、1 つ — Filter という名の 1 つ —
は 後 に絞り込みます。どれがどれかを知ることが、スキルのほとんどです。
DynamoDB でフィルタリングはどう機能するのか?
DynamoDB にはフィルタする方法が 4 つあり、課金された後に走るのは 1 つだけです。 はパーティションを選び、ソートキーはスライスを絞り、スパースインデックスは属性の有無でフィルタします — 3 つとも計測の前に読み取りコストを削ります。FilterExpression は読み取りの後に走るので、レスポンスは縮めても課金は決して縮めません。
- は最も安いフィルタです。パーティションを選ぶので、 テーブルの残りには決して触れません。
- はパーティション 内 を
begins_with、between、<、>で フィルタします — なお課金前で、なお安価です。 - は不在でフィルタします。アイテムは、インデックス対象の 属性を持つ場合にのみインデックスに現れるため、インデックス が フィルタされたセットです。
FilterExpressionは罠です。DynamoDB が読み取りを計測した後に走るので、レスポンスの サイズは削っても課金は決して削りません。
例を用意する
商品カタログ。1 つのテーブル、パーティションキー PK、ソートキー SK。
PK = "DEPT#kitchen" SK = "PROD#00194"
すべての商品は price、inStock(真偽値)、clearanceAt(unix タイムスタンプ、クリア
ランス対象としてマークされたアイテムにのみ存在)も持ちます。ある部門のアイテムは
パーティションを共有し、商品 id でソートされます。
4 つのアクセスパターンが欲しいとします。それぞれが異なるフィルタリング戦略に対応し —
そのいずれかで誤った選択をすると、永遠に支払い続ける 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> のソートキーは役立ちません — 価格をキーに焼き込むことになります。
それは ソートキー戦略 の決定で、クエリ時ではなく 設計時に行われます。
スパースインデックスでフィルタする
「今クリアランス中のすべてをくれ」。ほとんどの商品はそうではないので、その数少ないものを 見つけるためにカタログを読みたくはありません。
スパースインデックス はこれを不在で解決します。 は、アイテムがそのインデックスのキー属性の 両方 を持つ場合にのみ、そのアイテムを含みます。
定数 clearance = "CLEARANCE" フラグを GSI のパーティションキーとして設定し —
クリアランスアイテムにのみ書き込み — clearanceAt をソートキーにすれば、インデックスは
それ以外の何も保持しません。
AWS はこれを明記しています。グローバルセカンダリインデックスは、インデックスのキー属性を 持つアイテムのみを含むため、キー属性を欠いたアイテムは単純に伝播されません (AWS — Take advantage of sparse indexes)。
これでクエリはクリアランスアイテムだけを読み、それらの分だけ課金されます。
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 が完了した後、結果が返される前に適用され」、「Query は
フィルタ式の有無にかかわらず同じ量の読み取りキャパシティを消費します」 — あなたはすでに
完全な読み取り分を支払いました(AWS — Filter expressions for Query)。
だから kitchen に 10,000 商品があり、12 が在庫ありなら、10,000 を読む対価を支払います。
レスポンスは小さいですが、課金はそうではありません。FilterExpression は回線を渡る
ペイロードを縮めるのであって、読み取りを縮めることは決してありません。
もう 1 つ、より鋭い刃があります。ページネーションはフィルタリングの 前 に計測されます。 1 ページは 1 MB の 読み取られた アイテムであって、1 MB のマッチではありません。
フィルタは LastEvaluatedKey が設定された空のページを返しうります — DynamoDB は完全な
1 メガバイトを読み、何にもマッチせず、空の配列を渡しました。あなたはページ送りを続け、
すべての空のページの対価を支払いました。
式を — 名前、値、そして正しい予約語のエスケープを — DynamoDB 式ビルダー
で組み立てれば、#inStock/:val のプレースホルダーが一発で正しくなります。
下のビルダーは FilterExpression を伴う Scan — まさに上記のアンチパターン — に
プリセットされています。フィルタがキーのスライスではなくテーブル全体で走ることに注目
してください。
4 つを比較する
| いつフィルタするか | 読み取りコストを削る? | 述語の力 | セットアップのコスト | |
|---|---|---|---|---|
| パーティションキー | 読み取り前 | はい — 1 パーティション | 等価のみ | 無料(キーそのもの) |
| ソートキー | 読み取り前 | はい — 1 スライス | 範囲 / begins_with | ソートキー設計 |
| スパースインデックス | 読み取り前 | はい — インデックスのみ | 属性の存在 | 追加の GSI + 書き込みコスト |
| FilterExpression | 読み取り後 | いいえ | ほぼ任意の条件 | なし |
表を上から下へ読んでください。述語の力は 上がり、コスト管理は 下がり ます。
FilterExpression が何でも正確に表現できるのは、まさにすでに読み取られたアイテムに対して
走るからです — それが、お金を節約できない同じ理由でもあります。
DynoTable で見る
フィルタ付きの Query を実行するとき、読み取られた アイテムと 返された アイテムの
差がすべての物語です。DynoTable は、フィルタされた読み取りがストリームされる中で、スキャン
されたアイテムを返されたアイテムの隣に表示します — だからパーティション全体を静かに読んで
いるフィルタは、月次の請求書に隠れているのではなく、目に見えます。
フィルタが答えられない本物のアイテムをまたぐ問い — 「部門ごとの平均価格」、「在庫のある
商品をそのレビューに結合」 — には、DynoTable の SQL Workbench が GROUP BY、JOIN、集計を、
テーブル全体の Scan にコンパイルする代わりに、有界な結果セットに対してクライアント側で
実行します。
落とし穴と次のステップ
FilterExpressionを主要なアクセスパスにしない。 パターンが一般的なら、キーか スパースインデックスにモデル化しましょう。フィルタは最後のわずかな絞り込みのためのもので、 その大部分のためではありません。- 空のページに注意。 フィルタ付きのクエリは、何も返さずに長い間ページ送りしうります。
LastEvaluatedKeyを尊重し、空のページが「完了」を意味すると仮定しないでください。 - スパースインデックスは無料ではない。 そこに着地するすべてのアイテムに対して書き込み キャパシティとストレージがかかります — 属性がまれなときは安く、そうでないときは安く ありません。
フィルタ付きの読み取りが実際にいくらかかるかを 料金計算ツール で見積もり、DynoTable を試して 自分のテーブルで消費キャパシティを返された行に 対して観察しましょう。