闭包表

什么是闭包表?

闭包表 是一种用于在关系型数据库中高效表示和查询 树形层级结构(Tree / Hierarchy) 的设计模式。

它通过一张额外的“关系表”来显式存储 所有祖先(ancestor)与后代(descendant)之间的路径关系,包括自己到自己的关系。

为什么需要闭包表?

假设有如下分类结构:

1
2
3
4
5
6
7
电子产品
├── 手机
│   ├── 智能手机
│   └── 功能机
└── 电脑
    ├── 笔记本
    └── 台式机

传统做法(如邻接表模型)只存 parent_id

id name parent_id
1 电子产品 NULL
2 手机 1
3 智能手机 2
4 功能机 2
5 电脑 1
6 笔记本 5

这种设计在以下操作中效率很低:

  • 查询“电子产品”下所有子分类(需递归多次查询);
  • 判断“智能手机”是否属于“电子产品”(需向上遍历);
  • 移动整个子树(如把“手机”移到“家电”下)逻辑复杂。

那采用路径枚举呢? 多存储一个 path

即每个节点保持从跟到自己的完整路径。

id name parent_id path
1 电子产品 NULL “/1/”
2 手机 1 “/1/2/”
3 智能手机 2 “/1/2/3/”
5 电脑 1 “/1/5/”
6 笔记本 5 “/1/5/6/”

如果子树移动,则相关的树结构都要变动,优点是可读性高,实现简单。

1. 查询某个分类的所有子分类(含多级)

查询所有后代不包含自己

1
2
3
SELECT * 
FROM category 
WHERE path <@ '1' AND path != '1';

适用场景:

  • 树结构基本不变
  • 层级路径直接用于业务逻辑展示,如面包屑导航
  • 轻量级系统,单父节点

闭包表

新增一个表,记录所有祖先与后代之间的路径关系,包括自己到自己的关系。

ancestor_id descendant_id depth
1 1 0
1 2 1
1 3 2
1 4 2
1 5 1
1 6 2
2 2 0
2 3 1
2 4 1
3 3 0
4 4 0
5 5 0
5 6 1
6 6 0

1. 查询某个分类的所有子分类(含多级)

1
2
3
4
5
-- 查“电子产品”(id=1) 下所有子分类
SELECT c.*
FROM category c
JOIN category_closure cc ON c.id = cc.descendant_id
WHERE cc.ancestor_id = 1;

2. 查询某个分类的所有祖先(向上追溯)

1
2
3
4
5
-- 查“智能手机”(id=3) 的所有上级分类
SELECT c.*
FROM category c
JOIN category_closure cc ON c.id = cc.ancestor_id
WHERE cc.descendant_id = 3 AND cc.depth > 0;

3. 判断 A 是否是 B 的祖先

1
2
3
4
5
SELECT EXISTS (
    SELECT 1 FROM category_closure
    WHERE ancestor_id = 1 AND descendant_id = 3
);
-- 返回 true,说明“电子产品”是“智能手机”的祖先

4. 获取直接子节点(depth = 1)

1
2
3
4
5
SELECT c.*
FROM category c
JOIN category_closure cc ON c.id = cc.descendant_id
WHERE cc.ancestor_id = 1 AND cc.depth = 1;
-- 返回“手机”和“电脑”

5. 移动子树(例如把“手机”移到“家电”下)

  1. 删除以“手机”及其后代为 descendant 的所有旧祖先关系;
  2. 插入新祖先(“家电”及其祖先)到“手机”子树的新路径。

适用场景:

  • 商品分类系统
  • 组织架构
  • 评论嵌套(楼中楼)
  • 文件夹/目录结构
  • 权限菜单树

案例

背景知识

  • 分类有父级子级
  • 一个分类有多个资源
  • 一个资源可以绑定多个分类
  • 分类固定

解决方案

路径枚举 + 中间关联表

避免过度设计,中间关联表可以缓存 path,连表查询相关数据。

本文阅读量 次, 总访问量 ,总访客数
Built with Hugo .   Theme Stack designed by Jimmy