树结构的优化SQL

时间:2008-11-25 13:31:47

标签: sql sql-server tree-structure

如何从具有最佳性能的数据库中获取树结构数据?例如,假设您在数据库中有一个文件夹层次结构。 folder-database-row具有 ID 名称 ParentID 列的位置。

您是否会使用特殊算法一次性获取所有数据,最大限度地减少数据库调用量并在代码中处理它?<​​/ p>

或者你会使用多次调用数据库并直接从数据库中完成结构?

根据x数据库行数,层次结构深度或其他什么,可能有不同的答案?

编辑:我使用的是Microsoft SQL Server,但其他观点的答案也很有趣。

12 个答案:

答案 0 :(得分:16)

这实际上取决于您将如何访问树。

一种聪明的技术是为每个节点提供一个字符串id,其中父节点的id是子节点的可预测子字符串。例如,父级可以是“01”,子级可以是“0100”,“0101”,“0102”等。这样,您可以一次从数据库中选择整个子树:

SELECT * FROM treedata WHERE id LIKE '0101%';

因为标准是一个初始子字符串,所以ID列上的索引会加快查询速度。

答案 1 :(得分:15)

在RDMS中存储树的所有方法中,最常见的是邻接列表和嵌套集。嵌套集针对读取进行了优化,可以在单个查询中检索整个树。邻接列表针对写入进行了优化,可以在简单查询中添加。

对于邻接列表,每个节点a具有引用父节点或子节点的列(其他链接是可能的)。使用它可以基于父子关系构建层次结构。不幸的是,除非你限制你的树的深度,你不能在一个查询中拉出整个东西,阅读它通常比更新它慢。

使用嵌套集模型,反之亦然,读取快速简便,但更新变得复杂,因为您必须维护编号系统。嵌套集模型通过使用基于预订单的编号系统枚举所有节点来对父项和排序顺序进行编码。

我使用了嵌套集模型,虽然读取优化大型层次结构很复杂但值得。一旦你做了一些抽取树和编号节点的练习,你应该掌握它。

我对这种方法的研究始于本文:Managing Hierarchical Data in MySQL

答案 2 :(得分:13)

查看nested sets层次结构模型。 它很酷很有用。

答案 3 :(得分:6)

在我使用的产品中,我们在SQL Server中存储了一些树结构,并使用上述技术在记录中存储节点的层次结构。即。

tblTreeNode
TreeID = 1
TreeNodeID = 100
ParentTreeNodeID = 99
Hierarchy = ".33.59.99.100."
[...] (actual data payload for node)

维护层次结构当然是一个棘手的问题,并使用触发器。但是在插入/删除/移动中生成它永远不会递归,因为父或子的层次结构具有您需要的所有信息。

你可以得到所有节点的后代:

SELECT * FROM tblNode WHERE Hierarchy LIKE '%.100.%'

这是插入触发器:

--Setup the top level if there is any
UPDATE T 
SET T.TreeNodeHierarchy = '.' + CONVERT(nvarchar(10), T.TreeNodeID) + '.'
FROM tblTreeNode AS T
    INNER JOIN inserted i ON T.TreeNodeID = i.TreeNodeID
WHERE (i.ParentTreeNodeID IS NULL) AND (i.TreeNodeHierarchy IS NULL)

WHILE EXISTS (SELECT * FROM tblTreeNode WHERE TreeNodeHierarchy IS NULL)
    BEGIN
        --Update those items that we have enough information to update - parent has text in Hierarchy
        UPDATE CHILD 
        SET CHILD.TreeNodeHierarchy = PARENT.TreeNodeHierarchy + CONVERT(nvarchar(10),CHILD.TreeNodeID) + '.'
        FROM tblTreeNode AS CHILD 
            INNER JOIN tblTreeNode AS PARENT ON CHILD.ParentTreeNodeID = PARENT.TreeNodeID
        WHERE (CHILD.TreeNodeHierarchy IS NULL) AND (PARENT.TreeNodeHierarchy IS NOT NULL)
    END

这是更新触发器:

--Only want to do something if Parent IDs were changed
IF UPDATE(ParentTreeNodeID)
    BEGIN
        --Update the changed items to reflect their new parents
        UPDATE CHILD
        SET CHILD.TreeNodeHierarchy = CASE WHEN PARENT.TreeNodeID IS NULL THEN '.' + CONVERT(nvarchar,CHILD.TreeNodeID) + '.' ELSE PARENT.TreeNodeHierarchy + CONVERT(nvarchar, CHILD.TreeNodeID) + '.' END
        FROM tblTreeNode AS CHILD 
            INNER JOIN inserted AS I ON CHILD.TreeNodeID = I.TreeNodeID
            LEFT JOIN tblTreeNode AS PARENT ON CHILD.ParentTreeNodeID = PARENT.TreeNodeID

        --Now update any sub items of the changed rows if any exist
        IF EXISTS (
                SELECT * 
                FROM tblTreeNode 
                    INNER JOIN deleted ON tblTreeNode.ParentTreeNodeID = deleted.TreeNodeID
            )
            UPDATE CHILD 
            SET CHILD.TreeNodeHierarchy = NEWPARENT.TreeNodeHierarchy + RIGHT(CHILD.TreeNodeHierarchy, LEN(CHILD.TreeNodeHierarchy) - LEN(OLDPARENT.TreeNodeHierarchy))
            FROM tblTreeNode AS CHILD 
                INNER JOIN deleted AS OLDPARENT ON CHILD.TreeNodeHierarchy LIKE (OLDPARENT.TreeNodeHierarchy + '%')
                INNER JOIN tblTreeNode AS NEWPARENT ON OLDPARENT.TreeNodeID = NEWPARENT.TreeNodeID

    END

再多一点,一个检查约束来阻止树节点中的循环引用:

ALTER TABLE [dbo].[tblTreeNode]  WITH NOCHECK ADD  CONSTRAINT [CK_tblTreeNode_TreeNodeHierarchy] CHECK  
((charindex(('.' + convert(nvarchar(10),[TreeNodeID]) + '.'),[TreeNodeHierarchy],(charindex(('.' + convert(nvarchar(10),[TreeNodeID]) + '.'),[TreeNodeHierarchy]) + 1)) = 0))

我还建议使用触发器来防止每棵树有多个根节点(null父节点),并保持相关节点不属于不同的TreeID(但这些节点比上面的更简单。)

您需要检查特定情况,看看此解决方案是否可以接受。希望这有帮助!

答案 4 :(得分:4)

答案 5 :(得分:2)

针对层次结构有几种常见的查询类型。大多数其他类型的查询都是这些的变体。

  1. 从父母那里找到所有孩子。

    一个。到特定的深度。例如,鉴于我的直接父母,所有深度为1的孩子都是我的兄弟姐妹。

    湾到树的底部。

  2. 从孩子那里找到所有的父母。

    一个。到特定的深度。例如,我的直接父母是深度为1的父母。

    湾无限深度。

  3. SQL中的(a)个案(特定深度)更容易。特殊情况(深度= 1)在SQL中是微不足道的。非零深度更难。有限但非零的深度可以通过有限数量的连接来完成。 (b)无限深度(从顶部到底部)的情况非常困难。

    如果您的树巨大(数百万个节点),那么无论您尝试做什么,您都会受到伤害。

    如果您的树在一百万个节点下,只需将其全部提取到内存中并在那里进行处理。在OO世界中,生活更加简单。只需获取行并在返回行时构建树。

    如果您有巨大树,则有两种选择。

    • 用于处理无限制提取的递归游标。这意味着结构的维护是O(1) - 只需更新几个节点即可完成。但是,获取是O(n * log(n)),因为你必须为每个带有子节点的节点打开一个游标。

    • 聪明的“堆编号”算法可以编码每个节点的父级。一旦每个节点都被正确编号,一个简单的SQL SELECT就可以用于所有四种类型的查询。但是,对树结构的更改需要重新编号节点,与检索成本相比,变更的成本相当高。

答案 6 :(得分:1)

如果数据库中有很多树,并且你只能得到整棵树,我会为数据库中的每个节点存储树ID(或根节点ID)和父节点ID,得到所有特定树ID的节点,以及内存中的进程。

但是,如果您将获得子树,则只能获得特定父节点ID的子树,因此您需要存储每个节点的所有父节点以使用上述方法,或者执行多个SQL查询下降到树中(希望树中没有循环!),尽管您可以重用相同的Prepared语句(假设节点属于同一类型并且都存储在单个表中)以防止重新编译SQL,所以它可能不会慢,实际上将数据库优化应用于查询可能更好。可能想要运行一些测试来找出答案。

如果您只存储一棵树,那么您的问题只会成为查询子树的问题之一,并且会应用第二个答案。

答案 7 :(得分:1)

我喜欢存储与其parentID相关联的ID的简单方法:

ID     ParentID
1      null
2      null
3      1
4      2
...    ...

易于维护,并且具有很高的可扩展性。

答案 8 :(得分:1)

谷歌的“物化路径”或“遗传树”......

答案 9 :(得分:1)

在Oracle中,有SELECT ... CONNECT BY语句来检索树。

答案 10 :(得分:0)

This article很有意思,因为它显示了一些检索方法以及将谱系存储为派生列的方法。沿袭提供了一种快捷方法,可以在没有太多连接的情况下检索层次结构。

答案 11 :(得分:0)

不适用于所有情况,但例如给定评论结构:

ID | ParentCommentID

您还可以存储代表最高评论的TopCommentID

ID | ParentCommentID | TopCommentID

TopCommentIDParentCommentIDnull0时,它是最顶级的评论。对于儿童评论,ParentCommentID指向其上方的评论,TopCommentID指向最顶层的父母。