Кілька оновлень у MySQL


388

Я знаю, що ви можете вставити відразу кілька рядків, чи є спосіб оновити кілька рядків одночасно (як в одному запиті) в 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;

Відповіді:


651

Так, це можливо - ви можете використовувати 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);

22
Якщо дублікатів немає, то я не хочу, щоб цей рядок було вставлено. що я повинен робити? тому що я отримую інформацію з іншого сайту, який підтримує таблиці з ідентифікаторами. Я вставлю значення відносно цього ідентифікатора. якщо на сайті є нові записи, то я в кінцевому підсумку вставляю лише ідентифікатори та рахую, за винятком усієї іншої інформації. якщо і тільки якщо є запис для ідентифікатора, то він повинен оновитись інакше, він повинен пропустити. Що я буду робити?
Джаяпал Чандран

33
Примітка: ця відповідь також передбачає, що ідентифікатор є первинним ключем
JM4

11
@JayapalChandran, ви повинні використовувати INSERT IGNORE разом з НАД ДУПЛІКАТНЕ КЛЮЧНЕ ОНОВЛЕННЯ. dev.mysql.com/doc/refman/5.5/uk/insert.html
Харалан Добрев

16
@HaralanDobrev Використання INSERT IGNORE все ще вставляє не дублюються записи. чого Джаяпал хотів уникнути. INSERT IGNORE просто перетворює будь-які помилки на попередження :( stackoverflow.com/questions/548541/…
Takehiro Adachi

2
Ця відповідь передбачає, що ідентифікатор є унікальним ключем (може бути первинним, як говорили інші), але що ще важливіше, він передбачає, що інших унікальних ключів немає. Якщо такі є, це може кинути гайковий ключ у творах.
Стів Хорват

129

Оскільки у вас є динамічні значення, для оновлення стовпців потрібно використовувати IF чи CASE. Це стає якось потворно, але це має працювати.

Використовуючи свій приклад, ви можете це зробити так:

ОНОВЛЮВАННЯ таблиці SET Col1 = Ідентифікатор CASE 
                          КОЛИ 1 ТО 1 
                          КОЛИ 2 ЧАС 2 
                          КОЛИ 4 ТО 10 
                          ELSE Col1 
                        Кінець, 
                 Col2 = Ідентифікатор CASE 
                          КОЛИ 3 ТО 3 
                          КОЛИ 4 ТО 12 
                          ELSE Col2 
                        Кінець
             WHERE id IN (1, 2, 3, 4);

можливо, не так сильно писати для динамічного оновлення, але цікавий погляд на функціональність корпусу ...
me_

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 )).');';
Boolean_Type

86

Питання старе, але я хотів би продовжити тему ще однією відповіддю.

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

  • Як говориться, якщо вам трапляється запускати запит рядками, чиї первинні ключі не існують у таблиці, запит вставляє нові "напівзапечатані" записи. Напевно, це не те, що ти хочеш
  • Якщо у вас є таблиця з ненульовим полем без значення за замовчуванням, і ви не хочете торкатися цього поля в запиті, ви отримаєте "Field 'fieldname' doesn't have a default value"попередження MySQL, навіть якщо ви взагалі не вставляєте жодного рядка. Якщо ви вирішите бути суворими, ви перетворите перешкоди та перетворите попередження mysql у винятки з програми.

Я зробив кілька тестів на ефективність для трьох запропонованих варіантів, включаючи INSERT ... ON DUPLICATE KEY UPDATEваріант, варіант із пунктом "випадок / коли / тоді" та наївний підхід із транзакцією. Ви можете отримати код python та результати тут . Загальний висновок полягає в тому, що варіант із оператором case виявляється вдвічі швидшим, ніж два інші варіанти, але написати його правильний та безпечний для ін'єкції код досить важко, тому я особисто дотримуюся найпростішого підходу: використання транзакцій.

Редагувати: дані Дакусана свідчать про те, що мої оцінки ефективності не є справедливими. Будь ласка, подивіться цю відповідь для іншого, більш детального дослідження.


Використання транзакцій, дуже приємна (і проста) порада!
mTorres

Що робити, якщо мої таблиці не мають InnoDB Type?
TomeeNS

1
Чи міг би хтось надати посилання на те, як виглядають транзакції? І / або код для безпечного для ін'єкцій коду для варіанта із заявою?
Франсуа М.

1
Я вважаю, що інформація про швидкість у цій публікації неправдива. Про це я писав у публікації нижче. stackoverflow.com/questions/3432/multiple-updates-in-mysql / ...
Dakusan

1
@Dakusan, чудова відповідь. Дякую велике за розширення, коментування та закріплення моїх результатів.
Роман Іманкулов

72

Не впевнений, чому ще не згадується інший корисний варіант:

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, як це робив я.
v010дя

1
Це було чудово для оновлення на столі з величезною кількістю стовпців. Напевно, в майбутньому я буду багато використовувати цей запит. Дякую!
Каспер Вілкс

Я спробував цей тип запиту. Але коли записи досягають 30k, Прикордонний сервер припиняється. Чи є якесь інше рішення?
Бхавін Чаухан

Це виглядає чудово. Я спробую поєднати це з пунктом WHERE, де первинні ключі не оновлюються, але використовуються для ідентифікації стовпців для зміни.
nl-x

@BhavinChauhan Ви намагалися використовувати тимчасову таблицю замість вибору приєднання, щоб обійти проблему?
nl-x

41

Все наступне стосується InnoDB.

Я вважаю, що важливість швидкості застосування трьох різних методів є важливою.

Існує 3 методи:

  1. ВСТАВКА: ВСТАВИТЬСЯ НА ВКЛЮЧЕНОЮ ДОДАТКОВОЮ КЛЮЧНОЮ ОНОВЛЕННЯ
  2. ТРАНЗАКЦІЯ: Де ви робите оновлення для кожного запису в рамках транзакції
  3. СЛУЧАЙ: У якому випадку ви перебуваєте у випадку, коли для кожного іншого запису в межах оновлення

Я щойно це перевірив, і метод INSERT був для мене на 6,7 рази швидшим, ніж метод TRANSACTION. Я спробував набір як 3000, так і 30 000 рядів.

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

Ще гірше, що метод CASE був на 41,1 разів повільніше, ніж метод INSERT з 30000 записами (на 6,1 рази повільніше, ніж ТРАНЗАКЦІЯ). І в 75 разів повільніше в MyISAM. Методи INSERT і CASE побилися навіть при ~ 1000 записів. Навіть при 100 записах метод CASE БАРЕШЕ швидший.

Тому в цілому я вважаю, що метод INSERT є найкращим і найпростішим у використанні. Запити менші та простіші для читання та займають лише 1 запит дій. Це стосується і InnoDB, і MyISAM.

Бонусні речі:

Рішення завдання не за замовчуванням поля INSERT, щоб тимчасово відключити відповідні режими SQL: SET SESSION sql_mode=REPLACE(REPLACE(@@SESSION.sql_mode,"STRICT_TRANS_TABLES",""),"STRICT_ALL_TABLES",""). Не забудьте зберегти sql_modeперше, якщо плануєте його повернути.

Що стосується інших коментарів, які я бачив, що кажуть, що auto_increment збільшується за допомогою методу INSERT, здається, це стосується InnoDB, але не MyISAM.

Код для запуску тестів такий. Він також виводить .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
Ви тут робите роботу Господа;) Багато вдячний.
чилі

Тестуючи деяку ефективність між GoLang та PHP, використовуючи 40k рядків на MariaDB, я отримував 2 сек на PHP і більше 6 секунд на golang .... Ну, мені завжди говорили, що GoLang працюватиме швидше, ніж PHP !!! Так, я починаю цікавитись, як покращити продуктивність ... Використовуючи ВСТАВКА ... НА ДУПЛІКАТИ КЛЮЧОВОГО ОНОВЛЕННЯ ... У мене на Golang 0,74 сек і на PHP 0,86 сек !!!!
Дієго Фаверо

1
Суть мого коду полягає в обмеженні результатів синхронізації строго SQL-операторами, а не кодом для мови чи бібліотеки. GoLang і PHP - це 2 абсолютно окремі мови, призначені для абсолютно різних речей. PHP призначений для одноразового сценарію середовища на одній нитці з переважно обмеженим та пасивним збиранням сміття. GoLang призначений для тривалого запуску складених програм із агресивним збиранням сміття та багатопотоковою роботою як однією з особливостей основної мови. Вони ледве можуть бути більш різними з точки зору функціональності мови та розуму. [Продовження]
Дакусан

Тому під час запуску ваших тестів не забудьте обмежити вимірювання швидкості строго функцією "Запит", яка вимагає для оператора SQL. Порівнювати та оптимізувати інші частини вихідного коду, які не є строгими дзвінками на запит, як порівняння яблук та апельсинів. Якщо ви обмежите свої результати цим (попередньо складені рядки та готові до запуску), результати повинні бути дуже схожими. Будь-які розбіжності в цей момент є виною бібліотеки SQL мови, а не обов'язково самої мови. На мою думку, рішення "
ВСТАВКА ДУПЛІКАТУ"

Що стосується того, що ваш коментар щодо GoLang є більш швидким, то це неймовірно широке твердження, яке не враховує жодного з безлічі застережень або нюансів цих мов та їх дизайну. Java - це інтерпретована мова, але я з’ясував, що 15 років тому вона за певних сценаріїв фактично може майже відповідати (а може навіть і іноді перемагати) C за швидкістю. І C - це компільована мова, і найпоширеніша з системних мов найнижчого рівня, крім асемблера. Мені дуже подобається, що робить GoLang, і це, безумовно, має силу і плинність, щоб стати однією з найбільш поширених і оптимізованих систем [
Проти

9

Використовуйте тимчасову таблицю

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

8
UPDATE table1, table2 SET table1.col1='value', table2.col1='value' WHERE table1.col3='567' AND table2.col6='567'

Це повинно працювати для вас.

У посібнику MySQL є посилання на кілька таблиць.


6

Чому в одному запиті ніхто не згадує кілька заяв ?

У php ви використовуєте multi_queryметод екземпляра mysqli.

З посібника з php

MySQL необов'язково дозволяє мати декілька висловлювань в одному рядку тверджень. Надсилання декількох висловлювань одночасно зменшує зворотну поїздку клієнт-сервер, але вимагає спеціального оброблення.

Ось результат порівняно з іншими 3-ма методами оновлення 30 000 необроблених. Код можна знайти тут, який базується на відповіді від @Dakusan

Трансакція: 5.5194580554962
Вставка: 0.20669293403625
Корпус: 16.474853992462
Мульти: 0,0412278175354

Як бачимо, запит з декількома заявами є більш ефективним, ніж найвища відповідь.

Якщо ви отримуєте таке повідомлення про помилку:

PHP Warning:  Error while sending SET_OPTION packet

Можливо, вам доведеться збільшити max_allowed_packetконфігураційний файл mysql, який знаходиться в моїй машині, /etc/mysql/my.cnfа потім перезапустити mysqld.


Всі нижче порівняння порівнюються з тестом INSERT. Я просто провів тест в тих же умовах, і без транзакцій це було 145 разів повільніше на 300 рядків і 753 рази повільніше на 3000 рядків. Я спочатку починав із 30 000 рядів, але я пішов робити собі обід і повернувся, і він все ще йшов. Це має сенс, оскільки виконання окремих запитів та передача кожного з баз даних окремо було б смішно дорогим. Особливо з тиражуванням. Увімкнення транзакцій, однак, має велике значення. На 3 000 рядків було потрібно в 1,5 рази більше, а в 30 000 рядків 2,34x . [продовження]
Дакусан

Але ви мали рацію, що це швидко (з транзакціями). І в 3000, і в 30 000 рядків це було швидше за все, крім методу INSERT. Абсолютно немає можливості отримати кращі результати від запуску 1 запиту, ніж 30 000 запитів, навіть якщо вони зібрані в спеціальному виклику MySQL API. Запустивши лише 300 рядків, це було набагато швидше, ніж усі інші методи (на мій подив), що випливає приблизно з тієї ж кривої графіку, що і метод CASE. Це швидше можна пояснити двома способами. Перший з них полягає в тому, що метод INSERT по суті завжди вставляє 2 ряди завдяки "
КЛЮЧУ НА ДУПЛІКАТУ

ОНОВЛЕННЯ ", спричиняючи як" ВСТАВИТЬ ", так і" ОНОВЛЮВАТИ ". Інше, що в роботі SQL-процесора менше редагування лише 1 ряду за один раз через пошук індексу. Я не впевнений, як ви отримали інші результати, ніж я, але ваш додатковий тест виглядає солідним. Я насправді навіть не впевнений, як реплікація оброблятиме цей виклик. Це також спрацює лише для виконання UPDATE-дзвінків. Вставлення дзвінків ЗАВЖДИ буде найшвидшим за допомогою одного запиту INSERT.
Дакусан,

Я робив одночасно 300 ОНОВЛЕННЯ на столі, щоб переглянути помилку в циклі for, який зайняв 41 секунду. Введення одних і тих же UPDATE запитів в одну $mysqli->multi_query($sql)зайняло "0" секунд. Однак наступні запити не вдалися, через що я зробив це окремою "програмою".
Кріс К

Дякую. Вдалося оновити близько 5 к рядків (не тестував більше) за хвилину, використовуючи кілька запитів. Якщо хтось шукає рішення для PDO: stackoverflow.com/questions/6346674/…
Scofield

3

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

Тут ( http://dev.mysql.com/doc/refman/5.1/uk/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
Це те саме, що надсилати запити окремо. Єдина відмінність полягає в тому, що ви надсилаєте все це в одному мережевому пакеті, але ОНОВЛЕННЯ все одно будуть оброблятися як окремі запити. Краще загортати їх в одну транзакцію, тоді зміни будуть внесені до таблиці відразу.
Marki555

3
Як обгорнути їх однією транзакцією? Покажіть нам, будь ласка.
TomeeNS

@TomeeNS Використовуйте mysqli::begin_transaction(..)перед надсиланням запиту та mysql::commit(..)після. Або використовувати START TRANSACTIONяк перше і COMMITяк останнє твердження в самому запиті.
Juha Palomäki

3

Ви можете отримати псевдонім тієї самої таблиці, щоб дати вам ідентифікатор, який ви хочете вставити (якщо ви робите оновлення по рядках:

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

А тепер простий шлях

update speed m,
    (select 1 as id, 20 as speed union
     select 2 as id, 30 as speed union
     select 99 as id, 10 as speed
        ) t
set m.speed = t.speed where t.id=m.id

-1

використання

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

Будь ласка, запиши:

  • id повинен бути первинним унікальним ключем
  • якщо ви використовуєте сторонні ключі для посилання на таблицю, REPLACE видаляє, а потім вставляє, так що це може спричинити помилку

-3

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

Update Table Set
Column1 = 'New Value'

У наступному буде оновлено всі рядки, де значення стовпця2 перевищує 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'

-3

Так .. це можливо за допомогою оператора INSERT ON DUPLICATE KEY UPDATE sql. VALUES (a), b = VALUES (b), c = VALUES (c)


-5
UPDATE tableName SET col1='000' WHERE id='3' OR id='5'

Це повинно досягти того, що ви шукаєте. Просто додайте більше ідентифікаторів. Я це протестував.


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

Таким чином, ви можете оновити таблицю з отворами за допомогою одного запиту


Я не downvote, але я думаю , що це заперечення робити безліч, коли він не потрібен (і ви по - , як і раніше робити це, коли ви налаштовуєте somethingдо something)
v010dya
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.