not in 必然触发全表扫描且结果可能为空,因其语义遇null即判unknown而被where过滤;应改用left join + is null,并确保右表子查询排除null、连接字段类型一致、无隐式转换。

NOT IN 几乎必然触发全表扫描,且结果可能为空——这不是配置或索引问题,而是语义和执行逻辑决定的;必须改用 LEFT JOIN + IS NULL 重写。
NOT IN 遇到 NULL 就查不到数据,还让索引失效
只要子查询返回任意一行的连接字段为 NULL(比如 SELECT id FROM t2 中有一条 id IS NULL),整个 NOT IN 表达式就判为 UNKNOWN。而 SQL 标准规定:WHERE 只保留 TRUE 行,UNKNOWN 和 FALSE 全被过滤。结果就是——一条不返回,DELETE 一条不删。
- 优化器不敢对含
NULL的子查询下推索引,直接放弃使用左表上的索引 - 即使你给
t2.id加了索引,MySQL 5.7 及更早版本基本无视;8.0 虽支持物化,但只要统计信息不准或子查询不可物化,仍退化为全表扫描 -
EXPLAIN中常见type: ALL、key_len: NULL、Extra: Using where; Using join buffer,Handler_read_rnd_next动辄百万级
为什么加了索引,NOT IN 还是走全表扫描
B+ 树索引擅长“定位存在”,不擅长“确认不存在”。NOT IN 要求逐行比对是否“不在集合中”,没有起始/终止边界,无法利用树结构跳过数据块,本质上等价于全索引扫描甚至回表全查。
-
IN可走INDEX RANGE SCAN,查几个值很快;NOT IN没有范围概念,优化器无法导航子树 - 子查询若没加
WHERE过滤、返回行数多,优化器倾向选择最保守的嵌套循环策略 - 隐式类型转换(如左表
INT、右表VARCHAR)会让索引彻底失效,连LEFT JOIN都救不回来
安全重构:用 LEFT JOIN + IS NULL 替代 NOT IN
正确写法不是语法替换,而是守住三个关键点:
- 右表子查询必须显式排除
NULL:(SELECT id FROM t2 WHERE id IS NOT NULL),否则LEFT JOIN后t2.id IS NULL会把本该匹配成功的行也留下 - 连接条件必须是纯等值,不能带函数或转换,例如
ON t1.uid = t2.uid可以,ON t1.uid = CAST(t2.uid AS CHAR)不行 - 左右表连接字段类型、字符集必须一致;若不一致,先统一再改写,否则索引照样不走
- 原语句:
DELETE FROM t1 WHERE id NOT IN (SELECT id FROM t2)
安全改写:DELETE t1 FROM t1 LEFT JOIN t2 ON t1.id = t2.id WHERE t2.id IS NULL
容易被忽略的细节:LEFT JOIN 写法也会白忙一场
很多人以为只要换成 LEFT JOIN 就万事大吉,其实漏掉任一细节都会导致逻辑错误或性能不升反降:
- 没在右表子查询里加
WHERE id IS NOT NULL,会导致左表中本应被排除的行因右表匹配到NULL而被误留 - 连接字段类型不一致(比如
t1.id是BIGINT,t2.id是VARCHAR),MySQL 会做隐式转换,索引失效,执行计划仍可能退化为全表扫描 -
EXPLAIN看到type: ALL且rows接近左表总行数,说明改写失败,得回头检查右表字段是否真非空、类型是否对齐
真正卡住性能的,往往不是数据量,而是那一行 id IS NULL 引发的语义坍塌和执行计划失控。重构时盯紧 NULL、类型、索引三件事,比调优参数管用十倍。











