Yii2中batchInsert()是高性能批量插入方法,绕过AR校验,需严格匹配字段与数据顺序,建议单次500–1000行,不支持表达式和自动触发事件,大批量须分chunk+事务+错误隔离。

Yii2 中 batchInsert() 是最直接的批量新增方式
直接用 createCommand()->batchInsert() 绕过 ActiveRecord,性能高、无模型校验开销,适合纯数据导入或后台脚本。它本质是拼一条含多值的 INSERT INTO ... VALUES (),(),() 语句,MySQL/PostgreSQL 都支持。
常见错误是字段名和数据数组列顺序不一致,导致插入错列甚至报错 SQLSTATE[HY093]: Invalid parameter number;另外注意 batchInsert() 不触发 beforeSave 或验证逻辑,别在需要自动填充时间戳或业务校验的场景硬套。
-
batchInsert()第一个参数是表名(字符串),第二个是字段名数组(必须与后续每行数据顺序严格对应),第三个是二维数据数组 - 单次插入行数建议控制在 500–1000 行以内,避免 SQL 过长被 MySQL 的
max_allowed_packet截断 - 如果字段含表达式(如
NOW()),不能写进字段名数组,得用insert()+VALUES手动拼,batchInsert()不支持
$rows = [
['name' => 'Alice', 'status' => 1, 'created_at' => time()],
['name' => 'Bob', 'status' => 2, 'created_at' => time()],
];
Yii::$app->db->createCommand()->batchInsert('user', ['name', 'status', 'created_at'], $rows)->execute();
ActiveRecord 批量新增需手动循环 + insertAll()(Yii3)或分批 save()
Yii2 的 ActiveRecord 没有内置批量保存方法,强行用 new Model() + save() 循环,每条都走完整生命周期(验证、事件、事务),慢且易因某条失败中断全部。Yii3 引入了 insertAll(),但 Yii2 用户只能自己封装或换方案。
如果你必须用 AR(比如依赖 beforeValidate 做数据清洗),就得分批 + 显式事务包裹,否则单条失败会导致前面成功的数据滞留数据库。
- 不要裸写
foreach ($data as $item) { $model = new User(); ... $model->save(); }—— 没事务、没批量、没错误隔离 - 用
Yii::$app->db->transaction()包住循环,并在每次save()后检查返回值,!$model->save()时可记录错误并continue或break - Yii3 用户可直接用
User::insertAll(['name', 'status'], $rows),它底层仍调batchInsert(),但做了字段映射和类型转换
大批量数据(万级+)必须分 chunk + 控制内存和超时
一次性读几千行再 batchInsert(),PHP 内存可能爆掉,尤其字段含大文本时;Web 请求还容易超时。真实生产环境几乎不会一气呵成。
典型做法是:从文件或接口流式读取,每 500 行执行一次 batchInsert(),中间清空变量、手动 gc_collect_cycles()(非必需但稳妥),并加日志标记进度。
- 用
array_chunk($allData, 500)切分,比手写 for 循环更安全 - CLI 环境下设
set_time_limit(0),Web 环境建议改用队列(如 Yii2 的yii\queue)拆任务 - 别在循环里反复 new Command 对象,复用同一个
createCommand()实例即可
外键、唯一索引冲突时的错误捕获要具体到行
batchInsert() 报错时默认只抛出整个 SQL 失败,看不出哪一行、哪个字段冲突。例如唯一索引重复,你得知道是第 37 行的 email 字段撞了,而不是重试整批。
解决思路不是靠 try/catch 整批,而是提前查重(对关键字段 SELECT IN)、或事后用 INSERT IGNORE / ON DUPLICATE KEY UPDATE(需手写原生 SQL,batchInsert() 不支持)。
- MySQL 可改用
createCommand("INSERT IGNORE INTO ...")+ 手动拼值,但失去 Yii 的参数绑定保护 - 更稳的做法:先用
SELECT id FROM table WHERE email IN (:emails)查出已存在邮箱,过滤掉再插入 - 日志里记下当前 batch 的起始 offset 和原始数据片段(如
json_encode(array_slice($rows, 0, 3))),方便定位


















