
1. 这个问题到底是怎么冒出来的先直接说结论1138 - Invalid use of NULL value这个报错十有八九是你在对一张已有数据的表做ALTER TABLE修改列属性时想把某列改成NOT NULL但这列里面已经躺着 NULL 值了。MySQL 一看你要“上锁”可库里还有一堆空值直接给你甩脸子。我举个例子你就明白了。假设业务表users里有个phone字段早期开发时没管那么严允许为空后来业务调整要求每个用户必须绑定手机号你高高兴兴执行ALTER TABLE users MODIFY COLUMN phone VARCHAR(20) NOT NULL;结果啪一下报错ERROR 1138 (22003): Invalid use of NULL value很多人第一反应是“MySQL 出 bug 了”其实不是。这是 MySQL 在做约束变更前的一致性检查发现phone列中至少有一行的值是NULL如果强行改成NOT NULL数据就会违反新约束于是拒绝执行。别觉得这是小事在我接触过的生产环境故障里这种报错引发过线上服务不可用的案例一点都不少。尤其是一些老系统字段当初设计得很随意等过了几年业务方提需求要收紧约束一跑变更脚本就挂。今天这篇文章我就把这个问题彻底拆开原理、排查、解决方案、踩坑经验一次讲透希望能帮你在下次遇到时少走弯路。2. 为什么 MySQL 会拦截这次 ALTER 操作2.1 约束变更的本质是一次“安全性检查”要理解这个报错得先明白 MySQL 执行ALTER TABLE ... MODIFY COLUMN ... NOT NULL时内部到底在做什么。表面上你只是“改个列属性”但实际上 MySQL 需要逐行校验现有数据是否满足新约束。如果待修改列中存在NULL那么NOT NULL约束必然冲突MySQL 直接中断操作并返回错误1138。注意这个行为不是 MySQL 8.0 才有的在 5.5、5.6、5.7 等版本中同样存在。它本质上是数据库为防止“约束变更后数据非法”而做的前置校验。2.2 哪些操作会触发 1138不是只有MODIFY COLUMN会触发凡是涉及“添加或收紧 NULL 约束”的操作都可能有这个问题ALTER TABLE ... MODIFY COLUMN ... NOT NULL最典型。ALTER TABLE ... CHANGE COLUMN ... NOT NULL换列名或类型的同时加非空约束。新建表时直接定义NOT NULL通常没问题因为新表还没数据。通过某些 ORM 工具同步表结构时如果它生成的 DDL 是MODIFY同样会触发。我做过的运维工单里最常见的场景有两种一种是老表加字段后补数据历史记录里存在 NULL另一种是把外键关联列改为非空但某些“孤儿数据”关联不到主表那列就是 NULL。2.3 NULL 和空字符串不是一回事这里必须强调一个基础概念NULL 表示“未定义/不知道”空字符串是“有值只是空”。在 SQL 的世界里NULL参与任何比较运算都会得到UNKNOWN而空字符串参与比较是正常的“相等”判断。所以如果你的列里既有NULL又有空字符串在处理的时候千万要区分开。后面写修复 SQL 时什么时候用IS NULL什么时候用 逻辑要非常清楚否则一步错步步错。3. 解决之前先按这套思路排查数据3.1 第一步定位哪张表的哪一列有 NULL既然报错已经指名道姓但问题是你可能在批量执行多个建表/改表语句不一定马上知道是哪一列。这时候先定位问题列再排查数据。比如说我们要查users表phone列中 NULL 的数量SELECT COUNT(*) AS null_count FROM users WHERE phone IS NULL;如果返回结果大于 0那问题就出在这里。如果返回 0但依然报错就要查一下是不是其他列比如刚刚新增的score、nickname列存在 NULL。3.2 第二步确认要改成 NOT NULL 的列都有哪些拿我最近处理过的一个实际工单举例原始故障表结构是CREATE TABLE order_info ( id INT NOT NULL AUTO_INCREMENT, order_no VARCHAR(64) DEFAULT NULL, user_id INT DEFAULT NULL, coupon_id INT DEFAULT NULL, created_at DATETIME DEFAULT NULL, PRIMARY KEY (id) ) ENGINEInnoDB;业务方要求把user_id和order_no都改成NOT NULL并加上默认值。执行ALTER TABLE order_info MODIFY COLUMN order_no VARCHAR(64) NOT NULL, MODIFY COLUMN user_id INT NOT NULL;一下子报了 1138。清点一下发现order_no列有 127 行 NULLuser_id列有 3 行 NULL。问题被准确定位。3.3 第三步逐个或批量查看 NULL 数据的样板对于有 NULL 的行建议先抽样看看这些行的整体情况再决定如何填充。直接更新全部 NULL 为固定值可能会有副作用。比如把user_id全改成 0那后续关联用户表会查出不存在用户把order_no全改成默认UNKNOWN可能影响订单号唯一性索引。所以我习惯先查一下这些 NULL 行的完整记录SELECT * FROM order_info WHERE order_no IS NULL LIMIT 50; SELECT * FROM order_info WHERE user_id IS NULL LIMIT 50;看清楚这些记录是不是本身就是异常数据。如果是垃圾数据该清理清理如果是有效历史数据再决定填充值。4. 解决方案从“暴力”到“安全”的全套实操4.1 方案一直接填充默认值后修改新手最常用如果你的业务允许为这个列设置一个统一的默认值比如把phone为空的统一补上0或UNKNOWN那么直接 UPDATE 再 ALTER 是最快的UPDATE users SET phone 0 WHERE phone IS NULL; ALTER TABLE users MODIFY COLUMN phone VARCHAR(20) NOT NULL DEFAULT 0;优缺点都很明显。优点是简单直接一两行 SQL 就能解决缺点是会改变数据语义尤其当 NULL 表示“没有手机号”时强行改成0可能污染数据质量。这个方案适合测试环境、临时修数据或者业务上确实能接受某种约定俗成的填充值。4.2 方案二为 NULL 填充业务上有意义的默认值生产环境首选我强烈建议在生产环境用这个思路。还是拿order_info表来说user_id出现 NULL可以先看这些订单是否是“游客下单”。如果是业务上就应该填充0来表示“匿名用户”而不是随便填个别人的 ID。order_no出现 NULL那可能是老系统的一段历史数据如果当时要求不严格可以填充一个规则外的特殊编号比如LEGACY- id这样既保持唯一性语义又不会和现有订单号冲突。UPDATE order_info SET user_id 0 WHERE user_id IS NULL; UPDATE order_info SET order_no CONCAT(LEGACY-, id) WHERE order_no IS NULL; ALTER TABLE order_info MODIFY COLUMN order_no VARCHAR(64) NOT NULL, MODIFY COLUMN user_id INT NOT NULL DEFAULT 0;这里有个细节如果order_no有唯一索引且里面已经有LEGACY-1之类的值再次执行可能会冲突。所以填充值设计要结合现有数据分布确保不撞车。4.3 方案三新建表 数据迁移大表/可回滚场景对于几百 GB 的大表直接在原表上ALTER会锁表很久容易被 DBA 盯上。更稳妥的做法是“另起炉灶”CREATE TABLE order_info_new ( id INT NOT NULL AUTO_INCREMENT, order_no VARCHAR(64) NOT NULL, user_id INT NOT NULL DEFAULT 0, coupon_id INT DEFAULT NULL, created_at DATETIME DEFAULT NULL, PRIMARY KEY (id) ) ENGINEInnoDB; INSERT INTO order_info_new (id, order_no, user_id, coupon_id, created_at) SELECT id, COALESCE(order_no, CONCAT(LEGACY-, id)), COALESCE(user_id, 0), coupon_id, created_at FROM order_info; RENAME TABLE order_info TO order_info_old, order_info_new TO order_info;这个方案好处是新表结构完全可控不会受历史脏数据阻碍。INSERT ... SELECT过程中可以灵活处理 NULL 值COALESCE函数非常适合这种场景。最后用两个RENAME原子切换表名业务几乎无感。不过要注意RENAME TABLE不能在有外键引用该表时随意执行而且如果业务表有触发器、视图、存储过程依赖旧表名切换后可能需要一并处理。我建议先小范围测试确认无副作用后再上生产。4.4 方案四修改列的默认值 添加 GENERATED ALWAYS 列复杂场景的补充有一种更“不讲武德”的玩法适合你不想动已有数据、但新数据必须非空的情况。比如原列phone保持可空但新增一个生成列phone_not_null来强制非空满足业务方的“展示需求”而不动底层历史数据ALTER TABLE users ADD COLUMN phone_not_null VARCHAR(20) GENERATED ALWAYS AS (COALESCE(phone, 0)) STORED, ADD INDEX idx_phone_not_null (phone_not_null);这个方案相对少见但有时候业务方只是需要一个“永远非空”的字段来跑报表不想背负清洗历史数据的包袱那就用生成列。它的缺点是占用额外存储空间对已有表来说加列也需要重建表所以还是要谨慎。4.5 方案五使用工具完成在线 DDLPT-OSC适合超大型表如果表非常大MySQL 自带的ALGORITHMINPLACE也可能力不从心生产上常见方案是用 Percona Toolkit 的pt-online-schema-change。它能通过创建触发器 临时表的方式把结构变更对线上读写的影响降到最低。pt-online-schema-change \ Dyour_db,tusers \ --alter MODIFY COLUMN phone VARCHAR(20) NOT NULL DEFAULT 0 \ --no-drop-old-table \ --max-load Threads_running50 \ --chunk-size 1000 \ --execute在执行前工具也会做数据一致性检查如果发现 NULL 值会报错所以还是得先把 NULL 数据处理好。这个方案适合 DBA 或有一定基础的朋友使用新手不建议一上来就碰容易在触发器、外键、锁等待上翻车。5. 避坑指南这几个隐蔽的坑我已经替你踩过了5.1 别忽略复合索引和唯一索引带来的连锁问题我曾经在一个user_coupon表上执行MODIFY coupon_id INT NOT NULL结果报 1138。检查后发现coupon_id与user_id一起组成一个唯一索引。如果我把 NULL 都更新成一个固定值可能导致原本“一个用户多个 NULL coupon”的记录全部违反唯一性。这时候不能盲目 UPDATE要先看重复情况SELECT user_id, coupon_id, COUNT(*) FROM user_coupon WHERE coupon_id IS NULL OR coupon_id 0 GROUP BY user_id, coupon_id HAVING COUNT(*) 1;如果重复了得先想好去重策略再执行 ALTER。5.2 5.7 与 8.0 的默认行为有差异MySQL 5.7 中ALTER TABLE ... MODIFY COLUMN默认可能使用COPY算法重建表。而 8.0 中很多操作支持INPLACE速度快很多。但这不代表 8.0 会跳过 NULL 校验。不管哪个版本只要列里有 NULL报错照旧。建议 DDL 操作前先确认版本SELECT VERSION();如果涉及大数据量变更尽量选择业务低峰期执行并观察SHOW PROCESSLIST里是否有长事务阻塞。5.3 处理 NULL 时注意隐式类型转换比如一个INT列你用UPDATE ... SET coupon_id WHERE coupon_id IS NULLMySQL 可能会把空字符串转成 0这看起来没问题但如果是字符串列空字符串和 NULL 对业务来说完全是两回事这个细节要特别留意。还有一种更隐蔽的情况表里存的 NULL 是用程序写入的null字符串对就是字符串null你用IS NULL查不到用 null才能查到。这种脏数据一定要先清洗干净再改约束。5.4 外键列改 NOT NULL 要回查父表和子表有次我把order_info.user_id改成NOT NULL只关注了order_info本身的 NULL结果忽略了user表中有几条被物理删除的用户记录而order_info.user_id因为外键约束直接关联user.id。虽然 ALTER 没报 1138但后续插入新订单时如果传入了不存在的user_id直接报外键错误。所以遇到外键列修改约束时不仅要看当前表的 NULL还要检查关联表的数据完整性。5.5 事务中的 DDL 要小心MySQL 中 DDL 语句会导致隐式提交。如果你在事务里执行 UPDATE 清理 NULL再执行 ALTERALTER 触发隐式提交后之前的事务回滚就没用了。这算是个经验教训要么先提交 UPDATE确认结果后再单独执行 ALTER要么把整个流程拆成两个明确步骤避免混淆。6. 日常运维中的常见问题速查常见场景报错信息快速解法备注列内有 NULL直接 MODIFY NOT NULLERROR 1138 (22003)先 UPDATE 填充再 ALTER填充值要符合业务语义一个大表执行 ALTER 卡了很久才报 1138同上用SELECT ... WHERE 列 IS NULL提前预检先定位再操作ALTER 成功后应用程序仍写入 NULL不报 1138但业务异常检查 ORM 映射和接口参数校验补充 NOT NULL 约束数据库只是最后防线唯一索引列同时存在 NULL 和重复填充值唯一键冲突分组去重后再更新先 SELECT 分析再动手外键列有 NULL且关联主表无对应数据FK 校验失败先确定填充策略更新为合理的“空客户”ID外键列要特别谨慎执行 ALTER 中途客户端断开锁等待或元数据锁用SHOW PROCESSLIST查看状态必要时 KILL 对应线程评估业务影响这个表是我多年运维经验的浓缩你拿着它对照场景能快速定位问题不至于一上来手足无措。7. 预防 1138 的 4 个实操习惯7.1 建表时就定好约束最简单的方式就是源头控制。新表设计阶段字段能加NOT NULL就加不能加的也要加默认值。CREATE TABLE user_profile ( id INT NOT NULL AUTO_INCREMENT, nickname VARCHAR(50) NOT NULL DEFAULT , phone VARCHAR(20) NOT NULL DEFAULT , PRIMARY KEY (id) ) ENGINEInnoDB;这样后续想改结构就不会冒出一堆历史 NULL 来阻碍你。7.2 发布前自动扫描“即将改动列”的 NULL 比例我有一套简单的巡检脚本逻辑根据即将执行的 DDL 解析出ALTER涉及的列自动生成SELECT COUNT(*) ... WHERE 列 IS NULL的预检 SQL。一旦扫描到 NULL 行数大于 0就阻断上线。这个习惯尤其适合有规范化发布流程的团队。哪怕没有自动化工具开发同学自己在提工单前跑一条SELECT也比上了生产再回滚成本低得多。7.3 为可空列设定“业务空值”默认值有时候无法避免 NULL但可以约定一个业务上的“假空值”。比如用户性别可以默认0表示未知手机号可以默认表示没有。需要注意的是字符串列的和NULL毕竟有区别查询统计时要用COALESCE或统一过滤逻辑。这种方法特别适合需要经常加索引或唯一约束的列因为 NULL 在唯一索引中允许多行共存业务判断容易混乱。7.4 对 DDL 变更设置分级审批大表变更、核心表变更、外键列变更都建议单独走审批流程。不是说不让改而是要让改的人想清楚“遇到 NULL 怎么办、生产是否低峰期、是否需要在线工具”这些问题。很多时候 1138 只是表象真正危险的是你为了绕过它随手SET phone0把业务逻辑给写坏了。8. 聊聊个人经验像 1138 这种错误看着只是一个小报错但背后牵扯的是数据仓库的规范化治理说大不大说小不小。我在处理这类 DDL 变更时最长的一次花了三个多小时不是光跑一条 ALTER 就完事而是把相关表的数据分布、索引约束、应用代码的写入逻辑全部梳理了一遍才敢动手。有一句话我想分享给刚入行的同学数据库报错永远是最好的老师但它教你的不是“这个命令怎么改”而是“你的数据设计在哪个环节埋了雷”。遇见 1138别急着百度一条 SQL 糊过去先停下来看看数据想想业务再决定怎么修。这个过程反复几次你对 NULL、约束、索引和数据质量的理解会比看十篇教程都深刻。