在SQL查询中,经常需要实现查询A表中存在但B表中不存在的记录的需求,常见的实现方式有NOT IN和LEFT JOIN两种。很多实际案例显示NOT IN的查询性能往往弱于LEFT JOIN,这和NULL值的处理以及数据库执行计划的生成有直接关系。

两种查询方式的常规写法
假设我们有两个表,user表存储用户信息,order表存储订单信息,现在需要查询没有下过订单的用户,两种写法如下:
NOT IN写法
-- 查询没有下过订单的用户 SELECT u.id, u.name FROM user u WHERE u.id NOT IN (SELECT user_id FROM order);
LEFT JOIN写法
-- 查询没有下过订单的用户 SELECT u.id, u.name FROM user u LEFT JOIN order o ON u.id = o.user_id WHERE o.user_id IS NULL;
NULL值对NOT IN逻辑的影响
NOT IN的逻辑是判断左侧值是否不在右侧子查询的结果集中,这个判断过程对NULL值非常敏感。SQL中NULL代表未知值,任何值和NULL进行比较的结果都是NULL,而不是TRUE或FALSE。
如果子查询SELECT user_id FROM order的结果中包含NULL值,那么整个NOT IN的判断逻辑就会出问题。比如当u.id等于5时,子查询结果里有NULL,那么5 NOT IN (1,2,NULL)的判断过程是:先判断5不等于1、5不等于2,这两个是TRUE,但5不等于NULL的结果是NULL,最终整个判断结果就是NULL,不会被判定为TRUE,这条记录就不会被返回。
更严重的是,数据库优化器在处理NOT IN时,如果检测到子查询可能返回NULL,就无法使用高效的索引查找策略,只能选择全表扫描或者更复杂的执行路径,这会直接导致性能下降。
LEFT JOIN的执行逻辑优势
LEFT JOIN的执行逻辑是先对两个表进行连接,然后过滤连接结果为NULL的记录。这种逻辑对NULL值的处理更明确:只有当右表没有匹配到记录时,右表的字段才会是NULL,过滤条件o.user_id IS NULL的判断逻辑非常清晰,不会受到右表字段是否为NULL的额外影响。
数据库优化器在处理LEFT JOIN时,可以更灵活地选择执行计划,比如先对右表的连接字段建立索引,使用嵌套循环连接或者哈希连接等高效策略,只要右表的user_id字段有索引,就能快速定位匹配的记录,执行效率更高。
执行计划层面的差异对比
我们可以通过EXPLAIN命令查看两种写法的执行计划,以下是两种写法的典型执行计划对比:
| 对比项 | NOT IN写法执行计划 | LEFT JOIN写法执行计划 |
|---|---|---|
| 访问类型 | 子查询全表扫描,主表全表扫描 | 主表全表扫描,右表索引范围扫描 |
| 临时表使用 | 可能会生成临时表存储子查询结果 | 一般不需要临时表 |
| 索引利用率 | 子查询字段有索引也可能无法使用 | 右表连接字段索引可以被正常利用 |
| 预估执行行数 | 子查询结果集大时预估行数不准 | 预估行数更准确 |
从执行计划可以看出,NOT IN写法因为NULL值的潜在影响,优化器很难生成最优的执行策略,而LEFT JOIN的逻辑更明确,优化器可以生成更高效的执行路径。
实际开发中的优化建议
- 如果子查询的字段明确不可能为NULL(比如字段有NOT NULL约束),并且数据量较小,NOT IN和LEFT JOIN的性能差距可能不大,但仍建议优先使用LEFT JOIN。
- 如果子查询的字段允许为NULL,绝对不要使用NOT IN,避免逻辑错误和性能问题。
- 无论使用哪种写法,都要确保连接字段或者子查询的字段有合适的索引,这是提升查询性能的基础。
- 对于数据量大的表,尽量通过
EXPLAIN命令查看执行计划,确认查询是否使用了预期的索引,避免全表扫描。
总结
NOT IN比LEFT JOIN性能差的核心原因是NULL值对NOT IN逻辑判断的干扰,导致数据库优化器无法生成高效的执行计划。LEFT JOIN的处理逻辑对NULL值更友好,优化器可以更好地利用索引和高效的连接策略。实际开发中,实现存在性否定查询时,优先选择LEFT JOIN加IS NULL的写法,既能避免NULL值带来的逻辑问题,也能获得更好的查询性能。