Lambda から DynamoDB を叩く処理を書いていたとき、テーブル設計をなんとなくの感覚でやってしまって後からじわじわ後悔した経験があります。RDB の感覚でカラム設計してたら「あれ、これ Scan しないと取れない…」みたいな状況になりがちなんですよね。
そんな反省もあって、改めて DynamoDB の設計周りのベストプラクティスをちゃんと調べ直してみました。AWS 公式ドキュメントなども参考にしつつ、自分なりにまとめています。
この記事でわかること
- パーティションキー設計の基本と「ホットパーティション」問題
- GSI と LSI の違いと使い分け
- シングルテーブル設計は本当に使うべきか
- 2025年11月の新機能:GSI のマルチ属性キー(複合キー)対応
- キャパシティモードの選び方(オンデマンド vs プロビジョンド)
- Scan・FilterExpression の落とし穴
パーティションキー設計が DynamoDB の肝
DynamoDB を使ううえで、最初に絶対押さえたいのがパーティションキーの設計です。RDB でいう「主キー」に近い概念ではあるんですが、DynamoDB においてはパフォーマンスに直結するので、単に「ユニークであれば OK」では済まないところが難しい。
ホットパーティション問題とは
DynamoDB はデータをパーティション(物理的なノード)に分散して保存します。パーティションキーのカーディナリティが低いと、アクセスが特定のパーティションに集中してしまい、パフォーマンスのボトルネックになります。これがいわゆる「ホットパーティション」問題です。
たとえば「ステータス」みたいな値(active / inactive の2種類しかない)をパーティションキーにしてしまうと、ほぼすべてのリクエストがどちらか一方のパーティションに集中してしまいます。AWS 公式ドキュメントでも、アクセスが偏るキーはスロットリングの原因になりうる(= ホットパーティションになりうる)ので避けるべき、という趣旨で説明されています。
具体的な対策:カーディナリティとシャーディング
カーディナリティとは、特定の列に含まれる異なる値の数(種類の数)を指します。たとえば「都道府県」ならカーディナリティは 47。DynamoDB のパーティションキーはカーディナリティが高いものを選ぶのが基本です。
ユーザー ID や注文 ID のような UUID を使うとカーディナリティが高くなるので、自然と分散されやすくなります。一方、連番の ID については、アクセスが均等に分散されるなら連番であっても必ずしも避けるべきではないという考え方もあります。とはいえ実務では UUID を使っておくほうが安全圏に入りやすい、というのが正直なところです。
どうしてもカーディナリティが低いキーしか使えない場合は「書き込みシャーディング」という手法があります。パーティションキーの末尾にランダムなサフィックス(例:USER#001_shard3)を付与して人工的に分散させる方法です。読み取り時には全シャードを並列クエリする必要がありますが、特定パーティションへの書き込み集中を回避できます。
GSI と LSI、ちゃんと使い分けてますか
インデックス周りは DynamoDB 設計でよく詰まるところです。GSI と LSI は名前が似てるけど性質がかなり違うので、改めて整理します。
GSI(グローバルセカンダリインデックス)
GSI は、パーティションキーおよびソートキーをベーステーブルと異なる組み合わせで設定できるインデックスです。クエリがすべてのパーティションにまたがる可能性があることから「グローバル」と呼ばれています。
特徴は以下のとおり:
- テーブル作成後でも追加・削除できる(柔軟性◎)
- 1テーブルあたり最大20個まで作成可能(デフォルトのサービスクォータ)
- 独自のキャパシティ(スループット)設定を持てる(プロビジョンドの場合。オンデマンドでもインデックス側のコストは別で発生)
- 読み取りは結果整合性のみ(強い整合性は使えない)
LSI(ローカルセカンダリインデックス)
LSI は、パーティションキーはベーステーブルと同じで、ソートキーだけが異なるインデックスです。パーティション内での並び替えや絞り込みパターンを増やしたいときに使います。
重要なのがここ:LSI はテーブル作成と同時にしか作成できず、既存のテーブルへの追加や削除もできません。後から「あの属性でも検索したかった…」となってから付けようとしても、テーブルを作り直すしかありません。これは設計ミスとして割と致命的です。
一方で LSI ならではのメリットとして、強い整合性(strongly consistent read)を使った読み取りが必要な場合は、テーブル本体か LSI を使う必要があります。GSI は強い整合性をサポートしていません。
実践的な使い分けの指針
- アクセスパターンが確定していない・後から変わりそう → GSI
- パーティション内のソートや絞り込みを増やしたい+設計が確定している → LSI
- 強い整合性のある読み取りが必要 → テーブル本体 or LSI
2025年11月の新機能:GSI のマルチ属性キー(複合キー)対応
余談ですが、これを調べていて一番「あー、やっとか」と思ったのがこのアップデートです。
2025年11月、Amazon DynamoDB が GSI における「マルチ属性キー(multi-attribute keys)」のサポートを発表しています。GSI のパーティションキーを最大4属性、ソートキーも最大4属性(合計最大8属性)で構成できるようになりました。
これまでは複合キーを使いたい場合、「ORDER#PENDING#2025-11-04」のように属性を文字列として結合する「属性結合(Attribute Concatenation)」をやる場面が多くて、アプリケーション側で結合・分解ロジックを持つのが地味につらい作業でした。
この機能自体は追加料金が「別途かかる機能」ではなく、通常どおり GSI のストレージや(必要なら)スループットに対して課金される形です。AWS のブログ・ドキュメントでも「追加料金なし(標準の GSI コストの範囲)」という趣旨で説明されています。
シングルテーブル設計は本当に必要か
DynamoDB の話題でよく出てくる「シングルテーブル設計」。1つのテーブルに異なるエンティティを混在させる設計手法ですが、採用すべきかどうかは自分もかなり悩みました。
ここは正直、チームの状況次第かなと思っています。シングルテーブル設計はうまくハマるとクエリ回数や設計がキレイにまとまる一方で、キー設計のノウハウが必要で、途中参画の人に説明するコストも上がりやすいです。
なので結論としては、「シングルテーブルじゃないと厳しいアクセスパターンが見えている」場合に絞って採用を検討するのが現実的かなと感じました。個人開発で試してみるのはアリですが、チーム開発では運用・引き継ぎコストも含めて慎重に判断したほうがよさそうです。
キャパシティモードはどっちを選ぶ
DynamoDB にはオンデマンドモードとプロビジョンドモードの2種類があります。どちらを選ぶかでコストが大きく変わるので、整理しておきます。
オンデマンドモード
オンデマンドモードは使用したリクエストに対して料金が発生し、DynamoDB がスループットを自動的に調整します。急なスパイクが来ても勝手にスケールしてくれるので、運用負荷が低いのが魅力です。
あとこれは地味に嬉しいやつなんですが、DynamoDB のオンデマンド(とグローバルテーブル)料金は 2024年11月に値下げされています(有効日は 2024年11月1日)。以前より選びやすくはなっています。
プロビジョンドモード(+ Auto Scaling)
プロビジョンドモードでは、読み取りと書き込みのキャパシティをユーザーが手動で設定します。事前にデータ量やリクエスト数を予測できるのであれば必要なスループットを設定でき、予測可能なトラフィックであればオンデマンドより安価になることがあります。
Auto Scaling と組み合わせることで、ある程度のトラフィック変動には自動対応できます。ただし急激なスパイクには追いつけないこともあるので注意が必要です。
選び方まとめ
| 状況 | 推奨モード |
|---|---|
| スタートアップ期・アクセスパターン不明 | オンデマンド |
| トラフィックが予測可能・安定している | プロビジョンド |
| 突発的なスパイクが想定される | オンデマンド |
| 長期運用・大規模サービス | プロビジョンド+リザーブドキャパシティ |
まずオンデマンドで始めて CloudWatch でアクセスパターンを観察し、安定してきたらプロビジョンドへ移行する流れが無難だと思っています。
Scan と FilterExpression の落とし穴
これは設計というよりクエリ操作の話ですが、DynamoDB を使い始めた人がよくやりがちなミスなので触れておきます。
Scan はほぼ使ってはいけない
DynamoDB の Scan は、テーブル全体を読み取って条件でフィルタリングする操作です。RDB でいえば SELECT * FROM table WHERE ... に近いですが、DynamoDB の場合はテーブル全件を読み取った分の RCU が消費されます。フィルタ後に結果が0件でも同じです。
テーブルが大きくなるほどコストもレイテンシも爆増するので、プロダクションコードでは基本的に Scan は避けるべきです。どうしても使わなければならない場面(全件集計・管理画面での一時的な検索など)では、夜間バッチに限定するなど頻度を下げる工夫が必要です。
FilterExpression の誤解
FilterExpression を Query に組み合わせれば大丈夫…と思っている人もいますが、これも注意が必要です。FilterExpression は Query の結果を後から絞り込むだけで、Query で読み取った(評価した)アイテム分の RCU は消費されます。絞り込みはあくまでクライアント側に返すデータを減らすだけで、コスト削減にはなりません。
理想はパーティションキー+ソートキーのキー設計とインデックスで絞り込みを完結させることです。「GSI を追加すれば解決」と安易にインデックスを増やすと、書き込み時のコストやストレージが増えるので、アクセスパターンを最初に洗い出してから設計する習慣が大事です。
まとめ
DynamoDB のベストプラクティスをざっと調べてまとめてみました。要点をまとめると:
- パーティションキーはカーディナリティ高めに:ホットパーティションを避けるために、アクセスが均等に分散する設計を心がける
- GSI は後から追加できる、LSI はテーブル作成時限り:アクセスパターンが不確定なら GSI を優先し、LSI は慎重に設計する
- GSI のマルチ属性キー対応(2025年11月〜):属性結合の workaround が減りそう。既存設計の見直しチャンスでもある
- シングルテーブル設計は万能ではない:チームの習熟度や引き継ぎコストも考慮して判断する
- キャパシティモードはフェーズに合わせて選ぶ:まずオンデマンドで始めて、安定したらプロビジョンドも検討
- Scan と FilterExpression に頼らない設計を:クエリコストが爆発する前に、アクセスパターン設計を先に考える
自分もまだ全部を完璧に使いこなせているわけではなくて、都度調べながらやっています。特に設計フェーズでアクセスパターンをちゃんと洗い出す習慣づけが大事だと改めて感じました。誰かの参考になれば!

