NOT EXISTS通常比LEFT JOIN+IS NULL更快,因其采用半连接、找到匹配即终止且不生成中间结果;而后者需全量关联再过滤,易产生大体积中间数据,尤其右表无索引或字段宽时。

NOT EXISTS 通常比 LEFT JOIN + IS NULL 更快,尤其在右表数据量大、有索引、或存在 NULL 值时;但 LEFT JOIN 在需要复用右表字段或驱动表很小时可能更优——不能只看“哪个快”,要看你查的是什么、表结构如何、有没有索引。
NOT EXISTS 为什么常更快
数据库执行 NOT EXISTS 时,对左表每一行,只在右表中做一次“是否存在匹配”的探查,找到第一个就停(anti-join 半连接),不构造中间结果集。这直接避开两个性能黑洞:
- LEFT JOIN 会先全量关联,生成临时连接结果(比如百万 × 千行 → 十亿级中间行),再过滤
IS NULL,内存和 CPU 开销都大 - 当右表字段宽(如含
TEXT或多个VARCHAR(500))、或没索引时,LEFT JOIN的Hash Right Join或Materialize操作会显著拖慢 - MySQL 8.0+、PostgreSQL 12+ 对
NOT EXISTS有明确的 anti-join 优化路径,执行计划里常见Nested Loop Anti Join或Index Only Scan
典型表现:左表 1000 行,右表 100 万行且 customer_id 有索引 → NOT EXISTS 实际最多扫描 1000 次索引页;LEFT JOIN 可能扫描 100 万行 × 1000 次(Nested Loops 全扫右表)。
LEFT JOIN + IS NULL 什么时候反而更好
它不是“慢”,而是适用场景不同。以下情况 LEFT JOIN 可能更稳甚至更快:
- 你需要从右表取字段(比如查“没订单的客户”还要显示最近一条订单时间 → 这时
NOT EXISTS不行,必须LEFT JOIN后加MAX()或子查询) - 左表极小(LEFT JOIN 的哈希构建成本低,优化器可能选
Hash Left Join - 右表是物化临时结果(如
(SELECT 'a' AS val UNION ALL SELECT 'b') t),数据量固定且小,LEFT JOIN的计划更可预测 - 某些旧版 MySQL(5.6 及以前)对
NOT EXISTS优化不足,强制走全表扫描;此时加STRAIGHT_JOIN的LEFT JOIN反而可控
注意:LEFT JOIN 的 ON 条件字段类型必须和左表严格一致(比如都是 BIGINT),否则隐式转换会让索引失效 —— 这是线上最常被忽略的性能断点。
NULL 值让 NOT IN 和 LEFT JOIN 都可能出错,但 NOT EXISTS 不怕
这是语义层面的硬伤,不是性能问题:
-
NOT IN (SELECT id FROM blacklist):只要blacklist.id里有一个NULL,整条查询返回空结果(三值逻辑:id NOT IN (1, 2, NULL)→UNKNOWN→ 被过滤) -
LEFT JOIN ... ON a.id = b.id WHERE b.id IS NULL:如果b.id允许为NULL,那些b.id IS NULL的行也会被当作“不存在”,造成误判(实际是右表脏数据) -
NOT EXISTS (SELECT 1 FROM blacklist b WHERE b.id = a.id):完全不受blacklist.id是否为NULL影响,只关心“有没有b.id = a.id的非 NULL 匹配行”
所以,只要右表关联字段可能含 NULL(比如外键未约束 NOT NULL、ETL 脏数据残留),NOT EXISTS 就是唯一安全选项。
怎么一眼判断该用哪个
打开 EXPLAIN FORMAT=TREE(MySQL 8.0+)或 EXPLAIN (ANALYZE, BUFFERS)(PostgreSQL),盯住三处:
- 看右表操作符是否出现
Using index condition或Index Only Scan——NOT EXISTS更容易触发 - 看
rows_examined_per_scan:若LEFT JOIN的右表扫描行数远大于其总行数(比如 100 万行表扫了 500 万次),说明 Nested Loops 失控 - 看是否有
Materialize或Hash Right Join—— 这是LEFT JOIN性能拐点,而NOT EXISTS几乎不会出现
真实调试中,别猜索引有没有用上,直接改写成 NOT EXISTS 并加 FORCE INDEX(MySQL)或 /*+ IndexScan(t2) */(Oracle),再对比执行时间。很多“慢查询”本质不是语法选错,而是子查询里写了 UPPER(b.status) = 'ACTIVE' 这类索引失效表达式 —— 这比纠结 NOT EXISTS 还重要。


















