NOT EXISTS通常比LEFT JOIN...IS NULL性能更好,因其采用半连接机制,找到首个匹配即停止扫描,内存占用低;而后者需全量连接后再过滤,中间结果集大、易溢出。

NOT EXISTS 通常比 LEFT JOIN ... IS NULL 性能更好,尤其在子表(右表)数据量大、有索引、或字段允许 NULL 时。但不是绝对——最终得看执行计划,而不是语法本身。
为什么 NOT EXISTS 多数情况下更快?
NOT EXISTS 是半连接(semi-join / anti-join),优化器能在找到第一个匹配行后立即停止扫描;LEFT JOIN ... IS NULL 必须完成全量连接,再过滤出 NULL 行,中间结果集可能极大。
- 对每个主表行,
NOT EXISTS子查询最多查 1 次索引页(比如INDEX RANGE SCAN+STOP KEY) -
LEFT JOIN可能触发Hash Right Join或Materialize,内存占用高,甚至溢出到磁盘 - 当右表字段宽(如含
TEXT、JSON)、重复多、或无索引时,LEFT JOIN的中间数据膨胀更明显 - PostgreSQL 12+ 和 SQL Server 对
NOT EXISTS有原生 anti-join 优化;而LEFT JOIN ... IS NULL不一定被自动等价重写
LEFT JOIN ... IS NULL 什么时候反而快?
极少数场景下,LEFT JOIN 可能胜出,但需同时满足多个条件:
- 右表非常小(比如几百行),且已全部缓存在 buffer pool 中
- 优化器为
LEFT JOIN选了Nested Loop+Index Seek,而对NOT EXISTS错误预估子查询返回行数,触发了物化(MATERIALIZED)或临时表(MySQL 中预估 >100 行可能退化) - 你强制加了
STRAIGHT_JOIN(MySQL)或OPTION (RECOMPILE)(SQL Server),让驱动顺序可控,且统计信息新鲜 - 右表连接列无索引,但左表极小 —— 此时
LEFT JOIN的Nested Loop扫描总代价可能低于NOT EXISTS的多次随机 I/O(但这是病态设计,应先建索引)
常见错误:写法不对直接废掉性能
两种写法都容易因细节翻车,不注意就失去所有优势:
-
NOT EXISTS子查询漏掉关联条件(如写成WHERE o.status = 'active'而没写o.user_id = u.id),变成非相关子查询 → 对主表每行都扫一遍右表全量 -
LEFT JOIN的ON条件混入过滤逻辑(如ON o.user_id = u.id AND o.status = 'active'),会导致语义变化:它查的是「没有 active 订单的用户」,而非「没有任何订单的用户」 - 右表连接字段未建索引,或复合索引顺序错(比如
WHERE o.a = u.a AND o.b = u.b,但索引是(b, a)而非(a, b)) -
LEFT JOIN中右表字段本身允许NULL,又用WHERE o.user_id IS NULL—— 这会把本该匹配的行也判为“不存在”,逻辑错误优先于性能
怎么验证到底谁快?别猜,看执行计划
运行 EXPLAIN FORMAT=TREE(MySQL 8.0+)、EXPLAIN (ANALYZE, BUFFERS)(PostgreSQL)或 SET STATISTICS XML ON(SQL Server),重点关注:
- 是否出现
Nested Loop Anti Join、Hash Anti Join或Merge Anti Join—— 这是NOT EXISTS被正确优化的标志 -
rows_examined_per_scan值:越接近主表行数 × 1 越好(说明短路生效);若接近主表 × 右表,则NOT EXISTS已退化 - 右表操作符是否有
Using index condition或Index Only Scan;若显示Seq Scan或Heap Scan,先建索引 - 是否有
Materialize、Spill to disk、Temporary table等字样 —— 这些是LEFT JOIN性能崩坏的明确信号
NOT EXISTS 走不出 anti-join。动手前,先跑 EXPLAIN。


















