batchInsert()是最稳的批量插入起点,它生成单条多值INSERT语句,避免循环save()导致的性能灾难;需传严格对齐的二维数组、显式字段名,建议前置数据过滤或校验,并在超5万条时分批+事务控制。

直接用 batchInsert() 是最稳的起点
Yii 框架原生就支持高效批量插入,不用自己拼 SQL、也不用绕开 ORM 写事务循环。核心就是 batchInsert() 这个方法,它底层会生成一条含多值的 INSERT INTO ... VALUES (...), (...), (...) 语句,一次发给数据库,省掉 99% 的网络和解析开销。
常见错误现象:有人写循环调用 save() 或 insert(),1 万条数据跑出 30 秒+、内存溢出、超时——本质是把批量当单条用了。
-
batchInsert()要求传入严格对齐的二维数组:每行是字段值顺序一致的一组数据,不能缺列、不能类型错位 - 字段名数组(第二参数)必须显式写出,不能靠模型自动推导;
User::tableName()和User::attributes()可复用,但别直接传$model->attributes(可能含主键、时间戳等非插入字段) - 不校验数据合法性:如果字段有
NOT NULL约束或格式限制,失败会整个语句报错,建议前置过滤或捕获Exception
示例:
Yii::$app->db->createCommand()->batchInsert(
'user',
['username', 'email', 'created_at'],
[
['alice', 'a@b.com', time()],
['bob', 'b@c.com', time()],
['carol', 'c@d.com', time()],
]
)->execute();
想校验再插?先筛数据,别在循环里 validate()
硬要在插入前校验每条数据?千万别在 foreach 里反复调用 $model->validate() —— 每次都触发完整验证生命周期,性能断崖下跌。
正确做法是把校验逻辑“提上来”,一次性完成筛选:
- 用
array_map()+Model::load()或手动赋值,构造干净的二维数组 - 用
array_filter()做轻量级检查(如邮箱格式、长度),避免进 ActiveRecord - 若必须用模型验证,提前批量构建
Model实例并调用validate(),再用array_column($models, 'attributes')提取通过的数据
示例(跳过验证,仅提取属性):
$rows = [];
foreach ($rawData as $data) {
$model = new User();
$model->load($data, ''); // 不走场景,快速赋值
if ($model->validate(['username', 'email'])) { // 指定字段,减少开销
$rows[] = $model->getAttributes(['username', 'email', 'created_at']);
}
}
Yii::$app->db->createCommand()->batchInsert('user', ['username','email','created_at'], $rows)->execute();
超 5 万条?分批 + 关闭自动提交更安全
单次 batchInsert() 插入太多行(比如 10 万+),MySQL 可能报 Packet too large 或触发锁等待,PHP 也可能内存撑爆。这不是 Yii 的问题,是 MySQL 协议和配置限制。
解决方案不是“加大 max_allowed_packet”,而是主动分片:
- 每批控制在 1000–5000 行(MySQL 默认
max_allowed_packet=4M,按平均 200 字节/行算,约 2 万行封顶) - 用事务包住每一批:
$transaction = Yii::$app->db->beginTransaction(),成功才commit(),失败rollBack() - 确认数据库连接未开启
autocommit(Yii 默认关着,但自定义连接时可能开)
注意:不要用 Yii::$app->db->createCommand()->batchInsert(...)->execute() 在事务外反复调用——它每次都是独立语句,没事务保护。要显式控制事务边界。
比 batchInsert() 更快的路:绕过框架直写 LOAD DATA INFILE
真要插百万级数据?batchInsert() 已经不够看了。这时候该切到 MySQL 原生命令 LOAD DATA INFILE,速度能再提一个数量级(实测 100 万行常压在 3 秒内)。
但它有硬性前提:
- 数据得先落地成 CSV 文件(PHP 用
fputcsv()写),不能纯内存拼接 - MySQL 配置需开启
local_infile=ON,且用户权限含FILE - PHP 进程与 MySQL 服务最好同机,否则
LOAD DATA LOCAL INFILE可能被禁用
示例 SQL(在 createCommand() 中执行):
Yii::$app->db->createCommand("LOAD DATA LOCAL INFILE '/tmp/users.csv'
INTO TABLE user
FIELDS TERMINATED BY ','
ENCLOSED BY '\"'
LINES TERMINATED BY '\n'
IGNORE 1 ROWS")->execute();
这招快是快,但调试成本高、错误提示弱、权限卡得死——日常万级插入,老实用 batchInsert() 分批就好;只有真正上量、且环境可控时,才值得切过去。
最容易被忽略的一点:无论用哪种方式,插入前记得关掉 MySQL 的唯一索引、外键检查(SET UNIQUE_CHECKS=0, SET FOREIGN_KEY_CHECKS=0),插完再开——这对大表导入影响极大,但很多人根本没想到这层。










