关于sql:将平面表解析为树的最有效/优雅的方法是什么?

What is the most efficient/elegant way to parse a flat table into a tree?

假设您有一个平面表,它存储一个有序的树层次结构:

1
2
3
4
5
6
7
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是虚构的。

1
2
3
4
5
6
7
                       [0] ROOT
                          /    \
              [1] Node 1          [3] Node 2
              /       \                   \
    [2] Node 1.1     [6] Node 1.2      [5] Node 2.1
          /          
 [4] Node 1.1.1

您将使用哪种最低限度的方法将其输出到HTML(或文本,就这点而言)中,作为一个正确排序、正确缩进的树?

进一步假设您只有基本的数据结构(数组和哈希图),没有带有父/子引用的奇特对象,没有ORM,没有框架,只有双手。该表表示为结果集,可以随机访问。

伪码或纯英语也可以,这纯粹是一个概念性问题。

额外的问题:有没有一种从根本上更好的方法来在RDBMS中存储这样的树结构?

编辑和添加

要回答一个评论者(MarkBessey)的问题:根节点是不必要的,因为它永远不会被显示出来。parentID=0表示"这些是顶层"的约定。order列定义如何对具有相同父级的节点进行排序。

我提到的"结果集"可以被描绘成一组散列图(用这个术语)。因为我的例子本来就应该在那里。有些答案需要额外的一英里,并首先构建它,但这没关系。

这棵树可以任意深。每个节点可以有n个子节点。不过,我并没有确切地记住"百万条目的"树。

不要把我选择的节点命名("node 1.1.1")误认为是可以依赖的。节点也可以称为"frank"或"bob",不暗示命名结构,这只是为了使其可读。

我已经发布了我自己的解决方案,所以你们可以把它撕成碎片。


现在MySQL8.0即将发布,所有流行的SQL数据库都将支持标准语法的递归查询。

1
2
3
4
5
6
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年的演示文稿递归查询限制中测试了MySQL8.0中的递归查询。

以下是我2008年的原始答案:

在关系数据库中存储树结构数据有几种方法。在示例中显示的内容使用两种方法:

  • 相邻列表("父"列)和
  • 路径枚举(名称列中的点编号)。

另一个解决方案称为嵌套集,它也可以存储在同一个表中。有关这些设计的更多信息,请阅读JoeCelko的"智能体SQL中的树和层次结构"。

我通常更喜欢一种称为闭包表(也称为"邻接关系")的设计来存储树结构数据。它需要另一个表,但是查询树非常容易。

我在我的展示模型中介绍了使用SQL和PHP的分层数据的闭包表,在我的书中还介绍了SQL反模式:避免数据库编程的陷阱。

1
2
3
4
5
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)
);

将所有路径存储在封闭表中,其中有从一个节点到另一个节点的直接祖先。为每个节点包含一行以引用自身。例如,使用问题中显示的数据集:

1
2
3
4
5
6
7
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开始得到一个树,如下所示:

1
2
3
4
SELECT f.*
FROM FlatTable f
  JOIN ClosureTable a ON (f.id = a.descendant_id)
WHERE a.ancestor_id = 1;

输出(在mysql客户机中)如下所示:

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

换句话说,节点3和5被排除在外,因为它们是单独层次结构的一部分,而不是从节点1开始下降。

回复:来自e-satis关于直系子女(或直系父母)的评论。您可以在ClosureTable中添加一个"path_length列,以便于专门查询直接子级或父级(或任何其他距离)。

1
2
3
4
5
6
7
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);

然后,您可以在搜索中添加一个术语,用于查询给定节点的直接子节点。他们是以东十一〔0〕是一的子孙。

1
2
3
4
5
6
7
8
9
10
11
12
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)的平面表,并按名称排序。

1
2
3
4
5
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的回复:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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         |
+------------+-------------+

一位用户今天建议进行编辑。所以版主批准了编辑,但我正在撤销它。

编辑建议上面最后一个查询中的order by应该是ORDER BY b.path_length, f.name,大概是为了确保排序与层次结构匹配。但这不起作用,因为它会在"node 1.2"之后排序"node 1.1.1"。

如果希望排序以合理的方式匹配层次结构,这是可能的,但不只是通过按路径长度排序。例如,请参阅我对mysql闭包表层次数据库的回答——如何以正确的顺序提取信息。


如果使用嵌套集(有时称为修改的预顺序树遍历),则可以通过单个查询以树顺序提取整个树结构或其中的任何子树,但插入成本更高,因为需要管理描述按顺序路径thrug的列。树的结构。

对于django mptt,我使用了这样的结构:

1
2
3
4
5
6
7
8
9
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
8
 1
 +-- 2
 |   +-- 3
 |   +-- 4
 |
 +-- 5
     +-- 6
     +-- 7

或者,作为一个嵌套的集合图,它使lftrght值的工作方式更加明显:

1
2
3
4
5
6
7
8
9
 __________________________________________________________________________
|  Root 1                                                                  |
|   ________________________________    ________________________________   |
|  |  Child 1.1                     |  |  Child 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列允许您为每个顶级节点重新启动lftrght编号,这减少了受插入、移动和删除影响的列数,如edoc在进行这些操作时,必须相应地调整x1(1)和rght列,以创建或闭合间隙。当我试图围绕每个操作所需的查询进行总结时,我做了一些开发笔记。

在实际使用这些数据来显示树的过程中,我创建了一个tree_item_iterator实用程序函数,它为每个节点提供足够的信息来生成您想要的任何类型的显示。

有关MPTT的详细信息:

  • SQL中的树
  • 在数据库中存储分层数据
  • 在MySQL中管理分层数据


这是一个相当古老的问题,但由于它有很多观点,我认为有必要提出一个替代方案,在我看来,这个方案非常优雅。

为了读取树结构,可以使用递归公用表表达式(CTE)。它可以一次获取整棵树的结构,了解节点的级别、父节点以及父节点的子节点中的顺序。

我来告诉你在PostgreSQL 9.1中这是如何工作的。

  • 创建结构

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    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);
  • 编写查询

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    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;

    结果如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
     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)

    树节点按深度级别排序。在最终输出中,我们将在后面的行中显示它们。

    对于每个级别,它们都按照父级内的父级id和节点顺序进行排序。这告诉我们如何按照这个顺序将它们呈现在输出链接节点中给父节点。

    有了这样的结构,用HTML做一个非常好的演示就不难了。

    在PostgreSQL、IBM DB2、MS SQL Server和Oracle中提供递归CTE。

    如果您想了解有关递归SQL查询的更多信息,您可以查看您最喜欢的DBMS的文档,也可以阅读我关于此主题的两篇文章:

    • 在SQL中执行:递归树遍历
    • 了解SQL递归查询的功能

  • 从Oracle 9i开始,您可以使用Connect By。

    1
    2
    3
    4
    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)。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    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
    2
    3
    4
    5
    6
    7
    Name
    'Node 1'
    '    Node 1.1'
    '        Node 1.1.1'
    '    Node 1.2'
    'Node 2'
    '    Node 2.1'


    比尔的答案相当不错,这个答案增加了一些东西,这使我希望得到如此支持的线索式的答案。

    总之,我想支持树结构和订单属性。我在每个节点中都包含一个名为leftSibling的属性,它执行了与原始问题(保持从左到右的顺序)中Order所要执行的相同操作。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    mysql> DESC nodes ;
    +-------------+--------------+------+-----+---------+----------------+
    | FIELD       | TYPE         | NULL | KEY | DEFAULT | Extra          |
    +-------------+--------------+------+-----+---------+----------------+
    | id          | INT(11)      | NO   | PRI | NULL    | AUTO_INCREMENT |
    | name        | VARCHAR(255) | YES  |     | NULL    |                |
    | leftSibling | INT(11)      | NO   |     | 0       |                |
    +-------------+--------------+------+-----+---------+----------------+
    3 ROWS IN SET (0.00 sec)

    mysql> DESC adjacencies;
    +------------+---------+------+-----+---------+----------------+
    | FIELD      | TYPE    | NULL | KEY | DEFAULT | Extra          |
    +------------+---------+------+-----+---------+----------------+
    | relationId | INT(11) | NO   | PRI | NULL    | AUTO_INCREMENT |
    | parent     | INT(11) | NO   |     | NULL    |                |
    | child      | INT(11) | NO   |     | NULL    |                |
    | pathLen    | INT(11) | NO   |     | NULL    |                |
    +------------+---------+------+-----+---------+----------------+
    4 ROWS IN SET (0.00 sec)

    我的博客上有更多的细节和SQL代码。

    谢谢比尔,你的回答对入门很有帮助!


    考虑到选择,我会使用对象。我将为每个记录创建一个对象,其中每个对象都有一个children集合,并将它们全部存储在一个assoc数组(/hashtable)中,其中i d是键。并通过一次闪电式的收集,添加到相关的儿童领域的儿童。简单。

    但是,由于限制使用一些好的OOP,您没有乐趣,我可能会根据以下内容进行迭代:

    1
    2
    3
    4
    5
    6
    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路线。


    这篇文章写得很快,既不好看,也不高效(加上它有很多自动驾驶室,在intInteger之间转换很烦人!)但是它起作用了。

    这可能违反了规则,因为我正在创建自己的对象,但嘿,我这样做是为了从实际工作中转移注意力:)

    这还假设在开始构建节点之前,结果集/表完全被读取到某种结构中,如果您有数十万行,这将不是最佳解决方案。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    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;
        }
    }


    有很多很好的解决方案可以利用SQL索引的内部btree表示。这是基于1998年前后的一些伟大研究。

    这是一个示例表(在MySQL中)。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    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),根节点为空。
    • sz:节点分支的大小,包括它本身。
    • NC:用作句法糖。它是tw+nc,表示节点的"下一个子节点"的tw。

    下面是一个示例24节点填充,由tw排序:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    +-----+---------+----+------+------+------+
    | 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'处节点的祖先列表

    祖先

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    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为根的节点集(分支)。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    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(branch size)值需要分别在插入点之后的所有节点上和所有祖先节点上更新。

    分支移动涉及将分支的tw值移出范围,因此在移动分支时还需要禁用外键约束。移动分支基本上需要四个查询:

    • 将分支移出范围。
    • 把它留下的空隙关上。(剩下的树现在正常化了)。
    • 打开它将要到达的间隙。
    • 把树枝移到新的位置。

    调整树查询

    打开/关闭树中的间隙是create/update/delete方法使用的一个重要子函数,因此我将它包括在这里。

    我们需要两个参数——一个标志表示我们是在缩小规模还是在扩大规模,以及节点的tw索引。例如,tw=18(分支大小为5)。假设我们正在缩小大小(删除tw)-这意味着我们在以下示例的更新中使用了"-"而不是"+"。

    我们首先使用(稍微改变)祖先函数来更新sz值。

    1
    2
    3
    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。

    1
    2
    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高于要删除分支的人调整父级。

    1
    2
    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;


    您可以使用哈希图模拟任何其他数据结构,所以这不是一个可怕的限制。从上到下扫描,您为数据库的每一行创建一个哈希图,并为每一列创建一个条目。将这些散列图中的每一个添加到一个"master"散列图中,该散列图的键控ID。如果任何节点有一个"parent"您还没有看到,请在master散列图中为其创建一个占位符条目,并在看到实际节点时将其填充。

    要打印出来,先做一个简单的深度测试,通过数据,跟踪一路上的缩进量。您可以通过为每一行保留一个"children"条目,并在扫描数据时填充它来简化这一过程。

    至于在数据库中存储树是否有更好的方法,这取决于您将如何使用数据。我见过系统有一个已知的最大深度,它为层次结构中的每个级别使用不同的表。如果树中的级别毕竟不完全相同(顶级类别与树叶不同),这就很有意义。


    假设您知道根元素为零,下面是要输出到文本的伪代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    FUNCTION PrintLevel (INT curr, INT level)
        //print the indents
        FOR (i=1; i<=level; i++)
            print a tab
        print curr
    ;
        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)

    如果元素是按树顺序排列的,如示例中所示,则可以使用类似以下Python示例的内容:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    delimiter = '.'
    stack = []
    FOR item IN items:
      while stack AND NOT item.startswith(stack[-1]+delimiter):
        print""
        stack.pop()
      print""
      print item
      stack.append(item)

    这样做的目的是维护一个表示树中当前位置的堆栈。对于表中的每个元素,它将弹出堆栈元素(关闭匹配的div),直到找到当前项的父级。然后它输出该节点的开始并将其推送到堆栈。

    如果要使用缩进而不是嵌套元素输出树,只需跳过print语句来打印div,并在每个项之前打印等于堆栈大小的某个倍数的空格。例如,在python中:

    1
    print" " * len(stack)

    您还可以轻松地使用此方法来构造一组嵌套的列表或字典。

    编辑:我从您的澄清中看到,名称不打算是节点路径。这意味着另一种方法:

    1
    2
    3
    4
    5
    6
    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访问节点。


    要扩展Bill的SQL解决方案,基本上可以使用平面数组来实现相同的功能。此外,如果所有字符串的长度都相同,并且已知子字符串的最大数目(例如在二进制树中),则可以使用单个字符串(字符数组)来完成。如果你有任意数量的孩子,这会使事情有点复杂…我得检查一下我的旧笔记,看看能做些什么。

    然后,牺牲一点内存,特别是如果您的树是稀疏的和/或不完整的,您可以使用一点索引数学,通过将树、宽度首先存储在数组中(对于二进制树)来随机访问所有字符串:

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

    你知道你的弦长,你知道的。

    我现在在工作,所以不能花太多时间在上面,但我可以感兴趣地获取一些代码来完成这项工作。

    我们用它来搜索由DNA密码子组成的二叉树,一个过程建立了这棵树,然后我们把它展平以搜索文本模式,当我们找到它时,尽管索引数学(从上面反转)我们得到了节点……我们的树非常快速、高效、坚韧,很少有空节点,但我们可以瞬间烧掉千兆字节的数据。


    如果可以创建嵌套的散列映射或数组,那么我只需从一开始沿着表向下走,并将每个项添加到嵌套的数组中。我必须跟踪每一行到根节点,以便知道要插入到嵌套数组中的哪个级别。我可以使用记忆,这样我就不需要一遍又一遍地查找同一个家长。

    编辑:我会先将整个表读取到一个数组中,这样它就不会重复查询数据库。当然,如果你的桌子很大,这是不实际的。

    在构建结构之后,我必须先对其进行深度遍历,然后打印出HTML。

    没有更好的基本方法可以使用一个表来存储这些信息(但我可能是错的;),我希望看到更好的解决方案)。但是,如果您创建一个使用动态创建的DB表的方案,那么您将以简单性和SQL地狱的风险为代价打开一个全新的世界;)。


    考虑使用NoSQL工具(如NEO4J)进行层次结构。例如,像LinkedIn这样的联网应用程序使用CouchBase(另一种NoSQL解决方案)

    但仅对数据集市级别的查询使用NoSQL,而不存储/维护事务