ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

MySQL报错 Field doesn‘t have a default value 详解:严格模式与字段默认值排查

MySQL报错 Field doesn‘t have a default value 详解:严格模式与字段默认值排查 那天下午群里有人甩过来一段报错说测试环境联调插数据一直失败贴的日志第一行就是Field phone doesnt have a default value。这种错我见得多了基本上是 MySQL 严格模式在起作用但你真去搜解决方案网上答案要么是加默认值三个字就完事要么是关掉严格模式这种粗暴操作看完往往不知道该不该改、改了有没有副作用。这篇就把这个报错一次性讲透。你会搞明白这几个问题MySQL 为什么会管字段有没有默认值什么情况下会触发Field XXX doesnt have a default value怎么从一条报错反推出整条插入语句的问题所在以及取哪种修复方案最稳妥。不光是给个 SQL 抄连为什么这么改、改完有什么影响我都会说清楚适合正在被这个报错折磨的开发、DBA还有那些从老版本 MySQL 升级上来突然发现一堆问题的人。1. 先搞清楚这个报错到底在说啥1.1 一条报错背后藏着三个条件Field phone doesnt have a default value翻译成人话就是你往表里插数据时有一个叫phone的字段你没给值而这个字段在建表时既没写默认值又一个劲儿地要求你必须有值MySQL 觉得你是在耍无赖索性把整条 INSERT 语句给你拦下来。要触发这个报错三个条件缺一不可目标字段被定义为NOT NULL也就是非空约束数据库不允许这个字段存 NULL这个字段没有显式声明DEFAULT xxxMySQL 不知道该拿什么值去填坑你的 INSERT 语句里没有写这个字段或者写了 NULL 但被严格模式逮住了。你可以把这张表想象成一张入职登记表phone这一栏印着必填但又没有预先帮你填好一个兜底号码。你交表的时候把这一栏空着人事MySQL在非严格模式下会默默帮你填个默认的无然后把表收了但在严格模式下会直接退回来这栏必填、你又没填、表上也没预设值重填。1.2 为什么说没有默认值这件事在 5.7 之前不是问题MySQL 5.6 及更早的版本里sql_mode默认配置比较宽松数据处理方式偏向能塞就塞。遇到没默认值的非空字段没给值MySQL 会动用一套隐式默认值机制整型填 0字符串填空字符串时间字段填零值比如0000-00-00。所以老项目里经常出现明明字段没给默认值插入也能成功查出来却是 0 或空串的现象大家也习以为常。到了 MySQL 5.7官方把STRICT_TRANS_TABLES加进了默认sql_mode从此这类数据缺失就成了硬错误。MySQL 8.0 更是彻底默认开启严格模式而且默认配置更严格。很多老项目从 5.5、5.6 迁移到 5.7、8.0 之后原来能正常跑的插入逻辑突然开始疯狂报Field XXX doesnt have a default value大多数情况下不是你的业务代码变了是你的数据库再也不想替你打马虎眼了。2. 触发场景远比你想象的多2.1 四个最容易踩雷的日常操作这个报错不是只在从老库迁新库时才出现日常开发里我见过四类高频触发场景你可以对号入座。第一类是半路接手别人建的表。团队里某张核心表建表时偷懒字段全是NOT NULL但没设计默认值你基于这张表做新功能按自己的思路写 INSERT漏了其中一两个字段报错立刻就来。我处理过的案例里有张订单表的remark字段就是NOT NULL且没有默认值业务代码里有三处插入都没传这个字段前两年一直没炸因为跑在 MySQL 5.5 上数据库一升级三处接口同时报错查了半天才发现是同一个字段的问题。第二类是ORM 框架帮你掩盖了问题。用 MyBatis-Plus、Hibernate 这类框架时实体类里字段是 Java 的基本类型如int、long框架在做插入时如果你没给这个属性赋值生成的 SQL 就干脆不包含这个列。这跟上面说的情况一模一样SQL 没带字段、字段非空且无默认值严格模式直接拦截。很多人一开始会怀疑是 MyBatis 的 SQL 写错了其实回到数据库层面一查就明白了。第三类是测试环境不严格生产环境严格。有些公司测试库初始化时被前辈顺手改了sql_mode把严格模式去掉了于是测试环境怎么插怎么通一上生产就报这个错。这种环境差异比代码问题更难排查往往要折腾很久才发现两台数据库的sql_mode不一样。第四类是定时任务、批量导入脚本里漏字段。比如用LOAD DATA导入 CSV或者写了个一次性脚本同步数据CSV 里没准备age列而表里的age是NOT NULL且没默认值脚本跑到一半就报错。如果用的是普通 INSERT 还能快速定位用批量导入就得逐行排查了。2.2 一个关键误区显式插入 NULL 也会报错很多人有个直觉报错说没有默认值那我插入的时候显式给个 NULL 总行了吧事实恰恰相反对于NOT NULL字段你显式写NULL进 INSERT 语句在严格模式下照样被拦下来而且报错可能变成Column xxx cannot be null。这个更直白字段本身就写着不许为空你还硬塞 NULL。非严格模式下这两种情况都会被宽容处理但严格模式下统统走不通。搞清楚这个区别很重要因为你在排查时如果只盯着哪个字段没写进 SQL容易忽略另一种可能字段其实写了但业务代码里把它算成了 null拼进 SQL 里一样会炸。这两种报错对应的问题源头不同一个要在 SQL 层面的字段列表里找一个要在业务逻辑里找 null 是怎么产生的。3. 完整排查流程三步定位问题字段3.1 第一步把报错读完整很多新手看到Field phone doesnt have a default value就开始搜怎么改 MySQL 配置其实第一步应该做的是把整个报错连同上下文读完。真实的错误信息里前面通常会带表名、SQL 语句或事务上下文比如INSERT INTO user (username, password) VALUES (...)。报错里的字段名是结果不是原因真正的原因要回到 INSERT 语句和表结构里去对照。如果你用的框架异常堆栈里一般会打印出完整的 SQL如果是命令行直连报错本身就带 SQL 片段。我建议先把 SQL 完整复制出来和报错字段一起贴到排查文档里免得查着查着找不着原话了。3.2 第二步用 SHOW CREATE TABLE 看表结构定位问题字段的最佳方式不是猜而是直接看建表语句。连接数据库后执行SHOW CREATE TABLE user;然后重点看报错里提到的那个字段的定义。比如看到phone varchar(20) NOT NULL,这就实锤了非空、没默认值你的 INSERT 里没给它赋值严格模式下自然报错。如果看到的是age int NOT NULL DEFAULT 0,那这个字段就不会触发没有默认值的报错它有兜底值。所以排查的关键就是看字段定义里到底有没有DEFAULT那一截。这一步还能顺带发现其他隐患。我见过不少表报错字段只是第一个后面还有两个字段同样是非空无默认值只不过 INSERT 时给了值所以暂时没炸。如果你只修这一个字段下次业务改动漏了另一个报错换个字段名又会冒出来。所以排查时建议把整张表的前几个危险字段都过一遍。3.3 第三步回看 INSERT 语句并对着业务逻辑问三个问题拿到报错 SQL 后把它跟表结构并排看列出所有NOT NULL且无默认值的字段再从 INSERT 语句里找哪些字段被遗漏了。然后问自己三个问题这个字段的值对这条数据重不重要比如用户手机号显然应该由接口传进来而不是数据库兜底业务代码里是不是本来就该给这个字段赋值但某个分支漏了这个字段有没有可能在所有插入场景里都是固定值如果是那给默认值就很合理。我建议在本地或测试库复现一下把多余的 INSERT 字段删掉一条一条测试确认漏哪些字段会触发报错、触发的是哪个字段名。别在一个复杂 SQL 里盲猜拆开测几轮就能把报错字段和 SQL 位置的对应关系理清楚。4. 解决办法全解从治本到治标4.1 正规做法给字段补一个有业务含义的默认值如果这个字段在实际业务里确实经常不传或者有个对所有记录都合理的初始值那老老实实加默认值是最稳妥的。比如用户地址表里的status一开始只有老用户默认值1正常很合理再比如创建时间created_at设置DEFAULT CURRENT_TIMESTAMP几乎是标配。改法有两种效果等价但写法不同。第一种是改字段定义ALTER TABLE user MODIFY COLUMN phone varchar(20) NOT NULL DEFAULT ;第二种是只加默认值约束不改其他属性ALTER TABLE user ALTER COLUMN phone SET DEFAULT ;两个写法的区别在于MODIFY COLUMN是整体重新定义字段如果你漏写了原来的类型长度或注释可能会误改到别的属性用之前必须把SHOW CREATE TABLE里的完整定义抄出来在原来的基础上加DEFAULT部分ALTER COLUMN ... SET DEFAULT只动默认值不影响其他定义相对安全但有些老版本支持度不一样5.7 以上随便用。默认值选什么不是拍脑袋定的。字符串字段常用空字符串数值字段常用 0但前提是业务上允许这些值存在。如果你的业务后续要统计有没有填手机号空字符串和 NULL 在查询写法上有区别IS NULL查不到空字符串要用 这个语义差别如果没处理好后续会留下一堆隐患。4.2 改字段允许 NULL适合业务上确实可能没有值的场景如果你的业务逻辑里这个字段本来就是有就填、没有就不填的可选项那把这个字段改成允许 NULL 是合理的方向而不是硬给一个假默认值。比如用户昵称新用户注册时可以不填昵称那就该把字段定义里的NOT NULL去掉让数据库层面允许 NULL。操作也很简单ALTER TABLE user MODIFY COLUMN nickname varchar(50) NULL;或者写成ALTER TABLE user MODIFY COLUMN nickname varchar(50) DEFAULT NULL;DEFAULT NULL在 MySQL 里对可空字段来说就是默认值两者效果一致。但我必须提醒你一个实际情况很多老代码、老报表甚至前端页面都对这个字段一定非空有隐含假设。一旦改成可空查询结果里可能出现 NULLJava 基本类型直接接收会报空指针导出 Excel 时会显示空白代码里! null的判断分支可能被触发。所以这个方案最好配合代码评审一起做别只改数据库不改程序。4.3 最直接的应急手段INSERT 语句里显式带上这个字段不管是补默认值还是改可空都属于动表结构的操作在大型系统里要走审批流程不能随便在生产执行。如果线上正在报错等不了审批最直接的应急办法是在所有 INSERT 语句里把报错字段加上给一个合理的值。比如原来INSERT INTO user (username, password) VALUES (张三, 123456);报错说phone没有默认值那就改成INSERT INTO user (username, password, phone) VALUES (张三, 123456, );这个过程通常不是改一个 INSERT 就完事你得全局搜索这张表的插入语句。如果用的是 MyBatis看 XML 里的insert标签如果用了 MyBatis-Plus看实体类字段有没有被TableField标注、插入策略是什么默认对 null 字段是忽略的。改 SQL 是最局部、最不折腾数据库的方案缺点是代码里每一条漏字段的 INSERT 都得找到并改掉而且以后新增插入场景时还容易再犯。4.4 关闭严格模式我劝你把持住网上相当一部分答案会让你执行SET GLOBAL sql_mode NO_ENGINE_SUBSTITUTION;或者从sql_mode里去掉STRICT_TRANS_TABLES。这么干确实能一劳永逸消除这个报错但我强烈不建议在生产环境这么操作原因有两个。第一这相当于把 MySQL 5.7 以后的默认数据保护机制主动阉割了。严格模式拦截的不仅是你这条漏字段的 INSERT还有一大批数据质量问题插入超长字符串被静默截断、非法的日期被自动归零、除法结果精度异常被吞掉。今天你为了这一个报错关掉严格模式明天数据库里就会慢慢出现一堆脏数据而且这些数据不会报错会在某个意想不到的时间点爆雷。第二不同版本对sql_mode的处理方式有变化。MySQL 8.0 里NO_ZERO_DATE、NO_ZERO_IN_DATE这类参数行为跟 5.7 也不太一样你手工拼 sql_mode 字符串容易拼出个不合理的组合。而且SET GLOBAL只对之后新建的连接生效不重启 MySQL 的话连接池里的老连接可能还在旧模式里翻腾改完觉得没事了实际一堆连接还留着严格模式。真要关也得想清楚从配置文件里改并且全库重启但这个操作的影响面比你想的大得多。4.5 sql_mode 到底怎么查、我能不能理解它排查这个报错时你大概率会用到一条命令先把当前模式看清楚SELECT GLOBAL.sql_mode; SELECT SESSION.sql_mode;5.7 和 8.0 的默认值大致是ONLY_FULL_GROUP_BY, STRICT_TRANS_TABLES, NO_ZERO_IN_DATE, NO_ZERO_DATE, ERROR_FOR_DIVISION_BY_ZERO, NO_ENGINE_SUBSTITUTION这里面跟Field XXX doesnt have a default value直接相关的就是STRICT_TRANS_TABLES。如果sql_mode里有STRICT_ALL_TABLES也算严格模式不过默认不太出现。你只要看到STRICT_TRANS_TABLES再对照报错基本就能确认是严格模式卡住了你。如果真到了不得不绕开严格模式的边缘情况比如临时导数据、紧急恢复数据我建议只在当前会话里开一次绿灯做完就还原SET SESSION sql_mode NO_ENGINE_SUBSTITUTION;这个作用范围只对当前连接有效不碰全局、不碰配置文件风险相对可控。处理完别忘了在代码层面把 INSERT 语句补好别让错误再发生。5. 一次真实故障的完整复盘我拿之前帮同事排查的一个例子完整走一遍处理链路方便你把上面几节的顺序串起来。场景是一张用户扩展表CREATE TABLE user_profile ( id bigint(20) NOT NULL AUTO_INCREMENT, user_id bigint(20) NOT NULL, age int(11) NOT NULL, city varchar(50) NOT NULL, bio varchar(200) DEFAULT NULL, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;同事报的错误是Field city doesnt have a default value他是用 MyBatis-Plus 做插入的实体类里city字段是 String没有赋值。生成的 SQL 是INSERT INTO user_profile (user_id, age, bio) VALUES (1001, 25, hello);跟我前面说的完全吻合SQL 里压根没带city这个字段表定义里city是非空无默认值MySQL 严格模式直接拦截。当时我先看了完整报错和 SQL再执行了SHOW CREATE TABLE user_profile;确认city的定义非空且没有默认值。然后问同事这个字段的业务含义城市信息在注册流程里是可选项用户不填也能注册。这就说明问题不在于必须给 city 一个默认值而在于它的表结构定义跟业务需求不匹配。所以我给的方案是走 4.2 的路线把字段改成可空ALTER TABLE user_profile MODIFY COLUMN city varchar(50) DEFAULT NULL;改完之后同事重新跑插入不再报错。同时我提醒他检查两处代码第一实体类里city属性偶尔可能赋值为空字符串那会插入而不是 NULL如果后面要统计用户分布城市空字符串会被当成一个城市建议在业务层统一把空字符串转成 null第二报表系统里如果有WHERE city 这种写法改了 NULL 之后查询逻辑要跟着变化。这个案例里如果换成给city加DEFAULT 也能不报错但业务上城市未知和城市在数据库里是个空字符串是两个概念用 NULL 表示未知更语义化未来的统计、分析都不会被干扰。选方案不能只看能不能消掉报错要顺着业务语义找最贴合的。6. 常见问题速查与我的避坑心得6.1 高频疑问和对应操作问题原因处理建议同一个 SQL 测试环境不报错、生产报错两台库的 sql_mode 不一致对比GLOBAL.sql_mode统一后从代码里修复 INSERT5.5/5.6 迁移到 5.7/8.0 后大量报错老版本非严格模式下隐式填默认值新版本严格模式拦截逐个处理非空无默认值字段优先补默认值或改可空datetime 字段报没有默认值MySQL 5.6 之后 timestamp 默认值规则变化按业务需要显式加DEFAULT CURRENT_TIMESTAMP或允许 NULL显式 INSERT NULL 到非空字段字段本身NOT NULL不允许 NULL业务里把该字段转成实际值或把字段改成可空批量插入部分失败某几行数据缺字段值检查数据源补充缺失字段或对字段设置默认值兜底加了DEFAULT 0后还是报错改的是字段默认值但 INSERT 里显式写了 NULL看 INSERT 的字段列表把 NULL 改成 0 或移除该字段修改 sql_mode 后重启就还原只改了会话/全局没改配置文件或考虑用配置文件持久化但不建议关闭严格模式6.2 实操中的几点经验第一排查没有默认值报错时不要一上来就打开表结构大改。先把报错里的 INSERT 语句完整读一遍确认是没写字段还是写了 NULL两者修法完全不同前者改 SQL 或者补默认值后者要先去业务代码里查 null 是哪来的。第二给字段加默认值之前先问问这个字段在业务上是否真的有一个通用初始值。我见过有人为了消错把订单金额字段直接DEFAULT 0结果支付回调前订单都是 0 元报表统计时把大量未支付订单当成了 0 元单出了一堆对不上的数。默认值不是用来敷衍 MySQL 的它会在数据流里流动选错初始值等于埋雷。第三表结构变更要顺带查下游。一个字段从非空变可空或加了一个默认值影响的不只是这一张表还有所有依赖这张表的 SELECT、报表、导出任务。改完之后最好在测试环境把核心查询和导出流程都走一遍不要只看 INSERT 通不通。第四个人建议在项目规范里约定建表时所有非空字段必须带默认值或者必须能确认每次插入都会显式提供。MySQL 5.7 开始严格模式是默认的早一点把规范立起来比后面被报错追着改要舒服得多。这次这个报错说到底是数据库从宽松走向严格的必然结果也是倒逼你把表结构和业务逻辑整理清楚的机会。我在实际排查里的体会是报错本身从来不可怕可怕的是为了消掉报错而做出的短视决定。不管是补默认值、改可空还是改 INSERT 语句想清楚业务上这个字段到底是什么含义方案自然就出来了。最后再分享一个小习惯新表上线前跑一遍这个 SQL把非空且无默认值的字段都扫出来。SELECT TABLE_SCHEMA, TABLE_NAME, COLUMN_NAME, DATA_TYPE FROM information_schema.COLUMNS WHERE IS_NULLABLE NO AND COLUMN_DEFAULT IS NULL AND EXTRA NOT LIKE %auto_increment% AND TABLE_SCHEMA NOT IN (mysql, information_schema, performance_schema) ORDER BY TABLE_SCHEMA, TABLE_NAME, ORDINAL_POSITION;上线前扫一遍可能多花五分钟往后能省你好几个加班的晚上。
返回列表