ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

经典树形结构:闭包表

经典树形结构:闭包表 一、树形结构存储难在哪树形结构由节点和边组成每个节点可以有零个或多个子节点但只有一个父节点根节点除外。这种结构在现实中随处可见公司的组织架构、电商的商品类目、论坛的帖子回复……但在关系型数据库中存储和查询它们却并不直观。常见的诉求无非就几类查某个节点的所有子节点、查所有祖先节点、查两节点之间的距离、增删改节点。看似简单但不同的存储方案在这几项操作上的表现天差地别。二、四种常见方案速览在正式介绍闭包表之前我们先快速了解另外三种主流方案这样才能理解闭包表到底“优”在哪里。1. 邻接表最直观的方案——每个节点记录一个parent_id指向父节点。优点结构简单插入方便。缺点查询子树或祖先需要递归查询层级深时性能极差。2. 路径枚举在每个节点中存储从根节点到该节点的完整路径如/1/2/3。优点避免了递归查询查询子树效率高。缺点移动节点时需要更新该节点及所有子孙的路径维护成本极高路径长度有上限。3. 嵌套集为每个节点赋予左右值通过数值范围来判断祖先后代关系。优点查询子树极快。缺点插入、删除、移动节点时需要更新大量节点的左右值模型复杂维护困难。4. 闭包表——今天的主角单独创建一张关系表存储树中所有节点对之间的祖先-后代关系包括节点自身。三、闭包表的核心原理闭包表的核心思想是空间换时间——用额外的存储空间换取查询效率的大幅提升。它通常需要两张表节点表存储节点本身的信息CREATE TABLE nodes ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255) NOT NULL );闭包关系表存储所有祖先-后代关系CREATE TABLE node_paths ( ancestor_id INT, -- 祖先节点ID descendant_id INT, -- 后代节点ID depth INT, -- 两者之间的距离层数差 PRIMARY KEY (ancestor_id, descendant_id), FOREIGN KEY (ancestor_id) REFERENCES nodes(id), FOREIGN KEY (descendant_id) REFERENCES nodes(id) );关键点每个节点不仅要记录与所有祖先的关系还要记录与自身的关系即ancestor_id descendant_iddepth 0。举个例子假设有这样一棵树1 ├── 2 │ └── 4 └── 3闭包表中存储的数据是这样的ancestor_iddescendant_iddepth110121131142220241330440有了这张表查询就变得异常简单查询节点1的所有后代SELECT * FROM node_paths WHERE ancestor_id 1查询节点4的所有祖先SELECT * FROM node_paths WHERE descendant_id 4查询节点2的直接子节点SELECT * FROM node_paths WHERE ancestor_id 2 AND depth 1四、闭包表的优缺点优点查询效率极高无论树有多深查询任意节点的所有祖先或所有后代都只需要一次简单的索引查询无需递归。支持复杂查询可以轻松查询两节点之间的距离、某个节点的所有子孙等。节点移动方便移动一个子树时只需要删除该子树相关的旧路径再插入新路径即可操作相对可控。缺点存储空间较大闭包表存储了所有节点对的关系数据量约为 O(n²) 级别。树越大关系表膨胀越明显。插入成本较高插入一个新节点时需要为它和所有祖先节点各插入一条关系记录。五、什么时候该用闭包表综合来看闭包表最适合以下场景树形结构层级较深比如超过5层邻接表的递归查询难以承受。查询操作远多于写入操作愿意用存储空间换取查询性能。需要频繁查询祖先/后代关系比如权限系统中的部门归属查询、电商系统中的类目路径查询。如果树结构非常浅、数据量很小或者写入极其频繁邻接表可能是更轻量的选择。没有银弹只有最适合的方案。六、总结闭包表通过“空间换时间”的思路用一张专门的关系表存储所有节点对的祖先-后代关系将复杂的树形查询转化为简单的索引查询。虽然插入和存储成本有所增加但在查询性能和维护便利性上优势明显。
返回列表