161

I am currently using this type of SQL on MySQL to insert multiple rows of values in one single query:

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

On the readings on PDO, the use prepared statements should give me a better security than static queries.

I would therefore like to know whether it is possible to generate "inserting multiple rows of values by the use of one query" using prepared statements.

If yes, may I know how can I implement it?

hoball
  • 2,956
  • 4
  • 20
  • 15
  • careful with a lot of the answers for `$stmt->execute($data);` http://php.net/manual/en/pdostatement.execute.php#refsect1-pdostatement.execute-parameters Basically all of the params are passed validated as strings. Just loop through the data after building the query, and manually `bindValue` or `bindParam` passing type as third-argument. – MrMesees Aug 02 '17 at 18:10

21 Answers21

160

Multiple Values Insert with PDO Prepared Statements

Inserting multiple values in one execute statement. Why because according to this page it is faster than regular inserts.

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

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

more data values or you probably have a loop that populates data.

With prepared inserts you need to know the fields you're inserting to, and the number of fields to create the ? placeholders to bind your parameters.

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

That is basically how we want the insert statement to look like.

Now, the code:

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();

Although in my test, there was only a 1 sec difference when using multiple inserts and regular prepared inserts with single value.

Dharman
  • 21,838
  • 18
  • 57
  • 107
Herbert Balagtas
  • 1,767
  • 1
  • 11
  • 6
  • I agree and have supported your suggestion with some test data below for reference. – JM4 Jan 31 '12 at 23:29
  • 4
    A typo, in the explanation above it mentions $datafields although $datafield is used in $sql. Thus copy paste would result in error. Please do rectify. Thanks for this solution though. – pal4life Feb 14 '12 at 00:48
  • 1
    Used this for a while then noticed that values with single quotes in them aren't escaped properly. Using double quotes on implosion works like a charm for me: $a[] = '("' . implode(",", $question_marks) . '", NOW())'; – qwertzman Sep 24 '12 at 11:53
  • 1
    array_merge seems more expensive than just using a array_push. – K2xL Oct 17 '13 at 18:22
  • If any one likes : there is also a library https://github.com/auraphp/Aura.SqlQuery/tree/2.4.0#multiple-row-bulk-insert – Hari K T Mar 28 '15 at 07:21
  • 16
    When you say "there was only a 1 sec difference", how many rows a data were you inserting? 1 sec is pretty significant depending on the context. – Kevin Dice Jun 02 '15 at 23:03
  • The difference might be more noticeable if you are running the query against a remote database (where each individual insert would need to be sent to the remote db). – Phil LaNasa Mar 03 '16 at 13:07
  • 3
    Optimization: No point in calling `placeholders()` over and over again. Call it once before the loop with `sizeof($datafields)` and append the result string to `$question_marks[]` inside the loop. – AVIDeveloper Jun 28 '16 at 07:44
  • This is not the best way. Because none of the rows get inserted if one of the rows have any issues such as duplicate entry or whatever. – Trondro Mulligan Aug 18 '18 at 22:36
74

Same answer as Mr. Balagtas, slightly clearer...

Recent versions MySQL and PHP PDO do support multi-row INSERT statements.

SQL Overview

The SQL will look something like this, assuming a 3-column table you'd like to INSERT to.

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

ON DUPLICATE KEY UPDATE works as expected even with a multi-row INSERT; append this:

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

PHP Overview

Your PHP code will follow the usual $pdo->prepare($qry) and $stmt->execute($params) PDO calls.

$params will be a 1-dimensional array of all the values to pass to the INSERT.

In the above example, it should contain 9 elements; PDO will use every set of 3 as a single row of values. (Inserting 3 rows of 3 columns each = 9 element array.)

Implementation

Below code is written for clarity, not efficiency. Work with the PHP array_*() functions for better ways to map or walk through your data if you'd like. Whether you can use transactions obviously depends on your MySQL table type.

Assuming:

  • $tblName - the string name of the table to INSERT to
  • $colNames - 1-dimensional array of the column names of the table These column names must be valid MySQL column identifiers; escape them with backticks (``) if they are not
  • $dataVals - mutli-dimensional array, where each element is a 1-d array of a row of values to INSERT

Sample Code

// 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();
Dharman
  • 21,838
  • 18
  • 57
  • 107
jamesvl
  • 1,509
  • 11
  • 7
  • 8
    That is really too bad that PDO handles it this way, there are some very elegant ways to do this in other DB drivers. – Jonathon May 06 '13 at 02:00
  • This setups the placeholders even more tersely, making `$rowPlaces` no longer necessary: `$allPlaces = implode(',', array_fill(0, count($dataVals), '('.str_pad('', (count($colNames)*2)-1, '?,').')'));` – Phil Jun 19 '14 at 04:03
  • Works perfect. I would add to this answer the need to ensure the uniqueness of the (combination of) indexes in the table. Like in ALTER TABLE `votes` ADD UNIQUE `unique_index`(`user`, `email`, `address`); – Giuseppe Apr 02 '17 at 15:22
  • 1
    Awesome! BTW, using `array_push($dataToInsert, ...array_values($dataVals));` will be much faster then `foreach ($dataVals as $row => $data) {}` – Anis Aug 21 '17 at 09:29
42

For what it is worth, I have seen a lot of users recommend iterating through INSERT statements instead of building out as a single string query as the selected answer did. I decided to run a simple test with just two fields and a very basic insert statement:

<?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>";
?>

While the overall query itself took milliseconds or less, the latter (single string) query was consistently 8 times faster or more. If this was built out to say reflect an import of thousands of rows on many more columns, the difference could be enormous.

JM4
  • 6,356
  • 18
  • 74
  • 121
  • @JM4 - great idea to put 10 rows directly in *one execution*. But how can I insert thousands of rows when they are stored in an object like JSON? My code below works perferctly. But how can I adjust it to insert 10 rows in one execution? ` foreach($json_content as $datarow) { $id = $datarow[id]; $date = $datarow[date]; $row3 = $datarow[row3]; $row4 = $datarow[row4]; $row5 = $datarow[row5]; $row6 = $datarow[row6]; $row7= $datarow[row7]; // now execute $databaseinsert->execute(); } // end of foreach ` – Peter Sep 14 '17 at 15:57
  • @JM4 - ... and my second question is: "why is there no `bind_param` statement in the second import routine"? – Peter Sep 14 '17 at 17:39
  • Wouldn't you have to loop twice? You would also have to dynamically generate the `(?,?)`, right? – NoobishPro Feb 04 '18 at 02:26
  • @NoobishPro Yes, you can use the same for/foreach to generate both. – Chazy Chaz Jan 25 '19 at 23:49
34

The Accepted Answer by Herbert Balagtas works well when the $data array is small. With larger $data arrays the array_merge function becomes prohibitively slow. My test file to create the $data array has 28 cols and is about 80,000 lines. The final script took 41s to complete.

Using array_push() to create $insert_values instead of array_merge() resulted in a 100X speed up with execution time of 0.41s.

The problematic array_merge():

$insert_values = array();

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

To eliminate the need for array_merge(), you can build the following two arrays instead:

//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 ); 

These arrays can then be used as follows:

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();
Dharman
  • 21,838
  • 18
  • 57
  • 107
Chris M.
  • 443
  • 4
  • 4
  • 4
    In PHP 5.6 you can do `array_push($data, ...array_values($row))` instead of `$data = array_merge($data, array_values($row));`. Much faster. – mpen Feb 05 '15 at 18:08
  • Why 5.6 ? Documentation doesn't say anything about 5.6, `array_push()` is available even in php 4. – ZurabWeb Sep 20 '15 at 20:02
  • 1
    @Piero it is PHP 5.6+ only code not because of the use of `array_push()`, but because @Mark is using argument unpacking. Notice the `...array_values()` call there? – mariano.iglesias Sep 30 '15 at 13:16
  • @mariano.iglesias `array_values()` is as well available in php 4. Not sure if that's what you mean by `argument unpacking`. – ZurabWeb Sep 30 '15 at 21:14
  • i wish to plus 1 you more than one. great explanation – Bsienn Dec 11 '15 at 13:16
  • 2
    @Piero, Argument unpacking is a feature introduces in PHP 5.6. It's a way to provide multiple arguments as an array. Check here - http://php.net/manual/en/migration56.new-features.php#migration56.new-features.splat – Anis Aug 21 '17 at 05:41
15

Two possible approaches:

$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();

Or:

$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();
}

If the data for all the rows are in a single array, I would use the second solution.

Zyx
  • 445
  • 3
  • 8
  • 10
    in the latter aren't you then making several (possibly thousands) of separate execute calls instead of combining into one statement? – JM4 Jan 31 '12 at 20:24
  • @JM4, are you suggesting `$stmt->execute();` should be outside the foreach loop? – bafromca Aug 08 '13 at 16:13
  • @bafromca - Yes I am. See my answer above with upvotes. On a pure insert statement there is *no* reason i can logically come up with that it can't be a single statement. One call, one execute. In fact, my answer from early 2012 could be improved even further - something I will do later on when I have some more time. If you start throwing in Insert / update/ delete combinations, that is a different story. – JM4 Aug 08 '13 at 20:13
12

That's simply not the way you use prepared statements.

It is perfectly okay to insert one row per query because you can execute one prepared statement multiple times with different parameters. In fact that is one of the greatest advantages as it allows you to insert you a great number of rows in an efficient, secure and comfortable manner.

So it maybe possible to implement the scheme you proposing, at least for a fixed number of rows, but it is almost guaranteed that this is not really what you want.

sebasgo
  • 3,775
  • 20
  • 28
  • 1
    Can you suggest a better way to insert multiple rows into a table? – Crashthatch Jul 04 '14 at 15:51
  • @Crashthatch: Just do it the naive way: Setup the prepared statement once, then execute it for each row with different values for the bound parameters. That's the second approach in Zyk's answer. – sebasgo Jul 07 '14 at 07:53
  • 2
    The purpose you mentioned for prepared statement is right. But, using multi -insert is another technique to improve insert speed and it can be used with prepared statement too. In my experience, while migrating 30 million row data using PDO prepared statement, I saw multi-insert was 7-10 times faster then grouped single insert in transactions. – Anis Aug 21 '17 at 05:50
  • 1
    Absolutely agree with Anis. I have 100k rows and get a huge speed increase with muli row inserts. – Kenneth Mar 11 '18 at 09:09
  • Claiming that calling a relational database in a loop once per row is generally a good thing is something I cannot agree with. Downvote for that. Granted, sometimes it's ok. I don't believe in absolutes with engineering. But this is an anti-pattern that should be used only in select cases. – Brandon Nov 22 '18 at 03:32
8

A shorter answer: flatten the array of data ordered by columns then

//$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";

When inserting a 1,000 or so records you don't want to have to loop through every record to insert them when all you need is a count of the values.

fyrye
  • 259
  • 3
  • 3
5

Here is my simple approach.

    $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();
  • 7
    you're defeating the point of using prepared statements. the op is concerned about security in the question `On the readings on PDO, the use prepared statements should give me a better security than static queries.` – YesItsMe Sep 07 '17 at 06:08
  • 2
    Just imaging that you have non validated `$workouts_id`, which can have `$value`s with quite inexpected data. You can't guarantee that maybe not now but in future another developer make this data insecure. So I think quite more right make the query prepared by PDO. – Nikita_kharkov_ua May 31 '18 at 20:54
3

Here's a class I wrote do multiple inserts with purge option:

<?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;
    }
}
  • Hello Pierre. Maybe you're not active around here anymore. Nevertheless, I just wanted to point out that my idea for this issue looks nearly identical to yours. Pure coincidence, as I guess there isn't much more to this. I added classes for DELETE- AND UPDATE-Operations, too and involved some ideas from here, afterwards. I just didn't see your class. Please excuse my shameless self promotion here, but I guess it will be of help for someone. Hope this isn't against SO-Rules. Find it [here](https://github.com/JackLeEmmerdeur/PDOBatch). – JackLeEmmerdeur Aug 02 '17 at 14:55
2

Based on my experiments I found out that mysql insert statement with multiple value rows in single transaction is the fastest one.

However, if the data is too much then mysql's max_allowed_packet setting might restrict the single transaction insert with multiple value rows. Hence, following functions will fail when there is data greater than mysql's max_allowed_packet size:

  1. singleTransactionInsertWithRollback
  2. singleTransactionInsertWithPlaceholders
  3. singleTransactionInsert

The most successful one in insert huge data scenario is transactionSpeed method, but it consumes time more the above mentioned methods. So, to handle this problem you can either split your data into smaller chunks and call single transaction insert multiple times or give up speed of execution by using transactionSpeed method.

Here's my research

<?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();

The results for 100,000 entries for a table containing only two columns is as below

$ 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
theBuzzyCoder
  • 2,016
  • 1
  • 23
  • 25
1

This is how I did it:

First define the column names you'll use, or leave it blank and pdo will assume you want to use all the columns on the table - in which case you'll need to inform the row values in the exact order they appear on the table.

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

Now, suppose you have a two dimensional array already prepared. Iterate it, and construct a string with your row values, as such:

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

Now, what you just did was check if $rows was already defined, and if not, create it and store row values and the necessary SQL syntax so it will be a valid statement. Note that strings should go inside double quotes and single quotes, so they will be promptly recognized as such.

All it's left to do is prepare the statement and execute, as such:

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

Tested with up to 2000 rows so far, and the execution time is dismal. Will run some more tests and will get back here in case I have something further to contribute.

Regards.

Théo T. Carranza
  • 5,058
  • 1
  • 16
  • 14
1

Since it has not been suggested yet, I'm pretty sure LOAD DATA INFILE is still the fastest way to load data as it disables indexing, inserts all data, and then re-enables the indexes - all in a single request.

Saving the data as a csv should be fairly trivial keeping in mind fputcsv. MyISAM is fastest, but you still get big performance in InnoDB. There are other disadvantages, though so I would go this route if you are inserting a lot of data, and not bother with under 100 rows.

avatarofhope2
  • 158
  • 10
1

Although an old question all the contributions helped me a lot so here's my solution, which works within my own DbContext class. The $rows parameter is simply an array of associative arrays representing rows or models: field name => insert value.

If you use a pattern that uses models this fits in nicely when passed model data as an array, say from a ToRowArray method within the model class.

Note: It should go without saying but never allow the arguments passed to this method to be exposed to the user or reliant on any user input, other than the insert values, which have been validated and sanitised. The $tableName argument and the column names should be defined by the calling logic; for instance a User model could be mapped to the user table, which has its column list mapped to the model's member fields.

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();
}
Lee
  • 3,646
  • 8
  • 42
  • 61
  • get rid of a transaction, as it makes no sense to use one for a single query. and as usual, this code is vulnerable to SQL injection or query error. – Your Common Sense Feb 19 '17 at 10:57
  • You're right about the redundant use of transactions for this case, but I don't see how this is vulnerable to SQL injection. It's parameterised so I can only presume you're assuming `$tableName` is exposed to the user, which it's not, it's in the DAL. Can you expand on your claims? It's not helpful to just say things. – Lee Feb 19 '17 at 12:08
  • well, it's not only a table name but anyway: how can you know whether it will be exposed or not by **anyone who would use the code you posted here?** – Your Common Sense Feb 19 '17 at 12:12
  • So it's a poster's responsibility to outline every potential use of the code or every source for arguments? Maybe I have higher expectations of people. Would it make you happier if I added a note not to allow the user to have access to `$tableName`? – Lee Feb 19 '17 at 12:19
  • It's a poster's responsibility to post a reliable code, if their intent is to help someone, not just to show off. – Your Common Sense Feb 19 '17 at 12:32
  • Not to mention that column names are much more likely to be exposed. And to be a cause for a mere syntax error even being a part of DAL. – Your Common Sense Feb 19 '17 at 12:38
  • Who says the column names are more likely to be exposed? What's your reasoning? It seems like you're the one who's trying to show off and not help. In my particular setup the column names are taken from the model member fields. I'll take your advice on board and make sure it's clear but you need to back up your statements if you're going to be helpful. – Lee Feb 19 '17 at 12:53
  • Name one of your "model member fields" as "group" and see what happens. – Your Common Sense Feb 19 '17 at 13:00
  • The answer was to address the specific question the OP asked. You're being pedantic considering this consideration is outside the scope of the question. – Lee Feb 19 '17 at 13:03
  • If you didn't notice, the answer to address the specific question the OP asked has been posted almost a decade ago. – Your Common Sense Feb 19 '17 at 13:06
  • Yes, if you'd read my answer you would have noticed I'd mentioned that in the first line. Are you having a bad day? You seem to have really taken offence to my post. – Lee Feb 19 '17 at 15:27
1

Here is another (slim) solution for this issue:

At first you need to count the data of the source array (here: $aData) with count(). Then you use array_fill() and generate a new array wich as many entries as the source array has, each with the value "(?,?)" (the number of placeholders depends on the fields you use; here: 2). Then the generated array needs to be imploded and as glue a comma is used. Within the foreach loop, you need to generate another index regarding on the number of placeholders you use (number of placeholders * current array index + 1). You need to add 1 to the generated index after each binded value.

$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();
Bernhard
  • 1,745
  • 9
  • 18
0

You can insert multiple rows in a single query with this function:

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 is an array of arrays of values. In your case you would call the function with

insertMultiple("INSERT INTO tbl (`key1`,`key2`)",array(array('r1v1','r1v2'),array('r2v1','r2v2')));

This has the benefit that you use prepared statements, while inserting multiple rows with a single query. Security!

0

This worked for me

$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();
ROOT
  • 10,339
  • 5
  • 24
  • 40
0

Here is my solution: https://github.com/sasha-ch/Aura.Sql based on auraphp/Aura.Sql library.

Usage example:

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

Bugreports are welcome.

sasha-ch
  • 1
  • 3
  • As of 2.4 you can create multi insert with https://github.com/auraphp/Aura.SqlQuery/tree/2.4.0#multiple-row-bulk-insert and make use of [ExtendedPdo](https://github.com/auraphp/Aura.Sql) to execute :) . – Hari K T Mar 28 '15 at 07:19
0

My real world example to insert all german postcodes into an empty table (to add town names later):

// 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);
}

As you can see its fully flexible. You don't need to check the amount of columns or check on which position your column is. You only need to set the insert data:

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

I'm proud of some of the query string constructors as they work without heavy array-functions like array_merge. Especially vsprintf() was a good find.

Finally I needed to add 2x while() to avoid exceeding the memory limit. This depends on your memory limit but at all its a good general solution to avoid problems (and having 10 queries is still much better than 10.000).

mgutt
  • 4,830
  • 2
  • 44
  • 68
0

I had the same problem and this is how i accomplish for myself, and i made a function for myself for it ( and you can use it if that helps you).

Example:

INSERT INTO countries (country, city) VALUES (Germany, Berlin), (France, Paris);

$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;
    }

If insertMultipleData($table, $multi_params) returns TRUE, your data has been inserted to your database.

Dardan
  • 477
  • 7
  • 12
0

what about something like this:

        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);            
        }

The idea behind this is to cycle through your array values, adding "id numbers" to each loop for your prepared statement placeholders while at the same time, you add the values to your array for the binding parameters. If you don't like using the "key" index from the array, you could add $i=0, and $i++ inside the loop. Either works in this example, even if you have associative arrays with named keys, it would still work providing the keys were unique. With a little work it would be fine for nested arrays too..

**Note that substr strips the $sql variables last space and comma, if you don't have a space you'd need to change this to -1 rather than -2.

-1

Most of the solutions given here to create the prepared query are more complex that they need to be. Using PHP's built in functions you can easily creare the SQL statement without significant overhead.

Given $records, an array of records where each record is itself an indexed array (in the form of field => value), the following function will insert the records into the given table $table, on a PDO connection $connection, using only a single prepared statement. Note that this is a PHP 5.6+ solution because of the use of argument unpacking in the call to 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);
}
  • 1
    This code should never be used as it is vulnerable to SQL injection – Your Common Sense Sep 30 '15 at 13:23
  • @Your I don't see what you see. Compel me to vtd this answer. – mickmackusa Jan 25 '21 at 05:09
  • @mickmackusa I am implying [this](https://phpdelusions.net/pdo/sql_injection_example) – Your Common Sense Jan 25 '21 at 06:25
  • 1
    Ah It would have been good for the poster and researchers to understand this specific concern regarding field name injections. I know you were a grumpier lad back in these earlier days, but please be sure to blast that injection page on relevant SO pages in the future, so that people learn the vulnerability. Telling folks that a query is vulnerable but not explaining how -- is unhelpfully cryptic. VingTD. – mickmackusa Jan 25 '21 at 06:35