サーバレスアーキテクチャを検討する際に、データベース層をどうするかはよく議論になります。リレーショナルデータベースに慣れている人は、なんとか RDS を採用できないか考えるのですが、現状は DB のコネクションプール問題などで RDS を用いるのはアンチパターンと言われています。
代替として用いられるのが NoSQL 型のデータベースである DynamoDB です。前述のような問題は発生せず、AWS でサーバレスなシステムを構築する際にデータベース層に採用されることが多いです。
しかし、これは私だけかもしれませんが、DynamoDBの(というよりも NoSQL 型データベースの?)設計に慣れていないこともあり、
- 「この要求・要件を実現するときに、どうテーブル設計すべき?」
- 「この設計で将来の機能拡張に耐えられるの?」
と不安になるシーンが多いです。特に後者が多く、これまでも「既に見えている」要求・要件を満たす設計はできても、「まだ見えていない」要求・要件に本当に対応できるのか自信が持てないことが多くありました。サーバレスな設計力を上げたい私にとって、DynamoDB のテーブル設計力を上げるのは喫緊の課題だと思っています。
先日、昔お仕事でご一緒させていただいた方からDynamoDBに関する質問をもらい、自分の中でもベスプラがわからないなーと思うことがあったので、WIP記事としてまとめていきたいと思います。きっと有識者の方から良いアドバイスをもらえるはず?:D
DynamoDB について
このあとの説明で出てくる「テーブルの種類」と「インデックスの種類」について簡単に説明します。
テーブルの種類
まずはテーブルの種類についてです。大きく二種類の定義方法があります。
Partition Key のみを持つテーブル
Partition Key はいわゆる主キーです。各レコードは、同一テーブル内でユニークな Partition Key を持つ必要があります。Partition Key では範囲検索を行うことができません。(以前は Hash Key とも呼ばれていました。)
Partition Key と Sort Key を持つテーブル
Partition Key に加えて Sort Key と呼ばれる列を持つテーブルです。各レコードは (Partition Key, Sort Key) のペアでユニークである必要があります。Partition Key では範囲検索を行うことができませんが、Sort Key は範囲検索を行うことが可能です。(以前は Range Key とも呼ばれていました。。。よね?)
インデックスの種類
DynamoDB では2種類のインデックスを貼ることができます。
ローカルセカンダリインデックス(LSI)
LSI は複合キーテーブルに対して、同じ Partition Key
かつ Sort Key とは異なるキー
で検索を掛けたい場合に貼るインデックスです。上の例で紹介した、
- ユーザテーブル - ログイン ID(String: Partition Key) - 最終ログイン日時(String: Sort Key) - その他の情報
のケースでは、「ログインIDが同じで」&「その他の情報のうちの一つ(例:ユーザ作成日時)で範囲検索をしたい」という要件の場合に LSI を用いることができます。LSI は 同じ Partition Key
でしか貼ることができないことが注意点です。
グローバルセカンダリインデックス(GSI)
続いてGSIです。GSI は、「任意の列での equal 検索」 & 「それ以外の列での範囲検索」をしたい場合に貼るインデックスです。複合キーテーブルで最初に定義する Partition Key
と Sort Key
の関係に似ていますが、GSI で設定した場合、Partition Key は必須項目になりません。(そのため、GSI の Partition Key に該当する列に値の入っていないレコードは検索に引っかかりません。)
ケーススタディ:範囲検索したいときにどうする?
ここからが本題です。この記事は、昔お世話になったお客さんから下記のような質問を頂いたことが契機になっています。
「DynamoDB詳しいと聞いたんですけど、」
(やばい、全然詳しくないぞ。)
「こんな感じのDynamoDBテーブルを作りたいです。」 - ユーザテーブル - ログイン ID(String: Partition Key) - 最終ログイン日時(String: Sort Key) - その他の情報
元お客さんの現在の事業内容に少し関わるので、上の例は多少マスクしています。が、DynamoDB 的な議論点は変わりません。
「んで、こういう検索をしたいんです。」 1)ログイン ID での検索 2)最終ログイン日時が1日以内の人を検索 「1)は Partition Key(ログインID)を使って検索できますよね? 2)は、Sort Key を使いたいのですが、まずは Partition Key を指定しないとならず。 でも全てのユーザに対して検索をしたいんですよー。」
わかる、わかります。よくあるお困りごとですよね。でも、このケースのベスプラ分かってないんです。私のパッと思いついた設計は下の2つです。(【設計1】は後付けですが、、)
設計1:「最終ログイン日時テーブルを作る」案
【設計1】 「最終ログイン日時テーブルを作る」案 - 最終ログインテーブル - ダミー列(String: Partition Key) - 最終ログイン日時(String: Sort Key) - ログイン ID(String)
ログインする度にこのテーブルを更新します。ダミー列はどのレコードも同じ値を入れ、検索は下記のようにします。
last_login_tbl.query( KeyConditionExpression= Key('dummy').eq("dummy") & Key('last_logined_at').between(start_datetime, end_datetime) )
最終ログイン日時は Range キーなので範囲検索できます。Hash キーはダミーデータを指定してやれば要件を(一応)満たします。
この設計の問題点は、
- 何気なく書いた「ログインする度にこのテーブルを更新します。」はそんな簡単ではない。複数テーブル間の操作をする際に、いわゆるロック的なことも実装できるがそれなりに手間がかかる。
- 何かのワークショップで「DynamoDBを使うなら1システム1テーブルにすべきだ」と聞いた気がするぞ。複数テーブルな時点で良いパターンではない?
- そもそも Partition キーが全部同じって、美しいテーブル設計な訳がないのでは、、、
といったところでしょうか。
設計2:「ダミー列に対してグローバルセカンダリインデックスを貼る」案
【設計2】 「ダミー列に対してグローバルセカンダリインデックスを貼る」案 - ユーザテーブル - ログイン ID(String: Partition Key) - 最終ログイン日時(String: Sort Key) - その他の情報 - ダミー列 * ダミー列を Partition Key、最終ログイン日時 を Sort Key とした GSI を貼る
設計1と同じくダミー列はどのレコードも同じ値を入れ、検索は下記のようにします。
user_tbl.query( IndexName='dummy-last-logined-at-index', KeyConditionExpression= Key('dummy').eq("dummy") & Key('last-login-at').between(starg_datetime, end_datetime) )
設計1よりかは幾分マシな気がします。とはいえ問題点も多く残り、
- 値が全て同じ列を用意するのって(ry
- 一つのテーブルで GSI は5つまでしか使えない。将来的に検索項目が増えた場合に辛くなることがあるかもしれない。
といったところでしょうか。
まとめと今後
設計2の方で動くは動くと思うのですが、美しくないですよね。いっそ Scan で全部抜いてきて Lambda 側で処理をさせるのが正解だったり、、、ないですね。
「好きなサービスは Amazon DynamoDB」と言っておきながら、まだまだ分かっていない部分が多いことが分かりました。継続して考えていきたいです。取り急ぎ、今後も個人で作るプロダクト・サービスに DynamoDB を利用し、ぶつかった問題を解決することで設計パターンを整理していきたいと思います。
現在、下記の re:Invent の講演を聞いてます(2回目)。こちらについても整理したいと思います。
また、GSI の 5つまでという制限ですが、 hoge#bar
という形で2つの列をあわせることで回避するパターンを教えて頂いたことがあります(たしかこれも re:Invent)。また、文中で「「DynamoDBを使うなら1システム1テーブルにすべきだ」と聞いた気が」と書きましたが、これもその方から聞いた気がします。このあたりのナレッジも、別途思い出しながらまとめていきたいと考えています。
最後に、この記事のタイトルは「君の膵臓をたべたい」のオマージュです。桜良ちゃん可愛すぎるのでおすすめです。
http://kimisui.jp/index.html#/boards/kimisuikimisui.jp