
1. 先从一次线上事故说起我接手过一个用PHPMySQL写的旧项目某个深夜告警群突然炸了大量订单插入失败错误日志里全是同一行SQLSTATE[HY000]: General error: 1364 Field remark doesnt 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 doesnt 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) ) ENGINEInnoDB;如果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 testremark ! null这种条件片段会导致字段缺失需要改成显式赋默认值。这种方法的好处是不改变表结构不触碰生产数据风险最低。缺点是如果漏字段的地方非常多代码改动量会比较可观。4.2 方案B为字段设置默认值如果业务上这个字段其实大多数时候都用不到也可以给字段加一个默认值。例如ALTER TABLE orders ALTER COLUMN remark SET DEFAULT ;这样即使INSERT语句不包含remarkMySQL也会自动填一个空字符串。但这里有个细节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_modeONLY_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) ) ENGINEInnoDB;执行插入INSERT INTO users (name) VALUES (张三);在严格模式下会立即报错ERROR 1364 (HY000): Field age doesnt have a default value因为age字段是NOT NULL且没有默认值而INSERT语句里没提供age。修复步骤先确认业务是否允许age为空。如果允许修改字段ALTER TABLE users MODIFY COLUMN age int(11) NULL;如果age必须存在但普通用户注册时可以给一个默认年龄比如0ALTER TABLE users MODIFY COLUMN age int(11) NOT NULL DEFAULT 0;如果业务坚决不允许默认值那就在INSERT语句显式传入ageINSERT INTO users (name, age) VALUES (张三, 25);改完之后最好再把测试环境的数据跑一遍确认没有其他隐藏报错。5. 高频场景与避坑清单5.1 高频触发场景汇总我在处理这类问题时整理了几个最常见的触发场景场景产生原因影响老项目升级MySQL 5.7sql_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自带的ALGORITHMINPLACE。不过即使是这样也要评估IO和主从延迟。举个实际例子我处理过一个2亿行的订单表给一个非空字段加默认值。直接ALTER的话预计锁表超过3小时业务直接瘫痪。后来用pt-online-schema-change在凌晨跑总共花了50分钟期间业务只出现几次小抖动主从延迟最高10秒可控。如果用的MySQL 8.0可以尝试ALTER TABLE orders ALTER COLUMN remark SET DEFAULT , ALGORITHMINPLACE, LOCKNONE;但要注意ALTER COLUMN ... SET DEFAULT在MySQL 8.0里是元数据操作速度很快如果只是改默认值不建议用MODIFY COLUMN因为它会重建表。6. 排查这类报错的经验心得最后分享一点个人体会。处理Field XXX doesnt have a default value时我踩过几个印象很深的坑。第一次是改完sql_mode后忘记给已有连接刷新会话结果还是报错因为已有的连接没重新获取全局变量。后来我改用修改配置文件的方式才彻底解决。第二次是写修复SQL时用了ALTER TABLE ... MODIFY COLUMN给大表加默认值结果表和业务都卡住了最后只能回滚。这件事之后我给自己定了一条规矩凡是线上大表的表结构变更一律先看数据量再看是否需要工具辅助绝不在业务高峰期直接改。还有一次报错字段指向的是id我当时愣住了——自增主键怎么还会缺默认值查了半天才发现那条INSERT语句是显式指定了id为NULL而自增列在严格模式下不允许NULL。也就是说Field id doesnt have a default value这种情况本质上是往自增列里塞了NULL和普通非空字段缺值不是一回事。这个问题单独列出来是想提醒大家报错里的字段名虽然一样但背后可能对应不同的数据场景一定要结合具体SQL来看。如果你现在正被这个报错困扰我的建议是先别急着改表结构或关闭严格模式花十分钟把表结构、sql_mode、调用链这三样东西查清楚大概率能在半小时内找到根因。数据库层面的问题多数不是靠删字段或改配置解决的而是靠把表结构和数据流梳理清楚解决的。