コンセプトから学ぶAmazon DynamoDB【複合キーテーブル篇】

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

よく訓練されたアップル信者、都元です。DynamoDB楽しいです。みんなもっと使えばいいのにと思って最近のエントリーを書いています。今回は、前回名前だけ触れた「複合キーテーブル」について。DynamoDBについて全くご存知無い方は、まずは下記エントリーを読んで頂ければと思います。

今回のサンプルデータ

フォーラム・スレッド・返信投稿という3要素で構成した掲示板のデータベースとして、それぞれForum, Thread, Reply というDynamoDBのtableがあり、下記のようなitemがそれぞれのtableに入っているようなイメージをしてみてください。

// Forum
{
  "Name": "DynamoDB",
  "Category": "Amazon Web Services",
  "Threads": 3,
  "Messages": 4,
  "Views": 1000,
  "LastPostBy": "User A",
  "LastPostDateTime": "2012-01-03T00:40:57.165Z"
}
{     
  "Name": "Amazon S3",
  "Category": "AWS",
  "Threads": 1
}
// Thread
{
  "ForumName": "DynamoDB",
  "Subject": "DynamoDB Thread 1",
  "Message": "DynamoDB thread 1 message text",
  "LastPostedBy": "User A",
  "Views": 0,
  "Replies": 0,
  "Answered": 0,
  "Tags": [ "index", "primarykey", "table" ],
  "LastPostDateTime": "2012-01-03T00:40:57.165Z"
}
{
  "ForumName": "DynamoDB",
  "Subject": "DynamoDB Thread 2",
  "Message": "DynamoDB thread 2 message text",
  "LastPostedBy": "User A",
  "Views": 0,
  "Replies": 0,
  "Answered": 0,
  "Tags": [ "index", "primarykey", "rangekey" ],
  "LastPostDateTime": "2012-01-03T00:40:57.165Z"
}
{
  "ForumName": "Amazon S3",
  "Subject": "Amazon S3 Thread 1",
  "Message": "Amazon S3 Thread 1 message text",
  "LastPostedBy": "User A",
  "Views": 0,
  "Replies": 0,
  "Answered": 0,
  "Tags": [ "largeobject", "multipart upload" ],
  "LastPostDateTime": "2012-01-03T00:40:57.165Z" 
}
// Reply
{
  "Id": "DynamoDB#DynamoDB Thread 1",
  "ReplyDateTime": "2011-12-11T00:40:57.165Z",
  "Message": "DynamoDB Thread 1 Reply 1 text",
  "PostedBy": "User A"
}
{
  "Id": "DynamoDB#DynamoDB Thread 1",
  "ReplyDateTime": "2011-12-18T00:40:57.165Z",
  "Message": "DynamoDB Thread 1 Reply 1 text",
  "PostedBy": "User A"
}
{
  "Id": "DynamoDB#DynamoDB Thread 1",
  "ReplyDateTime": "2011-12-25T00:40:57.165Z",
  "Message": "DynamoDB Thread 1 Reply 3 text",
  "PostedBy": "User B"
}
{
  "Id": "DynamoDB#DynamoDB Thread 2",
  "ReplyDateTime": "2011-12-25T00:40:57.165Z",
  "Message": "DynamoDB Thread 2 Reply 1 text",
  "PostedBy": "User A"
}
{
  "Id": "DynamoDB#DynamoDB Thread 2",
  "ReplyDateTime": "2012-01-03T00:40:57.165Z",
  "Message": "DynamoDB Thread 2 Reply 2",
  "PostedBy": "User A"
}

うんうん、まぁよくありそうな感じですね。タグ辺りが正規化されていないのも、item毎にattributeの数や種類が違うのも、NoSQLならではです。

さて、このデータにもとづいて「フォーラム一覧画面」「DynamoDBフォーラムのスレッド一覧画面」「DynamoDB Thread 2の投稿一覧画面」を表示することを考えてみましょう。この場合、どのattributeをキーにすると良いでしょうか。

まず、ForumtableについてはNameがハッシュキーということで問題ないでしょう。全item取得の操作(これをDynamoDBではScanと呼びます)をすればフォーラム一覧画面が作れますね。

しかし、その他の画面用のデータアクセスについては、ハッシュキーテーブルとしては設計が難しそうです。というのも、ハッシュキーテーブルに対しては「キーを指定した1件取得(GetItem)」か「全件取得(Scan)」という選択肢しかありません。

この問題に対応するために、DynamoDBでは「一定の規則で並んだitemのリストから、特定の範囲を切り出して複数個のitemを取り出す(Query)」という取得操作を提供しています。ただし、Queryはハッシュキーテーブルに対しては実行できません。(正確には、実行できるが無意味)

複合キーテーブル

ハッシュキーテーブルでは、table作成時に1つのattributeを選び、それをハッシュキーとして宣言しました。

そうではなく、table作成時に2つのattributeを選び、1つをハッシュキーとして、もう一つをレンジキーと呼ばれるキーとして宣言する、という方法があります。この方法で定義したtableを「複合キーテーブル」と呼ぶことにします。(ハッシュキーテーブル及び複合キーテーブルは、本稿で便宜上名づけたもので、公式用語ではありませんのでご注意ください。)

さて具体的に。先ほどのThreadtableは、ForumNameをハッシュキーとして、そしてSubjectをレンジキーとして宣言した複合キーテーブルとします。また、Replytableは、Idをハッシュキー、ReplyDateTimeをレンジキーとして宣言します。

この2つのattributeは複合キーとして働きます。つまり、2つの値の組み合わせによって、1つのitemを特定します。ThreadtableにおいてForumName = 'DynamoDB' AND Subject = 'DynamoDB Thread 1'であるitemは一つだけしか保存できません。(2つ目を保存しようとすると古いitemを上書きします。)

言い換えると、ハッシュキーだけではitemを1つに特定できません。具体例を見てもそうなっていると思います。ThreadtableにおいてForumName = 'DynamoDB'であるitemは複数ありますね。

これがハッシュキーテーブルと異なるポイントです。ハッシュキーテーブルのハッシュキーは単独で重複を許しませんでしたが、複合キーテーブルのハッシュキーは、単独であれば重複が許されます。ここは理解のポイントだと(勝手に)思ってます。私がそうだったものでw

さて、このようなtableに対する読み出し操作 Scan, GetItem, Query をそれぞれ見ていきましょう。

まずScanはハッシュキーテーブルと全く同じです。パラメータ無くとにかく問い合わせれば全件返ってキます。

次にGetItemの場合は、ハッシュキーの値をレンジキーの値2つを与えます。その結果、0個または1個のitemを返します。複合主キーを使ったRDBの挙動と一緒ですね。

そしてQueryです。多分一般的な説明をしてもわかりづらいので、まずはThreadtableで考えてみましょう。Threadtableでは、ForumNameがハッシュキー、Subjectがレンジキーでした。このtableに対してForumName = 'DynamoDB'という条件でQueryができます。つまり「DynamoDBフォーラムのスレッド一覧画面」で必要なリストが取れます。

続いてReplytableです。こちらはIdがハッシュキー、ReplyDateTimeがレンジキーでした。このtableに対してId = 'DynamoDB#DynamoDB Thread 1' AND ReplyDateTime BETWEEN '2011-12-15T00:00:00.000Z' AND '2011-12-31T23:59:59.999Z' というような条件でQueryができます。そう、レンジキーで指定したカラムは、範囲(range)を検索条件として指定できるのです。もちろん、上記Threadの例の通り、レンジキーに対する条件は省略(つまりハッシュキーだけによる絞り込み)も可能です。

ちょっと複雑に感じるでしょうか。このような検索操作ができる(そして、そうでない検索操作が難しい=提供されていない)理由は、次にお話するパーティションの話によって理解しやすくなると思いますので、引き続きお付き合い下さい。

とりあえずここまでの話をまとめるとこんな感じです。

ハッシュキーテーブル 複合キーテーブル
Scan 全件取得 全件取得
GetItem hash-keyに対するequal-to条件値を1つ指定して、0〜1件取得 hash-keyとrange-key両者に対するequal-to条件値を指定して、0〜1件取得
Query (無意味) hash-keyに対するequal-to条件値を1つ、range-keyに対する範囲条件(optional)を指定して、0〜複数件取得

複合キーテーブルにおけるパーティショニング

さて、複合キーテーブルもDynamoDBとして性能の担保のためのパーティショニングを行っています。あるitemをどのパーティションに保存すべきかは、ハッシュキーテーブルの時と同じ、ハッシュキーに基づいて決定しています。

ただし、複合キーテーブルでは、1つのハッシュキーにつき複数の(レンジキー値の異なる)itemがあるため、それらが全て同じパーティション内に保存されることになります。そしてもう一つ。同じハッシュキーを持つitem群は、レンジキーによってソートされた状態で保存されています、多分。(実装は非公開のため、予想です)

具体的には下記の表を見てください。これはReplytableの例です。ハッシュキーによって、パーティションが1番と3番に分かれています。そして、レンジキーによってソートされています。

partition hash-key range-key value
1 DynamoDB#DynamoDB Thread 1 2011-12-11T00:40:57.165Z { ... }
1 DynamoDB#DynamoDB Thread 1 2011-12-18T00:40:57.165Z { ... }
1 DynamoDB#DynamoDB Thread 1 2011-12-25T00:40:57.165Z { ... }
3 DynamoDB#DynamoDB Thread 2 2011-12-25T00:40:57.165Z { ... }
3 DynamoDB#DynamoDB Thread 2 2012-01-03T00:40:57.165Z { ... }

このような状態でデータを保持していると、ハッシュキーを1つに特定(=操作対象のパーティションを特定)した上で、レンジキーを範囲条件で切り出して返す、という操作が非常に自然で高速に行えることが感覚的に理解してもらえると思います。一方、操作対象となるパーティションが絞れない操作はサポートされない、と考えれば問題ないです。

例えば、ハッシュキーは特に指定せず、レンジキーの範囲だけで検索(つまりrange BETWEEN x AND yによる検索)を考えてみましょう。これは、全てのパーティションで検索を行い、その結果をマージしなければならないため、コストが大きいですね。なのでこのような操作はサポートされていません。

そして「ハッシュキーの値が'A'から始まるもの」という検索も、RDBではhash LIKE 'A%'といった気軽な検索が出来たと思いますが、DynamoDBではサポートしません。

一方、hash = x AND range LIKE 'A%'であれば、自然に集合を切り出して来れそうですね。ご想像の通り、サポートしています。

ここまで想像ができると、ReplytableのIdattributeが何故こんな形(フォーラム名とスレッド名を#でjoinしたもの)になっているのか、にも一定の納得が行くと思います。RDBの設計では考えられないことですけどね。

あともう一つ。Queryには大きな特徴があります。Queryの結果は、常にレンジキーの値でソート済みで返って来ます。上記のイメージを持っていれば、当たり前なんですけどね。

ハッシュキーの設計

さて。前回のコンセプトから学ぶAmazon DynamoDB【ハッシュキーテーブル篇】では「すべてのデータが確率的には均一に全てのパーティションに分散するため、パーティション1つだけで受け持つ状況とくらべて16倍の性能が確保できる」と説明しました。

ただし、この原理には弱点があります。1つのitemだけを集中的に読み書きするようなシステムです。そりゃそうですよね、性能を確保するために複数のパーティションを並べているのに、1つのアイテムへの読み書きが集中したら、1つのパーティションだけが高負荷になるだけです。このようなパーティションのことを「ホットパーティション」と呼び、DynamoDBを利用する際はホットパーティションの発生を回避するように設計しなければなりません。その具体例については、また改めて説明する機会を設けられればと思っています。

で、ホットパーティションは、ハッシュキーテーブルよりも複合キーテーブルにおいて発生する可能性が高くなります。1つのitemに集中していなくても、1つのhash-keyに集中するだけでホットになってしまうからです。

例えば。前述の「レンジキーの範囲だけで検索(つまりrange BETWEEN x AND yによる検索)」の例を考えてみましょう。裏側のイメージが出来ていない人は、表面的なことだけを考えてこんなことを考えるかもしれません。

あ! ハッシュキー内でしか範囲検索できないのならば、ハッシュキーは固定で1とかにしておいて、そして使わなければいいんだ。そうすれば全件に対する範囲検索ができるぞー!

確かに、機能的には。そして開発中も上手く動く気がします。そして多分負荷テストでコケます。負荷テストをサボると本番でトビます。怖いですね。

DynamoDBにおいては、裏側のイメージをきちんと持ってキー設計をすることが大事です。