子查询在 WHERE 中被 MySQL 重写为关联时,性能未必差
MySQL 5.6+ 对很多
IN和
EXISTS子查询做了自动半连接(semi-join)优化,会把形如
SELECT * FROM t1 WHERE id IN (SELECT id FROM t2)重写为等价的
JOIN执行。是否触发该优化,取决于子查询是否满足“可物化”“无相关列”等条件。
实操建议:
用EXPLAIN查看执行计划,重点看
select_type字段:若显示
DEPENDENT SUBQUERY,说明未优化;若为
SIMPLE或出现
FirstMatch/
LooseScan,说明已转为半连接 避免在子查询中使用
ORDER BY、
LIMIT或外部表字段(即相关子查询),否则大概率无法重写 对小结果集子查询(如
SELECT status FROM config WHERE key = 'mode'),直接内联比
JOIN更轻量,MySQL 通常会自动物化为常量
LEFT JOIN 后加 WHERE 条件可能意外转成 INNER JOIN
这是最常踩的坑:写
LEFT JOIN t2 ON t1.id = t2.t1_id WHERE t2.status = 'active',逻辑上想保留所有
t1行、只过滤匹配的
t2,但
WHERE会把
t2.status为
NULL的行全部剔除——实际效果等同于
INNER JOIN。
正确做法是把过滤条件移到
ON子句:
SELECT t1.*, t2.name FROM t1 LEFT JOIN t2 ON t1.id = t2.t1_id AND t2.status = 'active';
注意:
ON中的条件只影响连接行为,
WHERE中的条件作用于最终结果集。若需保留
t1全量且只取特定
t2,必须用
ON过滤;若真要排除
t1中无匹配
t2的记录,才用
WHERE。
子查询在 SELECT 列表中(标量子查询)极易引发性能雪崩
形如
SELECT id, (SELECT COUNT(*) FROM log WHERE log.user_id = user.id) AS cnt FROM user是典型陷阱:MySQL 会为每一行
user执行一次子查询,复杂度 O(N×M),且无法利用
user.id上的索引加速子查询内部扫描。
优化方向明确:
改写为LEFT JOIN ... GROUP BY,让聚合在连接后一次性完成 确保子查询中的关联字段(如
log.user_id)有索引,否则每次子查询都全表扫
log若子查询结果稳定(如统计月活),考虑用物化视图或缓存表替代实时计算
EXISTS 比 IN 更适合检查存在性,尤其当子查询结果含 NULL
IN遇到子查询返回
NULL时,整个表达式结果为
UNKNOWN,导致行被过滤(即使其他条件为真);而
EXISTS只关心是否存在匹配行,不关心值是否为
NULL,语义更清晰、行为更可控。
示例对比:
SELECT * FROM t1 WHERE t1.id IN (SELECT t2.id FROM t2); -- 若 t2.id 有 NULL,整行失效 SELECT * FROM t1 WHERE EXISTS (SELECT 1 FROM t2 WHERE t2.id = t1.id); -- 安全
另外,
EXISTS在找到第一行匹配后即停止,而
IN子查询可能需生成完整结果集再做哈希查找——对大数据集,
EXISTS常有更低延迟。
真正难处理的是多层嵌套相关子查询,它既难读又难优化,一旦出现,优先重构为
JOIN+
GROUP BY或临时表分步计算。
