¿Cómo ACTUALIZO una fila en una tabla o la INSERTO si no existe?

83

Tengo la siguiente tabla de contadores:

CREATE TABLE cache (
    key text PRIMARY KEY,
    generation int
);

Me gustaría incrementar uno de los contadores o ponerlo en cero si la fila correspondiente aún no existe. ¿Hay alguna manera de hacer esto sin problemas de concurrencia en SQL estándar? La operación a veces es parte de una transacción, a veces por separado.

El SQL debe ejecutarse sin modificaciones en SQLite, PostgreSQL y MySQL, si es posible.

Una búsqueda arrojó varias ideas que sufren problemas de concurrencia o son específicas de una base de datos:

  • Intente con INSERTuna nueva fila y UPDATEsi hubo un error. Desafortunadamente, el error de INSERTaborta la transacción actual.

  • UPDATEla fila, y si no se modificaron filas, INSERTuna nueva fila.

  • MySQL tiene una ON DUPLICATE KEY UPDATEcláusula.

EDITAR: Gracias por todas las excelentes respuestas. Parece que Paul tiene razón y no hay una forma única y portátil de hacer esto. Eso me sorprende bastante, ya que parece una operación muy básica.

Remy en blanco
fuente
6
No encontrará una única solución que funcione para todos estos RDBMS. Lo siento.
Paul Tomblin
1
posible duplicado de SQLite - UPSERT * not * INSERT or REPLACE
PearsonArtPhoto
posible duplicado de Soluciones para INSERTAR O ACTUALIZAR en SQL Server
Jonathan Leffler

Respuestas:

137

MySQL (y posteriormente SQLite) también admiten la sintaxis REPLACE INTO:

REPLACE INTO my_table (pk_id, col1) VALUES (5, '123');

Esto identifica automáticamente la clave principal y encuentra una fila coincidente para actualizar, insertando una nueva si no se encuentra ninguna.

andygeers
fuente
13
En realidad, para ser específico, REPLACE de MySQL siempre hace una inserción, pero primero eliminará la fila si ya existe. dev.mysql.com/doc/refman/4.1/en/replace.html
Evan
90
Es importante entender que se trata de insertar + eliminar y nunca y actualizar. La consecuencia de esto es que siempre querrá asegurarse de que cuando realice un reemplazo, siempre debe incluir datos para todos los campos.
Zoredache
2
@Zoredache Técnicamente, es un 'eliminar, luego insertar', ya que un (n) 'insertar + eliminar' es efectivamente lo mismo que eliminar, pero eso es dividir los pelos.
Agi Hammerthief
2
@Agihammerthief hay una diferencia muy real, es decir, que la fila recién insertada NO tendrá la misma clave principal que la fila que se ha eliminado. Con ON DUPLICATE será la misma clave primaria (a menos que la cambie específicamente).
Tim Strijdhorst
32

SQLite admite reemplazar una fila si ya existe:

INSERT OR REPLACE INTO [...blah...]

Puedes acortar esto a

REPLACE INTO [...blah...]

Este atajo se agregó para que sea compatible con la REPLACE INTOexpresión MySQL .

Kyle Cronin
fuente
Requiere que hayas definido un PRAMARY KEYen tus valores.
DawnSong
24

Haría algo como lo siguiente:

INSERT INTO cache VALUES (key, generation)
ON DUPLICATE KEY UPDATE (key = key, generation = generation + 1);

Estableciendo el valor de generación en 0 en el código o en el sql pero usando el ON DUP ... para incrementar el valor. Creo que esa es la sintaxis de todos modos.

jmoz
fuente
1
Honestamente, esta respuesta debería ser la respuesta seleccionada. Es una edición no destructiva si no envía todos los campos a una entrada.
Jordan
9

la cláusula ON DUPLICATE KEY UPDATE es la mejor solución porque: REPLACE hace un DELETE seguido de un INSERT, por lo que durante un período muy breve, el registro se elimina, lo que crea la mínima posibilidad de que una consulta pueda volver después de haber saltado eso si la página se visto durante la consulta REPLACE.

Prefiero INSERT ... ON DUPLICATE UPDATE ... por esa razón.

La solución de jmoz es la mejor: aunque prefiero la sintaxis SET a los paréntesis

INSERT INTO cache 
SET key = 'key', generation = 'generation'
ON DUPLICATE KEY 
UPDATE key = 'key', generation = (generation + 1)
;
Cuervo de fuego
fuente
4
REPLACE es atómico, por lo que no hay un período en el que no exista la fila.
Brilliand
5

En PostgreSQL no hay un comando de fusión, y escribirlo no es trivial; en realidad, hay casos extremos extraños que hacen que la tarea sea "interesante".

El mejor enfoque (como en: trabajar en las condiciones más posibles) es usar la función, como la que se muestra en manual (merge_db).

Si no desea utilizar la función, generalmente puede salirse con la suya:

updated = db.execute(UPDATE ... RETURNING 1)
if (!updated)
  db.execute(INSERT...)

Sólo recuerde que no es culpa la prueba y que se fallará eventualmente.


fuente
4

SQL estándar proporciona la instrucción MERGE para esta tarea. No todos los DBMS admiten la declaración MERGE.

Jonathan Leffler
fuente
0

Si no tiene una forma común de actualizar o insertar atómicamente (por ejemplo, a través de una transacción), puede recurrir a otro esquema de bloqueo. Un archivo de 0 bytes, exclusión mutua del sistema, canalización con nombre, etc.

Ella a
fuente
0

¿Podrías usar un disparador de inserción? Si falla, realice una actualización.

Michael Todd
fuente
Trigger (al menos en PostgreSQL) se está ejecutando cuando el comando funcionó. es decir, no puede tener un disparador que se ejecute cuando el comando base falló.
0

Si está de acuerdo con el uso de una biblioteca que escribe el SQL por usted, entonces puede usar Upsert (actualmente Ruby y Python solamente):

Pet.upsert({:name => 'Jerry'}, :breed => 'beagle')
Pet.upsert({:name => 'Jerry'}, :color => 'brown')

Eso funciona en MySQL, Postgres y SQLite3.

Escribe un procedimiento almacenado o una función definida por el usuario (UDF) en MySQL y Postgres. Se utiliza INSERT OR REPLACEen SQLite3.

Seamus Abshere
fuente