1. 先从一次线上事故说起
我接手过一个用PHP+MySQL写的旧项目,某个深夜告警群突然炸了,大量订单插入失败,错误日志里全是同一行:
SQLSTATE[HY000]: General error: 1364 Field 'remark' doesn't have a default value乍看是业务代码少传了一个字段,但诡异的是这个remark字段已经存在好几年了,以前从没报过错。我记忆中第一次在真实项目里遇到这个报错,是在一次数据库从5.5升到5.7之后。从那以后,我才开始真正去了解MySQL的严格模式和非空字段之间的那点事。
这个报错翻译过来很直白:某张表的某个字段不允许为NULL,但同时没有默认值,而你往这张表插入数据时又没给它赋值,MySQL在严格的sql_mode下就直接把这条语句打回去了。
这篇文章就围绕这个经典报错来聊,我会把从现象到根因、从排查到修复的完整路径都走一遍。不管你是刚接触MySQL的新手,还是运维过线上库的老手,遇到类似问题都能照着这个思路快速定位,而不是靠猜。
2. 根因拆解:MySQL为什么拒绝写入
2.1 非空字段、NULL和默认值三者的关系
在MySQL建表时,一个字段可以有的属性组合其实不少:
- 允许NULL,不写默认值
- 允许NULL,有默认值
- 不允许NULL,有默认值
- 不允许NULL,且没有默认值
其中最容易踩坑的就是最后一种。很多人建表时习惯写not null但又不给default,理由是“反正程序里都会传值”。这个习惯在严格模式关闭的MySQL里问题不大,因为MySQL会悄悄把字段写成隐式默认值,比如数值类型默认0、字符串类型默认空字符串。
但一旦开启严格模式,MySQL的态度就变了。你插入的数据如果没包含这个非空且无默认值的字段,那就不是“帮你填个空值”的问题,而是直接判定为SQL语句非法,抛出1364错误码。
2.2 严格模式(sql_mode)是关键的开关
MySQL的sql_mode里有两个参数和这个报错直接相关:STRICT_TRANS_TABLES和STRICT_ALL_TABLES。
它们的区别在于适用范围:
STRICT_TRANS_TABLES:只对支持事务的表(InnoDB)启用严格模式,遇到数据问题会直接报错并回滚。STRICT_ALL_TABLES:对所有存储引擎都启用严格模式,即使是非事务表也会报错。
在我遇到的那个旧项目里,数据库连接用的是root账号,MySQL版本是5.6,默认sql_mode是空字符串,所以之前一直没报错。等升级到5.7以后,默认sql_mode变成了包含STRICT_TRANS_TABLES的一长串,问题就瞬间爆发了。
注意:MySQL 5.7及以上版本在Linux下默认sql_mode包含
ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION。很多老项目升级后出现各种异常,根源都是这个默认值的变化。
2.3 版本差异和安装方式带来的隐藏坑
不同发行版、不同安装方式,默认的sql_mode也不完全相同。
比如用源码编译安装的MySQL 5.6,默认sql_mode是空字符串,也就是非严格模式;而用官方RPM包安装的MySQL 5.6,默认可能是STRICT_TRANS_TABLES。这就解释了为什么同样的建表语句,在你本机跑得好好的,一到服务器上就报1364。
另外,Docker镜像里的MySQL也值得多看一眼。很多官方镜像启动时会挂载一个/etc/mysql/conf.d目录,如果你在自定义配置文件里修改了sql_mode,但没注意挂载顺序,可能改的根本不是MySQL读取的那个文件。
我在踩坑之后养成了一个习惯:不管在哪里部署MySQL,装完第一件事就是执行SHOW VARIABLES LIKE 'sql_mode';。这个习惯帮我避掉了很多后续的麻烦。
2.4 报错信息里的“XXX”怎么读
报错中的Field 'XXX' doesn't have a default value里的字段名是MySQL根据实际执行的INSERT或UPDATE语句解析出来的。它可能有几个来源:
- INSERT语句里没有包含该字段。
- INSERT语句里包含了该字段,但赋的值是
DEFAULT关键字,而字段本身又没有默认值。 - UPDATE语句把字段更新为
DEFAULT,但字段没有默认值。 - 存储过程、触发器内部执行的INSERT语句没有给字段赋值。
所以拿到报错后,第一步不是改代码,而是先确认这个字段到底在哪个语句里被漏掉了。
3. 排查路径:三步定位问题来源
3.1 第一步:确认表结构和报错字段的真实定义
无论报错来自哪个环境,我都会先看表结构:
SHOW CREATE TABLE your_table_name;重点看报错字段是否有NOT NULL,是否有DEFAULT。举个例子:
CREATE TABLE `orders` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `order_no` varchar(32) NOT NULL, `remark` varchar(255) NOT NULL, `created_at` datetime NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB;如果remark字段既没有DEFAULT,又带NOT NULL,那它在严格模式下就是个“隐形炸弹”。你只要写一条不带remark的INSERT,立刻就会炸。
3.2 第二步:查看当前会话和全局的sql_mode
确认字段定义后,再看当前MySQL的严格模式状态:
SHOW VARIABLES LIKE 'sql_mode';如果想看全局和会话分离的情况,还可以查:
SELECT @@GLOBAL.sql_mode; SELECT @@SESSION.sql_mode;如果结果里包含STRICT_TRANS_TABLES或STRICT_ALL_TABLES,那么这个报错就在理论上成立。如果两条结果都不包含严格模式,那问题可能不在MySQL服务端,而是在框架层自己做了字段校验。
注意:修改会话级sql_mode只对当前连接有效,要彻底修改需要在MySQL配置文件里调整,或者用
SET GLOBAL后再让新连接生效。
3.3 第三步:顺着SQL调用链找到漏字段的代码
表结构确认是“非空无默认值”后,就得去代码里找漏掉的地方了。我通常分三步:
- 打开MySQL通用日志,定位出错的那条完整SQL。
- 把SQL拿到测试库执行,看是否复现。
- 在代码里搜索INSERT、REPLACE、UPDATE、存储过程调用等所有可能写这张表的地方。
通用日志的开启方式(慎用于生产环境):
SET GLOBAL general_log = 'ON'; SET GLOBAL log_output = 'TABLE';然后查询mysql.general_log表就能看到实际执行的SQL。日志开完记得关掉,否则磁盘会疯涨。
3.4 隐藏触发点:触发器、存储过程和批量导入
排查时最容易被忽略的是触发器。假设orders表上有一个BEFORE INSERT触发器,内部会往order_log表插一条记录,但order_log表的某个非空字段没有默认值,那即使你的主INSERT语句字段写全了,依然会报错,而且报错信息里指向的字段可能是另一个表的字段。
存储过程同理。比如一个存储过程先插入主表,再循环插入明细表,明细表有个字段漏了,报错时指向的字段就是明细表里的那个。只看主流程的代码,很容易找不到头绪。
批量导入场景也很常见。用LOAD DATA INFILE或mysqldump导入时,如果表结构和数据不完全匹配,也会触发这个错误。特别是从5.6导出、再导入到5.7的库,因为两边sql_mode不同,导入时会突然冒出大量1364。
4. 修复方案:从正规到临时的完整解法
4.1 方案A:修改SQL或程序代码,显式提供字段值
这是最“正”的修法。既然字段是非空且无默认值,那就让每次写入都带这个字段值。针对文章开头的案例,代码里补上remark字段即可:
INSERT INTO orders (order_no, remark, created_at) VALUES ('NO2025001', '用户备注', NOW());如果是ORM框架,比如MyBatis、Hibernate,需要检查实体类字段是否映射完整。MyBatis里的insert语句如果是动态SQL,注意<if test="remark != null">这种条件片段会导致字段缺失,需要改成显式赋默认值。
这种方法的好处是不改变表结构,不触碰生产数据,风险最低。缺点是如果漏字段的地方非常多,代码改动量会比较可观。
4.2 方案B:为字段设置默认值
如果业务上这个字段其实大多数时候都用不到,也可以给字段加一个默认值。例如:
ALTER TABLE orders ALTER COLUMN remark SET DEFAULT '';这样即使INSERT语句不包含remark,MySQL也会自动填一个空字符串。
但这里有个细节:ALTER TABLE ... ALTER COLUMN ... SET DEFAULT只会影响之后的新数据,对已有数据不会做任何填充。如果这个字段之前本来就是NOT NULL并且已存在历史数据,那历史数据的值早就有了,不用担心;如果是新加的非空字段,实施前必须先把已有行的值补上。
在MySQL 8.0里,给已有字段设置默认值可以使用:
ALTER TABLE orders MODIFY COLUMN remark varchar(255) NOT NULL DEFAULT '';这个语句会同时修改字段属性和默认值,代价是可能重建表。对于千万级的大表,建议用工具或者放在低峰期操作。
4.3 方案C:把字段改成允许NULL
如果业务逻辑允许空值,直接把字段改成允许NULL也可以:
ALTER TABLE orders MODIFY COLUMN remark varchar(255) NULL;改完之后,代码里不传这个字段时,MySQL会写入NULL。但这会带来一个潜在问题:应用里如果对NULL判断不严,可能出现界面显示空白、统计COUNT不准确等连锁反应。所以这个方案最好配合代码review一起做。
4.4 方案D:关闭严格模式(强烈不建议)
有些老项目为了快速止血,会直接修改sql_mode,把STRICT_TRANS_TABLES去掉,恢复成宽泛模式。操作方式是在my.cnf里:
[mysqld] sql_mode="ONLY_FULL_GROUP_BY,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION"注意这里去掉了STRICT_TRANS_TABLES和STRICT_ALL_TABLES。
这个方案我一般只建议作为临时止血,不建议长期使用。因为严格模式除了拦截非空字段缺值,还会拦截其他很多数据问题,比如日期值非法、数值溢出、除数为零。一旦关闭,坏数据会悄悄写进库,后面排查数据质量问题时更痛苦。
注意:修改配置文件后需要重启MySQL服务才能生效,重启前一定要备份配置,并测试sql_mode写法是否正确。如果写错,MySQL可能直接起不来。
4.5 实战演练:一个完整修复案例
假设我们有一张users表:
CREATE TABLE `users` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(50) NOT NULL, `age` int(11) NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB;执行插入:
INSERT INTO users (name) VALUES ('张三');在严格模式下会立即报错:
ERROR 1364 (HY000): Field 'age' doesn't have a default value因为age字段是NOT NULL,且没有默认值,而INSERT语句里没提供age。
修复步骤:
- 先确认业务是否允许age为空。如果允许,修改字段:
ALTER TABLE users MODIFY COLUMN age int(11) NULL;- 如果age必须存在,但普通用户注册时可以给一个默认年龄,比如0:
ALTER TABLE users MODIFY COLUMN age int(11) NOT NULL DEFAULT 0;- 如果业务坚决不允许默认值,那就在INSERT语句显式传入age:
INSERT INTO users (name, age) VALUES ('张三', 25);改完之后,最好再把测试环境的数据跑一遍,确认没有其他隐藏报错。
5. 高频场景与避坑清单
5.1 高频触发场景汇总
我在处理这类问题时,整理了几个最常见的触发场景:
| 场景 | 产生原因 | 影响 |
|---|---|---|
| 老项目升级MySQL 5.7+ | sql_mode默认值变化,严格模式默认开启 | 原可写数据全部拦截 |
| 新表设计不严谨 | 非空字段忘记设置default | 开发环境偶尔报错 |
| ORM映射字段不全 | insert语句不包含全量字段 | 接口偶发报错 |
| 批量导入旧数据 | 源库宽松模式,目标库严格模式 | 导入任务中断 |
| 触发器/存储过程内嵌写表 | 内部SQL漏字段 | 主流程报错,但定位困难 |
| 程序依赖MySQL隐式默认值 | 业务代码从不传该字段 | 线上突发大量报错 |
5.2 避免“一刀切”式修复
很多人排查这种问题时,会在网上找到“关闭严格模式”的答案,然后照抄。但我不建议这么做。
关闭严格模式最直接的后果是MySQL会把缺字段的值写成隐式默认值。数值类型填0,字符串类型填空字符串,日期和时间类型填“零值”。这些值在业务上很难分辨是“用户真的填了0”还是“系统漏传了”。等到做报表、做数据审计时,你根本无法判断数据质量。
另一个后果是,MySQL 5.7之后的版本在不断强化数据规范,如果你把严格模式关掉,后续升级到8.0时可能会遇到更多兼容性问题。还不如趁现在把表结构和代码一起收拾干净。
5.3 上线前如何预防这类错误
预防比处理更重要。我现在的习惯是:
- 新表的所有非空字段,除了自增主键,一律设置默认值,即使默认值是空字符串。
- 写完建表语句后,先用
SHOW CREATE TABLE检查一遍。 - 开发环境和生产环境的sql_mode保持一致。
- 数据库变更上线前,先在测试环境开严格模式跑一遍回归用例。
- 建表脚本里用带
IF NOT EXISTS的语句,防止重复执行。
这些习惯看起来不起眼,但能省掉很多半夜查错的痛苦。
5.4 大表改字段时的注意事项
如果线上表数据量很大,直接执行ALTER TABLE ... MODIFY COLUMN会锁表,影响业务。常见的处理方式是用pt-online-schema-change,或者MySQL 8.0自带的ALGORITHM=INPLACE。不过即使是这样,也要评估IO和主从延迟。
举个实际例子:我处理过一个2亿行的订单表,给一个非空字段加默认值。直接ALTER的话,预计锁表超过3小时,业务直接瘫痪。后来用pt-online-schema-change在凌晨跑,总共花了50分钟,期间业务只出现几次小抖动,主从延迟最高10秒,可控。
如果用的MySQL 8.0,可以尝试:
ALTER TABLE orders ALTER COLUMN remark SET DEFAULT '', ALGORITHM=INPLACE, LOCK=NONE;但要注意,ALTER COLUMN ... SET DEFAULT在MySQL 8.0里是元数据操作,速度很快;如果只是改默认值,不建议用MODIFY COLUMN,因为它会重建表。
6. 排查这类报错的经验心得
最后分享一点个人体会。
处理Field 'XXX' doesn't have a default value时,我踩过几个印象很深的坑。第一次是改完sql_mode后忘记给已有连接刷新会话,结果还是报错,因为已有的连接没重新获取全局变量。后来我改用修改配置文件的方式,才彻底解决。
第二次是写修复SQL时,用了ALTER TABLE ... MODIFY COLUMN给大表加默认值,结果表和业务都卡住了,最后只能回滚。这件事之后,我给自己定了一条规矩:凡是线上大表的表结构变更,一律先看数据量,再看是否需要工具辅助,绝不在业务高峰期直接改。
还有一次,报错字段指向的是id,我当时愣住了——自增主键怎么还会缺默认值?查了半天才发现,那条INSERT语句是显式指定了id为NULL,而自增列在严格模式下不允许NULL。也就是说,Field 'id' doesn't have a default value这种情况本质上是往自增列里塞了NULL,和普通非空字段缺值不是一回事。这个问题单独列出来,是想提醒大家:报错里的字段名虽然一样,但背后可能对应不同的数据场景,一定要结合具体SQL来看。
如果你现在正被这个报错困扰,我的建议是先别急着改表结构或关闭严格模式,花十分钟把表结构、sql_mode、调用链这三样东西查清楚,大概率能在半小时内找到根因。数据库层面的问题,多数不是靠删字段或改配置解决的,而是靠把表结构和数据流梳理清楚解决的。