DynamoDBでGSIをうまく貼りたい
こんにちは。株式会社CHILLNNの永田です。
記事のスコープ
この記事は、DynamoDB使ってみたいけど公式のベストプラクティスが何故ベストプラクティスなのか納得できないし実現仕方もわからないという人向けの記事です。間違い等あると思いますのでご指摘していただけると幸いです。
モチベーション
現在プロトタイピングを終えたCHILLNNという予約エンジンのバックエンドのリファクタリングを行っており、プロトタイピング時点で意図せずアンチパターンになってしまっていたDynamoDBのスキーマ修正を行っております。その中でもう検索するのがめんどくさいから、社内向けにまとめておこうと思った知見に関してOUTPUTしています。
DynamoDBとRDBMS
さてさて、もう耳タコな比較ですが、DynamoDBはNoSQLであり、RDBMSとは異なったアプローチでスキーマ設計をおこないます。RDBMSが柔軟なクエリを実現するため、実装の詳細やパフォーマンスを気にせずに柔軟に設計できるのに対し、DynamoDBでは最も一般的で重要なクエリを、できるだけ速く、安価にするために、具体的にスキーマを設計する必要があるのです。(公式ドキュメントまんまです。)
つまり、DynamoDBのスキーマ自体を頻出するクエリで期待されるレスポンスとほぼ同じ形で保持し、また、クエリから安価に検索可能にしておく必要があるということです。
上記を踏まえて実際の設計フェーズでは、以下のような流れで設計を進めるとスムーズかと思います。
1. ビジネスドメインの理解と、各リソースの適切な分解をおこなう
まずやるべきは、アプリケーションが扱うリソース間の関係を抽象化することです。ビジネスドメインの理解に関しては、特にNoSQLだろうがRDBMS使っていようが変わらないのですが、特にNoSQLでは、具体的なスキーマに落とし込む際のリソースごとのリレーションに関して、ビジネス要件を明確にしなくては真価を発揮させることができません。
DynamoDBのテーブル内の項目には、深さが最大32までの入れ子の属性を指定することができます。そのため、RDBMSでマッピングテーブルが必要であったようなリソースに関して、一つの項目に全て含めてしまうことが可能です。
階層が深くなったデータに対してのクエリは、比較的高価になってしまうことが多いため慎重に検討すべきですが、今後のビジネス的な要件を考慮した上で、リソースの分解が不要と判断される項目に関して、構造化されたデータをそのままブチ込めます。(こんなときRDBのときはJSONをStringとして直でぶっ込んでました。一方DynamoDBは構造を保持してくれるのでクエリ後のフィルターとかでも効率的に処理できます。)
2. アプリケーションのUIのワイヤーフレーム作成
1でリソースを分割した後、実際のスキーマを設計する前に、それらのリソースに対するクエリがどのように発行されるのかを全てリストアップします。
DynamoDBでは、RDBMSのように柔軟なクエリを発行することはできず、基本的にPrimary Keyのみを使って検索を行います。Primary Keyはシンプル(パーティションキー[※以降PK]のみで一意)と、複合(PKとソートキー[※以降SK]のセットで一意)の二つの中から選択することができます。PrimaryKeyはテーブル作成後は変更できません。複合を選択しても、論理的にシンプルと同様の使い方ができるので、多対多や、一対多のリレーションを表現する可能性がある場合には、複合を選ぶのが良いと思います。また、ベストプラクティスとして、できるだけ少ないテーブル数でアプリケーションを作成することが推奨されているので、PKとSKの属性名はできるだけ項目を規程しないように決めるのが良いかと思います。(僕はそのまんまPKとSKで良いと思っております。)
クエリを書く際は、PKが完全一致である必要があり、SKは指定しないこともできますし、指定する場合は、完全一致、前方一致、または比較演算子(between、>、<)による絞り込みが可能です。なお、以下で示すとおり、SKの範囲は分断されていてはなりません。
// 無効なクエリ
#PK = :partitionKey AND #SK < 10 OR #SK > 30
// 有効なクエリ
#PK = :partitionKey AND #SK between 10 AND 30
#PK = :partitionKey
また、DynamoDBではPrimary Keyの他に、GSI(グローバルセカンダリインデックス)を指定できます。(やっと出てきました、、この記事の主題です。)
GSIとは、PrimaryKeyを含む項目の任意の属性に対して貼ることのできるIndexで、それぞれにPKと(必要があれば)SKを定義することができます。GSIではPKとSKがテーブルで一意である必要はありません。他にもLSI(ローカルセカンダリインデックス)なるものもあるのですが、あまり使わないかと思いますのでここでは触れません。
DynamoDBでクエリを書く際は、Primary KeyかGSI、LSI等のIndexを一つ指定して書くことになります。それゆえ、スキーマ設計前に必要な全てのクエリに関して理解しておくことが必須なのです。実際にアプリケーションを作ってみないとどんなクエリが必要なのかを全てイメージするのは困難かと思いますので、UIのワイヤーフレームを使って画面構成と必要なクエリを具体化することを勧めます。
3. スキーマ設計
ここまでやってやっとスキーマ設計の話になります。以降章を分けます。
DynamoDBと仲良くなるにはGSIを理解すること
当然PrimaryKeyだけでは、全てのクエリに対応するのは困難なので、DynamoDB設計はGSI貼りゲーだと思っております。いかにうまくGSIを貼るのか、いかにうまくSKを設定できるかが全てです。
GSIはかつて、テーブルに対して貼ることのできる上限数が決まっていましたが、2019/06/21に実質的にその制限は取り払われました。(嬉しすぎ、マジタピオカ。)しかしながら、思考停止で全てのクエリにGSIを貼りまくっていると、多分月末にAWSからの請求で死にますし、書き込みコストがエグくなってアプリケーションとしても遅くて死にます。
GSIの仕組みを理解し、適切にインデックスできるようにしましょう。詳しくは前掲の公式ドキュメントをみて欲しいですが、知っておけば最低限のアレルギーを克服できると思った2つのポイントを書いておきます。
POINT 1:
GSI貼るってテーブルもう一個作っているということ。
GSIを貼ったとき、書き込みコストは基本的に倍になります。というのは、GSIを貼ったとき、DynamoDBはベーステーブルのほかに、ベーステーブルを射影したGSI用のテーブルをもう一つ用意しているからです。つまり、書き込みをおこなう際に書き込むべきテーブルがGSIの数だけ増えていきます。
DynamoDBは単純化すると、書き込みリクエスト数とサイズに対して課金されます。この際、GSIが複数貼ってあると、書き込みリクエスト数がGSIの数だけ増えるのです。金もかかるし、当然パフォーマンスも損ないます。
POINT2:
GSIはデフォルトでスパースである。
POINT1がテーブルを少なく保つというベストプラクティスに矛盾していると思った皆様朗報です。GSIはデフォルトでスパースになるように設計されています。
基本的にGSIで作られたテーブルは、ベーステーブルのサブセットになっており、GSIで指定されたPKおよびSKを含まない項目はインデックスされません。
すなわち、GSIのPKおよび、SKに指定されたある特定の属性を持たない項目は、GSIで新たに作られるサブセットに含まれません。またそのサブセットに射影する属性に関してもGSIの作成時にコントロールできるので、サブセット内の項目は必要十分に小さく保つことができます。
この特性により、テーブル数を最小に保ったまま、書き込みのパフォーマンスと特定のクエリのパフォーマンスを維持できるのです。
終わりに
さて、多少なりとも、DynamoDBのベストプラクティスで言っていることの理解につながれば幸いです。あとは、マジで詳しいので公式ドキュメントを読みましょう。
これ以降論理飛躍するので、ポエムなのですが僕はDynamoDB設計こうするといいと思います。
1. Primary Keyの命名で項目の属性を規程しない
2. Primary Keyは複合キーにして一対多のリレーションの表現に用いる。
3. GSIはできるだけ小さなサブセットに分割する用途で用いて、項目の属性を規程するような命名をする。
さてさて、弊社では積極的にフロントエンドエンジニアの採用を勧めています。この記事をフロントエンドの人が見るなんてことは滅多にないかと思いますが何卒!