通过 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 试图弥补,有些磁盘甚至有缓存,用于存储大量临时数据。

但是,除非信息 确实写入磁盘,否则不会保存,因为它可能会丢失。这是数据库中 ACIDD 部分 - 数据是 持久的 ,因此它位于永久存储设备上。这就是为什么 MySQL 和其他事务性数据库很慢的原因——因为硬盘驱动器是非常慢的设备。机械硬盘驱动器每秒能够执行 100 - 300 次写入(我们称之为 IOPS 或每秒输入输出操作)。这是蜗牛般的慢。

因此,PDO 默认情况下会强制每个查询成为一个事务。这意味着您执行的每个查询都将采用 1 IOPS 而您只有其中的几个。因此,当您 运行 1000 个插入时,如果一切都很好并且您确实有 300 个 IOPS 可用,您的插入将需要一段时间。如果它们失败并且您必须重试它们,那么情况会更糟,因为它会持续更长时间。

那么你可以做些什么来让它更快呢?你做两件事。

1) 完成后,您使用 PDO 的方法 beginTransactioncommit 将多个插入包装到单个事务中。这使得硬盘使用 1 IOPS 写入多条记录。如果将所有 1000 次插入都打包到一个事务中,它很可能会非常快地写入。尽管磁盘的容量很低 IOPS,但它们包含相当多的带宽,因此它们可以一次吃完所有 1000 个插入

2) 确保您的所有插入操作都会成功。这意味着您可能应该在游戏的稍后阶段,一旦插入所有内容后生成您的优惠券代码。请记住,如果事务中的单个查询失败 - 所有查询都失败(ACIDA - 原子性)。

基本上,我在这里想强调的是 Mark Ba​​ker 发布了一个很好的答案,您很可能应该稍微修改一下您的逻辑。准备语句一次,执行多次。但是,wrap 在一个事务中多次调用执行 - 这将使其运行得非常快。