工作这么几年,MySQL 的报错见过不少,但有一种错看着特别“不科学”,第一次碰上会让人愣好半天:
Field 'remark' doesn't have a default value明明 SQL 语句里字段、值一一对得上,语法也没问题,凭什么报“没有默认值”?我最早遇到这个错是在处理一张订单表的回调写入时,前端传过来的数据里少了一个remark字段,INSERT 语句里也就没带它。当时我的第一反应是“SQL 写错了”,但反复核对字段名、表名、字段类型都没找出问题,最后才发现根子在表结构上。这篇文章就把这条报错从根因到修复完整拆一遍,顺便把排查链路整理出来,希望大家下次遇到时能在五分钟内定位,而不是像我当初那样对着屏幕怀疑人生。
1. 一条看似“无缘无故”的报错,实际是表结构约束在作怪
1.1 具体报错场景还原
这个错误最典型的出现场景,是往一张已有表里执行 INSERT 或 UPDATE 时,某个字段没有在语句中赋值,而 MySQL 严格模式下又不允许它为空。举个我实际用过的例子:
CREATE TABLE user_order ( id INT AUTO_INCREMENT PRIMARY KEY, order_no VARCHAR(64) NOT NULL, user_id INT NOT NULL, remark VARCHAR(255) NOT NULL, amount DECIMAL(10,2) NOT NULL DEFAULT 0.00 );注意这个remark字段:NOT NULL,但没有DEFAULT。接着执行:
INSERT INTO user_order (order_no, user_id, amount) VALUES ('NO20240101', 1001, 199.00);如果 MySQL 处于默认的严格模式(STRICT_TRANS_TABLES),这条 SQL 会直接报错:
ERROR 1364 (HY000): Field 'remark' doesn't have a default value而如果把sql_mode里的严格模式去掉,同样的 INSERT 会变成“warning + 插入空字符串”,数据就悄悄写进去了。这个差别非常关键,后面我会专门展开。
还有一种常见情况是 UPDATE 语句触发同一错误,比如:
UPDATE user_order SET amount = amount * 0.9 WHERE order_no = 'NO20240101';如果user_order上有个 UPDATE 触发器,往另一张表插入记录时漏了某个 NOT NULL 字段,一样会抛出这个错,而且乍看之下和报错表完全对不上。这也是这条报错最容易坑人的地方——报错的表不一定是你正在操作的表。
1.2 严格模式下 NOT NULL 字段的真实行为
要理解这条报错,先得接受一个事实:MySQL 对“NULL”和“没有默认值”是两件事。前者是数据取值,后者是表结构定义。NOT NULL约束只说明“这个字段不能存 NULL”,但并没有告诉我们“如果 INSERT 没给值,MySQL 该怎么办”。
在没有显式DEFAULT值的情况下,MySQL 有一套隐式默认值规则:数值类型默认 0,字符串类型默认空串,日期时间类型默认对应零值(如'0000-00-00')。但这套隐式默认值只在非严格模式下生效。一旦开启严格模式,MySQL 的选择就变了——直接抛错,把问题暴露给应用层,而不是默默替你做主。
说白了,严格模式是为了防止“数据悄悄变成奇怪的值”。如果业务里允许 remark 为空,你会希望它插入后是空字符串;如果业务里不允许 remark 为空,那这条 INSERT 本身就有问题。MySQL 没法替业务做决策,所以严格模式下它选择“宁可报错也不猜”。
提示:这个报错和字段是不是主键没关系。主键如果是 AUTO_INCREMENT,自增列本身有隐式生成规则,不会踩这个坑;但普通字段就不一样了。
2. 根因拆解:为什么 MySQL 会拒绝这条 INSERT 语句
2.1 字段定义与隐式默认值规则
先回到建表语句本身。一条完整的字段定义通常包含:字段名、数据类型、是否允许 NULL、默认值、字符集等。例如:
remark VARCHAR(255) NOT NULL,这里没有写DEFAULT,所以 MySQL 认为该字段“没有默认值”。此时,如果 INSERT 语句没有包含这个字段,MySQL 需要自己决定怎么处理。
MySQL 官方的行为规则大致是这样:
- 字段有显式
DEFAULT,直接用默认值; - 字段允许 NULL,且没有给值,就写入 NULL;
- 字段是
NOT NULL且没有DEFAULT:- 非严格模式:使用类型的隐式默认值,如字符串空串、数值 0;
- 严格模式:直接报错,拒绝写入。
很多人建表时会忽略“要不要写 DEFAULT”,觉得反正插入时都会带值。但表结构是给别人用的,也是给未来的你用的——一旦应用代码里漏传字段,这个问题就会以报错的形式冒出来。尤其在团队协作的项目里,一张表的字段可能被多个服务同时写入,A 服务传全了字段,B 服务只传了部分字段,那 B 服务就会成为这条报错的常客。
2.2 sql_mode 中 STRICT_TRANS_TABLES 的作用机制
sql_mode是 MySQL 服务端的一组会话级/全局级参数,控制着很多行为,其中和本条报错最相关的是STRICT_TRANS_TABLES和STRICT_ALL_TABLES。
默认安装的 MySQL 8.0 的 sql_mode 一般长这样:
ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION其中STRICT_TRANS_TABLES对事务表(如 InnoDB)启用严格模式,对非事务表(如 MyISAM)只在第一批数据出现错误时才报错,允许部分写入。STRICT_ALL_TABLES则对所有表一律严格。
在这个模式下,MySQL 拒绝向“NOT NULL 且无默认值”的字段插入缺失值。如果没有这个模式,MySQL 会悄悄把空字符串或 0 写进去,只在 warning 日志里留一条记录。很多“数据看起来正常,但后期统计对不上”的谜案,往往就源于这种静默写入。比如金额字段缺省变成 0,报表汇总时平白多了一批 0 元订单,排查起来相当费劲。
2.3 显式 DEFAULT 与隐式 DEFAULT 的差别
很多人把DEFAULT NULL和“没有默认值”搞混。其实:
DEFAULT NULL:只在字段允许 NULL 时合法,意思就是“不写就存 NULL”;DEFAULT 0/DEFAULT '':显式默认值,写入时未提供就用这个;- 什么都不写且是
NOT NULL:没有默认值,严格模式下报错。
另外一个容易混淆的是“默认值表达式”。MySQL 8.0 支持函数式默认值,例如:
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,这就是显式默认值,合法且常用。而:
create_time TIMESTAMP NOT NULL,同样是 NOT NULL 无默认值,如果 INSERT 没写入,严格模式一样会报Field 'create_time' doesn't have a default value。
这里要特别提醒:TIMESTAMP在没有显式默认值的情况下,MySQL 5.6 之前的旧版本会隐式给第一个 TIMESTAMP 字段加DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,容易让人误以为“时间字段不会报这个错”。到了 MySQL 8.0,这种隐式规则已经变了,时间字段同样会因为没有默认值而报错。所以看到时间字段报错时,别惊讶,直接按同一套流程排查。
3. 诊断链路:先从哪下手,再查哪
遇到报错,最忌讳一上来就改 sql_mode 或给所有字段加 DEFAULT。不加判断的“一刀切”很可能掩盖真正的业务问题。我的排查顺序固定如下三步。
3.1 第一步:看表结构的正确姿势
用SHOW CREATE TABLE而不是DESC来看表结构。为什么?因为DESC只显示字段类型、NULL 与否、默认值,而SHOW CREATE TABLE会完整展示索引、约束、默认值表达式,一眼能看出哪个字段既 NOT NULL 又没有 DEFAULT。
SHOW CREATE TABLE user_order\G重点关注NOT NULL且没有DEFAULT的字段。这些字段就是潜在的“报错点”。一次排查中,我甚至发现连续两个字段都少写了 DEFAULT,修复时要一起处理,只补一个的话,下一个报错马上会冒出来。
3.2 第二步:确认当前会话的 sql_mode
SELECT @@SESSION.sql_mode; SELECT @@GLOBAL.sql_mode;确认严格模式是否开启。如果@@SESSION.sql_mode里没有STRICT_TRANS_TABLES,那报错可能来自别处;如果有,再看是哪条 INSERT 漏了字段。
有人会问:为什么看会话级还要看全局级?因为连接池里的连接可能各自有不同的会话设置。比如应用中某个连接执行过SET SESSION sql_mode = '',那么这个连接上的行为就和别的连接不同。排查线上问题时,不光要看全局,还要考虑到连接级差异。
提示:MyBatis、JDBC 连接池一般不会主动改 sql_mode,但某些管理工具或脚本会在连接后执行 SET 语句,导致同一库在不同连接下行为不一致。这也是“测试环境正常、生产环境报错”的常见来源之一。
3.3 第三步:复现最小化用例
拿到有问题的表结构后,我会复制一张最小表来复现:
CREATE TABLE tmp_order LIKE user_order; INSERT INTO tmp_order (order_no, user_id, amount) VALUES ('T1', 1, 1.00);如果报错能复现,说明问题在表结构本身;如果复制表后没有报错,那就要把注意力转向触发器和应用层的插入路径。这一步很关键,因为触发器也可以给字段塞值,表面看起来 INSERT 没问题,实际是触发器里的逻辑碰到了同样约束。
4. 修复方案三选一:改表、改代码、还是调整 sql_mode
这个问题没有“唯一正确解”,只有“当前业务下最合适的解”。我通常按下面的顺序权衡。
4.1 方案 A:给字段补默认值
这是最直接的修复方式,也最推荐。
ALTER TABLE user_order ALTER COLUMN remark SET DEFAULT '';如果某个字段本来就不该为空,也可以设置一个有业务含义的默认值,比如状态字段默认'PENDING'、数字字段默认0。在 MySQL 8.0 中,还可以用表达式作为默认值:
ALTER TABLE user_order ALTER COLUMN create_time SET DEFAULT (CURRENT_TIMESTAMP);注意,修改默认值不会重建整张表,只是修改元数据,执行速度一般很快。但有一个前提:表里已有数据必须符合目标约束。比如你把一个字段从允许 NULL 改为 NOT NULL DEFAULT 0,若表里已经有 NULL,ALTER 会失败或需要额外处理。
执行前建议先备份,或者至少确认业务影响:
-- 查看可能受影响的行 SELECT COUNT(*) FROM user_order WHERE remark IS NULL;4.2 方案 B:修正 INSERT 语句或代码逻辑
如果这个字段在业务上“必须有值”,那么正确做法是改应用代码,让 INSERT 无论如何都带上该字段,而不是依赖数据库默认值兜底。
比如 Java 侧的 MyBatis 中,插入语句如果写成:
<insert id="insertOrder" parameterType="Order"> INSERT INTO user_order (order_no, user_id, amount) VALUES (#{orderNo}, #{userId}, #{amount}) </insert>那remark没传时就会触发报错。修复应该是补齐字段:
<insert id="insertOrder" parameterType="Order"> INSERT INTO user_order (order_no, user_id, amount, remark) VALUES (#{orderNo}, #{userId}, #{amount}, #{remark}) </insert>或者在应用层对空值做校验,提前拦截。这种修复方式的好处是让“数据完整性”的语义留在应用逻辑里,坏处是要动代码、发版。对于一两个字段的问题,动代码有时候显得“重”,但从数据质量角度考虑,核心字段必须走这条路。
4.3 方案 C:调整 MySQL 的 sql_mode 策略
很多人一遇到这个错就关严格模式。我觉得这需要谨慎。
直接执行:
SET GLOBAL sql_mode = 'ONLY_FULL_GROUP_BY,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION';把STRICT_TRANS_TABLES去掉,确实能消除报错。但代价是:以后所有“NOT NULL 无默认值”的插入都会静默写成空字符串或 0,数据质量风险直线上升。对线上核心业务表,我不建议这么做。
有一种相对可控的折中方案:只对某个会话调整。比如在应用连接初始化时:
SET SESSION sql_mode = 'NO_ENGINE_SUBSTITUTION';这样只影响该连接,不改变全局行为。但这要求你对业务很有把握,知道这些静默值不会造成脏数据。
4.4 各方案的适用场景与优缺点对比
为了让大家一眼看清差异,我整理了一个对比表:
| 修复方案 | 适用场景 | 优点 | 缺点 | 推荐度 |
|---|---|---|---|---|
| 补 DEFAULT | 字段允许有业务默认值 | 改动小、不涉及代码 | 业务语义易被隐藏 | 高 |
| 改 INSERT/代码 | 字段业务上必须有值 | 数据完整性由应用保证 | 需要发版 | 高 |
| 关掉严格模式 | 临时救急、非核心库 | 见效快 | 脏数据风险大 | 低 |
| 会话级禁用严格模式 | 局部任务、特定连接 | 影响面小 | 需应用改造连接 | 中 |
说实话,我个人的实践原则是:先补默认值,排查完业务后再决定要不要改代码。如果这个字段只是日志、备注类的可选信息,补默认值就够了;如果是金额、状态这类核心字段,那必须让应用显式传值,否则后期查数时一个0会把你带到沟里去。
5. 进阶场景:触发器、存储过程与批量导入中的坑
很多人以为把表和 INSERT 检查完就万事大吉,但我在实际排障中,至少三次遇到“源头不在 INSERT 本身”的情况。
5.1 触发器中触发的不默认值报错
MySQL 的触发器是在 INSERT/UPDATE/DELETE 前后自动执行的一组 SQL。如果触发器里对一张“NOT NULL 无默认值”的表执行了 INSERT,或者给某个字段赋值时漏了字段,报错也会从触发器里冒出来。
举个例子,用户表每次更新后要在审计表里记录日志:
CREATE TABLE user_audit ( id INT AUTO_INCREMENT PRIMARY KEY, user_id INT NOT NULL, action VARCHAR(32) NOT NULL, detail VARCHAR(255) NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE TRIGGER trg_user_audit AFTER UPDATE ON user FOR EACH ROW BEGIN INSERT INTO user_audit (user_id, action) VALUES (NEW.id, 'UPDATE'); END;这个触发器向detail字段没有赋值,如果detail是 NOT NULL 且无默认值,一个看似正常的 UPDATE 就会报:
ERROR 1364 (HY000): Field 'detail' doesn't have a default value排查时如果只看应用层的 UPDATE 语句,永远找不出问题。我建议每次遇到这个报错,先顺手查一下相关表的触发器:
SHOW TRIGGERS LIKE 'user_audit';5.2 存储过程动态 SQL 里的隐藏炸弹
存储过程里如果使用 PREPARE / EXECUTE 动态拼接 SQL,字段列表是运行时才确定的,非常容易出现“某些分支漏字段”的情况。更隐蔽的是,存储过程中用了临时表,临时表的结构继承了另一个表的 NOT NULL 约束,而后续插入时又没带全字段。
我的建议:存储过程插入前,先用一个显式变量补齐默认值:
SET @remark = IFNULL(p_remark, ''); INSERT INTO user_order (order_no, user_id, amount, remark) VALUES (p_order_no, p_user_id, p_amount, @remark);这样即使外部传入的存储过程参数为 NULL,也能被应用层逻辑转化为业务认可的默认值,而不是把锅甩给 MySQL 的隐式默认值。
5.3 批量导入(LOAD DATA / mysqldump)时的 NULL 处理
用LOAD DATA INFILE导入 CSV 时,如果文件里某列为空字符串,而表字段是 NOT NULL 且无默认值,LOAD DATA 的行为和 INSERT 不一样,空字符串会尝试写入,但如果是\N(NULL 的表示),就可能触发报错。
导入前先做数据清洗,或者在 LOAD DATA 语句里指定缺失值替换:
LOAD DATA INFILE '/tmp/orders.csv' INTO TABLE user_order FIELDS TERMINATED BY ',' (@order_no, @user_id, @amount, @remark) SET order_no = @order_no, user_id = @user_id, amount = @amount, remark = IFNULL(@remark, '');另外,mysqldump导出的备份在导入新库时,如果目标库的 sql_mode 更严格(比如备份时非严格,导入时严格),也会出现类似报错。多环境同步时,我会固定统一的 sql_mode 配置,避免环境差异带来的各种奇怪问题。
6. 防复发建议:建表规范、工具习惯与排错清单
走到这里,该聊点“避免下次再犯”的东西。
6.1 建表阶段的 DEFAULT 设计规范
现在团队每次评审建表语句,我都会要求三件事:
NOT NULL字段必须显式写DEFAULT;- 允许为空的字段,明确
DEFAULT NULL还是干脆不加 NOT NULL; - 时间字段尽量用
DEFAULT CURRENT_TIMESTAMP,避免历史数据时间缺失。
这条规范的核心逻辑很简单:不要依赖 MySQL 的隐式默认值,它只该是个兜底,不该成为日常依赖。只要表结构里所有 NOT NULL 字段都有显式 DEFAULT,这个报错的概率几乎为零。
6.2 排查这类报错的通用思路清单
以后遇到Field 'xxx' doesn't have a default value,我建议按这个顺序过一遍:
SHOW CREATE TABLE看目标表,找出所有 NOT NULL 且无 DEFAULT 的字段;- 检查报错的 INSERT/UPDATE 语句是否遗漏了这些字段;
- 如果 INSERT/UPDATE 语句没遗漏,继续查触发器、存储过程、事件;
- 查看当前会话和全局的 sql_mode,确认是否是严格模式;
- 用最小化用例复现,确认是表结构问题还是语句问题;
- 根据业务决定:补默认值、改代码,还是调整模式。
这个清单看起来简单,但能避免大部分无谓的折腾。尤其是在团队协作环境里,别人建的表你接手时并不清楚每个字段的约束细节,按这个清单走一遍基本能定位。
6.3 我个人的实际教训
最后分享一个我踩得比较深的坑。有一次因为赶项目,我在一个核心表里设计了score INT NOT NULL,当时想着“分数必须有值”,结果漏写了 DEFAULT。上游数据偶尔会漏传这个字段,刚开始测试环境没暴露,因为测试环境的 sql_mode 被某位同事在全局改过,严格模式被关掉了,INSERT 一直静默写入 0。上线后生产环境是严格模式,业务一跑就报错,那叫一个手忙脚乱。
后来我总结出一个非常重要的经验:测试环境、预发环境、生产环境的 sql_mode 必须保持一致,否则很多“环境差异”问题会被测试覆盖掉。当团队统一了 sql_mode 和建表规范,这类错误其实可以完全避免。
还有一个小技巧是,在排查这类报错时,把SHOW WARNINGS用起来。非严格模式下 MySQL 虽然不报错,但会在 warning 里记录隐式默认值的替换行为。执行完 INSERT 后立刻查一下:
SHOW WARNINGS;如果看到类似“Data truncated”或者“Field 'xxx' doesn't have a default value”的 warning,说明当前的 SQL 在严格模式下一定会报错,只是现在被静默处理了。用这个办法,可以提前在测试环境揪出一批潜在问题,而不是等生产环境炸了再救火。
这篇内容从根因、诊断、修复到进阶场景都拆了一遍,希望能帮到还在被这条报错折磨的朋友。如果你在排查中遇到了跟文中不太一样的表现,很可能问题出在触发器或环境差异上,按我上面的清单一步步来,大多数情况下都能快速定位到那一两个隐藏字段。