Sedia Disiapkan PDO beberapa baris dalam permintaan tunggal

146

Saat ini saya menggunakan tipe SQL ini di MySQL untuk memasukkan beberapa baris nilai dalam satu permintaan tunggal:

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

Pada bacaan di PDO, pernyataan penggunaan yang disiapkan harus memberi saya keamanan yang lebih baik daripada pertanyaan statis.

Karena itu saya ingin tahu apakah mungkin untuk menghasilkan "memasukkan beberapa baris nilai dengan menggunakan satu permintaan" menggunakan pernyataan yang disiapkan.

Jika ya, boleh saya tahu bagaimana saya bisa menerapkannya?

hoball
sumber
hati-hati dengan banyak jawaban untuk $stmt->execute($data); php.net/manual/en/... Pada dasarnya semua params disahkan divalidasi sebagai string. Hanya perulangan data setelah membangun kueri, dan secara manual bindValueatau bindParammengirimkan tipe sebagai argumen ketiga.
MrMesees

Jawaban:

151

Beberapa Nilai Masukkan dengan Laporan Disiapkan PDO

Memasukkan banyak nilai dalam satu pernyataan eksekusi. Mengapa karena menurut halaman ini lebih cepat dari sisipan biasa.

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

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

lebih banyak nilai data atau Anda mungkin memiliki loop yang mengisi data.

Dengan sisipan yang sudah disiapkan, Anda perlu mengetahui bidang yang Anda sisipkan, dan jumlah bidang yang akan dibuat? placeholder untuk mengikat parameter Anda.

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

Pada dasarnya itulah yang kita inginkan seperti pernyataan insert.

Sekarang, kodenya:

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);
try {
    $stmt->execute($insert_values);
} catch (PDOException $e){
    echo $e->getMessage();
}
$pdo->commit();

Meskipun dalam pengujian saya, hanya ada perbedaan 1 detik saat menggunakan beberapa sisipan dan sisipan reguler yang disiapkan dengan nilai tunggal.

Herbert Balagtas
sumber
4
Kesalahan ketik, dalam penjelasan di atas, ia menyebutkan $ datafields meskipun $ datafield digunakan dalam $ sql. Dengan demikian copy paste akan menghasilkan kesalahan. Mohon lakukan perbaikan. Terima kasih atas solusi ini.
pal4life
1
Digunakan ini untuk sementara waktu kemudian perhatikan bahwa nilai-nilai dengan tanda kutip tunggal di dalamnya tidak lolos dengan benar. Menggunakan tanda kutip ganda pada ledakan berfungsi seperti pesona bagi saya: $ a [] = '("'. Implode (", ", $ question_marks). '", NOW ())';
qwertzman
1
array_merge tampaknya lebih mahal daripada hanya menggunakan array_push.
K2xL
14
Ketika Anda mengatakan "hanya ada perbedaan 1 detik", berapa banyak baris data yang Anda sisipkan? 1 detik cukup signifikan tergantung pada konteksnya.
Kevin Dice
3
Optimalisasi: Tidak ada gunanya menelepon placeholders()berulang kali. Sebut satu kali sebelum loop dengan sizeof($datafields)dan tambahkan string hasil ke $question_marks[]dalam loop.
AVIDeveloper
71

Jawaban yang sama seperti Tuan Balagtas, sedikit lebih jelas ...

Versi terbaru MySQL dan PHP PDO melakukan dukungan multi-baris INSERTpernyataan.

Ikhtisar SQL

SQL akan terlihat seperti ini, dengan asumsi tabel 3-kolom yang Anda inginkan INSERT.

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

ON DUPLICATE KEY UPDATEberfungsi seperti yang diharapkan bahkan dengan INSERT multi-baris; tambahkan ini:

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

Tinjauan PHP

Kode PHP Anda akan mengikuti panggilan biasa $pdo->prepare($qry)dan $stmt->execute($params)PDO.

$paramsakan menjadi array 1 dimensi dari semua nilai untuk diteruskan ke INSERT.

Pada contoh di atas, harus mengandung 9 elemen; PDO akan menggunakan setiap set 3 sebagai satu baris nilai. (Memasukkan 3 baris 3 kolom masing-masing = 9 elemen array.)

Penerapan

Kode di bawah ini ditulis untuk kejelasan, bukan efisiensi. Bekerja dengan array_*()fungsi PHP untuk cara yang lebih baik untuk memetakan atau menelusuri data Anda jika Anda mau. Apakah Anda dapat menggunakan transaksi jelas tergantung pada jenis tabel MySQL Anda.

Asumsi:

  • $tblName - nama string tabel untuk INSERT ke
  • $colNames- array 1-dimensi dari nama kolom dari tabel. Nama-nama kolom ini harus merupakan pengidentifikasi kolom MySQL yang valid; melarikan diri mereka dengan backticks (``) jika tidak
  • $dataVals - array mutli-dimensional, di mana setiap elemen adalah array 1-d dari deretan nilai untuk INSERT

Kode sampel

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

try {
   $stmt->execute($dataToInsert);
} catch (PDOException $e){
   echo $e->getMessage();
}

$pdo->commit();
jamesvl
sumber
6
Itu benar-benar terlalu buruk bahwa PDO menanganinya seperti ini, ada beberapa cara yang sangat elegan untuk melakukan ini pada driver DB lainnya.
Jonathon
Ini membuat placeholder menjadi lebih singkat, sehingga $rowPlacestidak perlu lagi:$allPlaces = implode(',', array_fill(0, count($dataVals), '('.str_pad('', (count($colNames)*2)-1, '?,').')'));
Phil
Bekerja dengan sempurna. Saya akan menambahkan jawaban ini kebutuhan untuk memastikan keunikan (kombinasi) indeks dalam tabel. Seperti di ALTER TABLE votesADD UNIK unique_index( user, email, address);
Giuseppe
1
Luar biasa! BTW, menggunakan array_push($dataToInsert, ...array_values($dataVals));akan jauh lebih cepatforeach ($dataVals as $row => $data) {}
Anis
39

Untuk apa nilainya, saya telah melihat banyak pengguna merekomendasikan iterasi melalui pernyataan INSERT alih-alih membangun sebagai permintaan string tunggal seperti jawaban yang dipilih lakukan. Saya memutuskan untuk menjalankan tes sederhana hanya dengan dua bidang dan pernyataan sisipan yang sangat dasar:

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

Sementara kueri keseluruhan itu sendiri mengambil milidetik atau kurang, kueri yang terakhir (string tunggal) secara konsisten 8 kali lebih cepat atau lebih. Jika ini dibangun untuk mengatakan mencerminkan impor ribuan baris pada lebih banyak kolom, perbedaannya bisa sangat besar.

JM4
sumber
@ JM4 - ide bagus untuk meletakkan 10 baris langsung dalam satu eksekusi . Tetapi bagaimana saya bisa memasukkan ribuan baris ketika mereka disimpan dalam objek seperti JSON? Kode saya di bawah ini berfungsi dengan baik. Tetapi bagaimana saya bisa menyesuaikannya untuk memasukkan 10 baris dalam satu eksekusi? `foreach ($ json_content sebagai $ datarow) {$ id = $ datarow [id]; $ date = $ datarow [date]; $ row3 = $ datarow [row3]; $ row4 = $ datarow [row4]; $ row5 = $ datarow [row5]; $ row6 = $ datarow [row6]; $ row7 = $ datarow [row7]; // sekarang jalankan $ databaseinsert-> execute (); } // akhir masa depan `
Peter
@ JM4 - ... dan pertanyaan kedua saya adalah: "mengapa tidak ada bind_parampernyataan dalam rutin impor kedua"?
Peter
Tidakkah Anda harus mengulang dua kali? Anda juga harus menghasilkan secara dinamis (?,?), bukan?
NoobishPro
@NoobishPro Ya, Anda bisa menggunakan sama untuk / foreach untuk menghasilkan keduanya.
Chazy Chaz
34

Jawaban yang Diterima oleh Herbert Balagtas bekerja dengan baik ketika array $ data kecil. Dengan array $ data yang lebih besar, fungsi array_merge menjadi sangat lambat. File pengujian saya untuk membuat array data $ memiliki 28 cols dan sekitar 80.000 baris. Script terakhir mengambil 41 untuk menyelesaikan.

Menggunakan array_push () untuk membuat $ insert_values ​​daripada array_merge () menghasilkan kecepatan 100X dengan waktu eksekusi 0,41 detik .

Array_merge yang bermasalah ():

$insert_values = array();

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

Untuk menghilangkan perlunya array_merge (), Anda dapat membangun dua array berikut:

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

Array ini kemudian dapat digunakan sebagai berikut:

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);
try {
    $stmt->execute($insert_values);
} catch (PDOException $e){
    echo $e->getMessage();
}
$pdo->commit();
Chris M.
sumber
4
Dalam PHP 5.6 yang dapat Anda lakukan array_push($data, ...array_values($row))bukan $data = array_merge($data, array_values($row));. Lebih cepat.
mpen
Kenapa 5.6? Dokumentasi tidak mengatakan apa-apa tentang 5.6, array_push()tersedia bahkan di php 4.
ZurabWeb
1
@Piero itu hanya kode PHP 5.6+ bukan karena penggunaan array_push(), tetapi karena @Mark menggunakan argumen unpacking. Perhatikan ...array_values()panggilan di sana?
mariano.iglesias
@ mariano.iglesias array_values()juga tersedia di php 4. Tidak yakin apakah itu yang Anda maksud argument unpacking.
ZurabWeb
2
@Piero, Argument unpacking adalah fitur yang diperkenalkan di PHP 5.6. Ini adalah cara untuk memberikan beberapa argumen sebagai array. Lihat di sini - php.net/manual/en/…
Anis
14

Dua kemungkinan pendekatan:

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

Atau:

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

Jika data untuk semua baris dalam satu array, saya akan menggunakan solusi kedua.

Zyx
sumber
10
dalam yang terakhir bukankah Anda kemudian membuat beberapa (mungkin ribuan) panggilan eksekusi terpisah alih-alih digabungkan menjadi satu pernyataan?
JM4
@ JM4, apakah Anda menyarankan $stmt->execute();harus berada di luar foreach loop?
bafromca
@ bafromca - Ya saya. Lihat jawaban saya di atas dengan upvotes. Pada pernyataan insert murni tidak ada alasan saya bisa secara logis memunculkan bahwa itu tidak bisa menjadi pernyataan tunggal. Satu panggilan, satu eksekusi. Bahkan, jawaban saya dari awal 2012 dapat ditingkatkan lebih jauh - sesuatu yang akan saya lakukan nanti ketika saya memiliki lebih banyak waktu. Jika Anda mulai melempar Sisipkan / perbarui / hapus kombinasi, itu adalah cerita yang berbeda.
JM4
12

Itu sama sekali bukan cara Anda menggunakan pernyataan yang disiapkan.

Sangatlah oke untuk menyisipkan satu baris per kueri karena Anda dapat menjalankan satu pernyataan yang dipersiapkan beberapa kali dengan parameter yang berbeda. Sebenarnya itu adalah salah satu keuntungan terbesar karena memungkinkan Anda memasukkan banyak baris dengan efisien, aman, dan nyaman.

Jadi mungkin untuk mengimplementasikan skema yang Anda usulkan, setidaknya untuk jumlah baris tetap, tetapi hampir dijamin bahwa ini bukan yang Anda inginkan.

sebasgo
sumber
1
Bisakah Anda menyarankan cara yang lebih baik untuk memasukkan banyak baris ke dalam tabel?
Crashthatch
@ Crashthatch: Lakukan saja dengan cara naif: Atur pernyataan yang telah disiapkan sekali, lalu jalankan untuk setiap baris dengan nilai yang berbeda untuk parameter yang terikat. Itu pendekatan kedua dalam jawaban Zyk.
sebasgo
2
Tujuan yang Anda sebutkan untuk pernyataan yang disiapkan adalah benar. Tetapi, menggunakan multi-insert adalah teknik lain untuk meningkatkan kecepatan memasukkan dan dapat digunakan dengan pernyataan yang sudah disiapkan. Dalam pengalaman saya, saat memigrasikan 30 juta data baris menggunakan pernyataan yang disiapkan PDO, saya melihat multi-insert 7-10 kali lebih cepat daripada mengelompokkan insert tunggal dalam transaksi.
Anis
1
Sepenuhnya setuju dengan Anis. Saya memiliki 100 ribu baris dan mendapatkan peningkatan kecepatan besar dengan sisipan baris muli.
Kenneth
Mengklaim bahwa memanggil basis data relasional dalam satu lingkaran sekali per baris pada umumnya adalah hal yang baik adalah sesuatu yang tidak dapat saya setujui. Downvote untuk itu. Memang, kadang tidak apa-apa. Saya tidak percaya absolut dengan teknik. Tapi ini adalah anti-pola yang harus digunakan hanya dalam kasus-kasus tertentu.
Brandon
8

Jawaban yang lebih pendek: ratakan array data yang dipesan oleh kolom lalu

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

Saat menyisipkan 1.000 atau lebih catatan, Anda tidak ingin harus mengulang setiap catatan untuk memasukkannya ketika semua yang Anda butuhkan adalah hitungan nilai.

fyrye
sumber
5

Inilah pendekatan sederhana saya.

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

sumber
6
Anda mengalahkan titik menggunakan pernyataan yang disiapkan. op khawatir tentang keamanan dalam pertanyaanOn the readings on PDO, the use prepared statements should give me a better security than static queries.
YesItsMe
2
Hanya pencitraan yang belum Anda validasi $workouts_id, yang dapat dilakukan $valuedengan data yang tidak terduga. Anda tidak dapat menjamin bahwa mungkin tidak sekarang tetapi di masa depan pengembang lain membuat data ini tidak aman. Jadi saya pikir lebih tepat membuat kueri yang disiapkan oleh PDO.
Nikita_kharkov_ua
3

Inilah kelas yang saya tulis lakukan beberapa sisipan dengan opsi pembersihan:

<?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;
    }
}
Pierre Dumuid
sumber
Halo Pierre. Mungkin Anda tidak aktif lagi di sini. Namun demikian, saya hanya ingin menunjukkan bahwa ide saya untuk masalah ini terlihat hampir sama dengan Anda. Kebetulan murni, karena saya kira tidak ada yang lebih dari ini. Saya menambahkan kelas untuk DELETE- AND UPDATE-Operations, juga dan melibatkan beberapa ide dari sini, sesudahnya. Aku hanya tidak melihat kelasmu. Maafkan promosi diri saya yang tak tahu malu di sini, tapi saya kira itu akan membantu seseorang. Semoga ini tidak bertentangan dengan SO-Rules. Temukan di sini .
JackLeEmmerdeur
1

Beginilah cara saya melakukannya:

Pertama-tama tentukan nama kolom yang akan Anda gunakan, atau biarkan kosong dan pdo akan menganggap Anda ingin menggunakan semua kolom pada tabel - dalam hal ini Anda harus menginformasikan nilai-nilai baris dalam urutan yang tepat yang muncul di atas tabel .

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

Sekarang, misalkan Anda memiliki array dua dimensi yang sudah disiapkan. Iterasi, dan buat string dengan nilai baris Anda, seperti:

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

Sekarang, yang baru saja Anda lakukan adalah memeriksa apakah $ rows sudah didefinisikan, dan jika tidak, buat dan simpan nilai-nilai baris dan sintaks SQL yang diperlukan sehingga itu akan menjadi pernyataan yang valid. Perhatikan bahwa string harus masuk ke dalam tanda kutip ganda dan tanda kutip tunggal, sehingga mereka akan segera dikenali seperti itu.

Yang perlu dilakukan adalah menyiapkan pernyataan dan mengeksekusi, dengan demikian:

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

Diuji dengan 2000 baris sejauh ini, dan waktu pelaksanaannya suram. Akan menjalankan beberapa tes lagi dan akan kembali ke sini kalau-kalau ada sesuatu yang lebih lanjut untuk saya sumbangkan.

Salam.

Tho T. Carranza
sumber
1

Karena belum disarankan, saya cukup yakin LOAD DATA INFILE masih merupakan cara tercepat untuk memuat data karena menonaktifkan pengindeksan, memasukkan semua data, dan kemudian mengaktifkan kembali indeks - semua dalam satu permintaan.

Menyimpan data sebagai csv harus cukup sepele mengingat fputcsv. MyISAM adalah yang tercepat, tetapi Anda masih mendapatkan kinerja besar di InnoDB. Ada kelemahan lain, jadi saya akan memilih rute ini jika Anda memasukkan banyak data, dan tidak repot dengan di bawah 100 baris.

avatarofhope2
sumber
1

Meskipun pertanyaan lama, semua kontribusi sangat membantu saya, jadi inilah solusi saya, yang bekerja di dalam DbContextkelas saya sendiri . The $rowsparameter hanyalah sebuah array dari array asosiatif mewakili baris atau model: field name => insert value.

Jika Anda menggunakan pola yang menggunakan model ini cocok dengan baik ketika melewati data model sebagai array, katakanlah dari ToRowArraymetode dalam kelas model.

Catatan : Itu harus pergi tanpa mengatakan tetapi tidak pernah membiarkan argumen yang diteruskan ke metode ini untuk diekspos kepada pengguna atau bergantung pada input pengguna apa pun, selain dari nilai sisipan, yang telah divalidasi dan disanitasi. The $tableNameargumen dan nama kolom harus didefinisikan oleh logika panggilan; misalnya Usermodel dapat dipetakan ke tabel pengguna, yang daftar kolomnya dipetakan ke bidang anggota model.

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
sumber
singkirkan transaksi, karena tidak masuk akal untuk menggunakan satu untuk satu permintaan. dan seperti biasa, kode ini rentan terhadap injeksi SQL atau kesalahan kueri.
Akal Sehat Anda
Anda benar tentang penggunaan berlebihan transaksi untuk kasus ini, tapi saya tidak melihat bagaimana ini rentan terhadap injeksi SQL. Itu diparameterisasi jadi saya hanya bisa berasumsi Anda berasumsi $tableNameterkena pengguna, yang bukan, itu di DAL. Bisakah Anda memperluas klaim Anda? Tidak membantu hanya mengatakan sesuatu.
Lee
baik, ini bukan hanya nama tabel tetapi bagaimana pun: bagaimana Anda bisa tahu apakah itu akan diekspos atau tidak oleh siapa saja yang akan menggunakan kode yang Anda posting di sini?
Akal Sehat Anda
Jadi itu adalah tanggung jawab poster untuk menguraikan setiap potensi penggunaan kode atau setiap sumber untuk argumen? Mungkin saya memiliki harapan orang yang lebih tinggi. Apakah ini akan membuat Anda lebih bahagia jika saya menambahkan catatan untuk tidak mengizinkan pengguna memiliki akses $tableName?
Lee
Poster bertanggung jawab untuk mengeposkan kode yang dapat diandalkan, jika maksudnya adalah untuk membantu seseorang, bukan hanya untuk pamer.
Akal Sehat Anda
1

Berikut ini adalah solusi lain (ramping) untuk masalah ini:

Pada awalnya Anda perlu menghitung data dari array sumber (di sini: $ aData) dengan count (). Kemudian Anda menggunakan array_fill () dan menghasilkan array baru yang memiliki banyak entri seperti yang dimiliki array sumber, masing-masing dengan nilai "(?,?)" (Jumlah placeholder tergantung pada bidang yang Anda gunakan; di sini: 2). Maka array yang dihasilkan perlu di-implode dan sebagai lem digunakan koma. Dalam loop foreach, Anda perlu membuat indeks lain mengenai jumlah placeholder yang Anda gunakan (jumlah placeholder * indeks array saat ini + 1). Anda perlu menambahkan 1 ke indeks yang dihasilkan setelah setiap nilai yang diikat.

$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
sumber
0

Anda bisa menyisipkan banyak baris dalam satu permintaan dengan fungsi ini:

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 adalah array array nilai. Dalam kasus Anda, Anda akan memanggil fungsi dengan

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

Ini memiliki manfaat yang Anda gunakan pernyataan yang disiapkan , sambil menyisipkan beberapa baris dengan satu permintaan. Keamanan!

Chris Michaelides
sumber
0

Ini solusi saya: https://github.com/sasha-ch/Aura.Sql berdasarkan pustaka auraphp / Aura.Sql.

Contoh penggunaan:

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

Laporan bug dipersilakan.

sasha-ch
sumber
Pada 2.4 Anda dapat membuat multi sisipan dengan github.com/auraphp/Aura.SqlQuery/tree/… dan memanfaatkan ExtendedPdo untuk mengeksekusi :).
Hari KT
0

Contoh dunia nyata saya untuk memasukkan semua kode pos Jerman ke tabel kosong (untuk menambahkan nama kota nanti):

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

Seperti yang Anda lihat, ini sepenuhnya fleksibel. Anda tidak perlu memeriksa jumlah kolom atau memeriksa posisi kolom Anda. Anda hanya perlu mengatur data yang dimasukkan:

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

Saya bangga dengan beberapa konstruktor string kueri karena mereka bekerja tanpa fungsi array yang berat seperti array_merge. Terutama vsprintf () adalah temuan yang bagus.

Akhirnya saya perlu menambahkan 2x while () untuk menghindari melebihi batas memori. Ini tergantung pada batas memori Anda, tetapi ini adalah solusi umum yang baik untuk menghindari masalah (dan memiliki 10 permintaan masih jauh lebih baik dari 10.000).

mgutt
sumber
0

test.php

<?php
require_once('Database.php');

$obj = new Database();
$table = "test";

$rows = array(
    array(
    'name' => 'balasubramani',
    'status' => 1
    ),
    array(
    'name' => 'balakumar',
    'status' => 1
    ),
    array(
    'name' => 'mani',
    'status' => 1
    )
);

var_dump($obj->insertMultiple($table,$rows));
?>

Database.php

<?php
class Database 
{

    /* Initializing Database Information */

    var $host = 'localhost';
    var $user = 'root';
    var $pass = '';
    var $database = "database";
    var $dbh;

    /* Connecting Datbase */

    public function __construct(){
        try {
            $this->dbh = new PDO('mysql:host='.$this->host.';dbname='.$this->database.'', $this->user, $this->pass);
            //print "Connected Successfully";
        } 
        catch (PDOException $e) {
            print "Error!: " . $e->getMessage() . "<br/>";
            die();
        }
    }
/* Insert Multiple Rows in a table */

    public function insertMultiple($table,$rows){

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

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

        $stmt = $this->dbh->prepare ($sql);
        try {
            $stmt->execute($insert_values);
        } catch (PDOException $e){
            echo $e->getMessage();
        }
        return $this->dbh->commit();
    }

    /*  placeholders for prepared statements like (?,?,?)  */

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

        return implode($separator, $result);
    }

}
?>
sonofkrish
sumber
Selamat datang di stackoverflow. Bukan hanya kode, tolong posting apa masalah Anda dan jelaskan.
Prakash Palnati
pada dasarnya itu hanya implementasi dari kode yang disediakan dalam jawaban yang diterima
Your Common Sense
0

Saya memiliki masalah yang sama dan ini adalah bagaimana saya menyelesaikan untuk diri saya sendiri, dan saya membuat fungsi untuk diri saya sendiri untuk itu (dan Anda dapat menggunakannya jika itu membantu Anda).

Contoh:

Masukkan ke negara (negara, kota) NILAI (Jerman, Berlin), (Perancis, 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;
    }

Jika insertMultipleData ($ table, $ multi_params) mengembalikan TRUE , data Anda telah dimasukkan ke database Anda.

Dardan
sumber
0

Berdasarkan percobaan saya, saya menemukan bahwa pernyataan insert mysql dengan beberapa baris nilai dalam satu transaksi adalah yang tercepat.

Namun, jika data terlalu banyak maka max_allowed_packetpengaturan mysql mungkin membatasi insert transaksi tunggal dengan beberapa baris nilai. Karenanya, fungsi berikut akan gagal saat ada data yang lebih besar dari max_allowed_packetukuran mysql :

  1. singleTransactionInsertWithRollback
  2. singleTransactionInsertWithPlaceholders
  3. singleTransactionInsert

Yang paling berhasil dalam menyisipkan skenario data besar adalah transactionSpeedmetode, tetapi menghabiskan waktu lebih banyak metode yang disebutkan di atas. Jadi, untuk menangani masalah ini, Anda dapat membagi data menjadi potongan yang lebih kecil dan memanggil satu transaksi tunggal beberapa kali atau memberikan kecepatan eksekusi dengan menggunakan transactionSpeedmetode.

Ini penelitian saya

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

Hasil untuk 100.000 entri untuk tabel yang hanya berisi dua kolom adalah sebagai berikut

$ 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
sumber
0

Ini berhasil untuk saya

$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();
Andre Da Silva Poppi
sumber
0

Bagaimana dengan sesuatu yang seperti ini:

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

Gagasan di balik ini adalah untuk menggilir nilai array Anda, menambahkan "nomor id" ke setiap loop untuk tempat penampung pernyataan yang disiapkan Anda sementara pada saat yang sama, Anda menambahkan nilai ke array Anda untuk parameter yang mengikat. Jika Anda tidak suka menggunakan indeks "kunci" dari array, Anda bisa menambahkan $ i = 0, dan $ i ++ di dalam loop. Entah berfungsi dalam contoh ini, bahkan jika Anda memiliki array asosiatif dengan kunci bernama, itu tetap berfungsi asalkan kunci tersebut unik. Dengan sedikit kerja akan baik-baik saja untuk array bersarang juga ..

** Perhatikan bahwa substr menghapus variabel terakhir ruang $ dan sql, dan koma, jika Anda tidak memiliki ruang Anda perlu mengubahnya ke -1 daripada -2.

dekan williams
sumber
-1

Sebagian besar solusi yang diberikan di sini untuk membuat kueri yang disiapkan lebih kompleks dari yang seharusnya. Menggunakan fungsi bawaan PHP Anda dapat dengan mudah membuat pernyataan SQL tanpa overhead yang signifikan.

Diberikan $records, array catatan di mana setiap catatan itu sendiri merupakan array yang diindeks (dalam bentuk field => value), fungsi berikut akan menyisipkan catatan ke dalam tabel yang diberikan $table, pada koneksi PDO $connection, hanya menggunakan pernyataan tunggal yang disiapkan. Perhatikan bahwa ini adalah solusi PHP 5.6+ karena penggunaan argumen membongkar panggilan untuk 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);
}
mariano.iglesias
sumber
1
Kode ini tidak boleh digunakan karena rentan terhadap injeksi SQL
Your Common Sense