not in 在海量数据下必然失效,因含 null 时条件变 unknown 导致结果为空,且常引发全表扫描;应改用 left join + is null(需满足非空约束、过滤条件写 on 中)或 not exists(更稳,需联合索引)。

直接换掉 NOT IN,它在海量数据下不是“慢一点”,而是大概率失效+全表扫描+查不到数据——尤其当子查询结果含 NULL 时,整个条件变成 UNKNOWN,结果集为空,EXPLAIN 显示 type: ALL、key_len: NULL 就是铁证。
为什么 NOT IN 在海量数据下必然崩盘
根本问题不在语法,而在 MySQL 对 NOT IN 的语义处理和执行计划脱节:
-
SELECT * FROM t1 WHERE id NOT IN (SELECT id FROM t2)——只要t2.id里有一个NULL,整行判定为UNKNOWN,WHERE 不成立,结果为空 - 优化器不敢把索引下推进子查询,尤其子查询带
WHERE过滤(如status = 'active')时,只能先取全量再逐行比对 - 海量数据下,
Handler_read_rnd_next动辄几百万次,rows显示全表行数,就是它在反复随机读 - 即使
t2.id有索引,MySQL 5.7+ 以前基本不走;5.7+ 遇到复杂条件或NULL仍大概率放弃
LEFT JOIN + IS NULL 的硬性前提与写法
这不是简单替换,而是一套必须满足的约束,否则逻辑错或更慢:
- 连接字段必须是确定等值,如
ON a.ref_id = b.id,不能加函数、类型转换或表达式 - 右表用于连接的字段(如
b.id)必须非空:要么建表时加NOT NULL,要么在ON子句中显式排除:ON a.ref_id = b.id AND b.id IS NOT NULL - 原
SELECT子查询里的过滤条件(如WHERE status = 'inactive')必须写进ON,不能放WHERE,否则外连接变内连接,漏数据 -
WHERE子句中必须用IS NULL,写成= NULL永远不成立
错误示例:SELECT a.* FROM orders a LEFT JOIN customers b ON a.customer_id = b.id WHERE b.id IS NULL AND b.status = 'active' ——b.status = 'active' 放 WHERE 会让 b.id IS NULL 永远不生效,实际变成内连接。
海量数据时 NOT EXISTS 往往比 LEFT JOIN 更稳
当右表数据量极大、连接字段重复多或存在大量 NULL 时,NOT EXISTS 天然规避了 LEFT JOIN 的中间结果膨胀问题:
SELECT * FROM orders o WHERE NOT EXISTS (SELECT 1 FROM customers c WHERE o.customer_id = c.id AND c.status = 'active')- 它对每条左表记录只做一次存在性检查,找到第一个匹配就停,不构造临时结果集
- 只要
c.id和c.status上有联合索引(如INDEX idx_id_status (id, status)),就能高效跳查 - 不需要担心右表字段是否
NOT NULL,语义天然安全
注意:NOT EXISTS 子查询中的条件必须包含左表关联字段,否则会退化为相关子查询,性能爆炸。
索引和类型一致性是隐形门槛
写了 LEFT JOIN 或 NOT EXISTS 不等于自动快——没索引或类型不一致,照样全表扫描:
- 检查
EXPLAIN输出的key和key_len:若为NULL,说明没走索引 -
a.customer_id和b.id必须类型完全一致(比如都是BIGINT UNSIGNED),字符集也要一致(如都用utf8mb4_0900_as_cs) - 常见坑:
VARCHAR对CHAR、utf8mb4_general_ci对utf8mb4_0900_as_cs,会触发隐式转换,索引失效 - 右表如果是子查询(如
(SELECT id FROM customers WHERE status = 'active')),别名后字段无法直连索引,应提前物化为临时表或改用NOT EXISTS
真正卡住性能的,往往不是写法本身,而是连接字段没索引、类型不一致、或子查询条件被错误地放在 WHERE 而非 ON ——这些地方一错,前面所有优化都白搭。











