MySQLで階層化されたデータを扱う(再帰的に検索しないで済む方法)

Oracleの場合は、Start With〜Connect By Prior〜というSQLで階層化されたデータを取り扱うことができます。では、MySQLではどうか?下記のサイトでしっかり解説してありました。

Managing Hierarchical Data in MySQL
階層化されたデータをMySQLで扱う(上記の日本語訳)

要約すると、

  • 階層化データを扱うには、Adjacency List モデルとネストセットモデルの2つがある
  • Adjacency List モデルは直感的でわかりやすい。更新も簡単。
  • しかし、Adjacency List モデルで表されたデータを検索するのはやや面倒(MySQLでは。)
  • 一方、ネストセットモデルで表されたデータは検索しやすい
  • しかし、ネストセットモデルは更新が面倒

といったところです。どちらの方法にもそれぞれメリット・デメリットがありますが、Adjacency List モデルには、階層が深くなる(または深さの制限がない)と階層データとして取得することはできない、という致命的な問題があるようです。
今回、僕がやりたかったのは「メールのスレッド表示データ」の実現だったので、階層が深くなることが十分にありえます。なので、必然的にネストセットモデルを選ぶことにしました。


ネストセットモデルの問題
ネストセットモデルでは親子関係を表すために、左右の番号を使用します。が、その管理が非常に面倒です。上記サイトで紹介されているやり方では、末端にノードが追加されると、それだけで全体の番号の振り直しが必要になる場合があります。これでは、「メールのスレッド表示」をする上で問題です。という訳で、自分なりに解決策を考えました。

  • 左右の番号を連番にせずに、あらかじめ大きな値を設定しておく

上記サイトでは、左右の番号(lft,rgt)を[1,20][2,9][10,19]、、としていますが、これは必ずしも連番でなくても良さそうなので[1,10000][2,2499][2500,4999]、、というように番号を振ります。これは、右の値として予め大きな値を設定しておくことで、後でノードが追加になったときに他のノードに影響がでないようにする、という狙いです。

番号の振り方についてもう少し補足します。例えば、以下のようなデータがあったとします。


[乗り物]
+-- [車]
+-- [セダン]
+-- [バス]
+-- [トラック]
+-- [電車]
+-- [在来線]
+-- [新幹線]
ここで、左右の値として使用できる番号の範囲を 0〜10001 とします。とすると当然、[乗り物]=[0,10001]になります。次に[乗り物]の子(車,電車)ですが、[車]=[1,5000]、[電車]=[5001,10000]としてしまうと、次に[乗り物]の子が追加になったときに番号を振りなおさなければなりません。なので、あらかじめ1つ分だけ余裕を残しておき [車]=[1,3333]、[電車]=[3334,6666]とします(※66667〜10000が余裕分)。[車]の子についても同様に[セダン]=[2,832]、[バス]=[833,1665]、[トラック]=[1666,2498] とします(※2499〜3332が余裕分)。

このように番号を振ることで、例えば[セダン]の下に子ノードが追加になったときにも、他のノードには一切影響を与えることなく子ノードのみを追加することができます。これでネストセットモデルの番号の管理が大分楽になりました。一方で、以下の点には注意が必要です。

  • 番号の余裕分は、ツリーを初めに作った段階とき割り当てるため、後から追加していくと番号が足りなくなる可能性がある
例えば上記の例でいくと、後から[乗り物]の子ノード[二輪車]が追加になった場合を想定してみます。この場合、余裕分としてみていたのは6667〜10000なので、[二輪車]=[6667,8333]となります(8334〜10000が余裕分)。ここで、さらに後から[乗り物]の子ノード[飛行機]が追加になった場合を考えると、余裕分は8334〜10000しか残ってないので、[飛行機]=[8334,9167]となります(9168〜10000が余裕分)。[車]=[1,3333]と比べると、[飛行機]はずいぶんと値の幅が小さくなってしまっているのが分かります(1/4になっています)。すなわち、[飛行機]の下にぶら下がることのできる子ノードの数は、[車]と比べて 1/4 となってしまいます。

以上を踏まえた上で、左右の値として使用できる番号の範囲の検討が必要です。例えば、0〜1024とした場合は、1024=2^10 なので、子ノードは最大でも10階層しかもつことができません。今回の「メールのスレッド表示」では、階層が最大12、階層あたりの子ノードが最大6と見積もりましたので、値の範囲としては 0〜16777215(=2^24)としました。

以上、「MySQLで階層化されたデータを扱う」方法についての考察でしたが、Oracleの「Start With〜Connect By Prior〜」の便利さがよくわかりました。Adjacency List モデルなら何も考えずにリンクだけつないどけばいいですからね〜。