Aggiornamenti multipli in MySQL


388

So che puoi inserire più righe contemporaneamente, c'è un modo per aggiornare più righe contemporaneamente (come in, in una query) in MySQL?

Modifica: ad esempio ho il seguente

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

Voglio combinare tutti i seguenti aggiornamenti in una query

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;

Risposte:


652

Sì, è possibile: è possibile utilizzare INSERT ... ON DUPLICATE KEY UPDATE.

Usando il tuo esempio:

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
Se non ci sono duplicati, non voglio che quella riga venga inserita. cosa dovrebbe fare id? perché sto recuperando informazioni da un altro sito che mantiene tabelle con ID. Sto inserendo valori rispetto a quell'ID. se il sito ha nuovi record, finirò per inserire solo gli ID e contare tranne tutte le altre informazioni. se e solo se esiste una voce per l'id allora dovrebbe aggiornarsi altrimenti dovrebbe saltare. cosa devo fare?
Jayapal Chandran,

33
Nota: questa risposta presuppone anche che l'ID sia la chiave primaria
JM4

11
@JayapalChandran dovresti usare INSERT IGNORE insieme a ON DUPLICATE KEY UPDATE. dev.mysql.com/doc/refman/5.5/en/insert.html
Haralan Dobrev

16
@HaralanDobrev L'utilizzo di INSERT IGNORE inserisce comunque i record non duplicati. che Jayapal voleva evitare. INSERISCI IGNORA trasforma qualsiasi errore in avvertimento :( stackoverflow.com/questions/548541/…
Takehiro Adachi

2
Questa risposta presuppone che l'ID sia una chiave univoca (può essere primaria come altri hanno detto) ma, soprattutto, presuppone che non vi siano altre chiavi univoche. Se ce ne sono, può lanciare una chiave inglese nelle opere.
Steve Horvath,

130

Poiché hai valori dinamici, devi utilizzare un IF o CASE per le colonne da aggiornare. Diventa un po 'brutto, ma dovrebbe funzionare.

Usando il tuo esempio, potresti farlo come:

UPDATE table SET Col1 = ID CASE 
                          QUANDO 1 POI 1 
                          QUANDO 2 POI 2 
                          QUANDO 4 POI 10 
                          ELSE Col1 
                        FINE, 
                 Col2 = ID CASE 
                          QUANDO 3 POI 3 
                          QUANDO 4 POI 12 
                          ELSE Col2 
                        FINE
             DOVE ID IN (1, 2, 3, 4);

forse non è così carino da scrivere per l'aggiornamento dinamico ma interessante guardare la funzionalità del
case

1
@ user2536953, può essere utile anche per l'aggiornamento dinamico. Ad esempio, ho usato quella soluzione in loop in 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

La domanda è vecchia, ma vorrei estendere l'argomento con un'altra risposta.

Il mio punto è che il modo più semplice per raggiungerlo è semplicemente avvolgere più query con una transazione. La risposta accettata INSERT ... ON DUPLICATE KEY UPDATEè un bel trucco, ma si dovrebbe essere consapevoli dei suoi svantaggi e limitazioni:

  • Come detto, se ti capita di avviare la query con righe le cui chiavi primarie non esistono nella tabella, la query inserisce nuovi record "semi-cotti". Probabilmente non è quello che vuoi
  • Se hai una tabella con un campo non nullo senza valore predefinito e non vuoi toccare questo campo nella query, riceverai un "Field 'fieldname' doesn't have a default value"avviso MySQL anche se non inserisci affatto una singola riga. Ti metterà nei guai, se decidi di essere severo e trasformare gli avvisi mysql in eccezioni di runtime nella tua app.

Ho effettuato alcuni test delle prestazioni per tre delle varianti suggerite, tra cui la INSERT ... ON DUPLICATE KEY UPDATEvariante, una variante con la clausola "case / when / then" e un approccio ingenuo con la transazione. Puoi ottenere il codice Python e i risultati qui . La conclusione generale è che la variante con case case risulta essere due volte più veloce di altre due varianti, ma è abbastanza difficile scrivere un codice corretto e sicuro per l'iniezione, quindi mi attengo personalmente all'approccio più semplice: usare le transazioni.

Modifica: i risultati di Dakusan dimostrano che le mie stime delle prestazioni non sono del tutto valide. Si prega di vedere questa risposta per un'altra ricerca più elaborata.


Usando le transazioni, suggerimento molto bello (e semplice)!
mTorres,

Cosa succede se le mie tabelle non sono di tipo InnoDB?
TomeeNS,

1
Qualcuno potrebbe fornire un link a quali transazioni fare questo aspetto? E / o codice per un codice sicuro per l'iniezione per la variante con l'istruzione case?
François M.,

1
Trovo false le informazioni fornite sulla velocità in questo post. Ne ho scritto in un post qui sotto. stackoverflow.com/questions/3432/multiple-updates-in-mysql/...
Dakusan

1
@Dakusan, ottima risposta. Grazie mille per l'estensione, il commento e la correzione dei miei risultati.
Roman Imankulov,

72

Non sono sicuro del motivo per cui non è stata ancora menzionata un'altra utile opzione:

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
Questo è il migliore. Soprattutto se stai tirando i valori per l'aggiornamento da un'altra query SQL come stavo facendo.
v010dya,

1
Questo è stato ottimo per un aggiornamento su una tabella con un'enorme quantità di colonne. Probabilmente userò questa query molto in futuro. Grazie!
Casper Wilkes,

Ho provato questo tipo di query. Ma quando i record raggiungono 30k il server Boundary si è arrestato. c'è un'altra soluzione?
Bhavin Chauhan,

Questo sembra fantastico. Proverò a combinare questo con una clausola WHERE in cui le chiavi primarie non vengono aggiornate, ma utilizzate per identificare le colonne da modificare.
nl-x

@BhavinChauhan Hai provato a utilizzare una tabella temporanea anziché join-select per aggirare il problema?
nl-x

41

Tutto ciò che segue si applica a InnoDB.

Sento che conoscere le velocità dei 3 diversi metodi è importante.

Esistono 3 metodi:

  1. INSERISCI: INSERIRE con AGGIORNAMENTO CHIAVE SU DUPLICATO
  2. TRANSAZIONE: dove si esegue un aggiornamento per ciascun record all'interno di una transazione
  3. CASO: in cui è un caso / quando per ogni record diverso all'interno di un AGGIORNAMENTO

Ho appena provato questo, e il metodo INSERT è stato 6,7 volte più veloce per me rispetto al metodo TRANSACTION. Ho provato su un set di 3.000 e 30.000 righe.

Il metodo TRANSACTION deve ancora eseguire ogni query individualmente, il che richiede tempo, sebbene in batch i risultati in memoria o qualcosa del genere, durante l'esecuzione. Il metodo TRANSAZIONE è anche piuttosto costoso sia nei registri di replica che di query.

Ancora peggio, il metodo CASE era 41.1x più lento del metodo INSERT con 30.000 record (6.1x più lento di TRANSAZIONE). E 75 volte più lento in MyISAM. I metodi INSERT e CASE hanno battuto anche a ~ 1.000 record. Anche a 100 record, il metodo CASE è MOLTO più veloce.

Quindi, in generale, ritengo che il metodo INSERT sia il migliore e più facile da usare. Le query sono più piccole e più facili da leggere e richiedono solo 1 query di azione. Questo vale sia per InnoDB che per MyISAM.

Bonus:

La soluzione per il problema della non-default-campo INSERT è quello di disattivare temporaneamente le modalità SQL rilevanti: SET SESSION sql_mode=REPLACE(REPLACE(@@SESSION.sql_mode,"STRICT_TRANS_TABLES",""),"STRICT_ALL_TABLES",""). Assicurati di salvare il sql_modeprimo se prevedi di ripristinarlo.

Come per altri commenti, ho visto che dire che auto_increment sale usando il metodo INSERT, questo sembra essere il caso di InnoDB, ma non di MyISAM.

Il codice per eseguire i test è il seguente. Emette anche file .SQL per rimuovere l'overhead dell'interprete 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
Stai facendo il lavoro del Signore qui;) Molto apprezzato.
chili

Testando alcune prestazioni tra GoLang e PHP, usando 40k righe su MariaDB, stavo ottenendo 2 secondi su PHP e più di 6 secondi su Golang .... Beh, mi è sempre stato detto che GoLang avrebbe funzionato più velocemente di PHP !!! Quindi, comincio a chiedermi come migliorare le prestazioni ... Utilizzando INSERT ... ON DUPLICATE KEY UPDATE ... Ho ottenuto 0,74 secondi su Golang e 0,86 secondi su PHP !!!!
Diego Favero,

1
Il punto del mio codice è limitare i risultati di temporizzazione alle istruzioni SQL rigorosamente e non al codice per la lingua o le librerie. GoLang e PHP sono 2 lingue completamente separate pensate per cose completamente diverse. PHP è pensato per un ambiente di scripting a singola esecuzione su un singolo thread con garbage collection principalmente limitata e passiva. GoLang è pensato per applicazioni compilate di lunga durata con raccolta dei rifiuti aggressiva e multithreading come una delle funzionalità del linguaggio principale. Potrebbero a malapena essere più diversi in termini di funzionalità linguistica e ragione. [Continua]
Dakusan

Pertanto, quando si eseguono i test, assicurarsi di limitare le misurazioni della velocità alle rigorose richieste della funzione "Query" per l'istruzione SQL. Confrontare e ottimizzare le altre parti del codice sorgente che non sono strettamente la chiamata alla query è come confrontare mele e arance. Se limiti i tuoi risultati a questo (avendo le stringhe precompilate e pronte per l'uso), i risultati dovrebbero essere molto simili. Eventuali differenze a quel punto sono dovute alla libreria SQL del linguaggio e non necessariamente al linguaggio stesso. Secondo me, la soluzione INSERT ON DUPLICATE era e sarà sempre l'opzione migliore. [Cont]
Dakusan

Per quanto riguarda il tuo commento su GoLang che è più veloce, questa è un'affermazione incredibilmente ampia che non tiene conto di una moltitudine di avvertimenti o sfumature di questi linguaggi e dei loro disegni. Java è un linguaggio interpretato, ma ho scoperto 15 anni fa che in alcuni scenari può quasi eguagliare (e forse anche battere) la C in velocità. E C è un linguaggio compilato e il più comune dei linguaggi di sistema di livello più basso, oltre all'assemblatore. Amo davvero quello che sta facendo GoLang e ha sicuramente la potenza e la fluidità per diventare uno dei sistemi più comuni e ottimizzati [Cont]
Dakusan

9

Utilizzare una tabella temporanea

// 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'

Questo dovrebbe funzionare per te.

C'è un riferimento nel manuale di MySQL per più tabelle.


6

Perché nessuno menziona più istruzioni in una query ?

In php, usi il multi_querymetodo dell'istanza mysqli.

Dal manuale di php

MySQL facoltativamente consente di avere più istruzioni in una stringa di istruzioni. L'invio di più istruzioni contemporaneamente riduce i round trip client-server ma richiede una gestione speciale.

Ecco il risultato rispetto ad altri 3 metodi nell'aggiornamento 30.000 non elaborati. Qui puoi trovare il codice basato sulla risposta di @Dakusan

Transazione: 5.5194580554962
Inserto: 0,20669293403625
Caso: 16.474853992462
Multi: 0,0412278175354

Come puoi vedere, la query a più istruzioni è più efficiente della risposta più alta.

Se ricevi un messaggio di errore come questo:

PHP Warning:  Error while sending SET_OPTION packet

Potrebbe essere necessario aumentare il max_allowed_packetfile di configurazione in mysql presente nella mia macchina /etc/mysql/my.cnfe quindi riavviare mysqld.


Tutti i confronti di seguito vengono eseguiti rispetto al test INSERT. Ho appena eseguito il test nelle stesse condizioni e, senza transazioni, è stato 145 volte più lento su 300 file e 753 volte più lento su 3000 file. Inizialmente avevo iniziato con le 30.000 file, ma sono andato a pranzare e sono tornato ed era ancora in corso. Ciò ha senso poiché l'esecuzione di singole query e lo svuotamento di ciascuna nel database singolarmente sarebbe ridicolmente costoso. Soprattutto con la replica. L'attivazione delle transazioni fa comunque una grande differenza. A 3000 file ci sono volute 1,5 volte di più e a 30.000 file 2,34x . [continua]
Dakusan,

Ma avevi ragione sul fatto che è veloce (con le transazioni). Sia a 3.000 che a 30.000 file era più veloce di tutto tranne che del metodo INSERT. Non c'è assolutamente alcun modo di ottenere risultati migliori eseguendo 1 query di 30.000 query, anche se vengono raggruppate in una chiamata API MySQL speciale. Eseguendo solo 300 righe, è stato MOLTO più veloce di tutti gli altri metodi (con mia sorpresa), che segue circa la stessa curva del grafico del metodo CASE. Questo essere più veloce può essere spiegato in 2 modi. Il primo è che il metodo INSERT essenzialmente inserisce sempre 2 righe a causa di "ON DUPLICATE KEY [cont]
Dakusan,

UPDATE "che causa sia un" INSERT "che un" UPDATE ". L'altro è che è meno lavoro nel processore SQL per la modifica di 1 riga alla volta a causa delle ricerche dell'indice. Non sono sicuro di come tu abbia ottenuto risultati diversi da me, ma il tuo test aggiuntivo sembra solido. In realtà non sono nemmeno sicuro di come la replica gestirà questa chiamata.
Funzionerebbe

Stavo facendo 300 AGGIORNAMENTI alla volta su un tavolo per rivedere un errore all'interno di un ciclo for che ha impiegato 41 secondi. L'inserimento delle stesse query UPDATE in una $mysqli->multi_query($sql)ha richiesto "0" secondi. Tuttavia, le successive interrogazioni fallirono, facendomi diventare un "programma" separato.
Chris K,

Grazie. È stato in grado di aggiornare circa 5k righe (non ho provato più) in un minuto utilizzando più query. Se qualcuno è alla ricerca di una soluzione DOP: stackoverflow.com/questions/6346674/…
Scofield

3

C'è un'impostazione che puoi modificare chiamata "multi statement" che disabilita il "meccanismo di sicurezza" di MySQL implementato per prevenire (più di un) comando di iniezione. Tipico dell'implementazione "brillante" di MySQL, impedisce inoltre all'utente di eseguire query efficienti.

Qui ( http://dev.mysql.com/doc/refman/5.1/en/mysql-set-server-option.html ) sono disponibili alcune informazioni sull'implementazione C dell'impostazione.

Se stai usando PHP, puoi usare mysqli per fare dichiarazioni multiple (penso che php sia stato spedito con mysqli per un po 'di tempo)

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

Spero che aiuti.


4
Ciò equivale a inviare le query separatamente. L'unica differenza è che si invia tutto in un pacchetto di rete, ma gli AGGIORNAMENTI verranno comunque elaborati come query separate. Meglio avvolgerli in una transazione, quindi le modifiche verranno memorizzate nella tabella in una sola volta.
Marki555,

3
Come avvolgerli in una transazione? Mostraci, per favore.
TomeeNS

@TomeeNS Utilizzare mysqli::begin_transaction(..)prima di inviare la query e mysql::commit(..)dopo. O utilizzare START TRANSACTIONcome prima e COMMITcome ultima istruzione nella query stessa.
Juha Palomäki,

3

Puoi alias la stessa tabella per darti l'id che vuoi inserire (se stai facendo un aggiornamento riga per riga:

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

Inoltre, dovrebbe sembrare ovvio che è possibile aggiornare anche da altre tabelle. In questo caso, l'aggiornamento funge anche da un'istruzione "SELECT", che fornisce i dati della tabella che si sta specificando. Stai specificando esplicitamente nella tua query i valori di aggiornamento, quindi la seconda tabella non è interessata.


2

Potresti anche essere interessato a utilizzare i join sugli aggiornamenti, il che è possibile anche.

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.

Modifica: se i valori che stai aggiornando non provengono da un'altra parte del database, dovrai inviare più query di aggiornamento.


1

E ora il modo semplice

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

uso

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

Notare che:

  • id deve essere una chiave univoca primaria
  • se si utilizzano chiavi esterne per fare riferimento alla tabella, REPLACE elimina, quindi inserisce, pertanto ciò potrebbe causare un errore

-3

Quanto segue aggiornerà tutte le righe in una tabella

Update Table Set
Column1 = 'New Value'

Il prossimo aggiornerà tutte le righe in cui il valore di Column2 è superiore a 5

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

C'è tutto l'esempio di Unkwntech sull'aggiornamento di più di una tabella

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

-3

Sì .. è possibile utilizzando INSERT ON DUPLICATE KEY UPDATE istruzione sql .. sintassi: INSERT INTO table_name (a, b, c) VALUES (1,2,3), (4,5,6) ON DUPLICATE KEY UPDATE a = VALORI (a), b = VALORI (b), c = VALORI (c)


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

Questo dovrebbe raggiungere quello che stai cercando. Aggiungi solo altri ID. L'ho provato.


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

// Lo stai costruendo in php come

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

Quindi puoi aggiornare la tabella dei fori con una query


Non ho downvote, ma penso che l'obiezione è quello di fare il set, quando non è necessaria (e si sta ancora facendo, quando si imposta somethinga something)
v010dya
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.