如何在关系型数据库中存储树形结构

日常工作中,经常会使用到树形数据结构,比如说商品类目树,评论树,部门树,权限树等,如何在关系型数据库中存储树形结构呢?今天来介绍几种方案。

业务场景

文中使用公司部门结构树作为栗子,要在mysql中存储这个公司部门结构树


公司部门结构树

第一种方案:邻接表(Adjacency List)

邻接表想必大家都不陌生吧,用邻接表的关键是,在每个节点存储他的父节点的id。

CREATE TABLE `company_adjacency_list`  (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `name` varchar(200) NULL DEFAULT NULL COMMENT '部门名称',
  `parent_id` int(11) NULL DEFAULT NULL COMMENT '父id',
  PRIMARY KEY (`id`) USING BTREE
)

在每一个部门信息中都存储了他的父节点id,parent_id字段


表结构

导入数据的过程就不说了,直接来看下数据吧:


公司部门结构数据
数据查询方式

这里使用常用的几种查询方式来看下这种方案的查询

  • 查询一个部门的直属下级部门
select * from company_adjacency_list where parent_id = ?

可以通过parent_id做查询条件,可以快速查询到一个部门的直属下级部门

  • 查询一个部门的直属上级部门
select * from company_adjacency_list where id= 要查询的部门的parent_id

通过部门信息中的parent_id去查相应的父节点信息就可以快速实现

  • 查询一个部门的所有下级部门(查询子节点包括子节点的子节点)
    这种表结构进行查询时,是一个循环的过程,先查询到子节点,然后在查询子节点的子节点,这样循环下去查,虽然网上很多文章都给了循环查询的sql和用存储过程查,但是其实这两种方式的效率都比较低,尤其用在线上服务时,这种查询效率低的方式,不建议使用sql直接查询,这种查询可以把数据全查询出来,然后在应用中组装成树形结构,放到缓存中,通过定时更新、提前预热等方式来提高查询效率
数据更新方式

这种数据存储结构下,更新数据是比较方便快捷的,添加数据时直接找准父节点的id,组织部门变更时,也直接变更父id就好了,删除时候,看自己业务是否需要删除子节点这几种情况,

第二种方案:路径表(Path Enumeration)

路径标的要点,就是每个节点存储根节点到该节点的路径,其实我觉得和别的几种方案可以共用

CREATE TABLE `company_path_enumeration`  (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `name` varchar(200) NULL DEFAULT NULL COMMENT '部门名称',
  `path` varchar(200) NULL DEFAULT NULL COMMENT '路径',
  PRIMARY KEY (`id`) USING BTREE
)

在每一个部门信息中都存储了他完整的路径,path字段


company_path_enumeration表结构

导入数据的过程就不说了,直接来看下数据吧:


数据
数据查询方式

使用路径表,通过path这个字段查询起来是比较困难的,一般都需要使用like,CONCAT函数、REPLACE函数等做字符串的处理逻辑,查询起来比较复杂,这里不做展示了,线上服务不建议使用这种方式,查询效率低会影响到服务性能,一般建议和邻接表方式统一使用,同时添加parent_id和path字段,parent_id用来查询,path用来查看节点完整的路径

数据更新方式

这种数据存储结构下,更新数据是比较方便快捷的,添加数据时直接找准路径就好,组织部门变更时,也直接找准路径就好,删除时候,看自己业务是否需要删除子节点这几种情况,

第三种方案:闭包表?闭合表?(Closure Table)

Closure Table,百度直译过来叫闭合表,大多数人叫做闭包表,这种方案的要点是存储公司部门信息主表中,不存储节点关系的数据,使用另一张关系表来存储节点之间的关系,其中包含了任何两个有关系(上下级)节点的关联信息

公司部门信息主表

公司部门信息主表,只需要存储部门的本身信息


公司部门信息主表
公司部门结构关系表

主要包括三个字段

字段 字段直译 字段说明
ancestor 直译叫祖先节点 上级节点
descendant 直译叫子孙节点 下级节点
distance 直译叫距离 就上级节点与下级节点之间的距离

要点就是关系表的一条记录是一个上级节点、下级节点、与他们之间的路径距离。拿部门结构图来举例子


公司部门结构树

总公司-企划部的关系数据是:

ancestor:  主表中总公司的id  1
descendant: 主表中企划部的id  2
distance: 总公司到企划部的路径是: 总公司-企划部,所以他们的路径是 1

总公司-大区A的关系数据是:

ancestor:  主表中总公司的id  1
descendant: 主表中大区A的id  6
distance: 总公司到大区A的路径是: 总公司-大区- 大区A,所以他们的路径是 2

关系表中存储所有的节点路径信息,还用distance表示路径的距离,需要把树形结构中每两个节点之间的路径信息都维护进来。

CREATE TABLE `company_relation`  (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `ancestor` bigint(20) NULL DEFAULT NULL,
  `descendant` bigint(20) NULL DEFAULT NULL,
  `distance` int(11) NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE,
  INDEX `index_ancestor`(`ancestor`) USING BTREE,
  INDEX `descendant`(`descendant`) USING BTREE,
  INDEX `distance`(`distance`) USING BTREE
) ENGINE = InnoDB 
数据存储示例

数据存储的过程就拿导入总公司-门店A的过程做个示例。主表的数据存储就不说,说下关系中,存储部门结构的路径信息,总公司-门店A总共包含以下几条路径:

  • 总公司-大区
ancestor:  主表中总公司的id  1
descendant: 主表中大区的id 4
distance: 总公司到大区的路径是: 总公司-大区,所以他们的路径是 1
INSERT INTO `company_relation`(`ancestor`, `descendant`, `distance`) VALUES (1, 4, 1);
  • 总公司-大区A
ancestor:  主表中总公司的id  1
descendant: 主表中大区A的id 6
distance: 总公司到大区A的路径是: 总公司-大区-大区A,所以他们的路径是2
INSERT INTO `company_relation`(`ancestor`, `descendant`, `distance`) VALUES (1, 6, 2);
  • 总公司-门店A
ancestor:  主表中总公司的id  1
descendant: 主表中门店A的id 8
distance: 总公司到门店A的路径是: 总公司-大区-大区A-门店A,所以他们的路径是3
INSERT INTO `company_relation`(`ancestor`, `descendant`, `distance`) VALUES (1, 8, 3);
  • 大区-大区A
ancestor:  主表中大区的id  4
descendant: 主表中大区A的id 6
distance: 大区到大区A的路径是: 大区-大区A,所以他们的路径是1
INSERT INTO `company_relation`(`ancestor`, `descendant`, `distance`) VALUES (4, 6, 1);
  • 大区-门店A
ancestor:  主表中大区的id  4
descendant: 主表中门店A的id 8
distance: 大区到门店A的路径是: 大区-大区A-门店A,所以他们的路径是2
INSERT INTO `company_relation`(`ancestor`, `descendant`, `distance`) VALUES (4, 8, 2);
  • 大区A-门店A
ancestor:  主表中大区A的id  6
descendant: 主表中门店A的id 8
distance: 大区A到门店A的路径是: 大区A-门店A,所以他们的路径是1
INSERT INTO `company_relation`(`ancestor`, `descendant`, `distance`) VALUES (6, 8, 1);

看到了么,是存储了所有总公司-门店A之间的路径信息

数据查询方式

这里使用常用的几种查询方式来看下这种方案的查询

  • 查询一个部门的直属下级部门,只需要查询ancestor为本节点id,距离为1的
select * from company_relation where ancestor = 本节点id and distance=1
  • 查询一个部门的直属上级部门,只需要查询descendant为本节点id,距离为1的
select * from company_relation where distance= 本节点id and distance=1
  • 查询一个部门的所有下级部门(查询子节点包括子节点的子节点),只需要使用ancestor为本节点id就可以查询到所有的子部门
select * from company_relation where ancestor = 本节点id
  • 这种设计,也可以很方便的查询到指定层级的节点
数据更新方式

这种数据存储结构下,更新数据比较麻烦,因为他存储了两节点直接所有路径信息(包括中间节点的)

方案对比:

  • 方案一:邻接表(Adjacency List)
    优点:只用在主表中添加父id字段,空间占用低,在查询相邻节点关系,添加删除节点都比较简单
    缺点:对于多级节点的查询会比较麻烦,需要使用一些手段去优化
    适用场合:对于节点增删比较频繁,节点层级不是很复杂的场景
  • 方案二:路径表(Path Enumeration)
    我理解方案二,使用path字段存储节点到顶级节点之间的路径,查询其实比较麻烦,一般不会单独使用这种方案,一般和邻接表(Adjacency List) 方案配合使用,添加父id字段的同时在添加path字段,用来标识到顶级节点路径,用来快速支持节点整体路径的查询诉求
  • 方案三:闭包表?闭合表?(Closure Table)
    优点:在查询树形结构任意层级之间的关系时效率都很高
    缺点:是一种空间换时间的方案,关系表存储数据量,增删改都比较麻烦
    适用场合:查询诉求大,树形结构变动频率小的场景

如何在关系型数据库中存储树形结构就为大家讲到这,欢迎大家来交流,指出文中一些说错的地方,让我加深认识,愿大家没有bug,谢谢!

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

推荐阅读更多精彩内容