应使用 dao 的 updateall() 直接执行 sql 更新,避免 activerecord 生命周期开销;复杂逻辑用带命名参数的 case when 语句;大数量需分批(500–2000 行)、手动 gc 和日志 flush。

批量更新必须绕过ActiveRecord生命周期
直接调用 $model->update() 或 $model->save() 更新成百上千条记录,本质是 N 次独立 SQL 执行 + N 次模型实例化 + N 次验证/事件触发。这不仅慢,还会因内存累积和日志缓冲导致 CLI 脚本静默退出。
- ActiveRecord 的
beforeSave、afterSave、时间戳行为、软删除检查(如deleted_at IS NULL)全都会被触发,而你往往并不需要 - 每条记录都走一遍属性变更检测、脏字段计算、类型转换,开销远超纯 SQL
- 若模型绑定了
TimestampBehavior或BlameableBehavior,还会额外查询当前用户、生成时间戳,进一步拖慢速度
正确做法是:用 DAO 直接发 UPDATE 语句,不加载模型、不走 AR 生命周期。
用 updateAll() 替代循环 update(false)
updateAll() 是最轻量、最安全的批量字段更新方式,底层就是一条带 WHERE 条件的 UPDATE 语句,无 ORM 开销,也无事务自动包裹——你可以自己控制。
- 语法:
Yii::$app->db->createCommand()->update($table, $columns, $condition)->execute() -
$columns是关联数组,如['status' => 2, 'updated_at' => time()];注意它不会自动填充updated_at,必须显式传入 -
$condition支持数组格式(如['id' => [101, 102, 103]])或字符串(如"created_at ),后者需配合参数绑定防注入 - 返回值是影响行数,可用于校验是否全部更新成功
例如把某批订单状态设为已发货,并统一更新时间:Yii::$app->db->createCommand()->update('order', ['status' => 3, 'updated_at' => time()], ['id' => $orderIds])->execute();
多条件+表达式更新得用原生 SQL + CASE WHEN
当要根据每行不同值更新不同字段(比如按用户等级设不同折扣),updateAll() 不支持动态列值,必须拼原生 SQL。
- 避免字符串拼接,一律用命名参数绑定:
':level'、':ids' - 用
CASE WHEN实现行级逻辑,例如:"UPDATE user SET discount = CASE WHEN level = 1 THEN 0.05 WHEN level = 2 THEN 0.1 ELSE 0 END WHERE id IN (:ids)" - 若条件复杂(如涉及子查询、JOIN),DAO 也支持
queryScalar()或queryAll()先取 ID,再分批updateAll(),比全量 JOIN 更可控
错误示范:"UPDATE user SET discount = " . $discount . " WHERE id = " . $id —— SQL 注入高危,且无法批量。
大数量更新必须分批 + 显式 gc
即使用了 updateAll(),一次性更新 5 万行仍可能触发 MySQL max_allowed_packet、PHP 内存溢出或事务锁表太久。这不是“能不能”,而是“该不该一次干完”。
- 单批建议控制在 500–2000 行,具体看字段长度和索引覆盖情况
- 每批执行后加
gc_collect_cycles();,尤其在 CLI 脚本中,否则 PDO 和日志缓冲会持续占内存 - 手动 flush 日志:
Yii::getLogger()->flush();,防止flushInterval堆积 - 若需强一致性,外层套
beginTransaction()/commit(),但注意长事务会加重锁竞争
最容易被忽略的是:没清空日志缓冲 + 没强制 GC → 脚本跑着跑着就 OOM 退出,连错误都看不到。











