MySQL旧版本因无递归CTE,用SELF JOIN查树形结构会因层级写死和行数爆炸出错;正确做法是LEFT JOIN明确层级语义,8.0+应优先用WITH RECURSIVE。

为什么直接用 SELF JOIN 查树形结构会出问题
MySQL 没有原生递归 CTE(直到 8.0+ 才支持),所以用 SELF JOIN 查多层树(比如无限级分类、组织架构)时,必须手动写多层 JOIN。这导致两个硬伤:一是层级写死,查 5 层就得写 5 次 JOIN;二是结果行数爆炸——每条路径生成一行,父子关系一多就重复冗余。
常见错误现象:SELECT * FROM category t1 JOIN category t2 ON t1.id = t2.parent_id 只能拿到二级,想查三级还得再 JOIN category t3,但 t3 的 ON 条件容易写成 t2.id = t3.parent_id(对),也可能错写成 t1.id = t3.parent_id(漏掉中间层)。
使用场景有限:仅适合已知最大深度 ≤3 的静态结构(如省市区三级表),且数据量小;不适用于评论回复链、BOM 物料清单这类动态深度结构。
怎么写一个安全的 3 层 SELF JOIN 示例
核心是明确每一层的语义和连接条件,避免歧义:
SELECT t1.name AS level1, t2.name AS level2, t3.name AS level3 FROM category t1 LEFT JOIN category t2 ON t2.parent_id = t1.id LEFT JOIN category t3 ON t3.parent_id = t2.id WHERE t1.parent_id IS NULL;
关键点:
安装、修复、更新或打包一个基于 Python 和 PyMySQL 实现的 Windows `mysql` 命令。适用于用户希望在 PowerShell/CMD 中输入 `mysql` 时重定向到 Python 实现、需要交互式输入 MySQL 连接信息、需要同步到当前用户本地 bin 目录,或需要将该能力封装成可公开复用的 Codex skill。
-
t1是根节点(parent_id IS NULL),不是随便一张表别名 - 全部用
LEFT JOIN,确保根节点即使没子节点也能显示 - 连接条件严格按「下级的
parent_id= 上级的id」,顺序不能反 - 如果只要叶子节点,加
WHERE t3.id IS NOT NULL AND t3.parent_id IS NOT NULL(需结合业务判断)
MySQL 8.0+ 应该优先用 WITH RECURSIVE 而不是 SELF JOIN
WITH RECURSIVE 是专为树形设计的语法,可动态遍历任意深度:
WITH RECURSIVE tree AS ( SELECT id, name, parent_id, 1 AS depth FROM category WHERE parent_id IS NULL UNION ALL SELECT c.id, c.name, c.parent_id, t.depth + 1 FROM category c INNER JOIN tree t ON c.parent_id = t.id ) SELECT * FROM tree ORDER BY depth, id;
优势明显:
- 不用预估层数,自动终止于无子节点
- 可加
depth控制最大递归深度(SET SESSION cte_max_recursion_depth = 10) - 结果天然带层级序号,方便前端渲染缩进
- 性能通常优于多层
JOIN,尤其深度 >3 时
如果只能用 SELF JOIN,必须防住这 3 个坑
真实业务中常因疏忽导致查询空、重复或超时:
- 忘记给
parent_id字段建索引 → 全表扫描,三层JOIN变笛卡尔积 - 把
NULL当作有效值处理:例如WHERE t2.parent_id = t1.id会过滤掉所有t2.parent_id IS NULL行,但根节点的parent_id就是NULL,逻辑错位 - 没限制起始点:不加
WHERE t1.parent_id IS NULL或指定某个t1.id = 123,结果包含所有可能路径,数据不可控
真正难的不是写出 JOIN 语句,而是厘清“我要查的是从哪个节点出发的哪几层”,以及是否接受结果里混着不同深度的碎片行。用错一次,排查起来比重写成递归还费时间。










