1. 为什么“添加数据”值得单独拆开讲
不少朋友第一次写SQL,接触到的第一条语句大概率就是INSERT。加一行数据嘛,难吗?语法就那么几行,敲完回车,数据进去了,看起来一切顺利。但真正在业务里跑过的人都知道,事情远没有这么简单。多人在线操作时出现主键冲突,批量导入几十万条数据时插入耗时十几分钟,并发高峰期莫名其妙出现锁等待超时,甚至因为编码问题写入中文后页面显示乱码——这些问题藏得深,但一旦爆发,排查起来往往比写十条SELECT还费劲。
“SQL中如何添加数据”这个标题看起来基础,背后牵扯的东西却不少:一条INSERT语句在数据库内部要经历解析、权限校验、执行计划生成、存储引擎写入、事务日志记录、索引维护等多个环节。每个环节都有它的脾气。你写的是INSERT INTO user (name, age) VALUES (?, ?),但数据库里发生的事远比你看到的复杂。理解这一连串过程,才能真正明白为什么有些插入写法快得飞起,而另一些写法则会把数据库拖垮。
这篇文章我会从单行插入开始,讲到多行批量插入、INSERT ... SELECT、无重复插入,再到事务控制、常见报错、性能优化和防注入写法。适合刚学SQL的初学者把基础打牢,也适合已经写了几年SQL但没系统整理过插入场景的开发者查漏补缺。所有内容都是我实际用过的方案和踩过的坑,能直接抄作业。
1.1 基础DATABASE操作背后的三个关键事实
第一条事实:看似简单的INSERT,涉及数据库的写路径全链路。无论你用MySQL、PostgreSQL还是SQL Server,一次插入都要经历语法解析、计划生成、写入缓冲、落盘确认等步骤。这不是为了吓唬人,而是为了解释一个现象:单条插入慢不代表数据库性能差,而是单次提交的固定开销太高。你一条条插入一千行,数据库要重复解析一千次SQL,每次都要走一遍完整路径,效率自然上不去。
第二条事实:插入操作和索引维护是深度绑定的。很多人以为插入就只是往表里塞数据,但如果你在表上建了索引,每次插入都需要同步维护索引结构。以最常见的B+树索引为例,插入一条记录时,数据库要定位到对应的叶子节点,检查页空间是否充足,必要时还要做页分裂。页分裂会触发额外的随机IO和锁操作,这也是为什么无索引表插入快、有索引表插入慢的底层原因。可是反过来,如果为了插入快而把所有索引都删掉,查询又成了灾难,这个平衡点是需要根据业务场景来拿捏的。
第三条事实:数据的完整性约束在插入时全部生效。主键不能重复、非空字段不能为NULL、外键必须存在、唯一索引不能冲突、字段长度不能超限、类型必须匹配——这些约束都是在INSERT执行时逐条校验的。校验本身需要读取相关数据页和索引页,所以约束越多,单条插入的开销越大。理解了这三条,后面所有关于性能优化和问题排查的讨论才有基础。
1.2 这篇文章适合谁、能帮你解决什么
如果你是刚接触数据库的初学者,这篇文章可以帮你从“会写INSERT”升级到“理解INSERT在数据库内部发生了什么”。如果你已经在业务开发中摸爬滚打过一段时间,那么第3节的性能演进路线和第5节的问题排查实录,应该能帮你解决一些实操中的困惑。
后续内容会围绕这样一个核心场景展开:一个常见的订单系统或者用户系统,需要从外部文件或接口中导入大量数据,同时在线业务还在不断写入新订单。这种混合负载下的插入策略涉及事务粒度、批量大小、冲突处理和异常恢复,几乎能把添加数据的所有要点都串起来。看完全文你会发现,添加数据这门手艺,看起来浅,实际上很值得花时间深耕。
2. 插入数据的几种主流玩法
2.1 单行插入:基础语法与隐含细节
最基本的单行插入长这样:
INSERT INTO user (username, age, email) VALUES ('zhangsan', 28, 'zhangsan@example.com');这里有一个细节需要留意:如果表里设置了自增主键,插入时可以不指定主键列,数据库会自动生成。但如果你不写列名列表,直接INSERT INTO user VALUES (...),所有列都必须按表结构顺序一一提供,这种方式非常脆弱——表结构一旦调整,这条SQL就废了。所以我的习惯是永远显式写出列名,哪怕多打几个字。显式列名配合数值插入,还能保证值的顺序清晰可控。
另一个隐含细节是DEFAULT关键字的使用。比如某列建表时设置了DEFAULT CURRENT_TIMESTAMP,插入时如果省略这一列或者显式写作VALUES (..., DEFAULT),数据库就会用默认值填充。如果该列没有设置默认值且允许NULL,省略后就存入NULL。这两者对于业务统计语义完全不同:NULL表示未知或缺失,而默认值则是明确的兜底值。我在实际项目中就因为把“允许NULL”的列直接省略插入,导致后续统计报表里出现了一大堆应该有意义却没意义的空值,分析结果几次对不上账。所以在做表设计阶段,就应该明确每一列到底该用NULL还是DEFAULT来兜底,插入逻辑也要和这个约定一致。
单行插入还有一个常见用途:序列化地插入多行。有些框架底层会自动把一次插入多行数据的操作拆成多个单条INSERT逐条执行,代码层面看不出区别,但数据库层面压力完全不同。如果你的业务写入了大量数据,先检查ORM或者中间件有没有做批量合并,这往往是最先需要优化的点。
2.2 多行批量插入:语法差异与使用技巧
批量插入的标杆写法是VALUES后面跟多组括号:
INSERT INTO student (name, class_id, score) VALUES ('A同学', 101, 85), ('B同学', 101, 92), ('C同学', 102, 78);这种写法在MySQL、PostgreSQL、SQLite中直接支持,一条SQL完成多行插入,数据库只需要解析一次、执行一次,效率远高于拆成多条单行语句。但同样的语法在SQL Server中也能用,不过在Oracle中就不支持这种标准的多行VALUES写法,Oracle更习惯用INSERT ALL INTO ... INTO ... SELECT ... FROM dual这种变体。所以跨数据库写代码的时候,这类语法差异是必须提前确认的。
批量插入的批次大小选择是有讲究的。我见过有人一次插入十万行,结果数据库直接报max_allowed_packet超限;也见过有人一次插一行,效率低得让人崩溃。经验值是一个批次在几百到几千行之间,具体要看单行数据的大小,一般来说单批次总数据量控制在几百KB到几MB比较稳妥。有人可能会好奇:为什么不干脆把一整份数据一次性塞进去?原因很简单,单条SQL的执行时长受网络传输、内存分配、锁持有时间等因素影响,一条SQL执行太久会拖长事务时间,增加锁冲突的概率。分批插入还可以在每批之后做进度记录,出问题时能断点续传。
2.3 INSERT INTO ... SELECT:复制数据的正确姿势
另一种非常实用的插入方式是INSERT INTO ... SELECT,它把查询结果直接作为插入的数据源。比如把已归档的订单表数据迁移到历史表中:
INSERT INTO order_history (order_id, user_id, amount, create_time) SELECT order_id, user_id, amount, create_time FROM orders WHERE create_time < '2024-01-01';这种写法适合表之间的数据迁移、备份、报表数据抽取。执行时要注意两点:第一,SELECT结果集的列数、顺序要和INSERT列表完全对应,否则数据库会报列数不匹配;第二,SELECT语句涉及大表时,本身的执行计划也要关注,最好在WHERE条件列上有索引,否则迁移数据期间会跑出大量全表扫描,影响线上查询性能。
在某些数据库中还有另一种语法SELECT INTO,用于根据查询结果创建新表并填充数据。比如SQL Server支持SELECT * INTO backup_table FROM orders,MySQL则用CREATE TABLE ... AS SELECT。这种用法适合快速备份整张表,但需要注意它通常不会复制原表的索引、主键、约束等结构信息。我以前用这种方式备份过一张带自增主键的表,结果新表只有一个普通整数字段,后续插入不得不手工重建主键,麻烦得很。
2.4 无重复插入:不同数据库的幂等方案
业务场景中不可避免地会遇到重复执行插入的问题。比如消息队列消费时,同一个消息因为重试被处理了两次;或者定时任务拉取数据时,上一轮跑完还没记录状态,下一轮又开始了。这时候需要的就是“有则更新,无则插入”的幂等写入。
MySQL中常见的是ON DUPLICATE KEY UPDATE:
INSERT INTO user (id, username, email) VALUES (1001, 'zhangsan', 'zhangsan@example.com') ON DUPLICATE KEY UPDATE email = VALUES(email);PostgreSQL中则用ON CONFLICT:
INSERT INTO user (id, username, email) VALUES (1001, 'zhangsan', 'zhangsan@example.com') ON CONFLICT (id) DO UPDATE SET email = EXCLUDED.email;SQL Server和Oracle更偏爱MERGE语句,不过MERGE的语法比较复杂,而且历史上出现过并发问题缺陷,使用时需要注意锁和死锁的问题。
用这类语法的时候要弄清一个关键点:它依据的是哪个唯一约束?是主键冲突还是唯一索引冲突?条件写错了,可能该覆盖的没覆盖,不该覆盖的反而被更新了。我曾经在一个用户表上用手机号作为唯一标识做冲突更新,但业务方后续要求改成按用户ID更新,结果一批数据把多个用户资料串了号,还好是在测试环境及时发现,没有酿成生产事故。这类幂等方案非常方便,但一定先想清楚冲突判断依据。
3. 从单条插入到高效导入的演进路线
3.1 逐条插入为什么慢:三个瓶颈分析
不少初学SQL的开发者都有过这样的经历:从代码里循环调用INSERT语句,一次插一条,几百条数据下来还好,几万条就开始等得不耐烦。为什么逐条插入慢?三个瓶颈是最主要的。
第一是SQL解析开销。数据库每收到一条SQL,都要做词法解析、语法检查、权限校验、执行计划生成。即便你的INSERT语句结构完全相同,只是参数值不一样,数据库依然要走一遍完整的解析流程。如果换成参数化预处理语句,准备一次之后多次执行,就能省掉重复解析的部分成本。第二是事务日志同步开销。每一条INSERT如果独立提交,意味着每一次都要触发一次事务提交、一次日志写入、一次落盘确认。磁盘的同步IO是最贵的操作之一,一万条插入就是一万次同步IO,不慢才怪。第三是网络往返开销。应用端和数据库每交互一次,都涉及网络传输、等待响应。如果数据库和应用不在同一台机器上,这种往返延迟会放大得更明显。
3.2 事务与批量提交:控制粒度的关键
把多条INSERT放进同一个事务提交,是提升插入性能最简单也最有效的手段。事务的本质是把多次操作打包成一个原子单位,要么全部成功,要么全部回滚。从性能角度看,多条插入合并在一次事务里提交,日志刷盘从多次变成一次,网络往返也大幅减少。
BEGIN; INSERT INTO log (level, message) VALUES ('INFO', '第1行日志'); INSERT INTO log (level, message) VALUES ('ERROR', '第2行日志'); COMMIT;但是事务的批量也不是越大越好。事务太大意味着锁持有的时间变长、回滚段占用变大、一旦失败所有改动都要撤销。以我的经验,一个事务内存活的数据条数在几百到几千之间比较合适,既能获得批量提交的性能提升,又不会因为单事务过大造成副作用。更关键的是,如果导入任务可以分成多个批次,每批完成就提交一次,中途失败时回滚范围可控,也不用担心重启后一切重来。有人在导入大量数据之前会把表调整为无日志模式(比如MySQL的InnoDB下关闭binlog或者换用MyISAM引擎),这类操作性能提升确实明显,但风险也大,生产环境不建议折腾。数据安全永远排在性能前面。
3.3 高效导入的高级姿势:临时表、本机文件导入与双写校验
当数据的体量到了百万行级别,即使分批使用多合一INSERT,性能和便利性也开始显得不够用。这时候更专业的做法是使用数据库自带的高效导入工具。MySQL有LOAD DATA INFILE,PostgreSQL有COPY,SQL Server有BULK INSERT。它们的共同原理是把数据文件直接从服务器本机或客户端批量加载到表中,绕开了SQL解析层,以接近存储引擎极限速度的方式写入。
这类工具的注意事项也比较多。文件路径、字段分隔符、行分隔符、转义符、字符集都需要提前指定好,否则会出现数据错位、乱码、NULL值识别异常等问题。我记得有一次用LOAD DATA导入一批CSV时,某行文本数据里包含了换行符,导致这一行被错误地拆成了两条记录,直到后续做数据校验时才发现总数对不上。后来在导入前增加了预处理脚本,把所有字段内的换行符替换成占位符,问题才彻底解决。
用文件导入时还要注意唯一键去重。数据文件里如果存在重复ID,导入过程中会直接失败并中断,哪怕只有一行重复,整个导入任务也会报错。我的习惯是先做一次外部清洗,将主键去重后再导入,或者在导入时把数据先加载到临时表,用SQL做一次去重后再插入正式表。这种“先清洗、再暂存、后写入”的双写模式,虽然多一步操作,但能极大降低导入失败的概率。导入完成之后别忘了做全量校验,最简单的方式就是对比源文件行数和目标表行数,以及关键字段的SUM值是否匹配。
4. 一次订单数据导入的完整实操复盘
4.1 场景描述与目标拆解
某产品系统需要把一份历史订单数据从离线文件导入在线数据库。文件大约有八十万行,字段包括订单编号、用户编号、订单金额、支付方式、订单状态、创建时间。在线数据库使用的是MySQL,订单表已经在线运行,表上建有主键索引和用户编号上的普通索引。导入期间,在线业务仍然在正常写入新订单。目标是尽可能在不影响在线业务的情况下完成数据导入,同时保证数据完整、可以重复执行。
这个场景非常有代表性,因为它同时涉及三个维度的权衡:批量插入的效率、与在线写入的冲突控制、以及失败后的可恢复性。我把整个任务拆成了四个阶段:结构确认、索引策略调整、分批事务导入、事后校验。
4.2 表结构与索引设计
订单表的简化结构如下:
CREATE TABLE orders ( order_id BIGINT PRIMARY KEY, user_id BIGINT NOT NULL, amount DECIMAL(10,2) NOT NULL, pay_type TINYINT NOT NULL, status TINYINT NOT NULL, create_time DATETIME NOT NULL, KEY idx_user_id (user_id) ) ENGINE=InnoDB;表上有两个索引,一个是主键索引,一个是普通索引。导入八十万数据时必须考虑到:每次插入不仅仅写入聚簇索引,还需要维护idx_user_id这颗二级索引树。索引越多,插入成本越高。所以在做大批量导入时,有两种策略可选:一种是保留索引直接插,插入速度会慢一些;另一种是先删除非必要索引,导入完成后重建。后者的思路是,插入时完全不做二级索引维护,写入更快,等数据稳了再统一建索引,建索引的操作虽然也要时间,但通常比插入期间同步维护索引要高效不少。
这里需要强调一点:删除索引会直接影响在线查询。如果业务依赖该索引做实时查询,贸然删掉有风险。我当时评估后选择了先保留索引,用较慢的批量方式导入,因为在线业务对查询响应要求很高,不能拿业务可用性换导入速度。如果你的导入任务发生在业务低谷期,并且导入表不是热点表,那么先删索引再重建会快很多。
4.3 分批次插入的执行步骤与参数选择
有了表结构和索引策略,接下来是具体的导入节奏。我采用的方式是把源文件按每批五千行拆分,每批开启一个事务,执行一次多行INSERT。五千行的依据有两个:一是单批次数据量控制在合理范围,避免一条SQL过大;二是五千行插入的事务耗时较短,减少锁持有时间。实际执行下来,每批的耗时大约在几百毫秒到一秒多之间,整体导入八十万行大约用了二十来分钟,没有出现锁等待超时。
插入SQL长这样:
INSERT INTO orders (order_id, user_id, amount, pay_type, status, create_time) VALUES (1, 10001, 99.90, 1, 2, '2023-05-01 10:00:00'), (2, 10002, 129.00, 2, 2, '2023-05-01 10:05:30'), ... (5000, 10086, 59.00, 1, 1, '2023-05-03 23:59:59');关键参数方面:MySQL的max_allowed_packet默认值一般在几MB到几十MB之间,五千行的数据量通常不会触顶,但如果插入的是长文本字段,就需要适当调大这个参数。事务隔离级别使用的是默认的REPEATABLE READ,批量插入时并发冲突不大,这个级别没有造成额外问题。如果在线并发写很频繁,可以考虑降低到READ COMMITTED,能减少间隙锁带来的锁竞争,不过这种调整需要与应用层的读取逻辑配套评估,不能随手改。
批与批之间可以不用特意sleep,但如果发现数据库IO压力偏大或者主从延迟在爬升,适当加几百毫秒间隔是个好习惯。主从延迟这点特别容易被忽略——写入一旦太快,从库同步跟不上,在线业务读取从库的接口就会出现数据不一致的告警。
4.4 导入结果校验与回滚预案
数据处理最怕的就是导入完了才发现数据不对。我的校验分成三个层次。第一个层次是行数校验:源文件里的记录总数和orders表新增的总数对比,必须完全一致。第二个层次是关键字段的汇总校验:比如订单金额的总和、状态枚举值的分布,这些统计信息在源文件中是可以提前算好的,导入完成后用SELECT SUM(amount), status, COUNT(*) FROM orders WHERE order_id BETWEEN 起始ID AND 结束ID GROUP BY status来对比。第三个层次是抽样检查:随机挑出若干条记录,逐字段比对源文件和数据库内容是否完全一致。
导入之前我还会做一次幂等设计。大致思路是增加一个import_batch_id字段,每一批数据带上同一个批次标识,万一导入中途失败,可以针对这个批次做定向清理:
DELETE FROM orders WHERE import_batch_id = '20240518_full';然后重新执行该批次导入即可,不需要把整张表清空重来。如果线上表不能加额外字段,也可以在导入前记录某个自增ID的边界值,失败后只清理大于该边界的记录。回滚预案的价值在于:有备无患,出了事不至于手足无措。我在做任何批量数据变更之前,都会先确认好回滚方案,哪怕最终用不上。
5. 常见问题与排查技巧实录
5.1 自增主键冲突:数据恢复了,ID乱套了
插入数据时最典型的报错就是主键冲突。比如从备份里恢复数据,原表里的主键ID和其他来源的数据“撞车”了;或者一个表中既有手工插入的固定ID数据,又有依赖自增机制自动生成ID的数据,两者不小心交叉了。
如果是自增主键冲突,一个容易被忽视的问题是:即使你删除了部分行,自增计数器的值并不会自动回退。举例来说,orders表当前自增ID已经到5000,你把ID为5000的行删掉再新插一条,新ID会是5001而不是5000。这个特性在大多数主流数据库中是一致的,它不是bug,是避免ID复用的设计。如果你需要在导入后重置自增计数,可以手动调整(MySQL的ALTER TABLE orders AUTO_INCREMENT = n,SQL Server的DBCC CHECKIDENT)。但请一定想清楚再操作,如果业务上已经引用了旧ID,贸然重置会让之前暴露出去的ID变得不可追溯。
批量导入含自增ID的数据时,有个前置动作非常有用:导入前查一下当前AUTO_INCREMENT值,确保要导入的ID范围和它不冲突。如果真的有冲突,常见的做法是给导入数据统一加上一个偏移量,比如在原ID基础上加100000,保证和线上现有ID区间错开。这样既保持数据可区分,又避免与在线写入的自增数据碰撞。
5.2 中文乱码与字符集问题
插入中文后查询出来是乱码,这在SQL新手时期几乎人人都会遇到。乱码的根源百分之九十是字符集不一致:数据库连接字符集、客户端工具字符集、表字段字符集三者没有统一。
先说判断方法。可以用这条SQL检查表的字符集设置:
SHOW CREATE TABLE orders;在看到的结果里检查CHARSET是不是utf8mb4。utf8mb4是MySQL中真正完整支持中文和emoji的字符集,而utf8在MySQL里其实不是标准的四字节UTF-8,遇到部分生僻字和emoji会直接报错或乱码。建表时优先用utf8mb4,连接时也显式指定:
SET NAMES utf8mb4;对于已经乱码的数据,恢复起来很麻烦。如果是插入时就存坏了,需要把源数据重新清洗后再插入;如果只是连接层面没设置对,那么改对连接参数后重新查询可能就恢复显示正常。乱码问题的核心经验就一条:在建表、建连接的第一步就统一字符集,后面能省下无数痛心的排查时间。
5.3 插入超时、死锁:并发写入的噩梦
并发场景下的插入超时和死锁比乱码更难排查。一次插入操作如果迟迟拿不到锁,最终会触发锁等待超时,比如MySQL InnoDB的默认锁等待超时时间是50秒,超过就会报Lock wait timeout exceeded。而死锁则是两个事务各自持有对方需要的锁,互相等待,数据库会检测到死锁并强制回滚其中一个事务,客户端表现为“事务突然失败”。
解决死锁的思路有三个方向。第一,统一访问顺序,让并发事务都按照同样的顺序去操作表和行,避免交叉加锁。比如多个事务都要更新用户表再更新订单表,那就规定所有事务都按这个顺序执行,破坏环路条件。第二,缩小事务范围,尽早提交,减少锁的持有时间。第三,给高频更新的热点行做拆分或排队,实在不行就在应用层加分布式锁。实际开发中我见过最典型的死锁场景是:多个事务同时先更新订单再更新用户,而另一个事务先更新用户再更新订单,两个方向交叉,死锁就出现了。把方向统一之后,问题立刻消失。
5.4 NULL与DEFAULT:空值的两种姿势
NULL和DEFAULT非常容易被混用,但它们语义不同,并且会直接影响查询结果。一个字段如果建表时设置了DEFAULT '未知',插入不指定值时数据库会填上“未知”;如果字段允许NULL且没有默认值,插入不指定值就是NULL。用SELECT ... WHERE name IS NULL查出来的是后者;而WHERE name = '未知'查出来的是前者。两者差一个等号,业务上却可能代表完全不同的含义。
我最近遇到的一个真实案例:一张客户备注表里,运营人员导入的客户数据中少数行备注为空,其中一部分被存成了NULL,另一部分因为默认值设置被存成了空字符串。导出报表时,业务方要求把空备注统一显示为“暂无备注”,结果脚本里只判断了IS NULL,导致空字符串的那部分客户全部落空。这类问题排查起来非常费时,唯一的根治办法是在表设计阶段就明确每个字段的默认值策略,并且在插入代码中显式写入该列的取值,不依赖数据库的隐式填充。这样即使后续表结构调整,插入逻辑也是自解释的,不会因为默认值差异产生隐蔽的数据不一致。
6. 写入安全与最佳实践
6.1 SQL注入不是开玩笑
写过SQL的人对SQL注入应该都不陌生。拼接字符串构造INSERT是最容易中招的写法,比如:
INSERT INTO user (username, password) VALUES ('" + username + "', '" + password + "');一旦输入值里含有单引号、分号、注释符等特殊字符,这条语句就完全可能被改写成你意想不到的形式。轻则数据被篡改,重则整张表被删,而且后果不可逆。插入操作和查询操作一样需要防注入,甚至更危险——查询还能限制权限,插入往往直接就是写操作。
6.2 参数化写法怎么选
防止SQL注入最可靠的方案是参数化查询,也叫做预处理语句。以各种语言的驱动为例,统一范式是把SQL骨架和参数分离开,让数据库内部把参数作为纯数据处理,而不是SQL语法的一部分。
// 用JDBC参数化 PreparedStatement ps = conn.prepareStatement( "INSERT INTO user (username, email) VALUES (?, ?)"); ps.setString(1, "zhangsan"); ps.setString(2, "zhangsan@example.com"); ps.executeUpdate();# 用Python参数化 cursor.execute( "INSERT INTO user (username, email) VALUES (?, ?)", ("zhangsan", "zhangsan@example.com") )参数化不只在安全层面有价值,在性能层面同样有。对重复执行的同构INSERT语句,参数化可以让数据库缓存执行计划,降低解析成本,这对循环插入场景是非常可观的优化。如果你使用的ORM框架,绝大部分框架底层都会自动使用参数化执行,但手工拼接SQL的地方一定要小心翼翼地检查一遍。日常代码审查中如果发现字符串拼接式INSERT,基本可以直接打回重写。
6.3 个人实操心得汇总
写到这里,我把这些年和INSERT打交道攒下的经验浓缩成几条,对想要踏实掌握SQL数据插入的朋友也许最有价值。
第一条是关于“先想清楚再插数据”。插入数据前,先花时间明确表结构、索引策略、字符集、事务粒度和冲突处理方式。磨刀不误砍柴工,想清楚再动手,能规避掉大部分线上事故。
第二条是“每次操作都要有回滚预案”。无论是一次几十行的配置数据调整,还是几十万行的迁移任务,动手前把回滚方式想好,备好对应的清理SQL。表面上多花了点时间,实际上是在给自己的晚上睡眠上保险。
第三条是“永远假设数据会出错”。导入完成之后做一次校验,行数、汇总、抽样都是快速有效的,成本低但价值极高。另外,不要把校验逻辑写得太复杂,简单直接的检查更容易坚持执行。
第四条是“性能和安全并重,但安全永远优先”。批量插入再快,如果拆掉了安全防线,那就是一次事故的敲门砖。好的写入方案应该是在安全性约束之内把性能榨到极致,而不是为了性能牺牲数据安全。
最后补充一个很实用的小技巧:在开发和测试过程中,把INSERT语句放进事务里跑,不要直接COMMIT。先用SELECT确认插入结果没问题,再提交事务。这一套动作配合可视化工具,对数据正确性的确认效率提升非常明显。数据插入看着简单,真正下功夫琢磨之后,就会发现自己写的SQL明显更稳、更快、更不容易出问题了。