`
vtyi
  • 浏览: 83486 次
  • 性别: Icon_minigender_1
  • 来自: 深圳
社区版块
存档分类
最新评论

在数据库中存储层次数据

阅读更多

存储树形结构是一个很常见的问题,他有好几种解决方案。主要有两种方法:邻接列表模型和改进前序遍历树算法

在本文中,我们将探讨这两种保存层次数据的方法。我将举一个在线食品店树形图的例子。这个食品店通过类别、颜色和品种来组织食品。树形图如下:

本文包含了一些代码的例子来演示如何保存和获取数据。我选择PHP [3]来写例子,因为我常用这个语言,而且很多人也都使用或者知道这个语言。你可以很方便地把它们翻译成你自己用的语言。

邻接列表模型(The Adjacency List Model)

我们要尝试的第一个——也是最优美的——方法称为“邻接列表模型”或称为“递归方法”。它是一个很优雅的方法因为你只需要一个简单的方法来在你的树中进行迭代。在我们的食品店中,邻接列表的表格如下:

如你所见,对每个节点保存一个“父”节点。我们可以看到“Pear [4]”是“Green”的一个子节点,而后者又是“Fruit”的子节点,如此类推。根节点,“Food”,则他的父节点没有值。为了简单,我只用了“title”值来标识每个节点。当然,在实际的数据库中,你要使用数字的ID。

显示树

现在我们已经把树放入数据库中了,得写一个显示函数了。这个函数将从根节点开始——没有父节点的节点——同时要显示这个节点所有的子节点。对于这些子节点,函数也要获取并显示这个子节点的子节点。然后,对于他们的子节点,函数还要再显示所有的子节点,然后依次类推。

也许你已经注意到了,这种函数的描述,有一种普遍的模式。我们可以简单地只写一个函数,用来获得特定节点的子节点。这个函数然后要对每个子节点调用自身来再次显示他们的子节点。这就是“递归”机制,因此称这种方法叫“递归方法”。

<?php
// $parent 是我们要查看的子节点的父节点
// $level 会随着我们深入树的结构而不断增加,
//        用来显示一个清晰的缩进格式
function display_children($parent, $level) {
   // 获取$parent的全部子节点
   $result = mysql_query('SELECT title FROM tree '.
                          'WHERE parent="'.$parent.'";');

   // 显示每个节点
   while ($row = mysql_fetch_array($result)) {
       // 缩进并显示他的子节点的标题
       echo str_repeat('  ',$level).$row['title']."\n";

       // 再次调用这个函数来显着这个子节点的子节点
       display_children($row['title'], $level+1);
   }
}
?>

要实现整个树,我们只要调用函数时用一个空字符串作为$parent 和$level = 0: display_children('',0); 函数返回了我们的食品店的树状图如下:

Food
Fruit
Red
Cherry
Yellow
Banana
Meat
Beef
Pork

注意如果你只想看一个子树,你可以告诉函数从另一个节点开始。例如,要显示“Fruit”子树,你只要display_children('Fruit',0);

The Path to a Node节点的路径

利用差不多的函数,我们也可以查询某个节点的路径如果你只知道这个节点的名字或者ID。例如,“Cherry”的路径是“Food”> “Fruit”>“Red”。要获得这个路径,我们的函数要获得这个路径,这个函数必须从最深的层次开始:“Cheery”。但后查找这个节点的父 节点,并添加到路径中。在我们的例子中,这个父节点是“Red”。如果我们知道“Red”是“Cherry”的父节点。

<?php
// $node 是我们要查找路径的那个节点的名字
function get_path($node) {
   // 查找这个节点的父节点
   $result = mysql_query('SELECT parent FROM tree '.
                          'WHERE title="'.$node.'";');
   $row = mysql_fetch_array($result);

   // 在这个array [5] 中保存数组
   $path = array();

   // 如果 $node 不是根节点,那么继续
   if ($row['parent']!='') {
       // $node 的路径的最后一部分是$node父节点的名称
       $path[] = $row['parent'];

       // 我们要添加这个节点的父节点的路径到现在这个路径
       $path = array_merge(get_path($row['parent']), $path);
   }

   // 返回路径
   return $path;
}
?>

这个函数现在返回了指定节点的路径。他把路径作为数组返回,这样我们可以使用print_r(get_path('Cherry')); 来显示,其结果是:

Array
(
   [0] => Food
   [1] => Fruit
   [2] => Red
)

不足

正如我们所见,这确实是一个很好的方法。他很容易理解,同时代码也很简单。但是邻接列表模型的缺点在哪里呢?在大多数编程语言中,他运行很慢,效率很差。这主要是“递归”造成的。我们每次查询节点都要访问数据库。

每次数据库查询都要花费一些时间,这让函数处理庞大的树时会十分慢。

造成这个函数不是太快的第二个原因可能是你使用的语言。不像Lisp这类语言,大多数语言不是针对递归函数设计的。对于每个节点,函数都要调用他自 己,产生新的实例。这样,对于一个4层的树,你可能同时要运行4个函数副本。对于每个函数都要占用一块内存并且需要一定的时间初始化,这样处理大树时递归 就很慢了。

改进前序遍历树

现在,让我们看另一种存储树的方法。递归可能会很慢,所以我们就尽量不使用递归函数。我们也想尽量减少数据库查询的次数。最好是每次只需要查询一次。

我们先把树按照水平方式摆开。从根节点开始(“Food”),然后他的左边写上1。然后按照树的顺序(从上到下)给“Fruit”的左边写上2。这 样,你沿着树的边界走啊走(这就是“遍历”),然后同时在每个节点的左边和右边写上数字。最后,我们回到了根节点“Food”在右边写上18。下面是标上 了数字的树,同时把遍历的顺序用箭头标出来了。

我们称这些数字为左值和右值(如,“Food”的左值是1,右值是18)。正如你所见,这些数字按时了每个节点之间的关系。因为“Red”有3和6 两个值,所以,它是有拥有1-18值的“Food”节点的后续。同样的,我们可以推断所有左值大于2并且右值小于11的节点,都是有2-11的 “Food”节点的后续。这样,树的结构就通过左值和右值储存下来了。这种数遍整棵树算节点的方法叫做“改进前序遍历树”算法。

在继续前,我们先看看我们的表格里的这些值:

注意单词“left”和“right”在SQL中有特殊的含义。因此,我们只能用“lft”和“rgt”来表示这两个列。(译注——其实Mysql 中可以用“`”来表示,如“`left`”,MSSQL中可以用“[]”括出,如“[left]”,这样就不会和关键词冲突了。)同样注意这里我们已经不 需要“parent”列了。我们只需要使用lft和rgt就可以存储树的结构。

获取树

如果你要通过左值和右值来显示这个树的话,你要首先标识出你要获取的那些节点。例如,如果你想获得“Fruit”子树,你要选择那些左值在2到11的节点。用SQL语句表达:

SELECT * FROM tree WHERE lft BETWEEN 2 AND 11;

这个会返回:

好吧,现在整个树都在一个查询中了。现在就要像前面的递归函数那样显示这个树,我们要加入一个ORDER BY子句在这个查询中。如果你从表中添加和删除行,你的表可能就顺序不对了,我们因此需要按照他们的左值来进行排序。

SELECT * FROM tree WHERE lft BETWEEN 2 AND 11 ORDER BY lft ASC;

就只剩下缩进的问题了。

要显示树状结构,子节点应该比他们的父节点稍微缩进一些。我们可以通过保存一个右值的一个栈。每次你从一个节点的子节点开始时,你把这个节点的右值 添加到栈中。你也知道子节点的右值都比父节点的右值小,这样通过比较当前节点和栈中的前一个节点的右值,你可以判断你是不是在显示这个父节点的子节点。当 你显示完这个节点,你就要把他的右值从栈中删除。要获得当前节点的层数,只要数一下栈中的元素。

<?php
function display_tree($root) {
   // 获得$root节点的左边和右边的值
   $result = mysql_query('SELECT lft, rgt FROM tree '.
                          'WHERE title="'.$root.'";');
   $row = mysql_fetch_array($result);

   // 以一个空的$right栈开始
   $right = array();

   // 现在,获得$root节点的所有后序
   $result = mysql_query('SELECT title, lft, rgt FROM tree '.
                          'WHERE lft BETWEEN '.$row['lft'].' AND '.
                          $row['rgt'].' ORDER BY lft ASC;');

   // 显示每一行
  while ($row = mysql_fetch_array($result)) {
    // 检查栈里面有没有元素
    if (count($right)>0) {
      // 检查我们是否需要从栈中删除一个节点
      while ($right[count($right)-1]<$row['rgt']) {
        array_pop($right);
      }
    }

    // 显示缩进的节点标题
    echo str_repeat('  ',count($right)).$row['title']."\n";

    // 把这个节点添加到栈中
    $right[] = $row['rgt'];
  }
}
?>

如果运行这段代码,你可以获得和上一部分讨论的递归函数一样的结果。而这个函数可能会更快一点:他不采用递归而且只是用了两个查询

节点的路径

有了新的算法,我们还要另找一种新的方法来获得指定节点的路径。这样,我们就需要这个节点的祖先的一个列表。

由于新的表结构,这不需要花太多功夫。你可以看一下,例如,4-5的“Cherry”节点,你会发现祖先的左值都小于4,同时右值都大于5。这样,我们就可以使用下面这个查询:

SELECT title FROM tree WHERE lft < 4 AND rgt > 5 ORDER BY lft ASC;

注意,就像前面的查询一样,我们必须使用一个ORDER BY子句来对节点排序。这个查询将返回:

+-------+
| title |
+-------+
| Food  |
| Fruit |
| Red   |
+-------+

我们现在只要把各行连起来,就可以得到“Cherry”的路径了。

有多少个后续节点?How Many Descendants

如果你给我一个节点的左值和右值,我就可以告诉你他有多少个后续节点,只要利用一点点数学知识。

因为每个后续节点依次会对这个节点的右值增加2,所以后续节点的数量可以这样计算:

descendants = (right – left - 1) / 2

利用这个简单的公式,我可以立刻告诉你2-11的“Fruit”节点有4个后续节点,8-9的“Banana”节点只是1个子节点,而不是父节点。

自动化树遍历

现在你对这个表做一些事情,我们应该学习如何自动的建立表了。这是一个不错的练习,首先用一个小的树,我们也需要一个脚本来帮我们完成对节点的计数。

让我们先写一个脚本用来把一个邻接列表转换成前序遍历树表格。

<?php
function rebuild_tree($parent, $left) {
   // 这个节点的右值是左值加1
   $right = $left+1;

   // 获得这个节点的所有子节点
   $result = mysql_query('SELECT title FROM tree '.
                          'WHERE parent="'.$parent.'";');
   while ($row = mysql_fetch_array($result)) {
       // 对当前节点的每个子节点递归执行这个函数
       // $right 是当前的右值,它会被rebuild_tree函数增加
       $right = rebuild_tree($row['title'], $right);
   }

   // 我们得到了左值,同时现在我们已经处理这个节点我们知道右值的子节点
   mysql_query('UPDATE tree SET lft='.$left.', rgt='.
                $right.' WHERE title="'.$parent.'";');

   // 返回该节点的右值+1
   return $right+1;
}
?>

这是一个递归函数。你要从rebuild_tree('Food',1); 开始,这个函数就会获取所有的“Food”节点的子节点。

如果没有子节点,他就直接设置它的左值和右值。左值已经给出了,1,右值则是左值加1。如果有子节点,函数重复并且返回最后一个右值。这个右值用来作为“Food”的右值。

递归让这个函数有点复杂难于理解。然而,这个函数确实得到了同样的结果。他沿着树走,添加每一个他看见的节点。你运行了这个函数之后,你会发现左值和右值和预期的是一样的(一个快速检验的方法:根节点的右值应该是节点数量的两倍)。

添加一个节点

我们如何给这棵树添加一个节点?有两种方式:在表中保留“parent”列并且重新运行rebuild_tree() 函数——一个很简单但却不是很优雅的函数;或者你可以更新所有新节点右边的节点的左值和右值。

第一个想法比较简单。你使用邻接列表方法来更新,同时使用改进前序遍历树来查询。如果你想添加一个新的节点,你只需要把节点插入表格,并且设置好parent列。然后,你只需要重新运行rebuild_tree() 函数。这做起来很简单,但是对大的树效率不高。

第二种添加和删除节点的方法是更新新节点右边的所有节点。让我们看一下例子。我们要添加一种新的水果——“Strawberry”,作为“Red” 的最后一个子节点。首先,我们要腾出一个空间。“Red”的右值要从6变成8,7-10的“Yellow”节点要变成9-12,如此类推。更新“Red” 节点意味着我们要把所有左值和右值大于5的节点加上2。

我们用一下查询:

UPDATE tree SET rgt=rgt+2 WHERE rgt>5;
UPDATE tree SET lft=lft+2 WHERE lft>5;

现在我们可以添加一个新的节点“Strawberry”来填补这个新的空间。这个节点左值为6右值为7。

INSERT INTO tree SET lft=6, rgt=7, title='Strawberry';

如果我们运行display_tree() 函数,我们将发现我们新的“Strawberry”节点已经成功地插入了树中:

Food
 Fruit
   Red
     Cherry
     Strawberry
   Yellow
     Banana
 Meat
   Beef
   Pork

缺点

首先,改进前序遍历树算法看上去很难理解。它当然没有邻接列表方法简单。然而,一旦你习惯了左值和右值这两个属性,他就会变得清晰起来,你可以用这 个技术来完成临街列表能完成的所有事情,同时改进前序遍历树算法更快。当然,更新树需要很多查询,要慢一点,但是取得节点却可以只用一个查询。

总结

你现在已经对两种在数据库存储树方式熟悉了吧。虽然在我这儿改进前序遍历树算法性能更好,但是也许在你特殊的情况下邻接列表方法可能表现更好一些。这个就留给你自己决定了

最后一点:就像我已经说得我部推荐你使用节点的标题来引用这个节点。你应该遵循数据库标准化的基本规则。我没有使用数字标识是因为用了之后例子就比较难读。

分享到:
评论

相关推荐

    关系数据库表存储树形结构的方法

    多数据库表实现方法将树的不同层次数据分别存储在不同的表中。每个表只存储同一层的数据,而一个头部表存放各层表的名称和层数。每个记录除了有唯一编号外,非首层表的记录还会增加一个字段存放父节点的编号。 在...

    如何展开存储在数据库中的树形数据结构.pdf

    通过对这些方法的了解和应用,开发者可以更加高效地管理和查询存储在数据库中的树形数据结构,从而在实际工作中提升数据处理的能力和效率。同时,这些方法也能够帮助开发者深入理解树形数据结构的特点和遍历算法的...

    数据库存储层级结构数据源码

    数据库存储层级结构数据源码 将分层数据(层级结构)存储在数据库中。除非你使用一个类似xml的数据库,通用的关系型数据库是很难做到这一点。关系型数据库中的表不分层;他们只是一个简单列表。你必须...

    层次,网状,关系数据库

    数据库是存储和管理数据的重要工具,对于初学者来说,理解其基本类型——层次数据库、网状数据库和关系数据库,是掌握数据库系统的关键。这三种类型的数据库各有特点,适应不同的应用场景。 首先,层次数据库以树形...

    oracle数据库、表空间及数据文件之间的关系

    ### Oracle数据库、表空间及数据文件之间的关系 ...这种层次结构不仅有助于更好地组织和管理数据,还使得在扩展存储容量时变得更加灵活。对于DBA来说,理解这些组件之间的相互作用对于确保数据库的高性能运行至关重要。

    数据库原理基础(数据库管理系统 数据库系统 数据模型 )

    在实际应用中,设计者会根据业务需求选择合适的数据模型,然后转化为具体的DBMS支持的数据库模式。 总的来说,数据库原理基础涵盖了从数据管理的早期形态到现代高效数据库系统的演变,以及数据库管理系统如何通过...

    省市区域数据库设计、及数据脚本

    2. **层次结构**:中国行政区域有明显的层次结构,省-市-区县-乡镇,这种层次关系需要在数据库中体现,一般通过父ID字段来关联上级与下级行政区划。 3. **代码标准化**:为了方便管理和查询,通常会采用国家规定的...

    解析PHP无限级分类方法及代码

    无论你要构建自己的论坛,在你的网站上发布消息还是书写自己的CMS程序,你都会遇到要在数据库中存储层次数据的情况。同时,除非你使用一种像XML的数据库,否则关系数据库中的表都不是层次结构的,他们只是一个平坦的...

    关系数据库中树形数据结构的处理.pdf

    关系数据库中树形数据结构的处理,即利用关系数据库来处理具有层级和从属关系的数据,如商业领域的多层次传销系统的开发。传统的关系数据库设计是面向二维表的,对于树形数据结构的处理需要进行特殊的处理方式,以便...

    数据库与数据仓库数据库与数据仓库.ppt

    在数据库管理中,数据冗余是需要避免的问题,因为它可能导致数据不一致。数据完整性规则(如主键和外键约束)用于确保数据的准确性和一致性。数据的物理存储和逻辑结构是分离的,增强了数据的独立性,允许数据的物理...

    2023年数据库基础与应用练习题及答案.doc

    在数据库系统中,数据是结构化的,可以共享,具有独立性,数据粒度小,且具有独立的数据操作界面。 数据库设计 数据库设计需要考虑到数据的存储、查询、修改等操作。在设计数据库时,需要告诉数据库管理系统做什么...

    MySQL数据库:数据模型的应用层次.pptx

    本篇将深入探讨数据模型的应用层次及其在MySQL数据库中的重要性。 首先,我们要理解数据模型的基本概念。数据模型是描述数据结构、数据操作和数据约束的框架,它是数据库系统的基础。数据模型不仅包含数据的逻辑...

    学生选课系统数据库设计数据流图(超级详细包括前导课程判断)

    在这个场景中,我们关注的是一个“学生选课系统”的数据库设计,它涉及到数据流图(DFD)的创建,这是一种用于可视化系统数据处理流程的工具。本文将深入探讨数据流图在系统设计中的应用,特别是如何在学生选课系统...

    行业类别数据字典,数据库

    在这个压缩包文件中,我们有两个关键的资源:一个Excel源文件和一个数据库SQL语句,它们共同构成了对国民经济行业分类的详细描述。 首先,让我们来了解一下“国民经济行业分类(GB/T 4754-2011)”。这是中国国家质量...

    数据库和数据仓库管理教材.pptx

    在数据管理的传统方法中,数据通常被组织成层次结构,包括位、字节、字段、记录、文件和数据库。位是计算机存储的基本单位,字节通常由8个位组成,用于表示字符。字段是组成记录的元素,记录则由相关字段组成,形成...

    数据库应用技术:数据库的数据模型.pptx

    1. 层次型数据模型:层次数据模型是用树状结构来组织数据的数据模型。层次数据库系统的典型代表是IBM公司的IMS(Information Management System)数据库管理系统。 2. 网状型数据模型:网状数据模型是用网络结构...

Global site tag (gtag.js) - Google Analytics