RDBでのツリー表現⼊⾨
lagénorhynque
(defprofile lagénorhynque
:id @lagenorhynque
:reading "/laʒenɔʁɛ̃k/"
:aliases ["カマイルカ🐬 "]
:languages [Clojure Haskell English français]
:interests [programming language-learning law mathematics]
:commits ["github.com/lagenorhynque/duct.module.pedestal"
"github.com/lagenorhynque/duct.module.cambium"]
:contributes ["github.com/japan-clojurians/clojure-site-ja"])
0. 事前準備
1. 隣接リスト(adjacency list)
2. 経路列挙(path enumeration)
3. ⼊れ⼦集合(nested sets)
4. 閉包テーブル(closure table)
事前準備
サンプルコード:
ローカルDB:
tree-representations-in-rdb
docker-compose.yml
mariadb:10.5.5
ツリーの本体データ⽤テーブルdepartment
CREATE TABLE `department` (
`department_id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(100) NOT NULL,
PRIMARY KEY (`department_id`)
);
department の初期データ
表現したいツリー
隣接リスト(adjacency list)
「隣接リスト」モデル
ツリー管理⽤のテーブル
parent_id: 親ノード
本体データ⽤テーブルに含めても良い
CREATE TABLE `department_adjacency_list` (
`department_id` int(10) unsigned NOT NULL,
`parent_id` int(10) unsigned DEFAULT NULL,
PRIMARY KEY (`department_id`),
FOREIGN KEY (`department_id`)
REFERENCES `department` (`department_id`)
ON DELETE CASCADE ON UPDATE RESTRICT,
FOREIGN KEY (`parent_id`)
REFERENCES `department` (`department_id`)
ON DELETE CASCADE ON UPDATE RESTRICT
);
department_adjacency_list の初期データ
すべてのノードとその階層情報の取得
distance: ルートノードからの距離
cf.
WITH RECURSIVE department_tree AS (
SELECT d.*, dal.parent_id, 0 distance
FROM department d
JOIN department_adjacency_list dal USING (department_id)
WHERE dal.parent_id IS NULL
UNION ALL
SELECT d.*, dal.parent_id, dt.distance + 1
FROM department d
JOIN department_adjacency_list dal USING (department_id)
JOIN department_tree dt ON dal.parent_id = dt.department_id
)
SELECT *
FROM department_tree;
WITH - MariaDB Knowledge Base
特定のノードからの⼦孫ノードの取得
distance: ノード10 からの距離
WITH RECURSIVE department_tree AS (
SELECT d.*, dal.parent_id, 0 distance
FROM department d
JOIN department_adjacency_list dal USING (department_id)
WHERE d.department_id = 10
UNION ALL
SELECT d.*, dal.parent_id, dt.distance + 1
FROM department d
JOIN department_adjacency_list dal USING (department_id)
JOIN department_tree dt ON dal.parent_id = dt.department_id
)
SELECT *
FROM department_tree;
特定のノードの⼦ノードの取得
SELECT d.*
FROM department d
JOIN department_adjacency_list dal USING (department_id)
WHERE dal.parent_id = 10;
特定のノードの親ノードの取得
SELECT d.*
FROM department d
JOIN department_adjacency_list dal
ON d.department_id = dal.parent_id
WHERE dal.department_id = 10;
PROS
モデルとして単純(良くも悪くもナイーブ)
直近の親/⼦ノードに対するアクセスが容易
挿⼊操作が容易
参照整合性が保証できる
CONS
再帰CTE (recursive common table expressions)
が使えないと任意階層に対するクエリが困難
削除操作が煩雑
parent_id でNULL が現れてしまう
ルートノード管理⽤のテーブルを⽤意するこ
とで回避可能
経路列挙(path enumeration)
a.k.a. 経路実体化(materialized path)
「経路列挙」モデル
ツリー管理⽤のテーブル
path: 先祖からそのノードまでの経路(パス)
本体データ⽤テーブルに含めても良い
CREATE TABLE `department_path_enumeration` (
`department_id` int(10) unsigned NOT NULL,
`path` varchar(1000) NOT NULL,
PRIMARY KEY (`department_id`),
FOREIGN KEY (`department_id`)
REFERENCES `department` (`department_id`)
ON DELETE CASCADE ON UPDATE RESTRICT
);
department_path_enumeration の初期データ
すべてのノードとその階層情報の取得
depth: ルートノードからの距離
SELECT d.*, dpe.path,
CHAR_LENGTH(dpe.path) - CHAR_LENGTH(REPLACE(dpe.path, '/', ''))
- 1 depth
FROM department d
JOIN department_path_enumeration dpe USING (department_id);
特定のノードからの⼦孫ノードの取得
depth: ルートノードからの距離
SELECT d.*, dpe.path,
CHAR_LENGTH(dpe.path) - CHAR_LENGTH(REPLACE(dpe.path, '/', ''))
- 1 depth
FROM department d
JOIN department_path_enumeration dpe USING (department_id)
WHERE dpe.path LIKE CONCAT((
SELECT path
FROM department_path_enumeration
WHERE department_id = 10
), '%');
特定のノードの⼦ノードの取得
SELECT d.*
FROM department d
JOIN department_path_enumeration dpe USING (department_id)
WHERE dpe.path REGEXP CONCAT((
SELECT path
FROM department_path_enumeration
WHERE department_id = 10
), 'd+/$');
特定のノードの親ノードの取得
SELECT d.*
FROM department d
WHERE d.department_id = (
SELECT CASE WHEN path = CONCAT(department_id, '/') THEN
NULL
ELSE
REGEXP_REPLACE(path, '^(?:d+/)*(d+)/d+/$', '1')
END
FROM department_path_enumeration
WHERE department_id = 10
);
PROS
⼦孫/先祖ノードに対するアクセスが容易
更新系操作が容易
CONS
パス⽂字列をアプリケーションコードで管理しな
ければならない
正規化されていない(第1正規形でさえない)
参照整合性が保証できない
⼊れ⼦集合(nested sets)
cf. ⼊れ⼦区間(nested intervals)
「⼊れ⼦集合」モデル
ツリー管理⽤のテーブル
left, right: 数直線上の左右の点を表す
depth: ルートノードからの距離(必須ではない)
本体データ⽤テーブルに含めても良い
CREATE TABLE `department_nested_sets` (
`department_id` int(10) unsigned NOT NULL,
`left` int(11) NOT NULL,
`right` int(11) NOT NULL,
`depth` int(10) unsigned NOT NULL,
PRIMARY KEY (`department_id`),
FOREIGN KEY (`department_id`)
REFERENCES `department` (`department_id`)
ON DELETE CASCADE ON UPDATE RESTRICT
);
department_nested_sets の初期データ
すべてのノードとその階層情報の取得
SELECT d.*, dns.left, dns.right, dns.depth
FROM department d
JOIN department_nested_sets dns USING (department_id);
特定のノードからの⼦孫ノードの取得
SELECT d.*, dns.left, dns.right, dns.depth
FROM department d
JOIN department_nested_sets dns USING (department_id)
JOIN department_nested_sets dns2 ON dns.left BETWEEN dns2.left
AND dns2.right
WHERE dns2.department_id = 10;
特定のノードの⼦ノードの取得
SELECT d.*
FROM department d
JOIN department_nested_sets dns USING (department_id)
JOIN department_nested_sets dns2 ON dns.left BETWEEN dns2.left
AND dns2.right
WHERE dns2.department_id = 10
AND dns.depth = (
SELECT depth
FROM department_nested_sets
WHERE department_id = 10
) + 1;
特定のノードの親ノードの取得
SELECT d.*
FROM department d
JOIN department_nested_sets dns USING (department_id)
JOIN department_nested_sets dns2 ON dns2.left BETWEEN dns.left
AND dns.right
WHERE dns2.department_id = 10
AND dns.depth + 1 = (
SELECT depth
FROM department_nested_sets
WHERE department_id = 10
);
PROS
⼦孫/先祖ノードに対するアクセスが容易
削除操作が容易
CONS
左右の点の値をアプリケーションコードで管理し
なければならない
正規化されていない(第1正規形でさえない)
挿⼊操作が⾮常に煩雑でコストも⾼い
「⼊れ⼦区間」では改善される
参照整合性が保証できない
閉包テーブル(closure table)
「閉包テーブル」モデル
ツリー管理⽤のテーブル
ancestor, descendant: 先祖/⼦孫関係にある
ノードの組
path_length: ancestor からdescendant ま
での距離(必須ではない)
CREATE TABLE `department_closure_table` (
`ancestor` int(10) unsigned NOT NULL,
`descendant` int(10) unsigned NOT NULL,
`path_length` int(10) unsigned NOT NULL,
PRIMARY KEY (`ancestor`, `descendant`),
FOREIGN KEY (`ancestor`)
REFERENCES `department` (`department_id`)
ON DELETE CASCADE ON UPDATE RESTRICT,
FOREIGN KEY (`descendant`)
REFERENCES `department` (`department_id`)
ON DELETE CASCADE ON UPDATE RESTRICT
);
department_closure_table の初期データ
すべてのノードとその階層情報の取得
depth: ルートノードからの距離
MAX(dct.path_length) でも求められる
SELECT d.*, COUNT(*) - 1 depth
FROM department d
JOIN department_closure_table dct
ON d.department_id = dct.descendant
GROUP BY dct.descendant;
特定のノードからの⼦孫ノードの取得
distance: ノード10 からの距離
depth: ルートノードからの距離
SELECT d.*, dct.path_length distance, dct2.depth
FROM department d
JOIN department_closure_table dct
ON d.department_id = dct.descendant
JOIN (
SELECT descendant, COUNT(*) - 1 depth
FROM department_closure_table
GROUP BY descendant
) dct2
ON d.department_id = dct2.descendant
WHERE dct.ancestor = 10;
特定のノードの⼦ノードの取得
SELECT d.*
FROM department d
JOIN department_closure_table dct
ON d.department_id = dct.descendant
WHERE dct.ancestor = 10
AND dct.path_length = 1;
特定のノードの親ノードの取得
SELECT d.*
FROM department d
JOIN department_closure_table dct
ON d.department_id = dct.ancestor
WHERE dct.descendant = 10
AND dct.path_length = 1;
PROS
⼦孫/先祖ノードに対するアクセスが容易
更新系操作が容易
参照整合性が保証できる
CONS
他のモデルよりも保持すべきデータが多くなる
まとめ
「閉包テーブル」は総合的に優れている
「隣接リスト」は最も素朴(ナイーブ)な設計だが
デメリットもいくつかある
「経路列挙」「⼊れ⼦集合」は参照整合性が保証
できない(RDBの強みが犠牲になる)
Further Reading
2章 ナイーブツリー(素朴な⽊)
第10章 グラフに⽴ち向かう
10.4 ツリー(⽊)
第10章 階層的なデータ構造
『SQLアンチパターン』
『理論から学ぶデータベース実践⼊⾨』
『E ective SQL』
第9章 ⼀歩進んだ論理設計〜SQLで⽊構造
を扱う
『達⼈に学ぶDB設計徹底指南書』
『プログラマのためのSQLグラフ原論』
What are the options for storing hierarchical
data in a relational database? - Stack Over ow

RDBでのツリー表現入門