Подготовленный PDO Вставляет несколько строк в один запрос

В настоящее время я использую этот тип SQL в MySQL для вставки нескольких строк значений в один запрос:

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

При чтении PDO подготовленные операторы использования должны обеспечить мне лучшую безопасность, чем статические запросы.

Поэтому я хотел бы знать, возможно ли сгенерировать «вставку нескольких строк значений с использованием одного запроса» с использованием подготовленных операторов.

Если да, могу ли я узнать, как я могу это реализовать?


person hoball    schedule 24.07.2009    source источник
comment
будьте осторожны с множеством ответов для $stmt->execute($data); php.net / manual / en / В основном все параметры передаются как строки. Просто прокрутите данные после построения запроса и вручную bindValue или bindParam передайте тип в качестве третьего аргумента.   -  person MrMesees    schedule 02.08.2017


Ответы (21)


Вставка нескольких значений с помощью операторов, подготовленных PDO

Вставка нескольких значений в один оператор выполнения. Почему, потому что, согласно эта страница, она быстрее, чем обычная вставки.

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

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

больше значений данных или у вас, вероятно, есть цикл, который заполняет данные.

С подготовленными вставками вам нужно знать поля, в которые вы вставляете, и количество полей для создания? заполнители для привязки ваших параметров.

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

В основном это то, как мы хотим, чтобы оператор вставки выглядел.

Теперь код:

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

Хотя в моем тесте разница была всего в 1 секунду при использовании нескольких вставок и обычных подготовленных вставок с одним значением.

person Herbert Balagtas    schedule 20.01.2010
comment
Я согласен и поддержал ваше предложение некоторыми тестовыми данными, приведенными ниже для справки. - person JM4; 01.02.2012
comment
Опечатка, в приведенном выше объяснении упоминается $ datafields, хотя $ datafield используется в $ sql. Таким образом, копирование и вставка приведет к ошибке. Пожалуйста, исправьте. Тем не менее, спасибо за это решение. - person pal4life; 14.02.2012
comment
Некоторое время использовал это, затем заметил, что значения с одинарными кавычками в них не экранируются должным образом. Использование двойных кавычек при имплозии работает для меня как прелесть: $ a [] = '('. Implode (,, $ question_marks). ', NOW ())'; - person qwertzman; 24.09.2012
comment
array_merge кажется более дорогим, чем просто использование array_push. - person K2xL; 17.10.2013
comment
Если кому нравится: есть еще библиотека github.com/auraphp/Aura.SqlQuery/tree/ - person Hari K T; 28.03.2015
comment
Когда вы говорите, что разница составляет всего 1 секунду, сколько строк вы вставляли в данные? 1 секунда довольно значительна в зависимости от контекста. - person Kevin Dice; 03.06.2015
comment
Разница может быть более заметной, если вы выполняете запрос к удаленной базе данных (где каждая отдельная вставка должна быть отправлена ​​в удаленную базу данных). - person Phil LaNasa; 03.03.2016
comment
Оптимизация: Нет смысла звонить placeholders() снова и снова. Вызовите его один раз перед циклом с sizeof($datafields) и добавьте строку результата в $question_marks[] внутри цикла. - person AVIDeveloper; 28.06.2016
comment
Это не лучший способ. Потому что ни одна из строк не вставляется, если в одной из строк есть какие-либо проблемы, такие как повторяющаяся запись или что-то еще. - person Trondro Mulligan; 19.08.2018

Тот же ответ, что и господин Балагтас, немного яснее ...

Последние версии MySQL и PHP PDO действительно поддерживают многострочные INSERT операторы.

Обзор SQL

SQL будет выглядеть примерно так, если предположить, что вы хотите использовать таблицу с 3 столбцами INSERT.

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

ON DUPLICATE KEY UPDATE работает должным образом даже с многострочным INSERT; добавьте это:

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

Обзор PHP

Ваш PHP-код будет следовать обычным вызовам $pdo->prepare($qry) и $stmt->execute($params) PDO.

$params будет одномерным массивом всех значений, передаваемых в INSERT.

В приведенном выше примере он должен содержать 9 элементов; PDO будет использовать каждый набор из 3-х как одну строку значений. (Вставка 3 строк по 3 столбца в каждой = массив из 9 элементов.)

Выполнение

Код ниже написан для ясности, а не эффективности. При желании используйте функции PHP array_*(), чтобы улучшить отображение или просмотр ваших данных. Очевидно, можете ли вы использовать транзакции, зависит от типа вашей таблицы MySQL.

Предполагая:

  • $tblName - строковое имя таблицы, в которую ВСТАВИТЬ
  • $colNames - 1-мерный массив имен столбцов таблицы Эти имена столбцов должны быть допустимыми идентификаторами столбцов MySQL; экранируйте их обратными кавычками (``), если они не
  • $dataVals - многомерный массив, где каждый элемент представляет собой одномерный массив строки значений для INSERT

Образец кода

// 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();
person jamesvl    schedule 30.12.2010
comment
Это действительно плохо, что PDO обрабатывает это таким образом, есть несколько очень элегантных способов сделать это в других драйверах БД. - person Jonathon; 06.05.2013
comment
Это настраивает заполнители еще более кратко, делая $rowPlaces ненужным: $allPlaces = implode(',', array_fill(0, count($dataVals), '('.str_pad('', (count($colNames)*2)-1, '?,').')')); - person Phil; 19.06.2014
comment
Прекрасно работает. Я бы добавил к этому ответу необходимость обеспечения уникальности (комбинации) индексов в таблице. Как в ALTER TABLE votes ADD UNIQUE _2 _ (_ 3_, email, address); - person Giuseppe; 02.04.2017
comment
Потрясающий! Кстати, использование array_push($dataToInsert, ...array_values($dataVals)); будет намного быстрее, чем foreach ($dataVals as $row => $data) {} - person Anis; 21.08.2017

Как бы то ни было, я видел, как многие пользователи рекомендуют повторять инструкции INSERT вместо того, чтобы строить как одностроковый запрос, как это делал выбранный ответ. Я решил провести простой тест с двумя полями и очень простым оператором вставки:

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

Хотя сам запрос в целом занимал миллисекунды или меньше, последний (однострочный) запрос всегда выполнялся в 8 и более раз быстрее. Если бы это было построено так, чтобы, скажем, отразить импорт тысяч строк на гораздо большее количество столбцов, разница могла бы быть огромной.

person JM4    schedule 31.01.2012
comment
@ JM4 - отличная идея поместить 10 строк напрямую за одно выполнение. Но как мне вставить тысячи строк, если они хранятся в таком объекте, как JSON? Мой код ниже работает безупречно. Но как я могу настроить его для вставки 10 строк за одно выполнение? `foreach ($ json_content как $ datarow) {$ id = $ datarow [id]; $ date = $ datarow [дата]; $ row3 = $ datarow [row3]; $ row4 = $ datarow [row4]; $ row5 = $ datarow [row5]; $ row6 = $ datarow [row6]; $ row7 = $ datarow [row7]; // теперь выполняем $ databaseinsert- ›execute (); } // конец foreach ` - person Peter; 14.09.2017
comment
@ JM4 - ... и мой второй вопрос: почему во второй подпрограмме импорта нет оператора bind_param? - person Peter; 14.09.2017
comment
Разве вам не пришлось бы дважды повторять цикл? Вам также придется динамически генерировать (?,?), верно? - person NoobishPro; 04.02.2018
comment
@NoobishPro Да, вы можете использовать одно и то же для / foreach для генерации обоих. - person Chazy Chaz; 26.01.2019

Принятый ответ Герберта Балагтаса хорошо работает, когда массив $ data небольшой. С большими массивами $ data функция array_merge становится слишком медленной. Мой тестовый файл для создания массива $ data содержит 28 столбцов и около 80 000 строк. Завершение последнего сценария заняло 41 секунду.

Использование array_push () для создания $ insert_values ​​вместо array_merge () привело к 100-кратному ускорению при времени выполнения 0,41 с.

Проблемный array_merge ():

$insert_values = array();

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

Чтобы исключить необходимость в array_merge (), вы можете вместо этого построить следующие два массива:

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

Затем эти массивы можно использовать следующим образом:

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();
person Chris M.    schedule 19.05.2012
comment
В PHP 5.6 вы можете использовать array_push($data, ...array_values($row)) вместо $data = array_merge($data, array_values($row));. Намного быстрее. - person mpen; 05.02.2015
comment
Почему 5,6? В документации ничего не говорится о 5.6, array_push() доступен даже на php 4. - person ZurabWeb; 20.09.2015
comment
@Piero - это только код PHP 5.6+ не из-за использования array_push(), а из-за того, что @Mark использует распаковку аргументов. Заметили там ...array_values() звонок? - person mariano.iglesias; 30.09.2015
comment
@ mariano.iglesias array_values() также доступен на php 4. Не уверен, что это то, что вы имеете в виду под argument unpacking. - person ZurabWeb; 01.10.2015
comment
Я хочу добавить вам больше, чем один. отличное объяснение - person Bsienn; 11.12.2015
comment
@Piero, Распаковка аргументов - это функция, представленная в PHP 5.6. Это способ предоставить несколько аргументов в виде массива. Проверьте здесь - php.net/manual/en / - person Anis; 21.08.2017

Два возможных подхода:

$stmt = $pdo->prepare('INSERT INTO foo VALUES(:v1_1, :v1_2, :v1_3),
    (:v2_1, :v2_2, :v2_3),
    (:v2_1, :v2_2, :v2_3)');
$stmt->bindValue(':v1_1', $data[0][0]);
$stmt->bindValue(':v1_2', $data[0][1]);
$stmt->bindValue(':v1_3', $data[0][2]);
// etc...
$stmt->execute();

Or:

$stmt = $pdo->prepare('INSERT INTO foo VALUES(:a, :b, :c)');
foreach($data as $item)
{
    $stmt->bindValue(':a', $item[0]);
    $stmt->bindValue(':b', $item[1]);
    $stmt->bindValue(':c', $item[2]);
    $stmt->execute();
}

Если данные для всех строк находятся в одном массиве, я бы использовал второе решение.

person Zyx    schedule 24.07.2009
comment
в последнем случае разве вы не делаете несколько (возможно, тысячи) отдельных вызовов выполнения вместо объединения в один оператор? - person JM4; 01.02.2012
comment
@ JM4, вы предлагаете $stmt->execute(); быть вне цикла foreach? - person bafromca; 08.08.2013
comment
@bafromca - Да, я. См. Мой ответ выше с голосами "за". В чистом операторе вставки нет причины, по которой я могу логически придумать, что это не может быть один оператор. Один звонок, один выполнение. Фактически, мой ответ, полученный в начале 2012 года, можно было бы улучшить еще больше - чем я займусь позже, когда у меня будет еще немного времени. Если вы начнете использовать комбинации вставки / обновления / удаления, это совсем другая история. - person JM4; 09.08.2013

Это просто не то, как вы используете подготовленные операторы.

Вполне нормально вставлять одну строку в запрос, потому что вы можете выполнять один подготовленный оператор несколько раз с разными параметрами. Фактически, это одно из самых больших преимуществ, поскольку оно позволяет вам эффективно, безопасно и удобно вставлять большое количество строк.

Таким образом, возможно, удастся реализовать предложенную вами схему, по крайней мере, для фиксированного количества строк, но почти гарантировано, что это не совсем то, что вам нужно.

person sebasgo    schedule 24.07.2009
comment
Можете ли вы предложить лучший способ вставить несколько строк в таблицу? - person Crashthatch; 04.07.2014
comment
@Crashthatch: Просто сделайте это наивным способом: настройте подготовленный оператор один раз, а затем выполните его для каждой строки с разными значениями связанных параметров. Это второй подход в ответе Зика. - person sebasgo; 07.07.2014
comment
Указанная вами цель подготовленного заявления верна. Но использование multi -insert - это еще один метод повышения скорости вставки, который также можно использовать с подготовленным оператором. По моему опыту, при переносе 30 миллионов данных строк с использованием подготовленного оператора PDO я увидел, что множественная вставка была в 7-10 раз быстрее, чем сгруппированная одиночная вставка в транзакциях. - person Anis; 21.08.2017
comment
Абсолютно согласен с Анисом. У меня 100 тыс. Строк, и я получаю огромное увеличение скорости с помощью многорядных вставок. - person Kenneth; 11.03.2018
comment
Я не могу согласиться с утверждением, что вызов реляционной базы данных в цикле один раз для каждой строки - это вообще хорошо. Проголосуйте за это. Конечно, иногда это нормально. Я не верю в абсолют в инженерное дело. Но это антипаттерн, который следует использовать только в отдельных случаях. - person Brandon; 22.11.2018

Более короткий ответ: сгладьте массив данных, упорядоченных по столбцам, а затем

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

При вставке около 1000 записей вам не нужно перебирать каждую запись, чтобы вставить их, когда все, что вам нужно, - это количество значений.

person fyrye    schedule 09.12.2011

Вот мой простой подход.

    $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();
person Community    schedule 28.03.2017
comment
вы теряете смысл использовать заранее подготовленные операторы. Оператор озабочен безопасностью в вопросе On the readings on PDO, the use prepared statements should give me a better security than static queries. - person YesItsMe; 07.09.2017
comment
Просто представьте, что вы не проверили $workouts_id, что может иметь $value с совершенно неожиданными данными. Вы не можете гарантировать, что, возможно, не сейчас, а в будущем другой разработчик сделает эти данные незащищенными. Так что считаю правильнее сделать запрос, подготовленный PDO. - person Nikita_kharkov_ua; 31.05.2018

Вот класс, который я написал, делает несколько вставок с опцией очистки:

<?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;
    }
}
person Pierre Dumuid    schedule 28.06.2012
comment
Привет, Пьер. Может, ты здесь больше не активен. Тем не менее, я просто хотел указать, что моя идея по этому вопросу почти идентична вашей. Чистое совпадение, как я полагаю, в этом нет ничего особенного. Я также добавил классы для операций DELETE- и UPDATE-операций, а затем включил некоторые идеи отсюда. Я просто не видел твой класс. Прошу прощения за мою бессовестную саморекламу, но я думаю, это кому-то поможет. Надеюсь, это не противоречит правилам SO. Найдите его здесь. - person JackLeEmmerdeur; 02.08.2017

Основываясь на своих экспериментах, я обнаружил, что оператор mysql insert с несколькими строками значений в одной транзакции является самым быстрым.

Однако, если данных слишком много, тогда установка max_allowed_packet в mysql может ограничить вставку одной транзакции несколькими строками значений. Следовательно, следующие функции завершатся ошибкой, если есть данные, превышающие размер mysql max_allowed_packet:

  1. singleTransactionInsertWithRollback
  2. singleTransactionInsertWithPlaceholders
  3. singleTransactionInsert

Наиболее успешным в сценарии вставки огромных данных является метод transactionSpeed, но он требует больше времени, чем вышеупомянутые методы. Итак, чтобы справиться с этой проблемой, вы можете либо разделить данные на более мелкие части и вызвать вставку одной транзакции несколько раз, либо отказаться от скорости выполнения, используя метод transactionSpeed.

Вот мое исследование

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

Результаты для 100000 записей для таблицы, содержащей только два столбца, приведены ниже.

$ 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
person theBuzzyCoder    schedule 11.08.2019

Вот как я это сделал:

Сначала определите имена столбцов, которые вы будете использовать, или оставьте поле пустым, и pdo предположит, что вы хотите использовать все столбцы в таблице - в этом случае вам нужно сообщить значения строк в точном порядке, в котором они появляются в таблице. .

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

Теперь предположим, что у вас уже подготовлен двумерный массив. Итерируйте его и создайте строку со значениями вашей строки, как таковые:

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

Теперь, что вы только что сделали, это проверили, были ли уже определены $ rows, а если нет, создайте их и сохраните значения строк и необходимый синтаксис SQL, чтобы это был действительный оператор. Обратите внимание, что строки должны заключаться в двойные и одинарные кавычки, чтобы они сразу распознавались как таковые.

Все, что осталось сделать, это подготовить оператор и выполнить его как таковое:

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

Пока что тестировалось до 2000 строк, и время выполнения оставляет желать лучшего. Проведу еще несколько тестов и вернусь сюда, если у меня будет что-то еще, что можно внести.

С Уважением.

person Théo T. Carranza    schedule 17.03.2015

Поскольку это еще не было предложено, я почти уверен, что LOAD DATA INFILE по-прежнему является самым быстрым способом загрузки данных, поскольку он отключает индексацию, вставляет все данные, а затем повторно включает индексы - все в одном запросе.

Сохранение данных в формате csv должно быть довольно тривиальным, имея в виду fputcsv. MyISAM самый быстрый, но вы по-прежнему получаете большую производительность в InnoDB. Но есть и другие недостатки, поэтому я бы пошел по этому пути, если вы вставляете много данных и не беспокоитесь о менее чем 100 строках.

person avatarofhope2    schedule 26.01.2017

Хотя старый вопрос, все вклады мне очень помогли, поэтому вот мое решение, которое работает в моем собственном классе DbContext. Параметр $rows - это просто массив ассоциативных массивов, представляющих строки или модели: field name => insert value.

Если вы используете шаблон, в котором используются модели, он отлично вписывается при передаче данных модели в виде массива, например, из метода ToRowArray в классе модели.

Примечание. Само собой разумеется, но никогда не допускать, чтобы аргументы, переданные этому методу, открывались пользователю или полагались на какие-либо данные, введенные пользователем, кроме значений вставки, которые были проверены и обработаны. Аргумент $tableName и имена столбцов должны определяться логикой вызова; например, модель User может быть сопоставлена ​​с пользовательской таблицей, список столбцов которой сопоставлен с полями элементов модели.

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();
}
person Lee    schedule 19.02.2017
comment
избавиться от транзакции, поскольку нет смысла использовать ее для одного запроса. и, как обычно, этот код уязвим для SQL-инъекций или ошибок запроса. - person Your Common Sense; 19.02.2017
comment
Вы правы насчет избыточного использования транзакций в этом случае, но я не понимаю, насколько это уязвимо для SQL-инъекции. Он параметризован, поэтому я могу только предположить, что вы предполагаете, что $tableName открыт для пользователя, а это не так, он находится в DAL. Можете ли вы расширить свои претензии? Бесполезно просто говорить что-то. - person Lee; 19.02.2017
comment
ну, это не только имя таблицы, но в любом случае: как узнать, будет ли оно открыто или нет кем-либо, кто будет использовать код, который вы разместили здесь? - person Your Common Sense; 19.02.2017
comment
Значит, ответственность плаката - описывать каждое потенциальное использование кода или каждый источник аргументов? Может быть, у меня более высокие ожидания от людей. Вы бы обрадовались, если бы я добавил примечание, запрещающее пользователю иметь доступ к $tableName? - person Lee; 19.02.2017
comment
Ответственность за публикацию надежного кода лежит на плакате, если его намерение - помочь кому-то, а не просто выставить напоказ. - person Your Common Sense; 19.02.2017
comment
Не говоря уже о том, что имена столбцов могут быть раскрыты гораздо чаще. И быть причиной простой синтаксической ошибки, даже будучи частью DAL. - person Your Common Sense; 19.02.2017
comment
Кто сказал, что имена столбцов будут раскрыты с большей вероятностью? Что ты рассуждаешь? Похоже, это ты пытаешься выпендриться, а не помочь. В моей конкретной настройке имена столбцов берутся из полей элементов модели. Я учту ваш совет и позабочусь о том, чтобы он был ясным, но вам нужно подкрепить свои утверждения, если вы собираетесь быть полезными. - person Lee; 19.02.2017
comment
Назовите одно из полей элемента модели как group и посмотрите, что произойдет. - person Your Common Sense; 19.02.2017
comment
Ответ заключался в том, чтобы ответить на конкретный вопрос, заданный ОП. Вы педантичны, считая, что это соображение выходит за рамки вопроса. - person Lee; 19.02.2017
comment
Если вы не заметили, ответ на конкретный вопрос, заданный OP, был опубликован почти десять лет назад. - person Your Common Sense; 19.02.2017
comment
Да, если бы вы прочитали мой ответ, вы бы заметили, что я упомянул это в первой строке. У тебя плохой день? Вы, кажется, действительно обиделись на мой пост. - person Lee; 19.02.2017

Вот еще одно (тонкое) решение этой проблемы:

Сначала вам нужно подсчитать данные исходного массива (здесь: $ aData) с помощью count (). Затем вы используете array_fill () и генерируете новый массив, который содержит столько записей, сколько имеет исходный массив, каждая со значением «(?,?)» (Количество заполнителей зависит от используемых вами полей; здесь: 2). Затем сгенерированный массив нужно сжать и в качестве клея использовать запятую. В цикле foreach вам необходимо сгенерировать другой индекс относительно количества используемых заполнителей (количество заполнителей * текущий индекс массива + 1). Вам нужно добавить 1 к сгенерированному индексу после каждого привязанного значения.

$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();
person Bernhard    schedule 07.02.2020

Вы можете вставить несколько строк в один запрос с помощью этой функции:

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 - это массив массивов значений. В вашем случае вы должны вызвать функцию с помощью

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

Это дает то преимущество, что вы используете подготовленные операторы при вставке нескольких строк с помощью одного запроса. Безопасность!

person Chris Michaelides    schedule 07.03.2014

Вот мое решение: https://github.com/sasha-ch/Aura.Sql на основе библиотеки auraphp / Aura.Sql.

Пример использования:

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

Сообщения об ошибках приветствуются.

person sasha-ch    schedule 07.02.2015
comment
Начиная с версии 2.4 вы можете создавать множественные вставки с помощью github .com / auraphp / Aura.SqlQuery / tree / и использовать ExtendedPdo. выполнить :) . - person Hari K T; 28.03.2015

Мой реальный пример вставки всех немецких почтовых индексов в пустую таблицу (чтобы добавить названия городов позже):

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

Как видите, он полностью гибкий. Вам не нужно проверять количество столбцов или проверять, в каком положении находится ваш столбец. Вам нужно только установить данные для вставки:

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

Я горжусь некоторыми конструкторами строк запроса, поскольку они работают без тяжелых функций массива, таких как array_merge. Особенно удачной находкой оказался vsprintf ().

Наконец, мне нужно было добавить 2x while (), чтобы не превысить лимит памяти. Это зависит от вашего лимита памяти, но в целом это хорошее общее решение, позволяющее избежать проблем (и наличие 10 запросов по-прежнему намного лучше, чем 10.000).

person mgutt    schedule 05.03.2015

У меня была такая же проблема, и это то, что я решаю для себя, и я сделал для себя функцию (и вы можете использовать ее, если это вам поможет).

Пример:

ВСТАВИТЬ СТРАНЫ (страна, город) ЗНАЧЕНИЯ (Германия, Берлин), (Франция, Париж);

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

Если insertMultipleData ($ table, $ multi_params) возвращает TRUE, ваши данные были вставлены в вашу базу данных.

person Dardan    schedule 12.09.2018

Это сработало для меня

$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();
person Andre Da Silva Poppi    schedule 28.10.2014

как насчет этого:

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

Идея состоит в том, чтобы циклически перебирать значения вашего массива, добавляя «номера идентификаторов» в каждый цикл для ваших подготовленных заполнителей операторов, в то же время вы добавляете значения в свой массив для параметров привязки. Если вам не нравится использовать «ключевой» индекс из массива, вы можете добавить внутри цикла $ i = 0 и $ i ++. Любой из них работает в этом примере, даже если у вас есть ассоциативные массивы с именованными ключами, он все равно будет работать, если ключи будут уникальными. Немного поработав, это подойдет и для вложенных массивов.

** Обратите внимание, что substr удаляет последние пробелы и запятую переменных $ sql, если у вас нет пробела, вам нужно изменить это значение на -1, а не на -2.

person dean williams    schedule 26.05.2020

Большинство приведенных здесь решений для создания подготовленного запроса являются более сложными, чем они должны быть. Используя встроенные функции PHP, вы можете легко создать оператор SQL без значительных накладных расходов.

Учитывая $records, массив записей, где каждая запись сама по себе является индексированным массивом (в форме field => value), следующая функция вставит записи в данную таблицу $table при соединении PDO $connection, используя только один подготовленный оператор. Обратите внимание, что это решение PHP 5.6+ из-за использования распаковки аргументов при вызове 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);
}
person mariano.iglesias    schedule 30.09.2015
comment
Этот код никогда не следует использовать, так как он уязвим для SQL-инъекций. - person Your Common Sense; 30.09.2015
comment
@ Ты, я не вижу того, что видишь ты. Заставьте меня дать ответ на этот вопрос. - person mickmackusa; 25.01.2021
comment
@mickmackusa Я подразумеваю это - person Your Common Sense; 25.01.2021
comment
Ах, это было бы хорошо, если бы плакат и исследователи поняли эту конкретную проблему, связанную с введением названий полей. Я знаю, что в те дни вы были более сварливым парнем, но, пожалуйста, не забудьте разместить эту страницу инъекции на соответствующих страницах SO в будущем, чтобы люди узнали об уязвимости. Сказать людям, что запрос уязвим, но не объяснить, как это сделать - бесполезно. ВингТД. - person mickmackusa; 25.01.2021