上級読了 4 分

DynamoDB のキーオーバーロード

SQL から来ると、列は永遠に1つの意味を持ちます。orders.created_at は常に日付、 users.email は常にメールです。キーオーバーロードはそれを捨てます。パーティション キーとpksk といった汎用的な名前を付け、各アイテム型が それぞれ異なる意味をそこに流し込めるようにするのです。1つのテーブル、多数のエンティティ、 1つの形。

DynamoDB のキーオーバーロードとは何ですか?

キーオーバーロードとは、多数のエンティティ型を pk/sk のような汎用的なキー名の下に 1つのテーブルへ格納し、型を値にエンコードすること (USER#u_3001INVOICE#2026-0014) です。 属性名は中立のままなので、ユーザー、請求書、イベントが1つのパーティションを共有します。値が 型を運び、ソートキーのプレフィックスによって1回の Querybegins_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つに入れてキーをオーバーロードします。

pkskattributes
TENANT#acmeMETAname="Acme Inc", plan="team"
TENANT#acmeUSER#u_3001email, role="admin"
TENANT#acmeUSER#u_3002email, role="member"
TENANT#acmeINVOICE#2026-0014amount_cents, status="paid"
TENANT#acmeINVOICE#2026-0015amount_cents, status="open"
TENANT#acmeEVENT#2026-06-23T09:12Zactor="u_3001", action="invite"

どの行も pk = "TENANT#acme" を共有するので、1つのを形成します。すべてが同じ場所に配置され、すべてが1回のパーティション 読み取りで到達可能です。

パーティション: TENANT#acmesk: METAsk: USER#u_3001sk: INVOICE#2026-0015sk: EVENT#2026-06-23T09:12Z1 回の Query

本当の仕事をしているのはソートキーのプレフィックスです。エンティティをグループ化し、 なおかつ順序付けます。

オーバーロードされたコレクションをクエリする

型がソートキーのプレフィックスに宿っているので、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# と書いてしまうと、何も返らず、しかも黙って失敗します。 式ビルダーKeyConditionExpressionExpressionAttributeValues のマップを生成するので、プレフィックスが実際に書いたものと 一致します。

インデックスもオーバーロードする

同じ手口はにも当てはまります。汎用的なキー名 gsi1pkgsi1sk を与え、 各エンティティに必要なものを書かせます。そうすればインデックスが、ベーステーブルには できないパターンに答えます。

pkskgsi1pkgsi1sk
TENANT#acmeINVOICE#2026-0015STATUS#open2026-06-30
TENANT#acmeINVOICE#2026-0014STATUS#paid2026-06-12
TENANT#betaINVOICE#2026-0099STATUS#open2026-06-25

これで Query gsi1 WHERE gsi1pk = "STATUS#open" は、すべてのテナントにまたがって 未払いの請求書をすべて期日順で列挙します。ベーステーブルのテナント単位のキーでは決して 提供できないクロスパーティションのビューです。別のエンティティが gsi1 を自分なりの意味で 再利用できる (たとえば gsi1pk = "ROLE#admin") ので、1つのインデックスが複数の読み取りを カバーします。ただし GSI は結果整合性であることを忘れずに。その 書き込みはベーステーブルより遅れます。

DynoTable でやってみる

生のオーバーロードされたキーは読むのが苦痛です。INVOICE#2026-0015EVENT#2026-06-23T09:12Z はフラットなリストでは混ざり合ってしまいます。パーティション でグループ化してプレフィックスを浮き上がらせるビューアが、がらくた入れを再びエンティティ へと戻します。

DynoTable で1つのテナントのアイテムコレクションを閲覧している様子 — META、USER、INVOICE、EVENT のアイテムが1つのオーバーロードされたパーティションキーの下にグループ化されている。
DynoTable で1つのテナントのアイテムコレクションを閲覧している様子 — META、USER、INVOICE、EVENT のアイテムが1つのオーバーロードされたパーティションキーの下にグループ化されている。

落とし穴

  • 区切り文字は一度決めたら決して変えない。 # が慣例です。エンティティ間で #: を混ぜると、何も警告しないまま 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 の例クエリのスライス
テナントのメタMETAMETA単一アイテムの取得
ユーザーUSER#USER#u_3001begins_with(sk, "USER#")
請求書INVOICE#INVOICE#2026-0015begins_with(sk, "INVOICE#")
イベントEVENT#EVENT#2026-06-23T09:12Z降順読み取りによる時系列の末尾

新しいエンティティ型は、既存のプレフィックスの begins_with の下で衝突しないプレ フィックスを選ばなければなりません — USER#USERGROUP# はどちらも begins_with(sk, "USER") に一致してしまうので、注意深く長くするか区切り文字で分ける 必要があります。

更新日