Przygotowane PDO Wstawia wiele wierszy w jednym zapytaniu

145

Obecnie używam tego typu SQL w MySQL do wstawiania wielu wierszy wartości w jednym zapytaniu:

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

Przy odczytach dotyczących PDO, przygotowane instrukcje użytkowania powinny dać mi większe bezpieczeństwo niż statyczne zapytania.

Chciałbym zatem wiedzieć, czy możliwe jest wygenerowanie „wstawiania wielu wierszy wartości jednym zapytaniem” z przygotowanych zestawień.

Jeśli tak, czy mogę wiedzieć, jak mogę to wdrożyć?

hoball
źródło
ostrożnie z wieloma odpowiedziami dotyczącymi $stmt->execute($data); php.net/manual/en/ ... W zasadzie wszystkie parametry są przekazywane jako ciągi znaków. Po prostu przejrzyj dane po utworzeniu zapytania i ręcznie bindValuelub bindParamprzekaż typ jako trzeci argument.
MrMesees

Odpowiedzi:

150

Wstawianie wielu wartości z przygotowanymi wyciągami PDO

Wstawianie wielu wartości w jednej instrukcji wykonania. Dlaczego, ponieważ według tej strony jest szybszy niż zwykłe wkładki.

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

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

więcej wartości danych lub prawdopodobnie masz pętlę, która wypełnia dane.

Dzięki przygotowanym wstawkom musisz znać pola, do których wstawiasz, oraz liczbę pól do utworzenia? symbole zastępcze do powiązania parametrów.

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

Tak w zasadzie chcemy, aby wyglądała instrukcja insert.

Teraz kod:

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

Chociaż w moim teście różnica wynosi tylko 1 sekundę, gdy używam wielu wkładek i regularnie przygotowanych wkładek z jedną wartością.

Herbert Balagtas
źródło
4
Literówka, w powyższym wyjaśnieniu wspomina o $ datafields, chociaż $ datafield jest używana w $ sql. Zatem kopiowanie wklejania spowoduje błąd. Proszę to naprawić. Dzięki za to rozwiązanie.
pal4life
1
Używaliśmy tego przez jakiś czas, a potem zauważyłem, że wartości z pojedynczymi cudzysłowami nie są poprawnie zmieniane. Użycie podwójnych cudzysłowów na implozji działa jak urok dla mnie: $ a [] = '("'. Implode (", ", $ question_marks). '", NOW ())';
qwertzman
1
array_merge wydaje się droższe niż zwykłe użycie array_push.
K2xL
14
Kiedy mówisz „różnica była tylko 1 s”, ile wierszy wstawiałeś dane? 1 sekunda jest dość znacząca w zależności od kontekstu.
Kevin Dice
3
Optymalizacja: nie ma sensu dzwonić placeholders()w kółko. Wywołaj go raz przed pętlą sizeof($datafields)zi dołącz wynikowy ciąg znaków do $question_marks[]wnętrza pętli.
AVIDeveloper
71

Taka sama odpowiedź jak pan Balagtas, nieco jaśniejsza ...

Najnowsze wersje MySQL i PHP PDO zrobić wsparcia wielorzędowe INSERToświadczenia.

Przegląd SQL

SQL będzie wyglądał mniej więcej tak, przy założeniu, że chcesz utworzyć tabelę z trzema kolumnami INSERT.

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

ON DUPLICATE KEY UPDATEdziała zgodnie z oczekiwaniami nawet z wielorzędowym INSERT; dołącz to:

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

Przegląd PHP

Twój kod PHP będzie podążał za zwykłymi wywołaniami $pdo->prepare($qry)i $stmt->execute($params)PDO.

$paramsbędzie jednowymiarową tablicą wszystkich wartości do przekazania do INSERT.

W powyższym przykładzie powinien zawierać 9 elementów; PDO użyje każdego zestawu 3 jako pojedynczego wiersza wartości. (Wstawienie 3 wierszy po 3 kolumny = tablica 9-elementowa).

Realizacja

Poniższy kod jest napisany dla przejrzystości, a nie wydajności. Pracuj z array_*()funkcjami PHP, aby uzyskać lepsze sposoby mapowania lub przeglądania danych, jeśli chcesz. To, czy możesz używać transakcji, zależy oczywiście od typu Twojej tabeli MySQL.

Zarozumiały:

  • $tblName - nazwa ciągu tabeli do WSTAWIANIA
  • $colNames- 1-wymiarowa tablica nazw kolumn tabeli Te nazwy kolumn muszą być prawidłowymi identyfikatorami kolumn MySQL; uciekaj przed nimi za pomocą grawisów (``), jeśli nie są
  • $dataVals - tablica wielowymiarowa, gdzie każdy element jest 1-wymiarową tablicą wiersza wartości do INSERT

Przykładowy kod

// 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
źródło
6
Szkoda, że ​​PDO radzi sobie z tym w ten sposób, istnieje kilka bardzo eleganckich sposobów na zrobienie tego w innych sterownikach DB.
Jonathon
To ustawia symbole zastępcze jeszcze bardziej zwięźle, dzięki czemu $rowPlacesnie są już potrzebne:$allPlaces = implode(',', array_fill(0, count($dataVals), '('.str_pad('', (count($colNames)*2)-1, '?,').')'));
Phil,
Działa idealnie. Dodałbym do tej odpowiedzi konieczność zapewnienia niepowtarzalności (kombinacji) indeksów w tabeli. Jak Alter Tabela votesdodać unikatowy unique_index( user, email, address);
Giuseppe
1
Niesamowite! BTW, używanie array_push($dataToInsert, ...array_values($dataVals));będzie wtedy znacznie szybszeforeach ($dataVals as $row => $data) {}
Anis
39

Widziałem wielu użytkowników, którzy zalecają iterację po instrukcjach INSERT zamiast budowania jako pojedynczego zapytania łańcuchowego, tak jak zrobiła to wybrana odpowiedź. Zdecydowałem się przeprowadzić prosty test z tylko dwoma polami i bardzo prostą instrukcją wstawiania:

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

Podczas gdy samo zapytanie zajmowało milisekundy lub mniej, drugie zapytanie (pojedynczy ciąg) było konsekwentnie 8 razy szybsze lub więcej. Gdyby to zostało zbudowane tak, aby odzwierciedlało import tysięcy wierszy w wielu innych kolumnach, różnica mogłaby być ogromna.

JM4
źródło
@ JM4 - świetny pomysł, aby umieścić 10 rzędów bezpośrednio w jednym wykonaniu . Ale jak mogę wstawić tysiące wierszy, gdy są one przechowywane w obiekcie takim jak JSON? Mój kod poniżej działa perfekcyjnie. Ale jak mogę to dostosować, aby wstawić 10 wierszy w jednym wykonaniu? `foreach ($ json_content as $ datarow) {$ id = $ datarow [id]; $ data = $ datarow [data]; $ row3 = $ datarow [row3]; $ wiersz4 = $ datarow [wiersz4]; $ row5 = $ datarow [row5]; $ row6 = $ datarow [rzad6]; $ row7 = $ datarow [row7]; // teraz wykonaj $ databaseinsert-> execute (); } // koniec foreach `
Peter
@ JM4 - ... a moje drugie pytanie brzmi: „dlaczego nie ma bind_paraminstrukcji w drugiej procedurze importu”?
Peter,
Czy nie musiałbyś dwukrotnie wykonać pętli? Musiałbyś także dynamicznie generować (?,?), prawda?
NoobishPro
@NoobishPro Tak, możesz użyć tego samego for / foreach do wygenerowania obu.
Chazy Chaz
34

Zaakceptowana odpowiedź Herberta Balagtasa działa dobrze, gdy tablica $ data jest mała. W przypadku większych tablic danych $ funkcja array_merge staje się zbyt wolna. Mój plik testowy do utworzenia tablicy danych $ ma 28 kolumn i około 80 000 wierszy. Ukończenie ostatecznego scenariusza zajęło 41 sekund .

Użycie funkcji array_push () do utworzenia $ insert_values ​​zamiast array_merge () dało 100-krotne przyspieszenie z czasem wykonania 0,41 s .

Problematyczny array_merge ():

$insert_values = array();

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

Aby wyeliminować potrzebę array_merge (), możesz zamiast tego zbudować następujące dwie tablice:

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

Tych tablic można następnie używać w następujący sposób:

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.
źródło
4
W PHP 5.6 możesz zrobić array_push($data, ...array_values($row))zamiast $data = array_merge($data, array_values($row));. O wiele szybciej.
mpen
Dlaczego 5.6? Dokumentacja nie mówi nic o 5.6, array_push()jest dostępna nawet w php 4.
ZurabWeb
1
@Piero jest to kod PHP 5.6+, nie ze względu na użycie array_push(), ale dlatego, że @Mark używa rozpakowywania argumentów. Zauważyłeś ...array_values()wezwanie?
mariano.iglesias
@ mariano.iglesias array_values()jest również dostępny w php 4. Nie jestem pewien, czy to masz na myśli argument unpacking.
ZurabWeb
2
@Piero, Rozpakowywanie argumentów to funkcja wprowadzona w PHP 5.6. Jest to sposób na podanie wielu argumentów jako tablicy. Sprawdź tutaj - php.net/manual/en/…
Anis
14

Dwa możliwe podejścia:

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

Lub:

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

Jeśli dane dla wszystkich wierszy znajdują się w jednej tablicy, użyłbym drugiego rozwiązania.

Zyx
źródło
10
w drugim, czy nie tworzysz wtedy kilku (prawdopodobnie tysięcy) oddzielnych wywołań wykonania zamiast łączenia ich w jedną instrukcję?
JM4
@ JM4, czy sugerujesz, że $stmt->execute();powinien znajdować się poza pętlą foreach?
bafromca
@bafromca - Tak, jestem. Zobacz moją odpowiedź powyżej z upvotes. W przypadku czystej instrukcji wstawiania nie ma powodu, dla którego mogę logicznie wymyślić, że nie może to być pojedyncza instrukcja. Jedno połączenie, jedno wykonanie. W rzeczywistości moja odpowiedź z początku 2012 r. Mogłaby być jeszcze bardziej udoskonalona - coś, co zrobię później, gdy będę miał więcej czasu. Jeśli zaczniesz rzucać kombinacjami wstawiania / aktualizowania / usuwania, to inna historia.
JM4
12

Po prostu nie jest to sposób, w jaki używasz przygotowanych wypowiedzi.

Wstawianie jednego wiersza na zapytanie jest całkowicie w porządku, ponieważ jedną przygotowaną instrukcję można wykonać wielokrotnie z różnymi parametrami. W rzeczywistości jest to jedna z największych zalet, ponieważ umożliwia wstawienie dużej liczby rzędów w wydajny, bezpieczny i wygodny sposób.

Więc być może możliwe jest zaimplementowanie proponowanego schematu, przynajmniej dla ustalonej liczby wierszy, ale jest prawie pewne, że nie jest to tak naprawdę to, czego chcesz.

sebasgo
źródło
1
Czy możesz zaproponować lepszy sposób wstawiania wielu wierszy do tabeli?
Crashthatch
@ Crashthatch: Po prostu zrób to w naiwny sposób: skonfiguruj przygotowaną instrukcję raz, a następnie wykonaj ją dla każdego wiersza z różnymi wartościami powiązanych parametrów. To drugie podejście w odpowiedzi Zyk.
sebasgo
2
Cel, o którym wspomniałeś w przygotowanym oświadczeniu, jest słuszny. Ale użycie multi -insert jest kolejną techniką poprawiania szybkości wstawiania i może być również używane z przygotowaną instrukcją. Z mojego doświadczenia wynika, że ​​podczas migracji 30 milionów wierszy danych za pomocą przygotowanego wyciągu PDO zauważyłem, że multi-insert był 7-10 razy szybszy niż zgrupowany pojedynczy insert w transakcjach.
Anis
1
Całkowicie zgadzam się z Anisem. Mam 100 tys. Rzędów i uzyskuję ogromny wzrost prędkości dzięki wstawkom z wielu rzędów.
Kenneth
Twierdzenie, że wywoływanie relacyjnej bazy danych w pętli raz na wiersz jest generalnie dobrą rzeczą, z czym nie mogę się zgodzić. Głosuj za tym. To prawda, czasami jest w porządku. Nie wierzę w absoluty w inżynierii. Ale jest to anty-wzór, którego należy używać tylko w wybranych przypadkach.
Brandon
8

Krótsza odpowiedź: spłaszcz tablicę danych uporządkowanych według kolumn

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

Podczas wstawiania 1000 lub więcej rekordów nie chcesz przechodzić przez każdy rekord, aby je wstawić, gdy potrzebujesz tylko zliczenia wartości.

fyrye
źródło
5

Oto moje proste podejście.

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

źródło
6
pokonujesz sens używania przygotowanych wypowiedzi. op obawia się o bezpieczeństwo w pytaniuOn the readings on PDO, the use prepared statements should give me a better security than static queries.
YesItsMe
2
Po prostu obrazowanie, którego nie zweryfikowałeś $workouts_id, co może mieć $valuecałkiem nieoczekiwane dane. Nie możesz zagwarantować, że może nie teraz, ale w przyszłości inny programista sprawi, że te dane będą niezabezpieczone. Myślę więc, że bardziej trafne jest wykonanie zapytania przygotowanego przez PDO.
Nikita_kharkov_ua
3

Oto klasa, którą napisałem, wykonująca wiele wkładek z opcją czyszczenia:

<?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
źródło
Cześć Pierre. Może nie jesteś już tu aktywny. Niemniej jednak chciałem tylko zaznaczyć, że mój pomysł na ten numer wygląda prawie identycznie jak Twój. Czysty zbieg okoliczności, jak sądzę, nie ma w tym nic więcej. Dodałem również klasy dla operacji DELETE- AND UPDATE-Operations i później zaangażowałem się w kilka pomysłów. Po prostu nie widziałem twojej klasy. Proszę wybaczyć moją bezwstydną autopromocję, ale myślę, że komuś to pomoże. Mam nadzieję, że nie jest to sprzeczne z regułami SO. Znajdziesz to tutaj .
JackLeEmmerdeur
1

Oto jak to zrobiłem:

Najpierw zdefiniuj nazwy kolumn, których będziesz używać, lub pozostaw je puste, a pdo przyjmie, że chcesz użyć wszystkich kolumn w tabeli - w takim przypadku musisz podać wartości wierszy w dokładnej kolejności, w jakiej pojawiają się w tabeli .

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

Teraz załóżmy, że masz już przygotowaną dwuwymiarową tablicę. Wykonaj iterację i utwórz ciąg z wartościami wierszy, takimi jak:

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

Teraz właśnie sprawdziłeś, czy $ wiersze zostały już zdefiniowane, a jeśli nie, utwórz go i zapisz wartości wierszy oraz niezbędną składnię SQL, aby była poprawną instrukcją. Zwróć uwagę, że ciągi znaków powinny znajdować się w podwójnych cudzysłowach i pojedynczych cudzysłowach, więc zostaną natychmiast rozpoznane jako takie.

Pozostaje tylko przygotować instrukcję i wykonać w następujący sposób:

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

Do tej pory testowano do 2000 wierszy, a czas wykonania jest ponury. Przeprowadzę więcej testów i wrócę tutaj, jeśli będę miał coś więcej do wniesienia.

Pozdrowienia.

Théo T. Carranza
źródło
1

Ponieważ nie zostało to jeszcze zasugerowane, jestem prawie pewien, że LOAD DATA INFILE jest nadal najszybszym sposobem ładowania danych, ponieważ wyłącza indeksowanie, wstawia wszystkie dane, a następnie ponownie włącza indeksy - wszystko w jednym żądaniu.

Zapisanie danych jako csv powinno być dość trywialne, biorąc pod uwagę fputcsv. MyISAM jest najszybszy, ale nadal masz dużą wydajność w InnoDB. Są jednak inne wady, więc wybrałbym tę trasę, jeśli wstawiasz dużo danych i nie zawracasz sobie głowy wierszami poniżej 100.

avatarofhope2
źródło
1

Chociaż stare pytanie, wszystkie składki bardzo mi pomogły, więc oto moje rozwiązanie, które działa w mojej własnej DbContextklasie. $rowsParametr jest po prostu tablicą tablic asocjacyjnych reprezentującymi wiersze lub modele: field name => insert value.

Jeśli używasz wzorca, który używa modeli, pasuje to dobrze, gdy dane modelu są przekazywane jako tablica, powiedzmy z ToRowArraymetody w klasie modelu.

Uwaga : Powinno być oczywiste, ale nigdy nie zezwalaj na ujawnianie argumentów przekazanych do tej metody użytkownikowi lub poleganie na jakichkolwiek danych wejściowych użytkownika, innych niż wartości wstawiania, które zostały sprawdzone i oczyszczone. $tableNameArgumentem i nazwy kolumn powinny być określone przez logikę wywołującego; na przykład Usermodel można odwzorować na tabelę użytkownika, której lista kolumn jest odwzorowana na pola składowe modelu.

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();
}
Zawietrzny
źródło
pozbyć się transakcji, ponieważ nie ma sensu używać jej dla pojedynczego zapytania. i jak zwykle ten kod jest podatny na wstrzyknięcie SQL lub błąd zapytania.
Twój zdrowy rozsądek
Masz rację co do nadmiarowego wykorzystania transakcji w tym przypadku, ale nie widzę, jak jest to podatne na iniekcje SQL. Jest sparametryzowany, więc mogę tylko założyć, że zakładasz, że $tableNamejest on widoczny dla użytkownika, a tak nie jest, znajduje się w DAL. Czy możesz rozwinąć swoje roszczenia? Samo mówienie rzeczy nie jest pomocne.
Lee,
cóż, to nie tylko nazwa tabeli, ale w każdym razie: skąd możesz wiedzieć, czy zostanie ona ujawniona, czy nie, przez każdego, kto użyłby kodu, który tutaj opublikowałeś?
Twój zdrowy rozsądek
Zatem odpowiedzialność za nakreślenie każdego potencjalnego wykorzystania kodu lub każdego źródła argumentów spoczywa na nadawcy? Może mam większe oczekiwania wobec ludzi. Czy byłbyś szczęśliwszy, gdybym dodał notatkę, aby użytkownik nie miał do niej dostępu $tableName?
Lee,
Odpowiedzialność za zamieszczenie wiarygodnego kodu, jeśli ma na celu pomoc komuś, a nie tylko popisywanie się, jest obowiązkiem nadawcy.
Twój zdrowy rozsądek
1

Oto inne (wąskie) rozwiązanie tego problemu:

Najpierw musisz policzyć dane tablicy źródłowej (tutaj: $ aData) za pomocą count (). Następnie używasz array_fill () i generujesz nową tablicę zawierającą tyle wpisów, ile ma tablica źródłowa, każdy z wartością „(?,?)” (Liczba symboli zastępczych zależy od używanych pól; tutaj: 2). Następnie wygenerowaną tablicę należy implodować, a jako klej należy użyć przecinka. W pętli foreach musisz wygenerować kolejny indeks dotyczący liczby używanych symboli zastępczych (liczba symboli zastępczych * bieżący indeks tablicy + 1). Musisz dodać 1 do wygenerowanego indeksu po każdej powiązanej wartości.

$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
źródło
0

Za pomocą tej funkcji możesz wstawić wiele wierszy w jednym zapytaniu:

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

$ wiersz to tablica tablic wartości. W twoim przypadku możesz wywołać funkcję z

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

Ma to tę zaletę, że używasz przygotowanych instrukcji podczas wstawiania wielu wierszy za pomocą jednego zapytania. Bezpieczeństwo!

Chris Michaelides
źródło
0

Mój przykład z prawdziwego świata, aby wstawić wszystkie niemieckie kody pocztowe do pustej tabeli (aby później dodać nazwy miast):

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

Jak widać jest w pełni elastyczny. Nie musisz sprawdzać liczby kolumn ani sprawdzać, na jakiej pozycji znajduje się Twoja kolumna. Wystarczy ustawić dane wstawiania:

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

Jestem dumny z niektórych konstruktorów ciągów zapytań, ponieważ działają one bez ciężkich funkcji tablicowych, takich jak array_merge. Zwłaszcza vsprintf () było dobrym znaleziskiem.

Wreszcie musiałem dodać 2x while (), aby uniknąć przekroczenia limitu pamięci. Zależy to od limitu pamięci, ale w ogóle jest to dobre ogólne rozwiązanie, aby uniknąć problemów (a 10 zapytań jest nadal znacznie lepsze niż 10.000).

mgutt
źródło
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
źródło
Witamy w stackoverflow. Nie tylko kod, prześlij nam swój problem i wyjaśnij.
Prakash Palnati
gruntownie. to tylko implementacja kodu podanego w zaakceptowanej odpowiedzi
Your Common Sense
0

Miałem ten sam problem i tak sobie radzę i stworzyłem dla siebie funkcję (i możesz jej użyć, jeśli ci to pomoże).

Przykład:

INSERT INTO kraje (kraj, miasto) VALUES (Niemcy, Berlin), (Francja, Paryż);

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

Jeśli insertMultipleData ($ table, $ multi_params) zwraca TRUE , Twoje dane zostały wstawione do Twojej bazy danych.

Dardan
źródło
0

Na podstawie moich eksperymentów stwierdziłem, że instrukcja mysql insert z wieloma wierszami wartości w jednej transakcji jest najszybsza.

Jeśli jednak danych jest za dużo, max_allowed_packetustawienie mysql może ograniczyć wstawianie pojedynczej transakcji z wieloma wierszami wartości. W związku z tym następujące funkcje zawiodą, gdy dane będą większe niż max_allowed_packetrozmiar mysql :

  1. singleTransactionInsertWithRollback
  2. singleTransactionInsertWithPlaceholders
  3. singleTransactionInsert

Najbardziej skutecznym scenariuszem wstawiania ogromnych danych jest transactionSpeedmetoda, jednak wyżej wymienione metody pochłaniają więcej czasu. Aby rozwiązać ten problem, możesz podzielić dane na mniejsze części i wielokrotnie wywoływać wstawianie pojedynczej transakcji lub zrezygnować z szybkości wykonywania, używająctransactionSpeed metody.

Oto moje badania

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

Wyniki dla 100 000 wpisów dla tabeli zawierającej tylko dwie kolumny są takie, jak poniżej

$ 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
źródło
0

To zadziałało dla mnie

$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
źródło
0

co z czymś takim:

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

Pomysł polega na tym, aby cyklicznie zmieniać wartości tablicy, dodając „numery identyfikacyjne” do każdej pętli dla przygotowanych symboli zastępczych instrukcji, jednocześnie dodając wartości do tablicy dla parametrów wiązania. Jeśli nie lubisz używać indeksu „klucza” z tablicy, możesz dodać $ i = 0 i $ i ++ wewnątrz pętli. Każda z nich działa w tym przykładzie, nawet jeśli masz tablice asocjacyjne z nazwanymi kluczami, nadal działałoby, pod warunkiem, że klucze byłyby unikalne. Przy odrobinie pracy byłoby dobrze również w przypadku tablic zagnieżdżonych.

** Zauważ, że substr usuwa zmienne $ sql z ostatniej spacji i przecinka, jeśli nie masz spacji, musisz zmienić to na -1 zamiast -2.

dziekan williams
źródło
-1

Większość podanych tutaj rozwiązań do stworzenia przygotowanego zapytania jest bardziej złożona niż powinna. Korzystając z wbudowanych funkcji PHP, możesz łatwo utworzyć instrukcję SQL bez znacznego narzutu.

Biorąc pod uwagę $recordstablicę rekordów, w której każdy rekord jest sam w sobie tablicą indeksowaną (w postaci field => value), poniższa funkcja wstawi rekordy do podanej tabeli $table, na połączeniu PDO $connection, używając tylko jednej przygotowanej instrukcji. Zauważ, że jest to rozwiązanie PHP 5.6+ ze względu na użycie funkcji rozpakowywania argumentów w wywołaniu 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
źródło
1
Ten kod nigdy nie powinien być używany, ponieważ jest podatny na iniekcje SQL
Your Common Sense