with能显著提升sql可读性与维护性,但仅适用于重复子查询、多层嵌套及逻辑分段场景;滥用(如单次引用、相关子查询)反而降低性能与可调试性。

直接说结论:用 WITH 把重复子查询、多层嵌套和逻辑分段抽出来,SQL 就会立刻变清晰——但前提是别把 CTE 当成万能胶水,乱套反而更难 debug。
为什么嵌套子查询让人头疼?
比如统计每个客户的订单数 + 最高单笔金额 + 首次下单时间,不用 WITH 时容易写成三层嵌套:
SELECT c.name, (SELECT COUNT(*) FROM orders o WHERE o.customer_id = c.id) AS order_count, (SELECT MAX(amount) FROM orders o WHERE o.customer_id = c.id) AS max_amount, (SELECT MIN(created_at) FROM orders o WHERE o.customer_id = c.id) AS first_order FROM customers c;
问题不只在写法丑:每次子查询都全表扫描 orders,性能差;字段名分散在三处,改一个得找三遍;无法复用中间结果(比如想加个“是否 VIP”判断,还得再写一遍 COUNT)。
用 WITH 拆开后,逻辑就平铺直叙:
- 先定义
customer_stats算出每个客户的基础聚合 - 主查询只管关联和展示,不掺杂计算逻辑
- 后续加新字段(如
is_vip)直接在customer_stats里加一列就行
多个 CTE 怎么组织才不混乱?
当要拼接客户信息、订单汇总、地区销量排名三块数据时,别堆在一个 CTE 里硬塞。正确做法是按职责拆:
WITH
customer_orders AS (
SELECT customer_id, COUNT(*) AS cnt, SUM(amount) AS total
FROM orders GROUP BY customer_id
),
region_sales AS (
SELECT r.name AS region, SUM(o.amount) AS sales
FROM orders o JOIN stores s ON o.store_id = s.id
JOIN regions r ON s.region_id = r.id
GROUP BY r.name
),
top_customers AS (
SELECT customer_id, total
FROM customer_orders
ORDER BY total DESC LIMIT 10
)
SELECT c.name, co.cnt, co.total, rs.region
FROM customers c
JOIN customer_orders co ON c.id = co.customer_id
LEFT JOIN top_customers tc ON c.id = tc.customer_id
LEFT JOIN stores s ON c.preferred_store = s.id
LEFT JOIN regions rs ON s.region_id = rs.id;
关键点:
- 每个 CTE 名称必须见名知意(
customer_orders而不是tmp1) - CTE 之间可以互相引用(
top_customers基于customer_orders),但不能循环依赖 - 如果某个 CTE 只被用一次且逻辑极简(比如只
SELECT 1 AS flag),不如直接写进主查询——CTE 不是越多越好
递归 CTE 容易卡死,怎么防?
WITH RECURSIVE 看似强大,但生产环境最常踩的坑是无限递归。比如查部门树时,若数据里存在 parent_id = id 的脏数据,查询不会报错,只会跑到 cte_max_recursion_depth 限制才停,期间 CPU 暴涨。
安全写法必须带两层兜底:
- 显式限制递归深度:
WHERE level (比依赖全局变量更可控) - 用
NOT EXISTS或LEFT JOIN ... IS NULL检查自引用,提前过滤掉异常节点 - 始终在递归分支的
SELECT中包含level或path字段,方便排查哪一层崩了
例如组织树查询中,加一行 AND o.id != o.parent_id 就能避开最常见自环:
WITH RECURSIVE org_tree AS ( SELECT id, name, parent_id, 1 AS level, CAST(id AS CHAR(200)) AS path FROM org WHERE parent_id IS NULL UNION ALL SELECT o.id, o.name, o.parent_id, ot.level + 1, CONCAT(ot.path, '-', o.id) FROM org o INNER JOIN org_tree ot ON o.parent_id = ot.id WHERE o.id != o.parent_id AND ot.level <h3>CTE 和窗口函数混用时的陷阱</h3><p>很多人想在 CTE 里直接用 <code>ROW_NUMBER() OVER(...)</code> 再在外面过滤,结果发现 <code>WHERE row_num = 1</code> 报错——因为窗口函数执行顺序晚于 <code>WHERE</code>,CTE 里的别名在外部不可直接用于过滤。</p><p>正确姿势只有两种:</p>
- 把窗口函数放在 CTE 里,外部用
HAVING或子查询包装(推荐):
WITH ranked_orders AS (
SELECT
customer_id,
amount,
ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY created_at DESC) AS rn
FROM orders
)
SELECT customer_id, amount
FROM ranked_orders
WHERE rn = 1;
- 或者干脆不用 CTE,把
ROW_NUMBER()放在主查询的SELECT里,再套一层子查询
注意:ORDER BY 在 CTE 内部无效(MySQL 8.0 不允许 CTE 子句含 ORDER BY),排序必须放在最终 SELECT 或窗口函数的 OVER 中。
真正难处理的从来不是语法,而是当你把五六个 CTE 串在一起、又嵌了窗口函数、还加了递归时,没人能一眼看懂数据从哪来、到哪去、在哪断的。这时候宁可多拆一个 CTE,也不要在一行里塞三个 COALESCE 套嵌套。











