550

順序付けられたツリー階層を格納するフラット テーブルがあるとします。

Id   Name         ParentId   Order
 1   'Node 1'            0      10
 2   'Node 1.1'          1      10
 3   'Node 2'            0      20
 4   'Node 1.1.1'        2      10
 5   'Node 2.1'          3      10
 6   'Node 1.2'          1      20

ここに がある図があります[id] Name。ルート ノード 0 は架空のものです。

                       [0] ルート
                          / \
              [1] ノード 1 [3] ノード 2
              / \ \
    [2] ノード 1.1 [6] ノード 1.2 [5] ノード 2.1
          /          
 [4] ノード 1.1.1

それを正しい順序で正しくインデントされたツリーとして HTML (またはテキスト) に出力するには、どのような最小限のアプローチを使用しますか?

さらに、基本的なデータ構造 (配列とハッシュマップ) しかなく、親/子の参照を持つ派手なオブジェクトはなく、ORM もフレームワークもなく、両手だけしかないと仮定します。テーブルは結果セットとして表され、ランダムにアクセスできます。

疑似コードまたは平易な英語で問題ありません。これは純粋に概念上の問題です。

おまけの質問: このようなツリー構造を RDBMS に格納する根本的に優れた方法はありますか?


編集と追加

あるコメント投稿者 ( Mark Besseyさん) の質問に答えるには: ルート ノードは必要ありません。とにかく表示されることはないからです。ParentId = 0 は、「これらがトップ レベルであること」を表すための規則です。Order 列は、同じ親を持つノードがどのようにソートされるかを定義します。

私が話した「結果セット」は、ハッシュマップの配列として描くことができます (その用語にとどまります)。私の例では、すでにそこにあるはずでした。いくつかの答えは、さらに一歩進んで最初に構築しますが、それは問題ありません。

ツリーの深さは任意です。各ノードは N 個の子を持つことができます。ただし、「何百万ものエントリ」ツリーを念頭に置いているわけではありません。

私が選んだノード名 ('Node 1.1.1') を信頼できるものと間違えないでください。ノードは、'Frank' または 'Bob' と同じように呼ぶことができます。命名構造は暗示されていません。これは単に読みやすくするためです。

独自のソリューションを投稿したので、皆さんはそれをバラバラにすることができます。

4

14 に答える 14

482

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に対する私の回答を参照してください。

于 2008-10-10T17:58:07.263 に答える
62

ネストされたセット (Modified Pre-order Tree Traversal と呼ばれることもあります) を使用すると、単一のクエリでツリー構造全体またはその中のサブツリーをツリー順序で抽出できますが、必要に応じて挿入のコストが高くなります。ツリー構造を通る順番のパスを記述する列を管理します。

django-mptt の場合、次のような構造を使用しました。

id parent_id tree_id level lft rght
-- ---------- ------- ----- --- ----
 1ヌル 1 0 1 14
 2 1 1 1 2 7
 3 2 1 2 3 4
 4 2 1 2 5 6
 5 1 1 1 8 13
 6 5 1 2 9 10
 7 5 1 2 11 12

これは、次のようなツリーをid表します (各項目を表す):

1
 +-- 2
 | | +-- 3
 | | +-- 4
 | |
 +-- 5
     +-- 6
     +-- 7

lftまたは、との値がどのように機能するかをより明確にするネストされたセット ダイアグラムとしてrght:

__________________________________________________________________________
| | ルート 1 |
| | ________________________________ ________________________________ |
| | | | 子 1.1 | | | 子 1.2 | | |
| | | | ____________ ____________ | | | ____________ ____________ | | |
| | | | | | C 1.1.1 | | | C 1.1.2 | | | | | | | C 1.2.1 | | | C 1.2.2 | | | | |
1 2 3___________4 5__________6 7 8 9___________10 11__________12 13 14
| | |_________________| |_________________| | |
|__________________________________________________________________________|

ご覧のように、特定のノードのサブツリー全体をツリーの順序で取得するには、との値の間にlftとの値を持つすべての行を選択するだけです。特定のノードの先祖のツリーを取得するのも簡単です。rghtlftrght

level列は何よりも利便性のために少し非正規化されており、列を使用すると、最上位ノードごとにとtree_idの番号付けを再開できます。これにより、挿入、移動、および削除の影響を受ける列の数が減少します。ギャップを作成または閉鎖するためにこれらの操作が行われると、それに応じて調整されます。各操作に必要なクエリに頭を悩ませようとしていたときに、いくつかの開発ノートを作成しました。lftrghtlftrght

このデータを実際に操作してツリーを表示するという点でtree_item_iterator、各ノードについて、必要な表示を生成するのに十分な情報を提供するユーティリティ関数を作成しました。

MPTT に関する詳細情報:

于 2008-10-11T12:31:22.337 に答える
18

Oracle 9i 以降では、CONNECT BY を使用できます。

SELECT LPAD(' ', (LEVEL - 1) * 4) || "Name" AS "Name"
FROM (SELECT * FROM TMP_NODE ORDER BY "Order")
CONNECT BY PRIOR "Id" = "ParentId"
START WITH "Id" IN (SELECT "Id" FROM TMP_NODE WHERE "ParentId" = 0)

SQL Server 2005 では、再帰的な共通テーブル式 (CTE) を使用できます。

WITH [NodeList] (
  [Id]
  , [ParentId]
  , [Level]
  , [Order]
) AS (
  SELECT [Node].[Id]
    , [Node].[ParentId]
    , 0 AS [Level]
    , CONVERT([varchar](MAX), [Node].[Order]) AS [Order]
  FROM [Node]
  WHERE [Node].[ParentId] = 0
  UNION ALL
  SELECT [Node].[Id]
    , [Node].[ParentId]
    , [NodeList].[Level] + 1 AS [Level]
    , [NodeList].[Order] + '|'
      + CONVERT([varchar](MAX), [Node].[Order]) AS [Order]
  FROM [Node]
    INNER JOIN [NodeList] ON [NodeList].[Id] = [Node].[ParentId]
) SELECT REPLICATE(' ', [NodeList].[Level] * 4) + [Node].[Name] AS [Name]
FROM [Node]
  INNER JOIN [NodeList] ON [NodeList].[Id] = [Node].[Id]
ORDER BY [NodeList].[Order]

どちらも次の結果を出力します。

名前
「ノード 1」
「ノード 1.1」
「ノード 1.1.1」
「ノード 1.2」
「ノード 2」
「ノード 2.1」
于 2008-10-10T20:06:21.153 に答える
10

ビルの答えはかなりまあまあです、この答えはそれにいくつかのことを追加します。

とにかく、私はツリー構造とOrderプロパティをサポートしたかったのです。leftSibling呼び出された各ノードに、元の質問で同じことを行うことを意図した単一のプロパティを含めましたOrder(左から右の順序を維持します)。

mysql>descノード;
+ ------------- + -------------- + ------ + ----- + ------- -+ ---------------- +
| フィールド| タイプ| ヌル| キー| デフォルト| エクストラ|
+ ------------- + -------------- + ------ + ----- + ------- -+ ---------------- +
| id | int(11)| いいえ| PRI | NULL | auto_increment |
| 名前| varchar(255)| はい| | NULL | |
| leftSibling | int(11)| いいえ| | 0 | |
+ ------------- + -------------- + ------ + ----- + ------- -+ ---------------- +
セットの3行(0.00秒)

mysql>desc隣接;
+ ------------ + --------- + ------ + ----- + --------- + --- ------------- +
| フィールド| タイプ| ヌル| キー| デフォルト| エクストラ|
+ ------------ + --------- + ------ + ----- + --------- + --- ------------- +
| RelationId | int(11)| いいえ| PRI | NULL | auto_increment |
| 親| int(11)| いいえ| | NULL | |
| 子供| int(11)| いいえ| | NULL | |
| pathLen | int(11)| いいえ| | NULL | |
+ ------------ + --------- + ------ + ----- + --------- + --- ------------- +
セットの4行(0.00秒)

私のブログの詳細とSQLコード

ビルに感謝しますあなたの答えは始めるのに役立ちました!

于 2010-12-22T04:31:40.227 に答える
7

選択肢があれば、オブジェクトを使用します。各オブジェクトにコレクションがあるレコードごとにオブジェクトを作成しchildren、ID がキーである連想配列 (/hashtable) にそれらをすべて格納します。そして、コレクションを 1 回ブリッツして、関連する子フィールドに子を追加します。単純。

しかし、良いOOPの使用を制限することで面白くないので、おそらく次のことに基づいて反復します:

function PrintLine(int pID, int level)
    foreach record where ParentID == pID
        print level*tabs + record-data
        PrintLine(record.ID, level + 1)

PrintLine(0, 0)

編集: これは他のいくつかのエントリと似ていますが、少しすっきりしていると思います。付け加えておきますが、これは非常に SQL を多用します。それは厄介です。選択肢がある場合は、OOP ルートに進みます。

于 2008-10-10T17:36:21.690 に答える
5

これはすぐに作成され、きれいでも効率的でもありません(さらに、多くの自動ボックス化が行われ、変換intInteger煩わしいです!)が、機能します。

私は自分のオブジェクトを作成しているので、おそらくルールに違反していますが、実際の作業からの転換としてこれを行っています:)

これは、ノードの構築を開始する前に、resultSet / tableが何らかの構造に完全に読み込まれることも前提としています。これは、数十万の行がある場合は最善の解決策ではありません。

public class Node {

    private Node parent = null;

    private List<Node> children;

    private String name;

    private int id = -1;

    public Node(Node parent, int id, String name) {
        this.parent = parent;
        this.children = new ArrayList<Node>();
        this.name = name;
        this.id = id;
    }

    public int getId() {
        return this.id;
    }

    public String getName() {
        return this.name;
    }

    public void addChild(Node child) {
        children.add(child);
    }

    public List<Node> getChildren() {
        return children;
    }

    public boolean isRoot() {
        return (this.parent == null);
    }

    @Override
    public String toString() {
        return "id=" + id + ", name=" + name + ", parent=" + parent;
    }
}

public class NodeBuilder {

    public static Node build(List<Map<String, String>> input) {

        // maps id of a node to it's Node object
        Map<Integer, Node> nodeMap = new HashMap<Integer, Node>();

        // maps id of a node to the id of it's parent
        Map<Integer, Integer> childParentMap = new HashMap<Integer, Integer>();

        // create special 'root' Node with id=0
        Node root = new Node(null, 0, "root");
        nodeMap.put(root.getId(), root);

        // iterate thru the input
        for (Map<String, String> map : input) {

            // expect each Map to have keys for "id", "name", "parent" ... a
            // real implementation would read from a SQL object or resultset
            int id = Integer.parseInt(map.get("id"));
            String name = map.get("name");
            int parent = Integer.parseInt(map.get("parent"));

            Node node = new Node(null, id, name);
            nodeMap.put(id, node);

            childParentMap.put(id, parent);
        }

        // now that each Node is created, setup the child-parent relationships
        for (Map.Entry<Integer, Integer> entry : childParentMap.entrySet()) {
            int nodeId = entry.getKey();
            int parentId = entry.getValue();

            Node child = nodeMap.get(nodeId);
            Node parent = nodeMap.get(parentId);
            parent.addChild(child);
        }

        return root;
    }
}

public class NodePrinter {

    static void printRootNode(Node root) {
        printNodes(root, 0);
    }

    static void printNodes(Node node, int indentLevel) {

        printNode(node, indentLevel);
        // recurse
        for (Node child : node.getChildren()) {
            printNodes(child, indentLevel + 1);
        }
    }

    static void printNode(Node node, int indentLevel) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < indentLevel; i++) {
            sb.append("\t");
        }
        sb.append(node);

        System.out.println(sb.toString());
    }

    public static void main(String[] args) {

        // setup dummy data
        List<Map<String, String>> resultSet = new ArrayList<Map<String, String>>();
        resultSet.add(newMap("1", "Node 1", "0"));
        resultSet.add(newMap("2", "Node 1.1", "1"));
        resultSet.add(newMap("3", "Node 2", "0"));
        resultSet.add(newMap("4", "Node 1.1.1", "2"));
        resultSet.add(newMap("5", "Node 2.1", "3"));
        resultSet.add(newMap("6", "Node 1.2", "1"));

        Node root = NodeBuilder.build(resultSet);
        printRootNode(root);

    }

    //convenience method for creating our dummy data
    private static Map<String, String> newMap(String id, String name, String parentId) {
        Map<String, String> row = new HashMap<String, String>();
        row.put("id", id);
        row.put("name", name);
        row.put("parent", parentId);
        return row;
    }
}
于 2008-10-10T18:25:29.937 に答える
4

ルート要素がゼロであることがわかっていると仮定すると、テキストに出力する擬似コードは次のようになります。

function PrintLevel (int curr, int level)
    //print the indents
    for (i=1; i<=level; i++)
        print a tab
    print curr \n;
    for each child in the table with a parent of curr
        PrintLevel (child, level+1)


for each elementID where the parentid is zero
    PrintLevel(elementID, 0)
于 2008-10-10T16:59:37.677 に答える
3

ハッシュマップを使用して他のデータ構造をエミュレートできるため、それはひどい制限ではありません。上から下にスキャンして、データベースの各行のハッシュマップを作成し、各列のエントリを作成します。これらの各ハッシュマップを「マスター」ハッシュマップに追加し、ID をキーにします。ノードにまだ見たことのない「親」がある場合は、マスター ハッシュマップにプレースホルダー エントリを作成し、実際のノードが表示されたときに入力します。

印刷するには、途中でインデント レベルを追跡しながら、単純な深さ優先のデータ パスを実行します。各行に「子」エントリを保持し、データをスキャンするときに入力することで、これを簡単に行うことができます。

ツリーをデータベースに保存する「より良い」方法があるかどうかについては、データをどのように使用するかによって異なります。階層内の各レベルに異なるテーブルを使用する既知の最大深度を持つシステムを見てきました。結局のところ、ツリーのレベルがまったく同じではない場合 (最上位のカテゴリが葉とは異なる)、これは非常に理にかなっています。

于 2008-10-10T17:24:34.973 に答える
1

BillのSQLソリューションを拡張するには、基本的にフラット配列を使用して同じことを行うことができます。さらに、文字列がすべて同じ長さで、子の最大数がわかっている場合(たとえば、バイナリツリーで)、単一の文字列(文字配列)を使用してそれを行うことができます。あなたが任意の数の子供を持っているなら、これは物事を少し複雑にします...私は何ができるかを見るために私の古いメモをチェックしなければならないでしょう。

次に、メモリを少し犠牲にします。特に、ツリーがスパースであるか、バランスが取れていない場合は、少しのインデックス計算を使用して、ツリーを配列に最初に幅を格納することで、すべての文字列にランダムにアクセスできます(バイナリの場合)木):

String[] nodeArray = [L0root, L1child1, L1child2, L2Child1, L2Child2, L2Child3, L2Child4] ...

あなたはあなたの弦の長さを知っています、あなたはそれを知っています

私は今仕事をしているので、それに多くの時間を費やすことはできませんが、興味を持って、これを行うために少しのコードをフェッチすることができます。

DNAコドンで作られた二分木を検索するために使用し、ツリーを構築するプロセスで、テキストパターンを検索するためにフラット化し、見つかった場合、インデックス計算(上から逆)を使用してノードを取得します...非常に高速で効率的でタフなツリーに空のノードが存在することはめったにありませんが、ギガバイトのデータを簡単に検索できました。

于 2008-10-10T18:42:36.940 に答える
1

例に示すように、要素がツリー順になっている場合は、次の Python の例のようなものを使用できます。

delimiter = '.'
stack = []
for item in items:
  while stack and not item.startswith(stack[-1]+delimiter):
    print "</div>"
    stack.pop()
  print "<div>"
  print item
  stack.append(item)

これが行うことは、ツリー内の現在の位置を表すスタックを維持することです。テーブル内の各要素について、現在のアイテムの親が見つかるまで、スタック要素をポップします (一致する div を閉じます)。次に、そのノードの開始を出力し、スタックにプッシュします。

ネストされた要素ではなくインデントを使用してツリーを出力する場合は、単に print ステートメントをスキップして div を出力し、各項目の前にスタックのサイズの倍数に等しい数のスペースを出力できます。たとえば、Python では次のようになります。

print "  " * len(stack)

このメソッドを使用して、ネストされたリストまたは辞書のセットを簡単に作成することもできます。

編集:あなたの説明から、名前はノードパスを意図したものではないことがわかりました。それは別のアプローチを示唆しています:

idx = {}
idx[0] = []
for node in results:
  child_list = []
  idx[node.Id] = child_list
  idx[node.ParentId].append((node, child_list))

これにより、タプル (!) の配列のツリーが構築されます。idx[0] はツリーのルートを表します。配列内の各要素は、ノード自体とそのすべての子のリストで構成される 2 タプルです。構築したら、idx[0] を保持して idx を破棄できますが、ID でノードにアクセスする必要はありません。

于 2008-10-10T21:45:05.167 に答える
1

ネストされたハッシュ マップまたは配列を作成できる場合は、テーブルを最初から下に移動して、ネストされた配列に各項目を追加するだけです。ネストされた配列のどのレベルに挿入するかを知るために、各行をルート ノードまでトレースする必要があります。同じ親を何度も検索する必要がないように、メモ化を使用できます。

編集:最初にテーブル全体を配列に読み込むため、DB に繰り返しクエリを実行しません。もちろん、テーブルが非常に大きい場合、これは実用的ではありません。

構造が構築された後、深さ優先走査を行い、HTML を出力する必要があります。

1 つのテーブルを使用してこの情報を格納するためのより基本的な方法はありません (ただし、私は間違っている可能性があります;)。より良い解決策を知りたいです)。ただし、動的に作成された db テーブルを使用するスキームを作成すると、単純さを犠牲にしてまったく新しい世界が開かれ、SQL 地獄のリスクが生じます;)。

于 2008-10-10T17:02:40.103 に答える
0

階層構造に neo4j のような nosql ツールを使用することを考えてみてください。たとえば、linkedin のようなネットワーク化されたアプリケーションは、couchbase (別の nosql ソリューション) を使用します。

ただし、nosql はデータ マート レベルのクエリにのみ使用し、トランザクションの保存/維持には使用しないでください

于 2012-11-26T15:49:12.657 に答える