
我曾经在某个凌晨被线上告警吵醒业务同学反馈说有个订单创建接口一直报 500日志里刺眼的红字就是Field remark doesnt have a default value。当时的第一反应是代码没问题啊insert 语句明明没传 remark 字段但数据库偏偏不让过。后来翻了好一会儿才发现这不是代码的 bug而是 MySQL 的sql_mode在作祟。这种报错对新手来说特别容易懵因为在本地环境从来没见过一上测试或生产环境就冒出来。今天就把这个经典问题的完整排查思路、背后原理和几种解决方案拆开揉碎讲清楚希望能帮你少踩几个坑。1. 这个报错到底在说什么一场严格模式下的数据完整性拉锯战1.1 从报错文本看 PostgreSQL 在坚持什么先看这行报错的结构Field XXX doesnt have a default value翻译过来就是字段 XXX 没有默认值。但这句话其实是省略了前半句的完整的逻辑应该是在严格模式下你往表里插入了一条数据某个字段既没有在 insert 语句中被显式指定这个字段本身又没定义 default 值所以 MySQL 拒绝执行这条 SQL。MySQL 的默认行为在历史版本里其实是相当宽容的。比如在 5.5 及更早的版本里如果插入时缺了一个非 NULL 且无默认值的字段MySQL 不会直接报错而是会根据字段类型给一个隐式的默认值数字类型给 0字符串类型给空字符串日期时间类型给 0000-00-00然后默默地把数据写进去顶多给一条 warning。这种设计对开发者确实友好但对数据质量来说是个灾难因为脏数据往往就是在这种差不多得了的路径下产生的。5.7 版本之后MySQL 默认开启了STRICT_TRANS_TABLES从此走的是另一套逻辑只要事务表InnoDB上的 insert 或 update 语句出现了缺值或非法值直接报错回滚绝不迁就。我们遇到的这个报错就是这套严格模式机制下的典型代表所以它的真名其实叫严格模式 default 值缺失错误。1.2 触发这个报错的两个必要条件不是所有缺字段的 insert 都会报这个错它必须同时满足两个条件第一个条件数据库的sql_mode中包含了STRICT_TRANS_TABLES或者在 5.7 默认配置下直接就是全套严格模式。第二个条件目标表里存在某个字段属性是NOT NULL且没有显式DEFAULT值同时你的 insert 语句又恰好没给这个字段赋值。这里就容易出现一个理解偏差了。很多人以为没有默认值只是字面意思其实这里有个自增主键的特例如果字段是AUTO_INCREMENT就算你没写 default也不触发报错因为自增机制本身就是一种隐式默认值。真正的报错源头多半是那些业务字段——比如备注remark、扩展字段ext_info、状态字段status之类的建表时只写了NOT NULL没给默认值代码里 insert 时也漏了。所以这个报错的本质是开发者的insert 习惯和 DBA 的建表规范发生了冲突。你在本地 MySQL 5.6 或免安装版里跑得好好的环境一换到 5.7 或云数据库 RDS 上就翻车原因往往就在这里。2. sql_mode 机制拆解为什么 MySQL 要自找麻烦2.1 sql_mode 里到底装了什么sql_mode是 MySQL 的一个环境变量它决定了一组 SQL 语法校验规则和数据合法性规则。你可以把它理解为数据库的性格参数——每个实例都能有自己的脾气有些宽松有些严苛。5.7 版本默认的sql_mode大概是这样的ONLY_FULL_GROUP_BY, STRICT_TRANS_TABLES, NO_ZERO_IN_DATE, NO_ZERO_DATE, ERROR_FOR_DIVISION_BY_ZERO, NO_ENGINE_SUBSTITUTION8.0 版本在上一版基础上又加了NO_AUTO_CREATE_USER等条目但对我们这个报错来说最关键的就是STRICT_TRANS_TABLES这条。注意这个名字里有个TRANS指的是事务表也就是 InnoDB。对于 MyISAM 这种不支持事务的表严格模式会降级成STRICT_ALL_TABLES的处理逻辑——前者是遇到非法值就放弃整条语句对事务表而言后者则是尽量多插入些行能救一行是一行然后报错停止。2.2 严格模式改变的两个行为开启STRICT_TRANS_TABLES之后有两个行为变化最值得关注。一是数据类型的隐式转换不再被容忍。比如给int字段插入了abc这种字符串或者给decimal(10,2)插入了超出长度的数字旧版本可能截断或强转后写进去严格模式下直接报Incorrect integer value等错误。二是缺失值的处理变严格了。像我们标题里遇到的场景——字段没有默认值且 insert 中没有提供值——就直接拒绝执行。不光 insertupdate 的时候如果SET子句里把某个字段 set 成 NULL而字段是NOT NULL且没默认值同样会报错。你可以把sql_mode理解成一次高考监考的松紧程度宽松模式下学生没写答题卡上的姓名监考老师帮你补上严格模式下没写姓名直接按零分处理。严格模式确实会带来误伤但长期看它能逼着开发者在代码层面把数据语义表达清楚避免线上出现一堆来路不明的 0 和空字符串。2.3 为什么云数据库和 Docker 镜像更爱报这个错很多读者会问为什么本地从来没遇到过一上云或者一拉 Docker 镜像就爆因为 MySQL 官方 5.7 的默认配置文件my.cnf或my.ini里实际是注释掉了sql_mode这个配置项也就是说此时 MySQL 使用编译期的默认值。而云厂商的 RDS 和许多精心维护的 Docker 镜像会主动把sql_mode设置成更严格的全套模式级配置以对齐官方 5.7 推荐标准。你本地如果用的是老版本 MySQL 或者集成环境自带配置很可能sql_mode是空的或者只有一个NO_ENGINE_SUBSTITUTION自然看不到这个报错。另外还有一个特殊场景从 5.6 原地升级或者用 mysqldump 迁移到 5.7/8.0旧库里那些故意不写默认值的表全部存活但新实例的严格模式一开业务一写数据就炸。这类问题在迁移后的一个月内特别集中我见过好几个团队就是被这个报错逼着做了一次全库字段默认值体检。3. 完整排查链路从日志定位到根源确认的五个步骤3.1 第一步确认报错发生在哪一层遇到这类报错先不要急着改代码先确认报错来自 MySQL 服务端还是连接中间件。方法是直接看应用日志里的完整异常堆栈如果里面有SQLException和MysqlErrorNumbers相关类名基本就能确定是服务端返回的错误。如果报错发生在事务提交阶段并且在日志里能看到rollback字样那说明你的事务已经被 MySQL 主动回滚了。我习惯的做法是先拿着完整的报错信息去 MySQL 命令行里手动复现一次mysql INSERT INTO t_order (order_no, user_id, amount) VALUES (A001, 1001, 99.50); ERROR 1364 (HY000): Field remark doesnt have a default value这里报错编号1364是关键信息。HY000是通用的 ODBC 错误码不用管它重点是1364这个错误码对应的信息就是Field doesnt have a default value。3.2 第二步查询当前的 sql_mode 配置复现之后下一步就是看当前会话和全局的sql_mode-- 查看当前会话 SELECT SESSION.sql_mode; -- 查看全局配置 SELECT GLOBAL.sql_mode; -- 看 MySQL 编译时的默认值 SELECT DEFAULT_STORAGE_ENGINE;这里有个容易混淆的细节sql_mode是会话级session和全局级global双作用域的变量。你在命令行里查到的默认是会话级。如果你的业务连接池里的连接设置了会话级 sql_mode那全局改了也未必生效。排查时要两个都看一眼防止被某一条连接的特殊配置带偏。3.3 第三步检查表结构定位问题字段拿到报错里的字段名后就去核对表结构。最直接的方式是SHOW CREATE TABLE t_order;如果表结构很长可以只看目标字段的定义SELECT COLUMN_NAME, COLUMN_TYPE, IS_NULLABLE, COLUMN_DEFAULT FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA your_db AND TABLE_NAME t_order AND COLUMN_NAME remark;重点关注IS_NULLABLE列是不是NO以及COLUMN_DEFAULT是不是NULL。当IS_NULLABLE NO且COLUMN_DEFAULT NULL时这个字段就是本次报错的真凶。3.4 第四步对比有默认值和没默认值的行为差异为了彻底搞清楚边界条件我在排查时通常会做几组对照实验-- 实验1字段有默认值insert 不传应该成功 INSERT INTO t_user (name, age) VALUES (张三, 20); -- 输出Query OK -- 实验2字段无默认值且 NOT NULLinsert 不传报错 1364 INSERT INTO t_order (order_no, user_id) VALUES (A002, 1002); -- 输出ERROR 1364 -- 实验3字段无默认值但允许 NULLinsert 不传应该成功值为 NULL INSERT INTO t_log (content) VALUES (hello); -- 输出Query OK做这几组实验能帮你理清一个核心判断标准不是没默认值就必然报错必须同时满足NOT NULL 无默认值 insert 未赋值。很多人改了半天 default结果发现字段其实是nullable的白折腾。3.5 第五步判断是偶发还是必现区分代码问题还是环境问题最后一步是按频率判断问题性质。如果同一张表之前能插成功突然开始报错很可能是代码迭代后 insert 语句里少写了一个字段如果是新环境首次上線就报错那就是环境sql_mode和建表规范不匹配。这两种问题处理方式完全不同前者要改代码后者要调整配置或表结构。4. 三类解决方案与选型逻辑改代码、改表、改配置4.1 方案一代码层补全字段——最推荐的正路这类报错归根结底是 insert 语句没有覆盖所有必填字段所以最符合业务语义的修法是在代码里补上缺失值。比如原来 insert 语句长这样INSERT INTO t_order (order_no, user_id, amount) VALUES (A003, 1003, 88.00);报错之后对比表结构发现有remark和status两个必填字段没传那就补上INSERT INTO t_order (order_no, user_id, amount, remark, status) VALUES (A003, 1003, 88.00, 普通订单, 1);在业务代码里尤其是使用 MyBatis 这类 ORM 框架时推荐在 insert 语句的字段列表中显式列出每一个字段动态 SQL 中配合if test给可能为空的字段一个 fallback 值。这样做的好处很直接即使日后数据库的默认值调整了应用层的行为也是稳定可预期的。4.2 方案二表结构加默认值——最省事的治本方案如果这个字段在业务上确实有绝大多数情况下的合理默认值那么直接在表定义上加默认值最省心。常见的操作ALTER TABLE t_order ALTER COLUMN remark SET DEFAULT 无备注, ALTER COLUMN status SET DEFAULT 1;或者修改字段属性ALTER TABLE t_order MODIFY COLUMN remark VARCHAR(255) NOT NULL DEFAULT 无备注, MODIFY COLUMN status TINYINT NOT NULL DEFAULT 1;用MODIFY COLUMN时要注意它会重置字段的其他属性。如果你原来的字段有COMMENT、UNSIGNED等属性MODIFY语句里必须原样带上否则会丢。相比之下ALTER COLUMN ... SET DEFAULT只改默认值其他属性不动更安全。加默认值之后还有一层关系要注意MySQL 8.0 之前TIMESTAMP类型字段如果把默认值设为CURRENT_TIMESTAMP一个表只能有一个这样的字段。8.0 之后放宽了限制支持多个DATETIME字段都带DEFAULT CURRENT_TIMESTAMP这个细节在给表加创建时间更新时间默认值时会遇到。4.3 方案三调整 sql_mode——一刀切但要有敬畏心如果报错的表非常多且都是历史遗留表逐张加默认值工作量巨大有些团队会选择在数据库层去掉STRICT_TRANS_TABLES。操作很简单SET GLOBAL sql_mode ONLY_FULL_GROUP_BY,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION;但这样做我必须要泼一盆冷水去掉严格模式是把数据完整性的防线后移到了应用层治标不治本。一旦以后换个 DBA 或环境配置被重新收紧同样的报错会卷土重来。更麻烦的是严格模式的取消会让很多隐藏问题浮现——比如插入超长字符串被静默截断、非法日期被写成0000-00-00等你想做数据统计时会发现一堆莫名其妙的脏数据。所以我对调整sql_mode的定位是临时止血手段而非长期策略。真要做建议先做成会话级修改验证效果再考虑全局修改并且同步安排表结构的整改计划。4.4 三个方案怎么选一个实用的决策表为了方便你快速决策我把三种方案整理成了一个对照表方案改动范围见效速度长期风险适用场景代码补字段应用层单条 SQL/ORM 映射快低但靠开发自律字段业务含义明确代码能拿到值表加默认值单表结构快低但要注意历史数据字段有合理默认值适合绝大多数行修改 sql_mode数据库全局最快高脏数据风险上升历史表极多、短期无法整改的兜底手段个人经验是优先方案一和方案二方案三只有在今晚必须恢复业务这种极端情况下才考虑而且必须留下后续整改的技术债记录。5. 生产环境落地时的坑与细节从能跑到跑得稳5.1 修改 sql_mode 之后的持久化问题很多人执行完SET GLOBAL sql_mode...后第二天发现重启数据库又变回原样了。原因在于SET GLOBAL只修改了运行时的内存变量不会写进配置文件。要让修改持久化必须在配置文件里同步修改。MySQL 5.7 的配置文件是my.cnfLinux或my.iniWindows在[mysqld]段下添加[mysqld] sql_modeONLY_FULL_GROUP_BY,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION修改后需要重启 MySQL 服务。重启前建议先备份原配置另外要检查一下当前是否有长事务否则重启可能回滚一堆业务操作。5.2 会话级和全局级的区别你改的到底是谁这是一个很容易踩的坑。你可能会写SET SESSION sql_mode...或直接SET sql_mode...前者只对当前连接生效后者在 MySQL 里默认是只对当前会话生效的。如果应用使用的是连接池即使你全局改了连接池里已有的旧连接可能还带着启动时的会话级sql_mode。所以如果你怀疑某个连接有残留配置最稳妥的办法是让应用重启让连接池重新建立连接或者直接在数据库端执行SET GLOBAL的同时让应用侧DEFAULT的会话配置从全局同步。MySQL 8.0 中会话变量默认继承全局变量但同样只在建立新连接时生效。5.3 MySQL 版本差异5.7 和 8.0 的默认值处理这两个版本在默认值处理上有一个重要区别值得单独说。MySQL 8.0 明确废弃了AUTO_INCREMENT以外的隐式默认值并且ERROR_FOR_DIVISION_BY_ZERO的处理方式也有变化。同时 8.0 里如果你用了DEFAULT表达式比如DEFAULT (UUID_TO_BIN(UUID()))它允许默认值是一个表达式这给设计无感默认值提供了更多空间。在 8.0 里我还踩过一个细节有DEFAULT的BLOB/TEXT字段。5.7 中BLOB/TEXT不支持默认值语法如果你给这类字段加DEFAULT 会直接报语法错误。8.0 对此做了改进支持给BLOB/TEXT添加默认值所以升级到 8.0 后很多原本只能靠代码兜底的字段可以名正言顺地加默认值这类字段恰好也是Field doesnt have a default value报错的常见来源。5.4 避免为了默认值而默认值的设计异味最后想分享一个设计层面的体会。给字段加默认值不是越积极越好有些字段语义上就应该强制调用方显式传入比如订单来源、用户身份类型这类字段如果给了一个默认值等于在代码里埋了一个隐式分支容易掩盖上游传参的 bug。比较友好的做法是核心业务字段强制必填不加默认值扩展性字段给一个对业务无害的默认值。例如remark给无备注没问题但order_type这种关键枚举就别默认成1宁可报错暴露问题也不要让脏数据悄悄落地。6. 报错背后的能力建设一次排查长期受益如果你只是照着上面改了某个字段这个问题就算翻篇了但如果你想在团队里彻底降低这类报错的出现频率我建议把这套能力固化下来。一是建表规范中明确要求所有非自增、非逻辑删除标记的字段必须显式声明默认值除非它有强制的调用方必填语义。这个规范可以在 MySQL 8.0 中用CHECK约束辅助实现但主要还是靠代码评审和 DBA 审核来落地。二是把sql_mode的检查纳入到环境交付清单中。每次新环境搭建或者数据库迁移都要比对生产环境.sql_mode和测试环境.sql_mode是否一致。这个报错最典型的出现时机就是在测试环境不报错但生产环境报错的时候根源往往就是两边sql_mode不一致。如果你们的数据权限允许可以直接写个定时脚本把两边的GLOBAL.sql_mode拉出来做 diff。三是利用事件和告警把错误前置发现。MySQL 的 error log 里默认不会记录1364这种普通 SQL 报错但你的应用监控可以捕获。在告警规则里加上Field .* doesnt have a default value这个关键字匹配一旦出现立刻能感知到哪条 SQL 的必填字段被漏了而不是等到用户投诉才来翻日志。我在实际项目中还养成了一个习惯新表上线前用一条负向测试来验证表结构是否具备强约束能力。具体做法是准备一条故意缺字段的 insert 语句在测试环境执行如果报1364说明建表规范和 sql_mode 都在正常生效如果没报错就要小心是不是sql_mode被改松了。这个报错看起来只是一行红字但它背后是 MySQL 数据完整性机制、环境配置管理、开发规范三者之间的联动。把它的原理吃透你以后排查类似问题就会形成一个条件反射先看 sql_mode再看表结构最后才看代码。这个顺序能让你少走很多弯路。