MySQL 8.0+递归CTE无需开启SQL_MODE,ERROR 1221实为遗漏RECURSIVE关键字所致;必须显式写WITH RECURSIVE,且锚点在UNION ALL前、递归部分需WHERE终止,cte_max_recursion_depth仅是熔断上限而非开关。

MySQL递归CTE必须开启`SQL_MODE`才能用
MySQL 8.0+ 支持递归 WITH RECURSIVE,但默认不启用——如果执行时报错 ERROR 1221 (HY000): Incorrect usage of UNION and ORDER BY 或直接提示语法错误,大概率是 sql_mode 缺少 RECURSIVE 标志。不是版本问题,而是配置没开。
检查方式:SELECT @@sql_mode;,确认输出包含 RECURSIVE;若没有,需在启动配置或会话中显式设置:
SET SESSION sql_mode = CONCAT(@@sql_mode, ',RECURSIVE');
注意:仅会话级设置对当前连接生效,生产环境建议写入 my.cnf 的 [mysqld] 段落,加一行 sql_mode = "STRICT_TRANS_TABLES,NO_ENGINE_SUBSTITUTION,RECURSIVE"(保留原有关键模式)。
树形分组统计必须区分“层级聚合”和“路径聚合”
常见需求如“每个部门下员工数、下属部门数、总薪资”,不能只靠一层 GROUP BY——父节点要汇总所有子孙数据。这时递归 CTE 要拆成两步:先生成完整路径关系(含深度、路径字符串),再对路径结果做聚合。
假设表 orgs 有 id、name、parent_id,员工表 employees 有 org_id、salary:
- 递归部分只负责展开组织树,产出每个节点的全部祖先链(例如
1->3->7表示 id=7 属于 id=3 的子部门,而 id=3 又属 id=1),不在此阶段 JOIN 员工表——否则会因重复展开导致计数翻倍 - 聚合必须放在递归 CTE 外层,用
LEFT JOIN employees ON employees.org_id = t.id关联,再按t.id分组统计 - 若需“本级+所有下级”的汇总(如部门总人数),则关联时应 JOIN 到递归结果中的
ancestor_id(即路径中每个上级),而非仅t.id
MAX_RECURSION_DEPTH 默认值太小,深树易触发中断
MySQL 默认递归上限是 1000 层(@@cte_max_recursion_depth),但实际业务中组织树很少超 10 层;真正危险的是误写成无限递归——比如 ON t.parent_id = r.id 写反为 ON t.id = r.parent_id,或漏掉 WHERE r.parent_id IS NOT NULL 终止条件,会导致查询卡死或报错 ERROR 3636 (HY000): Recursive query aborted after 1001 iterations。
面向 CRM 系统的 MySQL 8 数据库模式设计。当用户需要为客户关系管理(CRM)系统设计、评审、优化或生成数据库模式时,应使用此项技能。
安全做法:
- 递归锚点(anchor)必须严格限定为根节点,例如
WHERE parent_id IS NULL或WHERE level = 1 - 递归成员(recursive member)的 JOIN 条件必须指向“子→父”或“父→子”单向,不可双向模糊匹配
- 上线前用
SELECT COUNT(*) FROM orgs和最大深度估算递归量,必要时临时调高:SET SESSION cte_max_recursion_depth = 500;
分组统计后无法直接排序树形结构
递归 CTE 输出的层级顺序(如 level)和最终聚合结果的分组顺序是两回事。执行完 WITH RECURSIVE ... SELECT ... GROUP BY ... ORDER BY level,看起来层级整齐,但一旦加入 SUM()、COUNT() 等聚合,ORDER BY level 就失效——因为分组后原始行已丢失。
正确做法是:把递归结果作为子查询或命名 CTE,在外层再套一层查询做排序:
WITH RECURSIVE tree AS ( SELECT id, name, parent_id, 1 AS level FROM orgs WHERE parent_id IS NULL UNION ALL SELECT o.id, o.name, o.parent_id, t.level + 1 FROM orgs o INNER JOIN tree t ON o.parent_id = t.id ) SELECT t.id, t.name, t.level, COUNT(e.id) AS emp_count, SUM(e.salary) AS total_salary FROM tree t LEFT JOIN employees e ON e.org_id = t.id GROUP BY t.id, t.name, t.level ORDER BY t.level, t.id;
注意 ORDER BY 必须在外层,且要包含分组字段(如 t.id)避免 MySQL 5.7 兼容模式报错;若需按树形缩进显示,可用 REPEAT(' ', t.level - 1) 拼接前缀,但别在 GROUP BY 里用它——会破坏分组粒度。
树形统计真正的复杂点不在语法,而在厘清“谁汇总谁”:是每个节点统计自己直属下级,还是穿透到叶子?递归展开时是否允许跨层级跳转?这些逻辑错一点,COUNT 就差十倍。










