做后端开发这些年天天跟表结构打交道被人问得最多的一句话反而是最基础的“SQL里到底怎么添加数据”一开始我也很不理解INSERT INTO谁不会写后来见过各种线上事故才明白这个动作看着简单真要做得稳、做得快、做得不重复里面全是细节。这篇文章就把SQL添加数据这件事从最简单的单行插入讲到批量导入、主键冲突、性能优化和常见报错给你一条能照着复现的经验路径。适合刚学SQL的新手也适合写过一阵子但没时间系统梳理过的研发同事。1. 添加数据前先搞清楚INSERT在数据库里是干什么的1.1 表结构就是添加数据时的“规则说明书”每张表在创建时都定好了列、类型、约束。你后来写INSERT本质就是往一张已经画好格子的表格里填一行。列名是表头上的字字段类型是格子的规格NOT NULL表示这一列不能空默认值表示你不填时有兜底主键是这一行的身份标识外键是行与行之间的引用关系。很多人加数据失败第一步就错在完全不看表结构。举例创建一张用户表CREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT, user_name VARCHAR(50) NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP );这里id是自增主键你插入时可以不写数据库会自动生成created_at带了默认值你也可不写数据库会用当前时间填上。于是最小插入语句只需要给user_nameINSERT INTO t_user (user_name) VALUES (张三);很多新手以为必须把所有列都写出来最后反而被AUTO_INCREMENT或默认时间搞得心烦。记住先看CREATE TABLE再看该写哪些列这才是加数据的第一步。不同数据库的默认值写法略有不同MySQL常用DEFAULT CURRENT_TIMESTAMPSQL Server常用DEFAULT GETDATE()PostgreSQL常用DEFAULT now()换库时别想当然。1.2 INSERT的标准语法两种写法差别很大INSERT INTO是SQL标准主要分两段指定目标表和列再给值。最规范的写法INSERT INTO 表名 (列1, 列2, 列3) VALUES (值1, 值2, 值3);如果你省略列名直接写INSERT INTO 表名 VALUES (值1, 值2, 值3);这表示你把表里所有列都按顺序填一遍。这种写法能跑但我不推荐。原因很简单一旦表结构增加一列或调整顺序这条SQL要么报列数不匹配要么把值塞进错误的列里。线上出过太多这样的事故。我的习惯是永远带列名哪怕多敲几个字至少自解释、可维护。如果你用Navicat这类工具它生成的INSERT默认也是带列名的照做就对了。另外部分数据库支持在插入后把自增主键或者一些计算列返回出来。PostgreSQL和SQLite可以写RETURNING idSQL Server可以写OUTPUT inserted.idMySQL没有标准RETURNING。实际开发里插入后立刻需要新ID的场景很常见既然数据库给了这个能力就不要插入后马上再查一次平白增加一次往返。2. 添加数据的主流姿势单行、多行、查询导入、UPSERT2.1 单行插入最直接但细节别丢单行插入最常用于手动补数据、写单元测试、初始化配置。拿刚才的用户表举例INSERT INTO t_user (user_name, id) VALUES (李四, 100);这里故意写了id说明你可以显式指定自增主键的值。但有前提id不能和已有记录冲突否则会报Duplicate entry。生产环境里我不建议手工指定自增ID除非是在做数据迁移且要保持原ID。你不写数据库自己排省心。字符串和日期是单行插入里最容易被坑的地方。字符串必须用单引号如果字符串里本身有单引号要写成两个单引号转义比如在MySQL和SQL Server里要写OBrien。日期最好用标准格式2025-07-01 12:00:00别依赖数据库区域的隐式转换否则不同环境可能解析错。时间戳类型还要考虑时区插入前先确认客户端会话和数据库时区是否一致时区不统一数据看起来就像“不翼而飞”。2.2 多行批量插入一条SQL塞进N条数据业务上要初始化一批数据常见做法是把多组VALUES拼在一条INSERT里INSERT INTO t_user (user_name) VALUES (王五), (赵六), (孙七);这条语句一次提交三行。相比循环执行三次单条INSERT它少了SQL解析、网络往返和日志刷新的次数性能提升非常明显。绝大多数主流数据库都支持这种多行VALUES但老版本Oracle有些限制早期写法要用INSERT ALL INTO t_user (user_name) VALUES (xx) INTO t_user (user_name) VALUES (yy) SELECT 1 FROM dual;。如果你在写PL/SQL要注意这个差异。批量大小不建议无脑贪大。一条SQL塞几万行SQL文本过长可能超过max_allowed_packet或数据库对SQL长度、参数个数的限制还容易造成长时间锁表。我一般控制在500到1000行一组分批次循环提交。还有个小坑调试的时候如果从日志里复制一条特别长的INSERT复制出来总是缺后半段很多时候不是SQL错了而是编辑器或控制台缓冲截断不代表数据库有问题。2.3 INSERT INTO ... SELECT表到表的“搬砖”比手动写VALUES更高效率的做法是把查询结果直接当成数据源插入目标表INSERT INTO t_user_archive (user_id, user_name) SELECT id, user_name FROM t_user WHERE created_at 2024-01-01;这一招特别适合做归档、横向扩展临时表、按条件导数据。目标表必须已存在且列数量和类型要和SELECT结果对得上。你也可以在SELECT里加DISTINCT让插入的数据先做一次去重这在数据清洗时很常用。比如把旧表里的重复手机号清理掉再入库INSERT INTO t_user_clean (mobile) SELECT DISTINCT mobile FROM t_user_temp WHERE mobile IS NOT NULL AND mobile ! ;“去除空值”这个需求用WHERE IS NOT NULL过滤比先插入再DELETE要干净。注意如果你是想快速复制一张全新表可以用CREATE TABLE new_table AS SELECT * FROM old_tableMySQL和PostgreSQLSQL Server则用SELECT * INTO new_table FROM old_table。它们把建表和导数据一步完成但通常不会复制原有索引和约束别指望得到一张带主键的完整副本。2.4 存在就更新不存在才插入UPSERT的四种写法同步数据是日常最常见的场景上游每天给我一批记录主键如果已存在就更新不存在就新增。这时别写“先SELECT判断再决定INSERT还是UPDATE”因为并发情况下两步之间有间隙两个人同时判断不存在就会一起插入还是冲突。正确做法是利用数据库的UPSERT能力。MySQLINSERT INTO t_user (id, user_name) VALUES (101, 小明) ON DUPLICATE KEY UPDATE user_name VALUES(user_name);SQLiteINSERT INTO t_user (id, user_name) VALUES (101, 小明) ON CONFLICT(id) DO UPDATE SET user_name excluded.user_name;PostgreSQLINSERT INTO t_user (id, user_name) VALUES (101, 小明) ON CONFLICT(id) DO UPDATE SET user_name EXCLUDED.user_name;SQL Server最常用的是MERGEMERGE t_user AS target USING (VALUES (101, 小明)) AS source (id, user_name) ON target.id source.id WHEN MATCHED THEN UPDATE SET user_name source.user_name WHEN NOT MATCHED THEN INSERT (id, user_name) VALUES (source.id, source.user_name);还要提一下SQLite的INSERT OR REPLACE它和ON CONFLICT DO UPDATE在语义上不一样。REPLACE会先把旧行删除再插入新行如果表里有外键指向这行可能触发级联删除或者自增ID变化非常危险。能用ON CONFLICT就不要随手用REPLACE。MERGE在某些SQL Server版本下遇到并发也可能有死锁使用前要评估不过对于日常小规模同步它已经够用了。最稳妥的兜底是给表加上唯一约束或唯一索引让数据库在约束层面拒绝真正的重复应用层再怎么错乱也不会把脏数据堆进去。补充一个安全习惯和INSERT的关系很直接不要用字符串拼接用户输入而要用参数化查询。无论你写Java、Python、Go还是Node.js都别把变量直接拼进SQL文本。比如Python里要写成cursor.execute(INSERT INTO t_user(user_name) VALUES (%s), (user_name,))而不是cursor.execute(fINSERT INTO t_user(user_name) VALUES ({user_name}))。前者让数据库把参数当纯数据处理拼字符串等于把用户输入当SQL代码执行一次注入事故能让整个库都陷入风险。这个话题点到为止理解“参数化是底线”就够了。3. 从文件、脚本和图形界面添加数据实战工具推荐3.1 命令行客户端和IDE别忘记COMMIT命令行里加数据其实很简单。以MySQL为例mysql -uroot -p use test_db; START TRANSACTION; INSERT INTO t_user (user_name) VALUES (命令行插入); SELECT * FROM t_user WHERE user_name 命令行插入; COMMIT;先开事务插入一条查一下再提交这是个好习惯。MySQL默认autocommit是开的你单独执行一条INSERT会自动提交所以我上面的写法特意开了START TRANSACTION防止手滑。SQL Server默认行为不一样很多环境仍然隐式事务需要显式COMMITOracle就更典型执行INSERT后如果不COMMIT别人会话看不到甚至你自己断开连接后数据就没了。我见过不少人用PL/SQL Developer执行完INSERT一看表里没数据以为是没写进去其实只是没有提交。如果你用Navicat管理SQL Server直接打开查询编辑器跑INSERT同样要留意事务规则。工具里的“提交”按钮有时候隐藏得比较深新手如果找不到就记住命令行里把SQL写完一定要分清楚当前是在autocommit还是手动事务模式下手动模式下最后必须有COMMIT。3.2 图形导入向导Excel、CSV、SHP都能变成表里的行日常偶发数据用INSERT还行真要从Excel里导两万行谁也不会一条条复制。Navicat这类工具早就给了导入向导右键目标表选择“导入向导”选Excel或CSV然后做列映射。这里真正决定成败的不是怎么按下一步而是三个细节字符集选对没有、日期格式认对没有、空值怎么处理。我建议先导一个几十行的子集到临时表检查完再全量导否则一个日期格式错乱就能把整批数据搞脏。简单提一个相关场景做GIS二次开发时经常要把SHP文件“添加”到数据库本质上也是在数据库表里加数据只不过加的是空间对象。比如把SHP导入PostGIS常用工具是shp2pgsql生成SQL或COPY文件后在psql里执行也可以用GDAL的ogr2ogr一步到位。转换过程中要处理坐标系、属性字段映射、几何类型转换。很多人卡在“SHP明明能打开但往库里加的时候报错”多半是字段类型不匹配或者坐标系参数没写。这个场景看着特殊背后的逻辑和Excel导入是一样的先弄清楚目标表结构再让工具把源数据翻译成SQL能理解的值。达梦DM这类国产数据库也有数据迁移工具可以把一个SQL文件导入目标库导入前先检查源SQL里的表名、字段名、字符集是否和目标库一致。不要一次性导入巨大的文件必要时用工具自带的分段选项或者把大文件拆成几份逐份导入出错范围更可控。3.3 大SQL文件导入的实操细节Linux服务器上导入一个几GB的SQL备份文件是很常见的需求mysql -uroot -p test_db /home/backup/data.sqlWindows下则通常在cmd里进到mysql/bin再执行同样的重定向。如果SQL文件是UTF-8数据库连接也指定了UTF-8基本就不会乱码。文件里如果同时包含建表和INSERT执行完别光顾着看“Query OK”要检查末尾有没有错误。重定向方式执行时错误会直接打在控制台但滚动太快往往看不到。我的做法是先把输出重定向到日志mysql -uroot -p test_db data.sql import.log 21导入结束后用grep -i error import.log定位失败点。有时候一条数据违反约束导致整批停止mysql客户端默认不是“遇到错误继续”如果你想跳过个别脏数据继续导入可以用--force参数但生产环境要谨慎因为你跳过的可能是数据质量问题后面迟早要还的。更稳妥的思路是先把所有数据导入临时表清洗完毕再INSERT INTO正式表。4. 添加数据性能优化为什么你的INSERT那么慢4.1 慢INSERT到底慢在哪一个很常见的现象代码里循环执行一万条INSERT跑了十分钟还没结束而同样数据量用批量插入几秒就完事。原因主要有四块。第一单条自动提交。每条INSERT如果单独提交数据库都要写事务日志、刷盘、更新binlog这和寄快递一样你非要把一百个包裹拆成一百趟快递时间全花在路上。第二索引维护。B树索引本质是一张“目录”每插入一条数据都要更新目录。索引越多每次变更要改的地方越多。第三锁与并发冲突。多条会话同时往同一张表插数据时为了保证唯一性和一致性数据库会加锁。并发高热度高的时候锁等待的时间可能比执行SQL还长。第四约束检查。主键唯一、外键是否存在、CHECK约束是否满足每一层都要过一遍。数据越不规范校验开销越大。4.2 亲测有效的批量导入优化方案我总结过一套“三步走”。第一步把单条INSERT改成批量。要么用一条SQL包含多组VALUES要么用PreparedStatement分批addBatch一次执行几百条。不要一上来纠结SQL文本大小先把批次定在500行左右试水。第二步用事务包住整批操作。MySQL里写START TRANSACTION; INSERT INTO t_user (user_name) VALUES (a),(b),(c); COMMIT;在程序里就关闭自动提交每批或每几万条COMMIT一次。这样把事务日志的fsync次数从N万次降到百次速度天差地别。第三步全库导入大历史数据时先把非唯一索引和外部检查关掉或移除导完再重建。注意这不是让你在生产核心时间乱来而是针对一次性数据初始化场景。比如一张表除了主键外还有两个普通索引导入500万行索引维护占了大部分时间如果先DROP掉两个普通索引导入后再CREATE INDEX很多情况下总时间反而更短。更极端的大批量性能需求可以直接使用数据库的批量加载工具MySQL的LOAD DATA INFILE、PostgreSQL的COPY、SQL Server的bcp。这些工具绕开了部分SQL层解析以数据流形式灌入比逐条INSERT快一个数量级。比如Python里psycopg2的copy_expert做了多次项目几百万行从一小时缩短到几十秒。下面是一个粗略对比参考方式常见耗时几十万行、多字段为例说明循环单条INSERT 自动提交数十分钟甚至更久事务日志、网络往返开销最大批量多行INSERT 手动事务几十秒到几分钟最简单收益最大LOAD DATA / COPY / bcp几秒到几十秒快但需要文件格式匹配可能跳过部分约束检查这对你的架构也有启发日常业务请求的插入量小用普通INSERT没问题数据平台或初始化任务永远优先走批量工具。4.3 并行导入的时候注意别帮倒忙有人一看数据量大立刻开几十个线程并发INSERT结果不仅没变快反而把数据库锁到炸。并行写同一张表的本质冲突是锁竞争不是CPU不够。真正有效的并行方式是分片按主键ID范围把数据分成多个独立区间每个线程导一个区间事务独立、数据不重合再把并发数控制在2到4个。导完一批后看锁等待时间和IO再决定加不加。不要看到“并行”就觉得一定是灵药并行SQL优化讲的是减少互相牵制而不是无脑堆线程。另外大批量写入时数据库日志和IO会被瞬间拉高。如果线上有业务流量务必错峰执行或通过限速比如每批之间sleep控制节奏避免一条导入SQL把主库拖成“慢SQL重灾区”。实测下来晚上低谷期跑批量任务是最稳妥的。4.4 遇到慢INSERT先查表结构再查锁如果你发现单条INSERT也很慢那问题大概率不在SQL本身而在锁。MySQL可以通过查询information_schema.innodb_trx看到当前未提交事务和持有的锁把长时间阻塞的会话KILL掉SQL Server可以看sys.dm_exec_requests的wait_type和blocking_session_idOracle可以用v$session_wait定位。慢INSERT不是一种先分清是“执行慢”还是“等锁慢”等锁慢就找堵它的会话执行慢再去看索引和磁盘IO。这个排查思路和查慢SELECT是同一套方法论只不过平时大家关注SELECT多一点忘了INSERT也会慢。5. 添加数据常见报错与排查实录5.1 报错速查表做开发最烦的就是INSERT报错因为错误提示往往不是“你第几个字段错了”而是“Row 1、Row 2”。我把自己遇到频率最高的几个整理成表你直接对号入座报错或现象常见原因解决方案Column count doesnt match value count at row 1INSERT列数不等于VALUES个数带列名数好括号避免省略列名Unknown column xxx in field list表里没有这个字段或大小写敏感用SHOW CREATE TABLE核对字段拼写Duplicate entry 1 for key PRIMARY主键或唯一索引冲突该场景是否该用UPSERT导入前DISTINCT去重Data too long for column mobile值长度超过字段长度ALTER TABLE加长VARCHAR入库前做长度校验Column xxx cannot be null非空字段没有提供值补值或给数据库默认值检查导入时空值处理Cannot add or update a child row: a foreign key constraint fails外键指向的父记录不存在先插父表数据再插子表检查外键值SQLiteException: no such column: test_urlORM模型和表结构不同步SQLite没自动加列ALTER TABLE ADD COLUMN更新表结构ORA-12518: 监听程序无法分发连接Oracle连接数达到上限监听无法分配调大processes/sessions参数释放空闲连接必要时重启监听中文乱码或问号客户端字符集、连接字符集、表字符集不一致统一UTF-8连接串加charsetutf8Oracle注意NLS_LANG插入很久没有反应表被锁事务未提交查锁和阻塞会话COMMIT或ROLLBACK分批重试SQLite那个no such column特别典型你用ORM建好实体类给字段加了一个test_url但SQLite本身的表结构不会因为这个改动自动加列除非你走迁移。所以你在执行INSERT时数据库才会说“根本没这个列”。这提醒我们开发中改模型后要同步改表结构不能指望框架帮你处处兜底。5.2 排查三步法先看整段再看表结构最后做最小复现不管报错多诡异我排INSERT问题永远是三步。第一步把完整报错信息复制下来不要只看“失败”两个字。错误信息里通常已经写明是哪张表、哪个字段、什么约束。第二步用SHOW CREATE TABLE核对表结构重点看字段名、字段类型、默认值、索引、外键。第三步造最小复现在一张临时表或事务里只插入一行最简单的数据然后逐步加字段不断缩小范围。绝大多数“加不进去”的问题在这一步都能定位。而且把正式数据往临时表里插一遍本身也是验证的好办法。这套方法不花哨但非常实用。有一次系统连续报错“Value too long for character string”我以为代码里没截断后来发现临时表字段是VARCHAR(20)源数据手机号区号加号码长度已经25了把字段扩大成VARCHAR(30)后问题立刻消失。不查表结构靠猜是猜不出来的。5.3 做顺手后的几个直觉先清洗、先备份、先看唯一键添加数据的场景越做越多后我会在写INSERT前默认做几件事。第一上游数据到底是不是干净的比如去重、去空值、统一大小写和日期格式在入库前用SQL过滤一下比入库后一遍遍DELETE要省事得多。第二如果数据量上千或涉及覆盖更新先在临时表跑一遍核对数量和质量没问题再落到正式表。第三永远关注主键和唯一索引这是“重复数据”的防火墙。业务逻辑可能会迟到但唯一键约束永远会兜底。顺带再提醒一句安全方面的基本功添加数据时凡是涉及用户输入都是注入风险的高发区。不要拼SQL用参数化。这段我前面说过但在报错场景里再强调一次如果你看到一条INSERT语句是拿字符串拼出来的别觉得“能跑就行”它在生产环境就是一颗定时炸弹。日志里也别打印完整SQL尤其别打印包含身份信息的参数脱敏之后再记录这是基本功。6. 写到最后关于添加数据我的三个习惯这些年写SQL我从“会写INSERT”到“敢在生产环境执行INSERT”靠的不是背语法而是三个习惯。第一个习惯动手之前先看表结构。花十秒钟SHOW CREATE TABLE省掉后面半小时的报错排查。第二个习惯任何手工操作先把事务开起来。INSERT先不要COMMIT用SELECT先确认数据确认无误再提交这个动作救过我很多次手滑。第三个习惯大批量数据永远先走临时表和批量导入。不管是CSV、SHP还是别的库导出的SQL先进临时表清洗再用INSERT INTO SELECT或UPSERT落到正式表这样即使有问题也不会把坏数据直接抹到线上。SQL里添加数据说到底就是“按照表结构把数据稳定地放进去”这一件事。步骤永远是那几个变的是你对细节的敏感度。下次再有人问你“SQL中如何添加数据”别只甩一条INSERT给他告诉他先看结构再选方式最后想清楚约束和性能这句话比任何语法都有用。