做MySQL开发这几年,我一直有一个执念:能一条SQL写完的业务逻辑,绝不在代码里绕第二层循环。早期版本里,复杂查询基本靠子查询、临时表、变量硬撑,写出来的SQL又长又绕,过俩月自己看都头疼。直到MySQL 8.0正式支持CTE(Common Table Expression,公用表表达式),我的SQL写法才真正发生了一次升级。CTE算不上什么新概念,PostgreSQL、SQL Server、Oracle早就有了,但它的到来确实改变了MySQL上复杂查询的写法。
这篇文章不是官方文档的翻译,而是结合我实际项目和面试辅导中积累的经验,聊聊CTE在MySQL里到底怎么用、什么时候用、有哪些非踩不可的坑。内容从基础语法到递归查询,从性能优化到排查实录,尽量做到让你看完就能在自己的SQL里用起来。
1. 为什么需要CTE:子查询的麻烦与CTE的破局
1.1 从“一层套一层”的SQL说起
先从一个实际场景切入。假设你现在有三张表:用户表users、订单表orders、订单明细表order_items,需要统计“每个用户最近一笔订单的商品总金额”。在没有CTE的MySQL 5.7时代,我用子查询大概会写成这样:
sql复制SELECT
u.id,
u.name,
t.total_amount
FROM users u
JOIN (
SELECT
o.user_id,
SUM(oi.amount) AS total_amount
FROM orders o
JOIN (
SELECT order_id, SUM(price * quantity) AS amount
FROM order_items
GROUP BY order_id
) oi ON oi.order_id = o.id
JOIN (
SELECT user_id, MAX(created_at) AS max_created_at
FROM orders
GROUP BY user_id
) latest ON latest.user_id = o.user_id
AND latest.max_created_at = o.created_at
GROUP BY o.user_id
) t ON t.user_id = u.id;
这段SQL能跑,但一个小需求就得套三层子查询。同事接手的时候,第一反应是“这写的什么东西”。更麻烦的是,如果中间某一层结果需要在两个地方引用,子查询只能重复写两遍,SQL体积直接翻倍,后期改需求等于重写。
1.2 CTE到底改变了什么
CTE本质上是一个“命名的临时结果集”,你用WITH关键字给它起个名字,然后在后面的查询里反复引用。上面那段SQL用CTE重写,效果完全不一样:
sql复制WITH order_amount AS (
SELECT order_id, SUM(price * quantity) AS amount
FROM order_items
GROUP BY order_id
),
latest_orders AS (
SELECT user_id, MAX(created_at) AS max_created_at
FROM orders
GROUP BY user_id
),
user_latest_order AS (
SELECT
o.user_id,
SUM(oa.amount) AS total_amount
FROM orders o
JOIN order_amount oa ON oa.order_id = o.id
JOIN latest_orders lo ON lo.user_id = o.user_id
AND lo.max_created_at = o.created_at
GROUP BY o.user_id
)
SELECT
u.id,
u.name,
ulo.total_amount
FROM users u
JOIN user_latest_order ulo ON ulo.user_id = u.id;
差别很明显:第一,每个逻辑片段都有名字,读SQL像读文章的目录,而不是在一团括号里猜层数;第二,order_amount这个中间结果如果后面多处引用,直接写名字就行,不用复制一大段子查询。
1.3 哪些MySQL版本能用CTE
CTE是MySQL 8.0引入的功能,8.0之前完全不可用。很多刚入行的同学还在用5.7甚至5.6,跑WITH直接语法报错,这是最常见的“CTE坑”之一。
注意:只有MySQL 8.0及以上版本才支持CTE。如果你公司还在用5.7,要么推动升级,要么暂时继续用子查询/临时表。另外MySQL 8.0里常规CTE用的是
WITH cte_name AS (...),递归CTE必须写成WITH RECURSIVE cte_name AS (...),这两个别搞混。
实际工作中,我判断一个团队能不能推行CTE,第一条就是看生产环境MySQL版本。如果线上是5.7,写了CTE的SQL连测试都过不了,更别提上线。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. CTE基础语法:从WITH AS开始
2.1 最简单的CTE写法
CTE的基础语法非常轻量:
sql复制WITH cte_name AS (
SELECT ...
)
SELECT * FROM cte_name;
核心就三部分:WITH关键字、CTE名字、AS后面的括号子查询。括号里就是普通SELECT,执行完之后cte_name相当于一张“临时表”供后续查询使用。
我写个最简单的例子,统计每个部门的员工数:
sql复制WITH dept_stats AS (
SELECT department_id, COUNT(*) AS emp_count
FROM employees
GROUP BY department_id
)
SELECT
d.department_name,
ds.emp_count
FROM departments d
LEFT JOIN dept_stats ds ON ds.department_id = d.id;
这种写法最直接的价值是让主查询聚焦在“如何关联最终结果”,而不是去纠结中间的聚合逻辑。你完全可以把它理解成“给一段SELECT起个名字,后面随时Call它”。
2.2 多个CTE串联
连续多个CTE用逗号分隔,后面CTE可以引用前面CTE的结果。它的顺序是有讲究的:后面的CTE能看到前面的,但前面的看不到后面的。
sql复制WITH base AS (
SELECT id, user_id, amount, created_at
FROM orders
WHERE status = 'paid'
),
user_total AS (
SELECT user_id, SUM(amount) AS total_amount
FROM base
GROUP BY user_id
)
SELECT * FROM user_total;
这里user_total引用了base,顺序就是先定义base再定义user_total,反过来写就会报“Unknown column”或“Table doesn't exist”类的错误。
多CTE串联非常适合那种“一步算出结果,下一步基于结果再加工”的查询场景,比如先过滤有效数据,再按用户分组,再排个名次,每一步都是独立的逻辑块。
2.3 CTE的可见范围与命名规则
有几个细节我在面试里经常问别人,也是实际使用中容易翻车的点:
第一,CTE只在定义它的那条SQL语句中有效。你执行完这条SQL,CTE就消失了,不会像临时表一样留在会话里。这既是优点(不用手动DROP)也是限制(没法跨语句复用)。
第二,CTE名称在同一层级必须唯一,不能定义两个同名CTE。但如果内层子查询里也出现了同名CTE,则遵循“就近原则”,内层会遮蔽外层。
第三,CTE列名默认继承SELECT输出的列名,也可以在CTE名后面显式指定:
sql复制WITH cte (col1, col2) AS (
SELECT a, b FROM some_table
)
SELECT * FROM cte;
显式指定列名在递归CTE里几乎是必须的,因为递归部分列名可能不一致,我会在下一节重点讲。
3. 递归CTE实战:树结构查询最舒服的姿势
3.1 递归CTE语法拆解
递归CTE是CTE里最有含金量的部分。常规CTE是“一次算完”,递归CTE则是“反复迭代计算,直到没有新数据为止”。
语法结构如下:
sql复制WITH RECURSIVE cte_name (col1, col2) AS (
-- 初始查询(锚点成员)
SELECT ...
UNION ALL
-- 递归查询(递归成员,引用cte_name自身)
SELECT ... FROM cte_name WHERE ...
)
SELECT * FROM cte_name;
关键点有三处:
- 锚点成员:第一次返回的初始数据集。
- 递归成员:引用CTE自身进行迭代,每一轮产生的新结果会进入下一轮计算。
UNION ALL:MySQL递归CTE支持UNION和UNION ALL,用UNION ALL时结果直接拼接;用UNION时还会去重,但性能开销更大,大多数场景用UNION ALL就够。
3.2 场景一:部门层级树
这是最常见的递归需求。假设departments表结构是id, parent_id, name,我们要查出某个部门下所有子部门(包括多级):
sql复制WITH RECURSIVE dept_tree AS (
-- 锚点:从指定部门开始
SELECT id, parent_id, name, 1 AS level
FROM departments
WHERE id = 1
UNION ALL
-- 递归:找当前层的子部门
SELECT
d.id,
d.parent_id,
d.name,
dt.level + 1
FROM departments d
JOIN dept_tree dt ON d.parent_id = dt.id
)
SELECT id, parent_id, name, level
FROM dept_tree
ORDER BY level, id;
这个SQL的执行心理过程是这样的:先查出id=1的部门,然后不断往下找子部门,每轮level加一,直到没有任何记录能JOIN上为止。输出结果就是一棵完整的部门树。
我在实际项目中用这个方法查组织架构,比老写法“先查出所有部门,然后在应用层用递归组装树”省了非常多代码。SQL返回直接就是平铺的带层级数据,前端拿到就能渲染。
3.3 场景二:连续日期补全
另一个高频场景是“补全缺失日期”。比如报表按天统计订单量,但某些天没有订单,自然没有记录,直接GROUP BY日期会导致那天空洞。递归CTE可以先生成一段连续日期区间,再LEFT JOIN订单数据:
sql复制WITH RECURSIVE date_range AS (
SELECT DATE('2024-01-01') AS day_date
UNION ALL
SELECT DATE_ADD(day_date, INTERVAL 1 DAY)
FROM date_range
WHERE day_date < DATE('2024-01-31')
)
SELECT
dr.day_date,
COUNT(o.id) AS order_count
FROM date_range dr
LEFT JOIN orders o ON DATE(o.created_at) = dr.day_date
GROUP BY dr.day_date
ORDER BY dr.day_date;
这个用法在生成报表、数据看板时简直是神器。老办法要么在代码里循环生成日期,要么维护一张日期维度表。递归CTE一张SQL全搞定,而且边界可控,改个结束日期就行。
3.4 递归深度限制与性能
递归CTE不是无限递归的,MySQL提供了一个系统参数cte_max_recursion_depth,默认值是1000。超过这个深度,SQL会直接报错“Recursive query aborted after 1001 iterations”。
有些场景确实需要更深的递归,比如组织架构特别深的集团层级,可以通过SET调大:
sql复制SET SESSION cte_max_recursion_depth = 5000;
但我个人的经验是:能不用那么深就别用那么深,递归层级越多,扫描次数越大,性能越差。树结构超过几千层的情况在业务里几乎不存在,反而更可能是你的数据有循环引用(比如parent_id指回自己),导致递归永远结束不了。
注意:递归CTE死循环是生产事故级别的问题。MySQL默认的1000层限制虽然够用,但真遇到环状数据,1000次迭代也很恐怖。一定要在递归成员里加合理的终止条件,比如
WHERE dt.level < 20这种业务层级上限。
4. CTE与经典方案的横向对比
4.1 CTE vs 子查询
子查询是MySQL 5.7时代复杂查询的主力,问题在于可读性差、重复代码多。一个中间结果要被引用两次时,子查询得复制两份。
CTE的优势是逻辑清晰、可复用。但要注意,两者在性能上没有绝对优劣,MySQL优化器经常会把CTE和子查询转换成类似的执行计划。CTE不是“性能银弹”,它首先是“可维护性银弹”。
不过有一类场景CTE有机会更快:当一个CTE被引用多次时,优化器可以选择只物化一次然后复用,而子查询写两遍就等于执行两遍相似逻辑。MySQL 8.0优化器对CTE的物化策略有专门处理,后面性能章节我会细说。
4.2 CTE vs 临时表
临时表(Temporary Table)是我以前处理复杂SQL的常用方案:
sql复制CREATE TEMPORARY TABLE tmp_stats AS
SELECT department_id, COUNT(*) AS cnt
FROM employees
GROUP BY department_id;
SELECT * FROM tmp_stats;
DROP TEMPORARY TABLE tmp_stats;
临时表的优势是跨语句复用,一条SQL计算完,下一条SQL还能用。CTE做不到,它绑定在单条SQL内。但临时表的缺点也明显:
- 手动
CREATE和DROP,多两条命令。 - 会话结束前临时表一直存在,忘记
DROP可能占内存。 - 事务里用临时表,回滚时表结构变化不随事务回滚(InnoDB临时表在MySQL 8.0中行为有调整)。
我的建议是:只有“同一会话、多条SQL需要共享中间结果”时,才用临时表;单条SQL内部的中间计算,优先CTE。
4.3 CTE vs 视图
视图(View)适合长期复用的查询模板,它把一段SQL封装成“虚拟表”,可以跨会话、跨用户使用。CTE只是单条SQL里的“临时命名结果集”。
两者的定位完全不同:
- 视图:通用、持久化、权限控制。
- CTE:局部、临时、快速拆分逻辑。
实际项目中,我见过有人为了一个只在某条报表里用一次的逻辑去创建视图,结果视图越建越多,后面自己都分不清哪个视图是干什么的。正确姿势是:单条SQL内的中间层用CTE,真正多个地方复用的通用逻辑才建视图。
4.4 CTE vs 变量拼接
MySQL的自定义变量(@var)也能在SQL中传递中间值,但用起来很别扭:
sql复制SELECT @avg_price := AVG(price) FROM products;
SELECT * FROM products WHERE price > @avg_price;
这种写法有两个致命问题:变量是会话级别的,状态不可预知;而且变量的赋值和执行顺序在不同SQL模式下可能不同,排查起来非常头疼。CTE完全没有这些状态问题,每次执行都是独立的。
在我接触的项目里,用@变量做复杂计算的SQL基本都成了遗留代码,后来优化的时候全部改成了CTE或子查询。变量并不是不能用,但在追求可读性和可维护性的现代SQL开发中,它的优先级已经很低了。
5. CTE在真实项目中的三个典型用法
5.1 数据去重:ROW_NUMBER配合CTE
MySQL 8.0引入了窗口函数,和CTE是绝配。最常见的组合用法就是按某字段分组后只保留每组最新一条记录。
比如订单表里有历史状态记录,一个订单有多条状态变更,我们要取每个订单最新一条状态:
sql复制WITH ranked_orders AS (
SELECT
order_id,
status,
created_at,
ROW_NUMBER() OVER (
PARTITION BY order_id
ORDER BY created_at DESC
) AS rn
FROM order_status_log
)
SELECT order_id, status, created_at
FROM ranked_orders
WHERE rn = 1;
CTE负责先把开窗函数的结果算出来,外层查询只负责过滤rn = 1。这种写法逻辑非常清晰,也是我面试时最常让候选人手写的题目之一。换在以前,要么子查询套子查询,要么先查临时表,复杂度高一个量级。
5.2 多表统计报表
报表场景是CTE最能发光的地方。一张报表往往涉及多个维度的聚合,每个聚合都是一个独立的逻辑块,用CTE串联起来,主查询只关心最终汇总。
举个例子,统计“各区域销售额、订单量、客单价,以及对总销售额的占比”:
sql复制WITH region_sales AS (
SELECT
region_id,
SUM(order_amount) AS total_sales,
COUNT(*) AS order_count
FROM orders
WHERE created_at >= '2024-01-01'
AND created_at < '2024-02-01'
GROUP BY region_id
),
overall AS (
SELECT SUM(total_sales) AS grand_total
FROM region_sales
)
SELECT
rs.region_id,
rs.total_sales,
rs.order_count,
rs.total_sales / rs.order_count AS avg_order_value,
rs.total_sales / ov.grand_total AS sales_ratio
FROM region_sales rs
CROSS JOIN overall ov
ORDER BY rs.total_sales DESC;
这个报表如果用子查询写,占比计算要嵌套一层,区域聚合和总聚合混在一起,很容易把SUM函数用错。CTE把“区域聚合”和“全局聚合”拆成两个清晰步骤,后续再增加维度(比如按城市再拆一层),只需要在CTE里加GROUP BY条件,主查询几乎不动。
5.3 简化存储过程中的复杂逻辑
存储过程里经常会遇到多步骤数据处理。以前我写存储过程,碰到中间结果就建临时表,存储过程里一堆CREATE TEMPORARY TABLE和DROP TEMPORARY TABLE,可读性极差。
MySQL 8.0之后,存储过程内部的复杂计算可以直接用CTE完成。比如先算一批目标用户,再关联行为数据,再算转化,整个过程可以写成一条完整SQL,也可以拆成多个CTE嵌套在INSERT或UPDATE语句里:
sql复制INSERT INTO daily_user_stats (user_id, order_count, total_amount)
WITH valid_users AS (
SELECT id FROM users WHERE status = 'active'
),
user_orders AS (
SELECT user_id, COUNT(*) AS cnt, SUM(amount) AS total
FROM orders
WHERE created_at >= '2024-01-01'
AND created_at < '2024-01-02'
GROUP BY user_id
)
SELECT
vu.id,
COALESCE(uo.cnt, 0),
COALESCE(uo.total, 0)
FROM valid_users vu
LEFT JOIN user_orders uo ON uo.user_id = vu.id;
这在存储过程里非常实用,一个INSERT语句就把计算和写入合在一起,不需要创建局部表,代码大幅精简。
6. CTE性能与优化经验实录
6.1 别被“只执行一次”骗了
网上很多文章喜欢说“CTE只执行一次,所以性能更好”,这个说法过于绝对。MySQL 8.0确实支持CTE物化(Materialization),但是否物化取决于优化器的成本决策。优化器可以选择把CTE当作派生表内联展开,也可以选择物化成一个临时结果集。
在EXPLAIN分析中,你如果看到Materialized字样,说明CTE被物化了;如果看到CTE名称直接出现在表名位置,说明被内联了。两者没有绝对优劣,物化多了一次临时表写入,但多引用时可能省去重复计算。
6.2 什么时候适合用CTE
从性能角度讲,CTE最适合以下场景:
- 中间结果集较小:物化成本低,多次引用收益高。
- 同一CTE被引用多次:一次计算、多处使用。
- 递归场景:递归CTE是唯一SQL写法。
如果你不确定CTE有没有被物化,直接在EXPLAIN FORMAT=JSON里看materialized_from_subquery即可。
我自己在优化一个集团报表时,有一个CTE被引用了3次,内容是一个大表的聚合。在MySQL 8.0中,优化器自动选择了物化,整体查询耗时从原来的3次聚合变成了1次聚合加2次读取临时结果,性能提升了近一倍。这就是CTE多引用带来的实打实收益。
6.3 什么时候不该用CTE
CTE不是万能的,下面这些情况我更建议换方案:
- 中间结果集超大,而且只引用一次:此时物化会白白写入磁盘或临时表,性能不如直接内联子查询。
- 递归层次极深:递归CTE每一轮迭代都有开销,深层次下性能很差。
- 需要跨多条SQL共享中间结果:CTE做不到,临时表或视图更合适。
- 频繁更新的OLTP查询:CTE本质是SELECT层面的简化,不会改变索引使用策略,该走索引还是走索引,该全表扫还是全表扫。
还有一种情况:MySQL优化器对CTE的物化判断并不总是最优。如果你发现CTE被物化导致性能下降,可以尝试用优化器提示强制不物化:
sql复制WITH cte AS (
SELECT /*+ NO_MERGE */ ...
)
或者反过来,在合适时候给CTE加上合并提示。但这些属于高级调优手段,普通业务SQL先看执行计划即可。
6.4 实际项目中CTE的慢查询排查
CTE引发的慢查询,大多数不是因为CTE本身,而是因为CTE内部的查询没走索引。CTE只是语法糖,不会自动优化里面的JOIN和WHERE。
我遇到过这样一种情况:递归CTE查部门树,部门表只有几万行,但查询耗时超过10秒。用EXPLAIN一看,递归成员里对parent_id的JOIN是全表扫描,因为parent_id没建索引。加上索引后,查询时间降到几十毫秒。
注意:递归CTE对JOIN字段的索引要求非常高。锚点成员可能只查一行,递归成员会反复查询,每次查询都建索引与否,直接影响整体性能。遇到递归CTE慢,第一反应是检查递归成员里关联字段的索引。
在性能调优上,我的建议是:先写清晰可读的CTE版本,再通过各种报表场景压测;如果性能不达标,优先用EXPLAIN看走到哪个环节慢,而不是一上来就怀疑CTE这个功能本身。
7. 常见问题与排查技巧实录
7.1 版本不支持报错
- 报错信息:
Syntax error或You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version - 原因:MySQL版本低于8.0,不支持CTE语法。
- 解决办法:升级MySQL,或改用子查询/临时表兼容老版本。
我在帮一个客户排查SQL时,第一眼看到的就是这种语法错误,结果一查版本是5.7.30,当时就建议把SQL改回子查询先上线,后续再规划升级。排查任何SQL问题,第一件事永远是确认MySQL版本。
7.2 递归CTE死循环
- 报错信息:
Recursive query aborted after 1001 iterations. Try increasing @@cte_max_recursion_depth - 原因:递归成员里的终止条件没写对,或者数据本身存在循环引用。
- 解决办法:检查数据是否有环,在递归成员里增加
level控制,或者设置合理的最大递归深度。
案例:有一次我查权限菜单树,菜单表里有个菜单的parent_id指向了自己,导致递归CTE无限循环。最后在递归成员里加了WHERE dt.level < 10才终止。从那以后,我写递归CTE基本都会带一个层级上限作为兜底。
7.3 列名重复导致报错
- 报错信息:
Duplicate column name 'id' - 原因:多个CTE拼接时,如果不同SELECT里列名重复且没有显式处理,可能在后续引用时冲突。
- 解决办法:在CTE定义中显式指定列名,或者用别名区分。
sql复制WITH cte (dept_id, dept_name, parent_id) AS (
SELECT id, name, parent_id FROM departments
)
SELECT * FROM cte;
7.4 递归CTE中UNION与UNION ALL选择不当
UNION ALL不去重,可能会让递归产生重复行;UNION去重,但数据库要做交集判断,迭代很多次时性能下降明显。
在实际业务中,如果数据本身不会产生重复(比如树结构按ID递归),优先用UNION ALL。如果确实可能重复,先查数据情况,再决定是否用UNION,别无脑选UNION。
7.5 CTE与GROUP BY一起用注意什么
CTE可以在外部引用时再聚合,也可以在CTE内部聚合。区别在于执行顺序和语义。
sql复制-- CTE内部聚合
WITH cte AS (
SELECT dept_id, COUNT(*) AS cnt
FROM employees
GROUP BY dept_id
)
SELECT * FROM cte;
-- CTE外部聚合
WITH cte AS (
SELECT dept_id, id
FROM employees
)
SELECT dept_id, COUNT(*)
FROM cte
GROUP BY dept_id;
两种写法都能用,但内部聚合能更早缩小数据量,对后续JOIN更友好;外部聚合则保留了更多明细字段,后续可能还要用。具体选哪种,取决于你想让CTE扮演“汇总结果”还是“明细数据源”。
7.6 排查技巧速查表
| 问题现象 | 可能原因 | 优先排查项 |
|---|---|---|
| WITH语法报错 | MySQL版本低于8.0 | SELECT VERSION(); |
| 递归CTE超时/报错 | 数据环、缺少终止条件 | 检查数据,加level上限 |
| 引用CTE时报表不存在 | 顺序问题或拼写错误 | 检查WITH中定义顺序 |
| 查询结果重复 | UNION ALL导致 | 确认是否需要UNION |
| 性能慢 | 缺少索引、CTE被物化 | EXPLAIN查看执行计划 |
| JOIN字段报错 | 列名歧义 | 显式指定表别名和列名 |
这张表是我自己的排查清单,基本覆盖了日常开发中会遇到的大多数CTE问题。遇到新问题就往上补,慢慢形成自己的SQL排查手册。
8. 从CTE到SQL思维升级
8.1 从“写过程”到“写逻辑”
以前写复杂SQL,我的思维习惯是“第一步查什么、第二步存哪里、第三步怎么拼”,像是在写程序代码。CTE改变了这个模式:你只需要声明“我需要哪些中间结果”,SQL引擎负责结果集之间的组装。这种思维方式更接近关系数据库的本质——描述结果,而不是描述过程。
我带的团队里,新人刚开始写SQL喜欢一层子查询嵌套,问我“能不能这样套”。我会让他们先用CTE把每层逻辑拆开,起个有意义的名字,然后观察他们对业务的理解是不是清晰了。大多数情况下,CTE写清楚了,业务逻辑也就理清楚了。
8.2 CTE对代码Review的帮助
代码Review中,SQL往往是看得最慢的部分。子查询嵌套的风格,每个人写法都不同,Reviewer要花时间在脑内生成执行逻辑。CTE让Review变得轻松很多:
- CTE名字本身就在讲业务。
- 每一层的输入输出都在WITH块内,逻辑边界清楚。
- 主查询部分通常只有几行,一眼就能看出最终要什么。
我甚至见过团队把CTE命名规范写进代码规范里:CTE名用名词短语表达业务含义,比如valid_orders、ranked_customers,禁止出现t1、tmp2这种无意义命名。这是个很好的习惯。
8.3 CTE不是终点,是起点
CTE在MySQL里算是“新功能”,但放到整个SQL生态里,它已经是基础能力了。除了CTE,MySQL 8.0还带来了窗口函数、CHECK约束、隐藏索引等一堆和其他数据库看齐的能力。会CTE不是终点,意味着你的SQL水平已经进入“现代SQL”的领域。
后续如果你想继续深入,建议往这几个方向扩展:
- 窗口函数配合CTE,解决TOPN、同比环比、移动平均等经典问题。
- CTE加上
JSON_TABLE,处理JSON数据的复杂解析。 - CTE与
INSERT ... ON DUPLICATE KEY UPDATE结合,做复杂数据同步。
这些都是CTE能延伸出的高级玩法,基础打牢了,进阶效率会高很多。
回到我自己,CTE算是我用MySQL以来,语法层面感觉最值的一次升级。以前写复杂的层级关系、数据补全、多步计算,总是要各种绕,现在一条WITH RECURSIVE或者几个CTE串联就搞定。这种体验上的改变,只有真正把CTE用进日常开发,才能感受到。
如果你正在做MySQL升级评估,或者只是想在面试中把SQL这块讲得更有亮点,我强烈建议把CTE用熟。它在生产环境中的实际价值、在笔试面试中的出现频率,都值得你花一个下午把它吃透。
