MySQL索引中允许NULL的列每条记录多占1字节空值标记位,导致key_len增大、索引效率下降;NULL参与比较返回UNKNOWN,使WHERE过滤丢失数据;联合索引中NULL引发半失效;TIMESTAMP等类型与NULL结合易触发隐式行为。

MySQL 索引中 NULL 值多占 1 字节,且影响 key_len 计算
在 InnoDB 中,每个允许为 NULL 的列,只要被加入索引,就会在每条索引记录里额外占用 1 字节的“空值标记位”。这个字节不存数据,只标识该字段是否为 NULL。你执行 EXPLAIN 查看执行计划时,key_len 值会比非空列明显大 1(比如 VARCHAR(10) 在 utf8mb3 下本应是 32,加了 NULL 就变成 33)。这不只是数字差异:它意味着索引页能容纳的记录数变少,B+Tree 层数可能增加,I/O 次数上升。
常见错误现象:ALTER TABLE t MODIFY COLUMN name VARCHAR(10) NULL 后发现联合索引命中率下降、key_len 异常变长;或者 MyISAM 表中,一个原本固定长度的整数索引(如 INT)因允许 NULL 被迫转为可变长度索引,引发存储碎片。
- MyISAM 和 InnoDB 都有该开销,但 MyISAM 更敏感(固定索引变可变)
-
NOT NULL列 + 显式默认值(如DEFAULT ''或DEFAULT 0)可完全避免此字节 - 复合索引中任一列为
NULL,该索引记录仍存在,但优化器对范围扫描和等值匹配的判断逻辑更保守
NOT IN、!=、COUNT 等操作在含 NULL 列上行为反直觉
NULL 不等于任何值,包括自身,所有涉及比较的操作符(=、!=、、IN、NOT IN)遇到 NULL 都返回 UNKNOWN,而 WHERE 子句只保留 TRUE 结果。这就导致看似合理的查询意外丢数据。
典型陷阱:
-
SELECT * FROM user WHERE status != 'active'—— 若status为NULL,该行不会出现在结果中(不是因为不匹配,而是因为条件求值为UNKNOWN) -
SELECT id FROM order WHERE user_id NOT IN (SELECT id FROM user)—— 只要子查询结果里任意id是NULL,整个NOT IN表达式恒为FALSE,结果集为空 -
COUNT(status)忽略所有NULL值,而COUNT(*)统计全部行 —— 容易误判字段填充率 -
CONCAT(first_name, last_name)中任一参数为NULL,结果直接是NULL,而非拼接字符串
联合索引中 NULL 值导致索引“半失效”
联合索引 (a, b) 的设计目标是支持 WHERE a = ? AND b = ? 的高效查找。但如果 a 允许 NULL,且实际数据中大量 a IS NULL,那么以下情况会发生:
-
WHERE a IS NULL AND b = 1:优化器可能只用到索引第一列(定位所有a IS NULL的记录),再对结果集逐行检查b,无法跳过b的索引部分 -
WHERE a = 1 AND b IS NULL:虽能走索引前缀,但b IS NULL条件无法利用 B+Tree 的有序性,本质是范围扫描后过滤 -
ORDER BY a, b:虽然排序仍可用索引,但NULL在排序中被统一视为最小值(或最大值,取决于ASC/DESC),与业务语义常不符,比如把“未知生日”排在“1900-01-01”之前
这不是语法报错,而是性能滑坡——看起来走了索引,实际 rows 扫描量远超预期。
timestamp 类型 + NULL 默认值极易触发隐式行为变更
MySQL 的 TIMESTAMP 列在未启用 explicit_defaults_for_timestamp 参数(5.6.6+ 默认关闭,但很多老环境仍为 OFF)时,会对 NULL 做特殊处理:
- 第一个
TIMESTAMP列若允许NULL,插入NULL会被自动转为当前时间(不是你想表达的“未知”,而是“现在”) - 若该列设为
NOT NULL DEFAULT CURRENT_TIMESTAMP,则插入NULL会报错,语义清晰;但若设为NULL DEFAULT NULL,行为就依赖 server 配置,迁移数据库时极易出错 -
ON UPDATE CURRENT_TIMESTAMP与NULL共存时,更新行为不可控:某些版本下NULL值会被覆盖,某些则保留
这种配置耦合让 DDL 变得脆弱,尤其在跨版本升级或主从同步场景下,NULL 不再是“无值”,而成了开关式的隐式逻辑开关。
真正麻烦的不是“能不能用 NULL”,而是它让同一句 SQL 在不同上下文里含义漂移:索引结构、统计信息、优化器决策、函数返回值、甚至服务端参数都会参与定义它的行为。与其花精力记住每种例外,不如从建表开始就用 NOT NULL + 有意义的默认值堵住源头。











