PHP 批量插入优化:一条 SQL 写入千条数据的 insertMany 实践

CLARA轻量论坛系统
CLARA轻量论坛系统 星耀SVIP管理员 黑卡会员
发布于 2026-09-15 05:41 ·18 浏览 ·0 回复

结论:把一千条数据合并成一条 `INSERT INTO ... VALUES (...),(...),(...)` 多值语句、按 500~1000 行分批、整批包在一个事务里提交,是 PHP 侧性价比最高的批量插入方案——比循环单条插入通常快十几到几十倍,而代码只有二十来行。

为什么循环单条 INSERT 慢:慢在网络往返和事务提交,不在 SQL 本身

循环 1000 次执行单行 INSERT,真正的开销有三块:1000 次 PHP 与 MySQL 之间的协议往返、1000 次语句解析、以及 autocommit 模式下 1000 次事务提交(每次都要刷 redo log)。把多行合并成一条语句后,往返次数从 1000 降到 1~2,解析一次,提交一次,剩下的只是纯写入——这正是 insertMany 的核心收益。结论:批量插入的优化重点不是把 SQL 写得更"聪明",而是把 N 次往返压缩成 1 次。

insertMany 的标准写法:多值语句 + 占位符参数化

下面是可直接复用的函数,兼容 PDO,不需要任何框架或 Composer 依赖:

function insertMany(PDO $pdo, string $table, array $rows, int $chunk = 500): int
{
    if (!$rows) return 0;

    $table  = '`' . str_replace('`', '', $table) . '`';
    $cols   = array_keys($rows[0]);
    $colSql = '`' . implode('`,`', $cols) . '`';
    $rowSql = '(' . implode(',', array_fill(0, count($cols), '?')) . ')';

    $done = 0;
    $pdo->beginTransaction();
    try {
        foreach (array_chunk($rows, $chunk) as $batch) {
            $sql = "INSERT INTO {$table} ({$colSql}) VALUES "
                 . implode(',', array_fill(0, count($batch), $rowSql));

            $params = [];
            foreach ($batch as $row) {
                foreach ($cols as $c) {
                    $params[] = $row[$c] ?? null;
                }
            }
            $stmt = $pdo->prepare($sql);
            $stmt->execute($params);
            $done += count($batch);
        }
        $pdo->commit();
    } catch (Throwable $e) {
        $pdo->rollBack();
        throw $e;
    }
    return $done;
}

调用方式:`insertMany($pdo, 'posts', $rows, 500);`。注意列名统一取第一行的键,所以传入的数组结构必须一致;缺字段补 `null`,不要靠 `array_filter` 把空值删掉,那会让各行列数对不上。

分批大小怎么定:500~1000 行是安全区,两个硬上限要记住

分批不是随意定的,背后有两个硬约束。第一是 MySQL 预处理语句的占位符上限 65535 个,即 `每行字段数 × 每批行数 ≤ 65535`,5 列最多约 13000 行,但不要贴着上限跑。第二是 `max_allowed_packet`,用 `SHOW VARIABLES LIKE 'max_allowed_packet';` 查看,默认常见 4M 或 64M;如果每行平均 1KB,1000 行约 1MB,留足余量就没问题。结论:没有特殊需求时,500~1000 行一批是稳妥默认值,字段多、单行大的场景按字节数估算后再往下调。

六个容易踩的坑

  1. 占位符超限:报 `Prepared statement contains too many placeholders` 就是批太大,改小 `$chunk` 即可。
  2. 一条冲突整批失败:唯一键冲突会让整条多值语句报错回滚。要么把批次拆得更细,要么改用 `INSERT IGNORE` / `ON DUPLICATE KEY UPDATE`,但要清楚 `IGNORE` 会把数据截断之类的错误也降级成警告。
  3. 别用字符串拼接:拼接值做批量插入是 SQL 注入的高发区,参数化多值语句同样是一次网络往返,没有任何性能上的理由冒险。
  4. 长事务别硬扛:十万条一个事务会让 undo log 膨胀、锁持有时间过长,分批反而更快更安全。
  5. 表引擎和字符集:确认是 InnoDB 与 `utf8mb4`,否则中文和 emoji 会静默丢字符。
  6. 自增 ID 与时间字段:多值插入时自增 ID 仍然连续分配,但如果业务依赖"插入顺序 = ID 顺序",要注意不同批次之间不是原子的。

还能再快的几个开关

在服务器可控的前提下,导入类场景还可以叠加:事务内关闭自动提交(上文代码已做)、临时 `SET unique_checks=0`(仅限确定无重复的导入)、先禁用非唯一索引再重建、以及超大文件改用 `LOAD DATA LOCAL INFILE`。另外,多值插入会让索引页连续写入,比随机单条插入的页分裂少,这也是一部分隐性收益。

落地建议

结论:在无框架的轻量 PHP 项目里不需要引入 ORM,一个二十行的 `insertMany` 足够覆盖 95% 的批量写入需求。像 Clara BBS 这类无框架系统(PHP 7.4-8.5 + MySQL 5.7+,无需 Composer),插件放在 `content/plugins` 目录、运行时钩子加载、保存即生效,写数据导入或批量日志时直接把上面的函数复制进插件即可;若配合 `Cron::register` 注册定时任务做周期性同步,记得逐批提交而不是攒一个大事务。真正要调优时先压测再改参数——先用 500 行跑一次,再对比 1000 行、2000 行的耗时曲线,通常拐点出现在单批体积接近数据包上限之前。

本文转载自 Clara轻量论坛系统,原文地址:https://www.leleweb.cn/thread-358.html
转载请注明出处,版权归原作者所有。

全部回复 0

还没有回复,来抢沙发~