简介:SQL插入数据是日常数据库操作的高频场景,这份PDF文档面向数据库初学者与需要规范化写法的开发人员,系统梳理了INSERT INTO VALUES、INSERT INTO SELECT、以及省略目标列的简写写法三种常用方式,并结合T-SQL与PL/SQL环境给出语法对比与注意事项。资源共1个文件,为34KB的PDF格式,便于随时查阅与离线学习。已有664人学习下载,适合在面试复习或实际开发前快速补齐插入操作的细节知识。文档不仅说明了各方法的适用场景,还重点强调了检查列约束、批量插入、事务处理、错误处理及性能优化等小贴士,并针对省略目标列时SELECT顺序必须与表结构一致这一易错点做了专门提醒。通过这一份笔记,读者可以快速掌握不同插入语句的选择依据,避免因主键冲突、非空约束未满足或列顺序错位而导致的插入失败。
1. SQL插入数据看着简单,翻车都在细节上
写SQL的插入数据,可能是大多数人第一个写熟的语句,但真正上了生产,坑全在细节里:批量插入拆错批、自增ID跳号、字符串拼接被注入、跨库迁移死活插不进去。这篇文章把三种最常用的插入方法——VALUES、INSERT INTO ... SELECT、UPSERT(冲突更新)从原理讲到落地,每段代码都标注了适用场景和参数,绕开我踩过的那些坑。适合写业务接口的开发、做数据同步和批处理的数据工程师,以及刚接手数据库方向的运维。
2. VALUES插入:从单行到批量,先把基础打牢
VALUES是最原始的插入方法,却撑起了从单元测试到生产环境的大部分写入操作。它看着简单,但字段顺序、默认值、自增主键返回值、批量上限,每一项都能影响线上行为。这一章从单行开始,逐步改成多行批量,最后给出三个主流数据库拿自增ID的写法。
2.1 单行插入与字段顺序:别被NULL误导
最常见的单行写入是“INSERT INTO 表名 (字段...) VALUES (值...)”,字段列表与值列表按位置一一对应,不是按名字匹配。这个位置对应关系是很多翻车的源头:源表字段顺序调整了,目标表没调整,插入就会静默串列,数据错位还报不了错。所以不要依赖表结构的自然顺序,INSERT和VALUES两侧都显式写清楚字段是最稳的。
INSERT INTO user (name, age, created_at) VALUES ('张三', 30, NOW());这条语句的含义是把“张三”、30、当前时间分别写入name、age、created_at三列。NOW()是MySQL取当前时间的函数,PostgreSQL需要写成CURRENT_TIMESTAMP,SQL Server也是CURRENT_TIMESTAMP。参数说明:字段列表中的顺序决定了VALUES里每个值的去向,多一个少一个都会直接报列数不匹配;类型不一致时,很多数据库会尝试隐式转换,比如把字符串'30'转成数字30,但这种隐式转换在字符集或格式不可识别时会中断整个语句。
省略字段列表时,没写的列会自动用默认值,但很多新手把这个行为理解成“NULL就触发默认值”,实际不是。
INSERT INTO user (name, age) VALUES ('李四', 25);假设created_at列带有DEFAULT CURRENT_TIMESTAMP,这条语句里没写created_at,数据库会填入默认值;但如果写成INSERT INTO user (name, age, created_at) VALUES ('李四', 25, NULL),除非该列允许NULL,否则数据库不会把NULL替换成默认值,而是会尝试写NULL,最终报NOT NULL约束错误。显式想用默认值,只能写DEFAULT关键字。
INSERT INTO user (id, name, age) VALUES (DEFAULT, '王五', 28);DEFAULT关键字只适用于该列确实有默认值的场景。比如SQL Server里默认值GUID的列,如果漏掉字段,会由DEFAULT约束生成新GUID;但如果在VALUES里写成空字符串,数据库就会把空串存进这个列,而不是生成GUID。这里很容易被“漏传就自动补”的直觉骗了。
2.2 多行VALUES就是批量插入的变种
VALUES语法天然支持多行,把多组值用逗号连接,一次发给数据库执行。
INSERT INTO user (name, age) VALUES ('赵六', 32), ('孙七', 24), ('周八', 27);一次多行插入比循环单行插入快很多,原因是网络往返从N次变成1次,数据库日志的写入次数也大幅下降,事务日志的合并效果更明显。这个优势在ORM框架里同样适用,比如JDBC的addBatch、C#的SqlDataAdapter批量提交,底层都是把多条参数合并成一次发送。
但多行VALUES有边界。MySQL的max_allowed_packet限制单次SQL包大小,超过就报“Packet too large”;SQL Server的限制更直接,单个批处理的参数总数上限是2100个。假设每行10个参数,2100个参数最多只能批量210行,强行塞500行写给新手看的示例代码可能没事,落到生产就会翻车。所以批量插入的行数要按参数个数反推,而不是按感觉定。
我一般把批量行的参数总数控制在1900以内,给数据库留一点余地。如果单次需要插入几万行,拆分批次跑,比如每批500行,分批提交。这里的“提交”是事务语义,不是每批都COMMIT,后面避坑章节会展开讲。
2.3 插入后拿自增ID:三种数据库的写法
业务在插入数据后通常需要拿到自增主键,去构造关联数据。但不同数据库的写法差异很大,写错一个关键字,轻则拿不到ID,重则拿到别人那条数据的ID。
MySQL里最常用的是LAST_INSERT_ID(),它和当前连接绑定,不会被其他连接干扰,同一个连接里连续执行两条INSERT,第二次会覆盖第一次的结果:
INSERT INTO user (name) VALUES ('测试'); SELECT LAST_INSERT_ID();PostgreSQL从INSERT语句里直接返回ID,不需要二次查询:
INSERT INTO user (name) VALUES ('测试') RETURNING id;RETURNING后面可以写多个字段,包括计算表达式,这是PG最推荐的做法,因为它在同一条语句里原子完成插入和取值。SQL Server的写法更讲究:
INSERT INTO [user] (name) OUTPUT INSERTED.id VALUES ('测试');OUTPUT可以直接返回插入后的整行数据,比SCOPE_IDENTITY()更直观。注意SQL Server还有一个@@IDENTITY全局变量,它返回的是当前会话最后一次插入操作生成的所有表的ID,如果表上有触发器又往别的表插了数据,@@IDENTITY就会返回触发器那张表的ID,而不是你真正插入的user表。这个问题我在老项目里栽过,后来一律用OUTPUT或SCOPE_IDENTITY()收口。
3. INSERT INTO ... SELECT:从查询表复制数据的最稳路径
当要插入的数据来自另一张表或一段查询,而不是手工写死的常量,VALUES就不合适了。INSERT INTO ... SELECT能把查询结果直接灌进目标表,常用于归档、数据同步、报表中间表、把一个库的查询结果插入到另一个库里。它最大的优点是逻辑和数据落在同一条SQL里,事务一致性好;缺点是目标表的约束和源数据质量问题,会在批量执行时集中爆发。
3.1 把查询结果直接灌进目标表
语法上就是把VALUES替换成SELECT查询:
INSERT INTO order_archive (order_id, user_id, amount, create_time) SELECT order_id, user_id, amount, create_time FROM orders WHERE create_time < '2024-01-01';这条语句会把orders表里2024年以前的数据复制到order_archive归档表。目标表字段和SELECT列表按位置对应,不是按名字对应——这是最容易踩的坑:源表字段顺序调整过,或者SELECT列表改了顺序,INSERT还是会按位置硬塞,如果类型恰好兼容,数据就错位了。所以SELECT里的字段顺序必须和INSERT字段列表严格一致,不要写SELECT *。
如果目标表已经存在,用INSERT INTO ... SELECT;如果目标表不存在,常见做法是CREATE TABLE ... AS SELECT,比如MySQL的CREATE TABLE order_archive AS SELECT * FROM orders WHERE ...。这会把字段类型一起复制,但不会复制索引、约束、默认值。所以建完表后要单独补主键、唯一键和索引,否则后续插入时没有约束拦截,重复数据就直接落库。
还要注意约束的影响:源表里可能有一行数据在目标表违反唯一键或NOT NULL,整条INSERT会被中断。数据库执行INSERT ... SELECT是单条语句,遇到第一个违规行就报错回滚,前面插入成功的行也会一起没掉。这时候要么先清洗源数据,要么拆成小批次,让失败影响范围可控。
3.2 跨库和跨实例迁移时的写法
同实例跨库是最简单的跨库插入,MySQL直接写库名.表名:
INSERT INTO analysis.user_snapshot (user_id, user_name, updated_at) SELECT user_id, user_name, NOW() FROM user_center.user WHERE status = 1;这条语句从user_center库的user表里选出有效用户,写入analysis库的user_snapshot快照表。前提是两个库在同一MySQL实例下,账号具备两个库的权限。SQL Server的同实例跨库写法类似,用[database].[schema].[table]限定。如果目标库在另一台服务器,就需要先建Linked Server,把远程表映射成本地对象再执行INSERT SELECT,性能不会太好,网络延迟和数据量都会拖慢。
跨实例不是一条SQL能解决的问题,这是数据库同步工具的主场。常见做法是把源库数据导出成中间文件,再在目标库批量导入,或者用专门的同步工具做增量订阅。我不建议在业务代码里通过跨机房连接的SQL直插,网络抖动一次,整个批任务就挂在半路,而且很难定位断点。
3.3 插入前先去重的两个习惯
从线上表往归档表插数据时,重复执行同一条任务是最常见的重复数据来源。第一次跑完没记录进度,第二次跑全量,重复数据就进来了。两个习惯能挡掉大部分问题。
第一个习惯:在源查询里加GROUP BY,让结果集天然去重。
INSERT INTO order_archive (order_id, user_id, amount, create_time) SELECT order_id, MAX(user_id), MAX(amount), MAX(create_time) FROM orders WHERE create_time BETWEEN '2024-01-01' AND '2024-06-30' GROUP BY order_id;GROUP BY适合对聚合后的结果去重,比如按订单ID取任意一组字段值。但如果要保留“每个订单最新一条”,GROUP BY就不够精确,这时候用窗口函数。
第二个习惯:用ROW_NUMBER按业务键分组,取排序第一的那条。
INSERT INTO order_archive (order_id, user_id, amount, create_time) WITH ranked AS ( SELECT order_id, user_id, amount, create_time, ROW_NUMBER() OVER (PARTITION BY order_id ORDER BY create_time DESC) AS rn FROM orders WHERE create_time BETWEEN '2024-01-01' AND '2024-06-30' ) SELECT order_id, user_id, amount, create_time FROM ranked WHERE rn = 1;PARTITION BY order_id表示按订单ID分组,ORDER BY create_time DESC表示每个分组里最新时间排第一,rn=1就是最新那条。这个方法在数据同步场景里尤其好用,比如上游表每天更新状态,同步任务重复跑,只要把去重键设成业务主键,就能稳定拿到最新一条,不会重复插入。
4. 幂等写入用UPSERT:ON DUPLICATE KEY UPDATE 与 INSERT OR REPLACE
实际业务里“存在就更新,不存在就插入”比“先查再写”可靠得多。两个请求同时到达时,先查再写会查都查不到,然后都走插入,产生重复数据。UPSERT把判断放到数据库内部,通过唯一索引保证幂等。不同数据库的写法差异很大,这一章分别给MySQL、PostgreSQL、SQLite和SQL Server的落地写法。
4.1 冲突更新:MySQL 的 ON DUPLICATE KEY UPDATE
MySQL里实现幂等插入是ON DUPLICATE KEY UPDATE,触发条件是插入的行撞上唯一索引或主键。
INSERT INTO user (user_id, user_name, mobile) VALUES ('u1001', '张三', '13800138000') ON DUPLICATE KEY UPDATE user_name = VALUES(user_name), mobile = VALUES(mobile);VALUES(user_name)在MySQL 8.0.20之后已经标记为废弃,建议用新别名写法:
INSERT INTO user (user_id, user_name, mobile) VALUES ('u1001', '张三', '13800138000') AS new ON DUPLICATE KEY UPDATE user_name = new.user_name, mobile = new.mobile;别名写法可读性更好,和PostgreSQL的EXCLUDED语义接近。前提是user_id或mobile必须存在唯一索引,否则数据库无法判断冲突,ON DUPLICATE KEY UPDATE就不会触发。这条语句有个让新手困惑的返回值:插入新行,受影响行数是1;发生了更新,受影响行数是2;更新前后值没变化,受影响行数是0。ORM框架拿到这些返回值时,别简单理解成“影响了几条数据”。
如果业务只想要“有就跳过,没有才插”,可以写ON DUPLICATE KEY UPDATE user_name = user_name,利用值没变时受影响行数为0的特性,但更清晰的写法还是先判断唯一键在不在,或者用INSERT IGNORE——不过INSERT IGNORE会吞掉很多其他错误,比如字段太长、类型错误也一起忽略,不建议在生产上开这个口子。
4.2 PostgreSQL 的 ON CONFLICT 写法
PostgreSQL的UPSERT是ON CONFLICT,比MySQL更严谨,可以指定冲突目标。
INSERT INTO user (user_id, user_name, mobile) VALUES ('u1001', '张三', '13800138000') ON CONFLICT (user_id) DO UPDATE SET user_name = EXCLUDED.user_name, mobile = EXCLUDED.mobile;EXCLUDED代表本次试图插入的那行数据,EXCLUDED.user_name就是VALUES里传进来的user_name。ON CONFLICT (user_id)里的user_id必须是唯一索引或主键,否则语句直接报错。如果不想更新,只想跳过冲突行,写法是DO NOTHING。
这里有个细节:如果表上有多个唯一键,插入的数据同时撞上两个不同唯一键,ON CONFLICT没有指定冲突目标时会报错,因为数据库不知道按哪个约束执行更新。所以冲突目标一定要写,不要想着靠省略它来简化。PostgreSQL还支持ON CONFLICT ON CONSTRAINT约束名,在联合唯一键场景下比列名更精准。
把Oracle查询结果插入到PostgreSQL表时,要注意两边对主键冲突的处理习惯完全不同,Oracle的MERGE和PG的ON CONFLICT不能直接平移。迁移时最好在目标表上先建好唯一索引,再套用ON CONFLICT,否则同步任务会反复报重复键。
4.3 SQLite 与 SQL Server 的替代方案
SQLite有个很老的INSERT OR REPLACE INTO,写法简单:
INSERT OR REPLACE INTO user (user_id, user_name, mobile) VALUES ('u1001', '张三', '13800138000');但REPLACE的语义是“删除旧行,插入新行”,不是原地更新。副作用是自增ID变了,外键关联的明细表也会因为主键变化产生孤儿数据。SQLite 3.24之后也支持ON CONFLICT DO UPDATE,和PostgreSQL的风格接近,能用这个的新语法就不要用OR REPLACE。
SQL Server比较尴尬,没有标准ON DUPLICATE KEY UPDATE。老项目里常见的是MERGE:
MERGE INTO [user] AS t USING (SELECT 'u1001' AS user_id, '张三' AS user_name, '13800138000' AS mobile) AS s ON t.user_id = s.user_id WHEN MATCHED THEN UPDATE SET user_name = s.user_name, mobile = s.mobile WHEN NOT MATCHED THEN INSERT (user_id, user_name, mobile) VALUES (s.user_id, s.user_name, s.mobile);MERGE在并发和触发器场景下有已知的偶发问题,比如重复键和意外锁升级,而且在简单幂等写入时它的执行计划并不好看。如果业务复杂度不高,我更推荐在应用层做“先UPDATE,受影响行数等于0再INSERT”,或者显式IF EXISTS判断,两条语句包在一个事务里。这样写虽然多一条SQL,但行为可预期,排错也更方便。升级到新版本SQL Server后仍然没有像MySQL那样顺手的原生UPSERT,这个习惯可以一直保留。
5. 插入数据的避坑手册:5个让我熬夜的教训
插入数据报错没什么,怕的是报错之后数据已经错了一半,或者线上安静地写入了错误数据。下面5条全是真实生产环境里遇到过的,按现象、原因、解决写,帮你省几晚加班。
5.1 SQL注入不是只有拼接字符串才会中招
现象:某个后台管理接口,用户输入的内容直接拼进VALUES字符串,随后数据库出现越权数据查询,甚至有人用万能密码绕过了登录校验。原因:SQL语句是拼接出来的,用户的单引号闭合了原有语句,改变了语义。解决:所有参数一律走预编译占位符,Java用PreparedStatement,C#用SqlParameter,Python用cursor.execute(sql, params),不要用f-string或format拼SQL。
INSERT INTO user (name, email) VALUES (?, ?)数据库收到的永远是参数值,而不是SQL片段。字符串里的引号、分号、注释符都被当作普通字符处理。这个习惯要从第一行代码养成,别等数据库被拖库再补。
5.2 批量插入导致参数上限
现象:往SQL Server一次性INSERT 2101行,直接报参数过多错误;MySQL这边则报max_allowed_packet相关错误。原因:SQL Server单个批处理最多2100个参数,MySQL限制单次SQL包大小。行数多时参数也跟着多,撞上限不奇怪。解决:按每批500行拆批,或者用SqlBulkCopy、LOAD DATA INFILE这类专门导入接口。拆批时注意事务边界,不要拆完一批就提交一批,否则中途失败只能留下半批数据,后面对账会非常痛苦。
5.3 默认值与NOT NULL的“假象”
现象:表中created_at有DEFAULT CURRENT_TIMESTAMP,业务代码漏传了这列,插入却报“column cannot be null”。原因:INSERT语句里写了created_at = NULL,显式传NULL会覆盖默认值,数据库不会自动把NULL替换成默认值。解决:要么省略该列,要么显式写成DEFAULT。同理,SQL Server默认值GUID的列,漏传时由DEFAULT生成新GUID,但如果显式传了空字符串,数据库会存空串而不是生成GUID。这两个细节都是“看上去该自动处理,实际不会”的典型坑。
INSERT INTO user (id, name, created_at) VALUES (DEFAULT, '张三', DEFAULT);DEFAULT关键字能用在所有有默认值的列上,但前提是该列没有NOT NULL且没有外键约束。比如SQL Server 2022里,GUID列如果用DEFAULT NEWID(),上面写法就会生成新GUID,不会报错。
5.4 时间与字符串编码的隐性错误
现象:插入中文后查询变“???”,或者时间字段比预期差8小时。原因:连接字符集与目标表字符集不一致,MySQL里utf8和utf8mb4混用是重灾区;时间问题则来自驱动会话时区与数据库时区不对齐。解决:连接串显式指定charset=utf8mb4,表字段也用utf8mb4;时间统一按UTC存储,应用层按业务时区渲染,或者JDBC连接串加serverTimezone=Asia/Shanghai。这类错误不在SQL本身,排查顺序应该是先看连接参数,再看表结构,最后才查SQL。
5.5 自增主键跳号:不是bug也不是玄学
现象:事务回滚后,自增ID从1变成5,业务方以为发号器坏了。原因:自增计数器不随事务回滚,MySQL的InnoDB在事务回滚后不会把已分配的自增值退回去,SQL Server重启还会根据种子重算,导致更大跳跃。解决:如果业务要求ID连续,自增主键直接不适合,改用序列或显式赋值;如果只需要唯一,就把跳号当作正常行为。插入失败出现ID空洞很正常,不要额外去“修补”ID,越修越乱。
6. 高级玩法:事务批量提交与性能验证方法
插入性能优化不是玄学,核心是三件事:减少网络往返、合并日志提交、避开索引维护高峰。这章给两个马上能用的技巧和一个验证方法。
把成千上万条插入包在一个事务里,能把多次磁盘同步变成一次:
BEGIN; INSERT INTO user (name) VALUES ('a'), ('b'), ('c'); COMMIT;注意事务不是越大越好,一个事务控制在1万行以内比较安全,或者按主键范围分批提交。事务太长,锁和日志都会压垮主库。
验证插入速度时不要用代码里的Stopwatch,应该在数据库端看实际耗时。MySQL可以开profiling或看慢查询日志的Query_time,SQL Server用SET STATISTICS TIME ON。最简单的验证是重复执行同一批数据,对比平均耗时,同时观察事务日志增长量。之前遇到“程序慢但SQL快”的怪事,最后定位到是ORM逐条提交SQL,而不是批量执行,这就是慢SQL优化里最常见的误判。
快速生成测试数据可以这样:
INSERT INTO user (name, age) SELECT CONCAT('user', n), n % 80 FROM ( SELECT (@i := @i + 1) AS n FROM information_schema.tables, (SELECT @i := 0) t LIMIT 10000 ) t;PostgreSQL直接用generate_series(1,10000),SQL Server用递归CTE。用途是压测插入和批量导入性能,别在生产库上跑。
我现在已经习惯在写任何插入逻辑前先自问三句:有没有唯一键保证幂等?批量参数上限是多少?事务边界在哪里?这三句能挡掉大部分线上写入事故。希望帮到你。
本文还有配套的精品资源,点击获取