一、先说结论:最高效的写法长什么样
无论你用哪种数据库,效率最高的批量插入,本质上都在做同一件事:把多次网络往返、多次事务提交、多次 sql 解析,合并成尽可能少的次数。
以 mysql 为例,效率从高到低大致是:
- ✅ load data infile
- ✅ 单条 insert 多 values(insert into t values (...),(...),(...))
- ✅ 批量 + 手动事务(关闭 autocommit)
- ✅ 单条 insert + autocommit(默认)
- ❌ 循环里一条一条 insert
下面我们逐层拆解为什么,以及怎么写才对。
二、为什么“一行一行插”这么慢?
先看一段最常见的反面教材:
for (user user : userlist) {
jdbctemplate.update(
"insert into user(name, age) values(?, ?)",
user.getname(), user.getage()
);
}
表面看没毛病,但背后发生了什么?
每一次 insert 的隐藏成本
- 网络往返(rtt) :应用 → 数据库 → 返回结果,一次往返通常 0.5–2ms。
- sql 解析与执行计划:每条 sql 都要解析、权限校验、生成执行计划。
- 事务日志刷盘(redo / binlog) :默认
autocommit=on,每插一行就刷一次日志。 - 索引维护:每行插入都要更新聚簇索引 + 二级索引。
假设 1 万条数据,每行 1ms 纯插入成本,光网络往返就可能再吃掉 1–2 万次 rtt,整体从几百毫秒拖到几十秒。
三、第一层优化:单条 sql,多个 values
写法示例
insert into user (name, age)
values
('alice', 18),
('bob', 20),
('charlie', 22),
-- ... 更多行
('zoe', 25);
为什么快?
- 一次网络往返
- 一次 sql 解析
- 一次事务提交(或合并提交)
- 索引批量维护,减少随机 io
实测对比(mysql 8.0,本地 ssd)
| 方式 | 1 万行耗时 |
|---|---|
| 逐条 insert | ~12 秒 |
| 单 sql 多 values | ~0.3 秒 |
性能差距 30–40 倍。
注意事项
1. 单条 sql 别太大
mysql 有 max_allowed_packet(默认 64mb),sql 超大会报错。
推荐每批 500~2000 行,视单行字段大小而定:
list<user> batch;
for (int i = 0; i < users.size(); i += 1000) {
batch = users.sublist(i, math.min(i + 1000, users.size()));
insertbatch(batch);
}2. 字段顺序要对齐
-- ❌ 容易出错
insert into user values ('alice', 18), (20, 'bob');
-- ✅ 显式指定列
insert into user (name, age) values (?, ?), (?, ?);
四、第二层优化:手动控制事务
即使你写了多 values,如果 autocommit=on,数据库仍可能每行/每批频繁刷日志。
正确姿势
start transaction; insert into user (name, age) values (...),(...),...; insert into user (name, age) values (...),(...),...; commit;
效果
- 多次插入共享一次事务
- redo / binlog 只刷一次
- 性能再提升 2–5 倍
jdbc 写法
conn.setautocommit(false);
preparedstatement ps = conn.preparestatement(
"insert into user (name, age) values (?, ?)"
);
for (user u : users) {
ps.setstring(1, u.getname());
ps.setint(2, u.getage());
ps.addbatch();
}
ps.executebatch();
conn.commit();五、第三层优化:数据库专属“大杀器”
mysql:load data infile
这是 mysql 批量导入的天花板。
load data infile '/data/users.csv' into table user fields terminated by ',' lines terminated by '\n' (name, age);
为什么最快?
- 跳过 sql 解析层
- 直接按行解析、批量写页
- 事务日志批量写入
- 可以禁用索引后重建
性能对比
| 方式 | 100 万行耗时 |
|---|---|
| 逐条 insert | ~20 分钟 |
| 多 values | ~30 秒 |
| load data | ~3–5 秒 |
注意
- 文件需在 mysql 服务器上(或用
local走客户端) - 权限要求高
- 不适合实时业务,适合初始化/迁移
postgresql:copy 命令
pg 的等价方案是 copy,性能同样碾压 insert。
copy user (name, age) from '/data/users.csv' delimiter ',' csv;
jdbc 用 copymanager api,性能比批量 insert 快 5–10 倍。
六、索引与表结构的隐藏陷阱
1. 插入前考虑“先删索引,再建回来”
如果你要导 百万级以上 数据:
alter table user disable keys; -- myisam -- 或手动记录索引,导入后重建 insert ... create index ...
innodb 不能 disable keys,但可以:
- 导入前不建二级索引
- 导入完再
create index(比边插边维护快很多)
2. 自增主键 vs uuid
- 自增 id:顺序写,页填充率高,插入快
- uuid / 随机字符串:随机写,页分 裂频繁,插入慢 3–5 倍
批量导入时,主键顺序越连续,性能越好。
3. 关闭不必要的约束
大批量导入期间可临时关闭:
set foreign_key_checks = 0; -- mysql set unique_checks = 0; -- 导入完再打开
七、不同语言的“正确姿势”
mybatis
<insert id="batchinsert">
insert into user (name, age)
values
<foreach collection="list" item="u" separator=",">
(#{u.name}, #{u.age})
</foreach>
</insert>配合:
rewritebatchedstatements=true # mysql jdbc 参数
python(pymysql / sqlalchemy)
with engine.begin() as conn:
conn.execute(
user.__table__.insert(),
[{"name": u.name, "age": u.age} for u in users]
)sqlalchemy executemany() 会自动批处理。
八、避坑清单
| 坑 | 后果 | 解法 |
|---|---|---|
| 循环里单条 insert | 慢 10–100 倍 | 用多 values 或 batch |
| autocommit 开着 | 频繁刷日志 | 手动事务 |
| 单条 sql 太大 | 超 max_allowed_packet | 分批 500–2000 |
| 表有太多索引 | 插入慢 | 导入前删索引 |
| 用 uuid 主键 | 页分 裂 | 改用自增或雪花 id |
| 批量插还开触发器 | 每行触发一次 | 导入前 disable |
| 网络延迟高 | rtt 放大 | 合并批次 + 长连接 |
九、决策树:你该用哪种方式?
数据量 < 1 万?
└─ 是 → 多 values + 手动事务 ✅
数据量 1 万 ~ 100 万?
└─ 是 → 分批多 values + 手动事务 + 关索引 ✅
数据量 > 100 万?
└─ 是 → load data / copy + 无索引导入 ✅
需要实时写入?
└─ 是 → 消息队列攒批 → 定时刷库 ✅
十、总结
批量插入的最高境界,不是“写一条更快的 sql”,而是“尽量少写 sql”。
记住三句话:
- 能一次插 1000 行,就别插 1000 次 1 行
- 能一次提交,就别提交 1000 次
- 能用 load data / copy,就别用 insert
把网络往返、sql 解析、事务刷盘这三座大山削平,批量插入就能从“分钟级”变成“秒级”。
以上就是mysql数据库批量插入数据的高效写法与优化技巧详解的详细内容,更多关于mysql批量插入数据的资料请关注代码网其它相关文章!
发表评论