不能直接递归引用自身表更新path字段,必须用递归CTE生成完整路径后JOIN更新;PostgreSQL用UPDATE...FROM,MySQL 8.0+需UPDATE JOIN CTE;需处理NULL、根节点格式、全量重算开销及深度限制问题。

UPDATE 语句里不能直接递归引用自身表
想用一条 UPDATE 把自引用树(比如 parent_id 指向同表的 id)的 path 字段(如 '/1/5/23')批量刷出来?别试 UPDATE t SET path = CONCAT((SELECT path FROM t AS p WHERE p.id = t.parent_id), '/', t.id) —— 大多数数据库会报错,比如 MySQL 提示 You can't specify target table 't' for update in FROM clause,PostgreSQL 则直接拒绝修改中子查询引用目标表。
根本原因是:标准 SQL 不允许在 UPDATE 的 SET 或 WHERE 子句中,对正被更新的表做非只读的子查询(哪怕你只是 SELECT)。这不是语法糖缺失,是事务一致性保护机制。
用 CTE + 递归查询生成路径,再 JOIN 更新
真正可行的做法是:先用递归 CTE 构建出所有节点的完整 path,再用这个结果集和原表 JOIN 更新。注意必须用可更新的 JOIN 形式(不同数据库语法略有差异):
- PostgreSQL:支持
UPDATE ... FROM,可直接 JOIN CTE - MySQL 8.0+:需用
UPDATE t JOIN (WITH RECURSIVE ...) AS cte ON t.id = cte.id SET t.path = cte.path - SQL Server:用
UPDATE t SET path = cte.path FROM t INNER JOIN (WITH RECURSIVE ...) AS cte ON t.id = cte.id
示例(PostgreSQL):
WITH RECURSIVE tree_path AS ( SELECT id, CAST(id AS TEXT) AS path FROM categories WHERE parent_id IS NULL UNION ALL SELECT c.id, tp.path || '/' || c.id::TEXT FROM categories c JOIN tree_path tp ON c.parent_id = tp.id ) UPDATE categories t SET path = tp.path FROM tree_path tp WHERE t.id = tp.id;
层级深度大时性能会断崖下跌
递归 CTE 在树深 > 100 层时,PostgreSQL 可能触发 ERROR: stack depth limit exceeded,MySQL 默认递归限制是 1000(可通过 max_recursive_depth 调,但不解决本质问题)。更麻烦的是:每次更新都要全量重算整棵树的路径,哪怕只改了一个叶子节点的 parent_id。
实际项目中建议:
- 如果树变动极少(如菜单结构半年一调),用 CTE 方案足够
- 如果频繁移动节点,改用「闭包表」或「嵌套集模型」替代路径字符串
- MySQL 用户注意:CTE 中的
CAST(... AS CHAR)比字符串拼接快,避免隐式转换拖慢递归
空字符串、NULL 和根节点的 path 格式要统一
根节点该设成 ''、'/' 还是 '/1'?这直接影响后续 LIKE '/1/%' 查询是否命中。常见错误是让根节点 path 为 NULL,结果 UPDATE 后全变成 NULL —— 因为 CONCAT(NULL, '/23') 结果还是 NULL。
务必提前约定并强制处理:
- 根节点
path统一设为'/' || id::TEXT(即'/1'),避免开头多斜杠 - 用
COALESCE(tp.path, '')替代裸tp.path防止拼接中断 - 更新前先清空旧
path:UPDATE categories SET path = NULL,再跑 CTE 更新,避免残留脏数据
路径字段不是“写一次就完事”,它和树结构强耦合,任何父级变更都意味着下游所有子路径失效——这点比外键约束还难自动维护。










