ちょっと話題の記事

[レポート]DynamoDBデータモデリング (CMY304) #reinvent

2019.12.11

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

DynamoDBのデータモデリングって、よくわからなくないですか?正規化してはいけないとか、一つのテーブルで全データを扱うんだとか聞きますが、具体的にどうしたらよいのか…

という疑問に正面から答えてくれるセッションにre:Invent中に遭遇しました!具体的なアプリケーション例を挙げて、そこでのデータモデリング方法を詳細に解説してくれます。DynamoDBデータモデリング初心者の方は是非ご一読ください!

※「DynamoDBとは」といった前段の説明が不要な方はここからどうぞ

セッションタイトル

CMY304 - Data modeling with Amazon DynamoDB

セッション概要

DynamoDBであなたのデータをモデリングするには伝統的なRDSでモデリングする場合と異なるアプローチが必要です。

Alex DeBrieはDynamoDBを使ったアプリケーションをいくつか実装しており、DynamoDBを学べる無料の資料であるDynamoDBGuide.comの著者でもあります。

このセッションでは、DynamoDBテーブルのモデリングにおける基本原理を振り返り、データモデルで使用する実用的なパターンをお伝えします。
セッション終了時にはあなたはDynamoDBを使用する際に従うべき手順と指針となる原則を身につけていることでしょう。

スピーカー

Alex DeBrie - Engineering Manager, Serverless, Inc.

セッション内容

DynamoDBとは

NoSQL データベース

  • 2000年代に登場
  • RDBが処理できないような大量のトラフィックが発生するアクセスパターン用に使われる
  • MongoDB、Cassandra、DynamoDB

AWS完全マネージド

  • サーバーを追加したり、パッチを当てたり、アップグレードしたり、フェイルオーバーのことを考えたりしなくて良い

IAM認証を伴うHTTPS

  • ほとんどのDBは永続的なTCP接続を再利用する
  • 一方DynamoDBはHTTPSを使う。よりステートレスな接続モデルを採用している
  • IAM認証も使用
  • EC2インスタンスやLambda関数といったAWSリソースからDynamoDBを使う場合、IAMロールの関連付けを使用できる。トークンのローテーションを考える必要がない

スケール時の高速かつ一貫した性能

  • 1桁ミリ秒以内の遅延
  • 100GB、1TB、10TBになっても同様の性能を得られる

二つの領域(ユースケース)

ハイパースケール
  • 大量のデータを格納したい
  • 迅速にそのデータを取り出したい
  • RDBでは難しい
  • 例:Amazon.comのショッピングカート
  • 事実、DynamoDBの基礎はAmazon.comがブラックフライデーやサイバーマンデー、プライムデー等にOracleでは対処できないということを経験したところから生まれた
  • 別の例:Lyft (UBERのような配車サービス)
    • 位置情報をDynamoDBに格納している
    • 世界中の利用者の位置情報を毎秒更新している
Hyper-Ephemeral compute
  • Hyper-Ephemeral compute = LambdaやAppSyncといったサービスのこと。迅速にスケールアップするコンピューティングリソース
  • RDBだと対処が難しい
  • HTTPS接続モデルを採用しているDynamoDBの方が容易に対処可能

キーコンセプト

以下のようなアプリケーションを想定

  • 認証、認可機能が必要
  • そのためユーザーログイン機能、セッションストアが必要

このサンプルアプリケーションを元に、以下4つのキーコンセプトを学びます。

  • Table
  • Item
  • Primary Key
  • Attributes

セッションストアとして、以下のようなデータがあるとします。4人のマーベルキャラクターのセッション情報が格納されています。

このデータ全体のことをTableと言います。RDBにおけるテーブルの概念とほぼ同じです。MongoDBでいうところのコレクションです。

各レコードのことはItemと言います。RDBでのRow、MongoDBのDocumentですね。

テーブルを定義するとPrimary Keyを指定する必要があります。全Itemがこのキーを持たなければなりません。今回はSessionIdをPrimary Keyとします。一意のUUIDです。Primary Key値は一意である必要があります。

ItemはPrimary Key以外にもデータを持つことができます。これらをAttributesと呼びます。今回の例ではUsernameやCreatedAtなどのデータを持たせています。RDSのカラムに似ていますが、事前に型定義しなくて良い点、同テーブルのItem間で違うAttributeを持っている場合がある点が異なります。

Primary Keyの種類

Primary Keyには二種類存在します。ひとつはSimple Primary Keyで、Partition Keyのみを持ちます。先ほどお見せしたようなものです。

もう一つはComposite Primary Keyです。Partition KeyとSort Keyを持ちます。

Composite Primary Keyの例です。

俳優と、彼らが出演した映画の情報を格納しています。俳優名をPartition Keyにして、映画タイトルをSort Keyにしています。トムハンクスのレコードが二つあります。が、映画タイトルが異なるので、これらのレコードは一意だと見なされます。

APIアクション

DynamoDBとのやりとりは基本SDKになるので、APIドリブンになります。クエリドリブンなRDBとは少し異なります。

3種のアクションに分類できます。

Item-based actions

Itemを追加する・更新する・削除するといったアクションです。単一のItemに対して処理を行ないます。

Item-based actionsについて重要な点は、Primary Key全体を指定しなければならない点です。先ほどの俳優と映画タイトルの例でいうと、ちゃんと「俳優名はトムハンクスで、映画タイトルはCast AwayであるItemを削除する」というように指定しないといけません。

Query

次はQueryアクションです。読み取りアクションですが1リクエストで複数Itemをフェッチすることができます。例えば、トムハンクスが出演している映画情報を取得したい、などというケースです。

重要な点は、必ずPartition Keyを指定しないといけない点です。Sort Key条件はオプションで指定可能です。

Scan

最後はScanです。RDBのScanと似ています。テーブル全体をScanします。

ほとんどの場合、この操作は避けるべきです。スケール時に高価になります。レスポンスに時間がかかりますし、必要なキャパシティも大きくなってしまいます。

Secondary Indexes

先ほどの俳優名と出演映画タイトルの例でいうと、俳優名でQueryすることはできます。が、映画タイトルでQueryし、Toy Story出演者を得たい場合はどうすれば良いでしょうか。

こういった際にSecondary Indexesを使います。下図のように、Partition KeyとSort Keyを逆にしたSecondary Indexesを作成します。

これでToy Story で検索可能です。

データモデリング例

データモデリングを実施する際、以下の3ステップを経ることをお勧めします。

基本

  • ER図から始める
  • アクセスパターンを定義する
  • Primary KeyとSecondary Indexを設計する

また、以下のRDBでの経験は忘れるべきです。

  • 正規化
  • JOIN(テーブル連結)
  • テーブル毎に一つのエンティティを持つ

ここからは以下のサンプルアプリケーションの場合においてのデータモデリングを考えていきます。

  • Eコマースストア(アマゾンの競合)
  • ユーザーが注文する
  • 注文には複数の商品が含まれうる

1. ER図を作成する

スピーカーが考えたER図です。4つのエンティティ、3つのリレーションがあります。

2. アクセスパターンを定義する

以下のアクセスパターンが想定されます。

  1. ユーザープロフィールを取得
  2. ユーザーの注文を取得
  3. 特定の注文とそこに含まれる商品を取得
  4. 特定のステータスのユーザーの注文を取得
  5. オープンな(まだ梱包完了していない)注文を取得 倉庫スタッフが利用することを想定

3.Primary KeyとSecondary Indexを設計する

4つあるエンティティのうち、ユーザーとオーダーが一番アプリケーションのコア部分に関わっているので、まずはユーザー部分のモデリングから始めましょう。

二つ興味深い点があります。一つ目はPrimary Keyです。

Composite Primary Keyを採用しています。Partition Key名とSort Key名両方とも非常に一般的な名前、PKとSKにしています。userIDやorderIDなどにはしていません。こうしている理由は、このテーブルの中に複数のエンティティタイプのデータを入れる予定だからです。

二つ目の点は、PK値が USER# で始まっている点です。そのあとにalexdebrieなどのユーザー名が続きます。

これは、どのタイプのエンティティを扱うか決める(把握する)際に役立ちます。USER#が頭に付いていればそれはユーザーに関するItemなのだとすぐにわかる、ということです。

重複を防ぐこともできます。例えばユーザーIDとオーダーIDが被った場合、互いに間違ったデータを更新してしまう恐れがあります。が、このようなプレフィックスをつけておくことで予防できます。

もちろんQueryやソート時にも便利です。後ほどお見せします。

エンティティチャートを使って、エンティティ毎にPKのパターンSKのパターンがどうなるか整理しましょう。Userエンティティのパターンは以下のようになります。

1対多の関係

Userは複数のUser Addressを持ち得ます。自宅住所、会社用の住所など。User Addressのモデリングについて考える際に最初に考えることは、住所を直接操作するようなアクセスパターンがいくつあるか、です。二つ目は、ユーザーは何個住所を持てるか、です。今回の場合、直接住所にアクセスすることはないでしょうし、住所数を制限したくもありません。そこで、非正規化をします。

User ProfileにAddressesというAttributeを追加し、そこにmapやlistといった型を使って住所データを格納します。

User AddressはUserのAttributeとなったため、PK・SKは不要になりました。

二つ目の1対多の関係に移りましょう。UserとOrderの関係です。Userは複数の注文を持ち得ます。

先ほどのテーブルにSKをORDER#<orderId>としてItemを追加します。 そしてこのItemは先ほどのProfileのItemとは異なるAttributeを持ちます。OrderIdとStatusです。

エンティティチャートは以下になります。

同一ユーザーの場合、UserとOrderは同じPK、同じPartitionになります。特定のユーザーの全注文を取得したい場合、以下のようなクエリを発行します。

PK = USER#Alexdebrie AND BEGINS_WITH(SK, 'ORDER#')

以下のように、特定ユーザーの注文データのみ取得できるでしょう。プロフィールは含まれません。

三つ目の1対多の関係パターンです。注文には複数の商品が含まれ得ます。

先ほどのやり方を使えないかと考えます。が、問題は、すでに1対多の関係にあるものに対してさらに1対多の関係をもたせる点です。ユーザーは注文を持っていて、注文には商品があります。このような場合、Composite Primary Keyは機能しません。

そこで、以下のようにします。

PKをITEM#<itemId>にし、SKを ORDER#<orderID> にします。

エンティティチャートに戻ります。

重要な点はSKがOrderとOrder Itemで同じということです。

Inverted Index

Partiion KeyとSort Keyを逆にしたSecondary Indexを作成します。Inverted Indexと呼ばれるテクニックです。

こうすることで、Partition KeyにORDER#<orderID>を指定する一つのリクエストで、二つの異なるエンティティを得ることができるようになります。注文したユーザーの情報と、注文に含まれる商品の情報ですね。

1対多の関係パターンまとめ
  • Attribute (list or map)
  • Primary key + query
  • Secondary index + query

フィルタリング

フィルタリングの話に移りましょう。RDBの場合はWhere句で柔軟に実現できます。JOINとかbuilt-in functionも使えます。

DynamoDBにはこれらに匹敵するものはありません。Primary Keyでフィルタリングを実現する必要があります。

OrderのStatusがSHIPPEDになっている全Itemを取得したいとします。

Python の SDKでこんな感じでQueryしたいところです。

が、これはできません。前述のAPIアクションのQuery欄でご説明した通り、Query時には必ずPartition Keyを指定する必要があります。欲しい2Itemは別のPartition Keyを持っています。Partition KeyをまたいでのQueryはできません。

Scanにもフィルターがあるからこっちを使ったら?と考えるかもしれません。

が、これはお勧めしません。どのようにScanが動作するか見てみましょう。

  1. ItemをTableから読み込んでメモリに格納
  2. マッチしないItemを除外する
  3. (マッチした)Itemを返す

問題は、「1.ItemをTableから読み込んでメモリに格納」のところに、1MBの制限があることです。1GBのテーブルがあったとしたら(それほど大きくありませんが)、1GB÷1MBで1000回のリクエストが発生します。非常に低速になりますし、プロビジョニングされたスループットが 1 回のオペレーションで枯渇する可能性があります。よってScanは使用しないことをお勧めします。


さて、今回のアプリでフィルタリングが必要なアクセスパターンを確認しましょう。

  1. ユーザーの注文を取得
  2. 特定のステータスのユーザーの注文を取得
  3. オープンな(まだ梱包完了していない)注文を取得 倉庫スタッフが利用することを想定

最初の「ユーザーの注文を取得」これは以下のようなクエリで実現可能です。

PK = USER#alexdebrie AND BEGINS_WITH(SK, 'ORDER#')


二つ目、「特定のステータスのユーザーの注文を取得」です。

ステータスはAttributeにあるので、このままではフィルタリングできないですね。

Composite Sort Keyという手法を使います。まず、StatusとCreatedAt値をくっつけた、OrderStatusDateというAttributeを追加します。

つぎに、このAttributeをSort KeyにしたSecondary Indexを作成します。PKは元のままです。

そして以下のQueryを実行します。

PK = USER#Alexdebrie AND BEGINS_WITH(OrderStatusDate, 'SHIPPED#')

欲しい「特定のステータスのユーザーの注文を取得」ができました!

特筆すべき点は、ステータスと注文日、両方をQueryに使える、ということです。ですので例えば以下のようなQueryも可能です。

  • Alexdebrieの今年4月から6月のSHIPPEDの注文データを頂戴
  • 今年のクリスマスまでにSHIPPEDになったAlexdebrieの注文データを頂戴

最後は「オープンな(まだ梱包完了していない)注文を取得」です。ユーザー用のアクセスパターンではなく、倉庫の従業員用です。該当商品を倉庫内でピックする際に使います。

基本的にこれはDynamoDBでは難しいQueryです。なぜなら、Queryする際にはPartition Keyの指定が必要だからです。このQueryはPartition Keyで絞れる類のものではなく、いわばGlobal Queryです。

Tableをみてみましょう。3つ、PLACEDステータスのOrderがあります。別々のPartition上に存在しています。どのようにQueryできるのでしょうか。

Sparse Index Patternというものを使います。

まず、PlacedIdという新Attributeを追加します。値は一意であればなんでも構いません。

次に、このPlacedIdをPartition KeyにしたSecondary Indexを作成します。

このSecondary Indexに現れるItemは、もとのTableでPlacedIdを持っているItemだけです。つまりUser ProfileにはPlacedIdがありませんのでここには現れません。Orderについては、ステータスがPLACEDの時にだけこのPlacedId Attributeを付与するようにすれば、他のステータスのOrderは現れません。PLACEDから次のステータスに遷移させる際にこのAttributeを削除するようにします。そうすれば、倉庫の従業員はこのSecondary Indexだけ見ていれば、ピックする必要のあるOrderを把握できるようになります。

フィルタリングパターンまとめ
  • Primary key
  • Composite sort key
  • Sparse index

感想

具体的な例を用いてモデリングパターンを説明してくださったので、非常に「ナルホド!」と腹落ち感のある良いセッションでした!DynamoDBデータモデリング面白い!

セッション動画