必须用db::insertall()批量插入,禁止单条循环;事务需同db实例、异常回滚、避免外部操作;关闭pdo预处理模拟可提速;replace/ignore选项可优化主键冲突场景。

批量插入必须用 Db::insertAll(),别手写循环
单条 Db::insert() 在循环里执行 1000 次,实际会发起 1000 次 SQL 请求,网络往返 + 连接开销直接拖垮性能。ThinkPHP 原生支持批量插入,核心就是 Db::insertAll() —— 它把多条数据拼成一条 INSERT INTO ... VALUES (),(),() 语句发送,实测 1000 条数据耗时可从 3s+ 降到 0.15s 左右。
注意三点:
-
insertAll()第二个参数是字段名数组(可选),显式传入能避免自动探测字段的开销,尤其表字段多、数据不全时; - 单次传入数据不宜超过 5000 行,MySQL 默认
max_allowed_packet通常为 4MB,超限会报错Packets larger than max_allowed_packet are not allowed; - 如果数据来自 CSV 或接口,先用
array_chunk($data, 500)分片再逐批插入,比硬扛更稳。
事务封装不能只套 Db::startTrans() 和 Db::commit()
事务不是“包住插入就完事”。常见错误是:在事务里混用不同数据库连接、或调用含隐式提交的操作(比如 CREATE TEMPORARY TABLE),导致事务提前结束却无报错。
安全封装要这样写:
- 所有操作必须使用同一个
Db实例(避免用Db::name('table')->insert()多次获取新实例); - 捕获异常后必须调用
Db::rollback(),且建议用try...catch...finally确保回滚执行; - 不要在事务中做日志记录(如
Log::write())、HTTP 请求或文件写入,这些操作不受事务控制,失败后状态会不一致。
示例关键片段:
Db::startTrans();
try {
Db::name('user')->insertAll($batchData);
Db::name('profile')->insertAll($profileData);
Db::commit();
} catch (\Exception $e) {
Db::rollback();
throw $e;
}
开启 PDO 的 PDO::ATTR_EMULATE_PREPARES = false 能显著提速
ThinkPHP 默认启用预处理模拟(emulatePrepares=true),这会让 MySQL 服务端收到的是普通 SQL 字符串而非真正预处理语句,批量插入时无法复用执行计划,还多一次 PHP 层拼 SQL 的开销。
在数据库配置中显式关闭它:
- 配置项加
'params' => [PDO::ATTR_EMULATE_PREPARES => false]; - 必须确保 MySQL 版本 ≥ 5.6,且启用了
prepare_statement功能(默认开启); - 关闭后,
insertAll()生成的多值 INSERT 会被真正作为预处理语句执行,TP 内部不再做字符串拼接,CPU 占用下降明显。
别忽略主键冲突场景下的 replace 和 ignore 选项
业务中常遇到“存在则更新,不存在则插入”,若用先 select 再判断插入/更新,性能断崖式下跌。ThinkPHP 6.1+ 支持 insertAll() 的第三个参数传 ['replace' => true] 或 ['ignore' => true],对应 MySQL 的 REPLACE INTO 和 INSERT IGNORE。
-
replace会先删后插,适合主键/唯一键冲突必须覆盖的场景; -
ignore直接跳过冲突行,适合去重导入类需求; - 二者都比“查-判-插/更”少至少一次查询,但要注意:
replace可能触发ON DELETE CASCADE,而ignore不会抛异常,需靠影响行数判断是否被跳过。
复杂点在于:这些选项只对 MySQL 有效,SQLite 或 PostgreSQL 不支持。跨库项目里,得在封装层判断当前驱动类型再分支处理,否则上线就报错 REPLACE is not supported by this database。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











