NOT IN 性能极差是因为遇到 NULL 返回 UNKNOWN 导致结果为空,迫使数据库放弃索引、全表扫描;应改用 LEFT JOIN + IS NULL,确保关联字段非空且有索引,并通过 EXPLAIN 验证 key_len 和 rows。

NOT IN 为什么慢得离谱
因为 NOT IN 遇到任意一个 NULL 值就整个表达式返回 UNKNOWN,最终结果集为空——这不仅是语义陷阱,更是执行计划杀手。数据库往往放弃走索引,转而全表扫描右表再逐行过滤,尤其当子查询返回大量数据时,性能断崖式下跌。
- 子查询里只要有一列含
NULL,NOT IN (subquery)就不返回任何行(哪怕逻辑上该有) - 优化器很难对
NOT IN推导出有效索引访问路径,即使被查字段明明建了索引 - MySQL 5.7+、PostgreSQL、SQL Server 都存在类似行为,不是某一家的 bug,而是 SQL 标准定义使然
LEFT JOIN + IS NULL 是更可控的替代写法
用 LEFT JOIN 显式关联后筛 NULL,语义清晰、索引友好、行为确定。关键在于:驱动表要选对,且关联字段必须非空或提前过滤 NULL。
- 确保左表是主业务表(如
orders),右表是被排除集合(如blacklisted_users) - JOIN 条件字段在右表上要有索引,例如
ON orders.user_id = blacklisted_users.user_id,且blacklisted_users.user_id不为NULL - 如果右表字段可能为
NULL,先用WHERE right_table.id IS NOT NULL预过滤,否则LEFT JOIN仍可能产生意外匹配 - 示例:
SELECT o.* FROM orders o LEFT JOIN blacklisted_users b ON o.user_id = b.user_id WHERE b.user_id IS NULL;
IN 和 NOT IN 的索引使用差异极大
IN 可以走索引查找(Index Seek / Index Range Scan),而 NOT IN 几乎必然触发索引全扫(Index Full Scan)甚至回表全查,本质是谓词方向不可下推。
-
WHERE id IN (1,2,3)→ 通常走INDEX RANGE SCAN -
WHERE id NOT IN (1,2,3)→ 即使id有索引,也可能退化为FULL TABLE SCAN,尤其在统计信息不准或值分布倾斜时 - 若硬要用
NOT IN,务必确认子查询结果不含NULL,可用WHERE col IS NOT NULL显式排除,但不如改写成LEFT JOIN干净
别忘了检查执行计划里的 key_len 和 rows
光看 SQL 写法没用,得看 EXPLAIN 输出里两个关键字段:实际用了索引哪几列(key_len),以及预估扫描行数(rows)。负向查询最容易在这里暴露问题。
-
key_len比预期小?说明只用了联合索引前缀,后面字段没生效 -
rows接近表总行数?基本等于没走有效索引,赶紧切到LEFT JOIN方案 - MySQL 中如果
Extra出现Using where; Using join buffer,大概率是NOT IN引发的嵌套循环低效回表
索引本身不能拯救负向逻辑,真正起作用的是把“排除”动作从谓词层移到连接层——这是数据库引擎更容易理解和优化的模式。很多人卡在改写后还是慢,其实是忘了右表关联字段的 NULL 处理或索引覆盖不全。






























