将平板餐桌解析成树的最有效/最优雅的方法是什么?


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


编辑和添加

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

我所说的“结果集”可以图片为一个哈希表数组(保留在该术语中)。对于我的示例,本应已经存在。一些答案需要付出额外的努力,然后再进行构建,但这没关系。

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

不要将我对节点命名(“节点1.1.1”)的选择误认为是要依赖的东西。这些节点也可以称为“ Frank”或“ Bob”,没有暗示命名结构,这仅仅是为了使其可读。

我已经发布了自己的解决方案,因此你们可以将它分解成碎片。


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

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

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

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

Answers:


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年的演讲Recursive Query Throwdown中介绍了MySQL 8.0中的递归查询

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


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

  • 邻接列表(“父”列)和
  • 路径枚举(名称列中的点号)。

另一个解决方案称为嵌套集,它也可以存储在同一表中。有关这些设计的更多信息,请阅读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派生而来。


回复: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应该为ORDER BY b.path_length, f.name,以确保顺序与层次结构匹配。但这是行不通的,因为它将在“节点1.2”之后对“节点1.1.1”进行排序。

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


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

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

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

1
@BillKarwin还有一点是,闭包表适用于具有到任何给定节点(例如,任何叶节点或非叶节点可能属于一个以上的父节点)的多个路径的图的图
用户

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

58

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

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

id parent_id tree_id级别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

或者,作为嵌套的集合图,它使lftand 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值之间的lft和的行rght。检索给定节点的祖先树也很简单。

level为了方便起见,该列有点非正规化,这比其他任何事情都多,并且该tree_id列允许您为每个顶级节点重新启动lftrght编号,这减少了受插入,移动和删除影响的列数,因为lftand rght列必须是在进行这些操作时进行相应调整,以创建或缩小差距。当我试图围绕每个操作所需的查询时,我做了一些开发说明

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

有关MPTT的更多信息:


9
我希望我们不再使用诸如lft和这样的缩写rght来表示列名,我的意思是我们不必键入多少个字符?一?!
orustammanapov

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的文档,也可以阅读我的两篇有关该主题的文章:


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

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

无论如何,我想支持树形结构和Order属性。我在每个调用的Node中都包含一个属性,该属性在原始问题中要做的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代码

谢谢Bill,您的回答对入门很有帮助!


7

好的选择,我会使用对象。我会为每个记录创建一个对象,其中每个对象都有一个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)

编辑:这类似于其他几个条目,但我认为它稍微干净一些。我要添加的一件事:这是非常消耗SQL的。这是肮脏的如果可以选择,请执行OOP路线。


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

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

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

5

这写得很快,既不美观也不高效(加上它会自动装箱很多,int并且在它们之间进行转换Integer很烦人!),但是它可以工作。

自从我创建自己的对象以来,它可能违反了规则,但是嘿,这是对实际工作的转移:)

这也假设在开始构建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

我现在明白你的意思了,如果不是很明显,主要算法是在NodeBuilder.build()中-我总结起来可能做得更好。
马特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。

这是一个示例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;

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

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

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

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

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


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

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访问节点。


1

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

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

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

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

我现在正在工作,所以不能花很多时间,但有兴趣的是,我可以获取一些代码来完成此工作。

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


0

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

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


阅读了SQL和“非表”结构的复杂性和性能后,这也是我第一次想到的Nosql。当然,导出有很多问题,等等。此外,OP仅提及表。那好吧。很明显,我不是数据库专家。
Josef.B 2015年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.