正确姿势是小批量+绕过模型(insertall)或控制验证(saveall),并主动分片;因saveall()拼单条insert values(),(),(),而循环save()每条都走完整模型生命周期,1000条即1000次开销。

别用 save() 循环,也别迷信单次大数组 —— 正确姿势是:小批量 + 绕过模型(insertAll())或控制验证(saveAll()),并主动分片。
为什么 saveAll() 比循环 save() 快一个数量级
因为 saveAll() 底层拼的是单条 INSERT INTO table (a,b) VALUES (),(),(),只发一次 SQL;而循环 save() 每次都走完整模型生命周期:验证、事件、钩子、事务判断、SQL 构建、执行 —— 1000 条就是 1000 次开销。
常见错误现象:PHP memory_limit exhausted 或 MySQL 报错 Packets larger than max_allowed_packet are not allowed,往往就来自一次性传入上万条数据给 saveAll()。
- 必须传二维数组,子数组键名要和数据库字段严格一致(或用
field参数映射) - 默认开启验证,若数据已清洗干净,加
['validate' => false]可提速 30%+ - 单次建议 ≤ 500 条;超了容易触发 PDO 绑定参数上限或 MySQL packet size 限制
- 主键为自增时返回 ID 数组;UUID 等非自增主键返回
true/false,无法获知具体 ID
insertAll() 适合什么场景
当你不需要模型层任何功能(验证、自动时间戳、类型转换、事件、钩子),只要把数据塞进表里,insertAll() 是更轻量的选择 —— 它直接走查询构建器,不加载模型类,内存占用低、启动快。
使用场景:日志归档、离线导入、后台数据迁移、Excel 批量入库等“可信数据源”写入。
- 字段名必须和数据库列名完全一致,
type映射、json自动序列化等功能全部失效 -
create_time/update_time不会自动填充,得手动塞进每条数据里 - 返回值是影响行数(
int),不是 ID 列表,无法反查插入记录 - 高并发下锁表风险更低,因不读模型配置、不初始化验证器
- 不处理主键冲突:
INSERT遇到重复主键/唯一索引直接报错中断,不会跳过
怎么安全地插入 10 万条数据
不能靠框架自动切片 —— ThinkPHP 所有批量方法都不做分段,全靠你自己控制。否则极易超时、OOM 或触发 MySQL max_allowed_packet 限制。
- 用
array_chunk($data, 500)主动分片,每次最多 500 条 - 显式开启事务:
Db::startTrans(),并在每个分片后Db::commit()或失败时Db::rollback() - 配合
set_time_limit(0)和ini_set('memory_limit', '-1')防止脚本中断(仅限 CLI 环境) - 避免在 Web 请求中干这事;改用命令行(
php think xxx:import)或队列异步处理 - 如果目标表有唯一索引且允许跳过重复,别用
insertAll(),改用Db::execute('INSERT IGNORE INTO ...')原生 SQL
TP5 和 TP6 的关键差异点
TP6 对非法字段更严格:传入 insertAll() 的字段若不在数据库中,TP6 直接报错;TP5.1 则静默过滤掉 —— 这会导致你以为写进去了,其实字段被吞了。
另外,TP6 默认开启字段白名单过滤,而 TP5 默认宽松。如果你从 TP5 迁移过来,insertAll() 突然失败,先检查字段名是否拼错或大小写不一致。
还有个易忽略点:updateAll() 不支持 SET sort = sort + 1 这类原字段引用,它只接受静态值;想实现递增更新,必须手写原生 SQL。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











