一、业务场景
在做问题分类、部门、菜单、商品类目等树形结构时,经常需要:
- 一次查出所有层级数据
- 自动生成
path(如:一级/二级/三级) - 把
path永久更新回表
本文以问题分类表 sys_problem_category 为例,手把手实现。
二、表结构
sql
sql
CREATE TABLE sys_problem_category (
problem_code VARCHAR(50) PRIMARY KEY,
problem_name VARCHAR(100),
parent_code VARCHAR(50),
path VARCHAR(500) COMMENT '层级路径'
);
三、需求
根据 problem_code / parent_code 关系,自动生成:
plaintext
根:SH001 售后问题
子:SH001 售后问题/DXBH001 对象编号问题
并把 path 更新回表。
四、完整 SQL(查询 + 更新)
1. 先查询预览(安全核对)
sql
sql
WITH RECURSIVE tree AS (
-- 1. 初始部分(Anchor):根节点
SELECT
problem_code,
problem_name,
parent_code,
CAST(CONCAT(problem_code, ' ', problem_name) AS VARCHAR(500)) AS path
FROM sys_problem_category
WHERE parent_code IS NULL OR parent_code = ''
UNION ALL
-- 2. 递归部分(Recursive):关联自己tree,找子节点
SELECT
t.problem_code,
t.problem_name,
t.parent_code,
CAST(CONCAT(tr.path, '/', t.problem_code, ' ', t.problem_name) AS VARCHAR(500)) AS path
FROM sys_problem_category t
JOIN tree tr
ON t.parent_code = tr.problem_code
)
SELECT * FROM tree ORDER BY path;
2. 直接更新回表
sql
sql
WITH RECURSIVE tree AS (
SELECT
problem_code,
problem_name,
parent_code,
CAST(CONCAT(problem_code, ' ', problem_name) AS VARCHAR(500)) AS path
FROM sys_problem_category
WHERE parent_code IS NULL OR parent_code = ''
UNION ALL
SELECT
t.problem_code,
t.problem_name,
t.parent_code,
CAST(CONCAT(tr.path, '/', t.problem_code, ' ', t.problem_name) AS VARCHAR(500)) AS path
FROM sys_problem_category t
JOIN tree tr
ON t.parent_code = tr.problem_code
)
UPDATE sys_problem_category c
JOIN tree t
ON c.problem_code = t.problem_code
SET c.path = t.path;
五、重点:WITH RECURSIVE 为什么能 "关联自己"?
很多人第一次看到下面这段时会懵:
sql
sql
WITH RECURSIVE tree AS (
-- 初始
SELECT ...
UNION ALL
-- 递归:FROM sys_problem_category t JOIN tree tr
SELECT ...
)
tree 是刚定义的,为什么下面就能 JOIN 它?
1. 递归 CTE 固定结构
WITH RECURSIVE 由两部分 组成,用 UNION ALL 连接:
plaintext
sql
WITH RECURSIVE cte AS (
初始查询(Anchor) -- 只执行1次,不引用cte
UNION ALL
递归查询(Recursive)-- 反复执行,引用cte自己
)
MySQL 把这两部分当成一个整体递归单元 ,允许第二部分引用自己的名字 cte(这里是 tree)MySQL。
2. 执行过程(迭代式,不是 "同时存在")
不是 "外层 tree 已经全部算好,再 JOIN",而是迭代生成:
-
第 1 轮:执行初始部分
- 查出所有根节点 ,放入临时结果集
tree(此时只有根)MySQL。
- 查出所有根节点 ,放入临时结果集
-
第 2 轮:执行递归部分
sql
cssFROM sys_problem_category t JOIN tree tr ON t.parent_code = tr.problem_code- 这里的
tree= 上一轮结果(根节点) - 找到根的直接子节点 ,拼 path,追加到
treeMySQL。
- 这里的
-
第 3 轮:再次执行递归部分
- 此时
tree= 根 + 一级子节点 - 找到一级子节点的子节点(二级) ,继续追加MySQL。
- 此时
-
...... 反复迭代
- 直到递归部分查不到新数据(没有更多子节点),停止MySQL。
一句话总结:
JOIN tree tr不是关联 "最终的大表",而是关联上一轮迭代的结果,层层往下钻,直到叶子节点MySQL。
3. 为什么不会死循环?
- 树结构天然无环(parent_code 不会循环指向自己)
- 每次迭代只找子节点,不会回头找父节点
- 无新行时自动终止MySQL
六、执行结果示例
原始数据:
表格
| problem_code | problem_name | parent_code | path |
|---|---|---|---|
| SH001 | 售后问题 | null | |
| DXBH001 | 对象编号问题 | SH001 |
执行后:
表格
| problem_code | problem_name | parent_code | path |
|---|---|---|---|
| SH001 | 售后问题 | null | SH001 售后问题 |
| DXBH001 | 对象编号问题 | SH001 | SH001 售后问题 / DXBH001 对象编号问题 |
七、适用场景 & 注意事项
- 适用 :菜单、部门、分类、组织架构等单父节点树形结构
- MySQL 版本 :必须 8.0+ (5.7 不支持
WITH RECURSIVE) - 安全:先执行查询预览,确认 path 格式正确再 UPDATE
- 维护:新增 / 修改节点后,重新跑一次 UPDATE 即可刷新全表 path
八、总结
WITH RECURSIVE= 初始查询 + 递归查询- 递归部分
JOIN tree是关联上一轮迭代结果,层层向下 - 一次 SQL 搞定查询全树 + 生成 path + 更新回表,简洁高效
递归 CTE 是处理树形结构的 "神器",理解了迭代执行过程,就再也不会疑惑 "为什么能关联自己" 了。