IS NULL查询常不走索引,因NULL不参与B+树排序,优化器估算高占比(>5%~10%)或统计过期时全表扫描成本更低;8.0.13+对单列索引中极低NULL占比场景有增强支持。

IS NULL 查询为什么经常不走索引
MySQL 的 B+ 树索引确实会存储 NULL,但它被当作特殊标记值处理,不参与常规排序和比较。优化器在估算成本时,常认为“扫一遍索引再过滤 IS NULL”不如直接全表扫描快——尤其当该列 NULL 占比超过 5%~10%,或统计信息过期时。EXPLAIN 显示 key 非空但 rows 接近总行数,基本就是这个原因。
例外情况:MySQL 8.0.13+ 对单列索引上的 IS NULL 做了增强,若该列 NULL 极少(比如 IS NULL(如 INDEX(a,b) + WHERE a IS NULL AND b = 1),整个前缀就失效,几乎必然回退到全索引扫描。
IS NOT NULL 几乎永远不用索引
IS NOT NULL 在绝大多数场景下不会走索引,哪怕你给它建了 UNIQUE INDEX 或普通 INDEX。根本原因是 B+ 树无法高效“跳过”所有 NULL 标记去定位非空值——优化器干脆放弃索引,选择全表扫描,Extra 里只显示 Using where,没有 Using index。
- 强制加
FORCE INDEX不但没用,反而更糟:它会先按索引顺序读取全部记录(含NULL),再逐行过滤,额外增加回表和 CPU 过滤开销 - 真正可行的绕过方式只有两个:
NOT NULL+ 默认值(如DEFAULT ''),或建生成列索引(如ALTER TABLE t ADD COLUMN email_filled TINYINT GENERATED ALWAYS AS (email IS NOT NULL) STORED,再给email_filled加索引)
联合索引中 NULL 字段位置不当会拖垮查询
联合索引 (a, b, c) 要生效,必须满足最左前缀。但如果 b 列允许 NULL 且实际占比高(比如 60%),那么 WHERE a = ? AND b IS NOT NULL 只能利用 a 部分,b IS NOT NULL 条件退化为 post-filter——因为 NULL 打乱了 b 在 B+ 树里的有序分布,优化器不敢信任索引顺序做范围查找。
高频用 IS NULL/IS NOT NULL 的字段,不要放在联合索引中间:
- 尽量靠右(如
(a, c, b),其中b是那个 NULL 列) - 或者直接剔除,单独建单列索引(仅当结果集极小,比如 0.1% 行)
- 用
COALESCE(b, -1)改写查询能“骗过”索引,但普通索引会失效——除非你建的是函数索引:CREATE INDEX idx_b_coal ON t ((COALESCE(b, -1)))(MySQL 8.0.13+)
每多一个可为 NULL 的索引列,就多占 1 字节存储开销
InnoDB 对每个允许 NULL 的索引列,在每条索引记录里额外分配 1 字节空值标记位(不是 1 bit,是按字节对齐的 1 字节)。这会导致:
-
key_len比预期大 1(例如VARCHAR(10)utf8mb3 本应 32,加NULL就变 33),影响索引页容纳记录数,B+ 树层级可能上升 - 1000 万行表,仅这一项就多占约 1.2MB 存储;若联合索引含 3 个可空列,就是 3 字节 × 1000 万 = 3MB
- MyISAM 更敏感:原本固定长度的整数索引(如
INT)因允许NULL被迫转为可变长度,易引发存储碎片
真正容易被忽略的是:这个开销不是“偶尔多占一点”,而是每条索引记录都恒定存在,且随数据增长线性放大。而 NOT NULL + DEFAULT '' 或 DEFAULT 0 完全规避该字节,语义也更清晰——空字符串是确定值,可被完整索引、参与等值比较,无需额外元数据。



















