Node.js + MySQL 批量插入性能优化实战:从 for + await 到高性能方案
在实际项目中, 批量写入数据库 是一个非常常见但又极易踩坑的问题。 最近在处理企业微信客户数据同步时,就遇到了一个典型的性能瓶颈: 数据插入速度极慢 。

本文将从一个真实代码示例出发,逐步分析问题原因,并给出 可落地、可扩展、适合生产环境的优化方案 。
一、问题背景:for + await 插入为何这么慢?
最初的代码类似下面这样:
for (let j = 0; j < res.data.external_userid.length; j++) {
await db.query(
`insert ignore into wj_qywx_customer values(null, '${res.data.external_userid[j]}', null)`
);
}表结构简化示例
CREATE TABLE wj_qywx_customer (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
external_userid VARCHAR(64) UNIQUE,
remark VARCHAR(255)
);现象
- 数据量:几百 / 几千条
- 执行时间:几秒到几十秒
- CPU、DB 连接数都在飙升
二、性能瓶颈分析(为什么会慢)
1️⃣ 串行执行(致命)
await db.query(...)- 每一条 SQL 必须等上一条执行完成
- 本质是: N 次网络往返 + N 次 SQL 解析 + N 次提交
2️⃣ 自动提交事务(隐形杀手)
MySQL 默认是 autocommit = ON ,意味着:
每条
INSERT都是一个独立事务每条都涉及:
redo log
binlog
磁盘 flush
3️⃣ 数据库完全无法发挥批处理优势
数据库最擅长的是:
一次处理多条数据,而不是被当成 for 循环的函数调用
三、正确解法一:批量 INSERT(强烈推荐)
✅ 核心思想
把 N 条 INSERT 合并成 1 条 SQL
优化后的代码
if (res.data.external_userid.length > 0) {
const values = res.data.external_userid
.map(id => `('${id}')`)
.join(',');
await db.query(
`INSERT IGNORE INTO wj_qywx_customer (external_userid) VALUES ${values}`
);
}性能提升幅度
| 数据量 | for + await | 批量 INSERT |
|---|---|---|
| 100 | ~500ms | ~30ms |
| 1000 | ~5–8s | ~100ms |
| 5000 | 超时 | ~300ms |
👉 数量级的提升
四、唯一索引会不会导致整批失败?
这是一个非常常见、也非常关键的问题。
❓ 问题
如果
external_userid是唯一索引,用批量插入会不会一条重复就全部失败?
✅ 答案
不会,只要你使用的是 INSERT IGNORE 。
示例
INSERT IGNORE INTO wj_qywx_customer (external_userid) VALUES
('id1'),
('id2'),
('id1'), -- 重复
('id3');结果:
id1(重复)→ 被忽略id2,id3→ 正常插入- 整个 SQL 不会报错
📌 非常适合:
- 数据同步
- 幂等写入
- 外部系统数据导入
五、进阶方案:分批批量插入(生产必备)
当数据量非常大时(几万条),单条 SQL 可能会:
- 超过
max_allowed_packet - 导致 SQL 过长、难以调试
推荐方案: 分批 + 批量 INSERT
const batchSize = 500;
const allIds = res.data.external_userid;
for (let i = 0; i < allIds.length; i += batchSize) {
const batch = allIds.slice(i, i + batchSize);
const values = batch.map(id => `('${id}')`).join(',');
await db.query(
`INSERT IGNORE INTO wj_qywx_customer (external_userid) VALUES ${values}`
);
}为什么 500 是个好数字?
- SQL 长度安全
- 性能和稳定性的平衡点
- 生产环境验证过 👍
六、事务方案:什么时候才需要?
如果你的需求是:
- 要么全部成功,要么全部失败
- 不能接受“部分插入”
那么可以使用事务:
await db.beginTransaction();
try {
for (const id of ids) {
await db.query(
`INSERT IGNORE INTO wj_qywx_customer (external_userid) VALUES (?)`,
[id]
);
}
await db.commit();
} catch (err) {
await db.rollback();
throw err;
}📌 但请注意:
- 性能 仍然不如批量 INSERT
- 事务 ≠ 性能优化工具,而是 一致性工具
七、最佳实践总结(经验级)
✅ 推荐优先级
- 批量 INSERT(最优)
- 分批批量 INSERT(生产必备)
- 事务 + 单条 INSERT(仅限一致性场景)
🚫 不推荐做法
- ❌
for + await单条插入 - ❌ 在高频同步中频繁自动提交
- ❌ 忽略唯一索引带来的设计问题
八、延伸思考(高级)
如果数据量继续增长,你可以考虑:
LOAD DATA INFILE(百万级)- 写入 Kafka / RabbitMQ,再异步落库
- 使用
ON DUPLICATE KEY UPDATE做统计型更新 - 临时表 + 去重 + 合并写入
结语
数据库性能问题,90% 不是数据库慢,而是用法不对。
从 for + await 到批量插入, 本质是从「把数据库当函数调用」 升级为「让数据库做它最擅长的事」。
发布评论
发布评论前请先 登录。
评论列表 0

暂无评论





