MySQL需CAST转字符并显式初始化path,PostgreSQL用ARRAY防注入且需NOT c.id = ANY(tp.path)防循环;二者递归成员均禁用ORDER BY/GROUP BY。

MySQL 8.0+ 和 PostgreSQL 都支持 WITH RECURSIVE,但路径拼接方式、类型安全和循环防护机制差异极大——直接照搬语法大概率查出乱码、截断或无限递归。
MySQL 8.0+ 中 CONCAT 拼路径必须做 CAST 类型转换
MySQL 对字符串长度和类型敏感,id 是整数,直接 CONCAT(tp.path, '/', c.id) 会导致隐式转换失败或截断(尤其当 path 超过默认 CHAR(1) 宽度时)。
- 锚点部分必须显式
CAST(id AS CHAR(255))初始化路径字段,否则递归中CONCAT会按最窄类型推导,后续拼接越长越容易被截断 - 递归部分拼接时,
c.id也要CAST(c.id AS CHAR),不能依赖隐式转换 - 分隔符建议用
'/'而非'.',避免和数值小数点混淆,也方便后续正则或SUBSTRING_INDEX提取
WITH RECURSIVE tree_path AS (
SELECT id, parent_id, name, CAST(id AS CHAR(255)) AS path
FROM categories
WHERE parent_id IS NULL
UNION ALL
SELECT c.id, c.parent_id, c.name,
CONCAT(tp.path, '/', CAST(c.id AS CHAR))
FROM categories c
INNER JOIN tree_path tp ON c.parent_id = tp.id
)
SELECT * FROM tree_path;PostgreSQL 用 ARRAY 存路径比字符串更安全
PostgreSQL 的 ARRAY 类型天然防注入、防非法 ID 拼接,且支持 @> 运算符做子树判定,不用字符串 LIKE 或正则匹配。
- 锚点写法:
ARRAY[id] AS path,类型自动推导为integer[] - 递归拼接用
tp.path || c.id,不是CONCAT,语义清晰且类型一致 - 防循环必须加
WHERE NOT c.id = ANY(tp.path),否则自引用节点(如parent_id = id)会爆栈 - 输出可读路径时,统一在最终
SELECT里用array_to_string(path, '/'),别在 CTE 内转——否则无法利用数组索引下推
WITH RECURSIVE tree_path AS ( SELECT id, parent_id, name, ARRAY[id] AS path FROM categories WHERE parent_id IS NULL UNION ALL SELECT c.id, c.parent_id, c.name, tp.path || c.id FROM categories c INNER JOIN tree_path tp ON c.parent_id = tp.id WHERE NOT c.id = ANY(tp.path) -- 关键:防自环 ) SELECT id, name, array_to_string(path, '/') AS path FROM tree_path;
用路径结果做 JOIN 关联时,别在递归 CTE 里 ORDER BY 或 GROUP BY
WITH RECURSIVE 的递归成员(UNION ALL 右侧)禁止出现 ORDER BY、GROUP BY、聚合函数,否则 MySQL 报错 ERROR 3642,PostgreSQL 报 ERROR: recursive query "xxx" does not have the form non-recursive-term UNION [ALL] recursive-term。
- 排序、去重、分组必须放在最外层
SELECT中,例如SELECT * FROM tree_path ORDER BY path - 如果要关联另一张表(比如查每个部门的员工数),先生成完整路径 CTE,再在外层
JOIN,不要试图在递归内部LEFT JOIN员工表 - 路径字段若用于
JOIN条件,注意 MySQL 字符串比较默认忽略末尾空格,而 PostgreSQL 区分;建议两端都用TRIM()或统一用ARRAY避免歧义
真正影响性能的不是递归本身,而是 parent_id 和 id 上缺索引
递归查询本质是多次单层 JOIN,每次都要根据 parent_id 找子节点。没索引时,每层都是全表扫描,10 层深度就可能扫 10×N 行。
- 必须在
parent_id字段建索引,(parent_id, id)联合索引更佳,覆盖查询所需字段 - 根节点筛选条件(如
WHERE name = '技术部')如果不在锚点里,而是放到外层,会导致先展开整棵树再过滤,极慢 - PostgreSQL 若用
hierarchyid(需扩展支持),物理存储结构优化了父子跳转,但前提是字段真实存在且已建 GIST 索引,不是光改类型就行
路径拼接看着只是字符串操作,但类型、索引、循环防护三者漏一,轻则结果错乱,重则查询卡死。别急着抄示例,先看执行计划里有没有 Using index condition 和 Recursive 步骤。

















