应优先使用 Db::insertAll() 或 saveAll() 替代 foreach+save(),单批200–500条并包裹事务;前者绕过模型层、纯SQL插入,适合高性能场景;后者走完整模型流程,支持验证、事件、自动时间戳等业务逻辑。

直接用 Db::insertAll() 或 saveAll(),别写循环;单批控制在 200–500 条,必须包事务,否则数据不一致风险极高。
为什么不能 foreach + save()?
每条 save() 都会触发完整模型生命周期:验证、事件、钩子、时间戳填充、SQL 构建、单次执行。1000 条 = 1000 次数据库往返 + 1000 次 PHP 对象初始化。实测耗时比 insertAll() 高 10 倍以上,且极易触发 MySQL 连接超时或 PHP 内存溢出。
常见错误现象:MySQL server has gone away、Allowed memory size exhausted、Web 请求卡在 30 秒后被 Nginx 断连。
- 循环中反复
new User()是隐形内存杀手,复用一个模型实例即可 - 若数据已清洗干净(如 Excel 导入后校验过),加
['validate' => false]跳过验证能再提速 20%–40% -
saveAll()返回值取决于主键类型:自增 ID 返回插入 ID 数组;UUID 等非自增主键只返回布尔值
Db::insertAll() 和 saveAll() 怎么选?
insertAll() 绕过模型层,纯 SQL 拼接,无验证、无事件、无自动时间戳,字段名必须严格匹配数据库列名。适合日志归档、后台导入、ETL 等对一致性要求低、但对速度和内存敏感的场景。
立即学习“PHP免费学习笔记(深入)”;
saveAll() 走模型流程,支持自动时间戳、类型转换、JSON 字段序列化、事件回调等,适合业务逻辑强、需触发钩子(如积分变更、消息通知)的写入。
- 需要
create_time/update_time自动填充?只能用saveAll(),或手动把时间塞进数据数组 - 字段含
json类型或decimal映射?saveAll()会自动处理,insertAll()不识别模型配置,得自己json_encode() - 高并发写入时,
insertAll()锁表时间更短,因不查模型结构、不读取验证规则
大批次怎么分段不翻车?
一次性传 10 万条进 insertAll(),大概率触发 Packets larger than max_allowed_packet are not allowed 或 PHP 超时。必须主动切片,框架不会帮你兜底。
推荐用 array_chunk($data, 400) 拆分,每批 400 条是 MySQL 默认 max_allowed_packet=64M 下较安全的上限(具体看单条数据体积)。
- CLI 脚本务必加
set_time_limit(0),并每批后调用gc_collect_cycles()防止内存缓慢上涨 - Web 请求别硬扛,改用队列(如 think-queue)拆成多个任务,避免用户端等待
- 别在事务里混用
saveAll()和insertAll(),它们底层可能用不同数据库连接实例,导致事务失效 - 每批执行完检查返回值:
insertAll()返回影响行数(int),saveAll()返回 ID 数组或布尔值,异常时要中断并回滚
还有更快的方案吗?
当数据量达百万级、且无业务逻辑约束时,LOAD DATA INFILE 是 MySQL 原生命令,比任何 PHP 层批量方法都快——它跳过 SQL 解析,直接从文件流写入引擎。但需满足:Web 服务器与 MySQL 在同一台机器(或开启 local_infile)、有文件系统写入权限、数据格式严格为 CSV/TXT。
ThinkPHP 不封装该能力,得手写 Db::execute('LOAD DATA INFILE ...'),且无法用 ORM 验证或钩子。日常业务中极少需要走到这步,除非做离线数据迁移。
真正容易被忽略的是:insertAll() 的第二个参数虽支持自动分批(如 insertAll($data, 200)),但它只是 PHP 层切片,仍在一个事务里执行——若某一批失败,前面成功的批次已无法回滚。所以显式事务 + 手动 array_chunk + 分批 try/catch,才是生产环境唯一稳妥路径。



















