Node.js + MySQL 批量插入性能优化实战:从 for + await 到高性能方案

2026-01-05 51 浏览 0 评论

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

本文将从一个真实代码示例出发,逐步分析问题原因,并给出 可落地、可扩展、适合生产环境的优化方案


一、问题背景: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
  • 事务 ≠ 性能优化工具,而是 一致性工具

七、最佳实践总结(经验级)

✅ 推荐优先级

  1. 批量 INSERT(最优)
  2. 分批批量 INSERT(生产必备)
  3. 事务 + 单条 INSERT(仅限一致性场景)

🚫 不推荐做法

  • for + await 单条插入
  • ❌ 在高频同步中频繁自动提交
  • ❌ 忽略唯一索引带来的设计问题

八、延伸思考(高级)

如果数据量继续增长,你可以考虑:

  • LOAD DATA INFILE (百万级)
  • 写入 Kafka / RabbitMQ,再异步落库
  • 使用 ON DUPLICATE KEY UPDATE 做统计型更新
  • 临时表 + 去重 + 合并写入

结语

数据库性能问题,90% 不是数据库慢,而是用法不对。

for + await 到批量插入, 本质是从「把数据库当函数调用」 升级为「让数据库做它最擅长的事」。


发布评论

发布评论前请先 登录

评论列表 0

暂无评论