2022年10月の「今月の学び」は、AWS DynamoDBについて。 これまでの「今月の学び」は人文系(というか雑学に近いもの?)の世界が多かったが、こういうテーマも取り扱うということは最初から考えていた。
DynamoDBについての基礎知識
AWSのWebサイトには
Amazon DynamoDB は、key-value およびドキュメントデータモデルをサポートする NoSQL データベースです。
とある。
レコードはPartition KeyとSort Keyの複合主キーで同定される。(インデックス)Partition KeyとSort Keyの組み合わせが重複するレコードを作ることはできない。
Partition Keyは、データが物理的に保存される場所を決めるためにも用いられ、Partition Keyは完全一致しているレコードを取ってくるしかできない。 クエリ時にはPartition Keyに加えて、Sort Keyについてはある程度の比較クエリを使うことができる。
順序・前方一致のみ利用可能で、SQLのLIKE句のような文字列を含むかどうかといった柔軟な検索はできない。
特定のPartition Keyのレコードを取ってくる or 特定のPartition Keyをもち、Sort Keyが指定した条件を持つレコードを取ってくる がDynamoDBのクエリにできることである。
DynamoDBについての理解
勝手な造語だが、DynamoDBはMulti key-valueモデルとでも言ったらいいような気がする。
Dynamo DBのテーブルには、LSI(Local Secondary Index)とGSI(Global Secondary Index)というのを作ることができる。 LSIとは、Partition Keyはメインのインデックスと同じだが違うSort Keyを持たせることができるというもの。GSIは、Partition KeyとSort Key両方をメインのインデックスから変えることができる。
細々とした制限は色々あるけれども、一つのレコードに唯一ではなく複数種の「キー」を割り当てられるKVSと一言で言えるだろうか。
テーブル(キー)設計について
かつてDynamoDBのドキュメントには、「優れた設計をすれば1つのテーブルに全部のデータが収まる」的なことが書いてあった。 今ではその記述は無くなったけれども、引き続きなるべく少ないテーブル数に収めることが推奨されている。 (一つの理由として、AWSアカウントごとにテーブル数が制限されているというのもある。)
RDB的な発想では、モデルごとにテーブルを作るのが当たり前だが、Dynamoでは一つのテーブルに複数のモデルのデータが混在する。 また、実際どういうクエリが使われるのかを先に考えてからクエリに合わせてデータモデルを作るという、RDBとは反対の作り方をする。 LSIも後から追加することはできないので、アプリケーション要件の変化には非常に弱い。
テーブル設計というとRDB的なイメージがつきまとってしまうので、「キー設計」と言った方がいいように思う。
テーブル設計実践
おことわり
これから紹介する僕のテーブル設計は、自作アプリをとにかく安く動かしたいのでDynamoDBが本来不得手とする検索機能を持たせている。 なので本来好ましくないPartition Keyの実装になっている。お仕事でお客さんに提供するアプリを作る情報収集でこの記事に辿り着いてしまったのなら、一つの刺激として捉えてあまり真似ないように、、。
対象となるアプリケーション
以前nexxtrackというWebアプリを作った。 これはインフラコストを下げるために一台のEC2の中でサーバー(Python/Flask)を立ち上げ、DBにはSQLiteを使うという構成だった。
なぜSQLiteを使っているか。それは、楽曲検索機能において全文検索を使いたかったからである。 しかしこの際、さらにインフラコストを下げるために全文検索を諦め前方一致検索だけでLambda + DynamoDBの構成とすることにした。
まずはクエリから
ベストプラクティスに沿って、まずは必要なクエリから考える。 全部を書くのは面倒なので、次の2つのクエリだけについて取り上げることとする。
- 楽曲をタイトル(trackNameb)またはアーティスト名(artistName)の前方一致で検索する->Track
- 特定の楽曲(trackId)に対して、次に続ける楽曲候補を、おすすめ順(score)に取得する->Recommended
テーブル設計
Type | Partition Key | Sort Key | primaryString | secondaryString | primaryNumber | attributes(モデルに存在するデータ) |
---|---|---|---|---|---|---|
Track | "Track" | trackId | normalize(trackName) | normalize(artistName) | N/A | trackId, trackName, artistName, trackUrl, artistUrl, artworkUrl |
Recommend | Recommend#${trackId} |
recommendedTrackId | N/A | N/A | score, | trackId, followingTrackId, score, tracklists |
上記で、primaryString, secondaryString, primaryNumberそれぞれを使ったLSIを定義している。
データとしては、attributesにあげたカラムに入っている。そして工夫として、LSIに使うカラムはデータの中身を表す名前でなくLSIに使うぞという意図とデータ型をカラム名とした。 複数の属性を文字列として結合してSort Keyとすることもあるし、複数モデルを取り扱うにしても一つのカラムのデータ型は揃えないといけない。
というわけで、データとしてのカラムとインデックス用のカラムは分けた方がいいんじゃないかということを考えた。
Track
本来Partition Keyはなるべくバラけるようにすべきなのだが、ここではあえて"Track"と全て共通にした。 こうすることで、Sort Keyとして楽曲名やアーティスト名を入れると前方一致検索をかけることができる。
データ量が多くなる場合、Partition Keyが一緒だとレコードが局所的に偏ってしまい、パフォーマンスには悪影響が出る。検索はAlgoliaなどを使おう。
Recommended
Partition Keyに、特定の楽曲のIDを入れることでPartition Keyがバラける。そして、Sort Keyには次に続ける楽曲IDを含む文字列を入れる。 しかし、実際の検索はprimaryNumberをSort KeyとするLSIを使っている。(おすすめ度はユニークではないのでメインのインデックスのSortKeyには使えない)
まとめ
初めて(個人制作とはいえ)アプリケーション開発ありでDynamoDBを使っている。 今回は、すでにできているアプリをサーバレス設計にしたいという明確な目標があるが、仕様が明確化していない時ほどRDBの方が適している。 早い・安い・でかいというメリットがある一方で、柔軟性はかなり失われることになりそう。
今後もRDBだけでなくKVSやらグラフDBの利用も視野に入れたデータモデリングについて学んでいきたい。