MySQL报错1364:非空字段无默认值?一文搞懂严格模式与修复方案
2026/9/18 21:05:41 网站建设 项目流程

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_TABLESSTRICT_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_TABLESSTRICT_ALL_TABLES,那么这个报错就在理论上成立。如果两条结果都不包含严格模式,那问题可能不在MySQL服务端,而是在框架层自己做了字段校验。

注意:修改会话级sql_mode只对当前连接有效,要彻底修改需要在MySQL配置文件里调整,或者用SET GLOBAL后再让新连接生效。

3.3 第三步:顺着SQL调用链找到漏字段的代码

表结构确认是“非空无默认值”后,就得去代码里找漏掉的地方了。我通常分三步:

  1. 打开MySQL通用日志,定位出错的那条完整SQL。
  2. 把SQL拿到测试库执行,看是否复现。
  3. 在代码里搜索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_TABLESSTRICT_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。

修复步骤:

  1. 先确认业务是否允许age为空。如果允许,修改字段:
ALTER TABLE users MODIFY COLUMN age int(11) NULL;
  1. 如果age必须存在,但普通用户注册时可以给一个默认年龄,比如0:
ALTER TABLE users MODIFY COLUMN age int(11) NOT NULL DEFAULT 0;
  1. 如果业务坚决不允许默认值,那就在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、调用链这三样东西查清楚,大概率能在半小时内找到根因。数据库层面的问题,多数不是靠删字段或改配置解决的,而是靠把表结构和数据流梳理清楚解决的。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询