子查询无法直接递归查出完整父子路径,只能获取直接父级;MySQL 8.0+须用WITH RECURSIVE递归CTE,通过锚点初始化+JOIN自关联逐层CONCAT拼接路径,注意CAST长度、分隔符及租户隔离。

子查询无法直接递归查出完整父子路径
SQL 标准子查询(即非递归的 SELECT 嵌套)只能返回单层结果,不能自动展开树形结构。如果你有一张分类表 categories,字段含 id、name、parent_id,用普通子查询写 SELECT name, (SELECT name FROM categories c2 WHERE c2.id = c1.parent_id) AS parent_name FROM categories c1,最多只能拿到**直接父级**,拿不到“祖父→父→子”整条路径。
常见错误是反复嵌套子查询试图“向上挖三代”,但这样硬编码层级既不可维护,又无法处理深度不一的树(比如有的分类在第 5 层)。
MySQL 8.0+ 必须用 WITH RECURSIVE
真正能生成完整路径的是递归 CTE(Common Table Expression),不是普通子查询。它分两部分:锚点(起始行)和递归成员(自关联拼接)。
WITH RECURSIVE category_path AS (- 锚点:选叶子节点或根节点,例如
SELECT id, name, parent_id, CAST(name AS CHAR(1000)) AS path FROM categories WHERE parent_id IS NULL - 递归:用
JOIN关联自身,拼接路径:SELECT c.id, c.name, c.parent_id, CONCAT(cp.path, ' → ', c.name) FROM categories c JOIN category_path cp ON c.parent_id = cp.id - 最后
SELECT * FROM category_path即得每条记录的完整路径
注意:CAST 或 CONCAT 的长度要足够(如 CHAR(1000)),否则路径截断;递归默认限 1000 层,超深树需设 SET SESSION cte_max_recursion_depth = 2000。
PostgreSQL 用 CONNECT BY 或递归 CTE
PostgreSQL 不支持 Oracle 风格的 CONNECT BY,但递归 CTE 语法更简洁:
WITH RECURSIVE tree AS ( SELECT id, name, parent_id, ARRAY[name] AS path FROM categories WHERE parent_id IS NULL UNION ALL SELECT c.id, c.name, c.parent_id, t.path || c.name FROM categories c JOIN tree t ON c.parent_id = t.id ) SELECT id, name, array_to_string(path, ' → ') AS path FROM tree;
用 ARRAY 存路径比字符串拼接更安全,避免空值或特殊字符引发的截断;array_to_string 在最终输出时格式化,中间计算不易出错。
旧版 MySQL(5.7 及之前)只能靠应用层或自定义函数
没有递归 CTE 时,纯 SQL 几乎无法可靠生成任意深度路径。常见折中方案有:
- 在应用代码里循环查父级(比如 PHP/Python 中 while 查询
parent_id,逐层拼接),简单但 N+1 查询明显 - 建冗余字段
path(如/1/5/12/),增删改时由业务逻辑维护,查询快但一致性难保 - 写存储过程模拟递归,但调试困难、移植性差,且多数运维禁用存储过程
如果必须用老 MySQL 且数据量不大,临时表 + 循环更新也能凑合,但别指望它在高并发下稳定——路径字段一旦写错,整棵树就乱了。
递归路径本质是图遍历问题,SQL 只是工具;别硬拗子查询,该换思路时就换。

















