Múltiples actualizaciones en MySQL

388

Sé que puede insertar varias filas a la vez, ¿hay alguna manera de actualizar varias filas a la vez (como en una consulta) en MySQL?

Editar: por ejemplo, tengo lo siguiente

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

Quiero combinar todas las siguientes actualizaciones en una consulta

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;
Teifion
fuente

Respuestas:

652

Sí, eso es posible: puede usar INSERT ... ON DUPLICATE KEY UPDATE.

Usando tu ejemplo:

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);
Michiel de Mare
fuente
22
Si no hay duplicados, entonces no quiero que se inserte esa fila. ¿Qué debería hacer id? porque estoy obteniendo información de otro sitio que mantiene tablas con id. Estoy insertando valores con respecto a esa identificación. si el sitio tiene nuevos registros, terminaré insertando solo los identificadores y el conteo, excepto toda otra información. si y solo si hay una entrada para la identificación, debería actualizarse; de ​​lo contrario, debería omitirse. ¿Qué debo hacer?
Jayapal Chandran
33
Nota: esta respuesta también supone que la identificación es la clave principal
JM4
11
@JayapalChandran debe usar INSERT IGNORE junto con ON DUPLICATE KEY UPDATE. dev.mysql.com/doc/refman/5.5/en/insert.html
Haralan Dobrev
16
@HaralanDobrev El uso de INSERT IGNORE todavía inserta los registros no duplicados. que Jayapal quería evitar. INSERT IGNORE solo convierte cualquier error en advertencia :( stackoverflow.com/questions/548541/…
Takehiro Adachi
2
Esta respuesta asume que la ID es una clave única (puede ser primaria como otros dicen) pero lo más importante es que no hay otras claves únicas. Si hay alguno, puede arrojar una llave en las obras.
Steve Horvath
130

Como tiene valores dinámicos, debe usar un IF o CASE para que las columnas se actualicen. Se pone un poco feo, pero debería funcionar.

Usando su ejemplo, podría hacerlo como:

ACTUALIZAR tabla SET Col1 = ID DE CASO 
                          CUANDO 1 ENTONCES 1 
                          CUANDO 2 ENTONCES 2 
                          CUANDO 4 ENTONCES 10 
                          ELSE Col1 
                        FINAL, 
                 Col2 = ID del CASO 
                          CUANDO 3 ENTONCES 3 
                          CUANDO 4 ENTONCES 12 
                          ELSE Col2 
                        FINAL
             WHERE id IN (1, 2, 3, 4);
Harrison Fisk
fuente
tal vez no sea tan bonito para escribir para una actualización dinámica, pero una mirada interesante a la funcionalidad de la carcasa ...
#
1
@ user2536953, también puede ser bueno para la actualización dinámica. Por ejemplo, usé esa solución en bucle en 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 pregunta es antigua, pero me gustaría ampliar el tema con otra respuesta.

Mi punto es que la forma más fácil de lograrlo es simplemente envolver múltiples consultas con una transacción. La respuesta aceptada INSERT ... ON DUPLICATE KEY UPDATEes un buen truco, pero uno debe tener en cuenta sus inconvenientes y limitaciones:

  • Como se dijo, si inicia la consulta con filas cuyas claves primarias no existen en la tabla, la consulta inserta nuevos registros "a medias". Probablemente no es lo que quieres
  • Si tiene una tabla con un campo no nulo sin valor predeterminado y no desea tocar este campo en la consulta, recibirá una "Field 'fieldname' doesn't have a default value"advertencia de MySQL incluso si no inserta una sola fila. Te meterá en problemas si decides ser estricto y convertir las advertencias de mysql en excepciones de tiempo de ejecución en tu aplicación.

Hice algunas pruebas de rendimiento para tres de las variantes sugeridas, incluida la INSERT ... ON DUPLICATE KEY UPDATEvariante, una variante con la cláusula "caso / cuándo / entonces" y un enfoque ingenuo con la transacción. Puede obtener el código de Python y los resultados aquí . La conclusión general es que la variante con declaración de caso resulta ser el doble de rápida que otras dos variantes, pero es bastante difícil escribir un código correcto y seguro para la inyección, por lo que personalmente me quedo con el enfoque más simple: usar transacciones.

Editar: Los resultados de Dakusan prueban que mis estimaciones de rendimiento no son del todo válidas. Vea esta respuesta para otra investigación más elaborada.

Roman Imankulov
fuente
Uso de transacciones, muy agradable (y simple) consejo!
mTorres
¿Qué pasa si mis tablas no son de tipo InnoDB?
TomeeNS
1
¿Podría alguien proporcionar un enlace a cómo se verán estas transacciones? ¿Y / o código para un código de inyección segura para la variante con declaración de caso?
François M.
1
Encuentro que la información dada sobre la velocidad en esta publicación es falsa. Escribí sobre esto en una publicación a continuación. stackoverflow.com/questions/3432/multiple-updates-in-mysql/…
Dakusan
1
@Dakusan, gran respuesta. Muchas gracias por extender, comentar y corregir mis resultados.
Roman Imankulov
72

No estoy seguro de por qué todavía no se menciona otra opción útil:

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;
nuevo
fuente
44
Esto es lo mejor. Especialmente si está extrayendo los valores para actualizar desde otra consulta SQL como lo estaba haciendo.
v010dya
1
Esto fue genial para una actualización en una tabla con una gran cantidad de columnas. Probablemente usaré esta consulta mucho en el futuro. ¡Gracias!
Casper Wilkes
He intentado este tipo de consulta. Pero cuando los registros alcanzan los 30k, el servidor de límite se detiene. hay alguna otra solucion?
Bhavin Chauhan
Esto se ve genial. Intentaré combinar esto con una cláusula WHERE donde las claves primarias no se actualicen, sino que se usen para identificar las columnas a cambiar.
nl-x
@BhavinChauhan ¿Has intentado usar una tabla temporal en lugar de la combinación de selección para evitar el problema?
nl-x
41

Todo lo siguiente se aplica a InnoDB.

Siento que conocer las velocidades de los 3 métodos diferentes es importante.

Hay 3 métodos:

  1. INSERTAR: INSERTAR CON ACTUALIZACIÓN DE LLAVE DUPLICADA
  2. TRANSACCIÓN: cuando realiza una actualización para cada registro dentro de una transacción
  3. CASO: en el cual usted es un caso / cuándo para cada registro diferente dentro de una ACTUALIZACIÓN

Acabo de probar esto, y el método INSERT fue 6.7 veces más rápido para mí que el método TRANSACTION. Probé en un conjunto de 3.000 y 30.000 filas.

El método TRANSACTION todavía tiene que ejecutar cada consulta individualmente, lo que lleva tiempo, aunque agrupa los resultados en la memoria, o algo así, mientras se ejecuta. El método de TRANSACCIÓN también es bastante costoso tanto en la réplica como en los registros de consultas.

Peor aún, el método CASE fue 41.1x más lento que el método INSERT con 30,000 registros (6.1x más lento que TRANSACTION). Y 75 veces más lento en MyISAM. Los métodos INSERT y CASE rompieron incluso a ~ 1,000 registros. Incluso con 100 registros, el método CASE es MUCHO más rápido.

Entonces, en general, creo que el método INSERT es el mejor y el más fácil de usar. Las consultas son más pequeñas y fáciles de leer y solo requieren 1 consulta de acción. Esto se aplica tanto a InnoDB como a MyISAM.

Cosas de bonificación:

La solución para el problema de la no-default-campo es INSERT para desactivar temporalmente los modos SQL pertinentes: SET SESSION sql_mode=REPLACE(REPLACE(@@SESSION.sql_mode,"STRICT_TRANS_TABLES",""),"STRICT_ALL_TABLES",""). Asegúrese de guardar el sql_modeprimero si planea revertirlo.

En cuanto a otros comentarios que he visto que dicen que el auto_increment aumenta usando el método INSERT, este parece ser el caso en InnoDB, pero no en MyISAM.

El código para ejecutar las pruebas es el siguiente. También genera archivos .SQL para eliminar la sobrecarga del intérprete 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).';');
}
Dakusan
fuente
1
Estás haciendo la obra del Señor aquí;) Muy apreciado.
chile
Al probar algo de rendimiento entre GoLang y PHP, usando 40k filas en MariaDB, estaba obteniendo 2 segundos en PHP y más de 6 segundos en golang ... Bueno, ¡siempre me dijeron que GoLang funcionaría más rápido que PHP! Entonces, empiezo a preguntarme cómo mejorar el rendimiento ... Usando INSERT ... EN LA ACTUALIZACIÓN DE LA CLAVE DUPLICADA ... ¡Obtuve 0.74 segundos en Golang y 0.86 segundos en PHP!
Diego Favero el
1
El objetivo de mi código es limitar los resultados de tiempo estrictamente a las declaraciones SQL, y no el código para el lenguaje o las bibliotecas. GoLang y PHP son 2 lenguajes completamente separados destinados a cosas completamente diferentes. PHP está diseñado para un entorno de secuencias de comandos de ejecución única en un solo subproceso con recolección de basura principalmente limitada y pasiva. GoLang está diseñado para aplicaciones compiladas de larga ejecución con recolección de basura agresiva y subprocesamiento múltiple como una de las características principales del lenguaje. Apenas podrían ser más diferentes en términos de funcionalidad y razón del lenguaje. [Continúa]
Dakusan
Por lo tanto, cuando ejecute sus pruebas, asegúrese de limitar las mediciones de velocidad estrictamente a la función "Consulta" para la instrucción SQL. Comparar y optimizar las otras partes del código fuente que no son estrictamente llamadas de consulta es como comparar manzanas y naranjas. Si limita sus resultados a esto (con las cadenas precompiladas y listas para usar), entonces los resultados deberían ser muy similares. Cualquier diferencia en ese punto es culpa de la biblioteca SQL del lenguaje, y no necesariamente del lenguaje en sí. En mi opinión, la solución INSERT ON DUPLICATE fue y siempre será la mejor opción. [Cont]
Dakusan
En cuanto a su comentario sobre que GoLang es más rápido, es una declaración increíblemente amplia que no tiene en cuenta ninguna de las múltiples advertencias o matices de estos idiomas y sus diseños. Java es un lenguaje interpretado, pero descubrí hace 15 años que en realidad casi puede igualar (y quizás incluso a veces vencer) C en velocidad en ciertos escenarios. Y C es un lenguaje compilado, y el más común de los lenguajes de sistema de nivel más bajo, además del ensamblador. Realmente me encanta lo que está haciendo GoLang y definitivamente tiene el poder y la fluidez para convertirse en uno de los sistemas más comunes y optimizados [Cont]
Dakusan
9

Usa una mesa temporal

// 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`");
}
Laymain
fuente
8
UPDATE table1, table2 SET table1.col1='value', table2.col1='value' WHERE table1.col3='567' AND table2.col6='567'

Esto debería funcionar para ti.

Hay una referencia en el manual de MySQL para varias tablas.

UnkwnTech
fuente
6

¿Por qué nadie menciona múltiples declaraciones en una consulta ?

En php, utiliza el multi_querymétodo de instancia mysqli.

Del manual de php

MySQL opcionalmente permite tener múltiples declaraciones en una cadena de declaración. El envío de varias declaraciones a la vez reduce los viajes de ida y vuelta cliente-servidor, pero requiere un manejo especial.

Aquí está el resultado en comparación con otros 3 métodos en la actualización 30,000 sin formato. El código se puede encontrar aquí, que se basa en la respuesta de @Dakusan

Transacción: 5.5194580554962
Insertar: 0.20669293403625
Caso: 16.474853992462
Multi: 0.0412278175354

Como puede ver, la consulta de múltiples declaraciones es más eficiente que la respuesta más alta.

Si recibe un mensaje de error como este:

PHP Warning:  Error while sending SET_OPTION packet

Es posible que deba aumentar el max_allowed_packetarchivo de configuración in mysql que está en mi máquina /etc/mysql/my.cnfy luego reiniciar mysqld.

mononoke
fuente
Todas las comparaciones a continuación se comparan con la prueba INSERT. Acabo de ejecutar la prueba en las mismas condiciones y, sin transacciones, fue 145 veces más lento en 300 filas y 753 veces más lento en 3000 filas. Originalmente había comenzado con las 30,000 filas, pero fui a prepararme el almuerzo y regresé y todavía estaba funcionando. Esto tiene sentido ya que ejecutar consultas individuales y vaciar cada una a la base de datos individualmente sería ridículamente costoso. Especialmente con replicación. Sin embargo, activar las transacciones hace una gran diferencia. En 3,000 filas tomó 1.5x más y en 30,000 filas 2.34x . [continuación]
Dakusan
Pero tenías razón acerca de que es rápido (con transacciones). Tanto en 3,000 como en 30,000 filas fue más rápido que todo excepto el método INSERT. No hay absolutamente ninguna manera de obtener mejores resultados al ejecutar 1 consulta que 30,000 consultas, incluso si están agrupadas en una llamada especial de API MySQL. Con solo 300 filas, fue MUCHO más rápido que todos los demás métodos (para mi sorpresa), que sigue aproximadamente la misma curva gráfica que el método CASE. Este ser más rápido puede explicarse de 2 maneras. El primero es que el método INSERT esencialmente siempre inserta 2 filas debido a la "CLAVE DUPLICADA [cont]
Dakusan
ACTUALIZAR "provocando tanto un" INSERTAR "como un" ACTUALIZAR ". El otro es que es menos trabajo en el procesador SQL para editar solo 1 fila a la vez debido a las búsquedas de índice. No estoy seguro de cómo obtuviste resultados diferentes a los de mí, pero su prueba adicional parece sólida. De hecho, ni siquiera estoy seguro de cómo la replicación manejaría esta llamada. Esto también funcionaría para hacer llamadas ACTUALIZADAS. Insertar llamadas SIEMPRE será más rápido con la única consulta INSERTAR
Dakusan
Estaba haciendo 300 ACTUALIZACIONES a la vez en una tabla para revisar un error dentro de un ciclo for que tardó 41 segundos. Poner las mismas consultas de ACTUALIZACIÓN en una $mysqli->multi_query($sql)tomó "0" segundos. Sin embargo, las consultas posteriores fallaron, lo que hizo que este fuera un "programa" separado.
Chris K
Gracias. Pude actualizar aproximadamente 5k filas (no probé más) en un minuto usando múltiples consultas. Si alguien está buscando una solución PDO: stackoverflow.com/questions/6346674/…
Scofield
3

Hay una configuración que puede modificar llamada 'declaración múltiple' que deshabilita el 'mecanismo de seguridad' de MySQL implementado para prevenir (más de un) comando de inyección. Típico de la implementación 'brillante' de MySQL, también evita que el usuario realice consultas eficientes.

Aquí ( http://dev.mysql.com/doc/refman/5.1/en/mysql-set-server-option.html ) hay información sobre la implementación en C de la configuración.

Si está usando PHP, puede usar mysqli para hacer múltiples declaraciones (creo que php se ha enviado con mysqli por un tiempo ahora)

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

Espero que ayude.

Arroyos
fuente
44
Esto es lo mismo que enviar las consultas por separado. La única diferencia es que lo envía todo en un paquete de red, pero las ACTUALIZACIONES aún se procesarán como consultas separadas. Lo mejor es envolverlos en una transacción, luego los cambios se confirmarán en la tabla a la vez.
Marki555
3
¿Cómo envolverlos en una transacción? Muéstranos, por favor.
TomeeNS
@TomeeNS Use mysqli::begin_transaction(..)antes de enviar la consulta y mysql::commit(..)después. O use START TRANSACTIONcomo primera y COMMITúltima declaración en la consulta misma.
Juha Palomäki el
3

Puede crear un alias en la misma tabla para proporcionarle los identificadores que desea insertar (si está realizando una actualización fila por fila:

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

Además, debería parecer obvio que también puede actualizar desde otras tablas. En este caso, la actualización se dobla como una instrucción "SELECCIONAR", proporcionándole los datos de la tabla que está especificando. Está indicando explícitamente en su consulta los valores de actualización, por lo que la segunda tabla no se ve afectada.

Eggmatters
fuente
2

También puede estar interesado en usar combinaciones en las actualizaciones, que también es posible.

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.

Editar: si los valores que está actualizando no provienen de otro lugar de la base de datos, deberá emitir múltiples consultas de actualización.

Shawn
fuente
1

Y ahora la manera fácil

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
Stan Sokolov
fuente
-1

utilizar

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

Tenga en cuenta:

  • ID tiene que ser una clave única primaria
  • si usa claves externas para hacer referencia a la tabla, REPLACE elimina y luego inserta, por lo que esto podría causar un error
Justin Levene
fuente
-3

Lo siguiente actualizará todas las filas en una tabla

Update Table Set
Column1 = 'New Value'

El siguiente actualizará todas las filas donde el valor de Column2 es más de 5

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

Hay todos los ejemplos de Unkwntech de actualizar más de una tabla

UPDATE table1, table2 SET
table1.col1 = 'value',
table2.col1 = 'value'
WHERE
table1.col3 = '567'
AND table2.col6='567'
GateKiller
fuente
-3

Sí ... es posible usando INSERTAR EN LA ACTUALIZACIÓN DE CLAVE DUPLICADA declaración sql ... sintaxis: INSERTAR EN nombre_tabla (a, b, c) VALORES (1,2,3), (4,5,6) EN LA ACTUALIZACIÓN DE CLAVE DUPLICADA a = VALORES (a), b = VALORES (b), c = VALORES (c)

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

Esto debería lograr lo que estás buscando. Simplemente agregue más id. Lo he probado

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

// Simplemente lo construyes en php como

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

Para que pueda actualizar la tabla de agujeros con una consulta

usuario2082581
fuente
No lo hice downvote, pero creo que la objeción es a hacer el conjunto, cuando no se necesita (y todavía lo están haciendo, cuando se está configurando somethinga something)
v010dya