👋

【Azure】cosmosDB for NoSQLのDB設計

2024/05/26に公開

初めに

Azure cosmosDBのDB設計について考える機会がありました。NoSQLのデータ保存、コンテナはどのように分割したらいいか、パーティションキーはどのような値を設定するのか良いかなどについて考えました。MSLearnで調べた内容やAzureのサポートに実際に話を聞けた内容も踏まえて、記事にまとめていこうと思います。私自身知識不足な部分もありますが、cosmosDBにおけるDB設計の経験がない人にとっては役に立つ記事にしたいと思います!

cosmosDBへの基本的な理解

まず、DB設計の前にcosmosDBへの基本的な理解を深めましょう。理解されている方は読み飛ばしてもらって大丈夫です。

cosmosDBとは

cosmosDBはMicrosoftAzureが提供しているフルマネージド型のNoSQLデータベースサービスです。大量のデータを高速に読み取り・書取りする場合に特に有用なものらしいです。ユースケースなども色々紹介されているので、詳しくは以下のMSLearnなどご確認ください。
https://learn.microsoft.com/ja-jp/azure/cosmos-db/introduction
https://learn.microsoft.com/ja-jp/azure/cosmos-db/use-cases?toc=%2Fazure%2Fcosmos-db%2Fsql%2Ftoc.json

コンテナ

cosmosDBのコンテナについては以下MSLearnに記述があります。
https://learn.microsoft.com/ja-jp/azure/cosmos-db/resource-model
コンテナはデータを格納する括りのようなものです。このコンテナはどのようなデータを格納しようという意味を持った箱といった感じです。しかし、NoSQLのため、RDBのように保存されるデータに型はなく、コンテナというデータ保存場所にいろんなデータを保存することができます(故にいろんなデータを突っ込むと何のコンテナかよくわからなくなってしまいます)。また、データを保存する場所と表現しましたが、コンテナのデータ量が増えるとcosmosDBは自動でスケールアウトしてくれるので、無制限にデータを保存できます(※厳密には多少制限あり)。
RDBのテーブルのようにコンテナ同士を結合(JOIN)してデータを取得することはできないので、クエリ検索で同時に取得したいデータは1つのコンテナに格納する必要があります。

論理パーティションと物理パーティション

少しわかりづらい話になります。パーティションについての詳細は以下のMSLearnをご確認ください。
https://learn.microsoft.com/ja-jp/azure/cosmos-db/partitioning-overview
ざっくり私のイメージベースで説明します。
cosmosDBではコンテナごとにパーティションキー(以降PKと略す)というものを設定することができます。これはどのキーをPKとして使用するか設定するということです。例えば以下のような3つのデータを保存するようなコンテナに対して、categoryをPKに選ぶみたいな感じです。

{"id": "1", "category": "animal", "content": "cat"}
{"id": "2", "category": "animal", "content": "dog"}
{"id": "3", "category": "language", "content": "english"}

PKの値が同じデータについては同じ論理パーティションになります。この場合だとidが1と2はcategoryがどちらもanimalなので、同じ論理パーティションになります。この論理パーティションのデータの合計は20GBが上限という制限があり、これを超えないようなPKを設定する必要があります。そして、実際にデータを保存している場所が物理パーティションです。1つの物理パーティションには複数の論理パーティションのデータが入りますが、同じ論理パーティションのデータが分割されて複数の物理パーティションに保存されることはありません。物理パーティションの上限は50GBなので、データが増えると物理パーティションが増えてスケールされるイメージだと思います。

ややこしいので、要は論理パーティションはデータのグループ分けをするもので、適切にグループ分けをしないと、データの上限に引っかかったり、データの読み込み・書き込み時にパフォーマンスが悪くなるくらいに捉えておけば大丈夫です。なので、この論理パーティションをグループ分けするための、PKを何に設定するかは重要な要素となります。categoryがPKの場合のパーティションのイメージは下図のような感じです!

上限値は先に理解しておこう

まず、上限値は先に理解しておかないと後で面倒なことになります。DB設計においては以下を把握しておけば良さそうかなと思っています。

  • 1アイテムのデータ量:2MB以内
  • 論理パーティション:20GB以内

アイテムとはコンテナに保存する1つ1つのデータのことです。上記の例を利用すると、以下のデータが1アイテムとなります。簡単には2MBを超えないと思いますが、一応注意が必要です。

{"id": "1", "category": "animal", "content": "cat"}

ユースケースを考えよう〜NoSQLのDB設計〜

まず、cosmosDBというより、NoSQLについて考えましょう。NoSQLではRDB(リレーショナルデータベース)とはDB設計の考え方が異なります。以下ドキュメントが非常に参考になりました。
https://learn.microsoft.com/ja-jp/azure/cosmos-db/social-media-apps

例えばRDBは下図のように正規化されることが多いと思います。しかし、たった1つの投稿(Post)を表示するために8つのテーブルを結合してクエリを実行しなければいけません。

参照:https://learn.microsoft.com/ja-jp/azure/cosmos-db/social-media-apps

NoSQLの考えでは非正規化をすることで、テーブルを結合しなくても一気にデータを取得できます(下図)。

参照:https://learn.microsoft.com/ja-jp/azure/cosmos-db/social-media-apps

ただ全てのデータを1アイテムに保存するとデータ容量も大きくなりますし、データの扱いも難しくなります。なので、ユースケースをしっかり考えて、どのようなデータを非正規化してまとめて保存すると良いのかを考えることは重要だと思います。ここは作りたいものによって大きく内容が異なる部分ですし、一度以下のMSLearnにも色々ユースケースが記載されているので読んでみると参考になると思います。
https://learn.microsoft.com/ja-jp/azure/cosmos-db/nosql/model-partition-example

考えたDB設計の例

以下のようなDB設計でコンテナを分割し、アイテム(データ)を保存することを考えました。パーティションキー(PK)にはどちらのコンテナでもユーザーIDプロパティを設定しています。

アプリのイメージとしては、下のchatGPTの画面のように、生成AIに質問して、その質問内容がスレッドでまとめられる感じですね。Threadsコンテナにスレッドの情報、Messagesコンテナに質問内容とその回答が保存される感じです。

chatGPTの画面

コンテナ分割

ThreadsとMessagesでコンテナを分けているのはユースケースとして、その2つのデータを同時に取得することは考えにくいと思ったからです。初期画面ではユーザーに紐づくスレッドの情報が左の履歴に表示され、スレッドを選択するとそのスレッドIDに基づく質問と回答をMessagesから取得するという処理のイメージです。また、スレッドの中にメッセージが複数入る形で非正規化して1つのコンテナでデータを保存することもできますが、1つのスレッドにつき配列のメッセージが無制限に増え続ける可能性があり、よくありません。これは以下MSLearnにも埋め込みをしない場合として紹介されていますので、参考にしてください。
https://learn.microsoft.com/ja-jp/azure/cosmos-db/nosql/model-partition-example

パーティションキー(PK)

PKでユーザーIDプロパティを設定した理由について解説していきましょう。
まず、Threadsコンテナはユースケースとして、ユーザーに紐づくスレッドをクエリで取得することを想定しています。クエリ検索で絞り込みたいプロパティをPKに設定するとパフォーマンスが良くなるようで、ユーザーIDをPKに設定しています。例えば一意な値であるidをPKに設定した場合、ユーザーIDで絞り込んでもそのデータがどこのパーティションに存在するか分からず探すのが大変だけど、ユーザーIDをPKにしておけばパーティションがどこかすぐわかるから見つけやすいみたいなイメージですね。

idがPKの場合

user_idがPKの場合
次に、Messagesコンテナですが、ここはユーザーIDではなく、スレッドIDでクエリ検索をかけるのでスレッドIDをPKにしても良いと思います。ただ、クエリ検索時にユーザーIDもスレッドIDと共に検索条件に含めることで、検索時のパフォーマンスが出るそうなのでユーザーIDがPKでも問題ないと考えています(パフォーマンス出るのはユーザーIDで絞った中に必ずスレッドIDが一致するデータが存在するからですね)。あとは、スレッドIDをPKにした場合、論理パーティションのデータ量がかなり小さくなります。これは私の予想なのですが、論理パーティションは小さすぎず大きすぎずで分散できるのが一番パフォーマンスが向上するのかなと思ったので、ユーザーIDをPKに設定しました。
また、Threadsコンテナ、MessagesコンテナともにユーザーIDをPKに設定して、論理パーティション上限の20GBは超えないという想定です。上限に引っかからないために、TTL機能などを利用して古いデータを削除していくことも可能です。

補足的な話

書き込み処理に重点を置く場合

上記のDB設計の例は主に読み取り処理のユースケースを考えて設計しました。実は書き込み処理に重点を置く場合は少し考え方が変わり、多数の書き込みをする場合は複数物理パーティションに対して書き込みを行う方が望ましいらしいです。書き込みは読み込み処理よりRUを消費するため、負荷分散の観点から複数パーティションに対して書き込むことでスループット不足によるスロットリングエラー等を回避することが可能らしいです。

インデックスについて

cosmosDBのインデックスについてですが、データを保存すると自動で各プロパティに対して、インデックスを作成してくれるようです。なので、cosmosDBはインデックスを意識せずとも、いい感じにパフォーマンスが出るようにしてくれる優れものなのです。ただし、物理パーティションをまたがる場合の検索についてはインデックスは効かないようなので、ここでもPKが重要になってくるということですね。

最後に

cosmosDBのDB設計について参考になったでしょうか?かなりややこしい内容も多く、まとめきれているのか不安な部分もあります。
cosmosDBのDB設計を行うためにはcosmosDBやNoSQLへの理解を深め、自身のユースケースをしっかり考慮して設計することが重要だと思いました。DB設計に絶対の正解はなく、奥は深いので、より良い設計を求めて日々勉強していきたいと思いました。

Discussion