DynamoDB の Type 属性
SQL では、行が属するテーブル そのもの が型を表します — documents の行はドキュメントです。DynamoDB のシングルテーブルはあらゆるエンティティを1つのスキーマの下に混在させるため、アイテム自身は「これは何か?」という問いに答える手段を持ちません。
Type 属性 はその答えを取り戻します。すべてのアイテムに、それが表すエンティティ名を記したただの文字列を載せるのです。
DynamoDB の Type 属性とは何ですか?
Type 属性は、すべてのアイテムに刻む単純な文字列です — EntityType: "Document" のように — そのアイテムが表すエンティティを名づけます。シングルテーブルは多くのエンティティを1つのスキーマの下に混在させるため、アイテム自身には組み込みの型がありません。Type はその型を取り戻します。コードが行を識別し、GSI を1つのエンティティに絞り込み、マイグレーションを乗り切れるようになります。
- 書き込みのたびに Type を刻む。 すべてのアイテムに1つの属性 —
EntityType: "Document"— を例外なく付ける。数バイトのコストで、後々を救う。 - 混在パーティションでエンティティを識別する。
Queryはワークスペース、ドキュメント、コメントをまとめて返す。Type があれば、キーのプレフィックスを解析せずにコードがどれがどれかを判別できる。 - GSI を単一エンティティに絞り込める。 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" は、課金される1回の読み取りで4つすべてのアイテムを返します。このときコードの手元には生のアイテムのリストがあるだけで、どれがドキュメントでどれがコメントかを確実に言い当てる手段はありません — 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 に Type) | 欲しいエンティティだけがインデックスに入る | 1つのエンティティが支配的で、無駄をゼロにしたいとき |
FilterExpression はアイテムが読み取られ、キャパシティが消費された 後 に実行されます — フィルタリングは読み取りコストを減らさないと AWS は明言しています(DynamoDB Developer Guide: FilterExpression)。Type でのフィルタリングは正直であって無料ではありません。捨てたコメントの分も支払うのです。
フィードをドキュメントに絞り込むには、クエリに 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つのエンティティのように感じられるなら、たいていモデリングの臭いです — それは各自のコレクションまたはソートキー範囲に置かれた2つのアイテムであるべきで、二役を兼ねる1つの行であってはなりません。
マイグレーションでの見返り
必要になる前に Type を刻んでおく理由は、再モデリングです。推奨される再モデリングの手順はエクスポート・変換・再インポートで、AWS はまさにこの種のオフライン整形のために S3 への一括エクスポートを文書化しています(DynamoDB を S3 へエクスポート)。
その日が来たとき、Type は GROUP BY する列になります。コメントを専用テーブルへ持ち上げたい、あるいは分析用ウェアハウスのためにエクスポートをエンティティ単位のファイルへ再正規化したい? EntityType でダンプを分割します。Type が無ければ、数百万行にわたってキーをリバースエンジニアリングする羽目に逆戻りです。
次のステップ
Type 属性は安い保険です。混在読み取りでエンティティを識別し、オーバーロードした GSI をフィルタし、再モデリングのときにきれいに分割できます。初日から書き込みのたびに刻んでおきましょう — 稼働中のテーブルに後付けするには、全件バックフィルが必要になります。
関連する読みもの: この属性が支える混在パーティションのパターンについてはシングルテーブル設計、スパースインデックスの背後にあるインデックスの形を選ぶにはGSI と LSI、FilterExpression が読み取りコストを決して節約しない理由はQuery と Scan。
DynamoDB 式ビルダーで Type へのフィルタを組み立て、DynoTable を試して実際の混在エンティティテーブルを閲覧し、すべてのアイテムにわたって Type 列が揃う様子を確かめましょう。