insertAll()才是批量插入的正确姿势,它拼接单条多值INSERT语句,避免循环save()的N次验证、SQL构建与事务开销;saveAll()实为循环save(),非真批量。

循环调用 save() 是性能黑洞,insertAll() 才是批量插入的正确姿势。 两者根本不在一个量级:前者每条数据触发一次 SQL、一次验证、一次事务判断;后者拼成一条多值 INSERT 语句,网络往返和解析开销降到最低。
为什么循环 save() 慢得离谱
ThinkPHP 的 save()(包括模型实例调用或 Db::save())本质是单条写入逻辑。哪怕你用 foreach 包一层,结果仍是 N 次独立查询:
- 每条记录都走完整模型生命周期:验证 → 自动时间戳 → 事件钩子 → SQL 构建 → 执行
- MySQL 连接频繁复用/重连,
max_allowed_packet容易被单条大字段触发报错(比如某条记录带长文本) - 事务若没手动包裹,每条都是隐式自动提交,锁表时间拉长、并发写入冲突概率上升
- 1000 条数据实测耗时常超 12 秒,而
insertAll()通常在 0.6 秒内完成
insertAll() 的真实行为和限制
insertAll() 是数据库查询构造器提供的纯 SQL 批量接口,不经过模型层,只做两件事:拼接 INSERT INTO ... VALUES (),(),() + 执行。它快,但有硬性前提:
- 传入必须是二维关联数组,每个子数组键名严格对应字段名(不支持自动映射别名,除非显式用
field参数) - 单次数据量建议控制在 200–500 条,避免超出 MySQL 的
max_allowed_packet(默认 4MB) - 不触发任何模型验证、事件、自动时间戳;若需这些,得自己在插入前补全字段(如
'create_time' => date('Y-m-d H:i:s')) - 返回值是成功插入的条数(整型),不是 ID 数组;主键为自增时,无法直接拿到新 ID —— 需要用
Db::getLastInsID()或改用insertGetId()(仅限单条)
saveAll() 不是 insertAll() 的替代品
很多人误以为 saveAll() 是“模型版批量插入”,其实它在 TP6.0–6.3 中仍是语法糖:内部循环调用 save(),每条单独 INSERT/UPDATE。它和 insertAll() 的差异不是“要不要模型”,而是“有没有批量”:
立即学习“PHP免费学习笔记(深入)”;
-
saveAll($data)→ 循环 N 次save($item)→ N 条独立 SQL -
Db::name('table')->insertAll($data)→ 1 条含 N 组值的 SQL - 想用模型又想要真批量?只能绕过
saveAll(),改用Db::name('table')->insertAll($data)+ 手动补全模型逻辑(如时间戳、软删除字段) - 报错
"Non-static method think\Model::saveAll() should not be called statically"是因为saveAll()是实例方法,必须通过模型类实例调用,不能直接Model::saveAll()
什么时候该用哪个
选型取决于你是否需要模型能力,而不是“看起来更高级”:
- 日志归档、后台导入、ETL 同步等数据可信、无业务逻辑场景 → 直接
Db::insertAll(),轻量、快、可控 - 需校验字段合法性、触发
before_write钩子、依赖模型关联或自动填充 → 先用模型验证/处理生成干净二维数组,再交给Db::insertAll(),别碰saveAll() - 数据量极小(save() 循环可接受,但别养成习惯
-
updateBatch()是更新场景的对应方案,和insertAll()同属真批量,但注意它底层用的是INSERT ... ON DUPLICATE KEY UPDATE,要求有唯一索引或主键
最常被忽略的一点:批量操作不是加个方法名就完事——分批、事务、字段对齐、引擎兼容性(比如 MyISAM 不支持事务)缺一不可。没包事务的 insertAll() 在中途失败时,可能留下脏数据。



















