在数据库中存储一棵树,实现无限级分类
哈喽!今天心血来潮给大家带来了《在数据库中存储一棵树,实现无限级分类》,想必大家应该对数据库都不陌生吧,那么阅读本文就都不会很困难,以下内容主要涉及到MySQL、Java、mybatis,若是你正在学习数据库,千万别错过这篇文章~希望能帮助到你!
原文发表于我的博客: https://blog.kaciras.com/article/6/store-tree-in-database
在一些系统中,对内容进行分类是必需的功能。比如电商就需要对商品做分类处理,以便于客户搜索;论坛也会分为很多板块;门户网站、也得对网站的内容做各种分类。
分类对于一个内容展示系统来说是不可缺少的,本博客也需要这么一个功能。众所周知,分类往往具有从属关系,比如铅笔盒钢笔属于笔,笔又是文具的一种,当然钢笔还可以按品牌来细分,每个品牌下面还有各种系列...
这个例子中从属关系具有5层,从上到下依次是:文具-笔-钢笔-XX牌-A系列,但实际中分类的层数却是无法估计的,比如生物中的界门纲目科属种有7级。显然对分类的级数做限制是不合理的,一个良好的分类系统,其应当能实现无限级分类。

在写自己的博客网站时,刚好需要这么一个功能。
完整的代码以及动态演示见 https://github.com/Kaciras/ClosureTable
1.需求分析
首先分析一下分类之间的关系是怎样的,很明显,一个分类下面有好多个下级分类,比如笔下面有铅笔和钢笔;那么反过来,一个下级分类能够属于几个上级分类呢?这其实并不确定,取决于如何对类型的划分。比如有办公用品和家具,那么办公桌可以同时属于这两者,不过这会带来一些问题,比如:我要显示从顶级分类到它之间的所有分类,那么这种情况下就很难决定办公用品和家具显示哪一个,并且如果是多对一,实现上将更加复杂,所以这里还是限定每个分类仅有一个上级分类。
这样一来,分类的关系可以表述为一父多子的继承关系,正好对应数据结构中的树,那么问题就转化成了如何在数据库中存储一棵树,并且对分类所需要的操作有较好的支持。
对于本博客来说,分类至少需要以下操作:
- 对单个分类的增删改查。
- 查询一个分类的直属下级和所有下级,在现实某一分类下所有文章时需要使用。
- 查询出由顶级分类到文章所在分类之间的所有分类,并且是有序的。
- 查询分类是哪一级的,比如顶级分类是1,顶级分类的直属下级是2,再往下依次递增。
- 移动一个分类,换句话说就是把一个子树(或者仅单个节点)移动到另外的节点下面,这个在分类结构不合理,需要修改时使用。
在性能的衡量上,这些操作并不是平等的。查询操作使用的更加频繁,毕竟分类不会没事就改着玩,性能考虑要以查询操作优先,特别是2和3分别用于搜索文章和在文章简介中显示其分类,所以是重中之重。
另外,每个分类除了继承关系外,还有名字,简介等属性,也需要存储在数据库中。每个分类都有一个id,由数据库自动生成(自增主键)。
无限级多分类多存在于企业应用中,比如电商、博客平台等,这些应用里一般都有缓存机制,对于分类这种不频繁修改的数据,即使在底层数据库中存在缓慢的操作,只要上层缓存能够命中,一样有很快的响应速度。但是对于抽象的需求:在关系数据库中存储一棵树,并不仅存在于有缓存的应用中,所以设计一个高效的存储方案,仍然有其意义。
下面就以这个卖文具的电商的场景为例,针对这6条需求,设计一个数据库存储方案(对过程没兴趣可以直接转到第4节)。
2.一些常见设计方案的不足
2.1 直接记录父分类的引用
在许多编程语言中,继承关系都是一个类仅继承于一个父类,添加这种继承关系仅需要声明一下父类即可,比如JAVA中extends xxx。根据这种思想,我们仅需要在每个分类上添加上直属上级的id,即可存储它们之间的继承关系。

表中
SELECT id,name FROM pathlist WHERE path LIKE '1,%'
一句话搞定,
SELECT id,name FROM pathlist WHERE path LIKE '%2'
而找出所有上级分类这个需求,直接查出
SELECT descendant FROM CategoryTree WHERE ancestor=5 AND distance=1
查询所有子节点:
SELECT descendant FROM CategoryTree WHERE ancestor=5 AND distance>0
查询某个上级节点的子节点,换句话说就是查询具有指定上级节点的节点,也就是
SELECT ancestor FROM CategoryTree WHERE descendant=10 ORDER BY distance DESC
查询id为10的节点(含)到id为3的节点(不含)之间的所有节点,按照层级由小到大排序
SELECT ancestor FROM CategoryTree WHERE descendant=10 AND distance
查询路径,只需要知道
SELECT distance FROM CategoryTree WHERE descendant=5 AND ancestor=0
查询id为5的节点是id为10的节点往下第几级
SELECT distance FROM CategoryTree WHERE descendant=5 AND ancestor=10
查询层级(深度)非常简单,因为
SELECT descendant FROM CategoryTree WHERE ancestor=0 AND distance=3
这个就不详细说了,非常简单。
4.5 插入
插入和移动就不是那么方便了,当一个节点插入到某个父节点下方时,它将具有与父节点相似的路径,然后再加上一个自身连接即可。
所以插入操作需要两条语句,第一条复制父节点的所有记录,并把这些记录的
INSERT INTO CategoryTree(ancestor,descendant,distance) (SELECT ancestor,10,distance+1 FROM CategoryTree WHERE descendant=5)
然后就是加入自身连接的记录。
INSERT INTO CategoryTree(ancestor,descendant,distance) VALUES(10,10,0)
4.6 移动
节点的移动没有很好的解决方法,因为新位置所在的深度、路径都可能不一样,这就导致移动操作不是仅靠UPDATE语句能完成的,这里选择删除+插入实现移动。
另外,在有子树的情况下,上级节点的移动还将导致下级节点的路径改变,所以移动上级节点之后还需要修复下级节点的记录,这就需要递归所有下级节点。
删除id=5节点的所有记录
DELETE FROM CategoryTree WHERE descendant=5
然后配合上面一节的插入操作实现移动。具体的实现直接上代码吧。
ClosureTableCategoryStore.java是主要的逻辑,这里只展示部分代码
/**
* 将一个分类移动到目标分类下面(成为其子分类)。被移动分类的子类将自动上浮(成为指定分类
* 父类的子分类),即使目标是指定分类原本的父类。
* <p>
* 例如下图(省略顶级分类):
* 1 1
* | / | \
* 2 3 4 5
* / | \ move(2,7) / \
* 3 4 5 ---------------> 6 7
* / \ / / | \
* 6 7 8 9 10 2
* / / \
* 8 9 10
*
* @param id 被移动分类的id
* @param target 目标分类的id
* @throws IllegalArgumentException 如果id或target所表示的分类不存在、或id==target
*/
public void move(int id, int target) {
if(id == target) {
throw new IllegalArgumentException("不能移动到自己下面");
}
moveSubTree(id, categoryMapper.selectAncestor(id, 1));
moveNode(id, target);
}
/**
* 将一个分类移动到目标分类下面(成为其子分类),被移动分类的子分类也会随着移动。
* 如果目标分类是被移动分类的子类,则先将目标分类(连带子类)移动到被移动分类原来的
* 的位置,再移动需要被移动的分类。
* </p><p>
* 例如下图(省略顶级分类):
* 1 1
* | |
* 2 7
* / | \ moveTree(2,7) / | \
* 3 4 5 ---------------> 9 10 2
* / \ / | \
* 6 7 3 4 5
* / / \ |
* 8 9 10 6
* |
* 8
*
* @param id 被移动分类的id
* @param target 目标分类的id
* @throws IllegalArgumentException 如果id或target所表示的分类不存在、或id==target
*/
public void moveTree(int id, int target) {
/* 移动分移到自己子树下和无关节点下两种情况 */
Integer distance = categoryMapper.selectDistance(id, target);
if (distance == null) {
// 移动到父节点或其他无关系节点,不需要做额外动作
} else if (distance == 0) {
throw new IllegalArgumentException("不能移动到自己下面");
} else {
// 如果移动的目标是其子类,需要先把子类移动到本类的位置
int parent = categoryMapper.selectAncestor(id, 1);
moveNode(target, parent);
moveSubTree(target, target);
}
moveNode(id, target);
moveSubTree(id, id);
}
/**
* 将指定节点移动到另某节点下面,该方法不修改子节点的相关记录,
* 为了保证数据的完整性,需要与moveSubTree()方法配合使用。
*
* @param id 指定节点id
* @param parent 某节点id
*/
private void moveNode(int id, int parent) {
categoryMapper.deletePath(id);
categoryMapper.insertPath(id, parent);
categoryMapper.insertNode(id);
}
/**
* 将指定节点的所有子树移动到某节点下
* 如果两个参数相同,则相当于重建子树,用于父节点移动后更新路径
*
* @param id 指定节点id
* @param parent 某节点id
*/
private void moveSubTree(int id, int parent) {
int[] subs = categoryMapper.selectSubId(id);
for (int sub : subs) {
moveNode(sub, parent);
moveSubTree(sub, sub);
}
}</p>其中的categoryMapper 是Mybatis的Mapper,这里只展示部分代码
/**
* 查询某个节点的第N级父节点。如果id指定的节点不存在、操作错误或是数据库被外部修改,
* 则可能查询不到父节点,此时返回null。
*
* @param id 节点id
* @param n 祖先距离(0表示自己,1表示直属父节点)
* @return 父节点id,如果不存在则返回null
*/
@Select("SELECT ancestor FROM CategoryTree WHERE descendant=#{id} AND distance=#{n}")
Integer selectAncestor(@Param("id") int id, @Param("n") int n);
/**
* 复制父节点的路径结构,并修改descendant和distance
*
* @param id 节点id
* @param parent 父节点id
*/
@Insert("INSERT INTO CategoryTree(ancestor,descendant,distance) " +
"(SELECT ancestor,#{id},distance+1 FROM CategoryTree WHERE descendant=#{parent})")
void insertPath(@Param("id") int id, @Param("parent") int parent);
/**
* 在关系表中插入对自身的连接
*
* @param id 节点id
*/
@Insert("INSERT INTO CategoryTree(ancestor,descendant,distance) VALUES(#{id},#{id},0)")
void insertNode(int id);
/**
* 从树中删除某节点的路径。注意指定的节点可能存在子树,而子树的节点在该节点之上的路径并没有改变,
* 所以使用该方法后还必须手动修改子节点的路径以确保树的正确性
*
* @param id 节点id
*/
@Delete("DELETE FROM CategoryTree WHERE descendant=#{id}")
void deletePath(int id);5.结语
在分析推论后,终于找到了一种既有查询简单、效率高等优点,也符合数据库设计范式,而且是真正的无限级分类的设计。本方案的写入操作虽然需要递归,但相比于前序遍历树效率仍高出许多,并且在本博客系统中分类不会频繁修改。可见对于在关系数据库中存储一棵树的需求,ClosureTable是一种比较完美的解决方案。
今天关于《在数据库中存储一棵树,实现无限级分类》的内容介绍就到此结束,如果有什么疑问或者建议,可以在golang学习网公众号下多多回复交流;文中若有不正之处,也希望回复留言以告知!
MySQL:插入更新语句ON DUPLICATE KEY UPDATE
- 上一篇
- MySQL:插入更新语句ON DUPLICATE KEY UPDATE
- 下一篇
- mysql,sql server等数据库连接集成库
-
- 数据库 · MySQL | 2天前 |
- MySQL数值函数大全及使用技巧
- 117浏览 收藏
-
- 数据库 · MySQL | 3天前 |
- 三种登录MySQL方法详解
- 411浏览 收藏
-
- 数据库 · MySQL | 4天前 |
- MySQL数据备份方法与工具推荐
- 420浏览 收藏
-
- 数据库 · MySQL | 4天前 |
- MySQL数据备份方法与工具推荐
- 264浏览 收藏
-
- 数据库 · MySQL | 5天前 |
- MySQL索引的作用是什么?
- 266浏览 收藏
-
- 数据库 · MySQL | 6天前 |
- MySQL排序原理与实战应用
- 392浏览 收藏
-
- 数据库 · MySQL | 1星期前 |
- MySQLwhere条件查询技巧
- 333浏览 收藏
-
- 数据库 · MySQL | 1星期前 |
- MySQL常用数据类型有哪些?怎么选更合适?
- 234浏览 收藏
-
- 数据库 · MySQL | 1星期前 |
- MySQL常用命令大全管理员必学30条
- 448浏览 收藏
-
- 数据库 · MySQL | 1星期前 |
- MySQL高效批量插入数据方法大全
- 416浏览 收藏
-
- 数据库 · MySQL | 1星期前 |
- MySQL性能优化技巧大全
- 225浏览 收藏
-
- 数据库 · MySQL | 1星期前 |
- MySQL数据备份4种方法保障安全
- 145浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 485次学习
-
- ChatExcel酷表
- ChatExcel酷表是由北京大学团队打造的Excel聊天机器人,用自然语言操控表格,简化数据处理,告别繁琐操作,提升工作效率!适用于学生、上班族及政府人员。
- 3182次使用
-
- Any绘本
- 探索Any绘本(anypicturebook.com/zh),一款开源免费的AI绘本创作工具,基于Google Gemini与Flux AI模型,让您轻松创作个性化绘本。适用于家庭、教育、创作等多种场景,零门槛,高自由度,技术透明,本地可控。
- 3393次使用
-
- 可赞AI
- 可赞AI,AI驱动的办公可视化智能工具,助您轻松实现文本与可视化元素高效转化。无论是智能文档生成、多格式文本解析,还是一键生成专业图表、脑图、知识卡片,可赞AI都能让信息处理更清晰高效。覆盖数据汇报、会议纪要、内容营销等全场景,大幅提升办公效率,降低专业门槛,是您提升工作效率的得力助手。
- 3424次使用
-
- 星月写作
- 星月写作是国内首款聚焦中文网络小说创作的AI辅助工具,解决网文作者从构思到变现的全流程痛点。AI扫榜、专属模板、全链路适配,助力新人快速上手,资深作者效率倍增。
- 4528次使用
-
- MagicLight
- MagicLight.ai是全球首款叙事驱动型AI动画视频创作平台,专注于解决从故事想法到完整动画的全流程痛点。它通过自研AI模型,保障角色、风格、场景高度一致性,让零动画经验者也能高效产出专业级叙事内容。广泛适用于独立创作者、动画工作室、教育机构及企业营销,助您轻松实现创意落地与商业化。
- 3802次使用
-
- golang MySQL实现对数据库表存储获取操作示例
- 2022-12-22 499浏览
-
- 搞一个自娱自乐的博客(二) 架构搭建
- 2023-02-16 244浏览
-
- B-Tree、B+Tree以及B-link Tree
- 2023-01-19 235浏览
-
- mysql面试题
- 2023-01-17 157浏览
-
- MySQL数据表简单查询
- 2023-01-10 101浏览

