用EXISTS替代LEFT JOIN + DISTINCT能提速,因为DISTINCT在JOIN后对膨胀的中间结果集去重,而EXISTS不构造中间集、仅检查存在性。典型改写是将SELECT DISTINCT u. FROM users u LEFT JOIN orders o ON u.id=o.user_id WHERE o.user_id IS NOT NULL改为SELECT u. FROM users u WHERE EXISTS (SELECT 1 FROM orders o WHERE o.user_id=u.id)。

为什么用EXISTS替代LEFT JOIN + DISTINCT能提速
因为DISTINCT是在JOIN完成之后才执行的,中间结果集可能比原始表大几倍甚至几十倍。比如users和orders各10万行,若平均每个用户有3个订单,JOIN后就是30万行——DISTINCT就得对这30万行做哈希或排序。而EXISTS只检查“是否存在匹配记录”,不构造中间结果集,天然绕开去重开销。
EXISTS替代的典型写法与关键约束
把“查有订单的用户”这类需求从:
SELECT DISTINCT u.id, u.name FROM users u LEFT JOIN orders o ON u.id = o.user_id WHERE o.user_id IS NOT NULL;
改成:
SELECT u.id, u.name FROM users u WHERE EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id);
必须注意的三点:
- 子查询里一定要用
SELECT 1(不能SELECT *或SELECT NULL),否则某些MySQL老版本会退化为全表扫描 - 关联条件
o.user_id = u.id必须写在子查询的WHERE中,不能漏掉,否则变成笛卡尔积式检查 - 主查询不能有
GROUP BY或ORDER BY依赖orders字段,否则语义不等价,强行替换会丢数据
什么时候不该硬切到EXISTS
不是所有LEFT JOIN场景都适合换。以下情况改用EXISTS反而更慢或逻辑错误:
- 需要返回
orders表里的字段(如order_date、amount),EXISTS无法取子表数据 - 子查询里用了聚合(如
COUNT(*) > 1)或GROUP BY,这时EXISTS语义不同,不能直接替代 - 驱动表极小但被驱动表极大,且子查询条件无法命中索引(如
WHERE o.status = 'pending' AND o.user_id = u.id但orders(status)没索引),此时EXISTS会循环执行N次全表扫描
索引和执行计划验证要点
换完之后必须看EXPLAIN确认是否生效:
- 子查询部分应出现
Using index或Using where; Using index condition,说明走了索引 - 避免看到
Using temporary或Using filesort出现在主查询层——那是DISTINCT残留或排序未下推的信号 -
orders表上至少要有(user_id)单列索引;如果还带状态过滤,优先建(user_id, status)复合索引
真正容易被忽略的是:EXISTS的性能优势高度依赖子查询能否走索引。一旦关联字段类型不一致(比如users.id是BIGINT,orders.user_id是VARCHAR),隐式转换会让索引失效,这时候比LEFT JOIN还慢。










