数据库树形结构存储方法的选择

树形结构存储方法的选择

简单的方法跟踪多级回复构成的树形分支:parent_id

一开始的思路

  1. 使用parent_id跟踪分支
  2. 使用先找出所有节点,按照一定顺序整合成树形结构

缺陷:

  • 在深度过深时仅用parent_id需要执行很多次SQL才能获取给定主题的所有数据
  • 每天的数据变动可能非常大,每访问一次整合一次过于浪费时间且不切实际

目标:一个更好的存储多级结构及简单高效获取一个完整分支的方法

分层存储合查询

树形结构举例:组织架构图、线程化讨论

解决方案
邻接表

依赖父节点构成的邻接表,由parent_id指向另一个元素的id作为父节点

在这里插入图片描述

缺陷:无法查询一个节点的所有后代

可使用关联查询来获取一条评论和它的直接后代

select * from c1.*,c2.* 
from Comments c1 left outer join Comments c2
on c2.parent_id = c1.id

只能获取两层数据

树的特性:可以任意深地扩展

缺陷:

  • 每想多查一层就得多一个连接,而SQL查询最多4层

  • 聚合函数的使用很困难例如count()

  • 可能只需部分子树信息/聚合信息

  • 删除困难,要删除某个节点困难必须删除某颗子树,通过多次查询找到所有后代 ,再从最低级逐个删除满足外键完整性,parent_id也是一个外键,只不过连接的表还是本表

在这里插入图片描述

  • 如果想删除某个节点并提升其子节点就必须修改子节点parent_id再删除这个节点

优势:

  • 增加一个叶子节点

    insert into Comments(bug_id,parent_id,author,comment)
    values(1234,7,'kukla','Thanks!')
    
  • 修改一个节点的位置或者一颗子树的位置

    update comments set parent_id = 3 where comment_id = 6
    
小结

简单,但有些操作例如删除、查询、提升子节点要付出额外代价,增加/修改简单

如果有一下问题,说明需要考虑其他实现方案

  1. 我们的树结构支持多少层

    在纠结不适用递归查询获取一个给定节点的所有祖先或者后代

    只支持有限层级操作,但多少层才能满足需求

  2. 我总是很怕解除那些管理树结构的代码

    每一项技术都能使一些任务变得很简单,但通常是以其他任务变得复杂为代价的,选择方案应该应对合适的场景

  3. 我需要一个脚本定期清理树种孤立节点的数据

    因为删除非叶子节点产生了一些迷失节点,采用其他数据结构后也要保证树结构的数据一致性

合理使用反模式

如果获取直接父子节点/插入新节点 如果这两个操作能满足现下所有需求,那么使用邻接表就能很好的工作了

不要过度设计

其他解决方案

路径枚举

解决邻接表获取祖先节点困难的问题

使用path字段将层级信息组合,例如/usr/local/lib这种类似的存储结构

在这里插入图片描述

可通过比较每个节点的路径来查询一个节点的祖先

缺陷:

  1. 数据库不能确保路径的格式总是正确或者路径种的节点确实存在。
  2. 依赖于应用程序的逻辑代码来维护路径的字符串,并且验证字符串的正确性的开销很大。
  3. 无论path设置为多大都存在长度限制,无法达到树形结构无限扩展的特性

优势:

  1. 很好的支持祖先的查找
  2. 支持深度查询

嵌套集

存储子孙节点的相关信息而不是节点的直接祖先。通过两个数字nsleft及nsright来存储

在这里插入图片描述

nsleft:所有小于该节点的后代id

nsright:所有大于该节点的后代id

这些数字和comment_id没有任何关系,通过深度遍历逐层分配nsleft的值,并在返回时依次递增地分配nsleft的值并在返回时一次递增地分配nsright的值。

在这里插入图片描述

根据他们来找到给定节点的祖先/后裔

栗子:

寻找4的所有后裔

select * from comment c1 join comment c2

on c2.nsleft between c1.nsleft and c2.nsright

where c1.comment_id = 4

优势:

  1. 想删除某个非叶子节点,它的后代会自动代替被删除的节点,称为其直接祖先节点的直接后代。节点虽然是有序分配,但删除一个并不会队范围查询造成影响。

缺陷:

  1. 获取直接父节点/后代变得比较复杂
  2. 对树进行操作,比如插入和移动比其他设计复杂得多,需要重新遍历计算n左右值

优势:

如果简单快速的查询是重要部门,嵌套集是最佳选择

频繁的插入、删除就不适合

闭包表

额外创建TreePaths的表,包含两列,每一列都是指向Comments种comment_id的外键

在这里插入图片描述

在这里插入图片描述

获取祖先/后代更加直接

获取评论4的后代:

等同于查询所有祖先节点是4的节点信息

select c.*

from comments as c

join treepaths as t on c.comment_id = t.descendant

where t.ancestor = 4;

获取6的祖先:

查询所有后裔为6的节点

select c.*

from comments c join treepaths t on c.comment_id = t.ancestor

where t.descendant = 6;

插入一个5的新的叶子节点:

首先插入一条自己到自己的关系,然后搜索treepaths 表中后代是评论5的节点,增加该节点和新插入节点的“祖先-后代”关系

然后插入新节点“祖先-后代”的关系

insert into treepaths (ancestor,decendant)

select t.ancestor,8

from treepaths as t

where t.descendant = 5

union all

select 8,8

删除一颗完整子树

删除评论4及其后代

删除所有在treepaths表中后代为4的行以及那些以评论#4的后代为后代的行

删除4及其子节点的关系,删除其他节点到4的子节点的关系

delete from treepaths

where descendant in (

select descendant from treepaths where ancestor = 4

)

这样的删除并非删除掉节点,只是删除了关系,把层次结构分离使得设计更加灵活

移动

要移动树则先端开子树与祖先们的关系:找到这颗子树的顶点,删除它的所有子节点和它的所有祖先节点之间的关系。

比如把6移动到3下

首先把自我引用删掉

delete from treepaths where descendant in (

​ select descendant from treepaths where ancestor = 6

)and ancestor in (

​ select ancestor from treepaths where descendant = 6 and ancestor != descendant

)

查询评论6的祖先(不包括自身)以及评论6的后代(包括自身),然后删除他们之间的关系,正确地移除评论6的祖先到评论6和它的后代之间的路径。删除 (1,6 )(1,7 )(4,6 )(4,7 )不会删除(6,6 )(6,7 )

再建立关系

对比
设计查询子查询树插入删除引用完整性
邻接表1简单困难简单简单
递归查询1简单简单简单简单
枚举路径1简单简单简单简单
嵌套集1困难简单困难困难
闭包表2简单简单简单简单
总结
  1. 邻接表最方便
  2. 如果支持with/connect by prior的递归查询能使邻接表更为高效
  3. 枚举路径直观展示出祖先到后代之间的路径,但同时由于它不能确保引用完整性,设计非常脆弱。存储也冗余
  4. 嵌套集是一个聪明的解决方案,但不能确保引用完整性。最好在一个查询性能要求高而对其他要求一般的场合使用
  5. 闭包表是最通用的设计,并且上述的设计中只有它能允许一个节点属于多棵树。它要求一张额外的表来存储关系,使用空间换时间的方案减少操作过程中由冗余的计算锁造成的消耗

如果简单就用邻接表但如果复杂,要查询整个树结构就用闭包表

跟着需求进行分析与设计

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值