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


517

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

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に格納するための根本的に優れた方法はありますか?


編集と追加

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

私が話した「結果セット」は、(その用語にとどまるために)ハッシュマップの配列として描くことができます。私の例では、すでにそこにあるはずです。いくつかの答えは、追加のマイルを行き、最初にそれを構築しますが、それは大丈夫です。

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

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

私は自分の解決策を投稿しましたので、皆さんがそれをバラバラにプルすることができます。


2
「親子参照を持つ派手なオブジェクトはありません」-なぜでしょうか?.addChild()、. getParent()メソッドを使用して基本的なNodeオブジェクトを作成すると、ノードの関係を適切にモデル化できます。
matt b

2
それは通常の(nの子はnが2より大きい場合があります)ツリーまたはバイナリツリー(ノードは0、1、または2の子を持つことができます)ですか?
BKimmel 2008年

ハッシュマップを使用して適切なノードデータ構造を実装できるため、ここでは実際の制限はなく、さらに作業が必要です。
Svante

...そして、それがまさにあなたがしたことです。
Svante

回答:


451

の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年のプレゼンテーションの再帰クエリスローダウンでMySQL 8.0の再帰クエリをテストしました。

以下は2008年からの私の元の答えです:


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

  • 隣接リスト(「親」列)および
  • パスの列挙(名前の列にあるドット付きの番号)。

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

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

私は、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から降りていない別の階層の一部であるため、除外されます。


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);

次に、特定のノードの直接の子を照会するための用語を検索に追加できます。これら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の子孫であるすべてのノードを返すクエリの例を次に示します。これらのノードをなどの他のノード属性を含む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がであるべきであることを示唆しました。しかし、「ノード1.2」の後に「ノード1.1.1」を注文するため、これは機能しません。

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


6
これはとてもエレガントです、ありがとう。ボーナスポイントを獲得しました。;-)ただし、小さな欠点が1つあります。これは、子リレーションを明示的および暗黙的に格納するため、ツリー構造の小さなシフトでも、多くの注意深いUPDATEを実行する必要があります。
Tomalak 2008年

16
確かに、データベースにツリー構造を保存するすべての方法では、ツリーを作成または更新するとき、またはツリーとサブツリーを照会するときに、いくらかの作業が必要です。シンプルにしたいデザイン(書き込みまたは読み取り)を選択します。
ビルカーウィン

2
@bufferでは、階層のすべての行を作成するときに不整合が生じる可能性があります。隣接リスト(parent_id)には、各親子関係を表す行が1つしかありませんが、クロージャテーブルには多くあります。
ビルカーウィン2014年

1
もう一つ@BillKarwin、任意の所与のノードへの複数のパスを持つグラフ(任意葉または非葉ノードが複数の親に属し例えばカテゴリ階層)に適した閉鎖テーブルである
ユーザ

2
@Reza。新しい子ノードを追加すると、(1)のすべての子孫を照会でき、それらは新しい子の祖先になります。
Bill Karwin、2015年

58

入れ子になったセット(変更された事前注文ツリートラバーサルと呼ばれることもあります)を使用する場合、1回のクエリでツリー構造全体またはその中のサブツリーをツリー順に抽出できますが、挿入のコストは高くなります。ツリー構造内の順序パスを説明する列を管理します。

ジャンゴ-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カラムは、何よりも便宜上denormalisationのビットであり、tree_idカラムは、再起動することを可能lftrght同様に、挿入、移動および削除によって影響を受ける列の数を減らす各トップレベルノードのための番号lftrght列がでなければなりませんギャップを作成または閉じるためにこれらの操作が行われるときに、それに応じて調整されます。各操作に必要なクエリに頭を回そうとしたときに、いくつかの開発メモを作成しました。

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

MPTTの詳細:


9
列名lftなどの省略形の使用をやめたいと思いrghtます。つまり、入力する必要がなかった文字数を教えてください。1?!
orustammanapov 2018

21

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

ツリー構造を読み取るには、再帰的な共通テーブル式(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」

cteはsqlserverとoracleの両方で使用できます@Eric Weilnau
Nisar

9

ビルの答えはかなりまともです、この答えはそれにいくつかのことを追加しますので、サポートされているスレッド化された答えを望みます。

とにかく、ツリー構造と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コード

ありがとうビルあなたの答えは、始めるのに役立ちました!


7

選択肢が与えられれば、オブジェクトを使用することになります。各オブジェクトにchildrenコレクションがあるレコードごとにオブジェクトを作成し、IDがキーであるassoc配列(/ 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)

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


それは私が「フレームワークなし」で意味したことです-あなたはLINQを使用していますね?最初の段落について:結果セットはすでにそこにありますが、なぜすべての情報を最初に新しいオブジェクト構造にコピーするのですか?(私はその事実を十分に明確にしていませんでした、申し訳ありません)
トマラック

トマラック-コードは疑似コードではありません。もちろん、物事を適切な選択と反復子に分解する必要があります...そして本当の構文!なぜOOPなのか?構造を正確にミラーリングできるからです。それは物事を良好に保ち、たまたまより効率的です(1つだけ選択)
Oli

繰り返し選択することも考えていませんでした。OOPについて:Mark Besseyは彼の回答で次のように述べています:「ハッシュマップで他のデータ構造をエミュレートできるので、それはひどい制限ではありません。」あなたの解決策は正しいですが、OOPがなくても改善の余地はあると思います。
Tomalak、2008年

5

これはすぐに書かれており、どちらもかなりも効率的である(プラスそれは間の変換、たくさんautoboxesた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;
    }
}

多くのソースコードが提示されたとき、アルゴリズム固有の部分を実装固有の部分からフィルターするのは常に難しいと思います。そのため、そもそも言語固有ではない解決策を求めました。しかし、それは仕事をしますので、あなたの時間をありがとう!
Tomalak 2008年

主なアルゴリズムがNodeBuilder.build()にあることが明らかでない場合は、今あなたが何を意味しているのかわかります。おそらく、これを合計するより良い仕事ができたでしょう。
matt b

5

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プレオーダーインデックス、root = 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インデックスとsz(分岐サイズ)の値を更新する必要があるため、挿入/削除のコストが高くなります。

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

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

ツリークエリを調整する

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

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

ルート要素がゼロであることがわかっていると仮定して、テキストに出力する疑似コードを次に示します。

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

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

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

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


1

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

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

構造が構築されたら、まず深さを調べてHTMLを出力する必要があります。

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


1
新しいレベルのサブノードが必要なからといって、DBレイアウトを変更したくありません。:-)
トマラック2008年

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タプルです。構築後は、IDでノードにアクセスする場合を除き、idx [0]を保持してidxを破棄できます。


1

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

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

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

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

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

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


0

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

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


SQLと「非テーブル」構造の複雑さとパフォーマンスを読んだので、これも私の最初の考えでした、nosql。もちろん、エクスポートなどには非常に多くの問題があります。さらに、OPはテーブルのみについて言及しました。しかたがない。明らかなように、私はDBの専門家ではありません。
Josef.B 2015
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.