All Articles

DynamoDB のテーブル設計

DynamoDB 歴 2 週間の私が DynamoDB のテーブルを設計を考えてみた。

初めに結論。

  • partition key, sort key は String にしろ
  • partition key と sort key の名前は単に pk, sk にしろ
  • スキーマは考えずにコードを書け

結局のところこうしないと柔軟性のある形できれいにデザインができない。

今回のお題

最近ホームオートメーション的なことをいろいろやっているのですが、そうするといわゆる IoT デバイスから取った情報をデータベースに入れておきたいわけです。電力情報とか、温度・湿度情報とか。

当初電力情報を格納するためのこういうテーブルを設計しました。

devid timestamp cumulative_kwh_p cumulative_kwh_n current_w
String Number (Float) (Float) (Number)

devid は私の場合は Nature E Lite にもともとついてる device id で、timestamp は UNIX 時間。他は積算電力の消費と供給、あと瞬間消費電力。

で、将来的には温度・湿度計のデータでも突っ込もうかと思っていた。

devid timestamp room temprature humidity
String Number (String) (Float) (Float)

どこの部屋の温度と湿度がいつのタイミングでいくつだったかと。devid と timestamp の構造は同じなので同一テーブルに格納できるわけです。

このシンプルな設計でも、もしかすると快適に過ごせたかもしれないのですが、やっぱり RDB 脳からすると「えーと、room の一覧とか取れんの?」とか、「devid の一覧とか取れんの?」とか思うわけです。でも NoSQL 的性質により、簡単に取る方法がない。グローバルにスキャンすることになってしまう。

でも電力も温度・湿度も、少なくとも数分に 1 エントリくらいの頻度で増えていくので、スキャンはさすがにありえない。でも一覧取れないのもデータベース的にはありえなくないですか?

partition key, sort key は String にしろ

結局のところ、DynamoDB は Key-Value ストアの亜種です。Key を指定しなければ話が始まらない。

でも sort key っていうのがあるから、なんかいつもの感じで query 投げられるような錯覚に陥る。だけど投げようと思っても思いどおりの query が投げられない。

key の種類は String, Number, Binary から選べますが、String は簡単に Number の代わりとかできますが、逆は難しいんですよね。将来どうなるかわからんからいろいろ柔軟にできるようにしておきたい、と思ったら、String 一択です。他を選んではいけない。さもなければテーブルから作り直す羽目になるだろう。

partition key と sort key の名前は単に pk, sk にしろ

繰り返しになりますが、DynamoDB は Key-Value ストアの亜種です。もしも key と value しか設定できないときに、わざわざ key と value に意味のある名前を付けますか?

まあ型とかの都合で value に名前(クラス)があるのは理解ができるのですか、key は key とか name とかとしか呼びようがない。それに対応するものが引けるだけだから。

逆に言うと、PK に devid、SK に timestamp とかいう名前を付けたその時点で、PK には ID 的なもの、SK にはタイムスタンプを入れるしかなくなるんですよ。これはあまりにも柔軟性に欠けている。

結局 key は key でしかないわけだから、partition key は pk、sort key は sk という名前以上のものを付ける意味がありません。

スキーマは考えずにコードを書け

もともと DynamoDB ではアトリビュートに型はつけられません。key だけが例外。

要するに、型チェック的なものが必要であれば、コードがやるしかない。DB は何も保証してくれない。これは普通の RDB との違いだと思います。

だから、ただ単にコードからは KVS だと思ってデータを格納すればいい。pk と sk が DB の item だけを見たときに意味がわからなくてもいいのです。それはコードがいじるから。コード内の変数の型と対応が取れてさえいればそれでいい。

いわゆる code-first approach ですね(これは GraphQL の文脈のポストですが)。

そしたらどうなった

まず、電力情報は単にこうなる。

pk sk cumulative_kwh_p cumulative_kwh_n current_w
[devid] TS#[ISO8061] (Float) (Float) (Number)

pk にはこれまで通り device id を突っ込む。sk には TS# を前置した上で、ISO8061 形式の時間をタイムゾーンは固定して突っ込みます(先頭に 0 埋めして UNIX 時間を入れる方法もありますが)。他は同じ。これで十分データは引くことができます。

温度湿度も同様。

pk sk room temprature humidity
[devid] TS#[ISO8061] (String) (Float) (Float)

次に room の一覧を取れるものを作る。同じテーブル内にこう突っ込む。

pk sk
ROOM (温度・湿度の room と同じ)

pk = ROOM で引けば、room の一覧が取れる。必要であれば name という attribute で部屋の別名を入れたりしてもいい(引っ越したケースなども想定して、room には chiyoda1-A みたいな家と部屋を接続した ID 的なもの、name を入れるとしたら「〜の部屋」みたいな変わりうる属性を入れるべきだろう)。

デバイス一覧も同様。

pk sk
DEVICE (devid と同じ)

pk = DEVICE で引けば、room の一覧が取れる。

ちなみに電力や温度湿度の sk には timestamp であることを示す TS# を前置しているのは必須ではないです。ただ、sk に値的なものを突っ込んでしまうケースが出てきているので、前置しておかないと紛らわしくなるケースが出てくる可能性はあります。

例えば PK=room, SK=sk とした GSI を考えたときに、sk は常に ISO8061 であると仮定できるだろうか…ということを考える必要があります。これが仮定できないのだとすると、sk で何らかの範囲を絞り込みたい場合に、数字で始まる何か意図しないエントリが返ってきてしまう可能性があります。プレフィックスを毎回付けたり外したりするのと、将来レコードを全部更新するか微妙なワークアラウンドをする羽目になるかのトレードオフでしょうか。

あとは、pk = DEVICE に登録されている devid が、各時系列エントリに登録されている devid と一対一対応が取れた状態を保証できるか、なんてことが問題になったりすると思いますが、これについては(DEVICE の数が十分に少ない前提で)プログラム起動時にリストとして取得しておいて、時系列エントリの登録時に知らない devid のやつが来たら保存しておく、ような形になると思います。Lambda で動かす場合だとコールドスタート時に 1 回読んでおく感じですね。時系列データをバンバン突っ込むテーブルの想定であれば、そう頻繁にはコールドスタートにはならないでしょうし、コールドスタートするほど間が開くのであれば RCU の消費もそれほど気にはならないでしょう。

まとめ

今回は「IoT デバイスから来る時系列データを保存したい」という状況において、「補助データは pk を RDB 的な TABLE に見立てて一緒に突っ込む」というアプローチによって、比較的自然な形で一覧取得可能にしました。

ただ、どのような種類のテーブルでも

  • partition key, sort key は String にしろ
  • partition key と sort key の名前は単に pk, sk にしろ
  • スキーマは考えずにコードを書け

という考え方でテーブルを作っておけば後から何でも突っ込めるので、発想次第でどのようなデータでも整理して格納できると思います。