IN查询通常能走索引,因其被优化器视为多个等值条件组合,只要列有索引且参数为确定常量或小结果子查询,B+Tree即可逐个定位;而NOT IN、!=等否定条件因低选择性及NULL问题,优化器倾向全表扫描。

IN 查询为什么通常能走索引
IN 是等值匹配的扩展形式,MySQL 优化器把它当作多个 = 条件的组合来处理。只要 IN 列本身有索引,且参数是确定的常量(或来自简单子查询),B+Tree 就能按有序性逐个定位。
常见有效场景:
-
SELECT * FROM user WHERE age IN (18, 20, 22);→ 走idx_age,执行三次等值查找 -
SELECT * FROM order WHERE user_id IN (SELECT id FROM user WHERE status = 1);→ 若子查询结果集小且可物化,仍可能走索引 - 参数个数不太多(一般
NOT IN 为什么大概率触发全表扫描
根本原因不是语法问题,而是优化器判断:否定范围往往覆盖绝大多数数据,回表过滤的成本 > 直接全表扫描。
典型失效现象:
-
SELECT * FROM user WHERE age NOT IN (18, 19, 20);→ 即使age有单列索引,key字段在EXPLAIN中为空 -
NOT IN遇到NULL值会整体返回空结果(三值逻辑),进一步增加优化器规避索引的倾向 - 联合索引中,
NOT IN出现在非最左位置时,前面的等值字段能用,后面直接截断:WHERE name = '张三' AND age NOT IN (18, 19)→ 只用到name,age不参与索引定位
!= 和 NOT IN 的底层逻辑一致吗
是的。MySQL 对 !=、<>、NOT IN、IS NOT NULL 这几类否定条件统一按“低选择性”处理——优化器估算出需要扫描行数占比过高,就放弃索引。
关键区别在于语义边界:
-
!= 18是明确的补集,但优化器无法预估补集大小(除非统计信息极准) -
NOT IN (18, 19, 20)看似范围更小,但一旦列表含NULL或实际数据分布倾斜(比如 95% 用户 age 都在 18–25),优化器就会果断选全表扫描 - 没有“否定索引”这种东西:B+Tree 天然适合定位起点和终点,不适合跳着排除
真要写否定逻辑,有什么替代方案
绕开 NOT IN 和 != 并不难,关键是把“排除”转成“包含”或“范围拼接”。
实操建议:
- 用
LEFT JOIN + IS NULL替代NOT IN子查询:SELECT u.* FROM user u LEFT JOIN banned b ON u.id = b.user_id WHERE b.user_id IS NULL - 用
BETWEEN或多个OR拆分范围:WHERE age < 18 OR age > 25(需确保范围连续且覆盖全集) - 对高频否定查询,考虑冗余一个标记字段(如
is_banned TINYINT),改查WHERE is_banned = 0,走等值索引 - 确认数据量:小表(NOT IN 失效不明显,优化器本就倾向全表扫描——别为此过度设计
最易被忽略的一点:NOT IN 是否失效,最终取决于 EXPLAIN 的 key 和 rows,而不是你“觉得应该走”。哪怕加了索引,只要优化器算出来全表更快,它就真不走。


















