mysql建议索引字段设为not null,因innodb的b+树索引根本不存储null值,导致is null必全表扫描、=查询可能弃用索引、复合索引范围查找失效,且增加存储开销与统计偏差。

MySQL官方建议索引字段设为 NOT NULL,不是为了“规范好看”,而是因为 NULL 值根本不会写入 InnoDB 的 B+ 树索引——它让索引失效、让优化器误判、还悄悄多占空间。
WHERE col IS NULL 为什么一定全表扫描?
InnoDB 的 B+ 树索引结构里压根不存 NULL 值。不是“跳过”,是连索引页都不写进去。所以哪怕你在 status 上建了索引,WHERE status IS NULL 也必然触发 type: ALL(全表扫描)。
-
EXPLAIN中key字段为空、rows预估远高于实际,就是典型信号 - 别指望优化器“聪明”地用索引找
NULL——它没得选,因为物理上不存在 - 想绕开?只能加计算列:比如
ALTER TABLE t ADD status_is_null TINYINT AS (status IS NULL) STORED,再给它建索引。但这属于事后补救,不是设计合理
WHERE col = ? 时索引可能被悄悄弃用
字段允许 NULL,会让优化器对选择率(selectivity)估算失真。哪怕你查的是 WHERE type = 'order',只要该列有较多 NULL(比如 >5%),优化器就可能直接放弃走索引,改用全表或别的索引。
-
EXPLAIN中key_len比预期大 1 字节?那是NULL位图标记位在起作用,说明存储层已为此付出隐式成本 - 执行
ANALYZE TABLE t后再EXPLAIN,如果rows预估突然变大,大概率是NULL扰乱了统计信息 - ORM(如 MyBatis、Django ORM)生成的
= ?查询,一旦底层列可空,就容易触发这种静默降级
复合索引中 NULL 列会破坏范围查找能力
联合索引 (user_id, created_at) 中,若 created_at 允许 NULL,那么 WHERE user_id = 123 AND created_at > '2025-01-01' 很可能无法下推 created_at 的范围条件。
- InnoDB 要求范围查询的右列必须“确定可比较”,而
NULL不参与大小比较,排序时统一排最前,逻辑上不可比 - 结果往往是只用到
user_id单列索引,后面靠回表过滤,I/O 成倍增加 - 修复方式不是删数据,而是
ALTER TABLE t MODIFY created_at DATETIME NOT NULL DEFAULT '1970-01-01',再批量更新历史空值
NOT NULL + DEFAULT 不是妥协,是适配 InnoDB 存储格式
很多人抗拒 NOT NULL DEFAULT '',觉得“空字符串不等于没值”。但对 InnoDB 来说,'' 是确定值:可完整进索引、能参与等值/范围比较、不额外占元数据空间;而 NULL 强制每行为每个可空列分配 1 bit(按字节对齐),1000 万行、10 个可空列,就多占约 1.2MB —— 看似不多,但会降低页内记录密度,间接抬高缓存和 I/O 压力。
-
UNIQUE索引下,NULL被特殊豁免:多行同时为NULL不报错。但业务上这常是漏洞(比如多个用户共享空手机号) - 从
NULL改成NOT NULL必须先UPDATE所有空值,否则ALTER TABLE ... SET NOT NULL直接报错;线上大表操作可能锁表数分钟 - 真正需要
NULL的场景极少,典型只有语义上必须区分“未发生”和“已发生”的字段,比如deleted_at、reviewed_at、mediator_id











