MySQL 8.0+ 必须用 WITH CTE 替代多层子查询,提升可读性、调试性与复用性;CTE 必须置于 SELECT 前、不可嵌套、需显式列名、支持顺序依赖与递归(须加 RECURSIVE)。

能用,而且必须用——只要你的 MySQL 版本 ≥ 8.0.0,就该把多层子查询全换成 WITH CTE。这不是锦上添花,是解决可读性、调试性和复用性硬伤的默认路径。
CTE 必须写在 SELECT 前面,不能嵌套也不能放错位置
很多人一写就报 ERROR 1064 (42000),根本原因不是语法错,而是位置错。WITH 不是“加个修饰”,它是整个语句的前置子句,必须紧贴主查询开头。
- ✅ 正确:
WITH recent_orders AS (...) SELECT ... FROM recent_orders; - ❌ 错误:
SELECT * FROM (WITH t AS (...) SELECT ...) s;—— CTE 禁止出现在子查询中 - ❌ 错误:
SELECT ..., (WITH t AS (...) SELECT 1) FROM ...;—— 不支持标量子查询里用 WITH - ❌ 错误:先写
SELECT,再补WITH—— 解析器直接拒绝,不给你机会运行
把重复子查询拎出来,起个有业务含义的名字
识别逻辑重复是第一步。比如同一张表被 JOIN 两次,或 WHERE 里反复出现 (SELECT AVG(salary) FROM employees WHERE dept_id = e1.dept_id),这就是典型信号。
- 先单独验证子查询是否能独立跑通,返回合理结果(如
dept_avg AS (SELECT dept_id, AVG(salary) AS avg_salary FROM employees GROUP BY dept_id)) - 列名尽量显式声明,避免隐式推断出错,尤其当子查询含表达式(如
id + 1)时,不声明会触发警告甚至失败 - 主查询里用
JOIN替代WHERE ... IN (SELECT ...),注意关联字段类型一致(INT对BIGINT可能触发隐式转换,拖慢性能)
多个 CTE 按顺序定义,后一个可引用前一个
这是模块化拆解的关键。比如算部门总薪 → 算人均 → 再按人均排名,三步可以写成三个 CTE,逐层依赖。
- 顺序敏感:
cte2能引用cte1,但cte1不能引用cte2 - 多个 CTE 用逗号分隔,结尾不加逗号
- 每个 CTE 的
AS ()里必须是完整、可独立执行的SELECT,不能是INSERT/UPDATE - 调试时可直接
SELECT * FROM cte1;验证中间结果,不用重跑整条长 SQL
递归 CTE 必须显式写 RECURSIVE,且锚点与递归部分要严格对齐
想查组织树、分类目录、文件路径?漏掉 RECURSIVE 关键字,哪怕逻辑完美也会卡在解析阶段,报 ERROR 1223 (HY000)。
- 必须写成
WITH RECURSIVE tree AS (...),WITH tree AS (...)无效 - 锚点(初始查询)和递归部分必须用
UNION ALL连接,UNION DISTINCT在递归中禁止使用 - 两个部分的字段数、类型、顺序必须一致;建议显式声明列名,如
WITH RECURSIVE org (id, name, level) AS (...) - 终止靠递归分支里的
WHERE控制(如WHERE level 或 <code>WHERE parent_id != id),不是靠行数或外部 LIMIT
最易被忽略的是:CTE 名在递归部分只能出现在 FROM 或 JOIN 右侧,不能在 WHERE 子查询里引用,也不能作为 LEFT JOIN 的右表——这些都会触发 ERROR 3641。递归不是“随便查查”,是引擎按轮次驱动的严格流程。


















