一句话里写多个 CTE
WITH 后面可以用逗号分隔定义多个公共表表达式,后定义的名字可以引用前面已经定义的名字,整体变成一条自上而下的加工流水线:先做过滤,再做聚合,最后连接输出。不能反向引用,也不能互相引用成环。把复杂查询拆成几个有名字的步骤,除了可读性更好,也方便在排查时逐个替换中间步骤、核对每一步的行数与字段。
基本信息
| 适用平台 | MySQL 8.0 / PostgreSQL 12 及以上 |
|---|---|
| 数量限制 | 一次 WITH 可定义任意多个,用逗号分隔 |
| 引用方向 | 只能引用前面已定义的步骤,不允许循环引用 |
| 递归写法 | 需要自引用时必须写成递归形式,显式声明递归关键字 |
| 作用范围 | 只在当前这一条语句里有效,语句结束即消失 |
典型使用场景
- 长 SQL 拆解:把过滤、聚合、排名分成几个命名步骤,便于评审
- 同一个中间结果被多次引用,避免重复粘贴相同子查询
- 分层加工:先取活跃用户,再取大额订单,最后做连接输出
- 排查线上慢查询时逐个步骤单独执行,快速定位是哪一步变慢
- 迁移与重构:把嵌套很深的子查询改写成平铺的流水线结构
多步骤流水线示例
多步骤流水线示例
WITH
active_users AS ( -- 第一步:筛出活跃用户
SELECT id, name FROM users WHERE status = 1
),
big_orders AS ( -- 第二步:按用户汇总大额订单
SELECT user_id, SUM(amount) AS total
FROM orders
WHERE amount > 100
GROUP BY user_id
)
SELECT a.id, a.name, b.total -- 第三步:两步骤连接输出
FROM active_users a
JOIN big_orders b ON b.user_id = a.id
ORDER BY b.total DESC;
-- 等价但仍然清晰的另一种组织方式:先算总量再筛
-- 注:后面定义的步骤可以引用前面定义的步骤,反过来不行使用要点
| 关注点 | 说明 |
|---|---|
| 命名可读性 | 步骤名写清楚在做什么,评审时不用读完整 SQL |
| 引用顺序 | 只允许向后引用,出现环时语句直接报错 |
| 排序作用 | 中间步骤里的排序通常没有意义,除非同时限制了行数 |
| 物化与否 | 由优化器决定,写的时候不要假定它一定被缓存或一定被内联 |
PostgreSQL 12 起,非递归且只被引用一次的公用表表达式默认可以被内联进外层查询,想强制物化可以显式标注;12 之前的版本一律物化。写语句时不要把「物化」当成可以依赖的性能手段,需要稳定中间结果时用临时表。
MySQL 要求递归写法显式声明,并且有递归深度上限;一次定义多个步骤时,只有自引用的那一个需要标记为递归。