Oracle: cómo UPSERT (¿actualizar o insertar en una tabla?)

293

La operación UPSERT actualiza o inserta una fila en una tabla, dependiendo de si la tabla ya tiene una fila que coincida con los datos:

if table t has a row exists that has key X:
    update t set mystuff... where mykey=X
else
    insert into t mystuff...

Dado que Oracle no tiene una declaración UPSERT específica, ¿cuál es la mejor manera de hacer esto?

Mark Harrison
fuente

Respuestas:

60

Una alternativa a MERGE (la "forma antigua"):

begin
   insert into t (mykey, mystuff) 
      values ('X', 123);
exception
   when dup_val_on_index then
      update t 
      set    mystuff = 123 
      where  mykey = 'X';
end;   
Tony Andrews
fuente
3
@chotchki: ¿en serio? Una explicación sería útil.
Tony Andrews
15
El problema es que tiene una ventana entre la inserción y la actualización donde otro proceso podría desencadenar una eliminación con éxito. Sin embargo, utilicé este patrón en una tabla que nunca tiene eliminaciones disparadas contra él.
chotchki
2
OK estoy de acuerdo. No sé por qué no era obvio para mí.
Tony Andrews
44
No estoy de acuerdo con Chotchki. "Duración de bloqueo: todos los bloqueos adquiridos por las declaraciones dentro de una transacción se mantienen durante la transacción, evitando interferencias destructivas, incluidas lecturas sucias, actualizaciones perdidas y operaciones DDL destructivas de transacciones concurrentes". Souce: enlace
yohannc
55
@yohannc: Creo que el punto es que no hemos adquirido ningún bloqueo simplemente al intentar y no insertar una fila.
Tony Andrews
211

La instrucción MERGE combina datos entre dos tablas. Usar DUAL nos permite usar este comando. Tenga en cuenta que esto no está protegido contra el acceso concurrente.

create or replace
procedure ups(xa number)
as
begin
    merge into mergetest m using dual on (a = xa)
         when not matched then insert (a,b) values (xa,1)
             when matched then update set b = b+1;
end ups;
/
drop table mergetest;
create table mergetest(a number, b number);
call ups(10);
call ups(10);
call ups(20);
select * from mergetest;

A                      B
---------------------- ----------------------
10                     2
20                     1
Mark Harrison
fuente
57
Aparentemente, la declaración "fusionarse" no es atómica. Puede resultar en "ORA-0001: restricción única" cuando se usa simultáneamente. La verificación de la existencia de una coincidencia y la inserción de un nuevo registro no están protegidas por un candado, por lo que existe una condición de carrera. Para hacer esto de manera confiable, debe detectar esta excepción y volver a ejecutar la fusión o hacer una actualización simple. En Oracle 10, puede usar la cláusula "errores de registro" para hacer que continúe con el resto de las filas cuando se produce un error y registrar la fila infractora en otra tabla, en lugar de simplemente detenerse.
Tim Sylvester
1
Hola, intenté usar el mismo patrón de consulta en mi consulta, pero de alguna manera mi consulta está insertando filas duplicadas. No puedo encontrar más información sobre la tabla DUAL. ¿Alguien puede decirme dónde puedo obtener información de DUAL y también sobre la sintaxis de fusión?
Shekhar
55
@Shekhar Dual es una tabla ficticia con una sola fila y columna adp-gmbh.ch/ora/misc/dual.html
YogoZuno
77
@TimSylvester: Oracle utiliza transacciones, por lo que garantiza que la instantánea de los datos al inicio de una transacción sea coherente a lo largo de la transacción, salvo los cambios realizados en ella. Las llamadas concurrentes a la base de datos usan la pila de deshacer; por lo tanto, Oracle administrará el estado final según el orden en que se iniciaron / completaron las transacciones concurrentes. Por lo tanto, nunca tendrá una condición de carrera si se realiza una comprobación de restricciones antes de la inserción, independientemente de cuántas llamadas concurrentes se realicen al mismo código SQL. En el peor de los casos, puede obtener mucha contención y Oracle tardará mucho más en llegar a un estado final.
Neo
2
@RandyMagruder ¿Es el caso de que es 2015 y todavía no podemos hacer un upsert confiablemente en Oracle! ¿Conoces una solución segura concurrente?
dan b
105

El ejemplo doble anterior que está en PL / SQL fue genial porque quería hacer algo similar, pero lo quería del lado del cliente ... así que aquí está el SQL que utilicé para enviar una declaración similar directamente desde algún C #

MERGE INTO Employee USING dual ON ( "id"=2097153 )
WHEN MATCHED THEN UPDATE SET "last"="smith" , "name"="john"
WHEN NOT MATCHED THEN INSERT ("id","last","name") 
    VALUES ( 2097153,"smith", "john" )

Sin embargo, desde una perspectiva de C #, esto proporciona ser más lento que hacer la actualización y ver si las filas afectadas fueron 0 y hacer la inserción si lo fue.

MyDeveloperDay
fuente
10
He vuelto aquí para ver este patrón nuevamente. Falla silenciosamente cuando se intentan inserciones concurrentes. Una inserción surte efecto, la segunda fusión no inserta ni actualiza. Sin embargo, el enfoque más rápido de hacer dos declaraciones separadas es seguro.
Synesso
3
los novatos de oralcle como yo pueden preguntar qué es esta tabla dual , ver esto: stackoverflow.com/q/73751/808698
Hajo Thelen
55
Lástima que con este patrón necesitemos escribir dos veces los datos (John, Smith ...). En este caso, no gano nada usando MERGE, y prefiero usar mucho más simple DELETEentonces INSERT.
Nicolas Barbulesco
@NicolasBarbulesco esta respuesta no necesita escribir los datos dos veces: stackoverflow.com/a/4015315/8307814
whyer
@NicolasBarbulescoMERGE INTO mytable d USING (SELECT 1 id, 'x' name from dual) s ON (d.id = s.id) WHEN MATCHED THEN UPDATE SET d.name = s.name WHEN NOT MATCHED THEN INSERT (id, name) VALUES (s.id, s.name);
whyer
46

Otra alternativa sin la verificación de excepción:

UPDATE tablename
    SET val1 = in_val1,
        val2 = in_val2
    WHERE val3 = in_val3;

IF ( sql%rowcount = 0 )
    THEN
    INSERT INTO tablename
        VALUES (in_val1, in_val2, in_val3);
END IF;
Brian Schmitt
fuente
Su solución provista no funciona para mí. ¿% Rowcount solo funciona con cursores explícitos?
Synesso
¿Qué sucede si la actualización devolvió 0 filas modificadas porque el registro ya estaba allí y los valores eran los mismos?
Adriano Varoli Piazza
10
@Adriano: sql% rowcount aún devolverá> 0 si la cláusula WHERE coincide con alguna fila, incluso si la actualización en realidad no cambia ningún dato en esas filas.
Tony Andrews
No funciona: PLS-00207: el identificador 'COUNT', aplicado al cursor implícito SQL, no es un atributo legal del cursor
Patrik Beck
Errores de sintaxis aquí :(
ilmirons
27
  1. insertar si no existe
  2. actualizar:
    
INSERTAR EN mytable (id1, t1) 
  SELECCIONE 11, 'x1' DE DUAL 
  DONDE NO EXISTE (SELECCIONE id1 de mytble WHERE id1 = 11); 

ACTUALIZAR mytable SET t1 = 'x1' DONDE id1 = 11;
prueba1
fuente
26

Ninguna de las respuestas dadas hasta ahora es segura frente a los accesos concurrentes , como se señaló en el comentario de Tim Sylvester, y generará excepciones en caso de carreras. Para solucionarlo, el combo de inserción / actualización debe estar envuelto en algún tipo de declaración de bucle, de modo que en caso de una excepción se vuelva a intentar todo.

Como ejemplo, así es como el código de Grommit se puede envolver en un bucle para que sea seguro cuando se ejecuta simultáneamente:

PROCEDURE MyProc (
 ...
) IS
BEGIN
 LOOP
  BEGIN
    MERGE INTO Employee USING dual ON ( "id"=2097153 )
      WHEN MATCHED THEN UPDATE SET "last"="smith" , "name"="john"
      WHEN NOT MATCHED THEN INSERT ("id","last","name") 
        VALUES ( 2097153,"smith", "john" );
    EXIT; -- success? -> exit loop
  EXCEPTION
    WHEN NO_DATA_FOUND THEN -- the entry was concurrently deleted
      NULL; -- exception? -> no op, i.e. continue looping
    WHEN DUP_VAL_ON_INDEX THEN -- an entry was concurrently inserted
      NULL; -- exception? -> no op, i.e. continue looping
  END;
 END LOOP;
END; 

Nota: en el modo de transacción SERIALIZABLE, que no recomiendo por cierto, es posible que se encuentre con ORA-08177: en su lugar, no puede serializar el acceso para las excepciones de esta transacción .

Eugene Beresovsky
fuente
3
¡Excelente! Finalmente, una respuesta segura de accesos concurrentes. ¿Alguna forma de usar tal construcción desde un cliente (por ejemplo, desde un cliente Java)?
Sebien
1
¿Quiere decir que no tiene que llamar a un proceso almacenado? Bueno, en ese caso, también podría capturar las excepciones Java específicas y volver a intentar en un bucle Java. Es mucho más conveniente en Java que el SQL de Oracle.
Eugene Beresovsky
Lo siento: no fui lo suficientemente específico. Pero entendiste el camino correcto. Renuncié a hacer lo que acabas de decir. Pero no estoy 100% satisfecho porque genera más consultas SQL, más viajes de ida y vuelta cliente / servidor. No es una buena solución en cuanto al rendimiento. Pero mi objetivo es dejar que los desarrolladores de Java de mi proyecto usen mi método para insertar en cualquier tabla (no puedo crear un procedimiento almacenado PLSQL por tabla, o un procedimiento por tipo de inserción).
Sebien
@Sebien Estoy de acuerdo, sería mejor encapsularlo en el ámbito de SQL, y creo que puedes hacerlo. Simplemente no me ofrezco para resolverlo ... :) Además, en realidad, estas excepciones probablemente ocurrirán menos de una vez en una luna azul, por lo que no debería ver un impacto en el rendimiento en el 99.9% de los casos. Excepto cuando se hacen pruebas de carga, por supuesto ...
Eugene Beresovsky
24

Me gustaría la respuesta de Grommit, excepto que requiere valores de duplicación. Encontré una solución donde puede aparecer una vez: http://forums.devshed.com/showpost.php?p=1182653&postcount=2

MERGE INTO KBS.NUFUS_MUHTARLIK B
USING (
    SELECT '028-01' CILT, '25' SAYFA, '6' KUTUK, '46603404838' MERNIS_NO
    FROM DUAL
) E
ON (B.MERNIS_NO = E.MERNIS_NO)
WHEN MATCHED THEN
    UPDATE SET B.CILT = E.CILT, B.SAYFA = E.SAYFA, B.KUTUK = E.KUTUK
WHEN NOT MATCHED THEN
    INSERT (  CILT,   SAYFA,   KUTUK,   MERNIS_NO)
    VALUES (E.CILT, E.SAYFA, E.KUTUK, E.MERNIS_NO); 
Hubbitus
fuente
2
Quiso decir INSERT (B.CILT, B.SAYFA, B.KUTUK, B.MERNIS_NO) VALUES (E.CILT, E.SAYFA, E.KUTUK, E.MERNIS_NO); ?
Matteo
Por supuesto. Gracias. Fijo.
Hubbitus
¡Afortunadamente editaste tu respuesta! :) mi edición fue tristemente rechazar stackoverflow.com/review/suggested-edits/7555674
Matteo
9

Una nota sobre las dos soluciones que sugieren:

1) Insertar, si es una excepción, luego actualizar,

o

2) Actualización, si sql% rowcount = 0 luego inserte

La cuestión de si insertar o actualizar primero también depende de la aplicación. ¿Esperas más inserciones o más actualizaciones? El que tiene más probabilidades de tener éxito debe ir primero.

Si elige el incorrecto, obtendrá un montón de lecturas de índice innecesarias. No es un gran problema, pero sigue siendo algo a tener en cuenta.

AnthonyVO
fuente
sql% notfound es mi preferencia personal
Arturo Hernandez
8

He estado usando el primer ejemplo de código durante años. Aviso no encontrado en lugar de contar.

UPDATE tablename SET val1 = in_val1, val2 = in_val2
    WHERE val3 = in_val3;
IF ( sql%notfound ) THEN
    INSERT INTO tablename
        VALUES (in_val1, in_val2, in_val3);
END IF;

El siguiente código es el código posiblemente nuevo y mejorado

MERGE INTO tablename USING dual ON ( val3 = in_val3 )
WHEN MATCHED THEN UPDATE SET val1 = in_val1, val2 = in_val2
WHEN NOT MATCHED THEN INSERT 
    VALUES (in_val1, in_val2, in_val3)

En el primer ejemplo, la actualización realiza una búsqueda de índice. Tiene que hacerlo para actualizar la fila derecha. Oracle abre un cursor implícito y lo usamos para ajustar una inserción correspondiente para que sepamos que la inserción solo ocurrirá cuando la clave no exista. Pero la inserción es un comando independiente y tiene que hacer una segunda búsqueda. No conozco el funcionamiento interno del comando merge, pero dado que el comando es una sola unidad, Oracle podría haber ejecutado la inserción o actualización correcta con una sola búsqueda de índice.

Creo que fusionar es mejor cuando hay que procesar algo, lo que significa tomar datos de algunas tablas y actualizar una tabla, posiblemente insertando o eliminando filas. Pero para el caso de una sola fila, puede considerar el primer caso ya que la sintaxis es más común.

Arturo Hernández
fuente
0

Ejemplo de copiar y pegar para insertar una tabla en otra, con MERGE:

CREATE GLOBAL TEMPORARY TABLE t1
    (id VARCHAR2(5) ,
     value VARCHAR2(5),
     value2 VARCHAR2(5)
     )
  ON COMMIT DELETE ROWS;

CREATE GLOBAL TEMPORARY TABLE t2
    (id VARCHAR2(5) ,
     value VARCHAR2(5),
     value2 VARCHAR2(5))
  ON COMMIT DELETE ROWS;
ALTER TABLE t2 ADD CONSTRAINT PK_LKP_MIGRATION_INFO PRIMARY KEY (id);

insert into t1 values ('a','1','1');
insert into t1 values ('b','4','5');
insert into t2 values ('b','2','2');
insert into t2 values ('c','3','3');


merge into t2
using t1
on (t1.id = t2.id) 
when matched then 
  update set t2.value = t1.value,
  t2.value2 = t1.value2
when not matched then
  insert (t2.id, t2.value, t2.value2)  
  values(t1.id, t1.value, t1.value2);

select * from t2

Resultado:

  1. b 4 5
  2. c 3 3
  3. a 1 1
Bechyňák Petr
fuente
-3

Prueba esto,

insert into b_building_property (
  select
    'AREA_IN_COMMON_USE_DOUBLE','Area in Common Use','DOUBLE', null, 9000, 9
  from dual
)
minus
(
  select * from b_building_property where id = 9
)
;
r4bitt
fuente
-6

De http://www.praetoriate.com/oracle_tips_upserts.htm :

"En Oracle9i, un UPSERT puede realizar esta tarea en una sola declaración:"

INSERT
FIRST WHEN
   credit_limit >=100000
THEN INTO
   rich_customers
VALUES(cust_id,cust_credit_limit)
   INTO customers
ELSE
   INTO customers SELECT * FROM new_customers;
Luego
fuente
14
-1 Típico Don Burleson cr @ p, me temo: este es un inserto en una mesa u otra, ¡aquí no hay un "upsert"!
Tony Andrews