DynamoDB でデータをモデル化する方法
SQL では、まずエンティティとリレーションシップをモデル化し、後で問い合わせたものを何でも組み立ててくれるクエリプランナーを信頼します。DynamoDB はそれを逆転させます。あなたは すでに行うとわかっている読み取り をモデル化し、キーはそれらに応えるために存在します。
結合エンジンも、実行時に戦略を選ぶプランナーもありません。Query は 1 つのキーに沿って 1 つのパーティションを読み、それがパフォーマンス契約のすべてです。だからキーは、整ったスキーマのためではなく、既知のアクセスパターンのために設計します。
AWS はベストプラクティスガイドではっきり述べています。「スキーマが答える必要のある問いを知るまで、スキーマの設計を始めるべきではない」。
このガイドは 1 つのドメインでプロセス全体をたどります。マルチプレイヤーゲームのリーダーボード で、プレイヤー、彼らがプレイするマッチ、そしてシーズンごとのランキングを追跡します。問いのリストから動くキースキーマへと進みます。
DynamoDB でデータをどうモデル化しますか?
テーブルではなく読み取りを先にモデル化します。アプリが行うすべてのクエリをリストアップし、各問いが単一の Query か GetItem に解決するようとを設計します。一緒に読まれる項目を同居させ、ソートキーで値の範囲を辿り、ベーステーブルが応えられないアクセスパターンには GSI を追加します。
- テーブルではなく読み取りを先にリストアップする。 問いが仕様であり、名詞は気を散らすものです。
- 各問いは 1 つの
QueryかGetItemでなければならない。 ある問いにScanが要るなら、モデルが間違っています。 - 同居する項目はを共有し、範囲を辿るものはすべてに入れる。
- ベーステーブルが答えられない問いには を — フィルタ付きの
Scanでは決してなく。
ステップ 1 — 問題をテーブルではなく問いとして枠組む
players、matches、scores テーブルを描きたくなる衝動に抵抗してください。その本能は SQL の癖で、ここでは間違いです。代わりに、アプリが実際に行うすべての読み取りを書き出します。私たちのリーダーボードでは:
- 1 人のプレイヤーのプロフィールを id で取得する。
- あるプレイヤーの最近のマッチを新しい順にリストする。
- あるシーズンの上位 N 人のプレイヤーをレーティング順に表示する。
- 公開ハンドルでプレイヤーを引く(例: プロフィール URL 用)。
これら 4 つの問い — 名詞ではなく — が仕様です。それぞれは単一の Query(または GetItem)に解決しなければなりません。それが DynamoDB がスケール時に安価に提供する唯一のアクセス形状だからです。
ある問いがテーブルをスキャンしてしか答えられないなら、モデルが間違っており、それをレイテンシとコストで痛感します — Scan が避けるべき落とし穴である理由は Query vs Scan を参照してください。
この手法全体は、ドメインごとに一度実行する短く順序づけられたパイプラインです。
以下の各ステップは 1 つのボックスに対応します。リストアップ、列挙、キー設計、残りのためのインデックス追加、そして検証です。
ステップ 2 — モデル化に使うプリミティブを理解する
テーブルには、項目がどの物理パーティションに存在するかを選ぶ パーティションキー(PK)と、そのパーティション 内 で項目を順序づけるオプションの ソートキー(SK)があります。
AWS のコアコンポーネントのドキュメントは、この対を項目のプライマリキーと呼びます。Query は常にちょうど 1 つの PK 値を狙い、SK を範囲スキャンまたはフィルタできます — それがツールキットのすべてです。
この単一パーティション設計こそが、DynamoDB に 2007 年の Amazon Dynamo 論文で最初に記述された、予測可能で低レイテンシで水平分割された読み取りを提供させるものです。
以下のすべての決定を駆動する 2 つの帰結があります。
- 一緒に読まれる項目はパーティションキーを共有すべき で、1 つの
Queryが単一の課金対象リクエストでそれらを返します。 - 範囲を辿りたいものすべて(最近のマッチ、上位レーティング)は ソートキー に存在しなければなりません。それが
Queryが順序づけ、境界を切れる唯一の属性だからです。
ある問いがベーステーブルの提供する形状と 異なる アクセス形状を必要とするとき、グローバルセカンダリインデックス を追加します — 異なる PK/SK の下でのテーブルの再射影です。
(GSI 対ローカルセカンダリインデックスについては GSI vs LSI を参照してください。)
ステップ 3 — キーを、1 つの問いずつ設計する
汎用でオーバーロードされたキー属性を持つ単一のテーブルを使います — シングルテーブルアプローチ — プレイヤーとそのマッチは一緒に読まれるからです。
自分のプレフィックスを考案します。ここでは PLAYER#、MATCH#、SEASON# が、それ以外は汎用のキーの中でエンティティ型をタグ付けします。
問い 1 と 2(プロフィール + 最近のマッチ)はパーティションを共有するので、両方とも同じ PK にぶら下がります。
| partitionId | rangeId | attributes |
|---|---|---|
| PLAYER#u8231 | PROFILE | handle, region, createdAt |
| PLAYER#u8231 | MATCH#2026-06-23T14 | result=win, ratingDelta=+18, mapId |
| PLAYER#u8231 | MATCH#2026-06-23T11 | result=loss, ratingDelta=-15, mapId |
Query partitionId = "PLAYER#u8231" はプロフィールとすべてのマッチを 1 回の読み取りで返します。プロフィールだけなら GetItem です。
最近のマッチには、ScanIndexForward = false を付けた rangeId begins_with "MATCH#" が新しい順にそれらを辿ります — ソートキーの中のタイムスタンプが順序づけを無料でやってくれます。
問い 3 と 4 はそのパーティションからは答えられません — シーズンのランクとハンドルを軸にしており、どちらもベースの PK ではないからです。それぞれが GSI を得ます。
汎用のインデックス属性を 2 対追加します — ランクインデックス用の seasonPartition / seasonSort と、ハンドルインデックス用の handlePartition / handleSort — 同じ プロフィール項目(ステップ 3 で書いたもの、いまインデックス属性が埋められた状態で示す)に投入されます。
| partitionId | rangeId | seasonPartition | seasonSort | handlePartition | handleSort |
|---|---|---|---|---|---|
| PLAYER#u8231 | PROFILE | SEASON#2026-Q2 | RATING#1842 | HANDLE#nighthawk | PLAYER#u8231 |
いまや ScanIndexForward = false を付けてシーズンインデックスを WHERE seasonPartition = "SEASON#2026-Q2" で Query すると、レーティング順にランクづけされたプレイヤーが返ります — それがリーダーボードです。
handlePartition = "HANDLE#…" をキーとする 2 つ目のインデックスは、公開ハンドルをプレイヤー id に 1 回の読み取りで解決します。1 つの物理テーブル、4 つの単一 Query のアクセスパターンです。
RATING#1842についてのの注記: DynamoDB はソートキーを数値順ではなく 辞書順 にソートするので、レーティングは固定幅にゼロ埋め(RATING#01842)しなければなりません。さもないと9が1000の後にソートされてしまいます。これは前もって正しくやる価値のある古典的なモデリングの落とし穴です。
ステップ 4 — DynoTable でモデルを検証する
キースキーマは、実際の Query が期待どおりの項目を、それ以上でも以下でもなく返すのを見て初めて信頼を得ます。
DynoTable でテーブルを開き、シーズンインデックスに対してリーダーボードのクエリを実行し、パーティションがランクづけされ境界の切られた状態で返るのを確認します — Scan も、クライアント側ソートもなしに。

これらのクエリの条件式を構築するとき — begins_with、seasonPartition = :p、プレースホルダー :p のバインディング — は DynamoDB 式ビルダーにやらせましょう。
それが KeyConditionExpression、ExpressionAttributeNames、ExpressionAttributeValues を生成するので、result のような予約語やタイプミスしたプレースホルダーが読み取りを静かに壊すことは決してありません。
ステップ 5 — 落とし穴と次のステップ
モデルを出荷する前に確認すべきいくつかの罠です。
- 一緒に読まないリレーションシップをモデル化しない。 問いごとの GSI は安価ですが、無駄な GSI は繰り返しかかるコストです。インデックスは問いのリストから追加し、投機的に追加しないでください。
- パーティションの熱を監視する。 1 つの PK(有名プレイヤー、単一のホットなシーズン)がトラフィックの大半を吸収すると、そのパーティションはスロットルしえます。キーが証明可能にホットなときはサフィックスシャードで書き込みを分散します — AWS はパーティションキー設計でこれを扱っています。
- ソートキー内の数値や時間的なものはすべてゼロ埋めと ISO-8601 に。 辞書順があなたの意図する順序に一致するように。
- 新しい問い = 新しいキーかインデックス、決して
Scanではない。 後から本当に新しいアクセスパターンが現れたら、キーを拡張します。フィルタでごまかさないでください。
問いを先にモデル化し、それぞれが 1 つの Query になるようキーを設計し、それを証明してください。
真ん中のステップを先取りするには、無料のシングルテーブル設計ツールが、このようなアクセスパターンのリストを、例のアイテムとコストのヒント付きの PK/SK/GSI プランに変換してくれます。
DynoTable を試してテーブルを閲覧し、これらのクエリをベーステーブルと GSI に対して並べて実行し、設計したアクセスパターンが計画どおりのものを返すのを見てください。そして、モデル化して いなかった 問いのためには、その SQL Workbench が本物の JOIN、GROUP BY、集計をクライアントサイドで実行します。


