Featured image of post RDB Tree Structure

RDB Tree Structure

組織の部署構造、投稿へのリプライ、ファイルシステム、分類 …etc

現実世界をモデリングするときに、ツリー構造がよく出現する。

graph TD
    Animals --> Mammals
    Animals --> Birds
    Mammals --> Cat
    Mammals --> Dog
    Birds --> Eagle
    Birds --> Sparrow

ツリー構造のデータをRDBで扱う方法はいくつかある。

  • 隣接リスト
  • 経路列挙
  • 入れ子集合
  • 閉包テーブル

それぞれの特徴とトレードオフについて考えてゆく。

ツリー構造の実装手法

隣接リスト

恐らく真っ先に思い浮かぶのがこの方式。

各ノードが親ノードを参照する列を持ち、隣接するノードのリストとして表現される。

id name parent_id
1 Animals NULL
2 Mammals 1
3 Birds 1
4 Cat 2
5 Dog 2
6 Eagle 3
7 Sparrow 3
  • メリット:
    • ノードの追加や更新が容易
    • ツリー内のノードの移動が効率的に行える
  • デメリット:
    • 特定の階層の親ノードや子ノードを見つけるために再帰的なクエリが必要となる(アンチパターン:ナイーブツリー)
    • 末端以外のノードの削除処理が複雑になる

隣接リストにおいては「構成のシンプルさ」と「検索と削除の複雑さ」がトレードオフとなる。

経路列挙

これも比較的直観的な方式。

各ノードがパス(経路)を持ち、それによって親子関係を表現する。

id name path
1 Animals /
2 Mammals /1/
3 Birds /1/
4 Cat /1/2/
5 Dog /1/2/
6 Eagle /1/3/
7 Sparrow /1/3/
  • メリット:
    • ルートからのパス探索は前方一致や完全一致の検索となるためIndexが利用でき、特定のノードやその子孫を高速に見つけることができる
    • 生のテーブルデータの可読性が高い
  • デメリット:
    • ツリーが深くなるとパスも長くなるため、経路が長くなるとDBのカラム制約によっては保存できなくなる場合がある
    • ルート以外からのパス探索は中間一致の検索となるためIndexが利用できず、性能が低下する場合がある
    • 末端以外のノードの変更が必要な場合に処理が複雑になる
    • Foreign Keyが使えないため、親子関係の整合性が担保できない(アンチパターン:ジェイウォーク)

経路列挙においては「構成のシンプルさ・可読性」と「変更の複雑さ・検索パフォーマンス(状況による)・データ整合性」がトレードオフとなる。

入れ子集合

ツリーではなく集合としてモデリングする方式。

区間の左端・右端それぞれのidを保持し、区間を表現する。

id name left right
1 Animals 1 14
2 Mammals 2 7
3 Birds 8 13
4 Cat 3 4
5 Dog 5 6
6 Eagle 9 10
7 Sparrow 11 12
  • メリット:
    • 階層的なデータを自然に表現できるため、データ取得が容易
    • 同一階層内での順序の管理ができる
  • デメリット:
    • データの追加や更新を行う際に、複数のレコードの更新が必要な場合がある
      • 例:CatとDogの間にPigを追加する場合、Mammalsの右端以降の座標が2ずつずれる
    • Foreign Keyを使わないため、異常なデータを制約で阻止できない

入れ子集合においては「検索の容易さ」と「更新の複雑さ・データ整合性」がトレードオフとなる。

閉包テーブル

親子関係を別テーブルで持つ方式。

id name
1 Animals
2 Mammals
3 Birds
4 Cat
5 Dog
6 Eagle
7 Sparrow
ancestor descendant
1 1
1 2
1 3
2 2
2 4
2 5
3 3
3 6
3 7
4 4
5 5
6 6
7 7

親子がどちらも自分自身であるようなデータも保持するのがポイント。

  • メリット:
    • シンプルなクエリで先祖・子孫・兄弟の取得ができる
    • 階層の深さに実質制限がない
  • デメリット:
    • テーブルサイズが大きくなる(閉包テーブルの行数はツリー深度の2乗に比例)
    • ノードの追加・変更・削除時に更新すべきレコードが多くなる

閉包テーブルにおいては「検索の容易さ」と「テーブルサイズ・更新レコード数」がトレードオフとなる。

使い分け

トレードオフを意識し、状況に応じて選択すればOK。

一般的に隣接リストや経路列挙はアンチパターンとされているが、要件によっては選択できる場合がある。

例えば

  • 直近の親子関係のみ取得できればよい場合・再帰クエリが利用できる場合は、隣接リストが採用候補となる
  • 柔軟なクエリを実行する必要がなく、ノードの更新が行われない場合は、経路列挙が採用候補となる

アンチパターンとされていない入れ子集合や閉包テーブルを使う場合も、デメリットが許容できるのか検討する必要がある。

例えば

  • ツリーの更新頻度が高く、データの整合性が重要である場合は、入れ子集合は採用しないほうがよいかもしれない
  • 極めて巨大なツリーを作成する想定がある場合は、閉包テーブルは採用しないほうがよいかもしれない

いずれも銀の弾丸ではないため、要件と照らし合わせて意思決定するほかない。


参考: