180 votes

PDO Prepared Insère plusieurs lignes dans une seule requête

J'utilise actuellement ce type de SQL sur MySQL pour insérer plusieurs lignes de valeurs en une seule requête :

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

D'après les lectures sur PDO, l'utilisation d'instructions préparées devrait me donner une meilleure sécurité que les requêtes statiques.

Je voudrais donc savoir s'il est possible de générer "l'insertion de plusieurs lignes de valeurs par l'utilisation d'une seule requête" en utilisant des instructions préparées.

Si oui, puis-je savoir comment le mettre en œuvre ?

0 votes

Prudent avec beaucoup de réponses pour $stmt->execute($data); php.net/manual/en/ En fait, tous les paramètres sont validés en tant que chaînes de caractères. Il suffit de boucler sur les données après avoir construit la requête, et manuellement bindValue ou bindParam en passant le type comme troisième argument.

180voto

Herbert Balagtas Points 672

Insertion de valeurs multiples avec des instructions préparées PDO

Insertion de plusieurs valeurs dans une instruction d'exécution. Pourquoi, car selon cette page il est plus rapide que les inserts ordinaires.

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

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

plus de valeurs de données ou vous avez probablement une boucle qui alimente les données.

Avec les insertions préparées, vous devez connaître les champs dans lesquels vous effectuez l'insertion, ainsi que le nombre de champs pour créer les caractères génériques ? afin de lier vos paramètres.

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

C'est en gros ce à quoi nous voulons que l'instruction d'insertion ressemble.

Maintenant, le 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();

Bien que dans mon test, il n'y avait qu'une différence d'une seconde entre l'utilisation d'encarts multiples et d'encarts ordinaires préparés avec une seule valeur.

0 votes

Je suis d'accord et j'ai appuyé votre suggestion avec quelques données de test ci-dessous pour référence.

4 votes

Une coquille, dans l'explication ci-dessus il est fait mention de $datafields alors que $datafield est utilisé dans $sql. Un copier-coller entraînerait donc une erreur. Veuillez rectifier. Merci quand même pour cette solution.

1 votes

Je l'ai utilisé pendant un certain temps, puis j'ai remarqué que les valeurs contenant des guillemets simples n'étaient pas échappées correctement. L'utilisation de guillemets doubles sur l'implosion fonctionne comme un charme pour moi : $a[] = '("" . implode(",", $question_marks) . '", NOW())' ;

86voto

jamesvl Points 411

Même réponse que M. Balagtas, légèrement plus claire...

Versions récentes de MySQL et PHP PDO faire supporter plusieurs rangées INSERT déclarations.

Aperçu de SQL

Le SQL ressemblera à quelque chose comme ceci, en supposant un tableau à 3 colonnes que vous souhaitez INSERT à.

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

ON DUPLICATE KEY UPDATE fonctionne comme prévu, même avec un INSERT à plusieurs rangs ; ajoutez ceci :

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

Présentation de PHP

Votre code PHP suivra le schéma habituel $pdo->prepare($qry) et $stmt->execute($params) Appels PDO.

$params sera un tableau unidimensionnel de tous les valeurs à transmettre à la INSERT .

Dans l'exemple ci-dessus, il devrait contenir 9 éléments ; PDO utilisera chaque ensemble de 3 comme une seule ligne de valeurs. (Insertion de 3 rangées de 3 colonnes chacune = tableau de 9 éléments).

Mise en œuvre

Le code ci-dessous est écrit pour la clarté, pas pour l'efficacité. Travaillez avec le PHP array_*() des fonctions pour de meilleures façons de cartographier ou de parcourir vos données si vous le souhaitez. La possibilité d'utiliser des transactions dépend évidemment du type de votre table MySQL.

Assumant :

  • $tblName - le nom en chaîne de la table à INSÉRER
  • $colNames - Tableau unidimensionnel des noms de colonnes de la table. Ces noms de colonne doivent être des identifiants de colonne MySQL valides ; échappez-les avec des barres obliques inversées (``) s'ils ne le sont pas.
  • $dataVals - tableau mutli-dimensionnel, où chaque élément est un tableau 1-d d'une rangée de valeurs à INSÉRER

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

10 votes

C'est vraiment dommage que PDO le gère de cette façon, il y a des façons très élégantes de le faire dans d'autres pilotes de BD.

0 votes

Cela met en place les placeholders encore plus rapidement, ce qui fait que $rowPlaces n'est plus nécessaire : $allPlaces = implode(',', array_fill(0, count($dataVals), '('.str_pad('', (count($colNames)*2)-1, '?,').')'));

0 votes

Il fonctionne parfaitement. J'ajouterais à cette réponse la nécessité d'assurer l'unicité des (combinaison d') index dans la table. Comme dans ALTER TABLE votes AJOUTER UNIQUE unique_index ( user , email , address ) ;

47voto

JM4 Points 2443

Pour ce que cela vaut, j'ai vu beaucoup d'utilisateurs recommander d'itérer à travers des déclarations INSERT au lieu de construire une seule requête de chaîne comme la réponse sélectionnée l'a fait. J'ai décidé d'effectuer un test simple avec seulement deux champs et une instruction d'insertion très basique :

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

Alors que la requête globale elle-même prenait quelques millisecondes ou moins, la dernière requête (chaîne unique) était systématiquement 8 fois plus rapide ou plus. Si cette requête était développée pour refléter une importation de milliers de lignes sur beaucoup plus de colonnes, la différence pourrait être énorme.

0 votes

@JM4 - bonne idée de mettre 10 rangs directement dans le système une exécution . Mais comment puis-je insérer des milliers de lignes quand elles sont stockées dans un objet comme JSON ? Mon code ci-dessous fonctionne parfaitement. Mais comment puis-je l'ajuster pour insérer 10 lignes en une seule exécution ? ` 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] ; // maintenant exécutez $databaseinsert->execute() ; } // fin du foreach `

0 votes

@JM4 - ... et ma deuxième question est : " pourquoi n'y a-t-il pas de bind_param dans la deuxième routine d'importation" ?

0 votes

Ne devriez-vous pas faire deux boucles ? Vous devriez également générer dynamiquement le (?,?) n'est-ce pas ?

41voto

Chris M. Points 93

La réponse acceptée par Herbert Balagtas fonctionne bien lorsque le tableau $data est petit. Avec des tableaux de $data plus grands, la fonction array_merge devient prohibitivement lente. Mon fichier de test pour créer le tableau $data a 28 cols et fait environ 80 000 lignes. Le script final a pris 41s à compléter.

Utilisation de array_push() pour créer $insert_values à la place de array_merge() a donné lieu à une Vitesse multipliée par 100 avec un temps d'exécution de 0.41s .

La problématique array_merge() :

$insert_values = array();

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

Pour éliminer le besoin de array_merge(), vous pouvez construire les deux tableaux suivants à la place :

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

Ces tableaux peuvent ensuite être utilisés comme suit :

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

4 votes

En PHP 5.6, vous pouvez faire array_push($data, ...array_values($row)) au lieu de $data = array_merge($data, array_values($row)); . Beaucoup plus rapide.

0 votes

Pourquoi 5.6 ? La documentation ne dit rien sur la 5.6, array_push() est disponible même en php 4.

1 votes

@Piero c'est du code PHP 5.6+ seulement pas à cause de l'utilisation de array_push() mais parce que @Mark utilise le déballage d'arguments. Remarquez le ...array_values() appeler là-bas ?

11voto

sebasgo Points 2784

Ce n'est tout simplement pas la façon dont vous utilisez les déclarations préparées.

Il est parfaitement acceptable d'insérer une ligne par requête car vous pouvez exécuter une instruction préparée plusieurs fois avec des paramètres différents. En fait, c'est l'un de ses plus grands avantages, car il vous permet d'insérer un grand nombre de lignes de manière efficace, sûre et confortable.

Il est donc peut-être possible de mettre en œuvre le schéma que vous proposez, au moins pour un nombre fixe de lignes, mais il est presque garanti que ce n'est pas vraiment ce que vous voulez.

1 votes

Pouvez-vous suggérer une meilleure façon d'insérer plusieurs lignes dans un tableau ?

0 votes

@Crashthatch : Il suffit de le faire de la manière naïve : Configurez l'instruction préparée une fois, puis exécutez-la pour chaque ligne avec différentes valeurs pour les paramètres liés. C'est la deuxième approche de la réponse de Zyk.

2 votes

Le but que vous avez mentionné pour la déclaration préparée est juste. Mais l'utilisation de l'insertion multiple est une autre technique pour améliorer la vitesse d'insertion et elle peut également être utilisée avec l'instruction préparée. D'après mon expérience, lors de la migration de 30 millions de lignes de données à l'aide d'une instruction préparée PDO, j'ai constaté que l'insertion multiple était 7 à 10 fois plus rapide que l'insertion unique groupée dans les transactions.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X