Oracle: como UPSERT (atualizar ou inserir em uma tabela?)

293

A operação UPSERT atualiza ou insere uma linha em uma tabela, dependendo se a tabela já tiver uma linha que corresponda aos dados:

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

Como a Oracle não possui uma declaração UPSERT específica, qual é a melhor maneira de fazer isso?

Mark Harrison
fonte

Respostas:

60

Uma alternativa ao MERGE (o "caminho antiquado"):

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
fonte
3
@chotchki: sério? Uma explicação seria útil.
Tony Andrews
15
O problema é que você tem uma janela entre a inserção e a atualização em que outro processo pode disparar uma exclusão com êxito. No entanto, eu usei esse padrão em uma tabela que nunca exclui disparos contra ele.
chotchki
2
OK, eu concordo. Não sei por que não era óbvio para mim.
Tony Andrews
4
Eu discordo de Chotchki. "Duração do bloqueio: todos os bloqueios adquiridos por instruções em uma transação são mantidos pelo período de duração da transação, impedindo interferências destrutivas, incluindo leituras sujas, atualizações perdidas e operações DDL destrutivas de transações simultâneas." Souce: link
yohannc
5
@yohannc: Eu acho que o ponto é que não adquirimos nenhum bloqueio apenas tentando e não inserindo uma linha.
Tony Andrews
211

A instrução MERGE mescla dados entre duas tabelas. O uso do DUAL nos permite usar este comando. Observe que isso não está protegido contra acesso simultâneo.

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
fonte
57
Aparentemente, a declaração "mesclar em" não é atômica. Isso pode resultar em "ORA-0001: restrição exclusiva" quando usado simultaneamente. A verificação da existência de uma correspondência e a inserção de um novo registro não são protegidas por um bloqueio, portanto, existe uma condição de corrida. Para fazer isso de forma confiável, você precisa capturar essa exceção e executar novamente a mesclagem ou fazer uma atualização simples. No Oracle 10, você pode usar a cláusula "erros de log" para continuar com o restante das linhas quando ocorrer um erro e registrar a linha incorreta em outra tabela, em vez de apenas parar.
22413 Tim Sylvester
1
Oi, Eu tentei usar o mesmo padrão de consulta na minha consulta, mas de alguma forma minha consulta está inserindo linhas duplicadas. Não consigo encontrar mais informações sobre a tabela DUAL. Alguém pode me dizer onde posso obter informações sobre o DUAL e também sobre a sintaxe de mesclagem?
Shekhar
5
@Shekhar dupla é uma tabela fictícia com uma única linha e columnn adp-gmbh.ch/ora/misc/dual.html
YogoZuno
7
@TimSylvester - A Oracle usa transações, portanto, garante que o instantâneo dos dados no início de uma transação seja consistente durante toda a transação, exceto as alterações feitas nela. Chamadas simultâneas para o banco de dados usam a pilha de desfazer; portanto, a Oracle gerenciará o estado final com base na ordem em que as transações simultâneas foram iniciadas / concluídas. Portanto, você nunca terá uma condição de corrida se uma verificação de restrição for feita antes da inserção, independentemente de quantas chamadas simultâneas sejam feitas para o mesmo código SQL. Na pior das hipóteses, você pode ter muitas contendas e o Oracle levará muito mais tempo para atingir um estado final.
Neo
2
@RandyMagruder É o caso de 2015 e ainda não podemos fazer um upsert de maneira confiável no Oracle! Você conhece uma solução segura simultânea?
dan b
105

O exemplo duplo acima, que está em PL / SQL, foi ótimo porque eu queria fazer algo semelhante, mas queria do lado do cliente ... então, aqui está o SQL que usei para enviar uma declaração semelhante diretamente de algum 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" )

No entanto, da perspectiva do C #, isso deve ser mais lento do que fazer a atualização e ver se as linhas afetadas eram 0 e fazer a inserção, se fosse.

MyDeveloperDay
fonte
10
Voltei aqui para verificar esse padrão novamente. Ele falha silenciosamente quando tentativas de inserção simultânea. Uma inserção entra em vigor, a segunda mesclagem não insere nem atualiza. No entanto, a abordagem mais rápida de fazer duas declarações separadas é segura.
Synesso
3
novatos oralcle como me pode perguntar o que é esta dupla mesa de ver isto: stackoverflow.com/q/73751/808698
Hajo Thelen
5
Pena que, com esse padrão, precisamos escrever duas vezes os dados (John, Smith ...). Neste caso, eu não ganhar nada usando MERGE, e eu prefiro usar muito mais simples DELETEentão INSERT.
Nicolas Barbulesco
@NicolasBarbulesco esta resposta não precisa escrever os dados duas vezes: 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 11/11/19
46

Outra alternativa sem a verificação de exceção:

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
fonte
Sua solução fornecida não funciona para mim. % Rowcount funciona apenas com cursores explícitos?
Synesso
E se a atualização retornasse 0 linhas modificadas porque o registro já estava lá e os valores fossem os mesmos?
Adriano Varoli Piazza
10
@Adriano: sql% rowcount ainda retornará> 0 se a cláusula WHERE corresponder a alguma linha, mesmo que a atualização não altere realmente nenhum dado nessas linhas.
Tony Andrews
Não funciona: PLS-00207: o identificador 'COUNT', aplicado ao cursor implícito SQL, não é um atributo legal do cursor
Patrik Beck
Erros de sintaxe aqui :(
ilmirons 12/11/19
27
  1. insira se não existir
  2. atualizar:
    
INSERIR EM MEU TUTORIAL (id1, t1) 
  SELECT 11, 'x1' DE DUAL 
  ONDE NÃO EXISTE (SELECT id1 FROM mytble WHERE id1 = 11); 

ATUALIZAÇÃO mytable SET t1 = 'x1' WHERE id1 = 11;
test1
fonte
26

Nenhuma das respostas dadas até agora é segura diante de acessos simultâneos , como apontado no comentário de Tim Sylvester, e criará exceções em caso de corridas. Para corrigir isso, a combinação de inserção / atualização deve ser agrupada em algum tipo de instrução de loop, para que, em caso de exceção, a coisa toda seja repetida.

Como exemplo, veja como o código do Grommit pode ser agrupado em um loop para torná-lo seguro quando executado simultaneamente:

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; 

NB No modo de transação SERIALIZABLE, que eu não recomendo, você pode encontrar o ORA-08177: não pode serializar o acesso a essas exceções de transação .

Eugene Beresovsky
fonte
3
Excelente! Finalmente, um concorrente acessa uma resposta segura. Alguma maneira de usar essa construção de um cliente (por exemplo, de um cliente Java)?
Sebien
1
Você quer dizer que não precisa chamar um proc armazenado? Bem, nesse caso, você também pode capturar as exceções específicas do Java e tentar novamente em um loop do Java. É muito mais conveniente em Java do que o SQL da Oracle.
Eugene Beresovsky
Me desculpe: eu não era específico o suficiente. Mas você entendeu o caminho certo. Renunciei a fazer o que você acabou de dizer. Mas não estou 100% satisfeito porque gera mais consultas SQL, mais ida e volta ao cliente / servidor. Não é uma boa solução em termos de desempenho. Mas meu objetivo é permitir que os desenvolvedores Java do meu projeto usem meu método para fazer upsert em qualquer tabela (não posso criar um procedimento armazenado PLSQL por tabela ou um procedimento por tipo de upsert).
Sebien
@ Savien Eu concordo, seria melhor tê-lo encapsulado no domínio SQL, e acho que você pode fazê-lo. Só não sou voluntário para descobrir isso para você ... :) Além disso, na realidade essas exceções provavelmente ocorrerão menos de uma vez na lua azul, portanto, você não verá um impacto no desempenho em 99,9% dos casos. Exceto quando fazê-carga testar é claro ...
Eugene Beresovsky
24

Eu gostaria da resposta do Grommit, exceto que isso exige valores duvidosos. Encontrei a solução em que ela pode aparecer uma 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
fonte
2
Você quis dizer INSERT (B.CILT, B.SAYFA, B.KUTUK, B.MERNIS_NO) VALUES (E.CILT, E.SAYFA, E.KUTUK, E.MERNIS_NO); ?
Matteo
Certo. Obrigado. Fixo.
Hubbitus
Felizmente, você editou sua resposta! :) minha edição foi, infelizmente rejeitar stackoverflow.com/review/suggested-edits/7555674
Matteo
9

Uma observação sobre as duas soluções que sugerem:

1) Insira, se a exceção for atualizada,

ou

2) Atualize, se sql% rowcount = 0, insira

A questão de inserir ou atualizar primeiro também depende do aplicativo. Você espera mais inserções ou mais atualizações? Aquele com maior probabilidade de sucesso deve ir primeiro.

Se você escolher o errado, receberá várias leituras de índice desnecessárias. Não é um grande negócio, mas ainda há algo a considerar.

AnthonyVO
fonte
sql% notfound é a minha preferência pessoal
Arturo Hernandez
8

Eu tenho usado o primeiro exemplo de código há anos. Observe não encontrado e não conte.

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;

O código abaixo é o código possivelmente novo e aprimorado

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)

No primeiro exemplo, a atualização faz uma pesquisa de índice. É necessário, para atualizar a linha direita. O Oracle abre um cursor implícito e o usamos para quebrar uma inserção correspondente, para que saibamos que a inserção acontecerá apenas quando a chave não existir. Mas a inserção é um comando independente e precisa fazer uma segunda pesquisa. Não conheço o funcionamento interno do comando mesclar, mas como o comando é uma única unidade, o Oracle pode executar a inserção ou atualização correta com uma única pesquisa de índice.

Acho que a mesclagem é melhor quando você tem algum processamento a ser feito, o que significa pegar dados de algumas tabelas e atualizar uma tabela, possivelmente inserindo ou excluindo linhas. Mas para o caso de linha única, você pode considerar o primeiro caso, já que a sintaxe é mais comum.

Arturo Hernandez
fonte
0

Copie e cole um exemplo para alterar uma tabela para outra, com 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
fonte
-3

Tente isso,

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
fonte
-6

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

"No Oracle9i, um UPSERT pode realizar esta tarefa em uma única declaração:"

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;
Anon
fonte
14
-1 Don Burleson típico cr @ p Receio - esta é uma inserção em uma tabela ou outra, não há "upsert" aqui!
Tony Andrews