数据库设计

结绳记事,记录、思考,方有成长

在做数据库设计时,有时我们需要保存一些树形的数据结构,比如评论、组织架构、话题讨论、知识管理、商品分类、省市县,这些数据存在一种递归关系。很多人想到的第一个解决方案往往是记录每个节点的父节点,例如以下的评论表:

CREATE TABLE comments (
	comment_id int(10) NOT NULL comment '评论id',
	parent_id int(10) DEFAULT NULL comment '父评论id',
	comment text NOT NULL comment '评论内容',
	PRIMARY KEY(comment_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8

评论内容
comment表的实际数据

comment_idparent_idcomment
10新冠病毒传播这么厉害,作为普通人应该注意什么
21哪儿都不能去啊,太危险了
31应该做好防护,多洗手、不出门
42是啊,还是待在家安全
52出门都不敢做公交了
63小区封了
74口罩买不到,捉急

如果采用这样的结构,当一篇帖子回复讨论的内容很多的时候,就需要编写复杂的代码递归检索很多记录,查询的效率就会很低。如果数据量不大、讨论内容相对固定,数据层次较少,采用这样的结构是很简单、清晰的。比如商品分类,通常采用3级分类,我们在设计表时,再增加一个字段level,就可以满足业务场景。那如果数据量很大,层次又不固定,查询就会变得复杂。
下面介绍两种更通用,扩展性更好的解决方案,路径枚举闭包表

(1) 路径枚举
对于上述表的结构,增加一个字段path,用于记录节点的所有祖先信息。记录的方式是把所有的祖先信息组织成一个字符串。结构如下:
comment表的实际数据(记录了父节点和祖先信息)

comment_idparent_idpathcomment
101/新冠病毒传播这么厉害,作为普通人应该注意什么
211/2/哪儿都不能去啊,太危险了
311/3/应该做好防护,多洗手、不出门
421/2/4/是啊,还是待在家安全
521/2/5/出门都不敢做公交了
741/2/4/7/口罩买不到,捉急

接下来分析下涉及到的各个场景:

  • 查询所有祖先:因为路径path字段包含了该节点的所有祖先信息,所以可以轻易地获取某个节点的所有祖先节点,可以用程序先获取path字符串,然后再使用切割字符串的函数处理得到所有的祖先节点
  • 查询所有后代:例如查找comment_id等于2的所有后代,可以使用如下方式
select * from comment where path like '1/2/_%'
  • 查询直接后代:例如查询comment_id等于2的直接后代
select * from comment where parent_id = 2
  • 插入:比如回复了评论#7,产生的新评论为8,则首先复制评论#7内容,path再拼接上/8即可
insert intio comment
select 8, 7, path+'/8', '评论8内容' from comment where comment_id = 7
  • 删除:比如删除了评论2,则其下的所有子评论都需要删除
# 首先查出评论2的path,为1/2/
delete from comment where path like '1/2/%'

总结:枚举路径的方式使得查询子树和祖先都变得更加简单,查看分隔符即可知道节点的层次,虽然冗余存储了一些数据,应用程序需要额外增加代码以确保路径信息的准确性,但这种设计的扩展性更好。
弊端:如果层次过深,path路径长度存在过长情况,字段长度越长,索引查询效率越低。

(2)闭包表
闭包表也是一种通用的方案,它需要额外增加一张表,用于记录节点之间的关系。它不仅记录了节点之间的父子关系。也记录了树中所有节点之间的关系,以及深度。使用如下命令建立path表:

CREATE TABLE path (
	ancestor int(11) NOT NULL,
	descendant int(11) NOT NULL,
	deep int(11) NOT NULL,
	PRIMARY KEY(ancestor, descendant)
) ENGINE=InnoDB DEFAULT CHARSET=utf8

ancestor表示祖先, descendant表示后代,deep表示祖先和后代之间的深度,存储的是对应的comment_id。数据类似于
path表的数据(记录了所有节点之间的关系)

ancestordescendantdeep
110
121
131
142
152
162
173
220
241
251
272
330
440
471
550
660
770

有了如表所示的完整的节点间关系,查找后代节点、祖先节点也变得更容易。

  • 查找评论 #2的所有后代,不包含自身
select * from path where ancestor = 2 and descendant <> 2
  • 查询评论 #2的直接后代
select * from path where ancestor = 2 and deep = 1
  • 插入:比如回复了评论#7,产生的新评论id为 #8。注意:所有的祖先节点都需要再关联上节点#8,并且深度在原来基础上加1。同时记录节点自身,深度为0
insert into path 
select ancestor, 8, deep+1 from path where descendant = 7
UNION ALL
select 8, 8, 0
  • 删除:比如删除了评论#2,则需要把#2的 所有子孙节点删除,同时也要和#2的父节点解除关系
delete from path where ancestor =2 or descendant = 2

总结:如上所述的数据结构,增加了一个表,用于存储节点之间的信息,是一种典型的“以空间换时间”的方案,而且一个节点可以属于多棵树。相对于路径枚举,闭包表的节点关系更容易维护。
缺点:对于层级为N的评论树,至少对应N*(N+1)/2条记录,不过,这也是它优于路径枚举而要付出的代价。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值