
本文深入探讨了在yii2框架下从json文件批量导入数据到mysql时遇到的性能瓶颈及优化策略。通过对比activerecord的save()方法与db命令的insert()及batchinsert(),并结合预加载关联数据,显著提升了导入效率。文章提供了详细的代码示例和注意事项,旨在帮助开发者高效处理大规模数据导入任务。
在Web应用开发中,尤其是在数据同步或初始化场景下,我们常需要将大量数据从文件(如JSON)导入到数据库。然而,如果不采用正确的策略,这一过程可能会非常耗时。原始代码中,开发者使用Yii2 ActiveRecord的save()方法在循环中逐条插入数据,导致了显著的性能下降。例如,导入数百条记录的时间会从几秒迅速增长到几十秒甚至更长,对于数万甚至百万级别的数据量,这种方法是不可接受的。
save()方法虽然方便,但它为每个模型实例执行了多项操作:实例化模型、数据验证、触发事件回调、以及最终执行一条独立的SQL INSERT 或 UPDATE 语句。当这些操作在循环中针对大量记录重复执行时,其累积的开销(包括PHP层面的处理和与数据库的多次往返通信)将成为严重的性能瓶颈。
考虑以下原始实现的核心逻辑:
foreach ($products as $product) {
$item = new Product_dub();
// ... 赋值属性 ...
$category = Category_dub::findOne(['id_1c_category' => $product->category_id]); // 每次循环查询
$brand = Brands_dub::findOne(['id_1c_brand' => $product->brand_id]); // 每次循环查询
// ... 赋值关联ID ...
if (!$item->save()) { // 每次循环执行一次INSERT
// ... 错误处理 ...
}
}从上述代码可以看出,除了每次循环进行两次数据库查询来获取category和brand之外,最主要的性能消耗在于$item-youjiankuohaophpcnsave()。即使移除了findOne()查询,save()本身的开销依然巨大。
解决save()性能问题的首要步骤是绕过ActiveRecord的全部生命周期,直接使用Yii2的数据库命令执行INSERT操作。Yii2的Yii::$app->db->createCommand()->insert()方法允许我们直接构建并执行SQL插入语句,极大地减少了框架层面的开销。
通过将$item->save()替换为insert()命令,性能得到了显著提升。例如,在测试中,1107行数据导入时间从最初的数分钟缩短到约40秒。
以下是使用insert()命令进行优化的示例:
foreach ($products as $product) {
Yii::$app->db->createCommand()->insert('product_dub', [
'id_1c_product' => $product->id,
// ... 其他属性 ...
'category_id' => $categoryMap[$product->category_id] ?? '0', // 假设categoryMap已预加载
'brand_id' => $brandMap[$product->brand_id] ?? 'No brand', // 假设brandMap已预加载
// ...
])->execute();
}这种方法虽然仍是循环中逐条插入,但每次循环仅执行一次SQL INSERT 命令,避免了ActiveRecord的验证、事件等额外处理,从而显著提高了效率。
Easily find JSON paths within JSON objects using our intuitive Json Path Finder
30
在原始代码中,Category_dub::findOne()和Brands_dub::findOne()在每次循环中都会执行一次数据库查询,以根据外部ID查找内部ID。对于N条记录,这将导致2N次额外的数据库查询,这就是典型的N+1查询问题,进一步拖慢了导入速度。
为了消除这些重复查询,我们应该在导入循环开始之前,一次性地从数据库中加载所有必要的关联数据,并将其存储在内存中的映射(Map)结构中。这样,在循环内部,我们只需进行内存查找,而非数据库查询。
以下是预加载分类和品牌数据的示例:
$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) {
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', // 从内存映射中获取
'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,
])->execute();
}通过结合insert()命令和预加载关联数据,我们可以看到一个完整的、大幅优化后的导入逻辑。
尽管insert()命令比save()快得多,但它仍然是循环中逐条执行SQL语句。对于处理数万甚至百万级别的数据,最佳实践是使用Yii2提供的batchInsert()方法。batchInsert()能够生成一条包含多行数据的INSERT SQL语句,一次性将多条记录发送到数据库,从而显著减少了与数据库的通信次数,进一步提升了性能。
batchInsert()方法的参数包括表名、列名数组和值数组(每个元素代表一行数据)。
public function importProductFile($file, $return = true)
{
$products = json_decode($file, true); // 解码为关联数组更方便
$dubTableName = Product::tableName() . "_dub";
$start = time();
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();
$rows = [];
foreach ($products as $product) {
$rows[] = [
'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 (!empty($rows)) {
Yii::$app->db->createCommand()->batchInsert('product_dub', array_keys($rows[0]), $rows)->execute();
}
}
$finish = time();
$res = $finish - $start . "sec. ";
if ($return) {
echo $res;
Answer::success();
}
}除了上述代码层面的优化,还有一些其他因素可以影响批量导入
以上就是Yii2中JSON数据批量导入MySQL的性能优化实践的详细内容,更多请关注php中文网其它相关文章!
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号