mysql 8.0 递归 cte 性能差的根本原因是执行模型导致每轮迭代全表扫描、中间结果物化开销大、索引无法下推且不支持并行;非递归 cte 默认物化也引发过滤下推失败和无索引问题。

MySQL 8.0 的 CTE(尤其是递归 CTE)性能不理想,根本原因不是语法本身慢,而是执行模型和优化器限制导致中间结果物化开销大、索引利用受限、且容易触发隐式瓶颈。直接改写成 JOIN 或子查询反而更快的情况很常见。
递归 CTE 实际是迭代执行,每轮都走一次全表扫描
MySQL 不像 PostgreSQL 那样对递归成员做“下推条件”或“索引驱动递归”,而是把递归过程拆成多轮独立执行:先跑锚定查询得到 R0,再用 R0 去 JOIN 原表得 R1,再用 R1 去 JOIN 得 R2……每轮都可能扫全表或大范围索引。
- 即使
employees.manager_id有索引,第 2 轮 JOIN 时优化器仍可能选错驱动表,导致type=ALL出现在 EXPLAIN 中 -
EXPLAIN ANALYZE会显示多行 “Recursive query iteration”,每行的rows累加就是实际扫描量 —— 5 层树可能扫出 10 倍原始数据量 - 没有
WHERE在递归成员中过滤(比如AND e.status = 'active'),这个条件不会下推到每次 JOIN,只会最后才过滤
CTE 默认物化,中间结果无法复用索引
非递归 CTE 在 MySQL 8.0 中默认被物化(materialized)为临时表,这意味着:
- CTE 定义里的
WHERE条件不会下推到基表扫描,可能先取出 100 万行再过滤剩 100 行 - 物化表默认无索引,后续 JOIN 或排序只能走 filesort 或全表扫描
- 即使你在 CTE 外层加
ORDER BY id LIMIT 10,优化器也不会“提前终止”,仍会生成全部中间结果 - 想绕过物化?可以加
/*+ NO_MERGE() */提示(MySQL 8.0.22+),强制让优化器把 CTE 内联展开,但仅适用于非递归场景
递归深度和内存配置不当会直接触发崩溃或降级
递归 CTE 对 cte_max_recursion_depth 和内存敏感,超限不是报错那么简单:
- 默认
cte_max_recursion_depth = 1000,但真实业务树深常超 200(如多级分销、BOM 展开),设太大会耗尽sort_buffer_size或tmp_table_size - MySQL 8.0.32 存在已知 CORE dump 问题:当递归产出大量中间行(比如单轮返回 50 万+ 行),服务进程可能直接 crash —— 这在 8.4.4 已修复,但老版本必须规避
-
WITH RECURSIVE查询无法使用并行执行,全程单线程,CPU 利用率低但延迟高 - 若递归路径存在环(比如 A→B→C→A),MySQL 不自动检测,只会跑满 depth 后报错
Recursive query aborted after 1000 iterations,而非提示环路
什么情况下 CTE 反而比子查询慢得多
不是所有“看着简洁”的 CTE 都值得用。以下场景应优先考虑改写:
- 只查一层下属(
WHERE manager_id = ?):用普通查询或关联子查询,别套 CTE - CTE 里含聚合(
GROUP BY)又在外层 JOIN:物化后聚合结果丢失统计信息,优化器误判行数 - 递归 CTE 后接
ORDER BY ... LIMIT:MySQL 不支持递归中的LIMIT,必须生成全部结果再排序截断 - 用 CTE 模拟窗口函数行为(如
ROW_NUMBER() OVER (PARTITION BY x ORDER BY y)):直接用窗口函数,性能差一个数量级
真正影响性能的从来不是“用了 CTE”,而是没看清它背后每一轮迭代怎么扫表、中间结果存哪、以及你的数据分布是否匹配 MySQL 当前的物化策略。线上遇到慢,第一反应不该是加索引,而是用 EXPLAIN ANALYZE 数清到底扫了多少行、物化了几次、有没有隐式类型转换拖累 JOIN —— 这些细节比语法选择重要得多。











