最直接有效的解法是把字段设为 NOT NULL 并配默认值,因为 MySQL 索引(B+Tree)默认不存储 NULL 值,导致 WHERE col IS NULL 几乎必然全表扫描;设 NOT NULL 后可彻底消除 IS NULL 查询需求,使等值与范围查询稳定走索引。

最直接有效的解法是把字段设为 NOT NULL 并配默认值,而不是绕着 IS NULL 做索引补救。
为什么 IS NULL 查询经常不走索引
MySQL 的 B+Tree 索引(InnoDB/MyISAM)默认不存储 NULL 值,哪怕你给该列建了单列索引,WHERE col IS NULL 也大概率触发全表扫描——EXPLAIN 里会看到 type: ALL、key: NULL。这不是 bug,是设计:索引结构里没存这个“空”,自然没法定位。
唯一例外是联合索引中 NULL 出现在非最左列(如 INDEX(a, b)),WHERE a = 1 AND b IS NULL 能用上索引,但仅限于 a 的等值过滤,b IS NULL 不参与范围下推,后续列也无法利用。
设置 NOT NULL DEFAULT 是根治手段
业务允许的前提下,直接改表结构比加生成列、建覆盖索引更彻底。它从源头消灭 IS NULL 查询需求,让所有等值、范围查询都能稳定走索引。
-
ALTER TABLE t MODIFY col INT NOT NULL DEFAULT 0;—— 数值类字段推荐用0或-1表示“未设置” -
ALTER TABLE t MODIFY email VARCHAR(255) NOT NULL DEFAULT '';—— 字符串类慎用空字符串,需确认业务逻辑是否接受 - 改之前先检查存量数据:
SELECT COUNT(*) FROM t WHERE col IS NULL;,若结果非零,得先清洗或迁移 - 注意外键和应用层兼容性:ORM 可能默认允许
null,Java 的Integer和int行为不同,需同步调整
哪些场景不适合直接设 NOT NULL
不是所有字段都适合一刀切。以下情况需谨慎:
- 日志/审计类字段(如
processed_at)天然存在大量未处理状态,强制设默认值会污染语义,此时更适合用生成列 + 索引 - 历史遗留系统,改表结构需停机或影响大,可先用应用层兜底(插入前校验 + 查询时改写为
WHERE col = 'placeholder') - 联合唯一索引中某列允许
NULL是业务必需(如“手机号或邮箱任一唯一”),此时设NOT NULL会破坏约束逻辑,应优先考虑拆物理列或应用层去重
别信“加个索引就能救 IS NULL”
有人试过 CREATE INDEX idx_col ON t(col); 后跑 EXPLAIN SELECT * FROM t WHERE col IS NULL;,发现仍不走索引——这不是配置问题,是 MySQL 底层机制决定的。8.0 的函数索引也绕不开这点:CAST(col AS CHAR) 遇到 NULL 还是 NULL,照样被跳过。真要高频查 IS NULL,唯一靠谱路径是把它转成非空确定值,比如加 processed_at_is_null TINYINT GENERATED ALWAYS AS (processed_at IS NULL) STORED 再建索引。
真正容易被忽略的是:改字段为 NOT NULL 后,旧 SQL 中的 = NULL 写法会永远返回空结果(SQL 标准要求),必须同步清理代码里所有这类错误条件。











