Несколько обновлений в MySQL

302

Я знаю, что вы можете вставлять сразу несколько строк, есть ли способ обновить сразу несколько строк (как в одном запросе) в MySQL?

Изменить: Например, у меня есть следующие

Name   id  Col1  Col2
Row1   1    6     1
Row2   2    2     3
Row3   3    9     5
Row4   4    16    8

Я хочу объединить все следующие обновления в один запрос

UPDATE table SET Col1 = 1 WHERE id = 1;
UPDATE table SET Col1 = 2 WHERE id = 2;
UPDATE table SET Col2 = 3 WHERE id = 3;
UPDATE table SET Col1 = 10 WHERE id = 4;
UPDATE table SET Col2 = 12 WHERE id = 4;
Теги:
sql-update

18 ответов

553
Лучший ответ

Да, это возможно - вы можете использовать INSERT... ON DUPLICATE KEY UPDATE.

Используя ваш пример:

INSERT INTO table (id,Col1,Col2) VALUES (1,1,1),(2,2,3),(3,9,3),(4,10,12)
ON DUPLICATE KEY UPDATE Col1=VALUES(Col1),Col2=VALUES(Col2);
  • 18
    Если дубликатов нет, я не хочу вставлять эту строку. что должен делать id? потому что я получаю информацию с другого сайта, который поддерживает таблицы с идентификаторами. Я вставляю значения относительно этого идентификатора. если на сайте появятся новые записи, то я в итоге вставлю только идентификаторы и количество, кроме всей остальной информации. если и только если есть запись для идентификатора, он должен обновиться, иначе он должен пропустить. что мне делать?
  • 0
    так что если это похоже на вставку в skip null или skip empty при повторном обновлении ... тогда это было бы хорошо.
Показать ещё 14 комментариев
104

Поскольку у вас есть динамические значения, вам нужно использовать IF или CASE для обновления столбцов. Он становится некрасивым, но он должен работать.

Используя ваш пример, вы можете сделать это так:

UPDATE table SET Col1 = CASE id 
                          WHEN 1 THEN 1 
                          WHEN 2 THEN 2 
                          WHEN 4 THEN 10 
                          ELSE Col1 
                        END, 
                 Col2 = CASE id 
                          WHEN 3 THEN 3 
                          WHEN 4 THEN 12 
                          ELSE Col2 
                        END
             WHERE id IN (1, 2, 3, 4);
  • 0
    может быть, не так красиво писать для динамического обновления, но интересно посмотреть на функциональность корпуса ...
  • 1
    @ user2536953, это может быть хорошо для динамического обновления тоже. Например, я использовал это решение в цикле в php: $commandTxt = 'UPDATE operations SET chunk_finished = CASE id '; foreach ($blockOperationChecked as $operationID => $operationChecked) $commandTxt .= " WHEN $operationID THEN $operationChecked "; $commandTxt .= 'ELSE id END WHERE id IN ('.implode(', ', array_keys(blockOperationChecked )).');';
73

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

Моя точка зрения, самый простой способ добиться этого - просто обернуть несколько запросов транзакцией. Принятый ответ INSERT ... ON DUPLICATE KEY UPDATE - хороший хак, но нужно знать его недостатки и ограничения:

  • Как говорится, если вы запускаете запрос с строками, чьи первичные ключи не существуют в таблице, запрос вставляет новые "полузаработанные" записи. Вероятно, это не то, что вы хотите.
  • Если у вас есть таблица с нулевым полем без значения по умолчанию и не хотите касаться этого поля в запросе, вы получите предупреждение "Field 'fieldname' doesn't have a default value" MySQL, даже если вы не вставляете ни одной строки вообще, Это вызовет у вас проблемы, если вы решите быть строгим и превратите предупреждения mysql в исключения из среды выполнения в своем приложении.

Я провел несколько тестов производительности для трех предложенных вариантов, включая вариант INSERT ... ON DUPLICATE KEY UPDATE, вариант с предложением "case/when/then" и наивный подход с транзакцией. Вы можете получить код и результаты python здесь. Общий вывод заключается в том, что вариант с аргументом case оказывается в два раза быстрее, чем два других варианта, но довольно сложно написать для него правильный и безопасный для инъекций код, поэтому я лично придерживаюсь самого простого подхода: используя транзакции.

Изменить: Выводы Dakusan доказывают, что мои оценки эффективности не совсем допустимы. См. этот ответ для другого, более подробного исследования.

  • 0
    Используя транзакции, очень хороший (и простой) совет!
  • 0
    Что если мои таблицы не InnoDB Type?
Показать ещё 3 комментария
41

Не знаю, почему еще один полезный вариант еще не упоминается:

UPDATE my_table m
JOIN (
    SELECT 1 as id, 10 as _col1, 20 as _col2
    UNION ALL
    SELECT 2, 5, 10
    UNION ALL
    SELECT 3, 15, 30
) vals ON m.id = vals.id
SET col1 = _col1, col2 = _col2;
  • 4
    Это лучшее. Особенно, если вы извлекаете значения для обновления из другого запроса SQL, как я делал.
  • 1
    Это было здорово для обновления таблицы с огромным количеством столбцов. Вероятно, я буду часто использовать этот запрос в будущем. Спасибо!
Показать ещё 1 комментарий
17

Все перечисленное применимо к InnoDB.

Я чувствую, что важны скорости трех разных методов.

Существует 3 метода:

  • INSERT: INSERT с ОБНОВЛЕНИЕМ КЛЮЧА DUPLICATE
  • TRANSACTION: когда вы делаете обновление для каждой записи в транзакции
  • CASE: В каком случае/когда для каждой отдельной записи в UPDATE

Я только что протестировал это, и метод INSERT был 6.7x быстрее для меня, чем метод TRANSACTION. Я пробовал набор из 3000 и 30 000 строк.

Метод TRANSACTION по-прежнему должен запускать каждый индивидуальный запрос, который требует времени, хотя он выполняет выборку в памяти или что-то еще при выполнении. Метод TRANSACTION также очень дорог в журналах репликации и запросов.

Хуже того, метод CASE был 41.1x медленнее, чем метод INSERT с 30 000 записей (на 6.1x медленнее, чем транзакция). И 75x медленнее в MyISAM. Методы INSERT и CASE сломались даже на ~ 1000 записей. Даже при 100 записях метод CASE BARELY быстрее.

В общем, я считаю, что метод INSERT является лучшим и простым в использовании. Запросы становятся меньше и легче читать, и они обрабатывают только один запрос. Это относится как к InnoDB, так и к MyISAM.

Бонусы:

Решение проблемы INSERT без полей по умолчанию - это временно отключить соответствующие режимы SQL: SET SESSION sql_mode=REPLACE(REPLACE(@@SESSION.sql_mode,"STRICT_TRANS_TA‌​BLES",""),"STRICT_AL‌​L_TABLES",""). Обязательно сначала сохраните sql_mode, если вы планируете вернуть его.

Что касается других комментариев, я видел, что auto_increment поднимается с использованием метода INSERT, я тоже это тестировал и, похоже, не так.

Код для запуска тестов следующий. Он также выводит файлы .SQL для удаления служебных сообщений на php-интерпретаторе

<?
//Variables
$NumRows=30000;

//These 2 functions need to be filled in
function InitSQL()
{

}
function RunSQLQuery($Q)
{

}

//Run the 3 tests
InitSQL();
for($i=0;$i<3;$i++)
    RunTest($i, $NumRows);

function RunTest($TestNum, $NumRows)
{
    $TheQueries=Array();
    $DoQuery=function($Query) use (&$TheQueries)
    {
        RunSQLQuery($Query);
        $TheQueries[]=$Query;
    };

    $TableName='Test';
    $DoQuery('DROP TABLE IF EXISTS '.$TableName);
    $DoQuery('CREATE TABLE '.$TableName.' (i1 int NOT NULL AUTO_INCREMENT, i2 int NOT NULL, primary key (i1)) ENGINE=InnoDB');
    $DoQuery('INSERT INTO '.$TableName.' (i2) VALUES ('.implode('), (', range(2, $NumRows+1)).')');

    if($TestNum==0)
    {
        $TestName='Transaction';
        $Start=microtime(true);
        $DoQuery('START TRANSACTION');
        for($i=1;$i<=$NumRows;$i++)
            $DoQuery('UPDATE '.$TableName.' SET i2='.(($i+5)*1000).' WHERE i1='.$i);
        $DoQuery('COMMIT');
    }

    if($TestNum==1)
    {
        $TestName='Insert';
        $Query=Array();
        for($i=1;$i<=$NumRows;$i++)
            $Query[]=sprintf("(%d,%d)", $i, (($i+5)*1000));
        $Start=microtime(true);
        $DoQuery('INSERT INTO '.$TableName.' VALUES '.implode(', ', $Query).' ON DUPLICATE KEY UPDATE i2=VALUES(i2)');
    }

    if($TestNum==2)
    {
        $TestName='Case';
        $Query=Array();
        for($i=1;$i<=$NumRows;$i++)
            $Query[]=sprintf('WHEN %d THEN %d', $i, (($i+5)*1000));
        $Start=microtime(true);
        $DoQuery("UPDATE $TableName SET i2=CASE i1\n".implode("\n", $Query)."\nEND\nWHERE i1 IN (".implode(',', range(1, $NumRows)).')');
    }

    print "$TestName: ".(microtime(true)-$Start)."<br>\n";

    file_put_contents("./$TestName.sql", implode(";\n", $TheQueries).';');
}
  • 1
    Вы делаете работу Господа здесь;) Очень ценится.
8
UPDATE table1, table2 SET table1.col1='value', table2.col1='value' WHERE table1.col3='567' AND table2.col6='567'

Это должно работать для ya.

В руководстве MySQL содержится несколько таблиц.

7

Использовать временную таблицу

// Reorder items
function update_items_tempdb(&$items)
{
    shuffle($items);
    $table_name = uniqid('tmp_test_');
    $sql = "CREATE TEMPORARY TABLE `$table_name` ("
        ."  `id` int(10) unsigned NOT NULL AUTO_INCREMENT"
        .", `position` int(10) unsigned NOT NULL"
        .", PRIMARY KEY (`id`)"
        .") ENGINE = MEMORY";
    query($sql);
    $i = 0;
    $sql = '';
    foreach ($items as &$item)
    {
        $item->position = $i++;
        $sql .= ($sql ? ', ' : '')."({$item->id}, {$item->position})";
    }
    if ($sql)
    {
        query("INSERT INTO `$table_name` (id, position) VALUES $sql");
        $sql = "UPDATE `test`, `$table_name` SET `test`.position = `$table_name`.position"
            ." WHERE `$table_name`.id = `test`.id";
        query($sql);
    }
    query("DROP TABLE `$table_name`");
}
3

Существует параметр, который вы можете изменить, называемый "multi statement", который отключает "механизм безопасности" MySQL, реализованный для предотвращения (более одной) команды впрыска. Типично для "блестящей" реализации MySQL, это также мешает пользователю выполнять эффективные запросы.

Здесь (http://dev.mysql.com/doc/refman/5.1/en/mysql-set-server-option.html) есть некоторая информация о реализации этого параметра C.

Если вы используете PHP, вы можете использовать mysqli для выполнения нескольких операторов (я думаю, что php уже давно поставляется с mysqli)

$con = new mysqli('localhost','user1','password','my_database');
$query = "Update MyTable SET col1='some value' WHERE id=1 LIMIT 1;";
$query .= "UPDATE MyTable SET col1='other value' WHERE id=2 LIMIT 1;";
//etc
$con->multi_query($query);
$con->close();

Надеюсь, что это поможет.

  • 4
    Это то же самое, что отправка запросов отдельно. Единственное отличие состоит в том, что вы отправляете все это в одном сетевом пакете, но ОБНОВЛЕНИЯ будут по-прежнему обрабатываться как отдельные запросы. Лучше заключить их в одну транзакцию, тогда изменения будут сразу внесены в таблицу.
  • 3
    Как обернуть их в одну транзакцию? Покажите нам, пожалуйста.
Показать ещё 1 комментарий
2

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

UPDATE table1 tab1, table1 tab2 -- alias references the same table
SET 
col1 = 1
,col2 = 2
. . . 
WHERE 
tab1.id = tab2.id;

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

2

Вы также можете быть заинтересованы в использовании объединений для обновлений, что также возможно.

Update someTable Set someValue = 4 From someTable s Inner Join anotherTable a on s.id = a.id Where a.id = 4
-- Only updates someValue in someTable who has a foreign key on anotherTable with a value of 4.

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

1

Почему никто не упоминает несколько операторов в одном запросе?

В php используется метод multi_query экземпляра mysqli.

Из руководство php

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

Вот результат, сравнимый с другими 3 способами в обновлении 30 000 raw. Код можно найти здесь, который основан на ответе от @Dakusan

Сделка: 5.5194580554962
Вставка: 0,20669293403625
Дело: 16.474853992462
Multi: 0.0412278175354

Как вы можете видеть, запрос нескольких операторов более эффективен, чем самый высокий ответ.

Если вы получите сообщение об ошибке следующим образом:

PHP Warning:  Error while sending SET_OPTION packet

Вам может потребоваться увеличить max_allowed_packet в конфигурационном файле mysql, который на моем компьютере /etc/mysql/my.cnf, а затем перезапустить mysqld.

  • 0
    Все приведенные ниже сравнения выполняются в соответствии с тестом INSERT. Я просто запустил тест в тех же условиях, и без транзакций он был в 145 раз медленнее на 300 строк и 753 раз медленнее на 3000 строк. Первоначально я начал с 30 000 строк, но я пошел приготовить себе обед и вернулся, и он все еще шел. Это имеет смысл, поскольку выполнение отдельных запросов и сброс каждого из них в базу данных будет смехотворно дорогим. Особенно с репликацией. Включение транзакций, тем не менее, имеет большое значение. При 3000 строк это заняло в 1,5 раза больше, а при 30000 строк - 2,34 . [Продолжение]
  • 0
    Но вы были правы в том, что это быстро (с транзакциями). И на 3000, и на 30000 строк это было быстрее, чем все, кроме метода INSERT. Совершенно невозможно получить лучший результат от выполнения 1 запроса, чем 30 000 запросов, даже если они объединены в специальный вызов MySQL API. Выполнение только 300 строк, это было НАМНОГО быстрее, чем все другие методы (к моему удивлению), что соответствует примерно той же кривой графика, что и метод CASE. Это быстрее можно объяснить двумя способами. Во-первых, метод INSERT по существу всегда вставляет 2 строки из-за «ON DUPLICATE KEY [продолжение]
Показать ещё 3 комментария
0

Для больших наборов данных лучше сделать это на языке высокого уровня, таком как Java:

public void UpdateTableDataWithNewValues(final String TableName, final ArrayList<MyClass> insuranceCellsToReplaceValues) {
    try {
        String sql = "UPDATE "+ TableName + " SET value = ? WHERE attributeId = ? AND entityRowId = ?";
        try (PreparedStatement ps = ballast.prepareStatement(sql)) {
            for(MyClass factor: insuranceCellsToReplaceValues) {
                Integer i = 0;
                ps.setFloat(++i, factor.getValue());
                ps.setInt(++i, factor.getAttributeId());
                ps.setInt(++i, factor.getEntityRowId());

                ps.addBatch();
            }
            ps.executeBatch();
            ps.close();                
        }
    } catch (Exception e) {
        logger.error("{}, {}", e.getMessage(), e.getCause());
    }                
}
-2

использование

REPLACE INTO`table` VALUES (`id`,`col1`,`col2`) VALUES
(1,6,1),(2,2,3),(3,9,5),(4,16,8);

Обратите внимание:

  • id должен быть основным уникальным ключом
  • если вы используете внешние ключи для ссылайтесь на таблицу, REPLACE удаляет, затем вставляет, так что это может вызвать ошибку
-4

С PHP я сделал это. Используйте точку с запятой, разделите ее на массив и затем подайте через цикл.

$con = new mysqli('localhost','user1','password','my_database');
$batchUpdate = true; /*You can choose between batch and single query */
$queryIn_arr = explode(";", $queryIn);

if($batchUpdate)    /* My SQL prevents multiple insert*/
{
    foreach($queryIn_arr as $qr)
    {
        if(strlen($qr)>3)
        {
            //echo '<br>Sending data to SQL1:<br>'.$qr.'</br>';
            $result = $conn->query($qr);
        }

    }
}
else
{
    $result = $conn->query($queryIn);
}
$con->close();
-4

Да.. это возможно с помощью инструкции INSERT ON DUPLICATE KEY UPDATE sql.. синтаксис: INSERT INTO table_name (a, b, c) ЦЕННОСТИ (1,2,3), (4,5,6)   ON DUPLICATE KEY UPDATE a = ЦЕННОСТИ (a), b = ЦЕННОСТИ (b), c = ЦЕННОСТИ (c)

-4

Далее будут обновлены все строки в одной таблице

Update Table Set
Column1 = 'New Value'

Следующий будет обновлять все строки, где значение Column2 больше 5

Update Table Set
Column1 = 'New Value'
Where
Column2 > 5

Есть все Unkwntech пример обновления более чем одной таблицы

UPDATE table1, table2 SET
table1.col1 = 'value',
table2.col1 = 'value'
WHERE
table1.col3 = '567'
AND table2.col6='567'
-5
UPDATE tableName SET col1='000' WHERE id='3' OR id='5'

Это должно достичь того, что вы ищете. Просто добавьте больше идентификаторов. Я протестировал его.

-6
UPDATE `your_table` SET 

`something` = IF(`id`="1","new_value1",`something`), `smth2` = IF(`id`="1", "nv1",`smth2`),
`something` = IF(`id`="2","new_value2",`something`), `smth2` = IF(`id`="2", "nv2",`smth2`),
`something` = IF(`id`="4","new_value3",`something`), `smth2` = IF(`id`="4", "nv3",`smth2`),
`something` = IF(`id`="6","new_value4",`something`), `smth2` = IF(`id`="6", "nv4",`smth2`),
`something` = IF(`id`="3","new_value5",`something`), `smth2` = IF(`id`="3", "nv5",`smth2`),
`something` = IF(`id`="5","new_value6",`something`), `smth2` = IF(`id`="5", "nv6",`smth2`) 

//Вы просто строите его в php, например

$q = 'UPDATE `your_table` SET ';

foreach($data as $dat){

  $q .= '

       `something` = IF(`id`="'.$dat->id.'","'.$dat->value.'",`something`), 
       `smth2` = IF(`id`="'.$dat->id.'", "'.$dat->value2.'",`smth2`),';

}

$q = substr($q,0,-1);

Итак, вы можете обновить таблицу отверстий одним запросом

  • 0
    Почему это решение так плохо? Я говорю о SQL, а не код PHP ...
  • 0
    Я не понизил голос, но я думаю, что возражение состоит в том, чтобы делать сет, когда он не нужен (и вы все еще делаете это, когда вы something something настраиваете)

Ещё вопросы

Сообщество Overcoder
Наверх
Меню