
1. 报错场景还原与现象复盘先说说这个报错是怎么出现在我面前的。当时接手一个老项目Spring Boot 后端连的 MySQL 5.7业务跑得好好的突然某天运营反馈新增用户时接口报 500日志里赫然写着Field nickname doesnt have a default value。第一反应是表结构被人动过查了一圈发现啥都没改再一细看原来是新同事部署时在另一台机器上初始化数据库用的 SQL 脚本里某张表的nickname字段写的是NOT NULL且没有DEFAULT而插入语句压根没传这个字段。其实这类报错在 MySQL 生态里非常典型你在搜索引擎里随便一搜满屏都是Field xxx doesnt have a default value的求助帖。它不是一个冷门 corner case而是每个 MySQL 使用者大概率会撞上的坎。凡是往表里插数据遇到某个字段没给值、字段本身又是 NOT NULL 无默认值、加上全局的 SQL_MODE 还开着严格模式这三个条件一碰报错就会冒出来。这个报错最让人头疼的地方在于它跟你写的 SQL 表面上看关系不大你明明没往那个字段插值错误却指向它。很多刚入门的朋友会怀疑是 MySQL 版本问题或以为是驱动问题甚至去重启服务其实方向全错了。理解这个报错核心就三件事SQL_MODE是什么、字段定义是否允许缺省、INSERT 语句到底有没有显式传值。把这三点捋清楚排查基本就见山见山了。这篇文章适合谁看正在被这个报错折磨的后端开发、DBA 新手还有那些写建表脚本时习惯性省略 DEFAULT 的兄弟们。我会从根因讲到排查从修法讲到预防尽量把细节掰开揉碎让你下次再撞见它时不用查资料也能五分钟定位。2. 报错根因SQL_MODE 与字段定义的博弈2.1 严格模式才是真正的主角这个报错的全名虽然是Field xxx doesnt have a default value但真正下判词的是 SQL_MODE 里的STRICT_TRANS_TABLES或STRICT_ALL_TABLES。MySQL 5.6 之前默认的 SQL_MODE 是个空字符串也就是宽松模式。那时候你往 NOT NULL 且无默认值的字段里不传值MySQL 不会报错而是偷偷给你塞一个隐式默认值——数字类型给 0字符串类型给空串时间类型给当前时间。听着挺方便对吧但代价是数据不可控。你以为是脏数据入侵其实是 MySQL 在帮你补窟窿只是这个窟窿补得毫无逻辑。到了 MySQL 5.7 之后官方把默认 SQL_MODE 改成了带上STRICT_TRANS_TABLES。严格模式下如果插入的数据违反约束MySQL 会直接拒绝执行并抛出错误而不是默默给你填一个默认值。所以同样的 SQL在 5.6 上跑得好好的升级到 5.7 就报这个错这根本不是MySQL 变笨了而是它终于开始讲规矩了。你可以执行下面的命令看看当前会话的 SQL_MODESELECT SESSION.sql_mode; SELECT GLOBAL.sql_mode;我见过不少生产环境全局 SQL_MODE 被前人改过加了一堆东西甚至把STRICT_TRANS_TABLES给去掉了。这种环境下这个报错确实不会出现但代价是数据库悄悄吞掉错误往表里写了一批看起来正常、实际全无意义的数据。真到数据清洗那天哭都来不及。2.2 字段定义中的 NOT NULL 陷阱再说字段定义。建表时最常见的陷阱就是nickname VARCHAR(50) NOT NULL写得很顺手却忘了加 DEFAULT。你要知道NOT NULL和DEFAULT是两回事NOT NULL表示字段不能存 NULL这是约束。DEFAULT表示当 INSERT 没给这个字段时用什么值来填充这是兜底。两者组合有四种情况字段定义组合INSERT 未传值时行为有无风险NOT NULL DEFAULT 默认值用默认值填充安全NOT NULL 无 DEFAULT严格模式报错宽松模式填隐式默认值高风险NULL DEFAULT NULL填 NULL安全但不推荐NULL 无 DEFAULT填 NULL一般安全你如果去翻那些报错案例九成都是第二种组合。还有更隐蔽的情况字段本身有 DEFAULT但 DEFAULT 的值非法。比如某个日期字段写的是DEFAULT 0000-00-00而 SQL_MODE 里开着NO_ZERO_DATE那插入时照样报错只是报错信息变成了Incorrect date value。这两种错误经常被混为一谈排查时要留意。2.3 最容易被忽略的 INSERT 细节还有一种场景特别容易踩INSERT 语句里明确列出了字段清单但清单外还有 NOT NULL 无默认值的字段或者用了INSERT INTO ... SELECT这种批量写法SELECT 出来的列数量和目标表字段数量没对齐。MySQL 在处理这类 SQL 时对于没出现在字段清单里的列一律按缺省处理然后去检查缺省行为是否合规。这里有个让我印象很深的案例两个库做数据同步源表有个remark字段允许 NULL目标表因为历史原因把它定义成了NOT NULL DEFAULT 逻辑上没问题。但同步任务用的是 Flink CDC 这类工具写入时有时候真的会传 NULL这时候严格模式就直接拒绝了报错信息一样是这个doesnt have a default value但其实根源是传入了 NULL 而非缺省。所以报错信息里的doesnt have a default value实际包含两种情况一是真的什么都没传二是传了 NULL。排查时一定要先想清楚自己属于哪一种。3. 三轮排查法从现象到定位的完整实操拿我自己常用的排查套路来说我习惯把它拆成三轮先看环境、再看表结构、最后盯住SQL本身。每一轮都有明确的目标和命令按部就班走下来基本不会漏。3.1 第一轮核实 SQL_MODE 与表结构第一步先确认你当前会话和全局的 SQL_MODE 是什么。注意Java 项目里用的连接池比如 HikariCP可以在 JDBC URL 上通过sessionVariables参数来覆盖会话级 SQL_MODE命令行连进去看到的SESSION.sql_mode只代表命令行这个会话不代表应用连接那个会话。这点容易误导人我踩过。-- 查看全局和当前会话的 sql_mode SELECT GLOBAL.sql_mode; SELECT SESSION.sql_mode; -- 查看具体表的建表语句注意 NOT NULL 和 DEFAULT 的组合情况 SHOW CREATE TABLE your_table_name\G然后沿着报错信息里那个字段名去建表语句里定位它的定义。如果看到的是NOT NULL且没有 DEFAULT那基本锁定了一半。如果字段定义没问题再看 SQL_MODE 里有没有严格模式关键词。两相对照原因就浮出水面了。3.2 第二轮模拟复现不同 session 环境差异我遇到过一种诡异情况应用报错但我在命令行里手动执行同样的 INSERT 却成功了。查了半天发现命令行客户端的 SQL_MODE 被某个登录脚本改过而应用连接没有。所以第二轮建议你直接用应用连接使用的那个账号和 JDBC URL 去连数据库再执行一次复现 SQL这样才贴近真实。-- 用应用账号登录后先看这个会话的 sql_mode SELECT SESSION.sql_mode; -- 尝试往那个表插入一条最简数据 INSERT INTO your_table_name (id) VALUES (100);注意如果这条复现 SQL 也报同样的错那可以肯定问题出在表结构或 SQL_MODE 上。如果复现成功那说明应用过来的 INSERT 语句和你在命令行里执行的不一样大概率是 ORM 框架生成的 SQL 存在差异或者代码里真的漏传了字段。这一步能帮你把问题从环境怀疑切换到代码怀疑。3.3 第三轮区分连接池、框架与同步工具的隐蔽触发路径第三轮比较进阶适合前面两轮都没揪出问题的情况。需要排查以下三个隐蔽入口第一个是连接池初始化 SQL。像 HikariCP 支持connection-init-sqlDruid 支持connectionInitSqls某些项目会在连接建立时统一执行SET sql_mode 或反过来设置。假如有人为了兼容老代码在连接池把严格模式关了那应用侧自然不报错但如果连接池配置被误删应用侧突然恢复严格模式那历史遗留的那些没 DEFAULT 的 NOT NULL 字段就会集中爆发。排查方法是看应用启动日志或连接池配置。第二个是 ORM 框架的动态 SQL。MyBatis-Plus、Hibernate 这类工具有时候会根据实体类的字段注解来生成 INSERT 语句。如果你实体类里的某个字段加了TableField(insertStrategy FieldStrategy.NEVER)或者 Hibernate 里的insertable false那框架生成的 INSERT 就会主动忽略这个字段数据库层面自然就缺省了。这种问题光看数据库表结构是看不出来的得把框架打印的 SQL 日志拉出来。第三个是 CDC 和 ETL 工具。比如 Canal、Flink CDC、DataX这类工具在跨库同步时如果源表字段为空NULL而目标表字段不允许 NULL即使目标表有 DEFAULT写入时也可能因为工具显式传了 NULL 而触发报错。这种情况下表结构和 SQL_MODE 都是正常的纯粹是同步逻辑和表约束不对齐。排查时要看同步任务的日志和字段映射配置。4. 解决方案与选型对比改会话、改表、还是改 SQL问题定位之后怎么修就清晰了。但清晰不等于随便修我见过太多人在生产环境直接把 SQL_MODE 改成空字符串图一时省事结果埋下长期隐患。下面把三种主流方案讲透并给出我的选型建议。4.1 方案一会话级或全局级放宽 SQL_MODE这个方案最直接去掉严格模式让 MySQL 回到宽松模式对缺省字段自动填隐式默认值。-- 会话级只对当前连接生效 SET SESSION sql_mode ; -- 全局级新连接生效已有连接不受影响 SET GLOBAL sql_mode ; -- 更稳妥一点只去掉严格模式保留其他校验 SET GLOBAL sql_mode NO_ENGINE_SUBSTITUTION;这个方法高效但我不推荐作为长期方案。原因很简单严格模式存在的意义是防止脏数据从入口混入。你把严格模式关了等于让数据库对所有非法输入睁一只眼闭一只眼。将来某天需要把严格模式开回来那些历史坏数据会瞬间变成一堆需要手工清理的麻烦。唯一适合用这个方案的场景是你有一个历史遗留系统表结构一时半会儿改不动业务又必须立刻恢复那就临时放宽 SQL_MODE 顶一下同时把整改表结构记进技术债清单限期处理。4.2 方案二为字段补 DEFAULT 或允许 NULL这个方案是从表结构层面把问题根除掉也是我最推荐的做法。-- 为字段补默认值字符串用空串数字用 0注意业务语义 ALTER TABLE your_table_name ALTER COLUMN nickname SET DEFAULT ; -- 如果字段本来就不该强制非空可以允许 NULL ALTER TABLE your_table_name MODIFY COLUMN nickname VARCHAR(50) NULL;补充 DEFAULT 时有一个核心决策点用什么值当默认值。我见过不少开发图省事字符串直接给空串数字给 0。如果这个字段在业务上本来就可以为空那空串没问题但如果业务上这个字段是必填但有默认规则的比如注册用户的默认昵称应该是用户手机尾号那 DEFAULT 也需要相应写成类似CONCAT(用户, RIGHT(phone, 4))的形式——当然MySQL 的 DEFAULT 表达式在 8.0.13 之后才支持函数表达式5.7 里只能用常量或特定表达式这一点要注意。另外提醒一句修改表结构时使用ALTER TABLE会触发元数据锁在千万行的表上执行要选业务低峰期否则可能把读写拖垮。虽然现在 MySQL 8.0 支持 INSTANT 算法但 5.7 还是需要评估代价。4.3 方案三INSERT 语句显式补全字段方案三是从代码侧入手把报错字段显式写进 INSERT 语句的字段清单里并给它一个合理的值。-- 修改前 INSERT INTO your_table_name (id, username) VALUES (100, kitty); -- 修改后nickname 显式给值 INSERT INTO your_table_name (id, username, nickname) VALUES (100, kitty, 用户100);在 ORM 框架层面如果是 MyBatis可以在插入 SQL 里把字段加上如果是 JPA可以调整实体类的 insertable 和 nullable 配置。这个方案的优点是不动表结构、不动数据库配置对已上线的系统最安全缺点是每处插入逻辑都得改漏一处就功亏一篑。如果项目里 INSERT 语句集中在少数几个 mapper 里方案三很合适如果插入路径很多我建议方案二和方案三组合先补 DEFAULT 兜底再把代码里能显式传值的补上双保险。4.4 三种方案对比与决策建议给个简单对照表方便你决策方案改动范围见效速度风险等级适用场景放宽 SQL_MODE数据库全局/会话立即高长期隐患应急恢复、历史遗留系统修改表结构单表立即中需评估锁表构建规范、长期根治修改 INSERT代码层取决于发布周期低插入路径少、无法改表我的个人偏好是能改表结构的优先改表结构改不了就用 INSERT 补值SQL_MODE 是最后手段。这几个方案的执行成本其实都不高但长期收益差异很大。5. 生产环境预防与治理经验报错解决了更值得做的是想清楚如何避免它再次发生。我在多个项目里推行过一套三步走的治理策略效果不错分享给你。5.1 从源头建表规范与 SQL 审查先说建表规范。很多报错的根源是建表语句写得随意。团队内部应该约定一个最低标准凡是 NOT NULL 的字段除了主键和业务上真正不允许默认值的字段一律要带 DEFAULT。比如用户表CREATE TABLE user_info ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 主键, username VARCHAR(50) NOT NULL COMMENT 用户名, nickname VARCHAR(50) NOT NULL DEFAULT COMMENT 昵称, age TINYINT UNSIGNED NOT NULL DEFAULT 0 COMMENT 年龄, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户信息表;这套标准的背后逻辑是让 INSERT 语句可以偷懒也允许业务代码在漏字段时不至于崩溃。同时凡是上线前经过 SQL 审核的工具比如 Yearning、Archery都应该把NOT NULL 且无 DEFAULT 且非主键作为一条强规则建表脚本提交时自动拦截。5.2 从运维SQL_MODE 统一管理与变更流程生产环境的 SQL_MODE 应该纳入配置管理而不是靠某个人手动 SET。你可以在 MySQL 配置文件my.cnf的[mysqld]段里固化[mysqld] sql_modeSTRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION如果你用容器化部署那就把 SQL_MODE 写进环境变量或 ConfigMap让所有实例保持一致。这样就不会出现开发环境不报错、测试环境报错、生产环境又好了的诡异情况。变更 SQL_MODE 时必须走变更流程先在测试环境跑一遍回归用例确认开发、测试、生产三套环境的 SQL_MODE 完全一致。5.3 从开发ORM 与框架层的配置约束如果说建表规范和 SQL_MODE 是从数据库侧发力那开发侧的约束同样重要。使用 MyBatis-Plus 时可以设置全局的字段插入策略比如insert-strategy: not_null这样实体类里为 null 的字段不会出现在 INSERT 语句里。但要注意这只解决缺省问题不能解决字段 NOT NULL 但传了 null的问题。相反如果你希望 null 也参与插入并触发数据库默认值需要把策略设置成ignored或根据情况调整。使用 JPA/Hibernate 时可以在实体类的Column注解里直接声明Column(name nickname, nullable false, columnDefinition varchar(50) not null default ) private String nickname;这样做的好处是表结构由代码驱动时建表脚本也带着 DEFAULT实体对象没赋值时数据库也能兜底。缺点是双写容易漂移需要保证代码里的 columnDefinition 和数据库实际结构一致。另外如果你是用了 CDC 同步工具比如 Flink CDC强烈建议在同步前做一次字段映射审查源表允许 NULL 的字段映射到目标表时要么目标表允许 NULL要么映射时用COALESCE补默认值不要硬写。这个坑我踩过一次之后就在所有同步任务里加了字段映射校验表每次改同步逻辑必须过这一关。6. 常见问题速查与最终经验小结6.1 几个高频变体问题的排查速查这个报错在真实环境中经常以不同变体出现我汇总一下常见场景方便你对照排查现象根因排查重点新表插入就报错建表脚本字段缺 DEFAULT 或 SQL_MODE 与开发环境不一致对比SHOW CREATE TABLE和GLOBAL.sql_mode老系统升级 MySQL 5.7 / 8.0 后开始报错从宽松模式变为严格模式确认新版本默认 SQL_MODE回看老建表脚本应用报错但命令行不报会话级 SQL_MODE 不同或 INSERT 语句不同查看应用连接池初始化 SQL抓 ORM 生成的 SQL 日志批量同步任务间歇性报错源表部分数据为 NULL目标表 NOT NULL 无默认值检查同步字段映射对 NULL 做COALESCE定时任务深夜报错字段有 DEFAULT 但 DEFAULT 是0000-00-00确认 SQL_MODE 是否含NO_ZERO_DATE主从复制报错主库放宽 SQL_MODE 写入数据从库严格模式拒绝对比主从两边的 SQL_MODE备份还原后报错备份文件中的建表语句丢失 DEFAULT导出时用mysqldump核对备份文件 DDL特别注意最后两行一个隐藏很深一个是运维操作引出的问题。尤其是主从复制场景如果主库的 SQL_MODE 已经被改过比如去掉了NO_ZERO_DATE而 binlog 里记录的 SQL 在从库执行时从库是严格模式那复制线程直接中断。排障时去SHOW SLAVE STATUS看Last_SQL_Error基本上全是这类错。解决思路不是简单改从库 SQL_MODE而是让两边保持一致并且检查已经写入主库的坏数据是否需要清理。6.2 我的个人体会跟这个报错较劲了几年我的感受是技术问题往往只是表象真正考验人的是你在多大程度上愿意把规范坚持到底。每次图省事改了 SQL_MODE未来必然在某处付出更大的代价。反过来第一次遇到报错时老老实实补 DEFAULT、补 INSERT 字段、统一 SQL_MODE后续的麻烦就少了。另外一个小技巧想分享给大家任何 DDL 变更都建议先在本地或测试环境跑一遍SHOW CREATE TABLE导出的完整建表语句用mysqldump备份还原模拟一遍千万别直接在几百 GB 的生产表上直接改。我见过不止一次因为少写一个DEFAULT导致整张表被ALTER重建业务停了快半小时。如果你正在被这个报错困扰按上面的三步走基本能稳准狠地解决。它不是复杂问题但需要你把每个细节都看透。