NOT EXISTS 通常更快更稳,因其采用半连接、找到匹配即终止;LEFT JOIN + IS NULL 需全量关联再过滤,易致中间结果膨胀;仅在特定索引与查询条件下 LEFT JOIN 可能更快。

没有绝对答案,但绝大多数真实场景下 NOT EXISTS 更快、更稳,尤其在反向排除(“A有但B没有”)时。 这不是玄学,而是执行模型差异决定的:NOT EXISTS 是半连接(semi-join),找到一个匹配就停;LEFT JOIN + IS NULL 必须先全量关联,再过滤,中间结果集可能爆炸。
为什么 NOT EXISTS 常常更快
核心在于数据库不生成冗余中间数据:
- NOT EXISTS 子查询只要确认“至少存在一行匹配”,立刻终止,不构造任何结果行 —— 执行计划里常见
Nested Loop Anti Join或Index Only Scan - LEFT JOIN 会把左表每行和右表所有潜在匹配行都拼出来,哪怕你最后只用
WHERE b.id IS NULL筛出“没匹配”的那部分。右表宽、重复多、无索引时,Hash Right Join或Materialize操作会吃光内存和IO - PostgreSQL 12+、SQL Server 对 NOT EXISTS 有原生 anti-join 优化;而 LEFT JOIN + IS NULL 不会自动转成等价 anti-join,除非统计信息极准且写法完全规范
LEFT JOIN 什么情况下反而更快
极少,但确实存在,典型是 MySQL 5.7/8.0 某些旧版本或特定索引结构下的反直觉表现:
- 右表关联字段有**覆盖索引**(比如
INDEX(a_id, status)),且 LEFT JOIN 的ON条件严格匹配该索引前缀,而 NOT EXISTS 子查询里用了函数(如UPPER(b.status))或模糊匹配(b.status LIKE '%active'),导致索引失效 - 左表极小(STRAIGHT_JOIN 强制后可能逆转结果)
- 你测的是带
UPDATE的完整语句,而非纯查询 —— LEFT JOIN 在某些存储引擎(如 InnoDB)的更新路径中可能减少锁范围,而 NOT EXISTS 子查询触发更多一致性读
最容易踩的坑:写法不对,性能直接归零
无论选哪个,写错就白搭:
- NOT EXISTS 子查询里漏写相关条件(比如
WHERE b.a_id = a.id),变成非相关子查询,变成对右表的全表扫描 × 左表行数 - LEFT JOIN 的
ON条件字段类型不一致(如INTvsVARCHAR),隐式转换让索引失效,执行计划里出现type: ALL - 右表关联字段允许
NULL,LEFT JOIN 的IS NULL判定会误伤 —— 因为ON b.id = a.ref_id遇到b.id IS NULL时整行被丢弃,逻辑上本该算“有匹配”,却被当成“不存在” - 用
NOT IN替代 NOT EXISTS,子查询结果含NULL时整个查询返回空集,这是经典陷阱,不是性能问题,是逻辑错误
真正关键的不是语法选哪个,而是看 EXPLAIN FORMAT=TREE 里有没有 rows_examined_per_scan 暴涨、有没有 Using temporary 或 Using filesort。同一张表,换索引、换统计信息、换参数值,最优写法都可能翻转 —— 别信“永远更快”,信你自己的执行计划。


















