web-dev-qa-db-ja.com

フラットテーブルをツリーに解析する最も効率的でエレガントな方法は何ですか?

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

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] ROOT 
/\ 
 [1]ノード1 [3]ノード2 
/\\
 [2]ノード1.1 [ 6]ノード1.2 [5]ノード2.1 
/
 [4]ノード1.1.1 

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

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

擬似コードまたは平易な英語でも構いません。これは純粋に概念的な質問です。

ボーナスの質問:このようなツリー構造をRDBMSに保存する基本的に良い方法はありますか?


編集と追加

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

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

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

私が選択したノードの命名(「ノード1.1.1」)に依存するものと間違えないでください。ノードは同様に「フランク」または「ボブ」と呼ばれることもありますが、ネーミング構造は暗示されていません。これは単にそれを読みやすくするためでした。

私は自分の解決策を投稿したので、皆さんはそれをばらばらに引き出せます。

498
Tomalak

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;

プレゼンテーションでMySQL 8.0の再帰クエリをテストしました Recursive Query Throwdown 2017.

以下は、2008年からの私の最初の回答です。


リレーショナルデータベースにツリー構造のデータを保存するには、いくつかの方法があります。例で示すものは2つの方法を使用します。

  • 隣接リスト(「親」列)および
  • パス列挙(名前列のドット付き数字)。

別のソリューションはNested Setsと呼ばれ、同じテーブルに保存することもできます。これらの設計の詳細については、Joe Celkoの「 SQL for Smartiesのツリーと階層 」を参照してください。

私は通常、ツリー構造のデータを保存するためにClosure Table(別名「隣接関係」)と呼ばれる設計を好みます。別のテーブルが必要ですが、ツリーのクエリは非常に簡単です。

プレゼンテーション SQLおよびPHPを使用した階層データのモデル および書籍 SQLアンチパターン:データベースプログラミングの落とし穴の回避 でクロージャーテーブルについて説明します。

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から派生したものではなく、別の階層の一部であるため除外されます。


再:直接の子供(または直接の親)に関する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);

次に、特定のノードの直接の子を照会するための検索に用語を追加できます。これらは、path_lengthが1の子孫です。

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の子孫であるすべてのノードを返すクエリの例を示します。これらのノードを、nameなどの他のノード属性を含むFlatTableに結合し、名前で並べ替えます。

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がORDER BY b.path_length, f.nameであることが示唆されました。これは、おそらく順序が階層と一致することを確認するためです。しかし、「Node 1.2」の後に「Node 1.1.1」を注文するため、これは機能しません。

順序を適切な方法で階層に一致させたい場合は可能ですが、単純にパスの長さによる順序付けではありません。たとえば、 MySQL Closure Table階層データベース-正しい順序で情報を引き出す方法 への私の答えを参照してください。

429
Bill Karwin

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

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

 id parent_id tree_id level lft rght 
---------- ------- ----- --- ---- 
 1 null 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 
 | | ________________________________ | | ________________________________ | | 
 | __________________________________________________________________________ || 

ご覧のとおり、特定のノードのサブツリー全体をツリー順に取得するには、lftrghtの値の間にlftrghtの値があるすべての行を選択するだけです。また、特定のノードの祖先のツリーを取得するのも簡単です。

level列は、何よりも便利な非正規化のビットであり、tree_id列を使用すると、各トップレベルノードのlftおよびrght番号付けを再開できます。これにより、lftそして、rght列は、これらの操作が行われたときにギャップを作成または閉じるために、それに応じて調整する必要があります。各操作に必要なクエリに頭を包み込もうとしたときに 開発ノート を作成しました。

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

MPTTに関する詳細情報:

55
Jonny Buchanan

これはかなり古い質問ですが、多くの意見があるので、代替案を提示する価値があると思います。私の意見では、非常にエレガントな解決策です。

ツリー構造を読み取るには、再帰共通テーブル式(CTE)を使用できます。ツリー構造全体を一度に取得し、ノードのレベル、親ノード、および親ノードの子内の順序に関する情報を取得する可能性があります。

これがPostgreSQL 9.1でどのように機能するかを説明します。

  1. 構造を作成する

    CREATE TABLE tree (
        id int  NOT NULL,
        name varchar(32)  NOT NULL,
        parent_id int  NULL,
        node_order int  NOT NULL,
        CONSTRAINT tree_pk PRIMARY KEY (id),
        CONSTRAINT tree_tree_fk FOREIGN KEY (parent_id) 
          REFERENCES tree (id) NOT DEFERRABLE
    );
    
    
    insert into tree values
      (0, 'ROOT', NULL, 0),
      (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);
    
  2. クエリを書く

    WITH RECURSIVE 
    tree_search (id, name, level, parent_id, node_order) AS (
      SELECT 
        id, 
        name,
        0,
        parent_id, 
        1 
      FROM tree
      WHERE parent_id is NULL
    
      UNION ALL 
      SELECT 
        t.id, 
        t.name,
        ts.level + 1, 
        ts.id, 
        t.node_order 
      FROM tree t, tree_search ts 
      WHERE t.parent_id = ts.id 
    ) 
    SELECT * FROM tree_search 
    WHERE level > 0 
    ORDER BY level, parent_id, node_order;
    

    結果は次のとおりです。

     id |    name    | level | parent_id | node_order 
    ----+------------+-------+-----------+------------
      1 | Node 1     |     1 |         0 |         10
      3 | Node 2     |     1 |         0 |         20
      2 | Node 1.1   |     2 |         1 |         10
      6 | Node 1.2   |     2 |         1 |         20
      5 | Node 2.1   |     2 |         3 |         10
      4 | Node 1.1.1 |     3 |         2 |         10
    (6 rows)
    

    ツリーノードは、深さのレベルで並べられます。最終出力では、それらを後続の行に表示します。

    レベルごとに、親内のparent_idとnode_orderで順序付けされます。これは、出力でそれらを表示する方法を示しています-この順序でノードを親にリンクします。

    このような構造があれば、HTMLで本当に素敵なプレゼンテーションを作成することは難しくありません。

    再帰CTEは、PostgreSQL、IBM DB2、MS SQL Server、およびOracleで利用できます。

    再帰SQLクエリの詳細については、お気に入りのDBMSのドキュメントを確認するか、このトピックに関する2つの記事を参照してください。

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 '
18
Eric Weilnau

ビルの答えは非常に素晴らしいです。この答えは、スレッド化された答えをSOがサポートすることを望みます。

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

 mysql> desc nodes; 
 + ------------- + -------------- + ----- -+ ----- + --------- + ---------------- + 
 |フィールド|タイプ|ヌル|キー|デフォルト|追加| 
 + ------------- + -------------- + ------ + ----- + --------- + ---------------- + 
 | id | int(11)|いいえ| PRI | NULL | auto_increment | 
 |名前| varchar(255)|はい| | NULL | | 
 | leftSibling | int(11)|いいえ| | 0 | | 
 + ------------- + -------------- + ------ + ----- +- -------- + ---------------- + 
 3行セット(0.00秒)
 
 mysql > desc adjacencies; 
 + ------------ + --------- + ------ + ----- + ---- ----- + ---------------- + 
 |フィールド|タイプ|ヌル|キー|デフォルト|追加| 
 + ------------ + --------- + ------ + ----- + ------ --- + ---------------- + 
 | relationId | int(11)|いいえ| PRI | NULL | auto_increment | 
 |親| int(11)|いいえ| | NULL | | 
 |子| int(11)|いいえ| | NULL | | 
 | pathLen | int(11)|いいえ| | NULL | | 
 + ------------ + --------- + ------ + ----- + ------- -+ ---------------- + 
 4行セット(0.00秒)

ブログの詳細とSQLコード

ビル、ありがとう。

9
bobobobo

選択肢があれば、オブジェクトを使用することになります。私は、各オブジェクトがchildrenコレクションを持つレコードごとにオブジェクトを作成し、それらをすべてIDがキーであるassoc配列(/ hashtable)に格納します。コレクションを一度ブリッツして、関連する子フィールドに子を追加します。 シンプル

しかし、優れた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)

編集:これは他のいくつかのエントリに似ていますが、私はそれが少しきれいだと思います。 1つ追加することは、これは非常にSQLを集中的に使用することです。それはnastyです。 選択がある場合は、OOPルートに進みます。

7
Oli

これはすぐに書かれており、きれいでも効率的でもありません(さらに、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;
    }
}
5
matt b

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

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

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

3
Mark Bessey

SQLインデックスの内部btree表現を活用する、本当に優れたソリューションがあります。これは、1998年頃に行われたいくつかの素晴らしい研究に基づいています。

以下にテーブルの例を示します(mysql内)。

CREATE TABLE `node` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(255) NOT NULL,
  `tw` int(10) unsigned NOT NULL,
  `pa` int(10) unsigned DEFAULT NULL,
  `sz` int(10) unsigned DEFAULT NULL,
  `nc` int(11) GENERATED ALWAYS AS (tw+sz) STORED,
  PRIMARY KEY (`id`),
  KEY `node_tw_index` (`tw`),
  KEY `node_pa_index` (`pa`),
  KEY `node_nc_index` (`nc`),
  CONSTRAINT `node_pa_fk` FOREIGN KEY (`pa`) REFERENCES `node` (`tw`) ON DELETE CASCADE
)

ツリー表現に必要なフィールドは次のとおりです。

  • tw:左から右へのDFS事前注文インデックス、ルート= 1。
  • pa:親ノードへの参照(twを使用)、ルートにはnullがあります。
  • sz:自身を含むノードのブランチのサイズ。
  • nc:構文糖として使用されます。 tw + ncであり、ノードの「次の子」のtwを表します。

以下は、twで順序付けられた24ノードの人口の例です。

+-----+---------+----+------+------+------+
| id  | name    | tw | pa   | sz   | nc   |
+-----+---------+----+------+------+------+
|   1 | Root    |  1 | NULL |   24 |   25 |
|   2 | A       |  2 |    1 |   14 |   16 |
|   3 | AA      |  3 |    2 |    1 |    4 |
|   4 | AB      |  4 |    2 |    7 |   11 |
|   5 | ABA     |  5 |    4 |    1 |    6 |
|   6 | ABB     |  6 |    4 |    3 |    9 |
|   7 | ABBA    |  7 |    6 |    1 |    8 |
|   8 | ABBB    |  8 |    6 |    1 |    9 |
|   9 | ABC     |  9 |    4 |    2 |   11 |
|  10 | ABCD    | 10 |    9 |    1 |   11 |
|  11 | AC      | 11 |    2 |    4 |   15 |
|  12 | ACA     | 12 |   11 |    2 |   14 |
|  13 | ACAA    | 13 |   12 |    1 |   14 |
|  14 | ACB     | 14 |   11 |    1 |   15 |
|  15 | AD      | 15 |    2 |    1 |   16 |
|  16 | B       | 16 |    1 |    1 |   17 |
|  17 | C       | 17 |    1 |    6 |   23 |
| 359 | C0      | 18 |   17 |    5 |   23 |
| 360 | C1      | 19 |   18 |    4 |   23 |
| 361 | C2(res) | 20 |   19 |    3 |   23 |
| 362 | C3      | 21 |   20 |    2 |   23 |
| 363 | C4      | 22 |   21 |    1 |   23 |
|  18 | D       | 23 |    1 |    1 |   24 |
|  19 | E       | 24 |    1 |    1 |   25 |
+-----+---------+----+------+------+------+

すべてのツリー結果は非再帰的に実行できます。たとえば、tw = '22 'にあるノードの祖先のリストを取得するには、

祖先

select anc.* from node me,node anc 
where me.tw=22 and anc.nc >= me.tw and anc.tw <= me.tw 
order by anc.tw;
+-----+---------+----+------+------+------+
| id  | name    | tw | pa   | sz   | nc   |
+-----+---------+----+------+------+------+
|   1 | Root    |  1 | NULL |   24 |   25 |
|  17 | C       | 17 |    1 |    6 |   23 |
| 359 | C0      | 18 |   17 |    5 |   23 |
| 360 | C1      | 19 |   18 |    4 |   23 |
| 361 | C2(res) | 20 |   19 |    3 |   23 |
| 362 | C3      | 21 |   20 |    2 |   23 |
| 363 | C4      | 22 |   21 |    1 |   23 |
+-----+---------+----+------+------+------+

兄弟と子はささいなことです-twでpaフィールドの順序を使用するだけです。

子孫

たとえば、tw = 17をルートとするノードのセット(ブランチ)。

select des.* from node me,node des 
where me.tw=17 and des.tw < me.nc and des.tw >= me.tw 
order by des.tw;
+-----+---------+----+------+------+------+
| id  | name    | tw | pa   | sz   | nc   |
+-----+---------+----+------+------+------+
|  17 | C       | 17 |    1 |    6 |   23 |
| 359 | C0      | 18 |   17 |    5 |   23 |
| 360 | C1      | 19 |   18 |    4 |   23 |
| 361 | C2(res) | 20 |   19 |    3 |   23 |
| 362 | C3      | 21 |   20 |    2 |   23 |
| 363 | C4      | 22 |   21 |    1 |   23 |
+-----+---------+----+------+------+------+

追記

この方法論は、挿入または更新よりもはるかに多くの読み取りがある場合に非常に役立ちます。

ツリー内のノードの挿入、移動、または更新では、ツリーを調整する必要があるため、アクションを開始する前にテーブルをロックする必要があります。

Tw indexとsz(ブランチサイズ)の値は、挿入ポイントの後のすべてのノードとすべての祖先でそれぞれ更新する必要があるため、挿入/削除のコストは高くなります。

ブランチの移動には、ブランチのtw値を範囲外に移動する必要があるため、ブランチを移動するときに外部キー制約を無効にする必要もあります。ブランチを移動するには、基本的に4つのクエリが必要です。

  • ブランチを範囲外に移動します。
  • 残った隙間を閉じます。 (残りのツリーは現在正規化されています)。
  • それが行くギャップを開きます。
  • ブランチを新しい位置に移動します。

ツリークエリの調整

ツリー内のギャップの開閉は、作成/更新/削除メソッドで使用される重要なサブ関数なので、ここに含めます。

2つのパラメーターが必要です-ダウンサイジングかアップサイジングかを表すフラグと、ノードのtwインデックス。したがって、たとえばtw = 18(ブランチサイズは5)です。ダウンサイジング(twを削除)すると仮定します-これは、次の例の更新で「+」の代わりに「-」を使用していることを意味します。

最初に(わずかに変更された)先祖関数を使用してsz値を更新します。

update node me, node anc set anc.sz = anc.sz - me.sz from 
node me, node anc where me.tw=18 
and ((anc.nc >= me.tw and anc.tw < me.pa) or (anc.tw=me.pa));

次に、削除するブランチよりもtwが大きいものについてtwを調整する必要があります。

update node me, node anc set anc.tw = anc.tw - me.sz from 
node me, node anc where me.tw=18 and anc.tw >= me.tw;

次に、paのtwが削除されるブランチよりも大きい人の親を調整する必要があります。

update node me, node anc set anc.pa = anc.pa - me.sz from 
node me, node anc where me.tw=18 and anc.pa >= me.tw;
3
Konchog

ルート要素がゼロであることを知っていると仮定すると、テキストに出力する擬似コードは次のとおりです。

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)
3
wcm

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

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

構造が構築された後、まず深さを調べて構造を走査し、HTMLを印刷する必要があります。

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

1
tchen

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

次に、少しメモリを犠牲にします。特に、ツリーがスパースおよび/またはバランシングされていない場合、少しのインデックス計算で、ツリーを格納してすべての文字列にランダムにアクセスできます。木):

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

あなたの文字列の長さを知っている、あなたはそれを知っている

私は今働いているので、あまり時間をかけることができませんが、興味を持って、これを行うために少しのコードを取得できます。

私たちはそれを使ってDNAコドンで作られたバイナリツリーを検索し、ツリーを構築したプロセスを使用し、それを平坦化してテキストパターンを検索し、見つかったらインデックス数学(上と逆)でノードを戻します...非常に速くて効率的で、タフなツリーには空のノードはめったにありませんが、Jiffyでギガバイトのデータを検索できます。

1
Newtopian

例に示すように要素がツリー順になっている場合、次の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を閉じます)。次に、そのノードの開始を出力し、スタックにプッシュします。

ネストされた要素ではなくインデントを使用してツリーを出力する場合は、印刷ステートメントをスキップして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タプルです。構築したら、IDでノードにアクセスする場合を除き、idx [0]を保持してidxを破棄できます。

1
Nick Johnson

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

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