部门表里每行存一个 parent_id 指向上级,想查出某个部门底下所有子孙部门,一条 where parent_id = 3 只能拿到直接子级,孙级就得一层层套查询。这种树形数据正是 sql递归查询 的主场。
标准写法:递归 CTE
MySQL 8.0、SQL Server、PostgreSQL 都支持 with recursive,结构是两段:锚点查询给出起点,递归部分拿上一步结果继续往下查,中间用 UNION ALL 连接:
WITH RECURSIVE t AS (SELECT id, name, parent_id FROM dept WHERE parent_id = 0 UNION ALL SELECT d.id, d.name, d.parent_id FROM dept d JOIN t ON d.parent_id = t.id) SELECT * FROM t;
想顺便带上层级,锚点里加一列 1 AS lvl,递归段写成 t.lvl + 1,查完按 lvl 排序就是一棵带深度的树;只要某个子树的话,在最外层加 where 条件过滤起点即可。
死循环是最常见的翻车点
数据里如果有环,比如 A 的上级是 B、B 的上级又绕回 A,UNION ALL 会一直递归到数据库报超出递归层数的错。MySQL 里把 UNION ALL 换成 UNION 可以去重挡住完全相同的行,但根治办法是把成环的脏数据修掉,录入时的校验比事后救火管用。
老版本怎么办
MySQL 5.7 没有递归 CTE,常见做法是程序里循环查询逐层往下拿,或者干脆在表里加一个 path 字段存整条祖先路径,查询时一个前缀匹配搞定,代价是挪动节点时要同步更新整棵子树的 path。
性能上注意递归的每一层都要走索引,parent_id 上的索引先建好,不确定快慢时把递归查询套一层 explain 看执行计划,每层走没走索引一目了然。组织树特别大、查询特别频繁的场景,物化路径或闭表方案比每次实时递归更稳。树不深、量不大时,递归 CTE 的可读性和维护性就是最优解。