前两年整理运营周报,我遇到一条特别恶心的 SQL。要把用户的充值总和、下单次数、最近一次下单时间都按用户先汇总一遍,然后再拿这堆汇总结果去和用户等级表做关联、算复购率、算高价值区间,原来的同事把这段逻辑直接内联成了四层嵌套子查询。想改一个字段,得从最内层翻起,外面还有好几处引用它的地方,每次改都会漏,运行效率也没好到哪去。当时线上库是 MySQL 5.7,不敢随便建临时表,视图又要申请权限,改一条报表 SQL 跟做外科手术一样。
后来业务库升级到 8.0,我第一件事就是用 WITH AS 把那条历史 SQL 重写了一遍。中间结果只写一遍,起个名字,后面所有统计直接读这个逻辑名,维护的人谁看谁懂。这篇文章就把这段时间用 WITH AS 整理出来的经验写全一点,包括语法边界、实用场景、递归用法、执行计划和几个容易翻车的坑。如果你是写完子查询再打个括号就完事那种风格,建议看完第三节和第五节,这两部分最容易踩雷。
1. WITH AS 到底在做什么:语法边界先立清楚
1.1 基本语法结构
WITH AS 的官方名字叫 Common Table Expression,简称 CTE,公共表表达式。它的意思很简单:先给一段 SELECT 查询结果起个临时名字,后面主查询可以像查普通表一样引用它。基础语法长这样:
sql复制WITH cte_name AS (
SELECT column1, column2
FROM some_table
WHERE some_condition
)
SELECT *
FROM cte_name;
注意几个细节:
WITH关键字本身不需要和任何表绑定,它位于整条语句的最前面。- CTE 名字后面可以跟列名列表,例如
WITH t(a, b) AS (...),相当于给查询结果列重命名。如果不写,就直接沿用SELECT里的列名。 - 主查询可以是
SELECT,在 MySQL 8.0.19 之后也可以是UPDATE和DELETE,这个后面专门讲。 - 整条语句以主查询末尾的分号结束,不要在
AS (...)后面提前打分号,否则 MySQL 会认为语句已经结束了。
如果一段业务逻辑需要拆成多个步骤,可以连续定义多个 CTE,中间用逗号分隔:
sql复制WITH step1 AS (
SELECT user_id, SUM(amount) AS total_amount
FROM orders
GROUP BY user_id
),
step2 AS (
SELECT user_id
FROM step1
WHERE total_amount > 1000
)
SELECT u.id, u.name
FROM users u
JOIN step2 s ON u.id = s.user_id;
后面的 CTE 可以引用前面已经定义好的 CTE,但反过来不行。这是 WITH AS 很顺手的一点:写分析 SQL 的时候天然就是一步一步往下推,代码顺序和思考顺序一致。
1.2 作用域和生命周期
CTE 只在定义它的这条语句内有效,语句执行完就释放了。它不进 information_schema,不占数据库物理文件空间,也不需要 DBA 去手动清理。和临时表最大的区别就是:临时表连接断开才消失,而且需要你先 CREATE TEMPORARY TABLE,再 INSERT,再查询,至少三条语句;CTE 则是把定义和查询揉进一条语句里,不落库、不占用连接状态。
CTE 的作用域还有一个容易被忽略的点:它只在主查询里可见。如果主查询里又嵌了一个子查询,子查询里是不是还能引用外层 CTE?这个取决于 MySQL 的解析规则。我在 8.0.32 上测试过,外层定义的 CTE 在内层子查询里依然可以引用,因为整个 WITH 的作用域覆盖后面整个语句。这个特性有时很好用,但也是命名冲突的温床,后面第五节细说。
1.3 CTE 和派生表、临时表的对比
我记得刚学 CTE 的时候,最困惑的就是它和 FROM (...) 的派生表有什么区别。用一张表来理清:
| 对比项 | CTE (WITH AS) | 派生表 (FROM 子查询) | 临时表 (TEMPORARY TABLE) |
|---|---|---|---|
| 定义位置 | 语句最前方 | FROM 子句内 | 独立建表语句 |
| 是否可被多次引用 | 同一语句内可多次引用 | 只能写一次,引用多次就要复制多遍 | 任意多次,跨语句 |
| 生命周期 | 当前 SQL 语句结束即释放 | 当前 SQL 语句结束即释放 | 会话结束或显式 DROP 才释放 |
| 是否需要建表权限 | 不需要 | 不需要 | 需要 CREATE TEMPORARY TABLE 权限 |
| 能否加索引 | 优化器内部决定,不能手动建索引 | 优化器内部决定,不能手动建索引 | 可以手动建索引 |
| 可读性 | 高,能命名每个中间逻辑 | 低,嵌套层级深时难读 | 中,要维护建表和清理语句 |
| 性能调优方式 | 通过执行计划观察合并或物化 | 类似 CTE,优化器可能合并或物化 | 可以主动分析、建索引、ANALYZE TABLE |
从上面能看得出来,CTE 主要解决的是代码组织问题,而不是替代临时表。数据量特别大且下游多个统计都要复用同一份中间结果时,临时表加上合适的索引,往往比让优化器去猜怎么处理 CTE 更可控。这一点千万别搞反,后文第五节还会再提。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 业务里最吃香的三个场景:不复写、能串联、有点名
2.1 场景一:同一份统计结果被多处引用
这是 CTE 最典型的价值。举个例子,现在要算两个数:平均每个付费用户的总充值金额,以及充值超过平均线的人数占比。
不用 CTE 的写法是什么样呢?平均值子查询要写一遍,超过平均值的人数统计里又要引用一遍同一个子查询:
sql复制SELECT
AVG(total_amount) AS avg_amount
FROM (
SE
