MySQL 8.0 が再帰クエリをサポートするようになったので、一般的な SQL データベースはすべて、標準構文の再帰クエリをサポートしていると言えます。
WITH RECURSIVE MyTree AS (
SELECT * FROM MyTable WHERE ParentId IS NULL
UNION ALL
SELECT m.* FROM MyTABLE AS m JOIN MyTree AS t ON m.ParentId = t.Id
)
SELECT * FROM MyTree;
2017 年のプレゼンテーションRecursive Query Throwdownで、MySQL 8.0 の再帰クエリをテストしました。
以下は、2008年からの私の最初の回答です。
ツリー構造のデータをリレーショナル データベースに格納するには、いくつかの方法があります。例で示しているのは、次の 2 つの方法を使用しています。
- 隣接リスト(「親」列) および
- パスの列挙(名前列のドット付き数字)。
もう 1 つのソリューションはNested Setsと呼ばれ、同じテーブルに格納することもできます。これらの設計の詳細については、Joe Celko による「Trees and Hierarchies in SQL for Smarties 」を参照してください。
私は通常、ツリー構造のデータを格納するためのClosure Table (別名 "Adjacency Relation") と呼ばれる設計を好みます。別のテーブルが必要ですが、ツリーのクエリは非常に簡単です。
Closure Table については、私のプレゼンテーションModels for Hierarchical Data with SQL and PHPと私の著書SQL Antipatterns: Avoiding the Pitfalls of Database Programming で説明しています。
CREATE TABLE ClosureTable (
ancestor_id INT NOT NULL REFERENCES FlatTable(id),
descendant_id INT NOT NULL REFERENCES FlatTable(id),
PRIMARY KEY (ancestor_id, descendant_id)
);
あるノードから別のノードへの直接の祖先があるクロージャー テーブルにすべてのパスを格納します。各ノードがそれ自体を参照する行を含めます。たとえば、質問で示したデータセットを使用すると、次のようになります。
INSERT INTO ClosureTable (ancestor_id, descendant_id) VALUES
(1,1), (1,2), (1,4), (1,6),
(2,2), (2,4),
(3,3), (3,5),
(4,4),
(5,5),
(6,6);
これで、次のようにノード 1 から始まるツリーを取得できます。
SELECT f.*
FROM FlatTable f
JOIN ClosureTable a ON (f.id = a.descendant_id)
WHERE a.ancestor_id = 1;
(MySQL クライアントでの) 出力は次のようになります。
+----+
| id |
+----+
| 1 |
| 2 |
| 4 |
| 6 |
+----+
つまり、ノード 3 と 5 は除外されます。これは、ノード 1 の子孫ではなく、別の階層の一部であるためです。
Re: 直接の子供 (または直接の親) に関する e-satis からのコメント。path_length
" " 列を に追加しClosureTable
て、直接の子または親 (またはその他の距離) を具体的に照会しやすくすることができます。
INSERT INTO ClosureTable (ancestor_id, descendant_id, path_length) VALUES
(1,1,0), (1,2,1), (1,4,2), (1,6,1),
(2,2,0), (2,4,1),
(3,3,0), (3,5,1),
(4,4,0),
(5,5,0),
(6,6,0);
次に、特定のノードの直接の子を照会するための検索に用語を追加できます。これらは 1 の子孫ですpath_length
。
SELECT f.*
FROM FlatTable f
JOIN ClosureTable a ON (f.id = a.descendant_id)
WHERE a.ancestor_id = 1
AND path_length = 1;
+----+
| id |
+----+
| 2 |
| 6 |
+----+
@ashraf からの再コメント: 「ツリー全体を [名前で] 並べ替えてみませんか?」
ノード 1 の子孫であるすべてのノードを返し、それらを などの他のノード属性を含む FlatTable に結合しname
、名前で並べ替えるクエリの例を次に示します。
SELECT f.name
FROM FlatTable f
JOIN ClosureTable a ON (f.id = a.descendant_id)
WHERE a.ancestor_id = 1
ORDER BY f.name;
@Nate からの再コメント:
SELECT f.name, GROUP_CONCAT(b.ancestor_id order by b.path_length desc) AS breadcrumbs
FROM FlatTable f
JOIN ClosureTable a ON (f.id = a.descendant_id)
JOIN ClosureTable b ON (b.descendant_id = a.descendant_id)
WHERE a.ancestor_id = 1
GROUP BY a.descendant_id
ORDER BY f.name
+------------+-------------+
| name | breadcrumbs |
+------------+-------------+
| Node 1 | 1 |
| Node 1.1 | 1,2 |
| Node 1.1.1 | 1,2,4 |
| Node 1.2 | 1,6 |
+------------+-------------+
今日、ユーザーが編集を提案しました。SO モデレーターは編集を承認しましたが、私はそれを取り消します。
ORDER BY b.path_length, f.name
編集では、おそらく順序が階層と一致することを確認するために、上記の最後のクエリの ORDER BY を にする必要があることが示唆されました。しかし、「Node 1.2」の後に「Node 1.1.1」を注文するため、これは機能しません。
賢明な方法で順序付けを階層と一致させたい場合は可能ですが、単にパスの長さで順序付けするだけではありません。たとえば、MySQL Closure Table hierarchy database - How to pull information out in the correct orderに対する私の回答を参照してください。