应优先使用 updateAll() 或原生 SQL 批处理更新,避免循环调用 save()/update();updateAll() 绕过模型层、零验证零事件,性能最优;不同值更新需用 CASE WHEN 语句;万级数据须分批+事务+延迟以保障稳定性。

直接用 updateAll() 或原生 SQL 批处理,别在循环里调 save() 或 update() —— 否则每条记录都触发一次查询+验证+事件,1000 条就是 1000 次 round-trip,性能断崖式下跌。
循环中调 update() 为什么慢得离谱
常见写法是先 find()->all() 加载全部模型,再 foreach 调 $model->update(false)。这看似跳过了验证,但依然逃不掉:
- ActiveRecord 实例化开销(属性赋值、内部状态初始化)
- 每条 update 都走完整 SQL 构建流程,哪怕只改一个字段
- 默认不带事务,出错时部分更新成功、部分失败,数据撕裂
- 内存爆满:
all()把整张表符合条件的记录全读进 PHP 内存
updateAll() 是最轻量的批量更新方案
它绕过模型层,直接拼 SQL,一条语句完成所有匹配行的字段更新,零验证、零事件、零对象实例化。
使用要点:
- 第一个参数是键值对数组,比如
['status' => 2, 'updated_at' => time()],字段名必须真实存在且可写 - 第二个参数是 WHERE 条件,支持数组格式(
['id' => [1,2,3]])、字符串('user_id = :uid')或表达式 - 若条件含用户输入,务必用参数绑定,例如:
['status = :s', [':s' => $status]] - 返回值是影响行数,不是布尔值;0 表示没匹配到任何行,不等于失败
示例:Yii::$app->db->createCommand()->update('order', ['status' => 3], ['id' => $ids])->execute();
需要按不同 ID 更新不同值?得用原生 SQL 拼 CASE
updateAll() 只支持统一值更新。如果每条记录要设不同字段值(比如一批订单各自更新不同 delivery_time),就得手写带 CASE WHEN 的 SQL:
- 构造形如
UPDATE order SET delivery_time = CASE id WHEN 1 THEN '2026-08-18' WHEN 2 THEN '2026-08-19' END WHERE id IN (1,2) - ID 和值必须严格一一对应,顺序错位会导致字段写错
- MySQL 对单条语句长度有限制(
max_allowed_packet),超 1000 行建议分批 - 别用字符串拼接用户输入,一律用
addParams()绑定参数
示例片段:
$sql = "UPDATE order SET status = CASE id ";
foreach ($updates as $id => $status) {
$sql .= "WHEN :id{$id} THEN :status{$id} ";
}
$sql .= "END WHERE id IN (" . implode(',', array_keys($updates)) . ")";
$command = Yii::$app->db->createCommand($sql);
foreach ($updates as $id => $status) {
$command->addParams([":id{$id}" => $id, ":status{$id}" => $status]);
}
$command->execute();
分批 + 事务是稳定性的底线
万级更新不加控制,容易触发锁等待、超时或主从延迟。必须:
- 按主键或索引字段分块,每批 500–1000 行(具体看字段宽度和服务器配置)
- 每批用独立事务包裹:
Yii::$app->db->transaction(function ($db) use ($batch) { ... }); - 批间加微小延迟(如
usleep(10000))缓解数据库压力,避免瞬间峰值 - 记录已处理 ID 范围,失败后可断点续跑,而不是重头再来
最容易被忽略的是:事务内不要混用 Active Record 查询(如 findOne()),否则可能因连接复用导致事务失效或死锁。



















