not exists 能触发半连接优化,left join 不能;前者语义明确、短路执行、索引利用好、null 安全,后者需全量连接再过滤,易产生大中间结果且存在 null 误判风险。

NOT EXISTS 能触发半连接(Semi-Join)优化,LEFT JOIN 不能
数据库优化器对 NOT EXISTS 的语义理解更直接:它只关心“是否存在匹配”,不关心匹配多少行。现代引擎(如 PostgreSQL 12+、SQL Server、MySQL 8.0+)会将其自动转为 ANTI JOIN 或 NESTED LOOPS ANTI,配合索引实现单次探查即终止。而 LEFT JOIN ... WHERE b.id IS NULL 是两步操作:先全量构造连接结果(哪怕右表有 1000 条匹配,也得全拉出来),再过滤 NULL 行——中间结果集体积大、内存占用高。
短路执行 vs 全量扫描,实际开销差距明显
对主表每行,NOT EXISTS 在子查询中找到第一个匹配就立即退出;LEFT JOIN 必须扫描右表所有匹配行(或全表),尤其当右表无索引、或连接键重复率高时:
- 右表 100 万行,主表 1 万行 →
NOT EXISTS最多做 1 万次索引探查 - 同场景下
LEFT JOIN可能生成千万级中间行,触发临时表、磁盘溢出甚至 O(n×m) 嵌套循环 - EXPLAIN 中常见信号:
Materialize、Hash Right Join、rows_examined_per_scan远高于预期
NULL 处理逻辑不同,LEFT JOIN 容易误判
LEFT JOIN ... IS NULL 的正确性依赖连接字段本身不能为 NULL。一旦右表连接列含 NULL(比如 user_id 允许为空),那些本该匹配的记录也会被当作“不存在”返回——这是语义漏洞,不是性能问题,但常被忽略:
-
NOT EXISTS子查询里WHERE b.user_id = a.user_id不会匹配NULL,天然规避该陷阱 -
LEFT JOIN的ON条件若涉及可空字段,IS NULL判断无法区分“没匹配”和“匹配了但字段是 NULL” - 真实案例中,这种误判导致报表多出 15% 的“幽灵用户”
索引利用效率差异取决于写法细节
NOT EXISTS 并非永远更快——它的索引是否生效,极度依赖子查询中条件是否“干净”:
- ✅ 高效写法:
NOT EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.user_id)→ 可走INDEX RANGE SCAN - ❌ 低效写法:
NOT EXISTS (SELECT 1 FROM orders o WHERE UPPER(o.user_id) = UPPER(u.user_id))→ 索引失效,退化为全表扫描 -
LEFT JOIN同样受此影响,但因必须构造完整连接,索引失效时代价更高 - 验证方法:用
EXPLAIN FORMAT=TREE(MySQL 8.0+)或EXPLAIN ANALYZE(PostgreSQL)看rows_examined和是否出现Using index condition
NOT EXISTS 的短路能力在子查询里被一个隐式类型转换或函数调用悄悄关掉了。










