23

I am looking to do multiple inserts using PHP PDO.

The closest answer I have found is this one

how-to-insert-an-array-into-a-single-mysql-prepared-statement

However the example thats been given uses ?? instead of real placeholders.

I have looked at the examples on the PHP doc site for place holders

php.net pdo.prepared-statements

$stmt = $dbh->prepare("INSERT INTO REGISTRY (name, value) VALUES (:name, :value)");
$stmt->bindParam(':name', $name);
$stmt->bindParam(':value', $value);

Now lets say I wanted to achieve the above but with an array

$valuesToInsert = array(
  0 => array('name' => 'Robert', 'value' => 'some value'),
  1 => array('name' -> 'Louise', 'value' => 'another value')
);

How would I go about it with PDO and multiple inserts per transaction?

I imagine it would start of with a loop?

$stmt = $dbh->prepare("INSERT INTO REGISTRY (name, value) VALUES (:name, :value)");

foreach($valuesToInsert as $insertRow){

    // now loop through each inner array to match binded values
    foreach($insertRow as $column => value){
        $stmt->bindParam(":{$column}", value);
    }
}
$stmt->execute();

However the above does not work but hopefully will demonstrate what im trying to achieve

Community
  • 1
  • 1
Robbo_UK
  • 11,351
  • 25
  • 81
  • 117
  • 2
    you need to execute inside the loop. otherwise you're just overwriting the bound parameter and end up binding only the LAST value. – Marc B Feb 25 '13 at 15:04
  • 1
    however if I execute then that will do the DB transaction one row at a time? Im trying to do it in batch – Robbo_UK Feb 25 '13 at 15:05
  • yes, but if you're trying to emulate the mysql extended `insert ... values (...), (...), (...)` insert syntax, this is not how you go about it. you'd have to pre-build a query statement that has a placeholder for every set of values you're inserting, prepare it, bind parameters, then execute. you'd end up doing just as much work as running a single prepared insert multiple times. – Marc B Feb 25 '13 at 15:07
  • 1
    I'm inserting some 40 or 50k rows at a time. I need to do it in batch. Its the DB transaction that takes up the time. – Robbo_UK Feb 25 '13 at 15:10
  • Then wrap the entire thing in a single transaction using PDO::beginTransaction() / PDO::commit() and execute queries as mentioned by @MarcB. – N.B. Feb 25 '13 at 15:13
  • 1
    JFYI: `??` are **real** placeholders and `:name` are fake ones. – Your Common Sense Feb 25 '13 at 15:14
  • how do I do PDO::beginTransaction() / PDO::commit() – Robbo_UK Feb 25 '13 at 15:23

5 Answers5

30

First of all, ? symbols are real place-holders (most drivers allow to use both syntaxes, positional and named place-holders). Secondly, prepared statements are nothing but a tool to inject raw input into SQL statements—the syntax of the SQL statement itself is unaffected. You already have all the elements you need:

  • How to insert multiple rows with a single query
  • How to generate SQL dynamically
  • How to use prepared statements with named place-holders.

It's fairly trivial to combine them all:

$sql = 'INSERT INTO table (memberID, programID) VALUES ';
$insertQuery = [];
$insertData = [];
$n = 0;
foreach ($data as $row) {
    $insertQuery[] = '(:memberID' . $n . ', :programID' . $n . ')';
    $insertData['memberID' . $n] = $memberid;
    $insertData['programID' . $n] = $row;
    $n++;
}

if (!empty($insertQuery)) {
    $sql .= implode(', ', $insertQuery);
    $stmt = $db->prepare($sql);
    $stmt->execute($insertData);
}
Álvaro González
  • 142,137
  • 41
  • 261
  • 360
  • 1
    Note that on large data sets you will hit a limit with SQL **VALUES** clause _(around 1 000 rows)_ and you might have to limit batch size. I noticed that increasing the batch size is not always performant, so test with different batch sizes. Make sure your named placeholders are not using numbers in the end other than the $n to avoid conflicts. – toni_lehtimaki Apr 05 '18 at 09:58
  • See also `array_chunk` – William Entriken Apr 08 '19 at 18:32
  • Experiment results: using `array_chunk` with 100 rows realized a 2x speed up versus normal for loop on InnoDB using transactions. Larger batches did not go any faster. – William Entriken Apr 08 '19 at 18:58
8

I'm assuming you are using InnoDB so this answer is only valid for that engine (or any other transaction-capable engine, meaning MyISAM isn't included).

By default InnoDB runs in auto-commit mode. That means each query is treated as its own contained transaction.

To translate that to something us mortals can understand, it means that every INSERT query you issue will force hard-disk to commit it by confirming it wrote down the query information. Considering how mechanical hard-disks are super slow since their input-output operation per second is low (if I'm not mistaken, the average is 300ish IO's), it means your 50 000 queries will be - well, super slow.

So what do you do? You commit all of your 50k queries in a single transaction. It might not be the best solution for various purposes but it'll be fast.

You do it like this:

$dbh->beginTransaction();

$stmt = $dbh->prepare("INSERT INTO REGISTRY (name, value) VALUES (:name, :value)");

foreach($valuesToInsert as $insertRow)
{    
    // now loop through each inner array to match bound values
    foreach($insertRow as $column => value)
    {
        $stmt->bindParam(":$column", value);
        $stmt->execute();
    }
}


$dbh->commit();
N.B.
  • 13,688
  • 3
  • 45
  • 55
  • I have about 50k rows to insert and was going to do it in batches of 25. I will try this example as it looks like it could be what im looking for. – Robbo_UK Feb 25 '13 at 15:39
  • 1
    This answer is correct but it doesn't talk about MySQL's multiple-insert syntax which is, in my experience, what can really speed up large batches. – Álvaro González Feb 26 '13 at 11:06
  • For the best performance, `LOAD DATA INFILE` is still the fastest option and constructing a file to be used with `LOAD DATA INFILE` should be fairly straightforward. I forgot to add that to the answer, and as @ÁlvaroG.Vicario mentioned - `INSERT INTO .... VALUES..` is faster than executing a prepared statement in the loop. – N.B. Feb 26 '13 at 11:38
  • 2
    The 'value' variable are missing dollar signs :) – bottleboot Dec 06 '13 at 19:14
1

A little modifications in solution provided by N.B
$stmt->execute() should be outside of inner loop because you may have one or more columns that need to bind before calling $stmt->execute() else you 'll get exception "Invalid parameter number: number of bound variables does not match number of token".
2nd "value" variable were missing dollar signs.

function batchinsert($sql,$params){
    try { 
                db->beginTransaction(); 

                $stmt = db->prepare($sql);

                foreach($params as $row)
                {    
                    // now loop through each inner array to match bound values
                    foreach($row as $column => $value)
                    {                           
                        $stmt->bindParam(":$column", $value);                           
                    }
                    $stmt->execute();
                }                                       
                db->commit();                   

        } catch(PDOExecption $e) {
            $db->rollback();                
        }
}

Test:

$sql = "INSERT INTO `test`(`name`, `value`) VALUES (:name, :value)" ;

$data = array();    

array_push($data, array('name'=>'Name1','value'=>'Value1')); 

array_push($data, array('name'=>'Name2','value'=>'Value2')); 

array_push($data, array('name'=>'Name3','value'=>'Value3')); 

array_push($data, array('name'=>'Name4','value'=>'Value4')); 

array_push($data, array('name'=>'Name5','value'=>'Value5')); 

batchinsert($sql,$data);
Mudassar
  • 1,566
  • 4
  • 21
  • 31
-1

Your code was actually ok, but had a problem in $stmt->bindParam(":$column", value); It should be $stmt->bindValue(":{$column}", $value); and it will work perfectly. This will assist others in future.

Full code:

foreach($params as $row)
{ 
    // now loop through each inner array to match bound values
    foreach($row as $column => $value)
    { 
        $stmt->bindValue(":{$column}", $value); //EDIT
    }
    // Execute statement to add to transaction
    $stmt->execute();
} 
gvlasov
  • 18,638
  • 21
  • 74
  • 110
cheruiyot
  • 41
  • 1
  • 6
-4

Move execute inside of the loop.

$stmt = $dbh->prepare("INSERT INTO REGISTRY (name, value) VALUES (:name, :value)");
foreach($valuesToInsert as $insertRow)
{
    $stmt->execute($insertRow);    
}

If you experience any problems with this such recommended way, you have to ask a question, describing these certain problems.

Your Common Sense
  • 156,878
  • 40
  • 214
  • 345
  • 1
    i dont think the above is doing it in batch looks like it does one at a time? – Robbo_UK Feb 25 '13 at 15:21
  • This isn't a batch insert, you're very clearly reusing the same SQL statement multiple times per row, but it's still doing a single insert at a time. – Tom Jun 06 '17 at 13:47