资讯动态

MySQL ERROR 1364 报错排查:NOT NULL字段无默认值的原理与修复

发布时间:2026/10/22 10:59:30 来源:尧图企业网站定制
说真的ERROR 1364 (HY000): Field xxx doesnt have a default value这个报错凡是写过一段时间后端的人基本都被它问候过。它翻译成人话就是你执行INSERT的时候漏掉了一个字段而这个字段恰好在表结构里被定义成NOT NULL、又没有默认值数据库不知道该往这个字段里填什么只能把整条SQL打回来。这个报错在本地开发环境偶尔不出现但一上生产就炸常常伴随接口500、批量任务中断、数据写到一半等一系列连锁反应。很多同学一开始会直接怀疑是数据库配置不对劲或者干脆给字段乱加默认值结果问题不但没根治还埋了更深的坑。这篇文章把我这些年和这个报错打交道的经验完整梳理了一遍从报错原理、高频触发场景到一套可复用的定位和修复路径再到和它长得像的“兄弟报错”希望能让新手少走弯路也让有经验的同学多一个排查参考。1. 先弄清ERROR 1364到底在抱怨什么1.1 报错形态与SQL mode机制先看一个最典型的报错现场INSERT INTO product(name, price) VALUES (测试商品, 9.9); -- 如果 product 表里有个 category_id 字段int 类型NOT NULL且没有默认值 -- 上面这条SQL就会得到 -- ERROR 1364 (HY000): Field category_id doesnt have a default value注意报错原文里的Field XXX是占位符实际会替换成具体的字段名。所以遇到这个报错时第一件事永远是搞清楚这个XXX到底是谁。关键机制藏在sql_mode里。MySQL有一个全局参数叫sql_mode它决定数据库以多严苛的方式执行SQL。其中有一项叫STRICT_TRANS_TABLES翻译过来就是“事务表严格模式”。这个模式开启后如果插入的数据违反了表的约束比如字段没有默认值、字符串超长、数字溢出MySQL会直接报错并拒绝执行。如果关闭严格模式MySQL不会报错而是会使用“隐式默认值”硬塞一个值进去数字类型填0字符串类型填空字符串时间类型填 0000-00-00 00:00:00可以插入NULL的字段则填NULL。这听起来好像更“宽容”但代价是脏数据会悄悄落库让人防不胜防。从MySQL 5.7开始STRICT_TRANS_TABLES已经默认包含在sql_mode里MySQL 8.0 同样如此。所以现在新部署的数据库只要业务代码、表结构有一处配合不上这个1364报错就会非常高频地出现。1.2 字段状态决定是否触发1364要搞懂为什么有的字段报错、有的字段不报错核心是看字段定义里NOT NULL和DEFAULT的组合关系字段定义插入时不传该字段的行为是否触发1364id int NOT NULL AUTO_INCREMENT自增主键自动生成不触发name varchar(64) NOT NULL无默认值严格模式下报错触发price decimal(10,2) NOT NULL DEFAULT 0.00自动填0.00不触发remark varchar(255) DEFAULT NULL自动填NULL不触发status tinyint NOT NULL DEFAULT 1自动填1不触发create_time datetime DEFAULT CURRENT_TIMESTAMP自动填当前时间不触发最容易踩坑的组合就是“NOT NULL 不写DEFAULT”。这种字段在业务语义上通常是“必填字段”数据库层面拒绝为空但如果你在INSERT语句里漏写了它严格模式下就会直接报1364。这里有一个容易混淆的点有些人以为DEFAULT NULL和“没有默认值”是一回事其实完全不同。DEFAULT NULL明确声明默认值是NULL所以插入时不传该字段MySQL会安全地填一个NULL进去不会触发1364。而“没有默认值”意味着MySQL根本不知道该怎么办只能按严格模式规则报错。还有一类特殊字段需要注意TIMESTAMP类型。在老版本MySQL里如果explicit_defaults_for_timestamp参数为OFF默认值表中第一个TIMESTAMP列在没有显式定义默认值时会自动使用CURRENT_TIMESTAMP作为默认值。这导致一些老表的建表语句看起来“没有默认值”但实际上有一个隐式默认值。后面我会专门讲这个隐性规则在MySQL 8.0里发生了变化是一个容易被忽略的兼容性坑。2. 高频触发场景从漏字段到环境差异2.1 动态SQL把字段“吃”掉了我见过最多的1364场景不是人为手写SQL漏字段而是ORM框架搭出来的动态SQL把字段“吃”了。以MyBatis为例很多人写INSERT用的是动态拼接insert idinsertProduct parameterTypecom.example.Product INSERT INTO product trim prefix( suffix) suffixOverrides, if testname ! nullname,/if if testprice ! nullprice,/if if testcategoryId ! nullcategory_id,/if /trim trim prefixVALUES ( suffix) suffixOverrides, if testname ! null#{name},/if if testprice ! null#{price},/if if testcategoryId ! null#{categoryId},/if /trim /insert这段代码的逻辑是实体类的某个字段为NULL时就不把它拼进INSERT语句。这本身是一种省事的写法省去了“每次必须传全字段”的约束但问题也随之而来数据库表加了category_id字段后如果实体类里categoryId是nullMyBatis就会把这一列整体忽略SQL变成INSERT INTO product(name, price) VALUES (...)接着数据库就在严格模式下报1364。类似的情况在insert on duplicate key update、insert ... select以及批量拼接SQL时也会出现。而且这个问题的触发往往不是稳定复现只有特定业务分支、特定参数为null时才出现。排查这类问题时最有效的办法是把完整SQL打出来看一遍对照表结构逐列比对。2.2 表结构重构后老代码成了定时炸弹第二种高频场景出现在数据库表结构调整之后。项目迭代过程中有人为了“列必须有值”这个规范把一个原本有默认值的字段改成NOT NULL且去掉默认值但没有同步排查所有写入方。比如某个表原本是status tinyint NOT NULL DEFAULT 1后来DBA认为状态字段必须由业务显式传递不允许依赖默认值于是执行了ALTER TABLE product MODIFY COLUMN status tinyint NOT NULL;这一改所有老代码里凡是没给status赋值的INSERT语句瞬间开始报1364。更麻烦的是这类字段往往还在历史代码的不同分支里被多次引用排查起来工作量不小。遇到这种场景我建议先把“表结构为什么要这样改”这件事拿出来重新评估。如果一个字段的默认值本身是合理的、业务上允许缺省那就不该为了“规范”而强行去掉默认值。只有业务上确实要求每次插入都必须显式传值的字段才值得做成NOT NULL且无默认值。否则用这种约束换来的不是数据质量而是一堆报错和脏数据补偿逻辑。2.3 批量导入和目标表结构对不上第三种场景和“数据导入、数据迁移、ETL”相关。比如从Excel导入数据到业务表或者从旧库迁移数据到新库源表和目标表的字段结构常常有差异。比较典型的情况是老库里某个字段允许NULL迁移到新库时被设计成了NOT NULL且无默认值而导入工具生成的INSERT语句只列举了它关心的少数几个字段。结果导入脚本一执行半路就报1364。批量导入场景还有另一层风险如果导入工具是一条条执行INSERT前面几条已经成功提交遇到某一条报错才会中断。这时候日志里只显示最后一条报错但库里已经混进了大量“成功”的数据后续清理比处理报错本身还麻烦。所以做批量导入时我一直强调要先把表结构对齐把所有必填、无默认值的字段都显式列进INSERT语句里再考虑跑批。2.4 开发环境正常、生产环境狂报错第四种场景最让人抓狂同一个功能开发环境跑得好好的一到生产环境就报1364而且完全无法本地复现。碰到这种情况很多人的第一反应是“数据有问题”但真相往往是两个环境的sql_mode配置不一致。比如开发库为了兼容某些历史项目全局设置了比较宽松的sql_mode去掉了严格模式而生产库用的是默认配置STRICT_TRANS_TABLES依然开着。同样的漏字段SQL在开发库不报错、在生产库报错。这类问题光靠人肉对配置很难发现我建议把数据库参数比对也纳入发布流程。每次新环境上线前至少确认一下sql_mode、character_set_server、collation_server这几个关键参数和既定规范是否一致。避免“环境依赖”吃掉代码本身的问题也更符合可重复部署的运维目标。3. 一套可复用的定位与修复路径3.1 快速定位是哪个字段在作妖碰到1364报错我习惯按下面这个顺序走基本不会跑偏先看应用的完整错误日志。很多ORM框架会打印出执行失败的SQL比如MyBatis会在日志里输出### Error updating database. Cause: java.sql.SQLException: Field xxx doesnt have a default value以及对应的SQL语句。从这条SQL里找出字段列表把报错提到的字段名对照一下很快就能判断是“SQL真的漏了这个列”还是“列名拼错了”。接着对可疑表执行SHOW CREATE TABLE product;重点看报错字段的定义里是不是同时存在NOT NULL和“没有DEFAULT子句”。如果确认是再检查INSERT语句的列清单里是否包含了这个字段。如果你怀疑的字段不止一个或者表很多、不想逐张看可以直接查information_schema.COLUMNSSELECT TABLE_NAME, COLUMN_NAME, COLUMN_DEFAULT, IS_NULLABLE, COLUMN_TYPE, EXTRA FROM information_schema.COLUMNS WHERE TABLE_SCHEMA 你的数据库名 AND IS_NULLABLE NO AND COLUMN_DEFAULT IS NULL AND EXTRA NOT LIKE %auto_increment% AND TABLE_NAME NOT LIKE sys_%;这条SQL会把指定库中所有“NOT NULL且无默认值且不是自增列”的字段全部捞出来。虽然这条SQL不能100%定位出具体哪条INSERT缺了字段但它能帮你把库里的“高危字段”一次看清楚是排查阶段特别省力的工具。3.2 确认SQL mode别凭记忆猜定位到字段之后下一步要确认当前会话和全局的严格模式状态。执行SELECT GLOBAL.sql_mode; SELECT SESSION.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或STRICT_ALL_TABLES就说明严格模式开着。这种情况下任何“NOT NULL且无默认值”的字段如果没被INSERT覆盖到必然触发1364。有一点需要提一下MySQL的sql_mode支持全局和会话两个层级。连接池、代理中间件、或者某个管理工具都可能临时修改会话级别的模式。所以排查时不要只看GLOBAL.sql_mode一定把会话级别也查一遍。如果你发现全局和会话不一致那说明中间肯定有“人”动过手脚顺着这个线索查往往会有意外发现。3.3 三种修复方案怎么选确认了问题字段是哪一个、严格模式是不是开着之后就到了做选择题的时候。我通常把人分成三种方案方案一改业务代码显式补上字段值。这是最推荐的治本方案尤其适合那些业务语义上“必须有值”的字段比如状态、类型标识、业务主键等。修法就是在INSERT语句、ORM实体类、动态SQL拼接逻辑里把这个字段明确传上去让数据库拿到真实的值。方案二给字段加一个合理默认值。适合那些“缺省无所谓”的业务字段比如创建时间、逻辑删除标记、排序值、状态值等。执行SQL也比较简单ALTER TABLE product ALTER COLUMN status SET DEFAULT 1;用ALTER COLUMN ... SET DEFAULT的好处是只添加默认值不会动字段本身的类型、约束比MODIFY COLUMN安全很多。MODIFY COLUMN必须把完整字段定义重写一遍一旦漏掉某个约束就可能出问题。方案三调整sql_mode临时关闭严格模式。这是应急方案我不建议长期使用。具体操作可以先把当前sql_mode里的严格项去掉SET SESSION sql_mode(SELECT REPLACE(sql_mode,STRICT_TRANS_TABLES,));如果确实需要全局调整再执行全局版本并把修改持久化到配置文件里。但要清楚关闭严格模式之后那些缺字段的INSERT不会报错而是会往列里塞0、空字符串、NULL这类隐式值。这种“看似成功”很容易掩盖真实的数据质量问题比报错更让人头疼。3.4 怎么快速做优先级判断把三种方案摆在一起时我一般按这个优先级来定如果报错缺的字段在业务上必须有值比如订单的支付状态、类目的上级ID那么优先改代码让数据显式传递数据库不要担这个责任。如果字段业务上允许缺省那给表结构加一个语义正确的默认值收益最高、改动最小。只有线上故障非常紧急、需要先恢复服务才适合临时调整sql_mode并且要当作“补丁”而不是“根治”事后必须补代码或表结构修复。记住一个原则1364报错本质上是数据库在替你拦截“逻辑不完整的数据”。虽然它给你添了很多麻烦但它其实在保护你的数据质量。最怕的不是它报错而是你为了消停直接关掉这只看门狗。4. 复盘一次线上“批量导入非空字段”连环故障4.1 故障现象与实际影响某次项目里一个电商后台的类目管理模块要做批量导入。表结构里有个排序字段sort_order int NOT NULL这个字段没有默认值设计初衷是“运营必须给每个类目指定排序值”不允许缺省。后端用MyBatis写了一条动态INSERT把sort_order也拼了进去。运营在导入Excel时有一部分行没有填“排序”列。后端读取到这些行后实体类的sortOrder是nullMyBatis动态SQL一判断就把sort_order从INSERT列清单里剔除了。于是这些行落库时SQL就变成INSERT INTO category(name, parent_id, status) VALUES (测试类目, 0, 1);线上数据库是默认严格模式执行到这一条时直接爆出java.sql.SQLException: Field sort_order doesnt have a default value批量任务中断运营那边看到导入进度卡住页面一直转圈反馈很快变成了“线上Bug”。4.2 排查过程和连环坑我们拿到日志后先定位到具体SQL发现INSERT列清单里确实没有sort_order于是去查SHOW CREATE TABLE确认sort_order是int NOT NULL且没有默认值。再查sql_mode线上是默认的严格模式链路就通了。排查过程中有一个特别容易误导人的点这个任务是一行一行循环插入的不是用一条多值INSERT批量插入。结果就是前几千行因为有排序值、插入成功遇到空值行才报错停住。日志里只显示最后一条报错但从业务上看库里已经混入了一批“成功”的数据。如果以为整个任务失败了全部回滚那就要出大问题。这个“部分成功、部分失败”的状态非常恶心。最后我们花了额外的精力去核对已入库的数据确认哪些是导入期间产生的哪些是原来的数据再决定保留还是清理。4.3 处理结果与后续改进核心结论是这个字段不允许缺省所以不能简单地给sort_order加默认值掩盖问题。最终修复落在代码层导入逻辑里加了必填校验sortOrder为null时直接拒绝该行并提示运营在哪一行、哪一列有问题同时调整了MyBatis的INSERT映射不再通过if动态剔除非空字段而是显式传给sort_order保证数据库始终能看到这一列。排障之后我们复盘又补了两个改进一是批量入库要控制事务边界不要在循环里逐条提交至少要按批次包事务失败整批回滚避免出现“一半成功、一半失败”的局面二是动态SQL剔除NULL字段这个功能虽然省事但对NOT NULL且无默认值的列来说非常危险以后新表设计时这类列要么保证代码永远传值要么干脆给它一个默认值兜底。5. 容易混淆的几个“亲兄弟”报错5.1 Data truncation 和 Incorrect value 其实是一家人排查1364的时候你很容易在日志里碰到另一些报错看起来毫不相关实际上根源都指向sql_mode严格模式。ERROR 1406 (22001): Data too long for column xxx是字符串写入长度超出字段定义时报的错。比如varchar(50)的列塞了60个字符非严格模式下会被截断严格模式下直接报错。ERROR 1366 (HY000): Incorrect integer value: abc for column xxx则是往整型列写入字符串时报的错。类似地往日期列写入非法格式也会得到相关报错。这三类报错本质上都是“传入的数据不符合字段类型/约束”。排查思路也一致先把SQL打出来看再核对字段定义最后检查sql_mode。区别只在于1364是“根本没传列”而1406、1366是“传了但内容不合法”。5.2 “Cannot be null”其实和1364不是一回事另外一个容易搞混的报错是ERROR 1048 (23000): Column xxx cannot be null。同样是NOT NULL字段1364是INSERT语句压根没包含这一列而1048是INSERT语句里带了这一列但传的值是NULL。举一个例子INSERT INTO product(name, price, category_id) VALUES (测试商品, 9.9, NULL); -- ERROR 1048 (23000): Column category_id cannot be null一条是“字段缺席”一条是“字段Present但值为NULL”修复方式完全不同。1364要补的是字段1048要改的是参数校验或者在代码里给这个字段一个真实值。排查时只要把SQL和实体类里的传参对照一遍很容易就能区分。5.3 从MySQL 5.7迁到8.0才出现的默认值变化还有一个和默认值密切相关的坑专门出现在老库升级到MySQL 8.0的场景里。早期版本explicit_defaults_for_timestamp默认是OFFTIMESTAMP字段在没有显式默认值时会走一套“隐式规则”表中第一个TIMESTAMP列自动获得CURRENT_TIMESTAMP作为默认值。很多老项目的建表语句长这样create_time timestamp NOT NULL从字面看这个字段没有默认值但实际跑到老版本MySQL上插入时不传这个字段也不会报错因为数据库自动用当前时间填了。MySQL 8.0把explicit_defaults_for_timestamp默认改成了ON这套隐式规则失效了。于是同一个表、同一条INSERT从5.7迁移到8.0之后突然开始报1364。处理方式很简单给TIMESTAMP字段显式加上默认值不要依赖隐式规则ALTER TABLE product ALTER COLUMN create_time SET DEFAULT CURRENT_TIMESTAMP;把这条经验写进迁移预检清单里能在数据库升级的时候省去不少半夜救火的痛苦。6. 预防手段与长期习惯6.1 建表字段设计建议回顾这些1364事故绝大多数在“建表设计”阶段就能避免。我在这里给出一套自己长期在用的建表字段约定id一律用BIGINT UNSIGNED AUTO_INCREMENT或BIGINT类型并作为主键。业务逻辑不区分状态时所有状态字段必须有默认值比如status TINYINT NOT NULL DEFAULT 1。排序字段如果允许缺省就设置默认值如果业务要求必填就不要默认值并且在代码层做必填校验。时间字段统一写DEFAULT CURRENT_TIMESTAMP更新时间字段再额外加上ON UPDATE CURRENT_TIMESTAMP。逻辑删除标记字段一律默认0别留空。创建人等审计字段如果系统有统一用户体系可以用默认值否则保持无默认并确保代码必填。这样设计的目的不是一刀切“所有字段必须有默认值”而是让默认值反映真实的业务缺省语义能缺省的就给默认值不能缺省的就当必填处理把约束和校验放在合适的一层。6.2 评审和发布前检查清单光是建表设计好了还不够代码和表结构的变更往往是不同步的。我建议在每次发版前把下面几个点过一遍新增字段时搜索所有相关的INSERT、UPDATE语句逐个确认字段是否会被正确的值填充。动态SQL拼接场景要格外小心重点看if判断会不会因为NULL把字段吃掉。实体类新加的字段默认值和数据库定义的默认值要保持一致。数据库变更脚本如果涉及修改字段约束要评估对存量写入链路的影响。一套评审流程不是万能的但至少能让大多数1364在代码评审阶段就暴露出来而不是等线上用户踩到。6.3 字段体检SQL与配置一致性检查最后分享一个我每次上线前都会跑的“字段体检SQL”。把前面提到的information_schema.COLUMNS查询整理成一个固定脚本发版前扫一遍所有业务表凡是查出“NOT NULL且无默认值且非自增”的字段逐个人工确认一遍是否有业务兜底。这个动作成本很低但效果一直都很好。同时把sql_mode的检查放进部署核对项里每个环境的配置都以一套基准参数为准避免开发库宽松、生产库严格这类环境差异问题。我个人和1364打交道的最大体会是这个报错并不复杂真正费时间的往往是“业务语义判断”。当报错指向某个字段时不要急着改库、改配置先想清楚这个字段缺省到底意味着什么。如果缺省会导致业务逻辑不完整那就在代码里补上必填逻辑如果缺省没有影响那就在表结构里给一个干净合理的默认值。搞清楚这两条边界1364基本就追不上你了。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑