当前位置:首页 > 问答 > 正文

数据库里怎么搞树形结构,步骤和思路分享一下

说到在数据库里搞树形结构,这确实是个挺常见也挺让人头疼的问题,想象一下,你要存一个公司的部门架构,或者一个论坛帖子的评论楼中楼,这种一层套一层的数据,就是典型的树形结构,直接在数据库里存,如果方法没选对,查询起来会非常慢,尤其是当数据量大了以后,下面我就分享几种常用的方法和各自的思路,尽量说得直白点。

最直接的办法:邻接表

这个方法最简单,最容易想到,你只需要在表里加一个字段,用来记录每个节点的“爸爸”是谁,比如说,你有一张部门表,字段大概是:部门ID(主键),部门名称,父部门ID,顶级部门(比如总公司)的父部门ID就设为空或者0。

  • 查询直接子部门:这很容易,比如要查ID为1的部门下面有哪些直接下属部门,SQL语句就是:SELECT * FROM 部门表 WHERE 父部门ID = 1,这个很快。
  • 查询所有子孙部门:麻烦就来了,比如你想知道ID为1的部门下面所有层级的部门(包括子部门、孙部门等等),用邻接表就很难一步到位,你可能得用程序写一个递归函数,先查直接子部门,再对每个子部门查它的子部门,一层层查下去,如果树很深,就要跟数据库交互很多次,性能很差。
  • 插入和移动:增删改操作倒是很方便,增加一个部门,只需要指定它的父部门ID就行,把一个部门从一个父部门移到另一个父部门,也只需要修改它的父部门ID字段。

总结一下:邻接表适合树结构不深、主要是查询直接父子关系、并且频繁进行增删改操作的场景,它的优点是简单直观,缺点是查询整棵树或某个分支很费劲。

为了优化查询:路径枚举

这个方法是为了解决邻接表“找子孙难”的问题,它在表里增加一个字段,比如叫“路径”,用来记录从根节点到当前节点的完整路径,通常用一串分隔符隔开的ID来表示,部门表结构变成:部门ID,部门名称,路径。

假设部门ID是1(总公司)下面有ID为2的部门(技术部),技术部下面有ID为5的部门(后端组)。

  • 总公司(1)的路径可能是:/1/

  • 技术部(2)的路径就是:/1/2/

    数据库里怎么搞树形结构,步骤和思路分享一下

  • 后端组(5)的路径就是:/1/2/5/

  • 查询所有子孙部门:这下就简单了!要查ID为2的技术部下面所有子孙部门,SQL语句可以这么写:SELECT * FROM 部门表 WHERE 路径 LIKE ‘/1/2/%’,一个模糊查询就搞定了,不需要递归,速度快了很多。

  • 查询上级路径:也很容易,通过解析路径字段,就能知道一个节点的所有祖先是谁。

  • 插入和移动:缺点来了,插入一个新节点时,你需要先确定好它的路径(基于父节点的路径),最麻烦的是移动节点,如果你把/1/2/5/这个后端组挪到另一个部门下面,比如挪到/1/3/(产品部)下面,那么不仅这个节点本身的路径要改成/1/3/5/,它所有子孙节点的路径都要跟着一起更新!这个操作成本很高,容易出错。

总结一下:路径枚举用空间(存储路径字段)换取了时间(查询效率),特别适合需要频繁查询子树但很少移动节点的场景。

更强大的方法:嵌套集

这个方法有点反直觉,但非常强大,尤其擅长查询关于“范围”的问题,它不再记录父节点,而是给每个节点分配两个数字,可以理解为“左值”和“右值”,这俩数字的设定规则是,采用深度优先遍历整棵树,当一个节点被第一次访问时,记录下它的左值,继续遍历它的子孙节点,当这个节点的所有子孙都被遍历完后,记录下它的右值,这样,每个节点都会对应一个区间(左值,右值)。

数据库里怎么搞树形结构,步骤和思路分享一下

关键特性在于:一个节点的所有子孙节点,其左右值都会包含在这个节点的左右值区间之内

  • 查询所有子孙部门:假设技术部的左值是2,右值是7,那么查询它的所有子孙,SQL就是:SELECT * FROM 部门表 WHERE 左值 > 2 AND 右值 < 7,非常快。
  • 查询祖先路径:要查一个节点的所有祖先,比如一个节点左值是4,右值是5,它的祖先就是那些左值小于4且右值大于5的节点:SELECT * FROM 部门表 WHERE 左值 < 4 AND 右值 > 5
  • 插入和移动:这是嵌套集的死穴,因为每个节点的左右值定义了严格的区间顺序,插入一个新节点,或者移动一个节点,都可能需要重新计算一大批兄弟节点及其子孙的左右值,这个更新操作的范围会很大,非常复杂和缓慢。

总结一下:嵌套集是查询的王者,适合几乎只读、需要频繁进行复杂树形关系查询(如查找所有子孙、所有祖先、计算层级等)的场景,但它的增删改操作极其昂贵。

综合方案:闭包表

这个方法可以看作是邻接表和路径枚举的一种折中和扩展,它完全解耦了树形结构,它需要两张表:

  1. 节点表:存放节点本身的信息,比如部门ID,部门名称。
  2. 关系表:专门用来存储节点之间的关系,这个表至少有两个字段:祖先节点ID,子孙节点ID,还可以加一个“距离”字段,表示两节点之间相隔多少层。

对于1 -> 2 -> 5这条路径,在关系表中会存储多条记录:

  • (祖先=1, 子孙=1, 距离=0) // 节点自身

  • (祖先=1, 子孙=2, 距离=1)

    数据库里怎么搞树形结构,步骤和思路分享一下

  • (祖先=1, 子孙=5, 距离=2)

  • (祖先=2, 子孙=2, 距离=0)

  • (祖先=2, 子孙=5, 距离=1)

  • (祖先=5, 子孙=5, 距离=0)

  • 查询所有子孙/祖先:变得非常简单,都是在关系表里做连接查询,比如查部门2的所有子孙:SELECT 子孙.* FROM 关系表 r JOIN 节点表 子孙 ON r.子孙ID = 子孙.ID WHERE r.祖先ID = 2 AND r.距离 >= 1

  • 插入和移动:插入一个节点时,需要插入这个节点自身的关系(距离0),以及它和所有祖先的关系(通过查询其父节点的祖先关系再加上一层得到),移动一个子树虽然也比邻接表复杂,但比路径枚举和嵌套集要可控,它只需要断开旧的关系,建立新的关系,操作范围局限在关系表内。

总结一下:闭包表是最灵活、功能最全的方案,查询和操作都比较均衡和强大,缺点是需要额外的一张表,存储空间会大一些,但通常这是值得的。

最后怎么选?

  • 简单管理,增删改多:用邻接表
  • 查询为主,几乎不改:用嵌套集
  • 需要快速找子树,结构稳定不常动:用路径枚举
  • 不差存储空间,想要功能全面和未来灵活性:用闭包表,这通常是大多数现代应用推荐的稳妥选择。

希望这些大白话的解释和步骤思路能帮你理解在数据库里处理树形结构的几种套路。