今回は以下の公式Youtubeを視聴し、まとめてみました。
https://www.youtube.com/watch?v=5gkCFQZVz_Q
https://www.youtube.com/watch?v=Wd5gbLQ2a1E
DynamoDBとは、フルマネージドかつサーバレスのKey-Value型NoSQLデータベースを指し、MySQLとかPostgresなどのRDBとは違ってSQLを使用しない。
基礎知識
ここまでは、RDBと大差ないので割と理解しやすい。
そして、PrimaryKeyの種類には、PartitionKey(PK)
とSortKey(SK)
がある。
PKで決められないItemsをSKと組み合わせて検索する訳だけど、これもRDBでいう複合主キーと同じだから難しくないね。
設計の重要な考え方
アクセスパターンからテーブルを設計する
1.テーブル名を決定する
2.Partion Keyの検討
カーディなりティを高くしてホットスポットとならないように注意する
3.Sort Keyの検討
KINDで複数スキーマを詰め込む
基本形はテーブル名@固有ID(@は区切り文字のため任意)
例えば、以下のようなUserテーブル構成が一つの例となる。
PKは一意となる値でなければいけないし、SKもPKとの複合キーとして一意となる必要があるためuuidなどを使用(以下は長くなるのでuuidではない)
PK | SK | Attributes...(自由) |
---|---|---|
userId:"1111" | kind:"item@1111" | name: "Yuta" |
userId:"1111" | kind:"item@1112" | Lv: 99 |
userId:"1111" | kind:"item@1113" | Address:"Tokyo" |
userId:"1112" | kind:"item@1114" | name:"Taro" |
userId:"1112" | kind:"item@1115" | Lv: 12 |
4.必要な情報を属性として追加
5.Read/Writeそれぞれのスループットを決定
+α テーブルはできるだけ少なくする
似たような情報をまとめることでテーブルを統合しておくことが重要。
例えば、Userが好きな本と好きな食べ物を持つテーブルを作成するとした場合
- PartitionKeyをuserId
- SortKeyをbook_favoriteとfood_favoriteとする
すると以下のように、同じテーブルに統合されることとなる。
PK | SK | Attributes |
---|---|---|
user1 | book_favorite | |
food_favorite |
ただ、ここで疑問に思う人もいると思うが、これで好きな食べ物だけを取得したい場合どうするのかということ。
そのように特定のSKを使用したい場合はPbegin_with
で検索をかければ特定の条件で絞り込むことができる。
begins_with(sk, "food_")
他にも以下のような検索ができる
1 | 2 |
---|---|
= | 等しい |
<> | 等しくない |
< | 小さい |
<= | 以下 |
> | 大きい |
>= | 以上 |
>= | 以上 |
SortedResults | 結果を取得する際にソートキーを使用してデータをソート |
Count | クエリまたはスキャンの結果として、返されるアイテムの数を取得 |
begins_with(attribute, substr) | 指定した属性が特定の文字列で始まるアイテムを検索 |
contains(attribute, operand) | 指定した属性に特定の値が含まれているアイテムを検索 |
attribute_exists(attribute) | 指定した属性が存在するアイテムを検索 |
attribute_not_exists(attribute) | 指定した属性が存在しないアイテムを検索 |
attribute_type(attribute, type) | 指定した属性が特定の型であるアイテムを検索 |
size(attribute) | 指定した属性のサイズ(文字列の長さまたはリストの要素数)を検索 |
その他のIndex
Local Secondary Index(LSI)
- 特定のパーティションキーに対して、異なるソートキーを使って効率的に絞り込み検索を行うために使用される
- Sort key以外に絞り込み検索を行うkeyを持つことができる
- Partition keyが同一で、他のattributesで検索のために利用
- テーブル作成時に追加
- 1テーブルに最大5個
PrimaryKeyとSortKey、例えばUserID(PK)とWeaponId(SK)があったとして、その二つの組み合わせて検索できるけど、例えば、SKとしてWeaponIdじゃなくて、例えばMemberIdをSKとして使いたい!ってこともあるよね。
だから、その時にPKと組み合わせて使えるようにLSIとして設定しておけばMemberIdもSKとして使えるってこと。つまり、第二のSKってことだ。
例えば以下のテーブルがあるとする。
{
"UserId": "user123",
"OrderId": "order789",
"OrderDate": "2024-05-21",
"Category": "Electronics"
}
パーティションキーはUserId、プライマリソートキーはOrderIdとした場合、LSIとしてOrderDateを追加することで、以下のように特定のユーザーの注文履歴を日付でソートして検索できるようになる。
{
"TableName": "Orders",
"IndexName": "OrderDateIndex",
"KeyConditionExpression": "UserId = :userId",
"ExpressionAttributeValues": {
":userId": { "S": "user123" }
},
"ScanIndexForward": false
}
Global Secondary Index(GSI)
- テーブルの主キー(パーティションキーとソートキー)とは異なるキーを使用してインデックスを作成するための機能
- テーブル作成後に追加
- 1テーブルに最大20個
まあ、これもLSIと考え方のベクトルは同じで、PKとSKの組み合わせを変えて検索したい!って時に作成すれば、使えるってこと。これはN:Mの時とかにスイッチして使うのに有用かな?
まあ簡単に違いをまとめると以下の感じ
LSI: パーティションキーは変えず、ソートキーのみを追加して検索を拡張。
GSI: パーティションキーとソートキーの両方を変更して、別の観点からデータを検索。
せっかくだからちゃんと違いをまとめると
項目 | Local Secondadry Index | Global Secondary Index |
---|---|---|
パーティションキー(PK) | テーブルのパーティションキーと同じ | テーブルとは異なるパーティションキーを指定できる |
ソートキー(SK) | 異なるソートキーを指定できる | 異なるソートキーを指定できる |
作成タイミング | テーブル作成時にのみ定義可能 | テーブル作成時でも後からでも追加可能 |
用途 | パーティションキーが同じデータセット内で異なるソートキーを使った検索を行いたい場合 | テーブル全体を異なるパーティションキーとソートキーで検索したい場合 |
制約 | 1テーブルあたり最大5つのLSI | 1テーブルあたり最大20のGSI |
アトリビュートに使用可能なデータ型
- String
- Number
- Binary
- Boolean
- Null
- 多値データ型
Set of String
Set of Number
Set of Binary - ドキュメントデータ型
List型
Map型
ただし、PKとSKの型には文字列、数値、またはバイナリのみ使用可能
キャパシティモード
-
プロビジョンドキャパシティモード
Read/Writeが予測可能な場合に使用
オンデマンドへの変更は1日1回まで
設定キャパシティに基づいて課金+データストレージに対する課金 -
オンデマンドキャパシティモード
Read/Writeが予測不可能な場合に使用
リクエスト数に応じて課金+データストレージに対する課金
単価はプロビジョンの方が安い。しかもオンデマンドではスロットルが発生する可能性があるのでスパイクに対応してくれる訳ではない。
したがって、負荷テストなどである程度キャパシティユニットを予測し、プロビジョンで高く設定しておき、大体の傾向が分かれば設定値を変更するのが良いらしい。
また、プロビジョンはリザーブができたり、オートスケールができたりするので積極的に活用すること。
そのほか設定
TTL
Itemの有効期限を設定し、データベースから自動削除されるタイミングを定義可能
追加料金は不要
ただし、期限切れ後、即座に削除される訳ではなく48時間以内に削除されるため、読み取り時にはアプリ側でフィルタリングが必要
パフォーマンスチューニング
まず遅い理由として以下の2点
クエリに問題がある
基本DynamoDBで問題が起きることはないが、あえて上げるなら
- 1アイテムのサイズが巨大
1アイテム400kbという制限があるが、400kbでも頻繁にやり取りをする場合はよくない
- Query取得で返す件数が多い
1回のやり取りで1MBしか返せないためそれ以上の場合はページングを行い何度もQueryを実行する必要がある。回避方法としては、行で伸ばすのではなく横に列を伸ばしていく。 - Scanは論外
実行回数が多すぎる
ただし、DynamoDBは取得のたびにHTTPリクエストが走るためどうしようもない。
あえて回避方法を挙げるなら以下の通り
- BatchWrite/BatchGet
一部失敗の可能性もあるのでそれにはアプリケーション側で対応が必要 - TransactGet/TransactWrite
AllorNothingにしたいなら素直にTransaction系を使用
検索を詳しくみてみる
Composite key
しかし、例えば以下のようにStatusも一緒に確認したい!となった時にまずいことが起きる。
何がまずいかというと、ここで使用しているFILTER
はまずPKとSKで取得した後にフィルターをするからである。
例えば、この組み合わせが大量に存在する場合、かなりコストがかかる。
これを解消する1つのアプローチとしてComposite key
がある。
これは何かというと、今回の場合Status
とDate
を組み合わせてStatusDate
を作成しておくことである。
このようにすればBEGINS_WITH
を使用して探索すればかなりローコストで実現可能となる。賢い。
Sparse Indexes
特定Itemにしか設定していない項目にGSIを含めることでプライマリーキー、ソートキー以外のキーでデータ抽出を容易にしてくれるテクニックの1つ
例えば、以下のようにGame-scores-tableのAward属性にChampが1人だけ設定するとしよう。
この場合、真正面から検索しようとするとテーブル全体をスキャンし、AwardがChampに該当するitemを検索することとなるが、これでは非常に非効率となる。
ここで、Sparse Indexes
の考え方を踏まえて、このAwardをGSIに設定する。
これにより、Award属性をインデックスを使用して効率的かつ高速に検索することが可能となる。
2本目の動画については22分あたりから触らないと理解が深まらなそうなので、言語化はまだできなそう。
また触り始めてから理解を深めて追記していきます〜。