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

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] 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中是否有一种从根本上更好的方法来存储这样的树结构?


编辑和添加

要回答一个评论者(马克·贝西)的问题:根节点不是必需的,因为无论如何它都不会显示。 ParentId = 0是表示“这些是顶级”的约定。 Order列定义了如何对具有相同父级的节点进行排序。

我所说的“结果集”可以描述为一个哈希表数组(保留该术语)。对于我的示例,本应已经存在。一些答案要花很多功夫,然后再进行构建,但这没关系。

树可以任意深。每个节点可以有N个子节点。但是,我并没有真正想到“成千上万的条目”树。

请不要误以为我选择的节点命名(“ Node 1.1.1”)取决于所依赖的内容。这些节点也可以被称为“ Frank”或“ Bob”,不暗示任何命名结构,这仅仅是为了使其可读。

我发布了自己的解决方案,以便您可以将其应用于件。

评论

“没有带有父母/孩子引用的特殊对象”-为什么不呢?使用.addChild()和.getParent()方法创建基本的Node对象,可以很好地对节点关系进行建模。

是规则树(n个子节点,其中n可以大于2)是二叉树还是二叉树(节点可以有0、1或2个子节点)?

由于您可以使用哈希图实现适当的节点数据结构,因此这里没有真正的限制,只是需要做更多的工作。

...而这正是您所做的。

#1 楼

现在,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中的递归查询2017年的递归查询量下降。下面是我从2008年开始的原始答案:您在示例中显示的内容使用两种方法:



邻接表(“父”列)和

路径枚举(点划线-名称栏中的数字)。

另一种解决方案称为嵌套集,它也可以存储在同一表中。有关这些设计的更多信息,请阅读Joe Celko的“用于Smarties的SQL中的树和层次结构”。

我通常更喜欢一种称为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,以确保顺序与层次结构匹配。但这是行不通的,因为它将在“节点1.2”之后对“节点1.1.1”进行排序。

如果您希望该排序以合理的方式与层次结构匹配,则可以,但是不只是通过路径长度排序。例如,请参阅我对MySQL Closure Table分层数据库的回答-如何以正确的顺序提取信息。

评论


这非常优雅,谢谢。奖励积分。 ;-)不过,我看到一个小缺点-由于它显式和隐式存储子关系,因此即使在树形结构中发生很小的变化,您也需要做很多仔细的UPDATE。

– Tomalak
08-10-11在9:51

的确,在创建或更新树或查询树和子树时,每种在数据库中存储树结构的方法都需要做一些工作。选择您想简化的设计:书写或阅读。

– Bill Karwin
08-10-11在19:29

@buffer,当您为层次结构创建所有行时,有可能创建不一致。邻接表(parent_id)仅具有一行来表示每个父子关系,而闭合表有很多。

– Bill Karwin
2014年5月13日15:21

@BillKarwin还有一点是,闭包表适用于具有到任何给定节点的多个路径的图(例如,类别层次结构,其中任何叶节点或非叶节点可能属于多个父节点)

–用户
14年5月13日在18:45

@Reza,因此,如果添加新的子节点,则可以查询(1)的所有后代,而这些子代是新子代的祖先。

– Bill Karwin
2015年11月9日在22:52

#2 楼

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

对于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


或者,作为嵌套集合图,它使lftrght值起作用:

 __________________________________________________________________________
|  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
|  |________________________________|  |________________________________|  |
|__________________________________________________________________________|


如您所见,要以树顺序获取给定节点的整个子树,只需选择所有具有lft的行rghtlft之间的值。检索给定节点的祖先树也很容易。顶层节点,减少了受插入,移动和删除影响的列数,因为在进行这些操作以创建或缩小间隔时必须相应地调整rghtlevel列。当我试图围绕每个操作所需的查询时写了一些开发说明。

关于实际使用此数据显示树的方法,我创建了一个tree_id实用程序函数,对于每个节点,该函数应为您提供足够的信息以生成所需的任何类型的显示。

有关MPTT的更多信息:


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


评论


我希望我们不再对列名使用lftand rght之类的缩写,我的意思是我们不必键入多少个字符?一?!

–orustammanapov
18年2月24日在11:57

#3 楼

这是一个很老的问题,但是由于有很多观点,我认为值得提出一个替代方案,而且我认为这是一种非常优雅的解决方案。

要读取树结构,可以使用递归Common表表达式(CTE)。它使您可以一次获取整个树结构,并获得有关节点级别,其父节点以及父节点的子节点中的顺序的信息。

让我向您展示这将如何工作在PostgreSQL 9.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);



编写查询

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或阅读我的两篇涉及该主题的文章:




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



#4 楼

从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]


两者都会输出以下结果。

Name
'Node 1'
'    Node 1.1'
'        Node 1.1.1'
'    Node 1.2'
'Node 2'
'    Node 2.1'


评论


可以在sqlserver和oracle @Eric Weilnau中使用cte

– Nisar
2014年5月25日12:38

#5 楼

Bill的答案非常不错,这个答案增加了一些东西,这让我希望得到SO支持的线程答案。

无论如何,我都想支持树结构和Order属性。我在每个节点中都包含一个名为leftSibling的属性,该属性执行Order在原始问题中打算做的相同事情(保持从左到右的顺序)。

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代码。

感谢Bill,您的回答对入门非常有帮助!

#6 楼

好的选择,我会使用对象。我会为每个记录创建一个对象,其中每个对象都有一个children集合,并将它们全部存储在ID为键的assoc数组(/ hashtable)中。并快速浏览该收藏集,将子级添加到相关的子级字段中。很简单。

但是由于限制一些好的OOP的使用而使您感到无聊,我可能会根据以下内容进行迭代:

>编辑:这与其他几个条目相似,但我认为它稍微干净一些。我要添加的一件事:这是非常消耗SQL的。真讨厌如果可以选择,请走OOP路线。

评论


这就是我所说的“没有框架”的意思-您正在使用LINQ,不是吗?关于您的第一段:结果集已经存在,为什么要先将所有信息复制到新的对象结构中? (对此我还不太清楚,对不起)

– Tomalak
08-10-11在10:15

Tomalak-没有代码是伪代码。当然,您必须将事情分解为适当的选择和迭代器……以及真正的语法!为什么要面向对象?因为您可以精确地镜像结构。它使事情保持良好状态,而且碰巧效率更高(只有一种选择)

–奥利
08-10-12在9:52

我也没有重复选择的念头。关于OOP:Mark Bessey在回答中说:“您可以使用哈希映射来模拟任何其他数据结构,因此这不是一个可怕的限制。”您的解决方案是正确的,但我认为即使没有OOP,仍有一些改进的余地。

– Tomalak
08-10-13在8:45

#7 楼

这写得很快,既不好,也不高效(再加上很多自动装箱,在intInteger之间进行转换很烦人!),但是它可以正常工作。自己的对象,但是我是从实际工作中转移出来的:)

这还假设在开始构建Node之前,将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
08-10-11在12:19

我现在明白您的意思了,如果不是很明显,主要算法是在NodeBuilder.build()中-我总结起来可能做得更好。

–马特b
08-10-13在20:05

#8 楼

确实有很好的解决方案可以利用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),root为null。
sz:节点本身(包括自身)的分支大小。
nc:用作语法糖。它是tw + nc,代表节点“下一个子节点”的tw。

这里是一个示例24节点填充,按tw排序:

+-----+---------+----+------+------+------+
| 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值超出范围,因此在移动分支时也必须禁用外键约束。移动分支基本上需要四个查询:


将分支移出范围。
缩小分支的剩余距离。 (剩下的树现在已标准化)。
打开将要到达的间隙。
将分支移到新位置。

调整树查询

树中间隙的打开/关闭是create / update / delete方法使用的重要子功能。 ,所以我将其包括在这里。

我们需要两个参数-一个表示是否要缩小或放大尺寸的标志,以及该节点的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;


#9 楼

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

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)


#10 楼

您可以使用哈希图模拟任何其他数据结构,因此这不是一个可怕的限制。从顶部到底部扫描,您将为数据库的每一行创建一个哈希图,并为每一列创建一个条目。将每个哈希表添加到键入ID的“主”哈希表。如果任何节点具有尚未出现的“父”节点,请在主哈希图中为其创建一个占位符条目,并在看到实际节点时将其填充。

要打印出来,对数据进行简单的深度优先传递,一路跟踪缩进级别。您可以通过为每行保留一个“子级”条目并在扫描数据时填充它来简化此操作。

关于是否有“更好”的方式将树存储在数据库中,这取决于您如何使用数据。我已经看到了具有已知最大深度的系统,该系统为层次结构中的每个级别使用了不同的表。如果树中的级别毕竟不完全相等(顶级类别与叶子不同),这很有道理。

#11 楼

如果可以创建嵌套的哈希映射或数组,那么我可以简单地从表头开始将表格添加到嵌套数组中。我必须跟踪每一行到根节点,以便知道要插入嵌套数组中的哪个级别。我可以使用记忆,这样就不必一次又一次地查找同一父级。

编辑:我会先将整个表读入数组,这样就不会重复查询数据库。当然,如果您的表很大,这将不切实际。

构建结构后,我必须先遍历深度并打印出HTML。

没有更好的基本方法来使用一个表存储此信息(尽管我可能错了;),并且希望看到一种更好的解决方案)。但是,如果您创建一个方案来使用动态创建的数据库表,那么您将在牺牲简单性和SQL地狱风险的情况下打开一个全新的世界;)。

评论


我不想仅仅因为需要新级别的子节点而更改数据库布局。 :-)

– Tomalak
08-10-11在12:06

#12 楼

如果元素按树顺序显示(如您的示例所示),则可以使用以下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访问节点。

#13 楼

要扩展Bill的SQL解决方案,您基本上可以使用平面数组进行相同的操作。此外,如果您的字符串都具有相同的长度,并且已知最大子代数(例如在二叉树中),则可以使用单个字符串(字符数组)来实现。如果您有任意数量的孩子,这会使事情变得有些复杂...我将不得不检查我的旧笔记以了解可以做什么。

然后,牺牲一点内存,尤其是如果您的树是稀疏的和/或不平衡的,您可以使用一些索引数学,通过将树存储在树中,首先像这样存储宽度(对于二叉树)来随机访问所有字符串(对于二叉树):

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


你知道你的字符串长度,你知道吗


我现在在工作,所以不能花很多时间在上面,但有兴趣的话,我可以拿一点代码来做到这一点。

我们通常使用它来搜索由DNA密码子组成的二叉树,然后建立树,然后将其展平以搜索文本模式,尽管找到了索引数学(从上面倒过来),取回节点...非常快速,高效,坚韧,我们的树很少有空节点,但是我们可以在极短的时间内获取千兆字节的数据。

#14 楼

考虑将noeo工具(例如neo4j)用于层次结构。
例如,诸如linkedin之类的网络应用程序使用了ouchbase(另一种nosql解决方案)

,但是仅将nosql用于数据集市级别的查询,而不存储/维护事务

评论


阅读了SQL和“非表”结构的复杂性和性能后,这也是我的第一个想法,即nosql。当然,导出有很多问题,等等。此外,OP仅提及表。那好吧。很明显,我不是数据库专家。

–Josef.B
2015年12月13日在14:14