☰
SQL插入数据三种方式详解:INSERT INTO VALUES、SELECT INTO与INSERT INTO SELECT
2026/10/11 19:34:19 网站建设 项目流程

简介:这份PDF资料面向SQL初学者与需要巩固数据库基础的开发者,系统梳理了插入数据的三种常用写法:基础INSERT INTO VALUES、INSERT INTO SELECT以及省略目标列的简写形式,并针对每种方法给出语法示例与适用场景。资料还整理了检查列约束、批量插入、事务处理、错误处理与性能优化等实用小贴士,帮助读者在实际操作中避开非空列遗漏、列顺序不一致等常见陷阱。资源包共1个PDF文件,大小约34KB,内容紧凑,适合快速查阅与复习。目前已有664人学习下载,可作为日常数据库开发与数据迁移场景的参考手册,帮助读者根据具体需求选择更合适的插入方式,提升数据管理效率。

1. 三种插入方式,选错了就是给自己埋雷

很多人写INSERT写了几年,其实只写过一种。日常业务里往表里塞一条记录,INSERT INTO ... VALUES确实够用,可一旦碰上数据迁移、表结构复制、跨库同步,还硬用单条 VALUES 去怼,那就是拿业务时间换自己的加班时长。SQL 插入数据这件事,表面看就三条路:INSERT INTO ... VALUES、SELECT ... INTO、INSERT INTO ... SELECT。但真正落到生产环境,选哪条、列顺序怎么对、非空列漏没漏、事务包没包,每一步都能让插入翻车。这篇笔记拆的就是这三种常用方法,把语法、参数、适用边界和几个血泪坑一次讲清,适合刚接触 SQL 的开发和需要做数据搬运的运维同学照着复现。

2. INSERT INTO VALUES:单条插入的地基,也是批量插入的起点

2.1 语法结构与参数含义

最基础的插入语句长这样:

INSERT INTO table1 (id, name, address) VALUES (1, 'ygl', 'beijing');

拆开看,INSERT INTO是固定动作,table1是目标表,括号里的id, name, address是目标列清单,VALUES后面括号里是对应列的值。列清单和值清单必须一一对应,数量对不上直接报错。字符串值要用单引号包住,数字不用。这条语句在 T-SQL 和 PL/SQL 里都通用,是插入数据最没有歧义的一种写法。

我一般会强制自己写全列名,哪怕表里只有三个列。原因很简单:表结构是会变的,今天你省略列名靠顺序蒙对,明天别人加了一列,你的插入语句就可能把值塞进错误的列,而且不报错,数据静默错位,这种问题排查起来非常折磨。

2.2 批量插入的写法与性能边界

单条 VALUES 一次只插一行,如果循环一千次去插一千行,网络往返和事务开销会把性能拖垮。常见做法是把多行合并到一条语句里:

INSERT INTO table1 (id, name, address) VALUES (1, 'ygl', 'beijing'), (2, 'lisi', 'shanghai'), (3, 'wangwu', 'guangzhou');

这种写法在 MySQL、PostgreSQL、SQL Server 2012 及以上都支持。它的优势是减少语句解析次数和网络交互,几百到几千行的批量插入用这种方式很划算。但要注意,单条语句的值列表不能无限长,MySQL 受max_allowed_packet限制,SQL Server 对单条语句也有批处理大小约束。超过几千行,建议分批提交,每批 500 到 1000 行,配合事务控制。

参数上还要留意自增列。如果id是自增主键,插入时不要把id写进列清单,让数据库自己分配:

INSERT INTO table1 (name, address) VALUES ('ygl', 'beijing');

强行给自增列指定值,在 SQL Server 里需要SET IDENTITY_INSERT table1 ON,在 MySQL 里虽然允许但容易造成自增计数器混乱。除非做数据迁移要保留原 ID,否则不要碰自增列。

2.3 事务包裹与错误处理

批量插入最怕插到一半失败,前面进去的数据成了脏数据。标准做法是用事务把整批操作包起来:

BEGIN TRANSACTION; INSERT INTO table1 (name, address) VALUES ('ygl', 'beijing'); INSERT INTO table1 (name, address) VALUES ('lisi', 'shanghai'); COMMIT;

如果中间任何一条失败,执行ROLLBACK回滚整批。SQL Server 里还可以配合TRY...CATCH:

BEGIN TRY BEGIN TRANSACTION; INSERT INTO table1 (name, address) VALUES ('ygl', 'beijing'); COMMIT; END TRY BEGIN CATCH ROLLBACK; THROW; END CATCH;

这里的关键参数是事务隔离级别和锁范围。大批量插入时,如果隔离级别是SERIALIZABLE,锁会升级,阻塞其他会话。一般业务插入用默认的READ COMMITTED就够。另外,插入前检查目标表的约束——主键冲突、唯一索引冲突、非空约束——这些是插入失败的高频原因,提前用SELECT验证一遍比事后回滚省事得多。

3. SELECT INTO 与 INSERT INTO SELECT:数据搬运的两把刀

3.1 SELECT INTO 自动建表:方便但有前提

T-SQL 里有一条很顺手的语句:

SELECT id, name, address INTO table2 FROM table1;

它的行为是:如果table2不存在,就按照SELECT出来的列结构和数据类型自动创建,然后把数据插进去。注意,是“如果不存在”。如果table2已经存在,这条语句直接报错,不会追加数据。这一点和INSERT INTO SELECT有本质区别。

自动建表时,源表的约束、索引、默认值不会带过来。主键、外键、唯一索引统统丢失,只有列名和数据类型被复制。所以SELECT INTO适合做临时表、中间表、数据快照,不适合直接当正式表的复制方案。我见过有人用SELECT INTO建了一张业务表,跑了半个月才发现没有主键,重复数据进了一堆,回头补约束时已经很难清理。

另外,SELECT INTO在 PL/SQL(Oracle)里没有直接对应写法,Oracle 要用CREATE TABLE ... AS SELECT。跨数据库方言时这一点要特别注意。

3.2 INSERT INTO SELECT 指定列:灵活但列清单不能漏

当目标表已经存在,要把源表数据插进去,用这条:

INSERT INTO table2 (id, name, address) SELECT id, name, address FROM table1 WHERE id > 100;

和SELECT INTO比,它不建表,只插数据。优势在于SELECT部分可以写得很复杂——多表 JOIN、WHERE 过滤、GROUP BY 聚合、窗口函数去重,都能往上堆。目标列清单可以只写部分列,但前提是没写的列允许 NULL 或者有默认值。

这里有一个硬性规则:目标表的非空列必须全部出现在列清单里,否则插入直接失败。比如table2的name列是NOT NULL,你只写了(id, address),数据库会拒绝执行。这个规则在数据迁移时最容易踩,因为源表和目标表的非空约束往往不一致。

3.3 省略列清单的简写:顺序必须完全一致

还有一种写法是省略目标列清单:

INSERT INTO table2 SELECT id, name, address FROM table1;

这种简写形式默认你按目标表的全部列顺序插入。也就是说,SELECT后面的列顺序必须和table2的列定义顺序完全一致,一个都不能错位。如果table2的列顺序是(id, address, name),而你SELECT出来是(id, name, address),数据就会串列——name的值进了address列,address的值进了name列。更麻烦的是,如果两列数据类型兼容,数据库不会报错,数据静默错位,等到业务发现时已经过去很久。

我个人的习惯是永远不写简写形式。多打几个列名,换来的是可读性和安全性。尤其是在生产环境做数据迁移,列清单就是你的后悔药,写全了,顺序错了数据库会报错;不写,顺序错了数据库沉默。

3.4 三种方式的选择对照

方式目标表不存在时目标表已存在时能否指定列典型场景
INSERT INTO VALUES报错插入指定值可以单条/批量写入
SELECT INTO自动建表并插入报错通过 SELECT 控制临时表、快照
INSERT INTO SELECT报错插入查询结果可以数据迁移、复制

选型逻辑很直接:写入已知值用 VALUES;建中间表用 SELECT INTO;往已有表搬数据用 INSERT INTO SELECT。三者不要混用,尤其是别拿 SELECT INTO 去追加重已存在的表,那是方向性错误。

4. 避坑与排查:插入数据时最容易翻车的五个点

4.1 现象:插入报“列名或所提供值的数目与表定义不匹配”

原因通常是列清单和值清单数量不一致,或者省略列清单时SELECT的列数和目标表列数不同。解决方法是把列清单写全,用SELECT *先看一眼目标表结构,确认列数和顺序。不要靠记忆,靠sp_help table2或DESC table2。

4.2 现象:插入成功但数据串列

原因就是省略了目标列清单,且SELECT列顺序和目标表定义顺序不一致。解决方法是永远写全列清单。如果已经发生串列,只能根据业务逻辑反向清洗,没有通用后悔药。

4.3 现象:批量插入中途失败,前面数据已入库

原因是没有用事务包裹。解决方法是把批量插入放进BEGIN TRANSACTION/COMMIT,失败时ROLLBACK。注意,DDL 语句(如SELECT INTO建表)在部分数据库里不能回滚,所以建表和插数据最好分开处理。

4.4 现象:插入速度极慢,一条比一条慢

原因可能是逐条提交、目标表索引过多、触发器逻辑复杂、或者锁等待。解决方法是改批量插入、插入前禁用非聚集索引(迁移场景)、检查触发器、用SET NOCOUNT ON减少网络往返。对于超大批量,考虑BULK INSERT或LOAD DATA INFILE。

4.5 现象:非空列没赋值,插入直接失败

原因是目标表有NOT NULL约束,而列清单里漏了该列,且该列没有默认值。解决方法是插入前查INFORMATION_SCHEMA.COLUMNS确认非空列清单,确保全部覆盖。迁移场景下,源表允许 NULL 的列在目标表可能是NOT NULL,这种差异要提前对齐。

提示:每次做数据迁移前,先用SELECT TOP 10把源数据拉出来看一眼,再对照目标表结构过一遍列清单,比直接跑全量插入安全得多。

5. 进阶技巧:用窗口函数去重后再插入

数据迁移时经常碰到源表有重复数据,直接插进目标表会撞唯一索引。这时候可以在SELECT里用窗口函数先去重,再插入。以 SQL Server 为例:

INSERT INTO table2 (id, name, address) SELECT id, name, address FROM ( SELECT id, name, address, ROW_NUMBER() OVER (PARTITION BY id ORDER BY id) AS rn FROM table1 ) t WHERE t.rn = 1;

这段代码的逻辑是:按id分组,给每组内的行编号,只取编号为 1 的那行,也就是每个id只保留一条记录。PARTITION BY id是分组依据,ORDER BY id决定保留哪一条——如果想保留最新的一条,把ORDER BY改成时间字段倒序即可。

参数上要注意,ROW_NUMBER()必须配合OVER子句,PARTITION BY后面跟去重维度,ORDER BY后面跟优先级字段。MySQL 8.0 以上、PostgreSQL、Oracle 都支持窗口函数,MySQL 5.7 不支持,需要用GROUP BY配合自连接模拟。

验证插入结果是否正确的习惯动作:插入后立刻用SELECT COUNT(*)对比源表和目标表的行数,再用EXCEPT或MINUS查差集:

SELECT id, name, address FROM table1 EXCEPT SELECT id, name, address FROM table2;

返回空集说明数据完全一致。这个验证步骤我每次迁移后都会跑一遍,花不了几秒钟,但能挡住大部分“以为插完了其实漏了”的问题。从那以后我每次做INSERT INTO SELECT都强制走一遍行数对比和差集校验,希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询