☰
SQL插入数据全解析:VALUES、SELECT INTO与INSERT INTO SELECT实战
2026/10/2 8:34:56 网站建设 项目流程

简介:面向数据库初学者与日常开发者,系统讲解SQL插入数据的三种常用方法及易错点,帮助避开约束、非空列、列顺序等常见陷阱,提升日常数据库操作的稳定性与效率;资源为PDF格式,共1个文件,压缩包仅34KB,轻量便携,便于随时翻阅,目前已有664人学习。内容从最基本的INSERT INTO VALUES语句讲起,覆盖单条记录插入;接着介绍INSERT INTO SELECT的两种变体:一种可自动创建目标表并复制数据,另一种可将查询结果写入已存在的表,适合数据迁移与复制;还特别提醒忽略目标列时,SELECT的列顺序必须与表定义完全一致,且所有非空列都要有对应值,否则插入会失败。同时,资料给出检查主键与唯一约束、批量插入、事务处理、错误处理及性能优化等实用小贴士,并提及MySQL批量VALUES写法、LOAD DATA INFILE等高效手段;阅读后可系统掌握三种写法的适用场景与排错思路,适合作为入门巩固和日常参考手册。

1. SQL 插入数据:从单条 INSERT 到数据迁移的完整选择

做数据库开发的人应该都有过这种经历:同样的“插入数据”,在业务系统里写一条 INSERT 就够了,但到了数据迁移、报表临时表、功能上线脚本里,单条 INSERT 往往不够用——要么是数据量太大插不动,要么是目标表结构没定义好,要么是简写形式把列顺序搞错导致整批数据错位。SQL 插入数据这件事看起来简单,真正踩过坑的人才知道,VALUES、SELECT INTO、INSERT INTO SELECT 这三种写法各有各的适用场景和边界条件,选错了轻则报错重则数据错位。这篇笔记把这三种常用方法的语法差异、列顺序陷阱、非空约束要求、批量插入性能和事务处理完整梳理一遍,适合刚接触数据库的开发新手,也适合写迁移脚本的老手对照排查。

2. INSERT INTO VALUES:从单条插入到批量写入的写法演进

2.1 基础语法:列清单与值清单必须一一对应

INSERT INTO VALUES 是日常开发里最常用的插入方式,T-SQL 和 PL/SQL 都支持。标准的完整写法是指定列清单,再给出对应的值清单:

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

这段代码的逻辑很直接:将 id、name、address 三列分别插入值 1、'ygl'、'beijing'。指定列清单最大的好处是,表结构的变更——比如新增了 phone 列——不会影响这条语句的执行。更重要的是,当目标表设置了默认值时,你完全不需要把这些列写进列清单,数据库会自动填充默认值。

参数说明:列清单的顺序不要求跟表定义完全一致,但值清单的顺序必须跟列清单一致。类型上要注意字符型必须加单引号,数值型不要加引号,日期类型不同数据库写法不同——SQL Server 里用 '2024-01-01',Oracle 里用 TO_DATE 函数转换更稳妥。

2.2 完整写入与自增主键的处理逻辑

如果省略列清单,写成 INSERT INTO table1 VALUES (1, 'ygl', 'beijing'),那么数据库会默认对表的全部列按定义顺序插入。这种简写方式有两个前提条件必须满足:一是表的所有列都要提供值,包括自增主键列;二是值的顺序必须与表定义完全一致。

-- 省略列的写法:必须包含所有列,顺序完全匹配表定义 INSERT INTO table1 VALUES (2, 'lisi', 'shanghai');

这里有一个常见的翻车点:如果 id 列是自增列(IDENTITY),你在 VALUES 里硬塞一个值进去,SQL Server 会直接报错——除非你显式开启 SET IDENTITY_INSERT table1 ON。而 MySQL 里虽然允许对自增列手动指定值,但后续自增计数器的行为可能跟你预期不一致。所以我的习惯是,只要表里有自增列,就老老实实写列清单,把自增列排除在外。

2.3 多行批量插入:一条语句写入多条记录

MySQL、PostgreSQL、SQLite 都支持多行 VALUES 批量插入:

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

这种写法的性能提升非常明显:省去了多次网络往返,也减少了事务日志的写入次数。在 MySQL 里,如果单条 INSERT 的 VALUES 特别长,需要注意 max_allowed_packet 参数,默认一般是 16MB,超出会报 Packet too large 错误。

但要注意,SQL Server 2012 以下版本不支持这种多行 VALUES 语法(2012 之前单条 INSERT 只能插入一行),SQL Server 2012 及以上版本才支持。如果你的项目还挂着 2008 R2 的实例,用这种写法直接报语法错误。

2.4 参数化写法:防注入的正确姿势

无论是单条还是批量插入,只要值来自用户输入或外部接口,就必须用参数化而不是拼接字符串。以 JDBC 为例:

// 错误的做法:字符串拼接,存在 SQL 注入风险 String sql = "INSERT INTO users(name, email) VALUES('" + name + "', '" + email + "')"; // 正确的做法:使用 PreparedStatement 参数占位 String sql = "INSERT INTO users(name, email) VALUES(?, ?)"; PreparedStatement ps = conn.prepareStatement(sql); ps.setString(1, name); ps.setString(2, email); ps.executeUpdate();

参数化不只是为了防注入。当值里含有单引号、反斜杠、换行符时,参数化写法完全不需要手动转义,避免了引号嵌套的噩梦。另外 JDBC 还支持 addBatch() + executeBatch() 的方式做批量插入,比一条一条 executeUpdate 快得多,配合手动提交事务效果更佳。

3. SELECT INTO 与 INSERT INTO SELECT:表复制与数据迁移的差异

3.1 两种写法的核心区别:建表还是写入已有表

SELECT INTO 和 INSERT INTO SELECT 长得像,功能完全不同。SELECT INTO 会创建一个新表并把查询结果写入,而 INSERT INTO SELECT 是把查询结果写入一张已存在的表。

对比项SELECT INTOINSERT INTO SELECT
目标表自动创建新表必须是已存在的表
适用数据库T-SQL(SQL Server、Sybase)几乎所有关系型数据库
列定义控制从查询结果推断可指定目标列,不匹配会报错
常用场景快速备份、建临时表数据迁移、数据汇总
-- T-SQL 专用:自动创建 table2,结构类似 table1 SELECT id, name, address INTO table2 FROM table1; -- 通用写法:把查询结果插入已存在的 table2 INSERT INTO table2 (id, name, address) SELECT id, name, address FROM table1;

SELECT INTO 的最大优势是省去了手动建表的步骤,查询跑完表就出来了,做临时备份非常高效。但它的局限性也很明显:MySQL、Oracle 都不支持 SELECT INTO 建表语法——MySQL 要用 CREATE TABLE table2 AS SELECT,Oracle 要用 CREATE TABLE table2 AS SELECT。所以跨数据库写脚本时,先确认目标库对这一语法的支持情况。

3.2 列顺序匹配:简写形式的致命陷阱

INSERT INTO SELECT 的简写形式是最容易出问题的写法:

-- 危险写法:省略目标表列清单 INSERT INTO table2 SELECT id, name, address FROM table1;

省略列清单意味着数据库默认对 table2 的全部列按顺序插入,此时 SELECT 返回的列顺序必须与 table2 的定义顺序完全一致,一个位置错了,后续数据全部错位。如果两表结构完全一致,简写没问题;但如果 table2 后面加了新列、调整了列顺序、或者列类型不一致,简写形式就会翻车。

更隐蔽的问题是隐式类型转换。比如 table2.address 是 VARCHAR(20),而 SELECT 返回的是 VARCHAR(100),SQL Server 可能会静默截断不报错,数据就悄悄丢了;MySQL 在严格模式(STRICT_TRANS_TABLES)下会报 Data too long,但非严格模式下也是静默截断。所以我写 INSERT INTO SELECT 永远指定列清单,绝不用简写。

3.3 源数据筛选与多表联合查询

INSERT INTO SELECT 真正强大之处在于,SELECT 部分可以是任意复杂查询,这使其能完成条件筛选迁移和多表关联汇总:

-- 只迁移指定城市、最近注册的用户 INSERT INTO active_users (user_id, user_name, register_date) SELECT u.id, u.name, u.register_date FROM users u WHERE u.city = 'beijing' AND u.register_date >= '2023-01-01'; -- 多表 JOIN 汇总后写入报表表 INSERT INTO report_orders (order_date, total_amount, order_count) SELECT o.order_date, SUM(o.amount), COUNT(*) FROM orders o INNER JOIN order_items oi ON o.id = oi.order_id WHERE o.pay_status = 'paid' GROUP BY o.order_date; ORDER BY o.order_date;

这些场景在实际工作中非常高频:把生产库的满足条件的数据同步到报表库、把多个表的关联结果物化成一张宽表、按维度聚合后写入汇总表。SELECT INTO 做不到这些——它不仅不能 JOIN 后写入已有表,而且建表时也不会带上主键、索引、约束,生成的新表就是一张裸表。

3.4 不同类型数据库的实现差异

写迁移脚本的人最头疼的就是方言差异。同一段逻辑,SQL Server、MySQL、PostgreSQL、Oracle 的写法各不相同:

  • SQL Server:SELECT INTO 建表,INSERT INTO SELECT 写入,两者都支持。
  • MySQL:SELECT INTO 不支持,用 CREATE TABLE table2 AS SELECT 代替;写入用 INSERT INTO ... SELECT。
  • PostgreSQL:不支持 SELECT INTO 建表(会报错),用 CREATE TABLE AS SELECT;写入也是 INSERT INTO ... SELECT。
  • Oracle:不支持 SELECT INTO 建表,用 CREATE TABLE AS SELECT;写入同样是 INSERT INTO ... SELECT。

如果要在 SQL Server 2008 R2 和 MySQL 之间迁移数据,就要准备两套脚本。这类坑在搜 SQL 插入数据相关内容时被问得很多,本质上不是语法不会,而是没意识到方言差异。

4. 插入数据避坑:五个高频翻车场景与排查方法

4.1 非空列未填导致插入失败

现象:执行 INSERT 语句时报 Cannot insert the value NULL into column 'name', table 'table1'; column does not allow nulls. INSERT fails。整条语句回滚,数据没插进去。

原因:目标表的某列设置了 NOT NULL 约束,但你的 INSERT 语句里既没在列清单中指定该列,也没有在 VALUES 里给它值。数据库无法给非空列填充默认值(没有默认值定义),只能报错。

解决:先查看表结构确认哪些列有 NOT NULL 约束,再确认这些列是否有默认值。可以用 SQL Server 的 sp_help 或者 MySQL 的 SHOW CREATE TABLE 查看。INSERT 语句的列清单必须覆盖所有非空列,或者确保这些列在表定义里有 DEFAULT 值。

提示:拿到一张不熟悉的表时,先执行 DESCRIBE 或 SHOW CREATE TABLE,永远不要凭记忆猜列。

4.2 简写 INSERT 列顺序不匹配导致数据错位

现象:INSERT 语句没有报错,但查询出来的数据完全错位——id 列里存的是姓名,address 列里存的是 id。

原因:省略列清单的简写写法按表定义的列顺序插入,如果 SELECT 的返回列顺序和表定义不一致,数据库不会校验,直接把值按位置塞进去,造成静默数据错位。这是最危险的情况,因为不报错,数据已经污染了。

解决:所有 INSERT 语句一律显式指定列清单,手动核对 SELECT 的列顺序与目标列清单一致。特别是表结构变更(新增列、调整列序)之后,旧脚本很可能在不自知的情况下中招。

4.3 主键冲突导致批量插入中断

现象:批量插入 1000 条数据,执行到第 300 条时报 Duplicate entry '123' for key 'PRIMARY',前面 299 条已插入,后面 700 条全部中断。如果不在事务内,前面插入的数据已提交,无法回滚。

原因:批量数据里存在与目标表主键或唯一索引重复的值。这是数据清洗和迁移时最常见的错误来源。

解决:插入前先用查询排除重复数据,批量操作放进事务里保证失败可回滚。MySQL 还提供了 INSERT IGNORE、ON DUPLICATE KEY UPDATE 或 REPLACE INTO,SQL Server 可以用 MERGE 语句,在撞到冲突时选择忽略、更新或跳过。

4.4 SELECT INTO 在 SQL Server 与 MySQL 之间的语法困惑

现象:从 SQL Server 复制到 MySQL 执行 SELECT id, name INTO table2 FROM table1,MySQL 直接报语法错误 near 'INTO table2'。

原因:SELECT INTO 在 MySQL 中是另一个含义(把查询结果写入变量),建表语法是 CREATE TABLE ... AS SELECT。跨数据库时没有意识到方言差异直接复制粘贴,是最常见的低级错误。

解决:确认目标数据库的类型再决定建表写法。MySQL、PostgreSQL、Oracle 用 CREATE TABLE table2 AS SELECT,只有 T-SQL 系支持 SELECT INTO。写迁移脚本时先梳理目标库语法清单。

4.5 自增列与标识列的手工插入冲突

现象:向 SQL Server 的含 IDENTITY 列的表插入数据,报错 Cannot insert explicit value for identity column in table 'table1' when IDENTITY_INSERT is set to OFF。

原因:IDENTITY 列不允许显式插入值,除非把会话级别的 SET IDENTITY_INSERT 开启。这在迁移历史数据、需要保留原主键值的时候特别常见。

解决:

SET IDENTITY_INSERT table1 ON; INSERT INTO table1 (id, name, address) VALUES (100, 'ygl', 'beijing'); SET IDENTITY_INSERT table1 OFF;

注意 IDENTITY_INSERT 一次只能对一个表开启,用完立刻关闭。MySQL 的自增列没有这个限制,Oracle 的序列机制又不同,迁移数据到 SQL Server 时必须先处理 IDENTITY_INSERT。

5. 事务、批量装载与性能优化:让插入数据不拖垮数据库

5.1 多条插入用事务包裹,避免半截数据

业务上多条 INSERT 往往是关联操作,比如订单表和订单明细表,要么全部成功,要么全部失败。默认自动提交模式下每条语句独立提交,第二条失败时第一条已经落库,数据状态就不一致了。用事务可以把一组 INSERT 绑定成一个原子操作:

BEGIN TRANSACTION; INSERT INTO orders (order_id, customer_id, order_date) VALUES (1001, 25, GETDATE()); INSERT INTO order_items (order_id, product_id, quantity) VALUES (1001, 300, 2), (1001, 301, 1); COMMIT TRANSACTION;

把 COMMIT 改为 ROLLBACK 则全部撤销。在 T-SQL 里推荐配合 TRY...CATCH 使用,出错自动回滚:

BEGIN TRY BEGIN TRANSACTION; INSERT INTO orders (order_id, customer_id, order_date) VALUES (1002, 26, GETDATE()); INSERT INTO order_items (order_id, product_id, quantity) VALUES (1002, 302, 5); COMMIT TRANSACTION; END TRY BEGIN CATCH ROLLBACK TRANSACTION; THROW; END CATCH

事务的代价是占用锁资源,事务持续时间越长,阻塞其他会话的概率越大。不要在一个事务里塞几千上万条插入,那样会让吞吐量断崖式下跌。

5.2 大容量插入:LOAD DATA INFILE 与 BULK INSERT 对比

当插入量达到十万行以上,逐条 INSERT 不管怎么写都太慢。这时候应该用批量装载工具。MySQL 的 LOAD DATA INFILE 和 SQL Server 的 BULK INSERT 是各自最常用的方案:

特性MySQL LOAD DATA INFILESQL Server BULK INSERT
数据源本地或远程文本文件本地文本文件
语法LOAD DATA INFILE 'file' INTO TABLE tBULK INSERT t FROM 'file'
性能极高,可达到每秒数万行极高,使用批量日志操作
约束检查可控制是否忽略可控制是否检查约束
事务单条语句自动完成,中途失败需重来可配合事务回滚
-- MySQL 从 CSV 批量装载 LOAD DATA INFILE '/tmp/users.csv' INTO TABLE users FIELDS TERMINATED BY ',' ENCLOSED BY '"' LINES TERMINATED BY '\n' IGNORE 1 ROWS (id, name, email, city);

参数说明:FIELDS TERMINATED BY 指定列分隔符,ENCLOSED BY 指定字段包围符,LINES TERMINATED BY 指定行分隔符,IGNORE 1 ROWS 跳过 CSV 表头。这是一条非常成熟的方案,十万行数据几秒钟就能完成,比逐条 INSERT 快一到两个数量级。

SQL Server 的 BULK INSERT 语法类似:

BULK INSERT users FROM 'C:\data\users.csv' WITH ( FIELDTERMINATOR = ',', ROWTERMINATOR = '\n', FIRSTROW = 2, TABLOCK );

TABLOCK 选项让批量插入获得表级锁,减少锁开销,速度更优。如果文本文件编码不是默认编码,还要指定 CODEPAGE 参数,否则中文会乱码,这是个很隐蔽的坑。

5.3 避免在循环里单条插入

新手常见做法是把 10 万条数据放到编程语言的循环里,一条一条 executeUpdate。每条 INSERT 都发起一次网络往返,还要单独写一次事务日志,性能极差。正确的思路是:能合并成多行 VALUES 就合并,不能合并就使用 JDBC 的 Batch 功能,再配合手动提交事务:

Connection conn = dataSource.getConnection(); conn.setAutoCommit(false); String sql = "INSERT INTO users(name, email) VALUES(?, ?)"; PreparedStatement ps = conn.prepareStatement(sql); for (User u : userList) { ps.setString(1, u.getName()); ps.setString(2, u.getEmail()); ps.addBatch(); // 每 500 条提交一次,避免事务过长 if (++count % 500 == 0) { ps.executeBatch(); conn.commit(); } } ps.executeBatch(); conn.commit();

这里的核心参数是批量提交的批次大小:太小则频繁提交浪费性能,太大则事务日志膨胀、锁持有时间过长,500 到 1000 是一个比较安全的经验值。批处理过程中如果报错,可以拿到 BatchUpdateException 的异常数组逐个排查,定位具体哪条数据出了问题。

5.4 关联表插入顺序:先主表后从表

有外键约束的两张表,插入顺序必须注意。如果先把订单明细数据插进 order_items,但对应的 orders 主记录还没插入,外键约束会直接报错。常规顺序是先插主表、后插从表,删数据时反过来先删从表。如果需要临时突破外键约束做数据修复,SQL Server 可以用 WITH NOCHECK 临时禁用约束,MySQL 可以用 SET FOREIGN_KEY_CHECKS = 0,但操作完必须恢复。插入完成后要检查外键关联的完整性,不能留下孤立子记录。

6. 插入数据的验证手段:写入后必须做的四个检查

数据插进去不等于万事大吉,尤其是批量迁移的场景。我自己的固定流程是至少跑四道检查,确保插入的数据完整、精确、没有错位。

第一道:行数校验。插入后立刻对比源表和目标表的行数,用 COUNT(*) 或者 SQL Server 的 @@ROWCOUNT 查看受影响行数是否与源数据一致。不一致就说明中途有失败或被过滤。

-- 检查受影响行数与源表行数是否一致 SELECT COUNT(*) FROM orders WHERE order_date >= '2023-01-01'; SELECT COUNT(*) FROM report_orders WHERE order_date >= '2023-01-01';

第二道:抽样比对。不能只看行数,要随机抽取若干条记录,对比关键字段在源表和目标表的值。重点看容易错位的字段——地址、备注这类长度可变的内容,以及时间类型的时区与格式。

第三道:唯一性与非空校验。用 GROUP BY 检查目标表主键和唯一索引有没有重复值,同时确认所有非空列没有 NULL。迁移过程中最常见的静默问题就是唯一约束在目标表没建,重复数据悄悄插进去了,等业务发现时已经是几天后。

-- 检查是否有重复主键 SELECT id, COUNT(*) FROM table2 GROUP BY id HAVING COUNT(*) > 1;

第四道:依赖对象可用性。如果插入数据依赖了触发器、外键或计算列,要确认这些对象在目标表上同样存在且有效。曾经遇到过一个案例:目标表有触发器自动更新更新时间字段,但迁移时目标表没建触发器,数据进去后时间字段全是 NULL,排查了很久才找到原因。

从那以后,我每次做数据迁移或大批量插入,都强制走一遍“源表查询 → 目标表结构确认 → 插入 → 行数校验 → 抽样比对 → 唯一性检查”这套流程,中途任何一个环节不通过就直接回滚,绝不带着疑问提交数据。批量插入的脚本顺手就写成参数化的形式留着复用,省得下次再踩一遍坑,希望帮到你。

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

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

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

立即咨询