mysql 5.7 不支持递归存储过程和 with recursive,需用临时表+while循环模拟递归,适用于组织架构等父子查询,但性能随层级加深急剧下降,推荐升级至8.0+使用原生cte。

MySQL 5.7 里没法直接写递归存储过程
MySQL 原生不支持 WITH RECURSIVE(直到 8.0.1 才加入),而存储过程本身也没有内置的递归调用机制——你不能在过程体里直接用 CALL 调自己,会报 ERROR 1424 (HY000): Recursive stored functions and triggers are not allowed。这不是语法写错了,是引擎层禁掉的。
所以“用存储过程模拟递归”,本质是用循环 + 临时表/变量堆栈来手动展开递归逻辑。
- 适用场景:需要在老版本 MySQL(如 5.6/5.7)中查组织架构、分类树、BOM 展开等父子关系数据
- 核心思路:用
WHILE循环代替函数调用,用INSERT ... SELECT把下一层子节点不断追加进临时结果集 - 必须建临时表(
CREATE TEMPORARY TABLE),不能只靠用户变量存中间结果——变量无法承载多行、多列或跨循环迭代的集合数据
用临时表 + 循环实现层级遍历
假设有一张 category 表,含 id、name、parent_id 字段,要查某个节点的所有后代(含自身)。
关键步骤是:先插入根节点,再循环把当前层所有节点的子节点插入临时表,直到没新数据可插。
- 临时表结构要和目标字段一致,且建议加
UNIQUE KEY(id)防重复插入(父节点可能有多个子路径汇入) - 每次循环前用
SELECT COUNT(*)判断是否还有新子节点;若为 0 就LEAVE,否则继续 -
INSERT INTO ... SELECT的WHERE parent_id IN (SELECT id FROM temp_result)这句最容易漏掉NOT IN (SELECT id FROM temp_result)去重,导致死循环
CREATE PROCEDURE get_descendants(IN root_id INT)
BEGIN
DECLARE row_count INT DEFAULT 1;
DROP TEMPORARY TABLE IF EXISTS temp_result;
CREATE TEMPORARY TABLE temp_result (
id INT PRIMARY KEY,
name VARCHAR(100),
level INT DEFAULT 0
);
INSERT INTO temp_result VALUES (root_id, (SELECT name FROM category WHERE id = root_id), 0);
<p>WHILE row_count > 0 DO
INSERT INTO temp_result
SELECT c.id, c.name, t.level + 1
FROM category c
INNER JOIN temp_result t ON c.parent_id = t.id
WHERE c.id NOT IN (SELECT id FROM temp_result);</p><pre class="brush:php;toolbar:false;">SELECT ROW_COUNT() INTO row_count;END WHILE; END
性能和层级深度的实际限制
这种模拟递归的方式不是“慢一点”,而是有明确瓶颈:每轮循环都是一次全表扫描 + 子查询,当树深超过 10 层、节点数过万时,IN (SELECT ...) 和 NOT IN (SELECT ...) 会迅速退化成 O(n²) 复杂度。
- 优化点:给
category(parent_id)加索引,比什么都管用;临时表的id主键也必须存在 - 别指望它处理深度 > 20 的树——不是逻辑错,是 MySQL 在循环中反复创建/扫描临时表带来的 I/O 和锁开销会卡住
- 如果业务允许,优先考虑在应用层做递归(比如 Python 用 DFS 拉一次全量数据再过滤),比在数据库里硬扛更稳
MySQL 8.0+ 直接用 WITH RECURSIVE 更干净
如果你能升级,就别绕路写存储过程。原生递归 CTE 不仅语法清晰,还能被优化器识别并提前剪枝。
比如同样查后代:
WITH RECURSIVE tree AS ( SELECT id, name, parent_id, 0 AS level FROM category WHERE id = ? -- 根节点 UNION ALL SELECT c.id, c.name, c.parent_id, t.level + 1 FROM category c INNER JOIN tree t ON c.parent_id = t.id ) SELECT * FROM tree;
注意:WITH RECURSIVE 默认最大递归深度是 1000,超了会报 ERROR 3636 (HY000): Recursive query aborted after 1001 iterations,可通过设置 cte_max_recursion_depth 系统变量调整,但别盲目调太高——栈溢出风险真实存在。
真正麻烦的从来不是“怎么写出来”,而是没搞清:临时表方案是兜底手段,不是推荐方案;而 CTE 的递归深度和内存占用,在复杂关联下很容易被低估。











