DynamoDB のキーオーバーロード
SQL から来ると、列は永遠に1つの意味を持ちます。orders.created_at は常に日付、
users.email は常にメールです。キーオーバーロードはそれを捨てます。パーティション
キーとに pk、sk といった汎用的な名前を付け、各アイテム型が
それぞれ異なる意味をそこに流し込めるようにするのです。1つのテーブル、多数のエンティティ、
1つの形。
DynamoDB のキーオーバーロードとは何ですか?
キーオーバーロードとは、多数のエンティティ型を pk/sk のような汎用的なキー名の下に
1つのテーブルへ格納し、型を値にエンコードすること (USER#u_3001、INVOICE#2026-0014) です。
属性名は中立のままなので、ユーザー、請求書、イベントが1つのパーティションを共有します。値が
型を運び、ソートキーのプレフィックスによって1回の Query が begins_with で各エンティティ
を切り出せます。
- 汎用的なキー名、型付きの値。 キーに
pk/skと名付け、エンティティ型を値に入れます。pk = "TENANT#acme"、sk = "USER#u_3001"。名前は無知で、値が型を運びます。 - これがシングルテーブル設計を成立させる仕組み。 オーバーロードなしでは、共有テーブルは
ただのがらくた入れです。それがあれば、あらゆるエンティティが
Queryできるパーティション に収まります。 begins_withが見返り。 ソートキーの型プレフィックスによって、1回のQueryが エンティティ全体を、あるいはその一部を、Scanもフィルターもなしに引き出せます。- 代償は可読性。 生の
pk/skのダンプは何も教えてくれません。プレフィックスを デコードするビューアが必要です。さもなければ文字列に目を凝らすことになります。
汎用的な名前が本物の名前に勝る理由
DynamoDB はテーブルごとに最大2つのキー属性しか与えず、Query は1つのパーティションキー
しか対象にできません。ですからキーを userId と名付ければ、そのテーブルにきれいに置ける
のはユーザーアイテムだけになり、それ以外はすべて userId を偽装するか、自前のテーブルに
移すしかありません。
オーバーロードはそれを回避します。pk のような中立的な名前はどのエンティティにも縛られ
ないので、ユーザー、請求書、監査イベントがすべて同じキー属性と同じテーブルを共有できます。
アイテムが何であるかを語るのは属性名ではなく値です。
これがシングルテーブル設計を理論から実際にクエリできる ものへと変える一手です。共有テーブルは容れ物で、オーバーロードはその中で別々のエンティティ を共存させる仕組みです。
マルチテナントの例
SaaS の請求プロダクトを運営しているとしましょう。各テナントにはメンバー、請求書、監査 証跡があります。3つのテーブルの代わりに、すべてを1つに入れてキーをオーバーロードします。
| pk | sk | attributes |
|---|---|---|
| TENANT#acme | META | name="Acme Inc", plan="team" |
| TENANT#acme | USER#u_3001 | email, role="admin" |
| TENANT#acme | USER#u_3002 | email, role="member" |
| TENANT#acme | INVOICE#2026-0014 | amount_cents, status="paid" |
| TENANT#acme | INVOICE#2026-0015 | amount_cents, status="open" |
| TENANT#acme | EVENT#2026-06-23T09:12Z | actor="u_3001", action="invite" |
どの行も pk = "TENANT#acme" を共有するので、1つのを形成します。すべてが同じ場所に配置され、すべてが1回のパーティション
読み取りで到達可能です。
本当の仕事をしているのはソートキーのプレフィックスです。エンティティをグループ化し、 なおかつ順序付けます。
オーバーロードされたコレクションをクエリする
型がソートキーのプレフィックスに宿っているので、begins_with は何もスキャンせずに
パーティションをエンティティ単位で切り出します。
Query pk = "TENANT#acme" -- the entire tenant, every type
Query pk = "TENANT#acme" AND begins_with(sk, "USER#") -- just members
Query pk = "TENANT#acme" AND begins_with(sk, "INVOICE#") -- just invoices
支払うのは条件に一致したアイテムの分だけで、パーティション全体ではありません。これは
フィルター付きのScan——読み取った行を後で捨てるために支払う——
の正反対です。AWS はこれをキー条件と呼びます。データがパーティションを離れる前に、
キーに対して実行されます。
その begins_with 条件を手で組み立てるなら、型タグを正しく書きましょう。USER# の
つもりで USERS# と書いてしまうと、何も返らず、しかも黙って失敗します。
式ビルダーは KeyConditionExpression と
ExpressionAttributeValues のマップを生成するので、プレフィックスが実際に書いたものと
一致します。
インデックスもオーバーロードする
同じ手口はにも当てはまります。汎用的なキー名 gsi1pk、gsi1sk を与え、
各エンティティに必要なものを書かせます。そうすればインデックスが、ベーステーブルには
できないパターンに答えます。
| pk | sk | gsi1pk | gsi1sk |
|---|---|---|---|
| TENANT#acme | INVOICE#2026-0015 | STATUS#open | 2026-06-30 |
| TENANT#acme | INVOICE#2026-0014 | STATUS#paid | 2026-06-12 |
| TENANT#beta | INVOICE#2026-0099 | STATUS#open | 2026-06-25 |
これで Query gsi1 WHERE gsi1pk = "STATUS#open" は、すべてのテナントにまたがって
未払いの請求書をすべて期日順で列挙します。ベーステーブルのテナント単位のキーでは決して
提供できないクロスパーティションのビューです。別のエンティティが gsi1 を自分なりの意味で
再利用できる (たとえば gsi1pk = "ROLE#admin") ので、1つのインデックスが複数の読み取りを
カバーします。ただし GSI は結果整合性であることを忘れずに。その
書き込みはベーステーブルより遅れます。
DynoTable でやってみる
生のオーバーロードされたキーは読むのが苦痛です。INVOICE#2026-0015 と
EVENT#2026-06-23T09:12Z はフラットなリストでは混ざり合ってしまいます。パーティション
でグループ化してプレフィックスを浮き上がらせるビューアが、がらくた入れを再びエンティティ
へと戻します。

落とし穴
- 区切り文字は一度決めたら決して変えない。
#が慣例です。エンティティ間で#と:を混ぜると、何も警告しないままbegins_withが壊れます。 - 範囲演算が必要な値をオーバーロードしない。
INVOICE#2026-0015というソートキーは 数値ではなく辞書順でソートされます。id をし、文字列順が意図 する順序に一致するよう ISO-8601 の日付を使いましょう。 - プレフィックスの名前空間を予約する。 どちらも
USERで始まる2つのエンティティ型 (たとえばUSER#とUSERGROUP#) はbegins_with(sk, "USER")の下で衝突します。 プレフィックスは最初の文字から曖昧さのないものにしましょう。 - キーの前に読み取りを設計する。 オーバーロードは、列挙済みのアクセスパターンに応える ものです。まだ読み取りが分からないなら、先にシングルテーブル設計 を参照してください。キーはクエリの下流にあります。
パーティションを設計したら、DynoTable をダウンロードして自分の
オーバーロードされたキーを閲覧し、1回の Query がテナント全体を一度に引き戻すのを
見てみましょう。
オーバーロードされたパーティションのクエリコスト
begins_with(sk, "USER#") で TENANT#acme の下のメンバーを列挙すると、読み取られるの
はユーザーの行だけで、請求書やイベントは読まれません。キー条件は、データがパーティション
を出る前にフィルタするからです。ユーザー200人(各2 KB)と監査イベント5,000件(各1 KB)を
持つテナントなら、そのクエリが触れるのはおよそ400 KB(結果整合性で約100 RCU)です。
ユーザーを探すためにテーブル全体を Scan すれば、すべてのテナントのすべてのアイテムが
計測されます。
代表的なオーバーロードされたアイテムを アイテムサイズ計算機に貼り付け、それから 料金計算ツールで一覧クエリを見積もりましょう。
シングルテーブル設計ツールで設計する
エンティティ(Tenant、User、Invoice、Event)とアクセスパターン(「テナントのユーザーを
列挙」「テナントをまたいだ未払いの請求書」)を
シングルテーブル設計ツールに入力してみてください。
CloudFormation にコミットする前に、本番で使うことになるオーバーロードのプレフィックスに
合った pk/sk のテンプレートと GSI のキーを提案してくれます。
パターンからクエリを出力する
プレフィックスが固まったら、式ビルダーでキー条件を
構築し、クエリビルダーからページネーション込みの
プログラムをエクスポートしましょう。プレフィックスのタイプミス(USER# と USERS#)は
エラーなしに空の結果を返します。生成された式は、その静かな失敗モードを減らします。
エンティティ型プレフィックスの登録簿
開発者が参照できる短い内部の表を維持しましょう。
| エンティティ | ソートプレフィックス | SK の例 | クエリのスライス |
|---|---|---|---|
| テナントのメタ | META | META | 単一アイテムの取得 |
| ユーザー | USER# | USER#u_3001 | begins_with(sk, "USER#") |
| 請求書 | INVOICE# | INVOICE#2026-0015 | begins_with(sk, "INVOICE#") |
| イベント | EVENT# | EVENT#2026-06-23T09:12Z | 降順読み取りによる時系列の末尾 |
新しいエンティティ型は、既存のプレフィックスの begins_with の下で衝突しないプレ
フィックスを選ばなければなりません — USER# と USERGROUP# はどちらも
begins_with(sk, "USER") に一致してしまうので、注意深く長くするか区切り文字で分ける
必要があります。


