経緯

DBスペシャリストを受験してみようと考え、DB設計本やらSQLアンチパターン本やらを読んでみたのですが、そもそも実システムのDBがどうなっているのかを知らないので無理やりにでも触れる機会を作ってみようと思った次第です。

今回は学習のネタとして、TwitterライクなSNSとして流行した(と信じている)マストドンを考えていきます。

ネタとしてマストドンを使う理由ですが、「マストドンがSNSとして他よりも優れているから」とか、そういったわけではありません。OSSなのでしっかりしたドキュメントが手に入るし、他の人による解説があるという点で学習ネタとして便利だからでございます。正直、答え合わせをしないのであれば「Twitterのテーブル構成予想図」で良かったかもしれません。

注意書き

この記事は自己学習用であり、実際にマストドンのDBのテーブル構成をお伝えすることを目的とした記事ではありません。マストドンの仕様や、DBのテーブル構成を必要とする方は次の情報を参考にしてください。

今回の学習

いきなりマストドン全体を書き起こしてテーブル構成考えてみるのは辛そうなので、一旦簡易的にします。最低限これだけあればTwitterにはなれなくても簡単なブログくらいにはなれるでしょうという要素です。

  • アカウント
  • ステータス (Twitterでいうツイート)
  • リプライ

各要素の詳細

アカウント・ステータス・リプライ(機能)をそれぞれ以下のように定義します:

アカウント

「トゥート」を行う実体です。インスタンス(ドメイン)に一意のIDによって特定されます。

アカウントはID、ユーザーネーム、ドメイン、ディスプレイネーム、アイコン画像、ヘッダー画像、ノート(自己紹介用)、登録日とを持ちます。

ステータス

投稿する文章(トゥート)などが格納されます。現時点では画像・音楽ファイルなどの添付は考えず、ただ500文字以内の文章と投稿日などの情報が格納されます。(なんでstatusという言葉が使われているのでしょう?)

リプライ

ある1つのトゥートに対してトゥートを行う機能です。

ER図

ER図を書くというだけなら数も少ないのであまり考えることはありません。
アカウントとステータスの関係は1対多であり、1つのステータスは複数のステータスの親になり得ます。

以下、簡単に書いてみたものになります。

MastodonのDBのテーブル構成を見てみると特にstatusesが私の書いたものと大きく異なり、 in_reply_to_account_id および reply というカラムがありました。

データ型を見てみますと、 reply はboolean型であり、つまり返信である/ないが記載されているということになります。

設計の意図を読み取りたい

設計本、アンチパターン本を読んだ私が教科書どおりに考えるのなら次の3点は「どうしてこうなっているんだろうか?」と考える必要のありそうなところです。

  • ID required (とりあえずID)
  • ナイーブツリー(素朴な木)
  • 正規化と非正規化

ID required

最初に思うのはaccountsの方にIDが必要かどうかです。実はマストドンにおいて usernamedomainは一度決定したあとは変更不可能なものであり、かつ、この2つの組はグローバルに一意です。当然 null もあり得ません。

これならusernamedomain とで複合主キーにしてしまっても良いように思います。

あえてこれらを複合主キーとすることが躊躇われる理由をあげるとすれば、これらのカラムのデータ型が普通永続的ではないデータが入りがちな VARCHARであるからというところになるのでしょうか?

正直その方がORMが使いやすいから...というところもありそうです。

ナイーブツリー

あるリプライをクリックしたときには当然、その親および子が表示されることが望まれます。この機能のおかげで、そもそもどのトゥートがやり取りの起点になったのか、そしてあるやり取りは最終的にどのように終わったのかを見返すことができます。

「リレーショナル・データベースで木構造をどのように扱うか」というのはまあよく議論されてきた問題であり、以下のような様々な解決策があげられています:

  • 隣接リストモデル
  • 入れ子集合モデル
  • 経路列挙モデル

興味があるのはなぜマストドンにおいて「隣接リストモデル」が用いられているかです。

思うに、マストドンがトゥートがいくつもされる(つまりレコードの挿入操作が何度も行われる)ようなアプリケーションであるからだと考えます。

入れ子集合モデルにおいては、子供が用意していおいた2つの整数値の間に収まることができなくなれば周辺のレコードの値も変更しなければならず負担が大きいですし、経路列挙のアプローチをとるとなると、どれだけの長さの文字列が格納されることになるかわかったものではありません。

素早く親・子トゥートの取得をすることを考えると入れ子集合モデルを選びたいところですが、レコードが続々と増えていくというアプリケーションの特性を考えると隣接リストモデルを使うしかないのかなと思います。

正規化と非正規化

statusesについて、主キーが1つですから部分従属ということはありえず、第二正規化の条件は満たされています。一方で in_reply_to_account_id というカラムは in_reply_to_id がわかれば導出できるもの(推移的関数従属)ですから、第三正規化の条件は満たされていないということになるでしょう。

じゃあなんで敢えてこうなっているのかと考えると、そういえばマストドンには通知機能があります。(あなたに対してメンションがありましたよ~的なやつです。)

何らかのstatus_idと紐付いた何らかのリプライがやってきたとき、わざわざそのstatus_idと紐付いたaccount_idが何であるかということを調べることなしに通知だけは飛ばしたい。そういうことなのかなと思いました。

読んだ設計本には「とりあえず正規化する。非正規化以外の方法でパフォーマンス改善が出来ない場合に非正規化を考える」といったことが書かれていたので、もう少しここが非正規化されている理由を深堀りしていきたいところではあります。

おわりに

意外と自分が使ったことがあるサービスをネタにあれこれ考えてみることが楽しかったのでまたやってみたいと思います。

おつかれさまでした!