Laravel批量更新应优先用upsert()实现单SQL多行插入或更新,需唯一索引支撑;updateOrCreate()适用于小批量且条件各异的单条操作;大数据量简单更新建议原生SQL;事务中谨慎加锁,避免死锁与性能损耗。

用 upsert() 一次性插入或更新,避免 N+1
批量更新最常踩的坑是写个循环调用 save(),结果生成 N 条 SQL —— 数据量一上来就卡死。Laravel 9+ 原生支持 upsert(),它底层走的是数据库的 INSERT ... ON DUPLICATE KEY UPDATE(MySQL)或 ON CONFLICT(PostgreSQL),一条语句搞定多行。
前提是你得有唯一索引(比如 id 或业务字段如 sku),否则 upsert() 不知道按什么去“匹配更新”。
- 只传需要更新的字段:第二个参数指定哪些字段用于判断冲突(
['id']),第三个参数指定哪些字段允许被更新(['name', 'price']) - 别把主键当普通字段塞进更新列表里 ——
id在冲突判断里用了,就别再放进第三个参数,否则可能意外覆盖 - SQLite 用户注意:
upsert()在 SQLite 3.24+ 才支持,旧版会抛出BadMethodCallException
$data = [
['id' => 1, 'name' => 'iPhone', 'price' => 6999],
['id' => 2, 'name' => 'iPad', 'price' => 3299],
];
Product::upsert($data, ['id'], ['name', 'price']);
用 updateOrCreate() 更新单条但逻辑复杂时
当你每条数据的“唯一性判断条件”不统一(比如有的靠 email,有的靠 phone),或者需要在创建时额外处理逻辑(比如生成默认头像、触发事件),updateOrCreate() 比 upsert() 更灵活,但它本质仍是单条操作 —— 别误以为能批量用。
常见错误是把它塞进 foreach 循环还觉得“我用了 Laravel 方法就很优雅”,其实和手写 save() 性能没区别。
- 只适合小批量(≤ 50 条),且每条的 where 条件差异大
- 第二个参数必须是完整待存字段数组,第一个参数只是“查找条件”,别漏掉必填字段
- 如果数据库没有对应记录,它会执行 insert;但不会自动填充
created_at等时间戳字段,除非你显式传入或模型开启了$timestamps = true
Product::updateOrCreate(
['sku' => 'IPHONE15'],
['name' => 'iPhone 15', 'price' => 7999, 'status' => 'active']
);
手动写原生 SQL 批量更新(绕过 Eloquent 开销)
当你要更新几千甚至上万条,且字段逻辑简单(比如全设为某个值、按规则加减)、不需要模型事件或访问器/修改器时,硬写 SQL 是最快方案。Eloquent 的属性转换、关系加载、事件触发全被跳过。
风险在于:绕过了模型层校验,也拿不到更新后的模型实例;如果字段名拼错或类型不匹配,错误信息不如 Eloquent 友好。
- 用
DB::statement()而不是DB::select(),后者只用于查询 - WHERE 条件尽量走索引字段,否则容易锁表;线上环境务必先在测试库压测
- MySQL 中
UPDATE ... WHERE id IN (...)的IN列表长度别超 1000,超了拆成多个批次
DB::statement("UPDATE products SET status = ? WHERE category_id = ?", ['draft', 5]);
用事务包住批量操作,但别盲目加锁
很多人一想到“批量更新要安全”,立马 wrap 一个 DB::transaction(),这没错;但紧接着加 sharedLock() 或 lockForUpdate() 就容易翻车 —— 多数场景根本不需要行锁。
锁只在你明确要防止并发修改同一行(比如库存扣减)时才有意义。普通后台批量改状态、分类,加锁反而拖慢整体吞吐,还可能引发死锁。
- 事务内别做 HTTP 请求、文件读写等耗时操作,否则锁持有时间不可控
- 如果真要锁,用
whereKey()显式限定范围,别直接Product::lockForUpdate()->get()全表扫 - PostgreSQL 用户注意:
lockForUpdate()在批量更新中可能升级为页锁,影响并发度
upsert()、手写 SQL 还是干脆拆成队列异步跑。










