【AWS公式ドキュメントを噛み砕く】DynamoDBベストプラクティス

# はじめに
[【AWS公式ドキュメントを噛み砕く】DynamoDBとは？（概要編）](https://qiita.com/nh321/items/7f43a87a0ec36b266222)で、概要レベルの話を抑えたので、ベストプラクティスみたいな具体的な話はこっちに書く。

結構抜粋してます。
そして、学習がてらのアウトプットなので、正しい情報はAWSの公式ドキュメント見てくださいね。
（そして誤りあれば指摘してもらえると嬉しいです）

# NoSQL 設計
[これ](https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/bp-general-nosql-design.html)

**リレーショナルデータ設計と NoSQL の相違点**

>* DynamoDB では、最も一般的で重要なクエリをできるだけ速く、安価にするために、具体的にスキーマを設計します。

DynamoDBは検索時のクエリの状態（非正規化）で、データ構造を設計せよってことだな。

**NoSQL 設計の 2 つの重要な概念**

>* DynamoDB の場合は対照的に、答えが必要な質問が分かるまで、スキーマの設計を開始しないでください。
>* DynamoDB アプリケーションではできるだけ少ないテーブルを維持する必要があります。

実際のユースケースでよばれる「クエリ」を具体化せよ＝テーブル設計
なるべくテーブルを減らせ

**NoSQL 設計へのアプローチ**

>アプリケーションのアクセスパターンの 3 つの基本的な特性を理解することが重要です。
>
>- **データサイズ**: 一度に格納され、リクエストされるデータの量を把握することは、データを分割する最も効果的な方法を決定するのに役立ちます。
>- **データシェイプ**: NoSQL データベースでデータを整理することで、データベース内の形状が、クエリされるものと一致するようにします。
>- **データ速度**: ピーク時のクエリの負荷を事前に把握することは、I/O キャパシティーを最大限に活用するためにデータを分割する方法を決定する上で役立ちます。

* データサイズ：「データを分割する」のはてっきりDynamoDBが勝手にやってくれるのかと思ったけど、設計で意識しなきゃいかんのか？
    * →普通にテーブルに格納する1レコードのサイズをちゃんと意識しましょうと言う話だった
    * 1アクセスごとのデータ量が多ければ、性能もコストも悪化すると言う普通の話。
* データシェイプ：「読み込む時」のクエリと一致するように、データ構造を作りなさい。ってことだろうな。分かる。
* データ速度：「ピーク時のクエリの負荷」が重要なのはわかったけど、それはどうやって調べるんだ？
    * →たぶんプライマリキー（パーティションキー）の決め方の話かな？
    * パーティションキーの持ち方によって、データアクセス性能が大きく異なる。（後述されてます）

> 特定のクエリ要件を特定したら、パフォーマンスを管理する一般的な原則に従ってデータを整理できます。
> 
>- **関連するデータをまとめてください。** 関連するデータ項目を複数のテーブルに分散するのではなく、NoSQL システム内の関連項目を可能な限り近くにまとめる必要があります。
>    - 一般的なルールとして、DynamoDB アプリケーションはできるだけ少ないテーブルを維持する必要があります。
>- **ソート順を使用します。** キーの設計が原因でソートされている場合は、関連項目をグループ化して、効率的にクエリできます。
>- **クエリを分散します。** 「ホットスポット」を避け、できるだけ多くのパーティションにトラフィックを均等に分散するように、データキーを設計する必要があります。
>- **グローバルセカンダリインデックスを使用します。** 特定のグローバルセカンダリインデックスを作成することで、メインテーブルでサポートできるクエリとは異なるクエリを有効にすることができます。これは、まだ高速で比較的安価です。

- **関連するデータをまとめてください。**：非正規化って話やろ。わかったよ。
- **ソート順を使用します。**：ソートキーもちゃんと設計しなさいってことらしい。（後ろに読み進めた結果）
- **クエリを分散します。**：検索時に特定のデータに偏るようなキーを「パティションキー」に指定するなってことだな。OK。
- **グローバルセカンダリインデックスを使用します。**：「グローバルセカンダリインデックス」が便利だってことな。OK。

# パーティションキーの設計
[これ](https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/bp-partition-key-design.html)

> トラフィックが流れ始めると、指定されたパーティションキーのトラフィックが 3,000 RCU または 1,000 WCU を超えない限り、DynamoDB はプロビジョニングしたスループットを使用してアクセスパターンを自動的にサポートします。

特定のパーティションキーにアクセスが偏って、上記制限を超えると、自動サポートできなくなるってことか。
要は、性能劣化か。

**バーストキャパシティーを効率的に使用する**

> 未使用の読み込みおよび書き込みキャパシティーは最大 5 分 (300 秒) 保持されます。

AWSでよく見る「バースト」の考え方ね。OK了解。

> 読み込みや書き込みのアクティビティの急激なバーストが頻繁に発生しない場合は、このような追加のキャパシティーユニットがすぐに消費される場合があります (テーブルに対して定義した 1 秒あたりのプロビジョンドスループット性能よりも速く消費されます)。
> DynamoDB はバックグラウンドメンテナンスやその他のタスクのために予告なしにバーストキャパシティーを消費する場合があります。

ざっくり言えば、DynamoDBが勝手に判断して、追加キャパシティユニットから消費する場合があるよって話だろうな。
ここらへんは具体的に性能テストとかしてみないと分からんし、本番運用始まらないと評価ができなさそうだなー。

**DynamoDB アダプティブキャパシティーを理解する**

> 不均等なアクセスパターンに対応できるように、DynamoDB のアダプティブキャパシティーを使用することで、スロットリングすることなく、アプリケーションでホットパーティションに対して読み書きを継続することができます。
> また、アダプティブキャパシティーは、すべての DynamoDB テーブルに対して自動的に有効になるため、明示的に有効または無効にする必要はありません。

ほう。アダプティブキャパシティとは、特定のパーティションキーにアクセスが偏っても、性能劣化を引き起こさない機能なのか。
しかも自動的に有効！

> ホットパーティションのスロットリングが開始してから、アダプティブキャパシティーがアクティベートされるまでには、通常 5 分から 30 分の間隔があります。

あ、ちょっとタイムラグはあるのね。性能テストの時とか、注意した方が良さそうだな。
これ知らないと、本番運用だとアダプティブキャパシティ期待できない状況にもかかわらず、テスト時はアダプティブキャパシティのおかげで、性能テスト上は問題ありませんでした！とかになっちゃいそう。

## ワークロードを分散する
[これ](https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/bp-partition-key-uniform-load.html)

基本的にパーティションキー（RDBMSで言うところのプライマリキー）は、パーティションが分散されるような値にしなさいとのこと。
以下のようなものが良いらしい。

* ユーザー ID（アプリケーションに多くのユーザーがある場合）
* デバイス ID（各デバイスが比較的類似した間隔でデータにアクセスする場合）

逆に、以下はダメらしい。

* ステータスコード（可能性のあるステータスコードが少しだけある場合）
* 項目の作成日。直近の期間 (日、時、分) に切り上げられます。
* デバイス ID (追跡中のデバイスが多数あり、そのうちの 1 つのデバイスが他のすべてのデバイスよりもずっと人気がある場合)

要は、以下ってことだな。

 * なるべく分散してアクセスされるようなキーであること
 * なおかつ項目（レコード）を一意に特定できる属性（カラム）であること

難しくね？
　→　それに対する対策は、次の章とかで話されている。（→書き込みシャーディング）

## 書き込みシャーディング
[これ](https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/bp-partition-key-sharding.html)

**ランダムなサフィックスを使用したシャーディング**

> パーティションキースペース全体に均等に負荷を分散するための戦略として、パーティションキー値の最後に乱数を追加する方法があります。

日付をパーティションキーにしたい場合、「2014-07-09」みたいになる。
でもそれだと、最新日付がホットパーティションになる。
なので、後ろに001～200の乱数をつけましょう、っていう戦略。
（2014-07-09.001～2014-07-09.200みたいな感じ）

書き込み時はそれで良いけど、日付単位で読み込みたい時は、001～200を全て読み込んだ上で、アプリでマージしてあげなきゃいけない。

疑問

* ランダム値だと、特定できなくない？　→　次に回答がある。
* 1レコードへの集中が、複数レコードには分散されたけど、その程度の分散で良いの？　→　それで良いっぽい
* 書き込みのスループットは上がりそうだけど、読み取りは200回に分かれるから遅くなるんじゃ？　→　そういう戦略っぽい
    * 書き込みがボトルネックになるのを避けましょうって話なんやな。

**計算されたサフィックスを使用したシャーディング**

> パーティション間で項目を分散させるには、乱数を使用せずに、クエリする項目に基づいて計算できる数値を使用します。

別のシーケンスなオブジェクトから、ハッシュ値を算出するなどして、分散させる方法らしい。（例えば、001～200になるようにするなど）
これも結局、日付単位で読み込みたい時は、001～200を全て読み込んだ上で、アプリでマージしてあげなきゃいけない。


## 効率的なデータのアップロード
[これ](https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/bp-partition-key-data-upload.html)

以下のようなデータがあった時、パーティションキーをループキーとして処理するんじゃなくて、ソートキーをループキーとして処理しなさいってことらしい。
そうすることで、キャパシティユニットを効率的に使えるよ。という話。

|パーティションキー（UserId）|ソートキー（MessageID）|
|---|---|
|U1|1|
|U1|2|
|U1|...|
|U1|最大 100|
|U2|1|
|U2|2|
|U2|...|
|U2|最大 200|

# ソートキーの設計
[これ](https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/bp-partition-key-data-upload.html)

> 設計が優れたソートキーには、2 つの主な利点があります。
>
>- starts-with、between、>、< などの演算子による範囲のクエリを使用して、一般的に必要な関連項目のグループを検索することができます。
>- 複合ソートキーを作成すれば、データの階層的 (1 対多) な関係を定義して、任意の階層レベルでクエリを実行することができます。

**バージョンコントロールにソートキーを使用する**

以下のようなデータ構造にする。実データと履歴データが同じテーブルに入っている感じ。（想像してなかった構造なのでびっくりした）

|パーティションキー（UserId）|ソートキー（version）|バージョンの意図|
|---|---|---|
|U1|v0|最新状態|
|U1|v1|一番最初に作られた状態|
|U1|v2|1回更新された後の状態|
|U1|v3|2回更新された後の状態|
|U1|v4|3回更新された後の状態|

これの何が嬉しいんだ・・・？
　→ これは、`監査またはコンプライアンス目的で項目レベルのリビジョン履歴を保持し、最新バージョンを簡単に取得`する場合の効果的な設計パターンらしい。

# セカンダリインデックス
[これ](https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/bp-indexes.html)

## 一般的なガイドライン
[これ](https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/bp-indexes-general.html)

|dynamoDB用語|RDBMSだと|強力な整合性の提供|
|---|---|---|
|**グローバル**セカンダリインデックス（GSI）|パーティションキーとソートキーを持つインデックス。テーブル内の項目とは異なる場合があります。|不可能（結果整合性のみ）|
|**ローカル**セカンダリインデックス|テーブルと同じパーティションキーと、異なるソートキーを持つインデックス。|可能|

> 一般的に、ローカルセカンダリインデックスではなく、グローバルセカンダリインデックスを使用する必要があります。

厳密な比較はこっちを見るのが良さそう。（そこまでこの記事で紹介するのはやりすぎなので、リンクのみ紹介。）
[セカンダリインデックスを使用したデータアクセス性の向上](https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/SecondaryIndexes.html)

**インデックスを効率的に使用する**

>- **インデックス数は最小限に抑えます。**ほとんど使用されていないインデックスは、ストレージおよび I/O のコスト増大の一因になり、アプリケーションのパフォーマンスには効果がありません。
>- **大量の書き込みアクティビティが発生するテーブルのインデックスは作成しないでください。**そのようなテーブルでデータのインデックスを作成する必要がある場合は、必要なインデックスが設定されている別のテーブルにコピーして、クエリを行う方が効率的な場合があります。

インデックスを最小に抑えるのはそりゃそうだ。
書き込みにキャパシティユニットを優先して割きたい場合は、読込テーブルをコピーして作ったほうが良いのか。なるほど。

**射影を慎重に選択する**
> 読み込みに比べて、テーブルでの書き込みアクティビティが多くなることが予想される場合には、次のベストプラクティスに従います。
> 
>- インデックスに書き込まれる項目のサイズが最小になるように、射影される属性が少なくなるようにします。
>- クエリではほとんど必要にならないことが分かっている属性は射影しないでください。インデックスで射影されている属性を更新する度に、インデックス更新による追加コストが発生します。
>- ALL(全てを射影する) は、異なるソートキーによってソートされるテーブル項目全体を返るようにする場合のみ、指定します。ほとんどの場合、ストレージおよび書き込みアクティビティに要するコストが倍加します。

まぁ要は、必要最低限を射影しなさいってことな。OK。

**フェッチを回避するための頻繁なクエリの最適化**
> レイテンシーを可能な限り小さくしてクエリを最速にするには、クエリによって返ることが予想されるすべての属性を射影します。

射影は必要最低限だけど、稀にでも返却が必要な属性（カラム）はちゃんと射影しておきなさいってことらしい。
理由は以下。

> 投影されていない属性のローカルセカンダリインデックスにクエリを実行すると、DynamoDB は自動的にテーブルからこれらの属性を取得します。


**ローカルセカンダリインデックス作成時に項目コレクションのサイズ制限を確認する**

用語定義

|用語|説明|
|---|---|
|項目コレクション|テーブルとそのローカルセカンダリインデックス内で、同じパーティションキーを持つすべての項目を意味します。|

> 特定のパーティションキー値に対するテーブルおよびインデックス項目の合計が 10 GB を超えると予想される場合は、そのインデックスの作成を回避できないかどうかを検討してください。

1パーティションキーで取得できるデータ量が10GBを超えないようにしましょう。（容量制限の問題）

超えざるを得ない時は、以下参照とのこと。
[項目コレクションのサイズ制限](https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/LSI.html#LSI.ItemCollections.SizeLimit)

## スパースなインデックス
[これ](https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/bp-indexes-general-sparse-indexes.html)

そもそもスパースなインデックスって何？
「ソートキーが必ずしもすべての項目（レコード）に現れないインデックス」を、「スパースなインデックス」と呼ぶらしいが、良く分からん。

[MongoDB入門 - indexを作成してみよう#スパースインデックス](https://qiita.com/soudai_s/items/d53d0c4a2021fb955dd4#%E3%82%B9%E3%83%91%E3%83%BC%E3%82%B9%E3%82%A4%E3%83%B3%E3%83%87%E3%83%83%E3%82%AF%E3%82%B9)の説明がわかりやすかった。
> スパースインデックスにより、もしユニークインデックスが生成されているキーだったとしても空の値についてはユニーク性を評価しないで登録することが出来るようになります。

なるほど、ユニーク性を考慮しないインデックスをスパースインデックスと呼ぶのね。
そして、DynamoDB特有の用語ではないのね。

**DynamoDB のスパースインデックスの例**

> グローバルセカンダリインデックスは、デフォルトでスパースです。

デフォルトでスパースなんか！

> グローバルセカンダリインデックスをスパースに設計することにより、優れたパフォーマンスを達成しながら、親テーブルの書き込みスループットよりも低くプロビジョニングできます。

要は、インデックスをスパースにすることで、必要最低限の項目だけになるよ＝優れたパフォーマンスを発揮できるよってことが言いたいっぽい。

# 取り上げてない話
以下みたいな固有データまでまとめると死にそうなので、ここまでに留める。

* [大きな項目](https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/bp-use-s3-too.html)
* [時系列データ](https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/bp-time-series.html)
* [多対多の関係](https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/bp-adjacency-graphs.html)

※他にも、GSIの細かい話とかは割愛している。

# 最後に
**わかったこと**

* 結構、アプリ側でいろいろやる必要がある。ということが分かった。

**わからなかったこと**

* ユースケースが決まるまで、設計するなってあるけど、ユースケース自体が変容していくのはどう対応するのだろうか？
* SQSとかと組み合わせたほうが良いよ的なベストプラクティスはどこに書いてあるんだ？（上記資料だと、dynamoDBに閉じたベストプラクティスだった）

# おまけ
実践的な話はここにまとまっているリンク先を見たほうが良さげだった。
[DynamoDBのキャッチアップに参考になった記事(個人用)](https://qiita.com/koshilife/items/94fa4809afb9381dca3c)


以上