直接逐条insert或update百万级数据是设计反模式,必须通过批量提交、避免隐式转换、确保索引生效来规避单行事务开销、锁竞争和日志刷盘放大。

直接用循环逐条 INSERT 或 UPDATE 百万级数据,基本等于主动给数据库“上刑”——不是语法错,是设计反模式。真正有效的方案必须绕过单行事务开销、锁竞争和日志刷盘放大,核心就三条:批量提交、避免隐式转换、让索引真正生效。
为什么存储过程里 WHILE 循环插入慢得离谱?
每条 INSERT 默认触发一次 redo 日志刷盘(innodb_flush_log_at_trx_commit = 1),加上语句解析、权限检查、行锁获取,实际吞吐常低于 300 行/秒。更致命的是,没加 START TRANSACTION 时,MySQL 会为每一行自动开启又提交一个事务,日志写放大严重。
- 未显式事务控制 → 日志刷盘次数 = 行数
- 循环内调用
RAND()、NOW()或字符串拼接 → 每次都要重新计算,无法预编译 - 目标表有唯一索引或外键 → 每插一行都触发全量校验,开销线性增长
- 没设
SET NOCOUNT ON(SQL Server)或等效抑制 → 客户端反复收发影响计数的元数据包
INSERT INTO SELECT 怎么用才不锁死线上服务?
INSERT INTO SELECT 在 InnoDB 的 REPEATABLE READ 隔离级别下默认走当前读,会对源表扫描范围加临键锁(next-key lock)。没走索引就全表扫描,等效于锁表;走了索引也锁住大量区间,极易阻塞其他写入。
- 执行前先跑
EXPLAIN确认是否命中索引,扫描行数是否可控 - 分批必须用主键范围切分,例如
WHERE id BETWEEN 1000001 AND 1010000,别依赖子查询LIMIT(MySQL 5.7 不支持) - 临时调低隔离级别到
READ COMMITTED可缩小锁范围,但需业务确认能容忍非重复读 - 冲突处理别用裸
INSERT INTO SELECT,优先选INSERT IGNORE INTO SELECT或INSERT ... ON DUPLICATE KEY UPDATE
ORM 框架批量插入到底该用哪个 API?
ORM 层的批量操作不是“有没有”,而是“绕不绕过 ORM 开销”。bulk_insert_mappings(SQLAlchemy)、Fastest.BulkCopy(SqlSugar)、executemany(DBAPI 底层)这类接口才是百万级的正确入口;add() + commit() 或 bulk_save_objects 仍带对象跟踪和事件触发,性能差一个数量级。
- 纯数据导入场景,直接用
bulk_insert_mappings或db.Fastest<t>().BulkCopy(list)</t> - MySQL 下 SqlSugar
BulkCopy底层走LOAD DATA,比手写多值INSERT快 5–10 倍 - SQL Server 用
SqlBulkCopy,PostgreSQL 用COPY,别强行统一接口 - 避免在批量前把全部数据加载进内存,改用流式分页拉取 + 分批提交
最易被忽略的点:事务边界和索引有效性永远比“用什么框架”重要。哪怕用了 BulkCopy,如果目标表缺唯一索引导致唯一约束校验退化成全表扫描,或者事务包了 50 万行导致 undo 日志暴涨、主从延迟飙升,照样卡死。性能优化不是堆工具,是让每一步操作都落在数据库引擎的高效路径上。











