DynamoDB の Type 属性
SQL では、行が属するテーブル そのものが その型です — documents の行はドキュメントです。DynamoDB のシングルテーブルは、あらゆるエンティティを 1 つのスキーマの下に混在させるため、項目には「これは何なのか」という問いに対する組み込みの答えがありません。
Type 属性 はその答えを取り戻します。すべての項目に付ける、そのエンティティ名を表すただの文字列です。
DynamoDB の Type 属性とは何ですか?
Type 属性は、EntityType: "Document" のように、すべての項目に刻み込む単なる文字列で、その項目が表すエンティティを名指しします。シングルテーブルは多くのエンティティを 1 つのスキーマの下に混在させるため、項目には組み込みの型がありません。Type はそれを取り戻し、コードが行を識別し、GSI を 1 つのエンティティに絞り込み、マイグレーションを乗り切れるようにします。
- 書き込みのたびに Type を刻む。 すべての項目に
EntityType: "Document"という属性を 1 つ、例外なく付けます。コストは数バイト、後で助かります。 - 混在パーティションの中でエンティティを識別する。
Queryはワークスペース、ドキュメント、コメントをまとめて返しますが、Type があればキーのプレフィックスを解析せずにどれがどれかをコードが判断できます。 - 上での単一エンティティフィルタリングを可能にする。 Type をインデックスに射影すれば、オーバーロードされたインデックスをちょうど 1 つのエンティティ型に絞り込めます。
- マイグレーションのための脱出口になる。 再モデリングのためにエクスポートしたり、あるエンティティを独自のテーブルへ移したりするとき、Type は分割の基準となる列です。
なぜ混在テーブルは型を失うのか
シングルテーブル設計は、PK や SK のような汎用キーの裏に、あらゆるエンティティを 1 つのテーブルへ格納します。それこそが狙いです — 1 回の Query が親とその子をまとめて返します。しかしそれは、パーティションが不均質になることを意味します。
SaaS のドキュメント共同編集アプリを考えてみましょう。1 つのワークスペースのパーティションには、ワークスペースのレコード、そのドキュメント、そしてそれらのドキュメントへのコメントが入っています。
| PK | SK | attributes |
|---|---|---|
| WS#acme | META | name, plan, seats |
| WS#acme | DOC#a1#META | title, owner, wordCount |
| WS#acme | DOC#a1#CMT#0007 | author, body, createdAt |
| WS#acme | DOC#a1#CMT#0008 | author, body, createdAt |
Query PK = "WS#acme" は 4 つの項目すべてを 1 回の課金対象の読み取りで返します。ここでコードの手元には生の項目のリストがありますが、どれがドキュメントでどれがコメントかを確実に見分ける手段はありません — SK を文字列マッチングする以外には。しかしそれは、キーのフォーマットが変わった瞬間に壊れる脆いやり方です。
すべての項目に Type を刻む
対処法は、書き込みのたびに 1 つの属性でエンティティ名を示すことです。
| PK | SK | EntityType | title |
|---|---|---|---|
| WS#acme | META | Workspace | — |
| WS#acme | DOC#a1#META | Document | Q3 Roadmap |
| WS#acme | DOC#a1#CMT#0007 | Comment | — |
item.EntityType === "Document" で分岐するのは安定した等価チェックです。SK.startsWith("DOC#") && SK.includes("#CMT#") を解析するのは、キーを変更すると壊れる当て推量です。Type は読み取りロジックをキーのエンコーディングから切り離します — それが本当の勝ちどころです。
1 回の読み取りが 3 つのエンティティ型を返し、Type 属性がキーに触れることなく各項目を正しいハンドラへ振り分けます。
GSI を 1 つのエンティティに絞り込む
Type はインデックスで真価を発揮します。「このワークスペースで最近変更されたものすべてを新しい順に」表示するために、GSI1PK = WS#acme、GSI1SK = updatedAt をキーとする GSI を追加したとします。オーバーロードされたインデックスはドキュメント も コメントも一緒にすくい上げますが、フィード UI ではドキュメントだけが欲しいかもしれません。
絞り込む方法は 2 つあり、その違いはお金に直結します。
| アプローチ | かかるコスト | 使いどころ |
|---|---|---|
Type への FilterExpression | 一致する項目をすべて読み取って課金し、読み取り後に非一致を捨てる | 結果に混在エンティティがまれ。手早く出したいとき |
スパースインデックス(GSI1PK を対象エンティティにのみ書き込む) | 欲しいエンティティだけがインデックスに載る | 1 つのエンティティが支配的で、無駄をゼロにしたいとき |
FilterExpression は項目が読み取られた 後、キャパシティが消費された 後 に実行されます — フィルタリングが読み取りコストを下げないことを AWS は明言しています(DynamoDB デベロッパーガイド: FilterExpression)。Type でのフィルタリングは正直な手段であって、無料ではありません — 捨てるコメントの分も支払います。
us-east-1 のオンデマンドでは、2 KB のアイテムが混在した 100 件を返す GSI の Query は、結果整合性でおよそ 100 RCU です — そして EntityType への FilterExpression は、コメントを捨てる前にすべての行を計測します。コメントを一切インデックスしないスパースインデックスなら、課金されるのはドキュメントの行だけです。両方の形を料金計算ツールでモデリングしてみましょう。
フィードをドキュメントに絞り込むには、クエリに Type 属性への条件を持たせます。FilterExpression、名前、値は DynamoDB 式ビルダーで組み立てましょう — 予約語をタイプミスしないように #t = :doc というプレースホルダーを出力してくれます。
KeyConditionExpression GSI1PK = :ws
FilterExpression #t = :doc
ExpressionAttributeNames { "#t": "EntityType" }
ExpressionAttributeValues { ":ws": "WS#acme", ":doc": "Document" }
インデックスに ドキュメントだけ を載せてフィルタを完全に省きたいなら、GSI1PK をドキュメント項目にのみ書き込みます — です。GSI キーを持たない項目はインデックスに複製されないため、読み取りはドキュメントだけに触れます。どの項目が該当するかを書き込み側に伝えるのが、まさに Type 属性です。
値を安定かつ単一に保つ
値は一度だけ決めて、それを enum のように扱います。Document であって、あるときは Doc、あるときは document、ではいけません — ブレる値は値がないより悪いのです。等価チェックが一方のケーシングでは通り、もう一方を静かに取りこぼすからです。
項目ごとに Type は 1 つ。ある項目が 2 つのエンティティのように感じられるなら、それはたいていモデリングの臭いです — 1 つの行が 2 つの顔を持つのではなく、それぞれ独自のコレクションやソートキー範囲に分かれた 2 つの項目であるべきです。
マイグレーションでの見返り
必要になる前に Type を刻んでおく理由は、再モデリングです。推奨される再モデリングの経路はエクスポート、変換、再インポートであり、AWS はまさにこの種のオフラインでの作り直しのために S3 への一括エクスポートを文書化しています(DynamoDB を S3 へエクスポート)。
その日が来たとき、Type は GROUP BY する対象の列になります。コメントを独自のテーブルへ持ち上げたい、あるいは分析ウェアハウス向けにエクスポートをエンティティ単位のファイルへ再正規化したい? ダンプを EntityType で分割します。それがなければ、数百万行にわたってキーをリバースエンジニアリングする作業に逆戻りです。
次のステップ
Type 属性は安価な保険です。混在した読み取りの中でエンティティを識別し、オーバーロードされた GSI をフィルタし、再モデリング時にきれいに分割します。初日から書き込みのたびに刻んでおきましょう — 稼働中のテーブルに後付けするとなると、フルバックフィルが必要になります。
関連する読み物: この属性が支える混在パーティションのパターンについてはシングルテーブル設計、スパースインデックスの裏側でインデックスの形を選ぶには GSI vs LSI、FilterExpression が読み取りコストを決して節約しない理由には Query vs Scan を参照してください。
Type へのフィルタを DynamoDB 式ビルダーで構築し、DynoTable を試して実際の混在エンティティテーブルを閲覧し、すべての項目にわたって Type 列が揃うのを確かめてください。