通过 PHP 循环中的串联优化字符串构建
Optimise string building with concatenation in PHP loop
我有一个 PHP 函数可以批量插入 MYSQL table。该函数将输入参数作为一个数组,然后遍历该数组以构建插入查询,如下所示:
public function batchInsert($values){
$nbValues = count($values);
$sql = 'INSERT INTO vouchers (`code`,`pin`,`owner_id`,`value`,`description`,`expire_date`,`lifetime`) VALUES ';
for ($i=0; $i < $nbValues; $i++) {
$sql .= '(:col1_'.$i.', :col2_'.$i.', :col3_'.$i.', :col4_'.$i.', :col5_'.$i.', :col6_'.$i.', :col7_'.$i.')';
if ($i !== ($nbValues-1))
$sql .= ',';
}
$command = Yii::app()->db->createCommand($sql);
for ($i=0; $i < $nbValues; $i++) {
$command->bindParam(':col1_'.$i, $values[$i]['code'], PDO::PARAM_STR);
$command->bindValue(':col2_'.$i, sha1($values[$i]['pin']), PDO::PARAM_STR);
$command->bindParam(':col3_'.$i, $values[$i]['owner_id'], PDO::PARAM_INT);
$command->bindParam(':col4_'.$i, $values[$i]['value'], PDO::PARAM_INT);
$command->bindParam(':col5_'.$i, $values[$i]['description'], PDO::PARAM_STR);
$command->bindParam(':col6_'.$i, $values[$i]['expire_date'], PDO::PARAM_STR);
$command->bindParam(':col7_'.$i, $values[$i]['lifetime'], PDO::PARAM_INT);
}
return $command->execute();
}
如果输入数组有 1K 个元素,构建这个 sql 查询将花费相当长的时间。我相信这是由 $sql 变量在每次循环后重建的方式引起的。有没有更好的方法可以建议我优化它?谢谢!
P/S:在本次批量插入的最后,我需要将所有生成的凭证导出到一个Excel文件中。因此,如果我构建了一个查询并且查询成功,则调用导出函数。通过多次单独插入,我无法跟踪哪个已插入,哪个未插入(例如,优惠券代码是唯一的、随机生成的,并且可能会发生冲突)。这就是为什么我需要一个查询(或者我错了吗?)。
我所做的是将字符串更改为数组,然后在最后一步将其内爆:
public function batchInsert($values){
$nbValues = count($values);
$sql = array();
for ($i=0; $i < $nbValues; $i++) {
$sql[] = '(:col1_'.$i.', :col2_'.$i.', :col3_'.$i.', :col4_'.$i.', :col5_'.$i.', :col6_'.$i.', :col7_'.$i.')';
}
$command = Yii::app()->db->createCommand('INSERT INTO vouchers (`code`,`pin`,`owner_id`,`value`,`description`,`expire_date`,`lifetime`) VALUES (' . implode('),(',$sql) . ')');
for ($i=0; $i < $nbValues; $i++) {
$command->bindParam(':col1_'.$i, $values[$i]['code'], PDO::PARAM_STR);
$command->bindValue(':col2_'.$i, sha1($values[$i]['pin']), PDO::PARAM_STR);
$command->bindParam(':col3_'.$i, $values[$i]['owner_id'], PDO::PARAM_INT);
$command->bindParam(':col4_'.$i, $values[$i]['value'], PDO::PARAM_INT);
$command->bindParam(':col5_'.$i, $values[$i]['description'], PDO::PARAM_STR);
$command->bindParam(':col6_'.$i, $values[$i]['expire_date'], PDO::PARAM_STR);
$command->bindParam(':col7_'.$i, $values[$i]['lifetime'], PDO::PARAM_INT);
}
return $command->execute();
}
与其构建一个巨大的字符串,不如考虑执行单独的插入,但要利用准备好的语句
public function batchInsert($values){
$nbValues = count($values);
$sql = 'INSERT INTO vouchers (`code`,`pin`,`owner_id`,`value`,`description`,`expire_date`,`lifetime`)
VALUES (:col1, :col2, :col3, :col4, :col5, :col6, :col7)';
$command = Yii::app()->db->createCommand($sql);
for ($i=0; $i < $nbValues; $i++) {
$command->bindParam(':col1', $values[$i]['code'], PDO::PARAM_STR);
$command->bindValue(':col2', sha1($values[$i]['pin']), PDO::PARAM_STR);
$command->bindParam(':col3', $values[$i]['owner_id'], PDO::PARAM_INT);
$command->bindParam(':col4', $values[$i]['value'], PDO::PARAM_INT);
$command->bindParam(':col5', $values[$i]['description'], PDO::PARAM_STR);
$command->bindParam(':col6', $values[$i]['expire_date'], PDO::PARAM_STR);
$command->bindParam(':col7', $values[$i]['lifetime'], PDO::PARAM_INT);
$command->execute();
}
}
所以你只准备了一次短插入语句,并且只是 bind/execute 在循环中
让我们先定义您的要求:
- 您需要插入1000条记录,记录被定义在一个数组中
- 应该很快
- 插入可能会失败,因此必须重复
第一个问题是您在这里处理数据库。现代 MySQL 使用 InnoDB
作为存储引擎——它是一个事务引擎。
PDO
,默认情况下,使用名为 auto-commit
的东西。
这一切对您来说意味着什么?基本上,这意味着事务引擎将强制 硬盘驱动器在它告诉您记录已写入之前真正写入记录。 MyISAM 或 NoSQL 等引擎不会这样做。他们只会让 OS 担心写入,而 OS 只会将它应该写入磁盘的信息排队。
磁盘非常慢,所以 OS 试图弥补,有些磁盘甚至有缓存,用于存储大量临时数据。
但是,除非信息 确实写入磁盘,否则不会保存,因为它可能会丢失。这是数据库中 ACID
的 D
部分 - 数据是 持久的 ,因此它位于永久存储设备上。这就是为什么 MySQL 和其他事务性数据库很慢的原因——因为硬盘驱动器是非常慢的设备。机械硬盘驱动器每秒能够执行 100 - 300 次写入(我们称之为 IOPS
或每秒输入输出操作)。这是蜗牛般的慢。
因此,PDO
默认情况下会强制每个查询成为一个事务。这意味着您执行的每个查询都将采用 1 IOPS
而您只有其中的几个。因此,当您 运行 1000 个插入时,如果一切都很好并且您确实有 300 个 IOPS
可用,您的插入将需要一段时间。如果它们失败并且您必须重试它们,那么情况会更糟,因为它会持续更长时间。
那么你可以做些什么来让它更快呢?你做两件事。
1) 完成后,您使用 PDO
的方法 beginTransaction
和 commit
将多个插入包装到单个事务中。这使得硬盘使用 1 IOPS
写入多条记录。如果将所有 1000 次插入都打包到一个事务中,它很可能会非常快地写入。尽管磁盘的容量很低 IOPS
,但它们包含相当多的带宽,因此它们可以一次吃完所有 1000 个插入
2) 确保您的所有插入操作都会成功。这意味着您可能应该在游戏的稍后阶段,一旦插入所有内容后生成您的优惠券代码。请记住,如果事务中的单个查询失败 - 所有查询都失败(ACID
的 A
- 原子性)。
基本上,我在这里想强调的是 Mark Baker 发布了一个很好的答案,您很可能应该稍微修改一下您的逻辑。准备语句一次,执行多次。但是,wrap 在一个事务中多次调用执行 - 这将使其运行得非常快。
我有一个 PHP 函数可以批量插入 MYSQL table。该函数将输入参数作为一个数组,然后遍历该数组以构建插入查询,如下所示:
public function batchInsert($values){
$nbValues = count($values);
$sql = 'INSERT INTO vouchers (`code`,`pin`,`owner_id`,`value`,`description`,`expire_date`,`lifetime`) VALUES ';
for ($i=0; $i < $nbValues; $i++) {
$sql .= '(:col1_'.$i.', :col2_'.$i.', :col3_'.$i.', :col4_'.$i.', :col5_'.$i.', :col6_'.$i.', :col7_'.$i.')';
if ($i !== ($nbValues-1))
$sql .= ',';
}
$command = Yii::app()->db->createCommand($sql);
for ($i=0; $i < $nbValues; $i++) {
$command->bindParam(':col1_'.$i, $values[$i]['code'], PDO::PARAM_STR);
$command->bindValue(':col2_'.$i, sha1($values[$i]['pin']), PDO::PARAM_STR);
$command->bindParam(':col3_'.$i, $values[$i]['owner_id'], PDO::PARAM_INT);
$command->bindParam(':col4_'.$i, $values[$i]['value'], PDO::PARAM_INT);
$command->bindParam(':col5_'.$i, $values[$i]['description'], PDO::PARAM_STR);
$command->bindParam(':col6_'.$i, $values[$i]['expire_date'], PDO::PARAM_STR);
$command->bindParam(':col7_'.$i, $values[$i]['lifetime'], PDO::PARAM_INT);
}
return $command->execute();
}
如果输入数组有 1K 个元素,构建这个 sql 查询将花费相当长的时间。我相信这是由 $sql 变量在每次循环后重建的方式引起的。有没有更好的方法可以建议我优化它?谢谢!
P/S:在本次批量插入的最后,我需要将所有生成的凭证导出到一个Excel文件中。因此,如果我构建了一个查询并且查询成功,则调用导出函数。通过多次单独插入,我无法跟踪哪个已插入,哪个未插入(例如,优惠券代码是唯一的、随机生成的,并且可能会发生冲突)。这就是为什么我需要一个查询(或者我错了吗?)。
我所做的是将字符串更改为数组,然后在最后一步将其内爆:
public function batchInsert($values){
$nbValues = count($values);
$sql = array();
for ($i=0; $i < $nbValues; $i++) {
$sql[] = '(:col1_'.$i.', :col2_'.$i.', :col3_'.$i.', :col4_'.$i.', :col5_'.$i.', :col6_'.$i.', :col7_'.$i.')';
}
$command = Yii::app()->db->createCommand('INSERT INTO vouchers (`code`,`pin`,`owner_id`,`value`,`description`,`expire_date`,`lifetime`) VALUES (' . implode('),(',$sql) . ')');
for ($i=0; $i < $nbValues; $i++) {
$command->bindParam(':col1_'.$i, $values[$i]['code'], PDO::PARAM_STR);
$command->bindValue(':col2_'.$i, sha1($values[$i]['pin']), PDO::PARAM_STR);
$command->bindParam(':col3_'.$i, $values[$i]['owner_id'], PDO::PARAM_INT);
$command->bindParam(':col4_'.$i, $values[$i]['value'], PDO::PARAM_INT);
$command->bindParam(':col5_'.$i, $values[$i]['description'], PDO::PARAM_STR);
$command->bindParam(':col6_'.$i, $values[$i]['expire_date'], PDO::PARAM_STR);
$command->bindParam(':col7_'.$i, $values[$i]['lifetime'], PDO::PARAM_INT);
}
return $command->execute();
}
与其构建一个巨大的字符串,不如考虑执行单独的插入,但要利用准备好的语句
public function batchInsert($values){
$nbValues = count($values);
$sql = 'INSERT INTO vouchers (`code`,`pin`,`owner_id`,`value`,`description`,`expire_date`,`lifetime`)
VALUES (:col1, :col2, :col3, :col4, :col5, :col6, :col7)';
$command = Yii::app()->db->createCommand($sql);
for ($i=0; $i < $nbValues; $i++) {
$command->bindParam(':col1', $values[$i]['code'], PDO::PARAM_STR);
$command->bindValue(':col2', sha1($values[$i]['pin']), PDO::PARAM_STR);
$command->bindParam(':col3', $values[$i]['owner_id'], PDO::PARAM_INT);
$command->bindParam(':col4', $values[$i]['value'], PDO::PARAM_INT);
$command->bindParam(':col5', $values[$i]['description'], PDO::PARAM_STR);
$command->bindParam(':col6', $values[$i]['expire_date'], PDO::PARAM_STR);
$command->bindParam(':col7', $values[$i]['lifetime'], PDO::PARAM_INT);
$command->execute();
}
}
所以你只准备了一次短插入语句,并且只是 bind/execute 在循环中
让我们先定义您的要求:
- 您需要插入1000条记录,记录被定义在一个数组中
- 应该很快
- 插入可能会失败,因此必须重复
第一个问题是您在这里处理数据库。现代 MySQL 使用 InnoDB
作为存储引擎——它是一个事务引擎。
PDO
,默认情况下,使用名为 auto-commit
的东西。
这一切对您来说意味着什么?基本上,这意味着事务引擎将强制 硬盘驱动器在它告诉您记录已写入之前真正写入记录。 MyISAM 或 NoSQL 等引擎不会这样做。他们只会让 OS 担心写入,而 OS 只会将它应该写入磁盘的信息排队。 磁盘非常慢,所以 OS 试图弥补,有些磁盘甚至有缓存,用于存储大量临时数据。
但是,除非信息 确实写入磁盘,否则不会保存,因为它可能会丢失。这是数据库中 ACID
的 D
部分 - 数据是 持久的 ,因此它位于永久存储设备上。这就是为什么 MySQL 和其他事务性数据库很慢的原因——因为硬盘驱动器是非常慢的设备。机械硬盘驱动器每秒能够执行 100 - 300 次写入(我们称之为 IOPS
或每秒输入输出操作)。这是蜗牛般的慢。
因此,PDO
默认情况下会强制每个查询成为一个事务。这意味着您执行的每个查询都将采用 1 IOPS
而您只有其中的几个。因此,当您 运行 1000 个插入时,如果一切都很好并且您确实有 300 个 IOPS
可用,您的插入将需要一段时间。如果它们失败并且您必须重试它们,那么情况会更糟,因为它会持续更长时间。
那么你可以做些什么来让它更快呢?你做两件事。
1) 完成后,您使用 PDO
的方法 beginTransaction
和 commit
将多个插入包装到单个事务中。这使得硬盘使用 1 IOPS
写入多条记录。如果将所有 1000 次插入都打包到一个事务中,它很可能会非常快地写入。尽管磁盘的容量很低 IOPS
,但它们包含相当多的带宽,因此它们可以一次吃完所有 1000 个插入
2) 确保您的所有插入操作都会成功。这意味着您可能应该在游戏的稍后阶段,一旦插入所有内容后生成您的优惠券代码。请记住,如果事务中的单个查询失败 - 所有查询都失败(ACID
的 A
- 原子性)。
基本上,我在这里想强调的是 Mark Baker 发布了一个很好的答案,您很可能应该稍微修改一下您的逻辑。准备语句一次,执行多次。但是,wrap 在一个事务中多次调用执行 - 这将使其运行得非常快。