not exists通常比left join...is null更快,因其采用短路式半连接,找到首个匹配即停止扫描;而后者需完成全量连接再过滤,易产生大体积中间结果,尤其右表数据量大或无索引时性能更差。

NOT EXISTS 通常比 LEFT JOIN ... IS NULL 更快,尤其在右表(子表)数据量大、有合适索引、且查询目标是“主表中无匹配的记录”时。但这不是绝对的——实际快慢取决于执行计划,而不是写法本身。
为什么NOT EXISTS多数情况下更快?
核心在于数据库如何执行这两种逻辑:
-
NOT EXISTS触发的是「半连接(Semi Join)」或「反连接(Anti Join)」,对主表每一行,只探查右表是否存在至少一条匹配;一旦找到,立刻停止扫描该用户的关联记录(短路行为) -
LEFT JOIN ... IS NULL必须完成全部连接:即使你只关心“没匹配”,数据库仍得把每个用户的全部订单都拼出来,再逐行判断o.user_id IS NULL - 右表若含宽字段(如大文本、JSON)、重复数据多、或未建索引,
LEFT JOIN极易生成巨量中间结果(比如Hash Right Join或Materialize),内存和IO压力陡增 -
NOT EXISTS子查询中只要SELECT 1,不取实际列,优化器更倾向走Index Only Scan或Nested Loop Anti Join
典型场景:查 100 万用户中哪些没下过单(orders 表 1 亿行),NOT EXISTS 成本常为 800,LEFT JOIN ... IS NULL 可能飙到 5000+。
LEFT JOIN ... IS NULL反而更快的例外情况
这不是理论缺陷,而是优化器在特定条件下做出了更优选择:
- 右表极小(比如只有几十行),且无索引 ——
LEFT JOIN可能直接用嵌套循环 + 全表扫描,而NOT EXISTS的相关子查询可能被物化(Materialize)成临时表,反而多一次落盘开销 - MySQL 5.7 或更早版本中,
NOT EXISTS子查询若含非等值条件(如status LIKE 'act%')、函数(如UPPER(status))或类型隐式转换,会导致索引失效,此时LEFT JOIN反而可能走上索引 - PostgreSQL 12+ 对
NOT EXISTS有强 anti-join 优化,但若统计信息过期(EXPLAIN显示 estimated rows 远高于 actual),优化器可能误判,退而选择LEFT JOIN路径 - 某些 OLAP 引擎(如 StarRocks、Doris)对
LEFT JOIN的向量化执行更成熟,而NOT EXISTS子查询尚未充分下推
真实案例:某 MySQL 5.7 环境中,NOT EXISTS 查询耗时 29 秒,改写为 LEFT JOIN ... IS NULL 后仅 1.2 秒 —— 原因是子查询里用了 DATE(created_at) = '2026-01-01',导致索引完全失效。
怎么验证哪个真更快?别猜,看执行计划
关键不是语法,是数据库实际干了什么。重点关注这几项:
- 运行
EXPLAIN FORMAT=TREE(MySQL 8.0+)或EXPLAIN (ANALYZE, BUFFERS)(PostgreSQL),看是否出现Nested Loop Anti Join、Index Range Scan(好);还是Hash Right Join、Materialize、Seq Scan on orders(危险信号) - 检查
rows_examined_per_scan:若LEFT JOIN中右表的这一值远大于主表行数(比如主表 10 万行,右表扫描 500 万行),说明没走索引或短路失效 - 确认子查询关联字段是否有复合索引,且顺序匹配:例如
WHERE o.user_id = u.id AND o.status = 'paid',索引应为(user_id, status),而非(status, user_id) - 避免在
NOT EXISTS子查询里写SELECT *或带计算的字段;必须用SELECT 1,且 WHERE 条件纯等值
一个容易被忽略的点:哪怕你写了 NOT EXISTS,优化器也可能悄悄转成 LEFT JOIN 执行 —— 所以不能只看 SQL 写法,得盯死执行计划里的实际操作符。











