WITH仅在中间结果被引用≥2次时才真正降低复杂度,否则只是换写法且解析开销略增;必须按定义顺序引用子句,禁用无意义ORDER BY/LIMIT,单次子查询硬套WITH加MATERIALIZED属多余操作。

用 WITH 拆解嵌套,但必须满足复用前提
不是所有嵌套都该用 WITH 替换。它只在中间结果被引用 ≥2 次时才真正减少复杂度——比如同一段聚合逻辑(SELECT user_id, MAX(created_at) FROM orders GROUP BY user_id)既用于 JOIN 又用于 WHERE EXISTS,这时拆成 last_order AS (...) 才有意义。否则只是把嵌套换个写法,解析开销反而略增。
常见错误:把单次使用的子查询硬套 WITH,还加 MATERIALIZED,结果数据库真去物化一次,纯属多此一举。
- PostgreSQL 8.0+ 和 SQL Server 支持
MATERIALIZED,MySQL 8.0.23+ 才支持;老版本直接报错ERROR 1235 - 子句间引用必须按定义顺序,后定义的可引用前定义的,反向不行
-
WITH里别写ORDER BY或LIMIT,除非是分页逻辑(如配合OFFSET),否则优化器大概率忽略
把 IN (SELECT ...) 改成 JOIN,尤其当子查询带 WHERE
像 SELECT * FROM orders WHERE customer_id IN (SELECT id FROM customers WHERE region = 'CN') 这种结构,在 MySQL 5.7 或旧版 PostgreSQL 中极易触发“依赖子查询”执行模式——外层每行都重跑一次内层,变成 N×M 扫描。手动改写为 INNER JOIN customers c ON o.customer_id = c.id WHERE c.region = 'CN' 后,优化器能走索引连接,执行计划立刻清晰。
例外情况:子查询含 LIMIT、GROUP BY 或 UNION,不能直接转 JOIN,此时优先考虑 WITH 物化或临时表。
- MySQL 8.0 要确认
optimizer_switch='semijoin=on'已启用,否则仍可能退化 - SQL Server 和 PostgreSQL 的 CTE 在这类场景下更可控,但得看
EXPLAIN里是否出现Materialize节点 - 避免在
JOIN条件里对字段用函数,比如UPPER(u.name) = UPPER(c.name),会强制全表扫描
每层 SELECT 只拿真正需要的字段
SELECT * FROM (SELECT * FROM (SELECT * FROM t1 JOIN t2) t23) t34 看似省事,实际让每一层都搬运全部字段,IO 和内存压力翻倍。更糟的是,优化器未必能剪掉外层没用到的字段——特别是包在视图或 CTE 里后,Shared Hit Blocks 在 EXPLAIN (ANALYZE, BUFFERS) 里会异常高。
实操上,从最内层开始收敛:只选下一层必需的 JOIN 键、过滤字段和聚合结果。例如订单汇总视图,内层只需 user_id 和 COUNT(*),别把 order_date、status 全拖下去。
- 给中间结果集起明确别名,如
t_user_orders AS (SELECT u.id AS user_id, COUNT(o.id) AS order_cnt ...) - 字段别名模糊是跨库迁移翻车主因,
SELECT *在 PostgreSQL 和 MySQL 之间列序可能不同 - MySQL 5.7 前无视图合并,
SELECT a_id FROM v_summary WHERE a_id = 123仍会先算完整视图再过滤
三层以上嵌套优先拆成独立查询 + 应用层拼接
当嵌套超过三层(比如 WHERE ... IN (SELECT ... WHERE ... IN (SELECT ...))),数据库优化器常放弃精确代价估算,执行计划随机漂移。此时硬扛 SQL 层优化收益极低,不如拆开:第一层查出 ID 列表,应用层缓存或批量传参,再发第二条查询。
尤其适合读多写少、ID 集合不大的场景。EF Core 的 Include 多级导航也是同理——三张表以上关联,生成的 SQL 易出笛卡尔积,ThenInclude 层级越多,重复数据越严重。
- 拆分后注意事务边界:若需强一致性,用临时表或
WITH HOLD游标(PostgreSQL) - 应用层拼接要防 SQL 注入,参数化传 ID 列表,别拼字符串
- MySQL 用
IN传参上限默认 1000 个,超限需分批或改用JOIN临时表
复杂点永远不在语法怎么写,而在数据边界上——比如某层子查询返回空集,LEFT JOIN 后字段全为 NULL,但业务代码没判空,直接参与计算,结果就偏了。

















