saveAll()本质是循环调用save(),非真批量SQL;insertAll()为真批量插入但无模型逻辑;update()/updateAll()快但需显式where防误操作;大批量更新应分批+CASE WHEN原生SQL。

saveAll() 本质是循环 save,不是真批量 SQL
ThinkPHP 8 的 saveAll() 底层仍是逐条调用 save(),每条记录触发一次 INSERT 或 UPDATE 查询,不生成类似 INSERT INTO ... VALUES (),(),() 或 UPDATE ... CASE WHEN 这类原生批量语句。它只是语法糖,适合小数据量(≤100 条)或需模型层能力的场景。
常见错误现象:saveAll() 处理 2000 条数据时耗时超 5 秒、MySQL 报 max_allowed_packet 错误、连接频繁超时——这些都不是数据库瓶颈,而是 N 次往返和重复解析导致的。
- 想触发
before_update、自动时间戳、验证器、allowField()过滤?用saveAll() - 主键字段名不一致(如前端传
user_id,模型定义主键为id),saveAll()会把本该更新的记录当成新增,全走 INSERT - 加了
Db::transaction()也救不了性能——事务只保证原子性,不减少 SQL 次数;反而可能因锁等待更慢
insertAll() 是真批量插入,但不处理冲突
Db::table('user')->insertAll($data) 直接拼接单条 INSERT INTO ... VALUES (),(),(),无模型开销,速度快、内存低,适合日志归档、后台导入等可信数据源场景。
但它默认不做任何冲突处理:遇到主键/唯一索引重复,整个批次直接报错中断,报错信息通常是 SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry。
立即学习“PHP免费学习笔记(深入)”;
- 要跳过重复行?必须手写
INSERT IGNORE INTO或INSERT ... ON DUPLICATE KEY UPDATE,用Db::execute()执行 - 字段名必须严格匹配数据库列名,
create_time不会自动填充,得自己塞进每条$data子数组 - 单次别超 500 条,否则易触发 MySQL 的
max_allowed_packet限制
update() 和 updateAll() 都要求显式 where,空条件=全表误改
Model::update()(静态)和 Db::table()->update() 都是原生级批量 SET,快但绕过模型逻辑。它们共用一个致命风险:where 条件为空时,会无提示清空全表对应字段。
例如 User::update(['status' => 0]) 或 Db::table('user')->update(['status' => 0]),结果是所有用户的 status 变成 0——ThinkPHP 8 默认不拦截这种操作。
- 必须显式带
where:优先用whereIn('id', $ids),别拼where('id', 'in', implode(',', $ids))(SQL 注入) -
updateAll()不支持字段值计算,比如不能写['sort' => 'sort + 1'];要实现就得用Db::execute('UPDATE user SET sort = sort + 1 WHERE ...') - 若需按 ID 更新不同值(如用户 A 积分+10、用户 B 积分+20),
update()无能为力,得切CASE WHEN原生 SQL
大批量更新该选哪条路
500 条以上、纯性能导向的更新,别碰 saveAll()。真实项目里最稳的组合是:分批 + 原生 CASE WHEN + 绑定参数。
先提取所有 ID:$ids = array_column($data, 'id');再为每个字段构造 CASE id WHEN ? THEN ? ... END 分支,用 Db::raw() 包裹,最后通过 Db::execute() 执行。这样既原子、又防注入、还避免 N 次查询。
- 表必须有唯一索引(PRIMARY 或 UNIQUE),否则
ON DUPLICATE KEY UPDATE不生效 -
REPLACE INTO看似方便,但会先 DELETE 再 INSERT,可能触发外键级联、丢失自增 ID、引发 binlog 异常 - CLI 脚本记得
set_time_limit(0),Web 请求务必走队列,别让 PHP 超时或 Nginx 断连



















