【问题标题】:PDO Prepared Inserts multiple rows in single queryPDO Prepared 在单个查询中插入多行
【发布时间】:2010-11-13 15:56:43
【问题描述】:

我目前在 MySQL 上使用这种类型的 SQL 在一个查询中插入多行值:

INSERT INTO `tbl` (`key1`,`key2`) VALUES ('r1v1','r1v2'),('r2v1','r2v2'),...

关于 PDO 的阅读,使用准备好的语句应该给我比静态查询更好的安全性。

因此,我想知道是否可以使用预准备语句生成“通过使用一个查询插入多行值”。

如果是,请问我该如何实施?

【问题讨论】:

  • 小心$stmt->execute($data); php.net/manual/en/… 的很多答案基本上所有的参数都作为字符串传递验证。只需在构建查询后遍历数据,并手动将bindValuebindParam 传递类型作为第三个参数。

标签: php pdo insert prepared-statement


【解决方案1】:

这根本不是您使用准备好的语句的方式。

每个查询插入一行是完全可以的,因为您可以使用不同的参数多次执行一个准备好的语句。事实上,这是最大的优势之一,因为它允许您以高效、安全和舒适的方式插入大量行。

因此,至少可以针对固定数量的行实施您提出的方案,但几乎可以肯定这不是您真正想要的。

【讨论】:

  • 你能推荐一种更好的方法来将多行插入到一个表中吗?
  • @Crashthatch:用天真的方式来做:设置一次准备好的语句,然后为每一行执行它,为绑定参数设置不同的值。这是 Zyk 回答中的第二种方法。
  • 你提到的prepared statement的目的是正确的。但是,使用 multi-insert 是另一种提高插入速度的技术,它也可以与准备好的语句一起使用。根据我的经验,在使用 PDO 准备语句迁移 3000 万行数据时,我发现多插入比在事务中分组单插入快 7-10 倍。
  • 完全同意 Anis。我有 100k 行,并且通过 muli 行插入获得了巨大的速度提升。
  • 声称在循环中每行调用一次关系数据库通常是一件好事,这是我不能同意的。对此投反对票。诚然,有时没关系。我不相信工程学的绝对性。但这是一种反模式,只应在特定情况下使用。
【解决方案2】:

两种可能的方法:

$stmt = $pdo->prepare('INSERT INTO foo VALUES(:v1_1, :v1_2, :v1_3),
    (:v2_1, :v2_2, :v2_3),
    (:v2_1, :v2_2, :v2_3)');
$stmt->bindValue(':v1_1', $data[0][0]);
$stmt->bindValue(':v1_2', $data[0][1]);
$stmt->bindValue(':v1_3', $data[0][2]);
// etc...
$stmt->execute();

或者:

$stmt = $pdo->prepare('INSERT INTO foo VALUES(:a, :b, :c)');
foreach($data as $item)
{
    $stmt->bindValue(':a', $item[0]);
    $stmt->bindValue(':b', $item[1]);
    $stmt->bindValue(':c', $item[2]);
    $stmt->execute();
}

如果所有行的数据都在一个数组中,我会使用第二种解决方案。

【讨论】:

  • 在后者中,您不是要进行几个(可能数千个)单独的执行调用而不是组合成一个语句吗?
  • @JM4,您是否建议 $stmt->execute(); 应该在 foreach 循环之外?
  • @bafromca - 是的,我是。用upvotes查看我上面的答案。在纯插入语句中,有 no 理由我可以从逻辑上提出它不能是单个语句。一调用,一执行。事实上,我在 2012 年初的回答可能会进一步改进——我稍后会在有更多时间时做一些事情。如果您开始使用插入/更新/删除组合,那就是另一回事了。
【解决方案3】:

使用 PDO 准备语句插入多个值

在一个执行语句中插入多个值。为什么,因为根据this page,它比常规插入要快。

$datafields = array('fielda', 'fieldb', ... );

$data[] = array('fielda' => 'value', 'fieldb' => 'value' ....);
$data[] = array('fielda' => 'value', 'fieldb' => 'value' ....);

更多数据值,或者您可能有一个填充数据的循环。

使用准备好的插入,您需要知道要插入的字段以及要创建的字段数?用于绑定参数的占位符。

insert into table (fielda, fieldb, ... ) values (?,?...), (?,?...)....

这基本上就是我们希望插入语句的样子。

现在,代码:

function placeholders($text, $count=0, $separator=","){
    $result = array();
    if($count > 0){
        for($x=0; $x<$count; $x++){
            $result[] = $text;
        }
    }

    return implode($separator, $result);
}

$pdo->beginTransaction(); // also helps speed up your inserts.
$insert_values = array();
foreach($data as $d){
    $question_marks[] = '('  . placeholders('?', sizeof($d)) . ')';
    $insert_values = array_merge($insert_values, array_values($d));
}

$sql = "INSERT INTO table (" . implode(",", $datafields ) . ") VALUES " .
       implode(',', $question_marks);

$stmt = $pdo->prepare ($sql);
$stmt->execute($insert_values);
$pdo->commit();

虽然在我的测试中,使用多个插入和具有单个值的常规准备插入时只有 1 秒的差异。

【讨论】:

  • 一个错字,在上面的解释中它提到了$datafields,尽管在$sql中使用了$datafield。因此复制粘贴会导致错误。请纠正。不过感谢这个解决方案。
  • 使用了一段时间,然后注意到其中带有单引号的值没有正确转义。对内爆使用双引号对我来说就像一个魅力: $a[] = '("' . implode(",", $question_marks) . '", NOW())';
  • array_merge 似乎比仅仅使用 array_push 更昂贵。
  • 当您说“只有 1 秒的差异”时,您插入了多少行数据? 1 秒非常重要,具体取决于上下文。
  • 优化:一遍又一遍地调用placeholders()没有意义。在循环前使用sizeof($datafields) 调用一次,并将结果字符串附加到循环内的$question_marks[]
【解决方案4】:

和巴拉格塔斯先生的回答一样,稍微清楚一点……

最新版本的 MySQL 和 PHP PDO支持多行 INSERT 语句。

SQL 概述

SQL 看起来像这样,假设您想要 INSERT 到一个 3 列表。

INSERT INTO tbl_name
            (colA, colB, colC)
     VALUES (?, ?, ?), (?, ?, ?), (?, ?, ?) [,...]

ON DUPLICATE KEY UPDATE 即使使用多行 INSERT 也能正常工作;附加这个:

ON DUPLICATE KEY UPDATE colA = VALUES(colA), colB = VALUES(colB), colC = VALUES(colC)

PHP 概述

您的 PHP 代码将遵循通常的 $pdo-&gt;prepare($qry)$stmt-&gt;execute($params) PDO 调用。

$params 将是一个一维数组,其中包含所有要传递给INSERT 的值。

在上面的例子中,它应该包含 9 个元素; PDO 将使用每组 3 作为单行值。 (插入 3 行,每行 3 列 = 9 个元素数组。)

实施

下面的代码是为了清晰而不是效率而编写的。如果您愿意,可以使用 PHP array_*() 函数来更好地映射或遍历您的数据。是否可以使用事务显然取决于您的 MySQL 表类型。

假设:

  • $tblName - 要插入到的表的字符串名称
  • $colNames - 表的列名的一维数组 这些列名必须是有效的 MySQL 列标识符;如果不是,请用反引号 (``) 转义它们
  • $dataVals - 多维数组,其中每个元素都是一维数组,其中包含要插入的一行值

示例代码

// setup data values for PDO
// memory warning: this is creating a copy all of $dataVals
$dataToInsert = array();

foreach ($dataVals as $row => $data) {
    foreach($data as $val) {
        $dataToInsert[] = $val;
    }
}

// (optional) setup the ON DUPLICATE column names
$updateCols = array();

foreach ($colNames as $curCol) {
    $updateCols[] = $curCol . " = VALUES($curCol)";
}

$onDup = implode(', ', $updateCols);

// setup the placeholders - a fancy way to make the long "(?, ?, ?)..." string
$rowPlaces = '(' . implode(', ', array_fill(0, count($colNames), '?')) . ')';
$allPlaces = implode(', ', array_fill(0, count($dataVals), $rowPlaces));

$sql = "INSERT INTO $tblName (" . implode(', ', $colNames) . 
    ") VALUES " . $allPlaces . " ON DUPLICATE KEY UPDATE $onDup";

// and then the PHP PDO boilerplate
$stmt = $pdo->prepare ($sql);

$stmt->execute($dataToInsert);

$pdo->commit();

【讨论】:

  • PDO 以这种方式处理它真的太糟糕了,在其他 DB 驱动程序中有一些非常优雅的方法可以做到这一点。
  • 这会更简洁地设置占位符,使 $rowPlaces 不再需要:$allPlaces = implode(',', array_fill(0, count($dataVals), '('.str_pad('', (count($colNames)*2)-1, '?,').')'));
  • 完美运行。我会在这个答案中添加需要确保表中索引(组合)的唯一性。就像在 ALTER TABLE votes 添加唯一 unique_index(user, email, address);
  • 太棒了!顺便说一句,使用array_push($dataToInsert, ...array_values($dataVals)); 会比foreach ($dataVals as $row =&gt; $data) {} 快得多
【解决方案5】:

更简短的答案:然后将按列排序的数据数组展平

//$array = array( '1','2','3','4','5', '1','2','3','4','5');
$arCount = count($array);
$rCount = ($arCount  ? $arCount - 1 : 0);
$criteria = sprintf("(?,?,?,?,?)%s", str_repeat(",(?,?,?,?,?)", $rCount));
$sql = "INSERT INTO table(c1,c2,c3,c4,c5) VALUES$criteria";

当插入大约 1,000 条记录时,您不希望在只需要计数值时遍历每条记录来插入它们。

【讨论】:

    【解决方案6】:

    对于它的价值,我看到很多用户建议迭代 INSERT 语句,而不是像所选答案那样构建为单个字符串查询。我决定用两个字段和一个非常基本的插入语句运行一个简单的测试:

    <?php
    require('conn.php');
    
    $fname = 'J';
    $lname = 'M';
    
    $time_start = microtime(true);
    $stmt = $db->prepare('INSERT INTO table (FirstName, LastName) VALUES (:fname, :lname)');
    
    for($i = 1; $i <= 10; $i++ )  {
        $stmt->bindParam(':fname', $fname);
        $stmt->bindParam(':lname', $lname);
        $stmt->execute();
    
        $fname .= 'O';
        $lname .= 'A';
    }
    
    
    $time_end = microtime(true);
    $time = $time_end - $time_start;
    
    echo "Completed in ". $time ." seconds <hr>";
    
    $fname2 = 'J';
    $lname2 = 'M';
    
    $time_start2 = microtime(true);
    $qry = 'INSERT INTO table (FirstName, LastName) VALUES ';
    $qry .= "(?,?), ";
    $qry .= "(?,?), ";
    $qry .= "(?,?), ";
    $qry .= "(?,?), ";
    $qry .= "(?,?), ";
    $qry .= "(?,?), ";
    $qry .= "(?,?), ";
    $qry .= "(?,?), ";
    $qry .= "(?,?), ";
    $qry .= "(?,?)";
    
    $stmt2 = $db->prepare($qry);
    $values = array();
    
    for($j = 1; $j<=10; $j++) {
        $values2 = array($fname2, $lname2);
        $values = array_merge($values,$values2);
    
        $fname2 .= 'O';
        $lname2 .= 'A';
    }
    
    $stmt2->execute($values);
    
    $time_end2 = microtime(true);
    $time2 = $time_end2 - $time_start2;
    
    echo "Completed in ". $time2 ." seconds <hr>";
    ?>
    

    虽然整个查询本身花费了几毫秒或更短的时间,但后者(单字符串)查询始终快 8 倍或更多。如果这是为了反映在更多列上导入数千行,那么差异可能是巨大的。

    【讨论】:

    • @JM4 - 将 10 行直接放入 一次执行 的好主意。但是当它们存储在像 JSON 这样的对象中时,如何插入数千行呢?我下面的代码完美运行。但是如何调整它以在一次执行中插入 10 行? ` foreach($json_content as $datarow) { $id = $datarow[id]; $date = $datarow[日期]; $row3 = $datarow[row3]; $row4 = $datarow[row4]; $row5 = $datarow[row5]; $row6 = $datarow[row6]; $row7= $datarow[row7]; // 现在执行 $databaseinsert->execute(); } // foreach 结束 `
    • @JM4 - ...我的第二个问题是:“为什么第二个导入例程中没有bind_param 语句”?
    • 你不需要循环两次吗?您还必须动态生成(?,?),对吧?
    • @NoobishPro 是的,您可以使用相同的 for/foreach 来生成两者。
    【解决方案7】:

    Herbert Balagtas 接受的答案在 $data 数组很小的情况下效果很好。对于较大的 $data 数组,array_merge 函数变得异常缓慢。我创建 $data 数组的测试文件有 28 列,大约 80,000 行。最终脚本花了 41 秒 完成。

    使用 array_push() 创建 $insert_values 而不是 array_merge() 导致 100 倍加速,执行时间为 0.41s

    有问题的array_merge():

    $insert_values = array();
    
    foreach($data as $d){
     $question_marks[] = '('  . placeholders('?', sizeof($d)) . ')';
     $insert_values = array_merge($insert_values, array_values($d));
    }
    

    要消除对 array_merge() 的需要,您可以构建以下两个数组:

    //Note that these fields are empty, but the field count should match the fields in $datafields.
    $data[] = array('','','','',... n ); 
    
    //getting rid of array_merge()
    array_push($insert_values, $value1, $value2, $value3 ... n ); 
    

    然后可以按如下方式使用这些数组:

    function placeholders($text, $count=0, $separator=","){
        $result = array();
        if($count > 0){
            for($x=0; $x<$count; $x++){
                $result[] = $text;
            }
        }
    
        return implode($separator, $result);
    }
    
    $pdo->beginTransaction();
    
    foreach($data as $d){
     $question_marks[] = '('  . placeholders('?', sizeof($d)) . ')';
    }
    
    $sql = "INSERT INTO table (" . implode(",", array_keys($datafield) ) . ") VALUES " . implode(',', $question_marks);
    
    $stmt = $pdo->prepare($sql);
    $stmt->execute($insert_values);
    $pdo->commit();
    

    【讨论】:

    • 在 PHP 5.6 中,您可以使用 array_push($data, ...array_values($row)) 而不是 $data = array_merge($data, array_values($row));。更快。
    • 为什么是 5.6 ?文档没有说明 5.6,array_push() 即使在 php 4 中也可用。
    • @Piero 它是 PHP 5.6+ 唯一的代码,不是因为使用了array_push(),而是因为@Mark 正在使用参数解包。注意到那里的...array_values() 呼叫了吗?
    • @mariano.iglesias array_values() 在 php 4 中也可用。不确定这是否是您所说的 argument unpacking 的意思。
    • @Piero,参数解包是 PHP 5.6 中引入的一项功能。这是一种将多个参数作为数组提供的方法。在这里查看 - php.net/manual/en/…
    【解决方案8】:

    这是我编写的一个使用清除选项进行多次插入的类:

    <?php
    
    /**
     * $pdo->beginTransaction();
     * $pmi = new PDOMultiLineInserter($pdo, "foo", array("a","b","c","e"), 10);
     * $pmi->insertRow($data);
     * ....
     * $pmi->insertRow($data);
     * $pmi->purgeRemainingInserts();
     * $pdo->commit();
     *
     */
    class PDOMultiLineInserter {
        private $_purgeAtCount;
        private $_bigInsertQuery, $_singleInsertQuery;
        private $_currentlyInsertingRows  = array();
        private $_currentlyInsertingCount = 0;
        private $_numberOfFields;
        private $_error;
        private $_insertCount = 0;
    
        function __construct(\PDO $pdo, $tableName, $fieldsAsArray, $bigInsertCount = 100) {
            $this->_numberOfFields = count($fieldsAsArray);
            $insertIntoPortion = "INSERT INTO `$tableName` (`".implode("`,`", $fieldsAsArray)."`) VALUES";
            $questionMarks  = " (?".str_repeat(",?", $this->_numberOfFields - 1).")";
    
            $this->_purgeAtCount = $bigInsertCount;
            $this->_bigInsertQuery    = $pdo->prepare($insertIntoPortion.$questionMarks.str_repeat(", ".$questionMarks, $bigInsertCount - 1));
            $this->_singleInsertQuery = $pdo->prepare($insertIntoPortion.$questionMarks);
        }
    
        function insertRow($rowData) {
            // @todo Compare speed
            // $this->_currentlyInsertingRows = array_merge($this->_currentlyInsertingRows, $rowData);
            foreach($rowData as $v) array_push($this->_currentlyInsertingRows, $v);
            //
            if (++$this->_currentlyInsertingCount == $this->_purgeAtCount) {
                if ($this->_bigInsertQuery->execute($this->_currentlyInsertingRows) === FALSE) {
                    $this->_error = "Failed to perform a multi-insert (after {$this->_insertCount} inserts), the following errors occurred:".implode('<br/>', $this->_bigInsertQuery->errorInfo());
                    return false;
                }
                $this->_insertCount++;
    
                $this->_currentlyInsertingCount = 0;
                $this->_currentlyInsertingRows = array();
            }
            return true;
        }
    
        function purgeRemainingInserts() {
            while ($this->_currentlyInsertingCount > 0) {
                $singleInsertData = array();
                // @todo Compare speed - http://www.evardsson.com/blog/2010/02/05/comparing-php-array_shift-to-array_pop/
                // for ($i = 0; $i < $this->_numberOfFields; $i++) $singleInsertData[] = array_pop($this->_currentlyInsertingRows); array_reverse($singleInsertData);
                for ($i = 0; $i < $this->_numberOfFields; $i++) array_unshift($singleInsertData, array_pop($this->_currentlyInsertingRows));
    
                if ($this->_singleInsertQuery->execute($singleInsertData) === FALSE) {
                    $this->_error = "Failed to perform a small-insert (whilst purging the remaining rows; the following errors occurred:".implode('<br/>', $this->_singleInsertQuery->errorInfo());
                    return false;
                }
                $this->_currentlyInsertingCount--;
            }
        }
    
        public function getError() {
            return $this->_error;
        }
    }
    

    【讨论】:

    • 你好皮埃尔。也许你不再在这里活跃了。不过,我只想指出,我对这个问题的想法与您的想法几乎相同。纯属巧合,因为我猜这没有更多。我也添加了删除和更新操作的类,然后从这里引入了一些想法。我只是没有看到你的课。请原谅我在这里无耻的自我推销,但我想这会对某人有所帮助。希望这不违反 SO-Rules。找到它here
    【解决方案9】:

    您可以使用此函数在单个查询中插入多行:

    function insertMultiple($query,$rows) {
        if (count($rows)>0) {
            $args = array_fill(0, count($rows[0]), '?');
    
            $params = array();
            foreach($rows as $row)
            {
                $values[] = "(".implode(',', $args).")";
                foreach($row as $value)
                {
                    $params[] = $value;
                }
            }
    
            $query = $query." VALUES ".implode(',', $values);
            $stmt = $PDO->prepare($query);
            $stmt->execute($params);
        }
    }
    

    $row 是一个 array 值数组。 在您的情况下,您可以使用

    调用该函数
    insertMultiple("INSERT INTO tbl (`key1`,`key2`)",array(array('r1v1','r1v2'),array('r2v1','r2v2')));
    

    这样做的好处是您使用准备好的语句,同时通过单个查询插入多行。安全!

    【讨论】:

      【解决方案10】:

      这对我有用

      $sql = 'INSERT INTO table(pk_pk1,pk_pk2,date,pk_3) VALUES '; 
      $qPart = array_fill(0, count($array), "(?, ?,UTC_TIMESTAMP(),?)");
      $sql .= implode(",", $qPart);
      $stmt =    DB::prepare('base', $sql);
      $i = 1;
      foreach ($array as $value) { 
        $stmt->bindValue($i++, $value);
        $stmt->bindValue($i++, $pk_pk1);
        $stmt->bindValue($i++, $pk_pk2); 
        $stmt->bindValue($i++, $pk_pk3); 
      } 
      $stmt->execute();
      

      【讨论】:

        【解决方案11】:

        这是我的解决方案:https://github.com/sasha-ch/Aura.Sql 基于 auraphp/Aura.Sql 库。

        使用示例:

        $q = "insert into t2(id,name) values (?,?), ... on duplicate key update name=name"; 
        $bind_values = [ [[1,'str1'],[2,'str2']] ];
        $pdo->perform($q, $bind_values);
        

        欢迎提交错误报告。

        【讨论】:

        【解决方案12】:

        我的真实世界示例将所有德国邮政编码插入一个空表(稍后添加城镇名称):

        // obtain column template
        $stmt = $db->prepare('SHOW COLUMNS FROM towns');
        $stmt->execute();
        $columns = array_fill_keys(array_values($stmt->fetchAll(PDO::FETCH_COLUMN)), null);
        // multiple INSERT
        $postcode = '01000';// smallest german postcode
        while ($postcode <= 99999) {// highest german postcode
            $values = array();
            while ($postcode <= 99999) {
                // reset row
                $row = $columns;
                // now fill our row with data
                $row['postcode'] = sprintf('%05d', $postcode);
                // build INSERT array
                foreach ($row as $value) {
                    $values[] = $value;
                }
                $postcode++;
                // avoid memory kill
                if (!($postcode % 10000)) {
                    break;
                }
            }
            // build query
            $count_columns = count($columns);
            $placeholder = ',(' . substr(str_repeat(',?', $count_columns), 1) . ')';//,(?,?,?)
            $placeholder_group = substr(str_repeat($placeholder, count($values) / $count_columns), 1);//(?,?,?),(?,?,?)...
            $into_columns = implode(',', array_keys($columns));//col1,col2,col3
            // this part is optional:
            $on_duplicate = array();
            foreach ($columns as $column => $row) {
                $on_duplicate[] = $column;
                $on_duplicate[] = $column;
            }
            $on_duplicate = ' ON DUPLICATE KEY UPDATE' . vsprintf(substr(str_repeat(', %s = VALUES(%s)', $count_columns), 1), $on_duplicate);
            // execute query
            $stmt = $db->prepare('INSERT INTO towns (' . $into_columns . ') VALUES' . $placeholder_group . $on_duplicate);//INSERT INTO towns (col1,col2,col3) VALUES(?,?,?),(?,?,?)... {ON DUPLICATE...}
            $stmt->execute($values);
        }
        

        正如您所见,它非常灵活。您无需检查列的数量或检查列的位置。你只需要设置插入数据:

            $row['postcode'] = sprintf('%05d', $postcode);
        

        我为某些查询字符串构造函数感到自豪,因为它们无需像 array_merge 这样繁重的数组函数即可工作。尤其是 vsprintf() 是一个很好的发现。

        最后我需要添加 2x while() 以避免超出内存限制。这取决于您的内存限制,但它完全是避免问题的一个很好的通用解决方案(并且有 10 个查询仍然比 10.000 好得多)。

        【讨论】:

          【解决方案13】:

          我就是这样做的:

          首先定义您将使用的列名,或者将其留空,pdo 将假定您要使用表上的所有列 - 在这种情况下,您需要按照行值出现的确切顺序告知行值在桌子上。

          $cols = 'name', 'middleName', 'eMail';
          $table = 'people';
          

          现在,假设您已经准备好一个二维数组。迭代它,并用你的行值构造一个字符串,如下所示:

          foreach ( $people as $person ) {
          if(! $rowVals ) {
          $rows = '(' . "'$name'" . ',' . "'$middleName'" . ',' .           "'$eMail'" . ')';
          } else { $rowVals  = '(' . "'$name'" . ',' . "'$middleName'" . ',' . "'$eMail'" . ')';
          }
          

          现在,您刚刚所做的是检查 $rows 是否已经定义,如果没有,则创建它并存储行值和必要的 SQL 语法,使其成为有效语句。请注意,字符串应该放在双引号和单引号内,这样它们会被及时识别。

          剩下要做的就是准备语句并执行,如下所示:

          $stmt = $db->prepare ( "INSERT INTO $table $cols VALUES $rowVals" );
          $stmt->execute ();
          

          到目前为止测试了多达 2000 行,执行时间很短。将运行更多测试并返回这里以防我有进一步的贡献。

          问候。

          【讨论】:

            【解决方案14】:

            这里给出的大多数用于创建准备好的查询的解决方案都比它们需要的复杂。使用 PHP 的内置函数,您可以轻松创建 SQL 语句,而无需大量开销。

            给定$records,一个记录数组,其中每个记录本身就是一个索引数组(以field =&gt; value 的形式),以下函数将在PDO 连接@ 上将记录插入给定表$table 987654325@,只使用一个准备好的语句。请注意,这是一个 PHP 5.6+ 解决方案,因为在对 array_push 的调用中使用了参数解包:

            private function import(PDO $connection, $table, array $records)
            {
                $fields = array_keys($records[0]);
                $placeHolders = substr(str_repeat(',?', count($fields)), 1);
                $values = [];
                foreach ($records as $record) {
                    array_push($values, ...array_values($record));
                }
            
                $query = 'INSERT INTO ' . $table . ' (';
                $query .= implode(',', $fields);
                $query .= ') VALUES (';
                $query .= implode('),(', array_fill(0, count($records), $placeHolders));
                $query .= ')';
            
                $statement = $connection->prepare($query);
                $statement->execute($values);
            }
            

            【讨论】:

            • 这个代码不应该被使用,因为它容易受到 SQL 注入的攻击
            • @Your 我没看到你看到的。强迫我回答这个问题。
            • @mickmackusa 我在暗示this
            • 啊如果发帖人和研究人员了解有关字段名称注入的具体问题,那将是一件好事。我知道你在早期是个脾气暴躁的小伙子,但请确保以后在相关的 SO 页面上炸掉那个注入页面,以便人们了解这个漏洞。告诉人们一个查询是脆弱的,但不解释如何——是无益的神秘。 VingTD。
            【解决方案15】:

            由于尚未提出建议,我很确定 LOAD DATA INFILE 仍然是加载数据的最快方式,因为它会禁用索引、插入所有数据,然后重新启用索引 - 所有这些都在一个请求中完成。

            将数据保存为 csv 应该是相当简单的,记住 fputcsv。 MyISAM 是最快的,但在 InnoDB 中您仍然可以获得出色的性能。不过还有其他缺点,所以如果你要插入大量数据,我会走这条路,而且不会打扰不到 100 行。

            【讨论】:

              【解决方案16】:

              虽然是一个老问题,但所有贡献都对我有很大帮助,所以这是我的解决方案,它适用于我自己的 DbContext 类。 $rows 参数只是一个表示行或模型的关联数组的数组:field name =&gt; insert value

              如果您使用的模式使用模型,则在将模型数据作为数组传递时非常适合,例如通过模型类中的 ToRowArray 方法。

              注意:不言而喻,但绝不允许传递参数 将此方法暴露给用户或依赖于任何用户输入,插入值除外,这些输入值已经过验证和清理。 $tableName 参数和列名应由调用逻辑定义;例如,User 模型可以映射到用户表,该表的列列表映射到模型的成员字段。

              public function InsertRange($tableName, $rows)
              {
                  // Get column list
                  $columnList = array_keys($rows[0]);
                  $numColumns = count($columnList);
                  $columnListString = implode(",", $columnList);
              
                  // Generate pdo param placeholders
                  $placeHolders = array();
              
                  foreach($rows as $row)
                  {
                      $temp = array();
              
                      for($i = 0; $i < count($row); $i++)
                          $temp[] = "?";
              
                      $placeHolders[] = "(" . implode(",", $temp) . ")";
                  }
              
                  $placeHolders = implode(",", $placeHolders);
              
                  // Construct the query
                  $sql = "insert into $tableName ($columnListString) values $placeHolders";
                  $stmt = $this->pdo->prepare($sql);
              
                  $j = 1;
                  foreach($rows as $row)
                  {
                      for($i = 0; $i < $numColumns; $i++)
                      {
                          $stmt->bindParam($j, $row[$columnList[$i]]);
                          $j++;
                      }
                  }
              
                  $stmt->execute();
              }
              

              【讨论】:

              • 摆脱一个事务,因为对单个查询使用一个事务是没有意义的。和往常一样,这段代码容易受到 SQL 注入或查询错误的影响。
              • 在这种情况下,您对事务的冗余使用是正确的,但我看不出这如何容易受到 SQL 注入的影响。它是参数化的,所以我只能假设您假设 $tableName 向用户公开,但事实并非如此,它在 DAL 中。你能扩展你的主张吗?光说是没用的。
              • 嗯,它不仅是一个表名,而且无论如何:你怎么知道它是否会被任何会使用你在此处发布的代码的人公开?跨度>
              • 因此,列出代码的每个潜在用途或每个参数来源是发帖人的责任吗?也许我对人们有更高的期望。如果我添加一个不允许用户访问$tableName 的注释,你会不会更开心?
              • 发布可靠代码是发布者的责任,如果他们的目的是帮助某人,而不仅仅是炫耀。
              【解决方案17】:

              这是我的简单方法。

                  $values = array();
                  foreach($workouts_id as $value){
                    $_value = "(".$value.",".$plan_id.")";
                    array_push($values,$_value);
                  }
                  $values_ = implode(",",$values);
              
                  $sql = "INSERT INTO plan_days(id,name) VALUES" . $values_."";
                  $stmt = $this->conn->prepare($sql);
                  $stmt->execute();
              

              【讨论】:

              • 您正在破坏使用准备好的语句的意义。操作人员担心问题On the readings on PDO, the use prepared statements should give me a better security than static queries. 中的安全性
              • 只是对您未验证的$workouts_id 进行成像,而$values 可能具有相当出乎意料的数据。您不能保证现在可能不会,但将来另一个开发人员会使这些数据不安全。所以我认为让 PDO 准备的查询更正确。
              【解决方案18】:

              我遇到了同样的问题,这就是我为自己完成的方法,我为自己制作了一个函数(如果对你有帮助,你可以使用它)。

              例子:

              INSERT INTO 国家(国家、城市)VALUES(德国、柏林)、(法国、巴黎);

              $arr1 = Array("Germany", "Berlin");
              $arr2 = Array("France", "France");
              
              insertMultipleData("countries", Array($arr1, $arr2));
              
              
              // Inserting multiple data to the Database.
              public function insertMultipleData($table, $multi_params){
                  try{
                      $db = $this->connect();
              
                      $beforeParams = "";
                      $paramsStr = "";
                      $valuesStr = "";
              
                      for ($i=0; $i < count($multi_params); $i++) { 
              
                          foreach ($multi_params[$i] as $j => $value) {                   
              
                              if ($i == 0) {
                                  $beforeParams .=  " " . $j . ",";
                              }
              
                              $paramsStr .= " :"  . $j . "_" . $i .",";                                       
                          }
              
                          $paramsStr = substr_replace($paramsStr, "", -1);
                          $valuesStr .=  "(" . $paramsStr . "),"; 
                          $paramsStr = "";
                      }
              
              
                      $beforeParams = substr_replace($beforeParams, "", -1);
                      $valuesStr = substr_replace($valuesStr, "", -1);
              
              
                      $sql = "INSERT INTO " . $table . " (" . $beforeParams . ") VALUES " . $valuesStr . ";";
              
                      $stmt = $db->prepare($sql);
              
              
                      for ($i=0; $i < count($multi_params); $i++) { 
                          foreach ($multi_params[$i] as $j => &$value) {
                              $stmt->bindParam(":" . $j . "_" . $i, $value);                                      
                          }
                      }
              
                      $this->close($db);
                      $stmt->execute();                       
              
                      return true;
              
                  }catch(PDOException $e){            
                      return false;
                  }
              
                  return false;
              }
              
              // Making connection to the Database 
                  public function connect(){
                      $host = Constants::DB_HOST;
                      $dbname = Constants::DB_NAME;
                      $user = Constants::DB_USER;
                      $pass = Constants::DB_PASS;
              
                      $mysql_connect_str = 'mysql:host='. $host . ';dbname=' .$dbname;
              
                      $dbConnection = new PDO($mysql_connect_str, $user, $pass);
                      $dbConnection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
              
                      return $dbConnection;
                  }
              
                  // Closing the connection
                  public function close($db){
                      $db = null;
                  }
              

              如果 insertMultipleData($table, $multi_params) 返回 TRUE,则您的数据已插入数据库。

              【讨论】:

              • 语法无效..
              【解决方案19】:

              根据我的实验,我发现在单个事务中具有多个值行的 mysql insert 语句是最快的。

              但是,如果数据太多,那么 mysql 的 max_allowed_packet 设置可能会限制具有多个值行的单个事务插入。因此,当数据大于 mysql 的max_allowed_packet 大小时,以下函数将失败:

              1. singleTransactionInsertWithRollback
              2. singleTransactionInsertWithPlaceholders
              3. singleTransactionInsert

              插入大数据场景中最成功的一种是transactionSpeed方法,但比上述方法更耗时。因此,要处理这个问题,您可以将数据拆分成更小的块并多次调用单个事务插入,或者使用transactionSpeed 方法放弃执行速度。

              这是我的研究

              <?php
              
              class SpeedTestClass
              {
                  private $data;
              
                  private $pdo;
              
                  public function __construct()
                  {
                      $this->data = [];
                      $this->pdo = new \PDO('mysql:dbname=test_data', 'admin', 'admin');
                      if (!$this->pdo) {
                          die('Failed to connect to database');
                      }
                  }
              
                  public function createData()
                  {
                      $prefix = 'test';
                      $postfix = 'unicourt.com';
                      $salutations = ['Mr.', 'Ms.', 'Dr.', 'Mrs.'];
              
                      $csv[] = ['Salutation', 'First Name', 'Last Name', 'Email Address'];
                      for ($i = 0; $i < 100000; ++$i) {
                          $csv[] = [
                              $salutations[$i % \count($salutations)],
                              $prefix.$i,
                              $prefix.$i,
                              $prefix.$i.'@'.$postfix,
                          ];
                      }
              
                      $this->data = $csv;
                  }
              
                  public function truncateTable()
                  {
                      $this->pdo->query('TRUNCATE TABLE `name`');
                  }
              
                  public function transactionSpeed()
                  {
                      $timer1 = microtime(true);
                      $this->pdo->beginTransaction();
                      $sql = 'INSERT INTO `name` (`first_name`, `last_name`) VALUES (:first_name, :last_name)';
                      $sth = $this->pdo->prepare($sql);
              
                      foreach (\array_slice($this->data, 1) as $values) {
                          $sth->execute([
                              ':first_name' => $values[1],
                              ':last_name' => $values[2],
                          ]);
                      }
              
                      // $timer2 = microtime(true);
                      // echo 'Prepare Time: '.($timer2 - $timer1).PHP_EOL;
                      // $timer3 = microtime(true);
              
                      if (!$this->pdo->commit()) {
                          echo "Commit failed\n";
                      }
                      $timer4 = microtime(true);
                      // echo 'Commit Time: '.($timer4 - $timer3).PHP_EOL;
              
                      return $timer4 - $timer1;
                  }
              
                  public function autoCommitSpeed()
                  {
                      $timer1 = microtime(true);
                      $sql = 'INSERT INTO `name` (`first_name`, `last_name`) VALUES (:first_name, :last_name)';
                      $sth = $this->pdo->prepare($sql);
                      foreach (\array_slice($this->data, 1) as $values) {
                          $sth->execute([
                              ':first_name' => $values[1],
                              ':last_name' => $values[2],
                          ]);
                      }
                      $timer2 = microtime(true);
              
                      return $timer2 - $timer1;
                  }
              
                  public function noBindAutoCommitSpeed()
                  {
                      $timer1 = microtime(true);
              
                      foreach (\array_slice($this->data, 1) as $values) {
                          $sth = $this->pdo->prepare("INSERT INTO `name` (`first_name`, `last_name`) VALUES ('{$values[1]}', '{$values[2]}')");
                          $sth->execute();
                      }
                      $timer2 = microtime(true);
              
                      return $timer2 - $timer1;
                  }
              
                  public function singleTransactionInsert()
                  {
                      $timer1 = microtime(true);
                      foreach (\array_slice($this->data, 1) as $values) {
                          $arr[] = "('{$values[1]}', '{$values[2]}')";
                      }
                      $sth = $this->pdo->prepare('INSERT INTO `name` (`first_name`, `last_name`) VALUES '.implode(', ', $arr));
                      $sth->execute();
                      $timer2 = microtime(true);
              
                      return $timer2 - $timer1;
                  }
              
                  public function singleTransactionInsertWithPlaceholders()
                  {
                      $placeholders = [];
                      $timer1 = microtime(true);
                      $sql = 'INSERT INTO `name` (`first_name`, `last_name`) VALUES ';
                      foreach (\array_slice($this->data, 1) as $values) {
                          $placeholders[] = '(?, ?)';
                          $arr[] = $values[1];
                          $arr[] = $values[2];
                      }
                      $sql .= implode(', ', $placeholders);
                      $sth = $this->pdo->prepare($sql);
                      $sth->execute($arr);
                      $timer2 = microtime(true);
              
                      return $timer2 - $timer1;
                  }
              
                  public function singleTransactionInsertWithRollback()
                  {
                      $placeholders = [];
                      $timer1 = microtime(true);
                      $sql = 'INSERT INTO `name` (`first_name`, `last_name`) VALUES ';
                      foreach (\array_slice($this->data, 1) as $values) {
                          $placeholders[] = '(?, ?)';
                          $arr[] = $values[1];
                          $arr[] = $values[2];
                      }
                      $sql .= implode(', ', $placeholders);
                      $this->pdo->beginTransaction();
                      $sth = $this->pdo->prepare($sql);
                      $sth->execute($arr);
                      $this->pdo->commit();
                      $timer2 = microtime(true);
              
                      return $timer2 - $timer1;
                  }
              }
              
              $s = new SpeedTestClass();
              $s->createData();
              $s->truncateTable();
              echo "Time Spent for singleTransactionInsertWithRollback: {$s->singleTransactionInsertWithRollback()}".PHP_EOL;
              $s->truncateTable();
              echo "Time Spent for single Transaction Insert: {$s->singleTransactionInsert()}".PHP_EOL;
              $s->truncateTable();
              echo "Time Spent for single Transaction Insert With Placeholders: {$s->singleTransactionInsertWithPlaceholders()}".PHP_EOL;
              $s->truncateTable();
              echo "Time Spent for transaction: {$s->transactionSpeed()}".PHP_EOL;
              $s->truncateTable();
              echo "Time Spent for AutoCommit: {$s->noBindAutoCommitSpeed()}".PHP_EOL;
              $s->truncateTable();
              echo "Time Spent for autocommit with bind: {$s->autoCommitSpeed()}".PHP_EOL;
              $s->truncateTable();
              

              仅包含两列的表的 100,000 个条目的结果如下

              $ php data.php
              Time Spent for singleTransactionInsertWithRollback: 0.75147604942322
              Time Spent for single Transaction Insert: 0.67445182800293
              Time Spent for single Transaction Insert With Placeholders: 0.71131205558777
              Time Spent for transaction: 8.0056409835815
              Time Spent for AutoCommit: 35.4979159832
              Time Spent for autocommit with bind: 33.303519010544
              

              【讨论】:

                【解决方案20】:

                这是解决此问题的另一个(精简)解决方案:

                首先你需要用count()计算源数组(这里是$aData)的数据。然后使用 array_fill() 并生成一个新数组,其中包含与源数组一样多的条目,每个条目的值都是“(?,?)”(占位符的数量取决于您使用的字段;这里:2)。然后需要对生成的数组进行内爆,并使用逗号作为胶水。 在 foreach 循环中,您需要生成另一个关于您使用的占位符数量的索引(占位符数量 * 当前数组索引 + 1)。您需要在每个绑定值之后将生成的索引加 1。

                $do = $db->prepare("INSERT INTO table (id, name) VALUES ".implode(',', array_fill(0, count($aData), '(?,?)')));
                
                foreach($aData as $iIndex => $aValues){
                 $iRealIndex = 2 * $iIndex + 1;
                 $do->bindValue($iRealIndex, $aValues['id'], PDO::PARAM_INT);
                 $iRealIndex = $iRealIndex + 1;
                 $do->bindValue($iRealIndex, $aValues['name'], PDO::PARAM_STR);
                }
                
                $do->execute();
                

                【讨论】:

                  【解决方案21】:

                  这样的事情怎么样:

                          if(count($types_of_values)>0){
                           $uid = 1;
                           $x = 0;
                           $sql = "";
                           $values = array();
                            foreach($types_of_values as $k=>$v){
                              $sql .= "(:id_$k,:kind_of_val_$k), ";
                              $values[":id_$k"] = $uid;
                              $values[":kind_of_val_$k"] = $v;
                            }
                           $sql = substr($sql,0,-2);
                           $query = "INSERT INTO table (id,value_type) VALUES $sql";
                           $res = $this->db->prepare($query);
                           $res->execute($values);            
                          }
                  

                  这背后的想法是循环遍历数组值,为准备好的语句占位符在每个循环中添加“id 编号”,同时将值添加到数组中以获取绑定参数。如果您不喜欢使用数组中的“键”索引,可以在循环中添加 $i=0 和 $i++。这两种方法都适用于这个例子,即使你有带有命名键的关联数组,只要键是唯一的,它仍然可以工作。只需一点点工作,嵌套数组也可以..

                  **注意 substr 去掉 $sql 变量的最后一个空格和逗号,如果没有空格,则需要将其更改为 -1 而不是 -2。

                  【讨论】:

                    猜你喜欢
                    • 2019-10-04
                    • 2012-04-21
                    • 2013-11-06
                    • 2012-09-16
                    • 2015-08-24
                    相关资源
                    最近更新 更多