
本教程旨在解决 php yii2 应用中向 mysql 数据库批量导入数据效率低下的问题。文章将深入分析 activerecord `save()` 方法的性能瓶颈,并提出一系列优化策略,包括使用原生 `insert()` 命令、预加载关联数据、采用 `batchinsert()` 批量插入以及利用数据库事务管理,旨在显著提升数据导入速度,为处理百万级数据量提供高效解决方案。
在 PHP Yii2 开发中,当需要从外部数据源(如 JSON 文件)导入大量数据到 MySQL 数据库时,开发者常会遇到性能瓶颈。传统的 ActiveRecord save() 方法虽然便捷,但在处理成千上万条记录时,其效率会急剧下降,导致导入时间过长。本教程将探讨导致此问题的原因,并提供一系列行之有效的优化方案。
最初的代码示例中,循环遍历 JSON 数据并为每条记录创建一个 Product_dub 实例,然后调用 $item->save()。这种做法在数据量较小时表现尚可,但随着数据量的增加,性能会显著恶化。主要原因如下:
这些因素叠加起来,使得导入速度呈现非线性下降,数据量越大,耗时越长。
Yii2 的 ActiveRecord 提供了便捷的 ORM 操作,但在追求极致性能的批量导入场景下,直接使用数据库命令执行 INSERT 操作是更优的选择。Yii::$app->db->createCommand()->insert() 方法绕过了 ActiveRecord 的大部分生命周期事件和对象实例化开销,直接构建并执行 SQL 语句。
优化前的代码片段(使用 save()):
foreach ($products as $product) {
$item = new Product_dub();
// ... 属性赋值 ...
if (!$item->save()) {
// 处理错误
}
}优化后的代码片段(使用 insert()):
foreach ($products as $product) {
Yii::$app->db->createCommand()->insert('product_dub', [
'id_1c_product' => $product->id,
'category_id' => $categoryMap[$product->category_id] ?? '0',
'title' => $product->title,
'brand_id' => $brandMap[$product->brand_id] ?? 'No brand',
// ... 其他属性 ...
])->execute();
}通过将 save() 替换为 insert()->execute(),导入速度将得到显著提升。原始问题中,1107 条记录的导入时间从数分钟缩短到 40 秒,证明了这种方法立竿见影的效果。
在循环内部进行数据库查询(如 findOne())是另一个严重的性能瓶颈。对于 category_id 和 brand_id 这类需要映射的字段,最佳实践是在循环开始前一次性加载所有必要的映射数据到内存中。
优化前的代码片段(循环内查询):
foreach ($products as $product) {
$category = Category_dub::findOne(['id_1c_category' => $product->category_id]);
$brand = Brands_dub::findOne(['id_1c_brand' => $product->brand->id]);
// ... 使用 $category->id 和 $brand->id ...
}优化后的代码片段(预加载映射):
$categoryMap = Category_dub::find()->select(['id', 'id_1c_category'])->indexBy('id_1c_category')->column();
$brandMap = Brands_dub::find()->select(['id', 'id_1c_brand'])->indexBy('id_1c_brand')->column();
foreach ($products as $product) {
// 直接从内存映射中获取 ID
$categoryId = $categoryMap[$product->category_id] ?? '0';
$brandId = $brandMap[$product->brand_id] ?? 'No brand';
// ... 使用 $categoryId 和 $brandId 进行插入 ...
}通过这种方式,将 N 次数据库查询减少为 2 次(加载分类和品牌映射),大大降低了数据库交互的频率。
即使使用 insert()->execute(),每次循环仍然会执行一条 SQL 语句。对于非常大的数据集(例如十万或百万条记录),更高效的方法是使用 batchInsert()。batchInsert() 允许一次性构建一个包含多条记录的 INSERT SQL 语句,从而显著减少数据库往返次数和 SQL 解析开销。
batchInsert() 示例代码:
public function importProductFileOptimized($file, $return = true)
{
$products = json_decode($file, true); // 解码为关联数组
$dubTableName = Product::tableName() . "_dub";
$start = microtime(true); // 使用 microtime 更精确
// 假设 createDuplicateTable 成功
// if ($this->db->createDuplicateTable(Product::tableName(), $dubTableName)) {
$categoryMap = Category_dub::find()->select(['id', 'id_1c_category'])->indexBy('id_1c_category')->column();
$brandMap = Brands_dub::find()->select(['id', 'id_1c_brand'])->indexBy('id_1c_brand')->column();
$rowsToInsert = [];
$batchSize = 1000; // 每批次插入的记录数
foreach ($products as $product) {
$rowsToInsert[] = [
'id_1c_product' => $product['id'],
'category_id' => $categoryMap[$product['category_id']] ?? '0',
'title' => $product['title'],
'brand_id' => $brandMap[$product['brand_id']] ?? 'No brand',
'content1' => $product['content1'],
'content2' => $product['content2'],
'content3' => $product['content3'],
'link_order' => $product['link_order'],
'img' => $product['img'] ?? 'no-image.png',
'in_stock' => $product['in_stock'] ? 1 : 0,
'is_popular' => $product['is_popular'] ? 1 : 0,
];
// 达到批次大小或遍历结束时执行插入
if (count($rowsToInsert) >= $batchSize) {
Yii::$app->db->createCommand()->batchInsert('product_dub', array_keys($rowsToInsert[0]), $rowsToInsert)->execute();
$rowsToInsert = []; // 清空数组准备下一批
}
}
// 插入剩余的记录(如果不足一个批次)
if (!empty($rowsToInsert)) {
Yii::$app->db->createCommand()->batchInsert('product_dub', array_keys($rowsToInsert[0]), $rowsToInsert)->execute();
}
// }
$finish = microtime(true);
$res = round($finish - $start, 2) . " sec. ";
if ($return) {
echo $res;
// Answer::success(); // 假设 Answer::success() 是一个自定义的成功响应方法
}
}在 batchInsert() 中,array_keys($rowsToInsert[0]) 用于获取字段名列表,$rowsToInsert 是一个包含所有待插入行数据的二维数组。选择合适的 $batchSize 很重要,过小则批次效果不明显,过大则可能导致 SQL 语句过长超出数据库或 PHP 内存限制。通常几百到几千条记录为一个批次是比较合理的。
对于任何批量数据操作,使用数据库事务是至关重要的。事务可以确保数据的一致性:要么所有记录都成功插入,要么在发生错误时全部回滚,避免部分数据插入的脏数据问题。
public function importProductFileWithTransaction($file, $return = true)
{
$products = json_decode($file, true);
$dubTableName = Product::tableName() . "_dub";
$start = microtime(true);
$transaction = Yii::$app->db->beginTransaction(); // 开启事务
try {
// 假设 createDuplicateTable 成功
// if ($this->db->createDuplicateTable(Product::tableName(), $dubTableName)) {
$categoryMap = Category_dub::find()->select(['id', 'id_1c_category'])->indexBy('id_1c_category')->column();
$brandMap = Brands_dub::find()->select(['id', 'id_1c_brand'])->indexBy('id_1c_brand')->column();
$rowsToInsert = [];
$batchSize = 1000;
foreach ($products as $product) {
$rowsToInsert[] = [
'id_1c_product' => $product['id'],
'category_id' => $categoryMap[$product['category_id']] ?? '0',
'title' => $product['title'],
'brand_id' => $brandMap[$product['brand_id']] ?? 'No brand',
'content1' => $product['content1'],
'content2' => $product['content2'],
'content3' => $product['content3'],
'link_order' => $product['link_order'],
'img' => $product['img'] ?? 'no-image.png',
'in_stock' => $product['in_stock'] ? 1 : 0,
'is_popular' => $product['is_popular'] ? 1 : 0,
];
if (count($rowsToInsert) >= $batchSize) {
Yii::$app->db->createCommand()->batchInsert('product_dub', array_keys($rowsToInsert[0]), $rowsToInsert)->execute();
$rowsToInsert = [];
}
}
if (!empty($rowsToInsert)) {
Yii::$app->db->createCommand()->batchInsert('product_dub', array_keys($rowsToInsert[0]), $rowsToInsert)->execute();
}
// }
$transaction->commit(); // 提交事务
$finish = microtime(true);
$res = round($finish - $start, 2) . " sec. ";
if ($return) {
echo $res;
// Answer::success();
}
} catch (\Exception $e) {
$transaction->rollBack(); // 发生错误时回滚事务
// Answer::failure("导入失败:" . $e->getMessage()); // 自定义错误处理
if ($return) {
echo "导入失败:" . $e->getMessage();
}
}
}在事务中执行批量操作可以显著提高性能,因为数据库只需要在事务结束时进行一次持久化操作,而不是每次插入都进行。
对于百万级甚至千万级的数据导入,上述 PHP 层的优化可能仍然不够。可以考虑以下更高级的策略:
批量数据导入是常见的业务需求,其性能直接影响用户体验和系统效率。通过本教程的分析和优化实践,我们可以得出以下最佳实践:
通过综合运用这些策略,即使面对百万级的数据导入任务,也能将其处理时间从数小时缩短到可接受的分钟级别。
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号