前阵子有朋友问我:“MySQL 的 INSERT 不就是一行 SQL 吗?有什么好研究的?”我当时没直接回答,反问了他一句:“如果你把一条 INSERT 写进一个高并发订单系统里,服务端突然报死锁,你知道要看哪个参数吗?如果你 insert 一条数据,程序没报错,但表里就是没有,你能在五分钟内定位吗?”他沉默了。
其实很多看似基础的知识点,恰恰是线上故障的第一来源。MySQL 的 INSERT(插入数据) 语法看起来简单,但往下挖,里面有默认值规则、自增机制、锁竞争、事务隔离、批量性能、变体语法(INSERT IGNORE、ON DUPLICATE KEY UPDATE、REPLACE INTO)等一堆门道。这篇内容我就从实际使用的角度,把 INSERT 相关的知识完整过一遍,从语法细节到性能优化,再到真实排错案例,最后聊聊面试里那些容易被问倒的点。无论你是刚接触 MySQL 的初学者,还是写了几年业务代码但没系统梳理过数据库细节的开发,这篇都值得花十几分钟看完。
1. 从一行 INSERT 说起:那些你以为理所当然的默认行为
1.1 完整的 INSERT 语法形态
先看最标准的形态:
sql复制INSERT INTO t_user (id, name, age) VALUES (1, '张三', 20);
这个写法大家都会,但我想强调几个平时容易忽视的细节。
第一,字段列表可以省略,但省略后必须按表结构里所有字段的顺序给值,少一个都不行:
sql复制INSERT INTO t_user VALUES (1, '张三', 20);
这种写法在实际项目里我非常不建议用,原因是表结构一旦调整(新增字段、调整顺序),这条 SQL 直接报错或错位插入,排查起来很痛苦。而且代码评审的时候,别人根本看不出你插的 1、张三、20 分别对应哪个字段。显式列出字段名,维护成本会低得多。
第二,MySQL 还有一个非标准但很实用的 INSERT ... SET 语法:
sql复制INSERT INTO t_user SET name = '李四', age = 25;
这种写法在某些 ORM 框架的底层会用到,适合插入的字段比较少、可读性要求高的场景。它的可执行效率和 INSERT ... VALUES 基本一致,没有性能差别。
第三,多值插入(也叫扩展插入)也是标准语法的一部分:
sql复制INSERT INTO t_user (name, age) VALUES
('王五', 22),
('赵六', 23),
('孙七', 24);
一条语句插入多行,是日常批量写入最常用的方式,后文我会专门讲它的性能优势。
1.2 没写进去的列到底填了什么
这是新手最容易误解的地方。执行 INSERT 时,如果某个字段既没出现在字段列表里,也没在 SET 里赋过值,MySQL 会按这条规则处理:
- 如果字段定义了
DEFAULT值,就填入默认值; - 如果字段没定义默认值,但允许
NULL,就填入NULL; - 如果字段既没默认值,又设置了
NOT NULL,在不报错的情况下,MySQL 会根据数据类型选择一个隐式默认值(比如整数填 0、字符串填空串、时间戳填当前时间)。
重点说一下第三种。这个“隐式默认值”行为受 sql_mode 控制。如果 sql_mode 里包含 STRICT_TRANS_TABLES(默认通常是有的),插入时报错:
sql复制ERROR 1364 (HY000): Field 'age' doesn't have a default value
若在非严格模式下,MySQL 会悄悄填一个隐式默认值进去。这种“悄悄填”非常危险,你本来以为 age 必须有值,结果插进去全是 0 或 NULL,后续统计全部失真。所以生产环境开启严格模式,是非常必要的基线配置。
1.3 sql_mode:宽松模式才是最隐蔽的坑
聊到严格模式,就展开多说几句 sql_mode,因为它直接影响 INSERT 的成败。
我见过一个真实案例:某个系统从测试环境迁移到生产环境,测试环境用的 MySQL 5.7 默认 sql_mode,生产环境被人手动改成了空。结果线上往一个 DECIMAL(10,2) 字段插 1000.126,严格模式会四舍五入成 1000.13 并给 warning,非严格模式直接插成功,但小数部分被截断,账面金额对不上,财务查了好几天才发现是数据库配置的问题。
检查当前 sql_mode 用这条:
sql复制SELECT @@sql_mode;
推荐在配置文件里显式设置,不要依赖默认值:
ini复制[mysqld]
sql_mode = "STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION"
这套设置能保证:插入非法日期、除零、字段超长等情况都会报错而不是静默处理。日常开发中,让数据库把问题暴露在当下,远比让它把错误藏进数据里要好。
1.4 自增列的手动插入会把序列顶到哪里去
还有一个容易被忽略的点:向 AUTO_INCREMENT 列手动指定值。
sql复制INSERT INTO t_user (id, name) VALUES (100, '测试');
如果你手动插入了 id=100,那 MySQL 的自增计数器会直接跳到 101。后续再插入不指定 id 的数据时,会从 101 开始,而不是 1。
这个行为本身是设计如此,但它的副作用是:当你把一张表的数据导入导出、清洗数据后,再插入新数据时,id 可能一下子跳到一个很大的数字。 有些人误以为这是“ID 泄露”或“数据错乱”,其实不是。如果确实需要重置自增计数,可以用:
sql复制ALTER TABLE t_user AUTO_INCREMENT = 1;
但注意,这个语句只会把计数调整为“大于当前表内最大 id”的值,如果表里已经有 id=100 的数据,你设成 1 也不会生效。另外在高并发插入场景下,自增锁的分配策略会影响跳号情况,这在后面第 4 章会详细讲。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
