mysql生产环境建表时,not null是强制要求:字段业务上能确定有值(含default),就必须加not null;否则易致查询异常、索引失效、统计失真、i/o暴增。

MySQL生产环境建表时,NOT NULL不是“建议”,而是事实上的强制要求——只要字段业务上能确定有值(哪怕用DEFAULT ''或DEFAULT 0),就必须加NOT NULL。不这么做,轻则查询结果出人意料,重则索引失效、统计失真、I/O暴增。
IS NULL 查询必然全表扫描
InnoDB的B+树索引物理上根本不存NULL值。不是“跳过”,是压根不写进去。所以哪怕你在status列建了索引,WHERE status IS NULL也一定触发type: ALL。你看到EXPLAIN里key为空、rows预估远高于实际,就是这个信号。
常见错误现象:
- 明明加了索引,
IS NULL却跑得比全表还慢 - 监控发现某条慢查总在凌晨执行,查出来就是
WHERE deleted_at IS NULL
修复方式不是加hint,而是:ALTER TABLE t MODIFY deleted_at DATETIME NOT NULL DEFAULT '1970-01-01',再批量补默认值。别信“后期再改”,上线后改NOT NULL要锁表、可能失败。
= 查询时索引可能被静默弃用
优化器估算选择率时,会把NULL占比纳入统计。哪怕你查的是WHERE type = 'order',只要该列有5%以上NULL,优化器就可能直接放弃走索引,改用全表或别的索引。
关键线索:
-
EXPLAIN中key_len比预期大1字节 → 那是NULL位图标记位在起作用 - 执行
ANALYZE TABLE t后rows预估突变 →NULL扰乱了统计信息 - ORM生成的
= ?查询(如MyBatis)更容易触发这种降级
这不是配置问题,是存储层已为每个可空列隐式付出成本:1000万行 × 10个可空列 ≈ 多占1.2MB以上空间,且每页能存的记录数下降。
复合索引范围查找直接失效
联合索引(user_id, created_at)中,若created_at允许NULL,那么WHERE user_id = 123 AND created_at > '2025-01-01'大概率无法下推created_at的范围条件。
原因很底层:
-
NULL不参与大小比较,排序时统一排最前,逻辑上“不可比” - InnoDB要求范围查询的右列必须“确定可比较”,而
NULL不满足 - 结果往往是只用到
user_id单列,后面全靠回表过滤,I/O成倍增加
别指望ORDER BY created_at DESC能救——它照样没法利用索引有序性,因为NULL的存在破坏了B+树的连续键分布。
聚合与比较行为完全反直觉
NULL不是空字符串、不是零、不是false,它是“未知”。这导致:
-
COUNT(name)忽略NULL行,但COUNT(*)不会 → 统计口径不一致 -
name != '张三'查不出name IS NULL的行,结果集永远漏数据 -
NOT IN (SELECT ...)只要子查询含一个NULL,整个结果为空 -
SUM(age) + 1遇到age IS NULL,结果直接变NULL
这些不是bug,是SQL标准定义。但业务代码几乎从不主动处理NULL分支,线上出问题时排查成本极高——你得先意识到“这里可能有NULL”,再翻表结构、查数据分布、重写SQL。
真正难的不是加NOT NULL,而是历史数据里那些“不知道该填啥”的字段。它们暴露的其实是业务语义模糊,而不是数据库限制。用DEFAULT ''或DEFAULT 'unknown'不是妥协,是把模糊语义显式落地——毕竟,对InnoDB来说,''是确定值,能进索引、能比较、不额外占位;而NULL是黑洞,吞掉性能、吞掉一致性、吞掉排查时间。











