in子查询改inner join必须加distinct或控制驱动顺序,否则因一对多关系导致重复行;exists改join需防语义漂移;标量子查询应先聚合再left join;not in改left join时is null判断对象须为右表非空主键,且全程需关注索引与null处理。

IN 子查询改 INNER JOIN 后必须加 DISTINCT 或控制驱动顺序
- 常见错误:直接把
WHERE id IN (SELECT user_id FROM orders)换成INNER JOIN orders ON users.id = orders.user_id,结果返回重复用户行(一个用户多笔订单 → 多行) - 正确做法是加
DISTINCT,或把过滤条件下推到ON子句里:SELECT DISTINCT u.name FROM users u INNER JOIN orders o ON u.id = o.user_id AND o.status = 'paid';
- 如果主表数据量远小于子表,可考虑让子表做驱动表(MySQL 8.0+ 可用
STRAIGHT_JOIN强制),避免全表扫描主表 - 必须确认
orders.user_id有索引;若没有,JOIN反而比原IN更慢
EXISTS 改 INNER JOIN 要小心语义漂移
-
EXISTS是存在性判断,天然去重、短路;INNER JOIN是匹配性连接,会拉出所有匹配行 - 直接替换后若不加
DISTINCT,结果集可能膨胀数倍,尤其当子表对主表是一对多时 - 更稳妥的等价写法是用
INNER JOIN+GROUP BY,或保留EXISTS并确保子查询字段有联合索引(如(user_id, status)) - 若子查询含复杂逻辑(如嵌套
GROUP BY或窗口函数),强行JOIN很可能破坏语义,此时应优先优化子查询本身或改用 CTE 预计算
SELECT 中的标量子查询必须先聚合再 LEFT JOIN
- 错误写法:
SELECT u.name, (SELECT MAX(created_at) FROM logs WHERE user_id = u.id) last_login FROM users u—— 每行触发一次子查询 - 正确路径是把聚合提前:
SELECT u.name, COALESCE(l.last_login, '1970-01-01') last_login FROM users u LEFT JOIN ( SELECT user_id, MAX(created_at) last_login FROM logs GROUP BY user_id ) l ON u.id = l.user_id;
-
logs(user_id, created_at)必须有联合索引,否则子查询GROUP BY会走临时表 + filesort -
COALESCE不可省略,否则无日志用户该字段为NULL,和原查询行为不一致
NOT IN 改 LEFT JOIN 时,IS NULL 判断对象不能错
- 常见坑:
LEFT JOIN logs l ON u.id = l.user_id WHERE l.user_id IS NULL—— 若logs.user_id允许NULL,这条语句会把“日志表里存了NULL”的脏数据也当成“用户无日志”,结果错误 - 安全写法是判断右表主键是否为
NULL:WHERE l.id IS NULL(假设logs.id是主键且非空) - 更彻底的解法是先清理
logs.user_id中的NULL值,或在ON条件中排除:ON u.id = l.user_id AND l.user_id IS NOT NULL
真正容易被忽略的不是语法,而是索引缺失和 NULL 语义。改写前不看 EXPLAIN,不查 SHOW INDEX,不验证 NULL 行处理逻辑,性能可能更差,结果可能出错。











