树形结构!别再用递归实现了这才是最佳的方案;更快更强更好用

本文探讨了在开发中遇到树形结构需求时,如何设计数据库方案。首先介绍了常见的父子关系方案,通过父ID递归查询子节点,但当数据量大、层级深时性能不佳。然后提出改进后的先序树方案,利用左值和右值高效查询、统计。该方案虽然结构复杂,但在查询性能上有显著优势。最后,详细对比了两种方案的优缺点,适用于不同的业务场景。
摘要由CSDN通过智能技术生成

不管你是做前端还是后端的开发,那我相信树形结构的需求一定有遇到过,特别是管理平台类型的项目,一般都会有一个树形结构的菜单栏,再比如说,公司组织架构层级关系归属关系等等需求,本质上都是树形结构的一种体现;

遇到这种需求,最常见也最容易想到的设计思路就是:父子关系的方式,子项通过一个字段来保存他的父ID,然后通过递归的方式得到层级关系;

前几天,技术交流群里面有小伙伴在问,实际的业务中,树形结构的数据太多、层级还深,查询过程一顿递归之后,性能表现得比较差,问有没有什么好的设计思路,让查询、统计更加的便捷高效;

今天就给大家介绍一种新的树形结构设计方案:改进后的先序树方式,在查询、统计方面的优势,要远大于父子关系的递归设计方案;

本文就来详细的讲解并对比一下两个方案的实现方式以及优缺点。

文章目录:

对于树形结构的需求,不管是采用什么方式,要处理的问题都是差不多的,下面先列举一下树形结构常见的问题点有哪些:

  • 节点的增删改
  • 是否存在子节点(叶子节点)
  • 查询出所有的子节点
  • 查询所有的节点
  • 查询所有的子孙节点
  • 父节点查询
  • 祖先节点查询
  • 统计所有子孙部门的数量

针对上面的这些问题,就以一个简单的公司组织架构示例,一起来看看,两种方案都是如何实现和解决的?

本文所有的示例都是采用的MySQL+Java实现,核心思路都类似,实际使用,可根据语言特性以及自己习惯的方式调整即可。

1父子关系方案

父子关系,顾名思义,就是当前节点只关注自己的父节点是谁,并将其保存起来即可,查询我的子节点有哪些,只需要全局找到所有父ID是和我的ID一致的项;

如下图所示:

方案特点

  • 优点
    • 方案简单易懂
    • 数据结构简单清晰
    • 层级直观、鲜明
    • 易维护层级关系只需要关注自己的父ID,所以在添加、修改的时候,一旦关系发生变化,调整对应的父ID即可。
  • 缺点
    • 查找麻烦、统计麻烦根据当前节点的数据,只能获取到子节点的数据,一旦查询、统计超出父子范围,就只能通过递归逐层查找了;

示例

根据上面的图示示例,与其对应的表结构如下:

IDdep_name(部门名称)level(层级)parent_id(父ID)1董事会102总经理213董事会秘书214产品部325行政总监326设计部447技术部448财务部459行政部4510客户端5711服务端57

SQL脚本:

DROP TABLE IF EXISTS `department_info`;
CREATE TABLE `department_info`  (
  `id` int(11) NOT NULL,
  `dep_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '名称',
  `level` int(11) NULL DEFAULT NULL,
  `parent_id` int(11) NULL DEFAULT NULL COMMENT '父ID',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;

INSERT INTO `department_info` VALUES (1, '董事会', 1, 0);
INSERT INTO `department_info` VALUES (2, '总经理', 2, 1);
INSERT INTO `department_info` VALUES (3, '董事会秘书', 2, 1);
INSERT INTO `department_info` VALUES (4, '产品部', 3, 2);
INSERT INTO `department_info` VALUES (5, '行政总监', 3, 2);
INSERT INTO `department_info` VALUES (6, '设计部', 4, 4);
INSERT INTO `department_info` VALUES (7, '技术部', 4, 4);
INSERT INTO `department_info` VALUES (8, '财务部', 4, 5);
INSERT INTO `department_info` VALUES (9, '行政部', 4, 5);
INSERT INTO `department_info` VALUES (10, '客户端', 5, 7);
INSERT INTO `department_info` VALUES (11, '服务端', 5, 7);

函数的创建

由于父子节点的查询,需要依赖递归,为了查询方便,这里创建两个函数:

  • 递归查询子孙节点ID的函数DROP FUNCTION IF EXISTS queryChildrenDepInfo;
    DELIMITER ;;CREATE FUNCTION queryChildrenDepInfo(dep_id INT)
    RETURNS VARCHAR(4000)
    BEGIN
    DECLARE sTemp VARCHAR(4000);
    DECLARE sTempChd VARCHAR(4000);
    SET sTemp='$';
    SET sTempChd = CAST(dep_id AS CHAR);
    WHILE sTempChd IS NOT NULL DO
    SET sTemp= CONCAT(sTemp,',',sTempChd);
    SELECT GROUP_CONCAT(id) INTO sTempChd FROM department_info WHERE FIND_IN_SET(parent_id,sTempChd)>0;
    END WHILE;
    RETURN sTemp;END
    ;;DELIMITER ;测试:查询技术部下的所有重要节点?SELECT queryChildrenDepInfo(4);
    SELECT * FROM department_info WHERE FIND_IN_SET(id,queryChildrenDepInfo(4));
  • 递归查询祖先节点ID的函数DROP FUNCTION IF EXISTS queryParentDepInfo;
    DELIMITER;;CREATE FUNCTION queryParentDepInfo(dep_id INT)
    RETURNS VARCHAR(4000)
    BEGIN
    DECLARE sTemp VARCHAR(4000);
    DECLARE sTempChd VARCHAR(4000);
    SET sTemp='$';
    SET sTempChd = CAST(dep_id AS CHAR);
    SET sTemp = CONCAT(sTemp,',',sTempChd);
    SELECT parent_id INTO sTempChd FROM department_info WHERE id = sTempChd;
    WHILE sTempChd <> 0 DO
    SET sTemp = CONCAT(sTemp,',',sTempChd);
    SELECT parent_id INTO sTempChd FROM department_info WHERE id = sTempChd;
    END WHILE;
    RETURN sTemp;END
    ;;DELIMITER ;测试:查询技术部所有的祖先节点?SELECT queryParentDepInfo(7);
    SELECT * FROM department_info WHERE FIND_IN_SET(id,queryParentDepInfo(7));

节点的增删改

  • 增加节点比如要在技术部下添加一个测试部门INSERT INTO department_info(`id`, `dep_name`, `level`, `parent_id`) VALUES (12, '测试部', 5, 7);
  • 修改节点比如:将测试部(ID = 12)提出来,放到产品部(ID = 4)下;就只需要将测试部对应的父节点ID修改为4即可SET @id = 12;
    SET @pid = 4;
    UPDATE department_info SET `parent_id` = @pid WHERE `id` = @id;
  • 删除节点删除相比于添加修改,情况就会特殊一些,如果删除的节点存在子节点,意味着子节点也需要同步删除掉;因此这里就需要用到上面创建的递归查询子孙节点ID的函数(queryChildrenDepInfo)比如:删除技术部门;DELETE FROM department_info WHERE FIND_IN_SET(id,queryChildrenDepInfo(7));

是否存在子节点(叶子节点)

在该方案下,要想判断是否是叶子节点,有两种实现方式:

  • 统计当前节点以及子孙节点的数量递归查询所有子节点的ID,并统计数量,由于函数查询包含了节点自身,所以这里使用了COUNT(*)-1来计算子节点的数量,如果等于0就是叶子节点,大于0说明不是叶子节点;-- 查看设计部(ID=6)是不是叶子节点
    SET @id = 6;
    -- 由于数量包含了自身,由于统计的是子节点的数量,所以这里需要-1将自己去掉
    SELECT COUNT(*)-1 FROM department_info WHERE FIND_IN_SET(id,queryChildrenDepInfo(@id));
  • 添加叶子节点的标记在表中添加一个isLeaf字段,当节点增删改操作的
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值