同样是插 10 万行,最快的写法比最慢的快 103 倍

同样是插 10 万行,最快的写法比最慢的快 103 倍

sql 复制代码
写法                           耗时        吞吐 (行/s)    相对最慢
-----------------------------------------------------------------
逐条 INSERT, 每条一个事务      52.54 s        1,903        1.0x
逐条 INSERT, 全部一个事务      16.06 s        6,228        3.3x
批量 INSERT, 每批 100 行        1.49 s       66,951       35.2x
批量 INSERT, 每批 1000 行       1.42 s       70,179       36.9x
LOAD DATA LOCAL INFILE         0.51 s      197,028      103.5x

52 秒 vs 0.51 秒。 数据完全一样,表结构完全一样,只是写法不同。

(这还是走 Unix socket 的本地连接。加上生产环境的网络往返,第一行会更惨------参考之前那篇 N+1 的实测:本地 17 倍的差距,在 1ms RTT 下变成 170 倍。)

第一层:事务

bash 复制代码
每条一个事务     1,903 行/s
一个大事务       6,228 行/s      3.3x

默认 autocommit=1 时,每一条 INSERT 都是一个完整的事务 :开启、写 undo log、写 redo log、提交、fsync。

那个 fsync 是大头。实测把 innodb_flush_log_at_trx_commit 调松:

ini 复制代码
flush=1   2,380 行/s    每次提交都 fsync (最安全, 默认)
flush=2   4,124 行/s    写 OS 缓存, 每秒 fsync       1.7x
flush=0   6,005 行/s    每秒写 + 每秒 fsync          2.5x

fsync 占了逐条提交开销的 2.5 倍。

但注意:把 10 万条放进一个事务只拿到 6,228 行/s,而批量 INSERT 有 70,000 行/s。也就是说,消掉 fsync 之后还有 11 倍的差距没解释。

第二层:每条 SQL 的固定开销

剩下那 11 倍,来自每一条 SQL 语句本身的成本:

  • 一次客户端 → 服务端的往返(即使是 Unix socket 也有系统调用)
  • SQL 文本的词法分析、语法分析
  • 查询优化器走一遍(哪怕是最简单的 INSERT)
  • 权限检查、表元数据查找
  • 一次 InnoDB 的行插入 + 索引维护

前面那一长串,和「插入一行」这件事本身没什么关系,但每条都要付一次。

批量 INSERT 把 1000 行合并成一条 SQL:

sql

sql 复制代码
INSERT INTO t(uid, city, amount) VALUES
  (1,'北京',100),
  (2,'上海',200),
  ... 再写 998 行

解析一次、优化一次、往返一次,然后连续插 1000 行。固定开销被摊薄了 1000 倍。

批大小:收益在 100 就饱和了

markdown 复制代码
每批行数      耗时       吞吐 (行/s)    相对逐条    单条 SQL 长度
-------------------------------------------------------------
    1      15.65 s        6,389        1.0x        ---
   10       2.78 s       35,970        5.6x       1 KB
   50       1.76 s       56,755        8.9x       5 KB
  100       1.53 s       65,270       10.2x       9 KB
  500       1.40 s       71,591       11.2x      46 KB
 1000       1.43 s       69,952       10.9x      93 KB
 5000       1.52 s       65,971       10.3x     464 KB
20000       1.41 s       70,692       11.1x    1855 KB

最大的一跳是从 1 到 10------5.6 倍。 之后迅速收敛,到 100 就拿到了 10.2 倍,500 之后完全是平台期。

所以实践上的建议很简单:批大小 100 到 1000 之间随便选,不用纠结。

追求更大的批次反而有风险:

  • 撞上 max_allowed_packet (MariaDB 默认 16 MB,MySQL 8 默认 64 MB),报错 Packet too large
  • 单个事务太大,undo log 膨胀、锁持有时间长、主从复制延迟
  • 失败时整批回滚,重试成本高

第三层:LOAD DATA 的 197,028 行/s

bash 复制代码
LOAD DATA LOCAL INFILE    0.51 s    197,028 行/s    103.5x

比批量 INSERT 还快 2.8 倍。

因为它根本不走 SQL 那条路:

  • 没有 SQL 解析------数据是按分隔符切的,不是按语法树
  • 流式读取------不需要把整批数据构造成一个巨大的字符串塞进内存
  • 批量构建索引------可以更高效地维护 B+ 树
  • 更少的内部锁获取

代价是它很「粗暴」:

  • 文件格式要和表结构严格对齐
  • 错误处理弱(一行格式错了,要么整批失败,要么静默变成默认值)
  • LOCAL 关键字涉及客户端读文件,有安全考量(MySQL 8 默认关闭 local_infile,要显式打开)

所以它的定位是数据导入、离线迁移、ETL 落库,不是在线业务写入。

一张选型表

场景 用什么 预期吞吐
在线单条写入(下单、注册) 单条 INSERT 不用优化,它本来就是单条
批处理任务、消息消费 批量 INSERT,每批 100~1000 几万行/秒
数据导入、迁移、回填 LOAD DATA 十几万行/秒
主键已知的 upsert INSERT ... ON DUPLICATE KEY UPDATE 批量 接近批量 INSERT

另外几个实用点:

1. 先删索引,灌完再建。 大批量导入时,逐行维护 5 个二级索引是很大的开销(之前那篇索引实测里,4 个二级索引让写入吞吐掉了 33%)。先 DROP INDEX,灌完 ADD INDEX 一次性构建,通常更快。

2. 按主键顺序插入。 之前那篇 UUID 主键的实测里,随机主键让插入慢 11.8 倍。批量导入前先按主键排序,收益很大。

3. ORM 的「批量」不一定是批量。 很多 ORM 的 bulk_create / saveAll 只是在一个事务里循环单条 INSERT------那只能拿到 3.3 倍,不是 36 倍。看日志确认它真的发出了多值 INSERT。

4. rewriteBatchedStatements=true。 JDBC 的 MySQL 驱动默认不会 把 addBatch 合并成多值 INSERT,必须显式开这个连接参数。这是 Java 生态里最常见的一个「以为自己在批量」的坑。

最后

52 秒和 0.51 秒的差距,不在数据库,也不在磁盘。

在于你跟数据库说了 10 万次话,还是说了 1 次。

相关推荐
weixin_417396831 小时前
星级酒店境外电视频道怎么合法接入?合规要点与核对清单
开发语言·前端·php·1024程序员节·综合布线
hiahiahia1231 小时前
Structured Output
前端·人工智能
FRing031 小时前
JavaScript入门到精通三十一(原型继承、原型链)
开发语言·前端·javascript
山雀~3 小时前
CSP 安全漏洞修复
前端·网络协议
陌离3 小时前
Go-Gui:一套 无限接近 现代化UI的 GPU绘制界面组件框架
前端
咚咚咚小柒3 小时前
【前端】LangChain.js学习
前端·javascript·人工智能·typescript·langchain·vue·ai编程
2401_885885044 小时前
巴林语音通知接口如何对接?系统告警语音API接入教程
android·前端·网络·python·语音识别
java1234_小锋4 小时前
【技术专题】CSS3 网页技术 - CSS3 盒模型
前端·css·css3
计算机魔术师4 小时前
手握芯片、又要吞下开源分发入口,英伟达被盯上了
前端