【问题标题】:How do you sort a tree stored using the nested set model?如何对使用嵌套集模型存储的树进行排序?
【发布时间】:2010-09-17 03:08:50
【问题描述】:

当我提到嵌套集模型时,我指的是 here. 所描述的内容

我需要构建一个新系统,用于在用户定义的层次结构中存储“类别”(我想不出更好的词来形容它)。由于嵌套集模型针对读取而不是写入进行了优化,因此我决定使用它。不幸的是,在我对嵌套集的研究和测试过程中,我遇到了如何显示带有排序节点的层次树的问题。例如,如果我有层次结构:

root
    finances
        budgeting
            fy08
    projects
        research
        fabrication
        release
    trash

我希望对其进行排序,使其显示为:

root
    finances
        budgeting
            fy08
    projects
        fabrication
        release
        research
    trash

请注意,制造出现在研究之前。

无论如何,经过长时间的搜索,我看到了诸如“将树存储在多维数组中并对其进行排序”和“对树进行排序并序列化回您的嵌套集合模型”之类的答案(我在解释... )。无论哪种方式,第一个解决方案都是对 RAM 和 CPU 的可怕浪费,它们都是非常有限的资源......第二个解决方案看起来就像很多痛苦的代码。

无论如何,我能够弄清楚如何(使用嵌套集模型):

  1. 在 SQL 中创建新树
  2. 插入一个节点作为树中另一个节点的子节点
  3. 在树中的兄弟节点之后插入一个节点
  4. 从 SQL 中拉出具有层次结构的整个树
  5. 从层次结构中的特定节点(包括根节点)拉取子树,有或没有深度限制
  6. 查找树中任意节点的父节点

所以我想 #5 和 #6 可以用来做我想要的排序,它也可以用来按排序顺序重建树。

但是,既然我已经查看了我所学做的所有这些事情,我发现 #3、#5 和 #6 可以一起用于执行排序插入。如果我做了排序插入,它总是被排序。但是,如果我改变了排序标准或者我想要一个不同的排序顺序,我会回到原点。

这可能只是嵌套集合模型的限制吗?它在输出的查询排序中的使用是否受到抑制?

【问题讨论】:

    标签: database tree hierarchy nested-sets


    【解决方案1】:

    我认为这确实是嵌套集合模型的一个局限。您不能轻松地在它们各自的父节点中对子节点进行排序,因为结果集的排序对于重构树结构至关重要。

    我认为这可能是在插入、更新或删除节点时保持树排序的最佳方法。这甚至使查询非常快,这是该数据结构的主要目标之一。如果对所有操作都实现了存储过程,那就非常好用了。

    您还可以反转预排序树的排序顺序。你只需要使用ORDER BY node.rgt DESC 而不是ORDER BY node.lft ASC

    如果您确实需要支持其他排序标准,您可以通过向每个节点添加第二个 lftrgt 索引来实现它,并在每次插入/更新/删除时保持按其他标准排序。

    【讨论】:

      【解决方案2】:

      我经常使用嵌套集,并且经常遇到同样的问题。我所做的以及我会推荐的只是不对数据库中的项目进行排序。而是在用户界面中对它们进行排序。从数据库中提取所有节点后,无论如何,您可能必须将它们转换为某种分层数据结构。在该结构中,对包含该节点子节点的所有数组进行排序。

      例如,如果您的前端是 Flex 应用程序,并且节点的子节点存储在 ICollectionView 中,您可以使用 sort 属性让它们以您想要的方式显示。

      另一个例子,如果你的前端是 PHP 脚本的一些输出,你可以将每个节点的子节点放在一个数组中,并使用 PHP 的数组排序函数来执行排序。

      当然,这仅在您不需要对实际的数据库条目进行排序时才有效,但是您会这样做吗?

      【讨论】:

        【解决方案3】:

        我刚刚完成了以下对我排序整个嵌套集合树的工作。

        排序(理想情况下)需要一个列出树中每个节点的当前级别的视图和交换两个节点的过程 - 两者都包含在下面,兄弟交换代码来自 Joe Celkos 的 Tree & Hierarchies 书中我强烈推荐给任何使用嵌套集的人。

        可以在“INSERT INTO @t”语句中更改排序,这里是对“名称”的简单字母数字排序

        这可能是一种糟糕的方法,尤其是使用光标来处理基于集合的代码,但正如我所说的那样,它对我有用,希望它有所帮助。

        更新:

        下面的代码现在显示版本而不使用光标。我看到大约 10 倍的速度提升

        CREATE VIEW dbo.tree_view
        
        AS
        
        SELECT t2.NodeID,t2.lft,t2.rgt ,t2.Name, COUNT(t1.NodeID) AS level  
        FROM dbo.tree t1,dbo.tree t2
        WHERE t2.lft BETWEEN t1.lft AND t1.rgt
        GROUP BY t2.NodeID,t2.lft,t2.rgt,t2.Name
        
        GO
        
        ----------------------------------------------
        
          DECLARE @CurrentNodeID int
        DECLARE @CurrentActualOrder int
        DECLARE @CurrentRequiredOrder int
        DECLARE @DestinationNodeID int
        DECLARE @i0 int
        DECLARE @i1 int
        DECLARE @i2 int
        DECLARE @i3 int
        
        DECLARE @t TABLE (TopLft int,NodeID int NOT NULL,lft int NOT NULL,rgt int NOT NULL,Name varchar(50),RequiredOrder int NOT NULL,ActualOrder int NOT NULL)
        
        
        INSERT INTO @t (toplft,NodeID,lft,rgt,Name,RequiredOrder,ActualOrder)
            SELECT tv2.lft,tv1.NodeID,tv1.lft,tv1.rgt,tv1.Name,ROW_NUMBER() OVER(PARTITION BY tv2.lft ORDER BY tv1.ColumnToSort),ROW_NUMBER() OVER(PARTITION BY tv2.lft ORDER BY tv1.lft ASC)
            FROM dbo.tree_view tv1 
            LEFT OUTER JOIN dbo.tree_view tv2 ON tv1.lft > tv2.lft and tv1.lft < tv2.rgt and tv1.level = tv2.level+1
            WHERE tv2.rgt > tv2.lft+1
        
            DELETE FROM @t where ActualOrder = RequiredOrder
        
        
        WHILE EXISTS(SELECT * FROM @t WHERE ActualOrder <> RequiredOrder)
        BEGIN
        
        
            SELECT Top 1 @CurrentNodeID = NodeID,@CurrentActualOrder = ActualOrder,@CurrentRequiredOrder = RequiredOrder
            FROM @t 
            WHERE ActualOrder <> RequiredOrder
            ORDER BY toplft,requiredorder
        
            SELECT @DestinationNodeID = NodeID
            FROM @t WHERE ActualOrder = @CurrentRequiredOrder AND TopLft = (SELECT TopLft FROM @t WHERE NodeID = @CurrentNodeID) 
        
            SELECT @i0 = CASE WHEN c.lft < d.lft THEN c.lft ELSE d.lft END,
                    @i1 =  CASE WHEN c.lft < d.lft THEN c.rgt ELSE d.rgt END,
                    @i2 =  CASE WHEN c.lft < d.lft THEN d.lft ELSE c.lft END,
                    @i3 =  CASE WHEN c.lft < d.lft THEN d.rgt ELSE c.rgt END
            FROM dbo.tree c
            CROSS JOIN dbo.tree d
            WHERE c.NodeID = @CurrentNodeID AND d.NodeID = @DestinationNodeID
        
            UPDATE dbo.tree
            SET lft = CASE  WHEN lft BETWEEN @i0 AND @i1 THEN @i3 + lft - @i1
                            WHEN lft BETWEEN @i2 AND @i3 THEN @i0 + lft - @i2
                    ELSE @i0 + @i3 + lft - @i1 - @i2
                    END,
                rgt = CASE  WHEN rgt BETWEEN @i0 AND @i1 THEN @i3 + rgt - @i1
                            WHEN rgt BETWEEN @i2 AND @i3 THEN @i0 + rgt - @i2
                    ELSE @i0 + @i3 + rgt - @i1 - @i2
                    END
            WHERE lft BETWEEN @i0 AND @i3 
            AND @i0 < @i1
            AND @i1 < @i2
            AND @i2 < @i3
        
            UPDATE @t SET actualorder = @CurrentRequiredOrder where NodeID = @CurrentNodeID
            UPDATE @t SET actualorder = @CurrentActualOrder where NodeID = @DestinationNodeID
        
            DELETE FROM @t where ActualOrder = RequiredOrder
        
        END
        

        【讨论】:

          【解决方案4】:

          是的,这是嵌套集模型的限制,因为嵌套集是层次结构的预排序表示。这种预购是阅读速度如此之快的原因。 邻接模型(也在您链接到的页面上进行了描述)提供了最灵活的排序和过滤,但对性能有显着影响。

          对于嵌套集合中的插入和移动,我首选的方法是像在邻接模型中一样处理受影响的分支:获取新兄弟的列表;在列表中找到新节点的正确位置;并构造所需的更新语句(这是您真正需要小心的地方)。至于改变你的订购标准:这是一个一次性的工作,所以你可以负担得起一些 RAM 和 CPU,最灵活的答案是将嵌套集表示分解为邻接表示并重建嵌套集基于新标准的邻接。

          【讨论】:

            【解决方案5】:

            我相信,在您想要交换的节点没有任何后代的情况下,您可以简单地交换 lft 和 rgt 值。考虑这棵树:

               A
             /   \
            B     C
                 / \
                D   E
            

            这可以变成这组嵌套集合:

            1 A 10 
            2 B 3  
            4 C 9
            5 D 6
            7 E 8
            

            现在考虑你想交换 D 和 E。以下嵌套集是有效的,并且 D 和 E 被交换:

            1 A 10
            2 B 3 
            4 C 9 
            7 D 8
            5 E 6 
            

            当然,不能以这种方式交换具有子树的节点,因为您还需要更新子节点的 lft 和 rgt 值。

            【讨论】:

              【解决方案6】:

              您可以在渲染时对其进行排序。我在这里解释了渲染How to render all records from a nested set into a real html tree

              【讨论】:

                【解决方案7】:

                从我的课堂方法中查看我的简单解决方案。 $this->table->order 是从 DB 获取数据的 Nette 框架代码。

                $tree = Array();
                $parents = Array();
                $nodes = $this->table->order('depth ASC, parent_id ASC, name ASC');
                $i = 0;
                $depth = 0;
                $parent_id = 0;
                
                foreach($nodes as $node) {
                    if($depth < $node->depth || $parent_id < $node->parent_id) {
                        $i = $parents["{$node->parent_id}"] + 1;
                    }
                    $tree[$i] = $node;
                    $parents["{$node->id}"] = $i;
                    $depth = $node->depth;
                    $parent_id = $node->parent_id;
                    $i += (($node->rgt - $node->lft - 1) / 2) + 1;
                }
                ksort($tree);
                

                【讨论】:

                  【解决方案8】:

                  对嵌套集进行排序没有限制,也不难。只需按左边的凉亭(锚,随便)排序,就完成了。如果每个节点都有一个 LEVEL,您还可以根据 Level 拉出正确的缩进。

                  【讨论】:

                  • 我认为问题是如何根据left 以外的其他标准(例如,姓名)对层次结构的每个级别的子项进行排序。
                  • 这就是我想要表达的真正意义(我会用 -1 来实现它;-)。甚至 Justin 的优秀解决方案仍然使用 While 循环,它仍然是一个没有 CURSOR 字样的游标。这一切的关键是最初以正确的顺序构建嵌套集。我可以发布几个链接,说明如何以足够的速度正确执行此操作,以便您可以轻松地进行任何更改,但我可能只是因为发布 URL 而不是像我曾经拥有的代码那样被抨击。 ;-)
                  猜你喜欢
                  • 2021-02-24
                  • 1970-01-01
                  • 1970-01-01
                  • 2016-05-02
                  • 1970-01-01
                  • 2018-10-22
                  • 2023-03-16
                  • 1970-01-01
                  • 1970-01-01
                  相关资源
                  最近更新 更多