应使用WITH替代三层及以上嵌套子查询,因其可强制物化中间结果、避免重复计算,并提升可读性与可控性;同时需配合EXISTS替换关联子查询、临时表加索引及规范JOIN用法。

用WITH替代多层嵌套,让执行路径可读可控
三层及以上子查询最典型的症状是:EXPLAIN里反复出现DEPENDENT SUBQUERY,且外层每行都触发内层全表扫描。这不是写法“不够优雅”的问题,而是MySQL优化器根本无法为深层嵌套生成有效连接路径。
直接把原结构拆成CTE(WITH),不是为了好看,而是强制数据库按顺序物化中间结果,避免重复计算:
WITH east_regions AS ( SELECT code FROM regions WHERE name LIKE '%华东%' ), province_regions AS ( SELECT code FROM regions WHERE parent_code IN (SELECT code FROM east_regions) ), target_users AS ( SELECT id FROM users WHERE region IN (SELECT code FROM province_regions) ) SELECT * FROM orders WHERE user_id IN (SELECT id FROM target_users);
- 每个CTE只做一件事,命名即语义,比如
east_regions比(SELECT code FROM regions WHERE name LIKE '%华东%')更易定位和复用 - MySQL 8.0+、PostgreSQL、SQL Server均支持;但MySQL 5.7不支持,别在老环境硬上
- CTE本身不建索引,若
province_regions结果集大(>1万行),后续IN仍慢,得配合临时表或改用JOIN
把IN + 子查询改成EXISTS,切断逐行依赖
当子查询出现在WHERE中且含关联字段(如user_id = u.id),它就是关联子查询——性能杀手。MySQL会为orders表的每一行,都执行一次内层查询。
改用EXISTS后,优化器有机会将嵌套循环转为哈希连接或物化连接:
SELECT * FROM orders o
WHERE EXISTS (
SELECT 1 FROM users u
WHERE u.id = o.user_id
AND EXISTS (
SELECT 1 FROM regions r
WHERE r.code = u.region
AND r.parent_code IN (
SELECT code FROM regions WHERE name LIKE '%华东%'
)
)
);
-
EXISTS只要找到一行就短路返回,比IN加载全部结果再比对更轻量 - 最内层
SELECT code FROM regions WHERE name LIKE '%华东%'仍是非关联子查询,可提前物化,不会被重复执行 - 注意
r.code = u.region这个等值条件必须写在EXISTS子句内部,不能挪到外层WHERE,否则破坏半连接语义
对高频复用的子查询,显式建临时表并加索引
如果某一层子查询(比如SELECT code FROM regions WHERE parent_code IN (...))在多个地方被引用,或单次执行耗时明显,就别靠优化器猜了——手动物化。
临时表不是权宜之计,而是可控性更强的手段:
CREATE TEMPORARY TABLE temp_province_regions AS SELECT code FROM regions WHERE parent_code IN ( SELECT code FROM regions WHERE name LIKE '%华东%' ); CREATE INDEX idx_temp_code ON temp_province_regions(code);
-
TEMPORARY TABLE只在当前会话可见,无需清理,也不影响并发 - 务必给关键过滤字段(这里是
code)建索引,否则后续JOIN或IN还是走全表扫描 - 别用
CREATE TABLE ... AS SELECT建普通表——没事务隔离,DDL锁表,线上慎用
JOIN条件里别塞子查询表达式,NULL会咬人
有人图省事,在ON里直接写u.region IN (SELECT code FROM ...),看起来像JOIN,实则仍是关联子查询,且逻辑脆弱。
真正扁平化的JOIN,所有关联必须落在具体字段上:
SELECT o.* FROM orders o JOIN users u ON o.user_id = u.id JOIN temp_province_regions r ON u.region = r.code;
- 一旦
u.region为NULL,这行就彻底消失(INNER JOIN)或变成NULL(LEFT JOIN),行为确定 - 而
IN (SELECT ...)遇到NULL时,整个判断结果为UNKNOWN,常导致数据丢失却不报错 - 所有JOIN字段(
u.id、u.region、r.code)都应有索引,否则连接成本翻倍
重构不是追求“没有子查询”,而是让每一层数据来源清晰、可干预、可索引。最麻烦的从来不是语法怎么写,而是哪一层该提前物化、哪一层该用EXISTS短路、哪一层的NULL会悄悄吃掉你的数据。










