最近サーバーレスWebアプリばかり開発しているhokです。
その際にDynamoDB設計もしたので備忘メモを残します。

RDBとはだいぶ考え方が異なりますが、その中で自分の中で落とし込んだ内容をまとめています。

AWSサービスは日進月歩で進化してます。
当ページに記載してある内容が変更されている場合があるので、最新は公式で確認願います。

DynamoDBとは 見出しへのリンク

以下Amazon DynamoDB よくある質問から引用

DynamoDB は、あらゆる規模に適した高速で柔軟な非リレーショナルデータベースサービスです。
DynamoDB を使用すると、分散データベースの運用と AWS にスケーリングするための管理負荷を軽減できます。
ハードウェアのプロビジョニング、設定と構成、スループット容量のプランニング、レプリケーション、ソフトウェアのパッチ適用、クラスターのスケーリングなどについて心配する必要はありません。

メリット 見出しへのリンク

  • 容量無制限
  • 高可用性
    • データが3箇所にクロスリージョンレプリケーションされるから
    • 結果整合性
      • DynamoDBは2箇所書き込んだ時点でOKと返す
      • 残1箇所は時間経てば結果的に書き込まれる ←このことを結果整合性という
      • なので変更前のデータを読み込んでしまうこともあり(詳細は以下「注意する点」に記載)
  • スケーラビリティが高い
  • フルマネージドサービスなので保守時の負担が減る。データ容量増加に伴うディスクなどの増設作業が不要。上のスケーラビリティが高い面も楽になる要因。
  • 早い(シンプルなKVS)

デメリット 見出しへのリンク

  • RDBができる事が容易に実装できない(工夫する必要あり)
    • 以下「注意する点」に記載した。
  • DB設計は必ずアクセスパターン(仕様)が決まってから着手すること
    • でないとDB設計根本から変更することになる
  • つまり運用は楽になるかもだけど、開発時に考え抜いて設計要

設計 見出しへのリンク

Primary Key 見出しへのリンク

データはPartition Key(PK)、または PK と Sort Key(SK)の組み合わせで識別される。= Primary Key
テーブル数を少なく設計する。

テーブル数 見出しへのリンク

テーブルが少ないとキャパシティユニットを最適化できる。
アクセス頻度によってテーブルが分かれていると、それぞれのCUの管理が難しい。
関連するデータをまとめとくと応答時間短縮に繋がる。←「参照の局所化」

ベストプラクティスにも以下記載あり。

DynamoDB アプリケーションではできるだけ少ないテーブルを維持する必要があります。
設計が優れたアプリケーションでは、必要なテーブルは 1 つのみです。

インデックス設計 見出しへのリンク

RDB 見出しへのリンク

RDBはアクセスパターンを考慮せず、正規化したデータモデルを作成します。
なので新たな検索要件がでたら拡張する事で対応可能となる。

DynamoDB 見出しへのリンク

対してDynamoDBは、最初にアクセスパターンを洗い出しそのデータを取得できるインデックスを設計する。
なのでアクセスパターン要件の洗い出しが完了するまでDynamoDB設計に取りかからない

設計順序としては、
ER図→ユースケース図(アクセスパターン洗い出し)→スキーマ定義書(テーブル定義書)→クエリ条件定義書(クエリ詳細と定義)
追加要件時はテーブル・インデックス設計を変更する

以下インデックス設計で利用する手法 見出しへのリンク

DynamoDBは柔軟で色々な設計ができるので、以下手法を利用して要件に沿った設計をする。

GSIオーバーローディング (多重定義) 見出しへのリンク

Query対象が増えてくると、GSIが多くなってしまいコストと管理が増になる。
その対処方法はGSIオーバーローディング。
1つのGSIに複数の検索要件を持たせる。
GSI制限もあるしこの手法は有効(申請して上限緩和可能だけど基本は少なく設計)。

  • 例1:1つのTBLに会社情報、部署情報、社員情報を詰める場合
    • Primary Key
      • PK: company_id, SK: type(company)
    • GSI1
      • PK: department_id, SK: type(department#{create_datetime})
      • PK: department_id, SK: type(employee_summary#{create_datetime})
    • Primary Keyで会社情報取得し、GSI1で部署情報と配下の社員サマリ情報を取得できる
    • GSI節約できた
  • 例2:検索条件が増えた場合
    • Primary Key
      • PK: document_id, SK: type(検索条件名)
    • GSI
      • PK: type(検索条件名), SK: value(条件項目の値)
        • ※条件値の型によってはさらにGSI追加になる

Composite Key (キーの結合) 見出しへのリンク

より効果的なセカンダリインデックスキーを実現。
クエリを最適化したい場面で効果的。

  • 例: 上のGSIオーバーローディング例1に記載してる
    • SK: type(department#{create_datetime})
    • 前方一致検索で「department」データを取得できる
    • department#{create_datetime}を一致検索しソート条件付与すれば、create_datetimeのソートで取得できる

Sparse Indexesによりクエリコストを削減 見出しへのリンク

例えばテーブル全体から受賞者を検索したい場合、Scanで全体取得したりフィルターで検索すると、取得するデータ自体大きいので料金が高くなります。
ではなく、受賞者(Award)がPKとなるGSIを作成することで少ないコストで検索可能となる

多対多のテーブル設計 見出しへのリンク

Adjancy List Design Pattern(隣接リストパターン)。
2つのエンティティ関係が多対多だった場合、Primary Keyを一方のidで指定し、GSIで別エンティティのidを指定する。
隣接関係のリスト設計パターン

例: グループとメンバーの多対多データ

GSIグループ紐付きメンバー
id: PKSK{groupId}{groupId}{memberId}
dataType:SKPKgrouprelation#{member}member

注意する点 見出しへのリンク

  • 検索系
    • 外部結合ができない為、複雑な検索が苦手
    • PKは完全一致検索、ソートがSort Key(SK)でないとできない
    • SK以外もフィルタリングはできるが、Primary Keyで取得したデータに対してのフィルタリングであることを忘れない事
    • 検索条件が増えればその分データも増える可能性高い。これは検索回数を増やすのか、データ格納量増やすかのどちらをとるかによる。
      • CU高消費でお金かかるから一般的には検索を減らし一回で取得するのが吉
      • だが、同じデータ格納しすぎると更新・削除時しんどくなる
    • ページネーション
      • limitはRDBと考え同じ
      • offsetが異なる
        • ExclusiveStartKey と LastEvaluatedKeyを利用
        • 一覧検索のレスポンスに「LastEvaluatedKey」が含まれる。それは検索時の最後の項目のキー情報。次検索時のQueryの「ExclusiveStartKey」に「LastEvaluatedKey」を加えると、そのキー以降のデータを取得する。
      • またlimit,offset指定し検索しても、一覧取得後フィルダリングかかるとページネーションの整合性がおかしくなる可能性あり。(検索時では正しくLastEvaluatedKeyを取得してもその後フィルタリングすると件数自体が変わったりもする)
    • 結果整合性の読み込み
      • メリットの可用性に記載した通り、参照時に変更前の値が読み取れることあり
      • どうしても更新後の値を参照したい場合は
        • 強力な整合性のある読み込み:ConsistentReadオプション(読み取り一貫性)を付与する。料金2倍。
  • トランザクション は可能だが制限がある(操作対象Item数上限、サイズ上限など)
    • 制限により一部機能はトランザクション 使えないなんてことがある
    • またコストや性能面を考えて適用箇所は局所化するのが一般的
  • 排他制御
    • これはDynamoDBに関わらずだが別途取り込む必要あり。

参照 見出しへのリンク

DynamoDB に合わせた NoSQL 設計
【イチから理解するサーバーレスアプリ開発】 サーバーレスアプリケーション向きの DB 設計ベストプラクティス 20190905版
Amazon DynamoDB よくある質問