mysql存储函数禁止递归调用,仅with recursive cte支持真递归;需注意union all、字段一致、索引优化及会话级深度/超时设置,且不适用超大规模树查询。

MySQL 函数本身不能递归调用
MySQL 的存储函数(CREATE FUNCTION)**明确禁止递归调用自身**,执行时会直接报错:ERROR 1424 (HY000): Recursive stored functions and triggers are not allowed.。这不是配置问题,而是内核级限制——哪怕你设了 cte_max_recursion_depth 或关掉 log_bin_trust_function_creators,也无效。
- 原因:函数设计用于返回单值、无副作用,递归易引发栈溢出和不可控状态,MySQL 索性一刀禁掉
- 替代方案只有两个:用
WITH RECURSIVECTE(推荐),或用存储过程(PROCEDURE)+ 循环模拟(不优雅,难维护) - 网上流传的
getChildList()类函数,本质是“字符串拼接模拟递归”,不是真递归,且严重依赖FIND_IN_SET和GROUP_CONCAT,数据量一过万就慢甚至截断
真正能递归的只有 WITH RECURSIVE CTE
MySQL 8.0+ 支持的递归查询,只存在于 WITH RECURSIVE 语法中,它不是函数,而是一次性临时结果集。它的递归行为由两部分驱动:锚定成员(起点)和递归成员(自连接),终止靠“没新行产出”自动结束。
- 向下查子树(如分类、组织架构):递归条件写成
t2.parent_id = t1.id - 向上查父链(如地区归属、菜单面包屑):写成
t2.id = t1.parent_id,注意别反了,反了就查空 - 必须用
UNION ALL,UNION会触发去重排序,极大拖慢速度,且可能意外截断重复 ID 的合法层级 - 字段数、类型、顺序在锚点和递归部分必须严格一致,否则报
ERROR 1248 (42000): Every derived table must have its own alias类错误
递归深度和超时的实际控制方式
默认最多递归 1000 层,但实际业务中常遇到“查到第 50 层就卡住”或“100 层就超时”,这不是 bug,是保护机制在起作用。
- 调高深度:运行时用
SET SESSION cte_max_recursion_depth = 5000;永久生效需SET GLOBAL cte_max_recursion_depth = 5000(需 SUPER 权限) - 防死循环超时:加
SET STATEMENT max_execution_time = 2000 FOR ...,单位毫秒,超时直接中断,比等 1000 层更安全 - 注意:这两个设置都**只对当前会话或语句生效**,应用层每次执行前得显式设置,ORM(如 MyBatis、Sequelize)里容易漏掉
- 索引必须落在递归连接字段上,比如
parent_id列没索引,10 万行表递归 5 层就可能秒变 30 秒
为什么别硬套 Oracle/PostgreSQL 写法
Oracle 的 START WITH ... CONNECT BY 和 PostgreSQL 的 WITH RECURSIVE 表面相似,但 MySQL 的实现更“朴素”:它不支持 ORDER SIBLINGS BY、不内置层级路径生成(sys_connect_by_path)、也不允许递归部分含 GROUP BY 或 LIMIT。
- 想拼路径?得自己用
CONCAT+ 递归累加,比如CONCAT(t1.path, '/', t2.id),初始path设为CAST(id AS CHAR) - 想限制每层最多取 3 个子节点?做不到,CTE 不支持分层
LIMIT,只能查完再用外层ROW_NUMBER() OVER (PARTITION BY level ORDER BY ...)过滤 - 最易忽略的一点:CTE 名字在递归部分**必须被显式 JOIN 或 WHERE 引用**,写成
FROM t2, recursion t3可以,但漏掉t3别名或写成FROM recursion直接报错
递归不是银弹,MySQL 的 CTE 递归适合几百到几千节点的树形结构;如果要查百万级目录或实时渲染多层无限展开菜单,该做预计算(闭包表 / 路径枚举)就别硬扛 CTE。










