DynamoDBにおけるデータベース設計手法(の断片)

AWS DynamoDBにおけるデータベース設計について、(例えばリレーショナルモデルのように)一般化された理論を基に述べているものが見つけられない。

せめて、リレーショナルモデルと比較するかたちででもNoSQLのデータベース設計の手法を言語化してみてはどうか。それがこのエントリです。

本稿であつかうテクニックは以下のふたつを実現するものです。

  1. DynamoDBの制約の中で、それでも目的の検索を可能とするインデックスを構成する。いわゆる”タテ持ち”に近い手法を用いることで。
  2. テーブルの全体数を減らす。リレーションを、RDBとは違う方法で表現することで。

2.について少し補足しておきましょう。実は、テーブルの全体数を減らすことが重要なのではありません。 そもそもNoSQLではテーブルが特定の型を持たないので、型の違いをテーブルの違いで表現する理由が無いんですね。 あらゆるエンティティをたったひとつのテーブルに格納することも出来るわけです。そんなデータベース設計が人間に理解しやすいとは僕には思えないので、この最も本質的なポイントは本稿ではあえて重要視しません。ただ、テーブルの数を減らすことはNoSQLにおいては必然の習慣であるのは確かです。

行に関数従属した任意の項目群を、絞り込みまたはソートのキーにする

特に、この絞り込みが範囲検索である場合に、以下に述べるような工夫が必要になってきます。

例えばブログシステムの記事テーブルに、公開開始日時と公開終了日時があり、これらの値を使った記事一覧取得処理について考える場合、以下の手順による準備が必要になります。

  1. 行の中から、絞り込みまたはソートのキーに使いたいkey-valueペア群を切り出す。f:id:nowavailable:20210108232839p:plain
  2. テーブルは、複合主キー型のテーブル、つまりソートキーを持つタイプのテーブルとする。ソートキー列は文字列型で列名は、例えば "indefinite" とする。つまりどんな値が入ってくるか不定な列である。f:id:nowavailable:20210108233409p:plain

  3. 切り出した部品に型としての名前をつける(例えば "for_search")。この名前をindefinite列の値とし、pertition-keyの値はオリジナル行と同一としたうえで、切り出したkey-valueペア群を派生行としてテーブルに挿入する。f:id:nowavailable:20210108234054p:plain

  4. 派生行の型の中から、任意の項をHASHキーまたはRANGEキーにしたGlobal Secondary Index(以下GSI)を構成しておく。RANGEキーにするなら、それと組になるHASHキーにはindefinite列を指定するのが妥当だろう。f:id:nowavailable:20210108234714p:plainf:id:nowavailable:20210108234725p:plain

この設定の結果、記事テーブル上の二つのインデックスによって、

  • 公開開始日時が現在日時より小さく
  • 公開終了日時が現在日時より大きく
  • indefinite列の値が"for_search"である

という条件での絞り込みが可能になりました。絞り込んだ結果には少なくともpertition-key、つまりエンティティの主キーの値が含まれるようになります。 それ以上の属性値、例えばタイトルを含ませたいのであれば、オリジナル行から

  { title: "1月10日の日記" } 

の項も、"for_search" 行にコピーしておく必要があります。(その場合当然、コピーしたkey-valueペア群の更新整合性は失われるわけですが)

ManyToManyな関係を交差テーブルを使わずに表現する

お馴染みの、FilmとActorの関係で考えましょう。 f:id:nowavailable:20210108223839p:plain

このFilmテーブルの側に視点を置きます。

これをNoSQLの流儀で表現するには以下の手順を必要とします。

  1. Actorテーブルの主キーFilmテーブルの主キー / Actorテーブルの型名 の3つの要素を含むkey-valueペア群を、同一pertition-key値に紐付く派生行としてテーブルに挿入する。テーブルの構成は上記の例と同様、複合主キー型のテーブルとする。派生行のindefinite列の値には、Actorの主キー値またはそれを代理する値を充てておくf:id:nowavailable:20210109013159p:plain
  2. Actorテーブルの主キーをHASHキーにしたGSIを設定する。これによって、Filmテーブル上で、Actorの任意の主キー値から、関係するFilmの主キー値群が取得できるようになる。

    f:id:nowavailable:20210109015217p:plain

    f:id:nowavailable:20210109015519p:plain

  3. 派生行内のFilmテーブルの主キーと、Actorテーブルの型名、の組からなるGSIを構成する。これによって、Filmテーブル上で、Filmの任意の主キー値から、関係するActorの主キー値群が取得できるようになる。(なお、扱うOneToManyが一種しか無いと分かっている状況下であれば、"Actorテーブルの型名" は不要)

    f:id:nowavailable:20210109015311p:plain

    f:id:nowavailable:20210109015538p:plain

まとめ

今回紹介した手法を一般化して言うと、以下のようになるでしょう。

  • DynamoDBの基本部品である、“2項から成る複合主キー”のうちの一方に単独主キーの値を、もう一方に任意の固定値を与えることで、ひとつの主キー配下に意味の異なる派生行を構成できる。その用途は例えば検索やソートのためのキーの設定など。この派生行はいわば、OneToOneの関係を持つテーブル同士をひとつのテーブル上で表現している状態である。
  • DynamoDBの基本部品である、“2項から成る複合主キー”のうちの一方に単独主キーの値を、もう一方にManyToManyの関係先の主キー値またはそれを代理する値を配することで、関係そのものを表現が出来る(つまり交差テーブルに相当するもの)。これはOneToManyやManyToManyの関係を持つテーブル同士をひとつのテーブル上で表現しようとしている状態といえる。

ひとつの主キー値配下に、用途別に複数の行を持たせるというやり方は、リレーショナルモデルに慣れた脳にはとんでもない無法に見えますね。行の概念を完全に覆しているので。つまりNoSQLにおけるテーブルとは、実はテーブルではなくて別のものであるということが、このことから分かるわけです。
NoSQLでは、ひとつのテーブルに型の異なるタプルを同居させられる。ゆえに、

  1. ある行の型を基にしたインデックスがすべての行にわたって作用するとは限らない。
  2. しかしテーブルの(複合)主キーは存在している。これはすべての行にわたって作用するインデックスとなる。

この2つの特徴を踏まえることで、テーブルの数を圧縮しつつ、関係を表現し、且つインデックスを自在に構成することが出来ました。

思い返せば、初期のGoogle AppEngineを扱ったときも、NoSQL上でいろんな工夫をして問題を解決しました。その記録を辿るのも困難なくらい時間が経ってしまい、工夫の数々は事実上、無に帰したわけです。その反省もこめて書きました。