批量操作应避免循环create()、save()、delete(),优先使用DB::table()->insert()、whereIn()->update()、upsert()、increment()等底层方法,兼顾性能、安全与语义清晰。

别用循环 create()、save() 或 delete() 做批量操作,99% 的性能问题和线上事故都出在这儿。Laravel 有更直接、更安全的底层方式,关键看你要做什么。
批量插入:用 DB::table()->insert(),不是 Model::create()
循环 create() 会触发模型事件、验证、时间戳自动填充、属性转换,100 条就是 100 次查询 + 100 次 PHP 开销,还容易超时或被 MySQL 的 max_allowed_packet 拦截。
-
DB::table('users')->insert($data)是唯一推荐方式,$data必须是二维数组,键名严格对应字段名(如['name' => 'a', 'email' => 'b@c.com']),不能带created_at等时间字段(除非你显式赋值) - 不走
$fillable、不调事件、不处理访问器,纯 SQL 执行,快且可控 - 单次插入别超 1000 行;超了就用
collect($data)->chunk(500)分批
批量更新统一字段:用 whereIn()->update(),不是模型 update()
User::whereIn('id', $ids)->update(['status' => 1]) 是标准解法,生成一条 UPDATE ... WHERE id IN (?, ?, ?)。但注意:模型上调用这个方法,软删除作用域和全局作用域默认不生效——语义上你以为走了完整生命周期,其实没走。
- 推荐直接用
DB::table('users')->whereIn('id', $ids)->update(...),意图清晰,无歧义 - 它不返回“影响行数为 0”,前端可能误判成功;真要校验,得手动查
User::whereIn('id', $ids)->count()对比count($ids) - MySQL 对
IN参数数量有限制,超 5000 项建议分块;关键业务表(如orders)上线前加if (count($ids) > 1000) { abort(400); }
批量更新不同字段值:用 upsert()(Laravel 9+),不是手写 CASE WHEN
当你要按 ID 分别设不同值(比如 id=1 → price=99,id=2 → price=199),update() 完全不支持。核心前提是:目标字段(如 id)必须有唯一索引(主键 or UNIQUE)。
-
User::upsert($data, ['id'], ['name', 'email'])一行搞定“存在则更新,不存在则插入”,底层是INSERT ... ON DUPLICATE KEY UPDATE -
$data每个子数组字段必须一致,缺字段 = 该列不更新;冲突字段(如id)绝不能放进第三个参数里,否则可能覆盖主键 - SQLite 用户注意:只在 3.24+ 支持,旧版本会抛
BadMethodCallException
数值字段原子递增:用 increment(),不是 update() 或 upsert()
库存 +5、阅读数 +1 这类操作,先查再改会引发竞态条件,update() 和 upsert() 都无法保证原子性。
-
Product::whereIn('id', [1, 2])->increment('stock', 5)生成SET stock = stock + 5,由数据库原生命令保障并发安全 - 只支持单字段、单增量值;若需对不同 ID 加不同值(如 id=1 加 3,id=2 加 7),只能退回到
DB::statement()手写CASE WHENSQL - 大数据量慎用循环调
increment(),仍是 N+1;应先取 ID 列表,再一次性执行
最常被忽略的是:软删除模型在批量操作中行为不一致——delete() 在查询构造器上只是设 deleted_at,而 forceDelete() 不支持批量;update() 默认过滤掉已软删记录,要改它们得显式加 withTrashed()。这些细节不提前确认,上线后就不是慢,而是错。


















