SQL插入数据全攻略:从INSERT语法到批量插入与安全实践
发布时间:2026/10/3 18:13:09 作者:尧图编辑部 阅读量:1,286

写 SQL 添加数据很多人第一反应就是一条INSERT INTO 表名 VALUES (...)往里怼。实际上我在日常开发里见过太多比这个复杂的情况表里有自增主键怎么插、日期字段用什么函数生成、批量插 1 万条怎么保证不卡死、主键撞了是忽略还是更新、插入操作会不会被注入攻击。这些问题看似零散但全是“添加数据”这个主题下绕不开的核心点。我打算结合自己这些年写过的 SQL从最简单的 INSERT 语法讲起一路把批量插入、跨表复制、冲突处理、事务、慢插入排查和注入防范都过一遍。这篇文章会覆盖 MySQL、SQL Server、PostgreSQL、Oracle 的常见差异尽量给你整套能直接抄的作业。无论你是刚接触数据库的新人还是被线上慢插入折磨的开发应该都能找到有用的东西。动手之前先记住一个基本原则添加数据从来不只是“写一条语句”而是要先搞懂这个表允许什么、拒绝什么。我们把表结构、约束和默认值梳理清楚再把各种插入姿势逐一展开。1. 添加数据前的准备工作先把表结构和约束摸清楚我最早做项目时吃过一个亏往一张订单表里直接写 INSERT结果连续报错一会提示某个字段不能为空一会又提示字段类型对不上。后来养成了习惯不管多简单的表写插入语句之前一定先看一眼表结构。这个习惯帮我避开了大量低级问题。1.1 INSERT 基本语法先记住这个公式不管什么数据库INSERT 的核心公式都长这样INSERT INTO 表名 (字段1, 字段2, ...) VALUES (值1, 值2, ...);这里有两件事非常关键。第一字段列表的顺序和你写在 VALUES 里的值顺序要一一对应不是按表里字段的物理顺序而是按你自己列的字段顺序。第二如果你省略字段列表直接写INSERT INTO 表名 VALUES (...)那么值的数量、顺序、类型必须和表的物理字段结构完全一致一个都不能少一个都不能乱。我建议你平时写 INSERT 时永远把字段列表显式列出来。原因很简单万一以后有人给表加了一个新字段比如加了个is_deleted逻辑删除标记省略字段列表的插入语句会立刻失效而显式指定字段的语句基本不受影响。这是我在实际项目里踩过最频繁的坑后来团队规范里就明确规定必须写字段列表。1.2 字段的默认值、自增主键和非空约束表结构里三个东西决定你能不能省字段默认值、自增主键、非空约束。自增主键通常是插入时不用管的。比如 MySQL 里id INT AUTO_INCREMENT PRIMARY KEY你插入时不写 id数据库会自动分配一个值。但你也可以手动指定 id指定之后数据库会尊重你的选择后续自增序号会基于你插入的最大值继续排。SQL Server 里叫 IDENTITYOracle 里则需要用序列来自动生成跟 MySQL 的自动增长不是一回事。非空字段是硬规则。字段定义成NOT NULL且没有默认值时你插入数据就必须给它一个值否则数据库会直接拒绝。最常见的报错就是Column 字段名 cannot be null或者 SQL Server 里的Cannot insert the value NULL into column。有默认值的字段插入时可以完全不写数据库会自动填入默认值。比如created_at DATETIME DEFAULT CURRENT_TIMESTAMP你只需要插入业务字段created_at 会自动带上当前时间。这里有个小技巧你也可以显式写DEFAULT关键字比如INSERT INTO user (name, created_at) VALUES (张三, DEFAULT)让语义更明确。Oracle 和 PostgreSQL 都支持这种写法SQL Server 里直接省略即可。1.3 动手前先确认表结构实际操作里我不建议裸写 INSERT而是先确认目标表。大多数 DBA 或开发的习惯是MySQL 用DESCRIBE 表名;或SHOW CREATE TABLE 表名;SQL Server 用EXEC sp_help 表名;或查 sys.columns 视图PostgreSQL 用\d 表名Oracle 用describe 表名不管用哪种方式核心目的是拿到字段列表、类型、长度、是否允许 NULL、是否有默认值。特别是字段长度很多人插入字符串时不看长度结果数据超长被截断或者直接报错Data too long for columnxxx。我曾经在处理用户备注字段时就遇到过这事源数据里有一批备注超长批量导入时白白跑了几分钟最后卡在一条超长数据上报错。自那以后凡是涉及批量插入我都会先估算目标字段最大长度必要时用SUBSTRING或LEFT做截断。有时你也会遇到 SQLite 的报错比如no such column: test_url。这种多半是表结构里根本没有你写的字段名跟 MySQL 的Unknown column是同一个道理。看到这种提示第一反应应该是去查表定义而不是怀疑 SQL 语法写错了。2. 实战单条插入、批量插入与跨库差异准备阶段过了接下来是真正的写 INSERT 环节。这一章我按实操路径拆开讲单条插入有哪几种写法批量插入怎么才能快跨数据库复制数据用什么语句以及 GIS 数据这类特殊数据的导入方式。2.1 单条插入的多种写法最传统的就是标准 INSERTINSERT INTO student (name, age, class_id) VALUES (王小明, 18, 3);如果你要插入当前时间不同数据库写法不同数据库当前时间函数MySQLNOW() 或 CURRENT_TIMESTAMPSQL ServerGETDATE()PostgreSQLNOW() 或 CURRENT_TIMESTAMPOracleSYSDATEMySQL 还提供一个简写语法INSERT ... SET类似 UPDATE 的写法INSERT INTO student SET name 王小明, age 18, class_id 3;这条语句在 MySQL 里完全合法但 Oracle、SQL Server、PostgreSQL 都不认所以跨库项目里尽量少用。我见过有人在接手 MySQL 项目时把这种写法带到了 PostgreSQL结果服务直接报语法错误当时排查还花了不少时间。PostgreSQL 和 SQLite 对标准 INSERT 的支持比较干净基本是INSERT INTO ... VALUES。SQLite 的类型约束相对宽松但主键冲突规则依然要遵守写插入时同样要留意。2.2 批量插入一次性塞进多条数据的四种方式我最初写批量插入是一条一条循环 INSERT后来数据量上来了才发现效率低得离谱。数据库每执行一条 INSERT都要做一次语法解析、权限检查和写日志网络来回开销也很高。如果能一次把多条数据塞进去性能提升非常明显。第一种VALUES 多组形式。MySQL 和 PostgreSQL 支持在一条 INSERT 里写多组值INSERT INTO student (name, age, class_id) VALUES (张三, 18, 1), (李四, 19, 2), (王五, 20, 3);SQL Server 2008 之后也支持这种写法但默认最多 1000 行一组超过就要拆成多条。Oracle 传统的写法是用INSERT ALL配合多条INTO子句或者用INSERT INTO ... SELECT ... FROM dual UNION ALL。Oracle 21c 之后也支持 VALUES 多组形式旧版本只能按老办法写。第二种INSERT ... SELECT。从一张表或视图查询数据再灌入目标表这在数据迁移、报表开发里用得非常多INSERT INTO order_copy (order_id, user_id, amount) SELECT order_id, user_id, amount FROM orders WHERE create_time 2025-01-01;这段 SQL 相当于把查询结果当作插入的数据源。注意 SELECT 出来的列顺序要和 INSERT 后面的字段列表一一对应类型也要兼容。我经常先在 SELECT 部分单独执行一遍确认结果集没问题后再套上 INSERT 前缀。第三种CREATE TABLE AS SELECT。直接把查询结果建一张新表同时把数据加进去常用于把线上表抽一部分数据做成临时分析表。CTAS 创建的新表不会完整保留原表的索引、约束、默认值所以只适合做临时表或报表表不适合直接当业务表用。第四种文件导入。数据量上了百万级别手写 INSERT 已经不够用MySQL 用LOAD DATA INFILE 文件路径 INTO TABLE student;SQL Server 用BULK INSERT或导入导出向导PostgreSQL 用COPY命令Oracle 用 SQL*Loader文件导入是我处理千万级数据迁移时最常用的方式。注意字段分隔符、编码、换行符都要提前确认。常见的坑是 Windows 下导出的 CSV 里带了 BOM 头导入后第一列的字段名或第一行数据混入一个不可见字符排查起来非常费劲。更稳妥的做法是先导入一个小文件验证再看数据是否完整最后才跑全量。有个和 GIS 相关的典型场景也在这里。如果你要给 GIS 二次开发添加 SHP 数据比如往 PostGIS 里导入一个县的行政区划矢量普通 SQL 写起来极其痛苦因为 SHP 里是几何对象SQL 里要写 ST_GeomFromText 这类函数手工拼几乎不可行。正确姿势是用专门工具shp2pgsql -s 4326 china_county.shp county_table | psql -U postgres -d gisdb这样会自动生成建表 SQL 和插入语句一次性把 SHP 的字段结构和几何数据灌进 PostgreSQL。其他数据库也有类似工具比如 Oracle Spatial 就有自己的导入向导。这类数据本质上也是“添加数据”但处理思路和业务表完全不同。2.3 跨表插入的细节字段映射与数据清洗INSERT ... SELECT 虽然省事但字段映射必须做对。比如从老的 user 表迁数据到新 user_profile 表两边字段名不一定一致你需要在 SELECT 后面显式做别名把源字段位置对准目标字段INSERT INTO user_profile (uid, nickname, mobile, create_time) SELECT id, user_name, phone, register_time FROM user WHERE status 1;这个过程中最容易出现四类问题NULL 值源表字段是空的但目标字段定义 NOT NULL插入直接报错。我一般用COALESCE给默认值兜底。重复数据源表有重复目标表有唯一索引插一半就报错。这时可以在 SELECT 后加DISTINCT或按业务主键用ROW_NUMBER()取每组第一条。超长截断源字段长度超过目标字段提前用 SUBSTRING 控制长度避免后面报错。类型不匹配源表存的是字符串2025-01-01目标表是 DATE需要转格式或靠隐式转换兜底。我常用的套路是先只写 SELECT 部分把 WHERE、DISTINCT、转换函数都处理完确认返回结果没问题再在前面套一层 INSERT INTO。这样可以减少一半以上的失败次数也让 SQL 更容易读。2.4 主流数据库的 INSERT 差异速查这里放一张我在团队内部文档里用过的对照表整理了几个核心差异点场景MySQLSQL ServerPostgreSQLOracle多行 VALUES 批量支持支持2008 之后注意 1000 行限制支持传统用 INSERT ALL21c 后支持当前时间函数NOW()GETDATE()NOW() 或 CURRENT_TIMESTAMPSYSDATE自增主键AUTO_INCREMENTIDENTITYSERIAL 或 IDENTITY序列 SEQUENCE手动指定自增值直接写 id需打开 IDENTITY_INSERT手动写主键序列需同步调整手动写序列值唯一键冲突处理ON DUPLICATE KEY UPDATEMERGE / IF NOT EXISTSON CONFLICTMERGE这张表对刚切换数据库的人来说特别实用。比如你在 MySQL 里写惯了 AUTO_INCREMENT换到 SQL Server 就会遇到 IDENTITY_INSERT 开关报错时提示Explicit value must be specified for identity column第一次遇到可能一头雾水。其实数据库就是在提醒你这个自增列不允许手工插入除非你显式打开开关。3. 插入冲突与数据一致性主键重复、去重和自增设置添加数据不是永远顺风顺水目标表往往立了不少规矩主键、唯一索引、外键、检查约束。数据一旦违规数据库就会抛错。这一章专门聊冲突重点是主键重复和唯一约束冲突的处理。3.1 主键冲突插不进去怎么办先明确一个概念主键或唯一键冲突本质是你想要写入的这一行在业务上已经存在。处理方法取决于业务意图。第一种跳过。MySQL 里是INSERT IGNOREINSERT IGNORE INTO student (id, name) VALUES (1, 张三);如果 id1 已经存在这条语句不会报错而是静默跳过。注意INSERT IGNORE 不止忽略主键冲突还会忽略其他一些错误比如数据超长被截断也可能会静默不建议在严格模式下无脑使用。PostgreSQL 里写法是ON CONFLICT DO NOTHINGINSERT INTO student (id, name) VALUES (1, 张三) ON CONFLICT (id) DO NOTHING;SQL Server 没有直接的 INSERT IGNORE通常用WHERE NOT EXISTS判断INSERT INTO student (id, name) SELECT 1, 张三 WHERE NOT EXISTS (SELECT 1 FROM student WHERE id 1);第二种更新。业务上遇到重复主键时很可能希望把已有记录更新成新值。MySQL 里INSERT INTO student (id, name, age) VALUES (1, 张三, 18) ON DUPLICATE KEY UPDATE name VALUES(name), age VALUES(age);MySQL 8.0.20 之后官方推荐把VALUES(name)换成别名写法INSERT INTO student (id, name, age) VALUES (1, 张三, 18) AS new ON DUPLICATE KEY UPDATE name new.name, age new.age;PostgreSQL 里用ON CONFLICT DO UPDATE引用待插入的值时用EXCLUDEDINSERT INTO student (id, name, age) VALUES (1, 张三, 18) ON CONFLICT (id) DO UPDATE SET name EXCLUDED.name, age EXCLUDED.age;Oracle 和 SQL Server 处理这类场景偏向 MERGE 语法整段写起来略长但适合对复杂情况做精细控制。MERGE 本质上就是“存在则更新不存在则插入”数据同步场景里几乎是标配写法。3.2 插入时去重怎么保证数据不会插重网上搜“添加数据”经常连带出现“sql语句去重”的热词可见重复数据问题有多普遍。如果你在建表时没有给业务唯一字段加唯一索引那程序完全可以插入两条看起来一样的记录。处理重复数据时我按三个方案排优先级第一个是从源头防止。表上给业务键建 UNIQUE 约束比如用户表的手机号唯一索引。这是最省事的方案也是 DBA 最推荐的做法。第二个是插入前先查询。SELECT 1 FROM 表 WHERE 关键字段 ?查到就不插。这种方案在单机低并发下可用但在多并发下会有竞态问题两个请求同时判断都不存在然后都插进去了。所以只能作为辅助不能作为唯一防线。第三个是数据库层解决。用上面说的ON DUPLICATE KEY UPDATE或ON CONFLICT让数据库自己判断并发安全逻辑上也最干净。如果你手里已经有一张几百万行的表而且已经存在大量重复数据需要先清洗再插入。我的建议是先 SELECT 出去重后的结果再用 INSERT ... SELECT 写入一张临时表。比如INSERT INTO user_new (id, phone, name) SELECT MIN(id), phone, name FROM user GROUP BY phone;这里MIN(id)表示每组保留最早的一条具体按业务需要决定保留哪条。之后再把临时表改名或交换数据。这个方案比直接 DELETE 大量行更安全尤其是有外键关联的场景因为 DELETE 可能触发级联删除风险大得多。3.3 自增主键的坑指定了 ID 之后序列就乱了这节是我特别想提醒你的。很多人在开发环境做测试时喜欢手动指定主键比如INSERT INTO student (id, name) VALUES (100, 测试)数据库不会拒绝但后续自增计数器就会跳到 101。业务上如果是订单号、流水号中间空了一大片客户看到会觉得很奇怪。SQL Server 的 IDENTITY 更严格你想手动给自增列插值必须先执行SET IDENTITY_INSERT 表名 ON插入完再SET IDENTITY_INSERT 表名 OFF否则直接报错。很多人第一次遇到这条报错都会懵其实是数据库在保护你防止你打乱自增序列。PostgreSQL 里如果表是用 IDENTITY 语法建的插入自增列时也有类似限制用GENERATED ALWAYS定义的自增列默认不允许手动插值除非用OVERRIDING SYSTEM VALUE。如果你用的是传统 SERIAL手动插主键是可以的但序列本身不会同步变化后续自增可能会撞上你插进去的值产生主键冲突。这个坑我在迁移数据时踩过。当时从旧库把用户数据导入新库手动指定了 id结果导入完成后再新增用户报主键冲突一查才发现序列还停在 1。后来总结出规则手动插带序列的主键必须同步调整序列的当前值比如在 PostgreSQL 里执行SELECT setval(student_id_seq, 1000, true);确保后续自增不会重复。MySQL 里虽然没有显式序列但如果你插入了较大的 id也可以通过ALTER TABLE student AUTO_INCREMENT 1001;来调整计数器避免意外冲突。4. 添加数据的事务管理、性能优化和安全底线能正确写出 INSERT 只是第一步在生产环境里数据一旦写错影响范围可能是线上业务。这一章讲三件大事事务怎么包性能怎么调安全怎么防。4.1 事务多条插入要么全成功要么全回滚我在刚接触数据库时习惯一条一条 INSERT两边表各插一半之后第二张表报错了第一张表的数据留在数据库里形成脏数据。这个问题的解法其实很简单把一组插入放进一个事务。MySQL 里这样写START TRANSACTION; INSERT INTO order_main (order_no, user_id) VALUES (A001, 1); INSERT INTO order_item (order_no, sku) VALUES (A001, SKU-1); COMMIT;如果两条插入中任何一条失败你可以在应用代码里捕获异常后执行ROLLBACK两条都不会生效。SQL Server 里用BEGIN TRANSACTION; ... COMMIT/ROLLBACK。PostgreSQL 一样支持 BEGIN、COMMIT、ROLLBACK。事务还有一层作用提高批量写入效率。如果你把 1 万条插入包在一个事务里数据库先把日志攒起来最后统一提交比自动提交模式一条一条写快很多。但也要注意一个事务太大比如 50 万条事务日志会非常大锁占用时间也长其他会话可能长时间等锁。我习惯把大事务拆成每 1000 条一批既保留事务的保护作用又把单次持锁时间控制在可接受范围。4.1.1 实操分批做带事务的批量插入假设你现在要初始化 5000 条订单数据我一般这样设计流程每 1000 条一组共 5 组。每组的开头执行 START TRANSACTION组内一次性插入 1000 条。每组的末尾执行 COMMIT。如果某批失败回滚这一组记录错误日志修正数据后单独重跑这一组。这样做的原因是如果 5000 条放一个事务某个中间数据格式错误可能前面 4000 条全被回滚重新来过代价很大。拆成 5 组后每组独立成功只有出问题的那组需要重跑。在 MySQL 环境下还要注意max_allowed_packet和innodb_log_buffer_size配置批量插入的数据包太大时会报错Packet too large。实际开发里这种分批逻辑不一定要写在 SQL 里也可以在应用层循环提交。但无论如何每一次提交的批大小最好固定下来别忽大忽小否则排查问题时很难判断是哪一批拖慢了整体过程。4.2 插入操作与 SQL 注入别把用户输入拼进 SQL这个题目我在排查线上问题时反复遇到。有些人觉得 SQL 注入是登录接口里拼用户名密码才会出现的问题实际上插入数据的接口同样危险。你在填表单提交新用户、写评论、上传配置时如果直接把页面传来的字符串拼进 INSERT 语句攻击者完全可以构造恶意字符串让插入语句变成多条执行甚至操作你的数据库。比如某段代码可能是这样写的String sql INSERT INTO user (name) VALUES ( name );如果 name 的值是); DROP TABLE user; --拼出来的语句就变成了INSERT INTO user (name) VALUES (); DROP TABLE user; --)后果可想而知。这就是典型的 SQL 注入也是为什么我在任何一篇数据库操作文章里都要反复强调永远不要用拼接字符串的方式执行 SQL改用参数化查询。JDBC 里用 PreparedStatement通过问号占位符?把参数值作为独立变量传入数据库会把它当作纯数据而不是 SQL 片段。MyBatis 里用#{}而不是${}。#{}会走预编译占位符${}是直接拼接危险性极高。Python 的 sqlite3、psycopg2、Node 的 mysql 库都有对应的参数绑定 API用法基本一致。如果说别人问你“添加数据到底难在哪”功夫不在于 INSERT 本身而在于你写代码时有没有把这个底线放在心上。用户输入绝不可直接进入 SQL 语句这一条无论对新手还是老手都适用。4.3 慢插入排查为什么批量插入越来越慢几乎每个做过大表写入的人都遇到过数据量越到后面同样的 INSERT 耗时越长。原因概括起来有几类索引过多。每插入一行数据库都要维护所有二级索引索引树的节点分裂和页写入都有开销。索引越多插入越慢。自增锁和序列竞争。多线程并发插入同一个自增主键表在 MySQL 的某些隔离级别下插入间隙锁会互相阻塞并发反而下降。触发器。表上有 INSERT 触发器每一条插入都会触发额外逻辑比如往日志表写记录。几百条数据可能没感觉几百万条就完全是另一种体量。事务日志膨胀。长时间不提交的大事务redo log / undo log 持续增长刷盘频率上升整体速度被拖慢。实战排查时我一般按下面顺序来看执行计划或慢查询日志确认耗时卡在哪一步。检查表上有多少索引评估能否在导入场景下先删掉非必需索引导入后再重建索引。这个技巧我经常用效果特别明显。检查是否有锁等待。用SHOW ENGINE INNODB STATUS或者 SQL Server 的sys.dm_tran_locks看当前阻塞关系。检查触发器是否被异常调用。如果数据量确实很大优先考虑文件导入方式而不是逐条 INSERT。这里要提醒一句删除索引再导入、导入后重建索引只适合一次性的大批量初始化。如果是线上实时插入千万不能这么做否则查询性能会先受损。实时应用里更应该关注索引设计是否合理、是否产生了不必要的二级索引这才是长期性能的关键。如果你正在做数据初始化或迁移我建议先把日志打开记录每个批次的耗时观察哪一批开始明显变慢。很多时候你会发现不是 SQL 写法有问题而是表上的某个二级索引在数据量到达一定规模后维护成本突然飙升。这时手动对比“带索引导入”和“无索引导入”两种方案的耗时差往往能得到一个非常直观的结论。就我个人的经验来说导入几分钟和导入几十秒的差距经常就差在几个不常用的索引上。