必须用 LEFT JOIN + IS NULL 替代 NOT IN,否则遇 NULL 结果全空且常触发全表扫描;因 NOT IN 遇子查询含 NULL 时整体判为 UNKNOWN 被 WHERE 过滤,且优化器难以利用索引。

直接说结论:用 LEFT JOIN + IS NULL 替代 NOT IN,不是“可选优化”,而是必须改写;否则一遇到 NULL,结果就全空,且执行计划大概率走全表扫描。
为什么 NOT IN 会查不出数据还巨慢
NOT IN 在语义上是“不在集合中”,但 SQL 标准规定:只要子查询返回的任意一行某列为 NULL,整个 NOT IN 表达式就判定为 UNKNOWN,而 WHERE 只认 TRUE,于是整条记录被过滤掉——最终结果集为空,哪怕逻辑上该有数据。
更糟的是,MySQL 优化器面对 NOT IN (subquery) 几乎无法下推索引访问,常被迫对右表做全表扫描(Handler_read_rnd_next 高到几百万就是典型信号),尤其当子查询没加 WHERE 过滤或字段无索引时。
-
NOT IN无法利用索引做范围查找,基本退化为Index Full Scan或回表全查 - 即使左表、右表都建了索引,
NOT IN也大概率不走 - 子查询里只要有一列含
NULL(比如SELECT uid FROM leaves WHERE mid=897返回了NULL),整条NOT IN就失效
LEFT JOIN + IS NULL 的正确写法
核心是显式控制关联逻辑,把“排除”动作拆成两步:先关联,再筛空。语义清晰,索引友好,行为确定。
假设要查 signshould 中 uid 不在 sign 和 leaves 里的记录:
SELECT a.* FROM signshould AS a LEFT JOIN (SELECT uid FROM sign WHERE mid = 897 AND thetype = 0) AS b ON a.uid = b.uid LEFT JOIN (SELECT uid FROM leaves WHERE mid = 897) AS c ON a.uid = c.uid WHERE a.mid = 897 AND b.uid IS NULL AND c.uid IS NULL;
- 驱动表必须是主业务表(这里是
signshould),确保LEFT JOIN左侧数据不丢 - 每个
JOIN子查询都要提前加好过滤条件(如mid = 897),避免拉全量右表 -
ON字段(如a.uid = b.uid)两端都必须有索引;若右表字段可能为NULL,子查询里加WHERE uid IS NOT NULL预过滤 -
WHERE中的IS NULL必须写在右表别名字段上(b.uid IS NULL),不能写成a.uid IS NULL
LEFT JOIN 和 NOT EXISTS 怎么选
两者都能绕过 NOT IN 的 NULL 陷阱,但适用场景略有差异:
- 右表数据量小、关联字段有索引 →
NOT EXISTS更轻量,执行计划通常显示为DEPENDENT SUBQUERY,IO 更少 - 右表数据量大、需复用同一张中间结果(比如多个
NOT IN条件)→LEFT JOIN更合适,一次关联多次判断 - 右表本身带复杂聚合或计算 → 倾向
LEFT JOIN,子查询更易读、易调试 - MySQL 5.7+ 对
NOT EXISTS优化较好,但若子查询含GROUP BY或窗口函数,LEFT JOIN兼容性更稳
示例 NOT EXISTS 写法:
SELECT * FROM signshould a
WHERE a.mid = 897
AND NOT EXISTS (
SELECT 1 FROM sign b
WHERE b.uid = a.uid AND b.mid = 897 AND b.thetype = 0
)
AND NOT EXISTS (
SELECT 1 FROM leaves c
WHERE c.uid = a.uid AND c.mid = 897
);
容易被忽略的索引和执行验证细节
写了 LEFT JOIN 不等于性能就上来,关键还得看执行计划是否真用了索引:
- 对每个
JOIN的右表字段(如sign.uid、leaves.uid)单独建索引,复合索引优先考虑(mid, uid)顺序(因子查询带WHERE mid = ?) - 用
EXPLAIN看key_len:如果值远小于索引定义长度,说明没用全索引;rows值应明显低于原NOT IN查询 - 检查
Extra列:出现Using where; Using index是理想状态;若看到Using temporary; Using filesort,说明JOIN子查询没写好或缺少索引 - 线上改写前务必在从库或测试环境对比
Handler_read_rnd_next,下降两个数量级才算有效优化
真正卡住人的从来不是语法怎么写,而是右表字段有没有 NULL、索引建没建对、EXPLAIN 里 key_len 没看懂——这三处漏掉任何一环,LEFT JOIN 也会白写。











