子查询嵌套在 WHERE 中常被误用为 JOIN 的替代
当需要根据另一张表的条件过滤主表数据时,
WHERE ... IN (SELECT ...)看似简洁,但容易触发全表扫描和重复执行。比如查“所有有订单的用户”,写成
SELECT * FROM users WHERE id IN (SELECT user_id FROM orders),MySQL 可能对每个
users行都重跑一次子查询(尤其未加
DISTINCT或索引时)。
更稳妥的做法是改用
INNER JOIN或
EXISTS:
SELECT DISTINCT u.* FROM users u INNER JOIN orders o ON u.id = o.user_id;
或:
SELECT * FROM users u WHERE EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id);
EXISTS在找到第一条匹配即停止,适合“是否存在”类判断
IN子查询若返回
NULL,整条
WHERE判断会变成
UNKNOWN,导致结果为空——这是最常被忽略的逻辑陷阱
JOIN更利于利用索引,且执行计划通常更稳定
LEFT JOIN 后 WHERE 条件写错位置会导致隐式转 INNER JOIN
想查“所有用户及其订单数(含 0)”,却写了:
SELECT u.name, COUNT(o.id) FROM users u LEFT JOIN orders o ON u.id = o.user_id WHERE o.status = 'paid' GROUP BY u.id;
结果只返回有已支付订单的用户——因为
WHERE o.status = 'paid'把
o为
NULL的行全过滤掉了,
LEFT JOIN形同虚设。
正确做法是把过滤条件移到
ON子句:
SELECT u.name, COUNT(o.id) FROM users u LEFT JOIN orders o ON u.id = o.user_id AND o.status = 'paid' GROUP BY u.id;
ON控制“如何连接”,
WHERE控制“连接后如何筛选” 多表
LEFT JOIN时,后续表的过滤条件也必须放在对应
ON中,否则会逐级收缩左表结果 用
EXPLAIN查看
type是否为
ALL,可快速识别是否意外丢失了外连接语义
JOIN 顺序影响性能,但 MySQL 8.0+ 优化器通常能自动调整
早期版本中,写
FROM large_table JOIN small_table比反过来快,因为驱动表(
large_table)决定循环次数。但现在 MySQL 优化器多数情况下会重排顺序,前提是各表有可用索引且统计信息准确。
仍需手动干预的典型场景:
使用了STRAIGHT_JOIN强制顺序(如复杂视图或物化中间结果) 某表有高选择性索引,但优化器因统计过期未选中——运行
ANALYZE TABLE更新统计信息 关联字段类型不一致,例如
users.id是
BIGINT,而
orders.user_id是
INT,会导致索引失效,此时无论怎么调顺序都慢
验证方式:执行
EXPLAIN FORMAT=TREE(MySQL 8.0+),直接看优化器选定的驱动表和访问路径。
UNION ALL 比多次 JOIN 更适合“垂直拼接”类需求
当要合并不同业务类型的记录(如“用户注册日志”和“用户登录日志”),有人习惯建宽表再用
LEFT JOIN填充空字段,结果表膨胀、查询变慢。
更自然的方式是用
UNION ALL水平堆叠结果:
SELECT 'register' AS event_type, user_id, created_at AS event_time FROM register_log UNION ALL SELECT 'login' AS event_type, user_id, created_at AS event_time FROM login_log ORDER BY event_time DESC LIMIT 100;
UNION ALL不去重、不排序,性能远高于
UNION避免在
UNION各分支里写复杂子查询——先物化到临时表或 CTE 中再拼接 字段名和类型需严格一致;字符串长度以最长分支为准,否则可能被截断
嵌套太深、JOIN 表太多时,优先考虑是否真需要一次性拉取全部关联数据——分页、懒加载、应用层聚合往往比单条 SQL 更可控。
