超赞,老外的一种避免递归查询所有子部门的树数据表设计与实现!

| 目录

  • 问题来了

  • 查出所有子孙部门

  • 查询子孙部门总数

  • 判断是否叶子节点

  • 要不试试这个方法?

  • 查出所有子孙部门

  • 查询子孙部门总数

  • 判断是否叶子节点

  • 其他基本操作

  • 完结


通常树形结构的存储,是在子节点上存储父节点的编号来确定各节点的父子关系,例如这样的组织结构:[图片上传失败...(image-a14d87-1651886833418)]

与之对应的表数据(department):

[图片上传失败...(image-a73b71-1651886833418)]

部门表结构(department)

id          部门编号name        部门名称level       所在树层级parent_id   上级部门编号

| 问题来了

这样的方式很不错,可以很直观的体现各个节点之间的关系,通常可以满足大多数需求。但是当业务需求变得多了,数据量庞大了,这样的方式就不再适合用于生产。

例如:PM加了以下需求:

  1. 查出指定部门下所有子孙部门
  2. 查询子孙部门总数
  3. 判断节点是否叶子节点

查出所有子孙部门

使用指定部门编号,一层一层使用递归往下查,可能是多数人会想到的方法。尽管在mysql8.0支持了 cte(公共表表达式),递归效率比传统递归方式有明显提升,但是查询效率仍会随着部门树层级深度的提高而变差。另外一种方法,一次性查出所有数据,放入内存中处理(数据量少时,可以选用。数据量多,不怕挨打的人也可以选这种)~

查询子孙部门总数

递归查询每一层的数量,最后相加。

判断是否叶子节点

方法1:可以加字段 isLeaf 的方式,来表示这个节点是否是叶子节点。方法2:直接通过查询parent_id=当前id的count是否大于0,大于0表示不是叶子节点,等于0表示为叶子节点。在日常中,可能会经常使用上述类似方法去解决类似的问题,但我觉得这样的方法在效率上不是最优解。于是乎开始查找更好的方案去解决这些问题。

| 要不试试这个方法?

直到后面查到国外一博客中,见到了所谓的《改进后的先序树遍历》文章(天哪,竟然是一篇2003年发表的文章)~他具体是怎么做的呢?还是回到刚刚的组织架构[图片上传失败...(image-8b11ac-1651886833417)]

我们从根节点开始,给董事长左值设为1,下级部门总经理左值设为2,以此类推地沿着边缘开始遍历,给每个节点加上左值,遇到叶子节点处给节点加上右值,再继续向上沿着边缘继续遍历,遍历结束回到根节点右侧,你将得到类似这样的结构。[图片上传失败...(image-f5f389-1651886833417)]

遍历完后每一个节点都有与之对应的左右值。这个时候可以去除parent_id字段,添加lft,rgt,来存储左右值。

[图片上传失败...(image-278dca-1651886833417)]

数据和结构准备完毕,我们来试试操作解决上面的需求~

查出所有子孙部门

根据当前表结构的规律,可以发现,要想查出所有子孙部门,只要查左值在 被查寻部门的左\右数之间的节点,查出来都是他的子节点。例如:查询行政总监的所有子部门,行政总监的左右数是9和18,因此只需要用9和18做lft字段的between查询,查询出的结果就是【被查部门本身数据和所有子孙部门】;

<pre mp-original-font-size="17" mp-original-line-height="27.200000762939453" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; word-wrap: break-word !important; caret-color: rgb(34, 34, 34); color: rgb(34, 34, 34); font-size: 17px; font-style: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: 0.5440000295639038px; orphans: auto; text-align: justify; text-indent: 0px; text-transform: none; widows: auto; word-spacing: 0px; -webkit-text-size-adjust: auto; -webkit-text-stroke-width: 0px; text-decoration: none; line-height: 27.200000762939453px;">

SET @lft := 9;SET @rgt := 18;SELECT * FROM department WHERE lft BETWEEN @lft AND @rgt ORDER BY lft ASC;/*例子中用BETWEEN将被查部门本身也查了出来。实际中可以用大于小于*/

完美~
</pre>

查询子孙部门总数

到这里可能会说,需求1都解决了,查总数自然也就解决了,直接上select count就可以了,确实没有错,但是没有那个必要,因为有个简单公式可以直接计算。公式:总数 = (右值 - 左值 - 1) / 2例如:

行政总监的子孙部门数 = (18 - 9 - 1) / 2 = 4董事长的子孙部门数 = (20 - 1 - 1) / 2 = 9会计的子部门数 =  (14 - 13 - 1) / 2 = 0可以数数看,确实没错哦~

判断是否叶子节点

通过有了上述计算公式算总数的经验后,现在判断是否叶子节点,有的小伙伴已经知道了怎么做,那就是:右值 - 1 == 左值那他就是叶子节点,或者左值 + 1 == 右值那他就是叶子节点,反之则不是叶子节点。例如:设计部,5 - 1 == 4,因此他是叶子节点。董事长,20 - 1 != 1,因此他不是叶子节点。至此已经完美的解决了上述需求问题,接下来再尝试一下业务的基本操作。

| 其他基本操作

新增部门

当新增一个部门时,需要对新增节点位置的后续边缘进行加2操作,因为每一个节点有左右两个数值。这个操作通常需要放到事务中进行处理。例如:在研发部门下添加一个新部门:[图片上传失败...(image-36a719-1651886833417)]

对应sql:

<pre mp-original-font-size="17" mp-original-line-height="27.200000762939453" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; word-wrap: break-word !important; caret-color: rgb(34, 34, 34); color: rgb(34, 34, 34); font-size: 17px; font-style: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: 0.5440000295639038px; orphans: auto; text-align: justify; text-indent: 0px; text-transform: none; widows: auto; word-spacing: 0px; -webkit-text-size-adjust: auto; -webkit-text-stroke-width: 0px; text-decoration: none; line-height: 27.200000762939453px;">

SET @lft := 7;/*新部门的左值*/SET @rgt := 8;/*新部门的左值*/SET @level := 5;/*新部门的层级*/begin;/*将插入的后续边缘的节点左右数+2*/UPDATE department SET lft=lft+2 WHERE lft > @lft;UPDATE department SET rgt=rgt+2 WHERE rgt >= @lft;/*插入数据*/INSERT INTO department(name,lft,rgt,level) VALUES('新部门',@lft,@rgt,level);/*新增影响行数为0时,必须回滚*/commit;/*rollback;*/

</pre>

删除部门

删除部门与新增部门类似,不同的是需要对删除节点的后续边缘节点减2操作。例如:删除刚刚添加的新部门:[图片上传失败...(image-504fd7-1651886833417)]

对应sql

<pre mp-original-font-size="17" mp-original-line-height="27.200000762939453" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; word-wrap: break-word !important; caret-color: rgb(34, 34, 34); color: rgb(34, 34, 34); font-size: 17px; font-style: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: 0.5440000295639038px; orphans: auto; text-align: justify; text-indent: 0px; text-transform: none; widows: auto; word-spacing: 0px; -webkit-text-size-adjust: auto; -webkit-text-stroke-width: 0px; text-decoration: none; line-height: 27.200000762939453px;">

SET @lft := 7;/*要删除的节点左值*/SET @rgt := 8;/*要删除的节点右值*/begin;UPDATE department SET lft=lft-2 WHERE lft > @lft;UPDATE department SET rgt=rgt-2 WHERE rgt > @lft;/*删除节点*/DELETE FROM department WHERE lft=@lft AND rgt=@rgt;/*删除影响行数为0时,必须回滚*/commit;/*rollback*/

</pre>

查询直接子部门

查询某部门的直接子部门(即不包含孙子部门),例如:查询总经理下的直接子部门。正常需要返回产品部和行政总监[图片上传失败...(image-e2c88e-1651886833417)]

对应的sql

<pre mp-original-font-size="17" mp-original-line-height="27.200000762939453" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; word-wrap: break-word !important; caret-color: rgb(34, 34, 34); color: rgb(34, 34, 34); font-size: 17px; font-style: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: 0.5440000295639038px; orphans: auto; text-align: justify; text-indent: 0px; text-transform: none; widows: auto; word-spacing: 0px; -webkit-text-size-adjust: auto; -webkit-text-stroke-width: 0px; text-decoration: none; line-height: 27.200000762939453px;">

SET @level := 2;/*总经理的level*/SET @lft := 2;/*总经理的左值*/SET @rgt := 19;/*总经理的右值*/SELECT * FROM department WHERE lft > @lft AND rgt < @rgt AND level = @level+1;

</pre>

查询祖链路径

查询某部门的祖链路径。例如:查询产品部的祖链路径,正常需要返回董事长,总经理

<pre mp-original-font-size="17" mp-original-line-height="27.200000762939453" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; word-wrap: break-word !important; caret-color: rgb(34, 34, 34); color: rgb(34, 34, 34); font-size: 17px; font-style: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: 0.5440000295639038px; orphans: auto; text-align: justify; text-indent: 0px; text-transform: none; widows: auto; word-spacing: 0px; -webkit-text-size-adjust: auto; -webkit-text-stroke-width: 0px; text-decoration: none; line-height: 27.200000762939453px;">

SET @lft := 3;/*产品部左值*/SET @rgt := 8;/*产品部右值*/SELECT * FROM department WHERE lft < @lft AND rgt > @rgt ORDER BY lft ASC;

</pre>

树形数据展示(JS示例)

let list = [//模拟sql查出来的列表。    {id:1,name:'root',lft:1,rgt:8,level:1},    {id:2,name:'child',lft:2,rgt:7,level:2},    {id:3,name:'grandson',lft:3,rgt:4,level:3},    {id:4,name:'grandson2',lft:5,rgt:6,level:3}];let rights = [] /*类似于一个栈结构(后进先出)*/let mp = {}//list.sort((a,b) => a.lft - b.lft)//如果你在sql中没有进行排序,需要在这里给他排序。list.forEach(item => {    if(rights.length > 0) {        while(rights[rights.length-1] < item.rgt) {            rights.splice(-1, 1)//从rights末尾去除        }    }    let _level = rights.length;    item._level = _level;    mp[_level] = item.id    item.parent_id = _level - 1 in mp ? mp[_level - 1] : null;//计算出上级部门编号    item.is_leaf = item.lft === item.rgt - 1;//判断是否叶子部门    rights.push(item.rgt)})/*上级部门计算出来了,和存parent_id的效果就一样了,后面只需要递归即可*//*递归函数 示例*/let recursive = (_list, parent_id = null) => {    let _tree = [];    _list.forEach(item => {        if(item.parent_id == parent_id) {            let childs = recursive(_list, item.id)            _tree.push({                ...item,                children: childs.length > 0 ? childs : (item.isLeaf ? null : [])            })        }    })    return _tree}console.log(recursive(list))

| 完结

在我目前看来,这个方法的唯一缺点就是,每一次的新增或删除,操作节点的后续边缘走到的节点都要加/减2操作。

欢迎指正、交流和评论,一起探讨更多解决方案 ...

-- 链接指导:
https://mp.weixin.qq.com/s/GDmrIwo89WVfDyAWYSBWQA

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350

推荐阅读更多精彩内容