递归CTE是处理动态深度层级关系的唯一可靠方式,因JOIN为静态结构无法适配未知层级,且易引发性能退化、循环引用和字符串注入等问题;PostgreSQL推荐用ARRAY安全存路径并加防循环判断,SQL Server需设MAXRECURSION,MySQL 8.0+须用逗号分隔字符串并建前缀索引。

直接用递归 CTE(WITH RECURSIVE)是唯一靠谱方式,硬写 N 层 JOIN 或子查询不仅难维护,还极易因层级变化崩掉——而且几乎所有主流数据库(PostgreSQL、SQL Server、MySQL 8.0+、Oracle)都支持它。
为什么不能用普通 JOIN 写多层父子路径?
自关联查层级本质是“动态深度遍历”,而 JOIN 是静态结构:你写 3 层 JOIN,就只能查到第 3 级;写 5 层,第 6 级就丢数据。更麻烦的是:
- 每加一层,执行计划复杂度指数上升,
EXPLAIN显示type很可能退化成ALL或index,rows翻倍增长 - 无法统一处理循环引用(比如
parent_id = id的脏数据),一查就卡死或报错 - 路径拼接依赖字符串操作(如
CONCAT),遇到含斜杠、单引号的name字段会破坏结构,甚至引入注入风险
PostgreSQL 中用 ARRAY 做安全路径管理
别碰 CONCAT(tp.path, '/', c.id) 这类字符串拼接。PostgreSQL 的 ARRAY 类型天生防污染、防注入,还能配合 @> 做高效祖先判断:
WITH RECURSIVE tree_path AS ( SELECT id, parent_id, name, ARRAY[id] AS path FROM categories WHERE parent_id IS NULL <p>UNION ALL</p><p>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 c.id != ALL(tp.path) -- 必加!防自引用循环 ) SELECT id, name, array_to_string(path, '/') AS path_str FROM tree_path ORDER BY path;</p>
-
ARRAY[id]初始化路径,tp.path || c.id扩展,全程类型安全 -
WHERE c.id != ALL(tp.path)是保命语句,漏掉就会无限递归 -
array_to_string()放在最终SELECT,别在 CTE 里转——否则路径字段无法走索引,后续WHERE path @> ARRAY[123]会失效
SQL Server 和 MySQL 8.0+ 的等效写法与关键差异
语法骨架类似,但控制递归终止和路径拼接方式不同:
- SQL Server 必须加
OPTION (MAXRECURSION n),比如OPTION (MAXRECURSION 32),不写默认只跑 100 层,超了直接报错Msg 530 - MySQL 8.0+ 不支持
ARRAY,路径得用字符串,但必须用CAST(id AS CHAR)初始化,并在递归中用CONCAT(tp.path, ',', c.id)——逗号比斜杠更安全,因为分类名里很少含逗号 - 所有数据库都要确保
parent_id字段有索引,否则递归每轮都要全表扫categories,EXPLAIN里key列会是NULL
最常被忽略的一点:路径字段(无论 ARRAY 还是 VARCHAR)如果要用于高频祖先查询(比如“查所有属于某父节点的子节点”),必须在生成后建函数索引(PostgreSQL)或前缀索引(MySQL),否则 @> 或 LIKE '123,%' 都走不了索引。这步不做,再好的 CTE 也扛不住高并发查询。










