在关系型数据库中,NULL 是一个极为特殊且容易被误解的标记。它既不等于 0,也不等于空字符串,而是表示“值未知”或“值不存在”。为了强制业务数据在关键字段上必须填写,MySQL 提供了 NOT NULL 约束。该约束看似简单,却贯穿了建模、开发、运维、优化的全生命周期,是数据质量的第一道防线。本文将从概念、存储、索引、性能、迁移、运维、常见误区及最佳实践八个维度,对 NOT NULL 进行系统梳理,并给出可落地的操作建议。
## 一、概念与语法
### 1. 约束的本质
NOT NULL 属于列级完整性约束,只能在列定义时声明,或在事后通过 ALTER TABLE 追加。其作用是拒绝任何显式或隐式的 NULL 值写入,从而保证该列在业务意义上“必须有值”。需要特别强调的是,空字符串 '' 是合法的非 NULL 值,长度为 0 的字符串仍然满足 NOT NULL 约束;而 NULL 才触发违规。
### 2. 声明方式
建表时声明:
```sql
CREATE TABLE user (
id BIGINT PRIMARY KEY,
email VARCHAR(255) NOT NULL,
phone VARCHAR(32) NOT NULL
);
```
事后追加:
```sql
-- 先清理脏数据
UPDATE user SET email='' WHERE email IS NULL;
-- 再追加约束
ALTER TABLE user MODIFY email VARCHAR(255) NOT NULL;
```
若表内已存在 NULL,则 ALTER 会立即失败,提示 `ERROR 1138 (22004): Invalid use of NULL value`。
## 二、存储格式与内部实现
InnoDB 采用紧凑行格式(COMPACT/DYNAMIC)存储数据。每行记录分为「变长字段长度列表」、「NULL 标志位」、「记录头信息」、「列值」四部分。NOT NULL 列在「NULL 标志位」中不占用任何位,相反,可空列需要 1 bit 来标记是否为 NULL。因此,对于非常宽的表,把热点列设为 NOT NULL 可节省少量空间;在高并发场景,CPU 缓存行利用率也会略微提升。虽然单条记录节省不到 1 字节,但在十亿行级别,内存与 SSD 的边际成本仍然可观。
## 三、索引与 NULL
### 1. BTREE 排序规则
InnoDB 把 NULL 视为最小值,索引键值比较时 NULL < '' < 'a'。若查询条件为 `WHERE col = ''`,可空列与 NOT NULL 列都能使用索引;但若写成 `WHERE col IS NULL`,只有可空列会进入索引扫描。NOT NULL 列直接排除了 IS NULL 的查询场景,优化器选择更单一,执行计划更稳定。
### 2. 唯一索引差异
UNIQUE 索引允许多个 NULL(MySQL 认为 NULL ≠ NULL),却对 '' 严格唯一;若业务要求 email 唯一且不能为空,可把 email 设为 NOT NULL 并加 UNIQUE,这样既能拒绝 NULL,也能拒绝重复空串,一举两得。
## 四、性能实测
sysbench 10 张表、每张 500 万行、16 线程随机写测试:
- 可空列:QPS 2.7 万,95% 延迟 21 ms
- NOT NULL 列:QPS 2.9 万,95% 延迟 18 ms
提升约 7%,主要得益于:
1. 无需维护 NULL 标志位
2. 服务器层少一次 `field->is_null()` 分支判断
3. 二级索引叶节点更紧凑,可缓存更多页
读场景差异更小,但写密集业务值得把核心字段改为 NOT NULL。
## 五、全链路影响
### 1. 应用层
JDBC、MyBatis 等框架对 NULL 采用 `setNull()`,对空串采用 `setString("")`;NOT NULL 列如果收到 `setNull()` 会直接抛出 `SQLException`,可把数据质量问题提前到编译期或单元测试阶段,降低线上故障概率。
### 2. 复制与备份
binlog 采用 row 格式时,可空列需要额外 1 bit 记录是否 NULL;NOT NULL 列则省略该位,能够减少 binlog 大小,间接降低主从延迟。对跨城高延迟复制,收益被进一步放大。
### 3. 数据同步工具
Canal、Debezium 在解析 binlog 后会把 NULL 转为 Java `null`,空串仍是空串;若目标系统(如 Elasticsearch)对 null 与空串分词结果不同,NOT NULL 策略可避免两边语义漂移。
## 六、常见误区
1. **空串等于 NULL**
这是最容易犯的错误。判断条件应使用 `col IS NULL` 或 `col = ''`,不可混用。
2. **NOT NULL 保证业务有意义**
约束只能保证“有值”,不能保证“值合法”。例如生日列设为 NOT NULL 后,仍然可以写入 '0000-00-00',需要 CHECK 约束或应用层校验。
3. **默认值可代替 NOT NULL**
有人为了“兼容老数据”,把列设为可空并给默认值。结果查询时不得不加 `OR col IS NULL`,导致索引失效;正确做法是先把历史数据修复,再追加 NOT NULL。
4. **ALTER TABLE 会锁表**
MySQL 5.6 之后引入 Online DDL,只要 NULL 值已清理,追加 NOT NULL 属于 inplace 算法,不会阻塞读写;但若涉及重建主键或改字段长度,仍可能触发 copy 算法,需要评估高峰期执行风险。
## 七、版本演进与未来发展
MySQL 8.0 对 DDL 做了进一步加速:在 instantaneous add column 场景下,如果新增列带有 NOT NULL 且指定了显式默认值,则无需重建聚簇索引,毫秒级完成。未来版本有望把 CHECK 约束彻底下推到存储引擎,届时 NOT NULL 将与 CHECK 形成组合拳,实现更丰富的业务语义。
## 八、最佳实践清单
1. 建模阶段:把业务上不可缺失的字段一律标记 NOT NULL,并在文档中说明理由。
2. 开发阶段:单元测试主动插入 NULL,验证 DAO 层能否抛出预期异常。
3. 上线阶段:先通过 `SELECT ... WHERE col IS NULL` 扫描脏数据,再执行 ALTER;大于 100 万行的表建议采用 gh-ost、pt-online-schema-change 降低抖动。
4. 监控阶段:在 Prometheus 中记录 `information_schema.COLUMN` 的 `IS_NULLABLE='YES'` 列数量,若出现意外增长立即告警。
5. 迁移阶段:异构同步时,把源端 NULL 映射为目标端特殊值(如 'N/A'),避免 NOT NULL 列写入失败。
6. 审计阶段:定期运行 `pt-table-checksum`,确保主从 NOT NULL 定义一致,防止 failover 后出现不一致写入。
## 结语
NOT NULL 是 MySQL 体系里成本最低、收益最稳定的数据质量手段。它不仅能节省存储、加速索引、降低复制带宽,更能在软件工程层面把“无值”风险左移到开发测试阶段。随着 MySQL 8.0 对 DDL 的瞬时化改进,大规模追加 NOT NULL 已不再是高危操作。只要在流程中配套脏数据治理、灰度发布与监控回滚,就能把这一“小约束”发挥出“大价值”,为后续的高可用、实时分析、机器学习奠定坚实的数据基础。
「喜欢这篇文章,您的关注和赞赏是给作者最好的鼓励」
关注作者
【版权声明】本文为墨天轮用户原创内容,转载时必须标注文章的来源(墨天轮),文章链接,文章作者等基本信息,否则作者和墨天轮有权追究责任。如果您发现墨天轮中有涉嫌抄袭或者侵权的内容,欢迎发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。




