PHP+Mysql树型结构(无限分类)数据库设计的2种方式实例


Posted in PHP onJuly 15, 2014

我们经常需要在关系型数据库中保存一些树状结构数据,比如分类、菜单、论坛帖子树状回复等。常用的方法有两种:

1. 领接表的方式;

2. 预排序遍历树方式;

假设树状结构如下图:

PHP+Mysql树型结构(无限分类)数据库设计的2种方式实例

领接表方式

主要依赖于一个 parent 字段,用于指向上级节点,将相邻的上下级节点连接起来,id 为自动递增自动,parent_id 为上级节点的 id。一目了然,“Java”是“Language”的子节点。

我们要显示树,PHP 代码也可以很直观,代码如下:

<?php

/**

 * 获取父节点下的所有子节点

 *

 * @since 2011-05-18

 * 

 * @param $parent_id 父节点 id,0 则显示整个树结构。

 * @param $level 当前节点所处的层级,用于缩进显示节点。

 * @return void

 */

function show_children ($parent_id = 0, $level = 0)

{

    // 获取父节点下的所有子节点

    $result = mysql_query('SELECT id, name FROM tree WHERE parent_id=' . intval($parent_id));

    // 显示每个子节点

    while ($row = mysql_fetch_array($result)) {

        // 缩进显示

        echo '<div style="margin-left:' . ($level * 12) . 'px">' . $row['name'] . '</div>';

        // 递归调用当前函数,显示再下一级的子节点

        show_children($row['id'], $level + 1);

    }

}

?>

想要显示整个树结构,调用 show_children()。想要显示“Database”子树,则调用 show_children(2),因为“Database”的 id 是 2。

还有一个经常用到的功能是获取节点路径,即给出一个节点,返回从根节点到当前节点的路径。用函数实现如下:

<?php

/**

 * @param $id 需要获取路径的当前节点的 id。

 * @return array

 */

function get_path($id) 

{

    // 获取当前节点的父节点 id 和当前节点名

    $result = mysql_query('SELECT parent_id, name FROM tree WHERE id='.intval($id));

    $row = mysql_fetch_array($result);

    // 使用此数组保存路径

    $path = array();

    // 将当前节点名保存进路径数组中

    $path[] = $row['name'];

    // 如果父节点非 0,即非根节点,则进行递归调用获取父节点的路径

    if ($row['parent_id']) {

        // 递归调用,获取父节点的路径,并且合并到当前路径数组的其它元素前边

        $path = array_merge(get_path($row['parent_id']), $path);

    }

    return $path;

}

?>

想要获取“MySQL 5.0”的路径,调用 get_path(4),4 即是这个节点的 id。

领接表方式的优点在于容易理解,代码也比较简单明了。缺点则是递归中的 SQL 查询会导致负载变大,特别是需要处理比较大型的树状结构的时候,查询语句会随着层级的增加而增加,WEB 应用的瓶颈基本都在数据库方面,所以这是一个比较致命的缺点,直接导致树结构的扩展困难重重。

排序遍历树方式

现在我们来聊聊第二种方式─预排序遍历树方式(即通常所说的 MPTT,Modified Preorder Tree Traversal)。此算法是在第一种方式的基础之上,给每个节点增加一个左、右数字,用于标识节点的遍历顺序,如下图所示:

PHP+Mysql树型结构(无限分类)数据库设计的2种方式实例

从根节点开始左边为 1,然后下一个节点的左边为 2,以此类推,到最低层节点之后,最低层节点的右边为其左边的数字加 1。顺着这些节点,我们可以很容易地遍历完整个树。根据上图,我们对数据表做一些改变,增加两个字段,lft 和 rgt 用于存储左右数字(由于 left 和 right 是 MySQL 的保留字,所以我们改用简写)。表中各行的内容也就变成了:

PHP+Mysql树型结构(无限分类)数据库设计的2种方式实例

接下来看看显示树/子树是多么简单,只需要一条 SQL 语句即可,比如显示“Database”子树,则需要获取到“Database”的左右数字,左为 2,右为 11,那么 SQL 语句是:

SELECT * FROM tree WHERE lft BETWEEN 2 AND 11;

SQL 语句是简单了,但我们所希望的缩进显示却是个问题。什么时候应该显示缩进?缩进多少单位?解决这个问题,需要使用堆栈,即后进先出(LIFO),每到一个节点,将其右边的数字压入堆栈中。我们知道,所有节点右边的值都比其父节点右边的值小,那么将当前节点右边的值和堆栈最上边的右边值进行比较,如果当前节点比堆栈最上边的值小,表示当前堆栈里边剩下的都是父节点了,这时可以显示缩进,堆栈的元素数量即是缩进深度。PHP 代码实现如下:

<?php

/**

 * @param $root_id 需要显示的树/子树根节点 id。

 */

function show_tree($root_id = 1) 

{

    // 获取当前根节点的左右数值

    $result = mysql_query('SELECT lft, rgt FROM tree WHERE id='.intval($root_id));

    $row = mysql_fetch_array($result);

    // 堆栈,存储节点右边的值,用于显示缩进

    $stack = array();

    // 获取 $root_id 节点的所有子孙节点

    $result = mysql_query('SELECT name, lft, rgt FROM tree WHERE lft BETWEEN '.$row['lft'].' AND '.$row['rgt'].' ORDER BY lft ASC');

    // 显示树的每个节点

    while ($row = mysql_fetch_array($result)) {

        if (count($stack)>0) { //仅当堆栈非空的时候检测

            // 如果当前节点右边的值比堆栈最上边的值大,则移除堆栈最上边的值,因为这个值对应的节点不是当前节点的父节点

            while ($row['rgt'] > $stack[count($stack)-1]) {

                array_pop($stack);

            } //while 循环结束之后,堆栈里边只剩下当前节点的父节点了

        }

        // 现在可以显示缩进了

        echo '<div style="margin-left:'.(count($stack)*12).'px">'.$row['name'].'</div>';

        // 将当前的节点压入堆栈里边,为循环后边的节点缩进显示做好准备

        array_push($stack, $row['rgt']);

    }

}

?>

获取整个树调用 show_tree(),获取“Database”子树调用show_tree(2)。在这个函数中,我们总算不需要用到递归了,呵呵。

接下来是显示从根节点到某节点的路径,这比起领接表方式来说也简单了很多,只需要一句 SQL 就行,不用递归  比如获取“ORACLE”这个节点的路径,其左右值分别是 7 和 10,则 SQL 语句为:

SELECT name FROM tree WHERE lft <= 7 AND rgt >= 10 ORDER BY lft ASC;

PHP+Mysql树型结构(无限分类)数据库设计的2种方式实例

PHP 函数实现如下:

<?php

/**

 * @param $node_id 需要获取路径的节点 id

 */

function get_path2($node_id) {

    // 获取当前节点的左右值

    $result = mysql_query('SELECT lft, rgt FROM tree WHERE id='.intval($node_id));

    $row = mysql_fetch_array($result);

    // 获取路径中的所有节点

    $result = mysql_query('SELECT name FROM tree WHERE lft <= '.$row['lft'].' AND rgt >= '.$row['rgt'].' ORDER BY lft ASC');

    $path = array();

    while ($row = mysql_fetch_array($result)) {

        $path[] = $row['name'];

    }

    return $path;

}

?>

显示树和路径都没问题了,现在需要了解一下如何插入一个节点。插入新节点之前,首先要给这个节点腾出空位来,假设我们现在要在“ORACLE 9i”这个节点右边增加一个“ORACLE 10”,则腾位的 SQL 语句如下(“ORACLE 9i”的右边值为 9):

UPDATE tree SET rgt=rgt+2 WHERE rgt>9;

UPDATE tree SET lft=lft+2 WHERE lft>9;

位置空出来了,开始插入新节点吧:

INSERT INTO tree SET lft=10, rgt=11, name='ORACLE 10';

调用 show_tree() 看看结果对不对  具体的 PHP 实现代码这里就不写了。

现在总结一下预排序遍历树方式的优缺点。缺点是算法比较抽象,不容易理解,增加节点的时候虽然只用了几条 SQL 语句,但可能会需要更新很多记录,从而造成阻塞。优点是树的构造,路径获取方面性能都比领接表方式好很多。也就是说,这个算法牺牲了一些写的性能来换取读的性能,在 WEB 应用中,读数据库的比例远大于写数据库的比例,所以预排序遍历树方式比领接表方式更加受欢迎,更加实用,很多应用中都能看到 MPTT 的影子,通常所用的表里都有字段 lft 和 rgt。

PHP 相关文章推荐
修改了一个很不错的php验证码(支持中文)
Feb 14 PHP
实用函数9
Nov 08 PHP
php 动态添加记录
Mar 10 PHP
php 短链接算法收集与分析
Dec 30 PHP
PHP获取客户端真实IP地址的5种情况分析和实现代码
Jul 08 PHP
PHP 读取和编写 XML
Nov 19 PHP
PHP中使用CURL获取页面title例子
Jan 07 PHP
php使用for语句输出三角形的方法
Jun 09 PHP
PHP文件上传操作实例详解
Sep 27 PHP
PHP无限极分类函数的实现方法详解
Apr 15 PHP
PHP children()函数讲解
Feb 03 PHP
imagettftext() 失效,不起作用
Mar 09 PHP
PHP ignore_user_abort函数详细介绍和使用实例
Jul 15 #PHP
Linux下手动编译安装PHP扩展的例子分享
Jul 15 #PHP
可以保证单词完整性的PHP英文字符串截取代码分享
Jul 15 #PHP
PHP魔术引号所带来的安全问题分析
Jul 15 #PHP
PH P5.2至5.5、5.6的新增功能详解
Jul 14 #PHP
PHP文件锁定写入实例解析
Jul 14 #PHP
ThinkPHP惯例配置文件详解
Jul 14 #PHP
You might like
如何跨站抓取别的站点的页面的补充
2006/10/09 PHP
jQuery+php实现ajax文件即时上传的详解
2013/06/17 PHP
php实现文件编码批量转换
2014/03/10 PHP
PHP实现根据数组的值进行分组的方法
2017/04/20 PHP
取得传值的函数
2006/10/27 Javascript
jQuery lazyload 的重复加载错误以及修复方法
2010/11/19 Javascript
检测jQuery.js是否已加载的判断代码
2011/05/20 Javascript
javascript开发技术大全-第3章 js数据类型
2011/07/03 Javascript
jquery中each方法示例和常用选择器
2014/07/08 Javascript
javascript制作的简单注册模块表单验证
2015/04/13 Javascript
WordPress中利用AJAX异步获取评论用户头像的方法
2016/01/08 Javascript
AngularJS 中的指令实践开发指南(一)
2016/03/20 Javascript
Seajs 简易文档 提供简单、极致的模块化开发体验
2016/04/13 Javascript
JavaScript实现相册弹窗功能(zepto.js)
2016/06/21 Javascript
浅谈js和css内联外联注意事项
2016/06/30 Javascript
Vue.js:使用Vue-Router 2实现路由功能介绍
2017/02/22 Javascript
vue 项目地址去掉 #的方法
2018/10/20 Javascript
微信小程序MUI侧滑导航菜单示例(Popup弹出式,左侧滑动,右侧不动)
2019/01/23 Javascript
详解如何使用webpack打包多页jquery项目
2019/02/01 jQuery
google广告之另类js调用实现代码
2020/08/22 Javascript
浅析JavaScript预编译和暗示全局变量
2020/09/03 Javascript
vue实现单一筛选、删除筛选条件
2020/10/26 Javascript
[34:39]DOTA2上海特级锦标赛主赛事日 - 4 败者组第四轮#1COL VS EG第二局
2016/03/05 DOTA
[51:17]Mski vs VGJ.S Supermajor小组赛C组 BO3 第三场 6.3
2018/06/04 DOTA
python爬虫获取淘宝天猫商品详细参数
2020/06/23 Python
python如何去除字符串中不想要的字符
2020/07/05 Python
使用IDLE的Python shell窗口实例详解
2019/11/19 Python
scrapy利用selenium爬取豆瓣阅读的全步骤
2020/09/20 Python
浅析数据存储的三种方式 cookie sessionstorage localstorage 的异同
2020/06/04 HTML / CSS
德国PC硬件网站:CASEKING
2016/10/20 全球购物
非凡女性奢华谦虚风格:The Modist
2017/10/28 全球购物
英国助听器购物网站:Hearing Direct
2018/08/21 全球购物
AMAVII眼镜官网:时尚和设计师太阳镜
2019/05/05 全球购物
酒店前台接待岗位职责
2013/12/03 职场文书
网络舆情信息简报
2015/07/21 职场文书
搞笑Gif:这么白这么长的腿像极了一楼的女朋友
2022/03/21 杂记